feat: 新增订单列表筛选条件与活动信息展示
refactor(orders): 重构订单列表查询逻辑,支持按消耗状态筛选 feat(orders): 订单列表返回新增活动分类与玩法类型信息 fix(orders): 修复订单支付时间空指针问题 docs(swagger): 更新订单相关接口文档 test(matching): 添加对对碰奖励匹配测试用例 chore: 清理无用脚本文件
This commit is contained in:
parent
c8b04e2bc6
commit
16e2ede037
@ -1,31 +1,50 @@
|
||||
[mysql]
|
||||
[language]
|
||||
local = 'zh-cn'
|
||||
|
||||
[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"
|
||||
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"
|
||||
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"
|
||||
|
||||
|
||||
|
||||
565
docs/docs.go
565
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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -22,6 +22,8 @@ type handler struct {
|
||||
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 {
|
||||
@ -35,5 +37,7 @@ func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler
|
||||
repo: db,
|
||||
user: usersvc.New(logger, db),
|
||||
redis: rdb,
|
||||
activityOrder: activitysvc.NewActivityOrderService(logger, db),
|
||||
rewardEffects: activitysvc.NewRewardEffectsService(logger, db),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
couponID = req.CouponID
|
||||
}
|
||||
var itemCardID *int64
|
||||
if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
|
||||
itemCardID = req.ItemCardID
|
||||
}
|
||||
|
||||
if discount > order.ActualAmount {
|
||||
discount = order.ActualAmount
|
||||
}
|
||||
if discount > 0 {
|
||||
order.ActualAmount -= discount
|
||||
order.DiscountAmount = discount
|
||||
appliedCouponVal = discount
|
||||
// Record usage later
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 精确匹配:用户消除的对子数 == 奖品设置的 MinScore
|
||||
if int64(req.TotalPairs) == r.MinScore {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 ""
|
||||
@ -468,7 +479,12 @@ func (h *handler) GetPayOrderDetail() core.HandlerFunc {
|
||||
}{
|
||||
Status: order.Status,
|
||||
ActualAmount: order.ActualAmount,
|
||||
PaidAt: order.PaidAt.Format("2006-01-02 15:04:05"),
|
||||
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,
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"bindbox-game/internal/code"
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
|
||||
"github.com/tealeg/xlsx"
|
||||
)
|
||||
|
||||
@ -53,7 +54,9 @@ func (h *handler) ExportPayOrders() core.HandlerFunc {
|
||||
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 }
|
||||
if r > 0 {
|
||||
pointsRate = r
|
||||
}
|
||||
}
|
||||
file := xlsx.NewFile()
|
||||
sheet, _ := file.AddSheet("orders")
|
||||
@ -66,14 +69,26 @@ func (h *handler) ExportPayOrders() core.HandlerFunc {
|
||||
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 } }
|
||||
for _, lg := range leds {
|
||||
if lg.Points < 0 {
|
||||
consumePointsSum += -lg.Points
|
||||
}
|
||||
}
|
||||
pa := o.PointsAmount
|
||||
if pa == 0 && consumePointsSum > 0 { pa = consumePointsSum / pointsRate }
|
||||
if pa == 0 && consumePointsSum > 0 {
|
||||
pa = consumePointsSum / pointsRate
|
||||
}
|
||||
pu := int64(0)
|
||||
if consumePointsSum > 0 { pu = consumePointsSum } else if pa > 0 { pu = pa * pointsRate }
|
||||
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 }
|
||||
for _, oc := range ocs {
|
||||
couponApplied += oc.AppliedAmount
|
||||
}
|
||||
r := sheet.AddRow()
|
||||
r.AddCell().Value = o.OrderNo
|
||||
r.AddCell().SetInt64(o.UserID)
|
||||
@ -85,7 +100,11 @@ func (h *handler) ExportPayOrders() core.HandlerFunc {
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -15,6 +15,7 @@ type listOrdersRequest struct {
|
||||
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
|
||||
|
||||
@ -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 {
|
||||
|
||||
309
internal/service/activity/activity_order_service.go
Normal file
309
internal/service/activity/activity_order_service.go
Normal file
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
265
internal/service/activity/reward_effects_service.go
Normal file
265
internal/service/activity/reward_effects_service.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
File diff suppressed because one or more lines are too long
61
migrations/matching_game_test_data.sql
Normal file
61
migrations/matching_game_test_data.sql
Normal file
@ -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对"
|
||||
-- ================================================================
|
||||
@ -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))
|
||||
}
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user