754275cbd6
新增 model.Date 类型,支持前端传入的纯日期格式 YYYY-MM-DD: - UnmarshalJSON 优先解析 YYYY-MM-DD,回退支持 RFC3339 - MarshalJSON 输出 YYYY-MM-DD - Value/Scan 实现 GORM MySQL DATE 列读写 - StockInOrder/StockOutOrder/InventoryCheck 的 OrderDate/CheckDate 改用 Date 类型 feat(client): SelectionArea 全局开启文字可选中复制 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
464 lines
16 KiB
Go
464 lines
16 KiB
Go
// 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.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_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()
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 门店
|
||
// ═══════════════════════════════════════════════════
|
||
shop := upsertShop(db)
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 用户
|
||
// ═══════════════════════════════════════════════════
|
||
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)
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 仓库
|
||
// ═══════════════════════════════════════════════════
|
||
wh1 := upsertWarehouse(db, shop.ID, "主仓库", "A栋1层", true)
|
||
wh2 := upsertWarehouse(db, shop.ID, "备用仓库", "B栋2层", false)
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 商品
|
||
// ═══════════════════════════════════════════════════
|
||
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"},
|
||
}
|
||
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)
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 往来单位
|
||
// ═══════════════════════════════════════════════════
|
||
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")
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 编号规则
|
||
// ═══════════════════════════════════════════════════
|
||
upsertNumberRule(db, shop.ID, "stock_in", "RK", 5)
|
||
upsertNumberRule(db, shop.ID, "stock_out", "CK", 2)
|
||
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())
|
||
|
||
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瓶
|
||
},
|
||
},
|
||
}
|
||
|
||
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
|
||
|
||
// 更新库存
|
||
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,
|
||
})
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 出库单(已审核)→ 减库存
|
||
// ═══════════════════════════════════════════════════
|
||
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,
|
||
})
|
||
|
||
// 备用仓库放一些商品(直接写库存)
|
||
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)
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// 打印汇总
|
||
// ═══════════════════════════════════════════════════
|
||
fmt.Println("═══════════════════════════════════════")
|
||
fmt.Println(" 测试数据写入完成")
|
||
fmt.Println("═══════════════════════════════════════")
|
||
fmt.Println()
|
||
fmt.Println(" 登录信息:")
|
||
fmt.Println(" 门店编号:H001")
|
||
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("═══════════════════════════════════════")
|
||
}
|
||
|
||
// ── 辅助函数 ────────────────────────────────────────────
|
||
|
||
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 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 {
|
||
r = model.NumberRule{ShopID: shopID, Type: ruleType, Prefix: prefix, CurrentNo: currentNo, DateFormat: "YYYYMMDD"}
|
||
db.Create(&r)
|
||
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)
|
||
}
|