package testutil import ( "fmt" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/wangjia/jiu/backend/config" "github.com/wangjia/jiu/backend/internal/middleware" "github.com/wangjia/jiu/backend/internal/model" ) // InitConfig 初始化测试用配置 func InitConfig() { config.C = config.Config{ Server: config.ServerConfig{ Port: "8080", Mode: "test", }, JWT: config.JWTConfig{ Secret: "test-secret-key-for-testing", AccessExpireMin: 60, RefreshExpireH: 168, }, License: config.LicenseConfig{ HMACSecret: "test-license-hmac-secret", }, } } // SetupTestDB 创建 SQLite in-memory 测试数据库并自动迁移 func SetupTestDB() *gorm.DB { InitConfig() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { panic(fmt.Sprintf("failed to open sqlite: %v", err)) } // SQLite 不支持 ENUM,直接用原始 SQL 建表 stmts := []string{ `CREATE TABLE IF NOT EXISTS shops ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, name TEXT NOT NULL, code TEXT UNIQUE, address TEXT, phone TEXT, manager_name TEXT, business_license TEXT, shop_photos TEXT, custom_fields TEXT )`, `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, username TEXT, password_hash TEXT, real_name TEXT, phone TEXT, role TEXT DEFAULT 'operator', is_active INTEGER DEFAULT 1, custom_fields TEXT )`, `CREATE UNIQUE INDEX IF NOT EXISTS uk_shop_username ON users(shop_id, username)`, `CREATE TABLE IF NOT EXISTS licenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, license_key TEXT UNIQUE, device_id TEXT, type TEXT DEFAULT 'trial', expires_at DATETIME, is_active INTEGER DEFAULT 1, features TEXT, activated_at DATETIME )`, `CREATE TABLE IF NOT EXISTS product_categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, name TEXT NOT NULL, parent_id INTEGER, sort_order INTEGER DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, code TEXT, barcode TEXT, name TEXT NOT NULL, series TEXT, spec TEXT, unit TEXT, category_id INTEGER, brand TEXT, purchase_price REAL, sale_price REAL, min_stock INTEGER DEFAULT 0, custom_fields TEXT, remark TEXT )`, `CREATE TABLE IF NOT EXISTS warehouses ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, name TEXT NOT NULL, location TEXT, is_default INTEGER DEFAULT 0, custom_fields TEXT )`, `CREATE TABLE IF NOT EXISTS partners ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, code TEXT, name TEXT NOT NULL, type TEXT DEFAULT 'supplier', contact TEXT, phone TEXT, address TEXT, bank_account TEXT, credit_limit REAL, balance REAL DEFAULT 0, custom_fields TEXT, remark TEXT )`, `CREATE TABLE IF NOT EXISTS stock_in_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, order_no TEXT, type TEXT DEFAULT 'purchase', warehouse_id INTEGER NOT NULL, partner_id INTEGER, operator_id INTEGER NOT NULL, reviewer_id INTEGER, status TEXT DEFAULT 'draft', order_date DATETIME, total_amount REAL DEFAULT 0, reviewed_at DATETIME, custom_fields TEXT, remark TEXT )`, `CREATE UNIQUE INDEX IF NOT EXISTS uk_shop_sio_order_no ON stock_in_orders(shop_id, order_no)`, `CREATE TABLE IF NOT EXISTS stock_in_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, order_id INTEGER NOT NULL, shop_id INTEGER NOT NULL, product_id INTEGER NOT NULL, quantity REAL NOT NULL, unit_price REAL DEFAULT 0, total_price REAL DEFAULT 0, batch_no TEXT, custom_fields TEXT, remark TEXT )`, `CREATE TABLE IF NOT EXISTS stock_out_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, order_no TEXT, type TEXT DEFAULT 'sale', warehouse_id INTEGER NOT NULL, partner_id INTEGER, operator_id INTEGER NOT NULL, reviewer_id INTEGER, status TEXT DEFAULT 'draft', order_date DATETIME, total_amount REAL DEFAULT 0, reviewed_at DATETIME, custom_fields TEXT, remark TEXT )`, `CREATE UNIQUE INDEX IF NOT EXISTS uk_shop_soo_order_no ON stock_out_orders(shop_id, order_no)`, `CREATE TABLE IF NOT EXISTS stock_out_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, order_id INTEGER NOT NULL, shop_id INTEGER NOT NULL, product_id INTEGER NOT NULL, quantity REAL NOT NULL, unit_price REAL DEFAULT 0, total_price REAL DEFAULT 0, custom_fields TEXT, remark TEXT )`, `CREATE TABLE IF NOT EXISTS inventories ( id INTEGER PRIMARY KEY AUTOINCREMENT, shop_id INTEGER NOT NULL, warehouse_id INTEGER NOT NULL, product_id INTEGER NOT NULL, quantity REAL DEFAULT 0, updated_at DATETIME, UNIQUE(shop_id, warehouse_id, product_id) )`, `CREATE TABLE IF NOT EXISTS inventory_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, shop_id INTEGER NOT NULL, warehouse_id INTEGER NOT NULL, product_id INTEGER NOT NULL, direction TEXT, quantity REAL, qty_before REAL, qty_after REAL, ref_type TEXT, ref_id INTEGER, operator_id INTEGER, created_at DATETIME )`, `CREATE TABLE IF NOT EXISTS inventory_checks ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, shop_id INTEGER NOT NULL, check_no TEXT, warehouse_id INTEGER NOT NULL, operator_id INTEGER NOT NULL, status TEXT DEFAULT 'draft', check_date DATETIME, remark TEXT )`, `CREATE UNIQUE INDEX IF NOT EXISTS uk_shop_check_no ON inventory_checks(shop_id, check_no)`, `CREATE TABLE IF NOT EXISTS inventory_check_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME, check_id INTEGER NOT NULL, shop_id INTEGER NOT NULL, product_id INTEGER NOT NULL, system_qty REAL, actual_qty REAL, remark TEXT )`, `CREATE TABLE IF NOT EXISTS number_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, shop_id INTEGER NOT NULL, type TEXT, prefix TEXT DEFAULT '', current_no INTEGER DEFAULT 0, date_format TEXT DEFAULT 'YYYYMMDD', updated_at DATETIME, UNIQUE(shop_id, type) )`, } for _, stmt := range stmts { if err := db.Exec(stmt).Error; err != nil { panic(fmt.Sprintf("failed to create table: %v\nSQL: %s", err, stmt)) } } return db } // CreateTestShop 创建测试门店 func CreateTestShop(db *gorm.DB, code string) *model.Shop { shop := &model.Shop{ Name: "Test Shop " + code, Code: code, } if err := db.Create(shop).Error; err != nil { panic(fmt.Sprintf("failed to create test shop: %v", err)) } return shop } // hashPassword 内部使用的密码哈希函数,避免循环依赖 func hashPassword(plain string) string { b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) if err != nil { panic(fmt.Sprintf("failed to hash password: %v", err)) } return string(b) } // CreateTestUser 创建测试用户 func CreateTestUser(db *gorm.DB, shopID uint64, username, password, role string) *model.User { hash := hashPassword(password) user := &model.User{ TenantBase: model.TenantBase{ Base: model.Base{}, ShopID: shopID, }, Username: username, PasswordHash: hash, RealName: "Test User " + username, Role: role, IsActive: true, } if err := db.Create(user).Error; err != nil { panic(fmt.Sprintf("failed to create test user: %v", err)) } return user } // CreateTestWarehouse 创建测试仓库 func CreateTestWarehouse(db *gorm.DB, shopID uint64, name string) *model.Warehouse { w := &model.Warehouse{ TenantBase: model.TenantBase{ ShopID: shopID, }, Name: name, IsDefault: true, } if err := db.Create(w).Error; err != nil { panic(fmt.Sprintf("failed to create test warehouse: %v", err)) } return w } // CreateTestProduct 创建测试商品 func CreateTestProduct(db *gorm.DB, shopID uint64, name string) *model.Product { p := &model.Product{ TenantBase: model.TenantBase{ ShopID: shopID, }, Name: name, Code: "P-" + name, Unit: "个", } if err := db.Create(p).Error; err != nil { panic(fmt.Sprintf("failed to create test product: %v", err)) } return p } // GetAuthToken 生成测试 JWT token func GetAuthToken(userID, shopID uint64, role string) string { InitConfig() now := time.Now() claims := middleware.Claims{ UserID: userID, ShopID: shopID, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)), IssuedAt: jwt.NewNumericDate(now), }, } token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims). SignedString([]byte(config.C.JWT.Secret)) if err != nil { panic(fmt.Sprintf("failed to sign token: %v", err)) } return token }