Files
jiu/backend/cmd/seed/main.go
T
wangjia 754275cbd6 fix(backend): 修复日期字段解析失败导致创建单据报错
新增 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>
2026-04-07 22:29:15 +08:00

464 lines
16 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.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)
}