From e19c498dba0cf5859bc1e81b865353ed852845e7 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 10 Jul 2026 16:51:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(backend):=20=E5=8E=86=E5=8F=B2=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=87=BA=E5=BA=93=E6=98=8E=E7=BB=86=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E9=94=99=E5=88=97=E2=80=94=E2=80=94=E5=94=AE=E4=BB=B7=E8=AF=AF?= =?UTF-8?q?=E8=90=BD=E6=88=90=E6=9C=AC=E5=88=97=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:旧系统出库明细「单价/金额」是售价口径(应收侧),import-history 误落 cost_price/cost_amount,sale_price 留 0(整单显示待定价);单头 sale_total 却按售价口径正确落库,明细与单头矛盾。实证:ZXZ027628 出库"成本"14300=售价, 真实进价 13600 在库存/入库侧。 - import-history:出库明细「单价/金额」改落 sale_price/sale_amount(成本留 0 待回查) - 新增 cmd/fix-history-prices 一次性修复线上数据:售价归位 + 按商品编号 回查真实进价(①入库明细 ②库存快照 ③商品进价)+ profit_total 同口径重算; sale_total 不动(应收不变);默认 dry-run,--apply 落库,幂等可重跑 - 测试:四种成本来源/dry-run 回滚/幂等/非占位明细不波及/利润应收口径 Co-Authored-By: Claude Fable 5 --- backend/cmd/fix-history-prices/main.go | 268 ++++++++++++++++++++ backend/cmd/fix-history-prices/main_test.go | 198 +++++++++++++++ backend/cmd/import-history/main.go | 10 +- 3 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 backend/cmd/fix-history-prices/main.go create mode 100644 backend/cmd/fix-history-prices/main_test.go diff --git a/backend/cmd/fix-history-prices/main.go b/backend/cmd/fix-history-prices/main.go new file mode 100644 index 0000000..110f943 --- /dev/null +++ b/backend/cmd/fix-history-prices/main.go @@ -0,0 +1,268 @@ +// fix-history-prices —— 一次性修复 import-history 出库明细的价格错列。 +// +// 根因:旧系统出库单明细的「单价/金额」是售价口径(卖给客户的价,应收侧), +// import-history 却把它落到了 cost_price/cost_amount(成本列),sale_price/ +// sale_amount 留 0(整单显示「待定价」);而单头合计已按售价口径落 sale_total, +// 明细与单头自相矛盾。实证:ZXZ027628 出库明细"成本"14300 = 售价, +// 真实进价 13600 在库存/入库侧。 +// +// 修复(仅动历史导入明细,即 product_id 指向 HIST-PLACEHOLDER 占位商品的行): +// - sale_price ← 原 cost_price、sale_amount ← 原 cost_amount(源售价归位) +// - cost_price/cost_amount ← 按商品编号(序列号,一物一码)回查真实进价: +// ① stock_in_items 同店同编号入库明细价 ② inventories.unit_price 库存快照价 +// ③ products.purchase_price 商品主数据进价;均查不到 → 0(成本待定) +// - 涉及单据 profit_total 按 service.recalcStockOutProfit 同口径重算 +// - sale_total 不动(= 源单头合计,应收不变,财务无需冲账) +// +// 幂等:只处理 sale_price=0 且 cost_price>0 的占位明细,重跑自动跳过已修行。 +// 默认 dry-run(事务内试算后回滚),--apply 才真正落库。 +// +// 用法(在 backend/ 目录下执行): +// +// go run ./cmd/fix-history-prices --shop-code S001 # dry-run +// go run ./cmd/fix-history-prices --shop-code S001 --apply # 落库 +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "math" + "os" + "strings" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "github.com/wangjia/jiu/backend/config" + "github.com/wangjia/jiu/backend/internal/model" +) + +const placeholderProductCode = "HIST-PLACEHOLDER" + +// errDryRun 哨兵错误:dry-run 模式下用它触发事务回滚(试算走完真实 SQL 但不落库)。 +var errDryRun = errors.New("dry-run rollback") + +type fixStats struct { + Items int // 修复的出库明细行数 + CostFromStockIn int // 成本回填来源:入库明细 + CostFromInventory int // 成本回填来源:库存快照 + CostFromProduct int // 成本回填来源:商品主数据进价 + CostUnmatched int // 查无进价(cost 置 0,成本待定) + Orders int // 涉及并重算利润的出库单数 + TotalMismatches []string // Σ明细售价小计 ≠ 单头 sale_total 的单号(只报不改) +} + +type outItemRow struct { + ID uint64 + OrderID uint64 + ProductCode string + Quantity float64 + CostPrice float64 + CostAmount float64 +} + +type costHit struct { + price float64 + src string // stock_in / inventory / product / ""(unmatched) +} + +func round2(x float64) float64 { return math.Round(x*100) / 100 } + +// lookupCost 按商品编号回查真实进价(商品编号=序列号,一物一码,匹配精确)。 +func lookupCost(tx *gorm.DB, shopID uint64, code string) costHit { + if strings.TrimSpace(code) == "" { + return costHit{} + } + var p float64 + // ① 同店同编号的入库明细成本(历史导入入库价与正常入库价同源) + row := tx.Raw(`SELECT cost_price FROM stock_in_items + WHERE shop_id = ? AND product_code = ? AND cost_price > 0 + ORDER BY id DESC LIMIT 1`, shopID, code).Row() + if row.Scan(&p) == nil && p > 0 { + return costHit{p, "stock_in"} + } + // ② 库存快照价(快照列或经 product 关联均可命中) + row = tx.Raw(`SELECT inv.unit_price FROM inventories inv + LEFT JOIN products pr ON pr.id = inv.product_id AND pr.deleted_at IS NULL + WHERE inv.shop_id = ? AND inv.deleted_at IS NULL AND inv.unit_price > 0 + AND (inv.product_code = ? OR pr.code = ?) + ORDER BY inv.id DESC LIMIT 1`, shopID, code, code).Row() + if row.Scan(&p) == nil && p > 0 { + return costHit{p, "inventory"} + } + // ③ 商品主数据进价 + row = tx.Raw(`SELECT purchase_price FROM products + WHERE shop_id = ? AND code = ? AND deleted_at IS NULL AND purchase_price > 0 + ORDER BY id DESC LIMIT 1`, shopID, code).Row() + if row.Scan(&p) == nil && p > 0 { + return costHit{p, "product"} + } + return costHit{} +} + +// fixShop 修复一家店的历史导入出库明细价格。apply=false 时事务回滚(dry-run)。 +func fixShop(db *gorm.DB, shopID uint64, apply bool) (*fixStats, error) { + // 占位商品可能已被用户在界面软删(线上即如此)——历史明细仍引用其 id, + // 展示走快照列不受影响,这里按 id 定位范围即可,故不过滤 deleted_at。 + var ph model.Product + if err := db.Where("shop_id = ? AND code = ?", + shopID, placeholderProductCode).First(&ph).Error; err != nil { + return nil, fmt.Errorf("未找到占位商品 %s(该店没有历史导入数据?): %w", placeholderProductCode, err) + } + + st := &fixStats{} + err := db.Transaction(func(tx *gorm.DB) error { + var items []outItemRow + if err := tx.Raw(`SELECT id, order_id, product_code, quantity, cost_price, cost_amount + FROM stock_out_items + WHERE shop_id = ? AND product_id = ? AND sale_price = 0 AND cost_price > 0 + ORDER BY id`, shopID, ph.ID).Scan(&items).Error; err != nil { + return err + } + st.Items = len(items) + + cache := map[string]costHit{} + orderIDs := map[uint64]bool{} + for _, it := range items { + hit, ok := cache[it.ProductCode] + if !ok { + hit = lookupCost(tx, shopID, it.ProductCode) + cache[it.ProductCode] = hit + } + switch hit.src { + case "stock_in": + st.CostFromStockIn++ + case "inventory": + st.CostFromInventory++ + case "product": + st.CostFromProduct++ + default: + st.CostUnmatched++ + } + if err := tx.Exec(`UPDATE stock_out_items + SET sale_price = ?, sale_amount = ?, cost_price = ?, cost_amount = ? + WHERE id = ?`, + it.CostPrice, it.CostAmount, hit.price, round2(it.Quantity*hit.price), it.ID).Error; err != nil { + return err + } + orderIDs[it.OrderID] = true + } + st.Orders = len(orderIDs) + + if len(orderIDs) > 0 { + ids := make([]uint64, 0, len(orderIDs)) + for id := range orderIDs { + ids = append(ids, id) + } + // 与 service.recalcStockOutProfit 同口径重算总利润 + if err := tx.Exec(`UPDATE stock_out_orders SET profit_total = ( + SELECT COALESCE(SUM(CASE WHEN i.sale_price > 0 + THEN (i.sale_price - i.cost_price) * i.quantity ELSE 0 END), 0) + FROM stock_out_items i WHERE i.order_id = stock_out_orders.id + ) WHERE shop_id = ? AND id IN ?`, shopID, ids).Error; err != nil { + return err + } + // 校验:修复后 Σ明细售价小计 应与源单头合计 sale_total 一致(只报不改) + if err := tx.Raw(`SELECT o.order_no FROM stock_out_orders o + WHERE o.shop_id = ? AND o.id IN ? + AND ABS(o.sale_total - (SELECT COALESCE(SUM(i.sale_amount),0) + FROM stock_out_items i WHERE i.order_id = o.id)) > 0.01`, + shopID, ids).Scan(&st.TotalMismatches).Error; err != nil { + return err + } + } + + if !apply { + return errDryRun + } + return nil + }) + if err != nil && !errors.Is(err, errDryRun) { + return nil, err + } + return st, nil +} + +func main() { + var ( + shopCode = flag.String("shop-code", "", "目标门店 code(与 shop-id 二选一)") + shopID = flag.Uint64("shop-id", 0, "目标门店 id(与 shop-code 二选一)") + apply = flag.Bool("apply", false, "真正落库(缺省 dry-run:试算后回滚)") + envFile = flag.String("env-file", "", "可选:systemd 风格 KEY=VALUE 环境文件(读取 DATABASE_DSN)") + ) + flag.Parse() + + if *envFile != "" { + loadEnvFile(*envFile) + } + config.Load() + db, err := gorm.Open(mysql.Open(config.C.Database.DSN), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + log.Fatalf("连接数据库失败: %v", err) + } + + var shop model.Shop + switch { + case *shopID > 0: + if err := db.First(&shop, *shopID).Error; err != nil { + log.Fatalf("门店 id=%d 不存在: %v", *shopID, err) + } + case *shopCode != "": + if err := db.Where("code = ?", *shopCode).First(&shop).Error; err != nil { + log.Fatalf("门店 code=%s 不存在: %v", *shopCode, err) + } + default: + log.Fatal("必须指定 --shop-code 或 --shop-id") + } + + st, err := fixShop(db, shop.ID, *apply) + if err != nil { + log.Fatalf("修复失败(已回滚,未改动任何数据): %v", err) + } + + mode := "*** DRY-RUN(已回滚,未写库)***" + if *apply { + mode = "已落库" + } + fmt.Println("═══════════════════════════════════════════") + fmt.Println(" 历史出库价格错列修复汇总 —— " + mode) + fmt.Println("═══════════════════════════════════════════") + fmt.Printf(" 门店:%s (id=%d)\n", shop.Name, shop.ID) + fmt.Printf(" 修复明细行(售价←原成本列):%d\n", st.Items) + fmt.Printf(" 成本回填:入库价 %d / 库存价 %d / 商品进价 %d / 查无(成本待定) %d\n", + st.CostFromStockIn, st.CostFromInventory, st.CostFromProduct, st.CostUnmatched) + fmt.Printf(" 涉及出库单(已重算利润):%d\n", st.Orders) + if len(st.TotalMismatches) > 0 { + fmt.Printf(" ⚠ 应收合计与明细不一致的单(未改动,需人工核):%v\n", st.TotalMismatches) + } else { + fmt.Println(" 应收合计校验:全部一致 ✓") + } +} + +func loadEnvFile(path string) { + data, err := os.ReadFile(path) + if err != nil { + log.Fatalf("读取 env 文件 %s 失败: %v", path, err) + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + eq := strings.IndexByte(line, '=') + if eq <= 0 { + continue + } + key := strings.TrimSpace(line[:eq]) + val := strings.TrimSpace(line[eq+1:]) + if len(val) >= 2 && (val[0] == '"' || val[0] == '\'') && val[len(val)-1] == val[0] { + val = val[1 : len(val)-1] + } + _ = os.Setenv(key, val) + } +} diff --git a/backend/cmd/fix-history-prices/main_test.go b/backend/cmd/fix-history-prices/main_test.go new file mode 100644 index 0000000..9989f1c --- /dev/null +++ b/backend/cmd/fix-history-prices/main_test.go @@ -0,0 +1,198 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" + + "github.com/wangjia/jiu/backend/internal/model" + "github.com/wangjia/jiu/backend/testutil" +) + +func f64(v float64) *float64 { return &v } + +// fixture 复刻线上错列形态: +// - 占位商品 HIST-PLACEHOLDER,历史出库明细 product_id 均指向它 +// - 明细 cost_price/cost_amount 实为售价口径(源「单价/金额」),sale_* = 0 +// - 单头 sale_total = 源合计(售价口径,与明细"成本"合计相等) +// +// 四个成本回填场景:A=入库明细价 B=库存快照价 D=商品进价 C=查无。 +type fixture struct { + db *gorm.DB + shopID uint64 + placeholder *model.Product + order1 *model.StockOutOrder // A/B/C 三行,sale_total 与明细一致 + order2 *model.StockOutOrder // D 一行,sale_total 故意不一致 + normalItem *model.StockOutItem // 非占位商品的正常明细,不得被动到 +} + +func setupFixture(t *testing.T) *fixture { + t.Helper() + db := testutil.SetupTestDB() + shop := testutil.CreateTestShop(db, "FIX01") + wh := testutil.CreateTestWarehouse(db, shop.ID, "主仓") + + // 复刻线上形态:占位商品已被界面软删,明细仍引用其 id,工具必须照常定位 + ph := &model.Product{TenantBase: model.TenantBase{ShopID: shop.ID}, + Code: placeholderProductCode, Name: "历史导入占位"} + require.NoError(t, db.Create(ph).Error) + require.NoError(t, db.Exec( + "UPDATE products SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?", ph.ID).Error) + + // A:入库明细里有真实进价 13600(同时给商品主数据一个更低优先级的进价,验证①优先) + prodA := &model.Product{TenantBase: model.TenantBase{ShopID: shop.ID}, + Code: "ZXZ027628", Name: "茅台铁盖1995五星", PublicID: "pub-a", PurchasePrice: 1} + require.NoError(t, db.Create(prodA).Error) + sin := &model.StockInOrder{TenantBase: model.TenantBase{ShopID: shop.ID}, + OrderNo: "RKTEST1", WarehouseID: wh.ID, OperatorID: 1, Status: "approved"} + require.NoError(t, db.Create(sin).Error) + require.NoError(t, db.Create(&model.StockInItem{ + OrderID: sin.ID, ShopID: shop.ID, ProductID: ph.ID, + ProductCode: "ZXZ027628", Quantity: 1, CostPrice: 13600, CostAmount: 13600, + }).Error) + + // B:只有库存快照价 500 + require.NoError(t, db.Create(&model.Inventory{ + ShopID: shop.ID, ProductCode: "B001", Quantity: 2, UnitPrice: f64(500), + }).Error) + + // D:只有商品主数据进价 1500 + prodD := &model.Product{TenantBase: model.TenantBase{ShopID: shop.ID}, + Code: "D001", Name: "商品D", PublicID: "pub-d", PurchasePrice: 1500} + require.NoError(t, db.Create(prodD).Error) + + // 出库单 1:A(14300×1) + B(400×2) + C(999×1),sale_total 与明细合计一致 + o1 := &model.StockOutOrder{TenantBase: model.TenantBase{ShopID: shop.ID}, + OrderNo: "CKTEST1", WarehouseID: wh.ID, OperatorID: 1, Status: "approved", + SaleTotal: 14300 + 800 + 999} + require.NoError(t, db.Create(o1).Error) + for _, it := range []*model.StockOutItem{ + {OrderID: o1.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "ZXZ027628", + Quantity: 1, CostPrice: 14300, CostAmount: 14300}, + {OrderID: o1.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "B001", + Quantity: 2, CostPrice: 400, CostAmount: 800}, + {OrderID: o1.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "C001", + Quantity: 1, CostPrice: 999, CostAmount: 999}, + } { + require.NoError(t, db.Create(it).Error) + } + + // 出库单 2:D(2000×1),sale_total 故意与明细不一致(源数据脏),应只报不改 + o2 := &model.StockOutOrder{TenantBase: model.TenantBase{ShopID: shop.ID}, + OrderNo: "CKTEST2", WarehouseID: wh.ID, OperatorID: 1, Status: "pending", + SaleTotal: 123} + require.NoError(t, db.Create(o2).Error) + require.NoError(t, db.Create(&model.StockOutItem{ + OrderID: o2.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "D001", + Quantity: 1, CostPrice: 2000, CostAmount: 2000, + }).Error) + + // 正常单(非占位商品):sale_price=0 且 cost>0,但 product_id 非占位 → 不得被动 + prodN := testutil.CreateTestProduct(db, shop.ID, "正常商品") + o3 := &model.StockOutOrder{TenantBase: model.TenantBase{ShopID: shop.ID}, + OrderNo: "CKTEST3", WarehouseID: wh.ID, OperatorID: 1, Status: "approved", + SaleTotal: 0} + require.NoError(t, db.Create(o3).Error) + normal := &model.StockOutItem{OrderID: o3.ID, ShopID: shop.ID, ProductID: prodN.ID, + ProductCode: "P-正常商品", Quantity: 1, CostPrice: 100, CostAmount: 100} + require.NoError(t, db.Create(normal).Error) + + return &fixture{db: db, shopID: shop.ID, placeholder: ph, + order1: o1, order2: o2, normalItem: normal} +} + +func TestFixShop_DryRunDoesNotWrite(t *testing.T) { + fx := setupFixture(t) + + st, err := fixShop(fx.db, fx.shopID, false) + require.NoError(t, err) + assert.Equal(t, 4, st.Items) + assert.Equal(t, 1, st.CostFromStockIn) + assert.Equal(t, 1, st.CostFromInventory) + assert.Equal(t, 1, st.CostFromProduct) + assert.Equal(t, 1, st.CostUnmatched) + assert.Equal(t, 2, st.Orders) + assert.Equal(t, []string{"CKTEST2"}, st.TotalMismatches) + + // 回滚后数据原样 + var it model.StockOutItem + require.NoError(t, fx.db.Where("product_code = ?", "ZXZ027628"). + Where("order_id = ?", fx.order1.ID).First(&it).Error) + assert.Equal(t, 14300.0, it.CostPrice) + assert.Equal(t, 0.0, it.SalePrice) + var o model.StockOutOrder + require.NoError(t, fx.db.First(&o, fx.order1.ID).Error) + assert.Equal(t, 0.0, o.ProfitTotal) +} + +func TestFixShop_Apply(t *testing.T) { + fx := setupFixture(t) + + st, err := fixShop(fx.db, fx.shopID, true) + require.NoError(t, err) + assert.Equal(t, 4, st.Items) + + get := func(orderID uint64, code string) model.StockOutItem { + var it model.StockOutItem + require.NoError(t, fx.db.Where("order_id = ? AND product_code = ?", orderID, code). + First(&it).Error) + return it + } + + // A:售价归位 14300,成本回填入库价 13600 + a := get(fx.order1.ID, "ZXZ027628") + assert.Equal(t, 14300.0, a.SalePrice) + assert.Equal(t, 14300.0, a.SaleAmount) + assert.Equal(t, 13600.0, a.CostPrice) + assert.Equal(t, 13600.0, a.CostAmount) + + // B:成本回填库存快照价 500,小计 = 2×500 + b := get(fx.order1.ID, "B001") + assert.Equal(t, 400.0, b.SalePrice) + assert.Equal(t, 800.0, b.SaleAmount) + assert.Equal(t, 500.0, b.CostPrice) + assert.Equal(t, 1000.0, b.CostAmount) + + // C:查无进价 → 成本待定 0 + c := get(fx.order1.ID, "C001") + assert.Equal(t, 999.0, c.SalePrice) + assert.Equal(t, 0.0, c.CostPrice) + assert.Equal(t, 0.0, c.CostAmount) + + // D:成本回填商品进价 1500 + d := get(fx.order2.ID, "D001") + assert.Equal(t, 2000.0, d.SalePrice) + assert.Equal(t, 1500.0, d.CostPrice) + + // 单头:利润按 recalcStockOutProfit 口径;sale_total 不动 + var o1, o2 model.StockOutOrder + require.NoError(t, fx.db.First(&o1, fx.order1.ID).Error) + // (14300-13600)×1 + (400-500)×2 + (999-0)×1 = 700 - 200 + 999 + assert.InDelta(t, 1499.0, o1.ProfitTotal, 0.001) + assert.Equal(t, 16099.0, o1.SaleTotal) + require.NoError(t, fx.db.First(&o2, fx.order2.ID).Error) + assert.InDelta(t, 500.0, o2.ProfitTotal, 0.001) + assert.Equal(t, 123.0, o2.SaleTotal) // 不一致只报不改 + assert.Equal(t, []string{"CKTEST2"}, st.TotalMismatches) + + // 正常明细未被波及 + var n model.StockOutItem + require.NoError(t, fx.db.First(&n, fx.normalItem.ID).Error) + assert.Equal(t, 100.0, n.CostPrice) + assert.Equal(t, 0.0, n.SalePrice) + + // 幂等:重跑无待修行 + st2, err := fixShop(fx.db, fx.shopID, true) + require.NoError(t, err) + assert.Equal(t, 0, st2.Items) + assert.Equal(t, 0, st2.Orders) +} + +func TestFixShop_NoPlaceholder(t *testing.T) { + db := testutil.SetupTestDB() + shop := testutil.CreateTestShop(db, "FIX02") + _, err := fixShop(db, shop.ID, false) + assert.Error(t, err) +} diff --git a/backend/cmd/import-history/main.go b/backend/cmd/import-history/main.go index c83abc0..d8c0983 100644 --- a/backend/cmd/import-history/main.go +++ b/backend/cmd/import-history/main.go @@ -529,13 +529,17 @@ func (ic *importCtx) writeOrder( TenantBase: model.TenantBase{ShopID: ic.shopID}, OrderNo: orderNo, Type: orderType, WarehouseID: ic.whID, PartnerID: partnerID, OperatorID: operatorID, CreatorID: creatorID, - // 忠实导入:源「金额」合计沿旧行为落应收合计(sale_price=0 待定价) + // 忠实导入:源「金额」合计是售价口径,落应收合计(与明细 sale_* 同口径) ReviewerID: reviewerID, Status: status, OrderDate: orderDate, SaleTotal: totalAmount, } if err := tx.Create(&o).Error; err != nil { return err } for _, dr := range details { + // 源出库明细「单价/金额」是售价口径(卖给客户的价,应收侧),落 sale 列; + // 真实成本导入时不可知,留 0(成本待定),事后用 cmd/fix-history-prices + // 按商品编号回查入库价/库存价/商品进价回填。 + // (2026-07-10 修正:此前误落 cost_price/cost_amount,线上数据已用同工具修复) it := model.StockOutItem{ OrderID: o.ID, ShopID: ic.shopID, ProductID: ic.placeholderProductID, ProductCode: get(dr, dCols, "商品编号"), @@ -543,8 +547,8 @@ func (ic *importCtx) writeOrder( Series: get(dr, dCols, "系列"), Spec: get(dr, dCols, "规格"), Quantity: parseFloatLoose(get(dr, dCols, "数量")), - CostPrice: parseFloatLoose(get(dr, dCols, "单价")), - CostAmount: parseFloatLoose(get(dr, dCols, "金额")), + SalePrice: parseFloatLoose(get(dr, dCols, "单价")), + SaleAmount: parseFloatLoose(get(dr, dCols, "金额")), BatchNo: get(dr, dCols, "批次号"), } it.ProductionDate = parseDatePtr(get(dr, dCols, "生产日期"))