From 4a1d02f040a03f01920a33cc24d705e9677c4138 Mon Sep 17 00:00:00 2001
From: weicw <594098497@qq.com>
Date: Fri, 30 Oct 2026 20:18:47 +0800
Subject: [PATCH 1/3] =?UTF-8?q?feat(approval):=20=E4=B8=9A=E5=8A=A1?=
=?UTF-8?q?=E5=AE=8C=E6=88=90=E7=A1=AE=E8=AE=A4=E5=8D=95=E8=8D=89=E7=A8=BF?=
=?UTF-8?q?=E3=80=81=E9=99=84=E4=BB=B6=E3=80=81=E6=8F=90=E4=BA=A4=E4=B8=8E?=
=?UTF-8?q?=E5=BC=95=E6=93=8E=E8=B5=B7=E6=AD=A5=20(#19)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 5 +
sql/approval.sql | 140 +++++
sql/approval_menu.sql | 77 +++
.../approval/constant/ApprovalConstants.java | 169 ++++++
.../ApprovalAttachmentController.java | 47 ++
.../ApprovalDirectoryController.java | 45 ++
.../ApprovalInstanceController.java | 65 +++
.../approval/entity/ApprovalAttachment.java | 41 ++
.../entity/ApprovalCompletionForm.java | 40 ++
.../entity/ApprovalCoordinationForm.java | 41 ++
.../entity/ApprovalFlowDefinition.java | 41 ++
.../approval/entity/ApprovalInstance.java | 80 +++
.../approval/entity/ApprovalNoSequence.java | 31 +
.../approval/entity/ApprovalRecord.java | 43 ++
.../approval/entity/ApprovalTask.java | 53 ++
.../mapper/ApprovalAttachmentMapper.java | 10 +
.../mapper/ApprovalCompletionFormMapper.java | 10 +
.../ApprovalCoordinationFormMapper.java | 10 +
.../mapper/ApprovalFlowDefinitionMapper.java | 10 +
.../mapper/ApprovalInstanceMapper.java | 35 ++
.../mapper/ApprovalNoSequenceMapper.java | 33 ++
.../approval/mapper/ApprovalRecordMapper.java | 10 +
.../approval/mapper/ApprovalTaskMapper.java | 10 +
.../mapper/xml/ApprovalInstanceMapper.xml | 59 ++
.../param/ApprovalCompletionParam.java | 30 +
.../approval/param/ApprovalMineParam.java | 20 +
.../service/ApprovalAttachmentService.java | 243 ++++++++
.../service/ApprovalDirectoryService.java | 177 ++++++
.../service/ApprovalEngineService.java | 159 ++++++
.../approval/service/ApprovalFileService.java | 94 +++
.../approval/service/ApprovalFlowService.java | 121 ++++
.../service/ApprovalInstanceService.java | 540 ++++++++++++++++++
.../approval/service/ApprovalNoService.java | 37 ++
.../approval/support/ApprovalApprover.java | 16 +
.../approval/support/ApprovalItemRow.java | 28 +
.../approval/support/ApprovalNode.java | 24 +
.../support/ApprovalTenantContext.java | 48 ++
.../approval/vo/ApprovalAttachmentVO.java | 36 ++
.../approval/vo/ApprovalConfirmVO.java | 26 +
.../approval/vo/ApprovalInstanceVO.java | 92 +++
.../gxwebsoft/approval/vo/ApprovalItemVO.java | 46 ++
.../approval/vo/ApprovalMyTaskVO.java | 23 +
.../approval/vo/ApprovalOrganizationVO.java | 27 +
.../approval/vo/ApprovalPermissionsVO.java | 33 ++
.../approval/vo/ApprovalRecordVO.java | 38 ++
.../gxwebsoft/approval/vo/ApprovalUserVO.java | 27 +
.../approval/ApprovalCompletionDraftIT.java | 519 +++++++++++++++++
.../gxwebsoft/support/ApprovalTestData.java | 203 +++++++
.../gxwebsoft/support/TestAuthentication.java | 23 +-
49 files changed, 3732 insertions(+), 3 deletions(-)
create mode 100644 sql/approval.sql
create mode 100644 sql/approval_menu.sql
create mode 100644 src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java
create mode 100644 src/main/java/com/gxwebsoft/approval/controller/ApprovalAttachmentController.java
create mode 100644 src/main/java/com/gxwebsoft/approval/controller/ApprovalDirectoryController.java
create mode 100644 src/main/java/com/gxwebsoft/approval/controller/ApprovalInstanceController.java
create mode 100644 src/main/java/com/gxwebsoft/approval/entity/ApprovalAttachment.java
create mode 100644 src/main/java/com/gxwebsoft/approval/entity/ApprovalCompletionForm.java
create mode 100644 src/main/java/com/gxwebsoft/approval/entity/ApprovalCoordinationForm.java
create mode 100644 src/main/java/com/gxwebsoft/approval/entity/ApprovalFlowDefinition.java
create mode 100644 src/main/java/com/gxwebsoft/approval/entity/ApprovalInstance.java
create mode 100644 src/main/java/com/gxwebsoft/approval/entity/ApprovalNoSequence.java
create mode 100644 src/main/java/com/gxwebsoft/approval/entity/ApprovalRecord.java
create mode 100644 src/main/java/com/gxwebsoft/approval/entity/ApprovalTask.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/ApprovalAttachmentMapper.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/ApprovalCompletionFormMapper.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/ApprovalCoordinationFormMapper.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/ApprovalFlowDefinitionMapper.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/ApprovalInstanceMapper.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/ApprovalNoSequenceMapper.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/ApprovalRecordMapper.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/ApprovalTaskMapper.java
create mode 100644 src/main/java/com/gxwebsoft/approval/mapper/xml/ApprovalInstanceMapper.xml
create mode 100644 src/main/java/com/gxwebsoft/approval/param/ApprovalCompletionParam.java
create mode 100644 src/main/java/com/gxwebsoft/approval/param/ApprovalMineParam.java
create mode 100644 src/main/java/com/gxwebsoft/approval/service/ApprovalAttachmentService.java
create mode 100644 src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java
create mode 100644 src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java
create mode 100644 src/main/java/com/gxwebsoft/approval/service/ApprovalFileService.java
create mode 100644 src/main/java/com/gxwebsoft/approval/service/ApprovalFlowService.java
create mode 100644 src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java
create mode 100644 src/main/java/com/gxwebsoft/approval/service/ApprovalNoService.java
create mode 100644 src/main/java/com/gxwebsoft/approval/support/ApprovalApprover.java
create mode 100644 src/main/java/com/gxwebsoft/approval/support/ApprovalItemRow.java
create mode 100644 src/main/java/com/gxwebsoft/approval/support/ApprovalNode.java
create mode 100644 src/main/java/com/gxwebsoft/approval/support/ApprovalTenantContext.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalAttachmentVO.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalConfirmVO.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalInstanceVO.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalItemVO.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalMyTaskVO.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalOrganizationVO.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalPermissionsVO.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalRecordVO.java
create mode 100644 src/main/java/com/gxwebsoft/approval/vo/ApprovalUserVO.java
create mode 100644 src/test/java/com/gxwebsoft/approval/ApprovalCompletionDraftIT.java
create mode 100644 src/test/java/com/gxwebsoft/support/ApprovalTestData.java
diff --git a/pom.xml b/pom.xml
index bc1296e..30b2cca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,6 +39,11 @@
spring-boot-starter-test
test
+
+ org.springframework.security
+ spring-security-test
+ test
+
diff --git a/sql/approval.sql b/sql/approval.sql
new file mode 100644
index 0000000..c87f202
--- /dev/null
+++ b/sql/approval.sql
@@ -0,0 +1,140 @@
+-- 一期线下审批公共表与业务表(可重复执行)
+-- 目标库:db_gxtyzx(master)
+
+CREATE TABLE IF NOT EXISTS approval_flow_definition (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_id INT NOT NULL DEFAULT 10150,
+ flow_code VARCHAR(40) NOT NULL,
+ version INT NOT NULL,
+ name VARCHAR(100) NOT NULL,
+ nodes_json JSON NOT NULL,
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_flow_ver (tenant_id, flow_code, version)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批流程定义版本';
+
+CREATE TABLE IF NOT EXISTS approval_instance (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_id INT NOT NULL DEFAULT 10150,
+ biz_no VARCHAR(32) DEFAULT NULL,
+ flow_code VARCHAR(40) NOT NULL,
+ definition_id BIGINT NOT NULL,
+ definition_version INT NOT NULL,
+ doc_type VARCHAR(40) NOT NULL,
+ biz_id BIGINT NOT NULL,
+ status VARCHAR(20) NOT NULL,
+ round INT NOT NULL DEFAULT 0,
+ reporter_id INT NOT NULL,
+ reporter_name VARCHAR(100) DEFAULT NULL,
+ report_org_id INT DEFAULT NULL,
+ report_org_name VARCHAR(200) DEFAULT NULL,
+ accept_org_id INT DEFAULT NULL,
+ accept_org_name VARCHAR(200) DEFAULT NULL,
+ current_node_key VARCHAR(40) DEFAULT NULL,
+ current_node_type VARCHAR(20) DEFAULT NULL,
+ dispatch_target_id INT DEFAULT NULL,
+ dispatch_target_name VARCHAR(100) DEFAULT NULL,
+ submit_time DATETIME DEFAULT NULL,
+ approve_time DATETIME DEFAULT NULL,
+ archive_time DATETIME DEFAULT NULL,
+ archivist_id INT DEFAULT NULL,
+ archivist_name VARCHAR(100) DEFAULT NULL,
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_biz_no (tenant_id, biz_no),
+ KEY idx_status_submit (tenant_id, status, submit_time),
+ KEY idx_reporter (tenant_id, reporter_id),
+ KEY idx_report_org (tenant_id, report_org_id),
+ KEY idx_accept_org (tenant_id, accept_org_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批实例';
+
+CREATE TABLE IF NOT EXISTS approval_task (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_id INT NOT NULL DEFAULT 10150,
+ instance_id BIGINT NOT NULL,
+ node_key VARCHAR(40) NOT NULL,
+ node_type VARCHAR(20) NOT NULL,
+ round INT NOT NULL,
+ task_type VARCHAR(20) NOT NULL,
+ assignee_id INT NOT NULL,
+ assignee_name VARCHAR(100) DEFAULT NULL,
+ status VARCHAR(20) NOT NULL,
+ handle_time DATETIME DEFAULT NULL,
+ opinion VARCHAR(500) DEFAULT NULL,
+ dispatch_target_id INT DEFAULT NULL,
+ dispatch_target_name VARCHAR(100) DEFAULT NULL,
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY idx_instance (instance_id),
+ KEY idx_assignee_status (tenant_id, assignee_id, status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批任务';
+
+CREATE TABLE IF NOT EXISTS approval_record (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_id INT NOT NULL DEFAULT 10150,
+ instance_id BIGINT NOT NULL,
+ task_id BIGINT DEFAULT NULL,
+ node_key VARCHAR(40) DEFAULT NULL,
+ operator_id INT NOT NULL,
+ operator_name VARCHAR(100) DEFAULT NULL,
+ action VARCHAR(20) NOT NULL,
+ opinion VARCHAR(500) DEFAULT NULL,
+ round INT NOT NULL,
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY idx_instance_time (instance_id, create_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批处理记录';
+
+CREATE TABLE IF NOT EXISTS approval_attachment (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_id INT NOT NULL DEFAULT 10150,
+ instance_id BIGINT NOT NULL,
+ file_id INT NOT NULL,
+ category VARCHAR(20) NOT NULL,
+ uploader_id INT NOT NULL,
+ deleted TINYINT NOT NULL DEFAULT 0,
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ KEY idx_instance (instance_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批附件';
+
+CREATE TABLE IF NOT EXISTS approval_no_sequence (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_id INT NOT NULL DEFAULT 10150,
+ prefix VARCHAR(8) NOT NULL,
+ seq_date DATE NOT NULL,
+ current_seq INT NOT NULL DEFAULT 0,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_seq (tenant_id, prefix, seq_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批单号序列';
+
+CREATE TABLE IF NOT EXISTS approval_completion_form (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_id INT NOT NULL DEFAULT 10150,
+ instance_id BIGINT NOT NULL,
+ name VARCHAR(100) NOT NULL,
+ implement_org VARCHAR(100) NOT NULL,
+ fee DECIMAL(12,2) NOT NULL,
+ service_content VARCHAR(2000) NOT NULL,
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_instance (instance_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务完成确认单';
+
+CREATE TABLE IF NOT EXISTS approval_coordination_form (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ tenant_id INT NOT NULL DEFAULT 10150,
+ instance_id BIGINT NOT NULL,
+ accept_org_id INT NOT NULL,
+ accept_org_name VARCHAR(200) NOT NULL,
+ title VARCHAR(100) NOT NULL,
+ content VARCHAR(2000) NOT NULL,
+ remark VARCHAR(500) DEFAULT NULL,
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_instance (instance_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='协调单';
diff --git a/sql/approval_menu.sql b/sql/approval_menu.sql
new file mode 100644
index 0000000..b92066c
--- /dev/null
+++ b/sql/approval_menu.sql
@@ -0,0 +1,77 @@
+-- 一期线下审批菜单与权限种子(可重复执行)
+-- 目标库:gxtyzx_core(server)
+
+-- 顶级菜单:审批管理
+INSERT INTO sys_menu (parent_id, title, path, component, sort_number, authority, icon, menu_type, tenant_id, deleted)
+SELECT 0, '审批管理', '/approval', '', 18, 'approval:instance:list', 'AuditOutlined', 0, 10150, 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND path = '/approval' AND menu_type = 0 AND deleted = 0
+);
+
+SET @parent = (SELECT menu_id FROM sys_menu WHERE tenant_id = 10150 AND path = '/approval' AND menu_type = 0 AND deleted = 0 ORDER BY menu_id LIMIT 1);
+
+-- 子菜单:待办审批
+INSERT INTO sys_menu (parent_id, title, path, component, sort_number, authority, icon, menu_type, tenant_id, deleted)
+SELECT @parent, '待办审批', '/approval/todo', '/approval/todo', 1, 'approval:instance:list', '', 0, 10150, 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND path = '/approval/todo' AND menu_type = 0 AND deleted = 0
+);
+
+-- 子菜单:单据查询
+INSERT INTO sys_menu (parent_id, title, path, component, sort_number, authority, icon, menu_type, tenant_id, deleted)
+SELECT @parent, '单据查询', '/approval/instance', '/approval/instance', 2, 'approval:instance:list', '', 0, 10150, 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND path = '/approval/instance' AND menu_type = 0 AND deleted = 0
+);
+
+SET @instanceMenu = (SELECT menu_id FROM sys_menu WHERE tenant_id = 10150 AND path = '/approval/instance' AND menu_type = 0 AND deleted = 0 ORDER BY menu_id LIMIT 1);
+
+-- 按钮权限(挂在单据查询下)
+INSERT INTO sys_menu (parent_id, title, path, component, sort_number, authority, icon, menu_type, tenant_id, deleted)
+SELECT @instanceMenu, '提交单据', '', '', 1, 'approval:instance:submit', '', 1, 10150, 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND authority = 'approval:instance:submit' AND menu_type = 1 AND deleted = 0
+);
+
+INSERT INTO sys_menu (parent_id, title, path, component, sort_number, authority, icon, menu_type, tenant_id, deleted)
+SELECT @instanceMenu, '审批通过', '', '', 2, 'approval:instance:approve', '', 1, 10150, 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND authority = 'approval:instance:approve' AND menu_type = 1 AND deleted = 0
+);
+
+INSERT INTO sys_menu (parent_id, title, path, component, sort_number, authority, icon, menu_type, tenant_id, deleted)
+SELECT @instanceMenu, '归档', '', '', 3, 'approval:instance:archive', '', 1, 10150, 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND authority = 'approval:instance:archive' AND menu_type = 1 AND deleted = 0
+);
+
+INSERT INTO sys_menu (parent_id, title, path, component, sort_number, authority, icon, menu_type, tenant_id, deleted)
+SELECT @instanceMenu, '改派', '', '', 4, 'approval:instance:assign', '', 1, 10150, 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND authority = 'approval:instance:assign' AND menu_type = 1 AND deleted = 0
+);
+
+INSERT INTO sys_menu (parent_id, title, path, component, sort_number, authority, icon, menu_type, tenant_id, deleted)
+SELECT @instanceMenu, '导出', '', '', 5, 'approval:instance:export', '', 1, 10150, 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND authority = 'approval:instance:export' AND menu_type = 1 AND deleted = 0
+);
+
+-- 授权 superAdmin(role_id = 586, tenant 10150)
+INSERT INTO sys_role_menu (role_id, menu_id, tenant_id)
+SELECT 586, m.menu_id, 10150
+FROM sys_menu m
+WHERE m.tenant_id = 10150
+ AND m.deleted = 0
+ AND (m.path IN ('/approval', '/approval/todo', '/approval/instance') OR m.authority LIKE 'approval:instance:%')
+ AND NOT EXISTS (
+ SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 586 AND rm.menu_id = m.menu_id AND rm.tenant_id = 10150
+ );
diff --git a/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java b/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java
new file mode 100644
index 0000000..3ea09af
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java
@@ -0,0 +1,169 @@
+package com.gxwebsoft.approval.constant;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 审批域常量:状态、节点、动作、单据类型、单号前缀与附件规则。
+ */
+public final class ApprovalConstants {
+
+ private ApprovalConstants() {
+ }
+
+ // 实例状态
+ public static final String STATUS_DRAFT = "DRAFT";
+ public static final String STATUS_PENDING = "PENDING";
+ public static final String STATUS_REJECTED = "REJECTED";
+ public static final String STATUS_WITHDRAWN = "WITHDRAWN";
+ public static final String STATUS_APPROVED = "APPROVED";
+ public static final String STATUS_ARCHIVED = "ARCHIVED";
+
+ /** 发起人可编辑、可提交的状态。 */
+ public static final Set EDITABLE_STATUSES =
+ Collections.unmodifiableSet(new java.util.HashSet<>(Arrays.asList(
+ STATUS_DRAFT, STATUS_REJECTED, STATUS_WITHDRAWN)));
+
+ // 任务状态
+ public static final String TASK_PENDING = "PENDING";
+ public static final String TASK_DONE = "DONE";
+ public static final String TASK_CANCELLED = "CANCELLED";
+
+ // 节点类型 / 任务类型
+ public static final String NODE_APPROVE = "APPROVE";
+ public static final String NODE_COUNTERSIGN = "COUNTERSIGN";
+ public static final String NODE_DISPATCH = "DISPATCH";
+ public static final String NODE_CONFIRM = "CONFIRM";
+ public static final String NODE_ARCHIVE = "ARCHIVE";
+
+ // 动作
+ public static final String ACTION_SUBMIT = "SUBMIT";
+ public static final String ACTION_RESUBMIT = "RESUBMIT";
+ public static final String ACTION_APPROVE = "APPROVE";
+ public static final String ACTION_REJECT = "REJECT";
+ public static final String ACTION_WITHDRAW = "WITHDRAW";
+ public static final String ACTION_DISPATCH = "DISPATCH";
+ public static final String ACTION_CONFIRM = "CONFIRM";
+ public static final String ACTION_ARCHIVE = "ARCHIVE";
+ public static final String ACTION_ASSIGN = "ASSIGN";
+
+ // 单据类型 / 流程编码
+ public static final String DOC_COMPLETION = "completion";
+ public static final String DOC_COORDINATION = "coordination";
+ public static final String FLOW_COMPLETION = "completion";
+ public static final String FLOW_COORDINATION_INTERNAL = "coordination_internal";
+
+ // 单号前缀
+ public static final String NO_PREFIX_COMPLETION = "WG";
+ public static final String NO_PREFIX_COORDINATION = "XT";
+
+ // 附件分类
+ public static final String CATEGORY_SIGNED = "SIGNED";
+ public static final String CATEGORY_OTHER = "OTHER";
+
+ // 角色编码
+ public static final String ROLE_ARCHIVIST = "approval_archivist";
+ public static final String ROLE_SUPER_ADMIN = "superAdmin";
+
+ // 附件规则
+ public static final long MAX_ATTACHMENT_SIZE = 20L * 1024 * 1024;
+ public static final int MAX_ATTACHMENT_COUNT = 9;
+ public static final List ATTACHMENT_EXTENSIONS =
+ Collections.unmodifiableList(Arrays.asList("jpg", "jpeg", "png", "pdf", "doc", "docx", "xls", "xlsx"));
+
+ /** 扩展名与允许的 content-type 对应关系(服务端双校验)。 */
+ public static final Map> ATTACHMENT_CONTENT_TYPES;
+
+ static {
+ Map> types = new LinkedHashMap<>();
+ types.put("jpg", Arrays.asList("image/jpeg", "image/jpg"));
+ types.put("jpeg", Arrays.asList("image/jpeg", "image/jpg"));
+ types.put("png", Collections.singletonList("image/png"));
+ types.put("pdf", Collections.singletonList("application/pdf"));
+ types.put("doc", Collections.singletonList("application/msword"));
+ types.put("docx", Collections.singletonList(
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
+ types.put("xls", Collections.singletonList("application/vnd.ms-excel"));
+ types.put("xlsx", Collections.singletonList(
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
+ ATTACHMENT_CONTENT_TYPES = Collections.unmodifiableMap(types);
+ }
+
+ private static final Map STATUS_NAMES;
+ private static final Map DOC_TYPE_NAMES;
+ private static final Map ACTION_NAMES;
+ private static final Map CATEGORY_NAMES;
+
+ static {
+ Map status = new LinkedHashMap<>();
+ status.put(STATUS_DRAFT, "未提交");
+ status.put(STATUS_PENDING, "审批中");
+ status.put(STATUS_REJECTED, "已驳回");
+ status.put(STATUS_WITHDRAWN, "已撤回");
+ status.put(STATUS_APPROVED, "审批完成");
+ status.put(STATUS_ARCHIVED, "已归档");
+ STATUS_NAMES = Collections.unmodifiableMap(status);
+
+ Map docType = new LinkedHashMap<>();
+ docType.put(DOC_COMPLETION, "业务完成确认单");
+ docType.put(DOC_COORDINATION, "内部协调单");
+ DOC_TYPE_NAMES = Collections.unmodifiableMap(docType);
+
+ Map action = new LinkedHashMap<>();
+ action.put(ACTION_SUBMIT, "提交");
+ action.put(ACTION_RESUBMIT, "重新提交");
+ action.put(ACTION_APPROVE, "通过");
+ action.put(ACTION_REJECT, "驳回");
+ action.put(ACTION_WITHDRAW, "撤回");
+ action.put(ACTION_DISPATCH, "派单");
+ action.put(ACTION_CONFIRM, "存档确认");
+ action.put(ACTION_ARCHIVE, "归档");
+ action.put(ACTION_ASSIGN, "改派");
+ ACTION_NAMES = Collections.unmodifiableMap(action);
+
+ Map category = new LinkedHashMap<>();
+ category.put(CATEGORY_SIGNED, "签字件");
+ category.put(CATEGORY_OTHER, "其他");
+ CATEGORY_NAMES = Collections.unmodifiableMap(category);
+ }
+
+ /** 业务完成确认单流程定义(version=1)。 */
+ public static final String COMPLETION_NODES_JSON = "["
+ + "{\"key\":\"deptLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgLeader\",\"name\":\"报送部门负责人\"},"
+ + "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"resolver\":\"reporter\",\"name\":\"存档\"},"
+ + "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"resolver\":\"archivist\",\"name\":\"归档\"}"
+ + "]";
+
+ /** 内部协调单流程定义(version=1,表单与派单由后续迭代启用)。 */
+ public static final String COORDINATION_NODES_JSON = "["
+ + "{\"key\":\"reportLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgLeader\",\"name\":\"报送部门负责人\"},"
+ + "{\"key\":\"acceptLeader\",\"type\":\"APPROVE\",\"resolver\":\"acceptOrgLeader\",\"name\":\"受理部门负责人\"},"
+ + "{\"key\":\"dispatch\",\"type\":\"DISPATCH\",\"resolver\":\"acceptOrgMembers\",\"name\":\"派单\"},"
+ + "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"resolver\":\"reporterAndDispatchTarget\",\"name\":\"存档\"},"
+ + "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"resolver\":\"archivist\",\"name\":\"归档\"}"
+ + "]";
+
+ public static String statusName(String status) {
+ return STATUS_NAMES.getOrDefault(status, status);
+ }
+
+ public static String docTypeName(String docType) {
+ return DOC_TYPE_NAMES.getOrDefault(docType, docType);
+ }
+
+ public static String actionName(String action) {
+ return ACTION_NAMES.getOrDefault(action, action);
+ }
+
+ public static String categoryName(String category) {
+ return CATEGORY_NAMES.getOrDefault(category, category);
+ }
+
+ public static String departmentLeaderRoleCode(Integer organizationId) {
+ return organizationId == null ? null : "dept_leader_" + organizationId;
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/approval/controller/ApprovalAttachmentController.java b/src/main/java/com/gxwebsoft/approval/controller/ApprovalAttachmentController.java
new file mode 100644
index 0000000..8893eeb
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/controller/ApprovalAttachmentController.java
@@ -0,0 +1,47 @@
+package com.gxwebsoft.approval.controller;
+
+import com.gxwebsoft.approval.service.ApprovalAttachmentService;
+import com.gxwebsoft.approval.vo.ApprovalAttachmentVO;
+import com.gxwebsoft.common.core.web.ApiResult;
+import com.gxwebsoft.common.core.web.BaseController;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+
+/**
+ * 审批附件接口:走审批域端点,不要求文件管理权限。
+ */
+@Api(tags = "审批附件")
+@RestController
+@RequestMapping("/api/approval/attachment")
+public class ApprovalAttachmentController extends BaseController {
+
+ @Resource
+ private ApprovalAttachmentService attachmentService;
+
+ @PreAuthorize("hasAuthority('approval:instance:submit')")
+ @ApiOperation("上传附件")
+ @PostMapping("/upload")
+ public ApiResult upload(@RequestParam("instanceId") Long instanceId,
+ @RequestParam("file") MultipartFile file,
+ @RequestParam(value = "category", required = false) String category) {
+ return success(attachmentService.upload(instanceId, file, category, getLoginUser()));
+ }
+
+ @PreAuthorize("hasAuthority('approval:instance:submit')")
+ @ApiOperation("逻辑删除附件")
+ @DeleteMapping("/{id}")
+ public ApiResult> delete(@PathVariable("id") Long id) {
+ attachmentService.delete(id, getLoginUser());
+ return success("删除成功");
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/approval/controller/ApprovalDirectoryController.java b/src/main/java/com/gxwebsoft/approval/controller/ApprovalDirectoryController.java
new file mode 100644
index 0000000..192eb34
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/controller/ApprovalDirectoryController.java
@@ -0,0 +1,45 @@
+package com.gxwebsoft.approval.controller;
+
+import com.gxwebsoft.approval.service.ApprovalDirectoryService;
+import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
+import com.gxwebsoft.approval.vo.ApprovalUserVO;
+import com.gxwebsoft.common.core.web.ApiResult;
+import com.gxwebsoft.common.core.web.BaseController;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 审批目录接口:组织与启用用户。
+ */
+@Api(tags = "审批目录")
+@RestController
+@RequestMapping("/api/approval/directory")
+public class ApprovalDirectoryController extends BaseController {
+
+ @Resource
+ private ApprovalDirectoryService directoryService;
+
+ @PreAuthorize("hasAuthority('approval:instance:list')")
+ @ApiOperation("启用组织")
+ @GetMapping("/organizations")
+ public ApiResult> organizations() {
+ return success(directoryService.listOrganizations());
+ }
+
+ @PreAuthorize("hasAuthority('approval:instance:assign')")
+ @ApiOperation("启用用户搜索")
+ @GetMapping("/users")
+ public ApiResult> users(
+ @RequestParam(value = "keywords", required = false) String keywords,
+ @RequestParam(value = "organizationId", required = false) Integer organizationId) {
+ return success(directoryService.searchUsers(keywords, organizationId));
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/approval/controller/ApprovalInstanceController.java b/src/main/java/com/gxwebsoft/approval/controller/ApprovalInstanceController.java
new file mode 100644
index 0000000..586a594
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/controller/ApprovalInstanceController.java
@@ -0,0 +1,65 @@
+package com.gxwebsoft.approval.controller;
+
+import com.gxwebsoft.approval.param.ApprovalCompletionParam;
+import com.gxwebsoft.approval.param.ApprovalMineParam;
+import com.gxwebsoft.approval.service.ApprovalInstanceService;
+import com.gxwebsoft.approval.vo.ApprovalInstanceVO;
+import com.gxwebsoft.approval.vo.ApprovalItemVO;
+import com.gxwebsoft.common.core.web.ApiResult;
+import com.gxwebsoft.common.core.web.BaseController;
+import com.gxwebsoft.common.core.web.PageResult;
+import com.gxwebsoft.common.system.entity.User;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 审批实例接口(员工端与 PC 共用)。
+ */
+@Api(tags = "审批实例")
+@RestController
+@RequestMapping("/api/approval")
+public class ApprovalInstanceController extends BaseController {
+
+ @Resource
+ private ApprovalInstanceService instanceService;
+
+ @PreAuthorize("hasAuthority('approval:instance:submit')")
+ @ApiOperation("新建或保存业务完成确认单草稿")
+ @PostMapping("/instance/completion")
+ public ApiResult saveCompletion(@RequestBody ApprovalCompletionParam param) {
+ User user = getLoginUser();
+ return success("保存成功", instanceService.saveCompletion(param, user));
+ }
+
+ @PreAuthorize("hasAuthority('approval:instance:submit')")
+ @ApiOperation("提交或重提")
+ @PostMapping("/instance/{id}/submit")
+ public ApiResult submit(@PathVariable("id") Long id) {
+ User user = getLoginUser();
+ String warning = instanceService.submit(id, user);
+ return success(warning == null ? "提交成功" : warning, instanceService.detail(id, user));
+ }
+
+ @PreAuthorize("hasAuthority('approval:instance:list')")
+ @ApiOperation("单据详情")
+ @GetMapping("/instance/{id}")
+ public ApiResult detail(@PathVariable("id") Long id) {
+ return success(instanceService.detail(id, getLoginUser()));
+ }
+
+ @PreAuthorize("hasAuthority('approval:instance:list')")
+ @ApiOperation("我发起的")
+ @GetMapping("/mine/page")
+ public ApiResult> mine(ApprovalMineParam param) {
+ return success(instanceService.mine(param, getLoginUser()));
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/approval/entity/ApprovalAttachment.java b/src/main/java/com/gxwebsoft/approval/entity/ApprovalAttachment.java
new file mode 100644
index 0000000..3aa1985
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/entity/ApprovalAttachment.java
@@ -0,0 +1,41 @@
+package com.gxwebsoft.approval.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 审批附件绑定(逻辑删除)。
+ */
+@Data
+@ApiModel(description = "审批附件")
+@TableName("approval_attachment")
+public class ApprovalAttachment implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Integer tenantId;
+
+ private Long instanceId;
+
+ private Integer fileId;
+
+ @ApiModelProperty("分类:SIGNED 签字件 / OTHER 其他")
+ private String category;
+
+ private Integer uploaderId;
+
+ @TableLogic
+ private Integer deleted;
+
+ private LocalDateTime createTime;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/entity/ApprovalCompletionForm.java b/src/main/java/com/gxwebsoft/approval/entity/ApprovalCompletionForm.java
new file mode 100644
index 0000000..456873b
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/entity/ApprovalCompletionForm.java
@@ -0,0 +1,40 @@
+package com.gxwebsoft.approval.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 业务完成确认单。
+ */
+@Data
+@ApiModel(description = "业务完成确认单")
+@TableName("approval_completion_form")
+public class ApprovalCompletionForm implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Integer tenantId;
+
+ private Long instanceId;
+
+ private String name;
+
+ private String implementOrg;
+
+ private BigDecimal fee;
+
+ private String serviceContent;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/entity/ApprovalCoordinationForm.java b/src/main/java/com/gxwebsoft/approval/entity/ApprovalCoordinationForm.java
new file mode 100644
index 0000000..121d497
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/entity/ApprovalCoordinationForm.java
@@ -0,0 +1,41 @@
+package com.gxwebsoft.approval.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 内部协调单(一期仅内部流程)。
+ */
+@Data
+@ApiModel(description = "内部协调单")
+@TableName("approval_coordination_form")
+public class ApprovalCoordinationForm implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Integer tenantId;
+
+ private Long instanceId;
+
+ private Integer acceptOrgId;
+
+ private String acceptOrgName;
+
+ private String title;
+
+ private String content;
+
+ private String remark;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/entity/ApprovalFlowDefinition.java b/src/main/java/com/gxwebsoft/approval/entity/ApprovalFlowDefinition.java
new file mode 100644
index 0000000..fce235c
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/entity/ApprovalFlowDefinition.java
@@ -0,0 +1,41 @@
+package com.gxwebsoft.approval.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 审批流程定义版本。
+ */
+@Data
+@ApiModel(description = "审批流程定义版本")
+@TableName("approval_flow_definition")
+public class ApprovalFlowDefinition implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ @ApiModelProperty("租户id")
+ private Integer tenantId;
+
+ @ApiModelProperty("流程编码")
+ private String flowCode;
+
+ @ApiModelProperty("流程版本")
+ private Integer version;
+
+ @ApiModelProperty("流程名称")
+ private String name;
+
+ @ApiModelProperty("节点JSON")
+ private String nodesJson;
+
+ private LocalDateTime createTime;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/entity/ApprovalInstance.java b/src/main/java/com/gxwebsoft/approval/entity/ApprovalInstance.java
new file mode 100644
index 0000000..ee9ff55
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/entity/ApprovalInstance.java
@@ -0,0 +1,80 @@
+package com.gxwebsoft.approval.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 审批实例。
+ */
+@Data
+@ApiModel(description = "审批实例")
+@TableName("approval_instance")
+public class ApprovalInstance implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Integer tenantId;
+
+ @ApiModelProperty("单号,草稿为空")
+ private String bizNo;
+
+ private String flowCode;
+
+ private Long definitionId;
+
+ private Integer definitionVersion;
+
+ @ApiModelProperty("单据类型:completion/coordination")
+ private String docType;
+
+ @ApiModelProperty("业务表主键")
+ private Long bizId;
+
+ private String status;
+
+ @ApiModelProperty("重提轮次")
+ private Integer round;
+
+ private Integer reporterId;
+
+ private String reporterName;
+
+ private Integer reportOrgId;
+
+ private String reportOrgName;
+
+ private Integer acceptOrgId;
+
+ private String acceptOrgName;
+
+ private String currentNodeKey;
+
+ private String currentNodeType;
+
+ private Integer dispatchTargetId;
+
+ private String dispatchTargetName;
+
+ private LocalDateTime submitTime;
+
+ private LocalDateTime approveTime;
+
+ private LocalDateTime archiveTime;
+
+ private Integer archivistId;
+
+ private String archivistName;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/entity/ApprovalNoSequence.java b/src/main/java/com/gxwebsoft/approval/entity/ApprovalNoSequence.java
new file mode 100644
index 0000000..149b313
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/entity/ApprovalNoSequence.java
@@ -0,0 +1,31 @@
+package com.gxwebsoft.approval.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+
+/**
+ * 审批单号序列(租户 + 前缀 + 日期)。
+ */
+@Data
+@ApiModel(description = "审批单号序列")
+@TableName("approval_no_sequence")
+public class ApprovalNoSequence implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Integer tenantId;
+
+ private String prefix;
+
+ private LocalDate seqDate;
+
+ private Integer currentSeq;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/entity/ApprovalRecord.java b/src/main/java/com/gxwebsoft/approval/entity/ApprovalRecord.java
new file mode 100644
index 0000000..22d99a2
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/entity/ApprovalRecord.java
@@ -0,0 +1,43 @@
+package com.gxwebsoft.approval.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 审批处理记录(只增不改)。
+ */
+@Data
+@ApiModel(description = "审批处理记录")
+@TableName("approval_record")
+public class ApprovalRecord implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Integer tenantId;
+
+ private Long instanceId;
+
+ private Long taskId;
+
+ private String nodeKey;
+
+ private Integer operatorId;
+
+ private String operatorName;
+
+ private String action;
+
+ private String opinion;
+
+ private Integer round;
+
+ private LocalDateTime createTime;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/entity/ApprovalTask.java b/src/main/java/com/gxwebsoft/approval/entity/ApprovalTask.java
new file mode 100644
index 0000000..81a34b8
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/entity/ApprovalTask.java
@@ -0,0 +1,53 @@
+package com.gxwebsoft.approval.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 审批任务。
+ */
+@Data
+@ApiModel(description = "审批任务")
+@TableName("approval_task")
+public class ApprovalTask implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Integer tenantId;
+
+ private Long instanceId;
+
+ private String nodeKey;
+
+ private String nodeType;
+
+ private Integer round;
+
+ private String taskType;
+
+ private Integer assigneeId;
+
+ private String assigneeName;
+
+ private String status;
+
+ private LocalDateTime handleTime;
+
+ private String opinion;
+
+ private Integer dispatchTargetId;
+
+ private String dispatchTargetName;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalAttachmentMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalAttachmentMapper.java
new file mode 100644
index 0000000..eeb5962
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalAttachmentMapper.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.approval.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.gxwebsoft.approval.entity.ApprovalAttachment;
+
+/**
+ * 审批附件Mapper。
+ */
+public interface ApprovalAttachmentMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalCompletionFormMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalCompletionFormMapper.java
new file mode 100644
index 0000000..0019df0
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalCompletionFormMapper.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.approval.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.gxwebsoft.approval.entity.ApprovalCompletionForm;
+
+/**
+ * 业务完成确认单Mapper。
+ */
+public interface ApprovalCompletionFormMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalCoordinationFormMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalCoordinationFormMapper.java
new file mode 100644
index 0000000..9c4c13f
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalCoordinationFormMapper.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.approval.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.gxwebsoft.approval.entity.ApprovalCoordinationForm;
+
+/**
+ * 内部协调单Mapper。
+ */
+public interface ApprovalCoordinationFormMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalFlowDefinitionMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalFlowDefinitionMapper.java
new file mode 100644
index 0000000..2ee3873
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalFlowDefinitionMapper.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.approval.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.gxwebsoft.approval.entity.ApprovalFlowDefinition;
+
+/**
+ * 审批流程定义Mapper。
+ */
+public interface ApprovalFlowDefinitionMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalInstanceMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalInstanceMapper.java
new file mode 100644
index 0000000..0212f77
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalInstanceMapper.java
@@ -0,0 +1,35 @@
+package com.gxwebsoft.approval.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.gxwebsoft.approval.entity.ApprovalInstance;
+import com.gxwebsoft.approval.param.ApprovalMineParam;
+import com.gxwebsoft.approval.support.ApprovalItemRow;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 审批实例Mapper。
+ */
+public interface ApprovalInstanceMapper extends BaseMapper {
+
+ /**
+ * 对实例行加锁(必须在事务内调用)。
+ */
+ ApprovalInstance selectByIdForUpdate(@Param("id") Long id);
+
+ /**
+ * 判断当前用户是否可查看实例。
+ */
+ int countVisible(@Param("id") Long id,
+ @Param("userId") Integer userId,
+ @Param("allVisible") Integer allVisible);
+
+ /**
+ * 我发起的列表。
+ */
+ List selectMinePage(@Param("page") IPage page,
+ @Param("param") ApprovalMineParam param,
+ @Param("userId") Integer userId);
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalNoSequenceMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalNoSequenceMapper.java
new file mode 100644
index 0000000..999e44d
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalNoSequenceMapper.java
@@ -0,0 +1,33 @@
+package com.gxwebsoft.approval.mapper;
+
+import com.gxwebsoft.approval.entity.ApprovalNoSequence;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+/**
+ * 审批单号序列Mapper:事务内行锁取号。
+ */
+public interface ApprovalNoSequenceMapper {
+
+ /**
+ * 确保当天序列行存在;唯一键冲突时静默忽略。
+ */
+ @Insert("INSERT IGNORE INTO approval_no_sequence (prefix, seq_date, current_seq) "
+ + "VALUES (#{prefix}, CURDATE(), 0)")
+ int insertTodayIfAbsent(@Param("prefix") String prefix);
+
+ /**
+ * 锁定当天序列行。
+ */
+ @Select("SELECT id, prefix, seq_date, current_seq FROM approval_no_sequence "
+ + "WHERE prefix = #{prefix} AND seq_date = CURDATE() FOR UPDATE")
+ ApprovalNoSequence selectTodayForUpdate(@Param("prefix") String prefix);
+
+ /**
+ * 序号自增。
+ */
+ @Update("UPDATE approval_no_sequence SET current_seq = current_seq + 1 WHERE id = #{id}")
+ int increment(@Param("id") Long id);
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalRecordMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalRecordMapper.java
new file mode 100644
index 0000000..34f58ed
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalRecordMapper.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.approval.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.gxwebsoft.approval.entity.ApprovalRecord;
+
+/**
+ * 审批处理记录Mapper。
+ */
+public interface ApprovalRecordMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalTaskMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalTaskMapper.java
new file mode 100644
index 0000000..9326221
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalTaskMapper.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.approval.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.gxwebsoft.approval.entity.ApprovalTask;
+
+/**
+ * 审批任务Mapper。
+ */
+public interface ApprovalTaskMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/xml/ApprovalInstanceMapper.xml b/src/main/java/com/gxwebsoft/approval/mapper/xml/ApprovalInstanceMapper.xml
new file mode 100644
index 0000000..61cdb83
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/mapper/xml/ApprovalInstanceMapper.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/gxwebsoft/approval/param/ApprovalCompletionParam.java b/src/main/java/com/gxwebsoft/approval/param/ApprovalCompletionParam.java
new file mode 100644
index 0000000..e8151d4
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/param/ApprovalCompletionParam.java
@@ -0,0 +1,30 @@
+package com.gxwebsoft.approval.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 业务完成确认单草稿参数;id 为审批实例id,更新草稿时传入。
+ */
+@Data
+public class ApprovalCompletionParam implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty("审批实例id(更新草稿时传入)")
+ private Long id;
+
+ @ApiModelProperty("名称")
+ private String name;
+
+ @ApiModelProperty("实施单位")
+ private String implementOrg;
+
+ @ApiModelProperty("费用(元)")
+ private BigDecimal fee;
+
+ @ApiModelProperty("服务内容")
+ private String serviceContent;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/param/ApprovalMineParam.java b/src/main/java/com/gxwebsoft/approval/param/ApprovalMineParam.java
new file mode 100644
index 0000000..05b67f5
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/param/ApprovalMineParam.java
@@ -0,0 +1,20 @@
+package com.gxwebsoft.approval.param;
+
+import com.gxwebsoft.common.core.web.BaseParam;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 我发起的查询参数。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ApprovalMineParam extends BaseParam {
+
+ @ApiModelProperty("实例状态")
+ private String status;
+
+ @ApiModelProperty("单据类型")
+ private String docType;
+}
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalAttachmentService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalAttachmentService.java
new file mode 100644
index 0000000..7a3dbfa
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalAttachmentService.java
@@ -0,0 +1,243 @@
+package com.gxwebsoft.approval.service;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.gxwebsoft.approval.constant.ApprovalConstants;
+import com.gxwebsoft.approval.entity.ApprovalAttachment;
+import com.gxwebsoft.approval.entity.ApprovalInstance;
+import com.gxwebsoft.approval.mapper.ApprovalAttachmentMapper;
+import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
+import com.gxwebsoft.approval.vo.ApprovalAttachmentVO;
+import com.gxwebsoft.approval.vo.ApprovalUserVO;
+import com.gxwebsoft.common.core.config.ConfigProperties;
+import com.gxwebsoft.common.core.exception.BusinessException;
+import com.gxwebsoft.common.core.utils.FileServerUtil;
+import com.gxwebsoft.common.system.entity.FileRecord;
+import com.gxwebsoft.common.system.entity.User;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 审批附件服务:上传、逻辑删除、详情拼装。
+ *
+ * 文件落盘与 sys_file_record 复用既有文件基础设施,附件行写 master 库。
+ * 上传不开启跨库事务:先落文件记录再绑定附件,失败时最多留下孤儿文件(允许)。
+ */
+@Service
+public class ApprovalAttachmentService {
+
+ private static final Logger log = LoggerFactory.getLogger(ApprovalAttachmentService.class);
+
+ @Resource
+ private ApprovalInstanceMapper instanceMapper;
+ @Resource
+ private ApprovalAttachmentMapper attachmentMapper;
+ @Resource
+ private ApprovalFileService fileService;
+ @Resource
+ private ApprovalDirectoryService directoryService;
+ @Resource
+ private ConfigProperties config;
+
+ /**
+ * 详情附件列表:过滤逻辑删除,并从文件记录补齐名称/地址/大小。
+ */
+ public List listVos(Long instanceId) {
+ List attachments = attachmentMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(ApprovalAttachment::getInstanceId, instanceId)
+ .orderByAsc(ApprovalAttachment::getId));
+ if (attachments.isEmpty()) {
+ return new ArrayList<>();
+ }
+ List fileIds = attachments.stream().map(ApprovalAttachment::getFileId).collect(Collectors.toList());
+ Map files = fileService.listByIds(fileIds).stream()
+ .collect(Collectors.toMap(FileRecord::getId, Function.identity(), (a, b) -> a));
+ List uploaderIds = attachments.stream().map(ApprovalAttachment::getUploaderId)
+ .distinct().collect(Collectors.toList());
+ Map uploaders = directoryService.listUsersByIds(uploaderIds).stream()
+ .collect(Collectors.toMap(ApprovalUserVO::getUserId, this::displayName, (a, b) -> a));
+ List result = new ArrayList<>();
+ for (ApprovalAttachment attachment : attachments) {
+ FileRecord file = files.get(attachment.getFileId());
+ if (file == null) {
+ // 文件记录缺失视为不可用附件,不向前端暴露悬空链接
+ continue;
+ }
+ result.add(toVO(attachment, file, uploaders.get(attachment.getUploaderId())));
+ }
+ return result;
+ }
+
+ public int count(Long instanceId) {
+ Integer count = attachmentMapper.selectCount(new LambdaQueryWrapper()
+ .eq(ApprovalAttachment::getInstanceId, instanceId));
+ return count == null ? 0 : count;
+ }
+
+ /**
+ * 上传附件:仅发起人且实例可编辑时可上传。
+ */
+ public ApprovalAttachmentVO upload(Long instanceId, MultipartFile file, String category, User user) {
+ ApprovalInstance instance = instanceMapper.selectById(instanceId);
+ if (instance == null) {
+ throw new BusinessException("单据不存在");
+ }
+ checkEditable(instance, user);
+
+ String normalizedCategory = StrUtil.isBlank(category) ? ApprovalConstants.CATEGORY_OTHER : category.trim();
+ if (!ApprovalConstants.CATEGORY_SIGNED.equals(normalizedCategory)
+ && !ApprovalConstants.CATEGORY_OTHER.equals(normalizedCategory)) {
+ throw new BusinessException("附件分类不正确");
+ }
+ if (file == null || file.isEmpty()) {
+ throw new BusinessException("附件内容为空");
+ }
+ if (count(instanceId) >= ApprovalConstants.MAX_ATTACHMENT_COUNT) {
+ throw new BusinessException("每单最多上传" + ApprovalConstants.MAX_ATTACHMENT_COUNT + "张附件");
+ }
+ if (file.getSize() > ApprovalConstants.MAX_ATTACHMENT_SIZE) {
+ throw new BusinessException("单个附件不能超过20MB");
+ }
+ String extension = extensionOf(file.getOriginalFilename());
+ checkExtensionAndContentType(extension, file.getContentType());
+
+ File stored;
+ String path;
+ try {
+ String base = config.getUploadPath();
+ String dir = (base.endsWith("/") ? base : base + "/") + "approval/";
+ stored = FileServerUtil.upload(file, dir, Boolean.TRUE.equals(config.getUploadUuidName()));
+ path = stored.getAbsolutePath().replace("\\", "/").substring(dir.length() - 1);
+ } catch (Exception e) {
+ log.error("审批附件落盘失败", e);
+ throw new BusinessException("附件上传失败");
+ }
+
+ FileRecord fileRecord = new FileRecord();
+ fileRecord.setCreateUserId(user.getUserId());
+ fileRecord.setName(StrUtil.isBlank(file.getOriginalFilename()) ? stored.getName() : file.getOriginalFilename());
+ fileRecord.setLength(stored.length());
+ fileRecord.setPath(path);
+ fileRecord.setContentType(normalizeContentType(file.getContentType()));
+ fileService.save(fileRecord);
+
+ ApprovalAttachment attachment = new ApprovalAttachment();
+ attachment.setInstanceId(instanceId);
+ attachment.setFileId(fileRecord.getId());
+ attachment.setCategory(normalizedCategory);
+ attachment.setUploaderId(user.getUserId());
+ attachment.setDeleted(0);
+ attachment.setCreateTime(LocalDateTime.now());
+ attachmentMapper.insert(attachment);
+
+ return toVO(attachment, fileRecord, displayName(user));
+ }
+
+ /**
+ * 逻辑删除附件,底层文件记录不删除。
+ */
+ public void delete(Long attachmentId, User user) {
+ ApprovalAttachment attachment = attachmentMapper.selectById(attachmentId);
+ if (attachment == null) {
+ throw new BusinessException("附件不存在");
+ }
+ ApprovalInstance instance = instanceMapper.selectById(attachment.getInstanceId());
+ if (instance == null) {
+ throw new BusinessException("单据不存在");
+ }
+ checkEditable(instance, user);
+ attachmentMapper.deleteById(attachmentId);
+ }
+
+ private void checkEditable(ApprovalInstance instance, User user) {
+ if (user == null || !instance.getReporterId().equals(user.getUserId())
+ || !ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
+ throw new BusinessException("当前状态不可修改");
+ }
+ }
+
+ private void checkExtensionAndContentType(String extension, String contentType) {
+ if (extension == null || !ApprovalConstants.ATTACHMENT_EXTENSIONS.contains(extension)) {
+ throw new BusinessException("不支持的附件类型,仅支持 jpg/jpeg/png/pdf/doc/docx/xls/xlsx");
+ }
+ List allowed = ApprovalConstants.ATTACHMENT_CONTENT_TYPES.get(extension);
+ String normalized = normalizeContentType(contentType);
+ if (normalized == null || allowed == null || !allowed.contains(normalized)) {
+ throw new BusinessException("不支持的附件类型,仅支持 jpg/jpeg/png/pdf/doc/docx/xls/xlsx");
+ }
+ }
+
+ private String extensionOf(String filename) {
+ if (filename == null) {
+ return null;
+ }
+ int index = filename.lastIndexOf('.');
+ if (index < 0 || index == filename.length() - 1) {
+ return null;
+ }
+ return filename.substring(index + 1).toLowerCase();
+ }
+
+ private String normalizeContentType(String contentType) {
+ if (contentType == null) {
+ return null;
+ }
+ int index = contentType.indexOf(';');
+ String value = index >= 0 ? contentType.substring(0, index) : contentType;
+ return value.trim().toLowerCase();
+ }
+
+ private ApprovalAttachmentVO toVO(ApprovalAttachment attachment, FileRecord file, String uploaderName) {
+ ApprovalAttachmentVO vo = new ApprovalAttachmentVO();
+ vo.setId(attachment.getId());
+ vo.setFileId(attachment.getFileId());
+ vo.setName(file.getName());
+ vo.setUrl(urlOf(file));
+ vo.setLength(file.getLength());
+ vo.setCategory(attachment.getCategory());
+ vo.setCategoryName(ApprovalConstants.categoryName(attachment.getCategory()));
+ vo.setUploaderId(attachment.getUploaderId());
+ vo.setUploaderName(uploaderName);
+ vo.setCreateTime(attachment.getCreateTime());
+ return vo;
+ }
+
+ private String urlOf(FileRecord file) {
+ if (StrUtil.isNotBlank(file.getUrl())) {
+ return file.getUrl();
+ }
+ return config.getServerUrl() + "/file" + (file.getPath() == null ? "" : file.getPath());
+ }
+
+ private String displayName(User user) {
+ if (StrUtil.isNotBlank(user.getRealName())) {
+ return user.getRealName();
+ }
+ if (StrUtil.isNotBlank(user.getNickname())) {
+ return user.getNickname();
+ }
+ return user.getUsername();
+ }
+
+ private String displayName(ApprovalUserVO user) {
+ if (StrUtil.isNotBlank(user.getRealName())) {
+ return user.getRealName();
+ }
+ if (StrUtil.isNotBlank(user.getNickname())) {
+ return user.getNickname();
+ }
+ return user.getUsername();
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java
new file mode 100644
index 0000000..0005653
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java
@@ -0,0 +1,177 @@
+package com.gxwebsoft.approval.service;
+
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.gxwebsoft.approval.support.ApprovalTenantContext;
+import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
+import com.gxwebsoft.approval.vo.ApprovalUserVO;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Service;
+
+import javax.sql.DataSource;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 审批目录服务:组织、部门负责人/收文员解析、启用用户查询。
+ *
+ * 直接使用 gxtyzx_core 的专用连接,而不是 MyBatis 动态数据源代理:
+ * 审批请求可能处在外层事务中,动态数据源在事务内无法切换库。
+ * 所有 SQL 显式带 tenant_id。
+ */
+@Service
+public class ApprovalDirectoryService {
+
+ private static final RowMapper ORGANIZATION_MAPPER = (resultSet, rowNum) -> {
+ ApprovalOrganizationVO vo = new ApprovalOrganizationVO();
+ vo.setOrganizationId(resultSet.getInt("organization_id"));
+ vo.setParentId(resultSet.getInt("parent_id"));
+ vo.setOrganizationName(resultSet.getString("organization_name"));
+ vo.setOrganizationFullName(resultSet.getString("organization_full_name"));
+ vo.setOrganizationCode(resultSet.getString("organization_code"));
+ int leaderId = resultSet.getInt("leader_id");
+ vo.setLeaderId(resultSet.wasNull() ? null : leaderId);
+ return vo;
+ };
+
+ private static final RowMapper USER_MAPPER = (resultSet, rowNum) -> {
+ ApprovalUserVO vo = new ApprovalUserVO();
+ vo.setUserId(resultSet.getInt("user_id"));
+ vo.setUsername(resultSet.getString("username"));
+ vo.setNickname(resultSet.getString("nickname"));
+ vo.setRealName(resultSet.getString("real_name"));
+ int organizationId = resultSet.getInt("organization_id");
+ vo.setOrganizationId(resultSet.wasNull() ? null : organizationId);
+ vo.setOrganizationName(resultSet.getString("organization_name"));
+ return vo;
+ };
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public ApprovalDirectoryService(DataSource dataSource) {
+ DataSource server = ((DynamicRoutingDataSource) dataSource).getDataSource("server");
+ this.jdbcTemplate = new JdbcTemplate(server);
+ }
+
+ /** 启用组织。 */
+ public List listOrganizations() {
+ return jdbcTemplate.query(
+ "SELECT organization_id, parent_id, organization_name, organization_full_name, "
+ + "organization_code, leader_id FROM sys_organization "
+ + "WHERE tenant_id = ? AND deleted = 0 ORDER BY sort_number, organization_id",
+ ORGANIZATION_MAPPER, ApprovalTenantContext.currentTenantId());
+ }
+
+ /** 按id取启用组织;不存在返回 null。 */
+ public ApprovalOrganizationVO getOrganization(Integer organizationId) {
+ if (organizationId == null || organizationId <= 0) {
+ return null;
+ }
+ List list = jdbcTemplate.query(
+ "SELECT organization_id, parent_id, organization_name, organization_full_name, "
+ + "organization_code, leader_id FROM sys_organization "
+ + "WHERE organization_id = ? AND tenant_id = ? AND deleted = 0",
+ ORGANIZATION_MAPPER, organizationId, ApprovalTenantContext.currentTenantId());
+ return list.isEmpty() ? null : list.get(0);
+ }
+
+ /** 角色下全部启用成员;角色不存在或成员为空时返回空列表。 */
+ public List listRoleMembers(String roleCode) {
+ if (roleCode == null) {
+ return Collections.emptyList();
+ }
+ Integer tenantId = ApprovalTenantContext.currentTenantId();
+ return jdbcTemplate.query(
+ "SELECT u.user_id, u.username, u.nickname, u.real_name, u.organization_id, o.organization_name "
+ + "FROM sys_user u "
+ + "JOIN sys_user_role ur ON ur.user_id = u.user_id AND ur.tenant_id = ? "
+ + "JOIN sys_role r ON r.role_id = ur.role_id AND r.tenant_id = ? "
+ + "LEFT JOIN sys_organization o ON o.organization_id = u.organization_id AND o.tenant_id = ? "
+ + "WHERE r.role_code = ? AND r.deleted = 0 AND u.status = 0 AND u.deleted = 0 "
+ + "AND u.tenant_id = ? ORDER BY u.user_id",
+ USER_MAPPER, tenantId, tenantId, tenantId, roleCode, tenantId);
+ }
+
+ /** 部门负责人角色是否已配置至少一名启用成员。 */
+ public boolean hasDepartmentLeader(Integer organizationId) {
+ return organizationId != null && !listRoleMembers("dept_leader_" + organizationId).isEmpty();
+ }
+
+ /** 启用用户搜索,最多 50 条。 */
+ public List searchUsers(String keywords, Integer organizationId) {
+ Integer tenantId = ApprovalTenantContext.currentTenantId();
+ StringBuilder sql = new StringBuilder(
+ "SELECT u.user_id, u.username, u.nickname, u.real_name, u.organization_id, o.organization_name "
+ + "FROM sys_user u "
+ + "LEFT JOIN sys_organization o ON o.organization_id = u.organization_id AND o.tenant_id = ? "
+ + "WHERE u.tenant_id = ? AND u.status = 0 AND u.deleted = 0");
+ List
-
- org.springframework.security
- spring-security-test
- test
-
From 3b599efb3b5466f6e28bc975b41c35b805b58c3e Mon Sep 17 00:00:00 2001
From: weicw <594098497@qq.com>
Date: Fri, 30 Oct 2026 20:27:11 +0800
Subject: [PATCH 3/3] =?UTF-8?q?test(approval):=20=E8=A6=86=E7=9B=96?=
=?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=AE=9A=E4=B9=89=E5=B9=82=E7=AD=89=EF=BC=9B?=
=?UTF-8?q?=E6=A0=B8=E5=BF=83=E5=BA=93=E6=9C=8D=E5=8A=A1=E6=98=BE=E5=BC=8F?=
=?UTF-8?q?=E6=A0=87=E6=B3=A8=20server=20=E6=95=B0=E6=8D=AE=E6=BA=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../approval/service/ApprovalDirectoryService.java | 2 ++
.../approval/service/ApprovalFileService.java | 2 ++
.../approval/ApprovalCompletionDraftIT.java | 13 +++++++++++++
3 files changed, 17 insertions(+)
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java
index 0005653..c032296 100644
--- a/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java
@@ -1,6 +1,7 @@
package com.gxwebsoft.approval.service;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.baomidou.dynamic.datasource.annotation.DS;
import com.gxwebsoft.approval.support.ApprovalTenantContext;
import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
import com.gxwebsoft.approval.vo.ApprovalUserVO;
@@ -20,6 +21,7 @@ import java.util.List;
* 所有 SQL 显式带 tenant_id。
*/
@Service
+@DS("server")
public class ApprovalDirectoryService {
private static final RowMapper ORGANIZATION_MAPPER = (resultSet, rowNum) -> {
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalFileService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalFileService.java
index c987404..102a855 100644
--- a/src/main/java/com/gxwebsoft/approval/service/ApprovalFileService.java
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalFileService.java
@@ -1,6 +1,7 @@
package com.gxwebsoft.approval.service;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.baomidou.dynamic.datasource.annotation.DS;
import com.gxwebsoft.approval.support.ApprovalTenantContext;
import com.gxwebsoft.common.system.entity.FileRecord;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -24,6 +25,7 @@ import java.util.List;
* 不依赖文件管理权限,审批上传/详情均通过本服务复用既有文件记录。
*/
@Service
+@DS("server")
public class ApprovalFileService {
private static final RowMapper FILE_MAPPER = (resultSet, rowNum) -> {
diff --git a/src/test/java/com/gxwebsoft/approval/ApprovalCompletionDraftIT.java b/src/test/java/com/gxwebsoft/approval/ApprovalCompletionDraftIT.java
index 953aa0e..509b4c0 100644
--- a/src/test/java/com/gxwebsoft/approval/ApprovalCompletionDraftIT.java
+++ b/src/test/java/com/gxwebsoft/approval/ApprovalCompletionDraftIT.java
@@ -96,6 +96,19 @@ class ApprovalCompletionDraftIT {
.andExpect(jsonPath("$.data.list[0].bizNo").doesNotExist());
}
+ @Test
+ void publishesSingleFlowDefinitionPerTenant() throws Exception {
+ createCompletion(reporter, organizationId, TENANT, "流程一");
+ createCompletion(reporter, organizationId, TENANT, "流程二");
+
+ Integer count = data.countMaster("SELECT COUNT(*) FROM approval_flow_definition "
+ + "WHERE tenant_id = ? AND flow_code = 'completion' AND version = 1", TENANT);
+ assertThat(count).isEqualTo(1);
+ assertThat(data.queryMaster("SELECT nodes_json FROM approval_flow_definition "
+ + "WHERE tenant_id = ? AND flow_code = 'completion' AND version = 1", String.class, TENANT))
+ .contains("deptLeader").contains("confirm").contains("archive");
+ }
+
@Test
void validatesCompletionFields() throws Exception {
expectSaveFailure(body("", "实施单位", "10.00", "服务内容"), "名称不能为空");