diff --git a/configs/dev_configs.toml b/configs/dev_configs.toml index 5abf92a..0ab1acd 100644 --- a/configs/dev_configs.toml +++ b/configs/dev_configs.toml @@ -1,31 +1,50 @@ -[mysql] - [mysql.read] - addr = "127.0.0.1:3306" - user = "root" - pass = "123456" - name = "bindbox_game" - [mysql.write] - addr = "127.0.0.1:3306" - user = "root" - pass = "123456" - name = "bindbox_game" +[language] +local = 'zh-cn' + +[mysql.read] +addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555' +name = 'bindbox_game' +pass = 'api2api..' +user = 'root' [redis] addr = "118.25.13.43:8379" pass = "xbm#2023by1024" db = 5 -[random] - commit_master_key = "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456" + +[mysql.write] +addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555' +name = 'bindbox_game' +pass = 'api2api..' +user = 'root' + +[jwt] +admin_secret = "m9ycX9RTPyuYTWw9FrCc" [wechat] - app_id = "" - app_secret = "" - lottery_result_template_id = "" +app_id = "wx26ad074017e1e63f" +app_secret = "026c19ce4f3bb090c56573024c59a8be" +lottery_result_template_id = "O2eqJQD3pn-vQ6g2z9DWzINVwOmPoz8yW-172J_YcpI" + +[cos] +bucket = "keaiya-1259195914" +region = "ap-shanghai" +secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6" +secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr" +# 可选:如有 CDN/自定义域名则填写,否则留空 +base_url = "" + +[random] +commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c" [wechatpay] - mchid = "" - serial_no = "" - private_key_path = "" - api_v3_key = "" - notify_url = "http://localhost:9991/api/pay/wechat/notify" \ No newline at end of file + mchid = "1610439635" + serial_no = "3AFD505D597831F8E931EBFFEEB5976B81F66F03" + private_key_path = "./configs/cert/apiclient_key.pem" + api_v3_key = "3tbwEFZV3fZtOslpUJC7Sacb8qjzhm05" + notify_url = "https://mini-chat.1024tool.vip/api/pay/wechat/notify" + public_key_id = "PUB_KEY_ID_0116104396352025041000211519001600" + public_key_path = "./configs/cert/pub_key.pem" + + diff --git a/docs/docs.go b/docs/docs.go index 58f6528..14f39fb 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3658,6 +3658,35 @@ const docTemplate = `{ } } }, + "/api/app/categories": { + "get": { + "description": "获取APP端商品分类列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "APP端.基础" + ], + "summary": "获取分类列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.listAppCategoriesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/api/app/lottery/join": { "post": { "security": [ @@ -3912,6 +3941,29 @@ const docTemplate = `{ } } }, + "/api/app/notices": { + "get": { + "description": "获取APP首页滚动公告", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "APP端.基础" + ], + "summary": "获取公告列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.listAppNoticesResponse" + } + } + } + } + }, "/api/app/orders/{order_id}": { "get": { "security": [ @@ -4148,6 +4200,189 @@ const docTemplate = `{ } } }, + "/api/app/store/items": { + "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "分页获取积分商城商品列表,支持按类型筛选(product/item_card/coupon)", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "APP端.积分商城" + ], + "summary": "获取积分商城商品列表", + "parameters": [ + { + "type": "string", + "description": "商品类型: product(默认), item_card, coupon", + "name": "kind", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.listStoreItemsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/api/app/task-center/tasks": { + "get": { + "description": "获取当前可用的任务列表,支持分页", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TaskCenter(App)" + ], + "summary": "获取任务列表(App)", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "任务列表", + "schema": { + "$ref": "#/definitions/taskcenter.listTasksResponse" + } + } + } + } + }, + "/api/app/task-center/tasks/{id}/claim/{user_id}": { + "post": { + "description": "用户领取指定任务层级的奖励", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TaskCenter(App)" + ], + "summary": "领取任务奖励(App)", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "用户ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "领取请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/taskcenter.claimTaskRequest" + } + } + ], + "responses": { + "200": { + "description": "领取成功", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/app/task-center/tasks/{id}/progress/{user_id}": { + "get": { + "description": "获取指定用户在特定任务上的进度详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TaskCenter(App)" + ], + "summary": "获取用户任务进度(App)", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "用户ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "任务进度详情", + "schema": { + "$ref": "#/definitions/taskcenter.taskProgressResponse" + } + } + } + } + }, "/api/app/users/weixin/login": { "post": { "description": "微信静默登录(需传递 code;可选 invite_code)", @@ -5218,6 +5453,18 @@ const docTemplate = `{ "name": "page_size", "in": "query", "required": true + }, + { + "type": "integer", + "description": "订单状态:1待支付 2已支付 3已取消 4已退款", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "是否已消耗/履约:0否 1是", + "name": "is_consumed", + "in": "query" } ], "responses": { @@ -5435,6 +5682,58 @@ const docTemplate = `{ } } }, + "/api/app/users/{user_id}/points/redeem-item-card": { + "post": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "使用积分兑换指定数量的道具卡", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "APP端.用户" + ], + "summary": "积分兑换道具卡", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "兑换请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.redeemItemCardRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.redeemItemCardResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/api/app/users/{user_id}/points/redeem-product": { "post": { "security": [ @@ -5728,133 +6027,6 @@ const docTemplate = `{ "responses": {} } }, - "/app/task_center/tasks": { - "get": { - "description": "获取当前可用的任务列表,支持分页", - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TaskCenter(App)" - ], - "summary": "获取任务列表(App)", - "parameters": [ - { - "type": "integer", - "default": 1, - "description": "页码", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "default": 20, - "description": "每页数量", - "name": "page_size", - "in": "query" - } - ], - "responses": { - "200": { - "description": "任务列表", - "schema": { - "$ref": "#/definitions/taskcenter.listTasksResponse" - } - } - } - } - }, - "/app/task_center/tasks/{id}/progress/{user_id}": { - "get": { - "description": "获取指定用户在特定任务上的进度详情", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TaskCenter(App)" - ], - "summary": "获取用户任务进度(App)", - "parameters": [ - { - "type": "integer", - "description": "任务ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "用户ID", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "任务进度详情", - "schema": { - "$ref": "#/definitions/taskcenter.taskProgressResponse" - } - } - } - } - }, - "/app/task_center/tasks/{id}/users/{user_id}/claim": { - "post": { - "description": "用户领取指定任务层级的奖励", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TaskCenter(App)" - ], - "summary": "领取任务奖励(App)", - "parameters": [ - { - "type": "integer", - "description": "任务ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "用户ID", - "name": "user_id", - "in": "path", - "required": true - }, - { - "description": "领取请求", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/taskcenter.claimTaskRequest" - } - } - ], - "responses": { - "200": { - "description": "领取成功", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - } - }, "/common/upload/wangeditor": { "post": { "description": "适配 WangEditor 的图片上传接口", @@ -8051,6 +8223,25 @@ const docTemplate = `{ } } }, + "app.appCategoryItem": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "app.appNoticeItem": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + }, "app.bindPhoneRequest": { "type": "object", "properties": { @@ -8488,6 +8679,28 @@ const docTemplate = `{ } } }, + "app.listAppCategoriesResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/app.appCategoryItem" + } + } + } + }, + "app.listAppNoticesResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/app.appNoticeItem" + } + } + } + }, "app.listAppProductsItem": { "type": "object", "properties": { @@ -8756,6 +8969,64 @@ const docTemplate = `{ } } }, + "app.listStoreItem": { + "type": "object", + "properties": { + "discount_type": { + "type": "integer" + }, + "discount_value": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "in_stock": { + "type": "boolean" + }, + "kind": { + "type": "string" + }, + "main_image": { + "type": "string" + }, + "min_spend": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "price": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "supported": { + "type": "boolean" + } + } + }, + "app.listStoreItemsResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/app.listStoreItem" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "app.listUserItemCardUsesResponse": { "type": "object", "properties": { @@ -9010,6 +9281,34 @@ const docTemplate = `{ } } }, + "app.redeemItemCardRequest": { + "type": "object", + "properties": { + "card_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + } + } + }, + "app.redeemItemCardResponse": { + "type": "object", + "properties": { + "card_id": { + "type": "integer" + }, + "ledger_id": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, "app.redeemProductRequest": { "type": "object", "properties": { @@ -10023,6 +10322,9 @@ const docTemplate = `{ "user.OrderWithItems": { "type": "object", "properties": { + "activity_id": { + "type": "integer" + }, "activity_name": { "type": "string" }, @@ -10034,6 +10336,12 @@ const docTemplate = `{ "description": "取消时间", "type": "string" }, + "category_id": { + "type": "integer" + }, + "category_name": { + "type": "string" + }, "created_at": { "description": "创建时间", "type": "string" @@ -10083,6 +10391,9 @@ const docTemplate = `{ "description": "关联预支付单ID(payment_preorder.id)", "type": "integer" }, + "play_type": { + "type": "string" + }, "points_amount": { "description": "积分抵扣金额(分)", "type": "integer" diff --git a/docs/swagger.json b/docs/swagger.json index 2d0aacd..0d52fd7 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3650,6 +3650,35 @@ } } }, + "/api/app/categories": { + "get": { + "description": "获取APP端商品分类列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "APP端.基础" + ], + "summary": "获取分类列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.listAppCategoriesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/api/app/lottery/join": { "post": { "security": [ @@ -3904,6 +3933,29 @@ } } }, + "/api/app/notices": { + "get": { + "description": "获取APP首页滚动公告", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "APP端.基础" + ], + "summary": "获取公告列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.listAppNoticesResponse" + } + } + } + } + }, "/api/app/orders/{order_id}": { "get": { "security": [ @@ -4140,6 +4192,189 @@ } } }, + "/api/app/store/items": { + "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "分页获取积分商城商品列表,支持按类型筛选(product/item_card/coupon)", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "APP端.积分商城" + ], + "summary": "获取积分商城商品列表", + "parameters": [ + { + "type": "string", + "description": "商品类型: product(默认), item_card, coupon", + "name": "kind", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.listStoreItemsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/api/app/task-center/tasks": { + "get": { + "description": "获取当前可用的任务列表,支持分页", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TaskCenter(App)" + ], + "summary": "获取任务列表(App)", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "任务列表", + "schema": { + "$ref": "#/definitions/taskcenter.listTasksResponse" + } + } + } + } + }, + "/api/app/task-center/tasks/{id}/claim/{user_id}": { + "post": { + "description": "用户领取指定任务层级的奖励", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TaskCenter(App)" + ], + "summary": "领取任务奖励(App)", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "用户ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "领取请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/taskcenter.claimTaskRequest" + } + } + ], + "responses": { + "200": { + "description": "领取成功", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/app/task-center/tasks/{id}/progress/{user_id}": { + "get": { + "description": "获取指定用户在特定任务上的进度详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TaskCenter(App)" + ], + "summary": "获取用户任务进度(App)", + "parameters": [ + { + "type": "integer", + "description": "任务ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "用户ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "任务进度详情", + "schema": { + "$ref": "#/definitions/taskcenter.taskProgressResponse" + } + } + } + } + }, "/api/app/users/weixin/login": { "post": { "description": "微信静默登录(需传递 code;可选 invite_code)", @@ -5210,6 +5445,18 @@ "name": "page_size", "in": "query", "required": true + }, + { + "type": "integer", + "description": "订单状态:1待支付 2已支付 3已取消 4已退款", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "是否已消耗/履约:0否 1是", + "name": "is_consumed", + "in": "query" } ], "responses": { @@ -5427,6 +5674,58 @@ } } }, + "/api/app/users/{user_id}/points/redeem-item-card": { + "post": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "使用积分兑换指定数量的道具卡", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "APP端.用户" + ], + "summary": "积分兑换道具卡", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "兑换请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.redeemItemCardRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.redeemItemCardResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/api/app/users/{user_id}/points/redeem-product": { "post": { "security": [ @@ -5720,133 +6019,6 @@ "responses": {} } }, - "/app/task_center/tasks": { - "get": { - "description": "获取当前可用的任务列表,支持分页", - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TaskCenter(App)" - ], - "summary": "获取任务列表(App)", - "parameters": [ - { - "type": "integer", - "default": 1, - "description": "页码", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "default": 20, - "description": "每页数量", - "name": "page_size", - "in": "query" - } - ], - "responses": { - "200": { - "description": "任务列表", - "schema": { - "$ref": "#/definitions/taskcenter.listTasksResponse" - } - } - } - } - }, - "/app/task_center/tasks/{id}/progress/{user_id}": { - "get": { - "description": "获取指定用户在特定任务上的进度详情", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TaskCenter(App)" - ], - "summary": "获取用户任务进度(App)", - "parameters": [ - { - "type": "integer", - "description": "任务ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "用户ID", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "任务进度详情", - "schema": { - "$ref": "#/definitions/taskcenter.taskProgressResponse" - } - } - } - } - }, - "/app/task_center/tasks/{id}/users/{user_id}/claim": { - "post": { - "description": "用户领取指定任务层级的奖励", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TaskCenter(App)" - ], - "summary": "领取任务奖励(App)", - "parameters": [ - { - "type": "integer", - "description": "任务ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "用户ID", - "name": "user_id", - "in": "path", - "required": true - }, - { - "description": "领取请求", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/taskcenter.claimTaskRequest" - } - } - ], - "responses": { - "200": { - "description": "领取成功", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - } - }, "/common/upload/wangeditor": { "post": { "description": "适配 WangEditor 的图片上传接口", @@ -8043,6 +8215,25 @@ } } }, + "app.appCategoryItem": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "app.appNoticeItem": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + }, "app.bindPhoneRequest": { "type": "object", "properties": { @@ -8480,6 +8671,28 @@ } } }, + "app.listAppCategoriesResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/app.appCategoryItem" + } + } + } + }, + "app.listAppNoticesResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/app.appNoticeItem" + } + } + } + }, "app.listAppProductsItem": { "type": "object", "properties": { @@ -8748,6 +8961,64 @@ } } }, + "app.listStoreItem": { + "type": "object", + "properties": { + "discount_type": { + "type": "integer" + }, + "discount_value": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "in_stock": { + "type": "boolean" + }, + "kind": { + "type": "string" + }, + "main_image": { + "type": "string" + }, + "min_spend": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "price": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "supported": { + "type": "boolean" + } + } + }, + "app.listStoreItemsResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/app.listStoreItem" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "app.listUserItemCardUsesResponse": { "type": "object", "properties": { @@ -9002,6 +9273,34 @@ } } }, + "app.redeemItemCardRequest": { + "type": "object", + "properties": { + "card_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + } + } + }, + "app.redeemItemCardResponse": { + "type": "object", + "properties": { + "card_id": { + "type": "integer" + }, + "ledger_id": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, "app.redeemProductRequest": { "type": "object", "properties": { @@ -10015,6 +10314,9 @@ "user.OrderWithItems": { "type": "object", "properties": { + "activity_id": { + "type": "integer" + }, "activity_name": { "type": "string" }, @@ -10026,6 +10328,12 @@ "description": "取消时间", "type": "string" }, + "category_id": { + "type": "integer" + }, + "category_name": { + "type": "string" + }, "created_at": { "description": "创建时间", "type": "string" @@ -10075,6 +10383,9 @@ "description": "关联预支付单ID(payment_preorder.id)", "type": "integer" }, + "play_type": { + "type": "string" + }, "points_amount": { "description": "积分抵扣金额(分)", "type": "integer" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b98af41..d2eede2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1353,6 +1353,18 @@ definitions: title: type: string type: object + app.appCategoryItem: + properties: + id: + type: integer + name: + type: string + type: object + app.appNoticeItem: + properties: + content: + type: string + type: object app.bindPhoneRequest: properties: code: @@ -1642,6 +1654,20 @@ definitions: $ref: '#/definitions/app.appBannerItem' type: array type: object + app.listAppCategoriesResponse: + properties: + list: + items: + $ref: '#/definitions/app.appCategoryItem' + type: array + type: object + app.listAppNoticesResponse: + properties: + list: + items: + $ref: '#/definitions/app.appNoticeItem' + type: array + type: object app.listAppProductsItem: properties: id: @@ -1816,6 +1842,44 @@ definitions: total: type: integer type: object + app.listStoreItem: + properties: + discount_type: + type: integer + discount_value: + type: integer + id: + type: integer + in_stock: + type: boolean + kind: + type: string + main_image: + type: string + min_spend: + type: integer + name: + type: string + price: + type: integer + status: + type: integer + supported: + type: boolean + type: object + app.listStoreItemsResponse: + properties: + list: + items: + $ref: '#/definitions/app.listStoreItem' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object app.listUserItemCardUsesResponse: properties: list: @@ -1981,6 +2045,24 @@ definitions: points: type: integer type: object + app.redeemItemCardRequest: + properties: + card_id: + type: integer + quantity: + type: integer + type: object + app.redeemItemCardResponse: + properties: + card_id: + type: integer + ledger_id: + type: integer + message: + type: string + success: + type: boolean + type: object app.redeemProductRequest: properties: product_id: @@ -2674,6 +2756,8 @@ definitions: type: object user.OrderWithItems: properties: + activity_id: + type: integer activity_name: type: string actual_amount: @@ -2682,6 +2766,10 @@ definitions: cancelled_at: description: 取消时间 type: string + category_id: + type: integer + category_name: + type: string created_at: description: 创建时间 type: string @@ -2717,6 +2805,8 @@ definitions: pay_preorder_id: description: 关联预支付单ID(payment_preorder.id) type: integer + play_type: + type: string points_amount: description: 积分抵扣金额(分) type: integer @@ -5153,6 +5243,25 @@ paths: summary: APP端轮播图列表 tags: - APP端.运营 + /api/app/categories: + get: + consumes: + - application/json + description: 获取APP端商品分类列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app.listAppCategoriesResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + summary: 获取分类列表 + tags: + - APP端.基础 /api/app/lottery/join: post: consumes: @@ -5313,6 +5422,21 @@ paths: summary: 获取对对碰游戏状态 tags: - APP端.活动 + /api/app/notices: + get: + consumes: + - application/json + description: 获取APP首页滚动公告 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app.listAppNoticesResponse' + summary: 获取公告列表 + tags: + - APP端.基础 /api/app/orders/{order_id}: get: consumes: @@ -5461,6 +5585,127 @@ paths: summary: 商品详情 tags: - APP端.商品 + /api/app/store/items: + get: + consumes: + - application/x-www-form-urlencoded + description: 分页获取积分商城商品列表,支持按类型筛选(product/item_card/coupon) + parameters: + - description: '商品类型: product(默认), item_card, coupon' + in: query + name: kind + type: string + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 20 + description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app.listStoreItemsResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] + summary: 获取积分商城商品列表 + tags: + - APP端.积分商城 + /api/app/task-center/tasks: + get: + consumes: + - application/x-www-form-urlencoded + description: 获取当前可用的任务列表,支持分页 + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 20 + description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 任务列表 + schema: + $ref: '#/definitions/taskcenter.listTasksResponse' + summary: 获取任务列表(App) + tags: + - TaskCenter(App) + /api/app/task-center/tasks/{id}/claim/{user_id}: + post: + consumes: + - application/json + description: 用户领取指定任务层级的奖励 + parameters: + - description: 任务ID + in: path + name: id + required: true + type: integer + - description: 用户ID + in: path + name: user_id + required: true + type: integer + - description: 领取请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/taskcenter.claimTaskRequest' + produces: + - application/json + responses: + "200": + description: 领取成功 + schema: + additionalProperties: true + type: object + summary: 领取任务奖励(App) + tags: + - TaskCenter(App) + /api/app/task-center/tasks/{id}/progress/{user_id}: + get: + consumes: + - application/json + description: 获取指定用户在特定任务上的进度详情 + parameters: + - description: 任务ID + in: path + name: id + required: true + type: integer + - description: 用户ID + in: path + name: user_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 任务进度详情 + schema: + $ref: '#/definitions/taskcenter.taskProgressResponse' + summary: 获取用户任务进度(App) + tags: + - TaskCenter(App) /api/app/users/{user_id}: put: consumes: @@ -6119,6 +6364,14 @@ paths: name: page_size required: true type: integer + - description: 订单状态:1待支付 2已支付 3已取消 4已退款 + in: query + name: status + type: integer + - description: 是否已消耗/履约:0否 1是 + in: query + name: is_consumed + type: integer produces: - application/json responses: @@ -6262,6 +6515,39 @@ paths: summary: 积分兑换优惠券 tags: - APP端.积分 + /api/app/users/{user_id}/points/redeem-item-card: + post: + consumes: + - application/json + description: 使用积分兑换指定数量的道具卡 + parameters: + - description: 用户ID + in: path + name: user_id + required: true + type: integer + - description: 兑换请求参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/app.redeemItemCardRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app.redeemItemCardResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] + summary: 积分兑换道具卡 + tags: + - APP端.用户 /api/app/users/{user_id}/points/redeem-product: post: consumes: @@ -6476,91 +6762,6 @@ paths: /api/v3/system/menus/simple: get: responses: {} - /app/task_center/tasks: - get: - consumes: - - application/x-www-form-urlencoded - description: 获取当前可用的任务列表,支持分页 - parameters: - - default: 1 - description: 页码 - in: query - name: page - type: integer - - default: 20 - description: 每页数量 - in: query - name: page_size - type: integer - produces: - - application/json - responses: - "200": - description: 任务列表 - schema: - $ref: '#/definitions/taskcenter.listTasksResponse' - summary: 获取任务列表(App) - tags: - - TaskCenter(App) - /app/task_center/tasks/{id}/progress/{user_id}: - get: - consumes: - - application/json - description: 获取指定用户在特定任务上的进度详情 - parameters: - - description: 任务ID - in: path - name: id - required: true - type: integer - - description: 用户ID - in: path - name: user_id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: 任务进度详情 - schema: - $ref: '#/definitions/taskcenter.taskProgressResponse' - summary: 获取用户任务进度(App) - tags: - - TaskCenter(App) - /app/task_center/tasks/{id}/users/{user_id}/claim: - post: - consumes: - - application/json - description: 用户领取指定任务层级的奖励 - parameters: - - description: 任务ID - in: path - name: id - required: true - type: integer - - description: 用户ID - in: path - name: user_id - required: true - type: integer - - description: 领取请求 - in: body - name: request - required: true - schema: - $ref: '#/definitions/taskcenter.claimTaskRequest' - produces: - - application/json - responses: - "200": - description: 领取成功 - schema: - additionalProperties: true - type: object - summary: 领取任务奖励(App) - tags: - - TaskCenter(App) /common/upload/wangeditor: post: consumes: diff --git a/internal/api/activity/app.go b/internal/api/activity/app.go index f09b88c..5a6a563 100644 --- a/internal/api/activity/app.go +++ b/internal/api/activity/app.go @@ -13,27 +13,31 @@ import ( ) type handler struct { - logger logger.CustomLogger - writeDB *dao.Query - readDB *dao.Query - activity activitysvc.Service - syscfg syscfgsvc.Service - title titlesvc.Service - repo mysql.Repo - user usersvc.Service - redis *redis.Client + logger logger.CustomLogger + writeDB *dao.Query + readDB *dao.Query + activity activitysvc.Service + syscfg syscfgsvc.Service + title titlesvc.Service + repo mysql.Repo + user usersvc.Service + redis *redis.Client + activityOrder activitysvc.ActivityOrderService // 活动订单服务 + rewardEffects activitysvc.RewardEffectsService // 奖励效果服务 } func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler { return &handler{ - logger: logger, - writeDB: dao.Use(db.GetDbW()), - readDB: dao.Use(db.GetDbR()), - activity: activitysvc.New(logger, db), - syscfg: syscfgsvc.New(logger, db), - title: titlesvc.New(logger, db), - repo: db, - user: usersvc.New(logger, db), - redis: rdb, + logger: logger, + writeDB: dao.Use(db.GetDbW()), + readDB: dao.Use(db.GetDbR()), + activity: activitysvc.New(logger, db), + syscfg: syscfgsvc.New(logger, db), + title: titlesvc.New(logger, db), + repo: db, + user: usersvc.New(logger, db), + redis: rdb, + activityOrder: activitysvc.NewActivityOrderService(logger, db), + rewardEffects: activitysvc.NewRewardEffectsService(logger, db), } } diff --git a/internal/api/activity/matching_game_app.go b/internal/api/activity/matching_game_app.go index 6a3f6d3..ddd6f18 100644 --- a/internal/api/activity/matching_game_app.go +++ b/internal/api/activity/matching_game_app.go @@ -22,7 +22,7 @@ import ( "github.com/redis/go-redis/v9" "go.uber.org/zap" - titlesvc "bindbox-game/internal/service/title" + activitysvc "bindbox-game/internal/service/activity" usersvc "bindbox-game/internal/service/user" ) @@ -542,129 +542,32 @@ func (h *handler) PreOrderMatchingGame() core.HandlerFunc { return } - // 2. Prepare Order - orderNo := h.randomID("O") // Reuse helper from lottery_app.go? No, handler is same struct but method not exported? - // randomID is private in handler. We need to duplicate or make it public. - // Let's implement a local randomID or use UUID. - // h.randomID is `func (h *handler) randomID(prefix string) string`, it IS available to `h`. - - total := activity.PriceDraw // Matching game ensures 1 draw basically? Or is it a session price? - // "PreOrder" usually implies 1 game session. - - // Create basic order model - order := &model.Orders{ - UserID: userID, - OrderNo: orderNo, - TotalAmount: total, - ActualAmount: total, - Status: 1, // Pending - SourceType: 3, // Other/Game - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - // Minimal Remark - order.Remark = fmt.Sprintf("matching_game:issue:%d", req.IssueID) - if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 { - order.Remark += fmt.Sprintf("|itemcard:%d", *req.ItemCardID) - } - - // 3. Apply Title Discount - titleEffects, _ := h.title.ResolveActiveEffects(ctx.RequestContext(), userID, titlesvc.EffectScope{ - ActivityID: &issue.ActivityID, - IssueID: &req.IssueID, - }) - for _, ef := range titleEffects { - if ef.EffectType == 2 { - 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" { - discount = order.ActualAmount * p.ValueX1000 / 1000 - } else if p.DiscountType == "fixed" { - discount = p.ValueX1000 - } - if p.MaxDiscountX1000 > 0 && discount > p.MaxDiscountX1000 { - discount = p.MaxDiscountX1000 - } - if discount > order.ActualAmount { - discount = order.ActualAmount - } - if discount > 0 { - order.ActualAmount -= discount - order.Remark += fmt.Sprintf("|title_discount:%d:%d", ef.ID, discount) - } - } - } - } - - // 4. Apply Coupon - var appliedCouponVal int64 + // 2. Create Order using ActivityOrderService + var couponID *int64 if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 { - // Reuse h.applyCouponWithCap? It is private. - // We need to either export it or duplicate logic. - // For safety and speed, let's duplicate the core check logic or use `usersvc` directly if possible. - // `h.applyCouponWithCap` logic: fetch coupon, check status, check expiry, check min_spend, calc discount. - // Let's implement simplified version here. - uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(*req.CouponID), h.readDB.UserCoupons.UserID.Eq(userID)).First() - if uc != nil && uc.Status == 1 && (uc.ValidEnd.IsZero() || uc.ValidEnd.After(time.Now())) { - sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID)).First() - if sc != nil && order.ActualAmount >= sc.MinSpend { - var discount int64 - if sc.DiscountType == 3 { // Rate (Discount) - // DiscountValue is rate x 1000 (e.g. 800 = 80%) - // Discount Amount = Total * (1 - Rate/1000) ? Or simply applied rate? - // Usually "DiscountValue" for Type 3 means "The Ratio you PAY" or "The Ratio OFF"? - // Standard "zhekou": 8 zhe = pay 80%. Discount = 20%. - // Let's assume DiscountValue=800 means 80% pay. - discount = order.ActualAmount * (1000 - sc.DiscountValue) / 1000 - } else { // Fixed (Type 1 or 2) - discount = sc.DiscountValue - } - - if discount > order.ActualAmount { - discount = order.ActualAmount - } - if discount > 0 { - order.ActualAmount -= discount - order.DiscountAmount = discount - appliedCouponVal = discount - // Record usage later - } - } - } + couponID = req.CouponID + } + var itemCardID *int64 + if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 { + itemCardID = req.ItemCardID } - // Create Order - if err := h.writeDB.Orders.WithContext(ctx.RequestContext()).Create(order); err != nil { + orderResult, err := h.activityOrder.CreateActivityOrder(ctx, activitysvc.CreateActivityOrderRequest{ + UserID: userID, + ActivityID: issue.ActivityID, + IssueID: req.IssueID, + Count: 1, + UnitPrice: activity.PriceDraw, + SourceType: 3, // 对对碰 + CouponID: couponID, + ItemCardID: itemCardID, + ExtraRemark: fmt.Sprintf("matching_game:issue:%d", req.IssueID), + }) + if err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 170002, err.Error())) return } - - // Record Coupon Usage - if appliedCouponVal > 0 && req.CouponID != nil { - _ = h.user.RecordOrderCouponUsage(ctx.RequestContext(), order.ID, *req.CouponID, appliedCouponVal) - } - - // Handle 0-amount auto-pay - if order.ActualAmount == 0 { - now := time.Now() - _, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{ - h.writeDB.Orders.Status.ColumnName().String(): 2, - h.writeDB.Orders.PaidAt.ColumnName().String(): now, - }) - // Consume Coupon - if req.CouponID != nil { - _, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(*req.CouponID)).Updates(map[string]any{ - h.readDB.UserCoupons.Status.ColumnName().String(): 2, - h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, - h.readDB.UserCoupons.UsedAt.ColumnName().String(): now, - }) - } - } + order := orderResult.Order // 2. 加载配置 configs, err := loadCardTypesFromDB(ctx.RequestContext(), h.readDB) @@ -799,6 +702,20 @@ func (h *handler) CheckMatchingGame() core.HandlerFunc { return } + // 校验:不能超过理论最大对数 + // 【关键校验】检查订单是否已支付 + // 对对碰游戏必须先支付才能结算和发奖 + order, err := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(game.OrderID)).First() + if err != nil || order == nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, 170003, "订单不存在")) + return + } + if order.Status != 2 { + fmt.Printf("[对对碰Check] ⏳ 订单支付确认中 order_id=%d status=%d,等待回调完成\n", order.ID, order.Status) + ctx.AbortWithError(core.Error(http.StatusBadRequest, 170004, "支付确认中,请稍后重试")) + return + } + // 校验:不能超过理论最大对数 if req.TotalPairs > game.MaxPossiblePairs { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, fmt.Sprintf("invalid total pairs: %d > %d", req.TotalPairs, game.MaxPossiblePairs))) @@ -818,19 +735,10 @@ func (h *handler) CheckMatchingGame() core.HandlerFunc { if r.Quantity <= 0 { continue } - if int64(req.TotalPairs) >= r.MinScore { - if candidate == nil { - candidate = r - } else { - // Prioritize Higher MinScore - if r.MinScore > candidate.MinScore { - candidate = r - } else if r.MinScore == candidate.MinScore { - if r.Sort < candidate.Sort { - candidate = r - } - } - } + // 精确匹配:用户消除的对子数 == 奖品设置的 MinScore + if int64(req.TotalPairs) == r.MinScore { + candidate = r + break // 精确匹配,直接使用 } } @@ -844,6 +752,101 @@ func (h *handler) CheckMatchingGame() core.HandlerFunc { Name: candidate.Name, Level: candidate.Level, } + + // 4. Apply Item Card Effects (if any) + ord, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(game.OrderID)).First() + if ord != nil { + icID := parseItemCardIDFromRemark(ord.Remark) + fmt.Printf("[道具卡-CheckMatchingGame] 从订单备注解析道具卡ID icID=%d 订单备注=%s\n", icID, ord.Remark) + if icID > 0 { + uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where( + h.readDB.UserItemCards.ID.Eq(icID), + h.readDB.UserItemCards.UserID.Eq(game.UserID), + h.readDB.UserItemCards.Status.Eq(1), + ).First() + 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() + now := time.Now() + if ic != nil && !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) { + scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == game.ActivityID) || (ic.ScopeType == 4 && ic.IssueID == game.IssueID) + fmt.Printf("[道具卡-CheckMatchingGame] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, game.ActivityID, game.IssueID, scopeOK) + if scopeOK { + // Apply effect based on type + if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 { + // Double reward + fmt.Printf("[道具卡-CheckMatchingGame] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s\n", ic.RewardMultiplierX1000, candidate.ID, candidate.Name) + rid := candidate.ID + _, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), game.UserID, usersvc.GrantRewardToOrderRequest{ + OrderID: game.OrderID, + ProductID: candidate.ProductID, + Quantity: 1, + ActivityID: &game.ActivityID, + RewardID: &rid, + Remark: candidate.Name + "(倍数)", + }) + } else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 { + // Probability boost - try to upgrade to better reward + fmt.Printf("[道具卡-CheckMatchingGame] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000) + allRewards, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where( + h.readDB.ActivityRewardSettings.IssueID.Eq(game.IssueID), + ).Find() + var better *model.ActivityRewardSettings + for _, r := range allRewards { + if r.MinScore > candidate.MinScore && r.Quantity > 0 { + if better == nil || r.MinScore < better.MinScore { + better = r + } + } + } + if better != nil { + // Use crypto/rand for secure random + randBytes := make([]byte, 4) + rand.Read(randBytes) + randVal := int32(binary.BigEndian.Uint32(randBytes) % 1000) + if randVal < ic.BoostRateX1000 { + fmt.Printf("[道具卡-CheckMatchingGame] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", better.ID, better.Name) + rid := better.ID + _, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), game.UserID, usersvc.GrantRewardToOrderRequest{ + OrderID: game.OrderID, + ProductID: better.ProductID, + Quantity: 1, + ActivityID: &game.ActivityID, + RewardID: &rid, + Remark: better.Name + "(升级)", + }) + } + } + } + + // Void the item card + fmt.Printf("[道具卡-CheckMatchingGame] 核销道具卡 用户道具卡ID=%d\n", icID) + // Get DrawLog ID for the order + drawLog, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where( + h.readDB.ActivityDrawLogs.OrderID.Eq(game.OrderID), + ).First() + var drawLogID int64 + if drawLog != nil { + drawLogID = drawLog.ID + } + _, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where( + h.writeDB.UserItemCards.ID.Eq(icID), + h.writeDB.UserItemCards.UserID.Eq(game.UserID), + h.writeDB.UserItemCards.Status.Eq(1), + ).Updates(map[string]any{ + h.writeDB.UserItemCards.Status.ColumnName().String(): 2, + h.writeDB.UserItemCards.UsedDrawLogID.ColumnName().String(): drawLogID, + h.writeDB.UserItemCards.UsedActivityID.ColumnName().String(): game.ActivityID, + h.writeDB.UserItemCards.UsedIssueID.ColumnName().String(): game.IssueID, + h.writeDB.UserItemCards.UsedAt.ColumnName().String(): now, + }) + } + } + } + } + } } } } diff --git a/internal/api/activity/matching_game_app_test.go b/internal/api/activity/matching_game_app_test.go index 4879f7a..bb5a413 100644 --- a/internal/api/activity/matching_game_app_test.go +++ b/internal/api/activity/matching_game_app_test.go @@ -1 +1,98 @@ package app + +import ( + "bindbox-game/internal/repository/mysql/model" + "testing" +) + +// TestSelectRewardByExactMatch 测试对对碰选奖逻辑:精确匹配 TotalPairs == MinScore +func TestSelectRewardByExactMatch(t *testing.T) { + // 模拟奖品设置 + rewards := []*model.ActivityRewardSettings{ + {ID: 1, Name: "奖品A-10对", MinScore: 10, Quantity: 5}, + {ID: 2, Name: "奖品B-20对", MinScore: 20, Quantity: 5}, + {ID: 3, Name: "奖品C-30对", MinScore: 30, Quantity: 5}, + {ID: 4, Name: "奖品D-40对", MinScore: 40, Quantity: 5}, + {ID: 5, Name: "奖品E-45对", MinScore: 45, Quantity: 5}, + } + + testCases := []struct { + name string + totalPairs int64 + expectReward *int64 // nil = 无匹配 + expectName string + }{ + {"精确匹配10对", 10, ptr(int64(1)), "奖品A-10对"}, + {"精确匹配20对", 20, ptr(int64(2)), "奖品B-20对"}, + {"精确匹配30对", 30, ptr(int64(3)), "奖品C-30对"}, + {"精确匹配40对", 40, ptr(int64(4)), "奖品D-40对"}, + {"精确匹配45对", 45, ptr(int64(5)), "奖品E-45对"}, + {"15对-无匹配", 15, nil, ""}, + {"25对-无匹配", 25, nil, ""}, + {"35对-无匹配", 35, nil, ""}, + {"50对-无匹配", 50, nil, ""}, + {"0对-无匹配", 0, nil, ""}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + candidate := selectRewardExact(rewards, tc.totalPairs) + + if tc.expectReward == nil { + if candidate != nil { + t.Errorf("期望无匹配,但得到奖品: %s (ID=%d)", candidate.Name, candidate.ID) + } + } else { + if candidate == nil { + t.Errorf("期望匹配奖品ID=%d,但无匹配", *tc.expectReward) + } else if candidate.ID != *tc.expectReward { + t.Errorf("期望奖品ID=%d,实际=%d", *tc.expectReward, candidate.ID) + } else if candidate.Name != tc.expectName { + t.Errorf("期望奖品名=%s,实际=%s", tc.expectName, candidate.Name) + } + } + }) + } +} + +// TestSelectRewardWithZeroQuantity 测试库存为0时不匹配 +func TestSelectRewardWithZeroQuantity(t *testing.T) { + rewards := []*model.ActivityRewardSettings{ + {ID: 1, Name: "奖品A-10对", MinScore: 10, Quantity: 0}, // 库存为0 + {ID: 2, Name: "奖品B-20对", MinScore: 20, Quantity: 5}, + } + + // 即使精确匹配,库存为0也不应匹配 + candidate := selectRewardExact(rewards, 10) + if candidate != nil { + t.Errorf("库存为0时不应匹配,但得到: %s", candidate.Name) + } + + // 库存>0应正常匹配 + candidate = selectRewardExact(rewards, 20) + if candidate == nil { + t.Error("库存>0时应匹配,但无匹配") + } else if candidate.ID != 2 { + t.Errorf("期望ID=2,实际=%d", candidate.ID) + } +} + +// selectRewardExact 精确匹配选奖逻辑(从matching_game_app.go提取) +// 这是 CheckMatchingGame 中实际使用的逻辑 +func selectRewardExact(rewards []*model.ActivityRewardSettings, totalPairs int64) *model.ActivityRewardSettings { + for _, r := range rewards { + if r.Quantity <= 0 { + continue + } + // 精确匹配:用户消除的对子数 == 奖品设置的 MinScore + if totalPairs == r.MinScore { + return r + } + } + return nil +} + +// ptr 辅助函数:创建int64指针 +func ptr(v int64) *int64 { + return &v +} diff --git a/internal/api/admin/pay_orders_admin.go b/internal/api/admin/pay_orders_admin.go index 8f628d2..4d8b0ea 100644 --- a/internal/api/admin/pay_orders_admin.go +++ b/internal/api/admin/pay_orders_admin.go @@ -5,6 +5,8 @@ import ( "net/http" "time" + "go.uber.org/zap" + "bindbox-game/configs" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" @@ -16,7 +18,9 @@ import ( type listPayOrdersRequest struct { Page int `form:"page"` + Current int `form:"current"` PageSize int `form:"page_size"` + Size int `form:"size"` Status *int32 `form:"status"` SourceType *int32 `form:"source_type"` ExcludeSourceType *int32 `form:"exclude_source_type"` @@ -44,6 +48,12 @@ func (h *handler) ListPayOrders() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } + if req.Page <= 0 && req.Current > 0 { + req.Page = req.Current + } + if req.PageSize <= 0 && req.Size > 0 { + req.PageSize = req.Size + } if req.Page <= 0 { req.Page = 1 } @@ -63,9 +73,6 @@ func (h *handler) ListPayOrders() core.HandlerFunc { if req.ExcludeSourceType != nil { q = q.Not(h.readDB.Orders.SourceType.Eq(*req.ExcludeSourceType)) } - if req.SourceType == nil && req.ExcludeSourceType == nil { - q = q.Not(h.readDB.Orders.SourceType.Eq(3)) - } if req.UserID != nil { q = q.Where(h.readDB.Orders.UserID.Eq(*req.UserID)) } @@ -114,9 +121,13 @@ func (h *handler) ListPayOrders() core.HandlerFunc { pointsRate = r } } + out := make([]map[string]any, 0, len(rows)) for _, o := range rows { - ledgers, _ := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserPointsLedger.RefTable.Eq("orders"), h.readDB.UserPointsLedger.RefID.Eq(o.OrderNo)).Find() + ledgers, err := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserPointsLedger.RefTable.Eq("orders"), h.readDB.UserPointsLedger.RefID.Eq(o.OrderNo)).Find() + if err != nil { + h.logger.Error("ListPayOrders fetch ledgers error", zap.Error(err), zap.String("order_no", o.OrderNo)) + } var consumePointsSum int64 for _, lg := range ledgers { if lg.Points < 0 { @@ -141,7 +152,7 @@ func (h *handler) ListPayOrders() core.HandlerFunc { "actual_amount": o.ActualAmount, "status": o.Status, "paid_at": func() string { - if !o.PaidAt.IsZero() { + if o.PaidAt != nil && !o.PaidAt.IsZero() { return o.PaidAt.Format("2006-01-02 15:04:05") } return "" @@ -466,9 +477,14 @@ func (h *handler) GetPayOrderDetail() core.HandlerFunc { CouponAppliedAmount int64 `json:"coupon_applied_amount"` TotalAmount int64 `json:"total_amount"` }{ - Status: order.Status, - ActualAmount: order.ActualAmount, - PaidAt: order.PaidAt.Format("2006-01-02 15:04:05"), + Status: order.Status, + ActualAmount: order.ActualAmount, + PaidAt: func() string { + if order.PaidAt != nil && !order.PaidAt.IsZero() { + return order.PaidAt.Format("2006-01-02 15:04:05") + } + return "" + }(), PayPreorderID: order.PayPreorderID, TransactionID: "", PointsAmount: pointsAmountCents, diff --git a/internal/api/admin/pay_orders_export.go b/internal/api/admin/pay_orders_export.go index ea8853d..4df86f8 100644 --- a/internal/api/admin/pay_orders_export.go +++ b/internal/api/admin/pay_orders_export.go @@ -1,98 +1,117 @@ package admin import ( - "bytes" - "fmt" - "net/http" - "time" + "bytes" + "fmt" + "net/http" + "time" - "bindbox-game/internal/code" - "bindbox-game/internal/pkg/core" - "bindbox-game/internal/pkg/validation" - "github.com/tealeg/xlsx" + "bindbox-game/internal/code" + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/validation" + + "github.com/tealeg/xlsx" ) type exportOrdersRequest struct { - Status *int32 `form:"status"` - SourceType *int32 `form:"source_type"` - StartDate string `form:"start_date"` - EndDate string `form:"end_date"` + Status *int32 `form:"status"` + SourceType *int32 `form:"source_type"` + StartDate string `form:"start_date"` + EndDate string `form:"end_date"` } func (h *handler) ExportPayOrders() core.HandlerFunc { - return func(ctx core.Context) { - req := new(exportOrdersRequest) - if err := ctx.ShouldBindForm(req); err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) - return - } - q := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB() - if req.Status != nil { - q = q.Where(h.readDB.Orders.Status.Eq(*req.Status)) - } - if req.SourceType != nil { - q = q.Where(h.readDB.Orders.SourceType.Eq(*req.SourceType)) - } - if req.StartDate != "" { - if t, err := time.Parse("2006-01-02", req.StartDate); err == nil { - q = q.Where(h.readDB.Orders.CreatedAt.Gte(t)) - } - } - if req.EndDate != "" { - if t, err := time.Parse("2006-01-02", req.EndDate); err == nil { - t = t.Add(24 * time.Hour).Add(-time.Second) - q = q.Where(h.readDB.Orders.CreatedAt.Lte(t)) - } - } - rows, err := q.Order(h.readDB.Orders.ID.Desc()).Limit(5000).Find() - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, 23001, err.Error())) - return - } - var pointsRate int64 = 1 - if cfgRate, _ := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq("points_exchange_per_cent")).First(); cfgRate != nil { - var r int64 - _, _ = fmt.Sscanf(cfgRate.ConfigValue, "%d", &r) - if r > 0 { pointsRate = r } - } - file := xlsx.NewFile() - sheet, _ := file.AddSheet("orders") - header := []string{"订单号", "用户ID", "来源", "状态", "总金额", "折扣", "积分抵扣(分)", "积分抵扣(积分)", "优惠券抵扣(分)", "实付", "支付时间", "创建时间"} - row := sheet.AddRow() - for _, hname := range header { - cell := row.AddCell() - cell.Value = hname - } - for _, o := range rows { - leds, _ := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserPointsLedger.RefTable.Eq("orders"), h.readDB.UserPointsLedger.RefID.Eq(o.OrderNo)).Find() - var consumePointsSum int64 - for _, lg := range leds { if lg.Points < 0 { consumePointsSum += -lg.Points } } - pa := o.PointsAmount - if pa == 0 && consumePointsSum > 0 { pa = consumePointsSum / pointsRate } - pu := int64(0) - if consumePointsSum > 0 { pu = consumePointsSum } else if pa > 0 { pu = pa * pointsRate } - ocs, _ := h.readDB.OrderCoupons.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.OrderCoupons.OrderID.Eq(o.ID)).Find() - var couponApplied int64 - for _, oc := range ocs { couponApplied += oc.AppliedAmount } - r := sheet.AddRow() - r.AddCell().Value = o.OrderNo - r.AddCell().SetInt64(o.UserID) - r.AddCell().SetInt(int(o.SourceType)) - r.AddCell().SetInt(int(o.Status)) - r.AddCell().SetInt64(o.TotalAmount) - r.AddCell().SetInt64(o.DiscountAmount) - r.AddCell().SetInt64(pa) - r.AddCell().SetInt64(pu) - r.AddCell().SetInt64(couponApplied) - r.AddCell().SetInt64(o.ActualAmount) - r.AddCell().Value = o.PaidAt.Format("2006-01-02 15:04:05") - r.AddCell().Value = o.CreatedAt.Format("2006-01-02 15:04:05") - } - var buf bytes.Buffer - if err := file.Write(&buf); err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, 23002, err.Error())) - return - } - ctx.ExcelData("orders.xlsx", buf.Bytes()) - } + return func(ctx core.Context) { + req := new(exportOrdersRequest) + if err := ctx.ShouldBindForm(req); err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) + return + } + q := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB() + if req.Status != nil { + q = q.Where(h.readDB.Orders.Status.Eq(*req.Status)) + } + if req.SourceType != nil { + q = q.Where(h.readDB.Orders.SourceType.Eq(*req.SourceType)) + } + if req.StartDate != "" { + if t, err := time.Parse("2006-01-02", req.StartDate); err == nil { + q = q.Where(h.readDB.Orders.CreatedAt.Gte(t)) + } + } + if req.EndDate != "" { + if t, err := time.Parse("2006-01-02", req.EndDate); err == nil { + t = t.Add(24 * time.Hour).Add(-time.Second) + q = q.Where(h.readDB.Orders.CreatedAt.Lte(t)) + } + } + rows, err := q.Order(h.readDB.Orders.ID.Desc()).Limit(5000).Find() + if err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, 23001, err.Error())) + return + } + var pointsRate int64 = 1 + if cfgRate, _ := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq("points_exchange_per_cent")).First(); cfgRate != nil { + var r int64 + _, _ = fmt.Sscanf(cfgRate.ConfigValue, "%d", &r) + if r > 0 { + pointsRate = r + } + } + file := xlsx.NewFile() + sheet, _ := file.AddSheet("orders") + header := []string{"订单号", "用户ID", "来源", "状态", "总金额", "折扣", "积分抵扣(分)", "积分抵扣(积分)", "优惠券抵扣(分)", "实付", "支付时间", "创建时间"} + row := sheet.AddRow() + for _, hname := range header { + cell := row.AddCell() + cell.Value = hname + } + for _, o := range rows { + leds, _ := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserPointsLedger.RefTable.Eq("orders"), h.readDB.UserPointsLedger.RefID.Eq(o.OrderNo)).Find() + var consumePointsSum int64 + for _, lg := range leds { + if lg.Points < 0 { + consumePointsSum += -lg.Points + } + } + pa := o.PointsAmount + if pa == 0 && consumePointsSum > 0 { + pa = consumePointsSum / pointsRate + } + pu := int64(0) + if consumePointsSum > 0 { + pu = consumePointsSum + } else if pa > 0 { + pu = pa * pointsRate + } + ocs, _ := h.readDB.OrderCoupons.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.OrderCoupons.OrderID.Eq(o.ID)).Find() + var couponApplied int64 + for _, oc := range ocs { + couponApplied += oc.AppliedAmount + } + r := sheet.AddRow() + r.AddCell().Value = o.OrderNo + r.AddCell().SetInt64(o.UserID) + r.AddCell().SetInt(int(o.SourceType)) + r.AddCell().SetInt(int(o.Status)) + r.AddCell().SetInt64(o.TotalAmount) + r.AddCell().SetInt64(o.DiscountAmount) + r.AddCell().SetInt64(pa) + r.AddCell().SetInt64(pu) + r.AddCell().SetInt64(couponApplied) + r.AddCell().SetInt64(o.ActualAmount) + if o.PaidAt != nil { + r.AddCell().Value = o.PaidAt.Format("2006-01-02 15:04:05") + } else { + r.AddCell().Value = "" + } + r.AddCell().Value = o.CreatedAt.Format("2006-01-02 15:04:05") + } + var buf bytes.Buffer + if err := file.Write(&buf); err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, 23002, err.Error())) + return + } + ctx.ExcelData("orders.xlsx", buf.Bytes()) + } } diff --git a/internal/api/admin/users_admin.go b/internal/api/admin/users_admin.go index 19117d8..73866a4 100644 --- a/internal/api/admin/users_admin.go +++ b/internal/api/admin/users_admin.go @@ -314,7 +314,7 @@ func (h *handler) ListUserOrders() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) return } - items, total, err := h.user.ListOrdersWithItems(ctx.RequestContext(), userID, 0, req.Page, req.PageSize) + items, total, err := h.user.ListOrdersWithItems(ctx.RequestContext(), userID, 0, nil, req.Page, req.PageSize) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20104, err.Error())) return diff --git a/internal/api/user/orders_app.go b/internal/api/user/orders_app.go index f722950..85e15f0 100644 --- a/internal/api/user/orders_app.go +++ b/internal/api/user/orders_app.go @@ -12,9 +12,10 @@ import ( ) type listOrdersRequest struct { - Page int `form:"page"` - PageSize int `form:"page_size"` - Status int32 `form:"status"` + Page int `form:"page"` + PageSize int `form:"page_size"` + Status int32 `form:"status"` + IsConsumed *int32 `form:"is_consumed"` } type listOrdersResponse struct { Page int `json:"page"` @@ -33,6 +34,8 @@ type listOrdersResponse struct { // @Security LoginVerifyToken // @Param page query int true "页码" default(1) // @Param page_size query int true "每页数量,最多100" default(20) +// @Param status query int false "订单状态:1待支付 2已支付 3已取消 4已退款" +// @Param is_consumed query int false "是否已消耗/履约:0否 1是" // @Success 200 {object} listOrdersResponse // @Failure 400 {object} code.Failure // @Router /api/app/users/{user_id}/orders [get] @@ -45,7 +48,8 @@ func (h *handler) ListUserOrders() core.HandlerFunc { return } userID := int64(ctx.SessionUserInfo().Id) - items, total, err := h.user.ListOrdersWithItems(ctx.RequestContext(), userID, req.Status, req.Page, req.PageSize) + + items, total, err := h.user.ListOrdersWithItems(ctx.RequestContext(), userID, req.Status, req.IsConsumed, req.Page, req.PageSize) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 10002, err.Error())) return diff --git a/internal/pkg/core/core.go b/internal/pkg/core/core.go index d26b3ab..97a302b 100644 --- a/internal/pkg/core/core.go +++ b/internal/pkg/core/core.go @@ -343,7 +343,7 @@ func New(logger logger.CustomLogger, options ...Option) (Mux, error) { if err := recover(); err != nil { panicStackInfo = string(debug.Stack()) panicError = fmt.Sprintf("%+v", err) - // logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", stackInfo)) + logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", panicStackInfo)) context.AbortWithError(Error( http.StatusInternalServerError, code.ServerError, @@ -490,17 +490,17 @@ func New(logger logger.CustomLogger, options ...Option) (Mux, error) { t.Success = !ctx.IsAborted() && (ctx.Writer.Status() == http.StatusOK) t.CostSeconds = time.Since(ts).Seconds() - //logger.Info("trace-log", - // zap.Any("method", ctx.Request.Method), - // zap.Any("path", decodedURL), - // zap.Any("http_code", ctx.Writer.Status()), - // zap.Any("business_code", businessCode), - // zap.Any("success", t.Success), - // zap.Any("cost_seconds", t.CostSeconds), - // zap.Any("trace_id", t.Identifier), - // zap.Any("trace_info", t), - // zap.Error(abortErr), - //) + logger.Info("trace-log", + zap.Any("method", ctx.Request.Method), + zap.Any("path", decodedURL), + zap.Any("http_code", ctx.Writer.Status()), + zap.Any("business_code", businessCode), + zap.Any("success", t.Success), + zap.Any("cost_seconds", t.CostSeconds), + zap.Any("trace_id", t.Identifier), + zap.Any("trace_info", t), + zap.Error(abortErr), + ) traceInfo := "" if traceJsonData, err := json.Marshal(t); err == nil { diff --git a/internal/service/activity/activity_order_service.go b/internal/service/activity/activity_order_service.go new file mode 100644 index 0000000..62a6afd --- /dev/null +++ b/internal/service/activity/activity_order_service.go @@ -0,0 +1,309 @@ +package activity + +import ( + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/logger" + "bindbox-game/internal/repository/mysql" + "bindbox-game/internal/repository/mysql/dao" + "bindbox-game/internal/repository/mysql/model" + titlesvc "bindbox-game/internal/service/title" + usersvc "bindbox-game/internal/service/user" + "context" + "encoding/json" + "fmt" + "time" +) + +// ActivityOrderService 活动订单创建服务 +// 统一处理一番赏和对对碰的订单创建逻辑 +type ActivityOrderService interface { + // CreateActivityOrder 创建活动订单 + // 统一处理优惠券、称号折扣、道具卡记录等 + CreateActivityOrder(ctx core.Context, req CreateActivityOrderRequest) (*CreateActivityOrderResult, error) +} + +// CreateActivityOrderRequest 订单创建请求 +type CreateActivityOrderRequest struct { + UserID int64 // 用户ID + ActivityID int64 // 活动ID + IssueID int64 // 期ID + Count int64 // 数量 + UnitPrice int64 // 单价(分) + SourceType int32 // 订单来源类型: 2=抽奖, 3=对对碰 + CouponID *int64 // 优惠券ID(可选) + ItemCardID *int64 // 道具卡ID(可选) + ExtraRemark string // 额外备注信息 +} + +// CreateActivityOrderResult 订单创建结果 +type CreateActivityOrderResult struct { + Order *model.Orders // 创建的订单 + AppliedCouponVal int64 // 应用的优惠券抵扣金额 +} + +type activityOrderService struct { + logger logger.CustomLogger + readDB *dao.Query + writeDB *dao.Query + repo mysql.Repo + title titlesvc.Service + user usersvc.Service +} + +// NewActivityOrderService 创建活动订单服务 +func NewActivityOrderService(l logger.CustomLogger, db mysql.Repo) ActivityOrderService { + return &activityOrderService{ + logger: l, + readDB: dao.Use(db.GetDbR()), + writeDB: dao.Use(db.GetDbW()), + repo: db, + title: titlesvc.New(l, db), + user: usersvc.New(l, db), + } +} + +// CreateActivityOrder 创建活动订单 +func (s *activityOrderService) CreateActivityOrder(ctx core.Context, req CreateActivityOrderRequest) (*CreateActivityOrderResult, error) { + userID := req.UserID + count := req.Count + if count <= 0 { + count = 1 + } + total := req.UnitPrice * count + + // 1. 创建订单基础信息 + orderNo := fmt.Sprintf("O%s", time.Now().Format("20060102150405")) + order := &model.Orders{ + UserID: userID, + OrderNo: orderNo, + SourceType: req.SourceType, + TotalAmount: total, + ActualAmount: total, + Status: 1, // Pending + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 设置备注 + if req.ExtraRemark != "" { + order.Remark = req.ExtraRemark + } else { + order.Remark = fmt.Sprintf("activity:%d|issue:%d|count:%d", req.ActivityID, req.IssueID, count) + } + + // 2. 应用称号折扣 (Title Discount) + titleEffects, _ := s.title.ResolveActiveEffects(ctx.RequestContext(), userID, titlesvc.EffectScope{ + ActivityID: &req.ActivityID, + IssueID: &req.IssueID, + }) + for _, ef := range titleEffects { + if ef.EffectType == 2 { // Discount effect + 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" { + discount = order.ActualAmount * p.ValueX1000 / 1000 + } else if p.DiscountType == "fixed" { + discount = p.ValueX1000 + } + if p.MaxDiscountX1000 > 0 && discount > p.MaxDiscountX1000 { + discount = p.MaxDiscountX1000 + } + if discount > order.ActualAmount { + discount = order.ActualAmount + } + if discount > 0 { + order.ActualAmount -= discount + order.Remark += fmt.Sprintf("|title_discount:%d:%d", ef.ID, discount) + } + } + } + } + + // 3. 应用优惠券 (using applyCouponWithCap logic) + var appliedCouponVal int64 + if req.CouponID != nil && *req.CouponID > 0 { + fmt.Printf("[订单服务] 尝试优惠券 用户券ID=%d 应用前实付(分)=%d\n", *req.CouponID, order.ActualAmount) + appliedCouponVal = s.applyCouponWithCap(ctx.RequestContext(), userID, order, req.ActivityID, *req.CouponID) + fmt.Printf("[订单服务] 优惠后 实付(分)=%d 累计优惠(分)=%d\n", order.ActualAmount, order.DiscountAmount) + } + + // 4. 记录道具卡到备注 + if req.ItemCardID != nil && *req.ItemCardID > 0 { + order.Remark += fmt.Sprintf("|itemcard:%d", *req.ItemCardID) + } + + // 5. 保存订单 + if err := s.writeDB.Orders.WithContext(ctx.RequestContext()).Omit(s.writeDB.Orders.PaidAt, s.writeDB.Orders.CancelledAt).Create(order); err != nil { + return nil, err + } + + // 6. 记录优惠券使用明细 + if appliedCouponVal > 0 && req.CouponID != nil && *req.CouponID > 0 { + _ = s.user.RecordOrderCouponUsage(ctx.RequestContext(), order.ID, *req.CouponID, appliedCouponVal) + } + + // 7. 处理0元订单自动支付 + if order.ActualAmount == 0 { + now := time.Now() + _, _ = s.writeDB.Orders.WithContext(ctx.RequestContext()).Where(s.writeDB.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{ + s.writeDB.Orders.Status.ColumnName().String(): 2, + s.writeDB.Orders.PaidAt.ColumnName().String(): now, + }) + order.Status = 2 + + // 核销优惠券 + if req.CouponID != nil && *req.CouponID > 0 { + s.consumeCouponOnZeroPay(ctx.RequestContext(), userID, order.ID, *req.CouponID, appliedCouponVal, now) + } + } + + fmt.Printf("[订单服务] 订单创建完成 订单号=%s 总额(分)=%d 优惠(分)=%d 实付(分)=%d 状态=%d\n", + order.OrderNo, order.TotalAmount, order.DiscountAmount, order.ActualAmount, order.Status) + + return &CreateActivityOrderResult{ + Order: order, + AppliedCouponVal: appliedCouponVal, + }, nil +} + +// applyCouponWithCap 优惠券抵扣(含50%封顶与金额券部分使用) +func (s *activityOrderService) applyCouponWithCap(ctx context.Context, userID int64, order *model.Orders, activityID int64, userCouponID int64) int64 { + uc, _ := s.readDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID), s.readDB.UserCoupons.UserID.Eq(userID), s.readDB.UserCoupons.Status.Eq(1)).First() + if uc == nil { + return 0 + } + fmt.Printf("[优惠券] 用户券ID=%d 开始=%s 结束=%s\n", userCouponID, uc.ValidStart.Format(time.RFC3339), func() string { + if uc.ValidEnd.IsZero() { + return "无截止" + } + return uc.ValidEnd.Format(time.RFC3339) + }()) + sc, _ := s.readDB.SystemCoupons.WithContext(ctx).Where(s.readDB.SystemCoupons.ID.Eq(uc.CouponID), s.readDB.SystemCoupons.Status.Eq(1)).First() + now := time.Now() + if sc == nil { + fmt.Printf("[优惠券] 模板不存在 用户券ID=%d\n", userCouponID) + return 0 + } + if uc.ValidStart.After(now) { + fmt.Printf("[优惠券] 未到开始时间 用户券ID=%d\n", userCouponID) + return 0 + } + if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) { + fmt.Printf("[优惠券] 已过期 用户券ID=%d\n", userCouponID) + return 0 + } + scopeOK := (sc.ScopeType == 1) || (sc.ScopeType == 2 && sc.ActivityID == activityID) + if !scopeOK { + fmt.Printf("[优惠券] 范围不匹配 用户券ID=%d scope_type=%d\n", userCouponID, sc.ScopeType) + return 0 + } + if order.TotalAmount < sc.MinSpend { + fmt.Printf("[优惠券] 未达使用门槛 用户券ID=%d min_spend(分)=%d 订单总额(分)=%d\n", userCouponID, sc.MinSpend, order.TotalAmount) + return 0 + } + + // 50% 封顶 + cap := order.TotalAmount / 2 + remainingCap := cap - order.DiscountAmount + if remainingCap <= 0 { + fmt.Printf("[优惠券] 已达封顶\n") + return 0 + } + + applied := int64(0) + switch sc.DiscountType { + case 1: // 金额券 + var bal int64 + _ = s.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error + if bal <= 0 { + bal = sc.DiscountValue + } + if bal > 0 { + if bal > remainingCap { + applied = remainingCap + } else { + applied = bal + } + } + case 2: // 满减券 + applied = sc.DiscountValue + if applied > remainingCap { + applied = remainingCap + } + case 3: // 折扣券 + rate := sc.DiscountValue + if rate < 0 { + rate = 0 + } + if rate > 1000 { + rate = 1000 + } + newAmt := order.ActualAmount * rate / 1000 + d := order.ActualAmount - newAmt + if d > remainingCap { + applied = remainingCap + } else { + applied = d + } + } + + if applied > order.ActualAmount { + applied = order.ActualAmount + } + if applied <= 0 { + return 0 + } + + order.ActualAmount -= applied + order.DiscountAmount += applied + order.Remark += fmt.Sprintf("|c:%d:%d", userCouponID, applied) + fmt.Printf("[优惠券] 本次抵扣(分)=%d\n", applied) + + return applied +} + +// consumeCouponOnZeroPay 0元支付时核销优惠券 +func (s *activityOrderService) consumeCouponOnZeroPay(ctx context.Context, userID int64, orderID int64, userCouponID int64, applied int64, now time.Time) { + uc, _ := s.readDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID), s.readDB.UserCoupons.UserID.Eq(userID)).First() + if uc == nil { + return + } + sc, _ := s.readDB.SystemCoupons.WithContext(ctx).Where(s.readDB.SystemCoupons.ID.Eq(uc.CouponID)).First() + if sc == nil { + return + } + + if sc.DiscountType == 1 { // 金额券 - 部分扣减 + var bal int64 + _ = s.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error + nb := bal - applied + if nb < 0 { + nb = 0 + } + if nb == 0 { + _, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{ + "balance_amount": nb, + s.readDB.UserCoupons.Status.ColumnName().String(): 2, + s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID, + s.readDB.UserCoupons.UsedAt.ColumnName().String(): now, + }) + } else { + _, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{ + "balance_amount": nb, + s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID, + s.readDB.UserCoupons.UsedAt.ColumnName().String(): now, + }) + } + } else { // 满减/折扣券 - 直接核销 + _, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{ + s.readDB.UserCoupons.Status.ColumnName().String(): 2, + s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID, + s.readDB.UserCoupons.UsedAt.ColumnName().String(): now, + }) + } +} diff --git a/internal/service/activity/reward_effects_service.go b/internal/service/activity/reward_effects_service.go new file mode 100644 index 0000000..1c5a8d0 --- /dev/null +++ b/internal/service/activity/reward_effects_service.go @@ -0,0 +1,265 @@ +package activity + +import ( + "bindbox-game/internal/pkg/logger" + "bindbox-game/internal/repository/mysql" + "bindbox-game/internal/repository/mysql/dao" + "bindbox-game/internal/repository/mysql/model" + usersvc "bindbox-game/internal/service/user" + "context" + "crypto/rand" + "encoding/binary" + "fmt" + "time" +) + +// RewardEffectsService 奖励效果服务 +// 统一处理奖励发放和道具卡效果应用 +type RewardEffectsService interface { + // GrantRewardWithEffects 发放奖励并应用道具卡效果 + GrantRewardWithEffects(ctx context.Context, req GrantRewardRequest) (*GrantRewardResult, error) +} + +// GrantRewardRequest 奖励发放请求 +type GrantRewardRequest struct { + UserID int64 // 用户ID + OrderID int64 // 订单ID + ActivityID int64 // 活动ID + IssueID int64 // 期ID + Reward *model.ActivityRewardSettings // 要发放的奖励 + AllRewards []*model.ActivityRewardSettings // 所有可用奖励(用于概率提升升级) +} + +// GrantRewardResult 奖励发放结果 +type GrantRewardResult struct { + RewardID int64 // 发放的奖励ID + RewardName string // 奖励名称 + ItemCardApplied bool // 是否应用了道具卡效果 + UpgradedReward *model.ActivityRewardSettings // 如果概率提升成功,升级后的奖励 + DrawLogID int64 // 创建的抽奖日志ID +} + +type rewardEffectsService struct { + logger logger.CustomLogger + readDB *dao.Query + writeDB *dao.Query + repo mysql.Repo + user usersvc.Service +} + +// NewRewardEffectsService 创建奖励效果服务 +func NewRewardEffectsService(l logger.CustomLogger, db mysql.Repo) RewardEffectsService { + return &rewardEffectsService{ + logger: l, + readDB: dao.Use(db.GetDbR()), + writeDB: dao.Use(db.GetDbW()), + repo: db, + user: usersvc.New(l, db), + } +} + +// GrantRewardWithEffects 发放奖励并应用道具卡效果 +func (s *rewardEffectsService) GrantRewardWithEffects(ctx context.Context, req GrantRewardRequest) (*GrantRewardResult, error) { + if req.Reward == nil { + return nil, fmt.Errorf("reward is nil") + } + + result := &GrantRewardResult{ + RewardID: req.Reward.ID, + RewardName: req.Reward.Name, + } + + // 1. 扣减库存 + res, err := s.writeDB.ActivityRewardSettings.WithContext(ctx).Where( + s.writeDB.ActivityRewardSettings.ID.Eq(req.Reward.ID), + s.writeDB.ActivityRewardSettings.Quantity.Gt(0), + ).UpdateSimple(s.writeDB.ActivityRewardSettings.Quantity.Add(-1)) + if err != nil { + return nil, err + } + if res.RowsAffected == 0 { + return nil, fmt.Errorf("reward out of stock") + } + + // 2. 发放奖励到订单 + rid := req.Reward.ID + _, err = s.user.GrantRewardToOrder(ctx, req.UserID, usersvc.GrantRewardToOrderRequest{ + OrderID: req.OrderID, + ProductID: req.Reward.ProductID, + Quantity: 1, + ActivityID: &req.ActivityID, + RewardID: &rid, + Remark: req.Reward.Name, + }) + if err != nil { + return nil, err + } + + // 3. 创建抽奖日志 + drawLog := &model.ActivityDrawLogs{ + UserID: req.UserID, + IssueID: req.IssueID, + OrderID: req.OrderID, + RewardID: req.Reward.ID, + IsWinner: 1, + Level: req.Reward.Level, + CurrentLevel: 1, + CreatedAt: time.Now(), + } + if err := s.writeDB.ActivityDrawLogs.WithContext(ctx).Create(drawLog); err != nil { + return nil, err + } + result.DrawLogID = drawLog.ID + + // 4. 从订单备注解析道具卡ID并应用效果 + ord, _ := s.readDB.Orders.WithContext(ctx).Where(s.readDB.Orders.ID.Eq(req.OrderID)).First() + if ord != nil { + icID := parseItemCardIDFromRemark(ord.Remark) + if icID > 0 { + applied, upgradedReward := s.applyItemCardEffects(ctx, req, icID, drawLog.ID) + result.ItemCardApplied = applied + result.UpgradedReward = upgradedReward + } + } + + return result, nil +} + +// applyItemCardEffects 应用道具卡效果 +func (s *rewardEffectsService) applyItemCardEffects(ctx context.Context, req GrantRewardRequest, icID int64, drawLogID int64) (bool, *model.ActivityRewardSettings) { + fmt.Printf("[道具卡-RewardEffects] 从订单备注解析道具卡ID icID=%d\n", icID) + + uic, _ := s.readDB.UserItemCards.WithContext(ctx).Where( + s.readDB.UserItemCards.ID.Eq(icID), + s.readDB.UserItemCards.UserID.Eq(req.UserID), + s.readDB.UserItemCards.Status.Eq(1), + ).First() + if uic == nil { + fmt.Printf("[道具卡-RewardEffects] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", req.UserID, icID) + return false, nil + } + + ic, _ := s.readDB.SystemItemCards.WithContext(ctx).Where( + s.readDB.SystemItemCards.ID.Eq(uic.CardID), + s.readDB.SystemItemCards.Status.Eq(1), + ).First() + if ic == nil { + fmt.Printf("[道具卡-RewardEffects] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID) + return false, nil + } + + now := time.Now() + if uic.ValidStart.After(now) || uic.ValidEnd.Before(now) { + fmt.Printf("[道具卡-RewardEffects] ❌ 道具卡不在有效期\n") + return false, nil + } + + // 范围检查 + scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == req.ActivityID) || (ic.ScopeType == 4 && ic.IssueID == req.IssueID) + if !scopeOK { + fmt.Printf("[道具卡-RewardEffects] ❌ 范围检查失败 ScopeType=%d\n", ic.ScopeType) + return false, nil + } + + var upgradedReward *model.ActivityRewardSettings + + // 应用效果 + if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 { + // 双倍奖励 + fmt.Printf("[道具卡-RewardEffects] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s\n", ic.RewardMultiplierX1000, req.Reward.ID, req.Reward.Name) + rid := req.Reward.ID + _, _ = s.user.GrantRewardToOrder(ctx, req.UserID, usersvc.GrantRewardToOrderRequest{ + OrderID: req.OrderID, + ProductID: req.Reward.ProductID, + Quantity: 1, + ActivityID: &req.ActivityID, + RewardID: &rid, + Remark: req.Reward.Name + "(倍数)", + }) + } else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 { + // 概率提升 - 尝试升级到更好的奖励 + fmt.Printf("[道具卡-RewardEffects] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000) + var better *model.ActivityRewardSettings + for _, r := range req.AllRewards { + if r.MinScore > req.Reward.MinScore && r.Quantity > 0 { + if better == nil || r.MinScore < better.MinScore { + better = r + } + } + } + if better != nil { + randBytes := make([]byte, 4) + rand.Read(randBytes) + randVal := int32(binary.BigEndian.Uint32(randBytes) % 1000) + if randVal < ic.BoostRateX1000 { + fmt.Printf("[道具卡-RewardEffects] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", better.ID, better.Name) + rid := better.ID + _, _ = s.user.GrantRewardToOrder(ctx, req.UserID, usersvc.GrantRewardToOrderRequest{ + OrderID: req.OrderID, + ProductID: better.ProductID, + Quantity: 1, + ActivityID: &req.ActivityID, + RewardID: &rid, + Remark: better.Name + "(升级)", + }) + upgradedReward = better + } + } + } + + // 核销道具卡 + fmt.Printf("[道具卡-RewardEffects] 核销道具卡 用户道具卡ID=%d\n", icID) + _, _ = s.writeDB.UserItemCards.WithContext(ctx).Where( + s.writeDB.UserItemCards.ID.Eq(icID), + s.writeDB.UserItemCards.UserID.Eq(req.UserID), + s.writeDB.UserItemCards.Status.Eq(1), + ).Updates(map[string]any{ + s.writeDB.UserItemCards.Status.ColumnName().String(): 2, + s.writeDB.UserItemCards.UsedDrawLogID.ColumnName().String(): drawLogID, + s.writeDB.UserItemCards.UsedActivityID.ColumnName().String(): req.ActivityID, + s.writeDB.UserItemCards.UsedIssueID.ColumnName().String(): req.IssueID, + s.writeDB.UserItemCards.UsedAt.ColumnName().String(): now, + }) + + return true, upgradedReward +} + +// parseItemCardIDFromRemark 从订单备注解析道具卡ID +func parseItemCardIDFromRemark(remark string) int64 { + if remark == "" { + return 0 + } + // 查找 |itemcard:xxx 模式 + prefix := "|itemcard:" + idx := -1 + for i := 0; i <= len(remark)-len(prefix); i++ { + if remark[i:i+len(prefix)] == prefix { + idx = i + len(prefix) + break + } + } + if idx < 0 { + // 也检查开头没有 | 的情况 + prefix = "itemcard:" + for i := 0; i <= len(remark)-len(prefix); i++ { + if remark[i:i+len(prefix)] == prefix { + idx = i + len(prefix) + break + } + } + } + if idx < 0 { + return 0 + } + + var n int64 + for idx < len(remark) { + c := remark[idx] + if c < '0' || c > '9' { + break + } + n = n*10 + int64(c-'0') + idx++ + } + return n +} diff --git a/internal/service/user/orders_list.go b/internal/service/user/orders_list.go index e768e7b..14fddc0 100644 --- a/internal/service/user/orders_list.go +++ b/internal/service/user/orders_list.go @@ -34,6 +34,10 @@ type OrderWithItems struct { *model.Orders Items []*model.OrderItems `json:"items"` ActivityName string `json:"activity_name"` + ActivityID int64 `json:"activity_id,omitempty"` + PlayType string `json:"play_type,omitempty"` + CategoryID int64 `json:"category_id,omitempty"` + CategoryName string `json:"category_name,omitempty"` IssueNumber string `json:"issue_number"` IsDraw bool `json:"is_draw"` IsWinner bool `json:"is_winner"` @@ -133,6 +137,15 @@ func (s *service) GetOrderWithItems(ctx context.Context, userID int64, orderID i act, _ := s.readDB.Activities.WithContext(ctx).ReadDB().Where(s.readDB.Activities.ID.Eq(issue.ActivityID)).First() if act != nil { res.ActivityName = act.Name + res.ActivityID = act.ID + res.PlayType = act.PlayType + res.CategoryID = act.ActivityCategoryID + if act.ActivityCategoryID > 0 { + cat, _ := s.readDB.ActivityCategories.WithContext(ctx).ReadDB().Where(s.readDB.ActivityCategories.ID.Eq(act.ActivityCategoryID)).First() + if cat != nil { + res.CategoryName = cat.Name + } + } } } @@ -184,12 +197,15 @@ func (s *service) GetOrderWithItems(ctx context.Context, userID int64, orderID i } // ListOrdersWithItems 查询用户的订单列表,包含订单项详情 -func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status int32, page, pageSize int) (items []*OrderWithItems, total int64, err error) { +func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status int32, isConsumed *int32, page, pageSize int) (items []*OrderWithItems, total int64, err error) { // 查询用户的所有订单,包括商城直购(1)、抽奖票据(2)和系统发放(3) q := s.readDB.Orders.WithContext(ctx).ReadDB().Where(s.readDB.Orders.UserID.Eq(userID)) if status > 0 { q = q.Where(s.readDB.Orders.Status.Eq(status)) } + if isConsumed != nil { + q = q.Where(s.readDB.Orders.IsConsumed.Eq(*isConsumed)) + } total, err = q.Count() if err != nil { return nil, 0, err @@ -293,6 +309,42 @@ func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status } } } + + // 为没有开奖记录的抽奖订单补充查询活动信息 + var extraActIDs []int64 + for _, order := range orders { + if order.SourceType == 2 { + if _, hasLogs := drawLogsListMap[order.ID]; !hasLogs { + actID := parseActivityIDFromRemark(order.Remark) + if actID > 0 { + if _, exists := activityMap[actID]; !exists { + extraActIDs = append(extraActIDs, actID) + } + } + } + } + } + if len(extraActIDs) > 0 { + extraActs, _ := s.readDB.Activities.WithContext(ctx).ReadDB().Where(s.readDB.Activities.ID.In(extraActIDs...)).Find() + for _, act := range extraActs { + activityMap[act.ID] = act + } + } + } + + // 批量查询活动分类信息 + categoryMap := make(map[int64]*model.ActivityCategories) + var categoryIDs []int64 + for _, act := range activityMap { + if act.ActivityCategoryID > 0 { + categoryIDs = append(categoryIDs, act.ActivityCategoryID) + } + } + if len(categoryIDs) > 0 { + categories, _ := s.readDB.ActivityCategories.WithContext(ctx).ReadDB().Where(s.readDB.ActivityCategories.ID.In(categoryIDs...)).Find() + for _, cat := range categories { + categoryMap[cat.ID] = cat + } } // 批量查询抽奖凭据 @@ -322,6 +374,12 @@ func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status items[i].IssueNumber = issue.IssueNumber if act, ok := activityMap[issue.ActivityID]; ok { items[i].ActivityName = act.Name + items[i].ActivityID = act.ID + items[i].PlayType = act.PlayType + items[i].CategoryID = act.ActivityCategoryID + if cat, ok := categoryMap[act.ActivityCategoryID]; ok { + items[i].CategoryName = cat.Name + } } } @@ -350,8 +408,45 @@ func (s *service) ListOrdersWithItems(ctx context.Context, userID int64, status }) } } + } else if order.SourceType == 2 { + // 抽奖订单但没有开奖记录(如对对碰支付后还没玩),从备注解析活动信息 + actID := parseActivityIDFromRemark(order.Remark) + if actID > 0 { + if act, ok := activityMap[actID]; ok { + items[i].ActivityName = act.Name + items[i].ActivityID = act.ID + items[i].PlayType = act.PlayType + items[i].CategoryID = act.ActivityCategoryID + if cat, ok := categoryMap[act.ActivityCategoryID]; ok { + items[i].CategoryName = cat.Name + } + } + } } } return items, total, nil } + +// parseActivityIDFromRemark 从订单备注解析活动ID +func parseActivityIDFromRemark(remark string) int64 { + if remark == "" { + return 0 + } + parts := strings.Split(remark, "|") + for _, p := range parts { + if strings.HasPrefix(p, "lottery:activity:") { + idStr := p[len("lottery:activity:"):] + var n int64 + for i := 0; i < len(idStr); i++ { + c := idStr[i] + if c < '0' || c > '9' { + break + } + n = n*10 + int64(c-'0') + } + return n + } + } + return 0 +} diff --git a/internal/service/user/user.go b/internal/service/user/user.go index d69b5c0..3129a2c 100644 --- a/internal/service/user/user.go +++ b/internal/service/user/user.go @@ -12,7 +12,7 @@ import ( type Service interface { UpdateProfile(ctx context.Context, userID int64, nickname *string, avatar *string) (*model.Users, error) ListOrders(ctx context.Context, userID int64, page, pageSize int) (items []*model.Orders, total int64, err error) - ListOrdersWithItems(ctx context.Context, userID int64, status int32, page, pageSize int) (items []*OrderWithItems, total int64, err error) + ListOrdersWithItems(ctx context.Context, userID int64, status int32, isConsumed *int32, page, pageSize int) (items []*OrderWithItems, total int64, err error) ListInventoryWithProduct(ctx context.Context, userID int64, page, pageSize int) (items []*InventoryWithProduct, total int64, err error) ListInventoryWithProductActive(ctx context.Context, userID int64, page, pageSize int) (items []*InventoryWithProduct, total int64, err error) ListCoupons(ctx context.Context, userID int64, page, pageSize int) (items []*model.UserCoupons, total int64, err error) diff --git a/migrations/matching_game_test_data.sql b/migrations/matching_game_test_data.sql new file mode 100644 index 0000000..e57137b --- /dev/null +++ b/migrations/matching_game_test_data.sql @@ -0,0 +1,61 @@ +-- 对对碰奖励测试数据 +-- 前提:需要先有一个活动和期次 +-- 假设期次ID为 ? (请根据实际情况替换) + +-- 查看现有期次 +-- SELECT id, activity_id, issue_number FROM activity_issues ORDER BY id DESC LIMIT 10; + +-- 设置期次ID (请替换为实际的期次ID) +SET @issue_id = 1; + +-- 清理该期次下的旧奖励数据(谨慎使用) +-- DELETE FROM activity_reward_settings WHERE issue_id = @issue_id; + +-- 插入对对碰奖励测试数据 +-- 每个 min_score 对应一个奖品,精确匹配 +INSERT INTO activity_reward_settings +(issue_id, product_id, name, weight, quantity, original_qty, level, sort, is_boss, min_score, created_at, updated_at) +VALUES +-- 0-10对:参与奖 +(@issue_id, 0, '参与奖-5对', 0, 100, 100, 10, 1, 0, 5, NOW(), NOW()), +(@issue_id, 0, '参与奖-10对', 0, 100, 100, 9, 2, 0, 10, NOW(), NOW()), + +-- 10-20对:铜奖 +(@issue_id, 0, '铜奖-15对', 0, 50, 50, 8, 3, 0, 15, NOW(), NOW()), +(@issue_id, 0, '铜奖-20对', 0, 50, 50, 7, 4, 0, 20, NOW(), NOW()), + +-- 20-30对:银奖 +(@issue_id, 0, '银奖-25对', 0, 30, 30, 6, 5, 0, 25, NOW(), NOW()), +(@issue_id, 0, '银奖-30对', 0, 30, 30, 5, 6, 0, 30, NOW(), NOW()), + +-- 30-40对:金奖 +(@issue_id, 0, '金奖-35对', 0, 20, 20, 4, 7, 0, 35, NOW(), NOW()), +(@issue_id, 0, '金奖-40对', 0, 20, 20, 3, 8, 0, 40, NOW(), NOW()), + +-- 40-45对:特等奖 +(@issue_id, 0, '特等奖-42对', 0, 10, 10, 2, 9, 0, 42, NOW(), NOW()), +(@issue_id, 0, '特等奖-44对', 0, 5, 5, 1, 10, 0, 44, NOW(), NOW()), + +-- 45对以上:大奖(理论最大45-49对) +(@issue_id, 0, '终极大奖-45对', 0, 3, 3, 0, 11, 1, 45, NOW(), NOW()); + +-- 验证插入的数据 +SELECT id, issue_id, name, min_score, quantity, level, sort +FROM activity_reward_settings +WHERE issue_id = @issue_id +ORDER BY min_score ASC; + +-- ================================================================ +-- 使用说明: +-- 1. 将 @issue_id 替换为实际用于对对碰测试的期次ID +-- 2. 执行此脚本插入测试数据 +-- 3. 通过APP或接口测试对对碰游戏 +-- 4. 验证不同对子数是否匹配到正确的奖品 +-- +-- 测试用例: +-- - 消除5对 → 获得 "参与奖-5对" +-- - 消除10对 → 获得 "参与奖-10对" +-- - 消除15对 → 获得 "铜奖-15对" +-- - 消除12对 → 无匹配(因为是精确匹配) +-- - 消除45对 → 获得 "终极大奖-45对" +-- ================================================================ diff --git a/scripts/check_api_json_tags.go b/scripts/check_api_json_tags.go deleted file mode 100644 index cc93c19..0000000 --- a/scripts/check_api_json_tags.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import "fmt" - -func CheckAPIJSONTags() { - tasks := doRequest("GET", "/api/admin/task-center/tasks", nil) - list, ok := tasks["list"].([]interface{}) - if !ok || len(list) == 0 { - fmt.Println("No tasks found") - return - } - firstTask, ok := list[0].(map[string]any) - if !ok { - fmt.Println("Invalid task list format") - return - } - taskID := int64(firstTask["id"].(float64)) - fmt.Printf("Checking Task ID: %d\n", taskID) - - fmt.Println("\n--- Checking Tiers API ---") - tiersRaw := doRequestRaw("GET", fmt.Sprintf("/api/admin/task-center/tasks/%d/tiers", taskID), nil) - fmt.Println(string(tiersRaw)) - - fmt.Println("\n--- Checking Rewards API ---") - rewardsRaw := doRequestRaw("GET", fmt.Sprintf("/api/admin/task-center/tasks/%d/rewards", taskID), nil) - fmt.Println(string(rewardsRaw)) -} diff --git a/scripts/matching_game_demo.py b/scripts/matching_game_demo.py deleted file mode 100644 index a710588..0000000 --- a/scripts/matching_game_demo.py +++ /dev/null @@ -1,169 +0,0 @@ -import os -import hmac -import hashlib -import time -from typing import List, Dict, Any, Tuple -from collections import Counter - -class SecureBlindBoxRNG: - """使用 HMAC-SHA256 的加密安全随机数生成器""" - - def __init__(self): - self.server_seed = os.urandom(32) - self.server_seed_hash = hashlib.sha256(self.server_seed).hexdigest() - self.nonce = 0 - - def _generate_random_bytes(self, context: str) -> bytes: - self.nonce += 1 - message = f"{context}|nonce:{self.nonce}".encode() - return hmac.new(self.server_seed, message, hashlib.sha256).digest() - - def secure_randint(self, max_value: int, context: str = "rand") -> int: - if max_value <= 0: - return 0 - rand_bytes = self._generate_random_bytes(context) - return int.from_bytes(rand_bytes[:8], 'big') % max_value - - def secure_shuffle(self, items: List) -> List: - items = items[:] - n = len(items) - for i in range(n - 1, 0, -1): - j = self.secure_randint(i + 1, f"shuffle:{i}") - items[i], items[j] = items[j], items[i] - return items - - -class MatchingGame: - """对对碰游戏 - 可视化版本""" - - def __init__(self): - self.rng = SecureBlindBoxRNG() - - # 11种卡牌,每种9张 - self.card_types = ['🍎', '🍊', '🍋', '🍇', '🍓', '🍒', '🥝', '🍑', '🍌', '🍉', '🫐'] - self.card_list = [card for card in self.card_types for _ in range(9)] - - # 安全洗牌 - self.card_list = self.rng.secure_shuffle(self.card_list) - - self.hand = self.card_list[:9] - self.deck = self.card_list[9:] - self.total_pairs = 0 - self.round = 0 - - def display_hand(self): - """显示当前手牌""" - print(f"\n{'='*50}") - print(f" 第 {self.round} 轮 | 手牌: {len(self.hand)}张 | 牌堆: {len(self.deck)}张") - print(f"{'='*50}") - print(f" 手牌: {' '.join(self.hand)}") - - # 统计每种牌的数量 - counter = Counter(self.hand) - pairs_info = [] - for card, count in counter.items(): - if count >= 2: - pairs_info.append(f"{card}×{count}") - - if pairs_info: - print(f" 可配对: {', '.join(pairs_info)}") - else: - print(f" 可配对: 无") - - def find_and_remove_pairs(self) -> Tuple[int, List[str]]: - """找出并移除配对的牌""" - counter = Counter(self.hand) - pairs_found = 0 - pairs_detail = [] - remaining = [] - - for card, count in counter.items(): - pairs = count // 2 - if pairs > 0: - pairs_found += pairs - pairs_detail.append(f"{card}×{pairs*2}") - - # 剩余的单张 - if count % 2 == 1: - remaining.append(card) - - return pairs_found, remaining, pairs_detail - - def play_round(self, is_first: bool = False): - """执行一轮游戏""" - self.round += 1 - - # 第一轮:如果有🍎,额外抽牌 - if is_first: - apple_count = self.hand.count('🍎') - if apple_count > 0: - print(f"\n 🎁 发现 {apple_count} 张🍎,额外抽取 {apple_count} 张牌!") - extra = self.deck[:apple_count] - self.hand.extend(extra) - self.deck = self.deck[apple_count:] - print(f" 抽到: {' '.join(extra)}") - - self.display_hand() - - # 配对 - pairs, remaining, pairs_detail = self.find_and_remove_pairs() - - if pairs > 0: - print(f"\n ✨ 配对成功: {', '.join(pairs_detail)}") - print(f" 本轮配对: {pairs} 对") - self.total_pairs += pairs - - # 抽取新牌 - draw_count = min(pairs, len(self.deck)) - if draw_count > 0: - new_cards = self.deck[:draw_count] - self.deck = self.deck[draw_count:] - remaining.extend(new_cards) - print(f" 从牌堆抽取: {' '.join(new_cards)}") - - self.hand = remaining - return True - else: - print(f"\n ❌ 无法配对,游戏结束") - self.hand = remaining - return False - - def play(self): - """完整游戏流程""" - print("\n" + "🎮" * 20) - print(" 欢迎来到 对对碰 游戏!") - print("🎮" * 20) - print(f"\n游戏规则:") - print(f" 1. 初始手牌 9 张") - print(f" 2. 相同的牌可以配对消除") - print(f" 3. 每配对一次,从牌堆抽取相应数量的新牌") - print(f" 4. 直到无法配对为止") - print(f"\n种子哈希: {self.rng.server_seed_hash[:16]}...") - - # 第一轮 - can_continue = self.play_round(is_first=True) - - # 后续轮次 - while can_continue: - input("\n 按回车继续...") - can_continue = self.play_round(is_first=False) - - # 游戏结束 - print(f"\n{'='*50}") - print(f" 🏆 游戏结束!") - print(f" 总配对数: {self.total_pairs} 对") - print(f" 剩余手牌: {len(self.hand)} 张 {' '.join(self.hand)}") - print(f" 剩余牌堆: {len(self.deck)} 张") - print(f"{'='*50}") - - return { - "total_pairs": self.total_pairs, - "remaining_hand": self.hand, - "remaining_deck_count": len(self.deck), - "seed_hash": self.rng.server_seed_hash - } - - -if __name__ == "__main__": - game = MatchingGame() - result = game.play() diff --git a/scripts/matching_simulation.py b/scripts/matching_simulation.py deleted file mode 100644 index 612a156..0000000 --- a/scripts/matching_simulation.py +++ /dev/null @@ -1,124 +0,0 @@ -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() diff --git a/scripts/setup_full_tasks.go b/scripts/setup_full_tasks.go deleted file mode 100644 index dec7995..0000000 --- a/scripts/setup_full_tasks.go +++ /dev/null @@ -1,203 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "os" -) - -const ( - BaseURL = "http://localhost:9991" - Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJTdXBlciIsIm5pY2tuYW1lIjoiU3VwZXIiLCJpc19zdXBlciI6MSwicGxhdGZvcm0iOiLnrqHnkIbnq68iLCJleHAiOjE3NjU2NDIwMDYsIm5iZiI6MTc2NTM4MjgwNiwiaWF0IjoxNzY1MzgyODA2fQ.1pYRmSmFd-b4PudiYhGXgCv-LBr49_MMfNdyuc_7LXA" -) - -func main() { - if len(os.Args) > 1 && os.Args[1] == "check-json" { - CheckAPIJSONTags() - return - } - - fmt.Println("Starting to setup complete task center...") - - // 1. 清理现有任务 - tasks := doRequest("GET", "/api/admin/task-center/tasks", nil) - list := tasks["list"].([]interface{}) - for _, t := range list { - task := t.(map[string]any) - id := int64(task["id"].(float64)) - fmt.Printf("Deleting existing task: %d - %s\n", id, task["name"]) - doRequest("DELETE", fmt.Sprintf("/api/admin/task-center/tasks/%d", id), nil) - } - - // --------------------------------------------------------- - // 任务 1: 每日订单挑战 (Daily Order Challenge) - // --------------------------------------------------------- - t1ID := createTask("每日订单挑战", "每日完成订单挑战,赢取积分与道具", 1, 1) - upsertTiers(t1ID, []map[string]any{ - {"metric": "order_count", "operator": ">=", "threshold": 1, "window": "daily", "repeatable": 1, "priority": 1}, - {"metric": "order_count", "operator": ">=", "threshold": 10, "window": "daily", "repeatable": 1, "priority": 2}, - {"metric": "order_count", "operator": ">=", "threshold": 100, "window": "daily", "repeatable": 1, "priority": 3}, - }) - // 获取 Tier IDs 并配置奖励 - t1Tiers := listTiers(t1ID) - var r1 []map[string]any - for _, t := range t1Tiers { - p := int(t["Priority"].(float64)) - tid := int64(t["ID"].(float64)) - if p == 1 { // 1单 -> 1元(100积分) - r1 = append(r1, map[string]any{"tier_id": tid, "reward_type": "points", "reward_payload": map[string]int{"points": 100}, "quantity": 1}) - } else if p == 2 { // 10单 -> 优惠券ID 1 - r1 = append(r1, map[string]any{"tier_id": tid, "reward_type": "coupon", "reward_payload": map[string]int{"coupon_id": 1}, "quantity": 1}) - } else if p == 3 { // 100单 -> 道具卡ID 1 - r1 = append(r1, map[string]any{"tier_id": tid, "reward_type": "item_card", "reward_payload": map[string]int{"card_id": 1, "quantity": 1}, "quantity": 1}) - } - } - upsertRewards(t1ID, r1) - fmt.Printf("Created Task [Daily Order]: %d\n", t1ID) - - // --------------------------------------------------------- - // 任务 2: 每日邀请挑战 (Daily Invite Challenge) - // --------------------------------------------------------- - t2ID := createTask("每日邀请挑战", "每日邀请好友,奖励领不停", 1, 1) - upsertTiers(t2ID, []map[string]any{ - {"metric": "invite_count", "operator": ">=", "threshold": 1, "window": "daily", "repeatable": 1, "priority": 1}, - {"metric": "invite_count", "operator": ">=", "threshold": 10, "window": "daily", "repeatable": 1, "priority": 2}, - {"metric": "invite_count", "operator": ">=", "threshold": 100, "window": "daily", "repeatable": 1, "priority": 3}, - }) - t2Tiers := listTiers(t2ID) - var r2 []map[string]any - for _, t := range t2Tiers { - p := int(t["Priority"].(float64)) - tid := int64(t["ID"].(float64)) - if p == 1 { // 1人 -> 1元(100积分) - r2 = append(r2, map[string]any{"tier_id": tid, "reward_type": "points", "reward_payload": map[string]int{"points": 100}, "quantity": 1}) - } else if p == 2 { // 10人 -> 优惠券ID 1 - r2 = append(r2, map[string]any{"tier_id": tid, "reward_type": "coupon", "reward_payload": map[string]int{"coupon_id": 1}, "quantity": 1}) - } else if p == 3 { // 100人 -> 道具卡ID 1 - r2 = append(r2, map[string]any{"tier_id": tid, "reward_type": "item_card", "reward_payload": map[string]int{"card_id": 1, "quantity": 1}, "quantity": 1}) - } - } - upsertRewards(t2ID, r2) - fmt.Printf("Created Task [Daily Invite]: %d\n", t2ID) - - // --------------------------------------------------------- - // 任务 3: 新人首单福利 (Newbie First Order) - Lifetime - // --------------------------------------------------------- - t3ID := createTask("新人首单福利", "完成首次订单,获得新人专属奖励", 1, 1) - upsertTiers(t3ID, []map[string]any{ - {"metric": "first_order", "operator": "==", "threshold": 1, "window": "lifetime", "repeatable": 0, "priority": 1}, - }) - t3Tiers := listTiers(t3ID) - var r3 []map[string]any - for _, t := range t3Tiers { - // 首单奖励 500 积分 - r3 = append(r3, map[string]any{"tier_id": int64(t["ID"].(float64)), "reward_type": "points", "reward_payload": map[string]int{"points": 500}, "quantity": 1}) - } - upsertRewards(t3ID, r3) - fmt.Printf("Created Task [Newbie First Order]: %d\n", t3ID) - - // --------------------------------------------------------- - // 任务 4: 邀请达人 (Master Inviter) - Lifetime Cumulative - // --------------------------------------------------------- - t4ID := createTask("邀请达人挑战", "累计邀请好友,解锁达人成就", 1, 1) - upsertTiers(t4ID, []map[string]any{ - {"metric": "invite_count", "operator": ">=", "threshold": 50, "window": "lifetime", "repeatable": 0, "priority": 1}, - {"metric": "invite_count", "operator": ">=", "threshold": 200, "window": "lifetime", "repeatable": 0, "priority": 2}, - {"metric": "invite_count", "operator": ">=", "threshold": 500, "window": "lifetime", "repeatable": 0, "priority": 3}, - }) - t4Tiers := listTiers(t4ID) - var r4 []map[string]any - for _, t := range t4Tiers { - p := int(t["Priority"].(float64)) - tid := int64(t["ID"].(float64)) - if p == 1 { // 50人 -> 道具卡 ID 1 x2 - r4 = append(r4, map[string]any{"tier_id": tid, "reward_type": "item_card", "reward_payload": map[string]int{"card_id": 1, "quantity": 2}, "quantity": 2}) - } else if p == 2 { // 200人 -> 优惠券 ID 1 x5 - r4 = append(r4, map[string]any{"tier_id": tid, "reward_type": "coupon", "reward_payload": map[string]int{"coupon_id": 1}, "quantity": 5}) - } else if p == 3 { // 500人 -> 10000 积分 - r4 = append(r4, map[string]any{"tier_id": tid, "reward_type": "points", "reward_payload": map[string]int{"points": 10000}, "quantity": 1}) - } - } - upsertRewards(t4ID, r4) - fmt.Printf("Created Task [Master Inviter]: %d\n", t4ID) - - fmt.Println("All tasks setup completed!") -} - -func doRequest(method, path string, body any) map[string]any { - var bodyReader io.Reader - if body != nil { - b, _ := json.Marshal(body) - bodyReader = bytes.NewReader(b) - } - req, _ := http.NewRequest(method, BaseURL+path, bodyReader) - req.Header.Set("Authorization", Token) - req.Header.Set("Content-Type", "application/json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - panic(err) - } - defer resp.Body.Close() - - b, _ := io.ReadAll(resp.Body) - if resp.StatusCode >= 400 { - panic(fmt.Sprintf("Status: %d, Body: %s", resp.StatusCode, string(b))) - } - - var res map[string]any - json.Unmarshal(b, &res) - return res -} - -func doRequestRaw(method, path string, body any) []byte { - var bodyReader io.Reader - if body != nil { - b, _ := json.Marshal(body) - bodyReader = bytes.NewReader(b) - } - req, _ := http.NewRequest(method, BaseURL+path, bodyReader) - req.Header.Set("Authorization", Token) - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - panic(err) - } - defer resp.Body.Close() - b, _ := io.ReadAll(resp.Body) - return b -} - -func createTask(name, desc string, status, visibility int) int64 { - res := doRequest("POST", "/api/admin/task-center/tasks", map[string]any{ - "name": name, "description": desc, "status": status, "visibility": visibility, - }) - return int64(res["id"].(float64)) -} - -func upsertTiers(taskID int64, tiers []map[string]any) { - doRequest("POST", fmt.Sprintf("/api/admin/task-center/tasks/%d/tiers", taskID), map[string]any{ - "tiers": tiers, - }) -} - -func listTiers(taskID int64) []map[string]any { - res := doRequest("GET", fmt.Sprintf("/api/admin/task-center/tasks/%d/tiers", taskID), nil) - list := res["list"].([]interface{}) - var ret []map[string]any - for _, v := range list { - ret = append(ret, v.(map[string]any)) - } - return ret -} - -func upsertRewards(taskID int64, rewards []map[string]any) { - doRequest("POST", fmt.Sprintf("/api/admin/task-center/tasks/%d/rewards", taskID), map[string]any{ - "rewards": rewards, - }) -}