瀏覽代碼

feat: 待办列表支持流程字段展示及首页、我的流程、菜单优化

- 待办列表接口返回 formData/formSchema,支持动态流程字段列
- 我的流程支持批量撤回和批量删除
- 首页按权限动态展示统计卡片和快捷入口
- 我的待办支持按流程名称下拉筛选
- 列表调整:到达时间、剩余时间置于操作列前最后两列
- 隐藏侧边栏菜单中的流程明细
- 补充 bpm_process_definition 缺失字段到 schema-dev.sql
ye-zhaojia 1 周之前
父節點
當前提交
df68156849

+ 5 - 28
src/main/java/com/qqflow/engine/config/EmbeddedRedisConfig.java

@@ -1,6 +1,5 @@
 package com.qqflow.engine.config;
 
-import jakarta.annotation.PostConstruct;
 import jakarta.annotation.PreDestroy;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Configuration;
@@ -8,33 +7,19 @@ import org.springframework.context.annotation.Profile;
 import redis.embedded.RedisServer;
 
 import java.io.IOException;
-import java.net.Socket;
 
+/**
+ * Dev 环境下负责关闭 Embedded Redis。
+ * 启动逻辑已移至 {@link EmbeddedRedisInitializer},确保在 Redisson 等组件初始化前 Redis 已就绪。
+ */
 @Slf4j
 @Configuration
 @Profile("dev")
 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() {
+        RedisServer redisServer = EmbeddedRedisInitializer.getRedisServer();
         if (redisServer != null && redisServer.isActive()) {
             try {
                 log.info("正在停止 Embedded Redis...");
@@ -44,12 +29,4 @@ public class EmbeddedRedisConfig {
             }
         }
     }
-
-    private boolean isPortAvailable(int port) {
-        try (Socket socket = new Socket("localhost", port)) {
-            return false;
-        } catch (IOException e) {
-            return true;
-        }
-    }
 }

+ 51 - 0
src/main/java/com/qqflow/engine/config/EmbeddedRedisInitializer.java

@@ -0,0 +1,51 @@
+package com.qqflow.engine.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.env.Profiles;
+import redis.embedded.RedisServer;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * 在 Spring Boot 启动最早期启动 Embedded Redis,确保 Redisson 等组件初始化时 Redis 已可用。
+ * 仅在 dev 环境下生效。
+ */
+@Slf4j
+public class EmbeddedRedisInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+    private static RedisServer redisServer;
+
+    @Override
+    public void initialize(ConfigurableApplicationContext applicationContext) {
+        if (!applicationContext.getEnvironment().acceptsProfiles(Profiles.of("dev"))) {
+            return;
+        }
+        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 启动");
+        }
+    }
+
+    private boolean isPortAvailable(int port) {
+        try (Socket socket = new Socket("localhost", port)) {
+            return false;
+        } catch (IOException e) {
+            return true;
+        }
+    }
+
+    public static RedisServer getRedisServer() {
+        return redisServer;
+    }
+}

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

@@ -76,6 +76,13 @@ public class ProcessInstanceController {
         return Result.ok();
     }
 
+    @PostMapping("/batch-revoke")
+    @Operation(summary = "批量撤回流程")
+    public Result<Void> batchRevoke(@RequestBody List<Long> ids) {
+        this.processInstanceService.batchRevoke(ids);
+        return Result.ok();
+    }
+
     @DeleteMapping("/{id}")
     @Operation(summary = "删除流程实例")
     public Result<Void> delete(@PathVariable Long id) {
@@ -83,6 +90,13 @@ public class ProcessInstanceController {
         return Result.ok();
     }
 
+    @PostMapping("/batch-delete")
+    @Operation(summary = "批量删除流程实例")
+    public Result<Void> batchDelete(@RequestBody List<Long> ids) {
+        this.processInstanceService.batchDelete(ids);
+        return Result.ok();
+    }
+
     @GetMapping("/{id}/progress")
     @Operation(summary = "流程实例进度")
     public Result<ProcessProgressDTO> progress(@PathVariable Long id) {

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

@@ -82,6 +82,12 @@ public class ApprovalTaskDTO {
     @Schema(description = "实例编号")
     private String instanceNo;
 
+    @Schema(description = "表单数据JSON")
+    private String formData;
+
+    @Schema(description = "表单字段定义JSON")
+    private String formSchema;
+
     public static ApprovalTaskDTO of(ApprovalTask po) {
         if (po == null) {
             return null;
@@ -106,6 +112,8 @@ public class ApprovalTaskDTO {
         dto.setDefinitionName(po.getProcessName());
         dto.setInstanceTitle(po.getInstanceTitle());
         dto.setInstanceNo(po.getInstanceNo());
+        dto.setFormData(po.getFormData());
+        dto.setFormSchema(po.getFormSchema());
         // 别名映射
         dto.setAction(po.getApprovalResult());
         dto.setComment(po.getApprovalComment());

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

@@ -97,4 +97,12 @@ public class ApprovalTask {
     @TableField(exist = false)
     @Schema(description = "实例编号")
     private String instanceNo;
+
+    @TableField(exist = false)
+    @Schema(description = "表单数据JSON")
+    private String formData;
+
+    @TableField(exist = false)
+    @Schema(description = "表单字段定义JSON")
+    private String formSchema;
 }

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

@@ -21,8 +21,12 @@ public interface ProcessInstanceService {
 
     void revoke(Long id);
 
+    void batchRevoke(List<Long> ids);
+
     void delete(Long id);
 
+    void batchDelete(List<Long> ids);
+
     ProcessProgressDTO getProgress(Long id);
 
     PageResult<ProcessInstanceDTO> participatedList(Long userId, String userType, Integer pageNum, Integer pageSize);

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

@@ -132,6 +132,17 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
 
     @Override
     @Transactional
+    public void batchDelete(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new BusinessException("流程实例ID不能为空");
+        }
+        for (Long id : ids) {
+            this.delete(id);
+        }
+    }
+
+    @Override
+    @Transactional
     public void delete(Long id) {
         Long operatorId = SecurityUtils.getUserId();
         ProcessInstance instance = this.processInstanceMapper.selectById(id);
@@ -149,6 +160,17 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
 
     @Override
     @Transactional
+    public void batchRevoke(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new BusinessException("流程实例ID不能为空");
+        }
+        for (Long id : ids) {
+            this.revoke(id);
+        }
+    }
+
+    @Override
+    @Transactional
     public void revoke(Long id) {
         ProcessInstance instance = this.processInstanceMapper.selectById(id);
         this.validateRevocable(instance);

+ 1 - 0
src/main/resources/META-INF/spring.factories

@@ -0,0 +1 @@
+org.springframework.context.ApplicationContextInitializer=com.qqflow.engine.config.EmbeddedRedisInitializer

+ 7 - 0
src/main/resources/application-dev.yml

@@ -43,6 +43,13 @@ jwt:
   secret: ${JWT_SECRET:qqflow-dev-secret-key-2024-very-long-and-secure}
   expiration: 86400000
 
+enterprise-wechat:
+  sCorp-id: ww76b6c51c3935e90d
+  token: jGgY9aLKA
+  encodingAESKey: IycRI8M3tgTIHrFiwWnYWJn9I1o8iIsQrhNy8M4MWnT
+  suite-id: dk0580416972e402b8
+  suite-secret: 3aV48BTZHxDBeltZgdiRAUhttqsE5RxNs5pzElGhHHg
+
 logging:
   level:
     root: info

+ 4 - 1
src/main/resources/mapper/flow/ApprovalTaskMapper.xml

@@ -23,10 +23,13 @@
         <result column="process_name" property="processName"/>
         <result column="instance_title" property="instanceTitle"/>
         <result column="instance_no" property="instanceNo"/>
+        <result column="form_data" property="formData"/>
+        <result column="form_schema" property="formSchema"/>
     </resultMap>
 
     <select id="selectTodoList" resultMap="BaseResultMap">
-        SELECT t.*, pi.title as instance_title, pi.instance_no as instance_no, pd.process_name as process_name
+        SELECT t.*, pi.title as instance_title, pi.instance_no as instance_no, pi.form_data as form_data,
+               pd.process_name as process_name, pd.form_schema as form_schema
         FROM bpm_approval_task t
         INNER JOIN bpm_process_instance pi ON t.instance_id = pi.id
         LEFT JOIN bpm_process_definition pd ON pi.process_definition_id = pd.id

+ 13 - 10
src/main/resources/schema-dev.sql

@@ -54,6 +54,9 @@ CREATE TABLE IF NOT EXISTS bpm_process_definition (
     category VARCHAR(50),
     form_id BIGINT,
     form_schema TEXT COMMENT '流程表单字段定义 JSON',
+    node_field_name VARCHAR(100) COMMENT '节点名称字段:导入时按此字段匹配流程节点',
+    owner_field_name VARCHAR(100) COMMENT '归属人字段:导入时按此字段匹配 sys_user 作为发起人',
+    biz_field_name VARCHAR(100) COMMENT '业务编号字段:列表中显示的业务标识对应的表单字段名',
     model_json TEXT NOT NULL,
     version INT DEFAULT 1,
     status TINYINT DEFAULT 0 COMMENT '0-设计中 1-启用中 2-历史',
@@ -113,9 +116,9 @@ CREATE TABLE IF NOT EXISTS bpm_approval_task (
     KEY idx_task_node (node_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
-CREATE INDEX idx_instance_status_end_definition ON bpm_process_instance(status, end_time, process_definition_id);
-CREATE INDEX idx_task_status_node_create ON bpm_approval_task(task_status, node_type, create_time);
-CREATE INDEX idx_task_status_timeout ON bpm_approval_task(task_status, timeout_time);
+CREATE INDEX IF NOT EXISTS idx_instance_status_end_definition ON bpm_process_instance(status, end_time, process_definition_id);
+CREATE INDEX IF NOT EXISTS idx_task_status_node_create ON bpm_approval_task(task_status, node_type, create_time);
+CREATE INDEX IF NOT EXISTS idx_task_status_timeout ON bpm_approval_task(task_status, timeout_time);
 
 CREATE TABLE IF NOT EXISTS bpm_attachment (
     id BIGINT PRIMARY KEY AUTO_INCREMENT,
@@ -157,17 +160,17 @@ CREATE TABLE IF NOT EXISTS bpm_approval_record (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- 兼容已存在的数据库:补充企业微信字段
-ALTER TABLE sys_user ADD COLUMN wecom_user_id VARCHAR(100) COMMENT '企业微信用户ID';
-ALTER TABLE sys_user ADD COLUMN wecom_remind_enabled TINYINT DEFAULT 1 COMMENT '是否开启企微提醒:0-否 1-是';
+ALTER TABLE sys_user ADD COLUMN IF NOT EXISTS wecom_user_id VARCHAR(100) COMMENT '企业微信用户ID';
+ALTER TABLE sys_user ADD COLUMN IF NOT EXISTS wecom_remind_enabled TINYINT DEFAULT 1 COMMENT '是否开启企微提醒:0-否 1-是';
 
 -- 兼容已存在的数据库:补充角色企业微信字段
-ALTER TABLE sys_role ADD COLUMN wecom_user_id VARCHAR(100) COMMENT '企业微信用户ID';
-ALTER TABLE sys_role ADD COLUMN wecom_remind_enabled TINYINT DEFAULT 1 COMMENT '是否开启企微提醒:0-否 1-是';
+ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS wecom_user_id VARCHAR(100) COMMENT '企业微信用户ID';
+ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS wecom_remind_enabled TINYINT DEFAULT 1 COMMENT '是否开启企微提醒:0-否 1-是';
 
 -- 兼容已存在的数据库:补充角色手机号字段及唯一索引
-ALTER TABLE sys_role ADD COLUMN phone VARCHAR(20) COMMENT '手机号';
-CREATE UNIQUE INDEX uk_role_phone ON sys_role(phone);
-CREATE UNIQUE INDEX uk_user_phone ON sys_user(phone);
+ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS phone VARCHAR(20) COMMENT '手机号';
+CREATE UNIQUE INDEX IF NOT EXISTS uk_role_phone ON sys_role(phone);
+CREATE UNIQUE INDEX IF NOT EXISTS uk_user_phone ON sys_user(phone);
 
 -- 通知配置表(预留:供企业微信/邮件/短信等渠道配置读取)
 CREATE TABLE IF NOT EXISTS sys_notification_config (

+ 2 - 2
src/main/resources/schema-mysql.sql

@@ -191,8 +191,8 @@ CREATE TABLE IF NOT EXISTS bpm_attachment (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程附件表';
 
 -- 兼容已存在的 MySQL 数据库:补充角色企业微信字段
-ALTER TABLE sys_role ADD COLUMN wecom_user_id VARCHAR(100) COMMENT '企业微信用户ID';
-ALTER TABLE sys_role ADD COLUMN wecom_remind_enabled TINYINT DEFAULT 1 COMMENT '是否开启企微提醒:0-否 1-是';
+ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS wecom_user_id VARCHAR(100) COMMENT '企业微信用户ID';
+ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS wecom_remind_enabled TINYINT DEFAULT 1 COMMENT '是否开启企微提醒:0-否 1-是';
 
 -- ----------------------------
 -- 9. 通知配置表