更新示例

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
- `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,
task metadata, task payload validation, task logs, task retry behavior, or
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": {
"get": {
"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": {
"get": {
"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": {
"type": "object",
"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": {
"get": {
"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": {
"get": {
"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": {
"type": "object",
"properties": {
+18 -64
View File
@@ -114,20 +114,6 @@ definitions:
description: '"select" 或 "exec"'
type: string
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:
properties:
data:
@@ -1365,56 +1351,6 @@ paths:
summary: 执行 SQL 查询
tags:
- 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:
get:
description: 返回当前数据库的所有用户自定义表名称列表,需要管理员权限
@@ -2509,6 +2445,24 @@ paths:
summary: 获取公共配置
tags:
- 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:
get:
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.DELETE("/auth-sources/:id", admin_auth_source.DeleteAuthSource)
}
// Register custom business routes
registerCustomRoutes(apiV1Router)
}
}