Files
jiu/backend/testutil/setup.go
T
wangjia fbb4f90ebd
Deploy / deploy (push) Successful in 1m15s
fix(test): shops 测试表补加 logo_url 列
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:15:39 +08:00

417 lines
11 KiB
Go

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,
logo_url TEXT DEFAULT '',
business_license TEXT,
business_hours 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,
public_id TEXT DEFAULT '',
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,
description TEXT,
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',
status TEXT NOT NULL DEFAULT 'enabled',
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,
production_date DATETIME,
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,
product_id INTEGER,
stock_in_item_id INTEGER,
inventory_check_id INTEGER,
quantity REAL DEFAULT 0,
product_code TEXT DEFAULT '',
product_name TEXT DEFAULT '',
series TEXT DEFAULT '',
spec TEXT DEFAULT '',
unit TEXT DEFAULT '',
warehouse_name TEXT DEFAULT '',
unit_price REAL,
production_date DATETIME,
batch_no TEXT DEFAULT '',
supplier_name TEXT DEFAULT '',
remark TEXT DEFAULT '',
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME
)`,
`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 finance_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
partner_id INTEGER,
type TEXT,
amount REAL DEFAULT 0,
balance REAL DEFAULT 0,
status TEXT DEFAULT 'open',
ref_type TEXT,
ref_id INTEGER,
operator_id INTEGER,
record_date DATETIME,
remark TEXT,
custom_fields 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{
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
}