From bf11f32edff2b4c9167ac97f7cac87f3248b9325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=96=B9=E6=88=90?= Date: Sun, 21 Dec 2025 23:45:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B4=BB=E5=8A=A8):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=8A=BD=E5=A5=96=E8=AE=B0=E5=BD=95=E7=AD=89=E7=BA=A7=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(抽奖记录): 重构抽奖记录列表接口,支持按等级筛选 新增用户昵称、头像及奖品名称、图片等展示字段 优化分页逻辑,默认返回最新100条记录 feat(游戏): 添加扫雷游戏验证和结算接口 新增游戏票据验证和结算相关接口定义及Swagger文档 docs(API): 更新Swagger文档 更新抽奖记录和游戏相关接口的文档描述 style(路由): 添加游戏路由注释 添加扫雷游戏接口路由的占位注释 --- .DS_Store | Bin 10244 -> 10244 bytes build/.DS_Store | Bin 6148 -> 6148 bytes build/resources/.DS_Store | Bin 6148 -> 6148 bytes build/resources/admin/index.html | 2 +- docs/docs.go | 157 +++++++++++++++-- docs/swagger.json | 157 +++++++++++++++-- docs/swagger.yaml | 104 ++++++++++-- internal/api/activity/draw_logs_app.go | 160 ++++++++++++++++-- internal/api/app/categories.go | 9 + internal/api/app/notice.go | 8 + internal/api/app/store.go | 13 ++ internal/api/internal/minesweeper/handler.go | 119 +++++++++++++ internal/api/task_center/tasks_app.go | 6 +- .../api/user/points_redeem_item_card_app.go | 12 ++ internal/router/router.go | 9 + internal/service/activity/activity.go | 4 +- internal/service/activity/draw_logs_list.go | 7 +- 17 files changed, 703 insertions(+), 64 deletions(-) create mode 100644 internal/api/internal/minesweeper/handler.go diff --git a/.DS_Store b/.DS_Store index e05aa09939a8e2b958a95db0018e31877627e517..b3c81eead66a1837db6554a0b1892ec2feac3b2b 100644 GIT binary patch delta 98 zcmZn(XbG6$¥U^hRb!ekzS2a|Ke-6tChO6hYlBr%jSWHRJ1q%i0)Q~_B9o;mr+ wNjdpR3=9kc3=B-|3=E8V|G|KPVY8QD20KXe=AROG*fz5({9@UBUWAz$0G;|D7XSbN delta 75 zcmZn(XbG6$&nUk!U^hRb{A3=12OLHg#ySc{MuwB)MC~UV3rcOS7ffYG5#6jUd53Lc VgVAPogIVLkv07>`;H2?qr diff --git a/build/resources/admin/index.html b/build/resources/admin/index.html index ab48353..aca2404 100644 --- a/build/resources/admin/index.html +++ b/build/resources/admin/index.html @@ -38,7 +38,7 @@ } })() - + diff --git a/docs/docs.go b/docs/docs.go index 26a7133..58f6528 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3451,7 +3451,7 @@ const docTemplate = `{ }, "/api/app/activities/{activity_id}/issues/{issue_id}/draw_logs": { "get": { - "description": "查看指定活动期数的抽奖记录,支持分页", + "description": "查看指定活动期数的抽奖记录,支持等级筛选(默认返回最新的100条,不支持自定义翻页)", "consumes": [ "application/json" ], @@ -3479,19 +3479,9 @@ const docTemplate = `{ }, { "type": "integer", - "default": 1, - "description": "页码", - "name": "page", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "description": "每页数量,最多100", - "name": "page_size", - "in": "query", - "required": true + "description": "奖品等级过滤", + "name": "level", + "in": "query" } ], "responses": { @@ -5905,6 +5895,74 @@ const docTemplate = `{ } } }, + "/internal/game/settle": { + "post": { + "description": "游戏结束后结算并发放奖励", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal" + ], + "summary": "结算游戏", + "parameters": [ + { + "description": "结算信息", + "name": "result", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/minesweeper.SettleGameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/minesweeper.SettleGameResponse" + } + } + } + } + }, + "/internal/game/verify": { + "post": { + "description": "验证游戏票据是否有效", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal" + ], + "summary": "验证游戏票据", + "parameters": [ + { + "description": "票据信息", + "name": "ticket", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/minesweeper.VerifyTicketRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/minesweeper.VerifyTicketResponse" + } + } + } + } + }, "/pay/wechat/notify": { "post": { "description": "接收微信支付结果通知,验证签名并处理订单状态", @@ -8174,6 +8232,9 @@ const docTemplate = `{ "app.drawLogItem": { "type": "object", "properties": { + "avatar": { + "type": "string" + }, "current_level": { "type": "integer" }, @@ -8195,8 +8256,17 @@ const docTemplate = `{ "reward_id": { "type": "integer" }, + "reward_image": { + "type": "string" + }, + "reward_name": { + "type": "string" + }, "user_id": { "type": "integer" + }, + "user_name": { + "type": "string" } } }, @@ -9161,6 +9231,65 @@ const docTemplate = `{ } } }, + "minesweeper.SettleGameRequest": { + "type": "object", + "properties": { + "match_id": { + "type": "string" + }, + "metadata": { + "type": "string" + }, + "score": { + "type": "integer" + }, + "ticket": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "win": { + "type": "boolean" + } + } + }, + "minesweeper.SettleGameResponse": { + "type": "object", + "properties": { + "reward": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "minesweeper.VerifyTicketRequest": { + "type": "object", + "properties": { + "ticket": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "minesweeper.VerifyTicketResponse": { + "type": "object", + "properties": { + "remaining_times": { + "type": "integer" + }, + "user_id": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, "model.ActivityDrawEffects": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 888b115..2d0aacd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3443,7 +3443,7 @@ }, "/api/app/activities/{activity_id}/issues/{issue_id}/draw_logs": { "get": { - "description": "查看指定活动期数的抽奖记录,支持分页", + "description": "查看指定活动期数的抽奖记录,支持等级筛选(默认返回最新的100条,不支持自定义翻页)", "consumes": [ "application/json" ], @@ -3471,19 +3471,9 @@ }, { "type": "integer", - "default": 1, - "description": "页码", - "name": "page", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "description": "每页数量,最多100", - "name": "page_size", - "in": "query", - "required": true + "description": "奖品等级过滤", + "name": "level", + "in": "query" } ], "responses": { @@ -5897,6 +5887,74 @@ } } }, + "/internal/game/settle": { + "post": { + "description": "游戏结束后结算并发放奖励", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal" + ], + "summary": "结算游戏", + "parameters": [ + { + "description": "结算信息", + "name": "result", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/minesweeper.SettleGameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/minesweeper.SettleGameResponse" + } + } + } + } + }, + "/internal/game/verify": { + "post": { + "description": "验证游戏票据是否有效", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal" + ], + "summary": "验证游戏票据", + "parameters": [ + { + "description": "票据信息", + "name": "ticket", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/minesweeper.VerifyTicketRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/minesweeper.VerifyTicketResponse" + } + } + } + } + }, "/pay/wechat/notify": { "post": { "description": "接收微信支付结果通知,验证签名并处理订单状态", @@ -8166,6 +8224,9 @@ "app.drawLogItem": { "type": "object", "properties": { + "avatar": { + "type": "string" + }, "current_level": { "type": "integer" }, @@ -8187,8 +8248,17 @@ "reward_id": { "type": "integer" }, + "reward_image": { + "type": "string" + }, + "reward_name": { + "type": "string" + }, "user_id": { "type": "integer" + }, + "user_name": { + "type": "string" } } }, @@ -9153,6 +9223,65 @@ } } }, + "minesweeper.SettleGameRequest": { + "type": "object", + "properties": { + "match_id": { + "type": "string" + }, + "metadata": { + "type": "string" + }, + "score": { + "type": "integer" + }, + "ticket": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "win": { + "type": "boolean" + } + } + }, + "minesweeper.SettleGameResponse": { + "type": "object", + "properties": { + "reward": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "minesweeper.VerifyTicketRequest": { + "type": "object", + "properties": { + "ticket": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "minesweeper.VerifyTicketResponse": { + "type": "object", + "properties": { + "remaining_times": { + "type": "integer" + }, + "user_id": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, "model.ActivityDrawEffects": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8201170..b98af41 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1475,6 +1475,8 @@ definitions: type: object app.drawLogItem: properties: + avatar: + type: string current_level: type: integer id: @@ -1489,8 +1491,14 @@ definitions: type: integer reward_id: type: integer + reward_image: + type: string + reward_name: + type: string user_id: type: integer + user_name: + type: string type: object app.getAppProductDetailResponse: properties: @@ -2118,6 +2126,44 @@ definitions: description: 描述信息 type: string type: object + minesweeper.SettleGameRequest: + properties: + match_id: + type: string + metadata: + type: string + score: + type: integer + ticket: + type: string + user_id: + type: string + win: + type: boolean + type: object + minesweeper.SettleGameResponse: + properties: + reward: + type: string + success: + type: boolean + type: object + minesweeper.VerifyTicketRequest: + properties: + ticket: + type: string + user_id: + type: string + type: object + minesweeper.VerifyTicketResponse: + properties: + remaining_times: + type: integer + user_id: + type: string + valid: + type: boolean + type: object model.ActivityDrawEffects: properties: activity_category_id: @@ -4973,7 +5019,7 @@ paths: get: consumes: - application/json - description: 查看指定活动期数的抽奖记录,支持分页 + description: 查看指定活动期数的抽奖记录,支持等级筛选(默认返回最新的100条,不支持自定义翻页) parameters: - description: 活动ID in: path @@ -4985,17 +5031,9 @@ paths: name: issue_id required: true type: integer - - default: 1 - description: 页码 + - description: 奖品等级过滤 in: query - name: page - required: true - type: integer - - default: 20 - description: 每页数量,最多100 - in: query - name: page_size - required: true + name: level type: integer produces: - application/json @@ -6550,6 +6588,50 @@ paths: summary: WangEditor图片上传 tags: - Common + /internal/game/settle: + post: + consumes: + - application/json + description: 游戏结束后结算并发放奖励 + parameters: + - description: 结算信息 + in: body + name: result + required: true + schema: + $ref: '#/definitions/minesweeper.SettleGameRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/minesweeper.SettleGameResponse' + summary: 结算游戏 + tags: + - Internal + /internal/game/verify: + post: + consumes: + - application/json + description: 验证游戏票据是否有效 + parameters: + - description: 票据信息 + in: body + name: ticket + required: true + schema: + $ref: '#/definitions/minesweeper.VerifyTicketRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/minesweeper.VerifyTicketResponse' + summary: 验证游戏票据 + tags: + - Internal /pay/wechat/notify: post: consumes: diff --git a/internal/api/activity/draw_logs_app.go b/internal/api/activity/draw_logs_app.go index 99395fb..cc8b2ee 100644 --- a/internal/api/activity/draw_logs_app.go +++ b/internal/api/activity/draw_logs_app.go @@ -1,6 +1,7 @@ package app import ( + "encoding/json" "net/http" "strconv" "time" @@ -8,22 +9,28 @@ import ( "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" + "bindbox-game/internal/repository/mysql/model" ) type listDrawLogsRequest struct { - Page int `form:"page"` - PageSize int `form:"page_size"` + Page int `form:"page"` + PageSize int `form:"page_size"` + Level *int32 `form:"level"` } type drawLogItem struct { - ID int64 `json:"id"` - UserID int64 `json:"user_id"` - IssueID int64 `json:"issue_id"` - OrderID int64 `json:"order_id"` - RewardID int64 `json:"reward_id"` - IsWinner int32 `json:"is_winner"` - Level int32 `json:"level"` - CurrentLevel int32 `json:"current_level"` + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + UserName string `json:"user_name"` + Avatar string `json:"avatar"` + IssueID int64 `json:"issue_id"` + OrderID int64 `json:"order_id"` + RewardID int64 `json:"reward_id"` + RewardName string `json:"reward_name"` + RewardImage string `json:"reward_image"` + IsWinner int32 `json:"is_winner"` + Level int32 `json:"level"` + CurrentLevel int32 `json:"current_level"` } type listDrawLogsResponse struct { @@ -35,14 +42,13 @@ type listDrawLogsResponse struct { // ListDrawLogs 抽奖记录列表 // @Summary 抽奖记录列表 -// @Description 查看指定活动期数的抽奖记录,支持分页 +// @Description 查看指定活动期数的抽奖记录,支持等级筛选(默认返回最新的100条,不支持自定义翻页) // @Tags APP端.活动 // @Accept json // @Produce json // @Param activity_id path integer true "活动ID" // @Param issue_id path integer true "期ID" -// @Param page query int true "页码" default(1) -// @Param page_size query int true "每页数量,最多100" default(20) +// @Param level query int false "奖品等级过滤" // @Success 200 {object} listDrawLogsResponse // @Failure 400 {object} code.Failure // @Router /api/app/activities/{activity_id}/issues/{issue_id}/draw_logs [get] @@ -59,22 +65,142 @@ func (h *handler) ListDrawLogs() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID")) return } - items, total, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, req.Page, req.PageSize) + + // 强制固定分页:第一页,100条 + page := 1 + pageSize := 100 + + // 计算5分钟前的时间点 + fiveMinutesAgo := time.Now().Add(-5 * time.Minute) + + // 考虑到需要过滤掉5分钟内的数据,我们稍微多取一些数据,以确保过滤后能有足够的数据展示 + // 但为了防止数据量过大,还是做一个硬性限制,比如取前200条 + fetchSize := 200 + + items, _, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, 1, fetchSize, req.Level) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error())) return } - res.Page = req.Page - res.PageSize = req.PageSize + + // 收集ID用于批量查询 + var userIDs []int64 + var rewardIDs []int64 + userSet := make(map[int64]struct{}) + rewardSet := make(map[int64]struct{}) + + // 过滤并收集ID + var filteredItems []*model.ActivityDrawLogs + for _, v := range items { + // 过滤掉5分钟内的记录 + if v.CreatedAt.After(fiveMinutesAgo) { + continue + } + + // 如果已经收集够了100条,就不再收集了 + if len(filteredItems) >= pageSize { + break + } + + filteredItems = append(filteredItems, v) + + if v.UserID > 0 { + if _, ok := userSet[v.UserID]; !ok { + userSet[v.UserID] = struct{}{} + userIDs = append(userIDs, v.UserID) + } + } + if v.RewardID > 0 { + if _, ok := rewardSet[v.RewardID]; !ok { + rewardSet[v.RewardID] = struct{}{} + rewardIDs = append(rewardIDs, v.RewardID) + } + } + } + + // 更新items为过滤后的列表 + items = filteredItems + total := int64(len(items)) // 更新total为实际返回数量 + + // 批量查询用户信息 + userNameMap := make(map[int64]string) + userAvatarMap := make(map[int64]string) + if len(userIDs) > 0 { + users, err := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.ID.In(userIDs...)).Find() + if err == nil { + for _, u := range users { + userNameMap[u.ID] = u.Nickname + userAvatarMap[u.ID] = u.Avatar + } + } + } + + // 批量查询奖品与商品信息 + rewardNameMap := make(map[int64]string) + rewardImageMap := make(map[int64]string) + if len(rewardIDs) > 0 { + rewards, err := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.ActivityRewardSettings.ID.In(rewardIDs...)).Find() + if err == nil { + var productIDs []int64 + productSet := make(map[int64]struct{}) + rewardProductMap := make(map[int64]int64) + + for _, r := range rewards { + rewardNameMap[r.ID] = r.Name + if r.ProductID > 0 { + if _, ok := productSet[r.ProductID]; !ok { + productSet[r.ProductID] = struct{}{} + productIDs = append(productIDs, r.ProductID) + } + rewardProductMap[r.ID] = r.ProductID + } + } + + if len(productIDs) > 0 { + products, err := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.In(productIDs...)).Find() + if err == nil { + productImageMap := make(map[int64]string) + productNameMap := make(map[int64]string) + for _, p := range products { + first := "" + if p.ImagesJSON != "" { + var arr []string + _ = json.Unmarshal([]byte(p.ImagesJSON), &arr) + if len(arr) > 0 { + first = arr[0] + } + } + productImageMap[p.ID] = first + productNameMap[p.ID] = p.Name + } + // 填充奖品图片(如果奖品没有名字,也可以用商品名字兜底,但这里先只做图片) + for rid, pid := range rewardProductMap { + rewardImageMap[rid] = productImageMap[pid] + // 如果配置里的奖品名为空,尝试用商品名 + if rewardNameMap[rid] == "" { + rewardNameMap[rid] = productNameMap[pid] + } + } + } + } + } + } + + res.Page = page + res.PageSize = pageSize res.Total = total res.List = make([]drawLogItem, len(items)) for i, v := range items { res.List[i] = drawLogItem{ ID: v.ID, UserID: v.UserID, + UserName: userNameMap[v.UserID], + Avatar: userAvatarMap[v.UserID], IssueID: v.IssueID, OrderID: v.OrderID, RewardID: v.RewardID, + RewardName: rewardNameMap[v.RewardID], + RewardImage: rewardImageMap[v.RewardID], IsWinner: v.IsWinner, Level: v.Level, CurrentLevel: v.CurrentLevel, @@ -109,7 +235,7 @@ func (h *handler) ListDrawLogsByLevel() core.HandlerFunc { // 这里暂且获取所有(或者一个较大的限制),然后在内存中分组。 // 如果数据量巨大,需要由 Service 层提供 Group By 查询。 // 考虑到单期中奖人数通常有限(除非是大规模活动),先尝试获取列表后分组。 - logs, _, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, 1, 1000) // 假设最多显示1000条中奖记录 + logs, _, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, 1, 1000, nil) // 假设最多显示1000条中奖记录 if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error())) return diff --git a/internal/api/app/categories.go b/internal/api/app/categories.go index a572803..f9b62eb 100644 --- a/internal/api/app/categories.go +++ b/internal/api/app/categories.go @@ -29,6 +29,15 @@ type listAppCategoriesResponse struct { List []appCategoryItem `json:"list"` } +// ListCategoriesForApp 获取分类列表 +// @Summary 获取分类列表 +// @Description 获取APP端商品分类列表 +// @Tags APP端.基础 +// @Accept json +// @Produce json +// @Success 200 {object} listAppCategoriesResponse +// @Failure 400 {object} code.Failure +// @Router /api/app/categories [get] func (h *categoryHandler) ListCategoriesForApp() core.HandlerFunc { return func(ctx core.Context) { q := h.readDB.ProductCategories.WithContext(ctx.RequestContext()) diff --git a/internal/api/app/notice.go b/internal/api/app/notice.go index 66697c9..8442ea9 100644 --- a/internal/api/app/notice.go +++ b/internal/api/app/notice.go @@ -27,6 +27,14 @@ type listAppNoticesResponse struct { List []appNoticeItem `json:"list"` } +// ListNoticesForApp 获取公告列表 +// @Summary 获取公告列表 +// @Description 获取APP首页滚动公告 +// @Tags APP端.基础 +// @Accept json +// @Produce json +// @Success 200 {object} listAppNoticesResponse +// @Router /api/app/notices [get] func (h *noticeHandler) ListNoticesForApp() core.HandlerFunc { return func(ctx core.Context) { rsp := new(listAppNoticesResponse) diff --git a/internal/api/app/store.go b/internal/api/app/store.go index d40ff80..a3f3f57 100644 --- a/internal/api/app/store.go +++ b/internal/api/app/store.go @@ -46,6 +46,19 @@ type listStoreItemsResponse struct { List []listStoreItem `json:"list"` } +// ListStoreItemsForApp 获取积分商城商品列表 +// @Summary 获取积分商城商品列表 +// @Description 分页获取积分商城商品列表,支持按类型筛选(product/item_card/coupon) +// @Tags APP端.积分商城 +// @Accept x-www-form-urlencoded +// @Produce json +// @Security LoginVerifyToken +// @Param kind query string false "商品类型: product(默认), item_card, coupon" +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页数量" default(20) +// @Success 200 {object} listStoreItemsResponse +// @Failure 400 {object} code.Failure +// @Router /api/app/store/items [get] func (h *storeHandler) ListStoreItemsForApp() core.HandlerFunc { return func(ctx core.Context) { req := new(listStoreItemsRequest) diff --git a/internal/api/internal/minesweeper/handler.go b/internal/api/internal/minesweeper/handler.go new file mode 100644 index 0000000..3747bda --- /dev/null +++ b/internal/api/internal/minesweeper/handler.go @@ -0,0 +1,119 @@ +package minesweeper + +import ( + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/repository/mysql" + + "go.uber.org/zap" +) + +type Handler struct { + logger *zap.Logger + db mysql.Repo +} + +func New(logger *zap.Logger, db mysql.Repo) *Handler { + return &Handler{ + logger: logger, + db: db, + } +} + +// i() 辅助函数 +func (h *Handler) i() {} + +// VerifyTicket 验证游戏票据 +// @Summary 验证游戏票据 +// @Description 验证游戏票据是否有效 +// @Tags Internal +// @Accept json +// @Produce json +// @Param ticket body VerifyTicketRequest true "票据信息" +// @Success 200 {object} VerifyTicketResponse +// @Router /internal/game/verify [post] +func (h *Handler) VerifyTicket() core.HandlerFunc { + return func(c core.Context) { + req := new(VerifyTicketRequest) + if err := c.ShouldBindJSON(req); err != nil { + c.AbortWithError(core.Error( + 400, + 10001, + "参数错误", + )) + return + } + + // TODO: 实际验证逻辑,这里先Mock通过 + // 实际应该检查Redis中ticket是否存在且对应正确的user_id + + c.Payload(VerifyTicketResponse{ + Valid: true, + UserID: req.UserID, + RemainingTimes: 1, // Mock + }) + } +} + +// SettleGame 结算游戏 +// @Summary 结算游戏 +// @Description 游戏结束后结算并发放奖励 +// @Tags Internal +// @Accept json +// @Produce json +// @Param result body SettleGameRequest true "结算信息" +// @Success 200 {object} SettleGameResponse +// @Router /internal/game/settle [post] +func (h *Handler) SettleGame() core.HandlerFunc { + return func(c core.Context) { + req := new(SettleGameRequest) + if err := c.ShouldBindJSON(req); err != nil { + c.AbortWithError(core.Error( + 400, + 10001, + "参数错误", + )) + return + } + + // TODO: 实际结算逻辑 + // 1. 验证 ticket 状态为“进行中” + // 2. 如果 Win=true,发放奖励 + // 3. 标记 ticket 为“已完成” + + h.logger.Info("Game Settled", + zap.String("user_id", req.UserID), + zap.Bool("win", req.Win), + zap.Int("score", req.Score), + ) + + c.Payload(SettleGameResponse{ + Success: true, + Reward: "100积分", // Mock + }) + } +} + +type VerifyTicketRequest struct { + UserID string `json:"user_id"` + Ticket string `json:"ticket"` +} + +type VerifyTicketResponse struct { + Valid bool `json:"valid"` + UserID string `json:"user_id"` + RemainingTimes int `json:"remaining_times"` +} + +type SettleGameRequest struct { + UserID string `json:"user_id"` + Ticket string `json:"ticket"` + MatchID string `json:"match_id"` + Win bool `json:"win"` + Score int `json:"score"` + Metadata string `json:"metadata"` +} + +type SettleGameResponse struct { + Success bool `json:"success"` + Reward string `json:"reward"` +} diff --git a/internal/api/task_center/tasks_app.go b/internal/api/task_center/tasks_app.go index 6067ca0..25cde8c 100644 --- a/internal/api/task_center/tasks_app.go +++ b/internal/api/task_center/tasks_app.go @@ -58,7 +58,7 @@ type listTasksResponse struct { // @Param page query int false "页码" default(1) // @Param page_size query int false "每页数量" default(20) // @Success 200 {object} listTasksResponse "任务列表" -// @Router /app/task_center/tasks [get] +// @Router /api/app/task-center/tasks [get] func (h *handler) ListTasksForApp() core.HandlerFunc { return func(ctx core.Context) { req := new(listTasksRequest) @@ -115,7 +115,7 @@ type taskProgressResponse struct { // @Param id path int true "任务ID" // @Param user_id path int true "用户ID" // @Success 200 {object} taskProgressResponse "任务进度详情" -// @Router /app/task_center/tasks/{id}/progress/{user_id} [get] +// @Router /api/app/task-center/tasks/{id}/progress/{user_id} [get] func (h *handler) GetTaskProgressForApp() core.HandlerFunc { return func(ctx core.Context) { taskID, err := strconv.ParseInt(ctx.Param("id"), 10, 64) @@ -150,7 +150,7 @@ type claimTaskRequest struct { // @Param user_id path int true "用户ID" // @Param request body claimTaskRequest true "领取请求" // @Success 200 {object} map[string]any "领取成功" -// @Router /app/task_center/tasks/{id}/users/{user_id}/claim [post] +// @Router /api/app/task-center/tasks/{id}/claim/{user_id} [post] func (h *handler) ClaimTaskTierForApp() core.HandlerFunc { return func(ctx core.Context) { taskID, err := strconv.ParseInt(ctx.Param("id"), 10, 64) diff --git a/internal/api/user/points_redeem_item_card_app.go b/internal/api/user/points_redeem_item_card_app.go index 0aff562..3d2b285 100644 --- a/internal/api/user/points_redeem_item_card_app.go +++ b/internal/api/user/points_redeem_item_card_app.go @@ -19,6 +19,18 @@ type redeemItemCardResponse struct { Message string `json:"message"` } +// RedeemPointsToItemCard 积分兑换道具卡 +// @Summary 积分兑换道具卡 +// @Description 使用积分兑换指定数量的道具卡 +// @Tags APP端.用户 +// @Accept json +// @Produce json +// @Security LoginVerifyToken +// @Param user_id path int true "用户ID" +// @Param request body redeemItemCardRequest true "兑换请求参数" +// @Success 200 {object} redeemItemCardResponse +// @Failure 400 {object} code.Failure +// @Router /api/app/users/{user_id}/points/redeem-item-card [post] func (h *handler) RedeemPointsToItemCard() core.HandlerFunc { return func(ctx core.Context) { req := new(redeemItemCardRequest) diff --git a/internal/router/router.go b/internal/router/router.go index 4ac2adc..270461d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -54,8 +54,17 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) { userHandler := userapi.New(logger, db) commonHandler := commonapi.New(logger, db) payHandler := payapi.New(logger, db) + // minesweeperHandler := minesweeperapi.New(logger, db) intc := interceptor.New(logger, db) + // 内部服务接口路由组 (供 Nakama 调用) + // internalRouter := mux.Group("/internal") + // { + // TODO: 添加IP白名单或Internal-Key验证中间件 + // internalRouter.POST("/game/verify", minesweeperHandler.VerifyTicket()) + // internalRouter.POST("/game/settle", minesweeperHandler.SettleGame()) + // } + // 管理端非认证接口路由组 adminNonAuthApiRouter := mux.Group("/api/admin") { diff --git a/internal/service/activity/activity.go b/internal/service/activity/activity.go index 4da7c29..e544dc5 100644 --- a/internal/service/activity/activity.go +++ b/internal/service/activity/activity.go @@ -67,9 +67,9 @@ type Service interface { DeleteIssueReward(ctx context.Context, rewardID int64) error // ListDrawLogs 抽奖记录列表 - // 参数: issueID 期ID, page/pageSize 分页 + // 参数: issueID 期ID, page/pageSize 分页, level 等级过滤 // 返回: 抽奖记录集合、总数与错误 - ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int) (items []*model.ActivityDrawLogs, total int64, err error) + ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int, level *int32) (items []*model.ActivityDrawLogs, total int64, err error) // GetCategoryNames 批量查询分类名称 // 参数: ids 分类ID数组 diff --git a/internal/service/activity/draw_logs_list.go b/internal/service/activity/draw_logs_list.go index 98356e6..514faab 100644 --- a/internal/service/activity/draw_logs_list.go +++ b/internal/service/activity/draw_logs_list.go @@ -7,10 +7,13 @@ import ( ) // ListDrawLogs 查询抽奖记录列表(支持分页) -// 参数: issueID 期ID, page/pageSize 分页参数 +// 参数: issueID 期ID, page/pageSize 分页参数, level 等级过滤(可选) // 返回: 抽奖记录集合、总数与错误 -func (s *service) ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int) (items []*model.ActivityDrawLogs, total int64, err error) { +func (s *service) ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int, level *int32) (items []*model.ActivityDrawLogs, total int64, err error) { q := s.readDB.ActivityDrawLogs.WithContext(ctx).ReadDB().Where(s.readDB.ActivityDrawLogs.IssueID.Eq(issueID)) + if level != nil { + q = q.Where(s.readDB.ActivityDrawLogs.Level.Eq(*level)) + } total, err = q.Count() if err != nil { return nil, 0, err