岩美酒库 · 开发手册
backend/ client/ deploy/ scripts/)1. 系统架构总览
五端 Flutter 客户端统一访问 https://jiu.51yanmei.com。阿里云主机上 nginx 1.24 终结 TLS(443 ssl+http2,certbot webroot 自动续期,80 → 443 跳转),按 location 分发:API/健康检查/版本反代到本机 8081 的 Go 后端(systemd jiu.service),静态资源(Flutter Web /app、商品图 /images、安装包 /downloads、营销站根路径)直接由 nginx 服务;/product/:id 与 /app/product/:id(扫码分享页)回代后端注入 OG 标签。后端连本机 Docker 容器 jiu_mysql(MySQL 8,127.0.0.1:3306)。未鉴权的 /api/v1/(public|auth)/ 在 nginx 层再加一道 per-IP 限流(10r/s burst 20)。源:deploy/nginx-jiu-ali.conf、deploy/jiu.service。
payd 占用,后端端口固定 8081;本地开发默认仍是 8080。改 nginx 时只动 jiu 的 server block(/etc/nginx/conf.d/jiu.conf),别碰同机其他站点。2. 后端(Go / Gin)
2.1 目录分层
| 目录 / 文件 | 职责 |
|---|---|
| main.go | 入口:配置加载、生产前置检查(CORS/JWT 密钥)、连库、AutoMigrate、启动回填、SetTrustedProxies、挂路由 |
| cmd/ | 一次性/运维工具(seed、gencode、import-history、fix-inventory-products、backfill-pinyin,详见 §6.4) |
| config/ | Viper 配置结构体 + config.yaml(本地,不入 git)+ version.yaml(发版清单,/version 实时读) |
| internal/handler/ | HTTP 处理器,每业务模块一个文件;参数校验 + 组织响应,*_test.go 为 SQLite in-memory 集成测试 |
| internal/service/ | 业务逻辑层:auth(登录/失败锁定)、license(兑换券)、stock(审核/库存事务)、migrate(启动回填)、session_cleanup |
| internal/model/ | GORM 模型;base.go 提供 Base/TenantBase/Date/JSON 公共类型 |
| internal/middleware/ | JWT 鉴权 + 会话校验、角色拦截(ReadOnly/AdminOnly/SuperAdminOnly)、LicenseGuard、限流 |
| internal/util/ | 通用工具(pinyin.go 拼音搜索列生成等) |
| internal/router/router.go | 全部路由注册与中间件编排(§5 的唯一真相源) |
| schema/schema.sql | 完整建表 DDL(新装用;线上结构演进靠 AutoMigrate) |
| seeds/ | 门店种子数据 SQL(S001 全量测试数据、S002 空门店) |
| testutil/setup.go | 测试工具:SQLite in-memory DB、CreateTestXxx 工厂、GetAuthToken |
2.2 JWT 与多租户隔离
源:backend/internal/middleware/auth.go。JWT(HS256,Access 60 分钟 / Refresh 7 天)Claims 携带 user_id / shop_id / role / sid / lic_exp。带 sid 的 token 每请求校验 user_sessions(撤销/禁用即时下线,last_seen_at 30s 节流刷新)。
shop_id 永远用 middleware.GetShopID(c) 从 JWT 取,绝不从请求参数/URL/请求体读;所有查询必须带 WHERE shop_id = ?。违反即多租户越权。2.3 角色中间件
| 中间件 | 挂载位置 | 行为 |
|---|---|---|
| ReadOnly() | 业务路由组全局 + /license | role=readonly 的非 GET 请求 → 403(code: READONLY_USER) |
| AdminOnly() | /users 组、/shop 写、/sessions/:id 删 | 仅 admin / superadmin 放行 |
| SuperAdminOnly() | /admin 组 | 仅 superadmin 放行(清数据/对账/错误报告/反馈后台) |
角色顺序:superadmin > admin > operator > readonly。新增写路由默认已受 ReadOnly 保护;仅管理员可用的要再包 AdminOnly 子组。
2.4 LicenseGuard(授权过期降级)
源:middleware/license_guard.go。按当前 DB 的有效授权实时算 phase(每店 30s 缓存,激活/续费调用 InvalidateLicensePhase 即时生效),不信任登录时 token 快照:
| phase | 条件 | 效果 |
|---|---|---|
| normal | 未过期 / 永久授权 | 正常读写 |
| grace | 过期 0–7 天 | 可写,前端展示提醒横幅 |
| readonly | 过期 7–15 天 | 非 GET → 403(phase: readonly) |
| locked | 过期 15+ 天,或授权全部被停用(吊销) | 所有业务请求 403(phase: locked) |
/license/* 与 登出/心跳/在线会话 挂在 LicenseGuard 之外——锁定时仍能查看授权状态并激活新码。
2.5 限流
源:middleware/ratelimit.go(进程内令牌桶 + janitor 清理,config.C.RateLimit.Enabled=false 可整体关闭)。两个维度:
- per-IP(未鉴权接口逐条挂):login / refresh / register / errors / 公开商品读 / 店铺商品列表各自独立配额,IP 取自 nginx 写入的
X-Real-IP(main.go 只信任 127.0.0.1 代理,不可伪造)。 - per-Shop(挂在 JWT 之后,作用于全部已鉴权路由):按
shop_id限 RPS+burst,防单店打爆共享后端。
超限返回 429 + Retry-After: 60。nginx 层对 /api/v1/(public|auth)/ 还有最外层 10r/s 兜底(§1)。
2.6 单据审核流状态机
入库单 / 出库单同一状态机(service/stock.go + handler/stock_in.go、stock_out.go)。审核通过时同一事务内:更新 inventories(FOR UPDATE 锁行)+ 写 inventory_logs + 生成财务应收/应付;出库先校验库存充足,不足回滚。
2.7 AutoMigrate 与启动回填
- AutoMigrate(main.go):启动时对 31 个 model 只增不删地同步表结构;另幂等显式建
uk_shop_code(products)唯一索引与idx_so_items_shop_code(stock_out_items)(嵌入字段无法用 struct tag 表达)。 - BackfillPricingColumns(
service/migrate.go):2026-07 定价字段消歧的一次性迁移,旧列(unit_price/total_price/total_amount)值拷入新列(cost_price/cost_amount/cost_total/sale_total/sale_amount/profit_total),幂等(新列为 0 才拷),旧列观察一版后手动 DROP;全新安装(无旧列)整体跳过。 - backfillPartnerPinyin(main.go):为
name_pinyin为空的存量往来单位补拼音搜索列;products 同机制由 handler 写入时生成 + 启动兜底。
2.8 拼音搜索
源:internal/util/pinyin.go(go-pinyin)。ToPinyin(name) 返回全拼 + 首字母,Create/Update 时自动写入 name_pinyin / name_initials 两列;搜索 SQL 同时 LIKE 匹配 name、code、name_pinyin、name_initials(汉字/全拼/首字母三种输入都命中)。新增需要拼音搜索的实体直接复用 ToPinyin()。
2.9 客户端异常上报
客户端 POST /api/v1/public/errors(per-IP 限流)→ error_reports 表;超管在 GET /api/v1/admin/errors 查看。前端两处统一捕获(main.dart 的 runZonedGuarded + Dio 拦截器上报 5xx),业务代码只在技术性异常处手动 reportError(e, st),已知业务错误(AppException)不上报。
3. 前端(Flutter)
3.1 目录分层
| 目录 | 职责 |
|---|---|
| lib/core/ | 横切基建:api(Dio 封装,401 自动刷新)、auth(AuthNotifier + 持久化)、config(AppConfig,BASE_URL 注入)、router(go_router 登录重定向)、theme(token 三主题)、responsive、update(应用内更新)、errors(reportError) |
| lib/models/ | 与后端 JSON 对应的数据模型 |
| lib/providers/ | Riverpod 状态管理,每业务模块一个 provider(列表缓存/刷新/心跳等) |
| lib/repositories/ | 数据访问层:封装 API 调用,供 provider 消费 |
| lib/screens/ | 页面(auth/shell/stock_in/stock_out/inventory/partners/finance/products/devices/settings/about/public/shared) |
| lib/widgets/ | 共享组件;widgets/ds/ 为设计系统组件库(唯一样式来源,见 3.3) |
3.2 数据流
单向:Screen 只组合 ds 组件并 watch provider;provider 调 repository;repository 走 Dio(ApiClient 统一注入 token、401 自动刷新、5xx 自动上报)。
3.3 ds 设计系统(单一真相源)
UI 的真相源是原型 design/prototype/(tokens + atoms.css);Flutter 侧一对一镜像,屏只组合 ds 组件、不自己堆样式。令牌层由 lib/core/theme/token_source/tokens.css codegen 生成 app_tokens.g.dart / app_dims.g.dart / app_chrome.g.dart——改样式改源后重跑脚本,禁止手改 .g 文件、禁止硬编码色值。
| widgets/ds/ 文件 | 镜像对象 |
|---|---|
| ds_atoms.dart | atoms.css 原子组件全集(按钮/输入/徽章/卡片…),尺寸引 AppDims、颜色引 context.tokens |
| ds_table.dart | 原型 .table + .toolbar + .pager(toolbar 与表连成一卡,pager 卡外透明) |
| ds_kpi.dart | 原型 .kpi 汇总卡(图标块 .ic 五种 tone) |
| ds_menu.dart | 原型 .menu 下拉(定位规则照抄 shell.js openMenu) |
| ds_toast.dart | 原型 .toast 1:1(单例顶替、2.2s 自动消失) |
| ds_bar_chart.dart | 原型 finance.html 收支趋势分组柱状图(度量逐像素照抄) |
| grid_combo_cell.dart | 表格内联可搜索下拉单元格(入库/出库编辑网格用) |
闸机 1 — 代码端:client/tool/check_ds_code.mjs 扫描 lib/ 禁 Color(0x..) 与 Colors.x 硬编码(合理特例需 // ds-ignore: 理由),--changed --strict 供 pre-commit。
3.4 golden × 3 主题 + fidelity 像素闸
- golden:
client/test/golden/*_golden_test.dart对每屏 × 三主题(a/b/c)出基准图test/golden/goldens/<prefix>_<theme>.png;更新用flutter test --update-goldens。 - 闸机 2 — fidelity:
tools/fidelity.mjs把原型截图当基准(Playwright 截design/prototype/screens/*.html,注入 Noto Sans SC 统一字体)与 Flutter golden 做 pixelmatch diff,超逐屏校准阈值即 exit 1;支持 zones 分区阈值抓局部错位。屏注册表在tools/screens.mjs(fidelity 与人工目检tools/ds-compare.mjs共用)。
cd client && flutter test --update-goldens test/golden/inventory_list_golden_test.dart
node tools/fidelity.mjs inventory # 仓库根运行;--themes a,b,c;--update 刷新原型基准
3.5 响应式
断点统一 context.isMobile(宽 < 600,core/responsive/responsive.dart),弹窗宽度 context.dialogWidth(X)(≤ 屏宽 92%)。列表屏 DataTableCard 必传 mobileCards(窄屏卡片流),窄屏导航为 Drawer 抽屉。平台判断先 kIsWeb 再 dart:io Platform。后端 URL 一律 AppConfig(--dart-define=BASE_URL=... 注入),禁止硬编码。
4. 数据库
MySQL 8 · 所有业务表含 shop_id 租户隔离列。schema.sql 定义 26 张表;另有 5 张仅由 AutoMigrate 创建(4 张商品属性字典 + error_reports),共 31 张。完整列级文档见 db-schema.html。
4.1 分组速览
| 分组 | 表 | 要点 |
|---|---|---|
| 租户与账号 | shops users user_sessions login_attempts | shops=租户根(code 唯一);users 角色 ENUM 四档、uk_shop_username;user_sessions 支撑 sid 会话校验/踢人(revoked_at、last_seen_at);login_attempts 记失败登录供锁定 |
| 授权 | licenses license_devices license_codes | 时长兑换券模型:license_codes 为平台码池(短码),激活兑入 licenses(expires_at 驱动 LicenseGuard),license_devices 记设备绑定 |
| 商品与字典 | products product_categories product_images product_*_options ×6 product_description_docs | product = 特有产品/序列号(非 SKU):code 同店唯一(uk_shop_code),name/series/spec=品牌/型号/版本,含 name_pinyin/name_initials 搜索列、public_id 公开页;字典表(名称/系列/规格/产地/保质期/储存/介绍)供入库选取后建 product |
| 仓库与往来 | warehouses partners | partners.type=supplier/customer,含拼音搜索列 |
| 入库 | stock_in_orders stock_in_items | 单据状态机(§2.6)、uk_order_no(shop_id,order_no);明细含快照列 + cost_price/cost_amount + 批次/生产日期/有效期 |
| 出库 | stock_out_orders stock_out_items | 同状态机;单头 sale_total/profit_total;明细 cost_price(成本快照)+ sale_price/sale_amount;idx_so_items_shop_code 支撑按编码反查 |
| 库存 | inventories inventory_logs inventory_checks inventory_check_items | inventories=批次行(每行一个入库批,stock_in_item_id 溯源,idx_fifo 支撑先进先出);logs 记每次变动 qty_before/after + ref;盘点单 draft→completed |
| 财务 | finance_records | type=receivable/payable/receipt/payment,balance=操作后余额,status=open/closed,ref_type/ref_id 关联单据 |
| 其他 | number_rules feedbacks error_reports | number_rules 每店每类型一行(uk_shop_type),单号=前缀+日期+6 位序号;feedbacks 意见反馈;error_reports 客户端异常 |
4.2 定价字段口径(2026-07 消歧)
| 新列 | 旧列(弃用) | 口径 |
|---|---|---|
| stock_in_items.cost_price | unit_price | 进价 · 单瓶 |
| stock_in_items.cost_amount | total_price | 总进价 = quantity × cost_price |
| stock_in_orders.cost_total | total_amount | 应付合计 = Σ 明细 cost_amount |
| stock_out_items.cost_price | unit_price | 成本单价(入库成本快照) |
| stock_out_items.cost_amount | total_price | 成本小计 = quantity × cost_price |
| stock_out_items.sale_price | —(新增) | 实际销售单价,0 = 待定价 |
| stock_out_items.sale_amount | —(新增) | 售价小计 = quantity × sale_price(待定价 = 0) |
| stock_out_orders.sale_total | total_amount | 应收合计 = Σ 明细 sale_amount |
| stock_out_orders.profit_total | —(新增) | 总利润 = Σ(sale_price>0 ? (sale_price−cost_price)×qty : 0),建单落库,确认售价/进价联动重算 |
4.3 关键机制
- 批次行 + 快照列:
inventories每行 = 某 product 在某仓的一个入库批;出入库明细与库存行都带product_code/product_name/series/spec/...快照列(导入/审核时拷贝),显示优先 product、回退快照(COALESCE/ 前端lineOrProduct)。快照列保留,勿删。 - 生成列:
inventory_check_items.diff_qty为GENERATED ALWAYS AS (actual_qty - system_qty) STORED,不可写。 - FOR UPDATE 并发锁:单号生成(number_rules)、库存扣减(inventories)等 check-then-act 一律事务内锁行;
products.code撞uk_shop_code时 max+1 重试(最多 5 次)。 - custom_fields JSON:新业务字段优先放 JSON 扩展列,不轻易加物理列。
5. API 参考
逐条核对自 backend/internal/router/router.go。权限图例:公开 无需登录(多带 per-IP 限流);JWT 登录即可(readonly 角色写操作被 ReadOnly 拦);Admin = AdminOnly;Super = SuperAdminOnly;限流 = 独立 IP 限流。所有 JWT 路由另受 per-Shop 限流;业务组(商品及以下)还受 LicenseGuard(§2.4)。
5.1 根路由 / 认证 / 公开接口
| Method | Path | 说明 | 权限 |
|---|---|---|---|
| GET | /health | 健康检查(连通性探测) | 公开 |
| GET | /version | 版本清单(实时读 version.yaml) | 公开 |
| GET | /product/:public_id | 扫码分享页(OG 标签注入的 index.html) | 公开 限流 |
| POST | /api/v1/auth/login | 登录(失败锁定 + IP 限流双保险) | 公开 限流 |
| POST | /api/v1/auth/refresh | 刷新 token | 公开 限流 |
| GET | /api/v1/public/products/:public_id | 公开商品详情(扫码页数据) | 公开 限流 |
| GET | /api/v1/public/shops/:shop_code/products | 店铺公开商品列表(限流最紧) | 公开 限流 |
| GET | /api/v1/public/release | 版本 + 下载链接 + changelog(官网/更新用) | 公开 限流 |
| POST | /api/v1/public/errors | 客户端异常上报 → error_reports | 公开 限流 |
| POST | /api/v1/public/register | 门店注册 | 公开 限流 |
5.2 会话 / 授权(豁免 LicenseGuard)
| Method | Path | 说明 | 权限 |
|---|---|---|---|
| POST | /api/v1/auth/logout | 登出(撤销会话) | JWT |
| POST | /api/v1/auth/ping | 在线心跳 | JWT |
| GET | /api/v1/sessions | 在线会话列表 | JWT |
| DELETE | /api/v1/sessions/:id | 强制下线 | Admin |
| GET | /api/v1/license/info | 授权状态(与 LicenseGuard 同一取数口径) | JWT |
| POST | /api/v1/license/activate | 兑换/激活授权码(锁定期仍可用) | JWT |
| GET | /api/v1/license/verify | 校验当前授权 | JWT |
| POST | /api/v1/license/deactivate | 解绑设备 | JWT |
| GET | /api/v1/license/devices | 已绑定设备列表 | JWT |
| POST | /api/v1/license/purchase | 在线购买/续费下单,返回 pay 收银台 pay_url(契约 pay-contract v1.0.0) | Admin |
| GET | /api/v1/license/purchase/:out_trade_no | 购买单状态(支付结果页轮询,本店可见) | JWT |
| POST | /api/v1/pay/callback | pay 支付成功 webhook(HMAC 验签+幂等+金额核对后续期) | 公开 |
5.3 商品 / 仓库 / 往来单位
| Method | Path | 说明 | 权限 |
|---|---|---|---|
| GET | /api/v1/products | 商品列表(汉字/全拼/首字母/编码搜索) | JWT |
| POST | /api/v1/products | 新建商品(独立序列号,code 自增 max+1) | JWT |
| POST | /api/v1/products/find-or-create | 按编号找/建 product(导入用;禁止按名称合并) | JWT |
| GET | /api/v1/products/:id/detail | 商品详情(含图片/字典属性) | JWT |
| GET | /api/v1/products/:id/price-history | 进价/售价历史 | JWT |
| GET | /api/v1/products/:id/qrcode | 公开页二维码 | JWT |
| PUT | /api/v1/products/:id | 更新商品 | JWT |
| DELETE | /api/v1/products/:id | 删除商品(软删) | JWT |
| POST | /api/v1/products/:id/images | 上传商品图片 | JWT |
| DELETE | /api/v1/products/:id/images/:image_id | 删除商品图片 | JWT |
| GET | /api/v1/warehouses | 仓库列表 | JWT |
| POST | /api/v1/warehouses | 新建仓库 | JWT |
| PUT | /api/v1/warehouses/:id | 更新仓库 | JWT |
| DELETE | /api/v1/warehouses/:id | 删除仓库 | JWT |
| GET | /api/v1/partners | 往来单位列表(?type&keyword,拼音搜索) | JWT |
| POST | /api/v1/partners | 新建往来单位 | JWT |
| PUT | /api/v1/partners/:id | 更新往来单位 | JWT |
| DELETE | /api/v1/partners/:id | 删除往来单位 | JWT |
5.4 入库 / 出库
| Method | Path | 说明 | 权限 |
|---|---|---|---|
| GET | /api/v1/stock-in/orders | 入库单列表(?status&keyword,编码双路匹配) | JWT |
| GET | /api/v1/stock-in/summary | 入库汇总(KPI 卡) | JWT |
| GET | /api/v1/stock-in/orders/:id | 入库单详情 | JWT |
| POST | /api/v1/stock-in/orders | 创建入库单(每明细行新建独立 product) | JWT |
| PUT | /api/v1/stock-in/orders/:id | 更新(仅 draft) | JWT |
| DELETE | /api/v1/stock-in/orders/:id | 删除(仅 draft) | JWT |
| PUT | /api/v1/stock-in/orders/:id/submit | 提交审核 draft→pending | JWT |
| PUT | /api/v1/stock-in/orders/:id/approve | 审核通过(事务:+库存 +流水 +应付) | JWT |
| PUT | /api/v1/stock-in/orders/:id/reject | 拒绝 | JWT |
| PUT | /api/v1/stock-in/orders/:id/withdraw | 撤回 pending→draft(管理员任意/操作员本人) | JWT |
| POST | /api/v1/stock-in/orders/:id/return | 退单(库存删除 + 冲应付;service 内判管理员) | Admin |
| POST | /api/v1/stock-in/orders/:id/confirm-cost | 确认进价(暂估 0 价→真实价,前向补偿) | Admin |
| GET | /api/v1/stock-out/orders | 出库单列表(?status&keyword) | JWT |
| GET | /api/v1/stock-out/summary | 出库汇总 | JWT |
| GET | /api/v1/stock-out/orders/:id | 出库单详情 | JWT |
| POST | /api/v1/stock-out/orders | 创建出库单 | JWT |
| PUT | /api/v1/stock-out/orders/:id | 更新(仅 draft) | JWT |
| DELETE | /api/v1/stock-out/orders/:id | 删除(仅 draft) | JWT |
| PUT | /api/v1/stock-out/orders/:id/submit | 提交审核 | JWT |
| PUT | /api/v1/stock-out/orders/:id/approve | 审核通过(校验库存充足,事务扣减 FIFO) | JWT |
| PUT | /api/v1/stock-out/orders/:id/reject | 拒绝 | JWT |
| PUT | /api/v1/stock-out/orders/:id/withdraw | 撤回 pending→draft | JWT |
| POST | /api/v1/stock-out/orders/:id/return | 退单(库存加回 + 冲应收;管理员或本人) | JWT |
| POST | /api/v1/stock-out/orders/:id/confirm-sale | 确认售价(先出后定价,重算应收/利润) | Admin |
5.5 库存 / 财务 / 用户 / 店铺 / 反馈 / 编号规则
| Method | Path | 说明 | 权限 |
|---|---|---|---|
| GET | /api/v1/inventory | 库存列表(?warehouse_id&keyword&series&spec&in_stock) | JWT |
| GET | /api/v1/inventory/summary | 库存汇总 | JWT |
| GET | /api/v1/inventory/logs | 库存流水 | JWT |
| PUT | /api/v1/inventory/:id/remark | 更新库存行备注 | JWT |
| POST | /api/v1/inventory/checks | 创建盘点单 | JWT |
| GET | /api/v1/inventory/checks/:id | 盘点单详情 | JWT |
| PUT | /api/v1/inventory/checks/:id/complete | 完成盘点(盘盈盘亏落库) | JWT |
| GET | /api/v1/finance/records | 财务流水列表(含日期区间) | JWT |
| POST | /api/v1/finance/records | 手工记账(收款/付款) | JWT |
| PUT | /api/v1/finance/records/:id/close | 结清单条应收/应付 | JWT |
| PUT | /api/v1/finance/records/close-by-ref | 按关联单据结清 | JWT |
| GET | /api/v1/finance/summary | 财务汇总 | JWT |
| GET | /api/v1/finance/trend | 收支趋势(柱状图数据) | JWT |
| GET | /api/v1/users | 用户列表 | Admin |
| POST | /api/v1/users | 新建用户 | Admin |
| PUT | /api/v1/users/:id | 更新用户(角色/启用) | Admin |
| PUT | /api/v1/users/:id/reset-password | 重置密码 | Admin |
| GET | /api/v1/shop/info | 店铺信息 | JWT |
| PUT | /api/v1/shop/info | 更新店铺信息 | Admin |
| POST | /api/v1/shop/logo | 上传店铺 logo | Admin |
| POST | /api/v1/feedback | 提交意见反馈 | JWT |
| POST | /api/v1/feedback/images | 上传反馈附图 | JWT |
| GET | /api/v1/number-rules | 单号规则列表 | JWT |
| PUT | /api/v1/number-rules/:id | 更新单号规则 | JWT |
5.6 数据导入(Excel)
| Method | Path | 说明 | 权限 |
|---|---|---|---|
| POST | /api/v1/import/products | 导入商品 | JWT |
| POST | /api/v1/import/partners | 导入往来单位 | JWT |
| POST | /api/v1/import/product-names | 导入名称字典 | JWT |
| POST | /api/v1/import/product-series | 导入系列字典 | JWT |
| POST | /api/v1/import/product-specs | 导入规格字典 | JWT |
| POST | /api/v1/import/product-codes | 导入商品编码 | JWT |
| POST | /api/v1/import/stock-in | 导入历史入库单 | JWT |
| POST | /api/v1/import/stock-out | 导入历史出库单 | JWT |
| POST | /api/v1/import/inventory | 导入库存(按编号建 product,禁按名称合并) | JWT |
5.7 基础数据字典(/api/v1/product-options)
八组字典同构:每组 GET(支持 ?keyword 服务端搜索)/ POST / PUT /:id / DELETE /:id,共 32 条路由,权限均为 JWT。
| 子路径 | 字典 |
|---|---|
| /names | 商品名称(品牌) |
| /series | 系列(型号) |
| /specs | 规格(版本) |
| /categories | 香型 / 分类 |
| /origins | 产地 |
| /shelf-lives | 保质期 |
| /storages | 储存方式 |
| /description-docs | 商品介绍文档 |
5.8 超级管理员(/api/v1/admin)
| Method | Path | 说明 | 权限 |
|---|---|---|---|
| POST | /api/v1/admin/clear-data | 清空本店业务数据 | Super |
| GET | /api/v1/admin/reconcile | 库存对账(inventories vs logs) | Super |
| GET | /api/v1/admin/errors | 客户端异常报告列表 | Super |
| GET | /api/v1/admin/feedback | 意见反馈列表 | Super |
| PATCH | /api/v1/admin/feedback/:id | 更新反馈处理状态 | Super |
6. 数据操作与运维
6.1 Schema 管理
backend/schema/schema.sql = 新装环境的完整 DDL(docker 初始化时执行);线上结构演进只靠启动时 AutoMigrate(只增不删),没有独立迁移文件。表结构变更 = 同步改 schema.sql + 对应 model。改生产库数据前先备份。
6.2 种子数据
sh scripts/dev.sh seed S001 # 清空并写入 S001 全量测试数据(docker exec 注入 MySQL 容器)
sh scripts/dev.sh seed S002 # 空门店(基础数据相同,无单据/库存)
每店一个 backend/seeds/<shop_code>.sql,文件顶部含 TRUNCATE,每次完整重建。
6.3 每日备份
.gitea/workflows/backup.yml:每日北京时间 02:00(cron UTC 18:00)在 mac runner(开发者本机)跑 scripts/ci/backup-db.sh——SSH 到阿里主库机 dump MySQL,落到本机 ~/jiu-db-backups,Telegram 通知结果;也可 workflow_dispatch 手动触发(外网 nginx 挡 API 触发,需 Forgejo UI 操作)。
6.4 一次性工具(backend/cmd/)
| 工具 | 用途 |
|---|---|
| cmd/seed | 初始化/重置测试数据(--reset 删表重建、--clear 清数据) |
| cmd/gencode | 平台方批量生成授权兑换券写入码池:go run ./cmd/gencode -type annual -days 365 -count 100 -batch 2026-summer |
| cmd/import-history | 旧系统历史进销存一次性迁入(历史只读、4 列快照直存、不回放库存) |
| cmd/fix-inventory-products | 修复历史库存导入按「名称|系列|规格」错误合并 product 的数据(按快照编号重指) |
| cmd/backfill-pinyin | 为存量 products 补 name_pinyin/name_initials(现启动已自动兜底) |
6.5 生产环境配置
生产 env 在 /opt/jiu/config/production.env(systemd EnvironmentFile 加载):SERVER_PORT=8081、DATABASE_DSN、JWT_SECRET(生产必须非默认值否则拒绝启动)、CORS origin(生产禁 *)。密钥不入 git,需要时从 Bitwarden 取。
7. 开发环境与工作流
7.1 本地开发
# 首次:起 MySQL 容器(127.0.0.1:3306 + Adminer :8888)
docker compose -f deploy/docker-compose.yml up -d
# 一键起前后端(后端代码未变则跳过重启;--force 强制)
sh scripts/dev.sh run
sh scripts/dev.sh stop
# 测试数据 + 账号(门店编号 S001,密码均 password123)
sh scripts/dev.sh seed S001 # admin=管理员 operator=操作员 test=只读
Go 命令前须 export PATH="/opt/homebrew/bin:$PATH"(dev.sh 已内置)。发版前自测「指向线上后端」的 macOS 包:sh scripts/local_test.sh(BASE_URL 固定生产域名,版本取最新 client tag patch+1)。
7.2 完成标准(DoD 门禁)
# 后端
cd backend && go build ./... && go vet ./... && go test ./...
# 前端
cd client && flutter analyze --no-fatal-infos --no-fatal-warnings && flutter test
# 设计系统闸(改了 UI 时)
cd client && node tool/check_ds_code.mjs --changed # 禁硬编码色
node tools/fidelity.mjs # golden vs 原型像素闸(仓库根)
7.3 Git 提交规范
{类型}({模块}): {简短说明} # 类型: feat|fix|test|docs|chore|security|refactor
# 模块: backend|client|db|deploy|docs
feat(backend): 新增财务报表接口
docs(api): 财务报表 API 接口文档
7.4 三条发版流水线
client / site / server 三条互不影响的流水线,各有 tag 前缀、独立版本序列、独立 CHANGELOG,由 Forgejo CI 按 tag 前缀路由(.gitea/workflows/deploy-*.yml + scripts/ci/)。用 /release <part> [version]:本地 build → test → 更新 CHANGELOG → commit → tag → push;省略版本则自增最新 tag 的 patch。回滚用 manual.yml 输入带前缀 tag,从 Forgejo Release 下载产物重部署。
| part | 范围 | tag 前缀 | 要点 |
|---|---|---|---|
| client | client/ 全平台 + version.yaml | client-v* | version.yaml 归 client;后端 /version 与 /public/release 每请求实时读——client 发版立即生效,不重启后端、不重建官网 |
| site | web/ 营销站(不含 Web 版 app) | site-v* | 官网下载页运行时 fetch /public/release 动态刷新版本徽章/链接/更新日志 |
| server | backend/ + nginx/systemd 基建 | server-v* | nginx conf、jiu.service 归 server 流水线 |
/release client 1.1.1 # 指定版本
/release server # 省略则自增最新 server-v* tag 的 patch
CI 脚本在 scripts/ci/:lib-forgejo.sh(公共函数)+ compile-{client-web,site,backend,macos,android,ios,windows}.sh + release-*.sh + deploy-*.sh。runner:mac = 开发者本机(launchd 看门狗)、windows = 另一台机(nssm)、Forgejo 在 NAS(git.51yanmei.com)。