mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
perf: skill
This commit is contained in:
+177
-104
@@ -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/<domain>/`,与平台包**平级** |
|
||||
|
||||
**一旦用脚手架开发具体产品,整个仓库就是该产品**——例如要做「消息平台」,业务模块应是 `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/<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`)。
|
||||
|
||||
---
|
||||
|
||||
## 两个自定义路由包的用法与区别 (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/<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` 调用 |
|
||||
|
||||
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/<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` | 平台,勿改用途 |
|
||||
|
||||
### 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/<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 组一致。
|
||||
|
||||
---
|
||||
|
||||
## 质量验证门禁
|
||||
|
||||
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/<domain>.go`(或 admin 对应处)注册,并由 `v1.go` 委派
|
||||
- [ ] Handler 用 `response.Abort*` / `response.OK`,logics 不依赖 gin
|
||||
- [ ] 需要时已跑 swagger / code-check
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.<Section>.<Field>`。
|
||||
- `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/<domain>/`,**禁止**再建 `apps/<产品名>/` 伞包再嵌套子模块(错误:`apps/message/channel`;正确:`apps/channel`)。
|
||||
- **`apps/custom` 与 `router/*/custom.go` 仅为示例占位**(如 `GET /api/v1/custom/hello`),不是真实业务的默认落点;产品 API 使用语义路径与独立 `apps/<domain>` + `router/v1/<domain>.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/<domain>/`(与平台包平级)+ `internal/router/v1/<domain>.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) 技能。
|
||||
|
||||
应用装配与跨模块集成:
|
||||
|
||||
|
||||
@@ -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/<domain>/ (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/<domain>
|
||||
// @Tags custom
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Any{data=string} "成功"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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/<domain>/ 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")
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user