| name | new-api |
| description | Wavelet 项目专用:当新增或修改业务 API、Handler、服务层逻辑、路由注册时必须使用。本技能指导 apps 业务包划分、路由注册、Handler/logics 分层、Swagger 与质量门禁;纠正把一切塞进 custom.go / apps/custom 或产品伞包的错误写法。 |
新增业务 API 开发与路由注册规范
本技能是 Wavelet 接口开发与路由注册的唯一指导规范。在开发任何新接口前,请按本指南做架构决策与路由注册。
先搞清:脚手架 vs 产品化
Wavelet 是通用全栈脚手架。仓库里的 custom 相关代码是示例/占位,不是产品业务的标准落点。
| 层级 | 含义 | 典型包 |
|---|
| 平台能力 | 脚手架自带、与具体产品无关 | oauth、user、admin/*、upload、cap、config、health、risk_control |
| 产品业务 | 基于脚手架做具体产品时新增的域 | 直接落在 internal/apps/<domain>/,与平台包平级 |
一旦用脚手架开发具体产品,整个仓库就是该产品——例如要做「消息平台」,业务模块应是 apps/channel、apps/conversation、apps/delivery 等,而不是先建 apps/message 伞包再往里塞子模块。
反模式(AI 最常踩的坑)
1. 把所有业务路由塞进 custom.go / 路径前缀 /custom
仓库中的:
internal/router/v1/custom.go
internal/router/root/custom.go
internal/apps/custom/
是演示如何挂一条示例接口(GET /api/v1/custom/hello),不是「所有自定义业务必须写在这里」的规定。
| 错误 | 正确 |
|---|
新功能一律改 v1/custom.go,路径全是 /api/v1/custom/... | 按域新建 apps/<domain>/,路由用语义化路径(如 /api/v1/channels),在 router/v1/ 下用独立注册文件挂载 |
把 custom 包当成业务垃圾桶 | 保留或删除示例均可;真正业务用独立包名 |
2. 产品伞包 + 深层子包
| 错误 | 正确 |
|---|
apps/message/channel、apps/message/inbox、apps/message/delivery(先套一层产品名) | apps/channel、apps/inbox、apps/delivery(域模块与 oauth/user 平级) |
apps/myapp/... 再嵌套所有业务 | 仓库即产品,不要再包一层产品根 |
判定:模块名应对齐业务能力/限界上下文(channel、order、invoice),而不是对齐产品营销名(message-platform、myapp)。
3. 其它仍须遵守的防线
- 不要在
internal/router/router.go 里直接挂业务 Handler(只做高层委派)。
- 不要破坏平台模块既有语义去硬塞无关业务(例如把消息逻辑塞进
apps/user)。
- 错误响应使用
response.Abort*,禁止 c.JSON(..., response.Err(...))(见 AGENTS.md)。
路由注册模型
谁可以改
| 文件 | 角色 | 产品化时 |
|---|
internal/router/router.go | 引擎、中间件、委派入口 | 一般不改;特殊全局中间件才动 |
internal/router/v1/v1.go | V1 分发:调用各 Register*Routes | 允许:增加对新业务注册函数的一行调用 |
internal/router/v1/user.go / admin.go | 平台用户端 / 管理端路由 | 优先不改;仅当扩展平台能力(OAuth、上传、用户资料)时修改 |
internal/router/v1/<domain>.go(新建) | 产品业务路由注册 | 推荐落点 |
internal/router/v1/custom.go | 示例 | 可删可留;不要把真实业务堆在这里 |
internal/router/root/default.go / frontend.go | 文件服务、health、前端静态 | 平台级,勿塞产品 API |
internal/router/root/custom.go | 根路径示例占位 | 仅当确需根路径回调/短链时,用语义路径注册,或新建 root/<domain>.go 并由 root.go 调用 |
路径归属(产品 API 用语义路径)
| 目标路径特征 | 注册位置 | 说明 |
|---|
/api/v1/<domain>/...(如 /api/v1/channels) | v1/<domain>.go 的 Register<Domain>Routes,在 v1.go 调用 | 产品业务默认做法 |
/api/v1/admin/<domain>/... | 管理端:可在 admin.go 增加小组,或 v1/admin_<domain>.go 再由 RegisterAdminRoutes/ v1.go 组装 | 需 admin.LoginAdminRequired() |
/api/v1/user/...、/oauth/...、/upload/... 等 | user.go 等平台文件 | 平台能力,勿把无关产品塞进来 |
| 根路径特殊接口(Webhook、短链) | root 下独立注册函数 | 不要默认塞进 custom 前缀 |
GET /f/:id、/api/health、robots.txt | root/default.go | 平台,勿改用途 |
custom.go 里现有的 /api/v1/custom/... 仅作脚手架演示,不代表业务必须挂在 /custom 下。
推荐目录结构(产品业务)
以「频道 / channel」域为例(消息平台中的一个限界上下文):
internal/
├── router/
│ └── v1/
│ ├── v1.go # [修改] 调用 RegisterChannelRoutes
│ └── channel.go # [新建] 只负责挂载 channel 路由
└── apps/
└── channel/ # 与 oauth、user、upload 平级
├── routers.go # HTTP Handlers(绑定、鉴权上下文、响应)
├── logics.go # 纯业务:context.Context,无 gin
├── errs.go # 模块错误文案常量(可选)
└── ... # 需要时再加 service.go、tasks.go 等
不要建成:
internal/apps/message/ # ❌ 产品伞包
channel/
inbox/
internal/apps/custom/ # ❌ 示例包当业务垃圾桶
channel_handler.go
模块内若复杂度高,可在该域包内分子目录(如 apps/channel/handler),但仍是一个域包,不是「产品名/子域」两层品牌结构。
路由注册示例
internal/router/v1/channel.go(产品业务)
package v1
import (
"github.com/Rain-kl/Wavelet/internal/apps/channel"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/gin-gonic/gin"
)
func RegisterChannelRoutes(apiV1Router *gin.RouterGroup) {
r := apiV1Router.Group("/channels")
r.Use(oauth.LoginRequired())
{
r.GET("", channel.ListChannels)
r.POST("", channel.CreateChannel)
r.GET("/:id", channel.GetChannel)
}
}
internal/router/v1/v1.go(增加一行委派)
func RegisterV1Routes(apiV1Router *gin.RouterGroup, apiGroup *gin.RouterGroup) {
RegisterUserRoutes(apiV1Router, apiGroup)
RegisterAdminRoutes(apiV1Router)
RegisterChannelRoutes(apiV1Router)
RegisterCustomRoutes(apiV1Router)
}
根路径 Webhook(确有需要时)
在 root 用语义路径,例如 POST /webhooks/stripe,注册函数可放在 root/webhooks.go 或扩展现有 root 注册;不要为了「只能写 custom」而使用无意义的 /custom 前缀。
核心开发步骤
步骤 1:划定域包名
- 用业务能力命名:
channel、order、invoice。
- 与现有
apps/ 下平台包平级;禁止产品伞包。
步骤 2:库表与 model
若涉及新表/字段:按 database-migration 在 goose 迁移与 internal/model/ 中定义。
步骤 3:logics.go / service.go
放在 internal/apps/<domain>/:
- 优先纯函数
logics.go:context.Context 入参,无 *gin.Context。
- 有状态依赖时用
service.go 构造注入。
- 跨模块副作用(推送、任务)经
internal/listener + bootstrap,禁止业务直接 import push(见 push-notification)。
步骤 4:Handler(routers.go)
ShouldBindJSON / ShouldBindQuery。
- 成功:
c.JSON(http.StatusOK, response.OK(data)) 或 response.OKNil()。
- 失败:
response.AbortBadRequest / AbortUnauthorized / AbortNotFound / AbortInternal 等,禁止 response.Err 直接 c.JSON。
- 完整 Swagger 注释;
@Router 使用真实语义路径。
参考:references/handler_example.go、logics_example.go、service_example.go(示例域名,非强制包名 custom)。
步骤 5:注册路由
新建 internal/router/v1/<domain>.go,在 v1.go 调用;管理端按需挂到 admin 组。
与平台路由的边界
- 扩展平台能力(用户资料字段、上传策略、OAuth 源):改对应平台
apps/* 与 user.go/admin.go。
- 新产品功能:新建
apps/<domain> + router/v1/<domain>.go,不要塞进 custom 或某个无关平台包。
- 管理端产品配置页 API:路径宜为
/api/v1/admin/<domain>/...,中间件与现有 admin 组一致。
质量验证门禁
make license(新 Go 文件许可头)
make swagger(Handler/Swagger 有变时)
make format 与 make code-check
go test 覆盖相关包
自检清单