fix:盈亏计算
This commit is contained in:
parent
5ad2f4ace3
commit
6d33cc7fd0
9
.gitignore
vendored
9
.gitignore
vendored
@ -27,3 +27,12 @@ go.work.sum
|
|||||||
resources/*
|
resources/*
|
||||||
build/resources/admin/
|
build/resources/admin/
|
||||||
logs/
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bindbox-game/configs"
|
||||||
|
"bindbox-game/internal/repository/mysql"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"gorm.io/driver/mysql"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
type RevenueStat struct {
|
||||||
dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local"
|
ActivityID int64
|
||||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
TotalRevenue int64
|
||||||
if err != nil {
|
TotalDiscount int64
|
||||||
log.Fatalf("failed to connect database: %v", err)
|
}
|
||||||
}
|
|
||||||
|
type DrawStat struct {
|
||||||
userID := int64(9082) // User from the report
|
ActivityID int64
|
||||||
|
TotalCount int64
|
||||||
// 1. Check Orders (ALL Status)
|
GamePassCount int64
|
||||||
var orders []model.Orders
|
PaymentCount int64
|
||||||
if err := db.Where("user_id = ?", userID).Find(&orders).Error; err != nil {
|
RefundCount int64
|
||||||
log.Printf("Error querying orders: %v", err)
|
PlayerCount int64
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalAmount int64
|
func main() {
|
||||||
var discountAmount int64
|
flag.Parse()
|
||||||
var pointsAmount int64
|
configs.Init()
|
||||||
|
dbRepo, err := mysql.New()
|
||||||
fmt.Printf("--- ALL Orders for User %d ---\n", userID)
|
if err != nil {
|
||||||
for _, o := range orders {
|
panic(err)
|
||||||
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)
|
db := dbRepo.GetDbR()
|
||||||
if o.Status == 2 { // Only count Paid for Spending simulation (if that's the logic)
|
|
||||||
totalAmount += o.TotalAmount
|
activityIDs := []int64{89}
|
||||||
discountAmount += o.DiscountAmount
|
|
||||||
pointsAmount += o.PointsAmount
|
// 1. Debug Step 2: Draw Stats
|
||||||
}
|
var drawStats []DrawStat
|
||||||
}
|
err = db.Table(model.TableNameActivityDrawLogs).
|
||||||
fmt.Printf("Total Points (Status 2): %d\n", pointsAmount)
|
Select(`
|
||||||
|
activity_issues.activity_id,
|
||||||
// 1.5 Check Points Ledger (Redemptions)
|
COUNT(activity_draw_logs.id) as total_count,
|
||||||
var ledgers []model.UserPointsLedger
|
SUM(CASE WHEN orders.status = 2 AND orders.actual_amount = 0 THEN 1 ELSE 0 END) as game_pass_count,
|
||||||
if err := db.Where("user_id = ? AND action = ?", userID, "redeem_reward").Find(&ledgers).Error; err != nil {
|
SUM(CASE WHEN orders.status = 2 AND orders.actual_amount > 0 THEN 1 ELSE 0 END) as payment_count,
|
||||||
log.Printf("Error querying ledgers: %v", err)
|
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
|
||||||
var totalRedeemedPoints int64
|
`).
|
||||||
fmt.Printf("\n--- Points Redemption (Decomposition) ---\n")
|
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||||
for _, l := range ledgers {
|
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||||||
fmt.Printf("ID: %d, Points: %d, Remark: %s, CreatedAt: %v\n", l.ID, l.Points, l.Remark, l.CreatedAt)
|
Where("activity_issues.activity_id IN ?", activityIDs).
|
||||||
totalRedeemedPoints += l.Points
|
Group("activity_issues.activity_id").
|
||||||
}
|
Scan(&drawStats).Error
|
||||||
fmt.Printf("Total Redeemed Points: %d\n", totalRedeemedPoints)
|
|
||||||
|
if err != nil {
|
||||||
// 2. Check Inventory (Output)
|
fmt.Printf("DrawStats Error: %v\n", err)
|
||||||
type InvItem struct {
|
} else {
|
||||||
ID int64
|
fmt.Printf("DrawStats: %+v\n", drawStats)
|
||||||
ProductID int64
|
}
|
||||||
Status int32
|
|
||||||
Price int64
|
// 2. Debug Step 3: Revenue Stats (With WHERE filter)
|
||||||
Name string
|
var revenueStats []RevenueStat
|
||||||
Remark string // Added Remark field
|
err = db.Table(model.TableNameOrders).
|
||||||
}
|
Select(`
|
||||||
var invItems []InvItem
|
activity_issues.activity_id,
|
||||||
|
SUM(orders.actual_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_revenue,
|
||||||
// Show ALL status
|
SUM(orders.discount_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_discount
|
||||||
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 (
|
||||||
Joins("JOIN products ON products.id = user_inventory.product_id").
|
SELECT activity_draw_logs.order_id, activity_issues.activity_id, COUNT(*) as draw_count
|
||||||
Where("user_inventory.user_id = ?", userID).
|
FROM activity_draw_logs
|
||||||
Where("user_inventory.remark NOT LIKE ? AND user_inventory.remark NOT LIKE ?", "%redeemed%", "%void%").
|
JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id
|
||||||
Scan(&invItems).Error
|
GROUP BY activity_draw_logs.order_id, activity_issues.activity_id
|
||||||
if err != nil {
|
) as order_activity_draws ON order_activity_draws.order_id = orders.id`).
|
||||||
log.Printf("Error querying inventory: %v", err)
|
Joins(`JOIN (
|
||||||
}
|
SELECT order_id, COUNT(*) as total_count
|
||||||
|
FROM activity_draw_logs
|
||||||
var totalPrizeValue int64
|
GROUP BY order_id
|
||||||
var status1Value int64
|
) as order_total_draws ON order_total_draws.order_id = orders.id`).
|
||||||
var status2Value int64
|
Joins("JOIN activity_issues ON activity_issues.activity_id = order_activity_draws.activity_id").
|
||||||
var status3Value int64
|
Where("orders.status = ? AND orders.status != ?", 2, 4).
|
||||||
|
Where("orders.actual_amount > ?", 0). // <--- The problematic filter?
|
||||||
fmt.Printf("\n--- Inventory (ALL Status) for User %d ---\n", userID)
|
Where("activity_issues.activity_id IN ?", activityIDs).
|
||||||
for _, item := range invItems {
|
Group("activity_issues.activity_id").
|
||||||
fmt.Printf("InvID: %d, ProductID: %d, Name: %s, Price: %d, Status: %d, Remark: %s\n",
|
Scan(&revenueStats).Error
|
||||||
item.ID, item.ProductID, item.Name, item.Price, item.Status, item.Remark)
|
|
||||||
|
if err != nil {
|
||||||
if item.Status == 1 || item.Status == 3 {
|
fmt.Printf("RevenueStats (With Filter) Error: %v\n", err)
|
||||||
totalPrizeValue += item.Price
|
} else {
|
||||||
}
|
fmt.Printf("RevenueStats (With Filter): %+v\n", revenueStats)
|
||||||
if item.Status == 1 {
|
}
|
||||||
status1Value += item.Price
|
|
||||||
}
|
// 3. Debug Step 3: Revenue Stats (Without WHERE filter, using CASE in Select)
|
||||||
if item.Status == 2 {
|
var revenueStats2 []RevenueStat
|
||||||
status2Value += item.Price
|
err = db.Table(model.TableNameOrders).
|
||||||
}
|
Select(`
|
||||||
if item.Status == 3 {
|
activity_issues.activity_id,
|
||||||
status3Value += item.Price
|
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
|
||||||
}
|
`).
|
||||||
fmt.Printf("Status 1 (Holding) Value: %d\n", status1Value)
|
Joins(`JOIN (
|
||||||
fmt.Printf("Status 2 (Void/Decomposed) Value: %d\n", status2Value)
|
SELECT activity_draw_logs.order_id, activity_issues.activity_id, COUNT(*) as draw_count
|
||||||
fmt.Printf("Status 3 (Shipped/Used) Value: %d\n", status3Value)
|
FROM activity_draw_logs
|
||||||
fmt.Printf("Total Effective Prize Value (1+3): %d\n", totalPrizeValue)
|
JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id
|
||||||
|
GROUP BY activity_draw_logs.order_id, activity_issues.activity_id
|
||||||
// 3. Calculate Profit
|
) as order_activity_draws ON order_activity_draws.order_id = orders.id`).
|
||||||
profit := totalAmount - totalPrizeValue - discountAmount
|
Joins(`JOIN (
|
||||||
fmt.Printf("\n--- Calculation ---\n")
|
SELECT order_id, COUNT(*) as total_count
|
||||||
fmt.Printf("Profit = Spending (%d) - PrizeValue (%d) - Discount (%d) = %d\n",
|
FROM activity_draw_logs
|
||||||
totalAmount, totalPrizeValue, discountAmount, profit)
|
GROUP BY order_id
|
||||||
|
) as order_total_draws ON order_total_draws.order_id = orders.id`).
|
||||||
fmt.Printf("Formatted:\nSpending: %.2f\nOutput: %.2f\nProfit: %.2f\n", float64(totalAmount)/100, float64(totalPrizeValue)/100, float64(profit)/100)
|
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]
|
[mysql.read]
|
||||||
addr = '150.158.78.154:3306'
|
addr = '150.158.78.154:3306'
|
||||||
name = 'bindbox_game'
|
name = 'dev_game'
|
||||||
pass = 'bindbox2025kdy'
|
pass = 'bindbox2025kdy'
|
||||||
user = 'root'
|
user = 'root'
|
||||||
|
|
||||||
[mysql.write]
|
[mysql.write]
|
||||||
addr = '150.158.78.154:3306'
|
addr = '150.158.78.154:3306'
|
||||||
name = 'bindbox_game'
|
name = 'dev_game'
|
||||||
pass = 'bindbox2025kdy'
|
pass = 'bindbox2025kdy'
|
||||||
user = 'root'
|
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/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.15.0
|
github.com/go-playground/validator/v10 v10.15.0
|
||||||
github.com/go-resty/resty/v2 v2.10.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/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/issue9/identicon/v2 v2.1.2
|
github.com/issue9/identicon/v2 v2.1.2
|
||||||
github.com/pkg/errors v0.9.1
|
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/endpoint-util v1.1.0 // indirect
|
||||||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // 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/aliyun/credentials-go v1.4.5 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // 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/jsonreference v0.19.6 // indirect
|
||||||
github.com/go-openapi/spec v0.20.4 // indirect
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
github.com/go-openapi/swag v0.19.15 // 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/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/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/go-querystring v1.0.0 // indirect
|
github.com/google/go-querystring v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.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/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // 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/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.9.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 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-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/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.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.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
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.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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
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=
|
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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/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/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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
|||||||
@ -39,28 +39,28 @@ type listActivitiesResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type activityDetailResponse struct {
|
type activityDetailResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Banner string `json:"banner"`
|
Banner string `json:"banner"`
|
||||||
ActivityCategoryID int64 `json:"activity_category_id"`
|
ActivityCategoryID int64 `json:"activity_category_id"`
|
||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
PriceDraw int64 `json:"price_draw"`
|
PriceDraw int64 `json:"price_draw"`
|
||||||
IsBoss int32 `json:"is_boss"`
|
IsBoss int32 `json:"is_boss"`
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime time.Time `json:"start_time"`
|
||||||
EndTime time.Time `json:"end_time"`
|
EndTime time.Time `json:"end_time"`
|
||||||
DrawMode string `json:"draw_mode"`
|
DrawMode string `json:"draw_mode"`
|
||||||
PlayType string `json:"play_type"`
|
PlayType string `json:"play_type"`
|
||||||
MinParticipants int64 `json:"min_participants"`
|
MinParticipants int64 `json:"min_participants"`
|
||||||
IntervalMinutes int64 `json:"interval_minutes"`
|
IntervalMinutes int64 `json:"interval_minutes"`
|
||||||
ScheduledTime time.Time `json:"scheduled_time"`
|
ScheduledTime *time.Time `json:"scheduled_time"`
|
||||||
LastSettledAt time.Time `json:"last_settled_at"`
|
LastSettledAt time.Time `json:"last_settled_at"`
|
||||||
RefundCouponID int64 `json:"refund_coupon_id"`
|
RefundCouponID int64 `json:"refund_coupon_id"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
GameplayIntro string `json:"gameplay_intro"`
|
GameplayIntro string `json:"gameplay_intro"`
|
||||||
AllowItemCards bool `json:"allow_item_cards"`
|
AllowItemCards bool `json:"allow_item_cards"`
|
||||||
AllowCoupons bool `json:"allow_coupons"`
|
AllowCoupons bool `json:"allow_coupons"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListActivities 活动列表
|
// ListActivities 活动列表
|
||||||
@ -86,6 +86,16 @@ func (h *handler) ListActivities() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
return
|
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
|
var isBossPtr *int32
|
||||||
if req.IsBoss == 0 || req.IsBoss == 1 {
|
if req.IsBoss == 0 || req.IsBoss == 1 {
|
||||||
isBossPtr = &req.IsBoss
|
isBossPtr = &req.IsBoss
|
||||||
@ -180,7 +190,7 @@ func (h *handler) GetActivityDetail() core.HandlerFunc {
|
|||||||
PlayType: item.PlayType,
|
PlayType: item.PlayType,
|
||||||
MinParticipants: item.MinParticipants,
|
MinParticipants: item.MinParticipants,
|
||||||
IntervalMinutes: item.IntervalMinutes,
|
IntervalMinutes: item.IntervalMinutes,
|
||||||
ScheduledTime: item.ScheduledTime,
|
ScheduledTime: &item.ScheduledTime,
|
||||||
LastSettledAt: item.LastSettledAt,
|
LastSettledAt: item.LastSettledAt,
|
||||||
RefundCouponID: item.RefundCouponID,
|
RefundCouponID: item.RefundCouponID,
|
||||||
Image: item.Image,
|
Image: item.Image,
|
||||||
@ -188,6 +198,13 @@ func (h *handler) GetActivityDetail() core.HandlerFunc {
|
|||||||
AllowItemCards: item.AllowItemCards,
|
AllowItemCards: item.AllowItemCards,
|
||||||
AllowCoupons: item.AllowCoupons,
|
AllowCoupons: item.AllowCoupons,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修复一番赏:即时模式下,清空 ScheduledTime (设置为 nil) 以绕过前端下单拦截
|
||||||
|
// 如果返回零值时间,前端会解析为很早的时间从而判定已结束,必须明确返回 nil
|
||||||
|
if rsp.PlayType == "ichiban" && rsp.DrawMode == "instant" {
|
||||||
|
rsp.ScheduledTime = nil
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,16 +77,18 @@ func (h *handler) ListDrawLogs() core.HandlerFunc {
|
|||||||
pageSize = 100
|
pageSize = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算5分钟前的时间点
|
now := time.Now()
|
||||||
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
// 计算5分钟前的时间点 (用于延迟显示)
|
||||||
|
fiveMinutesAgo := now.Add(-5 * time.Minute)
|
||||||
|
// 计算当天零点 (用于仅显示当天数据)
|
||||||
|
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||||
|
|
||||||
// 为了保证过滤后依然有足够数据,我们多取一些
|
// [修改] 强制获取当天最新的 100 条数据 (Service 层限制最大 100)
|
||||||
fetchPageSize := pageSize
|
// 忽略前端传入的 Page/PageSize,总是获取第一页的 100 条
|
||||||
if pageSize < 100 {
|
fetchPageSize := 100
|
||||||
fetchPageSize = 100 // 至少取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 {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error()))
|
||||||
return
|
return
|
||||||
@ -100,10 +102,21 @@ func (h *handler) ListDrawLogs() core.HandlerFunc {
|
|||||||
|
|
||||||
var filteredItems []*model.ActivityDrawLogs
|
var filteredItems []*model.ActivityDrawLogs
|
||||||
for _, v := range items {
|
for _, v := range items {
|
||||||
// 恢复 5 分钟过滤逻辑
|
// 1. 过滤掉太新的数据 (5分钟延迟)
|
||||||
if v.CreatedAt.After(fiveMinutesAgo) {
|
if v.CreatedAt.After(fiveMinutesAgo) {
|
||||||
continue
|
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 {
|
if len(filteredItems) >= pageSize {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,8 +136,10 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
|
if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
|
||||||
order.CouponID = *req.CouponID
|
|
||||||
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
|
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
|
||||||
|
if applied > 0 {
|
||||||
|
order.CouponID = *req.CouponID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Title Discount Logic
|
// Title Discount Logic
|
||||||
// 1. Fetch active effects for this user, scoped to this activity/issue/category
|
// 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
|
return nil
|
||||||
})
|
})
|
||||||
@ -413,16 +436,8 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
rsp.ActualAmount = order.ActualAmount
|
rsp.ActualAmount = order.ActualAmount
|
||||||
rsp.Status = order.Status
|
rsp.Status = order.Status
|
||||||
|
|
||||||
// Immediate Draw Trigger if Paid (e.g. Game Pass or Free)
|
// 即时开奖触发(已支付 + 即时开奖模式)
|
||||||
if order.Status == 2 && activity.DrawMode == "instant" {
|
if shouldTriggerInstantDraw(order.Status, activity.DrawMode) {
|
||||||
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.
|
|
||||||
go func() {
|
go func() {
|
||||||
_ = h.activity.ProcessOrderLottery(context.Background(), order.ID)
|
_ = h.activity.ProcessOrderLottery(context.Background(), order.ID)
|
||||||
}()
|
}()
|
||||||
|
|||||||
@ -1,11 +1,71 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func TestParseSlotFromRemark(t *testing.T) {
|
func TestParseSlotFromRemark(t *testing.T) {
|
||||||
r := parseSlotFromRemark("lottery:activity:1|issue:2|count:1|slot:42")
|
r := parseSlotFromRemark("lottery:activity:1|issue:2|count:1|slot:42")
|
||||||
if r != 42 { t.Fatalf("slot parse failed: %d", r) }
|
if r != 42 {
|
||||||
r2 := parseSlotFromRemark("lottery:activity:1|issue:2|count:1")
|
t.Fatalf("slot parse failed: %d", r)
|
||||||
if r2 != -1 { t.Fatalf("expected -1, got %d", r2) }
|
}
|
||||||
|
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
|
sc.discount_value
|
||||||
FROM user_coupons uc
|
FROM user_coupons uc
|
||||||
INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1
|
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
|
LIMIT 1
|
||||||
`, userCouponID, userID).Scan(&result).Error
|
`, userCouponID, userID).Scan(&result).Error
|
||||||
|
|
||||||
@ -82,9 +82,6 @@ func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *mode
|
|||||||
switch result.DiscountType {
|
switch result.DiscountType {
|
||||||
case 1: // 金额券
|
case 1: // 金额券
|
||||||
bal := result.BalanceAmount
|
bal := result.BalanceAmount
|
||||||
if bal <= 0 {
|
|
||||||
bal = result.DiscountValue
|
|
||||||
}
|
|
||||||
if bal > 0 {
|
if bal > 0 {
|
||||||
if bal > remainingCap {
|
if bal > remainingCap {
|
||||||
applied = remainingCap
|
applied = remainingCap
|
||||||
@ -125,6 +122,46 @@ func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *mode
|
|||||||
return applied
|
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 应用后更新用户券(扣减余额或核销)
|
// updateUserCouponAfterApply 应用后更新用户券(扣减余额或核销)
|
||||||
// 功能:根据订单 remark 中记录的 applied_amount,
|
// 功能:根据订单 remark 中记录的 applied_amount,
|
||||||
//
|
//
|
||||||
@ -154,7 +191,7 @@ func (h *handler) updateUserCouponAfterApply(ctx core.Context, userID int64, ord
|
|||||||
sc.discount_value
|
sc.discount_value
|
||||||
FROM user_coupons uc
|
FROM user_coupons uc
|
||||||
INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1
|
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
|
LIMIT 1
|
||||||
`, userCouponID, userID).Scan(&result).Error
|
`, userCouponID, userID).Scan(&result).Error
|
||||||
|
|
||||||
@ -274,3 +311,14 @@ func parseIssueIDFromRemark(remarkStr string) int64 {
|
|||||||
func parseCountFromRemark(remarkStr string) int64 {
|
func parseCountFromRemark(remarkStr string) int64 {
|
||||||
return remark.Parse(remarkStr).Count
|
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))
|
zap.Bool("is_ok", scopeOK))
|
||||||
|
|
||||||
if 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 {
|
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||||||
// Double reward
|
cardToVoid = icID // Mark for consumption
|
||||||
h.logger.Info("道具卡-CheckMatchingGame: 应用双倍奖励", zap.Int32("multiplier", ic.RewardMultiplierX1000))
|
h.logger.Info("道具卡-CheckMatchingGame: 应用双倍奖励", zap.Int32("multiplier", ic.RewardMultiplierX1000))
|
||||||
finalQuantity = 2
|
finalQuantity = 2
|
||||||
finalRemark += "(倍数)"
|
finalRemark += "(倍数)"
|
||||||
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
|
} 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))
|
h.logger.Debug("道具卡-CheckMatchingGame: 应用概率提升", zap.Int32("boost_rate", ic.BoostRateX1000))
|
||||||
allRewards, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(
|
allRewards, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(
|
||||||
h.readDB.ActivityRewardSettings.IssueID.Eq(game.IssueID),
|
h.readDB.ActivityRewardSettings.IssueID.Eq(game.IssueID),
|
||||||
@ -593,6 +597,11 @@ func (h *handler) CheckMatchingGame() core.HandlerFunc {
|
|||||||
} else {
|
} else {
|
||||||
h.logger.Debug("道具卡-CheckMatchingGame: 未找到更好的奖品可升级", zap.Int64("current_score", candidate.MinScore))
|
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 {
|
} else {
|
||||||
h.logger.Debug("道具卡-CheckMatchingGame: 范围校验失败")
|
h.logger.Debug("道具卡-CheckMatchingGame: 范围校验失败")
|
||||||
|
|||||||
@ -7,31 +7,31 @@ import (
|
|||||||
|
|
||||||
// TestSelectRewardExact 测试对对碰选奖逻辑:精确匹配 TotalPairs == MinScore
|
// TestSelectRewardExact 测试对对碰选奖逻辑:精确匹配 TotalPairs == MinScore
|
||||||
func TestSelectRewardExact(t *testing.T) {
|
func TestSelectRewardExact(t *testing.T) {
|
||||||
// 模拟奖品设置
|
// 模拟奖品设置 (使用 Level 作为标识,因为 ActivityRewardSettings 没有 Name 字段)
|
||||||
rewards := []*model.ActivityRewardSettings{
|
rewards := []*model.ActivityRewardSettings{
|
||||||
{ID: 1, Name: "奖品A-10对", MinScore: 10, Quantity: 5},
|
{ID: 1, Level: 1, MinScore: 10, Quantity: 5},
|
||||||
{ID: 2, Name: "奖品B-20对", MinScore: 20, Quantity: 5},
|
{ID: 2, Level: 2, MinScore: 20, Quantity: 5},
|
||||||
{ID: 3, Name: "奖品C-30对", MinScore: 30, Quantity: 5},
|
{ID: 3, Level: 3, MinScore: 30, Quantity: 5},
|
||||||
{ID: 4, Name: "奖品D-40对", MinScore: 40, Quantity: 5},
|
{ID: 4, Level: 4, MinScore: 40, Quantity: 5},
|
||||||
{ID: 5, Name: "奖品E-45对", MinScore: 45, Quantity: 5},
|
{ID: 5, Level: 5, MinScore: 45, Quantity: 5},
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
totalPairs int64
|
totalPairs int64
|
||||||
expectReward *int64 // nil = 无匹配
|
expectReward *int64 // nil = 无匹配
|
||||||
expectName string
|
expectLevel int32
|
||||||
}{
|
}{
|
||||||
{"精确匹配10对", 10, ptr(int64(1)), "奖品A-10对"},
|
{"精确匹配10对", 10, ptr(int64(1)), 1},
|
||||||
{"精确匹配20对", 20, ptr(int64(2)), "奖品B-20对"},
|
{"精确匹配20对", 20, ptr(int64(2)), 2},
|
||||||
{"精确匹配30对", 30, ptr(int64(3)), "奖品C-30对"},
|
{"精确匹配30对", 30, ptr(int64(3)), 3},
|
||||||
{"精确匹配40对", 40, ptr(int64(4)), "奖品D-40对"},
|
{"精确匹配40对", 40, ptr(int64(4)), 4},
|
||||||
{"精确匹配45对", 45, ptr(int64(5)), "奖品E-45对"},
|
{"精确匹配45对", 45, ptr(int64(5)), 5},
|
||||||
{"15对-无匹配", 15, nil, ""},
|
{"15对-无匹配", 15, nil, 0},
|
||||||
{"25对-无匹配", 25, nil, ""},
|
{"25对-无匹配", 25, nil, 0},
|
||||||
{"35对-无匹配", 35, nil, ""},
|
{"35对-无匹配", 35, nil, 0},
|
||||||
{"50对-无匹配", 50, nil, ""},
|
{"50对-无匹配", 50, nil, 0},
|
||||||
{"0对-无匹配", 0, nil, ""},
|
{"0对-无匹配", 0, nil, 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@ -40,15 +40,15 @@ func TestSelectRewardExact(t *testing.T) {
|
|||||||
|
|
||||||
if tc.expectReward == nil {
|
if tc.expectReward == nil {
|
||||||
if candidate != nil {
|
if candidate != nil {
|
||||||
t.Errorf("期望无匹配,但得到奖品: %s (ID=%d)", candidate.Name, candidate.ID)
|
t.Errorf("期望无匹配,但得到奖品: Level=%d (ID=%d)", candidate.Level, candidate.ID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if candidate == nil {
|
if candidate == nil {
|
||||||
t.Errorf("期望匹配奖品ID=%d,但无匹配", *tc.expectReward)
|
t.Errorf("期望匹配奖品ID=%d,但无匹配", *tc.expectReward)
|
||||||
} else if candidate.ID != *tc.expectReward {
|
} else if candidate.ID != *tc.expectReward {
|
||||||
t.Errorf("期望奖品ID=%d,实际=%d", *tc.expectReward, candidate.ID)
|
t.Errorf("期望奖品ID=%d,实际=%d", *tc.expectReward, candidate.ID)
|
||||||
} else if candidate.Name != tc.expectName {
|
} else if candidate.Level != tc.expectLevel {
|
||||||
t.Errorf("期望奖品名=%s,实际=%s", tc.expectName, candidate.Name)
|
t.Errorf("期望奖品Level=%d,实际=%d", tc.expectLevel, candidate.Level)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -58,14 +58,14 @@ func TestSelectRewardExact(t *testing.T) {
|
|||||||
// TestSelectRewardWithZeroQuantity 测试库存为0时不匹配
|
// TestSelectRewardWithZeroQuantity 测试库存为0时不匹配
|
||||||
func TestSelectRewardWithZeroQuantity(t *testing.T) {
|
func TestSelectRewardWithZeroQuantity(t *testing.T) {
|
||||||
rewards := []*model.ActivityRewardSettings{
|
rewards := []*model.ActivityRewardSettings{
|
||||||
{ID: 1, Name: "奖品A-10对", MinScore: 10, Quantity: 0}, // 库存为0
|
{ID: 1, Level: 1, MinScore: 10, Quantity: 0}, // 库存为0
|
||||||
{ID: 2, Name: "奖品B-20对", MinScore: 20, Quantity: 5},
|
{ID: 2, Level: 2, MinScore: 20, Quantity: 5},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 即使精确匹配,库存为0也不应匹配
|
// 即使精确匹配,库存为0也不应匹配
|
||||||
candidate := selectRewardExact(rewards, 10)
|
candidate := selectRewardExact(rewards, 10)
|
||||||
if candidate != nil {
|
if candidate != nil {
|
||||||
t.Errorf("库存为0时不应匹配,但得到: %s", candidate.Name)
|
t.Errorf("库存为0时不应匹配,但得到: Level=%d", candidate.Level)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 库存>0应正常匹配
|
// 库存>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 扫描超时未结算的对对碰游戏并自动开奖
|
// autoCheckExpiredGames 扫描超时未结算的对对碰游戏并自动开奖
|
||||||
func (h *handler) autoCheckExpiredGames() {
|
func (h *handler) autoCheckExpiredGames() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 0. 执行数据库兜底扫描 (低频执行,例如每次 autoCheck 都跑,或者加计数器)
|
||||||
|
// 由于 autoCheckHelper 是每3分钟跑一次,这里直接调用损耗可控
|
||||||
|
// 且查询走了索引 (created_at)
|
||||||
|
h.autoCheckDatabaseFallback()
|
||||||
|
|
||||||
// 1. 扫描 Redis 中所有 matching_game key
|
// 1. 扫描 Redis 中所有 matching_game key
|
||||||
keys, err := h.redis.Keys(ctx, activitysvc.MatchingGameKeyPrefix+"*").Result()
|
keys, err := h.redis.Keys(ctx, activitysvc.MatchingGameKeyPrefix+"*").Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -464,6 +464,7 @@ type activityItem struct {
|
|||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
PriceDraw int64 `json:"price_draw"`
|
PriceDraw int64 `json:"price_draw"`
|
||||||
IsBoss int32 `json:"is_boss"`
|
IsBoss int32 `json:"is_boss"`
|
||||||
|
PlayType string `json:"play_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type listActivitiesResponse struct {
|
type listActivitiesResponse struct {
|
||||||
@ -544,6 +545,7 @@ func (h *handler) ListActivities() core.HandlerFunc {
|
|||||||
Status: v.Status,
|
Status: v.Status,
|
||||||
PriceDraw: v.PriceDraw,
|
PriceDraw: v.PriceDraw,
|
||||||
IsBoss: v.IsBoss,
|
IsBoss: v.IsBoss,
|
||||||
|
PlayType: v.PlayType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.Payload(res)
|
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"`
|
ActivityName string `json:"activity_name"`
|
||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
DrawCount int64 `json:"draw_count"`
|
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"`
|
PlayerCount int64 `json:"player_count"`
|
||||||
TotalRevenue int64 `json:"total_revenue"` // 实际支付金额 (分)
|
TotalRevenue int64 `json:"total_revenue"` // 实际支付金额 (分)
|
||||||
TotalDiscount int64 `json:"total_discount"` // 优惠券抵扣金额 (分)
|
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 {
|
type drawStat struct {
|
||||||
ActivityID int64
|
ActivityID int64
|
||||||
DrawCount int64
|
TotalCount int64
|
||||||
PlayerCount int64
|
GamePassCount int64
|
||||||
|
PaymentCount int64
|
||||||
|
RefundCount int64
|
||||||
|
PlayerCount int64
|
||||||
}
|
}
|
||||||
var drawStats []drawStat
|
var drawStats []drawStat
|
||||||
db.Table(model.TableNameActivityDrawLogs).
|
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("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).
|
Where("activity_issues.activity_id IN ?", activityIDs).
|
||||||
Group("activity_issues.activity_id").
|
Group("activity_issues.activity_id").
|
||||||
Scan(&drawStats)
|
Scan(&drawStats)
|
||||||
|
|
||||||
for _, s := range drawStats {
|
for _, s := range drawStats {
|
||||||
if item, ok := activityMap[s.ActivityID]; ok {
|
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
|
item.PlayerCount = s.PlayerCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
||||||
// BUG修复:排除已退款订单(status=4)
|
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
||||||
|
// BUG修复:排除已退款订单(status=4)。
|
||||||
|
// 注意: MySQL SUM()运算涉及除法时会返回Decimal类型,需要Scan到float64
|
||||||
type revenueStat struct {
|
type revenueStat struct {
|
||||||
ActivityID int64
|
ActivityID int64
|
||||||
TotalRevenue int64
|
TotalRevenue float64
|
||||||
TotalDiscount int64
|
TotalDiscount float64
|
||||||
}
|
}
|
||||||
var revenueStats []revenueStat
|
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
|
var err error
|
||||||
err = db.Table(model.TableNameOrders).
|
err = db.Table(model.TableNameOrders).
|
||||||
Select("activity_issues.activity_id, SUM(orders.actual_amount) as total_revenue, SUM(orders.discount_amount) as total_discount").
|
Select(`
|
||||||
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").
|
order_activity_draws.activity_id,
|
||||||
Joins("JOIN activity_issues ON activity_issues.id = dl.issue_id").
|
SUM(1.0 * orders.actual_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_revenue,
|
||||||
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
|
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
|
||||||
Where("activity_issues.activity_id IN ?", activityIDs).
|
`).
|
||||||
Group("activity_issues.activity_id").
|
// 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
|
Scan(&revenueStats).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -171,12 +203,13 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
|||||||
|
|
||||||
for _, s := range revenueStats {
|
for _, s := range revenueStats {
|
||||||
if item, ok := activityMap[s.ActivityID]; ok {
|
if item, ok := activityMap[s.ActivityID]; ok {
|
||||||
item.TotalRevenue = s.TotalRevenue
|
item.TotalRevenue = int64(s.TotalRevenue)
|
||||||
item.TotalDiscount = s.TotalDiscount
|
item.TotalDiscount = int64(s.TotalDiscount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 统计成本 (通过 user_inventory 关联 products)
|
// 4. 统计成本 (通过 user_inventory 关联 products 和 orders)
|
||||||
|
// 修正:增加关联 orders 表,过滤掉已退款/取消的订单 (status!=2)
|
||||||
type costStat struct {
|
type costStat struct {
|
||||||
ActivityID int64
|
ActivityID int64
|
||||||
TotalCost int64
|
TotalCost int64
|
||||||
@ -185,7 +218,9 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
|||||||
db.Table(model.TableNameUserInventory).
|
db.Table(model.TableNameUserInventory).
|
||||||
Select("user_inventory.activity_id, SUM(products.price) as total_cost").
|
Select("user_inventory.activity_id, SUM(products.price) as total_cost").
|
||||||
Joins("JOIN products ON products.id = user_inventory.product_id").
|
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("user_inventory.activity_id IN ?", activityIDs).
|
||||||
|
Where("orders.status = ?", 2). // 仅统计已支付订单产生的成本
|
||||||
Group("user_inventory.activity_id").
|
Group("user_inventory.activity_id").
|
||||||
Scan(&costStats)
|
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").
|
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 activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||||
Joins("JOIN orders ON orders.id = activity_draw_logs.order_id").
|
Joins("JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||||||
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
|
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
|
||||||
Where("orders.actual_amount = 0"). // 0元订单 = 次卡支付
|
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).
|
Where("activity_issues.activity_id IN ?", activityIDs).
|
||||||
Group("activity_issues.activity_id").
|
Group("activity_issues.activity_id").
|
||||||
Scan(&gamePassStats)
|
Scan(&gamePassStats)
|
||||||
@ -376,6 +412,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
|||||||
OrderRemark string // BUG修复:增加remark字段用于解析次数卡使用信息
|
OrderRemark string // BUG修复:增加remark字段用于解析次数卡使用信息
|
||||||
OrderNo string // 订单号
|
OrderNo string // 订单号
|
||||||
DrawCount int64 // 该订单的总抽奖次数(用于金额分摊)
|
DrawCount int64 // 该订单的总抽奖次数(用于金额分摊)
|
||||||
|
UsedDrawLogID int64 // 道具卡实际使用的日志ID
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,6 +440,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
|||||||
COALESCE(orders.remark, '') as order_remark,
|
COALESCE(orders.remark, '') as order_remark,
|
||||||
COALESCE(orders.order_no, '') as order_no,
|
COALESCE(orders.order_no, '') as order_no,
|
||||||
COALESCE(order_draw_counts.draw_count, 1) as draw_count,
|
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
|
activity_draw_logs.created_at
|
||||||
`).
|
`).
|
||||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
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 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 products ON products.id = activity_reward_settings.product_id").
|
||||||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_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 user_coupons ON user_coupons.id = orders.coupon_id").
|
||||||
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = orders.item_card_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").
|
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).
|
Where("activity_issues.activity_id = ?", activityID).
|
||||||
Order("activity_draw_logs.id DESC").
|
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.ItemCardUsed = true
|
||||||
paymentDetails.ItemCardName = l.ItemCardName
|
paymentDetails.ItemCardName = l.ItemCardName
|
||||||
if paymentDetails.ItemCardName == "" {
|
if paymentDetails.ItemCardName == "" {
|
||||||
|
|||||||
@ -189,10 +189,11 @@ type trendRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type trendPoint struct {
|
type trendPoint struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
Value int64 `json:"value"`
|
Value int64 `json:"value"`
|
||||||
Gmv int64 `json:"gmv"`
|
Gmv int64 `json:"gmv"`
|
||||||
Orders int64 `json:"orders"`
|
Orders int64 `json:"orders"`
|
||||||
|
NewUsers int64 `json:"newUsers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type salesDrawTrendResponse struct {
|
type salesDrawTrendResponse struct {
|
||||||
@ -1063,11 +1064,14 @@ func (h *handler) DashboardActivityPrizeAnalysis() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
drawCounts := make(map[int64]int64)
|
drawCounts := make(map[int64]int64)
|
||||||
var dcRows []countResult
|
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")).
|
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.ActivityDrawLogs.IssueID.In(issueIDs...)).
|
||||||
|
Where(h.readDB.Orders.Status.Eq(2)).
|
||||||
Group(h.readDB.ActivityDrawLogs.IssueID).
|
Group(h.readDB.ActivityDrawLogs.IssueID).
|
||||||
Scan(&dcRows)
|
Scan(&dcRows)
|
||||||
|
|
||||||
for _, r := range dcRows {
|
for _, r := range dcRows {
|
||||||
drawCounts[r.Key] = r.Count
|
drawCounts[r.Key] = r.Count
|
||||||
}
|
}
|
||||||
@ -1075,10 +1079,12 @@ func (h *handler) DashboardActivityPrizeAnalysis() core.HandlerFunc {
|
|||||||
// 5.2 每个奖品的中奖数 (RewardID -> Count)
|
// 5.2 每个奖品的中奖数 (RewardID -> Count)
|
||||||
winCounts := make(map[int64]int64)
|
winCounts := make(map[int64]int64)
|
||||||
var wcRows []countResult
|
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")).
|
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.IssueID.In(issueIDs...)).
|
||||||
Where(h.readDB.ActivityDrawLogs.IsWinner.Eq(1)).
|
Where(h.readDB.ActivityDrawLogs.IsWinner.Eq(1)).
|
||||||
|
Where(h.readDB.Orders.Status.Eq(2)).
|
||||||
Group(h.readDB.ActivityDrawLogs.RewardID).
|
Group(h.readDB.ActivityDrawLogs.RewardID).
|
||||||
Scan(&wcRows)
|
Scan(&wcRows)
|
||||||
for _, r := range wcRows {
|
for _, r := range wcRows {
|
||||||
@ -1086,8 +1092,11 @@ func (h *handler) DashboardActivityPrizeAnalysis() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5.3 活动总参与人数 (Distinct UserID)
|
// 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.ActivityDrawLogs.IssueID.In(issueIDs...)).
|
||||||
|
Where(h.readDB.Orders.Status.Eq(2)).
|
||||||
Distinct(h.readDB.ActivityDrawLogs.UserID).
|
Distinct(h.readDB.ActivityDrawLogs.UserID).
|
||||||
Count()
|
Count()
|
||||||
|
|
||||||
@ -1605,11 +1614,18 @@ func (h *handler) DashboardSalesDrawTrend() core.HandlerFunc {
|
|||||||
Where(h.readDB.Orders.PaidAt.Lte(b.End)).
|
Where(h.readDB.Orders.PaidAt.Lte(b.End)).
|
||||||
Count()
|
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{
|
list[i] = trendPoint{
|
||||||
Date: b.Label,
|
Date: b.Label,
|
||||||
Value: draws,
|
Value: draws,
|
||||||
Gmv: gmv.Total / 100, // 转为元
|
Gmv: gmv.Total / 100, // 转为元
|
||||||
Orders: orders,
|
Orders: orders,
|
||||||
|
NewUsers: newUsers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1630,6 +1646,8 @@ type productPerformanceItem struct {
|
|||||||
SeriesName string `json:"seriesName"`
|
SeriesName string `json:"seriesName"`
|
||||||
SalesCount int64 `json:"salesCount"`
|
SalesCount int64 `json:"salesCount"`
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
|
Profit int64 `json:"profit"`
|
||||||
|
ProfitRate float64 `json:"profitRate"`
|
||||||
ContributionRate float64 `json:"contributionRate"`
|
ContributionRate float64 `json:"contributionRate"`
|
||||||
InventoryTurnover float64 `json:"inventoryTurnover"`
|
InventoryTurnover float64 `json:"inventoryTurnover"`
|
||||||
}
|
}
|
||||||
@ -1640,24 +1658,26 @@ func (h *handler) OperationsProductPerformance() core.HandlerFunc {
|
|||||||
|
|
||||||
// 按活动聚合抽奖数据
|
// 按活动聚合抽奖数据
|
||||||
type drawRow struct {
|
type drawRow struct {
|
||||||
ActivityID int64
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
Count int64
|
Count int64 `gorm:"column:count"`
|
||||||
Winners int64
|
TotalCost int64 `gorm:"column:total_cost"`
|
||||||
}
|
}
|
||||||
var rows []drawRow
|
var rows []drawRow
|
||||||
|
|
||||||
// 统计抽奖日志,按活动分组
|
// 统计抽奖日志,按活动分组,并计算奖品成本
|
||||||
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().
|
_ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).UnderlyingDB().
|
||||||
LeftJoin(h.readDB.ActivityIssues, h.readDB.ActivityIssues.ID.EqCol(h.readDB.ActivityDrawLogs.IssueID)).
|
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Gte(s)).
|
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
|
||||||
Where(h.readDB.ActivityDrawLogs.CreatedAt.Lte(e)).
|
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(
|
Select(
|
||||||
h.readDB.ActivityIssues.ActivityID,
|
"activity_issues.activity_id",
|
||||||
h.readDB.ActivityDrawLogs.ID.Count().As("count"),
|
"COUNT(activity_draw_logs.id) as count",
|
||||||
h.readDB.ActivityDrawLogs.IsWinner.Sum().As("winners"),
|
"SUM(IF(activity_draw_logs.is_winner = 1, products.price, 0)) as total_cost",
|
||||||
).
|
).
|
||||||
Group(h.readDB.ActivityIssues.ActivityID).
|
Group("activity_issues.activity_id").
|
||||||
Order(h.readDB.ActivityDrawLogs.ID.Count().Desc()).
|
Order("count DESC").
|
||||||
Limit(10).
|
Limit(10).
|
||||||
Scan(&rows)
|
Scan(&rows)
|
||||||
|
|
||||||
@ -1707,9 +1727,16 @@ func (h *handler) OperationsProductPerformance() core.HandlerFunc {
|
|||||||
SeriesName: info.Name,
|
SeriesName: info.Name,
|
||||||
SalesCount: r.Count,
|
SalesCount: r.Count,
|
||||||
Amount: (r.Count * info.PriceDraw) / 100, // 转换为元
|
Amount: (r.Count * info.PriceDraw) / 100, // 转换为元
|
||||||
|
Profit: (r.Count*info.PriceDraw - r.TotalCost) / 100,
|
||||||
|
ProfitRate: 0,
|
||||||
ContributionRate: float64(int(contribution*10)) / 10.0,
|
ContributionRate: float64(int(contribution*10)) / 10.0,
|
||||||
InventoryTurnover: float64(int(turnover*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)
|
ctx.Payload(out)
|
||||||
|
|||||||
@ -24,26 +24,30 @@ type spendingLeaderboardItem struct {
|
|||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
OrderCount int64 `json:"order_count"`
|
OrderCount int64 `json:"-"` // Hidden
|
||||||
TotalSpending int64 `json:"total_spending"` // Total Paid Amount (Fen)
|
TotalSpending int64 `json:"-"` // Hidden
|
||||||
TotalPrizeValue int64 `json:"total_prize_value"` // Total Product Price (Fen)
|
TotalPrizeValue int64 `json:"-"` // Hidden
|
||||||
TotalDiscount int64 `json:"total_discount"` // Total Coupon Discount (Fen)
|
TotalDiscount int64 `json:"total_discount"` // Total Coupon Discount (Fen)
|
||||||
TotalPoints int64 `json:"total_points"` // Total Points Discount (Fen)
|
TotalPoints int64 `json:"total_points"` // Total Points Discount (Fen)
|
||||||
GamePassCount int64 `json:"game_pass_count"` // Count of SourceType=4
|
GamePassCount int64 `json:"game_pass_count"` // Count of SourceType=4
|
||||||
ItemCardCount int64 `json:"item_card_count"` // Count where ItemCardID > 0
|
ItemCardCount int64 `json:"item_card_count"` // Count where ItemCardID > 0
|
||||||
// Breakdown by game type
|
// Breakdown by game type
|
||||||
IchibanSpending int64 `json:"ichiban_spending"`
|
IchibanSpending int64 `json:"ichiban_spending"`
|
||||||
IchibanPrize int64 `json:"ichiban_prize"`
|
IchibanPrize int64 `json:"ichiban_prize"`
|
||||||
|
IchibanProfit int64 `json:"ichiban_profit"`
|
||||||
IchibanCount int64 `json:"ichiban_count"`
|
IchibanCount int64 `json:"ichiban_count"`
|
||||||
InfiniteSpending int64 `json:"infinite_spending"`
|
InfiniteSpending int64 `json:"infinite_spending"`
|
||||||
InfinitePrize int64 `json:"infinite_prize"`
|
InfinitePrize int64 `json:"infinite_prize"`
|
||||||
|
InfiniteProfit int64 `json:"infinite_profit"`
|
||||||
InfiniteCount int64 `json:"infinite_count"`
|
InfiniteCount int64 `json:"infinite_count"`
|
||||||
MatchingSpending int64 `json:"matching_spending"`
|
MatchingSpending int64 `json:"matching_spending"`
|
||||||
MatchingPrize int64 `json:"matching_prize"`
|
MatchingPrize int64 `json:"matching_prize"`
|
||||||
|
MatchingProfit int64 `json:"matching_profit"`
|
||||||
MatchingCount int64 `json:"matching_count"`
|
MatchingCount int64 `json:"matching_count"`
|
||||||
// 直播间统计 (source_type=5)
|
// 直播间统计 (source_type=5)
|
||||||
LivestreamSpending int64 `json:"livestream_spending"`
|
LivestreamSpending int64 `json:"livestream_spending"`
|
||||||
LivestreamPrize int64 `json:"livestream_prize"`
|
LivestreamPrize int64 `json:"livestream_prize"`
|
||||||
|
LivestreamProfit int64 `json:"livestream_profit"`
|
||||||
LivestreamCount int64 `json:"livestream_count"`
|
LivestreamCount int64 `json:"livestream_count"`
|
||||||
|
|
||||||
Profit int64 `json:"profit"` // Spending - PrizeValue
|
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)))
|
h.logger.Info(fmt.Sprintf("SpendingLeaderboard SQL done: count=%d", len(stats)))
|
||||||
|
|
||||||
// 2. Collect User IDs
|
// 2. Collect User IDs
|
||||||
userIDs := make([]int64, len(stats))
|
userIDs := make([]int64, 0, len(stats))
|
||||||
statMap := make(map[int64]*spendingLeaderboardItem)
|
statMap := make(map[int64]*spendingLeaderboardItem)
|
||||||
for i, s := range stats {
|
for _, s := range stats {
|
||||||
userIDs[i] = s.UserID
|
userIDs = append(userIDs, s.UserID)
|
||||||
statMap[s.UserID] = &spendingLeaderboardItem{
|
statMap[s.UserID] = &spendingLeaderboardItem{
|
||||||
UserID: s.UserID,
|
UserID: s.UserID,
|
||||||
TotalSpending: s.TotalAmount,
|
TotalSpending: s.TotalAmount,
|
||||||
@ -155,11 +159,39 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
|||||||
InfiniteCount: s.InfiniteCount,
|
InfiniteCount: s.InfiniteCount,
|
||||||
MatchingSpending: s.MatchingSpending,
|
MatchingSpending: s.MatchingSpending,
|
||||||
MatchingCount: s.MatchingCount,
|
MatchingCount: s.MatchingCount,
|
||||||
LivestreamSpending: s.LivestreamSpending,
|
LivestreamSpending: 0, // Will be updated from douyin_orders
|
||||||
LivestreamCount: s.LivestreamCount,
|
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 {
|
if len(userIDs) > 0 {
|
||||||
// 3. Get User Info
|
// 3. Get User Info
|
||||||
// Use h.readDB.Users (GEN) as it's simple
|
// 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).
|
// Only include Holding (1) and Shipped/Used (3) items. Exclude Void/Decomposed (2).
|
||||||
query = query.Where("user_inventory.status IN ?", []int{1, 3}).
|
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(`
|
err := query.Select(`
|
||||||
user_inventory.user_id,
|
user_inventory.user_id,
|
||||||
SUM(products.price) as total_value,
|
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 = 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 = 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 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
|
|
||||||
`).
|
`).
|
||||||
Group("user_inventory.user_id").
|
Group("user_inventory.user_id").
|
||||||
Scan(&invStats).Error
|
Scan(&invStats).Error
|
||||||
@ -215,18 +246,58 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
|||||||
item.IchibanPrize = is.IchibanPrize
|
item.IchibanPrize = is.IchibanPrize
|
||||||
item.InfinitePrize = is.InfinitePrize
|
item.InfinitePrize = is.InfinitePrize
|
||||||
item.MatchingPrize = is.MatchingPrize
|
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
|
// 5. Calculate Profit and Final List
|
||||||
list := make([]spendingLeaderboardItem, 0, len(statMap))
|
list := make([]spendingLeaderboardItem, 0, len(statMap))
|
||||||
for _, item := range statMap {
|
for _, item := range statMap {
|
||||||
item.Profit = item.TotalSpending - item.TotalPrizeValue
|
// Calculate totals based on the 4 displayed categories to ensure UI consistency
|
||||||
if item.TotalSpending > 0 {
|
calculatedSpending := item.IchibanSpending + item.InfiniteSpending + item.MatchingSpending + item.LivestreamSpending
|
||||||
item.ProfitRate = float64(item.Profit) / float64(item.TotalSpending)
|
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)
|
list = append(list, *item)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,9 +70,11 @@ type douyinOrderItem struct {
|
|||||||
LocalUserID int64 `json:"local_user_id"`
|
LocalUserID int64 `json:"local_user_id"`
|
||||||
LocalUserNickname string `json:"local_user_nickname"`
|
LocalUserNickname string `json:"local_user_nickname"`
|
||||||
ActualReceiveAmount string `json:"actual_receive_amount"`
|
ActualReceiveAmount string `json:"actual_receive_amount"`
|
||||||
|
ActualPayAmount string `json:"actual_pay_amount"`
|
||||||
PayTypeDesc string `json:"pay_type_desc"`
|
PayTypeDesc string `json:"pay_type_desc"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
UserNickname string `json:"user_nickname"`
|
UserNickname string `json:"user_nickname"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,9 +131,11 @@ func (h *handler) ListDouyinOrders() core.HandlerFunc {
|
|||||||
LocalUserID: uid,
|
LocalUserID: uid,
|
||||||
LocalUserNickname: userNicknameMap[uid],
|
LocalUserNickname: userNicknameMap[uid],
|
||||||
ActualReceiveAmount: formatAmount(o.ActualReceiveAmount),
|
ActualReceiveAmount: formatAmount(o.ActualReceiveAmount),
|
||||||
|
ActualPayAmount: formatAmount(o.ActualPayAmount),
|
||||||
PayTypeDesc: o.PayTypeDesc,
|
PayTypeDesc: o.PayTypeDesc,
|
||||||
Remark: o.Remark,
|
Remark: o.Remark,
|
||||||
UserNickname: o.UserNickname,
|
UserNickname: o.UserNickname,
|
||||||
|
ProductCount: int64(o.ProductCount),
|
||||||
CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,7 +186,7 @@ func getOrderStatusText(status int32) string {
|
|||||||
case 3:
|
case 3:
|
||||||
return "已发货"
|
return "已发货"
|
||||||
case 4:
|
case 4:
|
||||||
return "已取消"
|
return "已退款/已取消"
|
||||||
case 5:
|
case 5:
|
||||||
return "已完成"
|
return "已完成"
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -8,8 +8,6 @@ import (
|
|||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/validation"
|
"bindbox-game/internal/pkg/validation"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
|
|
||||||
"gorm.io/datatypes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ======== 抖店商品奖励规则 CRUD ========
|
// ======== 抖店商品奖励规则 CRUD ========
|
||||||
@ -114,7 +112,7 @@ func (h *handler) CreateDouyinProductReward() core.HandlerFunc {
|
|||||||
ProductID: req.ProductID,
|
ProductID: req.ProductID,
|
||||||
ProductName: req.ProductName,
|
ProductName: req.ProductName,
|
||||||
RewardType: req.RewardType,
|
RewardType: req.RewardType,
|
||||||
RewardPayload: datatypes.JSON(req.RewardPayload),
|
RewardPayload: string(req.RewardPayload),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
}
|
}
|
||||||
@ -153,7 +151,7 @@ func (h *handler) UpdateDouyinProductReward() core.HandlerFunc {
|
|||||||
updates := map[string]any{
|
updates := map[string]any{
|
||||||
"product_name": req.ProductName,
|
"product_name": req.ProductName,
|
||||||
"reward_type": req.RewardType,
|
"reward_type": req.RewardType,
|
||||||
"reward_payload": datatypes.JSON(req.RewardPayload),
|
"reward_payload": string(req.RewardPayload),
|
||||||
"quantity": req.Quantity,
|
"quantity": req.Quantity,
|
||||||
"status": req.Status,
|
"status": req.Status,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
@ -10,6 +11,8 @@ import (
|
|||||||
"bindbox-game/internal/pkg/validation"
|
"bindbox-game/internal/pkg/validation"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
"bindbox-game/internal/service/livestream"
|
"bindbox-game/internal/service/livestream"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ========== 直播间活动管理 ==========
|
// ========== 直播间活动管理 ==========
|
||||||
@ -89,7 +92,7 @@ func (h *handler) CreateLivestreamActivity() core.HandlerFunc {
|
|||||||
StreamerContact: activity.StreamerContact,
|
StreamerContact: activity.StreamerContact,
|
||||||
AccessCode: activity.AccessCode,
|
AccessCode: activity.AccessCode,
|
||||||
DouyinProductID: activity.DouyinProductID,
|
DouyinProductID: activity.DouyinProductID,
|
||||||
TicketPrice: activity.TicketPrice,
|
TicketPrice: int64(activity.TicketPrice),
|
||||||
Status: activity.Status,
|
Status: activity.Status,
|
||||||
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
})
|
})
|
||||||
@ -224,7 +227,7 @@ func (h *handler) ListLivestreamActivities() core.HandlerFunc {
|
|||||||
StreamerContact: a.StreamerContact,
|
StreamerContact: a.StreamerContact,
|
||||||
AccessCode: a.AccessCode,
|
AccessCode: a.AccessCode,
|
||||||
DouyinProductID: a.DouyinProductID,
|
DouyinProductID: a.DouyinProductID,
|
||||||
TicketPrice: a.TicketPrice,
|
TicketPrice: int64(a.TicketPrice),
|
||||||
Status: a.Status,
|
Status: a.Status,
|
||||||
CreatedAt: a.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: a.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
@ -273,7 +276,7 @@ func (h *handler) GetLivestreamActivity() core.HandlerFunc {
|
|||||||
StreamerContact: activity.StreamerContact,
|
StreamerContact: activity.StreamerContact,
|
||||||
AccessCode: activity.AccessCode,
|
AccessCode: activity.AccessCode,
|
||||||
DouyinProductID: activity.DouyinProductID,
|
DouyinProductID: activity.DouyinProductID,
|
||||||
TicketPrice: activity.TicketPrice,
|
TicketPrice: int64(activity.TicketPrice),
|
||||||
Status: activity.Status,
|
Status: activity.Status,
|
||||||
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
@ -484,14 +487,25 @@ type listLivestreamDrawLogsResponse struct {
|
|||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PageSize int `json:"page_size"`
|
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 {
|
type listLivestreamDrawLogsRequest struct {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
PageSize int `form:"page_size"`
|
PageSize int `form:"page_size"`
|
||||||
StartTime string `form:"start_time"`
|
StartTime string `form:"start_time"`
|
||||||
EndTime string `form:"end_time"`
|
EndTime string `form:"end_time"`
|
||||||
Keyword string `form:"keyword"`
|
Keyword string `form:"keyword"`
|
||||||
|
ExcludeUserIDs string `form:"exclude_user_ids"` // 逗号分隔的 UserIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListLivestreamDrawLogs 获取中奖记录
|
// ListLivestreamDrawLogs 获取中奖记录
|
||||||
@ -530,21 +544,39 @@ func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
|
|||||||
pageSize = 20
|
pageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析时间范围
|
// 解析时间范围 (支持 YYYY-MM-DD HH:mm:ss 和 YYYY-MM-DD)
|
||||||
var startTime, endTime *time.Time
|
var startTime, endTime *time.Time
|
||||||
if req.StartTime != "" {
|
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
|
startTime = &t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.EndTime != "" {
|
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)
|
end := t.Add(24*time.Hour - time.Nanosecond)
|
||||||
endTime = &end
|
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
|
// 使用底层 GORM 直接查询以支持 keyword
|
||||||
db := h.repo.GetDbR().Model(&model.LivestreamDrawLogs{}).Where("activity_id = ?", activityID)
|
db := h.repo.GetDbR().Model(&model.LivestreamDrawLogs{}).Where("activity_id = ?", activityID)
|
||||||
|
|
||||||
@ -558,12 +590,115 @@ func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
|
|||||||
keyword := "%" + req.Keyword + "%"
|
keyword := "%" + req.Keyword + "%"
|
||||||
db = db.Where("(user_nickname LIKE ? OR shop_order_id LIKE ? OR prize_name LIKE ?)", keyword, keyword, 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
|
var total int64
|
||||||
db.Count(&total)
|
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
|
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()))
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -573,6 +708,7 @@ func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
|
|||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
|
Stats: stats,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, log := range logs {
|
for i, log := range logs {
|
||||||
@ -604,6 +740,7 @@ type livestreamCommitmentSummaryResponse struct {
|
|||||||
HasSeed bool `json:"has_seed"`
|
HasSeed bool `json:"has_seed"`
|
||||||
LenSeed int `json:"len_seed_master"`
|
LenSeed int `json:"len_seed_master"`
|
||||||
LenHash int `json:"len_seed_hash"`
|
LenHash int `json:"len_seed_hash"`
|
||||||
|
SeedHashHex string `json:"seed_hash_hex"` // 种子哈希的十六进制表示(可公开复制)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateLivestreamCommitment 生成直播间活动承诺
|
// GenerateLivestreamCommitment 生成直播间活动承诺
|
||||||
@ -666,6 +803,7 @@ func (h *handler) GetLivestreamCommitmentSummary() core.HandlerFunc {
|
|||||||
HasSeed: summary.HasSeed,
|
HasSeed: summary.HasSeed,
|
||||||
LenSeed: summary.LenSeed,
|
LenSeed: summary.LenSeed,
|
||||||
LenHash: summary.LenHash,
|
LenHash: summary.LenHash,
|
||||||
|
SeedHashHex: summary.SeedHashHex,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,16 +8,29 @@ import (
|
|||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type livestreamStatsResponse struct {
|
type dailyLivestreamStats struct {
|
||||||
TotalRevenue int64 `json:"total_revenue"` // 总营收(分)
|
Date string `json:"date"` // 日期
|
||||||
TotalRefund int64 `json:"total_refund"` // 总退款(分)
|
TotalRevenue int64 `json:"total_revenue"` // 营收
|
||||||
TotalCost int64 `json:"total_cost"` // 总成本(分)
|
TotalRefund int64 `json:"total_refund"` // 退款
|
||||||
NetProfit int64 `json:"net_profit"` // 净利润(分)
|
TotalCost int64 `json:"total_cost"` // 成本
|
||||||
|
NetProfit int64 `json:"net_profit"` // 净利润
|
||||||
|
ProfitMargin float64 `json:"profit_margin"` // 利润率
|
||||||
OrderCount int64 `json:"order_count"` // 订单数
|
OrderCount int64 `json:"order_count"` // 订单数
|
||||||
RefundCount int64 `json:"refund_count"` // 退款数
|
RefundCount int64 `json:"refund_count"` // 退款单数
|
||||||
ProfitMargin float64 `json:"profit_margin"` // 利润率 %
|
}
|
||||||
|
|
||||||
|
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 获取直播间盈亏统计
|
// GetLivestreamStats 获取直播间盈亏统计
|
||||||
@ -33,38 +46,124 @@ type livestreamStatsResponse struct {
|
|||||||
// @Security LoginVerifyToken
|
// @Security LoginVerifyToken
|
||||||
func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
||||||
return func(ctx core.Context) {
|
return func(ctx core.Context) {
|
||||||
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
||||||
if err != nil || activityID <= 0 {
|
if err != nil || id <= 0 {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
||||||
return
|
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. 获取活动信息(门票价格)
|
// 1. 获取活动信息(门票价格)
|
||||||
var activity model.LivestreamActivities
|
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, "活动不存在"))
|
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "活动不存在"))
|
||||||
return
|
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
|
var drawLogs []model.LivestreamDrawLogs
|
||||||
if err := h.repo.GetDbR().Where("activity_id = ?", activityID).Find(&drawLogs).Error; err != nil {
|
db := h.repo.GetDbR().Where("activity_id = ?", id)
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
if startTime != nil {
|
||||||
return
|
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))
|
// 4. 计算成本(只统计未退款订单的奖品成本)
|
||||||
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. 计算成本
|
|
||||||
prizeIDCountMap := make(map[int64]int64)
|
prizeIDCountMap := make(map[int64]int64)
|
||||||
for _, log := range drawLogs {
|
for _, log := range drawLogs {
|
||||||
|
// 排除已退款的订单 (检查 douyin_orders 状态)
|
||||||
|
if refundedShopOrderIDs[log.ShopOrderID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
prizeIDCountMap[log.PrizeID]++
|
prizeIDCountMap[log.PrizeID]++
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,11 +173,11 @@ func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var totalCost int64
|
var totalCost int64
|
||||||
|
prizeCostMap := make(map[int64]int64)
|
||||||
if len(prizeIDs) > 0 {
|
if len(prizeIDs) > 0 {
|
||||||
var prizes []model.LivestreamPrizes
|
var prizes []model.LivestreamPrizes
|
||||||
h.repo.GetDbR().Where("id IN ?", prizeIDs).Find(&prizes)
|
h.repo.GetDbR().Where("id IN ?", prizeIDs).Find(&prizes)
|
||||||
|
|
||||||
prizeCostMap := make(map[int64]int64)
|
|
||||||
productIDsNeedingFallback := make([]int64, 0)
|
productIDsNeedingFallback := make([]int64, 0)
|
||||||
prizeProductMap := make(map[int64]int64)
|
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
|
var margin float64
|
||||||
netRevenue := totalRevenue - totalRefund
|
netRevenue := totalRevenue - totalRefund
|
||||||
if netRevenue > 0 {
|
if netRevenue > 0 {
|
||||||
margin = float64(netProfit) / float64(netRevenue) * 100
|
margin = float64(netProfit) / float64(netRevenue) * 100
|
||||||
} else {
|
} else if netRevenue == 0 && totalCost > 0 {
|
||||||
margin = -100
|
margin = -100
|
||||||
|
} else {
|
||||||
|
margin = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Payload(&livestreamStatsResponse{
|
ctx.Payload(&livestreamStatsResponse{
|
||||||
@ -132,6 +327,7 @@ func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
|||||||
OrderCount: orderCount,
|
OrderCount: orderCount,
|
||||||
RefundCount: refundCount,
|
RefundCount: refundCount,
|
||||||
ProfitMargin: math.Trunc(margin*100) / 100,
|
ProfitMargin: math.Trunc(margin*100) / 100,
|
||||||
|
Daily: dailyList,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ type listUsersRequest struct {
|
|||||||
InviteCode string `form:"inviteCode"`
|
InviteCode string `form:"inviteCode"`
|
||||||
StartDate string `form:"startDate"`
|
StartDate string `form:"startDate"`
|
||||||
EndDate string `form:"endDate"`
|
EndDate string `form:"endDate"`
|
||||||
ID *int64 `form:"id"`
|
ID string `form:"id"`
|
||||||
}
|
}
|
||||||
type listUsersResponse struct {
|
type listUsersResponse struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
@ -74,8 +74,10 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 应用搜索条件
|
// 应用搜索条件
|
||||||
if req.ID != nil {
|
if req.ID != "" {
|
||||||
q = q.Where(h.readDB.Users.ID.Eq(*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 != "" {
|
if req.Nickname != "" {
|
||||||
q = q.Where(h.readDB.Users.Nickname.Like("%" + 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")).
|
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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)).
|
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
||||||
Group(h.readDB.Orders.UserID).
|
Group(h.readDB.Orders.UserID).
|
||||||
Scan(&todayRes)
|
Scan(&todayRes)
|
||||||
@ -209,6 +212,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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)).
|
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
||||||
Group(h.readDB.Orders.UserID).
|
Group(h.readDB.Orders.UserID).
|
||||||
Scan(&sevenRes)
|
Scan(&sevenRes)
|
||||||
@ -222,6 +226,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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)).
|
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
||||||
Group(h.readDB.Orders.UserID).
|
Group(h.readDB.Orders.UserID).
|
||||||
Scan(&thirtyRes)
|
Scan(&thirtyRes)
|
||||||
@ -235,6 +240,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
Where(h.readDB.Orders.Status.Eq(2)).
|
||||||
|
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||||
Group(h.readDB.Orders.UserID).
|
Group(h.readDB.Orders.UserID).
|
||||||
Scan(&totalRes)
|
Scan(&totalRes)
|
||||||
for _, r := range totalRes {
|
for _, r := range totalRes {
|
||||||
@ -385,8 +391,9 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
gpCount := gamePassCounts[v.ID]
|
gpCount := gamePassCounts[v.ID]
|
||||||
gtCount := gameTicketCounts[v.ID]
|
gtCount := gameTicketCounts[v.ID]
|
||||||
|
|
||||||
// 总资产估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次) + 游戏资格(1元/场)
|
// 总资产估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次)
|
||||||
assetVal := pointsBal*100 + invVal + cpVal + icVal + gpCount*200 + gtCount*100
|
// 游戏资格不计入估值(购买其他商品赠送,无实际价值)
|
||||||
|
assetVal := pointsBal*100 + invVal + cpVal + icVal + gpCount*200
|
||||||
|
|
||||||
rsp.List[i] = adminUserItem{
|
rsp.List[i] = adminUserItem{
|
||||||
ID: v.ID,
|
ID: v.ID,
|
||||||
@ -397,6 +404,8 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
InviterNickname: inviterNicknames[v.InviterID],
|
InviterNickname: inviterNicknames[v.InviterID],
|
||||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
DouyinID: v.DouyinID,
|
DouyinID: v.DouyinID,
|
||||||
|
DouyinUserID: v.DouyinUserID,
|
||||||
|
Remark: v.Remark,
|
||||||
ChannelName: v.ChannelName,
|
ChannelName: v.ChannelName,
|
||||||
ChannelCode: v.ChannelCode,
|
ChannelCode: v.ChannelCode,
|
||||||
PointsBalance: pointsBal,
|
PointsBalance: pointsBal,
|
||||||
@ -411,6 +420,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
GameTicketCount: gtCount,
|
GameTicketCount: gtCount,
|
||||||
InventoryValue: invVal,
|
InventoryValue: invVal,
|
||||||
TotalAssetValue: assetVal,
|
TotalAssetValue: assetVal,
|
||||||
|
Status: v.Status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
@ -485,6 +495,7 @@ type listInventoryRequest struct {
|
|||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
PageSize int `form:"page_size"`
|
PageSize int `form:"page_size"`
|
||||||
Keyword string `form:"keyword"` // 搜索关键词(商品名称)
|
Keyword string `form:"keyword"` // 搜索关键词(商品名称)
|
||||||
|
Status int32 `form:"status"` // 状态筛选:0=全部, 1=持有, 2=作废, 3=已使用
|
||||||
}
|
}
|
||||||
type listInventoryResponse struct {
|
type listInventoryResponse struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
@ -541,6 +552,8 @@ func (h *handler) ListUserOrders() core.HandlerFunc {
|
|||||||
// @Param user_id path integer true "用户ID"
|
// @Param user_id path integer true "用户ID"
|
||||||
// @Param page query int true "页码" default(1)
|
// @Param page query int true "页码" default(1)
|
||||||
// @Param page_size query int true "每页数量,最多100" default(20)
|
// @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
|
// @Success 200 {object} listInventoryResponse
|
||||||
// @Failure 400 {object} code.Failure
|
// @Failure 400 {object} code.Failure
|
||||||
// @Router /api/admin/users/{user_id}/inventory [get]
|
// @Router /api/admin/users/{user_id}/inventory [get]
|
||||||
@ -576,11 +589,34 @@ func (h *handler) ListUserInventory() core.HandlerFunc {
|
|||||||
ui := h.readDB.UserInventory
|
ui := h.readDB.UserInventory
|
||||||
p := h.readDB.Products
|
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().
|
countQ := h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
LeftJoin(p, p.ID.EqCol(ui.ProductID)).
|
LeftJoin(p, p.ID.EqCol(ui.ProductID)).
|
||||||
Where(ui.UserID.Eq(userID)).
|
Where(ui.UserID.Eq(userID))
|
||||||
Where(p.Name.Like("%" + req.Keyword + "%"))
|
|
||||||
|
// 应用状态筛选
|
||||||
|
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()
|
total, err := countQ.Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
||||||
@ -604,16 +640,35 @@ func (h *handler) ListUserInventory() core.HandlerFunc {
|
|||||||
ProductPrice int64
|
ProductPrice int64
|
||||||
}
|
}
|
||||||
var rows []inventoryRow
|
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,
|
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,
|
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
|
p.name as product_name, p.images_json as product_images, p.price as product_price
|
||||||
FROM user_inventory ui
|
FROM user_inventory ui
|
||||||
LEFT JOIN products p ON p.id = ui.product_id
|
LEFT JOIN products p ON p.id = ui.product_id
|
||||||
WHERE ui.user_id = ? AND p.name LIKE ?
|
WHERE ui.user_id = ?
|
||||||
ORDER BY ui.id DESC
|
`
|
||||||
LIMIT ? OFFSET ?
|
var args []interface{}
|
||||||
`, userID, "%"+req.Keyword+"%", req.PageSize, (req.Page-1)*req.PageSize).Scan(&rows).Error
|
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 {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
||||||
return
|
return
|
||||||
@ -1085,6 +1140,8 @@ type adminUserItem struct {
|
|||||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
DouyinID string `json:"douyin_id"`
|
DouyinID string `json:"douyin_id"`
|
||||||
|
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID
|
||||||
|
Remark string `json:"remark"` // 备注
|
||||||
ChannelName string `json:"channel_name"`
|
ChannelName string `json:"channel_name"`
|
||||||
ChannelCode string `json:"channel_code"`
|
ChannelCode string `json:"channel_code"`
|
||||||
PointsBalance int64 `json:"points_balance"`
|
PointsBalance int64 `json:"points_balance"`
|
||||||
@ -1099,6 +1156,7 @@ type adminUserItem struct {
|
|||||||
GameTicketCount int64 `json:"game_ticket_count"` // 游戏资格数量
|
GameTicketCount int64 `json:"game_ticket_count"` // 游戏资格数量
|
||||||
InventoryValue int64 `json:"inventory_value"` // 持有商品总价值
|
InventoryValue int64 `json:"inventory_value"` // 持有商品总价值
|
||||||
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
||||||
|
Status int32 `json:"status"` // 用户状态:1正常 2禁用 3黑名单
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListAppUsers 管理端用户列表GetUserPointsBalance 查看用户积分余额
|
// ListAppUsers 管理端用户列表GetUserPointsBalance 查看用户积分余额
|
||||||
@ -1496,3 +1554,145 @@ func (h *handler) ListUserCouponUsage() core.HandlerFunc {
|
|||||||
ctx.Payload(rsp)
|
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"`
|
ChannelID int64 `json:"channel_id"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
DouyinID string `json:"douyin_id"`
|
DouyinID string `json:"douyin_id"`
|
||||||
|
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID
|
||||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
||||||
|
|
||||||
// 邀请统计
|
// 邀请统计
|
||||||
@ -88,6 +89,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
rsp.InviterID = user.InviterID
|
rsp.InviterID = user.InviterID
|
||||||
rsp.ChannelID = user.ChannelID
|
rsp.ChannelID = user.ChannelID
|
||||||
rsp.DouyinID = user.DouyinID
|
rsp.DouyinID = user.DouyinID
|
||||||
|
rsp.DouyinUserID = user.DouyinUserID
|
||||||
rsp.CreatedAt = user.CreatedAt.Format(time.RFC3339)
|
rsp.CreatedAt = user.CreatedAt.Format(time.RFC3339)
|
||||||
|
|
||||||
// 1.1 查询邀请人昵称
|
// 1.1 查询邀请人昵称
|
||||||
@ -123,7 +125,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
).
|
).
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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)
|
Scan(&os)
|
||||||
|
|
||||||
// 分阶段统计
|
// 分阶段统计
|
||||||
@ -131,7 +133,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
Select(h.readDB.Orders.ActualAmount.Sum().As("today_paid")).
|
Select(h.readDB.Orders.ActualAmount.Sum().As("today_paid")).
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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)).
|
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
||||||
Scan(&os.TodayPaid)
|
Scan(&os.TodayPaid)
|
||||||
|
|
||||||
@ -139,7 +141,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
Select(h.readDB.Orders.ActualAmount.Sum().As("seven_day_paid")).
|
Select(h.readDB.Orders.ActualAmount.Sum().As("seven_day_paid")).
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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)).
|
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
||||||
Scan(&os.SevenDayPaid)
|
Scan(&os.SevenDayPaid)
|
||||||
|
|
||||||
@ -147,7 +149,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
Select(h.readDB.Orders.ActualAmount.Sum().As("thirty_day_paid")).
|
Select(h.readDB.Orders.ActualAmount.Sum().As("thirty_day_paid")).
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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)).
|
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
||||||
Scan(&os.ThirtyDayPaid)
|
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
|
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(available), 0) FROM user_game_tickets WHERE user_id = ?", userID).Scan(&rsp.CurrentAssets.GameTicketCount).Error
|
||||||
|
|
||||||
// 4.5 总资产估值
|
// 4.5 总资产估值
|
||||||
// 估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次) + 游戏资格(1元/场)
|
// 估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次)
|
||||||
gamePassValue := rsp.CurrentAssets.GamePassCount * 200 // 估值:2元/次
|
// 游戏资格不计入估值(购买其他商品赠送,无实际价值)
|
||||||
gameTicketValue := rsp.CurrentAssets.GameTicketCount * 100 // 估值:1元/场
|
gamePassValue := rsp.CurrentAssets.GamePassCount * 200 // 估值:2元/次
|
||||||
|
gameTicketValue := int64(0) // 游戏资格不计入估值
|
||||||
|
|
||||||
rsp.CurrentAssets.TotalAssetValue = rsp.CurrentAssets.PointsBalance +
|
rsp.CurrentAssets.TotalAssetValue = rsp.CurrentAssets.PointsBalance +
|
||||||
rsp.CurrentAssets.InventoryValue +
|
rsp.CurrentAssets.InventoryValue +
|
||||||
|
|||||||
@ -99,6 +99,7 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
|||||||
Select(h.readDB.Orders.ActualAmount.Sum()).
|
Select(h.readDB.Orders.ActualAmount.Sum()).
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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)).
|
Where(h.readDB.Orders.CreatedAt.Lt(start)).
|
||||||
Scan(&baseCostPtr)
|
Scan(&baseCostPtr)
|
||||||
if baseCostPtr != nil {
|
if baseCostPtr != nil {
|
||||||
@ -121,6 +122,7 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
|||||||
orderRows, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
orderRows, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
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.Gte(start)).
|
||||||
Where(h.readDB.Orders.CreatedAt.Lte(end)).
|
Where(h.readDB.Orders.CreatedAt.Lte(end)).
|
||||||
Find()
|
Find()
|
||||||
@ -195,6 +197,7 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
|||||||
Select(h.readDB.Orders.ActualAmount.Sum()).
|
Select(h.readDB.Orders.ActualAmount.Sum()).
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
Where(h.readDB.Orders.Status.Eq(2)).
|
||||||
|
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
||||||
Scan(&totalCostPtr)
|
Scan(&totalCostPtr)
|
||||||
if totalCostPtr != nil {
|
if totalCostPtr != nil {
|
||||||
totalCost = *totalCostPtr
|
totalCost = *totalCostPtr
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ConfigResponse struct {
|
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)
|
// GetPublicConfig 获取公开配置(包含订阅模板ID)
|
||||||
@ -18,19 +19,30 @@ type ConfigResponse struct {
|
|||||||
// @Router /api/app/config/public [get]
|
// @Router /api/app/config/public [get]
|
||||||
func (h *handler) GetPublicConfig() core.HandlerFunc {
|
func (h *handler) GetPublicConfig() core.HandlerFunc {
|
||||||
return func(ctx core.Context) {
|
return func(ctx core.Context) {
|
||||||
// 查询订阅消息模板 ID
|
// 查询配置
|
||||||
var val string
|
var subscribeTemplateID string
|
||||||
cfg, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).
|
var serviceQRCode string
|
||||||
Where(h.readDB.SystemConfigs.ConfigKey.Eq("wechat.lottery_result_template_id")).
|
|
||||||
First()
|
configs, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).
|
||||||
if err == nil && cfg != nil {
|
Where(h.readDB.SystemConfigs.ConfigKey.In("wechat.lottery_result_template_id", "contact.service_qrcode")).
|
||||||
val = cfg.ConfigValue
|
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{
|
rsp := ConfigResponse{
|
||||||
SubscribeTemplates: map[string]string{
|
SubscribeTemplates: map[string]string{
|
||||||
"lottery_result": val,
|
"lottery_result": subscribeTemplateID,
|
||||||
},
|
},
|
||||||
|
ContactServiceQRCode: serviceQRCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"go.uber.org/zap"
|
"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
|
remaining := 0
|
||||||
if ticket != nil {
|
if req.GameCode == "minesweeper_free" {
|
||||||
remaining = int(ticket.Available)
|
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服务器信息
|
// 从系统配置读取Nakama服务器信息
|
||||||
@ -312,8 +317,21 @@ func (h *handler) VerifyTicket() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从Redis验证token
|
// 从Redis验证token
|
||||||
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
storedValue, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
||||||
if err != nil || storedUserID != req.UserID {
|
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})
|
ctx.Payload(&verifyResponse{Valid: false})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -358,16 +376,36 @@ func (h *handler) SettleGame() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证token(可选,如果游戏服务器传了ticket则验证,否则信任internal调用)
|
// 验证token(可选,如果游戏服务器传了ticket则验证,否则信任internal调用)
|
||||||
|
isFreeMode := false
|
||||||
if req.Ticket != "" {
|
if req.Ticket != "" {
|
||||||
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
storedValue, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
||||||
if err != nil || storedUserID != req.UserID {
|
if err != nil {
|
||||||
h.logger.Warn("Ticket validation failed, but proceeding with internal trust",
|
h.logger.Warn("Ticket validation failed (not found)", zap.String("ticket", req.Ticket))
|
||||||
zap.String("ticket", req.Ticket), zap.String("user_id", req.UserID))
|
|
||||||
} else {
|
} else {
|
||||||
// 删除token防止重复使用
|
// Parse "userID:gameType"
|
||||||
h.redis.Del(ctx.RequestContext(), "game:token:ticket:"+req.Ticket)
|
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
|
// 注意:即使ticket验证失败,作为internal API我们仍然信任游戏服务器传来的UserID
|
||||||
|
|
||||||
// 奖品发放逻辑
|
// 奖品发放逻辑
|
||||||
@ -407,6 +445,9 @@ func (h *handler) SettleGame() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 发放奖励
|
// 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 {
|
if targetProductID > 0 {
|
||||||
res, err := h.userSvc.GrantReward(ctx.RequestContext(), uid, usersvc.GrantRewardRequest{
|
res, err := h.userSvc.GrantReward(ctx.RequestContext(), uid, usersvc.GrantRewardRequest{
|
||||||
ProductID: targetProductID,
|
ProductID: targetProductID,
|
||||||
@ -468,11 +509,16 @@ func (h *handler) ConsumeTicket() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 扣减游戏次数
|
// 扣减游戏次数
|
||||||
err := h.ticketSvc.UseTicket(ctx.RequestContext(), uid, gameCode)
|
if gameCode == "minesweeper_free" {
|
||||||
if err != nil {
|
// 免费场场不扣减次数,直接通过
|
||||||
h.logger.Error("Failed to consume ticket", zap.Int64("user_id", uid), zap.String("game_code", gameCode), zap.Error(err))
|
h.logger.Info("Free mode consume ticket skipped deduction", zap.Int64("user_id", uid))
|
||||||
ctx.Payload(&consumeTicketResponse{Success: false, Error: err.Error()})
|
} else {
|
||||||
return
|
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 失效(防止重复扣减)
|
// 使 ticket 失效(防止重复扣减)
|
||||||
|
|||||||
@ -306,6 +306,11 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
wxConfig = &wechat.WechatConfig{AppID: cfg.AppID, AppSecret: cfg.AppSecret}
|
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" {
|
if ord.SourceType == 2 && act != nil && act.DrawMode == "instant" {
|
||||||
_ = h.activity.ProcessOrderLottery(bgCtx, ord.ID)
|
_ = h.activity.ProcessOrderLottery(bgCtx, ord.ID)
|
||||||
} else if ord.SourceType == 4 {
|
} else if ord.SourceType == 4 {
|
||||||
|
|||||||
@ -298,6 +298,11 @@ func (h *handler) DrawLivestream() core.HandlerFunc {
|
|||||||
UserNickname: order.UserNickname,
|
UserNickname: order.UserNickname,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10007, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -344,14 +349,8 @@ func (h *handler) SyncLivestreamOrders() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
|
// 调用服务执行全量扫描 (基于时间更新,覆盖最近1小时变化)
|
||||||
if err != nil {
|
result, err := h.douyin.SyncAllOrders(ctx.RequestContext(), 1*time.Hour)
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用服务执行全量扫描 (此时已过滤 status=2)
|
|
||||||
result, err := h.douyin.SyncShopOrders(ctx.RequestContext(), activity.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10004, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10004, err.Error()))
|
||||||
return
|
return
|
||||||
@ -376,20 +375,14 @@ func (h *handler) GetLivestreamPendingOrders() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
|
// [核心优化] 自动同步:每次拉取待抽奖列表前,静默执行一次快速全局扫描 (最近 10 分钟)
|
||||||
if err != nil {
|
_, _ = h.douyin.SyncAllOrders(ctx.RequestContext(), 10*time.Minute)
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// [核心优化] 自动同步:每次拉取待抽奖列表前,静默执行一次全店扫描
|
|
||||||
_, _ = h.douyin.SyncShopOrders(ctx.RequestContext(), activity.ID)
|
|
||||||
|
|
||||||
// 查询全店范围内所有待抽奖记录 (Status 2 且未核销完: reward_granted < product_count)
|
// 查询全店范围内所有待抽奖记录 (Status 2 且未核销完: reward_granted < product_count)
|
||||||
var pendingOrders []model.DouyinOrders
|
var pendingOrders []model.DouyinOrders
|
||||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
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
|
Find(&pendingOrders).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -397,6 +390,40 @@ func (h *handler) GetLivestreamPendingOrders() core.HandlerFunc {
|
|||||||
return
|
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"`
|
ValidStart string `json:"valid_start"`
|
||||||
ValidEnd string `json:"valid_end"`
|
ValidEnd string `json:"valid_end"`
|
||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
|
StatusDesc string `json:"status_desc"` // 状态描述:未使用、已用完、已过期
|
||||||
Rules string `json:"rules"`
|
Rules string `json:"rules"`
|
||||||
UsedAt string `json:"used_at,omitempty"` // 使用时间(已使用时返回)
|
UsedAt string `json:"used_at,omitempty"` // 使用时间(已使用时返回)
|
||||||
|
UsedAmount int64 `json:"used_amount"` // 已使用金额
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUserCoupons 查看用户优惠券
|
// ListUserCoupons 查看用户优惠券
|
||||||
@ -58,13 +60,13 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
userID := int64(ctx.SessionUserInfo().Id)
|
userID := int64(ctx.SessionUserInfo().Id)
|
||||||
|
|
||||||
// 默认查询未使用的优惠券
|
// 状态:0未使用 1已使用 2已过期 (直接对接前端标准)
|
||||||
status := int32(1)
|
status := int32(0)
|
||||||
if req.Status != nil && *req.Status > 0 {
|
if req.Status != nil {
|
||||||
status = *req.Status
|
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 {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error()))
|
||||||
return
|
return
|
||||||
@ -100,14 +102,8 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
|||||||
rules := ""
|
rules := ""
|
||||||
if sc != nil {
|
if sc != nil {
|
||||||
name = sc.Name
|
name = sc.Name
|
||||||
// 金额券:amount 显示模板面值,remaining 显示当前余额
|
amount = sc.DiscountValue
|
||||||
if sc.DiscountType == 1 {
|
remaining = it.BalanceAmount
|
||||||
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
|
|
||||||
}
|
|
||||||
rules = buildCouponRules(sc)
|
rules = buildCouponRules(sc)
|
||||||
}
|
}
|
||||||
vs := it.ValidStart.Format("2006-01-02 15:04:05")
|
vs := it.ValidStart.Format("2006-01-02 15:04:05")
|
||||||
@ -119,7 +115,24 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
|||||||
if !it.UsedAt.IsZero() {
|
if !it.UsedAt.IsZero() {
|
||||||
usedAt = it.UsedAt.Format("2006-01-02 15:04:05")
|
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)
|
rsp.List = append(rsp.List, vi)
|
||||||
}
|
}
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
|
|||||||
@ -15,17 +15,19 @@ type Failure struct {
|
|||||||
Message string `json:"message"` // 描述信息
|
Message string `json:"message"` // 描述信息
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ServerError = 10101
|
ServerError = 10101
|
||||||
ParamBindError = 10102
|
ParamBindError = 10102
|
||||||
JWTAuthVerifyError = 10103
|
JWTAuthVerifyError = 10103
|
||||||
UploadError = 10104
|
UploadError = 10104
|
||||||
|
ForbiddenError = 10105
|
||||||
|
AuthorizationError = 10106
|
||||||
|
|
||||||
AdminLoginError = 20101
|
AdminLoginError = 20101
|
||||||
CreateAdminError = 20207
|
CreateAdminError = 20207
|
||||||
ListAdminError = 20208
|
ListAdminError = 20208
|
||||||
ModifyAdminError = 20209
|
ModifyAdminError = 20209
|
||||||
DeleteAdminError = 20210
|
DeleteAdminError = 20210
|
||||||
)
|
)
|
||||||
|
|
||||||
func Text(code int) string {
|
func Text(code int) string {
|
||||||
|
|||||||
@ -65,6 +65,18 @@ func uploadVirtualShippingInternal(ctx core.Context, accessToken string, key ord
|
|||||||
if itemDesc == "" {
|
if itemDesc == "" {
|
||||||
return fmt.Errorf("参数缺失")
|
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{
|
reqBody := &uploadShippingInfoRequest{
|
||||||
OrderKey: key,
|
OrderKey: key,
|
||||||
LogisticsType: 3,
|
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())
|
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)
|
// uploadVirtualShippingInternalBackground 后台虚拟发货内部实现(无 core.Context)
|
||||||
func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken string, key orderKey, payerOpenid string, itemDesc string, uploadTime time.Time) error {
|
func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken string, key orderKey, payerOpenid string, itemDesc string, uploadTime time.Time) error {
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
@ -249,6 +311,22 @@ func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken st
|
|||||||
if itemDesc == "" {
|
if itemDesc == "" {
|
||||||
return fmt.Errorf("参数缺失")
|
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{
|
reqBody := &uploadShippingInfoRequest{
|
||||||
OrderKey: key,
|
OrderKey: key,
|
||||||
LogisticsType: 3,
|
LogisticsType: 3,
|
||||||
@ -275,6 +353,11 @@ func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken st
|
|||||||
return fmt.Errorf("解析响应失败: %v", err)
|
return fmt.Errorf("解析响应失败: %v", err)
|
||||||
}
|
}
|
||||||
if cr.ErrCode != 0 {
|
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 fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", cr.ErrCode, cr.ErrMsg)
|
||||||
}
|
}
|
||||||
return nil
|
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.ALL = field.NewAsterisk(tableName)
|
||||||
_douyinOrders.ID = field.NewInt64(tableName, "id")
|
_douyinOrders.ID = field.NewInt64(tableName, "id")
|
||||||
_douyinOrders.ShopOrderID = field.NewString(tableName, "shop_order_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.OrderStatus = field.NewInt32(tableName, "order_status")
|
||||||
_douyinOrders.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
_douyinOrders.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
||||||
_douyinOrders.LocalUserID = field.NewString(tableName, "local_user_id")
|
_douyinOrders.LocalUserID = field.NewString(tableName, "local_user_id")
|
||||||
_douyinOrders.ActualReceiveAmount = field.NewInt64(tableName, "actual_receive_amount")
|
_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.PayTypeDesc = field.NewString(tableName, "pay_type_desc")
|
||||||
_douyinOrders.Remark = field.NewString(tableName, "remark")
|
_douyinOrders.Remark = field.NewString(tableName, "remark")
|
||||||
_douyinOrders.UserNickname = field.NewString(tableName, "user_nickname")
|
_douyinOrders.UserNickname = field.NewString(tableName, "user_nickname")
|
||||||
_douyinOrders.RawData = field.NewString(tableName, "raw_data")
|
_douyinOrders.RawData = field.NewString(tableName, "raw_data")
|
||||||
_douyinOrders.CreatedAt = field.NewTime(tableName, "created_at")
|
_douyinOrders.CreatedAt = field.NewTime(tableName, "created_at")
|
||||||
_douyinOrders.UpdatedAt = field.NewTime(tableName, "updated_at")
|
_douyinOrders.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||||
|
_douyinOrders.RewardGranted = field.NewBool(tableName, "reward_granted")
|
||||||
|
_douyinOrders.ProductCount = field.NewInt32(tableName, "product_count")
|
||||||
|
|
||||||
_douyinOrders.fillFieldMap()
|
_douyinOrders.fillFieldMap()
|
||||||
|
|
||||||
@ -52,16 +56,20 @@ type douyinOrders struct {
|
|||||||
ALL field.Asterisk
|
ALL field.Asterisk
|
||||||
ID field.Int64
|
ID field.Int64
|
||||||
ShopOrderID field.String // 抖店订单号
|
ShopOrderID field.String // 抖店订单号
|
||||||
|
DouyinProductID field.String // 关联商品ID
|
||||||
OrderStatus field.Int32 // 订单状态: 5=已完成
|
OrderStatus field.Int32 // 订单状态: 5=已完成
|
||||||
DouyinUserID field.String // 抖店用户ID
|
DouyinUserID field.String // 抖店用户ID
|
||||||
LocalUserID field.String // 匹配到的本地用户ID
|
LocalUserID field.String // 匹配到的本地用户ID
|
||||||
ActualReceiveAmount field.Int64 // 实收金额(分)
|
ActualReceiveAmount field.Int64 // 实收金额(分)
|
||||||
|
ActualPayAmount field.Int64 // 实付金额(分)
|
||||||
PayTypeDesc field.String // 支付方式描述
|
PayTypeDesc field.String // 支付方式描述
|
||||||
Remark field.String // 备注
|
Remark field.String // 备注
|
||||||
UserNickname field.String // 抖音昵称
|
UserNickname field.String // 抖音昵称
|
||||||
RawData field.String // 原始响应数据
|
RawData field.String // 原始响应数据
|
||||||
CreatedAt field.Time
|
CreatedAt field.Time
|
||||||
UpdatedAt field.Time
|
UpdatedAt field.Time
|
||||||
|
RewardGranted field.Bool // 奖励已发放: 0=否, 1=是
|
||||||
|
ProductCount field.Int32 // 商品数量
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@ -80,16 +88,20 @@ func (d *douyinOrders) updateTableName(table string) *douyinOrders {
|
|||||||
d.ALL = field.NewAsterisk(table)
|
d.ALL = field.NewAsterisk(table)
|
||||||
d.ID = field.NewInt64(table, "id")
|
d.ID = field.NewInt64(table, "id")
|
||||||
d.ShopOrderID = field.NewString(table, "shop_order_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.OrderStatus = field.NewInt32(table, "order_status")
|
||||||
d.DouyinUserID = field.NewString(table, "douyin_user_id")
|
d.DouyinUserID = field.NewString(table, "douyin_user_id")
|
||||||
d.LocalUserID = field.NewString(table, "local_user_id")
|
d.LocalUserID = field.NewString(table, "local_user_id")
|
||||||
d.ActualReceiveAmount = field.NewInt64(table, "actual_receive_amount")
|
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.PayTypeDesc = field.NewString(table, "pay_type_desc")
|
||||||
d.Remark = field.NewString(table, "remark")
|
d.Remark = field.NewString(table, "remark")
|
||||||
d.UserNickname = field.NewString(table, "user_nickname")
|
d.UserNickname = field.NewString(table, "user_nickname")
|
||||||
d.RawData = field.NewString(table, "raw_data")
|
d.RawData = field.NewString(table, "raw_data")
|
||||||
d.CreatedAt = field.NewTime(table, "created_at")
|
d.CreatedAt = field.NewTime(table, "created_at")
|
||||||
d.UpdatedAt = field.NewTime(table, "updated_at")
|
d.UpdatedAt = field.NewTime(table, "updated_at")
|
||||||
|
d.RewardGranted = field.NewBool(table, "reward_granted")
|
||||||
|
d.ProductCount = field.NewInt32(table, "product_count")
|
||||||
|
|
||||||
d.fillFieldMap()
|
d.fillFieldMap()
|
||||||
|
|
||||||
@ -106,19 +118,23 @@ func (d *douyinOrders) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *douyinOrders) fillFieldMap() {
|
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["id"] = d.ID
|
||||||
d.fieldMap["shop_order_id"] = d.ShopOrderID
|
d.fieldMap["shop_order_id"] = d.ShopOrderID
|
||||||
|
d.fieldMap["douyin_product_id"] = d.DouyinProductID
|
||||||
d.fieldMap["order_status"] = d.OrderStatus
|
d.fieldMap["order_status"] = d.OrderStatus
|
||||||
d.fieldMap["douyin_user_id"] = d.DouyinUserID
|
d.fieldMap["douyin_user_id"] = d.DouyinUserID
|
||||||
d.fieldMap["local_user_id"] = d.LocalUserID
|
d.fieldMap["local_user_id"] = d.LocalUserID
|
||||||
d.fieldMap["actual_receive_amount"] = d.ActualReceiveAmount
|
d.fieldMap["actual_receive_amount"] = d.ActualReceiveAmount
|
||||||
|
d.fieldMap["actual_pay_amount"] = d.ActualPayAmount
|
||||||
d.fieldMap["pay_type_desc"] = d.PayTypeDesc
|
d.fieldMap["pay_type_desc"] = d.PayTypeDesc
|
||||||
d.fieldMap["remark"] = d.Remark
|
d.fieldMap["remark"] = d.Remark
|
||||||
d.fieldMap["user_nickname"] = d.UserNickname
|
d.fieldMap["user_nickname"] = d.UserNickname
|
||||||
d.fieldMap["raw_data"] = d.RawData
|
d.fieldMap["raw_data"] = d.RawData
|
||||||
d.fieldMap["created_at"] = d.CreatedAt
|
d.fieldMap["created_at"] = d.CreatedAt
|
||||||
d.fieldMap["updated_at"] = d.UpdatedAt
|
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 {
|
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
|
AuditRollbackLogs *auditRollbackLogs
|
||||||
Banner *banner
|
Banner *banner
|
||||||
Channels *channels
|
Channels *channels
|
||||||
|
DouyinBlacklist *douyinBlacklist
|
||||||
|
DouyinOrders *douyinOrders
|
||||||
|
DouyinProductRewards *douyinProductRewards
|
||||||
GamePassPackages *gamePassPackages
|
GamePassPackages *gamePassPackages
|
||||||
GameTicketLogs *gameTicketLogs
|
GameTicketLogs *gameTicketLogs
|
||||||
IssuePositionClaims *issuePositionClaims
|
IssuePositionClaims *issuePositionClaims
|
||||||
@ -36,6 +39,7 @@ var (
|
|||||||
LivestreamPrizes *livestreamPrizes
|
LivestreamPrizes *livestreamPrizes
|
||||||
LogOperation *logOperation
|
LogOperation *logOperation
|
||||||
LogRequest *logRequest
|
LogRequest *logRequest
|
||||||
|
LotteryRefundLogs *lotteryRefundLogs
|
||||||
MatchingCardTypes *matchingCardTypes
|
MatchingCardTypes *matchingCardTypes
|
||||||
MenuActions *menuActions
|
MenuActions *menuActions
|
||||||
Menus *menus
|
Menus *menus
|
||||||
@ -62,9 +66,11 @@ var (
|
|||||||
SystemItemCards *systemItemCards
|
SystemItemCards *systemItemCards
|
||||||
SystemTitleEffects *systemTitleEffects
|
SystemTitleEffects *systemTitleEffects
|
||||||
SystemTitles *systemTitles
|
SystemTitles *systemTitles
|
||||||
|
TaskCenterEventLogs *taskCenterEventLogs
|
||||||
TaskCenterTaskRewards *taskCenterTaskRewards
|
TaskCenterTaskRewards *taskCenterTaskRewards
|
||||||
TaskCenterTaskTiers *taskCenterTaskTiers
|
TaskCenterTaskTiers *taskCenterTaskTiers
|
||||||
TaskCenterTasks *taskCenterTasks
|
TaskCenterTasks *taskCenterTasks
|
||||||
|
TaskCenterUserProgress *taskCenterUserProgress
|
||||||
UserAddresses *userAddresses
|
UserAddresses *userAddresses
|
||||||
UserCouponLedger *userCouponLedger
|
UserCouponLedger *userCouponLedger
|
||||||
UserCoupons *userCoupons
|
UserCoupons *userCoupons
|
||||||
@ -94,6 +100,9 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
|||||||
AuditRollbackLogs = &Q.AuditRollbackLogs
|
AuditRollbackLogs = &Q.AuditRollbackLogs
|
||||||
Banner = &Q.Banner
|
Banner = &Q.Banner
|
||||||
Channels = &Q.Channels
|
Channels = &Q.Channels
|
||||||
|
DouyinBlacklist = &Q.DouyinBlacklist
|
||||||
|
DouyinOrders = &Q.DouyinOrders
|
||||||
|
DouyinProductRewards = &Q.DouyinProductRewards
|
||||||
GamePassPackages = &Q.GamePassPackages
|
GamePassPackages = &Q.GamePassPackages
|
||||||
GameTicketLogs = &Q.GameTicketLogs
|
GameTicketLogs = &Q.GameTicketLogs
|
||||||
IssuePositionClaims = &Q.IssuePositionClaims
|
IssuePositionClaims = &Q.IssuePositionClaims
|
||||||
@ -102,6 +111,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
|||||||
LivestreamPrizes = &Q.LivestreamPrizes
|
LivestreamPrizes = &Q.LivestreamPrizes
|
||||||
LogOperation = &Q.LogOperation
|
LogOperation = &Q.LogOperation
|
||||||
LogRequest = &Q.LogRequest
|
LogRequest = &Q.LogRequest
|
||||||
|
LotteryRefundLogs = &Q.LotteryRefundLogs
|
||||||
MatchingCardTypes = &Q.MatchingCardTypes
|
MatchingCardTypes = &Q.MatchingCardTypes
|
||||||
MenuActions = &Q.MenuActions
|
MenuActions = &Q.MenuActions
|
||||||
Menus = &Q.Menus
|
Menus = &Q.Menus
|
||||||
@ -128,9 +138,11 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
|||||||
SystemItemCards = &Q.SystemItemCards
|
SystemItemCards = &Q.SystemItemCards
|
||||||
SystemTitleEffects = &Q.SystemTitleEffects
|
SystemTitleEffects = &Q.SystemTitleEffects
|
||||||
SystemTitles = &Q.SystemTitles
|
SystemTitles = &Q.SystemTitles
|
||||||
|
TaskCenterEventLogs = &Q.TaskCenterEventLogs
|
||||||
TaskCenterTaskRewards = &Q.TaskCenterTaskRewards
|
TaskCenterTaskRewards = &Q.TaskCenterTaskRewards
|
||||||
TaskCenterTaskTiers = &Q.TaskCenterTaskTiers
|
TaskCenterTaskTiers = &Q.TaskCenterTaskTiers
|
||||||
TaskCenterTasks = &Q.TaskCenterTasks
|
TaskCenterTasks = &Q.TaskCenterTasks
|
||||||
|
TaskCenterUserProgress = &Q.TaskCenterUserProgress
|
||||||
UserAddresses = &Q.UserAddresses
|
UserAddresses = &Q.UserAddresses
|
||||||
UserCouponLedger = &Q.UserCouponLedger
|
UserCouponLedger = &Q.UserCouponLedger
|
||||||
UserCoupons = &Q.UserCoupons
|
UserCoupons = &Q.UserCoupons
|
||||||
@ -161,6 +173,9 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
|||||||
AuditRollbackLogs: newAuditRollbackLogs(db, opts...),
|
AuditRollbackLogs: newAuditRollbackLogs(db, opts...),
|
||||||
Banner: newBanner(db, opts...),
|
Banner: newBanner(db, opts...),
|
||||||
Channels: newChannels(db, opts...),
|
Channels: newChannels(db, opts...),
|
||||||
|
DouyinBlacklist: newDouyinBlacklist(db, opts...),
|
||||||
|
DouyinOrders: newDouyinOrders(db, opts...),
|
||||||
|
DouyinProductRewards: newDouyinProductRewards(db, opts...),
|
||||||
GamePassPackages: newGamePassPackages(db, opts...),
|
GamePassPackages: newGamePassPackages(db, opts...),
|
||||||
GameTicketLogs: newGameTicketLogs(db, opts...),
|
GameTicketLogs: newGameTicketLogs(db, opts...),
|
||||||
IssuePositionClaims: newIssuePositionClaims(db, opts...),
|
IssuePositionClaims: newIssuePositionClaims(db, opts...),
|
||||||
@ -169,6 +184,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
|||||||
LivestreamPrizes: newLivestreamPrizes(db, opts...),
|
LivestreamPrizes: newLivestreamPrizes(db, opts...),
|
||||||
LogOperation: newLogOperation(db, opts...),
|
LogOperation: newLogOperation(db, opts...),
|
||||||
LogRequest: newLogRequest(db, opts...),
|
LogRequest: newLogRequest(db, opts...),
|
||||||
|
LotteryRefundLogs: newLotteryRefundLogs(db, opts...),
|
||||||
MatchingCardTypes: newMatchingCardTypes(db, opts...),
|
MatchingCardTypes: newMatchingCardTypes(db, opts...),
|
||||||
MenuActions: newMenuActions(db, opts...),
|
MenuActions: newMenuActions(db, opts...),
|
||||||
Menus: newMenus(db, opts...),
|
Menus: newMenus(db, opts...),
|
||||||
@ -195,9 +211,11 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
|||||||
SystemItemCards: newSystemItemCards(db, opts...),
|
SystemItemCards: newSystemItemCards(db, opts...),
|
||||||
SystemTitleEffects: newSystemTitleEffects(db, opts...),
|
SystemTitleEffects: newSystemTitleEffects(db, opts...),
|
||||||
SystemTitles: newSystemTitles(db, opts...),
|
SystemTitles: newSystemTitles(db, opts...),
|
||||||
|
TaskCenterEventLogs: newTaskCenterEventLogs(db, opts...),
|
||||||
TaskCenterTaskRewards: newTaskCenterTaskRewards(db, opts...),
|
TaskCenterTaskRewards: newTaskCenterTaskRewards(db, opts...),
|
||||||
TaskCenterTaskTiers: newTaskCenterTaskTiers(db, opts...),
|
TaskCenterTaskTiers: newTaskCenterTaskTiers(db, opts...),
|
||||||
TaskCenterTasks: newTaskCenterTasks(db, opts...),
|
TaskCenterTasks: newTaskCenterTasks(db, opts...),
|
||||||
|
TaskCenterUserProgress: newTaskCenterUserProgress(db, opts...),
|
||||||
UserAddresses: newUserAddresses(db, opts...),
|
UserAddresses: newUserAddresses(db, opts...),
|
||||||
UserCouponLedger: newUserCouponLedger(db, opts...),
|
UserCouponLedger: newUserCouponLedger(db, opts...),
|
||||||
UserCoupons: newUserCoupons(db, opts...),
|
UserCoupons: newUserCoupons(db, opts...),
|
||||||
@ -229,6 +247,9 @@ type Query struct {
|
|||||||
AuditRollbackLogs auditRollbackLogs
|
AuditRollbackLogs auditRollbackLogs
|
||||||
Banner banner
|
Banner banner
|
||||||
Channels channels
|
Channels channels
|
||||||
|
DouyinBlacklist douyinBlacklist
|
||||||
|
DouyinOrders douyinOrders
|
||||||
|
DouyinProductRewards douyinProductRewards
|
||||||
GamePassPackages gamePassPackages
|
GamePassPackages gamePassPackages
|
||||||
GameTicketLogs gameTicketLogs
|
GameTicketLogs gameTicketLogs
|
||||||
IssuePositionClaims issuePositionClaims
|
IssuePositionClaims issuePositionClaims
|
||||||
@ -237,6 +258,7 @@ type Query struct {
|
|||||||
LivestreamPrizes livestreamPrizes
|
LivestreamPrizes livestreamPrizes
|
||||||
LogOperation logOperation
|
LogOperation logOperation
|
||||||
LogRequest logRequest
|
LogRequest logRequest
|
||||||
|
LotteryRefundLogs lotteryRefundLogs
|
||||||
MatchingCardTypes matchingCardTypes
|
MatchingCardTypes matchingCardTypes
|
||||||
MenuActions menuActions
|
MenuActions menuActions
|
||||||
Menus menus
|
Menus menus
|
||||||
@ -263,9 +285,11 @@ type Query struct {
|
|||||||
SystemItemCards systemItemCards
|
SystemItemCards systemItemCards
|
||||||
SystemTitleEffects systemTitleEffects
|
SystemTitleEffects systemTitleEffects
|
||||||
SystemTitles systemTitles
|
SystemTitles systemTitles
|
||||||
|
TaskCenterEventLogs taskCenterEventLogs
|
||||||
TaskCenterTaskRewards taskCenterTaskRewards
|
TaskCenterTaskRewards taskCenterTaskRewards
|
||||||
TaskCenterTaskTiers taskCenterTaskTiers
|
TaskCenterTaskTiers taskCenterTaskTiers
|
||||||
TaskCenterTasks taskCenterTasks
|
TaskCenterTasks taskCenterTasks
|
||||||
|
TaskCenterUserProgress taskCenterUserProgress
|
||||||
UserAddresses userAddresses
|
UserAddresses userAddresses
|
||||||
UserCouponLedger userCouponLedger
|
UserCouponLedger userCouponLedger
|
||||||
UserCoupons userCoupons
|
UserCoupons userCoupons
|
||||||
@ -298,6 +322,9 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
|||||||
AuditRollbackLogs: q.AuditRollbackLogs.clone(db),
|
AuditRollbackLogs: q.AuditRollbackLogs.clone(db),
|
||||||
Banner: q.Banner.clone(db),
|
Banner: q.Banner.clone(db),
|
||||||
Channels: q.Channels.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),
|
GamePassPackages: q.GamePassPackages.clone(db),
|
||||||
GameTicketLogs: q.GameTicketLogs.clone(db),
|
GameTicketLogs: q.GameTicketLogs.clone(db),
|
||||||
IssuePositionClaims: q.IssuePositionClaims.clone(db),
|
IssuePositionClaims: q.IssuePositionClaims.clone(db),
|
||||||
@ -306,6 +333,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
|||||||
LivestreamPrizes: q.LivestreamPrizes.clone(db),
|
LivestreamPrizes: q.LivestreamPrizes.clone(db),
|
||||||
LogOperation: q.LogOperation.clone(db),
|
LogOperation: q.LogOperation.clone(db),
|
||||||
LogRequest: q.LogRequest.clone(db),
|
LogRequest: q.LogRequest.clone(db),
|
||||||
|
LotteryRefundLogs: q.LotteryRefundLogs.clone(db),
|
||||||
MatchingCardTypes: q.MatchingCardTypes.clone(db),
|
MatchingCardTypes: q.MatchingCardTypes.clone(db),
|
||||||
MenuActions: q.MenuActions.clone(db),
|
MenuActions: q.MenuActions.clone(db),
|
||||||
Menus: q.Menus.clone(db),
|
Menus: q.Menus.clone(db),
|
||||||
@ -332,9 +360,11 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
|||||||
SystemItemCards: q.SystemItemCards.clone(db),
|
SystemItemCards: q.SystemItemCards.clone(db),
|
||||||
SystemTitleEffects: q.SystemTitleEffects.clone(db),
|
SystemTitleEffects: q.SystemTitleEffects.clone(db),
|
||||||
SystemTitles: q.SystemTitles.clone(db),
|
SystemTitles: q.SystemTitles.clone(db),
|
||||||
|
TaskCenterEventLogs: q.TaskCenterEventLogs.clone(db),
|
||||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.clone(db),
|
TaskCenterTaskRewards: q.TaskCenterTaskRewards.clone(db),
|
||||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.clone(db),
|
TaskCenterTaskTiers: q.TaskCenterTaskTiers.clone(db),
|
||||||
TaskCenterTasks: q.TaskCenterTasks.clone(db),
|
TaskCenterTasks: q.TaskCenterTasks.clone(db),
|
||||||
|
TaskCenterUserProgress: q.TaskCenterUserProgress.clone(db),
|
||||||
UserAddresses: q.UserAddresses.clone(db),
|
UserAddresses: q.UserAddresses.clone(db),
|
||||||
UserCouponLedger: q.UserCouponLedger.clone(db),
|
UserCouponLedger: q.UserCouponLedger.clone(db),
|
||||||
UserCoupons: q.UserCoupons.clone(db),
|
UserCoupons: q.UserCoupons.clone(db),
|
||||||
@ -374,6 +404,9 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
|||||||
AuditRollbackLogs: q.AuditRollbackLogs.replaceDB(db),
|
AuditRollbackLogs: q.AuditRollbackLogs.replaceDB(db),
|
||||||
Banner: q.Banner.replaceDB(db),
|
Banner: q.Banner.replaceDB(db),
|
||||||
Channels: q.Channels.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),
|
GamePassPackages: q.GamePassPackages.replaceDB(db),
|
||||||
GameTicketLogs: q.GameTicketLogs.replaceDB(db),
|
GameTicketLogs: q.GameTicketLogs.replaceDB(db),
|
||||||
IssuePositionClaims: q.IssuePositionClaims.replaceDB(db),
|
IssuePositionClaims: q.IssuePositionClaims.replaceDB(db),
|
||||||
@ -382,6 +415,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
|||||||
LivestreamPrizes: q.LivestreamPrizes.replaceDB(db),
|
LivestreamPrizes: q.LivestreamPrizes.replaceDB(db),
|
||||||
LogOperation: q.LogOperation.replaceDB(db),
|
LogOperation: q.LogOperation.replaceDB(db),
|
||||||
LogRequest: q.LogRequest.replaceDB(db),
|
LogRequest: q.LogRequest.replaceDB(db),
|
||||||
|
LotteryRefundLogs: q.LotteryRefundLogs.replaceDB(db),
|
||||||
MatchingCardTypes: q.MatchingCardTypes.replaceDB(db),
|
MatchingCardTypes: q.MatchingCardTypes.replaceDB(db),
|
||||||
MenuActions: q.MenuActions.replaceDB(db),
|
MenuActions: q.MenuActions.replaceDB(db),
|
||||||
Menus: q.Menus.replaceDB(db),
|
Menus: q.Menus.replaceDB(db),
|
||||||
@ -408,9 +442,11 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
|||||||
SystemItemCards: q.SystemItemCards.replaceDB(db),
|
SystemItemCards: q.SystemItemCards.replaceDB(db),
|
||||||
SystemTitleEffects: q.SystemTitleEffects.replaceDB(db),
|
SystemTitleEffects: q.SystemTitleEffects.replaceDB(db),
|
||||||
SystemTitles: q.SystemTitles.replaceDB(db),
|
SystemTitles: q.SystemTitles.replaceDB(db),
|
||||||
|
TaskCenterEventLogs: q.TaskCenterEventLogs.replaceDB(db),
|
||||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.replaceDB(db),
|
TaskCenterTaskRewards: q.TaskCenterTaskRewards.replaceDB(db),
|
||||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.replaceDB(db),
|
TaskCenterTaskTiers: q.TaskCenterTaskTiers.replaceDB(db),
|
||||||
TaskCenterTasks: q.TaskCenterTasks.replaceDB(db),
|
TaskCenterTasks: q.TaskCenterTasks.replaceDB(db),
|
||||||
|
TaskCenterUserProgress: q.TaskCenterUserProgress.replaceDB(db),
|
||||||
UserAddresses: q.UserAddresses.replaceDB(db),
|
UserAddresses: q.UserAddresses.replaceDB(db),
|
||||||
UserCouponLedger: q.UserCouponLedger.replaceDB(db),
|
UserCouponLedger: q.UserCouponLedger.replaceDB(db),
|
||||||
UserCoupons: q.UserCoupons.replaceDB(db),
|
UserCoupons: q.UserCoupons.replaceDB(db),
|
||||||
@ -440,6 +476,9 @@ type queryCtx struct {
|
|||||||
AuditRollbackLogs *auditRollbackLogsDo
|
AuditRollbackLogs *auditRollbackLogsDo
|
||||||
Banner *bannerDo
|
Banner *bannerDo
|
||||||
Channels *channelsDo
|
Channels *channelsDo
|
||||||
|
DouyinBlacklist *douyinBlacklistDo
|
||||||
|
DouyinOrders *douyinOrdersDo
|
||||||
|
DouyinProductRewards *douyinProductRewardsDo
|
||||||
GamePassPackages *gamePassPackagesDo
|
GamePassPackages *gamePassPackagesDo
|
||||||
GameTicketLogs *gameTicketLogsDo
|
GameTicketLogs *gameTicketLogsDo
|
||||||
IssuePositionClaims *issuePositionClaimsDo
|
IssuePositionClaims *issuePositionClaimsDo
|
||||||
@ -448,6 +487,7 @@ type queryCtx struct {
|
|||||||
LivestreamPrizes *livestreamPrizesDo
|
LivestreamPrizes *livestreamPrizesDo
|
||||||
LogOperation *logOperationDo
|
LogOperation *logOperationDo
|
||||||
LogRequest *logRequestDo
|
LogRequest *logRequestDo
|
||||||
|
LotteryRefundLogs *lotteryRefundLogsDo
|
||||||
MatchingCardTypes *matchingCardTypesDo
|
MatchingCardTypes *matchingCardTypesDo
|
||||||
MenuActions *menuActionsDo
|
MenuActions *menuActionsDo
|
||||||
Menus *menusDo
|
Menus *menusDo
|
||||||
@ -474,9 +514,11 @@ type queryCtx struct {
|
|||||||
SystemItemCards *systemItemCardsDo
|
SystemItemCards *systemItemCardsDo
|
||||||
SystemTitleEffects *systemTitleEffectsDo
|
SystemTitleEffects *systemTitleEffectsDo
|
||||||
SystemTitles *systemTitlesDo
|
SystemTitles *systemTitlesDo
|
||||||
|
TaskCenterEventLogs *taskCenterEventLogsDo
|
||||||
TaskCenterTaskRewards *taskCenterTaskRewardsDo
|
TaskCenterTaskRewards *taskCenterTaskRewardsDo
|
||||||
TaskCenterTaskTiers *taskCenterTaskTiersDo
|
TaskCenterTaskTiers *taskCenterTaskTiersDo
|
||||||
TaskCenterTasks *taskCenterTasksDo
|
TaskCenterTasks *taskCenterTasksDo
|
||||||
|
TaskCenterUserProgress *taskCenterUserProgressDo
|
||||||
UserAddresses *userAddressesDo
|
UserAddresses *userAddressesDo
|
||||||
UserCouponLedger *userCouponLedgerDo
|
UserCouponLedger *userCouponLedgerDo
|
||||||
UserCoupons *userCouponsDo
|
UserCoupons *userCouponsDo
|
||||||
@ -506,6 +548,9 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
|||||||
AuditRollbackLogs: q.AuditRollbackLogs.WithContext(ctx),
|
AuditRollbackLogs: q.AuditRollbackLogs.WithContext(ctx),
|
||||||
Banner: q.Banner.WithContext(ctx),
|
Banner: q.Banner.WithContext(ctx),
|
||||||
Channels: q.Channels.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),
|
GamePassPackages: q.GamePassPackages.WithContext(ctx),
|
||||||
GameTicketLogs: q.GameTicketLogs.WithContext(ctx),
|
GameTicketLogs: q.GameTicketLogs.WithContext(ctx),
|
||||||
IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx),
|
IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx),
|
||||||
@ -514,6 +559,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
|||||||
LivestreamPrizes: q.LivestreamPrizes.WithContext(ctx),
|
LivestreamPrizes: q.LivestreamPrizes.WithContext(ctx),
|
||||||
LogOperation: q.LogOperation.WithContext(ctx),
|
LogOperation: q.LogOperation.WithContext(ctx),
|
||||||
LogRequest: q.LogRequest.WithContext(ctx),
|
LogRequest: q.LogRequest.WithContext(ctx),
|
||||||
|
LotteryRefundLogs: q.LotteryRefundLogs.WithContext(ctx),
|
||||||
MatchingCardTypes: q.MatchingCardTypes.WithContext(ctx),
|
MatchingCardTypes: q.MatchingCardTypes.WithContext(ctx),
|
||||||
MenuActions: q.MenuActions.WithContext(ctx),
|
MenuActions: q.MenuActions.WithContext(ctx),
|
||||||
Menus: q.Menus.WithContext(ctx),
|
Menus: q.Menus.WithContext(ctx),
|
||||||
@ -540,9 +586,11 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
|||||||
SystemItemCards: q.SystemItemCards.WithContext(ctx),
|
SystemItemCards: q.SystemItemCards.WithContext(ctx),
|
||||||
SystemTitleEffects: q.SystemTitleEffects.WithContext(ctx),
|
SystemTitleEffects: q.SystemTitleEffects.WithContext(ctx),
|
||||||
SystemTitles: q.SystemTitles.WithContext(ctx),
|
SystemTitles: q.SystemTitles.WithContext(ctx),
|
||||||
|
TaskCenterEventLogs: q.TaskCenterEventLogs.WithContext(ctx),
|
||||||
TaskCenterTaskRewards: q.TaskCenterTaskRewards.WithContext(ctx),
|
TaskCenterTaskRewards: q.TaskCenterTaskRewards.WithContext(ctx),
|
||||||
TaskCenterTaskTiers: q.TaskCenterTaskTiers.WithContext(ctx),
|
TaskCenterTaskTiers: q.TaskCenterTaskTiers.WithContext(ctx),
|
||||||
TaskCenterTasks: q.TaskCenterTasks.WithContext(ctx),
|
TaskCenterTasks: q.TaskCenterTasks.WithContext(ctx),
|
||||||
|
TaskCenterUserProgress: q.TaskCenterUserProgress.WithContext(ctx),
|
||||||
UserAddresses: q.UserAddresses.WithContext(ctx),
|
UserAddresses: q.UserAddresses.WithContext(ctx),
|
||||||
UserCouponLedger: q.UserCouponLedger.WithContext(ctx),
|
UserCouponLedger: q.UserCouponLedger.WithContext(ctx),
|
||||||
UserCoupons: q.UserCoupons.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.AccessCode = field.NewString(tableName, "access_code")
|
||||||
_livestreamActivities.DouyinProductID = field.NewString(tableName, "douyin_product_id")
|
_livestreamActivities.DouyinProductID = field.NewString(tableName, "douyin_product_id")
|
||||||
_livestreamActivities.Status = field.NewInt32(tableName, "status")
|
_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.StartTime = field.NewTime(tableName, "start_time")
|
||||||
_livestreamActivities.EndTime = field.NewTime(tableName, "end_time")
|
_livestreamActivities.EndTime = field.NewTime(tableName, "end_time")
|
||||||
_livestreamActivities.CreatedAt = field.NewTime(tableName, "created_at")
|
_livestreamActivities.CreatedAt = field.NewTime(tableName, "created_at")
|
||||||
_livestreamActivities.UpdatedAt = field.NewTime(tableName, "updated_at")
|
_livestreamActivities.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||||
_livestreamActivities.DeletedAt = field.NewField(tableName, "deleted_at")
|
_livestreamActivities.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||||
|
_livestreamActivities.TicketPrice = field.NewInt32(tableName, "ticket_price")
|
||||||
|
|
||||||
_livestreamActivities.fillFieldMap()
|
_livestreamActivities.fillFieldMap()
|
||||||
|
|
||||||
@ -49,19 +54,24 @@ func newLivestreamActivities(db *gorm.DB, opts ...gen.DOOption) livestreamActivi
|
|||||||
type livestreamActivities struct {
|
type livestreamActivities struct {
|
||||||
livestreamActivitiesDo
|
livestreamActivitiesDo
|
||||||
|
|
||||||
ALL field.Asterisk
|
ALL field.Asterisk
|
||||||
ID field.Int64 // 主键ID
|
ID field.Int64 // 主键ID
|
||||||
Name field.String // 活动名称
|
Name field.String // 活动名称
|
||||||
StreamerName field.String // 主播名称
|
StreamerName field.String // 主播名称
|
||||||
StreamerContact field.String // 主播联系方式
|
StreamerContact field.String // 主播联系方式
|
||||||
AccessCode field.String // 唯一访问码
|
AccessCode field.String // 唯一访问码
|
||||||
DouyinProductID field.String // 关联抖店商品ID
|
DouyinProductID field.String // 关联抖店商品ID
|
||||||
Status field.Int32 // 状态:1进行中 2已结束
|
Status field.Int32 // 状态:1进行中 2已结束
|
||||||
StartTime field.Time // 开始时间
|
CommitmentAlgo field.String // 承诺算法版本
|
||||||
EndTime field.Time // 结束时间
|
CommitmentSeedMaster field.Bytes // 主种子(32字节)
|
||||||
CreatedAt field.Time // 创建时间
|
CommitmentSeedHash field.Bytes // 种子SHA256哈希
|
||||||
UpdatedAt field.Time // 更新时间
|
CommitmentStateVersion field.Int32 // 状态版本
|
||||||
DeletedAt field.Field // 删除时间
|
StartTime field.Time // 开始时间
|
||||||
|
EndTime field.Time // 结束时间
|
||||||
|
CreatedAt field.Time // 创建时间
|
||||||
|
UpdatedAt field.Time // 更新时间
|
||||||
|
DeletedAt field.Field // 删除时间
|
||||||
|
TicketPrice field.Int32
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@ -85,11 +95,16 @@ func (l *livestreamActivities) updateTableName(table string) *livestreamActiviti
|
|||||||
l.AccessCode = field.NewString(table, "access_code")
|
l.AccessCode = field.NewString(table, "access_code")
|
||||||
l.DouyinProductID = field.NewString(table, "douyin_product_id")
|
l.DouyinProductID = field.NewString(table, "douyin_product_id")
|
||||||
l.Status = field.NewInt32(table, "status")
|
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.StartTime = field.NewTime(table, "start_time")
|
||||||
l.EndTime = field.NewTime(table, "end_time")
|
l.EndTime = field.NewTime(table, "end_time")
|
||||||
l.CreatedAt = field.NewTime(table, "created_at")
|
l.CreatedAt = field.NewTime(table, "created_at")
|
||||||
l.UpdatedAt = field.NewTime(table, "updated_at")
|
l.UpdatedAt = field.NewTime(table, "updated_at")
|
||||||
l.DeletedAt = field.NewField(table, "deleted_at")
|
l.DeletedAt = field.NewField(table, "deleted_at")
|
||||||
|
l.TicketPrice = field.NewInt32(table, "ticket_price")
|
||||||
|
|
||||||
l.fillFieldMap()
|
l.fillFieldMap()
|
||||||
|
|
||||||
@ -106,7 +121,7 @@ func (l *livestreamActivities) GetFieldByName(fieldName string) (field.OrderExpr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *livestreamActivities) fillFieldMap() {
|
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["id"] = l.ID
|
||||||
l.fieldMap["name"] = l.Name
|
l.fieldMap["name"] = l.Name
|
||||||
l.fieldMap["streamer_name"] = l.StreamerName
|
l.fieldMap["streamer_name"] = l.StreamerName
|
||||||
@ -114,11 +129,16 @@ func (l *livestreamActivities) fillFieldMap() {
|
|||||||
l.fieldMap["access_code"] = l.AccessCode
|
l.fieldMap["access_code"] = l.AccessCode
|
||||||
l.fieldMap["douyin_product_id"] = l.DouyinProductID
|
l.fieldMap["douyin_product_id"] = l.DouyinProductID
|
||||||
l.fieldMap["status"] = l.Status
|
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["start_time"] = l.StartTime
|
||||||
l.fieldMap["end_time"] = l.EndTime
|
l.fieldMap["end_time"] = l.EndTime
|
||||||
l.fieldMap["created_at"] = l.CreatedAt
|
l.fieldMap["created_at"] = l.CreatedAt
|
||||||
l.fieldMap["updated_at"] = l.UpdatedAt
|
l.fieldMap["updated_at"] = l.UpdatedAt
|
||||||
l.fieldMap["deleted_at"] = l.DeletedAt
|
l.fieldMap["deleted_at"] = l.DeletedAt
|
||||||
|
l.fieldMap["ticket_price"] = l.TicketPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l livestreamActivities) clone(db *gorm.DB) livestreamActivities {
|
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.ActivityID = field.NewInt64(tableName, "activity_id")
|
||||||
_livestreamDrawLogs.PrizeID = field.NewInt64(tableName, "prize_id")
|
_livestreamDrawLogs.PrizeID = field.NewInt64(tableName, "prize_id")
|
||||||
_livestreamDrawLogs.DouyinOrderID = field.NewInt64(tableName, "douyin_order_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.LocalUserID = field.NewInt64(tableName, "local_user_id")
|
||||||
_livestreamDrawLogs.DouyinUserID = field.NewString(tableName, "douyin_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.PrizeName = field.NewString(tableName, "prize_name")
|
||||||
_livestreamDrawLogs.Level = field.NewInt32(tableName, "level")
|
_livestreamDrawLogs.Level = field.NewInt32(tableName, "level")
|
||||||
_livestreamDrawLogs.SeedHash = field.NewString(tableName, "seed_hash")
|
_livestreamDrawLogs.SeedHash = field.NewString(tableName, "seed_hash")
|
||||||
_livestreamDrawLogs.RandValue = field.NewInt64(tableName, "rand_value")
|
_livestreamDrawLogs.RandValue = field.NewInt64(tableName, "rand_value")
|
||||||
_livestreamDrawLogs.WeightsTotal = field.NewInt64(tableName, "weights_total")
|
_livestreamDrawLogs.WeightsTotal = field.NewInt64(tableName, "weights_total")
|
||||||
_livestreamDrawLogs.CreatedAt = field.NewTime(tableName, "created_at")
|
_livestreamDrawLogs.CreatedAt = field.NewTime(tableName, "created_at")
|
||||||
|
_livestreamDrawLogs.IsGranted = field.NewBool(tableName, "is_granted")
|
||||||
|
_livestreamDrawLogs.IsRefunded = field.NewInt32(tableName, "is_refunded")
|
||||||
|
|
||||||
_livestreamDrawLogs.fillFieldMap()
|
_livestreamDrawLogs.fillFieldMap()
|
||||||
|
|
||||||
@ -54,14 +58,18 @@ type livestreamDrawLogs struct {
|
|||||||
ActivityID field.Int64 // 关联livestream_activities.id
|
ActivityID field.Int64 // 关联livestream_activities.id
|
||||||
PrizeID field.Int64 // 关联livestream_prizes.id
|
PrizeID field.Int64 // 关联livestream_prizes.id
|
||||||
DouyinOrderID field.Int64 // 关联douyin_orders.id
|
DouyinOrderID field.Int64 // 关联douyin_orders.id
|
||||||
|
ShopOrderID field.String // 抖店订单号
|
||||||
LocalUserID field.Int64 // 本地用户ID
|
LocalUserID field.Int64 // 本地用户ID
|
||||||
DouyinUserID field.String // 抖音用户ID
|
DouyinUserID field.String // 抖音用户ID
|
||||||
|
UserNickname field.String // 用户昵称
|
||||||
PrizeName field.String // 中奖奖品名称快照
|
PrizeName field.String // 中奖奖品名称快照
|
||||||
Level field.Int32 // 奖品等级
|
Level field.Int32 // 奖品等级
|
||||||
SeedHash field.String // 哈希种子
|
SeedHash field.String // 哈希种子
|
||||||
RandValue field.Int64 // 随机值
|
RandValue field.Int64 // 随机值
|
||||||
WeightsTotal field.Int64 // 权重总和
|
WeightsTotal field.Int64 // 权重总和
|
||||||
CreatedAt field.Time // 中奖时间
|
CreatedAt field.Time // 中奖时间
|
||||||
|
IsGranted field.Bool // 是否已发放奖品
|
||||||
|
IsRefunded field.Int32 // 订单是否已退款
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@ -82,14 +90,18 @@ func (l *livestreamDrawLogs) updateTableName(table string) *livestreamDrawLogs {
|
|||||||
l.ActivityID = field.NewInt64(table, "activity_id")
|
l.ActivityID = field.NewInt64(table, "activity_id")
|
||||||
l.PrizeID = field.NewInt64(table, "prize_id")
|
l.PrizeID = field.NewInt64(table, "prize_id")
|
||||||
l.DouyinOrderID = field.NewInt64(table, "douyin_order_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.LocalUserID = field.NewInt64(table, "local_user_id")
|
||||||
l.DouyinUserID = field.NewString(table, "douyin_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.PrizeName = field.NewString(table, "prize_name")
|
||||||
l.Level = field.NewInt32(table, "level")
|
l.Level = field.NewInt32(table, "level")
|
||||||
l.SeedHash = field.NewString(table, "seed_hash")
|
l.SeedHash = field.NewString(table, "seed_hash")
|
||||||
l.RandValue = field.NewInt64(table, "rand_value")
|
l.RandValue = field.NewInt64(table, "rand_value")
|
||||||
l.WeightsTotal = field.NewInt64(table, "weights_total")
|
l.WeightsTotal = field.NewInt64(table, "weights_total")
|
||||||
l.CreatedAt = field.NewTime(table, "created_at")
|
l.CreatedAt = field.NewTime(table, "created_at")
|
||||||
|
l.IsGranted = field.NewBool(table, "is_granted")
|
||||||
|
l.IsRefunded = field.NewInt32(table, "is_refunded")
|
||||||
|
|
||||||
l.fillFieldMap()
|
l.fillFieldMap()
|
||||||
|
|
||||||
@ -106,19 +118,23 @@ func (l *livestreamDrawLogs) GetFieldByName(fieldName string) (field.OrderExpr,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *livestreamDrawLogs) fillFieldMap() {
|
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["id"] = l.ID
|
||||||
l.fieldMap["activity_id"] = l.ActivityID
|
l.fieldMap["activity_id"] = l.ActivityID
|
||||||
l.fieldMap["prize_id"] = l.PrizeID
|
l.fieldMap["prize_id"] = l.PrizeID
|
||||||
l.fieldMap["douyin_order_id"] = l.DouyinOrderID
|
l.fieldMap["douyin_order_id"] = l.DouyinOrderID
|
||||||
|
l.fieldMap["shop_order_id"] = l.ShopOrderID
|
||||||
l.fieldMap["local_user_id"] = l.LocalUserID
|
l.fieldMap["local_user_id"] = l.LocalUserID
|
||||||
l.fieldMap["douyin_user_id"] = l.DouyinUserID
|
l.fieldMap["douyin_user_id"] = l.DouyinUserID
|
||||||
|
l.fieldMap["user_nickname"] = l.UserNickname
|
||||||
l.fieldMap["prize_name"] = l.PrizeName
|
l.fieldMap["prize_name"] = l.PrizeName
|
||||||
l.fieldMap["level"] = l.Level
|
l.fieldMap["level"] = l.Level
|
||||||
l.fieldMap["seed_hash"] = l.SeedHash
|
l.fieldMap["seed_hash"] = l.SeedHash
|
||||||
l.fieldMap["rand_value"] = l.RandValue
|
l.fieldMap["rand_value"] = l.RandValue
|
||||||
l.fieldMap["weights_total"] = l.WeightsTotal
|
l.fieldMap["weights_total"] = l.WeightsTotal
|
||||||
l.fieldMap["created_at"] = l.CreatedAt
|
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 {
|
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.Sort = field.NewInt32(tableName, "sort")
|
||||||
_livestreamPrizes.CreatedAt = field.NewTime(tableName, "created_at")
|
_livestreamPrizes.CreatedAt = field.NewTime(tableName, "created_at")
|
||||||
_livestreamPrizes.UpdatedAt = field.NewTime(tableName, "updated_at")
|
_livestreamPrizes.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||||
|
_livestreamPrizes.CostPrice = field.NewInt64(tableName, "cost_price")
|
||||||
|
|
||||||
_livestreamPrizes.fillFieldMap()
|
_livestreamPrizes.fillFieldMap()
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ type livestreamPrizes struct {
|
|||||||
Sort field.Int32 // 排序
|
Sort field.Int32 // 排序
|
||||||
CreatedAt field.Time // 创建时间
|
CreatedAt field.Time // 创建时间
|
||||||
UpdatedAt field.Time // 更新时间
|
UpdatedAt field.Time // 更新时间
|
||||||
|
CostPrice field.Int64 // 成本价(分)
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@ -90,6 +92,7 @@ func (l *livestreamPrizes) updateTableName(table string) *livestreamPrizes {
|
|||||||
l.Sort = field.NewInt32(table, "sort")
|
l.Sort = field.NewInt32(table, "sort")
|
||||||
l.CreatedAt = field.NewTime(table, "created_at")
|
l.CreatedAt = field.NewTime(table, "created_at")
|
||||||
l.UpdatedAt = field.NewTime(table, "updated_at")
|
l.UpdatedAt = field.NewTime(table, "updated_at")
|
||||||
|
l.CostPrice = field.NewInt64(table, "cost_price")
|
||||||
|
|
||||||
l.fillFieldMap()
|
l.fillFieldMap()
|
||||||
|
|
||||||
@ -106,7 +109,7 @@ func (l *livestreamPrizes) GetFieldByName(fieldName string) (field.OrderExpr, bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *livestreamPrizes) fillFieldMap() {
|
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["id"] = l.ID
|
||||||
l.fieldMap["activity_id"] = l.ActivityID
|
l.fieldMap["activity_id"] = l.ActivityID
|
||||||
l.fieldMap["name"] = l.Name
|
l.fieldMap["name"] = l.Name
|
||||||
@ -119,6 +122,7 @@ func (l *livestreamPrizes) fillFieldMap() {
|
|||||||
l.fieldMap["sort"] = l.Sort
|
l.fieldMap["sort"] = l.Sort
|
||||||
l.fieldMap["created_at"] = l.CreatedAt
|
l.fieldMap["created_at"] = l.CreatedAt
|
||||||
l.fieldMap["updated_at"] = l.UpdatedAt
|
l.fieldMap["updated_at"] = l.UpdatedAt
|
||||||
|
l.fieldMap["cost_price"] = l.CostPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l livestreamPrizes) clone(db *gorm.DB) livestreamPrizes {
|
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.DouyinID = field.NewString(tableName, "douyin_id")
|
||||||
_users.ChannelID = field.NewInt64(tableName, "channel_id")
|
_users.ChannelID = field.NewInt64(tableName, "channel_id")
|
||||||
_users.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
_users.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
||||||
|
_users.Remark = field.NewString(tableName, "remark")
|
||||||
|
|
||||||
_users.fillFieldMap()
|
_users.fillFieldMap()
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ type users struct {
|
|||||||
DouyinID field.String
|
DouyinID field.String
|
||||||
ChannelID field.Int64 // 渠道ID
|
ChannelID field.Int64 // 渠道ID
|
||||||
DouyinUserID field.String
|
DouyinUserID field.String
|
||||||
|
Remark field.String // 管ç†å‘˜å¤‡æ³¨
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@ -99,6 +101,7 @@ func (u *users) updateTableName(table string) *users {
|
|||||||
u.DouyinID = field.NewString(table, "douyin_id")
|
u.DouyinID = field.NewString(table, "douyin_id")
|
||||||
u.ChannelID = field.NewInt64(table, "channel_id")
|
u.ChannelID = field.NewInt64(table, "channel_id")
|
||||||
u.DouyinUserID = field.NewString(table, "douyin_user_id")
|
u.DouyinUserID = field.NewString(table, "douyin_user_id")
|
||||||
|
u.Remark = field.NewString(table, "remark")
|
||||||
|
|
||||||
u.fillFieldMap()
|
u.fillFieldMap()
|
||||||
|
|
||||||
@ -115,7 +118,7 @@ func (u *users) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *users) fillFieldMap() {
|
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["id"] = u.ID
|
||||||
u.fieldMap["created_at"] = u.CreatedAt
|
u.fieldMap["created_at"] = u.CreatedAt
|
||||||
u.fieldMap["updated_at"] = u.UpdatedAt
|
u.fieldMap["updated_at"] = u.UpdatedAt
|
||||||
@ -131,6 +134,7 @@ func (u *users) fillFieldMap() {
|
|||||||
u.fieldMap["douyin_id"] = u.DouyinID
|
u.fieldMap["douyin_id"] = u.DouyinID
|
||||||
u.fieldMap["channel_id"] = u.ChannelID
|
u.fieldMap["channel_id"] = u.ChannelID
|
||||||
u.fieldMap["douyin_user_id"] = u.DouyinUserID
|
u.fieldMap["douyin_user_id"] = u.DouyinUserID
|
||||||
|
u.fieldMap["remark"] = u.Remark
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u users) clone(db *gorm.DB) users {
|
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 抖店订单表
|
// DouyinOrders 抖店订单表
|
||||||
type DouyinOrders struct {
|
type DouyinOrders struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||||
ShopOrderID string `gorm:"column:shop_order_id;not null;comment:抖店订单号" json:"shop_order_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
|
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=已完成
|
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
|
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
|
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"` // 实收金额(分)
|
ActualReceiveAmount int64 `gorm:"column:actual_receive_amount;comment:实收金额(分)" json:"actual_receive_amount"` // 实收金额(分)
|
||||||
PayTypeDesc string `gorm:"column:pay_type_desc;comment:支付方式描述" json:"pay_type_desc"` // 支付方式描述
|
ActualPayAmount int64 `gorm:"column:actual_pay_amount;comment:实付金额(分)" json:"actual_pay_amount"` // 实付金额(分)
|
||||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
PayTypeDesc string `gorm:"column:pay_type_desc;comment:支付方式描述" json:"pay_type_desc"` // 支付方式描述
|
||||||
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
|
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||||
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
|
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
|
||||||
RewardGranted int32 `gorm:"column:reward_granted;not null;default:0;comment:奖励已发放: 0=否, 1=是" json:"reward_granted"` // 奖励已发放
|
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
|
||||||
ProductCount int64 `gorm:"column:product_count;not null;default:1;comment:商品数量" json:"product_count"` // 商品数量
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
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"`
|
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
|
// 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"` // 唯一访问码
|
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
|
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已结束
|
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"` // 承诺算法版本
|
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字节)
|
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哈希
|
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"` // 开始时间
|
StartTime time.Time `gorm:"column:start_time;comment:开始时间" json:"start_time"` // 开始时间
|
||||||
EndTime time.Time `gorm:"column:end_time;comment:结束时间" json:"end_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"` // 创建时间
|
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"` // 更新时间
|
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"` // 删除时间
|
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
|
// 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
|
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
|
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
|
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
|
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
|
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"` // 中奖奖品名称快照
|
PrizeName string `gorm:"column:prize_name;comment:中奖奖品名称快照" json:"prize_name"` // 中奖奖品名称快照
|
||||||
Level int32 `gorm:"column:level;default:1;comment:奖品等级" json:"level"` // 奖品等级
|
Level int32 `gorm:"column:level;default:1;comment:奖品等级" json:"level"` // 奖品等级
|
||||||
SeedHash string `gorm:"column:seed_hash;comment:哈希种子" json:"seed_hash"` // 哈希种子
|
SeedHash string `gorm:"column:seed_hash;comment:哈希种子" json:"seed_hash"` // 哈希种子
|
||||||
RandValue int64 `gorm:"column:rand_value;comment:随机值" json:"rand_value"` // 随机值
|
RandValue int64 `gorm:"column:rand_value;comment:随机值" json:"rand_value"` // 随机值
|
||||||
WeightsTotal int64 `gorm:"column:weights_total;comment:权重总和" json:"weights_total"` // 权重总和
|
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"` // 中奖时间
|
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
|
// TableName LivestreamDrawLogs's table name
|
||||||
|
|||||||
@ -21,10 +21,10 @@ type LivestreamPrizes struct {
|
|||||||
Remaining int32 `gorm:"column:remaining;not null;default:-1;comment:剩余数量" json:"remaining"` // 剩余数量
|
Remaining int32 `gorm:"column:remaining;not null;default:-1;comment:剩余数量" json:"remaining"` // 剩余数量
|
||||||
Level int32 `gorm:"column:level;not null;default:1;comment:奖品等级" json:"level"` // 奖品等级
|
Level int32 `gorm:"column:level;not null;default:1;comment:奖品等级" json:"level"` // 奖品等级
|
||||||
ProductID int64 `gorm:"column:product_id;comment:关联系统商品ID" json:"product_id"` // 关联系统商品ID
|
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"` // 排序
|
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"` // 创建时间
|
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"` // 更新时间
|
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
|
// TableName LivestreamPrizes's table name
|
||||||
|
|||||||
@ -12,26 +12,26 @@ const TableNameOrders = "orders"
|
|||||||
|
|
||||||
// Orders 订单
|
// Orders 订单
|
||||||
type Orders struct {
|
type Orders struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
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"` // 创建时间
|
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"` // 更新时间
|
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)
|
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"` // 业务订单号(唯一)
|
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系统发放
|
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"` // 订单总金额(分)
|
TotalAmount int64 `gorm:"column:total_amount;not null;comment:订单总金额(分)" json:"total_amount"` // 订单总金额(分)
|
||||||
DiscountAmount int64 `gorm:"column:discount_amount;not null;comment:优惠券抵扣金额(分)" json:"discount_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"` // 积分抵扣金额(分)
|
PointsAmount int64 `gorm:"column:points_amount;not null;comment:积分抵扣金额(分)" json:"points_amount"` // 积分抵扣金额(分)
|
||||||
ActualAmount int64 `gorm:"column:actual_amount;not null;comment:实际支付金额(分)" json:"actual_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已退款
|
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)
|
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"` // 支付完成时间
|
PaidAt time.Time `gorm:"column:paid_at;comment:支付完成时间" json:"paid_at"` // 支付完成时间
|
||||||
CancelledAt time.Time `gorm:"column:cancelled_at;comment:取消时间" json:"cancelled_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)
|
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"` // 是否已履约/消耗(对虚拟资产)
|
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)
|
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
|
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
|
ItemCardID int64 `gorm:"column:item_card_id;comment:使用的道具卡ID" json:"item_card_id"` // 使用的道具卡ID
|
||||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName Orders's table name
|
// TableName Orders's table name
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type Users struct {
|
|||||||
DouyinID string `gorm:"column:douyin_id" json:"douyin_id"`
|
DouyinID string `gorm:"column:douyin_id" json:"douyin_id"`
|
||||||
ChannelID int64 `gorm:"column:channel_id;comment:渠道ID" json:"channel_id"` // 渠道ID
|
ChannelID int64 `gorm:"column:channel_id;comment:渠道ID" json:"channel_id"` // 渠道ID
|
||||||
DouyinUserID string `gorm:"column:douyin_user_id" json:"douyin_user_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
|
// 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)
|
var _ Interceptor = (*interceptor)(nil)
|
||||||
|
|
||||||
type Interceptor interface {
|
type Interceptor interface {
|
||||||
// AdminTokenAuthVerify 管理端授权验证
|
// AdminTokenAuthVerify 管理端授权验证
|
||||||
AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
|
AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
|
||||||
|
|
||||||
// AppTokenAuthVerify APP端授权验证
|
// AppTokenAuthVerify APP端授权验证
|
||||||
AppTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
|
AppTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
|
||||||
|
|
||||||
RequireAdminRole() core.HandlerFunc
|
RequireAdminRole() core.HandlerFunc
|
||||||
RequireAdminAction(mark string) core.HandlerFunc
|
RequireAdminAction(mark string) core.HandlerFunc
|
||||||
|
CheckBlacklist() core.HandlerFunc
|
||||||
|
|
||||||
// i 为了避免被其他包实现
|
// i 为了避免被其他包实现
|
||||||
i()
|
i()
|
||||||
}
|
}
|
||||||
|
|
||||||
type interceptor struct {
|
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.POST("/livestream/activities/:id/commitment/generate", adminHandler.GenerateLivestreamCommitment())
|
||||||
adminAuthApiRouter.GET("/livestream/activities/:id/commitment/summary", adminHandler.GetLivestreamCommitmentSummary())
|
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
|
// 系统配置KV
|
||||||
adminAuthApiRouter.GET("/system/configs", adminHandler.ListSystemConfigs())
|
adminAuthApiRouter.GET("/system/configs", adminHandler.ListSystemConfigs())
|
||||||
adminAuthApiRouter.POST("/system/configs", adminHandler.UpsertSystemConfig())
|
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", 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/stats/profit_loss_details", intc.RequireAdminAction("user:view"), adminHandler.GetUserProfitLossDetails())
|
||||||
adminAuthApiRouter.GET("/users/:user_id/profile", intc.RequireAdminAction("user:view"), adminHandler.GetUserProfile())
|
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.GET("/users/:user_id/audit", intc.RequireAdminAction("user:view"), adminHandler.ListUserAuditLogs())
|
||||||
|
|
||||||
adminAuthApiRouter.POST("/users/:user_id/token", intc.RequireAdminAction("user:token:issue"), adminHandler.IssueUserToken())
|
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.PUT("/users/:user_id/addresses/:address_id", userHandler.UpdateUserAddress())
|
||||||
appAuthApiRouter.DELETE("/users/:user_id/addresses/:address_id", userHandler.DeleteUserAddress())
|
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 端
|
// 任务中心 APP 端
|
||||||
appAuthApiRouter.GET("/task-center/tasks", taskCenterHandler.ListTasksForApp())
|
appAuthApiRouter.GET("/task-center/tasks", taskCenterHandler.ListTasksForApp())
|
||||||
appAuthApiRouter.GET("/task-center/tasks/:id/progress/:user_id", taskCenterHandler.GetTaskProgressForApp())
|
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("/store/items", appapi.NewStore(logger, db, userSvc).ListStoreItemsForApp())
|
||||||
appAuthApiRouter.GET("/lottery/result", activityHandler.LotteryResultByOrder())
|
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.POST("/matching/check", activityHandler.CheckMatchingGame())
|
||||||
appAuthApiRouter.GET("/matching/state", activityHandler.GetMatchingGameState())
|
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/available", userHandler.GetGamePasses())
|
||||||
appAuthApiRouter.GET("/game-passes/packages", userHandler.GetGamePassPackages())
|
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.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/create", userHandler.CreateAddressShare())
|
||||||
appAuthApiRouter.POST("/users/:user_id/inventory/address-share/revoke", userHandler.RevokeAddressShare())
|
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())
|
mux.Group("/api/pay").POST("/wechat/notify", payHandler.WechatNotify())
|
||||||
|
|||||||
@ -11,18 +11,18 @@ import (
|
|||||||
// 参数: in 活动创建输入
|
// 参数: in 活动创建输入
|
||||||
// 返回: 新建的活动记录与错误
|
// 返回: 新建的活动记录与错误
|
||||||
func (s *service) CreateActivity(ctx context.Context, in CreateActivityInput) (*model.Activities, error) {
|
func (s *service) CreateActivity(ctx context.Context, in CreateActivityInput) (*model.Activities, error) {
|
||||||
item := &model.Activities{
|
item := &model.Activities{
|
||||||
Name: in.Name,
|
Name: in.Name,
|
||||||
Banner: in.Banner,
|
Banner: in.Banner,
|
||||||
Image: in.Image,
|
Image: in.Image,
|
||||||
GameplayIntro: sanitizeHTML(in.GameplayIntro),
|
GameplayIntro: sanitizeHTML(in.GameplayIntro),
|
||||||
ActivityCategoryID: in.ActivityCategoryID,
|
ActivityCategoryID: in.ActivityCategoryID,
|
||||||
Status: in.Status,
|
Status: in.Status,
|
||||||
PriceDraw: in.PriceDraw,
|
PriceDraw: in.PriceDraw,
|
||||||
IsBoss: in.IsBoss,
|
IsBoss: in.IsBoss,
|
||||||
AllowItemCards: in.AllowItemCards != 0,
|
AllowItemCards: in.AllowItemCards != 0,
|
||||||
AllowCoupons: in.AllowCoupons != 0,
|
AllowCoupons: in.AllowCoupons != 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if in.StartTime != nil {
|
if in.StartTime != nil {
|
||||||
item.StartTime = *in.StartTime
|
item.StartTime = *in.StartTime
|
||||||
@ -41,15 +41,15 @@ func (s *service) CreateActivity(ctx context.Context, in CreateActivityInput) (*
|
|||||||
if strings.TrimSpace(item.GameplayIntro) == "" {
|
if strings.TrimSpace(item.GameplayIntro) == "" {
|
||||||
do = do.Omit(s.writeDB.Activities.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)
|
do = do.Omit(s.writeDB.Activities.StartTime)
|
||||||
}
|
}
|
||||||
if in.EndTime == nil {
|
if in.EndTime == nil || in.EndTime.IsZero() {
|
||||||
do = do.Omit(s.writeDB.Activities.EndTime)
|
do = do.Omit(s.writeDB.Activities.EndTime)
|
||||||
}
|
}
|
||||||
// 避免零日期写入新增的时间列
|
// 避免零日期写入新增的时间列
|
||||||
do = do.Omit(s.writeDB.Activities.ScheduledTime, s.writeDB.Activities.LastSettledAt)
|
do = do.Omit(s.writeDB.Activities.ScheduledTime, s.writeDB.Activities.LastSettledAt)
|
||||||
err := do.Create(item)
|
err := do.Create(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActivityOrderService 活动订单创建服务
|
// ActivityOrderService 活动订单创建服务
|
||||||
@ -102,6 +104,9 @@ func (s *activityOrderService) CreateActivityOrder(ctx core.Context, req CreateA
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 应用称号折扣 (Title Discount)
|
// 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{
|
titleEffects, _ := s.title.ResolveActiveEffects(ctx.RequestContext(), userID, titlesvc.EffectScope{
|
||||||
ActivityID: &req.ActivityID,
|
ActivityID: &req.ActivityID,
|
||||||
IssueID: &req.IssueID,
|
IssueID: &req.IssueID,
|
||||||
@ -134,43 +139,69 @@ func (s *activityOrderService) CreateActivityOrder(ctx core.Context, req CreateA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 应用优惠券 (using applyCouponWithCap logic)
|
|
||||||
var appliedCouponVal int64
|
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
|
err := s.writeDB.Transaction(func(tx *dao.Query) error {
|
||||||
if req.ItemCardID != nil && *req.ItemCardID > 0 {
|
var deductionOp func(int64) error
|
||||||
fmt.Printf("[订单服务] 尝试道具卡 用户卡ID=%d 订单号=%s\n", *req.ItemCardID, order.OrderNo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 保存订单
|
// 3. 应用优惠券 (Lock & Calculate)
|
||||||
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
|
|
||||||
|
|
||||||
// 核销优惠券
|
|
||||||
if req.CouponID != nil && *req.CouponID > 0 {
|
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",
|
fmt.Printf("[订单服务] 订单创建完成 订单号=%s 总额(分)=%d 优惠(分)=%d 实付(分)=%d 状态=%d\n",
|
||||||
@ -182,40 +213,55 @@ func (s *activityOrderService) CreateActivityOrder(ctx core.Context, req CreateA
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyCouponWithCap 优惠券抵扣(含50%封顶与金额券部分使用)
|
// applyCouponWithLock 锁定计算并返回扣减操作闭包
|
||||||
func (s *activityOrderService) applyCouponWithCap(ctx context.Context, userID int64, order *model.Orders, activityID int64, userCouponID int64) int64 {
|
// 逻辑:锁定行 -> 计算优惠 -> 返回闭包(闭包内执行 UPDATE Balance + Insert Ledger)
|
||||||
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()
|
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 {
|
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() {
|
if uc.ValidEnd.IsZero() {
|
||||||
return "无截止"
|
return "无截止"
|
||||||
}
|
}
|
||||||
return uc.ValidEnd.Format(time.RFC3339)
|
return uc.ValidEnd.Format(time.RFC3339)
|
||||||
}())
|
}(), uc.BalanceAmount)
|
||||||
sc, _ := s.readDB.SystemCoupons.WithContext(ctx).Where(s.readDB.SystemCoupons.ID.Eq(uc.CouponID), s.readDB.SystemCoupons.Status.Eq(1)).First()
|
|
||||||
|
sc, _ := tx.SystemCoupons.WithContext(ctx).Where(tx.SystemCoupons.ID.Eq(uc.CouponID), tx.SystemCoupons.Status.Eq(1)).First()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
fmt.Printf("[优惠券] 模板不存在 用户券ID=%d\n", userCouponID)
|
fmt.Printf("[优惠券] 模板不存在 用户券ID=%d\n", userCouponID)
|
||||||
return 0
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
if uc.ValidStart.After(now) {
|
if uc.ValidStart.After(now) {
|
||||||
fmt.Printf("[优惠券] 未到开始时间 用户券ID=%d\n", userCouponID)
|
fmt.Printf("[优惠券] 未到开始时间 用户券ID=%d\n", userCouponID)
|
||||||
return 0
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
|
if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
|
||||||
fmt.Printf("[优惠券] 已过期 用户券ID=%d\n", userCouponID)
|
fmt.Printf("[优惠券] 已过期 用户券ID=%d\n", userCouponID)
|
||||||
return 0
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
scopeOK := (sc.ScopeType == 1) || (sc.ScopeType == 2 && sc.ActivityID == activityID)
|
scopeOK := (sc.ScopeType == 1) || (sc.ScopeType == 2 && sc.ActivityID == activityID)
|
||||||
if !scopeOK {
|
if !scopeOK {
|
||||||
fmt.Printf("[优惠券] 范围不匹配 用户券ID=%d scope_type=%d\n", userCouponID, sc.ScopeType)
|
fmt.Printf("[优惠券] 范围不匹配 用户券ID=%d scope_type=%d\n", userCouponID, sc.ScopeType)
|
||||||
return 0
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
if order.TotalAmount < sc.MinSpend {
|
if order.TotalAmount < sc.MinSpend {
|
||||||
fmt.Printf("[优惠券] 未达使用门槛 用户券ID=%d min_spend(分)=%d 订单总额(分)=%d\n", userCouponID, sc.MinSpend, order.TotalAmount)
|
fmt.Printf("[优惠券] 未达使用门槛 用户券ID=%d min_spend(分)=%d 订单总额(分)=%d\n", userCouponID, sc.MinSpend, order.TotalAmount)
|
||||||
return 0
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 50% 封顶
|
// 50% 封顶
|
||||||
@ -223,17 +269,13 @@ func (s *activityOrderService) applyCouponWithCap(ctx context.Context, userID in
|
|||||||
remainingCap := cap - order.DiscountAmount
|
remainingCap := cap - order.DiscountAmount
|
||||||
if remainingCap <= 0 {
|
if remainingCap <= 0 {
|
||||||
fmt.Printf("[优惠券] 已达封顶\n")
|
fmt.Printf("[优惠券] 已达封顶\n")
|
||||||
return 0
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
applied := int64(0)
|
applied := int64(0)
|
||||||
switch sc.DiscountType {
|
switch sc.DiscountType {
|
||||||
case 1: // 金额券
|
case 1: // 金额券 (Atomic Deduction)
|
||||||
var bal int64
|
var bal = uc.BalanceAmount
|
||||||
_ = s.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
|
|
||||||
if bal <= 0 {
|
|
||||||
bal = sc.DiscountValue
|
|
||||||
}
|
|
||||||
if bal > 0 {
|
if bal > 0 {
|
||||||
if bal > remainingCap {
|
if bal > remainingCap {
|
||||||
applied = remainingCap
|
applied = remainingCap
|
||||||
@ -267,54 +309,105 @@ func (s *activityOrderService) applyCouponWithCap(ctx context.Context, userID in
|
|||||||
applied = order.ActualAmount
|
applied = order.ActualAmount
|
||||||
}
|
}
|
||||||
if applied <= 0 {
|
if applied <= 0 {
|
||||||
return 0
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update order struct
|
||||||
order.ActualAmount -= applied
|
order.ActualAmount -= applied
|
||||||
order.DiscountAmount += applied
|
order.DiscountAmount += applied
|
||||||
order.Remark += fmt.Sprintf("|c:%d:%d", userCouponID, 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元支付时核销优惠券
|
// consumeCouponOnZeroPayTx 0元支付时核销优惠券 (With Tx)
|
||||||
func (s *activityOrderService) consumeCouponOnZeroPay(ctx context.Context, userID int64, orderID int64, userCouponID int64, applied int64, now time.Time) {
|
func (s *activityOrderService) consumeCouponOnZeroPayTx(ctx context.Context, tx *dao.Query, 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()
|
uc, _ := tx.UserCoupons.WithContext(ctx).Where(tx.UserCoupons.ID.Eq(userCouponID), tx.UserCoupons.UserID.Eq(userID)).First()
|
||||||
if uc == nil {
|
if uc == nil {
|
||||||
return
|
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 {
|
if sc == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if sc.DiscountType == 1 { // 金额券 - 部分扣减
|
// 如果是金额券,余额已经在 applyCouponWithCap 中扣减过了。
|
||||||
var bal int64
|
// 这里的逻辑主要是为了记录 used_order_id 等 meta 信息。
|
||||||
_ = s.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
|
if sc.DiscountType == 1 { // 金额券
|
||||||
nb := bal - applied
|
// 状态:
|
||||||
if nb < 0 {
|
// 如果余额 > 0 -> 状态 1
|
||||||
nb = 0
|
// 如果余额 = 0 -> 状态 2
|
||||||
}
|
// 不需要 status=4。
|
||||||
if nb == 0 {
|
|
||||||
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
// 我们只需要记录用于统计的 used_order_id, used_at
|
||||||
"balance_amount": nb,
|
// 注意:amounts update has been done.
|
||||||
s.readDB.UserCoupons.Status.ColumnName().String(): 2,
|
_, _ = tx.UserCoupons.WithContext(ctx).Where(tx.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
||||||
s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
tx.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||||
s.readDB.UserCoupons.UsedAt.ColumnName().String(): now,
|
tx.UserCoupons.UsedAt.ColumnName().String(): now,
|
||||||
})
|
})
|
||||||
} else {
|
} else { // 满减/折扣券
|
||||||
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
// Apply 时设置为 4 (Frozen)
|
||||||
"balance_amount": nb,
|
// 此时需要确认为 2 (Used)
|
||||||
s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
_, _ = tx.UserCoupons.WithContext(ctx).Where(tx.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
|
||||||
s.readDB.UserCoupons.UsedAt.ColumnName().String(): now,
|
tx.UserCoupons.Status.ColumnName().String(): 2,
|
||||||
})
|
tx.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||||
}
|
tx.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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 {
|
if u != nil {
|
||||||
payerOpenid = u.Openid
|
payerOpenid = u.Openid
|
||||||
}
|
}
|
||||||
c := configs.Get()
|
var cfg *wechat.WechatConfig
|
||||||
cfg := &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}
|
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)
|
errUpload := wechat.UploadVirtualShippingForBackground(ctx, cfg, tx.TransactionID, orderNo, payerOpenid, itemsDesc)
|
||||||
|
|
||||||
// 如果发货成功,或者微信提示已经发过货了(10060003),则标记本地订单为已履约
|
// 如果发货成功,或者微信提示已经发过货了(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) {
|
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)
|
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == aid) || (ic.ScopeType == 4 && ic.IssueID == iss)
|
||||||
if scopeOK {
|
if scopeOK {
|
||||||
|
effectApplied := false
|
||||||
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||||||
rw, _ := s.readDB.ActivityRewardSettings.WithContext(ctx).Where(s.readDB.ActivityRewardSettings.ID.Eq(log.RewardID)).First()
|
rw, _ := s.readDB.ActivityRewardSettings.WithContext(ctx).Where(s.readDB.ActivityRewardSettings.ID.Eq(log.RewardID)).First()
|
||||||
if rw != nil {
|
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 {
|
if prod, _ := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(rw.ProductID)).First(); prod != nil {
|
||||||
prodName = prod.Name
|
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{
|
} else {
|
||||||
"status": 2,
|
s.logger.Debug("道具卡-Lottery: 范围校验失败")
|
||||||
"used_draw_log_id": log.ID,
|
|
||||||
"used_activity_id": aid,
|
|
||||||
"used_issue_id": iss,
|
|
||||||
"used_at": now,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
} 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) {
|
func (s *DefaultStrategy) selectItemInternal(rewards []*model.ActivityRewardSettings, seedKey []byte, issueID int64, userID int64) (int64, map[string]any, error) {
|
||||||
// 统计有库存的奖品权重
|
// 统计所有奖品权重(不再过滤库存为0的项)
|
||||||
var total int64
|
var total int64
|
||||||
var validCount int
|
var validCount int
|
||||||
for _, r := range rewards {
|
for _, r := range rewards {
|
||||||
if r.Quantity != 0 {
|
total += int64(r.Weight)
|
||||||
total += int64(r.Weight)
|
validCount++
|
||||||
validCount++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
return 0, nil, fmt.Errorf("no weight: total_rewards=%d, valid_with_stock=%d", len(rewards), validCount)
|
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 acc int64
|
||||||
var picked int64
|
var picked int64
|
||||||
for _, r := range rewards {
|
for _, r := range rewards {
|
||||||
if r.Quantity == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
acc += int64(r.Weight)
|
acc += int64(r.Weight)
|
||||||
if rnd < acc {
|
if rnd < acc {
|
||||||
picked = r.ID
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,11 +11,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
@ -29,10 +31,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// FetchAndSyncOrders 从抖店 API 获取订单并同步到本地 (原有按用户同步逻辑)
|
// FetchAndSyncOrders 从抖店 API 获取订单并同步到本地 (按绑定用户同步)
|
||||||
FetchAndSyncOrders(ctx context.Context) (*SyncResult, error)
|
FetchAndSyncOrders(ctx context.Context) (*SyncResult, error)
|
||||||
// SyncShopOrders 同步店铺全量订单 (专供直播间等全扫描场景)
|
// SyncAllOrders 批量同步所有订单变更 (基于更新时间,不分状态)
|
||||||
SyncShopOrders(ctx context.Context, activityID int64) (*SyncResult, error)
|
SyncAllOrders(ctx context.Context, duration time.Duration) (*SyncResult, error)
|
||||||
// ListOrders 获取本地抖店订单列表
|
// ListOrders 获取本地抖店订单列表
|
||||||
ListOrders(ctx context.Context, page, pageSize int, status *int) ([]*model.DouyinOrders, int64, error)
|
ListOrders(ctx context.Context, page, pageSize int, status *int) ([]*model.DouyinOrders, int64, error)
|
||||||
// GetConfig 获取抖店配置
|
// GetConfig 获取抖店配置
|
||||||
@ -188,83 +190,7 @@ func (s *service) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncShopOrders 同步店铺全量订单 (专供直播间等全扫描场景)
|
// removed 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 抖店 API 响应结构
|
// 抖店 API 响应结构
|
||||||
type douyinOrderResponse struct {
|
type douyinOrderResponse struct {
|
||||||
@ -278,7 +204,8 @@ type DouyinOrderItem struct {
|
|||||||
ShopOrderID string `json:"shop_order_id"`
|
ShopOrderID string `json:"shop_order_id"`
|
||||||
OrderStatus int `json:"order_status"`
|
OrderStatus int `json:"order_status"`
|
||||||
UserID string `json:"user_id"`
|
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"`
|
PayTypeDesc string `json:"pay_type_desc"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
UserNickname string `json:"user_nickname"`
|
UserNickname string `json:"user_nickname"`
|
||||||
@ -346,10 +273,15 @@ func (s *service) fetchDouyinOrders(cookie string, params url.Values) ([]DouyinO
|
|||||||
|
|
||||||
var respData douyinOrderResponse
|
var respData douyinOrderResponse
|
||||||
if err := json.Unmarshal(body, &respData); err != nil {
|
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)
|
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 {
|
if respData.St != 0 && respData.Code != 0 {
|
||||||
return nil, fmt.Errorf("API 返回错误: %s (ST:%d CODE:%d)", respData.Msg, respData.St, respData.Code)
|
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) {
|
func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64, productID string) (isNew bool, isMatched bool) {
|
||||||
db := s.repo.GetDbW().WithContext(ctx)
|
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
|
var order model.DouyinOrders
|
||||||
err := db.Where("shop_order_id = ?", item.ShopOrderID).First(&order).Error
|
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)
|
fmt.Printf("[DEBUG] 抖店辅助关联成功: %s -> User %d\n", item.ShopOrderID, suggestUserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态与金额 (确保之前因解析失败导致的 0 金额被修复)
|
||||||
db.Model(&order).Updates(map[string]any{
|
db.Model(&order).Updates(map[string]any{
|
||||||
"order_status": item.OrderStatus,
|
"order_status": item.OrderStatus,
|
||||||
"remark": item.Remark,
|
"remark": item.Remark,
|
||||||
|
"actual_receive_amount": parseMoney(item.ActualReceiveAmount),
|
||||||
|
"actual_pay_amount": parseMoney(item.ActualPayAmount),
|
||||||
})
|
})
|
||||||
// 重要:同步内存状态,防止后续判断逻辑失效
|
// 重要:同步内存状态
|
||||||
order.OrderStatus = int32(item.OrderStatus)
|
order.OrderStatus = int32(item.OrderStatus)
|
||||||
order.Remark = item.Remark
|
order.Remark = item.Remark
|
||||||
} else {
|
} 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)
|
fmt.Printf("[DEBUG] 抖店新订单: %s, UserID: %s, Recommend: %s\n", item.ShopOrderID, item.UserID, localUserIDStr)
|
||||||
|
|
||||||
// 解析金额
|
amount := parseMoney(item.ActualReceiveAmount)
|
||||||
var amount int64
|
payAmount := parseMoney(item.ActualPayAmount)
|
||||||
if item.ActualReceiveAmount != "" {
|
|
||||||
if f, err := strconv.ParseFloat(strings.TrimSpace(item.ActualReceiveAmount), 64); err == nil {
|
|
||||||
amount = int64(f * 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算商品数量:如果指定了 productID,则只统计该商品的数量;否则使用总数量
|
// 计算商品数量:如果指定了 productID,则只统计该商品的数量;否则使用总数量
|
||||||
pCount := item.ProductCount
|
pCount := item.ProductCount
|
||||||
@ -429,11 +393,12 @@ func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestU
|
|||||||
|
|
||||||
order = model.DouyinOrders{
|
order = model.DouyinOrders{
|
||||||
ShopOrderID: item.ShopOrderID,
|
ShopOrderID: item.ShopOrderID,
|
||||||
DouyinProductID: productID, // 写入商品ID
|
DouyinProductID: productID, // 写入商品ID
|
||||||
ProductCount: pCount, // 写入计算后的商品数量
|
ProductCount: int32(pCount), // 写入计算后的商品数量
|
||||||
OrderStatus: int32(item.OrderStatus),
|
OrderStatus: int32(item.OrderStatus),
|
||||||
DouyinUserID: item.UserID,
|
DouyinUserID: item.UserID,
|
||||||
ActualReceiveAmount: amount,
|
ActualReceiveAmount: amount,
|
||||||
|
ActualPayAmount: payAmount,
|
||||||
PayTypeDesc: item.PayTypeDesc,
|
PayTypeDesc: item.PayTypeDesc,
|
||||||
Remark: item.Remark,
|
Remark: item.Remark,
|
||||||
UserNickname: item.UserNickname,
|
UserNickname: item.UserNickname,
|
||||||
@ -459,21 +424,31 @@ func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestU
|
|||||||
|
|
||||||
// ---- 统一处理:发放奖励 ----
|
// ---- 统一处理:发放奖励 ----
|
||||||
isMatched = order.LocalUserID != "" && order.LocalUserID != "0"
|
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 {
|
localUserID, _ := strconv.ParseInt(order.LocalUserID, 10, 64)
|
||||||
err := s.ticketSvc.GrantTicket(ctx, localUserID, "minesweeper", 1, "douyin_order", order.ID, "抖店订单奖励")
|
fmt.Printf("[DEBUG] 准备发放奖励: User: %d, ShopOrder: %s\n", localUserID, item.ShopOrderID)
|
||||||
if err == nil {
|
|
||||||
db.Model(&order).Update("reward_granted", 1)
|
if localUserID > 0 && s.ticketSvc != nil {
|
||||||
order.RewardGranted = 1
|
err := s.ticketSvc.GrantTicket(ctx, localUserID, "minesweeper", 1, "douyin_order", order.ID, "抖店订单奖励")
|
||||||
fmt.Printf("[DEBUG] 订单 %s 发放奖励成功\n", item.ShopOrderID)
|
if err == nil {
|
||||||
} else {
|
db.Model(&order).Update("reward_granted", 1)
|
||||||
fmt.Printf("[DEBUG] 订单 %s 发放奖励失败: %v\n", item.ShopOrderID, err)
|
order.RewardGranted = 1
|
||||||
|
fmt.Printf("[DEBUG] 订单 %s 发放奖励成功\n", item.ShopOrderID)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[DEBUG] 订单 %s 发放奖励失败: %v\n", item.ShopOrderID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
*/
|
||||||
|
|
||||||
return isNew, isMatched
|
return isNew, isMatched
|
||||||
}
|
}
|
||||||
@ -485,3 +460,55 @@ func min(a, b int) int {
|
|||||||
}
|
}
|
||||||
return b
|
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秒让服务完全启动
|
// 初始等待30秒让服务完全启动
|
||||||
time.Sleep(30 * time.Second)
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
|
firstRun := true
|
||||||
for {
|
for {
|
||||||
ctx := context.Background()
|
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 {
|
if err := svc.GrantLivestreamPrizes(ctx); err != nil {
|
||||||
l.Error("[定时发放] 发放直播奖品失败", zap.Error(err))
|
l.Error("[定时发放] 发放直播奖品失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 后置:按活动商品ID同步 (全量兜底) ==========
|
// ========== 核心:批量同步最近所有订单变更 (基于更新时间,不分状态) ==========
|
||||||
var activities []model.LivestreamActivities
|
// 首次运行同步最近 48 小时以修复潜在的历史遗漏,之后同步最近 1 小时
|
||||||
if err := repo.GetDbR().Where("status = ?", 1).Find(&activities).Error; err == nil && len(activities) > 0 {
|
syncDuration := 1 * time.Hour
|
||||||
l.Info("[抖店定时同步] 发现进行中的直播活动 (全量兜底)", zap.Int("count", len(activities)))
|
if firstRun {
|
||||||
for _, act := range activities {
|
syncDuration = 48 * time.Hour
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if res, err := svc.SyncAllOrders(ctx, syncDuration); err != nil {
|
||||||
// ========== 新增:自动补发扫雷游戏资格 ==========
|
l.Error("[定时同步] 全量同步失败", zap.Error(err))
|
||||||
if err := svc.GrantMinesweeperQualifications(ctx); err != nil {
|
} else {
|
||||||
l.Error("[定时补发] 补发扫雷资格失败", zap.Error(err))
|
l.Info("[定时同步] 全量同步完成", zap.String("info", res.DebugInfo))
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 新增:自动发放直播间奖品 ==========
|
|
||||||
if err := svc.GrantLivestreamPrizes(ctx); err != nil {
|
|
||||||
l.Error("[定时发放] 发放直播奖品失败", zap.Error(err))
|
|
||||||
}
|
}
|
||||||
|
firstRun = false
|
||||||
|
|
||||||
// ========== 新增:同步退款状态 ==========
|
// ========== 新增:同步退款状态 ==========
|
||||||
if err := svc.SyncRefundStatus(ctx); err != nil {
|
if err := svc.SyncRefundStatus(ctx); err != nil {
|
||||||
@ -131,6 +108,12 @@ func (s *service) GrantMinesweeperQualifications(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range users {
|
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)的订单
|
// 2. 查找该抖音ID下未关联(local_user_id=0 or empty)的订单
|
||||||
var orders []model.DouyinOrders
|
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 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 如果是已完成的订单(5),且未发奖,则补发
|
// 4. 如果是已支付待发货的订单(2),且未发奖,则补发
|
||||||
if order.OrderStatus == 5 && order.RewardGranted == 0 {
|
if order.OrderStatus == 2 && order.RewardGranted == 0 {
|
||||||
orderID := order.ID
|
orderID := order.ID
|
||||||
s.logger.Info("[自动补发] 开始补发扫雷资格", zap.Int64("user_id", u.ID), zap.String("shop_order_id", order.ShopOrderID))
|
s.logger.Info("[自动补发] 开始补发扫雷资格", zap.Int64("user_id", u.ID), zap.String("shop_order_id", order.ShopOrderID))
|
||||||
|
|
||||||
// 调用发奖服务
|
// 调用发奖服务
|
||||||
count := int64(1)
|
count := int64(1)
|
||||||
if order.ProductCount > 0 {
|
if order.ProductCount > 0 {
|
||||||
count = order.ProductCount
|
count = int64(order.ProductCount)
|
||||||
}
|
}
|
||||||
s.logger.Info("[自动补发] 发放数量", zap.Int64("count", count))
|
s.logger.Info("[自动补发] 发放数量", zap.Int64("count", count))
|
||||||
|
|
||||||
@ -223,7 +206,7 @@ func (s *service) GrantLivestreamPrizes(ctx context.Context) error {
|
|||||||
zap.String("prize", log.PrizeName),
|
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 {
|
if err != nil {
|
||||||
s.logger.Error("[自动发放] 发放失败", zap.Error(err))
|
s.logger.Error("[自动发放] 发放失败", zap.Error(err))
|
||||||
// 如果发放失败是库存原因等,可能需要告警。暂时不重试,等下个周期。
|
// 如果发放失败是库存原因等,可能需要告警。暂时不重试,等下个周期。
|
||||||
@ -231,6 +214,30 @@ func (s *service) GrantLivestreamPrizes(ctx context.Context) error {
|
|||||||
// 4. 更新发放状态
|
// 4. 更新发放状态
|
||||||
db.Model(&log).Update("is_granted", 1)
|
db.Model(&log).Update("is_granted", 1)
|
||||||
s.logger.Info("[自动发放] 发放成功", zap.Int64("log_id", log.ID))
|
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
|
return nil
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"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)
|
// 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) {
|
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)
|
// 1. Check if user has game tickets (do NOT deduct - will be done on match success)
|
||||||
var userTicket model.UserGameTickets
|
// For free mode, we skip this check
|
||||||
if err = s.db.GetDbR().Where("user_id = ? AND game_code = ? AND available > 0", userID, gameType).First(&userTicket).Error; err != nil {
|
if gameType != "minesweeper_free" {
|
||||||
return "", "", time.Time{}, fmt.Errorf("no available game tickets")
|
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
|
// 2. Generate unique ticket ID
|
||||||
ticket = fmt.Sprintf("GT%d%d", userID, time.Now().UnixNano())
|
ticket = fmt.Sprintf("GT%d%d", userID, time.Now().UnixNano())
|
||||||
|
|
||||||
// 3. Store ticket in Redis (for single-use validation)
|
// 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)
|
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
||||||
// Check for error when setting Redis key - CRITICAL FIX
|
// 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))
|
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)
|
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)
|
// 2. Check if ticket is still valid (not used)
|
||||||
// TODO: 临时跳过 Redis 验证,仅记录日志用于排查
|
// TODO: 临时跳过 Redis 验证,仅记录日志用于排查
|
||||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", claims.Ticket)
|
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 {
|
if err != nil {
|
||||||
s.logger.Warn("DEBUG: Ticket not found in Redis (SKIPPING validation temporarily)",
|
s.logger.Warn("DEBUG: Ticket not found in Redis (SKIPPING validation temporarily)",
|
||||||
zap.String("ticket", claims.Ticket),
|
zap.String("ticket", claims.Ticket),
|
||||||
@ -128,9 +134,19 @@ func (s *gameTokenService) ValidateToken(ctx context.Context, tokenString string
|
|||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
// 临时跳过验证,允许游戏继续
|
// 临时跳过验证,允许游戏继续
|
||||||
// return nil, fmt.Errorf("ticket not found or expired")
|
// return nil, fmt.Errorf("ticket not found or expired")
|
||||||
} else if storedUserID != fmt.Sprintf("%d", claims.UserID) {
|
} else {
|
||||||
s.logger.Warn("DEBUG: Ticket user mismatch", zap.String("stored", storedUserID), zap.Int64("claim_user", claims.UserID))
|
// Parse stored value "userID:gameType"
|
||||||
return nil, fmt.Errorf("ticket user mismatch")
|
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))
|
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"`
|
HasSeed bool `json:"has_seed"`
|
||||||
LenSeed int `json:"len_seed_master"`
|
LenSeed int `json:"len_seed_master"`
|
||||||
LenHash int `json:"len_seed_hash"`
|
LenHash int `json:"len_seed_hash"`
|
||||||
|
SeedHashHex string `json:"seed_hash_hex"` // 种子哈希的十六进制表示(可公开)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DrawReceipt 抽奖凭证
|
// DrawReceipt 抽奖凭证
|
||||||
@ -159,7 +160,7 @@ func (s *service) CreateActivity(ctx context.Context, input CreateActivityInput)
|
|||||||
StreamerContact: input.StreamerContact,
|
StreamerContact: input.StreamerContact,
|
||||||
AccessCode: accessCode,
|
AccessCode: accessCode,
|
||||||
DouyinProductID: input.DouyinProductID,
|
DouyinProductID: input.DouyinProductID,
|
||||||
TicketPrice: input.TicketPrice,
|
TicketPrice: int32(input.TicketPrice),
|
||||||
Status: 1,
|
Status: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +196,7 @@ func (s *service) UpdateActivity(ctx context.Context, id int64, input UpdateActi
|
|||||||
updates["douyin_product_id"] = input.DouyinProductID
|
updates["douyin_product_id"] = input.DouyinProductID
|
||||||
}
|
}
|
||||||
if input.TicketPrice != nil {
|
if input.TicketPrice != nil {
|
||||||
updates["ticket_price"] = *input.TicketPrice
|
updates["ticket_price"] = int32(*input.TicketPrice)
|
||||||
}
|
}
|
||||||
if input.Status != nil {
|
if input.Status != nil {
|
||||||
updates["status"] = *input.Status
|
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) {
|
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. 获取可用奖品
|
// 1. 获取可用奖品
|
||||||
prizes, err := s.ListPrizes(ctx, input.ActivityID)
|
prizes, err := s.ListPrizes(ctx, input.ActivityID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -566,12 +578,19 @@ func (s *service) GetCommitmentSummary(ctx context.Context, activityID int64) (*
|
|||||||
return nil, fmt.Errorf("活动不存在: %w", err)
|
return nil, fmt.Errorf("活动不存在: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将种子哈希转为十六进制字符串
|
||||||
|
seedHashHex := ""
|
||||||
|
if len(activity.CommitmentSeedHash) > 0 {
|
||||||
|
seedHashHex = hex.EncodeToString(activity.CommitmentSeedHash)
|
||||||
|
}
|
||||||
|
|
||||||
return &CommitmentSummary{
|
return &CommitmentSummary{
|
||||||
SeedVersion: activity.CommitmentStateVersion,
|
SeedVersion: activity.CommitmentStateVersion,
|
||||||
Algo: activity.CommitmentAlgo,
|
Algo: activity.CommitmentAlgo,
|
||||||
HasSeed: len(activity.CommitmentSeedMaster) > 0,
|
HasSeed: len(activity.CommitmentSeedMaster) > 0,
|
||||||
LenSeed: len(activity.CommitmentSeedMaster),
|
LenSeed: len(activity.CommitmentSeedMaster),
|
||||||
LenHash: len(activity.CommitmentSeedHash),
|
LenHash: len(activity.CommitmentSeedHash),
|
||||||
|
SeedHashHex: seedHashHex,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -324,23 +324,25 @@ func (d *DynamicConfig) GetCOS(ctx context.Context) COSConfig {
|
|||||||
|
|
||||||
// GetWechat 获取微信小程序配置
|
// GetWechat 获取微信小程序配置
|
||||||
func (d *DynamicConfig) GetWechat(ctx context.Context) WechatConfig {
|
func (d *DynamicConfig) GetWechat(ctx context.Context) WechatConfig {
|
||||||
|
staticCfg := configs.Get().Wechat
|
||||||
return WechatConfig{
|
return WechatConfig{
|
||||||
AppID: d.Get(ctx, KeyWechatAppID),
|
AppID: d.GetWithFallback(ctx, KeyWechatAppID, staticCfg.AppID),
|
||||||
AppSecret: d.Get(ctx, KeyWechatAppSecret),
|
AppSecret: d.GetWithFallback(ctx, KeyWechatAppSecret, staticCfg.AppSecret),
|
||||||
LotteryResultTemplateID: d.Get(ctx, KeyWechatLotteryResultTemplateID),
|
LotteryResultTemplateID: d.GetWithFallback(ctx, KeyWechatLotteryResultTemplateID, staticCfg.LotteryResultTemplateID),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWechatPay 获取微信支付配置
|
// GetWechatPay 获取微信支付配置
|
||||||
func (d *DynamicConfig) GetWechatPay(ctx context.Context) WechatPayConfig {
|
func (d *DynamicConfig) GetWechatPay(ctx context.Context) WechatPayConfig {
|
||||||
|
staticCfg := configs.Get().WechatPay
|
||||||
return WechatPayConfig{
|
return WechatPayConfig{
|
||||||
MchID: d.Get(ctx, KeyWechatPayMchID),
|
MchID: d.GetWithFallback(ctx, KeyWechatPayMchID, staticCfg.MchID),
|
||||||
SerialNo: d.Get(ctx, KeyWechatPaySerialNo),
|
SerialNo: d.GetWithFallback(ctx, KeyWechatPaySerialNo, staticCfg.SerialNo),
|
||||||
PrivateKey: d.Get(ctx, KeyWechatPayPrivateKey),
|
PrivateKey: d.Get(ctx, KeyWechatPayPrivateKey), // Key content only, no fallback to file path
|
||||||
ApiV3Key: d.Get(ctx, KeyWechatPayApiV3Key),
|
ApiV3Key: d.GetWithFallback(ctx, KeyWechatPayApiV3Key, staticCfg.ApiV3Key),
|
||||||
NotifyURL: d.Get(ctx, KeyWechatPayNotifyURL),
|
NotifyURL: d.GetWithFallback(ctx, KeyWechatPayNotifyURL, staticCfg.NotifyURL),
|
||||||
PublicKeyID: d.Get(ctx, KeyWechatPayPublicKeyID),
|
PublicKeyID: d.GetWithFallback(ctx, KeyWechatPayPublicKeyID, staticCfg.PublicKeyID),
|
||||||
PublicKey: d.Get(ctx, KeyWechatPayPublicKey),
|
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)
|
queryAmount.Select("COALESCE(SUM(total_amount), 0)").Scan(&orderAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 实时统计邀请数据(有效邀请:被邀请人有消费记录)
|
// 2. 实时统计邀请数据
|
||||||
// 同样应用“已开奖”逻辑过滤
|
|
||||||
var inviteCount int64
|
var inviteCount int64
|
||||||
if targetActivityID > 0 {
|
if targetActivityID > 0 {
|
||||||
|
// 根据配置计算:如果任务限定了活动,则只统计在该活动中有有效抽奖的人数(有效转化)
|
||||||
db.Raw(`
|
db.Raw(`
|
||||||
SELECT COUNT(DISTINCT ui.invitee_id)
|
SELECT COUNT(DISTINCT ui.invitee_id)
|
||||||
FROM user_invites ui
|
FROM user_invites ui
|
||||||
@ -362,13 +362,8 @@ func (s *service) GetUserProgress(ctx context.Context, userID int64, taskID int6
|
|||||||
)
|
)
|
||||||
`, userID, targetActivityID).Scan(&inviteCount)
|
`, userID, targetActivityID).Scan(&inviteCount)
|
||||||
} else {
|
} else {
|
||||||
db.Raw(`
|
// 全量统计(注册即计入):为了与前端“邀请记录”页面的总数对齐(针对全局任务)
|
||||||
SELECT COUNT(DISTINCT ui.invitee_id)
|
db.Model(&model.UserInvites{}).Where("inviter_id = ?", userID).Count(&inviteCount)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 首单判断
|
// 3. 首单判断
|
||||||
|
|||||||
@ -658,15 +658,37 @@ func TestGetUserProgress_ActivityFilter_Integration(t *testing.T) {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
status INTEGER NOT NULL DEFAULT 1,
|
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,
|
actual_amount INTEGER NOT NULL DEFAULT 0,
|
||||||
remark TEXT,
|
remark TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at DATETIME
|
||||||
);`).Error; err != nil {
|
);`).Error; err != nil {
|
||||||
t.Fatalf("创建 orders 表失败: %v", err)
|
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
|
// Create user_invites table
|
||||||
if !db.Migrator().HasTable("user_invites") {
|
if !db.Migrator().HasTable("user_invites") {
|
||||||
if err := db.Exec(`CREATE TABLE 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,
|
invitee_id INTEGER NOT NULL,
|
||||||
accumulated_amount INTEGER NOT NULL DEFAULT 0,
|
accumulated_amount INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at DATETIME
|
||||||
);`).Error; err != nil {
|
);`).Error; err != nil {
|
||||||
t.Fatalf("创建 user_invites 表失败: %v", err)
|
t.Fatalf("创建 user_invites 表失败: %v", err)
|
||||||
}
|
}
|
||||||
@ -699,28 +722,49 @@ func TestGetUserProgress_ActivityFilter_Integration(t *testing.T) {
|
|||||||
userID := int64(999)
|
userID := int64(999)
|
||||||
|
|
||||||
// 2. 插入不同类型的订单
|
// 2. 插入不同类型的订单
|
||||||
// 订单 A: 匹配活动 100
|
// 准备活动和期数数据
|
||||||
db.Exec("INSERT INTO orders (user_id, status, actual_amount, remark) VALUES (?, 2, 100, ?)", userID, "activity:100|count:1")
|
db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (10, 100)")
|
||||||
// 订单 B: 匹配活动 200 (不应被统计)
|
db.Exec("INSERT INTO activity_issues (id, activity_id) VALUES (20, 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")
|
|
||||||
|
|
||||||
// 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)
|
progress, err := svc.GetUserProgress(context.Background(), userID, taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetUserProgress 失败: %v", err)
|
t.Fatalf("GetUserProgress 失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 验证
|
// 6. 验证
|
||||||
if progress.OrderCount != 1 {
|
if progress.OrderCount != 1 {
|
||||||
t.Errorf("OrderCount 错误: 期望 1, 实际 %d", progress.OrderCount)
|
t.Errorf("OrderCount 错误: 期望 1, 实际 %d", progress.OrderCount)
|
||||||
}
|
}
|
||||||
if progress.OrderAmount != 100 {
|
if progress.OrderAmount != 100 {
|
||||||
t.Errorf("OrderAmount 错误: 期望 100, 实际 %d", progress.OrderAmount)
|
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