diff --git a/backend/cmd/seed/main.go b/backend/cmd/seed/main.go index 9ba8e4e..877be6f 100644 --- a/backend/cmd/seed/main.go +++ b/backend/cmd/seed/main.go @@ -1,10 +1,10 @@ // seed — 初始化/重置测试数据 // // 用法(在 backend/ 目录下执行): -// go run cmd/seed/main.go # 写入数据(已存在则跳过) -// go run cmd/seed/main.go --reset # 删表重建 + 写入数据 -// go run cmd/seed/main.go --clear # 清空业务数据 + 重新写入(保留表结构) - +// +// go run cmd/seed/main.go # 写入数据(已存在则跳过) +// go run cmd/seed/main.go --reset # 删表重建 + 写入数据 +// go run cmd/seed/main.go --clear # 清空业务数据 + 重新写入(保留表结构) package main import ( @@ -42,7 +42,7 @@ var allModels = []any{ &model.NumberRule{}, } -// 清空业务数据的表(有外键依赖的先删子表) +// 清空顺序(子表先清) var truncateOrder = []string{ "inventory_check_items", "inventory_checks", "inventory_logs", "inventories", @@ -65,10 +65,7 @@ func main() { db, err := gorm.Open(mysql.Open(config.C.Database.DSN), &gorm.Config{ Logger: logger.New( stdlog.New(os.Stdout, "\r\n", stdlog.LstdFlags), - logger.Config{ - LogLevel: logger.Warn, - IgnoreRecordNotFoundError: true, - }, + logger.Config{LogLevel: logger.Warn, IgnoreRecordNotFoundError: true}, ), }) if err != nil { @@ -107,192 +104,581 @@ func main() { fmt.Println("✅ 表结构已同步") fmt.Println() + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + d := func(daysAgo int) model.Date { return model.Date{Time: today.AddDate(0, 0, -daysAgo)} } + ptrT := func(t time.Time) *time.Time { return &t } + // ═══════════════════════════════════════════════════ // 门店 // ═══════════════════════════════════════════════════ - shop := upsertShop(db) + shop := upsert(db, &model.Shop{}, "code = ?", "H001", func() any { + return &model.Shop{ + Name: "测试酒库门店", + Code: "H001", + Address: "北京市朝阳区酒仙桥路甲1号", + Phone: "010-12345678", + ManagerName: "张三", + BusinessLicense: "91110105MA0000000X", + } + }).(model.Shop) // ═══════════════════════════════════════════════════ // 用户 // ═══════════════════════════════════════════════════ hash := mustHash("password123") - admin := upsertUser(db, shop.ID, "admin", "超级管理员", "admin", hash) - upsertUser(db, shop.ID, "operator", "操作员小李", "operator", hash) - upsertUser(db, shop.ID, "test", "测试账号", "readonly", hash) + admin := upsert(db, &model.User{}, "shop_id = ? AND username = ?", shop.ID, "admin", func() any { + return &model.User{ + TenantBase: model.TenantBase{ShopID: shop.ID}, + Username: "admin", PasswordHash: hash, RealName: "张三(管理员)", + Phone: "13800000001", Role: "admin", IsActive: true, + } + }).(model.User) + operator := upsert(db, &model.User{}, "shop_id = ? AND username = ?", shop.ID, "operator", func() any { + return &model.User{ + TenantBase: model.TenantBase{ShopID: shop.ID}, + Username: "operator", PasswordHash: hash, RealName: "李四(操作员)", + Phone: "13800000002", Role: "operator", IsActive: true, + } + }).(model.User) + upsert(db, &model.User{}, "shop_id = ? AND username = ?", shop.ID, "test", func() any { + return &model.User{ + TenantBase: model.TenantBase{ShopID: shop.ID}, + Username: "test", PasswordHash: hash, RealName: "王五(只读)", + Phone: "13800000003", Role: "readonly", IsActive: true, + } + }) // ═══════════════════════════════════════════════════ // 仓库 // ═══════════════════════════════════════════════════ - wh1 := upsertWarehouse(db, shop.ID, "主仓库", "A栋1层", true) - wh2 := upsertWarehouse(db, shop.ID, "备用仓库", "B栋2层", false) + wh1 := upsert(db, &model.Warehouse{}, "shop_id = ? AND name = ?", shop.ID, "主仓库", func() any { + return &model.Warehouse{ + TenantBase: model.TenantBase{ShopID: shop.ID}, + Name: "主仓库", Location: "A栋1层东侧", IsDefault: true, + } + }).(model.Warehouse) + wh2 := upsert(db, &model.Warehouse{}, "shop_id = ? AND name = ?", shop.ID, "进口酒专库", func() any { + return &model.Warehouse{ + TenantBase: model.TenantBase{ShopID: shop.ID}, + Name: "进口酒专库", Location: "B栋2层恒温区", IsDefault: false, + } + }).(model.Warehouse) + + // ═══════════════════════════════════════════════════ + // 商品分类 + // ═══════════════════════════════════════════════════ + catBaijiu := upsert(db, &model.ProductCategory{}, "shop_id = ? AND name = ?", shop.ID, "白酒", func() any { + return &model.ProductCategory{TenantBase: model.TenantBase{ShopID: shop.ID}, Name: "白酒", SortOrder: 1} + }).(model.ProductCategory) + catImport := upsert(db, &model.ProductCategory{}, "shop_id = ? AND name = ?", shop.ID, "进口烈酒", func() any { + return &model.ProductCategory{TenantBase: model.TenantBase{ShopID: shop.ID}, Name: "进口烈酒", SortOrder: 2} + }).(model.ProductCategory) // ═══════════════════════════════════════════════════ // 商品 // ═══════════════════════════════════════════════════ - products := []struct{ name, series, unit, sku string }{ - {"飞天茅台 53度 500ml", "茅台", "瓶", "MT-001"}, - {"五粮液 52度 500ml", "五粮液", "瓶", "WLY-001"}, - {"洋河梦之蓝 M6 500ml", "洋河", "瓶", "YH-001"}, - {"泸州老窖 特曲 500ml", "泸州老窖", "瓶", "LZ-001"}, - {"剑南春 水晶剑 500ml", "剑南春", "瓶", "JNC-001"}, - {"郎酒 红花郎 500ml", "郎酒", "瓶", "LJ-001"}, - {"拉菲古堡 2018 750ml", "波尔多", "瓶", "LF-001"}, - {"人头马 VSOP 700ml", "人头马", "瓶", "RTM-001"}, + type prodSeed struct { + code, barcode, name, series, spec, unit, brand string + catID *uint64 + purchase, sale float64 + minStock int + remark string + } + prodSeeds := []prodSeed{ + {"MT-001", "6901234567890", "飞天茅台 53度 500ml", "茅台", "500ml/瓶", "瓶", "贵州茅台", &catBaijiu.ID, 2350, 2800, 10, "酱香型白酒,53度,飞天系列"}, + {"WLY-001", "6902345678901", "五粮液 52度 500ml", "五粮液", "500ml/瓶", "瓶", "宜宾五粮液", &catBaijiu.ID, 950, 1200, 6, "浓香型白酒,52度,普五系列"}, + {"YH-001", "6903456789012", "洋河梦之蓝 M6 500ml", "洋河", "500ml/瓶", "瓶", "江苏洋河", &catBaijiu.ID, 560, 680, 6, "浓香型,绵柔苏酒代表"}, + {"LZ-001", "6904567890123", "泸州老窖 特曲 500ml", "泸州老窖", "500ml/瓶", "瓶", "泸州老窖", &catBaijiu.ID, 420, 520, 6, "浓香鼻祖,特曲系列"}, + {"JNC-001", "6905678901234", "剑南春 水晶剑 500ml", "剑南春", "500ml/瓶", "瓶", "剑南春", &catBaijiu.ID, 390, 480, 6, "浓香型,绵竹名酒"}, + {"LJ-001", "6906789012345", "郎酒 红花郎10 500ml", "郎酒", "500ml/瓶", "瓶", "古蔺郎酒", &catBaijiu.ID, 320, 420, 6, "酱香型,赤水河畔酿造"}, + {"LF-001", "3760093550058", "拉菲古堡 2018 750ml", "波尔多", "750ml/瓶", "瓶", "Château Lafite", &catImport.ID, 3800, 5200, 3, "波尔多一级名庄,2018年份"}, + {"RTM-001", "3021691010008", "人头马 VSOP 700ml", "人头马", "700ml/瓶", "瓶", "Rémy Martin", &catImport.ID, 480, 680, 3, "法国干邑,VSOP级别"}, } var prods []model.Product - for _, p := range products { - prod := upsertProduct(db, shop.ID, p.name, p.series, p.unit, p.sku) - prods = append(prods, prod) + for _, s := range prodSeeds { + p := upsert(db, &model.Product{}, "shop_id = ? AND code = ?", shop.ID, s.code, func() any { + return &model.Product{ + TenantBase: model.TenantBase{ShopID: shop.ID}, + Code: s.code, Barcode: s.barcode, Name: s.name, + Series: s.series, Spec: s.spec, Unit: s.unit, Brand: s.brand, + CategoryID: s.catID, PurchasePrice: s.purchase, SalePrice: s.sale, + MinStock: s.minStock, Remark: s.remark, + } + }).(model.Product) + prods = append(prods, p) } // ═══════════════════════════════════════════════════ // 往来单位 // ═══════════════════════════════════════════════════ - sup1 := upsertPartner(db, shop.ID, "贵州茅台酒股份有限公司", "supplier", "张经理", "0851-12345678") - sup2 := upsertPartner(db, shop.ID, "四川五粮液股份有限公司", "supplier", "王经理", "0831-87654321") - cus1 := upsertPartner(db, shop.ID, "北京君悦大酒店", "customer", "李采购", "010-65888888") - upsertPartner(db, shop.ID, "上海外滩华尔道夫", "customer", "陈主任", "021-62308888") + type partnerSeed struct { + code, name, ptype, contact, phone, address, bank string + creditLimit float64 + remark string + } + partnerSeeds := []partnerSeed{ + {"SUP001", "贵州茅台酒股份有限公司", "supplier", "张经理", "0851-22222001", + "贵州省仁怀市茅台镇", "工商银行仁怀支行 6222 0000 0001 0001", + 5000000, "茅台系列直供,账期30天"}, + {"SUP002", "四川五粮液股份有限公司", "supplier", "王总监", "0831-33333001", + "四川省宜宾市翠屏区", "建设银行宜宾支行 6227 0000 0001 0002", + 3000000, "五粮液系列授权经销"}, + {"SUP003", "江苏洋河酒厂股份有限公司", "supplier", "赵主任", "0527-44444001", + "江苏省宿迁市洋河新区", "农业银行宿迁支行 6228 0000 0001 0003", + 2000000, "洋河梦之蓝系列直供"}, + {"SUP004", "四川郎酒股份有限公司", "supplier", "刘经理", "0830-55555001", + "四川省古蔺县二郎镇", "中国银行古蔺支行 6013 0000 0001 0004", + 1500000, "郎酒红花郎系列"}, + {"CUS001", "北京君悦大酒店", "customer", "李采购", "010-65888001", + "北京市朝阳区建国门外大街2号", "中信银行北京朝阳支行 6217 0000 0002 0001", + 500000, "五星级酒店,月结"}, + {"CUS002", "上海外滩华尔道夫酒店", "customer", "陈主任", "021-62308001", + "上海市黄浦区中山东一路2号", "浦发银行上海黄浦支行 6210 0000 0002 0002", + 800000, "豪华酒店,按季结算"}, + {"CUS003", "广州白天鹅宾馆", "customer", "吴采购", "020-81886001", + "广州市荔湾区沙面南街1号", "招商银行广州荔湾支行 6225 0000 0002 0003", + 300000, "高端宾馆,月结30天"}, + } + partners := make(map[string]model.Partner) + for _, s := range partnerSeeds { + p := upsert(db, &model.Partner{}, "shop_id = ? AND code = ?", shop.ID, s.code, func() any { + return &model.Partner{ + TenantBase: model.TenantBase{ShopID: shop.ID}, + Code: s.code, + Name: s.name, + Type: s.ptype, + Contact: s.contact, + Phone: s.phone, + Address: s.address, + BankAccount: s.bank, + CreditLimit: s.creditLimit, + Balance: 0, + Remark: s.remark, + } + }).(model.Partner) + partners[s.code] = p + } // ═══════════════════════════════════════════════════ // 编号规则 // ═══════════════════════════════════════════════════ upsertNumberRule(db, shop.ID, "stock_in", "RK", 5) - upsertNumberRule(db, shop.ID, "stock_out", "CK", 2) + upsertNumberRule(db, shop.ID, "stock_out", "CK", 3) upsertNumberRule(db, shop.ID, "inventory_check", "PD", 1) // ═══════════════════════════════════════════════════ - // 入库单(已审核)→ 生成库存 + // 入库单 // ═══════════════════════════════════════════════════ - now := time.Now() - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + // #1 已审核:茅台+五粮液,主仓库,茅台供应商 + in1 := createStockInOrder(db, shop.ID, wh1.ID, admin.ID, partners["SUP001"].ID, + "RK20260401001", d(6), "approved", "purchase", + "茅台系列首批入库,含飞天茅台及五粮液", + []inItemSeed{ + {prods[0].ID, 120, 2350, "BATCH-MT-2024001", "飞天茅台 2024年第一批次"}, + {prods[1].ID, 60, 950, "BATCH-WLY-2024001", "五粮液普五 2024年批次"}, + }, admin.ID, ptrT(today.AddDate(0, 0, -5))) - inOrders := []struct { - orderNo string - partnerID uint64 - items []struct { - prodIdx int - qty float64 - price float64 - } - }{ - { - orderNo: "RK20260401001", - partnerID: sup1.ID, - items: []struct { - prodIdx int - qty float64 - price float64 - }{ - {0, 120, 2800}, // 茅台 120瓶 - {1, 60, 1200}, // 五粮液 60瓶 - }, - }, - { - orderNo: "RK20260403001", - partnerID: sup2.ID, - items: []struct { - prodIdx int - qty float64 - price float64 - }{ - {2, 48, 680}, // 洋河 48瓶 - {3, 36, 520}, // 泸州老窖 36瓶 - {4, 24, 480}, // 剑南春 24瓶 - }, - }, - } + // #2 已审核:洋河+泸州+剑南春,主仓库,洋河供应商 + in2 := createStockInOrder(db, shop.ID, wh1.ID, operator.ID, partners["SUP002"].ID, + "RK20260403001", d(4), "approved", "purchase", + "浓香型白酒补货入库", + []inItemSeed{ + {prods[2].ID, 48, 560, "BATCH-YH-2024003", "洋河梦之蓝 M6 3月批次"}, + {prods[3].ID, 36, 420, "BATCH-LZ-2024002", "泸州老窖特曲 春季批次"}, + {prods[4].ID, 24, 390, "BATCH-JNC-2024001", "剑南春水晶剑 首批"}, + }, admin.ID, ptrT(today.AddDate(0, 0, -3))) - for _, o := range inOrders { - order := upsertStockInOrder(db, shop.ID, wh1.ID, admin.ID, o.partnerID, o.orderNo, model.Date{Time: today.AddDate(0, 0, -3)}) - total := 0.0 - for _, it := range o.items { - item := model.StockInItem{ - OrderID: order.ID, - ShopID: shop.ID, - ProductID: prods[it.prodIdx].ID, - Quantity: it.qty, - UnitPrice: it.price, - TotalPrice: it.qty * it.price, - } - db.Create(&item) - total += item.TotalPrice + // #3 已审核:进口酒,进口酒专库,茅台供应商(代理进口) + in3 := createStockInOrder(db, shop.ID, wh2.ID, operator.ID, partners["SUP004"].ID, + "RK20260404001", d(3), "approved", "purchase", + "进口烈酒专库入库,含拉菲及人头马", + []inItemSeed{ + {prods[6].ID, 24, 3800, "BATCH-LF-2018001", "拉菲古堡2018,原箱"}, + {prods[7].ID, 36, 480, "BATCH-RTM-2024001", "人头马VSOP,2024年进口"}, + }, admin.ID, ptrT(today.AddDate(0, 0, -2))) - // 更新库存 - updateInventory(db, shop.ID, wh1.ID, prods[it.prodIdx].ID, it.qty, order.ID, admin.ID) - } - db.Model(&order).Updates(map[string]any{ - "status": "approved", - "total_amount": total, - "reviewer_id": admin.ID, - "reviewed_at": now, - }) - } + // #4 待审核:郎酒补货 + in4 := createStockInOrder(db, shop.ID, wh1.ID, operator.ID, partners["SUP004"].ID, + "RK20260406001", d(1), "pending", "purchase", + "郎酒红花郎补货,待仓库管理员审核", + []inItemSeed{ + {prods[5].ID, 60, 320, "BATCH-LJ-2024002", "郎酒红花郎10 第二批次"}, + }, 0, nil) + + // #5 草稿:追加茅台 + in5 := createStockInOrder(db, shop.ID, wh1.ID, operator.ID, partners["SUP001"].ID, + "RK20260407001", d(0), "draft", "purchase", + "茅台追加订货,草稿中", + []inItemSeed{ + {prods[0].ID, 60, 2380, "BATCH-MT-2024002", "飞天茅台 第二批次"}, + }, 0, nil) + + _ = in4 + _ = in5 + + // 根据已审核入库单更新库存 + updateInventoryBatch(db, shop.ID, wh1.ID, admin.ID, in1) + updateInventoryBatch(db, shop.ID, wh1.ID, admin.ID, in2) + updateInventoryBatch(db, shop.ID, wh2.ID, admin.ID, in3) // ═══════════════════════════════════════════════════ - // 出库单(已审核)→ 减库存 + // 出库单 // ═══════════════════════════════════════════════════ - outOrder := upsertStockOutOrder(db, shop.ID, wh1.ID, admin.ID, cus1.ID, "CK20260404001", model.Date{Time: today.AddDate(0, 0, -1)}) - outItems := []struct { - prodIdx int - qty float64 - price float64 - }{ - {0, 12, 3200}, // 茅台 12瓶 - {1, 6, 1380}, // 五粮液 6瓶 - } - outTotal := 0.0 - for _, it := range outItems { - item := model.StockOutItem{ - OrderID: outOrder.ID, - ShopID: shop.ID, - ProductID: prods[it.prodIdx].ID, - Quantity: it.qty, - UnitPrice: it.price, - TotalPrice: it.qty * it.price, - } - db.Create(&item) - outTotal += item.TotalPrice - updateInventoryOut(db, shop.ID, wh1.ID, prods[it.prodIdx].ID, it.qty, outOrder.ID, admin.ID) - } - db.Model(&outOrder).Updates(map[string]any{ - "status": "approved", - "total_amount": outTotal, - "reviewer_id": admin.ID, - "reviewed_at": now, - }) + // #1 已审核:北京君悦大酒店,主仓库 + out1 := createStockOutOrder(db, shop.ID, wh1.ID, operator.ID, partners["CUS001"].ID, + "CK20260404001", d(3), "approved", "sale", + "北京君悦大酒店 4月份定期供货", + []outItemSeed{ + {prods[0].ID, 12, 2800, "飞天茅台 12瓶,整箱出库"}, + {prods[1].ID, 6, 1200, "五粮液普五 6瓶"}, + {prods[2].ID, 12, 680, "洋河梦之蓝 M6 12瓶"}, + }, admin.ID, ptrT(today.AddDate(0, 0, -2))) - // 备用仓库放一些商品(直接写库存) - directInventory := []struct{ prodIdx int; qty float64 }{ - {5, 24}, {6, 18}, {7, 12}, - } - for _, it := range directInventory { - updateInventory(db, shop.ID, wh2.ID, prods[it.prodIdx].ID, it.qty, 0, admin.ID) - } + // #2 已审核:上海外滩华尔道夫,进口酒专库 + out2 := createStockOutOrder(db, shop.ID, wh2.ID, operator.ID, partners["CUS002"].ID, + "CK20260405001", d(2), "approved", "sale", + "上海外滩华尔道夫 进口酒专属采购", + []outItemSeed{ + {prods[6].ID, 6, 5200, "拉菲古堡2018 6瓶"}, + {prods[7].ID, 12, 680, "人头马VSOP 12瓶"}, + }, admin.ID, ptrT(today.AddDate(0, 0, -1))) + + // #3 待审核:广州白天鹅宾馆 + out3 := createStockOutOrder(db, shop.ID, wh1.ID, operator.ID, partners["CUS003"].ID, + "CK20260406001", d(1), "pending", "sale", + "广州白天鹅宾馆 月度供货申请", + []outItemSeed{ + {prods[3].ID, 24, 520, "泸州老窖特曲 24瓶"}, + {prods[4].ID, 12, 480, "剑南春水晶剑 12瓶"}, + {prods[5].ID, 12, 420, "郎酒红花郎10 12瓶"}, + }, 0, nil) + + // #4 草稿:君悦追加 + out4 := createStockOutOrder(db, shop.ID, wh1.ID, operator.ID, partners["CUS001"].ID, + "CK20260407001", d(0), "draft", "sale", + "北京君悦追加订单,草稿中", + []outItemSeed{ + {prods[0].ID, 6, 2800, "飞天茅台 追加 6瓶"}, + }, 0, nil) + + _ = out3 + _ = out4 + + // 根据已审核出库单减库存 + deductInventoryBatch(db, shop.ID, wh1.ID, admin.ID, out1) + deductInventoryBatch(db, shop.ID, wh2.ID, admin.ID, out2) + + // ═══════════════════════════════════════════════════ + // 财务记录(与已审核出库单对应的应收款) + // ═══════════════════════════════════════════════════ + createFinanceRecord(db, shop.ID, partners["CUS001"].ID, admin.ID, + "receivable", out1.order.TotalAmount, out1.order.TotalAmount, "stock_out", out1.order.ID, + d(3), "君悦大酒店4月供货应收款") + createFinanceRecord(db, shop.ID, partners["CUS002"].ID, admin.ID, + "receivable", out2.order.TotalAmount, out2.order.TotalAmount, "stock_out", out2.order.ID, + d(2), "外滩华尔道夫进口酒应收款") + // 模拟一笔已收款 + createFinanceRecord(db, shop.ID, partners["CUS001"].ID, admin.ID, + "receipt", out1.order.TotalAmount, 0, "stock_out", out1.order.ID, + d(1), "君悦大酒店回款,结清") // ═══════════════════════════════════════════════════ // 打印汇总 // ═══════════════════════════════════════════════════ - fmt.Println("═══════════════════════════════════════") + fmt.Println("═══════════════════════════════════════════") fmt.Println(" 测试数据写入完成") - fmt.Println("═══════════════════════════════════════") + fmt.Println("═══════════════════════════════════════════") fmt.Println() fmt.Println(" 登录信息:") - fmt.Println(" 门店编号:H001") + fmt.Printf(" 门店编号:%s\n", shop.Code) fmt.Println(" admin / password123 (管理员)") fmt.Println(" operator / password123 (操作员)") fmt.Println(" test / password123 (只读)") fmt.Println() fmt.Println(" 数据概览:") - fmt.Println(" 仓库:主仓库、备用仓库") - fmt.Printf(" 商品:%d 种\n", len(prods)) - fmt.Println(" 往来单位:2 供应商 + 2 客户") - fmt.Println(" 入库单:2 张(已审核)") - fmt.Println(" 出库单:1 张(已审核)") - fmt.Println(" 库存:主仓库 5 种商品,备用仓库 3 种商品") - fmt.Println("═══════════════════════════════════════") + fmt.Printf(" 仓库:%s / %s\n", wh1.Name, wh2.Name) + fmt.Printf(" 商品分类:白酒 / 进口烈酒,共 %d 种商品\n", len(prods)) + fmt.Println(" 往来单位:4 供应商 + 3 客户") + fmt.Println(" 入库单:5 张(2已审核 / 1待审核 / 1草稿 / 1进口专库已审核)") + fmt.Println(" 出库单:4 张(2已审核 / 1待审核 / 1草稿)") + fmt.Println(" 财务记录:2 应收 + 1 收款") + fmt.Println("═══════════════════════════════════════════") } -// ── 辅助函数 ──────────────────────────────────────────── +// ── 辅助类型 ──────────────────────────────────────────── + +type inItemSeed struct { + productID uint64 + qty float64 + price float64 + batchNo string + remark string +} + +type outItemSeed struct { + productID uint64 + qty float64 + price float64 + remark string +} + +type stockInResult struct { + order model.StockInOrder + items []inItemSeed +} + +type stockOutResult struct { + order model.StockOutOrder + items []outItemSeed +} + +// ── 核心 upsert ──────────────────────────────────────────── + +// upsert 通用:查不到就调 factory 建,返回具体值(interface{}) +func upsert(db *gorm.DB, dest any, cond string, args ...any) any { + // 最后一个 arg 是 factory func + factoryIdx := len(args) - 1 + factory := args[factoryIdx].(func() any) + queryArgs := args[:factoryIdx] + + found := false + switch v := dest.(type) { + case *model.Shop: + found = db.Where(cond, queryArgs...).First(v).Error == nil + if !found { + obj := factory().(*model.Shop) + db.Create(obj) + fmt.Printf("✅ 门店:%s (%s)\n", obj.Name, obj.Code) + return *obj + } + fmt.Printf("⏭ 门店:%s (%s)\n", v.Name, v.Code) + return *v + case *model.User: + found = db.Where(cond, queryArgs...).First(v).Error == nil + if !found { + obj := factory().(*model.User) + db.Create(obj) + fmt.Printf("✅ 用户:%s(%s)\n", obj.Username, obj.Role) + return *obj + } + fmt.Printf("⏭ 用户:%s\n", v.Username) + return *v + case *model.Warehouse: + found = db.Where(cond, queryArgs...).First(v).Error == nil + if !found { + obj := factory().(*model.Warehouse) + db.Create(obj) + fmt.Printf("✅ 仓库:%s(%s)\n", obj.Name, obj.Location) + return *obj + } + fmt.Printf("⏭ 仓库:%s\n", v.Name) + return *v + case *model.ProductCategory: + found = db.Where(cond, queryArgs...).First(v).Error == nil + if !found { + obj := factory().(*model.ProductCategory) + db.Create(obj) + fmt.Printf("✅ 分类:%s\n", obj.Name) + return *obj + } + fmt.Printf("⏭ 分类:%s\n", v.Name) + return *v + case *model.Product: + found = db.Where(cond, queryArgs...).First(v).Error == nil + if !found { + obj := factory().(*model.Product) + db.Create(obj) + fmt.Printf("✅ 商品:%s(%s)\n", obj.Name, obj.Code) + return *obj + } + fmt.Printf("⏭ 商品:%s\n", v.Name) + return *v + case *model.Partner: + found = db.Where(cond, queryArgs...).First(v).Error == nil + if !found { + obj := factory().(*model.Partner) + db.Create(obj) + fmt.Printf("✅ 往来单位:%s(%s)\n", obj.Name, obj.Type) + return *obj + } + fmt.Printf("⏭ 往来单位:%s\n", v.Name) + return *v + } + return nil +} + +// ── 入库单 ──────────────────────────────────────────── + +func createStockInOrder( + db *gorm.DB, shopID, whID, opID, partnerID uint64, + orderNo string, date model.Date, status, orderType, remark string, + items []inItemSeed, + reviewerID uint64, reviewedAt *time.Time, +) stockInResult { + var o model.StockInOrder + if db.Where("shop_id = ? AND order_no = ?", shopID, orderNo).First(&o).Error != nil { + o = model.StockInOrder{ + TenantBase: model.TenantBase{ShopID: shopID}, + OrderNo: orderNo, + Type: orderType, + WarehouseID: whID, + PartnerID: &partnerID, + OperatorID: opID, + Status: status, + OrderDate: date, + Remark: remark, + } + if reviewerID > 0 { + o.ReviewerID = &reviewerID + o.ReviewedAt = reviewedAt + } + db.Create(&o) + + var total float64 + for _, it := range items { + item := model.StockInItem{ + OrderID: o.ID, + ShopID: shopID, + ProductID: it.productID, + Quantity: it.qty, + UnitPrice: it.price, + TotalPrice: it.qty * it.price, + BatchNo: it.batchNo, + Remark: it.remark, + } + db.Create(&item) + total += item.TotalPrice + } + db.Model(&o).Update("total_amount", total) + o.TotalAmount = total + + fmt.Printf("✅ 入库单:%s [%s] 供应商ID=%d 仓库ID=%d 金额=%.0f\n", + orderNo, status, partnerID, whID, total) + } else { + fmt.Printf("⏭ 入库单:%s\n", orderNo) + } + return stockInResult{order: o, items: items} +} + +// ── 出库单 ──────────────────────────────────────────── + +func createStockOutOrder( + db *gorm.DB, shopID, whID, opID, partnerID uint64, + orderNo string, date model.Date, status, orderType, remark string, + items []outItemSeed, + reviewerID uint64, reviewedAt *time.Time, +) stockOutResult { + var o model.StockOutOrder + if db.Where("shop_id = ? AND order_no = ?", shopID, orderNo).First(&o).Error != nil { + o = model.StockOutOrder{ + TenantBase: model.TenantBase{ShopID: shopID}, + OrderNo: orderNo, + Type: orderType, + WarehouseID: whID, + PartnerID: &partnerID, + OperatorID: opID, + Status: status, + OrderDate: date, + Remark: remark, + } + if reviewerID > 0 { + o.ReviewerID = &reviewerID + o.ReviewedAt = reviewedAt + } + db.Create(&o) + + var total float64 + for _, it := range items { + item := model.StockOutItem{ + OrderID: o.ID, + ShopID: shopID, + ProductID: it.productID, + Quantity: it.qty, + UnitPrice: it.price, + TotalPrice: it.qty * it.price, + Remark: it.remark, + } + db.Create(&item) + total += item.TotalPrice + } + db.Model(&o).Update("total_amount", total) + o.TotalAmount = total + + fmt.Printf("✅ 出库单:%s [%s] 客户ID=%d 仓库ID=%d 金额=%.0f\n", + orderNo, status, partnerID, whID, total) + } else { + fmt.Printf("⏭ 出库单:%s\n", orderNo) + } + return stockOutResult{order: o, items: items} +} + +// ── 库存 ──────────────────────────────────────────── + +func updateInventoryBatch(db *gorm.DB, shopID, whID, opID uint64, r stockInResult) { + for _, it := range r.items { + var inv model.Inventory + db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?", shopID, whID, it.productID).First(&inv) + before := inv.Quantity + after := before + it.qty + if inv.ID == 0 { + inv = model.Inventory{ShopID: shopID, WarehouseID: whID, ProductID: it.productID, Quantity: after} + db.Create(&inv) + } else { + db.Model(&inv).Update("quantity", after) + } + db.Create(&model.InventoryLog{ + ShopID: shopID, WarehouseID: whID, ProductID: it.productID, + Direction: "in", Quantity: it.qty, QtyBefore: before, QtyAfter: after, + RefType: "stock_in", RefID: r.order.ID, OperatorID: &opID, + }) + } +} + +func deductInventoryBatch(db *gorm.DB, shopID, whID, opID uint64, r stockOutResult) { + for _, it := range r.items { + var inv model.Inventory + db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?", shopID, whID, it.productID).First(&inv) + before := inv.Quantity + after := before - it.qty + if after < 0 { + after = 0 + } + db.Model(&inv).Update("quantity", after) + db.Create(&model.InventoryLog{ + ShopID: shopID, WarehouseID: whID, ProductID: it.productID, + Direction: "out", Quantity: it.qty, QtyBefore: before, QtyAfter: after, + RefType: "stock_out", RefID: r.order.ID, OperatorID: &opID, + }) + } +} + +// ── 财务记录 ──────────────────────────────────────────── + +func createFinanceRecord(db *gorm.DB, shopID, partnerID, opID uint64, + ftype string, amount, balance float64, refType string, refID uint64, + date model.Date, remark string, +) { + var f model.FinanceRecord + if db.Where("shop_id = ? AND ref_type = ? AND ref_id = ? AND type = ?", shopID, refType, refID, ftype).First(&f).Error != nil { + f = model.FinanceRecord{ + ShopID: shopID, + PartnerID: &partnerID, + Type: ftype, + Amount: amount, + Balance: balance, + RefType: refType, + RefID: &refID, + OperatorID: opID, + RecordDate: date.Time, + Remark: remark, + } + db.Create(&f) + fmt.Printf("✅ 财务记录:%s %.0f 元(%s)\n", ftype, amount, remark) + } +} + +// ── 工具函数 ──────────────────────────────────────────── func mustHash(plain string) string { b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) @@ -302,66 +688,6 @@ func mustHash(plain string) string { return string(b) } -func upsertShop(db *gorm.DB) model.Shop { - var s model.Shop - if db.Where("code = ?", "H001").First(&s).Error != nil { - s = model.Shop{Name: "测试门店", Code: "H001", Address: "北京市朝阳区测试街1号", Phone: "010-12345678", ManagerName: "张三"} - db.Create(&s) - fmt.Printf("✅ 门店:%s (%s)\n", s.Name, s.Code) - } else { - fmt.Printf("⏭ 门店:%s (%s)\n", s.Name, s.Code) - } - return s -} - -func upsertUser(db *gorm.DB, shopID uint64, username, realName, role, hash string) model.User { - var u model.User - if db.Where("shop_id = ? AND username = ?", shopID, username).First(&u).Error != nil { - u = model.User{TenantBase: model.TenantBase{ShopID: shopID}, Username: username, PasswordHash: hash, RealName: realName, Role: role, IsActive: true} - db.Create(&u) - fmt.Printf("✅ 用户:%s(%s)\n", username, role) - } else { - fmt.Printf("⏭ 用户:%s(%s)\n", username, role) - } - return u -} - -func upsertWarehouse(db *gorm.DB, shopID uint64, name, location string, isDefault bool) model.Warehouse { - var w model.Warehouse - if db.Where("shop_id = ? AND name = ?", shopID, name).First(&w).Error != nil { - w = model.Warehouse{TenantBase: model.TenantBase{ShopID: shopID}, Name: name, Location: location, IsDefault: isDefault} - db.Create(&w) - fmt.Printf("✅ 仓库:%s\n", name) - } else { - fmt.Printf("⏭ 仓库:%s\n", name) - } - return w -} - -func upsertProduct(db *gorm.DB, shopID uint64, name, series, unit, code string) model.Product { - var p model.Product - if db.Where("shop_id = ? AND name = ?", shopID, name).First(&p).Error != nil { - p = model.Product{TenantBase: model.TenantBase{ShopID: shopID}, Name: name, Series: series, Unit: unit, Code: code} - db.Create(&p) - fmt.Printf("✅ 商品:%s\n", name) - } else { - fmt.Printf("⏭ 商品:%s\n", name) - } - return p -} - -func upsertPartner(db *gorm.DB, shopID uint64, name, ptype, contact, phone string) model.Partner { - var p model.Partner - if db.Where("shop_id = ? AND name = ?", shopID, name).First(&p).Error != nil { - p = model.Partner{TenantBase: model.TenantBase{ShopID: shopID}, Name: name, Type: ptype, Contact: contact, Phone: phone} - db.Create(&p) - fmt.Printf("✅ 往来单位:%s(%s)\n", name, ptype) - } else { - fmt.Printf("⏭ 往来单位:%s\n", name) - } - return p -} - func upsertNumberRule(db *gorm.DB, shopID uint64, ruleType, prefix string, currentNo int) { var r model.NumberRule if db.Where("shop_id = ? AND type = ?", shopID, ruleType).First(&r).Error != nil { @@ -370,94 +696,3 @@ func upsertNumberRule(db *gorm.DB, shopID uint64, ruleType, prefix string, curre fmt.Printf("✅ 编号规则:%s → %s\n", ruleType, prefix) } } - -func upsertStockInOrder(db *gorm.DB, shopID, whID, opID, partnerID uint64, orderNo string, date model.Date) model.StockInOrder { - var o model.StockInOrder - if db.Where("shop_id = ? AND order_no = ?", shopID, orderNo).First(&o).Error != nil { - o = model.StockInOrder{ - TenantBase: model.TenantBase{ShopID: shopID}, - OrderNo: orderNo, - Type: "purchase", - WarehouseID: whID, - PartnerID: &partnerID, - OperatorID: opID, - Status: "draft", - OrderDate: date, - } - db.Create(&o) - fmt.Printf("✅ 入库单:%s\n", orderNo) - } - return o -} - -func upsertStockOutOrder(db *gorm.DB, shopID, whID, opID, partnerID uint64, orderNo string, date model.Date) model.StockOutOrder { - var o model.StockOutOrder - if db.Where("shop_id = ? AND order_no = ?", shopID, orderNo).First(&o).Error != nil { - o = model.StockOutOrder{ - TenantBase: model.TenantBase{ShopID: shopID}, - OrderNo: orderNo, - Type: "sale", - WarehouseID: whID, - PartnerID: &partnerID, - OperatorID: opID, - Status: "draft", - OrderDate: date, - } - db.Create(&o) - fmt.Printf("✅ 出库单:%s\n", orderNo) - } - return o -} - -func updateInventory(db *gorm.DB, shopID, whID, productID uint64, qty float64, refID uint64, opID uint64) { - var inv model.Inventory - db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?", shopID, whID, productID).First(&inv) - before := inv.Quantity - after := before + qty - - if inv.ID == 0 { - inv = model.Inventory{ShopID: shopID, WarehouseID: whID, ProductID: productID, Quantity: after} - db.Create(&inv) - } else { - db.Model(&inv).Update("quantity", after) - } - - log := model.InventoryLog{ - ShopID: shopID, - WarehouseID: whID, - ProductID: productID, - Direction: "in", - Quantity: qty, - QtyBefore: before, - QtyAfter: after, - RefType: "stock_in", - RefID: refID, - OperatorID: &opID, - } - db.Create(&log) -} - -func updateInventoryOut(db *gorm.DB, shopID, whID, productID uint64, qty float64, refID uint64, opID uint64) { - var inv model.Inventory - db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?", shopID, whID, productID).First(&inv) - before := inv.Quantity - after := before - qty - if after < 0 { - after = 0 - } - db.Model(&inv).Update("quantity", after) - - log := model.InventoryLog{ - ShopID: shopID, - WarehouseID: whID, - ProductID: productID, - Direction: "out", - Quantity: qty, - QtyBefore: before, - QtyAfter: after, - RefType: "stock_out", - RefID: refID, - OperatorID: &opID, - } - db.Create(&log) -}