更新 skill 后端开发
This commit is contained in:
@@ -1,438 +0,0 @@
|
||||
---
|
||||
name: backend-go-gin-gorm
|
||||
|
||||
description: >
|
||||
使用 Gin + GORM 生成、编写、修改、评审 production-ready 的 Go 后端代码(Generate & Review Go backend code with Gin/GORM)。
|
||||
强制分层架构 handler → service → dao/repository(避免业务逻辑堆在 handler;DAO/Repo 只做数据访问与查询组装),并统一 API 响应包装
|
||||
consistent response envelope(code/message/data + request_id/trace_id 等可观测字段)。接口风格默认推荐 POST + JSON RequestBody
|
||||
as default(必要时遵循 REST 语义与幂等约定),规范 DTO/VO/DO 命名与字段映射 conventions(入参 DTO、出参 VO、持久化 DO/Model)。
|
||||
代码注释使用中文(Chinese comments for maintainability),时间处理默认 Asia/Shanghai(time zone aware time handling),
|
||||
采用结构化日志 structured logging(携带 request_id/trace_id/user_id/path/latency 等上下文),并遵循 Gin/GORM 工程化最佳实践
|
||||
(transactions, context propagation, error wrapping, pagination, soft delete, optimistic locking when needed)。
|
||||
触发场景 Trigger: Go 后端开发 / Gin Handler 创建 / GORM DAO/Repository 实现 / 代码走查与 Review(refactor suggestions, bug fixes, performance tips)。
|
||||
|
||||
argument-hint: "<动作 action> <目标 target>" 例如/ e.g.:
|
||||
"create user-handler", "review service/order.go", "scaffold api/v1/product", "add repo for table/users", "optimize gorm query"
|
||||
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
|
||||
---
|
||||
|
||||
# Go GIN/GORM 开发规范 Skill
|
||||
|
||||
## 触发条件
|
||||
- 用户请求创建/修改 Go 后端代码
|
||||
- 用户请求代码审查
|
||||
- 用户提及 API 开发、数据库操作、统一响应、日志、时间处理
|
||||
- 用户请求设计 API 接口、DTO 结构
|
||||
|
||||
## 上下文收集
|
||||
|
||||
执行前先收集项目信息:
|
||||
|
||||
!`ls -la go.mod go.sum 2>/dev/null || echo "No go.mod found"`
|
||||
|
||||
!`head -20 go.mod 2>/dev/null || echo ""`
|
||||
|
||||
## $ARGUMENTS 解析
|
||||
|
||||
期望格式:`<action> <target>`
|
||||
|
||||
| action | 说明 |
|
||||
|--------|------|
|
||||
| `create` | 创建新文件(handler/service/dao/dto) |
|
||||
| `review` | 审查现有代码 |
|
||||
| `scaffold` | 生成完整模块骨架 |
|
||||
| `fix` | 修复不符合规范的代码 |
|
||||
|
||||
---
|
||||
|
||||
## Plan 阶段
|
||||
|
||||
### 产物清单(按 action 确定)
|
||||
|
||||
| action | 产物 |
|
||||
|--------|------|
|
||||
| `create handler` | `/api/xxx_handler.go` 或 `/internal/handler/xxx.go` |
|
||||
| `create service` | `/internal/service/xxx_service.go` |
|
||||
| `create dao` | `/internal/dao/xxx_dao.go` |
|
||||
| `create dto` | `/internal/model/dto/xxx_dto.go` |
|
||||
| `scaffold` | 上述全部 + entity |
|
||||
|
||||
### 决策点
|
||||
|
||||
1. **目录风格**:检查项目是用 `/api` 还是 `/internal/handler`
|
||||
2. **模块命名**:从 $ARGUMENTS 提取资源名(如 `user`、`order`)
|
||||
3. **是否已存在**:先 Glob 检查目标文件
|
||||
|
||||
---
|
||||
|
||||
## Execute 阶段
|
||||
|
||||
### Handler 层编写规则
|
||||
|
||||
```
|
||||
1. 仅做:参数解析 → 调用 service → 返回响应
|
||||
2. 禁止:编写业务逻辑、直接操作数据库
|
||||
3. 必须:使用 common.ResponseSuccess / common.ResponseError
|
||||
4. 错误处理:gorm.ErrRecordNotFound → CodeNotFound
|
||||
```
|
||||
|
||||
### Service 层编写规则
|
||||
|
||||
```
|
||||
1. 编排 dao 层完成业务
|
||||
2. 记录关键业务日志(Info 级别)
|
||||
3. 错误包装:fmt.Errorf("xxx: %w", err)
|
||||
4. 业务异常记录 Warning 级别日志
|
||||
```
|
||||
|
||||
### DAO 层编写规则
|
||||
|
||||
```
|
||||
1. 封装所有 GORM 操作
|
||||
2. 禁止在 service 层写 SQL
|
||||
3. 复杂查询用 Raw/Exec
|
||||
4. 善用链式调用,但复杂场景优先原生 SQL
|
||||
```
|
||||
|
||||
### 统一响应格式(强制)
|
||||
|
||||
```go
|
||||
// 成功
|
||||
common.ResponseSuccess(c, data)
|
||||
common.ResponseSuccessWithMessage(c, data, "创建成功")
|
||||
|
||||
// 失败
|
||||
common.ResponseError(c, common.CodeParamError, "参数错误")
|
||||
common.ResponseErrorWithDetail(c, common.CodeServerError, "系统错误", err)
|
||||
```
|
||||
|
||||
错误码定义 → 读取 `reference/error-codes.go`
|
||||
|
||||
### 注释规范(强制中文)
|
||||
|
||||
```go
|
||||
// GetUserByID 根据用户ID获取用户信息
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param userID int64 - 用户唯一ID
|
||||
// @return *model.User - 用户信息,未找到返回nil
|
||||
// @return error - 查询错误
|
||||
func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*model.User, error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 设计规范(强制)
|
||||
|
||||
### 核心原则:POST + RequestBody
|
||||
|
||||
```
|
||||
所有 API 优先使用 POST 方法,参数通过 RequestBody 传递
|
||||
避免使用 PathVariables 和 RequestParams
|
||||
```
|
||||
|
||||
### 禁止与推荐
|
||||
|
||||
| 禁止 | 推荐 |
|
||||
|------|------|
|
||||
| `GET /api/projects/{project_id}` | `POST /api/projects/detail` + RequestBody |
|
||||
| `GET /api/users?role=admin&page=1` | `POST /api/users/list` + RequestBody |
|
||||
| URL 中传递敏感信息 | RequestBody 传递所有参数 |
|
||||
|
||||
### API 路径命名规范
|
||||
|
||||
| 操作 | 后缀 | 示例 |
|
||||
|------|------|------|
|
||||
| 列表查询 | `/list` | `POST /api/projects/list` |
|
||||
| 详情查询 | `/detail` | `POST /api/projects/detail` |
|
||||
| 创建 | `/create` | `POST /api/projects/create` |
|
||||
| 更新 | `/update` | `POST /api/projects/update` |
|
||||
| 删除 | `/delete` | `POST /api/projects/delete` |
|
||||
| 同步 | `/sync` | `POST /api/jenkins/organizations/sync` |
|
||||
| 触发 | `/trigger` | `POST /api/builds/trigger` |
|
||||
|
||||
### DTO 命名规范
|
||||
|
||||
| 类型 | 命名格式 | 示例 |
|
||||
|------|----------|------|
|
||||
| 列表请求 | `List{资源}Request` | `ListBuildsRequest` |
|
||||
| 详情请求 | `Get{资源}Request` | `GetBuildRequest` |
|
||||
| 创建请求 | `Create{资源}Request` | `CreateProjectRequest` |
|
||||
| 更新请求 | `Update{资源}Request` | `UpdateProjectRequest` |
|
||||
| 删除请求 | `Delete{资源}Request` | `DeleteProjectRequest` |
|
||||
| 列表响应 | `List{资源}Response` | `ListBuildsResponse` |
|
||||
| 详情响应 | `{资源}DetailResponse` | `BuildDetailResponse` |
|
||||
|
||||
### 通用分页结构
|
||||
|
||||
```go
|
||||
// 请求
|
||||
type PageRequest struct {
|
||||
Page int `json:"page" binding:"required,min=1"`
|
||||
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
// 响应
|
||||
type ListResponse struct {
|
||||
List []interface{} `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
```
|
||||
|
||||
### 模块错误码范围
|
||||
|
||||
| 范围 | 模块 |
|
||||
|------|------|
|
||||
| 0 | 成功 |
|
||||
| 1000-1999 | 通用错误 |
|
||||
| 2000-2999 | 用户/权限 |
|
||||
| 3000-3999 | Jenkins |
|
||||
| 4000-4999 | 项目管理 |
|
||||
| 5000-5999 | Exchange-Hub |
|
||||
|
||||
详细规范 → 读取 `reference/api-design-spec.md`
|
||||
|
||||
---
|
||||
|
||||
## 日志规范(强制)
|
||||
|
||||
### 指定框架
|
||||
项目统一使用 `rmdc-common/wdd_log/log_utils.go`
|
||||
|
||||
### 日志级别使用场景
|
||||
|
||||
| 级别 | 使用场景 | 示例 |
|
||||
|------|----------|------|
|
||||
| `Debug` | 开发调试,详细流程、变量值 | `log.Debug(ctx, "查询参数", map[string]interface{}{"userID": id})` |
|
||||
| `Info` | 关键业务节点 | `log.Info(ctx, "用户登录成功", ...)` / `log.Info(ctx, "订单创建成功", ...)` |
|
||||
| `Warning` | 可预期非致命异常,程序可继续 | `log.Warning(ctx, "外部API超时,启用备用方案", ...)` |
|
||||
| `Error` | 严重错误,业务流程中断 | `log.Error(ctx, "数据库连接失败", ...)` 必须记录堆栈 |
|
||||
|
||||
### 日志内容要求
|
||||
```
|
||||
1. 简练、关键
|
||||
2. 必须包含 TraceID、UserID 等追溯信息
|
||||
3. Error 级别必须记录完整错误堆栈
|
||||
```
|
||||
|
||||
### 日志记录位置
|
||||
|
||||
| 层级 | 记录内容 |
|
||||
|------|----------|
|
||||
| Handler | 使用 `ResponseErrorWithDetail` 自动记录 Error 日志 |
|
||||
| Service | 关键业务操作记录 Info;业务异常记录 Warning |
|
||||
| DAO | 一般不记录日志,错误向上抛出 |
|
||||
|
||||
---
|
||||
|
||||
## 时间处理(强制东八区)
|
||||
|
||||
### 核心规则
|
||||
```
|
||||
时区:Asia/Shanghai (UTC+8)
|
||||
格式:RFC3339
|
||||
```
|
||||
|
||||
### 禁止与必须
|
||||
|
||||
| 禁止 | 必须使用 |
|
||||
|------|----------|
|
||||
| `time.Now()` | `TimeUtils.Now()` |
|
||||
| `time.Parse()` | `TimeUtils.Parse()` |
|
||||
| 直接格式化 | `TimeUtils.Format()` |
|
||||
|
||||
### 工具库位置
|
||||
- 后端:`rmdc-common/utils/TimeUtils.go`
|
||||
- 前端:`TonyMask/src/utils/timeUtils.ts`
|
||||
|
||||
### 使用示例
|
||||
```go
|
||||
// ✅ 正确
|
||||
now := TimeUtils.Now()
|
||||
timestamp := TimeUtils.Now().Format(time.RFC3339)
|
||||
|
||||
// ❌ 错误
|
||||
now := time.Now() // 禁止直接使用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 框架使用规范
|
||||
|
||||
### GIN 框架
|
||||
|
||||
#### 路由组织(强制分组)
|
||||
```go
|
||||
// ✅ 正确:使用路由分组
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
users := v1.Group("/users")
|
||||
{
|
||||
users.GET("/:id", userHandler.GetByID)
|
||||
users.POST("/", userHandler.Create)
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:扁平路由
|
||||
r.GET("/api/v1/users/:id", ...)
|
||||
```
|
||||
|
||||
#### 中间件使用
|
||||
```go
|
||||
// 全局中间件
|
||||
r.Use(middleware.Recovery()) // 恢复
|
||||
r.Use(middleware.Logger()) // 日志
|
||||
r.Use(middleware.CORS()) // 跨域
|
||||
|
||||
// 路由组中间件
|
||||
authGroup := r.Group("/admin")
|
||||
authGroup.Use(middleware.Auth())
|
||||
```
|
||||
|
||||
#### 响应规范
|
||||
```
|
||||
所有 API 响应必须通过 pkg/common 统一响应函数
|
||||
禁止直接使用 c.JSON()、c.String() 等
|
||||
```
|
||||
|
||||
### GORM 框架
|
||||
|
||||
#### 操作位置
|
||||
```
|
||||
所有 GORM 操作必须在 dao 层
|
||||
严禁在 service 层拼接查询
|
||||
```
|
||||
|
||||
#### 链式调用 vs 原生 SQL
|
||||
|
||||
| 场景 | 推荐方式 |
|
||||
|------|----------|
|
||||
| 简单 CRUD | 链式调用 `db.Where().First()` |
|
||||
| 复杂查询(多表 JOIN、子查询) | `Raw()` / `Exec()` 原生 SQL |
|
||||
| 批量操作 | `Raw()` / `Exec()` 保证性能 |
|
||||
|
||||
```go
|
||||
// 简单查询 - 链式调用
|
||||
db.Where("status = ?", 1).Find(&users)
|
||||
|
||||
// 复杂查询 - 原生 SQL
|
||||
db.Raw(`
|
||||
SELECT u.*, COUNT(o.id) as order_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.status = ?
|
||||
GROUP BY u.id
|
||||
`, 1).Scan(&results)
|
||||
```
|
||||
|
||||
#### 错误处理
|
||||
```go
|
||||
// 必须处理 ErrRecordNotFound
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
common.ResponseError(c, common.CodeNotFound, "资源不存在")
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verify 阶段 Checklist
|
||||
|
||||
### 结构检查
|
||||
- [ ] 依赖方向正确:handler → service → dao(无反向引用)
|
||||
- [ ] handler 层无业务逻辑
|
||||
- [ ] dao 层无 service 引用
|
||||
- [ ] 使用 internal 包保护私有代码
|
||||
|
||||
### 响应检查
|
||||
- [ ] 所有 API 使用 `common.ResponseSuccess/Error`
|
||||
- [ ] 错误码来自 `common.Code*` 常量
|
||||
- [ ] 时间戳格式为 RFC3339
|
||||
- [ ] 无直接 `c.JSON()` 调用
|
||||
|
||||
### 代码检查
|
||||
- [ ] 公开函数/结构体有中文注释
|
||||
- [ ] 注释格式:`// 函数名 功能描述`
|
||||
- [ ] 无直接 `time.Now()` 调用
|
||||
- [ ] 无丢弃的 error(`_ = err` 禁止)
|
||||
- [ ] 包名小写无下划线
|
||||
|
||||
### 日志检查
|
||||
- [ ] 使用项目统一日志库
|
||||
- [ ] Error 日志包含完整堆栈
|
||||
- [ ] 关键业务操作有 Info 日志
|
||||
- [ ] 日志包含 TraceID 等追溯信息
|
||||
|
||||
### GORM 检查
|
||||
- [ ] `gorm.ErrRecordNotFound` 已处理
|
||||
- [ ] 复杂查询在 dao 层使用 Raw/Exec
|
||||
- [ ] 无 service 层直接 DB 操作
|
||||
|
||||
### GIN 检查
|
||||
- [ ] 使用路由分组组织 API
|
||||
- [ ] 通用逻辑使用中间件处理
|
||||
- [ ] 响应通过统一函数返回
|
||||
|
||||
### API 设计检查
|
||||
- [ ] 使用 POST + RequestBody(非 GET + PathVariables)
|
||||
- [ ] API 路径使用正确后缀(/list, /detail, /create 等)
|
||||
- [ ] DTO 命名符合规范(List/Get/Create/Update/Delete + 资源 + Request/Response)
|
||||
- [ ] 分页请求嵌入 PageRequest
|
||||
- [ ] 分页响应包含 list/total/page/page_size
|
||||
- [ ] 敏感信息不在 URL 中
|
||||
- [ ] 请求体必须验证(ShouldBindJSON)
|
||||
|
||||
---
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
| 陷阱 | 正确做法 |
|
||||
|------|----------|
|
||||
| handler 写业务逻辑 | 移到 service 层 |
|
||||
| 直接 `c.JSON()` | 用 `common.ResponseSuccess()` |
|
||||
| 忽略 `ErrRecordNotFound` | 转为 `CodeNotFound` 返回 |
|
||||
| `time.Now()` | `TimeUtils.Now()` |
|
||||
| 英文注释 | 改为中文 |
|
||||
| dao 引用 service | 违反依赖原则,重构 |
|
||||
| service 写 SQL | 移到 dao 层 |
|
||||
| 扁平路由 | 使用 Router Group |
|
||||
| 日志缺少上下文 | 添加 TraceID、UserID |
|
||||
| Error 日志无堆栈 | 记录完整错误信息 |
|
||||
| `GET /api/users/{id}` | `POST /api/users/detail` + RequestBody |
|
||||
| URL 传参数 `?page=1` | RequestBody 传递 |
|
||||
| DTO 命名不规范 | 使用 `List/Get/Create/Update/Delete` + 资源名 |
|
||||
| 敏感信息在 URL | 移到 RequestBody |
|
||||
|
||||
---
|
||||
|
||||
## Reference 文件索引
|
||||
|
||||
| 场景 | 读取文件 |
|
||||
|------|----------|
|
||||
| 需要完整目录结构说明 | `reference/project-structure.md` |
|
||||
| 需要响应结构体定义 | `reference/api-response-spec.md` |
|
||||
| 需要错误码完整列表 | `reference/error-codes.go` |
|
||||
| 需要编码规范细节 | `reference/coding-standards.md` |
|
||||
| 需要日志使用详细说明 | `reference/logging-standards.md` |
|
||||
| 需要时间处理详细说明 | `reference/time-handling.md` |
|
||||
| 需要框架使用详细说明 | `reference/framework-usage.md` |
|
||||
| 需要 API 设计详细说明 | `reference/api-design-spec.md` |
|
||||
| 需要代码示例 | `examples/*.go` |
|
||||
|
||||
---
|
||||
|
||||
## 快速命令
|
||||
|
||||
验证项目结构:
|
||||
```bash
|
||||
./scripts/validate-structure.sh
|
||||
```
|
||||
@@ -1,55 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"my-project/internal/model/entity"
|
||||
)
|
||||
|
||||
// UserDAO 用户数据访问对象
|
||||
type UserDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserDAO 创建用户DAO实例
|
||||
// @param db *gorm.DB - 数据库连接
|
||||
// @return *UserDAO - DAO实例
|
||||
func NewUserDAO(db *gorm.DB) *UserDAO {
|
||||
return &UserDAO{db: db}
|
||||
}
|
||||
|
||||
// FindByID 根据ID查询用户
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param id int64 - 用户ID
|
||||
// @return *entity.User - 用户实体
|
||||
// @return error - 查询错误,未找到返回gorm.ErrRecordNotFound
|
||||
func (d *UserDAO) FindByID(ctx context.Context, id int64) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := d.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param user *entity.User - 用户实体
|
||||
// @return error - 创建错误
|
||||
func (d *UserDAO) Create(ctx context.Context, user *entity.User) error {
|
||||
return d.db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
// FindByEmail 根据邮箱查询用户
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param email string - 用户邮箱
|
||||
// @return *entity.User - 用户实体
|
||||
// @return error - 查询错误
|
||||
func (d *UserDAO) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := d.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"my-project/internal/model/dto"
|
||||
"my-project/internal/service"
|
||||
"my-project/pkg/common"
|
||||
)
|
||||
|
||||
// UserHandler 用户相关API处理器
|
||||
type UserHandler struct {
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户Handler实例
|
||||
// @param userService *service.UserService - 用户服务
|
||||
// @return *UserHandler - Handler实例
|
||||
func NewUserHandler(userService *service.UserService) *UserHandler {
|
||||
return &UserHandler{userService: userService}
|
||||
}
|
||||
|
||||
// GetUserByID 根据ID获取用户信息
|
||||
// @param c *gin.Context - GIN上下文
|
||||
func (h *UserHandler) GetUserByID(c *gin.Context) {
|
||||
// 1. 参数解析
|
||||
idStr := c.Param("id")
|
||||
userID, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
common.ResponseError(c, common.CodeParamError, "用户ID格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 调用Service
|
||||
user, err := h.userService.GetUserByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
common.ResponseError(c, common.CodeNotFound, "用户不存在")
|
||||
return
|
||||
}
|
||||
common.ResponseErrorWithDetail(c, common.CodeServerError, "获取用户失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 成功响应
|
||||
common.ResponseSuccess(c, user)
|
||||
}
|
||||
|
||||
// CreateUser 创建用户
|
||||
// @param c *gin.Context - GIN上下文
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ResponseErrorWithDetail(c, common.CodeValidationFail, "参数验证失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.CreateUser(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
common.ResponseErrorWithDetail(c, common.CodeBusiness, "创建用户失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
common.ResponseSuccessWithMessage(c, user, "用户创建成功")
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"my-project/internal/dao"
|
||||
"my-project/internal/model/dto"
|
||||
"my-project/internal/model/entity"
|
||||
"my-project/pkg/log"
|
||||
)
|
||||
|
||||
// UserService 用户业务服务
|
||||
type UserService struct {
|
||||
userDAO *dao.UserDAO
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务实例
|
||||
// @param userDAO *dao.UserDAO - 用户数据访问对象
|
||||
// @return *UserService - 服务实例
|
||||
func NewUserService(userDAO *dao.UserDAO) *UserService {
|
||||
return &UserService{userDAO: userDAO}
|
||||
}
|
||||
|
||||
// GetUserByID 根据用户ID获取用户信息
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param userID int64 - 用户唯一ID
|
||||
// @return *entity.User - 用户实体
|
||||
// @return error - 查询错误
|
||||
func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*entity.User, error) {
|
||||
user, err := s.userDAO.FindByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询用户失败: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// CreateUser 创建新用户
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param req *dto.CreateUserRequest - 创建请求
|
||||
// @return *entity.User - 创建的用户实体
|
||||
// @return error - 创建错误
|
||||
func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) {
|
||||
user := &entity.User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
}
|
||||
|
||||
if err := s.userDAO.Create(ctx, user); err != nil {
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 记录关键业务日志
|
||||
log.Info(ctx, "用户创建成功", map[string]interface{}{
|
||||
"userID": user.ID,
|
||||
"username": user.Username,
|
||||
})
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
# API 设计规范
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 使用 POST + RequestBody
|
||||
|
||||
> **核心规范**: 所有 API 优先使用 POST 方法,参数通过 RequestBody 传递
|
||||
|
||||
```go
|
||||
// ✅ 推荐方式
|
||||
POST /api/jenkins/builds/list
|
||||
{
|
||||
"organization_folder": "Backend",
|
||||
"repository_name": "cmii-fly-center",
|
||||
"branch_name": "master",
|
||||
"page": 1,
|
||||
"page_size": 10
|
||||
}
|
||||
|
||||
// ❌ 避免使用
|
||||
GET /api/jenkins/organizations/{org}/repositories/{repo}/branches/{branch}/builds?page=1&page_size=10
|
||||
```
|
||||
|
||||
### 2. 避免 PathVariables
|
||||
|
||||
```go
|
||||
// ❌ 不推荐
|
||||
GET /api/projects/{project_id}
|
||||
GET /api/builds/{build_id}/console
|
||||
|
||||
// ✅ 推荐
|
||||
POST /api/projects/detail
|
||||
{
|
||||
"project_id": "namespace_abc12345"
|
||||
}
|
||||
|
||||
POST /api/builds/console
|
||||
{
|
||||
"organization_folder": "Backend",
|
||||
"repository_name": "cmii-fly-center",
|
||||
"branch_name": "master",
|
||||
"build_number": 123
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 避免 RequestParams
|
||||
|
||||
```go
|
||||
// ❌ 不推荐
|
||||
GET /api/users/list?role=admin&status=active&page=1
|
||||
|
||||
// ✅ 推荐
|
||||
POST /api/users/list
|
||||
{
|
||||
"role": "admin",
|
||||
"status": "active",
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 统一响应格式
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
// 业务数据
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分页响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [...],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1001,
|
||||
"message": "参数错误: organization_folder不能为空",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 请求结构规范
|
||||
|
||||
### 通用分页请求
|
||||
|
||||
```go
|
||||
type PageRequest struct {
|
||||
Page int `json:"page" binding:"required,min=1"`
|
||||
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
|
||||
}
|
||||
```
|
||||
|
||||
### 通用筛选请求
|
||||
|
||||
```go
|
||||
type ListRequest struct {
|
||||
PageRequest
|
||||
Keyword string `json:"keyword,omitempty"` // 搜索关键词
|
||||
Status string `json:"status,omitempty"` // 状态筛选
|
||||
SortBy string `json:"sort_by,omitempty"` // 排序字段
|
||||
SortOrder string `json:"sort_order,omitempty"` // asc/desc
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 命名规范
|
||||
|
||||
### 操作类型后缀
|
||||
|
||||
| 操作 | 后缀 | 示例 |
|
||||
|------|------|------|
|
||||
| 列表查询 | `/list` | `/api/projects/list` |
|
||||
| 详情查询 | `/detail` | `/api/projects/detail` |
|
||||
| 创建 | `/create` | `/api/projects/create` |
|
||||
| 更新 | `/update` | `/api/projects/update` |
|
||||
| 删除 | `/delete` | `/api/projects/delete` |
|
||||
| 同步 | `/sync` | `/api/jenkins/organizations/sync` |
|
||||
| 触发 | `/trigger` | `/api/builds/trigger` |
|
||||
| 导出 | `/export` | `/api/projects/export` |
|
||||
|
||||
### 模块前缀
|
||||
|
||||
| 模块 | 前缀 |
|
||||
|------|------|
|
||||
| Jenkins | `/api/jenkins/` |
|
||||
| 项目管理 | `/api/projects/` |
|
||||
| 用户 | `/api/users/` |
|
||||
| 权限 | `/api/permissions/` |
|
||||
| 权限-Jenkins | `/api/permissions/jenkins/` |
|
||||
| 权限-项目 | `/api/permissions/projects/` |
|
||||
| 审计 | `/api/audit/` |
|
||||
| Exchange-Hub | `/api/exchange-hub/` |
|
||||
| DCU | `/api/dcu/` |
|
||||
|
||||
---
|
||||
|
||||
## Handler 实现模板
|
||||
|
||||
```go
|
||||
// ListBuilds 获取构建列表
|
||||
// @Summary 获取构建列表
|
||||
// @Tags 构建管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.ListBuildsRequest true "请求参数"
|
||||
// @Success 200 {object} response.Response{data=dto.ListBuildsResponse}
|
||||
// @Router /api/jenkins/builds/list [post]
|
||||
func (h *BuildHandler) ListBuilds(c *gin.Context) {
|
||||
var req dto.ListBuildsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ParamError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.buildService.ListBuilds(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, resp)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DTO 设计规范
|
||||
|
||||
### 请求 DTO 命名
|
||||
|
||||
```go
|
||||
// 列表请求: List{资源}Request
|
||||
type ListBuildsRequest struct {
|
||||
PageRequest
|
||||
OrganizationFolder string `json:"organization_folder" binding:"required"`
|
||||
RepositoryName string `json:"repository_name" binding:"required"`
|
||||
BranchName string `json:"branch_name,omitempty"`
|
||||
}
|
||||
|
||||
// 详情请求: Get{资源}Request 或 {资源}DetailRequest
|
||||
type GetBuildRequest struct {
|
||||
OrganizationFolder string `json:"organization_folder" binding:"required"`
|
||||
RepositoryName string `json:"repository_name" binding:"required"`
|
||||
BranchName string `json:"branch_name" binding:"required"`
|
||||
BuildNumber int `json:"build_number" binding:"required"`
|
||||
}
|
||||
|
||||
// 创建请求: Create{资源}Request
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Namespace string `json:"namespace" binding:"required"`
|
||||
Province string `json:"province" binding:"required"`
|
||||
City string `json:"city" binding:"required"`
|
||||
}
|
||||
|
||||
// 更新请求: Update{资源}Request
|
||||
type UpdateProjectRequest struct {
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Province string `json:"province,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
}
|
||||
|
||||
// 删除请求: Delete{资源}Request
|
||||
type DeleteProjectRequest struct {
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
}
|
||||
```
|
||||
|
||||
### 响应 DTO 命名
|
||||
|
||||
```go
|
||||
// 列表响应: List{资源}Response
|
||||
type ListBuildsResponse struct {
|
||||
List []*BuildDTO `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// 详情响应: {资源}DetailResponse 或直接使用 {资源}DTO
|
||||
type BuildDetailResponse struct {
|
||||
*BuildDTO
|
||||
ConsoleOutput string `json:"console_output,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码规范
|
||||
|
||||
### 错误码范围
|
||||
|
||||
| 范围 | 模块 |
|
||||
|------|------|
|
||||
| 1000-1999 | 通用错误 |
|
||||
| 2000-2999 | 用户/权限 |
|
||||
| 3000-3999 | Jenkins模块 |
|
||||
| 4000-4999 | 项目管理 |
|
||||
| 5000-5999 | Exchange-Hub |
|
||||
| 6000-6999 | Watchdog |
|
||||
|
||||
### 通用错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 0 | 成功 |
|
||||
| 1001 | 参数错误 |
|
||||
| 1002 | 未授权 |
|
||||
| 1003 | 禁止访问 |
|
||||
| 1004 | 资源不存在 |
|
||||
| 1005 | 内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 前端调用示例
|
||||
|
||||
```typescript
|
||||
// api/modules/jenkins.ts
|
||||
export const jenkinsApi = {
|
||||
// 获取构建列表
|
||||
listBuilds: (data: ListBuildsRequest) =>
|
||||
request.post<ListBuildsResponse>('/api/jenkins/builds/list', data),
|
||||
|
||||
// 触发构建
|
||||
triggerBuild: (data: TriggerBuildRequest) =>
|
||||
request.post<TriggerBuildResponse>('/api/jenkins/builds/trigger', data),
|
||||
|
||||
// 获取构建详情
|
||||
getBuildDetail: (data: GetBuildRequest) =>
|
||||
request.post<BuildDetailResponse>('/api/jenkins/builds/detail', data),
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全规范
|
||||
|
||||
### 1. 敏感字段不出现在 URL
|
||||
|
||||
```go
|
||||
// ❌ 敏感信息泄露到URL
|
||||
GET /api/auth/login?username=admin&password=123456
|
||||
|
||||
// ✅ 使用RequestBody
|
||||
POST /api/auth/login
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 必须验证请求体
|
||||
|
||||
```go
|
||||
func (h *Handler) CreateProject(c *gin.Context) {
|
||||
var req dto.CreateProjectRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ParamError(c, err)
|
||||
return
|
||||
}
|
||||
// 后续处理...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 审计敏感操作
|
||||
|
||||
所有写操作需通过审计中间件记录。
|
||||
@@ -1,35 +0,0 @@
|
||||
# API 响应规范
|
||||
|
||||
## 统一响应结构
|
||||
|
||||
```go
|
||||
type Response struct {
|
||||
Code int `json:"code"` // 业务状态码,0=成功
|
||||
Status int `json:"status"` // HTTP 状态码
|
||||
Timestamp string `json:"timestamp"` // RFC3339 东八区
|
||||
Data interface{} `json:"data"` // 业务数据
|
||||
Message string `json:"message,omitempty"` // 消息
|
||||
Error string `json:"error,omitempty"` // 错误详情
|
||||
}
|
||||
```
|
||||
|
||||
## 使用函数
|
||||
|
||||
| 场景 | 函数 |
|
||||
|------|------|
|
||||
| 查询成功 | `ResponseSuccess(c, data)` |
|
||||
| 操作成功 | `ResponseSuccessWithMessage(c, data, "msg")` |
|
||||
| 普通错误 | `ResponseError(c, code, "msg")` |
|
||||
| 详细错误 | `ResponseErrorWithDetail(c, code, "msg", err)` |
|
||||
|
||||
## HTTP 状态码映射
|
||||
|
||||
| 业务码 | HTTP 状态码 |
|
||||
|--------|-------------|
|
||||
| CodeSuccess | 200 |
|
||||
| CodeParamError, CodeValidationFail | 400 |
|
||||
| CodeUnauthorized | 401 |
|
||||
| CodeForbidden | 403 |
|
||||
| CodeNotFound | 404 |
|
||||
| CodeTimeout | 408 |
|
||||
| 其他 | 500 |
|
||||
@@ -1,44 +0,0 @@
|
||||
# 编码规范
|
||||
|
||||
## 命名规范
|
||||
|
||||
| 类型 | 规则 | 示例 |
|
||||
|------|------|------|
|
||||
| 包名 | 小写单词,无下划线 | `service`, `utils` |
|
||||
| 变量/函数 | 驼峰命名 | `getUserByID` |
|
||||
| 公开标识 | 首字母大写 | `GetUserByID` |
|
||||
| 接口 | 单方法以 `er` 结尾 | `Reader`, `Writer` |
|
||||
|
||||
## 注释规范(中文,必须)
|
||||
|
||||
```go
|
||||
// GetUserByID 根据用户ID获取用户信息
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param userID int64 - 用户唯一ID
|
||||
// @return *model.User - 用户信息
|
||||
// @return error - 查询错误
|
||||
func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*model.User, error)
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
1. 必须 `if err != nil` 处理
|
||||
2. 用 `fmt.Errorf("xxx: %w", err)` 包装
|
||||
3. 禁止 `_ = err` 丢弃错误
|
||||
4. Handler 层必须通过统一响应返回
|
||||
|
||||
## 日志级别
|
||||
|
||||
| 级别 | 用途 |
|
||||
|------|------|
|
||||
| Debug | 开发调试,详细流程 |
|
||||
| Info | 关键业务节点 |
|
||||
| Warning | 可预期非致命异常 |
|
||||
| Error | 严重错误,必须记录堆栈 |
|
||||
|
||||
## 时间处理
|
||||
|
||||
- 时区:Asia/Shanghai (UTC+8)
|
||||
- 格式:RFC3339
|
||||
- 禁止:`time.Now()`
|
||||
- 使用:`TimeUtils.Now()`
|
||||
@@ -1,35 +0,0 @@
|
||||
package common
|
||||
|
||||
// 业务状态码常量
|
||||
const (
|
||||
CodeSuccess = 0 // 成功
|
||||
CodeServerError = 10001 // 服务器内部错误
|
||||
CodeParamError = 10002 // 参数错误
|
||||
CodeUnauthorized = 10003 // 未授权
|
||||
CodeForbidden = 10004 // 禁止访问
|
||||
CodeNotFound = 10005 // 资源不存在
|
||||
CodeTimeout = 10006 // 请求超时
|
||||
CodeValidationFail = 10007 // 验证失败
|
||||
CodeBusiness = 20001 // 业务逻辑错误 (20001-29999)
|
||||
)
|
||||
|
||||
// CodeMessage 错误码消息映射
|
||||
var CodeMessage = map[int]string{
|
||||
CodeSuccess: "success",
|
||||
CodeServerError: "服务器内部错误",
|
||||
CodeParamError: "参数错误",
|
||||
CodeUnauthorized: "未授权,请先登录",
|
||||
CodeForbidden: "权限不足,禁止访问",
|
||||
CodeNotFound: "请求的资源不存在",
|
||||
CodeTimeout: "请求超时",
|
||||
CodeValidationFail: "数据验证失败",
|
||||
CodeBusiness: "业务处理失败",
|
||||
}
|
||||
|
||||
// GetMessage 根据错误码获取默认消息
|
||||
func GetMessage(code int) string {
|
||||
if msg, ok := CodeMessage[code]; ok {
|
||||
return msg
|
||||
}
|
||||
return "未知错误"
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
# 框架使用规范
|
||||
|
||||
## GIN 框架
|
||||
|
||||
### 路由组织
|
||||
|
||||
#### 强制使用路由分组 (Router Group)
|
||||
|
||||
```go
|
||||
func SetupRouter(r *gin.Engine) {
|
||||
// API 版本分组
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
// 用户模块
|
||||
users := v1.Group("/users")
|
||||
{
|
||||
users.GET("/", userHandler.List)
|
||||
users.GET("/:id", userHandler.GetByID)
|
||||
users.POST("/", userHandler.Create)
|
||||
users.PUT("/:id", userHandler.Update)
|
||||
users.DELETE("/:id", userHandler.Delete)
|
||||
}
|
||||
|
||||
// 订单模块
|
||||
orders := v1.Group("/orders")
|
||||
{
|
||||
orders.GET("/", orderHandler.List)
|
||||
orders.GET("/:id", orderHandler.GetByID)
|
||||
orders.POST("/", orderHandler.Create)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 禁止扁平路由
|
||||
|
||||
```go
|
||||
// ❌ 错误:扁平路由,难以维护
|
||||
r.GET("/api/v1/users", ...)
|
||||
r.GET("/api/v1/users/:id", ...)
|
||||
r.POST("/api/v1/users", ...)
|
||||
r.GET("/api/v1/orders", ...)
|
||||
```
|
||||
|
||||
### 中间件使用
|
||||
|
||||
#### 全局中间件
|
||||
|
||||
```go
|
||||
func SetupMiddleware(r *gin.Engine) {
|
||||
// Recovery - 恢复 panic,防止程序崩溃
|
||||
r.Use(middleware.Recovery())
|
||||
|
||||
// Logger - 请求日志记录
|
||||
r.Use(middleware.Logger())
|
||||
|
||||
// CORS - 跨域处理
|
||||
r.Use(middleware.CORS())
|
||||
|
||||
// TraceID - 请求追踪
|
||||
r.Use(middleware.TraceID())
|
||||
}
|
||||
```
|
||||
|
||||
#### 路由组中间件
|
||||
|
||||
```go
|
||||
// 需要认证的路由组
|
||||
authGroup := r.Group("/api/v1/admin")
|
||||
authGroup.Use(middleware.Auth())
|
||||
{
|
||||
authGroup.GET("/dashboard", adminHandler.Dashboard)
|
||||
authGroup.GET("/users", adminHandler.ListUsers)
|
||||
}
|
||||
|
||||
// 需要特定权限的路由组
|
||||
superAdmin := authGroup.Group("/super")
|
||||
superAdmin.Use(middleware.RequireRole("super_admin"))
|
||||
{
|
||||
superAdmin.DELETE("/users/:id", adminHandler.DeleteUser)
|
||||
}
|
||||
```
|
||||
|
||||
#### 常用中间件职责
|
||||
|
||||
| 中间件 | 职责 |
|
||||
|--------|------|
|
||||
| Recovery | 捕获 panic,返回 500 错误 |
|
||||
| Logger | 记录请求日志(方法、路径、耗时等) |
|
||||
| CORS | 处理跨域请求 |
|
||||
| Auth | 验证用户身份(JWT/Session) |
|
||||
| TraceID | 生成/传递请求追踪 ID |
|
||||
| RateLimit | 请求频率限制 |
|
||||
|
||||
### 响应规范
|
||||
|
||||
#### 强制使用统一响应
|
||||
|
||||
```go
|
||||
// ✅ 正确:使用统一响应函数
|
||||
common.ResponseSuccess(c, data)
|
||||
common.ResponseError(c, common.CodeParamError, "参数错误")
|
||||
|
||||
// ❌ 错误:直接使用 GIN 原生方法
|
||||
c.JSON(200, data)
|
||||
c.String(200, "success")
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": "bad request"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GORM 框架
|
||||
|
||||
### 操作位置规范
|
||||
|
||||
```
|
||||
所有 GORM 操作必须在 dao 层实现
|
||||
严禁在 service 层直接操作数据库
|
||||
```
|
||||
|
||||
### 查询方式选择
|
||||
|
||||
#### 简单 CRUD - 链式调用
|
||||
|
||||
```go
|
||||
// 单条查询
|
||||
var user entity.User
|
||||
db.Where("id = ?", userID).First(&user)
|
||||
|
||||
// 列表查询
|
||||
var users []entity.User
|
||||
db.Where("status = ?", 1).
|
||||
Order("created_at DESC").
|
||||
Limit(10).
|
||||
Offset(0).
|
||||
Find(&users)
|
||||
|
||||
// 创建
|
||||
db.Create(&user)
|
||||
|
||||
// 更新
|
||||
db.Model(&user).Updates(map[string]interface{}{
|
||||
"name": "new name",
|
||||
"status": 1,
|
||||
})
|
||||
|
||||
// 删除
|
||||
db.Delete(&user, userID)
|
||||
```
|
||||
|
||||
#### 复杂查询 - Raw/Exec
|
||||
|
||||
**推荐场景**:
|
||||
- 多表 JOIN
|
||||
- 子查询
|
||||
- 复杂聚合
|
||||
- 批量操作
|
||||
- 性能敏感场景
|
||||
|
||||
```go
|
||||
// 多表 JOIN 查询
|
||||
type UserWithOrderCount struct {
|
||||
entity.User
|
||||
OrderCount int64 `json:"order_count"`
|
||||
}
|
||||
|
||||
var results []UserWithOrderCount
|
||||
db.Raw(`
|
||||
SELECT u.*, COUNT(o.id) as order_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.status = ?
|
||||
GROUP BY u.id
|
||||
ORDER BY order_count DESC
|
||||
LIMIT ?
|
||||
`, 1, 10).Scan(&results)
|
||||
|
||||
// 批量更新
|
||||
db.Exec(`
|
||||
UPDATE orders
|
||||
SET status = ?
|
||||
WHERE user_id = ? AND status = ?
|
||||
`, "completed", userID, "pending")
|
||||
|
||||
// 复杂子查询
|
||||
db.Raw(`
|
||||
SELECT * FROM users
|
||||
WHERE id IN (
|
||||
SELECT user_id FROM orders
|
||||
WHERE amount > ?
|
||||
GROUP BY user_id
|
||||
HAVING COUNT(*) > ?
|
||||
)
|
||||
`, 1000, 5).Scan(&users)
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
#### 必须处理 ErrRecordNotFound
|
||||
|
||||
```go
|
||||
// DAO 层
|
||||
func (d *UserDAO) FindByID(ctx context.Context, id int64) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := d.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
return nil, err // 包含 ErrRecordNotFound
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Handler 层
|
||||
user, err := h.userService.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
common.ResponseError(c, common.CodeNotFound, "用户不存在")
|
||||
return
|
||||
}
|
||||
common.ResponseErrorWithDetail(c, common.CodeServerError, "查询失败", err)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 事务处理
|
||||
|
||||
```go
|
||||
// Service 层事务
|
||||
func (s *OrderService) CreateOrder(ctx context.Context, req *dto.CreateOrderRequest) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 创建订单
|
||||
order := &entity.Order{...}
|
||||
if err := tx.Create(order).Error; err != nil {
|
||||
return fmt.Errorf("创建订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 扣减库存
|
||||
if err := tx.Model(&entity.Product{}).
|
||||
Where("id = ? AND stock >= ?", req.ProductID, req.Quantity).
|
||||
Update("stock", gorm.Expr("stock - ?", req.Quantity)).Error; err != nil {
|
||||
return fmt.Errorf("扣减库存失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 创建支付记录
|
||||
payment := &entity.Payment{...}
|
||||
if err := tx.Create(payment).Error; err != nil {
|
||||
return fmt.Errorf("创建支付记录失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Context 传递
|
||||
|
||||
```go
|
||||
// 必须使用 WithContext 传递上下文
|
||||
db.WithContext(ctx).First(&user, id)
|
||||
db.WithContext(ctx).Create(&order)
|
||||
|
||||
// 支持超时控制和取消
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
db.WithContext(ctx).Find(&users)
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
# 日志规范
|
||||
|
||||
## 指定框架
|
||||
|
||||
项目统一使用内部日志库:`rmdc-common/wdd_log/log_utils.go`
|
||||
|
||||
## 日志级别定义
|
||||
|
||||
### Debug
|
||||
- **用途**:开发调试,记录程序执行流程、变量值等详细信息
|
||||
- **场景**:默认开发日志级别
|
||||
- **示例**:
|
||||
```go
|
||||
log.Debug(ctx, "开始处理用户请求", map[string]interface{}{
|
||||
"userID": userID,
|
||||
"requestID": requestID,
|
||||
})
|
||||
```
|
||||
|
||||
### Info
|
||||
- **用途**:记录关键业务操作节点
|
||||
- **场景**:用户登录、订单创建、支付成功等关键业务
|
||||
- **示例**:
|
||||
```go
|
||||
log.Info(ctx, "用户登录成功", map[string]interface{}{
|
||||
"userID": user.ID,
|
||||
"username": user.Username,
|
||||
"ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
log.Info(ctx, "订单创建成功", map[string]interface{}{
|
||||
"orderID": order.ID,
|
||||
"amount": order.Amount,
|
||||
"userID": order.UserID,
|
||||
})
|
||||
```
|
||||
|
||||
### Warning
|
||||
- **用途**:记录可预期的、非致命的异常情况,程序仍可继续运行
|
||||
- **场景**:外部 API 超时启用备用方案、配置缺失使用默认值等
|
||||
- **示例**:
|
||||
```go
|
||||
log.Warning(ctx, "外部API调用超时,已启用备用方案", map[string]interface{}{
|
||||
"api": "payment-gateway",
|
||||
"timeout": "5s",
|
||||
"fallback": "local-cache",
|
||||
})
|
||||
```
|
||||
|
||||
### Error
|
||||
- **用途**:记录严重错误,导致当前业务流程无法继续
|
||||
- **场景**:数据库连接失败、关键参数校验失败等
|
||||
- **要求**:必须详细记录错误信息和堆栈
|
||||
- **示例**:
|
||||
```go
|
||||
log.Error(ctx, "数据库连接失败", map[string]interface{}{
|
||||
"host": dbConfig.Host,
|
||||
"port": dbConfig.Port,
|
||||
"error": err.Error(),
|
||||
"stack": debug.Stack(),
|
||||
})
|
||||
```
|
||||
|
||||
## 日志内容规范
|
||||
|
||||
### 必须包含
|
||||
1. **TraceID** - 请求追踪 ID
|
||||
2. **UserID** - 用户标识(如适用)
|
||||
3. **操作描述** - 简练的中文描述
|
||||
4. **关键参数** - 与操作相关的关键数据
|
||||
|
||||
### 格式要求
|
||||
```go
|
||||
log.Info(ctx, "操作描述", map[string]interface{}{
|
||||
"key1": value1,
|
||||
"key2": value2,
|
||||
})
|
||||
```
|
||||
|
||||
## 各层日志职责
|
||||
|
||||
### Handler 层
|
||||
- 使用 `ResponseErrorWithDetail` 自动记录 Error 日志
|
||||
- 一般不主动记录日志
|
||||
|
||||
### Service 层
|
||||
- **Info**:关键业务操作成功(创建订单、支付、用户注册等)
|
||||
- **Warning**:业务逻辑异常但可处理
|
||||
- **Error**:通过 ResponseErrorWithDetail 在 Handler 层统一记录
|
||||
|
||||
### DAO 层
|
||||
- 一般不记录日志
|
||||
- 错误向上抛出,由 Handler 层统一处理
|
||||
|
||||
## 禁止事项
|
||||
|
||||
1. 禁止在日志中记录敏感信息(密码、Token、完整银行卡号等)
|
||||
2. 禁止使用 `fmt.Println` 或 `log.Println`
|
||||
3. 禁止在循环中大量记录日志
|
||||
4. Error 日志禁止缺少堆栈信息
|
||||
@@ -1,39 +0,0 @@
|
||||
# 项目目录结构规范
|
||||
|
||||
## 核心目录
|
||||
|
||||
| 目录 | 职责 | 禁止事项 |
|
||||
|------|------|----------|
|
||||
| `/api` 或 `/internal/handler` | GIN Handler 层,解析请求、调用 service、返回响应 | 禁止写业务逻辑 |
|
||||
| `/internal/service` | 业务逻辑核心,编排 dao 完成功能 | - |
|
||||
| `/internal/dao` 或 `/internal/repository` | 数据访问层,封装 GORM 操作 | 禁止引用 service |
|
||||
| `/internal/model/entity` | 数据库表结构对应的持久化对象 | - |
|
||||
| `/internal/model/dto` | API 数据传输对象(请求/响应) | - |
|
||||
| `/pkg/common` | 统一响应、错误码、公共工具 | - |
|
||||
| `/configs` | 配置文件 | - |
|
||||
| `/cmd` | main.go 入口 | - |
|
||||
|
||||
## 依赖规则
|
||||
|
||||
```
|
||||
handler → service → dao
|
||||
↓ ↓ ↓
|
||||
pkg/common (任意层可引用)
|
||||
```
|
||||
|
||||
**严禁反向或跨层依赖**
|
||||
|
||||
## go.mod 内部模块引用
|
||||
|
||||
```go
|
||||
module my-project
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
wdd.io/TonyCommon v1.0.0
|
||||
)
|
||||
|
||||
// 本地开发使用 replace
|
||||
replace wdd.io/TonyCommon => ../TonyCommon
|
||||
```
|
||||
@@ -1,120 +0,0 @@
|
||||
# 时间处理规范
|
||||
|
||||
## 核心原则
|
||||
|
||||
所有在前端和后端之间传输、以及在数据库中存储的时间,**必须统一为东八区时间 (Asia/Shanghai, UTC+8)**。
|
||||
|
||||
## 指定工具库
|
||||
|
||||
| 端 | 工具库路径 |
|
||||
|----|-----------|
|
||||
| 后端 | `rmdc-common/utils/TimeUtils.go` |
|
||||
| 前端 | `TonyMask/src/utils/timeUtils.ts` |
|
||||
|
||||
## 时间格式
|
||||
|
||||
- API 响应中的 `timestamp` 字段统一使用 **RFC3339** 格式
|
||||
- 示例:`2024-01-15T14:30:00+08:00`
|
||||
|
||||
## 禁止与必须
|
||||
|
||||
### 禁止直接使用
|
||||
|
||||
```go
|
||||
// ❌ 禁止
|
||||
time.Now()
|
||||
time.Parse(layout, value)
|
||||
t.Format(layout)
|
||||
```
|
||||
|
||||
### 必须使用工具库
|
||||
|
||||
```go
|
||||
// ✅ 正确
|
||||
TimeUtils.Now()
|
||||
TimeUtils.Parse(layout, value)
|
||||
TimeUtils.Format(t, layout)
|
||||
```
|
||||
|
||||
## 常用场景示例
|
||||
|
||||
### 获取当前时间
|
||||
|
||||
```go
|
||||
// ❌ 错误
|
||||
now := time.Now()
|
||||
|
||||
// ✅ 正确
|
||||
now := TimeUtils.Now()
|
||||
```
|
||||
|
||||
### 格式化时间戳
|
||||
|
||||
```go
|
||||
// ❌ 错误
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
|
||||
// ✅ 正确
|
||||
timestamp := TimeUtils.Now().Format(time.RFC3339)
|
||||
```
|
||||
|
||||
### 解析时间字符串
|
||||
|
||||
```go
|
||||
// ❌ 错误
|
||||
t, err := time.Parse(time.RFC3339, timeStr)
|
||||
|
||||
// ✅ 正确
|
||||
t, err := TimeUtils.Parse(time.RFC3339, timeStr)
|
||||
```
|
||||
|
||||
### 数据库时间字段
|
||||
|
||||
```go
|
||||
type Order struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"` // GORM 自动处理
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"` // GORM 自动处理
|
||||
ExpireAt time.Time // 业务时间使用 TimeUtils
|
||||
}
|
||||
|
||||
// 设置业务时间
|
||||
order.ExpireAt = TimeUtils.Now().Add(24 * time.Hour)
|
||||
```
|
||||
|
||||
### API 响应时间
|
||||
|
||||
```go
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Status int `json:"status"`
|
||||
Timestamp string `json:"timestamp"` // RFC3339 格式
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
resp := Response{
|
||||
Timestamp: TimeUtils.Now().Format(time.RFC3339),
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## TimeUtils 常用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `Now()` | 获取当前东八区时间 |
|
||||
| `Parse(layout, value)` | 解析时间字符串(东八区) |
|
||||
| `Format(t, layout)` | 格式化时间 |
|
||||
| `StartOfDay(t)` | 获取当天零点 |
|
||||
| `EndOfDay(t)` | 获取当天 23:59:59 |
|
||||
| `AddDays(t, days)` | 增加天数 |
|
||||
|
||||
## 时区配置
|
||||
|
||||
确保服务器和数据库时区配置正确:
|
||||
|
||||
```go
|
||||
// 数据库连接配置
|
||||
dsn := "user:pass@tcp(host:3306)/db?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai"
|
||||
```
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 验证 Go GIN/GORM 项目结构
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Go 项目结构验证 ==="
|
||||
|
||||
# 检查 go.mod
|
||||
if [ ! -f "go.mod" ]; then
|
||||
echo "❌ 缺少 go.mod"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ go.mod 存在"
|
||||
|
||||
# 检查核心目录
|
||||
DIRS=("internal/service" "internal/dao" "internal/model" "pkg/common")
|
||||
for dir in "${DIRS[@]}"; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "✅ $dir 存在"
|
||||
else
|
||||
echo "⚠️ $dir 不存在"
|
||||
fi
|
||||
done
|
||||
|
||||
# 检查 handler 目录(两种风格)
|
||||
if [ -d "api" ] || [ -d "internal/handler" ]; then
|
||||
echo "✅ handler 目录存在"
|
||||
else
|
||||
echo "⚠️ 缺少 api/ 或 internal/handler/"
|
||||
fi
|
||||
|
||||
# 检查反向依赖(dao 不应引用 service)
|
||||
echo ""
|
||||
echo "=== 检查依赖方向 ==="
|
||||
if grep -r "internal/service" internal/dao/ 2>/dev/null; then
|
||||
echo "❌ dao 层存在对 service 的反向依赖"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 无反向依赖"
|
||||
|
||||
# 检查 time.Now() 使用
|
||||
echo ""
|
||||
echo "=== 检查 time.Now() 使用 ==="
|
||||
if grep -rn "time\.Now()" --include="*.go" internal/ api/ 2>/dev/null | grep -v "_test.go"; then
|
||||
echo "⚠️ 发现直接使用 time.Now(),应使用 TimeUtils.Now()"
|
||||
else
|
||||
echo "✅ 无直接 time.Now() 调用"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== 验证完成 ==="
|
||||
91
1-AgentSkills/coding-go-gin-gorm/SKILL.md
Normal file
91
1-AgentSkills/coding-go-gin-gorm/SKILL.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: developing-go-gin-gorm
|
||||
description: >
|
||||
用于 create / modify / review production-ready 的 Go Gin/GORM backend code。该 skill 强制执行
|
||||
POST + JSON RequestBody API、handler-service-dao layering、自包含 common runtime、AppError error mapping、
|
||||
single-DB / multi-DB design、Unit of Work / TxDAO transaction pattern、Asia/Shanghai time handling、
|
||||
structured logging、JWT / security / audit middleware 以及 cross-platform validation。
|
||||
---
|
||||
|
||||
# Go Gin/GORM 工程化开发 Skill
|
||||
|
||||
## 核心工作流
|
||||
|
||||
1. 先阅读目标模块,再动手修改。识别现有的 handler、service、DAO、DTO、entity、middleware、config 和数据库连接方式。
|
||||
2. 如果目标项目没有本地 common 基础能力,从 `reference/common-runtime.md` 脚手架化生成;不要依赖外部 common 仓库或某台机器上的特定目录。
|
||||
3. 按 `handler -> service -> dao` 分层新增或修改代码,DTO 与 entity 必须分离。
|
||||
4. 所有业务 API 统一设计为 `POST + JSON RequestBody`;不新增 GET、PUT、PATCH、DELETE、Path Param 或 Query Param 风格业务接口。
|
||||
5. DAO 返回底层错误,Service 将基础设施错误转换为领域错误或 `AppError`;Handler 只负责把 `AppError` 映射成统一响应,不能感知 GORM 细节。
|
||||
6. 根据模块实际情况选择单数据库或多数据库设计;涉及事务的写操作必须使用 Unit of Work / TxDAO 模式。
|
||||
7. 时间和日志只使用 skill 内定义的 common 运行时规范;时间固定为 `Asia/Shanghai`,API 时间戳使用 RFC3339 且带 `+08:00` 偏移。
|
||||
8. 受保护 API 必须接入安全与审计规则,包括 JWT claims、管理员校验、权限中间件、限流、CORS、敏感日志脱敏和关键操作审计。
|
||||
9. 完成后运行 `scripts/validate_go_gin_gorm.py` 跨平台验证器;当目标项目可本地编译时,再运行 `gofmt` 和 `go test ./...`。
|
||||
|
||||
## 强制规则
|
||||
|
||||
- API 路由必须使用 `POST("/xxx/action", handler.Method)`,请求参数必须使用 `ShouldBindJSON`。
|
||||
- Handler 禁止调用 `c.JSON`、`AbortWithStatusJSON`、`c.Param`、`c.Query`、`ShouldBindQuery`,也禁止 import `gorm`。
|
||||
- Handler 禁止承载业务规则、权限判断细节、事务边界和数据库访问。
|
||||
- Service 负责业务编排、领域校验、错误映射、事务边界和关键业务日志。
|
||||
- DAO 负责所有 GORM 操作,并且每个 GORM 调用必须使用 `WithContext(ctx)`。
|
||||
- DAO 返回底层错误;Service 负责转换为 `common.AppError` 或明确的领域错误。
|
||||
- 需要参与事务的 DAO 方法必须接收 `tx *gorm.DB`;同一个事务内的所有 DAO 调用必须使用同一个 `tx`。
|
||||
- DAO 禁止主动开启事务;事务只能由 Service 通过 Unit of Work 开启。
|
||||
- 时间必须使用 `common.Now()`、`common.ParseTime()`、`common.FormatTime()`;除 common 时间运行时外,禁止直接使用 `time.Now()` 和 `time.Parse()`。
|
||||
- 日志必须使用 `common.Debug/Info/Warn/Error`;禁止打印密码、Token、私钥、Secret、完整 Authorization Header 等敏感信息。
|
||||
- Go 导出标识符和非显然业务逻辑必须写有价值的中文注释。
|
||||
- 代码优先清晰、小而稳;只有在能消除真实复杂度或保护真实边界时才引入设计模式。
|
||||
|
||||
## 实现原则
|
||||
|
||||
- 以第一性原理为基础,优先解决真实业务流程、数据一致性、安全风险、可观测性缺口和核心工程问题。
|
||||
- 发挥资深架构师经验,优化系统的可维护性、可扩展性、可观测性、稳定性、安全性和工程落地成本。
|
||||
- 避免炫技式设计、性能表演、过早抽象和没有业务收益的复杂化。
|
||||
- 优先使用显式 DTO、显式错误映射、显式事务所有权和显式业务边界日志。
|
||||
- 只有当当前代码路径、数据量或故障模式证明成本真实存在时,才做针对性优化。
|
||||
|
||||
## 设计模式使用准则
|
||||
|
||||
- 使用分层架构约束 handler、service、dao 的依赖方向。
|
||||
- 使用 DTO + Mapper 防止数据库 entity 泄漏到 API 合约。
|
||||
- 使用 `AppError` 作为 Service 暴露给 Handler 的稳定错误契约。
|
||||
- 使用 Unit of Work 管理 Service 层事务边界。
|
||||
- 使用 TxDAO 模式,将 `tx *gorm.DB` 传入事务内 DAO 方法。
|
||||
- Repository/DAO 只处理持久化访问,不承载业务决策。
|
||||
- Handler、Service、DAO、DB Manager、Middleware 使用构造函数/工厂函数创建。
|
||||
- 只有存在多个真实可替换算法时才使用 Strategy。
|
||||
- 外部系统对接,例如 CI、支付、身份、对象存储,优先使用 Adapter 隔离边界。
|
||||
- Auth、Admin、Permission、RequestID、Audit、RateLimit、CORS、Recovery 等横切能力使用 Middleware / Decorator 风格组合。
|
||||
- 不为了设计模式而设计;只有一个实现、没有边界收益的模式不要引入。
|
||||
|
||||
## 参考资料加载规则
|
||||
|
||||
| 场景 | 读取文件 |
|
||||
|------|----------|
|
||||
| 本地 common 运行时:统一响应、错误码、AppError、时间、日志、请求上下文 | `reference/common-runtime.md` |
|
||||
| 编码规范、注释、命名、错误处理、DTO/entity 分离 | `reference/coding-standards.md` |
|
||||
| API 路由、DTO、分页、响应规范 | `reference/api-design-spec.md`, `reference/api-response-spec.md` |
|
||||
| 单数据库、多数据库、Unit of Work、TxDAO、GORM 规则 | `reference/database-patterns.md`, `reference/framework-usage.md` |
|
||||
| 日志分级、debug 模式、敏感字段脱敏 | `reference/logging-standards.md` |
|
||||
| 东八区时间处理 | `reference/time-handling.md` |
|
||||
| JWT、权限、管理员校验、审计、CORS、限流、安全规则 | `reference/security-audit.md` |
|
||||
| 目录结构和依赖边界 | `reference/project-structure.md` |
|
||||
| 代码示例 | `examples/*.go` |
|
||||
| 跨平台验证 | `scripts/validate_go_gin_gorm.py` |
|
||||
|
||||
> 错误码与 `AppError` 的唯一规范来源是 `reference/common-runtime.md`。不要再维护独立的 `error-codes.go` reference,避免双源漂移。
|
||||
|
||||
## 完成检查清单
|
||||
|
||||
- [ ] 所有业务 API 都是 POST + RequestBody。
|
||||
- [ ] Handler 只做 DTO 绑定、Service 调用和 common 统一响应。
|
||||
- [ ] Handler 没有 GORM import,也不检查 GORM 错误。
|
||||
- [ ] Service 将 DAO/基础设施错误映射为 `common.AppError`。
|
||||
- [ ] DAO 方法使用 `WithContext(ctx)` 并返回底层错误。
|
||||
- [ ] 事务使用 Unit of Work,并向所有参与事务的 DAO 方法传递同一个 `tx`。
|
||||
- [ ] 单数据库或多数据库归属明确。
|
||||
- [ ] 时间使用 `common.Now/ParseTime/FormatTime`。
|
||||
- [ ] 日志使用 common 结构化日志,并对敏感字段脱敏。
|
||||
- [ ] 需要保护的接口已接入 Auth/Admin/Permission/Audit/RateLimit/CORS 等规则。
|
||||
- [ ] `python scripts/validate_go_gin_gorm.py <project-root>` 通过。
|
||||
- [ ] `gofmt` 和 `go test ./...` 通过;如果因环境限制不能运行,需要明确说明。
|
||||
73
1-AgentSkills/coding-go-gin-gorm/examples/dao-example.go
Normal file
73
1-AgentSkills/coding-go-gin-gorm/examples/dao-example.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"my-project/internal/model/dto"
|
||||
"my-project/internal/model/entity"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserDAO 用户数据访问对象。DAO 只封装 GORM 操作,不做业务决策。
|
||||
type UserDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserDAO 创建用户 DAO 实例。
|
||||
func NewUserDAO(db *gorm.DB) *UserDAO {
|
||||
return &UserDAO{db: db}
|
||||
}
|
||||
|
||||
func (d *UserDAO) session(ctx context.Context, tx *gorm.DB) *gorm.DB {
|
||||
if tx != nil {
|
||||
return tx.WithContext(ctx)
|
||||
}
|
||||
return d.db.WithContext(ctx)
|
||||
}
|
||||
|
||||
// FindByID 根据用户 ID 查询用户。未找到时返回 GORM 原始错误。
|
||||
func (d *UserDAO) FindByID(ctx context.Context, tx *gorm.DB, userID int64) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := d.session(ctx, tx).First(&user, "id = ?", userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// ExistsByUsername 判断用户名是否已存在。
|
||||
func (d *UserDAO) ExistsByUsername(ctx context.Context, tx *gorm.DB, username string) (bool, error) {
|
||||
var count int64
|
||||
if err := d.session(ctx, tx).Model(&entity.User{}).Where("username = ?", username).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// Create 创建用户。事务内调用必须传入 tx。
|
||||
func (d *UserDAO) Create(ctx context.Context, tx *gorm.DB, user *entity.User) error {
|
||||
return d.session(ctx, tx).Create(user).Error
|
||||
}
|
||||
|
||||
// List 分页查询用户列表。
|
||||
func (d *UserDAO) List(ctx context.Context, tx *gorm.DB, req *dto.ListUsersRequest) ([]*entity.User, int64, error) {
|
||||
query := d.session(ctx, tx).Model(&entity.User{})
|
||||
if req.Keyword != "" {
|
||||
query = query.Where("username LIKE ?", "%"+req.Keyword+"%")
|
||||
}
|
||||
if req.Status != "" {
|
||||
query = query.Where("status = ?", req.Status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var users []*entity.User
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Limit(req.PageSize).Offset(offset).Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return users, total, nil
|
||||
}
|
||||
62
1-AgentSkills/coding-go-gin-gorm/examples/handler-example.go
Normal file
62
1-AgentSkills/coding-go-gin-gorm/examples/handler-example.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"my-project/internal/common"
|
||||
"my-project/internal/model/dto"
|
||||
"my-project/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserHandler 用户相关 API 处理器。
|
||||
type UserHandler struct {
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户 Handler 实例。
|
||||
func NewUserHandler(userService *service.UserService) *UserHandler {
|
||||
return &UserHandler{userService: userService}
|
||||
}
|
||||
|
||||
// RegisterUserRoutes 注册用户路由。业务 API 强制使用 POST + RequestBody。
|
||||
func RegisterUserRoutes(group *gin.RouterGroup, h *UserHandler) {
|
||||
users := group.Group("/users")
|
||||
{
|
||||
users.POST("/detail", h.GetUserDetail)
|
||||
users.POST("/create", h.CreateUser)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserDetail 根据请求体中的用户 ID 获取用户详情。
|
||||
func (h *UserHandler) GetUserDetail(c *gin.Context) {
|
||||
var req dto.GetUserDetailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ResponseAppError(c, common.WrapAppError(common.CodeParamError, "请求参数错误", err))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.userService.GetUserDetail(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
common.ResponseAppError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
common.ResponseSuccess(c, resp)
|
||||
}
|
||||
|
||||
// CreateUser 创建用户。
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ResponseAppError(c, common.WrapAppError(common.CodeParamError, "请求参数错误", err))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.userService.CreateUser(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
common.ResponseAppError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
common.ResponseSuccessWithMessage(c, resp, "用户创建成功")
|
||||
}
|
||||
79
1-AgentSkills/coding-go-gin-gorm/examples/service-example.go
Normal file
79
1-AgentSkills/coding-go-gin-gorm/examples/service-example.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"my-project/internal/common"
|
||||
"my-project/internal/dao"
|
||||
"my-project/internal/model/dto"
|
||||
"my-project/internal/model/entity"
|
||||
"my-project/internal/model/mapper"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UnitOfWork 定义 service 使用的事务边界。
|
||||
type UnitOfWork interface {
|
||||
Transaction(ctx context.Context, fn func(tx *gorm.DB) error) error
|
||||
}
|
||||
|
||||
// UserService 用户业务服务。
|
||||
type UserService struct {
|
||||
uow UnitOfWork
|
||||
userDAO *dao.UserDAO
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务实例。
|
||||
func NewUserService(uow UnitOfWork, userDAO *dao.UserDAO) *UserService {
|
||||
return &UserService{uow: uow, userDAO: userDAO}
|
||||
}
|
||||
|
||||
// GetUserDetail 根据用户 ID 获取用户详情。
|
||||
func (s *UserService) GetUserDetail(ctx context.Context, req *dto.GetUserDetailRequest) (*dto.UserDetailResponse, error) {
|
||||
user, err := s.userDAO.FindByID(ctx, nil, req.UserID)
|
||||
if err != nil {
|
||||
return nil, mapUserLookupError(err)
|
||||
}
|
||||
return mapper.ToUserDetailResponse(user), nil
|
||||
}
|
||||
|
||||
// CreateUser 创建用户并通过 UnitOfWork 保证事务一致性。
|
||||
func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*dto.UserDetailResponse, error) {
|
||||
var created *entity.User
|
||||
|
||||
err := s.uow.Transaction(ctx, func(tx *gorm.DB) error {
|
||||
exists, err := s.userDAO.ExistsByUsername(ctx, tx, req.Username)
|
||||
if err != nil {
|
||||
return common.WrapAppError(common.CodeServerError, "检查用户名失败", err)
|
||||
}
|
||||
if exists {
|
||||
return common.NewAppError(common.CodeDuplicate, "用户名已存在")
|
||||
}
|
||||
|
||||
user := &entity.User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
CreatedAt: common.Now(),
|
||||
UpdatedAt: common.Now(),
|
||||
}
|
||||
if err := s.userDAO.Create(ctx, tx, user); err != nil {
|
||||
return common.WrapAppError(common.CodeServerError, "创建用户失败", err)
|
||||
}
|
||||
created = user
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
common.Info(ctx, "用户创建成功", "user_id", created.ID, "username", created.Username)
|
||||
return mapper.ToUserDetailResponse(created), nil
|
||||
}
|
||||
|
||||
func mapUserLookupError(err error) error {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return common.WrapAppError(common.CodeNotFound, "用户不存在", err)
|
||||
}
|
||||
return common.WrapAppError(common.CodeServerError, "查询用户失败", err)
|
||||
}
|
||||
125
1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md
Normal file
125
1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# API 设计规范
|
||||
|
||||
所有业务 API 必须使用 `POST + JSON RequestBody`。这是生成代码的硬性规则。
|
||||
|
||||
## 路由规则
|
||||
|
||||
- 列表、详情、创建、更新、删除、同步、触发、导出、内部检查等接口都使用 `POST`。
|
||||
- 所有业务参数都放在 JSON request body 中。
|
||||
- 禁止使用 path variables。
|
||||
- 禁止使用 query parameters。
|
||||
- 禁止使用 `ShouldBindQuery`。
|
||||
- 路由名称保持 action-oriented,并保证语义稳定。
|
||||
|
||||
| 操作 | 路由后缀 | 示例 |
|
||||
|------|----------|------|
|
||||
| 列表 | `/list` | `/api/users/list` |
|
||||
| 详情 | `/detail` | `/api/users/detail` |
|
||||
| 创建 | `/create` | `/api/users/create` |
|
||||
| 更新 | `/update` | `/api/users/update` |
|
||||
| 删除 | `/delete` | `/api/users/delete` |
|
||||
| 同步 | `/sync` | `/api/ci/resources/sync` |
|
||||
| 触发 | `/trigger` | `/api/builds/trigger` |
|
||||
| 导出 | `/export` | `/api/audit/logs/export` |
|
||||
| 权限检查 | `/check` | `/api/permissions/check` |
|
||||
|
||||
## 路由注册
|
||||
|
||||
```go
|
||||
func RegisterUserRoutes(group *gin.RouterGroup, h *UserHandler) {
|
||||
users := group.Group("/users")
|
||||
{
|
||||
users.POST("/list", h.ListUsers)
|
||||
users.POST("/detail", h.GetUserDetail)
|
||||
users.POST("/create", h.CreateUser)
|
||||
users.POST("/update", h.UpdateUser)
|
||||
users.POST("/delete", h.DeleteUser)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DTO 规则
|
||||
|
||||
```go
|
||||
type PageRequest struct {
|
||||
Page int `json:"page" binding:"required,min=1"`
|
||||
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
type ListUsersRequest struct {
|
||||
PageRequest
|
||||
Keyword string `json:"keyword,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type GetUserDetailRequest struct {
|
||||
UserID int64 `json:"user_id" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
UserID int64 `json:"user_id" binding:"required,min=1"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type DeleteUserRequest struct {
|
||||
UserID int64 `json:"user_id" binding:"required,min=1"`
|
||||
}
|
||||
```
|
||||
|
||||
DTO 命名规则:
|
||||
|
||||
| 类型 | 命名 |
|
||||
|------|------|
|
||||
| 列表请求 | `List{Resource}Request` |
|
||||
| 详情请求 | `Get{Resource}DetailRequest` |
|
||||
| 创建请求 | `Create{Resource}Request` |
|
||||
| 更新请求 | `Update{Resource}Request` |
|
||||
| 删除请求 | `Delete{Resource}Request` |
|
||||
| 列表响应 | `List{Resource}Response` |
|
||||
| 详情响应 | `{Resource}DetailResponse` |
|
||||
|
||||
## Handler 模板
|
||||
|
||||
```go
|
||||
func (h *UserHandler) GetUserDetail(c *gin.Context) {
|
||||
var req dto.GetUserDetailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ResponseAppError(c, common.WrapAppError(common.CodeParamError, "请求参数错误", err))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.userService.GetUserDetail(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
common.ResponseAppError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
common.ResponseSuccess(c, resp)
|
||||
}
|
||||
```
|
||||
|
||||
## 分页响应
|
||||
|
||||
优先使用字段明确的类型化响应;通用工具或临时场景可以使用 `common.PageResponse`。
|
||||
|
||||
```go
|
||||
type ListUsersResponse struct {
|
||||
List []*UserDTO `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
```
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- 敏感值必须只放在 request body 中,并且仍然禁止写入日志。
|
||||
- 参数绑定和校验错误映射为 `CodeParamError` 或 `CodeValidationFail`。
|
||||
- Auth、Admin、Permission 中间件禁止从路径参数或查询参数中提取业务资源标识。
|
||||
- 删除接口在需要业务恢复或审计追溯时优先使用软删除。
|
||||
@@ -0,0 +1,64 @@
|
||||
# API 响应规范
|
||||
|
||||
所有 Handler 必须使用 `reference/common-runtime.md` 中定义的本地 common 响应函数。
|
||||
|
||||
## 响应体
|
||||
|
||||
```go
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 成功响应使用 `CodeSuccess`,并保持 `message=success`。
|
||||
- 失败响应使用非 0 业务码,且 `data=null`。
|
||||
- `timestamp` 使用 Asia/Shanghai RFC3339,例如 `2026-06-29T10:30:00+08:00`。
|
||||
- `request_id` 必须来自请求上下文或 RequestID 中间件。
|
||||
- Handler 通过 `ResponseAppError(c, err)` 返回错误。
|
||||
|
||||
## 响应函数
|
||||
|
||||
```go
|
||||
common.ResponseSuccess(c, data)
|
||||
common.ResponseSuccessWithMessage(c, data, "创建成功")
|
||||
common.ResponseError(c, common.CodeParamError, "请求参数错误")
|
||||
common.ResponseAppError(c, err)
|
||||
```
|
||||
|
||||
业务 Handler 禁止直接调用 Gin 原生响应方法。
|
||||
|
||||
## 错误映射
|
||||
|
||||
| 错误来源 | 映射责任方 | 响应码 |
|
||||
|----------|------------|--------|
|
||||
| JSON 绑定错误 | Handler | `CodeParamError` 或 `CodeValidationFail` |
|
||||
| GORM 未找到记录 | Service | `CodeNotFound` |
|
||||
| 唯一键冲突 | Service | `CodeDuplicate` |
|
||||
| 领域校验失败 | Service | `CodeValidationFail` 或 `CodeBusinessError` |
|
||||
| 权限不足 | Middleware/Service | `CodeForbidden` |
|
||||
| 外部 API 失败 | Service/Adapter | `CodeExternalAPIError` |
|
||||
| 未知基础设施失败 | Service 或 `ToAppError` fallback | `CodeServerError` |
|
||||
|
||||
## 分页结构
|
||||
|
||||
```go
|
||||
type PageRequest struct {
|
||||
Page int `json:"page" binding:"required,min=1"`
|
||||
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
type PageResponse struct {
|
||||
List any `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
```
|
||||
|
||||
能明确响应结构时优先使用类型化列表响应;通用工具场景再使用 `PageResponse`。
|
||||
@@ -0,0 +1,57 @@
|
||||
# 编码规范
|
||||
|
||||
## 命名规范
|
||||
|
||||
| 对象 | 规则 | 示例 |
|
||||
|------|------|------|
|
||||
| package | 小写、短名、不使用下划线 | `service`, `dao`, `common` |
|
||||
| 导出标识符 | PascalCase,并提供有价值的中文注释 | `CreateUser` |
|
||||
| 非导出标识符 | camelCase | `mapUserError` |
|
||||
| 请求 DTO | `{Action}{Resource}Request` | `CreateUserRequest` |
|
||||
| 响应 DTO | `{Resource}{Shape}Response` | `UserDetailResponse` |
|
||||
| DAO | `{Resource}DAO` | `UserDAO` |
|
||||
| Service | `{Resource}Service` | `UserService` |
|
||||
|
||||
## 注释规范
|
||||
|
||||
- 导出的 type、func、method 必须有中文注释。
|
||||
- 注释应解释意图、边界或业务含义。
|
||||
- 避免只复述代码行为的空注释。
|
||||
- 非显然的校验、事务、审计、补偿逻辑前应添加简短说明。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 禁止用 `_ = err` 丢弃错误。
|
||||
- Service 包装基础设施错误时必须补充业务上下文。
|
||||
- Handler 的参数绑定错误转换为 `common.AppError`。
|
||||
- DAO 返回的错误由 Service 转换为 `common.AppError`。
|
||||
- Handler 禁止检查 GORM 错误。
|
||||
|
||||
## DTO 与 Entity 分离
|
||||
|
||||
- Request DTO 承载客户端输入。
|
||||
- Response DTO 定义客户端输出。
|
||||
- Entity 定义数据库持久化结构。
|
||||
- Mapper 负责 entity 与 DTO 的转换。
|
||||
- 除非 entity 被明确设计为 API 合约,否则 API 禁止直接返回 entity。
|
||||
|
||||
## 实现纪律
|
||||
|
||||
- 优先解决真实业务问题。
|
||||
- 代码保持足够小,方便 review。
|
||||
- 只有存在清晰边界或重复复杂度时才添加抽象。
|
||||
- 依赖关系优先通过构造函数显式注入。
|
||||
- 除 logger 和只读配置类基础设施外,避免全局可变状态。
|
||||
- 在工作流边界打印日志,不要每行都打日志。
|
||||
- 让故障可观测:日志中应包含请求 ID、用户 ID、操作、资源 ID、耗时等关键字段。
|
||||
|
||||
## 业务代码禁用项
|
||||
|
||||
- 在 common 时间运行时之外直接使用 `time.Now()`。
|
||||
- 直接使用 `fmt.Println`、`log.Println` 或临时日志方案。
|
||||
- Handler 直接返回 Gin JSON 响应。
|
||||
- 业务 API 使用 path/query 参数绑定。
|
||||
- Service import Gin。
|
||||
- Handler import GORM。
|
||||
- DAO 禁止 import service 或 handler。
|
||||
- 用字符串拼接 SQL 并混入用户输入。
|
||||
373
1-AgentSkills/coding-go-gin-gorm/reference/common-runtime.md
Normal file
373
1-AgentSkills/coding-go-gin-gorm/reference/common-runtime.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# 自包含 Common Runtime
|
||||
|
||||
当目标项目没有等价的本地基础能力时,使用这些模板。不要依赖外部 common 仓库或某台机器上的固定路径。应用项目优先使用 `internal/common`;只有当模块确实需要向其他模块暴露这些辅助函数时,才使用 `pkg/common`。
|
||||
|
||||
## 包结构
|
||||
|
||||
```text
|
||||
internal/common/
|
||||
app_error.go
|
||||
codes.go
|
||||
response.go
|
||||
time.go
|
||||
logging.go
|
||||
request_context.go
|
||||
```
|
||||
|
||||
## 错误码与 AppError
|
||||
|
||||
```go
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
CodeSuccess = 0
|
||||
|
||||
CodeParamError = 1001
|
||||
CodeValidationFail = 1002
|
||||
CodeUnauthorized = 1003
|
||||
CodeForbidden = 1004
|
||||
CodeNotFound = 1005
|
||||
CodeTimeout = 1006
|
||||
CodeServerError = 1007
|
||||
CodeDuplicate = 1008
|
||||
CodeOperationFail = 1009
|
||||
|
||||
CodeBusinessError = 2001
|
||||
CodeDataNotReady = 2002
|
||||
CodeStatusInvalid = 2003
|
||||
CodeDependencyError = 2004
|
||||
CodeExternalAPIError = 2005
|
||||
CodeResourceLocked = 2006
|
||||
CodeQuotaExceeded = 2007
|
||||
CodeConcurrentConflict = 2008
|
||||
)
|
||||
|
||||
var CodeMessage = map[int]string{
|
||||
CodeSuccess: "success",
|
||||
CodeParamError: "参数错误",
|
||||
CodeValidationFail: "数据验证失败",
|
||||
CodeUnauthorized: "未授权,请先登录",
|
||||
CodeForbidden: "权限不足,禁止访问",
|
||||
CodeNotFound: "资源不存在",
|
||||
CodeTimeout: "请求超时",
|
||||
CodeServerError: "服务器内部错误",
|
||||
CodeDuplicate: "数据重复",
|
||||
CodeOperationFail: "操作失败",
|
||||
CodeBusinessError: "业务处理失败",
|
||||
CodeDataNotReady: "数据未就绪",
|
||||
CodeStatusInvalid: "状态不合法",
|
||||
CodeDependencyError: "依赖服务错误",
|
||||
CodeExternalAPIError: "外部服务调用失败",
|
||||
CodeResourceLocked: "资源被锁定",
|
||||
CodeQuotaExceeded: "配额超限",
|
||||
CodeConcurrentConflict: "并发冲突",
|
||||
}
|
||||
|
||||
// AppError 是 service 层向 handler 层暴露的稳定错误契约。
|
||||
// DAO 返回底层错误,service 负责转换为 AppError。
|
||||
type AppError struct {
|
||||
Code int
|
||||
Message string
|
||||
Cause error
|
||||
Fields map[string]any
|
||||
}
|
||||
|
||||
func NewAppError(code int, message string) *AppError {
|
||||
if message == "" {
|
||||
message = CodeMessage[code]
|
||||
}
|
||||
return &AppError{Code: code, Message: message}
|
||||
}
|
||||
|
||||
func WrapAppError(code int, message string, cause error) *AppError {
|
||||
appErr := NewAppError(code, message)
|
||||
appErr.Cause = cause
|
||||
return appErr
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Cause == nil {
|
||||
return fmt.Sprintf("%d:%s", e.Code, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("%d:%s: %v", e.Code, e.Message, e.Cause)
|
||||
}
|
||||
|
||||
func (e *AppError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func AsAppError(err error) (*AppError, bool) {
|
||||
var appErr *AppError
|
||||
if errors.As(err, &appErr) {
|
||||
return appErr, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func ToAppError(err error) *AppError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if appErr, ok := AsAppError(err); ok {
|
||||
return appErr
|
||||
}
|
||||
return WrapAppError(CodeServerError, CodeMessage[CodeServerError], err)
|
||||
}
|
||||
```
|
||||
|
||||
## 统一响应
|
||||
|
||||
```go
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response 是所有 API 的统一响应体。失败时 Data 必须为 nil。
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type PageResponse struct {
|
||||
List any `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
func ResponseSuccess(c *gin.Context, data any) {
|
||||
respond(c, http.StatusOK, CodeSuccess, CodeMessage[CodeSuccess], data)
|
||||
}
|
||||
|
||||
func ResponseSuccessWithMessage(c *gin.Context, data any, message string) {
|
||||
respond(c, http.StatusOK, CodeSuccess, message, data)
|
||||
}
|
||||
|
||||
func ResponseError(c *gin.Context, code int, message string) {
|
||||
respond(c, httpStatusByCode(code), code, message, nil)
|
||||
}
|
||||
|
||||
func ResponseAppError(c *gin.Context, err error) {
|
||||
appErr := ToAppError(err)
|
||||
Error(c.Request.Context(), appErr.Message, "code", appErr.Code, "error", appErr.Cause)
|
||||
respond(c, httpStatusByCode(appErr.Code), appErr.Code, appErr.Message, nil)
|
||||
}
|
||||
|
||||
func respond(c *gin.Context, httpStatus int, code int, message string, data any) {
|
||||
if message == "" {
|
||||
message = CodeMessage[code]
|
||||
}
|
||||
c.JSON(httpStatus, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
Timestamp: FormatTime(Now()),
|
||||
RequestID: RequestIDFromGin(c),
|
||||
})
|
||||
}
|
||||
|
||||
func httpStatusByCode(code int) int {
|
||||
switch code {
|
||||
case CodeUnauthorized:
|
||||
return http.StatusUnauthorized
|
||||
case CodeForbidden:
|
||||
return http.StatusForbidden
|
||||
case CodeParamError, CodeValidationFail:
|
||||
return http.StatusBadRequest
|
||||
case CodeNotFound:
|
||||
return http.StatusNotFound
|
||||
default:
|
||||
return http.StatusOK
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 请求上下文
|
||||
|
||||
```go
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
ContextKeyRequestID contextKey = "request_id"
|
||||
ContextKeyUserID contextKey = "user_id"
|
||||
ContextKeyUsername contextKey = "username"
|
||||
ContextKeyRole contextKey = "role"
|
||||
)
|
||||
|
||||
func WithRequestID(ctx context.Context, requestID string) context.Context {
|
||||
return context.WithValue(ctx, ContextKeyRequestID, requestID)
|
||||
}
|
||||
|
||||
func RequestIDFromContext(ctx context.Context) string {
|
||||
if value, ok := ctx.Value(ContextKeyRequestID).(string); ok {
|
||||
return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func RequestIDFromGin(c *gin.Context) string {
|
||||
if value, exists := c.Get(string(ContextKeyRequestID)); exists {
|
||||
if requestID, ok := value.(string); ok {
|
||||
return requestID
|
||||
}
|
||||
}
|
||||
return RequestIDFromContext(c.Request.Context())
|
||||
}
|
||||
|
||||
func UserIDFromContext(ctx context.Context) int64 {
|
||||
if value, ok := ctx.Value(ContextKeyUserID).(int64); ok {
|
||||
return value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
## Asia/Shanghai 时间
|
||||
|
||||
```go
|
||||
package common
|
||||
|
||||
import "time"
|
||||
|
||||
const TimeFormat = time.RFC3339
|
||||
|
||||
var shanghaiLocation = mustLoadShanghaiLocation()
|
||||
|
||||
func mustLoadShanghaiLocation() *time.Location {
|
||||
location, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
return time.FixedZone("Asia/Shanghai", 8*60*60)
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
// Now 返回东八区当前时间。只有 common/time.go 允许直接调用 time.Now。
|
||||
func Now() time.Time {
|
||||
return time.Now().In(shanghaiLocation)
|
||||
}
|
||||
|
||||
func FormatTime(t time.Time) string {
|
||||
return t.In(shanghaiLocation).Format(TimeFormat)
|
||||
}
|
||||
|
||||
func ParseTime(value string) (time.Time, error) {
|
||||
parsed, err := time.Parse(TimeFormat, value)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return parsed.In(shanghaiLocation), nil
|
||||
}
|
||||
|
||||
func StartOfDay(t time.Time) time.Time {
|
||||
local := t.In(shanghaiLocation)
|
||||
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, shanghaiLocation)
|
||||
}
|
||||
|
||||
func EndOfDay(t time.Time) time.Time {
|
||||
return StartOfDay(t).Add(24*time.Hour - time.Nanosecond)
|
||||
}
|
||||
```
|
||||
|
||||
## 结构化日志
|
||||
|
||||
```go
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
|
||||
func InitLogger(debug bool) {
|
||||
level := slog.LevelInfo
|
||||
if debug {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
|
||||
}
|
||||
|
||||
func Debug(ctx context.Context, message string, fields ...any) {
|
||||
logger.DebugContext(ctx, message, appendContextFields(ctx, sanitizeFields(fields)...)...)
|
||||
}
|
||||
|
||||
func Info(ctx context.Context, message string, fields ...any) {
|
||||
logger.InfoContext(ctx, message, appendContextFields(ctx, sanitizeFields(fields)...)...)
|
||||
}
|
||||
|
||||
func Warn(ctx context.Context, message string, fields ...any) {
|
||||
logger.WarnContext(ctx, message, appendContextFields(ctx, sanitizeFields(fields)...)...)
|
||||
}
|
||||
|
||||
func Error(ctx context.Context, message string, fields ...any) {
|
||||
logger.ErrorContext(ctx, message, appendContextFields(ctx, sanitizeFields(fields)...)...)
|
||||
}
|
||||
|
||||
func appendContextFields(ctx context.Context, fields ...any) []any {
|
||||
if requestID := RequestIDFromContext(ctx); requestID != "" {
|
||||
fields = append(fields, "request_id", requestID)
|
||||
}
|
||||
if userID := UserIDFromContext(ctx); userID > 0 {
|
||||
fields = append(fields, "user_id", userID)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func sanitizeFields(fields []any) []any {
|
||||
sanitized := make([]any, 0, len(fields))
|
||||
for i := 0; i < len(fields); i += 2 {
|
||||
key := fields[i]
|
||||
if i+1 >= len(fields) {
|
||||
sanitized = append(sanitized, key)
|
||||
break
|
||||
}
|
||||
value := fields[i+1]
|
||||
if isSensitiveKey(key) {
|
||||
value = "***REDACTED***"
|
||||
}
|
||||
sanitized = append(sanitized, key, value)
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func isSensitiveKey(key any) bool {
|
||||
name := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(fmt.Sprint(key), "_", "")))
|
||||
sensitive := []string{"password", "passwd", "token", "authorization", "secret", "privatekey", "apikey", "cookie"}
|
||||
for _, item := range sensitive {
|
||||
if strings.Contains(name, item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
226
1-AgentSkills/coding-go-gin-gorm/reference/database-patterns.md
Normal file
226
1-AgentSkills/coding-go-gin-gorm/reference/database-patterns.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 数据库设计模式
|
||||
|
||||
当任务涉及 GORM、DAO 设计、事务、迁移或多数据库时,读取本文件。
|
||||
|
||||
## 通用规则
|
||||
|
||||
- DAO 负责所有 GORM 调用。
|
||||
- DAO 返回底层错误,不把错误转换成响应码。
|
||||
- Service 将 DAO 错误映射为 `common.AppError` 或明确的领域错误。
|
||||
- Handler 禁止 import `gorm`,也禁止检查 `gorm.ErrRecordNotFound`。
|
||||
- 每个 GORM 操作都必须使用 `WithContext(ctx)`。
|
||||
- 可能参与事务的 DAO 方法必须接收 `tx *gorm.DB`。
|
||||
- DAO 禁止调用 `Transaction`;事务由 Service 通过 Unit of Work 开启。
|
||||
- 同一个事务工作流中禁止混用事务 DAO 调用和非事务 DAO 调用。
|
||||
- 更新操作必须显式指定字段,避免直接保存完整 request DTO。
|
||||
- 列表 API 必须使用分页限制和稳定排序。
|
||||
|
||||
## 单数据库设计
|
||||
|
||||
当模块所有数据都位于同一个物理数据库,并且一个一致性边界足够时,使用单数据库设计。
|
||||
|
||||
```text
|
||||
internal/db/
|
||||
database.go
|
||||
internal/dao/
|
||||
user_dao.go
|
||||
internal/service/
|
||||
user_service.go
|
||||
```
|
||||
|
||||
```go
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UnitOfWork 负责单数据库事务边界。
|
||||
type UnitOfWork struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUnitOfWork(db *gorm.DB) *UnitOfWork {
|
||||
return &UnitOfWork{db: db}
|
||||
}
|
||||
|
||||
func (u *UnitOfWork) DB() *gorm.DB {
|
||||
return u.db
|
||||
}
|
||||
|
||||
func (u *UnitOfWork) Transaction(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return u.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return fn(tx)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Service 构造函数接收 `*db.UnitOfWork` 和需要的 DAO。非事务读取传入 `nil` tx;事务写入必须把 Unit of Work 提供的 `tx` 传给每个 DAO 方法。
|
||||
|
||||
## 多数据库设计
|
||||
|
||||
当模块需要读写多个物理数据库,例如 `core`、`user`、`audit`、`ci`、`billing`,使用多数据库设计。
|
||||
|
||||
```go
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DatabaseName string
|
||||
|
||||
const (
|
||||
DBCore DatabaseName = "core"
|
||||
DBUser DatabaseName = "user"
|
||||
DBAudit DatabaseName = "audit"
|
||||
DBCI DatabaseName = "ci"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
dbs map[DatabaseName]*gorm.DB
|
||||
}
|
||||
|
||||
func NewManager(dbs map[DatabaseName]*gorm.DB) *Manager {
|
||||
return &Manager{dbs: dbs}
|
||||
}
|
||||
|
||||
func (m *Manager) DB(name DatabaseName) (*gorm.DB, error) {
|
||||
db, ok := m.dbs[name]
|
||||
if !ok || db == nil {
|
||||
return nil, fmt.Errorf("database %s is not configured", name)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Transaction(ctx context.Context, name DatabaseName, fn func(tx *gorm.DB) error) error {
|
||||
db, err := m.DB(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return fn(tx)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
多数据库约束:
|
||||
|
||||
- 每个 DAO 必须在构造函数或文件注释中声明数据库归属。
|
||||
- 一个事务只能覆盖一个物理数据库。
|
||||
- 禁止用嵌套 GORM transaction 伪造跨库事务。
|
||||
- 跨库工作流应使用幂等键、outbox 表、重试、对账或显式补偿动作。
|
||||
- 除非业务明确要求审计先落库再返回成功,否则审计写入失败不应破坏主业务事务。
|
||||
|
||||
## 带 tx 参数的 DAO
|
||||
|
||||
```go
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserDAO(db *gorm.DB) *UserDAO {
|
||||
return &UserDAO{db: db}
|
||||
}
|
||||
|
||||
func (d *UserDAO) session(ctx context.Context, tx *gorm.DB) *gorm.DB {
|
||||
if tx != nil {
|
||||
return tx.WithContext(ctx)
|
||||
}
|
||||
return d.db.WithContext(ctx)
|
||||
}
|
||||
|
||||
// Create 创建用户。事务内调用必须传入 tx,非事务调用传 nil。
|
||||
func (d *UserDAO) Create(ctx context.Context, tx *gorm.DB, user *User) error {
|
||||
return d.session(ctx, tx).Create(user).Error
|
||||
}
|
||||
|
||||
// FindByID 根据用户ID查询用户。未找到时返回 GORM 原始错误。
|
||||
func (d *UserDAO) FindByID(ctx context.Context, tx *gorm.DB, userID int64) (*User, error) {
|
||||
var user User
|
||||
if err := d.session(ctx, tx).First(&user, "id = ?", userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
```
|
||||
|
||||
上方 `User` 类型代表本地 entity 类型。实际项目中 entity 应放在 `internal/model/entity`;这里为了突出 TxDAO 规则而缩短示例。
|
||||
|
||||
## Service 层事务模式
|
||||
|
||||
```go
|
||||
func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*dto.UserDTO, error) {
|
||||
var created *entity.User
|
||||
|
||||
err := s.uow.Transaction(ctx, func(tx *gorm.DB) error {
|
||||
exists, err := s.userDAO.ExistsByUsername(ctx, tx, req.Username)
|
||||
if err != nil {
|
||||
return common.WrapAppError(common.CodeServerError, "检查用户名失败", err)
|
||||
}
|
||||
if exists {
|
||||
return common.NewAppError(common.CodeDuplicate, "用户名已存在")
|
||||
}
|
||||
|
||||
user := &entity.User{Username: req.Username, Email: req.Email}
|
||||
if err := s.userDAO.Create(ctx, tx, user); err != nil {
|
||||
return common.WrapAppError(common.CodeServerError, "创建用户失败", err)
|
||||
}
|
||||
created = user
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
common.Info(ctx, "用户创建成功", "user_id", created.ID, "username", created.Username)
|
||||
return mapper.ToUserDTO(created), nil
|
||||
}
|
||||
```
|
||||
|
||||
## GORM 错误映射
|
||||
|
||||
推荐在 Service 中完成错误映射:
|
||||
|
||||
```go
|
||||
func mapUserLookupError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return common.WrapAppError(common.CodeNotFound, "用户不存在", err)
|
||||
}
|
||||
return common.WrapAppError(common.CodeServerError, "查询用户失败", err)
|
||||
}
|
||||
```
|
||||
|
||||
各层 GORM import 规则:
|
||||
|
||||
| 层级 | 是否允许 import GORM | 原因 |
|
||||
|------|----------------------|------|
|
||||
| Handler | 不允许 | Handler 只应知道 HTTP、DTO、Service、common 统一响应 |
|
||||
| Service | 有限允许 | 用于错误映射和 Unit of Work 事务类型 |
|
||||
| DAO | 允许 | DAO 拥有 GORM 持久化访问 |
|
||||
| Entity | 允许 | 用于 GORM tags |
|
||||
| Common runtime | 默认不允许 | 仅当 DB/UnitOfWork 辅助能力被明确放在此处时例外 |
|
||||
|
||||
## 数据迁移规则
|
||||
|
||||
- 禁止在请求路径中随意调用 `AutoMigrate`。
|
||||
- 数据迁移只能在启动阶段或专用迁移命令中执行。
|
||||
- 生产 schema 变更应具备幂等性,经过 review,并尽量可回滚。
|
||||
- 索引设计必须结合查询模式、唯一性约束和分页方式。
|
||||
- 大表回填必须分批执行,并保证过程可观测。
|
||||
153
1-AgentSkills/coding-go-gin-gorm/reference/framework-usage.md
Normal file
153
1-AgentSkills/coding-go-gin-gorm/reference/framework-usage.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Gin 与 GORM 使用规范
|
||||
|
||||
## Gin
|
||||
|
||||
### 路由分组
|
||||
|
||||
使用 router group 表达模块边界,并且业务路由只能注册为 `POST`。
|
||||
|
||||
```go
|
||||
func RegisterRoutes(r *gin.Engine, userHandler *UserHandler, auditHandler *AuditHandler) {
|
||||
api := r.Group("/api")
|
||||
{
|
||||
users := api.Group("/users")
|
||||
{
|
||||
users.POST("/list", userHandler.ListUsers)
|
||||
users.POST("/detail", userHandler.GetUserDetail)
|
||||
users.POST("/create", userHandler.CreateUser)
|
||||
users.POST("/update", userHandler.UpdateUser)
|
||||
users.POST("/delete", userHandler.DeleteUser)
|
||||
}
|
||||
|
||||
audit := api.Group("/audit")
|
||||
{
|
||||
audit.POST("/logs/list", auditHandler.ListLogs)
|
||||
audit.POST("/logs/export", auditHandler.ExportLogs)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Handler 规则
|
||||
|
||||
允许使用:
|
||||
|
||||
- `ShouldBindJSON`
|
||||
- `c.Request.Context()`
|
||||
- `common.ResponseSuccess`
|
||||
- `common.ResponseAppError`
|
||||
|
||||
禁止使用:
|
||||
|
||||
- Path parameters
|
||||
- Query parameters
|
||||
- 直接调用 Gin 响应方法
|
||||
- GORM imports
|
||||
- 业务决策
|
||||
- 数据库调用
|
||||
|
||||
### 中间件(Middleware)
|
||||
|
||||
横切 middleware 只注册一次,让业务 handler 保持小而清晰。
|
||||
|
||||
```go
|
||||
func SetupMiddleware(r *gin.Engine, cfg Config, auditWriter AuditWriter) {
|
||||
r.Use(common.Recovery())
|
||||
r.Use(common.RequestIDMiddleware())
|
||||
r.Use(common.CORSMiddleware(cfg.CORS))
|
||||
r.Use(common.RateLimitMiddleware(cfg.RateLimit))
|
||||
r.Use(common.AccessLogMiddleware())
|
||||
r.Use(common.AuditMiddleware(auditWriter))
|
||||
}
|
||||
```
|
||||
|
||||
受保护路由组示例:
|
||||
|
||||
```go
|
||||
users := api.Group("/users")
|
||||
users.Use(common.AuthMiddleware(jwtSecret))
|
||||
{
|
||||
users.POST("/list", userHandler.ListUsers)
|
||||
users.POST("/detail", userHandler.GetUserDetail)
|
||||
}
|
||||
|
||||
adminUsers := api.Group("/admin/users")
|
||||
adminUsers.Use(common.AuthMiddleware(jwtSecret), common.RequireAdmin())
|
||||
{
|
||||
adminUsers.POST("/create", userHandler.CreateUser)
|
||||
adminUsers.POST("/delete", userHandler.DeleteUser)
|
||||
}
|
||||
```
|
||||
|
||||
## GORM
|
||||
|
||||
### 仅 DAO 操作 GORM
|
||||
|
||||
所有 GORM 调用必须放在 DAO 文件中。Service 只有在事务类型和基础设施错误映射需要时,才有限接触 GORM。
|
||||
|
||||
```go
|
||||
func (d *UserDAO) List(ctx context.Context, tx *gorm.DB, req *dto.ListUsersRequest) ([]*entity.User, int64, error) {
|
||||
session := d.session(ctx, tx).Model(&entity.User{})
|
||||
|
||||
if req.Keyword != "" {
|
||||
session = session.Where("username ILIKE ?", "%"+req.Keyword+"%")
|
||||
}
|
||||
if req.Status != "" {
|
||||
session = session.Where("status = ?", req.Status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := session.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var users []*entity.User
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := session.Order("created_at DESC").Limit(req.PageSize).Offset(offset).Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return users, total, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 原生 SQL(Raw SQL)
|
||||
|
||||
只在 DAO 中为复杂 SQL、性能敏感查询或数据库特性使用 Raw/Exec。参数必须绑定,禁止把不可信输入拼接进 SQL。
|
||||
|
||||
```go
|
||||
func (d *UserDAO) CountActiveUsersByRole(ctx context.Context, tx *gorm.DB) ([]RoleCount, error) {
|
||||
var rows []RoleCount
|
||||
err := d.session(ctx, tx).Raw(`
|
||||
SELECT role, COUNT(*) AS count
|
||||
FROM users
|
||||
WHERE status = ?
|
||||
GROUP BY role
|
||||
`, "active").Scan(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
```
|
||||
|
||||
### 事务
|
||||
|
||||
Unit of Work 和 TxDAO 规则详见 `reference/database-patterns.md`。简版规则如下:
|
||||
|
||||
- Service 开启事务。
|
||||
- DAO 接收 `tx *gorm.DB`。
|
||||
- 事务内每个 DAO 调用都使用同一个 `tx`。
|
||||
- DAO 永远不主动开启事务。
|
||||
|
||||
### 上下文(Context)
|
||||
|
||||
每个 GORM 调用都必须使用 request context:
|
||||
|
||||
```go
|
||||
d.session(ctx, tx).Create(entity)
|
||||
d.session(ctx, tx).First(&entity.User{}, "id = ?", userID)
|
||||
```
|
||||
|
||||
### 更新规则
|
||||
|
||||
- 使用 `Updates(map[string]any{...})` 或显式更新 DTO 映射。
|
||||
- 禁止直接持久化 request DTO。
|
||||
- 局部更新禁止使用 `Save`。
|
||||
- 状态流转必须先在 Service 中校验,再交给 DAO 更新。
|
||||
@@ -0,0 +1,95 @@
|
||||
# 日志规范
|
||||
|
||||
只能使用 `reference/common-runtime.md` 中定义的本地 common 日志函数。
|
||||
|
||||
## 日志 API
|
||||
|
||||
```go
|
||||
common.InitLogger(debug)
|
||||
common.Debug(ctx, "调试信息", "key", value)
|
||||
common.Info(ctx, "业务节点", "key", value)
|
||||
common.Warn(ctx, "可预期异常", "key", value)
|
||||
common.Error(ctx, "不可恢复错误", "key", value, "error", err)
|
||||
```
|
||||
|
||||
Debug 模式:
|
||||
|
||||
- 开发环境或排障场景可以通过配置或环境变量开启 debug 模式。
|
||||
- 生产环境默认使用 info 级别。
|
||||
- Debug 日志也必须进行敏感字段脱敏。
|
||||
|
||||
## 日志级别规则
|
||||
|
||||
| 级别 | 使用场景 | 示例 |
|
||||
|------|----------|------|
|
||||
| Debug | 本地诊断和详细流程 | 解析后的筛选条件、分支决策、缓存命中/未命中 |
|
||||
| Info | 成功的重要业务事件 | 登录成功、用户创建、权限分配 |
|
||||
| Warn | 可预期但异常的情况 | 参数校验失败、权限不足、限流触发 |
|
||||
| Error | 当前工作流发生非预期失败 | 数据库错误、外部 API 失败、事务回滚 |
|
||||
|
||||
## 必要字段
|
||||
|
||||
可获得时应包含这些字段:
|
||||
|
||||
- `request_id`
|
||||
- `user_id`
|
||||
- `username`
|
||||
- `action`
|
||||
- `resource_type`
|
||||
- `resource_id`
|
||||
- `elapsed_ms`
|
||||
- `error`
|
||||
- `external_service`
|
||||
|
||||
common logger 应在可获得时自动从上下文附加请求 ID 和用户 ID。
|
||||
|
||||
## 各层日志职责
|
||||
|
||||
| 层级 | 日志职责 |
|
||||
|------|----------|
|
||||
| Handler | 通常不主动打日志,参数绑定失败通过 common response 统一处理 |
|
||||
| Middleware | 请求开始/结束、认证失败、权限拒绝、限流、审计 |
|
||||
| Service | 业务成功、业务拒绝、事务失败 |
|
||||
| DAO | 默认不打常规日志,错误向上返回 |
|
||||
| External adapter | 请求失败、超时、重试、降级兜底 |
|
||||
|
||||
## 必打日志
|
||||
|
||||
以下场景必须打印日志:
|
||||
|
||||
- 应用启动和 debug 模式状态,禁止包含 secret。
|
||||
- 数据库连接成功/失败,禁止包含密码。
|
||||
- 登录成功/失败,失败日志使用脱敏用户名。
|
||||
- 注册、改密、用户状态变更。
|
||||
- 权限分配、撤销、复制、角色变更。
|
||||
- 管理员拒绝或权限拒绝。
|
||||
- 外部调用失败,包含耗时和脱敏后的错误。
|
||||
- 批处理结果,包含聚合计数。
|
||||
- 事务回滚,包含 action 和资源标识。
|
||||
|
||||
## 敏感数据
|
||||
|
||||
禁止记录:
|
||||
|
||||
- 密码或密码 hash。
|
||||
- Token、Authorization 头、Cookie、Refresh token。
|
||||
- 私钥、Secret、API key。
|
||||
- MFA secret 或验证码。
|
||||
- 可能包含 secret 的原始请求体。
|
||||
|
||||
使用脱敏后的标识:
|
||||
|
||||
```go
|
||||
common.Warn(ctx, "用户登录失败",
|
||||
"username", maskUsername(req.Username),
|
||||
"ip", clientIP,
|
||||
"reason", "invalid_credential",
|
||||
)
|
||||
```
|
||||
|
||||
## 反模式
|
||||
|
||||
- 禁止使用 `fmt.Println`、`log.Println` 或临时全局 logger。
|
||||
- 禁止在紧密循环中逐条打印日志,应使用聚合计数。
|
||||
- 禁止每层重复记录并返回同一个错误;只在拥有有效上下文的边界打印一次。
|
||||
- 普通校验失败或权限失败不打印 stack trace。
|
||||
@@ -0,0 +1,67 @@
|
||||
# 项目结构规范
|
||||
|
||||
除非目标仓库已经有兼容的本地约定,否则优先使用以下结构。生成代码必须自包含在目标 module 内。
|
||||
|
||||
```text
|
||||
cmd/
|
||||
server/
|
||||
internal/
|
||||
common/ # response, AppError, time, logging, request context
|
||||
config/
|
||||
db/ # 单数据库 UnitOfWork 或多数据库 Manager
|
||||
handler/ # Gin handler:绑定 DTO、调用 service、返回响应
|
||||
middleware/ # auth、admin、permission、request ID、audit、rate limit、CORS
|
||||
service/ # 业务编排、AppError 映射、事务、日志
|
||||
dao/ # 仅负责 GORM 持久化
|
||||
model/
|
||||
dto/ # request/response DTO
|
||||
entity/ # GORM entity
|
||||
mapper/ # entity <-> DTO 转换
|
||||
configs/
|
||||
scripts/
|
||||
```
|
||||
|
||||
## 依赖方向
|
||||
|
||||
```text
|
||||
handler -> service -> dao -> entity
|
||||
| | |
|
||||
+----------+--------+-> common
|
||||
|
||||
middleware -> common
|
||||
middleware -> service 仅在权限/认证逻辑确实需要业务数据时允许
|
||||
mapper -> dto + entity
|
||||
```
|
||||
|
||||
禁止依赖:
|
||||
|
||||
- `dao -> service`
|
||||
- `dao -> handler`
|
||||
- `service -> handler`
|
||||
- `entity -> dto`
|
||||
- `handler -> gorm`
|
||||
- `handler -> dao`
|
||||
- `common -> project business packages`
|
||||
|
||||
## 分层职责
|
||||
|
||||
| 层级 | 负责 | 禁止负责 |
|
||||
|------|------|----------|
|
||||
| Handler | JSON 绑定、Service 调用、common 统一响应 | 业务规则、GORM、事务 |
|
||||
| Service | 业务规则、AppError 映射、事务、业务日志 | Gin context、HTTP 响应 |
|
||||
| DAO | GORM 查询和持久化 | 业务决策、响应码 |
|
||||
| Entity | 数据库结构和 GORM tags | API 展示结构 |
|
||||
| DTO | API 请求/响应结构 | 数据库 tags 和持久化行为 |
|
||||
| Mapper | Entity/DTO 转换 | 数据库调用 |
|
||||
| Common | 横切基础能力 | 领域业务行为 |
|
||||
|
||||
## 新模块检查清单
|
||||
|
||||
- 先创建 DTO,再写 handler。
|
||||
- 只有需要持久化时才创建或复用 entity。
|
||||
- 对外返回 entity 数据时必须创建 mapper。
|
||||
- 每个持久化操作都通过 DAO 方法封装。
|
||||
- Service 方法负责将 DAO 错误映射为 `common.AppError`。
|
||||
- Handler 方法只绑定 JSON 并调用 Service。
|
||||
- 只注册带 action 后缀的 `POST` 路由。
|
||||
- 为变更文件补充验证脚本覆盖。
|
||||
170
1-AgentSkills/coding-go-gin-gorm/reference/security-audit.md
Normal file
170
1-AgentSkills/coding-go-gin-gorm/reference/security-audit.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 安全与审计规则
|
||||
|
||||
认证、授权、JWT claims、管理员校验、审计日志、限流、CORS、敏感数据处理相关任务需要读取本文件。
|
||||
|
||||
## 中间件(Middleware)顺序
|
||||
|
||||
按以下顺序注册 middleware:
|
||||
|
||||
```text
|
||||
Recovery -> RequestID -> CORS -> RateLimit -> AccessLog -> Auth -> RequireAdmin/RequirePermission -> Audit
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 公开认证接口可以跳过 `Auth`,但登录/注册尝试仍应使用 RequestID、CORS、RateLimit、AccessLog 和 Audit。
|
||||
- 受保护接口必须使用 `Auth`。
|
||||
- 管理员接口必须先使用 `Auth`,再使用 `RequireAdmin`。
|
||||
- 权限接口必须先使用 `Auth`,再使用 `RequirePermission`。
|
||||
- 变更数据的接口必须创建审计事件。
|
||||
|
||||
## JWT 声明(Claims)
|
||||
|
||||
使用本地 claims 类型。原始 token 字符串不得离开认证中间件。
|
||||
|
||||
```go
|
||||
type Claims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
```
|
||||
|
||||
Auth middleware 职责:
|
||||
|
||||
- 只读取 `Authorization: Bearer <token>` 请求头。
|
||||
- 校验签名、过期时间,以及配置要求的 issuer/audience。
|
||||
- 将 `user_id`、`username`、`role` 写入 Gin 上下文和请求上下文。
|
||||
- token 缺失、格式错误、过期、非法时返回 `CodeUnauthorized`。
|
||||
- 日志只记录请求 ID、脱敏用户名、IP、路径、失败原因;禁止记录 token 内容。
|
||||
|
||||
## 管理员与权限校验
|
||||
|
||||
```go
|
||||
func RequireAdmin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, _ := c.Get("role")
|
||||
if role != "admin" && role != "superadmin" {
|
||||
common.ResponseError(c, common.CodeForbidden, "需要管理员权限")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
权限 middleware 必须:
|
||||
|
||||
- 从 JSON body DTO 或上下文读取权限输入,禁止从路径参数或查询参数读取。
|
||||
- 资源标识缺失时 fail closed。
|
||||
- 访问被拒绝时返回 `CodeForbidden`。
|
||||
- 使用 warn 级别记录拒绝日志,字段包含 `user_id`、`resource_type`、`resource_id`、`action`、`request_id`。
|
||||
|
||||
## 敏感数据脱敏
|
||||
|
||||
禁止记录以下值:
|
||||
|
||||
- 密码、密码 hash、旧密码、新密码
|
||||
- Token、刷新 token、authorization 请求头、cookie
|
||||
- 私钥、secret、API key、access key
|
||||
- MFA secret、验证码
|
||||
- 非必要场景下的完整手机号或邮箱
|
||||
|
||||
推荐日志字段:
|
||||
|
||||
```go
|
||||
common.Info(ctx, "用户登录成功", "user_id", user.ID, "username", user.Username, "ip", clientIP)
|
||||
common.Warn(ctx, "用户登录失败", "username", maskUsername(req.Username), "ip", clientIP, "reason", "invalid_password")
|
||||
```
|
||||
|
||||
## 审计事件
|
||||
|
||||
以下操作必须审计:
|
||||
|
||||
| 分类 | 事件 |
|
||||
|------|------|
|
||||
| 认证 | 登录成功、登录失败、退出登录、token 刷新、密码修改 |
|
||||
| 用户 | 注册、创建用户、更新用户、禁用用户、删除用户 |
|
||||
| 权限 | 分配权限、复制权限、撤销权限、角色变更 |
|
||||
| 安全 | 管理员访问拒绝、权限拒绝、限流触发 |
|
||||
| 外部影响 | 触发构建、部署、同步、导出、删除远端资源 |
|
||||
|
||||
审计记录最小字段:
|
||||
|
||||
```go
|
||||
type AuditEvent struct {
|
||||
RequestID string
|
||||
UserID int64
|
||||
Username string
|
||||
Action string
|
||||
ResourceType string
|
||||
ResourceID string
|
||||
Result string
|
||||
Reason string
|
||||
IP string
|
||||
UserAgent string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
```
|
||||
|
||||
审计规则:
|
||||
|
||||
- 审计记录不得包含密码、token、私钥或请求体 secret。
|
||||
- 用户 lookup 前发生登录失败时,记录脱敏用户名和 IP。
|
||||
- 权限变更必须记录操作者、目标用户、资源、可获得的旧值和新值。
|
||||
- 审计写入失败时必须记录 error;是否中断请求由具体业务域决定。
|
||||
|
||||
## 限流
|
||||
|
||||
至少为以下接口增加限流:
|
||||
|
||||
- 登录/注册/重置密码接口
|
||||
- Token refresh 接口
|
||||
- 导出接口
|
||||
- 高成本列表/搜索接口
|
||||
- 外部触发类接口
|
||||
|
||||
默认建议:
|
||||
|
||||
| API 类型 | 默认值 |
|
||||
|----------|--------|
|
||||
| 登录 | 每 IP + 每用户名每分钟 5 次 |
|
||||
| 注册 | 每 IP 每分钟 10 次 |
|
||||
| 导出 | 每用户每分钟 3 次 |
|
||||
| 外部触发 | 每用户每分钟 10 次 |
|
||||
| 列表/搜索 | 每用户每分钟 60 次 |
|
||||
|
||||
单实例可以使用内存限流;多实例必须使用 Redis 或其他共享存储。
|
||||
|
||||
## CORS
|
||||
|
||||
CORS 规则:
|
||||
|
||||
- 生产环境禁止使用 wildcard origin。
|
||||
- 只允许配置中的前端来源。
|
||||
- 只允许必要方法;业务 API 应只允许 `POST` 和 `OPTIONS`。
|
||||
- 允许 `Authorization`、`Content-Type` 和请求 ID 请求头。
|
||||
- 除非明确使用 cookie,否则保持凭证携带能力关闭。
|
||||
|
||||
## 密码与 Token 处理
|
||||
|
||||
- 使用 bcrypt、argon2 或项目批准的密码哈希函数存储密码,禁止明文存储。
|
||||
- 使用库提供的恒定时间比较能力校验密码 hash。
|
||||
- JWT signing secret 必须放在代码和示例配置之外。
|
||||
- Secret rotate 必须有明确运维方案。
|
||||
- 登录失败时不要暴露到底是用户名错误还是密码错误,返回统一认证失败信息。
|
||||
|
||||
## 必要日志
|
||||
|
||||
必须打印这些日志:
|
||||
|
||||
- 启动配置摘要,禁止包含 secret。
|
||||
- 数据库连接成功/失败,禁止包含密码。
|
||||
- 认证成功/失败,必须脱敏。
|
||||
- 权限拒绝和管理员拒绝。
|
||||
- 数据变更操作成功,包含资源标识。
|
||||
- 外部服务调用失败,包含 endpoint 名称、耗时和脱敏错误。
|
||||
- 事务失败,包含操作、可获得的 entity ID 和请求 ID。
|
||||
|
||||
循环中禁止逐行打印噪声日志,应使用聚合计数。
|
||||
66
1-AgentSkills/coding-go-gin-gorm/reference/time-handling.md
Normal file
66
1-AgentSkills/coding-go-gin-gorm/reference/time-handling.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# 时间处理规范
|
||||
|
||||
时间是硬一致性规则,不是展示层偏好。
|
||||
|
||||
## 强制标准
|
||||
|
||||
- 时区:`Asia/Shanghai` (`UTC+08:00`)
|
||||
- API 格式:带 offset 的 RFC3339,例如 `2026-06-29T10:30:00+08:00`
|
||||
- 存储类型:`time.Time`
|
||||
- 运行时函数:本地 `common.Now()`、`common.FormatTime()`、`common.ParseTime()`
|
||||
|
||||
## 禁止项
|
||||
|
||||
```go
|
||||
time.Now()
|
||||
time.Parse(layout, value)
|
||||
t.Format(layout)
|
||||
```
|
||||
|
||||
除本地 common 时间运行时外,禁止直接使用上述调用。应使用:
|
||||
|
||||
```go
|
||||
now := common.Now()
|
||||
timestamp := common.FormatTime(now)
|
||||
parsed, err := common.ParseTime(req.StartTime)
|
||||
```
|
||||
|
||||
## GORM 实体(Entity)
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 持久化字段允许使用 GORM auto timestamps。
|
||||
- 业务时间必须使用 `common.Now()` 赋值。
|
||||
- API 响应中的时间由 mapper 统一格式化。
|
||||
- Handler 禁止手动格式化时间。
|
||||
|
||||
## Mapper 示例
|
||||
|
||||
```go
|
||||
func ToUserDTO(user *entity.User) *dto.UserDTO {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &dto.UserDTO{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
CreatedAt: common.FormatTime(user.CreatedAt),
|
||||
UpdatedAt: common.FormatTime(user.UpdatedAt),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库配置
|
||||
|
||||
- PostgreSQL 部署应显式设置服务端/会话时区。
|
||||
- 应用代码仍负责把 API 输出转换为 Asia/Shanghai。
|
||||
- 禁止依赖宿主机本地时区。
|
||||
@@ -0,0 +1,21 @@
|
||||
param(
|
||||
[string]$ProjectRoot = "."
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$Validator = Join-Path $ScriptDir "validate_go_gin_gorm.py"
|
||||
|
||||
$Python = Get-Command python -ErrorAction SilentlyContinue
|
||||
if ($Python) {
|
||||
& $Python.Source $Validator $ProjectRoot
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
$PyLauncher = Get-Command py -ErrorAction SilentlyContinue
|
||||
if ($PyLauncher) {
|
||||
& $PyLauncher.Source -3 $Validator $ProjectRoot
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Error "Python 3 is required to run the validator."
|
||||
exit 2
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
PROJECT_ROOT=${1:-.}
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
exec python3 "$SCRIPT_DIR/validate_go_gin_gorm.py" "$PROJECT_ROOT"
|
||||
fi
|
||||
|
||||
if command -v python >/dev/null 2>&1; then
|
||||
exec python "$SCRIPT_DIR/validate_go_gin_gorm.py" "$PROJECT_ROOT"
|
||||
fi
|
||||
|
||||
echo "Python 3 is required to run the validator." >&2
|
||||
exit 2
|
||||
214
1-AgentSkills/coding-go-gin-gorm/scripts/validate_go_gin_gorm.py
Normal file
214
1-AgentSkills/coding-go-gin-gorm/scripts/validate_go_gin_gorm.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cross-platform validator for the developing-go-gin-gorm skill."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SKIP_DIRS = {
|
||||
".git",
|
||||
".idea",
|
||||
".vscode",
|
||||
"vendor",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
"tmp",
|
||||
}
|
||||
|
||||
FORBIDDEN_ROUTE_METHOD = re.compile(r"\.\s*(GET|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(")
|
||||
FORBIDDEN_GIN_INPUT = re.compile(r"\.\s*(Param|Query|DefaultQuery|ShouldBindQuery)\s*\(")
|
||||
FORBIDDEN_DIRECT_RESPONSE = re.compile(r"\.\s*(JSON|AbortWithStatusJSON|String|XML|YAML)\s*\(")
|
||||
FORBIDDEN_TIME = re.compile(r"\btime\s*\.\s*(Now|Parse)\s*\(")
|
||||
FORBIDDEN_PRINT = re.compile(r"\b(fmt\.Println|log\.Println|println)\s*\(")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Finding:
|
||||
severity: str
|
||||
path: Path
|
||||
line: int
|
||||
message: str
|
||||
|
||||
def render(self, root: Path) -> str:
|
||||
rel = self.path.relative_to(root) if self.path.is_relative_to(root) else self.path
|
||||
return f"[{self.severity}] {rel}:{self.line}: {self.message}"
|
||||
|
||||
|
||||
def iter_files(root: Path, suffixes: set[str]) -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames[:] = [name for name in dirnames if name not in SKIP_DIRS]
|
||||
base = Path(dirpath)
|
||||
for filename in filenames:
|
||||
path = base / filename
|
||||
if path.suffix in suffixes:
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def is_under(path: Path, *names: str) -> bool:
|
||||
parts = path.parts
|
||||
if len(parts) < len(names):
|
||||
return False
|
||||
for i in range(0, len(parts) - len(names) + 1):
|
||||
if parts[i : i + len(names)] == names:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_common_runtime(path: Path) -> bool:
|
||||
return is_under(path, "internal", "common") or is_under(path, "pkg", "common")
|
||||
|
||||
|
||||
def is_handler(path: Path) -> bool:
|
||||
return "handler" in path.parts
|
||||
|
||||
|
||||
def is_service(path: Path) -> bool:
|
||||
return "service" in path.parts
|
||||
|
||||
|
||||
def is_dao(path: Path) -> bool:
|
||||
return "dao" in path.parts or "repository" in path.parts
|
||||
|
||||
|
||||
def scan_text(root: Path, path: Path, text: str) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
lines = text.splitlines()
|
||||
|
||||
for index, line in enumerate(lines, start=1):
|
||||
if FORBIDDEN_ROUTE_METHOD.search(line):
|
||||
findings.append(Finding("ERROR", path, index, "business routes must use POST only"))
|
||||
if FORBIDDEN_GIN_INPUT.search(line):
|
||||
findings.append(Finding("ERROR", path, index, "business APIs must bind JSON body, not path/query input"))
|
||||
if FORBIDDEN_DIRECT_RESPONSE.search(line) and not is_common_runtime(path):
|
||||
findings.append(Finding("ERROR", path, index, "use common response helpers instead of direct Gin responses"))
|
||||
if FORBIDDEN_TIME.search(line) and not is_common_runtime(path):
|
||||
findings.append(Finding("ERROR", path, index, "use common.Now/common.ParseTime instead of direct time calls"))
|
||||
if FORBIDDEN_PRINT.search(line):
|
||||
findings.append(Finding("ERROR", path, index, "use common structured logging instead of print/log.Println"))
|
||||
if is_dao(path) and ".Transaction(" in line:
|
||||
findings.append(Finding("ERROR", path, index, "DAO must not start transactions; service owns Unit of Work"))
|
||||
|
||||
if is_handler(path) and "gorm.io/" in text:
|
||||
findings.append(Finding("ERROR", path, 1, "handler must not import GORM"))
|
||||
if is_handler(path) and "/dao" in text:
|
||||
findings.append(Finding("ERROR", path, 1, "handler must not import DAO directly"))
|
||||
if is_service(path) and "github.com/gin-gonic/gin" in text:
|
||||
findings.append(Finding("ERROR", path, 1, "service must not import Gin"))
|
||||
if is_dao(path) and ("/service" in text or "/handler" in text):
|
||||
findings.append(Finding("ERROR", path, 1, "DAO must not import service or handler"))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def scan_crlf(root: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
for path in iter_files(root, {".go", ".md", ".sh", ".py", ".ps1", ".bat"}):
|
||||
data = path.read_bytes()
|
||||
if b"\r\n" in data:
|
||||
findings.append(Finding("ERROR", path, 1, "file uses CRLF; use LF to keep scripts portable"))
|
||||
return findings
|
||||
|
||||
|
||||
def run_gofmt(root: Path, go_files: list[Path]) -> list[Finding]:
|
||||
if not go_files or shutil.which("gofmt") is None:
|
||||
return []
|
||||
|
||||
findings: list[Finding] = []
|
||||
chunk_size = 100
|
||||
for start in range(0, len(go_files), chunk_size):
|
||||
chunk = go_files[start : start + chunk_size]
|
||||
result = subprocess.run(
|
||||
["gofmt", "-l", *[str(path) for path in chunk]],
|
||||
cwd=root,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
findings.append(Finding("ERROR", root, 1, f"gofmt failed: {result.stderr.strip()}"))
|
||||
continue
|
||||
for filename in result.stdout.splitlines():
|
||||
findings.append(Finding("ERROR", Path(filename), 1, "file is not gofmt-formatted"))
|
||||
return findings
|
||||
|
||||
|
||||
def run_go_test(root: Path) -> list[Finding]:
|
||||
if shutil.which("go") is None:
|
||||
return [Finding("WARN", root, 1, "go binary not found; skipped go test")]
|
||||
if not (root / "go.mod").exists():
|
||||
return [Finding("WARN", root, 1, "go.mod not found; skipped go test")]
|
||||
|
||||
result = subprocess.run(
|
||||
["go", "test", "./..."],
|
||||
cwd=root,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return []
|
||||
output = (result.stdout + "\n" + result.stderr).strip()
|
||||
first_line = output.splitlines()[0] if output else "go test failed"
|
||||
return [Finding("ERROR", root, 1, first_line)]
|
||||
|
||||
|
||||
def validate(root: Path, run_tests: bool, skip_gofmt: bool) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
go_files = iter_files(root, {".go"})
|
||||
|
||||
findings.extend(scan_crlf(root))
|
||||
for path in go_files:
|
||||
findings.extend(scan_text(root, path, read_text(path)))
|
||||
|
||||
if not skip_gofmt:
|
||||
findings.extend(run_gofmt(root, go_files))
|
||||
if run_tests:
|
||||
findings.extend(run_go_test(root))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Validate Go Gin/GORM engineering rules.")
|
||||
parser.add_argument("root", nargs="?", default=".", help="project root to validate")
|
||||
parser.add_argument("--run-go-test", action="store_true", help="also run go test ./...")
|
||||
parser.add_argument("--skip-gofmt", action="store_true", help="skip gofmt -l check")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve()
|
||||
if not root.exists():
|
||||
print(f"target root does not exist: {root}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
findings = validate(root, run_tests=args.run_go_test, skip_gofmt=args.skip_gofmt)
|
||||
errors = [finding for finding in findings if finding.severity == "ERROR"]
|
||||
warnings = [finding for finding in findings if finding.severity == "WARN"]
|
||||
|
||||
for finding in findings:
|
||||
print(finding.render(root))
|
||||
|
||||
if errors:
|
||||
print(f"\nValidation failed: {len(errors)} error(s), {len(warnings)} warning(s).")
|
||||
return 1
|
||||
|
||||
print(f"Validation passed: 0 error(s), {len(warnings)} warning(s).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user