diff --git a/.agents/skills/backend-go-gin-gorm/SKILL.md b/.agents/skills/backend-go-gin-gorm/SKILL.md deleted file mode 100644 index 92ed04b..0000000 --- a/.agents/skills/backend-go-gin-gorm/SKILL.md +++ /dev/null @@ -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 | 说明 | -|--------|------| -| `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 -``` diff --git a/.agents/skills/backend-go-gin-gorm/examples/dao-example.go b/.agents/skills/backend-go-gin-gorm/examples/dao-example.go deleted file mode 100644 index 6736817..0000000 --- a/.agents/skills/backend-go-gin-gorm/examples/dao-example.go +++ /dev/null @@ -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 -} diff --git a/.agents/skills/backend-go-gin-gorm/examples/handler-example.go b/.agents/skills/backend-go-gin-gorm/examples/handler-example.go deleted file mode 100644 index 2fe2dba..0000000 --- a/.agents/skills/backend-go-gin-gorm/examples/handler-example.go +++ /dev/null @@ -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, "用户创建成功") -} diff --git a/.agents/skills/backend-go-gin-gorm/examples/service-example.go b/.agents/skills/backend-go-gin-gorm/examples/service-example.go deleted file mode 100644 index 0fb9d7b..0000000 --- a/.agents/skills/backend-go-gin-gorm/examples/service-example.go +++ /dev/null @@ -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 -} diff --git a/.agents/skills/backend-go-gin-gorm/reference/api-design-spec.md b/.agents/skills/backend-go-gin-gorm/reference/api-design-spec.md deleted file mode 100644 index d06bdab..0000000 --- a/.agents/skills/backend-go-gin-gorm/reference/api-design-spec.md +++ /dev/null @@ -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('/api/jenkins/builds/list', data), - - // 触发构建 - triggerBuild: (data: TriggerBuildRequest) => - request.post('/api/jenkins/builds/trigger', data), - - // 获取构建详情 - getBuildDetail: (data: GetBuildRequest) => - request.post('/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. 审计敏感操作 - -所有写操作需通过审计中间件记录。 diff --git a/.agents/skills/backend-go-gin-gorm/reference/api-response-spec.md b/.agents/skills/backend-go-gin-gorm/reference/api-response-spec.md deleted file mode 100644 index b111a2e..0000000 --- a/.agents/skills/backend-go-gin-gorm/reference/api-response-spec.md +++ /dev/null @@ -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 | diff --git a/.agents/skills/backend-go-gin-gorm/reference/coding-standards.md b/.agents/skills/backend-go-gin-gorm/reference/coding-standards.md deleted file mode 100644 index 8fc48b7..0000000 --- a/.agents/skills/backend-go-gin-gorm/reference/coding-standards.md +++ /dev/null @@ -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()` diff --git a/.agents/skills/backend-go-gin-gorm/reference/error-codes.go b/.agents/skills/backend-go-gin-gorm/reference/error-codes.go deleted file mode 100644 index a61ebe3..0000000 --- a/.agents/skills/backend-go-gin-gorm/reference/error-codes.go +++ /dev/null @@ -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 "未知错误" -} diff --git a/.agents/skills/backend-go-gin-gorm/reference/framework-usage.md b/.agents/skills/backend-go-gin-gorm/reference/framework-usage.md deleted file mode 100644 index 66af57a..0000000 --- a/.agents/skills/backend-go-gin-gorm/reference/framework-usage.md +++ /dev/null @@ -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) -``` diff --git a/.agents/skills/backend-go-gin-gorm/reference/logging-standards.md b/.agents/skills/backend-go-gin-gorm/reference/logging-standards.md deleted file mode 100644 index 51f06a6..0000000 --- a/.agents/skills/backend-go-gin-gorm/reference/logging-standards.md +++ /dev/null @@ -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 日志禁止缺少堆栈信息 diff --git a/.agents/skills/backend-go-gin-gorm/reference/project-structure.md b/.agents/skills/backend-go-gin-gorm/reference/project-structure.md deleted file mode 100644 index 37df808..0000000 --- a/.agents/skills/backend-go-gin-gorm/reference/project-structure.md +++ /dev/null @@ -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 -``` diff --git a/.agents/skills/backend-go-gin-gorm/reference/time-handling.md b/.agents/skills/backend-go-gin-gorm/reference/time-handling.md deleted file mode 100644 index 6f6cbb7..0000000 --- a/.agents/skills/backend-go-gin-gorm/reference/time-handling.md +++ /dev/null @@ -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" -``` diff --git a/.agents/skills/backend-go-gin-gorm/scripts/validate-structure.sh b/.agents/skills/backend-go-gin-gorm/scripts/validate-structure.sh deleted file mode 100644 index 50aa584..0000000 --- a/.agents/skills/backend-go-gin-gorm/scripts/validate-structure.sh +++ /dev/null @@ -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 "=== 验证完成 ===" diff --git a/1-AgentSkills/coding-go-gin-gorm/SKILL.md b/1-AgentSkills/coding-go-gin-gorm/SKILL.md new file mode 100644 index 0000000..be542b6 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/SKILL.md @@ -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 ` 通过。 +- [ ] `gofmt` 和 `go test ./...` 通过;如果因环境限制不能运行,需要明确说明。 diff --git a/1-AgentSkills/coding-go-gin-gorm/examples/dao-example.go b/1-AgentSkills/coding-go-gin-gorm/examples/dao-example.go new file mode 100644 index 0000000..114c8d3 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/examples/dao-example.go @@ -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 +} diff --git a/1-AgentSkills/coding-go-gin-gorm/examples/handler-example.go b/1-AgentSkills/coding-go-gin-gorm/examples/handler-example.go new file mode 100644 index 0000000..b30ca80 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/examples/handler-example.go @@ -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, "用户创建成功") +} diff --git a/1-AgentSkills/coding-go-gin-gorm/examples/service-example.go b/1-AgentSkills/coding-go-gin-gorm/examples/service-example.go new file mode 100644 index 0000000..d9dd15a --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/examples/service-example.go @@ -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) +} diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md b/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md new file mode 100644 index 0000000..179c803 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md @@ -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 中间件禁止从路径参数或查询参数中提取业务资源标识。 +- 删除接口在需要业务恢复或审计追溯时优先使用软删除。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/api-response-spec.md b/1-AgentSkills/coding-go-gin-gorm/reference/api-response-spec.md new file mode 100644 index 0000000..5124b0b --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/api-response-spec.md @@ -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`。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/coding-standards.md b/1-AgentSkills/coding-go-gin-gorm/reference/coding-standards.md new file mode 100644 index 0000000..97f0a4b --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/coding-standards.md @@ -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 并混入用户输入。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/common-runtime.md b/1-AgentSkills/coding-go-gin-gorm/reference/common-runtime.md new file mode 100644 index 0000000..504bace --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/common-runtime.md @@ -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 +} +``` diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/database-patterns.md b/1-AgentSkills/coding-go-gin-gorm/reference/database-patterns.md new file mode 100644 index 0000000..4622dfd --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/database-patterns.md @@ -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,并尽量可回滚。 +- 索引设计必须结合查询模式、唯一性约束和分页方式。 +- 大表回填必须分批执行,并保证过程可观测。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/framework-usage.md b/1-AgentSkills/coding-go-gin-gorm/reference/framework-usage.md new file mode 100644 index 0000000..860359f --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/framework-usage.md @@ -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 更新。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/logging-standards.md b/1-AgentSkills/coding-go-gin-gorm/reference/logging-standards.md new file mode 100644 index 0000000..370a390 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/logging-standards.md @@ -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。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/project-structure.md b/1-AgentSkills/coding-go-gin-gorm/reference/project-structure.md new file mode 100644 index 0000000..44ec1fe --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/project-structure.md @@ -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` 路由。 +- 为变更文件补充验证脚本覆盖。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/security-audit.md b/1-AgentSkills/coding-go-gin-gorm/reference/security-audit.md new file mode 100644 index 0000000..9f50c37 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/security-audit.md @@ -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 ` 请求头。 +- 校验签名、过期时间,以及配置要求的 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。 + +循环中禁止逐行打印噪声日志,应使用聚合计数。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/time-handling.md b/1-AgentSkills/coding-go-gin-gorm/reference/time-handling.md new file mode 100644 index 0000000..18d630b --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/time-handling.md @@ -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。 +- 禁止依赖宿主机本地时区。 diff --git a/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.ps1 b/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.ps1 new file mode 100644 index 0000000..bcfd0ef --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.ps1 @@ -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 diff --git a/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.sh b/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.sh new file mode 100644 index 0000000..cdb26a2 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.sh @@ -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 diff --git a/1-AgentSkills/coding-go-gin-gorm/scripts/validate_go_gin_gorm.py b/1-AgentSkills/coding-go-gin-gorm/scripts/validate_go_gin_gorm.py new file mode 100644 index 0000000..14d3ed5 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/scripts/validate_go_gin_gorm.py @@ -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())