All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m6s
767 lines
26 KiB
Go
767 lines
26 KiB
Go
package order
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/alicebob/miniredis/v2"
|
||
"github.com/hibiken/asynq"
|
||
modelOrder "github.com/perfect-panel/server/internal/model/order"
|
||
"github.com/perfect-panel/server/internal/model/payment"
|
||
subModel "github.com/perfect-panel/server/internal/model/subscribe"
|
||
"github.com/perfect-panel/server/internal/model/user"
|
||
"github.com/perfect-panel/server/internal/svc"
|
||
"github.com/perfect-panel/server/internal/types"
|
||
"github.com/perfect-panel/server/pkg/constant"
|
||
"github.com/perfect-panel/server/pkg/xerr"
|
||
"github.com/redis/go-redis/v9"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
"gorm.io/driver/sqlite"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/logger"
|
||
)
|
||
|
||
// setupNewUserOnlyDB 创建带必要表的 SQLite 内存数据库
|
||
func setupNewUserOnlyDB(t *testing.T) *gorm.DB {
|
||
t.Helper()
|
||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Silent),
|
||
})
|
||
require.NoError(t, err, "failed to open in-memory SQLite")
|
||
db.Exec("PRAGMA foreign_keys = OFF")
|
||
|
||
sqls := []string{
|
||
`CREATE TABLE IF NOT EXISTS "subscribe" (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(255) NOT NULL DEFAULT '',
|
||
language VARCHAR(255) NOT NULL DEFAULT '',
|
||
description TEXT,
|
||
unit_price INTEGER NOT NULL DEFAULT 0,
|
||
unit_time VARCHAR(255) NOT NULL DEFAULT '',
|
||
discount TEXT,
|
||
replacement INTEGER NOT NULL DEFAULT 0,
|
||
inventory INTEGER NOT NULL DEFAULT -1,
|
||
traffic INTEGER NOT NULL DEFAULT 0,
|
||
speed_limit INTEGER NOT NULL DEFAULT 0,
|
||
device_limit INTEGER NOT NULL DEFAULT 0,
|
||
quota INTEGER NOT NULL DEFAULT 0,
|
||
new_user_only TINYINT DEFAULT 0,
|
||
nodes VARCHAR(255),
|
||
node_tags VARCHAR(255),
|
||
show TINYINT NOT NULL DEFAULT 0,
|
||
sell TINYINT NOT NULL DEFAULT 1,
|
||
sort INTEGER NOT NULL DEFAULT 0,
|
||
deduction_ratio INTEGER DEFAULT 0,
|
||
allow_deduction TINYINT DEFAULT 1,
|
||
reset_cycle INTEGER DEFAULT 0,
|
||
renewal_reset TINYINT DEFAULT 0,
|
||
show_original_price TINYINT NOT NULL DEFAULT 1,
|
||
created_at DATETIME,
|
||
updated_at DATETIME
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS "order" (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
parent_id INTEGER DEFAULT NULL,
|
||
user_id INTEGER NOT NULL DEFAULT 0,
|
||
subscription_user_id INTEGER NOT NULL DEFAULT 0,
|
||
order_no VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
|
||
type TINYINT NOT NULL DEFAULT 1,
|
||
quantity INTEGER NOT NULL DEFAULT 1,
|
||
price INTEGER NOT NULL DEFAULT 0,
|
||
amount INTEGER NOT NULL DEFAULT 0,
|
||
gift_amount INTEGER NOT NULL DEFAULT 0,
|
||
discount INTEGER NOT NULL DEFAULT 0,
|
||
coupon VARCHAR(255) DEFAULT NULL,
|
||
coupon_discount INTEGER NOT NULL DEFAULT 0,
|
||
commission INTEGER NOT NULL DEFAULT 0,
|
||
payment_id INTEGER NOT NULL DEFAULT 0,
|
||
method VARCHAR(255) NOT NULL DEFAULT '',
|
||
fee_amount INTEGER NOT NULL DEFAULT 0,
|
||
trade_no VARCHAR(255) DEFAULT NULL,
|
||
app_account_token VARCHAR(255) DEFAULT NULL,
|
||
status TINYINT NOT NULL DEFAULT 1,
|
||
subscribe_id INTEGER NOT NULL DEFAULT 0,
|
||
subscribe_token VARCHAR(255) DEFAULT NULL,
|
||
is_new TINYINT NOT NULL DEFAULT 0,
|
||
created_at DATETIME,
|
||
updated_at DATETIME,
|
||
deleted_at DATETIME DEFAULT NULL
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS "user" (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
password VARCHAR(100) NOT NULL DEFAULT '',
|
||
algo VARCHAR(20) DEFAULT 'default',
|
||
salt VARCHAR(20) DEFAULT NULL,
|
||
avatar TEXT,
|
||
balance INTEGER DEFAULT 0,
|
||
refer_code VARCHAR(20) DEFAULT '',
|
||
referer_id INTEGER DEFAULT 0,
|
||
commission INTEGER DEFAULT 0,
|
||
referral_percentage INTEGER DEFAULT 0,
|
||
only_first_purchase TINYINT DEFAULT 1,
|
||
gift_amount INTEGER DEFAULT 0,
|
||
enable TINYINT DEFAULT 1,
|
||
is_admin TINYINT DEFAULT 0,
|
||
enable_balance_notify TINYINT DEFAULT 0,
|
||
enable_login_notify TINYINT DEFAULT 0,
|
||
enable_subscribe_notify TINYINT DEFAULT 0,
|
||
enable_trade_notify TINYINT DEFAULT 0,
|
||
rules TEXT,
|
||
member_status VARCHAR(20) DEFAULT '',
|
||
created_at DATETIME,
|
||
updated_at DATETIME,
|
||
deleted_at DATETIME DEFAULT NULL
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS "payment" (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||
platform VARCHAR(100) NOT NULL DEFAULT '',
|
||
icon VARCHAR(255) DEFAULT '',
|
||
domain VARCHAR(255) DEFAULT '',
|
||
config TEXT NOT NULL DEFAULT '{}',
|
||
description TEXT,
|
||
fee_mode TINYINT NOT NULL DEFAULT 0,
|
||
fee_percent INTEGER DEFAULT 0,
|
||
fee_amount INTEGER DEFAULT 0,
|
||
enable TINYINT NOT NULL DEFAULT 1,
|
||
token VARCHAR(255) NOT NULL DEFAULT '' UNIQUE
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS "user_subscribe" (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL DEFAULT 0,
|
||
order_id INTEGER NOT NULL DEFAULT 0,
|
||
subscribe_id INTEGER NOT NULL DEFAULT 0,
|
||
start_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
expire_time DATETIME DEFAULT NULL,
|
||
finished_at DATETIME DEFAULT NULL,
|
||
traffic INTEGER DEFAULT 0,
|
||
download INTEGER DEFAULT 0,
|
||
upload INTEGER DEFAULT 0,
|
||
token VARCHAR(255) DEFAULT '' UNIQUE,
|
||
uuid VARCHAR(255) DEFAULT '' UNIQUE,
|
||
status TINYINT DEFAULT 0,
|
||
note VARCHAR(500) DEFAULT '',
|
||
created_at DATETIME,
|
||
updated_at DATETIME
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS "user_device" (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
ip VARCHAR(255) NOT NULL DEFAULT '',
|
||
user_id INTEGER NOT NULL DEFAULT 0,
|
||
user_agent TEXT,
|
||
identifier VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
|
||
short_code VARCHAR(255) NOT NULL DEFAULT '',
|
||
online TINYINT NOT NULL DEFAULT 0,
|
||
enabled TINYINT NOT NULL DEFAULT 1,
|
||
created_at DATETIME,
|
||
updated_at DATETIME
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS "user_family" (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
owner_user_id INTEGER NOT NULL DEFAULT 0,
|
||
max_members INTEGER NOT NULL DEFAULT 2,
|
||
status TINYINT DEFAULT 0,
|
||
created_at DATETIME,
|
||
updated_at DATETIME,
|
||
deleted_at DATETIME DEFAULT NULL
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS "user_family_member" (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
family_id INTEGER NOT NULL DEFAULT 0,
|
||
user_id INTEGER NOT NULL DEFAULT 0,
|
||
role TINYINT DEFAULT 0,
|
||
status TINYINT DEFAULT 0,
|
||
join_source VARCHAR(32) NOT NULL DEFAULT '',
|
||
joined_at DATETIME,
|
||
left_at DATETIME DEFAULT NULL,
|
||
created_at DATETIME,
|
||
updated_at DATETIME,
|
||
deleted_at DATETIME DEFAULT NULL
|
||
)`,
|
||
}
|
||
for _, sql := range sqls {
|
||
require.NoError(t, db.Exec(sql).Error)
|
||
}
|
||
return db
|
||
}
|
||
|
||
// setupNewUserOnlyRedis 启动 miniredis,返回 redis.Client 和 miniredis 句柄
|
||
func setupNewUserOnlyRedis(t *testing.T) (*redis.Client, *miniredis.Miniredis) {
|
||
t.Helper()
|
||
mr, err := miniredis.Run()
|
||
require.NoError(t, err)
|
||
t.Cleanup(mr.Close)
|
||
rds := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||
return rds, mr
|
||
}
|
||
|
||
// buildNewUserOnlySvcCtx 组装最小 ServiceContext(含 asynq Queue 使用 miniredis)
|
||
func buildNewUserOnlySvcCtx(db *gorm.DB, rds *redis.Client, mr *miniredis.Miniredis) *svc.ServiceContext {
|
||
queue := asynq.NewClient(asynq.RedisClientOpt{Addr: mr.Addr()})
|
||
return &svc.ServiceContext{
|
||
DB: db,
|
||
Redis: rds,
|
||
UserModel: user.NewModel(db, rds),
|
||
OrderModel: modelOrder.NewModel(db, rds),
|
||
SubscribeModel: subModel.NewModel(db, rds),
|
||
PaymentModel: payment.NewModel(db, rds),
|
||
Queue: queue,
|
||
}
|
||
}
|
||
|
||
// insertTestSubscribe 直接用 SQL 插入 subscribe 行(绕过 GORM hook 的 MySQL 方言)
|
||
// new_user_only=true 时同时写入 discount JSON,使代码里的 discount 检查生效
|
||
func insertTestSubscribe(t *testing.T, db *gorm.DB, id int64, newUserOnly bool) {
|
||
t.Helper()
|
||
nuOnly := 0
|
||
discount := ""
|
||
if newUserOnly {
|
||
nuOnly = 1
|
||
// discount JSON 包含一个 new_user_only=true 的 tier,匹配 quantity=1
|
||
discount = `[{"quantity":1,"discount":90,"new_user_only":true}]`
|
||
}
|
||
err := db.Exec(`INSERT INTO "subscribe"
|
||
(id, name, unit_price, inventory, sell, sort, new_user_only, discount, created_at, updated_at)
|
||
VALUES (?, 'Test Plan', 1000, -1, 1, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||
id, id, nuOnly, discount).Error
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
// insertTestPayment 插入支付方式行
|
||
func insertTestPayment(t *testing.T, db *gorm.DB, id int64) {
|
||
t.Helper()
|
||
err := db.Exec(`INSERT INTO "payment"
|
||
(id, name, platform, config, enable, fee_mode, token)
|
||
VALUES (?, 'Balance', 'balance', '{}', 1, 0, ?)`,
|
||
id, "test-token").Error
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
// insertTestUser 插入用户行,createdAt 可控
|
||
func insertTestUser(t *testing.T, db *gorm.DB, id int64, createdAt time.Time) *user.User {
|
||
t.Helper()
|
||
err := db.Exec(`INSERT INTO "user"
|
||
(id, password, balance, gift_amount, enable, created_at, updated_at)
|
||
VALUES (?, '', 0, 0, 1, ?, datetime('now'))`,
|
||
id, createdAt.UTC().Format("2006-01-02 15:04:05")).Error
|
||
require.NoError(t, err)
|
||
return &user.User{
|
||
Id: id,
|
||
GiftAmount: 0,
|
||
CreatedAt: createdAt,
|
||
}
|
||
}
|
||
|
||
func insertTestDevice(t *testing.T, db *gorm.DB, userID int64, identifier string, createdAt time.Time) {
|
||
t.Helper()
|
||
err := db.Exec(`INSERT INTO "user_device"
|
||
(user_id, ip, user_agent, identifier, short_code, online, enabled, created_at, updated_at)
|
||
VALUES (?, '127.0.0.1', 'test-agent', ?, '', 0, 1, ?, datetime('now'))`,
|
||
userID,
|
||
identifier,
|
||
createdAt.UTC().Format("2006-01-02 15:04:05"),
|
||
).Error
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func insertTestFamily(t *testing.T, db *gorm.DB, familyID, ownerUserID int64) {
|
||
t.Helper()
|
||
err := db.Exec(`INSERT INTO "user_family"
|
||
(id, owner_user_id, max_members, status, created_at, updated_at)
|
||
VALUES (?, ?, 3, 1, datetime('now'), datetime('now'))`,
|
||
familyID,
|
||
ownerUserID,
|
||
).Error
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func insertTestFamilyMember(t *testing.T, db *gorm.DB, familyID, userID int64, role, status uint8, joinSource string) {
|
||
t.Helper()
|
||
err := db.Exec(`INSERT INTO "user_family_member"
|
||
(family_id, user_id, role, status, join_source, joined_at, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), datetime('now'))`,
|
||
familyID,
|
||
userID,
|
||
role,
|
||
status,
|
||
joinSource,
|
||
).Error
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
// insertTestOrder 插入一条历史订单(status=2 表示已支付)
|
||
func insertTestOrder(t *testing.T, db *gorm.DB, userID, subscribeID int64, status uint8) {
|
||
t.Helper()
|
||
err := db.Exec(`INSERT INTO "order"
|
||
(user_id, order_no, type, status, subscribe_id, created_at, updated_at)
|
||
VALUES (?, ?, 1, ?, ?, datetime('now'), datetime('now'))`,
|
||
userID, "existing-order-no", status, subscribeID).Error
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
func insertScopedTestOrder(t *testing.T, db *gorm.DB, orderNo string, userID, subscribeID int64, status uint8) {
|
||
t.Helper()
|
||
err := db.Exec(`INSERT INTO "order"
|
||
(user_id, order_no, type, status, subscribe_id, created_at, updated_at)
|
||
VALUES (?, ?, 1, ?, ?, datetime('now'), datetime('now'))`,
|
||
userID, orderNo, status, subscribeID).Error
|
||
require.NoError(t, err)
|
||
}
|
||
|
||
// buildPurchaseCtx 把 user 放入 context(模拟中间件行为)
|
||
func buildPurchaseCtx(u *user.User) context.Context {
|
||
return context.WithValue(context.Background(), constant.CtxKeyUser, u)
|
||
}
|
||
|
||
// TestPurchase_NewUserOnly_UserTooOld 验证:new_user_only=true,用户注册超过 24h → 返回 SubscribeNewUserOnly
|
||
func TestPurchase_NewUserOnly_UserTooOld(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const subID = int64(1)
|
||
const payID = int64(1)
|
||
|
||
insertTestSubscribe(t, db, subID, true) // new_user_only = true
|
||
insertTestPayment(t, db, payID)
|
||
|
||
// 用户注册 48 小时前 → 超出 24h 限制
|
||
u := insertTestUser(t, db, 100, time.Now().Add(-48*time.Hour))
|
||
ctx := buildPurchaseCtx(u)
|
||
|
||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||
_, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
require.Error(t, err)
|
||
var errCode *xerr.CodeError
|
||
require.ErrorAs(t, err, &errCode)
|
||
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode(),
|
||
"注册超过24h应返回 SubscribeNewUserOnly 错误码")
|
||
|
||
// 验证订单未被创建
|
||
var count int64
|
||
db.Model(&modelOrder.Order{}).Where("user_id = ?", u.Id).Count(&count)
|
||
assert.Equal(t, int64(0), count, "用户注册超时,订单不应被创建")
|
||
}
|
||
|
||
// TestPurchase_NewUserOnly_AlreadyPurchased 验证:new_user_only=true,用户是新用户但已购买过
|
||
// → 允许下单(不拦截),但不享受新人折扣
|
||
func TestPurchase_NewUserOnly_AlreadyPurchased(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const subID = int64(2)
|
||
const payID = int64(1)
|
||
|
||
insertTestSubscribe(t, db, subID, true)
|
||
insertTestPayment(t, db, payID)
|
||
|
||
// 用户刚注册(2h前)→ 满足时间条件
|
||
u := insertTestUser(t, db, 200, time.Now().Add(-2*time.Hour))
|
||
|
||
// 但已有一条 status=2 的历史订单(已支付)
|
||
insertTestOrder(t, db, u.Id, subID, 2)
|
||
|
||
ctx := buildPurchaseCtx(u)
|
||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
// 不应被拦截,允许下单
|
||
require.NoError(t, err, "24h内已购用户应允许继续下单,不应返回错误")
|
||
require.NotNil(t, resp)
|
||
assert.NotEmpty(t, resp.OrderNo)
|
||
|
||
// 历史订单 +1(新增了一条)
|
||
var count int64
|
||
db.Model(&modelOrder.Order{}).Where("user_id = ?", u.Id).Count(&count)
|
||
assert.Equal(t, int64(2), count, "应新增一条订单")
|
||
|
||
// 新订单无折扣:Amount=Price=1000
|
||
var newOrder modelOrder.Order
|
||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
|
||
assert.Equal(t, int64(1000), newOrder.Amount, "已购用户不享受新人折扣,Amount 应等于 Price")
|
||
assert.Equal(t, int64(0), newOrder.Discount, "Discount 应为 0")
|
||
}
|
||
|
||
// TestPurchase_NewUserOnly_Success 验证:new_user_only=true,新用户首次购买 → 成功创建订单
|
||
func TestPurchase_NewUserOnly_Success(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const subID = int64(3)
|
||
const payID = int64(1)
|
||
|
||
insertTestSubscribe(t, db, subID, true)
|
||
insertTestPayment(t, db, payID)
|
||
|
||
// 用户 1 小时前注册(新用户),且没有历史订单
|
||
u := insertTestUser(t, db, 300, time.Now().Add(-1*time.Hour))
|
||
ctx := buildPurchaseCtx(u)
|
||
|
||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
require.NoError(t, err)
|
||
require.NotNil(t, resp)
|
||
assert.NotEmpty(t, resp.OrderNo, "新用户首次购买应成功,返回订单号")
|
||
|
||
// 验证订单已写入数据库
|
||
var o modelOrder.Order
|
||
err = db.Where("order_no = ?", resp.OrderNo).First(&o).Error
|
||
require.NoError(t, err)
|
||
assert.Equal(t, u.Id, o.UserId)
|
||
assert.Equal(t, subID, o.SubscribeId)
|
||
}
|
||
|
||
// TestPurchase_NewUserOnly_Disabled 验证:new_user_only=false 时,老用户也能正常购买
|
||
func TestPurchase_NewUserOnly_Disabled(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const subID = int64(4)
|
||
const payID = int64(1)
|
||
|
||
insertTestSubscribe(t, db, subID, false) // new_user_only = false
|
||
insertTestPayment(t, db, payID)
|
||
|
||
// 注册 30 天的老用户
|
||
u := insertTestUser(t, db, 400, time.Now().Add(-30*24*time.Hour))
|
||
ctx := buildPurchaseCtx(u)
|
||
|
||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
require.NoError(t, err)
|
||
require.NotNil(t, resp)
|
||
assert.NotEmpty(t, resp.OrderNo, "new_user_only=false时老用户应能正常购买")
|
||
}
|
||
|
||
// TestPurchase_SingleMode_PendingOldOrderCancelled 验证:单订阅模式下,已有 pending 订单时
|
||
// 第二次下单应关闭旧单并创建新单(而非复用旧单)
|
||
func TestPurchase_SingleMode_PendingOldOrderCancelled(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
svcCtx.Config.Subscribe.SingleModel = true
|
||
|
||
const subID = int64(5)
|
||
const payID = int64(1)
|
||
|
||
insertTestSubscribe(t, db, subID, false)
|
||
insertTestPayment(t, db, payID)
|
||
|
||
u := insertTestUser(t, db, 500, time.Now().Add(-1*time.Hour))
|
||
ctx := buildPurchaseCtx(u)
|
||
|
||
// 第一次下单(pending)
|
||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||
resp1, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
require.NoError(t, err)
|
||
require.NotNil(t, resp1)
|
||
firstOrderNo := resp1.OrderNo
|
||
assert.NotEmpty(t, firstOrderNo)
|
||
|
||
// 确认第一单 pending
|
||
var firstOrder modelOrder.Order
|
||
require.NoError(t, db.Where("order_no = ?", firstOrderNo).First(&firstOrder).Error)
|
||
assert.Equal(t, uint8(1), firstOrder.Status, "第一单应为 pending")
|
||
|
||
// 第二次下单(不同 quantity)
|
||
logic2 := NewPurchaseLogic(ctx, svcCtx)
|
||
resp2, err := logic2.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 3,
|
||
})
|
||
require.NoError(t, err)
|
||
require.NotNil(t, resp2)
|
||
secondOrderNo := resp2.OrderNo
|
||
|
||
// 新单与旧单不同
|
||
assert.NotEqual(t, firstOrderNo, secondOrderNo, "第二次下单应创建新订单,不复用旧单")
|
||
|
||
// 旧单应被关闭(status=3)
|
||
var closedOrder modelOrder.Order
|
||
require.NoError(t, db.Where("order_no = ?", firstOrderNo).First(&closedOrder).Error)
|
||
assert.Equal(t, uint8(3), closedOrder.Status, "旧 pending 单应被关闭")
|
||
|
||
// 新单的 quantity 应为 3
|
||
var newOrder modelOrder.Order
|
||
require.NoError(t, db.Where("order_no = ?", secondOrderNo).First(&newOrder).Error)
|
||
assert.Equal(t, int64(3), newOrder.Quantity, "新单 quantity 应为 3")
|
||
assert.Equal(t, uint8(1), newOrder.Status, "新单应为 pending 状态")
|
||
}
|
||
|
||
// TestPurchase_SingleMode_NoPendingOrder 验证:单订阅模式下,没有旧 pending 单时正常创建
|
||
func TestPurchase_SingleMode_NoPendingOrder(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
svcCtx.Config.Subscribe.SingleModel = true
|
||
|
||
const subID = int64(6)
|
||
const payID = int64(1)
|
||
|
||
insertTestSubscribe(t, db, subID, false)
|
||
insertTestPayment(t, db, payID)
|
||
|
||
u := insertTestUser(t, db, 600, time.Now().Add(-1*time.Hour))
|
||
ctx := buildPurchaseCtx(u)
|
||
|
||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 2,
|
||
})
|
||
require.NoError(t, err)
|
||
require.NotNil(t, resp)
|
||
assert.NotEmpty(t, resp.OrderNo, "无旧 pending 单时应正常创建新单")
|
||
|
||
var o modelOrder.Order
|
||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&o).Error)
|
||
assert.Equal(t, int64(2), o.Quantity)
|
||
assert.Equal(t, uint8(1), o.Status)
|
||
}
|
||
|
||
// TestPurchase_NewUserOnly_AlreadyPurchased_NoBlock 验证:new_user_only=true 套餐,
|
||
// 24小时内但已购买过 → 允许下单,但不享受新人折扣(Discount=0,Amount=Price)
|
||
func TestPurchase_NewUserOnly_AlreadyPurchased_NoBlock(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const subID = int64(7)
|
||
const payID = int64(1)
|
||
|
||
// 套餐:unit_price=1000,discount=[{quantity:1,discount:80,new_user_only:true}]
|
||
insertTestSubscribe(t, db, subID, true)
|
||
insertTestPayment(t, db, payID)
|
||
|
||
// 用户 1 小时前注册(新用户),但已有一条成功订单(status=2)
|
||
u := insertTestUser(t, db, 700, time.Now().Add(-1*time.Hour))
|
||
insertTestOrder(t, db, u.Id, subID, 2)
|
||
|
||
ctx := buildPurchaseCtx(u)
|
||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
// 不应被拦截
|
||
require.NoError(t, err, "24h内已购用户不应被拦截,应允许下单")
|
||
require.NotNil(t, resp)
|
||
assert.NotEmpty(t, resp.OrderNo)
|
||
|
||
// 验证订单金额:无折扣,Amount=Price=1000,Discount=0
|
||
var newOrder modelOrder.Order
|
||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
|
||
assert.Equal(t, int64(1000), newOrder.Price, "Price 应为原价 1000")
|
||
assert.Equal(t, int64(1000), newOrder.Amount, "已购用户不享受新人折扣,Amount 应等于 Price")
|
||
assert.Equal(t, int64(0), newOrder.Discount, "Discount 应为 0")
|
||
}
|
||
|
||
// TestPurchase_NewUserOnly_FirstPurchase_HasDiscount 验证:new_user_only=true 套餐,
|
||
// 24小时内首次购买 → 允许下单且享受新人折扣
|
||
func TestPurchase_NewUserOnly_FirstPurchase_HasDiscount(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const subID = int64(8)
|
||
const payID = int64(1)
|
||
|
||
// 套餐:unit_price=1000,discount=[{quantity:1,discount:80,new_user_only:true}](8折)
|
||
insertTestSubscribe(t, db, subID, true)
|
||
insertTestPayment(t, db, payID)
|
||
|
||
// 用户 1 小时前注册,无历史订单
|
||
u := insertTestUser(t, db, 800, time.Now().Add(-1*time.Hour))
|
||
ctx := buildPurchaseCtx(u)
|
||
|
||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
require.NoError(t, err)
|
||
require.NotNil(t, resp)
|
||
|
||
var newOrder modelOrder.Order
|
||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
|
||
assert.Equal(t, int64(1000), newOrder.Price, "Price 应为原价 1000")
|
||
assert.Equal(t, int64(900), newOrder.Amount, "首次购买应享受9折,Amount=900")
|
||
assert.Equal(t, int64(100), newOrder.Discount, "折扣金额应为 100")
|
||
}
|
||
|
||
func TestPurchase_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const (
|
||
subID = int64(9)
|
||
payID = int64(1)
|
||
ownerUserID = int64(901)
|
||
memberUserID = int64(902)
|
||
familyID = int64(99)
|
||
)
|
||
|
||
insertTestSubscribe(t, db, subID, true)
|
||
insertTestPayment(t, db, payID)
|
||
|
||
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
|
||
insertTestUser(t, db, memberUserID, time.Now().Add(-72*time.Hour))
|
||
insertTestDevice(t, db, memberUserID, "device-eligibility-old", time.Now().Add(-72*time.Hour))
|
||
insertTestFamily(t, db, familyID, ownerUserID)
|
||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
|
||
|
||
logic := NewPurchaseLogic(buildPurchaseCtx(owner), svcCtx)
|
||
_, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
require.Error(t, err)
|
||
var errCode *xerr.CodeError
|
||
require.ErrorAs(t, err, &errCode)
|
||
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode())
|
||
}
|
||
|
||
func TestPurchase_NewUserOnly_BindEmailScopeSharesHistory(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const (
|
||
subID = int64(10)
|
||
payID = int64(1)
|
||
ownerUserID = int64(1001)
|
||
memberUserID = int64(1002)
|
||
familyID = int64(109)
|
||
)
|
||
|
||
insertTestSubscribe(t, db, subID, true)
|
||
insertTestPayment(t, db, payID)
|
||
|
||
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
|
||
insertTestUser(t, db, memberUserID, time.Now().Add(-2*time.Hour))
|
||
insertTestDevice(t, db, memberUserID, "device-eligibility-shared", time.Now().Add(-2*time.Hour))
|
||
insertTestFamily(t, db, familyID, ownerUserID)
|
||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
|
||
insertScopedTestOrder(t, db, "existing-scope-order", memberUserID, subID, 2)
|
||
|
||
logic := NewPurchaseLogic(buildPurchaseCtx(owner), svcCtx)
|
||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Payment: payID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
require.NoError(t, err)
|
||
require.NotNil(t, resp)
|
||
|
||
var newOrder modelOrder.Order
|
||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
|
||
assert.Equal(t, int64(1000), newOrder.Amount)
|
||
assert.Equal(t, int64(0), newOrder.Discount)
|
||
}
|
||
|
||
func TestPreCreateOrder_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const (
|
||
subID = int64(11)
|
||
ownerUserID = int64(1101)
|
||
memberUserID = int64(1102)
|
||
familyID = int64(119)
|
||
)
|
||
|
||
insertTestSubscribe(t, db, subID, true)
|
||
|
||
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
|
||
insertTestUser(t, db, memberUserID, time.Now().Add(-96*time.Hour))
|
||
insertTestDevice(t, db, memberUserID, "device-precreate-old", time.Now().Add(-96*time.Hour))
|
||
insertTestFamily(t, db, familyID, ownerUserID)
|
||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
|
||
|
||
logic := NewPreCreateOrderLogic(buildPurchaseCtx(owner), svcCtx)
|
||
_, err := logic.PreCreateOrder(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
require.Error(t, err)
|
||
var errCode *xerr.CodeError
|
||
require.ErrorAs(t, err, &errCode)
|
||
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode())
|
||
}
|
||
|
||
func TestPreCreateOrder_NewUserOnly_OrdinaryFamilyMemberDoesNotAffectEligibility(t *testing.T) {
|
||
db := setupNewUserOnlyDB(t)
|
||
rds, mr := setupNewUserOnlyRedis(t)
|
||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||
|
||
const (
|
||
subID = int64(12)
|
||
ownerUserID = int64(1201)
|
||
memberUserID = int64(1202)
|
||
familyID = int64(129)
|
||
)
|
||
|
||
insertTestSubscribe(t, db, subID, true)
|
||
|
||
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
|
||
insertTestUser(t, db, memberUserID, time.Now().Add(-96*time.Hour))
|
||
insertTestDevice(t, db, memberUserID, "device-precreate-ordinary", time.Now().Add(-96*time.Hour))
|
||
insertTestFamily(t, db, familyID, ownerUserID)
|
||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "manual_invite")
|
||
|
||
logic := NewPreCreateOrderLogic(buildPurchaseCtx(owner), svcCtx)
|
||
resp, err := logic.PreCreateOrder(&types.PurchaseOrderRequest{
|
||
SubscribeId: subID,
|
||
Quantity: 1,
|
||
})
|
||
|
||
require.NoError(t, err)
|
||
require.NotNil(t, resp)
|
||
assert.Equal(t, int64(900), resp.Amount)
|
||
assert.Equal(t, int64(100), resp.Discount)
|
||
}
|