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:
wangjia
2026-04-07 22:20:12 +08:00
parent 37112d6599
commit 31ea370cea
50 changed files with 2675 additions and 951 deletions
+463
View File
@@ -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)
}