更新 skill 后端开发

This commit is contained in:
zeaslity
2026-07-01 13:45:30 +08:00
parent d213a1146c
commit 9cd57b92b8
30 changed files with 1952 additions and 1643 deletions

View File

@@ -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避免业务逻辑堆在 handlerDAO/Repo 只做数据访问与查询组装),并统一 API 响应包装
consistent response envelopecode/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/Shanghaitime 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 实现 / 代码走查与 Reviewrefactor 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
```

View File

@@ -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
}

View File

@@ -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, "用户创建成功")
}

View File

@@ -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
}

View File

@@ -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. 审计敏感操作
所有写操作需通过审计中间件记录。

View File

@@ -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 |

View File

@@ -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()`

View File

@@ -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 "未知错误"
}

View File

@@ -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)
```

View File

@@ -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 日志禁止缺少堆栈信息

View File

@@ -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
```

View File

@@ -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"
```

View File

@@ -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 "=== 验证完成 ==="

View 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 ./...` 通过;如果因环境限制不能运行,需要明确说明。

View 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
}

View 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, "用户创建成功")
}

View 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)
}

View 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 中间件禁止从路径参数或查询参数中提取业务资源标识。
- 删除接口在需要业务恢复或审计追溯时优先使用软删除。

View File

@@ -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`

View File

@@ -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 并混入用户输入。

View 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
}
```

View 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并尽量可回滚。
- 索引设计必须结合查询模式、唯一性约束和分页方式。
- 大表回填必须分批执行,并保证过程可观测。

View 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
}
```
### 原生 SQLRaw 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 更新。

View File

@@ -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。

View File

@@ -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` 路由。
- 为变更文件补充验证脚本覆盖。

View 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。
循环中禁止逐行打印噪声日志,应使用聚合计数。

View 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。
- 禁止依赖宿主机本地时区。

View File

@@ -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

View File

@@ -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

View 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())