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配置文件
164 lines
6.2 KiB
Markdown
164 lines
6.2 KiB
Markdown
# 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` 值,接口将自动附带令牌进行校验
|
||
|