bindbox-game/docs/开发规范/APP端开发统一规范.md
邹方成 42e7cb5f12
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 31s
feat(interceptor): 添加APP端token验证接口并实现用户私有数据鉴权
refactor(api/user): 重构用户相关接口使用token验证替代user_id路径参数

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

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

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

chore: 更新DS_Store等IDE配置文件
2025-11-15 00:49:53 +08:00

164 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# APP 端开发统一规范
## 1. 目标
统一 APP 端接口的目录结构、编码范式、错误处理与文档注释确保与管理端admin 模块)保持一致的风格与契约。
## 2. 目录结构
- 模块放置:`internal/api/activity/`
- 每个端点单文件:
- `activity_create.go`
- `activity_issue_add.go`
- `activity_list.go`
- `activity_detail.go`
- `activity_issues_list.go`
- `issue_rewards_list.go`
## 3. Handler 初始化
- 在模块根定义 `handler``New(logger, db)`,注入 `writeDB`/`readDB`
- 路由绑定示例:`appHandler := app.New(logger, db)`,方法统一返回 `core.HandlerFunc`
## 4. 请求/响应范式
- 所有接口必须定义 `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)`
- 绑定错误:`ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))`
## 6. 业务错误处理
- 统一:`ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))`
- 业务码:在 `internal/code/` 中约定并使用具体业务码
## 7. 分页与过滤规范
- 分页:`page` 默认 1、`page_size` 默认 20、最大 100超过返回参数错误
- 过滤:字符串使用 `Like("%xxx%")`,数值/枚举使用 `Eq(...)``is_boss` 仅允许 `0/1`
## 8. 路径参数规范
- 使用 `strconv.Atoi` 并校验 `> 0`,非法返回 `ParamBindError`
## 9. 时间规范
- 入参解析:`timeutil.ParseCSTInLocation`
- 响应时间:`FriendlyTime` 或固定格式化 `2006-01-02 15:04`
## 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 端列表接口标准参考:
```go
// ListActivityIssues 活动期列表
// @Summary 活动期列表
// @Description 获取指定活动的期列表,支持分页
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Param activity_id path int true "活动ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listIssuesResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/activities/{activity_id}/issues [get]
func (h *handler) ListActivityIssues() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("activity_id")
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"活动ID无效"),
)
return
}
req := new(listIssuesRequest)
res := new(listIssuesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 { req.Page = 1 }
if req.PageSize == 0 { req.PageSize = 20 }
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"每页最多100条"),
)
return
}
query := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).
Where(h.readDB.ActivityIssues.ActivityID.Eq(int64(id)))
listQuery := query
countQuery := query
issues, err := listQuery.Order(h.readDB.ActivityIssues.ID.Desc()).
Limit(req.PageSize).Offset((req.Page-1)*req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ServerError,
err.Error()),
)
return
}
total, err := countQuery.Count()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ServerError,
err.Error()),
)
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]issueListItem, len(issues))
for i, v := range issues {
res.List[i] = issueListItem{
ID: v.ID,
IssueNumber: v.IssueNumber,
Status: v.Status,
Sort: v.Sort,
CreatedAt: timeutil.FriendlyTime(v.CreatedAt),
UpdatedAt: timeutil.FriendlyTime(v.UpdatedAt),
}
}
ctx.Payload(res)
}
}
```
## 12. 文档生成与预览
- 生成:`make gen-swagger`
- 预览:`make serve-swagger`(默认端口 `36666`,访问 `http://localhost:36666/docs`
- 预览时在右上角 `Authorize` 设置 `Authorization` 值,接口将自动附带令牌进行校验