feat: 保存当前开发进度 - 直播抽奖验证功能

This commit is contained in:
邹方成 2026-01-18 01:55:54 +08:00
parent b21e2db8ef
commit 5ad2f4ace3
97 changed files with 7880 additions and 2110 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,21 +0,0 @@
{"level":"info","time":"2026-01-08 00:53:15","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
 ____ _ _ _ ____
 | __ ) (_) _ __ __| | | |__ ___ __ __ / ___| __ _ _ __ ___ ___
 | _ \ | | | '_ \ / _` | | '_ \ / _ \ \ \/ / | | _ / _` | | '_ ` _ \ / _ \
 | |_) | | | | | | | | (_| | | |_) | | (_) | > < | |_| | | (_| | | | | | | | | __/
 |____/ |_| |_| |_| \__,_| |_.__/ \___/ /_/\_\ \____| \__,_| |_| |_| |_| \___|
▌ 客户项目: 盲盒游戏
▌ 项目版本: Release-2025111111
▌ 启动时间: 2026-01-08 00:53:15
▌ 运行环境: darwin go1.24.2
▌ 服务端口: [:9991]
▌ 服务配置: [fat]
▌ 数据库连接: ✔ 已建立
{"level":"info","time":"2026-01-08 00:53:15","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 00:53:15","caller":"logger/logger.go:309","msg":"对对碰自动开奖: 后台任务已启动","domain":"mini-chat[fat]"}
{"level":"fatal","time":"2026-01-08 00:53:15","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
{"level":"info","time":"2026-01-08 00:53:15","caller":"logger/logger.go:309","msg":"[抖店定时同步] 定时任务已启动","domain":"mini-chat[fat]"}
exit status 1

View File

@ -1,127 +0,0 @@
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
 ____ _ _ _ ____
 | __ ) (_) _ __ __| | | |__ ___ __ __ / ___| __ _ _ __ ___ ___
 | _ \ | | | '_ \ / _` | | '_ \ / _ \ \ \/ / | | _ / _` | | '_ ` _ \ / _ \
 | |_) | | | | | | | | (_| | | |_) | | (_) | > < | |_| | | (_| | | | | | | | | __/
 |____/ |_| |_| |_| \__,_| |_.__/ \___/ /_/\_\ \____| \__,_| |_| |_| |_| \___|
▌ 客户项目: 盲盒游戏
▌ 项目版本: Release-2025111111
▌ 启动时间: 2026-01-08 01:00:31
▌ 运行环境: darwin go1.24.2
▌ 服务端口: [:9991]
▌ 服务配置: [fat]
▌ 数据库连接: ✔ 已建立
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"对对碰自动开奖: 后台任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"[抖店定时同步] 定时任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2026-01-08 01:00:31","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"error","time":"2026-01-08 01:00:32","caller":"logger/logger.go:327","msg":"解密配置失败","domain":"mini-chat[fat]","key":"douyin.app_secret","error":"ciphertext is not a multiple of the block size"}
{"level":"info","time":"2026-01-08 01:00:32","caller":"logger/logger.go:309","msg":"动态配置加载完成","domain":"mini-chat[fat]","count":26}
{"level":"info","time":"2026-01-08 01:00:44","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=O20260107213341575","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:45","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":4002,"rows":0}
{"level":"info","time":"2026-01-08 01:00:45","caller":"logger/logger.go:309","msg":"refund restore game_pass success: order=O20260107213341575 gp_id=66 count=1","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:45","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=O20260107213340076","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:45","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":4001,"rows":0}
{"level":"info","time":"2026-01-08 01:00:45","caller":"logger/logger.go:309","msg":"refund restore game_pass success: order=O20260107213340076 gp_id=66 count=1","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:45","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=O20260107213339935","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:45","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":4000,"rows":0}
{"level":"info","time":"2026-01-08 01:00:46","caller":"logger/logger.go:309","msg":"refund restore game_pass success: order=O20260107213339935 gp_id=66 count=1","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:46","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=O20260107213338161","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:46","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3999,"rows":0}
{"level":"info","time":"2026-01-08 01:00:46","caller":"logger/logger.go:309","msg":"refund restore game_pass success: order=O20260107213338161 gp_id=66 count=1","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:46","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=O20260107213337885","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:46","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3998,"rows":0}
{"level":"info","time":"2026-01-08 01:00:47","caller":"logger/logger.go:309","msg":"refund restore game_pass success: order=O20260107213337885 gp_id=66 count=1","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:49","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3997,"rows":0}
{"level":"info","time":"2026-01-08 01:00:51","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3996,"rows":0}
{"level":"info","time":"2026-01-08 01:00:52","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3995,"rows":0}
{"level":"info","time":"2026-01-08 01:00:52","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=GP202601071823419722","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:53","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3994,"rows":0}
{"level":"info","time":"2026-01-08 01:00:53","caller":"logger/logger.go:309","msg":"refund restore game_pass success: order=GP202601071823419722 game_pass_id=65","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:54","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3993,"rows":0}
{"level":"info","time":"2026-01-08 01:00:54","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=O20260107182203580","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:54","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3992,"rows":0}
{"level":"info","time":"2026-01-08 01:00:54","caller":"logger/logger.go:309","msg":"refund restore game_pass success: order=O20260107182203580 gp_id=64 count=1","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:56","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3991,"rows":0}
{"level":"info","time":"2026-01-08 01:00:57","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3984,"rows":0}
{"level":"info","time":"2026-01-08 01:00:57","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=RG20260107092128879615","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:00:57","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3973,"rows":0}
{"level":"info","time":"2026-01-08 01:00:59","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3972,"rows":0}
{"level":"info","time":"2026-01-08 01:01:00","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3971,"rows":0}
{"level":"info","time":"2026-01-08 01:01:00","caller":"logger/logger.go:309","msg":"refund: ActualAmount=0, skip wechat refund: order=GP202601070918119626","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:01:00","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3969,"rows":0}
{"level":"info","time":"2026-01-08 01:01:01","caller":"logger/logger.go:309","msg":"refund restore game_pass success: order=GP202601070918119626 game_pass_id=62","domain":"mini-chat[fat]"}
{"level":"debug","time":"2026-01-08 01:01:01","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:01:01"}
{"level":"info","time":"2026-01-08 01:01:01","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:03:14.807+08:00","last_settled":"2026-01-08T01:00:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:03:14","now":"2026-01-08 01:01:01","skip":true}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:02:59.385+08:00","last_settled":"2026-01-08T00:59:59.385+08:00"}
{"level":"info","time":"2026-01-08 01:01:02","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3968,"rows":0}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:02:59","now":"2026-01-08 01:01:01","skip":true}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:03:44.807+08:00","last_settled":"2026-01-08T00:58:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:03:44","now":"2026-01-08 01:01:01","skip":true}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:05:14.807+08:00","last_settled":"2026-01-08T01:00:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:05:14","now":"2026-01-08 01:01:01","skip":true}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 01:01:01","skip":true}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:01:01","skip":true}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:01:01","skip":true}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 01:01:01","skip":true}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:01:02","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:01:01","skip":true}
{"level":"info","time":"2026-01-08 01:01:03","caller":"logger/logger.go:309","msg":"清理一番赏占位成功","domain":"mini-chat[fat]","order_id":3965,"rows":0}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:01:06","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:01:06","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
2026/01/08 01:01:06 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_spending.go:113 Error 1054 (42S22): Unknown column 'orders.activity_id' in 'on clause'
[11.786ms] [rows:-] SELECT
orders.user_id,
SUM(orders.actual_amount) as total_amount,
COUNT(orders.id) as order_count,
SUM(orders.discount_amount) as total_discount,
SUM(orders.points_amount) as total_points,
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
SUM(CASE WHEN activities.play_type = 'ichiban' THEN orders.actual_amount ELSE 0 END) as ichiban_spending,
SUM(CASE WHEN activities.play_type = 'ichiban' THEN 1 ELSE 0 END) as ichiban_count,
SUM(CASE WHEN activities.play_type IN ('infinite', 'box') THEN orders.actual_amount ELSE 0 END) as infinite_spending,
SUM(CASE WHEN activities.play_type IN ('infinite', 'box') THEN 1 ELSE 0 END) as infinite_count,
SUM(CASE WHEN activities.play_type = 'matching' THEN orders.actual_amount ELSE 0 END) as matching_spending,
SUM(CASE WHEN activities.play_type = 'matching' THEN 1 ELSE 0 END) as matching_count
FROM `orders` LEFT JOIN activities ON activities.id = orders.activity_id WHERE orders.status = 2 AND orders.created_at >= '2026-01-01 01:01:06.706' AND orders.created_at <= '2026-01-08 01:01:06.706' GROUP BY `orders`.`user_id` ORDER BY total_amount DESC LIMIT 500
{"level":"debug","time":"2026-01-08 01:01:31","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:01:31"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:03:14.807+08:00","last_settled":"2026-01-08T01:00:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:03:14","now":"2026-01-08 01:01:31","skip":true}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:02:59.385+08:00","last_settled":"2026-01-08T00:59:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:02:59","now":"2026-01-08 01:01:31","skip":true}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:03:44.807+08:00","last_settled":"2026-01-08T00:58:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:03:44","now":"2026-01-08 01:01:31","skip":true}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:05:14.807+08:00","last_settled":"2026-01-08T01:00:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:05:14","now":"2026-01-08 01:01:31","skip":true}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 01:01:31","skip":true}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:01:31","skip":true}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:01:31","skip":true}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 01:01:31","skip":true}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:01:32","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:01:31","skip":true}
signal: killed

View File

@ -1,397 +0,0 @@
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
 ____ _ _ _ ____
 | __ ) (_) _ __ __| | | |__ ___ __ __ / ___| __ _ _ __ ___ ___
 | _ \ | | | '_ \ / _` | | '_ \ / _ \ \ \/ / | | _ / _` | | '_ ` _ \ / _ \
 | |_) | | | | | | | | (_| | | |_) | | (_) | > < | |_| | | (_| | | | | | | | | __/
 |____/ |_| |_| |_| \__,_| |_.__/ \___/ /_/\_\ \____| \__,_| |_| |_| |_| \___|
▌ 客户项目: 盲盒游戏
▌ 项目版本: Release-2025111111
▌ 启动时间: 2026-01-08 01:32:46
▌ 运行环境: darwin go1.24.2
▌ 服务端口: [:9991]
▌ 服务配置: [fat]
▌ 数据库连接: ✔ 已建立
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"对对碰自动开奖: 后台任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"[抖店定时同步] 定时任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"error","time":"2026-01-08 01:32:46","caller":"logger/logger.go:327","msg":"解密配置失败","domain":"mini-chat[fat]","key":"douyin.app_secret","error":"ciphertext is not a multiple of the block size"}
{"level":"info","time":"2026-01-08 01:32:46","caller":"logger/logger.go:309","msg":"动态配置加载完成","domain":"mini-chat[fat]","count":26}
{"level":"info","time":"2026-01-08 01:33:08","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-01 01:33:08.823091 +0800 CST m=-604777.351307833, end=2026-01-08 01:33:08.823091 +0800 CST m=+22.648692167, type=7d","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:08","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:09","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-01 01:33:09.794643 +0800 CST m=-604776.379748124, end=2026-01-08 01:33:09.794643 +0800 CST m=+23.620251876, type=7d","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:09","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:10","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-01 01:33:10.404066 +0800 CST m=-604775.770320333, end=2026-01-08 01:33:10.404066 +0800 CST m=+24.229679667, type=7d","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:10","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:11","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-01 01:33:11.624776 +0800 CST m=-604774.549601249, end=2026-01-08 01:33:11.624776 +0800 CST m=+25.450398751, type=7d","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:11","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:33:16"}
{"level":"info","time":"2026-01-08 01:33:16","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:33:59.385+08:00","last_settled":"2026-01-08T01:30:59.385+08:00"}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:33:59","now":"2026-01-08 01:33:16","skip":true}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:33:29.385+08:00","last_settled":"2026-01-08T01:30:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:33:29","now":"2026-01-08 01:33:16","skip":true}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:33:59.385+08:00","last_settled":"2026-01-08T01:28:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:33:59","now":"2026-01-08 01:33:16","skip":true}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:35:59.385+08:00","last_settled":"2026-01-08T01:30:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:35:59","now":"2026-01-08 01:33:16","skip":true}
{"level":"debug","time":"2026-01-08 01:33:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:34:29.385+08:00","last_settled":"2026-01-08T01:24:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:34:29","now":"2026-01-08 01:33:16","skip":true}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:33:16","skip":true}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:33:16","skip":true}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:33:16","skip":true}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:33:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:33:16","skip":true}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:33:21","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:33:21","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:33:21","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:22","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:22","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:33:22","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
2026/01/08 01:33:27 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_activity.go:269 Error 1054 (42S22): Unknown column 'products.image' in 'field list'
[10.836ms] [rows:-] SELECT
activity_draw_logs.id,
activity_draw_logs.user_id,
users.nickname,
users.avatar,
activity_reward_settings.product_id,
products.name as product_name,
products.image as product_image,
products.price as product_price,
orders.actual_amount as order_amount,
activity_draw_logs.created_at
FROM `activity_draw_logs` JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id LEFT JOIN users ON users.id = activity_draw_logs.user_id LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id LEFT JOIN products ON products.id = activity_reward_settings.product_id LEFT JOIN orders ON orders.id = activity_draw_logs.order_id WHERE activity_issues.activity_id = 88 ORDER BY activity_draw_logs.id DESC LIMIT 10
{"level":"error","time":"2026-01-08 01:33:27","caller":"logger/logger.go:327","msg":"GetActivityLogs error: Error 1054 (42S22): Unknown column 'products.image' in 'field list'","domain":"mini-chat[fat]"}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:33:46"}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:33:59.385+08:00","last_settled":"2026-01-08T01:30:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:33:59","now":"2026-01-08 01:33:46","skip":true}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:36:44.808+08:00","last_settled":"2026-01-08T01:33:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:36:44","now":"2026-01-08 01:33:46","skip":true}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:33:59.385+08:00","last_settled":"2026-01-08T01:28:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:33:59","now":"2026-01-08 01:33:46","skip":true}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:35:59.385+08:00","last_settled":"2026-01-08T01:30:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:35:59","now":"2026-01-08 01:33:46","skip":true}
{"level":"debug","time":"2026-01-08 01:33:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:34:29.385+08:00","last_settled":"2026-01-08T01:24:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:34:29","now":"2026-01-08 01:33:46","skip":true}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:33:46","skip":true}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:33:46","skip":true}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:33:46","skip":true}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:33:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:33:46","skip":true}
2026/01/08 01:34:09 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_activity.go:269 Error 1054 (42S22): Unknown column 'products.image' in 'field list'
[11.527ms] [rows:-] SELECT
activity_draw_logs.id,
activity_draw_logs.user_id,
users.nickname,
users.avatar,
activity_reward_settings.product_id,
products.name as product_name,
products.image as product_image,
products.price as product_price,
orders.actual_amount as order_amount,
activity_draw_logs.created_at
FROM `activity_draw_logs` JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id LEFT JOIN users ON users.id = activity_draw_logs.user_id LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id LEFT JOIN products ON products.id = activity_reward_settings.product_id LEFT JOIN orders ON orders.id = activity_draw_logs.order_id WHERE activity_issues.activity_id = 88 ORDER BY activity_draw_logs.id DESC LIMIT 10
{"level":"error","time":"2026-01-08 01:34:09","caller":"logger/logger.go:327","msg":"GetActivityLogs error: Error 1054 (42S22): Unknown column 'products.image' in 'field list'","domain":"mini-chat[fat]"}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:34:16"}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:37:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:37:14","now":"2026-01-08 01:34:16","skip":true}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:36:44.808+08:00","last_settled":"2026-01-08T01:33:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:36:44","now":"2026-01-08 01:34:16","skip":true}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:34:16","skip":true}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:35:59.385+08:00","last_settled":"2026-01-08T01:30:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:35:59","now":"2026-01-08 01:34:16","skip":true}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:34:29.385+08:00","last_settled":"2026-01-08T01:24:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:34:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:34:29","now":"2026-01-08 01:34:16","skip":true}
{"level":"debug","time":"2026-01-08 01:34:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:34:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:34:16","skip":true}
{"level":"debug","time":"2026-01-08 01:34:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:34:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:34:16","skip":true}
{"level":"debug","time":"2026-01-08 01:34:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:34:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:34:16","skip":true}
{"level":"debug","time":"2026-01-08 01:34:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:34:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:34:16","skip":true}
{"level":"info","time":"2026-01-08 01:34:21","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"info","time":"2026-01-08 01:34:22","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:34:22","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:34:23","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:34:23","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:34:26","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:34:26","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:34:46"}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:37:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:37:14","now":"2026-01-08 01:34:46","skip":true}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:36:44.808+08:00","last_settled":"2026-01-08T01:33:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:36:44","now":"2026-01-08 01:34:46","skip":true}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:34:46","skip":true}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:35:59.385+08:00","last_settled":"2026-01-08T01:30:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:35:59","now":"2026-01-08 01:34:46","skip":true}
{"level":"debug","time":"2026-01-08 01:34:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:34:46","skip":true}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:34:46","skip":true}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:34:46","skip":true}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:34:46","skip":true}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:34:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:34:46","skip":true}
{"level":"info","time":"2026-01-08 01:34:54","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:34:54","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:35:16"}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:37:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:37:14","now":"2026-01-08 01:35:16","skip":true}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:36:44.808+08:00","last_settled":"2026-01-08T01:33:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:36:44","now":"2026-01-08 01:35:16","skip":true}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:35:16","skip":true}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:35:59.385+08:00","last_settled":"2026-01-08T01:30:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:35:59","now":"2026-01-08 01:35:16","skip":true}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:35:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:35:16","skip":true}
{"level":"debug","time":"2026-01-08 01:35:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:35:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:35:16","skip":true}
{"level":"debug","time":"2026-01-08 01:35:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:35:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:35:16","skip":true}
{"level":"debug","time":"2026-01-08 01:35:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:35:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:35:16","skip":true}
{"level":"debug","time":"2026-01-08 01:35:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:35:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:35:16","skip":true}
{"level":"info","time":"2026-01-08 01:35:26","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:35:31","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:35:31","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:35:46"}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:37:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:37:14","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:36:44.808+08:00","last_settled":"2026-01-08T01:33:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:36:44","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:35:59.385+08:00","last_settled":"2026-01-08T01:30:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:35:59","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:35:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:35:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:35:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:35:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:35:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:35:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T02:05:29.386+08:00","last_settled":"2026-01-08T01:35:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:35:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 02:05:29","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:35:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:35:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:35:46","skip":true}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:36:16"}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:37:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:37:14","now":"2026-01-08 01:36:16","skip":true}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:36:44.808+08:00","last_settled":"2026-01-08T01:33:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:36:44","now":"2026-01-08 01:36:16","skip":true}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:36:16","skip":true}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:41:14.808+08:00","last_settled":"2026-01-08T01:36:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:41:14","now":"2026-01-08 01:36:16","skip":true}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:36:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:36:16","skip":true}
{"level":"debug","time":"2026-01-08 01:36:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:36:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:36:16","skip":true}
{"level":"debug","time":"2026-01-08 01:36:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:36:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:36:16","skip":true}
{"level":"debug","time":"2026-01-08 01:36:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T02:05:29.386+08:00","last_settled":"2026-01-08T01:35:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:36:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 02:05:29","now":"2026-01-08 01:36:16","skip":true}
{"level":"debug","time":"2026-01-08 01:36:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:36:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:36:16","skip":true}
{"level":"info","time":"2026-01-08 01:36:23","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:23","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:24","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:24","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:24","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:24","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:25","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:26","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:31","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-08 00:00:00 +0800 CST, end=2026-01-08 23:59:59 +0800 CST, type=today","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:31","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:31","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"info","time":"2026-01-08 01:36:32","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-01 01:36:32.749472 +0800 CST m=-604573.445416541, end=2026-01-08 01:36:32.749472 +0800 CST m=+226.554583459, type=7d","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:36:32","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:36:35","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:36:35","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:36:46","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:36:46"}
{"level":"debug","time":"2026-01-08 01:36:46","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:36:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:37:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:36:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:37:14","now":"2026-01-08 01:36:46","skip":true}
{"level":"debug","time":"2026-01-08 01:36:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:36:44.808+08:00","last_settled":"2026-01-08T01:33:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:36:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:36:44","now":"2026-01-08 01:36:46","skip":false}
{"level":"debug","time":"2026-01-08 01:36:46","caller":"logger/logger.go:315","msg":"定时开奖: 查询订单范围","domain":"mini-chat[fat]","id":65,"last":"2026-01-08 01:33:44","now":"2026-01-08 01:36:46"}
{"level":"debug","time":"2026-01-08 01:36:46","caller":"logger/logger.go:315","msg":"定时开奖: 查询到订单","domain":"mini-chat[fat]","id":65,"count":0,"min":1}
{"level":"info","time":"2026-01-08 01:36:46","caller":"logger/logger.go:309","msg":"定时开奖: 人数满足,开始开奖处理","domain":"mini-chat[fat]","id":65}
{"level":"info","time":"2026-01-08 01:36:46","caller":"logger/logger.go:309","msg":"定时开奖: 更新活动下次结算时间","domain":"mini-chat[fat]","id":65,"last":"2026-01-08 01:36:46","next":"2026-01-08 01:39:46"}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:36:46","skip":true}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:41:14.808+08:00","last_settled":"2026-01-08T01:36:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:41:14","now":"2026-01-08 01:36:46","skip":true}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:36:46","skip":true}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:36:46","skip":true}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:36:46","skip":true}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T02:05:29.386+08:00","last_settled":"2026-01-08T01:35:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 02:05:29","now":"2026-01-08 01:36:46","skip":true}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:36:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:36:46","skip":true}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:37:16"}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:40:14.807+08:00","last_settled":"2026-01-08T01:37:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:40:14","now":"2026-01-08 01:37:16","skip":true}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:39:46.48+08:00","last_settled":"2026-01-08T01:36:46.48+08:00"}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:39:46","now":"2026-01-08 01:37:16","skip":true}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:37:16","skip":true}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:41:14.808+08:00","last_settled":"2026-01-08T01:36:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:41:14","now":"2026-01-08 01:37:16","skip":true}
{"level":"debug","time":"2026-01-08 01:37:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:37:16","skip":true}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:37:16","skip":true}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:37:16","skip":true}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T02:05:29.386+08:00","last_settled":"2026-01-08T01:35:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 02:05:29","now":"2026-01-08 01:37:16","skip":true}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:37:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:37:16","skip":true}
{"level":"info","time":"2026-01-08 01:37:24","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-01 01:37:24.748226 +0800 CST m=-604521.447255708, end=2026-01-08 01:37:24.748226 +0800 CST m=+278.552744292, type=7d","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:37:24","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:37:25","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-01 01:37:25.231434 +0800 CST m=-604520.964047291, end=2026-01-08 01:37:25.231434 +0800 CST m=+279.035952709, type=7d","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:37:25","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:37:25","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: start=2026-01-01 01:37:25.639354 +0800 CST m=-604520.556125666, end=2026-01-08 01:37:25.639354 +0800 CST m=+279.443874334, type=7d","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:37:25","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
2026/01/08 01:37:28 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_activity.go:269 Error 1054 (42S22): Unknown column 'products.image' in 'field list'
[16.963ms] [rows:-] SELECT
activity_draw_logs.id,
activity_draw_logs.user_id,
users.nickname,
users.avatar,
activity_reward_settings.product_id,
products.name as product_name,
products.image as product_image,
products.price as product_price,
orders.actual_amount as order_amount,
activity_draw_logs.created_at
FROM `activity_draw_logs` JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id LEFT JOIN users ON users.id = activity_draw_logs.user_id LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id LEFT JOIN products ON products.id = activity_reward_settings.product_id LEFT JOIN orders ON orders.id = activity_draw_logs.order_id WHERE activity_issues.activity_id = 89 ORDER BY activity_draw_logs.id DESC LIMIT 10
{"level":"error","time":"2026-01-08 01:37:28","caller":"logger/logger.go:327","msg":"GetActivityLogs error: Error 1054 (42S22): Unknown column 'products.image' in 'field list'","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:37:35","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:37:40","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:37:40","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:37:46","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:37:46"}
{"level":"debug","time":"2026-01-08 01:37:46","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:37:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:40:14.807+08:00","last_settled":"2026-01-08T01:37:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:37:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:40:14","now":"2026-01-08 01:37:46","skip":true}
{"level":"debug","time":"2026-01-08 01:37:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:39:46.48+08:00","last_settled":"2026-01-08T01:36:46.48+08:00"}
{"level":"debug","time":"2026-01-08 01:37:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:39:46","now":"2026-01-08 01:37:46","skip":true}
{"level":"debug","time":"2026-01-08 01:37:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:37:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:37:46","skip":true}
{"level":"info","time":"2026-01-08 01:37:47","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:41:14.808+08:00","last_settled":"2026-01-08T01:36:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:41:14","now":"2026-01-08 01:37:46","skip":true}
{"level":"info","time":"2026-01-08 01:37:47","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:37:46","skip":true}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:37:46","skip":true}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:37:46","skip":true}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T02:05:29.386+08:00","last_settled":"2026-01-08T01:35:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 02:05:29","now":"2026-01-08 01:37:46","skip":true}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:37:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:37:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:38:16"}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:40:14.807+08:00","last_settled":"2026-01-08T01:37:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:40:14","now":"2026-01-08 01:38:16","skip":true}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:39:46.48+08:00","last_settled":"2026-01-08T01:36:46.48+08:00"}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:39:46","now":"2026-01-08 01:38:16","skip":true}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:38:16","skip":true}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:41:14.808+08:00","last_settled":"2026-01-08T01:36:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:41:14","now":"2026-01-08 01:38:16","skip":true}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:38:16","skip":true}
{"level":"debug","time":"2026-01-08 01:38:16","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:38:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:38:16","skip":true}
{"level":"debug","time":"2026-01-08 01:38:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:38:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:38:16","skip":true}
{"level":"debug","time":"2026-01-08 01:38:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T02:05:29.386+08:00","last_settled":"2026-01-08T01:35:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:38:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 02:05:29","now":"2026-01-08 01:38:16","skip":true}
{"level":"debug","time":"2026-01-08 01:38:17","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:38:17","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:38:16","skip":true}
{"level":"info","time":"2026-01-08 01:38:40","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"info","time":"2026-01-08 01:38:44","caller":"logger/logger.go:309","msg":"SpendingLeaderboard range: ALL TIME","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:38:44","caller":"logger/logger.go:309","msg":"SpendingLeaderboard SQL done: count=0","domain":"mini-chat[fat]"}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:38:45","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:38:45","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:38:46"}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:40:14.807+08:00","last_settled":"2026-01-08T01:37:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:40:14","now":"2026-01-08 01:38:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:39:46.48+08:00","last_settled":"2026-01-08T01:36:46.48+08:00"}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:39:46","now":"2026-01-08 01:38:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:39:14.807+08:00","last_settled":"2026-01-08T01:34:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:39:14","now":"2026-01-08 01:38:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:41:14.808+08:00","last_settled":"2026-01-08T01:36:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:41:14","now":"2026-01-08 01:38:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:44:44.807+08:00","last_settled":"2026-01-08T01:34:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:38:46","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:44:44","now":"2026-01-08 01:38:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:38:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:38:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:40:44.808+08:00","last_settled":"2026-01-08T01:25:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:38:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:40:44","now":"2026-01-08 01:38:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T02:05:29.386+08:00","last_settled":"2026-01-08T01:35:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:38:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 02:05:29","now":"2026-01-08 01:38:46","skip":true}
{"level":"debug","time":"2026-01-08 01:38:47","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T02:27:59.386+08:00","last_settled":"2026-01-08T01:27:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:38:47","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 02:27:59","now":"2026-01-08 01:38:46","skip":true}

View File

@ -1,193 +0,0 @@
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
 ____ _ _ _ ____
 | __ ) (_) _ __ __| | | |__ ___ __ __ / ___| __ _ _ __ ___ ___
 | _ \ | | | '_ \ / _` | | '_ \ / _ \ \ \/ / | | _ / _` | | '_ ` _ \ / _ \
 | |_) | | | | | | | | (_| | | |_) | | (_) | > < | |_| | | (_| | | | | | | | | __/
 |____/ |_| |_| |_| \__,_| |_.__/ \___/ /_/\_\ \____| \__,_| |_| |_| |_| \___|
▌ 客户项目: 盲盒游戏
▌ 项目版本: Release-2025111111
▌ 启动时间: 2026-01-08 01:15:05
▌ 运行环境: darwin go1.24.2
▌ 服务端口: [:9991]
▌ 服务配置: [fat]
▌ 数据库连接: ✔ 已建立
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"对对碰自动开奖: 后台任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"[抖店定时同步] 定时任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2026-01-08 01:15:05","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"error","time":"2026-01-08 01:15:06","caller":"logger/logger.go:327","msg":"解密配置失败","domain":"mini-chat[fat]","key":"douyin.app_secret","error":"ciphertext is not a multiple of the block size"}
{"level":"info","time":"2026-01-08 01:15:06","caller":"logger/logger.go:309","msg":"动态配置加载完成","domain":"mini-chat[fat]","count":26}
2026/01/08 01:15:11 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_spending.go:116
[22.453ms] [rows:0] SELECT
orders.user_id,
SUM(orders.actual_amount) as total_amount,
COUNT(orders.id) as order_count,
SUM(orders.discount_amount) as total_discount,
SUM(orders.points_amount) as total_points,
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN orders.actual_amount ELSE 0 END) as ichiban_spending,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN 1 ELSE 0 END) as ichiban_count,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN orders.actual_amount ELSE 0 END) as infinite_spending,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN 1 ELSE 0 END) as infinite_count,
SUM(CASE WHEN oa.play_type = 'matching' THEN orders.actual_amount ELSE 0 END) as matching_spending,
SUM(CASE WHEN oa.play_type = 'matching' THEN 1 ELSE 0 END) as matching_count
FROM `orders` LEFT JOIN (SELECT l.order_id, MAX(a.play_type) as play_type FROM activity_draw_logs l JOIN activity_issues i ON i.id = l.issue_id JOIN activities a ON a.id = i.activity_id WHERE l.created_at >= '2025-12-08 01:15:11.202' GROUP BY l.order_id) oa ON oa.order_id = orders.id WHERE orders.status = 2 AND orders.created_at >= '2025-12-09 01:15:11.202' AND orders.created_at <= '2026-01-08 01:15:11.202' GROUP BY `orders`.`user_id` ORDER BY total_amount DESC LIMIT 50
2026/01/08 01:15:13 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_spending.go:116
[23.908ms] [rows:0] SELECT
orders.user_id,
SUM(orders.actual_amount) as total_amount,
COUNT(orders.id) as order_count,
SUM(orders.discount_amount) as total_discount,
SUM(orders.points_amount) as total_points,
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN orders.actual_amount ELSE 0 END) as ichiban_spending,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN 1 ELSE 0 END) as ichiban_count,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN orders.actual_amount ELSE 0 END) as infinite_spending,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN 1 ELSE 0 END) as infinite_count,
SUM(CASE WHEN oa.play_type = 'matching' THEN orders.actual_amount ELSE 0 END) as matching_spending,
SUM(CASE WHEN oa.play_type = 'matching' THEN 1 ELSE 0 END) as matching_count
FROM `orders` LEFT JOIN (SELECT l.order_id, MAX(a.play_type) as play_type FROM activity_draw_logs l JOIN activity_issues i ON i.id = l.issue_id JOIN activities a ON a.id = i.activity_id WHERE l.created_at >= '2025-12-08 01:15:13.54' GROUP BY l.order_id) oa ON oa.order_id = orders.id WHERE orders.status = 2 AND orders.created_at >= '2025-12-09 01:15:13.54' AND orders.created_at <= '2026-01-08 01:15:13.54' GROUP BY `orders`.`user_id` ORDER BY total_amount DESC LIMIT 50
2026/01/08 01:15:14 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_spending.go:116
[21.376ms] [rows:0] SELECT
orders.user_id,
SUM(orders.actual_amount) as total_amount,
COUNT(orders.id) as order_count,
SUM(orders.discount_amount) as total_discount,
SUM(orders.points_amount) as total_points,
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN orders.actual_amount ELSE 0 END) as ichiban_spending,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN 1 ELSE 0 END) as ichiban_count,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN orders.actual_amount ELSE 0 END) as infinite_spending,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN 1 ELSE 0 END) as infinite_count,
SUM(CASE WHEN oa.play_type = 'matching' THEN orders.actual_amount ELSE 0 END) as matching_spending,
SUM(CASE WHEN oa.play_type = 'matching' THEN 1 ELSE 0 END) as matching_count
FROM `orders` LEFT JOIN (SELECT l.order_id, MAX(a.play_type) as play_type FROM activity_draw_logs l JOIN activity_issues i ON i.id = l.issue_id JOIN activities a ON a.id = i.activity_id WHERE l.created_at >= '2025-12-08 01:15:13.982' GROUP BY l.order_id) oa ON oa.order_id = orders.id WHERE orders.status = 2 AND orders.created_at >= '2025-12-09 01:15:13.982' AND orders.created_at <= '2026-01-08 01:15:13.982' GROUP BY `orders`.`user_id` ORDER BY total_amount DESC LIMIT 50
2026/01/08 01:15:14 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_spending.go:116
[20.622ms] [rows:0] SELECT
orders.user_id,
SUM(orders.actual_amount) as total_amount,
COUNT(orders.id) as order_count,
SUM(orders.discount_amount) as total_discount,
SUM(orders.points_amount) as total_points,
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN orders.actual_amount ELSE 0 END) as ichiban_spending,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN 1 ELSE 0 END) as ichiban_count,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN orders.actual_amount ELSE 0 END) as infinite_spending,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN 1 ELSE 0 END) as infinite_count,
SUM(CASE WHEN oa.play_type = 'matching' THEN orders.actual_amount ELSE 0 END) as matching_spending,
SUM(CASE WHEN oa.play_type = 'matching' THEN 1 ELSE 0 END) as matching_count
FROM `orders` LEFT JOIN (SELECT l.order_id, MAX(a.play_type) as play_type FROM activity_draw_logs l JOIN activity_issues i ON i.id = l.issue_id JOIN activities a ON a.id = i.activity_id WHERE l.created_at >= '2025-12-08 01:15:14.549' GROUP BY l.order_id) oa ON oa.order_id = orders.id WHERE orders.status = 2 AND orders.created_at >= '2025-12-09 01:15:14.549' AND orders.created_at <= '2026-01-08 01:15:14.549' GROUP BY `orders`.`user_id` ORDER BY total_amount DESC LIMIT 50
{"level":"debug","time":"2026-01-08 01:15:35","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:15:35"}
{"level":"info","time":"2026-01-08 01:15:35","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
{"level":"debug","time":"2026-01-08 01:15:35","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:15:35","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:44.807+08:00","last_settled":"2026-01-08T01:12:44.807+08:00"}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:15:44","now":"2026-01-08 01:15:35","skip":true}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:18:29.385+08:00","last_settled":"2026-01-08T01:15:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:18:29","now":"2026-01-08 01:15:35","skip":true}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:18:44.808+08:00","last_settled":"2026-01-08T01:13:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:18:44","now":"2026-01-08 01:15:35","skip":true}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:15:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:15:29","now":"2026-01-08 01:15:35","skip":false}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 查询订单范围","domain":"mini-chat[fat]","id":79,"last":"2026-01-08 01:10:29","now":"2026-01-08 01:15:35"}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 查询到订单","domain":"mini-chat[fat]","id":79,"count":0,"min":1}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖-一番赏: 检查售罄","domain":"mini-chat[fat]","issue_id":86,"sold":0,"total":5}
{"level":"info","time":"2026-01-08 01:15:36","caller":"logger/logger.go:309","msg":"定时开奖-一番赏: 未售罄,执行全额退款","domain":"mini-chat[fat]","issue_id":86}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖-一番赏: 剩余未处理订单记录","domain":"mini-chat[fat]","issue_id":86,"count":0}
{"level":"info","time":"2026-01-08 01:15:36","caller":"logger/logger.go:309","msg":"定时开奖-一番赏: 格位已重置,新一轮可以开始","domain":"mini-chat[fat]","issue_id":86}
{"level":"info","time":"2026-01-08 01:15:36","caller":"logger/logger.go:309","msg":"定时开奖: 人数满足,开始开奖处理","domain":"mini-chat[fat]","id":79}
{"level":"info","time":"2026-01-08 01:15:36","caller":"logger/logger.go:309","msg":"定时开奖: 更新活动下次结算时间","domain":"mini-chat[fat]","id":79,"last":"2026-01-08 01:15:35","next":"2026-01-08 01:20:35"}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:24:29.385+08:00","last_settled":"2026-01-08T01:14:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:24:29","now":"2026-01-08 01:15:35","skip":true}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:15:35","skip":true}
{"level":"debug","time":"2026-01-08 01:15:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:15:37","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:15:35","skip":true}
{"level":"debug","time":"2026-01-08 01:15:37","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:15:37","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:15:35","skip":true}
{"level":"debug","time":"2026-01-08 01:15:37","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:15:37","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:15:35","skip":true}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:15:40","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:15:40","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:16:05","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:16:05"}
{"level":"debug","time":"2026-01-08 01:16:05","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:16:05","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:18:44.807+08:00","last_settled":"2026-01-08T01:15:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:18:44","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:18:29.385+08:00","last_settled":"2026-01-08T01:15:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:18:29","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:18:44.808+08:00","last_settled":"2026-01-08T01:13:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:18:44","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:20:35.866+08:00","last_settled":"2026-01-08T01:15:35.866+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:20:35","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:24:29.385+08:00","last_settled":"2026-01-08T01:14:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:24:29","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:16:06","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:16:05","skip":true}
{"level":"debug","time":"2026-01-08 01:16:35","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:16:35"}
{"level":"debug","time":"2026-01-08 01:16:35","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:16:35","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:18:44.807+08:00","last_settled":"2026-01-08T01:15:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:18:44","now":"2026-01-08 01:16:35","skip":true}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:18:29.385+08:00","last_settled":"2026-01-08T01:15:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:18:29","now":"2026-01-08 01:16:35","skip":true}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:18:44.808+08:00","last_settled":"2026-01-08T01:13:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:18:44","now":"2026-01-08 01:16:35","skip":true}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:20:35.866+08:00","last_settled":"2026-01-08T01:15:35.866+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:20:35","now":"2026-01-08 01:16:35","skip":true}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:24:29.385+08:00","last_settled":"2026-01-08T01:14:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:24:29","now":"2026-01-08 01:16:35","skip":true}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:16:35","skip":true}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:16:35","skip":true}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:16:35","skip":true}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:16:36","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:16:35","skip":true}
2026/01/08 01:16:38 /Users/win/aicode/bindbox/bindbox_game/internal/api/admin/dashboard_spending.go:116
[24.774ms] [rows:0] SELECT
orders.user_id,
SUM(orders.actual_amount) as total_amount,
COUNT(orders.id) as order_count,
SUM(orders.discount_amount) as total_discount,
SUM(orders.points_amount) as total_points,
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN orders.actual_amount ELSE 0 END) as ichiban_spending,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN 1 ELSE 0 END) as ichiban_count,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN orders.actual_amount ELSE 0 END) as infinite_spending,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN 1 ELSE 0 END) as infinite_count,
SUM(CASE WHEN oa.play_type = 'matching' THEN orders.actual_amount ELSE 0 END) as matching_spending,
SUM(CASE WHEN oa.play_type = 'matching' THEN 1 ELSE 0 END) as matching_count
FROM `orders` LEFT JOIN (SELECT l.order_id, MAX(a.play_type) as play_type FROM activity_draw_logs l JOIN activity_issues i ON i.id = l.issue_id JOIN activities a ON a.id = i.activity_id WHERE l.created_at >= '2025-12-31 01:16:38.634' GROUP BY l.order_id) oa ON oa.order_id = orders.id WHERE orders.status = 2 AND orders.created_at >= '2026-01-01 01:16:38.634' AND orders.created_at <= '2026-01-08 01:16:38.634' GROUP BY `orders`.`user_id` ORDER BY total_amount DESC LIMIT 50
{"level":"info","time":"2026-01-08 01:16:40","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:16:45","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:16:45","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}

View File

@ -1,4 +0,0 @@
# bindbox-game/internal/api/admin
internal/api/admin/dashboard_spending.go:118:68: undefined: logger.Any
internal/api/admin/users_admin.go:112:58: undefined: logger.Any
internal/api/admin/users_admin.go:319:58: undefined: logger.Any

View File

@ -1,360 +0,0 @@
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
 ____ _ _ _ ____
 | __ ) (_) _ __ __| | | |__ ___ __ __ / ___| __ _ _ __ ___ ___
 | _ \ | | | '_ \ / _` | | '_ \ / _ \ \ \/ / | | _ / _` | | '_ ` _ \ / _ \
 | |_) | | | | | | | | (_| | | |_) | | (_) | > < | |_| | | (_| | | | | | | | | __/
 |____/ |_| |_| |_| \__,_| |_.__/ \___/ /_/\_\ \____| \__,_| |_| |_| |_| \___|
▌ 客户项目: 盲盒游戏
▌ 项目版本: Release-2025111111
▌ 启动时间: 2026-01-08 01:03:00
▌ 运行环境: darwin go1.24.2
▌ 服务端口: [:9991]
▌ 服务配置: [fat]
▌ 数据库连接: ✔ 已建立
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"对对碰自动开奖: 后台任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"[抖店定时同步] 定时任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"error","time":"2026-01-08 01:03:00","caller":"logger/logger.go:327","msg":"解密配置失败","domain":"mini-chat[fat]","key":"douyin.app_secret","error":"ciphertext is not a multiple of the block size"}
{"level":"info","time":"2026-01-08 01:03:00","caller":"logger/logger.go:309","msg":"动态配置加载完成","domain":"mini-chat[fat]","count":26}
{"level":"debug","time":"2026-01-08 01:03:30","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:03:30"}
{"level":"info","time":"2026-01-08 01:03:30","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"debug","time":"2026-01-08 01:03:30","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:03:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:06:14.807+08:00","last_settled":"2026-01-08T01:03:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:03:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:06:14","now":"2026-01-08 01:03:30","skip":true}
{"level":"debug","time":"2026-01-08 01:03:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:05:59.385+08:00","last_settled":"2026-01-08T01:02:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:03:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:05:59","now":"2026-01-08 01:03:30","skip":true}
{"level":"debug","time":"2026-01-08 01:03:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:03:44.807+08:00","last_settled":"2026-01-08T00:58:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:03:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:03:44","now":"2026-01-08 01:03:30","skip":true}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:05:14.807+08:00","last_settled":"2026-01-08T01:00:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:05:14","now":"2026-01-08 01:03:30","skip":true}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 01:03:30","skip":true}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:03:30","skip":true}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:03:30","skip":true}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 01:03:30","skip":true}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:03:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:03:30","skip":true}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:03:35","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:03:35","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:04:00"}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:06:14.807+08:00","last_settled":"2026-01-08T01:03:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:06:14","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:05:59.385+08:00","last_settled":"2026-01-08T01:02:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:05:59","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:05:14.807+08:00","last_settled":"2026-01-08T01:00:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:05:14","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:04:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:04:00","skip":true}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:04:30"}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:06:14.807+08:00","last_settled":"2026-01-08T01:03:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:06:14","now":"2026-01-08 01:04:30","skip":true}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:05:59.385+08:00","last_settled":"2026-01-08T01:02:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:05:59","now":"2026-01-08 01:04:30","skip":true}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:04:30","skip":true}
{"level":"debug","time":"2026-01-08 01:04:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:05:14.807+08:00","last_settled":"2026-01-08T01:00:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:05:14","now":"2026-01-08 01:04:30","skip":true}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:04:30","skip":true}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:04:30","skip":true}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:04:30","skip":true}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 01:04:30","skip":true}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:04:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:04:30","skip":true}
{"level":"info","time":"2026-01-08 01:04:35","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:04:40","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:04:40","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:05:00"}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:06:14.807+08:00","last_settled":"2026-01-08T01:03:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:06:14","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:05:59.385+08:00","last_settled":"2026-01-08T01:02:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:05:59","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:05:14.807+08:00","last_settled":"2026-01-08T01:00:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:05:14","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:05:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:05:00","skip":true}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:05:30"}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:06:14.807+08:00","last_settled":"2026-01-08T01:03:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:06:14","now":"2026-01-08 01:05:30","skip":true}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:05:59.385+08:00","last_settled":"2026-01-08T01:02:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:05:59","now":"2026-01-08 01:05:30","skip":true}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:05:30","skip":true}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:05:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:05:30","skip":true}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:05:30","skip":true}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:05:30","skip":true}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:05:30","skip":true}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:05:30","skip":true}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:05:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:05:30","skip":true}
{"level":"info","time":"2026-01-08 01:05:40","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:05:45","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:05:45","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:06:00"}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:06:14.807+08:00","last_settled":"2026-01-08T01:03:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:06:14","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:08:59.386+08:00","last_settled":"2026-01-08T01:05:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:08:59","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:06:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:06:00","skip":true}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:06:30"}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:09:14.808+08:00","last_settled":"2026-01-08T01:06:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:09:14","now":"2026-01-08 01:06:30","skip":true}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:08:59.386+08:00","last_settled":"2026-01-08T01:05:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:08:59","now":"2026-01-08 01:06:30","skip":true}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:06:30","skip":true}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:06:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:06:30","skip":true}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:06:30","skip":true}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:06:30","skip":true}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:06:30","skip":true}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:06:30","skip":true}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:06:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:06:30","skip":true}
{"level":"info","time":"2026-01-08 01:06:45","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:06:50","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:06:50","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:07:00"}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:09:14.808+08:00","last_settled":"2026-01-08T01:06:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:09:14","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:08:59.386+08:00","last_settled":"2026-01-08T01:05:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:08:59","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:07:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:07:00","skip":true}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:07:30"}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:09:14.808+08:00","last_settled":"2026-01-08T01:06:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:09:14","now":"2026-01-08 01:07:30","skip":true}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:08:59.386+08:00","last_settled":"2026-01-08T01:05:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:08:59","now":"2026-01-08 01:07:30","skip":true}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:07:30","skip":true}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:07:30","skip":true}
{"level":"debug","time":"2026-01-08 01:07:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:07:30","skip":true}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:07:30","skip":true}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:07:30","skip":true}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:07:30","skip":true}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:07:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:07:30","skip":true}
{"level":"info","time":"2026-01-08 01:07:50","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:07:54","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:07:54","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:08:00","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:08:00"}
{"level":"debug","time":"2026-01-08 01:08:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:08:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:09:14.808+08:00","last_settled":"2026-01-08T01:06:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:08:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:09:14","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:08:59.386+08:00","last_settled":"2026-01-08T01:05:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:08:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:08:59","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:08:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:08:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:08:00","skip":true}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:08:30"}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:09:14.808+08:00","last_settled":"2026-01-08T01:06:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:09:14","now":"2026-01-08 01:08:30","skip":true}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:08:59.386+08:00","last_settled":"2026-01-08T01:05:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:08:59","now":"2026-01-08 01:08:30","skip":true}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:08:44.807+08:00","last_settled":"2026-01-08T01:03:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:08:44","now":"2026-01-08 01:08:30","skip":true}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:08:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:08:30","skip":true}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:08:30","skip":true}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:08:30","skip":true}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:08:30","skip":true}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:08:30","skip":true}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:08:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:08:30","skip":true}
{"level":"info","time":"2026-01-08 01:08:54","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:08:59","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:08:59","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:09:00","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:09:00"}
{"level":"debug","time":"2026-01-08 01:09:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:09:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:09:14.808+08:00","last_settled":"2026-01-08T01:06:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:09:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:09:14","now":"2026-01-08 01:09:00","skip":true}
{"level":"debug","time":"2026-01-08 01:09:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:08:59.386+08:00","last_settled":"2026-01-08T01:05:59.386+08:00"}
{"level":"debug","time":"2026-01-08 01:09:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:08:59","now":"2026-01-08 01:09:00","skip":false}
{"level":"debug","time":"2026-01-08 01:09:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询订单范围","domain":"mini-chat[fat]","id":65,"last":"2026-01-08 01:05:59","now":"2026-01-08 01:09:00"}
{"level":"debug","time":"2026-01-08 01:09:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询到订单","domain":"mini-chat[fat]","id":65,"count":0,"min":1}
{"level":"info","time":"2026-01-08 01:09:00","caller":"logger/logger.go:309","msg":"定时开奖: 人数满足,开始开奖处理","domain":"mini-chat[fat]","id":65}
{"level":"info","time":"2026-01-08 01:09:00","caller":"logger/logger.go:309","msg":"定时开奖: 更新活动下次结算时间","domain":"mini-chat[fat]","id":65,"last":"2026-01-08 01:09:00","next":"2026-01-08 01:12:00"}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:13:44.807+08:00","last_settled":"2026-01-08T01:08:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:13:44","now":"2026-01-08 01:09:00","skip":true}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:09:00","skip":true}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:09:00","skip":true}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:09:00","skip":true}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:09:00","skip":true}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:09:00","skip":true}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:09:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:09:00","skip":true}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:09:30"}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:12:29.386+08:00","last_settled":"2026-01-08T01:09:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:12:29","now":"2026-01-08 01:09:30","skip":true}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:12:00.509+08:00","last_settled":"2026-01-08T01:09:00.509+08:00"}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:12:00","now":"2026-01-08 01:09:30","skip":true}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:13:44.807+08:00","last_settled":"2026-01-08T01:08:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:13:44","now":"2026-01-08 01:09:30","skip":true}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:09:30","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:09:30","skip":true}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:09:30","skip":true}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:09:30","skip":true}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:09:30","skip":true}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:09:30","skip":true}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:09:31","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:09:30","skip":true}
{"level":"info","time":"2026-01-08 01:09:59","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:10:00"}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:12:29.386+08:00","last_settled":"2026-01-08T01:09:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:12:29","now":"2026-01-08 01:10:00","skip":true}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:12:00.509+08:00","last_settled":"2026-01-08T01:09:00.509+08:00"}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:12:00","now":"2026-01-08 01:10:00","skip":true}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:13:44.807+08:00","last_settled":"2026-01-08T01:08:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:13:44","now":"2026-01-08 01:10:00","skip":true}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:10:14.808+08:00","last_settled":"2026-01-08T01:05:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:10:00","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:10:00","skip":true}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:10:00","skip":true}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:10:00","skip":true}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:10:00","skip":true}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:10:00","skip":true}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:10:01","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:10:00","skip":true}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:10:04","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:10:04","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
signal: killed

View File

@ -1,195 +0,0 @@
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
 ____ _ _ _ ____
 | __ ) (_) _ __ __| | | |__ ___ __ __ / ___| __ _ _ __ ___ ___
 | _ \ | | | '_ \ / _` | | '_ \ / _ \ \ \/ / | | _ / _` | | '_ ` _ \ / _ \
 | |_) | | | | | | | | (_| | | |_) | | (_) | > < | |_| | | (_| | | | | | | | | __/
 |____/ |_| |_| |_| \__,_| |_.__/ \___/ /_/\_\ \____| \__,_| |_| |_| |_| \___|
▌ 客户项目: 盲盒游戏
▌ 项目版本: Release-2025111111
▌ 启动时间: 2026-01-08 00:56:40
▌ 运行环境: darwin go1.24.2
▌ 服务端口: [:9991]
▌ 服务配置: [fat]
▌ 数据库连接: ✔ 已建立
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"对对碰自动开奖: 后台任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"[抖店定时同步] 定时任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"error","time":"2026-01-08 00:56:40","caller":"logger/logger.go:327","msg":"解密配置失败","domain":"mini-chat[fat]","key":"douyin.app_secret","error":"ciphertext is not a multiple of the block size"}
{"level":"info","time":"2026-01-08 00:56:40","caller":"logger/logger.go:309","msg":"动态配置加载完成","domain":"mini-chat[fat]","count":26}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 00:57:10"}
{"level":"info","time":"2026-01-08 00:57:10","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T00:56:59.386+08:00","last_settled":"2026-01-08T00:53:59.386+08:00"}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 00:56:59","now":"2026-01-08 00:57:10","skip":false}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 查询订单范围","domain":"mini-chat[fat]","id":52,"last":"2026-01-08 00:53:59","now":"2026-01-08 00:57:10"}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 查询到订单","domain":"mini-chat[fat]","id":52,"count":0,"min":1}
{"level":"info","time":"2026-01-08 00:57:10","caller":"logger/logger.go:309","msg":"定时开奖: 人数满足,开始开奖处理","domain":"mini-chat[fat]","id":52}
{"level":"info","time":"2026-01-08 00:57:10","caller":"logger/logger.go:309","msg":"定时开奖: 更新活动下次结算时间","domain":"mini-chat[fat]","id":52,"last":"2026-01-08 00:57:10","next":"2026-01-08 01:00:10"}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T00:59:44.808+08:00","last_settled":"2026-01-08T00:56:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 00:59:44","now":"2026-01-08 00:57:10","skip":true}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T00:58:44.807+08:00","last_settled":"2026-01-08T00:53:44.807+08:00"}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 00:58:44","now":"2026-01-08 00:57:10","skip":true}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:00:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:00:14","now":"2026-01-08 00:57:10","skip":true}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 00:57:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 00:57:10","skip":true}
{"level":"debug","time":"2026-01-08 00:57:11","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:57:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:57:10","skip":true}
{"level":"debug","time":"2026-01-08 00:57:11","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:57:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:57:10","skip":true}
{"level":"debug","time":"2026-01-08 00:57:11","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 00:57:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 00:57:10","skip":true}
{"level":"debug","time":"2026-01-08 00:57:11","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:57:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 00:57:10","skip":true}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 00:57:14","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 00:57:14","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 00:57:40"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:00:10.204+08:00","last_settled":"2026-01-08T00:57:10.204+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:00:10","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T00:59:44.808+08:00","last_settled":"2026-01-08T00:56:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 00:59:44","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T00:58:44.807+08:00","last_settled":"2026-01-08T00:53:44.807+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 00:58:44","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:00:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:00:14","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:57:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 00:57:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 00:58:10"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:00:10.204+08:00","last_settled":"2026-01-08T00:57:10.204+08:00"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:00:10","now":"2026-01-08 00:58:10","skip":true}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T00:59:44.808+08:00","last_settled":"2026-01-08T00:56:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 00:59:44","now":"2026-01-08 00:58:10","skip":true}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T00:58:44.807+08:00","last_settled":"2026-01-08T00:53:44.807+08:00"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 00:58:44","now":"2026-01-08 00:58:10","skip":true}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:00:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:00:14","now":"2026-01-08 00:58:10","skip":true}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 00:58:10","skip":true}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:58:10","skip":true}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:58:10","skip":true}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 00:58:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 00:58:10","skip":true}
{"level":"debug","time":"2026-01-08 00:58:11","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:58:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 00:58:10","skip":true}
{"level":"info","time":"2026-01-08 00:58:14","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 00:58:19","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 00:58:19","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 00:58:40"}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:00:10.204+08:00","last_settled":"2026-01-08T00:57:10.204+08:00"}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:00:10","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T00:59:44.808+08:00","last_settled":"2026-01-08T00:56:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 00:59:44","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T00:58:44.807+08:00","last_settled":"2026-01-08T00:53:44.807+08:00"}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 00:58:44","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:00:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:00:14","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:58:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:41","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 00:58:41","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:58:41","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:58:41","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 00:58:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 00:59:10"}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:00:10.204+08:00","last_settled":"2026-01-08T00:57:10.204+08:00"}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:00:10","now":"2026-01-08 00:59:10","skip":true}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T00:59:44.808+08:00","last_settled":"2026-01-08T00:56:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 00:59:44","now":"2026-01-08 00:59:10","skip":true}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:03:44.807+08:00","last_settled":"2026-01-08T00:58:44.807+08:00"}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:03:44","now":"2026-01-08 00:59:10","skip":true}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:00:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:00:14","now":"2026-01-08 00:59:10","skip":true}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 00:59:10","skip":true}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:59:10","skip":true}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:59:10","skip":true}
{"level":"debug","time":"2026-01-08 00:59:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 00:59:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 00:59:10","skip":true}
{"level":"debug","time":"2026-01-08 00:59:11","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:59:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 00:59:10","skip":true}
{"level":"info","time":"2026-01-08 00:59:19","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 00:59:24","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 00:59:24","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 00:59:40"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:00:10.204+08:00","last_settled":"2026-01-08T00:57:10.204+08:00"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:00:10","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T00:59:44.808+08:00","last_settled":"2026-01-08T00:56:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 00:59:44","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:03:44.807+08:00","last_settled":"2026-01-08T00:58:44.807+08:00"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:03:44","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:00:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:00:14","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 00:59:40","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 00:59:41","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 00:59:41","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 00:59:40","skip":true}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:00:10"}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:00:10.204+08:00","last_settled":"2026-01-08T00:57:10.204+08:00"}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:00:10","now":"2026-01-08 01:00:10","skip":true}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:02:59.385+08:00","last_settled":"2026-01-08T00:59:59.385+08:00"}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:02:59","now":"2026-01-08 01:00:10","skip":true}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:03:44.807+08:00","last_settled":"2026-01-08T00:58:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:03:44","now":"2026-01-08 01:00:10","skip":true}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:00:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:00:14","now":"2026-01-08 01:00:10","skip":true}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:04:18.148+08:00","last_settled":"2026-01-08T00:54:18.148+08:00"}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:04:18","now":"2026-01-08 01:00:10","skip":true}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:00:10","skip":true}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:10:14.807+08:00","last_settled":"2026-01-08T00:55:14.807+08:00"}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:10:14","now":"2026-01-08 01:00:10","skip":true}
{"level":"debug","time":"2026-01-08 01:00:10","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:05:26.489+08:00","last_settled":"2026-01-08T00:35:26.489+08:00"}
{"level":"debug","time":"2026-01-08 01:00:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:05:26","now":"2026-01-08 01:00:10","skip":true}
{"level":"debug","time":"2026-01-08 01:00:11","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:00:11","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:00:10","skip":true}
{"level":"info","time":"2026-01-08 01:00:24","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
signal: killed

View File

@ -1,147 +0,0 @@
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
 ____ _ _ _ ____
 | __ ) (_) _ __ __| | | |__ ___ __ __ / ___| __ _ _ __ ___ ___
 | _ \ | | | '_ \ / _` | | '_ \ / _ \ \ \/ / | | _ / _` | | '_ ` _ \ / _ \
 | |_) | | | | | | | | (_| | | |_) | | (_) | > < | |_| | | (_| | | | | | | | | __/
 |____/ |_| |_| |_| \__,_| |_.__/ \___/ /_/\_\ \____| \__,_| |_| |_| |_| \___|
▌ 客户项目: 盲盒游戏
▌ 项目版本: Release-2025111111
▌ 启动时间: 2026-01-08 01:11:58
▌ 运行环境: darwin go1.24.2
▌ 服务端口: [:9991]
▌ 服务配置: [fat]
▌ 数据库连接: ✔ 已建立
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"对对碰自动开奖: 后台任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"[抖店定时同步] 定时任务已启动","domain":"mini-chat[fat]"}
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"error","time":"2026-01-08 01:11:58","caller":"logger/logger.go:327","msg":"解密配置失败","domain":"mini-chat[fat]","key":"douyin.app_secret","error":"ciphertext is not a multiple of the block size"}
{"level":"info","time":"2026-01-08 01:11:58","caller":"logger/logger.go:309","msg":"动态配置加载完成","domain":"mini-chat[fat]","count":26}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:12:28"}
{"level":"info","time":"2026-01-08 01:12:28","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:12:29.386+08:00","last_settled":"2026-01-08T01:09:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:12:29","now":"2026-01-08 01:12:28","skip":true}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:14.808+08:00","last_settled":"2026-01-08T01:12:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:15:14","now":"2026-01-08 01:12:28","skip":true}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:13:44.807+08:00","last_settled":"2026-01-08T01:08:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:13:44","now":"2026-01-08 01:12:28","skip":true}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:15:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:15:29","now":"2026-01-08 01:12:28","skip":true}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:12:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:12:28","skip":true}
{"level":"debug","time":"2026-01-08 01:12:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:12:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:12:28","skip":true}
{"level":"debug","time":"2026-01-08 01:12:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:12:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:12:28","skip":true}
{"level":"debug","time":"2026-01-08 01:12:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:12:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:12:28","skip":true}
{"level":"debug","time":"2026-01-08 01:12:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:12:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:12:28","skip":true}
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:12:32","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:12:32","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:12:58"}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:44.807+08:00","last_settled":"2026-01-08T01:12:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:15:44","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:14.808+08:00","last_settled":"2026-01-08T01:12:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:15:14","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:13:44.807+08:00","last_settled":"2026-01-08T01:08:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:13:44","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:15:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:15:29","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:12:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:12:59","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:12:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:12:59","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:12:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:12:59","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:12:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:12:59","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:12:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:12:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:13:28"}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:44.807+08:00","last_settled":"2026-01-08T01:12:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:15:44","now":"2026-01-08 01:13:28","skip":true}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:14.808+08:00","last_settled":"2026-01-08T01:12:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:15:14","now":"2026-01-08 01:13:28","skip":true}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:13:44.807+08:00","last_settled":"2026-01-08T01:08:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:13:44","now":"2026-01-08 01:13:28","skip":true}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:15:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:15:29","now":"2026-01-08 01:13:28","skip":true}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:13:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:13:28","skip":true}
{"level":"debug","time":"2026-01-08 01:13:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:13:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:13:28","skip":true}
{"level":"debug","time":"2026-01-08 01:13:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:13:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:13:28","skip":true}
{"level":"debug","time":"2026-01-08 01:13:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:13:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:13:28","skip":true}
{"level":"debug","time":"2026-01-08 01:13:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:13:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:13:28","skip":true}
{"level":"info","time":"2026-01-08 01:13:32","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:13:37","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:13:37","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:13:58"}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:44.807+08:00","last_settled":"2026-01-08T01:12:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:15:44","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:14.808+08:00","last_settled":"2026-01-08T01:12:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:15:14","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:18:44.808+08:00","last_settled":"2026-01-08T01:13:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:18:44","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:15:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:15:29","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:58","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:13:59","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:13:58","skip":true}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 开始检查","domain":"mini-chat[fat]","now":"2026-01-08 01:14:28"}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 查询到活动","domain":"mini-chat[fat]","count":9}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":52,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:44.807+08:00","last_settled":"2026-01-08T01:12:44.807+08:00"}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":52,"st":"2026-01-08 01:15:44","now":"2026-01-08 01:14:28","skip":true}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":65,"play_type":"ichiban","interval":3,"scheduled_time":"2026-01-08T01:15:14.808+08:00","last_settled":"2026-01-08T01:12:14.808+08:00"}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":65,"st":"2026-01-08 01:15:14","now":"2026-01-08 01:14:28","skip":true}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":77,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:18:44.808+08:00","last_settled":"2026-01-08T01:13:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":77,"st":"2026-01-08 01:18:44","now":"2026-01-08 01:14:28","skip":true}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":79,"play_type":"ichiban","interval":5,"scheduled_time":"2026-01-08T01:15:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":79,"st":"2026-01-08 01:15:29","now":"2026-01-08 01:14:28","skip":true}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":80,"play_type":"ichiban","interval":10,"scheduled_time":"2026-01-08T01:14:29.385+08:00","last_settled":"2026-01-08T01:04:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:14:28","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":80,"st":"2026-01-08 01:14:29","now":"2026-01-08 01:14:28","skip":true}
{"level":"debug","time":"2026-01-08 01:14:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":81,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:14:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":81,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:14:28","skip":true}
{"level":"debug","time":"2026-01-08 01:14:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":82,"play_type":"ichiban","interval":15,"scheduled_time":"2026-01-08T01:25:29.386+08:00","last_settled":"2026-01-08T01:10:29.386+08:00"}
{"level":"debug","time":"2026-01-08 01:14:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":82,"st":"2026-01-08 01:25:29","now":"2026-01-08 01:14:28","skip":true}
{"level":"debug","time":"2026-01-08 01:14:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":83,"play_type":"ichiban","interval":30,"scheduled_time":"2026-01-08T01:35:29.385+08:00","last_settled":"2026-01-08T01:05:29.385+08:00"}
{"level":"debug","time":"2026-01-08 01:14:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":83,"st":"2026-01-08 01:35:29","now":"2026-01-08 01:14:28","skip":true}
{"level":"debug","time":"2026-01-08 01:14:29","caller":"logger/logger.go:315","msg":"定时开奖: 检查活动","domain":"mini-chat[fat]","id":84,"play_type":"ichiban","interval":60,"scheduled_time":"2026-01-08T01:27:44.808+08:00","last_settled":"2026-01-08T00:27:44.808+08:00"}
{"level":"debug","time":"2026-01-08 01:14:29","caller":"logger/logger.go:315","msg":"定时开奖: 计算开奖时间","domain":"mini-chat[fat]","id":84,"st":"2026-01-08 01:27:44","now":"2026-01-08 01:14:28","skip":true}
{"level":"info","time":"2026-01-08 01:14:37","caller":"logger/logger.go:309","msg":"[抖店定时同步] 开始同步","domain":"mini-chat[fat]","interval_minutes":1}
[DEBUG] 开始全量同步,共 3 个绑定用户
[DEBUG] 正在同步用户 ID: 9018 (昵称: 现实的迪斯蒂法诺, 抖音号: wyl0423333) 的订单...
[DEBUG] 正在同步用户 ID: 9019 (昵称: 约翰掐指一算, 抖音号: xrw200947752) 的订单...
[DEBUG] 正在同步用户 ID: 9047 (昵称: 巴乔横扫六合, 抖音号: 请输入您的抖店订单号即可完成绑定) 的订单...
{"level":"info","time":"2026-01-08 01:14:42","caller":"logger/logger.go:309","msg":"[抖店同步] 全量同步完成","domain":"mini-chat[fat]","users_count":3,"total_fetched":45,"new_orders":0,"matched_users":45}
{"level":"info","time":"2026-01-08 01:14:42","caller":"logger/logger.go:309","msg":"[抖店定时同步] 同步成功","domain":"mini-chat[fat]","total_fetched":45,"new_orders":0,"matched_users":45}

View File

@ -1 +0,0 @@
SELECT config_key, config_value FROM sys_configs WHERE config_key = 'wechat_miniprogram_lottery_result_template_id';

View File

@ -0,0 +1,95 @@
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func main() {
dsn := "root:bindbox2025kdy@tcp(106.54.232.2:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Info)})
if err != nil {
panic("failed to connect database: " + err.Error())
}
// 检查对对碰活动
fmt.Println("========== 检查对对碰活动 (activity_category_id=3) ==========")
type Activity struct {
ID int64
Name string
PlayType string
ActivityCategoryID int64
}
var matchingActs []Activity
db.Table("activities").Where("activity_category_id = ?", 3).Limit(5).Find(&matchingActs)
fmt.Printf("找到 %d 个对对碰活动\n", len(matchingActs))
for _, act := range matchingActs {
fmt.Printf("\n--- Activity ID=%d Name='%s' PlayType='%s' ---\n", act.ID, act.Name, act.PlayType)
// 获取该活动的 issues
type Issue struct {
ID int64
ActivityID int64
}
var issues []Issue
db.Table("activity_issues").Where("activity_id = ?", act.ID).Find(&issues)
if len(issues) == 0 {
fmt.Println(" No issues found")
continue
}
issueIDs := make([]int64, len(issues))
for i, iss := range issues {
issueIDs[i] = iss.ID
}
fmt.Printf(" Issues: %v\n", issueIDs)
// 统计 activity_draw_logs
var drawLogsCount int64
db.Table("activity_draw_logs").Where("issue_id IN ?", issueIDs).Count(&drawLogsCount)
fmt.Printf(" Draw Logs count: %d\n", drawLogsCount)
// 检查 reward_settings
type RewardStat struct {
Level int32
TotalOrig int64
TotalRemain int64
}
var rewardStats []RewardStat
db.Table("activity_reward_settings").
Select("level, SUM(original_qty) as total_orig, SUM(quantity) as total_remain").
Where("issue_id IN ?", issueIDs).
Group("level").
Scan(&rewardStats)
for _, rs := range rewardStats {
issued := rs.TotalOrig - rs.TotalRemain
fmt.Printf(" Level %d: OrigQty=%d Remain=%d Issued(库存差)=%d\n", rs.Level, rs.TotalOrig, rs.TotalRemain, issued)
}
// 统计 draw_logs 按 level
type DrawLogStat struct {
Level int32
WinCount int64
}
var drawStats []DrawLogStat
db.Table("activity_draw_logs").
Joins("JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
Where("activity_draw_logs.issue_id IN ?", issueIDs).
Where("activity_draw_logs.is_winner = ?", 1).
Select("activity_reward_settings.level, COUNT(activity_draw_logs.id) as win_count").
Group("activity_reward_settings.level").
Scan(&drawStats)
for _, ds := range drawStats {
fmt.Printf(" Level %d: WinCount(实际抽奖)=%d\n", ds.Level, ds.WinCount)
}
}
fmt.Println("\n============================================")
}

122
cmd/debug_dashboard/main.go Normal file
View File

@ -0,0 +1,122 @@
package main
import (
"fmt"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Orders struct {
ID int64
OrderNo string
SourceType int32
Status int32
UserID int64
TotalAmount int64
CreatedAt time.Time
}
type ActivityDrawLogs struct {
ID int64
OrderID int64
IssueID int64
}
type ActivityIssues struct {
ID int64
ActivityID int64
}
type Activities struct {
ID int64
PlayType string
Name string
}
func (Orders) TableName() string { return "orders" }
func (ActivityDrawLogs) TableName() string { return "activity_draw_logs" }
func (ActivityIssues) TableName() string { return "activity_issues" }
func (Activities) TableName() string { return "activities" }
func main() {
dsn := "root:bindbox2025kdy@tcp(106.54.232.2:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database: " + err.Error())
}
var count int64
db.Model(&Orders{}).Count(&count)
fmt.Printf("Total Orders in DB: %d\n", count)
var orders []Orders
if err := db.Order("id DESC").Limit(5).Find(&orders).Error; err != nil {
fmt.Printf("Error finding orders: %v\n", err)
return
}
fmt.Printf("========== Latest 5 Orders ==========\n")
for _, o := range orders {
fmt.Printf("Order %s (ID: %d): Status=%d, SourceType=%d, Amount=%d, Time=%s\n", o.OrderNo, o.ID, o.Status, o.SourceType, o.TotalAmount, o.CreatedAt)
}
fmt.Printf("=====================================\n\n")
checkSourceType(db, 3, "Matching") // SourceType 3 = Matching
checkSourceType(db, 2, "Ichiban") // SourceType 2 = Ichiban
checkPlayType(db, "default", "Default PlayType")
}
func checkPlayType(db *gorm.DB, playType string, label string) {
fmt.Printf("========== Checking %s (PlayType='%s') ==========\n", label, playType)
var acts []Activities
if err := db.Where("play_type = ?", playType).Limit(5).Find(&acts).Error; err != nil {
fmt.Printf("Error finding activities: %v\n", err)
return
}
for _, a := range acts {
fmt.Printf("Activity ID=%d Name='%s' PlayType='%s'\n", a.ID, a.Name, a.PlayType)
}
fmt.Printf("============================================\n\n")
}
func checkSourceType(db *gorm.DB, sourceType int, label string) {
fmt.Printf("========== Checking %s (SourceType=%d) ==========\n", label, sourceType)
var orders []Orders
// Get last 5 paid orders
if err := db.Where("source_type = ? AND status = 2", sourceType).Order("id DESC").Limit(5).Find(&orders).Error; err != nil {
fmt.Printf("Error finding orders: %v\n", err)
return
}
if len(orders) == 0 {
fmt.Printf("No paid orders found for %s\n", label)
return
}
for _, o := range orders {
fmt.Printf("Order %s (ID: %d): ", o.OrderNo, o.ID)
// Find DrawLog
var log ActivityDrawLogs
if err := db.Where("order_id = ?", o.ID).First(&log).Error; err != nil {
fmt.Printf("DrawLog MISSING (%v)\n", err)
continue
}
// Find Issue
var issue ActivityIssues
if err := db.Where("id = ?", log.IssueID).First(&issue).Error; err != nil {
fmt.Printf("Issue MISSING (ID: %d, Err: %v)\n", log.IssueID, err)
continue
}
// Find Activity
var act Activities
if err := db.Where("id = ?", issue.ActivityID).First(&act).Error; err != nil {
fmt.Printf("Activity MISSING (ID: %d, Err: %v)\n", issue.ActivityID, err)
continue
}
fmt.Printf("PlayType='%s' Name='%s' (ActivityID: %d)\n", act.PlayType, act.Name, act.ID)
}
fmt.Printf("============================================\n\n")
}

113
cmd/debug_stats/main.go Normal file
View File

@ -0,0 +1,113 @@
package main
import (
"bindbox-game/internal/repository/mysql/model"
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect database: %v", err)
}
userID := int64(9082) // User from the report
// 1. Check Orders (ALL Status)
var orders []model.Orders
if err := db.Where("user_id = ?", userID).Find(&orders).Error; err != nil {
log.Printf("Error querying orders: %v", err)
}
var totalAmount int64
var discountAmount int64
var pointsAmount int64
fmt.Printf("--- ALL Orders for User %d ---\n", userID)
for _, o := range orders {
fmt.Printf("ID: %d, OrderNo: %s, Status: %d, Total: %d, Actual: %d, Discount: %d, Points: %d, Source: %d\n",
o.ID, o.OrderNo, o.Status, o.TotalAmount, o.ActualAmount, o.DiscountAmount, o.PointsAmount, o.SourceType)
if o.Status == 2 { // Only count Paid for Spending simulation (if that's the logic)
totalAmount += o.TotalAmount
discountAmount += o.DiscountAmount
pointsAmount += o.PointsAmount
}
}
fmt.Printf("Total Points (Status 2): %d\n", pointsAmount)
// 1.5 Check Points Ledger (Redemptions)
var ledgers []model.UserPointsLedger
if err := db.Where("user_id = ? AND action = ?", userID, "redeem_reward").Find(&ledgers).Error; err != nil {
log.Printf("Error querying ledgers: %v", err)
}
var totalRedeemedPoints int64
fmt.Printf("\n--- Points Redemption (Decomposition) ---\n")
for _, l := range ledgers {
fmt.Printf("ID: %d, Points: %d, Remark: %s, CreatedAt: %v\n", l.ID, l.Points, l.Remark, l.CreatedAt)
totalRedeemedPoints += l.Points
}
fmt.Printf("Total Redeemed Points: %d\n", totalRedeemedPoints)
// 2. Check Inventory (Output)
type InvItem struct {
ID int64
ProductID int64
Status int32
Price int64
Name string
Remark string // Added Remark field
}
var invItems []InvItem
// Show ALL status
err = db.Table("user_inventory").
Select("user_inventory.id, user_inventory.product_id, user_inventory.status, user_inventory.remark, products.price, products.name").
Joins("JOIN products ON products.id = user_inventory.product_id").
Where("user_inventory.user_id = ?", userID).
Where("user_inventory.remark NOT LIKE ? AND user_inventory.remark NOT LIKE ?", "%redeemed%", "%void%").
Scan(&invItems).Error
if err != nil {
log.Printf("Error querying inventory: %v", err)
}
var totalPrizeValue int64
var status1Value int64
var status2Value int64
var status3Value int64
fmt.Printf("\n--- Inventory (ALL Status) for User %d ---\n", userID)
for _, item := range invItems {
fmt.Printf("InvID: %d, ProductID: %d, Name: %s, Price: %d, Status: %d, Remark: %s\n",
item.ID, item.ProductID, item.Name, item.Price, item.Status, item.Remark)
if item.Status == 1 || item.Status == 3 {
totalPrizeValue += item.Price
}
if item.Status == 1 {
status1Value += item.Price
}
if item.Status == 2 {
status2Value += item.Price
}
if item.Status == 3 {
status3Value += item.Price
}
}
fmt.Printf("Status 1 (Holding) Value: %d\n", status1Value)
fmt.Printf("Status 2 (Void/Decomposed) Value: %d\n", status2Value)
fmt.Printf("Status 3 (Shipped/Used) Value: %d\n", status3Value)
fmt.Printf("Total Effective Prize Value (1+3): %d\n", totalPrizeValue)
// 3. Calculate Profit
profit := totalAmount - totalPrizeValue - discountAmount
fmt.Printf("\n--- Calculation ---\n")
fmt.Printf("Profit = Spending (%d) - PrizeValue (%d) - Discount (%d) = %d\n",
totalAmount, totalPrizeValue, discountAmount, profit)
fmt.Printf("Formatted:\nSpending: %.2f\nOutput: %.2f\nProfit: %.2f\n", float64(totalAmount)/100, float64(totalPrizeValue)/100, float64(profit)/100)
}

View File

@ -34,6 +34,6 @@ eg :
```shell ```shell
# 根目录下执行 # 根目录下执行
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger,users,user_addresses,menu_actions,menus,role_actions,role_menus,role_users,roles,order_items,orders,products,shipping_records,product_categories,user_invites,system_item_cards,user_item_cards,activity_draw_effects,banner,activity_draw_receipts,system_titles,system_title_effects,user_titles,user_title_effect_claims,payment_preorders,payment_transactions,payment_refunds,payment_notify_events,payment_bills,payment_bill_diff,ops_shipping_stats,system_configs,issue_position_claims,task_center_tasks,task_center_task_tiers,task_center_task_rewards,order_coupons,matching_card_types,channels,user_game_tickets,game_ticket_logs,order_snapshots,audit_rollback_logs,user_coupon_ledger,user_game_passes,game_pass_packages" go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger,users,user_addresses,menu_actions,menus,role_actions,role_menus,role_users,roles,order_items,orders,products,shipping_records,product_categories,user_invites,system_item_cards,user_item_cards,activity_draw_effects,banner,activity_draw_receipts,system_titles,system_title_effects,user_titles,user_title_effect_claims,payment_preorders,payment_transactions,payment_refunds,payment_notify_events,payment_bills,payment_bill_diff,ops_shipping_stats,system_configs,issue_position_claims,task_center_tasks,task_center_task_tiers,task_center_task_rewards,order_coupons,matching_card_types,channels,user_game_tickets,game_ticket_logs,order_snapshots,audit_rollback_logs,user_coupon_ledger,user_game_passes,game_pass_packages,livestream_activities,livestream_prizes,livestream_draw_logs"
``` ```

102
cmd/test_notify/main.go Normal file
View File

@ -0,0 +1,102 @@
package main
import (
"context"
"fmt"
"time"
"bindbox-game/configs"
"bindbox-game/internal/pkg/notify"
gormmysql "gorm.io/driver/mysql"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)
func main() {
// 配置会在 init 时自动加载
c := configs.Get()
fmt.Printf("========== 微信通知配置检查 ==========\n")
fmt.Printf("静态配置 (configs):\n")
fmt.Printf(" AppID: %s\n", maskStr(c.Wechat.AppID))
fmt.Printf(" AppSecret: %s\n", maskStr(c.Wechat.AppSecret))
fmt.Printf(" LotteryResultTemplateID: %s\n", c.Wechat.LotteryResultTemplateID)
// 连接数据库检查 system_configs
dsn := "root:bindbox2025kdy@tcp(106.54.232.2:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(gormmysql.Open(dsn), &gorm.Config{Logger: gormlogger.Default.LogMode(gormlogger.Silent)})
if err != nil {
panic("failed to connect database: " + err.Error())
}
// 检查 system_configs 中的模板 ID
type SystemConfig struct {
ConfigKey string
ConfigValue string
}
var cfg SystemConfig
err = db.Table("system_configs").Where("config_key = ?", "wechat.lottery_result_template_id").First(&cfg).Error
if err == nil {
fmt.Printf("\n动态配置 (system_configs):\n")
fmt.Printf(" wechat.lottery_result_template_id: %s\n", cfg.ConfigValue)
} else {
fmt.Printf("\n动态配置 (system_configs): 未配置 wechat.lottery_result_template_id\n")
fmt.Println("将使用静态配置的模板 ID")
}
// 确定要使用的模板 ID
templateID := c.Wechat.LotteryResultTemplateID
if cfg.ConfigValue != "" {
templateID = cfg.ConfigValue
}
if templateID == "" {
fmt.Println("\n❌ LotteryResultTemplateID 未配置!")
return
}
fmt.Printf("\n使用的模板 ID: %s\n", templateID)
// 获取一个有 openid 的用户进行测试
type User struct {
ID int64
Openid string
}
var user User
if err := db.Table("users").Where("openid != ''").First(&user).Error; err != nil {
fmt.Printf("\n❌ 没有找到有 openid 的用户: %v\n", err)
return
}
fmt.Printf("测试用户: ID=%d, Openid=%s\n", user.ID, maskStr(user.Openid))
// 尝试发送通知
fmt.Println("\n========== 发送测试通知 ==========")
notifyCfg := &notify.WechatNotifyConfig{
AppID: c.Wechat.AppID,
AppSecret: c.Wechat.AppSecret,
LotteryResultTemplateID: templateID,
}
err = notify.SendLotteryResultNotification(
context.Background(),
notifyCfg,
user.Openid,
"测试活动名称",
[]string{"测试奖品A", "测试奖品B"},
"TEST_ORDER_001",
time.Now(),
)
if err != nil {
fmt.Printf("\n❌ 发送失败: %v\n", err)
} else {
fmt.Println("\n✅ 发送成功!请检查微信是否收到通知。")
}
}
func maskStr(s string) string {
if len(s) <= 8 {
return s
}
return s[:4] + "****" + s[len(s)-4:]
}

View File

@ -1,50 +0,0 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/service/douyin"
syscfgsvc "bindbox-game/internal/service/sysconfig"
)
func main() {
orderID := flag.String("id", "", "抖音订单号 (order_id)")
flag.Parse()
if *orderID == "" {
log.Fatal("请提供订单号,例如: -id=6946062444563338504")
}
// 1. 初始化 MySQL
dbRepo, err := mysql.New()
if err != nil {
log.Fatalf("MySQL 初始化失败: %v", err)
}
// 2. 初始化 Logger (简易版)
customLogger, _ := logger.NewCustomLogger(dao.Use(dbRepo.GetDbW()), logger.WithOutputInConsole())
// 3. 初始化 Service
sysCfgSvc := syscfgsvc.New(customLogger, dbRepo)
douyinSvc := douyin.New(customLogger, dbRepo, sysCfgSvc, nil)
// 4. 执行测试
fmt.Printf("--- 正在测试订单号: %s ---\n", *orderID)
ctx := context.Background()
order, err := douyinSvc.GetOrderByOrderID(ctx, *orderID)
if err != nil {
log.Fatalf("查询失败: %v", err)
}
// 5. 打印结果
fmt.Println("查询成功!返回数据如下:")
data, _ := json.MarshalIndent(order, "", " ")
fmt.Println(string(data))
}

View File

@ -110,7 +110,7 @@ var (
proConfigs []byte proConfigs []byte
) )
func init() { func Init() {
var r io.Reader var r io.Reader
switch env.Active().Value() { switch env.Active().Value() {

View File

@ -2,31 +2,28 @@
local = 'zh-cn' local = 'zh-cn'
[mysql.read] [mysql.read]
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555' addr = '150.158.78.154:3306'
name = 'bindbox_game' name = 'bindbox_game'
pass = 'api2api..' pass = 'bindbox2025kdy'
user = 'root'
[mysql.write]
addr = '150.158.78.154:3306'
name = 'bindbox_game'
pass = 'bindbox2025kdy'
user = 'root' user = 'root'
[redis] [redis]
addr = "118.25.13.43:8379" addr = "127.0.0.1:6379"
pass = "xbm#2023by1024" pass = ""
db = 5 db = 5
[mysql.write]
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
name = 'bindbox_game'
pass = 'api2api..'
user = 'root'
[jwt] [jwt]
admin_secret = "m9ycX9RTPyuYTWw9FrCc" admin_secret = "m9ycX9RTPyuYTWw9FrCc"
patient_secret = "AppUserJwtSecret2025" patient_secret = "AppUserJwtSecret2025"
[wechat]
app_id = "wx26ad074017e1e63f"
app_secret = "026c19ce4f3bb090c56573024c59a8be"
lottery_result_template_id = "O2eqJQD3pn-vQ6g2z9DWzINVwOmPoz8yW-172J_YcpI"
[cos] [cos]
bucket = "keaiya-1259195914" bucket = "keaiya-1259195914"
@ -40,13 +37,13 @@ base_url = ""
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c" commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"
[wechatpay] [wechatpay]
mchid = "1610439635" mchid = ""
serial_no = "3AFD505D597831F8E931EBFFEEB5976B81F66F03" serial_no = ""
private_key_path = "./configs/cert/apiclient_key.pem" private_key_path = ""
api_v3_key = "3tbwEFZV3fZtOslpUJC7Sacb8qjzhm05" api_v3_key = ""
notify_url = "https://mini-chat.1024tool.vip/api/pay/wechat/notify" notify_url = ""
public_key_id = "PUB_KEY_ID_0116104396352025041000211519001600" public_key_id = ""
public_key_path = "./configs/cert/pub_key.pem" public_key_path = ""
[aliyun_sms] [aliyun_sms]
access_key_id = "" access_key_id = ""
@ -54,5 +51,8 @@ access_key_secret = ""
sign_name = "" sign_name = ""
template_code = "" template_code = ""
[internal] [internal]
api_key = "bindbox-internal-secret-2024" api_key = "bindbox-internal-secret-2024"

View File

@ -2,54 +2,59 @@
local = 'zh-cn' local = 'zh-cn'
[mysql.read] [mysql.read]
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555' addr = "mysql:3306"
name = 'bindbox_game' user = "root"
pass = 'api2api..' pass = "bindbox2025kdy"
user = 'root' name = "bindbox_game"
[redis]
addr = "118.25.13.43:8379"
pass = "xbm#2023by1024"
db = 5
[mysql.write] [mysql.write]
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555' addr = "mysql:3306"
name = 'bindbox_game' user = "root"
pass = 'api2api..' pass = "bindbox2025kdy"
user = 'root' name = "bindbox_game"
[redis]
addr = "redis:6379"
pass = ""
db = 0
[jwt] [jwt]
admin_secret = "m9ycX9RTPyuYTWw9FrCc" admin_secret = "m9ycX9RTPyuYTWw9FrCc"
patient_secret = "AppUserJwtSecret2025" patient_secret = "AppUserJwtSecret2025"
[wechat] [wechat]
app_id = "wx26ad074017e1e63f" app_id = ""
app_secret = "026c19ce4f3bb090c56573024c59a8be" app_secret = ""
lottery_result_template_id = "O2eqJQD3pn-vQ6g2z9DWzINVwOmPoz8yW-172J_YcpI" lottery_result_template_id = ""
[cos] [cos]
bucket = "keaiya-1259195914" bucket = "keaiya-1259195914"
region = "ap-shanghai" region = "ap-shanghai"
secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6" secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6"
secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr" secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr"
# 可选:如有 CDN/自定义域名则填写,否则留空
base_url = "" base_url = ""
[random] [random]
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c" commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"
[wechatpay] [wechatpay]
mchid = "1610439635" mchid = ""
serial_no = "3AFD505D597831F8E931EBFFEEB5976B81F66F03" serial_no = ""
private_key_path = "./configs/cert/apiclient_key.pem" private_key_path = ""
api_v3_key = "3tbwEFZV3fZtOslpUJC7Sacb8qjzhm05" api_v3_key = ""
notify_url = "https://mini-chat.1024tool.vip/api/pay/wechat/notify" notify_url = ""
public_key_id = "PUB_KEY_ID_0116104396352025041000211519001600" public_key_id = ""
public_key_path = "./configs/cert/pub_key.pem" public_key_path = ""
[aliyun_sms] [aliyun_sms]
access_key_id = "LTAI5tJ55hp81F5HDa2oSYb3" access_key_id = ""
access_key_secret = "cUd3Ym73i7OKsDDBJre5IAkpwwTiLs" access_key_secret = ""
sign_name = "沙琪玛上海信息技术" sign_name = ""
template_code = "SMS_499200896" template_code = ""
[internal]
api_key = "bindbox-internal-secret-2024"
[otel]
enabled = true
endpoint = "tempo:4318"

View File

@ -23,9 +23,9 @@ admin_secret = "m9ycX9RTPyuYTWw9FrCc"
patient_secret = "AppUserJwtSecret2025" patient_secret = "AppUserJwtSecret2025"
[wechat] [wechat]
app_id = "wx26ad074017e1e63f" app_id = ""
app_secret = "026c19ce4f3bb090c56573024c59a8be" app_secret = ""
lottery_result_template_id = "O2eqJQD3pn-vQ6g2z9DWzINVwOmPoz8yW-172J_YcpI" lottery_result_template_id = ""
[cos] [cos]
bucket = "keaiya-1259195914" bucket = "keaiya-1259195914"
@ -38,13 +38,13 @@ base_url = ""
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c" commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"
[wechatpay] [wechatpay]
mchid = "1610439635" mchid = ""
serial_no = "3AFD505D597831F8E931EBFFEEB5976B81F66F03" serial_no = ""
private_key_path = "./configs/cert/apiclient_key.pem" private_key_path = ""
api_v3_key = "3tbwEFZV3fZtOslpUJC7Sacb8qjzhm05" api_v3_key = ""
notify_url = "https://kdy.1024tool.vip/api/pay/wechat/notify" notify_url = ""
public_key_id = "PUB_KEY_ID_0116104396352025041000211519001600" public_key_id = ""
public_key_path = "./configs/cert/pub_key.pem" public_key_path = ""
[aliyun_sms] [aliyun_sms]
access_key_id = "" access_key_id = ""

View File

@ -0,0 +1,100 @@
# BUG修复需求分析
## 任务概述
修复盲盒游戏系统中6个BUG问题。
## BUG清单
### BUG 1: 任务中心任务类型统计错误
**问题描述**: 设置任务是完成A活动才可以算完成但玩了一局B活动竟然也算任务成功了。
**根因分析**:
- 任务中心 `GetUserProgress` 函数 (`internal/service/task_center/service.go:290-386`)
- 该函数通过订单 `remark` 字段使用 LIKE 匹配来过滤活动ID
- 匹配模式: `%%activity:%d%%`
- **问题**: 虽然有活动ID过滤逻辑但需要确认任务配置时是否正确设置了 `activity_id`
- 相关代码位置: `service.go` 第306-312行
---
### BUG 2: 任务中心把商城订单也计入了
**问题描述**: 任务中心统计时不应该包含商城订单,应该根据设置的类型来结算。
**根因分析**:
- `GetUserProgress` 函数统计订单时只过滤了 `status = 2`(已支付)
- **问题**: 没有过滤 `source_type`,导致商城订单(`source_type = 1`)也被计入
- 订单 `source_type` 定义 (`model/orders.gen.go:20`):
- 1: 商城直购
- 2: 抽奖票据
- 3: 其他
- 4: 次数卡支付
---
### BUG 3: 活动盈亏仪表盘退款订单未排除
**问题描述**: 对用户订单进行退款了,统计不应该把这个订单累计进来。
**根因分析**:
- `DashboardActivityProfitLoss` 函数 (`internal/api/admin/dashboard_activity.go:132-139`)
- 营收统计查询条件: `orders.status = 2`(已支付)
- **问题**: 订单状态4表示已退款但当前只过滤了 `status = 2`,不会包含退款订单
- **实际问题**: 退款后订单状态应该从2变成4但如果状态未更新则会被统计。需要确认退款流程是否正确更新订单状态
---
### BUG 4: 活动盈亏抽奖记录缺少字段
**问题描述**: 需要在抽奖记录中体现 优惠券 / 道具卡 / 次数卡 字段。
**根因分析**:
- `DashboardActivityLogs` 函数 (`internal/api/admin/dashboard_activity.go:222-354`)
- 当前返回字段已包含:
- `coupon_name`: 通过 `orders.coupon_id` LEFT JOIN `system_coupons`
- `item_card_name`: 通过 `orders.item_card_id` LEFT JOIN `system_item_cards`
- **问题**: `source_type = 4` 表示次数卡支付,但次数卡使用信息存储在订单 `remark` 字段中(格式: `gp_use:ID:Count`),当前未解析显示
- 需要增加解析 `remark` 字段中的次数卡使用信息
---
### BUG 5: 一番赏不能使用优惠券
**问题描述**: 一番赏目前不能使用优惠券。
**根因分析**:
- `JoinLottery` 函数 (`internal/api/activity/lottery_app.go:78-81`)
- 优惠券检查逻辑: `if !activity.AllowCoupons && req.CouponID != nil`
- **问题**: 一番赏活动的 `AllowCoupons` 字段可能被设置为 `false`
- 数据库字段定义 (`model/activities.gen.go:27`): `AllowCoupons bool` 默认值为1允许
- **解决方向**:
1. 检查一番赏活动在数据库中的 `allow_coupons` 字段值
2. 如果业务上确实不允许,则是配置问题而非代码问题
3. 如果业务上应该允许,需修改活动配置
---
### BUG 6: 活动盈亏出现已下架活动数据
**问题描述**: 活动盈亏里面出现了以前已经下架了的数据,应该按照现在活动表存在的活动来统计。
**根因分析**:
- `DashboardActivityProfitLoss` 函数 (`internal/api/admin/dashboard_activity.go:58-75`)
- 当前查询直接从 `activities` 表获取活动列表
- 支持按 `status` 过滤1进行中 2下线
- **问题**: 虽然支持状态过滤,但默认不过滤任何状态
- 另外活动表使用了软删除 (`deleted_at`),但需确认是否正确应用了软删除条件
## 需求理解
| BUG编号 | 问题类型 | 修复难度 | 涉及文件 |
|---------|----------|----------|----------|
| BUG 1 | 业务逻辑 | 中 | `service/task_center/service.go` |
| BUG 2 | 业务逻辑 | 低 | `service/task_center/service.go` |
| BUG 3 | 数据过滤 | 低 | `api/admin/dashboard_activity.go` |
| BUG 4 | 字段缺失 | 低 | `api/admin/dashboard_activity.go` |
| BUG 5 | 配置问题 | 低 | 需检查数据库配置 |
| BUG 6 | 数据过滤 | 低 | `api/admin/dashboard_activity.go` |
## 待确认问题
1. **BUG 1**: 任务配置时,`task_center_task_tiers.activity_id` 字段是否正确设置?
2. **BUG 3**: 退款时订单状态是否正确更新为4
3. **BUG 5**: 一番赏活动的 `allow_coupons` 数据库字段当前值是什么?是配置问题还是需要代码修复?
4. **BUG 6**: 是否需要默认只显示在线活动status=1还是只过滤软删除的活动

149
docs/lottery_algorithm.md Normal file
View File

@ -0,0 +1,149 @@
# 抽奖与公平性算法技术白皮书
## 1. 概述
本系统采用 **「承诺机制 (Commitment Scheme)」** 结合 **HMAC-SHA256** 算法,确保抽奖过程的**不可预测性**、**可验证性**和**不可篡改性**。
核心原则:
1. **事前承诺**活动开始前生成随机种子并公布其哈希值Commitment
2. **事后验证**活动结束后公布种子明文Reveal用户可复算验证。
3. **确定性算法**:输入(种子 + 上下文)确定,输出必然唯一。
---
## 2. 核心机制:承诺方案
### 2.1 种子生成
每个活动 (`Activity`) 在创建或发布时,系统服务器端会生成一个高质量的 32 字节随机种子 (`ServerSeed`)。
```go
// 伪代码示例
seed := make([]byte, 32)
rand.Read(seed) // 使用 crypto/rand 生成强随机数
```
### 2.2 承诺哈希 (Commitment Hash)
在数据库确立活动数据的一瞬间,系统计算种子的 SHA256 哈希值,作为**承诺**存储并对用户可见(虽然前端可能选择性展示)。
$$ SeedHash = \text{SHA256}(ServerSeed) $$
此哈希值一经生成不可更改,确保了服务器无法在后续过程中偷偷替换种子来操纵结果。
### 2.3 验证凭据 (Receipt)
每次抽奖完成后,系统会生成一份数字凭据 (`ActivityDrawReceipts`),其中包含:
- `issue_id`: 期号
- `seed_hash`: 对应的种子哈希
- `nonce` / `salt`: 随机盐值或防重随机数
- `snapshot`: 当时的奖池状态快照(权重/格位)
---
## 3. 算法实现
### 3.1 无限赏 (Weighted Random)
适用于奖品无限库存或按权重概率抽取的模式。
**算法流程**
1. **输入**
- $Seed$: 全局活动种子
- $IssueID$: 期号
- $UserID$: 用户ID
- $Salt$: 每次请求生成的 16 字节随机盐值
- $Rewards$: 奖品列表,包含权重 $w_i$
2. **随机数生成**
使用 HMAC-SHA256 派生出一个确定性的随机数 $R$。
$$ \text{payload} = \text{fmt.Sprintf("draw:issue:\%d|user:\%d|salt:\%x", IssueID, UserID, Salt)} $$
$$ H = \text{HMAC-SHA256}(Seed, \text{payload}) $$
$$ R = \text{BigEndianUint64}(H[0:8]) \pmod {\sum w_i} $$
3. **结果选择**
遍历奖品列表,累加权重查找 $R$ 落在哪个区间。
**代码逻辑**
```go
mac := hmac.New(sha256.New, seedKey)
mac.Write([]byte(fmt.Sprintf("draw:issue:%d|user:%d|salt:%x", issueID, userID, salt)))
sum := mac.Sum(nil)
rnd := int64(binary.BigEndian.Uint64(sum[:8]) % uint64(totalWeight))
```
### 3.2 一番赏 (Ichiban / Shuffle)
适用于“箱内抽赏”模式,奖品总量固定,位置固定,采用先洗牌后抽取的逻辑。
**算法流程**
1. **输入**
- $Seed$: 全局活动种子
- $IssueID$: 期号(每一个箱子是一个 Issue
- $TotalSlots$: 总格位数(例如 80 发)
- $Rewards$: 初始有序的奖品列表(填充后的平铺列表)
2. **确定性洗牌 (Deterministic Shuffle)**
使用 Fisher-Yates 洗牌算法,配合 HMAC-SHA256 生成的随机序列对奖品位置进行打乱。
对于 $i$ 从 $TotalSlots-1$ 到 $1$
$$ \text{payload}_i = \text{fmt.Sprintf("shuffle:\%d|issue:\%d", i, IssueID)} $$
$$ H_i = \text{HMAC-SHA256}(Seed, \text{payload}_i) $$
$$ j = \text{BigEndianUint64}(H_i[0:8]) \pmod {(i+1)} $$
交换索引 $i$ 和 $j$ 的元素。
3. **结果获取**
用户选择的格位号 $k$ (1-based) 对应洗牌后数组的索引 $k-1$ 处的奖品。
$$ Reward = ShuffledRewards[SelectedSlot - 1] $$
**特性**
- **预定性**:只要种子确定,箱子那一刻的奖品排列就已注定,不论谁来抽、何时抽,第 N 格永远是那个奖品。
- **公平性**HMAC 的均匀分布保证了洗牌的随机性。
---
## 4. 验证指南
为了验证系统的公平性,用户或监管方可以使用官方提供的验证工具(`VerifyTool.exe`)进行独立计算。
### 4.1 获取验证参数
从 API 或页面获取以下信息(活动结束后公开):
1. `server_seed_hex`: 服务器种子(十六进制)
2. `issue_id`: 期号
3. `user_id` & `salt`: (仅无限赏需要)
4. `slot_index`: (仅一番赏需要)
5. `reward_config`: 奖品配置列表(验证前需要构建相同的初始列表)
### 4.2 运行验证工具
**无限赏验证命令示例**
```bash
./VerifyTool verify-unlimited \
--seed "WaitToReveal32BytesHex..." \
--issue 1001 \
--user 12345 \
--salt "RandomSaltHex..." \
--weights "10,50,200,500"
```
**一番赏验证命令示例**
```bash
./VerifyTool verify-ichiban \
--seed "WaitToReveal32BytesHex..." \
--issue 2002 \
--slot 5 \
--rewards "A:2,B:4,C:10,D:64"
```
*(注rewards 格式为 `奖项:数量`,如 A赏2个, B赏4个...)*
### 4.3 验证原理
验证工具内置了与服务器完全相同的算法逻辑Go 源码编译)。输入相同的种子和上下文,必将输出相同的中奖结果。
---
## 5. 安全性声明
1. **种子保密**`ServerSeed` 在存储层加密保存,仅在活动结束或特定审计时刻解密公开。
2. **结果不可逆**:无法通过哈希值反推种子。
3. **防预测**
- 无限赏:引入了 `Salt`(真随机生成),即使用户猜到了种子,也无法预测下一次抽奖结果(因为 Salt 每次不同)。
- 一番赏:种子一旦确定,序列即确定。我们在活动开始前才生成种子和 Commitment确保无人包括管理员能提前知晓排列。

View File

@ -0,0 +1,121 @@
# 抖音游戏翻牌特效需求对齐
## 原始需求
用户希望在 `douyin_game` 项目中开发一个翻牌 Web 应用,参考泡泡玛特直播间的翻牌抽盒效果。
## 参考截图分析
![参考图1](/Users/win/.gemini/antigravity/brain/192e2707-e873-48c7-9161-73a09b835351/uploaded_image_0_1768026980546.jpg)
![参考图2](/Users/win/.gemini/antigravity/brain/192e2707-e873-48c7-9161-73a09b835351/uploaded_image_1_1768026980546.jpg)
![参考图3](/Users/win/.gemini/antigravity/brain/192e2707-e873-48c7-9161-73a09b835351/uploaded_image_2_1768026980546.jpg)
### 核心功能分析
从截图中观察到以下特征:
1. **卡片网格布局**
- 3x4 的卡片网格(共 12 张卡片)
- 每张卡片显示挂件产品图片
- 绿色方格背景,白色卡片
2. **卡片状态**
- 未翻开状态:显示产品缩略图+用户头像+昵称+倒计时
- 翻开后状态:大图展示产品详情
3. **翻牌特效**图3展示
- 深色星空背景
- 星星闪烁粒子效果
- 产品大图居中展示
- 卡片 3D 翻转动画
4. **交互元素**
- 用户头像标识
- 昵称显示
- 抽取倒计时
---
## 边界确认
### 开发范围
- [x] 卡片网格布局 UI
- [x] 3D 翻牌动画效果
- [x] 星空背景特效
- [x] 粒子闪烁效果
- [x] 产品大图展示遮罩层
### 排除范围
- [ ] 后端抽盒逻辑(已有)
- [ ] 支付流程
- [ ] 用户身份认证
---
## 疑问澄清
> [!IMPORTANT]
> 以下问题需要用户确认
### 1. 项目位置
当前 `douyin_game` 目录为空,请确认:
- 是否在此目录新建独立项目?
- 还是集成到现有 `game/app` 项目中?
### 2. 技术栈选择
现有项目使用 **React + TypeScript + Vite + TailwindCSS**
- 是否沿用相同技术栈?
- 或者使用纯 HTML/CSS/JS 开发独立页面?
### 3. 数据来源
翻牌游戏的数据(产品信息、用户信息等):
- 是否需要对接后端 API
- 还是先开发静态演示版本?
### 4. 翻牌触发方式
用户如何触发翻牌:
- 点击自己预定的卡片?
- 观看他人翻牌的直播效果?
- 两者结合?
### 5. 特效细节偏好
关于"人物背后的翻牌特效",请确认:
- **星空背景**:是否需要动态渐变星空?
- **粒子效果**:闪烁星星数量和密度?
- **翻转动画**:水平翻转还是垂直翻转?
- **展示遮罩**:是否需要毛玻璃效果?
---
## 技术理解
### 现有项目分析
项目 `game/app` 技术栈:
- React 19 + TypeScript
- Vite 构建工具
- TailwindCSS 样式
- 已有丰富的 CSS 动画效果(`Explosion.css`
### 翻牌特效技术方案
| 特效组件 | 技术实现 |
|---------|---------|
| 3D 翻牌动画 | CSS `transform: rotateY()` + `perspective` |
| 星空背景 | 深色渐变 + CSS `radial-gradient` |
| 星星闪烁 | CSS `@keyframes` 动画 + 随机延迟 |
| 粒子效果 | Canvas API 或 CSS 伪元素 |
| 遮罩层 | `backdrop-filter: blur()` 毛玻璃效果 |
---
## 等待用户回复
上述疑问需要用户回复后才能进入架构设计阶段。

View File

@ -0,0 +1,27 @@
# 翻牌特效项目共识 (CONSENSUS)
## 需求描述
开发一个基于 React 的翻牌 Web 应用,模拟抽盒机的翻牌流程及特效。重点在于 3D 翻牌动画、星空粒子背景以及整体视觉体验。
## 验收标准
1. **网格布局**:实现 3x4 的响应式卡片网格。
2. **3D 翻牌**:卡片点击后执行平滑的 3D 翻转动画。
3. **特效层**:翻牌时伴随全屏星空背景和闪烁粒子特效。
4. **大图展示**:翻牌后产品大图居中弹出,具备毛玻璃遮罩。
5. **静态数据**:使用 Mock 数据驱动,包含产品图片、用户头像、昵称、倒计时。
## 技术方案
- **框架**React 19 + TypeScript
- **构建工具**Vite
- **样式**TailwindCSS + CSS Modules/Raw CSS (用于复杂动画)
- **动效库**Framer Motion (可选,若需更细腻控制) 或 纯 CSS 3D Transforms。
## 技术约束
- 纯前端实现,暂不对接后端接口。
- 代码部署在 `douyin_game` 目录下。
## 集成方案
- 作为一个独立项目在 `douyin_game` 中进行初始化。
## 风险与假设
- 所有图像资源(产品图、头像)暂时使用 generate_image 工具生成的占位图或默认素材。

View File

@ -0,0 +1,53 @@
# 翻牌特效架构设计 (DESIGN)
## 整体架构
项目采用单页应用架构,通过 React 状态驱动 UI 更新和动效触发。
```mermaid
graph TD
App[App Component] --> GameState[Game State: revealedCards, selectedId]
App --> StarLayer[StarryBackground Component]
App --> Grid[CardGrid Component]
Grid --> Card[FlipCard Component]
Card --> CardUI[Front: Avatar/Info | Back: ProductImg]
App --> Modal[ProductModal Component]
```
## 核心组件设计
### 1. FlipCard (翻牌组件)
- **Props**: `id`, `user`, `product`, `isRevealed`, `onFlip`.
- **CSS**:
- `.card-inner`: `transition: transform 0.6s; transform-style: preserve-3d;`
- `.card-front`, `.card-back`: `backface-visibility: hidden;`
### 2. StarryBackground (星空背景)
- 实现多层叠加背景:
- 底层:深蓝色渐变 `#0a0b1e` -> `#161b33`
- 中层静态微小星星CSS 粒状纹理)。
- 高层:关键帧动画模拟的闪烁星星(不同大小、延时)。
### 3. ProductModal (展示遮罩)
- 当 `selectedId` 存在时显示。
- **Style**: `fixed inset-0`, `backdrop-filter: blur(8px)`, `bg-black/40`
- **Animation**: 放大缩放并带有一圈光晕特效。
## 实现细节:粒子特效
当翻牌触发时,在卡片位置生成一组临时的粒子元素:
- 随机方向发射。
- 逐渐变小并透明。
- 使用 React `useState` 管理粒子生命周期。
## 目录结构 (douyin_game)
```text
src/
components/
CardGrid.tsx
FlipCard.tsx
StarryBackground.tsx
ProductModal.tsx
assets/
images/
App.tsx
index.css
```

View File

@ -0,0 +1,37 @@
# 翻牌特效原子任务 (TASK)
## 任务依赖图
```mermaid
graph TD
T1[T1: 初始化项目环境] --> T2[T2: 实现星空背景层]
T2 --> T3[T3: 开发 3D 翻牌组件]
T3 --> T4[T4: 组装网格与逻辑控制]
T4 --> T5[T5: 完善大图展示与粒子特效]
```
## 原子任务定义
### T1: 初始化项目环境
- **输入**: 空目录 `douyin_game`
- **输出**: Vite + React 项目骨架,安装 TailwindCSS
- **验收**: `npm run dev` 可正常启动
### T2: 实现星空背景层 (StarryBackground)
- **输入**: Tailwind 配置
- **输出**: 一个全屏背景组件,带有动态闪烁星星
- **验收**: 背景显示深蓝渐变,星星随机分布且有呼吸感
### T3: 开发 3D 翻牌组件 (FlipCard)
- **输入**: 基础 CSS 3D 知识
- **输出**: 支持正面(用户信息)和背面(产品图)切换的卡片
- **验收**: 点击触发平滑翻转,背面图片居中
### T4: 组装网格与逻辑控制 (CardGrid)
- **输入**: 3x4 布局需求
- **输出**: 一个包含 12 张卡片的网格,支持单次点击状态管理
- **验收**: 点击不同卡片各自翻转
### T5: 完善大图展示与粒子特效 (ProductModal)
- **输入**: 翻牌触发回调
- **输出**: 点击翻牌后弹出居中大图,背景变暗且带粒子飞散
- **验收**: 展示效果震撼,符合泡泡玛特直播间风格

View File

@ -1,47 +1,112 @@
package admin package admin
import ( import (
"net/http" "bindbox-game/internal/code"
"strconv" "bindbox-game/internal/pkg/core"
"bindbox-game/internal/code" "bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/pkg/core" activitysvc "bindbox-game/internal/service/activity"
"bindbox-game/internal/repository/mysql/dao" "net/http"
activitysvc "bindbox-game/internal/service/activity" "strconv"
) )
type activityCommitGenerateResp struct{ SeedVersion int32 `json:"seed_version"` } type activityCommitGenerateResp struct {
type activityCommitSummaryResp struct{ SeedVersion int32 `json:"seed_version"`; Algo string `json:"algo"`; HasSeed bool `json:"has_seed"`; LenSeedMaster int `json:"len_seed_master"`; LenSeedHash int `json:"len_seed_hash"`; LenItemsRoot int `json:"len_items_root"`; ItemsRootHex string `json:"items_root_hex"` } SeedVersion int32 `json:"seed_version"`
}
type activityCommitSummaryResp struct {
SeedVersion int32 `json:"seed_version"`
Algo string `json:"algo"`
HasSeed bool `json:"has_seed"`
LenSeedMaster int `json:"len_seed_master"`
LenSeedHash int `json:"len_seed_hash"`
LenItemsRoot int `json:"len_items_root"`
ItemsRootHex string `json:"items_root_hex"`
}
type activityCredentialResp struct {
SeedMasterHex string `json:"seed_master_hex"`
SeedHashHex string `json:"seed_hash_hex"`
ItemsRootHex string `json:"items_root_hex"`
}
func (h *handler) GenerateActivityCommitmentGeneral() core.HandlerFunc { func (h *handler) GenerateActivityCommitmentGeneral() core.HandlerFunc {
return func(ctx core.Context) { return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64); if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID")); return } activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
svc := activitysvc.NewActivityCommitmentService(dao.Use(h.repo.GetDbR()), dao.Use(h.repo.GetDbW()), h.repo) if err != nil {
ver, e := svc.Generate(ctx.RequestContext(), activityID) ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
if e != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 170301, e.Error())); return } return
ctx.Payload(&activityCommitGenerateResp{SeedVersion: ver}) }
} svc := activitysvc.NewActivityCommitmentService(dao.Use(h.repo.GetDbR()), dao.Use(h.repo.GetDbW()), h.repo)
ver, e := svc.Generate(ctx.RequestContext(), activityID)
if e != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170301, e.Error()))
return
}
ctx.Payload(&activityCommitGenerateResp{SeedVersion: ver})
}
} }
func (h *handler) GetActivityCommitmentSummaryGeneral() core.HandlerFunc { func (h *handler) GetActivityCommitmentSummaryGeneral() core.HandlerFunc {
return func(ctx core.Context) { return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64); if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID")); return } activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
svc := activitysvc.NewActivityCommitmentService(dao.Use(h.repo.GetDbR()), dao.Use(h.repo.GetDbW()), h.repo) if err != nil {
sum, e := svc.Summary(ctx.RequestContext(), activityID) ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
if e != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 170302, e.Error())); return } return
var lenMaster, lenHash, lenRoot *int }
_ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_master) FROM activities WHERE id=?", activityID).Scan(&lenMaster) svc := activitysvc.NewActivityCommitmentService(dao.Use(h.repo.GetDbR()), dao.Use(h.repo.GetDbW()), h.repo)
_ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_hash) FROM activities WHERE id=?", activityID).Scan(&lenHash) sum, e := svc.Summary(ctx.RequestContext(), activityID)
_ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_items_root) FROM activities WHERE id=?", activityID).Scan(&lenRoot) if e != nil {
var itemsHex *string ctx.AbortWithError(core.Error(http.StatusBadRequest, 170302, e.Error()))
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_items_root) FROM activities WHERE id=?", activityID).Scan(&itemsHex) return
}
lm, lh, lr := 0, 0, 0 var lenMaster, lenHash, lenRoot *int
if lenMaster != nil { lm = *lenMaster } _ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_master) FROM activities WHERE id=?", activityID).Scan(&lenMaster)
if lenHash != nil { lh = *lenHash } _ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_hash) FROM activities WHERE id=?", activityID).Scan(&lenHash)
if lenRoot != nil { lr = *lenRoot } _ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_items_root) FROM activities WHERE id=?", activityID).Scan(&lenRoot)
ih := "" var itemsHex *string
if itemsHex != nil { ih = *itemsHex } _ = h.repo.GetDbR().Raw("SELECT HEX(commitment_items_root) FROM activities WHERE id=?", activityID).Scan(&itemsHex)
ctx.Payload(&activityCommitSummaryResp{SeedVersion: sum.SeedVersion, Algo: sum.Algo, HasSeed: sum.HasSeed, LenSeedMaster: lm, LenSeedHash: lh, LenItemsRoot: lr, ItemsRootHex: ih}) lm, lh, lr := 0, 0, 0
} if lenMaster != nil {
lm = *lenMaster
}
if lenHash != nil {
lh = *lenHash
}
if lenRoot != nil {
lr = *lenRoot
}
ih := ""
if itemsHex != nil {
ih = *itemsHex
}
ctx.Payload(&activityCommitSummaryResp{SeedVersion: sum.SeedVersion, Algo: sum.Algo, HasSeed: sum.HasSeed, LenSeedMaster: lm, LenSeedHash: lh, LenItemsRoot: lr, ItemsRootHex: ih})
}
}
func (h *handler) GetActivityCredential() core.HandlerFunc {
return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
var seedMasterHex, seedHashHex, itemsRootHex *string
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_seed_master) FROM activities WHERE id=?", activityID).Scan(&seedMasterHex)
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_seed_hash) FROM activities WHERE id=?", activityID).Scan(&seedHashHex)
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_items_root) FROM activities WHERE id=?", activityID).Scan(&itemsRootHex)
resp := &activityCredentialResp{}
if seedMasterHex != nil {
resp.SeedMasterHex = *seedMasterHex
}
if seedHashHex != nil {
resp.SeedHashHex = *seedHashHex
}
if itemsRootHex != nil {
resp.ItemsRootHex = *itemsRootHex
}
ctx.Payload(resp)
}
} }

View File

@ -9,6 +9,8 @@ import (
bannersvc "bindbox-game/internal/service/banner" bannersvc "bindbox-game/internal/service/banner"
channelsvc "bindbox-game/internal/service/channel" channelsvc "bindbox-game/internal/service/channel"
douyinsvc "bindbox-game/internal/service/douyin" douyinsvc "bindbox-game/internal/service/douyin"
gamesvc "bindbox-game/internal/service/game"
livestreamsvc "bindbox-game/internal/service/livestream"
productsvc "bindbox-game/internal/service/product" productsvc "bindbox-game/internal/service/product"
snapshotsvc "bindbox-game/internal/service/snapshot" snapshotsvc "bindbox-game/internal/service/snapshot"
syscfgsvc "bindbox-game/internal/service/sysconfig" syscfgsvc "bindbox-game/internal/service/sysconfig"
@ -34,6 +36,7 @@ type handler struct {
snapshotSvc snapshotsvc.Service snapshotSvc snapshotsvc.Service
rollbackSvc snapshotsvc.RollbackService rollbackSvc snapshotsvc.RollbackService
douyinSvc douyinsvc.Service douyinSvc douyinsvc.Service
livestream livestreamsvc.Service
} }
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler { func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler {
@ -56,6 +59,7 @@ func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler
syscfg: syscfgSvc, syscfg: syscfgSvc,
snapshotSvc: snapshotSvc, snapshotSvc: snapshotSvc,
rollbackSvc: rollbackSvc, rollbackSvc: rollbackSvc,
douyinSvc: douyinsvc.New(logger, db, syscfgSvc, nil), douyinSvc: douyinsvc.New(logger, db, syscfgSvc, gamesvc.NewTicketService(logger, db), userSvc),
livestream: livestreamsvc.New(logger, db),
} }
} }

View File

@ -8,7 +8,9 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"strings"
"time" "time"
) )
@ -16,19 +18,22 @@ type activityProfitLossRequest struct {
Page int `form:"page"` Page int `form:"page"`
PageSize int `form:"page_size"` PageSize int `form:"page_size"`
Name string `form:"name"` Name string `form:"name"`
Status int32 `form:"status"` // 1进行中 2下线 Status int32 `form:"status"` // 1进行中 2下线
SortBy string `form:"sort_by"` // profit, profit_asc, profit_rate, draw_count
} }
type activityProfitLossItem struct { type activityProfitLossItem struct {
ActivityID int64 `json:"activity_id"` ActivityID int64 `json:"activity_id"`
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"`
PlayerCount int64 `json:"player_count"` PlayerCount int64 `json:"player_count"`
TotalRevenue int64 `json:"total_revenue"` // 实际支付金额 (分) TotalRevenue int64 `json:"total_revenue"` // 实际支付金额 (分)
TotalCost int64 `json:"total_cost"` // 奖品标价总和 (分) TotalDiscount int64 `json:"total_discount"` // 优惠券抵扣金额 (分)
Profit int64 `json:"profit"` // Revenue - Cost TotalGamePassValue int64 `json:"total_game_pass_value"` // 次卡价值 (分)
ProfitRate float64 `json:"profit_rate"` // Profit / Revenue TotalCost int64 `json:"total_cost"` // 奖品标价总和 (分)
Profit int64 `json:"profit"` // (Revenue + Discount + GamePassValue) - Cost
ProfitRate float64 `json:"profit_rate"` // Profit / (Revenue + Discount + GamePassValue)
} }
type activityProfitLossResponse struct { type activityProfitLossResponse struct {
@ -54,20 +59,42 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
db := h.repo.GetDbR().WithContext(ctx.RequestContext()) db := h.repo.GetDbR().WithContext(ctx.RequestContext())
// 1. 获取活动列表基础信息
// 1. 获取活动列表基础信息 // 1. 获取活动列表基础信息
var activities []model.Activities var activities []model.Activities
query := db.Table(model.TableNameActivities) // 仅查询有完整配置(Issue->RewardSettings)且未删除的活动
// 使用 Raw SQL 避免 GORM 自动注入 ambiguous 的 deleted_at
rawSubQuery := fmt.Sprintf(`
SELECT activity_issues.activity_id
FROM %s AS activity_issues
JOIN %s AS activity_reward_settings ON activity_reward_settings.issue_id = activity_issues.id
WHERE activity_issues.deleted_at IS NULL
AND activity_reward_settings.deleted_at IS NULL
`, model.TableNameActivityIssues, model.TableNameActivityRewardSettings)
query := db.Table(model.TableNameActivities).
Where("activities.deleted_at IS NULL").
Where(fmt.Sprintf("activities.id IN (%s)", rawSubQuery))
if req.Name != "" { if req.Name != "" {
query = query.Where("name LIKE ?", "%"+req.Name+"%") query = query.Where("activities.name LIKE ?", "%"+req.Name+"%")
} }
if req.Status > 0 { if req.Status > 0 {
query = query.Where("status = ?", req.Status) query = query.Where("activities.status = ?", req.Status)
} }
var total int64 var total int64
query.Count(&total) query.Count(&total)
if err := query.Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Order("id DESC").Find(&activities).Error; err != nil { // 如果有排序需求,先获取所有活动计算盈亏后排序,再分页
// 如果没有排序需求,直接数据库分页
needCustomSort := req.SortBy != ""
var limitQuery = query
if !needCustomSort {
limitQuery = query.Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize)
}
if err := limitQuery.Order("id DESC").Find(&activities).Error; err != nil {
h.logger.Error(fmt.Sprintf("GetActivityProfitLoss activities error: %v", err)) h.logger.Error(fmt.Sprintf("GetActivityProfitLoss activities error: %v", err))
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 21021, err.Error())) ctx.AbortWithError(core.Error(http.StatusInternalServerError, 21021, err.Error()))
return return
@ -115,10 +142,12 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
} }
} }
// 3. 统计营收 (通过 orders 关联 activity_draw_logs) // 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
// BUG修复排除已退款订单(status=4)
type revenueStat struct { type revenueStat struct {
ActivityID int64 ActivityID int64
TotalRevenue int64 TotalRevenue int64
TotalDiscount int64
} }
var revenueStats []revenueStat var revenueStats []revenueStat
@ -128,10 +157,10 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
// 然后通过 issue_id 关联 activity_issues 找到 activity_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"). Select("activity_issues.activity_id, SUM(orders.actual_amount) as total_revenue, SUM(orders.discount_amount) as total_discount").
Joins("JOIN (SELECT order_id, MAX(issue_id) as issue_id FROM activity_draw_logs GROUP BY order_id) dl ON dl.order_id = orders.id"). Joins("JOIN (SELECT order_id, MAX(issue_id) as issue_id FROM activity_draw_logs GROUP BY order_id) dl ON dl.order_id = orders.id").
Joins("JOIN activity_issues ON activity_issues.id = dl.issue_id"). Joins("JOIN activity_issues ON activity_issues.id = dl.issue_id").
Where("orders.status = ?", 2). // 已支付 Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
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(&revenueStats).Error Scan(&revenueStats).Error
@ -143,6 +172,7 @@ 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 = s.TotalRevenue
item.TotalDiscount = s.TotalDiscount
} }
} }
@ -165,17 +195,80 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
} }
} }
// 5. 计算盈亏和比率 // 5. 统计次卡价值 (0元订单按活动单价计算)
// 先获取各活动的单价
activityPriceMap := make(map[int64]int64)
for _, a := range activities {
activityPriceMap[a.ID] = a.PriceDraw
}
// 统计每个活动的0元订单对应的抽奖次数 (次卡支付)
// BUG修复之前统计的是订单数量但一个订单可能包含多次抽奖
// 正确做法是统计抽奖次数,再乘以活动单价
type gamePassStat struct {
ActivityID int64
GamePassDraws int64 // 抽奖次数,非订单数
}
var gamePassStats []gamePassStat
db.Table(model.TableNameActivityDrawLogs).
Select("activity_issues.activity_id, COUNT(activity_draw_logs.id) as game_pass_draws").
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
Joins("JOIN orders ON orders.id = activity_draw_logs.order_id").
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
Where("orders.actual_amount = 0"). // 0元订单 = 次卡支付
Where("activity_issues.activity_id IN ?", activityIDs).
Group("activity_issues.activity_id").
Scan(&gamePassStats)
for _, s := range gamePassStats {
if item, ok := activityMap[s.ActivityID]; ok {
// 次卡价值 = 次卡抽奖次数 * 活动单价
item.TotalGamePassValue = s.GamePassDraws * activityPriceMap[s.ActivityID]
}
}
// 6. 计算盈亏和比率
// 公式: 盈亏 = (支付金额 + 优惠券抵扣 + 次卡价值) - 产品成本
finalList := make([]activityProfitLossItem, 0, len(activities)) finalList := make([]activityProfitLossItem, 0, len(activities))
for _, a := range activities { for _, a := range activities {
item := activityMap[a.ID] item := activityMap[a.ID]
item.Profit = item.TotalRevenue - item.TotalCost totalIncome := item.TotalRevenue + item.TotalDiscount + item.TotalGamePassValue
if item.TotalRevenue > 0 { item.Profit = totalIncome - item.TotalCost
item.ProfitRate = float64(item.Profit) / float64(item.TotalRevenue) if totalIncome > 0 {
item.ProfitRate = float64(item.Profit) / float64(totalIncome)
} }
finalList = append(finalList, *item) finalList = append(finalList, *item)
} }
// 按请求的字段排序
if needCustomSort {
sort.Slice(finalList, func(i, j int) bool {
switch req.SortBy {
case "profit":
return finalList[i].Profit > finalList[j].Profit
case "profit_asc":
return finalList[i].Profit < finalList[j].Profit
case "profit_rate":
return finalList[i].ProfitRate > finalList[j].ProfitRate
case "draw_count":
return finalList[i].DrawCount > finalList[j].DrawCount
default:
return false // 保持原有顺序 (id DESC)
}
})
// 排序后再分页
start := (req.Page - 1) * req.PageSize
end := start + req.PageSize
if start > len(finalList) {
start = len(finalList)
}
if end > len(finalList) {
end = len(finalList)
}
finalList = finalList[start:end]
}
ctx.Payload(&activityProfitLossResponse{ ctx.Payload(&activityProfitLossResponse{
Page: req.Page, Page: req.Page,
PageSize: req.PageSize, PageSize: req.PageSize,
@ -191,20 +284,38 @@ type activityLogsRequest struct {
} }
type activityLogItem struct { type activityLogItem struct {
ID int64 `json:"id"` ID int64 `json:"id"`
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"`
ProductID int64 `json:"product_id"` ProductID int64 `json:"product_id"`
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
ProductImage string `json:"product_image"` ProductImage string `json:"product_image"`
ProductPrice int64 `json:"product_price"` ProductPrice int64 `json:"product_price"`
OrderAmount int64 `json:"order_amount"` ProductQuantity int64 `json:"product_quantity"` // 奖品数量
DiscountAmount int64 `json:"discount_amount"` // New: 优惠金额 OrderAmount int64 `json:"order_amount"`
PayType string `json:"pay_type"` // New: 支付方式/类型 (现金/道具卡/次数卡) OrderNo string `json:"order_no"` // 订单号
UsedCard string `json:"used_card"` // New: 使用的卡券名称 DiscountAmount int64 `json:"discount_amount"` // 优惠金额(分)
Profit int64 `json:"profit"` PayType string `json:"pay_type"` // 支付方式/类型 (现金/道具卡/次数卡)
CreatedAt time.Time `json:"created_at"` UsedCard string `json:"used_card"` // 使用的卡券名称(兼容旧字段)
OrderStatus int32 `json:"order_status"` // 订单状态: 1待支付 2已支付 3已取消 4已退款
Profit int64 `json:"profit"`
CreatedAt time.Time `json:"created_at"`
// 新增:详细支付信息
PaymentDetails PaymentDetails `json:"payment_details"`
}
// PaymentDetails 支付详细信息
type PaymentDetails struct {
CouponUsed bool `json:"coupon_used"` // 是否使用优惠券
CouponName string `json:"coupon_name"` // 优惠券名称
CouponDiscount int64 `json:"coupon_discount"` // 优惠券抵扣金额(分)
ItemCardUsed bool `json:"item_card_used"` // 是否使用道具卡
ItemCardName string `json:"item_card_name"` // 道具卡名称
GamePassUsed bool `json:"game_pass_used"` // 是否使用次数卡
GamePassInfo string `json:"game_pass_info"` // 次数卡使用信息
PointsUsed bool `json:"points_used"` // 是否使用积分
PointsDiscount int64 `json:"points_discount"` // 积分抵扣金额(分)
} }
type activityLogsResponse struct { type activityLogsResponse struct {
@ -253,9 +364,18 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
ProductPrice int64 ProductPrice int64
OrderAmount int64 OrderAmount int64
DiscountAmount int64 DiscountAmount int64
PointsAmount int64 // 积分抵扣金额
OrderStatus int32 // 订单状态
SourceType int32 SourceType int32
CouponID int64
CouponName string CouponName string
ItemCardID int64
ItemCardName string ItemCardName string
EffectType int32
Multiplier int32
OrderRemark string // BUG修复增加remark字段用于解析次数卡使用信息
OrderNo string // 订单号
DrawCount int64 // 该订单的总抽奖次数(用于金额分摊)
CreatedAt time.Time CreatedAt time.Time
} }
@ -271,9 +391,18 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
COALESCE(products.price, 0) as product_price, COALESCE(products.price, 0) as product_price,
COALESCE(orders.actual_amount, 0) as order_amount, COALESCE(orders.actual_amount, 0) as order_amount,
COALESCE(orders.discount_amount, 0) as discount_amount, COALESCE(orders.discount_amount, 0) as discount_amount,
COALESCE(orders.points_amount, 0) as points_amount,
COALESCE(orders.status, 0) as order_status,
orders.source_type, orders.source_type,
COALESCE(orders.coupon_id, 0) as coupon_id,
COALESCE(system_coupons.name, '') as coupon_name, COALESCE(system_coupons.name, '') as coupon_name,
COALESCE(orders.item_card_id, 0) as item_card_id,
COALESCE(system_item_cards.name, '') as item_card_name, COALESCE(system_item_cards.name, '') as item_card_name,
COALESCE(system_item_cards.effect_type, 0) as effect_type,
COALESCE(system_item_cards.reward_multiplier_x1000, 0) as multiplier,
COALESCE(orders.remark, '') as order_remark,
COALESCE(orders.order_no, '') as order_no,
COALESCE(order_draw_counts.draw_count, 1) as draw_count,
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").
@ -283,6 +412,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
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 system_coupons ON system_coupons.id = orders.coupon_id").
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = orders.item_card_id"). Joins("LEFT JOIN system_item_cards ON system_item_cards.id = orders.item_card_id").
Joins("LEFT JOIN (SELECT order_id, COUNT(*) as draw_count FROM activity_draw_logs GROUP BY order_id) as order_draw_counts ON order_draw_counts.order_id = activity_draw_logs.order_id").
Where("activity_issues.activity_id = ?", activityID). Where("activity_issues.activity_id = ?", activityID).
Order("activity_draw_logs.id DESC"). Order("activity_draw_logs.id DESC").
Offset((req.Page - 1) * req.PageSize). Offset((req.Page - 1) * req.PageSize).
@ -304,39 +434,133 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc {
productImage = images[0] productImage = images[0]
} }
// Determine PayType and UsedCard // Default quantity is 1
quantity := int64(1)
// Determine PayType and UsedCard + PaymentDetails
payType := "现金支付" payType := "现金支付"
usedCard := "" usedCard := ""
paymentDetails := PaymentDetails{} // 金额将在 drawCount 计算后设置
if l.SourceType == 2 { // Order SourceType 2 = Ticket/Count Card // 检查是否使用了优惠券
payType = "次数卡" if l.CouponID > 0 || l.CouponName != "" {
} paymentDetails.CouponUsed = true
paymentDetails.CouponName = l.CouponName
if l.ItemCardName != "" { if paymentDetails.CouponName == "" {
usedCard = l.ItemCardName paymentDetails.CouponName = "优惠券"
if payType == "现金支付" {
payType = "道具卡" // Override if item card is explicitly present
} }
} else if l.CouponName != "" { usedCard = paymentDetails.CouponName
usedCard = l.CouponName
payType = "优惠券" payType = "优惠券"
} }
// 检查是否使用了道具卡
if l.ItemCardID > 0 || l.ItemCardName != "" {
paymentDetails.ItemCardUsed = true
paymentDetails.ItemCardName = l.ItemCardName
if paymentDetails.ItemCardName == "" {
paymentDetails.ItemCardName = "道具卡"
}
if usedCard != "" {
usedCard = usedCard + " + " + paymentDetails.ItemCardName
} else {
usedCard = paymentDetails.ItemCardName
}
payType = "道具卡"
// 计算双倍/多倍卡数量
if l.EffectType == 1 && l.Multiplier > 1000 {
quantity = quantity * int64(l.Multiplier) / 1000
}
}
// 检查是否使用了次数卡 (source_type=4 或 remark包含use_game_pass)
if l.SourceType == 4 || strings.Contains(l.OrderRemark, "use_game_pass") {
paymentDetails.GamePassUsed = true
// 解析 gp_use:ID:Count 格式获取次数卡使用信息
gamePassInfo := "次数卡"
if strings.Contains(l.OrderRemark, "gp_use:") {
// 从remark中提取次数卡信息格式: use_game_pass;gp_use:ID:Count;gp_use:ID:Count
parts := strings.Split(l.OrderRemark, ";")
var gpParts []string
for _, p := range parts {
if strings.HasPrefix(p, "gp_use:") {
gpParts = append(gpParts, p)
}
}
if len(gpParts) > 0 {
gamePassInfo = fmt.Sprintf("使用%d种次数卡", len(gpParts))
}
}
paymentDetails.GamePassInfo = gamePassInfo
if usedCard != "" {
usedCard = usedCard + " + " + gamePassInfo
} else {
usedCard = gamePassInfo
}
payType = "次数卡"
}
// 检查是否使用了积分
if l.PointsAmount > 0 {
paymentDetails.PointsUsed = true
}
// 如果同时使用了多种方式,标记为组合支付
usedCount := 0
if paymentDetails.CouponUsed {
usedCount++
}
if paymentDetails.ItemCardUsed {
usedCount++
}
if paymentDetails.GamePassUsed {
usedCount++
}
if usedCount > 1 {
payType = "组合支付"
} else if usedCount == 0 && l.OrderAmount > 0 {
payType = "现金支付"
} else if usedCount == 0 && l.OrderAmount == 0 {
// 0元支付默认视为次数卡使用实际业务中几乎不存在真正免费的情况
payType = "次数卡"
paymentDetails.GamePassUsed = true
if paymentDetails.GamePassInfo == "" {
paymentDetails.GamePassInfo = "次数卡"
}
}
// 计算单次抽奖的分摊金额(一个订单可能包含多次抽奖)
drawCount := l.DrawCount
if drawCount <= 0 {
drawCount = 1
}
perDrawOrderAmount := l.OrderAmount / drawCount
perDrawDiscountAmount := l.DiscountAmount / drawCount
perDrawPointsAmount := l.PointsAmount / drawCount
// 设置支付详情中的分摊金额
paymentDetails.CouponDiscount = perDrawDiscountAmount
paymentDetails.PointsDiscount = perDrawPointsAmount
list[i] = activityLogItem{ list[i] = activityLogItem{
ID: l.ID, ID: l.ID,
UserID: l.UserID, UserID: l.UserID,
Nickname: l.Nickname, Nickname: l.Nickname,
Avatar: l.Avatar, Avatar: l.Avatar,
ProductID: l.ProductID, ProductID: l.ProductID,
ProductName: l.ProductName, ProductName: l.ProductName,
ProductImage: productImage, ProductImage: productImage,
ProductPrice: l.ProductPrice, ProductPrice: l.ProductPrice,
OrderAmount: l.OrderAmount, ProductQuantity: quantity,
DiscountAmount: l.DiscountAmount, OrderAmount: perDrawOrderAmount, // 单次抽奖分摊的支付金额
PayType: payType, OrderNo: l.OrderNo, // 订单号
UsedCard: usedCard, DiscountAmount: perDrawDiscountAmount, // 单次抽奖分摊的优惠金额
Profit: l.OrderAmount - l.ProductPrice, PayType: payType,
CreatedAt: l.CreatedAt, UsedCard: usedCard,
OrderStatus: l.OrderStatus,
Profit: perDrawOrderAmount + perDrawDiscountAmount - l.ProductPrice*quantity, // 单次盈亏 = 分摊收入 - 成本*数量
CreatedAt: l.CreatedAt,
PaymentDetails: paymentDetails,
} }
} }

View File

@ -9,6 +9,7 @@ import (
"bindbox-game/internal/code" "bindbox-game/internal/code"
"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"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -20,17 +21,18 @@ type cardsRequest struct {
} }
type cardStatResponse struct { type cardStatResponse struct {
ItemCardSales int64 `json:"itemCardSales"` ItemCardSales int64 `json:"itemCardSales"`
DrawCount int64 `json:"drawCount"` DrawCount int64 `json:"drawCount"`
NewUsers int64 `json:"newUsers"` NewUsers int64 `json:"newUsers"`
TotalPoints int64 `json:"totalPoints"` TotalPoints float64 `json:"totalPoints"`
TotalCoupons int64 `json:"totalCoupons"` TotalInventory int64 `json:"totalInventory"` // 存量盒柜资产
TotalItemCards int64 `json:"totalItemCards"` TotalCoupons int64 `json:"totalCoupons"`
TotalGamePasses int64 `json:"totalGamePasses"` TotalItemCards int64 `json:"totalItemCards"`
ItemCardChange string `json:"itemCardChange"` TotalGamePasses int64 `json:"totalGamePasses"`
DrawChange string `json:"drawChange"` ItemCardChange string `json:"itemCardChange"`
NewUserChange string `json:"newUserChange"` DrawChange string `json:"drawChange"`
PointsChange string `json:"pointsChange"` NewUserChange string `json:"newUserChange"`
PointsChange string `json:"pointsChange"`
} }
func (h *handler) DashboardCards() core.HandlerFunc { func (h *handler) DashboardCards() core.HandlerFunc {
@ -98,7 +100,7 @@ func (h *handler) DashboardCards() core.HandlerFunc {
var tpRows []struct{ Sum int64 } var tpRows []struct{ Sum int64 }
if err := h.readDB.UserPoints.WithContext(ctx.RequestContext()).ReadDB(). if err := h.readDB.UserPoints.WithContext(ctx.RequestContext()).ReadDB().
Where(h.readDB.UserPoints.ValidEnd.Gt(time.Now())). Where(h.readDB.UserPoints.ValidEnd.Gt(time.Now())).
Or(h.readDB.UserPoints.ValidEnd.Eq(time.Time{})). Or(h.readDB.UserPoints.ValidEnd.IsNull()).
Select(h.readDB.UserPoints.Points.Sum().As("sum")). Select(h.readDB.UserPoints.Points.Sum().As("sum")).
Scan(&tpRows); err != nil { Scan(&tpRows); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21007, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 21007, err.Error()))
@ -148,6 +150,11 @@ func (h *handler) DashboardCards() core.HandlerFunc {
Where(h.readDB.UserItemCards.Status.Eq(1)). Where(h.readDB.UserItemCards.Status.Eq(1)).
Count() Count()
// 批量:存量盒柜资产 (持有中)
tinvCur, _ := h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
Where(h.readDB.UserInventory.Status.Eq(1)).
Count()
// 批量:存量次卡 (剩余次数) // 批量:存量次卡 (剩余次数)
var tgpRows []struct{ Sum int64 } var tgpRows []struct{ Sum int64 }
_ = h.readDB.UserGamePasses.WithContext(ctx.RequestContext()).ReadDB(). _ = h.readDB.UserGamePasses.WithContext(ctx.RequestContext()).ReadDB().
@ -163,7 +170,8 @@ func (h *handler) DashboardCards() core.HandlerFunc {
rsp.ItemCardSales = icCur rsp.ItemCardSales = icCur
rsp.DrawCount = dlCur rsp.DrawCount = dlCur
rsp.NewUsers = nuCur rsp.NewUsers = nuCur
rsp.TotalPoints = int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), tpCur)) rsp.TotalPoints = h.userSvc.CentsToPointsFloat(ctx.RequestContext(), tpCur)
rsp.TotalInventory = tinvCur
rsp.TotalCoupons = tcCur rsp.TotalCoupons = tcCur
rsp.TotalItemCards = ticCur rsp.TotalItemCards = ticCur
rsp.TotalGamePasses = tgpCur rsp.TotalGamePasses = tgpCur
@ -842,37 +850,52 @@ type funnelItem struct {
func (h *handler) DashboardOrderFunnel() core.HandlerFunc { func (h *handler) DashboardOrderFunnel() core.HandlerFunc {
return func(ctx core.Context) { return func(ctx core.Context) {
s, e := parseRange(ctx.Request().URL.Query().Get("rangeType"), ctx.Request().URL.Query().Get("start"), ctx.Request().URL.Query().Get("end")) s, e := parseRange(ctx.Request().URL.Query().Get("rangeType"), ctx.Request().URL.Query().Get("start"), ctx.Request().URL.Query().Get("end"))
visitors, _ := h.readDB.LogRequest.WithContext(ctx.RequestContext()).ReadDB().
Where(h.readDB.LogRequest.CreatedAt.Gte(s)). // 1. 注册用户 (真实业务数据)
Where(h.readDB.LogRequest.CreatedAt.Lte(e)). visitors, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
Where(h.readDB.LogRequest.Path.Like("/api/app/%")). Where(h.readDB.Users.CreatedAt.Gte(s)).
Where(h.readDB.Users.CreatedAt.Lte(e)).
Count() Count()
orders, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
Where(h.readDB.Orders.CreatedAt.Gte(s)). // 获取周期内注册的用户 ID 列表,用于后续阶段的子集统计
Where(h.readDB.Orders.CreatedAt.Lte(e)). var newUserIDs []int64
Count() h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
payments, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB(). Where(h.readDB.Users.CreatedAt.Gte(s)).
Where(h.readDB.Orders.Status.Eq(2)). Where(h.readDB.Users.CreatedAt.Lte(e)).
Where(h.readDB.Orders.PaidAt.Gte(s)). Pluck(h.readDB.Users.ID, &newUserIDs)
Where(h.readDB.Orders.PaidAt.Lte(e)).
Count() // 2. 下单人数 (仅统计新注册用户中的下单人数)
shipped, _ := h.readDB.ShippingRecords.WithContext(ctx.RequestContext()).ReadDB(). var orders int64
Where(h.readDB.ShippingRecords.Status.In(2, 3)). if len(newUserIDs) > 0 {
Where(h.readDB.ShippingRecords.UpdatedAt.Gte(s)). _ = h.readDB.Orders.WithContext(ctx.RequestContext()).UnderlyingDB().
Where(h.readDB.ShippingRecords.UpdatedAt.Lte(e)). Model(&model.Orders{}).
Count() Where("user_id IN ?", newUserIDs).
consumed, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB(). Where("created_at >= ? AND created_at <= ?", s, e).
Where(h.readDB.Orders.Status.Eq(2), h.readDB.Orders.IsConsumed.Eq(1)). Select("COUNT(DISTINCT user_id)").
Where(h.readDB.Orders.UpdatedAt.Gte(s)). Scan(&orders)
Where(h.readDB.Orders.UpdatedAt.Lte(e)). }
Count()
completions := shipped + consumed // 3. 支付人数 (仅统计新注册用户中的支付人数)
var payments int64
if len(newUserIDs) > 0 {
_ = h.readDB.Orders.WithContext(ctx.RequestContext()).UnderlyingDB().
Model(&model.Orders{}).
Where("user_id IN ?", newUserIDs).
Where("status = ?", 2).
Where("paid_at >= ? AND paid_at <= ?", s, e).
Select("COUNT(DISTINCT user_id)").
Scan(&payments)
}
// 4. (已移除) 成功支付作为漏斗终点
stages := []struct { stages := []struct {
name string name string
val int64 val int64
}{ }{
{"访问用户", visitors}, {"下单用户", orders}, {"支付用户", payments}, {"完成订单", completions}, {"新注册用户", visitors}, {"下单人数", orders}, {"成功支付", payments},
} }
out := make([]funnelItem, 0, len(stages)) out := make([]funnelItem, 0, len(stages))
var prev int64 var prev int64
for i, st := range stages { for i, st := range stages {
@ -890,7 +913,12 @@ func (h *handler) DashboardOrderFunnel() core.HandlerFunc {
lost = 0 lost = 0
} }
} }
out = append(out, funnelItem{Stage: st.name, Count: st.val, Rate: float64(int(rate*10)) / 10.0, LostCount: lost}) out = append(out, funnelItem{
Stage: st.name,
Count: st.val,
Rate: float64(int(rate*10)) / 10.0,
LostCount: lost,
})
prev = st.val prev = st.val
} }
ctx.Payload(out) ctx.Payload(out)
@ -910,7 +938,7 @@ type activitiesItem struct {
func (h *handler) DashboardActivities() core.HandlerFunc { func (h *handler) DashboardActivities() core.HandlerFunc {
return func(ctx core.Context) { return func(ctx core.Context) {
rows, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).ReadDB().Find() rows, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Activities.DeletedAt.IsNull()).Find()
out := make([]activitiesItem, len(rows)) out := make([]activitiesItem, len(rows))
for i, a := range rows { for i, a := range rows {
issues, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityIssues.ActivityID.Eq(a.ID)).Find() issues, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityIssues.ActivityID.Eq(a.ID)).Find()
@ -1457,14 +1485,52 @@ func (h *handler) DashboardPrizeDistribution() core.HandlerFunc {
return return
} }
// 4. 从 activity_draw_logs 统计实际抽奖中奖数量(按 Level 分组)
// 这是为了解决无限赏等不扣减库存的情况
type drawLogStat struct {
Level int32
WinCount int64
}
var drawStats []drawLogStat
drawLogDB := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).UnderlyingDB().
Joins("JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
Joins("JOIN activities ON activities.id = activity_issues.activity_id").
Where("activity_draw_logs.is_winner = ?", 1)
if activityIdStr != "" {
drawLogDB = drawLogDB.Where("activities.id = ?", activityIdStr)
} else {
drawLogDB = drawLogDB.Where("activities.status = ?", 1)
}
drawLogDB.Select(
"activity_reward_settings.level",
"COUNT(activity_draw_logs.id) as win_count",
).
Group("activity_reward_settings.level").
Scan(&drawStats)
// 构建 level -> winCount 映射
drawLogWinMap := make(map[int32]int64)
for _, ds := range drawStats {
drawLogWinMap[ds.Level] = ds.WinCount
}
out := make([]prizeDistributionItem, len(rows)) out := make([]prizeDistributionItem, len(rows))
levelNames := map[int32]string{1: "隐藏款", 2: "A赏", 3: "B赏", 4: "C赏", 5: "D赏", 6: "E赏", 7: "F赏", 8: "Last赏"} levelNames := map[int32]string{1: "隐藏款", 2: "A赏", 3: "B赏", 4: "C赏", 5: "D赏", 6: "E赏", 7: "F赏", 8: "Last赏"}
for i, r := range rows { for i, r := range rows {
// 防御性计算:已发出数量 (不能为负) // 优先使用 activity_draw_logs 统计的实际中奖数
winCount := r.LevelTotalQty - r.LevelRemQty // 如果 drawLog 有记录则用它,否则回退到库存差计算
if winCount < 0 { winCount := drawLogWinMap[r.Level]
winCount = 0 if winCount == 0 {
// 回退到库存差计算(一番赏等会扣库存的场景)
winCount = r.LevelTotalQty - r.LevelRemQty
if winCount < 0 {
winCount = 0
}
} }
name := levelNames[r.Level] name := levelNames[r.Level]
@ -1888,31 +1954,32 @@ func (h *handler) OperationsCouponEffectiveness() core.HandlerFunc {
Where(h.readDB.UserCoupons.UsedAt.Lte(e)). Where(h.readDB.UserCoupons.UsedAt.Lte(e)).
Count() Count()
// 带动订单和总价值 (实收 + 优惠券面值) // 带动订单和总价值 (实收 + 实际产生的优惠金额)
var orderStats struct { var orderStats struct {
Orders int64 Orders int64
Amount int64 ActualSum int64
DiscountSum int64
} }
_ = h.readDB.Orders.WithContext(ctx.RequestContext()).UnderlyingDB(). _ = h.readDB.Orders.WithContext(ctx.RequestContext()).UnderlyingDB().
Where("coupon_id = ?", c.ID). Joins("JOIN user_coupons ON user_coupons.id = orders.coupon_id").
Where("status = ?", 2). Where("user_coupons.coupon_id = ?", c.ID).
Where("paid_at >= ?", s). Where("orders.status = ?", 2).
Where("paid_at <= ?", e). Where("orders.paid_at >= ?", s).
Select("COUNT(id) as orders, SUM(actual_amount) as amount"). Where("orders.paid_at <= ?", e).
Select("COUNT(orders.id) as orders, SUM(orders.actual_amount) as actual_sum, SUM(orders.discount_amount) as discount_sum").
Scan(&orderStats) Scan(&orderStats)
discountTotal := c.DiscountValue * used broughtTotalValue := orderStats.ActualSum + orderStats.DiscountSum // 总业务价值GMV口径
broughtTotalValue := orderStats.Amount + discountTotal // 总业务价值
var usedRate float64 var usedRate float64
if issued > 0 { if issued > 0 {
usedRate = float64(used) / float64(issued) * 100 usedRate = float64(used) / float64(issued) * 100
} }
// ROI计算: 总价值 / 优惠成本 // ROI计算: 带动总价值 / 实际优惠成本
var roi float64 var roi float64
if discountTotal > 0 { if orderStats.DiscountSum > 0 {
roi = float64(broughtTotalValue) / float64(discountTotal) roi = float64(broughtTotalValue) / float64(orderStats.DiscountSum)
} }
typeStr := "直减券" typeStr := "直减券"

View File

@ -41,6 +41,10 @@ type spendingLeaderboardItem struct {
MatchingSpending int64 `json:"matching_spending"` MatchingSpending int64 `json:"matching_spending"`
MatchingPrize int64 `json:"matching_prize"` MatchingPrize int64 `json:"matching_prize"`
MatchingCount int64 `json:"matching_count"` MatchingCount int64 `json:"matching_count"`
// 直播间统计 (source_type=5)
LivestreamSpending int64 `json:"livestream_spending"`
LivestreamPrize int64 `json:"livestream_prize"`
LivestreamCount int64 `json:"livestream_count"`
Profit int64 `json:"profit"` // Spending - PrizeValue Profit int64 `json:"profit"` // Spending - PrizeValue
ProfitRate float64 `json:"profit_rate"` // Profit / Spending ProfitRate float64 `json:"profit_rate"` // Profit / Spending
@ -79,24 +83,26 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
// 1. Get Top Spenders from Orders // 1. Get Top Spenders from Orders
type orderStat struct { type orderStat struct {
UserID int64 UserID int64
TotalAmount int64 // ActualAmount TotalAmount int64 // ActualAmount
OrderCount int64 OrderCount int64
TotalDiscount int64 TotalDiscount int64
TotalPoints int64 TotalPoints int64
GamePassCount int64 GamePassCount int64
ItemCardCount int64 ItemCardCount int64
IchibanSpending int64 IchibanSpending int64
IchibanCount int64 IchibanCount int64
InfiniteSpending int64 InfiniteSpending int64
InfiniteCount int64 InfiniteCount int64
MatchingSpending int64 MatchingSpending int64
MatchingCount int64 MatchingCount int64
LivestreamSpending int64
LivestreamCount int64
} }
var stats []orderStat var stats []orderStat
query := db.Table(model.TableNameOrders). query := db.Table(model.TableNameOrders).
Joins("LEFT JOIN (SELECT l.order_id, MAX(a.play_type) as play_type FROM activity_draw_logs l JOIN activity_issues i ON i.id = l.issue_id JOIN activities a ON a.id = i.activity_id GROUP BY l.order_id) oa ON oa.order_id = orders.id"). Joins("LEFT JOIN (SELECT l.order_id, MAX(a.activity_category_id) as category_id FROM activity_draw_logs l JOIN activity_issues i ON i.id = l.issue_id JOIN activities a ON a.id = i.activity_id GROUP BY l.order_id) oa ON oa.order_id = orders.id").
Where("orders.status = ?", 2) Where("orders.status = ?", 2)
if req.RangeType != "all" { if req.RangeType != "all" {
@ -105,22 +111,24 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
if err := query.Select(` if err := query.Select(`
orders.user_id, orders.user_id,
SUM(orders.actual_amount) as total_amount, SUM(orders.total_amount) as total_amount,
COUNT(orders.id) as order_count, COUNT(orders.id) as order_count,
SUM(orders.discount_amount) as total_discount, SUM(orders.discount_amount) as total_discount,
SUM(orders.points_amount) as total_points, SUM(orders.points_amount) as total_points,
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count, SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count, SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN orders.actual_amount ELSE 0 END) as ichiban_spending, SUM(CASE WHEN oa.category_id = 1 THEN orders.total_amount ELSE 0 END) as ichiban_spending,
SUM(CASE WHEN oa.play_type = 'ichiban' THEN 1 ELSE 0 END) as ichiban_count, SUM(CASE WHEN oa.category_id = 1 THEN 1 ELSE 0 END) as ichiban_count,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN orders.actual_amount ELSE 0 END) as infinite_spending, SUM(CASE WHEN oa.category_id = 2 THEN orders.total_amount ELSE 0 END) as infinite_spending,
SUM(CASE WHEN oa.play_type IN ('infinite', 'box') THEN 1 ELSE 0 END) as infinite_count, SUM(CASE WHEN oa.category_id = 2 THEN 1 ELSE 0 END) as infinite_count,
SUM(CASE WHEN oa.play_type = 'matching' THEN orders.actual_amount ELSE 0 END) as matching_spending, SUM(CASE WHEN oa.category_id = 3 THEN orders.total_amount ELSE 0 END) as matching_spending,
SUM(CASE WHEN oa.play_type = 'matching' THEN 1 ELSE 0 END) as matching_count SUM(CASE WHEN oa.category_id = 3 THEN 1 ELSE 0 END) as matching_count,
SUM(CASE WHEN orders.source_type = 5 THEN orders.total_amount ELSE 0 END) as livestream_spending,
SUM(CASE WHEN orders.source_type = 5 THEN 1 ELSE 0 END) as livestream_count
`). `).
Group("orders.user_id"). Group("orders.user_id").
Order("total_amount DESC"). Order("total_amount DESC").
Limit(50). Limit(100).
Scan(&stats).Error; err != nil { Scan(&stats).Error; err != nil {
h.logger.Error(fmt.Sprintf("SpendingLeaderboard SQL error: %v", err)) h.logger.Error(fmt.Sprintf("SpendingLeaderboard SQL error: %v", err))
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21020, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 21020, err.Error()))
@ -134,19 +142,21 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
for i, s := range stats { for i, s := range stats {
userIDs[i] = s.UserID userIDs[i] = s.UserID
statMap[s.UserID] = &spendingLeaderboardItem{ statMap[s.UserID] = &spendingLeaderboardItem{
UserID: s.UserID, UserID: s.UserID,
TotalSpending: s.TotalAmount, TotalSpending: s.TotalAmount,
OrderCount: s.OrderCount, OrderCount: s.OrderCount,
TotalDiscount: s.TotalDiscount, TotalDiscount: s.TotalDiscount,
TotalPoints: s.TotalPoints, TotalPoints: s.TotalPoints,
GamePassCount: s.GamePassCount, GamePassCount: s.GamePassCount,
ItemCardCount: s.ItemCardCount, ItemCardCount: s.ItemCardCount,
IchibanSpending: s.IchibanSpending, IchibanSpending: s.IchibanSpending,
IchibanCount: s.IchibanCount, IchibanCount: s.IchibanCount,
InfiniteSpending: s.InfiniteSpending, InfiniteSpending: s.InfiniteSpending,
InfiniteCount: s.InfiniteCount, InfiniteCount: s.InfiniteCount,
MatchingSpending: s.MatchingSpending, MatchingSpending: s.MatchingSpending,
MatchingCount: s.MatchingCount, MatchingCount: s.MatchingCount,
LivestreamSpending: s.LivestreamSpending,
LivestreamCount: s.LivestreamCount,
} }
} }
@ -163,31 +173,37 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
// 4. Calculate Prize Value (Inventory) // 4. Calculate Prize Value (Inventory)
type invStat struct { type invStat struct {
UserID int64 UserID int64
TotalValue int64 TotalValue int64
IchibanPrize int64 IchibanPrize int64
InfinitePrize int64 InfinitePrize int64
MatchingPrize int64 MatchingPrize int64
LivestreamPrize int64
} }
var invStats []invStat var invStats []invStat
// Join with Products and Activities // Join with Products, Activities, and Orders (for livestream detection)
query := db.Table(model.TableNameUserInventory). query := db.Table(model.TableNameUserInventory).
Joins("JOIN products ON products.id = user_inventory.product_id"). Joins("JOIN products ON products.id = user_inventory.product_id").
Joins("LEFT JOIN activities ON activities.id = user_inventory.activity_id"). Joins("LEFT JOIN activities ON activities.id = user_inventory.activity_id").
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
Where("user_inventory.user_id IN ?", userIDs) Where("user_inventory.user_id IN ?", userIDs)
if req.RangeType != "all" { if req.RangeType != "all" {
query = query.Where("user_inventory.created_at >= ?", start). query = query.Where("user_inventory.created_at >= ?", start).
Where("user_inventory.created_at <= ?", end) Where("user_inventory.created_at <= ?", end)
} }
// Only include Holding (1) and Shipped/Used (3) items. Exclude Void/Decomposed (2).
query = query.Where("user_inventory.status IN ?", []int{1, 3}).
Where("user_inventory.remark NOT LIKE ? AND user_inventory.remark NOT LIKE ?", "%redeemed%", "%void%")
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.play_type = 'ichiban' 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.play_type IN ('infinite', 'box') 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.play_type = 'matching' 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
@ -199,6 +215,7 @@ 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
} }
} }
} }

View File

@ -0,0 +1,671 @@
package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
"bindbox-game/internal/service/livestream"
)
// ========== 直播间活动管理 ==========
type createLivestreamActivityRequest struct {
Name string `json:"name" binding:"required"`
StreamerName string `json:"streamer_name"`
StreamerContact string `json:"streamer_contact"`
DouyinProductID string `json:"douyin_product_id"`
TicketPrice int64 `json:"ticket_price"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
type livestreamActivityResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
StreamerName string `json:"streamer_name"`
StreamerContact string `json:"streamer_contact"`
AccessCode string `json:"access_code"`
DouyinProductID string `json:"douyin_product_id"`
TicketPrice int64 `json:"ticket_price"`
Status int32 `json:"status"`
StartTime string `json:"start_time,omitempty"`
EndTime string `json:"end_time,omitempty"`
CreatedAt string `json:"created_at"`
}
// CreateLivestreamActivity 创建直播间活动
// @Summary 创建直播间活动
// @Description 创建新的直播间活动,自动生成唯一访问码
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param RequestBody body createLivestreamActivityRequest true "请求参数"
// @Success 200 {object} livestreamActivityResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities [post]
// @Security LoginVerifyToken
func (h *handler) CreateLivestreamActivity() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createLivestreamActivityRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
input := livestream.CreateActivityInput{
Name: req.Name,
StreamerName: req.StreamerName,
StreamerContact: req.StreamerContact,
DouyinProductID: req.DouyinProductID,
TicketPrice: req.TicketPrice,
}
if req.StartTime != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
input.StartTime = &t
}
}
if req.EndTime != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
input.EndTime = &t
}
}
activity, err := h.livestream.CreateActivity(ctx.RequestContext(), input)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&livestreamActivityResponse{
ID: activity.ID,
Name: activity.Name,
StreamerName: activity.StreamerName,
StreamerContact: activity.StreamerContact,
AccessCode: activity.AccessCode,
DouyinProductID: activity.DouyinProductID,
TicketPrice: activity.TicketPrice,
Status: activity.Status,
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
}
type updateLivestreamActivityRequest struct {
Name string `json:"name"`
StreamerName string `json:"streamer_name"`
StreamerContact string `json:"streamer_contact"`
DouyinProductID string `json:"douyin_product_id"`
TicketPrice *int64 `json:"ticket_price"`
Status *int32 `json:"status"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
// UpdateLivestreamActivity 更新直播间活动
// @Summary 更新直播间活动
// @Description 更新直播间活动信息
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Param RequestBody body updateLivestreamActivityRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id} [put]
// @Security LoginVerifyToken
func (h *handler) UpdateLivestreamActivity() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
req := new(updateLivestreamActivityRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
input := livestream.UpdateActivityInput{
Name: req.Name,
StreamerName: req.StreamerName,
StreamerContact: req.StreamerContact,
DouyinProductID: req.DouyinProductID,
TicketPrice: req.TicketPrice,
Status: req.Status,
}
if req.StartTime != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
input.StartTime = &t
}
}
if req.EndTime != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
input.EndTime = &t
}
}
if err := h.livestream.UpdateActivity(ctx.RequestContext(), id, input); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&simpleMessageResponse{Message: "操作成功"})
}
}
type listLivestreamActivitiesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Status *int32 `form:"status"`
}
type listLivestreamActivitiesResponse struct {
List []livestreamActivityResponse `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// ListLivestreamActivities 直播间活动列表
// @Summary 直播间活动列表
// @Description 获取直播间活动列表
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Param status query int false "状态过滤"
// @Success 200 {object} listLivestreamActivitiesResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities [get]
// @Security LoginVerifyToken
func (h *handler) ListLivestreamActivities() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listLivestreamActivitiesRequest)
if err := ctx.ShouldBindForm(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
}
list, total, err := h.livestream.ListActivities(ctx.RequestContext(), req.Page, req.PageSize, req.Status)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
res := &listLivestreamActivitiesResponse{
List: make([]livestreamActivityResponse, len(list)),
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}
for i, a := range list {
item := livestreamActivityResponse{
ID: a.ID,
Name: a.Name,
StreamerName: a.StreamerName,
StreamerContact: a.StreamerContact,
AccessCode: a.AccessCode,
DouyinProductID: a.DouyinProductID,
TicketPrice: a.TicketPrice,
Status: a.Status,
CreatedAt: a.CreatedAt.Format("2006-01-02 15:04:05"),
}
if !a.StartTime.IsZero() {
item.StartTime = a.StartTime.Format("2006-01-02 15:04:05")
}
if !a.EndTime.IsZero() {
item.EndTime = a.EndTime.Format("2006-01-02 15:04:05")
}
res.List[i] = item
}
ctx.Payload(res)
}
}
// GetLivestreamActivity 获取直播间活动详情
// @Summary 获取直播间活动详情
// @Description 根据ID获取直播间活动详情
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} livestreamActivityResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id} [get]
// @Security LoginVerifyToken
func (h *handler) GetLivestreamActivity() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
activity, err := h.livestream.GetActivity(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "活动不存在"))
return
}
res := &livestreamActivityResponse{
ID: activity.ID,
Name: activity.Name,
StreamerName: activity.StreamerName,
StreamerContact: activity.StreamerContact,
AccessCode: activity.AccessCode,
DouyinProductID: activity.DouyinProductID,
TicketPrice: activity.TicketPrice,
Status: activity.Status,
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
}
if !activity.StartTime.IsZero() {
res.StartTime = activity.StartTime.Format("2006-01-02 15:04:05")
}
if !activity.EndTime.IsZero() {
res.EndTime = activity.EndTime.Format("2006-01-02 15:04:05")
}
ctx.Payload(res)
}
}
// DeleteLivestreamActivity 删除直播间活动
// @Summary 删除直播间活动
// @Description 删除指定直播间活动
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteLivestreamActivity() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
if err := h.livestream.DeleteActivity(ctx.RequestContext(), id); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&simpleMessageResponse{Message: "删除成功"})
}
}
// ========== 直播间奖品管理 ==========
type createLivestreamPrizeRequest struct {
Name string `json:"name"`
Image string `json:"image"`
Level int32 `json:"level"`
Weight int32 `json:"weight"`
Quantity int32 `json:"quantity"`
ProductID int64 `json:"product_id"`
CostPrice int64 `json:"cost_price"`
}
type livestreamPrizeResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Level int32 `json:"level"`
Weight int32 `json:"weight"`
Quantity int32 `json:"quantity"`
Remaining int64 `json:"remaining"`
ProductID int64 `json:"product_id"`
CostPrice int64 `json:"cost_price"`
Sort int32 `json:"sort"`
}
// CreateLivestreamPrizes 批量创建奖品
// @Summary 批量创建直播间奖品
// @Description 为指定活动批量创建奖品
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param RequestBody body []createLivestreamPrizeRequest true "奖品列表"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{activity_id}/prizes [post]
// @Security LoginVerifyToken
func (h *handler) CreateLivestreamPrizes() core.HandlerFunc {
return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || activityID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
var req []createLivestreamPrizeRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
var inputs []livestream.CreatePrizeInput
for _, p := range req {
inputs = append(inputs, livestream.CreatePrizeInput{
Name: p.Name,
Image: p.Image,
Weight: p.Weight,
Quantity: p.Quantity,
Level: p.Level,
ProductID: p.ProductID,
CostPrice: p.CostPrice,
})
}
if err := h.livestream.CreatePrizes(ctx.RequestContext(), activityID, inputs); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&simpleMessageResponse{Message: "创建成功"})
}
}
// ListLivestreamPrizes 获取活动奖品列表
// @Summary 获取直播间活动奖品列表
// @Description 获取指定活动的所有奖品
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Success 200 {object} []livestreamPrizeResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{activity_id}/prizes [get]
// @Security LoginVerifyToken
func (h *handler) ListLivestreamPrizes() core.HandlerFunc {
return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || activityID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
prizes, err := h.livestream.ListPrizes(ctx.RequestContext(), activityID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
res := make([]livestreamPrizeResponse, len(prizes))
for i, p := range prizes {
res[i] = livestreamPrizeResponse{
ID: p.ID,
Name: p.Name,
Image: p.Image,
Weight: p.Weight,
Quantity: p.Quantity,
Remaining: int64(p.Remaining),
Level: p.Level,
ProductID: p.ProductID,
CostPrice: p.CostPrice,
Sort: p.Sort,
}
}
ctx.Payload(res)
}
}
// DeleteLivestreamPrize 删除奖品
// @Summary 删除直播间奖品
// @Description 删除指定奖品
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param prize_id path integer true "奖品ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/prizes/{prize_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteLivestreamPrize() core.HandlerFunc {
return func(ctx core.Context) {
prizeID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || prizeID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的奖品ID"))
return
}
if err := h.livestream.DeletePrize(ctx.RequestContext(), prizeID); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&simpleMessageResponse{Message: "删除成功"})
}
}
// ========== 直播间中奖记录 ==========
type livestreamDrawLogResponse struct {
ID int64 `json:"id"`
ActivityID int64 `json:"activity_id"`
PrizeID int64 `json:"prize_id"`
PrizeName string `json:"prize_name"`
Level int32 `json:"level"`
DouyinOrderID int64 `json:"douyin_order_id"` // 关联ID
ShopOrderID string `json:"shop_order_id"` // 店铺订单号
LocalUserID int64 `json:"local_user_id"`
DouyinUserID string `json:"douyin_user_id"`
UserNickname string `json:"user_nickname"` // 用户昵称
SeedHash string `json:"seed_hash"`
CreatedAt string `json:"created_at"`
}
type listLivestreamDrawLogsResponse struct {
List []livestreamDrawLogResponse `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type listLivestreamDrawLogsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
Keyword string `form:"keyword"`
}
// ListLivestreamDrawLogs 获取中奖记录
// @Summary 获取直播间中奖记录
// @Description 获取指定活动的中奖记录,支持时间范围和关键词筛选
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Param start_time query string false "开始时间 (YYYY-MM-DD)"
// @Param end_time query string false "结束时间 (YYYY-MM-DD)"
// @Param keyword query string false "搜索关键词 (昵称/订单号/奖品名称)"
// @Success 200 {object} listLivestreamDrawLogsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{activity_id}/draw_logs [get]
// @Security LoginVerifyToken
func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || activityID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
req := new(listLivestreamDrawLogsRequest)
_ = ctx.ShouldBindForm(req)
page := req.Page
pageSize := req.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
// 解析时间范围
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
}
}
// 使用底层 GORM 直接查询以支持 keyword
db := h.repo.GetDbR().Model(&model.LivestreamDrawLogs{}).Where("activity_id = ?", activityID)
if startTime != nil {
db = db.Where("created_at >= ?", startTime)
}
if endTime != nil {
db = db.Where("created_at <= ?", endTime)
}
if req.Keyword != "" {
keyword := "%" + req.Keyword + "%"
db = db.Where("(user_nickname LIKE ? OR shop_order_id LIKE ? OR prize_name LIKE ?)", keyword, keyword, keyword)
}
var total int64
db.Count(&total)
var logs []model.LivestreamDrawLogs
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&logs).Error; err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
res := &listLivestreamDrawLogsResponse{
List: make([]livestreamDrawLogResponse, len(logs)),
Total: total,
Page: page,
PageSize: pageSize,
}
for i, log := range logs {
res.List[i] = livestreamDrawLogResponse{
ID: log.ID,
ActivityID: log.ActivityID,
PrizeID: log.PrizeID,
PrizeName: log.PrizeName,
Level: log.Level,
DouyinOrderID: log.DouyinOrderID,
ShopOrderID: log.ShopOrderID,
LocalUserID: log.LocalUserID,
DouyinUserID: log.DouyinUserID,
UserNickname: log.UserNickname,
SeedHash: log.SeedHash,
CreatedAt: log.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
ctx.Payload(res)
}
}
// ========== 直播间承诺管理 ==========
type livestreamCommitmentSummaryResponse struct {
SeedVersion int32 `json:"seed_version"`
Algo string `json:"algo"`
HasSeed bool `json:"has_seed"`
LenSeed int `json:"len_seed_master"`
LenHash int `json:"len_seed_hash"`
}
// GenerateLivestreamCommitment 生成直播间活动承诺
// @Summary 生成直播间活动承诺
// @Description 为直播间活动生成可验证的承诺种子
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} map[string]int32
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id}/commitment/generate [post]
// @Security LoginVerifyToken
func (h *handler) GenerateLivestreamCommitment() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
version, err := h.livestream.GenerateCommitment(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(map[string]int32{"seed_version": version})
}
}
// GetLivestreamCommitmentSummary 获取直播间活动承诺摘要
// @Summary 获取直播间活动承诺摘要
// @Description 获取直播间活动的承诺状态信息
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} livestreamCommitmentSummaryResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id}/commitment/summary [get]
// @Security LoginVerifyToken
func (h *handler) GetLivestreamCommitmentSummary() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
summary, err := h.livestream.GetCommitmentSummary(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(&livestreamCommitmentSummaryResponse{
SeedVersion: summary.SeedVersion,
Algo: summary.Algo,
HasSeed: summary.HasSeed,
LenSeed: summary.LenSeed,
LenHash: summary.LenHash,
})
}
}

View File

@ -0,0 +1,137 @@
package admin
import (
"math"
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/repository/mysql/model"
)
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"` // 利润率 %
}
// GetLivestreamStats 获取直播间盈亏统计
// @Summary 获取直播间盈亏统计
// @Description 计算逻辑:净利润 = (营收 - 退款) - 奖品成本。营收 = 抽奖次数 * 门票价格。成本 = 中奖奖品成本总和。
// @Tags 管理端.直播间
// @Accept json
// @Produce json
// @Param id path integer true "活动ID"
// @Success 200 {object} livestreamStatsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/livestream/activities/{id}/stats [get]
// @Security LoginVerifyToken
func (h *handler) GetLivestreamStats() core.HandlerFunc {
return func(ctx core.Context) {
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil || activityID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
return
}
// 1. 获取活动信息(门票价格)
var activity model.LivestreamActivities
if err := h.repo.GetDbR().Where("id = ?", activityID).First(&activity).Error; err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "活动不存在"))
return
}
ticketPrice := activity.TicketPrice
// 2. 从 livestream_draw_logs 统计抽奖次数
var drawLogs []model.LivestreamDrawLogs
if err := h.repo.GetDbR().Where("activity_id = ?", activityID).Find(&drawLogs).Error; err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
orderCount := int64(len(drawLogs))
totalRevenue := orderCount * ticketPrice
// 3. 统计退款数量
var refundCount int64
h.repo.GetDbR().Model(&model.LivestreamDrawLogs{}).Where("activity_id = ? AND is_refunded = 1", activityID).Count(&refundCount)
totalRefund := refundCount * ticketPrice
// 4. 计算成本
prizeIDCountMap := make(map[int64]int64)
for _, log := range drawLogs {
prizeIDCountMap[log.PrizeID]++
}
prizeIDs := make([]int64, 0, len(prizeIDCountMap))
for pid := range prizeIDCountMap {
prizeIDs = append(prizeIDs, pid)
}
var totalCost int64
if len(prizeIDs) > 0 {
var prizes []model.LivestreamPrizes
h.repo.GetDbR().Where("id IN ?", prizeIDs).Find(&prizes)
prizeCostMap := make(map[int64]int64)
productIDsNeedingFallback := make([]int64, 0)
prizeProductMap := make(map[int64]int64)
for _, p := range prizes {
if p.CostPrice > 0 {
prizeCostMap[p.ID] = p.CostPrice
} else if p.ProductID > 0 {
productIDsNeedingFallback = append(productIDsNeedingFallback, p.ProductID)
prizeProductMap[p.ID] = p.ProductID
}
}
if len(productIDsNeedingFallback) > 0 {
var products []model.Products
h.repo.GetDbR().Where("id IN ?", productIDsNeedingFallback).Find(&products)
productPriceMap := make(map[int64]int64)
for _, prod := range products {
productPriceMap[prod.ID] = prod.Price
}
for prizeID, productID := range prizeProductMap {
if _, ok := prizeCostMap[prizeID]; !ok {
if price, found := productPriceMap[productID]; found {
prizeCostMap[prizeID] = price
}
}
}
}
for prizeID, count := range prizeIDCountMap {
if cost, ok := prizeCostMap[prizeID]; ok {
totalCost += cost * count
}
}
}
netProfit := (totalRevenue - totalRefund) - totalCost
var margin float64
netRevenue := totalRevenue - totalRefund
if netRevenue > 0 {
margin = float64(netProfit) / float64(netRevenue) * 100
} else {
margin = -100
}
ctx.Payload(&livestreamStatsResponse{
TotalRevenue: totalRevenue,
TotalRefund: totalRefund,
TotalCost: totalCost,
NetProfit: netProfit,
OrderCount: orderCount,
RefundCount: refundCount,
ProfitMargin: math.Trunc(margin*100) / 100,
})
}
}

View File

@ -1,15 +1,15 @@
package admin package admin
import ( import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"net/url" "net/url"
"bindbox-game/configs" "bindbox-game/internal/code"
"bindbox-game/internal/code" "bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation"
"bindbox-game/internal/pkg/validation" "bindbox-game/internal/pkg/wechat"
"bindbox-game/internal/pkg/wechat" "bindbox-game/internal/service/sysconfig"
) )
type miniappQRCodeRequest struct { type miniappQRCodeRequest struct {
@ -30,7 +30,7 @@ func (h *handler) GenerateMiniAppQRCode() 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
} }
q := url.Values{} q := url.Values{}
if req.InviteCode != "" { if req.InviteCode != "" {
q.Set("invite_code", req.InviteCode) q.Set("invite_code", req.InviteCode)
@ -41,15 +41,19 @@ func (h *handler) GenerateMiniAppQRCode() core.HandlerFunc {
if req.ChannelCode != "" { if req.ChannelCode != "" {
q.Set("channel_code", req.ChannelCode) q.Set("channel_code", req.ChannelCode)
} }
if len(q) == 0 { if len(q) == 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "参数不能为空")) ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "参数不能为空"))
return return
} }
path := "/pages/login/index?" + q.Encode() path := "/pages/login/index?" + q.Encode()
cfg := configs.Get()
wxcfg := &wechat.WechatConfig{AppID: cfg.Wechat.AppID, AppSecret: cfg.Wechat.AppSecret} // 使用动态配置
dc := sysconfig.GetDynamicConfig()
wxConfig := dc.GetWechat(ctx.RequestContext())
wxcfg := &wechat.WechatConfig{AppID: wxConfig.AppID, AppSecret: wxConfig.AppSecret}
qReq := &wechat.QRCodeRequest{Path: path} qReq := &wechat.QRCodeRequest{Path: path}
if req.Width != nil { if req.Width != nil {
qReq.Width = *req.Width qReq.Width = *req.Width
@ -62,4 +66,4 @@ func (h *handler) GenerateMiniAppQRCode() core.HandlerFunc {
out := &miniappQRCodeResponse{ImageBase64: base64.StdEncoding.EncodeToString(rsp.Buffer)} out := &miniappQRCodeResponse{ImageBase64: base64.StdEncoding.EncodeToString(rsp.Buffer)}
ctx.Payload(out) ctx.Payload(out)
} }
} }

View File

@ -45,6 +45,7 @@ type ShippingOrderGroup struct {
Name string `json:"name"` Name string `json:"name"`
Image string `json:"image"` Image string `json:"image"`
Price int64 `json:"price"` Price int64 `json:"price"`
Count int64 `json:"count"` // 增加数量字段
} `json:"products"` // 商品详情列表 } `json:"products"` // 商品详情列表
} }
@ -215,29 +216,32 @@ func (h *handler) ListShippingOrders() core.HandlerFunc {
} }
} }
// 获取商品信息(去重 // 获取商品信息(去重并计数
pidSet := make(map[int64]struct{}) pidCounts := make(map[int64]int64)
for _, pid := range a.pid { for _, pid := range a.pid {
pidSet[pid] = struct{}{} pidCounts[pid]++
} }
var products []struct { var products []struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Image string `json:"image"` Image string `json:"image"`
Price int64 `json:"price"` Price int64 `json:"price"`
Count int64 `json:"count"`
} }
for pid := range pidSet { for pid, count := range pidCounts {
if prod, _ := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.Eq(pid)).First(); prod != nil { if prod, _ := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.Eq(pid)).First(); prod != nil {
products = append(products, struct { products = append(products, struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Image string `json:"image"` Image string `json:"image"`
Price int64 `json:"price"` Price int64 `json:"price"`
Count int64 `json:"count"`
}{ }{
ID: prod.ID, ID: prod.ID,
Name: prod.Name, Name: prod.Name,
Image: prod.ImagesJSON, // 商品图片JSON Image: prod.ImagesJSON, // 商品图片JSON
Price: prod.Price, Price: prod.Price,
Count: count,
}) })
} }
} }

View File

@ -788,16 +788,17 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
MinSpend int64 MinSpend int64
BalanceAmount int64 BalanceAmount int64
} }
q := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB().
LeftJoin(h.readDB.SystemCoupons, h.readDB.SystemCoupons.ID.EqCol(h.readDB.UserCoupons.CouponID)). q := base.
Select( Select(
h.readDB.UserCoupons.ID, h.readDB.UserCoupons.CouponID, h.readDB.UserCoupons.Status, h.readDB.UserCoupons.ID, h.readDB.UserCoupons.CouponID, h.readDB.UserCoupons.Status,
h.readDB.UserCoupons.UsedOrderID, h.readDB.UserCoupons.UsedAt, h.readDB.UserCoupons.ValidStart, h.readDB.UserCoupons.ValidEnd, h.readDB.UserCoupons.UsedOrderID, h.readDB.UserCoupons.UsedAt,
h.readDB.SystemCoupons.Name, h.readDB.SystemCoupons.ScopeType, h.readDB.SystemCoupons.DiscountType, h.readDB.UserCoupons.ValidStart, h.readDB.UserCoupons.ValidEnd,
h.readDB.SystemCoupons.DiscountValue, h.readDB.SystemCoupons.MinSpend, h.readDB.SystemCoupons.Name, h.readDB.SystemCoupons.ScopeType,
h.readDB.UserCoupons.BalanceAmount, h.readDB.SystemCoupons.DiscountType, h.readDB.SystemCoupons.DiscountValue,
h.readDB.SystemCoupons.MinSpend,
). ).
Where(h.readDB.UserCoupons.UserID.Eq(userID)). LeftJoin(h.readDB.SystemCoupons, h.readDB.SystemCoupons.ID.EqCol(h.readDB.UserCoupons.CouponID)).
Order(h.readDB.UserCoupons.ID.Desc()). Order(h.readDB.UserCoupons.ID.Desc()).
Limit(req.PageSize).Offset((req.Page - 1) * req.PageSize) Limit(req.PageSize).Offset((req.Page - 1) * req.PageSize)
@ -832,6 +833,164 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
} }
} }
type AuditLogItem struct {
CreatedAt string `json:"created_at"` // 时间
Category string `json:"category"` // 大类: points/order/shipping/draw
SubType string `json:"sub_type"` // 子类: action/status
AmountStr string `json:"amount_str"` // 金额/数值变化 (带符号字符串)
RefInfo string `json:"ref_info"` // 关联信息 (RefID/OrderNo/ExpressNo)
DetailInfo string `json:"detail_info"` // 详细描述 (Remark/PrizeName)
}
type listAuditLogsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"` // 由于UNION ALL分页较难精确Count Total这里可能返回估算值或分步Count为简化MVP先只做翻页不用Total或者Total设为0
List []AuditLogItem `json:"list"`
}
// ListUserAuditLogs 查看用户行为审计日志
// @Summary 查看用户行为审计日志
// @Description 聚合查看用户的积分、订单、发货、抽奖等行为记录
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量" default(20)
// @Success 200 {object} listAuditLogsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/audit [get]
// @Security LoginVerifyToken
func (h *handler) ListUserAuditLogs() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listInvitesRequest) // 复用分页参数结构
if err := ctx.ShouldBindForm(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.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
offset := (req.Page - 1) * req.PageSize
limit := req.PageSize
var logs []AuditLogItem
// 构建 UNION ALL 查询
// 1. 积分流水
// 2. 订单记录
// 3. 发货记录
// 4. 抽奖记录 (只看中奖的? 或者全部? 这里先只看中奖 IsWinner=1 避免数据量太大)
sql := `
SELECT * FROM (
-- 1. Points Ledger
SELECT
created_at,
'points' as category,
CONVERT(action USING utf8mb4) as sub_type,
CAST(points AS CHAR) as amount_str,
CONCAT(CONVERT(ref_table USING utf8mb4), ':', CONVERT(ref_id USING utf8mb4)) as ref_info,
CONVERT(remark USING utf8mb4) as detail_info
FROM user_points_ledger
WHERE user_id = ?
UNION ALL
-- 2. Orders
SELECT
created_at,
'order' as category,
'paid' as sub_type,
CAST(actual_amount AS CHAR) as amount_str,
CONVERT(order_no USING utf8mb4) as ref_info,
CONVERT(remark USING utf8mb4) as detail_info
FROM orders
WHERE user_id = ? AND status = 2
UNION ALL
-- 3. Shipping Records
SELECT
created_at,
'shipping' as category,
CAST(status AS CHAR) as sub_type,
CAST(quantity AS CHAR) as amount_str,
CONCAT(IFNULL(CONVERT(express_code USING utf8mb4),''), ':', IFNULL(CONVERT(express_no USING utf8mb4),'')) as ref_info,
CONVERT(remark USING utf8mb4) as detail_info
FROM shipping_records
WHERE user_id = ?
UNION ALL
-- 4. Draw Logs (Winners)
SELECT
l.created_at,
'draw' as category,
IF(l.is_winner=1, 'win', 'lose') as sub_type,
CAST(1 AS CHAR) as amount_str,
CAST(l.order_id AS CHAR) as ref_info,
CONCAT(
'游戏: ', IFNULL(CONVERT(act.name USING utf8mb4), '未知'),
' | 奖品: ', IFNULL(CONVERT(prod.name USING utf8mb4), '未知'),
' | 级别: ', CASE l.level WHEN 1 THEN 'S' WHEN 2 THEN 'A' WHEN 3 THEN 'B' WHEN 4 THEN 'C' ELSE CAST(l.level AS CHAR) END
) as detail_info
FROM activity_draw_logs l
LEFT JOIN activity_issues issue ON l.issue_id = issue.id
LEFT JOIN activities act ON issue.activity_id = act.id
LEFT JOIN activity_reward_settings reward ON l.reward_id = reward.id
LEFT JOIN products prod ON reward.product_id = prod.id
WHERE l.user_id = ? AND l.is_winner = 1
) as combined_logs
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`
if err := h.repo.GetDbR().Raw(sql, userID, userID, userID, userID, limit, offset).Scan(&logs).Error; err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20107, err.Error()))
return
}
// 格式化处理 (Optional)
for i := range logs {
// 将时间标准化
if t, err := time.Parse(time.RFC3339, logs[i].CreatedAt); err == nil {
logs[i].CreatedAt = t.Format("2006-01-02 15:04:05")
}
// 翻译 Shipping Status 等 (可选项,也可以前端做)
if logs[i].Category == "shipping" {
switch logs[i].SubType {
case "1":
logs[i].SubType = "待发货"
case "2":
logs[i].SubType = "已发货"
case "3":
logs[i].SubType = "已签收"
case "4":
logs[i].SubType = "异常"
}
}
}
ctx.Payload(&listAuditLogsResponse{
Page: req.Page,
PageSize: req.PageSize,
Total: 0, // 为了性能暂时忽略
List: logs,
})
}
}
func nullableToString(s *string) string { func nullableToString(s *string) string {
if s == nil { if s == nil {
return "" return ""
@ -844,14 +1003,14 @@ type listPointsRequest struct {
PageSize int `form:"page_size"` PageSize int `form:"page_size"`
} }
type adminUserPointsLedgerItem struct { type adminUserPointsLedgerItem struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Action string `json:"action"` Action string `json:"action"`
Points int64 `json:"points"` Points float64 `json:"points"` // 改为 float64 支持小数积分
RefTable string `json:"ref_table"` RefTable string `json:"ref_table"`
RefID string `json:"ref_id"` RefID string `json:"ref_id"`
Remark string `json:"remark"` Remark string `json:"remark"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
} }
type listPointsResponse struct { type listPointsResponse struct {
@ -902,7 +1061,7 @@ func (h *handler) ListUserPoints() core.HandlerFunc {
ID: v.ID, ID: v.ID,
UserID: v.UserID, UserID: v.UserID,
Action: v.Action, Action: v.Action,
Points: int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), v.Points)), Points: h.userSvc.CentsToPointsFloat(ctx.RequestContext(), v.Points),
RefTable: v.RefTable, RefTable: v.RefTable,
RefID: v.RefID, RefID: v.RefID,
Remark: v.Remark, Remark: v.Remark,

View File

@ -123,6 +123,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)). // 仅统计商城直购和抽奖票据,排除兑换商品
Scan(&os) Scan(&os)
// 分阶段统计 // 分阶段统计
@ -130,6 +131,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.CreatedAt.Gte(todayStart)). Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
Scan(&os.TodayPaid) Scan(&os.TodayPaid)
@ -137,6 +139,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.CreatedAt.Gte(sevenDayStart)). Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
Scan(&os.SevenDayPaid) Scan(&os.SevenDayPaid)
@ -144,6 +147,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.CreatedAt.Gte(thirtyDayStart)). Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
Scan(&os.ThirtyDayPaid) Scan(&os.ThirtyDayPaid)

View File

@ -6,6 +6,9 @@ import (
"bindbox-game/configs" "bindbox-game/configs"
"bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/wechat" "bindbox-game/internal/pkg/wechat"
"bindbox-game/internal/service/sysconfig"
"go.uber.org/zap"
) )
type openidRequest struct { type openidRequest struct {
@ -26,8 +29,19 @@ func (h *handler) GetOpenID() core.HandlerFunc {
return return
} }
cfg := configs.Get() // 使用动态配置
wxcfg := &wechat.WechatConfig{AppID: cfg.Wechat.AppID, AppSecret: cfg.Wechat.AppSecret} wxcfg := &wechat.WechatConfig{}
if dc := sysconfig.GetDynamicConfig(); dc != nil {
c := dc.GetWechat(ctx.RequestContext().Context)
wxcfg.AppID = c.AppID
wxcfg.AppSecret = c.AppSecret
} else {
cfg := configs.Get()
wxcfg.AppID = cfg.Wechat.AppID
wxcfg.AppSecret = cfg.Wechat.AppSecret
}
h.logger.Info("GetOpenID Config", zap.String("AppID", wxcfg.AppID), zap.String("AppSecret", wxcfg.AppSecret))
c2s, err := wechat.Code2Session(ctx.RequestContext().Context, wxcfg, req.Code) c2s, err := wechat.Code2Session(ctx.RequestContext().Context, wxcfg, req.Code)
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error()))

View File

@ -8,7 +8,6 @@ import (
"strings" "strings"
"time" "time"
"bindbox-game/configs"
"bindbox-game/internal/code" "bindbox-game/internal/code"
"bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/pay" "bindbox-game/internal/pkg/pay"
@ -16,6 +15,7 @@ import (
"bindbox-game/internal/pkg/wechat" "bindbox-game/internal/pkg/wechat"
"bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model" "bindbox-game/internal/repository/mysql/model"
"bindbox-game/internal/service/sysconfig"
"go.uber.org/zap" "go.uber.org/zap"
@ -23,7 +23,6 @@ import (
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader" "github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
"github.com/wechatpay-apiv3/wechatpay-go/core/notify" "github.com/wechatpay-apiv3/wechatpay-go/core/notify"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments" "github.com/wechatpay-apiv3/wechatpay-go/services/payments"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
) )
type notifyAck struct { type notifyAck struct {
@ -45,39 +44,61 @@ type notifyAck struct {
// @Router /pay/wechat/notify [post] // @Router /pay/wechat/notify [post]
func (h *handler) WechatNotify() core.HandlerFunc { func (h *handler) WechatNotify() core.HandlerFunc {
return func(ctx core.Context) { return func(ctx core.Context) {
c := configs.Get() // Use dynamic configurations exclusively
if c.WechatPay.ApiV3Key == "" { dc := sysconfig.GetDynamicConfig()
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete")) cfg := dc.GetWechatPay(ctx.RequestContext().Context)
if cfg.ApiV3Key == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config (ApiV3Key) missing"))
return return
} }
var handler *notify.Handler
if c.WechatPay.PublicKeyID != "" && c.WechatPay.PublicKeyPath != "" { mchID := cfg.MchID
pubKey, err := utils.LoadPublicKeyWithPath(c.WechatPay.PublicKeyPath) serialNo := cfg.SerialNo
if err != nil { apiV3Key := cfg.ApiV3Key
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150001, err.Error())) publicKeyID := cfg.PublicKeyID
var notifyHandler *notify.Handler
if publicKeyID != "" {
// 使用公钥验签模式
if cfg.PublicKey == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay public key content missing"))
return return
} }
handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(c.WechatPay.PublicKeyID, *pubKey)) pubKey, err := pay.LoadPublicKeyFromBase64(cfg.PublicKey)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150001, "load public key err: "+err.Error()))
return
}
notifyHandler = notify.NewNotifyHandler(apiV3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(publicKeyID, *pubKey))
} else { } else {
if c.WechatPay.MchID == "" || c.WechatPay.SerialNo == "" || c.WechatPay.PrivateKeyPath == "" { // 使用证书自动下载模式
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete")) if mchID == "" || serialNo == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay mchid/serial_no missing for cert mode"))
return return
} }
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(c.WechatPay.PrivateKeyPath)
if cfg.PrivateKey == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay private key missing"))
return
}
mchPrivateKey, err := pay.LoadPrivateKeyFromBase64(cfg.PrivateKey)
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150002, err.Error())) ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150002, "load private key err: "+err.Error()))
return return
} }
if err := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx.RequestContext(), mchPrivateKey, c.WechatPay.SerialNo, c.WechatPay.MchID, c.WechatPay.ApiV3Key); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150003, err.Error())) if err := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx.RequestContext().Context, mchPrivateKey, serialNo, mchID, apiV3Key); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150003, "register downloader err: "+err.Error()))
return return
} }
certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(c.WechatPay.MchID) certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID)
handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) notifyHandler = notify.NewNotifyHandler(apiV3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
} }
var transaction payments.Transaction var transaction payments.Transaction
notification, err := handler.ParseNotifyRequest(ctx.RequestContext(), ctx.Request(), &transaction) notification, err := notifyHandler.ParseNotifyRequest(ctx.RequestContext().Context, ctx.Request(), &transaction)
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error()))
return return
@ -278,6 +299,13 @@ func (h *handler) WechatNotify() core.HandlerFunc {
rmk := remark.Parse(ord.Remark) rmk := remark.Parse(ord.Remark)
act, _ := h.readDB.Activities.WithContext(bgCtx).Where(h.readDB.Activities.ID.Eq(rmk.ActivityID)).First() act, _ := h.readDB.Activities.WithContext(bgCtx).Where(h.readDB.Activities.ID.Eq(rmk.ActivityID)).First()
// 获取微信配置 (动态)
var wxConfig *wechat.WechatConfig
if dc := sysconfig.GetDynamicConfig(); dc != nil {
cfg := dc.GetWechat(bgCtx)
wxConfig = &wechat.WechatConfig{AppID: cfg.AppID, AppSecret: cfg.AppSecret}
}
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 {
@ -311,7 +339,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
} }
return "" return ""
}(); txID != "" { }(); txID != "" {
if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: configs.Get().Wechat.AppID, AppSecret: configs.Get().Wechat.AppSecret}, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil { if err := wechat.UploadVirtualShippingForBackground(bgCtx, wxConfig, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
h.logger.Error("次数卡虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo)) h.logger.Error("次数卡虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
} else { } else {
h.logger.Info("次数卡虚拟发货成功", zap.String("order_no", ord.OrderNo)) h.logger.Info("次数卡虚拟发货成功", zap.String("order_no", ord.OrderNo))
@ -330,7 +358,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
} }
return "" return ""
}(); txID != "" { }(); txID != "" {
if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: configs.Get().Wechat.AppID, AppSecret: configs.Get().Wechat.AppSecret}, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil { if err := wechat.UploadVirtualShippingForBackground(bgCtx, wxConfig, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
h.logger.Error("对对碰虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo)) h.logger.Error("对对碰虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
} else { } else {
h.logger.Info("对对碰虚拟发货成功", zap.String("order_no", ord.OrderNo)) h.logger.Info("对对碰虚拟发货成功", zap.String("order_no", ord.OrderNo))
@ -349,7 +377,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
} }
return "" return ""
}(); txID != "" { }(); txID != "" {
if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: configs.Get().Wechat.AppID, AppSecret: configs.Get().Wechat.AppSecret}, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil { if err := wechat.UploadVirtualShippingForBackground(bgCtx, wxConfig, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
h.logger.Error("商户订单虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo)) h.logger.Error("商户订单虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
} else { } else {
h.logger.Info("商户订单虚拟发货成功", zap.String("order_no", ord.OrderNo)) h.logger.Info("商户订单虚拟发货成功", zap.String("order_no", ord.OrderNo))

View File

@ -0,0 +1,402 @@
package public
import (
"fmt"
"net/http"
"time"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/model"
douyinsvc "bindbox-game/internal/service/douyin"
livestreamsvc "bindbox-game/internal/service/livestream"
"go.uber.org/zap"
"gorm.io/gorm"
)
type handler struct {
logger logger.CustomLogger
repo mysql.Repo
livestream livestreamsvc.Service
douyin douyinsvc.Service
}
// New 创建公开接口处理器
func New(l logger.CustomLogger, repo mysql.Repo, douyin douyinsvc.Service) *handler {
return &handler{
logger: l,
repo: repo,
livestream: livestreamsvc.New(l, repo),
douyin: douyin,
}
}
// ========== 直播间公开接口 ==========
type publicActivityResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
StreamerName string `json:"streamer_name"`
Status int32 `json:"status"`
StartTime string `json:"start_time,omitempty"`
EndTime string `json:"end_time,omitempty"`
Prizes []publicPrizeResponse `json:"prizes"`
}
type publicPrizeResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Level int32 `json:"level"`
Remaining int32 `json:"remaining"`
Probability string `json:"probability"`
Weight int32 `json:"weight"`
}
// GetLivestreamByAccessCode 根据访问码获取直播间活动详情
// @Summary 获取直播间活动详情(公开)
// @Description 根据访问码获取直播间活动和奖品信息,无需登录
// @Tags 公开接口.直播间
// @Accept json
// @Produce json
// @Param access_code path string true "访问码"
// @Success 200 {object} publicActivityResponse
// @Failure 404 {object} code.Failure
// @Router /api/public/livestream/{access_code} [get]
func (h *handler) GetLivestreamByAccessCode() core.HandlerFunc {
return func(ctx core.Context) {
accessCode := ctx.Param("access_code")
if accessCode == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
return
}
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在或已结束"))
return
}
prizes, _ := h.livestream.ListPrizes(ctx.RequestContext(), activity.ID)
res := &publicActivityResponse{
ID: activity.ID,
Name: activity.Name,
StreamerName: activity.StreamerName,
Status: activity.Status,
Prizes: make([]publicPrizeResponse, len(prizes)),
}
if !activity.StartTime.IsZero() {
res.StartTime = activity.StartTime.Format("2006-01-02 15:04:05")
}
if !activity.EndTime.IsZero() {
res.EndTime = activity.EndTime.Format("2006-01-02 15:04:05")
}
// 计算总权重 (仅统计有库存的)
var totalWeight int64
for _, p := range prizes {
if p.Remaining != 0 {
totalWeight += int64(p.Weight)
}
}
for i, p := range prizes {
probStr := "0%"
if p.Remaining != 0 && totalWeight > 0 {
prob := (float64(p.Weight) / float64(totalWeight)) * 100
probStr = fmt.Sprintf("%.2f%%", prob)
}
res.Prizes[i] = publicPrizeResponse{
ID: p.ID,
Name: p.Name,
Image: p.Image,
Level: p.Level,
Remaining: p.Remaining,
Probability: probStr,
Weight: p.Weight,
}
}
ctx.Payload(res)
}
}
type publicDrawLogResponse struct {
PrizeName string `json:"prize_name"`
Level int32 `json:"level"`
DouyinUserID string `json:"douyin_user_id"`
CreatedAt string `json:"created_at"`
}
type listPublicDrawLogsResponse struct {
List []publicDrawLogResponse `json:"list"`
Total int64 `json:"total"`
}
// GetLivestreamWinners 获取中奖记录(公开)
// @Summary 获取直播间中奖记录(公开)
// @Description 根据访问码获取直播间中奖历史,无需登录
// @Tags 公开接口.直播间
// @Accept json
// @Produce json
// @Param access_code path string true "访问码"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} listPublicDrawLogsResponse
// @Failure 404 {object} code.Failure
// @Router /api/public/livestream/{access_code}/winners [get]
func (h *handler) GetLivestreamWinners() core.HandlerFunc {
return func(ctx core.Context) {
accessCode := ctx.Param("access_code")
if accessCode == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
return
}
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在"))
return
}
page := 1 // Default page 1
pageSize := 20 // Default pageSize 20
var startTime, endTime *time.Time
params := ctx.RequestInputParams()
if stStr := params.Get("start_time"); stStr != "" {
if t, err := time.Parse(time.RFC3339, stStr); err == nil {
startTime = &t
}
}
if etStr := params.Get("end_time"); etStr != "" {
if t, err := time.Parse(time.RFC3339, etStr); err == nil {
endTime = &t
}
}
logs, total, err := h.livestream.ListDrawLogs(ctx.RequestContext(), activity.ID, page, pageSize, startTime, endTime)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10003, err.Error()))
return
}
res := &listPublicDrawLogsResponse{
List: make([]publicDrawLogResponse, len(logs)),
Total: total,
}
for i, log := range logs {
// 隐藏部分抖音ID
maskedID := log.DouyinUserID
if len(maskedID) > 4 {
maskedID = maskedID[:2] + "****" + maskedID[len(maskedID)-2:]
}
res.List[i] = publicDrawLogResponse{
PrizeName: log.PrizeName,
Level: log.Level,
DouyinUserID: maskedID,
CreatedAt: log.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
ctx.Payload(res)
}
}
// ========== 直播间抽奖接口 ==========
type drawRequest struct {
DouyinOrderID string `json:"shop_order_id" binding:"required"` // 店铺订单号
DouyinUserID string `json:"douyin_user_id"` // 可选,兼容旧逻辑
}
type drawReceipt struct {
SeedVersion int32 `json:"seed_version"`
Timestamp int64 `json:"timestamp"`
Nonce int64 `json:"nonce"`
Signature string `json:"signature"`
Algorithm string `json:"algorithm"`
}
type drawResponse struct {
PrizeID int64 `json:"prize_id"`
PrizeName string `json:"prize_name"`
PrizeImage string `json:"prize_image"`
Level int32 `json:"level"`
SeedHash string `json:"seed_hash"`
UserNickname string `json:"user_nickname"`
Receipt *drawReceipt `json:"receipt,omitempty"`
}
// DrawLivestream 执行直播间抽奖
// @Summary 执行直播间抽奖(公开)
// @Description 根据访问码执行抽奖需提供抖音用户ID
// @Tags 公开接口.直播间
// @Accept json
// @Produce json
// @Param access_code path string true "访问码"
// @Param body body drawRequest true "抽奖参数"
// @Success 200 {object} drawResponse
// @Failure 400 {object} code.Failure
// @Failure 404 {object} code.Failure
// @Router /api/public/livestream/{access_code}/draw [post]
func (h *handler) DrawLivestream() core.HandlerFunc {
return func(ctx core.Context) {
accessCode := ctx.Param("access_code")
if accessCode == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
return
}
var req drawRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10002, "参数格式错误"))
return
}
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, 10003, "活动不存在或已结束"))
return
}
if activity.Status != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10004, "活动未开始"))
return
}
// 1. [核心重构] 根据店铺订单号查出本地记录并核销
var order model.DouyinOrders
db := h.repo.GetDbW().WithContext(ctx.RequestContext())
err = db.Where("shop_order_id = ?", req.DouyinOrderID).First(&order).Error
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, 10005, "订单不存在"))
return
}
if order.RewardGranted >= int32(order.ProductCount) {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, "该订单已完成抽奖,请勿重复操作"))
return
}
// 执行抽奖
result, err := h.livestream.Draw(ctx.RequestContext(), livestreamsvc.DrawInput{
ActivityID: activity.ID,
DouyinOrderID: order.ID,
ShopOrderID: order.ShopOrderID,
DouyinUserID: order.DouyinUserID,
UserNickname: order.UserNickname,
})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10007, err.Error()))
return
}
// 标记订单已核销 (增加已发放计数)
// 使用 GORM 表达式更新,确保并发安全
// update douyin_orders set reward_granted = reward_granted + 1, updated_at = now() where id = ?
if err := db.Model(&order).Update("reward_granted", gorm.Expr("reward_granted + 1")).Error; err != nil {
h.logger.Error("[Draw] 更新订单发放状态失败", zap.String("order_id", order.ShopOrderID), zap.Error(err))
// 注意:这里虽然更新失败,但已执行抽奖,可能会导致用户少一次抽奖机会(计数没加),但为了防止超发,宁可少发。
// 理想情况是放在事务中,但 livestream.Draw 内部可能有独立事务。
}
res := &drawResponse{
PrizeID: result.Prize.ID,
PrizeName: result.Prize.Name,
PrizeImage: result.Prize.Image,
Level: result.Prize.Level,
SeedHash: result.SeedHash,
UserNickname: order.UserNickname,
}
// 填充凭证信息
if result.Receipt != nil {
res.Receipt = &drawReceipt{
SeedVersion: result.Receipt.SeedVersion,
Timestamp: result.Receipt.Timestamp,
Nonce: result.Receipt.Nonce,
Signature: result.Receipt.Signature,
Algorithm: result.Receipt.Algorithm,
}
}
ctx.Payload(res)
}
}
// SyncLivestreamOrders 触发全店订单同步并尝试发奖
func (h *handler) SyncLivestreamOrders() core.HandlerFunc {
return func(ctx core.Context) {
accessCode := ctx.Param("access_code")
if accessCode == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
return
}
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在"))
return
}
// 调用服务执行全量扫描 (此时已过滤 status=2)
result, err := h.douyin.SyncShopOrders(ctx.RequestContext(), activity.ID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10004, err.Error()))
return
}
ctx.Payload(map[string]any{
"message": "同步完成",
"total_fetched": result.TotalFetched,
"new_orders": result.NewOrders,
"matched_users": result.MatchedUsers,
})
}
}
// GetLivestreamPendingOrders 获取当前用户在该活动下的待抽奖订单 (Status 2 且未 Grant)
func (h *handler) GetLivestreamPendingOrders() core.HandlerFunc {
return func(ctx core.Context) {
accessCode := ctx.Param("access_code")
if accessCode == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
return
}
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在"))
return
}
// [核心优化] 自动同步:每次拉取待抽奖列表前,静默执行一次全店扫描
_, _ = h.douyin.SyncShopOrders(ctx.RequestContext(), activity.ID)
// 查询全店范围内所有待抽奖记录 (Status 2 且未核销完: reward_granted < product_count)
var pendingOrders []model.DouyinOrders
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
err = db.Where("order_status = 2 AND reward_granted < product_count").
Find(&pendingOrders).Error
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10003, err.Error()))
return
}
ctx.Payload(pendingOrders)
}
}

View File

@ -8,6 +8,8 @@ import (
"bindbox-game/internal/code" "bindbox-game/internal/code"
"bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/jwtoken" "bindbox-game/internal/pkg/jwtoken"
"go.uber.org/zap"
) )
type addressShareSubmitRequest struct { type addressShareSubmitRequest struct {
@ -58,6 +60,9 @@ func (h *handler) SubmitAddressShare() core.HandlerFunc {
// 统一使用 ctx.RequestContext() 包含 context 内容 // 统一使用 ctx.RequestContext() 包含 context 内容
addrID, err := h.user.SubmitAddressShare(ctx.RequestContext(), req.ShareToken, req.Name, req.Mobile, req.Province, req.City, req.District, req.Address, submitUserID, &ip) addrID, err := h.user.SubmitAddressShare(ctx.RequestContext(), req.ShareToken, req.Name, req.Mobile, req.Province, req.City, req.District, req.Address, submitUserID, &ip)
if err != nil { if err != nil {
// Log the error for debugging
h.logger.Error("SubmitAddressShare API Error", zap.Error(err), zap.String("token_masked", req.ShareToken[:10]+"..."))
// 处理业务错误,映射到具体代码 // 处理业务错误,映射到具体代码
msg := err.Error() msg := err.Error()
errorCode := 10024 errorCode := 10024

View File

@ -5,6 +5,7 @@ import (
"bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/service/douyin" "bindbox-game/internal/service/douyin"
gamesvc "bindbox-game/internal/service/game"
"bindbox-game/internal/service/sysconfig" "bindbox-game/internal/service/sysconfig"
tasksvc "bindbox-game/internal/service/task_center" tasksvc "bindbox-game/internal/service/task_center"
usersvc "bindbox-game/internal/service/user" usersvc "bindbox-game/internal/service/user"
@ -22,13 +23,14 @@ type handler struct {
func New(logger logger.CustomLogger, db mysql.Repo, taskSvc tasksvc.Service) *handler { func New(logger logger.CustomLogger, db mysql.Repo, taskSvc tasksvc.Service) *handler {
syscfgSvc := sysconfig.New(logger, db) syscfgSvc := sysconfig.New(logger, db)
userSvc := usersvc.New(logger, db)
return &handler{ return &handler{
logger: logger, logger: logger,
writeDB: dao.Use(db.GetDbW()), writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()), readDB: dao.Use(db.GetDbR()),
user: usersvc.New(logger, db), user: userSvc,
task: taskSvc, task: taskSvc,
douyin: douyin.New(logger, db, syscfgSvc, nil), douyin: douyin.New(logger, db, syscfgSvc, gamesvc.NewTicketService(logger, db), userSvc),
repo: db, repo: db,
} }
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"fmt"
"net/http" "net/http"
"time" "time"
@ -11,6 +12,7 @@ import (
"bindbox-game/internal/pkg/validation" "bindbox-game/internal/pkg/validation"
"bindbox-game/internal/pkg/wechat" "bindbox-game/internal/pkg/wechat"
"bindbox-game/internal/proposal" "bindbox-game/internal/proposal"
"bindbox-game/internal/service/sysconfig"
usersvc "bindbox-game/internal/service/user" usersvc "bindbox-game/internal/service/user"
"go.uber.org/zap" "go.uber.org/zap"
@ -25,6 +27,7 @@ type weixinLoginResponse 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"`
Mobile string `json:"mobile"` // 新增手机号字段
InviteCode string `json:"invite_code"` InviteCode string `json:"invite_code"`
OpenID string `json:"openid"` OpenID string `json:"openid"`
Token string `json:"token"` Token string `json:"token"`
@ -48,8 +51,12 @@ func (h *handler) WeixinLogin() 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
} }
cfg := configs.Get() // Use dynamic config
wxcfg := &wechat.WechatConfig{AppID: cfg.Wechat.AppID, AppSecret: cfg.Wechat.AppSecret} wxCfgVal := sysconfig.GetDynamicConfig().GetWechat(ctx.RequestContext().Context)
wxcfg := &wechat.WechatConfig{AppID: wxCfgVal.AppID, AppSecret: wxCfgVal.AppSecret}
fmt.Printf("DEBUG WeixinLogin: Using Config AppID=%s\n", wxcfg.AppID)
c2s, err := wechat.Code2Session(ctx.RequestContext().Context, wxcfg, req.Code) c2s, err := wechat.Code2Session(ctx.RequestContext().Context, wxcfg, req.Code)
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error()))
@ -74,6 +81,7 @@ func (h *handler) WeixinLogin() core.HandlerFunc {
} }
} }
rsp.Avatar = u.Avatar rsp.Avatar = u.Avatar
rsp.Mobile = u.Mobile // 返回手机号
rsp.InviteCode = u.InviteCode rsp.InviteCode = u.InviteCode
rsp.OpenID = c2s.OpenID rsp.OpenID = c2s.OpenID
sessionUserInfo := proposal.SessionUserInfo{Id: int32(u.ID), UserName: u.Nickname, NickName: u.Nickname, IsSuper: 0, Platform: "APP"} sessionUserInfo := proposal.SessionUserInfo{Id: int32(u.ID), UserName: u.Nickname, NickName: u.Nickname, IsSuper: 0, Platform: "APP"}

View File

@ -26,6 +26,7 @@ type douyinLoginResponse 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"`
Mobile string `json:"mobile"` // 新增手机号字段
InviteCode string `json:"invite_code"` InviteCode string `json:"invite_code"`
Token string `json:"token"` Token string `json:"token"`
} }
@ -70,6 +71,7 @@ func (h *handler) DouyinLogin() core.HandlerFunc {
rsp.UserID = u.ID rsp.UserID = u.ID
rsp.Nickname = u.Nickname rsp.Nickname = u.Nickname
rsp.Avatar = u.Avatar rsp.Avatar = u.Avatar
rsp.Mobile = u.Mobile // 返回手机号
rsp.InviteCode = u.InviteCode rsp.InviteCode = u.InviteCode
// 触发邀请奖励逻辑 // 触发邀请奖励逻辑

View File

@ -3,12 +3,12 @@ package app
import ( import (
"net/http" "net/http"
"bindbox-game/configs"
"bindbox-game/internal/code" "bindbox-game/internal/code"
"bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/pay" "bindbox-game/internal/pkg/pay"
"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/sysconfig"
) )
type jsapiPreorderRequest struct { type jsapiPreorderRequest struct {
@ -45,11 +45,15 @@ func (h *handler) WechatJSAPIPreorder() 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 ok, err := pay.ValidateConfig(); !ok { if ok, err := pay.ValidateConfig(ctx.RequestContext()); !ok {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140001, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 140001, err.Error()))
return return
} }
c := configs.Get() // Use dynamic configurations
dynamicDC := sysconfig.GetDynamicConfig()
wxCfg := dynamicDC.GetWechat(ctx.RequestContext().Context)
wxPayCfg := dynamicDC.GetWechatPay(ctx.RequestContext().Context)
if req.OrderNo == "" || req.OpenID == "" { if req.OrderNo == "" || req.OpenID == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140002, "order_no/openid required")) ctx.AbortWithError(core.Error(http.StatusBadRequest, 140002, "order_no/openid required"))
return return
@ -76,18 +80,18 @@ func (h *handler) WechatJSAPIPreorder() core.HandlerFunc {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140004, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 140004, err.Error()))
return return
} }
pid, err := wc.JSAPIPrepay(ctx.RequestContext(), c.Wechat.AppID, c.WechatPay.MchID, "订单"+req.OrderNo, req.OrderNo, order.ActualAmount, req.OpenID, c.WechatPay.NotifyURL) pid, err := wc.JSAPIPrepay(ctx.RequestContext(), wxCfg.AppID, wxPayCfg.MchID, "订单"+req.OrderNo, req.OrderNo, order.ActualAmount, req.OpenID, wxPayCfg.NotifyURL)
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140005, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 140005, err.Error()))
return return
} }
prepayID = pid prepayID = pid
pre := &model.PaymentPreorders{OrderID: order.ID, OrderNo: order.OrderNo, OutTradeNo: order.OrderNo, PrepayID: prepayID, AmountTotal: order.ActualAmount, PayerOpenid: req.OpenID, NotifyURL: c.WechatPay.NotifyURL, Status: "created"} pre := &model.PaymentPreorders{OrderID: order.ID, OrderNo: order.OrderNo, OutTradeNo: order.OrderNo, PrepayID: prepayID, AmountTotal: order.ActualAmount, PayerOpenid: req.OpenID, NotifyURL: wxPayCfg.NotifyURL, Status: "created"}
if err := h.writeDB.PaymentPreorders.WithContext(ctx.RequestContext()).Omit(h.writeDB.PaymentPreorders.ExpiredAt).Create(pre); err == nil { if err := h.writeDB.PaymentPreorders.WithContext(ctx.RequestContext()).Omit(h.writeDB.PaymentPreorders.ExpiredAt).Create(pre); err == nil {
_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(order.ID)).Updates(map[string]any{h.readDB.Orders.PayPreorderID.ColumnName().String(): pre.ID}) _, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(order.ID)).Updates(map[string]any{h.readDB.Orders.PayPreorderID.ColumnName().String(): pre.ID})
} }
} }
ts, nonce, pkg, signType, paySign, err := pay.BuildJSAPIParams(c.Wechat.AppID, prepayID) ts, nonce, pkg, signType, paySign, err := pay.BuildJSAPIParams(ctx.RequestContext(), wxCfg.AppID, prepayID)
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140003, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 140003, err.Error()))
return return

View File

@ -3,12 +3,12 @@ package app
import ( import (
"net/http" "net/http"
"bindbox-game/configs"
"bindbox-game/internal/code" "bindbox-game/internal/code"
"bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/miniprogram" "bindbox-game/internal/pkg/miniprogram"
"bindbox-game/internal/pkg/validation" "bindbox-game/internal/pkg/validation"
"bindbox-game/internal/pkg/wechat" "bindbox-game/internal/pkg/wechat"
"bindbox-game/internal/service/sysconfig"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -48,11 +48,15 @@ func (h *handler) BindPhone() core.HandlerFunc {
return return
} }
cfg := configs.Get() // cfg := configs.Get()
// Use dynamic config
wxCfg := sysconfig.GetDynamicConfig().GetWechat(ctx.RequestContext().Context)
var tokenRes struct { var tokenRes struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
} }
if err := miniprogram.GetAccessToken(cfg.Wechat.AppID, cfg.Wechat.AppSecret, &tokenRes); err != nil || tokenRes.AccessToken == "" { if err := miniprogram.GetAccessToken(wxCfg.AppID, wxCfg.AppSecret, &tokenRes); err != nil || tokenRes.AccessToken == "" {
h.logger.Error("获取微信access_token失败", zap.Error(err), zap.String("app_id", wxCfg.AppID), zap.String("app_secret", wxCfg.AppSecret))
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "获取微信access_token失败")) ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "获取微信access_token失败"))
return return
} }

View File

@ -74,7 +74,16 @@ func (h *handler) RedeemPointsToProduct() core.HandlerFunc {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150102, errMsg)) ctx.AbortWithError(core.Error(http.StatusBadRequest, 150102, errMsg))
return return
} }
resp, err := h.user.GrantReward(ctx.RequestContext(), userID, usersvc.GrantRewardRequest{ProductID: req.ProductID, Quantity: req.Quantity, Remark: prod.Name, PointsAmount: needCents})
// Mall Direct Purchase (SourceType=1)
sourceType := int32(1)
resp, err := h.user.GrantReward(ctx.RequestContext(), userID, usersvc.GrantRewardRequest{
ProductID: req.ProductID,
Quantity: req.Quantity,
Remark: prod.Name,
PointsAmount: needCents,
SourceType: &sourceType,
})
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150103, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 150103, err.Error()))
return return

View File

@ -10,6 +10,8 @@ import (
"bindbox-game/internal/pkg/httpclient" "bindbox-game/internal/pkg/httpclient"
pkgutils "bindbox-game/internal/pkg/utils" pkgutils "bindbox-game/internal/pkg/utils"
"go.uber.org/zap"
) )
// WechatNotifyConfig 微信通知配置 // WechatNotifyConfig 微信通知配置
@ -30,13 +32,8 @@ type LotteryResultNotificationRequest struct {
} }
// LotteryResultNotificationData 开奖结果通知数据字段 // LotteryResultNotificationData 开奖结果通知数据字段
// 根据微信订阅消息模板字段定义 // 使用 map 支持动态字段类型,根据模板灵活配置
// thing1: 活动名称, phrase3: 中奖结果, thing4: 温馨提示 type LotteryResultNotificationData map[string]DataValue
type LotteryResultNotificationData struct {
Thing1 DataValue `json:"thing1"` // 活动名称
Phrase3 DataValue `json:"phrase3"` // 中奖结果
Thing4 DataValue `json:"thing4"` // 温馨提示
}
// DataValue 数据值包装 // DataValue 数据值包装
type DataValue struct { type DataValue struct {
@ -108,20 +105,24 @@ func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig,
// 获取 access_token // 获取 access_token
accessToken, err := getAccessToken(ctx, cfg.AppID, cfg.AppSecret) accessToken, err := getAccessToken(ctx, cfg.AppID, cfg.AppSecret)
if err != nil { if err != nil {
fmt.Printf("[开奖通知] 获取access_token失败: %v\n", err) zap.L().Error("[开奖通知] 获取access_token失败", zap.Error(err), zap.String("openid", openid))
return err return err
} }
// 活动名称限制长度thing类型不超过20个字符 // 活动名称限制长度thing类型不超过20个字符
activityName = pkgutils.TruncateRunes(activityName, 20) activityName = pkgutils.TruncateRunes(activityName, 20)
// 构建中奖结果描述phrase类型限制5个汉字以内 // 活动结果:展示奖品列表
// 由于奖品名称通常较长phrase3 放不下,改为固定文案 "恭喜中奖"
// 将奖品名称放入 Thing4 (温馨提示),限制 20 字符
resultPhrase := "恭喜中奖"
rewardsStr := strings.Join(rewardNames, ",") rewardsStr := strings.Join(rewardNames, ",")
warmTips := pkgutils.TruncateRunes(rewardsStr, 20) if rewardsStr == "" {
rewardsStr = "无奖励"
}
// thing类型限制20字符
resultVal := pkgutils.TruncateRunes(rewardsStr, 20)
// 当前进度:固定为"已发货"
progress := "已发货"
// 使用模板字段thing6=活动名称, thing8=当前进度, thing9=活动结果
req := &LotteryResultNotificationRequest{ req := &LotteryResultNotificationRequest{
Touser: openid, Touser: openid,
TemplateID: cfg.LotteryResultTemplateID, TemplateID: cfg.LotteryResultTemplateID,
@ -129,13 +130,13 @@ func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig,
MiniprogramState: "formal", // 正式版 MiniprogramState: "formal", // 正式版
Lang: "zh_CN", Lang: "zh_CN",
Data: LotteryResultNotificationData{ Data: LotteryResultNotificationData{
Thing1: DataValue{Value: activityName}, // 活动名称 "thing6": {Value: activityName}, // 活动名称
Phrase3: DataValue{Value: resultPhrase}, // 中奖结果 "thing8": {Value: progress}, // 当前进度
Thing4: DataValue{Value: warmTips}, // 温馨提示(中奖奖品) "thing9": {Value: resultVal}, // 活动结果
}, },
} }
fmt.Printf("[开奖通知] 尝试发送 openid=%s activity=%s rewards=%v\n", openid, activityName, rewardNames) zap.L().Info("[开奖通知] 尝试发送", zap.String("openid", openid), zap.String("activity", activityName), zap.Strings("rewards", rewardNames))
// 发送请求 // 发送请求
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s", accessToken) url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s", accessToken)
@ -145,13 +146,13 @@ func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig,
SetBody(req). SetBody(req).
Post(url) Post(url)
if err != nil { if err != nil {
fmt.Printf("[开奖通知] 发送失败: %v\n", err) zap.L().Error("[开奖通知] 发送失败", zap.Error(err), zap.String("openid", openid))
return err return err
} }
var result LotteryResultNotificationResponse var result LotteryResultNotificationResponse
if err := json.Unmarshal(resp.Body(), &result); err != nil { if err := json.Unmarshal(resp.Body(), &result); err != nil {
fmt.Printf("[开奖通知] 解析响应失败: %v\n", err) zap.L().Error("[开奖通知] 解析响应失败", zap.Error(err), zap.String("body", string(resp.Body())))
return err return err
} }
@ -159,10 +160,10 @@ func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig,
// 常见错误码: // 常见错误码:
// 43101: 用户拒绝接受消息 // 43101: 用户拒绝接受消息
// 47003: 模板参数不准确 // 47003: 模板参数不准确
fmt.Printf("[开奖通知] 发送失败 errcode=%d errmsg=%s\n", result.Errcode, result.Errmsg) zap.L().Warn("[开奖通知] 发送失败", zap.Int("errcode", result.Errcode), zap.String("errmsg", result.Errmsg), zap.String("openid", openid))
return fmt.Errorf("发送订阅消息失败: errcode=%d, errmsg=%s", result.Errcode, result.Errmsg) return fmt.Errorf("发送订阅消息失败: errcode=%d, errmsg=%s", result.Errcode, result.Errmsg)
} }
fmt.Printf("[开奖通知] ✅ 发送成功 openid=%s\n", openid) zap.L().Info("[开奖通知] ✅ 发送成功", zap.String("openid", openid))
return nil return nil
} }

View File

@ -1,17 +1,19 @@
package pay package pay
import ( import (
"bindbox-game/internal/service/sysconfig"
"context" "context"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors" "errors"
"sync" "sync"
"bindbox-game/configs"
"github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/option" "github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
refundsvc "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" refundsvc "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
) )
type WechatPayClient struct { type WechatPayClient struct {
@ -25,6 +27,62 @@ var (
clientErr error clientErr error
) )
// LoadPrivateKeyFromBase64 从 Base64 编码的私钥内容创建 RSA 私钥
func LoadPrivateKeyFromBase64(base64Key string) (*rsa.PrivateKey, error) {
// 解码 Base64
keyBytes, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return nil, errors.New("failed to decode base64 private key: " + err.Error())
}
// 解析 PEM
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, errors.New("invalid private key PEM format")
}
// 尝试 PKCS8 格式
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// 尝试 PKCS1 格式
rsaKey, err2 := x509.ParsePKCS1PrivateKey(block.Bytes)
if err2 != nil {
return nil, errors.New("failed to parse private key: " + err.Error())
}
return rsaKey, nil
}
rsaKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("private key is not RSA type")
}
return rsaKey, nil
}
// LoadPublicKeyFromBase64 从 Base64 编码的公钥内容创建 RSA 公钥
func LoadPublicKeyFromBase64(base64Key string) (*rsa.PublicKey, error) {
keyBytes, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return nil, errors.New("failed to decode base64 public key: " + err.Error())
}
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, errors.New("invalid public key PEM format")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, errors.New("failed to parse public key: " + err.Error())
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, errors.New("public key is not RSA type")
}
return rsaPub, nil
}
// NewWechatPayClient 获取微信支付客户端(单例模式) // NewWechatPayClient 获取微信支付客户端(单例模式)
// 首次调用会初始化客户端,后续调用直接返回缓存的实例 // 首次调用会初始化客户端,后续调用直接返回缓存的实例
func NewWechatPayClient(ctx context.Context) (*WechatPayClient, error) { func NewWechatPayClient(ctx context.Context) (*WechatPayClient, error) {
@ -38,35 +96,66 @@ func NewWechatPayClient(ctx context.Context) (*WechatPayClient, error) {
} }
// initWechatPayClient 初始化微信支付客户端(内部实现) // initWechatPayClient 初始化微信支付客户端(内部实现)
// 优先使用动态配置中的 Base64 私钥内容fallback 到静态配置的文件路径
func initWechatPayClient(ctx context.Context) (*WechatPayClient, error) { func initWechatPayClient(ctx context.Context) (*WechatPayClient, error) {
cfg := configs.Get() // 必须从动态配置获取
if cfg.WechatPay.ApiV3Key == "" { var dynamicCfg *sysconfig.WechatPayConfig
return nil, errors.New("wechat pay config incomplete") if dc := sysconfig.GetDynamicConfig(); dc != nil {
cfg := dc.GetWechatPay(ctx)
dynamicCfg = &cfg
} }
var opts []core.ClientOption
if cfg.WechatPay.PublicKeyID != "" && cfg.WechatPay.PublicKeyPath != "" { if dynamicCfg == nil {
if cfg.WechatPay.MchID == "" || cfg.WechatPay.SerialNo == "" || cfg.WechatPay.PrivateKeyPath == "" { return nil, errors.New("wechat pay dynamic config missing")
return nil, errors.New("wechat pay config incomplete") }
}
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(cfg.WechatPay.PrivateKeyPath) mchID := dynamicCfg.MchID
serialNo := dynamicCfg.SerialNo
apiV3Key := dynamicCfg.ApiV3Key
if apiV3Key == "" {
return nil, errors.New("wechat pay config incomplete: api_v3_key missing")
}
if mchID == "" || serialNo == "" {
return nil, errors.New("wechat pay config incomplete: mchid or serial_no missing")
}
// 加载私钥:动态配置 Base64 内容
var mchPrivateKey *rsa.PrivateKey
var err error
if dynamicCfg.PrivateKey != "" {
mchPrivateKey, err = LoadPrivateKeyFromBase64(dynamicCfg.PrivateKey)
if err != nil { if err != nil {
return nil, err return nil, errors.New("read private key from dynamic config err:" + err.Error())
} }
pubKey, err := utils.LoadPublicKeyWithPath(cfg.WechatPay.PublicKeyPath)
if err != nil {
return nil, err
}
opts = []core.ClientOption{option.WithWechatPayPublicKeyAuthCipher(cfg.WechatPay.MchID, cfg.WechatPay.SerialNo, mchPrivateKey, cfg.WechatPay.PublicKeyID, pubKey)}
} else { } else {
if cfg.WechatPay.MchID == "" || cfg.WechatPay.SerialNo == "" || cfg.WechatPay.PrivateKeyPath == "" { return nil, errors.New("wechat pay private key not configured")
return nil, errors.New("wechat pay config incomplete")
}
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(cfg.WechatPay.PrivateKeyPath)
if err != nil {
return nil, err
}
opts = []core.ClientOption{option.WithWechatPayAutoAuthCipher(cfg.WechatPay.MchID, cfg.WechatPay.SerialNo, mchPrivateKey, cfg.WechatPay.ApiV3Key)}
} }
// 构建客户端选项
var opts []core.ClientOption
// 检查是否有公钥配置(新版验签方式)
publicKeyID := dynamicCfg.PublicKeyID
if publicKeyID != "" {
// 使用公钥验签模式
var pubKey *rsa.PublicKey
if dynamicCfg.PublicKey != "" {
pubKey, err = LoadPublicKeyFromBase64(dynamicCfg.PublicKey)
if err != nil {
return nil, errors.New("read public key from dynamic config err:" + err.Error())
}
} else {
return nil, errors.New("wechat pay public key not configured")
}
opts = []core.ClientOption{option.WithWechatPayPublicKeyAuthCipher(mchID, serialNo, mchPrivateKey, publicKeyID, pubKey)}
} else {
// 使用自动证书模式
opts = []core.ClientOption{option.WithWechatPayAutoAuthCipher(mchID, serialNo, mchPrivateKey, apiV3Key)}
}
client, err := core.NewClient(ctx, opts...) client, err := core.NewClient(ctx, opts...)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,6 +1,7 @@
package pay package pay
import ( import (
"context"
"crypto" "crypto"
crand "crypto/rand" crand "crypto/rand"
"crypto/rsa" "crypto/rsa"
@ -16,18 +17,54 @@ import (
"time" "time"
"bindbox-game/configs" "bindbox-game/configs"
"bindbox-game/internal/service/sysconfig"
) )
// 私钥缓存 - 避免每次请求都从磁盘读取 // 私钥缓存 - 避免每次请求都重新加载
var ( var (
cachedRSAKey *rsa.PrivateKey cachedRSAKey *rsa.PrivateKey
rsaKeyOnce sync.Once rsaKeyOnce sync.Once
rsaKeyLoadErr error rsaKeyLoadErr error
rsaKeyConfigPath string // 记录加载时的路径,用于检测配置变更 rsaKeyLoadFrom string // "dynamic" 或 "file"
) )
// loadRSAPrivateKey 从磁盘加载私钥(内部函数,仅在首次调用时执行) // getCachedRSAKeyForSign 获取缓存的RSA私钥用于签名
func loadRSAPrivateKey(keyPath string) (*rsa.PrivateKey, error) { // 优先使用动态配置中的 Base64 私钥内容fallback 到静态文件路径
func getCachedRSAKeyForSign(ctx context.Context) (*rsa.PrivateKey, error) {
rsaKeyOnce.Do(func() {
staticCfg := configs.Get()
// 尝试从动态配置获取
var dynamicCfg *sysconfig.WechatPayConfig
if dc := sysconfig.GetDynamicConfig(); dc != nil {
cfg := dc.GetWechatPay(ctx)
dynamicCfg = &cfg
}
// 优先动态配置的 Base64 内容
if dynamicCfg != nil && dynamicCfg.PrivateKey != "" {
cachedRSAKey, rsaKeyLoadErr = LoadPrivateKeyFromBase64(dynamicCfg.PrivateKey)
if rsaKeyLoadErr == nil {
rsaKeyLoadFrom = "dynamic"
return
}
}
// fallback 到静态文件路径
if staticCfg.WechatPay.PrivateKeyPath != "" {
cachedRSAKey, rsaKeyLoadErr = loadRSAPrivateKeyFromFile(staticCfg.WechatPay.PrivateKeyPath)
if rsaKeyLoadErr == nil {
rsaKeyLoadFrom = "file"
}
} else if rsaKeyLoadErr == nil {
rsaKeyLoadErr = errors.New("wechat pay private key not configured")
}
})
return cachedRSAKey, rsaKeyLoadErr
}
// loadRSAPrivateKeyFromFile 从磁盘加载私钥(内部函数)
func loadRSAPrivateKeyFromFile(keyPath string) (*rsa.PrivateKey, error) {
b, err := os.ReadFile(keyPath) b, err := os.ReadFile(keyPath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -52,31 +89,13 @@ func loadRSAPrivateKey(keyPath string) (*rsa.PrivateKey, error) {
return rsaKey, nil return rsaKey, nil
} }
// getCachedRSAKey 获取缓存的RSA私钥
func getCachedRSAKey(keyPath string) (*rsa.PrivateKey, error) {
rsaKeyOnce.Do(func() {
rsaKeyConfigPath = keyPath
cachedRSAKey, rsaKeyLoadErr = loadRSAPrivateKey(keyPath)
})
// 如果配置路径变更(理论上不应该发生),返回错误以提示重启
if rsaKeyConfigPath != keyPath {
return nil, errors.New("private key path changed, please restart the server")
}
return cachedRSAKey, rsaKeyLoadErr
}
// BuildJSAPIParams 为小程序支付构造客户端参数 // BuildJSAPIParams 为小程序支付构造客户端参数
// 入参:appid(微信小程序AppID)、prepayID(统一下单返回的prepay_id) // 入参ctx(上下文)、appid(微信小程序AppID)、prepayID(统一下单返回的prepay_id)
// 返回timeStamp、nonceStr、package(格式为"prepay_id=***" )、signType(固定"RSA")、paySign(RSA-SHA256签名) // 返回timeStamp、nonceStr、package(格式为"prepay_id=***")、signType(固定"RSA")、paySign(RSA-SHA256签名)
// 错误:当私钥读取或签名失败时返回错误 // 错误:当私钥读取或签名失败时返回错误
func BuildJSAPIParams(appid string, prepayID string) (timeStamp string, nonceStr string, pkg string, signType string, paySign string, err error) { func BuildJSAPIParams(ctx context.Context, appid string, prepayID string) (timeStamp string, nonceStr string, pkg string, signType string, paySign string, err error) {
cfg := configs.Get() // 使用缓存的私钥,优先动态配置
if cfg.WechatPay.PrivateKeyPath == "" { rsaKey, err := getCachedRSAKeyForSign(ctx)
return "", "", "", "", "", errors.New("wechat pay private key path not configured")
}
// 使用缓存的私钥,避免每次都从磁盘读取
rsaKey, err := getCachedRSAKey(cfg.WechatPay.PrivateKeyPath)
if err != nil { if err != nil {
return "", "", "", "", "", err return "", "", "", "", "", err
} }
@ -103,14 +122,34 @@ func BuildJSAPIParams(appid string, prepayID string) (timeStamp string, nonceStr
} }
// ValidateConfig 校验微信支付必要配置 // ValidateConfig 校验微信支付必要配置
// 入参: // 入参:ctx(上下文)
// 返回true表示配置齐全false表示缺失并附带错误信息 // 返回true表示配置齐全false表示缺失并附带错误信息
func ValidateConfig() (bool, error) { func ValidateConfig(ctx context.Context) (bool, error) {
c := configs.Get() // 检查动态配置
if c.Wechat.AppID == "" { var dynamicCfg *sysconfig.WechatPayConfig
var wxCfg *sysconfig.WechatConfig
if dc := sysconfig.GetDynamicConfig(); dc != nil {
pCfg := dc.GetWechatPay(ctx)
dynamicCfg = &pCfg
wCfg := dc.GetWechat(ctx)
wxCfg = &wCfg
}
if wxCfg == nil || wxCfg.AppID == "" {
return false, errors.New("wechat app_id missing") return false, errors.New("wechat app_id missing")
} }
if c.WechatPay.MchID == "" || c.WechatPay.SerialNo == "" || c.WechatPay.PrivateKeyPath == "" || c.WechatPay.ApiV3Key == "" {
if dynamicCfg == nil {
return false, errors.New("wechat pay config incomplete")
}
mchID := dynamicCfg.MchID
serialNo := dynamicCfg.SerialNo
apiV3Key := dynamicCfg.ApiV3Key
hasPrivateKey := dynamicCfg.PrivateKey != ""
if mchID == "" || serialNo == "" || !hasPrivateKey || apiV3Key == "" {
return false, errors.New("wechat pay config incomplete") return false, errors.New("wechat pay config incomplete")
} }
return true, nil return true, nil

View File

@ -19,6 +19,7 @@ type Code2SessionResponse struct {
func Code2Session(ctx context.Context, config *WechatConfig, code string) (*Code2SessionResponse, error) { func Code2Session(ctx context.Context, config *WechatConfig, code string) (*Code2SessionResponse, error) {
if config == nil || config.AppID == "" || config.AppSecret == "" { if config == nil || config.AppID == "" || config.AppSecret == "" {
fmt.Printf("DEBUG: Code2Session Config Missing: %+v\n", config)
return nil, fmt.Errorf("微信配置缺失") return nil, fmt.Errorf("微信配置缺失")
} }
if code == "" { if code == "" {

View File

@ -31,6 +31,9 @@ var (
GamePassPackages *gamePassPackages GamePassPackages *gamePassPackages
GameTicketLogs *gameTicketLogs GameTicketLogs *gameTicketLogs
IssuePositionClaims *issuePositionClaims IssuePositionClaims *issuePositionClaims
LivestreamActivities *livestreamActivities
LivestreamDrawLogs *livestreamDrawLogs
LivestreamPrizes *livestreamPrizes
LogOperation *logOperation LogOperation *logOperation
LogRequest *logRequest LogRequest *logRequest
MatchingCardTypes *matchingCardTypes MatchingCardTypes *matchingCardTypes
@ -94,6 +97,9 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
GamePassPackages = &Q.GamePassPackages GamePassPackages = &Q.GamePassPackages
GameTicketLogs = &Q.GameTicketLogs GameTicketLogs = &Q.GameTicketLogs
IssuePositionClaims = &Q.IssuePositionClaims IssuePositionClaims = &Q.IssuePositionClaims
LivestreamActivities = &Q.LivestreamActivities
LivestreamDrawLogs = &Q.LivestreamDrawLogs
LivestreamPrizes = &Q.LivestreamPrizes
LogOperation = &Q.LogOperation LogOperation = &Q.LogOperation
LogRequest = &Q.LogRequest LogRequest = &Q.LogRequest
MatchingCardTypes = &Q.MatchingCardTypes MatchingCardTypes = &Q.MatchingCardTypes
@ -158,6 +164,9 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
GamePassPackages: newGamePassPackages(db, opts...), GamePassPackages: newGamePassPackages(db, opts...),
GameTicketLogs: newGameTicketLogs(db, opts...), GameTicketLogs: newGameTicketLogs(db, opts...),
IssuePositionClaims: newIssuePositionClaims(db, opts...), IssuePositionClaims: newIssuePositionClaims(db, opts...),
LivestreamActivities: newLivestreamActivities(db, opts...),
LivestreamDrawLogs: newLivestreamDrawLogs(db, opts...),
LivestreamPrizes: newLivestreamPrizes(db, opts...),
LogOperation: newLogOperation(db, opts...), LogOperation: newLogOperation(db, opts...),
LogRequest: newLogRequest(db, opts...), LogRequest: newLogRequest(db, opts...),
MatchingCardTypes: newMatchingCardTypes(db, opts...), MatchingCardTypes: newMatchingCardTypes(db, opts...),
@ -223,6 +232,9 @@ type Query struct {
GamePassPackages gamePassPackages GamePassPackages gamePassPackages
GameTicketLogs gameTicketLogs GameTicketLogs gameTicketLogs
IssuePositionClaims issuePositionClaims IssuePositionClaims issuePositionClaims
LivestreamActivities livestreamActivities
LivestreamDrawLogs livestreamDrawLogs
LivestreamPrizes livestreamPrizes
LogOperation logOperation LogOperation logOperation
LogRequest logRequest LogRequest logRequest
MatchingCardTypes matchingCardTypes MatchingCardTypes matchingCardTypes
@ -289,6 +301,9 @@ func (q *Query) clone(db *gorm.DB) *Query {
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),
LivestreamActivities: q.LivestreamActivities.clone(db),
LivestreamDrawLogs: q.LivestreamDrawLogs.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),
MatchingCardTypes: q.MatchingCardTypes.clone(db), MatchingCardTypes: q.MatchingCardTypes.clone(db),
@ -362,6 +377,9 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
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),
LivestreamActivities: q.LivestreamActivities.replaceDB(db),
LivestreamDrawLogs: q.LivestreamDrawLogs.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),
MatchingCardTypes: q.MatchingCardTypes.replaceDB(db), MatchingCardTypes: q.MatchingCardTypes.replaceDB(db),
@ -425,6 +443,9 @@ type queryCtx struct {
GamePassPackages *gamePassPackagesDo GamePassPackages *gamePassPackagesDo
GameTicketLogs *gameTicketLogsDo GameTicketLogs *gameTicketLogsDo
IssuePositionClaims *issuePositionClaimsDo IssuePositionClaims *issuePositionClaimsDo
LivestreamActivities *livestreamActivitiesDo
LivestreamDrawLogs *livestreamDrawLogsDo
LivestreamPrizes *livestreamPrizesDo
LogOperation *logOperationDo LogOperation *logOperationDo
LogRequest *logRequestDo LogRequest *logRequestDo
MatchingCardTypes *matchingCardTypesDo MatchingCardTypes *matchingCardTypesDo
@ -488,6 +509,9 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
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),
LivestreamActivities: q.LivestreamActivities.WithContext(ctx),
LivestreamDrawLogs: q.LivestreamDrawLogs.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),
MatchingCardTypes: q.MatchingCardTypes.WithContext(ctx), MatchingCardTypes: q.MatchingCardTypes.WithContext(ctx),

View File

@ -0,0 +1,364 @@
// 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 newLivestreamActivities(db *gorm.DB, opts ...gen.DOOption) livestreamActivities {
_livestreamActivities := livestreamActivities{}
_livestreamActivities.livestreamActivitiesDo.UseDB(db, opts...)
_livestreamActivities.livestreamActivitiesDo.UseModel(&model.LivestreamActivities{})
tableName := _livestreamActivities.livestreamActivitiesDo.TableName()
_livestreamActivities.ALL = field.NewAsterisk(tableName)
_livestreamActivities.ID = field.NewInt64(tableName, "id")
_livestreamActivities.Name = field.NewString(tableName, "name")
_livestreamActivities.StreamerName = field.NewString(tableName, "streamer_name")
_livestreamActivities.StreamerContact = field.NewString(tableName, "streamer_contact")
_livestreamActivities.AccessCode = field.NewString(tableName, "access_code")
_livestreamActivities.DouyinProductID = field.NewString(tableName, "douyin_product_id")
_livestreamActivities.Status = field.NewInt32(tableName, "status")
_livestreamActivities.StartTime = field.NewTime(tableName, "start_time")
_livestreamActivities.EndTime = field.NewTime(tableName, "end_time")
_livestreamActivities.CreatedAt = field.NewTime(tableName, "created_at")
_livestreamActivities.UpdatedAt = field.NewTime(tableName, "updated_at")
_livestreamActivities.DeletedAt = field.NewField(tableName, "deleted_at")
_livestreamActivities.fillFieldMap()
return _livestreamActivities
}
// livestreamActivities 直播间活动表
type livestreamActivities struct {
livestreamActivitiesDo
ALL field.Asterisk
ID field.Int64 // 主键ID
Name field.String // 活动名称
StreamerName field.String // 主播名称
StreamerContact field.String // 主播联系方式
AccessCode field.String // 唯一访问码
DouyinProductID field.String // 关联抖店商品ID
Status field.Int32 // 状态:1进行中 2已结束
StartTime field.Time // 开始时间
EndTime field.Time // 结束时间
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
fieldMap map[string]field.Expr
}
func (l livestreamActivities) Table(newTableName string) *livestreamActivities {
l.livestreamActivitiesDo.UseTable(newTableName)
return l.updateTableName(newTableName)
}
func (l livestreamActivities) As(alias string) *livestreamActivities {
l.livestreamActivitiesDo.DO = *(l.livestreamActivitiesDo.As(alias).(*gen.DO))
return l.updateTableName(alias)
}
func (l *livestreamActivities) updateTableName(table string) *livestreamActivities {
l.ALL = field.NewAsterisk(table)
l.ID = field.NewInt64(table, "id")
l.Name = field.NewString(table, "name")
l.StreamerName = field.NewString(table, "streamer_name")
l.StreamerContact = field.NewString(table, "streamer_contact")
l.AccessCode = field.NewString(table, "access_code")
l.DouyinProductID = field.NewString(table, "douyin_product_id")
l.Status = field.NewInt32(table, "status")
l.StartTime = field.NewTime(table, "start_time")
l.EndTime = field.NewTime(table, "end_time")
l.CreatedAt = field.NewTime(table, "created_at")
l.UpdatedAt = field.NewTime(table, "updated_at")
l.DeletedAt = field.NewField(table, "deleted_at")
l.fillFieldMap()
return l
}
func (l *livestreamActivities) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := l.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (l *livestreamActivities) fillFieldMap() {
l.fieldMap = make(map[string]field.Expr, 12)
l.fieldMap["id"] = l.ID
l.fieldMap["name"] = l.Name
l.fieldMap["streamer_name"] = l.StreamerName
l.fieldMap["streamer_contact"] = l.StreamerContact
l.fieldMap["access_code"] = l.AccessCode
l.fieldMap["douyin_product_id"] = l.DouyinProductID
l.fieldMap["status"] = l.Status
l.fieldMap["start_time"] = l.StartTime
l.fieldMap["end_time"] = l.EndTime
l.fieldMap["created_at"] = l.CreatedAt
l.fieldMap["updated_at"] = l.UpdatedAt
l.fieldMap["deleted_at"] = l.DeletedAt
}
func (l livestreamActivities) clone(db *gorm.DB) livestreamActivities {
l.livestreamActivitiesDo.ReplaceConnPool(db.Statement.ConnPool)
return l
}
func (l livestreamActivities) replaceDB(db *gorm.DB) livestreamActivities {
l.livestreamActivitiesDo.ReplaceDB(db)
return l
}
type livestreamActivitiesDo struct{ gen.DO }
func (l livestreamActivitiesDo) Debug() *livestreamActivitiesDo {
return l.withDO(l.DO.Debug())
}
func (l livestreamActivitiesDo) WithContext(ctx context.Context) *livestreamActivitiesDo {
return l.withDO(l.DO.WithContext(ctx))
}
func (l livestreamActivitiesDo) ReadDB() *livestreamActivitiesDo {
return l.Clauses(dbresolver.Read)
}
func (l livestreamActivitiesDo) WriteDB() *livestreamActivitiesDo {
return l.Clauses(dbresolver.Write)
}
func (l livestreamActivitiesDo) Session(config *gorm.Session) *livestreamActivitiesDo {
return l.withDO(l.DO.Session(config))
}
func (l livestreamActivitiesDo) Clauses(conds ...clause.Expression) *livestreamActivitiesDo {
return l.withDO(l.DO.Clauses(conds...))
}
func (l livestreamActivitiesDo) Returning(value interface{}, columns ...string) *livestreamActivitiesDo {
return l.withDO(l.DO.Returning(value, columns...))
}
func (l livestreamActivitiesDo) Not(conds ...gen.Condition) *livestreamActivitiesDo {
return l.withDO(l.DO.Not(conds...))
}
func (l livestreamActivitiesDo) Or(conds ...gen.Condition) *livestreamActivitiesDo {
return l.withDO(l.DO.Or(conds...))
}
func (l livestreamActivitiesDo) Select(conds ...field.Expr) *livestreamActivitiesDo {
return l.withDO(l.DO.Select(conds...))
}
func (l livestreamActivitiesDo) Where(conds ...gen.Condition) *livestreamActivitiesDo {
return l.withDO(l.DO.Where(conds...))
}
func (l livestreamActivitiesDo) Order(conds ...field.Expr) *livestreamActivitiesDo {
return l.withDO(l.DO.Order(conds...))
}
func (l livestreamActivitiesDo) Distinct(cols ...field.Expr) *livestreamActivitiesDo {
return l.withDO(l.DO.Distinct(cols...))
}
func (l livestreamActivitiesDo) Omit(cols ...field.Expr) *livestreamActivitiesDo {
return l.withDO(l.DO.Omit(cols...))
}
func (l livestreamActivitiesDo) Join(table schema.Tabler, on ...field.Expr) *livestreamActivitiesDo {
return l.withDO(l.DO.Join(table, on...))
}
func (l livestreamActivitiesDo) LeftJoin(table schema.Tabler, on ...field.Expr) *livestreamActivitiesDo {
return l.withDO(l.DO.LeftJoin(table, on...))
}
func (l livestreamActivitiesDo) RightJoin(table schema.Tabler, on ...field.Expr) *livestreamActivitiesDo {
return l.withDO(l.DO.RightJoin(table, on...))
}
func (l livestreamActivitiesDo) Group(cols ...field.Expr) *livestreamActivitiesDo {
return l.withDO(l.DO.Group(cols...))
}
func (l livestreamActivitiesDo) Having(conds ...gen.Condition) *livestreamActivitiesDo {
return l.withDO(l.DO.Having(conds...))
}
func (l livestreamActivitiesDo) Limit(limit int) *livestreamActivitiesDo {
return l.withDO(l.DO.Limit(limit))
}
func (l livestreamActivitiesDo) Offset(offset int) *livestreamActivitiesDo {
return l.withDO(l.DO.Offset(offset))
}
func (l livestreamActivitiesDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *livestreamActivitiesDo {
return l.withDO(l.DO.Scopes(funcs...))
}
func (l livestreamActivitiesDo) Unscoped() *livestreamActivitiesDo {
return l.withDO(l.DO.Unscoped())
}
func (l livestreamActivitiesDo) Create(values ...*model.LivestreamActivities) error {
if len(values) == 0 {
return nil
}
return l.DO.Create(values)
}
func (l livestreamActivitiesDo) CreateInBatches(values []*model.LivestreamActivities, batchSize int) error {
return l.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 (l livestreamActivitiesDo) Save(values ...*model.LivestreamActivities) error {
if len(values) == 0 {
return nil
}
return l.DO.Save(values)
}
func (l livestreamActivitiesDo) First() (*model.LivestreamActivities, error) {
if result, err := l.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamActivities), nil
}
}
func (l livestreamActivitiesDo) Take() (*model.LivestreamActivities, error) {
if result, err := l.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamActivities), nil
}
}
func (l livestreamActivitiesDo) Last() (*model.LivestreamActivities, error) {
if result, err := l.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamActivities), nil
}
}
func (l livestreamActivitiesDo) Find() ([]*model.LivestreamActivities, error) {
result, err := l.DO.Find()
return result.([]*model.LivestreamActivities), err
}
func (l livestreamActivitiesDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.LivestreamActivities, err error) {
buf := make([]*model.LivestreamActivities, 0, batchSize)
err = l.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 (l livestreamActivitiesDo) FindInBatches(result *[]*model.LivestreamActivities, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return l.DO.FindInBatches(result, batchSize, fc)
}
func (l livestreamActivitiesDo) Attrs(attrs ...field.AssignExpr) *livestreamActivitiesDo {
return l.withDO(l.DO.Attrs(attrs...))
}
func (l livestreamActivitiesDo) Assign(attrs ...field.AssignExpr) *livestreamActivitiesDo {
return l.withDO(l.DO.Assign(attrs...))
}
func (l livestreamActivitiesDo) Joins(fields ...field.RelationField) *livestreamActivitiesDo {
for _, _f := range fields {
l = *l.withDO(l.DO.Joins(_f))
}
return &l
}
func (l livestreamActivitiesDo) Preload(fields ...field.RelationField) *livestreamActivitiesDo {
for _, _f := range fields {
l = *l.withDO(l.DO.Preload(_f))
}
return &l
}
func (l livestreamActivitiesDo) FirstOrInit() (*model.LivestreamActivities, error) {
if result, err := l.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamActivities), nil
}
}
func (l livestreamActivitiesDo) FirstOrCreate() (*model.LivestreamActivities, error) {
if result, err := l.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamActivities), nil
}
}
func (l livestreamActivitiesDo) FindByPage(offset int, limit int) (result []*model.LivestreamActivities, count int64, err error) {
result, err = l.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 = l.Offset(-1).Limit(-1).Count()
return
}
func (l livestreamActivitiesDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = l.Count()
if err != nil {
return
}
err = l.Offset(offset).Limit(limit).Scan(result)
return
}
func (l livestreamActivitiesDo) Scan(result interface{}) (err error) {
return l.DO.Scan(result)
}
func (l livestreamActivitiesDo) Delete(models ...*model.LivestreamActivities) (result gen.ResultInfo, err error) {
return l.DO.Delete(models)
}
func (l *livestreamActivitiesDo) withDO(do gen.Dao) *livestreamActivitiesDo {
l.DO = *do.(*gen.DO)
return l
}

View File

@ -0,0 +1,364 @@
// 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 newLivestreamDrawLogs(db *gorm.DB, opts ...gen.DOOption) livestreamDrawLogs {
_livestreamDrawLogs := livestreamDrawLogs{}
_livestreamDrawLogs.livestreamDrawLogsDo.UseDB(db, opts...)
_livestreamDrawLogs.livestreamDrawLogsDo.UseModel(&model.LivestreamDrawLogs{})
tableName := _livestreamDrawLogs.livestreamDrawLogsDo.TableName()
_livestreamDrawLogs.ALL = field.NewAsterisk(tableName)
_livestreamDrawLogs.ID = field.NewInt64(tableName, "id")
_livestreamDrawLogs.ActivityID = field.NewInt64(tableName, "activity_id")
_livestreamDrawLogs.PrizeID = field.NewInt64(tableName, "prize_id")
_livestreamDrawLogs.DouyinOrderID = field.NewInt64(tableName, "douyin_order_id")
_livestreamDrawLogs.LocalUserID = field.NewInt64(tableName, "local_user_id")
_livestreamDrawLogs.DouyinUserID = field.NewString(tableName, "douyin_user_id")
_livestreamDrawLogs.PrizeName = field.NewString(tableName, "prize_name")
_livestreamDrawLogs.Level = field.NewInt32(tableName, "level")
_livestreamDrawLogs.SeedHash = field.NewString(tableName, "seed_hash")
_livestreamDrawLogs.RandValue = field.NewInt64(tableName, "rand_value")
_livestreamDrawLogs.WeightsTotal = field.NewInt64(tableName, "weights_total")
_livestreamDrawLogs.CreatedAt = field.NewTime(tableName, "created_at")
_livestreamDrawLogs.fillFieldMap()
return _livestreamDrawLogs
}
// livestreamDrawLogs 直播间中奖记录表
type livestreamDrawLogs struct {
livestreamDrawLogsDo
ALL field.Asterisk
ID field.Int64 // 主键ID
ActivityID field.Int64 // 关联livestream_activities.id
PrizeID field.Int64 // 关联livestream_prizes.id
DouyinOrderID field.Int64 // 关联douyin_orders.id
LocalUserID field.Int64 // 本地用户ID
DouyinUserID field.String // 抖音用户ID
PrizeName field.String // 中奖奖品名称快照
Level field.Int32 // 奖品等级
SeedHash field.String // 哈希种子
RandValue field.Int64 // 随机值
WeightsTotal field.Int64 // 权重总和
CreatedAt field.Time // 中奖时间
fieldMap map[string]field.Expr
}
func (l livestreamDrawLogs) Table(newTableName string) *livestreamDrawLogs {
l.livestreamDrawLogsDo.UseTable(newTableName)
return l.updateTableName(newTableName)
}
func (l livestreamDrawLogs) As(alias string) *livestreamDrawLogs {
l.livestreamDrawLogsDo.DO = *(l.livestreamDrawLogsDo.As(alias).(*gen.DO))
return l.updateTableName(alias)
}
func (l *livestreamDrawLogs) updateTableName(table string) *livestreamDrawLogs {
l.ALL = field.NewAsterisk(table)
l.ID = field.NewInt64(table, "id")
l.ActivityID = field.NewInt64(table, "activity_id")
l.PrizeID = field.NewInt64(table, "prize_id")
l.DouyinOrderID = field.NewInt64(table, "douyin_order_id")
l.LocalUserID = field.NewInt64(table, "local_user_id")
l.DouyinUserID = field.NewString(table, "douyin_user_id")
l.PrizeName = field.NewString(table, "prize_name")
l.Level = field.NewInt32(table, "level")
l.SeedHash = field.NewString(table, "seed_hash")
l.RandValue = field.NewInt64(table, "rand_value")
l.WeightsTotal = field.NewInt64(table, "weights_total")
l.CreatedAt = field.NewTime(table, "created_at")
l.fillFieldMap()
return l
}
func (l *livestreamDrawLogs) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := l.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (l *livestreamDrawLogs) fillFieldMap() {
l.fieldMap = make(map[string]field.Expr, 12)
l.fieldMap["id"] = l.ID
l.fieldMap["activity_id"] = l.ActivityID
l.fieldMap["prize_id"] = l.PrizeID
l.fieldMap["douyin_order_id"] = l.DouyinOrderID
l.fieldMap["local_user_id"] = l.LocalUserID
l.fieldMap["douyin_user_id"] = l.DouyinUserID
l.fieldMap["prize_name"] = l.PrizeName
l.fieldMap["level"] = l.Level
l.fieldMap["seed_hash"] = l.SeedHash
l.fieldMap["rand_value"] = l.RandValue
l.fieldMap["weights_total"] = l.WeightsTotal
l.fieldMap["created_at"] = l.CreatedAt
}
func (l livestreamDrawLogs) clone(db *gorm.DB) livestreamDrawLogs {
l.livestreamDrawLogsDo.ReplaceConnPool(db.Statement.ConnPool)
return l
}
func (l livestreamDrawLogs) replaceDB(db *gorm.DB) livestreamDrawLogs {
l.livestreamDrawLogsDo.ReplaceDB(db)
return l
}
type livestreamDrawLogsDo struct{ gen.DO }
func (l livestreamDrawLogsDo) Debug() *livestreamDrawLogsDo {
return l.withDO(l.DO.Debug())
}
func (l livestreamDrawLogsDo) WithContext(ctx context.Context) *livestreamDrawLogsDo {
return l.withDO(l.DO.WithContext(ctx))
}
func (l livestreamDrawLogsDo) ReadDB() *livestreamDrawLogsDo {
return l.Clauses(dbresolver.Read)
}
func (l livestreamDrawLogsDo) WriteDB() *livestreamDrawLogsDo {
return l.Clauses(dbresolver.Write)
}
func (l livestreamDrawLogsDo) Session(config *gorm.Session) *livestreamDrawLogsDo {
return l.withDO(l.DO.Session(config))
}
func (l livestreamDrawLogsDo) Clauses(conds ...clause.Expression) *livestreamDrawLogsDo {
return l.withDO(l.DO.Clauses(conds...))
}
func (l livestreamDrawLogsDo) Returning(value interface{}, columns ...string) *livestreamDrawLogsDo {
return l.withDO(l.DO.Returning(value, columns...))
}
func (l livestreamDrawLogsDo) Not(conds ...gen.Condition) *livestreamDrawLogsDo {
return l.withDO(l.DO.Not(conds...))
}
func (l livestreamDrawLogsDo) Or(conds ...gen.Condition) *livestreamDrawLogsDo {
return l.withDO(l.DO.Or(conds...))
}
func (l livestreamDrawLogsDo) Select(conds ...field.Expr) *livestreamDrawLogsDo {
return l.withDO(l.DO.Select(conds...))
}
func (l livestreamDrawLogsDo) Where(conds ...gen.Condition) *livestreamDrawLogsDo {
return l.withDO(l.DO.Where(conds...))
}
func (l livestreamDrawLogsDo) Order(conds ...field.Expr) *livestreamDrawLogsDo {
return l.withDO(l.DO.Order(conds...))
}
func (l livestreamDrawLogsDo) Distinct(cols ...field.Expr) *livestreamDrawLogsDo {
return l.withDO(l.DO.Distinct(cols...))
}
func (l livestreamDrawLogsDo) Omit(cols ...field.Expr) *livestreamDrawLogsDo {
return l.withDO(l.DO.Omit(cols...))
}
func (l livestreamDrawLogsDo) Join(table schema.Tabler, on ...field.Expr) *livestreamDrawLogsDo {
return l.withDO(l.DO.Join(table, on...))
}
func (l livestreamDrawLogsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *livestreamDrawLogsDo {
return l.withDO(l.DO.LeftJoin(table, on...))
}
func (l livestreamDrawLogsDo) RightJoin(table schema.Tabler, on ...field.Expr) *livestreamDrawLogsDo {
return l.withDO(l.DO.RightJoin(table, on...))
}
func (l livestreamDrawLogsDo) Group(cols ...field.Expr) *livestreamDrawLogsDo {
return l.withDO(l.DO.Group(cols...))
}
func (l livestreamDrawLogsDo) Having(conds ...gen.Condition) *livestreamDrawLogsDo {
return l.withDO(l.DO.Having(conds...))
}
func (l livestreamDrawLogsDo) Limit(limit int) *livestreamDrawLogsDo {
return l.withDO(l.DO.Limit(limit))
}
func (l livestreamDrawLogsDo) Offset(offset int) *livestreamDrawLogsDo {
return l.withDO(l.DO.Offset(offset))
}
func (l livestreamDrawLogsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *livestreamDrawLogsDo {
return l.withDO(l.DO.Scopes(funcs...))
}
func (l livestreamDrawLogsDo) Unscoped() *livestreamDrawLogsDo {
return l.withDO(l.DO.Unscoped())
}
func (l livestreamDrawLogsDo) Create(values ...*model.LivestreamDrawLogs) error {
if len(values) == 0 {
return nil
}
return l.DO.Create(values)
}
func (l livestreamDrawLogsDo) CreateInBatches(values []*model.LivestreamDrawLogs, batchSize int) error {
return l.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 (l livestreamDrawLogsDo) Save(values ...*model.LivestreamDrawLogs) error {
if len(values) == 0 {
return nil
}
return l.DO.Save(values)
}
func (l livestreamDrawLogsDo) First() (*model.LivestreamDrawLogs, error) {
if result, err := l.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamDrawLogs), nil
}
}
func (l livestreamDrawLogsDo) Take() (*model.LivestreamDrawLogs, error) {
if result, err := l.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamDrawLogs), nil
}
}
func (l livestreamDrawLogsDo) Last() (*model.LivestreamDrawLogs, error) {
if result, err := l.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamDrawLogs), nil
}
}
func (l livestreamDrawLogsDo) Find() ([]*model.LivestreamDrawLogs, error) {
result, err := l.DO.Find()
return result.([]*model.LivestreamDrawLogs), err
}
func (l livestreamDrawLogsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.LivestreamDrawLogs, err error) {
buf := make([]*model.LivestreamDrawLogs, 0, batchSize)
err = l.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 (l livestreamDrawLogsDo) FindInBatches(result *[]*model.LivestreamDrawLogs, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return l.DO.FindInBatches(result, batchSize, fc)
}
func (l livestreamDrawLogsDo) Attrs(attrs ...field.AssignExpr) *livestreamDrawLogsDo {
return l.withDO(l.DO.Attrs(attrs...))
}
func (l livestreamDrawLogsDo) Assign(attrs ...field.AssignExpr) *livestreamDrawLogsDo {
return l.withDO(l.DO.Assign(attrs...))
}
func (l livestreamDrawLogsDo) Joins(fields ...field.RelationField) *livestreamDrawLogsDo {
for _, _f := range fields {
l = *l.withDO(l.DO.Joins(_f))
}
return &l
}
func (l livestreamDrawLogsDo) Preload(fields ...field.RelationField) *livestreamDrawLogsDo {
for _, _f := range fields {
l = *l.withDO(l.DO.Preload(_f))
}
return &l
}
func (l livestreamDrawLogsDo) FirstOrInit() (*model.LivestreamDrawLogs, error) {
if result, err := l.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamDrawLogs), nil
}
}
func (l livestreamDrawLogsDo) FirstOrCreate() (*model.LivestreamDrawLogs, error) {
if result, err := l.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamDrawLogs), nil
}
}
func (l livestreamDrawLogsDo) FindByPage(offset int, limit int) (result []*model.LivestreamDrawLogs, count int64, err error) {
result, err = l.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 = l.Offset(-1).Limit(-1).Count()
return
}
func (l livestreamDrawLogsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = l.Count()
if err != nil {
return
}
err = l.Offset(offset).Limit(limit).Scan(result)
return
}
func (l livestreamDrawLogsDo) Scan(result interface{}) (err error) {
return l.DO.Scan(result)
}
func (l livestreamDrawLogsDo) Delete(models ...*model.LivestreamDrawLogs) (result gen.ResultInfo, err error) {
return l.DO.Delete(models)
}
func (l *livestreamDrawLogsDo) withDO(do gen.Dao) *livestreamDrawLogsDo {
l.DO = *do.(*gen.DO)
return l
}

View File

@ -0,0 +1,364 @@
// 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 newLivestreamPrizes(db *gorm.DB, opts ...gen.DOOption) livestreamPrizes {
_livestreamPrizes := livestreamPrizes{}
_livestreamPrizes.livestreamPrizesDo.UseDB(db, opts...)
_livestreamPrizes.livestreamPrizesDo.UseModel(&model.LivestreamPrizes{})
tableName := _livestreamPrizes.livestreamPrizesDo.TableName()
_livestreamPrizes.ALL = field.NewAsterisk(tableName)
_livestreamPrizes.ID = field.NewInt64(tableName, "id")
_livestreamPrizes.ActivityID = field.NewInt64(tableName, "activity_id")
_livestreamPrizes.Name = field.NewString(tableName, "name")
_livestreamPrizes.Image = field.NewString(tableName, "image")
_livestreamPrizes.Weight = field.NewInt32(tableName, "weight")
_livestreamPrizes.Quantity = field.NewInt32(tableName, "quantity")
_livestreamPrizes.Remaining = field.NewInt32(tableName, "remaining")
_livestreamPrizes.Level = field.NewInt32(tableName, "level")
_livestreamPrizes.ProductID = field.NewInt64(tableName, "product_id")
_livestreamPrizes.Sort = field.NewInt32(tableName, "sort")
_livestreamPrizes.CreatedAt = field.NewTime(tableName, "created_at")
_livestreamPrizes.UpdatedAt = field.NewTime(tableName, "updated_at")
_livestreamPrizes.fillFieldMap()
return _livestreamPrizes
}
// livestreamPrizes 直播间奖品表
type livestreamPrizes struct {
livestreamPrizesDo
ALL field.Asterisk
ID field.Int64 // 主键ID
ActivityID field.Int64 // 关联livestream_activities.id
Name field.String // 奖品名称
Image field.String // 奖品图片
Weight field.Int32 // 抽奖权重
Quantity field.Int32 // 库存数量(-1=无限)
Remaining field.Int32 // 剩余数量
Level field.Int32 // 奖品等级
ProductID field.Int64 // 关联系统商品ID
Sort field.Int32 // 排序
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
fieldMap map[string]field.Expr
}
func (l livestreamPrizes) Table(newTableName string) *livestreamPrizes {
l.livestreamPrizesDo.UseTable(newTableName)
return l.updateTableName(newTableName)
}
func (l livestreamPrizes) As(alias string) *livestreamPrizes {
l.livestreamPrizesDo.DO = *(l.livestreamPrizesDo.As(alias).(*gen.DO))
return l.updateTableName(alias)
}
func (l *livestreamPrizes) updateTableName(table string) *livestreamPrizes {
l.ALL = field.NewAsterisk(table)
l.ID = field.NewInt64(table, "id")
l.ActivityID = field.NewInt64(table, "activity_id")
l.Name = field.NewString(table, "name")
l.Image = field.NewString(table, "image")
l.Weight = field.NewInt32(table, "weight")
l.Quantity = field.NewInt32(table, "quantity")
l.Remaining = field.NewInt32(table, "remaining")
l.Level = field.NewInt32(table, "level")
l.ProductID = field.NewInt64(table, "product_id")
l.Sort = field.NewInt32(table, "sort")
l.CreatedAt = field.NewTime(table, "created_at")
l.UpdatedAt = field.NewTime(table, "updated_at")
l.fillFieldMap()
return l
}
func (l *livestreamPrizes) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := l.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (l *livestreamPrizes) fillFieldMap() {
l.fieldMap = make(map[string]field.Expr, 12)
l.fieldMap["id"] = l.ID
l.fieldMap["activity_id"] = l.ActivityID
l.fieldMap["name"] = l.Name
l.fieldMap["image"] = l.Image
l.fieldMap["weight"] = l.Weight
l.fieldMap["quantity"] = l.Quantity
l.fieldMap["remaining"] = l.Remaining
l.fieldMap["level"] = l.Level
l.fieldMap["product_id"] = l.ProductID
l.fieldMap["sort"] = l.Sort
l.fieldMap["created_at"] = l.CreatedAt
l.fieldMap["updated_at"] = l.UpdatedAt
}
func (l livestreamPrizes) clone(db *gorm.DB) livestreamPrizes {
l.livestreamPrizesDo.ReplaceConnPool(db.Statement.ConnPool)
return l
}
func (l livestreamPrizes) replaceDB(db *gorm.DB) livestreamPrizes {
l.livestreamPrizesDo.ReplaceDB(db)
return l
}
type livestreamPrizesDo struct{ gen.DO }
func (l livestreamPrizesDo) Debug() *livestreamPrizesDo {
return l.withDO(l.DO.Debug())
}
func (l livestreamPrizesDo) WithContext(ctx context.Context) *livestreamPrizesDo {
return l.withDO(l.DO.WithContext(ctx))
}
func (l livestreamPrizesDo) ReadDB() *livestreamPrizesDo {
return l.Clauses(dbresolver.Read)
}
func (l livestreamPrizesDo) WriteDB() *livestreamPrizesDo {
return l.Clauses(dbresolver.Write)
}
func (l livestreamPrizesDo) Session(config *gorm.Session) *livestreamPrizesDo {
return l.withDO(l.DO.Session(config))
}
func (l livestreamPrizesDo) Clauses(conds ...clause.Expression) *livestreamPrizesDo {
return l.withDO(l.DO.Clauses(conds...))
}
func (l livestreamPrizesDo) Returning(value interface{}, columns ...string) *livestreamPrizesDo {
return l.withDO(l.DO.Returning(value, columns...))
}
func (l livestreamPrizesDo) Not(conds ...gen.Condition) *livestreamPrizesDo {
return l.withDO(l.DO.Not(conds...))
}
func (l livestreamPrizesDo) Or(conds ...gen.Condition) *livestreamPrizesDo {
return l.withDO(l.DO.Or(conds...))
}
func (l livestreamPrizesDo) Select(conds ...field.Expr) *livestreamPrizesDo {
return l.withDO(l.DO.Select(conds...))
}
func (l livestreamPrizesDo) Where(conds ...gen.Condition) *livestreamPrizesDo {
return l.withDO(l.DO.Where(conds...))
}
func (l livestreamPrizesDo) Order(conds ...field.Expr) *livestreamPrizesDo {
return l.withDO(l.DO.Order(conds...))
}
func (l livestreamPrizesDo) Distinct(cols ...field.Expr) *livestreamPrizesDo {
return l.withDO(l.DO.Distinct(cols...))
}
func (l livestreamPrizesDo) Omit(cols ...field.Expr) *livestreamPrizesDo {
return l.withDO(l.DO.Omit(cols...))
}
func (l livestreamPrizesDo) Join(table schema.Tabler, on ...field.Expr) *livestreamPrizesDo {
return l.withDO(l.DO.Join(table, on...))
}
func (l livestreamPrizesDo) LeftJoin(table schema.Tabler, on ...field.Expr) *livestreamPrizesDo {
return l.withDO(l.DO.LeftJoin(table, on...))
}
func (l livestreamPrizesDo) RightJoin(table schema.Tabler, on ...field.Expr) *livestreamPrizesDo {
return l.withDO(l.DO.RightJoin(table, on...))
}
func (l livestreamPrizesDo) Group(cols ...field.Expr) *livestreamPrizesDo {
return l.withDO(l.DO.Group(cols...))
}
func (l livestreamPrizesDo) Having(conds ...gen.Condition) *livestreamPrizesDo {
return l.withDO(l.DO.Having(conds...))
}
func (l livestreamPrizesDo) Limit(limit int) *livestreamPrizesDo {
return l.withDO(l.DO.Limit(limit))
}
func (l livestreamPrizesDo) Offset(offset int) *livestreamPrizesDo {
return l.withDO(l.DO.Offset(offset))
}
func (l livestreamPrizesDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *livestreamPrizesDo {
return l.withDO(l.DO.Scopes(funcs...))
}
func (l livestreamPrizesDo) Unscoped() *livestreamPrizesDo {
return l.withDO(l.DO.Unscoped())
}
func (l livestreamPrizesDo) Create(values ...*model.LivestreamPrizes) error {
if len(values) == 0 {
return nil
}
return l.DO.Create(values)
}
func (l livestreamPrizesDo) CreateInBatches(values []*model.LivestreamPrizes, batchSize int) error {
return l.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 (l livestreamPrizesDo) Save(values ...*model.LivestreamPrizes) error {
if len(values) == 0 {
return nil
}
return l.DO.Save(values)
}
func (l livestreamPrizesDo) First() (*model.LivestreamPrizes, error) {
if result, err := l.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamPrizes), nil
}
}
func (l livestreamPrizesDo) Take() (*model.LivestreamPrizes, error) {
if result, err := l.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamPrizes), nil
}
}
func (l livestreamPrizesDo) Last() (*model.LivestreamPrizes, error) {
if result, err := l.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamPrizes), nil
}
}
func (l livestreamPrizesDo) Find() ([]*model.LivestreamPrizes, error) {
result, err := l.DO.Find()
return result.([]*model.LivestreamPrizes), err
}
func (l livestreamPrizesDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.LivestreamPrizes, err error) {
buf := make([]*model.LivestreamPrizes, 0, batchSize)
err = l.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 (l livestreamPrizesDo) FindInBatches(result *[]*model.LivestreamPrizes, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return l.DO.FindInBatches(result, batchSize, fc)
}
func (l livestreamPrizesDo) Attrs(attrs ...field.AssignExpr) *livestreamPrizesDo {
return l.withDO(l.DO.Attrs(attrs...))
}
func (l livestreamPrizesDo) Assign(attrs ...field.AssignExpr) *livestreamPrizesDo {
return l.withDO(l.DO.Assign(attrs...))
}
func (l livestreamPrizesDo) Joins(fields ...field.RelationField) *livestreamPrizesDo {
for _, _f := range fields {
l = *l.withDO(l.DO.Joins(_f))
}
return &l
}
func (l livestreamPrizesDo) Preload(fields ...field.RelationField) *livestreamPrizesDo {
for _, _f := range fields {
l = *l.withDO(l.DO.Preload(_f))
}
return &l
}
func (l livestreamPrizesDo) FirstOrInit() (*model.LivestreamPrizes, error) {
if result, err := l.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamPrizes), nil
}
}
func (l livestreamPrizesDo) FirstOrCreate() (*model.LivestreamPrizes, error) {
if result, err := l.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.LivestreamPrizes), nil
}
}
func (l livestreamPrizesDo) FindByPage(offset int, limit int) (result []*model.LivestreamPrizes, count int64, err error) {
result, err = l.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 = l.Offset(-1).Limit(-1).Count()
return
}
func (l livestreamPrizesDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = l.Count()
if err != nil {
return
}
err = l.Offset(offset).Limit(limit).Scan(result)
return
}
func (l livestreamPrizesDo) Scan(result interface{}) (err error) {
return l.DO.Scan(result)
}
func (l livestreamPrizesDo) Delete(models ...*model.LivestreamPrizes) (result gen.ResultInfo, err error) {
return l.DO.Delete(models)
}
func (l *livestreamPrizesDo) withDO(do gen.Dao) *livestreamPrizesDo {
l.DO = *do.(*gen.DO)
return l
}

View File

@ -51,7 +51,7 @@ func newShippingRecords(db *gorm.DB, opts ...gen.DOOption) shippingRecords {
return _shippingRecords return _shippingRecords
} }
// shippingRecords 发货记录(合并:单 // shippingRecords 发货记录
type shippingRecords struct { type shippingRecords struct {
shippingRecordsDo shippingRecordsDo

View File

@ -14,6 +14,7 @@ const TableNameDouyinOrders = "douyin_orders"
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
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
@ -23,6 +24,7 @@ type DouyinOrders struct {
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称 UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据 RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
RewardGranted int32 `gorm:"column:reward_granted;not null;default:0;comment:奖励已发放: 0=否, 1=是" json:"reward_granted"` // 奖励已发放 RewardGranted int32 `gorm:"column:reward_granted;not null;default:0;comment:奖励已发放: 0=否, 1=是" json:"reward_granted"` // 奖励已发放
ProductCount int64 `gorm:"column:product_count;not null;default:1;comment:商品数量" json:"product_count"` // 商品数量
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"`
} }

View File

@ -0,0 +1,39 @@
// 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"
"gorm.io/gorm"
)
const TableNameLivestreamActivities = "livestream_activities"
// LivestreamActivities 直播间活动表
type LivestreamActivities struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
Name string `gorm:"column:name;not null;comment:活动名称" json:"name"` // 活动名称
StreamerName string `gorm:"column:streamer_name;comment:主播名称" json:"streamer_name"` // 主播名称
StreamerContact string `gorm:"column:streamer_contact;comment:主播联系方式" json:"streamer_contact"` // 主播联系方式
AccessCode string `gorm:"column:access_code;not null;comment:唯一访问码" json:"access_code"` // 唯一访问码
DouyinProductID string `gorm:"column:douyin_product_id;comment:关联抖店商品ID" json:"douyin_product_id"` // 关联抖店商品ID
Status int32 `gorm:"column:status;not null;default:1;comment:状态:1进行中 2已结束" json:"status"` // 状态:1进行中 2已结束
TicketPrice int64 `gorm:"column:ticket_price;comment:门票价格(分)" json:"ticket_price"` // 门票价格(分)
CommitmentAlgo string `gorm:"column:commitment_algo;default:commit-v1;comment:承诺算法版本" json:"commitment_algo"` // 承诺算法版本
CommitmentSeedMaster []byte `gorm:"column:commitment_seed_master;comment:主种子(32字节)" json:"commitment_seed_master"` // 主种子(32字节)
CommitmentSeedHash []byte `gorm:"column:commitment_seed_hash;comment:种子SHA256哈希" json:"commitment_seed_hash"` // 种子SHA256哈希
CommitmentStateVersion int32 `gorm:"column:commitment_state_version;default:0;comment:状态版本" json:"commitment_state_version"` // 状态版本
StartTime time.Time `gorm:"column:start_time;comment:开始时间" json:"start_time"` // 开始时间
EndTime time.Time `gorm:"column:end_time;comment:结束时间" json:"end_time"` // 结束时间
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
}
// TableName LivestreamActivities's table name
func (*LivestreamActivities) TableName() string {
return TableNameLivestreamActivities
}

View File

@ -0,0 +1,36 @@
// 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 TableNameLivestreamDrawLogs = "livestream_draw_logs"
// LivestreamDrawLogs 直播间中奖记录表
type LivestreamDrawLogs struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键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
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"` // 抖店订单号
LocalUserID int64 `gorm:"column:local_user_id;comment:本地用户ID" json:"local_user_id"` // 本地用户ID
DouyinUserID string `gorm:"column:douyin_user_id;comment:抖音用户ID" json:"douyin_user_id"` // 抖音用户ID
UserNickname string `gorm:"column:user_nickname;default:'';comment:用户昵称" json:"user_nickname"` // 用户昵称
PrizeName string `gorm:"column:prize_name;comment:中奖奖品名称快照" json:"prize_name"` // 中奖奖品名称快照
Level int32 `gorm:"column:level;default:1;comment:奖品等级" json:"level"` // 奖品等级
SeedHash string `gorm:"column:seed_hash;comment:哈希种子" json:"seed_hash"` // 哈希种子
RandValue int64 `gorm:"column:rand_value;comment:随机值" json:"rand_value"` // 随机值
WeightsTotal int64 `gorm:"column:weights_total;comment:权重总和" json:"weights_total"` // 权重总和
IsGranted int32 `gorm:"column:is_granted;default:0;comment:是否已发放奖品" json:"is_granted"` // 是否已发放奖品
IsRefunded int32 `gorm:"column:is_refunded;default:0;comment:订单是否已退款" json:"is_refunded"` // 订单是否已退款
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:中奖时间" json:"created_at"` // 中奖时间
}
// TableName LivestreamDrawLogs's table name
func (*LivestreamDrawLogs) TableName() string {
return TableNameLivestreamDrawLogs
}

View File

@ -0,0 +1,33 @@
// 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 TableNameLivestreamPrizes = "livestream_prizes"
// LivestreamPrizes 直播间奖品表
type LivestreamPrizes struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
ActivityID int64 `gorm:"column:activity_id;not null;comment:关联livestream_activities.id" json:"activity_id"` // 关联livestream_activities.id
Name string `gorm:"column:name;not null;comment:奖品名称" json:"name"` // 奖品名称
Image string `gorm:"column:image;comment:奖品图片" json:"image"` // 奖品图片
Weight int32 `gorm:"column:weight;not null;default:1;comment:抽奖权重" json:"weight"` // 抽奖权重
Quantity int32 `gorm:"column:quantity;not null;default:-1;comment:库存数量(-1=无限)" json:"quantity"` // 库存数量(-1=无限)
Remaining int32 `gorm:"column:remaining;not null;default:-1;comment:剩余数量" json:"remaining"` // 剩余数量
Level int32 `gorm:"column:level;not null;default:1;comment:奖品等级" json:"level"` // 奖品等级
ProductID int64 `gorm:"column:product_id;comment:关联系统商品ID" json:"product_id"` // 关联系统商品ID
CostPrice int64 `gorm:"column:cost_price;comment:成本价(分)" json:"cost_price"` // 成本价(分)
Sort int32 `gorm:"column:sort;not null;comment:排序" json:"sort"` // 排序
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
}
// TableName LivestreamPrizes's table name
func (*LivestreamPrizes) TableName() string {
return TableNameLivestreamPrizes
}

View File

@ -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:下单用户IDuser_members.id" json:"user_id"` // 下单用户IDuser_members.id UserID int64 `gorm:"column:user_id;not null;comment:下单用户IDuser_members.id" json:"user_id"` // 下单用户IDuser_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其他" json:"source_type"` // 来源1商城直购 2抽奖票据 3其他 SourceType int32 `gorm:"column:source_type;not null;default:1;comment:来源1商城/积分 2抽奖 3对对碰 4次数卡 5直播间 6系统发放" json:"source_type"` // 来源1商城/积分 2抽奖 3对对碰 4次数卡 5直播间 6系统发放
TotalAmount int64 `gorm:"column:total_amount;not null;comment:订单总金额(分)" json:"total_amount"` // 订单总金额(分) 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:关联预支付单IDpayment_preorder.id" json:"pay_preorder_id"` // 关联预支付单IDpayment_preorder.id PayPreorderID int64 `gorm:"column:pay_preorder_id;comment:关联预支付单IDpayment_preorder.id" json:"pay_preorder_id"` // 关联预支付单IDpayment_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:收货地址IDuser_addresses.id" json:"user_address_id"` // 收货地址IDuser_addresses.id UserAddressID int64 `gorm:"column:user_address_id;comment:收货地址IDuser_addresses.id" json:"user_address_id"` // 收货地址IDuser_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:积分扣减流水IDuser_points_ledger.id" json:"points_ledger_id"` // 积分扣减流水IDuser_points_ledger.id PointsLedgerID int64 `gorm:"column:points_ledger_id;comment:积分扣减流水IDuser_points_ledger.id" json:"points_ledger_id"` // 积分扣减流水IDuser_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

View File

@ -10,7 +10,7 @@ import (
const TableNameShippingRecords = "shipping_records" const TableNameShippingRecords = "shipping_records"
// ShippingRecords 发货记录(合并:单 // ShippingRecords 发货记录
type ShippingRecords struct { type ShippingRecords 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"` // 创建时间

View File

@ -9,6 +9,7 @@ import (
commonapi "bindbox-game/internal/api/common" commonapi "bindbox-game/internal/api/common"
gameapi "bindbox-game/internal/api/game" gameapi "bindbox-game/internal/api/game"
payapi "bindbox-game/internal/api/pay" payapi "bindbox-game/internal/api/pay"
publicapi "bindbox-game/internal/api/public"
taskcenterapi "bindbox-game/internal/api/task_center" taskcenterapi "bindbox-game/internal/api/task_center"
userapi "bindbox-game/internal/api/user" userapi "bindbox-game/internal/api/user"
"bindbox-game/internal/dblogger" "bindbox-game/internal/dblogger"
@ -19,6 +20,9 @@ import (
"bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql"
"bindbox-game/internal/router/interceptor" "bindbox-game/internal/router/interceptor"
activitysvc "bindbox-game/internal/service/activity" activitysvc "bindbox-game/internal/service/activity"
douyinsvc "bindbox-game/internal/service/douyin"
gamesvc "bindbox-game/internal/service/game"
syscfgsvc "bindbox-game/internal/service/sysconfig"
tasksvc "bindbox-game/internal/service/task_center" tasksvc "bindbox-game/internal/service/task_center"
titlesvc "bindbox-game/internal/service/title" titlesvc "bindbox-game/internal/service/title"
usersvc "bindbox-game/internal/service/user" usersvc "bindbox-game/internal/service/user"
@ -62,6 +66,9 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
titleSvc := titlesvc.New(logger, db) titleSvc := titlesvc.New(logger, db)
taskSvc := tasksvc.New(logger, db, rdb, userSvc, titleSvc) taskSvc := tasksvc.New(logger, db, rdb, userSvc, titleSvc)
activitySvc := activitysvc.New(logger, db, userSvc, rdb) activitySvc := activitysvc.New(logger, db, userSvc, rdb)
syscfgSvc := syscfgsvc.New(logger, db)
ticketSvc := gamesvc.NewTicketService(logger, db)
douyinSvc := douyinsvc.New(logger, db, syscfgSvc, ticketSvc, userSvc)
// Context for Worker // Context for Worker
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -221,6 +228,20 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
adminAuthApiRouter.PUT("/douyin/product-rewards/:id", adminHandler.UpdateDouyinProductReward()) adminAuthApiRouter.PUT("/douyin/product-rewards/:id", adminHandler.UpdateDouyinProductReward())
adminAuthApiRouter.DELETE("/douyin/product-rewards/:id", adminHandler.DeleteDouyinProductReward()) adminAuthApiRouter.DELETE("/douyin/product-rewards/:id", adminHandler.DeleteDouyinProductReward())
// 直播间活动管理
adminAuthApiRouter.POST("/livestream/activities", adminHandler.CreateLivestreamActivity())
adminAuthApiRouter.GET("/livestream/activities", adminHandler.ListLivestreamActivities())
adminAuthApiRouter.GET("/livestream/activities/:id", adminHandler.GetLivestreamActivity())
adminAuthApiRouter.PUT("/livestream/activities/:id", adminHandler.UpdateLivestreamActivity())
adminAuthApiRouter.DELETE("/livestream/activities/:id", adminHandler.DeleteLivestreamActivity())
adminAuthApiRouter.POST("/livestream/activities/:id/prizes", adminHandler.CreateLivestreamPrizes())
adminAuthApiRouter.GET("/livestream/activities/:id/prizes", adminHandler.ListLivestreamPrizes())
adminAuthApiRouter.DELETE("/livestream/prizes/:id", adminHandler.DeleteLivestreamPrize())
adminAuthApiRouter.GET("/livestream/activities/:id/draw_logs", adminHandler.ListLivestreamDrawLogs())
adminAuthApiRouter.GET("/livestream/activities/:id/stats", adminHandler.GetLivestreamStats())
adminAuthApiRouter.POST("/livestream/activities/:id/commitment/generate", adminHandler.GenerateLivestreamCommitment())
adminAuthApiRouter.GET("/livestream/activities/:id/commitment/summary", adminHandler.GetLivestreamCommitmentSummary())
// 系统配置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())
@ -243,6 +264,7 @@ 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.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())
adminAuthApiRouter.POST("/users/batch/points/add", intc.RequireAdminAction("user:points:batch:add"), adminHandler.BatchAddUserPoints()) adminAuthApiRouter.POST("/users/batch/points/add", intc.RequireAdminAction("user:points:batch:add"), adminHandler.BatchAddUserPoints())
@ -329,6 +351,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
adminAuthApiRouter.POST("/activities/:activity_id/commitment/generate", adminHandler.GenerateActivityCommitmentGeneral()) adminAuthApiRouter.POST("/activities/:activity_id/commitment/generate", adminHandler.GenerateActivityCommitmentGeneral())
adminAuthApiRouter.GET("/activities/:activity_id/commitment/summary", adminHandler.GetActivityCommitmentSummaryGeneral()) adminAuthApiRouter.GET("/activities/:activity_id/commitment/summary", adminHandler.GetActivityCommitmentSummaryGeneral())
adminAuthApiRouter.GET("/activities/:activity_id/credential", adminHandler.GetActivityCredential())
adminAuthApiRouter.POST("/pay/bills/import", adminHandler.ImportPaymentBill()) adminAuthApiRouter.POST("/pay/bills/import", adminHandler.ImportPaymentBill())
adminAuthApiRouter.GET("/pay/bills/diff", adminHandler.ListPaymentBillDiff()) adminAuthApiRouter.GET("/pay/bills/diff", adminHandler.ListPaymentBillDiff())
adminAuthApiRouter.GET("/pay/orders", intc.RequireAdminAction("order:view"), adminHandler.ListPayOrders()) adminAuthApiRouter.GET("/pay/orders", intc.RequireAdminAction("order:view"), adminHandler.ListPayOrders())
@ -403,6 +426,17 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
appPublicApiRouter.GET("/config/public", commonHandler.GetPublicConfig()) appPublicApiRouter.GET("/config/public", commonHandler.GetPublicConfig())
} }
// 公开接口路由组 (无需登录)
publicApiRouter := mux.Group("/api/public")
{
publicHandler := publicapi.New(logger, db, douyinSvc)
publicApiRouter.GET("/livestream/:access_code", publicHandler.GetLivestreamByAccessCode())
publicApiRouter.GET("/livestream/:access_code/winners", publicHandler.GetLivestreamWinners())
publicApiRouter.POST("/livestream/:access_code/draw", publicHandler.DrawLivestream())
publicApiRouter.POST("/livestream/:access_code/sync", publicHandler.SyncLivestreamOrders())
publicApiRouter.GET("/livestream/:access_code/pending-orders", publicHandler.GetLivestreamPendingOrders())
}
// APP 端认证接口路由组 // APP 端认证接口路由组
appAuthApiRouter := mux.Group("/api/app", core.WrapAuthHandler(intc.AppTokenAuthVerify)) appAuthApiRouter := mux.Group("/api/app", core.WrapAuthHandler(intc.AppTokenAuthVerify))
{ {

View File

@ -12,6 +12,7 @@ import (
"bindbox-game/internal/pkg/wechat" "bindbox-game/internal/pkg/wechat"
"bindbox-game/internal/repository/mysql/model" "bindbox-game/internal/repository/mysql/model"
strat "bindbox-game/internal/service/activity/strategy" strat "bindbox-game/internal/service/activity/strategy"
"bindbox-game/internal/service/sysconfig"
usersvc "bindbox-game/internal/service/user" usersvc "bindbox-game/internal/service/user"
"go.uber.org/zap" "go.uber.org/zap"
@ -169,7 +170,7 @@ func (s *service) ProcessOrderLottery(ctx context.Context, orderID int64) error
// 无限赏模式下使用总数检测因为inventory.RewardID=0 // 无限赏模式下使用总数检测因为inventory.RewardID=0
// 如果已发放总数已达到开奖数量,说明已完成发放,跳过后续逻辑 // 如果已发放总数已达到开奖数量,说明已完成发放,跳过后续逻辑
if invTotalCount >= dc { if invTotalCount >= dc {
s.logger.Info("奖励已全部发放,跳过重复发放", zap.Int64("order_id", orderID), zap.Int64("dc", dc), zap.Int64("invTotalCount", invTotalCount)) // s.logger.Info("奖励已全部发放,跳过重复发放", zap.Int64("order_id", orderID), zap.Int64("dc", dc), zap.Int64("invTotalCount", invTotalCount))
} else { } else {
for i := int64(0); i < dc; i++ { for i := int64(0); i < dc; i++ {
log, ok := logMap[i] log, ok := logMap[i]
@ -302,13 +303,20 @@ func (s *service) TriggerVirtualShipping(ctx context.Context, orderID int64, ord
s.logger.Error("[虚拟发货] 上传失败", zap.Error(errUpload), zap.String("order_no", orderNo)) s.logger.Error("[虚拟发货] 上传失败", zap.Error(errUpload), zap.String("order_no", orderNo))
} }
// 发送开奖通知 - 仅一番赏使用动态配置system_configs 表)
if playType == "ichiban" { if playType == "ichiban" {
notifyCfg := &notify.WechatNotifyConfig{ dc := sysconfig.GetDynamicConfig()
AppID: c.Wechat.AppID, if dc != nil {
AppSecret: c.Wechat.AppSecret, wxCfg := dc.GetWechat(ctx)
LotteryResultTemplateID: c.Wechat.LotteryResultTemplateID, notifyCfg := &notify.WechatNotifyConfig{
AppID: wxCfg.AppID,
AppSecret: wxCfg.AppSecret,
LotteryResultTemplateID: wxCfg.LotteryResultTemplateID,
}
if err := notify.SendLotteryResultNotification(ctx, notifyCfg, payerOpenid, actName, rewardNames, orderNo, time.Now()); err != nil {
s.logger.Error("[虚拟发货] 发送开奖通知失败", zap.Error(err), zap.String("order_no", orderNo))
}
} }
_ = notify.SendLotteryResultNotification(ctx, notifyCfg, payerOpenid, actName, rewardNames, orderNo, time.Now())
} }
} }

View File

@ -62,12 +62,6 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo, rdb *redis
_ = repo.GetDbR().Raw("SELECT id, play_type, draw_mode, min_participants, interval_minutes, scheduled_time, refund_coupon_id, last_settled_at FROM activities WHERE draw_mode='scheduled' AND (scheduled_time IS NOT NULL OR interval_minutes > 0)").Scan(&acts) _ = repo.GetDbR().Raw("SELECT id, play_type, draw_mode, min_participants, interval_minutes, scheduled_time, refund_coupon_id, last_settled_at FROM activities WHERE draw_mode='scheduled' AND (scheduled_time IS NOT NULL OR interval_minutes > 0)").Scan(&acts)
l.Debug("定时开奖: 查询到活动", zap.Int("count", len(acts))) l.Debug("定时开奖: 查询到活动", zap.Int("count", len(acts)))
for _, a := range acts { for _, a := range acts {
l.Debug("定时开奖: 检查活动",
zap.Int64("id", a.ID),
zap.String("play_type", a.PlayType),
zap.Int64("interval", a.IntervalMinutes),
zap.Reflect("scheduled_time", a.ScheduledTime),
zap.Reflect("last_settled", a.LastSettledAt))
// 计算开奖时间 // 计算开奖时间
st := time.Time{} st := time.Time{}
@ -99,12 +93,6 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo, rdb *redis
} }
} }
l.Debug("定时开奖: 计算开奖时间",
zap.Int64("id", a.ID),
zap.Time("st", st),
zap.Time("now", now),
zap.Bool("skip", st.IsZero() || now.Before(st)))
if st.IsZero() || now.Before(st) { if st.IsZero() || now.Before(st) {
continue continue
} }
@ -117,10 +105,6 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo, rdb *redis
} }
// 【修复】查询从 last 到 now 的所有订单(而非到 st确保能找到最新订单 // 【修复】查询从 last 到 now 的所有订单(而非到 st确保能找到最新订单
l.Debug("定时开奖: 查询订单范围",
zap.Int64("id", aid),
zap.Time("last", last),
zap.Time("now", now))
orders, _ := r.Orders.WithContext(ctx).ReadDB().Where( orders, _ := r.Orders.WithContext(ctx).ReadDB().Where(
r.Orders.Status.Eq(2), r.Orders.Status.Eq(2),
r.Orders.SourceType.Eq(2), r.Orders.SourceType.Eq(2),
@ -129,10 +113,6 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo, rdb *redis
r.Orders.CreatedAt.Gte(last), r.Orders.CreatedAt.Gte(last),
).Find() ).Find()
count := int64(len(orders)) count := int64(len(orders))
l.Debug("定时开奖: 查询到订单",
zap.Int64("id", aid),
zap.Int64("count", count),
zap.Int64("min", a.MinParticipants))
// Initialize Wechat Client if needed // Initialize Wechat Client if needed
wc, _ := paypkg.NewWechatPayClient(ctx) wc, _ := paypkg.NewWechatPayClient(ctx)
@ -171,11 +151,6 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo, rdb *redis
soldSlots, _ := r.IssuePositionClaims.WithContext(ctx).Where(r.IssuePositionClaims.IssueID.Eq(iss)).Count() soldSlots, _ := r.IssuePositionClaims.WithContext(ctx).Where(r.IssuePositionClaims.IssueID.Eq(iss)).Count()
l.Debug("定时开奖-一番赏: 检查售罄",
zap.Int64("issue_id", iss),
zap.Int64("sold", soldSlots),
zap.Int64("total", totalSlots))
if soldSlots < totalSlots { if soldSlots < totalSlots {
l.Info("定时开奖-一番赏: 未售罄,执行全额退款", zap.Int64("issue_id", iss)) l.Info("定时开奖-一番赏: 未售罄,执行全额退款", zap.Int64("issue_id", iss))
refundedIssues[iss] = true refundedIssues[iss] = true
@ -277,16 +252,13 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo, rdb *redis
} }
if shouldRefund { if shouldRefund {
l.Info("定时开奖: 人数不足,执行退款完毕", zap.Int64("id", aid))
for _, o := range orders { for _, o := range orders {
refundOrder(ctx, l, o, "scheduled_not_enough", wc, r, w, us, a.RefundCouponID) refundOrder(ctx, l, o, "scheduled_not_enough", wc, r, w, us, a.RefundCouponID)
} }
} else { } else {
l.Info("定时开奖: 人数满足,开始开奖处理", zap.Int64("id", aid))
for _, o := range orders { for _, o := range orders {
iss := remark.Parse(o.Remark).IssueID iss := remark.Parse(o.Remark).IssueID
if a.PlayType == "ichiban" && refundedIssues[iss] { if a.PlayType == "ichiban" && refundedIssues[iss] {
l.Debug("定时开奖-一番赏: 订单已退款,跳过开奖", zap.Int64("order_id", o.ID), zap.Int64("issue_id", iss))
continue continue
} }
if err := activitySvc.ProcessOrderLottery(ctx, o.ID); err != nil { if err := activitySvc.ProcessOrderLottery(ctx, o.ID); err != nil {
@ -301,16 +273,8 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo, rdb *redis
// 更新开奖时间戳 // 更新开奖时间戳
next := now.Add(time.Duration(a.IntervalMinutes) * time.Minute) next := now.Add(time.Duration(a.IntervalMinutes) * time.Minute)
nextVal = sql.NullTime{Time: next.UTC(), Valid: true} nextVal = sql.NullTime{Time: next.UTC(), Valid: true}
l.Info("定时开奖: 更新活动下次结算时间", _ = repo.GetDbW().WithContext(ctx).Exec("UPDATE activities SET last_settled_at=?, scheduled_time=? WHERE id= ?", now.UTC(), nextVal, aid).Error
zap.Int64("id", aid),
zap.Time("last", now),
zap.Time("next", next))
} else {
// 如果没有间隔,则不设置下次计划时间
nextVal = sql.NullTime{Valid: false}
l.Info("定时开奖: 活动无间隔,不设置下次计划时间", zap.Int64("id", aid), zap.Time("last", now))
} }
_ = repo.GetDbW().WithContext(ctx).Exec("UPDATE activities SET last_settled_at=?, scheduled_time=? WHERE id= ?", now.UTC(), nextVal, aid).Error
} }
// 即时开奖:处理所有已支付且未记录抽奖日志的订单 // 即时开奖:处理所有已支付且未记录抽奖日志的订单

View File

@ -18,6 +18,8 @@ import (
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
"bindbox-game/internal/service/user"
) )
// 系统配置键 // 系统配置键
@ -27,16 +29,24 @@ const (
) )
type Service interface { type Service interface {
// FetchAndSyncOrders 从抖店 API 获取订单并同步到本地 // FetchAndSyncOrders 从抖店 API 获取订单并同步到本地 (原有按用户同步逻辑)
FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error)
// SyncShopOrders 同步店铺全量订单 (专供直播间等全扫描场景)
SyncShopOrders(ctx context.Context, activityID int64) (*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 获取抖店配置
GetConfig(ctx context.Context) (*DouyinConfig, error) GetConfig(ctx context.Context) (*DouyinConfig, error)
// SaveConfig 保存抖店配置 // SaveConfig 保存抖店配置
SaveConfig(ctx context.Context, cookie string, intervalMinutes int) error SaveConfig(ctx context.Context, cookie string, intervalMinutes int) error
// SyncOrder 同步单个订单到本地可传入建议关联的用户ID // SyncOrder 同步单个订单到本地可传入建议关联的用户ID和商品ID
SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64) (isNew bool, isMatched bool) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64, productID string) (isNew bool, isMatched bool)
// GrantMinesweeperQualifications 自动补发扫雷资格
GrantMinesweeperQualifications(ctx context.Context) error
// GrantLivestreamPrizes 自动发放直播间奖品
GrantLivestreamPrizes(ctx context.Context) error
// SyncRefundStatus 同步退款状态
SyncRefundStatus(ctx context.Context) error
} }
type DouyinConfig struct { type DouyinConfig struct {
@ -45,9 +55,11 @@ type DouyinConfig struct {
} }
type SyncResult struct { type SyncResult struct {
TotalFetched int `json:"total_fetched"` TotalFetched int `json:"total_fetched"`
NewOrders int `json:"new_orders"` NewOrders int `json:"new_orders"`
MatchedUsers int `json:"matched_users"` MatchedUsers int `json:"matched_users"`
Orders []*model.DouyinOrders `json:"orders"` // 新增:返回详情以供后续处理
DebugInfo string `json:"debug_info"`
} }
type service struct { type service struct {
@ -57,9 +69,10 @@ type service struct {
writeDB *dao.Query writeDB *dao.Query
syscfg sysconfig.Service syscfg sysconfig.Service
ticketSvc game.TicketService ticketSvc game.TicketService
userSvc user.Service
} }
func New(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticketSvc game.TicketService) Service { func New(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticketSvc game.TicketService, userSvc user.Service) Service {
return &service{ return &service{
logger: l, logger: l,
repo: repo, repo: repo,
@ -67,6 +80,7 @@ func New(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticke
writeDB: dao.Use(repo.GetDbW()), writeDB: dao.Use(repo.GetDbW()),
syscfg: syscfg, syscfg: syscfg,
ticketSvc: ticketSvc, ticketSvc: ticketSvc,
userSvc: userSvc,
} }
} }
@ -160,7 +174,7 @@ func (s *service) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) {
// 3. 同步 // 3. 同步
for _, order := range orders { for _, order := range orders {
// 同步订单(传入建议关联的用户 ID // 同步订单(传入建议关联的用户 ID
isNew, matched := s.SyncOrder(ctx, &order, u.ID) isNew, matched := s.SyncOrder(ctx, &order, u.ID, "")
if isNew { if isNew {
result.NewOrders++ result.NewOrders++
} }
@ -170,12 +184,84 @@ func (s *service) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) {
} }
} }
s.logger.Info("[抖店同步] 全量同步完成", result.DebugInfo += fmt.Sprintf("\n同步完成: 总抓取 %d, 新订单 %d, 匹配用户 %d", result.TotalFetched, result.NewOrders, result.MatchedUsers)
zap.Int("users_count", len(users)), return result, nil
zap.Int("total_fetched", result.TotalFetched), }
zap.Int("new_orders", result.NewOrders),
zap.Int("matched_users", result.MatchedUsers), // 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 return result, nil
} }
@ -189,19 +275,33 @@ type douyinOrderResponse struct {
} }
type DouyinOrderItem struct { 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 string `json:"actual_receive_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"` // 抖店返回的商品数量
ProductItemList []DouyinProductItem `json:"product_item"` // 商品详情列表
SkuOrderList []SkuOrderItem `json:"sku_order_list"`
} }
// fetchDouyinOrdersByBuyer 调用抖店 API 按 Buyer ID 获取订单 type DouyinProductItem struct {
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
ComboNum int64 `json:"combo_num"`
TotalProductCount int64 `json:"total_product_count"`
}
type SkuOrderItem struct {
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
SkuID string `json:"sku_id"`
}
// fetchDouyinOrdersByBuyer 调用抖店 API 按 Buyer ID 获取订单 (保持向后兼容)
func (s *service) fetchDouyinOrdersByBuyer(cookie string, buyer string) ([]DouyinOrderItem, error) { func (s *service) fetchDouyinOrdersByBuyer(cookie string, buyer string) ([]DouyinOrderItem, error) {
// 拼接带有业务标识的搜索 URL
baseUrl := "https://fxg.jinritemai.com/api/order/searchlist"
params := url.Values{} params := url.Values{}
params.Set("page", "0") params.Set("page", "0")
params.Set("pageSize", "100") params.Set("pageSize", "100")
@ -213,6 +313,12 @@ func (s *service) fetchDouyinOrdersByBuyer(cookie string, buyer string) ([]Douyi
params.Set("_bid", "ffa_order") params.Set("_bid", "ffa_order")
params.Set("aid", "4272") params.Set("aid", "4272")
return s.fetchDouyinOrders(cookie, params)
}
// fetchDouyinOrders 通用的抖店订单抓取方法
func (s *service) fetchDouyinOrders(cookie string, params url.Values) ([]DouyinOrderItem, error) {
baseUrl := "https://fxg.jinritemai.com/api/order/searchlist"
fullUrl := baseUrl + "?" + params.Encode() fullUrl := baseUrl + "?" + params.Encode()
req, err := http.NewRequest("GET", fullUrl, nil) req, err := http.NewRequest("GET", fullUrl, nil)
@ -245,14 +351,14 @@ func (s *service) fetchDouyinOrdersByBuyer(cookie string, buyer string) ([]Douyi
} }
if respData.St != 0 && respData.Code != 0 { if respData.St != 0 && respData.Code != 0 {
return nil, fmt.Errorf("API 返回错误: %s", respData.Msg) return nil, fmt.Errorf("API 返回错误: %s (ST:%d CODE:%d)", respData.Msg, respData.St, respData.Code)
} }
return respData.Data, nil return respData.Data, nil
} }
// SyncOrder 同步单个订单到本地 // SyncOrder 同步单个订单到本地
func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64) (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)
var order model.DouyinOrders var order model.DouyinOrders
@ -293,10 +399,38 @@ func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestU
amount = int64(f * 100) amount = int64(f * 100)
} }
} }
// 计算商品数量:如果指定了 productID则只统计该商品的数量否则使用总数量
pCount := item.ProductCount
if productID != "" && len(item.ProductItemList) > 0 {
var matchedCount int64
for _, pi := range item.ProductItemList {
if pi.ProductID == productID {
// 有些情况下 TotalProductCount 准确,有些 ComboNum 准确
// 用户反馈的 JSON 中 ComboNum=2, TotalProductCount=2
// 优先使用 ComboNum
if pi.ComboNum > 0 {
matchedCount += pi.ComboNum
} else {
matchedCount += pi.TotalProductCount
}
}
}
if matchedCount > 0 {
pCount = matchedCount
}
}
// 如果没指定 productID但 iterate 发现只有一个商品,也可以尝试自动填补 productID (可选优化)
if productID == "" && len(item.ProductItemList) == 1 {
productID = item.ProductItemList[0].ProductID
}
rawData, _ := json.Marshal(item) rawData, _ := json.Marshal(item)
order = model.DouyinOrders{ order = model.DouyinOrders{
ShopOrderID: item.ShopOrderID, ShopOrderID: item.ShopOrderID,
DouyinProductID: productID, // 写入商品ID
ProductCount: pCount, // 写入计算后的商品数量
OrderStatus: int32(item.OrderStatus), OrderStatus: int32(item.OrderStatus),
DouyinUserID: item.UserID, DouyinUserID: item.UserID,
ActualReceiveAmount: amount, ActualReceiveAmount: amount,
@ -309,7 +443,6 @@ func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestU
} }
if err := db.Create(&order).Error; err != nil { if err := db.Create(&order).Error; err != nil {
s.logger.Error("[抖店同步] 创建订单失败", zap.String("shop_order_id", item.ShopOrderID), zap.Error(err))
return false, false return false, false
} }
} }

View File

@ -3,18 +3,22 @@ package douyin
import ( import (
"bindbox-game/internal/pkg/logger" "bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/model"
"bindbox-game/internal/service/game" "bindbox-game/internal/service/game"
"bindbox-game/internal/service/sysconfig" "bindbox-game/internal/service/sysconfig"
"context" "context"
"fmt"
"strconv" "strconv"
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
"bindbox-game/internal/service/user"
) )
// StartDouyinOrderSync 启动抖店订单定时同步任务 // StartDouyinOrderSync 启动抖店订单定时同步任务
func StartDouyinOrderSync(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticketSvc game.TicketService) { func StartDouyinOrderSync(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticketSvc game.TicketService, userSvc user.Service) {
svc := New(l, repo, syscfg, ticketSvc) svc := New(l, repo, syscfg, ticketSvc, userSvc)
go func() { go func() {
// 初始等待30秒让服务完全启动 // 初始等待30秒让服务完全启动
@ -39,19 +43,74 @@ func StartDouyinOrderSync(l logger.CustomLogger, repo mysql.Repo, syscfg sysconf
continue continue
} }
// 执行同步
l.Info("[抖店定时同步] 开始同步", zap.Int("interval_minutes", intervalMinutes)) l.Info("[抖店定时同步] 开始同步", zap.Int("interval_minutes", intervalMinutes))
// ========== 优先:按用户同步 (Only valid users) ==========
// “优先遍历:代码先查 users 表中所有已绑定抖音的用户。 然后根据抖音id 去请求抖音的订单接口拿数据”
result, err := svc.FetchAndSyncOrders(ctx) result, err := svc.FetchAndSyncOrders(ctx)
if err != nil { if err != nil {
l.Error("[抖店定时同步] 同步失败", zap.Error(err)) l.Error("[抖店定时同步] 用户订单同步失败", zap.Error(err))
} else { } else {
l.Info("[抖店定时同步] 同步成功", l.Info("[抖店定时同步] 用户订单同步成功",
zap.Int("total_fetched", result.TotalFetched), zap.Int("total_fetched", result.TotalFetched),
zap.Int("new_orders", result.NewOrders), zap.Int("new_orders", result.NewOrders),
zap.Int("matched_users", result.MatchedUsers), zap.Int("matched_users", result.MatchedUsers),
) )
} }
// ========== 自动补发扫雷游戏资格 (针对刚才同步到的订单) ==========
if err := svc.GrantMinesweeperQualifications(ctx); err != nil {
l.Error("[定时补发] 补发扫雷资格失败", zap.Error(err))
}
// ========== 自动发放直播间奖品 ==========
if err := svc.GrantLivestreamPrizes(ctx); err != nil {
l.Error("[定时发放] 发放直播奖品失败", zap.Error(err))
}
// ========== 后置按活动商品ID同步 (全量兜底) ==========
var activities []model.LivestreamActivities
if err := repo.GetDbR().Where("status = ?", 1).Find(&activities).Error; err == nil && len(activities) > 0 {
l.Info("[抖店定时同步] 发现进行中的直播活动 (全量兜底)", zap.Int("count", len(activities)))
for _, act := range activities {
if act.DouyinProductID == "" {
continue // 跳过未配置商品ID的活动
}
// SyncShopOrders 会拉取所有订单,如果之前 UserSync 没拉到的(比如未绑定的用户下单),这里可以拉到
// 并在之后用户绑定时由 GrantMinesweeperQualifications 的关联逻辑进行补救
result, err := svc.SyncShopOrders(ctx, act.ID)
if err != nil {
l.Error("[抖店定时同步] 活动同步失败",
zap.Int64("activity_id", act.ID),
zap.String("product_id", act.DouyinProductID),
zap.Error(err),
)
} else {
l.Info("[抖店定时同步] 活动同步成功",
zap.Int64("activity_id", act.ID),
zap.String("product_id", act.DouyinProductID),
zap.Int("total_fetched", result.TotalFetched),
zap.Int("new_orders", result.NewOrders),
)
}
}
}
// ========== 新增:自动补发扫雷游戏资格 ==========
if err := svc.GrantMinesweeperQualifications(ctx); err != nil {
l.Error("[定时补发] 补发扫雷资格失败", zap.Error(err))
}
// ========== 新增:自动发放直播间奖品 ==========
if err := svc.GrantLivestreamPrizes(ctx); err != nil {
l.Error("[定时发放] 发放直播奖品失败", zap.Error(err))
}
// ========== 新增:同步退款状态 ==========
if err := svc.SyncRefundStatus(ctx); err != nil {
l.Error("[定时同步] 同步退款状态失败", zap.Error(err))
}
// 等待下次同步 // 等待下次同步
time.Sleep(time.Duration(intervalMinutes) * time.Minute) time.Sleep(time.Duration(intervalMinutes) * time.Minute)
} }
@ -59,3 +118,252 @@ func StartDouyinOrderSync(l logger.CustomLogger, repo mysql.Repo, syscfg sysconf
l.Info("[抖店定时同步] 定时任务已启动") l.Info("[抖店定时同步] 定时任务已启动")
} }
// GrantMinesweeperQualifications 自动补发扫雷资格
// 逻辑:遍历已绑定抖音的用户 -> 查找其未归属的订单 -> 关联订单 -> 补发资格
func (s *service) GrantMinesweeperQualifications(ctx context.Context) error {
db := s.repo.GetDbW().WithContext(ctx)
// 1. 查找所有已绑定抖音的用户
var users []model.Users
if err := s.repo.GetDbR().Where("douyin_user_id != '' AND douyin_user_id IS NOT NULL").Find(&users).Error; err != nil {
return err
}
for _, u := range users {
// 2. 查找该抖音ID下未关联(local_user_id=0 or empty)的订单
var orders []model.DouyinOrders
if err := db.Where("douyin_user_id = ? AND (local_user_id = '' OR local_user_id = '0')", u.DouyinUserID).Find(&orders).Error; err != nil {
continue
}
for _, order := range orders {
// 3. 关联订单到用户
if err := db.Model(&order).Update("local_user_id", strconv.FormatInt(u.ID, 10)).Error; err != nil {
s.logger.Error("[自动补发] 关联订单失败", zap.String("order_id", order.ShopOrderID), zap.Error(err))
continue
}
// 4. 如果是已完成的订单(5),且未发奖,则补发
if order.OrderStatus == 5 && order.RewardGranted == 0 {
orderID := order.ID
s.logger.Info("[自动补发] 开始补发扫雷资格", zap.Int64("user_id", u.ID), zap.String("shop_order_id", order.ShopOrderID))
// 调用发奖服务
count := int64(1)
if order.ProductCount > 0 {
count = order.ProductCount
}
s.logger.Info("[自动补发] 发放数量", zap.Int64("count", count))
if err := s.ticketSvc.GrantTicket(ctx, u.ID, "minesweeper", int(count), "douyin_order", orderID, "定时任务补发"); err == nil {
db.Model(&order).Update("reward_granted", int32(count))
s.logger.Info("[自动补发] 补发成功", zap.String("shop_order_id", order.ShopOrderID))
} else {
s.logger.Error("[自动补发] 补发失败", zap.Error(err))
}
}
}
}
return nil
}
// GrantLivestreamPrizes 自动发放直播间奖品
// 逻辑:扫描 livestream_draw_logs 中 is_granted=0 的记录 -> 找到对应 ProductID -> 发放商品
func (s *service) GrantLivestreamPrizes(ctx context.Context) error {
db := s.repo.GetDbW().WithContext(ctx)
// 1. 查找未发放的记录
var logs []model.LivestreamDrawLogs
if err := db.Where("is_granted = 0").Find(&logs).Error; err != nil {
return err
}
for _, log := range logs {
// 必须要有对应的本地用户ID
if log.LocalUserID == 0 {
// 尝试从 douyin_orders 补全 user_id
var order model.DouyinOrders
if err := s.repo.GetDbR().Where("shop_order_id = ?", log.ShopOrderID).First(&order).Error; err == nil {
if uid, _ := strconv.ParseInt(order.LocalUserID, 10, 64); uid > 0 {
log.LocalUserID = uid
db.Model(&log).Update("local_user_id", uid)
}
}
}
if log.LocalUserID == 0 {
continue // 还没关联到用户,跳过
}
// 2. 查奖品关联的 ProductID
var prize model.LivestreamPrizes
if err := s.repo.GetDbR().Where("id = ?", log.PrizeID).First(&prize).Error; err != nil {
s.logger.Error("[自动发放] 奖品不存在", zap.Int64("prize_id", log.PrizeID))
continue
}
if prize.ProductID == 0 {
s.logger.Warn("[自动发放] 奖品未关联商品ID跳过", zap.Int64("prize_id", log.PrizeID), zap.String("name", prize.Name))
continue
}
// 3. 发放商品 (使用 GrantReward 创建新订单发放)
sourceType := int32(5) // 5 代表直播间
req := user.GrantRewardRequest{
ProductID: prize.ProductID,
Quantity: 1,
SourceType: &sourceType,
Remark: fmt.Sprintf("直播间抽奖: %s (关联抖店订单: %s)", log.PrizeName, log.ShopOrderID),
}
s.logger.Info("[自动发放] 开始发放直播商品",
zap.Int64("user_id", log.LocalUserID),
zap.Int64("product_id", prize.ProductID),
zap.String("prize", log.PrizeName),
)
_, err := s.userSvc.GrantReward(ctx, log.LocalUserID, req)
if err != nil {
s.logger.Error("[自动发放] 发放失败", zap.Error(err))
// 如果发放失败是库存原因等,可能需要告警。暂时不重试,等下个周期。
} else {
// 4. 更新发放状态
db.Model(&log).Update("is_granted", 1)
s.logger.Info("[自动发放] 发放成功", zap.Int64("log_id", log.ID))
}
}
return nil
}
// SyncRefundStatus 同步退款状态
// 逻辑:检查 douyin_orders 的状态变更,如果订单已退款,则标记对应的 livestream_draw_logs
func (s *service) SyncRefundStatus(ctx context.Context) error {
db := s.repo.GetDbW().WithContext(ctx)
// 1. 查找所有关联直播抽奖但尚未标记退款的记录
var logs []model.LivestreamDrawLogs
if err := db.Where("is_refunded = 0 AND shop_order_id != ''").Find(&logs).Error; err != nil {
return err
}
refundedCount := 0
for _, log := range logs {
// 2. 查找对应的抖店订单
var order model.DouyinOrders
if err := s.repo.GetDbR().Where("shop_order_id = ?", log.ShopOrderID).First(&order).Error; err != nil {
continue // 找不到订单,跳过
}
// 3. 检查订单状态:抖店状态 4=已关闭 (包含退款/取消等关闭情况)
// 状态说明: 3=已发货, 4=已关闭, 5=已完成
if order.OrderStatus == 4 {
db.Model(&log).Update("is_refunded", 1)
refundedCount++
s.logger.Info("[退款同步] 标记退款记录",
zap.Int64("draw_log_id", log.ID),
zap.String("shop_order_id", log.ShopOrderID),
zap.Int32("order_status", order.OrderStatus),
)
// 4. 如果用户已关联,回收资产
if log.LocalUserID > 0 {
s.reclaimLivestreamAssets(ctx, &log)
}
}
}
if refundedCount > 0 {
s.logger.Info("[退款同步] 本次同步完成", zap.Int("refunded_count", refundedCount))
}
return nil
}
// reclaimLivestreamAssets 回收直播间发放的资产
// 逻辑:查找该用户通过此抽奖获得的 user_inventory作废或扣除积分
func (s *service) reclaimLivestreamAssets(ctx context.Context, log *model.LivestreamDrawLogs) {
db := s.repo.GetDbW().WithContext(ctx)
// 1. 查找关联的 user_inventory 记录
// 直播间奖品是通过 GrantReward 发放的,会创建一个新的本地订单
// 我们需要通过 remark 或其他方式找到关联的 inventory
// 由于 GrantReward 会在 remark 中记录 shop_order_id我们通过这个来查找
var inventories []model.UserInventory
// 查找用户持有的、来自直播间的资产(通过 remark 包含 shop_order_id 来关联)
searchPattern := "%" + log.ShopOrderID + "%"
if err := db.Where("user_id = ? AND status IN (1, 3) AND remark LIKE ?", log.LocalUserID, searchPattern).Find(&inventories).Error; err != nil {
s.logger.Error("[资产回收] 查询资产失败", zap.Error(err), zap.Int64("user_id", log.LocalUserID))
return
}
if len(inventories) == 0 {
// 尝试通过 prize_id 和 product_id 关联查找
var prize model.LivestreamPrizes
if err := s.repo.GetDbR().Where("id = ?", log.PrizeID).First(&prize).Error; err == nil && prize.ProductID > 0 {
// 查找该用户最近获得的该商品的资产(时间相近)
db.Where("user_id = ? AND product_id = ? AND status IN (1, 3) AND created_at >= ?",
log.LocalUserID, prize.ProductID, log.CreatedAt.Add(-time.Hour)).
Order("created_at DESC").Limit(1).Find(&inventories)
}
}
if len(inventories) == 0 {
s.logger.Warn("[资产回收] 未找到可回收资产",
zap.Int64("user_id", log.LocalUserID),
zap.String("shop_order_id", log.ShopOrderID),
)
return
}
// 2. 回收资产
for _, inv := range inventories {
if inv.Status == 1 {
// 状态1持有作废
db.Model(&inv).Updates(map[string]any{
"status": 2,
"remark": inv.Remark + "|refund_reclaimed",
})
s.logger.Info("[资产回收] 作废持有资产",
zap.Int64("inventory_id", inv.ID),
zap.Int64("user_id", inv.UserID),
)
} else if inv.Status == 3 {
// 状态3已兑换/发货):扣除积分
// 查找商品价格作为积分扣除依据
var product model.Products
if err := s.repo.GetDbR().Where("id = ?", inv.ProductID).First(&product).Error; err == nil {
pointsToDeduct := product.Price / 100 // 分转换为积分(假设 1积分=1分钱
if pointsToDeduct > 0 {
_, consumed, err := s.userSvc.ConsumePointsForRefund(ctx, inv.UserID, pointsToDeduct, "user_inventory", fmt.Sprintf("%d", inv.ID), "直播退款回收已兑换资产")
if err != nil {
s.logger.Error("[资产回收] 扣除积分失败", zap.Error(err), zap.Int64("user_id", inv.UserID))
}
if consumed < pointsToDeduct {
// 积分不足,标记用户
s.logger.Warn("[资产回收] 用户积分不足",
zap.Int64("user_id", inv.UserID),
zap.Int64("needed", pointsToDeduct),
zap.Int64("consumed", consumed),
)
// 可选:加入黑名单
// db.Exec("UPDATE users SET status = 3 WHERE id = ?", inv.UserID)
}
}
}
// 作废记录
db.Model(&inv).Updates(map[string]any{
"status": 2,
"remark": inv.Remark + "|refund_reclaimed_points_deducted",
})
s.logger.Info("[资产回收] 扣除积分并作废",
zap.Int64("inventory_id", inv.ID),
zap.Int64("user_id", inv.UserID),
)
}
}
// 3. 恢复奖品库存
db.Exec("UPDATE livestream_prizes SET remaining = remaining + 1 WHERE id = ? AND remaining >= 0", log.PrizeID)
s.logger.Info("[资产回收] 恢复奖品库存", zap.Int64("prize_id", log.PrizeID))
}

View File

@ -0,0 +1,584 @@
package livestream
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/big"
"time"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/model"
"go.uber.org/zap"
"gorm.io/gorm"
)
// Service 直播间游戏服务接口
type Service interface {
// CreateActivity 创建直播间活动
CreateActivity(ctx context.Context, input CreateActivityInput) (*model.LivestreamActivities, error)
// UpdateActivity 更新活动
UpdateActivity(ctx context.Context, id int64, input UpdateActivityInput) error
// GetActivity 获取活动详情
GetActivity(ctx context.Context, id int64) (*model.LivestreamActivities, error)
// GetActivityByAccessCode 根据访问码获取活动
GetActivityByAccessCode(ctx context.Context, code string) (*model.LivestreamActivities, error)
// ListActivities 活动列表
ListActivities(ctx context.Context, page, pageSize int, status *int32) ([]*model.LivestreamActivities, int64, error)
// DeleteActivity 删除活动
DeleteActivity(ctx context.Context, id int64) error
// CreatePrizes 批量创建奖品
CreatePrizes(ctx context.Context, activityID int64, prizes []CreatePrizeInput) error
// ListPrizes 获取活动奖品列表
ListPrizes(ctx context.Context, activityID int64) ([]*model.LivestreamPrizes, error)
// UpdatePrize 更新奖品
UpdatePrize(ctx context.Context, prizeID int64, input UpdatePrizeInput) error
// DeletePrize 删除奖品
DeletePrize(ctx context.Context, prizeID int64) error
// Draw 执行抽奖
Draw(ctx context.Context, input DrawInput) (*DrawResult, error)
// ListDrawLogs 获取中奖记录
ListDrawLogs(ctx context.Context, activityID int64, page, pageSize int, startTime, endTime *time.Time) ([]*model.LivestreamDrawLogs, int64, error)
// GetActivityByProductID 根据抖店商品ID获取活动
GetActivityByProductID(ctx context.Context, productID string) (*model.LivestreamActivities, error)
// GenerateCommitment 为活动生成承诺种子
GenerateCommitment(ctx context.Context, activityID int64) (int32, error)
// GetCommitmentSummary 获取活动承诺摘要
GetCommitmentSummary(ctx context.Context, activityID int64) (*CommitmentSummary, error)
}
type service struct {
logger logger.CustomLogger
repo mysql.Repo
}
// New 创建直播间服务
func New(l logger.CustomLogger, repo mysql.Repo) Service {
return &service{
logger: l,
repo: repo,
}
}
// ========== Input/Output 结构体 ==========
type CreateActivityInput struct {
Name string
StreamerName string
StreamerContact string
DouyinProductID string
TicketPrice int64
StartTime *time.Time
EndTime *time.Time
}
type UpdateActivityInput struct {
Name string
StreamerName string
StreamerContact string
DouyinProductID string
TicketPrice *int64
Status *int32
StartTime *time.Time
EndTime *time.Time
}
type CreatePrizeInput struct {
Name string
Image string
Weight int32
Quantity int32
Level int32
ProductID int64 `json:"product_id"`
CostPrice int64 `json:"cost_price"`
}
type UpdatePrizeInput struct {
ID int64 `json:"id"`
Name string `json:"name"`
Level int32 `json:"level"`
Weight int32 `json:"weight"`
Quantity int32 `json:"quantity"`
Image string `json:"image"`
ProductID int64 `json:"product_id"`
CostPrice int64 `json:"cost_price"`
}
type DrawInput struct {
ActivityID int64
DouyinOrderID int64
ShopOrderID string
LocalUserID int64
DouyinUserID string
UserNickname string
}
type DrawResult struct {
Prize *model.LivestreamPrizes
DrawLog *model.LivestreamDrawLogs
SeedHash string
Receipt *DrawReceipt
}
// CommitmentSummary 承诺摘要
type CommitmentSummary struct {
SeedVersion int32 `json:"seed_version"`
Algo string `json:"algo"`
HasSeed bool `json:"has_seed"`
LenSeed int `json:"len_seed_master"`
LenHash int `json:"len_seed_hash"`
}
// DrawReceipt 抽奖凭证
type DrawReceipt struct {
SeedVersion int32 `json:"seed_version"`
Timestamp int64 `json:"timestamp"`
Nonce int64 `json:"nonce"`
Signature string `json:"signature"`
Algorithm string `json:"algorithm"`
}
// ========== 活动管理 ==========
func (s *service) CreateActivity(ctx context.Context, input CreateActivityInput) (*model.LivestreamActivities, error) {
// 生成唯一访问码
accessCode := generateAccessCode()
activity := &model.LivestreamActivities{
Name: input.Name,
StreamerName: input.StreamerName,
StreamerContact: input.StreamerContact,
AccessCode: accessCode,
DouyinProductID: input.DouyinProductID,
TicketPrice: input.TicketPrice,
Status: 1,
}
// 构建要插入的字段列表,排除空的时间字段
columns := []string{"name", "streamer_name", "streamer_contact", "access_code", "douyin_product_id", "ticket_price", "status"}
if input.StartTime != nil {
activity.StartTime = *input.StartTime
columns = append(columns, "start_time")
}
if input.EndTime != nil {
activity.EndTime = *input.EndTime
columns = append(columns, "end_time")
}
if err := s.repo.GetDbW().WithContext(ctx).Select(columns).Create(activity).Error; err != nil {
return nil, fmt.Errorf("创建直播间活动失败: %w", err)
}
return activity, nil
}
func (s *service) UpdateActivity(ctx context.Context, id int64, input UpdateActivityInput) error {
updates := make(map[string]any)
if input.Name != "" {
updates["name"] = input.Name
}
if input.StreamerName != "" {
updates["streamer_name"] = input.StreamerName
}
if input.StreamerContact != "" {
updates["streamer_contact"] = input.StreamerContact
}
if input.DouyinProductID != "" {
updates["douyin_product_id"] = input.DouyinProductID
}
if input.TicketPrice != nil {
updates["ticket_price"] = *input.TicketPrice
}
if input.Status != nil {
updates["status"] = *input.Status
}
if input.StartTime != nil {
updates["start_time"] = *input.StartTime
}
if input.EndTime != nil {
updates["end_time"] = *input.EndTime
}
if len(updates) == 0 {
return nil
}
return s.repo.GetDbW().WithContext(ctx).Model(&model.LivestreamActivities{}).
Where("id = ?", id).Updates(updates).Error
}
func (s *service) GetActivity(ctx context.Context, id int64) (*model.LivestreamActivities, error) {
var activity model.LivestreamActivities
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", id).First(&activity).Error; err != nil {
return nil, err
}
return &activity, nil
}
func (s *service) GetActivityByAccessCode(ctx context.Context, code string) (*model.LivestreamActivities, error) {
var activity model.LivestreamActivities
if err := s.repo.GetDbR().WithContext(ctx).Where("access_code = ? AND deleted_at IS NULL", code).First(&activity).Error; err != nil {
return nil, err
}
return &activity, nil
}
func (s *service) GetActivityByProductID(ctx context.Context, productID string) (*model.LivestreamActivities, error) {
var activity model.LivestreamActivities
if err := s.repo.GetDbR().WithContext(ctx).
Where("douyin_product_id = ? AND status = 1 AND deleted_at IS NULL", productID).
First(&activity).Error; err != nil {
return nil, err
}
return &activity, nil
}
func (s *service) ListActivities(ctx context.Context, page, pageSize int, status *int32) ([]*model.LivestreamActivities, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
db := s.repo.GetDbR().WithContext(ctx).Model(&model.LivestreamActivities{}).Where("deleted_at IS NULL")
if status != nil {
db = db.Where("status = ?", *status)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var list []*model.LivestreamActivities
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
func (s *service) DeleteActivity(ctx context.Context, id int64) error {
return s.repo.GetDbW().WithContext(ctx).Delete(&model.LivestreamActivities{}, id).Error
}
// ========== 奖品管理 ==========
func (s *service) CreatePrizes(ctx context.Context, activityID int64, prizes []CreatePrizeInput) error {
if len(prizes) == 0 {
return nil
}
var models []*model.LivestreamPrizes
for _, p := range prizes {
remaining := p.Quantity
if remaining < 0 {
remaining = -1
}
models = append(models, &model.LivestreamPrizes{
ActivityID: activityID,
Name: p.Name,
Image: p.Image,
Weight: p.Weight,
Quantity: p.Quantity,
Remaining: remaining,
Level: p.Level,
ProductID: p.ProductID,
CostPrice: p.CostPrice,
Sort: 0, // Default sort value as it's removed from input
})
}
return s.repo.GetDbW().WithContext(ctx).Create(&models).Error
}
func (s *service) ListPrizes(ctx context.Context, activityID int64) ([]*model.LivestreamPrizes, error) {
var list []*model.LivestreamPrizes
if err := s.repo.GetDbR().WithContext(ctx).
Where("activity_id = ?", activityID).
Order("sort ASC, id ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (s *service) UpdatePrize(ctx context.Context, prizeID int64, input UpdatePrizeInput) error {
updates := make(map[string]any)
if input.Name != "" {
updates["name"] = input.Name
}
if input.Image != "" {
updates["image"] = input.Image
}
if input.Weight > 0 {
updates["weight"] = input.Weight
}
if input.Quantity >= 0 { // Allow 0 quantity update? Assuming yes. But 0 is zero-value. If user wants to set 0, logic needs distinction. Assuming Partial Update with Value Types is tricky. For now check > -1 if possible or just non-zero if quantity can't be 0? Usually quantity update is explicit.
// Since struct is Value type, we can't distinguish 0 from "not set".
// User changed to Value type likely assuming "replace" or front-end sends all fields.
// However, standard Update usually implies partial.
// Let's assume > 0 for now or if we treat input as full replacement?
// Given `UpdatePrizeInput` structure, it likely carries the full desired state or partial.
// If partial, int fields are problematic.
// Let's use > 0 for non-zero values, assuming 0 isn't a valid "change to" target for weight/level/quantity unless explicit.
// Actually, quantity can be 0 (sold out). But if client sends 0 as "no change", we have bug.
// Since user changed specific fields, they might be relying on frontend logic.
// I will assume standard non-zero checks for strings and >0 or specific logic for ints.
// If strictly following "compilation fix", I replacing nil checks with value checks.
updates["quantity"] = input.Quantity
updates["remaining"] = input.Quantity
}
if input.Level > 0 {
updates["level"] = input.Level
}
if input.ProductID > 0 {
updates["product_id"] = input.ProductID
}
if input.CostPrice > 0 { // Assume cost price is positive? Or allow 0? Updates map approach usually omits if 0.
updates["cost_price"] = input.CostPrice
}
// CostPrice and Sort removed from Input, so skip updates[cost_price] and updates[sort] logic completely.
if len(updates) == 0 {
return nil
}
return s.repo.GetDbW().WithContext(ctx).Model(&model.LivestreamPrizes{}).
Where("id = ?", prizeID).Updates(updates).Error
}
func (s *service) DeletePrize(ctx context.Context, prizeID int64) error {
return s.repo.GetDbW().WithContext(ctx).Delete(&model.LivestreamPrizes{}, prizeID).Error
}
// ========== 抽奖逻辑 ==========
func (s *service) Draw(ctx context.Context, input DrawInput) (*DrawResult, error) {
// 1. 获取可用奖品
prizes, err := s.ListPrizes(ctx, input.ActivityID)
if err != nil {
return nil, fmt.Errorf("获取奖品列表失败: %w", err)
}
// 2. 过滤有库存的奖品
var availablePrizes []*model.LivestreamPrizes
var totalWeight int64
for _, p := range prizes {
if p.Remaining != 0 { // -1 表示无限
availablePrizes = append(availablePrizes, p)
totalWeight += int64(p.Weight)
}
}
if len(availablePrizes) == 0 {
return nil, fmt.Errorf("没有可用奖品")
}
// 3. 生成随机种子
seedBytes := make([]byte, 32)
if _, err := rand.Read(seedBytes); err != nil {
return nil, fmt.Errorf("生成随机种子失败: %w", err)
}
seedHash := sha256.Sum256(seedBytes)
seedHex := hex.EncodeToString(seedHash[:])
// 4. 计算随机值
randBig, err := rand.Int(rand.Reader, big.NewInt(totalWeight))
if err != nil {
return nil, fmt.Errorf("生成随机数失败: %w", err)
}
randValue := randBig.Int64()
// 5. 按权重选择奖品
var selectedPrize *model.LivestreamPrizes
var cumulative int64
for _, p := range availablePrizes {
cumulative += int64(p.Weight)
if randValue < cumulative {
selectedPrize = p
break
}
}
if selectedPrize == nil {
selectedPrize = availablePrizes[len(availablePrizes)-1]
}
// 6. 事务:扣减库存 + 记录中奖
var drawLog *model.LivestreamDrawLogs
err = s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 扣减库存(仅当 remaining > 0 时)
if selectedPrize.Remaining > 0 {
result := tx.Model(&model.LivestreamPrizes{}).
Where("id = ? AND remaining > 0", selectedPrize.ID).
Update("remaining", gorm.Expr("remaining - 1"))
if result.RowsAffected == 0 {
return fmt.Errorf("库存不足")
}
}
// 记录中奖
drawLog = &model.LivestreamDrawLogs{
ActivityID: input.ActivityID,
PrizeID: selectedPrize.ID,
DouyinOrderID: input.DouyinOrderID,
ShopOrderID: input.ShopOrderID,
LocalUserID: input.LocalUserID,
DouyinUserID: input.DouyinUserID,
UserNickname: input.UserNickname,
PrizeName: selectedPrize.Name,
Level: selectedPrize.Level,
SeedHash: seedHex,
RandValue: randValue,
WeightsTotal: totalWeight,
}
return tx.Create(drawLog).Error
})
if err != nil {
s.logger.Error("直播间抽奖失败", zap.Error(err), zap.Int64("activity_id", input.ActivityID))
return nil, err
}
s.logger.Info("直播间抽奖成功",
zap.Int64("activity_id", input.ActivityID),
zap.Int64("prize_id", selectedPrize.ID),
zap.String("prize_name", selectedPrize.Name),
)
// 7. 生成可验证凭证
var receipt *DrawReceipt
var activity model.LivestreamActivities
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", input.ActivityID).First(&activity).Error; err == nil {
if len(activity.CommitmentSeedMaster) > 0 {
ts := time.Now().UnixMilli()
nonce := time.Now().UnixNano()
// 构建签名载荷
payload := fmt.Sprintf("activity:%d|order:%s|draw:%d|ts:%d|nonce:%d",
input.ActivityID, input.ShopOrderID, drawLog.ID, ts, nonce)
// HMAC-SHA256 签名
mac := hmac.New(sha256.New, activity.CommitmentSeedMaster)
mac.Write([]byte(payload))
sig := hex.EncodeToString(mac.Sum(nil))
receipt = &DrawReceipt{
SeedVersion: activity.CommitmentStateVersion,
Timestamp: ts,
Nonce: nonce,
Signature: sig,
Algorithm: "HMAC-SHA256",
}
}
}
return &DrawResult{
Prize: selectedPrize,
DrawLog: drawLog,
SeedHash: seedHex,
Receipt: receipt,
}, nil
}
func (s *service) ListDrawLogs(ctx context.Context, activityID int64, page, pageSize int, startTime, endTime *time.Time) ([]*model.LivestreamDrawLogs, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
db := s.repo.GetDbR().WithContext(ctx).Model(&model.LivestreamDrawLogs{}).Where("activity_id = ?", activityID)
if startTime != nil {
db = db.Where("created_at >= ?", startTime)
}
if endTime != nil {
db = db.Where("created_at <= ?", endTime)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var list []*model.LivestreamDrawLogs
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
// ========== 承诺管理 ==========
// GenerateCommitment 为活动生成承诺种子
func (s *service) GenerateCommitment(ctx context.Context, activityID int64) (int32, error) {
// 获取当前版本号
var activity model.LivestreamActivities
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", activityID).First(&activity).Error; err != nil {
return 0, fmt.Errorf("活动不存在: %w", err)
}
// 生成 32 字节随机种子
seed := make([]byte, 32)
if _, err := rand.Read(seed); err != nil {
return 0, fmt.Errorf("生成随机种子失败: %w", err)
}
// 计算 SHA256 哈希
seedHash := sha256.Sum256(seed)
// 更新数据库
newVersion := activity.CommitmentStateVersion + 1
if err := s.repo.GetDbW().WithContext(ctx).Model(&model.LivestreamActivities{}).
Where("id = ?", activityID).
Updates(map[string]any{
"commitment_algo": "commit-v1",
"commitment_seed_master": seed,
"commitment_seed_hash": seedHash[:],
"commitment_state_version": newVersion,
}).Error; err != nil {
return 0, fmt.Errorf("更新承诺失败: %w", err)
}
s.logger.Info("直播间活动承诺已生成",
zap.Int64("activity_id", activityID),
zap.Int32("version", newVersion),
)
return newVersion, nil
}
// GetCommitmentSummary 获取活动承诺摘要
func (s *service) GetCommitmentSummary(ctx context.Context, activityID int64) (*CommitmentSummary, error) {
var activity model.LivestreamActivities
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", activityID).First(&activity).Error; err != nil {
return nil, fmt.Errorf("活动不存在: %w", err)
}
return &CommitmentSummary{
SeedVersion: activity.CommitmentStateVersion,
Algo: activity.CommitmentAlgo,
HasSeed: len(activity.CommitmentSeedMaster) > 0,
LenSeed: len(activity.CommitmentSeedMaster),
LenHash: len(activity.CommitmentSeedHash),
}, nil
}
// ========== 辅助函数 ==========
func generateAccessCode() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}

View File

@ -6,11 +6,14 @@ import (
"bindbox-game/internal/pkg/logger" "bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql"
"context" "context"
"errors"
"fmt"
"strings" "strings"
"sync" "sync"
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm"
) )
// 敏感配置 Key 后缀列表,这些 Key 的值需要加密存储 // 敏感配置 Key 后缀列表,这些 Key 的值需要加密存储
@ -62,6 +65,19 @@ const (
KeyDouyinPaySalt = "douyin.pay_salt" KeyDouyinPaySalt = "douyin.pay_salt"
) )
// SystemConfig local model for raw GORM access
type SystemConfig struct {
ID int64 `gorm:"primaryKey"`
ConfigKey string `gorm:"uniqueIndex"`
ConfigValue string
Remark string
DeletedAt gorm.DeletedAt
}
func (SystemConfig) TableName() string {
return "system_configs"
}
// COSConfig COS 配置结构 // COSConfig COS 配置结构
type COSConfig struct { type COSConfig struct {
Bucket string Bucket string
@ -110,7 +126,7 @@ type DouyinConfig struct {
// DynamicConfig 动态配置服务 // DynamicConfig 动态配置服务
type DynamicConfig struct { type DynamicConfig struct {
cache sync.Map // key -> string value cache sync.Map // key -> string value
repo Service db *gorm.DB
logger logger.CustomLogger logger logger.CustomLogger
encKey string // 加密密钥 (32字节用于AES-256) encKey string // 加密密钥 (32字节用于AES-256)
ttl time.Duration // 缓存过期时间 ttl time.Duration // 缓存过期时间
@ -131,7 +147,7 @@ func NewDynamicConfig(l logger.CustomLogger, db mysql.Repo) *DynamicConfig {
} }
return &DynamicConfig{ return &DynamicConfig{
repo: New(l, db), db: db.GetDbR(),
logger: l, logger: l,
encKey: encKey, encKey: encKey,
ttl: 5 * time.Minute, ttl: 5 * time.Minute,
@ -160,8 +176,9 @@ func (d *DynamicConfig) decryptValue(value string) (string, error) {
// LoadAll 启动时预加载所有配置到缓存 // LoadAll 启动时预加载所有配置到缓存
func (d *DynamicConfig) LoadAll(ctx context.Context) error { func (d *DynamicConfig) LoadAll(ctx context.Context) error {
items, _, err := d.repo.List(ctx, 1, 10000, "") // 获取所有配置 var items []SystemConfig
if err != nil { // Raw GORM query
if err := d.db.WithContext(ctx).Find(&items).Error; err != nil {
return err return err
} }
@ -172,13 +189,14 @@ func (d *DynamicConfig) LoadAll(ctx context.Context) error {
if decrypted, err := d.decryptValue(value); err == nil { if decrypted, err := d.decryptValue(value); err == nil {
value = decrypted value = decrypted
} else { } else {
d.logger.Error("解密配置失败", // 解密失败,尝试使用原始值
zap.String("key", item.ConfigKey),
zap.Error(err))
// 解密失败,尝试使用原始值(可能未加密)
} }
} }
d.cache.Store(item.ConfigKey, value) d.cache.Store(item.ConfigKey, value)
// DEBUG: Print keys being cached
if item.ConfigKey == "wechat.app_id" || item.ConfigKey == "wechat.app_secret" {
fmt.Printf("DEBUG LoadAll: Caching key=%s value=%s\n", item.ConfigKey, value)
}
} }
d.mu.Lock() d.mu.Lock()
@ -201,6 +219,7 @@ func (d *DynamicConfig) NeedsRefresh() bool {
return time.Since(d.loadedAt) > d.ttl return time.Since(d.loadedAt) > d.ttl
} }
// Get 获取配置值(带缓存)
// Get 获取配置值(带缓存) // Get 获取配置值(带缓存)
func (d *DynamicConfig) Get(ctx context.Context, key string) string { func (d *DynamicConfig) Get(ctx context.Context, key string) string {
// 1. 从缓存读取 // 1. 从缓存读取
@ -208,18 +227,25 @@ func (d *DynamicConfig) Get(ctx context.Context, key string) string {
return v.(string) return v.(string)
} }
// 2. 从数据库读取 // 2. 从数据库读取 (Raw GORM)
cfg, err := d.repo.GetByKey(ctx, key) var cfg SystemConfig
if err == nil && cfg != nil { // Use WithContext and Where
err := d.db.WithContext(ctx).Where("config_key = ?", key).First(&cfg).Error
if err == nil {
value := cfg.ConfigValue value := cfg.ConfigValue
// 敏感配置需要解密 // 敏感配置需要解密
if IsSensitiveKey(key) && value != "" { if IsSensitiveKey(key) && value != "" {
if decrypted, err := d.decryptValue(value); err == nil { if decrypted, err := d.decryptValue(value); err == nil {
value = decrypted value = decrypted
} else {
d.logger.Warn("Failed to decrypt sensitive key", zap.String("key", key), zap.Error(err))
} }
} }
d.cache.Store(key, value) d.cache.Store(key, value)
return value return value
} else {
// Only log warning if not found, don't return error (return empty string)
// d.logger.Warn("Config NOT found in DB", zap.String("key", key), zap.Error(err))
} }
// 3. 返回空字符串(调用方需要处理 fallback // 3. 返回空字符串(调用方需要处理 fallback
@ -234,6 +260,21 @@ func (d *DynamicConfig) GetWithFallback(ctx context.Context, key, fallback strin
return fallback return fallback
} }
// UpdateCache 手动更新缓存(用于 Admin API 调用后同步)
func (d *DynamicConfig) UpdateCache(key, value string) {
// 敏感配置需要解密后再存入缓存?
// 这里假设 Admin API 传入的是明文 value数据库存的是密文。
// 但是 LoadAll 里是从 DB 读出(可能是密文)解密后存缓存。
// cache 里存的是 明文。
// Admin API UpsertByKey 传入的是明文。
d.cache.Store(key, value)
}
// DeleteCache 删除缓存
func (d *DynamicConfig) DeleteCache(key string) {
d.cache.Delete(key)
}
// Set 设置配置值(自动处理加密) // Set 设置配置值(自动处理加密)
func (d *DynamicConfig) Set(ctx context.Context, key, value, remark string) error { func (d *DynamicConfig) Set(ctx context.Context, key, value, remark string) error {
storeValue := value storeValue := value
@ -246,14 +287,27 @@ func (d *DynamicConfig) Set(ctx context.Context, key, value, remark string) erro
storeValue = encrypted storeValue = encrypted
} }
_, err := d.repo.UpsertByKey(ctx, key, storeValue, remark) // Upsert logic
if err != nil { var existing SystemConfig
return err err := d.db.WithContext(ctx).Where("config_key = ?", key).First(&existing).Error
if err == nil {
// Update
existing.ConfigValue = storeValue
existing.Remark = remark
return d.db.WithContext(ctx).Save(&existing).Error
} }
// 更新缓存(存储明文) // Create
d.cache.Store(key, value) if errors.Is(err, gorm.ErrRecordNotFound) {
return nil newItem := SystemConfig{
ConfigKey: key,
ConfigValue: storeValue,
Remark: remark,
}
return d.db.WithContext(ctx).Create(&newItem).Error
}
return err
} }
// GetCOS 获取 COS 配置 // GetCOS 获取 COS 配置
@ -270,25 +324,23 @@ 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.GetWithFallback(ctx, KeyWechatAppID, staticCfg.AppID), AppID: d.Get(ctx, KeyWechatAppID),
AppSecret: d.GetWithFallback(ctx, KeyWechatAppSecret, staticCfg.AppSecret), AppSecret: d.Get(ctx, KeyWechatAppSecret),
LotteryResultTemplateID: d.GetWithFallback(ctx, KeyWechatLotteryResultTemplateID, staticCfg.LotteryResultTemplateID), LotteryResultTemplateID: d.Get(ctx, KeyWechatLotteryResultTemplateID),
} }
} }
// 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.GetWithFallback(ctx, KeyWechatPayMchID, staticCfg.MchID), MchID: d.Get(ctx, KeyWechatPayMchID),
SerialNo: d.GetWithFallback(ctx, KeyWechatPaySerialNo, staticCfg.SerialNo), SerialNo: d.Get(ctx, KeyWechatPaySerialNo),
PrivateKey: d.Get(ctx, KeyWechatPayPrivateKey), // 私钥无静态 fallback需要 Base64 存储 PrivateKey: d.Get(ctx, KeyWechatPayPrivateKey),
ApiV3Key: d.GetWithFallback(ctx, KeyWechatPayApiV3Key, staticCfg.ApiV3Key), ApiV3Key: d.Get(ctx, KeyWechatPayApiV3Key),
NotifyURL: d.GetWithFallback(ctx, KeyWechatPayNotifyURL, staticCfg.NotifyURL), NotifyURL: d.Get(ctx, KeyWechatPayNotifyURL),
PublicKeyID: d.GetWithFallback(ctx, KeyWechatPayPublicKeyID, staticCfg.PublicKeyID), PublicKeyID: d.Get(ctx, KeyWechatPayPublicKeyID),
PublicKey: d.Get(ctx, KeyWechatPayPublicKey), // 公钥无静态 fallback需要 Base64 存储 PublicKey: d.Get(ctx, KeyWechatPayPublicKey),
} }
} }
@ -311,6 +363,17 @@ func (d *DynamicConfig) GetDouyin(ctx context.Context) DouyinConfig {
NotifyURL: d.Get(ctx, KeyDouyinNotifyURL), NotifyURL: d.Get(ctx, KeyDouyinNotifyURL),
PayAppID: d.Get(ctx, KeyDouyinPayAppID), PayAppID: d.Get(ctx, KeyDouyinPayAppID),
PaySecret: d.Get(ctx, KeyDouyinPaySecret), PaySecret: d.Get(ctx, KeyDouyinPaySecret),
PaySalt: d.Get(ctx, KeyDouyinPaySalt),
} }
} }
// innerSubstr 截取字符串,避免越界
func innerSubstr(s string, start, length int) string {
if start >= len(s) {
return ""
}
end := start + length
if end > len(s) {
end = len(s)
}
return s[start:end]
}

View File

@ -38,3 +38,8 @@ func MustGetGlobalDynamicConfig() *DynamicConfig {
} }
return globalDynamicConfig return globalDynamicConfig
} }
// GetDynamicConfig 获取全局动态配置实例(别名)
func GetDynamicConfig() *DynamicConfig {
return globalDynamicConfig
}

View File

@ -40,12 +40,20 @@ func (s *service) UpsertByKey(ctx context.Context, key string, value string, rem
} }
m.ConfigValue = value m.ConfigValue = value
m.Remark = remark m.Remark = remark
// 同步更新缓存
if dc := GetDynamicConfig(); dc != nil {
dc.UpdateCache(key, value)
}
return m, nil return m, nil
} }
m = &model.SystemConfigs{ConfigKey: key, ConfigValue: value, Remark: remark} m = &model.SystemConfigs{ConfigKey: key, ConfigValue: value, Remark: remark}
if e := q.Create(m); e != nil { if e := q.Create(m); e != nil {
return nil, e return nil, e
} }
// 同步更新缓存
if dc := GetDynamicConfig(); dc != nil {
dc.UpdateCache(key, value)
}
return m, nil return m, nil
} }
@ -60,12 +68,29 @@ func (s *service) ModifyByID(ctx context.Context, id int64, value *string, remar
if len(set) == 0 { if len(set) == 0 {
return nil return nil
} }
_, err := s.readDB.SystemConfigs.WithContext(ctx).Where(s.readDB.SystemConfigs.ID.Eq(id)).Updates(set) // 先查出 Key
item, err := s.readDB.SystemConfigs.WithContext(ctx).ReadDB().Where(s.readDB.SystemConfigs.ID.Eq(id)).Take()
if err != nil {
return err
}
_, err = s.readDB.SystemConfigs.WithContext(ctx).Where(s.readDB.SystemConfigs.ID.Eq(id)).Updates(set)
if err == nil && value != nil {
if dc := GetDynamicConfig(); dc != nil {
dc.UpdateCache(item.ConfigKey, *value)
}
}
return err return err
} }
func (s *service) DeleteByID(ctx context.Context, id int64) error { func (s *service) DeleteByID(ctx context.Context, id int64) error {
item, _ := s.readDB.SystemConfigs.WithContext(ctx).ReadDB().Where(s.readDB.SystemConfigs.ID.Eq(id)).Take()
_, err := s.readDB.SystemConfigs.WithContext(ctx).Where(s.readDB.SystemConfigs.ID.Eq(id)).Updates(map[string]any{"deleted_at": time.Now()}) _, err := s.readDB.SystemConfigs.WithContext(ctx).Where(s.readDB.SystemConfigs.ID.Eq(id)).Updates(map[string]any{"deleted_at": time.Now()})
if err == nil && item != nil {
if dc := GetDynamicConfig(); dc != nil {
dc.DeleteCache(item.ConfigKey)
}
}
return err return err
} }

View File

@ -300,56 +300,76 @@ func (s *service) GetUserProgress(ctx context.Context, userID int64, taskID int6
} }
// 1. 实时统计订单数据 // 1. 实时统计订单数据
// BUG修复排除商城订单(source_type=1),只统计抽奖相关订单
// 通过 activity_draw_logs 和 activity_issues 表关联订单到活动
var orderCount int64 var orderCount int64
var orderAmount int64 var orderAmount int64
query := db.Model(&model.Orders{}).Where("user_id = ? AND status = 2", userID)
if targetActivityID > 0 {
// 增加 activity_id 过滤
// 格式匹配 activity:{id} 或 lottery:activity:{id}
// remark 包分隔符为 |,所以匹配 activity:{id}|... 或 activity:{id} 结尾
likePattern := fmt.Sprintf("%%activity:%d%%", targetActivityID)
query = query.Where("remark LIKE ?", likePattern)
}
query.Count(&orderCount)
// 复用 query 对象需要 clone 或者重新构造GORM 的 query 是可变的
// 这里简单起见,重新构造 query 或者使用 session
// 上面的 query.Count 可能会修改 query保险起见重新构造
queryAmount := db.Model(&model.Orders{}).Where("user_id = ? AND status = 2", userID)
if targetActivityID > 0 { if targetActivityID > 0 {
likePattern := fmt.Sprintf("%%activity:%d%%", targetActivityID) // 有活动ID限制时通过 activity_draw_logs → activity_issues 关联过滤
queryAmount = queryAmount.Where("remark LIKE ?", likePattern) // 统计订单数量(使用 WHERE IN 子查询防止 JOIN 导致的重复计数问题)
db.Raw(`
SELECT COUNT(id)
FROM orders
WHERE user_id = ? AND status = 2 AND source_type != 1
AND id IN (
SELECT DISTINCT dl.order_id
FROM activity_draw_logs dl
INNER JOIN activity_issues ai ON ai.id = dl.issue_id
WHERE ai.activity_id = ?
)
`, userID, targetActivityID).Scan(&orderCount)
// 统计订单金额
// BUG修复已解决 JOIN activity_draw_logs 导致金额翻倍的问题
db.Raw(`
SELECT COALESCE(SUM(total_amount), 0)
FROM orders
WHERE user_id = ? AND status = 2 AND source_type != 1
AND id IN (
SELECT DISTINCT dl.order_id
FROM activity_draw_logs dl
INNER JOIN activity_issues ai ON ai.id = dl.issue_id
WHERE ai.activity_id = ?
)
`, userID, targetActivityID).Scan(&orderAmount)
} else {
// 无活动ID限制时统计所有非商城订单
// 增加 EXISTS 检查,确保订单已开奖(有开奖日志)
query := db.Model(&model.Orders{}).Where("user_id = ? AND status = 2 AND source_type != 1", userID)
query.Where("EXISTS (SELECT 1 FROM activity_draw_logs WHERE activity_draw_logs.order_id = orders.id)")
query.Count(&orderCount)
queryAmount := db.Model(&model.Orders{}).Where("user_id = ? AND status = 2 AND source_type != 1", userID)
queryAmount.Where("EXISTS (SELECT 1 FROM activity_draw_logs WHERE activity_draw_logs.order_id = orders.id)")
queryAmount.Select("COALESCE(SUM(total_amount), 0)").Scan(&orderAmount)
} }
queryAmount.Select("COALESCE(SUM(actual_amount), 0)").Scan(&orderAmount)
// 2. 实时统计邀请数据(有效邀请:被邀请人有消费记录) // 2. 实时统计邀请数据(有效邀请:被邀请人有消费记录)
// 注意:邀请统计是否也要过滤 ActivityID // 同样应用“已开奖”逻辑过滤
// 需求是“消费的是对对碰”,通常指邀请带来的“有效用户”需要在该活动消费。
// 之前的逻辑是INNER JOIN orders只要有任意消费就算有效。
// 如果任务是“对对碰”任务,那么被邀请人应该在“对对碰”消费才算有效邀请吗?
// 暂时保持原样,或者也加上过滤。根据常理,特定活动拉新通常要求在该活动消费。
// 这里加上过滤更安全。
var inviteCount int64 var inviteCount int64
inviteQuery := fmt.Sprintf(`
SELECT COUNT(DISTINCT ui.invitee_id)
FROM user_invites ui
INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2
WHERE ui.inviter_id = ?
`)
var args []interface{}
args = append(args, userID)
if targetActivityID > 0 { if targetActivityID > 0 {
inviteQuery = fmt.Sprintf(` db.Raw(`
SELECT COUNT(DISTINCT ui.invitee_id) SELECT COUNT(DISTINCT ui.invitee_id)
FROM user_invites ui FROM user_invites ui
INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2 AND o.remark LIKE ? 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 o.id IN (
SELECT DISTINCT dl.order_id
FROM activity_draw_logs dl
INNER JOIN activity_issues ai ON ai.id = dl.issue_id
WHERE ai.activity_id = ?
)
`, userID, targetActivityID).Scan(&inviteCount)
} else {
db.Raw(`
SELECT COUNT(DISTINCT ui.invitee_id)
FROM user_invites ui
INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2 AND o.source_type != 1
WHERE ui.inviter_id = ? WHERE ui.inviter_id = ?
`) AND EXISTS (SELECT 1 FROM activity_draw_logs WHERE activity_draw_logs.order_id = o.id)
args = []interface{}{fmt.Sprintf("%%activity:%d%%", targetActivityID), userID} `, userID).Scan(&inviteCount)
} }
db.Raw(inviteQuery, args...).Scan(&inviteCount)
// 3. 首单判断 // 3. 首单判断
hasFirstOrder := orderCount > 0 hasFirstOrder := orderCount > 0
@ -386,8 +406,60 @@ func (s *service) GetUserProgress(ctx context.Context, userID int64, taskID int6
} }
func (s *service) ClaimTier(ctx context.Context, userID int64, taskID int64, tierID int64) error { func (s *service) ClaimTier(ctx context.Context, userID int64, taskID int64, tierID int64) error {
// 事务中更新领取状态 // BUG FIX: 增加前置校验,确保用户真的完成了该档位任务
err := s.repo.GetDbW().Transaction(func(tx *gorm.DB) error { progress, err := s.GetUserProgress(ctx, userID, taskID)
if err != nil {
return err
}
// 获取档位配置
var tier tcmodel.TaskTier
if err := s.repo.GetDbR().First(&tier, tierID).Error; err != nil {
return err
}
// 校验是否达标
hit := false
switch tier.Metric {
case MetricFirstOrder:
hit = progress.FirstOrder
case MetricOrderCount:
if tier.Operator == OperatorGTE {
hit = progress.OrderCount >= tier.Threshold
} else {
hit = progress.OrderCount == tier.Threshold
}
case MetricOrderAmount:
if tier.Operator == OperatorGTE {
hit = progress.OrderAmount >= tier.Threshold
} else {
hit = progress.OrderAmount == tier.Threshold
}
case MetricInviteCount:
if tier.Operator == OperatorGTE {
hit = progress.InviteCount >= tier.Threshold
} else {
hit = progress.InviteCount == tier.Threshold
}
}
if !hit {
return errors.New("任务条件未达成,无法领取")
}
// 1. 先尝试发放奖励 (grantTierRewards 内部有幂等校验)
// IDK logic inside grantTierRewards ensures we don't double grant.
// We use "manual_claim" as source type.
// IMPORTANT: Call this BEFORE updating the progress status to avoid "Claimed but not received" state if grant fails.
s.logger.Info("ClaimTier: Starting reward grant...", zap.Int64("user_id", userID), zap.Int64("task_id", taskID), zap.Int64("tier_id", tierID))
if err := s.grantTierRewards(ctx, taskID, tierID, userID, "manual_claim", 0, fmt.Sprintf("claim:%d:%d:%d", userID, taskID, tierID)); err != nil {
s.logger.Error("ClaimTier: Reward grant failed", zap.Error(err), zap.Int64("user_id", userID), zap.Int64("tier_id", tierID))
return err
}
s.logger.Info("ClaimTier: Reward granted successfully", zap.Int64("user_id", userID), zap.Int64("tier_id", tierID))
// 2. 奖励发放成功后,事务中更新领取状态
err = s.repo.GetDbW().Transaction(func(tx *gorm.DB) error {
var p tcmodel.UserTaskProgress var p tcmodel.UserTaskProgress
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("user_id=? AND task_id=? AND activity_id=0", userID, taskID).First(&p).Error err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("user_id=? AND task_id=? AND activity_id=0", userID, taskID).First(&p).Error
if err != nil { if err != nil {
@ -412,7 +484,7 @@ func (s *service) ClaimTier(ctx context.Context, userID int64, taskID int64, tie
} }
for _, id := range claimed { for _, id := range claimed {
if id == tierID { if id == tierID {
return nil // 已领取,跳过 return nil // 已更新状态,无需重复更新
} }
} }
claimed = append(claimed, tierID) claimed = append(claimed, tierID)
@ -421,11 +493,11 @@ func (s *service) ClaimTier(ctx context.Context, userID int64, taskID int64, tie
return tx.Model(&tcmodel.UserTaskProgress{}).Where("id=?", p.ID).Update("claimed_tiers", p.ClaimedTiers).Error return tx.Model(&tcmodel.UserTaskProgress{}).Where("id=?", p.ID).Update("claimed_tiers", p.ClaimedTiers).Error
}) })
if err != nil { if err != nil {
s.logger.Error("ClaimTier: Failed to update status", zap.Error(err))
return err return err
} }
s.logger.Info("ClaimTier: Status updated successfully", zap.Int64("user_id", userID), zap.Int64("tier_id", tierID))
// 发放奖励 return nil
return s.grantTierRewards(ctx, taskID, tierID, userID, "manual_claim", 0, fmt.Sprintf("claim:%d:%d:%d", userID, taskID, tierID))
} }
func (s *service) CreateTask(ctx context.Context, in CreateTaskInput) (int64, error) { func (s *service) CreateTask(ctx context.Context, in CreateTaskInput) (int64, error) {
@ -862,10 +934,9 @@ func (s *service) grantTierRewards(ctx context.Context, taskID int64, tierID int
if len(rewards) == 0 { if len(rewards) == 0 {
var tier tcmodel.TaskTier var tier tcmodel.TaskTier
if err := s.repo.GetDbR().First(&tier, tierID).Error; err == nil { if err := s.repo.GetDbR().First(&tier, tierID).Error; err == nil {
// 查找具有相同业务指纹的“活跃”奖励(如果有的话,可能是由于管理员操作导致 ID 偏移) s.logger.Warn("Tier ID mismatch or no rewards configured", zap.Int64("tier_id", tierID))
// 虽然保留 ID 解决了大部分问题,但物理删除重建仍可能发生
s.logger.Warn("Tier ID mismatch, attempting fallback matching", zap.Int64("tier_id", tierID))
} }
return errors.New("no rewards configured for this tier")
} }
idk := fmt.Sprintf("%d:%d:%d:%s:%d", userID, taskID, tierID, sourceType, sourceID) idk := fmt.Sprintf("%d:%d:%d:%s:%d", userID, taskID, tierID, sourceType, sourceID)
@ -904,11 +975,14 @@ func (s *service) grantTierRewards(ctx context.Context, taskID int64, tierID int
} }
_ = json.Unmarshal([]byte(r.RewardPayload), &pl) _ = json.Unmarshal([]byte(r.RewardPayload), &pl)
if pl.CouponID > 0 { if pl.CouponID > 0 {
// BUG 修复:优先使用 r.Quantity仅当 > 1 时),否则使用 payload否则默认 1
qty := 1 qty := 1
if r.Quantity > 0 { if r.Quantity > 1 {
qty = int(r.Quantity) qty = int(r.Quantity)
} else if pl.Quantity > 0 { } else if pl.Quantity > 0 {
qty = pl.Quantity qty = pl.Quantity
} else if r.Quantity == 1 {
qty = 1 // 显式设置为 1
} }
s.logger.Info("Granting coupon reward", zap.Int64("user_id", userID), zap.Int64("coupon_id", pl.CouponID), zap.Int("quantity", qty)) s.logger.Info("Granting coupon reward", zap.Int64("user_id", userID), zap.Int64("coupon_id", pl.CouponID), zap.Int("quantity", qty))
for i := 0; i < qty; i++ { for i := 0; i < qty; i++ {
@ -924,11 +998,14 @@ func (s *service) grantTierRewards(ctx context.Context, taskID int64, tierID int
} }
_ = json.Unmarshal([]byte(r.RewardPayload), &pl) _ = json.Unmarshal([]byte(r.RewardPayload), &pl)
if pl.CardID > 0 { if pl.CardID > 0 {
// BUG 修复:优先使用 r.Quantity仅当 > 1 时),否则使用 payload否则默认 1
qty := 1 qty := 1
if r.Quantity > 0 { if r.Quantity > 1 {
qty = int(r.Quantity) qty = int(r.Quantity)
} else if pl.Quantity > 0 { } else if pl.Quantity > 0 {
qty = pl.Quantity qty = pl.Quantity
} else if r.Quantity == 1 {
qty = 1 // 显式设置为 1
} }
s.logger.Info("Granting item card reward", zap.Int64("user_id", userID), zap.Int64("card_id", pl.CardID), zap.Int("quantity", qty)) s.logger.Info("Granting item card reward", zap.Int64("user_id", userID), zap.Int64("card_id", pl.CardID), zap.Int("quantity", qty))
err = s.userSvc.AddItemCard(ctx, userID, pl.CardID, qty) err = s.userSvc.AddItemCard(ctx, userID, pl.CardID, qty)
@ -948,10 +1025,19 @@ func (s *service) grantTierRewards(ctx context.Context, taskID int64, tierID int
Amount int `json:"amount"` Amount int `json:"amount"`
} }
_ = json.Unmarshal([]byte(r.RewardPayload), &pl) _ = json.Unmarshal([]byte(r.RewardPayload), &pl)
if pl.GameCode != "" && pl.Amount > 0 { if pl.GameCode != "" {
s.logger.Info("Granting game ticket reward", zap.Int64("user_id", userID), zap.String("game_code", pl.GameCode), zap.Int("amount", pl.Amount)) // BUG 修复:增加对 r.Quantity 的支持,统一数量解析逻辑
amount := 1
if r.Quantity > 1 {
amount = int(r.Quantity)
} else if pl.Amount > 0 {
amount = pl.Amount
} else if r.Quantity == 1 {
amount = 1
}
s.logger.Info("Granting game ticket reward", zap.Int64("user_id", userID), zap.String("game_code", pl.GameCode), zap.Int("amount", amount))
gameSvc := gamesvc.NewTicketService(s.logger, s.repo) gameSvc := gamesvc.NewTicketService(s.logger, s.repo)
err = gameSvc.GrantTicket(ctx, userID, pl.GameCode, pl.Amount, "task_center", taskID, "任务奖励") err = gameSvc.GrantTicket(ctx, userID, pl.GameCode, amount, "task_center", taskID, "任务奖励")
} }
case "product": case "product":
var pl struct { var pl struct {
@ -960,11 +1046,14 @@ func (s *service) grantTierRewards(ctx context.Context, taskID int64, tierID int
} }
_ = json.Unmarshal([]byte(r.RewardPayload), &pl) _ = json.Unmarshal([]byte(r.RewardPayload), &pl)
if pl.ProductID > 0 { if pl.ProductID > 0 {
// BUG 修复:优先使用 r.Quantity仅当 > 1 时),否则使用 payload否则默认 1
qty := 1 qty := 1
if r.Quantity > 0 { if r.Quantity > 1 {
qty = int(r.Quantity) qty = int(r.Quantity)
} else if pl.Quantity > 0 { } else if pl.Quantity > 0 {
qty = pl.Quantity qty = pl.Quantity
} else if r.Quantity == 1 {
qty = 1 // 显式设置为 1
} }
s.logger.Info("Granting product reward", zap.Int64("user_id", userID), zap.Int64("product_id", pl.ProductID), zap.Int("quantity", qty)) s.logger.Info("Granting product reward", zap.Int64("user_id", userID), zap.Int64("product_id", pl.ProductID), zap.Int("quantity", qty))
// 通过用户服务发放商品(创建待发货订单) // 通过用户服务发放商品(创建待发货订单)

View File

@ -63,7 +63,8 @@ func (s *service) CreateAddressShare(ctx context.Context, userID int64, inventor
wcfg := &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret} wcfg := &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}
at, errat := wechat.GetAccessTokenWithContext(ctx, wcfg) at, errat := wechat.GetAccessTokenWithContext(ctx, wcfg)
if errat == nil { if errat == nil {
pagePath := fmt.Sprintf("pages/address/submit?token=%s", token) // BUG修复地址填写页在 pages-user 分包下,需添加 pages-user 前缀
pagePath := fmt.Sprintf("pages-user/address/submit?token=%s", token)
pageTitle := "送你一个好礼,快来填写地址领走吧!" pageTitle := "送你一个好礼,快来填写地址领走吧!"
if inv.Remark != "" { if inv.Remark != "" {
pageTitle = fmt.Sprintf("送你一个%s快来领走吧", inv.Remark) pageTitle = fmt.Sprintf("送你一个%s快来领走吧", inv.Remark)
@ -76,8 +77,8 @@ func (s *service) CreateAddressShare(ctx context.Context, userID int64, inventor
// 降级尝试生成 Scheme // 降级尝试生成 Scheme
s.logger.Warn("生成微信短链失败尝试降级为Scheme", zap.Error(errsl), zap.String("page_path", pagePath)) s.logger.Warn("生成微信短链失败尝试降级为Scheme", zap.Error(errsl), zap.String("page_path", pagePath))
// 修正 pagePath 格式URL Scheme 需要 path 和 query 分离 // 修正 pagePath 格式URL Scheme 需要 path 和 query 分离
// 假设 pagePath 格式为 "pages/address/submit?token=xxx" // BUG修复地址填写页在 pages-user 分包下
schemePath := "pages/address/submit" schemePath := "pages-user/address/submit"
schemeQuery := fmt.Sprintf("token=%s", token) schemeQuery := fmt.Sprintf("token=%s", token)
scheme, errScheme := wechat.GenerateScheme(at, schemePath, schemeQuery, "release") scheme, errScheme := wechat.GenerateScheme(at, schemePath, schemeQuery, "release")
@ -105,23 +106,29 @@ func (s *service) RevokeAddressShare(ctx context.Context, userID int64, inventor
func (s *service) SubmitAddressShare(ctx context.Context, shareToken string, name string, mobile string, province string, city string, district string, address string, submittedByUserID *int64, submittedIP *string) (int64, error) { func (s *service) SubmitAddressShare(ctx context.Context, shareToken string, name string, mobile string, province string, city string, district string, address string, submittedByUserID *int64, submittedIP *string) (int64, error) {
claims, err := parseShareToken(shareToken) claims, err := parseShareToken(shareToken)
if err != nil { if err != nil {
s.logger.Error("SubmitAddressShare: Token parse failed", zap.Error(err), zap.String("token_masked", shareToken[:10]+"..."))
return 0, fmt.Errorf("invalid_or_expired_token") return 0, fmt.Errorf("invalid_or_expired_token")
} }
s.logger.Info("SubmitAddressShare: Processing", zap.Int64("invID", claims.InventoryID), zap.Int64("owner", claims.OwnerUserID))
// 1. 基本安全校验 // 1. 基本安全校验
cnt, err := s.readDB.ShippingRecords.WithContext(ctx).Where( cnt, err := s.readDB.ShippingRecords.WithContext(ctx).Where(
s.readDB.ShippingRecords.InventoryID.Eq(claims.InventoryID), s.readDB.ShippingRecords.InventoryID.Eq(claims.InventoryID),
s.readDB.ShippingRecords.Status.Neq(5), // 排除已取消 s.readDB.ShippingRecords.Status.Neq(5), // 排除已取消
).Count() ).Count()
if err == nil && cnt > 0 { if err == nil && cnt > 0 {
s.logger.Warn("SubmitAddressShare: Already processed", zap.Int64("invID", claims.InventoryID))
return 0, fmt.Errorf("already_processed") return 0, fmt.Errorf("already_processed")
} }
inv, err := s.readDB.UserInventory.WithContext(ctx).Where(s.readDB.UserInventory.ID.Eq(claims.InventoryID)).First() inv, err := s.readDB.UserInventory.WithContext(ctx).Where(s.readDB.UserInventory.ID.Eq(claims.InventoryID)).First()
if err != nil { if err != nil {
s.logger.Error("SubmitAddressShare: Inventory not found", zap.Int64("invID", claims.InventoryID), zap.Error(err))
return 0, err return 0, err
} }
if inv.Status != 1 { if inv.Status != 1 {
s.logger.Warn("SubmitAddressShare: Inventory unavailable", zap.Int64("invID", claims.InventoryID), zap.Int32("status", inv.Status))
return 0, fmt.Errorf("inventory_unavailable") return 0, fmt.Errorf("inventory_unavailable")
} }

View File

@ -9,10 +9,10 @@ import (
"strconv" "strconv"
"time" "time"
"bindbox-game/configs"
"bindbox-game/internal/pkg/wechat" "bindbox-game/internal/pkg/wechat"
"bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model" "bindbox-game/internal/repository/mysql/model"
"bindbox-game/internal/service/sysconfig"
randomname "github.com/DanPlayer/randomname" randomname "github.com/DanPlayer/randomname"
identicon "github.com/issue9/identicon/v2" identicon "github.com/issue9/identicon/v2"
@ -43,11 +43,14 @@ type LoginWeixinOutput struct {
func (s *service) LoginWeixin(ctx context.Context, in LoginWeixinInput) (*LoginWeixinOutput, error) { func (s *service) LoginWeixin(ctx context.Context, in LoginWeixinInput) (*LoginWeixinOutput, error) {
// 1. 获取 OpenID (如果是小程序登录) // 1. 获取 OpenID (如果是小程序登录)
if in.Code != "" { if in.Code != "" {
cfg := configs.Get().Wechat
wcfg := &wechat.WechatConfig{ // 结合动态配置和静态配置
AppID: cfg.AppID, wcfg := &wechat.WechatConfig{}
AppSecret: cfg.AppSecret, wcfgVal := sysconfig.GetDynamicConfig().GetWechat(ctx)
} wcfg.AppID = wcfgVal.AppID
wcfg.AppSecret = wcfgVal.AppSecret
s.logger.Info("DEBUG: LoginWeixin Config", zap.String("AppID", wcfg.AppID), zap.String("AppSecret", wcfg.AppSecret))
resp, err := wechat.Code2Session(ctx, wcfg, in.Code) resp, err := wechat.Code2Session(ctx, wcfg, in.Code)
if err != nil { if err != nil {
s.logger.Error("code2session failed", zap.Error(err)) s.logger.Error("code2session failed", zap.Error(err))

View File

@ -20,6 +20,7 @@ type GrantRewardRequest struct {
AddressID *int64 `json:"address_id,omitempty"` // 收货地址ID可选实物商品需要 AddressID *int64 `json:"address_id,omitempty"` // 收货地址ID可选实物商品需要
Remark string `json:"remark,omitempty"` // 备注 Remark string `json:"remark,omitempty"` // 备注
PointsAmount int64 `json:"points_amount,omitempty"` // 消耗积分 PointsAmount int64 `json:"points_amount,omitempty"` // 消耗积分
SourceType *int32 `json:"source_type,omitempty"` // 订单来源可选默认3
} }
// GrantRewardResponse 奖励发放响应 // GrantRewardResponse 奖励发放响应
@ -83,9 +84,14 @@ func (s *service) GrantReward(ctx context.Context, userID int64, req GrantReward
// 避免使用零时间导致MySQL的'0000-00-00'错误 // 避免使用零时间导致MySQL的'0000-00-00'错误
minValidTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) minValidTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
order := &model.Orders{ order := &model.Orders{
OrderNo: orderNo, OrderNo: orderNo,
UserID: userID, UserID: userID,
SourceType: 3, // 系统发放 SourceType: func() int32 {
if req.SourceType != nil {
return *req.SourceType
}
return 6 // 默认:系统发放/管理员
}(),
Status: 2, // 已支付 Status: 2, // 已支付
TotalAmount: 0, TotalAmount: 0,
DiscountAmount: 0, DiscountAmount: 0,

View File

@ -16,6 +16,7 @@ import (
"bindbox-game/internal/pkg/sms" "bindbox-game/internal/pkg/sms"
"bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model" "bindbox-game/internal/repository/mysql/model"
"bindbox-game/internal/service/sysconfig"
randomname "github.com/DanPlayer/randomname" randomname "github.com/DanPlayer/randomname"
identicon "github.com/issue9/identicon/v2" identicon "github.com/issue9/identicon/v2"
@ -87,8 +88,13 @@ func (s *service) SendSmsCode(ctx context.Context, mobile string) error {
// 4. 生成6位验证码 // 4. 生成6位验证码
code := generateCode(codeLength) code := generateCode(codeLength)
// 5. 发送短信 // 5. 发送短信 - 使用动态配置system_configs 表)
cfg := configs.Get().AliyunSMS dc := sysconfig.GetDynamicConfig()
if dc == nil {
s.logger.Error("动态配置服务未初始化")
return errors.New("短信服务暂不可用,请稍后重试")
}
cfg := dc.GetAliyunSMS(ctx)
smsClient, err := sms.NewClient(sms.Config{ smsClient, err := sms.NewClient(sms.Config{
AccessKeyID: cfg.AccessKeyID, AccessKeyID: cfg.AccessKeyID,
AccessKeySecret: cfg.AccessKeySecret, AccessKeySecret: cfg.AccessKeySecret,

View File

@ -37,6 +37,9 @@ import (
func main() { func main() {
flag.Parse() flag.Parse()
// 初始化配置
configs.Init()
// 初始化 OpenTelemetry // 初始化 OpenTelemetry
cfg := configs.Get() cfg := configs.Get()
otelShutdown, err := otel.Init(otel.Config{ otelShutdown, err := otel.Init(otel.Config{
@ -101,7 +104,8 @@ func main() {
// 启动抖店订单同步定时任务 // 启动抖店订单同步定时任务
syscfgSvc := syscfgsvc.New(customLogger, dbRepo) syscfgSvc := syscfgsvc.New(customLogger, dbRepo)
ticketSvc := gamesvc.NewTicketService(customLogger, dbRepo) ticketSvc := gamesvc.NewTicketService(customLogger, dbRepo)
douyinsvc.StartDouyinOrderSync(customLogger, dbRepo, syscfgSvc, ticketSvc) userSvc := usersvc.New(customLogger, dbRepo)
douyinsvc.StartDouyinOrderSync(customLogger, dbRepo, syscfgSvc, ticketSvc, userSvc)
// 初始化全局动态配置服务 // 初始化全局动态配置服务
if err := syscfgsvc.InitGlobalDynamicConfig(customLogger, dbRepo); err != nil { if err := syscfgsvc.InitGlobalDynamicConfig(customLogger, dbRepo); err != nil {

View File

@ -1 +0,0 @@
INSERT INTO sys_configs (config_key, config_group, config_value, remark, is_encrypted) VALUES ('wechat_miniprogram_lottery_result_template_id', 'miniprogram', 'O2eqJQD3pn-vQ6g2z9DWzINVwOmPoz8yW-172J_YcpI', '微信小程序开奖结果通知模板ID', 0) ON DUPLICATE KEY UPDATE config_value='O2eqJQD3pn-vQ6g2z9DWzINVwOmPoz8yW-172J_YcpI';

View File

@ -0,0 +1,57 @@
-- 直播间活动表
CREATE TABLE IF NOT EXISTS `livestream_activities` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(255) NOT NULL COMMENT '活动名称',
`streamer_name` VARCHAR(128) DEFAULT '' COMMENT '主播名称',
`streamer_contact` VARCHAR(255) DEFAULT '' COMMENT '主播联系方式',
`access_code` VARCHAR(64) NOT NULL COMMENT '唯一访问码',
`douyin_product_id` VARCHAR(64) DEFAULT '' COMMENT '关联抖店商品ID',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1进行中 2已结束',
`start_time` DATETIME(3) DEFAULT NULL COMMENT '开始时间',
`end_time` DATETIME(3) DEFAULT NULL COMMENT '结束时间',
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
`deleted_at` DATETIME(3) DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_access_code` (`access_code`),
KEY `idx_product` (`douyin_product_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='直播间活动表';
-- 直播间奖品表
CREATE TABLE IF NOT EXISTS `livestream_prizes` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`activity_id` BIGINT NOT NULL COMMENT '关联livestream_activities.id',
`name` VARCHAR(255) NOT NULL COMMENT '奖品名称',
`image` VARCHAR(512) DEFAULT '' COMMENT '奖品图片',
`weight` INT NOT NULL DEFAULT 1 COMMENT '抽奖权重',
`quantity` INT NOT NULL DEFAULT -1 COMMENT '库存数量(-1=无限)',
`remaining` INT NOT NULL DEFAULT -1 COMMENT '剩余数量',
`level` TINYINT NOT NULL DEFAULT 1 COMMENT '奖品等级',
`product_id` BIGINT DEFAULT NULL COMMENT '关联系统商品ID',
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序',
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_activity` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='直播间奖品表';
-- 直播间中奖记录表
CREATE TABLE IF NOT EXISTS `livestream_draw_logs` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`activity_id` BIGINT NOT NULL COMMENT '关联livestream_activities.id',
`prize_id` BIGINT NOT NULL COMMENT '关联livestream_prizes.id',
`douyin_order_id` BIGINT DEFAULT NULL COMMENT '关联douyin_orders.id',
`local_user_id` BIGINT DEFAULT NULL COMMENT '本地用户ID',
`douyin_user_id` VARCHAR(64) DEFAULT '' COMMENT '抖音用户ID',
`prize_name` VARCHAR(255) DEFAULT '' COMMENT '中奖奖品名称快照',
`level` TINYINT DEFAULT 1 COMMENT '奖品等级',
`seed_hash` VARCHAR(128) DEFAULT '' COMMENT '哈希种子',
`rand_value` BIGINT DEFAULT 0 COMMENT '随机值',
`weights_total` BIGINT DEFAULT 0 COMMENT '权重总和',
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '中奖时间',
PRIMARY KEY (`id`),
KEY `idx_activity` (`activity_id`),
KEY `idx_douyin_order` (`douyin_order_id`),
KEY `idx_user` (`local_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='直播间中奖记录表';

View File

@ -0,0 +1,8 @@
-- 直播间活动表添加 commitment 字段
-- 用于生成可验证的抽奖凭证
ALTER TABLE `livestream_activities`
ADD COLUMN `commitment_algo` VARCHAR(32) DEFAULT 'commit-v1' COMMENT '承诺算法版本' AFTER `status`,
ADD COLUMN `commitment_seed_master` BLOB COMMENT '主种子(32字节)' AFTER `commitment_algo`,
ADD COLUMN `commitment_seed_hash` BLOB COMMENT '种子SHA256哈希' AFTER `commitment_seed_master`,
ADD COLUMN `commitment_state_version` INT DEFAULT 0 COMMENT '状态版本' AFTER `commitment_seed_hash`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
package main
import (
"bindbox-game/configs"
"bindbox-game/internal/repository/mysql"
"flag"
"fmt"
"os"
)
func main() {
flag.Parse()
configs.Init()
repo, err := mysql.New()
if err != nil {
fmt.Printf("DB Error: %v\n", err)
os.Exit(1)
}
db := repo.GetDbW()
// 添加 is_granted 字段
sql := "ALTER TABLE livestream_draw_logs ADD COLUMN is_granted TINYINT(1) DEFAULT 0 COMMENT '是否已发放奖品' AFTER created_at"
// 检查列是否存在
var count int64
db.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'livestream_draw_logs' AND COLUMN_NAME = 'is_granted'").Scan(&count)
if count == 0 {
if err := db.Exec(sql).Error; err != nil {
fmt.Printf("Failed to add column: %v\n", err)
os.Exit(1)
}
fmt.Println("SUCCESS: Added is_granted column")
} else {
fmt.Println("Column is_granted already exists")
}
}

View File

@ -0,0 +1,43 @@
package main
import (
"bindbox-game/configs"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/model"
"fmt"
"log"
)
func main() {
configs.Init()
repo, err := mysql.New()
if err != nil {
log.Fatalf("mysql init failed: %v", err)
}
db := repo.GetDbW()
// Use raw SQL to add column if not exists
tableName := model.TableNameDouyinOrders
columnName := "product_count"
// Check if column exists
var count int64
checkSQL := fmt.Sprintf("SELECT count(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '%s' AND COLUMN_NAME = '%s'", tableName, columnName)
if err := db.Raw(checkSQL).Scan(&count).Error; err != nil {
log.Fatalf("check column failed: %v", err)
}
if count == 0 {
log.Printf("Adding column %s to table %s...", columnName, tableName)
// Add column
alterSQL := fmt.Sprintf("ALTER TABLE `%s` ADD COLUMN `%s` INT NOT NULL DEFAULT 1 COMMENT '商品数量';", tableName, columnName)
if err := db.Exec(alterSQL).Error; err != nil {
log.Fatalf("add column failed: %v", err)
}
log.Println("Column added successfully.")
} else {
log.Println("Column already exists.")
}
}

48
scripts/fix_db_column.py Normal file
View File

@ -0,0 +1,48 @@
import pymysql
# DB Configs
host = '150.158.78.154'
port = 3306
user = 'root'
password = 'bindbox2025kdy'
database = 'bindbox_game'
# Connect
try:
connection = pymysql.connect(
host=host,
port=port,
user=user,
password=password,
database=database,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
with connection.cursor() as cursor:
# Check columns
cursor.execute("SHOW COLUMNS FROM livestream_draw_logs LIKE 'shop_order_id'")
result = cursor.fetchone()
if not result:
print("Adding shop_order_id column...")
cursor.execute("ALTER TABLE livestream_draw_logs ADD COLUMN shop_order_id VARCHAR(255) DEFAULT '' COMMENT '抖店订单号' AFTER douyin_order_id")
connection.commit()
print("shop_order_id added.")
else:
print("shop_order_id already exists.")
cursor.execute("SHOW COLUMNS FROM livestream_draw_logs LIKE 'user_nickname'")
result = cursor.fetchone()
if not result:
print("Adding user_nickname column...")
cursor.execute("ALTER TABLE livestream_draw_logs ADD COLUMN user_nickname VARCHAR(255) DEFAULT '' COMMENT '用户昵称' AFTER douyin_user_id")
connection.commit()
print("user_nickname added.")
else:
print("user_nickname already exists.")
except Exception as e:
print(f"Error: {e}")
finally:
if 'connection' in locals() and connection.open:
connection.close()

View File

@ -0,0 +1,43 @@
package main
import (
"fmt"
"log"
"bindbox-game/configs"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/model"
"gorm.io/gorm"
)
func main() {
// Initialize Config
configs.Init()
// Initialize Database
dbRepo, err := mysql.New()
if err != nil {
log.Fatalf("Failed to init db: %v", err)
}
db := dbRepo.GetDbW()
// Add column
msg := addColumn(db)
fmt.Println(msg)
}
func addColumn(db *gorm.DB) string {
// Check if column exists
if db.Migrator().HasColumn(&model.LivestreamPrizes{}, "CostPrice") {
return "Column 'cost_price' already exists in 'livestream_prizes'"
}
// Add column
err := db.Migrator().AddColumn(&model.LivestreamPrizes{}, "CostPrice")
if err != nil {
log.Fatalf("Failed to add column: %v", err)
}
return "Successfully added column 'cost_price' to 'livestream_prizes'"
}

15
scripts/output.json Normal file
View File

@ -0,0 +1,15 @@
: https://fxg.jinritemai.com/api/order/searchlist
: {
"page": "0",
"pageSize": "10",
"order_by": "create_time",
"order": "desc",
"tab": "all",
"appid": "1",
"_bid": "ffa_order",
"aid": "4272"
}
: 200
: ['order_id', 'shop_order_id', 'order_status', 'user_id', 'now_ts', 'pay_type', 'order_type', 'b_type', 'c_biz', 'biz', 'receive_type', 'e_express', 'repeat', 'is_dup', 'pre_receive_info_exist', 'has_write_off_record', 'is_already_modify_amount', 'user_is_auth', 'can_modify_amount', 'change_addr', 'store_name', 'wait_ship_count', 'shipped_count', 'product_count', 'total_post_amount', 'total_pay_amount', 'pay_amount', 'post_amount', 'total_tax_amount', 'total_include_tax_amount', 'total_excluding_tax_amount', 'total_goods_amount', 'promotion_amount', 'modify_amount', 'modify_post_amount', 'sku_modify_amount', 'shop_receive_amount', 'promotion_pay_amount', 'envelope_promotion_amount', 'total_tax_amount_desc', 'actual_pay_amount', 'actual_receive_amount', 'actual_receive_amount_desc', 'actual_receive_amount_int', 'create_time', 'confirm_time', 'pay_time', 'logistics_time', 'receipt_time', 'group_time', 'exp_ship_time', 'order_type_desc', 'pay_type_desc', 'write_off_desc', 'buyer_words', 'remark', 'star', 'user_nickname', 'has_write_off', 'has_more', 'pre_sale_desc', 'receive_info', 'receiver_info', 'policy_info', 'order_status_info', 'operation_actions', 'action_map', 'button', 'order_bottom_card', 'product_item', 'shop_order_tag', 'pay_amount_detail', 'way_bill_url', 'cross_border_send_type', 'order_amount_card', 'pay_amount_desc', 'shop_receive_amount_desc', 'serial_numbers', 'address_tag', 'support_detail', 'need_serial_number', 'b_type_desc', 'c_biz_desc', 'price_detail', 'promotion_detail', 'pay_type_desc_hover', 'manual_order_type', 'order_id_for_show', 'order_tag_stamp', 'url_map', 'user_profile_tag', 'supermarket_order_serial_no', 'deliver_name', 'deliver_mobile', 'receipt_time_fmt', 'logistics_status', 'greet_words', 'transfer_receiver_info', 'total_product_count', 'total_price', 'latest_logistic_info', 'create_time_str', 'amount_detail_map', 'extra_tag', 'shop_privilege_info_list', 'gift_receive_time_str', 'relate_infos', 'base_card', 'receiver_common', 'is_order_in_ab_test']

View File

@ -0,0 +1,58 @@
import requests
import json
import time
def test_douyin_sync():
# 用户提供的 Cookie
cookie = "zsgw_business_data=%7B%22uuid%22%3A%2286276357-089d-4844-93c9-bdf1b1e65ee6%22%2C%22platform%22%3A%22pc%22%2C%22source%22%3A%22bd.pcpz.30%22%7D; source=bd.pcpz.30; x-web-secsdk-uid=09481e47-b8cf-4757-81e0-af505a39d0aa; Hm_lvt_b6520b076191ab4b36812da4c90f7a5e=1768049462; HMACCOUNT=7DD25A4453689E0B; passport_csrf_token=77c40059afcea4fc1706178e96bacfaa; passport_csrf_token_default=77c40059afcea4fc1706178e96bacfaa; ttcid=aa179f0f1c514923b6b7d7e853ce373737; tt_scid=mHXrvwiVzL6PDC4sh38F8CI6aSw5GAAubYwmtKSTljHim8X5v3lMpai6cQ8asHkc4337; odin_tt=da3f1dde2546c094ba6a800e6f3117975c0dbd2162b8bd5478ac08aca51302144994ec271048d05b02f5a5c5744e175a2a071a31db8ed24d7eeb5be0bd7d8967; passport_auth_status=ce4c2cd652aff0df69f57a3a27d28284%2C; passport_auth_status_ss=ce4c2cd652aff0df69f57a3a27d28284%2C; uid_tt=73e8562f3280861db5ec3669ea4d06c2; uid_tt_ss=73e8562f3280861db5ec3669ea4d06c2; sid_tt=1d3b3b3c38d3f42f40dc2b28191e5039; sessionid=1d3b3b3c38d3f42f40dc2b28191e5039; sessionid_ss=1d3b3b3c38d3f42f40dc2b28191e5039; is_staff_user=false; PHPSESSID=4e4e0f987481cbd3f8a98dbb1fce5901; PHPSESSID_SS=4e4e0f987481cbd3f8a98dbb1fce5901; ucas_c0=CkEKBTEuMC4wEKOIjriN6ZKxaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0DRlonLBkjRysXNBlC_vL6Ekt3t1GdYbhIUwSNcefkX8KzDmEbEw61q_XBD2c4; ucas_c0_ss=CkEKBTEuMC4wEKOIjriN6ZKxaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0DRlonLBkjRysXNBlC_vL6Ekt3t1GdYbhIUwSNcefkX8KzDmEbEw61q_XBD2c4; csrf_session_id=3e3be9049498206e97df3a6d41696fe9; s_v_web_id=verify_mk8b0ntk_qptpzrvX_hjTu_4WCq_9zwq_FXtbbba6u272; COMPASS_LUOPAN_DT=session_7593712980437549363; Hm_lpvt_b6520b076191ab4b36812da4c90f7a5e=1768379293; ttwid=1%7CS_Ap3Z1--fOYmwY3vpxK8a2XoNu3eVhAT5kqA5mLGv4%7C1768379293%7C0eeb178b9757c52e917b36207bd87835b0efc4989e8f4c12cd4059bf88c8e885; gfkadpd=4272,23756; ecom_gray_shop_id=156231010; sid_guard=1d3b3b3c38d3f42f40dc2b28191e5039%7C1768379311%7C5184000%7CSun%2C+15-Mar-2026+08%3A28%3A31+GMT; session_tlb_tag=sttt%7C18%7CHTs7PDjT9C9A3CsoGR5QOf_________O-D0GAd06qWfkOVxj-KjALh7_D9vaVt0zdqsst9p2yRQ%3D; sid_ucp_v1=1.0.0-KGQ5OWQyNzI1NmJiYWU2OWJkZWE3YmZjZmJmNmFhMzRiMmJjYjZkMWUKGQib1oDYuM3aBxCvp53LBhiwISAMOAZA9AcaAmhsIiAxZDNiM2IzYzM4ZDNmNDJmNDBkYzJiMjgxOTFlNTAzOQ; ssid_ucp_v1=1.0.0-KGQ5OWQyNzI1NmJiYWU2OWJkZWE3YmZjZmJmNmFhMzRiMmJjYjZkMWUKGQib1oDYuM3aBxCvp53LBhiwISAMOAZA9AcaAmhsIiAxZDNiM2IzYzM4ZDNmNDJmNDBkYzJiMjgxOTFlNTAzOQ; BUYIN_SASID=SID2_7595130123662917930"
url = "https://fxg.jinritemai.com/api/order/searchlist"
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Cookie": cookie,
"Referer": "https://fxg.jinritemai.com/ffa/morder/order/list"
}
params = {
"page": "0",
"pageSize": "10",
"order_by": "create_time",
"order": "desc",
"tab": "all",
"appid": "1",
"_bid": "ffa_order",
"aid": "4272"
}
print(f"正在请求: {url}")
print(f"参数: {json.dumps(params, indent=2, ensure_ascii=False)}")
try:
response = requests.get(url, headers=headers, params=params, timeout=30)
print(f"状态码: {response.status_code}")
try:
data = response.json()
if data.get("st") == 0 or data.get("code") == 0:
print("\n✅ 测试成功")
orders = data.get("data", [])
if orders:
first_order = orders[0]
print(f"订单字段: {list(first_order.keys())}")
if "sku_order_list" in first_order:
print(f"SKU 列表第一个子项字段: {list(first_order['sku_order_list'][0].keys())}")
print(f"Product ID 示例: {first_order['sku_order_list'][0].get('product_id')}")
else:
print(f"\n❌ 测试失败: {data.get('msg')}")
except json.JSONDecodeError:
print(f"\n❌ 解析 JSON 失败")
print(f"原始响应内容: {response.text[:500]}")
except Exception as e:
print(f"\n❌ 发送请求失败: {str(e)}")
if __name__ == "__main__":
test_douyin_sync()

BIN
tools/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,266 @@
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
)
func main() {
// Subcommands
verifyUnlimitedCmd := flag.NewFlagSet("verify-unlimited", flag.ExitOnError)
verifyIchibanCmd := flag.NewFlagSet("verify-ichiban", flag.ExitOnError)
inspectIchibanCmd := flag.NewFlagSet("inspect-ichiban", flag.ExitOnError)
verifyFileCmd := flag.NewFlagSet("verify-file", flag.ExitOnError)
// Unlimited Args
vuSeed := verifyUnlimitedCmd.String("seed", "", "Server Seed (Hex)")
vuIssue := verifyUnlimitedCmd.Int64("issue", 0, "Issue ID")
vuUser := verifyUnlimitedCmd.Int64("user", 0, "User ID")
vuSalt := verifyUnlimitedCmd.String("salt", "", "Salt (Hex)")
vuWeights := verifyUnlimitedCmd.String("weights", "", "Rewards (Format: ID:Weight,ID:Weight...)")
// Ichiban Args
viSeed := verifyIchibanCmd.String("seed", "", "Server Seed (Hex)")
viIssue := verifyIchibanCmd.Int64("issue", 0, "Issue ID")
viSlot := verifyIchibanCmd.Int("slot", 0, "Selected Slot (1-based)")
viRewards := verifyIchibanCmd.String("rewards", "", "Rewards (Format: ID:Count,ID:Count...)")
// Inspect Ichiban Args
iiSeed := inspectIchibanCmd.String("seed", "", "Server Seed (Hex)")
iiIssue := inspectIchibanCmd.Int64("issue", 0, "Issue ID")
iiRewards := inspectIchibanCmd.String("rewards", "", "Rewards (Format: ID:Count,ID:Count...)")
// JSON File Args
vfPath := verifyFileCmd.String("path", "", "Path to JSON receipt file")
vfWeights := verifyFileCmd.String("weights", "", "Global Weights for Unlimited (Format: ID:Weight...)")
vfRewards := verifyFileCmd.String("rewards", "", "Global Rewards for Ichiban (Format: ID:Count...)")
if len(os.Args) < 2 {
printUsage()
return
}
switch os.Args[1] {
case "verify-unlimited":
verifyUnlimitedCmd.Parse(os.Args[2:])
runUnlimited(*vuSeed, *vuIssue, *vuUser, *vuSalt, *vuWeights)
case "verify-ichiban":
verifyIchibanCmd.Parse(os.Args[2:])
runIchiban(*viSeed, *viIssue, *viSlot, *viRewards)
case "inspect-ichiban":
inspectIchibanCmd.Parse(os.Args[2:])
runInspectIchiban(*iiSeed, *iiIssue, *iiRewards)
case "verify-file":
verifyFileCmd.Parse(os.Args[2:])
runVerifyFile(*vfPath, *vfWeights, *vfRewards)
default:
printUsage()
}
}
func printUsage() {
fmt.Println("BindBox Lottery Verifier Tool (v1.2)")
fmt.Println("\nUsage:")
fmt.Println(" verify-unlimited --seed <hex> --issue <id> --user <id> --salt <hex> --weights <list>")
fmt.Println(" verify-ichiban --seed <hex> --issue <id> --slot <num> --rewards <list>")
fmt.Println(" inspect-ichiban --seed <hex> --issue <id> --rewards <list> (Dump all slots)")
fmt.Println(" verify-file --path <json_file> [--weights <list>] (Load from JSON, support array)")
fmt.Println("\nExample Unlimited:")
fmt.Println(" verify-unlimited --seed aabbcc... --issue 1001 --user 888 --salt 1234... --weights 1:10,2:50,3:100")
fmt.Println("\nExample File (with global weights):")
fmt.Println(" verify-file --path receipts.json --weights \"280:88200,281:100...\"")
}
func runUnlimited(seed string, issue int64, user int64, salt string, weightsStr string) {
if seed == "" || issue == 0 || user == 0 || salt == "" || weightsStr == "" {
fmt.Println("Error: Missing required arguments.")
return
}
rewards, err := ParseRewardsString(weightsStr)
if err != nil {
fmt.Printf("Error parsing weights: %v\n", err)
return
}
fmt.Println("========================================")
fmt.Println(" UNLIMITED LOTTERY VERIFICATION ")
fmt.Println("========================================")
fmt.Printf("Server Seed : %s\n", seed)
fmt.Printf("Issue ID : %d\n", issue)
fmt.Printf("User ID : %d\n", user)
fmt.Printf("Salt : %s\n", salt)
fmt.Println("----------------------------------------")
id, log, err := VerifyUnlimited(seed, issue, user, salt, rewards)
if err != nil {
fmt.Printf("Verification FAILED: %v\n", err)
return
}
fmt.Println(log)
fmt.Println("----------------------------------------")
fmt.Printf("VERIFIED RESULT: Reward ID = %d\n", id)
fmt.Println("========================================")
}
func runIchiban(seed string, issue int64, slot int, rewardsStr string) {
if seed == "" || issue == 0 || slot == 0 || rewardsStr == "" {
fmt.Println("Error: Missing required arguments.")
return
}
rewards, err := ParseRewardsString(rewardsStr)
if err != nil {
fmt.Printf("Error parsing rewards: %v\n", err)
return
}
fmt.Println("========================================")
fmt.Println(" ICHIBAN LOTTERY VERIFICATION ")
fmt.Println("========================================")
fmt.Printf("Server Seed : %s\n", seed)
fmt.Printf("Issue ID : %d\n", issue)
fmt.Printf("Values : %d unique items expanded\n", len(rewards))
fmt.Println("----------------------------------------")
id, log, err := VerifyIchiban(seed, issue, slot, rewards)
if err != nil {
fmt.Printf("Verification FAILED: %v\n", err)
return
}
fmt.Println(log)
fmt.Println("----------------------------------------")
fmt.Printf("VERIFIED RESULT: Reward ID = %d\n", id)
fmt.Println("========================================")
}
func runInspectIchiban(seed string, issue int64, rewardsStr string) {
if seed == "" || issue == 0 || rewardsStr == "" {
fmt.Println("Error: Missing required arguments.")
return
}
rewards, err := ParseRewardsString(rewardsStr)
if err != nil {
fmt.Printf("Error parsing rewards: %v\n", err)
return
}
fmt.Println("========================================")
fmt.Println(" ICHIBAN GLOBAL INSPECTION ")
fmt.Println("========================================")
fmt.Printf("Server Seed : %s\n", seed)
fmt.Printf("Issue ID : %d\n", issue)
fmt.Println("----------------------------------------")
var slots []RewardItem
for _, r := range rewards {
for k := 0; k < r.Count; k++ {
slots = append(slots, r)
}
}
totalSlots := len(slots)
// Shuffle
seedKey, _ := decodeHex(seed)
// Create indices mapping
indices := make([]int, totalSlots)
for i := 0; i < totalSlots; i++ {
indices[i] = i
}
mac := hmac.New(sha256.New, seedKey)
// Reconstruct actual items
workingSlots := make([]RewardItem, totalSlots)
copy(workingSlots, slots)
for i := totalSlots - 1; i > 0; i-- {
mac.Reset()
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issue)))
sum := mac.Sum(nil)
j := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
workingSlots[i], workingSlots[j] = workingSlots[j], workingSlots[i]
}
// Print Grid
fmt.Printf("%-10s | %-10s | %s\n", "SLOT (NO.)", "REWARD ID", "NAME")
fmt.Println(strings.Repeat("-", 40))
for i, item := range workingSlots {
fmt.Printf("%-10d | %-10d | %s\n", i+1, item.ID, item.Name)
}
fmt.Println("========================================")
}
func runVerifyFile(path string, globalWeights string, globalRewards string) {
if path == "" {
fmt.Println("Error: Missing file path.")
return
}
data, err := os.ReadFile(path)
if err != nil {
fmt.Printf("Error reading file: %v\n", err)
return
}
type Receipt struct {
Mode string `json:"mode"`
Seed string `json:"seed"`
IssueID int64 `json:"issue_id"`
UserID int64 `json:"user_id"`
Salt string `json:"salt"`
Weights string `json:"weights"`
SlotIndex int `json:"slot_index"`
Rewards string `json:"rewards"`
}
var receipts []Receipt
// Try parsing as array first
if err := json.Unmarshal(data, &receipts); err != nil {
// Try parsing as single object
var single Receipt
if err2 := json.Unmarshal(data, &single); err2 != nil {
fmt.Printf("Error parsing JSON (tried both array and object): %v\n", err)
return
}
receipts = append(receipts, single)
}
fmt.Printf("Loaded %d receipt(s) from file.\n", len(receipts))
for i, r := range receipts {
fmt.Printf("\n>>> Verifying Receipt #%d <<<\n", i+1)
if r.Mode == "unlimited" {
w := r.Weights
if w == "" {
w = globalWeights
if w != "" {
fmt.Println("(Using global weights)")
}
}
runUnlimited(r.Seed, r.IssueID, r.UserID, r.Salt, w)
} else if r.Mode == "ichiban" {
rew := r.Rewards
if rew == "" {
rew = globalRewards
if rew != "" {
fmt.Println("(Using global rewards)")
}
}
runIchiban(r.Seed, r.IssueID, r.SlotIndex, rew)
} else {
fmt.Printf("Unknown or missing mode in JSON: %s\n", r.Mode)
}
}
}
// Helper to decode Hex inside main pkg if needed,
// but verify.go already has decodeHex.
// As they are in the same package 'main', main.go can call functions in verify.go

View File

@ -0,0 +1,206 @@
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"sort"
"strconv"
"strings"
)
// RewardItem 简化的奖品结构
type RewardItem struct {
ID int64
Name string
Weight int
Count int // for ichiban expansion
}
// VerifyUnlimited 验证无限赏结果
func VerifyUnlimited(seedHex string, issueID int64, userID int64, saltHex string, rewards []RewardItem) (int64, string, error) {
// 1. Decode inputs
seedKey, err := decodeHex(seedHex)
if err != nil {
return 0, "", fmt.Errorf("invalid seed: %v", err)
}
salt, err := decodeHex(saltHex)
if err != nil {
return 0, "", fmt.Errorf("invalid salt: %v", err)
}
// Sort rewards by ID to ensure consistency with backend (which usually iterates by ID)
// This fixes issues where input JSON is not sorted.
// Note: For Ichiban, sorting might be more complex (see VerifyIchiban comments).
// But for Unlimited, ID sort is the confirmed behavior.
sortRewardsByID(rewards)
// 2. Calculate Total Weight
var totalWeight int64
for _, r := range rewards {
totalWeight += int64(r.Weight)
}
if totalWeight <= 0 {
return 0, "", errors.New("total weight must be > 0")
}
// 3. HMAC Logic (Same as strategy/default.go)
mac := hmac.New(sha256.New, seedKey)
mac.Write([]byte(fmt.Sprintf("draw:issue:%d|user:%d|salt:%x", issueID, userID, salt)))
sum := mac.Sum(nil)
rnd := int64(binary.BigEndian.Uint64(sum[:8]) % uint64(totalWeight))
// 4. Select Reward
var acc int64
var pickedID int64
var pickedName string
processLog := fmt.Sprintf("Total Weight: %d\nRandom Value: %d\n", totalWeight, rnd)
for _, r := range rewards {
acc += int64(r.Weight)
if rnd < acc {
pickedID = r.ID
pickedName = r.Name
processLog += fmt.Sprintf("WIN: [%d] %s (Range: %d-%d)\n", r.ID, r.Name, acc-int64(r.Weight), acc)
processLog += fmt.Sprintf("Selected Item Name: %s\n", pickedName)
break
}
}
return pickedID, processLog, nil
}
// VerifyIchiban 验证一番赏结果
func VerifyIchiban(seedHex string, issueID int64, slotIndex int, rewardsInput []RewardItem) (int64, string, error) {
// 1. Expand Rewards to Slots
// 一番赏逻辑:将 A:2, B:3 展开为 [A, A, B, B, B]
// 且排序必须确定ID升序 或 输入顺序? Backend is: IsBoss Desc, Level Asc, Sort Asc, ID Asc.
// 验证工具这里简单起见,假设输入已经是有序的配置列表
var slots []RewardItem
for _, r := range rewardsInput {
for k := 0; k < r.Count; k++ {
slots = append(slots, r)
}
}
totalSlots := len(slots)
if totalSlots == 0 {
return 0, "", errors.New("no slots generated from rewards")
}
if slotIndex < 1 || slotIndex > totalSlots {
return 0, "", fmt.Errorf("slot index %d out of range [1, %d]", slotIndex, totalSlots)
}
// 2. Decode Seed
seedKey, err := decodeHex(seedHex)
if err != nil {
return 0, "", fmt.Errorf("invalid seed: %v", err)
}
// 3. Shuffle (Same as strategy/ichiban.go)
// Create a mapping index array to shuffle
indices := make([]int, totalSlots)
for i := 0; i < totalSlots; i++ {
indices[i] = i
}
mac := hmac.New(sha256.New, seedKey)
for i := totalSlots - 1; i > 0; i-- {
mac.Reset()
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issueID)))
sum := mac.Sum(nil)
// j := rnd % (i+1)
j := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
indices[i], indices[j] = indices[j], indices[i]
}
// 4. Get Result
// slotIndex is 1-based from user input
// mapped index is indices[slotIndex-1]
// BUT wait, backend: `picked = slots[slotIndex]` (after shuffle logic applied to `slots`)
// In the backend implementation:
/*
slots := ... // filled
for i:=... {
swap(slots[i], slots[j])
}
picked := slots[slotIndex] // slotIndex passed from req (0-based inside function usually? Let's check backend)
*/
// Checked backend: `req.SlotIndex` comes from frontend.
// `validateIchibanSlots` checks `si < 1`. Code uses `selectedSlots = append(..., si-1)`.
// `SelectItemBySlot` uses `slotIndex` arg.
// So `slotIndex` in `SelectItemBySlot` is 0-based.
// Shuffle operates on `slots`. Finally returns `slots[slotIndex]`.
// This means "Slot N" (user chosen) always points to PHYSICAL position N.
// The CONTENT of position N changes after shuffle.
// OK, my implementation below uses `indices` to track movements, let's align.
// Re-implement exactly as backend: shuffle the actual slice
workingSlots := make([]RewardItem, totalSlots)
copy(workingSlots, slots)
for i := totalSlots - 1; i > 0; i-- {
mac.Reset()
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issueID)))
sum := mac.Sum(nil)
j := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
workingSlots[i], workingSlots[j] = workingSlots[j], workingSlots[i]
}
finalReward := workingSlots[slotIndex-1] // 1-based input -> 0-based index
log := fmt.Sprintf("Total Slots: %d\nUser Selected Slot: %d\nResult Reward: [%d] %s\n", totalSlots, slotIndex, finalReward.ID, finalReward.Name)
return finalReward.ID, log, nil
}
// Helpers
func decodeHex(s string) ([]byte, error) {
if len(s)%2 != 0 {
return nil, errors.New("hex string length must be even")
}
// naive hex decode or use pkg
// here reusing simple implementation or adding imports
return parseHex(s)
}
func parseHex(s string) ([]byte, error) {
return hex.DecodeString(s)
}
// ParseRewardsString parses "ID:Weight,ID:Weight" or "ID:Name:Weight"
func ParseRewardsString(s string) ([]RewardItem, error) {
parts := strings.Split(s, ",")
var res []RewardItem
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
// Format: ID:Weight or ID:Name:Weight
sub := strings.Split(p, ":")
if len(sub) == 2 {
id, _ := strconv.ParseInt(sub[0], 10, 64)
w, _ := strconv.Atoi(sub[1])
res = append(res, RewardItem{ID: id, Name: fmt.Sprintf("Item-%d", id), Weight: w, Count: w})
} else if len(sub) == 3 {
id, _ := strconv.ParseInt(sub[0], 10, 64)
name := sub[1]
w, _ := strconv.Atoi(sub[2])
res = append(res, RewardItem{ID: id, Name: name, Weight: w, Count: w})
}
}
return res, nil
}
func sortRewardsByID(rewards []RewardItem) {
sort.Slice(rewards, func(i, j int) bool {
return rewards[i].ID < rewards[j].ID
})
}

View File

@ -0,0 +1,785 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>柯大鸭 抽奖验证工具</title>
<style>
:root {
--primary: #6366F1;
--primary-light: #818CF8;
--success: #10B981;
--error: #EF4444;
--warning: #F59E0B;
--bg: #0F172A;
--bg-card: #1E293B;
--bg-input: #334155;
--text: #F8FAFC;
--text-muted: #94A3B8;
--border: #475569;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 24px;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 32px;
padding: 24px;
background: linear-gradient(135deg, var(--primary) 0%, #8B5CF6 100%);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.3);
}
header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
}
header p {
opacity: 0.9;
font-size: 14px;
}
.card {
background: var(--bg-card);
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
border: 1px solid var(--border);
}
.card-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.tab {
flex: 1;
padding: 12px 16px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-muted);
cursor: pointer;
text-align: center;
font-size: 14px;
transition: all 0.2s;
}
.tab.active {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.tab:hover:not(.active) {
background: var(--border);
}
.input-group {
margin-bottom: 16px;
}
.input-group label {
display: block;
font-size: 14px;
color: var(--text-muted);
margin-bottom: 6px;
}
.input-group input,
.input-group textarea,
.input-group select {
width: 100%;
padding: 12px 16px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 14px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
transition: border-color 0.2s;
}
.input-group textarea {
min-height: 150px;
resize: vertical;
}
.input-group input:focus,
.input-group textarea:focus {
outline: none;
border-color: var(--primary);
}
.input-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 28px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, #8B5CF6 100%);
color: #fff;
width: 100%;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.result-card {
display: none;
}
.result-card.show {
display: block;
}
.result-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.result-header.success {
background: rgba(16, 185, 129, 0.15);
border: 1px solid var(--success);
}
.result-header.error {
background: rgba(239, 68, 68, 0.15);
border: 1px solid var(--error);
}
.result-icon {
font-size: 32px;
}
.result-title {
font-size: 18px;
font-weight: 600;
}
.result-subtitle {
font-size: 14px;
color: var(--text-muted);
}
.detail-section {
background: var(--bg);
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
.detail-section h4 {
font-size: 14px;
color: var(--text-muted);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--border);
font-size: 14px;
}
.detail-row:last-child {
border-bottom: none;
}
.detail-row .label {
color: var(--text-muted);
}
.detail-row .value {
font-family: 'Monaco', 'Menlo', monospace;
color: var(--text);
word-break: break-all;
text-align: right;
max-width: 60%;
}
.prize-info {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
border-radius: 12px;
margin-top: 16px;
}
.prize-image {
width: 120px;
height: 120px;
border-radius: 12px;
object-fit: cover;
background: var(--bg);
border: 2px solid var(--border);
}
.prize-details {
flex: 1;
}
.prize-name {
font-size: 20px;
font-weight: 700;
color: var(--text);
margin-bottom: 8px;
line-height: 1.3;
}
.prize-id-label {
font-size: 14px;
color: var(--text-muted);
}
.prize-id-label span {
font-family: 'Monaco', 'Menlo', monospace;
color: var(--primary-light);
font-weight: 600;
}
.help-text {
font-size: 12px;
color: var(--text-muted);
margin-top: 8px;
}
.config-status {
margin-top: 12px;
padding: 10px 14px;
border-radius: 6px;
font-size: 13px;
display: none;
}
.config-status.valid {
display: block;
background: rgba(16, 185, 129, 0.15);
border: 1px solid var(--success);
color: var(--success);
}
.config-status.invalid {
display: block;
background: rgba(239, 68, 68, 0.15);
border: 1px solid var(--error);
color: var(--error);
}
.step-indicator {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--primary);
color: #fff;
text-align: center;
line-height: 24px;
font-size: 12px;
font-weight: 600;
margin-right: 8px;
}
.hidden {
display: none !important;
}
footer {
text-align: center;
padding: 24px;
color: var(--text-muted);
font-size: 12px;
}
footer a {
color: var(--primary-light);
text-decoration: none;
}
@media (max-width: 600px) {
body {
padding: 16px;
}
.input-row {
grid-template-columns: 1fr;
}
header h1 {
font-size: 22px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🎰 柯大鸭 抽奖验证工具</h1>
<p>离线验证抽奖结果公平性 · 无需网络连接</p>
</header>
<!-- 步骤1: 活动配置 -->
<div class="card">
<div class="card-title">📦 步骤 1: 导入活动配置</div>
<p class="help-text" style="margin-bottom: 12px;">从后台活动管理页面复制完整配置 JSON</p>
<div class="input-group">
<label>活动配置 JSON包含奖品名称和图片</label>
<textarea id="activityConfig" placeholder='{
"weights": "1:100,2:500",
"rewards": [
{"id": 1, "name": "奖品A", "image": "...", "weight": 100}
]
}'></textarea>
</div>
<div id="configStatus" class="config-status"></div>
</div>
<!-- 步骤2: 抽奖凭证 -->
<div class="card">
<div class="card-title">🎫 步骤 2: 导入抽奖凭证</div>
<p class="help-text" style="margin-bottom: 12px;">从小程序订单详情复制验证凭据 JSON</p>
<div class="input-group">
<label>验证凭据 JSON</label>
<textarea id="jsonInput" placeholder='{
"seed": "aabbccdd...",
"issue_id": 1001,
"user_id": 12345,
"salt": "1234abcd"
}'></textarea>
</div>
<button class="btn btn-primary" onclick="verify()">
🔍 开始验证
</button>
</div>
<!-- 结果区域 -->
<div id="resultsContainer"></div>
<footer>
<p>柯大鸭 抽奖公平性验证工具 v1.1</p>
<p>采用 HMAC-SHA256 承诺机制 · 完全离线运行</p>
</footer>
</div>
<script>
// Hex 解码
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes;
}
// BigEndian Uint64
function bigEndianUint64(buffer) {
const view = new DataView(buffer);
const high = view.getUint32(0);
const low = view.getUint32(4);
return BigInt(high) * BigInt(0x100000000) + BigInt(low);
}
// HMAC-SHA256
async function hmacSha256(keyHex, message) {
const keyBytes = hexToBytes(keyHex);
const encoder = new TextEncoder();
const msgBytes = encoder.encode(message);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, msgBytes);
return new Uint8Array(signature);
}
// 解析奖品配置(自动排序)
function parseRewards(str) {
if (!str) return [];
const list = str.split(',').map(part => {
const [id, value] = part.trim().split(':');
return { id: parseInt(id), weight: parseInt(value), count: parseInt(value) };
}).filter(r => r.id && r.weight);
// 关键按ID排序以匹配后端逻辑
list.sort((a, b) => a.id - b.id);
return list;
}
// 全局存储活动配置
let activityConfig = null;
// 验证并显示活动配置状态
function validateConfig() {
const input = document.getElementById('activityConfig');
const status = document.getElementById('configStatus');
const configStr = input.value.trim();
if (!configStr) {
status.className = 'config-status';
status.textContent = '';
activityConfig = null;
return;
}
try {
const config = JSON.parse(configStr);
// 检查是否有 weights 字段或 rewards 数组
if (config.weights && config.rewards && Array.isArray(config.rewards)) {
activityConfig = config;
// 如果config里没有weights字符串我们自己生成一个按ID排序
// 但通常应该有。为了保险,重新解析并排序
const rewards = parseRewards(config.weights);
// 更新weights字符串为排序后的
activityConfig.weights = rewards.map(r => `${r.id}:${r.weight}`).join(',');
const totalWeight = rewards.reduce((sum, r) => sum + r.weight, 0);
status.className = 'config-status valid';
status.textContent = `✅ 已识别 ${config.rewards.length} 个奖品,总权重: ${totalWeight}`;
} else if (config.weights) {
// 只有 weights 字符串
const rewards = parseRewards(config.weights);
if (rewards.length > 0) {
activityConfig = { weights: config.weights, rewards: rewards };
// 更新weights字符串为排序后的
activityConfig.weights = rewards.map(r => `${r.id}:${r.weight}`).join(',');
const totalWeight = rewards.reduce((sum, r) => sum + r.weight, 0);
status.className = 'config-status valid';
status.textContent = `✅ 已识别 ${rewards.length} 个奖品,总权重: ${totalWeight}`;
} else {
throw new Error('无效的权重配置');
}
} else {
throw new Error('缺少 weights 字段');
}
} catch (e) {
// 尝试作为简单的权重字符串解析
const rewards = parseRewards(configStr);
if (rewards.length > 0) {
const sortedWeightsStr = rewards.map(r => `${r.id}:${r.weight}`).join(',');
activityConfig = { weights: sortedWeightsStr, rewards: rewards };
const totalWeight = rewards.reduce((sum, r) => sum + r.weight, 0);
status.className = 'config-status valid';
status.textContent = `✅ 已识别 ${rewards.length} 个奖品,总权重: ${totalWeight}`;
} else {
status.className = 'config-status invalid';
status.textContent = '❌ 配置格式无效,请粘贴后台导出的 JSON';
activityConfig = null;
}
}
}
// 初始化事件监听
document.addEventListener('DOMContentLoaded', function () {
const configInput = document.getElementById('activityConfig');
configInput.addEventListener('input', validateConfig);
configInput.addEventListener('paste', function () {
setTimeout(validateConfig, 10);
});
});
// 无限赏验证
async function verifyUnlimited(seed, issueId, userId, salt, weightsStr, rewardsInfo) {
const rewards = parseRewards(weightsStr);
if (rewards.length === 0) {
throw new Error('奖品权重配置无效');
}
const totalWeight = rewards.reduce((sum, r) => sum + r.weight, 0);
if (totalWeight <= 0) {
throw new Error('总权重必须大于 0');
}
// 构建 payload 并计算 HMAC
const payload = `draw:issue:${issueId}|user:${userId}|salt:${salt}`;
const hmac = await hmacSha256(seed, payload);
// 提取随机数
const randValue = bigEndianUint64(hmac.buffer) % BigInt(totalWeight);
const rnd = Number(randValue);
// 选择奖品
let acc = 0;
let pickedReward = null;
let rangeInfo = '';
for (const r of rewards) {
acc += r.weight;
if (rnd < acc) {
pickedReward = r;
rangeInfo = `${acc - r.weight} - ${acc}`;
break;
}
}
// 查找奖品详细信息
let prizeName = '';
let prizeImage = '';
if (pickedReward && rewardsInfo) {
// rewardsInfo 可能有 id 字段也可能没有,尝试匹配 id
const info = rewardsInfo.find(ri => (ri.id || ri.ID) === pickedReward.id);
if (info) {
prizeName = info.name || info.product_name || '';
// 处理图片可能是JSON数组字符串
const img = info.image || info.product_image_url;
if (img) {
try {
const images = JSON.parse(img);
prizeImage = Array.isArray(images) ? images[0] : img;
} catch {
prizeImage = img;
}
}
}
}
return {
mode: 'unlimited',
success: true,
rewardId: pickedReward ? pickedReward.id : 0,
prizeName: prizeName,
prizeImage: prizeImage,
details: [
{ label: '验证模式', value: '无限赏 (Weighted Random)' },
{ label: 'Payload', value: payload },
{ label: '总权重', value: totalWeight.toString() },
{ label: '随机数', value: rnd.toString() },
{ label: '命中区间', value: rangeInfo },
{ label: '奖品数量', value: rewards.length.toString() }
]
};
}
// 主验证函数
async function verify() {
try {
const resultsContainer = document.getElementById('resultsContainer');
resultsContainer.innerHTML = ''; // 清空之前结果
// 步骤1: 获取活动配置 (全局)
const globalWeightsStr = activityConfig ? activityConfig.weights : '';
const globalRewardsInfo = activityConfig ? activityConfig.rewards : [];
if (!globalWeightsStr) {
// 如果没提供全局配置,必须确保凭证里有配置
}
// 步骤2: 获取抽奖凭证
const jsonStr = document.getElementById('jsonInput').value.trim();
if (!jsonStr) {
throw new Error('请导入抽奖凭证步骤2');
}
let paramsList;
try {
const parsed = JSON.parse(jsonStr);
if (Array.isArray(parsed)) {
paramsList = parsed;
} else {
paramsList = [parsed];
}
} catch (e) {
throw new Error('凭证 JSON 格式无效');
}
// 批量验证
let allResults = [];
for (let i = 0; i < paramsList.length; i++) {
const params = paramsList[i];
try {
// 验证必要参数
if (!params.seed) throw new Error('缺少 seed');
if (!params.issue_id && params.issue_id !== 0) throw new Error('缺少 issue_id');
// 决定使用的权重配置
let currentWeightsStr = params.weights || globalWeightsStr;
if (!currentWeightsStr) {
throw new Error('未找到奖品权重配置 (请在步骤1导入或确保凭证包含配置)');
}
// 执行验证
const result = await verifyUnlimited(
params.seed,
params.issue_id,
params.user_id || 0,
params.salt || '',
currentWeightsStr,
globalRewardsInfo // 优先用全局的图片信息
);
// 附加索引
result.index = i + 1;
result.drawIndex = params.draw_index || (i + 1);
allResults.push(result);
} catch (err) {
allResults.push({
index: i + 1,
success: false,
error: err.message
});
}
}
// 显示所有结果
showResults(allResults);
} catch (err) {
// 全局错误如JSON解析失败
const resultsContainer = document.getElementById('resultsContainer');
resultsContainer.innerHTML = `
<div class="card result-card show">
<div class="result-header error">
<span class="result-icon"></span>
<div>
<div class="result-title">验证无法执行</div>
<div class="result-subtitle">${err.message}</div>
</div>
</div>
</div>
`;
// 滚动到结果
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
// 显示结果列表
function showResults(results) {
const container = document.getElementById('resultsContainer');
container.innerHTML = '';
results.forEach(res => {
const card = document.createElement('div');
card.className = 'card result-card show';
card.style.marginBottom = '20px';
if (res.success) {
const detailHtml = res.details.map(d => `
<div class="detail-row">
<span class="label">${d.label}</span>
<span class="value">${d.value}</span>
</div>
`).join('');
const imgHtml = res.prizeImage
? `<img class="prize-image" src="${res.prizeImage}" alt="奖品图片" style="display:block">`
: `<img class="prize-image" style="display:none">`;
const nameHtml = res.prizeName || ('奖品 #' + res.rewardId);
card.innerHTML = `
<div class="card-title">📊 第 ${res.drawIndex} 抽验证结果</div>
<div class="result-header success">
<span class="result-icon"></span>
<div>
<div class="result-title">验证通过</div>
<div class="result-subtitle">ID: ${res.rewardId} - ${nameHtml}</div>
</div>
</div>
<div class="prize-info">
${imgHtml}
<div class="prize-details">
<div class="prize-name">${nameHtml}</div>
<div class="prize-id-label">奖品 ID: <span>${res.rewardId}</span></div>
</div>
</div>
<div class="detail-section">
<h4>计算详情</h4>
${detailHtml}
</div>
`;
} else {
card.innerHTML = `
<div class="card-title">📊 第 ${res.index} 抽</div>
<div class="result-header error">
<span class="result-icon"></span>
<div>
<div class="result-title">验证失败</div>
<div class="result-subtitle">${res.error}</div>
</div>
</div>
`;
}
container.appendChild(card);
});
// 滚动到结果
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
</script>
</body>
</html>

40
tools/query_order.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
# 查询订单抽奖记录
mysql -h 150.158.78.154 -P 3306 -u root -p'bindbox2025kdy' bindbox_game -e "
SELECT
o.id as order_id,
o.order_no,
o.user_id,
o.activity_id,
o.issue_id,
dl.id as draw_log_id,
dl.reward_id,
dl.created_at as draw_time,
ar.product_id,
ar.weight,
p.name as reward_name
FROM orders o
LEFT JOIN activity_draw_logs dl ON dl.order_id = o.id
LEFT JOIN activity_rewards ar ON ar.id = dl.reward_id
LEFT JOIN products p ON p.id = ar.product_id
WHERE o.order_no = 'O20260115145414801';
"
echo ""
echo "=== 查询抽奖凭证 ==="
mysql -h 150.158.78.154 -P 3306 -u root -p'bindbox2025kdy' bindbox_game -e "
SELECT
dr.id,
dr.draw_log_id,
dr.algo_version,
dr.server_seed_hash,
dr.server_sub_seed,
dr.rand_salt,
dr.weights_snapshot,
dr.created_at
FROM activity_draw_receipts dr
JOIN activity_draw_logs dl ON dl.id = dr.draw_log_id
JOIN orders o ON o.id = dl.order_id
WHERE o.order_no = 'O20260115145414801';
"

99
tools/query_order/main.go Normal file
View File

@ -0,0 +1,99 @@
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
dsn := "root:bindbox2025kdy@tcp(150.158.78.154:3306)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal("连接失败:", err)
}
defer db.Close()
orderNo := "O20260115145414801"
// 查询订单和抽奖记录
fmt.Println("=== 订单抽奖记录 ===")
rows, err := db.Query(`
SELECT
o.id as order_id,
o.order_no,
o.user_id,
dl.id as draw_log_id,
dl.reward_id,
dl.issue_id,
dl.created_at as draw_time,
ar.product_id,
ar.weight,
p.name as reward_name
FROM orders o
LEFT JOIN activity_draw_logs dl ON dl.order_id = o.id
LEFT JOIN activity_reward_settings ar ON ar.id = dl.reward_id
LEFT JOIN products p ON p.id = ar.product_id
WHERE o.order_no = ?
`, orderNo)
if err != nil {
log.Fatal("查询订单失败:", err)
}
defer rows.Close()
for rows.Next() {
var orderID, userID, drawLogID, rewardID, issueID, productID sql.NullInt64
var orderNoRes, drawTime, rewardName sql.NullString
var weight sql.NullInt64
err := rows.Scan(&orderID, &orderNoRes, &userID, &drawLogID, &rewardID, &issueID, &drawTime, &productID, &weight, &rewardName)
if err != nil {
log.Fatal("扫描失败:", err)
}
fmt.Printf("订单ID: %d, 用户ID: %d, 期次ID: %d\n", orderID.Int64, userID.Int64, issueID.Int64)
fmt.Printf("抽奖记录ID: %d, 奖品ID: %d, 产品ID: %d, 权重: %d\n", drawLogID.Int64, rewardID.Int64, productID.Int64, weight.Int64)
fmt.Printf("中奖奖品: %s, 抽奖时间: %s\n", rewardName.String, drawTime.String)
}
// 查询抽奖凭证
fmt.Println("\n=== 抽奖凭证详情 ===")
receiptRows, err := db.Query(`
SELECT
dr.id,
dr.draw_log_id,
dr.algo_version,
dr.server_seed_hash,
dr.server_sub_seed,
dr.rand_salt,
dr.weights_snapshot,
dr.rand_value,
dr.result_reward_id
FROM activity_draw_receipts dr
JOIN activity_draw_logs dl ON dl.id = dr.draw_log_id
JOIN orders o ON o.id = dl.order_id
WHERE o.order_no = ?
`, orderNo)
if err != nil {
log.Fatal("查询凭证失败:", err)
}
defer receiptRows.Close()
for receiptRows.Next() {
var id, drawLogID, resultRewardID sql.NullInt64
var algoVersion, seedHash, subSeed, salt, weights sql.NullString
var randValue sql.NullInt64
err := receiptRows.Scan(&id, &drawLogID, &algoVersion, &seedHash, &subSeed, &salt, &weights, &randValue, &resultRewardID)
if err != nil {
log.Fatal("扫描凭证失败:", err)
}
fmt.Printf("凭证ID: %d, 抽奖记录ID: %d\n", id.Int64, drawLogID.Int64)
fmt.Printf("算法版本: %s\n", algoVersion.String)
fmt.Printf("种子哈希: %s\n", seedHash.String)
fmt.Printf("子种子: %s\n", subSeed.String)
fmt.Printf("Salt: %s\n", salt.String)
fmt.Printf("随机值: %d\n", randValue.Int64)
fmt.Printf("结果奖品ID: %d\n", resultRewardID.Int64)
fmt.Printf("权重快照: %s\n", weights.String)
}
}

76
tools/verify_seed/main.go Normal file
View File

@ -0,0 +1,76 @@
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"sort"
)
func main() {
// 数据库查到的正确seed (活动76)
seedHex := "162B08DAC70849C68FDBF499583199B9500EE96CCCFC64511719B54FC7D9AFC9"
seed, _ := hex.DecodeString(seedHex)
issueID := int64(83)
userID := int64(9054)
saltHex := "c4e144b991600f5a5a316d55b6929d19"
// payload格式
payload := fmt.Sprintf("draw:issue:%d|user:%d|salt:%s", issueID, userID, saltHex)
fmt.Printf("Seed: %s\n", seedHex)
fmt.Printf("Payload: %s\n", payload)
mac := hmac.New(sha256.New, seed)
mac.Write([]byte(payload))
sum := mac.Sum(nil)
totalWeight := int64(100000)
rnd := int64(binary.BigEndian.Uint64(sum[:8]) % uint64(totalWeight))
fmt.Printf("HMAC: %x\n", sum)
fmt.Printf("计算随机数: %d\n", rnd)
fmt.Printf("数据库记录: 90555\n")
if rnd == 90555 {
fmt.Println("\n✅ 随机数匹配!")
} else {
fmt.Println("\n❌ 随机数不匹配")
}
// 用计算的随机数选择奖品
// 用计算的随机数选择奖品
weights := []struct {
ID int64
Weight int64
Name string
}{
{280, 88200, "捏捏"},
{281, 100, "魔灵高达"},
{282, 6500, "高达徽章"},
{283, 3200, "打磨工具五件套"},
{284, 800, "SD随机款"},
{285, 800, "EG创制强袭高达"},
{286, 100, "V2高达AB型"},
{429, 100, "EG创制强袭超银河"},
{430, 100, "战国异端顽驮无"},
{431, 100, "牛高达"},
}
// 按ID排序模拟后端逻辑
sort.Slice(weights, func(i, j int) bool {
return weights[i].ID < weights[j].ID
})
var acc int64
for _, w := range weights {
acc += w.Weight
if rnd < acc {
fmt.Printf("\n计算中奖: ID %d (%s)\n", w.ID, w.Name)
break
}
}
fmt.Println("数据库实际: ID 282 (高达徽章)")
}

133
tools/wechat_debug/main.go Normal file
View File

@ -0,0 +1,133 @@
package main
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"bindbox-game/internal/pkg/wechat"
)
func main() {
// 1. Load config
appID, appSecret := loadConfig("../../configs/fat_configs.toml")
if appID == "" || appSecret == "" {
fmt.Println("Failed to load Wechat config from ../../configs/fat_configs.toml")
// Fallback to current dir if run from root
appID, appSecret = loadConfig("configs/fat_configs.toml")
if appID == "" || appSecret == "" {
fmt.Println("Failed to load Wechat config from configs/fat_configs.toml")
return
}
}
fmt.Printf("Loaded Config: AppID=%s\n", appID)
// 2. Get Access Token
ctx := context.Background()
cfg := &wechat.WechatConfig{AppID: appID, AppSecret: appSecret}
at, err := wechat.GetAccessTokenWithContext(ctx, cfg)
if err != nil {
fmt.Printf("Failed to get access token: %v\n", err)
return
}
fmt.Println("Got Access Token successfully")
// 3. Test Params
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvd25lcl91c2VyX2lkIjo5MDUzLCJpbnZlbnRvcnlfaWQiOjI4NzQ5LCJleHAiOjE3Njc5MzIwOTcsIm5iZiI6MTc2NzkyODQ5NywiaWF0IjoxNzY3OTI4NDk3fQ.8dQuqbJj7hlDlBtdNkTXnJaq7y0qOwefzP5XbhSrE10"
pagePath := fmt.Sprintf("pages-user/address/submit?token=%s", token)
schemePath := "pages-user/address/submit"
schemeQuery := fmt.Sprintf("token=%s", token)
// 4. Test GetShortLink
fmt.Println("\nTesting GetShortLink...")
// Test Case A: Current Target (Subpackage)
sl, err := wechat.GetShortLink(at, pagePath, "Test Title")
if err != nil {
fmt.Printf("Case A (Subpackage) Failed: %v\n", err)
} else {
fmt.Printf("Case A (Subpackage) Success: %s\n", sl)
}
// Test Case B: Main Page (Known good)
slMain, err := wechat.GetShortLink(at, "pages/index/index", "Main Page")
if err != nil {
fmt.Printf("Case B (Main Page) Failed: %v\n", err)
} else {
fmt.Printf("Case B (Main Page) Success: %s\n", slMain)
}
// Test Case C: Leading Slash
slSlash, err := wechat.GetShortLink(at, "/"+pagePath, "Slash Path")
if err != nil {
fmt.Printf("Case C (Leading Slash) Failed: %v\n", err)
} else {
fmt.Printf("Case C (Leading Slash) Success: %s\n", slSlash)
}
// 5. Test GenerateScheme (Release)
fmt.Println("\nTesting GenerateScheme (Release)...")
sch, err := wechat.GenerateScheme(at, schemePath, schemeQuery, "release")
if err != nil {
fmt.Printf("GenerateScheme (Release) Failed: %v\n", err)
} else {
fmt.Printf("GenerateScheme (Release) Success: %s\n", sch)
}
// 6. Test GenerateScheme (Trial)
fmt.Println("\nTesting GenerateScheme (Trial)...")
schTrial, err := wechat.GenerateScheme(at, schemePath, schemeQuery, "trial")
if err != nil {
fmt.Printf("GenerateScheme (Trial) Failed: %v\n", err)
} else {
fmt.Printf("GenerateScheme (Trial) Success: %s\n", schTrial)
}
// 7. Test Length with Short Token
fmt.Println("\nTesting GenerateScheme with Short Token (Release)...")
shortToken := "short_token_123"
schShort, err := wechat.GenerateScheme(at, schemePath, fmt.Sprintf("token=%s", shortToken), "release")
if err != nil {
fmt.Printf("GenerateScheme (Short Token) Failed: %v\n", err)
} else {
fmt.Printf("GenerateScheme (Short Token) Success: %s\n", schShort)
}
}
func loadConfig(path string) (string, string) {
f, err := os.Open(path)
if err != nil {
return "", ""
}
defer f.Close()
var appId, appSecret string
scanner := bufio.NewScanner(f)
inWechat := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.ToLower(line) == "[wechat]" {
inWechat = true
continue
}
if strings.HasPrefix(line, "[") && line != "[wechat]" && line != "[Wechat]" {
inWechat = false
}
if inWechat {
if strings.HasPrefix(line, "app_id") {
parts := strings.Split(line, "=")
if len(parts) == 2 {
appId = strings.Trim(strings.TrimSpace(parts[1]), "\"")
}
}
if strings.HasPrefix(line, "app_secret") {
parts := strings.Split(line, "=")
if len(parts) == 2 {
appSecret = strings.Trim(strings.TrimSpace(parts[1]), "\"")
}
}
}
}
return appId, appSecret
}