docs: 历史导入出库价格错列修复复盘 runbook(2026-07-10)
Design Source Checks / design-source (push) Successful in 47s

根因/修复设计/执行结果/遗留脏数据清单/回滚方式,登记 docs/index.html

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-10 17:29:19 +08:00
parent e19c498dba
commit 658df690f6
2 changed files with 131 additions and 0 deletions
+1
View File
@@ -62,6 +62,7 @@
<ul>
<li><a href="runbooks/ec2-to-ali-migration.html">EC2 → 阿里云 迁移计划</a><span class="tag html">HTML</span> <span class="hint">— jiu 服务/数据/DB/文件/域名/CI 整体迁移(2026-07-02 已割接)</span></li>
<li><a href="runbooks/ddos-response.html">DDoS / 爬取洪峰应急</a><span class="tag html">HTML</span> <span class="hint">— 判定/CC 处置/高防·CDN 切换/黑洞预案(2026-07-05,配套安全三件套加固)</span></li>
<li><a href="runbooks/fix-history-prices-20260710.html">历史导入出库价格错列修复复盘</a><span class="tag html">HTML</span> <span class="hint">— 售价误落成本列根因/修复设计/26335 行执行结果/遗留脏数据清单/回滚(2026-07-10shop8 生产)</span></li>
</ul>
<h2>🧪 测试 · 审计报告</h2>
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>历史导入出库价格错列修复复盘(2026-07-10) — 酒库管理系统</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;}
pre .c{color:#7A8699;}
.tag{display:inline-block;font-size:11px;padding:1px 8px;border-radius:10px;font-weight:600;}
.tag.risk{background:var(--danger-bg);color:var(--danger);}
.tag.ok{background:var(--success-bg);color:var(--success);}
.tag.warn{background:var(--warn-bg);color:var(--warn);}
.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);}
.callout.ok{border-color:var(--success);background:var(--success-bg);}
.callout b{color:var(--ink);}
ul,ol{margin:6px 0;padding-left:22px;}
li{margin:3px 0;font-size:13px;}
</style>
</head>
<body>
<h1>历史导入出库价格错列修复复盘</h1>
<div class="sub">门店:鼎晟酒行(shop_id=8, S000008)· 生产库 ali/jiu_mysql · 2026-07-10 执行 · 修复工具 <code>backend/cmd/fix-history-prices</code> · commit <code>e19c498</code></div>
<div class="callout ok"><b>一句话结论:</b>2026-06-20 历史进销存导入(<code>cmd/import-history</code>)把旧系统<b>出库明细的「单价/金额」(售价口径)误写入成本列</b> <code>cost_price/cost_amount</code>,售价列留 0,导致全部历史出库单显示「待定价」、成本列实为售价。本次用一次性工具修复 <b>26,335 行明细 / 4,927 张出库单</b>:售价归位、成本按序列号回查真实进价回填、利润重算;应收合计不动,财务零影响。</div>
<h2>一、症状与发现路径</h2>
<div class="card lead">
<ul>
<li>用户报障:出库单详情里售价/总售价/利润整列「待定价」(首例即历史导入单,成本价 2,130 实为售价)。</li>
<li>关键实证(用户比对截图):出库单 <b>CK202606150001</b> 明细「成本价」14,300,而同一序列号 <b>ZXZ027628 茅台铁盖1995五星</b> 在库存/入库侧的真实进价是 <b>13,600</b>——出库"成本"14,300 就是卖给客户的售价。</li>
<li>旁证:该单 9 瓶 × 14,300 = 128,700 恰好等于单头「合计金额」(应收 <code>sale_total</code>)——单头是售价口径、明细却落在成本列,自相矛盾。</li>
</ul>
</div>
<h2>二、根因</h2>
<div class="card lead">
<p><code>backend/cmd/import-history/main.go</code> 的出库明细映射(修复前):</p>
<pre>CostPrice: parseFloatLoose(get(dr, dCols, "单价")), <span class="c">// ✗ 源出库「单价」是售价口径</span>
CostAmount: parseFloatLoose(get(dr, dCols, "金额")), <span class="c">// ✗ 同上;sale_price / sale_amount 留 0</span></pre>
<ul>
<li>旧系统出库单的「单价/金额」= 卖给客户的价(应收侧);入库单的「单价/金额」= 进价(应付侧)。<b>入库映射本来就对,只有出库错列。</b></li>
<li>单头处理却是对的:源「单据金额」→ <code>sale_total</code>(应收合计),因此财务 KPI(直接 SUM 单头列)一直正确,问题只在明细两列 + 利润。</li>
<li>显示层连锁:明细 <code>sale_price=0</code> → 前端按「待定价」渲染(<code>order_detail_drawer.dart</code> 口径 <code>sale_price&lt;=0</code>);成本列展示的是售价,管理员看到的"成本/利润"失真。</li>
</ul>
<p>映射已随修复同步改对(出库明细「单价/金额」→ <code>sale_price/sale_amount</code>,成本留 0 待回查),以后再导不会复发。</p>
</div>
<h2>三、修复设计(cmd/fix-history-prices</h2>
<div class="card lead">
<table>
<tr><th>要点</th><th>说明</th></tr>
<tr><td>修复范围</td><td>仅历史导入明细:<code>stock_out_items.product_id = 占位商品 HIST-PLACEHOLDERshop8 id=4809</code><code>sale_price=0 AND cost_price&gt;0</code><b>注意占位商品已被界面软删</b>(2026-06-20),工具定位时不过滤 <code>deleted_at</code></td></tr>
<tr><td>售价归位</td><td><code>sale_price ← 原 cost_price</code><code>sale_amount ← 原 cost_amount</code>(忠实保留源售价,不做任何归一)。</td></tr>
<tr><td>成本回填</td><td>按商品编号(序列号,一物一码)逐级回查:① <code>stock_in_items</code> 同店同编号入库价 → ② <code>inventories.unit_price</code> 库存快照价(快照列或经 product 关联)→ ③ <code>products.purchase_price</code>;均查不到 → 0(成本待定)。与库存列表展示链 <code>COALESCE(sii.cost_price, inv.unit_price, p.purchase_price)</code> 同序。</td></tr>
<tr><td>利润重算</td><td>涉及单据 <code>profit_total</code><code>service.recalcStockOutProfit</code> 同口径:<code>Σ(sale_price&gt;0 ? (sale-cost)×qty : 0)</code>。成本待定行暂按全售价计(与系统既有"成本待定"语义一致)。</td></tr>
<tr><td>应收不动</td><td><code>sale_total</code> 保持源单头合计——财务应收/KPI 直接 SUM 单头列,<b>修复对财务零影响、无需冲账</b>。修复后校验 Σ明细售价小计 vs 单头合计,不一致只报不改。</td></tr>
<tr><td>安全机制</td><td>默认 dry-run(事务内试算后回滚);<code>--apply</code> 才落库;幂等(已修行 <code>sale_price&gt;0</code> 自动跳过,可重跑)。SQLite 内存库单测覆盖四种成本来源 / dry-run 回滚 / 幂等 / 非占位明细不波及 / 占位商品软删仍可定位。</td></tr>
</table>
<pre><span class="c"># 用法(本机交叉编译 → scp 到 ali 宿主执行;jiu_mysql 容器映射 127.0.0.1:3306</span>
cd backend && GOOS=linux GOARCH=amd64 go build -o /tmp/fix-history-prices ./cmd/fix-history-prices
scp /tmp/fix-history-prices ali:/tmp/
ssh ali '/tmp/fix-history-prices --shop-id 8 --env-file /opt/jiu/config/production.env' <span class="c"># dry-run</span>
ssh ali '/tmp/fix-history-prices --shop-id 8 --env-file /opt/jiu/config/production.env --apply' <span class="c"># 落库</span></pre>
</div>
<h2>四、执行记录与结果(2026-07-10)</h2>
<div class="card lead">
<table>
<tr><th>步骤</th><th>结果</th></tr>
<tr><td>1. 全库备份(改库前)</td><td><span class="tag ok">完成</span> 本机 <code>~/jiu-db-backups/jiu_db_prefix_fix_20260710.sql.gz</code>2.8MBgzip -t 校验通过)</td></tr>
<tr><td>2. dry-run</td><td>待修 26,335 行 / 4,927 单;成本回填:入库价 25,139 / 库存价 32 / 商品进价 0 / 查无 1,164;合计不一致单 2 张</td></tr>
<tr><td>3. --apply 落库</td><td><span class="tag ok">完成</span> 数字与 dry-run 完全一致</td></tr>
<tr><td>4. 核验</td><td><span class="tag ok">全绿</span> 待修行剩 0;抽查 ZXZ027628:成本 13,600 / 售价 14,300 ✓;CK202606150001:应收 128,700 / 利润 22,900 ✓</td></tr>
</table>
<p>成本回填命中率 95.5%25,139/26,335 走入库明细价,最精确来源);1,164 行查无进价 = 该序列号在导出的历史窗口内无入库记录(更早的老库存),成本置 0 待定。</p>
</div>
<h2>五、遗留脏数据清单(源数据问题,忠实保留,待业务决策)</h2>
<div class="card lead">
<table>
<tr><th>#</th><th>问题</th><th>影响</th><th>建议处置</th></tr>
<tr><td>1</td><td><span class="tag risk">数量脏</span> <b>CK202403050009</b> 行 ZXZ010370 茅台白皮2003 <b>数量=50,902</b>(该序列号入库仅 1 瓶),明细金额 2.62 亿</td><td>该单 <code>profit_total</code> 虚高至 3,312 万;单头应收 542,950 正常</td><td>业务核实真实数量(大概率 1)后单行修正并重算该单利润。同族问题见「装箱数当数量」脏数据(库存侧 784 行,另案待决策)</td></tr>
<tr><td>2</td><td><span class="tag warn">合计差</span> CK202401170005 单头 95,400 vs 明细合计 95,500(差 100</td><td>轻微,应收按单头</td><td>可不处理;或对账时人工核</td></tr>
<tr><td>3</td><td><span class="tag warn">零额单</span> 出库 47 张 / 入库 13 张 合计 ≤ 0</td><td>可能是赠送/内部调拨/源缺失</td><td>保留原貌</td></tr>
<tr><td>4</td><td><span class="tag warn">成本待定</span> 出库明细 1,164 行查无进价(cost=0);入库明细 59 行进价=0</td><td>所在单利润暂按全售价计,偏高</td><td>后续补录进价可自动纠正利润(确认进价/再跑回填)</td></tr>
</table>
</div>
<h2>六、回滚方式</h2>
<div class="card lead">
<div class="callout danger"><b>回滚会连带覆盖备份时刻之后的所有新业务数据</b>,仅在修复本身被证实有误时使用;局部纠错优先用幂等工具或单行 SQL。</div>
<pre><span class="c"># 全库恢复(备份 = 修复前全库快照)</span>
gunzip -c ~/jiu-db-backups/jiu_db_prefix_fix_20260710.sql.gz | ssh ali 'PW=...; docker exec -i -e MYSQL_PWD="$PW" jiu_mysql mysql -uroot jiu_db'</pre>
<p>密码取法同 <code>scripts/ci/backup-db.sh</code>:从 <code>/opt/jiu/config/production.env</code><code>DATABASE_DSN</code> 解析。</p>
</div>
<h2>七、关联索引</h2>
<div class="card lead">
<ul>
<li>修复工具与测试:<code>backend/cmd/fix-history-prices/</code>main.go + main_test.go</li>
<li>导入映射修正:<code>backend/cmd/import-history/main.go</code>(出库明细改落 sale 列)</li>
<li>提交:<code>e19c498</code> fix(backend): 历史导入出库明细价格错列——售价误落成本列,新增修复工具</li>
<li>历次同族修复:库存商品编号重复(2026-06-21,<code>cmd/fix-inventory-products</code>,按名合并 → 按编号重指);导入实跑记录见项目记忆 <code>project_history_import</code></li>
<li>定价字段口径(cost/sale 分列、待定价、利润公式):见项目 <code>CLAUDE.md</code>「定价字段口径」节与开发手册</li>
</ul>
</div>
</body>
</html>