更新示例

This commit is contained in:
ryan
2026-06-10 15:11:42 +08:00
parent 57944398e6
commit 15b3c83625
11 changed files with 378 additions and 270 deletions
+117
View File
@@ -0,0 +1,117 @@
---
name: "new-api"
description: "Wavelet 项目专用:当新增或修改自定义业务 API、新增业务路由、新增 service 层核心逻辑时必须使用。本技能指导包职责划分、推荐文件结构、路由解耦、Swagger 文档生成与质量门禁验证。"
---
# 新增业务 API / 接口开发规范
本技能涵盖 Wavelet 的业务接口开发规范。开始开发前先阅读仓库根目录 [AGENTS.md](file:///Users/ryan/DEV/Go/Wavelet/AGENTS.md),遵守项目级核心规则。
为了保持核心路由入口的稳定性,**所有新增的定制业务接口路由统一注册在独立的 go 文件中,严禁直接堆叠到 `router.go`**。
---
## 包职责划分 (Package Responsibilities)
按照 Go 语言最佳实践与 Google 的开发风格,接口开发应该进行严格的分层,以避免循环依赖和逻辑混乱。
| 目录/包名 | 职责定位 | 框架依赖限制 | 常见包含内容 |
| :--- | :--- | :--- | :--- |
| **`internal/router/`** | 路由分发层 | 依赖 Gin 框架 | `router.go` 核心路由、`custom.go` (自定义路由注册入口) |
| **`internal/apps/custom/`** | 应用入口层与本地逻辑层 | 依赖 Gin 框架 (路由/Handler 部分) | 接收 HTTP 请求、解析请求体(JSON/Query)、校验基础参数、提取 Session。对于**模块内闭环的简单业务逻辑**,直接在其下的 `logics.go` 或 `*_logic.go` 中实现。 |
| **`internal/service/`** | 核心跨模块业务服务层 | **禁止**依赖 Gin/HTTP 框架 | 仅存放**复杂、跨模块/跨领域交互,或被多端复用**(如同时被 Handler、后台 Asynq 任务、Cobra CLI 命令行调用)的业务核心逻辑。只接受 standard `context.Context`。 |
| **`internal/model/`** | 数据模型层 | 依赖 GORM / SQL 基础 | GORM 实体定义、表结构、主键生成、单表极简 SQL 查询方法。 |
| **`internal/db/`** | 数据存储层 | 依赖 SQL 驱动 / GORM 连接 | PostgreSQL, SQLite 等数据库连接管理与 Goose 数据库迁移文件。 |
---
## 建议创建/修改的文件结构
当新增一套定制的业务接口(例如名为 `custom` 的业务模块)时,根据逻辑复杂度建议采用以下文件结构:
```text
internal/
├── router/
│ └── custom.go # [修改/创建] 仅用于注册定制路由,将路由委托给 apps/custom
├── apps/
│ └── custom/
│ ├── routers.go # [新建] HTTP Handlers (Gin),负责参数绑定、校验与响应
│ ├── logics.go # [新建] 承载模块内闭环的简单业务逻辑(保持该逻辑仅局限在当前模块)
│ └── errs.go # [新建] 仅存放业务特有的错误常量定义(可选)
└── service/
└── custom.go # [新建/可选] 仅当出现跨模块交互、复杂多表事务或需要被 Task/CLI 复用时才创建
```
---
## 核心开发步骤 (Step-by-Step Flow)
### 步骤 1:如果有数据库变更,编写数据库迁移
如果需要新表或字段,请参考 [database-migration](../database-migration/SKILL.md) 技能,在 `internal/db/migrator/goose/` 目录下编写迁移文件。在 `internal/model/` 中定义 GORM 数据模型。
### 步骤 2:判断业务逻辑的归属与放置
在编写具体逻辑前,必须明确逻辑是属于**本地简单业务**还是**跨模块复杂业务**:
- **方案 A(推荐,轻量化优先)**:直接在 `internal/apps/custom/logics.go` 下定义函数。该函数虽然在 `apps` 目录下,但同样应该**保持纯 Go 参数**(不直接操作 `*gin.Context`),仅供 Handler 层直接调用。
- **方案 B(当满足“跨模块”、“复杂事务”、“多入口调用”时)**:在 `internal/service/` 下创建独立的业务 Service 方法,以实现逻辑复用和领域解耦。
参考示例:[service_example.go](file:///Users/ryan/DEV/Go/Wavelet/.agent/skills/new-api/references/service_example.go)
### 步骤 3:在 `internal/apps/custom/` 下编写 HTTP Handler
创建应用路由文件 `routers.go`,定义接口的请求和响应 DTO,编写 Handler 绑定参数并调用 Service,编写 Swagger 注释。
参考示例:[handler_example.go](file:///Users/ryan/DEV/Go/Wavelet/.agent/skills/new-api/references/handler_example.go)
### 步骤 4:在 `internal/router/custom.go` 中注册路由
创建路由挂载函数:
```go
package router
import (
"github.com/Rain-kl/Wavelet/internal/apps/custom"
"github.com/gin-gonic/gin"
)
func registerCustomRoutes(apiV1Router *gin.RouterGroup) {
customRouter := apiV1Router.Group("/custom")
{
customRouter.POST("/action", custom.DoActionHandler)
}
}
```
并在 [router.go](file:///Users/ryan/DEV/Go/Wavelet/internal/router/router.go) 中的 `/v1` 路由组末尾调用此函数。
---
## 质量验证与门禁 (Verification Quality Gates)
每当新增或修改 API 接口时,必须严格执行以下验证:
1. **生成授权许可**:
新增 Go 文件后,运行自动添加许可证头部命令:
```bash
make license
```
2. **生成 Swagger 文档**:
在 Handler 编写完 `@Summary` 等 Swagger 注释后,必须生成更新:
```bash
make swagger
```
*注意:若 Swagger 生成失败,请仔细排查注释格式或数据类型引用是否规范。*
3. **静态代码检查与 Linting**:
运行 `golangci-lint` 与前端 TypeScript 门禁,确保没有代码风格和类型安全隐患:
```bash
make code-check
```
4. **编译与功能测试**:
运行整包编译与自动化测试:
```bash
make build-test
```
---
## 相关 Skills
* [go-context](../go-context/SKILL.md):了解如何在 Service 层正确传递取消信号和追踪 Trace。
* [go-error-handling](../go-error-handling/SKILL.md):了解如何优雅地将业务错误向上传递,并在 Handler 层决定响应状态码。
* [database-migration](../database-migration/SKILL.md):当新增接口需要额外表结构或默认配置种子时配合使用。
@@ -0,0 +1,58 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package references
import (
"net/http"
"github.com/Rain-kl/Wavelet/internal/service"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/gin-gonic/gin"
)
// customRequest 客户端请求体 DTO
type customRequest struct {
Payload string `json:"payload" binding:"required,min=1,max=100"`
}
// customResponse API 响应体 DTO
type customResponse struct {
Result string `json:"result"`
}
// HandleCustomBusiness 示例 API Handler
// @Summary 示例定制业务接口
// @Description 接收数据载荷,调用 Service 执行核心逻辑,并返回统一格式的 JSON 结果。
// @Tags custom
// @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
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err("参数校验失败:载荷不能为空且在 1-100 字符内"))
return
}
// 2. 模拟获取当前上下文与已登录用户(例如从 Session 中提取)
// 通常结合 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)
if err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
// 4. 返回符合外层形状规范 { "error_msg": "", "data": ... } 的统一成功响应
c.JSON(http.StatusOK, util.OK(customResponse{
Result: resText,
}))
}
@@ -0,0 +1,32 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package references
import (
"context"
"errors"
"fmt"
"github.com/Rain-kl/Wavelet/internal/logger"
"go.uber.org/zap"
)
// ProcessLocalBusiness 示例的模块内部闭环业务逻辑
// 1. 虽然存放在 apps/custom/logics.go 下,但依然遵循纯 Go 规范,不强依赖 gin.Context,以便逻辑清晰和便于单元测试。
// 2. 仅用于当前应用模块私有的简单业务,避免滥用全局的 internal/service 从而导致 Service 臃肿。
func ProcessLocalBusiness(ctx context.Context, userID int64, param string) (string, error) {
if param == "" {
return "", errors.New("param cannot be empty")
}
logger.Info(ctx, "processing local business inside apps/custom/logics",
zap.Int64("user_id", userID),
zap.String("param", param),
)
// 执行轻量级、无需跨模块/多入口复用的本地计算或模型操作
result := fmt.Sprintf("Processed local logic for user %d: %s", userID, param)
return result, nil
}
@@ -0,0 +1,45 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package references
import (
"context"
"errors"
"fmt"
"github.com/Rain-kl/Wavelet/internal/logger"
"go.uber.org/zap"
)
// CustomService 示例业务 Service 结构体
type CustomService struct {
// 这里可以注入数据库连接、配置对象或者其他基础服务的客户端
// 例如:db *gorm.DB
}
// NewCustomService 创建 CustomService 实例的构造函数
func NewCustomService() *CustomService {
return &CustomService{}
}
// 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")
}
// 模拟执行业务逻辑...
logger.Info(ctx, "processing custom business data in service",
zap.Int64("user_id", userID),
zap.String("payload", payload),
)
// 这里可以包含数据库读写、事务控制、或者远程 API 调用等复杂逻辑。
result := fmt.Sprintf("Success processed data for user %d: %s", userID, payload)
return result, nil
}
+1
View File
@@ -5,6 +5,7 @@ specialized workflows still live in `.agent/skills/`.
## Always Read The Matching Skill ## Always Read The Matching Skill
- `new-api`: use when adding or changing custom business APIs, handlers, service layer logic, or registering customized endpoints.
- `new-async-task`: use when adding or changing Asynq tasks, scheduled jobs, - `new-async-task`: use when adding or changing Asynq tasks, scheduled jobs,
task metadata, task payload validation, task logs, task retry behavior, or task metadata, task payload validation, task logs, task retry behavior, or
Admin task APIs. Admin task APIs.
+32 -103
View File
@@ -677,88 +677,6 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/admin/db-manage/table-data": {
"get": {
"security": [
{
"SessionCookie": []
}
],
"description": "根据传入的数据表名称进行分页数据查询,返回表结构列名及动态行数据,需要管理员权限",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "获取数据表数据",
"parameters": [
{
"type": "string",
"description": "表名称",
"name": "table",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "页码,默认 1",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页大小,默认 10",
"name": "pageSize",
"in": "query"
}
],
"responses": {
"200": {
"description": "获取成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/util.ResponseAny"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/db_manage.TableDataResponse"
}
}
}
]
}
},
"400": {
"description": "参数错误",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
},
"401": {
"description": "未登录",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
},
"403": {
"description": "无管理员权限",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
},
"500": {
"description": "内部错误",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
}
}
}
},
"/api/v1/admin/db-manage/tables": { "/api/v1/admin/db-manage/tables": {
"get": { "get": {
"security": [ "security": [
@@ -2574,6 +2492,38 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/custom/hello": {
"get": {
"description": "A sample business API for customization",
"produces": [
"application/json"
],
"tags": [
"custom"
],
"summary": "Sample Hello API",
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/util.ResponseAny"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/api/v1/health": { "/api/v1/health": {
"get": { "get": {
"description": "检查服务是否正常运行,可用于负载均衡存活探测", "description": "检查服务是否正常运行,可用于负载均衡存活探测",
@@ -4165,27 +4115,6 @@ const docTemplate = `{
} }
} }
}, },
"db_manage.TableDataResponse": {
"type": "object",
"properties": {
"columns": {
"type": "array",
"items": {
"type": "string"
}
},
"results": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"total": {
"type": "integer"
}
}
},
"logger.LogEntry": { "logger.LogEntry": {
"type": "object", "type": "object",
"properties": { "properties": {
+32 -103
View File
@@ -670,88 +670,6 @@
} }
} }
}, },
"/api/v1/admin/db-manage/table-data": {
"get": {
"security": [
{
"SessionCookie": []
}
],
"description": "根据传入的数据表名称进行分页数据查询,返回表结构列名及动态行数据,需要管理员权限",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "获取数据表数据",
"parameters": [
{
"type": "string",
"description": "表名称",
"name": "table",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "页码,默认 1",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页大小,默认 10",
"name": "pageSize",
"in": "query"
}
],
"responses": {
"200": {
"description": "获取成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/util.ResponseAny"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/db_manage.TableDataResponse"
}
}
}
]
}
},
"400": {
"description": "参数错误",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
},
"401": {
"description": "未登录",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
},
"403": {
"description": "无管理员权限",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
},
"500": {
"description": "内部错误",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
}
}
}
},
"/api/v1/admin/db-manage/tables": { "/api/v1/admin/db-manage/tables": {
"get": { "get": {
"security": [ "security": [
@@ -2567,6 +2485,38 @@
} }
} }
}, },
"/api/v1/custom/hello": {
"get": {
"description": "A sample business API for customization",
"produces": [
"application/json"
],
"tags": [
"custom"
],
"summary": "Sample Hello API",
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/util.ResponseAny"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/api/v1/health": { "/api/v1/health": {
"get": { "get": {
"description": "检查服务是否正常运行,可用于负载均衡存活探测", "description": "检查服务是否正常运行,可用于负载均衡存活探测",
@@ -4158,27 +4108,6 @@
} }
} }
}, },
"db_manage.TableDataResponse": {
"type": "object",
"properties": {
"columns": {
"type": "array",
"items": {
"type": "string"
}
},
"results": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"total": {
"type": "integer"
}
}
},
"logger.LogEntry": { "logger.LogEntry": {
"type": "object", "type": "object",
"properties": { "properties": {
+18 -64
View File
@@ -114,20 +114,6 @@ definitions:
description: '"select" 或 "exec"' description: '"select" 或 "exec"'
type: string type: string
type: object type: object
db_manage.TableDataResponse:
properties:
columns:
items:
type: string
type: array
results:
items:
additionalProperties: true
type: object
type: array
total:
type: integer
type: object
logger.LogEntry: logger.LogEntry:
properties: properties:
data: data:
@@ -1365,56 +1351,6 @@ paths:
summary: 执行 SQL 查询 summary: 执行 SQL 查询
tags: tags:
- admin - admin
/api/v1/admin/db-manage/table-data:
get:
description: 根据传入的数据表名称进行分页数据查询,返回表结构列名及动态行数据,需要管理员权限
parameters:
- description: 表名称
in: query
name: table
required: true
type: string
- description: 页码,默认 1
in: query
name: page
type: integer
- description: 每页大小,默认 10
in: query
name: pageSize
type: integer
produces:
- application/json
responses:
"200":
description: 获取成功
schema:
allOf:
- $ref: '#/definitions/util.ResponseAny'
- properties:
data:
$ref: '#/definitions/db_manage.TableDataResponse'
type: object
"400":
description: 参数错误
schema:
$ref: '#/definitions/util.ResponseAny'
"401":
description: 未登录
schema:
$ref: '#/definitions/util.ResponseAny'
"403":
description: 无管理员权限
schema:
$ref: '#/definitions/util.ResponseAny'
"500":
description: 内部错误
schema:
$ref: '#/definitions/util.ResponseAny'
security:
- SessionCookie: []
summary: 获取数据表数据
tags:
- admin
/api/v1/admin/db-manage/tables: /api/v1/admin/db-manage/tables:
get: get:
description: 返回当前数据库的所有用户自定义表名称列表,需要管理员权限 description: 返回当前数据库的所有用户自定义表名称列表,需要管理员权限
@@ -2509,6 +2445,24 @@ paths:
summary: 获取公共配置 summary: 获取公共配置
tags: tags:
- config - config
/api/v1/custom/hello:
get:
description: A sample business API for customization
produces:
- application/json
responses:
"200":
description: 成功
schema:
allOf:
- $ref: '#/definitions/util.ResponseAny'
- properties:
data:
type: string
type: object
summary: Sample Hello API
tags:
- custom
/api/v1/health: /api/v1/health:
get: get:
description: 检查服务是否正常运行,可用于负载均衡存活探测 description: 检查服务是否正常运行,可用于负载均衡存活探测
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package custom provides custom business handlers
package custom
import (
"net/http"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/gin-gonic/gin"
)
// Hello is a sample handler for custom business logic
// @Summary Sample Hello API
// @Description A sample business API for customization
// @Tags custom
// @Produce json
// @Success 200 {object} util.ResponseAny{data=string} "成功"
// @Router /api/v1/custom/hello [get]
func Hello(c *gin.Context) {
c.JSON(http.StatusOK, util.OK("Hello from custom business module!"))
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package router
import (
"github.com/Rain-kl/Wavelet/internal/apps/custom"
"github.com/gin-gonic/gin"
)
// registerCustomRoutes registers custom business routes to keep router.go clean and stable.
func registerCustomRoutes(apiV1Router *gin.RouterGroup) {
customRouter := apiV1Router.Group("/custom")
{
customRouter.GET("/hello", custom.Hello)
}
}
+3
View File
@@ -280,6 +280,9 @@ func registerRoutes(r *gin.Engine) {
adminRouter.PUT("/auth-sources/:id/toggle", admin_auth_source.ToggleAuthSource) adminRouter.PUT("/auth-sources/:id/toggle", admin_auth_source.ToggleAuthSource)
adminRouter.DELETE("/auth-sources/:id", admin_auth_source.DeleteAuthSource) adminRouter.DELETE("/auth-sources/:id", admin_auth_source.DeleteAuthSource)
} }
// Register custom business routes
registerCustomRoutes(apiV1Router)
} }
} }