Artifact 9767c415eb59ac7a028323954d4e5d52a5482f6d0d8a32ed6bb75bea7e1d2e4d:
- File
backend/src/location_plan.gleam
— part of check-in
[3d773b2783]
at
2026-10-23 05:37:06
on branch trunk
— 在系统里按范围规划库位,先预览再建立。
按库区、货架、层、位的起止编号批量生成库位和条码,已有的库位跳过,一次最多 5000 个。只作显示的仓库不能规划。 (user: pluto size: 5560)
import clock import database.{type Conn, type DbError, type Database} import gleam/int import gleam/list import gleam/option.{None, Some} import gleam/result import gleam/string import location.{type Location, type LocationCode, Location} import repo pub const max_locations = 5000 pub type Outcome { Outcome(created: List(Location), existing: List(String)) } pub type PlanError { PlanRejected(String) PlanMissingWarehouse PlanDb(DbError) } /// commit 为 False 时只预览,不写库。 pub fn run(db: Database, plan: Plan, commit: Bool) -> Result(Outcome, PlanError) { case expand(plan) { Error(message) -> Error(PlanRejected(message)) Ok(codes) -> { let now = clock.now_local() case database.transaction(db, fn(conn) { apply(conn, plan.warehouse_id, codes, commit, now) }) { Error(err) -> Error(PlanDb(err)) Ok(Error(err)) -> Error(err) Ok(Ok(outcome)) -> Ok(outcome) } } } } fn apply( conn: Conn, warehouse_id: Int, codes: List(LocationCode), commit: Bool, now: String, ) -> Result(Result(Outcome, PlanError), DbError) { use warehouse <- result.try(repo.get_warehouse(conn, warehouse_id)) case warehouse { None -> Ok(Error(PlanMissingWarehouse)) Some(row) -> case row.operational { False -> Ok(Error(PlanRejected("该仓库只作显示,不能建立库存库位"))) True -> { use pairs <- result.try(list.try_map(codes, fn(code) { use found <- result.try(repo.find_location(conn, warehouse_id, code.code)) Ok(#(code, found)) })) let existing = list.filter_map(pairs, fn(pair) { case pair.1 { Some(_) -> Ok({ pair.0 }.code) None -> Error(Nil) } }) let created = list.filter_map(pairs, fn(pair) { case pair.1 { Some(_) -> Error(Nil) None -> { let code = pair.0 Ok(Location( warehouse_id: warehouse_id, area: code.area, aisle: code.aisle, level: code.level, bin: code.bin, code: code.code, barcode: location.barcode(row.warehouse.code, code.compact), enabled: True, created_at: now, updated_at: now, )) } } }) use _ <- result.try(case commit { False -> Ok(Nil) True -> list.try_fold(created, Nil, fn(_, item) { repo.insert_location(conn, item) }) }) Ok(Ok(Outcome(created: created, existing: existing))) } } } } pub type Range { Range(from: Int, to: Int, width: Int) } pub type Plan { Plan( warehouse_id: Int, areas: List(String), aisle: Range, level: Range, bin: Range, ) } pub fn expand(plan: Plan) -> Result(List(LocationCode), String) { use aisles <- result_try(numbers("货架", plan.aisle)) use levels <- result_try(numbers("层", plan.level)) use bins <- result_try(numbers("位", plan.bin)) let areas = plan.areas |> list.map(fn(area) { string.uppercase(string.trim(area)) }) |> list.unique case areas { [] -> Error("至少要有一个库区") _ -> { let count = list.length(areas) * list.length(aisles) * list.length(levels) * list.length(bins) case count > max_locations { True -> Error( "一次最多规划 " <> int.to_string(max_locations) <> " 个库位,本次是 " <> int.to_string(count) <> " 个", ) False -> list.try_map(combinations(areas, aisles, levels, bins), fn(parts) { let #(area, aisle, level, bin) = parts location.build(area, aisle, level, bin) }) } } } } fn combinations( areas: List(String), aisles: List(String), levels: List(String), bins: List(String), ) -> List(#(String, String, String, String)) { use area <- list.flat_map(areas) use aisle <- list.flat_map(aisles) use level <- list.flat_map(levels) use bin <- list.map(bins) #(area, aisle, level, bin) } fn numbers(label: String, range: Range) -> Result(List(String), String) { case range.width < 1 || range.width > 8 { True -> Error(label <> "编号位数应为 1 到 8") False -> case range.from < 0 || range.to < range.from || range.to - range.from >= max_locations { True -> Error(label <> "的起止编号无效") False -> { let values = int.range(from: range.to, to: range.from - 1, with: [], run: fn(acc, value) { [value, ..acc] }) |> list.map(fn(value) { pad(int.to_string(value), range.width) }) case list.any(values, fn(value) { string.length(value) > range.width }) { True -> Error(label <> "编号超过了设定的位数") False -> Ok(values) } } } } } fn pad(value: String, width: Int) -> String { case string.length(value) >= width { True -> value False -> pad("0" <> value, width) } } fn result_try(result: Result(a, b), next: fn(a) -> Result(c, b)) -> Result(c, b) { case result { Ok(value) -> next(value) Error(err) -> Error(err) } }