Преглед на файлове

feat: 完整流程审批引擎后端代码 + EmbeddedRedis自动兜底 + 构建文档

ye-zhaojia преди 9 часа
родител
ревизия
78a3651c9b
променени са 100 файла, в които са добавени 5049 реда и са изтрити 0 реда
  1. 38 0
      .gitignore
  2. 103 0
      README.md
  3. 130 0
      pom.xml
  4. 16 0
      src/main/java/com/qqflow/engine/QqFlowEngineApplication.java
  5. 24 0
      src/main/java/com/qqflow/engine/common/PageResult.java
  6. 41 0
      src/main/java/com/qqflow/engine/common/Result.java
  7. 19 0
      src/main/java/com/qqflow/engine/common/exception/BusinessException.java
  8. 44 0
      src/main/java/com/qqflow/engine/common/exception/GlobalExceptionHandler.java
  9. 80 0
      src/main/java/com/qqflow/engine/common/util/JwtUtils.java
  10. 31 0
      src/main/java/com/qqflow/engine/common/util/RedisCache.java
  11. 26 0
      src/main/java/com/qqflow/engine/common/util/SecurityUtils.java
  12. 26 0
      src/main/java/com/qqflow/engine/config/AsyncConfig.java
  13. 53 0
      src/main/java/com/qqflow/engine/config/EmbeddedRedisConfig.java
  14. 18 0
      src/main/java/com/qqflow/engine/config/MyBatisPlusConfig.java
  15. 33 0
      src/main/java/com/qqflow/engine/config/RedisConfig.java
  16. 19 0
      src/main/java/com/qqflow/engine/config/WebConfig.java
  17. 23 0
      src/main/java/com/qqflow/engine/config/security/AuthenticationManagerConfig.java
  18. 44 0
      src/main/java/com/qqflow/engine/config/security/JwtAuthenticationFilter.java
  19. 63 0
      src/main/java/com/qqflow/engine/config/security/LoginUser.java
  20. 19 0
      src/main/java/com/qqflow/engine/config/security/PermissionService.java
  21. 45 0
      src/main/java/com/qqflow/engine/config/security/SecurityConfig.java
  22. 27 0
      src/main/java/com/qqflow/engine/domain/flow/assembler/ApprovalRecordAssembler.java
  23. 34 0
      src/main/java/com/qqflow/engine/domain/flow/assembler/ApprovalTaskAssembler.java
  24. 34 0
      src/main/java/com/qqflow/engine/domain/flow/assembler/ProcessDefinitionAssembler.java
  25. 28 0
      src/main/java/com/qqflow/engine/domain/flow/assembler/ProcessInstanceAssembler.java
  26. 104 0
      src/main/java/com/qqflow/engine/domain/flow/controller/ApprovalTaskController.java
  27. 98 0
      src/main/java/com/qqflow/engine/domain/flow/controller/ProcessDefinitionController.java
  28. 82 0
      src/main/java/com/qqflow/engine/domain/flow/controller/ProcessInstanceController.java
  29. 68 0
      src/main/java/com/qqflow/engine/domain/flow/dto/ApprovalRecordDTO.java
  30. 102 0
      src/main/java/com/qqflow/engine/domain/flow/dto/ApprovalTaskDTO.java
  31. 19 0
      src/main/java/com/qqflow/engine/domain/flow/dto/ApproveTaskDTO.java
  32. 29 0
      src/main/java/com/qqflow/engine/domain/flow/dto/NodeProgressDTO.java
  33. 76 0
      src/main/java/com/qqflow/engine/domain/flow/dto/ProcessDefinitionDTO.java
  34. 92 0
      src/main/java/com/qqflow/engine/domain/flow/dto/ProcessInstanceDTO.java
  35. 26 0
      src/main/java/com/qqflow/engine/domain/flow/dto/ProcessProgressDTO.java
  36. 20 0
      src/main/java/com/qqflow/engine/domain/flow/dto/StartProcessDTO.java
  37. 23 0
      src/main/java/com/qqflow/engine/domain/flow/dto/TransferTaskDTO.java
  38. 25 0
      src/main/java/com/qqflow/engine/domain/flow/enums/ApprovalAction.java
  39. 22 0
      src/main/java/com/qqflow/engine/domain/flow/enums/ApprovalResult.java
  40. 30 0
      src/main/java/com/qqflow/engine/domain/flow/enums/DefinitionStatus.java
  41. 23 0
      src/main/java/com/qqflow/engine/domain/flow/enums/NodeType.java
  42. 24 0
      src/main/java/com/qqflow/engine/domain/flow/enums/ProcessEvent.java
  43. 35 0
      src/main/java/com/qqflow/engine/domain/flow/enums/ProcessStatus.java
  44. 32 0
      src/main/java/com/qqflow/engine/domain/flow/enums/TaskStatus.java
  45. 27 0
      src/main/java/com/qqflow/engine/domain/flow/event/ProcessCompletedEvent.java
  46. 29 0
      src/main/java/com/qqflow/engine/domain/flow/event/TaskAssignedEvent.java
  47. 34 0
      src/main/java/com/qqflow/engine/domain/flow/event/TaskCompletedEvent.java
  48. 69 0
      src/main/java/com/qqflow/engine/domain/flow/listener/NotificationEventListener.java
  49. 27 0
      src/main/java/com/qqflow/engine/domain/flow/mapper/ApprovalRecordMapper.java
  50. 43 0
      src/main/java/com/qqflow/engine/domain/flow/mapper/ApprovalTaskMapper.java
  51. 38 0
      src/main/java/com/qqflow/engine/domain/flow/mapper/ProcessDefinitionMapper.java
  52. 34 0
      src/main/java/com/qqflow/engine/domain/flow/mapper/ProcessInstanceMapper.java
  53. 15 0
      src/main/java/com/qqflow/engine/domain/flow/model/FlowEdge.java
  54. 12 0
      src/main/java/com/qqflow/engine/domain/flow/model/FlowModel.java
  55. 16 0
      src/main/java/com/qqflow/engine/domain/flow/model/FlowNode.java
  56. 70 0
      src/main/java/com/qqflow/engine/domain/flow/po/ApprovalRecord.java
  57. 94 0
      src/main/java/com/qqflow/engine/domain/flow/po/ApprovalTask.java
  58. 74 0
      src/main/java/com/qqflow/engine/domain/flow/po/ProcessDefinition.java
  59. 86 0
      src/main/java/com/qqflow/engine/domain/flow/po/ProcessInstance.java
  60. 30 0
      src/main/java/com/qqflow/engine/domain/flow/service/ApprovalTaskService.java
  61. 27 0
      src/main/java/com/qqflow/engine/domain/flow/service/FlowEngineService.java
  62. 43 0
      src/main/java/com/qqflow/engine/domain/flow/service/NotificationService.java
  63. 28 0
      src/main/java/com/qqflow/engine/domain/flow/service/ProcessDefinitionService.java
  64. 23 0
      src/main/java/com/qqflow/engine/domain/flow/service/ProcessInstanceService.java
  65. 214 0
      src/main/java/com/qqflow/engine/domain/flow/service/impl/ApprovalTaskServiceImpl.java
  66. 364 0
      src/main/java/com/qqflow/engine/domain/flow/service/impl/FlowEngineServiceImpl.java
  67. 160 0
      src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessDefinitionServiceImpl.java
  68. 223 0
      src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessInstanceServiceImpl.java
  69. 59 0
      src/main/java/com/qqflow/engine/domain/flow/service/impl/WeComNotificationService.java
  70. 68 0
      src/main/java/com/qqflow/engine/domain/flow/statemachine/ProcessInstanceStateMachineConfig.java
  71. 23 0
      src/main/java/com/qqflow/engine/domain/system/assembler/DeptAssembler.java
  72. 47 0
      src/main/java/com/qqflow/engine/domain/system/assembler/MenuAssembler.java
  73. 23 0
      src/main/java/com/qqflow/engine/domain/system/assembler/RoleAssembler.java
  74. 23 0
      src/main/java/com/qqflow/engine/domain/system/assembler/UserAssembler.java
  75. 66 0
      src/main/java/com/qqflow/engine/domain/system/controller/AuthController.java
  76. 63 0
      src/main/java/com/qqflow/engine/domain/system/controller/SysDeptController.java
  77. 75 0
      src/main/java/com/qqflow/engine/domain/system/controller/SysMenuController.java
  78. 97 0
      src/main/java/com/qqflow/engine/domain/system/controller/SysRoleController.java
  79. 69 0
      src/main/java/com/qqflow/engine/domain/system/controller/SysUserController.java
  80. 74 0
      src/main/java/com/qqflow/engine/domain/system/dto/DeptDTO.java
  81. 18 0
      src/main/java/com/qqflow/engine/domain/system/dto/LoginDTO.java
  82. 74 0
      src/main/java/com/qqflow/engine/domain/system/dto/MenuDTO.java
  83. 55 0
      src/main/java/com/qqflow/engine/domain/system/dto/RoleDTO.java
  84. 63 0
      src/main/java/com/qqflow/engine/domain/system/dto/UserDTO.java
  85. 43 0
      src/main/java/com/qqflow/engine/domain/system/entity/SysDept.java
  86. 41 0
      src/main/java/com/qqflow/engine/domain/system/entity/SysMenu.java
  87. 40 0
      src/main/java/com/qqflow/engine/domain/system/entity/SysRole.java
  88. 23 0
      src/main/java/com/qqflow/engine/domain/system/entity/SysRoleMenu.java
  89. 55 0
      src/main/java/com/qqflow/engine/domain/system/entity/SysUser.java
  90. 23 0
      src/main/java/com/qqflow/engine/domain/system/entity/SysUserRole.java
  91. 17 0
      src/main/java/com/qqflow/engine/domain/system/mapper/SysDeptMapper.java
  92. 22 0
      src/main/java/com/qqflow/engine/domain/system/mapper/SysMenuMapper.java
  93. 19 0
      src/main/java/com/qqflow/engine/domain/system/mapper/SysRoleMapper.java
  94. 7 0
      src/main/java/com/qqflow/engine/domain/system/mapper/SysRoleMenuMapper.java
  95. 21 0
      src/main/java/com/qqflow/engine/domain/system/mapper/SysUserMapper.java
  96. 7 0
      src/main/java/com/qqflow/engine/domain/system/mapper/SysUserRoleMapper.java
  97. 23 0
      src/main/java/com/qqflow/engine/domain/system/service/SysDeptService.java
  98. 26 0
      src/main/java/com/qqflow/engine/domain/system/service/SysMenuService.java
  99. 22 0
      src/main/java/com/qqflow/engine/domain/system/service/SysRoleService.java
  100. 35 0
      src/main/java/com/qqflow/engine/domain/system/service/SysUserService.java

+ 38 - 0
.gitignore

@@ -0,0 +1,38 @@
+# Maven
+/target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+# IDE
+.idea/
+*.iws
+*.iml
+*.ipr
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# H2 Database (dev runtime files)
+/data/
+*.mv.db
+*.lock.db
+*.trace.db
+
+# Logs
+*.log
+/logs/
+
+# Local config
+application-local.yml
+application-local.properties
+
+# Node (if any)
+node_modules/
+
+# Bundled tools
+apache-maven-3.9.6/

+ 103 - 0
README.md

@@ -0,0 +1,103 @@
+# qqflowengine-backend
+
+通用型流程审批设计与管理系统 — 后端服务
+
+## 技术栈
+
+- Java 17
+- Spring Boot 3.2.5
+- Spring Security 6 + JWT
+- MyBatis-Plus 3.5.7
+- H2 Database (开发模式文件数据库)
+- Redis (embedded-redis 自动兜底)
+- Spring State Machine 4.0
+
+## 环境要求
+
+- JDK 17+
+- Maven 3.8+(或项目内嵌的 apache-maven-3.9.6)
+- 可选:本地 Redis(如未安装,启动时会自动启动 embedded Redis)
+
+## 快速启动
+
+```bash
+cd qqflowengine-backend
+
+# 方式一:使用系统 Maven
+mvn spring-boot:run
+
+# 方式二:使用项目内嵌 Maven
+./apache-maven-3.9.6/bin/mvn spring-boot:run
+```
+
+服务启动后访问:
+- API 服务:`http://localhost:8080`
+- Swagger 文档:`http://localhost:8080/swagger-ui.html`
+
+## 开发说明
+
+### 数据库
+
+开发环境使用 **H2 文件数据库**,数据文件保存在项目根目录 `data/devdb.mv.db`:
+
+```yaml
+spring.datasource.url: jdbc:h2:file:./data/devdb;MODE=MySQL;AUTO_SERVER=TRUE
+```
+
+- `spring.sql.init.mode: always` — 每次启动都会重新执行 `schema-dev.sql` 和 `data-dev.sql`
+- 如需保留手动修改的数据,可将 `mode` 改为 `never`
+- H2 Console 未开启,如需调试可直接连接 `jdbc:h2:file:./data/devdb`
+
+### 默认账号
+
+启动后会自动初始化以下账号(密码均为 `admin123`):
+
+| 账号 | 角色 |
+|------|------|
+| admin | 超级管理员 |
+| zhangsan | 普通用户 |
+| lisi | 普通用户 |
+| wangwu | 普通用户 |
+
+### Redis
+
+- 优先连接本地 `localhost:6379`
+- 如果本地未安装 Redis,启动时会**自动启动 embedded Redis**(端口 6379)
+- 如果本地已有 Redis 运行,则直接使用外部 Redis
+
+### 构建打包
+
+```bash
+mvn clean package
+```
+
+打包后的 JAR 位于 `target/qqflowengine-backend-1.0.0.jar`,可直接运行:
+
+```bash
+java -jar target/qqflowengine-backend-1.0.0.jar
+```
+
+## 项目结构
+
+```
+src/main/java/com/qqflow/engine/
+├── common/          # 通用工具、异常、结果封装
+├── config/          # Spring 配置类
+├── domain/
+│   ├── flow/        # 流程引擎模块(定义、实例、任务、审批)
+│   └── system/      # 系统管理模块(用户、角色、菜单、部门)
+└── QqFlowEngineApplication.java
+
+src/main/resources/
+├── application.yml       # 默认配置
+├── application-dev.yml   # 开发配置
+├── application-test.yml  # 测试配置
+├── schema-dev.sql        # 开发环境表结构
+├── data-dev.sql          # 开发环境初始数据
+└── mapper/               # MyBatis XML 映射文件
+```
+
+## 注意事项
+
+1. `data/` 目录和 `target/` 目录已加入 `.gitignore`,不会被提交
+2. 如需切换 MySQL,修改 `pom.xml` 中 H2 的 scope 并配置 `application.yml` 即可

+ 130 - 0
pom.xml

@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.2.5</version>
+        <relativePath/>
+    </parent>
+    <groupId>com.qqflow</groupId>
+    <artifactId>qqflowengine-backend</artifactId>
+    <version>1.0.0</version>
+    <name>qqflowengine-backend</name>
+    <description>通用型流程审批设计与管理系统后端</description>
+    <properties>
+        <java.version>17</java.version>
+        <mybatis-plus.version>3.5.7</mybatis-plus.version>
+        <hutool.version>5.8.25</hutool.version>
+        <jjwt.version>0.12.5</jjwt.version>
+        <spring-statemachine.version>4.0.0</spring-statemachine.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.statemachine</groupId>
+            <artifactId>spring-statemachine-starter</artifactId>
+            <version>${spring-statemachine.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis-spring</artifactId>
+            <version>3.0.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>${jjwt.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>${jjwt.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId>
+            <version>${jjwt.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+            <version>2.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.github.codemonstur</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <version>1.4.3</version>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 16 - 0
src/main/java/com/qqflow/engine/QqFlowEngineApplication.java

@@ -0,0 +1,16 @@
+package com.qqflow.engine;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@EnableScheduling
+@MapperScan("com.qqflow.engine.**.mapper")
+@SpringBootApplication
+public class QqFlowEngineApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(QqFlowEngineApplication.class, args);
+    }
+}

+ 24 - 0
src/main/java/com/qqflow/engine/common/PageResult.java

@@ -0,0 +1,24 @@
+package com.qqflow.engine.common;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Schema(description = "分页响应结果")
+public class PageResult<T> {
+
+    @Schema(description = "总记录数")
+    private Long total;
+
+    @Schema(description = "数据列表")
+    private List<T> list;
+
+    public static <T> PageResult<T> of(Long total, List<T> list) {
+        PageResult<T> result = new PageResult<>();
+        result.setTotal(total);
+        result.setList(list);
+        return result;
+    }
+}

+ 41 - 0
src/main/java/com/qqflow/engine/common/Result.java

@@ -0,0 +1,41 @@
+package com.qqflow.engine.common;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "统一响应结果")
+public class Result<T> {
+
+    @Schema(description = "状态码")
+    private Integer code;
+
+    @Schema(description = "消息")
+    private String msg;
+
+    @Schema(description = "数据")
+    private T data;
+
+    public static <T> Result<T> ok() {
+        return ok(null);
+    }
+
+    public static <T> Result<T> ok(T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMsg("success");
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> error(String msg) {
+        return error(500, msg);
+    }
+
+    public static <T> Result<T> error(Integer code, String msg) {
+        Result<T> result = new Result<>();
+        result.setCode(code);
+        result.setMsg(msg);
+        return result;
+    }
+}

+ 19 - 0
src/main/java/com/qqflow/engine/common/exception/BusinessException.java

@@ -0,0 +1,19 @@
+package com.qqflow.engine.common.exception;
+
+import lombok.Getter;
+
+@Getter
+public class BusinessException extends RuntimeException {
+
+    private final Integer code;
+
+    public BusinessException(String msg) {
+        super(msg);
+        this.code = 500;
+    }
+
+    public BusinessException(Integer code, String msg) {
+        super(msg);
+        this.code = code;
+    }
+}

+ 44 - 0
src/main/java/com/qqflow/engine/common/exception/GlobalExceptionHandler.java

@@ -0,0 +1,44 @@
+package com.qqflow.engine.common.exception;
+
+import com.qqflow.engine.common.Result;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(BusinessException.class)
+    public Result<Void> handleBusinessException(BusinessException e) {
+        log.warn("业务异常: {}", e.getMessage());
+        return Result.error(e.getCode(), e.getMessage());
+    }
+
+    @ExceptionHandler(AccessDeniedException.class)
+    public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
+        log.warn("权限不足: {}", e.getMessage());
+        return Result.error(403, "权限不足,无法访问");
+    }
+
+    @ExceptionHandler(BadCredentialsException.class)
+    public Result<Void> handleBadCredentialsException(BadCredentialsException e) {
+        log.warn("认证失败: {}", e.getMessage());
+        return Result.error(400, "用户名或密码错误");
+    }
+
+    @ExceptionHandler(BindException.class)
+    public Result<Void> handleBindException(BindException e) {
+        String msg = e.getAllErrors().get(0).getDefaultMessage();
+        return Result.error(400, msg);
+    }
+
+    @ExceptionHandler(Exception.class)
+    public Result<Void> handleException(Exception e) {
+        log.error("系统异常", e);
+        return Result.error("系统繁忙,请稍后重试");
+    }
+}

+ 80 - 0
src/main/java/com/qqflow/engine/common/util/JwtUtils.java

@@ -0,0 +1,80 @@
+package com.qqflow.engine.common.util;
+
+import com.qqflow.engine.config.security.LoginUser;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+@Component
+public class JwtUtils {
+
+    @Value("${jwt.secret}")
+    private String secret;
+
+    @Value("${jwt.expiration}")
+    private Long expiration;
+
+    private SecretKey getKey() {
+        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+    }
+
+    public String generateToken(LoginUser loginUser) {
+        String uuid = generateUuid();
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + expiration);
+        return Jwts.builder()
+                .subject(uuid)
+                .claim("userId", loginUser.getUserId())
+                .claim("username", loginUser.getUsername())
+                .claim("realName", loginUser.getRealName())
+                .claim("deptId", loginUser.getDeptId())
+                .claim("employeeType", loginUser.getEmployeeType())
+                .claim("permissions", loginUser.getPermissions())
+                .claim("roles", loginUser.getRoles())
+                .issuedAt(now)
+                .expiration(expiry)
+                .signWith(getKey())
+                .compact();
+    }
+
+    public String generateUuid() {
+        return UUID.randomUUID().toString().replaceAll("-", "");
+    }
+
+    public Claims parseToken(String token) {
+        return Jwts.parser()
+                .verifyWith(getKey())
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
+
+    public String getUuidFromToken(String token) {
+        return parseToken(token).getSubject();
+    }
+
+    @SuppressWarnings("unchecked")
+    public LoginUser parseLoginUser(String token) {
+        Claims claims = parseToken(token);
+        LoginUser user = new LoginUser();
+        user.setUserId(claims.get("userId", Long.class));
+        user.setUsername(claims.get("username", String.class));
+        user.setRealName(claims.get("realName", String.class));
+        user.setDeptId(claims.get("deptId", Long.class));
+        user.setEmployeeType(claims.get("employeeType", String.class));
+        List<String> perms = claims.get("permissions", List.class);
+        user.setPermissions(perms != null ? new java.util.HashSet<>(perms) : null);
+        List<String> roles = claims.get("roles", List.class);
+        user.setRoles(roles != null ? roles : null);
+        return user;
+    }
+}

+ 31 - 0
src/main/java/com/qqflow/engine/common/util/RedisCache.java

@@ -0,0 +1,31 @@
+package com.qqflow.engine.common.util;
+
+import jakarta.annotation.Resource;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class RedisCache {
+
+    @Resource
+    private RedisTemplate<String, Object> redisTemplate;
+
+    public void setCacheObject(String key, Object value) {
+        redisTemplate.opsForValue().set(key, value);
+    }
+
+    public void setCacheObject(String key, Object value, long timeout, TimeUnit timeUnit) {
+        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T getCacheObject(String key) {
+        return (T) redisTemplate.opsForValue().get(key);
+    }
+
+    public Boolean deleteObject(String key) {
+        return redisTemplate.delete(key);
+    }
+}

+ 26 - 0
src/main/java/com/qqflow/engine/common/util/SecurityUtils.java

@@ -0,0 +1,26 @@
+package com.qqflow.engine.common.util;
+
+import com.qqflow.engine.config.security.LoginUser;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+public class SecurityUtils {
+
+    public static LoginUser getLoginUser() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication != null && authentication.getPrincipal() instanceof LoginUser) {
+            return (LoginUser) authentication.getPrincipal();
+        }
+        return null;
+    }
+
+    public static Long getUserId() {
+        LoginUser user = getLoginUser();
+        return user != null ? user.getUserId() : null;
+    }
+
+    public static boolean isAdmin() {
+        LoginUser user = getLoginUser();
+        return user != null && user.isAdmin();
+    }
+}

+ 26 - 0
src/main/java/com/qqflow/engine/config/AsyncConfig.java

@@ -0,0 +1,26 @@
+package com.qqflow.engine.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Configuration
+@EnableAsync
+public class AsyncConfig {
+
+    @Bean("notificationExecutor")
+    public Executor notificationExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(2);
+        executor.setMaxPoolSize(10);
+        executor.setQueueCapacity(100);
+        executor.setThreadNamePrefix("notification-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.initialize();
+        return executor;
+    }
+}

+ 53 - 0
src/main/java/com/qqflow/engine/config/EmbeddedRedisConfig.java

@@ -0,0 +1,53 @@
+package com.qqflow.engine.config;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import redis.embedded.RedisServer;
+
+import java.io.IOException;
+import java.net.Socket;
+
+@Slf4j
+@Configuration
+public class EmbeddedRedisConfig {
+
+    private RedisServer redisServer;
+
+    @PostConstruct
+    public void startRedis() {
+        if (isPortAvailable(6379)) {
+            try {
+                log.info("本地 Redis 未检测到,启动 Embedded Redis (端口: 6379)...");
+                redisServer = new RedisServer(6379);
+                redisServer.start();
+                log.info("Embedded Redis 启动成功");
+            } catch (IOException e) {
+                log.error("Embedded Redis 启动失败", e);
+            }
+        } else {
+            log.info("检测到外部 Redis 正在运行 (端口: 6379),跳过 Embedded Redis 启动");
+        }
+    }
+
+    @PreDestroy
+    public void stopRedis() {
+        if (redisServer != null && redisServer.isActive()) {
+            try {
+                log.info("正在停止 Embedded Redis...");
+                redisServer.stop();
+            } catch (IOException e) {
+                log.error("Embedded Redis 停止失败", e);
+            }
+        }
+    }
+
+    private boolean isPortAvailable(int port) {
+        try (Socket socket = new Socket("localhost", port)) {
+            return false;
+        } catch (IOException e) {
+            return true;
+        }
+    }
+}

+ 18 - 0
src/main/java/com/qqflow/engine/config/MyBatisPlusConfig.java

@@ -0,0 +1,18 @@
+package com.qqflow.engine.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MyBatisPlusConfig {
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        return interceptor;
+    }
+}

+ 33 - 0
src/main/java/com/qqflow/engine/config/RedisConfig.java

@@ -0,0 +1,33 @@
+package com.qqflow.engine.config;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+
+        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper);
+
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+        template.afterPropertiesSet();
+        return template;
+    }
+}

+ 19 - 0
src/main/java/com/qqflow/engine/config/WebConfig.java

@@ -0,0 +1,19 @@
+package com.qqflow.engine.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+                .allowedHeaders("*")
+                .allowCredentials(true)
+                .maxAge(3600);
+    }
+}

+ 23 - 0
src/main/java/com/qqflow/engine/config/security/AuthenticationManagerConfig.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.config.security;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+public class AuthenticationManagerConfig {
+
+    @Bean
+    public AuthenticationManager authenticationManager(
+            UserDetailsService userDetailsService,
+            PasswordEncoder passwordEncoder) {
+        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+        provider.setUserDetailsService(userDetailsService);
+        provider.setPasswordEncoder(passwordEncoder);
+        return new ProviderManager(provider);
+    }
+}

+ 44 - 0
src/main/java/com/qqflow/engine/config/security/JwtAuthenticationFilter.java

@@ -0,0 +1,44 @@
+package com.qqflow.engine.config.security;
+
+import com.qqflow.engine.common.util.JwtUtils;
+import jakarta.annotation.Resource;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+    @Resource
+    private JwtUtils jwtUtils;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request,
+                                    HttpServletResponse response,
+                                    FilterChain chain) throws ServletException, IOException {
+        String token = request.getHeader("Authorization");
+        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
+            token = token.substring(7);
+            try {
+                LoginUser loginUser = jwtUtils.parseLoginUser(token);
+                if (loginUser != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                    UsernamePasswordAuthenticationToken authentication =
+                            new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+                    SecurityContextHolder.getContext().setAuthentication(authentication);
+                }
+            } catch (Exception ignored) {
+            }
+        }
+        chain.doFilter(request, response);
+    }
+}

+ 63 - 0
src/main/java/com/qqflow/engine/config/security/LoginUser.java

@@ -0,0 +1,63 @@
+package com.qqflow.engine.config.security;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Data
+public class LoginUser implements UserDetails {
+
+    private Long userId;
+    private String username;
+    private String realName;
+    private Long deptId;
+    private String employeeType;
+    private Set<String> permissions;
+    private List<String> roles;
+
+    @JsonIgnore
+    private String password;
+
+    @Override
+    @JsonIgnore
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return permissions.stream()
+                .map(SimpleGrantedAuthority::new)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    @JsonIgnore
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    @JsonIgnore
+    public boolean isAccountNonLocked() {
+        return true;
+    }
+
+    @Override
+    @JsonIgnore
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    @JsonIgnore
+    public boolean isEnabled() {
+        return true;
+    }
+
+    public boolean isAdmin() {
+        return permissions != null && permissions.contains("*:*:*");
+    }
+}

+ 19 - 0
src/main/java/com/qqflow/engine/config/security/PermissionService.java

@@ -0,0 +1,19 @@
+package com.qqflow.engine.config.security;
+
+import com.qqflow.engine.common.util.SecurityUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.Set;
+
+@Service("ss")
+public class PermissionService {
+
+    public boolean hasPermi(String permission) {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (loginUser == null) {
+            return false;
+        }
+        Set<String> permissions = loginUser.getPermissions();
+        return permissions != null && (permissions.contains("*:*:*") || permissions.contains(permission));
+    }
+}

+ 45 - 0
src/main/java/com/qqflow/engine/config/security/SecurityConfig.java

@@ -0,0 +1,45 @@
+package com.qqflow.engine.config.security;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity(prePostEnabled = true)
+public class SecurityConfig {
+
+    @Resource
+    private JwtAuthenticationFilter jwtAuthenticationFilter;
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(AbstractHttpConfigurer::disable)
+                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                .authorizeHttpRequests(auth -> auth
+                        .requestMatchers("/auth/**").permitAll()
+                        .requestMatchers("/webhook/**").permitAll()
+                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
+                        .requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
+                        .anyRequest().authenticated()
+                )
+                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
+        return http.build();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}

+ 27 - 0
src/main/java/com/qqflow/engine/domain/flow/assembler/ApprovalRecordAssembler.java

@@ -0,0 +1,27 @@
+package com.qqflow.engine.domain.flow.assembler;
+
+import com.qqflow.engine.domain.flow.po.ApprovalRecord;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ApprovalRecordAssembler {
+
+    public ApprovalRecord buildNew(Long taskId, Long instanceId, String nodeId,
+                                    String nodeName, Long operatorId,
+                                    String operatorName, String actionType,
+                                    String actionResult, String comment,
+                                    String attachmentUrls) {
+        ApprovalRecord po = new ApprovalRecord();
+        po.setTaskId(taskId);
+        po.setInstanceId(instanceId);
+        po.setNodeId(nodeId);
+        po.setNodeName(nodeName);
+        po.setOperatorId(operatorId);
+        po.setOperatorName(operatorName);
+        po.setActionType(actionType);
+        po.setActionResult(actionResult);
+        po.setComment(comment);
+        po.setAttachmentUrls(attachmentUrls);
+        return po;
+    }
+}

+ 34 - 0
src/main/java/com/qqflow/engine/domain/flow/assembler/ApprovalTaskAssembler.java

@@ -0,0 +1,34 @@
+package com.qqflow.engine.domain.flow.assembler;
+
+import com.qqflow.engine.domain.flow.po.ApprovalTask;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+@Component
+public class ApprovalTaskAssembler {
+
+    public ApprovalTask buildNew(Long instanceId, String nodeId, String nodeName,
+                                  String nodeType, Long assigneeId,
+                                  String assigneeType, Integer taskStatus) {
+        ApprovalTask po = new ApprovalTask();
+        po.setInstanceId(instanceId);
+        po.setNodeId(nodeId);
+        po.setNodeName(nodeName);
+        po.setNodeType(nodeType);
+        po.setAssigneeId(assigneeId);
+        po.setAssigneeType(assigneeType);
+        po.setTaskStatus(taskStatus);
+        return po;
+    }
+
+    public ApprovalTask buildTransferTask(Long instanceId, String nodeId, String nodeName,
+                                           String nodeType, Long assigneeId,
+                                           String assigneeType, Integer taskStatus,
+                                           LocalDateTime timeoutTime, String timeoutAction) {
+        ApprovalTask po = buildNew(instanceId, nodeId, nodeName, nodeType, assigneeId, assigneeType, taskStatus);
+        po.setTimeoutTime(timeoutTime);
+        po.setTimeoutAction(timeoutAction);
+        return po;
+    }
+}

+ 34 - 0
src/main/java/com/qqflow/engine/domain/flow/assembler/ProcessDefinitionAssembler.java

@@ -0,0 +1,34 @@
+package com.qqflow.engine.domain.flow.assembler;
+
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ProcessDefinitionAssembler {
+
+    public ProcessDefinition buildFromSaveDto(com.qqflow.engine.domain.flow.dto.ProcessDefinitionDTO dto, Long createBy) {
+        ProcessDefinition po = new ProcessDefinition();
+        po.setProcessCode(dto.getProcessCode());
+        po.setProcessName(dto.getProcessName());
+        po.setCategory(dto.getCategory());
+        po.setFormId(dto.getFormId());
+        po.setModelJson(dto.getModelJson());
+        po.setVersion(1);
+        po.setStatus(0);
+        po.setDescription(dto.getDescription());
+        po.setCreateBy(createBy);
+        return po;
+    }
+
+    public ProcessDefinition buildFromUpdateDto(com.qqflow.engine.domain.flow.dto.ProcessDefinitionDTO dto) {
+        ProcessDefinition po = new ProcessDefinition();
+        po.setId(dto.getId());
+        po.setProcessCode(dto.getProcessCode());
+        po.setProcessName(dto.getProcessName());
+        po.setCategory(dto.getCategory());
+        po.setFormId(dto.getFormId());
+        po.setModelJson(dto.getModelJson());
+        po.setDescription(dto.getDescription());
+        return po;
+    }
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/flow/assembler/ProcessInstanceAssembler.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.flow.assembler;
+
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+@Component
+public class ProcessInstanceAssembler {
+
+    public ProcessInstance buildNew(String instanceNo, Long processDefinitionId,
+                                     Integer version, String title, Long applicantId,
+                                     Long applicantDeptId, String formData,
+                                     String currentNodeId, Integer status) {
+        ProcessInstance po = new ProcessInstance();
+        po.setInstanceNo(instanceNo);
+        po.setProcessDefinitionId(processDefinitionId);
+        po.setVersion(version);
+        po.setTitle(title);
+        po.setApplicantId(applicantId);
+        po.setApplicantDeptId(applicantDeptId);
+        po.setFormData(formData);
+        po.setCurrentNodeId(currentNodeId);
+        po.setStatus(status);
+        po.setStartTime(LocalDateTime.now());
+        return po;
+    }
+}

+ 104 - 0
src/main/java/com/qqflow/engine/domain/flow/controller/ApprovalTaskController.java

@@ -0,0 +1,104 @@
+package com.qqflow.engine.domain.flow.controller;
+
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.common.util.SecurityUtils;
+import com.qqflow.engine.domain.flow.dto.ApprovalRecordDTO;
+import com.qqflow.engine.domain.flow.dto.ApprovalTaskDTO;
+import com.qqflow.engine.domain.flow.dto.ApproveTaskDTO;
+import com.qqflow.engine.domain.flow.dto.TransferTaskDTO;
+import com.qqflow.engine.domain.flow.service.ApprovalTaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/flow/task")
+@RequiredArgsConstructor
+@Tag(name = "审批任务管理")
+public class ApprovalTaskController {
+
+    private final ApprovalTaskService approvalTaskService;
+
+    @GetMapping("/todo")
+    @Operation(summary = "我的待办列表")
+    public Result<PageResult<ApprovalTaskDTO>> todoList(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String processName) {
+        Long assigneeId = SecurityUtils.getUserId();
+        return Result.ok(this.approvalTaskService.todoList(assigneeId, processName, pageNum, pageSize));
+    }
+
+    @GetMapping("/handled")
+    @Operation(summary = "我的已办列表")
+    public Result<PageResult<ApprovalTaskDTO>> handledList(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String processName) {
+        Long assigneeId = SecurityUtils.getUserId();
+        return Result.ok(this.approvalTaskService.handledList(assigneeId, processName, pageNum, pageSize));
+    }
+
+    @PostMapping("/{taskId}/approve")
+    @Operation(summary = "审批通过")
+    public Result<Void> approve(@PathVariable Long taskId, @RequestBody @Valid ApproveTaskDTO dto) {
+        dto.setTaskId(taskId);
+        this.approvalTaskService.approve(dto);
+        return Result.ok();
+    }
+
+    @PostMapping("/{taskId}/reject")
+    @Operation(summary = "审批拒绝")
+    public Result<Void> reject(@PathVariable Long taskId, @RequestBody @Valid ApproveTaskDTO dto) {
+        dto.setTaskId(taskId);
+        this.approvalTaskService.reject(dto);
+        return Result.ok();
+    }
+
+    @PostMapping("/{taskId}/return")
+    @Operation(summary = "审批回退")
+    public Result<Void> returnTask(@PathVariable Long taskId, @RequestBody @Valid ApproveTaskDTO dto) {
+        dto.setTaskId(taskId);
+        this.approvalTaskService.returnTask(dto);
+        return Result.ok();
+    }
+
+    @PostMapping("/{taskId}/transfer")
+    @Operation(summary = "任务转办")
+    public Result<Void> transfer(@PathVariable Long taskId, @RequestBody @Valid TransferTaskDTO dto) {
+        dto.setTaskId(taskId);
+        this.approvalTaskService.transfer(dto);
+        return Result.ok();
+    }
+
+    @PostMapping("/{taskId}/add-sign")
+    @Operation(summary = "任务加签")
+    public Result<Void> addSign(@PathVariable Long taskId, @RequestParam Long assigneeId) {
+        this.approvalTaskService.addSign(taskId, assigneeId);
+        return Result.ok();
+    }
+
+    @GetMapping("/history/{instanceId}")
+    @Operation(summary = "审批历史记录")
+    public Result<List<ApprovalRecordDTO>> history(@PathVariable Long instanceId) {
+        return Result.ok(this.approvalTaskService.history(instanceId));
+    }
+
+    @GetMapping("/todo-count")
+    @Operation(summary = "我的待办任务数")
+    public Result<Long> todoCount() {
+        Long assigneeId = SecurityUtils.getUserId();
+        return Result.ok(this.approvalTaskService.todoCount(assigneeId));
+    }
+}

+ 98 - 0
src/main/java/com/qqflow/engine/domain/flow/controller/ProcessDefinitionController.java

@@ -0,0 +1,98 @@
+package com.qqflow.engine.domain.flow.controller;
+
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.common.util.SecurityUtils;
+import com.qqflow.engine.domain.flow.assembler.ProcessDefinitionAssembler;
+import com.qqflow.engine.domain.flow.dto.ProcessDefinitionDTO;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import com.qqflow.engine.domain.flow.service.ProcessDefinitionService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/flow/definition")
+@RequiredArgsConstructor
+@Tag(name = "流程定义管理")
+public class ProcessDefinitionController {
+
+    private final ProcessDefinitionService processDefinitionService;
+    private final ProcessDefinitionAssembler processDefinitionAssembler;
+
+    @PostMapping
+    @Operation(summary = "保存流程定义")
+    public Result<Long> save(@RequestBody @Valid ProcessDefinitionDTO dto) {
+        ProcessDefinition po = this.processDefinitionAssembler.buildFromSaveDto(dto, SecurityUtils.getUserId());
+        return Result.ok(this.processDefinitionService.saveDefinition(po));
+    }
+
+    @PutMapping
+    @Operation(summary = "更新流程定义")
+    public Result<Long> update(@RequestBody @Valid ProcessDefinitionDTO dto) {
+        ProcessDefinition po = this.processDefinitionAssembler.buildFromUpdateDto(dto);
+        Long newId = this.processDefinitionService.updateDefinition(po);
+        return Result.ok(newId);
+    }
+
+    @DeleteMapping("/{id}")
+    @Operation(summary = "删除流程定义")
+    public Result<Void> delete(@PathVariable Long id) {
+        this.processDefinitionService.deleteDefinition(id);
+        return Result.ok();
+    }
+
+    @GetMapping("/{id}")
+    @Operation(summary = "流程定义详情")
+    public Result<ProcessDefinitionDTO> getById(@PathVariable Long id) {
+        return Result.ok(this.processDefinitionService.getById(id));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "流程定义分页列表")
+    public Result<PageResult<ProcessDefinitionDTO>> page(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String processName) {
+        return Result.ok(this.processDefinitionService.page(pageNum, pageSize, processName));
+    }
+
+    @PostMapping("/{id}/publish")
+    @Operation(summary = "发布流程定义")
+    public Result<Void> publish(@PathVariable Long id) {
+        this.processDefinitionService.publish(id);
+        return Result.ok();
+    }
+
+    @PostMapping("/{id}/stop")
+    @Operation(summary = "停用流程定义")
+    public Result<Void> stop(@PathVariable Long id) {
+        this.processDefinitionService.stop(id);
+        return Result.ok();
+    }
+
+    @PostMapping("/{id}/enable")
+    @Operation(summary = "启用流程定义")
+    public Result<Void> enable(@PathVariable Long id) {
+        this.processDefinitionService.enable(id);
+        return Result.ok();
+    }
+
+    @GetMapping("/list-enabled")
+    @Operation(summary = "启用中的流程定义列表")
+    public Result<List<ProcessDefinitionDTO>> listEnabled() {
+        return Result.ok(this.processDefinitionService.listEnabled());
+    }
+}

+ 82 - 0
src/main/java/com/qqflow/engine/domain/flow/controller/ProcessInstanceController.java

@@ -0,0 +1,82 @@
+package com.qqflow.engine.domain.flow.controller;
+
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.common.util.SecurityUtils;
+import com.qqflow.engine.domain.flow.dto.ProcessInstanceDTO;
+import com.qqflow.engine.domain.flow.dto.ProcessProgressDTO;
+import com.qqflow.engine.domain.flow.dto.StartProcessDTO;
+import com.qqflow.engine.domain.flow.service.ProcessInstanceService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/flow/instance")
+@RequiredArgsConstructor
+@Tag(name = "流程实例管理")
+public class ProcessInstanceController {
+
+    private final ProcessInstanceService processInstanceService;
+
+    @PostMapping("/start")
+    @Operation(summary = "发起流程")
+    public Result<Long> start(@RequestBody @Valid StartProcessDTO dto) {
+        return Result.ok(this.processInstanceService.startProcess(dto));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "流程实例分页列表")
+    public Result<PageResult<ProcessInstanceDTO>> page(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) Integer status) {
+        Long applicantId = SecurityUtils.getUserId();
+        return Result.ok(this.processInstanceService.list(applicantId, status, pageNum, pageSize));
+    }
+
+    @GetMapping("/mine")
+    @Operation(summary = "我的流程列表")
+    public Result<PageResult<ProcessInstanceDTO>> mine(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        Long applicantId = SecurityUtils.getUserId();
+        return Result.ok(this.processInstanceService.list(applicantId, null, pageNum, pageSize));
+    }
+
+    @GetMapping("/{id}")
+    @Operation(summary = "流程实例详情")
+    public Result<ProcessInstanceDTO> getById(@PathVariable Long id) {
+        return Result.ok(this.processInstanceService.getDetail(id));
+    }
+
+    @PostMapping("/{id}/revoke")
+    @Operation(summary = "撤回流程")
+    public Result<Void> revoke(@PathVariable Long id) {
+        this.processInstanceService.revoke(id);
+        return Result.ok();
+    }
+
+    @GetMapping("/{id}/progress")
+    @Operation(summary = "流程实例进度")
+    public Result<ProcessProgressDTO> progress(@PathVariable Long id) {
+        return Result.ok(this.processInstanceService.getProgress(id));
+    }
+
+    @GetMapping("/participated")
+    @Operation(summary = "我参与的流程实例列表")
+    public Result<PageResult<ProcessInstanceDTO>> participated(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        Long userId = SecurityUtils.getUserId();
+        return Result.ok(this.processInstanceService.participatedList(userId, pageNum, pageSize));
+    }
+}

+ 68 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/ApprovalRecordDTO.java

@@ -0,0 +1,68 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import com.qqflow.engine.domain.flow.po.ApprovalRecord;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "审批记录DTO")
+public class ApprovalRecordDTO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "任务ID")
+    private Long taskId;
+
+    @Schema(description = "流程实例ID")
+    private Long instanceId;
+
+    @Schema(description = "节点ID")
+    private String nodeId;
+
+    @Schema(description = "节点名称")
+    private String nodeName;
+
+    @Schema(description = "操作人ID")
+    private Long operatorId;
+
+    @Schema(description = "操作人姓名")
+    private String operatorName;
+
+    @Schema(description = "操作类型")
+    private String actionType;
+
+    @Schema(description = "操作结果")
+    private String actionResult;
+
+    @Schema(description = "审批意见")
+    private String comment;
+
+    @Schema(description = "附件URL")
+    private String attachmentUrls;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    public static ApprovalRecordDTO of(ApprovalRecord po) {
+        if (po == null) {
+            return null;
+        }
+        ApprovalRecordDTO dto = new ApprovalRecordDTO();
+        dto.setId(po.getId());
+        dto.setTaskId(po.getTaskId());
+        dto.setInstanceId(po.getInstanceId());
+        dto.setNodeId(po.getNodeId());
+        dto.setNodeName(po.getNodeName());
+        dto.setOperatorId(po.getOperatorId());
+        dto.setOperatorName(po.getOperatorName());
+        dto.setActionType(po.getActionType());
+        dto.setActionResult(po.getActionResult());
+        dto.setComment(po.getComment());
+        dto.setAttachmentUrls(po.getAttachmentUrls());
+        dto.setCreateTime(po.getCreateTime());
+        return dto;
+    }
+}

+ 102 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/ApprovalTaskDTO.java

@@ -0,0 +1,102 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import com.qqflow.engine.domain.flow.po.ApprovalTask;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "审批任务DTO")
+public class ApprovalTaskDTO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "流程实例ID")
+    private Long instanceId;
+
+    @Schema(description = "节点ID")
+    private String nodeId;
+
+    @Schema(description = "节点名称")
+    private String nodeName;
+
+    @Schema(description = "节点类型")
+    private String nodeType;
+
+    @Schema(description = "处理人ID")
+    private Long assigneeId;
+
+    @Schema(description = "处理人类型")
+    private String assigneeType;
+
+    @Schema(description = "任务状态")
+    private Integer taskStatus;
+
+    @Schema(description = "审批结果")
+    private String approvalResult;
+
+    @Schema(description = "审批意见")
+    private String approvalComment;
+
+    @Schema(description = "操作结果(approvalResult别名)")
+    private String action;
+
+    @Schema(description = "审批意见(approvalComment别名)")
+    private String comment;
+
+    @Schema(description = "附件URL")
+    private String attachmentUrls;
+
+    @Schema(description = "超时时间")
+    private LocalDateTime timeoutTime;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "处理时间")
+    private LocalDateTime handleTime;
+
+    @Schema(description = "处理时间(handleTime别名)")
+    private LocalDateTime endTime;
+
+    @Schema(description = "流程名称")
+    private String definitionName;
+
+    @Schema(description = "实例标题")
+    private String instanceTitle;
+
+    @Schema(description = "实例编号")
+    private String instanceNo;
+
+    public static ApprovalTaskDTO of(ApprovalTask po) {
+        if (po == null) {
+            return null;
+        }
+        ApprovalTaskDTO dto = new ApprovalTaskDTO();
+        dto.setId(po.getId());
+        dto.setInstanceId(po.getInstanceId());
+        dto.setNodeId(po.getNodeId());
+        dto.setNodeName(po.getNodeName());
+        dto.setNodeType(po.getNodeType());
+        dto.setAssigneeId(po.getAssigneeId());
+        dto.setAssigneeType(po.getAssigneeType());
+        dto.setTaskStatus(po.getTaskStatus());
+        dto.setApprovalResult(po.getApprovalResult());
+        dto.setApprovalComment(po.getApprovalComment());
+        dto.setAttachmentUrls(po.getAttachmentUrls());
+        dto.setTimeoutTime(po.getTimeoutTime());
+        dto.setCreateTime(po.getCreateTime());
+        dto.setHandleTime(po.getHandleTime());
+        // 扩展字段(由XML查询填充)
+        dto.setDefinitionName(po.getProcessName());
+        dto.setInstanceTitle(po.getInstanceTitle());
+        dto.setInstanceNo(po.getInstanceNo());
+        // 别名映射
+        dto.setAction(po.getApprovalResult());
+        dto.setComment(po.getApprovalComment());
+        dto.setEndTime(po.getHandleTime());
+        return dto;
+    }
+}

+ 19 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/ApproveTaskDTO.java

@@ -0,0 +1,19 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+@Schema(description = "审批任务DTO")
+public class ApproveTaskDTO {
+
+    @Schema(description = "任务ID")
+    private Long taskId;
+
+    @Schema(description = "审批意见")
+    private String comment;
+
+    @Schema(description = "附件URL")
+    private String attachmentUrls;
+}

+ 29 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/NodeProgressDTO.java

@@ -0,0 +1,29 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Schema(description = "节点进度DTO")
+public class NodeProgressDTO {
+
+    @Schema(description = "节点ID")
+    private String nodeId;
+
+    @Schema(description = "节点名称")
+    private String nodeName;
+
+    @Schema(description = "节点类型")
+    private String nodeType;
+
+    @Schema(description = "状态: completed-已完成 current-当前 pending-未开始")
+    private String status;
+
+    @Schema(description = "该节点下的任务列表")
+    private List<ApprovalTaskDTO> tasks;
+
+    @Schema(description = "当前登录用户是否是需要处理的人")
+    private Boolean isMyTurn;
+}

+ 76 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/ProcessDefinitionDTO.java

@@ -0,0 +1,76 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "流程定义DTO")
+public class ProcessDefinitionDTO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @NotBlank(message = "流程编码不能为空")
+    @JsonProperty("code")
+    @Schema(description = "流程编码")
+    private String processCode;
+
+    @NotBlank(message = "流程名称不能为空")
+    @JsonProperty("name")
+    @Schema(description = "流程名称")
+    private String processName;
+
+    @JsonProperty("category")
+    @Schema(description = "分类")
+    private String category;
+
+    @Schema(description = "表单ID")
+    private Long formId;
+
+    @JsonProperty("flowJson")
+    @Schema(description = "模型JSON")
+    private String modelJson;
+
+    @Schema(description = "版本号")
+    private Integer version;
+
+    @Schema(description = "状态")
+    private Integer status;
+
+    @Schema(description = "描述")
+    private String description;
+
+    @Schema(description = "创建人")
+    private Long createBy;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间")
+    private LocalDateTime updateTime;
+
+    public static ProcessDefinitionDTO of(ProcessDefinition po) {
+        if (po == null) {
+            return null;
+        }
+        ProcessDefinitionDTO dto = new ProcessDefinitionDTO();
+        dto.setId(po.getId());
+        dto.setProcessCode(po.getProcessCode());
+        dto.setProcessName(po.getProcessName());
+        dto.setCategory(po.getCategory());
+        dto.setFormId(po.getFormId());
+        dto.setModelJson(po.getModelJson());
+        dto.setVersion(po.getVersion());
+        dto.setStatus(po.getStatus());
+        dto.setDescription(po.getDescription());
+        dto.setCreateBy(po.getCreateBy());
+        dto.setCreateTime(po.getCreateTime());
+        dto.setUpdateTime(po.getUpdateTime());
+        return dto;
+    }
+}

+ 92 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/ProcessInstanceDTO.java

@@ -0,0 +1,92 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "流程实例DTO")
+public class ProcessInstanceDTO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "实例编号")
+    private String instanceNo;
+
+    @Schema(description = "业务编号(instanceNo别名)")
+    private String businessKey;
+
+    @Schema(description = "流程定义ID")
+    private Long processDefinitionId;
+
+    @Schema(description = "版本号")
+    private Integer version;
+
+    @Schema(description = "标题")
+    private String title;
+
+    @Schema(description = "申请人ID")
+    private Long applicantId;
+
+    @Schema(description = "申请人部门ID")
+    private Long applicantDeptId;
+
+    @Schema(description = "表单数据JSON")
+    private String formData;
+
+    @Schema(description = "当前节点ID")
+    private String currentNodeId;
+
+    @Schema(description = "当前节点(currentNodeId别名)")
+    private String currentNode;
+
+    @Schema(description = "状态")
+    private Integer status;
+
+    @Schema(description = "结果")
+    private String result;
+
+    @Schema(description = "开始时间")
+    private LocalDateTime startTime;
+
+    @Schema(description = "结束时间")
+    private LocalDateTime endTime;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间")
+    private LocalDateTime updateTime;
+
+    @Schema(description = "流程名称")
+    private String definitionName;
+
+    public static ProcessInstanceDTO of(ProcessInstance po) {
+        if (po == null) {
+            return null;
+        }
+        ProcessInstanceDTO dto = new ProcessInstanceDTO();
+        dto.setId(po.getId());
+        dto.setInstanceNo(po.getInstanceNo());
+        dto.setProcessDefinitionId(po.getProcessDefinitionId());
+        dto.setVersion(po.getVersion());
+        dto.setTitle(po.getTitle());
+        dto.setApplicantId(po.getApplicantId());
+        dto.setApplicantDeptId(po.getApplicantDeptId());
+        dto.setFormData(po.getFormData());
+        dto.setCurrentNodeId(po.getCurrentNodeId());
+        dto.setStatus(po.getStatus());
+        dto.setResult(po.getResult());
+        dto.setStartTime(po.getStartTime());
+        dto.setEndTime(po.getEndTime());
+        dto.setCreateTime(po.getCreateTime());
+        dto.setUpdateTime(po.getUpdateTime());
+        dto.setDefinitionName(po.getProcessName());
+        dto.setCurrentNode(po.getCurrentNodeId());
+        dto.setBusinessKey(po.getInstanceNo());
+        return dto;
+    }
+}

+ 26 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/ProcessProgressDTO.java

@@ -0,0 +1,26 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Schema(description = "流程进度DTO")
+public class ProcessProgressDTO {
+
+    @Schema(description = "流程实例")
+    private ProcessInstanceDTO instance;
+
+    @Schema(description = "流程定义")
+    private ProcessDefinitionDTO definition;
+
+    @Schema(description = "节点进度列表")
+    private List<NodeProgressDTO> nodes;
+
+    @Schema(description = "审批历史记录")
+    private List<ApprovalRecordDTO> records;
+
+    @Schema(description = "剩余待审节点数")
+    private Integer remainingNodeCount;
+}

+ 20 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/StartProcessDTO.java

@@ -0,0 +1,20 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+@Schema(description = "发起流程DTO")
+public class StartProcessDTO {
+
+    @NotNull(message = "流程定义ID不能为空")
+    @Schema(description = "流程定义ID")
+    private Long processDefinitionId;
+
+    @Schema(description = "标题")
+    private String title;
+
+    @Schema(description = "表单数据JSON")
+    private String formData;
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/TransferTaskDTO.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+@Schema(description = "转办任务DTO")
+public class TransferTaskDTO {
+
+    @Schema(description = "任务ID")
+    private Long taskId;
+
+    @NotNull(message = "转办人ID不能为空")
+    @Schema(description = "转办人ID")
+    private Long transferToUserId;
+
+    @Schema(description = "转办人ID(transferToUserId别名)")
+    private Long transferTo;
+
+    @Schema(description = "审批意见")
+    private String comment;
+}

+ 25 - 0
src/main/java/com/qqflow/engine/domain/flow/enums/ApprovalAction.java

@@ -0,0 +1,25 @@
+package com.qqflow.engine.domain.flow.enums;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+@Schema(description = "审批操作类型")
+public enum ApprovalAction {
+
+    APPROVE("APPROVE", "通过"),
+    REJECT("REJECT", "拒绝"),
+    RETURN("RETURN", "回退"),
+    TRANSFER("TRANSFER", "转办"),
+    DELEGATE("DELEGATE", "委派"),
+    ADD_SIGN("ADD_SIGN", "加签"),
+    REVOKE("REVOKE", "撤回");
+
+    private final String code;
+    private final String desc;
+
+    ApprovalAction(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 22 - 0
src/main/java/com/qqflow/engine/domain/flow/enums/ApprovalResult.java

@@ -0,0 +1,22 @@
+package com.qqflow.engine.domain.flow.enums;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+@Schema(description = "审批结果")
+public enum ApprovalResult {
+
+    PASS("PASS", "通过"),
+    REJECT("REJECT", "拒绝"),
+    RETURN("RETURN", "回退"),
+    TRANSFER("TRANSFER", "转办");
+
+    private final String code;
+    private final String desc;
+
+    ApprovalResult(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 30 - 0
src/main/java/com/qqflow/engine/domain/flow/enums/DefinitionStatus.java

@@ -0,0 +1,30 @@
+package com.qqflow.engine.domain.flow.enums;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+@Schema(description = "流程定义状态")
+public enum DefinitionStatus {
+
+    DESIGNING(0, "设计中"),
+    ENABLED(1, "启用中"),
+    HISTORICAL(2, "历史");
+
+    private final Integer code;
+    private final String desc;
+
+    DefinitionStatus(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public static DefinitionStatus of(Integer code) {
+        for (DefinitionStatus status : values()) {
+            if (status.code.equals(code)) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/flow/enums/NodeType.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.flow.enums;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+@Schema(description = "节点类型")
+public enum NodeType {
+
+    START("start", "开始节点"),
+    APPROVAL("approval", "审批节点"),
+    CC("cc", "抄送节点"),
+    CONDITION("condition", "条件节点"),
+    END("end", "结束节点");
+
+    private final String code;
+    private final String desc;
+
+    NodeType(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 24 - 0
src/main/java/com/qqflow/engine/domain/flow/enums/ProcessEvent.java

@@ -0,0 +1,24 @@
+package com.qqflow.engine.domain.flow.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum ProcessEvent {
+
+    START("START", "发起"),
+    RECEIVE("RECEIVE", "接收"),
+    APPROVE("APPROVE", "通过"),
+    REJECT("REJECT", "拒绝"),
+    RETURN("RETURN", "回退"),
+    COMPLETE("COMPLETE", "完成"),
+    REVOKE("REVOKE", "撤回"),
+    TERMINATE("TERMINATE", "终止");
+
+    private final String code;
+    private final String desc;
+
+    ProcessEvent(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 35 - 0
src/main/java/com/qqflow/engine/domain/flow/enums/ProcessStatus.java

@@ -0,0 +1,35 @@
+package com.qqflow.engine.domain.flow.enums;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+@Schema(description = "流程实例状态")
+public enum ProcessStatus {
+
+    PENDING_RECEIVE(0, "待接收"),
+    PENDING(1, "待处理"),
+    APPROVED(2, "已通过"),
+    REJECTED(3, "已拒绝"),
+    RETURNED(4, "已回退"),
+    COMPLETED(5, "整体完成"),
+    REVOKED(6, "已撤回"),
+    TERMINATED(7, "已终止");
+
+    private final Integer code;
+    private final String desc;
+
+    ProcessStatus(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public static ProcessStatus of(Integer code) {
+        for (ProcessStatus status : values()) {
+            if (status.code.equals(code)) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 32 - 0
src/main/java/com/qqflow/engine/domain/flow/enums/TaskStatus.java

@@ -0,0 +1,32 @@
+package com.qqflow.engine.domain.flow.enums;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+@Getter
+@Schema(description = "审批任务状态")
+public enum TaskStatus {
+
+    PENDING(0, "待处理"),
+    HANDLED(1, "已处理"),
+    TRANSFERRED(2, "已转办"),
+    SKIPPED(3, "已跳过"),
+    RETURNED(4, "已回退");
+
+    private final Integer code;
+    private final String desc;
+
+    TaskStatus(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public static TaskStatus of(Integer code) {
+        for (TaskStatus status : values()) {
+            if (status.code.equals(code)) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 27 - 0
src/main/java/com/qqflow/engine/domain/flow/event/ProcessCompletedEvent.java

@@ -0,0 +1,27 @@
+package com.qqflow.engine.domain.flow.event;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 流程结束事件:当流程实例完成(通过/拒绝)时触发
+ */
+@Getter
+public class ProcessCompletedEvent extends ApplicationEvent {
+
+    private final Long instanceId;
+    private final String instanceTitle;
+    private final String processName;
+    private final Long applicantId;
+    private final String result; // PASS / REJECT
+
+    public ProcessCompletedEvent(Object source, Long instanceId, String instanceTitle,
+                                 String processName, Long applicantId, String result) {
+        super(source);
+        this.instanceId = instanceId;
+        this.instanceTitle = instanceTitle;
+        this.processName = processName;
+        this.applicantId = applicantId;
+        this.result = result;
+    }
+}

+ 29 - 0
src/main/java/com/qqflow/engine/domain/flow/event/TaskAssignedEvent.java

@@ -0,0 +1,29 @@
+package com.qqflow.engine.domain.flow.event;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+import java.util.List;
+
+/**
+ * 任务分配事件:当新的审批任务被创建时触发
+ */
+@Getter
+public class TaskAssignedEvent extends ApplicationEvent {
+
+    private final Long instanceId;
+    private final String instanceTitle;
+    private final String processName;
+    private final String nodeName;
+    private final List<Long> assigneeIds;
+
+    public TaskAssignedEvent(Object source, Long instanceId, String instanceTitle,
+                             String processName, String nodeName, List<Long> assigneeIds) {
+        super(source);
+        this.instanceId = instanceId;
+        this.instanceTitle = instanceTitle;
+        this.processName = processName;
+        this.nodeName = nodeName;
+        this.assigneeIds = assigneeIds;
+    }
+}

+ 34 - 0
src/main/java/com/qqflow/engine/domain/flow/event/TaskCompletedEvent.java

@@ -0,0 +1,34 @@
+package com.qqflow.engine.domain.flow.event;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 任务完成事件:当审批任务被通过/拒绝/退回/转办时触发
+ */
+@Getter
+public class TaskCompletedEvent extends ApplicationEvent {
+
+    private final Long instanceId;
+    private final String instanceTitle;
+    private final String processName;
+    private final String nodeName;
+    private final Long operatorId;
+    private final String operatorName;
+    private final String action;   // APPROVE / REJECT / RETURN / TRANSFER
+    private final String result;   // PASS / REJECT / RETURN / TRANSFER
+
+    public TaskCompletedEvent(Object source, Long instanceId, String instanceTitle,
+                              String processName, String nodeName, Long operatorId,
+                              String operatorName, String action, String result) {
+        super(source);
+        this.instanceId = instanceId;
+        this.instanceTitle = instanceTitle;
+        this.processName = processName;
+        this.nodeName = nodeName;
+        this.operatorId = operatorId;
+        this.operatorName = operatorName;
+        this.action = action;
+        this.result = result;
+    }
+}

+ 69 - 0
src/main/java/com/qqflow/engine/domain/flow/listener/NotificationEventListener.java

@@ -0,0 +1,69 @@
+package com.qqflow.engine.domain.flow.listener;
+
+import com.qqflow.engine.domain.flow.event.ProcessCompletedEvent;
+import com.qqflow.engine.domain.flow.event.TaskAssignedEvent;
+import com.qqflow.engine.domain.flow.event.TaskCompletedEvent;
+import com.qqflow.engine.domain.flow.service.NotificationService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+/**
+ * 流程事件监听器:监听流程相关事件并触发通知
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class NotificationEventListener {
+
+    private final NotificationService notificationService;
+
+    @Async("notificationExecutor")
+    @EventListener
+    public void onTaskAssigned(TaskAssignedEvent event) {
+        try {
+            notificationService.notifyTaskAssigned(
+                    event.getAssigneeIds(),
+                    event.getProcessName(),
+                    event.getInstanceTitle(),
+                    event.getNodeName()
+            );
+        } catch (Exception e) {
+            log.error("发送任务分配通知失败", e);
+        }
+    }
+
+    @Async("notificationExecutor")
+    @EventListener
+    public void onTaskCompleted(TaskCompletedEvent event) {
+        try {
+            notificationService.notifyTaskCompleted(
+                    event.getInstanceId(),
+                    event.getProcessName(),
+                    event.getInstanceTitle(),
+                    event.getNodeName(),
+                    event.getOperatorName(),
+                    event.getAction()
+            );
+        } catch (Exception e) {
+            log.error("发送任务完成通知失败", e);
+        }
+    }
+
+    @Async("notificationExecutor")
+    @EventListener
+    public void onProcessCompleted(ProcessCompletedEvent event) {
+        try {
+            notificationService.notifyProcessCompleted(
+                    event.getApplicantId(),
+                    event.getProcessName(),
+                    event.getInstanceTitle(),
+                    event.getResult()
+            );
+        } catch (Exception e) {
+            log.error("发送流程结束通知失败", e);
+        }
+    }
+}

+ 27 - 0
src/main/java/com/qqflow/engine/domain/flow/mapper/ApprovalRecordMapper.java

@@ -0,0 +1,27 @@
+package com.qqflow.engine.domain.flow.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.flow.po.ApprovalRecord;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface ApprovalRecordMapper extends BaseMapper<ApprovalRecord> {
+
+    default List<ApprovalRecord> selectByInstanceId(Long instanceId) {
+        LambdaQueryWrapper<ApprovalRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ApprovalRecord::getInstanceId, instanceId);
+        return this.selectList(wrapper);
+    }
+
+    default List<ApprovalRecord> selectByTaskId(Long taskId) {
+        LambdaQueryWrapper<ApprovalRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ApprovalRecord::getTaskId, taskId);
+        return this.selectList(wrapper);
+    }
+
+    List<ApprovalRecord> selectRecordListByInstanceId(@Param("instanceId") Long instanceId);
+}

+ 43 - 0
src/main/java/com/qqflow/engine/domain/flow/mapper/ApprovalTaskMapper.java

@@ -0,0 +1,43 @@
+package com.qqflow.engine.domain.flow.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.flow.po.ApprovalTask;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface ApprovalTaskMapper extends BaseMapper<ApprovalTask> {
+
+    default List<ApprovalTask> selectPendingByAssigneeId(Long assigneeId) {
+        LambdaQueryWrapper<ApprovalTask> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ApprovalTask::getAssigneeId, assigneeId)
+                .eq(ApprovalTask::getTaskStatus, 0);
+        return this.selectList(wrapper);
+    }
+
+    default List<ApprovalTask> selectByInstanceId(Long instanceId) {
+        LambdaQueryWrapper<ApprovalTask> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ApprovalTask::getInstanceId, instanceId);
+        return this.selectList(wrapper);
+    }
+
+    default ApprovalTask selectPendingById(Long taskId) {
+        LambdaQueryWrapper<ApprovalTask> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ApprovalTask::getId, taskId)
+                .eq(ApprovalTask::getTaskStatus, 0);
+        return this.selectOne(wrapper);
+    }
+
+    com.baomidou.mybatisplus.extension.plugins.pagination.Page<ApprovalTask> selectTodoList(
+            com.baomidou.mybatisplus.extension.plugins.pagination.Page<ApprovalTask> page,
+            @Param("assigneeId") Long assigneeId,
+            @Param("processName") String processName);
+
+    com.baomidou.mybatisplus.extension.plugins.pagination.Page<ApprovalTask> selectHandledList(
+            com.baomidou.mybatisplus.extension.plugins.pagination.Page<ApprovalTask> page,
+            @Param("assigneeId") Long assigneeId,
+            @Param("processName") String processName);
+}

+ 38 - 0
src/main/java/com/qqflow/engine/domain/flow/mapper/ProcessDefinitionMapper.java

@@ -0,0 +1,38 @@
+package com.qqflow.engine.domain.flow.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface ProcessDefinitionMapper extends BaseMapper<ProcessDefinition> {
+
+    default ProcessDefinition selectByProcessCode(String processCode) {
+        LambdaQueryWrapper<ProcessDefinition> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ProcessDefinition::getProcessCode, processCode);
+        return this.selectOne(wrapper);
+    }
+
+    default List<ProcessDefinition> selectByCategory(String category) {
+        LambdaQueryWrapper<ProcessDefinition> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ProcessDefinition::getCategory, category);
+        return this.selectList(wrapper);
+    }
+
+    default ProcessDefinition selectLatestByProcessCode(String processCode) {
+        LambdaQueryWrapper<ProcessDefinition> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ProcessDefinition::getProcessCode, processCode)
+                .orderByDesc(ProcessDefinition::getVersion)
+                .last("limit 1");
+        return this.selectOne(wrapper);
+    }
+
+    default List<ProcessDefinition> selectEnabledList() {
+        LambdaQueryWrapper<ProcessDefinition> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ProcessDefinition::getStatus, 1);
+        return this.selectList(wrapper);
+    }
+}

+ 34 - 0
src/main/java/com/qqflow/engine/domain/flow/mapper/ProcessInstanceMapper.java

@@ -0,0 +1,34 @@
+package com.qqflow.engine.domain.flow.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface ProcessInstanceMapper extends BaseMapper<ProcessInstance> {
+
+    default ProcessInstance selectByInstanceNo(String instanceNo) {
+        LambdaQueryWrapper<ProcessInstance> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ProcessInstance::getInstanceNo, instanceNo);
+        return this.selectOne(wrapper);
+    }
+
+    default List<ProcessInstance> selectByApplicantId(Long applicantId) {
+        LambdaQueryWrapper<ProcessInstance> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ProcessInstance::getApplicantId, applicantId);
+        return this.selectList(wrapper);
+    }
+
+    com.baomidou.mybatisplus.extension.plugins.pagination.Page<ProcessInstance> selectInstanceList(
+            com.baomidou.mybatisplus.extension.plugins.pagination.Page<ProcessInstance> page,
+            @Param("applicantId") Long applicantId,
+            @Param("status") Integer status);
+
+    com.baomidou.mybatisplus.extension.plugins.pagination.Page<ProcessInstance> selectParticipatedList(
+            com.baomidou.mybatisplus.extension.plugins.pagination.Page<ProcessInstance> page,
+            @Param("userId") Long userId);
+}

+ 15 - 0
src/main/java/com/qqflow/engine/domain/flow/model/FlowEdge.java

@@ -0,0 +1,15 @@
+package com.qqflow.engine.domain.flow.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class FlowEdge {
+
+    private String sourceNodeId;
+    private String targetNodeId;
+    private Map<String, Object> condition;
+}

+ 12 - 0
src/main/java/com/qqflow/engine/domain/flow/model/FlowModel.java

@@ -0,0 +1,12 @@
+package com.qqflow.engine.domain.flow.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class FlowModel {
+
+    private List<FlowNode> nodes;
+    private List<FlowEdge> edges;
+}

+ 16 - 0
src/main/java/com/qqflow/engine/domain/flow/model/FlowNode.java

@@ -0,0 +1,16 @@
+package com.qqflow.engine.domain.flow.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class FlowNode {
+
+    private String id;
+    private String type;
+    private String name;
+    private Map<String, Object> properties;
+}

+ 70 - 0
src/main/java/com/qqflow/engine/domain/flow/po/ApprovalRecord.java

@@ -0,0 +1,70 @@
+package com.qqflow.engine.domain.flow.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("bpm_approval_record")
+@Schema(description = "审批记录")
+public class ApprovalRecord {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "ID")
+    private Long id;
+
+    @TableField("task_id")
+    @Schema(description = "任务ID")
+    private Long taskId;
+
+    @TableField("instance_id")
+    @Schema(description = "流程实例ID")
+    private Long instanceId;
+
+    @TableField("node_id")
+    @Schema(description = "节点ID")
+    private String nodeId;
+
+    @TableField("node_name")
+    @Schema(description = "节点名称")
+    private String nodeName;
+
+    @TableField("operator_id")
+    @Schema(description = "操作人ID")
+    private Long operatorId;
+
+    @TableField("operator_name")
+    @Schema(description = "操作人姓名")
+    private String operatorName;
+
+    @TableField("action_type")
+    @Schema(description = "操作类型")
+    private String actionType;
+
+    @TableField("action_result")
+    @Schema(description = "操作结果")
+    private String actionResult;
+
+    @TableField("comment")
+    @Schema(description = "审批意见")
+    private String comment;
+
+    @TableField("attachment_urls")
+    @Schema(description = "附件URL")
+    private String attachmentUrls;
+
+    @TableField("create_time")
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @TableLogic
+    @TableField("deleted")
+    @Schema(description = "是否删除")
+    private Integer deleted;
+}

+ 94 - 0
src/main/java/com/qqflow/engine/domain/flow/po/ApprovalTask.java

@@ -0,0 +1,94 @@
+package com.qqflow.engine.domain.flow.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("bpm_approval_task")
+@Schema(description = "审批任务")
+public class ApprovalTask {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "ID")
+    private Long id;
+
+    @TableField("instance_id")
+    @Schema(description = "流程实例ID")
+    private Long instanceId;
+
+    @TableField("node_id")
+    @Schema(description = "节点ID")
+    private String nodeId;
+
+    @TableField("node_name")
+    @Schema(description = "节点名称")
+    private String nodeName;
+
+    @TableField("node_type")
+    @Schema(description = "节点类型")
+    private String nodeType;
+
+    @TableField("assignee_id")
+    @Schema(description = "处理人ID")
+    private Long assigneeId;
+
+    @TableField("assignee_type")
+    @Schema(description = "处理人类型")
+    private String assigneeType;
+
+    @TableField("task_status")
+    @Schema(description = "任务状态:0待处理1已处理2已转办3已跳过4已回退")
+    private Integer taskStatus;
+
+    @TableField("approval_result")
+    @Schema(description = "审批结果")
+    private String approvalResult;
+
+    @TableField("approval_comment")
+    @Schema(description = "审批意见")
+    private String approvalComment;
+
+    @TableField("attachment_urls")
+    @Schema(description = "附件URL")
+    private String attachmentUrls;
+
+    @TableField("timeout_time")
+    @Schema(description = "超时时间")
+    private LocalDateTime timeoutTime;
+
+    @TableField("timeout_action")
+    @Schema(description = "超时动作")
+    private String timeoutAction;
+
+    @TableField("create_time")
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @TableField("handle_time")
+    @Schema(description = "处理时间")
+    private LocalDateTime handleTime;
+
+    @TableLogic
+    @TableField("deleted")
+    @Schema(description = "是否删除")
+    private Integer deleted;
+
+    @TableField(exist = false)
+    @Schema(description = "流程名称")
+    private String processName;
+
+    @TableField(exist = false)
+    @Schema(description = "实例标题")
+    private String instanceTitle;
+
+    @TableField(exist = false)
+    @Schema(description = "实例编号")
+    private String instanceNo;
+}

+ 74 - 0
src/main/java/com/qqflow/engine/domain/flow/po/ProcessDefinition.java

@@ -0,0 +1,74 @@
+package com.qqflow.engine.domain.flow.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("bpm_process_definition")
+@Schema(description = "流程定义")
+public class ProcessDefinition {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "ID")
+    private Long id;
+
+    @TableField("process_code")
+    @Schema(description = "流程编码")
+    private String processCode;
+
+    @TableField("process_name")
+    @Schema(description = "流程名称")
+    private String processName;
+
+    @TableField("category")
+    @Schema(description = "分类")
+    private String category;
+
+    @TableField("form_id")
+    @Schema(description = "表单ID")
+    private Long formId;
+
+    @TableField("model_json")
+    @Schema(description = "模型JSON")
+    private String modelJson;
+
+    @TableField("version")
+    @Schema(description = "版本号")
+    private Integer version;
+
+    @TableField("status")
+    @Schema(description = "状态:0设计中1启用中2历史")
+    private Integer status;
+
+    @TableField("description")
+    @Schema(description = "描述")
+    private String description;
+
+    @TableField("create_by")
+    @Schema(description = "创建人")
+    private Long createBy;
+
+    @TableField("create_time")
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @TableField("update_by")
+    @Schema(description = "更新人")
+    private Long updateBy;
+
+    @TableField("update_time")
+    @Schema(description = "更新时间")
+    private LocalDateTime updateTime;
+
+    @TableLogic
+    @TableField("deleted")
+    @Schema(description = "是否删除")
+    private Integer deleted;
+}

+ 86 - 0
src/main/java/com/qqflow/engine/domain/flow/po/ProcessInstance.java

@@ -0,0 +1,86 @@
+package com.qqflow.engine.domain.flow.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("bpm_process_instance")
+@Schema(description = "流程实例")
+public class ProcessInstance {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "ID")
+    private Long id;
+
+    @TableField("instance_no")
+    @Schema(description = "实例编号")
+    private String instanceNo;
+
+    @TableField("process_definition_id")
+    @Schema(description = "流程定义ID")
+    private Long processDefinitionId;
+
+    @TableField("version")
+    @Schema(description = "版本号")
+    private Integer version;
+
+    @TableField("title")
+    @Schema(description = "标题")
+    private String title;
+
+    @TableField("applicant_id")
+    @Schema(description = "申请人ID")
+    private Long applicantId;
+
+    @TableField("applicant_dept_id")
+    @Schema(description = "申请人部门ID")
+    private Long applicantDeptId;
+
+    @TableField("form_data")
+    @Schema(description = "表单数据JSON")
+    private String formData;
+
+    @TableField("current_node_id")
+    @Schema(description = "当前节点ID")
+    private String currentNodeId;
+
+    @TableField("status")
+    @Schema(description = "状态:0待接收1待处理2已通过3已拒绝4已回退5整体完成6已撤回7已终止")
+    private Integer status;
+
+    @TableField("result")
+    @Schema(description = "结果")
+    private String result;
+
+    @TableField("start_time")
+    @Schema(description = "开始时间")
+    private LocalDateTime startTime;
+
+    @TableField("end_time")
+    @Schema(description = "结束时间")
+    private LocalDateTime endTime;
+
+    @TableField("create_time")
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @TableField("update_time")
+    @Schema(description = "更新时间")
+    private LocalDateTime updateTime;
+
+    @TableLogic
+    @TableField("deleted")
+    @Schema(description = "是否删除")
+    private Integer deleted;
+
+    @TableField(exist = false)
+    @Schema(description = "流程名称")
+    private String processName;
+}

+ 30 - 0
src/main/java/com/qqflow/engine/domain/flow/service/ApprovalTaskService.java

@@ -0,0 +1,30 @@
+package com.qqflow.engine.domain.flow.service;
+
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.domain.flow.dto.ApprovalRecordDTO;
+import com.qqflow.engine.domain.flow.dto.ApprovalTaskDTO;
+import com.qqflow.engine.domain.flow.dto.ApproveTaskDTO;
+import com.qqflow.engine.domain.flow.dto.TransferTaskDTO;
+
+import java.util.List;
+
+public interface ApprovalTaskService {
+
+    PageResult<ApprovalTaskDTO> todoList(Long assigneeId, String processName, Integer pageNum, Integer pageSize);
+
+    PageResult<ApprovalTaskDTO> handledList(Long assigneeId, String processName, Integer pageNum, Integer pageSize);
+
+    void approve(ApproveTaskDTO dto);
+
+    void reject(ApproveTaskDTO dto);
+
+    void returnTask(ApproveTaskDTO dto);
+
+    void transfer(TransferTaskDTO dto);
+
+    void addSign(Long taskId, Long assigneeId);
+
+    List<ApprovalRecordDTO> history(Long instanceId);
+
+    Long todoCount(Long assigneeId);
+}

+ 27 - 0
src/main/java/com/qqflow/engine/domain/flow/service/FlowEngineService.java

@@ -0,0 +1,27 @@
+package com.qqflow.engine.domain.flow.service;
+
+import com.qqflow.engine.domain.flow.enums.ApprovalAction;
+import com.qqflow.engine.domain.flow.model.FlowModel;
+import com.qqflow.engine.domain.flow.model.FlowNode;
+import com.qqflow.engine.domain.flow.po.ApprovalTask;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
+
+import java.util.List;
+
+public interface FlowEngineService {
+
+    FlowModel parseModel(String modelJson);
+
+    List<FlowNode> getNextNodes(FlowModel model, String currentNodeId);
+
+    List<Long> calculateAssignees(FlowNode node, ProcessInstance instance);
+
+    String getApproveMode(FlowNode node);
+
+    void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action);
+
+    void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action, String comment);
+
+    void startInstance(ProcessInstance instance, ProcessDefinition definition);
+}

+ 43 - 0
src/main/java/com/qqflow/engine/domain/flow/service/NotificationService.java

@@ -0,0 +1,43 @@
+package com.qqflow.engine.domain.flow.service;
+
+import java.util.List;
+
+/**
+ * 通知服务接口:用于向用户发送流程相关的通知
+ * 目前预留企业微信对接,后续可扩展邮件、短信、站内信等
+ */
+public interface NotificationService {
+
+    /**
+     * 发送任务分配通知(待我处理)
+     *
+     * @param assigneeIds   接收人ID列表
+     * @param processName   流程名称
+     * @param instanceTitle 实例标题
+     * @param nodeName      当前节点名称
+     */
+    void notifyTaskAssigned(List<Long> assigneeIds, String processName, String instanceTitle, String nodeName);
+
+    /**
+     * 发送任务完成通知(审批结果通知)
+     *
+     * @param instanceId    实例ID
+     * @param processName   流程名称
+     * @param instanceTitle 实例标题
+     * @param nodeName      节点名称
+     * @param operatorName  操作人姓名
+     * @param action        操作类型(通过/拒绝/退回/转办)
+     */
+    void notifyTaskCompleted(Long instanceId, String processName, String instanceTitle,
+                             String nodeName, String operatorName, String action);
+
+    /**
+     * 发送流程结束通知
+     *
+     * @param applicantId   发起人ID
+     * @param processName   流程名称
+     * @param instanceTitle 实例标题
+     * @param result        结果(通过/拒绝)
+     */
+    void notifyProcessCompleted(Long applicantId, String processName, String instanceTitle, String result);
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/flow/service/ProcessDefinitionService.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.flow.service;
+
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.domain.flow.dto.ProcessDefinitionDTO;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+
+import java.util.List;
+
+public interface ProcessDefinitionService {
+
+    Long saveDefinition(ProcessDefinition po);
+
+    Long updateDefinition(ProcessDefinition po);
+
+    void deleteDefinition(Long id);
+
+    ProcessDefinitionDTO getById(Long id);
+
+    PageResult<ProcessDefinitionDTO> page(Integer pageNum, Integer pageSize, String processName);
+
+    void publish(Long id);
+
+    void stop(Long id);
+
+    void enable(Long id);
+
+    List<ProcessDefinitionDTO> listEnabled();
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/flow/service/ProcessInstanceService.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.flow.service;
+
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.domain.flow.dto.ProcessInstanceDTO;
+import com.qqflow.engine.domain.flow.dto.ProcessProgressDTO;
+import com.qqflow.engine.domain.flow.dto.StartProcessDTO;
+
+import java.util.List;
+
+public interface ProcessInstanceService {
+
+    Long startProcess(StartProcessDTO dto);
+
+    PageResult<ProcessInstanceDTO> list(Long applicantId, Integer status, Integer pageNum, Integer pageSize);
+
+    ProcessInstanceDTO getDetail(Long id);
+
+    void revoke(Long id);
+
+    ProcessProgressDTO getProgress(Long id);
+
+    PageResult<ProcessInstanceDTO> participatedList(Long userId, Integer pageNum, Integer pageSize);
+}

+ 214 - 0
src/main/java/com/qqflow/engine/domain/flow/service/impl/ApprovalTaskServiceImpl.java

@@ -0,0 +1,214 @@
+package com.qqflow.engine.domain.flow.service.impl;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.common.exception.BusinessException;
+import com.qqflow.engine.common.util.SecurityUtils;
+import com.qqflow.engine.config.security.LoginUser;
+import com.qqflow.engine.domain.flow.assembler.ApprovalRecordAssembler;
+import com.qqflow.engine.domain.flow.assembler.ApprovalTaskAssembler;
+import com.qqflow.engine.domain.flow.dto.ApprovalRecordDTO;
+import com.qqflow.engine.domain.flow.dto.ApprovalTaskDTO;
+import com.qqflow.engine.domain.flow.dto.ApproveTaskDTO;
+import com.qqflow.engine.domain.flow.dto.TransferTaskDTO;
+import com.qqflow.engine.domain.flow.enums.ApprovalAction;
+import com.qqflow.engine.domain.flow.enums.ApprovalResult;
+import com.qqflow.engine.domain.flow.enums.ProcessStatus;
+import com.qqflow.engine.domain.flow.enums.TaskStatus;
+import com.qqflow.engine.domain.flow.mapper.ApprovalRecordMapper;
+import com.qqflow.engine.domain.flow.mapper.ApprovalTaskMapper;
+import com.qqflow.engine.domain.flow.mapper.ProcessInstanceMapper;
+import com.qqflow.engine.domain.flow.po.ApprovalRecord;
+import com.qqflow.engine.domain.flow.po.ApprovalTask;
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
+import com.qqflow.engine.domain.flow.event.TaskCompletedEvent;
+import com.qqflow.engine.domain.flow.service.ApprovalTaskService;
+import com.qqflow.engine.domain.flow.service.FlowEngineService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class ApprovalTaskServiceImpl implements ApprovalTaskService {
+
+    private final ApprovalTaskMapper approvalTaskMapper;
+    private final ApprovalRecordMapper approvalRecordMapper;
+    private final ProcessInstanceMapper processInstanceMapper;
+    private final ApprovalRecordAssembler approvalRecordAssembler;
+    private final ApprovalTaskAssembler approvalTaskAssembler;
+    private final FlowEngineService flowEngineService;
+    private final ApplicationEventPublisher eventPublisher;
+
+    @Override
+    public PageResult<ApprovalTaskDTO> todoList(Long assigneeId, String processName, Integer pageNum, Integer pageSize) {
+        Page<ApprovalTask> page = new Page<>(pageNum, pageSize);
+        this.approvalTaskMapper.selectTodoList(page, assigneeId, processName);
+        List<ApprovalTaskDTO> records = page.getRecords().stream()
+                .map(ApprovalTaskDTO::of)
+                .collect(Collectors.toList());
+        return PageResult.of(page.getTotal(), records);
+    }
+
+    @Override
+    public PageResult<ApprovalTaskDTO> handledList(Long assigneeId, String processName, Integer pageNum, Integer pageSize) {
+        Page<ApprovalTask> page = new Page<>(pageNum, pageSize);
+        this.approvalTaskMapper.selectHandledList(page, assigneeId, processName);
+        List<ApprovalTaskDTO> records = page.getRecords().stream()
+                .map(ApprovalTaskDTO::of)
+                .collect(Collectors.toList());
+        return PageResult.of(page.getTotal(), records);
+    }
+
+    @Override
+    @Transactional
+    public void approve(ApproveTaskDTO dto) {
+        ApprovalTask task = this.getPendingTask(dto.getTaskId());
+        ProcessInstance instance = this.getActiveInstance(task.getInstanceId());
+        Long operatorId = SecurityUtils.getUserId();
+        this.saveRecord(task, instance, operatorId, ApprovalAction.APPROVE, ApprovalResult.PASS.getCode(), dto.getComment(), dto.getAttachmentUrls());
+        this.flowEngineService.executeTransition(instance, task, ApprovalAction.APPROVE, dto.getComment());
+        this.publishTaskCompletedEvent(task, instance, operatorId, ApprovalAction.APPROVE.getCode(), ApprovalResult.PASS.getCode());
+    }
+
+    @Override
+    @Transactional
+    public void reject(ApproveTaskDTO dto) {
+        ApprovalTask task = this.getPendingTask(dto.getTaskId());
+        ProcessInstance instance = this.getActiveInstance(task.getInstanceId());
+        Long operatorId = SecurityUtils.getUserId();
+        this.saveRecord(task, instance, operatorId, ApprovalAction.REJECT, ApprovalResult.REJECT.getCode(), dto.getComment(), dto.getAttachmentUrls());
+        this.flowEngineService.executeTransition(instance, task, ApprovalAction.REJECT, dto.getComment());
+        this.publishTaskCompletedEvent(task, instance, operatorId, ApprovalAction.REJECT.getCode(), ApprovalResult.REJECT.getCode());
+    }
+
+    @Override
+    @Transactional
+    public void returnTask(ApproveTaskDTO dto) {
+        ApprovalTask task = this.getPendingTask(dto.getTaskId());
+        ProcessInstance instance = this.getActiveInstance(task.getInstanceId());
+        Long operatorId = SecurityUtils.getUserId();
+        this.saveRecord(task, instance, operatorId, ApprovalAction.RETURN, ApprovalResult.RETURN.getCode(), dto.getComment(), dto.getAttachmentUrls());
+        this.flowEngineService.executeTransition(instance, task, ApprovalAction.RETURN, dto.getComment());
+        this.publishTaskCompletedEvent(task, instance, operatorId, ApprovalAction.RETURN.getCode(), ApprovalResult.RETURN.getCode());
+    }
+
+    @Override
+    @Transactional
+    public void transfer(TransferTaskDTO dto) {
+        ApprovalTask task = this.getPendingTask(dto.getTaskId());
+        ProcessInstance instance = this.getActiveInstance(task.getInstanceId());
+        Long operatorId = SecurityUtils.getUserId();
+        Long transferToUserId = dto.getTransferToUserId() != null ? dto.getTransferToUserId() : dto.getTransferTo();
+        this.saveRecord(task, instance, operatorId, ApprovalAction.TRANSFER, ApprovalResult.TRANSFER.getCode(), dto.getComment(), null);
+        this.updateTaskToTransferred(task);
+        this.createTransferTask(task, instance, transferToUserId);
+        this.publishTaskCompletedEvent(task, instance, operatorId, ApprovalAction.TRANSFER.getCode(), ApprovalResult.TRANSFER.getCode());
+    }
+
+    @Override
+    @Transactional
+    public void addSign(Long taskId, Long assigneeId) {
+        ApprovalTask task = this.getPendingTask(taskId);
+        ApprovalTask newTask = this.approvalTaskAssembler.buildNew(
+                task.getInstanceId(), task.getNodeId(), task.getNodeName(),
+                task.getNodeType(), assigneeId, task.getAssigneeType(), TaskStatus.PENDING.getCode()
+        );
+        this.approvalTaskMapper.insert(newTask);
+        this.publishTaskCompletedEvent(task, null, SecurityUtils.getUserId(),
+                ApprovalAction.TRANSFER.getCode(), ApprovalResult.TRANSFER.getCode());
+    }
+
+    @Override
+    public List<ApprovalRecordDTO> history(Long instanceId) {
+        List<ApprovalRecord> records = this.approvalRecordMapper.selectRecordListByInstanceId(instanceId);
+        return records.stream()
+                .map(ApprovalRecordDTO::of)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Long todoCount(Long assigneeId) {
+        return this.approvalTaskMapper.selectCount(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalTask>()
+                        .eq(ApprovalTask::getAssigneeId, assigneeId)
+                        .eq(ApprovalTask::getTaskStatus, TaskStatus.PENDING.getCode()));
+    }
+
+    private ApprovalTask getPendingTask(Long taskId) {
+        ApprovalTask task = this.approvalTaskMapper.selectPendingById(taskId);
+        if (task == null) {
+            throw new BusinessException("任务不存在或已处理");
+        }
+        return task;
+    }
+
+    private ProcessInstance getActiveInstance(Long instanceId) {
+        ProcessInstance instance = this.processInstanceMapper.selectById(instanceId);
+        if (instance == null) {
+            throw new BusinessException("流程实例不存在");
+        }
+        if (!this.isActive(instance)) {
+            throw new BusinessException("流程已结束或不可操作");
+        }
+        return instance;
+    }
+
+    private boolean isActive(ProcessInstance instance) {
+        Integer status = instance.getStatus();
+        return ProcessStatus.PENDING_RECEIVE.getCode().equals(status)
+                || ProcessStatus.PENDING.getCode().equals(status)
+                || ProcessStatus.RETURNED.getCode().equals(status);
+    }
+
+    private void saveRecord(ApprovalTask task, ProcessInstance instance, Long operatorId,
+                            ApprovalAction action, String result, String comment, String attachmentUrls) {
+        String operatorName = null;
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (loginUser != null) {
+            operatorName = loginUser.getRealName();
+        }
+        ApprovalRecord record = this.approvalRecordAssembler.buildNew(
+                task.getId(), instance.getId(), task.getNodeId(), task.getNodeName(),
+                operatorId, operatorName, action.getCode(), result, comment, attachmentUrls
+        );
+        this.approvalRecordMapper.insert(record);
+    }
+
+    private void updateTaskToTransferred(ApprovalTask task) {
+        this.approvalTaskMapper.update(null,
+                new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
+                        .eq(ApprovalTask::getId, task.getId())
+                        .set(ApprovalTask::getTaskStatus, TaskStatus.TRANSFERRED.getCode()));
+    }
+
+    private void createTransferTask(ApprovalTask task, ProcessInstance instance, Long transferToUserId) {
+        ApprovalTask newTask = this.approvalTaskAssembler.buildNew(
+                instance.getId(), task.getNodeId(), task.getNodeName(),
+                task.getNodeType(), transferToUserId, "USER", TaskStatus.PENDING.getCode()
+        );
+        this.approvalTaskMapper.insert(newTask);
+    }
+
+    private void publishTaskCompletedEvent(ApprovalTask task, ProcessInstance instance, Long operatorId,
+                                           String action, String result) {
+        String operatorName = null;
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (loginUser != null) {
+            operatorName = loginUser.getRealName();
+        }
+        ProcessInstance inst = instance;
+        if (inst == null) {
+            inst = this.processInstanceMapper.selectById(task.getInstanceId());
+        }
+        String title = inst != null ? inst.getTitle() : "";
+        this.eventPublisher.publishEvent(new TaskCompletedEvent(
+                this, task.getInstanceId(), title, "", task.getNodeName(),
+                operatorId, operatorName, action, result
+        ));
+    }
+}

+ 364 - 0
src/main/java/com/qqflow/engine/domain/flow/service/impl/FlowEngineServiceImpl.java

@@ -0,0 +1,364 @@
+package com.qqflow.engine.domain.flow.service.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qqflow.engine.common.exception.BusinessException;
+import com.qqflow.engine.domain.flow.assembler.ApprovalTaskAssembler;
+import com.qqflow.engine.domain.flow.enums.ApprovalAction;
+import com.qqflow.engine.domain.flow.enums.ApprovalResult;
+import com.qqflow.engine.domain.flow.enums.NodeType;
+import com.qqflow.engine.domain.flow.enums.ProcessStatus;
+import com.qqflow.engine.domain.flow.enums.TaskStatus;
+import com.qqflow.engine.domain.flow.mapper.ApprovalTaskMapper;
+import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
+import com.qqflow.engine.domain.flow.mapper.ProcessInstanceMapper;
+import com.qqflow.engine.domain.flow.model.FlowEdge;
+import com.qqflow.engine.domain.flow.model.FlowModel;
+import com.qqflow.engine.domain.flow.model.FlowNode;
+import com.qqflow.engine.domain.flow.po.ApprovalTask;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
+import com.qqflow.engine.domain.flow.event.ProcessCompletedEvent;
+import com.qqflow.engine.domain.flow.event.TaskAssignedEvent;
+import com.qqflow.engine.domain.flow.service.FlowEngineService;
+import com.qqflow.engine.domain.system.entity.SysDept;
+import com.qqflow.engine.domain.system.entity.SysRole;
+import com.qqflow.engine.domain.system.entity.SysUser;
+import com.qqflow.engine.domain.system.entity.SysUserRole;
+import com.qqflow.engine.domain.system.mapper.SysDeptMapper;
+import com.qqflow.engine.domain.system.mapper.SysRoleMapper;
+import com.qqflow.engine.domain.system.mapper.SysUserMapper;
+import com.qqflow.engine.domain.system.mapper.SysUserRoleMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class FlowEngineServiceImpl implements FlowEngineService {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private final ProcessDefinitionMapper processDefinitionMapper;
+    private final ProcessInstanceMapper processInstanceMapper;
+    private final ApprovalTaskMapper approvalTaskMapper;
+    private final ApprovalTaskAssembler approvalTaskAssembler;
+    private final SysRoleMapper sysRoleMapper;
+    private final SysUserRoleMapper sysUserRoleMapper;
+    private final SysUserMapper sysUserMapper;
+    private final SysDeptMapper sysDeptMapper;
+    private final ApplicationEventPublisher eventPublisher;
+
+    @Override
+    @SneakyThrows
+    public FlowModel parseModel(String modelJson) {
+        return OBJECT_MAPPER.readValue(modelJson, FlowModel.class);
+    }
+
+    @Override
+    public List<FlowNode> getNextNodes(FlowModel model, String currentNodeId) {
+        List<String> targetIds = model.getEdges().stream()
+                .filter(e -> Objects.equals(e.getSourceNodeId(), currentNodeId))
+                .map(FlowEdge::getTargetNodeId)
+                .collect(Collectors.toList());
+        return model.getNodes().stream()
+                .filter(n -> targetIds.contains(n.getId()))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<Long> calculateAssignees(FlowNode node, ProcessInstance instance) {
+        Map<String, Object> props = node.getProperties();
+        if (props == null) {
+            return Collections.emptyList();
+        }
+        // 新格式
+        Object assigneeTypeObj = props.get("assigneeType");
+        Object assigneeValueObj = props.get("assigneeValue");
+        if (assigneeTypeObj != null) {
+            String assigneeType = assigneeTypeObj.toString();
+            String assigneeValue = assigneeValueObj != null ? assigneeValueObj.toString() : null;
+            return this.doCalculateAssignees(assigneeType, assigneeValue, instance);
+        }
+        // 兼容旧格式:approver + approveType
+        Object approverObj = props.get("approver");
+        if (approverObj != null) {
+            String approver = approverObj.toString();
+            return this.doCalculateAssignees("ROLE", approver, instance);
+        }
+        return Collections.emptyList();
+    }
+
+    @Override
+    public String getApproveMode(FlowNode node) {
+        Map<String, Object> props = node.getProperties();
+        if (props == null) {
+            return "or";
+        }
+        Object modeObj = props.get("approveMode");
+        if (modeObj != null) {
+            return modeObj.toString();
+        }
+        // 兼容旧格式
+        Object typeObj = props.get("approveType");
+        if (typeObj != null) {
+            return typeObj.toString();
+        }
+        return "or";
+    }
+
+    @Override
+    public void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action) {
+        this.executeTransition(instance, currentTask, action, null);
+    }
+
+    @Override
+    public void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action, String comment) {
+        switch (action) {
+            case APPROVE -> this.handleApprove(instance, currentTask, comment);
+            case REJECT -> this.handleReject(instance, currentTask, comment);
+            case RETURN -> this.handleReturn(instance, currentTask, comment);
+            default -> throw new BusinessException("不支持的操作类型");
+        }
+    }
+
+    @Override
+    public void startInstance(ProcessInstance instance, ProcessDefinition definition) {
+        FlowModel model = this.parseModel(definition.getModelJson());
+        FlowNode startNode = this.findStartNode(model);
+        List<FlowNode> nextNodes = this.getNextNodes(model, startNode.getId());
+        this.createTasksForNodes(instance, nextNodes, model);
+        this.updateInstanceNode(instance, nextNodes);
+    }
+
+    private List<Long> doCalculateAssignees(String assigneeType, String assigneeValue, ProcessInstance instance) {
+        if ("USER".equals(assigneeType) && assigneeValue != null) {
+            List<Long> list = new ArrayList<>();
+            for (String s : assigneeValue.split(",")) {
+                list.add(Long.valueOf(s.trim()));
+            }
+            return list;
+        }
+        if ("SELF".equals(assigneeType)) {
+            return Collections.singletonList(instance.getApplicantId());
+        }
+        if ("ROLE".equals(assigneeType) && assigneeValue != null) {
+            SysRole role = this.sysRoleMapper.selectByRoleCode(assigneeValue);
+            if (role == null) {
+                return Collections.emptyList();
+            }
+            List<SysUserRole> userRoles = this.sysUserRoleMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUserRole>()
+                            .eq(SysUserRole::getRoleId, role.getId()));
+            return userRoles.stream().map(SysUserRole::getUserId).distinct().collect(Collectors.toList());
+        }
+        if ("LEADER".equals(assigneeType)) {
+            SysUser applicant = this.sysUserMapper.selectById(instance.getApplicantId());
+            if (applicant == null || applicant.getDeptId() == null) {
+                return Collections.emptyList();
+            }
+            SysDept dept = this.sysDeptMapper.selectById(applicant.getDeptId());
+            if (dept == null || dept.getLeaderId() == null) {
+                return Collections.emptyList();
+            }
+            return Collections.singletonList(dept.getLeaderId());
+        }
+        return Collections.emptyList();
+    }
+
+    private void handleApprove(ProcessInstance instance, ApprovalTask currentTask, String comment) {
+        this.completeCurrentTask(currentTask, ApprovalResult.PASS.getCode(), comment);
+
+        FlowModel model = this.getModelByInstance(instance);
+        FlowNode currentNode = model.getNodes().stream()
+                .filter(n -> n.getId().equals(currentTask.getNodeId()))
+                .findFirst()
+                .orElse(null);
+        String approveMode = currentNode != null ? this.getApproveMode(currentNode) : "or";
+
+        // 会签模式:检查同节点是否还有未处理的任务
+        if ("and".equals(approveMode)) {
+            long pendingCount = this.approvalTaskMapper.selectCount(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalTask>()
+                            .eq(ApprovalTask::getInstanceId, instance.getId())
+                            .eq(ApprovalTask::getNodeId, currentTask.getNodeId())
+                            .eq(ApprovalTask::getTaskStatus, TaskStatus.PENDING.getCode()));
+            if (pendingCount > 0) {
+                // 还有未处理的任务,不推进流程
+                return;
+            }
+        } else {
+            // 或签模式:将同节点其他 PENDING 任务标记为 SKIPPED
+            this.approvalTaskMapper.update(null,
+                    new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
+                            .eq(ApprovalTask::getInstanceId, instance.getId())
+                            .eq(ApprovalTask::getNodeId, currentTask.getNodeId())
+                            .eq(ApprovalTask::getTaskStatus, TaskStatus.PENDING.getCode())
+                            .ne(ApprovalTask::getId, currentTask.getId())
+                            .set(ApprovalTask::getTaskStatus, TaskStatus.SKIPPED.getCode()));
+        }
+
+        List<FlowNode> nextNodes = this.getNextNodes(model, currentTask.getNodeId());
+        if (this.isEndNode(nextNodes)) {
+            this.completeInstance(instance);
+            return;
+        }
+        this.createTasksForNodes(instance, nextNodes, model);
+        this.updateInstanceNode(instance, nextNodes);
+    }
+
+    private void handleReject(ProcessInstance instance, ApprovalTask currentTask, String comment) {
+        this.completeCurrentTask(currentTask, ApprovalResult.REJECT.getCode(), comment);
+        this.rejectInstance(instance);
+    }
+
+    private void handleReturn(ProcessInstance instance, ApprovalTask currentTask, String comment) {
+        this.updateTaskStatus(currentTask, TaskStatus.RETURNED.getCode(), comment);
+        FlowModel model = this.getModelByInstance(instance);
+        FlowNode prevNode = this.findPreviousNode(model, currentTask.getNodeId());
+        if (prevNode == null) {
+            throw new BusinessException("无法找到回退目标节点");
+        }
+        List<FlowNode> returnTargets = Collections.singletonList(prevNode);
+        this.createTasksForNodes(instance, returnTargets, model);
+        this.updateInstanceNode(instance, returnTargets);
+    }
+
+    private FlowModel getModelByInstance(ProcessInstance instance) {
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
+        if (definition == null) {
+            throw new BusinessException("流程定义不存在");
+        }
+        return this.parseModel(definition.getModelJson());
+    }
+
+    private FlowNode findStartNode(FlowModel model) {
+        return model.getNodes().stream()
+                .filter(n -> NodeType.START.getCode().equals(n.getType()))
+                .findFirst()
+                .orElseThrow(() -> new BusinessException("未找到开始节点"));
+    }
+
+    private FlowNode findPreviousNode(FlowModel model, String currentNodeId) {
+        List<String> sourceIds = model.getEdges().stream()
+                .filter(e -> Objects.equals(e.getTargetNodeId(), currentNodeId))
+                .map(FlowEdge::getSourceNodeId)
+                .collect(Collectors.toList());
+        return model.getNodes().stream()
+                .filter(n -> sourceIds.contains(n.getId()) && !NodeType.START.getCode().equals(n.getType()))
+                .findFirst()
+                .orElse(null);
+    }
+
+    private boolean isEndNode(List<FlowNode> nodes) {
+        return nodes.stream().anyMatch(n -> NodeType.END.getCode().equals(n.getType()));
+    }
+
+    private void createTasksForNodes(ProcessInstance instance, List<FlowNode> nodes, FlowModel model) {
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
+        String processName = definition != null ? definition.getProcessName() : "";
+        for (FlowNode node : nodes) {
+            if (NodeType.START.getCode().equals(node.getType()) || NodeType.END.getCode().equals(node.getType())) {
+                continue;
+            }
+            // CC节点:不创建审批任务,仅记录(后续可扩展抄送通知)
+            if (NodeType.CC.getCode().equals(node.getType())) {
+                continue;
+            }
+            List<Long> assignees = this.calculateAssignees(node, instance);
+            if (assignees.isEmpty()) {
+                assignees = Collections.singletonList(instance.getApplicantId());
+            }
+            String approveMode = this.getApproveMode(node);
+            List<Long> finalAssignees = assignees;
+            if ("or".equals(approveMode) && assignees.size() > 1) {
+                finalAssignees = assignees;
+            }
+            for (Long assigneeId : finalAssignees) {
+                ApprovalTask task = this.approvalTaskAssembler.buildNew(
+                        instance.getId(), node.getId(), node.getName(),
+                        node.getType(), assigneeId, "USER", TaskStatus.PENDING.getCode()
+                );
+                this.approvalTaskMapper.insert(task);
+            }
+            // 发布任务分配通知事件
+            this.eventPublisher.publishEvent(new TaskAssignedEvent(
+                    this, instance.getId(), instance.getTitle(),
+                    processName, node.getName(), finalAssignees
+            ));
+        }
+    }
+
+    private void updateInstanceNode(ProcessInstance instance, List<FlowNode> nodes) {
+        FlowNode firstNode = nodes.stream()
+                .filter(n -> !NodeType.START.getCode().equals(n.getType()) && !NodeType.END.getCode().equals(n.getType()))
+                .findFirst()
+                .orElse(null);
+        if (firstNode == null) {
+            return;
+        }
+        Integer status = ProcessStatus.PENDING.getCode();
+        this.processInstanceMapper.update(null,
+                new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
+                        .eq(ProcessInstance::getId, instance.getId())
+                        .set(ProcessInstance::getCurrentNodeId, firstNode.getId())
+                        .set(ProcessInstance::getStatus, status));
+    }
+
+    private void completeInstance(ProcessInstance instance) {
+        this.processInstanceMapper.update(null,
+                new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
+                        .eq(ProcessInstance::getId, instance.getId())
+                        .set(ProcessInstance::getStatus, ProcessStatus.COMPLETED.getCode())
+                        .set(ProcessInstance::getEndTime, LocalDateTime.now())
+                        .set(ProcessInstance::getResult, ApprovalResult.PASS.getCode()));
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
+        String processName = definition != null ? definition.getProcessName() : "";
+        this.eventPublisher.publishEvent(new ProcessCompletedEvent(
+                this, instance.getId(), instance.getTitle(),
+                processName, instance.getApplicantId(), ApprovalResult.PASS.getCode()
+        ));
+    }
+
+    private void rejectInstance(ProcessInstance instance) {
+        this.processInstanceMapper.update(null,
+                new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
+                        .eq(ProcessInstance::getId, instance.getId())
+                        .set(ProcessInstance::getStatus, ProcessStatus.REJECTED.getCode())
+                        .set(ProcessInstance::getResult, ApprovalResult.REJECT.getCode())
+                        .set(ProcessInstance::getEndTime, LocalDateTime.now()));
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
+        String processName = definition != null ? definition.getProcessName() : "";
+        this.eventPublisher.publishEvent(new ProcessCompletedEvent(
+                this, instance.getId(), instance.getTitle(),
+                processName, instance.getApplicantId(), ApprovalResult.REJECT.getCode()
+        ));
+    }
+
+    private void completeCurrentTask(ApprovalTask task, String result, String comment) {
+        this.approvalTaskMapper.update(null,
+                new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
+                        .eq(ApprovalTask::getId, task.getId())
+                        .set(ApprovalTask::getTaskStatus, TaskStatus.HANDLED.getCode())
+                        .set(ApprovalTask::getApprovalResult, result)
+                        .set(ApprovalTask::getApprovalComment, comment)
+                        .set(ApprovalTask::getHandleTime, LocalDateTime.now()));
+    }
+
+    private void updateTaskStatus(ApprovalTask task, Integer status, String comment) {
+        this.approvalTaskMapper.update(null,
+                new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
+                        .eq(ApprovalTask::getId, task.getId())
+                        .set(ApprovalTask::getTaskStatus, status)
+                        .set(ApprovalTask::getApprovalComment, comment)
+                        .set(ApprovalTask::getHandleTime, LocalDateTime.now()));
+    }
+}

+ 160 - 0
src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessDefinitionServiceImpl.java

@@ -0,0 +1,160 @@
+package com.qqflow.engine.domain.flow.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.common.exception.BusinessException;
+import com.qqflow.engine.domain.flow.dto.ProcessDefinitionDTO;
+import com.qqflow.engine.domain.flow.enums.DefinitionStatus;
+import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import com.qqflow.engine.domain.flow.service.ProcessDefinitionService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class ProcessDefinitionServiceImpl implements ProcessDefinitionService {
+
+    private final ProcessDefinitionMapper processDefinitionMapper;
+
+    @Override
+    public Long saveDefinition(ProcessDefinition po) {
+        this.processDefinitionMapper.insert(po);
+        return po.getId();
+    }
+
+    @Override
+    public Long updateDefinition(ProcessDefinition po) {
+        ProcessDefinition existing = this.processDefinitionMapper.selectById(po.getId());
+        if (existing == null) {
+            throw new BusinessException("流程定义不存在");
+        }
+        if (DefinitionStatus.DESIGNING.getCode().equals(existing.getStatus())) {
+            // 设计中的流程直接更新
+            this.processDefinitionMapper.update(null, new LambdaUpdateWrapper<ProcessDefinition>()
+                    .eq(ProcessDefinition::getId, po.getId())
+                    .set(ProcessDefinition::getProcessCode, po.getProcessCode())
+                    .set(ProcessDefinition::getProcessName, po.getProcessName())
+                    .set(ProcessDefinition::getCategory, po.getCategory())
+                    .set(ProcessDefinition::getFormId, po.getFormId())
+                    .set(ProcessDefinition::getModelJson, po.getModelJson())
+                    .set(ProcessDefinition::getDescription, po.getDescription()));
+            return po.getId();
+        } else {
+            // 已发布/已停用的流程,复制为新版本进行设计
+            ProcessDefinition newDef = new ProcessDefinition();
+            newDef.setProcessCode(po.getProcessCode());
+            newDef.setProcessName(po.getProcessName());
+            newDef.setCategory(po.getCategory());
+            newDef.setFormId(po.getFormId());
+            newDef.setModelJson(po.getModelJson());
+            newDef.setDescription(po.getDescription());
+            newDef.setVersion(existing.getVersion() + 1);
+            newDef.setStatus(DefinitionStatus.DESIGNING.getCode());
+            newDef.setCreateBy(existing.getCreateBy());
+            this.processDefinitionMapper.insert(newDef);
+            return newDef.getId();
+        }
+    }
+
+    @Override
+    public void deleteDefinition(Long id) {
+        ProcessDefinition existing = this.processDefinitionMapper.selectById(id);
+        this.validateDesigning(existing);
+        this.processDefinitionMapper.deleteById(id);
+    }
+
+    @Override
+    public ProcessDefinitionDTO getById(Long id) {
+        ProcessDefinition po = this.processDefinitionMapper.selectById(id);
+        return ProcessDefinitionDTO.of(po);
+    }
+
+    @Override
+    public PageResult<ProcessDefinitionDTO> page(Integer pageNum, Integer pageSize, String processName) {
+        Page<ProcessDefinition> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<ProcessDefinition> wrapper = new LambdaQueryWrapper<>();
+        if (processName != null && !processName.isEmpty()) {
+            wrapper.like(ProcessDefinition::getProcessName, processName);
+        }
+        wrapper.orderByDesc(ProcessDefinition::getCreateTime);
+        this.processDefinitionMapper.selectPage(page, wrapper);
+        List<ProcessDefinitionDTO> records = page.getRecords().stream()
+                .map(ProcessDefinitionDTO::of)
+                .collect(Collectors.toList());
+        return PageResult.of(page.getTotal(), records);
+    }
+
+    @Override
+    public void publish(Long id) {
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(id);
+        this.validateDesigning(definition);
+        this.disableOldVersion(definition.getProcessCode());
+        this.enableDefinition(id);
+    }
+
+    @Override
+    public void stop(Long id) {
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(id);
+        if (definition == null) {
+            throw new BusinessException("流程定义不存在");
+        }
+        if (!DefinitionStatus.ENABLED.getCode().equals(definition.getStatus())) {
+            throw new BusinessException("只有启用中的流程才能停用");
+        }
+        LambdaUpdateWrapper<ProcessDefinition> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(ProcessDefinition::getId, id)
+                .set(ProcessDefinition::getStatus, DefinitionStatus.HISTORICAL.getCode());
+        this.processDefinitionMapper.update(null, wrapper);
+    }
+
+    @Override
+    public void enable(Long id) {
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(id);
+        if (definition == null) {
+            throw new BusinessException("流程定义不存在");
+        }
+        if (!DefinitionStatus.HISTORICAL.getCode().equals(definition.getStatus())) {
+            throw new BusinessException("只有已停用的流程才能启用");
+        }
+        this.disableOldVersion(definition.getProcessCode());
+        this.enableDefinition(id);
+    }
+
+    @Override
+    public List<ProcessDefinitionDTO> listEnabled() {
+        List<ProcessDefinition> list = this.processDefinitionMapper.selectEnabledList();
+        return list.stream()
+                .map(ProcessDefinitionDTO::of)
+                .collect(Collectors.toList());
+    }
+
+    private void validateDesigning(ProcessDefinition definition) {
+        if (definition == null) {
+            throw new BusinessException("流程定义不存在");
+        }
+        if (!DefinitionStatus.DESIGNING.getCode().equals(definition.getStatus())) {
+            throw new BusinessException("只有设计中的流程才能操作");
+        }
+    }
+
+    private void disableOldVersion(String processCode) {
+        LambdaUpdateWrapper<ProcessDefinition> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(ProcessDefinition::getProcessCode, processCode)
+                .eq(ProcessDefinition::getStatus, DefinitionStatus.ENABLED.getCode())
+                .set(ProcessDefinition::getStatus, DefinitionStatus.HISTORICAL.getCode());
+        this.processDefinitionMapper.update(null, wrapper);
+    }
+
+    private void enableDefinition(Long id) {
+        LambdaUpdateWrapper<ProcessDefinition> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(ProcessDefinition::getId, id)
+                .set(ProcessDefinition::getStatus, DefinitionStatus.ENABLED.getCode());
+        this.processDefinitionMapper.update(null, wrapper);
+    }
+}

+ 223 - 0
src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessInstanceServiceImpl.java

@@ -0,0 +1,223 @@
+package com.qqflow.engine.domain.flow.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.common.exception.BusinessException;
+import com.qqflow.engine.common.util.SecurityUtils;
+import com.qqflow.engine.domain.flow.assembler.ProcessInstanceAssembler;
+import com.qqflow.engine.domain.flow.dto.ApprovalRecordDTO;
+import com.qqflow.engine.domain.flow.dto.ApprovalTaskDTO;
+import com.qqflow.engine.domain.flow.dto.NodeProgressDTO;
+import com.qqflow.engine.domain.flow.dto.ProcessInstanceDTO;
+import com.qqflow.engine.domain.flow.dto.ProcessProgressDTO;
+import com.qqflow.engine.domain.flow.dto.StartProcessDTO;
+import com.qqflow.engine.domain.flow.enums.DefinitionStatus;
+import com.qqflow.engine.domain.flow.enums.NodeType;
+import com.qqflow.engine.domain.flow.enums.ProcessStatus;
+import com.qqflow.engine.domain.flow.enums.TaskStatus;
+import com.qqflow.engine.domain.flow.mapper.ApprovalRecordMapper;
+import com.qqflow.engine.domain.flow.mapper.ApprovalTaskMapper;
+import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
+import com.qqflow.engine.domain.flow.mapper.ProcessInstanceMapper;
+import com.qqflow.engine.domain.flow.model.FlowModel;
+import com.qqflow.engine.domain.flow.model.FlowNode;
+import com.qqflow.engine.domain.flow.po.ApprovalRecord;
+import com.qqflow.engine.domain.flow.po.ApprovalTask;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
+import com.qqflow.engine.domain.flow.service.FlowEngineService;
+import com.qqflow.engine.domain.flow.service.ProcessInstanceService;
+import com.qqflow.engine.domain.system.entity.SysUser;
+import com.qqflow.engine.domain.system.mapper.SysUserMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class ProcessInstanceServiceImpl implements ProcessInstanceService {
+
+    private final ProcessInstanceMapper processInstanceMapper;
+    private final ProcessDefinitionMapper processDefinitionMapper;
+    private final ProcessInstanceAssembler processInstanceAssembler;
+    private final FlowEngineService flowEngineService;
+    private final ApprovalTaskMapper approvalTaskMapper;
+    private final ApprovalRecordMapper approvalRecordMapper;
+    private final SysUserMapper sysUserMapper;
+
+    @Override
+    public Long startProcess(StartProcessDTO dto) {
+        ProcessDefinition definition = this.getEnabledDefinition(dto.getProcessDefinitionId());
+        ProcessInstance instance = this.buildInstance(dto, definition);
+        this.processInstanceMapper.insert(instance);
+        this.flowEngineService.startInstance(instance, definition);
+        return instance.getId();
+    }
+
+    @Override
+    public PageResult<ProcessInstanceDTO> list(Long applicantId, Integer status, Integer pageNum, Integer pageSize) {
+        Page<ProcessInstance> page = new Page<>(pageNum, pageSize);
+        this.processInstanceMapper.selectInstanceList(page, applicantId, status);
+        List<ProcessInstanceDTO> records = page.getRecords().stream()
+                .map(ProcessInstanceDTO::of)
+                .collect(Collectors.toList());
+        return PageResult.of(page.getTotal(), records);
+    }
+
+    @Override
+    public ProcessInstanceDTO getDetail(Long id) {
+        ProcessInstance po = this.processInstanceMapper.selectById(id);
+        if (po == null) {
+            throw new BusinessException("流程实例不存在");
+        }
+        return ProcessInstanceDTO.of(po);
+    }
+
+    @Override
+    public void revoke(Long id) {
+        ProcessInstance instance = this.processInstanceMapper.selectById(id);
+        this.validateRevocable(instance);
+        this.processInstanceMapper.update(null, Wrappers.<ProcessInstance>lambdaUpdate()
+                .eq(ProcessInstance::getId, id)
+                .set(ProcessInstance::getStatus, ProcessStatus.REVOKED.getCode())
+                .set(ProcessInstance::getEndTime, LocalDateTime.now()));
+    }
+
+    @Override
+    public ProcessProgressDTO getProgress(Long id) {
+        ProcessInstance instance = this.processInstanceMapper.selectById(id);
+        if (instance == null) {
+            throw new BusinessException("流程实例不存在");
+        }
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
+        if (definition == null) {
+            throw new BusinessException("流程定义不存在");
+        }
+
+        FlowModel model = this.flowEngineService.parseModel(definition.getModelJson());
+        if (model == null || model.getNodes() == null) {
+            throw new BusinessException("流程模型解析失败");
+        }
+        List<ApprovalTask> tasks = this.approvalTaskMapper.selectByInstanceId(instance.getId());
+        List<ApprovalRecord> records = this.approvalRecordMapper.selectRecordListByInstanceId(instance.getId());
+
+        Long currentUserId = SecurityUtils.getUserId();
+        String currentNodeId = instance.getCurrentNodeId();
+        Integer instanceStatus = instance.getStatus();
+
+        // 构建节点进度列表
+        List<NodeProgressDTO> nodeProgressList = new ArrayList<>();
+        int remainingCount = 0;
+
+        for (FlowNode node : model.getNodes()) {
+            if (NodeType.START.getCode().equals(node.getType()) || NodeType.END.getCode().equals(node.getType())) {
+                continue;
+            }
+            NodeProgressDTO dto = new NodeProgressDTO();
+            dto.setNodeId(node.getId());
+            dto.setNodeName(node.getName());
+            dto.setNodeType(node.getType());
+
+            List<ApprovalTask> nodeTasks = tasks.stream()
+                    .filter(t -> t.getNodeId().equals(node.getId()))
+                    .collect(Collectors.toList());
+            dto.setTasks(nodeTasks.stream().map(ApprovalTaskDTO::of).collect(Collectors.toList()));
+
+            boolean hasHandled = nodeTasks.stream().anyMatch(t -> TaskStatus.HANDLED.getCode().equals(t.getTaskStatus()));
+            boolean hasPending = nodeTasks.stream().anyMatch(t -> TaskStatus.PENDING.getCode().equals(t.getTaskStatus()));
+            boolean isCurrent = node.getId().equals(currentNodeId);
+            boolean isCompleted = hasHandled && !hasPending;
+
+            if (isCompleted || hasHandled) {
+                dto.setStatus("completed");
+            } else if (isCurrent || hasPending) {
+                dto.setStatus("current");
+                remainingCount++;
+            } else {
+                dto.setStatus("pending");
+                remainingCount++;
+            }
+
+            // 判断当前用户是否需要处理该节点
+            boolean isMyTurn = nodeTasks.stream()
+                    .anyMatch(t -> TaskStatus.PENDING.getCode().equals(t.getTaskStatus())
+                            && currentUserId.equals(t.getAssigneeId()));
+            dto.setIsMyTurn(isMyTurn);
+
+            nodeProgressList.add(dto);
+        }
+
+        // 如果流程已结束,剩余节点数为0
+        if (ProcessStatus.COMPLETED.getCode().equals(instanceStatus)
+                || ProcessStatus.REJECTED.getCode().equals(instanceStatus)
+                || ProcessStatus.REVOKED.getCode().equals(instanceStatus)
+                || ProcessStatus.TERMINATED.getCode().equals(instanceStatus)) {
+            remainingCount = 0;
+        }
+
+        ProcessProgressDTO progress = new ProcessProgressDTO();
+        progress.setInstance(ProcessInstanceDTO.of(instance));
+        progress.setDefinition(com.qqflow.engine.domain.flow.dto.ProcessDefinitionDTO.of(definition));
+        progress.setNodes(nodeProgressList);
+        progress.setRecords(records.stream().map(ApprovalRecordDTO::of).collect(Collectors.toList()));
+        progress.setRemainingNodeCount(remainingCount);
+        return progress;
+    }
+
+    @Override
+    public PageResult<ProcessInstanceDTO> participatedList(Long userId, Integer pageNum, Integer pageSize) {
+        Page<ProcessInstance> page = new Page<>(pageNum, pageSize);
+        this.processInstanceMapper.selectParticipatedList(page, userId);
+        List<ProcessInstanceDTO> records = page.getRecords().stream()
+                .map(ProcessInstanceDTO::of)
+                .collect(Collectors.toList());
+        return PageResult.of(page.getTotal(), records);
+    }
+
+    private ProcessDefinition getEnabledDefinition(Long processDefinitionId) {
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(processDefinitionId);
+        if (definition == null || !DefinitionStatus.ENABLED.getCode().equals(definition.getStatus())) {
+            throw new BusinessException("流程定义不存在或未启用");
+        }
+        return definition;
+    }
+
+    private ProcessInstance buildInstance(StartProcessDTO dto, ProcessDefinition definition) {
+        Long userId = SecurityUtils.getUserId();
+        SysUser user = this.sysUserMapper.selectById(userId);
+        Long deptId = user != null ? user.getDeptId() : null;
+        return this.processInstanceAssembler.buildNew(
+                this.generateInstanceNo(),
+                definition.getId(),
+                definition.getVersion(),
+                dto.getTitle(),
+                userId,
+                deptId,
+                dto.getFormData(),
+                null,
+                ProcessStatus.PENDING_RECEIVE.getCode()
+        );
+    }
+
+    private String generateInstanceNo() {
+        return "PI" + System.currentTimeMillis();
+    }
+
+    private void validateRevocable(ProcessInstance instance) {
+        if (instance == null) {
+            throw new BusinessException("流程实例不存在");
+        }
+        if (!ProcessStatus.PENDING_RECEIVE.getCode().equals(instance.getStatus())
+                && !ProcessStatus.PENDING.getCode().equals(instance.getStatus())) {
+            throw new BusinessException("当前状态不可撤回");
+        }
+    }
+}

+ 59 - 0
src/main/java/com/qqflow/engine/domain/flow/service/impl/WeComNotificationService.java

@@ -0,0 +1,59 @@
+package com.qqflow.engine.domain.flow.service.impl;
+
+import com.qqflow.engine.domain.flow.service.NotificationService;
+import com.qqflow.engine.domain.system.entity.SysUser;
+import com.qqflow.engine.domain.system.mapper.SysUserMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 企业微信通知服务(预留实现)
+ * <p>
+ * TODO: 后续对接企业微信 API,实现以下功能:
+ * 1. 通过企业微信应用消息推送审批通知
+ * 2. 支持@用户提醒
+ * 3. 支持消息模板(审批申请、审批结果、流程结束)
+ * <p>
+ * 对接时需要:
+ * - 企业微信 corpId, agentId, secret
+ * - 用户企微账号与系统用户的映射(建议通过手机号匹配)
+ * - 调用企微消息推送 API: POST https://qyapi.weixin.qq.com/cgi-bin/message/send
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WeComNotificationService implements NotificationService {
+
+    private final SysUserMapper sysUserMapper;
+
+    @Override
+    public void notifyTaskAssigned(List<Long> assigneeIds, String processName, String instanceTitle, String nodeName) {
+        for (Long userId : assigneeIds) {
+            SysUser user = sysUserMapper.selectById(userId);
+            String userName = user != null ? user.getRealName() : String.valueOf(userId);
+            log.info("[企微通知-预留] 待办提醒 | 接收人:{} | 流程:{} | 标题:{} | 节点:{}",
+                    userName, processName, instanceTitle, nodeName);
+        }
+        // TODO: 调用企微 API 发送应用消息
+    }
+
+    @Override
+    public void notifyTaskCompleted(Long instanceId, String processName, String instanceTitle,
+                                    String nodeName, String operatorName, String action) {
+        log.info("[企微通知-预留] 审批结果 | 实例ID:{} | 流程:{} | 标题:{} | 节点:{} | 操作人:{} | 操作:{}",
+                instanceId, processName, instanceTitle, nodeName, operatorName, action);
+        // TODO: 调用企微 API 发送审批结果通知(通知发起人或相关人)
+    }
+
+    @Override
+    public void notifyProcessCompleted(Long applicantId, String processName, String instanceTitle, String result) {
+        SysUser applicant = sysUserMapper.selectById(applicantId);
+        String applicantName = applicant != null ? applicant.getRealName() : String.valueOf(applicantId);
+        log.info("[企微通知-预留] 流程结束 | 接收人:{} | 流程:{} | 标题:{} | 结果:{}",
+                applicantName, processName, instanceTitle, result);
+        // TODO: 调用企微 API 发送流程结束通知
+    }
+}

+ 68 - 0
src/main/java/com/qqflow/engine/domain/flow/statemachine/ProcessInstanceStateMachineConfig.java

@@ -0,0 +1,68 @@
+package com.qqflow.engine.domain.flow.statemachine;
+
+import com.qqflow.engine.domain.flow.enums.ProcessEvent;
+import com.qqflow.engine.domain.flow.enums.ProcessStatus;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.statemachine.config.EnableStateMachineFactory;
+import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
+import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
+import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
+
+import java.util.EnumSet;
+
+@Configuration
+@EnableStateMachineFactory
+public class ProcessInstanceStateMachineConfig extends StateMachineConfigurerAdapter<ProcessStatus, ProcessEvent> {
+
+    @Override
+    public void configure(StateMachineStateConfigurer<ProcessStatus, ProcessEvent> states) throws Exception {
+        states.withStates()
+                .initial(ProcessStatus.PENDING_RECEIVE)
+                .states(EnumSet.allOf(ProcessStatus.class));
+    }
+
+    @Override
+    public void configure(StateMachineTransitionConfigurer<ProcessStatus, ProcessEvent> transitions) throws Exception {
+        this.configureActiveTransitions(transitions);
+        this.configureTerminalTransitions(transitions);
+    }
+
+    private void configureActiveTransitions(StateMachineTransitionConfigurer<ProcessStatus, ProcessEvent> transitions) throws Exception {
+        transitions
+                .withExternal()
+                .source(ProcessStatus.PENDING_RECEIVE).target(ProcessStatus.PENDING).event(ProcessEvent.RECEIVE)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.PENDING).target(ProcessStatus.APPROVED).event(ProcessEvent.APPROVE)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.PENDING).target(ProcessStatus.REJECTED).event(ProcessEvent.REJECT)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.PENDING).target(ProcessStatus.RETURNED).event(ProcessEvent.RETURN)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.RETURNED).target(ProcessStatus.PENDING).event(ProcessEvent.RECEIVE);
+    }
+
+    private void configureTerminalTransitions(StateMachineTransitionConfigurer<ProcessStatus, ProcessEvent> transitions) throws Exception {
+        transitions
+                .withExternal()
+                .source(ProcessStatus.APPROVED).target(ProcessStatus.COMPLETED).event(ProcessEvent.COMPLETE)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.PENDING_RECEIVE).target(ProcessStatus.REVOKED).event(ProcessEvent.REVOKE)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.PENDING).target(ProcessStatus.REVOKED).event(ProcessEvent.REVOKE)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.RETURNED).target(ProcessStatus.REVOKED).event(ProcessEvent.REVOKE)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.PENDING_RECEIVE).target(ProcessStatus.TERMINATED).event(ProcessEvent.TERMINATE)
+                .and()
+                .withExternal()
+                .source(ProcessStatus.PENDING).target(ProcessStatus.TERMINATED).event(ProcessEvent.TERMINATE);
+    }
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/system/assembler/DeptAssembler.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.system.assembler;
+
+import com.qqflow.engine.domain.system.dto.DeptDTO;
+import com.qqflow.engine.domain.system.entity.SysDept;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class DeptAssembler {
+
+    private DeptAssembler() {
+    }
+
+    public static DeptDTO toDTO(SysDept dept) {
+        return DeptDTO.of(dept);
+    }
+
+    public static List<DeptDTO> toDTOList(List<SysDept> depts) {
+        return depts.stream()
+                .map(DeptDTO::of)
+                .collect(Collectors.toList());
+    }
+}

+ 47 - 0
src/main/java/com/qqflow/engine/domain/system/assembler/MenuAssembler.java

@@ -0,0 +1,47 @@
+package com.qqflow.engine.domain.system.assembler;
+
+import com.qqflow.engine.domain.system.dto.MenuDTO;
+import com.qqflow.engine.domain.system.entity.SysMenu;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class MenuAssembler {
+
+    private MenuAssembler() {
+    }
+
+    public static MenuDTO toDTO(SysMenu menu) {
+        return MenuDTO.of(menu);
+    }
+
+    public static List<MenuDTO> toDTOList(List<SysMenu> menus) {
+        return menus.stream()
+                .map(MenuDTO::of)
+                .collect(Collectors.toList());
+    }
+
+    public static List<MenuDTO> toTree(List<SysMenu> menus) {
+        List<MenuDTO> dtoList = menus.stream()
+                .map(MenuDTO::of)
+                .sorted(Comparator.comparingInt(MenuDTO::getSortOrder))
+                .collect(Collectors.toList());
+        Map<Long, MenuDTO> map = dtoList.stream()
+                .collect(Collectors.toMap(MenuDTO::getId, dto -> dto));
+        List<MenuDTO> roots = new ArrayList<>();
+        for (MenuDTO dto : dtoList) {
+            if (dto.getParentId() == null || dto.getParentId() == 0L) {
+                roots.add(dto);
+                continue;
+            }
+            MenuDTO parent = map.get(dto.getParentId());
+            if (parent != null) {
+                parent.getChildren().add(dto);
+            }
+        }
+        return roots;
+    }
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/system/assembler/RoleAssembler.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.system.assembler;
+
+import com.qqflow.engine.domain.system.dto.RoleDTO;
+import com.qqflow.engine.domain.system.entity.SysRole;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class RoleAssembler {
+
+    private RoleAssembler() {
+    }
+
+    public static RoleDTO toDTO(SysRole role) {
+        return RoleDTO.of(role);
+    }
+
+    public static List<RoleDTO> toDTOList(List<SysRole> roles) {
+        return roles.stream()
+                .map(RoleDTO::of)
+                .collect(Collectors.toList());
+    }
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/system/assembler/UserAssembler.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.system.assembler;
+
+import com.qqflow.engine.domain.system.dto.UserDTO;
+import com.qqflow.engine.domain.system.entity.SysUser;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class UserAssembler {
+
+    private UserAssembler() {
+    }
+
+    public static UserDTO toDTO(SysUser user) {
+        return UserDTO.of(user);
+    }
+
+    public static List<UserDTO> toDTOList(List<SysUser> users) {
+        return users.stream()
+                .map(UserDTO::of)
+                .collect(Collectors.toList());
+    }
+}

+ 66 - 0
src/main/java/com/qqflow/engine/domain/system/controller/AuthController.java

@@ -0,0 +1,66 @@
+package com.qqflow.engine.domain.system.controller;
+
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.common.util.SecurityUtils;
+import com.qqflow.engine.config.security.LoginUser;
+import com.qqflow.engine.domain.system.assembler.UserAssembler;
+import com.qqflow.engine.domain.system.dto.LoginDTO;
+import com.qqflow.engine.domain.system.dto.UserDTO;
+import com.qqflow.engine.domain.system.entity.SysUser;
+import com.qqflow.engine.domain.system.service.SysUserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Tag(name = "认证管理")
+@RestController
+@RequestMapping("/auth")
+public class AuthController {
+
+    @Resource
+    private SysUserService sysUserService;
+
+    @Operation(summary = "用户登录")
+    @PostMapping("/login")
+    public Result<Map<String, String>> login(@Valid @RequestBody LoginDTO loginDTO) {
+        String token = sysUserService.login(loginDTO);
+        Map<String, String> map = new HashMap<>();
+        map.put("token", token);
+        return Result.ok(map);
+    }
+
+    @Operation(summary = "用户登出")
+    @PostMapping("/logout")
+    public Result<Void> logout(HttpServletRequest request) {
+        String token = request.getHeader("Authorization");
+        sysUserService.logout(token);
+        return Result.ok();
+    }
+
+    @Operation(summary = "刷新token")
+    @PostMapping("/refresh")
+    public Result<Map<String, String>> refresh(HttpServletRequest request) {
+        String token = request.getHeader("Authorization");
+        String newToken = sysUserService.refreshToken(token);
+        Map<String, String> map = new HashMap<>();
+        map.put("token", newToken);
+        return Result.ok(map);
+    }
+
+    @Operation(summary = "获取当前登录用户信息")
+    @GetMapping("/info")
+    public Result<UserDTO> info() {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (loginUser == null) {
+            return Result.error(401, "未登录");
+        }
+        SysUser user = sysUserService.getByUsername(loginUser.getUsername());
+        return Result.ok(UserAssembler.toDTO(user));
+    }
+}

+ 63 - 0
src/main/java/com/qqflow/engine/domain/system/controller/SysDeptController.java

@@ -0,0 +1,63 @@
+package com.qqflow.engine.domain.system.controller;
+
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.domain.system.assembler.DeptAssembler;
+import com.qqflow.engine.domain.system.dto.DeptDTO;
+import com.qqflow.engine.domain.system.entity.SysDept;
+import com.qqflow.engine.domain.system.service.SysDeptService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "部门管理")
+@RestController
+@RequestMapping("/system/dept")
+public class SysDeptController {
+
+    @Resource
+    private SysDeptService sysDeptService;
+
+    @Operation(summary = "查询部门树")
+    @GetMapping("/list")
+    public Result<List<DeptDTO>> list() {
+        return Result.ok(sysDeptService.buildDeptTree());
+    }
+
+    @Operation(summary = "根据ID查询部门")
+    @GetMapping("/{id}")
+    public Result<DeptDTO> getById(@PathVariable Long id) {
+        SysDept dept = sysDeptService.getDeptById(id);
+        return Result.ok(DeptAssembler.toDTO(dept));
+    }
+
+    @Operation(summary = "查询部门树")
+    @GetMapping("/tree")
+    public Result<List<DeptDTO>> tree() {
+        List<DeptDTO> trees = sysDeptService.buildDeptTree();
+        return Result.ok(trees);
+    }
+
+    @Operation(summary = "新增部门")
+    @PostMapping
+    public Result<Void> add(@RequestBody SysDept dept) {
+        sysDeptService.addDept(dept);
+        return Result.ok();
+    }
+
+    @Operation(summary = "修改部门")
+    @PutMapping
+    public Result<Void> update(@RequestBody SysDept dept) {
+        sysDeptService.updateDept(dept);
+        return Result.ok();
+    }
+
+    @Operation(summary = "删除部门")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        sysDeptService.removeDept(id);
+        return Result.ok();
+    }
+}

+ 75 - 0
src/main/java/com/qqflow/engine/domain/system/controller/SysMenuController.java

@@ -0,0 +1,75 @@
+package com.qqflow.engine.domain.system.controller;
+
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.common.util.SecurityUtils;
+import com.qqflow.engine.domain.system.assembler.MenuAssembler;
+import com.qqflow.engine.domain.system.dto.MenuDTO;
+import com.qqflow.engine.domain.system.entity.SysMenu;
+import com.qqflow.engine.domain.system.service.SysMenuService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Set;
+
+@Tag(name = "菜单管理")
+@RestController
+@RequestMapping("/system/menu")
+public class SysMenuController {
+
+    @Resource
+    private SysMenuService sysMenuService;
+
+    @Operation(summary = "查询菜单树")
+    @GetMapping("/list")
+    public Result<List<MenuDTO>> list() {
+        List<SysMenu> menus = sysMenuService.list();
+        return Result.ok(MenuAssembler.toTree(menus));
+    }
+
+    @Operation(summary = "根据ID查询菜单")
+    @GetMapping("/{id}")
+    public Result<MenuDTO> getById(@PathVariable Long id) {
+        SysMenu menu = sysMenuService.getMenuById(id);
+        return Result.ok(MenuAssembler.toDTO(menu));
+    }
+
+    @Operation(summary = "查询当前用户菜单树")
+    @GetMapping("/tree")
+    public Result<List<MenuDTO>> tree() {
+        Long userId = SecurityUtils.getUserId();
+        List<MenuDTO> trees = sysMenuService.listMenuTreeByUserId(userId);
+        return Result.ok(trees);
+    }
+
+    @Operation(summary = "查询当前用户权限标识")
+    @GetMapping("/permissions")
+    public Result<Set<String>> permissions() {
+        Long userId = SecurityUtils.getUserId();
+        Set<String> perms = sysMenuService.listPermissionsByUserId(userId);
+        return Result.ok(perms);
+    }
+
+    @Operation(summary = "新增菜单")
+    @PostMapping
+    public Result<Void> add(@RequestBody SysMenu menu) {
+        sysMenuService.addMenu(menu);
+        return Result.ok();
+    }
+
+    @Operation(summary = "修改菜单")
+    @PutMapping
+    public Result<Void> update(@RequestBody SysMenu menu) {
+        sysMenuService.updateMenu(menu);
+        return Result.ok();
+    }
+
+    @Operation(summary = "删除菜单")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        sysMenuService.removeMenu(id);
+        return Result.ok();
+    }
+}

+ 97 - 0
src/main/java/com/qqflow/engine/domain/system/controller/SysRoleController.java

@@ -0,0 +1,97 @@
+package com.qqflow.engine.domain.system.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.domain.system.assembler.RoleAssembler;
+import com.qqflow.engine.domain.system.dto.RoleDTO;
+import com.qqflow.engine.domain.system.entity.SysDept;
+import com.qqflow.engine.domain.system.entity.SysRole;
+import com.qqflow.engine.domain.system.service.SysDeptService;
+import com.qqflow.engine.domain.system.service.SysRoleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Tag(name = "角色管理")
+@RestController
+@RequestMapping("/system/role")
+public class SysRoleController {
+
+    @Resource
+    private SysRoleService sysRoleService;
+
+    @Resource
+    private SysDeptService sysDeptService;
+
+    @Operation(summary = "分页查询角色")
+    @GetMapping("/list")
+    public Result<PageResult<RoleDTO>> list(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) Long deptId,
+            @RequestParam(required = false) String roleCode,
+            @RequestParam(required = false) String roleName) {
+        Page<SysRole> page = sysRoleService.pageRoles(new Page<>(pageNum, pageSize), deptId, roleCode, roleName);
+        List<RoleDTO> dtoList = RoleAssembler.toDTOList(page.getRecords());
+
+        // 填充部门名称
+        List<SysDept> allDepts = sysDeptService.list();
+        Map<Long, String> deptNameMap = allDepts.stream()
+                .collect(Collectors.toMap(SysDept::getId, SysDept::getDeptName, (a, b) -> a));
+        for (RoleDTO dto : dtoList) {
+            if (dto.getDeptId() != null) {
+                dto.setDeptName(deptNameMap.getOrDefault(dto.getDeptId(), ""));
+            }
+        }
+
+        return Result.ok(PageResult.of(page.getTotal(), dtoList));
+    }
+
+    @Operation(summary = "根据ID查询角色")
+    @GetMapping("/{id}")
+    public Result<RoleDTO> getById(@PathVariable Long id) {
+        SysRole role = sysRoleService.getRoleById(id);
+        RoleDTO dto = RoleAssembler.toDTO(role);
+        if (dto != null && dto.getDeptId() != null) {
+            SysDept dept = sysDeptService.getById(dto.getDeptId());
+            if (dept != null) {
+                dto.setDeptName(dept.getDeptName());
+            }
+        }
+        return Result.ok(dto);
+    }
+
+    @Operation(summary = "查询用户角色列表")
+    @GetMapping("/user/{userId}")
+    public Result<List<RoleDTO>> listByUserId(@PathVariable Long userId) {
+        List<SysRole> roles = sysRoleService.listRolesByUserId(userId);
+        return Result.ok(RoleAssembler.toDTOList(roles));
+    }
+
+    @Operation(summary = "新增角色")
+    @PostMapping
+    public Result<Void> add(@RequestBody SysRole role) {
+        sysRoleService.addRole(role);
+        return Result.ok();
+    }
+
+    @Operation(summary = "修改角色")
+    @PutMapping
+    public Result<Void> update(@RequestBody SysRole role) {
+        sysRoleService.updateRole(role);
+        return Result.ok();
+    }
+
+    @Operation(summary = "删除角色")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        sysRoleService.removeRole(id);
+        return Result.ok();
+    }
+}

+ 69 - 0
src/main/java/com/qqflow/engine/domain/system/controller/SysUserController.java

@@ -0,0 +1,69 @@
+package com.qqflow.engine.domain.system.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.qqflow.engine.common.PageResult;
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.domain.system.assembler.UserAssembler;
+import com.qqflow.engine.domain.system.dto.UserDTO;
+import com.qqflow.engine.domain.system.entity.SysUser;
+import com.qqflow.engine.domain.system.service.SysUserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "用户管理")
+@RestController
+@RequestMapping("/system/user")
+public class SysUserController {
+
+    @Resource
+    private SysUserService sysUserService;
+
+    @Operation(summary = "分页查询用户")
+    @GetMapping("/list")
+    public Result<PageResult<UserDTO>> list(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) Long deptId,
+            @RequestParam(required = false) String username,
+            @RequestParam(required = false) Integer status) {
+        Page<UserDTO> page = sysUserService.pageUsers(new Page<>(pageNum, pageSize), deptId, username, status);
+        return Result.ok(PageResult.of(page.getTotal(), page.getRecords()));
+    }
+
+    @Operation(summary = "根据ID查询用户")
+    @GetMapping("/{id}")
+    public Result<UserDTO> getById(@PathVariable Long id) {
+        SysUser user = sysUserService.getUserById(id);
+        return Result.ok(UserAssembler.toDTO(user));
+    }
+
+    @Operation(summary = "根据用户名查询")
+    @GetMapping("/username/{username}")
+    public Result<UserDTO> getByUsername(@PathVariable String username) {
+        SysUser user = sysUserService.getByUsername(username);
+        return Result.ok(UserAssembler.toDTO(user));
+    }
+
+    @Operation(summary = "新增用户")
+    @PostMapping
+    public Result<Void> add(@RequestBody SysUser user) {
+        sysUserService.addUser(user);
+        return Result.ok();
+    }
+
+    @Operation(summary = "修改用户")
+    @PutMapping
+    public Result<Void> update(@RequestBody SysUser user) {
+        sysUserService.updateUser(user);
+        return Result.ok();
+    }
+
+    @Operation(summary = "删除用户")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        sysUserService.removeUser(id);
+        return Result.ok();
+    }
+}

+ 74 - 0
src/main/java/com/qqflow/engine/domain/system/dto/DeptDTO.java

@@ -0,0 +1,74 @@
+package com.qqflow.engine.domain.system.dto;
+
+import com.qqflow.engine.domain.system.entity.SysDept;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@Schema(description = "部门DTO")
+public class DeptDTO {
+
+    @Schema(description = "部门ID")
+    private Long id;
+
+    @JsonProperty("name")
+    @Schema(description = "部门名称")
+    private String deptName;
+
+    @Schema(description = "部门编码")
+    private String deptCode;
+
+    @Schema(description = "父部门ID")
+    private Long parentId;
+
+    @Schema(description = "负责人ID")
+    private Long leaderId;
+
+    @JsonProperty("leader")
+    @Schema(description = "负责人")
+    private String leaderName;
+
+    @JsonProperty("phone")
+    @Schema(description = "联系电话")
+    private String phone;
+
+    @JsonProperty("email")
+    @Schema(description = "邮箱")
+    private String email;
+
+    @JsonProperty("sort")
+    @Schema(description = "排序")
+    private Integer sortOrder;
+
+    @Schema(description = "状态:0-正常 1-禁用")
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "子部门")
+    private List<DeptDTO> children;
+
+    public static DeptDTO of(SysDept dept) {
+        if (dept == null) {
+            return null;
+        }
+        DeptDTO dto = new DeptDTO();
+        dto.setId(dept.getId());
+        dto.setDeptName(dept.getDeptName());
+        dto.setDeptCode(dept.getDeptCode());
+        dto.setParentId(dept.getParentId());
+        dto.setLeaderId(dept.getLeaderId());
+        dto.setSortOrder(dept.getSortOrder());
+        // 后端数据库 0=禁用, 1=正常; 前端约定 0=正常, 1=禁用
+        dto.setStatus(dept.getStatus() != null && dept.getStatus() == 1 ? 0 : 1);
+        dto.setCreateTime(dept.getCreateTime());
+        dto.setChildren(new ArrayList<>());
+        return dto;
+    }
+}

+ 18 - 0
src/main/java/com/qqflow/engine/domain/system/dto/LoginDTO.java

@@ -0,0 +1,18 @@
+package com.qqflow.engine.domain.system.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+@Schema(description = "登录DTO")
+public class LoginDTO {
+
+    @NotBlank(message = "用户名不能为空")
+    @Schema(description = "用户名")
+    private String username;
+
+    @NotBlank(message = "密码不能为空")
+    @Schema(description = "密码")
+    private String password;
+}

+ 74 - 0
src/main/java/com/qqflow/engine/domain/system/dto/MenuDTO.java

@@ -0,0 +1,74 @@
+package com.qqflow.engine.domain.system.dto;
+
+import com.qqflow.engine.domain.system.entity.SysMenu;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@Schema(description = "菜单DTO")
+public class MenuDTO {
+
+    @Schema(description = "菜单ID")
+    private Long id;
+
+    @JsonProperty("title")
+    @Schema(description = "菜单名称")
+    private String menuName;
+
+    @JsonProperty("name")
+    @Schema(description = "路由名称")
+    private String routeName;
+
+    @JsonProperty("path")
+    @Schema(description = "路由路径")
+    private String routePath;
+
+    @JsonProperty("type")
+    @Schema(description = "菜单类型:0-目录 1-菜单 2-按钮 3-接口")
+    private Integer menuType;
+
+    @Schema(description = "权限标识")
+    private String permission;
+
+    @Schema(description = "父菜单ID")
+    private Long parentId;
+
+    @JsonProperty("sort")
+    @Schema(description = "排序")
+    private Integer sortOrder;
+
+    @Schema(description = "组件路径")
+    private String component;
+
+    @Schema(description = "图标")
+    private String icon;
+
+    @Schema(description = "状态:0-正常 1-禁用")
+    private Integer status;
+
+    @Schema(description = "子菜单")
+    private List<MenuDTO> children;
+
+    public static MenuDTO of(SysMenu menu) {
+        if (menu == null) {
+            return null;
+        }
+        MenuDTO dto = new MenuDTO();
+        dto.setId(menu.getId());
+        dto.setMenuName(menu.getMenuName());
+        dto.setMenuType(menu.getMenuType());
+        dto.setPermission(menu.getPermission());
+        dto.setParentId(menu.getParentId());
+        dto.setSortOrder(menu.getSortOrder());
+        dto.setComponent(menu.getComponent());
+        dto.setIcon(menu.getIcon());
+        // 后端数据库 0=禁用, 1=正常; 前端约定 0=正常, 1=禁用
+        dto.setStatus(menu.getStatus() != null && menu.getStatus() == 1 ? 0 : 1);
+        dto.setChildren(new ArrayList<>());
+        return dto;
+    }
+}

+ 55 - 0
src/main/java/com/qqflow/engine/domain/system/dto/RoleDTO.java

@@ -0,0 +1,55 @@
+package com.qqflow.engine.domain.system.dto;
+
+import com.qqflow.engine.domain.system.entity.SysRole;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "角色DTO")
+public class RoleDTO {
+
+    @Schema(description = "角色ID")
+    private Long id;
+
+    @Schema(description = "角色编码")
+    private String roleCode;
+
+    @Schema(description = "角色名称")
+    private String roleName;
+
+    @Schema(description = "角色范围")
+    private String roleScope;
+
+    @Schema(description = "父角色ID")
+    private Long parentId;
+
+    @Schema(description = "所属部门ID")
+    private Long deptId;
+
+    @Schema(description = "所属部门名称")
+    private String deptName;
+
+    @Schema(description = "状态:0-禁用 1-正常")
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    public static RoleDTO of(SysRole role) {
+        if (role == null) {
+            return null;
+        }
+        RoleDTO dto = new RoleDTO();
+        dto.setId(role.getId());
+        dto.setRoleCode(role.getRoleCode());
+        dto.setRoleName(role.getRoleName());
+        dto.setRoleScope(role.getRoleScope());
+        dto.setParentId(role.getParentId());
+        dto.setDeptId(role.getDeptId());
+        dto.setStatus(role.getStatus());
+        dto.setCreateTime(role.getCreateTime());
+        return dto;
+    }
+}

+ 63 - 0
src/main/java/com/qqflow/engine/domain/system/dto/UserDTO.java

@@ -0,0 +1,63 @@
+package com.qqflow.engine.domain.system.dto;
+
+import com.qqflow.engine.domain.system.entity.SysUser;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Schema(description = "用户DTO")
+public class UserDTO {
+
+    @Schema(description = "用户ID")
+    private Long id;
+
+    @Schema(description = "用户名")
+    private String username;
+
+    @Schema(description = "真实姓名")
+    private String realName;
+
+    @Schema(description = "手机号")
+    private String phone;
+
+    @Schema(description = "邮箱")
+    private String email;
+
+    @Schema(description = "部门ID")
+    private Long deptId;
+
+    @Schema(description = "部门名称")
+    private String deptName;
+
+    @Schema(description = "员工类型")
+    private String employeeType;
+
+    @Schema(description = "状态:0-正常 1-禁用")
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "角色列表")
+    private List<String> roles;
+
+    public static UserDTO of(SysUser user) {
+        if (user == null) {
+            return null;
+        }
+        UserDTO dto = new UserDTO();
+        dto.setId(user.getId());
+        dto.setUsername(user.getUsername());
+        dto.setRealName(user.getRealName());
+        dto.setPhone(user.getPhone());
+        dto.setEmail(user.getEmail());
+        dto.setDeptId(user.getDeptId());
+        dto.setEmployeeType(user.getEmployeeType());
+        dto.setStatus(user.getStatus());
+        dto.setCreateTime(user.getCreateTime());
+        return dto;
+    }
+}

+ 43 - 0
src/main/java/com/qqflow/engine/domain/system/entity/SysDept.java

@@ -0,0 +1,43 @@
+package com.qqflow.engine.domain.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("sys_dept")
+@Schema(description = "系统部门")
+public class SysDept {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "部门ID")
+    private Long id;
+
+    @JsonProperty("name")
+    @Schema(description = "部门名称")
+    private String deptName;
+
+    @Schema(description = "部门编码")
+    private String deptCode;
+
+    @Schema(description = "父部门ID")
+    private Long parentId;
+
+    @Schema(description = "负责人ID")
+    private Long leaderId;
+
+    @JsonProperty("sort")
+    @Schema(description = "排序")
+    private Integer sortOrder;
+
+    @Schema(description = "状态:0-禁用 1-正常")
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+}

+ 41 - 0
src/main/java/com/qqflow/engine/domain/system/entity/SysMenu.java

@@ -0,0 +1,41 @@
+package com.qqflow.engine.domain.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@TableName("sys_menu")
+@Schema(description = "系统菜单")
+public class SysMenu {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "菜单ID")
+    private Long id;
+
+    @Schema(description = "菜单名称")
+    private String menuName;
+
+    @Schema(description = "菜单类型:0-目录 1-菜单 2-按钮 3-接口")
+    private Integer menuType;
+
+    @Schema(description = "权限标识")
+    private String permission;
+
+    @Schema(description = "父菜单ID")
+    private Long parentId;
+
+    @Schema(description = "排序")
+    private Integer sortOrder;
+
+    @Schema(description = "组件路径")
+    private String component;
+
+    @Schema(description = "图标")
+    private String icon;
+
+    @Schema(description = "状态:0-禁用 1-正常")
+    private Integer status;
+}

+ 40 - 0
src/main/java/com/qqflow/engine/domain/system/entity/SysRole.java

@@ -0,0 +1,40 @@
+package com.qqflow.engine.domain.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("sys_role")
+@Schema(description = "系统角色")
+public class SysRole {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "角色ID")
+    private Long id;
+
+    @Schema(description = "角色编码")
+    private String roleCode;
+
+    @Schema(description = "角色名称")
+    private String roleName;
+
+    @Schema(description = "角色范围")
+    private String roleScope;
+
+    @Schema(description = "父角色ID")
+    private Long parentId;
+
+    @Schema(description = "所属部门ID")
+    private Long deptId;
+
+    @Schema(description = "状态:0-禁用 1-正常")
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/system/entity/SysRoleMenu.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@TableName("sys_role_menu")
+@Schema(description = "角色菜单关联")
+public class SysRoleMenu {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "角色ID")
+    private Long roleId;
+
+    @Schema(description = "菜单ID")
+    private Long menuId;
+}

+ 55 - 0
src/main/java/com/qqflow/engine/domain/system/entity/SysUser.java

@@ -0,0 +1,55 @@
+package com.qqflow.engine.domain.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@TableName("sys_user")
+@Schema(description = "系统用户")
+public class SysUser {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "用户ID")
+    private Long id;
+
+    @Schema(description = "用户名")
+    private String username;
+
+    @Schema(description = "密码")
+    private String password;
+
+    @Schema(description = "真实姓名")
+    private String realName;
+
+    @Schema(description = "手机号")
+    private String phone;
+
+    @Schema(description = "邮箱")
+    private String email;
+
+    @Schema(description = "部门ID")
+    private Long deptId;
+
+    @Schema(description = "员工类型:super_admin-超级管理员, dept_manager-部门经理, flow_manager-流程管理员, common_user-普通用户")
+    private String employeeType;
+
+    @Schema(description = "状态:0-正常 1-禁用")
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间")
+    private LocalDateTime updateTime;
+
+    @TableField(exist = false)
+    @Schema(description = "角色ID列表")
+    private List<Long> roleIds;
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/system/entity/SysUserRole.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@TableName("sys_user_role")
+@Schema(description = "用户角色关联")
+public class SysUserRole {
+
+    @TableId(type = IdType.AUTO)
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "用户ID")
+    private Long userId;
+
+    @Schema(description = "角色ID")
+    private Long roleId;
+}

+ 17 - 0
src/main/java/com/qqflow/engine/domain/system/mapper/SysDeptMapper.java

@@ -0,0 +1,17 @@
+package com.qqflow.engine.domain.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.system.entity.SysDept;
+
+import java.util.List;
+
+public interface SysDeptMapper extends BaseMapper<SysDept> {
+
+    default List<SysDept> selectDeptList(Integer status) {
+        return this.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysDept>()
+                        .eq(status != null, SysDept::getStatus, status)
+                        .orderByAsc(SysDept::getSortOrder)
+        );
+    }
+}

+ 22 - 0
src/main/java/com/qqflow/engine/domain/system/mapper/SysMenuMapper.java

@@ -0,0 +1,22 @@
+package com.qqflow.engine.domain.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.system.entity.SysMenu;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface SysMenuMapper extends BaseMapper<SysMenu> {
+
+    default List<SysMenu> selectMenuList(Integer status) {
+        return this.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysMenu>()
+                        .eq(status != null, SysMenu::getStatus, status)
+                        .orderByAsc(SysMenu::getSortOrder)
+        );
+    }
+
+    List<SysMenu> selectMenusByUserId(@Param("userId") Long userId);
+
+    List<String> selectPermissionsByUserId(@Param("userId") Long userId);
+}

+ 19 - 0
src/main/java/com/qqflow/engine/domain/system/mapper/SysRoleMapper.java

@@ -0,0 +1,19 @@
+package com.qqflow.engine.domain.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.system.entity.SysRole;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface SysRoleMapper extends BaseMapper<SysRole> {
+
+    default SysRole selectByRoleCode(String roleCode) {
+        return this.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysRole>()
+                        .eq(SysRole::getRoleCode, roleCode)
+        );
+    }
+
+    List<SysRole> selectRolesByUserId(@Param("userId") Long userId);
+}

+ 7 - 0
src/main/java/com/qqflow/engine/domain/system/mapper/SysRoleMenuMapper.java

@@ -0,0 +1,7 @@
+package com.qqflow.engine.domain.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.system.entity.SysRoleMenu;
+
+public interface SysRoleMenuMapper extends BaseMapper<SysRoleMenu> {
+}

+ 21 - 0
src/main/java/com/qqflow/engine/domain/system/mapper/SysUserMapper.java

@@ -0,0 +1,21 @@
+package com.qqflow.engine.domain.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.system.entity.SysUser;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface SysUserMapper extends BaseMapper<SysUser> {
+
+    default SysUser selectByUsername(String username) {
+        return this.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUser>()
+                        .eq(SysUser::getUsername, username)
+        );
+    }
+
+    List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
+
+    List<String> selectPermissionsByUserId(@Param("userId") Long userId);
+}

+ 7 - 0
src/main/java/com/qqflow/engine/domain/system/mapper/SysUserRoleMapper.java

@@ -0,0 +1,7 @@
+package com.qqflow.engine.domain.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qqflow.engine.domain.system.entity.SysUserRole;
+
+public interface SysUserRoleMapper extends BaseMapper<SysUserRole> {
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/system/service/SysDeptService.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.system.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.qqflow.engine.domain.system.dto.DeptDTO;
+import com.qqflow.engine.domain.system.entity.SysDept;
+
+import java.util.List;
+
+public interface SysDeptService extends IService<SysDept> {
+
+    boolean addDept(SysDept dept);
+
+    boolean updateDept(SysDept dept);
+
+    boolean removeDept(Long id);
+
+    SysDept getDeptById(Long id);
+
+    Page<SysDept> pageDepts(Page<SysDept> page);
+
+    List<DeptDTO> buildDeptTree();
+}

+ 26 - 0
src/main/java/com/qqflow/engine/domain/system/service/SysMenuService.java

@@ -0,0 +1,26 @@
+package com.qqflow.engine.domain.system.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.qqflow.engine.domain.system.dto.MenuDTO;
+import com.qqflow.engine.domain.system.entity.SysMenu;
+
+import java.util.List;
+import java.util.Set;
+
+public interface SysMenuService extends IService<SysMenu> {
+
+    boolean addMenu(SysMenu menu);
+
+    boolean updateMenu(SysMenu menu);
+
+    boolean removeMenu(Long id);
+
+    SysMenu getMenuById(Long id);
+
+    Page<SysMenu> pageMenus(Page<SysMenu> page);
+
+    List<MenuDTO> listMenuTreeByUserId(Long userId);
+
+    Set<String> listPermissionsByUserId(Long userId);
+}

+ 22 - 0
src/main/java/com/qqflow/engine/domain/system/service/SysRoleService.java

@@ -0,0 +1,22 @@
+package com.qqflow.engine.domain.system.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.qqflow.engine.domain.system.entity.SysRole;
+
+import java.util.List;
+
+public interface SysRoleService extends IService<SysRole> {
+
+    boolean addRole(SysRole role);
+
+    boolean updateRole(SysRole role);
+
+    boolean removeRole(Long id);
+
+    SysRole getRoleById(Long id);
+
+    Page<SysRole> pageRoles(Page<SysRole> page, Long deptId, String roleCode, String roleName);
+
+    List<SysRole> listRolesByUserId(Long userId);
+}

+ 35 - 0
src/main/java/com/qqflow/engine/domain/system/service/SysUserService.java

@@ -0,0 +1,35 @@
+package com.qqflow.engine.domain.system.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.qqflow.engine.domain.system.dto.LoginDTO;
+import com.qqflow.engine.domain.system.dto.UserDTO;
+import com.qqflow.engine.domain.system.entity.SysUser;
+
+import java.util.List;
+import java.util.Set;
+
+public interface SysUserService extends IService<SysUser> {
+
+    boolean addUser(SysUser user);
+
+    boolean updateUser(SysUser user);
+
+    boolean removeUser(Long id);
+
+    SysUser getUserById(Long id);
+
+    Page<UserDTO> pageUsers(Page<UserDTO> page, Long deptId, String username, Integer status);
+
+    SysUser getByUsername(String username);
+
+    Set<String> loadUserPermissions(Long userId);
+
+    List<String> loadUserRoles(Long userId);
+
+    String login(LoginDTO loginDTO);
+
+    void logout(String token);
+
+    String refreshToken(String token);
+}

Някои файлове не бяха показани, защото твърде много файлове са промени