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) } }