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

6.2 KiB
Raw Blame History

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 初始化

  • 在模块根定义 handlerNew(logger, db),注入 writeDB/readDB
  • 路由绑定示例:appHandler := app.New(logger, db),方法统一返回 core.HandlerFunc

4. 请求/响应范式

  • 所有接口必须定义 RequestResponse 结构体
  • 成功统一: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. 参数绑定与校验

  • JSONctx.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 端列表接口标准参考:

// 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 值,接口将自动附带令牌进行校验