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 fc472950887b851844664882de45e3786f5dc5ef698cf2a5320b6ab2ead709e9:


import clock
import database.{type Conn, type Database, type DbError, SqliteConn, TextParam}
import gleam/dynamic/decode
import gleam/list
import gleam/result
import gleam/string
import ql361api/json_codec
import rbac

const statements = [
  "CREATE TABLE IF NOT EXISTS schema_migrations (
    version TEXT PRIMARY KEY,
    applied_at TEXT NOT NULL
  )",
  "CREATE TABLE IF NOT EXISTS sync_cursors (
    entity TEXT PRIMARY KEY,
    cursor_value TEXT NOT NULL,
    updated_at TEXT NOT NULL
  )",
  "CREATE TABLE IF NOT EXISTS sync_runs (
    id TEXT PRIMARY KEY,
    entity TEXT NOT NULL,
    trigger_kind TEXT NOT NULL,
    started_at TEXT NOT NULL,
    finished_at TEXT NOT NULL,
    status TEXT NOT NULL,
    fetched_count INTEGER NOT NULL,
    upserted_count INTEGER NOT NULL,
    error_message TEXT NOT NULL
  )",
  "CREATE TABLE IF NOT EXISTS warehouses (
    ql_id BIGINT PRIMARY KEY,
    code TEXT NOT NULL,
    name TEXT NOT NULL,
    easycode TEXT NOT NULL,
    operational INTEGER NOT NULL DEFAULT 0,
    synced_at TEXT NOT NULL
  )",
  "CREATE TABLE IF NOT EXISTS products (
    ql_id BIGINT PRIMARY KEY,
    code TEXT NOT NULL,
    name TEXT NOT NULL,
    barcode TEXT NOT NULL,
    spec TEXT NOT NULL,
    unit TEXT NOT NULL,
    shelf_life TEXT NOT NULL,
    shelf_life_days INTEGER NOT NULL,
    position_code TEXT NOT NULL,
    disabled INTEGER NOT NULL,
    is_category INTEGER NOT NULL,
    source_modified_at TEXT NOT NULL,
    source_created_at TEXT NOT NULL,
    synced_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_products_code ON products (code)",
  "CREATE INDEX IF NOT EXISTS idx_products_barcode ON products (barcode)",
  "CREATE TABLE IF NOT EXISTS outbound_orders (
    bill_id BIGINT PRIMARY KEY,
    bill_code TEXT NOT NULL,
    customer_name TEXT NOT NULL,
    contact_person TEXT NOT NULL,
    phone TEXT NOT NULL,
    warehouse_name TEXT NOT NULL,
    warehouse_id BIGINT NOT NULL,
    bill_date TEXT NOT NULL,
    status INTEGER NOT NULL,
    status_name TEXT NOT NULL,
    total_qty REAL NOT NULL,
    total_money REAL NOT NULL,
    remark TEXT NOT NULL,
    detail_synced_at TEXT NOT NULL,
    synced_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_outbound_orders_status ON outbound_orders (status)",
  "CREATE TABLE IF NOT EXISTS outbound_order_items (
    bill_id BIGINT NOT NULL,
    line_no INTEGER NOT NULL,
    bill_detail_id BIGINT NOT NULL,
    sku_id BIGINT NOT NULL,
    goods_code TEXT NOT NULL,
    goods_name TEXT NOT NULL,
    barcode TEXT NOT NULL,
    spec TEXT NOT NULL,
    package_qty REAL NOT NULL,
    package_unit TEXT NOT NULL,
    base_qty REAL NOT NULL,
    base_unit TEXT NOT NULL,
    price REAL NOT NULL,
    total_money REAL NOT NULL,
    position_code TEXT NOT NULL,
    area_code TEXT NOT NULL,
    PRIMARY KEY (bill_id, line_no)
  )",
  "CREATE INDEX IF NOT EXISTS idx_outbound_order_items_bill ON outbound_order_items (bill_id)",
  "CREATE TABLE IF NOT EXISTS inbound_orders (
    bill_id BIGINT PRIMARY KEY,
    bill_code TEXT NOT NULL,
    supplier_name TEXT NOT NULL,
    contact_person TEXT NOT NULL,
    phone TEXT NOT NULL,
    warehouse_name TEXT NOT NULL,
    warehouse_id BIGINT NOT NULL,
    bill_date TEXT NOT NULL,
    status INTEGER NOT NULL,
    status_name TEXT NOT NULL,
    total_qty REAL NOT NULL,
    total_money REAL NOT NULL,
    remark TEXT NOT NULL,
    detail_synced_at TEXT NOT NULL,
    synced_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_inbound_orders_status ON inbound_orders (status)",
  "CREATE TABLE IF NOT EXISTS inbound_order_items (
    bill_id BIGINT NOT NULL,
    line_no INTEGER NOT NULL,
    bill_detail_id BIGINT NOT NULL,
    sku_id BIGINT NOT NULL,
    goods_code TEXT NOT NULL,
    goods_name TEXT NOT NULL,
    barcode TEXT NOT NULL,
    spec TEXT NOT NULL,
    package_qty REAL NOT NULL,
    package_unit TEXT NOT NULL,
    base_qty REAL NOT NULL,
    base_unit TEXT NOT NULL,
    price REAL NOT NULL,
    total_money REAL NOT NULL,
    position_code TEXT NOT NULL,
    area_code TEXT NOT NULL,
    PRIMARY KEY (bill_id, line_no)
  )",
  "CREATE INDEX IF NOT EXISTS idx_inbound_order_items_bill ON inbound_order_items (bill_id)",
  "CREATE TABLE IF NOT EXISTS locations (
    warehouse_id BIGINT NOT NULL,
    area TEXT NOT NULL,
    aisle TEXT NOT NULL,
    level_code TEXT NOT NULL,
    bin_code TEXT NOT NULL,
    code TEXT NOT NULL,
    barcode TEXT NOT NULL,
    enabled INTEGER NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    PRIMARY KEY (warehouse_id, code)
  )",
  "CREATE UNIQUE INDEX IF NOT EXISTS idx_locations_barcode ON locations (barcode)",
  "CREATE TABLE IF NOT EXISTS inbound_receipts (
    id TEXT PRIMARY KEY,
    bill_id BIGINT NOT NULL,
    warehouse_id BIGINT NOT NULL,
    created_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_inbound_receipts_bill ON inbound_receipts (bill_id)",
  "CREATE TABLE IF NOT EXISTS inbound_receipt_lines (
    receipt_id TEXT NOT NULL,
    line_no INTEGER NOT NULL,
    bill_detail_id BIGINT NOT NULL,
    sku_id BIGINT NOT NULL,
    goods_code TEXT NOT NULL,
    goods_name TEXT NOT NULL,
    barcode TEXT NOT NULL,
    received_qty REAL NOT NULL,
    batch_no TEXT NOT NULL,
    produced_on TEXT NOT NULL,
    expires_on TEXT NOT NULL,
    location_code TEXT NOT NULL,
    PRIMARY KEY (receipt_id, line_no)
  )",
  "CREATE INDEX IF NOT EXISTS idx_inbound_receipt_lines_bill ON inbound_receipt_lines (bill_detail_id)",
  "CREATE TABLE IF NOT EXISTS inventory_batches (
    warehouse_id BIGINT NOT NULL,
    location_code TEXT NOT NULL,
    sku_id BIGINT NOT NULL,
    goods_code TEXT NOT NULL,
    batch_no TEXT NOT NULL,
    produced_on TEXT NOT NULL,
    expires_on TEXT NOT NULL,
    qty REAL NOT NULL,
    updated_at TEXT NOT NULL,
    PRIMARY KEY (
      warehouse_id, location_code, sku_id, batch_no, produced_on, expires_on
    )
  )",
  "CREATE TABLE IF NOT EXISTS outbound_shipments (
    id TEXT PRIMARY KEY,
    bill_id BIGINT NOT NULL,
    warehouse_id BIGINT NOT NULL,
    created_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_outbound_shipments_bill ON outbound_shipments (bill_id)",
  "CREATE TABLE IF NOT EXISTS outbound_shipment_lines (
    shipment_id TEXT NOT NULL,
    line_no INTEGER NOT NULL,
    bill_detail_id BIGINT NOT NULL,
    sku_id BIGINT NOT NULL,
    goods_code TEXT NOT NULL,
    goods_name TEXT NOT NULL,
    batch_no TEXT NOT NULL,
    produced_on TEXT NOT NULL,
    expires_on TEXT NOT NULL,
    location_code TEXT NOT NULL,
    qty REAL NOT NULL,
    PRIMARY KEY (shipment_id, line_no)
  )",
  "CREATE INDEX IF NOT EXISTS idx_outbound_shipment_lines_detail ON outbound_shipment_lines (bill_detail_id)",
  "CREATE TABLE IF NOT EXISTS inbound_batch_codes (
    bill_id BIGINT NOT NULL,
    bill_detail_id BIGINT NOT NULL,
    batch_code TEXT NOT NULL,
    created_at TEXT NOT NULL,
    PRIMARY KEY (bill_id, bill_detail_id)
  )",
  "CREATE INDEX IF NOT EXISTS idx_inbound_batch_codes_code ON inbound_batch_codes (batch_code)",
  "CREATE TABLE IF NOT EXISTS putaways (
    id TEXT PRIMARY KEY,
    batch_code TEXT NOT NULL,
    bill_id BIGINT NOT NULL,
    bill_detail_id BIGINT NOT NULL,
    warehouse_id BIGINT NOT NULL,
    sku_id BIGINT NOT NULL,
    goods_code TEXT NOT NULL,
    goods_name TEXT NOT NULL,
    location_code TEXT NOT NULL,
    produced_on TEXT NOT NULL,
    expires_on TEXT NOT NULL,
    qty REAL NOT NULL,
    created_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_putaways_batch ON putaways (batch_code)",
  "CREATE TABLE IF NOT EXISTS users (
    id TEXT PRIMARY KEY,
    username TEXT NOT NULL UNIQUE,
    display_name TEXT NOT NULL,
    password_hash TEXT NOT NULL,
    enabled INTEGER NOT NULL,
    failed_attempts INTEGER NOT NULL,
    locked_until TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
  )",
  "CREATE TABLE IF NOT EXISTS roles (
    code TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    terminals TEXT NOT NULL,
    builtin INTEGER NOT NULL
  )",
  "CREATE TABLE IF NOT EXISTS permissions (
    code TEXT PRIMARY KEY,
    name TEXT NOT NULL
  )",
  "CREATE TABLE IF NOT EXISTS role_permissions (
    role_code TEXT NOT NULL,
    permission_code TEXT NOT NULL,
    PRIMARY KEY (role_code, permission_code)
  )",
  "CREATE TABLE IF NOT EXISTS user_roles (
    user_id TEXT NOT NULL,
    role_code TEXT NOT NULL,
    PRIMARY KEY (user_id, role_code)
  )",
  "CREATE TABLE IF NOT EXISTS user_warehouses (
    user_id TEXT NOT NULL,
    warehouse_id BIGINT NOT NULL,
    PRIMARY KEY (user_id, warehouse_id)
  )",
  "CREATE TABLE IF NOT EXISTS user_sessions (
    token_hash TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    terminal TEXT NOT NULL,
    warehouse_id BIGINT NOT NULL,
    created_at TEXT NOT NULL,
    expires_at TEXT NOT NULL,
    last_seen_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_user_sessions_user ON user_sessions (user_id)",
  "CREATE TABLE IF NOT EXISTS audit_logs (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    username TEXT NOT NULL,
    action TEXT NOT NULL,
    target TEXT NOT NULL,
    warehouse_id BIGINT NOT NULL,
    detail TEXT NOT NULL,
    created_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs (created_at)",
  "CREATE TABLE IF NOT EXISTS waves (
    id TEXT PRIMARY KEY,
    warehouse_id BIGINT NOT NULL,
    status TEXT NOT NULL,
    created_by TEXT NOT NULL,
    created_at TEXT NOT NULL,
    cancelled_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_waves_warehouse ON waves (warehouse_id, status, created_at)",
  "CREATE TABLE IF NOT EXISTS wave_orders (
    wave_id TEXT NOT NULL,
    bill_id BIGINT NOT NULL,
    bill_code TEXT NOT NULL,
    customer_name TEXT NOT NULL,
    PRIMARY KEY (wave_id, bill_id)
  )",
  "CREATE INDEX IF NOT EXISTS idx_wave_orders_bill ON wave_orders (bill_id)",
  "CREATE TABLE IF NOT EXISTS wave_picks (
    wave_id TEXT NOT NULL,
    line_no INTEGER NOT NULL,
    sku_id BIGINT NOT NULL,
    goods_code TEXT NOT NULL,
    goods_name TEXT NOT NULL,
    barcode TEXT NOT NULL,
    spec TEXT NOT NULL,
    base_unit TEXT NOT NULL,
    location_code TEXT NOT NULL,
    batch_no TEXT NOT NULL,
    produced_on TEXT NOT NULL,
    expires_on TEXT NOT NULL,
    qty REAL NOT NULL,
    short_qty REAL NOT NULL,
    PRIMARY KEY (wave_id, line_no)
  )",
  "CREATE TABLE IF NOT EXISTS wave_allocations (
    wave_id TEXT NOT NULL,
    line_no INTEGER NOT NULL,
    bill_id BIGINT NOT NULL,
    bill_code TEXT NOT NULL,
    bill_detail_id BIGINT NOT NULL,
    qty REAL NOT NULL,
    PRIMARY KEY (wave_id, line_no, bill_id, bill_detail_id)
  )",
  "CREATE TABLE IF NOT EXISTS stock_moves (
    id TEXT PRIMARY KEY,
    warehouse_id BIGINT NOT NULL,
    from_location TEXT NOT NULL,
    to_location TEXT NOT NULL,
    sku_id BIGINT NOT NULL,
    goods_code TEXT NOT NULL,
    batch_no TEXT NOT NULL,
    produced_on TEXT NOT NULL,
    expires_on TEXT NOT NULL,
    qty REAL NOT NULL,
    created_by TEXT NOT NULL,
    created_at TEXT NOT NULL
  )",
  "CREATE INDEX IF NOT EXISTS idx_stock_moves_warehouse ON stock_moves (warehouse_id, created_at)",
]

pub fn migrate(db: Database) -> Result(Nil, DbError) {
  database.transaction(db, fn(conn) {
    use _ <- result.try(
      list.try_fold(statements, Nil, fn(_, statement) {
        database.exec_on(conn, statement, [])
      }),
    )
    use _ <- result.try(ensure_operational_column(conn))
    use _ <- result.try(ensure_bill_type_column(conn))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('001_init', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('002_inbound_orders', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('003_locations', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('004_receipts', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('005_shipments', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('006_purchase_receipts', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('007_putaways', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    use _ <- result.try(sync_builtin_roles(conn))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('008_rbac', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('009_waves', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    ))
    database.exec_on(
      conn,
      "INSERT INTO schema_migrations (version, applied_at)
       VALUES ('010_moves', ?)
       ON CONFLICT (version) DO NOTHING",
      [TextParam(clock.now_local())],
    )
  })
}

/// 预设角色和权限点以代码为准,每次启动覆盖一次。
fn sync_builtin_roles(conn: Conn) -> Result(Nil, DbError) {
  use _ <- result.try(
    list.try_fold(rbac.permissions(), Nil, fn(_, permission) {
      database.exec_on(
        conn,
        "INSERT INTO permissions (code, name) VALUES (?, ?)
         ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name",
        [TextParam(permission.code), TextParam(permission.name)],
      )
    }),
  )
  list.try_fold(rbac.roles(), Nil, fn(_, role) {
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO roles (code, name, terminals, builtin) VALUES (?, ?, ?, 1)
       ON CONFLICT (code) DO UPDATE SET
         name = EXCLUDED.name,
         terminals = EXCLUDED.terminals,
         builtin = 1",
      [
        TextParam(role.code),
        TextParam(role.name),
        TextParam(string.join(role.terminals, ",")),
      ],
    ))
    use _ <- result.try(database.exec_on(
      conn,
      "DELETE FROM role_permissions WHERE role_code = ?",
      [TextParam(role.code)],
    ))
    list.try_fold(role.permissions, Nil, fn(_, code) {
      database.exec_on(
        conn,
        "INSERT INTO role_permissions (role_code, permission_code) VALUES (?, ?)",
        [TextParam(role.code), TextParam(code)],
      )
    })
  })
}

fn ensure_operational_column(conn: Conn) -> Result(Nil, DbError) {
  let probe = case conn {
    SqliteConn(_) ->
      "SELECT COUNT(*) FROM pragma_table_info('warehouses') WHERE name = 'operational'"
    _ ->
      "SELECT COUNT(*) FROM information_schema.columns
       WHERE table_name = 'warehouses' AND column_name = 'operational'"
  }
  use rows <- result.try(database.query_on(conn, probe, [], {
    use count <- decode.field(0, json_codec.int())
    decode.success(count)
  }))
  case rows {
    [0] | [] ->
      database.exec_on(
        conn,
        "ALTER TABLE warehouses ADD COLUMN operational INTEGER NOT NULL DEFAULT 0",
        [],
      )
    _ -> Ok(Nil)
  }
}

fn ensure_bill_type_column(conn: Conn) -> Result(Nil, DbError) {
  let probe = case conn {
    SqliteConn(_) ->
      "SELECT COUNT(*) FROM pragma_table_info('inbound_orders') WHERE name = 'bill_type'"
    _ ->
      "SELECT COUNT(*) FROM information_schema.columns
       WHERE table_name = 'inbound_orders' AND column_name = 'bill_type'"
  }
  use rows <- result.try(database.query_on(conn, probe, [], {
    use count <- decode.field(0, json_codec.int())
    decode.success(count)
  }))
  case rows {
    [0] | [] ->
      database.exec_on(
        conn,
        "ALTER TABLE inbound_orders ADD COLUMN bill_type INTEGER NOT NULL DEFAULT 504",
        [],
      )
    _ -> Ok(Nil)
  }
}

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.