Fosi Fossil Service
Small teams. Open source. Built on Fossil.

IaC / SwiftFlow

High-performance WMS with wave picking, smart routing, and real-time inventory visibility.

Public | Owner: IaC | Default branch: trunk | Last update: 2026-10-23 14:37

Artifact 6bfebc313ba4586e8a307027af322610003c4bc9f981ca786f77eb72c1cbc6c8:


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
  }
}


Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/IaC/SwiftFlow

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.