backend/cmd/fix-history-prices · commit e19c498cmd/import-history)把旧系统出库明细的「单价/金额」(售价口径)误写入成本列 cost_price/cost_amount,售价列留 0,导致全部历史出库单显示「待定价」、成本列实为售价。本次用一次性工具修复 26,335 行明细 / 4,927 张出库单:售价归位、成本按序列号回查真实进价回填、利润重算;应收合计不动,财务零影响。sale_total)——单头是售价口径、明细却落在成本列,自相矛盾。backend/cmd/import-history/main.go 的出库明细映射(修复前):
CostPrice: parseFloatLoose(get(dr, dCols, "单价")), // ✗ 源出库「单价」是售价口径 CostAmount: parseFloatLoose(get(dr, dCols, "金额")), // ✗ 同上;sale_price / sale_amount 留 0
sale_total(应收合计),因此财务 KPI(直接 SUM 单头列)一直正确,问题只在明细两列 + 利润。sale_price=0 → 前端按「待定价」渲染(order_detail_drawer.dart 口径 sale_price<=0);成本列展示的是售价,管理员看到的"成本/利润"失真。映射已随修复同步改对(出库明细「单价/金额」→ sale_price/sale_amount,成本留 0 待回查),以后再导不会复发。
| 要点 | 说明 |
|---|---|
| 修复范围 | 仅历史导入明细: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_price、sale_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_total 按 service.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' # 落库
| 步骤 | 结果 |
|---|---|
| 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 | 所在单利润暂按全售价计,偏高 | 后续补录进价可自动纠正利润(确认进价/再跑回填) |
# 全库恢复(备份 = 修复前全库快照)
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.env 的 DATABASE_DSN 解析。
backend/cmd/fix-history-prices/(main.go + main_test.go)backend/cmd/import-history/main.go(出库明细改落 sale 列)e19c498 fix(backend): 历史导入出库明细价格错列——售价误落成本列,新增修复工具cmd/fix-inventory-products,按名合并 → 按编号重指);导入实跑记录见项目记忆 project_history_importCLAUDE.md「定价字段口径」节与开发手册