fix(backend): JWT config mapstructure tag 修复 + 模型从 hotel 重构为 shop
- 修复 JWTConfig 缺少 mapstructure tag 导致 access_expire_min 解析为 0, token 签发即过期,所有 API 请求返回 401 - 全部 config struct 补齐 mapstructure tag(secret/dsn/hmac_secret 等) - 模型层从 hotel/HotelID 统一重命名为 shop/ShopID - 删除旧 migrations(001-004),新增 001_init 综合迁移文件 - 更新 schema.sql、testutil、handler/service/model 相关引用 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,463 @@
|
||||
// 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, 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", 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 time.Time) 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 time.Time) 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