From b1f2241d0a173cf35778ebe288fc89e47884e102 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 22 Jul 2026 22:31:18 +0800 Subject: [PATCH] perf: skill --- .agent/skills/new-api/SKILL.md | 281 +++++++++++------- .../new-api/references/handler_example.go | 55 ++-- .../new-api/references/logics_example.go | 30 +- .../new-api/references/service_example.go | 37 +-- AGENTS.md | 12 +- internal/apps/custom/routers.go | 7 +- internal/router/root/custom.go | 6 +- internal/router/v1/custom.go | 4 +- internal/router/v1/v1.go | 3 +- 9 files changed, 258 insertions(+), 177 deletions(-) diff --git a/.agent/skills/new-api/SKILL.md b/.agent/skills/new-api/SKILL.md index 6d829fba..0df48dd0 100644 --- a/.agent/skills/new-api/SKILL.md +++ b/.agent/skills/new-api/SKILL.md @@ -1,146 +1,219 @@ --- name: "new-api" -description: "Wavelet 项目专用:当新增或修改自定义业务 API、新增业务路由、新增 service 层核心逻辑时必须使用。本技能指导包职责划分、推荐文件结构、路由解耦、Swagger 文档生成与质量门禁验证。" +description: "Wavelet 项目专用:当新增或修改业务 API、Handler、服务层逻辑、路由注册时必须使用。本技能指导 apps 业务包划分、路由注册、Handler/logics 分层、Swagger 与质量门禁;纠正把一切塞进 custom.go / apps/custom 或产品伞包的错误写法。" --- # 新增业务 API 开发与路由注册规范 -本技能是 Wavelet 项目接口开发与路由注册的唯一指导规范。在开发任何新接口前,请严格按照本指南进行架构决策与路由注册。 +本技能是 Wavelet 接口开发与路由注册的唯一指导规范。在开发任何新接口前,请按本指南做架构决策与路由注册。 --- -## 核心路由准则与防线 (Routing Governance & Guardrails) +## 先搞清:脚手架 vs 产品化 -Wavelet 后端路由采用了**严格的框架层与业务层隔离机制**。请牢记以下开发原则: +Wavelet 是**通用全栈脚手架**。仓库里的 `custom` 相关代码是**示例/占位**,不是产品业务的标准落点。 -1. **禁止修改框架级路由文件**: - - 以下文件属于系统框架/平台级接口,**禁止为了添加自定义业务接口而进行任何修改**: - - `internal/router/router.go`(核心入口委派) - - `internal/router/root/default.go`(公开文件服务、robots.txt、Swagger 及 /api/health 路由) - - `internal/router/root/frontend.go`(前端静态服务) - - `internal/router/v1/v1.go`(V1 分发层协调器) - - `internal/router/v1/admin.go`(框架管理员端管理接口) - - `internal/router/v1/user.go`(框架普通用户端基础接口、OAuth及公开接口) -2. **仅允许在 `custom.go` 中注册业务接口**: - - 所有的自定义/业务相关接口注册,有且仅有以下两个合法的承载点: - - [internal/router/root/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/root/custom.go)(用于挂载到根路径的特殊业务接口) - - [internal/router/v1/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/v1/custom.go)(用于挂载在 API V1 下的标准自定义业务接口) +| 层级 | 含义 | 典型包 | +| :--- | :--- | :--- | +| **平台能力** | 脚手架自带、与具体产品无关 | `oauth`、`user`、`admin/*`、`upload`、`cap`、`config`、`health`、`risk_control` | +| **产品业务** | 基于脚手架做具体产品时新增的域 | 直接落在 `internal/apps//`,与平台包**平级** | + +**一旦用脚手架开发具体产品,整个仓库就是该产品**——例如要做「消息平台」,业务模块应是 `apps/channel`、`apps/conversation`、`apps/delivery` 等,而不是先建 `apps/message` 伞包再往里塞子模块。 --- -## 路由归属判定表 (Where should I register my new API?) +## 反模式(AI 最常踩的坑) -根据接口的**访问路径特征**和**访问身份/限制条件**,决定将新开发的 API 挂载至何处: +### 1. 把所有业务路由塞进 `custom.go` / 路径前缀 `/custom` -| 目标 API 路径特征 | 访问身份/条件限制 | 对应的路由注册入口 | 是否允许修改 | -| :--- | :--- | :--- | :--- | -| **`/my-custom-path`** (挂载在根路径下的特殊业务接口) | 自定义控制 | `root/custom.go` 中的 `RegisterCustomRootRoutes` | **允许修改 (业务自定义入口)** | -| **`/api/v1/custom/...`** (API v1 下的定制业务接口) | 自定义控制 | `v1/custom.go` 中的 `RegisterCustomRoutes` | **允许修改 (业务自定义入口)** | -| **`/api/v1/admin/...`** (系统管理员管理端接口) | 需要管理员登录 (`admin.LoginAdminRequired()`) | `v1/admin.go` | **禁止修改 (仅限系统框架路由)** | -| **`/api/v1/user/...`** (框架普通用户基础接口) | 需要普通用户登录 (`oauth.LoginRequired()`) | `v1/user.go` | **禁止修改 (仅限系统框架路由)** | -| **`/api/v1/public/...`** (Captcha、Config 等系统公开接口) | 所有人 (无条件 / 公开) | `v1/user.go` | **禁止修改 (仅限系统框架路由)** | -| **`GET /f/:id`**, **`GET /robots.txt`**, **`GET /api/health`** (系统级默认及公开接口) | 所有人 (无条件 / 公开) | `root/default.go` | **禁止修改 (仅限系统框架路由)** | +仓库中的: + +- `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//`,路由用语义化路径(如 `/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`)。 --- -## 两个自定义路由包的用法与区别 (Root Custom vs V1 Custom) +## 路由注册模型 -### 1. 根路径自定义包:`root/custom.go` +### 谁可以改 -* **适用场景**:适用于需要**直接挂载在主域名根路径下**的特殊自定义业务接口(如第三方 Webhook 回调、特定的短链接重定向、外部数据接口等,不需要 `/api/v1` 前缀)。 -* **用法示例**: - 在 [root/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/root/custom.go) 中实现: - ```go - package root +| 文件 | 角色 | 产品化时 | +| :--- | :--- | :--- | +| `internal/router/router.go` | 引擎、中间件、委派入口 | 一般不改;特殊全局中间件才动 | +| `internal/router/v1/v1.go` | V1 分发:调用各 `Register*Routes` | **允许**:增加对新业务注册函数的一行调用 | +| `internal/router/v1/user.go` / `admin.go` | 平台用户端 / 管理端路由 | **优先不改**;仅当扩展平台能力(OAuth、上传、用户资料)时修改 | +| `internal/router/v1/.go`(新建) | 产品业务路由注册 | **推荐落点** | +| `internal/router/v1/custom.go` | **示例** | 可删可留;**不要**把真实业务堆在这里 | +| `internal/router/root/default.go` / `frontend.go` | 文件服务、health、前端静态 | 平台级,勿塞产品 API | +| `internal/router/root/custom.go` | 根路径**示例**占位 | 仅当确需根路径回调/短链时,用**语义路径**注册,或新建 `root/.go` 并由 `root.go` 调用 | - import ( - "github.com/Rain-kl/Wavelet/internal/apps/custom" - "github.com/gin-gonic/gin" - ) +### 路径归属(产品 API 用语义路径) - // RegisterCustomRootRoutes registers custom business routes that belong to the root path. - func RegisterCustomRootRoutes(r *gin.Engine) { - // 挂载到根路径下,如 GET /my-custom-webhook - r.GET("/my-custom-webhook", custom.HandleRootWebhook) - } - ``` - *(注:该函数已由 `root.go` 自动加载,你无需修改任何其他核心文件。)* +| 目标路径特征 | 注册位置 | 说明 | +| :--- | :--- | :--- | +| `/api/v1//...`(如 `/api/v1/channels`) | `v1/.go` 的 `RegisterRoutes`,在 `v1.go` 调用 | **产品业务默认做法** | +| `/api/v1/admin//...` | 管理端:可在 `admin.go` 增加小组,或 `v1/admin_.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` | 平台,勿改用途 | -### 2. V1 API 自定义包:`v1/custom.go` - -* **适用场景**:适用于普通的**自定义业务 API**,需要规范挂载在标准 API V1 路径下(即自动带有 `/api/v1/custom/...` 前缀,可选择性配置用户/管理员登录中间件)。 -* **用法示例**: - 在 [v1/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/v1/custom.go) 中实现: - ```go - package v1 - - import ( - "github.com/Rain-kl/Wavelet/internal/apps/custom" - "github.com/gin-gonic/gin" - ) - - // RegisterCustomRoutes registers standard custom API routes under /api/v1. - func RegisterCustomRoutes(apiV1Router *gin.RouterGroup) { - customRouter := apiV1Router.Group("/custom") - { - // 挂载到 /api/v1/custom 下,例如:POST /api/v1/custom/action - customRouter.POST("/action", custom.DoActionHandler) - } - } - ``` - *(注:该函数已由 `v1/v1.go` 自动加载,你无需修改任何其他核心文件。)* +`custom.go` 里现有的 `/api/v1/custom/...` **仅作脚手架演示**,不代表业务必须挂在 `/custom` 下。 --- -## 建议创建/修改的文件结构 (Recommended Directory Structure) +## 推荐目录结构(产品业务) -当新增一套定制的业务接口(例如名为 `custom` 的业务模块)时,建议采用以下标准文件结构: +以「频道 / channel」域为例(消息平台中的一个限界上下文): ```text internal/ ├── router/ -│ ├── root/ -│ │ └── custom.go # [修改] 若为根路径 API,在此处注册,将路由委派给 apps/custom │ └── v1/ -│ └── custom.go # [修改] 若为 v1 API,在此处注册,将路由委派给 apps/custom +│ ├── v1.go # [修改] 调用 RegisterChannelRoutes +│ └── channel.go # [新建] 只负责挂载 channel 路由 └── apps/ - └── custom/ - ├── routers.go # [新建] HTTP Handlers (Gin),负责参数绑定、校验与响应 - ├── logics.go # [新建] 业务逻辑层:承载模块内闭环的纯 Go 业务逻辑,不依赖 gin.Context - └── errs.go # [新建] 存放模块特有的业务错误常量定义(可选) + └── channel/ # 与 oauth、user、upload 平级 + ├── routers.go # HTTP Handlers(绑定、鉴权上下文、响应) + ├── logics.go # 纯业务:context.Context,无 gin + ├── errs.go # 模块错误文案常量(可选) + └── ... # 需要时再加 service.go、tasks.go 等 ``` ---- +**不要**建成: -## 核心开发步骤 (Step-by-Step Flow) +```text +internal/apps/message/ # ❌ 产品伞包 + channel/ + inbox/ +internal/apps/custom/ # ❌ 示例包当业务垃圾桶 + channel_handler.go +``` -### 步骤 1:数据库定义与迁移 -如果自定义功能涉及新表或字段,请参考 [database-migration](../database-migration/SKILL.md) 技能,在 `internal/db/migrator/goose/` 目录下编写迁移文件并在 `internal/model/` 中定义 GORM 数据模型。 - -### 步骤 2:在模块内实现业务逻辑 (`logics.go` / `service.go`) -业务逻辑逻辑应当实现于 `internal/apps/custom/` 目录下: -- **优先使用纯函数(`logics.go`)**:定义接收 `context.Context` 且不依赖 `*gin.Context` 的函数,易于单元测试与 Worker 复用。参考 `internal/apps/user/logics.go`。 -- **有状态服务(`service.go`)**:若需注入依赖(如 DB 连接、外部客户端等),可定义 Service 结构体和构造函数。 -- **跨模块副作用(推送、任务监听等)**:核心业务代码通过 `internal/listener` 发射域事件,禁止直接 `import` push 模块;装配在 `internal/bootstrap` 完成(参见 `push-notification` skill)。 - -### 步骤 3:编写 HTTP Handler (`routers.go`) -在 `internal/apps/custom/routers.go` 中编写 Handler: -- 负责请求参数绑定与校验(使用 `ShouldBindJSON`/`ShouldBindQuery`)。 -- 负责提取 Session / 用户身份。 -- 调用业务逻辑层,并使用 `github.com/Rain-kl/Wavelet/internal/common/response` 统一返回响应: - - 成功时返回:`response.OK(data)` 或 `response.OKNil()` - - 失败时返回:`response.Err(msg)` -- 编写规范的 Swagger 注释。 - -### 步骤 4:在自定义包中注册路由并委派 -根据 **路由归属判定表**,在 [root/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/root/custom.go) 或 [v1/custom.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/v1/custom.go) 中编写注册代码,将路由路径绑定到步骤 3 中编写的 Handler。 +模块内若复杂度高,可在**该域包内**分子目录(如 `apps/channel/handler`),但仍是一个域包,不是「产品名/子域」两层品牌结构。 --- -## 质量验证门禁 (Quality Gates) +## 路由注册示例 -每次新增或修改接口后,必须运行并验证以下各项: -1. **自动授权许可**:`make license`(新增 Go 文件时自动添加许可头) -2. **重新生成 Swagger 文档**:`make swagger`(若有 Swagger 注释修改) -3. **静态代码及风格检查**:`make code-check`(确保通过 golangci-lint 和前端 TS 检查) -4. **自动化单元测试**:`go test ./...`(确保所有测试 100% 通过) +### `internal/router/v1/channel.go`(产品业务) + +```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" +) + +// RegisterChannelRoutes mounts channel domain APIs under /api/v1. +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`(增加一行委派) + +```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](../database-migration/SKILL.md) 在 goose 迁移与 `internal/model/` 中定义。 + +### 步骤 3:`logics.go` / `service.go` + +放在 `internal/apps//`: + +- **优先**纯函数 `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/.go`,在 `v1.go` 调用;管理端按需挂到 admin 组。 + +--- + +## 与平台路由的边界 + +- **扩展平台能力**(用户资料字段、上传策略、OAuth 源):改对应平台 `apps/*` 与 `user.go`/`admin.go`。 +- **新产品功能**:新建 `apps/` + `router/v1/.go`,**不要**塞进 `custom` 或某个无关平台包。 +- 管理端产品配置页 API:路径宜为 `/api/v1/admin//...`,中间件与现有 admin 组一致。 + +--- + +## 质量验证门禁 + +1. `make license`(新 Go 文件许可头) +2. `make swagger`(Handler/Swagger 有变时) +3. `make format` 与 `make code-check` +4. `go test` 覆盖相关包 + +--- + +## 自检清单 + +- [ ] 未把真实业务堆进 `apps/custom` 或 `v1/custom.go` +- [ ] 未创建 `apps/<产品名>/` 伞包再塞子域 +- [ ] 业务包与 `oauth`/`user`/`upload` 平级,路径语义化(非强制 `/custom`) +- [ ] 路由在 `router/v1/.go`(或 admin 对应处)注册,并由 `v1.go` 委派 +- [ ] Handler 用 `response.Abort*` / `response.OK`,logics 不依赖 gin +- [ ] 需要时已跑 swagger / code-check diff --git a/.agent/skills/new-api/references/handler_example.go b/.agent/skills/new-api/references/handler_example.go index 7f1efa1f..c8efcddb 100644 --- a/.agent/skills/new-api/references/handler_example.go +++ b/.agent/skills/new-api/references/handler_example.go @@ -6,53 +6,50 @@ package references import ( "net/http" - "github.com/Rain-kl/Wavelet/internal/service" - "github.com/Rain-kl/Wavelet/internal/util" + "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" ) -// customRequest 客户端请求体 DTO -type customRequest struct { - Payload string `json:"payload" binding:"required,min=1,max=100"` +// createChannelRequest 客户端请求体 DTO +type createChannelRequest struct { + Name string `json:"name" binding:"required,min=1,max=100"` } -// customResponse API 响应体 DTO -type customResponse struct { - Result string `json:"result"` +// createChannelResponse API 响应体 DTO +type createChannelResponse struct { + ID int64 `json:"id"` + Name string `json:"name"` } -// HandleCustomBusiness 示例 API Handler -// @Summary 示例定制业务接口 -// @Description 接收数据载荷,调用 Service 执行核心逻辑,并返回统一格式的 JSON 结果。 -// @Tags custom +// CreateChannel 示例:产品域 Handler(应放在 internal/apps/channel/routers.go) +// @Summary 创建频道 +// @Description 示例:语义路径下的业务接口,而非 /api/v1/custom/... +// @Tags channel // @Accept json // @Produce json -// @Param request body customRequest true "业务请求参数" -// @Success 200 {object} util.ResponseAny{data=customResponse} "操作成功" -// @Router /api/v1/custom/business [post] -func HandleCustomBusiness(c *gin.Context) { - // 1. 参数绑定与校验 - var req customRequest +// @Param request body createChannelRequest true "业务请求参数" +// @Success 200 {object} response.Any{data=createChannelResponse} "操作成功" +// @Failure 400 {object} response.Any "参数错误" +// @Failure 401 {object} response.Any "未登录" +// @Router /api/v1/channels [post] +func CreateChannel(c *gin.Context) { + var req createChannelRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, util.Err("参数校验失败:载荷不能为空且在 1-100 字符内")) + response.AbortBadRequest(c, "参数校验失败") return } - // 2. 模拟获取当前上下文与已登录用户(例如从 Session 中提取) - // 通常结合 oauth.LoginRequired() 等中间件使用 + // 通常结合 oauth.LoginRequired();此处仅演示从上下文取用户 userID := int64(9527) - // 3. 实例化业务 Service 并调用核心逻辑 - // 注意传入 c.Request.Context() 以正确传递 OpenTelemetry Tracing 等上下文信息 - svc := service.NewCustomService() - resText, err := svc.ProcessBusinessData(c.Request.Context(), userID, req.Payload) + result, err := CreateChannelLogic(c.Request.Context(), userID, req.Name) if err != nil { - c.JSON(http.StatusInternalServerError, util.Err(err.Error())) + response.AbortBadRequest(c, err.Error()) return } - // 4. 返回符合外层形状规范 { "error_msg": "", "data": ... } 的统一成功响应 - c.JSON(http.StatusOK, util.OK(customResponse{ - Result: resText, + c.JSON(http.StatusOK, response.OK(createChannelResponse{ + ID: result.ID, + Name: result.Name, })) } diff --git a/.agent/skills/new-api/references/logics_example.go b/.agent/skills/new-api/references/logics_example.go index c24c2048..40e92984 100644 --- a/.agent/skills/new-api/references/logics_example.go +++ b/.agent/skills/new-api/references/logics_example.go @@ -12,21 +12,27 @@ import ( "go.uber.org/zap" ) -// ProcessLocalBusiness 示例的模块内部闭环业务逻辑 -// 1. 存放在 apps/custom/logics.go 下,遵循纯 Go 规范,不强依赖 gin.Context,以便逻辑清晰和便于单元测试。 -// 2. 用于当前应用模块内的简单业务或通用过程。 -func ProcessLocalBusiness(ctx context.Context, userID int64, param string) (string, error) { - if param == "" { - return "", errors.New("param cannot be empty") +// channelCreated 示例 logics 返回值(真实代码可用 model 或专用 DTO) +type channelCreated struct { + ID int64 + Name string +} + +// CreateChannelLogic 示例:模块内闭环业务(放在 apps/channel/logics.go) +// 接收 context.Context,不依赖 gin.Context,便于单测与 Worker 复用。 +func CreateChannelLogic(ctx context.Context, userID int64, name string) (*channelCreated, error) { + if name == "" { + return nil, errors.New("name cannot be empty") } - logger.Info(ctx, "processing local business inside apps/custom/logics", + logger.Info(ctx, "creating channel", zap.Int64("user_id", userID), - zap.String("param", param), + zap.String("name", name), ) - // 执行轻量级、无需跨模块/多入口复用的本地计算或模型操作 - result := fmt.Sprintf("Processed local logic for user %d: %s", userID, param) - - return result, nil + // 轻量级本地逻辑;复杂持久化可进 model/repository + return &channelCreated{ + ID: 1, + Name: fmt.Sprintf("%s (by %d)", name, userID), + }, nil } diff --git a/.agent/skills/new-api/references/service_example.go b/.agent/skills/new-api/references/service_example.go index 97ad6699..81e6b2a3 100644 --- a/.agent/skills/new-api/references/service_example.go +++ b/.agent/skills/new-api/references/service_example.go @@ -12,34 +12,29 @@ import ( "go.uber.org/zap" ) -// CustomService 示例业务 Service 结构体(通常放在 internal/apps/custom/service.go 中) -type CustomService struct { - // 这里可以注入数据库连接、配置对象或者其他基础服务的客户端 - // 例如:db *gorm.DB +// ChannelService 示例有状态 Service(放在 internal/apps/channel/service.go) +// 需要注入 DB/客户端时使用;简单逻辑优先 logics.go 纯函数。 +type ChannelService struct { + // 例如:repo ChannelRepository } -// NewCustomService 创建 CustomService 实例的构造函数 -func NewCustomService() *CustomService { - return &CustomService{} +// NewChannelService 构造函数 +func NewChannelService() *ChannelService { + return &ChannelService{} } -// ProcessBusinessData 演示核心业务处理逻辑的 Service 方法 -// 1. 首位参数必须是 context.Context,以传播链路追踪 (OTel) 和超时控制。 -// 2. 方法签名应该只包含纯 Go 的参数与返回值,禁止导入 Gin 或与 HTTP 相关的协议依赖。 -// 3. 将可能发生的核心异常通过 error 返回给上层,而不是在这一层转换成 HTTP 状态码。 -func (s *CustomService) ProcessBusinessData(ctx context.Context, userID int64, payload string) (string, error) { - if payload == "" { - return "", errors.New("payload cannot be empty") +// Create 核心业务:首位参数必须是 context.Context;禁止依赖 Gin。 +func (s *ChannelService) Create(ctx context.Context, userID int64, name string) (int64, error) { + if name == "" { + return 0, errors.New("name cannot be empty") } - // 模拟执行业务逻辑... - logger.Info(ctx, "processing custom business data in service", + logger.Info(ctx, "channel service create", zap.Int64("user_id", userID), - zap.String("payload", payload), + zap.String("name", name), ) - // 这里可以包含数据库读写、事务控制、或者远程 API 调用等复杂逻辑。 - result := fmt.Sprintf("Success processed data for user %d: %s", userID, payload) - - return result, nil + // DB 事务、远程调用等 + _ = fmt.Sprintf("user=%d name=%s", userID, name) + return 1, nil } diff --git a/AGENTS.md b/AGENTS.md index 54193fe7..b24722b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ | Skill | 何时使用 | | :--- | :--- | -| `new-api` | 添加或修改自定义业务 API、Handler、服务层逻辑、自定义路由注册 | +| `new-api` | 添加或修改业务 API、Handler、服务层逻辑、路由注册(含 apps 包划分;勿把业务堆进 custom 示例) | | `new-async-task` | 添加或修改 Asynq 任务、定时任务、TaskHandler、任务元数据 | | `new-setting` | 添加或修改系统/业务/公开设置、`/admin/system` 参数或 `/admin/settings` 图形化设置 | | `database-migration` | 数据库表结构变更、goose SQL 迁移(PG/SQLite/ClickHouse)、seed 数据 | @@ -40,7 +40,7 @@ - 切勿删除 `frontend/node_modules` - 保持 `internal/util/` 绝对纯净且不引入任何框架。禁止从 `internal/util/` 及其子包中导入 Gin、GORM、sessions 等 HTTP/Web/数据库相关框架包(例如,Web 会话选项已收敛至 `internal/apps/oauth/session.go`)。 - 编写测试用例时,禁止使用硬编码的相对路径(如 `"uploads/test_cache"`)在源码目录下创建临时测试目录,必须统一使用 Go 内置的 `t.TempDir()` 以避免污染源码目录。 -- 所有 HTTP 路由仅在 `internal/router/router.go` 中注册。 +- 所有 HTTP 路由经 `internal/router/` 注册(`router.go` 高层委派;业务挂载见 `new-api` skill)。禁止在 `router.go` 内直接挂业务 Handler。 - 当 API Handler 发生变化时,更新 Swagger 文档(运行 `make swagger`)。 - 在完成代码开发后必须运行 `make code-check`, 并修复报错。 - 在完成代码开发后/git 提交前必须运行 `make format`进行格式化。 @@ -83,7 +83,9 @@ - `internal/bootstrap/`:应用装配根(composition root)。集中注册任务 Handler、推送域事件订阅、任务完成监听器,并执行 `SyncEvents`、ClickHouse 访问日志写入等进程级初始化;所有注册函数使用 `sync.Once` 保证幂等。 - `internal/config/`:Viper 加载和配置结构体。运行时代码应使用 `config.Config.
.`。 - `internal/router/`:唯一的 HTTP 路由注册点。 -- `internal/apps/`:按功能(Feature-based)组织的 HTTP Handler、中间件、内部服务与模块逻辑。移除全局 service 层,模块内部业务逻辑(如验证码业务逻辑管理器 `internal/apps/cap/manager.go`)均收敛于各自模块中;管理端模块位于 `internal/apps/admin/`。 +- `internal/apps/`:按**业务能力/限界上下文**(Feature-based)组织的 HTTP Handler、中间件与模块逻辑;包与包**平级**(如 `oauth`、`user`、`upload` 与产品域 `channel` 等同级)。管理端位于 `internal/apps/admin/`。 + - **脚手架 vs 产品化**:本仓库是通用脚手架。基于它做具体产品时,仓库即该产品——业务模块直接建在 `apps//`,**禁止**再建 `apps/<产品名>/` 伞包再嵌套子模块(错误:`apps/message/channel`;正确:`apps/channel`)。 + - **`apps/custom` 与 `router/*/custom.go` 仅为示例占位**(如 `GET /api/v1/custom/hello`),不是真实业务的默认落点;产品 API 使用语义路径与独立 `apps/` + `router/v1/.go`(详见 `new-api` skill)。 - `internal/apps/upload/`:上传记录、文件访问控制、本地/S3 文件响应、下载及图片 WebP 压缩。业务应复用 `upload.Ingest` / `upload.Remove` 与 `GET /f/:id` 文件服务,不直接操作底层 storage 或旁路写 `w_uploads`。 - `internal/model/`:GORM 实体和模型级业务方法。 - `internal/db/`:PostgreSQL、Redis、ClickHouse、GORM 日志、ID 生成和 goose SQL 迁移的布线。 @@ -253,7 +255,9 @@ func doSomething(c *gin.Context) { response.AbortBadRequest(c, "...") } 路由与模块: - 仅在 `internal/router/router.go` 中作为统一高层入口进行路由分发委派,不允许在 `router.go` 中直接挂载业务 Handler。 -- 关于所有的路由归属划分、接口开发隔离防线以及详细的注册和开发步骤,请直接阅读并严格遵循 [new-api](file:///Users/ryan/DEV/Go/Wavelet/.claude/skills/new-api/SKILL.md) 技能。 +- 产品业务:新建 `internal/apps//`(与平台包平级)+ `internal/router/v1/.go`,并在 `v1.go` 调用注册函数;路径用语义化前缀(如 `/api/v1/channels`)。 +- **禁止**把真实业务堆进 `internal/apps/custom` 或 `internal/router/v1/custom.go`(二者是脚手架示例);**禁止** `apps/<产品伞包>/<子域>` 两层品牌结构。 +- 关于路由归属、包划分、Handler/logics 分层与质量门禁,请严格遵循 [new-api](file:///Users/ryan/DEV/Go/Wavelet/.claude/skills/new-api/SKILL.md) 技能。 应用装配与跨模块集成: diff --git a/internal/apps/custom/routers.go b/internal/apps/custom/routers.go index cb377fbe..4381e842 100644 --- a/internal/apps/custom/routers.go +++ b/internal/apps/custom/routers.go @@ -1,7 +1,8 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -// Package custom provides custom business handlers +// Package custom is a scaffold SAMPLE, not a product business home. +// Real domains live in internal/apps// (sibling of oauth, user, upload). package custom import ( @@ -12,9 +13,9 @@ import ( "github.com/Rain-kl/Wavelet/internal/common/response" ) -// Hello is a sample handler for custom business logic +// Hello is a sample handler only — do not grow real product logic in this package. // @Summary Sample Hello API -// @Description A sample business API for customization +// @Description Scaffold demo API; product APIs use semantic paths under apps/ // @Tags custom // @Produce json // @Success 200 {object} response.Any{data=string} "成功" diff --git a/internal/router/root/custom.go b/internal/router/root/custom.go index c34b2222..15c5be1c 100644 --- a/internal/router/root/custom.go +++ b/internal/router/root/custom.go @@ -8,7 +8,9 @@ import ( "github.com/gin-gonic/gin" ) -// RegisterCustomRootRoutes registers custom business routes that belong to the root path. +// RegisterCustomRootRoutes is a scaffold SAMPLE placeholder for root-path routes +// (webhooks, short links). Prefer semantic paths and/or a dedicated root file; +// do not treat this as the only place for all product APIs. See skill new-api. func RegisterCustomRootRoutes(_ *gin.Engine) { - // Add custom root routes here + // Sample only — add root-path demos here if needed } diff --git a/internal/router/v1/custom.go b/internal/router/v1/custom.go index ba88641d..7a9364da 100644 --- a/internal/router/v1/custom.go +++ b/internal/router/v1/custom.go @@ -9,7 +9,9 @@ import ( "github.com/gin-gonic/gin" ) -// RegisterCustomRoutes registers custom business routes to keep routing clean and stable. +// RegisterCustomRoutes is a scaffold SAMPLE only (demo: GET /api/v1/custom/hello). +// Real product APIs belong in apps// with semantic paths and a dedicated +// Register*Routes file (e.g. channel.go), not piled into this package. See skill new-api. func RegisterCustomRoutes(apiV1Router *gin.RouterGroup) { customRouter := apiV1Router.Group("/custom") { diff --git a/internal/router/v1/v1.go b/internal/router/v1/v1.go index 42213796..d21ca794 100644 --- a/internal/router/v1/v1.go +++ b/internal/router/v1/v1.go @@ -16,6 +16,7 @@ func RegisterV1Routes(apiV1Router *gin.RouterGroup, apiGroup *gin.RouterGroup) { // 2. Admin routes RegisterAdminRoutes(apiV1Router) - // 3. Register custom business routes + // 3. Product domain routes: RegisterXxxRoutes(apiV1Router) — see skill new-api + // 4. Scaffold sample only (optional demo under /api/v1/custom) RegisterCustomRoutes(apiV1Router) }