Zuncle 3db52af4b6 feat(activity): 重构福利活动并支持统一奖池
对齐福利活动新库表结构,支持商品、道具卡和优惠券统一建奖、开奖与中奖记录。
同时新增福利活动测试命令行工具,便于模拟消费、参与活动并验证完整开奖链路。
2026-04-29 17:21:11 +08:00

285 lines
7.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"bufio"
"context"
"flag"
"fmt"
"os"
"strconv"
"strings"
"bindbox-game/configs"
"bindbox-game/internal/pkg/env"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/model"
usersvc "bindbox-game/internal/service/user"
welfaresvc "bindbox-game/internal/service/welfare_activity"
"github.com/eiannone/keyboard"
)
func main() {
operator := flag.String("operator", "cli", "操作人标识")
flag.Parse()
env.Active()
configs.Init()
logg, err := logger.NewCustomLogger(logger.WithOutputInConsole(), logger.WithDebugLevel())
if err != nil {
fmt.Printf("[ERR] 初始化日志失败: %v\n", err)
os.Exit(1)
}
repo, err := mysql.New()
if err != nil {
fmt.Printf("[ERR] 初始化 MySQL 失败: %v\n", err)
os.Exit(1)
}
defer repo.DbRClose()
defer repo.DbWClose()
userSvc := usersvc.New(logg, repo)
welfareSvc := welfaresvc.New(logg, repo)
ctx := context.Background()
reader := bufio.NewReader(os.Stdin)
fmt.Printf("[WARN] 当前环境: %s\n", env.Active().Value())
fmt.Println("[WARN] 该工具会写入真实已支付测试订单,仅用于 dev/fat/uat")
userID, err := promptInt64(reader, "请输入用户ID")
if err != nil {
fmt.Printf("[ERR] 读取用户ID失败: %v\n", err)
os.Exit(1)
}
user, err := userSvc.GetProfile(ctx, userID)
if err != nil || user == nil {
fmt.Printf("[ERR] 用户不存在: %v\n", err)
os.Exit(1)
}
fmt.Println("[INFO] 用户信息")
fmt.Printf("- 用户名: %s\n", fallback(user.Nickname, "-"))
fmt.Printf("- 用户ID: %d\n", user.ID)
fmt.Printf("- 手机号: %s\n", fallback(user.Mobile, "-"))
packages, err := loadActivePackages(ctx, repo)
if err != nil {
fmt.Printf("[ERR] 加载活动次卡套餐失败: %v\n", err)
os.Exit(1)
}
if len(packages) == 0 {
fmt.Println("[WARN] 当前没有可用的活动次卡套餐")
return
}
selectedPackage, err := selectPackage(packages)
if err != nil {
fmt.Printf("[ERR] 选择套餐失败: %v\n", err)
os.Exit(1)
}
count, err := promptInt32(reader, "请输入购买次数")
if err != nil {
fmt.Printf("[ERR] 读取购买次数失败: %v\n", err)
os.Exit(1)
}
purchase, err := userSvc.CreatePaidGamePassOrderForTest(ctx, usersvc.CreatePaidGamePassOrderForTestInput{
UserID: user.ID,
PackageID: selectedPackage.ID,
Count: count,
Operator: *operator,
})
if err != nil {
fmt.Printf("[ERR] 模拟消费失败: %v\n", err)
os.Exit(1)
}
fmt.Println("[OK] 模拟消费成功")
fmt.Printf("- 订单号: %s\n", purchase.OrderNo)
fmt.Printf("- 套餐: %s\n", purchase.PackageName)
fmt.Printf("- 次数: %d\n", purchase.Count)
fmt.Printf("- 支付金额: %s\n", formatAmount(purchase.TotalAmount))
fmt.Printf("- 支付时间: %s\n", purchase.PaidAt.Format("2006-01-02 15:04:05"))
activities, err := welfareSvc.ListJoinableActivitiesForUser(ctx, user.ID)
if err != nil {
fmt.Printf("[ERR] 加载福利活动失败: %v\n", err)
os.Exit(1)
}
if len(activities) == 0 {
fmt.Println("[WARN] 当前没有可选的进行中福利活<E588A9><E6B4BB>")
return
}
selected, err := selectActivity(activities)
if err != nil {
fmt.Printf("[ERR] 选择活动失败: %v\n", err)
os.Exit(1)
}
fmt.Println("[INFO] 已选择福利活动")
fmt.Printf("- 活动ID: %d\n", selected.ActivityID)
fmt.Printf("- 活动名称: %s\n", selected.Title)
fmt.Printf("- 当前消费/门槛: %s/%s\n", formatAmount(selected.CurrentPaid), formatAmount(selected.ThresholdAmount))
if err := welfareSvc.Join(ctx, selected.ActivityID, user.ID); err != nil {
fmt.Printf("[ERR] 参与失败: %v\n", err)
os.Exit(1)
}
fmt.Println("[OK] 参与成功")
fmt.Printf("- 用户ID: %d\n", user.ID)
fmt.Printf("- 活动ID: %d\n", selected.ActivityID)
fmt.Printf("- 开奖时间: %s\n", selected.DrawTime.Format("2006-01-02 15:04:05"))
fmt.Println("[INFO] 后续请等待系统自动开奖,再登录验证中奖、订单、背包和中奖名单")
}
func promptInt64(reader *bufio.Reader, label string) (int64, error) {
fmt.Printf("%s: ", label)
text, err := reader.ReadString('\n')
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(text), 10, 64)
}
func promptInt32(reader *bufio.Reader, label string) (int32, error) {
value, err := promptInt64(reader, label)
if err != nil {
return 0, err
}
return int32(value), nil
}
func loadActivePackages(ctx context.Context, repo mysql.Repo) ([]*model.GamePassPackages, error) {
var packages []*model.GamePassPackages
err := repo.GetDbR().WithContext(ctx).
Where("status = 1 AND deleted_at IS NULL").
Order("sort_order DESC, id ASC").
Find(&packages).Error
return packages, err
}
func selectPackage(items []*model.GamePassPackages) (*model.GamePassPackages, error) {
if err := keyboard.Open(); err != nil {
return nil, err
}
defer keyboard.Close()
index := 0
for {
fmt.Println()
fmt.Println("请选择活动次卡套餐(↑↓ 选择Enter 确认Esc 退出)")
for i, item := range items {
prefix := " "
if i == index {
prefix = "> "
}
activityScope := "全局"
if item.ActivityID > 0 {
activityScope = fmt.Sprintf("活动ID:%d", item.ActivityID)
}
fmt.Printf("%s%s | 套餐ID:%d | %s | %d次 | %s\n", prefix, item.Name, item.ID, activityScope, item.PassCount, formatAmount(item.Price))
}
char, key, err := keyboard.GetKey()
if err != nil {
return nil, err
}
switch key {
case keyboard.KeyArrowUp:
if index > 0 {
index--
}
case keyboard.KeyArrowDown:
if index < len(items)-1 {
index++
}
case keyboard.KeyEnter:
return items[index], nil
case keyboard.KeyEsc, keyboard.KeyCtrlC:
return nil, fmt.Errorf("已取消选择")
default:
if char == 0 {
continue
}
}
}
}
func selectActivity(items []welfaresvc.JoinableActivityItem) (*welfaresvc.JoinableActivityItem, error) {
if err := keyboard.Open(); err != nil {
return nil, err
}
defer keyboard.Close()
index := 0
for {
fmt.Println()
fmt.Println("请选择福利活动(↑↓ 选择Enter 确认Esc 退出)")
for i, item := range items {
prefix := " "
if i == index {
prefix = "> "
}
status := "未达标"
if item.Joined {
status = "已参与"
} else if item.CanJoin {
status = "可参与"
}
fmt.Printf("%s%s %s %s/%s %s\n", prefix, item.Title, typeLabel(item.Type), formatAmount(item.CurrentPaid), formatAmount(item.ThresholdAmount), status)
}
char, key, err := keyboard.GetKey()
if err != nil {
return nil, err
}
switch key {
case keyboard.KeyArrowUp:
if index > 0 {
index--
}
case keyboard.KeyArrowDown:
if index < len(items)-1 {
index++
}
case keyboard.KeyEnter:
return &items[index], nil
case keyboard.KeyEsc, keyboard.KeyCtrlC:
return nil, fmt.Errorf("已取消选择")
default:
if char == 0 {
continue
}
}
}
}
func formatAmount(cents int64) string {
return fmt.Sprintf("%.2f", float64(cents)/100)
}
func typeLabel(t string) string {
switch t {
case welfaresvc.TypeWeekly:
return "每周福利"
case welfaresvc.TypeMonthly:
return "每月福利"
default:
return "每日福利"
}
}
func fallback(v, d string) string {
if strings.TrimSpace(v) == "" {
return d
}
return v
}