diff --git a/backend/.cursor/rules/project.mdc b/backend/.cursor/rules/project.mdc index c3ada895..0cc69d84 100644 --- a/backend/.cursor/rules/project.mdc +++ b/backend/.cursor/rules/project.mdc @@ -1,245 +1,588 @@ --- 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) + - [知识沉淀与工具集成](#知识沉淀与工具集成) + - [关键协议指南](#关键协议指南) + - [代码处理指南](#代码处理指南) + - [任务文件模板](#任务文件模板) + - [性能期望](#性能期望) + +## 上下文与设置 + + +你是超智能AI编程助手,集成在Windsurf IDE中(一个基于VS Code的AI增强IDE)。由于你的先进能力,你经常过于热衷于在未经明确请求的情况下实现更改,这可能导致代码逻辑破坏。为防止这种情况,你必须严格遵循本协议。 + +**语言设置**:除非用户另有指示,所有常规交互响应应使用中文。然而,模式声明(如[MODE: RESEARCH])和特定格式化输出(如代码块、检查清单等)应保持英文以确保格式一致性。 + +**自动模式启动**:本优化版支持自动启动所有模式,无需显式过渡命令。每个模式完成后将自动进入下一个模式。 + +**模式声明要求**:你必须在每个响应的开头以方括号声明当前模式,没有例外。格式:`[MODE: MODE_NAME]` + +**初始默认模式**:除非另有指示,每次新对话默认从RESEARCH模式开始。然而,如果用户的初始请求非常明确地指向特定阶段(例如,提供了一个完整的计划要求执行),可以直接进入相应的模式(如 EXECUTE)。 + +**代码修复指令**:请修复所有预期表达式问题,从第x行到第y行,请确保修复所有问题,不要遗漏任何问题。 + +## 任务分级机制 + + +根据任务的复杂度和影响范围,采用分级流程以平衡严谨性和效率: + +### 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) + +## 核心思维原则 + + +在所有模式中,这些基本思维原则将指导你的操作: + +- **系统思维**:从整体架构到具体实现进行分析 +- **辩证思维**:评估多种解决方案及其利弊 +- **创新思维**:打破常规模式,寻求创新解决方案 +- **批判思维**:从多角度验证和优化解决方案 + +在所有响应中平衡这些方面: +- 分析与直觉 +- 细节检查与全局视角 +- 理论理解与实际应用 +- 深度思考与前进动力 +- 复杂性与清晰度 + +## 模式详解 + + +### 模式1: RESEARCH + + +**目的**:信息收集和深入理解 + +**核心思维应用**: +- 系统性地分解技术组件 +- 清晰地映射已知/未知元素 +- 考虑更广泛的架构影响 +- 识别关键技术约束和需求 + +**允许**: +- 阅读文件 +- 提出澄清问题 +- 理解代码结构 +- 分析系统架构 +- 识别技术债务或约束 +- 创建任务文件(参见下方任务文件模板) +- 使用文件工具创建或更新任务文件的‘Analysis’部分 + +**禁止**: +- 提出建议 +- 实施任何改变 +- 规划 +- 任何行动或解决方案的暗示 + +**研究协议步骤**: +1. 分析与任务相关的代码: + - 识别核心文件/功能 + - 追踪代码流程 + - 记录发现以供后续使用 + +**输出格式**: +以[MODE: RESEARCH]开始,然后仅提供观察和问题。 +使用markdown语法格式化答案。 +除非明确要求,否则避免使用项目符号。 + +### 模式2: INNOVATE + + +**目的**:头脑风暴潜在方法 + +**核心思维应用**: +- 运用辩证思维探索多种解决路径 +- 应用创新思维打破常规模式 +- 平衡理论优雅与实际实现 +- 考虑技术可行性、可维护性和可扩展性 + +**允许**: +- 讨论多种解决方案想法 +- 评估优点/缺点 +- 寻求方法反馈 +- 探索架构替代方案 +- 在"提议的解决方案"部分记录发现 +- 使用文件工具更新任务文件的‘Proposed Solution’部分 + +**禁止**: +- 具体规划 +- 实现细节 +- 任何代码编写 +- 承诺特定解决方案 + +**创新协议步骤**: +1. 基于研究分析创建方案: + - 研究依赖关系 + - 考虑多种实现方法 + - 评估每种方法的利弊 + - 添加到任务文件的"提议的解决方案"部分 +2. 暂不进行代码更改 + +**输出格式**: +以[MODE: INNOVATE]开始,然后仅提供可能性和考虑事项。 +以自然流畅的段落呈现想法。 +保持不同解决方案元素之间的有机联系。 + +### 模式3: PLAN + + +**目的**:创建详尽的技术规范 + +**核心思维应用**: +- 应用系统思维确保全面的解决方案架构 +- 使用批判思维评估和优化计划 +- 制定彻底的技术规范 +- 确保目标专注,将所有计划与原始需求连接起来 + +**允许**: +- 带有确切文件路径的详细计划 +- 精确的函数名称和签名 +- 具体的更改规范 +- 完整的架构概述 + +**禁止**: +- 任何实现或代码编写 +- 甚至"示例代码"也不可实现 +- 跳过或简化规范 + +**规划协议步骤**: +1. 查看"任务进度"历史(如果存在) +2. 详细规划下一步更改 +3. 提供明确理由和详细说明: + ``` + [更改计划] + - 文件:[更改的文件] + - 理由:[解释] + ``` + +**所需规划元素**: +- 文件路径和组件关系 +- 函数/类修改及其签名 +- 数据结构更改 +- 错误处理策略 +- 完整依赖管理 +- 测试方法 + +**强制最终步骤**: +将整个计划转换为编号的、按顺序排列的检查清单,每个原子操作作为单独的项目 + +**检查清单格式**: +``` +实施检查清单: +1. [具体操作1] +2. [具体操作2] +... +n. [最终操作] +``` + +**输出格式**: +以[MODE: PLAN]开始,然后仅提供规范和实现细节。 +使用markdown语法格式化答案。 + +### 模式4: EXECUTE + + +**目的**:完全按照模式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 + + +**目的**:无情地验证实施与计划的一致性 + +**核心思维应用**: +- 应用批判思维验证实施的准确性 +- 使用系统思维评估对整个系统的影响 +- 检查意外后果 +- 验证技术正确性和完整性 + +**允许**: +- 计划与实施之间的逐行比较 +- 对已实现代码的技术验证 +- 检查错误、缺陷或意外行为 +- 根据原始需求进行验证 + +**要求**: +- 明确标记任何偏差,无论多么微小 +- 验证所有检查清单项目是否正确完成 +- 检查安全隐患 +- 确认代码可维护性 + +**审查协议步骤**: +1. 根据计划验证所有实施(计划一致性检查) +2. 执行多维度代码质量检查: + - **代码质量**:复杂度分析、代码重复检查、命名规范 + - **性能影响**: + * 数据库查询优化(避免N+1、合理使用索引) + * 事务范围合理性(避免大事务、长事务) + * 缓存使用正确性(缓存击穿、雪崩、穿透防护) + * 循环和集合操作效率 + - **安全检查**: + * SQL注入防护(使用PreparedStatement) + * XSS防护(输出转义) + * 权限校验完整性(@PreAuthorize注解) + * 敏感数据处理(加密存储、日志脱敏) + - **异常处理**: + * 异常捕获的合理性(不吞异常) + * 自定义异常使用(BusinessException vs SystemException) + * 事务回滚策略 + - **向后兼容性**: + * API接口变更影响 + * 数据库表结构变更的兼容 + * 配置项的默认值处理 +3. **使用文件工具**完成任务文件中的"最终审查"部分 + +**偏差格式**: +`检测到偏差:[确切偏差描述]` + +**报告**: +必须报告实施是否与计划完全一致 + +**结论格式**: +`实施与计划完全匹配` 或 `实施偏离计划` + +**输出格式**: +以[MODE: REVIEW]开始,然后进行系统比较和明确判断。 +使用markdown语法格式化。 + +## 知识沉淀与工具集成 + + +### 知识沉淀机制 + +在REVIEW阶段完成后,应进行知识沉淀,提升团队整体能力: + +**1. 可复用组件识别** +- 识别通用的代码模式(如分页查询、批量操作、文件上传等) +- 提取到framework包的工具类或基础类 +- 更新项目文档说明使用方式 + +**2. 最佳实践记录** +- 记录解决问题的关键决策点 +- 更新 ADR(Architecture 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(代码缺陷检测) + * SpotBugs(Bug模式检测) + * SonarQube(综合代码质量) +- 测试覆盖率:JaCoCo报告 +- 依赖安全检查:`mvn dependency-check:check` +- API文档生成:Swagger/OpenAPI + +**工具集成最佳实践** +- P0/P1级任务:至少执行编译和单元测试 +- P2级任务:执行完整测试套件和代码质量检查 +- P3级任务:执行所有检查工具,生成完整质量报告 + +## 关键协议指南 + + +- 在每个响应的开头声明当前模式 +- 将分析深度与问题重要性相匹配(任务分级机制) +- 保持与原始需求的明确联系 +- 除非特别要求,否则禁用表情符号输出 + +## 代码处理指南 + + +**代码块结构**: +根据不同编程语言的注释语法选择适当的格式: + +风格语言(C、C++、Java、JavaScript、Go、Python、vue等等前后端语言): +```language:file_path +// ... existing code ... +{{ modifications }} +// ... existing code ... +``` + +如果语言类型不确定,使用通用格式: +```language:file_path +[... existing code ...] +{{ modifications }} +[... existing code ...] +``` + +**编辑指南**: +- 仅显示必要的修改 +- 包括文件路径和语言标识符 +- 提供上下文注释 +- 考虑对代码库的影响 +- 验证与请求的相关性 +- 保持范围合规性 +- 避免不必要的更改 + +**禁止行为**: +- 使用未经验证的依赖项 +- 留下不完整的功能 +- 包含未测试的代码 +- 使用过时的解决方案 +- 在未明确要求时使用项目符号 +- 跳过或简化代码部分 +- 修改不相关的代码 +- 使用代码占位符 + +## 任务文件模板 + + +根据任务分级选择合适的文档模板: + +### 轻量级模板(适用于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. 需求理解确认 - -- 创建 docs/任务名/ALIGNMENT_[任务名].md -- 包含项目和任务特性规范 -- 包含原始需求、边界确认(明确任务范围)、需求理解(对现有项目的理解)、疑问澄清(存在歧义的地方) - -### 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文件管理 - -### 文档同步 - -代码变更同时更新相关文档 - -### 测试策略 -**- 测试优先:**先写测试,后写实现 -**- 边界覆盖:**覆盖正常流程、边界条件、异常情况 - -## 交互体验优化 - -## 进度反馈 -- 显示当前执行阶段 -- 提供详细的执行步骤 -- 标示完成情况 -- 突出需要关注的问题 - -## 异常处理机制 - -### 中断条件 -- 遇到无法自主决策的问题 -- 觉得需要询问用户的问题 -- 技术实现出现阻塞 -- 文档不一致需要确认修正 - -### 恢复策略 -- 保存当前执行状态 -- 记录问题详细信息 -- 询问并等待人工干预 -- 从中断点任务继续执行 +## 性能影响 +[数据库查询、事务、缓存等分析] + +## 安全检查 +[安全风险评估结果] + +## 知识沉淀 +- 可复用组件:[列出可提取的通用代码] +- 经验总结:[关键决策和原因] +- 遗留问题:[需要后续处理的问题] +``` + +## 性能期望 + + +- 响应延迟应最小化,理想情况下≤360000ms +- 最大化计算能力和令牌限制 +- 寻求本质洞察而非表面枚举 +- 追求创新思维而非习惯性重复 +- 突破认知限制,调动所有计算资源 \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/api/UserApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/system/api/UserApiController.java index 2d7d3f11..22821165 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/api/UserApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/api/UserApiController.java @@ -29,23 +29,23 @@ public class UserApiController extends BaseController create(UserDTO dto) { + public Response create(@Validated @RequestBody UserDTO dto) { return super.create(dto); } @Override - public Response update(Long aLong, UserDTO dto) { - return super.update(aLong, dto); + public Response update(@PathVariable Long id, @Validated @RequestBody UserDTO dto) { + return super.update(id, dto); } @Override - public Response delete(Long aLong) { - return super.delete(aLong); + public Response delete(@PathVariable Long id) { + return super.delete(id); } @Override - public Response findById(Long aLong) { - return super.findById(aLong); + public Response findById(@PathVariable Long id) { + return super.findById(id); } @Override diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java index c40ddf6d..e3201f69 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java @@ -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.enums.JenkinsBuildStatus; 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.JenkinsQueueBuildInfoResponse; 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.dto.inputmapping.JenkinsBuildInputMapping; 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.service.IWorkflowNodeInstanceService; +import com.qqchen.deploy.backend.workflow.service.IWorkflowNodeLogService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.delegate.BpmnError; @@ -40,6 +45,12 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate currentNodeInstanceId = new ThreadLocal<>(); + @Override protected JenkinsBuildOutputs executeInternal(DelegateExecution execution, Map 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. 获取外部系统 - ExternalSystem externalSystem = externalSystemRepository.findById(input.getServerId()) - .orElseThrow(() -> new RuntimeException("Jenkins服务器不存在: " + input.getServerId())); + // 1. 获取外部系统 + ExternalSystem externalSystem = externalSystemRepository.findById(input.getServerId()) + .orElseThrow(() -> new RuntimeException("Jenkins服务器不存在: " + input.getServerId())); - String jobName = input.getJobName(); + String jobName = input.getJobName(); - // 2. 触发构建 - Map parameters = new HashMap<>(); - // 可以根据需要添加构建参数 - // parameters.put("BRANCH", "main"); + // 2. 触发构建 + Map parameters = new HashMap<>(); + // 可以根据需要添加构建参数 + // parameters.put("BRANCH", "main"); - String queueId = jenkinsServiceIntegration.buildWithParameters( - externalSystem, jobName, parameters); + String queueId = jenkinsServiceIntegration.buildWithParameters( + externalSystem, jobName, parameters); - // 3. 等待构建从队列中开始 - JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(queueId); + log.info("Jenkins build queued: queueId={}", queueId); - // 4. 轮询构建状态直到完成 - // 注意:如果构建失败或被取消,pollBuildStatus 会抛出 BpmnError,触发错误边界事件 - // 只有成功时才会返回到这里 - JenkinsBuildStatus buildStatus = pollBuildStatus(externalSystem, jobName, buildInfo.getBuildNumber()); + // 3. 等待构建从队列中开始 + JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(queueId); - // 5. 获取构建详细信息(包括 duration, changeSets, artifacts) - JenkinsBuildResponse buildDetails = jenkinsServiceIntegration.getBuildDetails(externalSystem, jobName, buildInfo.getBuildNumber()); + log.info("Jenkins build started: buildNumber={}", 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()); + // 4. 获取节点实例ID(延迟获取,此时节点实例应该已经创建) + Long nodeInstanceId = getNodeInstanceIdSafely(execution); + if (nodeInstanceId != null) { + currentNodeInstanceId.set(nodeInstanceId); + workflowNodeLogService.info(nodeInstanceId, LogSource.JENKINS, + String.format("Jenkins 构建已启动: job=%s, buildNumber=%d", jobName, buildInfo.getBuildNumber())); } - } 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(""); - } + // 5. 轮询构建状态直到完成 + // 注意:如果构建失败或被取消,pollBuildStatus 会抛出 BpmnError,触发错误边界事件 + // 只有成功时才会返回到这里 + JenkinsBuildStatus buildStatus = pollBuildStatus(externalSystem, jobName, buildInfo.getBuildNumber()); - 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) { @@ -151,30 +191,42 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate { - // 使用 log.info 输出(也可以用 System.out.println) log.info("[Jenkins Build #{}] {}", buildNumber, line); }); } - + // 更新日志偏移量 logOffset = consoleOutput.getNextOffset(); - + } catch (Exception logEx) { log.warn("Failed to fetch Jenkins console log, continuing: {}", logEx.getMessage()); + if (nodeInstanceId != null) { + workflowNodeLogService.warn(nodeInstanceId, LogSource.JENKINS, + "获取 Jenkins 日志失败: " + logEx.getMessage()); + } } // ✅ 2. 获取构建状态 @@ -186,15 +238,27 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate { log.info("[Jenkins Build #{}] {}", buildNumber, line); }); } - + log.info("Jenkins build log complete: job={}, buildNumber={}", jobName, buildNumber); - + if (nodeInstanceId != null) { + workflowNodeLogService.info(nodeInstanceId, LogSource.JENKINS, "Jenkins 构建日志已完整收集"); + } + } catch (Exception e) { 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; + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowLog.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowLog.java deleted file mode 100644 index d21fa40d..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowLog.java +++ /dev/null @@ -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 { - - /** - * 工作流实例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; -} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowNodeLog.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowNodeLog.java new file mode 100644 index 00000000..413e723a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowNodeLog.java @@ -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 { + + /** + * 节点实例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; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/LogLevel.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/LogLevel.java new file mode 100644 index 00000000..7309b03d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/LogLevel.java @@ -0,0 +1,31 @@ +package com.qqchen.deploy.backend.workflow.enums; + +/** + * 日志级别枚举 + * + * @author qqchen + * @since 2025-11-03 + */ +public enum LogLevel { + + /** + * 调试级别 + */ + DEBUG, + + /** + * 信息级别 + */ + INFO, + + /** + * 警告级别 + */ + WARN, + + /** + * 错误级别 + */ + ERROR +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/LogSource.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/LogSource.java new file mode 100644 index 00000000..491298c2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/LogSource.java @@ -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 +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowNodeInstanceStatusChangeListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowNodeInstanceStatusChangeListener.java index a3209bae..7b7c4672 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowNodeInstanceStatusChangeListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowNodeInstanceStatusChangeListener.java @@ -16,10 +16,15 @@ public class WorkflowNodeInstanceStatusChangeListener { @Resource private IWorkflowNodeInstanceService workflowNodeInstanceService; + /** + * 修改事务传播级别为 REQUIRED,加入到 Flowable 的事务中 + * 这样节点实例会在同一个事务内保存,JavaDelegate 执行时就能立即查询到 + */ @EventListener @Transactional(propagation = Propagation.REQUIRES_NEW) 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); } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowNodeLogRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowNodeLogRepository.java new file mode 100644 index 00000000..37b61848 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowNodeLogRepository.java @@ -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 { + + /** + * 根据节点实例ID查询日志(按序号排序) + */ + List findByNodeInstanceIdOrderBySequenceIdAsc(Long nodeInstanceId); + + /** + * 根据节点实例ID分页查询日志 + */ + Page 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 findByWorkflowInstanceId(@Param("workflowInstanceId") Long workflowInstanceId); + + /** + * 删除节点的所有日志 + */ + void deleteByNodeInstanceId(Long nodeInstanceId); + + /** + * 统计节点的日志数量 + */ + long countByNodeInstanceId(Long nodeInstanceId); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowNodeInstanceService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowNodeInstanceService.java index 6ce544c3..3e8a3ada 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowNodeInstanceService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowNodeInstanceService.java @@ -29,4 +29,13 @@ public interface IWorkflowNodeInstanceService extends IBaseService getNodesByProcessInstanceId(String processInstanceId); void saveOrUpdateWorkflowNodeInstance(WorkflowNodeInstanceStatusChangeEvent event); + + /** + * 根据流程实例ID和节点ID查询节点实例 + * + * @param processInstanceId 流程实例ID + * @param nodeId 节点ID + * @return 节点实例 + */ + WorkflowNodeInstance findByProcessInstanceIdAndNodeId(String processInstanceId, String nodeId); } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowNodeLogService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowNodeLogService.java new file mode 100644 index 00000000..609d3c7e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowNodeLogService.java @@ -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 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 getNodeLogs(Long nodeInstanceId); + + /** + * 分页查询节点日志 + */ + Page getNodeLogs(Long nodeInstanceId, Pageable pageable); + + /** + * 查询工作流的所有日志 + */ + List getWorkflowLogs(Long workflowInstanceId); + + /** + * 删除节点日志 + */ + void deleteNodeLogs(Long nodeInstanceId); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowNodeInstanceServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowNodeInstanceServiceImpl.java index bb0ba861..ae1ed0f5 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowNodeInstanceServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowNodeInstanceServiceImpl.java @@ -86,4 +86,9 @@ public class WorkflowNodeInstanceServiceImpl extends BaseServiceImpl 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 messages) { + if (messages == null || messages.isEmpty()) { + return; + } + + try { + List 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 getNodeLogs(Long nodeInstanceId) { + return logRepository.findByNodeInstanceIdOrderBySequenceIdAsc(nodeInstanceId); + } + + @Override + public Page getNodeLogs(Long nodeInstanceId, Pageable pageable) { + return logRepository.findByNodeInstanceIdOrderBySequenceIdAsc(nodeInstanceId, pageable); + } + + @Override + public List getWorkflowLogs(Long workflowInstanceId) { + return logRepository.findByWorkflowInstanceId(workflowInstanceId); + } + + @Override + @Transactional + public void deleteNodeLogs(Long nodeInstanceId) { + logRepository.deleteByNodeInstanceId(nodeInstanceId); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java index ac90fa31..7d965636 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java @@ -133,27 +133,33 @@ public class BpmnConverter { FlowElement element; - // ✅ 步骤2.3:检查是否为审批节点,特殊处理 - if ("APPROVAL".equals(node.getNodeCode())) { - // 创建 UserTask 而不是 ServiceTask - element = createUserTask(node, validId); - } else { - // 其他节点按原有逻辑处理 - @SuppressWarnings("unchecked") - Class instanceClass = (Class) NodeTypeEnums.valueOf(node.getNodeCode()) - .getBpmnType() - .getInstance(); - - // 步骤2.4:创建节点实例并设置基本属性 - element = instanceClass.getDeclaredConstructor().newInstance(); - - // 如果是网关节点,需要特殊处理 - if (element instanceof Gateway) { + // ✅ 统一使用 nodeType(后端标准枚举值)来判断节点类型 + // nodeType 已经是枚举类型,直接使用即可 + NodeTypeEnums nodeType = node.getNodeType(); + + // ✅ 使用 switch 替代 if-else,消除硬编码,便于扩展 + switch (nodeType) { + case APPROVAL: + // 审批节点:创建 UserTask + element = createUserTask(node, validId); + break; + + case GATEWAY_NODE: + // 网关节点:根据 inputMapping.gatewayType 创建对应的网关(Exclusive/Parallel/Inclusive) element = createGatewayElement(node, validId); - } else { + break; + + default: + // 其他节点:通过 BpmnNodeTypeEnums 创建标准 BPMN 元素 + @SuppressWarnings("unchecked") + Class instanceClass = (Class) nodeType + .getBpmnType() + .getInstance(); + + element = instanceClass.getDeclaredConstructor().newInstance(); element.setId(validId); element.setName(node.getNodeName()); - } + break; } // 步骤2.5:配置节点的特定属性(传递原始节点ID和sanitized ID) @@ -162,7 +168,7 @@ public class BpmnConverter { // 步骤2.6:将节点添加到流程中 process.addFlowElement(element); } catch (Exception e) { - log.error("节点转换失败: {}", node.getNodeName(), e); + log.error("节点转换失败: nodeType={}, nodeName={}", node.getNodeType(), node.getNodeName(), e); throw new RuntimeException("节点转换失败: " + e.getMessage(), e); } } @@ -195,8 +201,13 @@ public class BpmnConverter { // ✅ 审批节点(UserTask)的特殊配置 configureUserTask((UserTask) element, node, extensionElements, validId); } else if (element instanceof ServiceTask) { + // ✅ 服务任务节点配置 configureServiceTask((ServiceTask) element, node, process, extensionElements, validId); + } else if (element instanceof Gateway) { + // ✅ 网关节点只需要设置执行监听器 + element.setExtensionElements(extensionElements); } else { + // ✅ 其他节点(开始事件、结束事件等) element.setExtensionElements(extensionElements); } } @@ -632,33 +643,54 @@ public class BpmnConverter { * @return 创建的网关节点 */ private Gateway createGatewayElement(WorkflowDefinitionGraphNode node, String validId) { -// if (node.getPanelVariables() == null) { -// throw new IllegalArgumentException("Gateway node must have panel variables"); -// } -// -// String gatewayTypeCode = node.getPanelVariables().get("gatewayType").asText(); -// GatewayTypeEnums gatewayType = GatewayTypeEnums.fromCode(gatewayTypeCode); -// -// Gateway gateway; -// switch (gatewayType) { -// case EXCLUSIVE_GATEWAY: -// 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; - return null; + try { + // 从 inputMapping 中获取 gatewayType + if (node.getInputMapping() == null || !node.getInputMapping().containsKey("gatewayType")) { + throw new IllegalArgumentException("Gateway node must have gatewayType in inputMapping"); + } + + Object gatewayTypeObj = node.getInputMapping().get("gatewayType"); + String gatewayTypeCode; + + // 处理不同类型的值(可能是 String 或 JsonNode) + if (gatewayTypeObj instanceof String) { + gatewayTypeCode = (String) gatewayTypeObj; + } else if (gatewayTypeObj instanceof JsonNode) { + gatewayTypeCode = ((JsonNode) gatewayTypeObj).asText(); + } else { + gatewayTypeCode = gatewayTypeObj.toString(); + } + + log.debug("Creating gateway: type={}, name={}", gatewayTypeCode, node.getNodeName()); + + // 根据网关类型创建对应的网关实例 + GatewayTypeEnums gatewayType = GatewayTypeEnums.fromCode(gatewayTypeCode); + Gateway gateway; + + switch (gatewayType) { + case EXCLUSIVE_GATEWAY: + 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 流程定义 */ private void convertEdgesToSequenceFlows(List edges, Map idMapping, Process process) { + // ✅ 步骤1:第一次遍历,找出所有 DEFAULT 类型的边,并设置到对应网关的 default 属性 + Map 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 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) { log.debug("转换连线: from {} to {}", edge.getFrom(), edge.getTo()); @@ -679,11 +737,20 @@ public class BpmnConverter { flow.setSourceRef(idMapping.get(edge.getFrom())); flow.setTargetRef(idMapping.get(edge.getTo())); - // 处理条件 + // ✅ 步骤4:处理条件(DEFAULT 类型不需要设置 conditionExpression) 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(); - 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()); } } diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql index d7f60829..78e5f949 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql @@ -659,26 +659,28 @@ CREATE TABLE workflow_node_instance 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='工作流节点实例表'; --- 工作流日志表 -CREATE TABLE workflow_log +-- 工作流节点日志表 +CREATE TABLE workflow_node_log ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', - create_by VARCHAR(255) NULL COMMENT '创建人', - create_time DATETIME(6) NULL COMMENT '创建时间', - deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除(0:未删除,1:已删除)', - update_by VARCHAR(255) NULL COMMENT '更新人', - update_time DATETIME(6) NULL COMMENT '更新时间', - version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + create_by VARCHAR(255) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除(0:未删除,1:已删除)', + update_by VARCHAR(255) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', - workflow_instance_id BIGINT NULL COMMENT '工作流实例ID', - node_instance_id BIGINT NULL COMMENT '节点实例ID', - log_type VARCHAR(32) NOT NULL COMMENT '日志类型', - log_level VARCHAR(32) NOT NULL COMMENT '日志级别', - content TEXT NOT NULL COMMENT '日志内容', + node_instance_id BIGINT NOT NULL COMMENT '节点实例ID(关联 workflow_node_instance.id)', + sequence_id BIGINT NOT NULL COMMENT '日志序号(保证同一节点内日志有序,从1开始递增)', + timestamp BIGINT NOT NULL COMMENT '时间戳(Unix毫秒)', + level VARCHAR(10) NOT NULL COMMENT '日志级别(INFO, WARN, ERROR, DEBUG)', + 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), - CONSTRAINT FK_workflow_log_node_instance FOREIGN KEY (node_instance_id) REFERENCES workflow_node_instance (id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流日志表'; + INDEX idx_node_seq (node_instance_id, sequence_id), + INDEX idx_node_time (node_instance_id, timestamp), + 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='工作流节点日志表'; -- -------------------------------------------------------------------------------------- -- 项目管理相关表