fix(backend): 架构质量改进批次二 (#32-37)

- #32 License 激活迁移到 license_devices 表:Activate/Verify/Deactivate 全部改用
  license_devices,新增 max_devices 校验和 GET /license/devices 端点;
  Activate 现在校验 shop_id 防跨租户激活
- #33 checkInventory 从 StockOutHandler 移到 StockService.CheckInventoryAvailability
- #34 新增 util/response.go 统一错误响应工具(RespondError/RespondSuccess/RespondCreated)
- #35 生产模式 CORS Origin='*' 启动时 Fatal
- #36 生产模式 License 私钥未配置启动时 Fatal
- #37 新增 util/page.go ValidatePageSize,应用到 partner/product/stock_in/stock_out handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-11 00:38:49 +08:00
parent 51cfe5fc6d
commit 3ab78dbf7a
15 changed files with 554 additions and 429 deletions
+257 -257
View File
@@ -221,12 +221,12 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<header>
<div class="wrap">
<h1>酒库管理系统 — 项目 TODO</h1>
<div class="header-meta">生成于 2026-06-10 · 真相源 todo/todo.json</div>
<div class="header-meta">生成于 2026-06-11 · 真相源 todo/todo.json</div>
<div class="stats">
<div class="stat-pill"><strong>36</strong>全部</div>
<div class="stat-pill"><strong>14</strong>待开始</div>
<div class="stat-pill"><strong>5</strong>待开始</div>
<div class="stat-pill"><strong>0</strong>开发中</div>
<div class="stat-pill"><strong>8</strong>待验收</div>
<div class="stat-pill"><strong>17</strong>待验收</div>
<div class="stat-pill"><strong>14</strong>已验收</div>
</div>
</div>
@@ -269,7 +269,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="section-block" id="section-open">
<div class="section-title st-open" data-toggle="open">
📋 待开始 <span class="s-count">14</span>
📋 待开始 <span class="s-count">5</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-open">
@@ -303,258 +303,6 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
</div>
</li>
<li class="todo-card s-open"
data-id="29"
data-level="high"
data-status="open"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">修复库存扣减 TOCTOU 竞态:SUM 预检纳入事务且在 FOR UPDATE 加锁后执行</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-block">高优 · 紧急</span>
<span class="tag tier-2">二级</span>
</div>
</div>
<div class="item-desc">stock.go ApproveStockOut 中库存总量 SUM 在 FOR UPDATE 加锁前执行,存在 check-then-act 窗口,高并发下可超扣。需将预检 SUM 也放入事务并在加锁后执行</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="30"
data-level="high"
data-status="open"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">统一 handler Update 改用白名单字段更新,防止 GORM Save() 跨租户覆盖</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-block">高优 · 紧急</span>
<span class="tag tier-2">二级</span>
</div>
</div>
<div class="item-desc">partner.go/product_attr.go/warehouse.go 等多处用 db.Save(&amp;object) 更新全字段,若中间件被绕过会造成 shop_id 被覆盖。改为 db.Model(&amp;x).Where(&quot;id=? AND shop_id=?&quot;).Updates(fields) 白名单模式</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="31"
data-level="high"
data-status="open"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">添加库存与财务流水定期对账检查,防止事务中断导致不平账</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-block">高优 · 紧急</span>
<span class="tag tier-2">二级</span>
</div>
</div>
<div class="item-desc">出库审批若 DB 断连,Rollback 后库存正确但财务记录可能未落地。需添加对账脚本:SUM(InventoryLog.quantity by direction) == SUM(Inventories.quantity),及告警机制</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="32"
data-level="mid"
data-status="open"
data-tier="2"
data-tags="后端,数据库">
<div class="card-header">
<span class="item-title">迁移 License 激活逻辑到 license_devices 表,废弃 licenses.device_id 字段</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
<span class="tag tier-2">二级</span>
</div>
</div>
<div class="item-desc">licenses.device_id 已标记 deprecated 但激活/解绑逻辑仍在使用它,license_devices 表未被完整采用。需将 Activate/Deactivate 逻辑迁移至 license_devices,并写数据迁移脚本</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="数据库">数据库</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="33"
data-level="mid"
data-status="open"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">将 checkInventory 业务逻辑从 StockOutHandler 移到 Service 层</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
<span class="tag tier-2">二级</span>
</div>
</div>
<div class="item-desc">checkInventory 实现在 handler/stock_out.go 中而非 service 层,导致单元测试困难、逻辑无法复用。应移至 StockService 或 InventoryService</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="34"
data-level="mid"
data-status="open"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">统一后端 API 错误响应格式为 {code, message},创建 util/response.go</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
<span class="tag tier-2">二级</span>
</div>
</div>
<div class="item-desc">各 handler 返回格式不一致:有的 {error: err.Error()},有的 {error: 硬编码字符串},有的直接 struct。前端解析困难。需定义 ErrorResponse struct 和 RespondError/RespondSuccess 辅助函数统一调用</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="35"
data-level="mid"
data-status="open"
data-tier="3"
data-tags="后端">
<div class="card-header">
<span class="item-title">生产环境 CORS 强制校验:非 debug 模式禁止 Origin=*</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
<span class="tag tier-3">三级</span>
</div>
</div>
<div class="item-desc">config.go 默认 CORSOrigin=&quot;*&quot;,注释提示生产需改但无代码强制。应在 main.go 中添加:server.mode!=debug 且 CORSOrigin==&quot;*&quot; 时 log.Fatal 拒绝启动</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="36"
data-level="mid"
data-status="open"
data-tier="3"
data-tags="后端">
<div class="card-header">
<span class="item-title">License 私钥未配置时改为 Fatal 而非静默跳过</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
<span class="tag tier-3">三级</span>
</div>
</div>
<div class="item-desc">license.go 中私钥未配置只打 log 并跳过,导致授权功能失效但程序正常运行。应改为 log.Fatal,确保运维人员知晓配置缺失</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="37"
data-level="mid"
data-status="open"
data-tier="3"
data-tags="后端">
<div class="card-header">
<span class="item-title">统一各 handler 的 pageSize 上限校验,创建 util.ValidatePageSize()</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
<span class="tag tier-3">三级</span>
</div>
</div>
<div class="item-desc">inventory.go 限制 pageSize 最大 500,其他 handler 无此限制,前端可传 pageSize=10000 打满 DB。创建 util.ValidatePageSize(n) 统一处理</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="23"
data-level="low"
@@ -682,7 +430,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
</div>
<div class="section-block" id="section-done">
<div class="section-title st-done" data-toggle="done">
🔍 待验收 <span class="s-count">8</span>
🔍 待验收 <span class="s-count">17</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-done">
@@ -716,6 +464,90 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
</div>
</li>
<li class="todo-card s-done"
data-id="29"
data-level="high"
data-status="done"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">修复库存扣减 TOCTOU 竞态:SUM 预检纳入事务且在 FOR UPDATE 加锁后执行</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-block">高优 · 紧急</span>
<span class="tag tier-2">二级</span>
<button class="reject-btn" data-id="29" data-title="修复库存扣减 TOCTOU 竞态:SUM 预检纳入事务且在 FOR UPDATE 加锁后执行">拒绝验收</button>
</div>
</div>
<div class="item-desc">stock.go ApproveStockOut 中库存总量 SUM 在 FOR UPDATE 加锁前执行,存在 check-then-act 窗口,高并发下可超扣。需将预检 SUM 也放入事务并在加锁后执行</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="30"
data-level="high"
data-status="done"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">统一 handler Update 改用白名单字段更新,防止 GORM Save() 跨租户覆盖</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-block">高优 · 紧急</span>
<span class="tag tier-2">二级</span>
<button class="reject-btn" data-id="30" data-title="统一 handler Update 改用白名单字段更新,防止 GORM Save() 跨租户覆盖">拒绝验收</button>
</div>
</div>
<div class="item-desc">partner.go/product_attr.go/warehouse.go 等多处用 db.Save(&amp;object) 更新全字段,若中间件被绕过会造成 shop_id 被覆盖。改为 db.Model(&amp;x).Where(&quot;id=? AND shop_id=?&quot;).Updates(fields) 白名单模式</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="31"
data-level="high"
data-status="done"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">添加库存与财务流水定期对账检查,防止事务中断导致不平账</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-block">高优 · 紧急</span>
<span class="tag tier-2">二级</span>
<button class="reject-btn" data-id="31" data-title="添加库存与财务流水定期对账检查,防止事务中断导致不平账">拒绝验收</button>
</div>
</div>
<div class="item-desc">出库审批若 DB 断连,Rollback 后库存正确但财务记录可能未落地。需添加对账脚本:SUM(InventoryLog.quantity by direction) == SUM(Inventories.quantity),及告警机制</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="20"
data-level="mid"
@@ -772,6 +604,174 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
</div>
</li>
<li class="todo-card s-done"
data-id="32"
data-level="mid"
data-status="done"
data-tier="2"
data-tags="后端,数据库">
<div class="card-header">
<span class="item-title">迁移 License 激活逻辑到 license_devices 表,废弃 licenses.device_id 字段</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-high">重要</span>
<span class="tag tier-2">二级</span>
<button class="reject-btn" data-id="32" data-title="迁移 License 激活逻辑到 license_devices 表,废弃 licenses.device_id 字段">拒绝验收</button>
</div>
</div>
<div class="item-desc">licenses.device_id 已标记 deprecated 但激活/解绑逻辑仍在使用它,license_devices 表未被完整采用。需将 Activate/Deactivate 逻辑迁移至 license_devices,并写数据迁移脚本</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="数据库">数据库</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="33"
data-level="mid"
data-status="done"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">将 checkInventory 业务逻辑从 StockOutHandler 移到 Service 层</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-high">重要</span>
<span class="tag tier-2">二级</span>
<button class="reject-btn" data-id="33" data-title="将 checkInventory 业务逻辑从 StockOutHandler 移到 Service 层">拒绝验收</button>
</div>
</div>
<div class="item-desc">checkInventory 实现在 handler/stock_out.go 中而非 service 层,导致单元测试困难、逻辑无法复用。应移至 StockService 或 InventoryService</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="34"
data-level="mid"
data-status="done"
data-tier="2"
data-tags="后端">
<div class="card-header">
<span class="item-title">统一后端 API 错误响应格式为 {code, message},创建 util/response.go</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-high">重要</span>
<span class="tag tier-2">二级</span>
<button class="reject-btn" data-id="34" data-title="统一后端 API 错误响应格式为 {code, message},创建 util/response.go">拒绝验收</button>
</div>
</div>
<div class="item-desc">各 handler 返回格式不一致:有的 {error: err.Error()},有的 {error: 硬编码字符串},有的直接 struct。前端解析困难。需定义 ErrorResponse struct 和 RespondError/RespondSuccess 辅助函数统一调用</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="35"
data-level="mid"
data-status="done"
data-tier="3"
data-tags="后端">
<div class="card-header">
<span class="item-title">生产环境 CORS 强制校验:非 debug 模式禁止 Origin=*</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-high">重要</span>
<span class="tag tier-3">三级</span>
<button class="reject-btn" data-id="35" data-title="生产环境 CORS 强制校验:非 debug 模式禁止 Origin=*">拒绝验收</button>
</div>
</div>
<div class="item-desc">config.go 默认 CORSOrigin=&quot;*&quot;,注释提示生产需改但无代码强制。应在 main.go 中添加:server.mode!=debug 且 CORSOrigin==&quot;*&quot; 时 log.Fatal 拒绝启动</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="36"
data-level="mid"
data-status="done"
data-tier="3"
data-tags="后端">
<div class="card-header">
<span class="item-title">License 私钥未配置时改为 Fatal 而非静默跳过</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-high">重要</span>
<span class="tag tier-3">三级</span>
<button class="reject-btn" data-id="36" data-title="License 私钥未配置时改为 Fatal 而非静默跳过">拒绝验收</button>
</div>
</div>
<div class="item-desc">license.go 中私钥未配置只打 log 并跳过,导致授权功能失效但程序正常运行。应改为 log.Fatal,确保运维人员知晓配置缺失</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="37"
data-level="mid"
data-status="done"
data-tier="3"
data-tags="后端">
<div class="card-header">
<span class="item-title">统一各 handler 的 pageSize 上限校验,创建 util.ValidatePageSize()</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-high">重要</span>
<span class="tag tier-3">三级</span>
<button class="reject-btn" data-id="37" data-title="统一各 handler 的 pageSize 上限校验,创建 util.ValidatePageSize()">拒绝验收</button>
</div>
</div>
<div class="item-desc">inventory.go 限制 pageSize 最大 500,其他 handler 无此限制,前端可传 pageSize=10000 打满 DB。创建 util.ValidatePageSize(n) 统一处理</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-10</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="21"
data-level="low"
+10 -10
View File
@@ -1,7 +1,7 @@
{
"meta": {
"title": "酒库管理系统 — 项目 TODO",
"updated_at": "2026-06-10T15:32:03.641Z"
"updated_at": "2026-06-10T16:38:37.763Z"
},
"seq": 40,
"items": [
@@ -477,7 +477,7 @@
"tags": [
"后端"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:13.249Z",
"done": false,
"completed_at": null,
@@ -492,7 +492,7 @@
"tags": [
"后端"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:15.102Z",
"done": false,
"completed_at": null,
@@ -507,7 +507,7 @@
"tags": [
"后端"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:16.854Z",
"done": false,
"completed_at": null,
@@ -523,7 +523,7 @@
"后端",
"数据库"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:25.715Z",
"done": false,
"completed_at": null,
@@ -538,7 +538,7 @@
"tags": [
"后端"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:28.106Z",
"done": false,
"completed_at": null,
@@ -553,7 +553,7 @@
"tags": [
"后端"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:33.596Z",
"done": false,
"completed_at": null,
@@ -568,7 +568,7 @@
"tags": [
"后端"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:44.159Z",
"done": false,
"completed_at": null,
@@ -583,7 +583,7 @@
"tags": [
"后端"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:46.792Z",
"done": false,
"completed_at": null,
@@ -598,7 +598,7 @@
"tags": [
"后端"
],
"status": "open",
"status": "done",
"created_at": "2026-06-10T15:31:49.032Z",
"done": false,
"completed_at": null,