feat(db): 补全测试种子数据所有字段
入库单补全供应商/仓库,出库单补全客户/仓库,商品补全价格/条码/规格, 往来单位补全地址/银行账户/信用额度,财务记录关联已审核出库单。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+528
-293
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user