Files
jiu/backend/testutil/setup.go
T
wangjia 7bbc944ae2
Deploy Server / release-deploy-server (push) Successful in 51s
chore: release server-v1.0.62
服务端安全加固:多维限流(按 IP/按门店)+ 敏感接口独立速率上限抵御 DDoS/刷接口;
登录暴力破解新增按来源 IP 锁定;反代后正确识别真实客户端 IP;
门店 custom_fields 轻量配置(录入默认值)透传保存。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
2026-06-19 20:20:44 +08:00

531 lines
14 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,
},
Session: config.SessionConfig{
LimitDesktop: 2,
LimitMobile: 2,
LimitWeb: 2,
MaxFailures: 5,
IPMaxFailures: 20,
LockMinutes: 15,
},
// 测试默认关闭限流,避免压测式用例触发 429;限流逻辑由 ratelimit_test.go 显式开启验证。
RateLimit: config.RateLimitConfig{Enabled: false},
}
}
// 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,
description TEXT,
address TEXT,
phone TEXT,
manager_name TEXT,
logo_url TEXT DEFAULT '',
wechat_id 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,
last_login_at DATETIME,
custom_fields TEXT
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS uk_shop_username ON users(shop_id, username)`,
`CREATE TABLE IF NOT EXISTS user_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
shop_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
sid TEXT NOT NULL UNIQUE,
device_id TEXT,
device_name TEXT,
platform TEXT,
platform_class TEXT,
ip TEXT,
user_agent TEXT,
refresh_jti TEXT,
created_at DATETIME,
last_seen_at DATETIME,
revoked_at DATETIME,
revoked_reason TEXT,
revoked_by INTEGER,
refresh_exp_at DATETIME
)`,
`CREATE TABLE IF NOT EXISTS login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
shop_code TEXT,
username TEXT,
ip TEXT,
user_agent TEXT,
success INTEGER DEFAULT 0,
reason TEXT,
created_at DATETIME
)`,
`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',
tier TEXT DEFAULT 'standard',
expires_at DATETIME,
is_active INTEGER DEFAULT 1,
max_devices INTEGER DEFAULT 3,
features TEXT,
activated_at DATETIME
)`,
`CREATE TABLE IF NOT EXISTS license_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
type TEXT DEFAULT 'annual',
tier TEXT DEFAULT 'standard',
duration_days INTEGER NOT NULL DEFAULT 0,
max_devices INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'unused',
redeemed_shop_id INTEGER,
redeemed_at DATETIME,
redeemed_device_id TEXT,
batch TEXT,
note TEXT,
created_at DATETIME,
updated_at DATETIME
)`,
`CREATE TABLE IF NOT EXISTS license_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
license_id INTEGER NOT NULL,
shop_id INTEGER NOT NULL,
device_id TEXT NOT NULL,
device_name TEXT,
platform TEXT,
activated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(license_id, device_id)
)`,
`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,
name_pinyin TEXT DEFAULT '',
name_initials TEXT DEFAULT '',
origin_id INTEGER,
shelf_life_id INTEGER,
storage_id INTEGER,
description_doc_id INTEGER
)`,
`CREATE TABLE IF NOT EXISTS product_origin_options (
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,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_shelf_life_options (
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,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_storage_options (
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,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_description_docs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
title TEXT NOT NULL,
content 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
}