Artifact 6bfebc313ba4586e8a307027af322610003c4bc9f981ca786f77eb72c1cbc6c8:
- File
backend/src/config.gleam
— part of check-in
[1821522f51]
at
2026-10-23 05:51:23
on branch trunk
— RBAC 权限与多仓数据隔离
- 新增用户、角色、权限点、用户角色、用户授权仓库、登录会话、审计日志表(008_rbac) - 预设五个角色(超级管理员、仓库主管、制单/调度员、打包复核员、库内作业员),按角色限制登录终端 - 账号密码登录,PBKDF2-SHA256 加盐存储,令牌只存哈希,会话按配置过期,连续输错锁定 - 首个超级管理员从 SWIFTFLOW_ADMIN_USERNAME / SWIFTFLOW_ADMIN_PASSWORD 创建,仅在用户表为空时生效 - 除健康检查与登录外所有接口需登录并校验权限点 - 订单、采购单、库位、库存、批次码按授权仓库过滤;收货、上架、出库只能在当前作业仓库进行 - 用户管理、角色查询、审计日志接口;同步、库位、上架、收货、出库写操作记录审计 (user: pluto size: 4774)
import clock import envoy import gleam/int import gleam/list import gleam/result import gleam/string import warehouse_scope import ql361api/client as ql_client import simplifile pub type DbDriver { Sqlite Postgres } pub type Config { Config( ql361_username: String, ql361_password: String, ql361_base_url: String, order_poll_ms: Int, request_gap_ms: Int, order_begin_date: String, order_end_offset_days: Int, db_driver: DbDriver, database_path: String, database_url: String, http_port: Int, stock_warehouses: List(String), ) } pub type AuthConfig { AuthConfig( session_hours: Int, max_failed_logins: Int, lockout_minutes: Int, admin_username: String, admin_password: String, ) } pub fn load_auth() -> AuthConfig { AuthConfig( session_hours: clamp(env_int("SWIFTFLOW_SESSION_HOURS", 12), 1, 720), max_failed_logins: clamp(env_int("SWIFTFLOW_MAX_FAILED_LOGINS", 5), 3, 20), lockout_minutes: clamp(env_int("SWIFTFLOW_LOCKOUT_MINUTES", 15), 1, 1440), admin_username: env_string("SWIFTFLOW_ADMIN_USERNAME", ""), admin_password: env_string("SWIFTFLOW_ADMIN_PASSWORD", ""), ) } fn clamp(value: Int, low: Int, high: Int) -> Int { int.min(int.max(value, low), high) } pub fn load_dotenv() -> Nil { load_file(".env") load_file("backend/.env") } pub fn load() -> Result(Config, String) { use driver <- result.try(parse_driver()) let poll_seconds = clamp_poll_seconds(env_int("QL361_ORDER_POLL_SECONDS", 300)) let request_gap_ms = clamp_request_gap_ms(env_int("QL361_REQUEST_GAP_MS", default_request_gap_ms)) Ok(Config( ql361_username: env_string("QL361_USERNAME", ""), ql361_password: env_string("QL361_PASSWORD", ""), ql361_base_url: env_string("QL361_BASE_URL", ql_client.default_base_url), order_poll_ms: poll_seconds * 1000, request_gap_ms: request_gap_ms, order_begin_date: env_string("QL361_ORDER_BEGIN_DATE", "2024-01-01"), order_end_offset_days: 1, db_driver: driver, database_path: env_string("DATABASE_PATH", "data/swiftflow.sqlite"), database_url: env_string("DATABASE_URL", ""), http_port: env_int("SWIFTFLOW_PORT", 8080), stock_warehouses: stock_warehouse_tokens(), )) } fn stock_warehouse_tokens() -> List(String) { case warehouse_scope.parse_tokens(env_string("SWIFTFLOW_STOCK_WAREHOUSES", "总仓")) { [] -> ["总仓"] tokens -> tokens } } pub const min_order_poll_seconds = 300 pub const default_request_gap_ms = 2000 pub const min_request_gap_ms = 1000 pub fn clamp_poll_seconds(value: Int) -> Int { case value < min_order_poll_seconds { True -> min_order_poll_seconds False -> value } } pub fn clamp_request_gap_ms(value: Int) -> Int { case value < min_request_gap_ms { True -> min_request_gap_ms False -> value } } pub fn order_window(config: Config) -> #(String, String) { #(config.order_begin_date, clock.shift_days_local(config.order_end_offset_days)) } fn parse_driver() -> Result(DbDriver, String) { case string.lowercase(env_string("DATABASE_DRIVER", "sqlite")) { "sqlite" -> Ok(Sqlite) "postgres" | "postgresql" -> Ok(Postgres) other -> Error("DATABASE_DRIVER 只能是 sqlite 或 postgres,当前为 " <> other) } } fn env_string(name: String, default: String) -> String { case envoy.get(name) { Ok(value) -> string.trim(value) Error(_) -> default } } fn env_int(name: String, default: Int) -> Int { case envoy.get(name) { Ok(value) -> case int.parse(string.trim(value)) { Ok(parsed) -> parsed Error(_) -> default } Error(_) -> default } } fn load_file(path: String) -> Nil { case simplifile.read(path) { Error(_) -> Nil Ok(contents) -> string.split(contents, "\n") |> list.each(apply_env_line) } } fn apply_env_line(line: String) -> Nil { let line = string.trim(line) case line { "" -> Nil "#" <> _ -> Nil _ -> { case string.split_once(line, "=") { Error(_) -> Nil Ok(#(key, value)) -> { let key = string.trim(key) let value = unquote(string.trim(value)) case key { "" -> Nil _ -> case envoy.get(key) { Ok(_) -> Nil Error(_) -> envoy.set(key, value) } } } } } } } fn unquote(value: String) -> String { case value { "\"" <> rest -> case string.ends_with(rest, "\"") { True -> string.slice(rest, 0, string.length(rest) - 1) False -> value } "'" <> rest -> case string.ends_with(rest, "'") { True -> string.slice(rest, 0, string.length(rest) - 1) False -> value } _ -> value } }