Files
jiu/backend/cmd/seed/main.go
T
wangjia 86baaf3a1c
Deploy / deploy (push) Successful in 1m13s
fix(model): users 唯一索引改为 (shop_id, username) 复合索引
原来 uniqueIndex:uk_shop_username 只在 Username 字段上,导致用户名全局唯一。
S002/S003 的 admin 用户与 S001 冲突,INSERT 被静默跳过,无法登录。

将 User 的 TenantBase 改为直接定义 ShopID,使其参与复合唯一索引。
需要 Reset DB 使新索引生效。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 14:50:34 +08:00

776 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// seed — 初始化/重置测试数据
//
// 用法(在 backend/ 目录下执行):
//
// go run cmd/seed/main.go # 写入数据(已存在则跳过)
// go run cmd/seed/main.go --reset # 删表重建 + 写入数据
// go run cmd/seed/main.go --clear # 清空业务数据 + 重新写入(保留表结构)
package main
import (
"fmt"
stdlog "log"
"os"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/model"
)
var allModels = []any{
&model.Shop{},
&model.User{},
&model.License{},
&model.ProductCategory{},
&model.Product{},
&model.ProductNameOption{},
&model.ProductSeriesOption{},
&model.ProductSpecOption{},
&model.Warehouse{},
&model.Partner{},
&model.StockInOrder{},
&model.StockInItem{},
&model.StockOutOrder{},
&model.StockOutItem{},
&model.Inventory{},
&model.InventoryLog{},
&model.InventoryCheck{},
&model.InventoryCheckItem{},
&model.FinanceRecord{},
&model.NumberRule{},
}
// 清空顺序(子表先清)
var truncateOrder = []string{
"inventory_check_items", "inventory_checks",
"inventory_logs", "inventories",
"stock_out_items", "stock_out_orders",
"stock_in_items", "stock_in_orders",
"finance_records", "number_rules",
"partners", "warehouses",
"products", "product_spec_options", "product_series_options", "product_name_options", "product_categories",
"licenses", "users", "shops",
}
func main() {
mode := ""
if len(os.Args) > 1 {
mode = os.Args[1]
}
config.Load()
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},
),
})
if err != nil {
stdlog.Fatalf("连接数据库失败: %v", err)
}
switch mode {
case "--reset":
fmt.Println("⚠️ --reset:删除所有表并重建...")
db.Exec("SET FOREIGN_KEY_CHECKS = 0")
if err := db.Migrator().DropTable(allModels...); err != nil {
stdlog.Fatalf("删表失败: %v", err)
}
db.Exec("SET FOREIGN_KEY_CHECKS = 1")
fmt.Println("✅ 所有表已删除")
case "--clear":
fmt.Println("🧹 --clear:清空所有业务数据...")
db.Exec("SET FOREIGN_KEY_CHECKS = 0")
for _, t := range truncateOrder {
if err := db.Exec("TRUNCATE TABLE `" + t + "`").Error; err != nil {
fmt.Printf(" 跳过 %s%v\n", t, err)
} else {
fmt.Printf(" 清空 %s\n", t)
}
}
db.Exec("SET FOREIGN_KEY_CHECKS = 1")
fmt.Println("✅ 数据已清空")
return
}
// 同步表结构
if err := db.AutoMigrate(allModels...); err != nil {
stdlog.Fatalf("AutoMigrate 失败: %v", err)
}
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 := upsert(db, &model.Shop{}, "code = ?", "S001", func() any {
return &model.Shop{
Name: "测试酒库门店",
Code: "S001",
Address: "北京市朝阳区酒仙桥路甲1号",
Phone: "010-12345678",
ManagerName: "张三",
BusinessLicense: "91110105MA0000000X",
}
}).(model.Shop)
// ═══════════════════════════════════════════════════
// 用户
// ═══════════════════════════════════════════════════
hash := mustHash("password123")
admin := upsert(db, &model.User{}, "shop_id = ? AND username = ?", shop.ID, "admin", func() any {
return &model.User{
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{
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{
ShopID: shop.ID,
Username: "test", PasswordHash: hash, RealName: "王五(只读)",
Phone: "13800000003", Role: "readonly", IsActive: true,
}
})
// ═══════════════════════════════════════════════════
// 仓库
// ═══════════════════════════════════════════════════
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)
// ═══════════════════════════════════════════════════
// 商品
// ═══════════════════════════════════════════════════
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/瓶", "瓶", "贵州茅台", &catBaijiu.ID, 2350, 2800, 10, "酱香型白酒,53度,飞天系列"},
{"WLY-001", "6902345678901", "五粮液 52度", "五粮液", "500ml/瓶", "瓶", "宜宾五粮液", &catBaijiu.ID, 950, 1200, 6, "浓香型白酒,52度,普五系列"},
{"YH-001", "6903456789012", "洋河梦之蓝 M6", "洋河", "500ml/瓶", "瓶", "江苏洋河", &catBaijiu.ID, 560, 680, 6, "浓香型,绵柔苏酒代表"},
{"LZ-001", "6904567890123", "泸州老窖 特曲", "泸州老窖", "500ml/瓶", "瓶", "泸州老窖", &catBaijiu.ID, 420, 520, 6, "浓香鼻祖,特曲系列"},
{"JNC-001", "6905678901234", "剑南春 水晶剑", "剑南春", "500ml/瓶", "瓶", "剑南春", &catBaijiu.ID, 390, 480, 6, "浓香型,绵竹名酒"},
{"LJ-001", "6906789012345", "郎酒 红花郎10", "郎酒", "500ml/瓶", "瓶", "古蔺郎酒", &catBaijiu.ID, 320, 420, 6, "酱香型,赤水河畔酿造"},
{"LF-001", "3760093550058", "拉菲古堡 2018", "波尔多", "750ml/瓶", "瓶", "Château Lafite", &catImport.ID, 3800, 5200, 3, "波尔多一级名庄,2018年份"},
{"RTM-001", "3021691010008", "人头马 VSOP", "人头马", "700ml/瓶", "瓶", "Rémy Martin", &catImport.ID, 480, 680, 3, "法国干邑,VSOP级别"},
}
var prods []model.Product
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)
}
// ═══════════════════════════════════════════════════
// 商品名称/系列/规格选项
// ═══════════════════════════════════════════════════
type dimSeed struct{ code, name string }
for _, s := range []dimSeed{
{"NA001", "飞天茅台 53度"}, {"NA002", "五粮液 52度"}, {"NA003", "洋河梦之蓝 M6"},
{"NA004", "泸州老窖 特曲"}, {"NA005", "剑南春 水晶剑"}, {"NA006", "郎酒 红花郎10"},
{"NA007", "拉菲古堡 2018"}, {"NA008", "人头马 VSOP"},
} {
var opt model.ProductNameOption
if db.Where("shop_id = ? AND name = ?", shop.ID, s.name).First(&opt).Error != nil {
opt = model.ProductNameOption{TenantBase: model.TenantBase{ShopID: shop.ID}, Code: s.code, Name: s.name}
db.Create(&opt)
fmt.Printf("✅ 名称选项:%s %s\n", s.code, s.name)
}
}
for _, s := range []dimSeed{
{"SE001", "茅台"}, {"SE002", "五粮液"}, {"SE003", "洋河"}, {"SE004", "泸州老窖"},
{"SE005", "剑南春"}, {"SE006", "郎酒"}, {"SE007", "波尔多"}, {"SE008", "人头马"},
} {
var opt model.ProductSeriesOption
if db.Where("shop_id = ? AND name = ?", shop.ID, s.name).First(&opt).Error != nil {
opt = model.ProductSeriesOption{TenantBase: model.TenantBase{ShopID: shop.ID}, Code: s.code, Name: s.name}
db.Create(&opt)
fmt.Printf("✅ 系列选项:%s %s\n", s.code, s.name)
}
}
for _, s := range []struct{ code, name string; quantity int }{
{"GG001", "500ml/瓶", 1}, {"GG002", "750ml/瓶", 1}, {"GG003", "700ml/瓶", 1},
} {
var opt model.ProductSpecOption
if db.Where("shop_id = ? AND name = ?", shop.ID, s.name).First(&opt).Error != nil {
opt = model.ProductSpecOption{
TenantBase: model.TenantBase{ShopID: shop.ID},
Code: s.code,
Name: s.name,
Quantity: s.quantity,
}
db.Create(&opt)
fmt.Printf("✅ 规格选项:%s\n", s.name)
}
}
// ═══════════════════════════════════════════════════
// 往来单位
// ═══════════════════════════════════════════════════
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", 3)
upsertNumberRule(db, shop.ID, "inventory_check", "PD", 1)
// ═══════════════════════════════════════════════════
// 入库单
// ═══════════════════════════════════════════════════
// #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)))
// #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)))
// #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", "人头马VSOP2024年进口"},
}, admin.ID, ptrT(today.AddDate(0, 0, -2)))
// #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)
// ═══════════════════════════════════════════════════
// 出库单
// ═══════════════════════════════════════════════════
// #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)))
// #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.Printf(" 门店编号:%s\n", shop.Code)
fmt.Println(" admin / password123 (管理员)")
fmt.Println(" operator / password123 (操作员)")
fmt.Println(" test / password123 (只读)")
fmt.Println()
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 qtyBefore float64
db.Model(&model.Inventory{}).
Where("shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL", shopID, whID, it.productID).
Select("COALESCE(SUM(quantity), 0)").Scan(&qtyBefore)
productIDCopy := it.productID
whIDCopy := whID
inv := model.Inventory{
ShopID: shopID,
WarehouseID: &whIDCopy,
ProductID: &productIDCopy,
Quantity: it.qty,
}
db.Create(&inv)
db.Create(&model.InventoryLog{
ShopID: shopID, WarehouseID: whID, ProductID: it.productID,
Direction: "in", Quantity: it.qty, QtyBefore: qtyBefore, QtyAfter: qtyBefore + it.qty,
RefType: "stock_in", RefID: r.order.ID, OperatorID: &opID,
})
}
}
func deductInventoryBatch(db *gorm.DB, shopID, whID, opID uint64, r stockOutResult) {
now := time.Now()
for _, it := range r.items {
var qtyBefore float64
db.Model(&model.Inventory{}).
Where("shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL", shopID, whID, it.productID).
Select("COALESCE(SUM(quantity), 0)").Scan(&qtyBefore)
// FIFO deduction
var batches []model.Inventory
db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ? AND quantity > 0 AND deleted_at IS NULL",
shopID, whID, it.productID).
Order("created_at ASC").Find(&batches)
remaining := it.qty
for i := range batches {
if remaining <= 0 {
break
}
b := &batches[i]
if b.Quantity <= remaining {
remaining -= b.Quantity
db.Model(b).Updates(map[string]interface{}{"quantity": 0, "deleted_at": now})
} else {
db.Model(b).Update("quantity", gorm.Expr("quantity - ?", remaining))
remaining = 0
}
}
after := qtyBefore - it.qty
if after < 0 {
after = 0
}
db.Create(&model.InventoryLog{
ShopID: shopID, WarehouseID: whID, ProductID: it.productID,
Direction: "out", Quantity: it.qty, QtyBefore: qtyBefore, 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)
if err != nil {
stdlog.Fatalf("bcrypt 失败: %v", err)
}
return string(b)
}
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 {
r = model.NumberRule{ShopID: shopID, Type: ruleType, Prefix: prefix, CurrentNo: currentNo, DateFormat: "YYYYMMDD"}
db.Create(&r)
fmt.Printf("✅ 编号规则:%s → %s\n", ruleType, prefix)
}
}