From f8624cca49ccf0b81b0e4d32d1c82e0bada739bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=96=B9=E6=88=90?= Date: Wed, 28 Jan 2026 21:41:47 +0800 Subject: [PATCH] delete --- bindbox_game.db | 0 cmd/9090_audit/main.go | 47 -- cmd/activity_repair/main.go | 85 ---- cmd/apply_migration/main.go | 28 - cmd/audit_cloud_db/main.go | 43 -- cmd/check_activity/main.go | 35 -- cmd/check_data_state/main.go | 33 -- cmd/check_index/main.go | 28 - cmd/check_refunds/main.go | 85 ---- cmd/create_admin/main.go | 20 - cmd/debug_9090_coupons/main.go | 38 -- cmd/debug_activity/main.go | 95 ---- cmd/debug_all_coupons/main.go | 32 -- cmd/debug_balance/main.go | 44 -- cmd/debug_card/main.go | 50 -- cmd/debug_coupon/main.go | 95 ---- cmd/debug_dashboard/main.go | 122 ----- cmd/debug_inventory/main.go | 48 -- cmd/debug_inventory_verify/main.go | 72 --- cmd/debug_ledger/main.go | 29 -- cmd/debug_ledger_full/main.go | 29 -- cmd/debug_query/main.go | 102 ---- cmd/debug_stats/main.go | 123 ----- cmd/debug_usage_detail/main.go | 35 -- cmd/debug_user_search/main.go | 29 -- cmd/diagnose_order/main.go | 74 --- cmd/find_bad_coupons/main.go | 40 -- cmd/find_live_activity/main.go | 42 -- cmd/fix_order/main.go | 36 -- cmd/full_9090_dump/main.go | 41 -- cmd/handlergen/README.md | 28 - cmd/handlergen/handler_template.go.tpl | 265 ---------- cmd/handlergen/main.go | 90 ---- cmd/inspect_order/main.go | 73 --- cmd/inspect_order_4746/main.go | 32 -- cmd/master_reconcile/main.go | 81 --- cmd/matching_sim/main.go | 93 ---- cmd/migrate_configs/main.go | 155 ------ cmd/reconcile_coupons/main.go | 85 ---- cmd/simulate_test/main.go | 186 ------- cmd/test_notify/main.go | 102 ---- cmd/tools/task_center_test/integration.go | 263 ---------- cmd/tools/task_center_test/main.go | 477 ------------------ cmd/trace_ledger_cloud/main.go | 42 -- cmd/verify_coupon_fix/main.go | 70 --- .../ALIGNMENT_bugfix.md | 100 ---- .../ALIGNMENT_loki_integration.md | 47 -- docs/lottery_algorithm.md | 149 ------ .../ALIGNMENT_standardize_points_ratio.md | 83 --- .../DESIGN_standardize_points_ratio.md | 67 --- .../TASK_standardize_points_ratio.md | 45 -- docs/standardize_points_ratio/migration.sql | 62 --- docs/standardize_points_ratio/walkthrough.md | 52 -- .../ALIGNMENT_yifanshang_count_card_fix.md | 46 -- .../CONSENSUS_yifanshang_count_card_fix.md | 43 -- .../DESIGN_yifanshang_count_card_fix.md | 85 ---- .../FINAL_yifanshang_count_card_fix.md | 28 - .../TASK_yifanshang_count_card_fix.md | 40 -- .../TODO_yifanshang_count_card_fix.md | 12 - .../ALIGNMENT_后台工作台接口.md | 259 ---------- .../CONSENSUS_后台工作台接口.md | 91 ---- .../ALIGNMENT_玩家管理Bug修复.md | 95 ---- .../CONSENSUS_玩家管理Bug修复.md | 59 --- .../玩家管理Bug修复/DESIGN_玩家管理Bug修复.md | 177 ------- docs/翻牌特效/ALIGNMENT_翻牌特效.md | 121 ----- docs/翻牌特效/CONSENSUS_翻牌特效.md | 27 - docs/翻牌特效/DESIGN_翻牌特效.md | 53 -- docs/翻牌特效/TASK_翻牌特效.md | 37 -- scripts/check_coupon.go | 98 ---- scripts/check_db_schema.go | 36 -- scripts/check_duplicates.go | 58 --- scripts/check_orders.go | 35 -- scripts/debug_matching_order.go | 311 ------------ scripts/fix_db_columns.go | 35 -- scripts/inspect_column_names.go | 33 -- scripts/list_tables.go | 33 -- scripts/query_user.go | 26 - scripts/read_raw_order.go | 28 - scripts/test_coupon_prededuct/main.go | 362 ------------- scripts/test_order_no.go | 24 - scripts/test_update.go | 28 - scripts/verify_coupon_fix.go | 42 -- 82 files changed, 6549 deletions(-) delete mode 100644 bindbox_game.db delete mode 100644 cmd/9090_audit/main.go delete mode 100644 cmd/activity_repair/main.go delete mode 100644 cmd/apply_migration/main.go delete mode 100644 cmd/audit_cloud_db/main.go delete mode 100644 cmd/check_activity/main.go delete mode 100644 cmd/check_data_state/main.go delete mode 100644 cmd/check_index/main.go delete mode 100644 cmd/check_refunds/main.go delete mode 100644 cmd/create_admin/main.go delete mode 100644 cmd/debug_9090_coupons/main.go delete mode 100644 cmd/debug_activity/main.go delete mode 100644 cmd/debug_all_coupons/main.go delete mode 100644 cmd/debug_balance/main.go delete mode 100644 cmd/debug_card/main.go delete mode 100644 cmd/debug_coupon/main.go delete mode 100644 cmd/debug_dashboard/main.go delete mode 100644 cmd/debug_inventory/main.go delete mode 100644 cmd/debug_inventory_verify/main.go delete mode 100644 cmd/debug_ledger/main.go delete mode 100644 cmd/debug_ledger_full/main.go delete mode 100644 cmd/debug_query/main.go delete mode 100644 cmd/debug_stats/main.go delete mode 100644 cmd/debug_usage_detail/main.go delete mode 100644 cmd/debug_user_search/main.go delete mode 100644 cmd/diagnose_order/main.go delete mode 100644 cmd/find_bad_coupons/main.go delete mode 100644 cmd/find_live_activity/main.go delete mode 100644 cmd/fix_order/main.go delete mode 100644 cmd/full_9090_dump/main.go delete mode 100644 cmd/handlergen/README.md delete mode 100644 cmd/handlergen/handler_template.go.tpl delete mode 100644 cmd/handlergen/main.go delete mode 100644 cmd/inspect_order/main.go delete mode 100644 cmd/inspect_order_4746/main.go delete mode 100644 cmd/master_reconcile/main.go delete mode 100644 cmd/matching_sim/main.go delete mode 100644 cmd/migrate_configs/main.go delete mode 100644 cmd/reconcile_coupons/main.go delete mode 100644 cmd/simulate_test/main.go delete mode 100644 cmd/test_notify/main.go delete mode 100644 cmd/tools/task_center_test/integration.go delete mode 100644 cmd/tools/task_center_test/main.go delete mode 100644 cmd/trace_ledger_cloud/main.go delete mode 100644 cmd/verify_coupon_fix/main.go delete mode 100644 docs/bugfix_task_center_activity_profit/ALIGNMENT_bugfix.md delete mode 100644 docs/loki_integration/ALIGNMENT_loki_integration.md delete mode 100644 docs/lottery_algorithm.md delete mode 100644 docs/standardize_points_ratio/ALIGNMENT_standardize_points_ratio.md delete mode 100644 docs/standardize_points_ratio/DESIGN_standardize_points_ratio.md delete mode 100644 docs/standardize_points_ratio/TASK_standardize_points_ratio.md delete mode 100644 docs/standardize_points_ratio/migration.sql delete mode 100644 docs/standardize_points_ratio/walkthrough.md delete mode 100644 docs/yifanshang_count_card_fix/ALIGNMENT_yifanshang_count_card_fix.md delete mode 100644 docs/yifanshang_count_card_fix/CONSENSUS_yifanshang_count_card_fix.md delete mode 100644 docs/yifanshang_count_card_fix/DESIGN_yifanshang_count_card_fix.md delete mode 100644 docs/yifanshang_count_card_fix/FINAL_yifanshang_count_card_fix.md delete mode 100644 docs/yifanshang_count_card_fix/TASK_yifanshang_count_card_fix.md delete mode 100644 docs/yifanshang_count_card_fix/TODO_yifanshang_count_card_fix.md delete mode 100644 docs/后台工作台接口分析/ALIGNMENT_后台工作台接口.md delete mode 100644 docs/后台工作台接口分析/CONSENSUS_后台工作台接口.md delete mode 100644 docs/玩家管理Bug修复/ALIGNMENT_玩家管理Bug修复.md delete mode 100644 docs/玩家管理Bug修复/CONSENSUS_玩家管理Bug修复.md delete mode 100644 docs/玩家管理Bug修复/DESIGN_玩家管理Bug修复.md delete mode 100644 docs/翻牌特效/ALIGNMENT_翻牌特效.md delete mode 100644 docs/翻牌特效/CONSENSUS_翻牌特效.md delete mode 100644 docs/翻牌特效/DESIGN_翻牌特效.md delete mode 100644 docs/翻牌特效/TASK_翻牌特效.md delete mode 100644 scripts/check_coupon.go delete mode 100644 scripts/check_db_schema.go delete mode 100644 scripts/check_duplicates.go delete mode 100644 scripts/check_orders.go delete mode 100644 scripts/debug_matching_order.go delete mode 100644 scripts/fix_db_columns.go delete mode 100644 scripts/inspect_column_names.go delete mode 100644 scripts/list_tables.go delete mode 100644 scripts/query_user.go delete mode 100644 scripts/read_raw_order.go delete mode 100644 scripts/test_coupon_prededuct/main.go delete mode 100644 scripts/test_order_no.go delete mode 100644 scripts/test_update.go delete mode 100644 scripts/verify_coupon_fix.go diff --git a/bindbox_game.db b/bindbox_game.db deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/9090_audit/main.go b/cmd/9090_audit/main.go deleted file mode 100644 index 9e0b961..0000000 --- a/cmd/9090_audit/main.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("--- User 9090 Deep Audit (Cloud DB) ---") - - var userCoupons []struct { - ID int64 - CouponID int64 - BalanceAmount int64 - Status int32 - ValidEnd string - } - db.Raw("SELECT id, coupon_id, balance_amount, status, valid_end FROM user_coupons WHERE user_id = 9090").Scan(&userCoupons) - - for _, uc := range userCoupons { - fmt.Printf("\n[Coupon %d] Status: %v, Balance: %v, ValidEnd: %v\n", uc.ID, uc.Status, uc.BalanceAmount, uc.ValidEnd) - - // Trace Ledger - var ledger []struct { - ChangeAmount int64 - OrderID int64 - Action string - CreatedAt string - } - db.Raw("SELECT change_amount, order_id, action, created_at FROM user_coupon_ledger WHERE user_coupon_id = ?", uc.ID).Scan(&ledger) - for _, l := range ledger { - fmt.Printf(" -> Action: %s, Change: %d, Order: %d, Created: %s\n", l.Action, l.ChangeAmount, l.OrderID, l.CreatedAt) - } - } -} diff --git a/cmd/activity_repair/main.go b/cmd/activity_repair/main.go deleted file mode 100644 index d2abcee..0000000 --- a/cmd/activity_repair/main.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("--- Activity-Based Coupon Data Repair Auditor (Cloud DB) ---") - - var records []struct { - ID int64 - UserID int64 - CouponID int64 - BalanceAmount int64 - Status int32 - ValidEnd string - OriginalValue int64 - DiscountType int32 - } - - // Fetch all coupons joined with system data - db.Raw(` - SELECT uc.id, uc.user_id, uc.coupon_id, uc.balance_amount, uc.status, uc.valid_end, sc.discount_value as original_value, sc.discount_type - FROM user_coupons uc - JOIN system_coupons sc ON uc.coupon_id = sc.id - `).Scan(&records) - - for _, r := range records { - // Calculate total usage from order_coupons - var orderUsage int64 - db.Raw("SELECT SUM(applied_amount) FROM order_coupons WHERE user_coupon_id = ?", r.ID).Scan(&orderUsage) - - // Calculate total usage from ledger - var ledgerUsage int64 - db.Raw("SELECT ABS(SUM(change_amount)) FROM user_coupon_ledger WHERE user_coupon_id = ? AND action = 'apply'", r.ID).Scan(&ledgerUsage) - - // Max usage between sources - finalUsage := orderUsage - if ledgerUsage > finalUsage { - finalUsage = ledgerUsage - } - - expectedBalance := r.OriginalValue - finalUsage - if expectedBalance < 0 { - expectedBalance = 0 - } - - // Determine Correct Status - // 1: Unused (or Partially Used but still valid) - // 2: Used (Exhausted) - // 3: Expired (Unused/Partially Used and time past) - - expectedStatus := r.Status - if expectedBalance == 0 { - expectedStatus = 2 - } else { - // Logic for expired vs unused would go here if needed, - // but we prioritize "Used" if balance is 0. - // Currently if balance > 0 and Status is 2, it's definitely an error. - if r.Status == 2 { - expectedStatus = 1 // Revert to unused/partial if balance > 0 - } - } - - if expectedBalance != r.BalanceAmount || expectedStatus != r.Status { - fmt.Printf("-- Coupon %d (User %d): Bal %d->%d, Status %v->%v, Usage %d/%d\n", - r.ID, r.UserID, r.BalanceAmount, expectedBalance, r.Status, expectedStatus, finalUsage, r.OriginalValue) - fmt.Printf("UPDATE user_coupons SET balance_amount = %d, status = %v, updated_at = NOW() WHERE id = %d;\n", - expectedBalance, expectedStatus, r.ID) - } - } -} diff --git a/cmd/apply_migration/main.go b/cmd/apply_migration/main.go deleted file mode 100644 index e693050..0000000 --- a/cmd/apply_migration/main.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("Applying migration to cloud DB...") - err = db.Exec("ALTER TABLE order_coupons ADD UNIQUE INDEX idx_order_user_coupon (order_id, user_coupon_id);").Error - if err != nil { - fmt.Printf("Migration failed: %v\n", err) - } else { - fmt.Println("Migration successful: Added unique index to order_coupons.") - } -} diff --git a/cmd/audit_cloud_db/main.go b/cmd/audit_cloud_db/main.go deleted file mode 100644 index 8343733..0000000 --- a/cmd/audit_cloud_db/main.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("--- Inconsistency Audit (Cloud DB) ---") - - fmt.Println("\n1. Coupons with Balance = 0 but Status != 2 (Used):") - var res1 []map[string]interface{} - db.Raw("SELECT id, user_id, coupon_id, status, balance_amount, valid_end FROM user_coupons WHERE balance_amount = 0 AND status != 2").Scan(&res1) - for _, res := range res1 { - fmt.Printf("ID: %v, User: %v, Status: %v, ValidEnd: %v\n", res["id"], res["user_id"], res["status"], res["valid_end"]) - } - - fmt.Println("\n2. Coupons in Status = 2 (Used) but Balance > 0:") - var res2 []map[string]interface{} - db.Raw("SELECT id, user_id, coupon_id, status, balance_amount, valid_end FROM user_coupons WHERE status = 2 AND balance_amount > 0").Scan(&res2) - for _, res := range res2 { - fmt.Printf("ID: %v, User: %v, Bal: %v, ValidEnd: %v\n", res["id"], res["user_id"], res["balance_amount"], res["valid_end"]) - } - - fmt.Println("\n3. Expired Time (valid_end < NOW) but Status = 1 (Unused):") - var res3 []map[string]interface{} - db.Raw("SELECT id, user_id, coupon_id, status, balance_amount, valid_end FROM user_coupons WHERE valid_end < NOW() AND status = 1").Scan(&res3) - for _, res := range res3 { - fmt.Printf("ID: %v, User: %v, Bal: %v, ValidEnd: %v\n", res["id"], res["user_id"], res["balance_amount"], res["valid_end"]) - } -} diff --git a/cmd/check_activity/main.go b/cmd/check_activity/main.go deleted file mode 100644 index 91ebe92..0000000 --- a/cmd/check_activity/main.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "bindbox-game/configs" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/model" - "flag" - "fmt" -) - -func main() { - flag.Parse() - configs.Init() - dbRepo, err := mysql.New() - if err != nil { - panic(err) - } - db := dbRepo.GetDbR() - - var activity model.Activities - if err := db.First(&activity, 82).Error; err != nil { - fmt.Printf("Activity 82 NOT found in `activities` table.\n") - } else { - // Only print Name - fmt.Printf("Activity 82 Found in `activities`: Name=%s, ID=%d\n", activity.Name, activity.ID) - } - - var liveActivity model.LivestreamActivities - // Livestream activities might have ID 82 in their own table? - if err := db.First(&liveActivity, 82).Error; err != nil { - fmt.Printf("Livestream Activity 82 NOT found in `livestream_activities` table.\n") - } else { - fmt.Printf("Livestream Activity 82 Found in `livestream_activities`: Name=%s, ID=%d, DouyinProductID=%s\n", liveActivity.Name, liveActivity.ID, liveActivity.DouyinProductID) - } -} diff --git a/cmd/check_data_state/main.go b/cmd/check_data_state/main.go deleted file mode 100644 index 7f6d20f..0000000 --- a/cmd/check_data_state/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("Checking User 9090 coupons status...") - var results []map[string]interface{} - db.Raw("SELECT id, coupon_id, status, balance_amount, valid_end FROM user_coupons WHERE user_id = 9090").Scan(&results) - for _, res := range results { - fmt.Printf("ID: %v, CouponID: %v, Status: %v, Balance: %v, ValidEnd: %v\n", - res["id"], res["coupon_id"], res["status"], res["balance_amount"], res["valid_end"]) - } - - fmt.Println("\nChecking for coupons that are status=3 but balance=0 and not yet time-expired...") - var count int64 - db.Raw("SELECT count(*) FROM user_coupons WHERE status = 3 AND balance_amount = 0 AND valid_end > NOW()").Scan(&count) - fmt.Printf("Found %d such coupons.\n", count) -} diff --git a/cmd/check_index/main.go b/cmd/check_index/main.go deleted file mode 100644 index 9d24ffd..0000000 --- a/cmd/check_index/main.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("Checking indexes on cloud DB...") - var results []map[string]interface{} - db.Raw("SHOW INDEX FROM order_coupons;").Scan(&results) - for _, res := range results { - fmt.Printf("Table: %s, Key_name: %s, Column: %s, Unique: %v\n", - res["Table"], res["Key_name"], res["Column_name"], res["Non_unique"]) - } -} diff --git a/cmd/check_refunds/main.go b/cmd/check_refunds/main.go deleted file mode 100644 index b897816..0000000 --- a/cmd/check_refunds/main.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "bindbox-game/configs" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/model" - "flag" - "fmt" - "time" -) - -func main() { - flag.Parse() - - // Initialize config - configs.Init() - - // Initialize MySQL - dbRepo, err := mysql.New() - if err != nil { - panic(err) - } - - db := dbRepo.GetDbR() - - startTime, _ := time.ParseInLocation("2006-01-02", "2026-01-19", time.Local) - endTime := startTime.Add(24 * time.Hour) - - fmt.Printf("Checking for refunds between %v and %v\n", startTime, endTime) - - var orders []model.Orders - // Check by UpdateAt (refund implies an update) - // Or just check Status=4 and CreatedAt (if refunded same day) or check Status=4 generally if it impacts that day's stats. - // Usually "Daily Stats" for 19th means events that happened on the 19th. - // Since the user image shows "Refund: 0", it means no refunds *attributed* to that day. - // Attribution could be by Order Creation Date or Refund Date. - // Let's check ANY order with status 4. - - db.Where("status = ?", 4). - Where("updated_at >= ? AND updated_at < ?", startTime, endTime). - Find(&orders) - - fmt.Printf("Found %d orders marked as Refunded (status=4) updated on 2026-01-19:\n", len(orders)) - for _, o := range orders { - fmt.Printf("ID: %d, OrderNo: %s, Amount: %d, CreatedAt: %v, UpdatedAt: %v\n", o.ID, o.OrderNo, o.ActualAmount, o.CreatedAt, o.UpdatedAt) - } - - // Also check created_at on that day for any order that is NOW status 4 - var createdOrders []model.Orders - db.Where("status = ?", 4). - Where("created_at >= ? AND created_at < ?", startTime, endTime). - Find(&createdOrders) - - fmt.Printf("Found %d orders created on 2026-01-19 that are currently Refunded (status=4):\n", len(createdOrders)) - for _, o := range createdOrders { - fmt.Printf("ID: %d, OrderNo: %s, Amount: %d, SourceType: %d, Remark: %s, CreatedAt: %v\n", o.ID, o.OrderNo, o.ActualAmount, o.SourceType, o.Remark, o.CreatedAt) - } - - // Check Douyin Orders - var douyinOrders []model.DouyinOrders - // OrderStatus 4 might be refund in DouyinOrders context if the code is correct. - // Let's check for any Status=4 in DouyinOrders on the 19th. - // Since DouyinOrders struct in gen.go has Clean fields, we can use it. - // Note: created_at might be in UTC or local, check logic. - // But let's just query by range. - db.Where("order_status = ?", 4). - Where("updated_at >= ? AND updated_at < ?", startTime, endTime). - Find(&douyinOrders) - - fmt.Printf("Found %d refunded Douyin orders (status=4) updated on 2026-01-19:\n", len(douyinOrders)) - for _, o := range douyinOrders { - fmt.Printf("ID: %v, ShopOrderID: %s, PayAmount: %d, UpdatedAt: %v\n", o.ID, o.ShopOrderID, o.ActualPayAmount, o.UpdatedAt) - } - - // Also check created_at for Douyin Orders - var createdDouyinOrders []model.DouyinOrders - db.Where("order_status = ?", 4). - Where("created_at >= ? AND created_at < ?", startTime, endTime). - Find(&createdDouyinOrders) - - fmt.Printf("Found %d refunded Douyin orders (status=4) created on 2026-01-19:\n", len(createdDouyinOrders)) - for _, o := range createdDouyinOrders { - fmt.Printf("ID: %v, ShopOrderID: %s, PayAmount: %d, CreatedAt: %v, UpdatedAt: %v\n", o.ID, o.ShopOrderID, o.ActualPayAmount, o.CreatedAt, o.UpdatedAt) - } -} diff --git a/cmd/create_admin/main.go b/cmd/create_admin/main.go deleted file mode 100644 index 0f9996a..0000000 --- a/cmd/create_admin/main.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "bindbox-game/internal/pkg/utils" - "fmt" -) - -func main() { - password := "123456" - hash, err := utils.GenerateAdminHashedPassword(password) - if err != nil { - fmt.Println("Error:", err) - return - } - fmt.Println("Password hash for '123456':") - fmt.Println(hash) - fmt.Println() - fmt.Println("SQL to insert admin:") - fmt.Printf("INSERT INTO admin (username, nickname, password, login_status, is_super, created_user, created_at) VALUES ('CC', 'CC', '%s', 1, 0, 'system', NOW());\n", hash) -} diff --git a/cmd/debug_9090_coupons/main.go b/cmd/debug_9090_coupons/main.go deleted file mode 100644 index 209928e..0000000 --- a/cmd/debug_9090_coupons/main.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("--- Debugging User 9090 Coupons (Cloud DB) ---") - - var coupons []map[string]interface{} - db.Raw("SELECT id, coupon_id, balance_amount, status, valid_end, used_order_id, used_at FROM user_coupons WHERE user_id = 9090").Scan(&coupons) - - fmt.Printf("%-5s | %-10s | %-15s | %-8s | %-20s | %-15s\n", "ID", "CouponID", "Balance", "Status", "ValidEnd", "UsedOrder") - fmt.Println("------------------------------------------------------------------------------------------") - for _, c := range coupons { - fmt.Printf("%-5v | %-10v | %-15v | %-8v | %-20v | %-15v\n", c["id"], c["coupon_id"], c["balance_amount"], c["status"], c["valid_end"], c["used_order_id"]) - } - - fmt.Println("\n--- Checking Ledger for these coupons ---") - var ledger []map[string]interface{} - db.Raw("SELECT user_coupon_id, change_amount, balance_after, order_id, action, created_at FROM user_coupon_ledger WHERE user_id = 9090 ORDER BY created_at DESC").Scan(&ledger) - for _, l := range ledger { - fmt.Printf("CouponID: %v, Change: %v, After: %v, Order: %v, Action: %v, Time: %v\n", l["user_coupon_id"], l["change_amount"], l["balance_after"], l["order_id"], l["action"], l["created_at"]) - } -} diff --git a/cmd/debug_activity/main.go b/cmd/debug_activity/main.go deleted file mode 100644 index f074e8c..0000000 --- a/cmd/debug_activity/main.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "fmt" - - "gorm.io/driver/mysql" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(106.54.232.2:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Info)}) - if err != nil { - panic("failed to connect database: " + err.Error()) - } - - // 检查对对碰活动 - fmt.Println("========== 检查对对碰活动 (activity_category_id=3) ==========") - - type Activity struct { - ID int64 - Name string - PlayType string - ActivityCategoryID int64 - } - var matchingActs []Activity - db.Table("activities").Where("activity_category_id = ?", 3).Limit(5).Find(&matchingActs) - fmt.Printf("找到 %d 个对对碰活动\n", len(matchingActs)) - - for _, act := range matchingActs { - fmt.Printf("\n--- Activity ID=%d Name='%s' PlayType='%s' ---\n", act.ID, act.Name, act.PlayType) - - // 获取该活动的 issues - type Issue struct { - ID int64 - ActivityID int64 - } - var issues []Issue - db.Table("activity_issues").Where("activity_id = ?", act.ID).Find(&issues) - if len(issues) == 0 { - fmt.Println(" No issues found") - continue - } - - issueIDs := make([]int64, len(issues)) - for i, iss := range issues { - issueIDs[i] = iss.ID - } - fmt.Printf(" Issues: %v\n", issueIDs) - - // 统计 activity_draw_logs - var drawLogsCount int64 - db.Table("activity_draw_logs").Where("issue_id IN ?", issueIDs).Count(&drawLogsCount) - fmt.Printf(" Draw Logs count: %d\n", drawLogsCount) - - // 检查 reward_settings - type RewardStat struct { - Level int32 - TotalOrig int64 - TotalRemain int64 - } - var rewardStats []RewardStat - db.Table("activity_reward_settings"). - Select("level, SUM(original_qty) as total_orig, SUM(quantity) as total_remain"). - Where("issue_id IN ?", issueIDs). - Group("level"). - Scan(&rewardStats) - - for _, rs := range rewardStats { - issued := rs.TotalOrig - rs.TotalRemain - fmt.Printf(" Level %d: OrigQty=%d Remain=%d Issued(库存差)=%d\n", rs.Level, rs.TotalOrig, rs.TotalRemain, issued) - } - - // 统计 draw_logs 按 level - type DrawLogStat struct { - Level int32 - WinCount int64 - } - var drawStats []DrawLogStat - db.Table("activity_draw_logs"). - Joins("JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id"). - Where("activity_draw_logs.issue_id IN ?", issueIDs). - Where("activity_draw_logs.is_winner = ?", 1). - Select("activity_reward_settings.level, COUNT(activity_draw_logs.id) as win_count"). - Group("activity_reward_settings.level"). - Scan(&drawStats) - - for _, ds := range drawStats { - fmt.Printf(" Level %d: WinCount(实际抽奖)=%d\n", ds.Level, ds.WinCount) - } - } - - fmt.Println("\n============================================") -} diff --git a/cmd/debug_all_coupons/main.go b/cmd/debug_all_coupons/main.go deleted file mode 100644 index 75226a4..0000000 --- a/cmd/debug_all_coupons/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - userID := 9090 - var userCoupons []map[string]interface{} - db.Table("user_coupons").Where("user_id = ?", userID).Order("id DESC").Find(&userCoupons) - - fmt.Printf("--- All Coupons for User %d ---\n", userID) - for _, uc := range userCoupons { - var sc map[string]interface{} - db.Table("system_coupons").Where("id = ?", uc["coupon_id"]).First(&sc) - fmt.Printf("ID: %v, Name: %v, Status: %v, ValidEnd: %v\n", - uc["id"], sc["name"], uc["status"], uc["valid_end"]) - } -} diff --git a/cmd/debug_balance/main.go b/cmd/debug_balance/main.go deleted file mode 100644 index e4ffc16..0000000 --- a/cmd/debug_balance/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -type UserCoupons struct { - ID int64 `gorm:"column:id"` - BalanceAmount int64 `gorm:"column:balance_amount"` - CouponID int64 `gorm:"column:coupon_id"` -} - -type SystemCoupons struct { - ID int64 `gorm:"column:id"` - Name string `gorm:"column:name"` - DiscountValue int64 `gorm:"column:discount_value"` -} - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - userID := 9090 - var userCoupons []UserCoupons - db.Table("user_coupons").Where("user_id = ?", userID).Find(&userCoupons) - - fmt.Printf("--- Balance Check for User %d ---\n", userID) - for _, uc := range userCoupons { - var sc SystemCoupons - db.Table("system_coupons").First(&sc, uc.CouponID) - fmt.Printf("Coupon ID: %d, Name: %s, Original: %d, Balance: %d\n", - uc.ID, sc.Name, sc.DiscountValue, uc.BalanceAmount) - } -} diff --git a/cmd/debug_card/main.go b/cmd/debug_card/main.go deleted file mode 100644 index dfe1de1..0000000 --- a/cmd/debug_card/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - // Connection string from simulate_test/main.go - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -type UserItemCards struct { - ID int64 `gorm:"column:id"` - CardID int64 `gorm:"column:card_id"` - Status int32 `gorm:"column:status"` - UsedDrawLogID int64 `gorm:"column:used_draw_log_id"` - UsedDiff int64 `gorm:"-"` // logic placeholder -} - -func (UserItemCards) TableName() string { return "user_item_cards" } - -func main() { - // 1. Connect DB - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - userCardID := 836 - - // 2. Query User Item Card - var userCard UserItemCards - err = db.Where("id = ?", userCardID).First(&userCard).Error - if err != nil { - log.Fatalf("User Card %d not found: %v", userCardID, err) - } - - fmt.Printf("UserCard %d Status: %d\n", userCard.ID, userCard.Status) - fmt.Printf("UsedDrawLogID: %d\n", userCard.UsedDrawLogID) - - if userCard.Status == 2 && userCard.UsedDrawLogID == 0 { - fmt.Println("WARNING: Card is USED (Status 2) but UsedDrawLogID is 0. Potential orphan data.") - } else if userCard.UsedDrawLogID > 0 { - fmt.Printf("Card correctly bound to DrawLog ID: %d\n", userCard.UsedDrawLogID) - } -} diff --git a/cmd/debug_coupon/main.go b/cmd/debug_coupon/main.go deleted file mode 100644 index 4a0ea35..0000000 --- a/cmd/debug_coupon/main.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -type UserCoupons struct { - ID int64 `gorm:"column:id"` - UserID int64 `gorm:"column:user_id"` - CouponID int64 `gorm:"column:coupon_id"` // System Coupon ID - Status int32 `gorm:"column:status"` // 1: Unused, 2: Used, 3: Expired - ValidStart time.Time `gorm:"column:valid_start"` - ValidEnd time.Time `gorm:"column:valid_end"` - UsedAt *time.Time `gorm:"column:used_at"` - CreatedAt time.Time `gorm:"column:created_at"` -} - -type SystemCoupons struct { - ID int64 `gorm:"column:id"` - Name string `gorm:"column:name"` - DiscountType int32 `gorm:"column:discount_type"` // 1: Direct, 2: Threshold, 3: Discount - DiscountValue int64 `gorm:"column:discount_value"` // Value in cents - MinOrderAmount int64 `gorm:"column:min_order_amount"` -} - -type Orders struct { - ID int64 `gorm:"column:id"` - OrderNo string `gorm:"column:order_no"` - ActualAmount int64 `gorm:"column:actual_amount"` - DiscountAmount int64 `gorm:"column:discount_amount"` - CouponID int64 `gorm:"column:coupon_id"` // Refers to system_coupons.id or user_coupons.id? usually user_coupons.id in many systems, need to check query. - CreatedAt time.Time -} - -func (UserCoupons) TableName() string { return "user_coupons" } -func (SystemCoupons) TableName() string { return "system_coupons" } -func (Orders) TableName() string { return "orders" } - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - userID := 9090 - - fmt.Printf("--- Querying Coupons for User %d ---\n", userID) - var userCoupons []UserCoupons - db.Where("user_id = ?", userID).Order("created_at DESC").Find(&userCoupons) - - for _, uc := range userCoupons { - var sc SystemCoupons - db.First(&sc, uc.CouponID) - - statusStr := "Unknown" - switch uc.Status { - case 1: - statusStr = "Unused" - case 2: - statusStr = "Used" - case 3: - statusStr = "Expired" - } - - fmt.Printf("\n[UserCoupon ID: %d]\n", uc.ID) - fmt.Printf(" Status: %d (%s)\n", uc.Status, statusStr) - fmt.Printf(" Name: %s\n", sc.Name) - fmt.Printf(" Type: %d, Value: %d (cents), Threshold: %d\n", sc.DiscountType, sc.DiscountValue, sc.MinOrderAmount) - fmt.Printf(" Valid: %v to %v\n", uc.ValidStart.Format("2006-01-02 15:04"), uc.ValidEnd.Format("2006-01-02 15:04")) - - if uc.Status == 2 { - // Find order used - var order Orders - // Note: orders table usually links to user_coupons ID via `coupon_id` column in this system (based on previous files). - // Let's verify if `orders.coupon_id` matches `user_coupons.id` or `system_coupons.id`. previous logs hinted `user_coupons.id`. - err := db.Where("coupon_id = ?", uc.ID).First(&order).Error - if err == nil { - fmt.Printf(" USED IN ORDER: %s (ID: %d)\n", order.OrderNo, order.ID) - fmt.Printf(" Order Total (Actual): %d cents\n", order.ActualAmount) - fmt.Printf(" Discount Applied: %d cents\n", order.DiscountAmount) - } else { - fmt.Printf(" WARNING: Status Used but Order not found linked to this UserCoupon ID.\n") - } - } - } -} diff --git a/cmd/debug_dashboard/main.go b/cmd/debug_dashboard/main.go deleted file mode 100644 index aa71154..0000000 --- a/cmd/debug_dashboard/main.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -type Orders struct { - ID int64 - OrderNo string - SourceType int32 - Status int32 - UserID int64 - TotalAmount int64 - CreatedAt time.Time -} -type ActivityDrawLogs struct { - ID int64 - OrderID int64 - IssueID int64 -} -type ActivityIssues struct { - ID int64 - ActivityID int64 -} -type Activities struct { - ID int64 - PlayType string - Name string -} - -func (Orders) TableName() string { return "orders" } -func (ActivityDrawLogs) TableName() string { return "activity_draw_logs" } -func (ActivityIssues) TableName() string { return "activity_issues" } -func (Activities) TableName() string { return "activities" } - -func main() { - dsn := "root:bindbox2025kdy@tcp(106.54.232.2:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) - if err != nil { - panic("failed to connect database: " + err.Error()) - } - - var count int64 - db.Model(&Orders{}).Count(&count) - fmt.Printf("Total Orders in DB: %d\n", count) - - var orders []Orders - if err := db.Order("id DESC").Limit(5).Find(&orders).Error; err != nil { - fmt.Printf("Error finding orders: %v\n", err) - return - } - - fmt.Printf("========== Latest 5 Orders ==========\n") - for _, o := range orders { - fmt.Printf("Order %s (ID: %d): Status=%d, SourceType=%d, Amount=%d, Time=%s\n", o.OrderNo, o.ID, o.Status, o.SourceType, o.TotalAmount, o.CreatedAt) - } - fmt.Printf("=====================================\n\n") - - checkSourceType(db, 3, "Matching") // SourceType 3 = Matching - checkSourceType(db, 2, "Ichiban") // SourceType 2 = Ichiban - checkPlayType(db, "default", "Default PlayType") -} - -func checkPlayType(db *gorm.DB, playType string, label string) { - fmt.Printf("========== Checking %s (PlayType='%s') ==========\n", label, playType) - var acts []Activities - if err := db.Where("play_type = ?", playType).Limit(5).Find(&acts).Error; err != nil { - fmt.Printf("Error finding activities: %v\n", err) - return - } - for _, a := range acts { - fmt.Printf("Activity ID=%d Name='%s' PlayType='%s'\n", a.ID, a.Name, a.PlayType) - } - fmt.Printf("============================================\n\n") -} - -func checkSourceType(db *gorm.DB, sourceType int, label string) { - fmt.Printf("========== Checking %s (SourceType=%d) ==========\n", label, sourceType) - var orders []Orders - // Get last 5 paid orders - if err := db.Where("source_type = ? AND status = 2", sourceType).Order("id DESC").Limit(5).Find(&orders).Error; err != nil { - fmt.Printf("Error finding orders: %v\n", err) - return - } - - if len(orders) == 0 { - fmt.Printf("No paid orders found for %s\n", label) - return - } - - for _, o := range orders { - fmt.Printf("Order %s (ID: %d): ", o.OrderNo, o.ID) - - // Find DrawLog - var log ActivityDrawLogs - if err := db.Where("order_id = ?", o.ID).First(&log).Error; err != nil { - fmt.Printf("DrawLog MISSING (%v)\n", err) - continue - } - - // Find Issue - var issue ActivityIssues - if err := db.Where("id = ?", log.IssueID).First(&issue).Error; err != nil { - fmt.Printf("Issue MISSING (ID: %d, Err: %v)\n", log.IssueID, err) - continue - } - - // Find Activity - var act Activities - if err := db.Where("id = ?", issue.ActivityID).First(&act).Error; err != nil { - fmt.Printf("Activity MISSING (ID: %d, Err: %v)\n", issue.ActivityID, err) - continue - } - - fmt.Printf("PlayType='%s' Name='%s' (ActivityID: %d)\n", act.PlayType, act.Name, act.ID) - } - fmt.Printf("============================================\n\n") -} diff --git a/cmd/debug_inventory/main.go b/cmd/debug_inventory/main.go deleted file mode 100644 index 049a7dd..0000000 --- a/cmd/debug_inventory/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "bindbox-game/configs" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/model" - "flag" - "fmt" -) - -func main() { - flag.Parse() - configs.Init() - dbRepo, err := mysql.New() - if err != nil { - panic(err) - } - db := dbRepo.GetDbR() - - userID := int64(9072) // - - // 1. Simple Count - var total int64 - db.Table(model.TableNameUserInventory). - Where("user_id = ?", userID). - Count(&total) - fmt.Printf("Total Inventory Count (All Status): %d\n", total) - - // 2. Count by Status 1 (Held) - var heldCount int64 - db.Table(model.TableNameUserInventory). - Where("user_id = ?", userID). - Where("status = ?", 1). - Count(&heldCount) - fmt.Printf("Held Inventory Count (Status=1): %d\n", heldCount) - - // 3. Count via Service Logic (ListInventoryWithProduct often filters by status=1) - // We simulate what the service might be doing. - // Check if there are products associated? - - // Let's list some items to see what they look like - var items []model.UserInventory - db.Where("user_id = ?", userID).Limit(50).Find(&items) - fmt.Printf("Found %d items details:\n", len(items)) - for _, item := range items { - fmt.Printf("ID: %d, ProductID: %d, OrderID: %d, Status: %d\n", item.ID, item.ProductID, item.OrderID, item.Status) - } -} diff --git a/cmd/debug_inventory_verify/main.go b/cmd/debug_inventory_verify/main.go deleted file mode 100644 index 5b533d4..0000000 --- a/cmd/debug_inventory_verify/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "bindbox-game/configs" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/model" - "flag" - "fmt" - "strconv" -) - -func main() { - // flag.StringVar(&env, "env", "dev", "env") - flag.Parse() - configs.Init() - dbRepo, err := mysql.New() - if err != nil { - panic(err) - } - db := dbRepo.GetDbR() - - userID := int64(9072) - - // 1. Simulate ListInventoryWithProduct Query (No Keyword) - var totalNoKeyword int64 - db.Table(model.TableNameUserInventory). - Where("user_id = ?", userID). - Count(&totalNoKeyword) - fmt.Printf("Scenario 1 (No Keyword) Total: %d\n", totalNoKeyword) - - // 2. Simulate With Keyword (Empty string? space?) - // If the frontend sends " " (space), let's see. - keyword := " " - var totalWithSpace int64 - db.Table(model.TableNameUserInventory). - Joins("LEFT JOIN products p ON p.id = user_inventory.product_id"). - Where("user_inventory.user_id = ?", userID). - Where("p.name LIKE ?", "%"+keyword+"%"). - Count(&totalWithSpace) - fmt.Printf("Scenario 2 (Keyword ' ') Total: %d\n", totalWithSpace) - - // 3. Simulate specific Keyword '小米' - keyword2 := "小米" - var totalXiaomi int64 - db.Table(model.TableNameUserInventory). - Joins("LEFT JOIN products p ON p.id = user_inventory.product_id"). - Where("user_inventory.user_id = ?", userID). - Where("p.name LIKE ?", "%"+keyword2+"%"). - Count(&totalXiaomi) - fmt.Printf("Scenario 3 (Keyword '小米') Total: %d\n", totalXiaomi) - - // 4. Simulate Numeric Keyword "29072" (Searching by ID) - keyword3 := "29072" - numKeyword, _ := strconv.ParseInt(keyword3, 10, 64) - var totalNumeric int64 - db.Table(model.TableNameUserInventory). - Joins("LEFT JOIN products p ON p.id = user_inventory.product_id"). - Where("user_inventory.user_id = ?", userID). - Where( - db.Where("p.name LIKE ?", "%"+keyword3+"%"). - Or("user_inventory.id = ?", numKeyword). - Or("user_inventory.order_id = ?", numKeyword), - ). - Count(&totalNumeric) - fmt.Printf("Scenario 4 (Numeric Keyword '%s') Total: %d\n", keyword3, totalNumeric) - - // 5. Check if there are soft deletes? - // Check if `deleted_at` column exists - var cols []string - db.Raw("SHOW COLUMNS FROM user_inventory").Pluck("Field", &cols) - fmt.Printf("Columns: %v\n", cols) -} diff --git a/cmd/debug_ledger/main.go b/cmd/debug_ledger/main.go deleted file mode 100644 index 99e80ba..0000000 --- a/cmd/debug_ledger/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - userCouponID := 260 - fmt.Printf("--- Ledger for UserCoupon %d ---\n", userCouponID) - var results []map[string]interface{} - db.Table("user_coupon_ledger").Where("user_coupon_id = ?", userCouponID).Find(&results) - for _, r := range results { - fmt.Printf("Action: %v, Change: %v, BalanceAfter: %v, CreatedAt: %v\n", - r["action"], r["change_amount"], r["balance_after"], r["created_at"]) - } -} diff --git a/cmd/debug_ledger_full/main.go b/cmd/debug_ledger_full/main.go deleted file mode 100644 index 5fa25ed..0000000 --- a/cmd/debug_ledger_full/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - userCouponID := 260 - fmt.Printf("--- Full Ledger Trace for UserCoupon %d ---\n", userCouponID) - var results []map[string]interface{} - db.Table("user_coupon_ledger").Where("user_coupon_id = ?", userCouponID).Find(&results) - for _, r := range results { - fmt.Printf("ID: %v, Action: %v, Change: %v, BalanceAfter: %v, OrderID: %v, CreatedAt: %v\n", - r["id"], r["action"], r["change_amount"], r["balance_after"], r["order_id"], r["created_at"]) - } -} diff --git a/cmd/debug_query/main.go b/cmd/debug_query/main.go deleted file mode 100644 index 1177b38..0000000 --- a/cmd/debug_query/main.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -type UserItemCards struct { - ID int64 `gorm:"column:id"` - UserID int64 `gorm:"column:user_id"` - CardID int64 `gorm:"column:card_id"` - Status int32 `gorm:"column:status"` - ValidStart time.Time `gorm:"column:valid_start"` - ValidEnd time.Time `gorm:"column:valid_end"` - UsedAt time.Time `gorm:"column:used_at"` - UsedActivityID int64 `gorm:"column:used_activity_id"` - UsedIssueID int64 `gorm:"column:used_issue_id"` -} - -func (UserItemCards) TableName() string { return "user_item_cards" } - -type SystemItemCards struct { - ID int64 `gorm:"column:id"` - Name string `gorm:"column:name"` - EffectType int32 `gorm:"column:effect_type"` - RewardMultiplierX1000 int32 `gorm:"column:reward_multiplier_x1000"` - BoostRateX1000 int32 `gorm:"column:boost_rate_x1000"` -} - -func (SystemItemCards) TableName() string { return "system_item_cards" } - -type ActivityDrawLogs struct { - ID int64 `gorm:"column:id"` - UserID int64 `gorm:"column:user_id"` - OrderID int64 `gorm:"column:order_id"` - IsWinner int32 `gorm:"column:is_winner"` - RewardID int64 `gorm:"column:reward_id"` - CreatedAt time.Time `gorm:"column:created_at"` -} - -func (ActivityDrawLogs) TableName() string { return "activity_draw_logs" } - -type Activities struct { - ID int64 `gorm:"column:id"` - Name string `gorm:"column:name"` - Type int32 `gorm:"column:type"` // 1: Ichiban, 2: Time-limited, 3: Matching? -} - -func (Activities) TableName() string { return "activities" } - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) - if err != nil { - log.Fatalf("failed to connect database: %v", err) - } - - var cards []UserItemCards - result := db.Where("user_id = ? AND used_activity_id = ?", 9090, 94).Find(&cards) - if result.Error != nil { - log.Fatalf("failed to query user cards: %v", result.Error) - } - - fmt.Printf("Found %d user item cards for user 9090, activity 94:\n", len(cards)) - for _, c := range cards { - fmt.Printf("- ID: %d, CardID: %d, Status: %d, UsedAt: %v\n", c.ID, c.CardID, c.Status, c.UsedAt) - - var sysCard SystemItemCards - if err := db.First(&sysCard, c.CardID).Error; err == nil { - fmt.Printf(" -> SystemCard: %s, EffectType: %d, Multiplier: %d, BoostRate: %d\n", - sysCard.Name, sysCard.EffectType, sysCard.RewardMultiplierX1000, sysCard.BoostRateX1000) - } else { - fmt.Printf(" -> SystemCard lookup failed: %v\n", err) - } - } - - fmt.Println("\nChecking Activity Draw Logs:") - var drawLogs []ActivityDrawLogs - startTime := time.Date(2026, 1, 21, 3, 0, 0, 0, time.Local) - endTime := time.Date(2026, 1, 21, 3, 5, 0, 0, time.Local) - - db.Where("user_id = ? AND created_at BETWEEN ? AND ?", 9090, startTime, endTime).Find(&drawLogs) - for _, log := range drawLogs { - fmt.Printf("- DrawLogID: %d, OrderID: %d, IsWinner: %d, RewardID: %d, Created: %v\n", - log.ID, log.OrderID, log.IsWinner, log.RewardID, log.CreatedAt) - - var remark string - db.Table("orders").Select("remark").Where("id = ?", log.OrderID).Scan(&remark) - fmt.Printf(" -> Order Remark: %s\n", remark) - } - - var act Activities - if err := db.First(&act, 94).Error; err == nil { - fmt.Printf("\nActivity 94: Name=%s, Type=%d\n", act.Name, act.Type) - } else { - fmt.Printf("\nActivity 94 lookup failed: %v\n", err) - } -} diff --git a/cmd/debug_stats/main.go b/cmd/debug_stats/main.go deleted file mode 100644 index 753d181..0000000 --- a/cmd/debug_stats/main.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "bindbox-game/configs" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/model" - "flag" - "fmt" -) - -type RevenueStat struct { - ActivityID int64 - TotalRevenue int64 - TotalDiscount int64 -} - -type DrawStat struct { - ActivityID int64 - TotalCount int64 - GamePassCount int64 - PaymentCount int64 - RefundCount int64 - PlayerCount int64 -} - -func main() { - flag.Parse() - configs.Init() - dbRepo, err := mysql.New() - if err != nil { - panic(err) - } - db := dbRepo.GetDbR() - - activityIDs := []int64{89} - - // 1. Debug Step 2: Draw Stats - var drawStats []DrawStat - err = db.Table(model.TableNameActivityDrawLogs). - Select(` - activity_issues.activity_id, - COUNT(activity_draw_logs.id) as total_count, - SUM(CASE WHEN orders.status = 2 AND orders.actual_amount = 0 THEN 1 ELSE 0 END) as game_pass_count, - SUM(CASE WHEN orders.status = 2 AND orders.actual_amount > 0 THEN 1 ELSE 0 END) as payment_count, - SUM(CASE WHEN orders.status IN (3, 4) THEN 1 ELSE 0 END) as refund_count, - COUNT(DISTINCT CASE WHEN orders.status = 2 THEN activity_draw_logs.user_id END) as player_count - `). - Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id"). - Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id"). - Where("activity_issues.activity_id IN ?", activityIDs). - Group("activity_issues.activity_id"). - Scan(&drawStats).Error - - if err != nil { - fmt.Printf("DrawStats Error: %v\n", err) - } else { - fmt.Printf("DrawStats: %+v\n", drawStats) - } - - // 2. Debug Step 3: Revenue Stats (With WHERE filter) - var revenueStats []RevenueStat - err = db.Table(model.TableNameOrders). - Select(` - activity_issues.activity_id, - SUM(orders.actual_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_revenue, - SUM(orders.discount_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_discount - `). - Joins(`JOIN ( - SELECT activity_draw_logs.order_id, activity_issues.activity_id, COUNT(*) as draw_count - FROM activity_draw_logs - JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id - GROUP BY activity_draw_logs.order_id, activity_issues.activity_id - ) as order_activity_draws ON order_activity_draws.order_id = orders.id`). - Joins(`JOIN ( - SELECT order_id, COUNT(*) as total_count - FROM activity_draw_logs - GROUP BY order_id - ) as order_total_draws ON order_total_draws.order_id = orders.id`). - Joins("JOIN activity_issues ON activity_issues.activity_id = order_activity_draws.activity_id"). - Where("orders.status = ? AND orders.status != ?", 2, 4). - Where("orders.actual_amount > ?", 0). // <--- The problematic filter? - Where("activity_issues.activity_id IN ?", activityIDs). - Group("activity_issues.activity_id"). - Scan(&revenueStats).Error - - if err != nil { - fmt.Printf("RevenueStats (With Filter) Error: %v\n", err) - } else { - fmt.Printf("RevenueStats (With Filter): %+v\n", revenueStats) - } - - // 3. Debug Step 3: Revenue Stats (Without WHERE filter, using CASE in Select) - var revenueStats2 []RevenueStat - err = db.Table(model.TableNameOrders). - Select(` - activity_issues.activity_id, - SUM(orders.actual_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_revenue, - SUM(CASE WHEN orders.actual_amount > 0 THEN orders.discount_amount * order_activity_draws.draw_count / order_total_draws.total_count ELSE 0 END) as total_discount - `). - Joins(`JOIN ( - SELECT activity_draw_logs.order_id, activity_issues.activity_id, COUNT(*) as draw_count - FROM activity_draw_logs - JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id - GROUP BY activity_draw_logs.order_id, activity_issues.activity_id - ) as order_activity_draws ON order_activity_draws.order_id = orders.id`). - Joins(`JOIN ( - SELECT order_id, COUNT(*) as total_count - FROM activity_draw_logs - GROUP BY order_id - ) as order_total_draws ON order_total_draws.order_id = orders.id`). - Joins("JOIN activity_issues ON activity_issues.activity_id = order_activity_draws.activity_id"). - Where("orders.status = ? AND orders.status != ?", 2, 4). - // Where("orders.actual_amount > ?", 0). // Removed - Where("activity_issues.activity_id IN ?", activityIDs). - Group("activity_issues.activity_id"). - Scan(&revenueStats2).Error - - if err != nil { - fmt.Printf("RevenueStats (With CASE) Error: %v\n", err) - } else { - fmt.Printf("RevenueStats (With CASE): %+v\n", revenueStats2) - } -} diff --git a/cmd/debug_usage_detail/main.go b/cmd/debug_usage_detail/main.go deleted file mode 100644 index 376c4d7..0000000 --- a/cmd/debug_usage_detail/main.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - orderID := 4695 - fmt.Printf("--- Order Coupons for Order %d ---\n", orderID) - var results []map[string]interface{} - db.Table("order_coupons").Where("order_id = ?", orderID).Find(&results) - for _, r := range results { - fmt.Printf("UserCouponID: %v, Applied: %v\n", r["user_coupon_id"], r["applied_amount"]) - } - - var uc []map[string]interface{} - db.Table("user_coupons").Where("id = ?", 260).Find(&uc) - if len(uc) > 0 { - fmt.Printf("\n--- UserCoupon 260 Final State ---\n") - fmt.Printf("Status: %v, Balance: %v, UsedAt: %v\n", uc[0]["status"], uc[0]["balance_amount"], uc[0]["used_at"]) - } -} diff --git a/cmd/debug_user_search/main.go b/cmd/debug_user_search/main.go deleted file mode 100644 index 62ccd2f..0000000 --- a/cmd/debug_user_search/main.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "bindbox-game/configs" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/model" - "flag" - "fmt" -) - -func main() { - flag.Parse() - configs.Init() - dbRepo, err := mysql.New() - if err != nil { - panic(err) - } - db := dbRepo.GetDbR() - - targetID := int64(9072) - - // Simulate the backend query in ListAppUsers - var count int64 - db.Table(model.TableNameUsers). - Where("id = ?", targetID). - Count(&count) - - fmt.Printf("Users found with ID %d: %d\n", targetID, count) -} diff --git a/cmd/diagnose_order/main.go b/cmd/diagnose_order/main.go deleted file mode 100644 index 6e8d022..0000000 --- a/cmd/diagnose_order/main.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -// 简单的 struct 映射 -type Orders struct { - ID int64 - OrderNo string - SourceType int32 - Status int32 - Remark string - UserID int64 - TotalAmount int64 - ActualAmount int64 - CreatedAt time.Time -} - -type IssuePositionClaims struct { - ID int64 - IssueID int64 - SlotIndex int64 - OrderID int64 - UserID int64 -} - -// 表名映射 -func (Orders) TableName() string { return "orders" } -func (IssuePositionClaims) TableName() string { return "issue_position_claims" } - -func main() { - // 尝试使用 config 中的密码 - dsn := "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) - if err != nil { - panic("failed to connect database: " + err.Error()) - } - - targetOrderNo := "O20260107092217073" - - var order Orders - if err := db.Where("order_no = ?", targetOrderNo).First(&order).Error; err != nil { - fmt.Printf("Error finding order: %v\n", err) - return - } - - fmt.Printf("========== Order Info ==========\n") - fmt.Printf("ID: %d\n", order.ID) - fmt.Printf("OrderNo: %s\n", order.OrderNo) - fmt.Printf("UserID: %d\n", order.UserID) - fmt.Printf("SourceType: %d\n", order.SourceType) - fmt.Printf("Status: %d (2=Paid)\n", order.Status) - fmt.Printf("Amount: Total=%d, Actual=%d\n", order.TotalAmount, order.ActualAmount) - fmt.Printf("Remark Length: %d\n", len(order.Remark)) - fmt.Printf("Remark Content: %s\n", order.Remark) - fmt.Printf("================================\n") - - var claims []IssuePositionClaims - if err := db.Where("order_id = ?", order.ID).Find(&claims).Error; err != nil { - fmt.Printf("Error checking claims: %v\n", err) - } - - fmt.Printf("========== Claims Info ==========\n") - fmt.Printf("Claims Count: %d\n", len(claims)) - for _, c := range claims { - fmt.Printf("- Claim: SlotIndex=%d IssueID=%d\n", c.SlotIndex, c.IssueID) - } - fmt.Printf("=================================\n") -} diff --git a/cmd/find_bad_coupons/main.go b/cmd/find_bad_coupons/main.go deleted file mode 100644 index 759bc9c..0000000 --- a/cmd/find_bad_coupons/main.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("Finding coupons with status=3 but valid_end > NOW()...") - var results []map[string]interface{} - db.Raw("SELECT id, user_id, coupon_id, balance_amount, valid_end, status FROM user_coupons WHERE status = 3 AND valid_end > NOW()").Scan(&results) - if len(results) == 0 { - fmt.Println("No coupons found with status=3 but valid_end in the future.") - } else { - fmt.Printf("Found %d coupons:\n", len(results)) - for _, res := range results { - fmt.Printf("ID: %v, UserID: %v, CouponID: %v, Balance: %v, ValidEnd: %v, Status: %v\n", - res["id"], res["user_id"], res["coupon_id"], res["balance_amount"], res["valid_end"], res["status"]) - } - } - - fmt.Println("\nChecking all coupons for User 9090 specifically...") - var results9090 []map[string]interface{} - db.Raw("SELECT id, status, balance_amount, valid_end FROM user_coupons WHERE user_id = 9090").Scan(&results9090) - for _, res := range results9090 { - fmt.Printf("ID: %v, Status: %v, Balance: %v, ValidEnd: %v\n", res["id"], res["status"], res["balance_amount"], res["valid_end"]) - } -} diff --git a/cmd/find_live_activity/main.go b/cmd/find_live_activity/main.go deleted file mode 100644 index 8c5a1e1..0000000 --- a/cmd/find_live_activity/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "bindbox-game/configs" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/model" - "flag" - "fmt" - "time" -) - -func main() { - flag.Parse() - configs.Init() - dbRepo, err := mysql.New() - if err != nil { - panic(err) - } - db := dbRepo.GetDbR() - - startTime, _ := time.ParseInLocation("2006-01-02", "2026-01-19", time.Local) - endTime := startTime.Add(24 * time.Hour) - - var logs []model.LivestreamDrawLogs - db.Where("created_at >= ? AND created_at < ?", startTime, endTime).Find(&logs) - - fmt.Printf("Found %d Livestream Draw Logs on Jan 19th.\n", len(logs)) - - activityCounts := make(map[int64]int) - for _, l := range logs { - activityCounts[l.ActivityID]++ - } - - for id, count := range activityCounts { - fmt.Printf("Livestream Activity ID: %d, Count: %d\n", id, count) - - // Get Name - var act model.LivestreamActivities - db.First(&act, id) - fmt.Printf(" -> Name: %s\n", act.Name) - } -} diff --git a/cmd/fix_order/main.go b/cmd/fix_order/main.go deleted file mode 100644 index 2c563ea..0000000 --- a/cmd/fix_order/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) - if err != nil { - log.Fatalf("failed to connect database: %v", err) - } - - targetID := "3791062042765557775" - - // Check current state - var granted int - db.Table("douyin_orders").Select("reward_granted").Where("shop_order_id = ?", targetID).Scan(&granted) - fmt.Printf("Current reward_granted: %d\n", granted) - - // Fix it - if granted == 1 { - err := db.Table("douyin_orders").Where("shop_order_id = ?", targetID).Update("reward_granted", 0).Error - if err != nil { - fmt.Printf("Update failed: %v\n", err) - } else { - fmt.Printf("✅ Successfully reset reward_granted to 0 for order %s\n", targetID) - } - } else { - fmt.Println("No update needed (already 0 or order not found).") - } -} diff --git a/cmd/full_9090_dump/main.go b/cmd/full_9090_dump/main.go deleted file mode 100644 index a55f30f..0000000 --- a/cmd/full_9090_dump/main.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("--- User 9090 Full Coupon Dump (Cloud DB) ---") - - var userCoupons []map[string]interface{} - db.Raw("SELECT * FROM user_coupons WHERE user_id = 9090").Scan(&userCoupons) - - for _, uc := range userCoupons { - fmt.Printf("\nCoupon Record: %+v\n", uc) - - id := uc["id"] - - // Get associated orders for this coupon - var orders []map[string]interface{} - db.Raw("SELECT order_id, applied_amount, created_at FROM order_coupons WHERE user_coupon_id = ?", id).Scan(&orders) - fmt.Printf(" Orders associated: %+v\n", orders) - - // Get ledger entries - var ledger []map[string]interface{} - db.Raw("SELECT action, change_amount, balance_after, created_at FROM user_coupon_ledger WHERE user_coupon_id = ?", id).Scan(&ledger) - fmt.Printf(" Ledger entries: %+v\n", ledger) - } -} diff --git a/cmd/handlergen/README.md b/cmd/handlergen/README.md deleted file mode 100644 index 3b5e397..0000000 --- a/cmd/handlergen/README.md +++ /dev/null @@ -1,28 +0,0 @@ -## 自动生成数据库模型和常见的 CRUD 操作 - -### Usage - -```shell -go run cmd/handlergen/main.go -h - -Usage of ./cmd/handlergen/main.go: - -table string - enter the required data table -``` - -#### -table - -指定要生成的表名称。 - -eg : - -```shell ---tables="admin" # generate from `admin` -``` - -## 示例 - -```shell -# 根目录下执行 -go run cmd/handlergen/main.go -table "customer" -``` diff --git a/cmd/handlergen/handler_template.go.tpl b/cmd/handlergen/handler_template.go.tpl deleted file mode 100644 index 9fa5d4a..0000000 --- a/cmd/handlergen/handler_template.go.tpl +++ /dev/null @@ -1,265 +0,0 @@ -package {{.PackageName}} - -import ( - "net/http" - "strconv" - - "bindbox-game/internal/code" - "bindbox-game/internal/pkg/core" - "WeChatService/internal/pkg/logger" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/dao" - "bindbox-game/internal/repository/mysql/model" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type handler struct { - logger logger.CustomLoggerLogger - writeDB *dao.Query - readDB *dao.Query -} - -type genResultInfo struct { - RowsAffected int64 `json:"rows_affected"` - Error error `json:"error"` -} - -func New(logger logger.CustomLogger, db mysql.Repo) *handler { - return &handler{ - logger: logger, - writeDB: dao.Use(db.GetDbW()), - readDB: dao.Use(db.GetDbR()), - } -} - -// Create 新增数据 -// @Summary 新增数据 -// @Description 新增数据 -// @Tags API.{{.VariableName}} -// @Accept json -// @Produce json -// @Param RequestBody body model.{{.StructName}} true "请求参数" -// @Success 200 {object} model.{{.StructName}} -// @Failure 400 {object} code.Failure -// @Router /api/{{.VariableName}} [post] -func (h *handler) Create() core.HandlerFunc { - return func(ctx core.Context) { - var createData model.{{.StructName}} - if err := ctx.ShouldBindJSON(&createData); err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ParamBindError, - err.Error()), - ) - return - } - - if err := h.writeDB.{{.StructName}}.WithContext(ctx.RequestContext()).Create(&createData); err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - err.Error()), - ) - return - } - - ctx.Payload(createData) - } -} - -// List 获取列表数据 -// @Summary 获取列表数据 -// @Description 获取列表数据 -// @Tags API.{{.VariableName}} -// @Accept json -// @Produce json -// @Success 200 {object} []model.{{.StructName}} -// @Failure 400 {object} code.Failure -// @Router /api/{{.VariableName}}s [get] -func (h *handler) List() core.HandlerFunc { - return func(ctx core.Context) { - list, err := h.readDB.{{.StructName}}.WithContext(ctx.RequestContext()).Find() - if err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - err.Error()), - ) - return - } - - ctx.Payload(list) - } -} - -// GetByID 根据 ID 获取数据 -// @Summary 根据 ID 获取数据 -// @Description 根据 ID 获取数据 -// @Tags API.{{.VariableName}} -// @Accept json -// @Produce json -// @Param id path string true "ID" -// @Success 200 {object} model.{{.StructName}} -// @Failure 400 {object} code.Failure -// @Router /api/{{.VariableName}}/{id} [get] -func (h *handler) GetByID() core.HandlerFunc { - return func(ctx core.Context) { - id, err := strconv.Atoi(ctx.Param("id")) - if err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ParamBindError, - err.Error()), - ) - return - } - - info, err := h.readDB.{{.StructName}}.WithContext(ctx.RequestContext()).Where(h.readDB.{{.StructName}}.ID.Eq(int32(id))).First() - if err != nil { - if err == gorm.ErrRecordNotFound { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - "record not found"), - ) - } else { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - err.Error()), - ) - } - return - } - - ctx.Payload(info) - } -} - -// DeleteByID 根据 ID 删除数据 -// @Summary 根据 ID 删除数据 -// @Description 根据 ID 删除数据 -// @Tags API.{{.VariableName}} -// @Accept json -// @Produce json -// @Param id path string true "ID" -// @Success 200 {object} genResultInfo -// @Failure 400 {object} code.Failure -// @Router /api/{{.VariableName}}/{id} [delete] -func (h *handler) DeleteByID() core.HandlerFunc { - return func(ctx core.Context) { - id, err := strconv.Atoi(ctx.Param("id")) - if err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ParamBindError, - err.Error()), - ) - return - } - - info, err := h.readDB.{{.StructName}}.WithContext(ctx.RequestContext()).Where(h.readDB.{{.StructName}}.ID.Eq(int32(id))).First() - if err != nil { - if err == gorm.ErrRecordNotFound { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - "record not found"), - ) - } else { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - err.Error()), - ) - } - return - } - - result, err := h.writeDB.{{.StructName}}.Delete(info) - if err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - err.Error()), - ) - } - - resultInfo := new(genResultInfo) - resultInfo.RowsAffected = result.RowsAffected - resultInfo.Error = result.Error - - ctx.Payload(resultInfo) - } -} - -// UpdateByID 根据 ID 更新数据 -// @Summary 根据 ID 更新数据 -// @Description 根据 ID 更新数据 -// @Tags API.{{.VariableName}} -// @Accept json -// @Produce json -// @Param id path string true "ID" -// @Param RequestBody body model.{{.StructName}} true "请求参数" -// @Success 200 {object} genResultInfo -// @Failure 400 {object} code.Failure -// @Router /api/{{.VariableName}}/{id} [put] -func (h *handler) UpdateByID() core.HandlerFunc { - return func(ctx core.Context) { - id, err := strconv.Atoi(ctx.Param("id")) - if err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ParamBindError, - err.Error()), - ) - return - } - - var updateData map[string]interface{} - if err := ctx.ShouldBindJSON(&updateData); err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - err.Error()), - ) - return - } - - info, err := h.readDB.{{.StructName}}.WithContext(ctx.RequestContext()).Where(h.readDB.{{.StructName}}.ID.Eq(int32(id))).First() - if err != nil { - if err == gorm.ErrRecordNotFound { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - "record not found"), - ) - } else { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - err.Error()), - ) - } - return - } - - result, err := h.writeDB.{{.StructName}}.WithContext(ctx.RequestContext()).Where(h.writeDB.{{.StructName}}.ID.Eq(info.ID)).Updates(updateData) - if err != nil { - ctx.AbortWithError(core.Error( - http.StatusBadRequest, - code.ServerError, - err.Error()), - ) - return - } - - resultInfo := new(genResultInfo) - resultInfo.RowsAffected = result.RowsAffected - resultInfo.Error = result.Error - - ctx.Payload(resultInfo) - } -} diff --git a/cmd/handlergen/main.go b/cmd/handlergen/main.go deleted file mode 100644 index 4767d82..0000000 --- a/cmd/handlergen/main.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "os" - "path/filepath" - "strings" - "text/template" -) - -type TemplateData struct { - PackageName string - VariableName string - StructName string -} - -func main() { - table := flag.String("table", "", "enter the required data table") - flag.Parse() - - tableName := *table - if tableName == "" { - log.Fatal("table cannot be empty, please provide a valid table name.") - } - - // 获取当前工作目录 - wd, err := os.Getwd() - if err != nil { - log.Fatalf("Error getting working directory:%s", err.Error()) - } - - // 模板文件路径 - tmplPath := fmt.Sprintf("%s/cmd/handlergen/handler_template.go.tpl", wd) - tmpl, err := template.ParseFiles(tmplPath) - if err != nil { - log.Fatal(err) - } - - log.Printf("Template file parsed: %s", tmplPath) - - // 替换的变量 - data := TemplateData{ - PackageName: tableName, - VariableName: tableName, - StructName: toCamelCase(tableName), - } - - // 生成文件的目录和文件名 - outputDir := fmt.Sprintf("%s/internal/api/%s", wd, tableName) - outputFile := filepath.Join(outputDir, fmt.Sprintf("%s.gen.go", tableName)) - - // 创建目录 - err = os.MkdirAll(outputDir, os.ModePerm) - if err != nil { - log.Fatal(err) - } - - // 创建文件 - file, err := os.Create(outputFile) - if err != nil { - log.Fatal(err) - } - defer file.Close() - - log.Printf("File created: %s", outputFile) - - // 执行模板并生成文件 - err = tmpl.Execute(file, data) - if err != nil { - log.Fatal(err) - } - - log.Println("Template execution completed successfully.") -} - -// 将字符串转为驼峰式命名 -func toCamelCase(s string) string { - // 用下划线分割字符串 - parts := strings.Split(s, "_") - - // 对每个部分首字母大写 - for i := 0; i < len(parts); i++ { - parts[i] = strings.Title(parts[i]) - } - - // 拼接所有部分 - return strings.Join(parts, "") -} diff --git a/cmd/inspect_order/main.go b/cmd/inspect_order/main.go deleted file mode 100644 index 5391a97..0000000 --- a/cmd/inspect_order/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - orderID := 4695 - fmt.Printf("--- Inspecting Order %d ---\n", orderID) - - type Order struct { - ID int64 - OrderNo string - ActualAmount int64 - DiscountAmount int64 - Remark string - } - var order Order - err = db.Table("orders").Where("id = ?", orderID).First(&order).Error - if err != nil { - log.Fatalf("Order not found: %v", err) - } - - fmt.Printf("Order No: %s\n", order.OrderNo) - fmt.Printf("Actual Pay: %d cents\n", order.ActualAmount) - fmt.Printf("Discount: %d cents\n", order.DiscountAmount) - fmt.Printf("Remark: %s\n", order.Remark) - - total := order.ActualAmount + order.DiscountAmount - fmt.Printf("Total implied: %d cents\n", total) - - var logs []map[string]interface{} - db.Table("activity_draw_logs"). - Select("activity_draw_logs.*, products.price as product_price"). - Joins("JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id"). - Joins("JOIN products ON products.id = activity_reward_settings.product_id"). - Where("order_id = ?", orderID). - Scan(&logs) - - fmt.Printf("Draw Logs Found: %d\n", len(logs)) - var sumPrice int64 - for i, l := range logs { - var price int64 - // Extract price carefully - switch p := l["product_price"].(type) { - case int64: - price = p - case int32: - price = int64(p) - case float64: - price = int64(p) - default: - fmt.Printf(" Item %d: Unknown price type %T\n", i, p) - } - - sumPrice += price - fmt.Printf(" Item %d: Price=%v (parsed: %d)\n", i, l["product_price"], price) - } - fmt.Printf("Sum of Products: %d cents\n", sumPrice) -} diff --git a/cmd/inspect_order_4746/main.go b/cmd/inspect_order_4746/main.go deleted file mode 100644 index 496c78a..0000000 --- a/cmd/inspect_order_4746/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - orderID := 4746 - var o []map[string]interface{} - db.Table("orders").Where("id = ?", orderID).Find(&o) - - if len(o) > 0 { - fmt.Printf("--- Order %d Details ---\n", orderID) - fmt.Printf("OrderNo: %v, Total: %v, ActualPay: %v, Discount: %v, CreatedAt: %v, Remark: %v\n", - o[0]["order_no"], o[0]["total_amount"], o[0]["actual_amount"], o[0]["discount_amount"], o[0]["created_at"], o[0]["remark"]) - } else { - fmt.Printf("Order %d not found\n", orderID) - } -} diff --git a/cmd/master_reconcile/main.go b/cmd/master_reconcile/main.go deleted file mode 100644 index 7cb6006..0000000 --- a/cmd/master_reconcile/main.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("--- Master Coupon Reconciliation & Repair Generator (Cloud DB) ---") - - var userCoupons []struct { - ID int64 - UserID int64 - CouponID int64 - BalanceAmount int64 - Status int32 - ValidEnd string - OriginalValue int64 - } - - // Join with system_coupons to get original discount value - db.Raw(` - SELECT uc.id, uc.user_id, uc.coupon_id, uc.balance_amount, uc.status, uc.valid_end, sc.discount_value as original_value - FROM user_coupons uc - JOIN system_coupons sc ON uc.coupon_id = sc.id - `).Scan(&userCoupons) - - for _, uc := range userCoupons { - // Calculate actual usage from ledger - var ledgerSum int64 - db.Raw("SELECT ABS(SUM(change_amount)) FROM user_coupon_ledger WHERE user_coupon_id = ? AND action = 'apply'", uc.ID).Scan(&ledgerSum) - - // Calculate actual usage from order_coupons - var orderSum int64 - db.Raw("SELECT SUM(applied_amount) FROM order_coupons WHERE user_coupon_id = ?", uc.ID).Scan(&orderSum) - - // Source of truth: Max of both (covering cases where one might be missing manually) - actualUsed := ledgerSum - if orderSum > actualUsed { - actualUsed = orderSum - } - - calculatedBalance := uc.OriginalValue - actualUsed - if calculatedBalance < 0 { - calculatedBalance = 0 - } - - // Determine correct status - // 1: Unused (Balance > 0 and Time not past) - // 2: Used (Balance == 0) - // 3: Expired (Balance > 0 and Time past) - - expectedStatus := uc.Status - if calculatedBalance == 0 { - expectedStatus = 2 - } else { - // If balance remaining, check time expiry - // (Assuming valid_end is in RFC3339 or similar from DB) - // For simplicity in SQL generation, we'll focus on the obvious mismatches found here. - } - - if calculatedBalance != uc.BalanceAmount || expectedStatus != uc.Status { - fmt.Printf("-- User %d Coupon %d: Bal %d->%d, Status %v->%v\n", - uc.UserID, uc.ID, uc.BalanceAmount, calculatedBalance, uc.Status, expectedStatus) - fmt.Printf("UPDATE user_coupons SET balance_amount = %d, status = %v, updated_at = NOW() WHERE id = %d;\n", - calculatedBalance, expectedStatus, uc.ID) - } - } -} diff --git a/cmd/matching_sim/main.go b/cmd/matching_sim/main.go deleted file mode 100644 index e1a3074..0000000 --- a/cmd/matching_sim/main.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "sort" - - "bindbox-game/internal/pkg/logger" - "bindbox-game/internal/pkg/redis" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/dao" - activitysvc "bindbox-game/internal/service/activity" -) - -// usage: go run cmd/matching_sim/main.go -env dev -runs 10000 -func main() { - runs := flag.Int("runs", 10000, "运行模拟的次数") - flag.Parse() - - // 1. 初始化数据库 - dbRepo, err := mysql.New() - if err != nil { - panic(fmt.Sprintf("数据库连接失败: %v", err)) - } - - // 2. 初始化日志 (模拟 Service 需要) - l, err := logger.NewCustomLogger(dao.Use(dbRepo.GetDbW())) - if err != nil { - panic(err) - } - - // 3. 初始化 Service (完全模拟真实注入) - // 注意:这里不需要真实的 user service,传入 nil 即可 - svc := activitysvc.New(l, dbRepo, nil, redis.GetClient()) - - ctx := context.Background() - - // 4. 从真实数据库加载卡牌配置 - fmt.Println(">>> 正在从数据库加载真实卡牌配置...") - configs, err := svc.ListMatchingCardTypes(ctx) - if err != nil { - panic(fmt.Sprintf("读取卡牌配置失败: %v", err)) - } - - if len(configs) == 0 { - fmt.Println("警告: 数据库中没有启用的卡牌配置,将使用默认配置。") - configs = []activitysvc.CardTypeConfig{ - {Code: "A", Quantity: 9}, {Code: "B", Quantity: 9}, {Code: "C", Quantity: 9}, - {Code: "D", Quantity: 9}, {Code: "E", Quantity: 9}, {Code: "F", Quantity: 9}, - {Code: "G", Quantity: 9}, {Code: "H", Quantity: 9}, {Code: "I", Quantity: 9}, - {Code: "J", Quantity: 9}, {Code: "K", Quantity: 9}, - } - } - - fmt.Println("当前生效配置:") - for _, c := range configs { - fmt.Printf(" - [%s]: %d张\n", c.Code, c.Quantity) - } - - // 5. 开始执行模拟 - fmt.Printf("\n>>> 正在执行 %d 次大规模真实模拟...\n", *runs) - results := make(map[int64]int) - mseed := []byte("production_simulation_seed") - position := "B" // 默认模拟选中 B 类型 - - for i := 0; i < *runs; i++ { - // 调用真实业务函数创建游戏 (固定数量逻辑) - game := activitysvc.NewMatchingGameWithConfig(configs, position, mseed) - - // 调用真实业务模拟函数 - pairs := game.SimulateMaxPairs() - results[pairs]++ - } - - // 6. 统计并输出 - fmt.Println("\n对数分布统计 (100% 模拟真实生产路径):") - var pairsList []int64 - for k := range results { - pairsList = append(pairsList, k) - } - sort.Slice(pairsList, func(i, j int) bool { - return pairsList[i] < pairsList[j] - }) - - sumPairs := int64(0) - for _, p := range pairsList { - count := results[p] - sumPairs += p * int64(count) - fmt.Printf(" %2d 对: %5d 次 (%5.2f%%)\n", p, count, float64(count)/float64(*runs)*100) - } - fmt.Printf("\n平均对数: %.4f\n\n", float64(sumPairs)/float64(*runs)) -} diff --git a/cmd/migrate_configs/main.go b/cmd/migrate_configs/main.go deleted file mode 100644 index 9101717..0000000 --- a/cmd/migrate_configs/main.go +++ /dev/null @@ -1,155 +0,0 @@ -package main - -import ( - "context" - "encoding/base64" - "flag" - "fmt" - "os" - - "bindbox-game/configs" - "bindbox-game/internal/pkg/logger" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/dao" - "bindbox-game/internal/service/sysconfig" -) - -var ( - dryRun = flag.Bool("dry-run", false, "仅打印将要写入的配置,不实际写入数据库") - force = flag.Bool("force", false, "强制覆盖已存在的配置") -) - -func main() { - flag.Parse() - - // 初始化数据库 - dbRepo, err := mysql.New() - if err != nil { - fmt.Printf("数据库连接失败: %v\n", err) - os.Exit(1) - } - - // 初始化 logger (简化版) - customLogger, err := logger.NewCustomLogger(dao.Use(dbRepo.GetDbW()), - logger.WithDebugLevel(), - logger.WithOutputInConsole(), - ) - if err != nil { - fmt.Printf("Logger 初始化失败: %v\n", err) - os.Exit(1) - } - - ctx := context.Background() - - // 创建动态配置服务 - dynamicCfg := sysconfig.NewDynamicConfig(customLogger, dbRepo) - staticCfg := configs.Get() - - // 定义要迁移的配置项 - type configItem struct { - Key string - Value string - Remark string - } - - // 读取证书文件内容并 Base64 编码 - readAndEncode := func(path string) string { - if path == "" { - return "" - } - data, err := os.ReadFile(path) - if err != nil { - fmt.Printf("警告: 读取文件 %s 失败: %v\n", path, err) - return "" - } - return base64.StdEncoding.EncodeToString(data) - } - - items := []configItem{ - // COS 配置 - {sysconfig.KeyCOSBucket, staticCfg.COS.Bucket, "COS Bucket名称"}, - {sysconfig.KeyCOSRegion, staticCfg.COS.Region, "COS 地域"}, - {sysconfig.KeyCOSSecretID, staticCfg.COS.SecretID, "COS SecretID (加密存储)"}, - {sysconfig.KeyCOSSecretKey, staticCfg.COS.SecretKey, "COS SecretKey (加密存储)"}, - {sysconfig.KeyCOSBaseURL, staticCfg.COS.BaseURL, "COS 自定义域名"}, - - // 微信小程序配置 - {sysconfig.KeyWechatAppID, staticCfg.Wechat.AppID, "微信小程序 AppID"}, - {sysconfig.KeyWechatAppSecret, staticCfg.Wechat.AppSecret, "微信小程序 AppSecret (加密存储)"}, - {sysconfig.KeyWechatLotteryResultTemplateID, staticCfg.Wechat.LotteryResultTemplateID, "中奖结果订阅消息模板ID"}, - - // 微信支付配置 - {sysconfig.KeyWechatPayMchID, staticCfg.WechatPay.MchID, "微信支付商户号"}, - {sysconfig.KeyWechatPaySerialNo, staticCfg.WechatPay.SerialNo, "微信支付证书序列号"}, - {sysconfig.KeyWechatPayPrivateKey, readAndEncode(staticCfg.WechatPay.PrivateKeyPath), "微信支付私钥 (Base64编码, 加密存储)"}, - {sysconfig.KeyWechatPayApiV3Key, staticCfg.WechatPay.ApiV3Key, "微信支付 API v3 密钥 (加密存储)"}, - {sysconfig.KeyWechatPayNotifyURL, staticCfg.WechatPay.NotifyURL, "微信支付回调地址"}, - {sysconfig.KeyWechatPayPublicKeyID, staticCfg.WechatPay.PublicKeyID, "微信支付公钥ID"}, - {sysconfig.KeyWechatPayPublicKey, readAndEncode(staticCfg.WechatPay.PublicKeyPath), "微信支付公钥 (Base64编码, 加密存储)"}, - - // 阿里云短信配置 - {sysconfig.KeyAliyunSMSAccessKeyID, staticCfg.AliyunSMS.AccessKeyID, "阿里云短信 AccessKeyID"}, - {sysconfig.KeyAliyunSMSAccessKeySecret, staticCfg.AliyunSMS.AccessKeySecret, "阿里云短信 AccessKeySecret (加密存储)"}, - {sysconfig.KeyAliyunSMSSignName, staticCfg.AliyunSMS.SignName, "短信签名"}, - {sysconfig.KeyAliyunSMSTemplateCode, staticCfg.AliyunSMS.TemplateCode, "短信模板Code"}, - } - - fmt.Println("========== 配置迁移工具 ==========") - fmt.Printf("环境: %s\n", configs.ProjectName) - fmt.Printf("Dry Run: %v\n", *dryRun) - fmt.Printf("Force: %v\n", *force) - fmt.Println() - - successCount := 0 - skipCount := 0 - failCount := 0 - - for _, item := range items { - if item.Value == "" { - fmt.Printf("[跳过] %s: 值为空\n", item.Key) - skipCount++ - continue - } - - // 检查是否已存在 - existing := dynamicCfg.Get(ctx, item.Key) - if existing != "" && !*force { - fmt.Printf("[跳过] %s: 已存在 (使用 -force 覆盖)\n", item.Key) - skipCount++ - continue - } - - // 脱敏显示 - displayValue := item.Value - if sysconfig.IsSensitiveKey(item.Key) { - if len(displayValue) > 8 { - displayValue = displayValue[:4] + "****" + displayValue[len(displayValue)-4:] - } else { - displayValue = "****" - } - } else if len(displayValue) > 50 { - displayValue = displayValue[:50] + "..." - } - - if *dryRun { - fmt.Printf("[预览] %s = %s\n", item.Key, displayValue) - successCount++ - } else { - if err := dynamicCfg.Set(ctx, item.Key, item.Value, item.Remark); err != nil { - fmt.Printf("[失败] %s: %v\n", item.Key, err) - failCount++ - } else { - fmt.Printf("[成功] %s = %s\n", item.Key, displayValue) - successCount++ - } - } - } - - fmt.Println() - fmt.Printf("========== 迁移结果 ==========\n") - fmt.Printf("成功: %d, 跳过: %d, 失败: %d\n", successCount, skipCount, failCount) - - if *dryRun { - fmt.Println("\n这只是预览,使用不带 -dry-run 参数执行实际迁移") - } -} diff --git a/cmd/reconcile_coupons/main.go b/cmd/reconcile_coupons/main.go deleted file mode 100644 index b0e2306..0000000 --- a/cmd/reconcile_coupons/main.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("--- Advanced Coupon Data Reconciliation (Cloud DB) ---") - - var userCoupons []struct { - ID int64 - UserID int64 - SystemAmount int64 - BalanceAmount int64 - Status int32 - ValidEnd string - } - - // Join with system_coupons to get original amount - db.Raw(` - SELECT uc.id, uc.user_id, sc.discount_value as system_amount, uc.balance_amount, uc.status, uc.valid_end - FROM user_coupons uc - JOIN system_coupons sc ON uc.coupon_id = sc.id - `).Scan(&userCoupons) - - fmt.Println("Generating repair SQL based on ledger and order association...") - - for _, uc := range userCoupons { - // Calculate total deduction from ledger - var totalDeduction struct { - Sum int64 - } - db.Raw("SELECT ABS(SUM(change_amount)) as sum FROM user_coupon_ledger WHERE user_coupon_id = ? AND action = 'apply'", uc.ID).Scan(&totalDeduction) - - // Calculate total deduction from order_coupons (secondary verification) - var orderDeduction struct { - Sum int64 - } - db.Raw("SELECT SUM(applied_amount) as sum FROM order_coupons WHERE user_coupon_id = ?", uc.ID).Scan(&orderDeduction) - - // Choose the source of truth (ledger usually preferred) - actualUsed := totalDeduction.Sum - if orderDeduction.Sum > actualUsed { - actualUsed = orderDeduction.Sum - } - - expectedBalance := uc.SystemAmount - actualUsed - if expectedBalance < 0 { - expectedBalance = 0 - } - - expectedStatus := uc.Status - if expectedBalance == 0 { - expectedStatus = 2 // Fully Used - } else if expectedBalance > 0 { - // Check if it should be expired or unused - // We won't downgrade Expired (3) back to Unused (1) unless balance > 0 and time is not up. - // However, usually if balance > 0 and Status is 2, it's a bug. - } - - // Check for status 3 that shouldn't be - // (Currently the user says they see status 3 but it's used?) - // If balance == 0, status must be 2. - - if expectedBalance != uc.BalanceAmount || expectedStatus != uc.Status { - fmt.Printf("-- Coupon %d (User %d): Bal %d->%d, Status %v->%v (Used: %d, Orig: %d)\n", - uc.ID, uc.UserID, uc.BalanceAmount, expectedBalance, uc.Status, expectedStatus, actualUsed, uc.SystemAmount) - fmt.Printf("UPDATE user_coupons SET balance_amount = %d, status = %v WHERE id = %d;\n", - expectedBalance, expectedStatus, uc.ID) - } - } -} diff --git a/cmd/simulate_test/main.go b/cmd/simulate_test/main.go deleted file mode 100644 index 74bcffa..0000000 --- a/cmd/simulate_test/main.go +++ /dev/null @@ -1,186 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "time" - - "github.com/golang-jwt/jwt/v4" - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -// Configs from dev_configs.toml -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" - JwtSecret = "AppUserJwtSecret2025" - ApiURL = "http://127.0.0.1:9991/api/app/lottery/join" -) - -type UserItemCards struct { - ID int64 `gorm:"column:id"` - Status int32 `gorm:"column:status"` - UsedAt time.Time `gorm:"column:used_at"` - UsedActivityID int64 `gorm:"column:used_activity_id"` - UsedIssueID int64 `gorm:"column:used_issue_id"` - UsedDrawLogID int64 `gorm:"column:used_draw_log_id"` -} - -func (UserItemCards) TableName() string { return "user_item_cards" } - -func main() { - // 1. Connect DB - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - // 2. Reset Item Card 836 - cardID := int64(836) - fmt.Printf("Restoring UserItemCard %d...\n", cardID) - // We set status to 1 and clear used fields - res := db.Model(&UserItemCards{}).Where("id = ?", cardID).Updates(map[string]interface{}{ - "status": 1, - "used_at": nil, - "used_activity_id": 0, - "used_issue_id": 0, - "used_draw_log_id": 0, - }) - if res.Error != nil { - log.Fatalf("Failed to restore card: %v", res.Error) - } - fmt.Println("Card restored to Status 1.") - - // 3. Generate JWT - tokenString, err := generateToken(9090, JwtSecret) - if err != nil { - log.Fatalf("Failed to generate token: %v", err) - } - fmt.Printf("Generated Token for User 9090.\n") - - // 4. Send API Request - targetSlot := int64(1) - - reqBody := map[string]interface{}{ - "activity_id": 94, - "issue_id": 105, - "count": 1, - "slot_index": []int64{targetSlot}, - "item_card_id": cardID, - "channel": "simulation", - } - jsonBody, _ := json.Marshal(reqBody) - - req, _ := http.NewRequest("POST", ApiURL, bytes.NewBuffer(jsonBody)) - req.Header.Set("Content-Type", "application/json") - // Backend seems to expect raw token string without Bearer prefix - req.Header.Set("Authorization", tokenString) - - client := &http.Client{} - fmt.Println("Sending JoinLottery request...") - resp, err := client.Do(req) - if err != nil { - log.Fatalf("Request failed: %v", err) - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - fmt.Printf("Response Status: %s\n", resp.Status) - fmt.Printf("Response Body: %s\n", string(body)) - - // If success (200 OK), parse order info, simulate payment, trigger draw - if resp.StatusCode == 200 { - var resMap map[string]interface{} - json.Unmarshal(body, &resMap) - // joinID := resMap["join_id"].(string) // Unused - orderNo := resMap["order_no"].(string) - - fmt.Printf("Order Created: %s. Simulating Payment...\n", orderNo) - - // Simulate Payment in DB - db.Table("orders").Where("order_no = ?", orderNo).Updates(map[string]interface{}{ - "status": 2, - "paid_at": time.Now(), - }) - fmt.Println("Order marked as PAID (Status 2).") - - // Trigger Draw via GetLotteryResult with order_no - resultURL := fmt.Sprintf("http://127.0.0.1:9991/api/app/lottery/result?order_no=%s", orderNo) - reqResult, _ := http.NewRequest("GET", resultURL, nil) - reqResult.Header.Set("Authorization", tokenString) - - fmt.Println("Triggering Draw (GetLotteryResult)...") - respResult, err := client.Do(reqResult) - if err != nil { - log.Fatalf("Result Request failed: %v", err) - } - defer respResult.Body.Close() - bodyResult, _ := io.ReadAll(respResult.Body) - fmt.Printf("Draw Response: %s\n", string(bodyResult)) - - time.Sleep(2 * time.Second) - checkLogs(db, cardID) - } -} - -func generateToken(userID int64, secret string) (string, error) { - // Claims mimicking bindbox-game/internal/pkg/jwtoken/jwtoken.go structure - claims := jwt.MapClaims{ - "id": int32(userID), - "username": "simulation", - "nickname": "simulation_user", - "is_super": 0, - "platform": "simulation", - // Standard claims - "exp": time.Now().Add(time.Hour).Unix(), - "iat": time.Now().Unix(), - "nbf": time.Now().Unix(), - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(secret)) -} - -func checkLogs(db *gorm.DB, cardID int64) { - fmt.Println("\n--- Verification Results ---") - // Check Card Status - var card UserItemCards - if err := db.First(&card, cardID).Error; err != nil { - fmt.Printf("Error fetching card: %v\n", err) - return - } - fmt.Printf("Card Status: %d (Expected 2 if success)\n", card.Status) - - if card.Status == 2 { - fmt.Printf("Card Used At: %v\n", card.UsedAt) - fmt.Printf("Used Draw Log ID: %d\n", card.UsedDrawLogID) - - if card.UsedDrawLogID > 0 { - // Check Draw Log - type ActivityDrawLogs struct { - ID int64 `gorm:"column:id"` - RewardID int64 `gorm:"column:reward_id"` - OrderID int64 `gorm:"column:order_id"` - } - var dl ActivityDrawLogs - db.Table("activity_draw_logs").First(&dl, card.UsedDrawLogID) - fmt.Printf("Original Draw Reward ID: %d, Order ID: %d\n", dl.RewardID, dl.OrderID) - - // Check Inventory Count for this Order (Should be > 1 if doubled) - var inventoryCount int64 - db.Table("user_inventory").Where("order_id = ?", dl.OrderID).Count(&inventoryCount) - fmt.Printf("Total Inventory Items for Order %d: %d\n", dl.OrderID, inventoryCount) - - if inventoryCount > 1 { - fmt.Println("SUCCESS: Double reward applied (Inventory count > 1)") - } else { - fmt.Println("WARNING: Inventory count is 1. Double reward might NOT have been applied.") - } - } - } else { - fmt.Println("FAILURE: Card status is still 1 (Not Consumed).") - } -} diff --git a/cmd/test_notify/main.go b/cmd/test_notify/main.go deleted file mode 100644 index 317f20a..0000000 --- a/cmd/test_notify/main.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "context" - "fmt" - "time" - - "bindbox-game/configs" - "bindbox-game/internal/pkg/notify" - - gormmysql "gorm.io/driver/mysql" - "gorm.io/gorm" - gormlogger "gorm.io/gorm/logger" -) - -func main() { - // 配置会在 init 时自动加载 - c := configs.Get() - - fmt.Printf("========== 微信通知配置检查 ==========\n") - fmt.Printf("静态配置 (configs):\n") - fmt.Printf(" AppID: %s\n", maskStr(c.Wechat.AppID)) - fmt.Printf(" AppSecret: %s\n", maskStr(c.Wechat.AppSecret)) - fmt.Printf(" LotteryResultTemplateID: %s\n", c.Wechat.LotteryResultTemplateID) - - // 连接数据库检查 system_configs - dsn := "root:bindbox2025kdy@tcp(106.54.232.2:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" - db, err := gorm.Open(gormmysql.Open(dsn), &gorm.Config{Logger: gormlogger.Default.LogMode(gormlogger.Silent)}) - if err != nil { - panic("failed to connect database: " + err.Error()) - } - - // 检查 system_configs 中的模板 ID - type SystemConfig struct { - ConfigKey string - ConfigValue string - } - var cfg SystemConfig - err = db.Table("system_configs").Where("config_key = ?", "wechat.lottery_result_template_id").First(&cfg).Error - if err == nil { - fmt.Printf("\n动态配置 (system_configs):\n") - fmt.Printf(" wechat.lottery_result_template_id: %s\n", cfg.ConfigValue) - } else { - fmt.Printf("\n动态配置 (system_configs): 未配置 wechat.lottery_result_template_id\n") - fmt.Println("将使用静态配置的模板 ID") - } - - // 确定要使用的模板 ID - templateID := c.Wechat.LotteryResultTemplateID - if cfg.ConfigValue != "" { - templateID = cfg.ConfigValue - } - - if templateID == "" { - fmt.Println("\n❌ LotteryResultTemplateID 未配置!") - return - } - fmt.Printf("\n使用的模板 ID: %s\n", templateID) - - // 获取一个有 openid 的用户进行测试 - type User struct { - ID int64 - Openid string - } - var user User - if err := db.Table("users").Where("openid != ''").First(&user).Error; err != nil { - fmt.Printf("\n❌ 没有找到有 openid 的用户: %v\n", err) - return - } - fmt.Printf("测试用户: ID=%d, Openid=%s\n", user.ID, maskStr(user.Openid)) - - // 尝试发送通知 - fmt.Println("\n========== 发送测试通知 ==========") - notifyCfg := ¬ify.WechatNotifyConfig{ - AppID: c.Wechat.AppID, - AppSecret: c.Wechat.AppSecret, - LotteryResultTemplateID: templateID, - } - - err = notify.SendLotteryResultNotification( - context.Background(), - notifyCfg, - user.Openid, - "测试活动名称", - []string{"测试奖品A", "测试奖品B"}, - "TEST_ORDER_001", - time.Now(), - ) - - if err != nil { - fmt.Printf("\n❌ 发送失败: %v\n", err) - } else { - fmt.Println("\n✅ 发送成功!请检查微信是否收到通知。") - } -} - -func maskStr(s string) string { - if len(s) <= 8 { - return s - } - return s[:4] + "****" + s[len(s)-4:] -} diff --git a/cmd/tools/task_center_test/integration.go b/cmd/tools/task_center_test/integration.go deleted file mode 100644 index 8918c02..0000000 --- a/cmd/tools/task_center_test/integration.go +++ /dev/null @@ -1,263 +0,0 @@ -package main - -import ( - "context" - "fmt" - "time" - - "bindbox-game/configs" - "bindbox-game/internal/pkg/logger" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/dao" - "bindbox-game/internal/repository/mysql/model" - tcmodel "bindbox-game/internal/repository/mysql/task_center" - tasksvc "bindbox-game/internal/service/task_center" - "bindbox-game/internal/service/title" - "bindbox-game/internal/service/user" - - "github.com/redis/go-redis/v9" -) - -// IntegrationTest 运行集成测试流 -func IntegrationTest(repo mysql.Repo) error { - ctx := context.Background() - cfg := configs.Get() - - // 1. 初始化日志(自定义) - l, err := logger.NewCustomLogger(dao.Use(repo.GetDbW())) - if err != nil { - return fmt.Errorf("初始化日志失败: %v", err) - } - - // 2. 初始化 Redis - rdb := redis.NewClient(&redis.Options{ - Addr: cfg.Redis.Addr, - Password: cfg.Redis.Pass, - DB: cfg.Redis.DB, - }) - if err := rdb.Ping(ctx).Err(); err != nil { - return fmt.Errorf("连接 Redis 失败: %v", err) - } - - // 3. 初始化依赖服务 - userSvc := user.New(l, repo) - titleSvc := title.New(l, repo) - taskSvc := tasksvc.New(l, repo, rdb, userSvc, titleSvc) - - // 3.5 清理缓存以确保能加载最新配置 - if err := rdb.Del(ctx, "task_center:active_tasks").Err(); err != nil { - fmt.Printf("⚠️ 清理缓存失败: %v\n", err) - } - - // 4. 选择一个测试用户和任务 - // ... (代码逻辑不变) - userID := int64(8888) - - // 搜索一个首单任务(满足 lifetime 窗口,奖励为点数) - var task tcmodel.Task - db := repo.GetDbW() - if err := db.Joins("JOIN task_center_task_tiers ON task_center_task_tiers.task_id = task_center_tasks.id"). - Joins("JOIN task_center_task_rewards ON task_center_task_rewards.task_id = task_center_tasks.id"). - Where("task_center_task_tiers.metric = ? AND task_center_task_tiers.window = ? AND task_center_task_rewards.reward_type = ?", "first_order", "lifetime", "points"). - First(&task).Error; err != nil { - return fmt.Errorf("未找到符合条件的集成测试任务: %v", err) - } - - fmt.Printf("--- 开始集成测试 ---\n") - fmt.Printf("用户ID: %d, 任务ID: %d (%s)\n", userID, task.ID, task.Name) - - // 5. 创建一个模拟订单 - orderNo := fmt.Sprintf("TEST_ORDER_%d", time.Now().Unix()) - order := &model.Orders{ - UserID: userID, - OrderNo: orderNo, - TotalAmount: 100, - ActualAmount: 100, - Status: 2, // 已支付 - PaidAt: time.Now(), - } - if err := db.Omit("cancelled_at").Create(order).Error; err != nil { - return fmt.Errorf("创建测试订单失败: %v", err) - } - fmt.Printf("创建测试订单: %s (ID: %d)\n", orderNo, order.ID) - - // 6. 触发 OnOrderPaid - fmt.Println("触发 OnOrderPaid 事件...") - if err := taskSvc.OnOrderPaid(ctx, userID, order.ID); err != nil { - return fmt.Errorf("OnOrderPaid 失败: %v", err) - } - - // 7. 验证结果 - // A. 检查进度是否更新 - var progress tcmodel.UserTaskProgress - if err := db.Where("user_id = ? AND task_id = ?", userID, task.ID).First(&progress).Error; err != nil { - fmt.Printf("❌ 进度记录未找到: %v\n", err) - } else { - fmt.Printf("✅ 进度记录已更新: first_order=%d\n", progress.FirstOrder) - } - - // B. 检查奖励日志 - time.Sleep(1 * time.Second) - - var eventLog tcmodel.TaskEventLog - if err := db.Where("user_id = ? AND task_id = ?", userID, task.ID).Order("id desc").First(&eventLog).Error; err != nil { - fmt.Printf("❌ 奖励日志未找到: %v\n", err) - } else { - fmt.Printf("✅ 奖励日志已找到: Status=%s, Result=%s\n", eventLog.Status, eventLog.Result) - if eventLog.Status == "granted" { - fmt.Printf("🎉 集成测试通过!奖励已成功发放。\n") - } else { - fmt.Printf("⚠️ 奖励发放状态异常: %s\n", eventLog.Status) - } - } - - return nil -} - -// InviteAndTaskIntegrationTest 运行邀请与任务全链路集成测试 -func InviteAndTaskIntegrationTest(repo mysql.Repo) error { - ctx := context.Background() - cfg := configs.Get() - db := repo.GetDbW() - - // 1. 初始化 - l, _ := logger.NewCustomLogger(dao.Use(db)) - rdb := redis.NewClient(&redis.Options{Addr: cfg.Redis.Addr, Password: cfg.Redis.Pass, DB: cfg.Redis.DB}) - userSvc := user.New(l, repo) - titleSvc := title.New(l, repo) - taskSvc := tasksvc.New(l, repo, rdb, userSvc, titleSvc) - - // 2. 准备角色 - inviterID := int64(9001) - inviteeID := int64(9002) - _ = ensureUserExists(repo, inviterID, "老司机(邀请者)") - _ = ensureUserExists(repo, inviteeID, "萌新(被邀请者)") - - // 3. 建立邀请关系 - if err := ensureInviteRelationship(repo, inviterID, inviteeID); err != nil { - return fmt.Errorf("建立邀请关系失败: %v", err) - } - - // 4. 清理 Redis 缓存 - _ = rdb.Del(ctx, "task_center:active_tasks").Err() - - // 5. 查找测试任务 - var inviteTask tcmodel.Task - if err := db.Joins("JOIN task_center_task_tiers ON task_center_task_tiers.task_id = task_center_tasks.id"). - Where("task_center_task_tiers.metric = ? AND task_center_task_tiers.window = ?", "invite_count", "lifetime"). - First(&inviteTask).Error; err != nil { - return fmt.Errorf("未找到邀请任务: %v", err) - } - - var firstOrderTask tcmodel.Task - if err := db.Joins("JOIN task_center_task_tiers ON task_center_task_tiers.task_id = task_center_tasks.id"). - Where("task_center_task_tiers.metric = ? AND task_center_task_tiers.window = ?", "first_order", "lifetime"). - First(&firstOrderTask).Error; err != nil { - return fmt.Errorf("未找到首单任务: %v", err) - } - - fmt.Printf("--- 开始邀请全链路测试 ---\n") - fmt.Printf("邀请人: %d, 被邀请人: %d\n", inviterID, inviteeID) - - // 6. 模拟邀请成功事件 (触发两次以确保达到默认阈值 2) - fmt.Println("触发 OnInviteSuccess 事件 (第1次)...") - if err := taskSvc.OnInviteSuccess(ctx, inviterID, inviteeID); err != nil { - return fmt.Errorf("OnInviteSuccess 失败: %v", err) - } - fmt.Println("触发 OnInviteSuccess 事件 (第2次, 换个用户ID)...") - if err := taskSvc.OnInviteSuccess(ctx, inviterID, 9999); err != nil { - return fmt.Errorf("OnInviteSuccess 失败: %v", err) - } - - // 7. 模拟被邀请者下单 - orderNo := fmt.Sprintf("INVITE_ORDER_%d", time.Now().Unix()) - order := &model.Orders{ - UserID: inviteeID, - OrderNo: orderNo, - TotalAmount: 100, - ActualAmount: 100, - Status: 2, // 已支付 - PaidAt: time.Now(), - } - if err := db.Omit("cancelled_at").Create(order).Error; err != nil { - return fmt.Errorf("创建被邀请者订单失败: %v", err) - } - fmt.Printf("被邀请者下单成功: %s (ID: %d)\n", orderNo, order.ID) - - fmt.Println("触发 OnOrderPaid 事件 (被邀请者)...") - if err := taskSvc.OnOrderPaid(ctx, inviteeID, order.ID); err != nil { - return fmt.Errorf("OnOrderPaid 失败: %v", err) - } - - // 8. 验证 - time.Sleep(1 * time.Second) - - fmt.Println("\n--- 数据库进度核查 ---") - var allProgress []tcmodel.UserTaskProgress - db.Where("user_id IN (?)", []int64{inviterID, inviteeID}).Find(&allProgress) - if len(allProgress) == 0 { - fmt.Println("⚠️ 数据库中未找到任何进度记录!") - } - for _, p := range allProgress { - userLabel := "邀请人" - if p.UserID == inviteeID { - userLabel = "被邀请人" - } - fmt.Printf("[%s] 用户:%d 任务:%d | Invite=%d, OrderCount=%d, FirstOrder=%d\n", - userLabel, p.UserID, p.TaskID, p.InviteCount, p.OrderCount, p.FirstOrder) - } - - fmt.Println("\n--- 奖励发放核查 ---") - var logs []tcmodel.TaskEventLog - db.Where("user_id IN (?) AND status = ?", []int64{inviterID, inviteeID}, "granted").Find(&logs) - fmt.Printf("✅ 累计发放奖励次数: %d\n", len(logs)) - for _, l := range logs { - fmt.Printf(" - 用户 %d 触发任务 %d 奖励 | Source:%s\n", l.UserID, l.TaskID, l.SourceType) - } - - if len(logs) >= 2 { - fmt.Println("\n🎉 邀请全链路集成测试通过!邀请人和被邀请人都获得了奖励。") - } else { - fmt.Printf("\n⚠️ 测试部分完成,奖励次数(%d)少于预期(2)\n", len(logs)) - } - return nil -} - -// 模拟创建用户的方法(如果不存在) -func ensureUserExists(repo mysql.Repo, userID int64, nickname string) error { - db := repo.GetDbW() - var user model.Users - if err := db.Where("id = ?", userID).First(&user).Error; err != nil { - user = model.Users{ - ID: userID, - Nickname: nickname, - Avatar: "http://example.com/a.png", - Status: 1, - InviteCode: fmt.Sprintf("CODE%d", userID), - } - if err := db.Create(&user).Error; err != nil { - return err - } - fmt.Printf("已确保测试用户存在: %d (%s)\n", userID, nickname) - } - return nil -} - -// 建立邀请关系 -func ensureInviteRelationship(repo mysql.Repo, inviterID, inviteeID int64) error { - db := repo.GetDbW() - var rel model.UserInvites - if err := db.Where("invitee_id = ?", inviteeID).First(&rel).Error; err != nil { - rel = model.UserInvites{ - InviterID: inviterID, - InviteeID: inviteeID, - InviteCode: fmt.Sprintf("CODE%d", inviterID), - } - return db.Omit("rewarded_at").Create(&rel).Error - } - // 如果已存在但邀请人不对,修正它 - if rel.InviterID != inviterID { - return db.Model(&rel).Update("inviter_id", inviterID).Error - } - return nil -} diff --git a/cmd/tools/task_center_test/main.go b/cmd/tools/task_center_test/main.go deleted file mode 100644 index 69eecf3..0000000 --- a/cmd/tools/task_center_test/main.go +++ /dev/null @@ -1,477 +0,0 @@ -// 任务中心配置组合测试工具 -// 功能: -// 1. 生成所有有效的任务配置组合到 MySQL 数据库 -// 2. 模拟用户任务进度 -// 3. 验证任务功能是否正常 - -package main - -import ( - "encoding/json" - "flag" - "fmt" - "log" - "os" - "time" - - "bindbox-game/configs" - "bindbox-game/internal/repository/mysql" - tcmodel "bindbox-game/internal/repository/mysql/task_center" - - "gorm.io/datatypes" -) - -// ================================ -// 常量定义 -// ================================ - -const ( - // 任务指标 - MetricFirstOrder = "first_order" - MetricOrderCount = "order_count" - MetricOrderAmount = "order_amount" - MetricInviteCount = "invite_count" - - // 操作符 - OperatorGTE = ">=" - OperatorEQ = "=" - - // 时间窗口 - WindowDaily = "daily" - WindowWeekly = "weekly" - WindowMonthly = "monthly" - WindowLifetime = "lifetime" - - // 奖励类型 - RewardTypePoints = "points" - RewardTypeCoupon = "coupon" - RewardTypeItemCard = "item_card" - RewardTypeTitle = "title" - RewardTypeGameTicket = "game_ticket" -) - -// TaskCombination 表示一种任务配置组合 -type TaskCombination struct { - Name string - Metric string - Operator string - Threshold int64 - Window string - RewardType string -} - -// TestResult 测试结果 -type TestResult struct { - Name string - Passed bool - Message string -} - -// ================================ -// 配置组合生成器 -// ================================ - -// GenerateAllCombinations 生成所有有效的任务配置组合 -func GenerateAllCombinations() []TaskCombination { - metrics := []struct { - name string - operators []string - threshold int64 - }{ - {MetricFirstOrder, []string{OperatorEQ}, 1}, - {MetricOrderCount, []string{OperatorGTE, OperatorEQ}, 3}, - {MetricOrderAmount, []string{OperatorGTE, OperatorEQ}, 10000}, - {MetricInviteCount, []string{OperatorGTE, OperatorEQ}, 2}, - } - windows := []string{WindowDaily, WindowWeekly, WindowMonthly, WindowLifetime} - rewards := []string{RewardTypePoints, RewardTypeCoupon, RewardTypeItemCard, RewardTypeTitle, RewardTypeGameTicket} - - var combinations []TaskCombination - idx := 0 - for _, m := range metrics { - for _, op := range m.operators { - for _, w := range windows { - for _, r := range rewards { - idx++ - combinations = append(combinations, TaskCombination{ - Name: fmt.Sprintf("测试任务%03d_%s_%s_%s", idx, m.name, w, r), - Metric: m.name, - Operator: op, - Threshold: m.threshold, - Window: w, - RewardType: r, - }) - } - } - } - } - return combinations -} - -// generateRewardPayload 根据奖励类型生成对应的 JSON payload -func generateRewardPayload(rewardType string) string { - switch rewardType { - case RewardTypePoints: - return `{"points": 100}` - case RewardTypeCoupon: - return `{"coupon_id": 1, "quantity": 1}` - case RewardTypeItemCard: - return `{"card_id": 1, "quantity": 1}` - case RewardTypeTitle: - return `{"title_id": 1}` - case RewardTypeGameTicket: - return `{"game_code": "minesweeper", "amount": 5}` - default: - return `{}` - } -} - -// ================================ -// 数据库操作 -// ================================ - -// SeedAllCombinations 将所有配置组合写入数据库 -func SeedAllCombinations(repo mysql.Repo, dryRun bool) error { - db := repo.GetDbW() - combos := GenerateAllCombinations() - - fmt.Printf("准备生成 %d 个任务配置组合\n", len(combos)) - if dryRun { - fmt.Println("【试运行模式】不会实际写入数据库") - for i, c := range combos { - fmt.Printf(" %3d. %s (指标=%s, 操作符=%s, 窗口=%s, 奖励=%s)\n", - i+1, c.Name, c.Metric, c.Operator, c.Window, c.RewardType) - } - return nil - } - - // 开始事务 - tx := db.Begin() - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - // 清理旧的测试数据 - if err := tx.Where("name LIKE ?", "测试任务%").Delete(&tcmodel.Task{}).Error; err != nil { - tx.Rollback() - return fmt.Errorf("清理旧任务失败: %v", err) - } - fmt.Println("已清理旧的测试任务数据") - - created := 0 - for _, combo := range combos { - // 检查是否已存在 - var exists tcmodel.Task - if err := tx.Where("name = ?", combo.Name).First(&exists).Error; err == nil { - fmt.Printf(" 跳过: %s (已存在)\n", combo.Name) - continue - } - - // 插入任务 - task := &tcmodel.Task{ - Name: combo.Name, - Description: fmt.Sprintf("测试 %s + %s + %s + %s", combo.Metric, combo.Operator, combo.Window, combo.RewardType), - Status: 1, - Visibility: 1, - } - if err := tx.Create(task).Error; err != nil { - tx.Rollback() - return fmt.Errorf("插入任务失败: %v", err) - } - - // 插入档位 - tier := &tcmodel.TaskTier{ - TaskID: task.ID, - Metric: combo.Metric, - Operator: combo.Operator, - Threshold: combo.Threshold, - Window: combo.Window, - Priority: 0, - } - if err := tx.Create(tier).Error; err != nil { - tx.Rollback() - return fmt.Errorf("插入档位失败: %v", err) - } - - // 插入奖励 - payload := generateRewardPayload(combo.RewardType) - reward := &tcmodel.TaskReward{ - TaskID: task.ID, - TierID: tier.ID, - RewardType: combo.RewardType, - RewardPayload: datatypes.JSON(payload), - Quantity: 10, - } - if err := tx.Create(reward).Error; err != nil { - tx.Rollback() - return fmt.Errorf("插入奖励失败: %v", err) - } - - created++ - if created%10 == 0 { - fmt.Printf(" 已创建 %d 个任务...\n", created) - } - } - - if err := tx.Commit().Error; err != nil { - return fmt.Errorf("提交事务失败: %v", err) - } - - fmt.Printf("✅ 成功创建 %d 个任务配置组合\n", created) - return nil -} - -// ================================ -// 模拟用户任务 -// ================================ - -// SimulateUserTask 模拟用户完成任务 -func SimulateUserTask(repo mysql.Repo, userID int64, taskID int64) error { - db := repo.GetDbW() - - // 查询任务和档位 - var task tcmodel.Task - if err := db.Where("id = ?", taskID).First(&task).Error; err != nil { - return fmt.Errorf("任务不存在: %v", err) - } - - var tier tcmodel.TaskTier - if err := db.Where("task_id = ?", taskID).First(&tier).Error; err != nil { - return fmt.Errorf("档位不存在: %v", err) - } - - fmt.Printf("模拟任务: %s (指标=%s, 阈值=%d)\n", task.Name, tier.Metric, tier.Threshold) - - // 创建或更新用户进度 - progress := &tcmodel.UserTaskProgress{ - UserID: userID, - TaskID: taskID, - ClaimedTiers: datatypes.JSON("[]"), - } - - // 根据指标类型设置进度 - switch tier.Metric { - case MetricFirstOrder: - progress.FirstOrder = 1 - progress.OrderCount = 1 - progress.OrderAmount = 10000 - case MetricOrderCount: - progress.OrderCount = tier.Threshold - case MetricOrderAmount: - progress.OrderAmount = tier.Threshold - progress.OrderCount = 1 - case MetricInviteCount: - progress.InviteCount = tier.Threshold - } - - // Upsert - if err := db.Where("user_id = ? AND task_id = ?", userID, taskID). - Assign(progress). - FirstOrCreate(progress).Error; err != nil { - return fmt.Errorf("创建进度失败: %v", err) - } - - fmt.Printf("✅ 用户 %d 的任务进度已更新: order_count=%d, order_amount=%d, invite_count=%d, first_order=%d\n", - userID, progress.OrderCount, progress.OrderAmount, progress.InviteCount, progress.FirstOrder) - - return nil -} - -// ================================ -// 验证功能 -// ================================ - -// VerifyAllConfigs 验证所有配置是否正确 -func VerifyAllConfigs(repo mysql.Repo) []TestResult { - db := repo.GetDbR() - var results []TestResult - - // 1. 检查任务数量 - var taskCount int64 - var sampleTasks []tcmodel.Task - db.Model(&tcmodel.Task{}).Where("name LIKE ?", "测试任务%").Count(&taskCount) - db.Model(&tcmodel.Task{}).Where("name LIKE ?", "测试任务%").Limit(5).Find(&sampleTasks) - - var sampleMsg string - for _, t := range sampleTasks { - sampleMsg += fmt.Sprintf("[%d:%s] ", t.ID, t.Name) - } - - results = append(results, TestResult{ - Name: "任务数量检查", - Passed: taskCount > 0, - Message: fmt.Sprintf("找到 %d 个测试任务. 样本: %s", taskCount, sampleMsg), - }) - - // 2. 检查每种指标的覆盖 - metrics := []string{MetricFirstOrder, MetricOrderCount, MetricOrderAmount, MetricInviteCount} - for _, m := range metrics { - var count int64 - db.Model(&tcmodel.TaskTier{}).Where("metric = ?", m).Count(&count) - results = append(results, TestResult{ - Name: fmt.Sprintf("指标覆盖: %s", m), - Passed: count > 0, - Message: fmt.Sprintf("找到 %d 个档位使用此指标", count), - }) - } - - // 3. 检查每种时间窗口的覆盖 - windows := []string{WindowDaily, WindowWeekly, WindowMonthly, WindowLifetime} - for _, w := range windows { - var count int64 - db.Model(&tcmodel.TaskTier{}).Where("window = ?", w).Count(&count) - results = append(results, TestResult{ - Name: fmt.Sprintf("时间窗口覆盖: %s", w), - Passed: count > 0, - Message: fmt.Sprintf("找到 %d 个档位使用此时间窗口", count), - }) - } - - // 4. 检查每种奖励类型的覆盖 - rewards := []string{RewardTypePoints, RewardTypeCoupon, RewardTypeItemCard, RewardTypeTitle, RewardTypeGameTicket} - for _, r := range rewards { - var count int64 - db.Model(&tcmodel.TaskReward{}).Where("reward_type = ?", r).Count(&count) - results = append(results, TestResult{ - Name: fmt.Sprintf("奖励类型覆盖: %s", r), - Passed: count > 0, - Message: fmt.Sprintf("找到 %d 个奖励使用此类型", count), - }) - } - - // 5. 检查奖励 payload 格式 - var rewardList []tcmodel.TaskReward - db.Limit(20).Find(&rewardList) - for _, r := range rewardList { - var data map[string]interface{} - err := json.Unmarshal([]byte(r.RewardPayload), &data) - passed := err == nil - msg := "JSON 格式正确" - if err != nil { - msg = fmt.Sprintf("JSON 解析失败: %v", err) - } - results = append(results, TestResult{ - Name: fmt.Sprintf("奖励Payload格式: ID=%d, Type=%s", r.ID, r.RewardType), - Passed: passed, - Message: msg, - }) - } - - return results -} - -// PrintResults 打印测试结果 -func PrintResults(results []TestResult) { - passed := 0 - failed := 0 - - fmt.Println("\n========== 测试结果 ==========") - for _, r := range results { - status := "✅ PASS" - if !r.Passed { - status = "❌ FAIL" - failed++ - } else { - passed++ - } - fmt.Printf("%s | %s | %s\n", status, r.Name, r.Message) - } - fmt.Println("==============================") - fmt.Printf("总计: %d 通过, %d 失败\n", passed, failed) -} - -// ================================ -// 主程序 -// ================================ - -func main() { - // 命令行参数 - action := flag.String("action", "help", "操作类型: seed/simulate/verify/integration/invite-test/help") - dryRun := flag.Bool("dry-run", false, "试运行模式,不实际写入数据库") - userID := flag.Int64("user", 8888, "用户ID (用于 simulate 或 integration)") - taskID := flag.Int64("task", 0, "任务ID") - flag.Parse() - - // 显示帮助 - if *action == "help" { - fmt.Println(` -任务中心配置组合测试工具 - -用法: - go run main.go -action=<操作> - -操作类型: - seed - 生成所有配置组合到数据库 - simulate - 简单模拟用户进度 (仅修改进度表) - integration - 真实集成测试 (触发 OnOrderPaid, 验证全流程) - invite-test - 邀请全链路测试 (模拟邀请、下单、双端奖励发放) - verify - 验证配置是否正确 - -参数: - -dry-run - 试运行模式,不实际写入数据库 - -user - 用户ID (默认: 8888) - -task - 任务ID - -示例: - # 邀请全链路测试 - go run main.go -action=invite-test -`) - return - } - - // 初始化数据库连接 - repo, err := mysql.New() - if err != nil { - log.Fatalf("连接数据库失败: %v", err) - } - - cfg := configs.Get() - fmt.Printf("已连接到数据库: %s\n", cfg.MySQL.Write.Name) - fmt.Printf("时间: %s\n", time.Now().Format("2006-01-02 15:04:05")) - - // 执行操作 - switch *action { - case "seed": - if err := SeedAllCombinations(repo, *dryRun); err != nil { - log.Printf("生成配置失败: %v", err) - os.Exit(1) - } - - case "simulate": - if *taskID == 0 { - fmt.Println("请指定任务ID: -task=") - os.Exit(1) - } - if err := SimulateUserTask(repo, *userID, *taskID); err != nil { - log.Printf("模拟失败: %v", err) - os.Exit(1) - } - - case "integration": - // 确保用户存在 - if err := ensureUserExists(repo, *userID, "测试用户"); err != nil { - log.Printf("预检用户失败: %v", err) - os.Exit(1) - } - if err := IntegrationTest(repo); err != nil { - log.Printf("集成测试失败: %v", err) - os.Exit(1) - } - - case "invite-test": - if err := InviteAndTaskIntegrationTest(repo); err != nil { - log.Printf("邀请测试失败: %v", err) - os.Exit(1) - } - - case "verify": - results := VerifyAllConfigs(repo) - PrintResults(results) - - default: - fmt.Printf("未知操作: %s\n", *action) - os.Exit(1) - } -} diff --git a/cmd/trace_ledger_cloud/main.go b/cmd/trace_ledger_cloud/main.go deleted file mode 100644 index ff3ff05..0000000 --- a/cmd/trace_ledger_cloud/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -const ( - DbDSN = "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=True&loc=Local" -) - -func main() { - db, err := gorm.Open(mysql.Open(DbDSN), &gorm.Config{}) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - - fmt.Println("--- User Coupon Ledger Structure (Cloud DB) ---") - var columns []struct { - Field string - Type string - Null string - Key string - Default *string - Extra string - } - db.Raw("DESC user_coupon_ledger").Scan(&columns) - for _, col := range columns { - fmt.Printf("%-15s %-15s %-5s %-5s\n", col.Field, col.Type, col.Null, col.Key) - } - - fmt.Println("\n--- User 9090 Coupon 260 Trace (Cloud DB) ---") - var results []map[string]interface{} - db.Raw("SELECT id, user_id, user_coupon_id, change_amount, balance_after, order_id, action, created_at FROM user_coupon_ledger WHERE user_coupon_id = 260 ORDER BY id ASC").Scan(&results) - for _, res := range results { - fmt.Printf("ID: %v, Action: %v, Change: %v, Bal: %v, Order: %v, Time: %v\n", - res["id"], res["action"], res["change_amount"], res["balance_after"], res["order_id"], res["created_at"]) - } -} diff --git a/cmd/verify_coupon_fix/main.go b/cmd/verify_coupon_fix/main.go deleted file mode 100644 index e7e5b34..0000000 --- a/cmd/verify_coupon_fix/main.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" - - "bindbox-game/configs" - "bindbox-game/internal/pkg/logger" - "bindbox-game/internal/repository/mysql" - "bindbox-game/internal/repository/mysql/dao" - usersvc "bindbox-game/internal/service/user" - "context" - "flag" -) - -func main() { - flag.Parse() - // 1. Initialize Configs (Requires being in the project root or having CONFIG_FILE env) - configs.Init() - - // 2. Initialize Real MySQL Repo - dbRepo, err := mysql.New() - if err != nil { - log.Fatalf("MySQL init failed: %v", err) - } - - // 3. Initialize Logger - d := dao.Use(dbRepo.GetDbW()) - l, err := logger.NewCustomLogger(d, logger.WithOutputInConsole()) - if err != nil { - log.Fatalf("Logger creation failed: %v", err) - } - - // 4. Initialize User Service - us := usersvc.New(l, dbRepo) - - userID := int64(9090) - orderID := int64(4695) // The order that already had double deduction - ctx := context.Background() - - fmt.Printf("--- Verifying Idempotency for Order %d ---\n", orderID) - - // Using raw DB for checking counts - db := dbRepo.GetDbR() - - // Count existing ledger records - var beforeCount int64 - db.Table("user_coupon_ledger").Where("order_id = ? AND action = 'apply'", orderID).Count(&beforeCount) - fmt.Printf("Before call: %d ledger records\n", beforeCount) - - // Call DeductCouponsForPaidOrder - fmt.Println("Calling DeductCouponsForPaidOrder...") - // Pass nil as tx, the service will use dbRepo.GetDbW() - err = us.DeductCouponsForPaidOrder(ctx, nil, userID, orderID, time.Now()) - if err != nil { - fmt.Printf("Error during DeductCouponsForPaidOrder: %v\n", err) - } - - // Count after ledger records - var afterCount int64 - db.Table("user_coupon_ledger").Where("order_id = ? AND action = 'apply'", orderID).Count(&afterCount) - fmt.Printf("After call: %d ledger records\n", afterCount) - - if beforeCount == afterCount { - fmt.Println("\nSUCCESS: Fix is idempotent! No new records added.") - } else { - fmt.Println("\nFAILURE: Still adding duplicate records.") - } -} diff --git a/docs/bugfix_task_center_activity_profit/ALIGNMENT_bugfix.md b/docs/bugfix_task_center_activity_profit/ALIGNMENT_bugfix.md deleted file mode 100644 index 58741a2..0000000 --- a/docs/bugfix_task_center_activity_profit/ALIGNMENT_bugfix.md +++ /dev/null @@ -1,100 +0,0 @@ -# BUG修复需求分析 - -## 任务概述 - -修复盲盒游戏系统中6个BUG问题。 - -## BUG清单 - -### BUG 1: 任务中心任务类型统计错误 -**问题描述**: 设置任务是完成A活动才可以算完成,但玩了一局B活动竟然也算任务成功了。 - -**根因分析**: -- 任务中心 `GetUserProgress` 函数 (`internal/service/task_center/service.go:290-386`) -- 该函数通过订单 `remark` 字段使用 LIKE 匹配来过滤活动ID -- 匹配模式: `%%activity:%d%%` -- **问题**: 虽然有活动ID过滤逻辑,但需要确认任务配置时是否正确设置了 `activity_id` -- 相关代码位置: `service.go` 第306-312行 - ---- - -### BUG 2: 任务中心把商城订单也计入了 -**问题描述**: 任务中心统计时不应该包含商城订单,应该根据设置的类型来结算。 - -**根因分析**: -- `GetUserProgress` 函数统计订单时只过滤了 `status = 2`(已支付) -- **问题**: 没有过滤 `source_type`,导致商城订单(`source_type = 1`)也被计入 -- 订单 `source_type` 定义 (`model/orders.gen.go:20`): - - 1: 商城直购 - - 2: 抽奖票据 - - 3: 其他 - - 4: 次数卡支付 - ---- - -### BUG 3: 活动盈亏仪表盘退款订单未排除 -**问题描述**: 对用户订单进行退款了,统计不应该把这个订单累计进来。 - -**根因分析**: -- `DashboardActivityProfitLoss` 函数 (`internal/api/admin/dashboard_activity.go:132-139`) -- 营收统计查询条件: `orders.status = 2`(已支付) -- **问题**: 订单状态4表示已退款,但当前只过滤了 `status = 2`,不会包含退款订单 -- **实际问题**: 退款后订单状态应该从2变成4,但如果状态未更新则会被统计。需要确认退款流程是否正确更新订单状态 - ---- - -### BUG 4: 活动盈亏抽奖记录缺少字段 -**问题描述**: 需要在抽奖记录中体现 优惠券 / 道具卡 / 次数卡 字段。 - -**根因分析**: -- `DashboardActivityLogs` 函数 (`internal/api/admin/dashboard_activity.go:222-354`) -- 当前返回字段已包含: - - `coupon_name`: 通过 `orders.coupon_id` LEFT JOIN `system_coupons` - - `item_card_name`: 通过 `orders.item_card_id` LEFT JOIN `system_item_cards` -- **问题**: `source_type = 4` 表示次数卡支付,但次数卡使用信息存储在订单 `remark` 字段中(格式: `gp_use:ID:Count`),当前未解析显示 -- 需要增加解析 `remark` 字段中的次数卡使用信息 - ---- - -### BUG 5: 一番赏不能使用优惠券 -**问题描述**: 一番赏目前不能使用优惠券。 - -**根因分析**: -- `JoinLottery` 函数 (`internal/api/activity/lottery_app.go:78-81`) -- 优惠券检查逻辑: `if !activity.AllowCoupons && req.CouponID != nil` -- **问题**: 一番赏活动的 `AllowCoupons` 字段可能被设置为 `false` -- 数据库字段定义 (`model/activities.gen.go:27`): `AllowCoupons bool` 默认值为1(允许) -- **解决方向**: - 1. 检查一番赏活动在数据库中的 `allow_coupons` 字段值 - 2. 如果业务上确实不允许,则是配置问题而非代码问题 - 3. 如果业务上应该允许,需修改活动配置 - ---- - -### BUG 6: 活动盈亏出现已下架活动数据 -**问题描述**: 活动盈亏里面出现了以前已经下架了的数据,应该按照现在活动表存在的活动来统计。 - -**根因分析**: -- `DashboardActivityProfitLoss` 函数 (`internal/api/admin/dashboard_activity.go:58-75`) -- 当前查询直接从 `activities` 表获取活动列表 -- 支持按 `status` 过滤(1进行中 2下线) -- **问题**: 虽然支持状态过滤,但默认不过滤任何状态 -- 另外活动表使用了软删除 (`deleted_at`),但需确认是否正确应用了软删除条件 - -## 需求理解 - -| BUG编号 | 问题类型 | 修复难度 | 涉及文件 | -|---------|----------|----------|----------| -| BUG 1 | 业务逻辑 | 中 | `service/task_center/service.go` | -| BUG 2 | 业务逻辑 | 低 | `service/task_center/service.go` | -| BUG 3 | 数据过滤 | 低 | `api/admin/dashboard_activity.go` | -| BUG 4 | 字段缺失 | 低 | `api/admin/dashboard_activity.go` | -| BUG 5 | 配置问题 | 低 | 需检查数据库配置 | -| BUG 6 | 数据过滤 | 低 | `api/admin/dashboard_activity.go` | - -## 待确认问题 - -1. **BUG 1**: 任务配置时,`task_center_task_tiers.activity_id` 字段是否正确设置? -2. **BUG 3**: 退款时订单状态是否正确更新为4? -3. **BUG 5**: 一番赏活动的 `allow_coupons` 数据库字段当前值是什么?是配置问题还是需要代码修复? -4. **BUG 6**: 是否需要默认只显示在线活动(status=1)?还是只过滤软删除的活动? diff --git a/docs/loki_integration/ALIGNMENT_loki_integration.md b/docs/loki_integration/ALIGNMENT_loki_integration.md deleted file mode 100644 index 03acfd9..0000000 --- a/docs/loki_integration/ALIGNMENT_loki_integration.md +++ /dev/null @@ -1,47 +0,0 @@ -# 任务:后台Go项目接入 Loki 成本评估与实施方案 (ALIGNMENT) - 性能分析版 - -## 1. 核心问题:性能消耗分析 -用户疑问:**"Loki + Promtail + Grafana 这个组合消耗性能么"** - -### 1.1 结论先行 -在**极简模式**(只采服务端日志)下,**消耗非常低**。 -对于现代开发机(或 2核4G 以上服务器),**几乎无感**。 - -### 1.2 详细资源开销预估 (以每天产生 100MB 日志为例) - -| 组件 | 作用 | 内存 (RAM) | CPU (平时) | CPU (查询时) | 磁盘 IO | 评价 | -| :--- | :--- | :--- | :--- | :--- | :--- | :--- | -| **Promtail** | 搬运工 | ~50MB | < 1% | - | 极低 (仅读取) | **极轻**,就像运行了一个 `tail -f` 命令 | -| **Loki** | 存储 | ~150MB - 300MB | < 2% | 10% - 30% | 低 (顺序写入) | **轻量**,不像 ES 那样建全文索引,只压缩存储,不做繁重计算 | -| **Grafana** | 界面 | ~100MB - 200MB | 0% (Idle) | 5% - 10% | 忽略不计 | **静默**,没人访问网页时几乎不干活 | -| **总计** | **全套** | **约 400MB - 600MB** | **日常忽略不计** | **查询时微升** | **可控** | **安全** | - -### 1.3 为什么这么省? -1. **不建全文索引**: Loki 的设计哲学是 "Log everything, index only labels"。它不像 Elasticsearch (ELK) 那样对每个词都建索引(那样极耗内存和CPU)。Loki 只索引 "时间" 和 "标签" (如 `app=bindbox-game`)。 -2. **流式压缩**: 日志被压缩成块存储,占用很少写 IO。 -3. **按需计算**: 只有当你发起查询(比如搜 "error")时,CPU 才会工作去解压和匹配数据。平时它只是在静静地写文件。 - -### 1.4 极端情况 -- **甚至可以更省**: 我们可以限制 docker 容器的内存上限。 -- **对比 ELK**: ELK 动辄需要 2G-4G 起步内存,Loki 是专为云原生和低资源环境设计的轻量级替代品。 - ---- - -## 2. 极简实施方案 (Server Logs Only) -### 2.1 架构调整 -- **Log Source**: 仅采集 `bindbox-game` 容器。 -- **采集过滤**: 在 Promtail 配置中设置 Filter,**丢弃** 所有非 `bindbox-game` 的日志。 - -### 2.2 资源进一步优化 -- 由于只采集核心服务,Loki 和 Promtail 的 CPU/内存消耗将降至最低。 -- 磁盘占用将非常小。 - ---- - -## 3. 实施步骤 -1. **配置 Promtail**: 编写 `promtail-config.yaml`,只监听 `container_name="bindbox-game"`。 -2. **配置 Docker Compose**: 添加 Loki, Promtail, Grafana 服务。 -3. **配置 Grafana**: 预配置好 Loki 数据源,开箱即用。 - -## 4. 结论 -可以放心接入。这套组合主要消耗的是**几百兆内存**,对 CPU 和磁盘的影响极小,完全适合本地开发和中小型服务器部署。 diff --git a/docs/lottery_algorithm.md b/docs/lottery_algorithm.md deleted file mode 100644 index 248bd8b..0000000 --- a/docs/lottery_algorithm.md +++ /dev/null @@ -1,149 +0,0 @@ -# 抽奖与公平性算法技术白皮书 - -## 1. 概述 - -本系统采用 **「承诺机制 (Commitment Scheme)」** 结合 **HMAC-SHA256** 算法,确保抽奖过程的**不可预测性**、**可验证性**和**不可篡改性**。 - -核心原则: -1. **事前承诺**:活动开始前生成随机种子并公布其哈希值(Commitment)。 -2. **事后验证**:活动结束后公布种子明文(Reveal),用户可复算验证。 -3. **确定性算法**:输入(种子 + 上下文)确定,输出必然唯一。 - ---- - -## 2. 核心机制:承诺方案 - -### 2.1 种子生成 -每个活动 (`Activity`) 在创建或发布时,系统服务器端会生成一个高质量的 32 字节随机种子 (`ServerSeed`)。 - -```go -// 伪代码示例 -seed := make([]byte, 32) -rand.Read(seed) // 使用 crypto/rand 生成强随机数 -``` - -### 2.2 承诺哈希 (Commitment Hash) -在数据库确立活动数据的一瞬间,系统计算种子的 SHA256 哈希值,作为**承诺**存储并对用户可见(虽然前端可能选择性展示)。 - -$$ SeedHash = \text{SHA256}(ServerSeed) $$ - -此哈希值一经生成不可更改,确保了服务器无法在后续过程中偷偷替换种子来操纵结果。 - -### 2.3 验证凭据 (Receipt) -每次抽奖完成后,系统会生成一份数字凭据 (`ActivityDrawReceipts`),其中包含: -- `issue_id`: 期号 -- `seed_hash`: 对应的种子哈希 -- `nonce` / `salt`: 随机盐值或防重随机数 -- `snapshot`: 当时的奖池状态快照(权重/格位) - ---- - -## 3. 算法实现 - -### 3.1 无限赏 (Weighted Random) - -适用于奖品无限库存或按权重概率抽取的模式。 - -**算法流程**: -1. **输入**: - - $Seed$: 全局活动种子 - - $IssueID$: 期号 - - $UserID$: 用户ID - - $Salt$: 每次请求生成的 16 字节随机盐值 - - $Rewards$: 奖品列表,包含权重 $w_i$ - -2. **随机数生成**: - 使用 HMAC-SHA256 派生出一个确定性的随机数 $R$。 - - $$ \text{payload} = \text{fmt.Sprintf("draw:issue:\%d|user:\%d|salt:\%x", IssueID, UserID, Salt)} $$ - $$ H = \text{HMAC-SHA256}(Seed, \text{payload}) $$ - $$ R = \text{BigEndianUint64}(H[0:8]) \pmod {\sum w_i} $$ - -3. **结果选择**: - 遍历奖品列表,累加权重查找 $R$ 落在哪个区间。 - -**代码逻辑**: -```go -mac := hmac.New(sha256.New, seedKey) -mac.Write([]byte(fmt.Sprintf("draw:issue:%d|user:%d|salt:%x", issueID, userID, salt))) -sum := mac.Sum(nil) -rnd := int64(binary.BigEndian.Uint64(sum[:8]) % uint64(totalWeight)) -``` - -### 3.2 一番赏 (Ichiban / Shuffle) - -适用于“箱内抽赏”模式,奖品总量固定,位置固定,采用先洗牌后抽取的逻辑。 - -**算法流程**: -1. **输入**: - - $Seed$: 全局活动种子 - - $IssueID$: 期号(每一个箱子是一个 Issue) - - $TotalSlots$: 总格位数(例如 80 发) - - $Rewards$: 初始有序的奖品列表(填充后的平铺列表) - -2. **确定性洗牌 (Deterministic Shuffle)**: - 使用 Fisher-Yates 洗牌算法,配合 HMAC-SHA256 生成的随机序列对奖品位置进行打乱。 - - 对于 $i$ 从 $TotalSlots-1$ 到 $1$: - $$ \text{payload}_i = \text{fmt.Sprintf("shuffle:\%d|issue:\%d", i, IssueID)} $$ - $$ H_i = \text{HMAC-SHA256}(Seed, \text{payload}_i) $$ - $$ j = \text{BigEndianUint64}(H_i[0:8]) \pmod {(i+1)} $$ - 交换索引 $i$ 和 $j$ 的元素。 - -3. **结果获取**: - 用户选择的格位号 $k$ (1-based) 对应洗牌后数组的索引 $k-1$ 处的奖品。 - - $$ Reward = ShuffledRewards[SelectedSlot - 1] $$ - -**特性**: -- **预定性**:只要种子确定,箱子那一刻的奖品排列就已注定,不论谁来抽、何时抽,第 N 格永远是那个奖品。 -- **公平性**:HMAC 的均匀分布保证了洗牌的随机性。 - ---- - -## 4. 验证指南 - -为了验证系统的公平性,用户或监管方可以使用官方提供的验证工具(`VerifyTool.exe`)进行独立计算。 - -### 4.1 获取验证参数 -从 API 或页面获取以下信息(活动结束后公开): -1. `server_seed_hex`: 服务器种子(十六进制) -2. `issue_id`: 期号 -3. `user_id` & `salt`: (仅无限赏需要) -4. `slot_index`: (仅一番赏需要) -5. `reward_config`: 奖品配置列表(验证前需要构建相同的初始列表) - -### 4.2 运行验证工具 - -**无限赏验证命令示例**: -```bash -./VerifyTool verify-unlimited \ - --seed "WaitToReveal32BytesHex..." \ - --issue 1001 \ - --user 12345 \ - --salt "RandomSaltHex..." \ - --weights "10,50,200,500" -``` - -**一番赏验证命令示例**: -```bash -./VerifyTool verify-ichiban \ - --seed "WaitToReveal32BytesHex..." \ - --issue 2002 \ - --slot 5 \ - --rewards "A:2,B:4,C:10,D:64" -``` -*(注:rewards 格式为 `奖项:数量`,如 A赏2个, B赏4个...)* - -### 4.3 验证原理 -验证工具内置了与服务器完全相同的算法逻辑(Go 源码编译)。输入相同的种子和上下文,必将输出相同的中奖结果。 - ---- - -## 5. 安全性声明 - -1. **种子保密**:`ServerSeed` 在存储层加密保存,仅在活动结束或特定审计时刻解密公开。 -2. **结果不可逆**:无法通过哈希值反推种子。 -3. **防预测**: - - 无限赏:引入了 `Salt`(真随机生成),即使用户猜到了种子,也无法预测下一次抽奖结果(因为 Salt 每次不同)。 - - 一番赏:种子一旦确定,序列即确定。我们在活动开始前才生成种子和 Commitment,确保无人(包括管理员)能提前知晓排列。 diff --git a/docs/standardize_points_ratio/ALIGNMENT_standardize_points_ratio.md b/docs/standardize_points_ratio/ALIGNMENT_standardize_points_ratio.md deleted file mode 100644 index bf195d2..0000000 --- a/docs/standardize_points_ratio/ALIGNMENT_standardize_points_ratio.md +++ /dev/null @@ -1,83 +0,0 @@ -# 任务对齐:统一积分与元比例 (Standardize Points and Yuan Ratio) - -## 1. 项目上下文分析 -- **项目结构**: - - 后端: `bindbox_game` (Go) - - 管理后台: `bindbox_game/web/admin` (Vue 3) - - 小程序: `bindbox-mini` (UniApp / Vue) -- **核心问题**: - - 当前代码逻辑隐含 "1积分 = 1分钱" (100积分=1元),前端通过 `/100` 强行展示为 "1.00",导致逻辑割裂。 - - 用户期望标准: **1元 = 1积分**。 - - 后端配置缺失,且计算公式需要适配 "元" 为单位。 - -## 2. 需求确认 -- **目标**: - 1. **统一比例**: 全局统一为 **1 元 = 1 积分** (即 1 积分价值 100 分钱)。 - 2. **配置化**: 后台可配置 "积分/元 兑换比例" (Rate),当前固定为 1。 - 3. **整改范围**: 修正后端转换公式,修正前端展示逻辑,添加后台配置界面。 - 4. **业务场景**: 暂时不涉及"消费送积分" (即订单完成后自动按比例赠送),主要关注积分价值本身(如充值、抵扣、退款等场景的换算)。 - -## 3. 现状分析 (As-Is) -- **后端 (`bindbox_game`)**: - - `CentsToPoints`: 依赖 `points_exchange_per_cent` (分换积分比例),默认为 1。即 1 分钱 = 1 积分单位。 - - `RefundPointsAmount`: 存在硬编码 `/ 100`,逻辑存疑。 -- **小程序 (`bindbox-mini`)**: - - `formatPoints`: `value / 100`。 - - 如果后端给 100 积分单位,前端展示为 "1.0"。 - - 含义模糊:是“1积分”还是“1.0元价值”? -- **后台 (`web/admin`)**: - - 缺少积分比例配置界面。 - -## 4. 关键决策点 (Resolved) -1. **积分定义**: - - 确认采用 **1 积分 = 1 元** 的价值锚点。 - - 数据库存储: 存 `1` 代表 1 积分 (即 1 元)。 - - *变更*: 现在的 `100` (分) 对应 1 元,未来 `1` (积分) 对应 1 元。需要明确是否存在历史数据需要迁移(或者是新项目/可重置)。**假设目前无存量包袱或接受重置/迁移,或者我们调整代码适配现有数值**。 - - *风险提示*: 如果仅仅改代码不改数据,原来的 100 积分 (1元) 瞬间变成 100 积分 (100元)。**必须确认是否需要数据清洗脚本**。 - -2. **兑换比例配置**: - - 配置项: `points_exchange_rate` (1 元对应多少积分)。 - - 默认值: 1。 - - 公式: - - 元转积分 (Amount -> Points): `points = amount_yuan * rate` => `points = (cents / 100) * rate` - - 积分转元 (Points -> Amount): `cents = (points / rate) * 100` - -## 5. 实施方案 (Architecture) -### 5.1 数据库与配置 -- **配置表 (`sys_configs`)**: - - Key: `points_exchange_rate` - - Value: `1` (Default) - - Description: "积分/元 兑换比例(多少积分=1元)"。 - -### 5.2 后端改造 (`bindbox_game`) -- **`internal/pkg/points/convert.go`**: - - `CentsToPoints(cents, rate)`: - - Old: `cents * rate` (Assumed rate per cent) - - New: `(cents * rate) / 100` (Rate per Yuan) - - `PointsToCents(points, rate)`: - - Old: `points / rate` - - New: `(points * 100) / rate` - - `RefundPointsAmount`: 适配新公式。 -- **Service Layer**: - - 确保读取新的配置 Key。 - - 检查所有手动计算积分的地方,全部收敛到 `convert` 包。 - -### 5.3 后台改造 (`web/admin`) -- **界面**: 在 `SystemConfigs` (系统配置) -> 新增 "积分配置" 分组。 -- **功能**: 编辑 `points_exchange_rate`。 - -### 5.4 小程序/前端改造 (`bindbox-mini`) -- **展示逻辑**: - - 移除 `formatPoints` 中的 `/ 100`。 - - 直接展示后端返回的整数积分。 - - 检查 "积分抵扣" 等页面,确保传给后端的数值正确。 - -## 6. 执行计划 (Task Split) -1. **Design**: 确认方案无误 (当前步骤)。 -2. **Backend**: - - 修改 Convert 算法。 - - 确保 Config 读取逻辑正确。 - - (Optional) 数据迁移脚本/重置脚本 (如果已有数据)。 -3. **Frontend (Admin)**: 添加配置界面。 -4. **Frontend (App)**: 修正展示逻辑。 -5. **Verify**: 验证 1 元订单是否对应 1 积分(模拟),或者充值/手动增加 1 积分是否显示为 1。 diff --git a/docs/standardize_points_ratio/DESIGN_standardize_points_ratio.md b/docs/standardize_points_ratio/DESIGN_standardize_points_ratio.md deleted file mode 100644 index de3108e..0000000 --- a/docs/standardize_points_ratio/DESIGN_standardize_points_ratio.md +++ /dev/null @@ -1,67 +0,0 @@ -# 架构设计:统一积分与元比例 (Design) - -## 1. 总体架构 -本次改造主要涉及 **计算逻辑层的标准化** 和 **配置数据的动态化**,不涉及大规模架构重构。 -核心思路:**后端收敛计算逻辑,前端收敛展示逻辑**。 - -```mermaid -graph TD - A[Admin User] -->|配置 ExchangeRate| B(Admin Panel) - B -->|API: upsertSystemConfig| C(Backend API) - C -->|Update| D[(MySQL: sys_configs)] - - E[App User] -->|Action: Pay/Refund| F(Backend Service) - F -->|Read Config| D - F -->|Call| G{pkg/points/convert} - G -->|Calculate based on Rate| F - F -->|Save Integer Points| D - - H[Mini App] -->|Read Points| F - H -->|Display Raw Integer| I[UI Display] -``` - -## 2. 模块设计 - -### 2.1 后端 (`bindbox_game`) -- **`internal/pkg/points`**: 核心计算包。 - - 职责:提供基于汇率的 `Cents <-> Points` 转换函数。 - - 变更: - - `CentsToPoints(cents, rate)`: 逻辑改为 `cents * rate / 100`。 - - `PointsToCents(points, rate)`: 逻辑改为 `points * 100 / rate`。 -- **`internal/service/user`**: 业务服务层。 - - 职责:在积分变动(增加、扣减、退款)时,从 `sys_configs` 读取最新 `points_exchange_rate` 并传入计算包。 - - 变更:检查所有调用点,确保不再硬编码。 - -### 2.2 数据库 (`MySQL`) -- **Schema**: 无变更。 -- **Data Migration**: - - 需执行数据清洗,将现有的积分数值 `x` 更新为 `x / 100` (假设之前是按分存的)。 - - **Risk**: 需要用户确认是否执行此 SQL。**默认提供 SQL 但不自动运行**。 - -### 2.3 管理后台 (`web/admin`) -- **SystemConfigs**: - - 新增 "积分配置" Section。 - - Key: `points_exchange_rate`。 - - 验证:必须为正整数,默认为 1。 - -### 2.4 小程序 (`bindbox-mini`) -- **Utils**: - - `formatPoints`: 移除除以 100 的逻辑,仅保留千分位格式化。 -- **Pages**: - - 检查积分明细、下单抵扣、商品兑换等页面的展示。 -- **Vue Filters/Formatters**: 全局搜索使用 `/ 100` 展示积分的地方进行替换。 - -## 3. 接口规范 -- **API**: `POST /admin/system/configs` (现有) - - Payload: `{ "key": "points_exchange_rate", "value": "1" }` - -## 4. 迁移策略 -1. **停机维护** (建议): 防止数据在迁移过程中变动。 -2. **代码部署**: 部署新版后端(新算法)。 -3. **数据清洗**: - ```sql - -- 假设所有用户的积分都需要缩小 100 倍以适配新算法 - UPDATE user_points SET points = FLOOR(points / 100); - UPDATE user_points_ledger SET points = FLOOR(points / 100); - ``` -4. **验证**: 检查某测试账号积分是否符合预期。 diff --git a/docs/standardize_points_ratio/TASK_standardize_points_ratio.md b/docs/standardize_points_ratio/TASK_standardize_points_ratio.md deleted file mode 100644 index 066ea32..0000000 --- a/docs/standardize_points_ratio/TASK_standardize_points_ratio.md +++ /dev/null @@ -1,45 +0,0 @@ -# 任务任务:统一积分与元比例 (Task List) - -## 0. 预备工作 -- [ ] **确认数据备份**: 提醒用户备份数据库。 -- [ ] **代码同步**: 确保本地代码是最新的。 - -## 1. 后端改造 (Backend) -- [ ] **修改核心计算包 (`internal/pkg/points/convert.go`)** - - [ ] 修改 `CentsToPoints` 为 `(cents * rate) / 100` - - [ ] 修改 `PointsToCents` 为 `(points * 100) / rate` - - [ ] 修改 `RefundPointsAmount` 适配新公式 -- [ ] **业务逻辑适配 (`internal/service/user`)** - - [ ] 检查 `points_convert.go` 中的 `CentsToPoints` 调用,确保读取配置 Key `points_exchange_rate` (或复用旧 Key 但明确含义)。 - - [ ] 检查 `points_consume.go` 等文件,确保无其他硬编码。 -- [ ] **单元测试** - - [ ] 运行 `internal/pkg/points` 的测试,确保 100 分钱在 Rate=1 时转为 1 积分。 - -## 2. 管理后台改造 (Admin Frontend) -- [ ] **更新系统配置页 (`web/admin/src/views/system/configs/index.vue`)** - - [ ] 新增 "积分配置" 卡片。 - - [ ] 添加 `points_exchange_rate` 编辑项。 - - [ ] 添加说明: "1元 = ? 积分"。 - -## 3. 小程序改造 (Mini Program) -- [ ] **全局搜索积分展示** - - [ ] 搜索 `/ 100` 或 `* 0.01` 相关的积分代码。 -- [ ] **修复工具函数** - - [ ] 修改 `utils/format.js` 或类似文件中的 `formatPoints`。 -- [ ] **修复页面展示** - - [ ] `pages-user/points/index.vue` (积分明细) - - [ ] `pages-user/orders/detail.vue` (如果有积分抵扣展示) - - [ ] 其他涉及积分展示的页面。 - -## 4. 数据迁移 (Migration) -- [ ] **提供 SQL 脚本** - - [ ] 存入 `docs/standardize_points_ratio/migration.sql`。 -- [ ] **(Optional) 执行迁移** - - [ ] 根据用户指示决定是否执行。 - -## 5. 验证 (Verification) -- [ ] **后端验证** - - [ ] 重启服务。 - - [ ] 模拟支付 1 元,查看数据库增加 1 积分。 -- [ ] **前端验证** - - [ ] 查看积分列表,显示为 1 (而不是 0.01 或 100)。 diff --git a/docs/standardize_points_ratio/migration.sql b/docs/standardize_points_ratio/migration.sql deleted file mode 100644 index 95ac626..0000000 --- a/docs/standardize_points_ratio/migration.sql +++ /dev/null @@ -1,62 +0,0 @@ --- Standardize Points Ratio Migration Script --- Purpose: Convert point values from "1 Yuan = 100 Points" (Cents) to "1 Yuan = 1 Point" (Integer). --- Target Tables: user_points, user_points_ledger, orders. - -BEGIN; - --- 1. Update User Points Current Balance --- Description: Reduce all current point balances by factor of 100. -UPDATE user_points -SET points = FLOOR(points / 100); - --- 2. Update User Points Ledger (History) --- Description: Adjust historical records to reflect the new unit. -UPDATE user_points_ledger -SET points = FLOOR(points / 100); - --- 3. Update Orders Points Usage --- Description: Adjust recorded points usage in orders. -UPDATE orders -SET points_amount = FLOOR(points_amount / 100) -WHERE points_amount > 0; - --- 4. Update Task Center Rewards (Points) --- Description: Adjust configured point rewards in Task Center. --- Note: Logic prioritizes 'points' in JSON payload, then 'quantity'. --- Updating 'quantity' is safe. Updating JSON is complex in standard SQL without knowing exact structure/version. --- Assuming simple structure {"points": 100} or similar. --- 4a. Update Quantity for type 'points' -UPDATE task_center_task_rewards -SET quantity = FLOOR(quantity / 100) -WHERE reward_type = 'points' AND quantity >= 100; - --- 4b. Update RewardPayload? (Optional/Manual) --- Warning: Modifying JSON string requires robust parsing. --- Providing a best-effort text replacement for simple cases {"points": 100} -> {"points": 1} --- This relies on the pattern '"points": ' followed by numbers. --- Recommendation: Manually verify Task Center configurations in Admin Panel after migration. - - --- 5. System Config Update (Values) --- Description: Convert known config values if they represent points. --- Example: 'register_points', 'daily_sign_in_points' (if they exist). --- This attempts to update common point-related configs. -UPDATE system_configs -SET config_value = FLOOR(config_value / 100) -WHERE config_key IN ('register_reward_points', 'daily_sign_in_reward_points') AND config_value REGEXP '^[0-9]+$'; - --- 6. System Config Update (Exchange Rate) --- Description: Ensure the new config key is set. -INSERT INTO system_configs (config_key, config_value, remark, created_at, updated_at) -VALUES ('points_exchange_rate', '1', '1元对应多少积分', NOW(), NOW()) -ON DUPLICATE KEY UPDATE config_value = '1'; - -COMMIT; - --- NOTE on 'user_inventory': --- Some points data might be embedded in 'remark' field (e.g. '|redeemed_points=100'). --- Updating these embedded strings via SQL is complex and risky. --- Refunds for OLD items (redeemed before migration) might fail or deduct excessive points --- if this field is not updated. --- Recommendation: Handle 'refund' logic gracefully for legacy items in code if possible, --- or accept that old items cannot be refunded automatically. diff --git a/docs/standardize_points_ratio/walkthrough.md b/docs/standardize_points_ratio/walkthrough.md deleted file mode 100644 index 9b5df23..0000000 --- a/docs/standardize_points_ratio/walkthrough.md +++ /dev/null @@ -1,52 +0,0 @@ -# Walkthrough: Standardize Points Ratio - -## 1. Goal -Standardize the Points-to-Yuan ratio to **1 Yuan = 1 Point**, and remove the decimal display logic (previous 100 Points = 1 Yuan) across the system. - -## 2. Changes - -### Backend (`bindbox_game`) -- **Core Logic (`internal/pkg/points/convert.go`)**: - - Updated `CentsToPoints` to `(cents * rate) / 100`. - - Updated `PointsToCents` to `(points * 100) / rate`. - - Added unit tests to verify 100 Cents = 1 Point. -- **Service Layer (`internal/service/user`)**: - - Updated `points_convert.go` to use `points_exchange_rate` config key. - - Added `PointsToCents` method for reverse calculation. - - Updated `lottery_app.go` to use `PointsToCents` for accurate deduction calculation. -- **DB Config**: - - Expects `points_exchange_rate` (default 1) instead of `points_exchange_per_cent`. - -### Admin Frontend (`web/admin`) -- **System Configs**: - - Added "Points Configuration" section. - - Allows setting "1 Yuan = N Points" (Default 1). - -### Mini-Program Frontend Display Logic -- **Goal**: Ensure Points are displayed as integers and values are consistent. -- **Files Modified**: - - `pages-user/points/index.vue` - - `pages-user/orders/detail.vue` - - `pages-user/tasks/index.vue` - - `pages/shop/index.vue` (Fixed `/ 100` division for points) - - `pages-shop/shop/detail.vue` (Fixed `/ 100` division for points) -- **Changes**: - - Removed incorrect `/ 100` division for Points display while keeping it for Money (Yuan) display. - - formated points to use `.toFixed(0)` to remove decimal places. - -### Database Migration -- **Script**: `docs/standardize_points_ratio/migration.sql` -- **Action Required**: Run this script to shrink existing point values by 100x to match the new 1:1 definition. - -## 3. Verification -- **Unit Tests**: - - `go test internal/pkg/points/...` passed. -- **Manual Check**: - - `TestCentsToPoints` confirmed 100 Cents -> 1 Point. - - `TestRefundPointsAmount` confirmed proportional refund works with integer points. - -## 4. Next Steps for User -1. **Backup Database**. -2. **Deploy Backend & Admin**. -3. **Run Migration Script** (`docs/standardize_points_ratio/migration.sql`). -4. **Deploy Mini Program**. diff --git a/docs/yifanshang_count_card_fix/ALIGNMENT_yifanshang_count_card_fix.md b/docs/yifanshang_count_card_fix/ALIGNMENT_yifanshang_count_card_fix.md deleted file mode 100644 index 6d9330f..0000000 --- a/docs/yifanshang_count_card_fix/ALIGNMENT_yifanshang_count_card_fix.md +++ /dev/null @@ -1,46 +0,0 @@ -# 任务:修复一番赏次数卡支付与退款逻辑 - -## 1. 项目上下文分析 -- **项目**: BindBox (Blind Box / Ichiban Kuji Game Platform) -- **技术栈**: Go (Backend), Vue3/UniApp (Frontend) -- **涉及模块**: - - 前端:一番赏活动页 (`bindbox-mini/pages-activity/activity/yifanshang/index.vue`) - - 后端:抽奖接口 (`internal/api/activity/lottery_app.go`) - - 后端:退款接口 (`internal/api/admin/pay_refund_admin.go`) - -## 2. 需求理解与确认 -### 原始需求 -1. **不掉支付**: 使用次数卡(Count Card)进行一番赏抽奖时,不应拉起微信支付(因为金额应为0)。 -2. **退款退卡**: 对使用次数卡支付的订单进行退款时,应退还次数卡(次数),而非退款金额(因为金额为0)或无操作。 - -### 问题分析 -通过代码审查,发现以下问题: -1. **前端支付逻辑缺陷**: `index.vue` 在调用 `joinLottery` 后,未根据返回的订单状态或金额判断是否需要支付,而是无条件调用 `createWechatOrder` 并拉起支付。 -2. **后端抽奖逻辑缺陷**: `lottery_app.go` 在使用次数卡扣费时,虽然将 `ActualAmount` 设为 0,但在 `order.Remark` 中仅记录了 `use_game_pass`,未记录具体使用的次数卡 ID 和扣除数量。 -3. **后端退款逻辑缺陷**: `pay_refund_admin.go` 在退款时尝试从 `order.Remark` 解析 `game_pass:ID`,但由于上述原因无法解析。且当前逻辑仅尝试恢复 `remaining + 1`,未考虑到一单多抽(Count > 1)的情况。 - -## 3. 智能决策策略 -### 决策点 1:前端如何判断跳过支付? -- **方案**: 检查 `joinLottery` 返回的 `actual_amount` 或 `status`。 -- **依据**: `lottery_app.go` 中,若金额为 0,`Status` 会被置为 2 (Paid),且 `ActualAmount` 为 0。 -- **实现**: 若 `res.actual_amount === 0` 或 `res.status === 2`,则直接进入抽奖结果展示流程。 - -### 决策点 2:后端如何记录次数卡使用情况? -- **方案**: 修改 `lottery_app.go`,在 Deduction 循环中记录所有使用的 Pass ID 和对应扣除数。 -- **格式建议**: `gp_use:ID1:Count1|gp_use:ID2:Count2` -- **兼容性**: 需确保不破坏现有 Remark 的其他信息。 - -### 决策点 3:退款如何恢复次数? -- **方案**: 修改 `pay_refund_admin.go`,解析新的 Remark 格式。 -- **逻辑**: 遍历所有记录的 ID,按记录的扣除数执行 `UPDATE user_game_passes SET remaining = remaining + ?, total_used = total_used - ? WHERE id = ?`。 - -## 4. 最终共识 (Consensus) -### 任务边界 -1. **Frontend**: 修改 `yifanshang/index.vue` 的 `onPaymentConfirm` 方法。 -2. **Backend**: 修改 `lottery_app.go` 记录 Game Pass Usage。 -3. **Backend**: 修改 `pay_refund_admin.go` 实现精准的次数卡退还。 - -### 验收标准 -1. **支付测试**: 使用次数卡购买一番赏(单抽或多抽),前端不弹出微信支付,直接显示抽奖结果。 -2. **数据验证**: 数据库 `orders` 表的 `remark` 字段包含具体的次数卡使用记录(如 `gp_use:101:5`)。 -3. **退款测试**: 对该订单执行全额退款,对应的次数卡(ID 101)的 `remaining` 增加 5,`total_used` 减少 5。 diff --git a/docs/yifanshang_count_card_fix/CONSENSUS_yifanshang_count_card_fix.md b/docs/yifanshang_count_card_fix/CONSENSUS_yifanshang_count_card_fix.md deleted file mode 100644 index 48a0672..0000000 --- a/docs/yifanshang_count_card_fix/CONSENSUS_yifanshang_count_card_fix.md +++ /dev/null @@ -1,43 +0,0 @@ -# 共识:修复一番赏次数卡支付与退款逻辑 - -## 1. 需求描述与验收标准 -### 需求描述 -- **不掉支付**: 使用次数卡支付时,若订单金额为0,前端不应拉起微信支付,直接视为支付成功。 -- **退款退卡**: 次数卡支付的订单退款时,需准确退还使用的次数卡次数。 - -### 验收标准 -1. **前端支付流程**: - - [ ] 使用次数卡全额抵扣时,点击“去支付”后直接弹出成功/翻牌界面,无微信支付弹窗。 - - [ ] 混合支付(次数卡不足补差价)时,仍正常拉起支付(本次主要关注全额抵扣)。 -2. **后端订单记录**: - - [ ] 数据库 `orders.remark` 字段正确记录 `gp_use::`。 - - [ ] 支持单次使用多张次数卡的情况(如 `gp_use:101:5|gp_use:102:1`)。 -3. **退款流程**: - - [ ] 对次数卡订单执行退款,对应的 `user_game_passes` 记录 `remaining` 增加,`total_used` 减少。 - - [ ] 退还数量与扣除数量完全一致。 - - [ ] 退款流水清晰。 - -## 2. 技术实现方案 -### 前端 (Mini-Program) -- 修改 `bindbox-mini/pages-activity/activity/yifanshang/index.vue`。 -- 在 `onPaymentConfirm` 中,接收 `joinLottery` 响应后,判断 `order.ActualAmount == 0` 或 `Status == 2`。 -- 若满足,直接调用 `onPaymentSuccess` 或模拟成功逻辑,跳过 `createWechatOrder`。 - -### 后端 (Go) -- **抽奖 (Lottery)**: - - 修改 `internal/api/activity/lottery_app.go`。 - - 在扣除 Game Pass 的循环中,动态构建 Remark 字符串,记录每个 Pass 的扣除量。 -- **退款 (Refund)**: - - 修改 `internal/api/admin/pay_refund_admin.go`。 - - 升级 Remark 解析逻辑,支持 `gp_use:ID:Count` 格式。 - - 遍历所有使用的 Pass,逐一执行恢复 SQL。 - -## 3. 任务边界限制 -- 仅针对一番赏业务(Ichiban)。 -- 仅针对 Game Pass (次数卡) 支付方式。 -- 不涉及优惠券退款逻辑变更(除非受全额退款逻辑影响,需确保兼容)。 - -## 4. 确认所有不确定性已解决 -- 已确认当前 Frontend 无条件拉起支付是 Bug。 -- 已确认当前 Backend 未记录详细 Pass ID 是导致无法精确退款的原因。 -- 已确认 Refund 逻辑需升级以支持多卡/多数量退还。 diff --git a/docs/yifanshang_count_card_fix/DESIGN_yifanshang_count_card_fix.md b/docs/yifanshang_count_card_fix/DESIGN_yifanshang_count_card_fix.md deleted file mode 100644 index c6e2fc4..0000000 --- a/docs/yifanshang_count_card_fix/DESIGN_yifanshang_count_card_fix.md +++ /dev/null @@ -1,85 +0,0 @@ -# 设计:修复一番赏次数卡支付与退款逻辑 - -## 1. 整体架构与流程 -### 支付流程 (Modified) -```mermaid -sequenceDiagram - participant User - participant Frontend (Vue) - participant Backend (API) - participant DB - - User->>Frontend: 选择一番赏格位,点击支付 (使用次数卡) - Frontend->>Backend: API: /api/app/lottery/join (use_game_pass=true) - Backend->>DB: Check Game Pass Balance - Backend->>DB: Deduct Game Pass (Remaining - N) - Backend->>DB: Create Order (Amount=0, Status=2, Remark="gp_use:ID:N") - Backend-->>Frontend: Response (Success, Amount=0, Status=2) - - alt Amount == 0 - Frontend->>Frontend: Skip WeChat Pay - Frontend->>Frontend: Show Result / Flip Cards - else Amount > 0 - Frontend->>Backend: Create WeChat Order - Frontend->>WeChat: Request Payment - end -``` - -### 退款流程 (Modified) -```mermaid -sequenceDiagram - participant Admin - participant Backend (Refund API) - participant DB - - Admin->>Backend: API: /api/admin/refund (OrderNo) - Backend->>DB: Get Order & Remark - Backend->>Backend: Parse Remark for "gp_use:ID:Count" pairs - loop For Each Pass - Backend->>DB: Update user_game_passes SET remaining+=Count - end - Backend->>DB: Update Order Status = Refunded - Backend-->>Admin: Success -``` - -## 2. 核心组件设计 -### 2.1 Lottery App (`lottery_app.go`) -- **Logic**: In `JoinLottery` handler, inside the transaction where `useGamePass` is true. -- **Change**: - ```go - // Before - deducted += canDeduct - // After - deducted += canDeduct - gamePassUsage = append(gamePassUsage, fmt.Sprintf("gp_use:%d:%d", p.ID, canDeduct)) - ... - order.Remark += "|" + strings.Join(gamePassUsage, "|") - ``` - -### 2.2 Refund Admin (`pay_refund_admin.go`) -- **Logic**: In `CreateRefund`. -- **Change**: - - Remove simple regex `game_pass:(\d+)`. - - Add loop to find all `gp_use:(\d+):(\d+)`. - - Execute restoration for each match. - -### 2.3 Frontend (`index.vue`) -- **Logic**: `onPaymentConfirm`. -- **Change**: - ```javascript - if (joinResult.actual_amount === 0 || joinResult.status === 2 || joinResult.status === 'paid') { - // Direct Success - onPaymentSuccess({ result: lotteryResult }) - return - } - ``` - -## 3. 接口契约 -- 无新增接口。 -- `JoinLottery` 响应保持不变,前端需利用现有的 `actual_amount` 和 `status` 字段。 - -## 4. 异常处理 -- **Refund Partial Failure**: Cannot easily happen in transaction. If one update fails, whole refund fails. -- **Legacy Orders**: Old orders have `use_game_pass` but no `gp_use:ID`. Refund logic should fallback to old behavior (try to find 1 pass or just warn/skip). - - *Fallback Strategy*: If no `gp_use` found but `use_game_pass` is present, log warning or try to restore 1 count to *any* valid pass of user? - - *Decision*: Since user specifically asked for this fix, we assume it's for FUTURE/NEW orders or current testing. For legacy orders, we can leave as is or try best effort. Given "Strict" requirement, we will implement the new logic. Legacy fallback: Scan `game_pass:ID` (old format if any?) - wait, old code didn't write ID at all. So legacy orders cannot be automatically restored safely. This is acceptable for a "Fix". diff --git a/docs/yifanshang_count_card_fix/FINAL_yifanshang_count_card_fix.md b/docs/yifanshang_count_card_fix/FINAL_yifanshang_count_card_fix.md deleted file mode 100644 index aa02c0b..0000000 --- a/docs/yifanshang_count_card_fix/FINAL_yifanshang_count_card_fix.md +++ /dev/null @@ -1,28 +0,0 @@ -# 项目总结报告:修复一番赏次数卡支付与退款逻辑 - -## 1. 任务概述 -本任务旨在修复“一番赏”业务中,使用次数卡(Game Pass)支付时前端仍拉起微信支付的问题,以及退款时未能正确退还次数卡次数的问题。 - -## 2. 完成情况 -### 2.1 需求实现 -- [x] **前端支付优化**: `yifanshang/index.vue` 已增加判断逻辑,当 `ActualAmount == 0` 或 `Status == 2` 时,直接跳过微信支付流程,进入开奖结果查询。 -- [x] **后端记录优化**: `lottery_app.go` 现在会在订单 `Remark` 中以 `gp_use:ID:Count` 格式记录具体的次数卡使用明细。 -- [x] **后端退款优化**: `pay_refund_admin.go` 已支持解析新的 `gp_use` 格式,并能准确恢复多张卡、多数量的消耗。同时保留了对旧格式 `game_pass:ID` 的兼容支持。 - -### 2.2 代码变更 -- `internal/api/activity/lottery_app.go`: 记录 Game Pass 扣除明细。 -- `internal/api/admin/pay_refund_admin.go`: 增强退款逻辑,支持多卡恢复。 -- `bindbox-mini/pages-activity/activity/yifanshang/index.vue`: 优化支付流程,支持 0 元订单。 - -## 3. 质量评估 -- **编译通过**: 后端代码 `go build` 成功,无语法错误。 -- **逻辑完备性**: - - 覆盖了“不掉支付”的核心诉求。 - - 覆盖了“精准退卡”的核心诉求。 - - 考虑了新旧数据格式兼容性。 -- **风险控制**: - - 仅针对 `Ichiban` 逻辑生效,不影响其他业务。 - - 前端改动范围局限于支付确认回调,风险可控。 - -## 4. 交付结论 -以完成所有关键路径的修复,代码已准备就绪,可以部署测试。 diff --git a/docs/yifanshang_count_card_fix/TASK_yifanshang_count_card_fix.md b/docs/yifanshang_count_card_fix/TASK_yifanshang_count_card_fix.md deleted file mode 100644 index a36a95a..0000000 --- a/docs/yifanshang_count_card_fix/TASK_yifanshang_count_card_fix.md +++ /dev/null @@ -1,40 +0,0 @@ -# 任务拆解:修复一番赏次数卡支付与退款逻辑 - -## 1. Backend Tasks -### 1.1 Update Lottery Logic (Atomize) -- **File**: `internal/api/activity/lottery_app.go` -- **Goal**: Record specific Game Pass usage in Order Remark. -- **Steps**: - - Locate `JoinLottery` function. - - Inside `useGamePass` block, accumulate used pass IDs and counts. - - Append formatted string `gp_use:ID:Count` to `order.Remark`. -- **Verification**: Run local test, buy with count card, check DB `orders` table remark. - -### 1.2 Update Refund Logic (Atomize) -- **File**: `internal/api/admin/pay_refund_admin.go` -- **Goal**: Parse `gp_use` and restore counts. -- **Steps**: - - Locate `CreateRefund` function. - - Replace/Extend existing Game Pass restoration logic. - - Implement regex to find all `gp_use:(\d+):(\d+)`. - - Loop and execute SQL updates. -- **Verification**: Create order with count card (using 1.1), then call refund API, check `user_game_passes` table restoration. - -## 2. Frontend Tasks -### 2.1 Update Payment Flow (Atomize) -- **File**: `bindbox-mini/pages-activity/activity/yifanshang/index.vue` -- **Goal**: Skip WeChat Pay for 0-amount orders. -- **Steps**: - - Locate `onPaymentConfirm`. - - After `joinLottery` returns, check `joinResult.actual_amount === 0` or `status`. - - If true, directly call logic to show results (e.g. `onPaymentSuccess` or equivalent logic to flip cards). - - Ensure `getLotteryResult` is called (which is already there in logic, just need to skip `createWechatOrder`). -- **Verification**: UI test with count card. - -## 3. Dependency Graph -```mermaid -graph TD - B1[Backend: Lottery Logic] --> B2[Backend: Refund Logic] - B1 --> F1[Frontend: Payment Flow] -``` -(B2 and F1 can be parallel, but B1 is prerequisite for B2 to be testable with new data). diff --git a/docs/yifanshang_count_card_fix/TODO_yifanshang_count_card_fix.md b/docs/yifanshang_count_card_fix/TODO_yifanshang_count_card_fix.md deleted file mode 100644 index 66b4740..0000000 --- a/docs/yifanshang_count_card_fix/TODO_yifanshang_count_card_fix.md +++ /dev/null @@ -1,12 +0,0 @@ -# 待办事项:一番赏次数卡修复后续 - -## 1. 测试与验证 -- [ ] **真机验证**: 需要在真机小程序环境测试使用次数卡购买一番赏,确认是否直接跳过支付。 -- [ ] **退款验证**: 需要在管理后台对生成的次数卡订单进行退款,并检查数据库 `user_game_passes` 表确认次数是否正确恢复。 - -## 2. 遗留/潜在问题 -- [ ] **历史订单处理**: 此修复仅对新生成的订单生效(因为旧订单缺少 `gp_use:ID:Count` 记录)。旧订单退款仍将使用旧逻辑(仅退 1 次)。如果需要处理大量历史订单的批量退款,建议通过 SQL 手动修复或编写一次性脚本。 -- [ ] **多端兼容**: 目前仅修改了 `bindbox-mini`(小程序端)。如果存在 App 端或 H5 端,需确认是否复用相同逻辑或需要单独修改。 - -## 3. 配置建议 -- 无新增配置项。 diff --git a/docs/后台工作台接口分析/ALIGNMENT_后台工作台接口.md b/docs/后台工作台接口分析/ALIGNMENT_后台工作台接口.md deleted file mode 100644 index 425bc8a..0000000 --- a/docs/后台工作台接口分析/ALIGNMENT_后台工作台接口.md +++ /dev/null @@ -1,259 +0,0 @@ -# 后台工作台页面接口分析 - -## 一、原始需求 -分析后台工作台页面的所有设计,确定需要对应哪些接口,并补充后端缺失的接口实现。 - -## 二、项目特性规范 - -### 技术栈 -- **前端**: Vue 3 + TypeScript + Element Plus -- **后端**: Go + Gin + GORM -- **API风格**: RESTful - -### 现有架构 -- 前端API定义: `web/admin/src/api/dashboard.ts`, `web/admin/src/api/operations.ts` -- 后端处理器: `internal/api/admin/dashboard_admin.go` - ---- - -## 三、工作台页面模块与接口对应关系 - -### 维度1: 经营大盘 (overview) - -| 组件 | 功能 | 前端API | 后端状态 | 备注 | -|------|------|---------|----------|------| -| `card-list.vue` | 顶部统计卡片 | `fetchCardStats` | ✅ 已实现 | `DashboardCards` | -| `sales-overview.vue` | 销售趋势分析 | `fetchSalesDrawTrend` | ✅ 已实现 | `DashboardSalesDrawTrend` | -| `product-performance.vue` | 产品动销排行 | `fetchProductPerformance` | ⚠️ Mock数据 | 需要实现 | -| `user-economics.vue` | 用户经济分析 | `fetchUserEconomics` | ✅ 已实现 | `DashboardUserEconomics` | - ---- - -### 维度2: 奖池与欧气 (lottery) - -| 组件 | 功能 | 前端API | 后端状态 | 备注 | -|------|------|---------|----------|------| -| `prize-pool-health.vue` | 奖池健康度分析 | `fetchPrizeDistribution` | ✅ 已实现 | `DashboardPrizeDistribution` | -| `live-stream-premium.vue` | 全服欧气实时播报 | 无(模拟数据) | ⚠️ 需要实现 | 需要新增实时中奖播报接口 | - ---- - -### 维度3: 营销转化 (marketing) - -| 组件 | 功能 | 前端API | 后端状态 | 备注 | -|------|------|---------|----------|------| -| `growth-analytics.vue` | 增长经济模型分析 | `fetchUserEconomics` | ✅ 已实现 | 复用 `DashboardUserEconomics` | -| `coupon-roi.vue` | 营销券效能排行 | `fetchCouponEffectiveness` | ⚠️ Mock数据 | 需要实现 | -| `retention-cohort.vue` | 留存同类群组分析 | `fetchRetentionAnalytics` | ✅ 已实现 | `DashboardRetentionAnalytics` | -| `marketing-conversion.vue` | 订单转化全链路监控 | `fetchOrderFunnel` | ✅ 已实现 | `DashboardOrderFunnel` | - ---- - -### 维度4: 风控预警 (security) - -| 组件 | 功能 | 前端API | 后端状态 | 备注 | -|------|------|---------|----------|------| -| `inventory-alert.vue` | 库存预警监控 | `fetchInventoryAlerts` | ⚠️ Mock数据 | 需要实现 | -| `risk-monitor.vue` | 异常风险监控 | `fetchRiskEvents` | ⚠️ Mock数据 | 需要实现 | -| `points-economy.vue` | 积分经济总览 | `fetchPointsEconomySummary`
`fetchPointsTrend`
`fetchPointsStructure` | ⚠️ Mock数据 | 需要实现3个接口 | - ---- - -## 四、需要补充的后端接口清单 - -### 4.1 运营分析接口 (Operations) - -#### 1. 产品动销排行 `GET /admin/operations/product_performance` -```json -// Request -{ "rangeType": "7d|30d|today" } - -// Response -[ - { - "id": 1, - "seriesName": "系列名称", - "salesCount": 1540, - "amount": 285000, // 销售金额(分) - "contributionRate": 35.5, // 利润贡献率% - "inventoryTurnover": 8.5 // 周转率 - } -] -``` - -#### 2. 优惠券效能排行 `GET /admin/operations/coupon_effectiveness` -```json -// Request -{ "rangeType": "7d|30d" } - -// Response -[ - { - "couponId": 1, - "couponName": "新用户专享券", - "type": "满减券", - "issuedCount": 1200, - "usedCount": 680, - "usedRate": 56.7, // 使用率% - "broughtOrders": 720, // 带动订单数 - "broughtAmount": 3600000, // 带动金额(分) - "roi": 3.2 // 投资回报率 - } -] -``` - -#### 3. 库存预警列表 `GET /admin/operations/inventory_alerts` -```json -// 无请求参数 - -// Response -[ - { - "id": 101, - "name": "商品名称", - "type": "physical|virtual|coupon", - "stock": 3, - "threshold": 5, - "salesSpeed": 1.2 // 日均消耗速度 - } -] -``` - -#### 4. 风险事件监控 `GET /admin/operations/risk_events` -```json -// 无请求参数 - -// Response -[ - { - "userId": 5001, - "nickname": "用户昵称", - "avatar": "头像URL", - "type": "frequent_win|batch_register|ip_clash", - "description": "24小时内中奖5次一等奖", - "riskLevel": "high|medium|low", - "createdAt": "13:20" - } -] -``` - ---- - -### 4.2 积分经济接口 (Points Economy) - -#### 5. 积分经济总览 `GET /admin/operations/points_economy_summary` -```json -// Request -{ "rangeType": "7d|30d" } - -// Response -{ - "totalIssued": 1258400, // 发行总积分 - "totalConsumed": 985600, // 消耗总积分 - "netChange": 272800, // 净变化 - "activeUsersWithPoints": 5640, // 持分活跃用户数 - "conversionRate": 78.5 // 活跃持仓率% -} -``` - -#### 6. 积分趋势 `GET /admin/operations/points_trend` -```json -// Request -{ "rangeType": "7d|30d" } - -// Response -[ - { - "date": "2026-01-01", - "issued": 20000, - "consumed": 15000, - "expired": 1000, - "netChange": 4000, - "balance": 250000 - } -] -``` - -#### 7. 积分收支结构 `GET /admin/operations/points_structure` -```json -// Request -{ "rangeType": "7d|30d" } - -// Response -[ - { - "category": "任务奖励", - "amount": 85000, - "percentage": 45.2, - "trend": "+12.5%" - } -] -``` - ---- - -### 4.3 实时播报接口 (Live Stream) - -#### 8. 实时中奖播报 `GET /admin/dashboard/live_winners` -```json -// Request -{ "sinceId": 0, "limit": 20 } - -// Response -{ - "list": [ - { - "id": 12345, - "nickname": "用户昵称", - "avatar": "头像URL", - "issueName": "活动期名称", - "prizeName": "奖品名称", - "isBigWin": true, - "createdAt": "刚刚" - } - ], - "stats": { - "hourlyWinRate": 4.2, // 近1小时爆率 - "drawsPerMinute": 128 // 连抽频率 - } -} -``` - ---- - -## 五、疑问澄清 - -### 5.1 需要确认的问题 - -1. **风险事件监控**: 目前系统是否有用户行为日志表可供分析?如果没有,是否需要先创建相关基础设施? - -2. **库存预警阈值**: 库存预警的阈值应该从哪里获取?是固定配置还是每个商品可单独设置? - -3. **积分经济统计范围**: 积分发行/消耗是否需要区分来源类型(任务奖励、抽奖中奖、兑换消耗等)? - -4. **实时播报频率**: 前端轮询间隔建议设为多少?3秒是否合适? - ---- - -## 六、边界与限制 - -### 6.1 任务边界 -- ✅ 补充后端缺失的运营分析接口 -- ✅ 实现积分经济相关接口 -- ✅ 实现库存预警和风险监控接口 -- ✅ 更新前端API调用从Mock数据改为真实后端调用 -- ❌ 不涉及前端UI重构 -- ❌ 不涉及权限管理改动 - -### 6.2 依赖关系 -- 依赖现有数据库表: `users`, `orders`, `user_points_logs`, `coupons`, `user_coupons`, `prizes`, `draw_logs` 等 -- 可能需要新增数据库视图或缓存层以提高查询性能 - ---- - -## 七、预期验收标准 - -1. 所有前端Mock数据的接口均已替换为真实后端调用 -2. 后端接口响应格式与前端类型定义一致 -3. 各项统计数据计算逻辑准确 -4. 查询性能在可接受范围内(响应时间 < 500ms) diff --git a/docs/后台工作台接口分析/CONSENSUS_后台工作台接口.md b/docs/后台工作台接口分析/CONSENSUS_后台工作台接口.md deleted file mode 100644 index 4a03f4e..0000000 --- a/docs/后台工作台接口分析/CONSENSUS_后台工作台接口.md +++ /dev/null @@ -1,91 +0,0 @@ -# 后台工作台接口 - 共识文档 - -## 一、明确需求描述 - -补充后台工作台页面中使用Mock数据的8个接口,实现真实的后端数据查询逻辑。 - -### 需求范围 -1. **运营分析接口** (4个) - - 产品动销排行 - - 优惠券效能排行 - - 库存预警列表 - - 风险事件监控 - -2. **积分经济接口** (3个) - - 积分经济总览 - - 积分趋势 - - 积分收支结构 - -3. **实时播报接口** (1个) - - 实时中奖播报 - ---- - -## 二、技术实现方案 - -### 2.1 后端接口实现位置 -- **文件**: `internal/api/admin/dashboard_admin.go` (现有文件,追加新接口) -- **路由注册**: 在现有admin路由组中添加新路径 - -### 2.2 数据来源表 -| 接口 | 主要数据表 | 关联表 | -|------|-----------|--------| -| 产品动销排行 | `orders`, `activities` | `issues`, `products` | -| 优惠券效能 | `user_coupons`, `coupons` | `orders` | -| 库存预警 | `issues`, `prizes` | `products` | -| 风险事件 | `draw_logs`, `users` | `user_login_logs` (如存在) | -| 积分经济 | `user_points_logs` | `users` | -| 实时中奖 | `draw_logs` | `users`, `prizes` | - -### 2.3 接口设计原则 -- 保持与现有接口风格一致 -- 使用 `rangeType` 参数统一时间范围过滤 -- 金额单位统一为**分** -- 百分比保留2位小数 - ---- - -## 三、验收标准 - -### 功能验收 -- [ ] 所有8个接口正常返回数据 -- [ ] 响应格式与前端TypeScript类型定义一致 -- [ ] 时间范围过滤逻辑正确 - -### 性能验收 -- [ ] 单接口响应时间 < 500ms -- [ ] 无N+1查询问题 - -### 集成验收 -- [ ] 前端调用后端接口无报错 -- [ ] 工作台各模块正确展示真实数据 - ---- - -## 四、技术约束 - -1. **不引入新依赖**: 使用现有GORM查询 -2. **复用现有工具函数**: 如 `parseRange()`, `percentChange()` 等 -3. **统一错误处理**: 使用现有 `core.HandlerFunc` 模式 - ---- - -## 五、已解决的不确定性 - -基于现有代码分析: -- ✅ 积分日志表 `user_points_logs` 已存在,包含 `change_type` 字段可区分来源 -- ✅ 用户登录日志可通过 `draw_logs` 和 `orders` 推断活跃度 -- ✅ 库存阈值可通过 `prizes.quantity` 与剩余数量对比计算 - ---- - -## 六、实现优先级 - -| 优先级 | 接口 | 原因 | -|--------|------|------| -| P0 | 产品动销排行 | 经营大盘核心指标 | -| P0 | 积分经济总览+趋势 | 风控预警必需 | -| P1 | 优惠券效能 | 营销分析重要 | -| P1 | 库存预警 | 运营监控 | -| P2 | 风险事件 | 可先用简化版 | -| P2 | 实时中奖播报 | 可复用现有 `draw_stream` | diff --git a/docs/玩家管理Bug修复/ALIGNMENT_玩家管理Bug修复.md b/docs/玩家管理Bug修复/ALIGNMENT_玩家管理Bug修复.md deleted file mode 100644 index a2d7445..0000000 --- a/docs/玩家管理Bug修复/ALIGNMENT_玩家管理Bug修复.md +++ /dev/null @@ -1,95 +0,0 @@ -# 玩家管理Bug修复 - 需求对齐 - -## 项目上下文 -- **前端**: Vue 3 + TypeScript + Element Plus -- **后端**: Go (Gin框架) -- **关键文件**: - - 玩家列表: `web/admin/src/views/player-manage/index.vue` - - 分页Hook: `web/admin/src/hooks/core/useTable.ts` - - 用户详情抽屉: `web/admin/src/views/player-manage/modules/player-detail-drawer.vue` - - 用户盈亏图表: `web/admin/src/views/player-manage/modules/player-profit-loss-chart.vue` - - 活动分析抽屉: `web/admin/src/views/activity/manage/components/ActivityAnalysisDrawer.vue` - ---- - -## Bug列表 - -### Bug 1: 玩家列表分页失效 -**现象**: 点击下一页后会自动跳回第1页 - -**初步分析**: -- 玩家列表使用 `useTable` hook 管理分页 -- `handleCurrentChange` 函数会修改 `pagination.current` 并调用 `getData` -- 可能原因: - 1. `handleSearch` 函数在搜索时重置了页码但没有正确更新 - 2. `getDataDebounced` 使用的参数可能覆盖了新的页码值 - 3. 搜索参数和分页参数同步问题 - -**需要确认**: 具体是在什么场景下触发?是否有搜索条件? - ---- - -### Bug 2: 活动的游戏盈亏仪表盘 -**现象描述不清,需要澄清** - -**可能的理解**: -1. 需要在仪表盘(Dashboard)添加活动的游戏盈亏分析组件? -2. 现有的 `ActivityAnalysisDrawer` 有问题需要修复? -3. 需要一个全局的活动盈亏汇总仪表盘? - -**当前现有功能**: -- `ActivityAnalysisDrawer.vue`: 单个活动的数据分析抽屉,包含总营收、总成本、毛利润、参与人数等 - -**需要澄清**: 具体需要什么功能?是新增组件还是修复现有问题? - ---- - -### Bug 3: 用户盈亏分析需要明细 -**需求理解**: -- 当前 `player-profit-loss-chart.vue` 显示用户盈亏趋势图表和汇总数据 -- 需要增加订单明细列表,可以点击查看每笔订单的盈亏 -- 明细需包含: 道具卡、优惠券等使用情况 - -**当前支持**: -- 有资产分项概览: 商品产出、积分收益、道具卡价值、优惠券价值 -- 有趋势图表展示投入、产出、净盈亏 - -**待实现**: -- 盈亏明细列表(可分页、可搜索) -- 每条记录显示: 订单信息、支付金额、获得奖品价值、使用的优惠券/道具卡及其价值 - ---- - -### Bug 4: 用户资产加一个搜索 -**需求理解**: -- 在用户详情抽屉的"资产"Tab中增加搜索功能 -- 当前资产列表使用 `ArtDataListCard` 组件展示 - -**待实现**: -- 搜索框(按商品名称、订单号等搜索) -- 可能需要修改后端API支持搜索参数 - ---- - -## 疑问澄清 - -### 优先级问题 -1. **Bug 2** 描述不够清晰,需要进一步说明具体需求: - - 是否需要在主仪表盘添加新组件? - - 还是修复现有 `ActivityAnalysisDrawer` 的问题? - - 需要展示哪些数据? - -### 技术问题 -2. **Bug 1** 分页问题: - - 是否只在有搜索条件时出现? - - 是否与特定浏览器相关? - -### 范围确认 -3. **Bug 3** 盈亏明细: - - 明细是否需要导出功能? - - 是否需要按时间范围筛选? - - 每条明细需要展示哪些具体字段? - -4. **Bug 4** 资产搜索: - - 支持哪些搜索条件?商品名称?订单号? - - 是否需要后端支持模糊搜索? diff --git a/docs/玩家管理Bug修复/CONSENSUS_玩家管理Bug修复.md b/docs/玩家管理Bug修复/CONSENSUS_玩家管理Bug修复.md deleted file mode 100644 index 854292f..0000000 --- a/docs/玩家管理Bug修复/CONSENSUS_玩家管理Bug修复.md +++ /dev/null @@ -1,59 +0,0 @@ -# 玩家管理Bug修复 - 共识文档 - -## 需求确认 - -### Bug 1: 玩家列表分页失效 -**现象**: 点击下一页后自动跳回第1页 -**确认范围**: 在玩家列表页面点击分页控件的下一页时出现 - -### Bug 2: 活动游戏盈亏仪表盘 -**确认理解**: 需要修复/完善活动的盈亏分析功能 -- 目标:完善现有的 `ActivityAnalysisDrawer` 组件功能 - -### Bug 3: 用户盈亏分析明细 -**确认理解**: 在用户盈亏分析中增加订单级明细列表 -- 每条明细显示:订单信息、支付金额、获得奖品价值 -- 包含使用的优惠券、道具卡及其价值 - -### Bug 4: 用户资产搜索 -**确认理解**: 在用户详情抽屉的资产Tab中增加搜索功能 - ---- - -## 技术实现方案 - -### Bug 1 修复方案 -**根因分析**: -- 玩家管理页面使用 `useTable` hook 管理分页 -- `ArtTable` 组件通过 `pagination:current-change` 事件通知页码变化 -- 页面监听该事件调用 `handleCurrentChange` → `getData(params)` -- 问题可能出在 `useTable.ts` 中搜索参数和分页参数的同步逻辑 - -**修复方案**: -- 检查 `handleCurrentChange` 函数中页码参数的传递 -- 确保分页参数不被搜索参数覆盖 - -### Bug 2 修复方案 -**现有功能**: `ActivityAnalysisDrawer.vue` 已有活动数据分析功能 -**待完善**: 确认功能是否正常工作,是否需要增强 - -### Bug 3 实现方案 -**新增功能**: -1. 后端新增API: `GET /api/admin/users/{user_id}/profit_loss/details` - - 返回每笔订单的盈亏明细 - - 包含:订单信息、支付金额、获得价值、使用的优惠券/道具卡 -2. 前端增加明细列表组件 - -### Bug 4 实现方案 -**新增功能**: -1. 后端API修改: `GET /api/admin/users/{user_id}/inventory` 支持搜索参数 -2. 前端在资产Tab增加搜索框 - ---- - -## 验收标准 - -1. **Bug 1**: 玩家列表可以正常翻页,页码不会跳回第一页 -2. **Bug 2**: 活动盈亏仪表盘功能正常工作 -3. **Bug 3**: 用户盈亏分析页可以查看订单明细列表 -4. **Bug 4**: 用户资产列表可以按商品名称搜索 diff --git a/docs/玩家管理Bug修复/DESIGN_玩家管理Bug修复.md b/docs/玩家管理Bug修复/DESIGN_玩家管理Bug修复.md deleted file mode 100644 index 0290fbd..0000000 --- a/docs/玩家管理Bug修复/DESIGN_玩家管理Bug修复.md +++ /dev/null @@ -1,177 +0,0 @@ -# 玩家管理Bug修复 - 架构设计 - -## 整体架构图 - -```mermaid -graph TB - subgraph Frontend["前端 Vue3"] - PM[玩家管理页面] - PD[用户详情抽屉] - PL[盈亏分析组件] - AD["活动分析抽屉(已有)"] - end - - subgraph Backend["后端 Go/Gin"] - UA[users_admin.go] - UP[users_profit_loss.go] - AA[activities_admin.go] - end - - PM --> |分页请求| UA - PD --> |资产列表+搜索| UA - PL --> |盈亏趋势| UP - PL --> |盈亏明细| UP -``` - ---- - -## Bug 1: 分页修复 - -### 问题分析 -``` -player-manage/index.vue - └── useTable hook - ├── handleCurrentChange(newCurrent) - │ └── getData(params) // params中需要包含正确的page参数 - └── 问题:searchParams可能覆盖新的页码 -``` - -### 修复方案 -在 `index.vue` 中,`handleSearch` 函数调用 `getDataDebounced` 时传递了搜索参数,这可能导致分页参数被覆盖。 - -需要检查的代码路径: -1. `ArtTable` 组件发出 `pagination:current-change` 事件 -2. `useTable` 的 `handleCurrentChange` 接收新页码 -3. 确保页码正确传递给 API 请求 - ---- - -## Bug 2: 活动盈亏仪表盘 - -### 现有组件 -- `ActivityAnalysisDrawer.vue`: 单活动数据分析 -- 计算: 总营收、总成本、毛利润、参与人数 - -### 待确认/完善 -- 检查计算逻辑是否正确 -- 确保数据展示完整 - ---- - -## Bug 3: 用户盈亏明细 - -### 新增API - -#### 后端: `GET /api/admin/users/{user_id}/stats/profit_loss_details` - -**请求参数**: -```go -type profitLossDetailsRequest struct { - Page int `form:"page"` - PageSize int `form:"page_size"` - RangeType string `form:"rangeType"` // today, 7d, 30d, all -} -``` - -**响应结构**: -```go -type profitLossDetailItem struct { - OrderID int64 `json:"order_id"` - OrderNo string `json:"order_no"` - CreatedAt string `json:"created_at"` - ActualAmount int64 `json:"actual_amount"` // 实际支付金额(分) - RefundAmount int64 `json:"refund_amount"` // 退款金额(分) - NetCost int64 `json:"net_cost"` // 净投入(分) - PrizeValue int64 `json:"prize_value"` // 获得奖品价值(分) - PointsEarned int64 `json:"points_earned"` // 获得积分 - PointsValue int64 `json:"points_value"` // 积分价值(分) - CouponUsedValue int64 `json:"coupon_used_value"` // 使用优惠券价值(分) - ItemCardUsed string `json:"item_card_used"` // 使用的道具卡名称 - ItemCardValue int64 `json:"item_card_value"` // 道具卡价值(分) - NetProfit int64 `json:"net_profit"` // 净盈亏 - ActivityName string `json:"activity_name"` // 活动名称 - PrizeName string `json:"prize_name"` // 奖品名称 - SourceType int `json:"source_type"` // 来源类型 -} - -type profitLossDetailsResponse struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - Total int64 `json:"total"` - List []profitLossDetailItem `json:"list"` - Summary struct { - TotalCost int64 `json:"total_cost"` - TotalValue int64 `json:"total_value"` - TotalProfit int64 `json:"total_profit"` - } `json:"summary"` -} -``` - -### 前端组件修改 - -在 `player-profit-loss-chart.vue` 中增加: -1. "查看明细" 按钮 -2. 明细表格(支持分页) -3. 显示字段:时间、订单号、支付金额、获得价值、使用优惠券/道具卡、净盈亏 - ---- - -## Bug 4: 资产搜索 - -### 后端修改 - -修改 `users_admin.go` 中的 `ListUserInventory`: -```go -type listInventoryRequest struct { - Page int `form:"page"` - PageSize int `form:"page_size"` - Keyword string `form:"keyword"` // 新增:搜索关键词 -} -``` - -查询逻辑增加: -```go -if req.Keyword != "" { - query = query.Where(db.Products.Name.Like("%" + req.Keyword + "%")) -} -``` - -### 前端修改 - -在 `player-detail-drawer.vue` 资产Tab中: -1. 增加搜索输入框 -2. 调用API时传递 `keyword` 参数 - ---- - -## 数据流图 - -```mermaid -sequenceDiagram - participant User as 用户 - participant FE as 前端 - participant BE as 后端 - participant DB as 数据库 - - Note over User, DB: Bug 3: 盈亏明细查询 - User->>FE: 点击"查看明细" - FE->>BE: GET /users/:id/stats/profit_loss_details - BE->>DB: 查询订单+奖品+优惠券+道具卡 - DB-->>BE: 返回数据 - BE-->>FE: JSON响应 - FE-->>User: 展示明细列表 -``` - ---- - -## 文件变更清单 - -| 文件 | 变更类型 | 说明 | -|------|---------|------| -| `web/admin/src/views/player-manage/index.vue` | 修改 | 修复分页问题 | -| `web/admin/src/hooks/core/useTable.ts` | 检查 | 确认分页逻辑 | -| `web/admin/src/views/player-manage/modules/player-profit-loss-chart.vue` | 修改 | 添加明细列表 | -| `web/admin/src/views/player-manage/modules/player-detail-drawer.vue` | 修改 | 添加资产搜索 | -| `web/admin/src/api/player-manage.ts` | 修改 | 添加明细API调用 | -| `internal/api/admin/users_profit_loss.go` | 修改 | 添加明细API | -| `internal/api/admin/users_admin.go` | 修改 | 资产列表添加搜索 | diff --git a/docs/翻牌特效/ALIGNMENT_翻牌特效.md b/docs/翻牌特效/ALIGNMENT_翻牌特效.md deleted file mode 100644 index e157556..0000000 --- a/docs/翻牌特效/ALIGNMENT_翻牌特效.md +++ /dev/null @@ -1,121 +0,0 @@ -# 抖音游戏翻牌特效需求对齐 - -## 原始需求 - -用户希望在 `douyin_game` 项目中开发一个翻牌 Web 应用,参考泡泡玛特直播间的翻牌抽盒效果。 - -## 参考截图分析 - -![参考图1](/Users/win/.gemini/antigravity/brain/192e2707-e873-48c7-9161-73a09b835351/uploaded_image_0_1768026980546.jpg) -![参考图2](/Users/win/.gemini/antigravity/brain/192e2707-e873-48c7-9161-73a09b835351/uploaded_image_1_1768026980546.jpg) -![参考图3](/Users/win/.gemini/antigravity/brain/192e2707-e873-48c7-9161-73a09b835351/uploaded_image_2_1768026980546.jpg) - -### 核心功能分析 - -从截图中观察到以下特征: - -1. **卡片网格布局** - - 3x4 的卡片网格(共 12 张卡片) - - 每张卡片显示挂件产品图片 - - 绿色方格背景,白色卡片 - -2. **卡片状态** - - 未翻开状态:显示产品缩略图+用户头像+昵称+倒计时 - - 翻开后状态:大图展示产品详情 - -3. **翻牌特效**(图3展示) - - 深色星空背景 - - 星星闪烁粒子效果 - - 产品大图居中展示 - - 卡片 3D 翻转动画 - -4. **交互元素** - - 用户头像标识 - - 昵称显示 - - 抽取倒计时 - ---- - -## 边界确认 - -### 开发范围 - -- [x] 卡片网格布局 UI -- [x] 3D 翻牌动画效果 -- [x] 星空背景特效 -- [x] 粒子闪烁效果 -- [x] 产品大图展示遮罩层 - -### 排除范围 - -- [ ] 后端抽盒逻辑(已有) -- [ ] 支付流程 -- [ ] 用户身份认证 - ---- - -## 疑问澄清 - -> [!IMPORTANT] -> 以下问题需要用户确认 - -### 1. 项目位置 - -当前 `douyin_game` 目录为空,请确认: -- 是否在此目录新建独立项目? -- 还是集成到现有 `game/app` 项目中? - -### 2. 技术栈选择 - -现有项目使用 **React + TypeScript + Vite + TailwindCSS**: -- 是否沿用相同技术栈? -- 或者使用纯 HTML/CSS/JS 开发独立页面? - -### 3. 数据来源 - -翻牌游戏的数据(产品信息、用户信息等): -- 是否需要对接后端 API? -- 还是先开发静态演示版本? - -### 4. 翻牌触发方式 - -用户如何触发翻牌: -- 点击自己预定的卡片? -- 观看他人翻牌的直播效果? -- 两者结合? - -### 5. 特效细节偏好 - -关于"人物背后的翻牌特效",请确认: -- **星空背景**:是否需要动态渐变星空? -- **粒子效果**:闪烁星星数量和密度? -- **翻转动画**:水平翻转还是垂直翻转? -- **展示遮罩**:是否需要毛玻璃效果? - ---- - -## 技术理解 - -### 现有项目分析 - -项目 `game/app` 技术栈: -- React 19 + TypeScript -- Vite 构建工具 -- TailwindCSS 样式 -- 已有丰富的 CSS 动画效果(`Explosion.css`) - -### 翻牌特效技术方案 - -| 特效组件 | 技术实现 | -|---------|---------| -| 3D 翻牌动画 | CSS `transform: rotateY()` + `perspective` | -| 星空背景 | 深色渐变 + CSS `radial-gradient` | -| 星星闪烁 | CSS `@keyframes` 动画 + 随机延迟 | -| 粒子效果 | Canvas API 或 CSS 伪元素 | -| 遮罩层 | `backdrop-filter: blur()` 毛玻璃效果 | - ---- - -## 等待用户回复 - -上述疑问需要用户回复后才能进入架构设计阶段。 diff --git a/docs/翻牌特效/CONSENSUS_翻牌特效.md b/docs/翻牌特效/CONSENSUS_翻牌特效.md deleted file mode 100644 index 7b9541c..0000000 --- a/docs/翻牌特效/CONSENSUS_翻牌特效.md +++ /dev/null @@ -1,27 +0,0 @@ -# 翻牌特效项目共识 (CONSENSUS) - -## 需求描述 -开发一个基于 React 的翻牌 Web 应用,模拟抽盒机的翻牌流程及特效。重点在于 3D 翻牌动画、星空粒子背景以及整体视觉体验。 - -## 验收标准 -1. **网格布局**:实现 3x4 的响应式卡片网格。 -2. **3D 翻牌**:卡片点击后执行平滑的 3D 翻转动画。 -3. **特效层**:翻牌时伴随全屏星空背景和闪烁粒子特效。 -4. **大图展示**:翻牌后产品大图居中弹出,具备毛玻璃遮罩。 -5. **静态数据**:使用 Mock 数据驱动,包含产品图片、用户头像、昵称、倒计时。 - -## 技术方案 -- **框架**:React 19 + TypeScript -- **构建工具**:Vite -- **样式**:TailwindCSS + CSS Modules/Raw CSS (用于复杂动画) -- **动效库**:Framer Motion (可选,若需更细腻控制) 或 纯 CSS 3D Transforms。 - -## 技术约束 -- 纯前端实现,暂不对接后端接口。 -- 代码部署在 `douyin_game` 目录下。 - -## 集成方案 -- 作为一个独立项目在 `douyin_game` 中进行初始化。 - -## 风险与假设 -- 所有图像资源(产品图、头像)暂时使用 generate_image 工具生成的占位图或默认素材。 diff --git a/docs/翻牌特效/DESIGN_翻牌特效.md b/docs/翻牌特效/DESIGN_翻牌特效.md deleted file mode 100644 index 22c58c0..0000000 --- a/docs/翻牌特效/DESIGN_翻牌特效.md +++ /dev/null @@ -1,53 +0,0 @@ -# 翻牌特效架构设计 (DESIGN) - -## 整体架构 -项目采用单页应用架构,通过 React 状态驱动 UI 更新和动效触发。 - -```mermaid -graph TD - App[App Component] --> GameState[Game State: revealedCards, selectedId] - App --> StarLayer[StarryBackground Component] - App --> Grid[CardGrid Component] - Grid --> Card[FlipCard Component] - Card --> CardUI[Front: Avatar/Info | Back: ProductImg] - App --> Modal[ProductModal Component] -``` - -## 核心组件设计 - -### 1. FlipCard (翻牌组件) -- **Props**: `id`, `user`, `product`, `isRevealed`, `onFlip`. -- **CSS**: - - `.card-inner`: `transition: transform 0.6s; transform-style: preserve-3d;` - - `.card-front`, `.card-back`: `backface-visibility: hidden;` - -### 2. StarryBackground (星空背景) -- 实现多层叠加背景: - - 底层:深蓝色渐变 `#0a0b1e` -> `#161b33`。 - - 中层:静态微小星星(CSS 粒状纹理)。 - - 高层:关键帧动画模拟的闪烁星星(不同大小、延时)。 - -### 3. ProductModal (展示遮罩) -- 当 `selectedId` 存在时显示。 -- **Style**: `fixed inset-0`, `backdrop-filter: blur(8px)`, `bg-black/40`。 -- **Animation**: 放大缩放并带有一圈光晕特效。 - -## 实现细节:粒子特效 -当翻牌触发时,在卡片位置生成一组临时的粒子元素: -- 随机方向发射。 -- 逐渐变小并透明。 -- 使用 React `useState` 管理粒子生命周期。 - -## 目录结构 (douyin_game) -```text -src/ - components/ - CardGrid.tsx - FlipCard.tsx - StarryBackground.tsx - ProductModal.tsx - assets/ - images/ - App.tsx - index.css -``` diff --git a/docs/翻牌特效/TASK_翻牌特效.md b/docs/翻牌特效/TASK_翻牌特效.md deleted file mode 100644 index 4250bb7..0000000 --- a/docs/翻牌特效/TASK_翻牌特效.md +++ /dev/null @@ -1,37 +0,0 @@ -# 翻牌特效原子任务 (TASK) - -## 任务依赖图 -```mermaid -graph TD - T1[T1: 初始化项目环境] --> T2[T2: 实现星空背景层] - T2 --> T3[T3: 开发 3D 翻牌组件] - T3 --> T4[T4: 组装网格与逻辑控制] - T4 --> T5[T5: 完善大图展示与粒子特效] -``` - -## 原子任务定义 - -### T1: 初始化项目环境 -- **输入**: 空目录 `douyin_game` -- **输出**: Vite + React 项目骨架,安装 TailwindCSS -- **验收**: `npm run dev` 可正常启动 - -### T2: 实现星空背景层 (StarryBackground) -- **输入**: Tailwind 配置 -- **输出**: 一个全屏背景组件,带有动态闪烁星星 -- **验收**: 背景显示深蓝渐变,星星随机分布且有呼吸感 - -### T3: 开发 3D 翻牌组件 (FlipCard) -- **输入**: 基础 CSS 3D 知识 -- **输出**: 支持正面(用户信息)和背面(产品图)切换的卡片 -- **验收**: 点击触发平滑翻转,背面图片居中 - -### T4: 组装网格与逻辑控制 (CardGrid) -- **输入**: 3x4 布局需求 -- **输出**: 一个包含 12 张卡片的网格,支持单次点击状态管理 -- **验收**: 点击不同卡片各自翻转 - -### T5: 完善大图展示与粒子特效 (ProductModal) -- **输入**: 翻牌触发回调 -- **输出**: 点击翻牌后弹出居中大图,背景变暗且带粒子飞散 -- **验收**: 展示效果震撼,符合泡泡玛特直播间风格 diff --git a/scripts/check_coupon.go b/scripts/check_coupon.go deleted file mode 100644 index 003201a..0000000 --- a/scripts/check_coupon.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - // 连接数据库 - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?parseTime=true" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal("连接失败:", err) - } - defer db.Close() - - fmt.Println("=== 优惠券 ID 275 当前状态 ===") - var ucID, userID, couponID int64 - var status int32 - var balanceAmount int64 - var usedOrderID sql.NullInt64 - var usedAt sql.NullTime - err = db.QueryRow("SELECT id, user_id, coupon_id, status, balance_amount, used_order_id, used_at FROM user_coupons WHERE id = 275").Scan( - &ucID, &userID, &couponID, &status, &balanceAmount, &usedOrderID, &usedAt, - ) - if err != nil { - log.Println("查询优惠券失败:", err) - } else { - fmt.Printf("用户券ID: %d | 用户ID: %d | 模板ID: %d | 状态: %d | 余额(分): %d | 使用订单ID: %v | 使用时间: %v\n", - ucID, userID, couponID, status, balanceAmount, usedOrderID, usedAt) - } - - // 查询系统券模板 - fmt.Println("\n=== 系统优惠券模板 ===") - var scID int64 - var scName string - var discountType int32 - var discountValue int64 - err = db.QueryRow("SELECT id, name, discount_type, discount_value FROM system_coupons WHERE id = ?", couponID).Scan(&scID, &scName, &discountType, &discountValue) - if err == nil { - fmt.Printf("模板ID: %d | 名称: %s | 类型: %d | 面值(分): %d\n", scID, scName, discountType, discountValue) - } - - fmt.Println("\n=== 优惠券 ID 275 的所有流水记录 ===") - rows, err := db.Query(` - SELECT id, user_id, user_coupon_id, change_amount, balance_after, order_id, action, created_at - FROM user_coupon_ledger - WHERE user_coupon_id = 275 - ORDER BY created_at DESC - `) - if err != nil { - log.Println("查询流水失败:", err) - } else { - defer rows.Close() - for rows.Next() { - var id, userID, userCouponID, changeAmount, balanceAfter, orderID int64 - var action string - var createdAt sql.NullTime - rows.Scan(&id, &userID, &userCouponID, &changeAmount, &balanceAfter, &orderID, &action, &createdAt) - fmt.Printf("流水ID: %d | 变动: %d分 | 余额: %d分 | 订单ID: %d | 动作: %s | 时间: %v\n", - id, changeAmount, balanceAfter, orderID, action, createdAt) - } - } - - fmt.Println("\n=== order_coupons 表中使用优惠券 275 的记录 ===") - rows2, err := db.Query(` - SELECT oc.id, oc.order_id, oc.user_coupon_id, oc.applied_amount, oc.created_at, - o.order_no, o.status, o.total_amount, o.discount_amount, o.actual_amount - FROM order_coupons oc - LEFT JOIN orders o ON o.id = oc.order_id - WHERE oc.user_coupon_id = 275 - ORDER BY oc.created_at DESC - `) - if err != nil { - log.Println("查询 order_coupons 失败:", err) - } else { - defer rows2.Close() - for rows2.Next() { - var id, orderID, userCouponID, appliedAmount int64 - var createdAt sql.NullTime - var orderNo sql.NullString - var orderStatus sql.NullInt32 - var totalAmount, discountAmount, actualAmount sql.NullInt64 - rows2.Scan(&id, &orderID, &userCouponID, &appliedAmount, &createdAt, &orderNo, &orderStatus, &totalAmount, &discountAmount, &actualAmount) - fmt.Printf("记录ID: %d | 订单ID: %d | 订单号: %v | 扣减金额: %d分 | 时间: %v\n", - id, orderID, orderNo.String, appliedAmount, createdAt) - } - } - - // 计算总扣减金额 - var totalApplied int64 - db.QueryRow("SELECT COALESCE(SUM(applied_amount), 0) FROM order_coupons WHERE user_coupon_id = 275").Scan(&totalApplied) - fmt.Printf("\n=== 统计 ===\n优惠券 275 累计扣减: %d 分 (%.2f 元)\n", totalApplied, float64(totalApplied)/100) - fmt.Printf("当前余额: %d 分 (%.2f 元)\n", balanceAmount, float64(balanceAmount)/100) -} diff --git a/scripts/check_db_schema.go b/scripts/check_db_schema.go deleted file mode 100644 index 514bf51..0000000 --- a/scripts/check_db_schema.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - rows, err := db.Query("DESCRIBE douyin_orders") - if err != nil { - log.Fatal(err) - } - defer rows.Close() - - fmt.Println("Table: douyin_orders") - fmt.Println("Field | Type | Null | Key | Default | Extra") - for rows.Next() { - var field, typ, null, key, extra string - var def sql.NullString - err := rows.Scan(&field, &typ, &null, &key, &def, &extra) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%s | %s | %s | %s | %v | %s\n", field, typ, null, key, def.String, extra) - } -} diff --git a/scripts/check_duplicates.go b/scripts/check_duplicates.go deleted file mode 100644 index 5aee8e1..0000000 --- a/scripts/check_duplicates.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - // 1. 检查直播间抽奖是否有重复 (Draw Count > Product Count) - // 注意:shop_order_id 是字符串,join 时注意字符集,不过这里都是 utf8mb4 应该没问题 - query := ` - SELECT - o.shop_order_id, - o.product_count, - COUNT(l.id) as draw_count, - o.user_nickname - FROM douyin_orders o - JOIN livestream_draw_logs l ON CONVERT(o.shop_order_id USING utf8mb4) = CONVERT(l.shop_order_id USING utf8mb4) - GROUP BY o.shop_order_id, o.product_count, o.user_nickname - HAVING draw_count > o.product_count - ` - - rows, err := db.Query(query) - if err != nil { - log.Fatal(err) - } - defer rows.Close() - - fmt.Println("--- Duplicate Rewards Check (Livestream) ---") - found := false - for rows.Next() { - var orderID, nickname string - var pCount, dCount int - if err := rows.Scan(&orderID, &pCount, &dCount, &nickname); err != nil { - log.Fatal(err) - } - fmt.Printf("Order: %s | Nickname: %s | Bought: %d | Issued: %d\n", orderID, nickname, pCount, dCount) - found = true - } - - if !found { - fmt.Println("No duplicate rewards found in livestream_draw_logs.") - } - - // 2. 额外检查:是否有同一个 shop_order_id 在极短时间内产生多条 log (并发问题特质) - // 这里简单检查是否有完全重复的 log (除了主键不同,其他关键字段相同) - // 或者检查是否有订单在非直播抽奖表也发了奖 (如果两边系统混用) -} diff --git a/scripts/check_orders.go b/scripts/check_orders.go deleted file mode 100644 index 12622ff..0000000 --- a/scripts/check_orders.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - rows, err := db.Query("SELECT shop_order_id, actual_pay_amount, actual_receive_amount, raw_data FROM douyin_orders ORDER BY id DESC LIMIT 5") - if err != nil { - log.Fatal(err) - } - defer rows.Close() - - for rows.Next() { - var id string - var pay, recv int64 - var raw string - err := rows.Scan(&id, &pay, &recv, &raw) - if err != nil { - log.Fatal(err) - } - fmt.Printf("ID: %s | Pay(DB): %d | Recv(DB): %d\nRaw: %s\n\n", id, pay, recv, raw) - } -} diff --git a/scripts/debug_matching_order.go b/scripts/debug_matching_order.go deleted file mode 100644 index e0d0f87..0000000 --- a/scripts/debug_matching_order.go +++ /dev/null @@ -1,311 +0,0 @@ -package main - -import ( - "crypto/hmac" - "crypto/sha256" - "database/sql" - "encoding/binary" - "encoding/hex" - "fmt" - "log" - "time" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?parseTime=true" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal("Connection failed:", err) - } - defer db.Close() - - orderNo := "O20260125201015731" - fmt.Printf("Querying Order: %s\n", orderNo) - - var id, userID int64 - var status, sourceType int32 - var total, discount, actual int64 - var remark, createdAt string - - // Check Order - err = db.QueryRow("SELECT id, user_id, status, source_type, total_amount, discount_amount, actual_amount, remark, created_at FROM orders WHERE order_no = ?", orderNo). - Scan(&id, &userID, &status, &sourceType, &total, &discount, &actual, &remark, &createdAt) - - if err != nil { - log.Fatalf("Order not found: %v", err) - } - - fmt.Printf("ID: %d\nUser: %d\nStatus: %d (1=Pending, 2=Paid)\nSourceType: %d\nTotal: %d\nDiscount: %d\nActual: %d\nRemark: %s\nCreated: %s\n", - id, userID, status, sourceType, total, discount, actual, remark, createdAt) - - // Check Draw Logs - rows, err := db.Query("SELECT id, reward_id, is_winner, draw_index, created_at FROM activity_draw_logs WHERE order_id = ?", id) - if err != nil { - log.Fatal("Query logs failed:", err) - } - defer rows.Close() - - fmt.Println("\n--- Draw Logs ---") - count := 0 - for rows.Next() { - var logID, rID, dIdx int64 - var isWinner int32 - var ca time.Time - rows.Scan(&logID, &rID, &isWinner, &dIdx, &ca) - fmt.Printf("LogID: %d, RewardID: %d, IsWinner: %d, DrawIndex: %d, Time: %s\n", logID, rID, isWinner, dIdx, ca) - count++ - } - if count == 0 { - fmt.Println("No draw logs found.") - } - - // Check Inventory (Grants) - rowsInv, err := db.Query("SELECT id, reward_id, product_id, status FROM user_inventory WHERE order_id = ?", id) - if err != nil { - log.Fatal("Query inventory failed:", err) - } - defer rowsInv.Close() - - fmt.Println("\n--- Inventory (Grants) ---") - invCount := 0 - for rowsInv.Next() { - var invID, rID, pID int64 - var s int - rowsInv.Scan(&invID, &rID, &pID, &s) - fmt.Printf("InvID: %d, RewardID: %d, ProductID: %d, Status: %d\n", invID, rID, pID, s) - invCount++ - } - - if invCount == 0 { - fmt.Println("No inventory grants found.") - } - - // Check Issue 104 - fmt.Println("\n--- Issue 104 ---") - var actID int64 - var issueNumber string - err = db.QueryRow("SELECT activity_id, issue_number FROM activity_issues WHERE id = 104").Scan(&actID, &issueNumber) - if err != nil { - fmt.Printf("Issue query failed: %v\n", err) - } else { - fmt.Printf("Issue Number: %s, ActivityID: %d\n", issueNumber, actID) - // Query Activity by ID - var actName, playType string - err = db.QueryRow("SELECT name, play_type FROM activities WHERE id = ?", actID).Scan(&actName, &playType) - if err != nil { - fmt.Printf("Activity query failed: %v\n", err) - } else { - fmt.Printf("Activity: %s, PlayType: %s\n", actName, playType) - - // Reconstruct Game - fmt.Println("\n--- Reconstructing Game ---") - - // Fetch Draw Log - var drawLogID, issueID int64 - err = db.QueryRow("SELECT id, issue_id FROM activity_draw_logs WHERE order_id = ?", id).Scan(&drawLogID, &issueID) - if err != nil { - log.Fatal("Draw log not found:", err) - } - - // Fetch Receipt - var subSeedHex, position string - err = db.QueryRow("SELECT server_sub_seed, client_seed FROM activity_draw_receipts WHERE draw_log_id = ?", drawLogID).Scan(&subSeedHex, &position) - if err != nil { - log.Printf("Receipt not found: %v", err) - return - } - - fmt.Printf("Receipt Found. SubSeed: %s, Position (ClientSeed): %s\n", subSeedHex, position) - - serverSeed, err := hex.DecodeString(subSeedHex) - if err != nil { - log.Fatal("Invalid seed hex:", err) - } - - // Fetch Card Configs - rowsCards, err := db.Query("SELECT code, name, quantity FROM matching_card_types WHERE status=1 ORDER BY sort ASC") - if err != nil { - log.Fatal("Card types query failed:", err) - } - defer rowsCards.Close() - - type CardTypeConfig struct { - Code string - Name string - Quantity int32 - } - var configs []CardTypeConfig - for rowsCards.Next() { - var c CardTypeConfig - rowsCards.Scan(&c.Code, &c.Name, &c.Quantity) - configs = append(configs, c) - } - - // Create Game Logic - fmt.Println("Simulating Game Logic...") - - cardIDCounter := int64(0) - type MatchingCard struct { - ID string - Type string - } - - var deck []*MatchingCard - for _, cfg := range configs { - for i := int32(0); i < cfg.Quantity; i++ { - cardIDCounter++ - deck = append(deck, &MatchingCard{ - ID: fmt.Sprintf("c%d", cardIDCounter), - Type: cfg.Code, - }) - } - } - - // SecureShuffle - secureRandInt := func(max int, context string, nonce *int64) int { - *nonce++ - message := fmt.Sprintf("%s|nonce:%d", context, *nonce) - mac := hmac.New(sha256.New, serverSeed) - mac.Write([]byte(message)) - sum := mac.Sum(nil) - val := binary.BigEndian.Uint64(sum[:8]) - return int(val % uint64(max)) - } - - nonce := int64(0) - n := len(deck) - for i := n - 1; i > 0; i-- { - j := secureRandInt(i+1, fmt.Sprintf("shuffle:%d", i), &nonce) - deck[i], deck[j] = deck[j], deck[i] - } - - // Distribute to Board (first 9) - board := make([]*MatchingCard, 9) - for i := 0; i < 9; i++ { - if len(deck) > 0 { - board[i] = deck[0] - deck = deck[1:] - } - } - - fmt.Printf("Board Types: ") - for _, c := range board { - if c != nil { - fmt.Printf("%s ", c.Type) - } - } - fmt.Println() - - // SimulateMaxPairs - // Reconstruct allCards (Board + Deck) - allCards := make([]*MatchingCard, 0, len(board)+len(deck)) - for _, c := range board { - if c != nil { - allCards = append(allCards, c) - } - } - allCards = append(allCards, deck...) - - selectedType := position - hand := make([]*MatchingCard, 9) - copy(hand, allCards[:9]) - deckIndex := 9 - chance := int64(0) - for _, c := range hand { - if c != nil && c.Type == selectedType { - chance++ - } - } - - fmt.Printf("Selected Type: %s, Initial Chance: %d\n", selectedType, chance) - - totalPairs := int64(0) - - guard := 0 - for guard < 1000 { - guard++ - - // canEliminate - counts := make(map[string]int) - pairType := "" - for _, c := range hand { - if c == nil { - continue - } - counts[c.Type]++ - if counts[c.Type] >= 2 { - pairType = c.Type - break - } - } - - if pairType != "" { - // Eliminate - first, second := -1, -1 - for i, c := range hand { - if c == nil || c.Type != pairType { - continue - } - if first < 0 { - first = i - } else { - second = i - break - } - } - if first >= 0 && second >= 0 { - newHand := make([]*MatchingCard, 0, len(hand)-2) - for i, c := range hand { - if i != first && i != second { - newHand = append(newHand, c) - } - } - hand = newHand - totalPairs++ - chance++ - continue - } - } - - // Draw - if chance > 0 && deckIndex < len(allCards) { - newCard := allCards[deckIndex] - hand = append(hand, newCard) - deckIndex++ - chance-- - continue - } - - break - } - - fmt.Printf("Simulation Finished. Total Pairs: %d\n", totalPairs) - - // Check Rewards - rowsRewards, err := db.Query("SELECT id, min_score, product_id, quantity, level FROM activity_reward_settings WHERE issue_id = ?", issueID) - if err != nil { - log.Printf("Query rewards failed: %v", err) - } else { - defer rowsRewards.Close() - fmt.Println("\n--- Matching Rewards ---") - found := false - for rowsRewards.Next() { - var rwID, minScore, pID int64 - var qty, level int32 - rowsRewards.Scan(&rwID, &minScore, &pID, &qty, &level) - if int64(minScore) == totalPairs { - var pName string - _ = db.QueryRow("SELECT name FROM products WHERE id=?", pID).Scan(&pName) - fmt.Printf("MATCH! RewardID: %d, ProductID: %d (%s), Qty: %d, Level: %d\n", rwID, pID, pName, qty, level) - found = true - } - } - if !found { - fmt.Println("No matching reward found for this score.") - } - } - } - } -} diff --git a/scripts/fix_db_columns.go b/scripts/fix_db_columns.go deleted file mode 100644 index 58c709e..0000000 --- a/scripts/fix_db_columns.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - sqls := []string{ - "ALTER TABLE douyin_orders ADD COLUMN douyin_product_id VARCHAR(128) COMMENT '关联商品ID' AFTER shop_order_id", - "ALTER TABLE douyin_orders ADD COLUMN product_count BIGINT NOT NULL DEFAULT 1 COMMENT '商品数量' AFTER douyin_product_id", - "ALTER TABLE douyin_orders ADD COLUMN actual_pay_amount BIGINT DEFAULT 0 COMMENT '实付金额(分)' AFTER actual_receive_amount", - "ALTER TABLE douyin_orders ADD COLUMN reward_granted INT NOT NULL DEFAULT 0 COMMENT '奖励已发放' AFTER raw_data", - } - - for _, s := range sqls { - fmt.Printf("Executing: %s\n", s) - _, err := db.Exec(s) - if err != nil { - fmt.Printf("Error executing %s: %v\n", s, err) - } else { - fmt.Println("Success") - } - } -} diff --git a/scripts/inspect_column_names.go b/scripts/inspect_column_names.go deleted file mode 100644 index 17c1528..0000000 --- a/scripts/inspect_column_names.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - rows, err := db.Query("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME='douyin_orders' AND TABLE_SCHEMA='bindbox_game'") - if err != nil { - log.Fatal(err) - } - defer rows.Close() - - fmt.Println("Columns in douyin_orders:") - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - log.Fatal(err) - } - fmt.Printf("[%s] (len:%d)\n", name, len(name)) - } -} diff --git a/scripts/list_tables.go b/scripts/list_tables.go deleted file mode 100644 index bd2cbad..0000000 --- a/scripts/list_tables.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - rows, err := db.Query("SHOW TABLES") - if err != nil { - log.Fatal(err) - } - defer rows.Close() - - fmt.Println("Tables in bindbox_game:") - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - log.Fatal(err) - } - fmt.Println(name) - } -} diff --git a/scripts/query_user.go b/scripts/query_user.go deleted file mode 100644 index c2bc0b5..0000000 --- a/scripts/query_user.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?parseTime=true" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal("Connection failed:", err) - } - defer db.Close() - - userID := 9116 - var nickname, mobile string - err = db.QueryRow("SELECT nickname, mobile FROM users WHERE id = ?", userID).Scan(&nickname, &mobile) - if err != nil { - log.Fatal("Query failed:", err) - } - fmt.Printf("User ID: %d\nNickname: %s\nMobile: %s\n", userID, nickname, mobile) -} diff --git a/scripts/read_raw_order.go b/scripts/read_raw_order.go deleted file mode 100644 index 74932c0..0000000 --- a/scripts/read_raw_order.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - shopOrderID := "6950061667188872453" - var rawData string - err = db.QueryRow("SELECT raw_data FROM douyin_orders WHERE shop_order_id=?", shopOrderID).Scan(&rawData) - if err != nil { - log.Fatal(err) - } - - fmt.Println("Raw Data for Order 6950061667188872453:") - fmt.Println(rawData) -} diff --git a/scripts/test_coupon_prededuct/main.go b/scripts/test_coupon_prededuct/main.go deleted file mode 100644 index 9e7e1c2..0000000 --- a/scripts/test_coupon_prededuct/main.go +++ /dev/null @@ -1,362 +0,0 @@ -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) - } -} diff --git a/scripts/test_order_no.go b/scripts/test_order_no.go deleted file mode 100644 index 9aecdca..0000000 --- a/scripts/test_order_no.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "fmt" - "math/rand" - "time" -) - -func generateOrderNo() string { - // 使用当前时间戳 + 随机数生成订单号 - // 格式:RG + 年月日时分秒 + 6位随机数 - r := rand.New(rand.NewSource(time.Now().UnixNano())) - return fmt.Sprintf("RG%s%06d", - time.Now().Format("20060102150405"), - r.Intn(1000000), - ) -} - -func main() { - for i := 0; i < 10; i++ { - fmt.Println(generateOrderNo()) - time.Sleep(1 * time.Millisecond) - } -} diff --git a/scripts/test_update.go b/scripts/test_update.go deleted file mode 100644 index 5cd5974..0000000 --- a/scripts/test_update.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - // 尝试更新一个已存在的订单 (从日志中找一个 ID) - shopOrderID := "6950057259045885189" - res, err := db.Exec("UPDATE douyin_orders SET actual_pay_amount=100, actual_receive_amount=100 WHERE shop_order_id=?", shopOrderID) - if err != nil { - fmt.Printf("Update failed: %v\n", err) - } else { - affected, _ := res.RowsAffected() - fmt.Printf("Update success, rows affected: %d\n", affected) - } -} diff --git a/scripts/verify_coupon_fix.go b/scripts/verify_coupon_fix.go deleted file mode 100644 index efe0f1f..0000000 --- a/scripts/verify_coupon_fix.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - // 连接数据库 - dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?parseTime=true" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatal("连接失败:", err) - } - defer db.Close() - - // 1. 重置优惠券状态用于测试 - // 注意:这里为了不破坏现场,我们创建一个新的优惠券用于测试 - // 或者,如果用户允许,我们可以修复已经超额的优惠券? - // 这里我们只观察,不测试下单(因为需要调用 API)。 - // 但我们可以手动验证 "Mock" ApplyCouponWithCap 逻辑。 - - fmt.Println("此脚本仅打印当前优惠券 275 的状态,请手动进行下单测试验证修复效果。") - - // 查询优惠券状态 - var bal int64 - var status int32 - err = db.QueryRow("SELECT balance_amount, status FROM user_coupons WHERE id = 275").Scan(&bal, &status) - if err != nil { - log.Fatal(err) - } - fmt.Printf("当前优惠券余额: %d 分, 状态: %d\n", bal, status) - - if bal > 0 { - fmt.Println("余额 > 0,正常情况。请尝试下单直到余额耗尽,再次下单应失败或不使用优惠券。") - } else { - fmt.Println("余额 <= 0,修复前这会导致重新使用 5000 面值。修复后应该无法使用。") - } -}