fix:盈亏计算
This commit is contained in:
parent
5ad2f4ace3
commit
6d33cc7fd0
9
.gitignore
vendored
9
.gitignore
vendored
@ -27,3 +27,12 @@ go.work.sum
|
||||
resources/*
|
||||
build/resources/admin/
|
||||
logs/
|
||||
|
||||
# 敏感配置文件
|
||||
configs/*.toml
|
||||
!configs/*.example.toml
|
||||
|
||||
# 环境变量
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
0
bindbox_game.db
Normal file
0
bindbox_game.db
Normal file
47
cmd/9090_audit/main.go
Normal file
47
cmd/9090_audit/main.go
Normal file
@ -0,0 +1,47 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
cmd/activity_repair/main.go
Normal file
85
cmd/activity_repair/main.go
Normal file
@ -0,0 +1,85 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
cmd/apply_migration/main.go
Normal file
28
cmd/apply_migration/main.go
Normal file
@ -0,0 +1,28 @@
|
||||
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.")
|
||||
}
|
||||
}
|
||||
43
cmd/audit_cloud_db/main.go
Normal file
43
cmd/audit_cloud_db/main.go
Normal file
@ -0,0 +1,43 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
35
cmd/check_activity/main.go
Normal file
35
cmd/check_activity/main.go
Normal file
@ -0,0 +1,35 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
33
cmd/check_data_state/main.go
Normal file
33
cmd/check_data_state/main.go
Normal file
@ -0,0 +1,33 @@
|
||||
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)
|
||||
}
|
||||
28
cmd/check_index/main.go
Normal file
28
cmd/check_index/main.go
Normal file
@ -0,0 +1,28 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
85
cmd/check_refunds/main.go
Normal file
85
cmd/check_refunds/main.go
Normal file
@ -0,0 +1,85 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
38
cmd/debug_9090_coupons/main.go
Normal file
38
cmd/debug_9090_coupons/main.go
Normal file
@ -0,0 +1,38 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
32
cmd/debug_all_coupons/main.go
Normal file
32
cmd/debug_all_coupons/main.go
Normal file
@ -0,0 +1,32 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
44
cmd/debug_balance/main.go
Normal file
44
cmd/debug_balance/main.go
Normal file
@ -0,0 +1,44 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
50
cmd/debug_card/main.go
Normal file
50
cmd/debug_card/main.go
Normal file
@ -0,0 +1,50 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
95
cmd/debug_coupon/main.go
Normal file
95
cmd/debug_coupon/main.go
Normal file
@ -0,0 +1,95 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
cmd/debug_inventory/main.go
Normal file
48
cmd/debug_inventory/main.go
Normal file
@ -0,0 +1,48 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
72
cmd/debug_inventory_verify/main.go
Normal file
72
cmd/debug_inventory_verify/main.go
Normal file
@ -0,0 +1,72 @@
|
||||
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)
|
||||
}
|
||||
29
cmd/debug_ledger/main.go
Normal file
29
cmd/debug_ledger/main.go
Normal file
@ -0,0 +1,29 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
29
cmd/debug_ledger_full/main.go
Normal file
29
cmd/debug_ledger_full/main.go
Normal file
@ -0,0 +1,29 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
102
cmd/debug_query/main.go
Normal file
102
cmd/debug_query/main.go
Normal file
@ -0,0 +1,102 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,113 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bindbox-game/configs"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"flag"
|
||||
"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)
|
||||
}
|
||||
|
||||
userID := int64(9082) // User from the report
|
||||
|
||||
// 1. Check Orders (ALL Status)
|
||||
var orders []model.Orders
|
||||
if err := db.Where("user_id = ?", userID).Find(&orders).Error; err != nil {
|
||||
log.Printf("Error querying orders: %v", err)
|
||||
}
|
||||
|
||||
var totalAmount int64
|
||||
var discountAmount int64
|
||||
var pointsAmount int64
|
||||
|
||||
fmt.Printf("--- ALL Orders for User %d ---\n", userID)
|
||||
for _, o := range orders {
|
||||
fmt.Printf("ID: %d, OrderNo: %s, Status: %d, Total: %d, Actual: %d, Discount: %d, Points: %d, Source: %d\n",
|
||||
o.ID, o.OrderNo, o.Status, o.TotalAmount, o.ActualAmount, o.DiscountAmount, o.PointsAmount, o.SourceType)
|
||||
if o.Status == 2 { // Only count Paid for Spending simulation (if that's the logic)
|
||||
totalAmount += o.TotalAmount
|
||||
discountAmount += o.DiscountAmount
|
||||
pointsAmount += o.PointsAmount
|
||||
}
|
||||
}
|
||||
fmt.Printf("Total Points (Status 2): %d\n", pointsAmount)
|
||||
|
||||
// 1.5 Check Points Ledger (Redemptions)
|
||||
var ledgers []model.UserPointsLedger
|
||||
if err := db.Where("user_id = ? AND action = ?", userID, "redeem_reward").Find(&ledgers).Error; err != nil {
|
||||
log.Printf("Error querying ledgers: %v", err)
|
||||
}
|
||||
var totalRedeemedPoints int64
|
||||
fmt.Printf("\n--- Points Redemption (Decomposition) ---\n")
|
||||
for _, l := range ledgers {
|
||||
fmt.Printf("ID: %d, Points: %d, Remark: %s, CreatedAt: %v\n", l.ID, l.Points, l.Remark, l.CreatedAt)
|
||||
totalRedeemedPoints += l.Points
|
||||
}
|
||||
fmt.Printf("Total Redeemed Points: %d\n", totalRedeemedPoints)
|
||||
|
||||
// 2. Check Inventory (Output)
|
||||
type InvItem struct {
|
||||
ID int64
|
||||
ProductID int64
|
||||
Status int32
|
||||
Price int64
|
||||
Name string
|
||||
Remark string // Added Remark field
|
||||
}
|
||||
var invItems []InvItem
|
||||
|
||||
// Show ALL status
|
||||
err = db.Table("user_inventory").
|
||||
Select("user_inventory.id, user_inventory.product_id, user_inventory.status, user_inventory.remark, products.price, products.name").
|
||||
Joins("JOIN products ON products.id = user_inventory.product_id").
|
||||
Where("user_inventory.user_id = ?", userID).
|
||||
Where("user_inventory.remark NOT LIKE ? AND user_inventory.remark NOT LIKE ?", "%redeemed%", "%void%").
|
||||
Scan(&invItems).Error
|
||||
if err != nil {
|
||||
log.Printf("Error querying inventory: %v", err)
|
||||
}
|
||||
|
||||
var totalPrizeValue int64
|
||||
var status1Value int64
|
||||
var status2Value int64
|
||||
var status3Value int64
|
||||
|
||||
fmt.Printf("\n--- Inventory (ALL Status) for User %d ---\n", userID)
|
||||
for _, item := range invItems {
|
||||
fmt.Printf("InvID: %d, ProductID: %d, Name: %s, Price: %d, Status: %d, Remark: %s\n",
|
||||
item.ID, item.ProductID, item.Name, item.Price, item.Status, item.Remark)
|
||||
|
||||
if item.Status == 1 || item.Status == 3 {
|
||||
totalPrizeValue += item.Price
|
||||
}
|
||||
if item.Status == 1 {
|
||||
status1Value += item.Price
|
||||
}
|
||||
if item.Status == 2 {
|
||||
status2Value += item.Price
|
||||
}
|
||||
if item.Status == 3 {
|
||||
status3Value += item.Price
|
||||
}
|
||||
}
|
||||
fmt.Printf("Status 1 (Holding) Value: %d\n", status1Value)
|
||||
fmt.Printf("Status 2 (Void/Decomposed) Value: %d\n", status2Value)
|
||||
fmt.Printf("Status 3 (Shipped/Used) Value: %d\n", status3Value)
|
||||
fmt.Printf("Total Effective Prize Value (1+3): %d\n", totalPrizeValue)
|
||||
|
||||
// 3. Calculate Profit
|
||||
profit := totalAmount - totalPrizeValue - discountAmount
|
||||
fmt.Printf("\n--- Calculation ---\n")
|
||||
fmt.Printf("Profit = Spending (%d) - PrizeValue (%d) - Discount (%d) = %d\n",
|
||||
totalAmount, totalPrizeValue, discountAmount, profit)
|
||||
|
||||
fmt.Printf("Formatted:\nSpending: %.2f\nOutput: %.2f\nProfit: %.2f\n", float64(totalAmount)/100, float64(totalPrizeValue)/100, float64(profit)/100)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
35
cmd/debug_usage_detail/main.go
Normal file
35
cmd/debug_usage_detail/main.go
Normal file
@ -0,0 +1,35 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
29
cmd/debug_user_search/main.go
Normal file
29
cmd/debug_user_search/main.go
Normal file
@ -0,0 +1,29 @@
|
||||
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)
|
||||
}
|
||||
40
cmd/find_bad_coupons/main.go
Normal file
40
cmd/find_bad_coupons/main.go
Normal file
@ -0,0 +1,40 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
42
cmd/find_live_activity/main.go
Normal file
42
cmd/find_live_activity/main.go
Normal file
@ -0,0 +1,42 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
36
cmd/fix_order/main.go
Normal file
36
cmd/fix_order/main.go
Normal file
@ -0,0 +1,36 @@
|
||||
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).")
|
||||
}
|
||||
}
|
||||
41
cmd/full_9090_dump/main.go
Normal file
41
cmd/full_9090_dump/main.go
Normal file
@ -0,0 +1,41 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
73
cmd/inspect_order/main.go
Normal file
73
cmd/inspect_order/main.go
Normal file
@ -0,0 +1,73 @@
|
||||
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)
|
||||
}
|
||||
32
cmd/inspect_order_4746/main.go
Normal file
32
cmd/inspect_order_4746/main.go
Normal file
@ -0,0 +1,32 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
81
cmd/master_reconcile/main.go
Normal file
81
cmd/master_reconcile/main.go
Normal file
@ -0,0 +1,81 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
cmd/reconcile_coupons/main.go
Normal file
85
cmd/reconcile_coupons/main.go
Normal file
@ -0,0 +1,85 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
186
cmd/simulate_test/main.go
Normal file
186
cmd/simulate_test/main.go
Normal file
@ -0,0 +1,186 @@
|
||||
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).")
|
||||
}
|
||||
}
|
||||
42
cmd/trace_ledger_cloud/main.go
Normal file
42
cmd/trace_ledger_cloud/main.go
Normal file
@ -0,0 +1,42 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
70
cmd/verify_coupon_fix/main.go
Normal file
70
cmd/verify_coupon_fix/main.go
Normal file
@ -0,0 +1,70 @@
|
||||
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.")
|
||||
}
|
||||
}
|
||||
@ -3,13 +3,13 @@ local = 'zh-cn'
|
||||
|
||||
[mysql.read]
|
||||
addr = '150.158.78.154:3306'
|
||||
name = 'bindbox_game'
|
||||
name = 'dev_game'
|
||||
pass = 'bindbox2025kdy'
|
||||
user = 'root'
|
||||
|
||||
[mysql.write]
|
||||
addr = '150.158.78.154:3306'
|
||||
name = 'bindbox_game'
|
||||
name = 'dev_game'
|
||||
pass = 'bindbox2025kdy'
|
||||
user = 'root'
|
||||
|
||||
|
||||
5
go.mod
5
go.mod
@ -20,6 +20,7 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.15.0
|
||||
github.com/go-resty/resty/v2 v2.10.0
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/issue9/identicon/v2 v2.1.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
@ -62,6 +63,7 @@ require (
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/alicebob/miniredis/v2 v2.36.1 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
@ -81,8 +83,8 @@ require (
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-querystring v1.0.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@ -119,6 +121,7 @@ require (
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@ -96,6 +96,8 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI=
|
||||
github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
@ -204,6 +206,8 @@ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
@ -466,6 +470,8 @@ github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
|
||||
@ -39,28 +39,28 @@ type listActivitiesResponse struct {
|
||||
}
|
||||
|
||||
type activityDetailResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Name string `json:"name"`
|
||||
Banner string `json:"banner"`
|
||||
ActivityCategoryID int64 `json:"activity_category_id"`
|
||||
Status int32 `json:"status"`
|
||||
PriceDraw int64 `json:"price_draw"`
|
||||
IsBoss int32 `json:"is_boss"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
DrawMode string `json:"draw_mode"`
|
||||
PlayType string `json:"play_type"`
|
||||
MinParticipants int64 `json:"min_participants"`
|
||||
IntervalMinutes int64 `json:"interval_minutes"`
|
||||
ScheduledTime time.Time `json:"scheduled_time"`
|
||||
LastSettledAt time.Time `json:"last_settled_at"`
|
||||
RefundCouponID int64 `json:"refund_coupon_id"`
|
||||
Image string `json:"image"`
|
||||
GameplayIntro string `json:"gameplay_intro"`
|
||||
AllowItemCards bool `json:"allow_item_cards"`
|
||||
AllowCoupons bool `json:"allow_coupons"`
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Name string `json:"name"`
|
||||
Banner string `json:"banner"`
|
||||
ActivityCategoryID int64 `json:"activity_category_id"`
|
||||
Status int32 `json:"status"`
|
||||
PriceDraw int64 `json:"price_draw"`
|
||||
IsBoss int32 `json:"is_boss"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
DrawMode string `json:"draw_mode"`
|
||||
PlayType string `json:"play_type"`
|
||||
MinParticipants int64 `json:"min_participants"`
|
||||
IntervalMinutes int64 `json:"interval_minutes"`
|
||||
ScheduledTime *time.Time `json:"scheduled_time"`
|
||||
LastSettledAt time.Time `json:"last_settled_at"`
|
||||
RefundCouponID int64 `json:"refund_coupon_id"`
|
||||
Image string `json:"image"`
|
||||
GameplayIntro string `json:"gameplay_intro"`
|
||||
AllowItemCards bool `json:"allow_item_cards"`
|
||||
AllowCoupons bool `json:"allow_coupons"`
|
||||
}
|
||||
|
||||
// ListActivities 活动列表
|
||||
@ -86,6 +86,16 @@ func (h *handler) ListActivities() core.HandlerFunc {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
if req.PageSize > 100 {
|
||||
req.PageSize = 100
|
||||
}
|
||||
var isBossPtr *int32
|
||||
if req.IsBoss == 0 || req.IsBoss == 1 {
|
||||
isBossPtr = &req.IsBoss
|
||||
@ -180,7 +190,7 @@ func (h *handler) GetActivityDetail() core.HandlerFunc {
|
||||
PlayType: item.PlayType,
|
||||
MinParticipants: item.MinParticipants,
|
||||
IntervalMinutes: item.IntervalMinutes,
|
||||
ScheduledTime: item.ScheduledTime,
|
||||
ScheduledTime: &item.ScheduledTime,
|
||||
LastSettledAt: item.LastSettledAt,
|
||||
RefundCouponID: item.RefundCouponID,
|
||||
Image: item.Image,
|
||||
@ -188,6 +198,13 @@ func (h *handler) GetActivityDetail() core.HandlerFunc {
|
||||
AllowItemCards: item.AllowItemCards,
|
||||
AllowCoupons: item.AllowCoupons,
|
||||
}
|
||||
|
||||
// 修复一番赏:即时模式下,清空 ScheduledTime (设置为 nil) 以绕过前端下单拦截
|
||||
// 如果返回零值时间,前端会解析为很早的时间从而判定已结束,必须明确返回 nil
|
||||
if rsp.PlayType == "ichiban" && rsp.DrawMode == "instant" {
|
||||
rsp.ScheduledTime = nil
|
||||
}
|
||||
|
||||
ctx.Payload(rsp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,16 +77,18 @@ func (h *handler) ListDrawLogs() core.HandlerFunc {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
// 计算5分钟前的时间点
|
||||
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||
now := time.Now()
|
||||
// 计算5分钟前的时间点 (用于延迟显示)
|
||||
fiveMinutesAgo := now.Add(-5 * time.Minute)
|
||||
// 计算当天零点 (用于仅显示当天数据)
|
||||
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
// 为了保证过滤后依然有足够数据,我们多取一些
|
||||
fetchPageSize := pageSize
|
||||
if pageSize < 100 {
|
||||
fetchPageSize = 100 // 至少取100条来过滤
|
||||
}
|
||||
// [修改] 强制获取当天最新的 100 条数据 (Service 层限制最大 100)
|
||||
// 忽略前端传入的 Page/PageSize,总是获取第一页的 100 条
|
||||
fetchPageSize := 100
|
||||
fetchPage := 1
|
||||
|
||||
items, total, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, page, fetchPageSize, req.Level)
|
||||
items, total, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, fetchPage, fetchPageSize, req.Level)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error()))
|
||||
return
|
||||
@ -100,10 +102,21 @@ func (h *handler) ListDrawLogs() core.HandlerFunc {
|
||||
|
||||
var filteredItems []*model.ActivityDrawLogs
|
||||
for _, v := range items {
|
||||
// 恢复 5 分钟过滤逻辑
|
||||
// 1. 过滤掉太新的数据 (5分钟延迟)
|
||||
if v.CreatedAt.After(fiveMinutesAgo) {
|
||||
continue
|
||||
}
|
||||
// 2. 过滤掉非当天的数据 (当天零点之前)
|
||||
if v.CreatedAt.Before(startOfToday) {
|
||||
// 因为是按时间倒序返回的,一旦遇到早于今天的,后续的更早,直接结束
|
||||
break
|
||||
}
|
||||
// 3. 数量限制 (虽然 Service 取了 100,这里再保个底,或者遵循前端 pageSize?
|
||||
// 需求是 "获取当天的 最新100 个数据",这里我们以 filteredItems 为准,
|
||||
// 如果前端 pageSize 传了比如 20,是否应该只给 20?
|
||||
// 按照通常逻辑,列表接口应遵循 pageSize。但在这种定制逻辑下,用户似乎想要的是“当天数据的视图”。
|
||||
// 保持原逻辑:遵循 pageSize 限制输出数量,但我们上面强行取了 100 作为源数据。
|
||||
// 如果用户原本想看 100 条,前端传 100 即可。
|
||||
if len(filteredItems) >= pageSize {
|
||||
break
|
||||
}
|
||||
|
||||
@ -136,8 +136,10 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
||||
}
|
||||
|
||||
if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
|
||||
order.CouponID = *req.CouponID
|
||||
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
|
||||
if applied > 0 {
|
||||
order.CouponID = *req.CouponID
|
||||
}
|
||||
}
|
||||
// Title Discount Logic
|
||||
// 1. Fetch active effects for this user, scoped to this activity/issue/category
|
||||
@ -395,9 +397,30 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Inline RecordOrderCouponUsage (no logging)
|
||||
if applied > 0 && req.CouponID != nil && *req.CouponID > 0 {
|
||||
_ = tx.Orders.UnderlyingDB().Exec("INSERT INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?,?,?,NOW(3))", order.ID, *req.CouponID, applied).Error
|
||||
// 优惠券预扣:在事务中原子性扣减余额
|
||||
// 如果余额不足(被其他并发订单消耗),事务回滚
|
||||
if applied > 0 && order.CouponID > 0 {
|
||||
// 原子更新优惠券余额和状态
|
||||
now := time.Now()
|
||||
res := tx.Orders.UnderlyingDB().Exec(`
|
||||
UPDATE user_coupons
|
||||
SET balance_amount = balance_amount - ?,
|
||||
status = CASE WHEN balance_amount - ? <= 0 THEN 4 ELSE 4 END,
|
||||
used_order_id = ?,
|
||||
used_at = ?
|
||||
WHERE id = ? AND user_id = ? AND balance_amount >= ? AND status IN (1, 4)
|
||||
`, applied, applied, order.ID, now, order.CouponID, userID, applied)
|
||||
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("优惠券预扣失败: %w", res.Error)
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
// 余额不足或状态不对,事务回滚
|
||||
return errors.New("优惠券余额不足或已被使用")
|
||||
}
|
||||
|
||||
// 记录使用关系
|
||||
_ = tx.Orders.UnderlyingDB().Exec("INSERT INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?,?,?,NOW(3))", order.ID, order.CouponID, applied).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@ -413,16 +436,8 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
||||
rsp.ActualAmount = order.ActualAmount
|
||||
rsp.Status = order.Status
|
||||
|
||||
// Immediate Draw Trigger if Paid (e.g. Game Pass or Free)
|
||||
if order.Status == 2 && activity.DrawMode == "instant" {
|
||||
go func() {
|
||||
_ = h.activity.ProcessOrderLottery(context.Background(), order.ID)
|
||||
}()
|
||||
}
|
||||
// Immediate Draw Trigger if Paid (e.g. Game Pass or Free)
|
||||
if order.Status == 2 && activity.DrawMode == "instant" {
|
||||
// Trigger process asynchronously or synchronously?
|
||||
// Usually WechatNotify triggers it. Since we bypass WechatNotify, we must trigger it.
|
||||
// 即时开奖触发(已支付 + 即时开奖模式)
|
||||
if shouldTriggerInstantDraw(order.Status, activity.DrawMode) {
|
||||
go func() {
|
||||
_ = h.activity.ProcessOrderLottery(context.Background(), order.ID)
|
||||
}()
|
||||
|
||||
@ -1,11 +1,71 @@
|
||||
package app
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseSlotFromRemark(t *testing.T) {
|
||||
r := parseSlotFromRemark("lottery:activity:1|issue:2|count:1|slot:42")
|
||||
if r != 42 { t.Fatalf("slot parse failed: %d", r) }
|
||||
r2 := parseSlotFromRemark("lottery:activity:1|issue:2|count:1")
|
||||
if r2 != -1 { t.Fatalf("expected -1, got %d", r2) }
|
||||
r := parseSlotFromRemark("lottery:activity:1|issue:2|count:1|slot:42")
|
||||
if r != 42 {
|
||||
t.Fatalf("slot parse failed: %d", r)
|
||||
}
|
||||
r2 := parseSlotFromRemark("lottery:activity:1|issue:2|count:1")
|
||||
if r2 != -1 {
|
||||
t.Fatalf("expected -1, got %d", r2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestShouldTriggerInstantDraw 验证即时开奖触发条件
|
||||
func TestShouldTriggerInstantDraw(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
orderStatus int32
|
||||
drawMode string
|
||||
shouldTrigger bool
|
||||
}{
|
||||
{"已支付+即时开奖", 2, "instant", true},
|
||||
{"已支付+定时开奖", 2, "scheduled", false},
|
||||
{"未支付+即时开奖", 1, "instant", false},
|
||||
{"未支付+定时开奖", 1, "scheduled", false},
|
||||
{"已取消+即时开奖", 3, "instant", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := shouldTriggerInstantDraw(tc.orderStatus, tc.drawMode)
|
||||
if result != tc.shouldTrigger {
|
||||
t.Errorf("期望触发=%v,实际触发=%v", tc.shouldTrigger, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInstantDrawTriggerOnce 验证即时开奖只触发一次
|
||||
// 这个测试模拟 JoinLottery 中的触发逻辑,确保不会重复触发
|
||||
func TestInstantDrawTriggerOnce(t *testing.T) {
|
||||
var callCount int32 = 0
|
||||
|
||||
// 模拟 ProcessOrderLottery 的调用
|
||||
processOrderLottery := func() {
|
||||
atomic.AddInt32(&callCount, 1)
|
||||
}
|
||||
|
||||
// 模拟订单状态
|
||||
orderStatus := int32(2)
|
||||
drawMode := "instant"
|
||||
|
||||
// 执行触发逻辑(使用辅助函数,避免重复代码)
|
||||
if shouldTriggerInstantDraw(orderStatus, drawMode) {
|
||||
go processOrderLottery()
|
||||
}
|
||||
|
||||
// 等待 goroutine 完成
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 验证只调用一次
|
||||
if callCount != 1 {
|
||||
t.Errorf("ProcessOrderLottery 应该只被调用 1 次,实际调用了 %d 次", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *mode
|
||||
sc.discount_value
|
||||
FROM user_coupons uc
|
||||
INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1
|
||||
WHERE uc.id = ? AND uc.user_id = ? AND uc.status = 1
|
||||
WHERE uc.id = ? AND uc.user_id = ? AND uc.status IN (1, 4)
|
||||
LIMIT 1
|
||||
`, userCouponID, userID).Scan(&result).Error
|
||||
|
||||
@ -82,9 +82,6 @@ func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *mode
|
||||
switch result.DiscountType {
|
||||
case 1: // 金额券
|
||||
bal := result.BalanceAmount
|
||||
if bal <= 0 {
|
||||
bal = result.DiscountValue
|
||||
}
|
||||
if bal > 0 {
|
||||
if bal > remainingCap {
|
||||
applied = remainingCap
|
||||
@ -125,6 +122,46 @@ func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *mode
|
||||
return applied
|
||||
}
|
||||
|
||||
// preDeductCouponInTx 在事务中预扣优惠券余额
|
||||
// 功能:原子性地扣减余额并设置 status=4(预扣中),防止并发超额使用
|
||||
// 参数:
|
||||
// - ctx:请求上下文
|
||||
// - tx:数据库事务(必须在事务中调用)
|
||||
// - userID:用户ID
|
||||
// - userCouponID:用户持券ID
|
||||
// - appliedAmount:要预扣的金额(分)
|
||||
// - orderID:关联的订单ID
|
||||
//
|
||||
// 返回:是否成功预扣
|
||||
func (h *handler) preDeductCouponInTx(ctx core.Context, txDB interface {
|
||||
Exec(sql string, values ...interface{}) interface {
|
||||
RowsAffected() int64
|
||||
Error() error
|
||||
}
|
||||
}, userID int64, userCouponID int64, appliedAmount int64, orderID int64) bool {
|
||||
if appliedAmount <= 0 || userCouponID <= 0 {
|
||||
return false
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
// 原子更新:扣减余额 + 设置状态为预扣中(4) + 关联订单
|
||||
// 条件:余额足够 且 状态为未使用(1)或使用中(4,支持同一券多订单分批扣减场景,但需余额足够)
|
||||
result := txDB.Exec(`
|
||||
UPDATE user_coupons
|
||||
SET balance_amount = balance_amount - ?,
|
||||
status = CASE WHEN balance_amount - ? <= 0 THEN 4 ELSE 4 END,
|
||||
used_order_id = ?,
|
||||
used_at = ?
|
||||
WHERE id = ? AND user_id = ? AND balance_amount >= ? AND status IN (1, 4)
|
||||
`, appliedAmount, appliedAmount, orderID, now, userCouponID, userID, appliedAmount)
|
||||
|
||||
if result.Error() != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return result.RowsAffected() > 0
|
||||
}
|
||||
|
||||
// updateUserCouponAfterApply 应用后更新用户券(扣减余额或核销)
|
||||
// 功能:根据订单 remark 中记录的 applied_amount,
|
||||
//
|
||||
@ -154,7 +191,7 @@ func (h *handler) updateUserCouponAfterApply(ctx core.Context, userID int64, ord
|
||||
sc.discount_value
|
||||
FROM user_coupons uc
|
||||
INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1
|
||||
WHERE uc.id = ? AND uc.user_id = ? AND uc.status = 1
|
||||
WHERE uc.id = ? AND uc.user_id = ? AND uc.status IN (1, 4)
|
||||
LIMIT 1
|
||||
`, userCouponID, userID).Scan(&result).Error
|
||||
|
||||
@ -274,3 +311,14 @@ func parseIssueIDFromRemark(remarkStr string) int64 {
|
||||
func parseCountFromRemark(remarkStr string) int64 {
|
||||
return remark.Parse(remarkStr).Count
|
||||
}
|
||||
|
||||
// shouldTriggerInstantDraw 判断是否应该触发即时开奖
|
||||
// 功能:封装即时开奖触发条件判断,避免条件重复
|
||||
// 参数:
|
||||
// - orderStatus:订单状态(2=已支付)
|
||||
// - drawMode:开奖模式("instant"=即时开奖)
|
||||
//
|
||||
// 返回:是否应该触发即时开奖
|
||||
func shouldTriggerInstantDraw(orderStatus int32, drawMode string) bool {
|
||||
return orderStatus == 2 && drawMode == "instant"
|
||||
}
|
||||
|
||||
@ -545,14 +545,18 @@ func (h *handler) CheckMatchingGame() core.HandlerFunc {
|
||||
zap.Bool("is_ok", scopeOK))
|
||||
|
||||
if scopeOK {
|
||||
cardToVoid = icID
|
||||
// Fix: Don't set cardToVoid immediately. Only set it if an effect is actually applied.
|
||||
|
||||
// Double reward
|
||||
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||||
// Double reward
|
||||
cardToVoid = icID // Mark for consumption
|
||||
h.logger.Info("道具卡-CheckMatchingGame: 应用双倍奖励", zap.Int32("multiplier", ic.RewardMultiplierX1000))
|
||||
finalQuantity = 2
|
||||
finalRemark += "(倍数)"
|
||||
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
|
||||
// Probability boost - try to upgrade to better reward
|
||||
// Probability boost
|
||||
cardToVoid = icID // Mark for consumption (even if RNG fails, the card is "used")
|
||||
|
||||
h.logger.Debug("道具卡-CheckMatchingGame: 应用概率提升", zap.Int32("boost_rate", ic.BoostRateX1000))
|
||||
allRewards, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(
|
||||
h.readDB.ActivityRewardSettings.IssueID.Eq(game.IssueID),
|
||||
@ -593,6 +597,11 @@ func (h *handler) CheckMatchingGame() core.HandlerFunc {
|
||||
} else {
|
||||
h.logger.Debug("道具卡-CheckMatchingGame: 未找到更好的奖品可升级", zap.Int64("current_score", candidate.MinScore))
|
||||
}
|
||||
} else {
|
||||
// Effect not recognized or params too low
|
||||
h.logger.Warn("道具卡-CheckMatchingGame: 效果类型未知或参数无效,不消耗卡片",
|
||||
zap.Int32("effect_type", ic.EffectType),
|
||||
zap.Int32("multiplier", ic.RewardMultiplierX1000))
|
||||
}
|
||||
} else {
|
||||
h.logger.Debug("道具卡-CheckMatchingGame: 范围校验失败")
|
||||
|
||||
@ -7,31 +7,31 @@ import (
|
||||
|
||||
// TestSelectRewardExact 测试对对碰选奖逻辑:精确匹配 TotalPairs == MinScore
|
||||
func TestSelectRewardExact(t *testing.T) {
|
||||
// 模拟奖品设置
|
||||
// 模拟奖品设置 (使用 Level 作为标识,因为 ActivityRewardSettings 没有 Name 字段)
|
||||
rewards := []*model.ActivityRewardSettings{
|
||||
{ID: 1, Name: "奖品A-10对", MinScore: 10, Quantity: 5},
|
||||
{ID: 2, Name: "奖品B-20对", MinScore: 20, Quantity: 5},
|
||||
{ID: 3, Name: "奖品C-30对", MinScore: 30, Quantity: 5},
|
||||
{ID: 4, Name: "奖品D-40对", MinScore: 40, Quantity: 5},
|
||||
{ID: 5, Name: "奖品E-45对", MinScore: 45, Quantity: 5},
|
||||
{ID: 1, Level: 1, MinScore: 10, Quantity: 5},
|
||||
{ID: 2, Level: 2, MinScore: 20, Quantity: 5},
|
||||
{ID: 3, Level: 3, MinScore: 30, Quantity: 5},
|
||||
{ID: 4, Level: 4, MinScore: 40, Quantity: 5},
|
||||
{ID: 5, Level: 5, MinScore: 45, Quantity: 5},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
totalPairs int64
|
||||
expectReward *int64 // nil = 无匹配
|
||||
expectName string
|
||||
expectLevel int32
|
||||
}{
|
||||
{"精确匹配10对", 10, ptr(int64(1)), "奖品A-10对"},
|
||||
{"精确匹配20对", 20, ptr(int64(2)), "奖品B-20对"},
|
||||
{"精确匹配30对", 30, ptr(int64(3)), "奖品C-30对"},
|
||||
{"精确匹配40对", 40, ptr(int64(4)), "奖品D-40对"},
|
||||
{"精确匹配45对", 45, ptr(int64(5)), "奖品E-45对"},
|
||||
{"15对-无匹配", 15, nil, ""},
|
||||
{"25对-无匹配", 25, nil, ""},
|
||||
{"35对-无匹配", 35, nil, ""},
|
||||
{"50对-无匹配", 50, nil, ""},
|
||||
{"0对-无匹配", 0, nil, ""},
|
||||
{"精确匹配10对", 10, ptr(int64(1)), 1},
|
||||
{"精确匹配20对", 20, ptr(int64(2)), 2},
|
||||
{"精确匹配30对", 30, ptr(int64(3)), 3},
|
||||
{"精确匹配40对", 40, ptr(int64(4)), 4},
|
||||
{"精确匹配45对", 45, ptr(int64(5)), 5},
|
||||
{"15对-无匹配", 15, nil, 0},
|
||||
{"25对-无匹配", 25, nil, 0},
|
||||
{"35对-无匹配", 35, nil, 0},
|
||||
{"50对-无匹配", 50, nil, 0},
|
||||
{"0对-无匹配", 0, nil, 0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@ -40,15 +40,15 @@ func TestSelectRewardExact(t *testing.T) {
|
||||
|
||||
if tc.expectReward == nil {
|
||||
if candidate != nil {
|
||||
t.Errorf("期望无匹配,但得到奖品: %s (ID=%d)", candidate.Name, candidate.ID)
|
||||
t.Errorf("期望无匹配,但得到奖品: Level=%d (ID=%d)", candidate.Level, candidate.ID)
|
||||
}
|
||||
} else {
|
||||
if candidate == nil {
|
||||
t.Errorf("期望匹配奖品ID=%d,但无匹配", *tc.expectReward)
|
||||
} else if candidate.ID != *tc.expectReward {
|
||||
t.Errorf("期望奖品ID=%d,实际=%d", *tc.expectReward, candidate.ID)
|
||||
} else if candidate.Name != tc.expectName {
|
||||
t.Errorf("期望奖品名=%s,实际=%s", tc.expectName, candidate.Name)
|
||||
} else if candidate.Level != tc.expectLevel {
|
||||
t.Errorf("期望奖品Level=%d,实际=%d", tc.expectLevel, candidate.Level)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -58,14 +58,14 @@ func TestSelectRewardExact(t *testing.T) {
|
||||
// TestSelectRewardWithZeroQuantity 测试库存为0时不匹配
|
||||
func TestSelectRewardWithZeroQuantity(t *testing.T) {
|
||||
rewards := []*model.ActivityRewardSettings{
|
||||
{ID: 1, Name: "奖品A-10对", MinScore: 10, Quantity: 0}, // 库存为0
|
||||
{ID: 2, Name: "奖品B-20对", MinScore: 20, Quantity: 5},
|
||||
{ID: 1, Level: 1, MinScore: 10, Quantity: 0}, // 库存为0
|
||||
{ID: 2, Level: 2, MinScore: 20, Quantity: 5},
|
||||
}
|
||||
|
||||
// 即使精确匹配,库存为0也不应匹配
|
||||
candidate := selectRewardExact(rewards, 10)
|
||||
if candidate != nil {
|
||||
t.Errorf("库存为0时不应匹配,但得到: %s", candidate.Name)
|
||||
t.Errorf("库存为0时不应匹配,但得到: Level=%d", candidate.Level)
|
||||
}
|
||||
|
||||
// 库存>0应正常匹配
|
||||
|
||||
@ -65,10 +65,73 @@ func (h *handler) startMatchingGameCleanup() {
|
||||
})
|
||||
}
|
||||
|
||||
// autoCheckDatabaseFallback 数据库扫描兜底(防止Redis缓存过期导致漏单)
|
||||
func (h *handler) autoCheckDatabaseFallback() {
|
||||
ctx := context.Background()
|
||||
|
||||
// 1. 查询 30分钟前~24小时内 已支付 但 未开奖 的对对碰订单 (SourceType=3)
|
||||
// 这个时间窗口是为了避开正常游戏中的订单 (Redis TTL 30m)
|
||||
startTime := time.Now().Add(-24 * time.Hour)
|
||||
endTime := time.Now().Add(-30 * time.Minute)
|
||||
|
||||
// 使用 left join 排除已有日志的订单
|
||||
var orderNos []string
|
||||
err := h.readDB.Orders.WithContext(ctx).UnderlyingDB().Raw(`
|
||||
SELECT o.order_no
|
||||
FROM orders o
|
||||
LEFT JOIN activity_draw_logs l ON o.id = l.order_id
|
||||
WHERE o.source_type = 3
|
||||
AND o.status = 2
|
||||
AND o.created_at BETWEEN ? AND ?
|
||||
AND l.id IS NULL
|
||||
`, startTime, endTime).Scan(&orderNos).Error
|
||||
|
||||
if err != nil {
|
||||
h.logger.Error("对对碰兜底扫描: 查询失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(orderNos) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("对对碰兜底扫描: 发现异常订单", zap.Int("count", len(orderNos)))
|
||||
|
||||
for _, orderNo := range orderNos {
|
||||
// 2. 加载订单详情
|
||||
order, err := h.readDB.Orders.WithContext(ctx).Where(h.readDB.Orders.OrderNo.Eq(orderNo)).First()
|
||||
if err != nil || order == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. 重构游戏状态
|
||||
// 我们需要从 Seed, Position 等信息重构 Memory Graph
|
||||
game, err := h.activity.ReconstructMatchingGame(ctx, orderNo)
|
||||
if err != nil {
|
||||
h.logger.Error("对对碰兜底扫描: 游戏状态重构失败", zap.String("order_no", orderNo), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 4. 重构 GameID (模拟)
|
||||
// 注意:原始 GameID 可能丢失,这里我们并不真的需要精确的 Request GameID,
|
||||
// 因为 doAutoCheck 主要依赖 game 对象和 OrderID。
|
||||
// 但为了锁的唯一性,我们使用 MG_FALLBACK_{OrderID}
|
||||
fakeGameID := fmt.Sprintf("FALLBACK_%d", order.ID)
|
||||
|
||||
h.logger.Info("对对碰兜底扫描: 触发补单", zap.String("order_no", orderNo))
|
||||
h.doAutoCheck(ctx, fakeGameID, game, order)
|
||||
}
|
||||
}
|
||||
|
||||
// autoCheckExpiredGames 扫描超时未结算的对对碰游戏并自动开奖
|
||||
func (h *handler) autoCheckExpiredGames() {
|
||||
ctx := context.Background()
|
||||
|
||||
// 0. 执行数据库兜底扫描 (低频执行,例如每次 autoCheck 都跑,或者加计数器)
|
||||
// 由于 autoCheckHelper 是每3分钟跑一次,这里直接调用损耗可控
|
||||
// 且查询走了索引 (created_at)
|
||||
h.autoCheckDatabaseFallback()
|
||||
|
||||
// 1. 扫描 Redis 中所有 matching_game key
|
||||
keys, err := h.redis.Keys(ctx, activitysvc.MatchingGameKeyPrefix+"*").Result()
|
||||
if err != nil {
|
||||
|
||||
@ -464,6 +464,7 @@ type activityItem struct {
|
||||
Status int32 `json:"status"`
|
||||
PriceDraw int64 `json:"price_draw"`
|
||||
IsBoss int32 `json:"is_boss"`
|
||||
PlayType string `json:"play_type"`
|
||||
}
|
||||
|
||||
type listActivitiesResponse struct {
|
||||
@ -544,6 +545,7 @@ func (h *handler) ListActivities() core.HandlerFunc {
|
||||
Status: v.Status,
|
||||
PriceDraw: v.PriceDraw,
|
||||
IsBoss: v.IsBoss,
|
||||
PlayType: v.PlayType,
|
||||
}
|
||||
}
|
||||
ctx.Payload(res)
|
||||
|
||||
319
internal/api/admin/blacklist_admin.go
Normal file
319
internal/api/admin/blacklist_admin.go
Normal file
@ -0,0 +1,319 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
// ========== 黑名单管理 ==========
|
||||
|
||||
type addBlacklistRequest struct {
|
||||
DouyinUserID string `json:"douyin_user_id" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type blacklistResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
DouyinUserID string `json:"douyin_user_id"`
|
||||
Reason string `json:"reason"`
|
||||
OperatorID int64 `json:"operator_id"`
|
||||
Status int32 `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type listBlacklistRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
Keyword string `form:"keyword"`
|
||||
}
|
||||
|
||||
type listBlacklistResponse struct {
|
||||
List []blacklistResponse `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// ListBlacklist 获取黑名单列表
|
||||
// @Summary 获取黑名单列表
|
||||
// @Description 获取抖音用户黑名单列表,支持分页和关键词搜索
|
||||
// @Tags 管理端.黑名单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(20)
|
||||
// @Param keyword query string false "搜索关键词(抖音ID)"
|
||||
// @Success 200 {object} listBlacklistResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/blacklist [get]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) ListBlacklist() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(listBlacklistRequest)
|
||||
if err := ctx.ShouldBindQuery(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext()).
|
||||
Table("douyin_blacklist").
|
||||
Where("status = 1")
|
||||
|
||||
if req.Keyword != "" {
|
||||
db = db.Where("douyin_user_id LIKE ?", "%"+req.Keyword+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var list []model.DouyinBlacklist
|
||||
if err := db.Order("id DESC").
|
||||
Offset((req.Page - 1) * req.PageSize).
|
||||
Limit(req.PageSize).
|
||||
Find(&list).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
rsp := &listBlacklistResponse{
|
||||
List: make([]blacklistResponse, len(list)),
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}
|
||||
|
||||
for i, item := range list {
|
||||
rsp.List[i] = blacklistResponse{
|
||||
ID: item.ID,
|
||||
DouyinUserID: item.DouyinUserID,
|
||||
Reason: item.Reason,
|
||||
OperatorID: item.OperatorID,
|
||||
Status: item.Status,
|
||||
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(rsp)
|
||||
}
|
||||
}
|
||||
|
||||
// AddBlacklist 添加黑名单
|
||||
// @Summary 添加黑名单
|
||||
// @Description 将抖音用户添加到黑名单
|
||||
// @Tags 管理端.黑名单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body addBlacklistRequest true "请求参数"
|
||||
// @Success 200 {object} blacklistResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/blacklist [post]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) AddBlacklist() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(addBlacklistRequest)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已在黑名单
|
||||
var existCount int64
|
||||
h.repo.GetDbR().WithContext(ctx.RequestContext()).
|
||||
Table("douyin_blacklist").
|
||||
Where("douyin_user_id = ? AND status = 1", req.DouyinUserID).
|
||||
Count(&existCount)
|
||||
|
||||
if existCount > 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "该用户已在黑名单中"))
|
||||
return
|
||||
}
|
||||
|
||||
operatorID := int64(0)
|
||||
if ctx.SessionUserInfo().Id > 0 {
|
||||
operatorID = int64(ctx.SessionUserInfo().Id)
|
||||
}
|
||||
|
||||
blacklist := &model.DouyinBlacklist{
|
||||
DouyinUserID: req.DouyinUserID,
|
||||
Reason: req.Reason,
|
||||
OperatorID: operatorID,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if err := h.repo.GetDbW().WithContext(ctx.RequestContext()).Create(blacklist).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Payload(&blacklistResponse{
|
||||
ID: blacklist.ID,
|
||||
DouyinUserID: blacklist.DouyinUserID,
|
||||
Reason: blacklist.Reason,
|
||||
OperatorID: blacklist.OperatorID,
|
||||
Status: blacklist.Status,
|
||||
CreatedAt: blacklist.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveBlacklist 移除黑名单
|
||||
// @Summary 移除黑名单
|
||||
// @Description 将用户从黑名单中移除(软删除,status设为0)
|
||||
// @Tags 管理端.黑名单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path integer true "黑名单ID"
|
||||
// @Success 200 {object} simpleMessageResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/blacklist/{id} [delete]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) RemoveBlacklist() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
idStr := ctx.Param("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的ID"))
|
||||
return
|
||||
}
|
||||
|
||||
result := h.repo.GetDbW().WithContext(ctx.RequestContext()).
|
||||
Table("douyin_blacklist").
|
||||
Where("id = ?", id).
|
||||
Update("status", 0)
|
||||
|
||||
if result.Error != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, result.Error.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ParamBindError, "黑名单记录不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Payload(&simpleMessageResponse{Message: "移除成功"})
|
||||
}
|
||||
}
|
||||
|
||||
// CheckBlacklist 检查用户是否在黑名单
|
||||
// @Summary 检查黑名单状态
|
||||
// @Description 检查指定抖音用户是否在黑名单中
|
||||
// @Tags 管理端.黑名单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param douyin_user_id query string true "抖音用户ID"
|
||||
// @Success 200 {object} map[string]bool
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/blacklist/check [get]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) CheckBlacklist() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
douyinUserID := ctx.RequestInputParams().Get("douyin_user_id")
|
||||
if douyinUserID == "" {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "抖音用户ID不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
var count int64
|
||||
h.repo.GetDbR().WithContext(ctx.RequestContext()).
|
||||
Table("douyin_blacklist").
|
||||
Where("douyin_user_id = ? AND status = 1", douyinUserID).
|
||||
Count(&count)
|
||||
|
||||
ctx.Payload(map[string]any{
|
||||
"douyin_user_id": douyinUserID,
|
||||
"is_blacklisted": count > 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BatchAddBlacklist 批量添加黑名单
|
||||
// @Summary 批量添加黑名单
|
||||
// @Description 批量将抖音用户添加到黑名单
|
||||
// @Tags 管理端.黑名单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body batchAddBlacklistRequest true "请求参数"
|
||||
// @Success 200 {object} batchAddBlacklistResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/blacklist/batch [post]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) BatchAddBlacklist() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
var req struct {
|
||||
DouyinUserIDs []string `json:"douyin_user_ids" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.DouyinUserIDs) == 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "抖音用户ID列表不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取操作人ID
|
||||
operatorID := int64(0)
|
||||
if ctx.SessionUserInfo().Id > 0 {
|
||||
operatorID = int64(ctx.SessionUserInfo().Id)
|
||||
}
|
||||
|
||||
// 查询已存在的黑名单
|
||||
var existingIDs []string
|
||||
h.repo.GetDbR().WithContext(ctx.RequestContext()).
|
||||
Table("douyin_blacklist").
|
||||
Where("douyin_user_id IN ? AND status = 1", req.DouyinUserIDs).
|
||||
Pluck("douyin_user_id", &existingIDs)
|
||||
|
||||
existMap := make(map[string]bool)
|
||||
for _, id := range existingIDs {
|
||||
existMap[id] = true
|
||||
}
|
||||
|
||||
// 过滤出需要新增的
|
||||
var toAdd []model.DouyinBlacklist
|
||||
for _, uid := range req.DouyinUserIDs {
|
||||
if !existMap[uid] {
|
||||
toAdd = append(toAdd, model.DouyinBlacklist{
|
||||
DouyinUserID: uid,
|
||||
Reason: req.Reason,
|
||||
OperatorID: operatorID,
|
||||
Status: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
addedCount := 0
|
||||
if len(toAdd) > 0 {
|
||||
if err := h.repo.GetDbW().WithContext(ctx.RequestContext()).Create(&toAdd).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||
return
|
||||
}
|
||||
addedCount = len(toAdd)
|
||||
}
|
||||
|
||||
ctx.Payload(map[string]any{
|
||||
"total_requested": len(req.DouyinUserIDs),
|
||||
"added": addedCount,
|
||||
"skipped": len(req.DouyinUserIDs) - addedCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,9 @@ type activityProfitLossItem struct {
|
||||
ActivityName string `json:"activity_name"`
|
||||
Status int32 `json:"status"`
|
||||
DrawCount int64 `json:"draw_count"`
|
||||
GamePassCount int64 `json:"game_pass_count"` // 次卡抽奖次数
|
||||
PaymentCount int64 `json:"payment_count"` // 现金/优惠券抽奖次数
|
||||
RefundCount int64 `json:"refund_count"` // 退款/取消抽奖次数
|
||||
PlayerCount int64 `json:"player_count"`
|
||||
TotalRevenue int64 `json:"total_revenue"` // 实际支付金额 (分)
|
||||
TotalDiscount int64 `json:"total_discount"` // 优惠券抵扣金额 (分)
|
||||
@ -121,48 +124,77 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 统计抽奖次数和人数 (通过 activity_draw_logs 关联 activity_issues)
|
||||
// 2. 统计抽奖次数和人数 (通过 activity_draw_logs 关联 activity_issues 和 orders)
|
||||
type drawStat struct {
|
||||
ActivityID int64
|
||||
DrawCount int64
|
||||
PlayerCount int64
|
||||
ActivityID int64
|
||||
TotalCount int64
|
||||
GamePassCount int64
|
||||
PaymentCount int64
|
||||
RefundCount int64
|
||||
PlayerCount int64
|
||||
}
|
||||
var drawStats []drawStat
|
||||
db.Table(model.TableNameActivityDrawLogs).
|
||||
Select("activity_issues.activity_id, COUNT(activity_draw_logs.id) as draw_count, COUNT(DISTINCT activity_draw_logs.user_id) as player_count").
|
||||
Select(`
|
||||
activity_issues.activity_id,
|
||||
COUNT(activity_draw_logs.id) as total_count,
|
||||
SUM(CASE WHEN orders.status = 2 AND (orders.source_type = 4 OR orders.order_no LIKE 'GP%') THEN 1 ELSE 0 END) as game_pass_count,
|
||||
SUM(CASE WHEN orders.status = 2 AND NOT (orders.source_type = 4 OR orders.order_no LIKE 'GP%') 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)
|
||||
|
||||
for _, s := range drawStats {
|
||||
if item, ok := activityMap[s.ActivityID]; ok {
|
||||
item.DrawCount = s.DrawCount
|
||||
item.DrawCount = s.GamePassCount + s.PaymentCount // 仅统计有效抽奖(次卡+支付)
|
||||
item.GamePassCount = s.GamePassCount
|
||||
item.PaymentCount = s.PaymentCount
|
||||
item.RefundCount = s.RefundCount
|
||||
item.PlayerCount = s.PlayerCount
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
||||
// BUG修复:排除已退款订单(status=4)
|
||||
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
||||
// BUG修复:排除已退款订单(status=4)。
|
||||
// 注意: MySQL SUM()运算涉及除法时会返回Decimal类型,需要Scan到float64
|
||||
type revenueStat struct {
|
||||
ActivityID int64
|
||||
TotalRevenue int64
|
||||
TotalDiscount int64
|
||||
TotalRevenue float64
|
||||
TotalDiscount float64
|
||||
}
|
||||
var revenueStats []revenueStat
|
||||
|
||||
// 修正: 先找到每个订单对应的一个 activity_id (去重),再关联 orders 统计 actual_amount。
|
||||
// 避免一个订单包含多个 draw logs 时导致 orders.actual_amount 被重复累加。
|
||||
// 子查询: SELECT order_id, MAX(issue_id) as issue_id FROM activity_draw_logs GROUP BY order_id
|
||||
// 然后通过 issue_id 关联 activity_issues 找到 activity_id
|
||||
// 修正: 按抽奖次数比例分摊订单金额 (解决多活动订单归因问题)
|
||||
// 逻辑: 活动分摊收入 = 订单实际金额 * (该活动在该订单中的抽奖次数 / 该订单总抽奖次数)
|
||||
var err error
|
||||
err = db.Table(model.TableNameOrders).
|
||||
Select("activity_issues.activity_id, SUM(orders.actual_amount) as total_revenue, SUM(orders.discount_amount) as total_discount").
|
||||
Joins("JOIN (SELECT order_id, MAX(issue_id) as issue_id FROM activity_draw_logs GROUP BY order_id) dl ON dl.order_id = orders.id").
|
||||
Joins("JOIN activity_issues ON activity_issues.id = dl.issue_id").
|
||||
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
|
||||
Where("activity_issues.activity_id IN ?", activityIDs).
|
||||
Group("activity_issues.activity_id").
|
||||
Select(`
|
||||
order_activity_draws.activity_id,
|
||||
SUM(1.0 * orders.actual_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_revenue,
|
||||
SUM(CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' THEN 0 ELSE 1.0 * orders.discount_amount * order_activity_draws.draw_count / order_total_draws.total_count END) as total_discount
|
||||
`).
|
||||
// Subquery 1: Calculate draw counts per order per activity (and link to issue->activity)
|
||||
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`).
|
||||
// Subquery 2: Calculate total draw counts per order
|
||||
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`).
|
||||
Where("orders.status = ?", 2). // 已支付(排除待支付、取消、退款状态)
|
||||
Where("order_activity_draws.activity_id IN ?", activityIDs).
|
||||
Group("order_activity_draws.activity_id").
|
||||
Scan(&revenueStats).Error
|
||||
|
||||
if err != nil {
|
||||
@ -171,12 +203,13 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
||||
|
||||
for _, s := range revenueStats {
|
||||
if item, ok := activityMap[s.ActivityID]; ok {
|
||||
item.TotalRevenue = s.TotalRevenue
|
||||
item.TotalDiscount = s.TotalDiscount
|
||||
item.TotalRevenue = int64(s.TotalRevenue)
|
||||
item.TotalDiscount = int64(s.TotalDiscount)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 统计成本 (通过 user_inventory 关联 products)
|
||||
// 4. 统计成本 (通过 user_inventory 关联 products 和 orders)
|
||||
// 修正:增加关联 orders 表,过滤掉已退款/取消的订单 (status!=2)
|
||||
type costStat struct {
|
||||
ActivityID int64
|
||||
TotalCost int64
|
||||
@ -185,7 +218,9 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
||||
db.Table(model.TableNameUserInventory).
|
||||
Select("user_inventory.activity_id, SUM(products.price) as total_cost").
|
||||
Joins("JOIN products ON products.id = user_inventory.product_id").
|
||||
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
||||
Where("user_inventory.activity_id IN ?", activityIDs).
|
||||
Where("orders.status = ?", 2). // 仅统计已支付订单产生的成本
|
||||
Group("user_inventory.activity_id").
|
||||
Scan(&costStats)
|
||||
|
||||
@ -214,8 +249,9 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
||||
Select("activity_issues.activity_id, COUNT(activity_draw_logs.id) as game_pass_draws").
|
||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||
Joins("JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||||
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
|
||||
Where("orders.actual_amount = 0"). // 0元订单 = 次卡支付
|
||||
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
|
||||
Where("orders.actual_amount = 0"). // 0元订单
|
||||
Where("orders.source_type = 4 OR orders.order_no LIKE 'GP%'"). // 次数卡 (Lottery SourceType=4 OR Matching Game GP prefix)
|
||||
Where("activity_issues.activity_id IN ?", activityIDs).
|
||||
Group("activity_issues.activity_id").
|
||||
Scan(&gamePassStats)
|
||||
@ -376,6 +412,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
||||
OrderRemark string // BUG修复:增加remark字段用于解析次数卡使用信息
|
||||
OrderNo string // 订单号
|
||||
DrawCount int64 // 该订单的总抽奖次数(用于金额分摊)
|
||||
UsedDrawLogID int64 // 道具卡实际使用的日志ID
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
@ -403,6 +440,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
||||
COALESCE(orders.remark, '') as order_remark,
|
||||
COALESCE(orders.order_no, '') as order_no,
|
||||
COALESCE(order_draw_counts.draw_count, 1) as draw_count,
|
||||
COALESCE(user_item_cards.used_draw_log_id, 0) as used_draw_log_id,
|
||||
activity_draw_logs.created_at
|
||||
`).
|
||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||
@ -410,8 +448,10 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
||||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
|
||||
Joins("LEFT JOIN products ON products.id = activity_reward_settings.product_id").
|
||||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||||
Joins("LEFT JOIN system_coupons ON system_coupons.id = orders.coupon_id").
|
||||
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = orders.item_card_id").
|
||||
Joins("LEFT JOIN user_coupons ON user_coupons.id = orders.coupon_id").
|
||||
Joins("LEFT JOIN system_coupons ON system_coupons.id = user_coupons.coupon_id").
|
||||
Joins("LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id").
|
||||
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id").
|
||||
Joins("LEFT JOIN (SELECT order_id, COUNT(*) as draw_count FROM activity_draw_logs GROUP BY order_id) as order_draw_counts ON order_draw_counts.order_id = activity_draw_logs.order_id").
|
||||
Where("activity_issues.activity_id = ?", activityID).
|
||||
Order("activity_draw_logs.id DESC").
|
||||
@ -454,7 +494,11 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
||||
}
|
||||
|
||||
// 检查是否使用了道具卡
|
||||
if l.ItemCardID > 0 || l.ItemCardName != "" {
|
||||
// BUG FIX: 仅当该条日志的 ID 等于 item_card 记录的 used_draw_log_id 时,才显示道具卡信息
|
||||
// 防止一个订单下的所有抽奖记录都显示 "双倍快乐水"
|
||||
isCardValidForThisLog := (l.UsedDrawLogID == 0) || (l.UsedDrawLogID == l.ID)
|
||||
|
||||
if (l.ItemCardID > 0 || l.ItemCardName != "") && isCardValidForThisLog {
|
||||
paymentDetails.ItemCardUsed = true
|
||||
paymentDetails.ItemCardName = l.ItemCardName
|
||||
if paymentDetails.ItemCardName == "" {
|
||||
|
||||
@ -189,10 +189,11 @@ type trendRequest struct {
|
||||
}
|
||||
|
||||
type trendPoint struct {
|
||||
Date string `json:"date"`
|
||||
Value int64 `json:"value"`
|
||||
Gmv int64 `json:"gmv"`
|
||||
Orders int64 `json:"orders"`
|
||||
Date string `json:"date"`
|
||||
Value int64 `json:"value"`
|
||||
Gmv int64 `json:"gmv"`
|
||||
Orders int64 `json:"orders"`
|
||||
NewUsers int64 `json:"newUsers"`
|
||||
}
|
||||
|
||||
type salesDrawTrendResponse struct {
|
||||
@ -1063,11 +1064,14 @@ func (h *handler) DashboardActivityPrizeAnalysis() core.HandlerFunc {
|
||||
}
|
||||
drawCounts := make(map[int64]int64)
|
||||
var dcRows []countResult
|
||||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().Debug().
|
||||
Select(h.readDB.ActivityDrawLogs.IssueID.As("key"), h.readDB.ActivityDrawLogs.ID.Count().As("count")).
|
||||
LeftJoin(h.readDB.Orders, h.readDB.Orders.ID.EqCol(h.readDB.ActivityDrawLogs.OrderID)).
|
||||
Where(h.readDB.ActivityDrawLogs.IssueID.In(issueIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Group(h.readDB.ActivityDrawLogs.IssueID).
|
||||
Scan(&dcRows)
|
||||
|
||||
for _, r := range dcRows {
|
||||
drawCounts[r.Key] = r.Count
|
||||
}
|
||||
@ -1075,10 +1079,12 @@ func (h *handler) DashboardActivityPrizeAnalysis() core.HandlerFunc {
|
||||
// 5.2 每个奖品的中奖数 (RewardID -> Count)
|
||||
winCounts := make(map[int64]int64)
|
||||
var wcRows []countResult
|
||||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().Debug().
|
||||
Select(h.readDB.ActivityDrawLogs.RewardID.As("key"), h.readDB.ActivityDrawLogs.ID.Count().As("count")).
|
||||
LeftJoin(h.readDB.Orders, h.readDB.Orders.ID.EqCol(h.readDB.ActivityDrawLogs.OrderID)).
|
||||
Where(h.readDB.ActivityDrawLogs.IssueID.In(issueIDs...)).
|
||||
Where(h.readDB.ActivityDrawLogs.IsWinner.Eq(1)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Group(h.readDB.ActivityDrawLogs.RewardID).
|
||||
Scan(&wcRows)
|
||||
for _, r := range wcRows {
|
||||
@ -1086,8 +1092,11 @@ func (h *handler) DashboardActivityPrizeAnalysis() core.HandlerFunc {
|
||||
}
|
||||
|
||||
// 5.3 活动总参与人数 (Distinct UserID)
|
||||
participants, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||||
|
||||
participants, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().Debug().
|
||||
LeftJoin(h.readDB.Orders, h.readDB.Orders.ID.EqCol(h.readDB.ActivityDrawLogs.OrderID)).
|
||||
Where(h.readDB.ActivityDrawLogs.IssueID.In(issueIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Distinct(h.readDB.ActivityDrawLogs.UserID).
|
||||
Count()
|
||||
|
||||
@ -1605,11 +1614,18 @@ func (h *handler) DashboardSalesDrawTrend() core.HandlerFunc {
|
||||
Where(h.readDB.Orders.PaidAt.Lte(b.End)).
|
||||
Count()
|
||||
|
||||
// 新注册用户数
|
||||
newUsers, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Where(h.readDB.Users.CreatedAt.Gte(b.Start)).
|
||||
Where(h.readDB.Users.CreatedAt.Lte(b.End)).
|
||||
Count()
|
||||
|
||||
list[i] = trendPoint{
|
||||
Date: b.Label,
|
||||
Value: draws,
|
||||
Gmv: gmv.Total / 100, // 转为元
|
||||
Orders: orders,
|
||||
Date: b.Label,
|
||||
Value: draws,
|
||||
Gmv: gmv.Total / 100, // 转为元
|
||||
Orders: orders,
|
||||
NewUsers: newUsers,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1630,6 +1646,8 @@ type productPerformanceItem struct {
|
||||
SeriesName string `json:"seriesName"`
|
||||
SalesCount int64 `json:"salesCount"`
|
||||
Amount int64 `json:"amount"`
|
||||
Profit int64 `json:"profit"`
|
||||
ProfitRate float64 `json:"profitRate"`
|
||||
ContributionRate float64 `json:"contributionRate"`
|
||||
InventoryTurnover float64 `json:"inventoryTurnover"`
|
||||
}
|
||||
@ -1640,24 +1658,26 @@ func (h *handler) OperationsProductPerformance() core.HandlerFunc {
|
||||
|
||||
// 按活动聚合抽奖数据
|
||||
type drawRow struct {
|
||||
ActivityID int64
|
||||
Count int64
|
||||
Winners int64
|
||||
ActivityID int64 `gorm:"column:activity_id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
TotalCost int64 `gorm:"column:total_cost"`
|
||||
}
|
||||
var rows []drawRow
|
||||
|
||||
// 统计抽奖日志,按活动分组
|
||||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
||||
LeftJoin(h.readDB.ActivityIssues, h.readDB.ActivityIssues.ID.EqCol(h.readDB.ActivityDrawLogs.IssueID)).
|
||||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Gte(s)).
|
||||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Lte(e)).
|
||||
// 统计抽奖日志,按活动分组,并计算奖品成本
|
||||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).UnderlyingDB().
|
||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
|
||||
Joins("LEFT JOIN products ON products.id = activity_reward_settings.product_id").
|
||||
Where("activity_draw_logs.created_at >= ?", s).
|
||||
Where("activity_draw_logs.created_at <= ?", e).
|
||||
Select(
|
||||
h.readDB.ActivityIssues.ActivityID,
|
||||
h.readDB.ActivityDrawLogs.ID.Count().As("count"),
|
||||
h.readDB.ActivityDrawLogs.IsWinner.Sum().As("winners"),
|
||||
"activity_issues.activity_id",
|
||||
"COUNT(activity_draw_logs.id) as count",
|
||||
"SUM(IF(activity_draw_logs.is_winner = 1, products.price, 0)) as total_cost",
|
||||
).
|
||||
Group(h.readDB.ActivityIssues.ActivityID).
|
||||
Order(h.readDB.ActivityDrawLogs.ID.Count().Desc()).
|
||||
Group("activity_issues.activity_id").
|
||||
Order("count DESC").
|
||||
Limit(10).
|
||||
Scan(&rows)
|
||||
|
||||
@ -1707,9 +1727,16 @@ func (h *handler) OperationsProductPerformance() core.HandlerFunc {
|
||||
SeriesName: info.Name,
|
||||
SalesCount: r.Count,
|
||||
Amount: (r.Count * info.PriceDraw) / 100, // 转换为元
|
||||
Profit: (r.Count*info.PriceDraw - r.TotalCost) / 100,
|
||||
ProfitRate: 0,
|
||||
ContributionRate: float64(int(contribution*10)) / 10.0,
|
||||
InventoryTurnover: float64(int(turnover*10)) / 10.0,
|
||||
}
|
||||
if r.Count > 0 && info.PriceDraw > 0 {
|
||||
revenue := r.Count * info.PriceDraw
|
||||
pr := float64(revenue-r.TotalCost) / float64(revenue) * 100
|
||||
out[i].ProfitRate = float64(int(pr*10)) / 10.0
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(out)
|
||||
|
||||
@ -24,26 +24,30 @@ type spendingLeaderboardItem struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
TotalSpending int64 `json:"total_spending"` // Total Paid Amount (Fen)
|
||||
TotalPrizeValue int64 `json:"total_prize_value"` // Total Product Price (Fen)
|
||||
TotalDiscount int64 `json:"total_discount"` // Total Coupon Discount (Fen)
|
||||
TotalPoints int64 `json:"total_points"` // Total Points Discount (Fen)
|
||||
GamePassCount int64 `json:"game_pass_count"` // Count of SourceType=4
|
||||
ItemCardCount int64 `json:"item_card_count"` // Count where ItemCardID > 0
|
||||
OrderCount int64 `json:"-"` // Hidden
|
||||
TotalSpending int64 `json:"-"` // Hidden
|
||||
TotalPrizeValue int64 `json:"-"` // Hidden
|
||||
TotalDiscount int64 `json:"total_discount"` // Total Coupon Discount (Fen)
|
||||
TotalPoints int64 `json:"total_points"` // Total Points Discount (Fen)
|
||||
GamePassCount int64 `json:"game_pass_count"` // Count of SourceType=4
|
||||
ItemCardCount int64 `json:"item_card_count"` // Count where ItemCardID > 0
|
||||
// Breakdown by game type
|
||||
IchibanSpending int64 `json:"ichiban_spending"`
|
||||
IchibanPrize int64 `json:"ichiban_prize"`
|
||||
IchibanProfit int64 `json:"ichiban_profit"`
|
||||
IchibanCount int64 `json:"ichiban_count"`
|
||||
InfiniteSpending int64 `json:"infinite_spending"`
|
||||
InfinitePrize int64 `json:"infinite_prize"`
|
||||
InfiniteProfit int64 `json:"infinite_profit"`
|
||||
InfiniteCount int64 `json:"infinite_count"`
|
||||
MatchingSpending int64 `json:"matching_spending"`
|
||||
MatchingPrize int64 `json:"matching_prize"`
|
||||
MatchingProfit int64 `json:"matching_profit"`
|
||||
MatchingCount int64 `json:"matching_count"`
|
||||
// 直播间统计 (source_type=5)
|
||||
LivestreamSpending int64 `json:"livestream_spending"`
|
||||
LivestreamPrize int64 `json:"livestream_prize"`
|
||||
LivestreamProfit int64 `json:"livestream_profit"`
|
||||
LivestreamCount int64 `json:"livestream_count"`
|
||||
|
||||
Profit int64 `json:"profit"` // Spending - PrizeValue
|
||||
@ -137,10 +141,10 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
||||
h.logger.Info(fmt.Sprintf("SpendingLeaderboard SQL done: count=%d", len(stats)))
|
||||
|
||||
// 2. Collect User IDs
|
||||
userIDs := make([]int64, len(stats))
|
||||
userIDs := make([]int64, 0, len(stats))
|
||||
statMap := make(map[int64]*spendingLeaderboardItem)
|
||||
for i, s := range stats {
|
||||
userIDs[i] = s.UserID
|
||||
for _, s := range stats {
|
||||
userIDs = append(userIDs, s.UserID)
|
||||
statMap[s.UserID] = &spendingLeaderboardItem{
|
||||
UserID: s.UserID,
|
||||
TotalSpending: s.TotalAmount,
|
||||
@ -155,11 +159,39 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
||||
InfiniteCount: s.InfiniteCount,
|
||||
MatchingSpending: s.MatchingSpending,
|
||||
MatchingCount: s.MatchingCount,
|
||||
LivestreamSpending: s.LivestreamSpending,
|
||||
LivestreamSpending: 0, // Will be updated from douyin_orders
|
||||
LivestreamCount: s.LivestreamCount,
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 Fetch Real Douyin Spending
|
||||
if len(userIDs) > 0 {
|
||||
type dyStat struct {
|
||||
UserID int64
|
||||
Amount int64
|
||||
Count int64
|
||||
}
|
||||
var dyStats []dyStat
|
||||
dyQuery := h.repo.GetDbR().Table("douyin_orders").
|
||||
Select("CAST(local_user_id AS SIGNED) as user_id, SUM(actual_pay_amount) as amount, COUNT(*) as count").
|
||||
Where("local_user_id IN ?", userIDs).
|
||||
Where("local_user_id != '' AND local_user_id != '0'")
|
||||
|
||||
if req.RangeType != "all" {
|
||||
dyQuery = dyQuery.Where("created_at >= ?", start).Where("created_at <= ?", end)
|
||||
}
|
||||
|
||||
if err := dyQuery.Group("local_user_id").Scan(&dyStats).Error; err == nil {
|
||||
for _, ds := range dyStats {
|
||||
if item, ok := statMap[ds.UserID]; ok {
|
||||
item.LivestreamSpending = ds.Amount
|
||||
item.LivestreamCount = ds.Count // Use real paid order count
|
||||
item.TotalSpending += ds.Amount // Add to total since orders.total_amount was 0 for these
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(userIDs) > 0 {
|
||||
// 3. Get User Info
|
||||
// Use h.readDB.Users (GEN) as it's simple
|
||||
@ -195,15 +227,14 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
||||
}
|
||||
// Only include Holding (1) and Shipped/Used (3) items. Exclude Void/Decomposed (2).
|
||||
query = query.Where("user_inventory.status IN ?", []int{1, 3}).
|
||||
Where("user_inventory.remark NOT LIKE ? AND user_inventory.remark NOT LIKE ?", "%redeemed%", "%void%")
|
||||
Where("user_inventory.remark NOT LIKE ?", "%void%")
|
||||
|
||||
err := query.Select(`
|
||||
user_inventory.user_id,
|
||||
SUM(products.price) as total_value,
|
||||
SUM(CASE WHEN activities.activity_category_id = 1 THEN products.price ELSE 0 END) as ichiban_prize,
|
||||
SUM(CASE WHEN activities.activity_category_id = 2 THEN products.price ELSE 0 END) as infinite_prize,
|
||||
SUM(CASE WHEN activities.activity_category_id = 3 THEN products.price ELSE 0 END) as matching_prize,
|
||||
SUM(CASE WHEN orders.source_type = 5 THEN products.price ELSE 0 END) as livestream_prize
|
||||
SUM(CASE WHEN activities.activity_category_id = 3 THEN products.price ELSE 0 END) as matching_prize
|
||||
`).
|
||||
Group("user_inventory.user_id").
|
||||
Scan(&invStats).Error
|
||||
@ -215,18 +246,58 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
||||
item.IchibanPrize = is.IchibanPrize
|
||||
item.InfinitePrize = is.InfinitePrize
|
||||
item.MatchingPrize = is.MatchingPrize
|
||||
item.LivestreamPrize = is.LivestreamPrize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4.1 Calculate Livestream Prize Value (From Draw Logs)
|
||||
type lsStat struct {
|
||||
UserID int64
|
||||
Amount int64
|
||||
}
|
||||
var lsStats []lsStat
|
||||
lsQuery := db.Table(model.TableNameLivestreamDrawLogs).
|
||||
Joins("JOIN livestream_prizes ON livestream_prizes.id = livestream_draw_logs.prize_id").
|
||||
Joins("JOIN products ON products.id = livestream_prizes.product_id").
|
||||
Select("livestream_draw_logs.local_user_id as user_id, SUM(products.price) as amount").
|
||||
Where("livestream_draw_logs.local_user_id IN ?", userIDs).
|
||||
Where("livestream_draw_logs.is_refunded = 0")
|
||||
|
||||
if req.RangeType != "all" {
|
||||
lsQuery = lsQuery.Where("livestream_draw_logs.created_at >= ?", start).
|
||||
Where("livestream_draw_logs.created_at <= ?", end)
|
||||
}
|
||||
|
||||
if err := lsQuery.Group("livestream_draw_logs.local_user_id").Scan(&lsStats).Error; err == nil {
|
||||
for _, ls := range lsStats {
|
||||
if item, ok := statMap[ls.UserID]; ok {
|
||||
item.LivestreamPrize = ls.Amount
|
||||
// item.TotalPrizeValue += ls.Amount // Already included in user_inventory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4.2 Calculate Profit for each category
|
||||
for _, item := range statMap {
|
||||
item.IchibanProfit = item.IchibanSpending - item.IchibanPrize
|
||||
item.InfiniteProfit = item.InfiniteSpending - item.InfinitePrize
|
||||
item.MatchingProfit = item.MatchingSpending - item.MatchingPrize
|
||||
item.LivestreamProfit = item.LivestreamSpending - item.LivestreamPrize
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Calculate Profit and Final List
|
||||
list := make([]spendingLeaderboardItem, 0, len(statMap))
|
||||
for _, item := range statMap {
|
||||
item.Profit = item.TotalSpending - item.TotalPrizeValue
|
||||
if item.TotalSpending > 0 {
|
||||
item.ProfitRate = float64(item.Profit) / float64(item.TotalSpending)
|
||||
// Calculate totals based on the 4 displayed categories to ensure UI consistency
|
||||
calculatedSpending := item.IchibanSpending + item.InfiniteSpending + item.MatchingSpending + item.LivestreamSpending
|
||||
calculatedProfit := item.IchibanProfit + item.InfiniteProfit + item.MatchingProfit + item.LivestreamProfit
|
||||
|
||||
item.Profit = calculatedProfit
|
||||
if calculatedSpending > 0 {
|
||||
item.ProfitRate = float64(item.Profit) / float64(calculatedSpending)
|
||||
} else {
|
||||
item.ProfitRate = 0
|
||||
}
|
||||
list = append(list, *item)
|
||||
}
|
||||
|
||||
@ -70,9 +70,11 @@ type douyinOrderItem struct {
|
||||
LocalUserID int64 `json:"local_user_id"`
|
||||
LocalUserNickname string `json:"local_user_nickname"`
|
||||
ActualReceiveAmount string `json:"actual_receive_amount"`
|
||||
ActualPayAmount string `json:"actual_pay_amount"`
|
||||
PayTypeDesc string `json:"pay_type_desc"`
|
||||
Remark string `json:"remark"`
|
||||
UserNickname string `json:"user_nickname"`
|
||||
ProductCount int64 `json:"product_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
@ -129,9 +131,11 @@ func (h *handler) ListDouyinOrders() core.HandlerFunc {
|
||||
LocalUserID: uid,
|
||||
LocalUserNickname: userNicknameMap[uid],
|
||||
ActualReceiveAmount: formatAmount(o.ActualReceiveAmount),
|
||||
ActualPayAmount: formatAmount(o.ActualPayAmount),
|
||||
PayTypeDesc: o.PayTypeDesc,
|
||||
Remark: o.Remark,
|
||||
UserNickname: o.UserNickname,
|
||||
ProductCount: int64(o.ProductCount),
|
||||
CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
@ -182,7 +186,7 @@ func getOrderStatusText(status int32) string {
|
||||
case 3:
|
||||
return "已发货"
|
||||
case 4:
|
||||
return "已取消"
|
||||
return "已退款/已取消"
|
||||
case 5:
|
||||
return "已完成"
|
||||
default:
|
||||
|
||||
@ -8,8 +8,6 @@ import (
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// ======== 抖店商品奖励规则 CRUD ========
|
||||
@ -114,7 +112,7 @@ func (h *handler) CreateDouyinProductReward() core.HandlerFunc {
|
||||
ProductID: req.ProductID,
|
||||
ProductName: req.ProductName,
|
||||
RewardType: req.RewardType,
|
||||
RewardPayload: datatypes.JSON(req.RewardPayload),
|
||||
RewardPayload: string(req.RewardPayload),
|
||||
Quantity: req.Quantity,
|
||||
Status: req.Status,
|
||||
}
|
||||
@ -153,7 +151,7 @@ func (h *handler) UpdateDouyinProductReward() core.HandlerFunc {
|
||||
updates := map[string]any{
|
||||
"product_name": req.ProductName,
|
||||
"reward_type": req.RewardType,
|
||||
"reward_payload": datatypes.JSON(req.RewardPayload),
|
||||
"reward_payload": string(req.RewardPayload),
|
||||
"quantity": req.Quantity,
|
||||
"status": req.Status,
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package admin
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
@ -10,6 +11,8 @@ import (
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"bindbox-game/internal/service/livestream"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ========== 直播间活动管理 ==========
|
||||
@ -89,7 +92,7 @@ func (h *handler) CreateLivestreamActivity() core.HandlerFunc {
|
||||
StreamerContact: activity.StreamerContact,
|
||||
AccessCode: activity.AccessCode,
|
||||
DouyinProductID: activity.DouyinProductID,
|
||||
TicketPrice: activity.TicketPrice,
|
||||
TicketPrice: int64(activity.TicketPrice),
|
||||
Status: activity.Status,
|
||||
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
@ -224,7 +227,7 @@ func (h *handler) ListLivestreamActivities() core.HandlerFunc {
|
||||
StreamerContact: a.StreamerContact,
|
||||
AccessCode: a.AccessCode,
|
||||
DouyinProductID: a.DouyinProductID,
|
||||
TicketPrice: a.TicketPrice,
|
||||
TicketPrice: int64(a.TicketPrice),
|
||||
Status: a.Status,
|
||||
CreatedAt: a.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
@ -273,7 +276,7 @@ func (h *handler) GetLivestreamActivity() core.HandlerFunc {
|
||||
StreamerContact: activity.StreamerContact,
|
||||
AccessCode: activity.AccessCode,
|
||||
DouyinProductID: activity.DouyinProductID,
|
||||
TicketPrice: activity.TicketPrice,
|
||||
TicketPrice: int64(activity.TicketPrice),
|
||||
Status: activity.Status,
|
||||
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
@ -484,14 +487,25 @@ type listLivestreamDrawLogsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Stats *livestreamDrawLogsStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
type livestreamDrawLogsStats struct {
|
||||
UserCount int64 `json:"user_count"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
TotalRev int64 `json:"total_revenue"` // 总流水
|
||||
TotalRefund int64 `json:"total_refund"`
|
||||
TotalCost int64 `json:"total_cost"`
|
||||
NetProfit int64 `json:"net_profit"`
|
||||
}
|
||||
|
||||
type listLivestreamDrawLogsRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
StartTime string `form:"start_time"`
|
||||
EndTime string `form:"end_time"`
|
||||
Keyword string `form:"keyword"`
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
StartTime string `form:"start_time"`
|
||||
EndTime string `form:"end_time"`
|
||||
Keyword string `form:"keyword"`
|
||||
ExcludeUserIDs string `form:"exclude_user_ids"` // 逗号分隔的 UserIDs
|
||||
}
|
||||
|
||||
// ListLivestreamDrawLogs 获取中奖记录
|
||||
@ -530,21 +544,39 @@ func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 解析时间范围
|
||||
// 解析时间范围 (支持 YYYY-MM-DD HH:mm:ss 和 YYYY-MM-DD)
|
||||
var startTime, endTime *time.Time
|
||||
if req.StartTime != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", req.StartTime, time.Local); err == nil {
|
||||
// 尝试解析完整时间
|
||||
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
|
||||
startTime = &t
|
||||
} else if t, err := time.ParseInLocation("2006-01-02", req.StartTime, time.Local); err == nil {
|
||||
// 只有日期,默认 00:00:00
|
||||
startTime = &t
|
||||
}
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", req.EndTime, time.Local); err == nil {
|
||||
// 结束时间设为当天结束
|
||||
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
|
||||
endTime = &t
|
||||
} else if t, err := time.ParseInLocation("2006-01-02", req.EndTime, time.Local); err == nil {
|
||||
// 只有日期,设为当天结束 23:59:59.999
|
||||
end := t.Add(24*time.Hour - time.Nanosecond)
|
||||
endTime = &end
|
||||
}
|
||||
}
|
||||
|
||||
// 解析排除用户ID
|
||||
var excludeUIDs []int64
|
||||
if req.ExcludeUserIDs != "" {
|
||||
parts := strings.Split(req.ExcludeUserIDs, ",")
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if val, err := strconv.ParseInt(p, 10, 64); err == nil && val > 0 {
|
||||
excludeUIDs = append(excludeUIDs, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用底层 GORM 直接查询以支持 keyword
|
||||
db := h.repo.GetDbR().Model(&model.LivestreamDrawLogs{}).Where("activity_id = ?", activityID)
|
||||
|
||||
@ -558,12 +590,115 @@ func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
|
||||
keyword := "%" + req.Keyword + "%"
|
||||
db = db.Where("(user_nickname LIKE ? OR shop_order_id LIKE ? OR prize_name LIKE ?)", keyword, keyword, keyword)
|
||||
}
|
||||
if len(excludeUIDs) > 0 {
|
||||
db = db.Where("local_user_id NOT IN ?", excludeUIDs)
|
||||
}
|
||||
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
// 计算统计数据 (仅当有数据时)
|
||||
var stats *livestreamDrawLogsStats
|
||||
if total > 0 {
|
||||
stats = &livestreamDrawLogsStats{}
|
||||
// 1. 统计用户数
|
||||
// 使用 Session() 避免污染主 db 对象
|
||||
db.Session(&gorm.Session{}).Select("COUNT(DISTINCT douyin_user_id)").Scan(&stats.UserCount)
|
||||
|
||||
// 2. 获取所有相关的 douyin_order_id 和 prize_id,用于在内存中聚合金额和成本
|
||||
// 注意:如果数据量极大,这里可能有性能隐患。但考虑到这是后台查询且通常带有筛选,暂且全量拉取 ID。
|
||||
// 优化:只查需要的字段
|
||||
type logMeta struct {
|
||||
DouyinOrderID int64
|
||||
PrizeID int64
|
||||
ShopOrderID string // 用于关联退款状态查 douyin_orders
|
||||
}
|
||||
var metas []logMeta
|
||||
// 使用不带分页的 db 克隆
|
||||
if err := db.Session(&gorm.Session{}).Select("douyin_order_id, prize_id, shop_order_id").Scan(&metas).Error; err == nil {
|
||||
orderIDs := make([]int64, 0, len(metas))
|
||||
distinctOrderIDs := make(map[int64]bool)
|
||||
prizeIDCount := make(map[int64]int64)
|
||||
|
||||
for _, m := range metas {
|
||||
if !distinctOrderIDs[m.DouyinOrderID] {
|
||||
distinctOrderIDs[m.DouyinOrderID] = true
|
||||
orderIDs = append(orderIDs, m.DouyinOrderID)
|
||||
}
|
||||
}
|
||||
stats.OrderCount = int64(len(orderIDs))
|
||||
|
||||
// 3. 查询订单金额和退款状态
|
||||
if len(orderIDs) > 0 {
|
||||
var orders []model.DouyinOrders
|
||||
// 分批查询防止 IN 子句过长? 暂时假设量级可控
|
||||
h.repo.GetDbR().Select("id, actual_pay_amount, order_status").
|
||||
Where("id IN ?", orderIDs).Find(&orders)
|
||||
|
||||
orderRefundMap := make(map[int64]bool)
|
||||
|
||||
for _, o := range orders {
|
||||
// 统计营收 (总流水)
|
||||
stats.TotalRev += int64(o.ActualPayAmount)
|
||||
|
||||
if o.OrderStatus == 4 { // 已退款
|
||||
stats.TotalRefund += int64(o.ActualPayAmount)
|
||||
orderRefundMap[o.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 统计成本 (剔除退款订单)
|
||||
for _, m := range metas {
|
||||
if !orderRefundMap[m.DouyinOrderID] {
|
||||
prizeIDCount[m.PrizeID]++
|
||||
}
|
||||
}
|
||||
|
||||
// 计算奖品成本 (逻辑参考 GetLivestreamStats,简化版)
|
||||
if len(prizeIDCount) > 0 {
|
||||
prizeIDs := make([]int64, 0, len(prizeIDCount))
|
||||
for pid := range prizeIDCount {
|
||||
prizeIDs = append(prizeIDs, pid)
|
||||
}
|
||||
|
||||
var prizes []model.LivestreamPrizes
|
||||
h.repo.GetDbR().Where("id IN ?", prizeIDs).Find(&prizes)
|
||||
|
||||
// 批量获取关联商品
|
||||
productIDs := make([]int64, 0)
|
||||
for _, p := range prizes {
|
||||
if p.CostPrice == 0 && p.ProductID > 0 {
|
||||
productIDs = append(productIDs, p.ProductID)
|
||||
}
|
||||
}
|
||||
productPriceMap := make(map[int64]int64)
|
||||
if len(productIDs) > 0 {
|
||||
var products []model.Products
|
||||
h.repo.GetDbR().Select("id, price").Where("id IN ?", productIDs).Find(&products)
|
||||
for _, prod := range products {
|
||||
productPriceMap[prod.ID] = prod.Price
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range prizes {
|
||||
cost := p.CostPrice
|
||||
if cost == 0 && p.ProductID > 0 {
|
||||
cost = productPriceMap[p.ProductID]
|
||||
}
|
||||
count := prizeIDCount[p.ID]
|
||||
stats.TotalCost += cost * count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.NetProfit = (stats.TotalRev - stats.TotalRefund) - stats.TotalCost
|
||||
}
|
||||
|
||||
var logs []model.LivestreamDrawLogs
|
||||
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&logs).Error; err != nil {
|
||||
// 重置 Select,确保查询 logs 时获取所有字段 (或者指定 default fields)
|
||||
// db 对象如果被污染,这里需要显式清除 Select。使用 Session 应该能避免。
|
||||
// 安全起见,这里也可以用 db.Session(&gorm.Session{})
|
||||
if err := db.Session(&gorm.Session{}).Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&logs).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||
return
|
||||
}
|
||||
@ -573,6 +708,7 @@ func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Stats: stats,
|
||||
}
|
||||
|
||||
for i, log := range logs {
|
||||
@ -604,6 +740,7 @@ type livestreamCommitmentSummaryResponse struct {
|
||||
HasSeed bool `json:"has_seed"`
|
||||
LenSeed int `json:"len_seed_master"`
|
||||
LenHash int `json:"len_seed_hash"`
|
||||
SeedHashHex string `json:"seed_hash_hex"` // 种子哈希的十六进制表示(可公开复制)
|
||||
}
|
||||
|
||||
// GenerateLivestreamCommitment 生成直播间活动承诺
|
||||
@ -666,6 +803,7 @@ func (h *handler) GetLivestreamCommitmentSummary() core.HandlerFunc {
|
||||
HasSeed: summary.HasSeed,
|
||||
LenSeed: summary.LenSeed,
|
||||
LenHash: summary.LenHash,
|
||||
SeedHashHex: summary.SeedHashHex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,16 +8,29 @@ import (
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type livestreamStatsResponse struct {
|
||||
TotalRevenue int64 `json:"total_revenue"` // 总营收(分)
|
||||
TotalRefund int64 `json:"total_refund"` // 总退款(分)
|
||||
TotalCost int64 `json:"total_cost"` // 总成本(分)
|
||||
NetProfit int64 `json:"net_profit"` // 净利润(分)
|
||||
type dailyLivestreamStats struct {
|
||||
Date string `json:"date"` // 日期
|
||||
TotalRevenue int64 `json:"total_revenue"` // 营收
|
||||
TotalRefund int64 `json:"total_refund"` // 退款
|
||||
TotalCost int64 `json:"total_cost"` // 成本
|
||||
NetProfit int64 `json:"net_profit"` // 净利润
|
||||
ProfitMargin float64 `json:"profit_margin"` // 利润率
|
||||
OrderCount int64 `json:"order_count"` // 订单数
|
||||
RefundCount int64 `json:"refund_count"` // 退款数
|
||||
ProfitMargin float64 `json:"profit_margin"` // 利润率 %
|
||||
RefundCount int64 `json:"refund_count"` // 退款单数
|
||||
}
|
||||
|
||||
type livestreamStatsResponse struct {
|
||||
TotalRevenue int64 `json:"total_revenue"` // 总营收(分)
|
||||
TotalRefund int64 `json:"total_refund"` // 总退款(分)
|
||||
TotalCost int64 `json:"total_cost"` // 总成本(分)
|
||||
NetProfit int64 `json:"net_profit"` // 净利润(分)
|
||||
OrderCount int64 `json:"order_count"` // 订单数
|
||||
RefundCount int64 `json:"refund_count"` // 退款数
|
||||
ProfitMargin float64 `json:"profit_margin"` // 利润率 %
|
||||
Daily []dailyLivestreamStats `json:"daily"` // 每日明细
|
||||
}
|
||||
|
||||
// GetLivestreamStats 获取直播间盈亏统计
|
||||
@ -33,38 +46,124 @@ type livestreamStatsResponse struct {
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||
if err != nil || activityID <= 0 {
|
||||
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(struct {
|
||||
StartTime string `form:"start_time"`
|
||||
EndTime string `form:"end_time"`
|
||||
})
|
||||
_ = ctx.ShouldBindQuery(req)
|
||||
|
||||
var startTime, endTime *time.Time
|
||||
if req.StartTime != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", req.StartTime, time.Local); err == nil {
|
||||
startTime = &t
|
||||
}
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", req.EndTime, time.Local); err == nil {
|
||||
end := t.Add(24*time.Hour - time.Nanosecond)
|
||||
endTime = &end
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取活动信息(门票价格)
|
||||
var activity model.LivestreamActivities
|
||||
if err := h.repo.GetDbR().Where("id = ?", activityID).First(&activity).Error; err != nil {
|
||||
if err := h.repo.GetDbR().Where("id = ?", id).First(&activity).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "活动不存在"))
|
||||
return
|
||||
}
|
||||
ticketPrice := activity.TicketPrice
|
||||
// ticketPrice 暂未使用,但在统计中可能作为参考,这里移除未使用的报错
|
||||
|
||||
// 2. 从 livestream_draw_logs 统计抽奖次数
|
||||
// 2. 核心统计逻辑重构:从关联的订单表获取真实金额
|
||||
// 使用子查询或 Join 来获取不重复的订单金额,确保即便一次订单对应多次抽奖,金额也不重计
|
||||
var totalRevenue, orderCount int64
|
||||
// 统计营收:来自已参与过抽奖(产生过日志)且未退款的订单 (order_status != 4)
|
||||
// 使用 actual_pay_amount (实付金额)
|
||||
queryRevenue := `
|
||||
SELECT
|
||||
CAST(IFNULL(SUM(distinct_orders.actual_pay_amount), 0) AS SIGNED) as rev,
|
||||
COUNT(*) as cnt
|
||||
FROM (
|
||||
SELECT DISTINCT o.id, o.actual_pay_amount
|
||||
FROM douyin_orders o
|
||||
JOIN livestream_draw_logs l ON o.id = l.douyin_order_id
|
||||
WHERE l.activity_id = ?
|
||||
`
|
||||
if startTime != nil {
|
||||
queryRevenue += " AND l.created_at >= '" + startTime.Format("2006-01-02 15:04:05") + "'"
|
||||
}
|
||||
if endTime != nil {
|
||||
queryRevenue += " AND l.created_at <= '" + endTime.Format("2006-01-02 15:04:05") + "'"
|
||||
}
|
||||
queryRevenue += ") as distinct_orders"
|
||||
|
||||
_ = h.repo.GetDbR().Raw(queryRevenue, id).Row().Scan(&totalRevenue, &orderCount)
|
||||
|
||||
// 统计退款:来自已参与过抽奖且标记为退款的订单 (order_status = 4)
|
||||
var totalRefund, refundCount int64
|
||||
queryRefund := `
|
||||
SELECT
|
||||
CAST(IFNULL(SUM(distinct_orders.actual_pay_amount), 0) AS SIGNED) as ref_amt,
|
||||
COUNT(*) as ref_cnt
|
||||
FROM (
|
||||
SELECT DISTINCT o.id, o.actual_pay_amount
|
||||
FROM douyin_orders o
|
||||
JOIN livestream_draw_logs l ON o.id = l.douyin_order_id
|
||||
WHERE l.activity_id = ? AND o.order_status = 4
|
||||
`
|
||||
if startTime != nil {
|
||||
queryRefund += " AND l.created_at >= '" + startTime.Format("2006-01-02 15:04:05") + "'"
|
||||
}
|
||||
if endTime != nil {
|
||||
queryRefund += " AND l.created_at <= '" + endTime.Format("2006-01-02 15:04:05") + "'"
|
||||
}
|
||||
queryRefund += ") as distinct_orders"
|
||||
|
||||
_ = h.repo.GetDbR().Raw(queryRefund, id).Row().Scan(&totalRefund, &refundCount)
|
||||
|
||||
// 3. 获取所有抽奖记录用于成本计算
|
||||
var drawLogs []model.LivestreamDrawLogs
|
||||
if err := h.repo.GetDbR().Where("activity_id = ?", activityID).Find(&drawLogs).Error; err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||
return
|
||||
db := h.repo.GetDbR().Where("activity_id = ?", id)
|
||||
if startTime != nil {
|
||||
db = db.Where("created_at >= ?", startTime)
|
||||
}
|
||||
if endTime != nil {
|
||||
db = db.Where("created_at <= ?", endTime)
|
||||
}
|
||||
db.Find(&drawLogs)
|
||||
|
||||
// 3.1 获取该时间段内所有退款的 shop_order_id 集合,用于过滤成本
|
||||
refundedShopOrderIDs := make(map[string]bool)
|
||||
var refundedOrders []string
|
||||
qRefundIDs := `
|
||||
SELECT DISTINCT o.shop_order_id
|
||||
FROM douyin_orders o
|
||||
JOIN livestream_draw_logs l ON o.id = l.douyin_order_id
|
||||
WHERE l.activity_id = ? AND o.order_status = 4
|
||||
`
|
||||
if startTime != nil {
|
||||
qRefundIDs += " AND l.created_at >= '" + startTime.Format("2006-01-02 15:04:05") + "'"
|
||||
}
|
||||
if endTime != nil {
|
||||
qRefundIDs += " AND l.created_at <= '" + endTime.Format("2006-01-02 15:04:05") + "'"
|
||||
}
|
||||
h.repo.GetDbR().Raw(qRefundIDs, id).Scan(&refundedOrders)
|
||||
for _, oid := range refundedOrders {
|
||||
refundedShopOrderIDs[oid] = true
|
||||
}
|
||||
|
||||
orderCount := int64(len(drawLogs))
|
||||
totalRevenue := orderCount * ticketPrice
|
||||
|
||||
// 3. 统计退款数量
|
||||
var refundCount int64
|
||||
h.repo.GetDbR().Model(&model.LivestreamDrawLogs{}).Where("activity_id = ? AND is_refunded = 1", activityID).Count(&refundCount)
|
||||
totalRefund := refundCount * ticketPrice
|
||||
|
||||
// 4. 计算成本
|
||||
// 4. 计算成本(只统计未退款订单的奖品成本)
|
||||
prizeIDCountMap := make(map[int64]int64)
|
||||
for _, log := range drawLogs {
|
||||
// 排除已退款的订单 (检查 douyin_orders 状态)
|
||||
if refundedShopOrderIDs[log.ShopOrderID] {
|
||||
continue
|
||||
}
|
||||
prizeIDCountMap[log.PrizeID]++
|
||||
}
|
||||
|
||||
@ -74,11 +173,11 @@ func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
||||
}
|
||||
|
||||
var totalCost int64
|
||||
prizeCostMap := make(map[int64]int64)
|
||||
if len(prizeIDs) > 0 {
|
||||
var prizes []model.LivestreamPrizes
|
||||
h.repo.GetDbR().Where("id IN ?", prizeIDs).Find(&prizes)
|
||||
|
||||
prizeCostMap := make(map[int64]int64)
|
||||
productIDsNeedingFallback := make([]int64, 0)
|
||||
prizeProductMap := make(map[int64]int64)
|
||||
|
||||
@ -114,14 +213,110 @@ func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
netProfit := (totalRevenue - totalRefund) - totalCost
|
||||
// 5. 按天分组统计
|
||||
dailyMap := make(map[string]*dailyLivestreamStats)
|
||||
|
||||
// 5.1 统计每日营收和退款(直接累加订单实付金额)
|
||||
type DailyAmount struct {
|
||||
DateKey string
|
||||
Amount int64
|
||||
Count int64
|
||||
IsRefunded int32
|
||||
}
|
||||
var dailyAmounts []DailyAmount
|
||||
queryDailyCorrect := `
|
||||
SELECT
|
||||
date_key,
|
||||
CAST(SUM(actual_pay_amount) AS SIGNED) as amount,
|
||||
COUNT(id) as cnt,
|
||||
refund_flag as is_refunded
|
||||
FROM (
|
||||
SELECT
|
||||
o.id,
|
||||
DATE_FORMAT(MIN(l.created_at), '%Y-%m-%d') as date_key,
|
||||
o.actual_pay_amount,
|
||||
IF(o.order_status = 4, 1, 0) as refund_flag
|
||||
FROM douyin_orders o
|
||||
JOIN livestream_draw_logs l ON o.id = l.douyin_order_id
|
||||
WHERE l.activity_id = ?
|
||||
`
|
||||
if startTime != nil {
|
||||
queryDailyCorrect += " AND l.created_at >= '" + startTime.Format("2006-01-02 15:04:05") + "'"
|
||||
}
|
||||
if endTime != nil {
|
||||
queryDailyCorrect += " AND l.created_at <= '" + endTime.Format("2006-01-02 15:04:05") + "'"
|
||||
}
|
||||
queryDailyCorrect += `
|
||||
GROUP BY o.id
|
||||
) as t
|
||||
GROUP BY date_key, is_refunded
|
||||
`
|
||||
rows, _ := h.repo.GetDbR().Raw(queryDailyCorrect, id).Rows()
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var da DailyAmount
|
||||
_ = rows.Scan(&da.DateKey, &da.Amount, &da.Count, &da.IsRefunded)
|
||||
dailyAmounts = append(dailyAmounts, da)
|
||||
}
|
||||
|
||||
for _, da := range dailyAmounts {
|
||||
if _, ok := dailyMap[da.DateKey]; !ok {
|
||||
dailyMap[da.DateKey] = &dailyLivestreamStats{Date: da.DateKey}
|
||||
}
|
||||
|
||||
// 修正口径:营收(Revenue) = 总流水 (含退款与未退款)
|
||||
// 这样下面的 NetProfit = TotalRevenue - TotalRefund - TotalCost 才不会双重扣除
|
||||
dailyMap[da.DateKey].TotalRevenue += da.Amount
|
||||
dailyMap[da.DateKey].OrderCount += da.Count
|
||||
|
||||
if da.IsRefunded == 1 {
|
||||
dailyMap[da.DateKey].TotalRefund += da.Amount
|
||||
dailyMap[da.DateKey].RefundCount += da.Count
|
||||
}
|
||||
}
|
||||
|
||||
// 5.2 统计每日成本(基于 Logs)
|
||||
for _, log := range drawLogs {
|
||||
// 排除退款订单
|
||||
if refundedShopOrderIDs[log.ShopOrderID] {
|
||||
continue
|
||||
}
|
||||
dateKey := log.CreatedAt.Format("2006-01-02")
|
||||
ds := dailyMap[dateKey]
|
||||
if ds != nil {
|
||||
if cost, ok := prizeCostMap[log.PrizeID]; ok {
|
||||
ds.TotalCost += cost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 汇总每日数据并计算总体指标
|
||||
var calcTotalRevenue, calcTotalRefund, calcTotalCost int64
|
||||
dailyList := make([]dailyLivestreamStats, 0, len(dailyMap))
|
||||
for _, ds := range dailyMap {
|
||||
ds.NetProfit = (ds.TotalRevenue - ds.TotalRefund) - ds.TotalCost
|
||||
netRev := ds.TotalRevenue - ds.TotalRefund
|
||||
if netRev > 0 {
|
||||
ds.ProfitMargin = math.Trunc(float64(ds.NetProfit)/float64(netRev)*10000) / 100
|
||||
} else if netRev == 0 && ds.TotalCost > 0 {
|
||||
ds.ProfitMargin = -100
|
||||
}
|
||||
dailyList = append(dailyList, *ds)
|
||||
|
||||
calcTotalRevenue += ds.TotalRevenue
|
||||
calcTotalRefund += ds.TotalRefund
|
||||
calcTotalCost += ds.TotalCost
|
||||
}
|
||||
|
||||
netProfit := (totalRevenue - totalRefund) - totalCost
|
||||
var margin float64
|
||||
netRevenue := totalRevenue - totalRefund
|
||||
if netRevenue > 0 {
|
||||
margin = float64(netProfit) / float64(netRevenue) * 100
|
||||
} else {
|
||||
} else if netRevenue == 0 && totalCost > 0 {
|
||||
margin = -100
|
||||
} else {
|
||||
margin = 0
|
||||
}
|
||||
|
||||
ctx.Payload(&livestreamStatsResponse{
|
||||
@ -132,6 +327,7 @@ func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
||||
OrderCount: orderCount,
|
||||
RefundCount: refundCount,
|
||||
ProfitMargin: math.Trunc(margin*100) / 100,
|
||||
Daily: dailyList,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ type listUsersRequest struct {
|
||||
InviteCode string `form:"inviteCode"`
|
||||
StartDate string `form:"startDate"`
|
||||
EndDate string `form:"endDate"`
|
||||
ID *int64 `form:"id"`
|
||||
ID string `form:"id"`
|
||||
}
|
||||
type listUsersResponse struct {
|
||||
Page int `json:"page"`
|
||||
@ -74,8 +74,10 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
)
|
||||
|
||||
// 应用搜索条件
|
||||
if req.ID != nil {
|
||||
q = q.Where(h.readDB.Users.ID.Eq(*req.ID))
|
||||
if req.ID != "" {
|
||||
if id, err := strconv.ParseInt(req.ID, 10, 64); err == nil {
|
||||
q = q.Where(h.readDB.Users.ID.Eq(id))
|
||||
}
|
||||
}
|
||||
if req.Nickname != "" {
|
||||
q = q.Where(h.readDB.Users.Nickname.Like("%" + req.Nickname + "%"))
|
||||
@ -196,6 +198,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
||||
Group(h.readDB.Orders.UserID).
|
||||
Scan(&todayRes)
|
||||
@ -209,6 +212,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
||||
Group(h.readDB.Orders.UserID).
|
||||
Scan(&sevenRes)
|
||||
@ -222,6 +226,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
||||
Group(h.readDB.Orders.UserID).
|
||||
Scan(&thirtyRes)
|
||||
@ -235,6 +240,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||
Group(h.readDB.Orders.UserID).
|
||||
Scan(&totalRes)
|
||||
for _, r := range totalRes {
|
||||
@ -385,8 +391,9 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
gpCount := gamePassCounts[v.ID]
|
||||
gtCount := gameTicketCounts[v.ID]
|
||||
|
||||
// 总资产估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次) + 游戏资格(1元/场)
|
||||
assetVal := pointsBal*100 + invVal + cpVal + icVal + gpCount*200 + gtCount*100
|
||||
// 总资产估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次)
|
||||
// 游戏资格不计入估值(购买其他商品赠送,无实际价值)
|
||||
assetVal := pointsBal*100 + invVal + cpVal + icVal + gpCount*200
|
||||
|
||||
rsp.List[i] = adminUserItem{
|
||||
ID: v.ID,
|
||||
@ -397,6 +404,8 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
InviterNickname: inviterNicknames[v.InviterID],
|
||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
DouyinID: v.DouyinID,
|
||||
DouyinUserID: v.DouyinUserID,
|
||||
Remark: v.Remark,
|
||||
ChannelName: v.ChannelName,
|
||||
ChannelCode: v.ChannelCode,
|
||||
PointsBalance: pointsBal,
|
||||
@ -411,6 +420,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
||||
GameTicketCount: gtCount,
|
||||
InventoryValue: invVal,
|
||||
TotalAssetValue: assetVal,
|
||||
Status: v.Status,
|
||||
}
|
||||
}
|
||||
ctx.Payload(rsp)
|
||||
@ -485,6 +495,7 @@ type listInventoryRequest struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
Keyword string `form:"keyword"` // 搜索关键词(商品名称)
|
||||
Status int32 `form:"status"` // 状态筛选:0=全部, 1=持有, 2=作废, 3=已使用
|
||||
}
|
||||
type listInventoryResponse struct {
|
||||
Page int `json:"page"`
|
||||
@ -541,6 +552,8 @@ func (h *handler) ListUserOrders() core.HandlerFunc {
|
||||
// @Param user_id path integer true "用户ID"
|
||||
// @Param page query int true "页码" default(1)
|
||||
// @Param page_size query int true "每页数量,最多100" default(20)
|
||||
// @Param keyword query string false "搜索关键词"
|
||||
// @Param status query int false "状态筛选: 0=全部, 1=持有, 2=作废, 3=已使用"
|
||||
// @Success 200 {object} listInventoryResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/users/{user_id}/inventory [get]
|
||||
@ -576,11 +589,34 @@ func (h *handler) ListUserInventory() core.HandlerFunc {
|
||||
ui := h.readDB.UserInventory
|
||||
p := h.readDB.Products
|
||||
|
||||
// 首先统计符合条件的总数
|
||||
// Check if keyword is numeric
|
||||
numKeyword, errNum := strconv.ParseInt(req.Keyword, 10, 64)
|
||||
|
||||
// Count query logic
|
||||
countQ := h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
||||
LeftJoin(p, p.ID.EqCol(ui.ProductID)).
|
||||
Where(ui.UserID.Eq(userID)).
|
||||
Where(p.Name.Like("%" + req.Keyword + "%"))
|
||||
Where(ui.UserID.Eq(userID))
|
||||
|
||||
// 应用状态筛选
|
||||
if req.Status > 0 {
|
||||
countQ = countQ.Where(ui.Status.Eq(req.Status))
|
||||
} else {
|
||||
// 默认只过滤掉已软删除的记录(如果有的话,status=2是作废,通常后台要能看到作废的,所以这里如果不传status默认查所有非删除的?)
|
||||
// 既然是管理端,如果不传status,应该显示所有状态的记录
|
||||
}
|
||||
|
||||
if errNum == nil {
|
||||
// Keyword is numeric, search by name OR ID OR OrderID
|
||||
countQ = countQ.Where(
|
||||
ui.Where(p.Name.Like("%" + req.Keyword + "%")).
|
||||
Or(ui.ID.Eq(numKeyword)).
|
||||
Or(ui.OrderID.Eq(numKeyword)),
|
||||
)
|
||||
} else {
|
||||
// Keyword is not numeric, search by name only
|
||||
countQ = countQ.Where(p.Name.Like("%" + req.Keyword + "%"))
|
||||
}
|
||||
|
||||
total, err := countQ.Count()
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
||||
@ -604,16 +640,35 @@ func (h *handler) ListUserInventory() core.HandlerFunc {
|
||||
ProductPrice int64
|
||||
}
|
||||
var rows []inventoryRow
|
||||
err = h.repo.GetDbR().Raw(`
|
||||
|
||||
sql := `
|
||||
SELECT ui.id, ui.user_id, ui.product_id, ui.order_id, ui.activity_id, ui.reward_id,
|
||||
ui.status, ui.remark, ui.created_at, ui.updated_at,
|
||||
p.name as product_name, p.images_json as product_images, p.price as product_price
|
||||
FROM user_inventory ui
|
||||
LEFT JOIN products p ON p.id = ui.product_id
|
||||
WHERE ui.user_id = ? AND p.name LIKE ?
|
||||
ORDER BY ui.id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, userID, "%"+req.Keyword+"%", req.PageSize, (req.Page-1)*req.PageSize).Scan(&rows).Error
|
||||
WHERE ui.user_id = ?
|
||||
`
|
||||
var args []interface{}
|
||||
args = append(args, userID)
|
||||
|
||||
if req.Status > 0 {
|
||||
sql += " AND ui.status = ?"
|
||||
args = append(args, req.Status)
|
||||
}
|
||||
|
||||
if errNum == nil {
|
||||
sql += " AND (p.name LIKE ? OR ui.id = ? OR ui.order_id = ?)"
|
||||
args = append(args, "%"+req.Keyword+"%", numKeyword, numKeyword)
|
||||
} else {
|
||||
sql += " AND p.name LIKE ?"
|
||||
args = append(args, "%"+req.Keyword+"%")
|
||||
}
|
||||
|
||||
sql += " ORDER BY ui.id DESC LIMIT ? OFFSET ?"
|
||||
args = append(args, req.PageSize, (req.Page-1)*req.PageSize)
|
||||
|
||||
err = h.repo.GetDbR().Raw(sql, args...).Scan(&rows).Error
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
||||
return
|
||||
@ -648,7 +703,7 @@ func (h *handler) ListUserInventory() core.HandlerFunc {
|
||||
}
|
||||
|
||||
// 无搜索关键词时使用原有逻辑
|
||||
rows, total, err := h.userSvc.ListInventoryWithProduct(ctx.RequestContext(), userID, req.Page, req.PageSize)
|
||||
rows, total, err := h.userSvc.ListInventoryWithProduct(ctx.RequestContext(), userID, req.Page, req.PageSize, req.Status)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
||||
return
|
||||
@ -1085,6 +1140,8 @@ type adminUserItem struct {
|
||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
||||
CreatedAt string `json:"created_at"`
|
||||
DouyinID string `json:"douyin_id"`
|
||||
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID
|
||||
Remark string `json:"remark"` // 备注
|
||||
ChannelName string `json:"channel_name"`
|
||||
ChannelCode string `json:"channel_code"`
|
||||
PointsBalance int64 `json:"points_balance"`
|
||||
@ -1099,6 +1156,7 @@ type adminUserItem struct {
|
||||
GameTicketCount int64 `json:"game_ticket_count"` // 游戏资格数量
|
||||
InventoryValue int64 `json:"inventory_value"` // 持有商品总价值
|
||||
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
||||
Status int32 `json:"status"` // 用户状态:1正常 2禁用 3黑名单
|
||||
}
|
||||
|
||||
// ListAppUsers 管理端用户列表GetUserPointsBalance 查看用户积分余额
|
||||
@ -1496,3 +1554,145 @@ func (h *handler) ListUserCouponUsage() core.HandlerFunc {
|
||||
ctx.Payload(rsp)
|
||||
}
|
||||
}
|
||||
|
||||
// LinkUserDouyinRequest 关联用户抖音账号请求
|
||||
type LinkUserDouyinRequest struct {
|
||||
DouyinUserID string `json:"douyin_user_id" binding:"required"`
|
||||
}
|
||||
|
||||
// UpdateUserDouyinID 更新用户的抖音账号ID
|
||||
// @Summary 更新用户抖音ID
|
||||
// @Description 管理员绑定或修改用户的抖音账号ID
|
||||
// @Tags 管理端.用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path integer true "用户ID"
|
||||
// @Param body body LinkUserDouyinRequest true "抖音用户ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/users/{user_id}/douyin_user_id [put]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) UpdateUserDouyinID() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "用户ID无效"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(LinkUserDouyinRequest)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户抖音ID
|
||||
_, err = h.writeDB.Users.WithContext(ctx.RequestContext()).
|
||||
Where(h.writeDB.Users.ID.Eq(userID)).
|
||||
Update(h.writeDB.Users.DouyinUserID, req.DouyinUserID)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20301, "更新失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Payload(map[string]any{
|
||||
"success": true,
|
||||
"message": "抖音ID更新成功",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// updateUserRemarkRequest 更新用户备注请求
|
||||
type updateUserRemarkRequest struct {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// UpdateUserRemark 更新用户备注
|
||||
// @Summary 更新用户备注
|
||||
// @Description 管理员修改用户备注
|
||||
// @Tags 管理端.用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path integer true "用户ID"
|
||||
// @Param body body updateUserRemarkRequest true "备注信息"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/users/{user_id}/remark [put]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) UpdateUserRemark() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "用户ID无效"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(updateUserRemarkRequest)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户备注
|
||||
_, err = h.writeDB.Users.WithContext(ctx.RequestContext()).
|
||||
Where(h.writeDB.Users.ID.Eq(userID)).
|
||||
Update(h.writeDB.Users.Remark, req.Remark)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20302, "更新失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Payload(map[string]any{
|
||||
"success": true,
|
||||
"message": "备注更新成功",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type updateUserStatusRequest struct {
|
||||
Status int32 `json:"status" form:"status"` // 1=正常 2=禁用 3=黑名单
|
||||
}
|
||||
|
||||
// UpdateUserStatus 修改用户状态
|
||||
// @Summary 修改用户状态
|
||||
// @Description 管理员修改用户状态(1正常 2禁用 3黑名单)
|
||||
// @Tags 管理端.用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path integer true "用户ID"
|
||||
// @Param body body updateUserStatusRequest true "状态信息"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/admin/users/{user_id}/status [put]
|
||||
// @Security LoginVerifyToken
|
||||
func (h *handler) UpdateUserStatus() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
req := new(updateUserStatusRequest)
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||
return
|
||||
}
|
||||
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != 1 && req.Status != 2 && req.Status != 3 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的状态值"))
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 Updates 以支持更新为 0 (虽然这里status不为0) 但 gorm Update 单列更安全
|
||||
_, err = h.writeDB.Users.WithContext(ctx.RequestContext()).
|
||||
Where(h.writeDB.Users.ID.Eq(userID)).
|
||||
Update(h.writeDB.Users.Status, req.Status)
|
||||
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Payload(map[string]any{"success": true})
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ type UserProfileResponse struct {
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
DouyinID string `json:"douyin_id"`
|
||||
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID
|
||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
||||
|
||||
// 邀请统计
|
||||
@ -88,6 +89,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
rsp.InviterID = user.InviterID
|
||||
rsp.ChannelID = user.ChannelID
|
||||
rsp.DouyinID = user.DouyinID
|
||||
rsp.DouyinUserID = user.DouyinUserID
|
||||
rsp.CreatedAt = user.CreatedAt.Format(time.RFC3339)
|
||||
|
||||
// 1.1 查询邀请人昵称
|
||||
@ -123,7 +125,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(1, 2)). // 仅统计商城直购和抽奖票据,排除兑换商品
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
||||
Scan(&os)
|
||||
|
||||
// 分阶段统计
|
||||
@ -131,7 +133,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.ActualAmount.Sum().As("today_paid")).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(1, 2)). // 排除兑换商品
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
||||
Scan(&os.TodayPaid)
|
||||
|
||||
@ -139,7 +141,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.ActualAmount.Sum().As("seven_day_paid")).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(1, 2)). // 排除兑换商品
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
||||
Scan(&os.SevenDayPaid)
|
||||
|
||||
@ -147,7 +149,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.ActualAmount.Sum().As("thirty_day_paid")).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(1, 2)). // 排除兑换商品
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
||||
Scan(&os.ThirtyDayPaid)
|
||||
|
||||
@ -237,9 +239,10 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
||||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(available), 0) FROM user_game_tickets WHERE user_id = ?", userID).Scan(&rsp.CurrentAssets.GameTicketCount).Error
|
||||
|
||||
// 4.5 总资产估值
|
||||
// 估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次) + 游戏资格(1元/场)
|
||||
gamePassValue := rsp.CurrentAssets.GamePassCount * 200 // 估值:2元/次
|
||||
gameTicketValue := rsp.CurrentAssets.GameTicketCount * 100 // 估值:1元/场
|
||||
// 估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次)
|
||||
// 游戏资格不计入估值(购买其他商品赠送,无实际价值)
|
||||
gamePassValue := rsp.CurrentAssets.GamePassCount * 200 // 估值:2元/次
|
||||
gameTicketValue := int64(0) // 游戏资格不计入估值
|
||||
|
||||
rsp.CurrentAssets.TotalAssetValue = rsp.CurrentAssets.PointsBalance +
|
||||
rsp.CurrentAssets.InventoryValue +
|
||||
|
||||
@ -99,6 +99,7 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.ActualAmount.Sum()).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||
Where(h.readDB.Orders.CreatedAt.Lt(start)).
|
||||
Scan(&baseCostPtr)
|
||||
if baseCostPtr != nil {
|
||||
@ -121,6 +122,7 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
||||
orderRows, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||
Where(h.readDB.Orders.CreatedAt.Gte(start)).
|
||||
Where(h.readDB.Orders.CreatedAt.Lte(end)).
|
||||
Find()
|
||||
@ -195,6 +197,7 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
||||
Select(h.readDB.Orders.ActualAmount.Sum()).
|
||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||
Where(h.readDB.Orders.Status.Eq(2)).
|
||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||
Scan(&totalCostPtr)
|
||||
if totalCostPtr != nil {
|
||||
totalCost = *totalCostPtr
|
||||
|
||||
@ -5,7 +5,8 @@ import (
|
||||
)
|
||||
|
||||
type ConfigResponse struct {
|
||||
SubscribeTemplates map[string]string `json:"subscribe_templates"`
|
||||
SubscribeTemplates map[string]string `json:"subscribe_templates"`
|
||||
ContactServiceQRCode string `json:"contact_service_qrcode"` // 客服二维码
|
||||
}
|
||||
|
||||
// GetPublicConfig 获取公开配置(包含订阅模板ID)
|
||||
@ -18,19 +19,30 @@ type ConfigResponse struct {
|
||||
// @Router /api/app/config/public [get]
|
||||
func (h *handler) GetPublicConfig() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
// 查询订阅消息模板 ID
|
||||
var val string
|
||||
cfg, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.SystemConfigs.ConfigKey.Eq("wechat.lottery_result_template_id")).
|
||||
First()
|
||||
if err == nil && cfg != nil {
|
||||
val = cfg.ConfigValue
|
||||
// 查询配置
|
||||
var subscribeTemplateID string
|
||||
var serviceQRCode string
|
||||
|
||||
configs, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.SystemConfigs.ConfigKey.In("wechat.lottery_result_template_id", "contact.service_qrcode")).
|
||||
Find()
|
||||
|
||||
if err == nil {
|
||||
for _, cfg := range configs {
|
||||
switch cfg.ConfigKey {
|
||||
case "wechat.lottery_result_template_id":
|
||||
subscribeTemplateID = cfg.ConfigValue
|
||||
case "contact.service_qrcode":
|
||||
serviceQRCode = cfg.ConfigValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rsp := ConfigResponse{
|
||||
SubscribeTemplates: map[string]string{
|
||||
"lottery_result": val,
|
||||
"lottery_result": subscribeTemplateID,
|
||||
},
|
||||
ContactServiceQRCode: serviceQRCode,
|
||||
}
|
||||
|
||||
ctx.Payload(rsp)
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
@ -191,10 +192,14 @@ func (h *handler) EnterGame() core.HandlerFunc {
|
||||
}
|
||||
|
||||
// 查询剩余次数
|
||||
ticket, _ := h.ticketSvc.GetUserTicketByGame(ctx.RequestContext(), userID, req.GameCode)
|
||||
remaining := 0
|
||||
if ticket != nil {
|
||||
remaining = int(ticket.Available)
|
||||
if req.GameCode == "minesweeper_free" {
|
||||
remaining = 999999 // Represent infinite for free mode
|
||||
} else {
|
||||
ticket, _ := h.ticketSvc.GetUserTicketByGame(ctx.RequestContext(), userID, req.GameCode)
|
||||
if ticket != nil {
|
||||
remaining = int(ticket.Available)
|
||||
}
|
||||
}
|
||||
|
||||
// 从系统配置读取Nakama服务器信息
|
||||
@ -312,8 +317,21 @@ func (h *handler) VerifyTicket() core.HandlerFunc {
|
||||
}
|
||||
|
||||
// 从Redis验证token
|
||||
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
||||
if err != nil || storedUserID != req.UserID {
|
||||
storedValue, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
||||
if err != nil {
|
||||
ctx.Payload(&verifyResponse{Valid: false})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse "userID:gameType"
|
||||
parts := strings.Split(storedValue, ":")
|
||||
if len(parts) < 2 {
|
||||
ctx.Payload(&verifyResponse{Valid: false})
|
||||
return
|
||||
}
|
||||
storedUserID := parts[0]
|
||||
|
||||
if storedUserID != req.UserID {
|
||||
ctx.Payload(&verifyResponse{Valid: false})
|
||||
return
|
||||
}
|
||||
@ -358,16 +376,36 @@ func (h *handler) SettleGame() core.HandlerFunc {
|
||||
}
|
||||
|
||||
// 验证token(可选,如果游戏服务器传了ticket则验证,否则信任internal调用)
|
||||
isFreeMode := false
|
||||
if req.Ticket != "" {
|
||||
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
||||
if err != nil || storedUserID != req.UserID {
|
||||
h.logger.Warn("Ticket validation failed, but proceeding with internal trust",
|
||||
zap.String("ticket", req.Ticket), zap.String("user_id", req.UserID))
|
||||
storedValue, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
||||
if err != nil {
|
||||
h.logger.Warn("Ticket validation failed (not found)", zap.String("ticket", req.Ticket))
|
||||
} else {
|
||||
// 删除token防止重复使用
|
||||
h.redis.Del(ctx.RequestContext(), "game:token:ticket:"+req.Ticket)
|
||||
// Parse "userID:gameType"
|
||||
parts := strings.Split(storedValue, ":")
|
||||
storedUserID := parts[0]
|
||||
|
||||
if len(parts) > 1 && parts[1] == "minesweeper_free" {
|
||||
isFreeMode = true
|
||||
}
|
||||
|
||||
if storedUserID != req.UserID {
|
||||
h.logger.Warn("Ticket validation failed (user mismatch)",
|
||||
zap.String("ticket", req.Ticket), zap.String("user_id", req.UserID), zap.String("stored", storedUserID))
|
||||
} else {
|
||||
// 删除token防止重复使用
|
||||
h.redis.Del(ctx.RequestContext(), "game:token:ticket:"+req.Ticket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 拦截免费场结算
|
||||
if isFreeMode {
|
||||
ctx.Payload(&settleResponse{Success: true, Reward: "体验模式无奖励"})
|
||||
return
|
||||
}
|
||||
|
||||
// 注意:即使ticket验证失败,作为internal API我们仍然信任游戏服务器传来的UserID
|
||||
|
||||
// 奖品发放逻辑
|
||||
@ -407,6 +445,9 @@ func (h *handler) SettleGame() core.HandlerFunc {
|
||||
}
|
||||
|
||||
// 3. 发放奖励
|
||||
// Note: Free mode (minesweeper_free) Settle logic is currently same as paid.
|
||||
// If needed, configure 0 rewards in system config or handle here in future.
|
||||
|
||||
if targetProductID > 0 {
|
||||
res, err := h.userSvc.GrantReward(ctx.RequestContext(), uid, usersvc.GrantRewardRequest{
|
||||
ProductID: targetProductID,
|
||||
@ -468,11 +509,16 @@ func (h *handler) ConsumeTicket() core.HandlerFunc {
|
||||
}
|
||||
|
||||
// 扣减游戏次数
|
||||
err := h.ticketSvc.UseTicket(ctx.RequestContext(), uid, gameCode)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to consume ticket", zap.Int64("user_id", uid), zap.String("game_code", gameCode), zap.Error(err))
|
||||
ctx.Payload(&consumeTicketResponse{Success: false, Error: err.Error()})
|
||||
return
|
||||
if gameCode == "minesweeper_free" {
|
||||
// 免费场场不扣减次数,直接通过
|
||||
h.logger.Info("Free mode consume ticket skipped deduction", zap.Int64("user_id", uid))
|
||||
} else {
|
||||
err := h.ticketSvc.UseTicket(ctx.RequestContext(), uid, gameCode)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to consume ticket", zap.Int64("user_id", uid), zap.String("game_code", gameCode), zap.Error(err))
|
||||
ctx.Payload(&consumeTicketResponse{Success: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 使 ticket 失效(防止重复扣减)
|
||||
|
||||
@ -306,6 +306,11 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
wxConfig = &wechat.WechatConfig{AppID: cfg.AppID, AppSecret: cfg.AppSecret}
|
||||
}
|
||||
|
||||
if wxConfig == nil || wxConfig.AppID == "" {
|
||||
h.logger.Error("微信配置缺失(AppID为空),跳过虚拟发货/抽奖", zap.String("order_no", order.OrderNo))
|
||||
return
|
||||
}
|
||||
|
||||
if ord.SourceType == 2 && act != nil && act.DrawMode == "instant" {
|
||||
_ = h.activity.ProcessOrderLottery(bgCtx, ord.ID)
|
||||
} else if ord.SourceType == 4 {
|
||||
|
||||
@ -298,6 +298,11 @@ func (h *handler) DrawLivestream() core.HandlerFunc {
|
||||
UserNickname: order.UserNickname,
|
||||
})
|
||||
if err != nil {
|
||||
// 检查是否为黑名单错误
|
||||
if err.Error() == "该用户已被列入黑名单,无法开奖" {
|
||||
ctx.AbortWithError(core.Error(http.StatusForbidden, 10008, err.Error()))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10007, err.Error()))
|
||||
return
|
||||
}
|
||||
@ -344,14 +349,8 @@ func (h *handler) SyncLivestreamOrders() core.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务执行全量扫描 (此时已过滤 status=2)
|
||||
result, err := h.douyin.SyncShopOrders(ctx.RequestContext(), activity.ID)
|
||||
// 调用服务执行全量扫描 (基于时间更新,覆盖最近1小时变化)
|
||||
result, err := h.douyin.SyncAllOrders(ctx.RequestContext(), 1*time.Hour)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10004, err.Error()))
|
||||
return
|
||||
@ -376,20 +375,14 @@ func (h *handler) GetLivestreamPendingOrders() core.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
// [核心优化] 自动同步:每次拉取待抽奖列表前,静默执行一次全店扫描
|
||||
_, _ = h.douyin.SyncShopOrders(ctx.RequestContext(), activity.ID)
|
||||
// [核心优化] 自动同步:每次拉取待抽奖列表前,静默执行一次快速全局扫描 (最近 10 分钟)
|
||||
_, _ = h.douyin.SyncAllOrders(ctx.RequestContext(), 10*time.Minute)
|
||||
|
||||
// 查询全店范围内所有待抽奖记录 (Status 2 且未核销完: reward_granted < product_count)
|
||||
var pendingOrders []model.DouyinOrders
|
||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
||||
|
||||
err = db.Where("order_status = 2 AND reward_granted < product_count").
|
||||
err := db.Where("order_status = 2 AND reward_granted < product_count").
|
||||
Find(&pendingOrders).Error
|
||||
|
||||
if err != nil {
|
||||
@ -397,6 +390,40 @@ func (h *handler) GetLivestreamPendingOrders() core.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Payload(pendingOrders)
|
||||
// 查询黑名单用户
|
||||
blacklistMap := make(map[string]bool)
|
||||
if len(pendingOrders) > 0 {
|
||||
var douyinUserIDs []string
|
||||
for _, order := range pendingOrders {
|
||||
if order.DouyinUserID != "" {
|
||||
douyinUserIDs = append(douyinUserIDs, order.DouyinUserID)
|
||||
}
|
||||
}
|
||||
if len(douyinUserIDs) > 0 {
|
||||
var blacklistUsers []model.DouyinBlacklist
|
||||
db.Table("douyin_blacklist").
|
||||
Where("douyin_user_id IN ? AND status = 1", douyinUserIDs).
|
||||
Find(&blacklistUsers)
|
||||
for _, bl := range blacklistUsers {
|
||||
blacklistMap[bl.DouyinUserID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构造响应,包含黑名单状态
|
||||
type OrderWithBlacklist struct {
|
||||
model.DouyinOrders
|
||||
IsBlacklisted bool `json:"is_blacklisted"`
|
||||
}
|
||||
|
||||
result := make([]OrderWithBlacklist, len(pendingOrders))
|
||||
for i, order := range pendingOrders {
|
||||
result[i] = OrderWithBlacklist{
|
||||
DouyinOrders: order,
|
||||
IsBlacklisted: blacklistMap[order.DouyinUserID],
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,8 +30,10 @@ type couponItem struct {
|
||||
ValidStart string `json:"valid_start"`
|
||||
ValidEnd string `json:"valid_end"`
|
||||
Status int32 `json:"status"`
|
||||
StatusDesc string `json:"status_desc"` // 状态描述:未使用、已用完、已过期
|
||||
Rules string `json:"rules"`
|
||||
UsedAt string `json:"used_at,omitempty"` // 使用时间(已使用时返回)
|
||||
UsedAmount int64 `json:"used_amount"` // 已使用金额
|
||||
}
|
||||
|
||||
// ListUserCoupons 查看用户优惠券
|
||||
@ -58,13 +60,13 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
||||
}
|
||||
userID := int64(ctx.SessionUserInfo().Id)
|
||||
|
||||
// 默认查询未使用的优惠券
|
||||
status := int32(1)
|
||||
if req.Status != nil && *req.Status > 0 {
|
||||
// 状态:0未使用 1已使用 2已过期 (直接对接前端标准)
|
||||
status := int32(0)
|
||||
if req.Status != nil {
|
||||
status = *req.Status
|
||||
}
|
||||
|
||||
items, total, err := h.user.ListCouponsByStatus(ctx.RequestContext(), userID, status, req.Page, req.PageSize)
|
||||
items, total, err := h.user.ListAppCoupons(ctx.RequestContext(), userID, status, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error()))
|
||||
return
|
||||
@ -100,14 +102,8 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
||||
rules := ""
|
||||
if sc != nil {
|
||||
name = sc.Name
|
||||
// 金额券:amount 显示模板面值,remaining 显示当前余额
|
||||
if sc.DiscountType == 1 {
|
||||
amount = sc.DiscountValue
|
||||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", it.ID).Scan(&remaining).Error
|
||||
} else {
|
||||
amount = sc.DiscountValue
|
||||
remaining = sc.DiscountValue
|
||||
}
|
||||
amount = sc.DiscountValue
|
||||
remaining = it.BalanceAmount
|
||||
rules = buildCouponRules(sc)
|
||||
}
|
||||
vs := it.ValidStart.Format("2006-01-02 15:04:05")
|
||||
@ -119,7 +115,24 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
||||
if !it.UsedAt.IsZero() {
|
||||
usedAt = it.UsedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
vi := couponItem{ID: it.ID, Name: name, Amount: amount, Remaining: remaining, ValidStart: vs, ValidEnd: ve, Status: it.Status, Rules: rules, UsedAt: usedAt}
|
||||
statusDesc := "未使用"
|
||||
if it.Status == 2 {
|
||||
if it.BalanceAmount == 0 {
|
||||
statusDesc = "已使用"
|
||||
} else {
|
||||
statusDesc = "使用中"
|
||||
}
|
||||
} else if it.Status == 3 {
|
||||
// 若面值等于余额,说明完全没用过,否则为“已到期”
|
||||
sc, ok := mp[it.CouponID]
|
||||
if ok && it.BalanceAmount < sc.DiscountValue {
|
||||
statusDesc = "已到期"
|
||||
} else {
|
||||
statusDesc = "已过期"
|
||||
}
|
||||
}
|
||||
usedAmount := amount - remaining
|
||||
vi := couponItem{ID: it.ID, Name: name, Amount: amount, Remaining: remaining, UsedAmount: usedAmount, ValidStart: vs, ValidEnd: ve, Status: it.Status, StatusDesc: statusDesc, Rules: rules, UsedAt: usedAt}
|
||||
rsp.List = append(rsp.List, vi)
|
||||
}
|
||||
ctx.Payload(rsp)
|
||||
|
||||
@ -15,17 +15,19 @@ type Failure struct {
|
||||
Message string `json:"message"` // 描述信息
|
||||
}
|
||||
|
||||
const (
|
||||
ServerError = 10101
|
||||
ParamBindError = 10102
|
||||
JWTAuthVerifyError = 10103
|
||||
UploadError = 10104
|
||||
const (
|
||||
ServerError = 10101
|
||||
ParamBindError = 10102
|
||||
JWTAuthVerifyError = 10103
|
||||
UploadError = 10104
|
||||
ForbiddenError = 10105
|
||||
AuthorizationError = 10106
|
||||
|
||||
AdminLoginError = 20101
|
||||
CreateAdminError = 20207
|
||||
ListAdminError = 20208
|
||||
ModifyAdminError = 20209
|
||||
DeleteAdminError = 20210
|
||||
AdminLoginError = 20101
|
||||
CreateAdminError = 20207
|
||||
ListAdminError = 20208
|
||||
ModifyAdminError = 20209
|
||||
DeleteAdminError = 20210
|
||||
)
|
||||
|
||||
func Text(code int) string {
|
||||
|
||||
@ -65,6 +65,18 @@ func uploadVirtualShippingInternal(ctx core.Context, accessToken string, key ord
|
||||
if itemDesc == "" {
|
||||
return fmt.Errorf("参数缺失")
|
||||
}
|
||||
|
||||
// Step 1: Check if already shipped to avoid invalid request
|
||||
state, err := GetOrderShippingStatus(context.Background(), accessToken, key)
|
||||
if err == nil {
|
||||
if state >= 2 && state <= 4 {
|
||||
fmt.Printf("[虚拟发货] 订单已发货/完成(state=%d),跳过上传 order_key=%+v\n", state, key)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[虚拟发货] 查询订单状态失败: %v, 继续尝试发货\n", err)
|
||||
}
|
||||
|
||||
reqBody := &uploadShippingInfoRequest{
|
||||
OrderKey: key,
|
||||
LogisticsType: 3,
|
||||
@ -241,6 +253,56 @@ func UploadVirtualShippingForBackground(ctx context.Context, config *WechatConfi
|
||||
return uploadVirtualShippingInternalBackground(ctx, accessToken, orderKey{OrderNumberType: 1, MchID: mchID, OutTradeNo: outTradeNo}, payerOpenid, itemDesc, time.Now())
|
||||
}
|
||||
|
||||
// GetOrderShippingStatusResponse 查询订单发货状态响应
|
||||
type GetOrderShippingStatusResponse struct {
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
Order struct {
|
||||
OrderState int `json:"order_state"` // 1: 待发货, 2: 已发货, 3: 确认收货, 4: 交易完成, 5: 已退款
|
||||
} `json:"order"`
|
||||
}
|
||||
|
||||
// GetOrderShippingStatus 查询订单发货状态
|
||||
// 返回: orderState (1: 待发货, 2: 已发货, 3: 确认收货, 4: 交易完成, 5: 已退款), error
|
||||
func GetOrderShippingStatus(ctx context.Context, accessToken string, key orderKey) (int, error) {
|
||||
if accessToken == "" {
|
||||
return 0, fmt.Errorf("access_token 不能为空")
|
||||
}
|
||||
// 文档: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html#三、查询订单发货状态
|
||||
// get_order 接口参数是扁平的,不使用 order_key 结构
|
||||
reqBody := map[string]any{}
|
||||
if key.TransactionID != "" {
|
||||
reqBody["transaction_id"] = key.TransactionID
|
||||
} else {
|
||||
reqBody["merchant_id"] = key.MchID
|
||||
reqBody["merchant_trade_no"] = key.OutTradeNo
|
||||
}
|
||||
b, _ := json.Marshal(reqBody)
|
||||
|
||||
// fmt.Printf("[虚拟发货-查询] 请求 get_order order_key=%+v\n", key) // Debug log
|
||||
client := httpclient.GetHttpClient()
|
||||
resp, err := client.R().
|
||||
SetQueryParam("access_token", accessToken).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(b).
|
||||
Post("https://api.weixin.qq.com/wxa/sec/order/get_order")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var r GetOrderShippingStatusResponse
|
||||
if err := json.Unmarshal(resp.Body(), &r); err != nil {
|
||||
return 0, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
if r.ErrCode != 0 {
|
||||
// 10060001 = 支付单不存在,视为待发货(或未知的)
|
||||
if r.ErrCode == 10060001 {
|
||||
return 0, nil // Not found
|
||||
}
|
||||
return 0, fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", r.ErrCode, r.ErrMsg)
|
||||
}
|
||||
return r.Order.OrderState, nil
|
||||
}
|
||||
|
||||
// uploadVirtualShippingInternalBackground 后台虚拟发货内部实现(无 core.Context)
|
||||
func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken string, key orderKey, payerOpenid string, itemDesc string, uploadTime time.Time) error {
|
||||
if accessToken == "" {
|
||||
@ -249,6 +311,22 @@ func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken st
|
||||
if itemDesc == "" {
|
||||
return fmt.Errorf("参数缺失")
|
||||
}
|
||||
|
||||
// Step 1: Check if already shipped to avoid invalid request
|
||||
state, err := GetOrderShippingStatus(ctx, accessToken, key)
|
||||
if err == nil {
|
||||
if state >= 2 && state <= 4 {
|
||||
fmt.Printf("[虚拟发货-后台] 订单已发货/完成(state=%d),跳过上传 order_key=%+v\n", state, key)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// Log error but continue to try upload? Or just return error?
|
||||
// If query fails, maybe we should try upload anyway or just log warning.
|
||||
// Let's log warning and continue.
|
||||
fmt.Printf("[虚拟发货-后台] 查询订单状态失败: %v, 继续尝试发货\n", err)
|
||||
}
|
||||
|
||||
// Step 2: Upload shipping info
|
||||
reqBody := &uploadShippingInfoRequest{
|
||||
OrderKey: key,
|
||||
LogisticsType: 3,
|
||||
@ -275,6 +353,11 @@ func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken st
|
||||
return fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
if cr.ErrCode != 0 {
|
||||
// 10060003 = 订单已发货 (Redundant check if status check above passed but state changed or query returned 0)
|
||||
if cr.ErrCode == 10060003 {
|
||||
fmt.Printf("[虚拟发货-后台] 微信返回已发货(10060003),视为成功\n")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", cr.ErrCode, cr.ErrMsg)
|
||||
}
|
||||
return nil
|
||||
|
||||
0
internal/repository/mysql/bindbox.db
Normal file
0
internal/repository/mysql/bindbox.db
Normal file
344
internal/repository/mysql/dao/douyin_blacklist.gen.go
Normal file
344
internal/repository/mysql/dao/douyin_blacklist.gen.go
Normal file
@ -0,0 +1,344 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
|
||||
"gorm.io/plugin/dbresolver"
|
||||
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
func newDouyinBlacklist(db *gorm.DB, opts ...gen.DOOption) douyinBlacklist {
|
||||
_douyinBlacklist := douyinBlacklist{}
|
||||
|
||||
_douyinBlacklist.douyinBlacklistDo.UseDB(db, opts...)
|
||||
_douyinBlacklist.douyinBlacklistDo.UseModel(&model.DouyinBlacklist{})
|
||||
|
||||
tableName := _douyinBlacklist.douyinBlacklistDo.TableName()
|
||||
_douyinBlacklist.ALL = field.NewAsterisk(tableName)
|
||||
_douyinBlacklist.ID = field.NewInt64(tableName, "id")
|
||||
_douyinBlacklist.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
||||
_douyinBlacklist.Reason = field.NewString(tableName, "reason")
|
||||
_douyinBlacklist.OperatorID = field.NewInt64(tableName, "operator_id")
|
||||
_douyinBlacklist.Status = field.NewInt32(tableName, "status")
|
||||
_douyinBlacklist.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_douyinBlacklist.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
|
||||
_douyinBlacklist.fillFieldMap()
|
||||
|
||||
return _douyinBlacklist
|
||||
}
|
||||
|
||||
// douyinBlacklist 抖音用户黑名单表
|
||||
type douyinBlacklist struct {
|
||||
douyinBlacklistDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 主键ID
|
||||
DouyinUserID field.String // 抖音用户ID
|
||||
Reason field.String // 拉黑原因
|
||||
OperatorID field.Int64 // 操作人ID
|
||||
Status field.Int32 // 状态: 1=生效, 0=已解除
|
||||
CreatedAt field.Time // 创建时间
|
||||
UpdatedAt field.Time // 更新时间
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
func (d douyinBlacklist) Table(newTableName string) *douyinBlacklist {
|
||||
d.douyinBlacklistDo.UseTable(newTableName)
|
||||
return d.updateTableName(newTableName)
|
||||
}
|
||||
|
||||
func (d douyinBlacklist) As(alias string) *douyinBlacklist {
|
||||
d.douyinBlacklistDo.DO = *(d.douyinBlacklistDo.As(alias).(*gen.DO))
|
||||
return d.updateTableName(alias)
|
||||
}
|
||||
|
||||
func (d *douyinBlacklist) updateTableName(table string) *douyinBlacklist {
|
||||
d.ALL = field.NewAsterisk(table)
|
||||
d.ID = field.NewInt64(table, "id")
|
||||
d.DouyinUserID = field.NewString(table, "douyin_user_id")
|
||||
d.Reason = field.NewString(table, "reason")
|
||||
d.OperatorID = field.NewInt64(table, "operator_id")
|
||||
d.Status = field.NewInt32(table, "status")
|
||||
d.CreatedAt = field.NewTime(table, "created_at")
|
||||
d.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
|
||||
d.fillFieldMap()
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *douyinBlacklist) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
_f, ok := d.fieldMap[fieldName]
|
||||
if !ok || _f == nil {
|
||||
return nil, false
|
||||
}
|
||||
_oe, ok := _f.(field.OrderExpr)
|
||||
return _oe, ok
|
||||
}
|
||||
|
||||
func (d *douyinBlacklist) fillFieldMap() {
|
||||
d.fieldMap = make(map[string]field.Expr, 7)
|
||||
d.fieldMap["id"] = d.ID
|
||||
d.fieldMap["douyin_user_id"] = d.DouyinUserID
|
||||
d.fieldMap["reason"] = d.Reason
|
||||
d.fieldMap["operator_id"] = d.OperatorID
|
||||
d.fieldMap["status"] = d.Status
|
||||
d.fieldMap["created_at"] = d.CreatedAt
|
||||
d.fieldMap["updated_at"] = d.UpdatedAt
|
||||
}
|
||||
|
||||
func (d douyinBlacklist) clone(db *gorm.DB) douyinBlacklist {
|
||||
d.douyinBlacklistDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d douyinBlacklist) replaceDB(db *gorm.DB) douyinBlacklist {
|
||||
d.douyinBlacklistDo.ReplaceDB(db)
|
||||
return d
|
||||
}
|
||||
|
||||
type douyinBlacklistDo struct{ gen.DO }
|
||||
|
||||
func (d douyinBlacklistDo) Debug() *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Debug())
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) WithContext(ctx context.Context) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) ReadDB() *douyinBlacklistDo {
|
||||
return d.Clauses(dbresolver.Read)
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) WriteDB() *douyinBlacklistDo {
|
||||
return d.Clauses(dbresolver.Write)
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Session(config *gorm.Session) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Session(config))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Clauses(conds ...clause.Expression) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Clauses(conds...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Returning(value interface{}, columns ...string) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Returning(value, columns...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Not(conds ...gen.Condition) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Not(conds...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Or(conds ...gen.Condition) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Or(conds...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Select(conds ...field.Expr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Select(conds...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Where(conds ...gen.Condition) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Where(conds...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Order(conds ...field.Expr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Order(conds...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Distinct(cols ...field.Expr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Distinct(cols...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Omit(cols ...field.Expr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Omit(cols...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Join(table schema.Tabler, on ...field.Expr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Join(table, on...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) LeftJoin(table schema.Tabler, on ...field.Expr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.LeftJoin(table, on...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) RightJoin(table schema.Tabler, on ...field.Expr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.RightJoin(table, on...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Group(cols ...field.Expr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Group(cols...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Having(conds ...gen.Condition) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Having(conds...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Limit(limit int) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Limit(limit))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Offset(offset int) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Offset(offset))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Scopes(funcs...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Unscoped() *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Unscoped())
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Create(values ...*model.DouyinBlacklist) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return d.DO.Create(values)
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) CreateInBatches(values []*model.DouyinBlacklist, batchSize int) error {
|
||||
return d.DO.CreateInBatches(values, batchSize)
|
||||
}
|
||||
|
||||
// Save : !!! underlying implementation is different with GORM
|
||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
||||
func (d douyinBlacklistDo) Save(values ...*model.DouyinBlacklist) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return d.DO.Save(values)
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) First() (*model.DouyinBlacklist, error) {
|
||||
if result, err := d.DO.First(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinBlacklist), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Take() (*model.DouyinBlacklist, error) {
|
||||
if result, err := d.DO.Take(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinBlacklist), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Last() (*model.DouyinBlacklist, error) {
|
||||
if result, err := d.DO.Last(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinBlacklist), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Find() ([]*model.DouyinBlacklist, error) {
|
||||
result, err := d.DO.Find()
|
||||
return result.([]*model.DouyinBlacklist), err
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.DouyinBlacklist, err error) {
|
||||
buf := make([]*model.DouyinBlacklist, 0, batchSize)
|
||||
err = d.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
||||
defer func() { results = append(results, buf...) }()
|
||||
return fc(tx, batch)
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) FindInBatches(result *[]*model.DouyinBlacklist, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
||||
return d.DO.FindInBatches(result, batchSize, fc)
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Attrs(attrs ...field.AssignExpr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Attrs(attrs...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Assign(attrs ...field.AssignExpr) *douyinBlacklistDo {
|
||||
return d.withDO(d.DO.Assign(attrs...))
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Joins(fields ...field.RelationField) *douyinBlacklistDo {
|
||||
for _, _f := range fields {
|
||||
d = *d.withDO(d.DO.Joins(_f))
|
||||
}
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Preload(fields ...field.RelationField) *douyinBlacklistDo {
|
||||
for _, _f := range fields {
|
||||
d = *d.withDO(d.DO.Preload(_f))
|
||||
}
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) FirstOrInit() (*model.DouyinBlacklist, error) {
|
||||
if result, err := d.DO.FirstOrInit(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinBlacklist), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) FirstOrCreate() (*model.DouyinBlacklist, error) {
|
||||
if result, err := d.DO.FirstOrCreate(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinBlacklist), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) FindByPage(offset int, limit int) (result []*model.DouyinBlacklist, count int64, err error) {
|
||||
result, err = d.Offset(offset).Limit(limit).Find()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
||||
count = int64(size + offset)
|
||||
return
|
||||
}
|
||||
|
||||
count, err = d.Offset(-1).Limit(-1).Count()
|
||||
return
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
||||
count, err = d.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = d.Offset(offset).Limit(limit).Scan(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Scan(result interface{}) (err error) {
|
||||
return d.DO.Scan(result)
|
||||
}
|
||||
|
||||
func (d douyinBlacklistDo) Delete(models ...*model.DouyinBlacklist) (result gen.ResultInfo, err error) {
|
||||
return d.DO.Delete(models)
|
||||
}
|
||||
|
||||
func (d *douyinBlacklistDo) withDO(do gen.Dao) *douyinBlacklistDo {
|
||||
d.DO = *do.(*gen.DO)
|
||||
return d
|
||||
}
|
||||
@ -29,16 +29,20 @@ func newDouyinOrders(db *gorm.DB, opts ...gen.DOOption) douyinOrders {
|
||||
_douyinOrders.ALL = field.NewAsterisk(tableName)
|
||||
_douyinOrders.ID = field.NewInt64(tableName, "id")
|
||||
_douyinOrders.ShopOrderID = field.NewString(tableName, "shop_order_id")
|
||||
_douyinOrders.DouyinProductID = field.NewString(tableName, "douyin_product_id")
|
||||
_douyinOrders.OrderStatus = field.NewInt32(tableName, "order_status")
|
||||
_douyinOrders.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
||||
_douyinOrders.LocalUserID = field.NewString(tableName, "local_user_id")
|
||||
_douyinOrders.ActualReceiveAmount = field.NewInt64(tableName, "actual_receive_amount")
|
||||
_douyinOrders.ActualPayAmount = field.NewInt64(tableName, "actual_pay_amount")
|
||||
_douyinOrders.PayTypeDesc = field.NewString(tableName, "pay_type_desc")
|
||||
_douyinOrders.Remark = field.NewString(tableName, "remark")
|
||||
_douyinOrders.UserNickname = field.NewString(tableName, "user_nickname")
|
||||
_douyinOrders.RawData = field.NewString(tableName, "raw_data")
|
||||
_douyinOrders.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_douyinOrders.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_douyinOrders.RewardGranted = field.NewBool(tableName, "reward_granted")
|
||||
_douyinOrders.ProductCount = field.NewInt32(tableName, "product_count")
|
||||
|
||||
_douyinOrders.fillFieldMap()
|
||||
|
||||
@ -52,16 +56,20 @@ type douyinOrders struct {
|
||||
ALL field.Asterisk
|
||||
ID field.Int64
|
||||
ShopOrderID field.String // 抖店订单号
|
||||
DouyinProductID field.String // 关联商品ID
|
||||
OrderStatus field.Int32 // 订单状态: 5=已完成
|
||||
DouyinUserID field.String // 抖店用户ID
|
||||
LocalUserID field.String // 匹配到的本地用户ID
|
||||
ActualReceiveAmount field.Int64 // 实收金额(分)
|
||||
ActualPayAmount field.Int64 // 实付金额(分)
|
||||
PayTypeDesc field.String // 支付方式描述
|
||||
Remark field.String // 备注
|
||||
UserNickname field.String // 抖音昵称
|
||||
RawData field.String // 原始响应数据
|
||||
CreatedAt field.Time
|
||||
UpdatedAt field.Time
|
||||
RewardGranted field.Bool // 奖励已发放: 0=否, 1=是
|
||||
ProductCount field.Int32 // 商品数量
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@ -80,16 +88,20 @@ func (d *douyinOrders) updateTableName(table string) *douyinOrders {
|
||||
d.ALL = field.NewAsterisk(table)
|
||||
d.ID = field.NewInt64(table, "id")
|
||||
d.ShopOrderID = field.NewString(table, "shop_order_id")
|
||||
d.DouyinProductID = field.NewString(table, "douyin_product_id")
|
||||
d.OrderStatus = field.NewInt32(table, "order_status")
|
||||
d.DouyinUserID = field.NewString(table, "douyin_user_id")
|
||||
d.LocalUserID = field.NewString(table, "local_user_id")
|
||||
d.ActualReceiveAmount = field.NewInt64(table, "actual_receive_amount")
|
||||
d.ActualPayAmount = field.NewInt64(table, "actual_pay_amount")
|
||||
d.PayTypeDesc = field.NewString(table, "pay_type_desc")
|
||||
d.Remark = field.NewString(table, "remark")
|
||||
d.UserNickname = field.NewString(table, "user_nickname")
|
||||
d.RawData = field.NewString(table, "raw_data")
|
||||
d.CreatedAt = field.NewTime(table, "created_at")
|
||||
d.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
d.RewardGranted = field.NewBool(table, "reward_granted")
|
||||
d.ProductCount = field.NewInt32(table, "product_count")
|
||||
|
||||
d.fillFieldMap()
|
||||
|
||||
@ -106,19 +118,23 @@ func (d *douyinOrders) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
||||
}
|
||||
|
||||
func (d *douyinOrders) fillFieldMap() {
|
||||
d.fieldMap = make(map[string]field.Expr, 12)
|
||||
d.fieldMap = make(map[string]field.Expr, 16)
|
||||
d.fieldMap["id"] = d.ID
|
||||
d.fieldMap["shop_order_id"] = d.ShopOrderID
|
||||
d.fieldMap["douyin_product_id"] = d.DouyinProductID
|
||||
d.fieldMap["order_status"] = d.OrderStatus
|
||||
d.fieldMap["douyin_user_id"] = d.DouyinUserID
|
||||
d.fieldMap["local_user_id"] = d.LocalUserID
|
||||
d.fieldMap["actual_receive_amount"] = d.ActualReceiveAmount
|
||||
d.fieldMap["actual_pay_amount"] = d.ActualPayAmount
|
||||
d.fieldMap["pay_type_desc"] = d.PayTypeDesc
|
||||
d.fieldMap["remark"] = d.Remark
|
||||
d.fieldMap["user_nickname"] = d.UserNickname
|
||||
d.fieldMap["raw_data"] = d.RawData
|
||||
d.fieldMap["created_at"] = d.CreatedAt
|
||||
d.fieldMap["updated_at"] = d.UpdatedAt
|
||||
d.fieldMap["reward_granted"] = d.RewardGranted
|
||||
d.fieldMap["product_count"] = d.ProductCount
|
||||
}
|
||||
|
||||
func (d douyinOrders) clone(db *gorm.DB) douyinOrders {
|
||||
|
||||
352
internal/repository/mysql/dao/douyin_product_rewards.gen.go
Normal file
352
internal/repository/mysql/dao/douyin_product_rewards.gen.go
Normal file
@ -0,0 +1,352 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
|
||||
"gorm.io/plugin/dbresolver"
|
||||
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
func newDouyinProductRewards(db *gorm.DB, opts ...gen.DOOption) douyinProductRewards {
|
||||
_douyinProductRewards := douyinProductRewards{}
|
||||
|
||||
_douyinProductRewards.douyinProductRewardsDo.UseDB(db, opts...)
|
||||
_douyinProductRewards.douyinProductRewardsDo.UseModel(&model.DouyinProductRewards{})
|
||||
|
||||
tableName := _douyinProductRewards.douyinProductRewardsDo.TableName()
|
||||
_douyinProductRewards.ALL = field.NewAsterisk(tableName)
|
||||
_douyinProductRewards.ID = field.NewInt64(tableName, "id")
|
||||
_douyinProductRewards.ProductID = field.NewString(tableName, "product_id")
|
||||
_douyinProductRewards.ProductName = field.NewString(tableName, "product_name")
|
||||
_douyinProductRewards.RewardType = field.NewString(tableName, "reward_type")
|
||||
_douyinProductRewards.RewardPayload = field.NewString(tableName, "reward_payload")
|
||||
_douyinProductRewards.Quantity = field.NewInt32(tableName, "quantity")
|
||||
_douyinProductRewards.Status = field.NewInt32(tableName, "status")
|
||||
_douyinProductRewards.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_douyinProductRewards.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
|
||||
_douyinProductRewards.fillFieldMap()
|
||||
|
||||
return _douyinProductRewards
|
||||
}
|
||||
|
||||
// douyinProductRewards 抖店商品奖励规则
|
||||
type douyinProductRewards struct {
|
||||
douyinProductRewardsDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Int64
|
||||
ProductID field.String // 抖店商品ID
|
||||
ProductName field.String // 商品名称
|
||||
RewardType field.String // 奖励类型
|
||||
RewardPayload field.String // 奖励参数JSON
|
||||
Quantity field.Int32 // 发放数量
|
||||
Status field.Int32 // 状态: 1=启用 0=禁用
|
||||
CreatedAt field.Time
|
||||
UpdatedAt field.Time
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
func (d douyinProductRewards) Table(newTableName string) *douyinProductRewards {
|
||||
d.douyinProductRewardsDo.UseTable(newTableName)
|
||||
return d.updateTableName(newTableName)
|
||||
}
|
||||
|
||||
func (d douyinProductRewards) As(alias string) *douyinProductRewards {
|
||||
d.douyinProductRewardsDo.DO = *(d.douyinProductRewardsDo.As(alias).(*gen.DO))
|
||||
return d.updateTableName(alias)
|
||||
}
|
||||
|
||||
func (d *douyinProductRewards) updateTableName(table string) *douyinProductRewards {
|
||||
d.ALL = field.NewAsterisk(table)
|
||||
d.ID = field.NewInt64(table, "id")
|
||||
d.ProductID = field.NewString(table, "product_id")
|
||||
d.ProductName = field.NewString(table, "product_name")
|
||||
d.RewardType = field.NewString(table, "reward_type")
|
||||
d.RewardPayload = field.NewString(table, "reward_payload")
|
||||
d.Quantity = field.NewInt32(table, "quantity")
|
||||
d.Status = field.NewInt32(table, "status")
|
||||
d.CreatedAt = field.NewTime(table, "created_at")
|
||||
d.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
|
||||
d.fillFieldMap()
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *douyinProductRewards) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
_f, ok := d.fieldMap[fieldName]
|
||||
if !ok || _f == nil {
|
||||
return nil, false
|
||||
}
|
||||
_oe, ok := _f.(field.OrderExpr)
|
||||
return _oe, ok
|
||||
}
|
||||
|
||||
func (d *douyinProductRewards) fillFieldMap() {
|
||||
d.fieldMap = make(map[string]field.Expr, 9)
|
||||
d.fieldMap["id"] = d.ID
|
||||
d.fieldMap["product_id"] = d.ProductID
|
||||
d.fieldMap["product_name"] = d.ProductName
|
||||
d.fieldMap["reward_type"] = d.RewardType
|
||||
d.fieldMap["reward_payload"] = d.RewardPayload
|
||||
d.fieldMap["quantity"] = d.Quantity
|
||||
d.fieldMap["status"] = d.Status
|
||||
d.fieldMap["created_at"] = d.CreatedAt
|
||||
d.fieldMap["updated_at"] = d.UpdatedAt
|
||||
}
|
||||
|
||||
func (d douyinProductRewards) clone(db *gorm.DB) douyinProductRewards {
|
||||
d.douyinProductRewardsDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d douyinProductRewards) replaceDB(db *gorm.DB) douyinProductRewards {
|
||||
d.douyinProductRewardsDo.ReplaceDB(db)
|
||||
return d
|
||||
}
|
||||
|
||||
type douyinProductRewardsDo struct{ gen.DO }
|
||||
|
||||
func (d douyinProductRewardsDo) Debug() *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Debug())
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) WithContext(ctx context.Context) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) ReadDB() *douyinProductRewardsDo {
|
||||
return d.Clauses(dbresolver.Read)
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) WriteDB() *douyinProductRewardsDo {
|
||||
return d.Clauses(dbresolver.Write)
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Session(config *gorm.Session) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Session(config))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Clauses(conds ...clause.Expression) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Clauses(conds...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Returning(value interface{}, columns ...string) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Returning(value, columns...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Not(conds ...gen.Condition) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Not(conds...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Or(conds ...gen.Condition) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Or(conds...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Select(conds ...field.Expr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Select(conds...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Where(conds ...gen.Condition) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Where(conds...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Order(conds ...field.Expr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Order(conds...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Distinct(cols ...field.Expr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Distinct(cols...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Omit(cols ...field.Expr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Omit(cols...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Join(table schema.Tabler, on ...field.Expr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Join(table, on...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.LeftJoin(table, on...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) RightJoin(table schema.Tabler, on ...field.Expr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.RightJoin(table, on...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Group(cols ...field.Expr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Group(cols...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Having(conds ...gen.Condition) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Having(conds...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Limit(limit int) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Limit(limit))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Offset(offset int) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Offset(offset))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Scopes(funcs...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Unscoped() *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Unscoped())
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Create(values ...*model.DouyinProductRewards) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return d.DO.Create(values)
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) CreateInBatches(values []*model.DouyinProductRewards, batchSize int) error {
|
||||
return d.DO.CreateInBatches(values, batchSize)
|
||||
}
|
||||
|
||||
// Save : !!! underlying implementation is different with GORM
|
||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
||||
func (d douyinProductRewardsDo) Save(values ...*model.DouyinProductRewards) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return d.DO.Save(values)
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) First() (*model.DouyinProductRewards, error) {
|
||||
if result, err := d.DO.First(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinProductRewards), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Take() (*model.DouyinProductRewards, error) {
|
||||
if result, err := d.DO.Take(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinProductRewards), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Last() (*model.DouyinProductRewards, error) {
|
||||
if result, err := d.DO.Last(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinProductRewards), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Find() ([]*model.DouyinProductRewards, error) {
|
||||
result, err := d.DO.Find()
|
||||
return result.([]*model.DouyinProductRewards), err
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.DouyinProductRewards, err error) {
|
||||
buf := make([]*model.DouyinProductRewards, 0, batchSize)
|
||||
err = d.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
||||
defer func() { results = append(results, buf...) }()
|
||||
return fc(tx, batch)
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) FindInBatches(result *[]*model.DouyinProductRewards, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
||||
return d.DO.FindInBatches(result, batchSize, fc)
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Attrs(attrs ...field.AssignExpr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Attrs(attrs...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Assign(attrs ...field.AssignExpr) *douyinProductRewardsDo {
|
||||
return d.withDO(d.DO.Assign(attrs...))
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Joins(fields ...field.RelationField) *douyinProductRewardsDo {
|
||||
for _, _f := range fields {
|
||||
d = *d.withDO(d.DO.Joins(_f))
|
||||
}
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Preload(fields ...field.RelationField) *douyinProductRewardsDo {
|
||||
for _, _f := range fields {
|
||||
d = *d.withDO(d.DO.Preload(_f))
|
||||
}
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) FirstOrInit() (*model.DouyinProductRewards, error) {
|
||||
if result, err := d.DO.FirstOrInit(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinProductRewards), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) FirstOrCreate() (*model.DouyinProductRewards, error) {
|
||||
if result, err := d.DO.FirstOrCreate(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*model.DouyinProductRewards), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) FindByPage(offset int, limit int) (result []*model.DouyinProductRewards, count int64, err error) {
|
||||
result, err = d.Offset(offset).Limit(limit).Find()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
||||
count = int64(size + offset)
|
||||
return
|
||||
}
|
||||
|
||||
count, err = d.Offset(-1).Limit(-1).Count()
|
||||
return
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
||||
count, err = d.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = d.Offset(offset).Limit(limit).Scan(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Scan(result interface{}) (err error) {
|
||||
return d.DO.Scan(result)
|
||||
}
|
||||
|
||||
func (d douyinProductRewardsDo) Delete(models ...*model.DouyinProductRewards) (result gen.ResultInfo, err error) {
|
||||
return d.DO.Delete(models)
|
||||
}
|
||||
|
||||
func (d *douyinProductRewardsDo) withDO(do gen.Dao) *douyinProductRewardsDo {
|
||||
d.DO = *do.(*gen.DO)
|
||||
return d
|
||||
}
|
||||
@ -28,6 +28,9 @@ var (
|
||||
AuditRollbackLogs *auditRollbackLogs
|
||||
Banner *banner
|
||||
Channels *channels
|
||||
DouyinBlacklist *douyinBlacklist
|
||||
DouyinOrders *douyinOrders
|
||||
DouyinProductRewards *douyinProductRewards
|
||||
GamePassPackages *gamePassPackages
|
||||
GameTicketLogs *gameTicketLogs
|
||||
IssuePositionClaims *issuePositionClaims
|
||||
@ -36,6 +39,7 @@ var (
|
||||
LivestreamPrizes *livestreamPrizes
|
||||
LogOperation *logOperation
|
||||
LogRequest *logRequest
|
||||
LotteryRefundLogs *lotteryRefundLogs
|
||||
MatchingCardTypes *matchingCardTypes
|
||||
MenuActions *menuActions
|
||||
Menus *menus
|
||||
@ -62,9 +66,11 @@ var (
|
||||
SystemItemCards *systemItemCards
|
||||
SystemTitleEffects *systemTitleEffects
|
||||
SystemTitles *systemTitles
|
||||
TaskCenterEventLogs *taskCenterEventLogs
|
||||
TaskCenterTaskRewards *taskCenterTaskRewards
|
||||
TaskCenterTaskTiers *taskCenterTaskTiers
|
||||
TaskCenterTasks *taskCenterTasks
|
||||
TaskCenterUserProgress *taskCenterUserProgress
|
||||
UserAddresses *userAddresses
|
||||
UserCouponLedger *userCouponLedger
|
||||
UserCoupons *userCoupons
|
||||
@ -94,6 +100,9 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
AuditRollbackLogs = &Q.AuditRollbackLogs
|
||||
Banner = &Q.Banner
|
||||
Channels = &Q.Channels
|
||||
DouyinBlacklist = &Q.DouyinBlacklist
|
||||
DouyinOrders = &Q.DouyinOrders
|
||||
DouyinProductRewards = &Q.DouyinProductRewards
|
||||
GamePassPackages = &Q.GamePassPackages
|
||||
GameTicketLogs = &Q.GameTicketLogs
|
||||
IssuePositionClaims = &Q.IssuePositionClaims
|
||||
@ -102,6 +111,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
LivestreamPrizes = &Q.LivestreamPrizes
|
||||
LogOperation = &Q.LogOperation
|
||||
LogRequest = &Q.LogRequest
|
||||
LotteryRefundLogs = &Q.LotteryRefundLogs
|
||||
MatchingCardTypes = &Q.MatchingCardTypes
|
||||
MenuActions = &Q.MenuActions
|
||||
Menus = &Q.Menus
|
||||
@ -128,9 +138,11 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
SystemItemCards = &Q.SystemItemCards
|
||||
SystemTitleEffects = &Q.SystemTitleEffects
|
||||
SystemTitles = &Q.SystemTitles
|
||||
TaskCenterEventLogs = &Q.TaskCenterEventLogs
|
||||
TaskCenterTaskRewards = &Q.TaskCenterTaskRewards
|
||||
TaskCenterTaskTiers = &Q.TaskCenterTaskTiers
|
||||
TaskCenterTasks = &Q.TaskCenterTasks
|
||||
TaskCenterUserProgress = &Q.TaskCenterUserProgress
|
||||
UserAddresses = &Q.UserAddresses
|
||||
UserCouponLedger = &Q.UserCouponLedger
|
||||
UserCoupons = &Q.UserCoupons
|
||||
@ -161,6 +173,9 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
AuditRollbackLogs: newAuditRollbackLogs(db, opts...),
|
||||
Banner: newBanner(db, opts...),
|
||||
Channels: newChannels(db, opts...),
|
||||
DouyinBlacklist: newDouyinBlacklist(db, opts...),
|
||||
DouyinOrders: newDouyinOrders(db, opts...),
|
||||
DouyinProductRewards: newDouyinProductRewards(db, opts...),
|
||||
GamePassPackages: newGamePassPackages(db, opts...),
|
||||
GameTicketLogs: newGameTicketLogs(db, opts...),
|
||||
IssuePositionClaims: newIssuePositionClaims(db, opts...),
|
||||
@ -169,6 +184,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
LivestreamPrizes: newLivestreamPrizes(db, opts...),
|
||||
LogOperation: newLogOperation(db, opts...),
|
||||
LogRequest: newLogRequest(db, opts...),
|
||||
LotteryRefundLogs: newLotteryRefundLogs(db, opts...),
|
||||
MatchingCardTypes: newMatchingCardTypes(db, opts...),
|
||||
MenuActions: newMenuActions(db, opts...),
|
||||
Menus: newMenus(db, opts...),
|
||||
@ -195,9 +211,11 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
SystemItemCards: newSystemItemCards(db, opts...),
|
||||
SystemTitleEffects: newSystemTitleEffects(db, opts...),
|
||||
SystemTitles: newSystemTitles(db, opts...),
|
||||
TaskCenterEventLogs: newTaskCenterEventLogs(db, opts...),
|
||||
TaskCenterTaskRewards: newTaskCenterTaskRewards(db, opts...),
|
||||
TaskCenterTaskTiers: newTaskCenterTaskTiers(db, opts...),
|
||||
TaskCenterTasks: newTaskCenterTasks(db, opts...),
|
||||
TaskCenterUserProgress: newTaskCenterUserProgress(db, opts...),
|
||||
UserAddresses: newUserAddresses(db, opts...),
|
||||
UserCouponLedger: newUserCouponLedger(db, opts...),
|
||||
UserCoupons: newUserCoupons(db, opts...),
|
||||
@ -229,6 +247,9 @@ type Query struct {
|
||||
AuditRollbackLogs auditRollbackLogs
|
||||
Banner banner
|
||||
Channels channels
|
||||
DouyinBlacklist douyinBlacklist
|
||||
DouyinOrders douyinOrders
|
||||
DouyinProductRewards douyinProductRewards
|
||||
GamePassPackages gamePassPackages
|
||||
GameTicketLogs gameTicketLogs
|
||||
IssuePositionClaims issuePositionClaims
|
||||
@ -237,6 +258,7 @@ type Query struct {
|
||||
LivestreamPrizes livestreamPrizes
|
||||
LogOperation logOperation
|
||||
LogRequest logRequest
|
||||
LotteryRefundLogs lotteryRefundLogs
|
||||
MatchingCardTypes matchingCardTypes
|
||||
MenuActions menuActions
|
||||
Menus menus
|
||||
@ -263,9 +285,11 @@ type Query struct {
|
||||
SystemItemCards systemItemCards
|
||||
SystemTitleEffects systemTitleEffects
|
||||
SystemTitles systemTitles
|
||||
TaskCenterEventLogs taskCenterEventLogs
|
||||
TaskCenterTaskRewards taskCenterTaskRewards
|
||||
TaskCenterTaskTiers taskCenterTaskTiers
|
||||
TaskCenterTasks taskCenterTasks
|
||||
TaskCenterUserProgress taskCenterUserProgress
|
||||
UserAddresses userAddresses
|
||||
UserCouponLedger userCouponLedger
|
||||
UserCoupons userCoupons
|
||||
@ -298,6 +322,9 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
AuditRollbackLogs: q.AuditRollbackLogs.clone(db),
|
||||
Banner: q.Banner.clone(db),
|
||||
Channels: q.Channels.clone(db),
|
||||
DouyinBlacklist: q.DouyinBlacklist.clone(db),
|
||||
DouyinOrders: q.DouyinOrders.clone(db),
|
||||
DouyinProductRewards: q.DouyinProductRewards.clone(db),
|
||||
GamePassPackages: q.GamePassPackages.clone(db),
|
||||
GameTicketLogs: q.GameTicketLogs.clone(db),
|
||||
IssuePositionClaims: q.IssuePositionClaims.clone(db),
|
||||
@ -306,6 +333,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
LivestreamPrizes: q.LivestreamPrizes.clone(db),
|
||||
LogOperation: q.LogOperation.clone(db),
|
||||
LogRequest: q.LogRequest.clone(db),
|
||||
LotteryRefundLogs: q.LotteryRefundLogs.clone(db),
|
||||
MatchingCardTypes: q.MatchingCardTypes.clone(db),
|
||||
MenuActions: q.MenuActions.clone(db),
|
||||
Menus: q.Menus.clone(db),
|
||||
@ -332,9 +360,11 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
SystemItemCards: q.SystemItemCards.clone(db),
|
||||
SystemTitleEffects: q.SystemTitleEffects.clone(db),
|
||||
SystemTitles: q.SystemTitles.clone(db),
|
||||
TaskCenterEventLogs: q.TaskCenterEventLogs.clone(db),
|
||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.clone(db),
|
||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.clone(db),
|
||||
TaskCenterTasks: q.TaskCenterTasks.clone(db),
|
||||
TaskCenterUserProgress: q.TaskCenterUserProgress.clone(db),
|
||||
UserAddresses: q.UserAddresses.clone(db),
|
||||
UserCouponLedger: q.UserCouponLedger.clone(db),
|
||||
UserCoupons: q.UserCoupons.clone(db),
|
||||
@ -374,6 +404,9 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
AuditRollbackLogs: q.AuditRollbackLogs.replaceDB(db),
|
||||
Banner: q.Banner.replaceDB(db),
|
||||
Channels: q.Channels.replaceDB(db),
|
||||
DouyinBlacklist: q.DouyinBlacklist.replaceDB(db),
|
||||
DouyinOrders: q.DouyinOrders.replaceDB(db),
|
||||
DouyinProductRewards: q.DouyinProductRewards.replaceDB(db),
|
||||
GamePassPackages: q.GamePassPackages.replaceDB(db),
|
||||
GameTicketLogs: q.GameTicketLogs.replaceDB(db),
|
||||
IssuePositionClaims: q.IssuePositionClaims.replaceDB(db),
|
||||
@ -382,6 +415,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
LivestreamPrizes: q.LivestreamPrizes.replaceDB(db),
|
||||
LogOperation: q.LogOperation.replaceDB(db),
|
||||
LogRequest: q.LogRequest.replaceDB(db),
|
||||
LotteryRefundLogs: q.LotteryRefundLogs.replaceDB(db),
|
||||
MatchingCardTypes: q.MatchingCardTypes.replaceDB(db),
|
||||
MenuActions: q.MenuActions.replaceDB(db),
|
||||
Menus: q.Menus.replaceDB(db),
|
||||
@ -408,9 +442,11 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
SystemItemCards: q.SystemItemCards.replaceDB(db),
|
||||
SystemTitleEffects: q.SystemTitleEffects.replaceDB(db),
|
||||
SystemTitles: q.SystemTitles.replaceDB(db),
|
||||
TaskCenterEventLogs: q.TaskCenterEventLogs.replaceDB(db),
|
||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.replaceDB(db),
|
||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.replaceDB(db),
|
||||
TaskCenterTasks: q.TaskCenterTasks.replaceDB(db),
|
||||
TaskCenterUserProgress: q.TaskCenterUserProgress.replaceDB(db),
|
||||
UserAddresses: q.UserAddresses.replaceDB(db),
|
||||
UserCouponLedger: q.UserCouponLedger.replaceDB(db),
|
||||
UserCoupons: q.UserCoupons.replaceDB(db),
|
||||
@ -440,6 +476,9 @@ type queryCtx struct {
|
||||
AuditRollbackLogs *auditRollbackLogsDo
|
||||
Banner *bannerDo
|
||||
Channels *channelsDo
|
||||
DouyinBlacklist *douyinBlacklistDo
|
||||
DouyinOrders *douyinOrdersDo
|
||||
DouyinProductRewards *douyinProductRewardsDo
|
||||
GamePassPackages *gamePassPackagesDo
|
||||
GameTicketLogs *gameTicketLogsDo
|
||||
IssuePositionClaims *issuePositionClaimsDo
|
||||
@ -448,6 +487,7 @@ type queryCtx struct {
|
||||
LivestreamPrizes *livestreamPrizesDo
|
||||
LogOperation *logOperationDo
|
||||
LogRequest *logRequestDo
|
||||
LotteryRefundLogs *lotteryRefundLogsDo
|
||||
MatchingCardTypes *matchingCardTypesDo
|
||||
MenuActions *menuActionsDo
|
||||
Menus *menusDo
|
||||
@ -474,9 +514,11 @@ type queryCtx struct {
|
||||
SystemItemCards *systemItemCardsDo
|
||||
SystemTitleEffects *systemTitleEffectsDo
|
||||
SystemTitles *systemTitlesDo
|
||||
TaskCenterEventLogs *taskCenterEventLogsDo
|
||||
TaskCenterTaskRewards *taskCenterTaskRewardsDo
|
||||
TaskCenterTaskTiers *taskCenterTaskTiersDo
|
||||
TaskCenterTasks *taskCenterTasksDo
|
||||
TaskCenterUserProgress *taskCenterUserProgressDo
|
||||
UserAddresses *userAddressesDo
|
||||
UserCouponLedger *userCouponLedgerDo
|
||||
UserCoupons *userCouponsDo
|
||||
@ -506,6 +548,9 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
AuditRollbackLogs: q.AuditRollbackLogs.WithContext(ctx),
|
||||
Banner: q.Banner.WithContext(ctx),
|
||||
Channels: q.Channels.WithContext(ctx),
|
||||
DouyinBlacklist: q.DouyinBlacklist.WithContext(ctx),
|
||||
DouyinOrders: q.DouyinOrders.WithContext(ctx),
|
||||
DouyinProductRewards: q.DouyinProductRewards.WithContext(ctx),
|
||||
GamePassPackages: q.GamePassPackages.WithContext(ctx),
|
||||
GameTicketLogs: q.GameTicketLogs.WithContext(ctx),
|
||||
IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx),
|
||||
@ -514,6 +559,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
LivestreamPrizes: q.LivestreamPrizes.WithContext(ctx),
|
||||
LogOperation: q.LogOperation.WithContext(ctx),
|
||||
LogRequest: q.LogRequest.WithContext(ctx),
|
||||
LotteryRefundLogs: q.LotteryRefundLogs.WithContext(ctx),
|
||||
MatchingCardTypes: q.MatchingCardTypes.WithContext(ctx),
|
||||
MenuActions: q.MenuActions.WithContext(ctx),
|
||||
Menus: q.Menus.WithContext(ctx),
|
||||
@ -540,9 +586,11 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
SystemItemCards: q.SystemItemCards.WithContext(ctx),
|
||||
SystemTitleEffects: q.SystemTitleEffects.WithContext(ctx),
|
||||
SystemTitles: q.SystemTitles.WithContext(ctx),
|
||||
TaskCenterEventLogs: q.TaskCenterEventLogs.WithContext(ctx),
|
||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.WithContext(ctx),
|
||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.WithContext(ctx),
|
||||
TaskCenterTasks: q.TaskCenterTasks.WithContext(ctx),
|
||||
TaskCenterUserProgress: q.TaskCenterUserProgress.WithContext(ctx),
|
||||
UserAddresses: q.UserAddresses.WithContext(ctx),
|
||||
UserCouponLedger: q.UserCouponLedger.WithContext(ctx),
|
||||
UserCoupons: q.UserCoupons.WithContext(ctx),
|
||||
|
||||
@ -34,11 +34,16 @@ func newLivestreamActivities(db *gorm.DB, opts ...gen.DOOption) livestreamActivi
|
||||
_livestreamActivities.AccessCode = field.NewString(tableName, "access_code")
|
||||
_livestreamActivities.DouyinProductID = field.NewString(tableName, "douyin_product_id")
|
||||
_livestreamActivities.Status = field.NewInt32(tableName, "status")
|
||||
_livestreamActivities.CommitmentAlgo = field.NewString(tableName, "commitment_algo")
|
||||
_livestreamActivities.CommitmentSeedMaster = field.NewBytes(tableName, "commitment_seed_master")
|
||||
_livestreamActivities.CommitmentSeedHash = field.NewBytes(tableName, "commitment_seed_hash")
|
||||
_livestreamActivities.CommitmentStateVersion = field.NewInt32(tableName, "commitment_state_version")
|
||||
_livestreamActivities.StartTime = field.NewTime(tableName, "start_time")
|
||||
_livestreamActivities.EndTime = field.NewTime(tableName, "end_time")
|
||||
_livestreamActivities.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_livestreamActivities.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_livestreamActivities.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_livestreamActivities.TicketPrice = field.NewInt32(tableName, "ticket_price")
|
||||
|
||||
_livestreamActivities.fillFieldMap()
|
||||
|
||||
@ -49,19 +54,24 @@ func newLivestreamActivities(db *gorm.DB, opts ...gen.DOOption) livestreamActivi
|
||||
type livestreamActivities struct {
|
||||
livestreamActivitiesDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 主键ID
|
||||
Name field.String // 活动名称
|
||||
StreamerName field.String // 主播名称
|
||||
StreamerContact field.String // 主播联系方式
|
||||
AccessCode field.String // 唯一访问码
|
||||
DouyinProductID field.String // 关联抖店商品ID
|
||||
Status field.Int32 // 状态:1进行中 2已结束
|
||||
StartTime field.Time // 开始时间
|
||||
EndTime field.Time // 结束时间
|
||||
CreatedAt field.Time // 创建时间
|
||||
UpdatedAt field.Time // 更新时间
|
||||
DeletedAt field.Field // 删除时间
|
||||
ALL field.Asterisk
|
||||
ID field.Int64 // 主键ID
|
||||
Name field.String // 活动名称
|
||||
StreamerName field.String // 主播名称
|
||||
StreamerContact field.String // 主播联系方式
|
||||
AccessCode field.String // 唯一访问码
|
||||
DouyinProductID field.String // 关联抖店商品ID
|
||||
Status field.Int32 // 状态:1进行中 2已结束
|
||||
CommitmentAlgo field.String // 承诺算法版本
|
||||
CommitmentSeedMaster field.Bytes // 主种子(32字节)
|
||||
CommitmentSeedHash field.Bytes // 种子SHA256哈希
|
||||
CommitmentStateVersion field.Int32 // 状态版本
|
||||
StartTime field.Time // 开始时间
|
||||
EndTime field.Time // 结束时间
|
||||
CreatedAt field.Time // 创建时间
|
||||
UpdatedAt field.Time // 更新时间
|
||||
DeletedAt field.Field // 删除时间
|
||||
TicketPrice field.Int32
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@ -85,11 +95,16 @@ func (l *livestreamActivities) updateTableName(table string) *livestreamActiviti
|
||||
l.AccessCode = field.NewString(table, "access_code")
|
||||
l.DouyinProductID = field.NewString(table, "douyin_product_id")
|
||||
l.Status = field.NewInt32(table, "status")
|
||||
l.CommitmentAlgo = field.NewString(table, "commitment_algo")
|
||||
l.CommitmentSeedMaster = field.NewBytes(table, "commitment_seed_master")
|
||||
l.CommitmentSeedHash = field.NewBytes(table, "commitment_seed_hash")
|
||||
l.CommitmentStateVersion = field.NewInt32(table, "commitment_state_version")
|
||||
l.StartTime = field.NewTime(table, "start_time")
|
||||
l.EndTime = field.NewTime(table, "end_time")
|
||||
l.CreatedAt = field.NewTime(table, "created_at")
|
||||
l.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
l.DeletedAt = field.NewField(table, "deleted_at")
|
||||
l.TicketPrice = field.NewInt32(table, "ticket_price")
|
||||
|
||||
l.fillFieldMap()
|
||||
|
||||
@ -106,7 +121,7 @@ func (l *livestreamActivities) GetFieldByName(fieldName string) (field.OrderExpr
|
||||
}
|
||||
|
||||
func (l *livestreamActivities) fillFieldMap() {
|
||||
l.fieldMap = make(map[string]field.Expr, 12)
|
||||
l.fieldMap = make(map[string]field.Expr, 17)
|
||||
l.fieldMap["id"] = l.ID
|
||||
l.fieldMap["name"] = l.Name
|
||||
l.fieldMap["streamer_name"] = l.StreamerName
|
||||
@ -114,11 +129,16 @@ func (l *livestreamActivities) fillFieldMap() {
|
||||
l.fieldMap["access_code"] = l.AccessCode
|
||||
l.fieldMap["douyin_product_id"] = l.DouyinProductID
|
||||
l.fieldMap["status"] = l.Status
|
||||
l.fieldMap["commitment_algo"] = l.CommitmentAlgo
|
||||
l.fieldMap["commitment_seed_master"] = l.CommitmentSeedMaster
|
||||
l.fieldMap["commitment_seed_hash"] = l.CommitmentSeedHash
|
||||
l.fieldMap["commitment_state_version"] = l.CommitmentStateVersion
|
||||
l.fieldMap["start_time"] = l.StartTime
|
||||
l.fieldMap["end_time"] = l.EndTime
|
||||
l.fieldMap["created_at"] = l.CreatedAt
|
||||
l.fieldMap["updated_at"] = l.UpdatedAt
|
||||
l.fieldMap["deleted_at"] = l.DeletedAt
|
||||
l.fieldMap["ticket_price"] = l.TicketPrice
|
||||
}
|
||||
|
||||
func (l livestreamActivities) clone(db *gorm.DB) livestreamActivities {
|
||||
|
||||
@ -31,14 +31,18 @@ func newLivestreamDrawLogs(db *gorm.DB, opts ...gen.DOOption) livestreamDrawLogs
|
||||
_livestreamDrawLogs.ActivityID = field.NewInt64(tableName, "activity_id")
|
||||
_livestreamDrawLogs.PrizeID = field.NewInt64(tableName, "prize_id")
|
||||
_livestreamDrawLogs.DouyinOrderID = field.NewInt64(tableName, "douyin_order_id")
|
||||
_livestreamDrawLogs.ShopOrderID = field.NewString(tableName, "shop_order_id")
|
||||
_livestreamDrawLogs.LocalUserID = field.NewInt64(tableName, "local_user_id")
|
||||
_livestreamDrawLogs.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
||||
_livestreamDrawLogs.UserNickname = field.NewString(tableName, "user_nickname")
|
||||
_livestreamDrawLogs.PrizeName = field.NewString(tableName, "prize_name")
|
||||
_livestreamDrawLogs.Level = field.NewInt32(tableName, "level")
|
||||
_livestreamDrawLogs.SeedHash = field.NewString(tableName, "seed_hash")
|
||||
_livestreamDrawLogs.RandValue = field.NewInt64(tableName, "rand_value")
|
||||
_livestreamDrawLogs.WeightsTotal = field.NewInt64(tableName, "weights_total")
|
||||
_livestreamDrawLogs.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_livestreamDrawLogs.IsGranted = field.NewBool(tableName, "is_granted")
|
||||
_livestreamDrawLogs.IsRefunded = field.NewInt32(tableName, "is_refunded")
|
||||
|
||||
_livestreamDrawLogs.fillFieldMap()
|
||||
|
||||
@ -54,14 +58,18 @@ type livestreamDrawLogs struct {
|
||||
ActivityID field.Int64 // 关联livestream_activities.id
|
||||
PrizeID field.Int64 // 关联livestream_prizes.id
|
||||
DouyinOrderID field.Int64 // 关联douyin_orders.id
|
||||
ShopOrderID field.String // 抖店订单号
|
||||
LocalUserID field.Int64 // 本地用户ID
|
||||
DouyinUserID field.String // 抖音用户ID
|
||||
UserNickname field.String // 用户昵称
|
||||
PrizeName field.String // 中奖奖品名称快照
|
||||
Level field.Int32 // 奖品等级
|
||||
SeedHash field.String // 哈希种子
|
||||
RandValue field.Int64 // 随机值
|
||||
WeightsTotal field.Int64 // 权重总和
|
||||
CreatedAt field.Time // 中奖时间
|
||||
IsGranted field.Bool // 是否已发放奖品
|
||||
IsRefunded field.Int32 // 订单是否已退款
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@ -82,14 +90,18 @@ func (l *livestreamDrawLogs) updateTableName(table string) *livestreamDrawLogs {
|
||||
l.ActivityID = field.NewInt64(table, "activity_id")
|
||||
l.PrizeID = field.NewInt64(table, "prize_id")
|
||||
l.DouyinOrderID = field.NewInt64(table, "douyin_order_id")
|
||||
l.ShopOrderID = field.NewString(table, "shop_order_id")
|
||||
l.LocalUserID = field.NewInt64(table, "local_user_id")
|
||||
l.DouyinUserID = field.NewString(table, "douyin_user_id")
|
||||
l.UserNickname = field.NewString(table, "user_nickname")
|
||||
l.PrizeName = field.NewString(table, "prize_name")
|
||||
l.Level = field.NewInt32(table, "level")
|
||||
l.SeedHash = field.NewString(table, "seed_hash")
|
||||
l.RandValue = field.NewInt64(table, "rand_value")
|
||||
l.WeightsTotal = field.NewInt64(table, "weights_total")
|
||||
l.CreatedAt = field.NewTime(table, "created_at")
|
||||
l.IsGranted = field.NewBool(table, "is_granted")
|
||||
l.IsRefunded = field.NewInt32(table, "is_refunded")
|
||||
|
||||
l.fillFieldMap()
|
||||
|
||||
@ -106,19 +118,23 @@ func (l *livestreamDrawLogs) GetFieldByName(fieldName string) (field.OrderExpr,
|
||||
}
|
||||
|
||||
func (l *livestreamDrawLogs) fillFieldMap() {
|
||||
l.fieldMap = make(map[string]field.Expr, 12)
|
||||
l.fieldMap = make(map[string]field.Expr, 16)
|
||||
l.fieldMap["id"] = l.ID
|
||||
l.fieldMap["activity_id"] = l.ActivityID
|
||||
l.fieldMap["prize_id"] = l.PrizeID
|
||||
l.fieldMap["douyin_order_id"] = l.DouyinOrderID
|
||||
l.fieldMap["shop_order_id"] = l.ShopOrderID
|
||||
l.fieldMap["local_user_id"] = l.LocalUserID
|
||||
l.fieldMap["douyin_user_id"] = l.DouyinUserID
|
||||
l.fieldMap["user_nickname"] = l.UserNickname
|
||||
l.fieldMap["prize_name"] = l.PrizeName
|
||||
l.fieldMap["level"] = l.Level
|
||||
l.fieldMap["seed_hash"] = l.SeedHash
|
||||
l.fieldMap["rand_value"] = l.RandValue
|
||||
l.fieldMap["weights_total"] = l.WeightsTotal
|
||||
l.fieldMap["created_at"] = l.CreatedAt
|
||||
l.fieldMap["is_granted"] = l.IsGranted
|
||||
l.fieldMap["is_refunded"] = l.IsRefunded
|
||||
}
|
||||
|
||||
func (l livestreamDrawLogs) clone(db *gorm.DB) livestreamDrawLogs {
|
||||
|
||||
@ -39,6 +39,7 @@ func newLivestreamPrizes(db *gorm.DB, opts ...gen.DOOption) livestreamPrizes {
|
||||
_livestreamPrizes.Sort = field.NewInt32(tableName, "sort")
|
||||
_livestreamPrizes.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_livestreamPrizes.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_livestreamPrizes.CostPrice = field.NewInt64(tableName, "cost_price")
|
||||
|
||||
_livestreamPrizes.fillFieldMap()
|
||||
|
||||
@ -62,6 +63,7 @@ type livestreamPrizes struct {
|
||||
Sort field.Int32 // 排序
|
||||
CreatedAt field.Time // 创建时间
|
||||
UpdatedAt field.Time // 更新时间
|
||||
CostPrice field.Int64 // 成本价(分)
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@ -90,6 +92,7 @@ func (l *livestreamPrizes) updateTableName(table string) *livestreamPrizes {
|
||||
l.Sort = field.NewInt32(table, "sort")
|
||||
l.CreatedAt = field.NewTime(table, "created_at")
|
||||
l.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
l.CostPrice = field.NewInt64(table, "cost_price")
|
||||
|
||||
l.fillFieldMap()
|
||||
|
||||
@ -106,7 +109,7 @@ func (l *livestreamPrizes) GetFieldByName(fieldName string) (field.OrderExpr, bo
|
||||
}
|
||||
|
||||
func (l *livestreamPrizes) fillFieldMap() {
|
||||
l.fieldMap = make(map[string]field.Expr, 12)
|
||||
l.fieldMap = make(map[string]field.Expr, 13)
|
||||
l.fieldMap["id"] = l.ID
|
||||
l.fieldMap["activity_id"] = l.ActivityID
|
||||
l.fieldMap["name"] = l.Name
|
||||
@ -119,6 +122,7 @@ func (l *livestreamPrizes) fillFieldMap() {
|
||||
l.fieldMap["sort"] = l.Sort
|
||||
l.fieldMap["created_at"] = l.CreatedAt
|
||||
l.fieldMap["updated_at"] = l.UpdatedAt
|
||||
l.fieldMap["cost_price"] = l.CostPrice
|
||||
}
|
||||
|
||||
func (l livestreamPrizes) clone(db *gorm.DB) livestreamPrizes {
|
||||
|
||||
@ -42,6 +42,7 @@ func newUsers(db *gorm.DB, opts ...gen.DOOption) users {
|
||||
_users.DouyinID = field.NewString(tableName, "douyin_id")
|
||||
_users.ChannelID = field.NewInt64(tableName, "channel_id")
|
||||
_users.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
||||
_users.Remark = field.NewString(tableName, "remark")
|
||||
|
||||
_users.fillFieldMap()
|
||||
|
||||
@ -68,6 +69,7 @@ type users struct {
|
||||
DouyinID field.String
|
||||
ChannelID field.Int64 // 渠道ID
|
||||
DouyinUserID field.String
|
||||
Remark field.String // 管ç†å‘˜å¤‡æ³¨
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@ -99,6 +101,7 @@ func (u *users) updateTableName(table string) *users {
|
||||
u.DouyinID = field.NewString(table, "douyin_id")
|
||||
u.ChannelID = field.NewInt64(table, "channel_id")
|
||||
u.DouyinUserID = field.NewString(table, "douyin_user_id")
|
||||
u.Remark = field.NewString(table, "remark")
|
||||
|
||||
u.fillFieldMap()
|
||||
|
||||
@ -115,7 +118,7 @@ func (u *users) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
func (u *users) fillFieldMap() {
|
||||
u.fieldMap = make(map[string]field.Expr, 15)
|
||||
u.fieldMap = make(map[string]field.Expr, 16)
|
||||
u.fieldMap["id"] = u.ID
|
||||
u.fieldMap["created_at"] = u.CreatedAt
|
||||
u.fieldMap["updated_at"] = u.UpdatedAt
|
||||
@ -131,6 +134,7 @@ func (u *users) fillFieldMap() {
|
||||
u.fieldMap["douyin_id"] = u.DouyinID
|
||||
u.fieldMap["channel_id"] = u.ChannelID
|
||||
u.fieldMap["douyin_user_id"] = u.DouyinUserID
|
||||
u.fieldMap["remark"] = u.Remark
|
||||
}
|
||||
|
||||
func (u users) clone(db *gorm.DB) users {
|
||||
|
||||
27
internal/repository/mysql/model/douyin_blacklist.gen.go
Normal file
27
internal/repository/mysql/model/douyin_blacklist.gen.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameDouyinBlacklist = "douyin_blacklist"
|
||||
|
||||
// DouyinBlacklist 抖音用户黑名单表
|
||||
type DouyinBlacklist struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
DouyinUserID string `gorm:"column:douyin_user_id;not null;comment:抖音用户ID" json:"douyin_user_id"` // 抖音用户ID
|
||||
Reason string `gorm:"column:reason;comment:拉黑原因" json:"reason"` // 拉黑原因
|
||||
OperatorID int64 `gorm:"column:operator_id;comment:操作人ID" json:"operator_id"` // 操作人ID
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态: 1=生效, 0=已解除" json:"status"` // 状态: 1=生效, 0=已解除
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
}
|
||||
|
||||
// TableName DouyinBlacklist's table name
|
||||
func (*DouyinBlacklist) TableName() string {
|
||||
return TableNameDouyinBlacklist
|
||||
}
|
||||
@ -13,20 +13,21 @@ const TableNameDouyinOrders = "douyin_orders"
|
||||
// DouyinOrders 抖店订单表
|
||||
type DouyinOrders struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||
ShopOrderID string `gorm:"column:shop_order_id;not null;comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
||||
DouyinProductID string `gorm:"column:douyin_product_id;comment:关联商品ID" json:"douyin_product_id"` // 关联商品ID
|
||||
OrderStatus int32 `gorm:"column:order_status;not null;comment:订单状态: 5=已完成" json:"order_status"` // 订单状态: 5=已完成
|
||||
DouyinUserID string `gorm:"column:douyin_user_id;not null;comment:抖店用户ID" json:"douyin_user_id"` // 抖店用户ID
|
||||
LocalUserID string `gorm:"column:local_user_id;default:0;comment:匹配到的本地用户ID" json:"local_user_id"` // 匹配到的本地用户ID
|
||||
ActualReceiveAmount int64 `gorm:"column:actual_receive_amount;comment:实收金额(分)" json:"actual_receive_amount"` // 实收金额(分)
|
||||
PayTypeDesc string `gorm:"column:pay_type_desc;comment:支付方式描述" json:"pay_type_desc"` // 支付方式描述
|
||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
|
||||
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
|
||||
RewardGranted int32 `gorm:"column:reward_granted;not null;default:0;comment:奖励已发放: 0=否, 1=是" json:"reward_granted"` // 奖励已发放
|
||||
ProductCount int64 `gorm:"column:product_count;not null;default:1;comment:商品数量" json:"product_count"` // 商品数量
|
||||
ShopOrderID string `gorm:"column:shop_order_id;not null;comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
||||
DouyinProductID string `gorm:"column:douyin_product_id;comment:关联商品ID" json:"douyin_product_id"` // 关联商品ID
|
||||
OrderStatus int32 `gorm:"column:order_status;not null;comment:订单状态: 5=已完成" json:"order_status"` // 订单状态: 5=已完成
|
||||
DouyinUserID string `gorm:"column:douyin_user_id;not null;comment:抖店用户ID" json:"douyin_user_id"` // 抖店用户ID
|
||||
LocalUserID string `gorm:"column:local_user_id;default:0;comment:匹配到的本地用户ID" json:"local_user_id"` // 匹配到的本地用户ID
|
||||
ActualReceiveAmount int64 `gorm:"column:actual_receive_amount;comment:实收金额(分)" json:"actual_receive_amount"` // 实收金额(分)
|
||||
ActualPayAmount int64 `gorm:"column:actual_pay_amount;comment:实付金额(分)" json:"actual_pay_amount"` // 实付金额(分)
|
||||
PayTypeDesc string `gorm:"column:pay_type_desc;comment:支付方式描述" json:"pay_type_desc"` // 支付方式描述
|
||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
|
||||
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
|
||||
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
||||
RewardGranted int32 `gorm:"column:reward_granted;not null;comment:奖励已发放: 0=否, 1=是" json:"reward_granted"` // 奖励已发放: 0=否, 1=是
|
||||
ProductCount int32 `gorm:"column:product_count;not null;default:1;comment:商品数量" json:"product_count"` // 商品数量
|
||||
}
|
||||
|
||||
// TableName DouyinOrders's table name
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameDouyinProductRewards = "douyin_product_rewards"
|
||||
|
||||
// DouyinProductRewards 抖店商品奖励规则
|
||||
type DouyinProductRewards struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||
ProductID string `gorm:"column:product_id;not null;comment:抖店商品ID" json:"product_id"` // 抖店商品ID
|
||||
ProductName string `gorm:"column:product_name;not null;comment:商品名称" json:"product_name"` // 商品名称
|
||||
RewardType string `gorm:"column:reward_type;not null;comment:奖励类型" json:"reward_type"` // 奖励类型
|
||||
RewardPayload string `gorm:"column:reward_payload;comment:奖励参数JSON" json:"reward_payload"` // 奖励参数JSON
|
||||
Quantity int32 `gorm:"column:quantity;not null;default:1;comment:发放数量" json:"quantity"` // 发放数量
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态: 1=启用 0=禁用" json:"status"` // 状态: 1=启用 0=禁用
|
||||
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName DouyinProductRewards's table name
|
||||
func (*DouyinProductRewards) TableName() string {
|
||||
return TableNameDouyinProductRewards
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
const TableNameDouyinProductRewards = "douyin_product_rewards"
|
||||
|
||||
// DouyinProductRewards 抖店商品奖励规则表
|
||||
type DouyinProductRewards struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||
ProductID string `gorm:"column:product_id;not null;uniqueIndex:uk_product_id;comment:抖店商品ID" json:"product_id"`
|
||||
ProductName string `gorm:"column:product_name;not null;default:'';comment:商品名称" json:"product_name"`
|
||||
RewardType string `gorm:"column:reward_type;not null;comment:奖励类型" json:"reward_type"`
|
||||
RewardPayload datatypes.JSON `gorm:"column:reward_payload;comment:奖励参数JSON" json:"reward_payload"`
|
||||
Quantity int32 `gorm:"column:quantity;not null;default:1;comment:发放数量" json:"quantity"`
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态: 1=启用 0=禁用" json:"status"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (*DouyinProductRewards) TableName() string {
|
||||
return TableNameDouyinProductRewards
|
||||
}
|
||||
@ -21,16 +21,16 @@ type LivestreamActivities struct {
|
||||
AccessCode string `gorm:"column:access_code;not null;comment:唯一访问码" json:"access_code"` // 唯一访问码
|
||||
DouyinProductID string `gorm:"column:douyin_product_id;comment:关联抖店商品ID" json:"douyin_product_id"` // 关联抖店商品ID
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态:1进行中 2已结束" json:"status"` // 状态:1进行中 2已结束
|
||||
TicketPrice int64 `gorm:"column:ticket_price;comment:门票价格(分)" json:"ticket_price"` // 门票价格(分)
|
||||
CommitmentAlgo string `gorm:"column:commitment_algo;default:commit-v1;comment:承诺算法版本" json:"commitment_algo"` // 承诺算法版本
|
||||
CommitmentSeedMaster []byte `gorm:"column:commitment_seed_master;comment:主种子(32字节)" json:"commitment_seed_master"` // 主种子(32字节)
|
||||
CommitmentSeedHash []byte `gorm:"column:commitment_seed_hash;comment:种子SHA256哈希" json:"commitment_seed_hash"` // 种子SHA256哈希
|
||||
CommitmentStateVersion int32 `gorm:"column:commitment_state_version;default:0;comment:状态版本" json:"commitment_state_version"` // 状态版本
|
||||
CommitmentStateVersion int32 `gorm:"column:commitment_state_version;comment:状态版本" json:"commitment_state_version"` // 状态版本
|
||||
StartTime time.Time `gorm:"column:start_time;comment:开始时间" json:"start_time"` // 开始时间
|
||||
EndTime time.Time `gorm:"column:end_time;comment:结束时间" json:"end_time"` // 结束时间
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
|
||||
TicketPrice int32 `gorm:"column:ticket_price" json:"ticket_price"`
|
||||
}
|
||||
|
||||
// TableName LivestreamActivities's table name
|
||||
|
||||
@ -16,18 +16,18 @@ type LivestreamDrawLogs struct {
|
||||
ActivityID int64 `gorm:"column:activity_id;not null;comment:关联livestream_activities.id" json:"activity_id"` // 关联livestream_activities.id
|
||||
PrizeID int64 `gorm:"column:prize_id;not null;comment:关联livestream_prizes.id" json:"prize_id"` // 关联livestream_prizes.id
|
||||
DouyinOrderID int64 `gorm:"column:douyin_order_id;comment:关联douyin_orders.id" json:"douyin_order_id"` // 关联douyin_orders.id
|
||||
ShopOrderID string `gorm:"column:shop_order_id;default:'';comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
||||
ShopOrderID string `gorm:"column:shop_order_id;comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
||||
LocalUserID int64 `gorm:"column:local_user_id;comment:本地用户ID" json:"local_user_id"` // 本地用户ID
|
||||
DouyinUserID string `gorm:"column:douyin_user_id;comment:抖音用户ID" json:"douyin_user_id"` // 抖音用户ID
|
||||
UserNickname string `gorm:"column:user_nickname;default:'';comment:用户昵称" json:"user_nickname"` // 用户昵称
|
||||
UserNickname string `gorm:"column:user_nickname;comment:用户昵称" json:"user_nickname"` // 用户昵称
|
||||
PrizeName string `gorm:"column:prize_name;comment:中奖奖品名称快照" json:"prize_name"` // 中奖奖品名称快照
|
||||
Level int32 `gorm:"column:level;default:1;comment:奖品等级" json:"level"` // 奖品等级
|
||||
SeedHash string `gorm:"column:seed_hash;comment:哈希种子" json:"seed_hash"` // 哈希种子
|
||||
RandValue int64 `gorm:"column:rand_value;comment:随机值" json:"rand_value"` // 随机值
|
||||
WeightsTotal int64 `gorm:"column:weights_total;comment:权重总和" json:"weights_total"` // 权重总和
|
||||
IsGranted int32 `gorm:"column:is_granted;default:0;comment:是否已发放奖品" json:"is_granted"` // 是否已发放奖品
|
||||
IsRefunded int32 `gorm:"column:is_refunded;default:0;comment:订单是否已退款" json:"is_refunded"` // 订单是否已退款
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:中奖时间" json:"created_at"` // 中奖时间
|
||||
IsGranted bool `gorm:"column:is_granted;comment:是否已发放奖品" json:"is_granted"` // 是否已发放奖品
|
||||
IsRefunded int32 `gorm:"column:is_refunded;comment:订单是否已退款" json:"is_refunded"` // 订单是否已退款
|
||||
}
|
||||
|
||||
// TableName LivestreamDrawLogs's table name
|
||||
|
||||
@ -21,10 +21,10 @@ type LivestreamPrizes struct {
|
||||
Remaining int32 `gorm:"column:remaining;not null;default:-1;comment:剩余数量" json:"remaining"` // 剩余数量
|
||||
Level int32 `gorm:"column:level;not null;default:1;comment:奖品等级" json:"level"` // 奖品等级
|
||||
ProductID int64 `gorm:"column:product_id;comment:关联系统商品ID" json:"product_id"` // 关联系统商品ID
|
||||
CostPrice int64 `gorm:"column:cost_price;comment:成本价(分)" json:"cost_price"` // 成本价(分)
|
||||
Sort int32 `gorm:"column:sort;not null;comment:排序" json:"sort"` // 排序
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
CostPrice int64 `gorm:"column:cost_price;comment:成本价(分)" json:"cost_price"` // 成本价(分)
|
||||
}
|
||||
|
||||
// TableName LivestreamPrizes's table name
|
||||
|
||||
@ -12,26 +12,26 @@ const TableNameOrders = "orders"
|
||||
|
||||
// Orders 订单
|
||||
type Orders struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
UserID int64 `gorm:"column:user_id;not null;comment:下单用户ID(user_members.id)" json:"user_id"` // 下单用户ID(user_members.id)
|
||||
OrderNo string `gorm:"column:order_no;not null;comment:业务订单号(唯一)" json:"order_no"` // 业务订单号(唯一)
|
||||
SourceType int32 `gorm:"column:source_type;not null;default:1;comment:来源:1商城/积分 2抽奖 3对对碰 4次数卡 5直播间 6系统发放" json:"source_type"` // 来源:1商城/积分 2抽奖 3对对碰 4次数卡 5直播间 6系统发放
|
||||
TotalAmount int64 `gorm:"column:total_amount;not null;comment:订单总金额(分)" json:"total_amount"` // 订单总金额(分)
|
||||
DiscountAmount int64 `gorm:"column:discount_amount;not null;comment:优惠券抵扣金额(分)" json:"discount_amount"` // 优惠券抵扣金额(分)
|
||||
PointsAmount int64 `gorm:"column:points_amount;not null;comment:积分抵扣金额(分)" json:"points_amount"` // 积分抵扣金额(分)
|
||||
ActualAmount int64 `gorm:"column:actual_amount;not null;comment:实际支付金额(分)" json:"actual_amount"` // 实际支付金额(分)
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:订单状态:1待支付 2已支付 3已取消 4已退款" json:"status"` // 订单状态:1待支付 2已支付 3已取消 4已退款
|
||||
PayPreorderID int64 `gorm:"column:pay_preorder_id;comment:关联预支付单ID(payment_preorder.id)" json:"pay_preorder_id"` // 关联预支付单ID(payment_preorder.id)
|
||||
PaidAt time.Time `gorm:"column:paid_at;comment:支付完成时间" json:"paid_at"` // 支付完成时间
|
||||
CancelledAt time.Time `gorm:"column:cancelled_at;comment:取消时间" json:"cancelled_at"` // 取消时间
|
||||
UserAddressID int64 `gorm:"column:user_address_id;comment:收货地址ID(user_addresses.id)" json:"user_address_id"` // 收货地址ID(user_addresses.id)
|
||||
IsConsumed int32 `gorm:"column:is_consumed;not null;comment:是否已履约/消耗(对虚拟资产)" json:"is_consumed"` // 是否已履约/消耗(对虚拟资产)
|
||||
PointsLedgerID int64 `gorm:"column:points_ledger_id;comment:积分扣减流水ID(user_points_ledger.id)" json:"points_ledger_id"` // 积分扣减流水ID(user_points_ledger.id)
|
||||
CouponID int64 `gorm:"column:coupon_id;comment:使用的优惠券ID" json:"coupon_id"` // 使用的优惠券ID
|
||||
ItemCardID int64 `gorm:"column:item_card_id;comment:使用的道具卡ID" json:"item_card_id"` // 使用的道具卡ID
|
||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
UserID int64 `gorm:"column:user_id;not null;comment:下单用户ID(user_members.id)" json:"user_id"` // 下单用户ID(user_members.id)
|
||||
OrderNo string `gorm:"column:order_no;not null;comment:业务订单号(唯一)" json:"order_no"` // 业务订单号(唯一)
|
||||
SourceType int32 `gorm:"column:source_type;not null;default:1;comment:来源:1商城直购 2抽奖票据 3其他" json:"source_type"` // 来源:1商城直购 2抽奖票据 3其他
|
||||
TotalAmount int64 `gorm:"column:total_amount;not null;comment:订单总金额(分)" json:"total_amount"` // 订单总金额(分)
|
||||
DiscountAmount int64 `gorm:"column:discount_amount;not null;comment:优惠券抵扣金额(分)" json:"discount_amount"` // 优惠券抵扣金额(分)
|
||||
PointsAmount int64 `gorm:"column:points_amount;not null;comment:积分抵扣金额(分)" json:"points_amount"` // 积分抵扣金额(分)
|
||||
ActualAmount int64 `gorm:"column:actual_amount;not null;comment:实际支付金额(分)" json:"actual_amount"` // 实际支付金额(分)
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:订单状态:1待支付 2已支付 3已取消 4已退款" json:"status"` // 订单状态:1待支付 2已支付 3已取消 4已退款
|
||||
PayPreorderID int64 `gorm:"column:pay_preorder_id;comment:关联预支付单ID(payment_preorder.id)" json:"pay_preorder_id"` // 关联预支付单ID(payment_preorder.id)
|
||||
PaidAt time.Time `gorm:"column:paid_at;comment:支付完成时间" json:"paid_at"` // 支付完成时间
|
||||
CancelledAt time.Time `gorm:"column:cancelled_at;comment:取消时间" json:"cancelled_at"` // 取消时间
|
||||
UserAddressID int64 `gorm:"column:user_address_id;comment:收货地址ID(user_addresses.id)" json:"user_address_id"` // 收货地址ID(user_addresses.id)
|
||||
IsConsumed int32 `gorm:"column:is_consumed;not null;comment:是否已履约/消耗(对虚拟资产)" json:"is_consumed"` // 是否已履约/消耗(对虚拟资产)
|
||||
PointsLedgerID int64 `gorm:"column:points_ledger_id;comment:积分扣减流水ID(user_points_ledger.id)" json:"points_ledger_id"` // 积分扣减流水ID(user_points_ledger.id)
|
||||
CouponID int64 `gorm:"column:coupon_id;comment:使用的优惠券ID" json:"coupon_id"` // 使用的优惠券ID
|
||||
ItemCardID int64 `gorm:"column:item_card_id;comment:使用的道具卡ID" json:"item_card_id"` // 使用的道具卡ID
|
||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||
}
|
||||
|
||||
// TableName Orders's table name
|
||||
|
||||
@ -29,6 +29,7 @@ type Users struct {
|
||||
DouyinID string `gorm:"column:douyin_id" json:"douyin_id"`
|
||||
ChannelID int64 `gorm:"column:channel_id;comment:渠道ID" json:"channel_id"` // 渠道ID
|
||||
DouyinUserID string `gorm:"column:douyin_user_id" json:"douyin_user_id"`
|
||||
Remark string `gorm:"column:remark;not null;comment:管ç†å‘˜å¤‡æ³¨" json:"remark"` // 管ç†å‘˜å¤‡æ³¨
|
||||
}
|
||||
|
||||
// TableName Users's table name
|
||||
|
||||
19
internal/repository/mysql/test_helper.go
Normal file
19
internal/repository/mysql/test_helper.go
Normal file
@ -0,0 +1,19 @@
|
||||
package mysql
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
// TestRepo exposes a repo implementation for testing purposes
|
||||
type TestRepo struct {
|
||||
Db *gorm.DB
|
||||
}
|
||||
|
||||
func (t *TestRepo) i() {}
|
||||
func (t *TestRepo) GetDbR() *gorm.DB { return t.Db }
|
||||
func (t *TestRepo) GetDbW() *gorm.DB { return t.Db }
|
||||
func (t *TestRepo) DbRClose() error { return nil }
|
||||
func (t *TestRepo) DbWClose() error { return nil }
|
||||
|
||||
// NewTestRepo creates a new Repo implementation wrapping the given gorm.DB
|
||||
func NewTestRepo(db *gorm.DB) Repo {
|
||||
return &TestRepo{Db: db}
|
||||
}
|
||||
49
internal/router/interceptor/blacklist.go
Normal file
49
internal/router/interceptor/blacklist.go
Normal file
@ -0,0 +1,49 @@
|
||||
package interceptor
|
||||
|
||||
import (
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CheckBlacklist 检查用户是否在黑名单中
|
||||
func (i *interceptor) CheckBlacklist() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
// 1. 获取当前用户 ID (需在 Token 认证后使用)
|
||||
userID := ctx.SessionUserInfo().Id
|
||||
if userID <= 0 {
|
||||
// 如果没有用户信息,可能是不需要认证的接口误用了此中间件,或者 Token 解析失败
|
||||
// 这里偏向于放行还是拦截需根据业务决定,一般拦截
|
||||
ctx.AbortWithError(core.Error(http.StatusUnauthorized, code.AuthorizationError, "未授权访问"))
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 查询用户状态
|
||||
// 这里每次请求都查库,如果有性能问题后续可加 Redis 缓存
|
||||
var status int32
|
||||
err := i.db.GetDbR().Model(&model.Users{}).
|
||||
Select("status").
|
||||
Where("id = ?", userID).
|
||||
Scan(&status).Error
|
||||
|
||||
if err != nil {
|
||||
// 数据库错误,安全起见放行还是报错?建议报错
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "系统繁忙"))
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 检查黑名单状态
|
||||
if status == model.UserStatusBlacklist {
|
||||
ctx.AbortWithError(core.Error(http.StatusForbidden, code.ForbiddenError, "账号异常,禁止操作"))
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 用户被禁用
|
||||
if status == model.UserStatusDisabled {
|
||||
ctx.AbortWithError(core.Error(http.StatusForbidden, code.ForbiddenError, "账号已被禁用"))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -10,17 +10,18 @@ import (
|
||||
var _ Interceptor = (*interceptor)(nil)
|
||||
|
||||
type Interceptor interface {
|
||||
// AdminTokenAuthVerify 管理端授权验证
|
||||
AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
|
||||
// AdminTokenAuthVerify 管理端授权验证
|
||||
AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
|
||||
|
||||
// AppTokenAuthVerify APP端授权验证
|
||||
AppTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
|
||||
// AppTokenAuthVerify APP端授权验证
|
||||
AppTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
|
||||
|
||||
RequireAdminRole() core.HandlerFunc
|
||||
RequireAdminAction(mark string) core.HandlerFunc
|
||||
RequireAdminRole() core.HandlerFunc
|
||||
RequireAdminAction(mark string) core.HandlerFunc
|
||||
CheckBlacklist() core.HandlerFunc
|
||||
|
||||
// i 为了避免被其他包实现
|
||||
i()
|
||||
// i 为了避免被其他包实现
|
||||
i()
|
||||
}
|
||||
|
||||
type interceptor struct {
|
||||
|
||||
@ -242,6 +242,13 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
adminAuthApiRouter.POST("/livestream/activities/:id/commitment/generate", adminHandler.GenerateLivestreamCommitment())
|
||||
adminAuthApiRouter.GET("/livestream/activities/:id/commitment/summary", adminHandler.GetLivestreamCommitmentSummary())
|
||||
|
||||
// 抖音用户黑名单管理
|
||||
adminAuthApiRouter.GET("/blacklist", adminHandler.ListBlacklist())
|
||||
adminAuthApiRouter.POST("/blacklist", adminHandler.AddBlacklist())
|
||||
adminAuthApiRouter.DELETE("/blacklist/:id", adminHandler.RemoveBlacklist())
|
||||
adminAuthApiRouter.GET("/blacklist/check", adminHandler.CheckBlacklist())
|
||||
adminAuthApiRouter.POST("/blacklist/batch", adminHandler.BatchAddBlacklist())
|
||||
|
||||
// 系统配置KV
|
||||
adminAuthApiRouter.GET("/system/configs", adminHandler.ListSystemConfigs())
|
||||
adminAuthApiRouter.POST("/system/configs", adminHandler.UpsertSystemConfig())
|
||||
@ -264,6 +271,9 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
adminAuthApiRouter.GET("/users/:user_id/stats/profit_loss", intc.RequireAdminAction("user:view"), adminHandler.GetUserProfitLossTrend())
|
||||
adminAuthApiRouter.GET("/users/:user_id/stats/profit_loss_details", intc.RequireAdminAction("user:view"), adminHandler.GetUserProfitLossDetails())
|
||||
adminAuthApiRouter.GET("/users/:user_id/profile", intc.RequireAdminAction("user:view"), adminHandler.GetUserProfile())
|
||||
adminAuthApiRouter.PUT("/users/:user_id/douyin_user_id", intc.RequireAdminAction("user:modify"), adminHandler.UpdateUserDouyinID())
|
||||
adminAuthApiRouter.PUT("/users/:user_id/remark", intc.RequireAdminAction("user:modify"), adminHandler.UpdateUserRemark())
|
||||
adminAuthApiRouter.PUT("/users/:user_id/status", intc.RequireAdminAction("user:modify"), adminHandler.UpdateUserStatus())
|
||||
adminAuthApiRouter.GET("/users/:user_id/audit", intc.RequireAdminAction("user:view"), adminHandler.ListUserAuditLogs())
|
||||
|
||||
adminAuthApiRouter.POST("/users/:user_id/token", intc.RequireAdminAction("user:token:issue"), adminHandler.IssueUserToken())
|
||||
@ -464,9 +474,6 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
appAuthApiRouter.PUT("/users/:user_id/addresses/:address_id", userHandler.UpdateUserAddress())
|
||||
appAuthApiRouter.DELETE("/users/:user_id/addresses/:address_id", userHandler.DeleteUserAddress())
|
||||
|
||||
appAuthApiRouter.POST("/pay/wechat/jsapi/preorder", userHandler.WechatJSAPIPreorder())
|
||||
appAuthApiRouter.POST("/lottery/join", activityHandler.JoinLottery())
|
||||
|
||||
// 任务中心 APP 端
|
||||
appAuthApiRouter.GET("/task-center/tasks", taskCenterHandler.ListTasksForApp())
|
||||
appAuthApiRouter.GET("/task-center/tasks/:id/progress/:user_id", taskCenterHandler.GetTaskProgressForApp())
|
||||
@ -481,29 +488,45 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
|
||||
appAuthApiRouter.GET("/store/items", appapi.NewStore(logger, db, userSvc).ListStoreItemsForApp())
|
||||
appAuthApiRouter.GET("/lottery/result", activityHandler.LotteryResultByOrder())
|
||||
|
||||
// 对对碰游戏
|
||||
appAuthApiRouter.POST("/matching/preorder", activityHandler.PreOrderMatchingGame())
|
||||
// 需要黑名单检查的抽奖接口组
|
||||
lotteryGroup := appAuthApiRouter.Group("", intc.CheckBlacklist())
|
||||
{
|
||||
lotteryGroup.POST("/pay/wechat/jsapi/preorder", userHandler.WechatJSAPIPreorder()) // 支付前也检查
|
||||
lotteryGroup.POST("/lottery/join", activityHandler.JoinLottery())
|
||||
|
||||
// 对对碰游戏
|
||||
lotteryGroup.POST("/matching/preorder", activityHandler.PreOrderMatchingGame())
|
||||
|
||||
// 扫雷游戏
|
||||
lotteryGroup.POST("/games/enter", gameHandler.EnterGame())
|
||||
|
||||
// 积分兑换操作也应该检查黑名单
|
||||
lotteryGroup.POST("/users/:user_id/points/redeem-coupon", userHandler.RedeemPointsToCoupon())
|
||||
lotteryGroup.POST("/users/:user_id/points/redeem-product", userHandler.RedeemPointsToProduct())
|
||||
lotteryGroup.POST("/users/:user_id/points/redeem-item-card", userHandler.RedeemPointsToItemCard())
|
||||
|
||||
// 资产操作(发货/回收)
|
||||
lotteryGroup.POST("/users/:user_id/inventory/request-shipping", userHandler.RequestShippingBatch())
|
||||
lotteryGroup.POST("/users/:user_id/inventory/cancel-shipping", userHandler.CancelShipping())
|
||||
lotteryGroup.POST("/users/:user_id/inventory/redeem", userHandler.RedeemInventoryToPoints())
|
||||
}
|
||||
|
||||
// 对对碰其他接口(不需要严查黑名单,或者已在preorder查过)
|
||||
appAuthApiRouter.POST("/matching/check", activityHandler.CheckMatchingGame())
|
||||
appAuthApiRouter.GET("/matching/state", activityHandler.GetMatchingGameState())
|
||||
appAuthApiRouter.GET("/matching/cards", activityHandler.GetMatchingGameCards()) // 支付成功后获取游戏数据
|
||||
appAuthApiRouter.GET("/matching/cards", activityHandler.GetMatchingGameCards())
|
||||
|
||||
// 次数卡
|
||||
appAuthApiRouter.GET("/game-passes/available", userHandler.GetGamePasses())
|
||||
appAuthApiRouter.GET("/game-passes/packages", userHandler.GetGamePassPackages())
|
||||
appAuthApiRouter.POST("/game-passes/purchase", userHandler.PurchaseGamePassPackage())
|
||||
appAuthApiRouter.POST("/game-passes/purchase", userHandler.PurchaseGamePassPackage()) // 购买次数卡是否要查?
|
||||
|
||||
// 扫雷游戏
|
||||
// 扫雷游戏其他接口
|
||||
appAuthApiRouter.GET("/users/:user_id/game_tickets", gameHandler.GetMyTickets())
|
||||
appAuthApiRouter.POST("/games/enter", gameHandler.EnterGame())
|
||||
|
||||
appAuthApiRouter.POST("/users/:user_id/inventory/address-share/create", userHandler.CreateAddressShare())
|
||||
appAuthApiRouter.POST("/users/:user_id/inventory/address-share/revoke", userHandler.RevokeAddressShare())
|
||||
appAuthApiRouter.POST("/users/:user_id/inventory/request-shipping", userHandler.RequestShippingBatch())
|
||||
appAuthApiRouter.POST("/users/:user_id/inventory/cancel-shipping", userHandler.CancelShipping())
|
||||
appAuthApiRouter.POST("/users/:user_id/inventory/redeem", userHandler.RedeemInventoryToPoints())
|
||||
appAuthApiRouter.POST("/users/:user_id/points/redeem-coupon", userHandler.RedeemPointsToCoupon())
|
||||
appAuthApiRouter.POST("/users/:user_id/points/redeem-product", userHandler.RedeemPointsToProduct())
|
||||
appAuthApiRouter.POST("/users/:user_id/points/redeem-item-card", userHandler.RedeemPointsToItemCard())
|
||||
|
||||
}
|
||||
// 微信支付平台回调(无需鉴权)
|
||||
mux.Group("/api/pay").POST("/wechat/notify", payHandler.WechatNotify())
|
||||
|
||||
@ -11,18 +11,18 @@ import (
|
||||
// 参数: in 活动创建输入
|
||||
// 返回: 新建的活动记录与错误
|
||||
func (s *service) CreateActivity(ctx context.Context, in CreateActivityInput) (*model.Activities, error) {
|
||||
item := &model.Activities{
|
||||
Name: in.Name,
|
||||
Banner: in.Banner,
|
||||
Image: in.Image,
|
||||
GameplayIntro: sanitizeHTML(in.GameplayIntro),
|
||||
ActivityCategoryID: in.ActivityCategoryID,
|
||||
Status: in.Status,
|
||||
PriceDraw: in.PriceDraw,
|
||||
IsBoss: in.IsBoss,
|
||||
AllowItemCards: in.AllowItemCards != 0,
|
||||
AllowCoupons: in.AllowCoupons != 0,
|
||||
}
|
||||
item := &model.Activities{
|
||||
Name: in.Name,
|
||||
Banner: in.Banner,
|
||||
Image: in.Image,
|
||||
GameplayIntro: sanitizeHTML(in.GameplayIntro),
|
||||
ActivityCategoryID: in.ActivityCategoryID,
|
||||
Status: in.Status,
|
||||
PriceDraw: in.PriceDraw,
|
||||
IsBoss: in.IsBoss,
|
||||
AllowItemCards: in.AllowItemCards != 0,
|
||||
AllowCoupons: in.AllowCoupons != 0,
|
||||
}
|
||||
|
||||
if in.StartTime != nil {
|
||||
item.StartTime = *in.StartTime
|
||||
@ -41,15 +41,15 @@ func (s *service) CreateActivity(ctx context.Context, in CreateActivityInput) (*
|
||||
if strings.TrimSpace(item.GameplayIntro) == "" {
|
||||
do = do.Omit(s.writeDB.Activities.GameplayIntro)
|
||||
}
|
||||
if in.StartTime == nil {
|
||||
if in.StartTime == nil || in.StartTime.IsZero() {
|
||||
do = do.Omit(s.writeDB.Activities.StartTime)
|
||||
}
|
||||
if in.EndTime == nil {
|
||||
do = do.Omit(s.writeDB.Activities.EndTime)
|
||||
}
|
||||
// 避免零日期写入新增的时间列
|
||||
do = do.Omit(s.writeDB.Activities.ScheduledTime, s.writeDB.Activities.LastSettledAt)
|
||||
err := do.Create(item)
|
||||
if in.EndTime == nil || in.EndTime.IsZero() {
|
||||
do = do.Omit(s.writeDB.Activities.EndTime)
|
||||
}
|
||||
// 避免零日期写入新增的时间列
|
||||
do = do.Omit(s.writeDB.Activities.ScheduledTime, s.writeDB.Activities.LastSettledAt)
|
||||
err := do.Create(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// ActivityOrderService 活动订单创建服务
|
||||
@ -102,6 +104,9 @@ func (s *activityOrderService) CreateActivityOrder(ctx core.Context, req CreateA
|
||||
}
|
||||
|
||||
// 2. 应用称号折扣 (Title Discount)
|
||||
// Title effects logic usually doesn't involve race conditions on balance, so we keep it outside/before critical section if possible,
|
||||
// or inside. Since it's read-only mostly, good to keep.
|
||||
// NOTE: If title service needs transaction, we might need to refactor it. For now assuming it's safe.
|
||||
titleEffects, _ := s.title.ResolveActiveEffects(ctx.RequestContext(), userID, titlesvc.EffectScope{
|
||||
ActivityID: &req.ActivityID,
|
||||
IssueID: &req.IssueID,
|
||||
@ -134,43 +139,69 @@ func (s *activityOrderService) CreateActivityOrder(ctx core.Context, req CreateA
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 应用优惠券 (using applyCouponWithCap logic)
|
||||
var appliedCouponVal int64
|
||||
if req.CouponID != nil && *req.CouponID > 0 {
|
||||
fmt.Printf("[订单服务] 尝试优惠券 用户券ID=%d 应用前实付(分)=%d\n", *req.CouponID, order.ActualAmount)
|
||||
appliedCouponVal = s.applyCouponWithCap(ctx.RequestContext(), userID, order, req.ActivityID, *req.CouponID)
|
||||
fmt.Printf("[订单服务] 优惠后 实付(分)=%d 累计优惠(分)=%d\n", order.ActualAmount, order.DiscountAmount)
|
||||
}
|
||||
|
||||
// 4. 记录道具卡到备注 (Removed duplicate append here as it was already done in Step 1)
|
||||
// Log for debugging
|
||||
if req.ItemCardID != nil && *req.ItemCardID > 0 {
|
||||
fmt.Printf("[订单服务] 尝试道具卡 用户卡ID=%d 订单号=%s\n", *req.ItemCardID, order.OrderNo)
|
||||
}
|
||||
// 开启事务处理订单创建与优惠券扣减
|
||||
err := s.writeDB.Transaction(func(tx *dao.Query) error {
|
||||
var deductionOp func(int64) error
|
||||
|
||||
// 5. 保存订单
|
||||
if err := s.writeDB.Orders.WithContext(ctx.RequestContext()).Omit(s.writeDB.Orders.PaidAt, s.writeDB.Orders.CancelledAt).Create(order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 6. 记录优惠券使用明细
|
||||
if appliedCouponVal > 0 && req.CouponID != nil && *req.CouponID > 0 {
|
||||
_ = s.user.RecordOrderCouponUsage(ctx.RequestContext(), order.ID, *req.CouponID, appliedCouponVal)
|
||||
}
|
||||
|
||||
// 7. 处理0元订单自动支付
|
||||
if order.ActualAmount == 0 {
|
||||
now := time.Now()
|
||||
_, _ = s.writeDB.Orders.WithContext(ctx.RequestContext()).Where(s.writeDB.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{
|
||||
s.writeDB.Orders.Status.ColumnName().String(): 2,
|
||||
s.writeDB.Orders.PaidAt.ColumnName().String(): now,
|
||||
})
|
||||
order.Status = 2
|
||||
|
||||
// 核销优惠券
|
||||
// 3. 应用优惠券 (Lock & Calculate)
|
||||
if req.CouponID != nil && *req.CouponID > 0 {
|
||||
s.consumeCouponOnZeroPay(ctx.RequestContext(), userID, order.ID, *req.CouponID, appliedCouponVal, now)
|
||||
fmt.Printf("[订单服务] 尝试优惠券 用户券ID=%d 应用前实付(分)=%d\n", *req.CouponID, order.ActualAmount)
|
||||
|
||||
val, op, err := s.applyCouponWithLock(ctx.RequestContext(), tx, userID, order, req.ActivityID, *req.CouponID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appliedCouponVal = val
|
||||
deductionOp = op
|
||||
fmt.Printf("[订单服务] 优惠后 实付(分)=%d 累计优惠(分)=%d\n", order.ActualAmount, order.DiscountAmount)
|
||||
}
|
||||
|
||||
// 4. 记录道具卡到备注
|
||||
if req.ItemCardID != nil && *req.ItemCardID > 0 {
|
||||
fmt.Printf("[订单服务] 尝试道具卡 用户卡ID=%d 订单号=%s\n", *req.ItemCardID, order.OrderNo)
|
||||
}
|
||||
|
||||
// 5. 保存订单
|
||||
if err := tx.Orders.WithContext(ctx.RequestContext()).Omit(tx.Orders.PaidAt, tx.Orders.CancelledAt).Create(order); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Execute deferred deduction now that we have Order ID
|
||||
if deductionOp != nil {
|
||||
if err := deductionOp(order.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 记录优惠券使用明细
|
||||
if appliedCouponVal > 0 && req.CouponID != nil && *req.CouponID > 0 {
|
||||
err := tx.OrderCoupons.WithContext(ctx.RequestContext()).UnderlyingDB().Exec(
|
||||
"INSERT IGNORE INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?,?,?,NOW(3))",
|
||||
order.ID, *req.CouponID, appliedCouponVal).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 处理0元订单自动支付
|
||||
if order.ActualAmount == 0 {
|
||||
now := time.Now()
|
||||
_, _ = tx.Orders.WithContext(ctx.RequestContext()).Where(tx.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{
|
||||
tx.Orders.Status.ColumnName().String(): 2,
|
||||
tx.Orders.PaidAt.ColumnName().String(): now,
|
||||
})
|
||||
order.Status = 2
|
||||
|
||||
s.consumeCouponOnZeroPayTx(ctx.RequestContext(), tx, userID, order.ID, *req.CouponID, appliedCouponVal, now)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("[订单服务] 创建订单失败: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("[订单服务] 订单创建完成 订单号=%s 总额(分)=%d 优惠(分)=%d 实付(分)=%d 状态=%d\n",
|
||||
@ -182,40 +213,55 @@ func (s *activityOrderService) CreateActivityOrder(ctx core.Context, req CreateA
|
||||
}, nil
|
||||
}
|
||||
|
||||
// applyCouponWithCap 优惠券抵扣(含50%封顶与金额券部分使用)
|
||||
func (s *activityOrderService) applyCouponWithCap(ctx context.Context, userID int64, order *model.Orders, activityID int64, userCouponID int64) int64 {
|
||||
uc, _ := s.readDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID), s.readDB.UserCoupons.UserID.Eq(userID), s.readDB.UserCoupons.Status.Eq(1)).First()
|
||||
// applyCouponWithLock 锁定计算并返回扣减操作闭包
|
||||
// 逻辑:锁定行 -> 计算优惠 -> 返回闭包(闭包内执行 UPDATE Balance + Insert Ledger)
|
||||
func (s *activityOrderService) applyCouponWithLock(ctx context.Context, tx *dao.Query, userID int64, order *model.Orders, activityID int64, userCouponID int64) (int64, func(int64) error, error) {
|
||||
// 使用 SELECT ... FOR UPDATE 锁定行
|
||||
uc, _ := tx.UserCoupons.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where(
|
||||
tx.UserCoupons.ID.Eq(userCouponID),
|
||||
tx.UserCoupons.UserID.Eq(userID),
|
||||
).First()
|
||||
|
||||
if uc == nil {
|
||||
return 0
|
||||
return 0, nil, nil
|
||||
}
|
||||
fmt.Printf("[优惠券] 用户券ID=%d 开始=%s 结束=%s\n", userCouponID, uc.ValidStart.Format(time.RFC3339), func() string {
|
||||
// 重新检查状态 (status must be 1=Active, or maybe 4 if we allow concurrent usage but that's complex. Let's strict to 1 for new orders)
|
||||
// 如果是金额券,status=1。
|
||||
// 如果是满减券,status=1。
|
||||
if uc.Status != 1 {
|
||||
fmt.Printf("[优惠券] 状态不可用 id=%d status=%d\n", userCouponID, uc.Status)
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
fmt.Printf("[优惠券] 用户券ID=%d 开始=%s 结束=%s 余额=%d\n", userCouponID, uc.ValidStart.Format(time.RFC3339), func() string {
|
||||
if uc.ValidEnd.IsZero() {
|
||||
return "无截止"
|
||||
}
|
||||
return uc.ValidEnd.Format(time.RFC3339)
|
||||
}())
|
||||
sc, _ := s.readDB.SystemCoupons.WithContext(ctx).Where(s.readDB.SystemCoupons.ID.Eq(uc.CouponID), s.readDB.SystemCoupons.Status.Eq(1)).First()
|
||||
}(), uc.BalanceAmount)
|
||||
|
||||
sc, _ := tx.SystemCoupons.WithContext(ctx).Where(tx.SystemCoupons.ID.Eq(uc.CouponID), tx.SystemCoupons.Status.Eq(1)).First()
|
||||
now := time.Now()
|
||||
if sc == nil {
|
||||
fmt.Printf("[优惠券] 模板不存在 用户券ID=%d\n", userCouponID)
|
||||
return 0
|
||||
return 0, nil, nil
|
||||
}
|
||||
if uc.ValidStart.After(now) {
|
||||
fmt.Printf("[优惠券] 未到开始时间 用户券ID=%d\n", userCouponID)
|
||||
return 0
|
||||
return 0, nil, nil
|
||||
}
|
||||
if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
|
||||
fmt.Printf("[优惠券] 已过期 用户券ID=%d\n", userCouponID)
|
||||
return 0
|
||||
return 0, nil, nil
|
||||
}
|
||||
scopeOK := (sc.ScopeType == 1) || (sc.ScopeType == 2 && sc.ActivityID == activityID)
|
||||
if !scopeOK {
|
||||
fmt.Printf("[优惠券] 范围不匹配 用户券ID=%d scope_type=%d\n", userCouponID, sc.ScopeType)
|
||||
return 0
|
||||
return 0, nil, nil
|
||||
}
|
||||
if order.TotalAmount < sc.MinSpend {
|
||||
fmt.Printf("[优惠券] 未达使用门槛 用户券ID=%d min_spend(分)=%d 订单总额(分)=%d\n", userCouponID, sc.MinSpend, order.TotalAmount)
|
||||
return 0
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// 50% 封顶
|
||||
@ -223,17 +269,13 @@ func (s *activityOrderService) applyCouponWithCap(ctx context.Context, userID in
|
||||
remainingCap := cap - order.DiscountAmount
|
||||
if remainingCap <= 0 {
|
||||
fmt.Printf("[优惠券] 已达封顶\n")
|
||||
return 0
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
applied := int64(0)
|
||||
switch sc.DiscountType {
|
||||
case 1: // 金额券
|
||||
var bal int64
|
||||
_ = s.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
|
||||
if bal <= 0 {
|
||||
bal = sc.DiscountValue
|
||||
}
|
||||
case 1: // 金额券 (Atomic Deduction)
|
||||
var bal = uc.BalanceAmount
|
||||
if bal > 0 {
|
||||
if bal > remainingCap {
|
||||
applied = remainingCap
|
||||
@ -267,54 +309,105 @@ func (s *activityOrderService) applyCouponWithCap(ctx context.Context, userID in
|
||||
applied = order.ActualAmount
|
||||
}
|
||||
if applied <= 0 {
|
||||
return 0
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Update order struct
|
||||
order.ActualAmount -= applied
|
||||
order.DiscountAmount += applied
|
||||
order.Remark += fmt.Sprintf("|c:%d:%d", userCouponID, applied)
|
||||
fmt.Printf("[优惠券] 本次抵扣(分)=%d\n", applied)
|
||||
fmt.Printf("[优惠券] 本次抵扣(分)=%d 余额更新扣减(Defer)\n", applied)
|
||||
|
||||
return applied
|
||||
// Construct deferred operation
|
||||
op := func(orderID int64) error {
|
||||
if sc.DiscountType == 1 {
|
||||
// 金额券:扣余额
|
||||
newBal := uc.BalanceAmount - applied
|
||||
newStatus := int32(1)
|
||||
if newBal <= 0 {
|
||||
newBal = 0
|
||||
newStatus = 2 // Used/Exhausted
|
||||
}
|
||||
// 使用乐观锁或直接 Update,因为我们已经加了行锁 (FOR UPDATE)
|
||||
res := tx.UserCoupons.WithContext(ctx).UnderlyingDB().Exec(
|
||||
"UPDATE user_coupons SET balance_amount = ?, status = ? WHERE id = ?",
|
||||
newBal, newStatus, userCouponID)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
// 记录扣减流水
|
||||
_ = tx.UserCouponLedger.WithContext(ctx).Create(&model.UserCouponLedger{
|
||||
UserID: userID,
|
||||
UserCouponID: userCouponID,
|
||||
ChangeAmount: -applied, // Negative for deduction
|
||||
BalanceAfter: newBal,
|
||||
OrderID: orderID,
|
||||
Action: "usage",
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
} else {
|
||||
// 满减/折扣券:标记为冻结 (4) 以防止并在使用
|
||||
// 支付成功后 -> 2
|
||||
// 超时/取消 -> 1
|
||||
res := tx.UserCoupons.WithContext(ctx).UnderlyingDB().Exec(
|
||||
"UPDATE user_coupons SET status = 4 WHERE id = ? AND status = 1", userCouponID)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return fmt.Errorf("coupon conflict for id %d", userCouponID)
|
||||
}
|
||||
|
||||
// 满减券流水
|
||||
_ = tx.UserCouponLedger.WithContext(ctx).Create(&model.UserCouponLedger{
|
||||
UserID: userID,
|
||||
UserCouponID: userCouponID,
|
||||
ChangeAmount: 0,
|
||||
BalanceAfter: 0,
|
||||
OrderID: orderID,
|
||||
Action: "usage",
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return applied, op, nil
|
||||
}
|
||||
|
||||
// consumeCouponOnZeroPay 0元支付时核销优惠券
|
||||
func (s *activityOrderService) consumeCouponOnZeroPay(ctx context.Context, userID int64, orderID int64, userCouponID int64, applied int64, now time.Time) {
|
||||
uc, _ := s.readDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID), s.readDB.UserCoupons.UserID.Eq(userID)).First()
|
||||
// consumeCouponOnZeroPayTx 0元支付时核销优惠券 (With Tx)
|
||||
func (s *activityOrderService) consumeCouponOnZeroPayTx(ctx context.Context, tx *dao.Query, userID int64, orderID int64, userCouponID int64, applied int64, now time.Time) {
|
||||
uc, _ := tx.UserCoupons.WithContext(ctx).Where(tx.UserCoupons.ID.Eq(userCouponID), tx.UserCoupons.UserID.Eq(userID)).First()
|
||||
if uc == nil {
|
||||
return
|
||||
}
|
||||
sc, _ := s.readDB.SystemCoupons.WithContext(ctx).Where(s.readDB.SystemCoupons.ID.Eq(uc.CouponID)).First()
|
||||
sc, _ := tx.SystemCoupons.WithContext(ctx).Where(tx.SystemCoupons.ID.Eq(uc.CouponID)).First()
|
||||
if sc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if sc.DiscountType == 1 { // 金额券 - 部分扣减
|
||||
var bal int64
|
||||
_ = s.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
|
||||
nb := bal - applied
|
||||
if nb < 0 {
|
||||
nb = 0
|
||||
}
|
||||
if nb == 0 {
|
||||
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
||||
"balance_amount": nb,
|
||||
s.readDB.UserCoupons.Status.ColumnName().String(): 2,
|
||||
s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||
s.readDB.UserCoupons.UsedAt.ColumnName().String(): now,
|
||||
})
|
||||
} else {
|
||||
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
||||
"balance_amount": nb,
|
||||
s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||
s.readDB.UserCoupons.UsedAt.ColumnName().String(): now,
|
||||
})
|
||||
}
|
||||
} else { // 满减/折扣券 - 直接核销
|
||||
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
||||
s.readDB.UserCoupons.Status.ColumnName().String(): 2,
|
||||
s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||
s.readDB.UserCoupons.UsedAt.ColumnName().String(): now,
|
||||
// 如果是金额券,余额已经在 applyCouponWithCap 中扣减过了。
|
||||
// 这里的逻辑主要是为了记录 used_order_id 等 meta 信息。
|
||||
if sc.DiscountType == 1 { // 金额券
|
||||
// 状态:
|
||||
// 如果余额 > 0 -> 状态 1
|
||||
// 如果余额 = 0 -> 状态 2
|
||||
// 不需要 status=4。
|
||||
|
||||
// 我们只需要记录用于统计的 used_order_id, used_at
|
||||
// 注意:amounts update has been done.
|
||||
_, _ = tx.UserCoupons.WithContext(ctx).Where(tx.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
||||
tx.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||
tx.UserCoupons.UsedAt.ColumnName().String(): now,
|
||||
})
|
||||
} else { // 满减/折扣券
|
||||
// Apply 时设置为 4 (Frozen)
|
||||
// 此时需要确认为 2 (Used)
|
||||
_, _ = tx.UserCoupons.WithContext(ctx).Where(tx.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
||||
tx.UserCoupons.Status.ColumnName().String(): 2,
|
||||
tx.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||
tx.UserCoupons.UsedAt.ColumnName().String(): now,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
176
internal/service/activity/concurrency_test.go
Normal file
176
internal/service/activity/concurrency_test.go
Normal file
@ -0,0 +1,176 @@
|
||||
package activity
|
||||
|
||||
import (
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"context"
|
||||
"mime/multipart"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
drivermysql "gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Mock Context
|
||||
type mockContext struct {
|
||||
core.Context // Embed interface to satisfy compiler
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (m *mockContext) RequestContext() core.StdContext {
|
||||
return core.StdContext{Context: m.ctx}
|
||||
}
|
||||
|
||||
// Satisfy compiler for embedded interface methods if needed by runtime checks (unlikely for this test path)
|
||||
func (m *mockContext) ShouldBindQuery(obj interface{}) error { return nil }
|
||||
func (m *mockContext) ShouldBindPostForm(obj interface{}) error { return nil }
|
||||
func (m *mockContext) ShouldBindForm(obj interface{}) error { return nil }
|
||||
func (m *mockContext) ShouldBindJSON(obj interface{}) error { return nil }
|
||||
func (m *mockContext) ShouldBindXML(obj interface{}) error { return nil }
|
||||
func (m *mockContext) ShouldBindURI(obj interface{}) error { return nil }
|
||||
func (m *mockContext) Redirect(code int, location string) {}
|
||||
func (m *mockContext) Trace() core.Trace { return nil }
|
||||
func (m *mockContext) setTrace(trace core.Trace) {}
|
||||
func (m *mockContext) disableTrace() {}
|
||||
func (m *mockContext) Logger() logger.CustomLogger { return nil }
|
||||
func (m *mockContext) setLogger(logger logger.CustomLogger) {}
|
||||
func (m *mockContext) Payload(payload interface{}) {}
|
||||
func (m *mockContext) getPayload() interface{} { return nil }
|
||||
func (m *mockContext) File(filePath string) {}
|
||||
func (m *mockContext) HTML(name string, obj interface{}) {}
|
||||
func (m *mockContext) String(str string) {}
|
||||
func (m *mockContext) XML(obj interface{}) {}
|
||||
func (m *mockContext) ExcelData(filename string, byteData []byte) {}
|
||||
func (m *mockContext) FormFile(name string) (*multipart.FileHeader, error) { return nil, nil }
|
||||
func (m *mockContext) SaveUploadedFile(file *multipart.FileHeader, dst string) error { return nil }
|
||||
func (m *mockContext) AbortWithError(err core.BusinessError) {}
|
||||
func (m *mockContext) abortError() core.BusinessError { return nil }
|
||||
|
||||
// Header signature mismatch was an issue. core.Context: Header() http.Header.
|
||||
// We need imports if we want to implement it fully or just use "core.Context" embedding trick.
|
||||
// Since we embed, we don't *need* to implement them unless called.
|
||||
// But some might be called by dependencies we don't see.
|
||||
|
||||
// TestConcurrencyCoupon verifies that concurrent concurrent orders do not over-deduct coupon balance.
|
||||
// NOTE: This test requires a real DB connection.
|
||||
func TestConcurrencyCoupon(t *testing.T) {
|
||||
// 1. Setup DB
|
||||
dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/dev_game?charset=utf8mb4&parseTime=true&loc=Local"
|
||||
db, err := gorm.Open(drivermysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Skipf("Skipping test due to DB connection failure: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize dao generic
|
||||
dao.Use(db)
|
||||
|
||||
// Use helper from mysql package to get a valid Repo implementation
|
||||
repo := mysql.NewTestRepo(db)
|
||||
log, _ := logger.NewCustomLogger(nil, logger.WithOutputInConsole())
|
||||
|
||||
// 2. Init Service
|
||||
svc := NewActivityOrderService(log, repo)
|
||||
|
||||
// 3. Prepare Data
|
||||
userID := int64(99999) // Test User
|
||||
|
||||
// Find a valid system coupon (Type 1 - Amount)
|
||||
var sysCoupon model.SystemCoupons
|
||||
if err := db.Where("discount_type = 1 AND status = 1").First(&sysCoupon).Error; err != nil {
|
||||
t.Skipf("No valid system coupon found (Type 1), skipping: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create User Coupon
|
||||
userCoupon := model.UserCoupons{
|
||||
UserID: userID,
|
||||
CouponID: sysCoupon.ID,
|
||||
Status: 1,
|
||||
BalanceAmount: 5000, // 50 yuan
|
||||
ValidStart: time.Now().Add(-1 * time.Hour),
|
||||
ValidEnd: time.Now().Add(24 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := db.Omit("UsedAt", "UsedOrderID").Create(&userCoupon).Error; err != nil {
|
||||
t.Fatalf("Failed to create user coupon: %v", err)
|
||||
}
|
||||
t.Logf("Created test coupon ID: %d with balance 5000", userCoupon.ID)
|
||||
|
||||
// 4. Concurrency Test
|
||||
var wg sync.WaitGroup
|
||||
successCount := 0
|
||||
failCount := 0
|
||||
var mu sync.Mutex
|
||||
|
||||
concurrency := 20
|
||||
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
|
||||
req := CreateActivityOrderRequest{
|
||||
UserID: userID,
|
||||
ActivityID: 1, // Dummy
|
||||
IssueID: 1, // Dummy
|
||||
Count: 1,
|
||||
UnitPrice: 1000, // 10 yuan
|
||||
SourceType: 2,
|
||||
CouponID: &userCoupon.ID,
|
||||
}
|
||||
|
||||
// Mock Context
|
||||
mockCtx := &mockContext{ctx: context.Background()}
|
||||
|
||||
res, err := svc.CreateActivityOrder(mockCtx, req)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
t.Logf("[%d] Success. Discount: %d", idx, res.AppliedCouponVal)
|
||||
if res.AppliedCouponVal > 0 {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 5. Verify Result
|
||||
var finalCoupon model.UserCoupons
|
||||
err = db.First(&finalCoupon, userCoupon.ID).Error
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to query final coupon: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Final Balance: %d. Success Orders with Discount: %d. Failures: %d", finalCoupon.BalanceAmount, successCount, failCount)
|
||||
|
||||
if finalCoupon.BalanceAmount < 0 {
|
||||
t.Errorf("Balance is negative: %d", finalCoupon.BalanceAmount)
|
||||
}
|
||||
|
||||
// Verify total deducted
|
||||
var orderCoupons []model.OrderCoupons
|
||||
db.Where("user_coupon_id = ?", userCoupon.ID).Find(&orderCoupons)
|
||||
|
||||
totalDeducted := int64(0)
|
||||
for _, oc := range orderCoupons {
|
||||
totalDeducted += oc.AppliedAmount
|
||||
}
|
||||
|
||||
t.Logf("Total Deducted from OrderCoupons table: %d", totalDeducted)
|
||||
|
||||
expectedDeduction := 5000 - finalCoupon.BalanceAmount
|
||||
if expectedDeduction != totalDeducted {
|
||||
t.Errorf("Mismatch! Initial-Final(%d) != OrderCoupons Sum(%d)", expectedDeduction, totalDeducted)
|
||||
}
|
||||
}
|
||||
@ -289,8 +289,14 @@ func (s *service) TriggerVirtualShipping(ctx context.Context, orderID int64, ord
|
||||
if u != nil {
|
||||
payerOpenid = u.Openid
|
||||
}
|
||||
c := configs.Get()
|
||||
cfg := &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}
|
||||
var cfg *wechat.WechatConfig
|
||||
if dc := sysconfig.GetDynamicConfig(); dc != nil {
|
||||
wc := dc.GetWechat(ctx)
|
||||
cfg = &wechat.WechatConfig{AppID: wc.AppID, AppSecret: wc.AppSecret}
|
||||
} else {
|
||||
c := configs.Get()
|
||||
cfg = &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}
|
||||
}
|
||||
errUpload := wechat.UploadVirtualShippingForBackground(ctx, cfg, tx.TransactionID, orderNo, payerOpenid, itemsDesc)
|
||||
|
||||
// 如果发货成功,或者微信提示已经发过货了(10060003),则标记本地订单为已履约
|
||||
@ -330,6 +336,7 @@ func (s *service) applyItemCardEffect(ctx context.Context, icID int64, aid int64
|
||||
if ic != nil && !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
|
||||
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == aid) || (ic.ScopeType == 4 && ic.IssueID == iss)
|
||||
if scopeOK {
|
||||
effectApplied := false
|
||||
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||||
rw, _ := s.readDB.ActivityRewardSettings.WithContext(ctx).Where(s.readDB.ActivityRewardSettings.ID.Eq(log.RewardID)).First()
|
||||
if rw != nil {
|
||||
@ -338,17 +345,38 @@ func (s *service) applyItemCardEffect(ctx context.Context, icID int64, aid int64
|
||||
if prod, _ := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(rw.ProductID)).First(); prod != nil {
|
||||
prodName = prod.Name
|
||||
}
|
||||
_, _ = s.user.GrantRewardToOrder(ctx, log.UserID, usersvc.GrantRewardToOrderRequest{OrderID: log.OrderID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &aid, RewardID: &log.RewardID, Remark: prodName + "(倍数)"})
|
||||
// 尝试发放额外奖励
|
||||
_, err := s.user.GrantRewardToOrder(ctx, log.UserID, usersvc.GrantRewardToOrderRequest{OrderID: log.OrderID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &aid, RewardID: &log.RewardID, Remark: prodName + "(倍数)"})
|
||||
if err != nil {
|
||||
s.logger.Error("道具卡-Lottery: 双倍奖励发放失败", zap.Int64("order_id", log.OrderID), zap.Error(err))
|
||||
} else {
|
||||
effectApplied = true
|
||||
s.logger.Info("道具卡-Lottery: 应用双倍奖励成功", zap.Int64("order_id", log.OrderID))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Other effects? If none implemented, do not void.
|
||||
s.logger.Warn("道具卡-Lottery: 未知效果类型或参数无效,不消耗卡片", zap.Int32("effect_type", ic.EffectType))
|
||||
}
|
||||
|
||||
// Only void if effect was successfully applied
|
||||
if effectApplied {
|
||||
_, err := s.writeDB.UserItemCards.WithContext(ctx).Where(s.readDB.UserItemCards.ID.Eq(icID)).Updates(map[string]any{
|
||||
"status": 2,
|
||||
"used_draw_log_id": log.ID,
|
||||
"used_activity_id": aid,
|
||||
"used_issue_id": iss,
|
||||
"used_at": now,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("道具卡-Lottery: 核销卡片失败", zap.Int64("icID", icID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
_, _ = s.writeDB.UserItemCards.WithContext(ctx).Where(s.readDB.UserItemCards.ID.Eq(icID)).Updates(map[string]any{
|
||||
"status": 2,
|
||||
"used_draw_log_id": log.ID,
|
||||
"used_activity_id": aid,
|
||||
"used_issue_id": iss,
|
||||
"used_at": now,
|
||||
})
|
||||
} else {
|
||||
s.logger.Debug("道具卡-Lottery: 范围校验失败")
|
||||
}
|
||||
} else {
|
||||
s.logger.Debug("道具卡-Lottery: 卡片状态无效")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,14 +56,12 @@ func (s *DefaultStrategy) SelectItemFromCache(rewards []*model.ActivityRewardSet
|
||||
}
|
||||
|
||||
func (s *DefaultStrategy) selectItemInternal(rewards []*model.ActivityRewardSettings, seedKey []byte, issueID int64, userID int64) (int64, map[string]any, error) {
|
||||
// 统计有库存的奖品权重
|
||||
// 统计所有奖品权重(不再过滤库存为0的项)
|
||||
var total int64
|
||||
var validCount int
|
||||
for _, r := range rewards {
|
||||
if r.Quantity != 0 {
|
||||
total += int64(r.Weight)
|
||||
validCount++
|
||||
}
|
||||
total += int64(r.Weight)
|
||||
validCount++
|
||||
}
|
||||
if total <= 0 {
|
||||
return 0, nil, fmt.Errorf("no weight: total_rewards=%d, valid_with_stock=%d", len(rewards), validCount)
|
||||
@ -84,9 +82,6 @@ func (s *DefaultStrategy) selectItemInternal(rewards []*model.ActivityRewardSett
|
||||
var acc int64
|
||||
var picked int64
|
||||
for _, r := range rewards {
|
||||
if r.Quantity == 0 {
|
||||
continue
|
||||
}
|
||||
acc += int64(r.Weight)
|
||||
if rnd < acc {
|
||||
picked = r.ID
|
||||
@ -112,17 +107,7 @@ func (s *DefaultStrategy) SelectItemBySlot(ctx context.Context, activityID int64
|
||||
}
|
||||
|
||||
func (s *DefaultStrategy) GrantReward(ctx context.Context, userID int64, rewardID int64) error {
|
||||
// 【使用乐观锁扣减库存】直接用 Quantity > 0 作为更新条件,避免并发超卖
|
||||
result, err := s.write.ActivityRewardSettings.WithContext(ctx).Where(
|
||||
s.write.ActivityRewardSettings.ID.Eq(rewardID),
|
||||
s.write.ActivityRewardSettings.Quantity.Gt(0), // 乐观锁:只有库存>0才能扣减
|
||||
).UpdateSimple(s.write.ActivityRewardSettings.Quantity.Add(-1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("sold out or reward not found")
|
||||
}
|
||||
// 默认策略(纯权重模式)不再执行库存扣减
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -11,11 +11,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@ -29,10 +31,10 @@ const (
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
// FetchAndSyncOrders 从抖店 API 获取订单并同步到本地 (原有按用户同步逻辑)
|
||||
// FetchAndSyncOrders 从抖店 API 获取订单并同步到本地 (按绑定用户同步)
|
||||
FetchAndSyncOrders(ctx context.Context) (*SyncResult, error)
|
||||
// SyncShopOrders 同步店铺全量订单 (专供直播间等全扫描场景)
|
||||
SyncShopOrders(ctx context.Context, activityID int64) (*SyncResult, error)
|
||||
// SyncAllOrders 批量同步所有订单变更 (基于更新时间,不分状态)
|
||||
SyncAllOrders(ctx context.Context, duration time.Duration) (*SyncResult, error)
|
||||
// ListOrders 获取本地抖店订单列表
|
||||
ListOrders(ctx context.Context, page, pageSize int, status *int) ([]*model.DouyinOrders, int64, error)
|
||||
// GetConfig 获取抖店配置
|
||||
@ -188,83 +190,7 @@ func (s *service) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SyncShopOrders 同步店铺全量订单 (专供直播间等全扫描场景)
|
||||
func (s *service) SyncShopOrders(ctx context.Context, activityID int64) (*SyncResult, error) {
|
||||
cfg, err := s.GetConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 临时:强制使用用户提供的最新 Cookie (调试模式)
|
||||
// if cfg.Cookie == "" || len(cfg.Cookie) < 100 {
|
||||
cfg.Cookie = "passport_csrf_token=afcc4debfeacce6454979bb9465999dc; passport_csrf_token_default=afcc4debfeacce6454979bb9465999dc; is_staff_user=false; zsgw_business_data=%7B%22uuid%22%3A%22fa769974-ba17-4daf-94cb-3162ba299c40%22%2C%22platform%22%3A%22pc%22%2C%22source%22%3A%22seo.fxg.jinritemai.com%22%7D; s_v_web_id=verify_mjqlw6yx_mNQjOEnB_oXBo_4Etb_AVQ9_7tQGH9WORNRy; SHOP_ID=47668214; PIGEON_CID=3501298428676440; x-web-secsdk-uid=663d5a20-e75c-4789-bc98-839744bf70bc; Hm_lvt_b6520b076191ab4b36812da4c90f7a5e=1766891015,1766979339,1767628404,1768381245; HMACCOUNT=95F3EBE1C47ED196; ttcid=7962a054674f4dd7bf895af73ae3f34142; passport_mfa_token=CjfZetGovLzEQb6MwoEpMQnvCSomMC9o0P776kEFy77vhrRCAdFvvrnTSpTXY2aib8hCdU5w3tQvGkoKPAAAAAAAAAAAAABP88E%2FGYNOqYg7lJ6fcoAzlVHbNi0bqTR%2Fru8noACGHR%2BtNjtq%2FnW9rBK32mcHCC5TzRDW8YYOGPax0WwgAiIBA3WMQyg%3D; source=seo.fxg.jinritemai.com; gfkadpd=4272,23756; csrf_session_id=b7b4150c5eeefaede4ef5e71473e9dc1; Hm_lpvt_b6520b076191ab4b36812da4c90f7a5e=1768381314; ttwid=1%7CAwu3-vdDBhOP12XdEzmCJlbyX3Qt_5RcioPVgjBIDps%7C1768381315%7Ca763fd05ed6fa274ed997007385cc0090896c597cfac0b812c962faf34f04897; tt_scid=f4YqIWnO3OdWrfVz0YVnJmYahx-qu9o9j.VZC2op7nwrQRodgrSh1ka0Ow3g5nyKd42a; odin_tt=bcf942ae72bd6b4b8f357955b71cc21199b6aec5e9acee4ce64f80704f08ea1cbaaa6e70f444f6a09712806aa424f4d0cce236e77b0bfa2991aa8a23dab27e1e; passport_auth_status=b3b3a865e0bd3857e6a28ea5a6854830%2C228cf6630632c26472c096506639ed6e; passport_auth_status_ss=b3b3a865e0bd3857e6a28ea5a6854830%2C228cf6630632c26472c096506639ed6e; uid_tt=4dfa662033e2e4eefe629ad8815f076f; uid_tt_ss=4dfa662033e2e4eefe629ad8815f076f; sid_tt=4cc6aa2f1a6e338ec72d663a0b611d3c; sessionid=4cc6aa2f1a6e338ec72d663a0b611d3c; sessionid_ss=4cc6aa2f1a6e338ec72d663a0b611d3c; PHPSESSID=a1b2fd062c1346e5c6f94bac3073cd7d; PHPSESSID_SS=a1b2fd062c1346e5c6f94bac3073cd7d; ucas_c0=CkEKBTEuMC4wEJOIgezc9NazaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Cpt53LBkip69nNBlC_vL6Ekt3t1GdYbhIU2LuS6yHmC8_SKu9Jok5ToGxfQIg; ucas_c0_ss=CkEKBTEuMC4wEJOIgezc9NazaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Cpt53LBkip69nNBlC_vL6Ekt3t1GdYbhIU2LuS6yHmC8_SKu9Jok5ToGxfQIg; ecom_gray_shop_id=156231010; sid_guard=4cc6aa2f1a6e338ec72d663a0b611d3c%7C1768381360%7C5184000%7CSun%2C+15-Mar-2026+09%3A02%3A40+GMT; session_tlb_tag=sttt%7C4%7CTMaqLxpuM47HLWY6C2EdPP________-x3_oZvMYjz8-Uw3dAm6JiPFDhS1ih9XTV79AgAO_5cvo%3D; sid_ucp_v1=1.0.0-KGRmNzNkZjM2YjUwZDk2M2M0MjQ5MGE2NzNkNGZkZjNhZWFhYmJkMmIKGQib1oDYuM3aBxCwt53LBhiwISAMOAZA9AcaAmxmIiA0Y2M2YWEyZjFhNmUzMzhlYzcyZDY2M2EwYjYxMWQzYw; ssid_ucp_v1=1.0.0-KGRmNzNkZjM2YjUwZDk2M2M0MjQ5MGE2NzNkNGZkZjNhZWFhYmJkMmIKGQib1oDYuM3aBxCwt53LBhiwISAMOAZA9AcaAmxmIiA0Y2M2YWEyZjFhNmUzMzhlYzcyZDY2M2EwYjYxMWQzYw; COMPASS_LUOPAN_DT=session_7595137429020049706; BUYIN_SASID=SID2_7595138116287152420"
|
||||
// }
|
||||
|
||||
// 1. 获取活动信息以拿到 ProductID
|
||||
var activity model.LivestreamActivities
|
||||
if err := s.repo.GetDbR().Where("id = ?", activityID).First(&activity).Error; err != nil {
|
||||
return nil, fmt.Errorf("查询活动失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 直播间全量同步开始: ActivityID=%d, ProductID=%s\n", activityID, activity.DouyinProductID)
|
||||
|
||||
// 构建请求参数
|
||||
queryParams := url.Values{
|
||||
"page": {"0"},
|
||||
"pageSize": {"20"}, // 增大每页数量以确保覆盖
|
||||
"order_by": {"create_time"},
|
||||
"order": {"desc"},
|
||||
"appid": {"1"},
|
||||
"_bid": {"ffa_order"},
|
||||
"aid": {"4272"},
|
||||
// 新增过滤参数
|
||||
"order_status": {"stock_up"}, // 仅同步待发货/备货中
|
||||
"tab": {"stock_up"},
|
||||
"compact_time[select]": {"create_time_start,create_time_end"},
|
||||
}
|
||||
|
||||
// 如果活动绑定了某些商品,则过滤这些商品
|
||||
if activity.DouyinProductID != "" {
|
||||
queryParams.Set("product", activity.DouyinProductID)
|
||||
}
|
||||
|
||||
// 2. 抓取订单
|
||||
orders, err := s.fetchDouyinOrders(cfg.Cookie, queryParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("抓取全店订单失败: %w", err)
|
||||
}
|
||||
|
||||
result := &SyncResult{
|
||||
TotalFetched: len(orders),
|
||||
DebugInfo: fmt.Sprintf("Activity: %d, ProductID: %s, Fetched: %d", activityID, activity.DouyinProductID, len(orders)),
|
||||
}
|
||||
|
||||
// 3. 遍历并同步
|
||||
for _, order := range orders {
|
||||
// SyncOrder 内部会根据 status 更新或创建,传入 productID
|
||||
isNew, matched := s.SyncOrder(ctx, &order, 0, activity.DouyinProductID)
|
||||
if isNew {
|
||||
result.NewOrders++
|
||||
}
|
||||
if matched {
|
||||
result.MatchedUsers++
|
||||
}
|
||||
|
||||
// 查出同步后的订单记录
|
||||
var dbOrder model.DouyinOrders
|
||||
if err := s.repo.GetDbR().Where("shop_order_id = ?", order.ShopOrderID).First(&dbOrder).Error; err == nil {
|
||||
result.Orders = append(result.Orders, &dbOrder)
|
||||
}
|
||||
|
||||
// 【新增】自动将订单与当前活动绑定 (如果尚未绑定)
|
||||
// 这一步确保即使订单之前存在,也能关联到当前的新活动 ID(如果业务需要一对多,这里可能需要额外表,但目前模型看来是一对一或多对一)
|
||||
// 假设通过 livestream_draw_logs 关联,或者仅仅是同步下来即可。
|
||||
// 目前 SyncOrder 只存 douyin_orders。真正的绑定在 Draw 阶段,或者这里可以做一些预处理。
|
||||
// 暂时保持 SyncOrder 原样,因为 SyncResult 返回给前端后,前端会展示 Pending Orders。
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
// removed SyncShopOrders
|
||||
|
||||
// 抖店 API 响应结构
|
||||
type douyinOrderResponse struct {
|
||||
@ -278,7 +204,8 @@ type DouyinOrderItem struct {
|
||||
ShopOrderID string `json:"shop_order_id"`
|
||||
OrderStatus int `json:"order_status"`
|
||||
UserID string `json:"user_id"`
|
||||
ActualReceiveAmount string `json:"actual_receive_amount"`
|
||||
ActualReceiveAmount any `json:"actual_receive_amount"`
|
||||
ActualPayAmount any `json:"actual_pay_amount"`
|
||||
PayTypeDesc string `json:"pay_type_desc"`
|
||||
Remark string `json:"remark"`
|
||||
UserNickname string `json:"user_nickname"`
|
||||
@ -346,10 +273,15 @@ func (s *service) fetchDouyinOrders(cookie string, params url.Values) ([]DouyinO
|
||||
|
||||
var respData douyinOrderResponse
|
||||
if err := json.Unmarshal(body, &respData); err != nil {
|
||||
s.logger.Error("[抖店API] 解析响应失败", zap.String("response", string(body[:min(len(body), 500)])))
|
||||
s.logger.Error("[抖店API] 解析响应失败", zap.String("response", string(body[:min(len(body), 5000)])))
|
||||
return nil, fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 临时调试日志:打印第一笔订单的金额字段
|
||||
if len(respData.Data) > 0 {
|
||||
fmt.Printf("[DEBUG] 抖店订单 0 金额测试: RawBody(500)=%s\n", string(body[:min(len(body), 500)]))
|
||||
}
|
||||
|
||||
if respData.St != 0 && respData.Code != 0 {
|
||||
return nil, fmt.Errorf("API 返回错误: %s (ST:%d CODE:%d)", respData.Msg, respData.St, respData.Code)
|
||||
}
|
||||
@ -361,6 +293,41 @@ func (s *service) fetchDouyinOrders(cookie string, params url.Values) ([]DouyinO
|
||||
func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64, productID string) (isNew bool, isMatched bool) {
|
||||
db := s.repo.GetDbW().WithContext(ctx)
|
||||
|
||||
// 解析金额工具函数
|
||||
parseMoney := func(val any) int64 {
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
// JSON 数字会被解析为 float64
|
||||
if f, ok := val.(float64); ok {
|
||||
// 如果是数值类型,但带有小数部分(如 138.4),通常是元单位
|
||||
if f != math.Trunc(f) {
|
||||
return int64(f*100 + 0.5)
|
||||
}
|
||||
// 如果是整数,保持原样(分)
|
||||
return int64(f)
|
||||
}
|
||||
|
||||
s := fmt.Sprintf("%v", val)
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
// 只保留数字和点号 (处理 "¥158.40" 这种情况)
|
||||
var sb strings.Builder
|
||||
for _, r := range s {
|
||||
if unicode.IsDigit(r) || r == '.' {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
cleanStr := sb.String()
|
||||
if f, err := strconv.ParseFloat(cleanStr, 64); err == nil {
|
||||
// 字符串一律按元转分处理 (兼容旧逻辑)
|
||||
return int64(f*100 + 0.5)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var order model.DouyinOrders
|
||||
err := db.Where("shop_order_id = ?", item.ShopOrderID).First(&order).Error
|
||||
|
||||
@ -374,12 +341,14 @@ func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestU
|
||||
fmt.Printf("[DEBUG] 抖店辅助关联成功: %s -> User %d\n", item.ShopOrderID, suggestUserID)
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
// 更新状态与金额 (确保之前因解析失败导致的 0 金额被修复)
|
||||
db.Model(&order).Updates(map[string]any{
|
||||
"order_status": item.OrderStatus,
|
||||
"remark": item.Remark,
|
||||
"order_status": item.OrderStatus,
|
||||
"remark": item.Remark,
|
||||
"actual_receive_amount": parseMoney(item.ActualReceiveAmount),
|
||||
"actual_pay_amount": parseMoney(item.ActualPayAmount),
|
||||
})
|
||||
// 重要:同步内存状态,防止后续判断逻辑失效
|
||||
// 重要:同步内存状态
|
||||
order.OrderStatus = int32(item.OrderStatus)
|
||||
order.Remark = item.Remark
|
||||
} else {
|
||||
@ -392,13 +361,8 @@ func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestU
|
||||
|
||||
fmt.Printf("[DEBUG] 抖店新订单: %s, UserID: %s, Recommend: %s\n", item.ShopOrderID, item.UserID, localUserIDStr)
|
||||
|
||||
// 解析金额
|
||||
var amount int64
|
||||
if item.ActualReceiveAmount != "" {
|
||||
if f, err := strconv.ParseFloat(strings.TrimSpace(item.ActualReceiveAmount), 64); err == nil {
|
||||
amount = int64(f * 100)
|
||||
}
|
||||
}
|
||||
amount := parseMoney(item.ActualReceiveAmount)
|
||||
payAmount := parseMoney(item.ActualPayAmount)
|
||||
|
||||
// 计算商品数量:如果指定了 productID,则只统计该商品的数量;否则使用总数量
|
||||
pCount := item.ProductCount
|
||||
@ -429,11 +393,12 @@ func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestU
|
||||
|
||||
order = model.DouyinOrders{
|
||||
ShopOrderID: item.ShopOrderID,
|
||||
DouyinProductID: productID, // 写入商品ID
|
||||
ProductCount: pCount, // 写入计算后的商品数量
|
||||
DouyinProductID: productID, // 写入商品ID
|
||||
ProductCount: int32(pCount), // 写入计算后的商品数量
|
||||
OrderStatus: int32(item.OrderStatus),
|
||||
DouyinUserID: item.UserID,
|
||||
ActualReceiveAmount: amount,
|
||||
ActualPayAmount: payAmount,
|
||||
PayTypeDesc: item.PayTypeDesc,
|
||||
Remark: item.Remark,
|
||||
UserNickname: item.UserNickname,
|
||||
@ -459,21 +424,31 @@ func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestU
|
||||
|
||||
// ---- 统一处理:发放奖励 ----
|
||||
isMatched = order.LocalUserID != "" && order.LocalUserID != "0"
|
||||
if isMatched && order.RewardGranted == 0 && order.OrderStatus == 5 {
|
||||
localUserID, _ := strconv.ParseInt(order.LocalUserID, 10, 64)
|
||||
fmt.Printf("[DEBUG] 准备发放奖励: User: %d, ShopOrder: %s\n", localUserID, item.ShopOrderID)
|
||||
// [修复] 禁用自动发放扫雷资格,防止占用直播间抽奖配额
|
||||
/*
|
||||
if isMatched && order.RewardGranted == 0 && order.OrderStatus == 2 {
|
||||
// 检查黑名单
|
||||
var blacklistCount int64
|
||||
if err := db.Table("douyin_blacklist").Where("douyin_user_id = ?", item.UserID).Count(&blacklistCount).Error; err == nil && blacklistCount > 0 {
|
||||
fmt.Printf("[DEBUG] 用户 %s 在黑名单中,跳过发奖\n", item.UserID)
|
||||
return isNew, isMatched
|
||||
}
|
||||
|
||||
if localUserID > 0 && s.ticketSvc != nil {
|
||||
err := s.ticketSvc.GrantTicket(ctx, localUserID, "minesweeper", 1, "douyin_order", order.ID, "抖店订单奖励")
|
||||
if err == nil {
|
||||
db.Model(&order).Update("reward_granted", 1)
|
||||
order.RewardGranted = 1
|
||||
fmt.Printf("[DEBUG] 订单 %s 发放奖励成功\n", item.ShopOrderID)
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] 订单 %s 发放奖励失败: %v\n", item.ShopOrderID, err)
|
||||
localUserID, _ := strconv.ParseInt(order.LocalUserID, 10, 64)
|
||||
fmt.Printf("[DEBUG] 准备发放奖励: User: %d, ShopOrder: %s\n", localUserID, item.ShopOrderID)
|
||||
|
||||
if localUserID > 0 && s.ticketSvc != nil {
|
||||
err := s.ticketSvc.GrantTicket(ctx, localUserID, "minesweeper", 1, "douyin_order", order.ID, "抖店订单奖励")
|
||||
if err == nil {
|
||||
db.Model(&order).Update("reward_granted", 1)
|
||||
order.RewardGranted = 1
|
||||
fmt.Printf("[DEBUG] 订单 %s 发放奖励成功\n", item.ShopOrderID)
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] 订单 %s 发放奖励失败: %v\n", item.ShopOrderID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return isNew, isMatched
|
||||
}
|
||||
@ -485,3 +460,55 @@ func min(a, b int) int {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// SyncAllOrders 批量同步所有订单变更 (基于更新时间,不分状态)
|
||||
func (s *service) SyncAllOrders(ctx context.Context, duration time.Duration) (*SyncResult, error) {
|
||||
cfg, err := s.GetConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取配置失败: %w", err)
|
||||
}
|
||||
if cfg.Cookie == "" {
|
||||
return nil, fmt.Errorf("抖店 Cookie 未配置")
|
||||
}
|
||||
|
||||
// 临时:强制使用用户提供的最新 Cookie (与 SyncShopOrders 保持一致的调试逻辑)
|
||||
if len(cfg.Cookie) < 100 {
|
||||
cfg.Cookie = "passport_csrf_token=afcc4debfeacce6454979bb9465999dc; passport_csrf_token_default=afcc4debfeacce6454979bb9465999dc; is_staff_user=false; zsgw_business_data=%7B%22uuid%22%3A%22fa769974-ba17-4daf-94cb-3162ba299c40%22%2C%22platform%22%3A%22pc%22%2C%22source%22%3A%22seo.fxg.jinritemai.com%22%7D; s_v_web_id=verify_mjqlw6yx_mNQjOEnB_oXBo_4Etb_AVQ9_7tQGH9WORNRy; SHOP_ID=47668214; PIGEON_CID=3501298428676440; x-web-secsdk-uid=663d5a20-e75c-4789-bc98-839744bf70bc; Hm_lvt_b6520b076191ab4b36812da4c90f7a5e=1766891015,1766979339,1767628404,1768381245; HMACCOUNT=95F3EBE1C47ED196; ttcid=7962a054674f4dd7bf895af73ae3f34142; passport_mfa_token=CjfZetGovLzEQb6MwoEpMQnvCSomMC9o0P776kEFy77vhrRCAdFvvrnTSpTXY2aib8hCdU5w3tQvGkoKPAAAAAAAAAAAAABP88E%2FGYNOqYg7lJ6fcoAzlVHbNi0bqTR%2Fru8noACGHR%2BtNjtq%2FnW9rBK32mcHCC5TzRDW8YYOGPax0WwgAiIBA3WMQyg%3D; source=seo.fxg.jinritemai.com; gfkadpd=4272,23756; csrf_session_id=b7b4150c5eeefaede4ef5e71473e9dc1; Hm_lpvt_b6520b076191ab4b36812da4c90f7a5e=1768381314; ttwid=1%7CAwu3-vdDBhOP12XdEzmCJlbyX3Qt_5RcioPVgjBIDps%7C1768381315%7Ca763fd05ed6fa274ed997007385cc0090896c597cfac0b812c962faf34f04897; tt_scid=f4YqIWnO3OdWrfVz0YVnJmYahx-qu9o9j.VZC2op7nwrQRodgrSh1ka0Ow3g5nyKd42a; odin_tt=bcf942ae72bd6b4b8f357955b71cc21199b6aec5e9acee4ce64f80704f08ea1cbaaa6e70f444f6a09712806aa424f4d0cce236e77b0bfa2991aa8a23dab27e1e; passport_auth_status=b3b3a865e0bd3857e6a28ea5a6854830%2C228cf6630632c26472c096506639ed6e; passport_auth_status_ss=b3b3a865e0bd3857e6a28ea5a6854830%2C228cf6630632c26472c096506639ed6e; uid_tt=4dfa662033e2e4eefe629ad8815f076f; uid_tt_ss=4dfa662033e2e4eefe629ad8815f076f; sid_tt=4cc6aa2f1a6e338ec72d663a0b611d3c; sessionid=4cc6aa2f1a6e338ec72d663a0b611d3c; sessionid_ss=4cc6aa2f1a6e338ec72d663a0b611d3c; PHPSESSID=a1b2fd062c1346e5c6f94bac3073cd7d; PHPSESSID_SS=a1b2fd062c1346e5c6f94bac3073cd7d; ucas_c0=CkEKBTEuMC4wEJOIgezc9NazaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Cpt53LBkip69nNBlC_vL6Ekt3t1GdYbhIU2LuS6yHmC8_SKu9Jok5ToGxfQIg; ucas_c0_ss=CkEKBTEuMC4wEJOIgezc9NazaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Cpt53LBkip69nNBlC_vL6Ekt3t1GdYbhIU2LuS6yHmC8_SKu9Jok5ToGxfQIg; ecom_gray_shop_id=156231010; sid_guard=4cc6aa2f1a6e338ec72d663a0b611d3c%7C1768381360%7C5184000%7CSun%2C+15-Mar-2026+09%3A02%3A40+GMT; session_tlb_tag=sttt%7C4%7CTMaqLxpuM47HLWY6C2EdPP________-x3_oZvMYjz8-Uw3dAm6JiPFDhS1ih9XTV79AgAO_5cvo%3D; sid_ucp_v1=1.0.0-KGRmNzNkZjM2YjUwZDk2M2M0MjQ5MGE2NzNkNGZkZjNhZWFhYmJkMmIKGQib1oDYuM3aBxCwt53LBhiwISAMOAZA9AcaAmxmIiA0Y2M2YWEyZjFhNmUzMzhlYzcyZDY2M2EwYjYxMWQzYw; ssid_ucp_v1=1.0.0-KGRmNzNkZjM2YjUwZDk2M2M0MjQ5MGE2NzNkNGZkZjNhZWFhYmJkMmIKGQib1oDYuM3aBxCwt53LBhiwISAMOAZA9AcaAmxmIiA0Y2M2YWEyZjFhNmUzMzhlYzcyZDY2M2EwYjYxMWQzYw; COMPASS_LUOPAN_DT=session_7595137429020049706; BUYIN_SASID=SID2_7595138116287152420"
|
||||
}
|
||||
|
||||
startTime := time.Now().Add(-duration)
|
||||
|
||||
queryParams := url.Values{
|
||||
"page": {"0"},
|
||||
"pageSize": {"50"},
|
||||
"order_by": {"update_time"},
|
||||
"order": {"desc"},
|
||||
"appid": {"1"},
|
||||
"_bid": {"ffa_order"},
|
||||
"aid": {"4272"},
|
||||
"tab": {"all"}, // 全量状态
|
||||
"update_time_start": {strconv.FormatInt(startTime.Unix(), 10)},
|
||||
}
|
||||
|
||||
orders, err := s.fetchDouyinOrders(cfg.Cookie, queryParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("抓取增量订单失败: %w", err)
|
||||
}
|
||||
|
||||
result := &SyncResult{
|
||||
TotalFetched: len(orders),
|
||||
DebugInfo: fmt.Sprintf("UpdateSince: %s, Fetched: %d", startTime.Format("15:04:05"), len(orders)),
|
||||
}
|
||||
|
||||
for _, order := range orders {
|
||||
isNew, matched := s.SyncOrder(ctx, &order, 0, "") // 不指定 productID,主要用于更新状态
|
||||
if isNew {
|
||||
result.NewOrders++
|
||||
}
|
||||
if matched {
|
||||
result.MatchedUsers++
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ func StartDouyinOrderSync(l logger.CustomLogger, repo mysql.Repo, syscfg sysconf
|
||||
// 初始等待30秒让服务完全启动
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
firstRun := true
|
||||
for {
|
||||
ctx := context.Background()
|
||||
|
||||
@ -59,52 +60,28 @@ func StartDouyinOrderSync(l logger.CustomLogger, repo mysql.Repo, syscfg sysconf
|
||||
}
|
||||
|
||||
// ========== 自动补发扫雷游戏资格 (针对刚才同步到的订单) ==========
|
||||
if err := svc.GrantMinesweeperQualifications(ctx); err != nil {
|
||||
l.Error("[定时补发] 补发扫雷资格失败", zap.Error(err))
|
||||
}
|
||||
// [修复] 禁用自动补发逻辑,防止占用直播间抽奖配额
|
||||
// if err := svc.GrantMinesweeperQualifications(ctx); err != nil {
|
||||
// l.Error("[定时补发] 补发扫雷资格失败", zap.Error(err))
|
||||
// }
|
||||
|
||||
// ========== 自动发放直播间奖品 ==========
|
||||
if err := svc.GrantLivestreamPrizes(ctx); err != nil {
|
||||
l.Error("[定时发放] 发放直播奖品失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// ========== 后置:按活动商品ID同步 (全量兜底) ==========
|
||||
var activities []model.LivestreamActivities
|
||||
if err := repo.GetDbR().Where("status = ?", 1).Find(&activities).Error; err == nil && len(activities) > 0 {
|
||||
l.Info("[抖店定时同步] 发现进行中的直播活动 (全量兜底)", zap.Int("count", len(activities)))
|
||||
for _, act := range activities {
|
||||
if act.DouyinProductID == "" {
|
||||
continue // 跳过未配置商品ID的活动
|
||||
}
|
||||
// SyncShopOrders 会拉取所有订单,如果之前 UserSync 没拉到的(比如未绑定的用户下单),这里可以拉到
|
||||
// 并在之后用户绑定时由 GrantMinesweeperQualifications 的关联逻辑进行补救
|
||||
result, err := svc.SyncShopOrders(ctx, act.ID)
|
||||
if err != nil {
|
||||
l.Error("[抖店定时同步] 活动同步失败",
|
||||
zap.Int64("activity_id", act.ID),
|
||||
zap.String("product_id", act.DouyinProductID),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
l.Info("[抖店定时同步] 活动同步成功",
|
||||
zap.Int64("activity_id", act.ID),
|
||||
zap.String("product_id", act.DouyinProductID),
|
||||
zap.Int("total_fetched", result.TotalFetched),
|
||||
zap.Int("new_orders", result.NewOrders),
|
||||
)
|
||||
}
|
||||
}
|
||||
// ========== 核心:批量同步最近所有订单变更 (基于更新时间,不分状态) ==========
|
||||
// 首次运行同步最近 48 小时以修复潜在的历史遗漏,之后同步最近 1 小时
|
||||
syncDuration := 1 * time.Hour
|
||||
if firstRun {
|
||||
syncDuration = 48 * time.Hour
|
||||
}
|
||||
|
||||
// ========== 新增:自动补发扫雷游戏资格 ==========
|
||||
if err := svc.GrantMinesweeperQualifications(ctx); err != nil {
|
||||
l.Error("[定时补发] 补发扫雷资格失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// ========== 新增:自动发放直播间奖品 ==========
|
||||
if err := svc.GrantLivestreamPrizes(ctx); err != nil {
|
||||
l.Error("[定时发放] 发放直播奖品失败", zap.Error(err))
|
||||
if res, err := svc.SyncAllOrders(ctx, syncDuration); err != nil {
|
||||
l.Error("[定时同步] 全量同步失败", zap.Error(err))
|
||||
} else {
|
||||
l.Info("[定时同步] 全量同步完成", zap.String("info", res.DebugInfo))
|
||||
}
|
||||
firstRun = false
|
||||
|
||||
// ========== 新增:同步退款状态 ==========
|
||||
if err := svc.SyncRefundStatus(ctx); err != nil {
|
||||
@ -131,6 +108,12 @@ func (s *service) GrantMinesweeperQualifications(ctx context.Context) error {
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
// 1.1 检查是否在黑名单中
|
||||
var blacklistCount int64
|
||||
if err := s.repo.GetDbR().Table("douyin_blacklist").Where("douyin_user_id = ?", u.DouyinUserID).Count(&blacklistCount).Error; err == nil && blacklistCount > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. 查找该抖音ID下未关联(local_user_id=0 or empty)的订单
|
||||
var orders []model.DouyinOrders
|
||||
if err := db.Where("douyin_user_id = ? AND (local_user_id = '' OR local_user_id = '0')", u.DouyinUserID).Find(&orders).Error; err != nil {
|
||||
@ -144,15 +127,15 @@ func (s *service) GrantMinesweeperQualifications(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// 4. 如果是已完成的订单(5),且未发奖,则补发
|
||||
if order.OrderStatus == 5 && order.RewardGranted == 0 {
|
||||
// 4. 如果是已支付待发货的订单(2),且未发奖,则补发
|
||||
if order.OrderStatus == 2 && order.RewardGranted == 0 {
|
||||
orderID := order.ID
|
||||
s.logger.Info("[自动补发] 开始补发扫雷资格", zap.Int64("user_id", u.ID), zap.String("shop_order_id", order.ShopOrderID))
|
||||
|
||||
// 调用发奖服务
|
||||
count := int64(1)
|
||||
if order.ProductCount > 0 {
|
||||
count = order.ProductCount
|
||||
count = int64(order.ProductCount)
|
||||
}
|
||||
s.logger.Info("[自动补发] 发放数量", zap.Int64("count", count))
|
||||
|
||||
@ -223,7 +206,7 @@ func (s *service) GrantLivestreamPrizes(ctx context.Context) error {
|
||||
zap.String("prize", log.PrizeName),
|
||||
)
|
||||
|
||||
_, err := s.userSvc.GrantReward(ctx, log.LocalUserID, req)
|
||||
res, err := s.userSvc.GrantReward(ctx, log.LocalUserID, req)
|
||||
if err != nil {
|
||||
s.logger.Error("[自动发放] 发放失败", zap.Error(err))
|
||||
// 如果发放失败是库存原因等,可能需要告警。暂时不重试,等下个周期。
|
||||
@ -231,6 +214,30 @@ func (s *service) GrantLivestreamPrizes(ctx context.Context) error {
|
||||
// 4. 更新发放状态
|
||||
db.Model(&log).Update("is_granted", 1)
|
||||
s.logger.Info("[自动发放] 发放成功", zap.Int64("log_id", log.ID))
|
||||
|
||||
// 5. 自动虚拟发货 (本地状态更新)
|
||||
// 直播间奖品通常为虚拟发货,直接标记为已消费/已发货
|
||||
if res != nil && res.OrderID > 0 {
|
||||
updates := map[string]interface{}{
|
||||
"is_consumed": 1,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if err := s.repo.GetDbW().WithContext(ctx).Model(&model.Orders{}).Where("id = ?", res.OrderID).Updates(updates).Error; err != nil {
|
||||
s.logger.Error("[自动发放] 更新订单状态失败", zap.Int64("order_id", res.OrderID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 更新发货记录
|
||||
shippingUpdates := map[string]interface{}{
|
||||
"status": 2, // 已发货
|
||||
"shipped_at": time.Now(),
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if err := s.repo.GetDbW().WithContext(ctx).Model(&model.ShippingRecords{}).Where("order_id = ?", res.OrderID).Updates(shippingUpdates).Error; err != nil {
|
||||
s.logger.Error("[自动发放] 更新发货记录失败", zap.Int64("order_id", res.OrderID), zap.Error(err))
|
||||
} else {
|
||||
s.logger.Info("[自动发放] 虚拟发货完成(本地)", zap.Int64("order_id", res.OrderID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@ -53,18 +54,23 @@ func NewGameTokenService(l logger.CustomLogger, db mysql.Repo, rdb *redis.Client
|
||||
// GenerateToken creates a new game token for a user (does NOT consume ticket, only validates)
|
||||
func (s *gameTokenService) GenerateToken(ctx context.Context, userID int64, username, avatar, gameType string) (token string, ticket string, expiresAt time.Time, err error) {
|
||||
// 1. Check if user has game tickets (do NOT deduct - will be done on match success)
|
||||
var userTicket model.UserGameTickets
|
||||
if err = s.db.GetDbR().Where("user_id = ? AND game_code = ? AND available > 0", userID, gameType).First(&userTicket).Error; err != nil {
|
||||
return "", "", time.Time{}, fmt.Errorf("no available game tickets")
|
||||
// For free mode, we skip this check
|
||||
if gameType != "minesweeper_free" {
|
||||
var userTicket model.UserGameTickets
|
||||
if err = s.db.GetDbR().Where("user_id = ? AND game_code = ? AND available > 0", userID, gameType).First(&userTicket).Error; err != nil {
|
||||
return "", "", time.Time{}, fmt.Errorf("no available game tickets")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Generate unique ticket ID
|
||||
ticket = fmt.Sprintf("GT%d%d", userID, time.Now().UnixNano())
|
||||
|
||||
// 3. Store ticket in Redis (for single-use validation)
|
||||
// Value format: "{userID}:{gameType}" (to allow game type verification in settlement)
|
||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
||||
// Check for error when setting Redis key - CRITICAL FIX
|
||||
if err := s.redis.Set(ctx, ticketKey, fmt.Sprintf("%d", userID), 30*time.Minute).Err(); err != nil {
|
||||
redisValue := fmt.Sprintf("%d:%s", userID, gameType)
|
||||
if err := s.redis.Set(ctx, ticketKey, redisValue, 30*time.Minute).Err(); err != nil {
|
||||
s.logger.Error("Failed to store ticket in Redis", zap.Error(err), zap.String("ticket", ticket), zap.Int64("user_id", userID))
|
||||
return "", "", time.Time{}, fmt.Errorf("failed to generate ticket: %w", err)
|
||||
}
|
||||
@ -120,7 +126,7 @@ func (s *gameTokenService) ValidateToken(ctx context.Context, tokenString string
|
||||
// 2. Check if ticket is still valid (not used)
|
||||
// TODO: 临时跳过 Redis 验证,仅记录日志用于排查
|
||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", claims.Ticket)
|
||||
storedUserID, err := s.redis.Get(ctx, ticketKey).Result()
|
||||
storedValue, err := s.redis.Get(ctx, ticketKey).Result()
|
||||
if err != nil {
|
||||
s.logger.Warn("DEBUG: Ticket not found in Redis (SKIPPING validation temporarily)",
|
||||
zap.String("ticket", claims.Ticket),
|
||||
@ -128,9 +134,19 @@ func (s *gameTokenService) ValidateToken(ctx context.Context, tokenString string
|
||||
zap.Error(err))
|
||||
// 临时跳过验证,允许游戏继续
|
||||
// return nil, fmt.Errorf("ticket not found or expired")
|
||||
} else if storedUserID != fmt.Sprintf("%d", claims.UserID) {
|
||||
s.logger.Warn("DEBUG: Ticket user mismatch", zap.String("stored", storedUserID), zap.Int64("claim_user", claims.UserID))
|
||||
return nil, fmt.Errorf("ticket user mismatch")
|
||||
} else {
|
||||
// Parse stored value "userID:gameType"
|
||||
parts := strings.Split(storedValue, ":")
|
||||
if len(parts) < 2 {
|
||||
s.logger.Warn("DEBUG: Invalid ticket format in Redis", zap.String("value", storedValue))
|
||||
return nil, fmt.Errorf("invalid ticket format")
|
||||
}
|
||||
storedUserID := parts[0]
|
||||
|
||||
if storedUserID != fmt.Sprintf("%d", claims.UserID) {
|
||||
s.logger.Warn("DEBUG: Ticket user mismatch", zap.String("stored", storedUserID), zap.Int64("claim_user", claims.UserID))
|
||||
return nil, fmt.Errorf("ticket user mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("DEBUG: Token validated successfully", zap.String("ticket", claims.Ticket))
|
||||
|
||||
121
internal/service/game/token_test.go
Normal file
121
internal/service/game/token_test.go
Normal file
@ -0,0 +1,121 @@
|
||||
package game_test
|
||||
|
||||
import (
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/service/game"
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Mock logger
|
||||
type MockLogger struct {
|
||||
logger.CustomLogger
|
||||
}
|
||||
|
||||
func (l *MockLogger) Info(msg string, fields ...zap.Field) {}
|
||||
func (l *MockLogger) Error(msg string, fields ...zap.Field) {}
|
||||
func (l *MockLogger) Warn(msg string, fields ...zap.Field) {}
|
||||
func (l *MockLogger) Debug(msg string, fields ...zap.Field) {}
|
||||
|
||||
func TestGenerateToken_FreeMode(t *testing.T) {
|
||||
// 1. Setup Miniredis
|
||||
mr, err := miniredis.Run()
|
||||
assert.NoError(t, err)
|
||||
defer mr.Close()
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: mr.Addr(),
|
||||
})
|
||||
|
||||
// 2. Setup GORM (SQLite in-memory)
|
||||
// We use an empty DB to ensure NO ticket exists
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// AutoMigrate to make sure table exists (even if empty) to avoid SQL errors
|
||||
// err = db.AutoMigrate(&model.UserGameTickets{})
|
||||
// assert.NoError(t, err)
|
||||
|
||||
repo := mysql.NewTestRepo(db)
|
||||
|
||||
// 3. Create Service
|
||||
svc := game.NewGameTokenService(&MockLogger{}, repo, rdb)
|
||||
|
||||
// 4. Test Case: minesweeper_free
|
||||
ctx := context.Background()
|
||||
userID := int64(12345)
|
||||
username := "testuser"
|
||||
gameCode := "minesweeper_free"
|
||||
|
||||
// Should succeed even though DB is empty (bypasses ticket check)
|
||||
token, ticket, expiresAt, err := svc.GenerateToken(ctx, userID, username, "", gameCode)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
assert.NotEmpty(t, ticket)
|
||||
assert.True(t, expiresAt.After(time.Now()))
|
||||
|
||||
// 5. Verify Redis Key Format
|
||||
// Expected: "userID:gameCode"
|
||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
||||
val, err := rdb.Get(ctx, ticketKey).Result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fmt.Sprintf("%d:%s", userID, gameCode), val)
|
||||
|
||||
// 6. Test Validation
|
||||
claims, err := svc.ValidateToken(ctx, token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, userID, claims.UserID)
|
||||
assert.Equal(t, gameCode, claims.GameType)
|
||||
}
|
||||
|
||||
func TestGenerateToken_PaidMode_NoTicket(t *testing.T) {
|
||||
// 1. Setup
|
||||
mr, err := miniredis.Run()
|
||||
assert.NoError(t, err)
|
||||
defer mr.Close()
|
||||
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
db, _ := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
// db.AutoMigrate(&model.UserGameTickets{}) // Empty table
|
||||
repo := mysql.NewTestRepo(db)
|
||||
svc := game.NewGameTokenService(&MockLogger{}, repo, rdb)
|
||||
|
||||
// 2. Test Case: normal game
|
||||
// Should FAIL because no ticket in DB
|
||||
_, _, _, err = svc.GenerateToken(context.Background(), 12345, "user", "", "minesweeper_paid")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no available game tickets")
|
||||
}
|
||||
|
||||
func TestValidateToken_LegacyFormat_ShouldFail(t *testing.T) {
|
||||
// Test that strict mode rejects legacy keys
|
||||
mr, _ := miniredis.Run()
|
||||
defer mr.Close()
|
||||
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
|
||||
svc := game.NewGameTokenService(&MockLogger{}, mysql.NewTestRepo(nil), rdb)
|
||||
userID := int64(999)
|
||||
|
||||
// Generate valid token first
|
||||
token, ticket, _, _ := svc.GenerateToken(context.Background(), userID, "user", "", "minesweeper_free")
|
||||
|
||||
// Overwrite Redis with legacy format (just userID)
|
||||
rdb.Set(context.Background(), "game:token:ticket:"+ticket, fmt.Sprintf("%d", userID), time.Hour)
|
||||
|
||||
// Now Validate - SHOULD FAIL
|
||||
_, err := svc.ValidateToken(context.Background(), token)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid ticket format")
|
||||
}
|
||||
@ -136,6 +136,7 @@ type CommitmentSummary struct {
|
||||
HasSeed bool `json:"has_seed"`
|
||||
LenSeed int `json:"len_seed_master"`
|
||||
LenHash int `json:"len_seed_hash"`
|
||||
SeedHashHex string `json:"seed_hash_hex"` // 种子哈希的十六进制表示(可公开)
|
||||
}
|
||||
|
||||
// DrawReceipt 抽奖凭证
|
||||
@ -159,7 +160,7 @@ func (s *service) CreateActivity(ctx context.Context, input CreateActivityInput)
|
||||
StreamerContact: input.StreamerContact,
|
||||
AccessCode: accessCode,
|
||||
DouyinProductID: input.DouyinProductID,
|
||||
TicketPrice: input.TicketPrice,
|
||||
TicketPrice: int32(input.TicketPrice),
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
@ -195,7 +196,7 @@ func (s *service) UpdateActivity(ctx context.Context, id int64, input UpdateActi
|
||||
updates["douyin_product_id"] = input.DouyinProductID
|
||||
}
|
||||
if input.TicketPrice != nil {
|
||||
updates["ticket_price"] = *input.TicketPrice
|
||||
updates["ticket_price"] = int32(*input.TicketPrice)
|
||||
}
|
||||
if input.Status != nil {
|
||||
updates["status"] = *input.Status
|
||||
@ -364,6 +365,17 @@ func (s *service) DeletePrize(ctx context.Context, prizeID int64) error {
|
||||
// ========== 抽奖逻辑 ==========
|
||||
|
||||
func (s *service) Draw(ctx context.Context, input DrawInput) (*DrawResult, error) {
|
||||
// 0. 检查黑名单
|
||||
if input.DouyinUserID != "" {
|
||||
var blacklistCount int64
|
||||
if err := s.repo.GetDbR().WithContext(ctx).
|
||||
Table("douyin_blacklist").
|
||||
Where("douyin_user_id = ? AND status = 1", input.DouyinUserID).
|
||||
Count(&blacklistCount).Error; err == nil && blacklistCount > 0 {
|
||||
return nil, fmt.Errorf("该用户已被列入黑名单,无法开奖")
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取可用奖品
|
||||
prizes, err := s.ListPrizes(ctx, input.ActivityID)
|
||||
if err != nil {
|
||||
@ -566,12 +578,19 @@ func (s *service) GetCommitmentSummary(ctx context.Context, activityID int64) (*
|
||||
return nil, fmt.Errorf("活动不存在: %w", err)
|
||||
}
|
||||
|
||||
// 将种子哈希转为十六进制字符串
|
||||
seedHashHex := ""
|
||||
if len(activity.CommitmentSeedHash) > 0 {
|
||||
seedHashHex = hex.EncodeToString(activity.CommitmentSeedHash)
|
||||
}
|
||||
|
||||
return &CommitmentSummary{
|
||||
SeedVersion: activity.CommitmentStateVersion,
|
||||
Algo: activity.CommitmentAlgo,
|
||||
HasSeed: len(activity.CommitmentSeedMaster) > 0,
|
||||
LenSeed: len(activity.CommitmentSeedMaster),
|
||||
LenHash: len(activity.CommitmentSeedHash),
|
||||
SeedHashHex: seedHashHex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -324,23 +324,25 @@ func (d *DynamicConfig) GetCOS(ctx context.Context) COSConfig {
|
||||
|
||||
// GetWechat 获取微信小程序配置
|
||||
func (d *DynamicConfig) GetWechat(ctx context.Context) WechatConfig {
|
||||
staticCfg := configs.Get().Wechat
|
||||
return WechatConfig{
|
||||
AppID: d.Get(ctx, KeyWechatAppID),
|
||||
AppSecret: d.Get(ctx, KeyWechatAppSecret),
|
||||
LotteryResultTemplateID: d.Get(ctx, KeyWechatLotteryResultTemplateID),
|
||||
AppID: d.GetWithFallback(ctx, KeyWechatAppID, staticCfg.AppID),
|
||||
AppSecret: d.GetWithFallback(ctx, KeyWechatAppSecret, staticCfg.AppSecret),
|
||||
LotteryResultTemplateID: d.GetWithFallback(ctx, KeyWechatLotteryResultTemplateID, staticCfg.LotteryResultTemplateID),
|
||||
}
|
||||
}
|
||||
|
||||
// GetWechatPay 获取微信支付配置
|
||||
func (d *DynamicConfig) GetWechatPay(ctx context.Context) WechatPayConfig {
|
||||
staticCfg := configs.Get().WechatPay
|
||||
return WechatPayConfig{
|
||||
MchID: d.Get(ctx, KeyWechatPayMchID),
|
||||
SerialNo: d.Get(ctx, KeyWechatPaySerialNo),
|
||||
PrivateKey: d.Get(ctx, KeyWechatPayPrivateKey),
|
||||
ApiV3Key: d.Get(ctx, KeyWechatPayApiV3Key),
|
||||
NotifyURL: d.Get(ctx, KeyWechatPayNotifyURL),
|
||||
PublicKeyID: d.Get(ctx, KeyWechatPayPublicKeyID),
|
||||
PublicKey: d.Get(ctx, KeyWechatPayPublicKey),
|
||||
MchID: d.GetWithFallback(ctx, KeyWechatPayMchID, staticCfg.MchID),
|
||||
SerialNo: d.GetWithFallback(ctx, KeyWechatPaySerialNo, staticCfg.SerialNo),
|
||||
PrivateKey: d.Get(ctx, KeyWechatPayPrivateKey), // Key content only, no fallback to file path
|
||||
ApiV3Key: d.GetWithFallback(ctx, KeyWechatPayApiV3Key, staticCfg.ApiV3Key),
|
||||
NotifyURL: d.GetWithFallback(ctx, KeyWechatPayNotifyURL, staticCfg.NotifyURL),
|
||||
PublicKeyID: d.GetWithFallback(ctx, KeyWechatPayPublicKeyID, staticCfg.PublicKeyID),
|
||||
PublicKey: d.Get(ctx, KeyWechatPayPublicKey), // Key content only
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
76
internal/service/task_center/invite_logic_test.go
Normal file
76
internal/service/task_center/invite_logic_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package taskcenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
)
|
||||
|
||||
// TestInviteLogicSymmetry 专项测试:验证邀请人数在不同配置下的统计对称性
|
||||
func TestInviteLogicSymmetry(t *testing.T) {
|
||||
repo, err := mysql.NewSQLiteRepoForTest()
|
||||
if err != nil {
|
||||
t.Fatalf("创建 repo 失败: %v", err)
|
||||
}
|
||||
db := repo.GetDbW()
|
||||
initTestTables(t, db)
|
||||
|
||||
// 手动补齐必要的表结构(集成测试需要)
|
||||
db.Exec(`CREATE TABLE orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
source_type INTEGER NOT NULL DEFAULT 0,
|
||||
total_amount INTEGER NOT NULL DEFAULT 0,
|
||||
remark TEXT,
|
||||
deleted_at DATETIME
|
||||
);`)
|
||||
db.Exec(`CREATE TABLE user_invites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
inviter_id INTEGER NOT NULL,
|
||||
invitee_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME
|
||||
);`)
|
||||
db.Exec(`CREATE TABLE activity_draw_logs (order_id INTEGER, issue_id INTEGER);`)
|
||||
db.Exec(`CREATE TABLE activity_issues (id INTEGER, activity_id INTEGER);`)
|
||||
|
||||
svc := New(nil, repo, nil, nil, nil)
|
||||
inviterID := int64(888)
|
||||
|
||||
// === 数据准备 ===
|
||||
// 邀请了 3 个人:101, 102, 103
|
||||
db.Exec("INSERT INTO user_invites (inviter_id, invitee_id) VALUES (?, 101), (?, 102), (?, 103)", inviterID, inviterID, inviterID)
|
||||
|
||||
// 只有 101 在活动 77 中下过单并开奖
|
||||
db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (1, 77)")
|
||||
db.Exec("INSERT INTO orders (id, user_id, status, total_amount, source_type) VALUES (10, 101, 2, 100, 0)")
|
||||
db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (10, 1)")
|
||||
|
||||
// === 场景 1:全局任务 (ActivityID = 0) ===
|
||||
t.Run("GlobalInviteTask", func(t *testing.T) {
|
||||
taskID, _ := InsertTaskWithTierAndReward(t, db, TaskCombination{Metric: MetricInviteCount})
|
||||
db.Exec("UPDATE task_center_task_tiers SET activity_id = 0 WHERE task_id = ?", taskID)
|
||||
|
||||
progress, _ := svc.GetUserProgress(context.Background(), inviterID, taskID)
|
||||
if progress.InviteCount != 3 {
|
||||
t.Errorf("全局任务失败: 期望 3 (注册即计入), 实际 %d", progress.InviteCount)
|
||||
} else {
|
||||
t.Logf("✅ 全局任务验证通过: 邀请人数为 %d (与邀请记录页一致)", progress.InviteCount)
|
||||
}
|
||||
})
|
||||
|
||||
// === 场景 2:特定活动任务 (ActivityID = 77) ===
|
||||
t.Run("ActivitySpecificInviteTask", func(t *testing.T) {
|
||||
taskID, _ := InsertTaskWithTierAndReward(t, db, TaskCombination{Metric: MetricInviteCount})
|
||||
db.Exec("UPDATE task_center_task_tiers SET activity_id = 77 WHERE task_id = ?", taskID)
|
||||
|
||||
progress, _ := svc.GetUserProgress(context.Background(), inviterID, taskID)
|
||||
if progress.InviteCount != 1 {
|
||||
t.Errorf("活动专属任务失败: 期望 1 (仅统计活动77的有效转化), 实际 %d", progress.InviteCount)
|
||||
} else {
|
||||
t.Logf("✅ 活动专属任务验证通过: 邀请人数为 %d (仅包含活动77下的有效用户)", progress.InviteCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -345,10 +345,10 @@ func (s *service) GetUserProgress(ctx context.Context, userID int64, taskID int6
|
||||
queryAmount.Select("COALESCE(SUM(total_amount), 0)").Scan(&orderAmount)
|
||||
}
|
||||
|
||||
// 2. 实时统计邀请数据(有效邀请:被邀请人有消费记录)
|
||||
// 同样应用“已开奖”逻辑过滤
|
||||
// 2. 实时统计邀请数据
|
||||
var inviteCount int64
|
||||
if targetActivityID > 0 {
|
||||
// 根据配置计算:如果任务限定了活动,则只统计在该活动中有有效抽奖的人数(有效转化)
|
||||
db.Raw(`
|
||||
SELECT COUNT(DISTINCT ui.invitee_id)
|
||||
FROM user_invites ui
|
||||
@ -362,13 +362,8 @@ func (s *service) GetUserProgress(ctx context.Context, userID int64, taskID int6
|
||||
)
|
||||
`, userID, targetActivityID).Scan(&inviteCount)
|
||||
} else {
|
||||
db.Raw(`
|
||||
SELECT COUNT(DISTINCT ui.invitee_id)
|
||||
FROM user_invites ui
|
||||
INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2 AND o.source_type != 1
|
||||
WHERE ui.inviter_id = ?
|
||||
AND EXISTS (SELECT 1 FROM activity_draw_logs WHERE activity_draw_logs.order_id = o.id)
|
||||
`, userID).Scan(&inviteCount)
|
||||
// 全量统计(注册即计入):为了与前端“邀请记录”页面的总数对齐(针对全局任务)
|
||||
db.Model(&model.UserInvites{}).Where("inviter_id = ?", userID).Count(&inviteCount)
|
||||
}
|
||||
|
||||
// 3. 首单判断
|
||||
|
||||
@ -658,15 +658,37 @@ func TestGetUserProgress_ActivityFilter_Integration(t *testing.T) {
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
source_type INTEGER NOT NULL DEFAULT 0,
|
||||
total_amount INTEGER NOT NULL DEFAULT 0,
|
||||
actual_amount INTEGER NOT NULL DEFAULT 0,
|
||||
remark TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME
|
||||
);`).Error; err != nil {
|
||||
t.Fatalf("创建 orders 表失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create activity_draw_logs and activity_issues table for joins
|
||||
if !db.Migrator().HasTable("activity_draw_logs") {
|
||||
if err := db.Exec(`CREATE TABLE activity_draw_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
issue_id INTEGER NOT NULL
|
||||
);`).Error; err != nil {
|
||||
t.Fatalf("创建 activity_draw_logs 表失败: %v", err)
|
||||
}
|
||||
}
|
||||
if !db.Migrator().HasTable("activity_issues") {
|
||||
if err := db.Exec(`CREATE TABLE activity_issues (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
activity_id INTEGER NOT NULL
|
||||
);`).Error; err != nil {
|
||||
t.Fatalf("创建 activity_issues 表失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create user_invites table
|
||||
if !db.Migrator().HasTable("user_invites") {
|
||||
if err := db.Exec(`CREATE TABLE user_invites (
|
||||
@ -675,7 +697,8 @@ func TestGetUserProgress_ActivityFilter_Integration(t *testing.T) {
|
||||
invitee_id INTEGER NOT NULL,
|
||||
accumulated_amount INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME
|
||||
);`).Error; err != nil {
|
||||
t.Fatalf("创建 user_invites 表失败: %v", err)
|
||||
}
|
||||
@ -699,28 +722,49 @@ func TestGetUserProgress_ActivityFilter_Integration(t *testing.T) {
|
||||
userID := int64(999)
|
||||
|
||||
// 2. 插入不同类型的订单
|
||||
// 订单 A: 匹配活动 100
|
||||
db.Exec("INSERT INTO orders (user_id, status, actual_amount, remark) VALUES (?, 2, 100, ?)", userID, "activity:100|count:1")
|
||||
// 订单 B: 匹配活动 200 (不应被统计)
|
||||
db.Exec("INSERT INTO orders (user_id, status, actual_amount, remark) VALUES (?, 2, 200, ?)", userID, "activity:200|count:1")
|
||||
// 订单 C: 普通订单 (不应被统计)
|
||||
db.Exec("INSERT INTO orders (user_id, status, actual_amount, remark) VALUES (?, 2, 300, ?)", userID, "normal_order")
|
||||
// 订单 D: 匹配活动 100 但未支付 (不应被统计)
|
||||
db.Exec("INSERT INTO orders (user_id, status, actual_amount, remark) VALUES (?, 1, 100, ?)", userID, "activity:100|count:1")
|
||||
// 准备活动和期数数据
|
||||
db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (10, 100)")
|
||||
db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (20, 200)")
|
||||
|
||||
// 3. 调用 GetUserProgress
|
||||
// 订单 A: 匹配活动 100
|
||||
db.Exec("INSERT INTO orders (id, user_id, status, total_amount, source_type, remark) VALUES (1, ?, 2, 100, 0, ?)", userID, "activity:100|count:1")
|
||||
db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (1, 10)")
|
||||
|
||||
// 订单 B: 匹配活动 200 (不应被统计,因为任务关联的是 100)
|
||||
db.Exec("INSERT INTO orders (id, user_id, status, total_amount, source_type, remark) VALUES (2, ?, 2, 200, 0, ?)", userID, "activity:200|count:1")
|
||||
db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (2, 20)")
|
||||
|
||||
// 订单 C: 普通订单 (不应被统计,因为没有关联活动 100)
|
||||
db.Exec("INSERT INTO orders (id, user_id, status, total_amount, source_type, remark) VALUES (3, ?, 2, 300, 0, ?)", userID, "normal_order")
|
||||
|
||||
// 订单 D: 匹配活动 100 但未支付 (不应被统计)
|
||||
db.Exec("INSERT INTO orders (id, user_id, status, total_amount, source_type, remark) VALUES (4, ?, 1, 100, 0, ?)", userID, "activity:100|count:1")
|
||||
|
||||
// 3. 插入邀请记录
|
||||
db.Exec("INSERT INTO user_invites (inviter_id, invitee_id) VALUES (?, 1001)", userID)
|
||||
db.Exec("INSERT INTO user_invites (inviter_id, invitee_id) VALUES (?, 1002)", userID)
|
||||
|
||||
// 4. 让其中一个被邀请人(1001)在活动 100 中产生有效订单(使其成为“有效邀请”)
|
||||
db.Exec("INSERT INTO orders (id, user_id, status, total_amount, source_type, remark) VALUES (10, 1001, 2, 50, 0, ?)", "activity:100|count:1")
|
||||
db.Exec("INSERT INTO activity_draw_logs (order_id, issue_id) VALUES (10, 10)")
|
||||
|
||||
// 5. 调用 GetUserProgress
|
||||
progress, err := svc.GetUserProgress(context.Background(), userID, taskID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserProgress 失败: %v", err)
|
||||
}
|
||||
|
||||
// 4. 验证
|
||||
// 6. 验证
|
||||
if progress.OrderCount != 1 {
|
||||
t.Errorf("OrderCount 错误: 期望 1, 实际 %d", progress.OrderCount)
|
||||
}
|
||||
if progress.OrderAmount != 100 {
|
||||
t.Errorf("OrderAmount 错误: 期望 100, 实际 %d", progress.OrderAmount)
|
||||
}
|
||||
// 期望 1 (只有 1001 在活动 100 有消费,1002 虽然被邀请但没消费)
|
||||
if progress.InviteCount != 1 {
|
||||
t.Errorf("InviteCount 错误: 期望 1, 实际 %d", progress.InviteCount)
|
||||
}
|
||||
|
||||
t.Logf("ActivityID 过滤测试通过: OrderCount=%d, OrderAmount=%d", progress.OrderCount, progress.OrderAmount)
|
||||
t.Logf("ActivityID 过滤测试通过: OrderCount=%d, OrderAmount=%d, InviteCount=%d", progress.OrderCount, progress.OrderAmount, progress.InviteCount)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user