feat(interceptor): 添加APP端token验证接口并实现用户私有数据鉴权
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 31s

refactor(api/user): 重构用户相关接口使用token验证替代user_id路径参数

docs: 更新API文档规范,明确私有接口需携带token及返回字段要求

fix(service/user): 避免写入未使用字段的零值导致MySQL校验错误

style: 统一格式化部分代码缩进和导入顺序

chore: 更新DS_Store等IDE配置文件
This commit is contained in:
邹方成 2025-11-15 00:49:53 +08:00
parent 1ab39d2f5a
commit 42e7cb5f12
23 changed files with 687 additions and 455 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
bindboxgame_api Executable file

Binary file not shown.

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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": "券模板IDsystem_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": "核销的订单IDorders.id",
"type": "integer"
},
"user_id": {
"description": "用户IDuser_members.id",
"type": "integer"
},
"valid_end": {
"description": "有效期结束",
"type": "string"
},
"valid_start": {
"description": "有效期开始",
"type": "string"
}
}
},
"model.UserItemCards": {
"type": "object",
"properties": {

View File

@ -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": "券模板IDsystem_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": "核销的订单IDorders.id",
"type": "integer"
},
"user_id": {
"description": "用户IDuser_members.id",
"type": "integer"
},
"valid_end": {
"description": "有效期结束",
"type": "string"
},
"valid_start": {
"description": "有效期开始",
"type": "string"
}
}
},
"model.UserItemCards": {
"type": "object",
"properties": {

View File

@ -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: 下单用户IDuser_members.id
type: integer
type: object
model.UserCoupons:
properties:
coupon_id:
description: 券模板IDsystem_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: 核销的订单IDorders.id
type: integer
user_id:
description: 用户IDuser_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:

View File

@ -21,6 +21,20 @@
- 所有接口必须定义 `Request``Response` 结构体
- 成功统一:`ctx.Payload(res)``res.Message` 使用统一文案 `操作成功`
### 4.1 认证与鉴权(强制)
- 所有“用户私有数据”接口必须携带请求头 `Authorization: <token>`(微信登录返回的令牌),后端基于令牌解析当前用户。
- 路径中的 `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` 值,接口将自动附带令牌进行校验

View File

@ -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. 代办事项: 显示用户需要完成的任务; 比如 绑定手机号 绑定邮箱 绑定工会

View File

@ -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 {

View File

@ -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"`

View File

@ -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 ""
}
}

View File

@ -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

View File

@ -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()))

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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()

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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 {