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
|
export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||||
docker build -t zfc931912343/bindbox-game:v1.6 .
|
docker build -t zfc931912343/bindbox-game:v1.8 .
|
||||||
docker push zfc931912343/bindbox-game:v1.6
|
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:"write" toml:"write"`
|
||||||
} `mapstructure:"mysql" toml:"mysql"`
|
} `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 {
|
JWT struct {
|
||||||
AdminSecret string `mapstructure:"admin_secret" toml:"admin_secret"`
|
AdminSecret string `mapstructure:"admin_secret" toml:"admin_secret"`
|
||||||
PatientSecret string `mapstructure:"patient_secret" toml:"patient_secret"`
|
PatientSecret string `mapstructure:"patient_secret" toml:"patient_secret"`
|
||||||
|
|||||||
@ -11,9 +11,9 @@
|
|||||||
name = "bindbox_game"
|
name = "bindbox_game"
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
addr = "127.0.0.1:6379"
|
addr = "118.25.13.43:8379"
|
||||||
pass = ""
|
pass = "xbm#2023by1024"
|
||||||
db = 0
|
db = 5
|
||||||
|
|
||||||
[random]
|
[random]
|
||||||
commit_master_key = "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
|
commit_master_key = "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
|
||||||
|
|||||||
@ -7,6 +7,12 @@ name = 'bindbox_game'
|
|||||||
pass = 'api2api..'
|
pass = 'api2api..'
|
||||||
user = 'root'
|
user = 'root'
|
||||||
|
|
||||||
|
[redis]
|
||||||
|
addr = "118.25.13.43:8379"
|
||||||
|
pass = "xbm#2023by1024"
|
||||||
|
db = 5
|
||||||
|
|
||||||
|
|
||||||
[mysql.write]
|
[mysql.write]
|
||||||
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
|
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
|
||||||
name = 'bindbox_game'
|
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": {
|
"/api/app/activities/{activity_id}/issues/{issue_id}/rewards": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "获取指定期的奖励配置列表",
|
"description": "获取指定期的奖励配置列表",
|
||||||
@ -3711,14 +3756,9 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/app/matching/play": {
|
"/api/app/matching/card_types": {
|
||||||
"post": {
|
"get": {
|
||||||
"security": [
|
"description": "获取所有启用的卡牌类型配置,用于App端预览或动画展示",
|
||||||
{
|
|
||||||
"LoginVerifyToken": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "执行一轮配对,返回配对结果和新抽取的牌",
|
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -3728,23 +3768,15 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"APP端.活动"
|
"APP端.活动"
|
||||||
],
|
],
|
||||||
"summary": "执行一轮对对碰游戏",
|
"summary": "列出对对碰卡牌类型",
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "请求参数",
|
|
||||||
"name": "RequestBody",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/app.matchingGamePlayRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/app.matchingGamePlayResponse"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/app.CardTypeConfig"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@ -3756,14 +3788,14 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/app/matching/start": {
|
"/api/app/matching/check": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"LoginVerifyToken": []
|
"LoginVerifyToken": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "创建新的对对碰游戏会话,返回初始手牌和第一轮结果",
|
"description": "前端游戏结束后上报结果,服务器发放奖励",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -3773,7 +3805,7 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"APP端.活动"
|
"APP端.活动"
|
||||||
],
|
],
|
||||||
"summary": "开始对对碰游戏",
|
"summary": "游戏结束结算校验",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "请求参数",
|
"description": "请求参数",
|
||||||
@ -3781,7 +3813,7 @@ const docTemplate = `{
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/app.matchingGameStartRequest"
|
"$ref": "#/definitions/app.matchingGameCheckRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -3789,7 +3821,52 @@ const docTemplate = `{
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"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": {
|
"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": {
|
"/api/app/pay/wechat/jsapi/preorder": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -7254,6 +7425,9 @@ const docTemplate = `{
|
|||||||
"level": {
|
"level": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"min_score": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -7366,6 +7540,9 @@ const docTemplate = `{
|
|||||||
"level": {
|
"level": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"min_score": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -7547,11 +7724,11 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.MatchingCard": {
|
"app.CardTypeConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"code": {
|
||||||
"type": "integer"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"image_url": {
|
"image_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -7559,56 +7736,32 @@ const docTemplate = `{
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"quantity": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app.MatchingCard": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.MatchingPair": {
|
"app.MatchingRewardInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"card_type": {
|
"level": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"count": {
|
"reward_id": {
|
||||||
"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": {
|
|
||||||
"type": "integer"
|
"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": {
|
"app.couponDetail": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"app.drawLogItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -8309,6 +8504,17 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app.listDrawLogsByLevelResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"groups": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/app.drawLogGroup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"app.listDrawLogsResponse": {
|
"app.listDrawLogsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -8520,58 +8726,75 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.matchingGamePlayRequest": {
|
"app.matchingGameCheckRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"game_id"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"game_id": {
|
"game_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"total_pairs": {
|
||||||
|
"description": "客户端上报的消除总对数",
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.matchingGamePlayResponse": {
|
"app.matchingGameCheckResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"final_state": {
|
"finished": {
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {}
|
|
||||||
},
|
|
||||||
"game_over": {
|
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"round": {
|
"game_id": {
|
||||||
"$ref": "#/definitions/app.MatchingRoundResult"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reward": {
|
||||||
|
"$ref": "#/definitions/app.MatchingRewardInfo"
|
||||||
},
|
},
|
||||||
"total_pairs": {
|
"total_pairs": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.matchingGameStartRequest": {
|
"app.matchingGamePreOrderRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"coupon_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"issue_id": {
|
"issue_id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"item_card_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.matchingGameStartResponse": {
|
"app.matchingGamePreOrderResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"deck_count": {
|
"all_cards": {
|
||||||
"type": "integer"
|
"description": "全量99张卡牌(乱序)",
|
||||||
},
|
|
||||||
"first_round": {
|
|
||||||
"$ref": "#/definitions/app.MatchingRoundResult"
|
|
||||||
},
|
|
||||||
"game_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"hand": {
|
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/app.MatchingCard"
|
"$ref": "#/definitions/app.MatchingCard"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"game_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order_no": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pay_status": {
|
||||||
|
"description": "1=Pending, 2=Paid",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"server_seed_hash": {
|
"server_seed_hash": {
|
||||||
"type": "string"
|
"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": {
|
"user.InventoryWithProduct": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -9631,6 +9913,12 @@ const docTemplate = `{
|
|||||||
"description": "优惠券抵扣金额(分)",
|
"description": "优惠券抵扣金额(分)",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"draw_receipts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/user.DrawReceiptInfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"description": "主键ID",
|
"description": "主键ID",
|
||||||
"type": "integer"
|
"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": {
|
"/api/app/activities/{activity_id}/issues/{issue_id}/rewards": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "获取指定期的奖励配置列表",
|
"description": "获取指定期的奖励配置列表",
|
||||||
@ -3703,14 +3748,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/app/matching/play": {
|
"/api/app/matching/card_types": {
|
||||||
"post": {
|
"get": {
|
||||||
"security": [
|
"description": "获取所有启用的卡牌类型配置,用于App端预览或动画展示",
|
||||||
{
|
|
||||||
"LoginVerifyToken": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "执行一轮配对,返回配对结果和新抽取的牌",
|
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -3720,23 +3760,15 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"APP端.活动"
|
"APP端.活动"
|
||||||
],
|
],
|
||||||
"summary": "执行一轮对对碰游戏",
|
"summary": "列出对对碰卡牌类型",
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "请求参数",
|
|
||||||
"name": "RequestBody",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/app.matchingGamePlayRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/app.matchingGamePlayResponse"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/app.CardTypeConfig"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@ -3748,14 +3780,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/app/matching/start": {
|
"/api/app/matching/check": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"LoginVerifyToken": []
|
"LoginVerifyToken": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "创建新的对对碰游戏会话,返回初始手牌和第一轮结果",
|
"description": "前端游戏结束后上报结果,服务器发放奖励",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -3765,7 +3797,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"APP端.活动"
|
"APP端.活动"
|
||||||
],
|
],
|
||||||
"summary": "开始对对碰游戏",
|
"summary": "游戏结束结算校验",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "请求参数",
|
"description": "请求参数",
|
||||||
@ -3773,7 +3805,7 @@
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/app.matchingGameStartRequest"
|
"$ref": "#/definitions/app.matchingGameCheckRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -3781,7 +3813,52 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"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": {
|
"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": {
|
"/api/app/pay/wechat/jsapi/preorder": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -7246,6 +7417,9 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"min_score": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -7358,6 +7532,9 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"min_score": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -7539,11 +7716,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.MatchingCard": {
|
"app.CardTypeConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"code": {
|
||||||
"type": "integer"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"image_url": {
|
"image_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -7551,56 +7728,32 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"quantity": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app.MatchingCard": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.MatchingPair": {
|
"app.MatchingRewardInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"card_type": {
|
"level": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"count": {
|
"reward_id": {
|
||||||
"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": {
|
|
||||||
"type": "integer"
|
"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": {
|
"app.couponDetail": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"app.drawLogItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -8301,6 +8496,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app.listDrawLogsByLevelResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"groups": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/app.drawLogGroup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"app.listDrawLogsResponse": {
|
"app.listDrawLogsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -8512,58 +8718,75 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.matchingGamePlayRequest": {
|
"app.matchingGameCheckRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"game_id"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"game_id": {
|
"game_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"total_pairs": {
|
||||||
|
"description": "客户端上报的消除总对数",
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.matchingGamePlayResponse": {
|
"app.matchingGameCheckResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"final_state": {
|
"finished": {
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {}
|
|
||||||
},
|
|
||||||
"game_over": {
|
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"round": {
|
"game_id": {
|
||||||
"$ref": "#/definitions/app.MatchingRoundResult"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reward": {
|
||||||
|
"$ref": "#/definitions/app.MatchingRewardInfo"
|
||||||
},
|
},
|
||||||
"total_pairs": {
|
"total_pairs": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.matchingGameStartRequest": {
|
"app.matchingGamePreOrderRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"coupon_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"issue_id": {
|
"issue_id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"item_card_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app.matchingGameStartResponse": {
|
"app.matchingGamePreOrderResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"deck_count": {
|
"all_cards": {
|
||||||
"type": "integer"
|
"description": "全量99张卡牌(乱序)",
|
||||||
},
|
|
||||||
"first_round": {
|
|
||||||
"$ref": "#/definitions/app.MatchingRoundResult"
|
|
||||||
},
|
|
||||||
"game_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"hand": {
|
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/app.MatchingCard"
|
"$ref": "#/definitions/app.MatchingCard"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"game_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order_no": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pay_status": {
|
||||||
|
"description": "1=Pending, 2=Paid",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"server_seed_hash": {
|
"server_seed_hash": {
|
||||||
"type": "string"
|
"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": {
|
"user.InventoryWithProduct": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -9623,6 +9905,12 @@
|
|||||||
"description": "优惠券抵扣金额(分)",
|
"description": "优惠券抵扣金额(分)",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"draw_receipts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/user.DrawReceiptInfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"description": "主键ID",
|
"description": "主键ID",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
|||||||
@ -981,6 +981,8 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
level:
|
level:
|
||||||
type: integer
|
type: integer
|
||||||
|
min_score:
|
||||||
|
type: integer
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
original_qty:
|
original_qty:
|
||||||
@ -1049,6 +1051,8 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
level:
|
level:
|
||||||
type: integer
|
type: integer
|
||||||
|
min_score:
|
||||||
|
type: integer
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
original_qty:
|
original_qty:
|
||||||
@ -1174,47 +1178,31 @@ definitions:
|
|||||||
total:
|
total:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
app.MatchingCard:
|
app.CardTypeConfig:
|
||||||
properties:
|
properties:
|
||||||
id:
|
code:
|
||||||
type: integer
|
type: string
|
||||||
image_url:
|
image_url:
|
||||||
type: string
|
type: string
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
quantity:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
app.MatchingCard:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
app.MatchingPair:
|
app.MatchingRewardInfo:
|
||||||
properties:
|
properties:
|
||||||
card_type:
|
level:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
type: string
|
type: string
|
||||||
count:
|
reward_id:
|
||||||
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:
|
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
app.activityDetailResponse:
|
app.activityDetailResponse:
|
||||||
@ -1377,6 +1365,22 @@ definitions:
|
|||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
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:
|
app.couponDetail:
|
||||||
properties:
|
properties:
|
||||||
amount:
|
amount:
|
||||||
@ -1458,6 +1462,17 @@ definitions:
|
|||||||
description: 订单号
|
description: 订单号
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
app.drawLogGroup:
|
||||||
|
properties:
|
||||||
|
level:
|
||||||
|
type: integer
|
||||||
|
level_name:
|
||||||
|
type: string
|
||||||
|
list:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/app.drawLogItem'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
app.drawLogItem:
|
app.drawLogItem:
|
||||||
properties:
|
properties:
|
||||||
current_level:
|
current_level:
|
||||||
@ -1675,6 +1690,13 @@ definitions:
|
|||||||
total:
|
total:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
app.listDrawLogsByLevelResponse:
|
||||||
|
properties:
|
||||||
|
groups:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/app.drawLogGroup'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
app.listDrawLogsResponse:
|
app.listDrawLogsResponse:
|
||||||
properties:
|
properties:
|
||||||
list:
|
list:
|
||||||
@ -1812,40 +1834,52 @@ definitions:
|
|||||||
total:
|
total:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
app.matchingGamePlayRequest:
|
app.matchingGameCheckRequest:
|
||||||
properties:
|
properties:
|
||||||
game_id:
|
game_id:
|
||||||
type: string
|
type: string
|
||||||
|
total_pairs:
|
||||||
|
description: 客户端上报的消除总对数
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- game_id
|
||||||
type: object
|
type: object
|
||||||
app.matchingGamePlayResponse:
|
app.matchingGameCheckResponse:
|
||||||
properties:
|
properties:
|
||||||
final_state:
|
finished:
|
||||||
additionalProperties: {}
|
|
||||||
type: object
|
|
||||||
game_over:
|
|
||||||
type: boolean
|
type: boolean
|
||||||
round:
|
game_id:
|
||||||
$ref: '#/definitions/app.MatchingRoundResult'
|
type: string
|
||||||
|
reward:
|
||||||
|
$ref: '#/definitions/app.MatchingRewardInfo'
|
||||||
total_pairs:
|
total_pairs:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
app.matchingGameStartRequest:
|
app.matchingGamePreOrderRequest:
|
||||||
properties:
|
properties:
|
||||||
|
coupon_id:
|
||||||
|
type: integer
|
||||||
issue_id:
|
issue_id:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
item_card_id:
|
||||||
app.matchingGameStartResponse:
|
|
||||||
properties:
|
|
||||||
deck_count:
|
|
||||||
type: integer
|
type: integer
|
||||||
first_round:
|
position:
|
||||||
$ref: '#/definitions/app.MatchingRoundResult'
|
|
||||||
game_id:
|
|
||||||
type: string
|
type: string
|
||||||
hand:
|
type: object
|
||||||
|
app.matchingGamePreOrderResponse:
|
||||||
|
properties:
|
||||||
|
all_cards:
|
||||||
|
description: 全量99张卡牌(乱序)
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/app.MatchingCard'
|
$ref: '#/definitions/app.MatchingCard'
|
||||||
type: array
|
type: array
|
||||||
|
game_id:
|
||||||
|
type: string
|
||||||
|
order_no:
|
||||||
|
type: string
|
||||||
|
pay_status:
|
||||||
|
description: 1=Pending, 2=Paid
|
||||||
|
type: integer
|
||||||
server_seed_hash:
|
server_seed_hash:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
@ -2461,6 +2495,45 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
type: object
|
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:
|
user.InventoryWithProduct:
|
||||||
properties:
|
properties:
|
||||||
activity_id:
|
activity_id:
|
||||||
@ -2569,6 +2642,10 @@ definitions:
|
|||||||
discount_amount:
|
discount_amount:
|
||||||
description: 优惠券抵扣金额(分)
|
description: 优惠券抵扣金额(分)
|
||||||
type: integer
|
type: integer
|
||||||
|
draw_receipts:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/user.DrawReceiptInfo'
|
||||||
|
type: array
|
||||||
id:
|
id:
|
||||||
description: 主键ID
|
description: 主键ID
|
||||||
type: integer
|
type: integer
|
||||||
@ -4934,6 +5011,36 @@ paths:
|
|||||||
summary: 抽奖记录列表
|
summary: 抽奖记录列表
|
||||||
tags:
|
tags:
|
||||||
- APP端.活动
|
- 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:
|
/api/app/activities/{activity_id}/issues/{issue_id}/rewards:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@ -5063,60 +5170,81 @@ paths:
|
|||||||
summary: 抽奖订单结果查询
|
summary: 抽奖订单结果查询
|
||||||
tags:
|
tags:
|
||||||
- APP端.抽奖
|
- APP端.抽奖
|
||||||
/api/app/matching/play:
|
/api/app/matching/card_types:
|
||||||
post:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: 执行一轮配对,返回配对结果和新抽取的牌
|
description: 获取所有启用的卡牌类型配置,用于App端预览或动画展示
|
||||||
parameters:
|
|
||||||
- description: 请求参数
|
|
||||||
in: body
|
|
||||||
name: RequestBody
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/app.matchingGamePlayRequest'
|
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/app.matchingGamePlayResponse'
|
items:
|
||||||
|
$ref: '#/definitions/app.CardTypeConfig'
|
||||||
|
type: array
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/code.Failure'
|
$ref: '#/definitions/code.Failure'
|
||||||
security:
|
summary: 列出对对碰卡牌类型
|
||||||
- LoginVerifyToken: []
|
|
||||||
summary: 执行一轮对对碰游戏
|
|
||||||
tags:
|
tags:
|
||||||
- APP端.活动
|
- APP端.活动
|
||||||
/api/app/matching/start:
|
/api/app/matching/check:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: 创建新的对对碰游戏会话,返回初始手牌和第一轮结果
|
description: 前端游戏结束后上报结果,服务器发放奖励
|
||||||
parameters:
|
parameters:
|
||||||
- description: 请求参数
|
- description: 请求参数
|
||||||
in: body
|
in: body
|
||||||
name: RequestBody
|
name: RequestBody
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/app.matchingGameStartRequest'
|
$ref: '#/definitions/app.matchingGameCheckRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/app.matchingGameStartResponse'
|
$ref: '#/definitions/app.matchingGameCheckResponse'
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/code.Failure'
|
$ref: '#/definitions/code.Failure'
|
||||||
security:
|
security:
|
||||||
- LoginVerifyToken: []
|
- 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:
|
tags:
|
||||||
- APP端.活动
|
- APP端.活动
|
||||||
/api/app/matching/state:
|
/api/app/matching/state:
|
||||||
@ -5147,6 +5275,65 @@ paths:
|
|||||||
summary: 获取对对碰游戏状态
|
summary: 获取对对碰游戏状态
|
||||||
tags:
|
tags:
|
||||||
- APP端.活动
|
- 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:
|
/api/app/pay/wechat/jsapi/preorder:
|
||||||
post:
|
post:
|
||||||
consumes:
|
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/issue9/identicon/v2 v2.1.2
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.17.0
|
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/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644
|
||||||
github.com/spf13/cast v1.5.1
|
github.com/spf13/cast v1.5.1
|
||||||
github.com/spf13/viper v1.17.0
|
github.com/spf13/viper v1.17.0
|
||||||
@ -48,10 +49,11 @@ require (
|
|||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
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/clbanning/mxj v1.8.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // 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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
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 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
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 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
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/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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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/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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
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/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 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
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.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.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
|||||||
@ -1,32 +1,39 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bindbox-game/internal/pkg/logger"
|
"bindbox-game/internal/pkg/logger"
|
||||||
"bindbox-game/internal/repository/mysql"
|
"bindbox-game/internal/repository/mysql"
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
activitysvc "bindbox-game/internal/service/activity"
|
activitysvc "bindbox-game/internal/service/activity"
|
||||||
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
||||||
usersvc "bindbox-game/internal/service/user"
|
titlesvc "bindbox-game/internal/service/title"
|
||||||
|
usersvc "bindbox-game/internal/service/user"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
logger logger.CustomLogger
|
logger logger.CustomLogger
|
||||||
writeDB *dao.Query
|
writeDB *dao.Query
|
||||||
readDB *dao.Query
|
readDB *dao.Query
|
||||||
activity activitysvc.Service
|
activity activitysvc.Service
|
||||||
syscfg syscfgsvc.Service
|
syscfg syscfgsvc.Service
|
||||||
repo mysql.Repo
|
title titlesvc.Service
|
||||||
user usersvc.Service
|
repo mysql.Repo
|
||||||
|
user usersvc.Service
|
||||||
|
redis *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
|
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler {
|
||||||
return &handler{
|
return &handler{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
writeDB: dao.Use(db.GetDbW()),
|
writeDB: dao.Use(db.GetDbW()),
|
||||||
readDB: dao.Use(db.GetDbR()),
|
readDB: dao.Use(db.GetDbR()),
|
||||||
activity: activitysvc.New(logger, db),
|
activity: activitysvc.New(logger, db),
|
||||||
syscfg: syscfgsvc.New(logger, db),
|
syscfg: syscfgsvc.New(logger, db),
|
||||||
repo: db,
|
title: titlesvc.New(logger, db),
|
||||||
user: usersvc.New(logger, db),
|
repo: db,
|
||||||
}
|
user: usersvc.New(logger, db),
|
||||||
|
redis: rdb,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package app
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
@ -82,3 +83,115 @@ func (h *handler) ListDrawLogs() core.HandlerFunc {
|
|||||||
ctx.Payload(res)
|
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/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
titlesvc "bindbox-game/internal/service/title"
|
||||||
)
|
)
|
||||||
|
|
||||||
type joinLotteryRequest struct {
|
type joinLotteryRequest struct {
|
||||||
@ -69,6 +72,11 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170010, "本活动不支持道具卡"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170010, "本活动不支持道具卡"))
|
||||||
return
|
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"
|
cfgMode := "scheduled"
|
||||||
if activity.DrawMode != "" {
|
if activity.DrawMode != "" {
|
||||||
cfgMode = 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)
|
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
|
||||||
fmt.Printf("[抽奖下单] 优惠后 实付(分)=%d 累计优惠(分)=%d 备注=%s\n", order.ActualAmount, order.DiscountAmount, order.Remark)
|
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 {
|
if req.UsePoints != nil && *req.UsePoints > 0 {
|
||||||
bal, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
bal, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
||||||
usePts := *req.UsePoints
|
usePts := *req.UsePoints
|
||||||
@ -300,28 +363,44 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
if e != nil {
|
if e != nil {
|
||||||
break
|
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 {
|
} 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
|
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)
|
fmt.Printf("[道具卡-JoinLottery] 开始检查 活动允许道具卡=%t 请求道具卡ID=%v\n", activity.AllowItemCards, req.ItemCardID)
|
||||||
if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
|
if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
|
||||||
@ -367,7 +446,17 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
} else {
|
} 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.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 {
|
} else {
|
||||||
fmt.Printf("[道具卡-JoinLottery] 概率提升未触发\n")
|
fmt.Printf("[道具卡-JoinLottery] 概率提升未触发\n")
|
||||||
}
|
}
|
||||||
@ -515,12 +604,15 @@ func (h *handler) GetLotteryResult() core.HandlerFunc {
|
|||||||
slot := parseSlotFromRemark(ord.Remark)
|
slot := parseSlotFromRemark(ord.Remark)
|
||||||
if slot >= 0 {
|
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 {
|
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 {
|
if e2 == nil && rid > 0 {
|
||||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||||
if rw != nil {
|
if rw != nil {
|
||||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
_ = 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}
|
rsp.Result = map[string]any{"reward_id": rid, "reward_name": rw.Name}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -528,7 +620,8 @@ func (h *handler) GetLotteryResult() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sel := strat.NewDefault(h.readDB, h.writeDB)
|
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 {
|
if e2 == nil && rid > 0 {
|
||||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||||
_ = sel.GrantReward(ctx.RequestContext(), userID, rid)
|
_ = sel.GrantReward(ctx.RequestContext(), userID, rid)
|
||||||
@ -539,6 +632,7 @@ func (h *handler) GetLotteryResult() core.HandlerFunc {
|
|||||||
return 1
|
return 1
|
||||||
}(), CurrentLevel: 1}
|
}(), CurrentLevel: 1}
|
||||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||||
|
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, issueID, userID, proof)
|
||||||
icID := parseItemCardIDFromRemark(ord.Remark)
|
icID := parseItemCardIDFromRemark(ord.Remark)
|
||||||
fmt.Printf("[道具卡-GetLotteryResult] 从订单备注解析道具卡ID icID=%d 订单备注=%s\n", icID, ord.Remark)
|
fmt.Printf("[道具卡-GetLotteryResult] 从订单备注解析道具卡ID icID=%d 订单备注=%s\n", icID, ord.Remark)
|
||||||
if icID > 0 {
|
if icID > 0 {
|
||||||
@ -591,7 +685,17 @@ func (h *handler) GetLotteryResult() core.HandlerFunc {
|
|||||||
rid2 := better.ID
|
rid2 := better.ID
|
||||||
fmt.Printf("[道具卡-GetLotteryResult] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
|
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.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 {
|
} else {
|
||||||
fmt.Printf("[道具卡-GetLotteryResult] 概率提升未触发\n")
|
fmt.Printf("[道具卡-GetLotteryResult] 概率提升未触发\n")
|
||||||
}
|
}
|
||||||
@ -1065,115 +1169,132 @@ func (h *handler) processInstantDraw(ctx core.Context, userID int64, activity *m
|
|||||||
rid := int64(0)
|
rid := int64(0)
|
||||||
var e2 error
|
var e2 error
|
||||||
if activity.PlayType == "ichiban" {
|
if activity.PlayType == "ichiban" {
|
||||||
slot := func() int64 {
|
// ... (inside loop)
|
||||||
if len(slotsIdx) > 0 && len(slotsIdx) == len(rem) {
|
var proof map[string]any
|
||||||
for cur < len(rem) && rem[cur] == 0 {
|
if activity.PlayType == "ichiban" {
|
||||||
cur++
|
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 parseSlotFromRemark(ord.Remark)
|
||||||
return -1
|
}()
|
||||||
|
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]--
|
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
|
||||||
return slotsIdx[cur] - 1
|
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 {
|
} else {
|
||||||
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
rid, proof, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
||||||
}
|
}
|
||||||
} else {
|
if e2 != nil || rid <= 0 {
|
||||||
rid, _, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
|
break
|
||||||
}
|
}
|
||||||
if e2 != nil || rid <= 0 {
|
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||||
break
|
if rw == nil {
|
||||||
}
|
break
|
||||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
}
|
||||||
if rw == nil {
|
if activity.PlayType == "ichiban" {
|
||||||
break
|
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||||
}
|
} else {
|
||||||
if activity.PlayType == "ichiban" {
|
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name})
|
||||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
}
|
||||||
} else {
|
log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
||||||
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name})
|
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
||||||
}
|
_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, log.ID, issueID, userID, proof)
|
||||||
log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
|
fmt.Printf("[道具卡-processInstantDraw] 开始检查 活动允许道具卡=%t itemCardID=%v\n", activity.AllowItemCards, itemCardID)
|
||||||
_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
|
if activity.AllowItemCards && itemCardID != nil && *itemCardID > 0 {
|
||||||
fmt.Printf("[道具卡-processInstantDraw] 开始检查 活动允许道具卡=%t itemCardID=%v\n", activity.AllowItemCards, itemCardID)
|
fmt.Printf("[道具卡-processInstantDraw] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
|
||||||
if activity.AllowItemCards && itemCardID != nil && *itemCardID > 0 {
|
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()
|
||||||
fmt.Printf("[道具卡-processInstantDraw] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
|
if uic != nil {
|
||||||
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()
|
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))
|
||||||
if uic != nil {
|
ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
|
||||||
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))
|
now := time.Now()
|
||||||
ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
|
if ic != nil {
|
||||||
now := time.Now()
|
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)
|
||||||
if ic != nil {
|
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))
|
||||||
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)
|
if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
|
||||||
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))
|
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == activityID) || (ic.ScopeType == 4 && ic.IssueID == issueID)
|
||||||
if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
|
fmt.Printf("[道具卡-processInstantDraw] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, activityID, issueID, scopeOK)
|
||||||
scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == activityID) || (ic.ScopeType == 4 && ic.IssueID == issueID)
|
if scopeOK {
|
||||||
fmt.Printf("[道具卡-processInstantDraw] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, activityID, issueID, scopeOK)
|
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||||||
if scopeOK {
|
fmt.Printf("[道具卡-processInstantDraw] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s PlayType=%s\n", ic.RewardMultiplierX1000, rid, rw.Name, activity.PlayType)
|
||||||
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
if activity.PlayType == "ichiban" {
|
||||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s PlayType=%s\n", ic.RewardMultiplierX1000, rid, rw.Name, activity.PlayType)
|
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
||||||
if activity.PlayType == "ichiban" {
|
} else {
|
||||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
|
||||||
} 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] ✅ 双倍奖励发放完成\n")
|
fmt.Printf("[道具卡-processInstantDraw] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
|
||||||
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
|
uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
|
||||||
fmt.Printf("[道具卡-processInstantDraw] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
|
var better *model.ActivityRewardSettings
|
||||||
uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
|
for _, r := range uprw {
|
||||||
var better *model.ActivityRewardSettings
|
if r.Level < rw.Level && r.Quantity != 0 {
|
||||||
for _, r := range uprw {
|
if better == nil || r.Level < better.Level {
|
||||||
if r.Level < rw.Level && r.Quantity != 0 {
|
better = r
|
||||||
if better == nil || r.Level < better.Level {
|
}
|
||||||
better = r
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if better != nil && rand.Int31n(1000) < ic.BoostRateX1000 {
|
||||||
if better != nil && rand.Int31n(1000) < ic.BoostRateX1000 {
|
rid2 := better.ID
|
||||||
rid2 := better.ID
|
fmt.Printf("[道具卡-processInstantDraw] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
|
||||||
fmt.Printf("[道具卡-processInstantDraw] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
|
if activity.PlayType == "ichiban" {
|
||||||
if activity.PlayType == "ichiban" {
|
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid2)
|
||||||
_ = 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 {
|
} 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 {
|
} 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 {
|
} 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 {
|
} else {
|
||||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 范围检查失败\n")
|
fmt.Printf("[道具卡-processInstantDraw] ❌ 时间检查失败\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 时间检查失败\n")
|
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
|
fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if cnt > 0 {
|
||||||
st = "slot_unavailable"
|
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 {
|
} 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 {
|
if e2 == nil && rid > 0 {
|
||||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||||
if rw != nil {
|
if rw != nil {
|
||||||
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
|
_ = 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++
|
completed++
|
||||||
items = append(items, orderResultItem{RewardID: rid, RewardName: rw.Name, Level: rw.Level, DrawIndex: int32(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 {
|
} else {
|
||||||
sel := strat.NewDefault(h.readDB, h.writeDB)
|
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 {
|
if e2 == nil && rid > 0 {
|
||||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||||
if rw != nil {
|
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.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++
|
completed++
|
||||||
items = append(items, orderResultItem{RewardID: rid, RewardName: rw.Name, Level: rw.Level, DrawIndex: int32(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 == "" {
|
if remark == "" {
|
||||||
return 0
|
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++ {
|
for i := 0; i < len(remark); i++ {
|
||||||
if remark[i] == '|' {
|
if remark[i] == '|' {
|
||||||
seg := remark[p:i]
|
segs = append(segs, remark[last:i])
|
||||||
if len(seg) > 6 && seg[:6] == "issue:" {
|
last = i + 1
|
||||||
var n int64
|
}
|
||||||
for j := 6; j < len(seg); j++ {
|
}
|
||||||
c := seg[j]
|
if last < len(remark) {
|
||||||
if c < '0' || c > '9' {
|
segs = append(segs, remark[last:])
|
||||||
break
|
}
|
||||||
}
|
|
||||||
n = n*10 + int64(c-'0')
|
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
|
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)
|
s := strat.NewDefault(h.readDB, h.writeDB)
|
||||||
for _, o := range orders {
|
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 {
|
if e2 != nil || rid <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||||
_ = s.GrantReward(ctx.RequestContext(), o.UserID, rid)
|
_ = 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 {
|
if rw != nil {
|
||||||
return rw.Level
|
return rw.Level
|
||||||
}
|
}
|
||||||
return 1
|
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++
|
granted++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,18 +208,21 @@ func (h *handler) SettleIssue() core.HandlerFunc {
|
|||||||
// 即时或强制:统一开奖
|
// 即时或强制:统一开奖
|
||||||
s := strat.NewDefault(h.readDB, h.writeDB)
|
s := strat.NewDefault(h.readDB, h.writeDB)
|
||||||
for _, o := range orders {
|
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 {
|
if e2 != nil || rid <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
|
||||||
_ = s.GrantReward(ctx.RequestContext(), o.UserID, rid)
|
_ = 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 {
|
if rw != nil {
|
||||||
return rw.Level
|
return rw.Level
|
||||||
}
|
}
|
||||||
return 1
|
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++
|
granted++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"bindbox-game/internal/pkg/validation"
|
"bindbox-game/internal/pkg/validation"
|
||||||
"bindbox-game/internal/pkg/wechat"
|
"bindbox-game/internal/pkg/wechat"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
|
usersvc "bindbox-game/internal/service/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type listPayOrdersRequest struct {
|
type listPayOrdersRequest struct {
|
||||||
@ -224,7 +225,8 @@ type getPayOrderResponse struct {
|
|||||||
UnitPrice int64 `json:"unit_price"`
|
UnitPrice int64 `json:"unit_price"`
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
} `json:"reward_items"`
|
} `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 {
|
func (h *handler) GetPayOrderDetail() core.HandlerFunc {
|
||||||
@ -595,6 +597,51 @@ func (h *handler) GetPayOrderDetail() core.HandlerFunc {
|
|||||||
rsp.RewardShipments = shipsAll
|
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)
|
ctx.Payload(rsp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,20 +98,70 @@ func (h *handler) CreateRefund() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 160002, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 160002, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 全额退款:恢复订单中使用的优惠券(优先使用结构化明细表)
|
// 全额退款:恢复订单中使用的优惠券(细化余额与状态逻辑)
|
||||||
var cnt int64
|
type ocRow struct {
|
||||||
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM order_coupons WHERE order_id=?", order.ID).Scan(&cnt).Error
|
UserCouponID int64
|
||||||
if cnt > 0 {
|
AppliedAmount int64
|
||||||
type ocRow struct {
|
DiscountType int32
|
||||||
UserCouponID int64
|
DiscountValue int64
|
||||||
AppliedAmount int64
|
BalanceAmount int64
|
||||||
}
|
}
|
||||||
var rows []ocRow
|
var rows []ocRow
|
||||||
_ = h.repo.GetDbR().Raw("SELECT user_coupon_id, applied_amount FROM order_coupons WHERE order_id=?", order.ID).Scan(&rows).Error
|
_ = 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 {
|
for _, r := range rows {
|
||||||
if r.UserCouponID > 0 && r.AppliedAmount > 0 {
|
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
|
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"`
|
Level int32 `json:"level" binding:"required"`
|
||||||
Sort int32 `json:"sort"`
|
Sort int32 `json:"sort"`
|
||||||
IsBoss int32 `json:"is_boss"`
|
IsBoss int32 `json:"is_boss"`
|
||||||
|
MinScore int64 `json:"min_score"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type createRewardsRequest struct {
|
type createRewardsRequest struct {
|
||||||
@ -75,6 +76,7 @@ func (h *handler) CreateIssueRewards() core.HandlerFunc {
|
|||||||
Level: r.Level,
|
Level: r.Level,
|
||||||
Sort: r.Sort,
|
Sort: r.Sort,
|
||||||
IsBoss: r.IsBoss,
|
IsBoss: r.IsBoss,
|
||||||
|
MinScore: r.MinScore,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +126,7 @@ func (h *handler) ListIssueRewards() core.HandlerFunc {
|
|||||||
Level: v.Level,
|
Level: v.Level,
|
||||||
Sort: v.Sort,
|
Sort: v.Sort,
|
||||||
IsBoss: v.IsBoss,
|
IsBoss: v.IsBoss,
|
||||||
|
MinScore: v.MinScore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.Payload(res)
|
ctx.Payload(res)
|
||||||
@ -139,6 +142,7 @@ type modifyRewardRequest struct {
|
|||||||
Level *int32 `json:"level"`
|
Level *int32 `json:"level"`
|
||||||
Sort *int32 `json:"sort"`
|
Sort *int32 `json:"sort"`
|
||||||
IsBoss *int32 `json:"is_boss"`
|
IsBoss *int32 `json:"is_boss"`
|
||||||
|
MinScore *int64 `json:"min_score"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModifyIssueReward 更新期数奖励
|
// ModifyIssueReward 更新期数奖励
|
||||||
@ -180,6 +184,7 @@ func (h *handler) ModifyIssueReward() core.HandlerFunc {
|
|||||||
Level: req.Level,
|
Level: req.Level,
|
||||||
Sort: req.Sort,
|
Sort: req.Sort,
|
||||||
IsBoss: req.IsBoss,
|
IsBoss: req.IsBoss,
|
||||||
|
MinScore: req.MinScore,
|
||||||
}
|
}
|
||||||
if err := h.activity.ModifyIssueReward(ctx.RequestContext(), rewardID, in); err != nil {
|
if err := h.activity.ModifyIssueReward(ctx.RequestContext(), rewardID, in); err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, err.Error()))
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package pay
|
package pay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -344,7 +345,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
break
|
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)
|
fmt.Printf("[支付回调-抽奖] SelectItemBySlot 结果 rid=%d err=%v\n", rid, e2)
|
||||||
if e2 != nil || rid <= 0 {
|
if e2 != nil || rid <= 0 {
|
||||||
fmt.Printf("[支付回调-抽奖] ❌ SelectItemBySlot 失败,跳出循环\n")
|
fmt.Printf("[支付回调-抽奖] ❌ SelectItemBySlot 失败,跳出循环\n")
|
||||||
@ -362,6 +363,8 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
fmt.Printf("[支付回调-抽奖] ❌ 创建开奖日志失败 err=%v,可能已存在,跳过\n", errLog)
|
fmt.Printf("[支付回调-抽奖] ❌ 创建开奖日志失败 err=%v,可能已存在,跳过\n", errLog)
|
||||||
continue
|
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)
|
_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), ord.UserID, rid)
|
||||||
// 道具卡效果处理
|
// 道具卡效果处理
|
||||||
fmt.Printf("[支付回调-道具卡] 开始检查 活动允许道具卡=%t 道具卡ID=%d\n", act.AllowItemCards, icID)
|
fmt.Printf("[支付回调-道具卡] 开始检查 活动允许道具卡=%t 道具卡ID=%d\n", act.AllowItemCards, icID)
|
||||||
@ -398,7 +401,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
} else {
|
} else {
|
||||||
sel := strat.NewDefault(h.readDB, h.writeDB)
|
sel := strat.NewDefault(h.readDB, h.writeDB)
|
||||||
for i := done; i < dc; i++ {
|
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 {
|
if e2 != nil || rid <= 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -412,6 +415,8 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
fmt.Printf("[支付回调-默认玩法] ❌ 创建开奖日志失败 err=%v,可能已存在,跳过\n", errLog)
|
fmt.Printf("[支付回调-默认玩法] ❌ 创建开奖日志失败 err=%v,可能已存在,跳过\n", errLog)
|
||||||
break
|
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})
|
_, 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 {
|
if errGrant != nil {
|
||||||
fmt.Printf("[支付回调-默认玩法] ❌ 发奖失败 err=%v,执行退款\n", errGrant)
|
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) {
|
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 {
|
if len(drawLogs) == 0 {
|
||||||
fmt.Printf("[即时开奖-虚拟发货] 没有开奖记录,跳过 order_id=%d\n", orderID)
|
fmt.Printf("[即时开奖-虚拟发货] 没有开奖记录,跳过 order_id=%d\n", orderID)
|
||||||
return
|
return
|
||||||
@ -475,7 +481,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
// 收集赏品名称
|
// 收集赏品名称
|
||||||
var rewardNames []string
|
var rewardNames []string
|
||||||
for _, lg := range drawLogs {
|
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)
|
rewardNames = append(rewardNames, rw.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -485,20 +491,20 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
// 获取支付交易信息
|
// 获取支付交易信息
|
||||||
var tx *model.PaymentTransactions
|
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 == "" {
|
if tx == nil || tx.TransactionID == "" {
|
||||||
fmt.Printf("[即时开奖-虚拟发货] 没有支付交易记录,跳过 order_no=%s\n", orderNo)
|
fmt.Printf("[即时开奖-虚拟发货] 没有支付交易记录,跳过 order_no=%s\n", orderNo)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 获取用户openid
|
// 获取用户openid
|
||||||
var u *model.Users
|
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 := ""
|
payerOpenid := ""
|
||||||
if u != nil {
|
if u != nil {
|
||||||
payerOpenid = u.Openid
|
payerOpenid = u.Openid
|
||||||
}
|
}
|
||||||
fmt.Printf("[即时开奖-虚拟发货] 上传 order_no=%s transaction_id=%s items_desc=%s\n", orderNo, tx.TransactionID, itemsDesc)
|
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)
|
fmt.Printf("[即时开奖-虚拟发货] 上传失败: %v\n", err)
|
||||||
}
|
}
|
||||||
// 【开奖后推送通知】
|
// 【开奖后推送通知】
|
||||||
@ -507,7 +513,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
AppSecret: c.Wechat.AppSecret,
|
AppSecret: c.Wechat.AppSecret,
|
||||||
LotteryResultTemplateID: c.Wechat.LotteryResultTemplateID,
|
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)
|
}(ord.ID, ord.OrderNo, ord.UserID, act.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,10 +96,10 @@ type cancelOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type cancelOrderResponse struct {
|
type cancelOrderResponse struct {
|
||||||
OrderID int64 `json:"order_id"`
|
OrderID int64 `json:"order_id"`
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
CancelledAt time.Time `json:"cancelled_at"`
|
CancelledAt *time.Time `json:"cancelled_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelOrder 取消订单
|
// 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)
|
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"` // 排序
|
Sort int32 `gorm:"column:sort;comment:排序" json:"sort"` // 排序
|
||||||
IsBoss int32 `gorm:"column:is_boss;comment:Boss 1 是 0 不是" json:"is_boss"` // Boss 1 是 0 不是
|
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"`
|
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,24 +12,24 @@ const TableNameOrders = "orders"
|
|||||||
|
|
||||||
// Orders 订单
|
// Orders 订单
|
||||||
type Orders struct {
|
type Orders struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
||||||
UserID int64 `gorm:"column:user_id;not null;comment:下单用户ID(user_members.id)" json:"user_id"` // 下单用户ID(user_members.id)
|
UserID int64 `gorm:"column:user_id;not null;comment:下单用户ID(user_members.id)" json:"user_id"` // 下单用户ID(user_members.id)
|
||||||
OrderNo string `gorm:"column:order_no;not null;comment:业务订单号(唯一)" json:"order_no"` // 业务订单号(唯一)
|
OrderNo string `gorm:"column:order_no;not null;comment:业务订单号(唯一)" json:"order_no"` // 业务订单号(唯一)
|
||||||
SourceType int32 `gorm:"column:source_type;not null;default:1;comment:来源:1商城直购 2抽奖票据 3其他" json:"source_type"` // 来源:1商城直购 2抽奖票据 3其他
|
SourceType int32 `gorm:"column:source_type;not null;default:1;comment:来源:1商城直购 2抽奖票据 3其他" json:"source_type"` // 来源:1商城直购 2抽奖票据 3其他
|
||||||
TotalAmount int64 `gorm:"column:total_amount;not null;comment:订单总金额(分)" json:"total_amount"` // 订单总金额(分)
|
TotalAmount int64 `gorm:"column:total_amount;not null;comment:订单总金额(分)" json:"total_amount"` // 订单总金额(分)
|
||||||
DiscountAmount int64 `gorm:"column:discount_amount;not null;comment:优惠券抵扣金额(分)" json:"discount_amount"` // 优惠券抵扣金额(分)
|
DiscountAmount int64 `gorm:"column:discount_amount;not null;comment:优惠券抵扣金额(分)" json:"discount_amount"` // 优惠券抵扣金额(分)
|
||||||
PointsAmount int64 `gorm:"column:points_amount;not null;comment:积分抵扣金额(分)" json:"points_amount"` // 积分抵扣金额(分)
|
PointsAmount int64 `gorm:"column:points_amount;not null;comment:积分抵扣金额(分)" json:"points_amount"` // 积分抵扣金额(分)
|
||||||
ActualAmount int64 `gorm:"column:actual_amount;not null;comment:实际支付金额(分)" json:"actual_amount"` // 实际支付金额(分)
|
ActualAmount int64 `gorm:"column:actual_amount;not null;comment:实际支付金额(分)" json:"actual_amount"` // 实际支付金额(分)
|
||||||
Status int32 `gorm:"column:status;not null;default:1;comment:订单状态:1待支付 2已支付 3已取消 4已退款" json:"status"` // 订单状态:1待支付 2已支付 3已取消 4已退款
|
Status int32 `gorm:"column:status;not null;default:1;comment:订单状态:1待支付 2已支付 3已取消 4已退款" json:"status"` // 订单状态:1待支付 2已支付 3已取消 4已退款
|
||||||
PayPreorderID int64 `gorm:"column:pay_preorder_id;comment:关联预支付单ID(payment_preorder.id)" json:"pay_preorder_id"` // 关联预支付单ID(payment_preorder.id)
|
PayPreorderID int64 `gorm:"column:pay_preorder_id;comment:关联预支付单ID(payment_preorder.id)" json:"pay_preorder_id"` // 关联预支付单ID(payment_preorder.id)
|
||||||
PaidAt time.Time `gorm:"column:paid_at;comment:支付完成时间" json:"paid_at"` // 支付完成时间
|
PaidAt *time.Time `gorm:"column:paid_at;comment:支付完成时间" json:"paid_at"` // 支付完成时间
|
||||||
CancelledAt time.Time `gorm:"column:cancelled_at;comment:取消时间" json:"cancelled_at"` // 取消时间
|
CancelledAt *time.Time `gorm:"column:cancelled_at;comment:取消时间" json:"cancelled_at"` // 取消时间
|
||||||
UserAddressID int64 `gorm:"column:user_address_id;comment:收货地址ID(user_addresses.id)" json:"user_address_id"` // 收货地址ID(user_addresses.id)
|
UserAddressID int64 `gorm:"column:user_address_id;comment:收货地址ID(user_addresses.id)" json:"user_address_id"` // 收货地址ID(user_addresses.id)
|
||||||
IsConsumed int32 `gorm:"column:is_consumed;not null;comment:是否已履约/消耗(对虚拟资产)" json:"is_consumed"` // 是否已履约/消耗(对虚拟资产)
|
IsConsumed int32 `gorm:"column:is_consumed;not null;comment:是否已履约/消耗(对虚拟资产)" json:"is_consumed"` // 是否已履约/消耗(对虚拟资产)
|
||||||
PointsLedgerID int64 `gorm:"column:points_ledger_id;comment:积分扣减流水ID(user_points_ledger.id)" json:"points_ledger_id"` // 积分扣减流水ID(user_points_ledger.id)
|
PointsLedgerID int64 `gorm:"column:points_ledger_id;comment:积分扣减流水ID(user_points_ledger.id)" json:"points_ledger_id"` // 积分扣减流水ID(user_points_ledger.id)
|
||||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName Orders's table name
|
// TableName Orders's table name
|
||||||
|
|||||||
@ -12,8 +12,10 @@ import (
|
|||||||
"bindbox-game/internal/dblogger"
|
"bindbox-game/internal/dblogger"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/logger"
|
"bindbox-game/internal/pkg/logger"
|
||||||
|
"bindbox-game/internal/pkg/redis"
|
||||||
"bindbox-game/internal/repository/mysql"
|
"bindbox-game/internal/repository/mysql"
|
||||||
"bindbox-game/internal/router/interceptor"
|
"bindbox-game/internal/router/interceptor"
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -39,9 +41,15 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init Redis
|
||||||
|
if err := redis.Init(context.Background(), logger); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
rdb := redis.GetClient()
|
||||||
|
|
||||||
// 实例化拦截器
|
// 实例化拦截器
|
||||||
adminHandler := admin.New(logger, db)
|
adminHandler := admin.New(logger, db)
|
||||||
activityHandler := activityapi.New(logger, db)
|
activityHandler := activityapi.New(logger, db, rdb)
|
||||||
taskCenterHandler := taskcenterapi.New(logger, db)
|
taskCenterHandler := taskcenterapi.New(logger, db)
|
||||||
userHandler := userapi.New(logger, db)
|
userHandler := userapi.New(logger, db)
|
||||||
commonHandler := commonapi.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", activityHandler.ListActivityIssues())
|
||||||
appPublicApiRouter.GET("/activities/:activity_id/issues/:issue_id/rewards", activityHandler.ListIssueRewards())
|
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", activityHandler.ListDrawLogs())
|
||||||
|
appPublicApiRouter.GET("/activities/:activity_id/issues/:issue_id/draw_logs_grouped", activityHandler.ListDrawLogsByLevel())
|
||||||
|
|
||||||
// APP 端轮播图
|
// APP 端轮播图
|
||||||
appPublicApiRouter.GET("/banners", appapi.NewBanner(logger, db).ListBannersForApp())
|
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("/users/weixin/login", userHandler.WeixinLogin())
|
||||||
appPublicApiRouter.POST("/address-share/submit", userHandler.SubmitAddressShare())
|
appPublicApiRouter.POST("/address-share/submit", userHandler.SubmitAddressShare())
|
||||||
|
appPublicApiRouter.GET("/matching/card_types", activityHandler.ListMatchingCardTypes())
|
||||||
}
|
}
|
||||||
|
|
||||||
// APP 端认证接口路由组
|
// APP 端认证接口路由组
|
||||||
@ -317,8 +327,8 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
|||||||
appAuthApiRouter.GET("/lottery/result", activityHandler.LotteryResultByOrder())
|
appAuthApiRouter.GET("/lottery/result", activityHandler.LotteryResultByOrder())
|
||||||
|
|
||||||
// 对对碰游戏
|
// 对对碰游戏
|
||||||
appAuthApiRouter.POST("/matching/start", activityHandler.StartMatchingGame())
|
appAuthApiRouter.POST("/matching/preorder", activityHandler.PreOrderMatchingGame())
|
||||||
appAuthApiRouter.POST("/matching/play", activityHandler.PlayMatchingGame())
|
appAuthApiRouter.POST("/matching/check", activityHandler.CheckMatchingGame())
|
||||||
appAuthApiRouter.GET("/matching/state", activityHandler.GetMatchingGameState())
|
appAuthApiRouter.GET("/matching/state", activityHandler.GetMatchingGameState())
|
||||||
|
|
||||||
appAuthApiRouter.POST("/users/:user_id/inventory/address-share/create", userHandler.CreateAddressShare())
|
appAuthApiRouter.POST("/users/:user_id/inventory/address-share/create", userHandler.CreateAddressShare())
|
||||||
|
|||||||
@ -11,219 +11,221 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// CreateActivity 创建活动
|
// CreateActivity 创建活动
|
||||||
// 参数: in 活动创建输入
|
// 参数: in 活动创建输入
|
||||||
// 返回: 活动记录与错误
|
// 返回: 活动记录与错误
|
||||||
CreateActivity(ctx context.Context, in CreateActivityInput) (*model.Activities, error)
|
CreateActivity(ctx context.Context, in CreateActivityInput) (*model.Activities, error)
|
||||||
// ModifyActivity 修改活动
|
// ModifyActivity 修改活动
|
||||||
// 参数: id 活动ID, in 修改输入
|
// 参数: id 活动ID, in 修改输入
|
||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
ModifyActivity(ctx context.Context, id int64, in ModifyActivityInput) error
|
ModifyActivity(ctx context.Context, id int64, in ModifyActivityInput) error
|
||||||
// DeleteActivity 删除活动
|
// DeleteActivity 删除活动
|
||||||
// 参数: id 活动ID
|
// 参数: id 活动ID
|
||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
DeleteActivity(ctx context.Context, id int64) error
|
DeleteActivity(ctx context.Context, id int64) error
|
||||||
// GetActivity 获取活动详情
|
// GetActivity 获取活动详情
|
||||||
// 参数: id 活动ID
|
// 参数: id 活动ID
|
||||||
// 返回: 活动记录与错误
|
// 返回: 活动记录与错误
|
||||||
GetActivity(ctx context.Context, id int64) (*model.Activities, error)
|
GetActivity(ctx context.Context, id int64) (*model.Activities, error)
|
||||||
// ListActivities 活动列表
|
// ListActivities 活动列表
|
||||||
// 参数: in 列表查询输入
|
// 参数: in 列表查询输入
|
||||||
// 返回: 活动集合、总数与错误
|
// 返回: 活动集合、总数与错误
|
||||||
ListActivities(ctx context.Context, in ListActivitiesInput) (items []*model.Activities, total int64, err error)
|
ListActivities(ctx context.Context, in ListActivitiesInput) (items []*model.Activities, total int64, err error)
|
||||||
|
|
||||||
// ListIssues 活动期列表
|
// ListIssues 活动期列表
|
||||||
// 参数: activityID 活动ID, page 页码, pageSize 每页数量
|
// 参数: activityID 活动ID, page 页码, pageSize 每页数量
|
||||||
// 返回: 期列表、总数与错误
|
// 返回: 期列表、总数与错误
|
||||||
ListIssues(ctx context.Context, activityID int64, page, pageSize int) (items []*model.ActivityIssues, total int64, err error)
|
ListIssues(ctx context.Context, activityID int64, page, pageSize int) (items []*model.ActivityIssues, total int64, err error)
|
||||||
// CreateIssue 创建期
|
// CreateIssue 创建期
|
||||||
// 参数: activityID 活动ID, in 创建输入
|
// 参数: activityID 活动ID, in 创建输入
|
||||||
// 返回: 期记录与错误
|
// 返回: 期记录与错误
|
||||||
CreateIssue(ctx context.Context, activityID int64, in CreateIssueInput) (*model.ActivityIssues, error)
|
CreateIssue(ctx context.Context, activityID int64, in CreateIssueInput) (*model.ActivityIssues, error)
|
||||||
// ModifyIssue 修改期
|
// ModifyIssue 修改期
|
||||||
// 参数: issueID 期ID, in 修改输入
|
// 参数: issueID 期ID, in 修改输入
|
||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
ModifyIssue(ctx context.Context, issueID int64, in ModifyIssueInput) error
|
ModifyIssue(ctx context.Context, issueID int64, in ModifyIssueInput) error
|
||||||
// DeleteIssue 删除期
|
// DeleteIssue 删除期
|
||||||
// 参数: issueID 期ID
|
// 参数: issueID 期ID
|
||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
DeleteIssue(ctx context.Context, issueID int64) error
|
DeleteIssue(ctx context.Context, issueID int64) error
|
||||||
|
|
||||||
// CreateIssueRewards 批量创建期奖励
|
// CreateIssueRewards 批量创建期奖励
|
||||||
// 参数: issueID 期ID, rewards 奖励创建输入数组
|
// 参数: issueID 期ID, rewards 奖励创建输入数组
|
||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
CreateIssueRewards(ctx context.Context, issueID int64, rewards []CreateRewardInput) error
|
CreateIssueRewards(ctx context.Context, issueID int64, rewards []CreateRewardInput) error
|
||||||
// ListIssueRewards 查询期奖励列表
|
// ListIssueRewards 查询期奖励列表
|
||||||
// 参数: issueID 期ID
|
// 参数: issueID 期ID
|
||||||
// 返回: 奖励集合与错误
|
// 返回: 奖励集合与错误
|
||||||
ListIssueRewards(ctx context.Context, issueID int64) (items []*model.ActivityRewardSettings, err error)
|
ListIssueRewards(ctx context.Context, issueID int64) (items []*model.ActivityRewardSettings, err error)
|
||||||
// ModifyIssueReward 修改单个奖励
|
// ModifyIssueReward 修改单个奖励
|
||||||
// 参数: rewardID 奖励ID, in 修改输入
|
// 参数: rewardID 奖励ID, in 修改输入
|
||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
ModifyIssueReward(ctx context.Context, rewardID int64, in ModifyRewardInput) error
|
ModifyIssueReward(ctx context.Context, rewardID int64, in ModifyRewardInput) error
|
||||||
// DeleteIssueReward 删除单个奖励
|
// DeleteIssueReward 删除单个奖励
|
||||||
// 参数: rewardID 奖励ID
|
// 参数: rewardID 奖励ID
|
||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
DeleteIssueReward(ctx context.Context, rewardID int64) error
|
DeleteIssueReward(ctx context.Context, rewardID int64) error
|
||||||
|
|
||||||
// ListDrawLogs 抽奖记录列表
|
// ListDrawLogs 抽奖记录列表
|
||||||
// 参数: issueID 期ID, page/pageSize 分页
|
// 参数: issueID 期ID, page/pageSize 分页
|
||||||
// 返回: 抽奖记录集合、总数与错误
|
// 返回: 抽奖记录集合、总数与错误
|
||||||
ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int) (items []*model.ActivityDrawLogs, total int64, err error)
|
ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int) (items []*model.ActivityDrawLogs, total int64, err error)
|
||||||
|
|
||||||
// GetCategoryNames 批量查询分类名称
|
// GetCategoryNames 批量查询分类名称
|
||||||
// 参数: ids 分类ID数组
|
// 参数: ids 分类ID数组
|
||||||
// 返回: id->名称映射与错误
|
// 返回: id->名称映射与错误
|
||||||
GetCategoryNames(ctx context.Context, ids []int64) (map[int64]string, error)
|
GetCategoryNames(ctx context.Context, ids []int64) (map[int64]string, error)
|
||||||
|
|
||||||
// CopyActivity 复制活动及其期次与奖励
|
// CopyActivity 复制活动及其期次与奖励
|
||||||
// 参数: activityID 源活动ID
|
// 参数: activityID 源活动ID
|
||||||
// 返回: 新活动ID与错误
|
// 返回: 新活动ID与错误
|
||||||
CopyActivity(ctx context.Context, activityID int64) (int64, error)
|
CopyActivity(ctx context.Context, activityID int64) (int64, error)
|
||||||
|
|
||||||
// SaveActivityDrawConfig 保存活动开奖配置
|
// SaveActivityDrawConfig 保存活动开奖配置
|
||||||
// 参数: activityID 活动ID, cfg 配置
|
// 参数: activityID 活动ID, cfg 配置
|
||||||
// 返回: 错误信息
|
// 返回: 错误信息
|
||||||
SaveActivityDrawConfig(ctx context.Context, activityID int64, cfg DrawConfig) error
|
SaveActivityDrawConfig(ctx context.Context, activityID int64, cfg DrawConfig) error
|
||||||
// GetActivityDrawConfig 读取活动开奖配置
|
// GetActivityDrawConfig 读取活动开奖配置
|
||||||
// 参数: activityID 活动ID
|
// 参数: activityID 活动ID
|
||||||
// 返回: 配置与错误
|
// 返回: 配置与错误
|
||||||
GetActivityDrawConfig(ctx context.Context, activityID int64) (*DrawConfig, error)
|
GetActivityDrawConfig(ctx context.Context, activityID int64) (*DrawConfig, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
logger logger.CustomLogger
|
logger logger.CustomLogger
|
||||||
readDB *dao.Query
|
readDB *dao.Query
|
||||||
writeDB *dao.Query
|
writeDB *dao.Query
|
||||||
repo mysql.Repo
|
repo mysql.Repo
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(l logger.CustomLogger, db mysql.Repo) Service {
|
func New(l logger.CustomLogger, db mysql.Repo) Service {
|
||||||
return &service{
|
return &service{
|
||||||
logger: l,
|
logger: l,
|
||||||
readDB: dao.Use(db.GetDbR()),
|
readDB: dao.Use(db.GetDbR()),
|
||||||
writeDB: dao.Use(db.GetDbW()),
|
writeDB: dao.Use(db.GetDbW()),
|
||||||
repo: db,
|
repo: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateActivityInput struct {
|
type CreateActivityInput struct {
|
||||||
// Name 活动名称
|
// Name 活动名称
|
||||||
Name string
|
Name string
|
||||||
// Banner 活动头图
|
// Banner 活动头图
|
||||||
Banner string
|
Banner string
|
||||||
// Image 活动主图
|
// Image 活动主图
|
||||||
Image string
|
Image string
|
||||||
// GameplayIntro 玩法介绍
|
// GameplayIntro 玩法介绍
|
||||||
GameplayIntro string
|
GameplayIntro string
|
||||||
// ActivityCategoryID 活动分类ID
|
// ActivityCategoryID 活动分类ID
|
||||||
ActivityCategoryID int64
|
ActivityCategoryID int64
|
||||||
// Status 活动状态
|
// Status 活动状态
|
||||||
Status int32
|
Status int32
|
||||||
// PriceDraw 单次抽奖价格(分)
|
// PriceDraw 单次抽奖价格(分)
|
||||||
PriceDraw int64
|
PriceDraw int64
|
||||||
// IsBoss 是否Boss活动
|
// IsBoss 是否Boss活动
|
||||||
IsBoss int32
|
IsBoss int32
|
||||||
// StartTime 活动开始时间(可选)
|
// StartTime 活动开始时间(可选)
|
||||||
StartTime *time.Time
|
StartTime *time.Time
|
||||||
// EndTime 活动结束时间(可选)
|
// EndTime 活动结束时间(可选)
|
||||||
EndTime *time.Time
|
EndTime *time.Time
|
||||||
AllowItemCards int32
|
AllowItemCards int32
|
||||||
AllowCoupons int32
|
AllowCoupons int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModifyActivityInput struct {
|
type ModifyActivityInput struct {
|
||||||
// Name 活动名称
|
// Name 活动名称
|
||||||
Name string
|
Name string
|
||||||
// Banner 活动头图
|
// Banner 活动头图
|
||||||
Banner string
|
Banner string
|
||||||
// Image 活动主图
|
// Image 活动主图
|
||||||
Image string
|
Image string
|
||||||
// GameplayIntro 玩法介绍
|
// GameplayIntro 玩法介绍
|
||||||
GameplayIntro string
|
GameplayIntro string
|
||||||
// ActivityCategoryID 活动分类ID
|
// ActivityCategoryID 活动分类ID
|
||||||
ActivityCategoryID int64
|
ActivityCategoryID int64
|
||||||
// Status 活动状态
|
// Status 活动状态
|
||||||
Status int32
|
Status int32
|
||||||
// PriceDraw 单次抽奖价格(分)
|
// PriceDraw 单次抽奖价格(分)
|
||||||
PriceDraw int64
|
PriceDraw int64
|
||||||
// IsBoss 是否Boss活动
|
// IsBoss 是否Boss活动
|
||||||
IsBoss int32
|
IsBoss int32
|
||||||
// StartTime 活动开始时间(可选)
|
// StartTime 活动开始时间(可选)
|
||||||
StartTime *time.Time
|
StartTime *time.Time
|
||||||
// EndTime 活动结束时间(可选)
|
// EndTime 活动结束时间(可选)
|
||||||
EndTime *time.Time
|
EndTime *time.Time
|
||||||
AllowItemCards *int32
|
AllowItemCards *int32
|
||||||
AllowCoupons *int32
|
AllowCoupons *int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListActivitiesInput struct {
|
type ListActivitiesInput struct {
|
||||||
// Name 名称过滤
|
// Name 名称过滤
|
||||||
Name string
|
Name string
|
||||||
// CategoryID 分类过滤
|
// CategoryID 分类过滤
|
||||||
CategoryID int64
|
CategoryID int64
|
||||||
// IsBoss Boss过滤
|
// IsBoss Boss过滤
|
||||||
IsBoss *int32
|
IsBoss *int32
|
||||||
// Status 状态过滤
|
// Status 状态过滤
|
||||||
Status *int32
|
Status *int32
|
||||||
// Page 页码
|
// Page 页码
|
||||||
Page int
|
Page int
|
||||||
// PageSize 每页数量
|
// PageSize 每页数量
|
||||||
PageSize int
|
PageSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateIssueInput struct {
|
type CreateIssueInput struct {
|
||||||
// IssueNumber 期号
|
// IssueNumber 期号
|
||||||
IssueNumber string
|
IssueNumber string
|
||||||
// Status 状态
|
// Status 状态
|
||||||
Status int32
|
Status int32
|
||||||
// Sort 排序
|
// Sort 排序
|
||||||
Sort int32
|
Sort int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModifyIssueInput struct {
|
type ModifyIssueInput struct {
|
||||||
// IssueNumber 期号
|
// IssueNumber 期号
|
||||||
IssueNumber string
|
IssueNumber string
|
||||||
// Status 状态
|
// Status 状态
|
||||||
Status int32
|
Status int32
|
||||||
// Sort 排序
|
// Sort 排序
|
||||||
Sort int32
|
Sort int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateRewardInput struct {
|
type CreateRewardInput struct {
|
||||||
// ProductID 商品ID
|
// ProductID 商品ID
|
||||||
ProductID int64
|
ProductID int64
|
||||||
// Name 奖励名称
|
// Name 奖励名称
|
||||||
Name string
|
Name string
|
||||||
// Weight 权重
|
// Weight 权重
|
||||||
Weight int32
|
Weight int32
|
||||||
// Quantity 数量(-1 表示不限)
|
// Quantity 数量(-1 表示不限)
|
||||||
Quantity int64
|
Quantity int64
|
||||||
// OriginalQty 初始数量
|
// OriginalQty 初始数量
|
||||||
OriginalQty int64
|
OriginalQty int64
|
||||||
// Level 奖励等级
|
// Level 奖励等级
|
||||||
Level int32
|
Level int32
|
||||||
// Sort 排序
|
// Sort 排序
|
||||||
Sort int32
|
Sort int32
|
||||||
// IsBoss 是否Boss奖励
|
// IsBoss 是否Boss奖励
|
||||||
IsBoss int32
|
IsBoss int32
|
||||||
|
MinScore int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModifyRewardInput struct {
|
type ModifyRewardInput struct {
|
||||||
// ProductID 商品ID
|
// ProductID 商品ID
|
||||||
ProductID *int64
|
ProductID *int64
|
||||||
// Name 奖励名称
|
// Name 奖励名称
|
||||||
Name string
|
Name string
|
||||||
// Weight 权重
|
// Weight 权重
|
||||||
Weight *int32
|
Weight *int32
|
||||||
// Quantity 数量(-1 表示不限)
|
// Quantity 数量(-1 表示不限)
|
||||||
Quantity *int64
|
Quantity *int64
|
||||||
// OriginalQty 初始数量
|
// OriginalQty 初始数量
|
||||||
OriginalQty *int64
|
OriginalQty *int64
|
||||||
// Level 奖励等级
|
// Level 奖励等级
|
||||||
Level *int32
|
Level *int32
|
||||||
// Sort 排序
|
// Sort 排序
|
||||||
Sort *int32
|
Sort *int32
|
||||||
// IsBoss 是否Boss奖励
|
// IsBoss 是否Boss奖励
|
||||||
IsBoss *int32
|
IsBoss *int32
|
||||||
|
MinScore *int64
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ func (s *service) CreateIssueRewards(ctx context.Context, issueID int64, rewards
|
|||||||
Level: r.Level,
|
Level: r.Level,
|
||||||
Sort: r.Sort,
|
Sort: r.Sort,
|
||||||
IsBoss: r.IsBoss,
|
IsBoss: r.IsBoss,
|
||||||
|
MinScore: r.MinScore,
|
||||||
}
|
}
|
||||||
if err := tx.ActivityRewardSettings.WithContext(ctx).Create(item); err != nil {
|
if err := tx.ActivityRewardSettings.WithContext(ctx).Create(item); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -39,6 +39,9 @@ func (s *service) ModifyIssueReward(ctx context.Context, rewardID int64, in Modi
|
|||||||
if in.IsBoss != nil {
|
if in.IsBoss != nil {
|
||||||
item.IsBoss = *in.IsBoss
|
item.IsBoss = *in.IsBoss
|
||||||
}
|
}
|
||||||
|
if in.MinScore != nil {
|
||||||
|
item.MinScore = *in.MinScore
|
||||||
|
}
|
||||||
item.UpdatedAt = time.Now()
|
item.UpdatedAt = time.Now()
|
||||||
return s.writeDB.ActivityRewardSettings.WithContext(ctx).Save(item)
|
return s.writeDB.ActivityRewardSettings.WithContext(ctx).Save(item)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -177,7 +177,7 @@ func StartScheduledSettlement(l logger.CustomLogger, repo mysql.Repo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用 claim 中的 slot_index 直接获取奖品
|
// 使用 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 {
|
if err != nil || rid <= 0 {
|
||||||
fmt.Printf("[定时开奖-一番赏] ❌ SelectItemBySlot失败 err=%v rid=%d\n", err, rid)
|
fmt.Printf("[定时开奖-一番赏] ❌ SelectItemBySlot失败 err=%v rid=%d\n", err, rid)
|
||||||
continue
|
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)
|
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 {
|
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})
|
_, _ = 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 {
|
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})
|
_, _ = 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
|
package strategy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
"crypto/sha256"
|
"context"
|
||||||
"encoding/binary"
|
"crypto/hmac"
|
||||||
"errors"
|
"crypto/sha256"
|
||||||
"fmt"
|
"encoding/binary"
|
||||||
"context"
|
"errors"
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ichibanStrategy struct {
|
type ichibanStrategy struct {
|
||||||
read *dao.Query
|
read *dao.Query
|
||||||
write *dao.Query
|
write *dao.Query
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIchiban(read *dao.Query, write *dao.Query) *ichibanStrategy {
|
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) {
|
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()
|
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") }
|
if err != nil || act == nil || len(act.CommitmentSeedMaster) == 0 {
|
||||||
rewards, err := s.read.ActivityRewardSettings.WithContext(ctx).ReadDB().Where(s.read.ActivityRewardSettings.IssueID.Eq(issueID)).Order(
|
return 0, nil, errors.New("commitment not found")
|
||||||
s.read.ActivityRewardSettings.Level.Desc(),
|
}
|
||||||
s.read.ActivityRewardSettings.Sort.Asc(),
|
rewards, err := s.read.ActivityRewardSettings.WithContext(ctx).ReadDB().Where(s.read.ActivityRewardSettings.IssueID.Eq(issueID)).Order(
|
||||||
s.read.ActivityRewardSettings.ID.Asc(),
|
s.read.ActivityRewardSettings.Level.Desc(),
|
||||||
).Find()
|
s.read.ActivityRewardSettings.Sort.Asc(),
|
||||||
if err != nil || len(rewards) == 0 { return 0, nil, errors.New("no rewards") }
|
s.read.ActivityRewardSettings.ID.Asc(),
|
||||||
var totalSlots int64
|
).Find()
|
||||||
for _, r := range rewards {
|
if err != nil || len(rewards) == 0 {
|
||||||
if r.OriginalQty > 0 { totalSlots += r.OriginalQty }
|
return 0, nil, errors.New("no rewards")
|
||||||
}
|
}
|
||||||
if totalSlots <= 0 { return 0, nil, errors.New("no slots") }
|
var totalSlots int64
|
||||||
if slotIndex < 0 || slotIndex >= totalSlots { return 0, nil, errors.New("slot out of range") }
|
for _, r := range rewards {
|
||||||
// build list
|
if r.OriginalQty > 0 {
|
||||||
slots := make([]int64, 0, totalSlots)
|
totalSlots += r.OriginalQty
|
||||||
for _, r := range rewards {
|
}
|
||||||
for i := int64(0); i < r.OriginalQty; i++ { slots = append(slots, r.ID) }
|
}
|
||||||
}
|
if totalSlots <= 0 {
|
||||||
// deterministic shuffle by server seed
|
return 0, nil, errors.New("no slots")
|
||||||
mac := hmac.New(sha256.New, act.CommitmentSeedMaster)
|
}
|
||||||
for i := int(totalSlots-1); i > 0; i-- {
|
if slotIndex < 0 || slotIndex >= totalSlots {
|
||||||
mac.Reset()
|
return 0, nil, errors.New("slot out of range")
|
||||||
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issueID)))
|
}
|
||||||
sum := mac.Sum(nil)
|
// build list
|
||||||
rnd := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
|
slots := make([]int64, 0, totalSlots)
|
||||||
slots[i], slots[rnd] = slots[rnd], slots[i]
|
for _, r := range rewards {
|
||||||
}
|
for i := int64(0); i < r.OriginalQty; i++ {
|
||||||
picked := slots[slotIndex]
|
slots = append(slots, r.ID)
|
||||||
proof := map[string]any{"total_slots": totalSlots, "slot_index": slotIndex}
|
}
|
||||||
return picked, proof, nil
|
}
|
||||||
|
// 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 {
|
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"
|
"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 包含订单项的订单信息
|
// OrderWithItems 包含订单项的订单信息
|
||||||
type OrderWithItems struct {
|
type OrderWithItems struct {
|
||||||
*model.Orders
|
*model.Orders
|
||||||
@ -16,6 +38,7 @@ type OrderWithItems struct {
|
|||||||
IsDraw bool `json:"is_draw"`
|
IsDraw bool `json:"is_draw"`
|
||||||
IsWinner bool `json:"is_winner"`
|
IsWinner bool `json:"is_winner"`
|
||||||
RewardLevel int32 `json:"reward_level"`
|
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) {
|
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()
|
logs, _ := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.OrderID.Eq(order.ID)).Find()
|
||||||
if log != nil {
|
if len(logs) > 0 {
|
||||||
res.IsDraw = true
|
res.IsDraw = true
|
||||||
|
// 取第一条记录的信息
|
||||||
|
log := logs[0]
|
||||||
res.IsWinner = log.IsWinner == 1
|
res.IsWinner = log.IsWinner == 1
|
||||||
res.RewardLevel = log.Level
|
res.RewardLevel = log.Level
|
||||||
issue, _ := s.readDB.ActivityIssues.WithContext(ctx).ReadDB().Where(s.readDB.ActivityIssues.ID.Eq(log.IssueID)).First()
|
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
|
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
|
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)
|
activityMap := make(map[int64]*model.Activities)
|
||||||
issueMap := make(map[int64]*model.ActivityIssues)
|
issueMap := make(map[int64]*model.ActivityIssues)
|
||||||
|
var allDrawLogIDs []int64
|
||||||
|
|
||||||
if len(orderIDs) > 0 {
|
if len(orderIDs) > 0 {
|
||||||
logs, _ := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.OrderID.In(orderIDs...)).Find()
|
logs, _ := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.OrderID.In(orderIDs...)).Find()
|
||||||
var issueIDs []int64
|
var issueIDs []int64
|
||||||
for _, log := range logs {
|
for _, log := range logs {
|
||||||
drawLogsMap[log.OrderID] = log
|
drawLogsListMap[log.OrderID] = append(drawLogsListMap[log.OrderID], log)
|
||||||
issueIDs = append(issueIDs, log.IssueID)
|
issueIDs = append(issueIDs, log.IssueID)
|
||||||
|
allDrawLogIDs = append(allDrawLogIDs, log.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(issueIDs) > 0 {
|
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))
|
items = make([]*OrderWithItems, len(orders))
|
||||||
for i, order := range orders {
|
for i, order := range orders {
|
||||||
@ -233,8 +312,10 @@ func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status
|
|||||||
Items: itemsMap[order.ID],
|
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
|
items[i].IsDraw = true
|
||||||
|
// 取第一条记录的基本信息
|
||||||
|
log := logs[0]
|
||||||
items[i].IsWinner = log.IsWinner == 1
|
items[i].IsWinner = log.IsWinner == 1
|
||||||
items[i].RewardLevel = log.Level
|
items[i].RewardLevel = log.Level
|
||||||
if issue, ok := issueMap[log.IssueID]; ok {
|
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
|
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,
|
PointsAmount: 0,
|
||||||
ActualAmount: 0,
|
ActualAmount: 0,
|
||||||
IsConsumed: 0,
|
IsConsumed: 0,
|
||||||
PaidAt: now, // 设置支付时间为当前时间
|
PaidAt: &now, // 设置支付时间为当前时间
|
||||||
CancelledAt: minValidTime, // 设置取消时间为最小有效时间,避免MySQL错误
|
CancelledAt: &minValidTime, // 设置取消时间为最小有效时间,避免MySQL错误
|
||||||
Remark: req.Remark,
|
Remark: req.Remark,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: 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-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 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":"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