2026-01-27 01:33:32 +08:00

363 lines
12 KiB
Go

package main
import (
"context"
"database/sql"
"fmt"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
)
// 测试优惠券预扣机制的各种场景
// 数据库连接配置来自 configs/dev_configs.toml
const (
// 从 dev_configs.toml [mysql.read] 读取
dsn = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?parseTime=true&loc=Local"
)
var db *sql.DB
func main() {
var err error
db, err = sql.Open("mysql", dsn)
if err != nil {
fmt.Printf("❌ 数据库连接失败: %v\n", err)
os.Exit(1)
}
defer db.Close()
if err = db.Ping(); err != nil {
fmt.Printf("❌ 数据库 Ping 失败: %v\n", err)
os.Exit(1)
}
fmt.Println("========================================")
fmt.Println("优惠券预扣机制测试")
fmt.Println("========================================")
// 准备测试数据
testUserID := int64(99999) // 测试用户
testCouponValue := int64(500) // 5元 = 500分
// 清理之前的测试数据
cleanup(testUserID)
// 创建测试优惠券模板和用户券
systemCouponID := createTestSystemCoupon(testCouponValue)
if systemCouponID == 0 {
fmt.Println("❌ 创建测试优惠券模板失败")
return
}
fmt.Println("\n========================================")
fmt.Println("测试场景 1: 正常下单→支付流程")
fmt.Println("========================================")
testNormalFlow(testUserID, systemCouponID, testCouponValue)
fmt.Println("\n========================================")
fmt.Println("测试场景 2: 下单→取消流程")
fmt.Println("========================================")
testCancelFlow(testUserID, systemCouponID, testCouponValue)
fmt.Println("\n========================================")
fmt.Println("测试场景 3: 并发下单(同一优惠券)")
fmt.Println("========================================")
testConcurrentFlow(testUserID, systemCouponID, testCouponValue)
fmt.Println("\n========================================")
fmt.Println("测试场景 4: 金额券部分使用")
fmt.Println("========================================")
testPartialUse(testUserID, systemCouponID, testCouponValue)
fmt.Println("\n========================================")
fmt.Println("测试场景 5: 状态查询兼容性")
fmt.Println("========================================")
testStatusQueries(testUserID, systemCouponID)
// 清理测试数据
cleanup(testUserID)
cleanupSystemCoupon(systemCouponID)
fmt.Println("\n========================================")
fmt.Println("所有测试完成!")
fmt.Println("========================================")
}
func cleanup(userID int64) {
db.Exec("DELETE FROM order_coupons WHERE order_id IN (SELECT id FROM orders WHERE user_id = ?)", userID)
db.Exec("DELETE FROM user_coupon_ledger WHERE user_id = ?", userID)
db.Exec("DELETE FROM orders WHERE user_id = ?", userID)
db.Exec("DELETE FROM user_coupons WHERE user_id = ?", userID)
}
func cleanupSystemCoupon(id int64) {
db.Exec("DELETE FROM system_coupons WHERE id = ?", id)
}
func createTestSystemCoupon(value int64) int64 {
res, err := db.Exec(`
INSERT INTO system_coupons
(name, discount_type, discount_value, min_spend, scope_type, status, created_at, updated_at)
VALUES ('测试金额券', 1, ?, 0, 1, 1, NOW(), NOW())
`, value)
if err != nil {
fmt.Printf("❌ 创建优惠券模板失败: %v\n", err)
return 0
}
id, _ := res.LastInsertId()
fmt.Printf("✅ 创建测试优惠券模板 ID=%d 面值=%d分\n", id, value)
return id
}
func createTestUserCoupon(userID int64, systemCouponID int64, balance int64) int64 {
validEnd := time.Now().Add(24 * time.Hour)
res, err := db.Exec(`
INSERT INTO user_coupons
(user_id, coupon_id, balance_amount, status, valid_start, valid_end, created_at, updated_at)
VALUES (?, ?, ?, 1, NOW(), ?, NOW(), NOW())
`, userID, systemCouponID, balance, validEnd)
if err != nil {
fmt.Printf("❌ 创建用户优惠券失败: %v\n", err)
return 0
}
id, _ := res.LastInsertId()
return id
}
func getCouponStatus(userCouponID int64) (status int32, balance int64) {
db.QueryRow("SELECT status, balance_amount FROM user_coupons WHERE id = ?", userCouponID).Scan(&status, &balance)
return
}
// 测试场景 1: 正常下单→支付流程
func testNormalFlow(userID int64, systemCouponID int64, couponValue int64) {
userCouponID := createTestUserCoupon(userID, systemCouponID, couponValue)
if userCouponID == 0 {
return
}
fmt.Printf("✅ 创建用户优惠券 ID=%d 余额=%d分 状态=1(未使用)\n", userCouponID, couponValue)
status, balance := getCouponStatus(userCouponID)
assert("初始状态", status == 1 && balance == couponValue, fmt.Sprintf("status=%d balance=%d", status, balance))
// 模拟下单预扣
orderID := createTestOrder(userID, userCouponID, 200) // 扣200分
if orderID == 0 {
return
}
simulatePreDeduct(userCouponID, 200)
status, balance = getCouponStatus(userCouponID)
assert("下单后预扣", status == 4 && balance == couponValue-200, fmt.Sprintf("status=%d balance=%d", status, balance))
// 模拟支付成功确认
simulatePayConfirm(userCouponID, balance)
status, balance = getCouponStatus(userCouponID)
assert("支付确认后", (status == 1 || status == 2) && balance == couponValue-200, fmt.Sprintf("status=%d balance=%d", status, balance))
}
// 测试场景 2: 下单→取消流程
func testCancelFlow(userID int64, systemCouponID int64, couponValue int64) {
userCouponID := createTestUserCoupon(userID, systemCouponID, couponValue)
if userCouponID == 0 {
return
}
fmt.Printf("✅ 创建用户优惠券 ID=%d 余额=%d分\n", userCouponID, couponValue)
// 模拟下单预扣
orderID := createTestOrder(userID, userCouponID, 300)
if orderID == 0 {
return
}
simulatePreDeduct(userCouponID, 300)
status, balance := getCouponStatus(userCouponID)
assert("下单后", status == 4 && balance == couponValue-300, fmt.Sprintf("status=%d balance=%d", status, balance))
// 模拟取消订单,恢复优惠券
simulateCancelRestore(userCouponID, 300)
status, balance = getCouponStatus(userCouponID)
assert("取消后恢复", status == 1 && balance == couponValue, fmt.Sprintf("status=%d balance=%d", status, balance))
}
// 测试场景 3: 并发下单(同一优惠券)
func testConcurrentFlow(userID int64, systemCouponID int64, couponValue int64) {
userCouponID := createTestUserCoupon(userID, systemCouponID, couponValue) // 500分
if userCouponID == 0 {
return
}
fmt.Printf("✅ 创建用户优惠券 ID=%d 余额=%d分\n", userCouponID, couponValue)
// 模拟两个并发请求,都尝试使用全部余额
// 第一个应该成功,第二个应该失败
// 使用事务模拟并发操作
ctx := context.Background()
tx1, err := db.BeginTx(ctx, nil)
if err != nil {
fmt.Printf("❌ 事务1创建失败: %v\n", err)
return
}
tx2, err := db.BeginTx(ctx, nil)
if err != nil {
tx1.Rollback()
fmt.Printf("❌ 事务2创建失败: %v\n", err)
return
}
// 事务1: 原子预扣 (应该成功)
res1, err1 := tx1.Exec(`
UPDATE user_coupons
SET balance_amount = balance_amount - ?,
status = 4
WHERE id = ? AND balance_amount >= ? AND status IN (1, 4)
`, couponValue, userCouponID, couponValue)
var affected1 int64
if err1 == nil && res1 != nil {
affected1, _ = res1.RowsAffected()
}
success1 := err1 == nil && affected1 > 0
// 提交事务1
tx1.Commit()
// 事务2: 原子预扣 (应该失败,余额不足)
res2, err2 := tx2.Exec(`
UPDATE user_coupons
SET balance_amount = balance_amount - ?,
status = 4
WHERE id = ? AND balance_amount >= ? AND status IN (1, 4)
`, couponValue, userCouponID, couponValue)
var affected2 int64
if err2 == nil && res2 != nil {
affected2, _ = res2.RowsAffected()
}
success2 := err2 == nil && affected2 > 0
tx2.Commit()
assert("并发测试: 第一个成功", success1, fmt.Sprintf("err=%v affected=%d", err1, affected1))
assert("并发测试: 第二个失败", !success2, fmt.Sprintf("err=%v affected=%d", err2, affected2))
}
// 测试场景 4: 金额券部分使用
func testPartialUse(userID int64, systemCouponID int64, couponValue int64) {
userCouponID := createTestUserCoupon(userID, systemCouponID, couponValue) // 500分
if userCouponID == 0 {
return
}
fmt.Printf("✅ 创建用户优惠券 ID=%d 余额=%d分\n", userCouponID, couponValue)
// 第一次使用200分
createTestOrder(userID, userCouponID, 200)
simulatePreDeduct(userCouponID, 200)
simulatePayConfirm(userCouponID, couponValue-200)
status, balance := getCouponStatus(userCouponID)
assert("第一次使用后", status == 1 && balance == 300, fmt.Sprintf("status=%d balance=%d", status, balance))
// 第二次使用150分
createTestOrder(userID, userCouponID, 150)
simulatePreDeduct(userCouponID, 150)
simulatePayConfirm(userCouponID, 150)
status, balance = getCouponStatus(userCouponID)
assert("第二次使用后", status == 1 && balance == 150, fmt.Sprintf("status=%d balance=%d", status, balance))
// 第三次用完剩余
createTestOrder(userID, userCouponID, 150)
simulatePreDeduct(userCouponID, 150)
simulatePayConfirm(userCouponID, 0)
status, balance = getCouponStatus(userCouponID)
assert("用完后", status == 2 && balance == 0, fmt.Sprintf("status=%d balance=%d", status, balance))
}
// 测试场景 5: 状态查询兼容性
func testStatusQueries(userID int64, systemCouponID int64) {
// 创建不同状态的优惠券
uc1 := createTestUserCoupon(userID, systemCouponID, 100) // status=1
uc2 := createTestUserCoupon(userID, systemCouponID, 200)
simulatePreDeduct(uc2, 100) // status=4
uc3 := createTestUserCoupon(userID, systemCouponID, 300)
db.Exec("UPDATE user_coupons SET status = 2 WHERE id = ?", uc3) // status=2
// 测试 applyCouponWithCap 的查询条件 (应该能找到 status IN (1, 4))
var count int
db.QueryRow(`
SELECT COUNT(*) FROM user_coupons
WHERE user_id = ? AND status IN (1, 4)
`, userID).Scan(&count)
assert("IN (1,4) 查询", count >= 2, fmt.Sprintf("count=%d (应>=2)", count))
// 测试 expiration 的查询条件 (应该包含 status IN (1, 2, 4))
db.QueryRow(`
SELECT COUNT(*) FROM user_coupons
WHERE user_id = ? AND status IN (1, 2, 4)
`, userID).Scan(&count)
assert("IN (1,2,4) 查询", count >= 3, fmt.Sprintf("count=%d (应>=3)", count))
fmt.Printf("✅ 优惠券 ID %d (status=1), %d (status=4), %d (status=2)\n", uc1, uc2, uc3)
}
// 辅助函数
func createTestOrder(userID int64, couponID int64, amount int64) int64 {
orderNo := fmt.Sprintf("TEST%d", time.Now().UnixNano())
res, err := db.Exec(`
INSERT INTO orders
(user_id, order_no, source_type, total_amount, discount_amount, actual_amount, coupon_id, status, created_at, updated_at)
VALUES (?, ?, 2, ?, ?, ?, ?, 1, NOW(), NOW())
`, userID, orderNo, 1000, amount, 1000-amount, couponID)
if err != nil {
fmt.Printf("❌ 创建订单失败: %v\n", err)
return 0
}
id, _ := res.LastInsertId()
// 记录 order_coupons
db.Exec(`INSERT INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?, ?, ?, NOW())`, id, couponID, amount)
return id
}
func simulatePreDeduct(userCouponID int64, amount int64) {
db.Exec(`
UPDATE user_coupons
SET balance_amount = balance_amount - ?,
status = 4
WHERE id = ? AND balance_amount >= ?
`, amount, userCouponID, amount)
}
func simulatePayConfirm(userCouponID int64, newBalance int64) {
newStatus := 1
if newBalance <= 0 {
newStatus = 2
}
db.Exec(`UPDATE user_coupons SET status = ? WHERE id = ? AND status = 4`, newStatus, userCouponID)
}
func simulateCancelRestore(userCouponID int64, amount int64) {
db.Exec(`
UPDATE user_coupons
SET balance_amount = balance_amount + ?,
status = 1
WHERE id = ? AND status = 4
`, amount, userCouponID)
}
func assert(name string, condition bool, detail string) {
if condition {
fmt.Printf("✅ %s: PASS (%s)\n", name, detail)
} else {
fmt.Printf("❌ %s: FAIL (%s)\n", name, detail)
}
}