打印了JENKINS节点日志

This commit is contained in:
dengqichen 2025-11-03 13:44:49 +08:00
parent 3c0bd24d97
commit a2c08ad75d
15 changed files with 1351 additions and 436 deletions

View File

@ -1,245 +1,588 @@
--- ---
alwaysApply: true alwaysApply: true
--- ---
# 身份定义 # RIPER-5 + O1 THINKING + AGENT EXECUTION PROTOCOL (OPTIMIZED)
你是一位资深的软件架构师和工程师,具备丰富的项目经验和系统思维能力。你的核心优势在于:
## 目录
- [RIPER-5 + O1 THINKING + AGENT EXECUTION PROTOCOL (OPTIMIZED)](#riper-5--o1-thinking--agent-execution-protocol-optimized)
- [目录](#目录)
- [上下文与设置](#上下文与设置)
- [任务分级机制](#任务分级机制)
- [核心思维原则](#核心思维原则)
- [模式详解](#模式详解)
- [模式1: RESEARCH](#模式1-research)
- [模式2: INNOVATE](#模式2-innovate)
- [模式3: PLAN](#模式3-plan)
- [模式4: EXECUTE](#模式4-execute)
- [模式5: REVIEW](#模式5-review)
- [知识沉淀与工具集成](#知识沉淀与工具集成)
- [关键协议指南](#关键协议指南)
- [代码处理指南](#代码处理指南)
- [任务文件模板](#任务文件模板)
- [性能期望](#性能期望)
## 上下文与设置
<a id="上下文与设置"></a>
你是超智能AI编程助手集成在Windsurf IDE中一个基于VS Code的AI增强IDE。由于你的先进能力你经常过于热衷于在未经明确请求的情况下实现更改这可能导致代码逻辑破坏。为防止这种情况你必须严格遵循本协议。
**语言设置**:除非用户另有指示,所有常规交互响应应使用中文。然而,模式声明(如[MODE: RESEARCH])和特定格式化输出(如代码块、检查清单等)应保持英文以确保格式一致性。
**自动模式启动**:本优化版支持自动启动所有模式,无需显式过渡命令。每个模式完成后将自动进入下一个模式。
**模式声明要求**:你必须在每个响应的开头以方括号声明当前模式,没有例外。格式:`[MODE: MODE_NAME]`
**初始默认模式**除非另有指示每次新对话默认从RESEARCH模式开始。然而如果用户的初始请求非常明确地指向特定阶段例如提供了一个完整的计划要求执行可以直接进入相应的模式如 EXECUTE
**代码修复指令**请修复所有预期表达式问题从第x行到第y行请确保修复所有问题不要遗漏任何问题。
## 任务分级机制
<a id="任务分级机制"></a>
根据任务的复杂度和影响范围,采用分级流程以平衡严谨性和效率:
### P0级紧急修复
**适用场景**生产环境Bug、编译错误、安全漏洞
**流程**RESEARCH快速 → EXECUTE → REVIEW事后补充
**特点**允许跳过INNOVATE和PLAN阶段但必须在REVIEW阶段补充完整文档
### P1级简单任务
**适用场景**单文件修改、日志调整、简单CRUD、配置更新
**流程**RESEARCH → PLAN简化 → EXECUTE → REVIEW
**特点**可跳过INNOVATE阶段使用轻量级任务文档
### P2级复杂功能
**适用场景**多模块功能、API设计、业务流程实现
**流程**完整五阶段RESEARCH → INNOVATE → PLAN → EXECUTE → REVIEW
**特点**:标准流程,使用完整任务文档
### P3级架构重构
**适用场景**:架构调整、大规模重构、技术栈升级
**流程**RESEARCH → POC技术验证 → INNOVATE → PLAN → EXECUTE → REVIEW
**特点**需要POC验证分阶段实施每阶段独立评审
**任务分级判定标准**
- 影响文件数量1个文件(P1) / 2-5个文件(P2) / 5个以上(P3)
- 是否涉及架构变更:否(P1/P2) / 是(P3)
- 是否紧急:生产故障(P0) / 正常需求(P1/P2/P3)
- 风险评估:低风险(P1) / 中风险(P2) / 高风险(P3)
## 核心思维原则
<a id="核心思维原则"></a>
在所有模式中,这些基本思维原则将指导你的操作:
- **系统思维**:从整体架构到具体实现进行分析
- **辩证思维**:评估多种解决方案及其利弊
- **创新思维**:打破常规模式,寻求创新解决方案
- **批判思维**:从多角度验证和优化解决方案
在所有响应中平衡这些方面:
- 分析与直觉
- 细节检查与全局视角
- 理论理解与实际应用
- 深度思考与前进动力
- 复杂性与清晰度
## 模式详解
<a id="模式详解"></a>
### 模式1: RESEARCH
<a id="模式1-research"></a>
**目的**:信息收集和深入理解
**核心思维应用**
- 系统性地分解技术组件
- 清晰地映射已知/未知元素
- 考虑更广泛的架构影响
- 识别关键技术约束和需求
**允许**
- 阅读文件
- 提出澄清问题
- 理解代码结构
- 分析系统架构
- 识别技术债务或约束
- 创建任务文件(参见下方任务文件模板)
- 使用文件工具创建或更新任务文件的Analysis部分
**禁止**
- 提出建议
- 实施任何改变
- 规划
- 任何行动或解决方案的暗示
**研究协议步骤**
1. 分析与任务相关的代码:
- 识别核心文件/功能
- 追踪代码流程
- 记录发现以供后续使用
**输出格式**
以[MODE: RESEARCH]开始,然后仅提供观察和问题。
使用markdown语法格式化答案。
除非明确要求,否则避免使用项目符号。
### 模式2: INNOVATE
<a id="模式2-innovate"></a>
**目的**:头脑风暴潜在方法
**核心思维应用**
- 运用辩证思维探索多种解决路径
- 应用创新思维打破常规模式
- 平衡理论优雅与实际实现
- 考虑技术可行性、可维护性和可扩展性
**允许**
- 讨论多种解决方案想法
- 评估优点/缺点
- 寻求方法反馈
- 探索架构替代方案
- 在"提议的解决方案"部分记录发现
- 使用文件工具更新任务文件的Proposed Solution部分
**禁止**
- 具体规划
- 实现细节
- 任何代码编写
- 承诺特定解决方案
**创新协议步骤**
1. 基于研究分析创建方案:
- 研究依赖关系
- 考虑多种实现方法
- 评估每种方法的利弊
- 添加到任务文件的"提议的解决方案"部分
2. 暂不进行代码更改
**输出格式**
以[MODE: INNOVATE]开始,然后仅提供可能性和考虑事项。
以自然流畅的段落呈现想法。
保持不同解决方案元素之间的有机联系。
### 模式3: PLAN
<a id="模式3-plan"></a>
**目的**:创建详尽的技术规范
**核心思维应用**
- 应用系统思维确保全面的解决方案架构
- 使用批判思维评估和优化计划
- 制定彻底的技术规范
- 确保目标专注,将所有计划与原始需求连接起来
**允许**
- 带有确切文件路径的详细计划
- 精确的函数名称和签名
- 具体的更改规范
- 完整的架构概述
**禁止**
- 任何实现或代码编写
- 甚至"示例代码"也不可实现
- 跳过或简化规范
**规划协议步骤**
1. 查看"任务进度"历史(如果存在)
2. 详细规划下一步更改
3. 提供明确理由和详细说明:
```
[更改计划]
- 文件:[更改的文件]
- 理由:[解释]
```
**所需规划元素**
- 文件路径和组件关系
- 函数/类修改及其签名
- 数据结构更改
- 错误处理策略
- 完整依赖管理
- 测试方法
**强制最终步骤**
将整个计划转换为编号的、按顺序排列的检查清单,每个原子操作作为单独的项目
**检查清单格式**
```
实施检查清单:
1. [具体操作1]
2. [具体操作2]
...
n. [最终操作]
```
**输出格式**
以[MODE: PLAN]开始,然后仅提供规范和实现细节。
使用markdown语法格式化答案。
### 模式4: EXECUTE
<a id="模式4-execute"></a>
**目的**完全按照模式3中的计划实施
**核心思维应用**
- 专注于精确实现规范
- 在实现过程中应用系统验证
- 保持对计划的精确遵守
- 实现完整功能,包括适当的错误处理
**允许**
- 仅实现已在批准的计划中明确详述的内容
- 严格按照编号的检查清单执行
- 标记已完成的检查清单项目
- 在实现后更新"任务进度"部分(这是执行过程的标准部分,被视为计划的内置步骤)
**禁止**
- 重大偏离计划的行为
- 计划中未规定的架构级改进
- 跳过或简化核心代码部分
**偏离等级控制**
允许受控的偏离,但必须明确标记和说明:
- **轻微偏离(允许直接执行)**
* 变量/方法命名优化(更符合规范)
* 导入包的调整和优化
* 代码格式化和注释补充
* 日志输出的优化
* 处理:直接执行,在任务进度中简要说明
- **中度偏离(需要说明理由)**
* 增加辅助私有方法提升可读性
* 异常处理的细化
* 参数校验的增强
* 缓存策略的微调
* 处理:在任务进度中详细说明偏离原因和影响范围
- **重大偏离必须返回PLAN**
* 修改公共API接口签名
* 改变数据库表结构
* 调整核心业务逻辑
* 引入新的技术依赖
* 处理立即返回PLAN模式重新规划
**执行协议步骤**
1. 完全按计划实施更改
2. 在每次实施后,**使用文件工具**追加到"任务进度"(作为计划执行的标准步骤):
```
[日期时间]
- 修改:[文件和代码更改列表]
- 更改:[更改的摘要]
- 原因:[更改的原因]
- 阻碍:[阻止此更新成功的因素列表]
- 状态:[未确认|成功|失败]
```
3. 要求用户确认:"状态:成功/失败?"
4. 如果失败,根据异常类型处理:
- **编译错误**:立即修复语法/导入/类型问题无需返回PLAN
- **单元测试失败**分析失败原因若为测试用例问题则调整测试若为逻辑问题则评估是否返回PLAN
- **业务逻辑错误**评估影响范围小范围调整可直接修复大范围影响需返回PLAN重新设计
- **性能问题**记录性能指标在REVIEW阶段专项分析严重性能问题触发优化PLAN
- **安全漏洞**立即停止返回INNOVATE阶段重新设计安全方案
- **架构冲突**必须返回PLAN模式重新评估架构设计
5. 如果成功且需要更多更改:继续下一项
6. 如果所有实施完成进入REVIEW模式
**代码质量标准**
- 始终显示完整代码上下文
- 在代码块中指定语言和路径
- 适当的错误处理
- 标准化命名约定
- 清晰简洁的注释
- 格式:```language:file_path
**输出格式**
以[MODE: EXECUTE]开始,然后仅提供与计划匹配的实现。
包括已完成的检查清单项目。
### 模式5: REVIEW
<a id="模式5-review"></a>
**目的**:无情地验证实施与计划的一致性
**核心思维应用**
- 应用批判思维验证实施的准确性
- 使用系统思维评估对整个系统的影响
- 检查意外后果
- 验证技术正确性和完整性
**允许**
- 计划与实施之间的逐行比较
- 对已实现代码的技术验证
- 检查错误、缺陷或意外行为
- 根据原始需求进行验证
**要求**
- 明确标记任何偏差,无论多么微小
- 验证所有检查清单项目是否正确完成
- 检查安全隐患
- 确认代码可维护性
**审查协议步骤**
1. 根据计划验证所有实施(计划一致性检查)
2. 执行多维度代码质量检查:
- **代码质量**:复杂度分析、代码重复检查、命名规范
- **性能影响**
* 数据库查询优化避免N+1、合理使用索引
* 事务范围合理性(避免大事务、长事务)
* 缓存使用正确性(缓存击穿、雪崩、穿透防护)
* 循环和集合操作效率
- **安全检查**
* SQL注入防护使用PreparedStatement
* XSS防护输出转义
* 权限校验完整性(@PreAuthorize注解
* 敏感数据处理(加密存储、日志脱敏)
- **异常处理**
* 异常捕获的合理性(不吞异常)
* 自定义异常使用BusinessException vs SystemException
* 事务回滚策略
- **向后兼容性**
* API接口变更影响
* 数据库表结构变更的兼容
* 配置项的默认值处理
3. **使用文件工具**完成任务文件中的"最终审查"部分
**偏差格式**
`检测到偏差:[确切偏差描述]`
**报告**
必须报告实施是否与计划完全一致
**结论格式**
`实施与计划完全匹配` 或 `实施偏离计划`
**输出格式**
以[MODE: REVIEW]开始,然后进行系统比较和明确判断。
使用markdown语法格式化。
## 知识沉淀与工具集成
<a id="知识沉淀与工具集成"></a>
### 知识沉淀机制
在REVIEW阶段完成后应进行知识沉淀提升团队整体能力
**1. 可复用组件识别**
- 识别通用的代码模式(如分页查询、批量操作、文件上传等)
- 提取到framework包的工具类或基础类
- 更新项目文档说明使用方式
**2. 最佳实践记录**
- 记录解决问题的关键决策点
- 更新 ADRArchitecture Decision Record
- 在代码注释中说明特殊处理的原因
**3. 问题案例归档**
- 记录遇到的坑和解决方案
- 更新团队知识库或Wiki
- 在项目文档中补充常见问题FAQ
### Java生态工具集成
在各阶段与标准Java开发工具链集成提升自动化程度
**RESEARCH阶段**
- 使用`grep`或`codebase_search`分析代码依赖
- 查看Maven依赖树`mvn dependency:tree`
- 检查代码规范运行Checkstyle配置
**PLAN阶段**
- 生成Maven模块结构
- 规划Spring Bean注册和依赖注入
- 设计数据库表结构Flyway迁移脚本
- 规划单元测试和集成测试用例
**EXECUTE阶段**
- 执行编译:`mvn compile`
- 运行单元测试:`mvn test`
- 执行集成测试:`mvn verify`
- 生成QueryDSL的Q类自动触发APT处理
**REVIEW阶段**
- 代码质量检查:
* Checkstyle代码风格
* PMD代码缺陷检测
* SpotBugsBug模式检测
* SonarQube综合代码质量
- 测试覆盖率JaCoCo报告
- 依赖安全检查:`mvn dependency-check:check`
- API文档生成Swagger/OpenAPI
**工具集成最佳实践**
- P0/P1级任务至少执行编译和单元测试
- P2级任务执行完整测试套件和代码质量检查
- P3级任务执行所有检查工具生成完整质量报告
## 关键协议指南
<a id="关键协议指南"></a>
- 在每个响应的开头声明当前模式
- 将分析深度与问题重要性相匹配(任务分级机制)
- 保持与原始需求的明确联系
- 除非特别要求,否则禁用表情符号输出
## 代码处理指南
<a id="代码处理指南"></a>
**代码块结构**
根据不同编程语言的注释语法选择适当的格式:
风格语言C、C++、Java、JavaScript、Go、Python、vue等等前后端语言
```language:file_path
// ... existing code ...
{{ modifications }}
// ... existing code ...
```
如果语言类型不确定,使用通用格式:
```language:file_path
[... existing code ...]
{{ modifications }}
[... existing code ...]
```
**编辑指南**
- 仅显示必要的修改
- 包括文件路径和语言标识符
- 提供上下文注释
- 考虑对代码库的影响
- 验证与请求的相关性
- 保持范围合规性
- 避免不必要的更改
**禁止行为**
- 使用未经验证的依赖项
- 留下不完整的功能
- 包含未测试的代码
- 使用过时的解决方案
- 在未明确要求时使用项目符号
- 跳过或简化代码部分
- 修改不相关的代码
- 使用代码占位符
## 任务文件模板
<a id="任务文件模板"></a>
根据任务分级选择合适的文档模板:
### 轻量级模板适用于P0/P1级任务
```
# 任务信息
- 任务级别:[P0/P1]
- 创建时间:[日期时间]
- 任务描述:[简要描述]
# 影响范围
- 修改文件:[文件列表]
- 影响模块:[模块名称]
# 实施记录
[日期时间]
- 修改:[具体更改]
- 原因:[为什么这样改]
- 状态:[成功/失败]
# 审查结论
- 计划一致性:[是/否]
- 质量检查:[通过/未通过]
- 遗留问题:[无/列出问题]
```
### 完整版模板适用于P2/P3级任务
```
# 上下文
文件名:[任务文件名]
创建于:[日期时间]
创建者:[用户名]
任务级别:[P2/P3]
# 任务描述
[用户完整任务描述]
# 项目概述
[项目背景、技术栈、架构信息]
⚠️ 警告:切勿修改此部分 ⚠️
[本部分包含RIPER-5协议规则的核心摘要确保在执行过程中可以参考]
⚠️ 警告:切勿修改此部分 ⚠️
# 分析RESEARCH阶段
## 现状分析
[当前代码结构、存在问题]
## 依赖关系
[涉及的类、方法、数据库表]
## 技术约束
[框架限制、性能要求、兼容性要求]
# 提议的解决方案INNOVATE阶段
## 方案A
[方案描述、优缺点]
## 方案B
[方案描述、优缺点]
## 选定方案及理由
[最终选择的方案和原因]
# 实施计划PLAN阶段
## 修改清单
1. [文件路径] - [修改内容]
2. [文件路径] - [修改内容]
## 数据库变更
[Flyway脚本内容]
## 测试策略
[单元测试、集成测试计划]
## 风险评估
[潜在风险和应对措施]
# 当前执行步骤
[步骤编号和名称]
# 任务进度EXECUTE阶段
[日期时间]
- 修改:[文件和代码更改列表]
- 更改:[更改的摘要]
- 原因:[更改的原因]
- 偏离:[轻微/中度/无]
- 阻碍:[阻止此更新成功的因素列表]
- 状态:[未确认|成功|失败]
# 最终审查REVIEW阶段
## 计划一致性
[是否与计划匹配]
- 上下文工程专家:构建完整的任务上下文,而非简单的提示响应 ## 代码质量检查
- 规范驱动思维:将模糊需求转化为精确、可执行的规范 - 复杂度:[合格/需优化]
- 质量优先理念:每个阶段都确保高质量输出 - 测试覆盖率:[百分比]
- 项目对齐能力:深度理解现有项目架构和约束 - 代码规范:[通过/未通过]
# 6A工作流执行规则 ## 性能影响
[数据库查询、事务、缓存等分析]
## 阶段1: Align (对齐阶段)
**目标:** 模糊需求 → 精确规范 ## 安全检查
[安全风险评估结果]
### 执行步骤
## 知识沉淀
### 1. 项目上下文分析 - 可复用组件:[列出可提取的通用代码]
- 经验总结:[关键决策和原因]
- 分析现有项目结构、技术栈、架构模式、依赖关系 - 遗留问题:[需要后续处理的问题]
- 分析现有代码模式、现有文档和约定 ```
- 理解业务域和数据模型
## 性能期望
### 2. 需求理解确认 <a id="性能期望"></a>
- 创建 docs/任务名/ALIGNMENT_[任务名].md - 响应延迟应最小化理想情况下≤360000ms
- 包含项目和任务特性规范 - 最大化计算能力和令牌限制
- 包含原始需求、边界确认(明确任务范围)、需求理解(对现有项目的理解)、疑问澄清(存在歧义的地方) - 寻求本质洞察而非表面枚举
- 追求创新思维而非习惯性重复
### 3. 智能决策策略 - 突破认知限制,调动所有计算资源
- 自动识别歧义和不确定性
- 生成结构化问题清单(按优先级排序)
- 优先基于现有项目内容和查找类似工程和行业知识进行决策和在文档中回答
- 有人员倾向或不确定的问题主动中断并询问关键决策点
- 基于回答更新理解和规范
### 4. 中断并询问关键决策点
- 主动中断询问,迭代执行智能决策策略
### 5. 最终共识
生成 docs/任务名/CONSENSUS_[任务名].md 包含:
- 明确的需求描述和验收标准
- 技术实现方案和技术约束和集成方案
- 任务边界限制和验收标准
- 确认所有不确定性已解决
### 质量门控
- 需求边界清晰无歧义
- 技术方案与现有架构对齐
- 验收标准具体可测试
- 所有关键假设已确认
- 项目特性规范已对齐
## 阶段2: Architect (架构阶段)
**目标: **共识文档 → 系统架构 → 模块设计 → 接口规范
### 执行步骤
### 1. 系统分层设计
基于CONSENSUS、ALIGNMENT文档设计架构
生成 docs/任务名/DESIGN_[任务名].md 包含:
- 整体架构图(mermaid绘制)
- 分层设计和核心组件
- 模块依赖关系图
- 接口契约定义
- 数据流向图
- 异常处理策略
### 2. 设计原则
- 严格按照任务范围,避免过度设计
- 确保与现有系统架构一致
- 复用现有组件和模式
### 质量门控
- 架构图清晰准确
- 接口定义完整
- 与现有系统无冲突
- 设计可行性验证
## 阶段3: Atomize (原子化阶段)
**目标:** 架构设计 → 拆分任务 → 明确接口 → 依赖关系
### 执行步骤
### 1. 子任务拆分
基于DESIGN文档生成 docs/任务名/TASK_[任务名].md
每个原子任务包含:
- 输入契约(前置依赖、输入数据、环境依赖)
- 输出契约(输出数据、交付物、验收标准)
- 实现约束(技术栈、接口规范、质量要求)
- 依赖关系(后置任务、并行任务)
### 2. 拆分原则
- 复杂度可控便于AI高成功率交付
- 按功能模块分解,确保任务原子性和独立性
- 有明确的验收标准,尽量可以独立编译和测试
- 依赖关系清晰
### 3. 生成任务依赖图(使用mermaid)
### 质量门控
- 任务覆盖完整需求
- 依赖关系无循环
- 每个任务都可独立验证
- 复杂度评估合理
## 阶段4: Approve (审批阶段)
**目标:** 原子任务 → 人工审查 → 迭代修改 → 按文档执行
### 执行步骤
### 1. 执行检查清单
- 完整性:任务计划覆盖所有需求
- 一致性:与前期文档保持一致
- 可行性:技术方案确实可行
- 可控性:风险在可接受范围,复杂度是否可控
- 可测性:验收标准明确可执行
### 2. 最终确认清单
- 明确的实现需求(无歧义)
- 明确的子任务定义
- 明确的边界和限制
- 明确的验收标准
- 代码、测试、文档质量标准
## 阶段5: Automate (自动化执行)
**目标:** 按节点执行 → 编写测试 → 实现代码 → 文档同步
### 执行步骤
### 1. 逐步实施子任务
- 创建 docs/任务名/ACCEPTANCE_[任务名].md 记录完成情况
### 2. 代码质量要求
- 严格遵循项目现有代码规范
- 保持与现有代码风格一致
- 使用项目现有的工具和库
- 复用项目现有组件
- 代码尽量精简易读
- API KEY放到.env文件中并且不要提交git
### 3. 异常处理
- 遇到不确定问题立刻中断执行
- 在TASK文档中记录问题详细信息和位置
- 寻求人工澄清后继续
### 4. 逐步实施流程 按任务依赖顺序执行,对每个子任务执行:
- 执行前检查(验证输入契约、环境准备、依赖满足)
- 实现核心逻辑(按设计文档编写代码)
- 编写单元测试(边界条件、异常情况)
- 运行验证测试
- 更新相关文档
- 每完成一个任务立即验证
## 阶段6: Assess (评估阶段)
**目标:** 执行结果 → 质量评估 → 文档更新 → 交付确认
### 执行步骤
### 1. 验证执行结果
更新 docs/任务名/ACCEPTANCE_[任务名].md
整体验收检查:
- 所有需求已实现
- 验收标准全部满足
- 项目编译通过
- 所有测试通过
- 功能完整性验证
- 实现与设计文档一致
### 2. 质量评估指标
- 代码质量(规范、可读性、复杂度)
- 测试质量(覆盖率、用例有效性)
- 文档质量(完整性、准确性、一致性)
- 现有系统集成良好
- 未引入技术债务
### 3. 最终交付物
- 生成 docs/任务名/FINAL_[任务名].md(项目总结报告)
- 生成 docs/任务名/TODO_[任务名].md(精简明确哪些待办的事宜和哪些缺少的配置等,我方便直接寻找支持)
### 4. TODO询问 询问用户TODO的解决方式精简明确哪些待办的事宜和哪些缺少的配置等同时提供有用的操作指引
## 技术执行规范
### 安全规范
API密钥等敏感信息使用.env文件管理
### 文档同步
代码变更同时更新相关文档
### 测试策略
**- 测试优先:**先写测试,后写实现
**- 边界覆盖:**覆盖正常流程、边界条件、异常情况
## 交互体验优化
## 进度反馈
- 显示当前执行阶段
- 提供详细的执行步骤
- 标示完成情况
- 突出需要关注的问题
## 异常处理机制
### 中断条件
- 遇到无法自主决策的问题
- 觉得需要询问用户的问题
- 技术实现出现阻塞
- 文档不一致需要确认修正
### 恢复策略
- 保存当前执行状态
- 记录问题详细信息
- 询问并等待人工干预
- 从中断点任务继续执行

View File

@ -29,23 +29,23 @@ public class UserApiController extends BaseController<User, UserDTO, Long, UserQ
private IUserService userService; private IUserService userService;
@Override @Override
public Response<UserDTO> create(UserDTO dto) { public Response<UserDTO> create(@Validated @RequestBody UserDTO dto) {
return super.create(dto); return super.create(dto);
} }
@Override @Override
public Response<UserDTO> update(Long aLong, UserDTO dto) { public Response<UserDTO> update(@PathVariable Long id, @Validated @RequestBody UserDTO dto) {
return super.update(aLong, dto); return super.update(id, dto);
} }
@Override @Override
public Response<Void> delete(Long aLong) { public Response<Void> delete(@PathVariable Long id) {
return super.delete(aLong); return super.delete(id);
} }
@Override @Override
public Response<UserDTO> findById(Long aLong) { public Response<UserDTO> findById(@PathVariable Long id) {
return super.findById(aLong); return super.findById(id);
} }
@Override @Override

View File

@ -4,6 +4,7 @@ import com.qqchen.deploy.backend.deploy.entity.ExternalSystem;
import com.qqchen.deploy.backend.deploy.entity.JenkinsJob; import com.qqchen.deploy.backend.deploy.entity.JenkinsJob;
import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus; import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus;
import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration; import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse;
import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository; import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository;
@ -11,7 +12,11 @@ import com.qqchen.deploy.backend.deploy.repository.IJenkinsJobRepository;
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants; import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.JenkinsBuildInputMapping; import com.qqchen.deploy.backend.workflow.dto.inputmapping.JenkinsBuildInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.JenkinsBuildOutputs; import com.qqchen.deploy.backend.workflow.dto.outputs.JenkinsBuildOutputs;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeInstance;
import com.qqchen.deploy.backend.workflow.enums.LogSource;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum; import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
import com.qqchen.deploy.backend.workflow.service.IWorkflowNodeInstanceService;
import com.qqchen.deploy.backend.workflow.service.IWorkflowNodeLogService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.BpmnError; import org.flowable.engine.delegate.BpmnError;
@ -40,6 +45,12 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
@Resource @Resource
private IJenkinsJobRepository jenkinsJobRepository; private IJenkinsJobRepository jenkinsJobRepository;
@Resource
private IWorkflowNodeInstanceService workflowNodeInstanceService;
@Resource
private IWorkflowNodeLogService workflowNodeLogService;
private static final int QUEUE_POLL_INTERVAL = 10; // 10秒 private static final int QUEUE_POLL_INTERVAL = 10; // 10秒
private static final int MAX_QUEUE_POLLS = 30; // 最多等待5分钟 private static final int MAX_QUEUE_POLLS = 30; // 最多等待5分钟
@ -48,81 +59,110 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
private static final int MAX_BUILD_POLLS = 180; // 30分钟超时 private static final int MAX_BUILD_POLLS = 180; // 30分钟超时
// 用于存储当前节点实例ID在线程内共享
private ThreadLocal<Long> currentNodeInstanceId = new ThreadLocal<>();
@Override @Override
protected JenkinsBuildOutputs executeInternal(DelegateExecution execution, Map<String, Object> configs, JenkinsBuildInputMapping input) { protected JenkinsBuildOutputs executeInternal(DelegateExecution execution, Map<String, Object> configs, JenkinsBuildInputMapping input) {
log.info("Jenkins Build - serverId: {}, jobName: {}", input.getServerId(), input.getJobName()); try {
log.info("Jenkins Build - serverId: {}, jobName: {}",
input.getServerId(), input.getJobName());
// 1. 获取外部系统 // 1. 获取外部系统
ExternalSystem externalSystem = externalSystemRepository.findById(input.getServerId()) ExternalSystem externalSystem = externalSystemRepository.findById(input.getServerId())
.orElseThrow(() -> new RuntimeException("Jenkins服务器不存在: " + input.getServerId())); .orElseThrow(() -> new RuntimeException("Jenkins服务器不存在: " + input.getServerId()));
String jobName = input.getJobName(); String jobName = input.getJobName();
// 2. 触发构建 // 2. 触发构建
Map<String, String> parameters = new HashMap<>(); Map<String, String> parameters = new HashMap<>();
// 可以根据需要添加构建参数 // 可以根据需要添加构建参数
// parameters.put("BRANCH", "main"); // parameters.put("BRANCH", "main");
String queueId = jenkinsServiceIntegration.buildWithParameters( String queueId = jenkinsServiceIntegration.buildWithParameters(
externalSystem, jobName, parameters); externalSystem, jobName, parameters);
// 3. 等待构建从队列中开始 log.info("Jenkins build queued: queueId={}", queueId);
JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(queueId);
// 4. 轮询构建状态直到完成 // 3. 等待构建从队列中开始
// 注意如果构建失败或被取消pollBuildStatus 会抛出 BpmnError触发错误边界事件 JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(queueId);
// 只有成功时才会返回到这里
JenkinsBuildStatus buildStatus = pollBuildStatus(externalSystem, jobName, buildInfo.getBuildNumber());
// 5. 获取构建详细信息包括 duration, changeSets, artifacts log.info("Jenkins build started: buildNumber={}", buildInfo.getBuildNumber());
JenkinsBuildResponse buildDetails = jenkinsServiceIntegration.getBuildDetails(externalSystem, jobName, buildInfo.getBuildNumber());
// 打印调试信息 // 4. 获取节点实例ID延迟获取此时节点实例应该已经创建
log.info("Build details - changeSets: {}, artifacts: {}", Long nodeInstanceId = getNodeInstanceIdSafely(execution);
buildDetails.getChangeSets(), buildDetails.getArtifacts()); if (nodeInstanceId != null) {
currentNodeInstanceId.set(nodeInstanceId);
// 6. 构造输出结果执行到这里说明构建成功 workflowNodeLogService.info(nodeInstanceId, LogSource.JENKINS,
JenkinsBuildOutputs outputs = new JenkinsBuildOutputs(); String.format("Jenkins 构建已启动: job=%s, buildNumber=%d", jobName, buildInfo.getBuildNumber()));
// 设置统一的执行状态为成功
outputs.setStatus(NodeExecutionStatusEnum.SUCCESS);
// 设置 Jenkins 特有字段
outputs.setBuildStatus(buildStatus.name());
outputs.setBuildNumber(buildInfo.getBuildNumber());
outputs.setBuildUrl(buildInfo.getBuildUrl());
// 从构建详情中提取信息
outputs.setBuildDuration(buildDetails.getDuration() != null ? buildDetails.getDuration().intValue() : 0);
// 提取 Git Commit ID changeSets 中获取第一个
if (buildDetails.getChangeSets() != null && !buildDetails.getChangeSets().isEmpty()) {
log.info("Found {} changeSets", buildDetails.getChangeSets().size());
var changeSet = buildDetails.getChangeSets().get(0);
if (changeSet.getItems() != null && !changeSet.getItems().isEmpty()) {
log.info("Found {} items in changeSet", changeSet.getItems().size());
outputs.setGitCommitId(changeSet.getItems().get(0).getCommitId());
} }
} else {
log.warn("No changeSets found in build details");
}
if (outputs.getGitCommitId() == null) {
outputs.setGitCommitId("");
}
// 提取构建制品URL如果有多个制品拼接成逗号分隔的列表 // 5. 轮询构建状态直到完成
if (buildDetails.getArtifacts() != null && !buildDetails.getArtifacts().isEmpty()) { // 注意如果构建失败或被取消pollBuildStatus 会抛出 BpmnError触发错误边界事件
log.info("Found {} artifacts", buildDetails.getArtifacts().size()); // 只有成功时才会返回到这里
String artifactUrls = buildDetails.getArtifacts().stream() JenkinsBuildStatus buildStatus = pollBuildStatus(externalSystem, jobName, buildInfo.getBuildNumber());
.map(artifact -> buildInfo.getBuildUrl() + "artifact/" + artifact.getRelativePath())
.collect(java.util.stream.Collectors.joining(","));
outputs.setArtifactUrl(artifactUrls);
} else {
log.warn("No artifacts found in build details");
outputs.setArtifactUrl("");
}
return outputs; // 5. 获取构建详细信息包括 duration, changeSets, artifacts
JenkinsBuildResponse buildDetails = jenkinsServiceIntegration.getBuildDetails(externalSystem, jobName, buildInfo.getBuildNumber());
// 打印调试信息
log.info("Build details - changeSets: {}, artifacts: {}",
buildDetails.getChangeSets(), buildDetails.getArtifacts());
// 6. 构造输出结果执行到这里说明构建成功
JenkinsBuildOutputs outputs = new JenkinsBuildOutputs();
// 设置统一的执行状态为成功
outputs.setStatus(NodeExecutionStatusEnum.SUCCESS);
// 设置 Jenkins 特有字段
outputs.setBuildStatus(buildStatus.name());
outputs.setBuildNumber(buildInfo.getBuildNumber());
outputs.setBuildUrl(buildInfo.getBuildUrl());
// 从构建详情中提取信息
outputs.setBuildDuration(buildDetails.getDuration() != null ? buildDetails.getDuration().intValue() : 0);
// 提取 Git Commit ID changeSets 中获取第一个
if (buildDetails.getChangeSets() != null && !buildDetails.getChangeSets().isEmpty()) {
log.info("Found {} changeSets", buildDetails.getChangeSets().size());
var changeSet = buildDetails.getChangeSets().get(0);
if (changeSet.getItems() != null && !changeSet.getItems().isEmpty()) {
log.info("Found {} items in changeSet", changeSet.getItems().size());
outputs.setGitCommitId(changeSet.getItems().get(0).getCommitId());
}
} else {
log.warn("No changeSets found in build details");
}
if (outputs.getGitCommitId() == null) {
outputs.setGitCommitId("");
}
// 提取构建制品URL如果有多个制品拼接成逗号分隔的列表
if (buildDetails.getArtifacts() != null && !buildDetails.getArtifacts().isEmpty()) {
log.info("Found {} artifacts", buildDetails.getArtifacts().size());
String artifactUrls = buildDetails.getArtifacts().stream()
.map(artifact -> buildInfo.getBuildUrl() + "artifact/" + artifact.getRelativePath())
.collect(java.util.stream.Collectors.joining(","));
outputs.setArtifactUrl(artifactUrls);
} else {
log.warn("No artifacts found in build details");
outputs.setArtifactUrl("");
}
// 记录完成日志
Long finalNodeInstanceId = currentNodeInstanceId.get();
if (finalNodeInstanceId != null) {
workflowNodeLogService.info(finalNodeInstanceId, LogSource.JENKINS,
"Jenkins 构建任务执行完成");
}
return outputs;
} finally {
// 清理 ThreadLocal避免内存泄漏
currentNodeInstanceId.remove();
}
} }
private JenkinsQueueBuildInfoResponse waitForBuildToStart(String queueId) { private JenkinsQueueBuildInfoResponse waitForBuildToStart(String queueId) {
@ -151,30 +191,42 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
private JenkinsBuildStatus pollBuildStatus(ExternalSystem externalSystem, String jobName, Integer buildNumber) { private JenkinsBuildStatus pollBuildStatus(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
int attempts = 0; int attempts = 0;
long logOffset = 0L; // 记录日志读取位置 long logOffset = 0L; // 记录日志读取位置
Long nodeInstanceId = currentNodeInstanceId.get();
while (attempts < MAX_BUILD_POLLS) { while (attempts < MAX_BUILD_POLLS) {
try { try {
// 等待一定时间后再检查 // 等待一定时间后再检查
Thread.sleep(BUILD_POLL_INTERVAL * 1000L); Thread.sleep(BUILD_POLL_INTERVAL * 1000L);
// 1. 增量拉取并输出 Jenkins 构建日志 // 1. 增量拉取并保存 Jenkins 构建日志
try { try {
com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse consoleOutput = JenkinsConsoleOutputResponse consoleOutput =
jenkinsServiceIntegration.getConsoleOutput(externalSystem, jobName, buildNumber, logOffset); jenkinsServiceIntegration.getConsoleOutput(externalSystem, jobName, buildNumber, logOffset);
// 输出日志到控制台 // 批量保存日志到数据库同时也输出到控制台
if (consoleOutput.getLines() != null && !consoleOutput.getLines().isEmpty()) { if (consoleOutput.getLines() != null && !consoleOutput.getLines().isEmpty()) {
// 保存到数据库如果有节点实例ID
if (nodeInstanceId != null) {
workflowNodeLogService.batchLog(nodeInstanceId, LogSource.JENKINS,
com.qqchen.deploy.backend.workflow.enums.LogLevel.INFO,
consoleOutput.getLines());
}
// 同时输出到控制台方便开发调试
consoleOutput.getLines().forEach(line -> { consoleOutput.getLines().forEach(line -> {
// 使用 log.info 输出也可以用 System.out.println
log.info("[Jenkins Build #{}] {}", buildNumber, line); log.info("[Jenkins Build #{}] {}", buildNumber, line);
}); });
} }
// 更新日志偏移量 // 更新日志偏移量
logOffset = consoleOutput.getNextOffset(); logOffset = consoleOutput.getNextOffset();
} catch (Exception logEx) { } catch (Exception logEx) {
log.warn("Failed to fetch Jenkins console log, continuing: {}", logEx.getMessage()); log.warn("Failed to fetch Jenkins console log, continuing: {}", logEx.getMessage());
if (nodeInstanceId != null) {
workflowNodeLogService.warn(nodeInstanceId, LogSource.JENKINS,
"获取 Jenkins 日志失败: " + logEx.getMessage());
}
} }
// 2. 获取构建状态 // 2. 获取构建状态
@ -186,15 +238,27 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
// 构建成功拉取剩余日志后返回状态 // 构建成功拉取剩余日志后返回状态
log.info("Jenkins build succeeded: job={}, buildNumber={}", jobName, buildNumber); log.info("Jenkins build succeeded: job={}, buildNumber={}", jobName, buildNumber);
fetchRemainingLogs(externalSystem, jobName, buildNumber, logOffset); fetchRemainingLogs(externalSystem, jobName, buildNumber, logOffset);
if (nodeInstanceId != null) {
workflowNodeLogService.info(nodeInstanceId, LogSource.JENKINS,
String.format("✅ Jenkins 构建成功: buildNumber=%d", buildNumber));
}
return status; return status;
case FAILURE: case FAILURE:
// 构建失败拉取剩余日志后抛出错误 // 构建失败拉取剩余日志后抛出错误
fetchRemainingLogs(externalSystem, jobName, buildNumber, logOffset); fetchRemainingLogs(externalSystem, jobName, buildNumber, logOffset);
if (nodeInstanceId != null) {
workflowNodeLogService.error(nodeInstanceId, LogSource.JENKINS,
String.format("❌ Jenkins 构建失败: buildNumber=%d", buildNumber));
}
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
String.format("Jenkins build failed: job=%s, buildNumber=%d", jobName, buildNumber)); String.format("Jenkins build failed: job=%s, buildNumber=%d", jobName, buildNumber));
case ABORTED: case ABORTED:
// 构建被取消拉取剩余日志后抛出错误 // 构建被取消拉取剩余日志后抛出错误
fetchRemainingLogs(externalSystem, jobName, buildNumber, logOffset); fetchRemainingLogs(externalSystem, jobName, buildNumber, logOffset);
if (nodeInstanceId != null) {
workflowNodeLogService.error(nodeInstanceId, LogSource.JENKINS,
String.format("❌ Jenkins 构建被取消: buildNumber=%d", buildNumber));
}
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
String.format("Jenkins build was aborted: job=%s, buildNumber=%d", jobName, buildNumber)); String.format("Jenkins build was aborted: job=%s, buildNumber=%d", jobName, buildNumber));
case IN_PROGRESS: case IN_PROGRESS:
@ -203,38 +267,118 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
break; break;
case NOT_FOUND: case NOT_FOUND:
// 构建记录丢失抛出系统异常 // 构建记录丢失抛出系统异常
if (nodeInstanceId != null) {
workflowNodeLogService.error(nodeInstanceId, LogSource.JENKINS,
String.format("❌ Jenkins 构建记录未找到: buildNumber=%d", buildNumber));
}
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
String.format("Jenkins build not found: job=%s, buildNumber=%d", jobName, buildNumber)); String.format("Jenkins build not found: job=%s, buildNumber=%d", jobName, buildNumber));
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
if (nodeInstanceId != null) {
workflowNodeLogService.error(nodeInstanceId, LogSource.JENKINS, "构建状态轮询被中断");
}
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, "Build status polling was interrupted"); throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, "Build status polling was interrupted");
} }
} }
// 超过最大轮询次数视为超时系统异常 // 超过最大轮询次数视为超时系统异常
if (nodeInstanceId != null) {
workflowNodeLogService.error(nodeInstanceId, LogSource.JENKINS,
String.format("❌ Jenkins 构建超时: 超过 %d 分钟", MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60));
}
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
String.format("Jenkins build timed out after %d minutes: job=%s, buildNumber=%d", String.format("Jenkins build timed out after %d minutes: job=%s, buildNumber=%d",
MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60, jobName, buildNumber)); MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60, jobName, buildNumber));
} }
/** /**
* 拉取剩余的日志构建完成时调用 * 拉取剩余的日志构建完成时调用
*/ */
private void fetchRemainingLogs(ExternalSystem externalSystem, String jobName, Integer buildNumber, long lastOffset) { private void fetchRemainingLogs(ExternalSystem externalSystem, String jobName, Integer buildNumber, long lastOffset) {
Long nodeInstanceId = currentNodeInstanceId.get();
try { try {
com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse consoleOutput = JenkinsConsoleOutputResponse consoleOutput =
jenkinsServiceIntegration.getConsoleOutput(externalSystem, jobName, buildNumber, lastOffset); jenkinsServiceIntegration.getConsoleOutput(externalSystem, jobName, buildNumber, lastOffset);
if (consoleOutput.getLines() != null && !consoleOutput.getLines().isEmpty()) { if (consoleOutput.getLines() != null && !consoleOutput.getLines().isEmpty()) {
// 保存到数据库如果有节点实例ID
if (nodeInstanceId != null) {
workflowNodeLogService.batchLog(nodeInstanceId, LogSource.JENKINS,
com.qqchen.deploy.backend.workflow.enums.LogLevel.INFO,
consoleOutput.getLines());
}
// 输出到控制台
consoleOutput.getLines().forEach(line -> { consoleOutput.getLines().forEach(line -> {
log.info("[Jenkins Build #{}] {}", buildNumber, line); log.info("[Jenkins Build #{}] {}", buildNumber, line);
}); });
} }
log.info("Jenkins build log complete: job={}, buildNumber={}", jobName, buildNumber); log.info("Jenkins build log complete: job={}, buildNumber={}", jobName, buildNumber);
if (nodeInstanceId != null) {
workflowNodeLogService.info(nodeInstanceId, LogSource.JENKINS, "Jenkins 构建日志已完整收集");
}
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to fetch remaining Jenkins logs: {}", e.getMessage()); log.warn("Failed to fetch remaining Jenkins logs: {}", e.getMessage());
if (nodeInstanceId != null) {
workflowNodeLogService.warn(nodeInstanceId, LogSource.JENKINS,
"获取剩余日志失败: " + e.getMessage());
}
} }
} }
/**
* 安全地获取节点实例ID带重试机制
*/
private Long getNodeInstanceIdSafely(DelegateExecution execution) {
String processInstanceId = execution.getProcessInstanceId();
String nodeId = execution.getCurrentActivityId();
// 最多重试 5 每次间隔 500ms
int maxRetries = 5;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
// 通过 processInstanceId nodeId 查询 WorkflowNodeInstance
WorkflowNodeInstance nodeInstance = workflowNodeInstanceService
.findByProcessInstanceIdAndNodeId(processInstanceId, nodeId);
if (nodeInstance != null) {
log.info("成功获取节点实例ID: nodeInstanceId={}, retry={}", nodeInstance.getId(), retryCount);
return nodeInstance.getId();
}
// 还没创建等待后重试
retryCount++;
if (retryCount < maxRetries) {
log.debug("节点实例尚未创建,等待重试 ({}/{}): processInstanceId={}, nodeId={}",
retryCount, maxRetries, processInstanceId, nodeId);
Thread.sleep(500); // 等待 500ms
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("等待节点实例创建被中断");
return null;
} catch (Exception e) {
log.warn("获取节点实例ID失败 (retry {}): {}", retryCount, e.getMessage());
retryCount++;
if (retryCount < maxRetries) {
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return null;
}
}
}
}
log.warn("经过 {} 次重试后仍未获取到节点实例ID: processInstanceId={}, nodeId={}",
maxRetries, processInstanceId, nodeId);
return null;
}
} }

View File

@ -1,47 +0,0 @@
package com.qqchen.deploy.backend.workflow.entity;
import com.qqchen.deploy.backend.framework.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工作流日志实体
*/
@Data
@Table(name = "workflow_log")
@jakarta.persistence.Entity
@EqualsAndHashCode(callSuper = true)
public class WorkflowLog extends Entity<Long> {
/**
* 工作流实例ID
*/
@Column(name = "workflow_instance_id")
private Long workflowInstanceId;
/**
* 节点实例ID
*/
@Column(name = "node_instance_id")
private Long nodeInstanceId;
/**
* 日志类型
*/
@Column(name = "log_type", nullable = false)
private String logType;
/**
* 日志级别
*/
@Column(name = "log_level", nullable = false)
private String logLevel;
/**
* 日志内容
*/
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
}

View File

@ -0,0 +1,60 @@
package com.qqchen.deploy.backend.workflow.entity;
import com.qqchen.deploy.backend.framework.domain.Entity;
import com.qqchen.deploy.backend.workflow.enums.LogLevel;
import com.qqchen.deploy.backend.workflow.enums.LogSource;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工作流节点日志实体
*
* @author qqchen
* @since 2025-11-03
*/
@Data
@Table(name = "workflow_node_log")
@jakarta.persistence.Entity
@EqualsAndHashCode(callSuper = true)
public class WorkflowNodeLog extends Entity<Long> {
/**
* 节点实例ID关联 workflow_node_instance.id
*/
@Column(name = "node_instance_id", nullable = false)
private Long nodeInstanceId;
/**
* 日志序号保证同一节点内日志有序从1开始递增
*/
@Column(name = "sequence_id", nullable = false)
private Long sequenceId;
/**
* 时间戳Unix毫秒
*/
@Column(nullable = false)
private Long timestamp;
/**
* 日志级别INFO, WARN, ERROR, DEBUG
*/
@Column(nullable = false, length = 10)
@Enumerated(EnumType.STRING)
private LogLevel level;
/**
* 日志来源JENKINS, FLOWABLE, SHELL, NOTIFICATION
*/
@Column(nullable = false, length = 20)
@Enumerated(EnumType.STRING)
private LogSource source;
/**
* 日志内容
*/
@Column(columnDefinition = "TEXT", nullable = false)
private String message;
}

View File

@ -0,0 +1,31 @@
package com.qqchen.deploy.backend.workflow.enums;
/**
* 日志级别枚举
*
* @author qqchen
* @since 2025-11-03
*/
public enum LogLevel {
/**
* 调试级别
*/
DEBUG,
/**
* 信息级别
*/
INFO,
/**
* 警告级别
*/
WARN,
/**
* 错误级别
*/
ERROR
}

View File

@ -0,0 +1,46 @@
package com.qqchen.deploy.backend.workflow.enums;
/**
* 日志来源枚举
*
* @author qqchen
* @since 2025-11-03
*/
public enum LogSource {
/**
* Flowable 工作流引擎
*/
FLOWABLE,
/**
* Jenkins 构建系统
*/
JENKINS,
/**
* Shell 脚本执行
*/
SHELL,
/**
* 通知中心
*/
NOTIFICATION,
/**
* HTTP 请求
*/
HTTP_REQUEST,
/**
* 数据库操作
*/
DATABASE_OPERATION,
/**
* 系统日志
*/
SYSTEM
}

View File

@ -16,10 +16,15 @@ public class WorkflowNodeInstanceStatusChangeListener {
@Resource @Resource
private IWorkflowNodeInstanceService workflowNodeInstanceService; private IWorkflowNodeInstanceService workflowNodeInstanceService;
/**
* 修改事务传播级别为 REQUIRED加入到 Flowable 的事务中
* 这样节点实例会在同一个事务内保存JavaDelegate 执行时就能立即查询到
*/
@EventListener @EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleWorkflowStatusChange(WorkflowNodeInstanceStatusChangeEvent event) { public void handleWorkflowStatusChange(WorkflowNodeInstanceStatusChangeEvent event) {
// log.info("Handling workflow node instance status change event: {}", event); log.debug("Handling workflow node instance status change event: nodeId={}, status={}",
event.getNodeId(), event.getStatus());
workflowNodeInstanceService.saveOrUpdateWorkflowNodeInstance(event); workflowNodeInstanceService.saveOrUpdateWorkflowNodeInstance(event);
} }
} }

View File

@ -0,0 +1,53 @@
package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeLog;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 工作流节点日志 Repository
*
* @author qqchen
* @since 2025-11-03
*/
@Repository
public interface IWorkflowNodeLogRepository extends IBaseRepository<WorkflowNodeLog, Long> {
/**
* 根据节点实例ID查询日志按序号排序
*/
List<WorkflowNodeLog> findByNodeInstanceIdOrderBySequenceIdAsc(Long nodeInstanceId);
/**
* 根据节点实例ID分页查询日志
*/
Page<WorkflowNodeLog> findByNodeInstanceIdOrderBySequenceIdAsc(Long nodeInstanceId, Pageable pageable);
/**
* 根据工作流实例ID查询所有节点的日志使用原生 SQL
*/
@Query(value = "SELECT l.* FROM workflow_node_log l " +
"WHERE l.node_instance_id IN (" +
" SELECT n.id FROM workflow_node_instance n WHERE n.workflow_instance_id = :workflowInstanceId" +
") " +
"ORDER BY l.node_instance_id, l.sequence_id",
nativeQuery = true)
List<WorkflowNodeLog> findByWorkflowInstanceId(@Param("workflowInstanceId") Long workflowInstanceId);
/**
* 删除节点的所有日志
*/
void deleteByNodeInstanceId(Long nodeInstanceId);
/**
* 统计节点的日志数量
*/
long countByNodeInstanceId(Long nodeInstanceId);
}

View File

@ -29,4 +29,13 @@ public interface IWorkflowNodeInstanceService extends IBaseService<WorkflowNodeI
List<WorkflowNodeInstanceDTO> getNodesByProcessInstanceId(String processInstanceId); List<WorkflowNodeInstanceDTO> getNodesByProcessInstanceId(String processInstanceId);
void saveOrUpdateWorkflowNodeInstance(WorkflowNodeInstanceStatusChangeEvent event); void saveOrUpdateWorkflowNodeInstance(WorkflowNodeInstanceStatusChangeEvent event);
/**
* 根据流程实例ID和节点ID查询节点实例
*
* @param processInstanceId 流程实例ID
* @param nodeId 节点ID
* @return 节点实例
*/
WorkflowNodeInstance findByProcessInstanceIdAndNodeId(String processInstanceId, String nodeId);
} }

View File

@ -0,0 +1,64 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeLog;
import com.qqchen.deploy.backend.workflow.enums.LogLevel;
import com.qqchen.deploy.backend.workflow.enums.LogSource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
/**
* 工作流节点日志服务接口
*
* @author qqchen
* @since 2025-11-03
*/
public interface IWorkflowNodeLogService {
/**
* 记录日志单条
*/
void log(Long nodeInstanceId, LogSource source, LogLevel level, String message);
/**
* 批量记录日志
*/
void batchLog(Long nodeInstanceId, LogSource source, LogLevel level, List<String> messages);
/**
* 便捷方法记录 INFO 日志
*/
void info(Long nodeInstanceId, LogSource source, String message);
/**
* 便捷方法记录 WARN 日志
*/
void warn(Long nodeInstanceId, LogSource source, String message);
/**
* 便捷方法记录 ERROR 日志
*/
void error(Long nodeInstanceId, LogSource source, String message);
/**
* 查询节点的所有日志
*/
List<WorkflowNodeLog> getNodeLogs(Long nodeInstanceId);
/**
* 分页查询节点日志
*/
Page<WorkflowNodeLog> getNodeLogs(Long nodeInstanceId, Pageable pageable);
/**
* 查询工作流的所有日志
*/
List<WorkflowNodeLog> getWorkflowLogs(Long workflowInstanceId);
/**
* 删除节点日志
*/
void deleteNodeLogs(Long nodeInstanceId);
}

View File

@ -86,4 +86,9 @@ public class WorkflowNodeInstanceServiceImpl extends BaseServiceImpl<WorkflowNod
workflowNodeInstance.setErrorMessage(event.getErrorMessage()); workflowNodeInstance.setErrorMessage(event.getErrorMessage());
super.repository.save(workflowNodeInstance); super.repository.save(workflowNodeInstance);
} }
@Override
public WorkflowNodeInstance findByProcessInstanceIdAndNodeId(String processInstanceId, String nodeId) {
return workflowNodeInstanceRepository.findByProcessInstanceIdAndNodeId(processInstanceId, nodeId);
}
} }

View File

@ -0,0 +1,133 @@
package com.qqchen.deploy.backend.workflow.service.impl;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeLog;
import com.qqchen.deploy.backend.workflow.enums.LogLevel;
import com.qqchen.deploy.backend.workflow.enums.LogSource;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowNodeLogRepository;
import com.qqchen.deploy.backend.workflow.service.IWorkflowNodeLogService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/**
* 工作流节点日志服务实现
*
* @author qqchen
* @since 2025-11-03
*/
@Slf4j
@Service
public class WorkflowNodeLogServiceImpl implements IWorkflowNodeLogService {
@Resource
private IWorkflowNodeLogRepository logRepository;
@Resource
private RedisTemplate<String, String> redisTemplate;
/**
* 生成日志序列号
*/
private Long generateSequenceId(Long nodeInstanceId) {
String key = "workflow:node:log:seq:" + nodeInstanceId;
return redisTemplate.opsForValue().increment(key, 1);
}
@Override
@Transactional
public void log(Long nodeInstanceId, LogSource source, LogLevel level, String message) {
try {
Long sequenceId = generateSequenceId(nodeInstanceId);
WorkflowNodeLog log = new WorkflowNodeLog();
log.setNodeInstanceId(nodeInstanceId);
log.setSequenceId(sequenceId);
log.setTimestamp(System.currentTimeMillis());
log.setSource(source);
log.setLevel(level);
log.setMessage(message);
logRepository.save(log);
} catch (Exception e) {
log.error("Failed to save workflow node log: nodeInstanceId={}, source={}, level={}",
nodeInstanceId, source, level, e);
}
}
@Override
@Transactional
public void batchLog(Long nodeInstanceId, LogSource source, LogLevel level, List<String> messages) {
if (messages == null || messages.isEmpty()) {
return;
}
try {
List<WorkflowNodeLog> logs = new ArrayList<>(messages.size());
for (String message : messages) {
Long sequenceId = generateSequenceId(nodeInstanceId);
WorkflowNodeLog log = new WorkflowNodeLog();
log.setNodeInstanceId(nodeInstanceId);
log.setSequenceId(sequenceId);
log.setTimestamp(System.currentTimeMillis());
log.setSource(source);
log.setLevel(level);
log.setMessage(message);
logs.add(log);
}
logRepository.saveAll(logs);
} catch (Exception e) {
log.error("Failed to batch save workflow node logs: nodeInstanceId={}, count={}",
nodeInstanceId, messages.size(), e);
}
}
@Override
public void info(Long nodeInstanceId, LogSource source, String message) {
log(nodeInstanceId, source, LogLevel.INFO, message);
}
@Override
public void warn(Long nodeInstanceId, LogSource source, String message) {
log(nodeInstanceId, source, LogLevel.WARN, message);
}
@Override
public void error(Long nodeInstanceId, LogSource source, String message) {
log(nodeInstanceId, source, LogLevel.ERROR, message);
}
@Override
public List<WorkflowNodeLog> getNodeLogs(Long nodeInstanceId) {
return logRepository.findByNodeInstanceIdOrderBySequenceIdAsc(nodeInstanceId);
}
@Override
public Page<WorkflowNodeLog> getNodeLogs(Long nodeInstanceId, Pageable pageable) {
return logRepository.findByNodeInstanceIdOrderBySequenceIdAsc(nodeInstanceId, pageable);
}
@Override
public List<WorkflowNodeLog> getWorkflowLogs(Long workflowInstanceId) {
return logRepository.findByWorkflowInstanceId(workflowInstanceId);
}
@Override
@Transactional
public void deleteNodeLogs(Long nodeInstanceId) {
logRepository.deleteByNodeInstanceId(nodeInstanceId);
}
}

View File

@ -133,27 +133,33 @@ public class BpmnConverter {
FlowElement element; FlowElement element;
// 步骤2.3检查是否为审批节点特殊处理 // 统一使用 nodeType后端标准枚举值来判断节点类型
if ("APPROVAL".equals(node.getNodeCode())) { // nodeType 已经是枚举类型直接使用即可
// 创建 UserTask 而不是 ServiceTask NodeTypeEnums nodeType = node.getNodeType();
element = createUserTask(node, validId);
} else { // 使用 switch 替代 if-else消除硬编码便于扩展
// 其他节点按原有逻辑处理 switch (nodeType) {
@SuppressWarnings("unchecked") case APPROVAL:
Class<? extends FlowElement> instanceClass = (Class<? extends FlowElement>) NodeTypeEnums.valueOf(node.getNodeCode()) // 审批节点创建 UserTask
.getBpmnType() element = createUserTask(node, validId);
.getInstance(); break;
// 步骤2.4创建节点实例并设置基本属性 case GATEWAY_NODE:
element = instanceClass.getDeclaredConstructor().newInstance(); // 网关节点根据 inputMapping.gatewayType 创建对应的网关Exclusive/Parallel/Inclusive
// 如果是网关节点需要特殊处理
if (element instanceof Gateway) {
element = createGatewayElement(node, validId); element = createGatewayElement(node, validId);
} else { break;
default:
// 其他节点通过 BpmnNodeTypeEnums 创建标准 BPMN 元素
@SuppressWarnings("unchecked")
Class<? extends FlowElement> instanceClass = (Class<? extends FlowElement>) nodeType
.getBpmnType()
.getInstance();
element = instanceClass.getDeclaredConstructor().newInstance();
element.setId(validId); element.setId(validId);
element.setName(node.getNodeName()); element.setName(node.getNodeName());
} break;
} }
// 步骤2.5配置节点的特定属性传递原始节点ID和sanitized ID // 步骤2.5配置节点的特定属性传递原始节点ID和sanitized ID
@ -162,7 +168,7 @@ public class BpmnConverter {
// 步骤2.6将节点添加到流程中 // 步骤2.6将节点添加到流程中
process.addFlowElement(element); process.addFlowElement(element);
} catch (Exception e) { } catch (Exception e) {
log.error("节点转换失败: {}", node.getNodeName(), e); log.error("节点转换失败: nodeType={}, nodeName={}", node.getNodeType(), node.getNodeName(), e);
throw new RuntimeException("节点转换失败: " + e.getMessage(), e); throw new RuntimeException("节点转换失败: " + e.getMessage(), e);
} }
} }
@ -195,8 +201,13 @@ public class BpmnConverter {
// 审批节点UserTask的特殊配置 // 审批节点UserTask的特殊配置
configureUserTask((UserTask) element, node, extensionElements, validId); configureUserTask((UserTask) element, node, extensionElements, validId);
} else if (element instanceof ServiceTask) { } else if (element instanceof ServiceTask) {
// 服务任务节点配置
configureServiceTask((ServiceTask) element, node, process, extensionElements, validId); configureServiceTask((ServiceTask) element, node, process, extensionElements, validId);
} else if (element instanceof Gateway) {
// 网关节点只需要设置执行监听器
element.setExtensionElements(extensionElements);
} else { } else {
// 其他节点开始事件结束事件等
element.setExtensionElements(extensionElements); element.setExtensionElements(extensionElements);
} }
} }
@ -632,33 +643,54 @@ public class BpmnConverter {
* @return 创建的网关节点 * @return 创建的网关节点
*/ */
private Gateway createGatewayElement(WorkflowDefinitionGraphNode node, String validId) { private Gateway createGatewayElement(WorkflowDefinitionGraphNode node, String validId) {
// if (node.getPanelVariables() == null) { try {
// throw new IllegalArgumentException("Gateway node must have panel variables"); // inputMapping 中获取 gatewayType
// } if (node.getInputMapping() == null || !node.getInputMapping().containsKey("gatewayType")) {
// throw new IllegalArgumentException("Gateway node must have gatewayType in inputMapping");
// String gatewayTypeCode = node.getPanelVariables().get("gatewayType").asText(); }
// GatewayTypeEnums gatewayType = GatewayTypeEnums.fromCode(gatewayTypeCode);
// Object gatewayTypeObj = node.getInputMapping().get("gatewayType");
// Gateway gateway; String gatewayTypeCode;
// switch (gatewayType) {
// case EXCLUSIVE_GATEWAY: // 处理不同类型的值可能是 String JsonNode
// gateway = new ExclusiveGateway(); if (gatewayTypeObj instanceof String) {
// break; gatewayTypeCode = (String) gatewayTypeObj;
// case PARALLEL_GATEWAY: } else if (gatewayTypeObj instanceof JsonNode) {
// gateway = new ParallelGateway(); gatewayTypeCode = ((JsonNode) gatewayTypeObj).asText();
// break; } else {
// case INCLUSIVE_GATEWAY: gatewayTypeCode = gatewayTypeObj.toString();
// gateway = new InclusiveGateway(); }
// break;
// default: log.debug("Creating gateway: type={}, name={}", gatewayTypeCode, node.getNodeName());
// throw new IllegalArgumentException("Unsupported gateway type: " + gatewayType);
// } // 根据网关类型创建对应的网关实例
// GatewayTypeEnums gatewayType = GatewayTypeEnums.fromCode(gatewayTypeCode);
// gateway.setId(validId); Gateway gateway;
// gateway.setName(node.getNodeName());
// switch (gatewayType) {
// return gateway; case EXCLUSIVE_GATEWAY:
return null; gateway = new ExclusiveGateway();
break;
case PARALLEL_GATEWAY:
gateway = new ParallelGateway();
break;
case INCLUSIVE_GATEWAY:
gateway = new InclusiveGateway();
break;
default:
throw new IllegalArgumentException("Unsupported gateway type: " + gatewayType);
}
// 设置基本属性
gateway.setId(validId);
gateway.setName(node.getNodeName());
return gateway;
} catch (Exception e) {
log.error("Failed to create gateway element: {}", node.getNodeName(), e);
throw new RuntimeException("Failed to create gateway element: " + e.getMessage(), e);
}
} }
/** /**
@ -669,6 +701,32 @@ public class BpmnConverter {
* @param process 流程定义 * @param process 流程定义
*/ */
private void convertEdgesToSequenceFlows(List<WorkflowDefinitionGraphEdge> edges, Map<String, String> idMapping, Process process) { private void convertEdgesToSequenceFlows(List<WorkflowDefinitionGraphEdge> edges, Map<String, String> idMapping, Process process) {
// 步骤1第一次遍历找出所有 DEFAULT 类型的边并设置到对应网关的 default 属性
Map<String, String> gatewayDefaultFlows = new HashMap<>();
for (WorkflowDefinitionGraphEdge edge : edges) {
if (edge.getConfig() != null && edge.getConfig().getCondition() != null) {
if ("DEFAULT".equals(edge.getConfig().getCondition().getType())) {
// 记录哪个网关节点的默认路径是哪条边
String sourceNodeId = idMapping.get(edge.getFrom());
gatewayDefaultFlows.put(sourceNodeId, edge.getId());
log.debug("网关 {} 的默认路径为边 {}", sourceNodeId, edge.getId());
}
}
}
// 步骤2设置网关的 default 属性
for (Map.Entry<String, String> entry : gatewayDefaultFlows.entrySet()) {
String gatewayId = entry.getKey();
String defaultFlowId = entry.getValue();
FlowElement element = process.getFlowElement(gatewayId);
if (element instanceof Gateway) {
((Gateway) element).setDefaultFlow(defaultFlowId);
log.debug("设置网关 {} 的默认路径: {}", gatewayId, defaultFlowId);
}
}
// 步骤3第二次遍历创建所有的 SequenceFlow
for (WorkflowDefinitionGraphEdge edge : edges) { for (WorkflowDefinitionGraphEdge edge : edges) {
log.debug("转换连线: from {} to {}", edge.getFrom(), edge.getTo()); log.debug("转换连线: from {} to {}", edge.getFrom(), edge.getTo());
@ -679,11 +737,20 @@ public class BpmnConverter {
flow.setSourceRef(idMapping.get(edge.getFrom())); flow.setSourceRef(idMapping.get(edge.getFrom()));
flow.setTargetRef(idMapping.get(edge.getTo())); flow.setTargetRef(idMapping.get(edge.getTo()));
// 处理条件 // 步骤4处理条件DEFAULT 类型不需要设置 conditionExpression
if (edge.getConfig() != null && edge.getConfig().getCondition() != null) { if (edge.getConfig() != null && edge.getConfig().getCondition() != null) {
if ("EXPRESSION".equals(edge.getConfig().getCondition().getType())) { String conditionType = edge.getConfig().getCondition().getType();
if ("EXPRESSION".equals(conditionType)) {
// EXPRESSION 类型设置条件表达式
String expression = edge.getConfig().getCondition().getExpression(); String expression = edge.getConfig().getCondition().getExpression();
flow.setConditionExpression(expression); if (expression != null && !expression.trim().isEmpty()) {
flow.setConditionExpression(expression);
log.debug("设置边 {} 的条件表达式: {}", edge.getId(), expression);
}
} else if ("DEFAULT".equals(conditionType)) {
// DEFAULT 类型不设置 conditionExpression已在步骤2中设置网关的 default 属性
log.debug("边 {} 为默认路径,不设置条件表达式", edge.getId());
} }
} }

View File

@ -659,26 +659,28 @@ CREATE TABLE workflow_node_instance
CONSTRAINT FK_workflow_node_instance_instance FOREIGN KEY (workflow_instance_id) REFERENCES workflow_instance (id) CONSTRAINT FK_workflow_node_instance_instance FOREIGN KEY (workflow_instance_id) REFERENCES workflow_instance (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流节点实例表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流节点实例表';
-- 工作流日志表 -- 工作流节点日志表
CREATE TABLE workflow_log CREATE TABLE workflow_node_log
( (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL COMMENT '创建人', create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间', create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL COMMENT '更新人', update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间', update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
workflow_instance_id BIGINT NULL COMMENT '工作流实例ID', node_instance_id BIGINT NOT NULL COMMENT '节点实例ID关联 workflow_node_instance.id',
node_instance_id BIGINT NULL COMMENT '节点实例ID', sequence_id BIGINT NOT NULL COMMENT '日志序号保证同一节点内日志有序从1开始递增',
log_type VARCHAR(32) NOT NULL COMMENT '日志类型', timestamp BIGINT NOT NULL COMMENT '时间戳Unix毫秒',
log_level VARCHAR(32) NOT NULL COMMENT '日志级别', level VARCHAR(10) NOT NULL COMMENT '日志级别INFO, WARN, ERROR, DEBUG',
content TEXT NOT NULL COMMENT '日志内容', source VARCHAR(20) NOT NULL COMMENT '日志来源JENKINS, FLOWABLE, SHELL, NOTIFICATION',
message TEXT NOT NULL COMMENT '日志内容',
CONSTRAINT FK_workflow_log_instance FOREIGN KEY (workflow_instance_id) REFERENCES workflow_instance (id), INDEX idx_node_seq (node_instance_id, sequence_id),
CONSTRAINT FK_workflow_log_node_instance FOREIGN KEY (node_instance_id) REFERENCES workflow_node_instance (id) INDEX idx_node_time (node_instance_id, timestamp),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流日志表'; CONSTRAINT FK_workflow_node_log_instance FOREIGN KEY (node_instance_id) REFERENCES workflow_node_instance (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流节点日志表';
-- -------------------------------------------------------------------------------------- -- --------------------------------------------------------------------------------------
-- 项目管理相关表 -- 项目管理相关表