363 lines
12 KiB
Go
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)
|
|
}
|
|
}
|