历史导入出库价格错列修复复盘

门店:鼎晟酒行(shop_id=8, S000008)· 生产库 ali/jiu_mysql · 2026-07-10 执行 · 修复工具 backend/cmd/fix-history-prices · commit e19c498
一句话结论:2026-06-20 历史进销存导入(cmd/import-history)把旧系统出库明细的「单价/金额」(售价口径)误写入成本列 cost_price/cost_amount,售价列留 0,导致全部历史出库单显示「待定价」、成本列实为售价。本次用一次性工具修复 26,335 行明细 / 4,927 张出库单:售价归位、成本按序列号回查真实进价回填、利润重算;应收合计不动,财务零影响。

一、症状与发现路径

二、根因

backend/cmd/import-history/main.go 的出库明细映射(修复前):

CostPrice:   parseFloatLoose(get(dr, dCols, "单价")),   // ✗ 源出库「单价」是售价口径
CostAmount:  parseFloatLoose(get(dr, dCols, "金额")),   // ✗ 同上;sale_price / sale_amount 留 0

映射已随修复同步改对(出库明细「单价/金额」→ sale_price/sale_amount,成本留 0 待回查),以后再导不会复发。

三、修复设计(cmd/fix-history-prices)

要点说明
修复范围仅历史导入明细:stock_out_items.product_id = 占位商品 HIST-PLACEHOLDER(shop8 id=4809)sale_price=0 AND cost_price>0注意占位商品已被界面软删(2026-06-20),工具定位时不过滤 deleted_at
售价归位sale_price ← 原 cost_pricesale_amount ← 原 cost_amount(忠实保留源售价,不做任何归一)。
成本回填按商品编号(序列号,一物一码)逐级回查:① stock_in_items 同店同编号入库价 → ② inventories.unit_price 库存快照价(快照列或经 product 关联)→ ③ products.purchase_price;均查不到 → 0(成本待定)。与库存列表展示链 COALESCE(sii.cost_price, inv.unit_price, p.purchase_price) 同序。
利润重算涉及单据 profit_totalservice.recalcStockOutProfit 同口径:Σ(sale_price>0 ? (sale-cost)×qty : 0)。成本待定行暂按全售价计(与系统既有"成本待定"语义一致)。
应收不动sale_total 保持源单头合计——财务应收/KPI 直接 SUM 单头列,修复对财务零影响、无需冲账。修复后校验 Σ明细售价小计 vs 单头合计,不一致只报不改。
安全机制默认 dry-run(事务内试算后回滚);--apply 才落库;幂等(已修行 sale_price>0 自动跳过,可重跑)。SQLite 内存库单测覆盖四种成本来源 / dry-run 回滚 / 幂等 / 非占位明细不波及 / 占位商品软删仍可定位。
# 用法(本机交叉编译 → scp 到 ali 宿主执行;jiu_mysql 容器映射 127.0.0.1:3306)
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'          # dry-run
ssh ali '/tmp/fix-history-prices --shop-id 8 --env-file /opt/jiu/config/production.env --apply'  # 落库

四、执行记录与结果(2026-07-10)

步骤结果
1. 全库备份(改库前)完成 本机 ~/jiu-db-backups/jiu_db_prefix_fix_20260710.sql.gz(2.8MB,gzip -t 校验通过)
2. dry-run待修 26,335 行 / 4,927 单;成本回填:入库价 25,139 / 库存价 32 / 商品进价 0 / 查无 1,164;合计不一致单 2 张
3. --apply 落库完成 数字与 dry-run 完全一致
4. 核验全绿 待修行剩 0;抽查 ZXZ027628:成本 13,600 / 售价 14,300 ✓;CK202606150001:应收 128,700 / 利润 22,900 ✓

成本回填命中率 95.5%(25,139/26,335 走入库明细价,最精确来源);1,164 行查无进价 = 该序列号在导出的历史窗口内无入库记录(更早的老库存),成本置 0 待定。

五、遗留脏数据清单(源数据问题,忠实保留,待业务决策)

#问题影响建议处置
1数量脏 CK202403050009 行 ZXZ010370 茅台白皮2003 数量=50,902(该序列号入库仅 1 瓶),明细金额 2.62 亿该单 profit_total 虚高至 3,312 万;单头应收 542,950 正常业务核实真实数量(大概率 1)后单行修正并重算该单利润。同族问题见「装箱数当数量」脏数据(库存侧 784 行,另案待决策)
2合计差 CK202401170005 单头 95,400 vs 明细合计 95,500(差 100)轻微,应收按单头可不处理;或对账时人工核
3零额单 出库 47 张 / 入库 13 张 合计 ≤ 0可能是赠送/内部调拨/源缺失保留原貌
4成本待定 出库明细 1,164 行查无进价(cost=0);入库明细 59 行进价=0所在单利润暂按全售价计,偏高后续补录进价可自动纠正利润(确认进价/再跑回填)

六、回滚方式

回滚会连带覆盖备份时刻之后的所有新业务数据,仅在修复本身被证实有误时使用;局部纠错优先用幂等工具或单行 SQL。
# 全库恢复(备份 = 修复前全库快照)
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'

密码取法同 scripts/ci/backup-db.sh:从 /opt/jiu/config/production.envDATABASE_DSN 解析。

七、关联索引