73955a139c
- inventories 加 status 枚举列(默认 stock,AutoMigrate 存量即全部「库存」)+ 索引 - 出库审核扣光置 sold 不再软删(留痕台账);退单恢复行置回 stock;盘亏仍软删 - 新接口 PUT /inventory/:id/status(仅收 stock/on_sale,已售行拒改,租户校验) - List 默认排除已售(显式 status=sold 才显示)+ status 多值筛选 - Summary:SKU/货值/数量排除已售,新增 sold_count - 公开店铺页/API 只列 on_sale——酒单从全量裸奔改为人工精选 - 测试:卖光置售/部分不改/退单回库/接口边界/跨租户/汇总口径 8 用例 - 设计方案 docs/design/inventory-sale-status.html + db-schema/CLAUDE.md 同步 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
130 lines
11 KiB
HTML
130 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>库存三态改造(在售/库存/已售)设计方案 — 酒库管理系统</title>
|
||
<style>
|
||
:root{
|
||
--primary:#2563AC; --primary-dark:#154072; --danger:#D14343; --danger-bg:#FDECEC;
|
||
--success:#2E8B57; --success-bg:#E6F3EC; --warn:#B45309; --warn-bg:#FFF4E5; --accent:#8B2331;
|
||
--ink:#232934; --muted:#6E7888; --border:#DCE2EB; --paper:#F5F7FA; --head:#F0F4FF;
|
||
}
|
||
*{box-sizing:border-box;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
|
||
body{margin:0;background:var(--paper);color:var(--ink);padding:28px;line-height:1.65;}
|
||
h1{font-size:22px;margin:0 0 4px;}
|
||
h2{font-size:17px;margin:30px 0 12px;color:var(--primary-dark);border-bottom:2px solid var(--head);padding-bottom:6px;}
|
||
h3{font-size:14px;margin:18px 0 8px;color:var(--accent);}
|
||
.sub{color:var(--muted);font-size:13px;margin-bottom:18px;}
|
||
.card{background:#fff;border:1px solid var(--border);border-radius:10px;padding:16px 20px;margin:14px 0;}
|
||
.lead{font-size:13.5px;color:var(--ink);}
|
||
table{width:100%;border-collapse:collapse;font-size:13px;margin:8px 0;}
|
||
th{background:var(--head);color:var(--primary-dark);font-weight:600;font-size:12px;text-align:left;padding:9px 10px;border-bottom:1px solid var(--border);}
|
||
td{padding:8px 10px;border-bottom:1px solid #EEF1F5;vertical-align:top;}
|
||
.mono{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;}
|
||
code{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#EEF2F8;padding:1px 5px;border-radius:4px;color:var(--primary-dark);}
|
||
pre{background:#1E2430;color:#D8E0EC;border-radius:8px;padding:12px 14px;overflow:auto;font-size:12px;line-height:1.55;}
|
||
.tag{display:inline-block;font-size:11px;padding:1px 8px;border-radius:10px;font-weight:600;}
|
||
.tag.ok{background:var(--success-bg);color:var(--success);}
|
||
.tag.info{background:var(--head);color:var(--primary);}
|
||
.tag.gray{background:#EEF1F5;color:var(--muted);}
|
||
.callout{border-left:4px solid var(--primary);background:#F0F6FF;padding:10px 14px;border-radius:4px;margin:12px 0;font-size:13px;}
|
||
.callout.danger{border-color:var(--danger);background:var(--danger-bg);}
|
||
.callout.warn{border-color:var(--warn);background:var(--warn-bg);}
|
||
ul,ol{margin:6px 0;padding-left:22px;}
|
||
li{margin:3px 0;font-size:13px;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>库存三态改造:在售 / 库存 / 已售</h1>
|
||
<div class="sub">2026-07-07 设计 · 用户需求:库存行持久化三态 + 公开酒单只挂「在售」+ 卖光留痕「已售」+ 抽屉 switch 切换(桌面/移动)</div>
|
||
|
||
<h2>一、现状与目标</h2>
|
||
<div class="card lead">
|
||
<p><b>现状</b>:库存「状态」是显示时派生的(在售/预警/缺货 = 数量 vs 安全库存),数据库无字段;出库扣减到 0 的批次被<b>软删除</b>,从列表彻底消失;公开店铺页展示<b>全部</b>有库存商品(3841 件全裸奔)。</p>
|
||
<p><b>目标</b>:状态落库为三态,门店可控地把商品挂上/撤下公开酒单,卖光的序列号留痕可查。</p>
|
||
<table>
|
||
<tr><th>状态</th><th>DB 值</th><th>语义</th><th>进入方式</th><th>公开酒单</th></tr>
|
||
<tr><td><span class="tag info">库存</span></td><td class="mono">stock</td><td>在库、不对外展示(默认)</td><td>入库审核默认 / switch 撤售 / 退单恢复</td><td>不显示</td></tr>
|
||
<tr><td><span class="tag ok">在售</span></td><td class="mono">on_sale</td><td>在库、公开酒单可见</td><td>抽屉 switch 手动挂售</td><td><b>显示</b></td></tr>
|
||
<tr><td><span class="tag gray">已售</span></td><td class="mono">sold</td><td>出库卖光,留痕台账</td><td>出库审核扣减到 0 自动置入(<b>不再软删</b>)</td><td>不显示</td></tr>
|
||
</table>
|
||
<p>流转:<code>stock ⇄ on_sale</code>(手动)→ <code>sold</code>(出库审核自动,不可手动进出);出库<b>退单</b>恢复数量时置回 <code>stock</code>(保守,需重新手动挂售)。盘点<b>盘亏</b>归零维持现状软删(灭失≠售出,不算已售)。</p>
|
||
</div>
|
||
|
||
<div class="callout danger"><b>⚠️ 上线即时影响,需确认:</b>存量 3841 行全部迁移为「库存」(你拍板的口径)→ <b>公开店铺页上线瞬间清空</b>,之后靠门店逐个把商品切到「在售」才会出现在酒单里。这是预期行为(酒单从「全量裸奔」变「人工精选」),但要提前告知门店有这步运营动作。</div>
|
||
|
||
<h2>二、数据模型</h2>
|
||
<div class="card lead">
|
||
<ul>
|
||
<li><code>inventories</code> 加列:<code>status enum('stock','on_sale','sold') NOT NULL DEFAULT 'stock'</code> + 索引 <code>idx_inv_shop_status(shop_id, status)</code>。<code>schema.sql</code> 与 model 同步,AutoMigrate 自动加列,存量行落默认值 <code>stock</code>——「现在所有状态改成库存」零迁移脚本达成。</li>
|
||
<li><b>为什么不走 custom_fields JSON</b>(破例说明):这是参与列表筛选、公开页过滤、KPI 统计的核心查询列,需要索引与枚举约束,JSON 扩展列不适用。</li>
|
||
<li>软删语义收窄:<code>deleted_at</code> 今后只表示「行不存在」(盘亏灭失/数据修正),「卖光」不再软删。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>三、后端改动</h2>
|
||
<div class="card lead">
|
||
<table>
|
||
<tr><th>#</th><th>位置</th><th>改动</th></tr>
|
||
<tr><td>1</td><td class="mono">model/stock.go + schema/schema.sql</td><td>Inventory 加 Status 字段;<code>docs/db-schema.html</code> 同步</td></tr>
|
||
<tr><td>2</td><td class="mono">service/stock.go:414(出库 FIFO)</td><td>批次扣到 0:<code>quantity=0 + status='sold'</code>,<b>不再打 deleted_at</b>;部分扣减不改状态</td></tr>
|
||
<tr><td>3</td><td class="mono">service/stock.go(出库退单)</td><td>恢复数量的行若为 sold → 置回 <code>stock</code>(并清 quantity 归还)</td></tr>
|
||
<tr><td>4</td><td>新接口</td><td><code>PUT /api/v1/inventory/:id/status</code>,body <code>{"status":"on_sale"|"stock"}</code>——只收这两值(sold 拒 400);shop_id 归属校验;受 ReadOnly() 保护,operator 可用(店员挂酒单是日常操作)</td></tr>
|
||
<tr><td>5</td><td>库存 List</td><td>返回 <code>status</code>;新增 <code>status=</code> 筛选参数(可多值逗号分隔);<b>默认排除已售</b>(2026-07-07 用户拍板:仅点「已售」KPI 卡或状态筛选选已售时才显示),显式传 <code>status=sold</code> 才返回已售行</td></tr>
|
||
<tr><td>6</td><td>库存 Summary</td><td><code>sku_count / in_stock_qty / stock_value / 月初快照</code> 一律排除 sold;新增 <code>sold_count</code>;<code>shortage_count</code>(qty≤0 未删行)语义被 sold 取代 → 保留字段但预期归零</td></tr>
|
||
<tr><td>7</td><td>公开接口 + SSR 店铺页</td><td><code>ListShopProducts</code>/<code>queryShopProducts</code> 的库存子查询加 <code>status='on_sale'</code>(qty>0 保留)——公开酒单只剩在售;<b>商品单页不过滤</b>(历史标签扫码永远可看,已售商品批次块自然消失)</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<h2>四、设计系统(L1 先行——switch 组件不存在)</h2>
|
||
<div class="card lead">
|
||
<ol>
|
||
<li>原型 <code>.superpowers/prototype/atoms.css</code> 登记 <code>.switch</code> 组件(38×22 pill 轨道 + 圆点滑块,on=<code>--primary</code>、off=<code>--border</code>,禁用态 faint;全走 tokens)+ <code>index.html</code> 组件清单登记;<code>mobile-atoms.css</code> 如需尺寸变体一并登记。</li>
|
||
<li>Flutter 对照新建 <code>client/lib/widgets/ds/ds_switch.dart</code>(颜色单源过 <code>check_ds_code</code> 闸)。</li>
|
||
<li>状态徽章三态色复用现有 DsBadge tone:在售=ok 绿、库存=info 蓝;「已售」需中性灰 tone——确认 DsBadge 有无 neutral,无则原型 badge 先补 neutral 变体再同步。</li>
|
||
<li>原型 <code>inventory.html / m-inventory.html</code>(同步态屏)状态列/徽章文案同步为三态。</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<h2>五、前端改动(client)</h2>
|
||
<div class="card lead">
|
||
<table>
|
||
<tr><th>#</th><th>位置</th><th>改动</th></tr>
|
||
<tr><td>1</td><td class="mono">models/inventory.dart</td><td>加 <code>status</code> 解析(缺省 'stock' 容错旧响应)</td></tr>
|
||
<tr><td>2</td><td>库存列表(桌面+移动)</td><td>状态列/卡片徽章改三态显示;状态筛选改「全部/在售/库存/已售」走后端参数;<b>预警/缺货退出状态列</b>——低于安全库存仍由数量列染色(既有 qty.low)承担;KPI 第四卡「缺货预警」改为「已售」(主数 sold_count,点击筛选已售;副行保留「需补货 N 项」预警数)</td></tr>
|
||
<tr><td>3</td><td class="mono">product_editor_drawer.dart</td><td>「状态」行 = 三态徽章 + <b>徽章后紧跟 DsSwitch</b>(在售⇄库存);已售时只显示灰徽章无 switch;切换调 PUT 接口,成功即时更新徽章并刷新列表;readonly 用户 WriteGuard 禁用。桌面 drawer 与移动 sheet 同组件,一处生效</td></tr>
|
||
<tr><td>4</td><td>_openEditor 调用</td><td>传入库存行 id 与 status(switch 操作对象=该 product 的库存行;序列号模型下 1 product≈1 行)</td></tr>
|
||
<tr><td>5</td><td>golden</td><td>库存列表桌面/移动 + 抽屉相关 golden 重录(管理员形态)</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<h2>六、兼容与发版顺序</h2>
|
||
<div class="card lead">
|
||
<ol>
|
||
<li><b>server-v1.1.10</b> 先发:AutoMigrate 加列默认 stock;旧 client 无 switch、仍显示旧派生状态(升级窗口内两套口径并存,可接受);公开酒单即刻只剩在售(上线即空,见顶部提醒)。</li>
|
||
<li><b>client-v1.1.6</b> 跟发:三态列表 + 抽屉 switch。</li>
|
||
<li>回滚:server 回退上一 tag——新列留在库里无害;已置 sold 的行回退后在旧逻辑下表现为「qty=0 未删」即旧「缺货」态,数据不损。</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<h2>七、测试与验收</h2>
|
||
<div class="card lead">
|
||
<ul>
|
||
<li>单测:出库审核卖光 → 行 status=sold 且 deleted_at 为空;部分出库不改状态;退单恢复 → 回 stock;状态接口(合法值/拒 sold/跨租户 404/readonly 403);公开列表与 SSR 店铺页只含 on_sale;Summary 排除 sold + sold_count 正确。</li>
|
||
<li>手工验收:抽屉切「在售」→ 公开酒单出现该商品;出库卖光 → 列表变「已售」灰徽章、酒单消失、KPI 已售 +1;已售商品抽屉无 switch;手机端 sheet 内 switch 可操作。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>八、明确不做(本期)</h2>
|
||
<div class="card lead">
|
||
<ul>
|
||
<li>不做「批量挂售」(列表多选批量切在售)——首期逐个切,量大再加。</li>
|
||
<li>不做已售行的自动归档/清理——台账语义,先留着观察增长。</li>
|
||
<li>不动商品公开单页的可见性——历史标签永远可扫。</li>
|
||
<li>盘亏归零不置已售——维持软删。</li>
|
||
</ul>
|
||
</div>
|
||
</body>
|
||
</html>
|