diff --git a/.DS_Store b/.DS_Store index aac48b0..e4535d2 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/bindboxgame_api b/bindboxgame_api new file mode 100755 index 0000000..04eb924 Binary files /dev/null and b/bindboxgame_api differ diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 0000000..d4984d8 Binary files /dev/null and b/docs/.DS_Store differ diff --git a/docs/docs.go b/docs/docs.go index ed7eb82..aed7f48 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1978,6 +1978,30 @@ const docTemplate = `{ "name": "page_size", "in": "query", "required": true + }, + { + "type": "string", + "description": "用户昵称", + "name": "nickname", + "in": "query" + }, + { + "type": "string", + "description": "邀请码", + "name": "inviteCode", + "in": "query" + }, + { + "type": "string", + "description": "开始日期(YYYY-MM-DD)", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "description": "结束日期(YYYY-MM-DD)", + "name": "endDate", + "in": "query" } ], "responses": { @@ -3170,46 +3194,6 @@ const docTemplate = `{ } } }, - "/api/app/users/phone/login": { - "post": { - "description": "使用手机号登录,沿用 code 字段承载手机号或手机号授权码", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "APP端.用户" - ], - "summary": "手机号登录", - "parameters": [ - { - "description": "请求参数", - "name": "RequestBody", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/app.phoneLoginRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/app.phoneLoginResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/code.Failure" - } - } - } - } - }, "/api/app/users/weixin/login": { "post": { "description": "微信静默登录(需传递 code;可选 invite_code)", @@ -3252,6 +3236,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}": { "put": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "修改用户昵称与头像", "consumes": [ "application/json" @@ -3299,6 +3288,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}/coupons": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看用户持有的优惠券列表", "consumes": [ "application/json" @@ -3353,6 +3347,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}/invites": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看被该用户邀请的用户列表", "consumes": [ "application/json" @@ -3407,6 +3406,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}/item_cards": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "获取指定用户的道具卡列表,支持分页", "consumes": [ "application/json" @@ -3469,6 +3473,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}/item_cards/uses": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "获取指定用户的道具卡使用记录,支持分页", "consumes": [ "application/json" @@ -3531,6 +3540,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}/orders": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看用户抽奖来源订单记录", "consumes": [ "application/json" @@ -3585,6 +3599,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}/phone/bind": { "post": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "使用微信手机号 code 换取手机号并绑定到指定用户", "consumes": [ "application/json" @@ -3632,6 +3651,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}/points": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看用户积分流水记录", "consumes": [ "application/json" @@ -3686,6 +3710,11 @@ const docTemplate = `{ }, "/api/app/users/{user_id}/points/balance": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看用户积分余额(过滤过期积分)", "consumes": [ "application/json" @@ -5430,6 +5459,32 @@ const docTemplate = `{ } } }, + "app.couponItem": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rules": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "valid_end": { + "type": "string" + }, + "valid_start": { + "type": "string" + } + } + }, "app.drawLogItem": { "type": "object", "properties": { @@ -5573,7 +5628,7 @@ const docTemplate = `{ "list": { "type": "array", "items": { - "$ref": "#/definitions/model.UserCoupons" + "$ref": "#/definitions/app.couponItem" } }, "page": { @@ -5814,35 +5869,6 @@ const docTemplate = `{ } } }, - "app.phoneLoginRequest": { - "type": "object", - "properties": { - "code": { - "description": "兼容参数名:这里作为手机号或第三方手机号code", - "type": "string" - }, - "invite_code": { - "type": "string" - } - } - }, - "app.phoneLoginResponse": { - "type": "object", - "properties": { - "avatar": { - "type": "string" - }, - "invite_code": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "user_id": { - "type": "integer" - } - } - }, "app.pointsBalanceResponse": { "type": "object", "properties": { @@ -5931,6 +5957,9 @@ const docTemplate = `{ "nickname": { "type": "string" }, + "token": { + "type": "string" + }, "user_id": { "type": "integer" } @@ -6234,51 +6263,6 @@ const docTemplate = `{ } } }, - "model.UserCoupons": { - "type": "object", - "properties": { - "coupon_id": { - "description": "券模板ID(system_coupons.id)", - "type": "integer" - }, - "created_at": { - "description": "创建时间", - "type": "string" - }, - "id": { - "description": "主键ID", - "type": "integer" - }, - "status": { - "description": "状态:1未使用 2已使用 3已过期", - "type": "integer" - }, - "updated_at": { - "description": "更新时间", - "type": "string" - }, - "used_at": { - "description": "核销时间", - "type": "string" - }, - "used_order_id": { - "description": "核销的订单ID(orders.id)", - "type": "integer" - }, - "user_id": { - "description": "用户ID(user_members.id)", - "type": "integer" - }, - "valid_end": { - "description": "有效期结束", - "type": "string" - }, - "valid_start": { - "description": "有效期开始", - "type": "string" - } - } - }, "model.UserItemCards": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index b5ad90a..92dae1e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1970,6 +1970,30 @@ "name": "page_size", "in": "query", "required": true + }, + { + "type": "string", + "description": "用户昵称", + "name": "nickname", + "in": "query" + }, + { + "type": "string", + "description": "邀请码", + "name": "inviteCode", + "in": "query" + }, + { + "type": "string", + "description": "开始日期(YYYY-MM-DD)", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "description": "结束日期(YYYY-MM-DD)", + "name": "endDate", + "in": "query" } ], "responses": { @@ -3162,46 +3186,6 @@ } } }, - "/api/app/users/phone/login": { - "post": { - "description": "使用手机号登录,沿用 code 字段承载手机号或手机号授权码", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "APP端.用户" - ], - "summary": "手机号登录", - "parameters": [ - { - "description": "请求参数", - "name": "RequestBody", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/app.phoneLoginRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/app.phoneLoginResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/code.Failure" - } - } - } - } - }, "/api/app/users/weixin/login": { "post": { "description": "微信静默登录(需传递 code;可选 invite_code)", @@ -3244,6 +3228,11 @@ }, "/api/app/users/{user_id}": { "put": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "修改用户昵称与头像", "consumes": [ "application/json" @@ -3291,6 +3280,11 @@ }, "/api/app/users/{user_id}/coupons": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看用户持有的优惠券列表", "consumes": [ "application/json" @@ -3345,6 +3339,11 @@ }, "/api/app/users/{user_id}/invites": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看被该用户邀请的用户列表", "consumes": [ "application/json" @@ -3399,6 +3398,11 @@ }, "/api/app/users/{user_id}/item_cards": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "获取指定用户的道具卡列表,支持分页", "consumes": [ "application/json" @@ -3461,6 +3465,11 @@ }, "/api/app/users/{user_id}/item_cards/uses": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "获取指定用户的道具卡使用记录,支持分页", "consumes": [ "application/json" @@ -3523,6 +3532,11 @@ }, "/api/app/users/{user_id}/orders": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看用户抽奖来源订单记录", "consumes": [ "application/json" @@ -3577,6 +3591,11 @@ }, "/api/app/users/{user_id}/phone/bind": { "post": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "使用微信手机号 code 换取手机号并绑定到指定用户", "consumes": [ "application/json" @@ -3624,6 +3643,11 @@ }, "/api/app/users/{user_id}/points": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看用户积分流水记录", "consumes": [ "application/json" @@ -3678,6 +3702,11 @@ }, "/api/app/users/{user_id}/points/balance": { "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], "description": "查看用户积分余额(过滤过期积分)", "consumes": [ "application/json" @@ -5422,6 +5451,32 @@ } } }, + "app.couponItem": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "rules": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "valid_end": { + "type": "string" + }, + "valid_start": { + "type": "string" + } + } + }, "app.drawLogItem": { "type": "object", "properties": { @@ -5565,7 +5620,7 @@ "list": { "type": "array", "items": { - "$ref": "#/definitions/model.UserCoupons" + "$ref": "#/definitions/app.couponItem" } }, "page": { @@ -5806,35 +5861,6 @@ } } }, - "app.phoneLoginRequest": { - "type": "object", - "properties": { - "code": { - "description": "兼容参数名:这里作为手机号或第三方手机号code", - "type": "string" - }, - "invite_code": { - "type": "string" - } - } - }, - "app.phoneLoginResponse": { - "type": "object", - "properties": { - "avatar": { - "type": "string" - }, - "invite_code": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "user_id": { - "type": "integer" - } - } - }, "app.pointsBalanceResponse": { "type": "object", "properties": { @@ -5923,6 +5949,9 @@ "nickname": { "type": "string" }, + "token": { + "type": "string" + }, "user_id": { "type": "integer" } @@ -6226,51 +6255,6 @@ } } }, - "model.UserCoupons": { - "type": "object", - "properties": { - "coupon_id": { - "description": "券模板ID(system_coupons.id)", - "type": "integer" - }, - "created_at": { - "description": "创建时间", - "type": "string" - }, - "id": { - "description": "主键ID", - "type": "integer" - }, - "status": { - "description": "状态:1未使用 2已使用 3已过期", - "type": "integer" - }, - "updated_at": { - "description": "更新时间", - "type": "string" - }, - "used_at": { - "description": "核销时间", - "type": "string" - }, - "used_order_id": { - "description": "核销的订单ID(orders.id)", - "type": "integer" - }, - "user_id": { - "description": "用户ID(user_members.id)", - "type": "integer" - }, - "valid_end": { - "description": "有效期结束", - "type": "string" - }, - "valid_start": { - "description": "有效期开始", - "type": "string" - } - } - }, "model.UserItemCards": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 69fc36d..db417e1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1044,6 +1044,23 @@ definitions: success: type: boolean type: object + app.couponItem: + properties: + amount: + type: integer + id: + type: integer + name: + type: string + rules: + type: string + status: + type: integer + valid_end: + type: string + valid_start: + type: string + type: object app.drawLogItem: properties: current_level: @@ -1137,7 +1154,7 @@ definitions: properties: list: items: - $ref: '#/definitions/model.UserCoupons' + $ref: '#/definitions/app.couponItem' type: array page: type: integer @@ -1293,25 +1310,6 @@ definitions: user: $ref: '#/definitions/app.userItem' type: object - app.phoneLoginRequest: - properties: - code: - description: 兼容参数名:这里作为手机号或第三方手机号code - type: string - invite_code: - type: string - type: object - app.phoneLoginResponse: - properties: - avatar: - type: string - invite_code: - type: string - nickname: - type: string - user_id: - type: integer - type: object app.pointsBalanceResponse: properties: balance: @@ -1369,6 +1367,8 @@ definitions: type: string nickname: type: string + token: + type: string user_id: type: integer type: object @@ -1591,39 +1591,6 @@ definitions: description: 下单用户ID(user_members.id) type: integer type: object - model.UserCoupons: - properties: - coupon_id: - description: 券模板ID(system_coupons.id) - type: integer - created_at: - description: 创建时间 - type: string - id: - description: 主键ID - type: integer - status: - description: 状态:1未使用 2已使用 3已过期 - type: integer - updated_at: - description: 更新时间 - type: string - used_at: - description: 核销时间 - type: string - used_order_id: - description: 核销的订单ID(orders.id) - type: integer - user_id: - description: 用户ID(user_members.id) - type: integer - valid_end: - description: 有效期结束 - type: string - valid_start: - description: 有效期开始 - type: string - type: object model.UserItemCards: properties: card_id: @@ -3083,6 +3050,22 @@ paths: name: page_size required: true type: integer + - description: 用户昵称 + in: query + name: nickname + type: string + - description: 邀请码 + in: query + name: inviteCode + type: string + - description: 开始日期(YYYY-MM-DD) + in: query + name: startDate + type: string + - description: 结束日期(YYYY-MM-DD) + in: query + name: endDate + type: string produces: - application/json responses: @@ -3871,6 +3854,8 @@ paths: description: Bad Request schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 修改用户信息 tags: - APP端.用户 @@ -3908,6 +3893,8 @@ paths: description: Bad Request schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 查看用户优惠券 tags: - APP端.用户 @@ -3945,6 +3932,8 @@ paths: description: Bad Request schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 查看用户邀请记录 tags: - APP端.用户 @@ -3986,6 +3975,8 @@ paths: description: 服务器内部错误 schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 获取用户道具卡列表 tags: - APP端.用户 @@ -4027,6 +4018,8 @@ paths: description: 服务器内部错误 schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 获取用户道具卡使用记录 tags: - APP端.用户 @@ -4064,6 +4057,8 @@ paths: description: Bad Request schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 查看用户订单记录 tags: - APP端.用户 @@ -4095,6 +4090,8 @@ paths: description: Bad Request schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 绑定手机号 tags: - APP端.用户 @@ -4132,6 +4129,8 @@ paths: description: Bad Request schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 查看用户积分记录 tags: - APP端.用户 @@ -4157,35 +4156,11 @@ paths: description: Bad Request schema: $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] summary: 查看用户积分余额 tags: - APP端.用户 - /api/app/users/phone/login: - post: - consumes: - - application/json - description: 使用手机号登录,沿用 code 字段承载手机号或手机号授权码 - parameters: - - description: 请求参数 - in: body - name: RequestBody - required: true - schema: - $ref: '#/definitions/app.phoneLoginRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/app.phoneLoginResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/code.Failure' - summary: 手机号登录 - tags: - - APP端.用户 /api/app/users/weixin/login: post: consumes: diff --git a/docs/开发规范/APP端开发统一规范.md b/docs/开发规范/APP端开发统一规范.md index dd7ca23..528db44 100644 --- a/docs/开发规范/APP端开发统一规范.md +++ b/docs/开发规范/APP端开发统一规范.md @@ -21,6 +21,20 @@ - 所有接口必须定义 `Request` 与 `Response` 结构体 - 成功统一:`ctx.Payload(res)`,`res.Message` 使用统一文案 `操作成功` +### 4.1 认证与鉴权(强制) +- 所有“用户私有数据”接口必须携带请求头 `Authorization: `(微信登录返回的令牌),后端基于令牌解析当前用户。 +- 路径中的 `user_id` 为 REST 占位参数,后端不使用该值进行身份识别;实际的用户身份以令牌为准,禁止越权访问他人数据。 +- 未携带或令牌无效时返回 `401` 与业务码 `JWTAuthVerifyError`。 + +### 4.2 统一返回字段(示例:优惠券) +- 优惠券列表返回 `list` 中的元素为: + - `id`:持券记录ID + - `name`:优惠券名称 + - `amount`:优惠面值(分;折扣为千分比已转百分比文案) + - `valid_start`/`valid_end`:有效期(`yyyy-MM-dd HH:mm:ss`) + - `status`:状态(1未使用 2已使用 3已过期) + - `rules`:使用规则说明(直减/满减/折扣) + ## 5. 参数绑定与校验 - JSON:`ctx.ShouldBindJSON(req)` - 表单/查询:`ctx.ShouldBindForm(req)` @@ -44,6 +58,8 @@ ## 10. Swagger 注释规范 - 标签:APP 端统一使用 `@Tags APP端.活动`(或模块名) - 必填注释:`@Summary/@Description/@Accept/@Produce/@Param/@Success/@Failure/@Router` + - 安全注释:用户私有接口必须添加 `@Security LoginVerifyToken` + - `user_id` 参数文档需注明“占位,不参与鉴权,按令牌解析用户”,示例:`@Param user_id path integer true "用户ID(占位,按令牌解析)"` ## 11. 统一 Handler 模板(示例) 以下示例来自 `internal/api/activity/activity_issues_list.go:46-127`,作为 APP 端列表接口标准参考: @@ -143,4 +159,5 @@ func (h *handler) ListActivityIssues() core.HandlerFunc { ## 12. 文档生成与预览 - 生成:`make gen-swagger` - 预览:`make serve-swagger`(默认端口 `36666`,访问 `http://localhost:36666/docs`) + - 预览时在右上角 `Authorize` 设置 `Authorization` 值,接口将自动附带令牌进行校验 diff --git a/docs/需求文档.md b/docs/需求文档.md index 7f8278f..a65e3c9 100644 --- a/docs/需求文档.md +++ b/docs/需求文档.md @@ -148,3 +148,22 @@ 1.8 查看商品列表: /api/admin/products + + + +@6A 现在有以下问题需要修改 +1. 用户查看自己的优惠券 接口有问题: 'http://127.0.0.1:9991/api/app/users/12/coupons?page=1&page_size=20' 这里不应该用 user_id 来查询; 应该用 token 来查询; 因为用户可以查看自己的优惠券, 但是其他用户不能查看; 所以这里应该用 token 来查询; +2. 优惠券返回的信息中; 应该包含 优惠券的名称, 优惠券的金额, 优惠券的有效期, 优惠券的使用规则; +3. 用户邀请记录也是 接口有问题: 'http://127.0.0.1:9991/api/app/users/12/invites?page=1&page_size=20' 这里不应该用 user_id 来查询; 应该用 token 来查询; 因为用户可以查看自己的邀请记录, 但是其他用户不能查看; 所以这里应该用 token 来查询; +4. 其他的 接口都存在这个问题 修复一下 + + +仪表盘数据分析: + 1. 总访问次数 在线访客数 点击量 新用户 : 这一块我想改成:道具卡销量 / 活动抽奖次数 / 新用户注册数 用户总积分 + + + + 2. 用户概述: 想统计出 用户增长趋势 访问量: 改成活动抽奖量统计 + 3. 新用户:这里面可以放用户 积分 资产等数据 + 4. 动态: 显示实时抽奖数据 + 5. 代办事项: 显示用户需要完成的任务; 比如 绑定手机号 绑定邮箱 绑定工会 diff --git a/internal/api/admin/system_user.go b/internal/api/admin/system_user.go index 168d127..69e3c4d 100644 --- a/internal/api/admin/system_user.go +++ b/internal/api/admin/system_user.go @@ -76,11 +76,20 @@ func (h *handler) ListUsers() core.HandlerFunc { q = q.Where(h.readDB.Admin.Username.Like("%" + req.UserName + "%")) } if req.UserEmail != "" { + // 注意:Admin模型没有邮箱字段,这里用昵称字段来支持搜索 q = q.Where(h.readDB.Admin.Nickname.Like("%" + req.UserEmail + "%")) } if req.UserPhone != "" { q = q.Where(h.readDB.Admin.Mobile.Like("%" + req.UserPhone + "%")) } + if req.Status != "" { + // 状态筛选:1-正常 0-禁用 + if req.Status == "1" || req.Status == "正常" { + q = q.Where(h.readDB.Admin.LoginStatus.Eq(1)) + } else if req.Status == "0" || req.Status == "禁用" { + q = q.Where(h.readDB.Admin.LoginStatus.Eq(0)) + } + } total, err := q.Count() if err != nil { diff --git a/internal/api/admin/users_admin.go b/internal/api/admin/users_admin.go index 8682eee..7e4b218 100644 --- a/internal/api/admin/users_admin.go +++ b/internal/api/admin/users_admin.go @@ -13,8 +13,12 @@ import ( ) type listUsersRequest struct { - Page int `form:"page"` - PageSize int `form:"page_size"` + Page int `form:"page"` + PageSize int `form:"page_size"` + Nickname string `form:"nickname"` + InviteCode string `form:"inviteCode"` + StartDate string `form:"startDate"` + EndDate string `form:"endDate"` } type listUsersResponse struct { Page int `json:"page"` @@ -31,6 +35,10 @@ type listUsersResponse struct { // @Produce json // @Param page query int true "页码" default(1) // @Param page_size query int true "每页数量,最多100" default(20) +// @Param nickname query string false "用户昵称" +// @Param inviteCode query string false "邀请码" +// @Param startDate query string false "开始日期(YYYY-MM-DD)" +// @Param endDate query string false "结束日期(YYYY-MM-DD)" // @Success 200 {object} listUsersResponse // @Failure 400 {object} code.Failure // @Router /api/admin/users [get] @@ -53,6 +61,27 @@ func (h *handler) ListAppUsers() core.HandlerFunc { req.PageSize = 100 } q := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB() + + // 应用搜索条件 + if req.Nickname != "" { + q = q.Where(h.readDB.Users.Nickname.Like("%" + req.Nickname + "%")) + } + if req.InviteCode != "" { + q = q.Where(h.readDB.Users.InviteCode.Eq(req.InviteCode)) + } + if req.StartDate != "" { + if startTime, err := time.Parse("2006-01-02", req.StartDate); err == nil { + q = q.Where(h.readDB.Users.CreatedAt.Gte(startTime)) + } + } + if req.EndDate != "" { + if endTime, err := time.Parse("2006-01-02", req.EndDate); err == nil { + // 设置结束时间为当天的23:59:59 + endTime = endTime.Add(24 * time.Hour).Add(-time.Second) + q = q.Where(h.readDB.Users.CreatedAt.Lte(endTime)) + } + } + total, err := q.Count() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 20101, err.Error())) @@ -227,6 +256,57 @@ func (h *handler) ListUserInventory() core.HandlerFunc { } } +type listUserItemCardsRequest struct { + Page int `form:"page"` + PageSize int `form:"page_size"` +} + +type listUserItemCardsResponse struct { + Page int `json:"page"` + PageSize int `json:"page_size"` + Total int64 `json:"total"` + List []*user.ItemCardWithTemplate `json:"list"` +} + +// ListUserItemCards 查看用户道具卡列表 +// @Summary 查看用户道具卡列表 +// @Description 查看指定用户的道具卡持有记录 +// @Tags 管理端.用户 +// @Accept json +// @Produce json +// @Param user_id path integer true "用户ID" +// @Param page query int true "页码" default(1) +// @Param page_size query int true "每页数量,最多100" default(20) +// @Success 200 {object} listUserItemCardsResponse +// @Failure 400 {object} code.Failure +// @Router /api/admin/users/{user_id}/item_cards [get] +// @Security LoginVerifyToken +func (h *handler) ListUserItemCards() core.HandlerFunc { + return func(ctx core.Context) { + req := new(listUserItemCardsRequest) + rsp := new(listUserItemCardsResponse) + if err := ctx.ShouldBindForm(req); err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) + return + } + userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) + if err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) + return + } + items, total, err := h.user.ListUserItemCardsWithTemplate(ctx.RequestContext(), userID, req.Page, req.PageSize) + if err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, 20106, err.Error())) + return + } + rsp.Page = req.Page + rsp.PageSize = req.PageSize + rsp.Total = total + rsp.List = items + ctx.Payload(rsp) + } +} + type listCouponsRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` diff --git a/internal/api/user/coupons_app.go b/internal/api/user/coupons_app.go index 3207f30..979d3b7 100644 --- a/internal/api/user/coupons_app.go +++ b/internal/api/user/coupons_app.go @@ -1,13 +1,13 @@ package app import ( - "net/http" - "strconv" + "fmt" + "net/http" - "bindbox-game/internal/code" - "bindbox-game/internal/pkg/core" - "bindbox-game/internal/pkg/validation" - "bindbox-game/internal/repository/mysql/model" + "bindbox-game/internal/code" + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/validation" + "bindbox-game/internal/repository/mysql/model" ) type listCouponsRequest struct { @@ -15,10 +15,20 @@ type listCouponsRequest struct { PageSize int `form:"page_size"` } type listCouponsResponse struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - Total int64 `json:"total"` - List []*model.UserCoupons `json:"list"` + Page int `json:"page"` + PageSize int `json:"page_size"` + Total int64 `json:"total"` + List []couponItem `json:"list"` +} + +type couponItem struct { + ID int64 `json:"id"` + Name string `json:"name"` + Amount int64 `json:"amount"` + ValidStart string `json:"valid_start"` + ValidEnd string `json:"valid_end"` + Status int32 `json:"status"` + Rules string `json:"rules"` } // ListUserCoupons 查看用户优惠券 @@ -27,7 +37,8 @@ type listCouponsResponse struct { // @Tags APP端.用户 // @Accept json // @Produce json -// @Param user_id path integer true "用户ID" + // @Param user_id path integer true "用户ID" + // @Security LoginVerifyToken // @Param page query int true "页码" default(1) // @Param page_size query int true "每页数量,最多100" default(20) // @Success 200 {object} listCouponsResponse @@ -41,20 +52,70 @@ func (h *handler) ListUserCoupons() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } - userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) - return - } - items, total, err := h.user.ListCoupons(ctx.RequestContext(), userID, req.Page, req.PageSize) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error())) - return - } - rsp.Page = req.Page - rsp.PageSize = req.PageSize - rsp.Total = total - rsp.List = items - ctx.Payload(rsp) - } + userID := int64(ctx.SessionUserInfo().Id) + items, total, err := h.user.ListCoupons(ctx.RequestContext(), userID, req.Page, req.PageSize) + if err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error())) + return + } + rsp.Page = req.Page + rsp.PageSize = req.PageSize + rsp.Total = total + if len(items) == 0 { + rsp.List = []couponItem{} + ctx.Payload(rsp) + return + } + ids := make([]int64, 0, len(items)) + for _, it := range items { + ids = append(ids, it.CouponID) + } + rows, err := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.In(ids...)).Find() + if err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error())) + return + } + mp := make(map[int64]*model.SystemCoupons, len(rows)) + for i := range rows { + c := rows[i] + mp[c.ID] = c + } + rsp.List = make([]couponItem, 0, len(items)) + for _, it := range items { + sc := mp[it.CouponID] + name := "" + amount := int64(0) + rules := "" + if sc != nil { + name = sc.Name + amount = sc.DiscountValue + rules = buildCouponRules(sc) + } + vi := couponItem{ + ID: it.ID, + Name: name, + Amount: amount, + ValidStart: it.ValidStart.Format("2006-01-02 15:04:05"), + ValidEnd: it.ValidEnd.Format("2006-01-02 15:04:05"), + Status: it.Status, + Rules: rules, + } + rsp.List = append(rsp.List, vi) + } + ctx.Payload(rsp) + } +} + +func buildCouponRules(c *model.SystemCoupons) string { + switch c.DiscountType { + case 1: + return fmt.Sprintf("直减%v分,满%v分可用", c.DiscountValue, c.MinSpend) + case 2: + return fmt.Sprintf("满%v分减%v分", c.MinSpend, c.DiscountValue) + case 3: + p := float64(c.DiscountValue) / 10.0 + return fmt.Sprintf("折扣%0.1f%%,满%v分可用", p, c.MinSpend) + default: + return "" + } } diff --git a/internal/api/user/invites_app.go b/internal/api/user/invites_app.go index 0aefeb2..4c57e6c 100644 --- a/internal/api/user/invites_app.go +++ b/internal/api/user/invites_app.go @@ -1,12 +1,11 @@ package app import ( - "net/http" - "strconv" + "net/http" - "bindbox-game/internal/code" - "bindbox-game/internal/pkg/core" - "bindbox-game/internal/pkg/validation" + "bindbox-game/internal/code" + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/validation" ) type listInvitesRequest struct { @@ -32,7 +31,8 @@ type listInvitesResponse struct { // @Tags APP端.用户 // @Accept json // @Produce json -// @Param user_id path integer true "用户ID" + // @Param user_id path integer true "用户ID" + // @Security LoginVerifyToken // @Param page query int true "页码" default(1) // @Param page_size query int true "每页数量,最多100" default(20) // @Success 200 {object} listInvitesResponse @@ -46,12 +46,8 @@ func (h *handler) ListUserInvites() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } - userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) - return - } - items, total, err := h.user.ListInvites(ctx.RequestContext(), userID, req.Page, req.PageSize) + userID := int64(ctx.SessionUserInfo().Id) + items, total, err := h.user.ListInvites(ctx.RequestContext(), userID, req.Page, req.PageSize) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 10007, err.Error())) return diff --git a/internal/api/user/item_cards_app.go b/internal/api/user/item_cards_app.go index 8f48713..c8dac70 100644 --- a/internal/api/user/item_cards_app.go +++ b/internal/api/user/item_cards_app.go @@ -2,7 +2,6 @@ package app import ( "net/http" - "strconv" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" @@ -29,6 +28,7 @@ type listUserItemCardsResponse struct { // @Accept json // @Produce json // @Param user_id path integer true "用户ID" +// @Security LoginVerifyToken // @Param page query integer false "页码,默认1" // @Param page_size query integer false "每页条数,默认10" // @Success 200 {object} listUserItemCardsResponse @@ -44,11 +44,7 @@ func (h *handler) ListUserItemCards() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } - userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) - return - } + userID := int64(ctx.SessionUserInfo().Id) items, total, err := h.user.ListUserItemCards(ctx.RequestContext(), userID, req.Page, req.PageSize) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 10008, err.Error())) @@ -81,6 +77,7 @@ type listUserItemCardUsesResponse struct { // @Accept json // @Produce json // @Param user_id path integer true "用户ID" +// @Security LoginVerifyToken // @Param page query integer false "页码,默认1" // @Param page_size query integer false "每页条数,默认10" // @Success 200 {object} listUserItemCardUsesResponse @@ -96,11 +93,7 @@ func (h *handler) ListUserItemCardUses() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } - userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) - return - } + userID := int64(ctx.SessionUserInfo().Id) items, total, err := h.user.ListUserItemCardUses(ctx.RequestContext(), userID, req.Page, req.PageSize) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 10009, err.Error())) diff --git a/internal/api/user/orders_app.go b/internal/api/user/orders_app.go index 784257e..5d631e6 100644 --- a/internal/api/user/orders_app.go +++ b/internal/api/user/orders_app.go @@ -1,13 +1,12 @@ package app import ( - "net/http" - "strconv" + "net/http" - "bindbox-game/internal/code" - "bindbox-game/internal/pkg/core" - "bindbox-game/internal/pkg/validation" - "bindbox-game/internal/repository/mysql/model" + "bindbox-game/internal/code" + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/validation" + "bindbox-game/internal/repository/mysql/model" ) type listOrdersRequest struct { @@ -27,7 +26,8 @@ type listOrdersResponse struct { // @Tags APP端.用户 // @Accept json // @Produce json -// @Param user_id path integer true "用户ID" + // @Param user_id path integer true "用户ID" + // @Security LoginVerifyToken // @Param page query int true "页码" default(1) // @Param page_size query int true "每页数量,最多100" default(20) // @Success 200 {object} listOrdersResponse @@ -41,12 +41,8 @@ func (h *handler) ListUserOrders() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } - userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) - return - } - items, total, err := h.user.ListOrders(ctx.RequestContext(), userID, req.Page, req.PageSize) + userID := int64(ctx.SessionUserInfo().Id) + items, total, err := h.user.ListOrders(ctx.RequestContext(), userID, req.Page, req.PageSize) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 10002, err.Error())) return diff --git a/internal/api/user/phone_bind.go b/internal/api/user/phone_bind.go index 547990c..4e0ec21 100644 --- a/internal/api/user/phone_bind.go +++ b/internal/api/user/phone_bind.go @@ -1,15 +1,14 @@ package app import ( - "net/http" - "strconv" + "net/http" - "bindbox-game/configs" - "bindbox-game/internal/code" - "bindbox-game/internal/pkg/core" - "bindbox-game/internal/pkg/miniprogram" - "bindbox-game/internal/pkg/validation" - "bindbox-game/internal/pkg/wechat" + "bindbox-game/configs" + "bindbox-game/internal/code" + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/miniprogram" + "bindbox-game/internal/pkg/validation" + "bindbox-game/internal/pkg/wechat" ) type bindPhoneRequest struct { @@ -27,7 +26,8 @@ type bindPhoneResponse struct { // @Tags APP端.用户 // @Accept json // @Produce json -// @Param user_id path integer true "用户ID" + // @Param user_id path integer true "用户ID" + // @Security LoginVerifyToken // @Param RequestBody body bindPhoneRequest true "请求参数" // @Success 200 {object} bindPhoneResponse // @Failure 400 {object} code.Failure @@ -40,12 +40,11 @@ func (h *handler) BindPhone() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } - uidStr := ctx.Param("user_id") - userID, _ := strconv.ParseInt(uidStr, 10, 64) - if userID <= 0 || req.Code == "" { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "缺少必要参数")) - return - } + userID := int64(ctx.SessionUserInfo().Id) + if userID <= 0 || req.Code == "" { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "缺少必要参数")) + return + } cfg := configs.Get() var tokenRes struct { @@ -66,10 +65,10 @@ func (h *handler) BindPhone() core.HandlerFunc { return } - if _, err := h.writeDB.Users.WithContext(ctx.RequestContext()).Where(h.writeDB.Users.ID.Eq(userID)).Updates(map[string]any{"mobile": mobile}); err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error())) - return - } + if _, err := h.writeDB.Users.WithContext(ctx.RequestContext()).Where(h.writeDB.Users.ID.Eq(userID)).Updates(map[string]any{"mobile": mobile}); err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error())) + return + } rsp.Success = true rsp.Mobile = mobile ctx.Payload(rsp) diff --git a/internal/api/user/points_app.go b/internal/api/user/points_app.go index 0c0dfb4..6ae221f 100644 --- a/internal/api/user/points_app.go +++ b/internal/api/user/points_app.go @@ -1,13 +1,12 @@ package app import ( - "net/http" - "strconv" + "net/http" - "bindbox-game/internal/code" - "bindbox-game/internal/pkg/core" - "bindbox-game/internal/pkg/validation" - "bindbox-game/internal/repository/mysql/model" + "bindbox-game/internal/code" + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/validation" + "bindbox-game/internal/repository/mysql/model" ) type listPointsRequest struct { @@ -30,7 +29,8 @@ type pointsBalanceResponse struct { // @Tags APP端.用户 // @Accept json // @Produce json -// @Param user_id path integer true "用户ID" + // @Param user_id path integer true "用户ID" + // @Security LoginVerifyToken // @Param page query int true "页码" default(1) // @Param page_size query int true "每页数量,最多100" default(20) // @Success 200 {object} listPointsResponse @@ -44,12 +44,8 @@ func (h *handler) ListUserPoints() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } - userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) - return - } - items, total, err := h.user.ListPointsLedger(ctx.RequestContext(), userID, req.Page, req.PageSize) + userID := int64(ctx.SessionUserInfo().Id) + items, total, err := h.user.ListPointsLedger(ctx.RequestContext(), userID, req.Page, req.PageSize) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 10004, err.Error())) return @@ -68,19 +64,16 @@ func (h *handler) ListUserPoints() core.HandlerFunc { // @Tags APP端.用户 // @Accept json // @Produce json -// @Param user_id path integer true "用户ID" + // @Param user_id path integer true "用户ID" + // @Security LoginVerifyToken // @Success 200 {object} pointsBalanceResponse // @Failure 400 {object} code.Failure // @Router /api/app/users/{user_id}/points/balance [get] func (h *handler) GetUserPointsBalance() core.HandlerFunc { return func(ctx core.Context) { rsp := new(pointsBalanceResponse) - userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) - return - } - total, err := h.user.GetPointsBalance(ctx.RequestContext(), userID) + userID := int64(ctx.SessionUserInfo().Id) + total, err := h.user.GetPointsBalance(ctx.RequestContext(), userID) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 10005, err.Error())) return diff --git a/internal/api/user/profile_app.go b/internal/api/user/profile_app.go index 6802c30..1c6ba2b 100644 --- a/internal/api/user/profile_app.go +++ b/internal/api/user/profile_app.go @@ -1,12 +1,11 @@ package app import ( - "net/http" - "strconv" + "net/http" - "bindbox-game/internal/code" - "bindbox-game/internal/pkg/core" - "bindbox-game/internal/pkg/validation" + "bindbox-game/internal/code" + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/validation" ) type modifyUserRequest struct { @@ -30,7 +29,8 @@ type modifyUserResponse struct { // @Tags APP端.用户 // @Accept json // @Produce json -// @Param user_id path integer true "用户ID" + // @Param user_id path integer true "用户ID" + // @Security LoginVerifyToken // @Param RequestBody body modifyUserRequest true "请求参数" // @Success 200 {object} modifyUserResponse // @Failure 400 {object} code.Failure @@ -43,12 +43,8 @@ func (h *handler) ModifyUser() core.HandlerFunc { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } - userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) - if err != nil { - ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) - return - } - item, err := h.user.UpdateProfile(ctx.RequestContext(), userID, req.Nickname, req.Avatar) + userID := int64(ctx.SessionUserInfo().Id) + item, err := h.user.UpdateProfile(ctx.RequestContext(), userID, req.Nickname, req.Avatar) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, err.Error())) return diff --git a/internal/router/interceptor/app_auth.go b/internal/router/interceptor/app_auth.go new file mode 100644 index 0000000..74e1847 --- /dev/null +++ b/internal/router/interceptor/app_auth.go @@ -0,0 +1,36 @@ +package interceptor + +import ( + "net/http" + + "bindbox-game/configs" + "bindbox-game/internal/code" + "bindbox-game/internal/pkg/core" + "bindbox-game/internal/pkg/jwtoken" + "bindbox-game/internal/proposal" +) + +func (i *interceptor) AppTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError) { + headerAuthorizationString := ctx.GetHeader("Authorization") + if headerAuthorizationString == "" { + err = core.Error( + http.StatusUnauthorized, + code.JWTAuthVerifyError, + "无法确认您的身份,请进行登录。", + ) + return + } + + jwtClaims, jwtErr := jwtoken.New(configs.Get().JWT.PatientSecret).Parse(headerAuthorizationString) + if jwtErr != nil { + err = core.Error( + http.StatusUnauthorized, + code.JWTAuthVerifyError, + "您的账号登录过期,请重新登录。", + ) + return + } + + sessionUserInfo = jwtClaims.SessionUserInfo + return +} \ No newline at end of file diff --git a/internal/router/interceptor/interceptor.go b/internal/router/interceptor/interceptor.go index 125207e..7e5df6e 100644 --- a/internal/router/interceptor/interceptor.go +++ b/internal/router/interceptor/interceptor.go @@ -10,8 +10,11 @@ import ( var _ Interceptor = (*interceptor)(nil) type Interceptor interface { - // AdminTokenAuthVerify 管理端授权验证 - AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError) + // AdminTokenAuthVerify 管理端授权验证 + AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError) + + // AppTokenAuthVerify APP端授权验证 + AppTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError) // i 为了避免被其他包实现 i() diff --git a/internal/router/router.go b/internal/router/router.go index b0b1be6..41e6821 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -115,6 +115,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) { adminAuthApiRouter.POST("/users/:user_id/coupons/add", adminHandler.AddUserCoupon()) adminAuthApiRouter.POST("/users/:user_id/rewards/grant", adminHandler.GrantReward()) adminAuthApiRouter.GET("/users/:user_id/inventory", adminHandler.ListUserInventory()) + adminAuthApiRouter.GET("/users/:user_id/item_cards", adminHandler.ListUserItemCards()) // 道具卡管理 adminAuthApiRouter.POST("/system_item_cards", adminHandler.CreateSystemItemCard()) @@ -152,35 +153,41 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) { systemApiRouter.POST("/role/:role_id/actions", adminHandler.AssignRoleActions()) } - // APP 端接口路由组 - appApiRouter := mux.Group("/api/app") - { - appApiRouter.GET("/activities", activityHandler.ListActivities()) - appApiRouter.GET("/activities/:activity_id", activityHandler.GetActivityDetail()) - appApiRouter.GET("/activities/:activity_id/issues", activityHandler.ListActivityIssues()) - appApiRouter.GET("/activities/:activity_id/issues/:issue_id/rewards", activityHandler.ListIssueRewards()) - appApiRouter.GET("/activities/:activity_id/issues/:issue_id/draw_logs", activityHandler.ListDrawLogs()) + // APP 端公开接口路由组 + appPublicApiRouter := mux.Group("/api/app") + { + appPublicApiRouter.GET("/activities", activityHandler.ListActivities()) + appPublicApiRouter.GET("/activities/:activity_id", activityHandler.GetActivityDetail()) + appPublicApiRouter.GET("/activities/:activity_id/issues", activityHandler.ListActivityIssues()) + appPublicApiRouter.GET("/activities/:activity_id/issues/:issue_id/rewards", activityHandler.ListIssueRewards()) + appPublicApiRouter.GET("/activities/:activity_id/issues/:issue_id/draw_logs", activityHandler.ListDrawLogs()) - appApiRouter.GET("/guilds", guildHandler.ListGuilds()) - appApiRouter.GET("/guilds/:guild_id", guildHandler.GetGuildDetail()) - appApiRouter.POST("/guilds/:guild_id/members", guildHandler.JoinGuild()) - appApiRouter.DELETE("/guilds/:guild_id/members/:user_id", guildHandler.LeaveGuild()) - appApiRouter.GET("/guilds/:guild_id/members", guildHandler.ListGuildMembers()) + appPublicApiRouter.GET("/guilds", guildHandler.ListGuilds()) + appPublicApiRouter.GET("/guilds/:guild_id", guildHandler.GetGuildDetail()) + appPublicApiRouter.POST("/guilds/:guild_id/members", guildHandler.JoinGuild()) + appPublicApiRouter.DELETE("/guilds/:guild_id/members/:user_id", guildHandler.LeaveGuild()) + appPublicApiRouter.GET("/guilds/:guild_id/members", guildHandler.ListGuildMembers()) - // APP 端轮播图 - appApiRouter.GET("/banners", appapi.NewBanner(logger, db).ListBannersForApp()) + // APP 端轮播图 + appPublicApiRouter.GET("/banners", appapi.NewBanner(logger, db).ListBannersForApp()) - appApiRouter.PUT("/users/:user_id", userHandler.ModifyUser()) - appApiRouter.GET("/users/:user_id/orders", userHandler.ListUserOrders()) - appApiRouter.GET("/users/:user_id/coupons", userHandler.ListUserCoupons()) - appApiRouter.GET("/users/:user_id/points", userHandler.ListUserPoints()) - appApiRouter.GET("/users/:user_id/points/balance", userHandler.GetUserPointsBalance()) - appApiRouter.POST("/users/weixin/login", userHandler.WeixinLogin()) - appApiRouter.POST("/users/:user_id/phone/bind", userHandler.BindPhone()) - appApiRouter.GET("/users/:user_id/invites", userHandler.ListUserInvites()) - appApiRouter.GET("/users/:user_id/item_cards", userHandler.ListUserItemCards()) - appApiRouter.GET("/users/:user_id/item_cards/uses", userHandler.ListUserItemCardUses()) - } + // 登录保持公开 + appPublicApiRouter.POST("/users/weixin/login", userHandler.WeixinLogin()) + } + + // APP 端认证接口路由组 + appAuthApiRouter := mux.Group("/api/app", core.WrapAuthHandler(intc.AppTokenAuthVerify)) + { + appAuthApiRouter.PUT("/users/:user_id", userHandler.ModifyUser()) + appAuthApiRouter.GET("/users/:user_id/orders", userHandler.ListUserOrders()) + appAuthApiRouter.GET("/users/:user_id/coupons", userHandler.ListUserCoupons()) + appAuthApiRouter.GET("/users/:user_id/points", userHandler.ListUserPoints()) + appAuthApiRouter.GET("/users/:user_id/points/balance", userHandler.GetUserPointsBalance()) + appAuthApiRouter.POST("/users/:user_id/phone/bind", userHandler.BindPhone()) + appAuthApiRouter.GET("/users/:user_id/invites", userHandler.ListUserInvites()) + appAuthApiRouter.GET("/users/:user_id/item_cards", userHandler.ListUserItemCards()) + appAuthApiRouter.GET("/users/:user_id/item_cards/uses", userHandler.ListUserItemCardUses()) + } return mux, nil } diff --git a/internal/service/user/item_card_add.go b/internal/service/user/item_card_add.go index e68e768..fdd39f9 100644 --- a/internal/service/user/item_card_add.go +++ b/internal/service/user/item_card_add.go @@ -45,6 +45,13 @@ func (s *service) AddItemCard(ctx context.Context, userID int64, cardID int64, q item.ValidEnd = tpl.ValidEnd } do := s.writeDB.UserItemCards.WithContext(ctx) + // 避免写入未使用相关字段的零值,触发 MySQL 时间字段校验错误 + do = do.Omit( + s.writeDB.UserItemCards.UsedAt, + s.writeDB.UserItemCards.UsedDrawLogID, + s.writeDB.UserItemCards.UsedActivityID, + s.writeDB.UserItemCards.UsedIssueID, + ) if tpl.ValidEnd.IsZero() { do = do.Omit(s.writeDB.UserItemCards.ValidEnd) } diff --git a/internal/service/user/item_cards_list.go b/internal/service/user/item_cards_list.go index eb9bd6c..fd6b04c 100644 --- a/internal/service/user/item_cards_list.go +++ b/internal/service/user/item_cards_list.go @@ -6,6 +6,16 @@ import ( "bindbox-game/internal/repository/mysql/model" ) +type ItemCardWithTemplate struct { + *model.UserItemCards + Name string `json:"name"` + CardType int32 `json:"card_type"` + ScopeType int32 `json:"scope_type"` + EffectType int32 `json:"effect_type"` + StackingStrategy int32 `json:"stacking_strategy"` + Remark string `json:"remark"` +} + // ListUserItemCards 获取用户道具卡列表 // 功能描述: // - 查询指定用户的道具卡列表 @@ -44,3 +54,69 @@ func (s *service) ListUserItemCards(ctx context.Context, userID int64, page, pag return items, total, nil } +func (s *service) ListUserItemCardsWithTemplate(ctx context.Context, userID int64, page, pageSize int) (items []*ItemCardWithTemplate, total int64, err error) { + q := s.readDB.UserItemCards.WithContext(ctx).ReadDB().Where(s.readDB.UserItemCards.UserID.Eq(userID)) + total, err = q.Count() + if err != nil { + return nil, 0, err + } + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 20 + } + if pageSize > 100 { + pageSize = 100 + } + rows, err := q.Order(s.readDB.UserItemCards.ID.Desc()).Offset((page-1)*pageSize).Limit(pageSize).Find() + if err != nil { + return nil, 0, err + } + cidMap := make(map[int64]struct{}) + for _, r := range rows { + if r.CardID > 0 { + cidMap[r.CardID] = struct{}{} + } + } + tpls := map[int64]*model.SystemItemCards{} + if len(cidMap) > 0 { + ids := make([]int64, 0, len(cidMap)) + for id := range cidMap { + ids = append(ids, id) + } + list, err := s.readDB.SystemItemCards.WithContext(ctx).ReadDB().Where(s.readDB.SystemItemCards.ID.In(ids...)).Find() + if err != nil { + return nil, 0, err + } + for _, it := range list { + tpls[it.ID] = it + } + } + items = make([]*ItemCardWithTemplate, len(rows)) + for i, r := range rows { + tpl := tpls[r.CardID] + var name string + var cardType, scopeType, effectType, stacking int32 + var remark string + if tpl != nil { + name = tpl.Name + cardType = tpl.CardType + scopeType = tpl.ScopeType + effectType = tpl.EffectType + stacking = tpl.StackingStrategy + remark = tpl.Remark + } + items[i] = &ItemCardWithTemplate{ + UserItemCards: r, + Name: name, + CardType: cardType, + ScopeType: scopeType, + EffectType: effectType, + StackingStrategy: stacking, + Remark: remark, + } + } + return items, total, nil +} + diff --git a/internal/service/user/user.go b/internal/service/user/user.go index ad1094d..d731b4c 100644 --- a/internal/service/user/user.go +++ b/internal/service/user/user.go @@ -23,8 +23,9 @@ type Service interface { AddCoupon(ctx context.Context, userID int64, couponID int64) error GrantReward(ctx context.Context, userID int64, req GrantRewardRequest) (*GrantRewardResponse, error) AddItemCard(ctx context.Context, userID int64, cardID int64, quantity int) error - ListUserItemCards(ctx context.Context, userID int64, page, pageSize int) (items []*model.UserItemCards, total int64, err error) - ListUserItemCardUses(ctx context.Context, userID int64, page, pageSize int) (items []*model.ActivityDrawEffects, total int64, err error) + ListUserItemCards(ctx context.Context, userID int64, page, pageSize int) (items []*model.UserItemCards, total int64, err error) + ListUserItemCardsWithTemplate(ctx context.Context, userID int64, page, pageSize int) (items []*ItemCardWithTemplate, total int64, err error) + ListUserItemCardUses(ctx context.Context, userID int64, page, pageSize int) (items []*model.ActivityDrawEffects, total int64, err error) } type service struct {