feat: 添加对对碰游戏功能与Redis支持
refactor: 重构抽奖逻辑以支持可验证凭据 feat(redis): 集成Redis客户端并添加配置支持 fix: 修复订单取消时的优惠券和库存处理逻辑 docs: 添加对对碰游戏前端对接指南和示例JSON test: 添加对对碰游戏模拟测试和验证逻辑
This commit is contained in:
parent
45815bfb7d
commit
e2782a69d3
@ -18,5 +18,7 @@ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -tags timetzda
|
||||
|
||||
|
||||
export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
docker build -t zfc931912343/bindbox-game:v1.6 .
|
||||
docker push zfc931912343/bindbox-game:v1.6
|
||||
docker build -t zfc931912343/bindbox-game:v1.8 .
|
||||
docker push zfc931912343/bindbox-game:v1.8
|
||||
|
||||
docker pull zfc931912343/bindbox-game:v1.8 &&docker rm -f bindbox-game && docker run -d --name bindbox-game -p 9991:9991 zfc931912343/bindbox-game:v1.8
|
||||
@ -29,6 +29,12 @@ type Config struct {
|
||||
} `mapstructure:"write" toml:"write"`
|
||||
} `mapstructure:"mysql" toml:"mysql"`
|
||||
|
||||
Redis struct {
|
||||
Addr string `mapstructure:"addr" toml:"addr"`
|
||||
Pass string `mapstructure:"pass" toml:"pass"`
|
||||
DB int `mapstructure:"db" toml:"db"`
|
||||
} `mapstructure:"redis" toml:"redis"`
|
||||
|
||||
JWT struct {
|
||||
AdminSecret string `mapstructure:"admin_secret" toml:"admin_secret"`
|
||||
PatientSecret string `mapstructure:"patient_secret" toml:"patient_secret"`
|
||||
|
||||
@ -11,9 +11,9 @@
|
||||
name = "bindbox_game"
|
||||
|
||||
[redis]
|
||||
addr = "127.0.0.1:6379"
|
||||
pass = ""
|
||||
db = 0
|
||||
addr = "118.25.13.43:8379"
|
||||
pass = "xbm#2023by1024"
|
||||
db = 5
|
||||
|
||||
[random]
|
||||
commit_master_key = "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
|
||||
|
||||
@ -7,6 +7,12 @@ name = 'bindbox_game'
|
||||
pass = 'api2api..'
|
||||
user = 'root'
|
||||
|
||||
[redis]
|
||||
addr = "118.25.13.43:8379"
|
||||
pass = "xbm#2023by1024"
|
||||
db = 5
|
||||
|
||||
|
||||
[mysql.write]
|
||||
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
|
||||
name = 'bindbox_game'
|
||||
|
||||
470
docs/docs.go
470
docs/docs.go
@ -3510,6 +3510,51 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/activities/{activity_id}/issues/{issue_id}/draw_logs_grouped": {
|
||||
"get": {
|
||||
"description": "查看指定活动期数的抽奖记录,按奖品等级分组返回",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"APP端.活动"
|
||||
],
|
||||
"summary": "按奖品等级分类的抽奖记录",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "活动ID",
|
||||
"name": "activity_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "期ID",
|
||||
"name": "issue_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.listDrawLogsByLevelResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/code.Failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/activities/{activity_id}/issues/{issue_id}/rewards": {
|
||||
"get": {
|
||||
"description": "获取指定期的奖励配置列表",
|
||||
@ -3711,14 +3756,9 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/matching/play": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "执行一轮配对,返回配对结果和新抽取的牌",
|
||||
"/api/app/matching/card_types": {
|
||||
"get": {
|
||||
"description": "获取所有启用的卡牌类型配置,用于App端预览或动画展示",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@ -3728,23 +3768,15 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"APP端.活动"
|
||||
],
|
||||
"summary": "执行一轮对对碰游戏",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "请求参数",
|
||||
"name": "RequestBody",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGamePlayRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "列出对对碰卡牌类型",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGamePlayResponse"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.CardTypeConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@ -3756,14 +3788,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/matching/start": {
|
||||
"/api/app/matching/check": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "创建新的对对碰游戏会话,返回初始手牌和第一轮结果",
|
||||
"description": "前端游戏结束后上报结果,服务器发放奖励",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@ -3773,7 +3805,7 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"APP端.活动"
|
||||
],
|
||||
"summary": "开始对对碰游戏",
|
||||
"summary": "游戏结束结算校验",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "请求参数",
|
||||
@ -3781,7 +3813,7 @@ const docTemplate = `{
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGameStartRequest"
|
||||
"$ref": "#/definitions/app.matchingGameCheckRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -3789,7 +3821,52 @@ const docTemplate = `{
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGameStartResponse"
|
||||
"$ref": "#/definitions/app.matchingGameCheckResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/code.Failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/matching/preorder": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "用户下单,服务器扣费并返回全量99张乱序卡牌,前端自行负责游戏流程",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"APP端.活动"
|
||||
],
|
||||
"summary": "下单并获取对对碰全量数据",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "请求参数",
|
||||
"name": "RequestBody",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGamePreOrderRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGamePreOrderResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@ -3845,6 +3922,100 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/orders/{order_id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "获取指定订单的详细信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"APP端.用户"
|
||||
],
|
||||
"summary": "获取订单详情",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "订单ID",
|
||||
"name": "order_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/user.OrderWithItems"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/code.Failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/orders/{order_id}/cancel": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "取消指定订单(仅限待付款状态)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"APP端.用户"
|
||||
],
|
||||
"summary": "取消订单",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "订单ID",
|
||||
"name": "order_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "取消原因",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.cancelOrderRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.cancelOrderResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/code.Failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/pay/wechat/jsapi/preorder": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -7254,6 +7425,9 @@ const docTemplate = `{
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"min_score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -7366,6 +7540,9 @@ const docTemplate = `{
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"min_score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -7547,11 +7724,11 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.MatchingCard": {
|
||||
"app.CardTypeConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"image_url": {
|
||||
"type": "string"
|
||||
@ -7559,56 +7736,32 @@ const docTemplate = `{
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.MatchingCard": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.MatchingPair": {
|
||||
"app.MatchingRewardInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card_type": {
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.MatchingRoundResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"drawn_cards": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hand_after": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hand_before": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"pairs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.MatchingPair"
|
||||
}
|
||||
},
|
||||
"pairs_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"round": {
|
||||
"reward_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
@ -7859,6 +8012,31 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.cancelOrderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.cancelOrderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cancelled_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"order_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"order_no": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.couponDetail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -7976,6 +8154,23 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.drawLogGroup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"level_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.drawLogItem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.drawLogItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8309,6 +8504,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.listDrawLogsByLevelResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.drawLogGroup"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.listDrawLogsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8520,58 +8726,75 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.matchingGamePlayRequest": {
|
||||
"app.matchingGameCheckRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"game_id"
|
||||
],
|
||||
"properties": {
|
||||
"game_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_pairs": {
|
||||
"description": "客户端上报的消除总对数",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.matchingGamePlayResponse": {
|
||||
"app.matchingGameCheckResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"final_state": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"game_over": {
|
||||
"finished": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"round": {
|
||||
"$ref": "#/definitions/app.MatchingRoundResult"
|
||||
"game_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"reward": {
|
||||
"$ref": "#/definitions/app.MatchingRewardInfo"
|
||||
},
|
||||
"total_pairs": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.matchingGameStartRequest": {
|
||||
"app.matchingGamePreOrderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"coupon_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"issue_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"item_card_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"position": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.matchingGameStartResponse": {
|
||||
"app.matchingGamePreOrderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deck_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"first_round": {
|
||||
"$ref": "#/definitions/app.MatchingRoundResult"
|
||||
},
|
||||
"game_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"hand": {
|
||||
"all_cards": {
|
||||
"description": "全量99张卡牌(乱序)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.MatchingCard"
|
||||
}
|
||||
},
|
||||
"game_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"order_no": {
|
||||
"type": "string"
|
||||
},
|
||||
"pay_status": {
|
||||
"description": "1=Pending, 2=Paid",
|
||||
"type": "integer"
|
||||
},
|
||||
"server_seed_hash": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -9481,6 +9704,65 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"user.DrawReceiptInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"algo_version": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"client_seed": {
|
||||
"type": "string"
|
||||
},
|
||||
"draw_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"draw_index": {
|
||||
"type": "integer"
|
||||
},
|
||||
"draw_log_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"items_root": {
|
||||
"type": "string"
|
||||
},
|
||||
"items_snapshot": {
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rand_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"reward_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"round_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"selected_index": {
|
||||
"type": "integer"
|
||||
},
|
||||
"server_seed_hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"server_sub_seed": {
|
||||
"type": "string"
|
||||
},
|
||||
"signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "integer"
|
||||
},
|
||||
"weights_total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user.InventoryWithProduct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -9631,6 +9913,12 @@ const docTemplate = `{
|
||||
"description": "优惠券抵扣金额(分)",
|
||||
"type": "integer"
|
||||
},
|
||||
"draw_receipts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/user.DrawReceiptInfo"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"description": "主键ID",
|
||||
"type": "integer"
|
||||
|
||||
105
docs/matching_game_api_example.jsonc
Normal file
105
docs/matching_game_api_example.jsonc
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
// 游戏唯一ID,用于后续校验和记录查询
|
||||
"game_id": "MG10011639882000",
|
||||
|
||||
// 初始棋盘状态(9个格子)
|
||||
// null 表示该格子为空(理论上初始时不应有空,除非卡牌不够)
|
||||
"initial_board": [
|
||||
{
|
||||
"id": "c1", // 卡牌唯一实例ID
|
||||
"type": "A", // 卡牌类型代码(对应配置中的 Code)
|
||||
"name": "苹果", // 卡牌名称
|
||||
"image_url": "apple.png" // 卡牌图片地址
|
||||
},
|
||||
{ "id": "c2", "type": "A", "name": "苹果", "image_url": "apple.png" },
|
||||
{ "id": "c3", "type": "B", "name": "香蕉", "image_url": "banana.png" },
|
||||
{ "id": "c4", "type": "B", "name": "香蕉", "image_url": "banana.png" },
|
||||
{ "id": "c5", "type": "C", "name": "樱桃", "image_url": "cherry.png" },
|
||||
{ "id": "c6", "type": "C", "name": "樱桃", "image_url": "cherry.png" },
|
||||
{ "id": "c7", "type": "A", "name": "苹果", "image_url": "apple.png" },
|
||||
{ "id": "c8", "type": "A", "name": "苹果", "image_url": "apple.png" },
|
||||
{ "id": "c9", "type": "B", "name": "香蕉", "image_url": "banana.png" }
|
||||
],
|
||||
|
||||
// 游戏完整时间轴,前端按顺序播放数组中的每一个 Round
|
||||
"timeline": [
|
||||
// --- 第 1 回合 ---
|
||||
{
|
||||
"round": 1, // 回合数
|
||||
|
||||
// 当前回合开始时的棋盘状态(主要用于校准,前端动画可基于此状态开始)
|
||||
"board": [ ... ],
|
||||
|
||||
// 本回合消除的配对信息
|
||||
"pairs": [
|
||||
{
|
||||
"card_type": "A", // 消除的卡牌类型
|
||||
"count": 2, // 消除数量
|
||||
"card_ids": ["c1", "c2"], // 被消除的卡牌ID
|
||||
"slot_indices": [0, 1] // 被消除的棋盘格子索引 (0-8) -> 前端需播放这些位置的消除动画
|
||||
}
|
||||
],
|
||||
"pairs_count": 1, // 本回合总消除对数
|
||||
|
||||
// 消除后,从牌堆抽卡填补空位的信息
|
||||
"drawn_cards": [
|
||||
{
|
||||
"slot_index": 0, // 填补到哪个格子 (0-8)
|
||||
"card": { // 填补的具体卡牌信息
|
||||
"id": "c10",
|
||||
"type": "B",
|
||||
"name": "香蕉",
|
||||
"image_url": "banana.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slot_index": 1,
|
||||
"card": {
|
||||
"id": "c11",
|
||||
"type": "C",
|
||||
"name": "樱桃",
|
||||
"image_url": "cherry.png"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"reshuffled": false, // 本回合是否触发了“死局重洗”。如果为 true,前端需播放棋盘重洗动画(所有卡牌重新排列)
|
||||
"can_continue": true // 游戏是否继续。如果为 false,表示游戏结束
|
||||
},
|
||||
|
||||
// --- 第 2 回合 ---
|
||||
{
|
||||
"round": 2,
|
||||
"pairs": [
|
||||
{
|
||||
"card_type": "B",
|
||||
"count": 4, // 一次消除了4张!(2对)
|
||||
"card_ids": ["c3", "c4", "c9", "c10"],
|
||||
"slot_indices": [2, 3, 8, 0] // 这些位置的卡牌一起消失
|
||||
}
|
||||
],
|
||||
"pairs_count": 2,
|
||||
"drawn_cards": [ ... ], // 可能会填补4张新卡
|
||||
"reshuffled": false,
|
||||
"can_continue": true
|
||||
},
|
||||
|
||||
// ... 中间省略若干回合 ...
|
||||
|
||||
// --- 最后回合 ---
|
||||
{
|
||||
"round": 5,
|
||||
"pairs": [], // 没有可消除的了
|
||||
"pairs_count": 0,
|
||||
"drawn_cards": [],
|
||||
"reshuffled": false,
|
||||
"can_continue": false // 游戏结束
|
||||
}
|
||||
],
|
||||
|
||||
// 游戏总消除对数(最终得分)
|
||||
"total_pairs": 8,
|
||||
|
||||
// 服务器种子哈希(用于公平性验证,可忽略)
|
||||
"server_seed_hash": "a1b2c3d4..."
|
||||
}
|
||||
127
docs/matching_game_full_example.json
Normal file
127
docs/matching_game_full_example.json
Normal file
@ -0,0 +1,127 @@
|
||||
{
|
||||
"game_id": "MG1001_FULL_DEMO_2025",
|
||||
"initial_board": [
|
||||
{ "id": "c1", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
{ "id": "c2", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
{ "id": "c3", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c4", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c5", "type": "C", "name": "樱桃", "image_url": "https://example.com/cherry.png" },
|
||||
{ "id": "c6", "type": "C", "name": "樱桃", "image_url": "https://example.com/cherry.png" },
|
||||
{ "id": "c7", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
{ "id": "c8", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c9", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" }
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"round": 1,
|
||||
"board": [
|
||||
{ "id": "c1", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
{ "id": "c2", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
{ "id": "c3", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c4", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c5", "type": "C", "name": "樱桃", "image_url": "https://example.com/cherry.png" },
|
||||
{ "id": "c6", "type": "C", "name": "樱桃", "image_url": "https://example.com/cherry.png" },
|
||||
{ "id": "c7", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
{ "id": "c8", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c9", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" }
|
||||
],
|
||||
"pairs": [
|
||||
{
|
||||
"card_type": "A",
|
||||
"count": 2,
|
||||
"card_ids": ["c1", "c2"],
|
||||
"slot_indices": [0, 1]
|
||||
}
|
||||
],
|
||||
"pairs_count": 1,
|
||||
"drawn_cards": [
|
||||
{
|
||||
"slot_index": 0,
|
||||
"card": { "id": "c10", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" }
|
||||
}
|
||||
],
|
||||
"reshuffled": false,
|
||||
"can_continue": true
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"board": [
|
||||
{ "id": "c10", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
null,
|
||||
{ "id": "c3", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c4", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c5", "type": "C", "name": "樱桃", "image_url": "https://example.com/cherry.png" },
|
||||
{ "id": "c6", "type": "C", "name": "樱桃", "image_url": "https://example.com/cherry.png" },
|
||||
{ "id": "c7", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
{ "id": "c8", "type": "B", "name": "香蕉", "image_url": "https://example.com/banana.png" },
|
||||
{ "id": "c9", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" }
|
||||
],
|
||||
"pairs": [
|
||||
{
|
||||
"card_type": "B",
|
||||
"count": 4,
|
||||
"card_ids": ["c10", "c3", "c4", "c8"],
|
||||
"slot_indices": [0, 2, 3, 7]
|
||||
}
|
||||
],
|
||||
"pairs_count": 2,
|
||||
"drawn_cards": [],
|
||||
"reshuffled": false,
|
||||
"can_continue": true
|
||||
},
|
||||
{
|
||||
"round": 3,
|
||||
"board": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ "id": "c5", "type": "C", "name": "樱桃", "image_url": "https://example.com/cherry.png" },
|
||||
{ "id": "c6", "type": "C", "name": "樱桃", "image_url": "https://example.com/cherry.png" },
|
||||
{ "id": "c7", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
null,
|
||||
{ "id": "c9", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" }
|
||||
],
|
||||
"pairs": [
|
||||
{
|
||||
"card_type": "C",
|
||||
"count": 2,
|
||||
"card_ids": ["c5", "c6"],
|
||||
"slot_indices": [4, 5]
|
||||
}
|
||||
],
|
||||
"pairs_count": 1,
|
||||
"drawn_cards": [],
|
||||
"reshuffled": false,
|
||||
"can_continue": true
|
||||
},
|
||||
{
|
||||
"round": 4,
|
||||
"board": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ "id": "c7", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" },
|
||||
null,
|
||||
{ "id": "c9", "type": "A", "name": "苹果", "image_url": "https://example.com/apple.png" }
|
||||
],
|
||||
"pairs": [
|
||||
{
|
||||
"card_type": "A",
|
||||
"count": 2,
|
||||
"card_ids": ["c7", "c9"],
|
||||
"slot_indices": [6, 8]
|
||||
}
|
||||
],
|
||||
"pairs_count": 1,
|
||||
"drawn_cards": [],
|
||||
"reshuffled": false,
|
||||
"can_continue": false
|
||||
}
|
||||
],
|
||||
"total_pairs": 5,
|
||||
"server_seed_hash": "a1b2c3d4e5f6g7h8i9j0"
|
||||
}
|
||||
@ -3502,6 +3502,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/activities/{activity_id}/issues/{issue_id}/draw_logs_grouped": {
|
||||
"get": {
|
||||
"description": "查看指定活动期数的抽奖记录,按奖品等级分组返回",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"APP端.活动"
|
||||
],
|
||||
"summary": "按奖品等级分类的抽奖记录",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "活动ID",
|
||||
"name": "activity_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "期ID",
|
||||
"name": "issue_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.listDrawLogsByLevelResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/code.Failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/activities/{activity_id}/issues/{issue_id}/rewards": {
|
||||
"get": {
|
||||
"description": "获取指定期的奖励配置列表",
|
||||
@ -3703,14 +3748,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/matching/play": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "执行一轮配对,返回配对结果和新抽取的牌",
|
||||
"/api/app/matching/card_types": {
|
||||
"get": {
|
||||
"description": "获取所有启用的卡牌类型配置,用于App端预览或动画展示",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@ -3720,23 +3760,15 @@
|
||||
"tags": [
|
||||
"APP端.活动"
|
||||
],
|
||||
"summary": "执行一轮对对碰游戏",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "请求参数",
|
||||
"name": "RequestBody",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGamePlayRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "列出对对碰卡牌类型",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGamePlayResponse"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.CardTypeConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@ -3748,14 +3780,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/matching/start": {
|
||||
"/api/app/matching/check": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "创建新的对对碰游戏会话,返回初始手牌和第一轮结果",
|
||||
"description": "前端游戏结束后上报结果,服务器发放奖励",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@ -3765,7 +3797,7 @@
|
||||
"tags": [
|
||||
"APP端.活动"
|
||||
],
|
||||
"summary": "开始对对碰游戏",
|
||||
"summary": "游戏结束结算校验",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "请求参数",
|
||||
@ -3773,7 +3805,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGameStartRequest"
|
||||
"$ref": "#/definitions/app.matchingGameCheckRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -3781,7 +3813,52 @@
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGameStartResponse"
|
||||
"$ref": "#/definitions/app.matchingGameCheckResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/code.Failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/matching/preorder": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "用户下单,服务器扣费并返回全量99张乱序卡牌,前端自行负责游戏流程",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"APP端.活动"
|
||||
],
|
||||
"summary": "下单并获取对对碰全量数据",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "请求参数",
|
||||
"name": "RequestBody",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGamePreOrderRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.matchingGamePreOrderResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@ -3837,6 +3914,100 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/orders/{order_id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "获取指定订单的详细信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"APP端.用户"
|
||||
],
|
||||
"summary": "获取订单详情",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "订单ID",
|
||||
"name": "order_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/user.OrderWithItems"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/code.Failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/orders/{order_id}/cancel": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"LoginVerifyToken": []
|
||||
}
|
||||
],
|
||||
"description": "取消指定订单(仅限待付款状态)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"APP端.用户"
|
||||
],
|
||||
"summary": "取消订单",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "订单ID",
|
||||
"name": "order_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "取消原因",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.cancelOrderRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/app.cancelOrderResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/code.Failure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/app/pay/wechat/jsapi/preorder": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -7246,6 +7417,9 @@
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"min_score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -7358,6 +7532,9 @@
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"min_score": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -7539,11 +7716,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.MatchingCard": {
|
||||
"app.CardTypeConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"image_url": {
|
||||
"type": "string"
|
||||
@ -7551,56 +7728,32 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.MatchingCard": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.MatchingPair": {
|
||||
"app.MatchingRewardInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card_type": {
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.MatchingRoundResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"drawn_cards": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hand_after": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hand_before": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"pairs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.MatchingPair"
|
||||
}
|
||||
},
|
||||
"pairs_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"round": {
|
||||
"reward_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
@ -7851,6 +8004,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.cancelOrderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.cancelOrderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cancelled_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"order_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"order_no": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.couponDetail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -7968,6 +8146,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.drawLogGroup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "integer"
|
||||
},
|
||||
"level_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.drawLogItem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.drawLogItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8301,6 +8496,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.listDrawLogsByLevelResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.drawLogGroup"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.listDrawLogsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8512,58 +8718,75 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.matchingGamePlayRequest": {
|
||||
"app.matchingGameCheckRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"game_id"
|
||||
],
|
||||
"properties": {
|
||||
"game_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_pairs": {
|
||||
"description": "客户端上报的消除总对数",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.matchingGamePlayResponse": {
|
||||
"app.matchingGameCheckResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"final_state": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"game_over": {
|
||||
"finished": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"round": {
|
||||
"$ref": "#/definitions/app.MatchingRoundResult"
|
||||
"game_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"reward": {
|
||||
"$ref": "#/definitions/app.MatchingRewardInfo"
|
||||
},
|
||||
"total_pairs": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.matchingGameStartRequest": {
|
||||
"app.matchingGamePreOrderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"coupon_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"issue_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"item_card_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"position": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app.matchingGameStartResponse": {
|
||||
"app.matchingGamePreOrderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deck_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"first_round": {
|
||||
"$ref": "#/definitions/app.MatchingRoundResult"
|
||||
},
|
||||
"game_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"hand": {
|
||||
"all_cards": {
|
||||
"description": "全量99张卡牌(乱序)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/app.MatchingCard"
|
||||
}
|
||||
},
|
||||
"game_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"order_no": {
|
||||
"type": "string"
|
||||
},
|
||||
"pay_status": {
|
||||
"description": "1=Pending, 2=Paid",
|
||||
"type": "integer"
|
||||
},
|
||||
"server_seed_hash": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -9473,6 +9696,65 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"user.DrawReceiptInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"algo_version": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"client_seed": {
|
||||
"type": "string"
|
||||
},
|
||||
"draw_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"draw_index": {
|
||||
"type": "integer"
|
||||
},
|
||||
"draw_log_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"items_root": {
|
||||
"type": "string"
|
||||
},
|
||||
"items_snapshot": {
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rand_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"reward_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"round_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"selected_index": {
|
||||
"type": "integer"
|
||||
},
|
||||
"server_seed_hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"server_sub_seed": {
|
||||
"type": "string"
|
||||
},
|
||||
"signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "integer"
|
||||
},
|
||||
"weights_total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user.InventoryWithProduct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -9623,6 +9905,12 @@
|
||||
"description": "优惠券抵扣金额(分)",
|
||||
"type": "integer"
|
||||
},
|
||||
"draw_receipts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/user.DrawReceiptInfo"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"description": "主键ID",
|
||||
"type": "integer"
|
||||
|
||||
@ -981,6 +981,8 @@ definitions:
|
||||
type: integer
|
||||
level:
|
||||
type: integer
|
||||
min_score:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
original_qty:
|
||||
@ -1049,6 +1051,8 @@ definitions:
|
||||
type: integer
|
||||
level:
|
||||
type: integer
|
||||
min_score:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
original_qty:
|
||||
@ -1174,47 +1178,31 @@ definitions:
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
app.MatchingCard:
|
||||
app.CardTypeConfig:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
image_url:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
quantity:
|
||||
type: integer
|
||||
type: object
|
||||
app.MatchingCard:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
app.MatchingPair:
|
||||
app.MatchingRewardInfo:
|
||||
properties:
|
||||
card_type:
|
||||
level:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
count:
|
||||
type: integer
|
||||
type: object
|
||||
app.MatchingRoundResult:
|
||||
properties:
|
||||
can_continue:
|
||||
type: boolean
|
||||
drawn_cards:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
hand_after:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
hand_before:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
pairs:
|
||||
items:
|
||||
$ref: '#/definitions/app.MatchingPair'
|
||||
type: array
|
||||
pairs_count:
|
||||
type: integer
|
||||
round:
|
||||
reward_id:
|
||||
type: integer
|
||||
type: object
|
||||
app.activityDetailResponse:
|
||||
@ -1377,6 +1365,22 @@ definitions:
|
||||
success:
|
||||
type: boolean
|
||||
type: object
|
||||
app.cancelOrderRequest:
|
||||
properties:
|
||||
reason:
|
||||
type: string
|
||||
type: object
|
||||
app.cancelOrderResponse:
|
||||
properties:
|
||||
cancelled_at:
|
||||
type: string
|
||||
order_id:
|
||||
type: integer
|
||||
order_no:
|
||||
type: string
|
||||
status:
|
||||
type: integer
|
||||
type: object
|
||||
app.couponDetail:
|
||||
properties:
|
||||
amount:
|
||||
@ -1458,6 +1462,17 @@ definitions:
|
||||
description: 订单号
|
||||
type: string
|
||||
type: object
|
||||
app.drawLogGroup:
|
||||
properties:
|
||||
level:
|
||||
type: integer
|
||||
level_name:
|
||||
type: string
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/app.drawLogItem'
|
||||
type: array
|
||||
type: object
|
||||
app.drawLogItem:
|
||||
properties:
|
||||
current_level:
|
||||
@ -1675,6 +1690,13 @@ definitions:
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
app.listDrawLogsByLevelResponse:
|
||||
properties:
|
||||
groups:
|
||||
items:
|
||||
$ref: '#/definitions/app.drawLogGroup'
|
||||
type: array
|
||||
type: object
|
||||
app.listDrawLogsResponse:
|
||||
properties:
|
||||
list:
|
||||
@ -1812,40 +1834,52 @@ definitions:
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
app.matchingGamePlayRequest:
|
||||
app.matchingGameCheckRequest:
|
||||
properties:
|
||||
game_id:
|
||||
type: string
|
||||
total_pairs:
|
||||
description: 客户端上报的消除总对数
|
||||
type: integer
|
||||
required:
|
||||
- game_id
|
||||
type: object
|
||||
app.matchingGamePlayResponse:
|
||||
app.matchingGameCheckResponse:
|
||||
properties:
|
||||
final_state:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
game_over:
|
||||
finished:
|
||||
type: boolean
|
||||
round:
|
||||
$ref: '#/definitions/app.MatchingRoundResult'
|
||||
game_id:
|
||||
type: string
|
||||
reward:
|
||||
$ref: '#/definitions/app.MatchingRewardInfo'
|
||||
total_pairs:
|
||||
type: integer
|
||||
type: object
|
||||
app.matchingGameStartRequest:
|
||||
app.matchingGamePreOrderRequest:
|
||||
properties:
|
||||
coupon_id:
|
||||
type: integer
|
||||
issue_id:
|
||||
type: integer
|
||||
type: object
|
||||
app.matchingGameStartResponse:
|
||||
properties:
|
||||
deck_count:
|
||||
item_card_id:
|
||||
type: integer
|
||||
first_round:
|
||||
$ref: '#/definitions/app.MatchingRoundResult'
|
||||
game_id:
|
||||
position:
|
||||
type: string
|
||||
hand:
|
||||
type: object
|
||||
app.matchingGamePreOrderResponse:
|
||||
properties:
|
||||
all_cards:
|
||||
description: 全量99张卡牌(乱序)
|
||||
items:
|
||||
$ref: '#/definitions/app.MatchingCard'
|
||||
type: array
|
||||
game_id:
|
||||
type: string
|
||||
order_no:
|
||||
type: string
|
||||
pay_status:
|
||||
description: 1=Pending, 2=Paid
|
||||
type: integer
|
||||
server_seed_hash:
|
||||
type: string
|
||||
type: object
|
||||
@ -2461,6 +2495,45 @@ definitions:
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
user.DrawReceiptInfo:
|
||||
properties:
|
||||
algo_version:
|
||||
type: string
|
||||
client_id:
|
||||
type: integer
|
||||
client_seed:
|
||||
type: string
|
||||
draw_id:
|
||||
type: integer
|
||||
draw_index:
|
||||
type: integer
|
||||
draw_log_id:
|
||||
type: integer
|
||||
items_root:
|
||||
type: string
|
||||
items_snapshot:
|
||||
type: string
|
||||
nonce:
|
||||
type: integer
|
||||
rand_proof:
|
||||
type: string
|
||||
reward_id:
|
||||
type: integer
|
||||
round_id:
|
||||
type: integer
|
||||
selected_index:
|
||||
type: integer
|
||||
server_seed_hash:
|
||||
type: string
|
||||
server_sub_seed:
|
||||
type: string
|
||||
signature:
|
||||
type: string
|
||||
timestamp:
|
||||
type: integer
|
||||
weights_total:
|
||||
type: integer
|
||||
type: object
|
||||
user.InventoryWithProduct:
|
||||
properties:
|
||||
activity_id:
|
||||
@ -2569,6 +2642,10 @@ definitions:
|
||||
discount_amount:
|
||||
description: 优惠券抵扣金额(分)
|
||||
type: integer
|
||||
draw_receipts:
|
||||
items:
|
||||
$ref: '#/definitions/user.DrawReceiptInfo'
|
||||
type: array
|
||||
id:
|
||||
description: 主键ID
|
||||
type: integer
|
||||
@ -4934,6 +5011,36 @@ paths:
|
||||
summary: 抽奖记录列表
|
||||
tags:
|
||||
- APP端.活动
|
||||
/api/app/activities/{activity_id}/issues/{issue_id}/draw_logs_grouped:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 查看指定活动期数的抽奖记录,按奖品等级分组返回
|
||||
parameters:
|
||||
- description: 活动ID
|
||||
in: path
|
||||
name: activity_id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 期ID
|
||||
in: path
|
||||
name: issue_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/app.listDrawLogsByLevelResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/code.Failure'
|
||||
summary: 按奖品等级分类的抽奖记录
|
||||
tags:
|
||||
- APP端.活动
|
||||
/api/app/activities/{activity_id}/issues/{issue_id}/rewards:
|
||||
get:
|
||||
consumes:
|
||||
@ -5063,60 +5170,81 @@ paths:
|
||||
summary: 抽奖订单结果查询
|
||||
tags:
|
||||
- APP端.抽奖
|
||||
/api/app/matching/play:
|
||||
post:
|
||||
/api/app/matching/card_types:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 执行一轮配对,返回配对结果和新抽取的牌
|
||||
parameters:
|
||||
- description: 请求参数
|
||||
in: body
|
||||
name: RequestBody
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/app.matchingGamePlayRequest'
|
||||
description: 获取所有启用的卡牌类型配置,用于App端预览或动画展示
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/app.matchingGamePlayResponse'
|
||||
items:
|
||||
$ref: '#/definitions/app.CardTypeConfig'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/code.Failure'
|
||||
security:
|
||||
- LoginVerifyToken: []
|
||||
summary: 执行一轮对对碰游戏
|
||||
summary: 列出对对碰卡牌类型
|
||||
tags:
|
||||
- APP端.活动
|
||||
/api/app/matching/start:
|
||||
/api/app/matching/check:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 创建新的对对碰游戏会话,返回初始手牌和第一轮结果
|
||||
description: 前端游戏结束后上报结果,服务器发放奖励
|
||||
parameters:
|
||||
- description: 请求参数
|
||||
in: body
|
||||
name: RequestBody
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/app.matchingGameStartRequest'
|
||||
$ref: '#/definitions/app.matchingGameCheckRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/app.matchingGameStartResponse'
|
||||
$ref: '#/definitions/app.matchingGameCheckResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/code.Failure'
|
||||
security:
|
||||
- LoginVerifyToken: []
|
||||
summary: 开始对对碰游戏
|
||||
summary: 游戏结束结算校验
|
||||
tags:
|
||||
- APP端.活动
|
||||
/api/app/matching/preorder:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 用户下单,服务器扣费并返回全量99张乱序卡牌,前端自行负责游戏流程
|
||||
parameters:
|
||||
- description: 请求参数
|
||||
in: body
|
||||
name: RequestBody
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/app.matchingGamePreOrderRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/app.matchingGamePreOrderResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/code.Failure'
|
||||
security:
|
||||
- LoginVerifyToken: []
|
||||
summary: 下单并获取对对碰全量数据
|
||||
tags:
|
||||
- APP端.活动
|
||||
/api/app/matching/state:
|
||||
@ -5147,6 +5275,65 @@ paths:
|
||||
summary: 获取对对碰游戏状态
|
||||
tags:
|
||||
- APP端.活动
|
||||
/api/app/orders/{order_id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取指定订单的详细信息
|
||||
parameters:
|
||||
- description: 订单ID
|
||||
in: path
|
||||
name: order_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/user.OrderWithItems'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/code.Failure'
|
||||
security:
|
||||
- LoginVerifyToken: []
|
||||
summary: 获取订单详情
|
||||
tags:
|
||||
- APP端.用户
|
||||
/api/app/orders/{order_id}/cancel:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 取消指定订单(仅限待付款状态)
|
||||
parameters:
|
||||
- description: 订单ID
|
||||
in: path
|
||||
name: order_id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 取消原因
|
||||
in: body
|
||||
name: body
|
||||
schema:
|
||||
$ref: '#/definitions/app.cancelOrderRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/app.cancelOrderResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/code.Failure'
|
||||
security:
|
||||
- LoginVerifyToken: []
|
||||
summary: 取消订单
|
||||
tags:
|
||||
- APP端.用户
|
||||
/api/app/pay/wechat/jsapi/preorder:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
122
docs/前端对接指南_对对碰.md
Normal file
122
docs/前端对接指南_对对碰.md
Normal file
@ -0,0 +1,122 @@
|
||||
# 对对碰(Matching Game)前端对接指南
|
||||
|
||||
## 1. 核心流程概述
|
||||
|
||||
本版本采用 **"预计算 + 客户端渲染"** 模式,以确保流畅体验并防止网络延迟影响游戏节奏。
|
||||
|
||||
### 交互时序
|
||||
1. **开始游戏 (API)**: 用户点击开始 -> 调用 `PreOrder` 接口 -> 服务器扣费并返回**完整游戏剧本**。
|
||||
2. **动画播放 (Frontend)**: 前端根据返回的 `timeline` 数据,按顺序播放每一轮的 消除、填补、重洗 动画。
|
||||
3. **游戏结算 (API)**: 动画播放完毕(或用户点击跳过) -> 调用 `Check` 接口 -> 服务器确认结束并发放奖励。
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口调用详解
|
||||
|
||||
### 2.1 第一步:下单并获取数据 (Start)
|
||||
|
||||
* **接口**: `POST /api/app/matching/preorder`
|
||||
* **参数**: `{ "issue_id": 123 }`
|
||||
* **核心响应**:
|
||||
```json
|
||||
{
|
||||
"game_id": "MG_123456", // 保存此ID用于结算
|
||||
"initial_board": [ ... ], // 初始9宫格数据 (用于渲染第一帧)
|
||||
"timeline": [ ... ], // 关键!动画剧本数组
|
||||
"total_pairs": 5 // 预计总消除对数 (可用于显示"目标")
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 第二步:前端渲染循环 (Render)
|
||||
|
||||
前端拿到 `timeline` 后,不需要再请求服务器,直接在本地执行以下逻辑:
|
||||
|
||||
```javascript
|
||||
// 伪代码示例
|
||||
async function playFullGame(data) {
|
||||
// 1. 渲染初始棋盘
|
||||
renderBoard(data.initial_board);
|
||||
|
||||
// 2. 遍历时间轴,逐回合播放
|
||||
for (const round of data.timeline) {
|
||||
|
||||
// --- 阶段 A: 消除动画 ---
|
||||
if (round.pairs.length > 0) {
|
||||
// 高亮要消除的卡牌
|
||||
highlightCards(round.pairs);
|
||||
await wait(500); // 停顿
|
||||
|
||||
// 播放消除特效(根据 slot_indices 找到对应格子)
|
||||
for (const pair of round.pairs) {
|
||||
// pair.slot_indices 数组包含了所有要消除的格子下标 (0-8)
|
||||
playEliminateEffect(pair.slot_indices);
|
||||
}
|
||||
await wait(500); // 等待特效结束
|
||||
|
||||
// 从界面移除卡牌 DOM
|
||||
removeCardsFromBoard(round.pairs);
|
||||
}
|
||||
|
||||
// --- 阶段 B: 填补动画 ---
|
||||
if (round.drawn_cards.length > 0) {
|
||||
// 播放发牌/飞入动画
|
||||
for (const draw of round.drawn_cards) {
|
||||
// draw.slot_index 是目标格子
|
||||
// draw.card 是新卡牌数据
|
||||
flyCardToSlot(draw.card, draw.slot_index);
|
||||
}
|
||||
await wait(500);
|
||||
}
|
||||
|
||||
// --- 阶段 C: 死局重洗 (如果有) ---
|
||||
if (round.reshuffled) {
|
||||
showToast("死局重洗中...");
|
||||
playShuffleAnimation(); // 播放所有卡牌重新排列的动画
|
||||
// 动画结束后,根据 round.board 更新整个棋盘状态,确保位置正确
|
||||
updateFullBoard(round.board);
|
||||
await wait(1000);
|
||||
}
|
||||
|
||||
// 每一轮结束后稍作停顿
|
||||
await wait(300);
|
||||
}
|
||||
|
||||
// 3. 全部播放完毕,调用结算
|
||||
doCheck(data.game_id);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 第三步:结算与领奖 (Check)
|
||||
|
||||
* **接口**: `POST /api/app/matching/check`
|
||||
* **参数**: `{ "game_id": "MG_123456" }`
|
||||
* **作用**: 告知服务器动画已播完,触发最终奖励发放(虽然服务器在PreOrder时已经计算好了,但这个调用作为完整的业务闭环)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据结构字典
|
||||
|
||||
### `MatchingRoundResult` (时间轴中的每一项)
|
||||
|
||||
| 字段 | 类型 | 说明 | 前端处理建议 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `round` | int | 回合数 | 用于UI显示"第几轮" |
|
||||
| `pairs` | array | 消除列表 | **关键**:遍历此数组,读取 `slot_indices` 来决定哪些格子要播消除动画 |
|
||||
| `drawn_cards` | array | 填补列表 | **关键**:遍历此数组,读取 `slot_index` 来决定新卡牌飞入哪个格子 |
|
||||
| `reshuffled` | bool | 是否重洗 | 若为 true,必须播放全屏洗牌特效,并强制刷新棋盘数据 |
|
||||
| `can_continue` | bool | 是否继续 | 若为 false,播放完本轮后显示"游戏结束"弹窗 |
|
||||
|
||||
### `MatchingPair` (消除详情)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `card_type` | string | 消除的卡牌类型 |
|
||||
| `count` | int | 消除数量 (通常是2,连消可能是4,6) |
|
||||
| `slot_indices` | int[] | **核心**:被消除的格子下标列表,如 `[0, 1]` |
|
||||
|
||||
### `DrawnCardInfo` (填补详情)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `slot_index` | int | **核心**:填入的目标格子下标 (0-8) |
|
||||
| `card` | object | 新卡牌的完整数据 (id, image_url, name) |
|
||||
4
go.mod
4
go.mod
@ -19,6 +19,7 @@ require (
|
||||
github.com/issue9/identicon/v2 v2.1.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/spf13/viper v1.17.0
|
||||
@ -48,10 +49,11 @@ require (
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@ -53,6 +53,8 @@ github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
@ -61,8 +63,8 @@ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@ -82,6 +84,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@ -298,6 +302,8 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
|
||||
@ -1,32 +1,39 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
activitysvc "bindbox-game/internal/service/activity"
|
||||
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
||||
usersvc "bindbox-game/internal/service/user"
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
activitysvc "bindbox-game/internal/service/activity"
|
||||
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
||||
titlesvc "bindbox-game/internal/service/title"
|
||||
usersvc "bindbox-game/internal/service/user"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
logger logger.CustomLogger
|
||||
writeDB *dao.Query
|
||||
readDB *dao.Query
|
||||
activity activitysvc.Service
|
||||
syscfg syscfgsvc.Service
|
||||
repo mysql.Repo
|
||||
user usersvc.Service
|
||||
logger logger.CustomLogger
|
||||
writeDB *dao.Query
|
||||
readDB *dao.Query
|
||||
activity activitysvc.Service
|
||||
syscfg syscfgsvc.Service
|
||||
title titlesvc.Service
|
||||
repo mysql.Repo
|
||||
user usersvc.Service
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
|
||||
return &handler{
|
||||
logger: logger,
|
||||
writeDB: dao.Use(db.GetDbW()),
|
||||
readDB: dao.Use(db.GetDbR()),
|
||||
activity: activitysvc.New(logger, db),
|
||||
syscfg: syscfgsvc.New(logger, db),
|
||||
repo: db,
|
||||
user: usersvc.New(logger, db),
|
||||
}
|
||||
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler {
|
||||
return &handler{
|
||||
logger: logger,
|
||||
writeDB: dao.Use(db.GetDbW()),
|
||||
readDB: dao.Use(db.GetDbR()),
|
||||
activity: activitysvc.New(logger, db),
|
||||
syscfg: syscfgsvc.New(logger, db),
|
||||
title: titlesvc.New(logger, db),
|
||||
repo: db,
|
||||
user: usersvc.New(logger, db),
|
||||
redis: rdb,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
@ -82,3 +83,115 @@ func (h *handler) ListDrawLogs() core.HandlerFunc {
|
||||
ctx.Payload(res)
|
||||
}
|
||||
}
|
||||
|
||||
// ListDrawLogsByLevel 按奖品等级分类的抽奖记录
|
||||
// @Summary 按奖品等级分类的抽奖记录
|
||||
// @Description 查看指定活动期数的抽奖记录,按奖品等级分组返回
|
||||
// @Tags APP端.活动
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param activity_id path integer true "活动ID"
|
||||
// @Param issue_id path integer true "期ID"
|
||||
// @Success 200 {object} listDrawLogsByLevelResponse
|
||||
// @Failure 400 {object} code.Failure
|
||||
// @Router /api/app/activities/{activity_id}/issues/{issue_id}/draw_logs_grouped [get]
|
||||
func (h *handler) ListDrawLogsByLevel() core.HandlerFunc {
|
||||
return func(ctx core.Context) {
|
||||
issueID, err := strconv.ParseInt(ctx.Param("issue_id"), 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 获取所有中奖记录
|
||||
// 我们假设这里不需要分页,或者分页逻辑比较复杂(每个等级分页?)。
|
||||
// 根据需求描述“按奖品等级进行归类”,通常 implied 展示所有或者前N个。
|
||||
// 这里暂且获取所有(或者一个较大的限制),然后在内存中分组。
|
||||
// 如果数据量巨大,需要由 Service 层提供 Group By 查询。
|
||||
// 考虑到单期中奖人数通常有限(除非是大规模活动),先尝试获取列表后分组。
|
||||
logs, _, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, 1, 1000) // 假设最多显示1000条中奖记录
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 获取奖品配置以获取等级名称
|
||||
rewards, err := h.activity.ListIssueRewards(ctx.RequestContext(), issueID)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.GetActivityError, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
levelNameMap := make(map[int32]string)
|
||||
for _, r := range rewards {
|
||||
// 通常 Level 1 是大奖,名字如 "一等奖"
|
||||
// 也有可能 ActivityRewardSettings 里没有直接存 "一等奖" 这种字样,而是 Name="iPhone 15"。
|
||||
// 如果需要 "一等奖" 这种分类名,可能需要额外配置或从 Name 推断。
|
||||
// 此处暂且使用 Reward 的 Name 作为 fallback,或者如果有 LevelName 字段最好。
|
||||
// 查看 model 定义,ActivityRewardSettings 只有 Name。
|
||||
// 假如用户希望看到的是 "一等奖", "二等奖",我们需要确立 Level 到 显示名的映射。
|
||||
// 现阶段简单起见,我们将同一 Level 的奖品视为一组。
|
||||
// 组名可以使用该 Level 下任意一个奖品的 Name,或者如果不一致,则可能需要前端映射。
|
||||
// 为了通用性,我们返回 level 值,并尝试找到一个代表性的 Name。
|
||||
if _, ok := levelNameMap[r.Level]; !ok {
|
||||
levelNameMap[r.Level] = r.Name // 简单取第一个遇到的名字
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 分组 (只显示5分钟前的记录)
|
||||
groupsMap := make(map[int32][]drawLogItem)
|
||||
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||
for _, v := range logs {
|
||||
// 过滤掉5分钟内的记录
|
||||
if v.CreatedAt.After(fiveMinutesAgo) {
|
||||
continue
|
||||
}
|
||||
if v.IsWinner == 1 {
|
||||
item := drawLogItem{
|
||||
ID: v.ID,
|
||||
UserID: v.UserID,
|
||||
IssueID: v.IssueID,
|
||||
OrderID: v.OrderID,
|
||||
RewardID: v.RewardID,
|
||||
IsWinner: v.IsWinner,
|
||||
Level: v.Level,
|
||||
CurrentLevel: v.CurrentLevel,
|
||||
}
|
||||
groupsMap[v.Level] = append(groupsMap[v.Level], item)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 构造响应
|
||||
var resp listDrawLogsByLevelResponse
|
||||
for level, items := range groupsMap {
|
||||
group := drawLogGroup{
|
||||
Level: level,
|
||||
LevelName: levelNameMap[level],
|
||||
List: items,
|
||||
}
|
||||
resp.Groups = append(resp.Groups, group)
|
||||
}
|
||||
|
||||
// 排序 Groups (Level 升序? 也就是 1等奖在前)
|
||||
// 简单的冒泡排序或 slice sort
|
||||
for i := 0; i < len(resp.Groups)-1; i++ {
|
||||
for j := 0; j < len(resp.Groups)-1-i; j++ {
|
||||
if resp.Groups[j].Level > resp.Groups[j+1].Level {
|
||||
resp.Groups[j], resp.Groups[j+1] = resp.Groups[j+1], resp.Groups[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(resp)
|
||||
}
|
||||
}
|
||||
|
||||
type drawLogGroup struct {
|
||||
Level int32 `json:"level"`
|
||||
LevelName string `json:"level_name"`
|
||||
List []drawLogItem `json:"list"`
|
||||
}
|
||||
|
||||
type listDrawLogsByLevelResponse struct {
|
||||
Groups []drawLogGroup `json:"groups"`
|
||||
}
|
||||
|
||||
@ -10,10 +10,13 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
titlesvc "bindbox-game/internal/service/title"
|
||||
)
|
||||
|
||||
type joinLotteryRequest struct {
|
||||
@ -69,6 +72,11 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170010, "本活动不支持道具卡"))
|
||||
return
|
||||
}
|
||||
// Ichiban Restriction: No Item Cards allowed (even if Activity logic might technically allow it, enforce strict rule here)
|
||||
if activity.PlayType == "ichiban" && req.ItemCardID != nil && *req.ItemCardID > 0 {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170010, "一番赏活动不支持道具卡"))
|
||||
return
|
||||
}
|
||||
cfgMode := "scheduled"
|
||||
if activity.DrawMode != "" {
|
||||
cfgMode = activity.DrawMode
|
||||
@ -117,6 +125,61 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
||||
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
|
||||
fmt.Printf("[抽奖下单] 优惠后 实付(分)=%d 累计优惠(分)=%d 备注=%s\n", order.ActualAmount, order.DiscountAmount, order.Remark)
|
||||
}
|
||||
// Title Discount Logic
|
||||
// 1. Fetch active effects for this user, scoped to this activity/issue/category
|
||||
// Note: Category ID is not readily available on Activity struct in this scope easily without join, skipping detailed scope for now or fetch if needed.
|
||||
// Assuming scope by ActivityID and IssueID is enough.
|
||||
titleEffects, _ := h.title.ResolveActiveEffects(ctx.RequestContext(), userID, titlesvc.EffectScope{
|
||||
ActivityID: &req.ActivityID,
|
||||
IssueID: &req.IssueID,
|
||||
})
|
||||
|
||||
// 2. Apply Type=2 (Discount) effects
|
||||
for _, ef := range titleEffects {
|
||||
if ef.EffectType == 2 {
|
||||
// Parse ParamsJSON: {"discount_type":"percentage","value_x1000":...,"max_discount_x1000":...}
|
||||
// Simple parsing here or helper
|
||||
var p struct {
|
||||
DiscountType string `json:"discount_type"`
|
||||
ValueX1000 int64 `json:"value_x1000"`
|
||||
MaxDiscountX1000 int64 `json:"max_discount_x1000"`
|
||||
}
|
||||
if jsonErr := json.Unmarshal([]byte(ef.ParamsJSON), &p); jsonErr == nil {
|
||||
var discount int64
|
||||
if p.DiscountType == "percentage" {
|
||||
// e.g. 900 = 90% (10% off), or value_x1000 is the discount rate?
|
||||
// Usually "value" is what you pay? Title "8折" -> value=800?
|
||||
// Let's assume value_x1000 is the DISCOUNT amount (e.g. 200 = 20% off).
|
||||
// Wait, standard is usually "multiplier". Title "Discount" usually means "Cut".
|
||||
// Let's look at `ValidateEffectParams`: "percentage" or "fixed".
|
||||
// Assume ValueX1000 is discount ratio. 200 = 20% off.
|
||||
discount = order.ActualAmount * p.ValueX1000 / 1000
|
||||
} else if p.DiscountType == "fixed" {
|
||||
discount = p.ValueX1000 // In cents
|
||||
}
|
||||
|
||||
if p.MaxDiscountX1000 > 0 && discount > p.MaxDiscountX1000 {
|
||||
discount = p.MaxDiscountX1000
|
||||
}
|
||||
|
||||
if discount > order.ActualAmount {
|
||||
discount = order.ActualAmount
|
||||
}
|
||||
|
||||
if discount > 0 {
|
||||
order.ActualAmount -= discount
|
||||
fmt.Printf("[抽奖下单] Title Discount Applied: -%d (EffectID: %d)\n", discount, ef.ID)
|
||||
// Append to remark or separate logging?
|
||||
if order.Remark == "" {
|
||||
order.Remark = fmt.Sprintf("title_discount:%d:%d", ef.ID, discount)
|
||||
} else {
|
||||
order.Remark += fmt.Sprintf("|title_discount:%d:%d", ef.ID, discount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.UsePoints != nil && *req.UsePoints > 0 {
|
||||
bal, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
||||
usePts := *req.UsePoints
|
||||
@ -300,28 +363,44 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
||||
if e != nil {
|
||||
break
|
||||
}
|
||||
rid, _, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), req.ActivityID, req.IssueID, slot)
|
||||
} else {
|
||||
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), req.ActivityID, req.IssueID, userID)
|
||||
}
|
||||
}
|
||||
var proof map[string]any
|
||||
var rw *model.ActivityRewardSettings
|
||||
var log *model.ActivityDrawLogs
|
||||
if activity.PlayType == "ichiban" {
|
||||
slot := parseSlotFromRemark(ord.Remark)
|
||||
rid, proof, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), req.ActivityID, req.IssueID, slot)
|
||||
if e2 == nil && rid > 0 {
|
||||
rw, _ = h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
if rw != nil {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||
// 创建抽奖日志
|
||||
log = &model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||
// 保存凭证
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, req.IssueID, userID, proof)
|
||||
// ... 道具卡逻辑
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), req.ActivityID, req.IssueID, userID)
|
||||
rid, proof, e2 = sel.SelectItem(ctx.RequestContext(), req.ActivityID, req.IssueID, userID)
|
||||
if e2 == nil && rid > 0 {
|
||||
rw, _ = h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
if rw != nil {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid, Remark: rw.Name})
|
||||
// 创建抽奖日志
|
||||
log = &model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||
// 保存凭证
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, req.IssueID, userID, proof)
|
||||
// ... 道具卡逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
if e2 != nil || rid <= 0 {
|
||||
if log == nil {
|
||||
break
|
||||
}
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
if rw == nil {
|
||||
break
|
||||
}
|
||||
if activity.PlayType == "ichiban" {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||
} else {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid, Remark: rw.Name})
|
||||
}
|
||||
// 创建抽奖日志(获取ID用于道具卡核销)
|
||||
log := &model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||
// 道具卡效果(奖励倍数/概率提升的简单实现:奖励倍数=额外发同奖品;概率提升=尝试升级到更高等级)
|
||||
fmt.Printf("[道具卡-JoinLottery] 开始检查 活动允许道具卡=%t 请求道具卡ID=%v\n", activity.AllowItemCards, req.ItemCardID)
|
||||
if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
|
||||
@ -367,7 +446,17 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
||||
} else {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: better.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
|
||||
}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
|
||||
// 创建抽奖日志并保存凭据
|
||||
drawLog := &model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1}
|
||||
if err := h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog); err != nil {
|
||||
fmt.Printf("[道具卡-JoinLottery] ❌ 创建抽奖日志失败 err=%v\n", err)
|
||||
} else {
|
||||
if err := strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, req.IssueID, userID, proof); err != nil {
|
||||
fmt.Printf("[道具卡-JoinLottery] ⚠️ 保存凭据失败 DrawLogID=%d IssueID=%d UserID=%d err=%v\n", drawLog.ID, req.IssueID, userID, err)
|
||||
} else {
|
||||
fmt.Printf("[道具卡-JoinLottery] ✅ 保存凭据成功 DrawLogID=%d IssueID=%d\n", drawLog.ID, req.IssueID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[道具卡-JoinLottery] 概率提升未触发\n")
|
||||
}
|
||||
@ -515,12 +604,15 @@ func (h *handler) GetLotteryResult() core.HandlerFunc {
|
||||
slot := parseSlotFromRemark(ord.Remark)
|
||||
if slot >= 0 {
|
||||
if err := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", issueID, slot, userID, orderID).Error; err == nil {
|
||||
rid, _, e2 := strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), activityID, issueID, slot)
|
||||
var proof map[string]any
|
||||
rid, proof, e2 := strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), activityID, issueID, slot)
|
||||
if e2 == nil && rid > 0 {
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
if rw != nil {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1})
|
||||
log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, issueID, userID, proof)
|
||||
rsp.Result = map[string]any{"reward_id": rid, "reward_name": rw.Name}
|
||||
}
|
||||
}
|
||||
@ -528,7 +620,8 @@ func (h *handler) GetLotteryResult() core.HandlerFunc {
|
||||
}
|
||||
} else {
|
||||
sel := strat.NewDefault(h.readDB, h.writeDB)
|
||||
rid, _, e2 := sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
||||
var proof map[string]any
|
||||
rid, proof, e2 := sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
||||
if e2 == nil && rid > 0 {
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
_ = sel.GrantReward(ctx.RequestContext(), userID, rid)
|
||||
@ -539,6 +632,7 @@ func (h *handler) GetLotteryResult() core.HandlerFunc {
|
||||
return 1
|
||||
}(), CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, issueID, userID, proof)
|
||||
icID := parseItemCardIDFromRemark(ord.Remark)
|
||||
fmt.Printf("[道具卡-GetLotteryResult] 从订单备注解析道具卡ID icID=%d 订单备注=%s\n", icID, ord.Remark)
|
||||
if icID > 0 {
|
||||
@ -591,7 +685,17 @@ func (h *handler) GetLotteryResult() core.HandlerFunc {
|
||||
rid2 := better.ID
|
||||
fmt.Printf("[道具卡-GetLotteryResult] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: orderID, ProductID: better.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
|
||||
// 创建升级后的抽奖日志并保存凭据
|
||||
drawLog2 := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1}
|
||||
if err := h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog2); err != nil {
|
||||
fmt.Printf("[道具卡-GetLotteryResult] ❌ 创建升级抽奖日志失败 err=%v\n", err)
|
||||
} else {
|
||||
if err := strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog2.ID, issueID, userID, proof); err != nil {
|
||||
fmt.Printf("[道具卡-GetLotteryResult] ⚠️ 保存升级凭据失败 DrawLogID=%d IssueID=%d UserID=%d err=%v\n", drawLog2.ID, issueID, userID, err)
|
||||
} else {
|
||||
fmt.Printf("[道具卡-GetLotteryResult] ✅ 保存升级凭据成功 DrawLogID=%d IssueID=%d\n", drawLog2.ID, issueID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[道具卡-GetLotteryResult] 概率提升未触发\n")
|
||||
}
|
||||
@ -1065,115 +1169,132 @@ func (h *handler) processInstantDraw(ctx core.Context, userID int64, activity *m
|
||||
rid := int64(0)
|
||||
var e2 error
|
||||
if activity.PlayType == "ichiban" {
|
||||
slot := func() int64 {
|
||||
if len(slotsIdx) > 0 && len(slotsIdx) == len(rem) {
|
||||
for cur < len(rem) && rem[cur] == 0 {
|
||||
cur++
|
||||
// ... (inside loop)
|
||||
var proof map[string]any
|
||||
if activity.PlayType == "ichiban" {
|
||||
slot := func() int64 {
|
||||
// ... (existing slot parsing logic)
|
||||
if len(slotsIdx) > 0 && len(slotsIdx) == len(rem) {
|
||||
for cur < len(rem) && rem[cur] == 0 {
|
||||
cur++
|
||||
}
|
||||
if cur >= len(rem) {
|
||||
return -1
|
||||
}
|
||||
rem[cur]--
|
||||
return slotsIdx[cur] - 1
|
||||
}
|
||||
if cur >= len(rem) {
|
||||
return -1
|
||||
return parseSlotFromRemark(ord.Remark)
|
||||
}()
|
||||
if slot >= 0 {
|
||||
var cnt int64
|
||||
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index=?", issueID, slot).Scan(&cnt).Error
|
||||
if cnt > 0 {
|
||||
break
|
||||
}
|
||||
rem[cur]--
|
||||
return slotsIdx[cur] - 1
|
||||
e := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", issueID, slot, userID, ord.ID).Error
|
||||
if e != nil {
|
||||
break
|
||||
}
|
||||
rid, proof, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), activityID, issueID, slot)
|
||||
} else {
|
||||
rid, proof, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
||||
}
|
||||
return parseSlotFromRemark(ord.Remark)
|
||||
}()
|
||||
if slot >= 0 {
|
||||
var cnt int64
|
||||
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index=?", issueID, slot).Scan(&cnt).Error
|
||||
if cnt > 0 {
|
||||
break
|
||||
}
|
||||
e := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", issueID, slot, userID, ord.ID).Error
|
||||
if e != nil {
|
||||
break
|
||||
}
|
||||
rid, _, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), activityID, issueID, slot)
|
||||
} else {
|
||||
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
||||
rid, proof, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
||||
}
|
||||
} else {
|
||||
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
||||
}
|
||||
if e2 != nil || rid <= 0 {
|
||||
break
|
||||
}
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
if rw == nil {
|
||||
break
|
||||
}
|
||||
if activity.PlayType == "ichiban" {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||
} else {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name})
|
||||
}
|
||||
log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||
fmt.Printf("[道具卡-processInstantDraw] 开始检查 活动允许道具卡=%t itemCardID=%v\n", activity.AllowItemCards, itemCardID)
|
||||
if activity.AllowItemCards && itemCardID != nil && *itemCardID > 0 {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
|
||||
uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*itemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).First()
|
||||
if uic != nil {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 找到用户道具卡 ID=%d CardID=%d Status=%d ValidStart=%s ValidEnd=%s\n", uic.ID, uic.CardID, uic.Status, uic.ValidStart.Format(time.RFC3339), uic.ValidEnd.Format(time.RFC3339))
|
||||
ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
|
||||
now := time.Now()
|
||||
if ic != nil {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 找到系统道具卡 ID=%d Name=%s EffectType=%d RewardMultiplierX1000=%d ScopeType=%d ActivityID=%d IssueID=%d Status=%d\n", ic.ID, ic.Name, ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, ic.ActivityID, ic.IssueID, ic.Status)
|
||||
fmt.Printf("[道具卡-processInstantDraw] 时间检查 当前时间=%s ValidStart.After(now)=%t ValidEnd.Before(now)=%t\n", now.Format(time.RFC3339), uic.ValidStart.After(now), uic.ValidEnd.Before(now))
|
||||
if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
|
||||
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == activityID) || (ic.ScopeType == 4 && ic.IssueID == issueID)
|
||||
fmt.Printf("[道具卡-processInstantDraw] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, activityID, issueID, scopeOK)
|
||||
if scopeOK {
|
||||
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s PlayType=%s\n", ic.RewardMultiplierX1000, rid, rw.Name, activity.PlayType)
|
||||
if activity.PlayType == "ichiban" {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||
} else {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
|
||||
}
|
||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 双倍奖励发放完成\n")
|
||||
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
|
||||
uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
|
||||
var better *model.ActivityRewardSettings
|
||||
for _, r := range uprw {
|
||||
if r.Level < rw.Level && r.Quantity != 0 {
|
||||
if better == nil || r.Level < better.Level {
|
||||
better = r
|
||||
if e2 != nil || rid <= 0 {
|
||||
break
|
||||
}
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
if rw == nil {
|
||||
break
|
||||
}
|
||||
if activity.PlayType == "ichiban" {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||
} else {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name})
|
||||
}
|
||||
log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, issueID, userID, proof)
|
||||
fmt.Printf("[道具卡-processInstantDraw] 开始检查 活动允许道具卡=%t itemCardID=%v\n", activity.AllowItemCards, itemCardID)
|
||||
if activity.AllowItemCards && itemCardID != nil && *itemCardID > 0 {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
|
||||
uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*itemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).First()
|
||||
if uic != nil {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 找到用户道具卡 ID=%d CardID=%d Status=%d ValidStart=%s ValidEnd=%s\n", uic.ID, uic.CardID, uic.Status, uic.ValidStart.Format(time.RFC3339), uic.ValidEnd.Format(time.RFC3339))
|
||||
ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
|
||||
now := time.Now()
|
||||
if ic != nil {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 找到系统道具卡 ID=%d Name=%s EffectType=%d RewardMultiplierX1000=%d ScopeType=%d ActivityID=%d IssueID=%d Status=%d\n", ic.ID, ic.Name, ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, ic.ActivityID, ic.IssueID, ic.Status)
|
||||
fmt.Printf("[道具卡-processInstantDraw] 时间检查 当前时间=%s ValidStart.After(now)=%t ValidEnd.Before(now)=%t\n", now.Format(time.RFC3339), uic.ValidStart.After(now), uic.ValidEnd.Before(now))
|
||||
if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
|
||||
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == activityID) || (ic.ScopeType == 4 && ic.IssueID == issueID)
|
||||
fmt.Printf("[道具卡-processInstantDraw] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, activityID, issueID, scopeOK)
|
||||
if scopeOK {
|
||||
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s PlayType=%s\n", ic.RewardMultiplierX1000, rid, rw.Name, activity.PlayType)
|
||||
if activity.PlayType == "ichiban" {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||
} else {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
|
||||
}
|
||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 双倍奖励发放完成\n")
|
||||
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
|
||||
uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
|
||||
var better *model.ActivityRewardSettings
|
||||
for _, r := range uprw {
|
||||
if r.Level < rw.Level && r.Quantity != 0 {
|
||||
if better == nil || r.Level < better.Level {
|
||||
better = r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if better != nil && rand.Int31n(1000) < ic.BoostRateX1000 {
|
||||
rid2 := better.ID
|
||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
|
||||
if activity.PlayType == "ichiban" {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid2)
|
||||
if better != nil && rand.Int31n(1000) < ic.BoostRateX1000 {
|
||||
rid2 := better.ID
|
||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
|
||||
if activity.PlayType == "ichiban" {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid2)
|
||||
} else {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: better.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
|
||||
}
|
||||
// 创建升级后的抽奖日志并保存凭据
|
||||
drawLog2 := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1}
|
||||
if err := h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog2); err != nil {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 创建升级抽奖日志失败 err=%v\n", err)
|
||||
} else {
|
||||
if err := strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog2.ID, issueID, userID, proof); err != nil {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ⚠️ 保存升级凭据失败 DrawLogID=%d IssueID=%d UserID=%d err=%v\n", drawLog2.ID, issueID, userID, err)
|
||||
} else {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 保存升级凭据成功 DrawLogID=%d IssueID=%d\n", drawLog2.ID, issueID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: better.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
|
||||
fmt.Printf("[道具卡-processInstantDraw] 概率提升未触发\n")
|
||||
}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
|
||||
} else {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 概率提升未触发\n")
|
||||
fmt.Printf("[道具卡-processInstantDraw] ⚠️ 道具卡效果类型不匹配或倍数不足 EffectType=%d RewardMultiplierX1000=%d\n", ic.EffectType, ic.RewardMultiplierX1000)
|
||||
}
|
||||
fmt.Printf("[道具卡-processInstantDraw] 核销道具卡 用户道具卡ID=%d\n", *itemCardID)
|
||||
_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*itemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).Updates(map[string]any{h.readDB.UserItemCards.Status.ColumnName().String(): 2, h.readDB.UserItemCards.UsedDrawLogID.ColumnName().String(): log.ID, h.readDB.UserItemCards.UsedActivityID.ColumnName().String(): activityID, h.readDB.UserItemCards.UsedIssueID.ColumnName().String(): issueID, h.readDB.UserItemCards.UsedAt.ColumnName().String(): time.Now()})
|
||||
} else {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ⚠️ 道具卡效果类型不匹配或倍数不足 EffectType=%d RewardMultiplierX1000=%d\n", ic.EffectType, ic.RewardMultiplierX1000)
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 范围检查失败\n")
|
||||
}
|
||||
fmt.Printf("[道具卡-processInstantDraw] 核销道具卡 用户道具卡ID=%d\n", *itemCardID)
|
||||
_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*itemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).Updates(map[string]any{h.readDB.UserItemCards.Status.ColumnName().String(): 2, h.readDB.UserItemCards.UsedDrawLogID.ColumnName().String(): log.ID, h.readDB.UserItemCards.UsedActivityID.ColumnName().String(): activityID, h.readDB.UserItemCards.UsedIssueID.ColumnName().String(): issueID, h.readDB.UserItemCards.UsedAt.ColumnName().String(): time.Now()})
|
||||
} else {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 范围检查失败\n")
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 时间检查失败\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 时间检查失败\n")
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
|
||||
fmt.Printf("[道具卡-processInstantDraw] 跳过道具卡检查\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[道具卡-processInstantDraw] 跳过道具卡检查\n")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -129,12 +129,15 @@ func (h *handler) LotteryResultByOrder() core.HandlerFunc {
|
||||
if cnt > 0 {
|
||||
st = "slot_unavailable"
|
||||
} else if err := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", iss, slot, userID, ord.ID).Error; err == nil {
|
||||
rid, _, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), act.ID, iss, slot)
|
||||
var proof map[string]any
|
||||
rid, proof, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), act.ID, iss, slot)
|
||||
if e2 == nil && rid > 0 {
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
if rw != nil {
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1})
|
||||
drawLog := &model.ActivityDrawLogs{UserID: userID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, iss, userID, proof)
|
||||
completed++
|
||||
items = append(items, orderResultItem{RewardID: rid, RewardName: rw.Name, Level: rw.Level, DrawIndex: int32(completed)})
|
||||
}
|
||||
@ -143,12 +146,15 @@ func (h *handler) LotteryResultByOrder() core.HandlerFunc {
|
||||
}
|
||||
} else {
|
||||
sel := strat.NewDefault(h.readDB, h.writeDB)
|
||||
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), act.ID, iss, userID)
|
||||
var proof map[string]any
|
||||
rid, proof, e2 = sel.SelectItem(ctx.RequestContext(), act.ID, iss, userID)
|
||||
if e2 == nil && rid > 0 {
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
if rw != nil {
|
||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &act.ID, RewardID: &rid, Remark: rw.Name})
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1})
|
||||
drawLog := &model.ActivityDrawLogs{UserID: userID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, iss, userID, proof)
|
||||
completed++
|
||||
items = append(items, orderResultItem{RewardID: rid, RewardName: rw.Name, Level: rw.Level, DrawIndex: int32(completed)})
|
||||
}
|
||||
@ -256,22 +262,44 @@ func parseIssueIDFromRemark(remark string) int64 {
|
||||
if remark == "" {
|
||||
return 0
|
||||
}
|
||||
p := 0
|
||||
// Try "issue:" or "matching_game:issue:"
|
||||
// Split by |
|
||||
segs := make([]string, 0)
|
||||
last := 0
|
||||
for i := 0; i < len(remark); i++ {
|
||||
if remark[i] == '|' {
|
||||
seg := remark[p:i]
|
||||
if len(seg) > 6 && seg[:6] == "issue:" {
|
||||
var n int64
|
||||
for j := 6; j < len(seg); j++ {
|
||||
c := seg[j]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
n = n*10 + int64(c-'0')
|
||||
segs = append(segs, remark[last:i])
|
||||
last = i + 1
|
||||
}
|
||||
}
|
||||
if last < len(remark) {
|
||||
segs = append(segs, remark[last:])
|
||||
}
|
||||
|
||||
for _, seg := range segs {
|
||||
// handle 'issue:123'
|
||||
if len(seg) > 6 && seg[:6] == "issue:" {
|
||||
var n int64
|
||||
for j := 6; j < len(seg); j++ {
|
||||
c := seg[j]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
return n
|
||||
n = n*10 + int64(c-'0')
|
||||
}
|
||||
p = i + 1
|
||||
return n
|
||||
}
|
||||
// handle 'matching_game:issue:123'
|
||||
if len(seg) > 20 && seg[:20] == "matching_game:issue:" {
|
||||
var n int64
|
||||
for j := 20; j < len(seg); j++ {
|
||||
c := seg[j]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
n = n*10 + int64(c-'0')
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1
internal/api/activity/matching_game_app_test.go
Normal file
1
internal/api/activity/matching_game_app_test.go
Normal file
@ -0,0 +1 @@
|
||||
package app
|
||||
21
internal/api/activity/matching_game_cleanup_test.go
Normal file
21
internal/api/activity/matching_game_cleanup_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// MockLogger for testing
|
||||
type MockLogger struct {
|
||||
logger.CustomLogger
|
||||
}
|
||||
|
||||
func TestCleanupExpiredMatchingGames(t *testing.T) {
|
||||
// Deprecated: Redis handles expiry now.
|
||||
t.Skip("Deprecated: Redis handles expiry now")
|
||||
/*
|
||||
// 1. Setup
|
||||
gameSessionsMutex.Lock()
|
||||
...
|
||||
*/
|
||||
}
|
||||
137
internal/api/activity/matching_game_verify_test.go
Normal file
137
internal/api/activity/matching_game_verify_test.go
Normal file
@ -0,0 +1,137 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// SimulateClientPlay mimics the frontend logic: Match -> Eliminate -> Fill -> Reshuffle
|
||||
func SimulateClientPlay(initialCards []*MatchingCard) int {
|
||||
// Deep copy to avoid modifying original test data
|
||||
deck := make([]*MatchingCard, len(initialCards))
|
||||
copy(deck, initialCards)
|
||||
|
||||
board := make([]*MatchingCard, 9)
|
||||
// Initial fill
|
||||
for i := 0; i < 9; i++ {
|
||||
board[i] = deck[0]
|
||||
deck = deck[1:]
|
||||
}
|
||||
|
||||
pairsFound := 0
|
||||
|
||||
for {
|
||||
// 1. Check for matches on board
|
||||
counts := make(map[string][]int) // type -> list of indices
|
||||
for i, c := range board {
|
||||
if c != nil {
|
||||
// Cast CardType to string for map key
|
||||
counts[string(c.Type)] = append(counts[string(c.Type)], i)
|
||||
}
|
||||
}
|
||||
|
||||
matchedType := ""
|
||||
for t, indices := range counts {
|
||||
if len(indices) >= 2 {
|
||||
matchedType = t
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchedType != "" {
|
||||
// Elimination
|
||||
pairsFound++
|
||||
indices := counts[matchedType]
|
||||
// Remove first 2
|
||||
idx1, idx2 := indices[0], indices[1]
|
||||
board[idx1] = nil
|
||||
board[idx2] = nil
|
||||
|
||||
// Filling: Fill empty slots from deck
|
||||
for i := 0; i < 9; i++ {
|
||||
if board[i] == nil && len(deck) > 0 {
|
||||
board[i] = deck[0]
|
||||
deck = deck[1:]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Deadlock (No matches on board)
|
||||
// User requirement: "Stop when no pairs can be generated" (i.e., No Reshuffle)
|
||||
// If we are stuck, we stop.
|
||||
break
|
||||
}
|
||||
}
|
||||
return pairsFound
|
||||
}
|
||||
|
||||
// TestVerification_DataIntegrity simulates the PreOrder logic 10000 times
|
||||
func TestVerification_DataIntegrity(t *testing.T) {
|
||||
fmt.Println("=== Starting Full Game Simulation (10000 Runs) ===")
|
||||
// Using 10k runs to keep test time reasonable
|
||||
|
||||
configs := []CardTypeConfig{
|
||||
{Code: "A", Name: "TypeA", Quantity: 9},
|
||||
{Code: "B", Name: "TypeB", Quantity: 9},
|
||||
{Code: "C", Name: "TypeC", Quantity: 9},
|
||||
{Code: "D", Name: "TypeD", Quantity: 9},
|
||||
{Code: "E", Name: "TypeE", Quantity: 9},
|
||||
{Code: "F", Name: "TypeF", Quantity: 9},
|
||||
{Code: "G", Name: "TypeG", Quantity: 9},
|
||||
{Code: "H", Name: "TypeH", Quantity: 9},
|
||||
{Code: "I", Name: "TypeI", Quantity: 9},
|
||||
{Code: "J", Name: "TypeJ", Quantity: 9},
|
||||
{Code: "K", Name: "TypeK", Quantity: 9},
|
||||
}
|
||||
|
||||
scoreDist := make(map[int]int)
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
// 1. Simulate PreOrder generation
|
||||
game := NewMatchingGameWithConfig(configs, fmt.Sprintf("pos_%d", i))
|
||||
|
||||
// 2. Reconstruct "all_cards"
|
||||
allCards := make([]*MatchingCard, 0, 99)
|
||||
for _, c := range game.Board {
|
||||
if c != nil {
|
||||
allCards = append(allCards, c)
|
||||
}
|
||||
}
|
||||
allCards = append(allCards, game.Deck...)
|
||||
|
||||
// 3. Play the game!
|
||||
score := SimulateClientPlay(allCards)
|
||||
scoreDist[score]++
|
||||
// Note: Without reshuffle, score < 44 is expected.
|
||||
}
|
||||
|
||||
// Calculate Stats
|
||||
totalScore := 0
|
||||
var allScores []int
|
||||
for s := 0; s <= 44; s++ {
|
||||
count := scoreDist[s]
|
||||
for c := 0; c < count; c++ {
|
||||
allScores = append(allScores, s)
|
||||
totalScore += s
|
||||
}
|
||||
}
|
||||
|
||||
mean := float64(totalScore) / float64(len(allScores))
|
||||
median := allScores[len(allScores)/2]
|
||||
|
||||
fmt.Println("\n=== No-Reshuffle Statistical Analysis (10000 Runs) ===")
|
||||
fmt.Printf("Mean Score: %.2f / 44\n", mean)
|
||||
fmt.Printf("Median Score: %d / 44\n", median)
|
||||
fmt.Printf("Pass Rate: %.2f%%\n", float64(scoreDist[44])/100.0)
|
||||
fmt.Println("------------------------------------------------")
|
||||
|
||||
// Output Distribution Segments
|
||||
fmt.Println("Detailed Distribution:")
|
||||
cumulative := 0
|
||||
for s := 0; s <= 44; s++ {
|
||||
count := scoreDist[s]
|
||||
if count > 0 {
|
||||
cumulative += count
|
||||
fmt.Printf("Score %d: %d times (%.2f%%) [Cum: %.2f%%]\n", s, count, float64(count)/100.0, float64(cumulative)/100.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,18 +186,21 @@ func (h *handler) SettleIssue() core.HandlerFunc {
|
||||
// 人数达标统一开奖
|
||||
s := strat.NewDefault(h.readDB, h.writeDB)
|
||||
for _, o := range orders {
|
||||
rid, _, e2 := s.SelectItem(ctx.RequestContext(), iss.ActivityID, iss.ID, o.UserID)
|
||||
rid, proof, e2 := s.SelectItem(ctx.RequestContext(), iss.ActivityID, iss.ID, o.UserID)
|
||||
if e2 != nil || rid <= 0 {
|
||||
continue
|
||||
}
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
_ = s.GrantReward(ctx.RequestContext(), o.UserID, rid)
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: o.UserID, IssueID: iss.ID, OrderID: o.ID, RewardID: rid, IsWinner: 1, Level: func() int32 {
|
||||
drawLog := &model.ActivityDrawLogs{UserID: o.UserID, IssueID: iss.ID, OrderID: o.ID, RewardID: rid, IsWinner: 1, Level: func() int32 {
|
||||
if rw != nil {
|
||||
return rw.Level
|
||||
}
|
||||
return 1
|
||||
}(), CurrentLevel: 1})
|
||||
}(), CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)
|
||||
// 保存可验证凭据
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, iss.ID, o.UserID, proof)
|
||||
granted++
|
||||
}
|
||||
}
|
||||
@ -205,18 +208,21 @@ func (h *handler) SettleIssue() core.HandlerFunc {
|
||||
// 即时或强制:统一开奖
|
||||
s := strat.NewDefault(h.readDB, h.writeDB)
|
||||
for _, o := range orders {
|
||||
rid, _, e2 := s.SelectItem(ctx.RequestContext(), iss.ActivityID, iss.ID, o.UserID)
|
||||
rid, proof, e2 := s.SelectItem(ctx.RequestContext(), iss.ActivityID, iss.ID, o.UserID)
|
||||
if e2 != nil || rid <= 0 {
|
||||
continue
|
||||
}
|
||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||
_ = s.GrantReward(ctx.RequestContext(), o.UserID, rid)
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: o.UserID, IssueID: iss.ID, OrderID: o.ID, RewardID: rid, IsWinner: 1, Level: func() int32 {
|
||||
drawLog := &model.ActivityDrawLogs{UserID: o.UserID, IssueID: iss.ID, OrderID: o.ID, RewardID: rid, IsWinner: 1, Level: func() int32 {
|
||||
if rw != nil {
|
||||
return rw.Level
|
||||
}
|
||||
return 1
|
||||
}(), CurrentLevel: 1})
|
||||
}(), CurrentLevel: 1}
|
||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)
|
||||
// 保存可验证凭据
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, iss.ID, o.UserID, proof)
|
||||
granted++
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
"bindbox-game/internal/pkg/wechat"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
usersvc "bindbox-game/internal/service/user"
|
||||
)
|
||||
|
||||
type listPayOrdersRequest struct {
|
||||
@ -224,7 +225,8 @@ type getPayOrderResponse struct {
|
||||
UnitPrice int64 `json:"unit_price"`
|
||||
Amount int64 `json:"amount"`
|
||||
} `json:"reward_items"`
|
||||
RewardShipments []*model.ShippingRecords `json:"reward_shipments"`
|
||||
RewardShipments []*model.ShippingRecords `json:"reward_shipments"`
|
||||
DrawReceipts []*usersvc.DrawReceiptInfo `json:"draw_receipts"`
|
||||
}
|
||||
|
||||
func (h *handler) GetPayOrderDetail() core.HandlerFunc {
|
||||
@ -595,6 +597,51 @@ func (h *handler) GetPayOrderDetail() core.HandlerFunc {
|
||||
rsp.RewardShipments = shipsAll
|
||||
}
|
||||
}
|
||||
|
||||
// 填充抽奖凭据 Verify Seed Data
|
||||
if order.SourceType == 2 { // Buffer/Lottery orders
|
||||
drawLogs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityDrawLogs.OrderID.Eq(order.ID)).Find()
|
||||
var logIDs []int64
|
||||
for _, l := range drawLogs {
|
||||
logIDs = append(logIDs, l.ID)
|
||||
}
|
||||
if len(logIDs) > 0 {
|
||||
receipts, _ := h.readDB.ActivityDrawReceipts.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityDrawReceipts.DrawLogID.In(logIDs...)).Find()
|
||||
var drList []*usersvc.DrawReceiptInfo
|
||||
for i, r := range receipts {
|
||||
// Find rewardID from logs if possible, though receipt has DrawLogID
|
||||
var rid int64
|
||||
for _, l := range drawLogs {
|
||||
if l.ID == r.DrawLogID {
|
||||
rid = l.RewardID
|
||||
break
|
||||
}
|
||||
}
|
||||
drList = append(drList, &usersvc.DrawReceiptInfo{
|
||||
DrawLogID: r.DrawLogID,
|
||||
RewardID: rid,
|
||||
DrawIndex: i + 1, // approximate
|
||||
AlgoVersion: r.AlgoVersion,
|
||||
RoundID: r.RoundID,
|
||||
DrawID: r.DrawID,
|
||||
ClientID: r.ClientID,
|
||||
Timestamp: r.Timestamp,
|
||||
ServerSeedHash: r.ServerSeedHash,
|
||||
ServerSubSeed: r.ServerSubSeed,
|
||||
ClientSeed: r.ClientSeed,
|
||||
Nonce: r.Nonce,
|
||||
ItemsRoot: r.ItemsRoot,
|
||||
WeightsTotal: r.WeightsTotal,
|
||||
SelectedIndex: r.SelectedIndex,
|
||||
RandProof: r.RandProof,
|
||||
Signature: r.Signature,
|
||||
ItemsSnapshot: r.ItemsSnapshot,
|
||||
})
|
||||
}
|
||||
rsp.DrawReceipts = drList
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Payload(rsp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,20 +98,70 @@ func (h *handler) CreateRefund() core.HandlerFunc {
|
||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 160002, err.Error()))
|
||||
return
|
||||
}
|
||||
// 全额退款:恢复订单中使用的优惠券(优先使用结构化明细表)
|
||||
var cnt int64
|
||||
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM order_coupons WHERE order_id=?", order.ID).Scan(&cnt).Error
|
||||
if cnt > 0 {
|
||||
type ocRow struct {
|
||||
UserCouponID int64
|
||||
AppliedAmount int64
|
||||
}
|
||||
var rows []ocRow
|
||||
_ = h.repo.GetDbR().Raw("SELECT user_coupon_id, applied_amount FROM order_coupons WHERE order_id=?", order.ID).Scan(&rows).Error
|
||||
for _, r := range rows {
|
||||
if r.UserCouponID > 0 && r.AppliedAmount > 0 {
|
||||
_ = h.repo.GetDbW().Exec("UPDATE user_coupons SET balance_amount=COALESCE(balance_amount,0)+?, status=1, used_order_id=0, used_at=NULL WHERE id=? AND user_id=?", r.AppliedAmount, r.UserCouponID, order.UserID).Error
|
||||
// 全额退款:恢复订单中使用的优惠券(细化余额与状态逻辑)
|
||||
type ocRow struct {
|
||||
UserCouponID int64
|
||||
AppliedAmount int64
|
||||
DiscountType int32
|
||||
DiscountValue int64
|
||||
BalanceAmount int64
|
||||
}
|
||||
var rows []ocRow
|
||||
_ = h.repo.GetDbR().Raw("SELECT oc.user_coupon_id, oc.applied_amount, sc.discount_type, sc.discount_value, uc.balance_amount FROM order_coupons oc JOIN user_coupons uc ON uc.id=oc.user_coupon_id JOIN system_coupons sc ON sc.id=uc.coupon_id WHERE oc.order_id=?", order.ID).Scan(&rows).Error
|
||||
for _, r := range rows {
|
||||
if r.UserCouponID > 0 && r.AppliedAmount > 0 {
|
||||
newBal := r.BalanceAmount + r.AppliedAmount
|
||||
if r.DiscountType == 1 { // 直金额券:判断回退后是否全满
|
||||
if newBal >= r.DiscountValue {
|
||||
_ = h.repo.GetDbW().Exec("UPDATE user_coupons SET balance_amount=?, status=1, used_order_id=0, used_at=NULL WHERE id=?", r.DiscountValue, r.UserCouponID).Error
|
||||
newBal = r.DiscountValue
|
||||
} else {
|
||||
// 若金额未满,维持 status=2 (已使用/使用中)
|
||||
_ = h.repo.GetDbW().Exec("UPDATE user_coupons SET balance_amount=?, status=2 WHERE id=?", newBal, r.UserCouponID).Error
|
||||
}
|
||||
} else { // 满减/折扣券:一律恢复为未使用
|
||||
_ = h.repo.GetDbW().Exec("UPDATE user_coupons SET status=1, used_order_id=0, used_at=NULL WHERE id=?", r.UserCouponID).Error
|
||||
newBal = 0
|
||||
}
|
||||
// 记录流水
|
||||
ledger := &model.UserCouponLedger{
|
||||
UserID: order.UserID,
|
||||
UserCouponID: r.UserCouponID,
|
||||
ChangeAmount: r.AppliedAmount,
|
||||
BalanceAfter: newBal,
|
||||
OrderID: order.ID,
|
||||
Action: "refund_restore",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
_ = h.repo.GetDbW().Create(ledger).Error
|
||||
}
|
||||
}
|
||||
|
||||
// 全额退款:回收中奖资产与奖品库存
|
||||
type invRow struct {
|
||||
ID int64
|
||||
RewardID int64
|
||||
}
|
||||
var invs []invRow
|
||||
_ = h.repo.GetDbR().Raw("SELECT id, reward_id FROM user_inventory WHERE order_id=? AND status=1", order.ID).Scan(&invs).Error
|
||||
for _, inv := range invs {
|
||||
// 更新状态为2 (作废)
|
||||
_ = h.repo.GetDbW().Exec("UPDATE user_inventory SET status=2, updated_at=NOW(3), remark=CONCAT(IFNULL(remark,''),'|refund_void') WHERE id=?", inv.ID).Error
|
||||
// 恢复奖品库存 (ActivityRewardSettings)
|
||||
if inv.RewardID > 0 {
|
||||
_ = h.repo.GetDbW().Exec("UPDATE activity_reward_settings SET quantity = quantity + 1 WHERE id=?", inv.RewardID).Error
|
||||
}
|
||||
}
|
||||
|
||||
// 全额退款:取消待发货记录
|
||||
_ = h.repo.GetDbW().Exec("UPDATE shipping_records SET status=4, updated_at=NOW(3), remark=CONCAT(IFNULL(remark,''),'|refund_cancel') WHERE order_id=? AND status=1", order.ID).Error
|
||||
|
||||
// 全额退款:回退道具卡
|
||||
var itemCardIDs []int64
|
||||
_ = h.repo.GetDbR().Raw("SELECT user_item_card_id FROM activity_draw_effects WHERE draw_log_id IN (SELECT id FROM activity_draw_logs WHERE order_id=?)", order.ID).Scan(&itemCardIDs).Error
|
||||
for _, icID := range itemCardIDs {
|
||||
if icID > 0 {
|
||||
_ = h.repo.GetDbW().Exec("UPDATE user_item_cards SET status=1, used_at=NULL, used_draw_log_id=0, used_activity_id=0, used_issue_id=0, updated_at=NOW(3) WHERE id=?", icID).Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ type rewardItem struct {
|
||||
Level int32 `json:"level" binding:"required"`
|
||||
Sort int32 `json:"sort"`
|
||||
IsBoss int32 `json:"is_boss"`
|
||||
MinScore int64 `json:"min_score"`
|
||||
}
|
||||
|
||||
type createRewardsRequest struct {
|
||||
@ -75,6 +76,7 @@ func (h *handler) CreateIssueRewards() core.HandlerFunc {
|
||||
Level: r.Level,
|
||||
Sort: r.Sort,
|
||||
IsBoss: r.IsBoss,
|
||||
MinScore: r.MinScore,
|
||||
})
|
||||
}
|
||||
|
||||
@ -124,6 +126,7 @@ func (h *handler) ListIssueRewards() core.HandlerFunc {
|
||||
Level: v.Level,
|
||||
Sort: v.Sort,
|
||||
IsBoss: v.IsBoss,
|
||||
MinScore: v.MinScore,
|
||||
}
|
||||
}
|
||||
ctx.Payload(res)
|
||||
@ -139,6 +142,7 @@ type modifyRewardRequest struct {
|
||||
Level *int32 `json:"level"`
|
||||
Sort *int32 `json:"sort"`
|
||||
IsBoss *int32 `json:"is_boss"`
|
||||
MinScore *int64 `json:"min_score"`
|
||||
}
|
||||
|
||||
// ModifyIssueReward 更新期数奖励
|
||||
@ -180,6 +184,7 @@ func (h *handler) ModifyIssueReward() core.HandlerFunc {
|
||||
Level: req.Level,
|
||||
Sort: req.Sort,
|
||||
IsBoss: req.IsBoss,
|
||||
MinScore: req.MinScore,
|
||||
}
|
||||
if err := h.activity.ModifyIssueReward(ctx.RequestContext(), rewardID, in); err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, err.Error()))
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package pay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@ -344,7 +345,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
break
|
||||
}
|
||||
// 位置已在上面占用,这里直接选择奖品
|
||||
rid, _, e2 := strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), aid, iss, slot)
|
||||
rid, proof, e2 := strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), aid, iss, slot)
|
||||
fmt.Printf("[支付回调-抽奖] SelectItemBySlot 结果 rid=%d err=%v\n", rid, e2)
|
||||
if e2 != nil || rid <= 0 {
|
||||
fmt.Printf("[支付回调-抽奖] ❌ SelectItemBySlot 失败,跳出循环\n")
|
||||
@ -362,6 +363,8 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
fmt.Printf("[支付回调-抽奖] ❌ 创建开奖日志失败 err=%v,可能已存在,跳过\n", errLog)
|
||||
continue
|
||||
}
|
||||
// 保存抽奖凭据(种子数据)供用户验证
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, iss, ord.UserID, proof)
|
||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), ord.UserID, rid)
|
||||
// 道具卡效果处理
|
||||
fmt.Printf("[支付回调-道具卡] 开始检查 活动允许道具卡=%t 道具卡ID=%d\n", act.AllowItemCards, icID)
|
||||
@ -398,7 +401,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
} else {
|
||||
sel := strat.NewDefault(h.readDB, h.writeDB)
|
||||
for i := done; i < dc; i++ {
|
||||
rid, _, e2 := sel.SelectItem(ctx.RequestContext(), aid, iss, ord.UserID)
|
||||
rid, proof, e2 := sel.SelectItem(ctx.RequestContext(), aid, iss, ord.UserID)
|
||||
if e2 != nil || rid <= 0 {
|
||||
break
|
||||
}
|
||||
@ -412,6 +415,8 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
fmt.Printf("[支付回调-默认玩法] ❌ 创建开奖日志失败 err=%v,可能已存在,跳过\n", errLog)
|
||||
break
|
||||
}
|
||||
// 保存抽奖凭据(种子数据)供用户验证
|
||||
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, iss, ord.UserID, proof)
|
||||
_, errGrant := h.user.GrantRewardToOrder(ctx.RequestContext(), ord.UserID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &aid, RewardID: &rid, Remark: rw.Name})
|
||||
if errGrant != nil {
|
||||
fmt.Printf("[支付回调-默认玩法] ❌ 发奖失败 err=%v,执行退款\n", errGrant)
|
||||
@ -467,7 +472,8 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
}
|
||||
// 【开奖后虚拟发货】即时开奖完成后上传虚拟发货
|
||||
go func(orderID int64, orderNo string, userID int64, actName string) {
|
||||
drawLogs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(orderID)).Find()
|
||||
bgCtx := context.Background()
|
||||
drawLogs, _ := h.readDB.ActivityDrawLogs.WithContext(bgCtx).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(orderID)).Find()
|
||||
if len(drawLogs) == 0 {
|
||||
fmt.Printf("[即时开奖-虚拟发货] 没有开奖记录,跳过 order_id=%d\n", orderID)
|
||||
return
|
||||
@ -475,7 +481,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
// 收集赏品名称
|
||||
var rewardNames []string
|
||||
for _, lg := range drawLogs {
|
||||
if rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(lg.RewardID)).First(); rw != nil {
|
||||
if rw, _ := h.readDB.ActivityRewardSettings.WithContext(bgCtx).Where(h.readDB.ActivityRewardSettings.ID.Eq(lg.RewardID)).First(); rw != nil {
|
||||
rewardNames = append(rewardNames, rw.Name)
|
||||
}
|
||||
}
|
||||
@ -485,20 +491,20 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
}
|
||||
// 获取支付交易信息
|
||||
var tx *model.PaymentTransactions
|
||||
tx, _ = h.readDB.PaymentTransactions.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentTransactions.OrderNo.Eq(orderNo)).First()
|
||||
tx, _ = h.readDB.PaymentTransactions.WithContext(bgCtx).Where(h.readDB.PaymentTransactions.OrderNo.Eq(orderNo)).First()
|
||||
if tx == nil || tx.TransactionID == "" {
|
||||
fmt.Printf("[即时开奖-虚拟发货] 没有支付交易记录,跳过 order_no=%s\n", orderNo)
|
||||
return
|
||||
}
|
||||
// 获取用户openid
|
||||
var u *model.Users
|
||||
u, _ = h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(userID)).First()
|
||||
u, _ = h.readDB.Users.WithContext(bgCtx).Where(h.readDB.Users.ID.Eq(userID)).First()
|
||||
payerOpenid := ""
|
||||
if u != nil {
|
||||
payerOpenid = u.Openid
|
||||
}
|
||||
fmt.Printf("[即时开奖-虚拟发货] 上传 order_no=%s transaction_id=%s items_desc=%s\n", orderNo, tx.TransactionID, itemsDesc)
|
||||
if err := wechat.UploadVirtualShippingWithFallback(ctx, &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}, tx.TransactionID, orderNo, payerOpenid, itemsDesc, time.Now()); err != nil {
|
||||
if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}, tx.TransactionID, orderNo, payerOpenid, itemsDesc); err != nil {
|
||||
fmt.Printf("[即时开奖-虚拟发货] 上传失败: %v\n", err)
|
||||
}
|
||||
// 【开奖后推送通知】
|
||||
@ -507,7 +513,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
||||
AppSecret: c.Wechat.AppSecret,
|
||||
LotteryResultTemplateID: c.Wechat.LotteryResultTemplateID,
|
||||
}
|
||||
_ = lotterynotify.SendLotteryResultNotification(ctx.RequestContext(), notifyCfg, payerOpenid, actName, rewardNames, orderNo, time.Now())
|
||||
_ = lotterynotify.SendLotteryResultNotification(bgCtx, notifyCfg, payerOpenid, actName, rewardNames, orderNo, time.Now())
|
||||
}(ord.ID, ord.OrderNo, ord.UserID, act.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,10 +96,10 @@ type cancelOrderRequest struct {
|
||||
}
|
||||
|
||||
type cancelOrderResponse struct {
|
||||
OrderID int64 `json:"order_id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
Status int32 `json:"status"`
|
||||
CancelledAt time.Time `json:"cancelled_at"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
Status int32 `json:"status"`
|
||||
CancelledAt *time.Time `json:"cancelled_at"`
|
||||
}
|
||||
|
||||
// CancelOrder 取消订单
|
||||
|
||||
56
internal/pkg/redis/redis.go
Normal file
56
internal/pkg/redis/redis.go
Normal file
@ -0,0 +1,56 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"bindbox-game/configs"
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
client *redis.Client
|
||||
)
|
||||
|
||||
// Init Initialize Redis client
|
||||
func Init(ctx context.Context, logger logger.CustomLogger) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
cfg := configs.Get().Redis
|
||||
client = redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Addr,
|
||||
Password: cfg.Pass,
|
||||
DB: cfg.DB,
|
||||
DialTimeout: 5 * time.Second,
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
PoolSize: 20,
|
||||
})
|
||||
|
||||
// Ping to verify connection
|
||||
pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
if err = client.Ping(pingCtx).Err(); err != nil {
|
||||
logger.Error("Failed to connect to Redis", zap.String("addr", cfg.Addr), zap.Error(err))
|
||||
return // err set above
|
||||
}
|
||||
logger.Info("Connected to Redis", zap.String("addr", cfg.Addr))
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetClient Returns the singleton Redis client
|
||||
func GetClient() *redis.Client {
|
||||
if client == nil {
|
||||
// Should have been initialized. If not, panic or try init?
|
||||
// For safety in this strict environment, let's assume Init was called in main.
|
||||
// Panic might be safer to detect misuse early.
|
||||
panic("Redis client not initialized")
|
||||
}
|
||||
return client
|
||||
}
|
||||
@ -26,6 +26,7 @@ type ActivityRewardSettings struct {
|
||||
Level int32 `gorm:"column:level;not null;comment:奖级(如1=S 2=A 3=B)" json:"level"` // 奖级(如1=S 2=A 3=B)
|
||||
Sort int32 `gorm:"column:sort;comment:排序" json:"sort"` // 排序
|
||||
IsBoss int32 `gorm:"column:is_boss;comment:Boss 1 是 0 不是" json:"is_boss"` // Boss 1 是 0 不是
|
||||
MinScore int64 `gorm:"column:min_score;not null;default:0;comment:最低得分/碰数要求" json:"min_score"` // 最低得分/碰数要求
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
|
||||
}
|
||||
|
||||
|
||||
@ -12,24 +12,24 @@ const TableNameOrders = "orders"
|
||||
|
||||
// Orders 订单
|
||||
type Orders struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
UserID int64 `gorm:"column:user_id;not null;comment:下单用户ID(user_members.id)" json:"user_id"` // 下单用户ID(user_members.id)
|
||||
OrderNo string `gorm:"column:order_no;not null;comment:业务订单号(唯一)" json:"order_no"` // 业务订单号(唯一)
|
||||
SourceType int32 `gorm:"column:source_type;not null;default:1;comment:来源:1商城直购 2抽奖票据 3其他" json:"source_type"` // 来源:1商城直购 2抽奖票据 3其他
|
||||
TotalAmount int64 `gorm:"column:total_amount;not null;comment:订单总金额(分)" json:"total_amount"` // 订单总金额(分)
|
||||
DiscountAmount int64 `gorm:"column:discount_amount;not null;comment:优惠券抵扣金额(分)" json:"discount_amount"` // 优惠券抵扣金额(分)
|
||||
PointsAmount int64 `gorm:"column:points_amount;not null;comment:积分抵扣金额(分)" json:"points_amount"` // 积分抵扣金额(分)
|
||||
ActualAmount int64 `gorm:"column:actual_amount;not null;comment:实际支付金额(分)" json:"actual_amount"` // 实际支付金额(分)
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:订单状态:1待支付 2已支付 3已取消 4已退款" json:"status"` // 订单状态:1待支付 2已支付 3已取消 4已退款
|
||||
PayPreorderID int64 `gorm:"column:pay_preorder_id;comment:关联预支付单ID(payment_preorder.id)" json:"pay_preorder_id"` // 关联预支付单ID(payment_preorder.id)
|
||||
PaidAt time.Time `gorm:"column:paid_at;comment:支付完成时间" json:"paid_at"` // 支付完成时间
|
||||
CancelledAt time.Time `gorm:"column:cancelled_at;comment:取消时间" json:"cancelled_at"` // 取消时间
|
||||
UserAddressID int64 `gorm:"column:user_address_id;comment:收货地址ID(user_addresses.id)" json:"user_address_id"` // 收货地址ID(user_addresses.id)
|
||||
IsConsumed int32 `gorm:"column:is_consumed;not null;comment:是否已履约/消耗(对虚拟资产)" json:"is_consumed"` // 是否已履约/消耗(对虚拟资产)
|
||||
PointsLedgerID int64 `gorm:"column:points_ledger_id;comment:积分扣减流水ID(user_points_ledger.id)" json:"points_ledger_id"` // 积分扣减流水ID(user_points_ledger.id)
|
||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||
UserID int64 `gorm:"column:user_id;not null;comment:下单用户ID(user_members.id)" json:"user_id"` // 下单用户ID(user_members.id)
|
||||
OrderNo string `gorm:"column:order_no;not null;comment:业务订单号(唯一)" json:"order_no"` // 业务订单号(唯一)
|
||||
SourceType int32 `gorm:"column:source_type;not null;default:1;comment:来源:1商城直购 2抽奖票据 3其他" json:"source_type"` // 来源:1商城直购 2抽奖票据 3其他
|
||||
TotalAmount int64 `gorm:"column:total_amount;not null;comment:订单总金额(分)" json:"total_amount"` // 订单总金额(分)
|
||||
DiscountAmount int64 `gorm:"column:discount_amount;not null;comment:优惠券抵扣金额(分)" json:"discount_amount"` // 优惠券抵扣金额(分)
|
||||
PointsAmount int64 `gorm:"column:points_amount;not null;comment:积分抵扣金额(分)" json:"points_amount"` // 积分抵扣金额(分)
|
||||
ActualAmount int64 `gorm:"column:actual_amount;not null;comment:实际支付金额(分)" json:"actual_amount"` // 实际支付金额(分)
|
||||
Status int32 `gorm:"column:status;not null;default:1;comment:订单状态:1待支付 2已支付 3已取消 4已退款" json:"status"` // 订单状态:1待支付 2已支付 3已取消 4已退款
|
||||
PayPreorderID int64 `gorm:"column:pay_preorder_id;comment:关联预支付单ID(payment_preorder.id)" json:"pay_preorder_id"` // 关联预支付单ID(payment_preorder.id)
|
||||
PaidAt *time.Time `gorm:"column:paid_at;comment:支付完成时间" json:"paid_at"` // 支付完成时间
|
||||
CancelledAt *time.Time `gorm:"column:cancelled_at;comment:取消时间" json:"cancelled_at"` // 取消时间
|
||||
UserAddressID int64 `gorm:"column:user_address_id;comment:收货地址ID(user_addresses.id)" json:"user_address_id"` // 收货地址ID(user_addresses.id)
|
||||
IsConsumed int32 `gorm:"column:is_consumed;not null;comment:是否已履约/消耗(对虚拟资产)" json:"is_consumed"` // 是否已履约/消耗(对虚拟资产)
|
||||
PointsLedgerID int64 `gorm:"column:points_ledger_id;comment:积分扣减流水ID(user_points_ledger.id)" json:"points_ledger_id"` // 积分扣减流水ID(user_points_ledger.id)
|
||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||
}
|
||||
|
||||
// TableName Orders's table name
|
||||
|
||||
@ -12,8 +12,10 @@ import (
|
||||
"bindbox-game/internal/dblogger"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/logger"
|
||||
"bindbox-game/internal/pkg/redis"
|
||||
"bindbox-game/internal/repository/mysql"
|
||||
"bindbox-game/internal/router/interceptor"
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -39,9 +41,15 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Init Redis
|
||||
if err := redis.Init(context.Background(), logger); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rdb := redis.GetClient()
|
||||
|
||||
// 实例化拦截器
|
||||
adminHandler := admin.New(logger, db)
|
||||
activityHandler := activityapi.New(logger, db)
|
||||
activityHandler := activityapi.New(logger, db, rdb)
|
||||
taskCenterHandler := taskcenterapi.New(logger, db)
|
||||
userHandler := userapi.New(logger, db)
|
||||
commonHandler := commonapi.New(logger, db)
|
||||
@ -266,6 +274,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
||||
appPublicApiRouter.GET("/activities/:activity_id/issues", activityHandler.ListActivityIssues())
|
||||
appPublicApiRouter.GET("/activities/:activity_id/issues/:issue_id/rewards", activityHandler.ListIssueRewards())
|
||||
appPublicApiRouter.GET("/activities/:activity_id/issues/:issue_id/draw_logs", activityHandler.ListDrawLogs())
|
||||
appPublicApiRouter.GET("/activities/:activity_id/issues/:issue_id/draw_logs_grouped", activityHandler.ListDrawLogsByLevel())
|
||||
|
||||
// APP 端轮播图
|
||||
appPublicApiRouter.GET("/banners", appapi.NewBanner(logger, db).ListBannersForApp())
|
||||
@ -275,6 +284,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
||||
// 登录保持公开
|
||||
appPublicApiRouter.POST("/users/weixin/login", userHandler.WeixinLogin())
|
||||
appPublicApiRouter.POST("/address-share/submit", userHandler.SubmitAddressShare())
|
||||
appPublicApiRouter.GET("/matching/card_types", activityHandler.ListMatchingCardTypes())
|
||||
}
|
||||
|
||||
// APP 端认证接口路由组
|
||||
@ -317,8 +327,8 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
||||
appAuthApiRouter.GET("/lottery/result", activityHandler.LotteryResultByOrder())
|
||||
|
||||
// 对对碰游戏
|
||||
appAuthApiRouter.POST("/matching/start", activityHandler.StartMatchingGame())
|
||||
appAuthApiRouter.POST("/matching/play", activityHandler.PlayMatchingGame())
|
||||
appAuthApiRouter.POST("/matching/preorder", activityHandler.PreOrderMatchingGame())
|
||||
appAuthApiRouter.POST("/matching/check", activityHandler.CheckMatchingGame())
|
||||
appAuthApiRouter.GET("/matching/state", activityHandler.GetMatchingGameState())
|
||||
|
||||
appAuthApiRouter.POST("/users/:user_id/inventory/address-share/create", userHandler.CreateAddressShare())
|
||||
|
||||
@ -11,219 +11,221 @@ import (
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
// CreateActivity 创建活动
|
||||
// 参数: in 活动创建输入
|
||||
// 返回: 活动记录与错误
|
||||
CreateActivity(ctx context.Context, in CreateActivityInput) (*model.Activities, error)
|
||||
// ModifyActivity 修改活动
|
||||
// 参数: id 活动ID, in 修改输入
|
||||
// 返回: 错误信息
|
||||
ModifyActivity(ctx context.Context, id int64, in ModifyActivityInput) error
|
||||
// DeleteActivity 删除活动
|
||||
// 参数: id 活动ID
|
||||
// 返回: 错误信息
|
||||
DeleteActivity(ctx context.Context, id int64) error
|
||||
// GetActivity 获取活动详情
|
||||
// 参数: id 活动ID
|
||||
// 返回: 活动记录与错误
|
||||
GetActivity(ctx context.Context, id int64) (*model.Activities, error)
|
||||
// ListActivities 活动列表
|
||||
// 参数: in 列表查询输入
|
||||
// 返回: 活动集合、总数与错误
|
||||
ListActivities(ctx context.Context, in ListActivitiesInput) (items []*model.Activities, total int64, err error)
|
||||
// CreateActivity 创建活动
|
||||
// 参数: in 活动创建输入
|
||||
// 返回: 活动记录与错误
|
||||
CreateActivity(ctx context.Context, in CreateActivityInput) (*model.Activities, error)
|
||||
// ModifyActivity 修改活动
|
||||
// 参数: id 活动ID, in 修改输入
|
||||
// 返回: 错误信息
|
||||
ModifyActivity(ctx context.Context, id int64, in ModifyActivityInput) error
|
||||
// DeleteActivity 删除活动
|
||||
// 参数: id 活动ID
|
||||
// 返回: 错误信息
|
||||
DeleteActivity(ctx context.Context, id int64) error
|
||||
// GetActivity 获取活动详情
|
||||
// 参数: id 活动ID
|
||||
// 返回: 活动记录与错误
|
||||
GetActivity(ctx context.Context, id int64) (*model.Activities, error)
|
||||
// ListActivities 活动列表
|
||||
// 参数: in 列表查询输入
|
||||
// 返回: 活动集合、总数与错误
|
||||
ListActivities(ctx context.Context, in ListActivitiesInput) (items []*model.Activities, total int64, err error)
|
||||
|
||||
// ListIssues 活动期列表
|
||||
// 参数: activityID 活动ID, page 页码, pageSize 每页数量
|
||||
// 返回: 期列表、总数与错误
|
||||
ListIssues(ctx context.Context, activityID int64, page, pageSize int) (items []*model.ActivityIssues, total int64, err error)
|
||||
// CreateIssue 创建期
|
||||
// 参数: activityID 活动ID, in 创建输入
|
||||
// 返回: 期记录与错误
|
||||
CreateIssue(ctx context.Context, activityID int64, in CreateIssueInput) (*model.ActivityIssues, error)
|
||||
// ModifyIssue 修改期
|
||||
// 参数: issueID 期ID, in 修改输入
|
||||
// 返回: 错误信息
|
||||
ModifyIssue(ctx context.Context, issueID int64, in ModifyIssueInput) error
|
||||
// DeleteIssue 删除期
|
||||
// 参数: issueID 期ID
|
||||
// 返回: 错误信息
|
||||
DeleteIssue(ctx context.Context, issueID int64) error
|
||||
// ListIssues 活动期列表
|
||||
// 参数: activityID 活动ID, page 页码, pageSize 每页数量
|
||||
// 返回: 期列表、总数与错误
|
||||
ListIssues(ctx context.Context, activityID int64, page, pageSize int) (items []*model.ActivityIssues, total int64, err error)
|
||||
// CreateIssue 创建期
|
||||
// 参数: activityID 活动ID, in 创建输入
|
||||
// 返回: 期记录与错误
|
||||
CreateIssue(ctx context.Context, activityID int64, in CreateIssueInput) (*model.ActivityIssues, error)
|
||||
// ModifyIssue 修改期
|
||||
// 参数: issueID 期ID, in 修改输入
|
||||
// 返回: 错误信息
|
||||
ModifyIssue(ctx context.Context, issueID int64, in ModifyIssueInput) error
|
||||
// DeleteIssue 删除期
|
||||
// 参数: issueID 期ID
|
||||
// 返回: 错误信息
|
||||
DeleteIssue(ctx context.Context, issueID int64) error
|
||||
|
||||
// CreateIssueRewards 批量创建期奖励
|
||||
// 参数: issueID 期ID, rewards 奖励创建输入数组
|
||||
// 返回: 错误信息
|
||||
CreateIssueRewards(ctx context.Context, issueID int64, rewards []CreateRewardInput) error
|
||||
// ListIssueRewards 查询期奖励列表
|
||||
// 参数: issueID 期ID
|
||||
// 返回: 奖励集合与错误
|
||||
ListIssueRewards(ctx context.Context, issueID int64) (items []*model.ActivityRewardSettings, err error)
|
||||
// ModifyIssueReward 修改单个奖励
|
||||
// 参数: rewardID 奖励ID, in 修改输入
|
||||
// 返回: 错误信息
|
||||
ModifyIssueReward(ctx context.Context, rewardID int64, in ModifyRewardInput) error
|
||||
// DeleteIssueReward 删除单个奖励
|
||||
// 参数: rewardID 奖励ID
|
||||
// 返回: 错误信息
|
||||
DeleteIssueReward(ctx context.Context, rewardID int64) error
|
||||
// CreateIssueRewards 批量创建期奖励
|
||||
// 参数: issueID 期ID, rewards 奖励创建输入数组
|
||||
// 返回: 错误信息
|
||||
CreateIssueRewards(ctx context.Context, issueID int64, rewards []CreateRewardInput) error
|
||||
// ListIssueRewards 查询期奖励列表
|
||||
// 参数: issueID 期ID
|
||||
// 返回: 奖励集合与错误
|
||||
ListIssueRewards(ctx context.Context, issueID int64) (items []*model.ActivityRewardSettings, err error)
|
||||
// ModifyIssueReward 修改单个奖励
|
||||
// 参数: rewardID 奖励ID, in 修改输入
|
||||
// 返回: 错误信息
|
||||
ModifyIssueReward(ctx context.Context, rewardID int64, in ModifyRewardInput) error
|
||||
// DeleteIssueReward 删除单个奖励
|
||||
// 参数: rewardID 奖励ID
|
||||
// 返回: 错误信息
|
||||
DeleteIssueReward(ctx context.Context, rewardID int64) error
|
||||
|
||||
// ListDrawLogs 抽奖记录列表
|
||||
// 参数: issueID 期ID, page/pageSize 分页
|
||||
// 返回: 抽奖记录集合、总数与错误
|
||||
ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int) (items []*model.ActivityDrawLogs, total int64, err error)
|
||||
// ListDrawLogs 抽奖记录列表
|
||||
// 参数: issueID 期ID, page/pageSize 分页
|
||||
// 返回: 抽奖记录集合、总数与错误
|
||||
ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int) (items []*model.ActivityDrawLogs, total int64, err error)
|
||||
|
||||
// GetCategoryNames 批量查询分类名称
|
||||
// 参数: ids 分类ID数组
|
||||
// 返回: id->名称映射与错误
|
||||
GetCategoryNames(ctx context.Context, ids []int64) (map[int64]string, error)
|
||||
// GetCategoryNames 批量查询分类名称
|
||||
// 参数: ids 分类ID数组
|
||||
// 返回: id->名称映射与错误
|
||||
GetCategoryNames(ctx context.Context, ids []int64) (map[int64]string, error)
|
||||
|
||||
// CopyActivity 复制活动及其期次与奖励
|
||||
// 参数: activityID 源活动ID
|
||||
// 返回: 新活动ID与错误
|
||||
CopyActivity(ctx context.Context, activityID int64) (int64, error)
|
||||
// CopyActivity 复制活动及其期次与奖励
|
||||
// 参数: activityID 源活动ID
|
||||
// 返回: 新活动ID与错误
|
||||
CopyActivity(ctx context.Context, activityID int64) (int64, error)
|
||||
|
||||
// SaveActivityDrawConfig 保存活动开奖配置
|
||||
// 参数: activityID 活动ID, cfg 配置
|
||||
// 返回: 错误信息
|
||||
SaveActivityDrawConfig(ctx context.Context, activityID int64, cfg DrawConfig) error
|
||||
// GetActivityDrawConfig 读取活动开奖配置
|
||||
// 参数: activityID 活动ID
|
||||
// 返回: 配置与错误
|
||||
GetActivityDrawConfig(ctx context.Context, activityID int64) (*DrawConfig, error)
|
||||
// SaveActivityDrawConfig 保存活动开奖配置
|
||||
// 参数: activityID 活动ID, cfg 配置
|
||||
// 返回: 错误信息
|
||||
SaveActivityDrawConfig(ctx context.Context, activityID int64, cfg DrawConfig) error
|
||||
// GetActivityDrawConfig 读取活动开奖配置
|
||||
// 参数: activityID 活动ID
|
||||
// 返回: 配置与错误
|
||||
GetActivityDrawConfig(ctx context.Context, activityID int64) (*DrawConfig, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger logger.CustomLogger
|
||||
readDB *dao.Query
|
||||
writeDB *dao.Query
|
||||
repo mysql.Repo
|
||||
logger logger.CustomLogger
|
||||
readDB *dao.Query
|
||||
writeDB *dao.Query
|
||||
repo mysql.Repo
|
||||
}
|
||||
|
||||
func New(l logger.CustomLogger, db mysql.Repo) Service {
|
||||
return &service{
|
||||
logger: l,
|
||||
readDB: dao.Use(db.GetDbR()),
|
||||
writeDB: dao.Use(db.GetDbW()),
|
||||
repo: db,
|
||||
}
|
||||
return &service{
|
||||
logger: l,
|
||||
readDB: dao.Use(db.GetDbR()),
|
||||
writeDB: dao.Use(db.GetDbW()),
|
||||
repo: db,
|
||||
}
|
||||
}
|
||||
|
||||
type CreateActivityInput struct {
|
||||
// Name 活动名称
|
||||
Name string
|
||||
// Banner 活动头图
|
||||
Banner string
|
||||
// Image 活动主图
|
||||
Image string
|
||||
// GameplayIntro 玩法介绍
|
||||
GameplayIntro string
|
||||
// ActivityCategoryID 活动分类ID
|
||||
ActivityCategoryID int64
|
||||
// Status 活动状态
|
||||
Status int32
|
||||
// PriceDraw 单次抽奖价格(分)
|
||||
PriceDraw int64
|
||||
// IsBoss 是否Boss活动
|
||||
IsBoss int32
|
||||
// StartTime 活动开始时间(可选)
|
||||
StartTime *time.Time
|
||||
// EndTime 活动结束时间(可选)
|
||||
EndTime *time.Time
|
||||
AllowItemCards int32
|
||||
AllowCoupons int32
|
||||
// Name 活动名称
|
||||
Name string
|
||||
// Banner 活动头图
|
||||
Banner string
|
||||
// Image 活动主图
|
||||
Image string
|
||||
// GameplayIntro 玩法介绍
|
||||
GameplayIntro string
|
||||
// ActivityCategoryID 活动分类ID
|
||||
ActivityCategoryID int64
|
||||
// Status 活动状态
|
||||
Status int32
|
||||
// PriceDraw 单次抽奖价格(分)
|
||||
PriceDraw int64
|
||||
// IsBoss 是否Boss活动
|
||||
IsBoss int32
|
||||
// StartTime 活动开始时间(可选)
|
||||
StartTime *time.Time
|
||||
// EndTime 活动结束时间(可选)
|
||||
EndTime *time.Time
|
||||
AllowItemCards int32
|
||||
AllowCoupons int32
|
||||
}
|
||||
|
||||
type ModifyActivityInput struct {
|
||||
// Name 活动名称
|
||||
Name string
|
||||
// Banner 活动头图
|
||||
Banner string
|
||||
// Image 活动主图
|
||||
Image string
|
||||
// GameplayIntro 玩法介绍
|
||||
GameplayIntro string
|
||||
// ActivityCategoryID 活动分类ID
|
||||
ActivityCategoryID int64
|
||||
// Status 活动状态
|
||||
Status int32
|
||||
// PriceDraw 单次抽奖价格(分)
|
||||
PriceDraw int64
|
||||
// IsBoss 是否Boss活动
|
||||
IsBoss int32
|
||||
// StartTime 活动开始时间(可选)
|
||||
StartTime *time.Time
|
||||
// EndTime 活动结束时间(可选)
|
||||
EndTime *time.Time
|
||||
AllowItemCards *int32
|
||||
AllowCoupons *int32
|
||||
// Name 活动名称
|
||||
Name string
|
||||
// Banner 活动头图
|
||||
Banner string
|
||||
// Image 活动主图
|
||||
Image string
|
||||
// GameplayIntro 玩法介绍
|
||||
GameplayIntro string
|
||||
// ActivityCategoryID 活动分类ID
|
||||
ActivityCategoryID int64
|
||||
// Status 活动状态
|
||||
Status int32
|
||||
// PriceDraw 单次抽奖价格(分)
|
||||
PriceDraw int64
|
||||
// IsBoss 是否Boss活动
|
||||
IsBoss int32
|
||||
// StartTime 活动开始时间(可选)
|
||||
StartTime *time.Time
|
||||
// EndTime 活动结束时间(可选)
|
||||
EndTime *time.Time
|
||||
AllowItemCards *int32
|
||||
AllowCoupons *int32
|
||||
}
|
||||
|
||||
type ListActivitiesInput struct {
|
||||
// Name 名称过滤
|
||||
Name string
|
||||
// CategoryID 分类过滤
|
||||
CategoryID int64
|
||||
// IsBoss Boss过滤
|
||||
IsBoss *int32
|
||||
// Status 状态过滤
|
||||
Status *int32
|
||||
// Page 页码
|
||||
Page int
|
||||
// PageSize 每页数量
|
||||
PageSize int
|
||||
// Name 名称过滤
|
||||
Name string
|
||||
// CategoryID 分类过滤
|
||||
CategoryID int64
|
||||
// IsBoss Boss过滤
|
||||
IsBoss *int32
|
||||
// Status 状态过滤
|
||||
Status *int32
|
||||
// Page 页码
|
||||
Page int
|
||||
// PageSize 每页数量
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type CreateIssueInput struct {
|
||||
// IssueNumber 期号
|
||||
IssueNumber string
|
||||
// Status 状态
|
||||
Status int32
|
||||
// Sort 排序
|
||||
Sort int32
|
||||
// IssueNumber 期号
|
||||
IssueNumber string
|
||||
// Status 状态
|
||||
Status int32
|
||||
// Sort 排序
|
||||
Sort int32
|
||||
}
|
||||
|
||||
type ModifyIssueInput struct {
|
||||
// IssueNumber 期号
|
||||
IssueNumber string
|
||||
// Status 状态
|
||||
Status int32
|
||||
// Sort 排序
|
||||
Sort int32
|
||||
// IssueNumber 期号
|
||||
IssueNumber string
|
||||
// Status 状态
|
||||
Status int32
|
||||
// Sort 排序
|
||||
Sort int32
|
||||
}
|
||||
|
||||
type CreateRewardInput struct {
|
||||
// ProductID 商品ID
|
||||
ProductID int64
|
||||
// Name 奖励名称
|
||||
Name string
|
||||
// Weight 权重
|
||||
Weight int32
|
||||
// Quantity 数量(-1 表示不限)
|
||||
Quantity int64
|
||||
// OriginalQty 初始数量
|
||||
OriginalQty int64
|
||||
// Level 奖励等级
|
||||
Level int32
|
||||
// Sort 排序
|
||||
Sort int32
|
||||
// IsBoss 是否Boss奖励
|
||||
IsBoss int32
|
||||
// ProductID 商品ID
|
||||
ProductID int64
|
||||
// Name 奖励名称
|
||||
Name string
|
||||
// Weight 权重
|
||||
Weight int32
|
||||
// Quantity 数量(-1 表示不限)
|
||||
Quantity int64
|
||||
// OriginalQty 初始数量
|
||||
OriginalQty int64
|
||||
// Level 奖励等级
|
||||
Level int32
|
||||
// Sort 排序
|
||||
Sort int32
|
||||
// IsBoss 是否Boss奖励
|
||||
IsBoss int32
|
||||
MinScore int64
|
||||
}
|
||||
|
||||
type ModifyRewardInput struct {
|
||||
// ProductID 商品ID
|
||||
ProductID *int64
|
||||
// Name 奖励名称
|
||||
Name string
|
||||
// Weight 权重
|
||||
Weight *int32
|
||||
// Quantity 数量(-1 表示不限)
|
||||
Quantity *int64
|
||||
// OriginalQty 初始数量
|
||||
OriginalQty *int64
|
||||
// Level 奖励等级
|
||||
Level *int32
|
||||
// Sort 排序
|
||||
Sort *int32
|
||||
// IsBoss 是否Boss奖励
|
||||
IsBoss *int32
|
||||
// ProductID 商品ID
|
||||
ProductID *int64
|
||||
// Name 奖励名称
|
||||
Name string
|
||||
// Weight 权重
|
||||
Weight *int32
|
||||
// Quantity 数量(-1 表示不限)
|
||||
Quantity *int64
|
||||
// OriginalQty 初始数量
|
||||
OriginalQty *int64
|
||||
// Level 奖励等级
|
||||
Level *int32
|
||||
// Sort 排序
|
||||
Sort *int32
|
||||
// IsBoss 是否Boss奖励
|
||||
IsBoss *int32
|
||||
MinScore *int64
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ func (s *service) CreateIssueRewards(ctx context.Context, issueID int64, rewards
|
||||
Level: r.Level,
|
||||
Sort: r.Sort,
|
||||
IsBoss: r.IsBoss,
|
||||
MinScore: r.MinScore,
|
||||
}
|
||||
if err := tx.ActivityRewardSettings.WithContext(ctx).Create(item); err != nil {
|
||||
return err
|
||||
|
||||
@ -39,6 +39,9 @@ func (s *service) ModifyIssueReward(ctx context.Context, rewardID int64, in Modi
|
||||
if in.IsBoss != nil {
|
||||
item.IsBoss = *in.IsBoss
|
||||
}
|
||||
if in.MinScore != nil {
|
||||
item.MinScore = *in.MinScore
|
||||
}
|
||||
item.UpdatedAt = time.Now()
|
||||
return s.writeDB.ActivityRewardSettings.WithContext(ctx).Save(item)
|
||||
}
|
||||
|
||||
@ -177,7 +177,7 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo) {
|
||||
}
|
||||
|
||||
// 使用 claim 中的 slot_index 直接获取奖品
|
||||
rid, _, err := ichibanSel.SelectItemBySlot(ctx, aid, iss, claim.SlotIndex)
|
||||
rid, proof, err := ichibanSel.SelectItemBySlot(ctx, aid, iss, claim.SlotIndex)
|
||||
if err != nil || rid <= 0 {
|
||||
fmt.Printf("[定时开奖-一番赏] ❌ SelectItemBySlot失败 err=%v rid=%d\n", err, rid)
|
||||
continue
|
||||
@ -219,6 +219,13 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo) {
|
||||
}
|
||||
|
||||
fmt.Printf("[定时开奖-一番赏] ✅ 开奖成功 UserID=%d OrderID=%d RewardID=%d\n", uid, o.ID, rid)
|
||||
|
||||
// 保存可验证凭据
|
||||
if err := strat.SaveDrawReceipt(ctx, w, drawLog.ID, iss, uid, proof); err != nil {
|
||||
fmt.Printf("[定时开奖-一番赏] ⚠️ 保存凭据失败 DrawLogID=%d IssueID=%d UserID=%d err=%v proof=%+v\n", drawLog.ID, iss, uid, err, proof)
|
||||
} else {
|
||||
fmt.Printf("[定时开奖-一番赏] ✅ 保存凭据成功 DrawLogID=%d IssueID=%d\n", drawLog.ID, iss)
|
||||
}
|
||||
}
|
||||
// 【开奖后虚拟发货】定时一番赏开奖后上传虚拟发货
|
||||
uploadVirtualShippingForScheduledDraw(ctx, r, o.ID, o.OrderNo, uid, func() string {
|
||||
@ -259,7 +266,11 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo) {
|
||||
// 发放奖励(在原订单上添加中奖商品,不创建新订单)
|
||||
_, _ = us.GrantRewardToOrder(ctx, uid, usersvc.GrantRewardToOrderRequest{OrderID: o.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &aid, RewardID: &rid, Remark: rw.Name})
|
||||
// 保存可验证凭据
|
||||
_ = strat.SaveDrawReceipt(ctx, w, drawLog.ID, iss, uid, proof)
|
||||
if err := strat.SaveDrawReceipt(ctx, w, drawLog.ID, iss, uid, proof); err != nil {
|
||||
fmt.Printf("[定时开奖-默认] ⚠️ 保存凭据失败 DrawLogID=%d IssueID=%d UserID=%d err=%v proof=%+v\n", drawLog.ID, iss, uid, err, proof)
|
||||
} else {
|
||||
fmt.Printf("[定时开奖-默认] ✅ 保存凭据成功 DrawLogID=%d IssueID=%d\n", drawLog.ID, iss)
|
||||
}
|
||||
}
|
||||
// 【开奖后虚拟发货】定时开奖后上传虚拟发货
|
||||
uploadVirtualShippingForScheduledDraw(ctx, r, o.ID, o.OrderNo, uid, func() string {
|
||||
@ -324,7 +335,11 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo) {
|
||||
}
|
||||
_, _ = us.GrantRewardToOrder(ctx, uid, usersvc.GrantRewardToOrderRequest{OrderID: o2.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &ia.ID, RewardID: &rid, Remark: rw.Name})
|
||||
// 保存可验证凭据
|
||||
_ = strat.SaveDrawReceipt(ctx, w, drawLog.ID, iss, uid, proof)
|
||||
if err := strat.SaveDrawReceipt(ctx, w, drawLog.ID, iss, uid, proof); err != nil {
|
||||
fmt.Printf("[即时开奖补偿] ⚠️ 保存凭据失败 DrawLogID=%d IssueID=%d UserID=%d err=%v proof=%+v\n", drawLog.ID, iss, uid, err, proof)
|
||||
} else {
|
||||
fmt.Printf("[即时开奖补偿] ✅ 保存凭据成功 DrawLogID=%d IssueID=%d\n", drawLog.ID, iss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,56 +1,77 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"context"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ichibanStrategy struct {
|
||||
read *dao.Query
|
||||
write *dao.Query
|
||||
read *dao.Query
|
||||
write *dao.Query
|
||||
}
|
||||
|
||||
func NewIchiban(read *dao.Query, write *dao.Query) *ichibanStrategy {
|
||||
return &ichibanStrategy{read: read, write: write}
|
||||
return &ichibanStrategy{read: read, write: write}
|
||||
}
|
||||
|
||||
func (s *ichibanStrategy) SelectItemBySlot(ctx context.Context, activityID int64, issueID int64, slotIndex int64) (int64, map[string]any, error) {
|
||||
act, err := s.read.Activities.WithContext(ctx).Where(s.read.Activities.ID.Eq(activityID)).First()
|
||||
if err != nil || act == nil || len(act.CommitmentSeedMaster) == 0 { return 0, nil, errors.New("commitment not found") }
|
||||
rewards, err := s.read.ActivityRewardSettings.WithContext(ctx).ReadDB().Where(s.read.ActivityRewardSettings.IssueID.Eq(issueID)).Order(
|
||||
s.read.ActivityRewardSettings.Level.Desc(),
|
||||
s.read.ActivityRewardSettings.Sort.Asc(),
|
||||
s.read.ActivityRewardSettings.ID.Asc(),
|
||||
).Find()
|
||||
if err != nil || len(rewards) == 0 { return 0, nil, errors.New("no rewards") }
|
||||
var totalSlots int64
|
||||
for _, r := range rewards {
|
||||
if r.OriginalQty > 0 { totalSlots += r.OriginalQty }
|
||||
}
|
||||
if totalSlots <= 0 { return 0, nil, errors.New("no slots") }
|
||||
if slotIndex < 0 || slotIndex >= totalSlots { return 0, nil, errors.New("slot out of range") }
|
||||
// build list
|
||||
slots := make([]int64, 0, totalSlots)
|
||||
for _, r := range rewards {
|
||||
for i := int64(0); i < r.OriginalQty; i++ { slots = append(slots, r.ID) }
|
||||
}
|
||||
// deterministic shuffle by server seed
|
||||
mac := hmac.New(sha256.New, act.CommitmentSeedMaster)
|
||||
for i := int(totalSlots-1); i > 0; i-- {
|
||||
mac.Reset()
|
||||
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issueID)))
|
||||
sum := mac.Sum(nil)
|
||||
rnd := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
|
||||
slots[i], slots[rnd] = slots[rnd], slots[i]
|
||||
}
|
||||
picked := slots[slotIndex]
|
||||
proof := map[string]any{"total_slots": totalSlots, "slot_index": slotIndex}
|
||||
return picked, proof, nil
|
||||
act, err := s.read.Activities.WithContext(ctx).Where(s.read.Activities.ID.Eq(activityID)).First()
|
||||
if err != nil || act == nil || len(act.CommitmentSeedMaster) == 0 {
|
||||
return 0, nil, errors.New("commitment not found")
|
||||
}
|
||||
rewards, err := s.read.ActivityRewardSettings.WithContext(ctx).ReadDB().Where(s.read.ActivityRewardSettings.IssueID.Eq(issueID)).Order(
|
||||
s.read.ActivityRewardSettings.Level.Desc(),
|
||||
s.read.ActivityRewardSettings.Sort.Asc(),
|
||||
s.read.ActivityRewardSettings.ID.Asc(),
|
||||
).Find()
|
||||
if err != nil || len(rewards) == 0 {
|
||||
return 0, nil, errors.New("no rewards")
|
||||
}
|
||||
var totalSlots int64
|
||||
for _, r := range rewards {
|
||||
if r.OriginalQty > 0 {
|
||||
totalSlots += r.OriginalQty
|
||||
}
|
||||
}
|
||||
if totalSlots <= 0 {
|
||||
return 0, nil, errors.New("no slots")
|
||||
}
|
||||
if slotIndex < 0 || slotIndex >= totalSlots {
|
||||
return 0, nil, errors.New("slot out of range")
|
||||
}
|
||||
// build list
|
||||
slots := make([]int64, 0, totalSlots)
|
||||
for _, r := range rewards {
|
||||
for i := int64(0); i < r.OriginalQty; i++ {
|
||||
slots = append(slots, r.ID)
|
||||
}
|
||||
}
|
||||
// deterministic shuffle by server seed
|
||||
mac := hmac.New(sha256.New, act.CommitmentSeedMaster)
|
||||
for i := int(totalSlots - 1); i > 0; i-- {
|
||||
mac.Reset()
|
||||
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issueID)))
|
||||
sum := mac.Sum(nil)
|
||||
rnd := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
|
||||
slots[i], slots[rnd] = slots[rnd], slots[i]
|
||||
}
|
||||
picked := slots[slotIndex]
|
||||
|
||||
// Calculate seed hash for proof
|
||||
sha := sha256.Sum256(act.CommitmentSeedMaster)
|
||||
seedHash := fmt.Sprintf("%x", sha)
|
||||
|
||||
proof := map[string]any{
|
||||
"total_slots": totalSlots,
|
||||
"slot_index": slotIndex,
|
||||
"seed_hash": seedHash,
|
||||
}
|
||||
return picked, proof, nil
|
||||
}
|
||||
|
||||
func (s *ichibanStrategy) GrantReward(ctx context.Context, userID int64, rewardID int64) error {
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package strategy
|
||||
|
||||
// Mock objects for testing (simplified)
|
||||
// Since we can't easily mock the full DAO chain without a comprehensive mock library or interface,
|
||||
// we will rely on checking the logic I just added: manual inspection of the code change was logically sound.
|
||||
// However, to be thorough, I'll write a test that mocks the DB interactions if possible, or use the existing test file `ichiban_test.go` as a base.
|
||||
|
||||
// Let's assume `ichiban_test.go` exists (I saw it in the file list earlier).
|
||||
// I will read it first to see if I can add a case there.
|
||||
66
internal/service/activity/strategy/ichiban_verify_test.go
Normal file
66
internal/service/activity/strategy/ichiban_verify_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"bindbox-game/internal/repository/mysql/dao"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestIchibanProofHasSeedHash(t *testing.T) {
|
||||
// 1. Setup In-Memory DB
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("db open err: %v", err)
|
||||
}
|
||||
|
||||
// 2. Migrate Tables
|
||||
db.Exec(`CREATE TABLE activities (id INTEGER PRIMARY KEY AUTOINCREMENT, commitment_seed_master BLOB, commitment_state_version INTEGER, deleted_at DATETIME);`)
|
||||
db.Exec(`CREATE TABLE activity_reward_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, issue_id INTEGER NOT NULL, product_id INTEGER, name TEXT NOT NULL, weight INTEGER NOT NULL, quantity INTEGER NOT NULL, original_qty INTEGER NOT NULL, level INTEGER NOT NULL, sort INTEGER NOT NULL, is_boss INTEGER NOT NULL, deleted_at DATETIME);`)
|
||||
|
||||
q := dao.Use(db)
|
||||
|
||||
// 3. Insert Test Data
|
||||
seedBytes := []byte("testseedvalue")
|
||||
expectedHash := fmt.Sprintf("%x", sha256.Sum256(seedBytes))
|
||||
|
||||
db.Exec("INSERT INTO activities (id, commitment_seed_master, commitment_state_version) VALUES (?, ?, ?)", 100, seedBytes, 1)
|
||||
|
||||
r1 := &model.ActivityRewardSettings{IssueID: 10, Name: "A", Weight: 10, Quantity: 10, OriginalQty: 5, Level: 1, Sort: 1}
|
||||
q.ActivityRewardSettings.Create(r1)
|
||||
|
||||
// 4. Test SelectItemBySlot
|
||||
s := NewIchiban(q, q)
|
||||
ctx := context.Background()
|
||||
|
||||
_, proof, err := s.SelectItemBySlot(ctx, 100, 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectItemBySlot failed: %v", err)
|
||||
}
|
||||
|
||||
// 5. Verify seed_hash is in proof
|
||||
if proof == nil {
|
||||
t.Fatal("proof is nil")
|
||||
}
|
||||
|
||||
val, ok := proof["seed_hash"]
|
||||
if !ok {
|
||||
t.Fatal("seed_hash missing from proof")
|
||||
}
|
||||
|
||||
seedHash, ok := val.(string)
|
||||
if !ok {
|
||||
t.Fatalf("seed_hash is not a string, got %T", val)
|
||||
}
|
||||
|
||||
if seedHash != expectedHash {
|
||||
t.Fatalf("seed_hash mismatch. got %s, want %s", seedHash, expectedHash)
|
||||
}
|
||||
|
||||
t.Logf("Success: seed_hash found in proof: %s", seedHash)
|
||||
}
|
||||
@ -7,6 +7,28 @@ import (
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
)
|
||||
|
||||
// DrawReceiptInfo 抽奖验证凭据信息
|
||||
type DrawReceiptInfo struct {
|
||||
DrawLogID int64 `json:"draw_log_id"`
|
||||
RewardID int64 `json:"reward_id,omitempty"`
|
||||
DrawIndex int `json:"draw_index"`
|
||||
AlgoVersion string `json:"algo_version"`
|
||||
RoundID int64 `json:"round_id"`
|
||||
DrawID int64 `json:"draw_id"`
|
||||
ClientID int64 `json:"client_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
ServerSeedHash string `json:"server_seed_hash"`
|
||||
ServerSubSeed string `json:"server_sub_seed"`
|
||||
ClientSeed string `json:"client_seed"`
|
||||
Nonce int64 `json:"nonce"`
|
||||
ItemsRoot string `json:"items_root"`
|
||||
WeightsTotal int64 `json:"weights_total"`
|
||||
SelectedIndex int32 `json:"selected_index"`
|
||||
RandProof string `json:"rand_proof"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ItemsSnapshot string `json:"items_snapshot,omitempty"`
|
||||
}
|
||||
|
||||
// OrderWithItems 包含订单项的订单信息
|
||||
type OrderWithItems struct {
|
||||
*model.Orders
|
||||
@ -16,6 +38,7 @@ type OrderWithItems struct {
|
||||
IsDraw bool `json:"is_draw"`
|
||||
IsWinner bool `json:"is_winner"`
|
||||
RewardLevel int32 `json:"reward_level"`
|
||||
DrawReceipts []*DrawReceiptInfo `json:"draw_receipts"`
|
||||
}
|
||||
|
||||
func (s *service) ListOrders(ctx context.Context, userID int64, page, pageSize int) (items []*model.Orders, total int64, err error) {
|
||||
@ -97,9 +120,11 @@ func (s *service) GetOrderWithItems(ctx context.Context, userID int64, orderID i
|
||||
}
|
||||
|
||||
// 补充开奖信息
|
||||
log, _ := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.OrderID.Eq(order.ID)).First()
|
||||
if log != nil {
|
||||
logs, _ := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.OrderID.Eq(order.ID)).Find()
|
||||
if len(logs) > 0 {
|
||||
res.IsDraw = true
|
||||
// 取第一条记录的信息
|
||||
log := logs[0]
|
||||
res.IsWinner = log.IsWinner == 1
|
||||
res.RewardLevel = log.Level
|
||||
issue, _ := s.readDB.ActivityIssues.WithContext(ctx).ReadDB().Where(s.readDB.ActivityIssues.ID.Eq(log.IssueID)).First()
|
||||
@ -110,6 +135,49 @@ func (s *service) GetOrderWithItems(ctx context.Context, userID int64, orderID i
|
||||
res.ActivityName = act.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 查询抽奖凭据
|
||||
drawLogIDs := make([]int64, len(logs))
|
||||
drawLogIDToIndex := make(map[int64]int)
|
||||
for i, lg := range logs {
|
||||
drawLogIDs[i] = lg.ID
|
||||
drawLogIDToIndex[lg.ID] = i + 1
|
||||
}
|
||||
receipts, _ := s.readDB.ActivityDrawReceipts.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawReceipts.DrawLogID.In(drawLogIDs...)).Find()
|
||||
if len(receipts) > 0 {
|
||||
res.DrawReceipts = make([]*DrawReceiptInfo, 0, len(receipts))
|
||||
for _, r := range receipts {
|
||||
drawIndex := drawLogIDToIndex[r.DrawLogID]
|
||||
// 找到对应的 reward_id
|
||||
var rewardID int64
|
||||
for _, lg := range logs {
|
||||
if lg.ID == r.DrawLogID {
|
||||
rewardID = lg.RewardID
|
||||
break
|
||||
}
|
||||
}
|
||||
res.DrawReceipts = append(res.DrawReceipts, &DrawReceiptInfo{
|
||||
DrawLogID: r.DrawLogID,
|
||||
RewardID: rewardID,
|
||||
DrawIndex: drawIndex,
|
||||
AlgoVersion: r.AlgoVersion,
|
||||
RoundID: r.RoundID,
|
||||
DrawID: r.DrawID,
|
||||
ClientID: r.ClientID,
|
||||
Timestamp: r.Timestamp,
|
||||
ServerSeedHash: r.ServerSeedHash,
|
||||
ServerSubSeed: r.ServerSubSeed,
|
||||
ClientSeed: r.ClientSeed,
|
||||
Nonce: r.Nonce,
|
||||
ItemsRoot: r.ItemsRoot,
|
||||
WeightsTotal: r.WeightsTotal,
|
||||
SelectedIndex: r.SelectedIndex,
|
||||
RandProof: r.RandProof,
|
||||
Signature: r.Signature,
|
||||
ItemsSnapshot: r.ItemsSnapshot,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
@ -196,16 +264,18 @@ func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status
|
||||
}
|
||||
|
||||
// 批量查询活动开奖信息
|
||||
drawLogsMap := make(map[int64]*model.ActivityDrawLogs)
|
||||
drawLogsListMap := make(map[int64][]*model.ActivityDrawLogs) // orderID -> logs
|
||||
activityMap := make(map[int64]*model.Activities)
|
||||
issueMap := make(map[int64]*model.ActivityIssues)
|
||||
var allDrawLogIDs []int64
|
||||
|
||||
if len(orderIDs) > 0 {
|
||||
logs, _ := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.OrderID.In(orderIDs...)).Find()
|
||||
var issueIDs []int64
|
||||
for _, log := range logs {
|
||||
drawLogsMap[log.OrderID] = log
|
||||
drawLogsListMap[log.OrderID] = append(drawLogsListMap[log.OrderID], log)
|
||||
issueIDs = append(issueIDs, log.IssueID)
|
||||
allDrawLogIDs = append(allDrawLogIDs, log.ID)
|
||||
}
|
||||
|
||||
if len(issueIDs) > 0 {
|
||||
@ -225,6 +295,15 @@ func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询抽奖凭据
|
||||
receiptsMap := make(map[int64]*model.ActivityDrawReceipts) // drawLogID -> receipt
|
||||
if len(allDrawLogIDs) > 0 {
|
||||
receipts, _ := s.readDB.ActivityDrawReceipts.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawReceipts.DrawLogID.In(allDrawLogIDs...)).Find()
|
||||
for _, r := range receipts {
|
||||
receiptsMap[r.DrawLogID] = r
|
||||
}
|
||||
}
|
||||
|
||||
// 构建返回结果
|
||||
items = make([]*OrderWithItems, len(orders))
|
||||
for i, order := range orders {
|
||||
@ -233,8 +312,10 @@ func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status
|
||||
Items: itemsMap[order.ID],
|
||||
}
|
||||
|
||||
if log, ok := drawLogsMap[order.ID]; ok {
|
||||
if logs, ok := drawLogsListMap[order.ID]; ok && len(logs) > 0 {
|
||||
items[i].IsDraw = true
|
||||
// 取第一条记录的基本信息
|
||||
log := logs[0]
|
||||
items[i].IsWinner = log.IsWinner == 1
|
||||
items[i].RewardLevel = log.Level
|
||||
if issue, ok := issueMap[log.IssueID]; ok {
|
||||
@ -243,6 +324,32 @@ func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status
|
||||
items[i].ActivityName = act.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 填充抽奖凭据
|
||||
for idx, lg := range logs {
|
||||
if r, ok := receiptsMap[lg.ID]; ok {
|
||||
items[i].DrawReceipts = append(items[i].DrawReceipts, &DrawReceiptInfo{
|
||||
DrawLogID: r.DrawLogID,
|
||||
RewardID: lg.RewardID,
|
||||
DrawIndex: idx + 1,
|
||||
AlgoVersion: r.AlgoVersion,
|
||||
RoundID: r.RoundID,
|
||||
DrawID: r.DrawID,
|
||||
ClientID: r.ClientID,
|
||||
Timestamp: r.Timestamp,
|
||||
ServerSeedHash: r.ServerSeedHash,
|
||||
ServerSubSeed: r.ServerSubSeed,
|
||||
ClientSeed: r.ClientSeed,
|
||||
Nonce: r.Nonce,
|
||||
ItemsRoot: r.ItemsRoot,
|
||||
WeightsTotal: r.WeightsTotal,
|
||||
SelectedIndex: r.SelectedIndex,
|
||||
RandProof: r.RandProof,
|
||||
Signature: r.Signature,
|
||||
ItemsSnapshot: r.ItemsSnapshot,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -91,8 +91,8 @@ func (s *service) GrantReward(ctx context.Context, userID int64, req GrantReward
|
||||
PointsAmount: 0,
|
||||
ActualAmount: 0,
|
||||
IsConsumed: 0,
|
||||
PaidAt: now, // 设置支付时间为当前时间
|
||||
CancelledAt: minValidTime, // 设置取消时间为最小有效时间,避免MySQL错误
|
||||
PaidAt: &now, // 设置支付时间为当前时间
|
||||
CancelledAt: &minValidTime, // 设置取消时间为最小有效时间,避免MySQL错误
|
||||
Remark: req.Remark,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
|
||||
@ -18,3 +18,4 @@
|
||||
{"level":"fatal","time":"2025-12-08 21:00:04","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
|
||||
{"level":"fatal","time":"2025-12-11 00:56:11","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
|
||||
{"level":"fatal","time":"2025-12-11 15:05:14","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":"2025-12-21 14:34:33","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
|
||||
|
||||
124
scripts/matching_simulation.py
Normal file
124
scripts/matching_simulation.py
Normal file
@ -0,0 +1,124 @@
|
||||
import random
|
||||
import collections
|
||||
|
||||
# Configuration: 11 types, 9 cards each. Total 99 cards.
|
||||
CARD_TYPES = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']
|
||||
CARDS_PER_TYPE = 9
|
||||
BOARD_SIZE = 9
|
||||
|
||||
def create_deck():
|
||||
"""Create a full deck of 99 cards."""
|
||||
deck = []
|
||||
for t in CARD_TYPES:
|
||||
for _ in range(CARDS_PER_TYPE):
|
||||
deck.append(t)
|
||||
random.shuffle(deck)
|
||||
return deck
|
||||
|
||||
def play_one_game():
|
||||
"""Simulate one full game session."""
|
||||
# 1. Initialize
|
||||
full_deck = create_deck()
|
||||
board = full_deck[:BOARD_SIZE]
|
||||
deck = full_deck[BOARD_SIZE:]
|
||||
|
||||
pairs_found = 0
|
||||
|
||||
# Loop until game over
|
||||
while True:
|
||||
# 2. Check for matches on board
|
||||
# Strategy: Always eliminate the first pair found.
|
||||
# (In real game, user choice might matter, but for max pairs,
|
||||
# elimination order rarely changes the total count if we have infinite reshuffles)
|
||||
|
||||
counts = collections.Counter(board)
|
||||
match_type = None
|
||||
for t, count in counts.items():
|
||||
if count >= 2:
|
||||
match_type = t
|
||||
break
|
||||
|
||||
if match_type:
|
||||
# ELIMINATE
|
||||
pairs_found += 1
|
||||
|
||||
# Remove 2 instances of match_type
|
||||
removed_count = 0
|
||||
new_board = []
|
||||
for card in board:
|
||||
if card == match_type and removed_count < 2:
|
||||
removed_count += 1
|
||||
continue # Skip (remove)
|
||||
new_board.append(card)
|
||||
board = new_board
|
||||
|
||||
# Fill from deck
|
||||
while len(board) < BOARD_SIZE and len(deck) > 0:
|
||||
board.append(deck.pop(0))
|
||||
|
||||
else:
|
||||
# DEADLOCK (No matches on board)
|
||||
if len(deck) == 0:
|
||||
# Game Over
|
||||
break
|
||||
else:
|
||||
# RESHUFFLE
|
||||
# Collect all remaining cards (board + deck)
|
||||
remaining = board + deck
|
||||
random.shuffle(remaining)
|
||||
|
||||
# Refill board and deck
|
||||
board = remaining[:BOARD_SIZE]
|
||||
deck = remaining[BOARD_SIZE:]
|
||||
|
||||
# Check if new board has matches.
|
||||
# Theoretically possibility of repeated deadlocks, but loop continues.
|
||||
# To prevent infinite loop in case of bad logic (e.g. 1 card left), check solvability.
|
||||
# But here we have types. If we have pairs left in TOTAL, reshuffle will eventually bring them to board.
|
||||
|
||||
# Optimization: If total remaining of every type is < 2, we can never match again.
|
||||
total_counts = collections.Counter(remaining)
|
||||
can_match = False
|
||||
for t, c in total_counts.items():
|
||||
if c >= 2:
|
||||
can_match = True
|
||||
break
|
||||
|
||||
if not can_match:
|
||||
# Impossible to match anything more
|
||||
break
|
||||
|
||||
return pairs_found
|
||||
|
||||
def run_simulation(times=10000):
|
||||
print(f"Simulating {times} games...")
|
||||
print(f"Config: {len(CARD_TYPES)} types x {CARDS_PER_TYPE} cards = {len(CARD_TYPES)*CARDS_PER_TYPE} total.")
|
||||
|
||||
results = []
|
||||
for i in range(times):
|
||||
score = play_one_game()
|
||||
results.append(score)
|
||||
if (i+1) % 1000 == 0:
|
||||
print(f"Progress: {i+1}/{times}")
|
||||
|
||||
max_score = max(results)
|
||||
min_score = min(results)
|
||||
avg_score = sum(results) / len(results)
|
||||
|
||||
print("\n=== Results ===")
|
||||
print(f"Max Pairs: {max_score}")
|
||||
print(f"Min Pairs: {min_score}")
|
||||
print(f"Avg Pairs: {avg_score:.2f}")
|
||||
|
||||
# Theoretical Max
|
||||
# floor(9/2) * 11 = 4 * 11 = 44
|
||||
print(f"Theoretical Max: {int(CARDS_PER_TYPE/2) * len(CARD_TYPES)}")
|
||||
|
||||
# Distribution
|
||||
dist = collections.Counter(results)
|
||||
print("\nDistribution:")
|
||||
for score in sorted(dist.keys()):
|
||||
print(f"{score} pairs: {dist[score]} times")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_simulation()
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user