8.7 KiB
8.7 KiB
项目管理模块 - HTTP回调设计方案(备选方案)
文档状态: 备选方案存档
当前实现: 接口注入方式(参见2-rmdc-project-management-DDS.md第3.4节)
适用场景: 未来微服务化部署时可考虑迁移到此方案
1. 概述
本文档描述项目与工单模块之间基于 HTTP 回调 的状态同步机制。当系统需要分布式部署时,可考虑从当前的"接口注入"方式迁移到此方案。
1.1 设计原则
项目模块(rmdc-project-management)调用工单模块(rmdc-work-procedure)创建和管理工单,工单状态变更需要同步更新项目的生命周期状态。采用 HTTP 回调机制 + 领域事件 的设计模式实现状态同步。
核心思想:工单状态变更时,由工单模块主动 HTTP 回调项目模块,项目模块根据事件类型更新自身的生命周期状态。
2. 架构设计
2.1 时序图
sequenceDiagram
participant PM as rmdc-project-management
participant WP as rmdc-work-procedure
participant DB as Database
Note over PM,WP: 回调机制:工单状态变更 → 回调业务模块 → 更新项目状态
PM->>WP: 1. 创建工单 (携带回调配置)
WP->>DB: 2. 存储工单 + 回调配置
WP-->>PM: 3. 返回 workflow_id
Note over WP: 工单状态变更事件发生...
WP->>WP: 4. 状态机转换 (pending_review → approved)
WP->>PM: 5. HTTP回调 /api/project/workflow-callback
PM->>PM: 6. 解析事件,更新项目 lifecycle_status
PM-->>WP: 7. 确认回调成功
WP->>DB: 8. 标记回调已完成
2.2 为什么不算循环依赖
虽然调用关系形成了"环",但这是 松耦合的事件通知模式,不是真正的循环依赖:
| 依赖类型 | 说明 | 是否存在 | 问题程度 |
|---|---|---|---|
| 编译依赖 | 模块A的代码import模块B | ❌ 不存在 | - |
| 启动依赖 | 模块A启动必须依赖模块B先启动 | ⚠️ 单向 | 低 |
| 运行时调用 | 模块A在运行时调用模块B的API | ✅ 双向 | 需设计 |
| 逻辑依赖 | 模块A的业务逻辑依赖模块B的存在 | ⚠️ 单向 | 低 |
关键点:工单模块不import项目模块任何代码,只知道一个回调URL字符串,两个模块在代码层面完全解耦。
3. 回调配置设计
3.1 创建工单时注册回调
// 项目模块创建工单时,注册回调配置
type CreateWorkflowRequest struct {
ModuleCode string `json:"module_code"` // "project_management"
WorkflowType string `json:"workflow_type"` // "project_detail" / "project_modify"
// ... 其他字段 ...
// 回调配置
CallbackConfig *CallbackConfig `json:"callback_config"`
}
type CallbackConfig struct {
// 回调URL(工单状态变更时调用)
CallbackURL string `json:"callback_url"` // "/api/project/workflow-callback"
// 关注的事件列表(只回调这些事件)
SubscribedEvents []string `json:"subscribed_events"` // ["submit", "approve", "return", "revoke"]
// 业务上下文(回调时原样返回)
BusinessContext map[string]interface{} `json:"business_context"` // {"project_id": "xxx"}
}
3.2 回调请求结构
// 工单模块发起的回调请求
type WorkflowCallbackRequest struct {
WorkflowID string `json:"workflow_id"`
WorkflowType string `json:"workflow_type"`
Event string `json:"event"` // "approve", "return", "submit" 等
FromStatus string `json:"from_status"`
ToStatus string `json:"to_status"`
OperatorID int64 `json:"operator_id"`
OperatorName string `json:"operator_name"`
BusinessContext map[string]interface{} `json:"business_context"`
Timestamp time.Time `json:"timestamp"`
}
4. 回调处理实现
4.1 项目模块接收回调
// 项目模块 - 接收工单回调并更新项目状态
func (h *ProjectHandler) WorkflowCallback(c *gin.Context) {
var req WorkflowCallbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, errcode.InvalidParams)
return
}
// 幂等性检查
idempotencyKey := fmt.Sprintf("%s:%s:%d", req.WorkflowID, req.Event, req.Timestamp.Unix())
if h.cache.Exists(idempotencyKey) {
response.Success(c, nil) // 幂等返回成功
return
}
projectID := req.BusinessContext["project_id"].(string)
// 根据工单事件映射项目生命周期状态
newLifecycleStatus := h.mapWorkflowEventToLifecycle(req.Event, req.ToStatus, req.WorkflowType)
if newLifecycleStatus != "" {
err := h.projectService.UpdateLifecycleStatus(c, projectID, newLifecycleStatus)
if err != nil {
response.Error(c, errcode.InternalError)
return
}
}
// 标记已处理(设置24小时过期)
h.cache.Set(idempotencyKey, "1", 24*time.Hour)
response.Success(c, nil)
}
4.2 事件到生命周期状态的映射
// 事件到生命周期状态的映射
func (h *ProjectHandler) mapWorkflowEventToLifecycle(event, toStatus, workflowType string) string {
// 填写工单
if workflowType == "project_detail" {
switch event {
case "create": // 创建工单
return "DRAFTING"
case "submit": // 提交审核
return "REVIEWING"
case "return": // 审核打回
return "DRAFTING"
case "approve": // 审核通过
return "RELEASED"
case "revoke": // 撤销工单
return "INIT"
}
}
// 修改工单
if workflowType == "project_modify" {
switch event {
case "create": // 发起修改
return "MODIFYING"
case "submit": // 提交审核
return "REVIEWING"
case "return": // 审核打回
return "MODIFYING"
case "approve": // 审核通过
return "RELEASED"
case "revoke": // 撤销工单
return "RELEASED" // 撤销后恢复为 RELEASED
}
}
return ""
}
5. 风险处理
| 风险 | 解决方案 |
|---|---|
| 回调再次调用工单API导致死循环 | 回调处理中只更新项目状态,不调用工单API |
| 回调失败导致状态不一致 | 工单模块实现重试机制(3次重试 + 指数退避) |
| 回调请求超时阻塞工单流程 | 设置短超时(5秒)或完全异步化 |
| 重复回调导致重复处理 | 使用 workflow_id + event + timestamp 作为幂等键 |
6. 回调失败重试表
// 工单模块 - 回调失败时存储待重试
type CallbackRetry struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
WorkflowID string `gorm:"type:varchar(64);index" json:"workflow_id"`
CallbackURL string `gorm:"type:varchar(256)" json:"callback_url"`
Payload string `gorm:"type:text" json:"payload"` // JSON
RetryCount int `gorm:"default:0" json:"retry_count"` // 已重试次数
MaxRetries int `gorm:"default:3" json:"max_retries"` // 最大重试次数
NextRetryAt time.Time `json:"next_retry_at"` // 下次重试时间
Status string `gorm:"type:varchar(16);default:'pending'" json:"status"` // pending/success/failed
LastError string `gorm:"type:text" json:"last_error"` // 最后一次错误信息
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
7. 与接口注入方式的对比
| 特性 | HTTP回调(本方案) | 接口注入(当前实现) |
|---|---|---|
| 模块解耦 | ✅ 完全解耦(仅URL字符串) | ⚠️ 接口级解耦 |
| 分布式支持 | ✅ 支持 | ❌ 不支持 |
| 性能 | ⚠️ 网络开销 | ✅ 进程内调用 |
| 复杂度 | ⚠️ 需要重试/幂等处理 | ✅ 简单直接 |
| 事务一致性 | ⚠️ 最终一致性 | ✅ 强一致性 |
| 适用场景 | 微服务架构 | 模块化单体架构 |
8. 迁移指南
如果未来需要从"接口注入"迁移到"HTTP回调",需要:
-
工单模块:
- 实现 HTTP 客户端发起回调
- 添加回调配置存储
- 实现重试机制和
callback_retries表
-
项目模块:
- 实现
/api/project/workflow-callback接口 - 添加幂等性检查(Redis/缓存)
- 删除
ProjectLifecycleUpdater接口注入逻辑
- 实现
-
rmdc-core:
- 删除
ProjectWorkflowCallback适配器 - 删除
workflowSvc.SetProjectLifecycleUpdater()调用
- 删除