Files
jiu/todo/todo.html
T
wangjia 3ab78dbf7a 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>
2026-06-11 00:38:49 +08:00

1545 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>酒库管理系统 — 项目 TODO</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0; padding: 0 0 80px;
font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #1f2933; background: #f7f9fb; line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
header {
background: linear-gradient(135deg, #0A1F3B 0%, #15407D 100%);
color: #fff; padding: 36px 32px 28px;
}
header h1 { margin: 0 0 6px; font-size: 24px; font-weight: 700; }
header .header-meta { color: #ADC9EA; font-size: 13px; margin-top: 4px; }
.stats { display: flex; gap: 16px; margin-top: 14px; flex-wrap: wrap; }
.stat-pill {
background: rgba(255,255,255,0.12); border-radius: 20px;
padding: 4px 14px; font-size: 13px; color: #fff;
}
.stat-pill strong { font-size: 18px; font-weight: 700; margin-right: 2px; }
/* ── 筛选栏 ── */
.filter-bar {
position: sticky; top: 0; z-index: 10;
background: #fff; border-bottom: 1px solid #e4e9ef;
padding: 10px 28px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
}
.filter-group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.filter-label { font-size: 12px; color: #7b8794; white-space: nowrap; }
.filter-chip, .filter-btn {
padding: 3px 12px; border-radius: 14px; border: 1.5px solid #d1d9e6;
background: #f7f9fb; color: #52606d; font-size: 12.5px; cursor: pointer;
transition: all .15s; white-space: nowrap;
}
.filter-chip:hover, .filter-btn:hover { border-color: #2563AC; color: #2563AC; }
.filter-chip.active, .filter-btn.active {
background: #2563AC; border-color: #2563AC; color: #fff;
}
.filter-sep { width: 1px; height: 20px; background: #e4e9ef; margin: 0 4px; }
/* ── 内容区 ── */
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px; }
.section-block { margin-bottom: 8px; }
.section-title {
font-size: 15px; font-weight: 700;
padding: 10px 14px; border-radius: 8px;
display: flex; align-items: center; gap: 8px; cursor: pointer;
margin: 20px 0 8px; user-select: none;
}
.section-title .s-count {
font-size: 12px; font-weight: 500; padding: 1px 8px; border-radius: 10px;
background: rgba(0,0,0,0.08);
}
.section-title .s-arrow { margin-left: auto; font-size: 12px; color: inherit; opacity: .6; }
/* 状态主题色 */
.st-open { background: #eff6ff; color: #1e40af; border-left: 4px solid #3b82f6; }
.st-doing { background: #fff7ed; color: #9a3412; border-left: 4px solid #f97316; }
.st-done { background: #fefce8; color: #854d0e; border-left: 4px solid #eab308; }
.st-accepted{ background: #f0fdf4; color: #166534; border-left: 4px solid #22c55e; }
ul.todo-list { list-style: none; margin: 0; padding: 0; }
.todo-card {
background: #fff; border: 1px solid #e4e9ef; border-radius: 10px;
padding: 14px 18px; margin-bottom: 10px;
border-left-width: 4px;
}
.todo-card.s-open { border-left-color: #3b82f6; }
.todo-card.s-doing { border-left-color: #f97316; }
.todo-card.s-done { border-left-color: #eab308; }
.todo-card.s-accepted { border-left-color: #22c55e; background: #f0fdf4; opacity: .8; }
.card-header { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
.item-title { font-weight: 600; font-size: 15px; flex: 1; }
.todo-card.s-accepted .item-title { text-decoration: line-through; color: #52606d; }
.card-badges { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.item-desc { font-size: 13.5px; color: #52606d; margin-top: 6px; }
.item-desc code {
background: #f0f2f5; padding: 1px 5px; border-radius: 3px;
font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: #b91c1c;
}
.card-footer { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.tag-row { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; }
.item-meta { display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px; color: #8aa3c4; }
.meta-date { white-space: nowrap; }
/* ── 标签样式 ── */
.tag { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; }
.t-block { background: #fde8e8; color: #b91c1c; }
.t-high { background: #fef0d8; color: #9a6700; }
.t-low { background: #e6f0fb; color: #1d4ed8; }
.t-tag { background: #efefef; color: #555; cursor: pointer; transition: .12s; }
.t-tag:hover { background: #d9e8fb; color: #1d4ed8; }
.ver-badge { font-weight: 700; color: #1f7a44; }
.sub-progress-badge { background: #f0f4ff; color: #3730a3; }
/* 状态徽章 */
.status-badge { font-size: 11.5px; }
.s-open { background: #dbeafe; color: #1e40af; }
.s-doing { background: #ffedd5; color: #9a3412; }
.s-done { background: #fef9c3; color: #854d0e; }
.s-accepted { background: #dcfce7; color: #166534; }
/* ── 折叠 ── */
.section-list-wrap.collapsed { display: none; }
.empty-tip { font-size: 14px; color: #8aa3c4; margin: 8px 0 24px; }
.hidden { display: none !important; }
/* ── 改动等级徽章 ── */
.tier-1 { background: #ede9fe; color: #6d28d9; }
.tier-2 { background: #ccfbf1; color: #0f766e; }
.tier-3 { background: #f1f5f9; color: #475569; }
/* ── 拒绝按钮 & 拒绝原因 ── */
.reject-btn {
padding: 3px 10px; border-radius: 6px; border: 1.5px solid #dc2626;
background: #fff; color: #dc2626; font-size: 12px; font-weight: 600;
cursor: pointer; transition: all .15s; white-space: nowrap;
}
.reject-btn:hover { background: #dc2626; color: #fff; }
.reject-note {
margin: 8px 0 4px; padding: 7px 12px; border-radius: 6px;
background: #fef2f2; border: 1px solid #fecaca;
font-size: 13px; color: #b91c1c; line-height: 1.5;
}
.reject-date { color: #ef9999; font-size: 12px; }
/* ── 子任务区 ── */
.subtask-block {
margin-top: 12px; padding: 10px 14px;
background: #f8fafc; border: 1px solid #e4e9ef; border-radius: 8px;
}
.subtask-header {
display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap;
}
.subtask-label {
font-size: 11px; font-weight: 700; color: #7b8794;
text-transform: uppercase; letter-spacing: .6px;
}
.subtask-progress-text { font-size: 12px; color: #52606d; }
.subtask-progress-bar {
flex: 1; height: 5px; background: #e4e9ef; border-radius: 3px; min-width: 60px;
}
.subtask-progress-fill { height: 100%; background: #22c55e; border-radius: 3px; transition: width .4s; }
.subtask-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.subtask-item {
display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
padding: 6px 10px; border-radius: 6px; font-size: 13px;
background: #fff; border: 1px solid #e8edf2;
}
.subtask-item.s-done { background: #f0fdf4; border-color: #bbf7d0; }
.subtask-item.s-doing { background: #fff7ed; border-color: #fed7aa; }
.subtask-item.s-accepted { background: #f0fdf4; border-color: #bbf7d0; opacity: .75; }
.sub-icon {
width: 16px; text-align: center; font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.sub-icon.s-done { color: #16a34a; }
.sub-icon.s-doing { color: #ea580c; }
.sub-icon.s-open { color: #93aec8; }
.sub-icon.s-accepted { color: #16a34a; }
.sub-sid {
font-family: "JetBrains Mono", monospace; font-size: 11.5px;
color: #52606d; font-weight: 700; min-width: 32px; flex-shrink: 0;
}
.sub-title { flex: 1; font-size: 13px; min-width: 120px; }
.subtask-item.s-done .sub-title,
.subtask-item.s-accepted .sub-title { text-decoration: line-through; color: #52606d; }
.sub-badges { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.dep-section { display: flex; align-items: center; gap: 3px; }
.dep-label { font-size: 11px; color: #8aa3c4; margin-right: 2px; }
.dep-badge {
padding: 1px 6px; border-radius: 8px; font-size: 11px;
font-weight: 700; font-family: monospace; cursor: default;
}
.dep-done { background: #dcfce7; color: #166534; }
.dep-pending { background: #fee2e2; color: #b91c1c; }
/* ── 拒绝 Modal ── */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.45);
display: flex; align-items: center; justify-content: center; z-index: 999;
}
.modal-box {
background: #fff; border-radius: 12px; padding: 28px 32px;
width: min(480px, 94vw); box-shadow: 0 20px 60px rgba(0,0,0,.2);
}
.modal-box h3 { margin: 0 0 4px; font-size: 17px; color: #1f2933; }
.modal-subtitle { font-size: 13px; color: #52606d; margin: 0 0 18px; }
.modal-label { font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 6px; display: block; }
.modal-label .req { color: #dc2626; margin-left: 2px; }
.modal-textarea {
width: 100%; border: 1.5px solid #d1d9e6; border-radius: 8px;
padding: 10px 12px; font-size: 14px; font-family: inherit; resize: vertical;
min-height: 88px; outline: none; transition: border-color .15s;
}
.modal-textarea:focus { border-color: #dc2626; }
.modal-textarea.error { border-color: #dc2626; background: #fff8f8; }
.modal-err { font-size: 12px; color: #dc2626; margin-top: 4px; display: none; }
.modal-err.show { display: block; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
.modal-btn { padding: 8px 20px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: all .15s; }
.modal-btn-cancel { background: #f3f4f6; color: #374151; }
.modal-btn-cancel:hover { background: #e5e7eb; }
.modal-btn-reject { background: #dc2626; color: #fff; }
.modal-btn-reject:hover { background: #b91c1c; }
.modal-cmd-wrap { margin-top: 16px; padding: 12px 14px; background: #1e293b; border-radius: 8px; }
.modal-cmd-label { font-size: 12px; color: #94a3b8; margin-bottom: 8px; }
.modal-cmd-code { font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 13px; color: #86efac; word-break: break-all; display: block; }
.modal-copy-btn { margin-top: 10px; padding: 5px 14px; border-radius: 6px; background: #334155; color: #e2e8f0; border: none; font-size: 12px; cursor: pointer; }
.modal-copy-btn:hover { background: #475569; }
</style>
</head>
<body>
<header>
<div class="wrap">
<h1>酒库管理系统 — 项目 TODO</h1>
<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>5</strong>待开始</div>
<div class="stat-pill"><strong>0</strong>开发中</div>
<div class="stat-pill"><strong>17</strong>待验收</div>
<div class="stat-pill"><strong>14</strong>已验收</div>
</div>
</div>
</header>
<div class="filter-bar" id="filterBar">
<div class="filter-group">
<span class="filter-label">重要度</span>
<button class="filter-btn active" data-filter-level="all">全部</button>
<button class="filter-btn" data-filter-level="high">高优</button>
<button class="filter-btn" data-filter-level="mid">重要</button>
<button class="filter-btn" data-filter-level="low">一般</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">状态</span>
<button class="filter-btn active" data-filter-status="all">全部</button>
<button class="filter-btn" data-filter-status="open">待开始</button>
<button class="filter-btn" data-filter-status="doing">开发中</button>
<button class="filter-btn" data-filter-status="done">待验收</button>
<button class="filter-btn" data-filter-status="accepted">已验收</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">改动等级</span>
<button class="filter-btn active" data-filter-tier="all">全部</button>
<button class="filter-btn" data-filter-tier="1">一级</button>
<button class="filter-btn" data-filter-tier="2">二级</button>
<button class="filter-btn" data-filter-tier="3">三级</button>
</div>
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">平台 / 标签</span>
<button class="filter-chip" data-filter-tag="Android">Android</button><button class="filter-chip" data-filter-tag="Web">Web</button><button class="filter-chip" data-filter-tag="Windows">Windows</button><button class="filter-chip" data-filter-tag="iOS">iOS</button><button class="filter-chip" data-filter-tag="mac">mac</button><button class="filter-chip" data-filter-tag="前端">前端</button><button class="filter-chip" data-filter-tag="后端">后端</button><button class="filter-chip" data-filter-tag="安全">安全</button><button class="filter-chip" data-filter-tag="数据库">数据库</button><button class="filter-chip" data-filter-tag="文档">文档</button>
</div>
</div>
<div class="wrap">
<div class="section-block" id="section-open">
<div class="section-title st-open" data-toggle="open">
📋 待开始 <span class="s-count">5</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-open">
<ul class="todo-list" id="list-open">
<li class="todo-card s-open"
data-id="2"
data-level="high"
data-status="open"
data-tier=""
data-tags="Web">
<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>
</div>
</div>
<div class="item-desc">现为沪ICP备2026000000号(全0),上线前必须替换真实备案号。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="23"
data-level="low"
data-status="open"
data-tier=""
data-tags="前端,Web">
<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-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">当前注册成功页和网站联系支持入口引导用户「发邮件」,体验较弱。升级为扫码加客服微信,同时梳理相关文案措辞,使表达更专业、有亲和力。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="38"
data-level="low"
data-status="open"
data-tier="3"
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-low">一般 / 优化</span>
<span class="tag tier-3">三级</span>
</div>
</div>
<div class="item-desc">main.go 每次启动执行 backfillPinyin 全表扫描,数据量大后拖慢启动。改为 go run ./cmd/migrate-pinyin 一次性迁移,后续新数据靠写入时自动生成</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="39"
data-level="low"
data-status="open"
data-tier="3"
data-tags="后端">
<div class="card-header">
<span class="item-title">DB 连接池参数(MaxIdleConns/MaxOpenConns)改为可配置项</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-low">一般 / 优化</span>
<span class="tag tier-3">三级</span>
</div>
</div>
<div class="item-desc">main.go 中硬编码 MaxIdleConns=10, MaxOpenConns=100,无法运维调优。改为读取 config(viper),并在文档中说明推荐值</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="40"
data-level="low"
data-status="open"
data-tier="2"
data-tags="后端,数据库">
<div class="card-header">
<span class="item-title">明确 Inventories 反范式字段语义:是当前值还是入库快照</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-low">一般 / 优化</span>
<span class="tag tier-2">二级</span>
</div>
</div>
<div class="item-desc">inventories 表存了 product_name/warehouse_name/supplier_name 等,既可能是冗余(与 products JOIN 重复),也可能是历史快照(入库时锁定)。需明确语义并在 model 注释中说明;若是快照,应只在写入时固定,不随主表变化</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>
</ul>
</div>
</div>
<div class="section-block" id="section-doing">
<div class="section-title st-doing" data-toggle="doing">
🔨 开发中 <span class="s-count">0</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-doing">
<ul class="todo-list" id="list-doing">
<p class="empty-tip">暂无条目</p>
</ul>
</div>
</div>
<div class="section-block" id="section-done">
<div class="section-title st-done" data-toggle="done">
🔍 待验收 <span class="s-count">17</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-done">
<ul class="todo-list" id="list-done">
<li class="todo-card s-done"
data-id="19"
data-level="high"
data-status="done"
data-tier=""
data-tags="前端,iOS,Android">
<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>
<button class="reject-btn" data-id="19" data-title="入库单新建表单增加产地/保质期/储存方式,选填项默认折叠">拒绝验收</button>
</div>
</div>
<div class="item-desc">新建入库单的商品行增加产地、保质期、储存方式三个字典下拉选项(已有字典数据)。必填项(商品名、数量等)正常显示;选填项默认隐藏,点击展开按钮后在下一行展示。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</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"
data-status="done"
data-tier=""
data-tags="前端,iOS,Android">
<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-high">重要</span>
<button class="reject-btn" data-id="20" data-title="商品详情页重设计:展示所有字段,内容为公开页超集">拒绝验收</button>
</div>
</div>
<div class="item-desc">当前 product_detail_screen.dart 显示内容过于简单。需先做页面设计(UI 方案),内容要求是公开扫码页的超集:包含商品基本信息、参数(产地/保质期/储存方式)、价格、描述文档、图片、库存批次、入库记录等管理侧专属字段。另:商品公开链接展示为可点击超链接(点击可跳转公开扫码页)。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="27"
data-level="mid"
data-status="done"
data-tier=""
data-tags="Windows,mac">
<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-high">重要</span>
<button class="reject-btn" data-id="27" data-title="桌面客户端单实例限制,禁止同时运行多个进程">拒绝验收</button>
</div>
</div>
<div class="item-desc">Windows 和 macOS 客户端启动时检测是否已有实例运行;若有,则激活已有窗口并退出新进程。Flutter desktop 可通过 window_single_instance 或 dart:io 锁文件实现。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="mac">mac</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</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"
data-status="done"
data-tier=""
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-low">一般 / 优化</span>
<span class="tag sub-progress-badge">8/8 子任务</span>
<button class="reject-btn" data-id="21" data-title="授权信息功能方案设计">拒绝验收</button>
</div>
</div>
<div class="item-desc">完整方案设计,涵盖:界面设计与交互、前端实现方案、后端接口与数据模型、安全策略(防盗用、防滥用、授权校验)等。需产出设计文档后再进入开发阶段。</div>
<div class="subtask-block">
<div class="subtask-header">
<span class="subtask-label">子任务</span>
<span class="subtask-progress-text">8 / 8 完成</span>
<div class="subtask-progress-bar"><div class="subtask-progress-fill" style="width:100%"></div></div>
</div>
<ul class="subtask-list"><li class="subtask-item s-done" data-sid="21A">
<span class="sub-icon s-done"></span>
<span class="sub-sid">21A</span>
<span class="sub-title">后端:Ed25519 签发验签工具 + GenerateKey 修复</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span></div>
</li>
<li class="subtask-item s-done" data-sid="21B">
<span class="sub-icon s-done"></span>
<span class="sub-sid">21B</span>
<span class="sub-title">数据库:license_devices 表 + licenses 表改造</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-done" title="21A 已完成">21A</span></span></div>
</li>
<li class="subtask-item s-done" data-sid="21C">
<span class="sub-icon s-done"></span>
<span class="sub-sid">21C</span>
<span class="sub-title">后端:注册自动签发 30 天 trial license</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-done" title="21A 已完成">21A</span></span></div>
</li>
<li class="subtask-item s-done" data-sid="21D">
<span class="sub-icon s-done"></span>
<span class="sub-sid">21D</span>
<span class="sub-title">后端:LicenseGuard 中间件 + JWT claim 扩展</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-done" title="21B 已完成">21B</span><span class="dep-badge dep-done" title="21C 已完成">21C</span></span></div>
</li>
<li class="subtask-item s-done" data-sid="21E">
<span class="sub-icon s-done"></span>
<span class="sub-sid">21E</span>
<span class="sub-title">前端:license model/repository/device_id 采集</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-done" title="21A 已完成">21A</span></span></div>
</li>
<li class="subtask-item s-done" data-sid="21F">
<span class="sub-icon s-done"></span>
<span class="sub-sid">21F</span>
<span class="sub-title">前端:激活与设备管理 UI(授权 Tab 重构)</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-done" title="21D 已完成">21D</span><span class="dep-badge dep-done" title="21E 已完成">21E</span></span></div>
</li>
<li class="subtask-item s-done" data-sid="21G">
<span class="sub-icon s-done"></span>
<span class="sub-sid">21G</span>
<span class="sub-title">前端:gating 横幅 + 到期弹窗(app_shell 注入)</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-done" title="21D 已完成">21D</span><span class="dep-badge dep-done" title="21E 已完成">21E</span></span></div>
</li>
<li class="subtask-item s-done" data-sid="21H">
<span class="sub-icon s-done"></span>
<span class="sub-sid">21H</span>
<span class="sub-title">端到端验证:phase 切换 + 签名拒绝 + 多设备上限</span>
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span><span class="dep-section"><span class="dep-label">依赖:</span><span class="dep-badge dep-done" title="21D 已完成">21D</span><span class="dep-badge dep-done" title="21F 已完成">21F</span><span class="dep-badge dep-done" title="21G 已完成">21G</span></span></div>
</li></ul>
</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> <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-08</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="22"
data-level="low"
data-status="done"
data-tier=""
data-tags="前端,Web,文档">
<div class="card-header">
<span class="item-title">关于我们页文档/更新日志链接更新,Web 新增版本历史页</span>
<div class="card-badges">
<span class="tag status-badge s-done">待验收</span>
<span class="tag t-low">一般 / 优化</span>
<button class="reject-btn" data-id="22" data-title="关于我们页文档/更新日志链接更新,Web 新增版本历史页">拒绝验收</button>
</div>
</div>
<div class="item-desc">1. 客户端「关于我们」页面的「打开文档」「更新日志」链接指向需更新为正确地址。2. Web 营销站新增版本更新页面(/changelog 或 /download/),从 CHANGELOG.md 自动渲染,展示所有历史版本更新记录。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="文档">文档</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="24"
data-level="low"
data-status="done"
data-tier=""
data-tags="Web,文档">
<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-low">一般 / 优化</span>
<button class="reject-btn" data-id="24" data-title="服务条款与隐私政策页面">拒绝验收</button>
</div>
</div>
<div class="item-desc">在营销网站(jiu.51yanmei.com)补充服务条款(Terms of Service)和隐私政策(Privacy Policy)页面,注册页底部添加链接。最低优先级,待其他功能稳定后处理。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="文档">文档</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="25"
data-level="low"
data-status="done"
data-tier=""
data-tags="前端,iOS,Android">
<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-low">一般 / 优化</span>
<button class="reject-btn" data-id="25" data-title="移动端左侧抽屉菜单支持手势侧滑唤出">拒绝验收</button>
</div>
</div>
<div class="item-desc">窄屏(手机)模式下,导航 Drawer 当前需点击汉堡按钮打开,需支持从屏幕左边缘向右滑动手势唤出菜单,符合移动端操作习惯。涉及 app_shell.dart 的 Drawer 配置。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
<li class="todo-card s-done"
data-id="26"
data-level="low"
data-status="done"
data-tier=""
data-tags="前端,iOS,Android">
<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-low">一般 / 优化</span>
<button class="reject-btn" data-id="26" data-title="入库单详情商品明细改为卡片式竖排布局">拒绝验收</button>
</div>
</div>
<div class="item-desc">当前入库单详情的商品明细以横向表格展示,字段多时在窄屏上挤压严重。改为每件商品独占一个卡片区块,字段竖向排列(商品名、规格、数量、单价、产地、保质期等各占一行),信息层次清晰,移动端阅读体验更好。宽屏可保持表格,窄屏自动切换为卡片流(复用 MobileListCard 模式)。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
</div>
</div>
</li>
</ul>
</div>
</div>
<div class="section-block" id="section-accepted">
<div class="section-title st-accepted" data-toggle="accepted">
✅ 已验收 <span class="s-count">14</span>
<span class="s-arrow">▾ 展开</span>
</div>
<div class="section-list-wrap collapsed" id="list-wrap-accepted">
<ul class="todo-list" id="list-accepted">
<li class="todo-card s-accepted"
data-id="13"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="后端,iOS,Android">
<div class="card-header">
<span class="item-title">公开商品 API 补充 batch.quantity 和 product code/barcode</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">Flutter _AuthenticityCard 读取 batch['quantity'] 但后端 public.go batchData 未返回 quantity。另 product code/barcode 未暴露,参数卡无法显示商品编码。handler/public.go GetProduct</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.25</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="6"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">注册成功提示路径需核对</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">提示「忘记门店编码可在「系统设置→关于」查看」,需与实际客户端菜单路径一致。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.25</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="5"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">页脚双份维护</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">njk 站点用 footer.njk 读变量,功能页把页脚硬编码各写一份,改一处不同步。根因同「两套模板」。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.25</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="3"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">注册接口同源依赖问题</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">用 window.location.origin+'/api/v1/public/register',要求同源部署;分离部署时指错。需确认部署形态。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.25</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="18"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">基础数据管理 UI 增加 4 个字典维护 Tab</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">products_screen.dart 基础数据管理页增加产地/保质期/储存方式/描述文档 4 个维护 Tab,复用现有 option 维护组件(增删改查);描述文档 Tab 含 title+content 多行输入</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.24</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="14"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">Footer「商品报错」「意见反馈」补充实际功能</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">public_product_screen.dart _Footer 底部两个链接无 url/action,点击静默。需设计方案:跳转反馈表单、弹出联系方式、或发邮件。line:1353-1355</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.24</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="10"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,后端,iOS,Android">
<div class="card-header">
<span class="item-title">公开商品 API 补充 sale_price 并在页面展示价格</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">product model 有 sale_price 字段但 public.go GetProduct 未返回。需:1) 后端 batchData/productData 加 sale_price2) Flutter _TitleBlock 或单独区块展示建议零售价。client: public_product_screen.dart, backend: handler/public.go</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> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.23</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="9"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">设计并实现添加门店微信功能</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">PublicProductScreen _ShopCard「添加门店微信」按钮 onPressed: () {} 为空。需设计方案:门店维护微信号字段 or 二维码图片,扫码页展示。涉及后端 shop model 扩展 + 前端展示。client/lib/screens/public/public_product_screen.dart:1255</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.23</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="8"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">补全「查看本店其他商品」功能</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">PublicProductScreen _ShopCard 底部按钮 onPressed: () {} 为空,需实现跳转至该门店商品列表页。client/lib/screens/public/public_product_screen.dart:1240</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.23</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="17"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,iOS,Android">
<div class="card-header">
<span class="item-title">入库表单关联 4 个商品属性字典</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">stock_in_form_screen.dart 生产日期字段附近增加产地/保质期/储存方式/描述文档 4 个下拉选择(数据来自字典 List 接口);FindOrCreate/Update 接受并写入 4 个 id;前端新增对应 API client 方法</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.23</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="16"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="后端,前端,iOS,Android">
<div class="card-header">
<span class="item-title">public.go 返回产地/保质期/储存/介绍,公开页去硬编码</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">public.go GetProduct Preload 4 张新表 + 三级回退(描述文档→旧Description→通用中性兜底)+ 默认值常量(保质期/储存方式);Flutter public_product_screen.dart _ParamsCard/_DescriptionSection 改读真实数据,移除茅台 mock</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> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.23</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="15"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="后端,数据库">
<div class="card-header">
<span class="item-title">4 张商品属性字典表 + 后端 CRUD</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">新增 product_origin_options/product_shelf_life_options/product_storage_options/product_description_docs 4 张字典表(TenantBase+shop_id 隔离);Product 增加 4 个可空外键;AutoMigrate 注册;handler/product_attr.go CRUD16 个方法);router.go 追加 4 组路由在 /product-options/ 下</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-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.23</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="4"
data-level="low"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">scan.html / features 走 passthrough 不享模板</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-low">一般 / 优化</span>
</div>
</div>
<div class="item-desc">features/scan.html 直接复制,不经 njk 模板,页脚/备案/CSS 都得手动同步。建议长期改造为模板化。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.23</span></span>
</div>
</div>
</li>
<li class="todo-card s-accepted"
data-id="1"
data-level="mid"
data-status="accepted"
data-tier=""
data-tags="前端,Web">
<div class="card-header">
<span class="item-title">两套 CSS / 设计令牌并存</span>
<div class="card-badges">
<span class="tag status-badge s-accepted">已验收</span>
<span class="tag t-high">重要</span>
</div>
</div>
<div class="item-desc">主站 njk 用 color.css+style.css,功能页用 colors_and_type.css,导航/页脚各写一份,维护分裂。建议合并到一套,功能页走 base.njk 模板。</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span></div>
<div class="item-meta">
<span class="meta-date">🕐 2026-06-08</span>
<span class="meta-date">✅ 验收 2026-06-08 · <span class="ver-badge">v1.0.23</span></span>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
<!-- 拒绝验收 Modal -->
<div id="reject-modal" class="modal-overlay" style="display:none">
<div class="modal-box">
<h3>拒绝验收</h3>
<p class="modal-subtitle" id="reject-modal-subtitle"></p>
<label class="modal-label">拒绝原因<span class="req">*</span></label>
<textarea class="modal-textarea" id="reject-reason-input" rows="3" placeholder="请说明具体问题,Claude 将以此为依据修复…"></textarea>
<div class="modal-err" id="reject-reason-err">请填写拒绝原因</div>
<div id="reject-cmd-wrap" class="modal-cmd-wrap" style="display:none">
<div class="modal-cmd-label">在终端运行以下命令:</div>
<code class="modal-cmd-code" id="reject-cmd-text"></code>
<button class="modal-copy-btn" id="reject-copy-btn">复制命令</button>
</div>
<div class="modal-actions">
<button class="modal-btn modal-btn-cancel" id="reject-cancel-btn">取消</button>
<button class="modal-btn modal-btn-reject" id="reject-confirm-btn">确认拒绝</button>
</div>
</div>
</div>
<script>
(function() {
let curLevel = 'all';
let curStatus = 'all';
let curTier = 'all';
let curTags = new Set();
function applyFilter() {
document.querySelectorAll('.todo-card').forEach(card => {
const lvl = card.dataset.level;
const status = card.dataset.status;
const tier = card.dataset.tier;
const tags = card.dataset.tags ? card.dataset.tags.split(',') : [];
let show = true;
if (curLevel !== 'all' && lvl !== curLevel) show = false;
if (curStatus !== 'all' && status !== curStatus) show = false;
if (curTier !== 'all' && String(tier) !== curTier) show = false;
if (curTags.size > 0 && ![...curTags].some(t => tags.includes(t))) show = false;
card.classList.toggle('hidden', !show);
});
if (curStatus !== 'all') {
const wrap = document.getElementById('list-wrap-' + curStatus);
if (wrap) {
wrap.classList.remove('collapsed');
const title = document.querySelector('[data-toggle="' + curStatus + '"] .s-arrow');
if (title) title.textContent = '▴ 收起';
}
}
}
document.querySelectorAll('[data-filter-level]').forEach(btn => {
btn.addEventListener('click', () => {
curLevel = btn.dataset.filterLevel;
document.querySelectorAll('[data-filter-level]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
document.querySelectorAll('[data-filter-tier]').forEach(btn => {
btn.addEventListener('click', () => {
curTier = btn.dataset.filterTier;
document.querySelectorAll('[data-filter-tier]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
document.querySelectorAll('[data-filter-status]').forEach(btn => {
btn.addEventListener('click', () => {
curStatus = btn.dataset.filterStatus;
document.querySelectorAll('[data-filter-status]').forEach(b => b.classList.toggle('active', b === btn));
applyFilter();
});
});
document.querySelectorAll('[data-filter-tag]').forEach(btn => {
btn.addEventListener('click', () => {
const t = btn.dataset.filterTag;
if (curTags.has(t)) { curTags.delete(t); btn.classList.remove('active'); }
else { curTags.add(t); btn.classList.add('active'); }
applyFilter();
});
});
document.querySelectorAll('.t-tag').forEach(tag => {
tag.addEventListener('click', e => {
e.stopPropagation();
const t = tag.dataset.tag;
const chip = document.querySelector('[data-filter-tag="'+t+'"]');
if (chip) chip.click();
});
});
document.querySelectorAll('.section-title[data-toggle]').forEach(title => {
title.addEventListener('click', () => {
const st = title.dataset.toggle;
const wrap = document.getElementById('list-wrap-' + st);
const arrow = title.querySelector('.s-arrow');
if (!wrap) return;
const collapsed = wrap.classList.toggle('collapsed');
if (arrow) arrow.textContent = collapsed ? '▾ 展开' : '▴ 收起';
});
});
// ── 拒绝验收 Modal ──
const modal = document.getElementById('reject-modal');
const reasonInput = document.getElementById('reject-reason-input');
const reasonErr = document.getElementById('reject-reason-err');
const cmdWrap = document.getElementById('reject-cmd-wrap');
const cmdText = document.getElementById('reject-cmd-text');
const copyBtn = document.getElementById('reject-copy-btn');
let rejectId = null;
document.querySelectorAll('.reject-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
rejectId = btn.dataset.id;
document.getElementById('reject-modal-subtitle').textContent = '#' + rejectId + ' ' + btn.dataset.title;
reasonInput.value = '';
reasonInput.classList.remove('error');
reasonErr.classList.remove('show');
cmdWrap.style.display = 'none';
document.getElementById('reject-confirm-btn').style.display = '';
copyBtn.textContent = '复制命令';
modal.style.display = 'flex';
setTimeout(() => reasonInput.focus(), 80);
});
});
document.getElementById('reject-cancel-btn').addEventListener('click', () => {
modal.style.display = 'none';
});
modal.addEventListener('click', e => {
if (e.target === modal) modal.style.display = 'none';
});
document.getElementById('reject-confirm-btn').addEventListener('click', () => {
const reason = reasonInput.value.trim();
if (!reason) {
reasonInput.classList.add('error');
reasonErr.classList.add('show');
reasonInput.focus();
return;
}
const escaped = reason.replace(/\/g, '\\').replace(/"/g, '\"');
const cmd = 'node todo/todo.mjs reject ' + rejectId + ' --reason "' + escaped + '"';
cmdText.textContent = cmd;
cmdWrap.style.display = 'block';
document.getElementById('reject-confirm-btn').style.display = 'none';
});
reasonInput.addEventListener('input', () => {
if (reasonInput.value.trim()) {
reasonInput.classList.remove('error');
reasonErr.classList.remove('show');
}
});
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(cmdText.textContent).then(() => {
copyBtn.textContent = '已复制 ';
});
});
})();
</script>
</body>
</html>