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

