RMDC系统设计文档 整体转换为SKILL

This commit is contained in:
zeaslity
2026-01-21 16:15:49 +08:00
parent fc72a7312e
commit 631cce9e1e
163 changed files with 37099 additions and 114 deletions

View File

@@ -0,0 +1,425 @@
---
name: developing-go-gin-gorm
description: Generates and reviews Go backend code using GIN and GORM frameworks. Enforces layered architecture (handler→service→dao), unified API response format, POST+RequestBody API design, DTO naming conventions, Chinese comments, Asia/Shanghai timezone, structured logging, and framework best practices. Trigger on Go API development, GIN handler creation, GORM repository implementation, or code review requests.
argument-hint: "<action> <target>" e.g., "create user-handler", "review service/order.go", "scaffold api/v1/product"
allowed-tools:
- Read
- Write
- Edit
- Glob
- Grep
- Bash
---
# Go GIN/GORM 开发规范 Skill
## 触发条件
- 用户请求创建/修改 Go 后端代码GIN handler、GORM dao、service
- 用户请求代码审查
- 用户提及 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

@@ -0,0 +1,55 @@
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

@@ -0,0 +1,70 @@
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

@@ -0,0 +1,60 @@
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

@@ -0,0 +1,330 @@
# 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/` |
| 审计 | `/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

@@ -0,0 +1,35 @@
# 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

@@ -0,0 +1,44 @@
# 编码规范
## 命名规范
| 类型 | 规则 | 示例 |
|------|------|------|
| 包名 | 小写单词,无下划线 | `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

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,264 @@
# 框架使用规范
## 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

@@ -0,0 +1,100 @@
# 日志规范
## 指定框架
项目统一使用内部日志库:`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

@@ -0,0 +1,39 @@
# 项目目录结构规范
## 核心目录
| 目录 | 职责 | 禁止事项 |
|------|------|----------|
| `/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

@@ -0,0 +1,120 @@
# 时间处理规范
## 核心原则
所有在前端和后端之间传输、以及在数据库中存储的时间,**必须统一为东八区时间 (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

@@ -0,0 +1,51 @@
#!/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,141 @@
---
name: coding-vue3-vuetify
description: Build production-grade Vue 3 + TypeScript + Vuetify 3 interfaces with architectural rigor. Use when creating Vue components, pages, layouts, Pinia stores, or API modules. Enforces strict typing, Composition API patterns, Material Design 3 aesthetics, and bulletproof data handling.
---
This skill crafts Vue 3 + Vuetify 3 code that is architecturally sound, type-safe to the bone, and visually polished. Every component should feel like it belongs in a production codebase that senior engineers would be proud to maintain.
The user provides: $ARGUMENTS (component specs, page requirements, feature requests, or architectural questions).
## Architectural Thinking
Before writing a single line, establish clarity:
- **Component Identity**: Is this a Page, Layout, Reusable Component, Composable, Store, or API Module? Each has distinct patterns.
- **Data Gravity**: Where does state live? Props flow down, events bubble up. Pinia for cross-component state. `provide/inject` for deep hierarchies.
- **Scroll Strategy**: Which container owns the scroll? Never the body. Always explicit. Always controlled.
- **Failure Modes**: What happens when data is `null`? Empty array? Network timeout? Design for the unhappy path first.
**CRITICAL**: Production code anticipates chaos. Type everything. Guard everything. Gracefully degrade everything.
## Core Dogma
### TypeScript Absolutism
- `<script setup lang="ts">` — the ONLY acceptable incantation
- `any` is forbidden — use `unknown` + type guards, generics, utility types
- Every prop, emit, ref, and API response wears its type proudly
- Types live in `@/types/`, organized by domain: `user.d.ts`, `order.d.ts`
### Composition API Purity
- `ref`, `reactive`, `computed`, `watchEffect` — master these four
- `shallowRef`, `readonly`, `toRaw` — know when to reach for optimization
- Lifecycle via `onMounted`, `onUnmounted` — never mix Options API
- Pinia stores: typed state, typed getters, typed actions — no exceptions
### Vuetify 3 + Material Design 3
- ALL UI through Vuetify components — no raw HTML for UI elements
- Theme-aware always — `rgb(var(--v-theme-surface))`, never `#ffffff`
- `useDisplay()` for responsive logic — breakpoints are first-class citizens
- Density matters — `density="compact"` for data-heavy interfaces
### Layout Philosophy
```
┌─────────────────────────────────┐
│ Toolbar (flex-shrink-0) │
├─────────────────────────────────┤
│ │
│ Content Area │
│ (flex-grow-1, overflow-y-auto) │
│ (min-height: 0) ← CRITICAL │
│ │
├─────────────────────────────────┤
│ Footer (flex-shrink-0) │
└─────────────────────────────────┘
```
- **No body scroll** — viewport locked, content scrolls in containers
- **Flexbox trap**: `flex-grow-1` children MUST have `min-height: 0`
- **Sticky elements**: filters, table headers — always visible during scroll
## Data Robustness Patterns
Treat all external data as hostile:
```typescript
// Defensive access
const userName = user?.profile?.name ?? 'Unknown'
// Array safety
const items = Array.isArray(response.data) ? response.data : []
// Existence guards in templates
<template v-if="user">{{ user.name }}</template>
<v-empty-state v-else />
```
## UI State Trinity
Every data-driven view handles THREE states:
| State | Component | Never Do |
|-------|-----------|----------|
| **Loading** | `v-skeleton-loader` | Show stale data or blank screen |
| **Empty** | `v-empty-state` with action | Leave white void |
| **Error** | Snackbar + retry option | Silent failure |
## Table & List Commandments
- `fixed-header` on every `v-data-table` — non-negotiable
- Truncated text gets `v-tooltip` — users deserve full content on hover
- 100+ items? `v-virtual-scroll` — DOM nodes stay constant
- Column widths explicit — no layout lottery
## Anti-Patterns (NEVER)
- `.js` files in a TypeScript project
- `any` without a blood oath and written justification
- Hardcoded colors: `color="#1976d2"``color="primary"`
- Body-level scrolling in SPA layouts
- Tables without fixed headers
- Truncated text without tooltips
- Empty states that are literally empty
- Loading states that freeze the UI
- API calls without error handling
## Reference Files
Consult these for implementation details:
| Need | Read |
|------|------|
| Advanced TypeScript patterns | `reference/typescript-rules.md` |
| Complex layout structures | `reference/layout-patterns.md` |
| API client architecture | `reference/api-patterns.md` |
| Tables, lists, forms, feedback | `reference/ui-interaction.md` |
## Project Anatomy
```
src/
├── api/ # Axios instance + modules
├── components/ # Shared components
├── composables/ # Reusable hooks
├── layouts/ # Page shells
├── pages/ # Route views
├── plugins/ # Vuetify, Pinia, Router
├── store/ # Pinia stores
├── styles/ # Global SCSS
├── types/ # Type definitions
└── utils/ # Pure functions
```
## Output Protocol
1. State the architectural approach (2-3 sentences)
2. List files to create with their purposes
3. Implement each file completely — no placeholders, no TODOs
4. Verify against the anti-patterns list
5. Call out any assumptions or trade-offs made
---
Remember: You're not writing code that works. You're writing code that works, scales, maintains, and delights. Every `ref` is typed. Every edge case is handled. Every loading state is beautiful. This is what production-grade means.

View File

@@ -0,0 +1,113 @@
// @/api/modules/user.ts
import request from '@/api'
import type { PageParams, PageResult } from '@/types/api'
// ============================================
// 类型定义
// ============================================
export interface User {
id: string
name: string
email: string
avatar?: string
status: 'active' | 'disabled'
role: 'admin' | 'user' | 'guest'
createdAt: string
updatedAt: string
}
export interface CreateUserDto {
name: string
email: string
password: string
role?: User['role']
}
export interface UpdateUserDto {
name?: string
email?: string
status?: User['status']
role?: User['role']
}
export interface UserListParams extends PageParams {
search?: string
status?: User['status']
role?: User['role']
}
// ============================================
// API 封装
// ============================================
export const userApi = {
/**
* 获取用户分页列表
*/
getPage: (params: UserListParams) =>
request.get<PageResult<User>>('/users', { params }),
/**
* 获取用户列表(无分页,用于下拉选择等场景)
*/
getList: (params?: Partial<UserListParams>) =>
request.get<User[]>('/users/list', { params }),
/**
* 获取用户详情
*/
getById: (id: string) =>
request.get<User>(`/users/${id}`),
/**
* 检查邮箱是否已存在
*/
checkEmail: (email: string) =>
request.get<{ exists: boolean }>('/users/check-email', { params: { email } }),
/**
* 创建用户
*/
create: (data: CreateUserDto) =>
request.post<User>('/users', data),
/**
* 更新用户
*/
update: (id: string, data: UpdateUserDto) =>
request.put<User>(`/users/${id}`, data),
/**
* 删除用户
*/
remove: (id: string) =>
request.delete<void>(`/users/${id}`),
/**
* 批量删除用户
*/
batchRemove: (ids: string[]) =>
request.post<void>('/users/batch-delete', { ids }),
/**
* 启用/禁用用户
*/
toggleStatus: (id: string, status: User['status']) =>
request.patch<User>(`/users/${id}/status`, { status }),
/**
* 重置用户密码
*/
resetPassword: (id: string) =>
request.post<{ tempPassword: string }>(`/users/${id}/reset-password`),
/**
* 导出用户列表
*/
export: (params?: UserListParams) =>
request.get<Blob>('/users/export', {
params,
responseType: 'blob',
}),
}

View File

@@ -0,0 +1,409 @@
<template>
<div class="d-flex flex-column h-100">
<!-- 页面头部 -->
<v-toolbar density="compact" class="flex-shrink-0">
<v-toolbar-title>订单管理</v-toolbar-title>
<v-spacer />
<v-btn
icon="mdi-refresh"
:loading="loading"
@click="fetchData"
/>
<v-btn
color="primary"
prepend-icon="mdi-download"
:loading="exporting"
@click="exportData"
>
导出
</v-btn>
</v-toolbar>
<!-- 筛选栏 - 粘性定位 -->
<v-sheet class="flex-shrink-0 pa-4 sticky-filter">
<v-row dense>
<v-col cols="12" sm="6" md="3">
<v-text-field
v-model="filters.search"
label="搜索订单号/客户名"
prepend-inner-icon="mdi-magnify"
clearable
hide-details
@update:model-value="debouncedFetch"
/>
</v-col>
<v-col cols="12" sm="6" md="2">
<v-select
v-model="filters.status"
:items="statusOptions"
label="状态"
clearable
hide-details
@update:model-value="fetchData"
/>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-text-field
v-model="filters.dateRange"
label="日期范围"
prepend-inner-icon="mdi-calendar"
readonly
hide-details
@click="showDatePicker = true"
/>
</v-col>
<v-col cols="12" sm="6" md="2">
<v-btn
variant="outlined"
block
@click="resetFilters"
>
重置筛选
</v-btn>
</v-col>
</v-row>
</v-sheet>
<v-divider />
<!-- 主内容区 - 可滚动 -->
<div class="flex-grow-1 overflow-y-auto" style="min-height: 0">
<!-- 加载状态 -->
<v-skeleton-loader
v-if="loading && !orders.length"
type="table-heading, table-row@8"
class="ma-4"
/>
<!-- 空状态 -->
<v-empty-state
v-else-if="!orders.length"
icon="mdi-package-variant"
title="暂无订单"
text="当前筛选条件下没有找到订单记录"
>
<template #actions>
<v-btn variant="outlined" @click="resetFilters">
清除筛选
</v-btn>
<v-btn color="primary" @click="fetchData">
刷新
</v-btn>
</template>
</v-empty-state>
<!-- 数据表格 -->
<v-data-table-server
v-else
v-model:items-per-page="pagination.pageSize"
v-model:page="pagination.page"
:headers="headers"
:items="orders"
:items-length="pagination.total"
:loading="loading"
fixed-header
hover
@update:options="onOptionsChange"
>
<!-- 订单号 - 可点击 -->
<template #item.orderNo="{ item }">
<a
href="#"
class="text-primary text-decoration-none"
@click.prevent="viewDetail(item)"
>
{{ item.orderNo }}
</a>
</template>
<!-- 客户名 - 截断 + Tooltip -->
<template #item.customerName="{ value }">
<v-tooltip :text="value" location="top">
<template #activator="{ props }">
<span
v-bind="props"
class="text-truncate d-inline-block"
style="max-width: 120px"
>
{{ value }}
</span>
</template>
</v-tooltip>
</template>
<!-- 金额 - 格式化 -->
<template #item.amount="{ value }">
<span class="font-weight-medium">
¥{{ formatNumber(value) }}
</span>
</template>
<!-- 状态 - Chip -->
<template #item.status="{ value }">
<v-chip
:color="getStatusColor(value)"
size="small"
variant="tonal"
>
{{ getStatusText(value) }}
</v-chip>
</template>
<!-- 备注 - 多行截断 -->
<template #item.remark="{ value }">
<div v-if="value" class="remark-cell">
<v-tooltip :text="value" location="top" max-width="300">
<template #activator="{ props }">
<span v-bind="props" class="line-clamp-2">
{{ value }}
</span>
</template>
</v-tooltip>
</div>
<span v-else class="text-grey">-</span>
</template>
<!-- 操作列 -->
<template #item.actions="{ item }">
<v-btn
icon="mdi-eye"
size="small"
variant="text"
@click="viewDetail(item)"
/>
<v-btn
icon="mdi-pencil"
size="small"
variant="text"
:disabled="item.status === 'completed'"
@click="editOrder(item)"
/>
<v-menu>
<template #activator="{ props }">
<v-btn
v-bind="props"
icon="mdi-dots-vertical"
size="small"
variant="text"
/>
</template>
<v-list density="compact">
<v-list-item @click="copyOrderNo(item)">
<template #prepend>
<v-icon size="small">mdi-content-copy</v-icon>
</template>
<v-list-item-title>复制订单号</v-list-item-title>
</v-list-item>
<v-list-item
:disabled="item.status !== 'pending'"
@click="cancelOrder(item)"
>
<template #prepend>
<v-icon size="small" color="error">mdi-cancel</v-icon>
</template>
<v-list-item-title class="text-error">取消订单</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<!-- 空数据插槽 -->
<template #no-data>
<v-empty-state
icon="mdi-database-off"
title="暂无数据"
text="请尝试调整筛选条件"
/>
</template>
</v-data-table-server>
</div>
<!-- 底部统计栏 -->
<v-sheet class="flex-shrink-0 pa-2 border-t d-flex align-center justify-space-between">
<span class="text-body-2 text-grey">
{{ pagination.total }} 条记录
</span>
<span class="text-body-2">
已选 <strong>{{ selectedCount }}</strong>
<v-btn
v-if="selectedCount > 0"
variant="text"
size="small"
color="primary"
@click="batchAction"
>
批量操作
</v-btn>
</span>
</v-sheet>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import type { Order, OrderStatus } from '@/types/order'
import { orderApi } from '@/api/modules/order'
import { useSnackbar } from '@/composables/useSnackbar'
// Composables
const snackbar = useSnackbar()
// State
const loading = ref(false)
const exporting = ref(false)
const showDatePicker = ref(false)
const orders = ref<Order[]>([])
const selectedCount = ref(0)
const filters = reactive({
search: '',
status: null as OrderStatus | null,
dateRange: '',
})
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0,
})
// Table Headers
const headers = [
{ title: '订单号', key: 'orderNo', width: 160 },
{ title: '客户名称', key: 'customerName', width: 150 },
{ title: '金额', key: 'amount', width: 120, align: 'end' as const },
{ title: '状态', key: 'status', width: 100 },
{ title: '备注', key: 'remark', width: 200 },
{ title: '创建时间', key: 'createdAt', width: 170 },
{ title: '操作', key: 'actions', width: 140, sortable: false },
]
const statusOptions = [
{ title: '全部', value: null },
{ title: '待处理', value: 'pending' },
{ title: '处理中', value: 'processing' },
{ title: '已完成', value: 'completed' },
{ title: '已取消', value: 'cancelled' },
]
// Methods
async function fetchData() {
loading.value = true
try {
const result = await orderApi.getPage({
page: pagination.page,
pageSize: pagination.pageSize,
search: filters.search || undefined,
status: filters.status || undefined,
})
orders.value = result.list
pagination.total = result.total
} catch (error) {
console.error('Failed to fetch orders:', error)
} finally {
loading.value = false
}
}
const debouncedFetch = useDebounceFn(fetchData, 300)
function onOptionsChange(options: { page: number; itemsPerPage: number }) {
pagination.page = options.page
pagination.pageSize = options.itemsPerPage
fetchData()
}
function resetFilters() {
filters.search = ''
filters.status = null
filters.dateRange = ''
pagination.page = 1
fetchData()
}
async function exportData() {
exporting.value = true
try {
const blob = await orderApi.export(filters)
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `orders_${Date.now()}.xlsx`
a.click()
URL.revokeObjectURL(url)
snackbar.success('导出成功')
} catch (error) {
snackbar.error('导出失败')
} finally {
exporting.value = false
}
}
function viewDetail(item: Order) {
// Navigate to detail page
}
function editOrder(item: Order) {
// Open edit dialog
}
function copyOrderNo(item: Order) {
navigator.clipboard.writeText(item.orderNo)
snackbar.success('订单号已复制')
}
function cancelOrder(item: Order) {
// Show confirm dialog
}
function batchAction() {
// Show batch action menu
}
// Helpers
function formatNumber(value: number): string {
return value.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
}
function getStatusColor(status: OrderStatus): string {
const colors: Record<OrderStatus, string> = {
pending: 'warning',
processing: 'info',
completed: 'success',
cancelled: 'grey',
}
return colors[status] || 'grey'
}
function getStatusText(status: OrderStatus): string {
const texts: Record<OrderStatus, string> = {
pending: '待处理',
processing: '处理中',
completed: '已完成',
cancelled: '已取消',
}
return texts[status] || status
}
onMounted(fetchData)
</script>
<style scoped>
.sticky-filter {
position: sticky;
top: 0;
z-index: 1;
}
.remark-cell {
max-width: 180px;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,155 @@
<template>
<v-container fluid class="d-flex flex-column h-100 pa-0">
<!-- 固定工具栏 -->
<v-toolbar density="compact" class="flex-shrink-0">
<v-toolbar-title>用户管理</v-toolbar-title>
<v-spacer />
<v-btn icon="mdi-refresh" :loading="loading" @click="fetchData" />
<v-btn color="primary" prepend-icon="mdi-plus" @click="openCreate">
新建
</v-btn>
</v-toolbar>
<!-- 筛选区域 -->
<v-sheet class="flex-shrink-0 pa-4">
<v-row dense>
<v-col cols="12" md="3">
<v-text-field
v-model="filters.search"
label="搜索"
prepend-inner-icon="mdi-magnify"
clearable
hide-details
/>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="filters.status"
:items="statusOptions"
label="状态"
clearable
hide-details
/>
</v-col>
</v-row>
</v-sheet>
<v-divider />
<!-- 可滚动内容区 -->
<div class="flex-grow-1 overflow-y-auto" style="min-height: 0">
<v-skeleton-loader v-if="loading" type="table-heading, table-row@10" />
<v-empty-state
v-else-if="!users.length"
icon="mdi-account-off"
title="暂无用户"
text="点击新建按钮添加第一个用户"
>
<template #actions>
<v-btn color="primary" @click="openCreate">新建用户</v-btn>
</template>
</v-empty-state>
<v-data-table
v-else
:headers="headers"
:items="users"
fixed-header
hover
>
<template #item.name="{ value }">
<v-tooltip :text="value" location="top">
<template #activator="{ props }">
<span
v-bind="props"
class="text-truncate d-inline-block"
style="max-width: 150px"
>
{{ value }}
</span>
</template>
</v-tooltip>
</template>
<template #item.status="{ value }">
<v-chip
:color="value === 'active' ? 'success' : 'grey'"
size="small"
>
{{ value === 'active' ? '活跃' : '禁用' }}
</v-chip>
</template>
<template #item.actions="{ item }">
<v-btn
icon="mdi-pencil"
size="small"
variant="text"
@click="edit(item)"
/>
<v-btn
icon="mdi-delete"
size="small"
variant="text"
color="error"
@click="remove(item)"
/>
</template>
</v-data-table>
</div>
</v-container>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { User } from '@/types/user'
import { userApi } from '@/api/modules/user'
// State
const loading = ref(false)
const users = ref<User[]>([])
const filters = reactive({
search: '',
status: null as string | null,
})
// Table config
const headers = [
{ title: '姓名', key: 'name', width: 200 },
{ title: '邮箱', key: 'email' },
{ title: '状态', key: 'status', width: 120 },
{ title: '创建时间', key: 'createdAt', width: 180 },
{ title: '操作', key: 'actions', width: 120, sortable: false },
]
const statusOptions = [
{ title: '全部', value: null },
{ title: '活跃', value: 'active' },
{ title: '禁用', value: 'disabled' },
]
// Methods
async function fetchData() {
loading.value = true
try {
users.value = await userApi.getList(filters)
} finally {
loading.value = false
}
}
function openCreate() {
// TODO: Open create dialog
}
function edit(item: User) {
// TODO: Open edit dialog
}
function remove(item: User) {
// TODO: Confirm and delete
}
onMounted(fetchData)
</script>

View File

@@ -0,0 +1,238 @@
# API 客户端模式
## 标准响应类型
```typescript
// @/types/api.d.ts
export interface ApiResponse<T = unknown> {
code: number
status: number
timestamp: string
data: T
message?: string
error?: string
}
export interface PageParams {
page: number
pageSize: number
sort?: string
order?: 'asc' | 'desc'
}
export interface PageResult<T> {
list: T[]
total: number
page: number
pageSize: number
}
export enum ApiErrorCode {
Success = 0,
ServerError = 10001,
ParamError = 10002,
Unauthorized = 10003,
Forbidden = 10004,
NotFound = 10005,
Timeout = 10006,
ValidationFail = 10007,
BusinessError = 20001,
}
```
## Axios 实例
```typescript
// @/api/index.ts
import axios from 'axios'
import { setupInterceptors } from './interceptors'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
})
setupInterceptors(request)
export default request
```
## 响应拦截器
```typescript
// @/api/interceptors.ts
import type { AxiosInstance, AxiosResponse } from 'axios'
import { useSnackbar } from '@/composables/useSnackbar'
import { useAuthStore } from '@/store/auth'
import router from '@/router'
import { ApiErrorCode, type ApiResponse } from '@/types/api'
export function setupInterceptors(instance: AxiosInstance) {
// 请求拦截器
instance.interceptors.request.use(
(config) => {
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
instance.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const { code, data, message } = response.data
// 业务成功
if (code === ApiErrorCode.Success) {
return data
}
// 业务失败
const snackbar = useSnackbar()
snackbar.error(message || '操作失败')
return Promise.reject(new Error(message))
},
(error) => {
const snackbar = useSnackbar()
const status = error.response?.status
switch (status) {
case 401:
useAuthStore().logout()
router.push('/login')
snackbar.error('登录已过期,请重新登录')
break
case 403:
snackbar.error('无权访问')
break
case 404:
snackbar.error('请求的资源不存在')
break
case 500:
snackbar.error('服务器错误,请稍后重试')
break
default:
if (error.code === 'ECONNABORTED') {
snackbar.error('请求超时,请检查网络')
} else if (!error.response) {
snackbar.error('网络连接失败')
}
}
return Promise.reject(error)
}
)
}
```
## API 模块模板
```typescript
// @/api/modules/[domain].ts
import request from '@/api'
import type { PageParams, PageResult } from '@/types/api'
import type { Entity, CreateDto, UpdateDto } from '@/types/[domain]'
export const entityApi = {
// 分页列表
getPage: (params: PageParams) =>
request.get<PageResult<Entity>>('/entities', { params }),
// 详情
getById: (id: string) =>
request.get<Entity>(`/entities/${id}`),
// 新增
create: (data: CreateDto) =>
request.post<Entity>('/entities', data),
// 更新
update: (id: string, data: UpdateDto) =>
request.put<Entity>(`/entities/${id}`, data),
// 删除
remove: (id: string) =>
request.delete<void>(`/entities/${id}`),
}
```
## 请求取消处理
```typescript
// composables/useCancelableRequest.ts
import { onUnmounted } from 'vue'
import type { AxiosRequestConfig } from 'axios'
export function useCancelableRequest() {
const controller = new AbortController()
onUnmounted(() => {
controller.abort()
})
function withCancel<T>(
requestFn: (config?: AxiosRequestConfig) => Promise<T>
): Promise<T> {
return requestFn({ signal: controller.signal })
}
return { withCancel, abort: () => controller.abort() }
}
```
## 请求重试
```typescript
// utils/retryRequest.ts
export async function retryRequest<T>(
fn: () => Promise<T>,
options: { retries?: number; delay?: number } = {}
): Promise<T> {
const { retries = 3, delay = 1000 } = options
for (let attempt = 0; attempt < retries; attempt++) {
try {
return await fn()
} catch (error) {
if (attempt === retries - 1) throw error
await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)))
}
}
throw new Error('Max retries exceeded')
}
```
## 并发请求处理
```typescript
// 使用 Promise.all 并发请求
async function fetchDashboardData() {
const [users, orders, stats] = await Promise.all([
userApi.getList(),
orderApi.getRecent(),
statsApi.getSummary(),
])
return { users, orders, stats }
}
// 使用 Promise.allSettled 处理部分失败
async function fetchWithFallback() {
const results = await Promise.allSettled([
userApi.getList(),
orderApi.getList(),
])
return results.map((result) =>
result.status === 'fulfilled' ? result.value : []
)
}
```

View File

@@ -0,0 +1,182 @@
# 布局模式参考
## 标准页面骨架
```vue
<template>
<v-container fluid class="d-flex flex-column h-100 pa-0">
<!-- 固定头部 -->
<v-toolbar density="compact" class="flex-shrink-0">
<v-toolbar-title>页面标题</v-toolbar-title>
<v-spacer />
<v-btn icon="mdi-refresh" @click="refresh" />
</v-toolbar>
<!-- 可滚动内容区 -->
<div class="flex-grow-1 overflow-y-auto pa-4" style="min-height: 0">
<slot />
</div>
<!-- 固定底部可选 -->
<v-footer app class="flex-shrink-0">
<v-btn block color="primary">操作</v-btn>
</v-footer>
</v-container>
</template>
```
## Flexbox 滚动陷阱解决方案
```css
/* 问题:子元素撑破父容器 */
.parent {
display: flex;
flex-direction: column;
height: 100%;
}
.content {
flex-grow: 1;
/* 必须添加以下任一属性 */
min-height: 0; /* 推荐 */
/* 或 */
overflow: hidden;
}
```
## 粘性筛选栏
```vue
<template>
<div class="flex-grow-1 overflow-y-auto" style="min-height: 0">
<!-- 粘性筛选区 -->
<div class="sticky-top bg-surface pa-4" style="z-index: 1">
<v-row>
<v-col cols="4">
<v-text-field v-model="search" label="搜索" />
</v-col>
<v-col cols="4">
<v-select v-model="status" :items="statusOptions" label="状态" />
</v-col>
</v-row>
</div>
<!-- 列表内容 -->
<v-list>...</v-list>
</div>
</template>
<style scoped>
.sticky-top {
position: sticky;
top: 0;
}
</style>
```
## 分栏布局(侧边栏 + 主内容)
```vue
<template>
<div class="d-flex h-100">
<!-- 固定宽度侧边栏 -->
<v-navigation-drawer permanent width="280">
<v-list nav>...</v-list>
</v-navigation-drawer>
<!-- 自适应主内容 -->
<div class="flex-grow-1 d-flex flex-column" style="min-width: 0">
<v-toolbar>主内容头部</v-toolbar>
<div class="flex-grow-1 overflow-y-auto pa-4" style="min-height: 0">
主内容区域
</div>
</div>
</div>
</template>
```
## 双栏详情布局
```vue
<template>
<div class="d-flex flex-column h-100">
<v-toolbar density="compact" class="flex-shrink-0">
<v-btn icon="mdi-arrow-left" @click="goBack" />
<v-toolbar-title>详情</v-toolbar-title>
</v-toolbar>
<div class="flex-grow-1 d-flex" style="min-height: 0">
<!-- 左侧主信息 -->
<div class="flex-grow-1 overflow-y-auto pa-4" style="min-width: 0">
<v-card>...</v-card>
</div>
<!-- 右侧边栏 -->
<v-sheet width="320" class="flex-shrink-0 overflow-y-auto border-s">
<v-list>相关信息</v-list>
</v-sheet>
</div>
</div>
</template>
```
## Tab 切换布局
```vue
<template>
<div class="d-flex flex-column h-100">
<v-tabs v-model="activeTab" class="flex-shrink-0">
<v-tab value="info">基本信息</v-tab>
<v-tab value="logs">操作日志</v-tab>
<v-tab value="settings">设置</v-tab>
</v-tabs>
<v-divider />
<v-tabs-window v-model="activeTab" class="flex-grow-1" style="min-height: 0">
<v-tabs-window-item value="info" class="h-100 overflow-y-auto">
<!-- 内容 -->
</v-tabs-window-item>
<v-tabs-window-item value="logs" class="h-100 overflow-y-auto">
<!-- 内容 -->
</v-tabs-window-item>
<v-tabs-window-item value="settings" class="h-100 overflow-y-auto">
<!-- 内容 -->
</v-tabs-window-item>
</v-tabs-window>
</div>
</template>
```
## 响应式断点处理
```vue
<script setup lang="ts">
import { useDisplay } from 'vuetify'
const { mobile, mdAndUp, lgAndUp } = useDisplay()
// 根据屏幕尺寸调整列数
const gridCols = computed(() => {
if (lgAndUp.value) return 4
if (mdAndUp.value) return 3
return 2
})
</script>
<template>
<v-row>
<v-col v-for="item in items" :key="item.id" :cols="12 / gridCols">
<v-card>...</v-card>
</v-col>
</v-row>
<!-- 移动端特殊处理 -->
<v-bottom-navigation v-if="mobile" grow>
<v-btn value="home">
<v-icon>mdi-home</v-icon>
<span>首页</span>
</v-btn>
</v-bottom-navigation>
</template>
```

View File

@@ -0,0 +1,142 @@
# TypeScript 严格规范
## 类型定义位置
```
src/types/
├── api.d.ts # ApiResponse, PageParams, etc.
├── user.d.ts # User domain types
├── order.d.ts # Order domain types
└── common.d.ts # Shared utilities
```
## 禁止 `any` 的替代方案
| 场景 | 错误 | 正确 |
|------|------|------|
| 未知对象 | `any` | `Record<string, unknown>` |
| 动态数组 | `any[]` | `unknown[]` + type guard |
| 回调函数 | `(x: any) => any` | 泛型 `<T>(x: T) => T` |
| 第三方库缺失类型 | `any` | 创建 `.d.ts` 声明文件 |
## Props 与 Emits 类型化
```typescript
// Props
interface Props {
user: User
mode?: 'view' | 'edit'
onSave?: (data: User) => void
}
const props = withDefaults(defineProps<Props>(), {
mode: 'view',
})
// Emits
interface Emits {
(e: 'update', value: User): void
(e: 'cancel'): void
}
const emit = defineEmits<Emits>()
```
## 泛型组合式函数
```typescript
// composables/useFetch.ts
export function useFetch<T>(
fetcher: () => Promise<T>,
options?: { immediate?: boolean }
) {
const data = ref<T | null>(null) as Ref<T | null>
const loading = ref(false)
const error = ref<Error | null>(null)
async function execute() {
loading.value = true
error.value = null
try {
data.value = await fetcher()
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
} finally {
loading.value = false
}
}
if (options?.immediate !== false) {
onMounted(execute)
}
return { data, loading, error, execute }
}
```
## 类型守卫
```typescript
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj
)
}
// 使用
const data: unknown = await fetchData()
if (isUser(data)) {
console.log(data.name) // TypeScript 知道这是 User
}
```
## 联合类型与字面量类型
```typescript
// 状态枚举替代方案
type UserStatus = 'active' | 'disabled' | 'pending'
// 带类型的事件处理
type TableAction =
| { type: 'edit'; payload: User }
| { type: 'delete'; payload: string }
| { type: 'view'; payload: User }
function handleAction(action: TableAction) {
switch (action.type) {
case 'edit':
openEditDialog(action.payload) // payload 是 User
break
case 'delete':
confirmDelete(action.payload) // payload 是 string (id)
break
}
}
```
## 工具类型使用
```typescript
// Partial - 所有属性可选
type UpdateUserDto = Partial<User>
// Pick - 选择部分属性
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>
// Omit - 排除部分属性
type CreateUserDto = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
// Required - 所有属性必填
type CompleteUser = Required<User>
// Record - 键值映射
type UserMap = Record<string, User>
// 自定义工具类型
type Nullable<T> = T | null
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : never
```

View File

@@ -0,0 +1,345 @@
# UI 交互规范
## 数据表格
```vue
<template>
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
fixed-header
height="calc(100vh - 200px)"
>
<!-- 截断文本 + Tooltip -->
<template #item.description="{ value }">
<v-tooltip :text="value" location="top">
<template #activator="{ props }">
<span
v-bind="props"
class="text-truncate d-inline-block"
style="max-width: 200px"
>
{{ value }}
</span>
</template>
</v-tooltip>
</template>
<!-- 空状态 -->
<template #no-data>
<v-empty-state
icon="mdi-database-off"
title="暂无数据"
text="请尝试调整筛选条件或新建记录"
>
<template #actions>
<v-btn color="primary" @click="refresh">刷新</v-btn>
</template>
</v-empty-state>
</template>
</v-data-table>
</template>
```
## 虚拟滚动列表
```vue
<template>
<v-virtual-scroll :items="largeList" height="400" item-height="64">
<template #default="{ item }">
<v-list-item :title="item.name" :subtitle="item.description">
<template #append>
<v-btn icon="mdi-chevron-right" variant="text" />
</template>
</v-list-item>
</template>
</v-virtual-scroll>
</template>
```
## 骨架屏加载
```vue
<template>
<div>
<!-- 表格骨架 -->
<v-skeleton-loader v-if="loading" type="table-heading, table-row@5" />
<!-- 卡片骨架 -->
<v-skeleton-loader v-if="loading" type="card" />
<!-- 列表骨架 -->
<v-skeleton-loader v-if="loading" type="list-item-avatar-two-line@3" />
<!-- 文章骨架 -->
<v-skeleton-loader v-if="loading" type="article" />
<!-- 自定义组合骨架 -->
<v-skeleton-loader
v-if="loading"
type="heading, list-item-two-line@3, actions"
/>
<!-- 实际内容 -->
<template v-else>...</template>
</div>
</template>
```
## 空状态组件
```vue
<template>
<v-empty-state
:icon="icon"
:title="title"
:text="description"
>
<template #actions>
<v-btn v-if="showRefresh" variant="outlined" @click="$emit('refresh')">
刷新
</v-btn>
<v-btn v-if="showCreate" color="primary" @click="$emit('create')">
新建
</v-btn>
</template>
</v-empty-state>
</template>
<script setup lang="ts">
interface Props {
icon?: string
title?: string
description?: string
showRefresh?: boolean
showCreate?: boolean
}
withDefaults(defineProps<Props>(), {
icon: 'mdi-folder-open-outline',
title: '暂无数据',
description: '当前没有可显示的内容',
showRefresh: true,
showCreate: false,
})
defineEmits<{
(e: 'refresh'): void
(e: 'create'): void
}>()
</script>
```
## 多行文本截断
```vue
<template>
<div class="line-clamp-container">
<p :class="{ 'line-clamp-2': !expanded }">{{ longText }}</p>
<v-btn
v-if="needsExpand"
variant="text"
size="small"
@click="expanded = !expanded"
>
{{ expanded ? '收起' : '展开' }}
</v-btn>
</div>
</template>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
```
## 确认对话框
```vue
<template>
<v-dialog v-model="dialog" max-width="400" persistent>
<v-card>
<v-card-title class="d-flex align-center">
<v-icon :color="iconColor" class="mr-2">{{ icon }}</v-icon>
{{ title }}
</v-card-title>
<v-card-text>{{ message }}</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="cancel">取消</v-btn>
<v-btn :color="confirmColor" :loading="loading" @click="confirm">
{{ confirmText }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
interface Props {
title?: string
message: string
icon?: string
iconColor?: string
confirmText?: string
confirmColor?: string
}
withDefaults(defineProps<Props>(), {
title: '确认操作',
icon: 'mdi-alert-circle-outline',
iconColor: 'warning',
confirmText: '确认',
confirmColor: 'primary',
})
const dialog = defineModel<boolean>({ default: false })
const loading = ref(false)
const emit = defineEmits<{
(e: 'confirm'): void
(e: 'cancel'): void
}>()
function confirm() {
emit('confirm')
}
function cancel() {
dialog.value = false
emit('cancel')
}
</script>
```
## 表单验证
```vue
<template>
<v-form ref="formRef" v-model="valid" @submit.prevent="submit">
<v-text-field
v-model="form.name"
:rules="rules.name"
label="姓名"
required
/>
<v-text-field
v-model="form.email"
:rules="rules.email"
label="邮箱"
type="email"
/>
<v-btn type="submit" :disabled="!valid" :loading="loading">
提交
</v-btn>
</v-form>
</template>
<script setup lang="ts">
import type { VForm } from 'vuetify/components'
const formRef = ref<VForm | null>(null)
const valid = ref(false)
const loading = ref(false)
const form = reactive({
name: '',
email: '',
})
const rules = {
name: [
(v: string) => !!v || '姓名不能为空',
(v: string) => v.length <= 20 || '姓名不能超过20个字符',
],
email: [
(v: string) => !!v || '邮箱不能为空',
(v: string) => /.+@.+\..+/.test(v) || '请输入有效的邮箱地址',
],
}
async function submit() {
const { valid } = await formRef.value!.validate()
if (!valid) return
loading.value = true
try {
await api.submit(form)
} finally {
loading.value = false
}
}
</script>
```
## Snackbar 全局通知
```typescript
// composables/useSnackbar.ts
import { ref } from 'vue'
interface SnackbarOptions {
text: string
color?: string
timeout?: number
}
const snackbar = ref<SnackbarOptions & { show: boolean }>({
show: false,
text: '',
color: 'success',
timeout: 3000,
})
export function useSnackbar() {
function show(options: SnackbarOptions) {
snackbar.value = { ...snackbar.value, ...options, show: true }
}
function success(text: string) {
show({ text, color: 'success' })
}
function error(text: string) {
show({ text, color: 'error', timeout: 5000 })
}
function warning(text: string) {
show({ text, color: 'warning' })
}
function info(text: string) {
show({ text, color: 'info' })
}
return { snackbar, show, success, error, warning, info }
}
```
```vue
<!-- App.vue 中使用 -->
<template>
<v-app>
<router-view />
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
>
{{ snackbar.text }}
</v-snackbar>
</v-app>
</template>
<script setup lang="ts">
import { useSnackbar } from '@/composables/useSnackbar'
const { snackbar } = useSnackbar()
</script>
```

View File

@@ -0,0 +1,184 @@
---
name: developing-project-management
description: Guides development of rmdc-project-management module including project lifecycle management, version control (Git-like), ACL permissions, TOTP authorization, and workflow integration. Triggered when modifying project CRUD, draft/version APIs, permission grants, or authorization features. Keywords: project lifecycle, version snapshot, ACL, TOTP, workflow callback, SuperAdmin.
argument-hint: "<change-type> [target]" where change-type is one of: api|entity|service|migration|frontend|auth. Example: "api draft-submit" or "migration add-field"
allowed-tools:
- Read
- Glob
- Grep
- Bash
- Edit
- Write
---
# Developing Project Management Module
本 Skill 指导 `rmdc-project-management` 模块的开发,该模块是 RMDC 系统的核心业务模块,负责以 K8s Namespace 为粒度的项目全生命周期管理。
## 模块定位
- **核心职责**: 项目 CRUD、版本控制(Git-like)、细粒度 ACL 权限、一级 TOTP 授权
- **技术栈**: Go + Gin + GORM + PostgreSQL (JSONB)
- **架构**: 模块化单体,通过接口注入与 `rmdc-work-procedure` 工单模块协作
## 动态上下文注入
使用前先获取当前仓库状态:
```bash
# 查看项目管理模块目录结构
!`find . -path "*/rmdc-project-management/*" -name "*.go" | head -20`
# 查找生命周期状态相关代码
!`grep -rn "lifecycle_status\|LifecycleStatus" --include="*.go" | head -15`
```
---
## Plan计划阶段
### 产物清单
根据 `$ARGUMENTS` 确定变更范围:
| 变更类型 | 产物文件 | 影响模块 |
|:---|:---|:---|
| `api` | `handler/*.go`, `router.go` | rmdc-core 路由注册 |
| `entity` | `entity/*.go` | 数据库迁移、DTO 映射 |
| `service` | `service/*.go` | 业务逻辑、版本快照 |
| `migration` | `migrations/*.sql` | 数据库 Schema |
| `frontend` | `pages/*.vue`, `components/*.vue` | 前端联调 |
| `auth` | `service/auth_*.go` | TOTP 授权、Exchange-Hub 交互 |
### 决策点
1. **是否涉及生命周期状态变更?**
- 若涉及,必须同步更新状态机转换逻辑
- 检查 `reference/lifecycle-state-machine.md`
2. **是否修改版本快照结构?**
- 若涉及,需评估历史版本兼容性
- 更新 `VersionSnapshot` 结构体
3. **是否变更 ACL 权限模型?**
- 若涉及,需同步 `rmdc-user-auth` 模块
- 检查 `reference/acl-permission-model.md`
4. **是否影响工单模块回调?**
- 若涉及,需更新 `ProjectLifecycleUpdater` 接口实现
- 检查 `reference/workflow-state-mapping.md`
---
## Verify验证阶段
### Checklist
- [ ] **生命周期状态机完整性**: 所有状态转换有明确的触发条件和权限控制
- [ ] **版本快照一致性**: `projects` 表与 `project_versions` 表数据同步
- [ ] **乐观锁检查**: 并发修改时 `base_version == current_version` 校验存在
- [ ] **ACL 权限验证**: 接口权限注解与业务逻辑一致
- [ ] **工单回调幂等**: 状态更新操作具备幂等性
- [ ] **敏感字段加密**: 密码字段使用 AES-256 加密存储
- [ ] **审计日志**: 所有写操作记录到 `rmdc-audit-log`
- [ ] **TOTP 授权安全**: 一级密钥仅 SuperAdmin 可访问
### 验证命令
```bash
# 检查实体字段与数据库 Schema 一致性
!`grep -rn "gorm:\"" entity/project.go | head -20`
# 检查 API 路由权限注解
!`grep -rn "RequireRole\|RequirePermission" handler/*.go`
# 运行模块单元测试
go test ./internal/project/... -v -cover
```
---
## Execute执行阶段
### API 开发流程
1. **定义请求/响应结构体**`dto/project_dto.go`
2. **实现 Service 方法**`service/project_service.go`
3. **实现 Handler 方法**`handler/project_handler.go`
4. **注册路由**`router.go` (注意权限中间件)
5. **编写单元测试**`*_test.go`
### 版本快照变更流程
1. 更新 `VersionSnapshot` 结构体定义
2. 确保 `CompareVersions` Diff 算法兼容新字段
3. 添加字段到 Diff 结果的字段名映射表
4. 测试历史版本查看功能不受影响
### 生命周期状态变更流程
1. 更新 `reference/lifecycle-state-machine.md` 状态图
2. 修改 `service/lifecycle_service.go` 状态转换逻辑
3. 同步更新 `ProjectLifecycleUpdater` 接口实现
4. 验证与工单模块的状态映射表一致
### 授权功能变更流程
1. 检查 `project_auth_configs` 表结构
2. 更新 `AuthorizationInfo` 结构体
3. 确保 TOTP 密钥生成/验证逻辑正确
4. 测试与 Exchange-Hub 的授权指令下发
---
## Pitfalls常见问题
1. **超管直改未生成版本**: SuperAdmin 直接修改 `projects` 表时,必须同时插入 `project_versions` 记录,否则版本链断裂
2. **草稿基准版本过期**: 用户 A 基于 v3 创建草稿,超管修改产生 v4用户 A 提交时需检测冲突并提示 Rebase
3. **工单回调重复处理**: 工单模块可能重试回调,`ProjectLifecycleUpdater` 实现必须幂等
4. **ACL 权限遗漏授权模块**: `authorization_info` 模块仅 SuperAdmin 可见,其他角色查询时需过滤
5. **密码字段明文泄露**: `AdminPassword``SSHPwd` 等字段响应时必须脱敏或不返回
6. **省市级联校验缺失**: 前端省市级联选择后,后端需校验省市对应关系有效性
7. **Namespace 唯一性**: 创建项目时必须校验 `namespace` 全局唯一且符合 RFC 1123 DNS 标签规范
8. **JSONB 字段空值处理**: `basic_info``deploy_business` 等 JSONB 字段为空时,需返回空对象 `{}` 而非 `null`
---
## 模块依赖关系
```
rmdc-project-management
├── → rmdc-user-auth (用户鉴权、ACL 权限查询)
├── → rmdc-work-procedure (工单创建、状态转换)
├── → rmdc-audit-log (操作审计记录)
├── → rmdc-exchange-hub (授权指令下发)
└── ← rmdc-core (路由注册、依赖注入)
```
## 关键接口
| 类别 | 路径 | 权限 |
|:---|:---|:---|
| 项目列表 | `POST /api/project/list` | Login |
| 项目详情 | `POST /api/project/detail` | View ACL |
| 创建项目 | `POST /api/project/create` | SuperAdmin |
| 直接更新 | `POST /api/project/update` | SuperAdmin |
| 保存草稿 | `POST /api/project/draft/save` | View ACL |
| 提交审核 | `POST /api/project/draft/submit` | View ACL |
| 版本历史 | `POST /api/project/version/list` | View ACL |
| 权限分配 | `POST /api/project/permission/grant` | SuperAdmin |
## 相关文档
- 生命周期状态机: `reference/lifecycle-state-machine.md`
- API 端点清单: `reference/api-endpoints.md`
- 数据库 Schema: `reference/database-schema.md`
- ACL 权限模型: `reference/acl-permission-model.md`
- 工单状态映射: `reference/workflow-state-mapping.md`

View File

@@ -0,0 +1,280 @@
package service
import (
"context"
"fmt"
"time"
)
// ProjectLifecycleUpdater 项目生命周期状态更新接口
// 由 rmdc-core 注入,工单模块状态变更时调用
type ProjectLifecycleUpdater interface {
UpdateLifecycleStatus(projectID, lifecycleStatus string) error
SetLifecycleToDrafting(projectID string) error
SetLifecycleToReviewing(projectID string) error
SetLifecycleToReleased(projectID string) error
SetLifecycleToModifying(projectID string) error
}
// ProjectLifecycleService 项目生命周期服务实现
type ProjectLifecycleService struct {
repo ProjectRepository
versionSvc *VersionService
auditSvc AuditService
idempotency IdempotencyChecker
}
// NewProjectLifecycleService 创建项目生命周期服务
func NewProjectLifecycleService(
repo ProjectRepository,
versionSvc *VersionService,
auditSvc AuditService,
) *ProjectLifecycleService {
return &ProjectLifecycleService{
repo: repo,
versionSvc: versionSvc,
auditSvc: auditSvc,
}
}
// UpdateLifecycleStatus 更新项目生命周期状态
func (s *ProjectLifecycleService) UpdateLifecycleStatus(projectID, lifecycleStatus string) error {
ctx := context.Background()
// 幂等性检查
idempotencyKey := fmt.Sprintf("lifecycle:%s:%s:%d", projectID, lifecycleStatus, time.Now().Unix()/60)
if s.idempotency != nil && s.idempotency.Exists(idempotencyKey) {
return nil // 已处理,直接返回
}
// 更新生命周期状态
err := s.repo.UpdateLifecycleStatus(ctx, projectID, lifecycleStatus)
if err != nil {
return fmt.Errorf("update lifecycle status failed: %w", err)
}
// 记录审计日志
s.auditSvc.Log(ctx, AuditLog{
Resource: "project",
Action: "lifecycle_change",
ResourceID: projectID,
Details: map[string]interface{}{
"new_status": lifecycleStatus,
},
})
// 标记已处理
if s.idempotency != nil {
s.idempotency.Set(idempotencyKey, 24*time.Hour)
}
return nil
}
// SetLifecycleToReleased 设置为已发布状态(审批通过时)
func (s *ProjectLifecycleService) SetLifecycleToReleased(projectID string) error {
ctx := context.Background()
// 1. 获取项目信息
project, err := s.repo.GetByProjectID(ctx, projectID)
if err != nil {
return fmt.Errorf("get project failed: %w", err)
}
// 2. 获取草稿快照
draftVersion, err := s.repo.GetActiveDraft(ctx, projectID)
if err != nil {
return fmt.Errorf("get draft failed: %w", err)
}
// 3. 开启事务
return s.repo.Transaction(ctx, func(txCtx context.Context) error {
// 3.1 更新生命周期状态
if err := s.repo.UpdateLifecycleStatus(txCtx, projectID, LifecycleReleased); err != nil {
return err
}
// 3.2 更新认证状态为正式
if err := s.repo.UpdateCertificationStatus(txCtx, projectID, CertificationOfficial); err != nil {
return err
}
// 3.3 将草稿内容合并到主表
if err := s.repo.MergeDraftToMaster(txCtx, projectID, draftVersion.SnapshotData); err != nil {
return err
}
// 3.4 创建正式版本快照
snapshot := &VersionSnapshot{}
if err := parseSnapshot(draftVersion.SnapshotData, snapshot); err != nil {
return err
}
if err := s.versionSvc.CreateOfficialVersion(
txCtx,
projectID,
snapshot,
draftVersion.CommitterID,
draftVersion.CommitterName,
"审批通过,发布正式版本",
); err != nil {
return err
}
// 3.5 删除草稿
if err := s.repo.DeleteDraft(txCtx, projectID, draftVersion.UserID); err != nil {
return err
}
return nil
})
}
// SetLifecycleToDrafting 设置为填写中状态(工单被打回后)
func (s *ProjectLifecycleService) SetLifecycleToDrafting(projectID string) error {
ctx := context.Background()
err := s.repo.UpdateLifecycleStatus(ctx, projectID, LifecycleDrafting)
if err != nil {
return fmt.Errorf("set lifecycle to drafting failed: %w", err)
}
s.auditSvc.Log(ctx, AuditLog{
Resource: "project",
Action: "lifecycle_change",
ResourceID: projectID,
Details: map[string]interface{}{
"new_status": LifecycleDrafting,
"reason": "workflow_returned",
},
})
return nil
}
// SetLifecycleToReviewing 设置为审核中状态(提交审核时)
func (s *ProjectLifecycleService) SetLifecycleToReviewing(projectID string) error {
ctx := context.Background()
// 更新生命周期状态
err := s.repo.UpdateLifecycleStatus(ctx, projectID, LifecycleReviewing)
if err != nil {
return fmt.Errorf("set lifecycle to reviewing failed: %w", err)
}
// 更新认证状态为待审核
err = s.repo.UpdateCertificationStatus(ctx, projectID, CertificationPending)
if err != nil {
return fmt.Errorf("set certification to pending failed: %w", err)
}
s.auditSvc.Log(ctx, AuditLog{
Resource: "project",
Action: "lifecycle_change",
ResourceID: projectID,
Details: map[string]interface{}{
"new_status": LifecycleReviewing,
},
})
return nil
}
// SetLifecycleToModifying 设置为变更中状态(发起修改工单时)
func (s *ProjectLifecycleService) SetLifecycleToModifying(projectID string) error {
ctx := context.Background()
err := s.repo.UpdateLifecycleStatus(ctx, projectID, LifecycleModifying)
if err != nil {
return fmt.Errorf("set lifecycle to modifying failed: %w", err)
}
s.auditSvc.Log(ctx, AuditLog{
Resource: "project",
Action: "lifecycle_change",
ResourceID: projectID,
Details: map[string]interface{}{
"new_status": LifecycleModifying,
},
})
return nil
}
// CheckVersionConflict 检查版本冲突(乐观锁)
func (s *ProjectLifecycleService) CheckVersionConflict(
ctx context.Context,
projectID string,
baseVersion int,
) error {
currentVersion, err := s.repo.GetCurrentVersion(ctx, projectID)
if err != nil {
return fmt.Errorf("get current version failed: %w", err)
}
if currentVersion != baseVersion {
return &VersionConflictError{
ProjectID: projectID,
BaseVersion: baseVersion,
CurrentVersion: currentVersion,
}
}
return nil
}
// VersionConflictError 版本冲突错误
type VersionConflictError struct {
ProjectID string
BaseVersion int
CurrentVersion int
}
func (e *VersionConflictError) Error() string {
return fmt.Sprintf(
"version conflict: project %s base version %d, current version %d",
e.ProjectID, e.BaseVersion, e.CurrentVersion,
)
}
// 辅助类型和函数
type ProjectRepository interface {
GetByProjectID(ctx context.Context, projectID string) (*Project, error)
UpdateLifecycleStatus(ctx context.Context, projectID, status string) error
UpdateCertificationStatus(ctx context.Context, projectID, status string) error
GetActiveDraft(ctx context.Context, projectID string) (*ProjectVersion, error)
MergeDraftToMaster(ctx context.Context, projectID string, data []byte) error
DeleteDraft(ctx context.Context, projectID string, userID int64) error
GetCurrentVersion(ctx context.Context, projectID string) (int, error)
Transaction(ctx context.Context, fn func(ctx context.Context) error) error
}
type AuditService interface {
Log(ctx context.Context, log AuditLog)
}
type AuditLog struct {
Resource string
Action string
ResourceID string
Details map[string]interface{}
}
type IdempotencyChecker interface {
Exists(key string) bool
Set(key string, ttl time.Duration)
}
type Project struct {
ID int64
ProjectID string
LifecycleStatus string
CertificationStatus string
CurrentVersion int
}
func parseSnapshot(data []byte, snapshot *VersionSnapshot) error {
// JSON 解析实现
return nil
}

View File

@@ -0,0 +1,132 @@
package entity
import (
"encoding/json"
"time"
)
// Project 项目主表实体
type Project struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ProjectID string `gorm:"type:varchar(64);uniqueIndex;not null" json:"project_id"`
Name string `gorm:"type:varchar(128);not null" json:"name"`
Namespace string `gorm:"type:varchar(64);uniqueIndex;not null" json:"namespace"`
// 生命周期状态: INIT/DRAFTING/REVIEWING/RELEASED/MODIFYING/ARCHIVED
LifecycleStatus string `gorm:"type:varchar(32);default:'INIT'" json:"lifecycle_status"`
// 认证状态: draft/pending/official
CertificationStatus string `gorm:"type:varchar(32);default:'draft'" json:"certification_status"`
// 当前正式版本号
CurrentVersion int `gorm:"default:0" json:"current_version"`
// JSONB 存储
BasicInfo json.RawMessage `gorm:"type:jsonb" json:"basic_info"`
DeployBusiness json.RawMessage `gorm:"type:jsonb" json:"deploy_business"`
DeployEnv json.RawMessage `gorm:"type:jsonb" json:"deploy_env"`
DeployMiddleware json.RawMessage `gorm:"type:jsonb" json:"deploy_middleware"`
// 填写人
DetailFillerID int64 `json:"detail_filler_id"`
DetailFillerName string `gorm:"type:varchar(64)" json:"detail_filler_name"`
// 审计字段
CreatedBy int64 `json:"created_by"`
CreatedByName string `gorm:"type:varchar(64)" json:"created_by_name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
}
// TableName 指定表名
func (Project) TableName() string {
return "projects"
}
// 生命周期状态常量
const (
LifecycleInit = "INIT"
LifecycleDrafting = "DRAFTING"
LifecycleReviewing = "REVIEWING"
LifecycleReleased = "RELEASED"
LifecycleModifying = "MODIFYING"
LifecycleArchived = "ARCHIVED"
)
// 认证状态常量
const (
CertificationDraft = "draft"
CertificationPending = "pending"
CertificationOfficial = "official"
)
// ProjectVersion 项目版本表 (含草稿)
type ProjectVersion struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ProjectID string `gorm:"type:varchar(64);index;not null" json:"project_id"`
// 版本号 (正式版本递增, 草稿为0)
Version int `gorm:"not null;default:0" json:"version"`
// 版本类型: official/fill_draft/modify_draft
VersionType string `gorm:"type:varchar(32);not null" json:"version_type"`
// 基准版本号(草稿基于哪个正式版本创建)
BaseVersion int `gorm:"default:0" json:"base_version"`
// 草稿所属用户ID (仅草稿类型有值)
UserID int64 `gorm:"index" json:"user_id"`
UserName string `gorm:"type:varchar(64)" json:"user_name"`
// 关联工单ID (1:1关系, 仅草稿类型有值)
WorkflowID string `gorm:"type:varchar(64);index" json:"workflow_id"`
// 完整快照数据
SnapshotData json.RawMessage `gorm:"type:jsonb" json:"snapshot_data"`
// 变更信息
CommitMessage string `gorm:"type:varchar(255)" json:"commit_message"`
CommitterID int64 `json:"committer_id"`
CommitterName string `gorm:"type:varchar(64)" json:"committer_name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 指定表名
func (ProjectVersion) TableName() string {
return "project_versions"
}
// 版本类型常量
const (
VersionTypeOfficial = "official"
VersionTypeFillDraft = "fill_draft"
VersionTypeModifyDraft = "modify_draft"
)
// ProjectAuthConfig 项目授权配置
type ProjectAuthConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ProjectID string `gorm:"type:varchar(64);uniqueIndex;not null" json:"project_id"`
TierOneSecret string `gorm:"type:varchar(128)" json:"tier_one_secret"` // 一级TOTP密钥
TimeOffset int `gorm:"default:30" json:"time_offset"` // 允许时间偏移(秒)
TOTPEnabled bool `gorm:"default:false" json:"totp_enabled"`
TierTwoSecret string `gorm:"type:varchar(128)" json:"tier_two_secret"` // 二级TOTP密钥
AuthType string `gorm:"type:varchar(32)" json:"auth_type"` // permanent/time_limited
AuthDays int `json:"auth_days"` // 授权有效期(天)
AuthorizedAt *time.Time `json:"authorized_at"`
RevokedAt *time.Time `json:"revoked_at"`
IsOffline bool `gorm:"default:false" json:"is_offline"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 指定表名
func (ProjectAuthConfig) TableName() string {
return "project_auth_configs"
}

View File

@@ -0,0 +1,315 @@
package service
import (
"context"
"encoding/json"
"fmt"
)
// VersionSnapshot 版本快照结构
type VersionSnapshot struct {
BasicInfo *BasicInfo `json:"basic_info"`
DeployBusiness *DeployBusiness `json:"deploy_business"`
DeployEnv *DeployEnv `json:"deploy_env"`
DeployMiddleware *DeployMiddleware `json:"deploy_middleware"`
}
// BasicInfo 基本信息
type BasicInfo struct {
Province string `json:"province"`
City string `json:"city"`
IndustryContact string `json:"industry_contact"`
IndustryPhone string `json:"industry_phone"`
ProjectNature string `json:"project_nature"`
}
// DeployBusiness 部署业务信息
type DeployBusiness struct {
DeployerName string `json:"deployer_name"`
DeployerPhone string `json:"deployer_phone"`
DeployStartTime string `json:"deploy_start_time"`
DeployEndTime string `json:"deploy_end_time"`
SystemVersion string `json:"system_version"`
SystemType string `json:"system_type"`
MainEntrance string `json:"main_entrance"`
AdminUsername string `json:"admin_username"`
AdminPassword string `json:"admin_password"` // 加密存储
}
// DeployEnv 部署环境信息
type DeployEnv struct {
Hosts []HostInfo `json:"hosts"`
NetworkType string `json:"network_type"`
MainPublicIP string `json:"main_public_ip"`
DomainURL string `json:"domain_url"`
SSLEnabled bool `json:"ssl_enabled"`
ManagementType string `json:"management_type"`
ManagementURL string `json:"management_url"`
ManagementUser string `json:"management_user"`
ManagementPwd string `json:"management_pwd"` // 加密存储
HostCount int `json:"host_count"`
TotalCPU int `json:"total_cpu"`
CPUModel string `json:"cpu_model"`
TotalMemory int `json:"total_memory"`
TotalStorage int `json:"total_storage"`
}
// HostInfo 主机信息
type HostInfo struct {
Hostname string `json:"hostname"`
InternalIP string `json:"internal_ip"`
PublicIP string `json:"public_ip"`
CanAccessPublic bool `json:"can_access_public"`
SSHPort int `json:"ssh_port"`
SSHUser string `json:"ssh_user"`
SSHPwd string `json:"ssh_pwd"` // 加密存储
Role string `json:"role"` // master/worker/storage
}
// DeployMiddleware 部署中间件信息
type DeployMiddleware struct {
MySQL MiddlewareInfo `json:"mysql"`
Redis MiddlewareInfo `json:"redis"`
EMQX MiddlewareInfo `json:"emqx"`
MinIO MiddlewareInfo `json:"minio"`
InfluxDB MiddlewareInfo `json:"influxdb"`
Nacos MiddlewareInfo `json:"nacos"`
K8SDashboard MiddlewareInfo `json:"k8s_dashboard"`
}
// MiddlewareInfo 中间件信息
type MiddlewareInfo struct {
PublicIP string `json:"public_ip"`
PublicPort int `json:"public_port"`
InternalIP string `json:"internal_ip"`
InternalPort int `json:"internal_port"`
K8SAddress string `json:"k8s_address"`
K8SPort int `json:"k8s_port"`
AdminUser string `json:"admin_user"`
AdminPwd string `json:"admin_pwd"` // 加密存储
Version string `json:"version"`
}
// DiffResult 差异结果
type DiffResult struct {
Module string `json:"module"`
FieldDiffs []FieldDiff `json:"field_diffs"`
}
// FieldDiff 字段差异
type FieldDiff struct {
FieldPath string `json:"field_path"`
FieldName string `json:"field_name"`
OldValue interface{} `json:"old_value"`
NewValue interface{} `json:"new_value"`
ChangeType string `json:"change_type"` // add/modify/delete
}
// VersionService 版本服务
type VersionService struct {
repo VersionRepository
fieldMap map[string]string // 字段路径到中文名的映射
}
// NewVersionService 创建版本服务
func NewVersionService(repo VersionRepository) *VersionService {
return &VersionService{
repo: repo,
fieldMap: initFieldNameMap(),
}
}
// initFieldNameMap 初始化字段名映射
func initFieldNameMap() map[string]string {
return map[string]string{
"basic_info.province": "省份",
"basic_info.city": "城市",
"basic_info.industry_contact": "行业组人员",
"basic_info.industry_phone": "行业组电话",
"basic_info.project_nature": "项目性质",
"deploy_business.deployer_name": "部署人姓名",
"deploy_business.system_version": "系统版本",
"deploy_env.host_count": "主机台数",
"deploy_env.main_public_ip": "主公网IP",
"deploy_env.domain_url": "域名URL",
// ... 更多字段映射
}
}
// CompareVersions 比较两个版本的差异
func (s *VersionService) CompareVersions(
ctx context.Context,
baseVersion, targetVersion *VersionSnapshot,
) ([]DiffResult, error) {
var results []DiffResult
modules := []struct {
Name string
Base interface{}
Target interface{}
}{
{"基本信息", baseVersion.BasicInfo, targetVersion.BasicInfo},
{"部署业务", baseVersion.DeployBusiness, targetVersion.DeployBusiness},
{"部署环境", baseVersion.DeployEnv, targetVersion.DeployEnv},
{"部署中间件", baseVersion.DeployMiddleware, targetVersion.DeployMiddleware},
}
for _, m := range modules {
diffs := s.diffJSON(m.Name, m.Base, m.Target)
if len(diffs) > 0 {
results = append(results, DiffResult{
Module: m.Name,
FieldDiffs: diffs,
})
}
}
return results, nil
}
// diffJSON 对比两个结构体的差异
func (s *VersionService) diffJSON(moduleName string, base, target interface{}) []FieldDiff {
var diffs []FieldDiff
baseBytes, _ := json.Marshal(base)
targetBytes, _ := json.Marshal(target)
var baseMap, targetMap map[string]interface{}
json.Unmarshal(baseBytes, &baseMap)
json.Unmarshal(targetBytes, &targetMap)
// 遍历目标版本的字段
for key, newVal := range targetMap {
oldVal, exists := baseMap[key]
fieldPath := fmt.Sprintf("%s.%s", moduleName, key)
if !exists {
// 新增字段
diffs = append(diffs, FieldDiff{
FieldPath: fieldPath,
FieldName: s.getFieldName(fieldPath),
OldValue: nil,
NewValue: newVal,
ChangeType: "add",
})
} else if !equalValues(oldVal, newVal) {
// 修改字段
diffs = append(diffs, FieldDiff{
FieldPath: fieldPath,
FieldName: s.getFieldName(fieldPath),
OldValue: oldVal,
NewValue: newVal,
ChangeType: "modify",
})
}
}
// 检查删除的字段
for key, oldVal := range baseMap {
if _, exists := targetMap[key]; !exists {
fieldPath := fmt.Sprintf("%s.%s", moduleName, key)
diffs = append(diffs, FieldDiff{
FieldPath: fieldPath,
FieldName: s.getFieldName(fieldPath),
OldValue: oldVal,
NewValue: nil,
ChangeType: "delete",
})
}
}
return diffs
}
// getFieldName 获取字段中文名
func (s *VersionService) getFieldName(fieldPath string) string {
if name, ok := s.fieldMap[fieldPath]; ok {
return name
}
return fieldPath
}
// equalValues 比较两个值是否相等
func equalValues(a, b interface{}) bool {
aBytes, _ := json.Marshal(a)
bBytes, _ := json.Marshal(b)
return string(aBytes) == string(bBytes)
}
// CreateOfficialVersion 创建正式版本(审批通过时调用)
func (s *VersionService) CreateOfficialVersion(
ctx context.Context,
projectID string,
snapshot *VersionSnapshot,
committerID int64,
committerName string,
commitMessage string,
) error {
// 1. 获取当前版本号
currentVersion, err := s.repo.GetCurrentVersion(ctx, projectID)
if err != nil {
return fmt.Errorf("get current version failed: %w", err)
}
// 2. 序列化快照
snapshotData, err := json.Marshal(snapshot)
if err != nil {
return fmt.Errorf("marshal snapshot failed: %w", err)
}
// 3. 创建新版本记录
newVersion := &ProjectVersion{
ProjectID: projectID,
Version: currentVersion + 1,
VersionType: VersionTypeOfficial,
SnapshotData: snapshotData,
CommitMessage: commitMessage,
CommitterID: committerID,
CommitterName: committerName,
}
// 4. 事务中插入版本并更新项目当前版本号
return s.repo.CreateVersionAndUpdateProject(ctx, newVersion)
}
// GetVersionHistory 获取版本历史
func (s *VersionService) GetVersionHistory(
ctx context.Context,
projectID string,
page, pageSize int,
) ([]*VersionHistory, int64, error) {
return s.repo.GetVersionHistory(ctx, projectID, page, pageSize)
}
// VersionHistory 版本历史记录
type VersionHistory struct {
Version int `json:"version"`
VersionType string `json:"version_type"`
CommitMessage string `json:"commit_message"`
CommitterID int64 `json:"committer_id"`
CommitterName string `json:"committer_name"`
WorkflowID string `json:"workflow_id"`
CreatedAt string `json:"created_at"`
ChangeSummary string `json:"change_summary"`
}
// VersionRepository 版本仓储接口
type VersionRepository interface {
GetCurrentVersion(ctx context.Context, projectID string) (int, error)
CreateVersionAndUpdateProject(ctx context.Context, version *ProjectVersion) error
GetVersionHistory(ctx context.Context, projectID string, page, pageSize int) ([]*VersionHistory, int64, error)
GetVersionByNumber(ctx context.Context, projectID string, version int) (*ProjectVersion, error)
}
// ProjectVersion 项目版本实体(简化版)
type ProjectVersion struct {
ProjectID string
Version int
VersionType string
BaseVersion int
UserID int64
WorkflowID string
SnapshotData []byte
CommitMessage string
CommitterID int64
CommitterName string
}

View File

@@ -0,0 +1,158 @@
# ACL 权限模型
## 功能权限 (RBAC)
| 权限代码 | 说明 | 角色 |
|:---|:---|:---|
| `project:create` | 创建项目 | SuperAdmin |
| `project:delete` | 删除/归档项目 | SuperAdmin |
| `project:edit` | 直接编辑项目 | SuperAdmin |
| `project:edit_workflow` | 通过工单编辑项目 | User (有ACL权限) |
| `project:auth_manage` | 一级/二级授权管理 | SuperAdmin |
| `project:permission_manage` | 项目权限分配 | SuperAdmin |
## 数据权限 (ACL) - 模块级别
### 模块定义
| 模块代码 | 模块名称 | 说明 |
|:---|:---|:---|
| `basic_info` | 基本信息模块 | 项目名称、命名空间、省份城市等 |
| `business_info` | 部署业务模块 | 部署人、部署时间、系统版本等 |
| `environment_info` | 部署环境模块 | 主机信息、网络环境、域名等 |
| `middleware_info` | 部署中间件模块 | MySQL、Redis、EMQX等配置 |
| `authorization_info` | 项目授权模块 | TOTP授权信息仅SuperAdmin |
### 权限类型
| 权限类型 | 说明 |
|:---|:---|
| `view` | 查看权限(可查看项目信息,可发起修改工单) |
| `export` | 导出权限(可导出项目信息) |
> **说明**:编辑权限通过工单系统实现,拥有 `view` 权限的用户可以发起修改工单,由 SuperAdmin 审批后生效。
## 权限规则
1. **SuperAdmin**: 拥有所有项目的所有模块的全部权限,可直接修改
2. **Admin**: 可以访问自己被授权的项目模块,可以向普通用户转授权限
3. **Normal User**: 只能访问被授权的项目模块,修改需通过工单
4. **项目填写人**: 自动获得该项目的查看权限
5. **授权模块**: 仅 SuperAdmin 可见
## ACL 表结构(位于 rmdc-user-auth
```go
// ProjectACL 项目权限表 (模块级别)
type ProjectACL struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
ProjectID string `gorm:"type:varchar(64);index;not null" json:"project_id"`
UserID int64 `gorm:"index;not null" json:"user_id"`
// 模块代码: basic_info/business_info/environment_info/middleware_info/authorization_info
ModuleCode string `gorm:"type:varchar(32);not null" json:"module_code"`
// 权限类型
CanView bool `gorm:"default:false" json:"can_view"`
CanExport bool `gorm:"default:false" json:"can_export"`
// 授权信息
GrantedBy int64 `json:"granted_by"`
GrantedAt time.Time `json:"granted_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
## 权限检查流程
```go
// CheckProjectModulePermission 检查用户对项目模块的权限
func (s *ACLService) CheckProjectModulePermission(
ctx context.Context,
userID int64,
projectID string,
moduleCode string,
permType string, // "view" or "export"
) (bool, error) {
// 1. 检查是否为 SuperAdmin
if s.IsSuperAdmin(ctx, userID) {
return true, nil
}
// 2. 授权模块仅 SuperAdmin 可访问
if moduleCode == "authorization_info" {
return false, nil
}
// 3. 查询 ACL 表
var acl ProjectACL
err := s.db.Where("project_id = ? AND user_id = ? AND module_code = ?",
projectID, userID, moduleCode).First(&acl).Error
if err != nil {
return false, nil
}
// 4. 检查权限类型
switch permType {
case "view":
return acl.CanView, nil
case "export":
return acl.CanExport, nil
default:
return false, nil
}
}
```
## 权限分配接口
### 授予权限
```json
// POST /api/project/permission/grant
{
"project_id": "proj_001",
"user_id": 123,
"modules": [
{
"module_code": "basic_info",
"can_view": true,
"can_export": false
},
{
"module_code": "business_info",
"can_view": true,
"can_export": true
}
]
}
```
### 批量设置权限
```json
// POST /api/project/permission/batch
{
"project_id": "proj_001",
"permissions": [
{
"user_id": 123,
"modules": ["basic_info", "business_info"]
},
{
"user_id": 456,
"modules": ["basic_info"]
}
]
}
```
## 权限继承规则
| 场景 | 规则 |
|:---|:---|
| 项目创建 | 填写人自动获得所有模块的 view 权限 |
| 权限转授 | Admin 只能转授自己拥有的权限 |
| 权限撤销 | 不影响已创建的草稿/工单 |
| 项目归档 | 保留权限记录,但无法访问 |

View File

@@ -0,0 +1,151 @@
# API 端点清单
## 项目管理
| 方法 | 路径 | 描述 | 权限 |
|:---|:---|:---|:---|
| POST | `/api/project/list` | 获取项目列表 (自动过滤ACL) | Login |
| POST | `/api/project/detail` | 获取项目详情 (Master版本) | View ACL |
| POST | `/api/project/create` | 创建项目 | SuperAdmin |
| POST | `/api/project/update` | 直接更新项目 | SuperAdmin |
| POST | `/api/project/delete` | 删除项目 (软删除) | SuperAdmin |
| POST | `/api/project/export` | 导出项目信息 | Export ACL |
## 版本管理
| 方法 | 路径 | 描述 | 权限 |
|:---|:---|:---|:---|
| POST | `/api/project/version/list` | 获取版本历史列表 | View ACL |
| POST | `/api/project/version/detail` | 获取指定版本详情 | View ACL |
| POST | `/api/project/version/diff` | 获取版本差异 | View ACL |
| POST | `/api/project/version/diff-with-current` | 对比指定版本与当前版本差异 | View ACL |
## 草稿管理
| 方法 | 路径 | 描述 | 权限 |
|:---|:---|:---|:---|
| POST | `/api/project/draft/get` | 获取当前用户的草稿 | View ACL |
| POST | `/api/project/draft/save` | 保存草稿 | View ACL |
| POST | `/api/project/draft/submit` | 提交审核 | View ACL |
| POST | `/api/project/draft/discard` | 放弃草稿 | View ACL |
## 权限管理 (SuperAdmin)
| 方法 | 路径 | 描述 | 权限 |
|:---|:---|:---|:---|
| POST | `/api/project/permission/list` | 获取项目权限列表 | SuperAdmin |
| POST | `/api/project/permission/grant` | 授予权限 | SuperAdmin |
| POST | `/api/project/permission/revoke` | 撤销权限 | SuperAdmin |
| POST | `/api/project/permission/batch` | 批量设置权限 | SuperAdmin |
## 授权管理 (SuperAdmin)
| 方法 | 路径 | 描述 | 权限 |
|:---|:---|:---|:---|
| POST | `/api/project/auth/config` | 获取授权配置 | SuperAdmin |
| POST | `/api/project/auth/update` | 更新授权配置 | SuperAdmin |
| POST | `/api/project/auth/grant` | 下发授权 | SuperAdmin |
| POST | `/api/project/auth/revoke` | 撤销授权 | SuperAdmin |
## 请求/响应示例
### 项目列表
**Request:**
```json
{
"page": 1,
"page_size": 20,
"keyword": "",
"lifecycle_status": "",
"province": ""
}
```
**Response:**
```json
{
"code": 0,
"message": "success",
"data": {
"total": 100,
"list": [
{
"project_id": "proj_001",
"name": "示例项目",
"namespace": "example-ns",
"lifecycle_status": "RELEASED",
"certification_status": "official",
"province": "北京市",
"city": "北京市"
}
]
}
}
```
### 保存草稿
**Request:**
```json
{
"project_id": "proj_001",
"basic_info": {
"province": "北京市",
"city": "北京市",
"industry_contact": "张三",
"industry_phone": "13800138000"
},
"deploy_business": {
"deployer_name": "李四",
"system_version": "v2.0.0"
}
}
```
**Response:**
```json
{
"code": 0,
"message": "草稿保存成功",
"data": {
"draft_id": 123,
"updated_at": "2026-01-21T10:00:00Z"
}
}
```
### 版本对比
**Request:**
```json
{
"project_id": "proj_001",
"base_version": 2,
"target_version": 3
}
```
**Response:**
```json
{
"code": 0,
"message": "success",
"data": {
"diffs": [
{
"module": "部署环境",
"field_diffs": [
{
"field_path": "deploy_env.host_count",
"field_name": "主机台数",
"old_value": 3,
"new_value": 5,
"change_type": "modify"
}
]
}
]
}
}
```

View File

@@ -0,0 +1,239 @@
# 数据库 Schema
## projects 表(项目主表)
```sql
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
project_id VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(128) NOT NULL,
namespace VARCHAR(64) UNIQUE NOT NULL,
-- 生命周期状态: INIT/DRAFTING/REVIEWING/RELEASED/MODIFYING/ARCHIVED
lifecycle_status VARCHAR(32) DEFAULT 'INIT',
-- 认证状态: draft/pending/official
certification_status VARCHAR(32) DEFAULT 'draft',
-- 当前正式版本号
current_version INT DEFAULT 0,
-- JSONB 存储
basic_info JSONB,
deploy_business JSONB,
deploy_env JSONB,
deploy_middleware JSONB,
-- 填写人
detail_filler_id BIGINT,
detail_filler_name VARCHAR(64),
-- 审计字段
created_by BIGINT,
created_by_name VARCHAR(64),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_projects_namespace ON projects(namespace);
CREATE INDEX idx_projects_lifecycle ON projects(lifecycle_status);
CREATE INDEX idx_projects_deleted ON projects(deleted_at);
```
## project_versions 表(版本历史)
```sql
CREATE TABLE project_versions (
id BIGSERIAL PRIMARY KEY,
project_id VARCHAR(64) NOT NULL,
-- 版本号 (正式版本递增, 草稿为0)
version INT NOT NULL DEFAULT 0,
-- 版本类型: official/fill_draft/modify_draft
version_type VARCHAR(32) NOT NULL,
-- 基准版本号(草稿基于哪个正式版本创建)
base_version INT DEFAULT 0,
-- 草稿所属用户
user_id BIGINT,
user_name VARCHAR(64),
-- 关联工单
workflow_id VARCHAR(64),
-- 完整快照
snapshot_data JSONB,
-- 变更信息
commit_message VARCHAR(255),
committer_id BIGINT,
committer_name VARCHAR(64),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_versions_project ON project_versions(project_id);
CREATE INDEX idx_versions_workflow ON project_versions(workflow_id);
CREATE INDEX idx_versions_user ON project_versions(user_id);
CREATE UNIQUE INDEX idx_versions_project_version ON project_versions(project_id, version) WHERE version > 0;
```
## project_workflows 表(项目工单关联)
```sql
CREATE TABLE project_workflows (
id BIGSERIAL PRIMARY KEY,
project_id VARCHAR(64) NOT NULL,
workflow_id VARCHAR(64) UNIQUE NOT NULL,
-- 工单类型: fill(填写)/modify(修改)
workflow_type VARCHAR(32) NOT NULL,
-- 工单状态 (冗余存储,便于查询)
status VARCHAR(32),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_pw_project ON project_workflows(project_id);
CREATE INDEX idx_pw_status ON project_workflows(status);
```
## project_auth_configs 表(授权配置)
```sql
CREATE TABLE project_auth_configs (
id BIGSERIAL PRIMARY KEY,
project_id VARCHAR(64) UNIQUE NOT NULL,
tier_one_secret VARCHAR(128), -- 一级TOTP密钥 (加密存储)
time_offset INT DEFAULT 30, -- 允许时间偏移(秒)
totp_enabled BOOLEAN DEFAULT FALSE,
tier_two_secret VARCHAR(128), -- 二级TOTP密钥 (来自Watchdog)
auth_type VARCHAR(32), -- permanent/time_limited
auth_days INT, -- 授权有效期(天)
authorized_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
is_offline BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_auth_project ON project_auth_configs(project_id);
```
## JSONB 结构定义
### basic_info
```json
{
"province": "北京市",
"city": "北京市",
"industry_contact": "张三",
"industry_phone": "13800138000",
"project_nature": "market"
}
```
### deploy_business
```json
{
"deployer_name": "李四",
"deployer_phone": "13900139000",
"deploy_start_time": "2026-01-01",
"deploy_end_time": "2026-01-15",
"system_version": "v2.0.0",
"system_type": "fly-control",
"main_entrance": "https://example.com",
"admin_username": "admin",
"admin_password": "<encrypted>"
}
```
### deploy_env
```json
{
"hosts": [
{
"hostname": "master-01",
"internal_ip": "192.168.1.10",
"public_ip": "",
"can_access_public": false,
"ssh_port": 22,
"ssh_user": "root",
"ssh_pwd": "<encrypted>",
"role": "master"
}
],
"network_type": "internal",
"main_public_ip": "",
"domain_url": "",
"ssl_enabled": false,
"management_type": "bastion",
"management_url": "",
"management_user": "",
"management_pwd": "<encrypted>",
"host_count": 3,
"total_cpu": 24,
"cpu_model": "Intel Xeon",
"total_memory": 64,
"total_storage": 1000
}
```
### deploy_middleware
```json
{
"mysql": {
"public_ip": "",
"public_port": 0,
"internal_ip": "192.168.1.10",
"internal_port": 3306,
"k8s_address": "mysql-svc",
"k8s_port": 3306,
"admin_user": "root",
"admin_pwd": "<encrypted>",
"version": "8.0"
},
"redis": { ... },
"emqx": { ... },
"minio": { ... },
"influxdb": { ... },
"nacos": { ... },
"k8s_dashboard": { ... }
}
```
## 敏感字段加密说明
以下字段必须使用 AES-256 加密存储:
| 表 | 字段路径 | 说明 |
|:---|:---|:---|
| projects | `deploy_business.admin_password` | 系统超管密码 |
| projects | `deploy_env.hosts[*].ssh_pwd` | SSH密码 |
| projects | `deploy_env.management_pwd` | 管理后台密码 |
| projects | `deploy_middleware.*.admin_pwd` | 中间件超管密码 |
| project_auth_configs | `tier_one_secret` | 一级TOTP密钥 |
| project_auth_configs | `tier_two_secret` | 二级TOTP密钥 |
## 索引优化建议
```sql
-- 常用查询优化
CREATE INDEX idx_projects_lifecycle_cert ON projects(lifecycle_status, certification_status);
CREATE INDEX idx_projects_province ON projects((basic_info->>'province'));
-- JSONB GIN 索引(按需添加)
CREATE INDEX idx_projects_basic_gin ON projects USING GIN (basic_info);
```

View File

@@ -0,0 +1,90 @@
# 项目生命周期状态机
## 状态定义
| 状态 | 说明 | 触发动作 | 权限 |
|:---|:---|:---|:---|
| **INIT** | 项目元数据已创建,等待详细信息录入 | 超级管理员创建项目 | SuperAdmin |
| **DRAFTING** | 正在进行初始信息填写(关联填写工单) | 指定填写人保存/编辑 | 填写人/SuperAdmin |
| **REVIEWING** | 初始信息或变更信息提交审核 | 提交审核 | SuperAdmin |
| **RELEASED** | 审核通过,正常运行中 | 审核通过 | All (View) |
| **MODIFYING** | 存在活跃的变更工单(不影响主线运行) | 发起修改工单 | Owner/SuperAdmin |
| **ARCHIVED** | 软删除状态,不可见但保留数据 | 删除项目 | SuperAdmin |
## 状态转换图
```
[*] ──创建项目──> INIT
├──分配填写人──> DRAFTING ──保存草稿──> DRAFTING
│ │
│ └──提交审核──> REVIEWING
│ │
│ ┌──审核打回──<────┤
│ │ │
│ v └──审核通过──> RELEASED
│ DRAFTING │
│ │
│ ┌──发起修改工单──<───────────────┤
│ │ │
│ v └──归档删除──> ARCHIVED ──> [*]
│ MODIFYING ──保存草稿──> MODIFYING
│ │
│ ├──提交变更审核──> REVIEWING
│ │
│ └──撤销变更──> RELEASED
```
## 状态转换条件
| From | To | 事件 | 条件 |
|:---|:---|:---|:---|
| INIT | DRAFTING | 分配填写人 | 填写人 ID 有效 |
| DRAFTING | DRAFTING | 保存草稿 | 表单数据有效 |
| DRAFTING | REVIEWING | 提交审核 | 必填字段完整 |
| REVIEWING | DRAFTING | 审核打回 | SuperAdmin 操作 |
| REVIEWING | RELEASED | 审核通过 | SuperAdmin 操作 |
| RELEASED | MODIFYING | 发起修改工单 | 有 View ACL 权限 |
| RELEASED | ARCHIVED | 归档删除 | SuperAdmin 操作 |
| MODIFYING | MODIFYING | 保存草稿 | 表单数据有效 |
| MODIFYING | REVIEWING | 提交变更审核 | 必填字段完整 |
| MODIFYING | RELEASED | 撤销变更/审核通过 | 用户操作/SuperAdmin |
## Mermaid 状态图
```mermaid
stateDiagram-v2
[*] --> INIT: 创建项目
INIT --> DRAFTING: 分配填写人
DRAFTING --> DRAFTING: 保存草稿
DRAFTING --> REVIEWING: 提交审核
REVIEWING --> DRAFTING: 审核打回
REVIEWING --> RELEASED: 审核通过
RELEASED --> MODIFYING: 发起修改工单
RELEASED --> ARCHIVED: 归档删除
MODIFYING --> MODIFYING: 保存草稿
MODIFYING --> REVIEWING: 提交变更审核
MODIFYING --> RELEASED: 撤销变更/审核通过
ARCHIVED --> [*]
note right of RELEASED: 项目认证状态=official
note right of DRAFTING: 支持多次保存草稿
note right of MODIFYING: 可同时存在多个变更工单
```
## 状态与认证状态映射
| 生命周期状态 | 认证状态 (certification_status) |
|:---|:---|
| INIT | draft |
| DRAFTING | draft |
| REVIEWING | pending |
| RELEASED | official |
| MODIFYING | official (主线不变) |
| ARCHIVED | official (保留) |

View File

@@ -0,0 +1,146 @@
# 工单状态映射表
## 设计原则
项目模块(`rmdc-project-management`)与工单模块(`rmdc-work-procedure`)之间需要双向协作:
1. **项目 → 工单**:项目模块调用工单模块创建/转换工单
2. **工单 → 项目**:工单状态变更后同步更新项目生命周期状态
当前系统采用 **"模块化单体"架构**,使用 **接口注入(依赖注入)** 方式实现模块间回调。
## 填写工单 (project_detail)
| 工单事件 | 工单From状态 | 工单To状态 | 项目生命周期状态 | 说明 |
|:---|:---|:---|:---|:---|
| `create` | - | `created` | `INIT→DRAFTING` | 创建填写工单 |
| `accept` | `assigned` | `in_progress` | `DRAFTING` (保持) | 填写人开始填写 |
| `draft_save` | `in_progress` | `in_progress` | `DRAFTING` (保持) | 保存草稿 |
| `submit/complete` | `in_progress` | `pending_review` | `REVIEWING` | 提交审核 |
| `return` | `pending_review` | `returned` | `DRAFTING` | 审核人打回 |
| `resubmit` | `returned` | `pending_review` | `REVIEWING` | 重新提交 |
| `approve` | `pending_review` | `approved` | `RELEASED` | 审核通过 |
| `revoke` | any | `revoked` | `INIT` | 撤销工单 |
## 修改工单 (project_modify)
| 工单事件 | 工单From状态 | 工单To状态 | 项目生命周期状态 | 说明 |
|:---|:---|:---|:---|:---|
| `create` | - | `created` | `RELEASED→MODIFYING` | 发起修改工单 |
| `accept` | `assigned` | `in_progress` | `MODIFYING` (保持) | 开始修改 |
| `draft_save` | `in_progress` | `in_progress` | `MODIFYING` (保持) | 保存草稿 |
| `submit/complete` | `in_progress` | `pending_review` | `REVIEWING` | 提交审核 |
| `return` | `pending_review` | `returned` | `MODIFYING` | 审核人打回 |
| `resubmit` | `returned` | `pending_review` | `REVIEWING` | 重新提交 |
| `approve` | `pending_review` | `approved` | `RELEASED` | 审核通过 |
| `revoke` | any | `revoked` | `RELEASED` | 撤销工单 |
## 回调接口定义
### 项目模块提供的回调接口
```go
// ProjectLifecycleUpdater 项目生命周期状态更新接口
// 由 rmdc-core 在初始化时注入,工单模块状态变更时调用
type ProjectLifecycleUpdater interface {
// UpdateLifecycleStatus 更新项目生命周期状态
UpdateLifecycleStatus(projectID, lifecycleStatus string) error
// SetLifecycleToDrafting 设置为填写中状态(工单被打回后)
SetLifecycleToDrafting(projectID string) error
// SetLifecycleToReviewing 设置为审核中状态(提交审核时)
SetLifecycleToReviewing(projectID string) error
// SetLifecycleToReleased 设置为已发布状态(审批通过时)
SetLifecycleToReleased(projectID string) error
// SetLifecycleToModifying 设置为变更中状态(发起修改工单时)
SetLifecycleToModifying(projectID string) error
}
```
### 项目模块调用工单模块的接口
```go
// WorkflowTransitioner 工单状态转换接口
// 由 rmdc-core 在初始化时注入,项目模块通过此接口调用工单模块
type WorkflowTransitioner interface {
// TransitionWorkflow 触发工单状态转换
TransitionWorkflow(workflowID, event string, operatorID uint64,
operatorName string, remark string) (newStatus string, err error)
}
// WorkflowCreator 工单创建接口
type WorkflowCreator interface {
// CreateProjectWorkflow 创建项目相关工单
CreateProjectWorkflow(req CreateWorkflowRequest) (workflowID string, err error)
}
```
## 依赖注入流程
`rmdc-core/cmd/main.go` 中完成模块间的依赖注入:
```go
// 1. 初始化项目和工单服务
projectSvc := projectHandler.RegisterRoutes(r, dbs.Project, authMiddleware)
workflowSvc := workflowHandler.RegisterRoutes(r, dbs.Workflow, authMiddleware)
// 2. 注入工单→项目的回调(状态同步)
projectCallbackSvc := initProjectWorkflowCallbacks(dbs.Project)
workflowSvc.SetProjectLifecycleUpdater(projectCallbackSvc)
// 3. 注入项目→工单的调用(创建/转换工单)
workflowCreator := initProjectWorkflowCreator(workflowSvc)
projectSvc.SetWorkflowCreator(workflowCreator)
workflowTransitioner := initProjectWorkflowTransitioner(workflowSvc)
projectSvc.SetWorkflowTransitioner(workflowTransitioner)
```
## 回调处理实现
工单模块状态转换后,自动调用已注入的 `ProjectLifecycleUpdater` 接口更新项目状态:
```go
// 工单模块 - 状态转换后触发项目状态更新
func (s *WorkflowService) handleProjectLifecycleCallback(workflow *entity.Workflow, event string) {
// 从业务载荷中获取项目ID
projectID, ok := workflow.BusinessPayload["project_id"].(string)
if !ok || projectID == "" {
return
}
// 根据事件类型更新项目生命周期状态
if s.projectLifecycleUpdater != nil {
switch event {
case entity.EventApprove:
s.projectLifecycleUpdater.SetLifecycleToReleased(projectID)
case entity.EventReturn:
s.projectLifecycleUpdater.SetLifecycleToDrafting(projectID)
case entity.EventComplete, entity.EventResubmit:
s.projectLifecycleUpdater.SetLifecycleToReviewing(projectID)
}
}
}
```
## 回调处理要点
1. **幂等性**: 使用 `projectID + event + timestamp` 作为幂等键,防止重复处理
2. **事务边界**: 状态更新与版本快照生成应在同一事务
3. **审计日志**: 记录状态变更操作到 `rmdc-audit-log`
4. **错误处理**: 回调失败不应阻塞工单状态转换,记录日志后继续
## 与 HTTP 回调的对比
| 特性 | 接口注入(当前实现) | HTTP 回调 |
|:---|:---|:---|
| **模块解耦** | ⚠️ 接口级解耦 | ✅ 完全解耦 |
| **分布式支持** | ❌ 不支持 | ✅ 支持 |
| **性能** | ✅ 进程内调用 | ⚠️ 网络开销 |
| **复杂度** | ✅ 简单直接 | ⚠️ 需要重试/幂等处理 |
| **事务一致性** | ✅ 强一致性 | ⚠️ 最终一致性 |
| **适用场景** | 模块化单体架构 | 微服务架构 |
> 如果未来系统需要微服务化,可参考备选文档迁移到 HTTP 回调方案。

View File

@@ -0,0 +1,231 @@
#!/bin/bash
# verify-project-module.sh
# 验证 rmdc-project-management 模块的完整性
# 依赖: go, grep, find
# 用法: ./verify-project-module.sh [project-root-path]
set -e
PROJECT_ROOT="${1:-.}"
echo "=========================================="
echo "RMDC Project Management Module Verification"
echo "=========================================="
echo "Project Root: $PROJECT_ROOT"
echo ""
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
PASS_COUNT=0
FAIL_COUNT=0
WARN_COUNT=0
check_pass() {
echo -e "${GREEN}[PASS]${NC} $1"
((PASS_COUNT++))
}
check_fail() {
echo -e "${RED}[FAIL]${NC} $1"
((FAIL_COUNT++))
}
check_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
((WARN_COUNT++))
}
# 1. 检查实体定义
echo "1. Checking Entity Definitions..."
if grep -rq "LifecycleStatus" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then
check_pass "LifecycleStatus field exists in Project entity"
else
check_fail "LifecycleStatus field missing in Project entity"
fi
if grep -rq "CurrentVersion" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then
check_pass "CurrentVersion field exists for version tracking"
else
check_fail "CurrentVersion field missing for version tracking"
fi
if grep -rq "CertificationStatus" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then
check_pass "CertificationStatus field exists"
else
check_fail "CertificationStatus field missing"
fi
# 2. 检查生命周期状态常量
echo ""
echo "2. Checking Lifecycle Status Constants..."
REQUIRED_STATES=("INIT" "DRAFTING" "REVIEWING" "RELEASED" "MODIFYING" "ARCHIVED")
for state in "${REQUIRED_STATES[@]}"; do
if grep -rq "\"$state\"" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then
check_pass "Lifecycle state '$state' defined"
else
check_fail "Lifecycle state '$state' not found"
fi
done
# 3. 检查 API 路由
echo ""
echo "3. Checking API Routes..."
REQUIRED_ROUTES=(
"/api/project/list"
"/api/project/detail"
"/api/project/create"
"/api/project/update"
"/api/project/draft/save"
"/api/project/draft/submit"
"/api/project/version/list"
"/api/project/permission/grant"
)
for route in "${REQUIRED_ROUTES[@]}"; do
if grep -rq "$route" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then
check_pass "Route '$route' registered"
else
check_fail "Route '$route' not found"
fi
done
# 4. 检查权限中间件
echo ""
echo "4. Checking Permission Middleware..."
if grep -rq "SuperAdmin\|RequireRole\|RequirePermission" "$PROJECT_ROOT"/internal/project/handler/*.go 2>/dev/null; then
check_pass "Permission middleware applied"
else
check_warn "Permission middleware not detected - verify manually"
fi
# 5. 检查版本服务
echo ""
echo "5. Checking Version Service..."
if grep -rq "CompareVersions" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then
check_pass "Version comparison service implemented"
else
check_fail "Version comparison service not found"
fi
if grep -rq "CreateOfficialVersion" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then
check_pass "CreateOfficialVersion method exists"
else
check_fail "CreateOfficialVersion method not found"
fi
# 6. 检查回调接口
echo ""
echo "6. Checking Lifecycle Callback Interface..."
if grep -rq "ProjectLifecycleUpdater" "$PROJECT_ROOT"/internal/project/*.go 2>/dev/null; then
check_pass "ProjectLifecycleUpdater interface defined"
else
check_fail "ProjectLifecycleUpdater interface not found"
fi
CALLBACK_METHODS=("SetLifecycleToDrafting" "SetLifecycleToReviewing" "SetLifecycleToReleased" "SetLifecycleToModifying")
for method in "${CALLBACK_METHODS[@]}"; do
if grep -rq "$method" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then
check_pass "Callback method '$method' implemented"
else
check_fail "Callback method '$method' not found"
fi
done
# 7. 检查敏感字段加密
echo ""
echo "7. Checking Sensitive Field Encryption..."
if grep -rq "Encrypt\|Decrypt\|AES\|crypto" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then
check_pass "Encryption functions detected"
else
check_warn "Encryption functions not detected - verify password field handling"
fi
# 8. 检查审计日志集成
echo ""
echo "8. Checking Audit Log Integration..."
if grep -rq "audit\|AuditService\|auditSvc" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then
check_pass "Audit log integration detected"
else
check_warn "Audit log integration not detected - verify manually"
fi
# 9. 检查乐观锁实现
echo ""
echo "9. Checking Optimistic Lock..."
if grep -rq "base_version\|BaseVersion\|VersionConflict" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then
check_pass "Optimistic lock (version conflict check) implemented"
else
check_warn "Optimistic lock not detected - concurrent modification may cause issues"
fi
# 10. 运行单元测试(如果可用)
echo ""
echo "10. Running Unit Tests..."
if [ -d "$PROJECT_ROOT/internal/project" ]; then
cd "$PROJECT_ROOT"
if go test ./internal/project/... -v -short -count=1 2>&1 | head -20; then
check_pass "Unit tests executed"
else
check_warn "Unit tests may have issues - check output above"
fi
else
check_warn "Project directory not found, skipping tests"
fi
# 11. 检查编译
echo ""
echo "11. Checking Build..."
if [ -d "$PROJECT_ROOT/internal/project" ]; then
cd "$PROJECT_ROOT"
if go build ./internal/project/... 2>/dev/null; then
check_pass "Module compiles successfully"
else
check_fail "Module compilation failed"
fi
else
check_warn "Project directory not found, skipping build check"
fi
# 12. 检查 JSONB 字段定义
echo ""
echo "12. Checking JSONB Field Definitions..."
JSONB_FIELDS=("basic_info" "deploy_business" "deploy_env" "deploy_middleware")
for field in "${JSONB_FIELDS[@]}"; do
if grep -rq "\"$field\"" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then
check_pass "JSONB field '$field' defined"
else
check_fail "JSONB field '$field' not found"
fi
done
# 总结
echo ""
echo "=========================================="
echo "Verification Summary"
echo "=========================================="
echo -e "Passed: ${GREEN}$PASS_COUNT${NC}"
echo -e "Failed: ${RED}$FAIL_COUNT${NC}"
echo -e "Warnings: ${YELLOW}$WARN_COUNT${NC}"
if [ $FAIL_COUNT -gt 0 ]; then
echo ""
echo -e "${RED}Some checks failed. Please review and fix the issues.${NC}"
echo ""
echo "Common fixes:"
echo " - Missing lifecycle states: Add constants in entity/constants.go"
echo " - Missing routes: Register in handler/router.go"
echo " - Missing callback interface: Implement ProjectLifecycleUpdater"
echo " - Missing version service: Implement version comparison logic"
exit 1
elif [ $WARN_COUNT -gt 0 ]; then
echo ""
echo -e "${YELLOW}Some warnings detected. Please verify manually.${NC}"
exit 0
else
echo ""
echo -e "${GREEN}All checks passed!${NC}"
exit 0
fi

View File

@@ -0,0 +1,229 @@
---
name: developing-rmdc-system
description: Guides development and architecture decisions for the RMDC (Runtime Management & DevOps Center) platform. Use when creating new modules, understanding module dependencies, implementing cross-module features, or reviewing system-level changes. Keywords: RMDC, architecture, module, dependency, API gateway, MQTT, watchdog, exchange-hub, authorization.
argument-hint: "<module-name> | <change-type: add-module|cross-module|dependency-change> | <design-doc-path>"
allowed-tools:
- Read
- Glob
- Grep
- Bash
---
# RMDC System Development Guide
## System Overview
RMDC (Runtime Management & DevOps Center) 是以项目(K8s Namespace)为核心维度的统一运维与交付平台。通过"边缘代理(Watchdog) + 消息总线(Exchange-Hub)"架构打通内外网边界。
### Architecture Layers
```
┌─────────────────────────────────────────────────────────────┐
│ Presentation │ Vue3 + Vuetify3 + TypeScript │
├─────────────────────────────────────────────────────────────┤
│ Gateway │ rmdc-core (API Gateway + Auth + Routing) │
├─────────────────────────────────────────────────────────────┤
│ Business │ jenkins-dac | project-mgmt | user-auth │
│ │ audit-log | notice-center | monitor │
├─────────────────────────────────────────────────────────────┤
│ Communication │ rmdc-exchange-hub (MQTT Gateway) │
├─────────────────────────────────────────────────────────────┤
│ Message Broker │ MQTT Broker (EMQX/Mosquitto) │
├─────────────────────────────────────────────────────────────┤
│ Edge │ rmdc-watchdog → watchdog-node/agent │
├─────────────────────────────────────────────────────────────┤
│ Data │ PostgreSQL 13+ │
└─────────────────────────────────────────────────────────────┘
```
---
## Module Registry
| Module | Responsibility | Tech Stack | Depends On |
|--------|---------------|------------|------------|
| **rmdc-core** | API Gateway, Auth, Routing | Go + Gin | rmdc-common |
| **rmdc-jenkins-branch-dac** | Jenkins DAC, Build Mgmt | Jenkins API, MinIO | rmdc-common, rmdc-audit-log |
| **rmdc-exchange-hub** | MQTT Gateway, Command Lifecycle | MQTT, PostgreSQL | rmdc-common, rmdc-project-mgmt |
| **rmdc-watchdog** | Edge Proxy, K8S Ops, L2 Auth | K8S API, TOTP | rmdc-common |
| **rmdc-project-management** | Project CRUD, L1 Auth Center | PostgreSQL | rmdc-common, rmdc-audit-log |
| **rmdc-audit-log** | Audit Logging | PostgreSQL | rmdc-common |
| **rmdc-user-auth** | User Auth, RBAC | JWT, PostgreSQL | rmdc-common |
> 详细依赖矩阵见 `reference/module-dependencies.md`
---
## Plan Phase
当开始 RMDC 相关开发任务时,首先执行以下检查:
### 1. Identify Affected Modules
```bash
# 动态注入:查看当前模块结构
!`ls -la 8-CMII-RMDC/`
# 动态注入:搜索涉及的模块设计文档
!`grep -rnE "module|模块|service|接口" 8-CMII-RMDC/1-rmdc-system/ | head -30`
```
### 2. Produce Checklist
- [ ] 确定变更涉及的模块列表
- [ ] 确认是否涉及跨模块通信MQTT/HTTP
- [ ] 确认是否涉及契约变更API/Event/Schema
- [ ] 确认是否涉及授权层级变更L1/L2
- [ ] 确认是否需要数据库迁移
### 3. Decision Points
| Decision | Options | Impact |
|----------|---------|--------|
| New module vs extend existing | 新增模块需注册到rmdc-core | 路由、鉴权、审计 |
| Sync vs Async communication | HTTP同步 / MQTT异步 | 延迟、可靠性 |
| L1 vs L2 authorization | project-mgmt(L1) / watchdog(L2) | 安全边界 |
---
## Verify Phase
### Cross-Module Compatibility Checklist
- [ ] **API Gateway**: rmdc-core 路由配置已更新
- [ ] **Authentication**: JWT claims 字段兼容
- [ ] **RBAC**: 权限点已在 rmdc-user-auth 注册
- [ ] **Audit**: 审计日志已按模块分表配置
- [ ] **MQTT Topics**: 新增 topic 已在 exchange-hub 注册
- [ ] **Authorization**: L1/L2 授权流程已验证
### Dependency Verification
```bash
# 动态注入:检查模块间 import 关系
!`grep -rn "import.*rmdc-" --include="*.go" . | grep -v vendor | head -20`
# 动态注入:验证 go.mod 依赖
!`cat go.mod | grep -E "rmdc-|wdd.io"`
```
### Integration Points
| From | To | Protocol | Verify |
|------|----|----------|--------|
| rmdc-core | Business modules | HTTP/Internal | 路由注册 |
| Business modules | exchange-hub | HTTP | 指令下发 |
| exchange-hub | MQTT Broker | MQTT Pub/Sub | Topic 配置 |
| MQTT Broker | watchdog | MQTT | 公网连通性 |
| watchdog | watchdog-node/agent | HTTP/gRPC | 内网通信 |
---
## Execute Phase
### Adding New Business Module
1. Create module directory following structure:
```
rmdc-{module-name}/
├── cmd/main.go
├── configs/
├── internal/
│ ├── config/
│ ├── dao/
│ ├── handler/
│ ├── model/{dto,entity}/
│ └── service/
└── pkg/
```
2. Register routes in `rmdc-core`:
```go
// rmdc-core/internal/router/router.go
moduleGroup := r.Group("/api/{module}")
moduleGroup.Use(middleware.AuthMiddleware())
```
3. Configure audit logging:
```go
// Add module to determineModule() in audit_service.go
case strings.Contains(path, "/{module}/"):
return "{module}"
```
4. Update RBAC permissions in `rmdc-user-auth`
### Cross-Module Communication
**HTTP (Sync)**: 模块间直接调用
```go
resp, err := http.Post("http://rmdc-exchange-hub:8080/api/commands/send", ...)
```
**MQTT (Async)**: 通过 exchange-hub 下发
```go
exhub.SendCommand(ctx, &Command{
ProjectID: projectID,
CommandType: "k8s_exec",
Payload: payload,
})
```
---
## Pitfalls
1. **循环依赖**: 业务模块间禁止直接 import必须通过 rmdc-common 定义接口
2. **JWT Claims 不一致**: 修改 JWT 结构需同步更新所有解析方验证逻辑
3. **MQTT Topic 命名冲突**: 新增 topic 前必须检查 `reference/mqtt-topics.md`
4. **L1/L2 授权边界模糊**: 平台侧操作走 L1(project-mgmt),边缘侧操作走 L2(watchdog)
5. **审计日志遗漏**: 新模块必须配置独立审计表并注册到 DAOManager
6. **数据库连接池耗尽**: 每个模块独立配置连接池,注意总数不超过 PostgreSQL max_connections
7. **MQTT QoS 选择错误**: 指令类消息必须使用 QoS=1状态类可用 QoS=0
---
## Related Skills
- `developing-rmdc-core` - API Gateway 开发
- `developing-rmdc-jenkins-dac` - Jenkins DAC 模块开发
- `developing-rmdc-exchange-hub` - MQTT 网关开发
- `developing-rmdc-watchdog` - 边缘代理开发
- `developing-rmdc-project-mgmt` - 项目管理模块开发
- `developing-rmdc-audit-log` - 审计日志模块开发
- `developing-rmdc-user-auth` - 用户认证模块开发
- `designing-rmdc-contracts` - API/事件契约设计
- `managing-rmdc-migrations` - 数据库迁移管理
- `implementing-rmdc-observability` - 可观测性实现
---
## Quick Reference
### Tech Stack
| Layer | Technology |
|-------|------------|
| Frontend | Vue3, TypeScript, Vuetify3 |
| Backend | Go 1.21+, Gin, GORM |
| Database | PostgreSQL 13+ |
| Message | MQTT (EMQX/Mosquitto) |
| Storage | MinIO |
| Container | Docker, Kubernetes |
### API Response Format
```json
{
"code": 0,
"message": "success",
"data": {...}
}
```
### Authorization Layers
| Layer | Scope | Validity | Algorithm |
|-------|-------|----------|-----------|
| L1 (一级) | project-mgmt ↔ watchdog | 30 min | SHA256, 8-digit |
| L2 (二级) | watchdog ↔ agent/node | 30 sec | SHA1, 6-digit (TOTP) |

View File

@@ -0,0 +1,177 @@
// RMDC Module Standard Structure Example
// This file demonstrates the recommended project structure for RMDC business modules
package main
/*
Directory Structure:
rmdc-{module-name}/
├── cmd/
│ └── main.go # Entry point
├── configs/
│ └── config.yaml # Configuration file
├── internal/
│ ├── config/
│ │ └── config.go # Config struct and loader
│ ├── dao/
│ │ └── {entity}_dao.go # Data access layer
│ ├── handler/
│ │ └── {feature}_handler.go # HTTP handlers
│ ├── model/
│ │ ├── dto/
│ │ │ ├── request.go # Request DTOs
│ │ │ └── response.go # Response DTOs
│ │ └── entity/
│ │ └── {table}.go # Database entities
│ └── service/
│ └── {feature}_service.go # Business logic
├── pkg/
│ └── {shared}/ # Shared packages
├── go.mod
└── go.sum
*/
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ============== Entity Layer ==============
// Project represents the database entity
type Project struct {
ID int64 `gorm:"primaryKey" json:"id"`
ProjectID string `gorm:"uniqueIndex;size:64" json:"project_id"`
Name string `gorm:"size:128" json:"name"`
Namespace string `gorm:"uniqueIndex;size:64" json:"namespace"`
Status string `gorm:"size:32" json:"status"`
}
func (Project) TableName() string {
return "projects"
}
// ============== DTO Layer ==============
// CreateProjectRequest is the request DTO
type CreateProjectRequest struct {
Name string `json:"name" binding:"required"`
Namespace string `json:"namespace" binding:"required"`
}
// ProjectResponse is the response DTO
type ProjectResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// ============== DAO Layer ==============
// ProjectDAO handles database operations
type ProjectDAO struct {
db *gorm.DB
}
func NewProjectDAO(db *gorm.DB) *ProjectDAO {
return &ProjectDAO{db: db}
}
func (d *ProjectDAO) Create(ctx context.Context, project *Project) error {
return d.db.WithContext(ctx).Create(project).Error
}
func (d *ProjectDAO) GetByID(ctx context.Context, id int64) (*Project, error) {
var project Project
err := d.db.WithContext(ctx).First(&project, id).Error
return &project, err
}
// ============== Service Layer ==============
// ProjectService contains business logic
type ProjectService struct {
dao *ProjectDAO
}
func NewProjectService(dao *ProjectDAO) *ProjectService {
return &ProjectService{dao: dao}
}
func (s *ProjectService) CreateProject(ctx context.Context, req *CreateProjectRequest) (*Project, error) {
project := &Project{
Name: req.Name,
Namespace: req.Namespace,
Status: "PENDING",
}
// Business logic: generate project_id
project.ProjectID = "proj_" + req.Namespace
if err := s.dao.Create(ctx, project); err != nil {
return nil, err
}
return project, nil
}
// ============== Handler Layer ==============
// ProjectHandler handles HTTP requests
type ProjectHandler struct {
service *ProjectService
}
func NewProjectHandler(service *ProjectService) *ProjectHandler {
return &ProjectHandler{service: service}
}
// CreateProject godoc
// @Summary Create a new project
// @Tags projects
// @Accept json
// @Produce json
// @Param request body CreateProjectRequest true "Create project request"
// @Success 200 {object} ProjectResponse
// @Router /api/projects/create [post]
func (h *ProjectHandler) CreateProject(c *gin.Context) {
var req CreateProjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ProjectResponse{
Code: 10004,
Message: "Invalid parameters: " + err.Error(),
})
return
}
project, err := h.service.CreateProject(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, ProjectResponse{
Code: 10005,
Message: "Failed to create project: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, ProjectResponse{
Code: 0,
Message: "success",
Data: project,
})
}
// ============== Router Registration ==============
// RegisterRoutes registers module routes to the router group
func RegisterRoutes(r *gin.RouterGroup, handler *ProjectHandler) {
projects := r.Group("/projects")
{
projects.POST("/create", handler.CreateProject)
// projects.POST("/list", handler.ListProjects)
// projects.POST("/detail", handler.GetProject)
// projects.POST("/update", handler.UpdateProject)
// projects.POST("/delete", handler.DeleteProject)
}
}

View File

@@ -0,0 +1,131 @@
# RMDC API Design Rules
## Core Principles
1. **POST + RequestBody 优先**: 所有 API 优先使用 POST 方法
2. **避免 PathVariables**: 资源标识放入 RequestBody
3. **避免 RequestParams**: 查询参数放入 RequestBody
4. **统一响应格式**: 所有响应遵循标准结构
## URL Naming Convention
| Operation | Suffix | Example |
|-----------|--------|---------|
| List | `/list` | `POST /api/projects/list` |
| Detail | `/detail` | `POST /api/projects/detail` |
| Create | `/create` | `POST /api/projects/create` |
| Update | `/update` | `POST /api/projects/update` |
| Delete | `/delete` | `POST /api/projects/delete` |
## Request Format
```json
{
"project_id": "namespace_xxx",
"page": 1,
"page_size": 20,
"filters": {
"status": "ONLINE",
"name": "keyword"
}
}
```
## Response Format
### Success Response
```json
{
"code": 0,
"message": "success",
"data": {
// response payload
}
}
```
### Paginated Response
```json
{
"code": 0,
"message": "success",
"data": {
"list": [...],
"total": 100,
"page": 1,
"page_size": 20
}
}
```
### Error Response
```json
{
"code": 40001,
"message": "Invalid parameter: project_id is required",
"data": null
}
```
## Error Code Ranges
| Range | Module | Description |
|-------|--------|-------------|
| 0 | - | Success |
| 10000-19999 | rmdc-core | Gateway errors |
| 20000-29999 | rmdc-jenkins-dac | Jenkins errors |
| 30000-39999 | rmdc-project-mgmt | Project errors |
| 40000-49999 | rmdc-exchange-hub | Exchange errors |
| 50000-59999 | rmdc-watchdog | Watchdog errors |
| 60000-69999 | rmdc-user-auth | Auth errors |
| 70000-79999 | rmdc-audit-log | Audit errors |
## Common Error Codes
| Code | HTTP Status | Description |
|------|-------------|-------------|
| 10001 | 401 | Unauthorized |
| 10002 | 403 | Forbidden |
| 10003 | 404 | Resource not found |
| 10004 | 400 | Invalid parameters |
| 10005 | 500 | Internal server error |
| 10006 | 429 | Rate limit exceeded |
## HTTP Headers
### Request Headers
| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | Yes | `Bearer {jwt_token}` |
| `Content-Type` | Yes | `application/json` |
| `X-Request-ID` | No | 请求追踪 ID |
### Response Headers
| Header | Description |
|--------|-------------|
| `X-Request-ID` | 请求追踪 ID回显或生成 |
| `X-Response-Time` | 响应耗时(毫秒) |
## JWT Claims
```json
{
"user_id": 1,
"username": "admin",
"role": "SuperAdmin",
"permissions": ["project:read", "project:write"],
"exp": 1704501234,
"iat": 1704414834
}
```
## Versioning Strategy
- 当前版本: 无版本前缀 (`/api/...`)
- 破坏性变更: 使用版本前缀 (`/api/v2/...`)
- 废弃策略: 旧版本保留 6 个月,响应头标记 `Deprecation: true`

View File

@@ -0,0 +1,105 @@
# RMDC Authorization Layers
## Overview
RMDC 采用双层授权架构,确保跨网络操作的安全性。
```
┌────────────────────────────────────────────────────────────┐
│ RMDC Platform (内网) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ project-mgmt │────▶│ exchange-hub │ │
│ │ (L1 Auth Center) │ │ (MQTT Gateway) │ │
│ └──────────────────┘ └────────┬─────────┘ │
└────────────────────────────────────┼───────────────────────┘
│ MQTT (公网)
┌────────────────────────────────────┼───────────────────────┐
│ Project Env (边缘/外网) │
│ ┌───────▼─────────┐ │
│ │ rmdc-watchdog │ │
│ │ (L2 Auth Center)│ │
│ └───────┬─────────┘ │
│ ┌───────────────┼───────────────┐ │
│ ┌─────▼─────┐ ┌─────▼─────┐ │
│ │ watchdog │ │ watchdog │ │
│ │ -node │ │ -agent │ │
│ └───────────┘ └───────────┘ │
└────────────────────────────────────────────────────────────┘
```
## L1 Authorization (一级授权)
| Attribute | Value |
|-----------|-------|
| Scope | project-management ↔ watchdog |
| Algorithm | SHA256 |
| Code Length | 8 digits |
| Validity | 30 minutes |
| Transport | MQTT message payload |
### L1 Flow
1. `project-management` 创建项目时生成 `auth_secret`
2. 部署 watchdog 时将 `auth_secret` 写入配置文件
3. watchdog 启动后生成 TOTP 验证码发送注册请求
4. `exchange-hub` 转发给 `project-management` 验证
5. 验证通过后项目状态变为 `ONLINE`
### L1 Code Generation
```go
func GenerateL1Code(secret string, timestamp int64) string {
// 30-minute window
counter := timestamp / (30 * 60)
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(fmt.Sprintf("%d", counter)))
hash := h.Sum(nil)
offset := hash[len(hash)-1] & 0x0F
code := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7FFFFFFF
return fmt.Sprintf("%08d", code%100000000)
}
```
## L2 Authorization (二级授权)
| Attribute | Value |
|-----------|-------|
| Scope | watchdog ↔ watchdog-agent/node |
| Algorithm | SHA1 (RFC 6238 TOTP) |
| Code Length | 6 digits |
| Validity | 30 seconds |
| Transport | HTTP Header / gRPC Metadata |
### L2 Flow
1. watchdog 作为二级授权中心持有本项目 `agent_secret`
2. agent/node 启动时从配置获取 `agent_secret`
3. 每次请求携带当前 TOTP 验证码
4. watchdog 验证后执行操作
### L2 Code Generation
```go
// Standard TOTP (RFC 6238)
func GenerateL2Code(secret string) string {
key, _ := base32.StdEncoding.DecodeString(secret)
return totp.GenerateCode(key, time.Now())
}
```
## Security Considerations
1. **Secret Storage**: 所有 secret 必须加密存储AES-256-GCM
2. **Transport Security**: MQTT 必须启用 TLS
3. **Clock Sync**: 所有节点时间偏差 < 30
4. **Replay Protection**: 每个 message_id 只能使用一次
5. **Rate Limiting**: 验证失败超过 5 次锁定 15 分钟
## Error Codes
| Code | Description | Action |
|------|-------------|--------|
| AUTH_L1_INVALID | L1 验证码无效 | 检查 secret 配置 |
| AUTH_L1_EXPIRED | L1 验证码过期 | 重新生成验证码 |
| AUTH_L2_INVALID | L2 TOTP 无效 | 检查时间同步 |
| AUTH_L2_LOCKED | L2 验证已锁定 | 等待 15 分钟 |

View File

@@ -0,0 +1,61 @@
# RMDC Module Dependencies
## Dependency Matrix
```
rmdc-common rmdc-core audit-log user-auth project-mgmt exchange-hub watchdog jenkins-dac
rmdc-core ✓ - ✓ ✓ ✓ ✓ - ✓
rmdc-jenkins-dac ✓ - ✓ - - - - -
rmdc-exchange-hub ✓ - ✓ - ✓ - - -
rmdc-watchdog ✓ - - - - - - -
rmdc-project-mgmt ✓ - ✓ - - ✓ - -
rmdc-audit-log ✓ - - - - - - -
rmdc-user-auth ✓ - ✓ - - - - -
```
## Dependency Rules
### Allowed Dependencies
| Module | Can Import |
|--------|------------|
| rmdc-core | All business modules (as router aggregator) |
| Business modules | rmdc-common, rmdc-audit-log (for logging) |
| rmdc-exchange-hub | rmdc-project-mgmt (for project validation) |
### Prohibited Dependencies
- **Business → Business**: 业务模块间禁止直接依赖
- **Circular**: 任何形成环的依赖关系
- **Edge → Platform**: watchdog 不可依赖平台侧模块(仅通过 MQTT 通信)
## External Service Dependencies
| Module | External Service | Protocol | Purpose |
|--------|-----------------|----------|---------|
| rmdc-jenkins-dac | Jenkins | REST API | Build management |
| rmdc-jenkins-dac | MinIO | S3 API | Artifact storage |
| rmdc-exchange-hub | MQTT Broker | MQTT | Message relay |
| rmdc-watchdog | K8S API Server | HTTPS | Container orchestration |
| All modules | PostgreSQL | TCP/5432 | Data persistence |
## Data Flow Direction
```
Platform Side Edge Side
┌─────────────┐ ┌─────────────┐
│ rmdc-core │ │ watchdog │
│ ↓ │ │ ↓ │
│ Business │ ──MQTT via ExHub──→ │ node/agent │
│ Modules │ ←─MQTT via ExHub─── │ │
└─────────────┘ └─────────────┘
```
## Version Compatibility
| Component | Min Version | Recommended |
|-----------|-------------|-------------|
| Go | 1.21 | 1.24+ |
| PostgreSQL | 13 | 15+ |
| MQTT Broker | 3.1.1 | 5.0 |
| Kubernetes | 1.24 | 1.28+ |

View File

@@ -0,0 +1,75 @@
# RMDC MQTT Topic Registry
## Topic Naming Convention
```
wdd/RDMC/{type}/{direction}[/{project_id}]
```
| Segment | Values | Description |
|---------|--------|-------------|
| type | `command`, `message` | 指令类 / 数据类 |
| direction | `up`, `down` | 上行(边缘→平台) / 下行(平台→边缘) |
| project_id | namespace_xxx | 项目标识(下行必需) |
## Registered Topics
### Uplink (Edge → Platform)
| Topic | Publisher | Subscriber | Purpose |
|-------|-----------|------------|---------|
| `wdd/RDMC/command/up` | watchdog | exchange-hub | 边缘发送指令响应 |
| `wdd/RDMC/message/up` | watchdog | exchange-hub | 边缘发送数据/状态 |
### Downlink (Platform → Edge)
| Topic Pattern | Publisher | Subscriber | Purpose |
|---------------|-----------|------------|---------|
| `wdd/RDMC/command/down/{project_id}` | exchange-hub | watchdog | 下发操作指令 |
| `wdd/RDMC/message/down/{project_id}` | exchange-hub | watchdog | 下发配置/数据 |
## Message Payload Schema
```json
{
"message_id": "uuid-v4",
"type": "command|message",
"project_id": "namespace_xxx",
"command_type": "k8s_exec|host_exec|register|heartbeat|...",
"timestamp": 1704501234567,
"version": "1.0",
"signature": "hmac-sha256-hex",
"payload": {
// command-specific data
}
}
```
## Command Types
| command_type | Direction | Description |
|--------------|-----------|-------------|
| `register` | up | Watchdog 注册请求 |
| `heartbeat` | up/down | 心跳保活 |
| `k8s_exec` | down | K8S 操作指令 |
| `host_exec` | down | 主机操作指令 |
| `result` | up | 指令执行结果 |
| `log_stream` | up | 日志流数据 |
| `metrics` | up | 监控指标数据 |
## QoS Requirements
| Message Type | QoS Level | Reason |
|--------------|-----------|--------|
| Command (指令) | 1 | 至少一次,确保送达 |
| Result (结果) | 1 | 至少一次,确保响应 |
| Heartbeat | 0 | 最多一次,允许丢失 |
| Metrics | 0 | 最多一次,允许丢失 |
| Log stream | 0 | 最多一次,高频数据 |
## Adding New Topic
1. Update this file with new topic definition
2. Register in `exchange-hub/internal/mqtt/subscriber.go`
3. Add handler in `exchange-hub/internal/handler/mqtt_handler.go`
4. Update watchdog subscription if needed

View File

@@ -0,0 +1,172 @@
#!/bin/bash
# verify-module-deps.sh
# Verifies RMDC module dependency rules
#
# Dependencies:
# - bash 4.0+
# - grep
# - find
#
# Usage:
# ./verify-module-deps.sh [module-path]
# ./verify-module-deps.sh /path/to/rmdc-project-management
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
MODULE_PATH="${1:-.}"
ERRORS=0
WARNINGS=0
echo "=========================================="
echo "RMDC Module Dependency Verification"
echo "=========================================="
echo "Checking: $MODULE_PATH"
echo ""
# Define prohibited dependencies
declare -A PROHIBITED_DEPS
PROHIBITED_DEPS["rmdc-jenkins-branch-dac"]="rmdc-project-management rmdc-exchange-hub rmdc-watchdog rmdc-user-auth"
PROHIBITED_DEPS["rmdc-project-management"]="rmdc-jenkins-branch-dac rmdc-exchange-hub rmdc-watchdog rmdc-user-auth"
PROHIBITED_DEPS["rmdc-exchange-hub"]="rmdc-jenkins-branch-dac rmdc-watchdog rmdc-user-auth"
PROHIBITED_DEPS["rmdc-watchdog"]="rmdc-jenkins-branch-dac rmdc-project-management rmdc-exchange-hub rmdc-user-auth rmdc-audit-log"
PROHIBITED_DEPS["rmdc-user-auth"]="rmdc-jenkins-branch-dac rmdc-project-management rmdc-exchange-hub rmdc-watchdog"
PROHIBITED_DEPS["rmdc-audit-log"]="rmdc-jenkins-branch-dac rmdc-project-management rmdc-exchange-hub rmdc-watchdog rmdc-user-auth"
# Detect current module
detect_module() {
if [ -f "$MODULE_PATH/go.mod" ]; then
grep "^module" "$MODULE_PATH/go.mod" | awk '{print $2}' | xargs basename
else
basename "$MODULE_PATH"
fi
}
CURRENT_MODULE=$(detect_module)
echo "Detected module: $CURRENT_MODULE"
echo ""
# Check 1: Verify go.mod exists
echo "[Check 1] go.mod existence..."
if [ -f "$MODULE_PATH/go.mod" ]; then
echo -e "${GREEN}PASS${NC}: go.mod found"
else
echo -e "${RED}FAIL${NC}: go.mod not found"
((ERRORS++))
fi
echo ""
# Check 2: Verify prohibited imports in go.mod
echo "[Check 2] Prohibited dependencies in go.mod..."
if [ -f "$MODULE_PATH/go.mod" ]; then
PROHIBITED="${PROHIBITED_DEPS[$CURRENT_MODULE]:-}"
if [ -n "$PROHIBITED" ]; then
for dep in $PROHIBITED; do
if grep -q "$dep" "$MODULE_PATH/go.mod" 2>/dev/null; then
echo -e "${RED}FAIL${NC}: Prohibited dependency found: $dep"
((ERRORS++))
fi
done
if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}PASS${NC}: No prohibited dependencies in go.mod"
fi
else
echo -e "${YELLOW}SKIP${NC}: No prohibition rules for $CURRENT_MODULE"
fi
else
echo -e "${YELLOW}SKIP${NC}: go.mod not found"
fi
echo ""
# Check 3: Verify import statements in Go files
echo "[Check 3] Prohibited imports in source code..."
if [ -d "$MODULE_PATH" ]; then
PROHIBITED="${PROHIBITED_DEPS[$CURRENT_MODULE]:-}"
if [ -n "$PROHIBITED" ]; then
for dep in $PROHIBITED; do
FOUND=$(find "$MODULE_PATH" -name "*.go" -exec grep -l "\".*$dep" {} \; 2>/dev/null | head -5)
if [ -n "$FOUND" ]; then
echo -e "${RED}FAIL${NC}: Found import of $dep in:"
echo "$FOUND" | while read -r f; do echo " - $f"; done
((ERRORS++))
fi
done
if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}PASS${NC}: No prohibited imports in source files"
fi
else
echo -e "${YELLOW}SKIP${NC}: No prohibition rules for $CURRENT_MODULE"
fi
fi
echo ""
# Check 4: Verify standard directory structure
echo "[Check 4] Standard directory structure..."
REQUIRED_DIRS=("cmd" "internal" "configs")
OPTIONAL_DIRS=("pkg" "scripts" "docs")
for dir in "${REQUIRED_DIRS[@]}"; do
if [ -d "$MODULE_PATH/$dir" ]; then
echo -e "${GREEN}PASS${NC}: Required directory exists: $dir/"
else
echo -e "${RED}FAIL${NC}: Missing required directory: $dir/"
((ERRORS++))
fi
done
for dir in "${OPTIONAL_DIRS[@]}"; do
if [ -d "$MODULE_PATH/$dir" ]; then
echo -e "${GREEN}INFO${NC}: Optional directory exists: $dir/"
fi
done
echo ""
# Check 5: Verify internal structure
echo "[Check 5] Internal package structure..."
INTERNAL_DIRS=("config" "dao" "handler" "model" "service")
if [ -d "$MODULE_PATH/internal" ]; then
for dir in "${INTERNAL_DIRS[@]}"; do
if [ -d "$MODULE_PATH/internal/$dir" ]; then
echo -e "${GREEN}PASS${NC}: internal/$dir/ exists"
else
echo -e "${YELLOW}WARN${NC}: internal/$dir/ not found"
((WARNINGS++))
fi
done
else
echo -e "${YELLOW}SKIP${NC}: internal/ directory not found"
fi
echo ""
# Check 6: Verify rmdc-common dependency
echo "[Check 6] rmdc-common dependency..."
if [ -f "$MODULE_PATH/go.mod" ]; then
if grep -q "rmdc-common" "$MODULE_PATH/go.mod"; then
echo -e "${GREEN}PASS${NC}: rmdc-common dependency found"
else
echo -e "${YELLOW}WARN${NC}: rmdc-common dependency not found (may be acceptable for some modules)"
((WARNINGS++))
fi
fi
echo ""
# Summary
echo "=========================================="
echo "Verification Summary"
echo "=========================================="
echo -e "Errors: ${RED}$ERRORS${NC}"
echo -e "Warnings: ${YELLOW}$WARNINGS${NC}"
echo ""
if [ $ERRORS -gt 0 ]; then
echo -e "${RED}VERIFICATION FAILED${NC}"
exit 1
else
echo -e "${GREEN}VERIFICATION PASSED${NC}"
exit 0
fi

View File

@@ -0,0 +1,101 @@
---
name: developing-watchdog
description: Guides development of rmdc-watchdog edge agent module including K8S operations, MQTT messaging, authorization management, and node/agent coordination. Use when implementing watchdog features, adding K8S actions, modifying heartbeat logic, or debugging authorization flows. Keywords: watchdog, edge-agent, k8s-operator, mqtt, authorization, heartbeat, node, agent.
argument-hint: "<feature-type>: k8s-action | heartbeat | mqtt-handler | node-comm | auth-flow"
allowed-tools:
- Read
- Glob
- Grep
- Bash
- Edit
- Write
---
# Developing rmdc-watchdog
rmdc-watchdog 是部署在项目环境的边缘代理职责包括二级授权中心、K8S操作代理、指令接收执行、监控数据上报。
## 动态上下文注入
```bash
# 查看项目结构
!`ls -la rmdc-watchdog/internal/`
# 查找现有Handler实现
!`grep -rn "func.*Handler" rmdc-watchdog/internal/handler/`
# 查找MQTT消息路由
!`grep -n "case\|switch" rmdc-watchdog/internal/service/message_router.go`
```
## Plan
根据 `$ARGUMENTS` 确定开发类型:
| 类型 | 产物 | 影响模块 |
|------|------|----------|
| k8s-action | `pkg/k8s/client.go`, `service/k8s_service.go` | exchange-hub指令定义 |
| heartbeat | `handler/heartbeat_handler.go`, `service/auth_service.go` | watchdog-agent同步修改 |
| mqtt-handler | `service/mqtt_service.go`, `service/message_router.go` | exchange-hub Topic契约 |
| node-comm | `service/node_service.go` | watchdog-node API同步 |
| auth-flow | `service/auth_service.go`, `dao/auth_dao.go` | project-management授权契约 |
**决策点**
1. 是否新增MQTT消息类型→ 需同步 exchange-hub
2. 是否修改心跳结构?→ 需同步 watchdog-agent
3. 是否修改K8S指令参数→ 需同步 octopus-operator
## Verify
- [ ] TOTP验证逻辑一级(8位/30分钟/SHA256) vs 二级(6位/30秒/SHA1)
- [ ] K8S操作边界仅允许审计过的操作(logs/exec/scale/restart/delete/get/apply)
- [ ] MQTT Topic格式`wdd/RDMC/{command|message}/{up|down}/{project_id}`
- [ ] 时间戳校验:|now - timestamp| < 5分钟
- [ ] Node通信HTTP + Tier-Two TOTP认证
- [ ] 执行结果上报包含 command_id, status, exit_code, output, duration
```bash
# 验证编译
!`cd rmdc-watchdog && go build ./...`
# 验证单元测试
!`cd rmdc-watchdog && go test ./internal/... -v`
```
## Execute
### 添加新K8S操作
1. `pkg/k8s/client.go` 添加K8S API方法
2. `internal/service/k8s_service.go` switch 添加 case
3. 更新 `K8sExecCommand` 结构如需新参数
4. 同步更新 exchange-hub 指令下发定义
### 添加新指令类型
1. `message_router.go` 添加路由分支
2. 创建对应 Handler Service
3. 同步更新 exchange-hub 指令下发
### 修改心跳逻辑
1. 修改 `auth_service.go` `VerifyHeartbeat`
2. 同步修改 watchdog-agent 心跳发送
3. 更新 DTO 结构
## Pitfalls
1. **TOTP层级混淆**一级授权(project-managementwatchdog)与二级授权(watchdogagent/node)使用不同参数
2. **时间偏移未处理**授权文件需计算 `timeOffset = now - firstAuthTime`
3. **Node离线未检测**转发主机指令前需 `CheckHostOnline(host_id)`
4. **日志截断遗漏**业务故障日志仅回传最近300行
5. **密钥公网传输**tier_one_secret/tier_two_secret 必须通过配置文件离线部署禁止MQTT传输
6. **响应TOTP缺失**双向验证要求服务端返回TOTP供客户端校验
7. **心跳间隔不一致**watchdogexchange-hub 5秒agent/nodewatchdog 10秒默认
## Reference
- [状态机](reference/state-machine.md)
- [MQTT Topics](reference/mqtt-topics.md)
- [API端点](reference/api-endpoints.md)
- [安全机制](reference/security-mechanisms.md)

View File

@@ -0,0 +1,56 @@
# Watchdog API 端点
## Watchdog HTTP API (Port: 8990)
| 路径 | 方法 | 说明 | 认证 |
|------|------|------|------|
| `/api/heartbeat` | POST | Agent心跳接口 | Tier-Two TOTP |
| `/api/heartbeat/hosts` | GET | 获取所有心跳主机 | 内部调用 |
| `/api/node/info` | POST | Node信息上报接口 | Tier-Two TOTP |
| `/api/node/list` | GET | 获取所有Node列表 | 内部调用 |
| `/api/node/metrics/:node_id` | GET | 获取指定Node运行指标 | 内部调用 |
| `/api/authorization/generate` | GET | 生成授权文件 | 内部调用 |
| `/api/authorization/auth` | POST | 接收授权码 | Tier-One TOTP |
| `/api/authorization/hosts` | GET | 获取所有已授权主机 | 内部调用 |
## Node HTTP API (Port: 8081)
| 路径 | 方法 | 说明 | 认证 |
|------|------|------|------|
| `/api/exec` | POST | 执行命令 | Tier-Two TOTP |
| `/api/info` | GET | 获取主机信息 | Tier-Two TOTP |
| `/api/metrics` | GET | 获取运行指标 | Tier-Two TOTP |
| `/api/dltu` | POST | 镜像操作(Download-Load-Tag-Upload) | Tier-Two TOTP |
## 请求/响应结构
### HeartbeatRequest
```go
type HeartbeatRequest struct {
HostInfo HostInfo `json:"host_info"`
EnvInfo EnvInfo `json:"env_info"`
Timestamp int64 `json:"timestamp"`
TOTPCode string `json:"totp_code"`
}
```
### HeartbeatResponse
```go
type HeartbeatResponse struct {
Authorized bool `json:"authorized"`
TOTPCode string `json:"totp_code"`
Timestamp int64 `json:"timestamp"`
SecondTOTPSecret string `json:"second_totp_secret,omitempty"`
}
```
### NodeInfoRequest
```go
type NodeInfoRequest struct {
NodeID string `json:"node_id"`
HostInfo HostInfo `json:"host_info"`
Metrics NodeRuntimeMetrics `json:"metrics"`
Timestamp int64 `json:"timestamp"`
TOTPCode string `json:"totp_code"`
}
```

View File

@@ -0,0 +1,50 @@
# MQTT Topic 定义
## 上行Watchdog → Exchange-Hub
| Topic | 消息类型 | 说明 |
|-------|----------|------|
| `wdd/RDMC/command/up` | register | 项目注册 |
| `wdd/RDMC/command/up` | auth_request | 授权申请 |
| `wdd/RDMC/message/up` | register_complete | 注册完成确认 |
| `wdd/RDMC/message/up` | heartbeat | 心跳数据 |
| `wdd/RDMC/message/up` | monitor | 监控数据上报 |
| `wdd/RDMC/message/up` | exec_result | 指令执行结果 |
| `wdd/RDMC/message/up` | log_result | 日志查询结果 |
| `wdd/RDMC/message/up` | alert | 告警信息 |
## 下行Exchange-Hub → Watchdog
| Topic | 消息类型 | 说明 |
|-------|----------|------|
| `wdd/RDMC/command/down/{project_id}` | auth_response | 授权响应 |
| `wdd/RDMC/command/down/{project_id}` | auth_revoke | 授权撤销 |
| `wdd/RDMC/command/down/{project_id}` | log_query | 日志查询指令 |
| `wdd/RDMC/command/down/{project_id}` | host_exec | 主机执行指令 |
| `wdd/RDMC/command/down/{project_id}` | k8s_exec | K8S执行指令 |
| `wdd/RDMC/command/down/{project_id}` | update | 业务更新指令 |
| `wdd/RDMC/message/down/{project_id}` | register_ack | 注册确认消息 |
## Topic命名规范
- 前缀:`wdd/RDMC/`
- 类型:`command`(指令)或 `message`(消息)
- 方向:`up`(上行)或 `down`(下行)
- 项目ID下行Topic需包含 `{project_id}` 用于路由
## 消息结构
```go
type BaseMessage struct {
MessageID string `json:"message_id"`
Type string `json:"type"` // command | message
ProjectID string `json:"project_id"`
Timestamp int64 `json:"timestamp"`
}
type DataMessage struct {
BaseMessage
DataType string `json:"data_type"` // 具体消息类型
Payload interface{} `json:"payload"`
}
```

View File

@@ -0,0 +1,43 @@
# 安全机制汇总
## 通信安全
| 场景 | 安全机制 | 参数 |
|------|----------|------|
| Center ↔ Watchdog | Tier-One TOTP + AES-GCM | 8位码, 30分钟有效期, SHA256 |
| Watchdog ↔ Agent | Tier-Two TOTP | 6位码, 30秒有效期, SHA1 |
| Watchdog ↔ Node | Tier-Two TOTP复用 | 内网HTTP + TOTP认证 |
| HTTP备用接口 | 复用Tier-Two TOTP密钥 | 需要TOTP认证 |
| 消息传输 | TLS加密 | MQTT over TLS |
| 敏感数据 | AES-256-GCM加密 | 授权码、密钥等 |
## 身份认证
| 机制 | 说明 |
|------|------|
| 主机信息 | 硬件指纹绑定: MachineID+CPU+Memory+Serial |
| 双向TOTP验证 | 请求方发送TOTP响应方返回新TOTP |
| 挑战应答 | 32位随机挑战码确保通信双方身份 |
## 授权保护
| 机制 | 说明 |
|------|------|
| 死手系统 | 心跳失败自毁连续12次失败触发SIGTERM |
| 授权时间校验 | 检测时间篡改timeOffset异常触发降级 |
| 授权撤销 | 支持远程撤销项目授权 |
## 密钥传输原则
- tier_one_secret 和 tier_two_secret 在 project-management 创建项目时生成
- 密钥通过项目配置文件离线部署到 Watchdog
- **禁止通过公网MQTT传输密钥**
## 操作审计
| 操作类型 | 审计要求 |
|----------|----------|
| K8S操作 | 记录command_id, action, 执行结果 |
| 主机命令 | 记录script, args, exit_code |
| 授权变更 | 记录授权/撤销时间、操作人 |
| 数据导出 | 需签名+TOTP校验写审计日志 |

View File

@@ -0,0 +1,45 @@
# Watchdog 状态机
## 连接状态机
```
状态流转offline -> connecting -> verifying -> online -> disconnecting -> offline
```
| 状态 | 触发条件 | 下一状态 |
|------|----------|----------|
| offline | 初始/心跳超时30秒 | connecting |
| connecting | 尝试MQTT连接 | verifying |
| verifying | TOTP双向验证 | online/offline |
| online | 验证成功 | disconnecting |
| disconnecting | 主动断开/网络异常 | offline |
## 授权状态机
```
未初始化 -> 收集主机信息 -> 等待授权 -> 已授权
授权过期/撤销 -> 未授权 -> 等待授权(重新申请)
```
## 状态转换详情
### 未初始化 → 收集主机信息
- 触发Node/Agent首次连接
- 动作AddHostInfo()
### 收集主机信息 → 等待授权
- 触发GenerateAuthorizationFile()
- 动作发布授权申请Command到MQTT
### 等待授权 → 已授权
- 触发:收到有效授权码
- 动作:解密并持久化授权信息
### 已授权 → 授权过期
- 触发:时间篡改检测
- 动作设置initialized=false
### 已授权 → 未授权
- 触发收到auth_revoke指令
- 动作:清除本地授权存储

View File

@@ -0,0 +1,89 @@
#!/bin/bash
# verify-watchdog.sh - Watchdog模块验证脚本
# 依赖: go 1.21+, golangci-lint (可选)
# 用法: ./verify-watchdog.sh [watchdog_dir]
set -e
WATCHDOG_DIR="${1:-./rmdc-watchdog}"
echo "=== Watchdog 模块验证 ==="
echo "目标目录: $WATCHDOG_DIR"
echo ""
# 检查目录存在
if [ ! -d "$WATCHDOG_DIR" ]; then
echo "错误: 目录不存在 $WATCHDOG_DIR"
exit 1
fi
# 1. 编译检查
echo "[1/5] 编译检查..."
cd "$WATCHDOG_DIR"
if go build ./... 2>&1; then
echo "✓ 编译通过"
else
echo "✗ 编译失败"
exit 1
fi
# 2. 单元测试
echo ""
echo "[2/5] 单元测试..."
if go test ./internal/... -v -cover 2>&1; then
echo "✓ 单元测试通过"
else
echo "⚠ 部分测试失败,请检查"
fi
# 3. Lint检查
echo ""
echo "[3/5] Lint检查..."
if command -v golangci-lint &> /dev/null; then
if golangci-lint run ./... 2>&1; then
echo "✓ Lint检查通过"
else
echo "⚠ Lint检查有警告"
fi
else
echo "⚠ golangci-lint未安装跳过"
fi
# 4. TOTP参数验证
echo ""
echo "[4/5] TOTP参数验证..."
# Tier-One: 8位/30分钟
if grep -rq "Digits.*8" pkg/totp/ 2>/dev/null || grep -rq "8.*Digits" pkg/totp/ 2>/dev/null; then
echo "✓ Tier-One TOTP位数配置存在"
else
echo "⚠ 未找到Tier-One TOTP位数定义 (应为8位)"
fi
# Tier-Two: 6位/30秒
if grep -rq "Digits.*6" pkg/totp/ 2>/dev/null || grep -rq "6.*Digits" pkg/totp/ 2>/dev/null; then
echo "✓ Tier-Two TOTP位数配置存在"
else
echo "⚠ 未找到Tier-Two TOTP位数定义 (应为6位)"
fi
# 5. K8S操作白名单验证
echo ""
echo "[5/5] K8S操作白名单验证..."
ALLOWED_ACTIONS="logs exec scale restart delete get apply"
K8S_SERVICE="internal/service/k8s_service.go"
if [ -f "$K8S_SERVICE" ]; then
for action in $ALLOWED_ACTIONS; do
if grep -q "case \"$action\"" "$K8S_SERVICE" 2>/dev/null; then
echo "✓ K8S操作 '$action' 已实现"
else
echo "⚠ K8S操作 '$action' 未找到"
fi
done
else
echo "⚠ K8S服务文件不存在: $K8S_SERVICE"
fi
echo ""
echo "=== 验证完成 ==="

View File

@@ -0,0 +1,145 @@
---
name: developing-work-procedure
description: Guides development of rmdc-work-procedure workflow module including state machine implementation, workflow CRUD operations, status transitions, optimistic locking, WebSocket events, and Mermaid diagram generation. Triggers when modifying workflow tables, adding workflow types, changing state transitions, or implementing workflow APIs. Keywords: workflow, work-procedure, state-machine, optimistic-lock, workflow-type, transition, mermaid.
argument-hint: "<workflow-type|api-path|table-name|state-name> - specify the workflow type (user_registration|user_management|project_detail|microservice_update), API path, database table, or state to work on"
allowed-tools:
- Read
- Edit
- Write
- Glob
- Grep
- Bash
---
# Developing Work Procedure Module
本 Skill 指导 `rmdc-work-procedure` 工单流程模块的开发,包括状态机实现、工单 CRUD、状态转换、并发控制、事件通知与流程图生成。
## Quick Context
```bash
# 动态注入:查看模块结构
!`find . -name "*.go" -path "*/rmdc-work-procedure/*" | head -20`
# 动态注入:查看现有状态定义
!`grep -rn "status.*=" --include="*.go" ./rmdc-work-procedure/`
```
## Plan
### 产物清单
- [ ] 状态机配置/代码(状态定义、转换规则、权限矩阵)
- [ ] 数据库 Migrationworkflows主表、steps表、history表、扩展表
- [ ] API Handler + Service 实现
- [ ] WebSocket 事件推送逻辑
- [ ] Mermaid 流程图生成器
- [ ] 单元测试 + 集成测试
### 决策点
1. **工单类型**新增还是修改现有类型user_registration / user_management / project_detail / microservice_update
2. **状态扩展**:是否需要扩展状态?(如 microservice_update 的 executing/monitoring/rollbacked
3. **并发策略**:乐观锁版本号校验是否充分?
4. **回调契约**:业务模块回调接口是否需要变更?
## Verify
### 状态机完整性
- [ ] 所有状态均有明确的入口和出口转换
- [ ] 终态approved/rejected/revoked/closed不可再转换
- [ ] 撤销revoke仅对非终态生效
- [ ] 状态转换权限矩阵与角色匹配SuperAdmin/Creator/Assignee/System
### 数据库一致性
- [ ] `workflows` 主表包含 `version` 字段(乐观锁)
- [ ] 扩展表 FK 正确引用 `workflows.id`
- [ ] `workflow_track_history` 记录所有状态变更
- [ ] Migration 可回滚(提供 DOWN 脚本)
### API 契约兼容
- [ ] POST 统一风格(非 RESTful GET/PUT/DELETE
- [ ] 响应结构 `{code, message, data}` 一致
- [ ] 错误码对齐全局规范409 版本冲突、403 权限不足、404 工单不存在)
- [ ] `version` 字段在更新请求中必传
### 事件一致性
- [ ] WebSocket 事件类型枚举完整(见 reference/websocket-events.md
- [ ] 事件 payload 包含 workflow_id、from_status、to_status
- [ ] 通知中心模块接口调用正确
### 流程图生成
- [ ] Mermaid 源码语法正确stateDiagram-v2
- [ ] 当前节点高亮classDef current
- [ ] 前端异步加载 mermaid 库
## Execute
### 1. 状态机实现
```bash
# 检查现有状态定义
!`grep -rn "const.*Status" ./rmdc-work-procedure/`
# 创建/更新状态枚举
# 参考 reference/state-machine.md
```
### 2. 数据库变更
```bash
# 生成 migration 文件
go run cmd/migrate/main.go create add_workflow_tables
# 验证 schema
./scripts/verify-schema.sh
```
### 3. API 实现
```bash
# 查看现有 handler
!`ls -la ./rmdc-work-procedure/internal/handler/`
# 实现核心接口(参考 reference/api-contracts.md
# - /api/workflow/create
# - /api/workflow/transition
# - /api/workflow/callback
```
### 4. 并发控制
```go
// 乐观锁更新模板(见 examples/state-transition.go
result := db.Model(&Workflow{}).
Where("id = ? AND version = ?", id, version).
Updates(map[string]interface{}{
"status": newStatus,
"version": gorm.Expr("version + 1"),
})
if result.RowsAffected == 0 {
return ErrVersionConflict // 409
}
```
### 5. 测试验证
```bash
# 运行状态机测试
go test ./rmdc-work-procedure/... -run TestStateMachine -v
# 验证 API 契约
./scripts/verify-api-contracts.sh
```
## Pitfalls
1. **版本号遗漏**:更新工单时忘记传递 `version` 字段,导致乐观锁失效;客户端需缓存并回传 version
2. **终态误转换**:对 approved/rejected/revoked/closed 状态尝试非法转换,需在服务层硬校验
3. **扩展表不同步**:创建工单时忘记同步写入对应 `*_ext` 扩展表,导致业务数据丢失
4. **事件推送遗漏**:状态变更后忘记调用通知中心,处理人无法收到实时通知
5. **撤销硬删除**user_registration 类型撤销需硬删除用户,其他类型仅标记状态
6. **回调幂等**:业务模块回调 `/api/workflow/callback` 需做幂等处理,避免重复状态变更
7. **草稿状态混淆**project_detail 的 `draft_saved` 是扩展状态,不属于基础状态机
8. **Mermaid 安全**:前端初始化 mermaid 需设置 `securityLevel: 'strict'`,避免 XSS
## Related References
- [状态机定义](reference/state-machine.md)
- [数据库 Schema](reference/database-schema.md)
- [API 契约](reference/api-contracts.md)
- [工单类型详情](reference/workflow-types.md)
- [WebSocket 事件](reference/websocket-events.md)

View File

@@ -0,0 +1,227 @@
package mermaid
import (
"fmt"
"strings"
)
// DiagramResponse 流程图响应
type DiagramResponse struct {
MermaidCode string `json:"mermaid_code"`
CurrentNode string `json:"current_node"`
Nodes []DiagramNode `json:"nodes"`
}
// DiagramNode 节点信息
type DiagramNode struct {
ID string `json:"id"`
Label string `json:"label"`
Status string `json:"status"` // completed / current / pending / error
Assignee string `json:"assignee"`
CompletedAt string `json:"completed_at"`
}
// DiagramGenerator 流程图生成器
type DiagramGenerator struct{}
// NewDiagramGenerator 创建生成器
func NewDiagramGenerator() *DiagramGenerator {
return &DiagramGenerator{}
}
// Generate 根据工单类型生成流程图
func (g *DiagramGenerator) Generate(workflowType, currentStatus string, nodes []DiagramNode) *DiagramResponse {
switch workflowType {
case "user_registration":
return g.generateUserRegistration(currentStatus, nodes)
case "user_management":
return g.generateUserManagement(currentStatus, nodes)
case "project_detail":
return g.generateProjectDetail(currentStatus, nodes)
case "microservice_update":
return g.generateMicroserviceUpdate(currentStatus, nodes)
default:
return g.generateDefault(currentStatus, nodes)
}
}
// generateUserRegistration 生成用户注册流程图
func (g *DiagramGenerator) generateUserRegistration(currentStatus string, nodes []DiagramNode) *DiagramResponse {
var sb strings.Builder
sb.WriteString("stateDiagram-v2\n")
sb.WriteString(" direction LR\n\n")
sb.WriteString(" [*] --> created: 用户注册\n\n")
sb.WriteString(" created --> pending_review: 自动提交审核\n")
sb.WriteString(" created --> revoked: 用户撤销\n\n")
sb.WriteString(" pending_review --> approved: 超管审批通过\n")
sb.WriteString(" pending_review --> rejected: 超管审批拒绝\n")
sb.WriteString(" pending_review --> revoked: 用户撤销\n\n")
sb.WriteString(" rejected --> pending_review: 用户修改后重新提交\n")
sb.WriteString(" rejected --> revoked: 用户撤销\n\n")
sb.WriteString(" approved --> closed: 关闭工单\n")
sb.WriteString(" rejected --> closed: 关闭工单\n")
sb.WriteString(" revoked --> closed: 关闭工单\n\n")
sb.WriteString(" closed --> [*]\n\n")
g.appendStyles(&sb, currentStatus)
return &DiagramResponse{
MermaidCode: sb.String(),
CurrentNode: currentStatus,
Nodes: nodes,
}
}
// generateUserManagement 生成用户管理流程图
func (g *DiagramGenerator) generateUserManagement(currentStatus string, nodes []DiagramNode) *DiagramResponse {
var sb strings.Builder
sb.WriteString("stateDiagram-v2\n")
sb.WriteString(" direction LR\n\n")
sb.WriteString(" [*] --> created: 发起管理操作\n\n")
sb.WriteString(" created --> pending_review: 提交审核\n")
sb.WriteString(" created --> revoked: 用户撤销\n")
sb.WriteString(" created --> closed: 用户关闭\n\n")
sb.WriteString(" pending_review --> approved: 超管审批通过\n")
sb.WriteString(" pending_review --> rejected: 超管审批拒绝\n")
sb.WriteString(" pending_review --> returned: 超管打回\n")
sb.WriteString(" pending_review --> revoked: 用户撤销\n\n")
sb.WriteString(" returned --> pending_review: 重新提交\n")
sb.WriteString(" returned --> revoked: 用户撤销\n")
sb.WriteString(" returned --> closed: 用户关闭\n\n")
sb.WriteString(" rejected --> closed: 关闭工单\n")
sb.WriteString(" approved --> closed: 关闭工单\n")
sb.WriteString(" revoked --> closed: 关闭工单\n\n")
sb.WriteString(" closed --> [*]\n\n")
g.appendStyles(&sb, currentStatus)
return &DiagramResponse{
MermaidCode: sb.String(),
CurrentNode: currentStatus,
Nodes: nodes,
}
}
// generateProjectDetail 生成项目详情工单流程图
func (g *DiagramGenerator) generateProjectDetail(currentStatus string, nodes []DiagramNode) *DiagramResponse {
var sb strings.Builder
sb.WriteString("stateDiagram-v2\n")
sb.WriteString(" direction LR\n\n")
sb.WriteString(" [*] --> created: 创建项目\n")
sb.WriteString(" created --> assigned: 分配填写人\n\n")
sb.WriteString(" assigned --> in_progress: 用户开始填写\n")
sb.WriteString(" assigned --> assigned: 重新分配\n\n")
sb.WriteString(" in_progress --> draft_saved: 保存草稿\n")
sb.WriteString(" draft_saved --> in_progress: 继续填写\n")
sb.WriteString(" in_progress --> pending_review: 提交审核\n")
sb.WriteString(" in_progress --> assigned: 重新分配\n\n")
sb.WriteString(" pending_review --> approved: 审批通过\n")
sb.WriteString(" pending_review --> returned: 审批打回\n\n")
sb.WriteString(" returned --> in_progress: 用户重新填写\n")
sb.WriteString(" returned --> pending_review: 重新提交\n\n")
sb.WriteString(" approved --> closed: 项目认证完成\n\n")
sb.WriteString(" closed --> [*]\n\n")
g.appendStyles(&sb, currentStatus)
return &DiagramResponse{
MermaidCode: sb.String(),
CurrentNode: currentStatus,
Nodes: nodes,
}
}
// generateMicroserviceUpdate 生成微服务更新流程图
func (g *DiagramGenerator) generateMicroserviceUpdate(currentStatus string, nodes []DiagramNode) *DiagramResponse {
var sb strings.Builder
sb.WriteString("stateDiagram-v2\n")
sb.WriteString(" direction LR\n\n")
sb.WriteString(" [*] --> created: 发起更新请求\n\n")
sb.WriteString(" created --> pending_review: 提交审核\n")
sb.WriteString(" created --> revoked: 用户撤销\n\n")
sb.WriteString(" pending_review --> approved: 审批通过\n")
sb.WriteString(" pending_review --> returned: 审批打回\n")
sb.WriteString(" pending_review --> rejected: 审批拒绝\n")
sb.WriteString(" pending_review --> revoked: 用户撤销\n\n")
sb.WriteString(" returned --> pending_review: 修改后重新提交\n")
sb.WriteString(" returned --> revoked: 用户撤销\n\n")
sb.WriteString(" approved --> executing: 开始执行\n\n")
sb.WriteString(" executing --> monitoring: 更新成功\n")
sb.WriteString(" executing --> rollbacked: 更新失败回滚\n\n")
sb.WriteString(" monitoring --> closed: 运行正常\n")
sb.WriteString(" monitoring --> rollbacked: 运行异常回滚\n\n")
sb.WriteString(" rollbacked --> closed: 记录失败信息\n")
sb.WriteString(" rejected --> closed: 关闭工单\n")
sb.WriteString(" revoked --> closed: 关闭工单\n\n")
sb.WriteString(" closed --> [*]\n\n")
g.appendStyles(&sb, currentStatus)
return &DiagramResponse{
MermaidCode: sb.String(),
CurrentNode: currentStatus,
Nodes: nodes,
}
}
// generateDefault 生成默认流程图
func (g *DiagramGenerator) generateDefault(currentStatus string, nodes []DiagramNode) *DiagramResponse {
var sb strings.Builder
sb.WriteString("stateDiagram-v2\n")
sb.WriteString(" direction LR\n\n")
sb.WriteString(" [*] --> created: 工单创建\n\n")
sb.WriteString(" created --> pending: 提交\n")
sb.WriteString(" created --> assigned: 自动分配\n\n")
sb.WriteString(" pending --> assigned: 分配\n\n")
sb.WriteString(" assigned --> in_progress: 接单\n\n")
sb.WriteString(" in_progress --> pending_review: 完成\n\n")
sb.WriteString(" pending_review --> approved: 通过\n")
sb.WriteString(" pending_review --> rejected: 拒绝\n")
sb.WriteString(" pending_review --> returned: 打回\n\n")
sb.WriteString(" returned --> in_progress: 重新提交\n\n")
sb.WriteString(" approved --> closed: 关闭\n")
sb.WriteString(" rejected --> closed: 关闭\n\n")
sb.WriteString(" closed --> [*]\n\n")
g.appendStyles(&sb, currentStatus)
return &DiagramResponse{
MermaidCode: sb.String(),
CurrentNode: currentStatus,
Nodes: nodes,
}
}
// appendStyles 添加样式定义
func (g *DiagramGenerator) appendStyles(sb *strings.Builder, currentStatus string) {
sb.WriteString(" classDef completed fill:#4caf50,color:#fff\n")
sb.WriteString(" classDef current fill:#ff9800,color:#fff,stroke-width:3px\n")
sb.WriteString(" classDef pending fill:#e0e0e0,color:#666\n")
sb.WriteString(" classDef error fill:#f44336,color:#fff\n\n")
sb.WriteString(fmt.Sprintf(" class %s current\n", currentStatus))
}
// InjectNodeStyles 根据节点状态注入样式
func (g *DiagramGenerator) InjectNodeStyles(mermaidCode string, nodes []DiagramNode) string {
var sb strings.Builder
sb.WriteString(mermaidCode)
sb.WriteString("\n")
for _, node := range nodes {
switch node.Status {
case "completed":
sb.WriteString(fmt.Sprintf(" class %s completed\n", node.ID))
case "current":
sb.WriteString(fmt.Sprintf(" class %s current\n", node.ID))
case "error":
sb.WriteString(fmt.Sprintf(" class %s error\n", node.ID))
}
}
return sb.String()
}

View File

@@ -0,0 +1,198 @@
package statemachine
// State 工单状态
type State string
const (
StatusCreated State = "created"
StatusPending State = "pending"
StatusAssigned State = "assigned"
StatusInProgress State = "in_progress"
StatusPendingReview State = "pending_review"
StatusReturned State = "returned"
StatusApproved State = "approved"
StatusRejected State = "rejected"
StatusRevoked State = "revoked"
StatusClosed State = "closed"
// 扩展状态 - microservice_update
StatusExecuting State = "executing"
StatusMonitoring State = "monitoring"
StatusRollbacked State = "rollbacked"
// 扩展状态 - project_detail
StatusDraftSaved State = "draft_saved"
)
// Event 转换事件
type Event string
const (
EventSubmit Event = "submit"
EventAutoAssign Event = "auto_assign"
EventAssign Event = "assign"
EventAccept Event = "accept"
EventReassign Event = "reassign"
EventComplete Event = "complete"
EventApprove Event = "approve"
EventReject Event = "reject"
EventReturn Event = "return"
EventResubmit Event = "resubmit"
EventRevoke Event = "revoke"
EventClose Event = "close"
// 扩展事件 - microservice_update
EventExecute Event = "execute"
EventExecuteOK Event = "execute_ok"
EventExecuteFail Event = "execute_fail"
EventMonitorOK Event = "monitor_ok"
EventMonitorFail Event = "monitor_fail"
EventRollbackDone Event = "rollback_done"
// 扩展事件 - project_detail
EventSaveDraft Event = "save_draft"
EventContinueEdit Event = "continue_edit"
)
// Transition 状态转换规则
type Transition struct {
From State
Event Event
To State
}
// BaseTransitions 基础状态转换规则
var BaseTransitions = []Transition{
// 创建后流转
{StatusCreated, EventSubmit, StatusPending},
{StatusCreated, EventAutoAssign, StatusAssigned},
{StatusCreated, EventRevoke, StatusRevoked},
// 待分配流转
{StatusPending, EventAssign, StatusAssigned},
{StatusPending, EventRevoke, StatusRevoked},
// 已分配流转
{StatusAssigned, EventAccept, StatusInProgress},
{StatusAssigned, EventReassign, StatusAssigned},
{StatusAssigned, EventRevoke, StatusRevoked},
// 处理中流转
{StatusInProgress, EventComplete, StatusPendingReview},
{StatusInProgress, EventReturn, StatusReturned},
{StatusInProgress, EventReassign, StatusAssigned},
{StatusInProgress, EventRevoke, StatusRevoked},
// 待审核流转
{StatusPendingReview, EventApprove, StatusApproved},
{StatusPendingReview, EventReject, StatusRejected},
{StatusPendingReview, EventReturn, StatusReturned},
{StatusPendingReview, EventRevoke, StatusRevoked},
// 已打回流转
{StatusReturned, EventResubmit, StatusInProgress},
{StatusReturned, EventRevoke, StatusRevoked},
// 终态关闭
{StatusApproved, EventClose, StatusClosed},
{StatusRejected, EventClose, StatusClosed},
{StatusRevoked, EventClose, StatusClosed},
}
// MicroserviceUpdateTransitions 微服务更新扩展转换规则
var MicroserviceUpdateTransitions = []Transition{
{StatusApproved, EventExecute, StatusExecuting},
{StatusExecuting, EventExecuteOK, StatusMonitoring},
{StatusExecuting, EventExecuteFail, StatusRollbacked},
{StatusMonitoring, EventMonitorOK, StatusClosed},
{StatusMonitoring, EventMonitorFail, StatusRollbacked},
{StatusRollbacked, EventRollbackDone, StatusClosed},
}
// ProjectDetailTransitions 项目详情扩展转换规则
var ProjectDetailTransitions = []Transition{
{StatusInProgress, EventSaveDraft, StatusDraftSaved},
{StatusDraftSaved, EventContinueEdit, StatusInProgress},
}
// TerminalStates 终态集合
var TerminalStates = map[State]bool{
StatusApproved: true,
StatusRejected: true,
StatusRevoked: true,
StatusClosed: true,
}
// AllTransitions 获取所有转换规则(包含扩展)
func AllTransitions(workflowType string) []Transition {
transitions := make([]Transition, len(BaseTransitions))
copy(transitions, BaseTransitions)
switch workflowType {
case "microservice_update":
transitions = append(transitions, MicroserviceUpdateTransitions...)
case "project_detail":
transitions = append(transitions, ProjectDetailTransitions...)
}
return transitions
}
// CanTransition 检查是否可以转换
func CanTransition(from State, event Event) (State, bool) {
for _, t := range BaseTransitions {
if t.From == from && t.Event == event {
return t.To, true
}
}
return "", false
}
// CanTransitionWithType 检查是否可以转换(带工单类型)
func CanTransitionWithType(from State, event Event, workflowType string) (State, bool) {
transitions := AllTransitions(workflowType)
for _, t := range transitions {
if t.From == from && t.Event == event {
return t.To, true
}
}
return "", false
}
// IsTerminal 检查是否为终态
func IsTerminal(s State) bool {
return TerminalStates[s]
}
// CanRevoke 检查是否可撤销
func CanRevoke(s State) bool {
return !IsTerminal(s)
}
// GetAvailableEvents 获取当前状态可用的事件
func GetAvailableEvents(from State, workflowType string) []Event {
transitions := AllTransitions(workflowType)
events := make([]Event, 0)
seen := make(map[Event]bool)
for _, t := range transitions {
if t.From == from && !seen[t.Event] {
events = append(events, t.Event)
seen[t.Event] = true
}
}
return events
}
// ValidateTransitionChain 验证转换链是否合法
func ValidateTransitionChain(workflowType string, states []State, events []Event) bool {
if len(states) != len(events)+1 {
return false
}
for i, event := range events {
toState, valid := CanTransitionWithType(states[i], event, workflowType)
if !valid || toState != states[i+1] {
return false
}
}
return true
}

View File

@@ -0,0 +1,252 @@
package service
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
)
// Workflow 工单实体
type Workflow struct {
ID string `gorm:"primaryKey;size:64"`
Version int `gorm:"not null;default:1"`
ModuleCode string `gorm:"size:32;not null"`
WorkflowType string `gorm:"size:32;not null"`
Priority int `gorm:"default:3"`
Status string `gorm:"size:32;not null"`
CurrentStep int `gorm:"default:1"`
TotalSteps int `gorm:"default:1"`
CreatorID int64 `gorm:"not null"`
CreatorName string `gorm:"size:64"`
AssigneeID *int64
AssigneeName string `gorm:"size:64"`
DelegatedBy *int64
Deadline *time.Time
StartedAt *time.Time
CompletedAt *time.Time
Title string `gorm:"size:256"`
Description string `gorm:"type:text"`
BusinessPayload string `gorm:"type:json"`
ResultPayload string `gorm:"type:json"`
CreatedAt time.Time
UpdatedAt time.Time
}
// WorkflowTrackHistory 工单追踪历史
type WorkflowTrackHistory struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
WorkflowID string `gorm:"size:64;not null"`
FromStatus string `gorm:"size:32"`
ToStatus string `gorm:"size:32"`
Event string `gorm:"size:32"`
OperatorID int64
OperatorType string `gorm:"size:16"` // user / system / timer
OperatorIP string `gorm:"size:45"`
ChangeDetails string `gorm:"type:json"`
Remark string `gorm:"type:text"`
CreatedAt time.Time
}
// CreateWorkflowRequest 创建工单请求
type CreateWorkflowRequest struct {
ModuleCode string
ModulePrefix string
WorkflowType string
Title string
Description string
Priority int
CreatorID int64
CreatorName string
CreatorUsername string
AssigneeID *int64
Deadline *time.Time
BusinessPayload string
}
// TransitionRequest 状态转换请求
type TransitionRequest struct {
WorkflowID string
Version int
FromStatus string
ToStatus string
Event string
OperatorID int64
OperatorType string
OperatorIP string
Remark string
}
// 错误定义
var (
ErrVersionConflict = fmt.Errorf("version conflict")
ErrWorkflowNotFound = fmt.Errorf("workflow not found")
ErrInvalidTransition = fmt.Errorf("invalid state transition")
)
// WorkflowService 工单服务
type WorkflowService struct {
db *gorm.DB
}
// NewWorkflowService 创建工单服务
func NewWorkflowService(db *gorm.DB) *WorkflowService {
return &WorkflowService{db: db}
}
// CreateWorkflow 创建工单
func (s *WorkflowService) CreateWorkflow(ctx context.Context, req *CreateWorkflowRequest) (*Workflow, error) {
// 生成工单ID: {module_prefix}-{timestamp}-{creator}
workflowID := fmt.Sprintf("%s-%s-%s",
req.ModulePrefix,
time.Now().Format("20060102150405"),
req.CreatorUsername,
)
workflow := &Workflow{
ID: workflowID,
Version: 1,
ModuleCode: req.ModuleCode,
WorkflowType: req.WorkflowType,
Status: StatusCreated,
Priority: req.Priority,
CreatorID: req.CreatorID,
CreatorName: req.CreatorName,
AssigneeID: req.AssigneeID,
Title: req.Title,
Description: req.Description,
Deadline: req.Deadline,
BusinessPayload: req.BusinessPayload,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// 事务:同时创建主表和扩展表
err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(workflow).Error; err != nil {
return err
}
// 根据工单类型创建扩展表记录
return s.createExtension(tx, workflow, req)
})
if err != nil {
return nil, err
}
return workflow, nil
}
// createExtension 根据工单类型创建扩展表记录
func (s *WorkflowService) createExtension(tx *gorm.DB, workflow *Workflow, req *CreateWorkflowRequest) error {
switch workflow.WorkflowType {
case WorkflowTypeUserRegistration:
// 创建用户注册扩展记录
return tx.Exec(`
INSERT INTO user_registration_ext (workflow_id, target_user_id, original_status)
VALUES (?, ?, ?)
`, workflow.ID, 0, "disabled").Error
case WorkflowTypeUserManagement:
// 创建用户管理扩展记录
return tx.Exec(`
INSERT INTO user_management_ext (workflow_id, target_user_id, action_type)
VALUES (?, ?, ?)
`, workflow.ID, 0, "modify").Error
case WorkflowTypeProjectDetail:
// 创建项目详情扩展记录
return tx.Exec(`
INSERT INTO project_detail_ext (workflow_id, project_id)
VALUES (?, ?)
`, workflow.ID, "").Error
case WorkflowTypeMicroserviceUpdate:
// 创建微服务更新扩展记录
return tx.Exec(`
INSERT INTO microservice_update_ext (workflow_id, project_id, namespace, service_name)
VALUES (?, ?, ?, ?)
`, workflow.ID, "", "", "").Error
}
return nil
}
// TransitionWorkflow 状态转换(带乐观锁)
func (s *WorkflowService) TransitionWorkflow(ctx context.Context, req *TransitionRequest) error {
// 验证状态转换是否合法
toStatus, valid := CanTransition(State(req.FromStatus), Event(req.Event))
if !valid {
return ErrInvalidTransition
}
req.ToStatus = string(toStatus)
return s.db.Transaction(func(tx *gorm.DB) error {
// 1. 乐观锁更新
result := tx.Model(&Workflow{}).
Where("id = ? AND version = ?", req.WorkflowID, req.Version).
Updates(map[string]interface{}{
"status": req.ToStatus,
"version": gorm.Expr("version + 1"),
"updated_at": time.Now(),
})
if result.RowsAffected == 0 {
return ErrVersionConflict // 409
}
// 2. 记录状态变更历史
history := &WorkflowTrackHistory{
WorkflowID: req.WorkflowID,
FromStatus: req.FromStatus,
ToStatus: req.ToStatus,
Event: req.Event,
OperatorID: req.OperatorID,
OperatorType: req.OperatorType,
OperatorIP: req.OperatorIP,
Remark: req.Remark,
CreatedAt: time.Now(),
}
return tx.Create(history).Error
})
}
// GetWorkflow 获取工单详情
func (s *WorkflowService) GetWorkflow(ctx context.Context, workflowID string) (*Workflow, error) {
var workflow Workflow
if err := s.db.Where("id = ?", workflowID).First(&workflow).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrWorkflowNotFound
}
return nil, err
}
return &workflow, nil
}
// GetWorkflowHistory 获取工单历史
func (s *WorkflowService) GetWorkflowHistory(ctx context.Context, workflowID string) ([]WorkflowTrackHistory, error) {
var history []WorkflowTrackHistory
err := s.db.Where("workflow_id = ?", workflowID).
Order("created_at ASC").
Find(&history).Error
return history, err
}
// 工单类型常量
const (
WorkflowTypeUserRegistration = "user_registration"
WorkflowTypeUserManagement = "user_management"
WorkflowTypeProjectDetail = "project_detail"
WorkflowTypeMicroserviceUpdate = "microservice_update"
)
// 状态常量
const (
StatusCreated = "created"
StatusPending = "pending"
StatusAssigned = "assigned"
StatusInProgress = "in_progress"
StatusPendingReview = "pending_review"
StatusReturned = "returned"
StatusApproved = "approved"
StatusRejected = "rejected"
StatusRevoked = "revoked"
StatusClosed = "closed"
)

View File

@@ -0,0 +1,357 @@
# API 契约
## 通用响应格式
```json
{
"code": 0,
"message": "success",
"data": { ... }
}
```
## 错误码
| 错误码 | HTTP Status | 说明 |
|-------|-------------|------|
| 0 | 200 | 成功 |
| 40001 | 400 | 参数校验失败 |
| 40301 | 403 | 权限不足 |
| 40401 | 404 | 工单不存在 |
| 40901 | 409 | 版本冲突(乐观锁) |
| 50001 | 500 | 服务器内部错误 |
## 工单核心接口
### POST /api/workflow/create
创建工单
**Request:**
```json
{
"module_code": "rmdc-project-management",
"workflow_type": "project_detail",
"title": "项目信息填写-ProjectA",
"description": "请填写项目详细信息",
"priority": 3,
"assignee_id": 123,
"deadline": "2026-01-15T18:00:00Z",
"business_payload": {
"project_id": "proj-001"
}
}
```
**Response:**
```json
{
"code": 0,
"message": "success",
"data": {
"workflow_id": "project-20260108100000-admin",
"status": "created",
"version": 1,
"created_at": "2026-01-08T10:00:00Z"
}
}
```
### POST /api/workflow/detail
获取工单详情
**Request:**
```json
{
"workflow_id": "project-20260108100000-admin"
}
```
**Response:**
```json
{
"code": 0,
"message": "success",
"data": {
"id": "project-20260108100000-admin",
"version": 2,
"module_code": "rmdc-project-management",
"workflow_type": "project_detail",
"status": "in_progress",
"title": "项目信息填写-ProjectA",
"creator_id": 1,
"creator_name": "超级管理员",
"assignee_id": 123,
"assignee_name": "张三",
"current_step": 2,
"total_steps": 3,
"steps": [...],
"history": [...],
"business_payload": {...},
"created_at": "2026-01-08T10:00:00Z",
"updated_at": "2026-01-08T11:30:00Z"
}
}
```
### POST /api/workflow/status
获取工单状态(轻量)
**Request:**
```json
{
"workflow_id": "project-20260108100000-admin"
}
```
**Response:**
```json
{
"code": 0,
"data": {
"workflow_id": "project-20260108100000-admin",
"status": "in_progress",
"version": 2,
"updated_at": "2026-01-08T11:30:00Z"
}
}
```
### POST /api/workflow/transition
状态转换
**Request:**
```json
{
"workflow_id": "project-20260108100000-admin",
"version": 2,
"event": "complete",
"remark": "项目信息填写完成,提交审核"
}
```
**Response:**
```json
{
"code": 0,
"message": "success",
"data": {
"workflow_id": "project-20260108100000-admin",
"from_status": "in_progress",
"to_status": "pending_review",
"version": 3
}
}
```
**版本冲突响应 (409):**
```json
{
"code": 40901,
"message": "版本冲突,工单已被其他人修改,请刷新后重试"
}
```
### POST /api/workflow/assign
分配处理人
**Request:**
```json
{
"workflow_id": "project-20260108100000-admin",
"version": 1,
"assignee_id": 123,
"remark": "分配给张三填写"
}
```
### POST /api/workflow/delegate
委派给他人
**Request:**
```json
{
"workflow_id": "microservice-20260108150000-user",
"version": 3,
"delegate_to_id": 456,
"remark": "委派给李四执行更新"
}
```
### POST /api/workflow/revoke
撤销工单
**Request:**
```json
{
"workflow_id": "user-20260108090000-zhangsan",
"version": 2,
"confirm": true,
"remark": "用户取消注册"
}
```
### POST /api/workflow/callback
业务模块回调
**Request:**
```json
{
"workflow_id": "microservice-20260108150000-user",
"callback_type": "execute_complete",
"result": "success",
"result_payload": {
"execute_time": "2026-01-08T15:35:00Z",
"new_pod_count": 3
}
}
```
**callback_type 枚举:**
- `execute_start` - 开始执行
- `execute_complete` - 执行完成
- `execute_failed` - 执行失败
- `monitor_healthy` - 监控健康
- `monitor_unhealthy` - 监控异常
- `rollback_complete` - 回滚完成
## 工单列表与查询
### POST /api/workflow/list
工单列表(分页)
**Request:**
```json
{
"page": 1,
"page_size": 20,
"workflow_type": "project_detail",
"status": ["in_progress", "pending_review"],
"creator_id": 1,
"assignee_id": 123,
"start_time": "2026-01-01T00:00:00Z",
"end_time": "2026-01-31T23:59:59Z"
}
```
**Response:**
```json
{
"code": 0,
"data": {
"total": 50,
"page": 1,
"page_size": 20,
"list": [...]
}
}
```
### POST /api/workflow/my/created
我发起的工单
### POST /api/workflow/my/assigned
分配给我的工单
### POST /api/workflow/my/pending
待我处理的工单
### POST /api/workflow/history
工单历史记录
**Request:**
```json
{
"workflow_id": "project-20260108100000-admin"
}
```
**Response:**
```json
{
"code": 0,
"data": {
"history": [
{
"id": 1,
"from_status": "created",
"to_status": "assigned",
"event": "assign",
"operator_name": "超级管理员",
"remark": "分配给张三填写",
"created_at": "2026-01-08T10:05:00Z"
}
]
}
}
```
### POST /api/workflow/diagram
获取工单流程图
**Request:**
```json
{
"workflow_id": "project-20260108100000-admin"
}
```
**Response:**
```json
{
"code": 0,
"data": {
"mermaid_code": "stateDiagram-v2\n direction LR\n [*] --> created: 创建项目\n ...",
"current_node": "pending_review",
"nodes": [
{
"id": "created",
"label": "创建项目",
"status": "completed",
"assignee": "超级管理员",
"completed_at": "2026-01-08T10:00:00Z"
},
{
"id": "in_progress",
"label": "填写信息",
"status": "completed",
"assignee": "张三",
"completed_at": "2026-01-08T14:00:00Z"
},
{
"id": "pending_review",
"label": "待审核",
"status": "current",
"assignee": "超级管理员"
}
]
}
}
```
## 权限管理接口(仅 SuperAdmin
### POST /api/workflow/permission/grant
授予权限
### POST /api/workflow/permission/revoke
撤销权限
### POST /api/workflow/permission/list
权限列表

View File

@@ -0,0 +1,236 @@
# 数据库 Schema
## 主表 workflows
```sql
CREATE TABLE workflows (
id VARCHAR(64) PRIMARY KEY, -- 格式: {module}-{timestamp}-{creator}
version INT NOT NULL DEFAULT 1, -- 乐观锁版本号
-- 工单类型与来源
module_code VARCHAR(32) NOT NULL, -- rmdc-user-auth / rmdc-project-management / deliver-update
workflow_type VARCHAR(32) NOT NULL, -- user_registration / user_management / project_detail / microservice_update
priority INT DEFAULT 3, -- 1-5 优先级
-- 状态与流程
status VARCHAR(32) NOT NULL,
current_step INT DEFAULT 1,
total_steps INT DEFAULT 1,
-- 人员关联
creator_id BIGINT NOT NULL,
creator_name VARCHAR(64),
assignee_id BIGINT,
assignee_name VARCHAR(64),
delegated_by BIGINT, -- 委派来源
-- 时间管理
deadline DATETIME,
started_at DATETIME,
completed_at DATETIME,
-- 业务数据
title VARCHAR(256),
description TEXT,
business_payload JSON, -- 通用业务数据
result_payload JSON, -- 处理结果数据
-- 审计
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_module_code (module_code),
INDEX idx_workflow_type (workflow_type),
INDEX idx_status (status),
INDEX idx_creator (creator_id),
INDEX idx_assignee (assignee_id)
);
```
## 步骤表 workflow_steps
```sql
CREATE TABLE workflow_steps (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
workflow_id VARCHAR(64) NOT NULL,
step_order INT NOT NULL,
step_name VARCHAR(64),
step_type VARCHAR(32), -- submit / approve / execute / monitor
assignee_id BIGINT,
assignee_name VARCHAR(64),
status VARCHAR(32),
result VARCHAR(32),
remark TEXT,
input_data JSON,
output_data JSON,
started_at DATETIME,
completed_at DATETIME,
created_at DATETIME NOT NULL,
INDEX idx_workflow (workflow_id),
FOREIGN KEY (workflow_id) REFERENCES workflows(id)
);
```
## 追踪历史表 workflow_track_history
```sql
CREATE TABLE workflow_track_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
workflow_id VARCHAR(64) NOT NULL,
from_status VARCHAR(32),
to_status VARCHAR(32),
event VARCHAR(32), -- submit / approve / reject / return / revoke / close
operator_id BIGINT,
operator_type VARCHAR(16), -- user / system / timer
operator_ip VARCHAR(45),
change_details JSON,
remark TEXT,
created_at DATETIME NOT NULL,
INDEX idx_workflow (workflow_id),
INDEX idx_operator (operator_id)
);
```
## 扩展表
### user_registration_ext
用户注册工单扩展表
```sql
CREATE TABLE user_registration_ext (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
workflow_id VARCHAR(64) NOT NULL UNIQUE,
target_user_id BIGINT NOT NULL, -- 被注册用户ID
original_status VARCHAR(32), -- 原用户状态(备份)
FOREIGN KEY (workflow_id) REFERENCES workflows(id)
);
```
### user_management_ext
用户管理工单扩展表
```sql
CREATE TABLE user_management_ext (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
workflow_id VARCHAR(64) NOT NULL UNIQUE,
target_user_id BIGINT NOT NULL, -- 被管理用户ID
action_type VARCHAR(32) NOT NULL, -- modify / delete / enable / disable
original_data JSON, -- 变更前数据快照
modified_data JSON, -- 期望变更数据
FOREIGN KEY (workflow_id) REFERENCES workflows(id)
);
```
**original_data / modified_data 示例:**
```json
{
"username": "zhangsan",
"chinese_name": "张三",
"email": "zhangsan@example.com",
"phone": "13800138000",
"group_name": "产品与解决方案组",
"dev_role": "backend",
"rmdc_role": "normal"
}
```
### project_detail_ext
项目详情工单扩展表
```sql
CREATE TABLE project_detail_ext (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
workflow_id VARCHAR(64) NOT NULL UNIQUE,
project_id VARCHAR(64) NOT NULL, -- 关联项目ID
detail_filler_id BIGINT, -- 填写人ID
detail_filler_name VARCHAR(64), -- 填写人姓名
draft_data JSON, -- 草稿数据
FOREIGN KEY (workflow_id) REFERENCES workflows(id)
);
```
**draft_data 示例:**
```json
{
"deploy_business": {
"deployer_name": "张三",
"deployer_phone": "13800138000",
"deploy_start_time": "2026-01-01",
"system_version": "v3.2.0"
},
"deploy_env": {
"network_type": "internal",
"main_public_ip": "10.0.0.1",
"host_count": 3
},
"deploy_middleware": {
"mysql": {
"internal_ip": "10.0.0.10",
"internal_port": 3306
}
},
"last_saved_at": "2026-01-08T10:30:00Z"
}
```
### microservice_update_ext
微服务更新工单扩展表
```sql
CREATE TABLE microservice_update_ext (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
workflow_id VARCHAR(64) NOT NULL UNIQUE,
project_id VARCHAR(64) NOT NULL, -- 目标项目ID
namespace VARCHAR(64) NOT NULL, -- K8S命名空间
service_name VARCHAR(128) NOT NULL, -- 微服务名称
build_id VARCHAR(128), -- 构建物ID
current_version VARCHAR(64), -- 当前版本
target_version VARCHAR(64), -- 目标版本
scheduled_at DATETIME, -- 计划执行时间
execute_result VARCHAR(32), -- success / failed / rollbacked
rollback_info JSON, -- 回滚信息
FOREIGN KEY (workflow_id) REFERENCES workflows(id)
);
```
**rollback_info 示例:**
```json
{
"rollback_reason": "Pod健康检查失败",
"rollback_time": "2026-01-08T15:30:00Z",
"rollback_version": "v3.1.5",
"error_details": {
"pod_name": "service-abc-7d8f9c6b5d-xyz",
"error_message": "Readiness probe failed: connection refused",
"failed_checks": 3
}
}
```
## Migration 回滚脚本
```sql
-- DOWN migration
DROP TABLE IF EXISTS microservice_update_ext;
DROP TABLE IF EXISTS project_detail_ext;
DROP TABLE IF EXISTS user_management_ext;
DROP TABLE IF EXISTS user_registration_ext;
DROP TABLE IF EXISTS workflow_track_history;
DROP TABLE IF EXISTS workflow_steps;
DROP TABLE IF EXISTS workflows;
```

View File

@@ -0,0 +1,107 @@
# 工单状态机定义
## 基础状态(所有工单类型共享)
| 状态代码 | 状态名称 | 说明 | 是否终态 |
|---------|---------|------|---------|
| `created` | 已创建 | 工单刚创建,等待分配或自动分配 | 否 |
| `pending` | 待分配 | 等待管理员手动分配处理人 | 否 |
| `assigned` | 已分配 | 已分配处理人,等待接单 | 否 |
| `in_progress` | 处理中 | 处理人正在业务模块中处理 | 否 |
| `pending_review` | 待审核 | 处理完成,等待审核 | 否 |
| `returned` | 已打回 | 审核未通过,需重新处理 | 否 |
| `approved` | 已通过 | 审核通过 | **是** |
| `rejected` | 已拒绝 | 审核拒绝,流程终止 | **是** |
| `revoked` | 已撤销 | 发起人撤销 | **是** |
| `closed` | 已关闭 | 流程完结 | **是** |
## 扩展状态(按工单类型)
| 工单类型 | 扩展状态 | 说明 |
|---------|---------|------|
| `microservice_update` | `executing` | 微服务更新执行中 |
| `microservice_update` | `monitoring` | 微服务运行状态监控中 |
| `microservice_update` | `rollbacked` | 更新失败已回滚 |
| `project_detail` | `draft_saved` | 草稿已保存 |
## 状态转换权限矩阵
| 转换事件 | 触发角色 | 前置条件 |
|---------|---------|---------|
| `submit` | 创建人 | 状态为 `created` |
| `auto_assign` | 系统 | 配置了自动分配规则 |
| `assign` | SuperAdmin | 状态为 `pending` |
| `accept` | 处理人 | 状态为 `assigned` |
| `reassign` | SuperAdmin | 状态为 `assigned`/`in_progress` |
| `complete` | 处理人 | 状态为 `in_progress` |
| `approve` | SuperAdmin | 状态为 `pending_review` |
| `reject` | SuperAdmin | 状态为 `pending_review` |
| `return` | SuperAdmin | 状态为 `pending_review`/`in_progress` |
| `resubmit` | 创建人/处理人 | 状态为 `returned` |
| `revoke` | 创建人 | 状态非终态 |
| `close` | 系统/SuperAdmin | 状态为终态或 `revoked` |
## 通用状态转换图
```mermaid
stateDiagram-v2
[*] --> created: 工单创建
created --> pending: submit
created --> assigned: auto_assign
pending --> assigned: assign
assigned --> in_progress: accept
assigned --> assigned: reassign
in_progress --> pending_review: complete
in_progress --> returned: return
in_progress --> assigned: reassign
pending_review --> approved: approve
pending_review --> rejected: reject
pending_review --> returned: return
returned --> in_progress: resubmit
approved --> closed: close
rejected --> closed: close
created --> revoked: revoke
pending --> revoked: revoke
assigned --> revoked: revoke
in_progress --> revoked: revoke(需确认)
pending_review --> revoked: revoke(需确认)
returned --> revoked: revoke
revoked --> closed: close
closed --> [*]
```
## 微服务更新扩展状态转换
```mermaid
stateDiagram-v2
approved --> executing: 开始执行
approved --> approved: 等待定时执行
executing --> monitoring: 更新成功
executing --> executing: 执行重试
executing --> rollbacked: 更新失败回滚
monitoring --> closed: 运行正常
monitoring --> rollbacked: 运行异常回滚
rollbacked --> closed: 记录失败信息
```
## 项目详情扩展状态转换
```mermaid
stateDiagram-v2
in_progress --> draft_saved: 保存草稿
draft_saved --> in_progress: 继续填写
in_progress --> pending_review: 提交审核
```

View File

@@ -0,0 +1,237 @@
# WebSocket 事件定义
## 事件类型
| 事件类型 | 触发时机 | 接收方 |
|---------|---------|-------|
| `WORKFLOW_CREATED` | 工单创建 | 处理人 |
| `WORKFLOW_ASSIGNED` | 工单分配 | 处理人 |
| `WORKFLOW_REASSIGNED` | 工单重新委派 | 原处理人、新处理人 |
| `WORKFLOW_STATUS_CHANGED` | 状态变更 | 发起人、处理人 |
| `WORKFLOW_REVOKED` | 工单撤销 | 处理人 |
| `WORKFLOW_MODIFIED` | 工单内容修改 | 处理人 |
| `WORKFLOW_TIMEOUT` | 工单超时 | 发起人、处理人 |
| `WORKFLOW_COMPLETED` | 工单完成 | 发起人 |
## 事件 Payload 结构
```json
{
"event_type": "WORKFLOW_STATUS_CHANGED",
"workflow_id": "project-20260108100000-admin",
"workflow_type": "project_detail",
"from_status": "pending_review",
"to_status": "approved",
"operator_id": 1,
"operator_name": "超级管理员",
"timestamp": "2026-01-08T15:30:00Z",
"extra": {
"remark": "审批通过"
}
}
```
## 各事件详细说明
### WORKFLOW_CREATED
工单创建事件
```json
{
"event_type": "WORKFLOW_CREATED",
"workflow_id": "project-20260108100000-admin",
"workflow_type": "project_detail",
"title": "项目信息填写-ProjectA",
"creator_id": 1,
"creator_name": "超级管理员",
"assignee_id": 123,
"assignee_name": "张三",
"priority": 3,
"deadline": "2026-01-15T18:00:00Z",
"timestamp": "2026-01-08T10:00:00Z"
}
```
### WORKFLOW_ASSIGNED
工单分配事件
```json
{
"event_type": "WORKFLOW_ASSIGNED",
"workflow_id": "project-20260108100000-admin",
"assignee_id": 123,
"assignee_name": "张三",
"assigned_by_id": 1,
"assigned_by_name": "超级管理员",
"remark": "请在3天内完成项目信息填写",
"timestamp": "2026-01-08T10:05:00Z"
}
```
### WORKFLOW_REASSIGNED
工单重新委派事件
```json
{
"event_type": "WORKFLOW_REASSIGNED",
"workflow_id": "project-20260108100000-admin",
"from_assignee_id": 123,
"from_assignee_name": "张三",
"to_assignee_id": 456,
"to_assignee_name": "李四",
"reassigned_by_id": 1,
"reassigned_by_name": "超级管理员",
"remark": "张三请假,转交给李四处理",
"timestamp": "2026-01-09T09:00:00Z"
}
```
### WORKFLOW_STATUS_CHANGED
状态变更事件
```json
{
"event_type": "WORKFLOW_STATUS_CHANGED",
"workflow_id": "project-20260108100000-admin",
"workflow_type": "project_detail",
"from_status": "in_progress",
"to_status": "pending_review",
"event": "complete",
"operator_id": 123,
"operator_name": "张三",
"timestamp": "2026-01-08T14:00:00Z",
"extra": {
"remark": "项目信息填写完成"
}
}
```
### WORKFLOW_REVOKED
工单撤销事件
```json
{
"event_type": "WORKFLOW_REVOKED",
"workflow_id": "user-20260108090000-zhangsan",
"workflow_type": "user_registration",
"revoked_by_id": 100,
"revoked_by_name": "张三",
"remark": "用户取消注册",
"timestamp": "2026-01-08T11:00:00Z",
"extra": {
"target_user_deleted": true
}
}
```
### WORKFLOW_MODIFIED
工单内容修改事件
```json
{
"event_type": "WORKFLOW_MODIFIED",
"workflow_id": "project-20260108100000-admin",
"modified_by_id": 1,
"modified_by_name": "超级管理员",
"modified_fields": ["deadline", "priority"],
"timestamp": "2026-01-08T16:00:00Z",
"extra": {
"old_deadline": "2026-01-15T18:00:00Z",
"new_deadline": "2026-01-20T18:00:00Z",
"old_priority": 3,
"new_priority": 1
}
}
```
### WORKFLOW_TIMEOUT
工单超时事件
```json
{
"event_type": "WORKFLOW_TIMEOUT",
"workflow_id": "project-20260108100000-admin",
"workflow_type": "project_detail",
"deadline": "2026-01-15T18:00:00Z",
"current_status": "in_progress",
"assignee_id": 123,
"assignee_name": "张三",
"timestamp": "2026-01-15T18:00:01Z"
}
```
### WORKFLOW_COMPLETED
工单完成事件
```json
{
"event_type": "WORKFLOW_COMPLETED",
"workflow_id": "project-20260108100000-admin",
"workflow_type": "project_detail",
"final_status": "approved",
"result": "success",
"creator_id": 1,
"creator_name": "超级管理员",
"timestamp": "2026-01-10T10:00:00Z",
"extra": {
"total_duration_hours": 48,
"steps_completed": 3
}
}
```
## 前端处理策略
| 事件 | 前端行为 |
|-----|---------|
| `WORKFLOW_CREATED` | 待办列表增加新工单提醒 |
| `WORKFLOW_ASSIGNED` | 显示分配通知,可跳转工单详情 |
| `WORKFLOW_REASSIGNED` | 原处理人:显示提示弹窗,切换只读模式;新处理人:显示接收通知 |
| `WORKFLOW_STATUS_CHANGED` | 更新工单状态显示,刷新流程图高亮 |
| `WORKFLOW_REVOKED` | 自动关闭处理页面,引导至工单列表 |
| `WORKFLOW_MODIFIED` | 提示"工单已被修改",自动刷新或提示手动刷新 |
| `WORKFLOW_TIMEOUT` | 显示超时警告,标红显示 |
| `WORKFLOW_COMPLETED` | 显示完成通知,可查看结果 |
## 并发冲突处理
### 乐观锁冲突场景
当用户正在处理工单时,收到 `WORKFLOW_REASSIGNED``WORKFLOW_MODIFIED` 事件:
1. **WORKFLOW_REASSIGNED 处理流程**
- 显示弹窗「此工单已被重新委派给XXX您的处理权限已转移」
- 页面切换为只读模式
- 提供"返回列表"按钮
- 若用户仍尝试提交,后端返回 409 Conflict
2. **WORKFLOW_MODIFIED 处理流程**
- 显示提示:「工单内容已被修改,请刷新页面」
- 若用户有未保存的本地修改,提示冲突
- 提供"刷新"和"查看变更"选项
### 版本号同步
前端需在每次获取工单详情时缓存 `version` 字段,提交操作时携带该版本号:
```javascript
// 获取工单详情
const { data } = await api.getWorkflowDetail(workflowId);
this.workflowVersion = data.version;
// 提交状态转换
await api.transitionWorkflow({
workflow_id: workflowId,
version: this.workflowVersion, // 必传
event: 'complete',
remark: '处理完成'
});
```

View File

@@ -0,0 +1,175 @@
# 工单类型定义
## 类型列表
| 工单类型代码 | 类型名称 | 来源模块 | 可撤销 | 特殊处理 |
|-------------|---------|---------|-------|---------|
| `user_registration` | 用户注册工单 | rmdc-user-auth | 是 | 撤销时硬删除用户 |
| `user_management` | 用户管理工单 | rmdc-user-auth | 是 | 保存变更前后快照 |
| `project_detail` | 项目信息填写工单 | rmdc-project-management | 否(仅超管) | 支持草稿保存 |
| `microservice_update` | 微服务更新工单 | deliver-update | 是 | 执行/监控/回滚扩展状态 |
## 工单 ID 生成规则
**格式**`{module_prefix}-{timestamp}-{creator_username}`
**示例**
- `project-20260106110303-zeaslity`
- `user-20260108093000-admin`
- `microservice-20260108150000-zhangsan`
## 各类型详细说明
### user_registration用户注册工单
**业务场景**:非超级管理员用户注册新用户后,需要经过超级管理员审批才能激活用户账号。
**业务规则**
| 规则项 | 说明 |
|-------|------|
| 发起人 | 管理员、普通用户 |
| 默认处理人 | 超级管理员 |
| 被注册用户初始状态 | `disabled` |
| 审批通过后用户状态 | `active` |
| 可撤销 | 是(创建人可撤销) |
| 撤销后处理 | **硬删除**被注册用户 |
**步骤定义**
| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 |
|---------|---------|---------|-------|
| 1 | 提交注册 | `submit` | 系统自动 |
| 2 | 审核注册 | `approve` | 超级管理员 |
**扩展字段**user_registration_ext
- `target_user_id` - 被注册用户ID
- `original_status` - 原用户状态(备份)
---
### user_management用户管理工单
**业务场景**:非超级管理员用户对已注册用户进行管理操作(修改、删除、启用、禁用)时,需要经过超级管理员审批。
**业务规则**
| 规则项 | 说明 |
|-------|------|
| 发起人 | 管理员、普通用户(只能管理自己注册的用户) |
| 默认处理人 | 超级管理员 |
| 操作类型 | `modify`/`delete`/`enable`/`disable` |
| 可撤销 | 是 |
| 可关闭 | 是(创建人可主动关闭) |
**权限矩阵**
| 操作人角色 | 可管理用户范围 |
|-----------|--------------|
| 超级管理员 | 所有用户(无需工单) |
| 管理员 | 普通用户、三方用户(自己注册的) |
| 普通用户 | 三方用户(自己注册的) |
| 三方用户 | 无 |
**步骤定义**
| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 |
|---------|---------|---------|-------|
| 1 | 提交管理操作 | `submit` | 创建人 |
| 2 | 审核管理操作 | `approve` | 超级管理员 |
**扩展字段**user_management_ext
- `target_user_id` - 被管理用户ID
- `action_type` - 操作类型
- `original_data` - 变更前数据快照
- `modified_data` - 期望变更数据
---
### project_detail项目信息填写工单
**业务场景**:超级管理员创建项目元数据后,分配给普通用户填写项目详情信息,填写完成后提交审核。
**业务规则**
| 规则项 | 说明 |
|-------|------|
| 发起人 | 超级管理员 |
| 填写人 | 被分配的普通用户 |
| 审批人 | 超级管理员 |
| 支持草稿 | 是(可多次保存草稿) |
| 可撤销 | 否(仅超级管理员可取消) |
| 项目状态关联 | 草稿状态绑定唯一工单 |
**项目状态对应关系**
| 工单状态 | 项目认证状态 |
|---------|------------|
| `assigned` / `in_progress` | `draft` (草稿) |
| `pending_review` | `pending` (待审核) |
| `approved` / `closed` | `official` (正式) |
| `returned` | `draft` (草稿) |
**步骤定义**
| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 |
|---------|---------|---------|-------|
| 1 | 分配填写人 | `assign` | 超级管理员 |
| 2 | 填写项目信息 | `execute` | 被分配用户 |
| 3 | 审核项目信息 | `approve` | 超级管理员 |
**扩展字段**project_detail_ext
- `project_id` - 关联项目ID
- `detail_filler_id` - 填写人ID
- `detail_filler_name` - 填写人姓名
- `draft_data` - 草稿数据
---
### microservice_update微服务更新工单
**业务场景**:普通用户在构建详情页面发起微服务更新请求,经过超级管理员审批后执行更新操作,并监控微服务运行状态。
**业务规则**
| 规则项 | 说明 |
|-------|------|
| 发起人 | 普通用户 |
| 审批人 | 超级管理员 |
| 执行人 | 超级管理员或委派人 |
| 支持定时执行 | 是 |
| 支持委派 | 是 |
| 自动回滚 | 是(更新失败时自动回滚) |
**相关模块**
| 模块 | 职责 |
|-----|------|
| rmdc-jenkins-branch-dac | 提供构建物信息 |
| rmdc-exchange-hub | MQTT消息中继 |
| rmdc-watchdog | K8S操作执行、状态监控 |
**步骤定义**
| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 |
|---------|---------|---------|-------|
| 1 | 提交更新请求 | `submit` | 普通用户 |
| 2 | 审核更新请求 | `approve` | 超级管理员 |
| 3 | 执行更新 | `execute` | 超级管理员/委派人 |
| 4 | 监控运行状态 | `monitor` | 系统自动 |
**扩展字段**microservice_update_ext
- `project_id` - 目标项目ID
- `namespace` - K8S命名空间
- `service_name` - 微服务名称
- `build_id` - 构建物ID
- `current_version` - 当前版本
- `target_version` - 目标版本
- `scheduled_at` - 计划执行时间
- `execute_result` - 执行结果
- `rollback_info` - 回滚信息
**更新页面布局**
```
┌─────────────────────────────────────────────────────────────────┐
│ 微服务更新 │
├─────────────────┬─────────────────────────┬────────────────────┤
│ 构建物信息 │ 更新表单 │ 操作区 │
│ │ │ │
│ 组织: acme │ 目标项目: [下拉选择] │ │
│ 仓库: service │ 目标微服务: [下拉选择] │ [发起更新] │
│ 分支: main │ 更新时间: [日期选择] │ │
│ 构建号: #123 │ ○ 立即执行 │ [取消] │
│ 版本: v3.2.0 │ ○ 定时执行 │ │
│ │ 备注: [文本框] │ │
└─────────────────┴─────────────────────────┴────────────────────┘
```

View File

@@ -0,0 +1,143 @@
#!/bin/bash
# 验证 API 契约一致性
# 依赖: curl, jq
# 用法: ./verify-api-contracts.sh [API_BASE_URL]
set -e
API_BASE="${1:-http://localhost:8080}"
echo "=== 验证 API 契约 ==="
echo "API 地址: $API_BASE"
echo ""
# 检查依赖
command -v curl >/dev/null 2>&1 || { echo "[ERROR] 需要安装 curl"; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "[ERROR] 需要安装 jq"; exit 1; }
FAILED=0
PASSED=0
# 必需的接口列表
ENDPOINTS=(
"/api/workflow/create"
"/api/workflow/detail"
"/api/workflow/status"
"/api/workflow/transition"
"/api/workflow/assign"
"/api/workflow/delegate"
"/api/workflow/revoke"
"/api/workflow/callback"
"/api/workflow/list"
"/api/workflow/my/created"
"/api/workflow/my/assigned"
"/api/workflow/my/pending"
"/api/workflow/history"
"/api/workflow/diagram"
)
echo "--- 检查接口可访问性 ---"
for endpoint in "${ENDPOINTS[@]}"; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$API_BASE$endpoint" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-token" \
-d '{}' 2>/dev/null || echo "000")
if [ "$HTTP_CODE" == "000" ]; then
echo "[FAIL] 接口 $endpoint 无法连接"
((FAILED++))
elif [ "$HTTP_CODE" == "404" ]; then
echo "[FAIL] 接口 $endpoint 不存在 (404)"
((FAILED++))
elif [ "$HTTP_CODE" == "401" ] || [ "$HTTP_CODE" == "403" ]; then
echo "[PASS] 接口 $endpoint 存在 (需要认证: $HTTP_CODE)"
((PASSED++))
else
echo "[PASS] 接口 $endpoint 可访问 (HTTP $HTTP_CODE)"
((PASSED++))
fi
done
echo ""
echo "--- 检查响应格式 ---"
# 测试 /api/workflow/list 接口响应格式
RESP=$(curl -s -X POST "$API_BASE/api/workflow/list" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-token" \
-d '{"page": 1, "page_size": 1}' 2>/dev/null || echo '{}')
# 检查 code 字段
if echo "$RESP" | jq -e '.code' > /dev/null 2>&1; then
CODE=$(echo "$RESP" | jq '.code')
echo "[PASS] 响应包含 code 字段 (值: $CODE)"
((PASSED++))
else
echo "[FAIL] 响应缺少 code 字段"
((FAILED++))
fi
# 检查 message 字段
if echo "$RESP" | jq -e '.message' > /dev/null 2>&1; then
echo "[PASS] 响应包含 message 字段"
((PASSED++))
else
echo "[FAIL] 响应缺少 message 字段"
((FAILED++))
fi
# 检查 data 字段
if echo "$RESP" | jq -e '.data' > /dev/null 2>&1; then
echo "[PASS] 响应包含 data 字段"
((PASSED++))
else
echo "[WARN] 响应可能缺少 data 字段(空响应或错误)"
fi
echo ""
echo "--- 检查错误码规范 ---"
# 测试 404 错误码
RESP_404=$(curl -s -X POST "$API_BASE/api/workflow/detail" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-token" \
-d '{"workflow_id": "non-existent-id"}' 2>/dev/null || echo '{}')
if echo "$RESP_404" | jq -e '.code' > /dev/null 2>&1; then
ERROR_CODE=$(echo "$RESP_404" | jq '.code')
if [ "$ERROR_CODE" == "40401" ] || [ "$ERROR_CODE" == "404" ]; then
echo "[PASS] 404 错误码符合规范 (code: $ERROR_CODE)"
((PASSED++))
else
echo "[INFO] 404 错误码: $ERROR_CODE"
fi
fi
echo ""
echo "--- 检查版本冲突处理 ---"
# 测试 409 版本冲突
RESP_409=$(curl -s -X POST "$API_BASE/api/workflow/transition" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-token" \
-d '{"workflow_id": "test", "version": -1, "event": "approve"}' 2>/dev/null || echo '{}')
if echo "$RESP_409" | jq -e '.code' > /dev/null 2>&1; then
ERROR_CODE=$(echo "$RESP_409" | jq '.code')
echo "[INFO] 版本冲突响应码: $ERROR_CODE"
fi
echo ""
echo "=== 验证结果 ==="
echo "通过: $PASSED"
echo "失败: $FAILED"
if [ $FAILED -gt 0 ]; then
echo ""
echo "部分 API 契约验证失败,请检查接口实现"
exit 1
else
echo ""
echo "API 契约验证通过!"
exit 0
fi

View File

@@ -0,0 +1,134 @@
#!/bin/bash
# 验证数据库 Schema 完整性
# 依赖: psql (PostgreSQL) 或 mysql client
# 用法: ./verify-schema.sh
set -e
DB_TYPE="${DB_TYPE:-postgres}"
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-rmdc}"
DB_USER="${DB_USER:-rmdc}"
DB_PASSWORD="${DB_PASSWORD:-}"
echo "=== 验证工单模块 Schema ==="
echo "数据库: $DB_TYPE://$DB_HOST:$DB_PORT/$DB_NAME"
echo ""
# 必需的表
REQUIRED_TABLES=(
"workflows"
"workflow_steps"
"workflow_track_history"
"user_registration_ext"
"user_management_ext"
"project_detail_ext"
"microservice_update_ext"
)
# workflows 表必需的列
WORKFLOWS_COLUMNS=(
"id"
"version"
"module_code"
"workflow_type"
"status"
"creator_id"
"assignee_id"
"created_at"
"updated_at"
)
FAILED=0
PASSED=0
# 检查表是否存在
check_table_exists() {
local table=$1
if [ "$DB_TYPE" == "postgres" ]; then
result=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c \
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = '$table')" 2>/dev/null | tr -d ' ')
[ "$result" == "t" ] && return 0 || return 1
else
result=$(mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" -D "$DB_NAME" -N -e \
"SELECT COUNT(*) FROM information_schema.tables WHERE table_name = '$table'" 2>/dev/null)
[ "$result" -gt 0 ] && return 0 || return 1
fi
}
# 检查列是否存在
check_column_exists() {
local table=$1
local column=$2
if [ "$DB_TYPE" == "postgres" ]; then
result=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c \
"SELECT EXISTS (SELECT FROM information_schema.columns WHERE table_name = '$table' AND column_name = '$column')" 2>/dev/null | tr -d ' ')
[ "$result" == "t" ] && return 0 || return 1
else
result=$(mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" -D "$DB_NAME" -N -e \
"SELECT COUNT(*) FROM information_schema.columns WHERE table_name = '$table' AND column_name = '$column'" 2>/dev/null)
[ "$result" -gt 0 ] && return 0 || return 1
fi
}
echo "--- 检查表是否存在 ---"
for table in "${REQUIRED_TABLES[@]}"; do
if check_table_exists "$table"; then
echo "[PASS] 表 $table 存在"
((PASSED++))
else
echo "[FAIL] 表 $table 不存在"
((FAILED++))
fi
done
echo ""
echo "--- 检查 workflows 表核心字段 ---"
for column in "${WORKFLOWS_COLUMNS[@]}"; do
if check_column_exists "workflows" "$column"; then
echo "[PASS] workflows.$column 字段存在"
((PASSED++))
else
echo "[FAIL] workflows.$column 字段缺失"
((FAILED++))
fi
done
echo ""
echo "--- 检查乐观锁字段 ---"
if check_column_exists "workflows" "version"; then
echo "[PASS] workflows.version 字段存在(乐观锁)"
((PASSED++))
else
echo "[FAIL] workflows.version 字段缺失(乐观锁必需)"
((FAILED++))
fi
echo ""
echo "--- 检查外键约束 ---"
EXT_TABLES=("user_registration_ext" "user_management_ext" "project_detail_ext" "microservice_update_ext")
for table in "${EXT_TABLES[@]}"; do
if check_column_exists "$table" "workflow_id"; then
echo "[PASS] $table.workflow_id 字段存在"
((PASSED++))
else
echo "[FAIL] $table.workflow_id 字段缺失"
((FAILED++))
fi
done
echo ""
echo "=== 验证结果 ==="
echo "通过: $PASSED"
echo "失败: $FAILED"
if [ $FAILED -gt 0 ]; then
echo ""
echo "请运行数据库迁移修复缺失的表/字段"
exit 1
else
echo ""
echo "所有 Schema 检查通过!"
exit 0
fi

View File

@@ -0,0 +1,156 @@
#!/bin/bash
# 验证状态机定义完整性
# 依赖: go (用于解析 Go 源码)
# 用法: ./verify-state-machine.sh [项目根目录]
set -e
PROJECT_ROOT="${1:-.}"
echo "=== 验证状态机定义 ==="
echo "项目目录: $PROJECT_ROOT"
echo ""
# 基础状态列表
BASE_STATES=(
"created"
"pending"
"assigned"
"in_progress"
"pending_review"
"returned"
"approved"
"rejected"
"revoked"
"closed"
)
# 扩展状态列表
EXTENDED_STATES=(
"executing"
"monitoring"
"rollbacked"
"draft_saved"
)
# 终态列表
TERMINAL_STATES=(
"approved"
"rejected"
"revoked"
"closed"
)
# 事件列表
EVENTS=(
"submit"
"auto_assign"
"assign"
"accept"
"reassign"
"complete"
"approve"
"reject"
"return"
"resubmit"
"revoke"
"close"
)
FAILED=0
PASSED=0
WARNINGS=0
echo "--- 检查基础状态定义 ---"
for state in "${BASE_STATES[@]}"; do
if grep -rq "\"$state\"" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then
echo "[PASS] 状态 '$state' 已定义"
((PASSED++))
else
echo "[FAIL] 状态 '$state' 未定义"
((FAILED++))
fi
done
echo ""
echo "--- 检查扩展状态定义 ---"
for state in "${EXTENDED_STATES[@]}"; do
if grep -rq "\"$state\"" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then
echo "[PASS] 扩展状态 '$state' 已定义"
((PASSED++))
else
echo "[WARN] 扩展状态 '$state' 未定义(可选)"
((WARNINGS++))
fi
done
echo ""
echo "--- 检查事件定义 ---"
for event in "${EVENTS[@]}"; do
if grep -rq "\"$event\"" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then
echo "[PASS] 事件 '$event' 已定义"
((PASSED++))
else
echo "[FAIL] 事件 '$event' 未定义"
((FAILED++))
fi
done
echo ""
echo "--- 检查终态保护 ---"
for state in "${TERMINAL_STATES[@]}"; do
# 检查是否有 TerminalStates 或 IsTerminal 相关定义
if grep -rq "Terminal.*$state\|IsTerminal\|终态" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then
echo "[PASS] 终态 '$state' 有保护机制"
((PASSED++))
else
echo "[WARN] 终态 '$state' 可能缺少保护机制"
((WARNINGS++))
fi
done
echo ""
echo "--- 检查撤销权限 ---"
if grep -rq "CanRevoke\|revoke.*Terminal\|IsTerminal" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then
echo "[PASS] 撤销权限检查已实现"
((PASSED++))
else
echo "[WARN] 未找到撤销权限检查逻辑"
((WARNINGS++))
fi
echo ""
echo "--- 检查乐观锁实现 ---"
if grep -rq "version.*=.*version.*\+.*1\|version.*Expr\|RowsAffected.*==.*0" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then
echo "[PASS] 乐观锁机制已实现"
((PASSED++))
else
echo "[FAIL] 未找到乐观锁实现"
((FAILED++))
fi
echo ""
echo "--- 检查状态历史记录 ---"
if grep -rq "WorkflowTrackHistory\|workflow_track_history\|TrackHistory" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then
echo "[PASS] 状态历史记录已实现"
((PASSED++))
else
echo "[FAIL] 未找到状态历史记录实现"
((FAILED++))
fi
echo ""
echo "=== 验证结果 ==="
echo "通过: $PASSED"
echo "警告: $WARNINGS"
echo "失败: $FAILED"
if [ $FAILED -gt 0 ]; then
echo ""
echo "请检查并修复失败项"
exit 1
else
echo ""
echo "状态机验证通过!"
exit 0
fi

View File

@@ -0,0 +1,146 @@
---
name: implementing-deadman-switch
description: Guides implementation of deadman switch (dead hand system) and heartbeat mechanism in watchdog-agent for authorization enforcement. Use when modifying heartbeat intervals, failure thresholds, or business process termination logic. Keywords: deadman, heartbeat, agent, authorization, sigterm, fail-count, self-destruct.
argument-hint: "<component>: agent-heartbeat | fail-threshold | kill-logic | interval-config"
allowed-tools:
- Read
- Glob
- Grep
- Bash
- Edit
- Write
---
# Implementing Deadman Switch
watchdog-agent 内置死手系统,当连续授权失败达到阈值时终止业务进程。
## 动态上下文注入
```bash
# 查找Agent心跳实现
!`grep -rn "heartbeat\|Heartbeat" rmdc-watchdog-agent/`
# 查找kill逻辑
!`grep -n "SIGTERM\|Kill\|Signal" rmdc-watchdog-agent/`
```
## Plan
根据 `$ARGUMENTS` 确定修改范围:
| Component | 涉及文件 | 关键参数 |
|-----------|----------|----------|
| agent-heartbeat | agent心跳模块 | HeartbeatRequest/Response |
| fail-threshold | 失败计数逻辑 | maxRetryCount=12 |
| kill-logic | 进程终止逻辑 | SIGTERM信号 |
| interval-config | 心跳间隔配置 | 成功2h/失败1h |
**产物清单**
- Agent心跳循环实现
- 失败计数与阈值判断
- 业务进程终止逻辑
## Verify
- [ ] 失败阈值maxRetryCount = 12
- [ ] 心跳间隔成功后2小时失败后1小时
- [ ] TOTP验证首次连接获取密钥后续请求双向验证
- [ ] 终止信号使用SIGTERM优雅终止非SIGKILL
- [ ] 计数重置:授权成功后 failCount = 1非0
- [ ] 时间戳校验:|now - timestamp| < 5分钟
```bash
# 验证Agent编译
!`cd rmdc-watchdog-agent && go build ./...`
# 验证心跳逻辑
!`cd rmdc-watchdog-agent && go test ./... -v -run TestHeartbeat`
```
## Execute
### 心跳循环实现
```go
func (a *Agent) heartbeatLoop() {
failCount := 0
for {
resp, err := a.sendHeartbeat()
if err != nil || !resp.Authorized {
failCount++
if failCount >= 12 {
a.killBusiness()
return
}
time.Sleep(1 * time.Hour) // 失败后等待1小时
} else {
failCount = 1 // 成功后重置为1
time.Sleep(2 * time.Hour) // 成功后等待2小时
}
}
}
```
### 业务终止实现
```go
func (a *Agent) killBusiness() {
log.Warn("deadman switch triggered, terminating business process")
a.businessProcess.Signal(syscall.SIGTERM)
}
```
### 首次连接处理
```go
func (a *Agent) sendHeartbeat() (*HeartbeatResponse, error) {
req := &HeartbeatRequest{
HostInfo: a.hostInfo,
EnvInfo: a.envInfo,
Timestamp: time.Now().UnixMilli(),
TOTPCode: "", // 首次为空
}
// 非首次连接生成TOTP
if a.tierTwoSecret != "" {
req.TOTPCode = totp.GenerateTierTwo(a.tierTwoSecret)
}
resp, err := a.httpClient.Post(a.watchdogURL+"/api/heartbeat", req)
if err != nil {
return nil, err
}
// 首次连接,保存密钥
if resp.SecondTOTPSecret != "" {
a.tierTwoSecret = resp.SecondTOTPSecret
}
// 验证服务端TOTP双向验证
if req.TOTPCode != "" && !totp.VerifyTierTwo(resp.TOTPCode, a.tierTwoSecret) {
return nil, errors.New("invalid server totp")
}
return resp, nil
}
```
## Pitfalls
1. **failCount初始值**成功后设为1而非0避免边界条件错误
2. **SIGKILL误用**应使用SIGTERM允许业务优雅退出
3. **心跳阻塞**sendHeartbeat需设置超时避免网络问题导致卡死
4. **双向验证遗漏**必须验证服务端返回的TOTP
5. **首次连接特殊处理**TOTPCode为空时获取密钥不计入失败
6. **间隔配置硬编码**应支持配置化便于不同项目调整
7. **日志泄露**禁止在日志中打印TOTP密钥
## Reference
- [心跳参数配置](reference/heartbeat-params.md)
- [Agent生命周期](reference/agent-lifecycle.md)

View File

@@ -0,0 +1,92 @@
# Agent 生命周期
## 启动流程
```
1. Agent作为Sidecar随业务Pod启动
2. 收集主机信息(MachineID, CPU, Memory, Serial, IP)
3. 收集环境信息(K8S_NAMESPACE, APPLICATION_NAME)
4. 首次心跳请求(TOTPCode="")
5. 获取并保存tier_two_secret
6. 进入心跳循环
```
## 心跳状态机
```
初始化 -> 首次心跳 -> 获取密钥 -> 心跳循环
成功: failCount=1, 等待2h
失败: failCount++, 等待1h
failCount >= 12
触发死手系统 -> 终止业务
```
## 心跳循环详情
### 成功路径
1. 生成Tier-Two TOTP (6位/30秒)
2. 发送心跳请求
3. Watchdog验证TOTP
4. Watchdog检查授权状态
5. 返回 {Authorized: true, TOTPCode: xxx}
6. Agent验证响应TOTP
7. failCount = 1
8. 等待2小时
### 失败路径
1. 发送心跳请求
2. 返回错误或 {Authorized: false}
3. failCount++
4. 检查 failCount >= 12
5. 未达阈值: 等待1小时继续循环
6. 达到阈值: 触发死手系统
## 死手系统触发
```go
func (a *Agent) killBusiness() {
log.Warn("deadman switch triggered after %d failures", failCount)
// 发送SIGTERM允许优雅退出
if err := a.businessProcess.Signal(syscall.SIGTERM); err != nil {
log.Error("failed to send SIGTERM: %v", err)
// 最后手段SIGKILL
a.businessProcess.Signal(syscall.SIGKILL)
}
}
```
## 与Watchdog交互
### 请求结构
```go
type HeartbeatRequest struct {
HostInfo HostInfo `json:"host_info"`
EnvInfo EnvInfo `json:"env_info"`
Timestamp int64 `json:"timestamp"`
TOTPCode string `json:"totp_code"` // 首次为空
}
```
### 响应结构
```go
type HeartbeatResponse struct {
Authorized bool `json:"authorized"`
TOTPCode string `json:"totp_code"` // 双向验证
Timestamp int64 `json:"timestamp"`
SecondTOTPSecret string `json:"second_totp_secret"` // 首次返回
}
```
## 异常处理
| 场景 | 处理方式 |
|------|----------|
| 网络超时 | 计入失败,继续循环 |
| Watchdog不可达 | 计入失败,继续循环 |
| TOTP验证失败 | 计入失败,继续循环 |
| 响应TOTP无效 | 计入失败,可能中间人攻击 |
| 授权状态false | 计入失败,等待授权恢复 |

View File

@@ -0,0 +1,54 @@
# 心跳参数配置
## 核心参数
| 参数 | 值 | 说明 |
|------|------|------|
| maxRetryCount | 12 | 最大连续失败次数 |
| defaultHeartbeatInterval | 2小时 | 成功后心跳间隔 |
| failWaitInterval | 1小时 | 失败后等待间隔 |
| timestampTolerance | 5分钟 | 时间戳校验容忍度 |
## 触发条件
死手系统触发条件:**连续12次心跳失败**
### 时间计算
- 最短触发时间12次 × 1小时 = 12小时
- 实际触发时间取决于网络和服务状态
### 失败场景
1. 网络不可达
2. Watchdog服务异常
3. TOTP验证失败
4. 授权状态为false
5. 响应TOTP校验失败
## 终止方式
| 属性 | 值 |
|------|-----|
| 信号 | SIGTERM (15) |
| 目的 | 允许业务进程优雅退出 |
| 后续 | 由K8S重启策略决定是否重启 |
## 配置示例
```yaml
# agent配置
heartbeat:
watchdog_url: "http://rmdc-watchdog:8990"
max_retry_count: 12
success_interval: "2h"
fail_interval: "1h"
timestamp_tolerance: "5m"
```
## 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| WATCHDOG_URL | Watchdog地址 | http://rmdc-watchdog:8990 |
| MAX_RETRY_COUNT | 最大重试次数 | 12 |
| SUCCESS_INTERVAL | 成功后间隔 | 2h |
| FAIL_INTERVAL | 失败后间隔 | 1h |

View File

@@ -0,0 +1,100 @@
#!/bin/bash
# verify-deadman.sh - 死手系统实现验证脚本
# 依赖: go 1.21+
# 用法: ./verify-deadman.sh [agent_dir]
set -e
AGENT_DIR="${1:-./rmdc-watchdog-agent}"
echo "=== 死手系统实现验证 ==="
echo "目标目录: $AGENT_DIR"
echo ""
# 检查目录
if [ ! -d "$AGENT_DIR" ]; then
echo "⚠ 目录不存在: $AGENT_DIR"
echo "尝试查找watchdog-agent目录..."
AGENT_DIR=$(find . -type d -name "*watchdog*agent*" 2>/dev/null | head -1)
if [ -z "$AGENT_DIR" ]; then
echo "✗ 未找到agent目录"
exit 1
fi
echo "找到: $AGENT_DIR"
fi
# 1. 验证心跳实现
echo ""
echo "[1/5] 验证心跳实现..."
if grep -rq "heartbeat\|Heartbeat" "$AGENT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到心跳相关代码"
HEARTBEAT_FILES=$(grep -rl "heartbeat\|Heartbeat" "$AGENT_DIR" --include="*.go" 2>/dev/null | head -3)
echo " 文件: $HEARTBEAT_FILES"
else
echo "⚠ 未找到心跳相关代码"
fi
# 2. 验证失败阈值
echo ""
echo "[2/5] 验证失败阈值 (maxRetryCount=12)..."
if grep -rq "12" "$AGENT_DIR" --include="*.go" 2>/dev/null | grep -qi "retry\|count\|max\|fail"; then
echo "✓ 找到阈值配置"
else
echo "⚠ 未明确找到阈值12的配置"
fi
# 3. 验证SIGTERM信号
echo ""
echo "[3/5] 验证终止信号 (SIGTERM)..."
if grep -rq "SIGTERM\|syscall.Signal" "$AGENT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到SIGTERM信号使用"
else
echo "⚠ 未找到SIGTERM信号"
fi
if grep -rq "SIGKILL" "$AGENT_DIR" --include="*.go" 2>/dev/null; then
echo "⚠ 发现SIGKILL使用请确认仅作为最后手段"
fi
# 4. 验证心跳间隔
echo ""
echo "[4/5] 验证心跳间隔..."
if grep -rq "2.*[Hh]our\|time.Hour.*2\|7200" "$AGENT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到2小时间隔配置"
else
echo "⚠ 未找到2小时成功间隔"
fi
if grep -rq "1.*[Hh]our\|time.Hour\|3600" "$AGENT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到1小时间隔配置"
else
echo "⚠ 未找到1小时失败间隔"
fi
# 5. 验证TOTP双向验证
echo ""
echo "[5/5] 验证TOTP双向验证..."
if grep -rq "SecondTOTPSecret\|second_totp_secret" "$AGENT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到密钥获取逻辑"
else
echo "⚠ 未找到密钥获取逻辑"
fi
if grep -rq "response.*[Tt]otp\|verify.*[Tt]otp" "$AGENT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到响应TOTP验证"
else
echo "⚠ 未找到响应TOTP验证请确认双向验证"
fi
# 编译检查
echo ""
echo "[额外] 编译检查..."
cd "$AGENT_DIR"
if go build ./... 2>&1; then
echo "✓ 编译通过"
else
echo "⚠ 编译有问题"
fi
echo ""
echo "=== 验证完成 ==="

View File

@@ -0,0 +1,112 @@
---
name: implementing-k8s-ops
description: Guides implementation of K8S operation proxy in rmdc-watchdog for executing Kubernetes API calls including logs, exec, scale, restart, delete, get, and apply actions. Use when adding new K8S actions or modifying execution logic. Keywords: kubernetes, k8s, operator, logs, exec, scale, restart, deployment, pod.
argument-hint: "<action>: logs | exec | scale | restart | delete | get | apply | new-action"
allowed-tools:
- Read
- Glob
- Grep
- Bash
- Edit
- Write
---
# Implementing K8S Operations
rmdc-watchdog 作为K8S操作代理执行来自 exchange-hub 下发的K8S指令。
## 动态上下文注入
```bash
# 查看K8S客户端实现
!`cat rmdc-watchdog/pkg/k8s/client.go`
# 查找现有action处理
!`grep -n "case \"" rmdc-watchdog/internal/service/k8s_service.go`
```
## Plan
根据 `$ARGUMENTS` 确定操作类型:
| Action | 目标资源 | 关键参数 |
|--------|----------|----------|
| logs | Pod | container, tail_lines, follow |
| exec | Pod | container, command[], timeout |
| scale | Deployment/StatefulSet | scale_count |
| restart | Deployment/StatefulSet | - |
| delete | 任意资源 | - |
| get | 任意资源 | output_format |
| apply | 任意资源 | yaml_content |
**产物清单**
- `pkg/k8s/client.go` - K8S API调用封装
- `internal/service/k8s_service.go` - K8S服务逻辑
- `internal/handler/k8s_handler.go` - K8S请求处理
**决策点**
1. 新action是否需要额外参数→ 更新 K8sExecCommand 结构
2. 是否涉及敏感操作?→ 需添加审计日志
3. 是否需要超时控制?→ 使用 context.WithTimeout
## Verify
- [ ] 操作白名单:仅允许 logs/exec/scale/restart/delete/get/apply
- [ ] 超时处理所有K8S API调用必须设置timeout
- [ ] 结果格式ExecResult包含command_id, status, exit_code, output, error, duration
- [ ] 日志截断tail_lines限制避免大日志阻塞
- [ ] 权限边界仅操作项目namespace内资源
- [ ] 执行上报结果通过MQTT `wdd/RDMC/message/up` 上报
```bash
# 验证K8S客户端
!`cd rmdc-watchdog && go test ./pkg/k8s/... -v`
# 验证K8S服务
!`cd rmdc-watchdog && go test ./internal/service/... -v -run TestK8s`
```
## Execute
### 添加新K8S操作
1. **扩展K8S Client**
```go
// pkg/k8s/client.go
func (c *Client) NewAction(namespace, name string, params Params) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Second)
defer cancel()
// K8S API调用
}
```
2. **添加Service分支**
```go
// internal/service/k8s_service.go
case "new-action":
output, err = s.k8sClient.NewAction(cmd.Namespace, cmd.Name, params)
```
3. **更新指令结构(如需)**
```go
type K8sExecCommand struct {
// 新增字段
NewParam string `json:"new_param,omitempty"`
}
```
4. **同步exchange-hub指令定义**
## Pitfalls
1. **Namespace逃逸**必须校验操作仅限项目namespace
2. **超时未设置**K8S API调用卡住会阻塞整个handler
3. **大日志OOM**logs操作未设置tail_lines导致内存溢出
4. **exec命令注入**command[]需过滤危险命令
5. **follow日志未清理**流式日志需session管理用户停止时清理
6. **结果丢失**执行完成必须通过MQTT上报失败重试
## Reference
- [K8S操作类型](reference/k8s-actions.md)
- [指令结构定义](reference/command-structure.md)

View File

@@ -0,0 +1,74 @@
# K8S 指令结构定义
## K8sExecCommand - 执行指令
```go
type K8sExecCommand struct {
CommandID string `json:"command_id"` // 唯一指令ID
Namespace string `json:"namespace"` // 目标namespace
Resource string `json:"resource"` // 资源类型: deployment, pod, statefulset...
Name string `json:"name"` // 资源名称
Action string `json:"action"` // 操作: logs, exec, scale, restart, delete, get, apply
Container string `json:"container,omitempty"` // 容器名(logs/exec用)
Command []string `json:"command,omitempty"` // 命令(exec用)
Timeout int `json:"timeout"` // 超时秒数
TailLines int `json:"tail_lines,omitempty"` // 日志行数(logs用)
FollowLogs bool `json:"follow_logs,omitempty"`// 实时日志(logs用)
Scale int `json:"scale,omitempty"` // 副本数(scale用)
YamlContent string `json:"yaml_content,omitempty"`// YAML内容(apply用)
}
```
## ExecResult - 执行结果
```go
type ExecResult struct {
CommandID string `json:"command_id"` // 对应指令ID
Status string `json:"status"` // success, failure, timeout
ExitCode int `json:"exit_code"` // 退出码
Output string `json:"output"` // 标准输出
Error string `json:"error"` // 错误信息
StartTime int64 `json:"start_time"` // 开始时间戳(ms)
EndTime int64 `json:"end_time"` // 结束时间戳(ms)
Duration int64 `json:"duration"` // 执行时长(ms)
}
```
## MQTT消息封装
### 下发指令 (Exchange-Hub → Watchdog)
```json
{
"message_id": "uuid",
"type": "command",
"project_id": "project_12345678",
"timestamp": 1700000000000,
"data_type": "k8s_exec",
"payload": {
"command_id": "cmd_uuid",
"namespace": "my-namespace",
"resource": "pod",
"name": "my-pod",
"action": "logs",
"tail_lines": 100
}
}
```
### 上报结果 (Watchdog → Exchange-Hub)
```json
{
"message_id": "uuid",
"type": "message",
"project_id": "project_12345678",
"timestamp": 1700000001000,
"data_type": "exec_result",
"payload": {
"command_id": "cmd_uuid",
"status": "success",
"exit_code": 0,
"output": "log content...",
"duration": 1500
}
}
```

View File

@@ -0,0 +1,69 @@
# K8S 操作类型定义
## 支持的操作
| Action | 说明 | 目标资源 | 参数 |
|--------|------|----------|------|
| `logs` | 获取日志 | Pod | container, tail_lines, follow |
| `exec` | 执行命令 | Pod | container, command[], timeout |
| `scale` | 扩缩容 | Deployment/StatefulSet | scale_count |
| `restart` | 滚动重启 | Deployment/StatefulSet | - |
| `delete` | 删除资源 | Pod/Deployment/Service等 | - |
| `get` | 获取信息 | 任意资源 | output_format |
| `apply` | 应用配置 | 任意资源 | yaml_content |
## 操作详情
### logs - 获取日志
```go
type LogsParams struct {
Container string `json:"container"`
TailLines int `json:"tail_lines"` // 默认100最大1000
Follow bool `json:"follow"` // 是否实时跟踪
}
```
### exec - 执行命令
```go
type ExecParams struct {
Container string `json:"container"`
Command []string `json:"command"`
Timeout int `json:"timeout"` // 秒
}
```
**安全约束**:禁止执行 rm -rf、shutdown、reboot 等危险命令
### scale - 扩缩容
```go
type ScaleParams struct {
Replicas int `json:"replicas"` // 目标副本数
}
```
### restart - 滚动重启
无额外参数,使用 kubectl rollout restart 等效操作
### delete - 删除资源
无额外参数,需确认资源类型和名称
### get - 获取信息
```go
type GetParams struct {
OutputFormat string `json:"output_format"` // json, yaml, wide
}
```
### apply - 应用配置
```go
type ApplyParams struct {
YamlContent string `json:"yaml_content"` // YAML内容
}
```
## 安全约束
1. **Namespace限制**仅操作项目namespace内资源
2. **操作白名单**仅允许上述7种操作
3. **命令过滤**exec操作过滤危险命令
4. **日志限制**tail_lines最大1000行
5. **超时控制**所有操作必须设置timeout

View File

@@ -0,0 +1,81 @@
#!/bin/bash
# verify-k8s-ops.sh - K8S操作实现验证脚本
# 依赖: go 1.21+
# 用法: ./verify-k8s-ops.sh [watchdog_dir]
set -e
WATCHDOG_DIR="${1:-./rmdc-watchdog}"
echo "=== K8S 操作实现验证 ==="
echo "目标目录: $WATCHDOG_DIR"
echo ""
# 1. 检查K8S客户端文件
echo "[1/4] 检查K8S客户端..."
K8S_CLIENT="$WATCHDOG_DIR/pkg/k8s/client.go"
if [ -f "$K8S_CLIENT" ]; then
echo "✓ K8S客户端文件存在: $K8S_CLIENT"
else
echo "✗ K8S客户端文件不存在"
fi
# 2. 验证操作白名单
echo ""
echo "[2/4] 验证K8S操作白名单..."
K8S_SERVICE="$WATCHDOG_DIR/internal/service/k8s_service.go"
ALLOWED_ACTIONS="logs exec scale restart delete get apply"
if [ -f "$K8S_SERVICE" ]; then
echo "检查 $K8S_SERVICE:"
for action in $ALLOWED_ACTIONS; do
if grep -q "case \"$action\"" "$K8S_SERVICE" 2>/dev/null; then
echo " ✓ '$action' 已实现"
else
echo " ⚠ '$action' 未找到"
fi
done
else
echo "⚠ K8S服务文件不存在: $K8S_SERVICE"
fi
# 3. 验证超时处理
echo ""
echo "[3/4] 验证超时处理..."
if grep -rq "context.WithTimeout\|WithDeadline" "$WATCHDOG_DIR/pkg/k8s/" 2>/dev/null; then
echo "✓ 找到超时控制实现"
else
echo "⚠ 未找到超时控制请确认K8S调用设置了timeout"
fi
# 4. 验证结果结构
echo ""
echo "[4/4] 验证ExecResult结构..."
if grep -rq "ExecResult" "$WATCHDOG_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到ExecResult定义"
# 检查必要字段
REQUIRED_FIELDS="CommandID Status ExitCode Output Duration"
for field in $REQUIRED_FIELDS; do
if grep -rq "$field" "$WATCHDOG_DIR" --include="*.go" 2>/dev/null | grep -q "ExecResult\|exec_result"; then
echo " ✓ 字段 '$field' 存在"
else
echo " ⚠ 字段 '$field' 未找到"
fi
done
else
echo "⚠ 未找到ExecResult定义"
fi
# 5. 运行测试
echo ""
echo "[5/5] 运行K8S相关测试..."
cd "$WATCHDOG_DIR"
if go test ./pkg/k8s/... -v 2>&1 | head -20; then
echo "✓ K8S测试执行完成"
else
echo "⚠ K8S测试执行有问题"
fi
echo ""
echo "=== 验证完成 ==="

View File

@@ -0,0 +1,108 @@
---
name: implementing-totp-auth
description: Guides implementation of TOTP-based two-tier authorization mechanism for RMDC system. Use when implementing authorization flows, modifying TOTP parameters, or debugging auth failures between project-management, watchdog, agent, and node components. Keywords: totp, authorization, tier-one, tier-two, security, authentication, hmac.
argument-hint: "<scope>: tier-one | tier-two | dual-verify | auth-file"
allowed-tools:
- Read
- Glob
- Grep
- Bash
- Edit
- Write
---
# Implementing TOTP Authorization
RMDC采用双层TOTP授权机制保护项目环境安全。
## 动态上下文注入
```bash
# 查找TOTP实现
!`grep -rn "TOTP\|totp" rmdc-watchdog/pkg/totp/`
# 查找授权验证逻辑
!`grep -n "Verify.*TOTP\|Generate.*TOTP" rmdc-watchdog/internal/service/auth_service.go`
```
## Plan
根据 `$ARGUMENTS` 确定实现范围:
| Scope | 涉及组件 | 关键参数 |
|-------|----------|----------|
| tier-one | project-management ↔ watchdog | 8位/30分钟/SHA256 |
| tier-two | watchdog ↔ agent/node | 6位/30秒/SHA1 |
| dual-verify | 所有通信 | 请求TOTP + 响应TOTP |
| auth-file | watchdog授权申请/解析 | 加密主机信息 + TOTP + namespace |
**产物清单**
- `pkg/totp/totp.go` - TOTP生成与验证
- `internal/service/auth_service.go` - 授权服务
- `internal/model/entity/auth_info.go` - 授权信息实体
## Verify
- [ ] Tier-One参数8位码、30分钟有效、SHA256、AES-GCM加密
- [ ] Tier-Two参数6位码、30秒有效、SHA1
- [ ] 双向验证服务端响应必须包含TOTP供客户端校验
- [ ] 时间戳校验:|now - timestamp| < 5分钟可配置
- [ ] 密钥存储tier_one_secret/tier_two_secret不通过公网传输
- [ ] 授权文件包含EncryptedHostMap, TOTPCode, EncryptedNamespace
```bash
# 验证TOTP生成
!`cd rmdc-watchdog && go test ./pkg/totp/... -v -run TestGenerate`
# 验证授权流程
!`cd rmdc-watchdog && go test ./internal/service/... -v -run TestAuth`
```
## Execute
### 实现Tier-One TOTP
```go
// 8位30分钟有效SHA256
func GenerateTierOneTOTP(secret string) string {
return totp.Generate(secret, totp.Config{
Digits: 8,
Period: 1800, // 30分钟
Algorithm: crypto.SHA256,
})
}
```
### 实现Tier-Two TOTP
```go
// 6位30秒有效SHA1
func GenerateTierTwoTOTP(secret string) string {
return totp.Generate(secret, totp.Config{
Digits: 6,
Period: 30,
Algorithm: crypto.SHA1,
})
}
```
### 实现双向验证
1. 客户端生成TOTP并发送请求
2. 服务端验证客户端TOTP
3. 服务端生成新TOTP并返回
4. 客户端验证服务端TOTP
## Pitfalls
1. **算法混淆**Tier-One用SHA256Tier-Two用SHA1不可互换
2. **有效期错配**30分钟 vs 30秒单位差异大
3. **时间同步问题**需配置 `time_offset_allowed` 容忍时钟偏差
4. **单向验证漏洞**缺少响应TOTP会导致中间人攻击风险
5. **密钥泄露**禁止在日志/错误信息中打印secret
6. **首次连接处理**TOTPCode为空时需返回密钥后续请求必须验证
## Reference
- [TOTP层级说明](reference/totp-tiers.md)
- [授权流程图](reference/auth-flow.md)

View File

@@ -0,0 +1,72 @@
# 授权流程图
## 项目注册流程
```
1. 管理员在RMDC Portal创建项目
2. project-management生成:
- project_id = namespace_<8位随机数>
- tier_one_secret
- tier_two_secret
- time_offset_allowed
3. 返回项目配置文件给管理员
4. 管理员部署watchdog时使用配置文件(密钥不经公网)
```
## Watchdog注册流程
```
1. Watchdog连接MQTT Broker
2. 订阅: wdd/RDMC/command/down/<project_id>
3. 订阅: wdd/RDMC/message/down/<project_id>
4. 生成Tier-One TOTP验证码(8位,30分钟)
5. 发布注册Command到 wdd/RDMC/command/up
6. Exchange-Hub验证TOTP
7. 生成32位随机挑战码
8. 返回register_ack + 服务端TOTP
9. Watchdog验证服务端TOTP
10. 回复挑战码确认
11. 注册完成,状态变为Online
```
## 授权申请流程
```
1. Node上报主机信息 → Watchdog
2. Agent首次心跳 → Watchdog
3. Watchdog收集完成后生成授权文件:
- EncryptedHostMap
- TOTPCode(8位,30分钟)
- EncryptedNamespace
4. 发布auth_request到MQTT
5. Center验证TOTP和主机信息
6. 返回授权码(加密)
7. Watchdog解密并持久化
```
## Agent心跳流程
```
首次连接:
1. Agent收集主机信息
2. 发送心跳(TOTPCode="")
3. Watchdog返回SecondTOTPSecret
4. Agent保存密钥
后续心跳:
1. Agent生成Tier-Two TOTP(6位,30秒)
2. 发送心跳
3. Watchdog验证TOTP
4. 检查主机授权状态
5. 返回{Authorized, ResponseTOTP}
6. Agent验证ResponseTOTP
7. 根据结果更新failCount
```
## 死手系统触发
```
failCount >= 12 时:
1. Agent发送SIGTERM信号
2. 业务进程优雅终止
```

View File

@@ -0,0 +1,53 @@
# TOTP 层级定义
## Tier-One一级授权
| 属性 | 值 |
|------|-----|
| 通信方 | project-management ↔ watchdog |
| 位数 | 8位 |
| 有效期 | 30分钟 (1800秒) |
| 算法 | SHA256 |
| 加密 | AES-256-GCM |
| 用途 | 项目注册、授权申请/下发 |
## Tier-Two二级授权
| 属性 | 值 |
|------|-----|
| 通信方 | watchdog ↔ agent/node |
| 位数 | 6位 |
| 有效期 | 30秒 |
| 算法 | SHA1 |
| 加密 | 无(内网通信) |
| 用途 | 心跳验证、指令执行 |
## 双向验证流程
```
1. 客户端生成TOTP → 发送请求
2. 服务端验证客户端TOTP → 生成新TOTP返回
3. 客户端验证服务端TOTP → 确认双方身份
```
## 密钥管理
### 生成时机
- tier_one_secret: project-management创建项目时生成
- tier_two_secret: project-management创建项目时生成
### 传输方式
- 通过项目配置文件离线部署到Watchdog
- **禁止通过MQTT公网传输**
### 存储位置
- project-management: 数据库加密存储
- watchdog: 配置文件/环境变量
- agent/node: 首次连接时从watchdog获取tier_two_secret
## 时间容忍度
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| time_offset_allowed | 300秒 | 时间戳校验容忍度 |
| totp_window | 1 | TOTP验证窗口前后各1个周期 |

View File

@@ -0,0 +1,92 @@
#!/bin/bash
# verify-totp-impl.sh - TOTP实现验证脚本
# 依赖: go 1.21+
# 用法: ./verify-totp-impl.sh [project_dir]
set -e
PROJECT_DIR="${1:-.}"
echo "=== TOTP 实现验证 ==="
echo "项目目录: $PROJECT_DIR"
echo ""
# 1. 查找TOTP实现文件
echo "[1/4] 查找TOTP实现..."
TOTP_FILES=$(find "$PROJECT_DIR" -name "*.go" -exec grep -l "totp\|TOTP" {} \; 2>/dev/null || true)
if [ -n "$TOTP_FILES" ]; then
echo "找到TOTP相关文件:"
echo "$TOTP_FILES" | head -10
echo ""
else
echo "⚠ 未找到TOTP相关文件"
fi
# 2. 验证Tier-One参数
echo "[2/4] 验证Tier-One参数 (8位/30分钟/SHA256)..."
TIER_ONE_OK=true
if grep -rq "8" "$PROJECT_DIR" --include="*.go" 2>/dev/null | grep -qi "digit\|Digit"; then
echo "✓ 找到8位配置"
else
echo "⚠ 未明确找到8位配置"
TIER_ONE_OK=false
fi
if grep -rq "1800\|30.*[Mm]in" "$PROJECT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到30分钟有效期配置"
else
echo "⚠ 未找到30分钟有效期配置"
TIER_ONE_OK=false
fi
if grep -rq "SHA256\|sha256" "$PROJECT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到SHA256算法"
else
echo "⚠ 未找到SHA256算法"
TIER_ONE_OK=false
fi
# 3. 验证Tier-Two参数
echo ""
echo "[3/4] 验证Tier-Two参数 (6位/30秒/SHA1)..."
TIER_TWO_OK=true
if grep -rq "6" "$PROJECT_DIR" --include="*.go" 2>/dev/null | grep -qi "digit\|Digit"; then
echo "✓ 找到6位配置"
else
echo "⚠ 未明确找到6位配置"
TIER_TWO_OK=false
fi
if grep -rq "30" "$PROJECT_DIR" --include="*.go" 2>/dev/null | grep -qi "period\|second\|Period\|Second"; then
echo "✓ 找到30秒有效期配置"
else
echo "⚠ 未找到30秒有效期配置"
TIER_TWO_OK=false
fi
if grep -rq "SHA1\|sha1" "$PROJECT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到SHA1算法"
else
echo "⚠ 未找到SHA1算法"
TIER_TWO_OK=false
fi
# 4. 验证双向验证
echo ""
echo "[4/4] 验证双向验证实现..."
if grep -rq "response.*[Tt]otp\|[Tt]otp.*response" "$PROJECT_DIR" --include="*.go" 2>/dev/null; then
echo "✓ 找到响应TOTP相关代码"
else
echo "⚠ 未找到响应TOTP实现请确认双向验证"
fi
# 总结
echo ""
echo "=== 验证总结 ==="
if [ "$TIER_ONE_OK" = true ] && [ "$TIER_TWO_OK" = true ]; then
echo "✓ TOTP参数配置基本正确"
else
echo "⚠ 部分TOTP参数需要确认"
fi