feat: 二期审批引擎扩展与会签/明细/回填/附件分类共享层(#46)
- ApprovalDocumentType SPI + 注册表:新增单据不再改引擎 - ApprovalApproverResolver:会签组、orgLeaderByCode/orgDeputyByCode、租户岗位解析器 - 会签:任务带 group_key/group_name/group_seq,一人多组一次通过,只取消同组待办 - 回填:节点 writableFields、BACKFILL 动作、approval_field_change 留痕 - 附件:新增 VOUCHER 分类并按单据类型校验必传 - 详情:document.fields/lines/lineColumns/groupProgress + permissions.writableFields - 列表/导出:title/search_text 落库、flowCode 筛选、明细行数/合计金额/会签进度列、明细导出 - sql/05-approval_phase2.sql(可重复执行)
This commit is contained in:
@@ -75,3 +75,22 @@ WHERE m.tenant_id = 10150
|
||||
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
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
-- 价格表查看(ADR 0008):受众全体员工,独立权限点,配 /approval/price 只读页
|
||||
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/price', '/approval/price', 3, 'approval:price:view', '', 0, 10150, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND path = '/approval/price' 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 @instanceMenu, '价格表查看', '', '', 6, 'approval:price:view', '', 1, 10150, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND authority = 'approval:price:view' AND menu_type = 1 AND deleted = 0
|
||||
);
|
||||
|
||||
@@ -57,3 +57,42 @@ WHERE r.tenant_id = @tenant_id AND r.deleted = 0 AND r.role_code = 'approval_arc
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id AND rm.tenant_id = @tenant_id
|
||||
);
|
||||
|
||||
-- 4. 相关部门(体验券领用申请单固定角色,ADR 0013;待客户确认)
|
||||
INSERT INTO sys_role (role_name, role_code, comments, sort_number, tenant_id, deleted)
|
||||
SELECT '相关部门', 'approval_related_dept', '审批种子:体验券领用申请单「相关部门」节点,多人或签(ADR 0013)', 926, @tenant_id, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_role WHERE tenant_id = @tenant_id AND deleted = 0 AND role_code = 'approval_related_dept'
|
||||
);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id, tenant_id)
|
||||
SELECT r.role_id, m.menu_id, @tenant_id
|
||||
FROM sys_role r
|
||||
JOIN sys_menu m
|
||||
ON m.tenant_id = @tenant_id AND m.deleted = 0
|
||||
AND (m.path IN ('/approval', '/approval/todo', '/approval/instance')
|
||||
OR m.authority IN ('approval:instance:list', 'approval:instance:submit', 'approval:instance:approve'))
|
||||
WHERE r.tenant_id = @tenant_id AND r.deleted = 0 AND r.role_code = 'approval_related_dept'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id AND rm.tenant_id = @tenant_id
|
||||
);
|
||||
|
||||
-- 5. 价格表查看:全体员工可查当前生效与历史版本(ADR 0008)
|
||||
INSERT INTO sys_role_menu (role_id, menu_id, tenant_id)
|
||||
SELECT r.role_id, m.menu_id, @tenant_id
|
||||
FROM sys_role r
|
||||
JOIN sys_menu m
|
||||
ON m.tenant_id = @tenant_id AND m.deleted = 0
|
||||
AND (m.path = '/approval/price' OR m.authority = 'approval:price:view')
|
||||
WHERE r.tenant_id = @tenant_id AND r.deleted = 0
|
||||
AND (r.role_code = 'approval_staff'
|
||||
OR LEFT(r.role_code, 12) = 'dept_leader_'
|
||||
OR LEFT(r.role_code, 12) = 'dept_deputy_'
|
||||
OR r.role_code IN ('approval_archivist', 'approval_cashier', 'approval_typist',
|
||||
'approval_issuer', 'approval_general_manager', 'approval_related_dept'))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu rm
|
||||
WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id AND rm.tenant_id = @tenant_id
|
||||
);
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
-- 二期审批引擎扩展(可重复执行)
|
||||
-- 目标库:db_gxtyzx(master)
|
||||
-- 执行顺序:01-approval.sql -> 02-approval_menu.sql -> 03-approval_orgs_roles.sql
|
||||
-- -> 04-approval_role_grants.sql -> 05-approval_phase2.sql
|
||||
--
|
||||
-- 覆盖 ADR 0003(会签)、0004(明细行)、0006(回填)、0007(流程类型筛选)。
|
||||
-- MySQL 8 不支持 ADD COLUMN IF NOT EXISTS,因此用 information_schema 判断后动态执行。
|
||||
|
||||
-- 老数据回填:一期实例没有 title/search_text(幂等,只补空值)
|
||||
UPDATE approval_instance i
|
||||
JOIN approval_completion_form f ON f.instance_id = i.id
|
||||
SET i.title = f.name,
|
||||
i.search_text = CONCAT_WS(' ', f.name, f.implement_org, f.service_content)
|
||||
WHERE i.title IS NULL;
|
||||
|
||||
UPDATE approval_instance i
|
||||
JOIN approval_coordination_form c ON c.instance_id = i.id
|
||||
SET i.title = c.title,
|
||||
i.search_text = CONCAT_WS(' ', c.title, c.content, c.remark)
|
||||
WHERE i.title IS NULL;
|
||||
|
||||
DROP PROCEDURE IF EXISTS approval_phase2_add_column;
|
||||
DROP PROCEDURE IF EXISTS approval_phase2_add_index;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE approval_phase2_add_column(IN p_table VARCHAR(64), IN p_column VARCHAR(64),
|
||||
IN p_definition TEXT)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = p_table AND column_name = p_column
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE ', p_table, ' ADD COLUMN ', p_definition);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
CREATE PROCEDURE approval_phase2_add_index(IN p_table VARCHAR(64), IN p_index VARCHAR(64),
|
||||
IN p_definition TEXT)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = p_table AND index_name = p_index
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE ', p_table, ' ADD ', p_definition);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
-- 会签任务分组:非会签节点三列为空(ADR 0003)
|
||||
CALL approval_phase2_add_column('approval_task', 'group_key',
|
||||
'group_key VARCHAR(40) DEFAULT NULL COMMENT ''会签组标识'' AFTER opinion');
|
||||
CALL approval_phase2_add_column('approval_task', 'group_name',
|
||||
'group_name VARCHAR(100) DEFAULT NULL COMMENT ''会签组显示名'' AFTER group_key');
|
||||
CALL approval_phase2_add_column('approval_task', 'group_seq',
|
||||
'group_seq INT DEFAULT NULL COMMENT ''会签组展示顺序'' AFTER group_name');
|
||||
|
||||
-- 列表标题与检索文本:由单据实现计算落库,列表/检索不再按 doc_type 分支(ADR 0007)
|
||||
CALL approval_phase2_add_column('approval_instance', 'title',
|
||||
'title VARCHAR(200) DEFAULT NULL COMMENT ''列表显示标题'' AFTER biz_id');
|
||||
CALL approval_phase2_add_column('approval_instance', 'search_text',
|
||||
'search_text VARCHAR(500) DEFAULT NULL COMMENT ''关键词检索文本'' AFTER title');
|
||||
|
||||
-- 回填唯一性检索用:同类单据按 flow_code 过滤
|
||||
CALL approval_phase2_add_index('approval_instance', 'idx_flow',
|
||||
'KEY idx_flow (tenant_id, flow_code, status)');
|
||||
|
||||
-- 字段改值历史(ADR 0006)
|
||||
CREATE TABLE IF NOT EXISTS approval_field_change (
|
||||
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,
|
||||
field_key VARCHAR(40) NOT NULL,
|
||||
field_label VARCHAR(100) DEFAULT NULL,
|
||||
old_value VARCHAR(500) DEFAULT NULL,
|
||||
new_value VARCHAR(500) DEFAULT NULL,
|
||||
operator_id INT NOT NULL,
|
||||
operator_name VARCHAR(100) DEFAULT NULL,
|
||||
round INT NOT NULL,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_instance (instance_id, create_time),
|
||||
KEY idx_field (tenant_id, field_key, new_value)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批字段改值历史';
|
||||
|
||||
-- 老数据回填:一期实例没有 title/search_text(幂等,只补空值)
|
||||
UPDATE approval_instance i
|
||||
JOIN approval_completion_form f ON f.instance_id = i.id
|
||||
SET i.title = f.name,
|
||||
i.search_text = CONCAT_WS(' ', f.name, f.implement_org, f.service_content)
|
||||
WHERE i.title IS NULL;
|
||||
|
||||
UPDATE approval_instance i
|
||||
JOIN approval_coordination_form c ON c.instance_id = i.id
|
||||
SET i.title = c.title,
|
||||
i.search_text = CONCAT_WS(' ', c.title, c.content, c.remark)
|
||||
WHERE i.title IS NULL;
|
||||
|
||||
DROP PROCEDURE IF EXISTS approval_phase2_add_column;
|
||||
DROP PROCEDURE IF EXISTS approval_phase2_add_index;
|
||||
@@ -44,6 +44,7 @@ public final class ApprovalConstants {
|
||||
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_BACKFILL = "BACKFILL";
|
||||
public static final String ACTION_REJECT = "REJECT";
|
||||
public static final String ACTION_WITHDRAW = "WITHDRAW";
|
||||
public static final String ACTION_DISPATCH = "DISPATCH";
|
||||
@@ -56,13 +57,27 @@ public final class ApprovalConstants {
|
||||
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 FLOW_COORDINATION_EXTERNAL = "coordination_external";
|
||||
public static final String FLOW_SETTLEMENT = "settlement";
|
||||
public static final String FLOW_RECEIPT = "receipt";
|
||||
public static final String FLOW_VEHICLE = "vehicle";
|
||||
public static final String FLOW_VENUE = "venue";
|
||||
public static final String FLOW_COUPON = "coupon";
|
||||
public static final String FLOW_FACILITY = "facility";
|
||||
|
||||
// 单号前缀
|
||||
public static final String NO_PREFIX_COMPLETION = "WG";
|
||||
public static final String NO_PREFIX_COORDINATION = "XT";
|
||||
public static final String NO_PREFIX_SETTLEMENT = "JS";
|
||||
public static final String NO_PREFIX_RECEIPT = "SJ";
|
||||
public static final String NO_PREFIX_VEHICLE = "CL";
|
||||
public static final String NO_PREFIX_VENUE = "CD";
|
||||
public static final String NO_PREFIX_COUPON = "TY";
|
||||
public static final String NO_PREFIX_FACILITY = "JG";
|
||||
|
||||
// 附件分类
|
||||
public static final String CATEGORY_SIGNED = "SIGNED";
|
||||
public static final String CATEGORY_VOUCHER = "VOUCHER";
|
||||
public static final String CATEGORY_OTHER = "OTHER";
|
||||
|
||||
// 角色编码
|
||||
@@ -73,6 +88,16 @@ public final class ApprovalConstants {
|
||||
/** 部门岗位角色前缀(dept_leader_<orgCode> / dept_deputy_<orgCode>)。 */
|
||||
public static final String ROLE_DEPT_LEADER_PREFIX = "dept_leader_";
|
||||
public static final String ROLE_DEPT_DEPUTY_PREFIX = "dept_deputy_";
|
||||
/** 租户岗位角色:出纳。 */
|
||||
public static final String ROLE_CASHIER = "approval_cashier";
|
||||
/** 租户岗位角色:录入员(运营管理部指定专人)。 */
|
||||
public static final String ROLE_TYPIST = "approval_typist";
|
||||
/** 租户岗位角色:发放人。 */
|
||||
public static final String ROLE_ISSUER = "approval_issuer";
|
||||
/** 租户岗位角色:总经理 / 主要领导 / 公司领导。 */
|
||||
public static final String ROLE_GENERAL_MANAGER = "approval_general_manager";
|
||||
/** 租户岗位角色:体验券「相关部门」固定角色(待客户确认,见 ADR 0013)。 */
|
||||
public static final String ROLE_RELATED_DEPT = "approval_related_dept";
|
||||
|
||||
// 权限串(与 spec 约定一致;菜单/按钮共用)
|
||||
public static final String PERMISSION_LIST = "approval:instance:list";
|
||||
@@ -81,6 +106,31 @@ public final class ApprovalConstants {
|
||||
public static final String PERMISSION_ARCHIVE = "approval:instance:archive";
|
||||
public static final String PERMISSION_ASSIGN = "approval:instance:assign";
|
||||
public static final String PERMISSION_EXPORT = "approval:instance:export";
|
||||
/** 价格表查看:受众全体员工,与「看审批单据」的列表权限分开(ADR 0008)。 */
|
||||
public static final String PERMISSION_PRICE_VIEW = "approval:price:view";
|
||||
|
||||
/** 明细行上限(ADR 0004)。 */
|
||||
public static final int MAX_LINE_COUNT = 100;
|
||||
/** 金额上限,沿用一期 DECIMAL(12,2)。 */
|
||||
public static final java.math.BigDecimal MAX_FEE = new java.math.BigDecimal("99999999.99");
|
||||
|
||||
/** 审批人解析器标识。 */
|
||||
public static final String RESOLVER_REPORTER_ORG_LEADER = "reporterOrgLeader";
|
||||
public static final String RESOLVER_REPORTER_ORG_DEPUTY = "reporterOrgDeputy";
|
||||
public static final String RESOLVER_ACCEPT_ORG_LEADER = "acceptOrgLeader";
|
||||
public static final String RESOLVER_ACCEPT_ORG_DEPUTY = "acceptOrgDeputy";
|
||||
public static final String RESOLVER_ARCHIVIST = "archivist";
|
||||
public static final String RESOLVER_REPORTER = "reporter";
|
||||
public static final String RESOLVER_REPORTER_AND_DISPATCH_TARGET = "reporterAndDispatchTarget";
|
||||
public static final String RESOLVER_ACCEPT_ORG_MEMBERS = "acceptOrgMembers";
|
||||
public static final String RESOLVER_ORG_LEADER_BY_CODE = "orgLeaderByCode";
|
||||
public static final String RESOLVER_ORG_DEPUTY_BY_CODE = "orgDeputyByCode";
|
||||
public static final String RESOLVER_ROLE_CASHIER = "roleCashier";
|
||||
public static final String RESOLVER_ROLE_TYPIST = "roleTypist";
|
||||
public static final String RESOLVER_ROLE_ISSUER = "roleIssuer";
|
||||
public static final String RESOLVER_ROLE_GENERAL_MANAGER = "roleGeneralManager";
|
||||
public static final String RESOLVER_ROLE_RELATED_DEPT = "roleRelatedDept";
|
||||
public static final String RESOLVER_ROLE_ARCHIVIST = "roleArchivist";
|
||||
|
||||
// 附件规则
|
||||
public static final long MAX_ATTACHMENT_SIZE = 20L * 1024 * 1024;
|
||||
@@ -123,13 +173,14 @@ public final class ApprovalConstants {
|
||||
|
||||
Map<String, String> docType = new LinkedHashMap<>();
|
||||
docType.put(DOC_COMPLETION, "业务完成确认单");
|
||||
docType.put(DOC_COORDINATION, "内部协调单");
|
||||
docType.put(DOC_COORDINATION, "工作协调单");
|
||||
DOC_TYPE_NAMES = Collections.unmodifiableMap(docType);
|
||||
|
||||
Map<String, String> action = new LinkedHashMap<>();
|
||||
action.put(ACTION_SUBMIT, "提交");
|
||||
action.put(ACTION_RESUBMIT, "重新提交");
|
||||
action.put(ACTION_APPROVE, "通过");
|
||||
action.put(ACTION_BACKFILL, "回填");
|
||||
action.put(ACTION_REJECT, "驳回");
|
||||
action.put(ACTION_WITHDRAW, "撤回");
|
||||
action.put(ACTION_DISPATCH, "派单");
|
||||
@@ -140,10 +191,15 @@ public final class ApprovalConstants {
|
||||
|
||||
Map<String, String> category = new LinkedHashMap<>();
|
||||
category.put(CATEGORY_SIGNED, "签字件");
|
||||
category.put(CATEGORY_VOUCHER, "凭证");
|
||||
category.put(CATEGORY_OTHER, "其他");
|
||||
CATEGORY_NAMES = Collections.unmodifiableMap(category);
|
||||
}
|
||||
|
||||
/** 附件分类白名单(上传与提交预检共用)。 */
|
||||
public static final List<String> ATTACHMENT_CATEGORIES =
|
||||
Collections.unmodifiableList(Arrays.asList(CATEGORY_SIGNED, CATEGORY_VOUCHER, CATEGORY_OTHER));
|
||||
|
||||
/** 业务完成确认单流程定义(version=1)。 */
|
||||
public static final String COMPLETION_NODES_JSON = "["
|
||||
+ "{\"key\":\"deptLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgLeader\",\"name\":\"报送部门负责人\"},"
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.gxwebsoft.common.system.entity.FileRecord;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
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;
|
||||
@@ -31,10 +32,20 @@ public class ApprovalExportController extends BaseController {
|
||||
|
||||
@PreAuthorize("hasAuthority('approval:instance:export')")
|
||||
@OperationLog(module = "审批管理", comments = "导出单据查询结果")
|
||||
@ApiOperation("导出单据")
|
||||
@ApiOperation("导出单据总表")
|
||||
@PostMapping("/export")
|
||||
public ApiResult<FileRecord> export(@RequestBody ApprovalQueryParam param) throws IOException {
|
||||
ApprovalExportResult result = exportService.export(param, getLoginUser());
|
||||
return success("导出成功,共" + result.getRowCount() + "条", result.getFile());
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('approval:instance:export')")
|
||||
@OperationLog(module = "审批管理", comments = "导出单据明细")
|
||||
@ApiOperation("按单据类型导出明细(含逐行数据)")
|
||||
@PostMapping("/export/{flowCode}/lines")
|
||||
public ApiResult<FileRecord> exportLines(@PathVariable("flowCode") String flowCode,
|
||||
@RequestBody ApprovalQueryParam param) throws IOException {
|
||||
ApprovalExportResult result = exportService.exportLines(flowCode, param, getLoginUser());
|
||||
return success("导出成功,共" + result.getRowCount() + "条明细", result.getFile());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.gxwebsoft.approval.controller;
|
||||
|
||||
import com.gxwebsoft.approval.param.ApprovalCompletionParam;
|
||||
import com.gxwebsoft.approval.param.ApprovalCoordinationParam;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentRegistry;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentType;
|
||||
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.exception.BusinessException;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
@@ -21,9 +25,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 审批实例接口(员工端与 PC 共用)。
|
||||
*
|
||||
* <p>草稿保存按 flow_code 路由到单据实现;新增单据只加一个 {@link ApprovalDocumentType},
|
||||
* 不再加 Controller 方法。</p>
|
||||
*/
|
||||
@Api(tags = "审批实例")
|
||||
@RestController
|
||||
@@ -32,21 +40,46 @@ public class ApprovalInstanceController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private ApprovalInstanceService instanceService;
|
||||
@Resource
|
||||
private ApprovalDocumentRegistry documentRegistry;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 新建或保存任意单据草稿,返回实例 id。
|
||||
*
|
||||
* <p>body 为目标单据的 Param JSON(含 id 表示更新草稿)。</p>
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('approval:instance:submit')")
|
||||
@ApiOperation("新建或保存业务完成确认单草稿")
|
||||
@PostMapping("/instance/completion")
|
||||
public ApiResult<Long> saveCompletion(@RequestBody ApprovalCompletionParam param) {
|
||||
@ApiOperation("新建或保存单据草稿")
|
||||
@PostMapping("/instance/{flowCode}")
|
||||
public ApiResult<Long> save(@PathVariable("flowCode") String flowCode,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
User user = getLoginUser();
|
||||
return success("保存成功", instanceService.saveCompletion(param, user));
|
||||
ApprovalDocumentType type = documentRegistry.require(flowCode);
|
||||
ApprovalDocumentParam param = objectMapper.convertValue(body, type.paramClass());
|
||||
if (param == null) {
|
||||
throw new BusinessException("参数不能为空");
|
||||
}
|
||||
return success("保存成功", instanceService.save(flowCode, param, user));
|
||||
}
|
||||
|
||||
/** 一期端点保留:等价于 POST /instance/completion。 */
|
||||
@PreAuthorize("hasAuthority('approval:instance:submit')")
|
||||
@ApiOperation("新建或保存内部协调单草稿")
|
||||
@ApiOperation("新建或保存业务完成确认单草稿(一期)")
|
||||
@PostMapping("/instance/completion")
|
||||
public ApiResult<Long> saveCompletion(@RequestBody com.gxwebsoft.approval.param.ApprovalCompletionParam param) {
|
||||
return success("保存成功", instanceService.save(ApprovalConstants.FLOW_COMPLETION, param, getLoginUser()));
|
||||
}
|
||||
|
||||
/** 一期端点保留:等价于 POST /instance/coordination_internal。 */
|
||||
@PreAuthorize("hasAuthority('approval:instance:submit')")
|
||||
@ApiOperation("新建或保存内部协调单草稿(一期)")
|
||||
@PostMapping("/instance/coordination")
|
||||
public ApiResult<Long> saveCoordination(@RequestBody ApprovalCoordinationParam param) {
|
||||
User user = getLoginUser();
|
||||
return success("保存成功", instanceService.saveCoordination(param, user));
|
||||
public ApiResult<Long> saveCoordination(
|
||||
@RequestBody com.gxwebsoft.approval.param.ApprovalCoordinationParam param) {
|
||||
return success("保存成功",
|
||||
instanceService.save(ApprovalConstants.FLOW_COORDINATION_INTERNAL, param, getLoginUser()));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('approval:instance:submit')")
|
||||
|
||||
@@ -68,11 +68,23 @@ public class ApprovalTaskController extends BaseController {
|
||||
public ApiResult<ApprovalInstanceVO> approve(@PathVariable("id") Long id,
|
||||
@RequestBody(required = false) ApprovalTaskActionParam param) {
|
||||
User user = getLoginUser();
|
||||
ApprovalTaskService.Result result = taskService.approve(id, param == null ? null : param.getOpinion(), user);
|
||||
ApprovalTaskService.Result result = taskService.approve(id,
|
||||
param == null ? null : param.getOpinion(),
|
||||
param == null ? null : param.getFields(), user);
|
||||
return success(result.getWarning() == null ? "审批成功" : result.getWarning(),
|
||||
instanceService.detail(result.getInstanceId(), user));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('approval:instance:approve')")
|
||||
@ApiOperation("回填(只写字段,不推进节点)")
|
||||
@PostMapping("/task/{id}/backfill")
|
||||
public ApiResult<ApprovalInstanceVO> backfill(@PathVariable("id") Long id,
|
||||
@RequestBody(required = false) ApprovalTaskActionParam param) {
|
||||
User user = getLoginUser();
|
||||
ApprovalTaskService.Result result = taskService.backfill(id, param == null ? null : param.getFields(), user);
|
||||
return success("回填成功", instanceService.detail(result.getInstanceId(), user));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('approval:instance:approve')")
|
||||
@ApiOperation("驳回")
|
||||
@PostMapping("/task/{id}/reject")
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.gxwebsoft.approval.doc.completion;
|
||||
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.document.AbstractApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import com.gxwebsoft.approval.entity.ApprovalCompletionForm;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalCompletionFormMapper;
|
||||
import com.gxwebsoft.approval.param.ApprovalCompletionParam;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业务完成确认单(一期):报送部门负责人 → 报送人存档 → 归档。
|
||||
*/
|
||||
@Component
|
||||
public class CompletionDocumentType extends AbstractApprovalDocumentType {
|
||||
|
||||
@Resource
|
||||
private ApprovalCompletionFormMapper completionFormMapper;
|
||||
|
||||
private static final String NODES_JSON = "["
|
||||
+ "{\"key\":\"deptLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgLeader\",\"name\":\"报送部门负责人\"},"
|
||||
+ "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"resolver\":\"reporter\",\"name\":\"存档\"},"
|
||||
+ "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"resolver\":\"archivist\",\"name\":\"归档\"}"
|
||||
+ "]";
|
||||
|
||||
@Override
|
||||
public String flowCode() {
|
||||
return ApprovalConstants.FLOW_COMPLETION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String docType() {
|
||||
return ApprovalConstants.DOC_COMPLETION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String flowName() {
|
||||
return "业务完成确认单";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nodesJson() {
|
||||
return NODES_JSON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String noPrefix() {
|
||||
return ApprovalConstants.NO_PREFIX_COMPLETION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ApprovalDocumentParam> paramClass() {
|
||||
return ApprovalCompletionParam.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(ApprovalDocumentParam param, User user) {
|
||||
ApprovalCompletionParam completion = cast(param);
|
||||
requireText("名称", completion.getName(), 100);
|
||||
requireText("实施单位", completion.getImplementOrg(), 100);
|
||||
BigDecimal fee = completion.getFee();
|
||||
if (fee == null) {
|
||||
throw new BusinessException("费用不能为空");
|
||||
}
|
||||
if (fee.scale() > 2 || fee.compareTo(BigDecimal.ZERO) < 0
|
||||
|| fee.compareTo(ApprovalConstants.MAX_FEE) > 0) {
|
||||
throw new BusinessException("费用必须为0到99999999.99之间的两位小数");
|
||||
}
|
||||
requireText("服务内容", completion.getServiceContent(), 2000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
|
||||
ApprovalCompletionParam completion = cast(param);
|
||||
ApprovalCompletionForm form = bizId == null ? null : completionFormMapper.selectById(bizId);
|
||||
if (form == null) {
|
||||
form = new ApprovalCompletionForm();
|
||||
form.setInstanceId(instanceId);
|
||||
}
|
||||
form.setName(completion.getName().trim());
|
||||
form.setImplementOrg(completion.getImplementOrg().trim());
|
||||
form.setFee(completion.getFee());
|
||||
form.setServiceContent(completion.getServiceContent().trim());
|
||||
if (form.getId() == null) {
|
||||
completionFormMapper.insert(form);
|
||||
} else {
|
||||
completionFormMapper.updateById(form);
|
||||
}
|
||||
return form.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(ApprovalInstance instance, ApprovalDetail detail) {
|
||||
ApprovalCompletionForm form = findByInstance(instance.getId());
|
||||
if (form == null) {
|
||||
return;
|
||||
}
|
||||
detail.addField("name", "名称", form.getName());
|
||||
detail.addField("implementOrg", "实施单位", form.getImplementOrg());
|
||||
detail.addField("fee", "费用(元)", form.getFee());
|
||||
detail.addField("serviceContent", "服务内容", form.getServiceContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title(ApprovalInstance instance) {
|
||||
ApprovalCompletionForm form = findByInstance(instance.getId());
|
||||
return form == null ? null : form.getName();
|
||||
}
|
||||
|
||||
private ApprovalCompletionForm findByInstance(Long instanceId) {
|
||||
List<ApprovalCompletionForm> forms = completionFormMapper.selectList(
|
||||
new LambdaQueryWrapper<ApprovalCompletionForm>()
|
||||
.eq(ApprovalCompletionForm::getInstanceId, instanceId)
|
||||
.last("LIMIT 1"));
|
||||
return forms.isEmpty() ? null : forms.get(0);
|
||||
}
|
||||
|
||||
private ApprovalCompletionParam cast(ApprovalDocumentParam param) {
|
||||
if (!(param instanceof ApprovalCompletionParam)) {
|
||||
throw new BusinessException("单据类型不匹配");
|
||||
}
|
||||
return (ApprovalCompletionParam) param;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.gxwebsoft.approval.doc.coordination;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.document.AbstractApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import com.gxwebsoft.approval.entity.ApprovalCoordinationForm;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalCoordinationFormMapper;
|
||||
import com.gxwebsoft.approval.param.ApprovalCoordinationParam;
|
||||
import com.gxwebsoft.approval.service.ApprovalApproverResolver;
|
||||
import com.gxwebsoft.approval.service.ApprovalDirectoryService;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工作协调单(内部流程,一期):报送部门负责人 → 受理部门负责人 → 派单 →
|
||||
* 存档(报送人 + 被派单人)→ 归档。
|
||||
*
|
||||
* <p>外部流程见 {@link CoordinationExternalDocumentType}:同一张表、同一份表单,
|
||||
* 只多一个受理部门分管领导节点(ADR 0007)。</p>
|
||||
*/
|
||||
@Component
|
||||
public class CoordinationDocumentType extends AbstractApprovalDocumentType {
|
||||
|
||||
static final String NODES_JSON_INTERNAL = "["
|
||||
+ "{\"key\":\"reportLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgLeader\",\"name\":\"报送部门负责人\"},"
|
||||
+ "{\"key\":\"acceptLeader\",\"type\":\"APPROVE\",\"resolver\":\"acceptOrgLeader\",\"name\":\"受理部门负责人\"},"
|
||||
+ "{\"key\":\"dispatch\",\"type\":\"DISPATCH\",\"resolver\":\"acceptOrgLeader\",\"name\":\"派单\"},"
|
||||
+ "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"resolver\":\"reporterAndDispatchTarget\",\"name\":\"存档\"},"
|
||||
+ "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"resolver\":\"archivist\",\"name\":\"归档\"}"
|
||||
+ "]";
|
||||
|
||||
@Resource
|
||||
private ApprovalCoordinationFormMapper coordinationFormMapper;
|
||||
@Resource
|
||||
private ApprovalDirectoryService directoryService;
|
||||
@Resource
|
||||
private ApprovalApproverResolver approverResolver;
|
||||
|
||||
@Override
|
||||
public String flowCode() {
|
||||
return ApprovalConstants.FLOW_COORDINATION_INTERNAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String docType() {
|
||||
return ApprovalConstants.DOC_COORDINATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String flowName() {
|
||||
return "内部协调单";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nodesJson() {
|
||||
return NODES_JSON_INTERNAL;
|
||||
}
|
||||
|
||||
/** 一期 v1 按部门全员派单、v2 改为受理部门负责人;版本不可变。 */
|
||||
@Override
|
||||
public int latestVersion() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String noPrefix() {
|
||||
return ApprovalConstants.NO_PREFIX_COORDINATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ApprovalDocumentParam> paramClass() {
|
||||
return ApprovalCoordinationParam.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(ApprovalDocumentParam param, User user) {
|
||||
ApprovalCoordinationParam coordination = cast(param);
|
||||
if (coordination.getAcceptOrgId() == null || coordination.getAcceptOrgId() <= 0) {
|
||||
throw new BusinessException("请选择受理部门");
|
||||
}
|
||||
if (directoryService.getOrganization(coordination.getAcceptOrgId()) == null) {
|
||||
throw new BusinessException("受理部门不存在或已停用");
|
||||
}
|
||||
requireText("标题", coordination.getTitle(), 100);
|
||||
requireText("内容", coordination.getContent(), 2000);
|
||||
optionalText("备注", coordination.getRemark(), 500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
|
||||
ApprovalCoordinationParam coordination = cast(param);
|
||||
ApprovalOrganizationVO accept = directoryService.getOrganization(coordination.getAcceptOrgId());
|
||||
if (accept == null) {
|
||||
throw new BusinessException("受理部门不存在或已停用");
|
||||
}
|
||||
ApprovalCoordinationForm form = bizId == null ? null : coordinationFormMapper.selectById(bizId);
|
||||
if (form == null) {
|
||||
form = new ApprovalCoordinationForm();
|
||||
form.setInstanceId(instanceId);
|
||||
}
|
||||
form.setAcceptOrgId(accept.getOrganizationId());
|
||||
form.setAcceptOrgName(accept.getOrganizationName());
|
||||
form.setTitle(coordination.getTitle().trim());
|
||||
form.setContent(coordination.getContent().trim());
|
||||
form.setRemark(optionalText("备注", coordination.getRemark(), 500));
|
||||
if (form.getId() == null) {
|
||||
coordinationFormMapper.insert(form);
|
||||
} else {
|
||||
coordinationFormMapper.updateById(form);
|
||||
}
|
||||
return form.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(ApprovalInstance instance, ApprovalDetail detail) {
|
||||
ApprovalCoordinationForm form = findByInstance(instance.getId());
|
||||
if (form == null) {
|
||||
return;
|
||||
}
|
||||
detail.addField("acceptOrgName", "受理部门", form.getAcceptOrgName());
|
||||
detail.addField("title", "标题", form.getTitle());
|
||||
detail.addField("content", "协调内容", form.getContent());
|
||||
detail.addField("remark", "备注", form.getRemark());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title(ApprovalInstance instance) {
|
||||
ApprovalCoordinationForm form = findByInstance(instance.getId());
|
||||
return form == null ? null : form.getTitle();
|
||||
}
|
||||
|
||||
private ApprovalCoordinationForm findByInstance(Long instanceId) {
|
||||
List<ApprovalCoordinationForm> forms = coordinationFormMapper.selectList(
|
||||
new LambdaQueryWrapper<ApprovalCoordinationForm>()
|
||||
.eq(ApprovalCoordinationForm::getInstanceId, instanceId)
|
||||
.last("LIMIT 1"));
|
||||
return forms.isEmpty() ? null : forms.get(0);
|
||||
}
|
||||
|
||||
private ApprovalCoordinationParam cast(ApprovalDocumentParam param) {
|
||||
if (!(param instanceof ApprovalCoordinationParam)) {
|
||||
throw new BusinessException("单据类型不匹配");
|
||||
}
|
||||
return (ApprovalCoordinationParam) param;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.gxwebsoft.approval.doc.coordination;
|
||||
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 工作协调单(外部流程):节点链在受理部门负责人之后多一个受理部门分管领导,
|
||||
* 其余与内部流程完全一致;共用同一张业务表、同一套表单(ADR 0007)。
|
||||
*/
|
||||
@Component
|
||||
public class CoordinationExternalDocumentType extends CoordinationDocumentType {
|
||||
|
||||
private static final String NODES_JSON_EXTERNAL = "["
|
||||
+ "{\"key\":\"reportLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgLeader\",\"name\":\"报送部门负责人\"},"
|
||||
+ "{\"key\":\"acceptLeader\",\"type\":\"APPROVE\",\"resolver\":\"acceptOrgLeader\",\"name\":\"受理部门负责人\"},"
|
||||
+ "{\"key\":\"acceptDeputy\",\"type\":\"APPROVE\",\"resolver\":\"acceptOrgDeputy\",\"name\":\"分管领导\"},"
|
||||
+ "{\"key\":\"dispatch\",\"type\":\"DISPATCH\",\"resolver\":\"acceptOrgLeader\",\"name\":\"派单\"},"
|
||||
+ "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"resolver\":\"reporterAndDispatchTarget\",\"name\":\"存档\"},"
|
||||
+ "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"resolver\":\"archivist\",\"name\":\"归档\"}"
|
||||
+ "]";
|
||||
|
||||
@Override
|
||||
public String flowCode() {
|
||||
return ApprovalConstants.FLOW_COORDINATION_EXTERNAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String flowName() {
|
||||
return "外部协调单";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nodesJson() {
|
||||
return NODES_JSON_EXTERNAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int latestVersion() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(ApprovalInstance instance, ApprovalDetail detail) {
|
||||
super.load(instance, detail);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.gxwebsoft.approval.document;
|
||||
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.support.ApprovalFieldSpec;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 单据 SPI 的空实现基类:只覆盖本单需要的钩子。
|
||||
*/
|
||||
public abstract class AbstractApprovalDocumentType implements ApprovalDocumentType {
|
||||
|
||||
@Override
|
||||
public List<ApprovalFieldSpec> fieldSpecs() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String requiredAttachmentCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(ApprovalInstance instance, ApprovalDetail detail) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String searchText(ApprovalInstance instance) {
|
||||
return title(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApprovalFieldUpdate> applyWritableFields(ApprovalInstance instance, Map<String, String> fields,
|
||||
User user, boolean requireComplete) {
|
||||
if (fields != null && !fields.isEmpty()) {
|
||||
throw new BusinessException("当前节点不支持回填");
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/** 必填文本校验,返回去掉首尾空格的值。 */
|
||||
protected String requireText(String label, String value, int maxLength) {
|
||||
String trimmed = value == null ? "" : value.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
throw new BusinessException(label + "不能为空");
|
||||
}
|
||||
if (trimmed.length() > maxLength) {
|
||||
throw new BusinessException(label + "不能超过" + maxLength + "字");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/** 选填文本:空白归一为 null。 */
|
||||
protected String optionalText(String label, String value, int maxLength) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed.length() > maxLength) {
|
||||
throw new BusinessException(label + "不能超过" + maxLength + "字");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.gxwebsoft.approval.document;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 单据草稿参数基类:`id` 为审批实例id,更新草稿时传入。
|
||||
*/
|
||||
@Data
|
||||
public abstract class ApprovalDocumentParam implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("审批实例id(更新草稿时传入)")
|
||||
private Long id;
|
||||
|
||||
/** 明细行(明细类单据使用):每行以列 key 为键;无明细单据忽略。 */
|
||||
private java.util.List<java.util.Map<String, Object>> lines;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.gxwebsoft.approval.document;
|
||||
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 单据类型注册表:按 flow_code / doc_type 找到实现。
|
||||
*
|
||||
* <p>Spring 启动时收集全部 {@link ApprovalDocumentType};同一 flow_code 重复注册直接失败,
|
||||
* 避免「改了实现却没生效」的静默错误。</p>
|
||||
*/
|
||||
@Component
|
||||
public class ApprovalDocumentRegistry {
|
||||
|
||||
private final Map<String, ApprovalDocumentType> byFlowCode = new LinkedHashMap<>();
|
||||
private final Map<String, ApprovalDocumentType> byDocType = new LinkedHashMap<>();
|
||||
|
||||
public ApprovalDocumentRegistry(List<ApprovalDocumentType> documentTypes) {
|
||||
for (ApprovalDocumentType type : documentTypes) {
|
||||
ApprovalDocumentType previous = byFlowCode.put(type.flowCode(), type);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("审批流程编码重复注册: " + type.flowCode());
|
||||
}
|
||||
// doc_type 可被多个流程共用(内部/外部协调单),以第一个注册的为准
|
||||
byDocType.putIfAbsent(type.docType(), type);
|
||||
}
|
||||
}
|
||||
|
||||
/** 按流程编码取实现;未知流程抛业务异常。 */
|
||||
public ApprovalDocumentType require(String flowCode) {
|
||||
ApprovalDocumentType type = byFlowCode.get(flowCode);
|
||||
if (type == null) {
|
||||
throw new BusinessException("未知的审批流程: " + flowCode);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public ApprovalDocumentType find(String flowCode) {
|
||||
return flowCode == null ? null : byFlowCode.get(flowCode);
|
||||
}
|
||||
|
||||
public ApprovalDocumentType findByDocType(String docType) {
|
||||
return docType == null ? null : byDocType.get(docType);
|
||||
}
|
||||
|
||||
public List<ApprovalDocumentType> all() {
|
||||
return Collections.unmodifiableList(new ArrayList<>(byFlowCode.values()));
|
||||
}
|
||||
|
||||
/** 明细行数;无实现返回 0。 */
|
||||
public int lineCount(ApprovalDocumentType type, com.gxwebsoft.approval.entity.ApprovalInstance instance) {
|
||||
return type == null ? 0 : type.lineCount(instance);
|
||||
}
|
||||
|
||||
/** 明细合计金额;无实现返回 null。 */
|
||||
public java.math.BigDecimal lineAmountTotal(ApprovalDocumentType type,
|
||||
com.gxwebsoft.approval.entity.ApprovalInstance instance) {
|
||||
return type == null ? null : type.lineAmountTotal(instance);
|
||||
}
|
||||
|
||||
/** 列表/详情显示的流程名称;未知流程回退 doc_type 名称。 */
|
||||
public String flowName(String flowCode, String docType) {
|
||||
ApprovalDocumentType type = find(flowCode);
|
||||
if (type != null) {
|
||||
return type.flowName();
|
||||
}
|
||||
return ApprovalConstants.docTypeName(docType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.gxwebsoft.approval.document;
|
||||
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.support.ApprovalFieldSpec;
|
||||
import com.gxwebsoft.approval.support.ApprovalLineSpec;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 单据类型 SPI:每张单据一个实现,引擎按 flow_code 找到它。
|
||||
*
|
||||
* <p>引擎负责审批流转与公共留痕,表单字段的校验、读写、回填与展示由实现承担。
|
||||
* 新增单据 = 新增一个实现 + 一张 DDL + 一个前端表单页,不改引擎。</p>
|
||||
*/
|
||||
public interface ApprovalDocumentType {
|
||||
|
||||
/** 流程编码,如 settlement。 */
|
||||
String flowCode();
|
||||
|
||||
/** 业务表族(doc_type),外部协调单与内部协调单同为 coordination。 */
|
||||
String docType();
|
||||
|
||||
/** 流程名称,同时作为单据类型显示名(ADR 0007)。 */
|
||||
String flowName();
|
||||
|
||||
/** 节点定义 JSON。 */
|
||||
String nodesJson();
|
||||
|
||||
/**
|
||||
* 本实现的当前发布版本号:调整节点时必须递增并保留旧版本行(ADR 0001)。
|
||||
* 新增单据默认 1。
|
||||
*/
|
||||
default int latestVersion() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** 单号前缀,如 JS。 */
|
||||
String noPrefix();
|
||||
|
||||
/** 明细列元数据;无明细返回 null。列结构随表结构走,不进流程定义 JSON(ADR 0004)。 */
|
||||
default List<ApprovalLineSpec> lineColumns() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 可写字段元数据;无回填字段返回空。 */
|
||||
default List<ApprovalFieldSpec> fieldSpecs() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/** 提交预检要求的附件分类;null 沿用「整单至少一张附件」。 */
|
||||
default String requiredAttachmentCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 草稿参数类型。 */
|
||||
Class<? extends ApprovalDocumentParam> paramClass();
|
||||
|
||||
/** 字段校验,失败抛 BusinessException。 */
|
||||
void validate(ApprovalDocumentParam param, User user);
|
||||
|
||||
/**
|
||||
* 写业务表:新建时 bizId 为 null,更新时传入已有 bizId。
|
||||
*
|
||||
* @return 业务表主键
|
||||
*/
|
||||
Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user);
|
||||
|
||||
/** 把业务字段与明细写入详情。 */
|
||||
void load(ApprovalInstance instance, ApprovalDetail detail);
|
||||
|
||||
/** 明细行:每行以列 key 为键;无明细返回空列表。 */
|
||||
default List<Map<String, Object>> lines(ApprovalInstance instance) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/** 明细行数(无明细返回 0),供跨单据总表导出与详情汇总。 */
|
||||
default int lineCount(ApprovalInstance instance) {
|
||||
return lines(instance).size();
|
||||
}
|
||||
|
||||
/** 明细合计金额(无金额列返回 null),查询与导出时求和,不落库。 */
|
||||
default java.math.BigDecimal lineAmountTotal(ApprovalInstance instance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 读取一个业务字段的当前值(回填展示用)。 */
|
||||
default String readField(ApprovalInstance instance, String field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 列表显示标题。 */
|
||||
String title(ApprovalInstance instance);
|
||||
|
||||
/** 关键词检索文本(含车牌号、收据号等业务检索主语)。 */
|
||||
String searchText(ApprovalInstance instance);
|
||||
|
||||
/**
|
||||
* 回填节点声明的可写字段。
|
||||
*
|
||||
* @param requireComplete true 表示「通过」动作,必填必须齐全;false 表示「回填」,允许缺项
|
||||
* @return 本次产生的改值记录
|
||||
*/
|
||||
List<ApprovalFieldUpdate> applyWritableFields(ApprovalInstance instance, Map<String, String> fields,
|
||||
User user, boolean requireComplete);
|
||||
|
||||
/** 撤回/驳回时清空本单需清空的字段(业务表不随引擎回滚)。 */
|
||||
default void onRoundReset(ApprovalInstance instance, User user) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.gxwebsoft.approval.document;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单据实现产生的一条改值记录;引擎负责补齐实例、任务、节点、操作人与轮次后落库。
|
||||
*
|
||||
* <p>实现负责判断旧值/新值;与实体 `ApprovalFieldChange` 区分开,避免同名混淆。</p>
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalFieldUpdate {
|
||||
|
||||
private String fieldKey;
|
||||
|
||||
private String fieldLabel;
|
||||
|
||||
private String oldValue;
|
||||
|
||||
private String newValue;
|
||||
|
||||
public ApprovalFieldUpdate() {
|
||||
}
|
||||
|
||||
public ApprovalFieldUpdate(String fieldKey, String fieldLabel, String oldValue, String newValue) {
|
||||
this.fieldKey = fieldKey;
|
||||
this.fieldLabel = fieldLabel;
|
||||
this.oldValue = oldValue;
|
||||
this.newValue = newValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.gxwebsoft.approval.document;
|
||||
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.support.ApprovalLineSpec;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 明细行校验与金额重算(ADR 0004),供各单据实现类在 persist 前调用。
|
||||
*
|
||||
* <p>服务端一律重算 `amount = round(quantity × unitPrice, 2)`(HALF_UP);客户端值与
|
||||
* 重算值不一致即拒绝并指出行号,不静默改写。行号按提交顺序重排为 1..N。</p>
|
||||
*/
|
||||
public final class ApprovalLineSupport {
|
||||
|
||||
/** 金额列参与重算时使用的数量列与单价列键。 */
|
||||
public static final String KEY_QUANTITY = "quantity";
|
||||
public static final String KEY_UNIT_PRICE = "unitPrice";
|
||||
public static final String KEY_AMOUNT = "amount";
|
||||
public static final String KEY_LINE_NO = "lineNo";
|
||||
|
||||
private ApprovalLineSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并归一化明细行。
|
||||
*
|
||||
* @param rows 客户端提交的行(key → 原始值)
|
||||
* @param columns 列元数据
|
||||
* @return 行号已重排、金额已重算的规范行
|
||||
*/
|
||||
public static List<Map<String, Object>> normalize(List<Map<String, Object>> rows,
|
||||
List<ApprovalLineSpec> columns) {
|
||||
if (columns == null || columns.isEmpty()) {
|
||||
throw new BusinessException("该单据没有明细列");
|
||||
}
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
throw new BusinessException("请至少填写一行明细");
|
||||
}
|
||||
if (rows.size() > ApprovalConstants.MAX_LINE_COUNT) {
|
||||
throw new BusinessException("明细不能超过" + ApprovalConstants.MAX_LINE_COUNT + "行");
|
||||
}
|
||||
List<Map<String, Object>> normalized = new ArrayList<>(rows.size());
|
||||
boolean money = columns.stream().anyMatch(ApprovalLineSpec::isMoney);
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
int lineNo = index + 1;
|
||||
Map<String, Object> source = rows.get(index);
|
||||
if (source == null) {
|
||||
throw new BusinessException("第" + lineNo + "行明细为空");
|
||||
}
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put(KEY_LINE_NO, lineNo);
|
||||
for (ApprovalLineSpec column : columns) {
|
||||
Object raw = firstNonNull(source, column.getKey(), snake(column.getKey()));
|
||||
Object value = convert(column, raw, lineNo);
|
||||
if (column.isRequired() && isBlank(value)) {
|
||||
throw new BusinessException("第" + lineNo + "行" + column.getLabel() + "不能为空");
|
||||
}
|
||||
row.put(column.getKey(), value);
|
||||
}
|
||||
if (money) {
|
||||
recomputeAmount(row, lineNo);
|
||||
}
|
||||
normalized.add(row);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额重算:客户端提交的金额必须与 quantity × unitPrice 一致,否则拒绝并指出行号。
|
||||
*/
|
||||
private static void recomputeAmount(Map<String, Object> row, int lineNo) {
|
||||
BigDecimal quantity = decimal(row.get(KEY_QUANTITY));
|
||||
BigDecimal unitPrice = decimal(row.get(KEY_UNIT_PRICE));
|
||||
if (quantity == null || unitPrice == null) {
|
||||
throw new BusinessException("第" + lineNo + "行数量与单价不能为空");
|
||||
}
|
||||
BigDecimal amount = quantity.multiply(unitPrice).setScale(2, RoundingMode.HALF_UP);
|
||||
if (amount.compareTo(BigDecimal.ZERO) < 0 || amount.compareTo(ApprovalLineSpec.MAX_MONEY) > 0) {
|
||||
throw new BusinessException("第" + lineNo + "行金额必须为0到99999999.99之间");
|
||||
}
|
||||
Object clientAmount = row.get(KEY_AMOUNT);
|
||||
if (!isBlank(clientAmount)) {
|
||||
BigDecimal submitted = decimal(clientAmount);
|
||||
if (submitted == null || submitted.compareTo(amount) != 0) {
|
||||
throw new BusinessException("第" + lineNo + "行金额与数量×单价不一致,应为"
|
||||
+ amount.toPlainString());
|
||||
}
|
||||
}
|
||||
row.put(KEY_AMOUNT, amount);
|
||||
}
|
||||
|
||||
/** 合计金额:仅金额类单据有值。 */
|
||||
public static BigDecimal amountTotal(List<Map<String, Object>> rows) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
boolean hasAmount = false;
|
||||
for (Map<String, Object> row : rows) {
|
||||
BigDecimal amount = decimal(row.get(KEY_AMOUNT));
|
||||
if (amount != null) {
|
||||
hasAmount = true;
|
||||
total = total.add(amount);
|
||||
}
|
||||
}
|
||||
return hasAmount ? total.setScale(2, RoundingMode.HALF_UP) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 百分号金额区间校验:0~99999999.99 且最多两位小数。
|
||||
*/
|
||||
public static BigDecimal money(String label, BigDecimal value, boolean required) {
|
||||
if (value == null) {
|
||||
if (required) {
|
||||
throw new BusinessException(label + "不能为空");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (value.scale() > 2) {
|
||||
throw new BusinessException(label + "最多保留两位小数");
|
||||
}
|
||||
if (value.compareTo(BigDecimal.ZERO) < 0 || value.compareTo(ApprovalConstants.MAX_FEE) > 0) {
|
||||
throw new BusinessException(label + "必须为0到99999999.99之间");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Object convert(ApprovalLineSpec column, Object raw, int lineNo) {
|
||||
String type = column.getType() == null ? ApprovalLineSpec.TYPE_TEXT : column.getType();
|
||||
switch (type) {
|
||||
case ApprovalLineSpec.TYPE_TEXT:
|
||||
String text = raw == null ? null : String.valueOf(raw).trim();
|
||||
if (text != null && column.getMaxLength() != null && text.length() > column.getMaxLength()) {
|
||||
throw new BusinessException("第" + lineNo + "行" + column.getLabel()
|
||||
+ "不能超过" + column.getMaxLength() + "字");
|
||||
}
|
||||
return text == null || text.isEmpty() ? null : text;
|
||||
case ApprovalLineSpec.TYPE_DECIMAL:
|
||||
BigDecimal decimal = decimal(raw);
|
||||
if (decimal != null) {
|
||||
int scale = column.getScale() == null ? 2 : column.getScale();
|
||||
if (decimal.scale() > scale) {
|
||||
throw new BusinessException("第" + lineNo + "行" + column.getLabel()
|
||||
+ "最多保留" + scale + "位小数");
|
||||
}
|
||||
if (column.isMoney() || KEY_UNIT_PRICE.equals(column.getKey())
|
||||
|| KEY_AMOUNT.equals(column.getKey())) {
|
||||
if (decimal.compareTo(BigDecimal.ZERO) < 0
|
||||
|| decimal.compareTo(ApprovalLineSpec.MAX_MONEY) > 0) {
|
||||
throw new BusinessException("第" + lineNo + "行" + column.getLabel()
|
||||
+ "必须为0到99999999.99之间");
|
||||
}
|
||||
}
|
||||
}
|
||||
return decimal;
|
||||
case ApprovalLineSpec.TYPE_INT:
|
||||
Integer integer = integer(raw);
|
||||
return integer;
|
||||
case ApprovalLineSpec.TYPE_DATE:
|
||||
LocalDate date = date(raw);
|
||||
return date;
|
||||
case ApprovalLineSpec.TYPE_BOOL:
|
||||
return bool(raw, lineNo, column.getLabel());
|
||||
case ApprovalLineSpec.TYPE_ENUM:
|
||||
String code = raw == null ? null : String.valueOf(raw).trim();
|
||||
if (code != null && !code.isEmpty() && column.getOptions() != null
|
||||
&& column.getOptions().stream().noneMatch(option -> option.getValue().equals(code))) {
|
||||
throw new BusinessException("第" + lineNo + "行" + column.getLabel() + "取值不正确");
|
||||
}
|
||||
return code == null || code.isEmpty() ? null : code;
|
||||
default:
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean bool(Object raw, int lineNo, String label) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
if (raw instanceof Boolean) {
|
||||
return (Boolean) raw;
|
||||
}
|
||||
String text = String.valueOf(raw).trim();
|
||||
if ("1".equals(text) || "true".equalsIgnoreCase(text) || "是".equals(text)) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
if ("0".equals(text) || "false".equalsIgnoreCase(text) || "否".equals(text)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
throw new BusinessException("第" + lineNo + "行" + label + "取值不正确");
|
||||
}
|
||||
|
||||
private static Integer integer(Object raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
if (raw instanceof Number) {
|
||||
return ((Number) raw).intValue();
|
||||
}
|
||||
String text = String.valueOf(raw).trim();
|
||||
if (text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(text);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BusinessException("行内数量必须为整数");
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal decimal(Object raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
if (raw instanceof BigDecimal) {
|
||||
return (BigDecimal) raw;
|
||||
}
|
||||
if (raw instanceof Number) {
|
||||
return new BigDecimal(raw.toString());
|
||||
}
|
||||
String text = String.valueOf(raw).trim();
|
||||
if (text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(text);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BusinessException("行内数值格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalDate date(Object raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
if (raw instanceof LocalDate) {
|
||||
return (LocalDate) raw;
|
||||
}
|
||||
String text = String.valueOf(raw).trim();
|
||||
if (text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(text.length() > 10 ? text.substring(0, 10) : text);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new BusinessException("行内日期格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private static Object firstNonNull(Map<String, Object> source, String... keys) {
|
||||
for (String key : keys) {
|
||||
if (source.containsKey(key) && source.get(key) != null) {
|
||||
return source.get(key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 列 key 的蛇形兼容:前端偶尔传 line_no。 */
|
||||
private static String snake(String key) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (char c : key.toCharArray()) {
|
||||
if (Character.isUpperCase(c)) {
|
||||
builder.append('_').append(Character.toLowerCase(c));
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static boolean isBlank(Object value) {
|
||||
return value == null || (value instanceof String && ((String) value).trim().isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 审批字段改值历史(只增不改,ADR 0006)。
|
||||
*
|
||||
* <p>记录「谁在第几轮把哪个字段从什么值改成了什么值」;每次回填按字段写一行。</p>
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "审批字段改值历史")
|
||||
@TableName("approval_field_change")
|
||||
public class ApprovalFieldChange 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 String fieldKey;
|
||||
|
||||
@ApiModelProperty("字段显示名快照")
|
||||
private String fieldLabel;
|
||||
|
||||
private String oldValue;
|
||||
|
||||
private String newValue;
|
||||
|
||||
private Integer operatorId;
|
||||
|
||||
private String operatorName;
|
||||
|
||||
private Integer round;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -39,6 +39,12 @@ public class ApprovalInstance implements Serializable {
|
||||
@ApiModelProperty("业务表主键")
|
||||
private Long bizId;
|
||||
|
||||
@ApiModelProperty("列表显示标题(单据实现计算并落库)")
|
||||
private String title;
|
||||
|
||||
@ApiModelProperty("关键词检索文本(单据实现计算并落库)")
|
||||
private String searchText;
|
||||
|
||||
private String status;
|
||||
|
||||
@ApiModelProperty("重提轮次")
|
||||
|
||||
@@ -43,6 +43,15 @@ public class ApprovalTask implements Serializable {
|
||||
|
||||
private String opinion;
|
||||
|
||||
/** 会签组标识;非会签节点为空。 */
|
||||
private String groupKey;
|
||||
|
||||
/** 会签组显示名(冗余存储,详情与导出不必解析节点 JSON)。 */
|
||||
private String groupName;
|
||||
|
||||
/** 会签组展示顺序,从 1 开始;非会签节点为空。 */
|
||||
private Integer groupSeq;
|
||||
|
||||
private Integer dispatchTargetId;
|
||||
|
||||
private String dispatchTargetName;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.gxwebsoft.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFieldChange;
|
||||
|
||||
/**
|
||||
* 审批字段改值历史Mapper。
|
||||
*/
|
||||
public interface ApprovalFieldChangeMapper extends BaseMapper<ApprovalFieldChange> {
|
||||
}
|
||||
@@ -27,6 +27,11 @@ public interface ApprovalInstanceMapper extends BaseMapper<ApprovalInstance> {
|
||||
@Param("allVisible") Integer allVisible,
|
||||
@Param("assignVisible") Integer assignVisible);
|
||||
|
||||
/**
|
||||
* 协调单受理部门快照(提交预检用)。
|
||||
*/
|
||||
java.util.Map<String, Object> selectCoordinationAcceptOrg(@Param("instanceId") Long instanceId);
|
||||
|
||||
/**
|
||||
* 我发起的列表。
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
SELECT i.id,
|
||||
i.biz_no,
|
||||
i.doc_type,
|
||||
i.flow_code,
|
||||
i.status,
|
||||
i.reporter_name,
|
||||
i.report_org_name,
|
||||
@@ -17,11 +18,10 @@
|
||||
i.approve_time,
|
||||
i.archive_time,
|
||||
i.archivist_name,
|
||||
f.name,
|
||||
i.title,
|
||||
f.implement_org,
|
||||
f.fee,
|
||||
f.service_content,
|
||||
co.title,
|
||||
co.content,
|
||||
co.remark
|
||||
FROM approval_instance i
|
||||
@@ -40,6 +40,9 @@
|
||||
<if test="param.docType != null and param.docType != ''">
|
||||
AND i.doc_type = #{param.docType}
|
||||
</if>
|
||||
<if test="param.flowCode != null and param.flowCode != ''">
|
||||
AND i.flow_code = #{param.flowCode}
|
||||
</if>
|
||||
<if test="param.bizNo != null and param.bizNo != ''">
|
||||
AND i.biz_no LIKE CONCAT('%', #{param.bizNo}, '%')
|
||||
</if>
|
||||
@@ -57,6 +60,8 @@
|
||||
</if>
|
||||
<if test="param.keywords != null and param.keywords != ''">
|
||||
AND (i.biz_no LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.search_text LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.name LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR co.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.implement_org LIKE CONCAT('%', #{param.keywords}, '%'))
|
||||
|
||||
@@ -25,10 +25,18 @@
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="selectCoordinationAcceptOrg" resultType="java.util.Map">
|
||||
SELECT accept_org_id
|
||||
FROM approval_coordination_form
|
||||
WHERE instance_id = #{instanceId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectMinePage" resultType="com.gxwebsoft.approval.support.ApprovalItemRow">
|
||||
SELECT i.id,
|
||||
i.biz_no,
|
||||
i.doc_type,
|
||||
i.flow_code,
|
||||
i.status,
|
||||
i.reporter_id,
|
||||
i.reporter_name,
|
||||
@@ -38,7 +46,7 @@
|
||||
i.current_node_key,
|
||||
i.definition_id,
|
||||
i.submit_time,
|
||||
COALESCE(f.name, co.title) AS title
|
||||
i.title AS title
|
||||
FROM approval_instance i
|
||||
LEFT JOIN approval_completion_form f ON f.instance_id = i.id
|
||||
LEFT JOIN approval_coordination_form co ON co.instance_id = i.id
|
||||
@@ -49,10 +57,16 @@
|
||||
<if test="param.docType != null and param.docType != ''">
|
||||
AND i.doc_type = #{param.docType}
|
||||
</if>
|
||||
<if test="param.flowCode != null and param.flowCode != ''">
|
||||
AND i.flow_code = #{param.flowCode}
|
||||
</if>
|
||||
<if test="param.keywords != null and param.keywords != ''">
|
||||
AND (i.biz_no LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.search_text LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.name LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR co.title LIKE CONCAT('%', #{param.keywords}, '%'))
|
||||
OR co.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.implement_org LIKE CONCAT('%', #{param.keywords}, '%'))
|
||||
</if>
|
||||
ORDER BY (i.submit_time IS NULL) ASC, i.submit_time DESC, i.id DESC
|
||||
</select>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
SELECT i.id,
|
||||
i.biz_no,
|
||||
i.doc_type,
|
||||
i.flow_code,
|
||||
i.status,
|
||||
i.reporter_id,
|
||||
i.reporter_name,
|
||||
@@ -16,7 +17,7 @@
|
||||
i.current_node_key,
|
||||
i.definition_id,
|
||||
i.submit_time,
|
||||
COALESCE(f.name, co.title) AS title
|
||||
i.title AS title
|
||||
FROM approval_instance i
|
||||
LEFT JOIN approval_completion_form f ON f.instance_id = i.id
|
||||
LEFT JOIN approval_coordination_form co ON co.instance_id = i.id
|
||||
@@ -33,6 +34,9 @@
|
||||
<if test="param.docType != null and param.docType != ''">
|
||||
AND i.doc_type = #{param.docType}
|
||||
</if>
|
||||
<if test="param.flowCode != null and param.flowCode != ''">
|
||||
AND i.flow_code = #{param.flowCode}
|
||||
</if>
|
||||
<if test="param.bizNo != null and param.bizNo != ''">
|
||||
AND i.biz_no LIKE CONCAT('%', #{param.bizNo}, '%')
|
||||
</if>
|
||||
@@ -50,6 +54,8 @@
|
||||
</if>
|
||||
<if test="param.keywords != null and param.keywords != ''">
|
||||
AND (i.biz_no LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.search_text LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.name LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR co.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.implement_org LIKE CONCAT('%', #{param.keywords}, '%'))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
SELECT i.id,
|
||||
i.biz_no,
|
||||
i.doc_type,
|
||||
i.flow_code,
|
||||
i.status,
|
||||
i.reporter_id,
|
||||
i.reporter_name,
|
||||
@@ -15,7 +16,7 @@
|
||||
i.current_node_key,
|
||||
i.definition_id,
|
||||
i.submit_time,
|
||||
COALESCE(f.name, co.title) AS title
|
||||
i.title AS title
|
||||
FROM approval_instance i
|
||||
LEFT JOIN approval_completion_form f ON f.instance_id = i.id
|
||||
LEFT JOIN approval_coordination_form co ON co.instance_id = i.id
|
||||
@@ -31,8 +32,13 @@
|
||||
<if test="param.docType != null and param.docType != ''">
|
||||
AND i.doc_type = #{param.docType}
|
||||
</if>
|
||||
<if test="param.flowCode != null and param.flowCode != ''">
|
||||
AND i.flow_code = #{param.flowCode}
|
||||
</if>
|
||||
<if test="param.keywords != null and param.keywords != ''">
|
||||
AND (i.biz_no LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.search_text LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.name LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR co.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.implement_org LIKE CONCAT('%', #{param.keywords}, '%'))
|
||||
@@ -44,6 +50,7 @@
|
||||
SELECT i.id,
|
||||
i.biz_no,
|
||||
i.doc_type,
|
||||
i.flow_code,
|
||||
i.status,
|
||||
i.reporter_id,
|
||||
i.reporter_name,
|
||||
@@ -53,7 +60,7 @@
|
||||
i.current_node_key,
|
||||
i.definition_id,
|
||||
i.submit_time,
|
||||
COALESCE(f.name, co.title) AS title
|
||||
i.title AS title
|
||||
FROM approval_instance i
|
||||
LEFT JOIN approval_completion_form f ON f.instance_id = i.id
|
||||
LEFT JOIN approval_coordination_form co ON co.instance_id = i.id
|
||||
@@ -67,8 +74,13 @@
|
||||
<if test="param.docType != null and param.docType != ''">
|
||||
AND i.doc_type = #{param.docType}
|
||||
</if>
|
||||
<if test="param.flowCode != null and param.flowCode != ''">
|
||||
AND i.flow_code = #{param.flowCode}
|
||||
</if>
|
||||
<if test="param.keywords != null and param.keywords != ''">
|
||||
AND (i.biz_no LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR i.search_text LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.name LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR co.title LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
OR f.implement_org LIKE CONCAT('%', #{param.keywords}, '%'))
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
package com.gxwebsoft.approval.param;
|
||||
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 业务完成确认单草稿参数;id 为审批实例id,更新草稿时传入。
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalCompletionParam implements Serializable {
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ApprovalCompletionParam extends ApprovalDocumentParam {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("审批实例id(更新草稿时传入)")
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty("名称")
|
||||
private String name;
|
||||
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
package com.gxwebsoft.approval.param;
|
||||
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 内部协调单草稿参数;id 为审批实例id,更新草稿时传入。
|
||||
* 工作协调单草稿参数(内部/外部流程共用);id 为审批实例id,更新草稿时传入。
|
||||
*
|
||||
* <p>报送部门与报送人由服务端按登录态带出,不在参数中接收。</p>
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalCoordinationParam implements Serializable {
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ApprovalCoordinationParam extends ApprovalDocumentParam {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("审批实例id(更新草稿时传入)")
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty("受理部门id,必须是启用组织")
|
||||
private Integer acceptOrgId;
|
||||
|
||||
|
||||
@@ -17,4 +17,7 @@ public class ApprovalMineParam extends BaseParam {
|
||||
|
||||
@ApiModelProperty("单据类型")
|
||||
private String docType;
|
||||
|
||||
@ApiModelProperty("流程编码,如 settlement")
|
||||
private String flowCode;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ public class ApprovalQueryParam extends BaseParam {
|
||||
@ApiModelProperty("单据类型 completion/coordination")
|
||||
private String docType;
|
||||
|
||||
@ApiModelProperty("流程编码,如 coordination_external")
|
||||
private String flowCode;
|
||||
|
||||
@ApiModelProperty("单号")
|
||||
private String bizNo;
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@ import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 任务动作参数(通过/驳回)。
|
||||
* 任务动作参数(通过 / 驳回 / 回填)。
|
||||
*
|
||||
* <p>`fields` 为节点声明可写字段的 key → 值;回填时只写字段,通过时写字段并推进
|
||||
* (ADR 0006)。</p>
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalTaskActionParam implements Serializable {
|
||||
@@ -14,4 +18,7 @@ public class ApprovalTaskActionParam implements Serializable {
|
||||
|
||||
@ApiModelProperty("处理意见;驳回必填")
|
||||
private String opinion;
|
||||
|
||||
@ApiModelProperty("可写字段 key → 值")
|
||||
private Map<String, String> fields;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ public class ApprovalTaskListParam extends BaseParam {
|
||||
@ApiModelProperty("单据类型")
|
||||
private String docType;
|
||||
|
||||
@ApiModelProperty("流程编码,如 settlement")
|
||||
private String flowCode;
|
||||
|
||||
/** 查询范围:mine(默认,本人待办/已办)或 all(全租户待办,需 assign 权限)。 */
|
||||
@ApiModelProperty("查询范围 mine/all")
|
||||
private String scope;
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
package com.gxwebsoft.approval.service;
|
||||
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.support.ApprovalApprover;
|
||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||
import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalUserVO;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 审批人解析:把节点定义中的解析器(含会签组)解析为审批人快照。
|
||||
*
|
||||
* <p>解析全部走 {@link ApprovalDirectoryService}(core 专用连接),调用方必须在事务外执行。
|
||||
* 缺配时不抛异常,而是返回 {@link Resolution#getMissingMessage()},由调用方决定
|
||||
* 「提交预检直接阻断」还是「节点激活时停留并提示管理员」。</p>
|
||||
*/
|
||||
@Service
|
||||
public class ApprovalApproverResolver {
|
||||
|
||||
@Resource
|
||||
private ApprovalDirectoryService directoryService;
|
||||
|
||||
/**
|
||||
* 解析节点的全部审批人;会签节点按「组 × 启用成员」展开并附带组信息。
|
||||
*
|
||||
* <p>会签节点任一组缺配即返回缺配消息且 {@link Resolution#getApprovers()} 为空——
|
||||
* 全有或全无,避免半激活(ADR 0003)。</p>
|
||||
*/
|
||||
public Resolution resolve(ApprovalInstance instance, ApprovalNode node) {
|
||||
if (node == null) {
|
||||
return Resolution.missing("审批节点未配置");
|
||||
}
|
||||
if (hasGroups(node)) {
|
||||
return resolveGroups(instance, node);
|
||||
}
|
||||
List<ApprovalApprover> approvers = resolveSingle(instance, node.getResolver(), null);
|
||||
if (approvers.isEmpty()) {
|
||||
return Resolution.missing(missingMessage(instance, node.getResolver(), null, node.getName()));
|
||||
}
|
||||
return Resolution.resolved(approvers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交预检:逐节点解析全部审批人,靠后的节点也检查(ADR 0003)。
|
||||
*
|
||||
* @return 缺配提示;全部可解析返回 null
|
||||
*/
|
||||
public String preflight(ApprovalInstance instance, List<ApprovalNode> nodes) {
|
||||
for (ApprovalNode node : nodes) {
|
||||
Resolution resolution = resolve(instance, node);
|
||||
if (!resolution.isResolved()) {
|
||||
return resolution.getMissingMessage();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按组声明解析:会签节点组间会签、组内或签;CONFIRM 节点用组表达「多组全确认才前进」。
|
||||
* 任一组缺配即整节点不激活。
|
||||
*/
|
||||
private Resolution resolveGroups(ApprovalInstance instance, ApprovalNode node) {
|
||||
List<ApprovalApprover> approvers = new ArrayList<>();
|
||||
int seq = 0;
|
||||
for (ApprovalNode.ApprovalGroup group : node.getGroups()) {
|
||||
seq++;
|
||||
if (group == null || group.getResolver() == null || group.getResolver().trim().isEmpty()) {
|
||||
return Resolution.missing(groupName(group, seq) + "未配置解析器");
|
||||
}
|
||||
List<ApprovalApprover> members = resolveSingle(instance, group.getResolver(), group.getOrgCode());
|
||||
if (members.isEmpty()) {
|
||||
return Resolution.missing(missingMessage(instance, group.getResolver(), group.getOrgCode(),
|
||||
groupName(group, seq)));
|
||||
}
|
||||
String groupKey = group.getKey() == null || group.getKey().trim().isEmpty()
|
||||
? "group" + seq : group.getKey().trim();
|
||||
String name = groupName(group, seq);
|
||||
for (ApprovalApprover member : members) {
|
||||
approvers.add(new ApprovalApprover(member.getUserId(), member.getUserName(),
|
||||
groupKey, name, seq));
|
||||
}
|
||||
}
|
||||
if (approvers.isEmpty()) {
|
||||
return Resolution.missing(node.getName() + "未配置审批组");
|
||||
}
|
||||
return Resolution.resolved(approvers);
|
||||
}
|
||||
|
||||
private boolean hasGroups(ApprovalNode node) {
|
||||
return node.getGroups() != null && !node.getGroups().isEmpty();
|
||||
}
|
||||
|
||||
private String groupName(ApprovalNode.ApprovalGroup group, int seq) {
|
||||
if (group != null && group.getName() != null && !group.getName().trim().isEmpty()) {
|
||||
return group.getName().trim();
|
||||
}
|
||||
return "审批组" + seq;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个解析器 → 审批人。[orgCode] 仅对按组织编码的解析器生效。
|
||||
*/
|
||||
private List<ApprovalApprover> resolveSingle(ApprovalInstance instance, String resolver,
|
||||
String orgCode) {
|
||||
if (resolver == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
switch (resolver) {
|
||||
case ApprovalConstants.RESOLVER_REPORTER_ORG_LEADER:
|
||||
return departmentLeaderApprovers(instance.getReportOrgId());
|
||||
case ApprovalConstants.RESOLVER_REPORTER_ORG_DEPUTY:
|
||||
return departmentDeputyApprovers(instance.getReportOrgId());
|
||||
case ApprovalConstants.RESOLVER_ACCEPT_ORG_LEADER:
|
||||
return departmentLeaderApprovers(instance.getAcceptOrgId());
|
||||
case ApprovalConstants.RESOLVER_ACCEPT_ORG_DEPUTY:
|
||||
return departmentDeputyApprovers(instance.getAcceptOrgId());
|
||||
case ApprovalConstants.RESOLVER_ARCHIVIST:
|
||||
case ApprovalConstants.RESOLVER_ROLE_ARCHIVIST:
|
||||
return roleApprovers(ApprovalConstants.ROLE_ARCHIVIST);
|
||||
case ApprovalConstants.RESOLVER_REPORTER:
|
||||
return userApprovers(Collections.singletonList(instance.getReporterId()));
|
||||
case ApprovalConstants.RESOLVER_REPORTER_AND_DISPATCH_TARGET:
|
||||
return userApprovers(confirmCandidateIds(instance));
|
||||
case ApprovalConstants.RESOLVER_ACCEPT_ORG_MEMBERS:
|
||||
return organizationApprovers(instance.getAcceptOrgId());
|
||||
case ApprovalConstants.RESOLVER_ORG_LEADER_BY_CODE:
|
||||
return codeLeaderApprovers(orgCode);
|
||||
case ApprovalConstants.RESOLVER_ORG_DEPUTY_BY_CODE:
|
||||
return codeDeputyApprovers(orgCode);
|
||||
case ApprovalConstants.RESOLVER_ROLE_CASHIER:
|
||||
return roleApprovers(ApprovalConstants.ROLE_CASHIER);
|
||||
case ApprovalConstants.RESOLVER_ROLE_TYPIST:
|
||||
return roleApprovers(ApprovalConstants.ROLE_TYPIST);
|
||||
case ApprovalConstants.RESOLVER_ROLE_ISSUER:
|
||||
return roleApprovers(ApprovalConstants.ROLE_ISSUER);
|
||||
case ApprovalConstants.RESOLVER_ROLE_GENERAL_MANAGER:
|
||||
return roleApprovers(ApprovalConstants.ROLE_GENERAL_MANAGER);
|
||||
case ApprovalConstants.RESOLVER_ROLE_RELATED_DEPT:
|
||||
return roleApprovers(ApprovalConstants.ROLE_RELATED_DEPT);
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺配提示:按解析器给出可定位到岗位/部门的文案,供提交预检直接展示。
|
||||
*/
|
||||
private String missingMessage(ApprovalInstance instance, String resolver, String orgCode,
|
||||
String fallbackName) {
|
||||
if (resolver == null) {
|
||||
return fallbackName + "未配置审批人";
|
||||
}
|
||||
switch (resolver) {
|
||||
case ApprovalConstants.RESOLVER_REPORTER_ORG_LEADER:
|
||||
return "报送部门负责人角色缺配";
|
||||
case ApprovalConstants.RESOLVER_REPORTER_ORG_DEPUTY:
|
||||
return "报送部门分管领导角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ACCEPT_ORG_LEADER:
|
||||
return "受理部门负责人角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ACCEPT_ORG_DEPUTY:
|
||||
return "受理部门分管领导角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ARCHIVIST:
|
||||
case ApprovalConstants.RESOLVER_ROLE_ARCHIVIST:
|
||||
return "收文员角色缺配";
|
||||
case ApprovalConstants.RESOLVER_REPORTER:
|
||||
return "报送人账号不可用";
|
||||
case ApprovalConstants.RESOLVER_REPORTER_AND_DISPATCH_TARGET:
|
||||
return "存档确认人账号不可用";
|
||||
case ApprovalConstants.RESOLVER_ACCEPT_ORG_MEMBERS:
|
||||
return "受理部门无可用审批参与人";
|
||||
case ApprovalConstants.RESOLVER_ORG_LEADER_BY_CODE:
|
||||
return organizationLabel(orgCode) + "负责人角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ORG_DEPUTY_BY_CODE:
|
||||
return organizationLabel(orgCode) + "分管领导角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ROLE_CASHIER:
|
||||
return "出纳角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ROLE_TYPIST:
|
||||
return "录入员角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ROLE_ISSUER:
|
||||
return "发放人角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ROLE_GENERAL_MANAGER:
|
||||
return "总经理角色缺配";
|
||||
case ApprovalConstants.RESOLVER_ROLE_RELATED_DEPT:
|
||||
return "相关部门角色缺配";
|
||||
default:
|
||||
return (fallbackName == null ? "审批节点" : fallbackName) + "审批人缺配";
|
||||
}
|
||||
}
|
||||
|
||||
/** 组织编码 → 展示名;组织不存在时退回编码本身。 */
|
||||
private String organizationLabel(String orgCode) {
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganizationByCode(orgCode);
|
||||
return organization == null ? (orgCode == null ? "指定部门" : orgCode)
|
||||
: organization.getOrganizationName();
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> departmentLeaderApprovers(Integer organizationId) {
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganization(organizationId);
|
||||
String roleCode = organization == null
|
||||
? null : ApprovalConstants.departmentLeaderRoleCode(organization.getOrganizationCode());
|
||||
return roleCode == null ? Collections.emptyList() : roleApprovers(roleCode);
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> departmentDeputyApprovers(Integer organizationId) {
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganization(organizationId);
|
||||
String roleCode = organization == null
|
||||
? null : ApprovalConstants.departmentDeputyRoleCode(organization.getOrganizationCode());
|
||||
return roleCode == null ? Collections.emptyList() : roleApprovers(roleCode);
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> codeLeaderApprovers(String orgCode) {
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganizationByCode(orgCode);
|
||||
String roleCode = organization == null
|
||||
? null : ApprovalConstants.departmentLeaderRoleCode(organization.getOrganizationCode());
|
||||
return roleCode == null ? Collections.emptyList() : roleApprovers(roleCode);
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> codeDeputyApprovers(String orgCode) {
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganizationByCode(orgCode);
|
||||
String roleCode = organization == null
|
||||
? null : ApprovalConstants.departmentDeputyRoleCode(organization.getOrganizationCode());
|
||||
return roleCode == null ? Collections.emptyList() : roleApprovers(roleCode);
|
||||
}
|
||||
|
||||
private List<Integer> confirmCandidateIds(ApprovalInstance instance) {
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
if (instance.getReporterId() != null) {
|
||||
ids.add(instance.getReporterId());
|
||||
}
|
||||
if (instance.getDispatchTargetId() != null && !ids.contains(instance.getDispatchTargetId())) {
|
||||
ids.add(instance.getDispatchTargetId());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> roleApprovers(String roleCode) {
|
||||
return toApprovers(directoryService.listRoleMembers(roleCode));
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> userApprovers(List<Integer> userIds) {
|
||||
return toApprovers(directoryService.listActiveUsersByIds(userIds));
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> organizationApprovers(Integer organizationId) {
|
||||
return toApprovers(directoryService.listOrganizationMembers(organizationId));
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> toApprovers(List<ApprovalUserVO> users) {
|
||||
return users.stream()
|
||||
.map(user -> new ApprovalApprover(user.getUserId(), displayName(user)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** 审批人展示名:实名 → 昵称 → 账号。 */
|
||||
public String displayName(ApprovalUserVO user) {
|
||||
if (user.getRealName() != null && !user.getRealName().isEmpty()) {
|
||||
return user.getRealName();
|
||||
}
|
||||
if (user.getNickname() != null && !user.getNickname().isEmpty()) {
|
||||
return user.getNickname();
|
||||
}
|
||||
return user.getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结果:审批人快照或指名缺配提示。
|
||||
*/
|
||||
public static final class Resolution {
|
||||
private final List<ApprovalApprover> approvers;
|
||||
private final String missingMessage;
|
||||
|
||||
private Resolution(List<ApprovalApprover> approvers, String missingMessage) {
|
||||
this.approvers = approvers;
|
||||
this.missingMessage = missingMessage;
|
||||
}
|
||||
|
||||
public static Resolution resolved(List<ApprovalApprover> approvers) {
|
||||
return new Resolution(approvers == null ? Collections.emptyList() : approvers, null);
|
||||
}
|
||||
|
||||
public static Resolution missing(String message) {
|
||||
return new Resolution(Collections.emptyList(), message);
|
||||
}
|
||||
|
||||
public boolean isResolved() {
|
||||
return missingMessage == null;
|
||||
}
|
||||
|
||||
public List<ApprovalApprover> getApprovers() {
|
||||
return approvers;
|
||||
}
|
||||
|
||||
public String getMissingMessage() {
|
||||
return missingMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,17 @@ public class ApprovalAttachmentService {
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
/** 指定分类的附件数量(提交预检按用途校验必传,ADR 0005)。 */
|
||||
public int countCategory(Long instanceId, String category) {
|
||||
if (StrUtil.isBlank(category)) {
|
||||
return 0;
|
||||
}
|
||||
Integer count = attachmentMapper.selectCount(new LambdaQueryWrapper<ApprovalAttachment>()
|
||||
.eq(ApprovalAttachment::getInstanceId, instanceId)
|
||||
.eq(ApprovalAttachment::getCategory, category));
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传附件:仅报送人且实例可编辑时可上传。
|
||||
*/
|
||||
@@ -97,8 +108,7 @@ public class ApprovalAttachmentService {
|
||||
checkEditable(instance, user);
|
||||
|
||||
String normalizedCategory = StrUtil.isBlank(category) ? ApprovalConstants.CATEGORY_OTHER : category.trim();
|
||||
if (!ApprovalConstants.CATEGORY_SIGNED.equals(normalizedCategory)
|
||||
&& !ApprovalConstants.CATEGORY_OTHER.equals(normalizedCategory)) {
|
||||
if (!ApprovalConstants.ATTACHMENT_CATEGORIES.contains(normalizedCategory)) {
|
||||
throw new BusinessException("附件分类不正确");
|
||||
}
|
||||
if (file == null || file.isEmpty()) {
|
||||
|
||||
@@ -78,6 +78,20 @@ public class ApprovalDirectoryService {
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
}
|
||||
|
||||
/** 按组织编码取启用组织;编码为空或不存在时返回 null(节点定义固定编码用)。 */
|
||||
public ApprovalOrganizationVO getOrganizationByCode(String organizationCode) {
|
||||
if (organizationCode == null || organizationCode.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<ApprovalOrganizationVO> list = jdbcTemplate.query(
|
||||
"SELECT organization_id, parent_id, organization_name, organization_full_name, "
|
||||
+ "organization_code, leader_id FROM sys_organization "
|
||||
+ "WHERE organization_code = ? AND tenant_id = ? AND deleted = 0 "
|
||||
+ "ORDER BY organization_id LIMIT 1",
|
||||
ORGANIZATION_MAPPER, organizationCode.trim(), ApprovalTenantContext.currentTenantId());
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
}
|
||||
|
||||
/** 角色下全部启用成员;角色不存在或成员为空时返回空列表。 */
|
||||
public List<ApprovalUserVO> listRoleMembers(String roleCode) {
|
||||
if (roleCode == null) {
|
||||
@@ -210,7 +224,7 @@ public class ApprovalDirectoryService {
|
||||
+ "JOIN sys_role pr ON pr.role_id = pur.role_id AND pr.tenant_id = ? AND pr.deleted = 0 "
|
||||
+ "WHERE pur.user_id = u.user_id AND pur.tenant_id = ? AND ("
|
||||
+ "pr.role_code = ? OR LEFT(pr.role_code, 12) = ? OR LEFT(pr.role_code, 12) = ? "
|
||||
+ "OR pr.role_code IN (?, ?, ?, ?, ?, ?)))";
|
||||
+ "OR pr.role_code IN (?, ?, ?, ?, ?, ?, ?)))";
|
||||
|
||||
private static List<Object> participantArgs(Integer tenantId) {
|
||||
// 与 sql/04-approval_role_grants.sql 的授予矩阵保持一致(superAdmin 在 02 已获全部审批权限)
|
||||
@@ -220,7 +234,9 @@ public class ApprovalDirectoryService {
|
||||
ApprovalConstants.ROLE_DEPT_DEPUTY_PREFIX,
|
||||
ApprovalConstants.ROLE_ARCHIVIST,
|
||||
ApprovalConstants.ROLE_SUPER_ADMIN,
|
||||
"approval_cashier", "approval_typist", "approval_issuer", "approval_general_manager");
|
||||
ApprovalConstants.ROLE_CASHIER, ApprovalConstants.ROLE_TYPIST,
|
||||
ApprovalConstants.ROLE_ISSUER, ApprovalConstants.ROLE_GENERAL_MANAGER,
|
||||
ApprovalConstants.ROLE_RELATED_DEPT);
|
||||
}
|
||||
|
||||
/** 是否持有任一角色(用于 allVisible 判断)。 */
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package com.gxwebsoft.approval.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentRegistry;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFlowDefinition;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.entity.ApprovalTask;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
|
||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import com.gxwebsoft.approval.vo.ApprovalGroupProgressVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 单据服务:所有单据共用的新建/更新草稿入口与提交预检。
|
||||
*
|
||||
* <p>跨库约束同 {@link ApprovalInstanceService}:组织解析在事务外完成,写 master 的
|
||||
* 部分放 TransactionTemplate;字段校验与业务表读写委托给 {@link ApprovalDocumentType}。</p>
|
||||
*/
|
||||
@Service
|
||||
public class ApprovalDocumentService {
|
||||
|
||||
/** 检索文本上限,避免超出 approval_instance.search_text 列宽。 */
|
||||
private static final int MAX_SEARCH_TEXT = 500;
|
||||
|
||||
@Resource
|
||||
private ApprovalDocumentRegistry documentRegistry;
|
||||
@Resource
|
||||
private ApprovalInstanceMapper instanceMapper;
|
||||
@Resource
|
||||
private ApprovalFlowService flowService;
|
||||
@Resource
|
||||
private ApprovalDirectoryService directoryService;
|
||||
@Resource
|
||||
private ApprovalAttachmentService attachmentService;
|
||||
@Resource
|
||||
private ApprovalApproverResolver approverResolver;
|
||||
@Resource
|
||||
private ApprovalEngineService engineService;
|
||||
@Resource
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
/**
|
||||
* 新建或更新草稿,返回实例 id。
|
||||
*/
|
||||
public Long save(String flowCode, ApprovalDocumentParam param, User user) {
|
||||
ApprovalDocumentType type = documentRegistry.require(flowCode);
|
||||
if (param == null) {
|
||||
throw new BusinessException("参数不能为空");
|
||||
}
|
||||
type.validate(param, user);
|
||||
Integer organizationId = user == null ? null : user.getOrganizationId();
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganization(organizationId);
|
||||
String organizationName = organization == null ? null : organization.getOrganizationName();
|
||||
|
||||
if (param.getId() != null) {
|
||||
return inTransaction(() ->
|
||||
updateDraft(type, param, user, organizationId, organizationName));
|
||||
}
|
||||
ApprovalFlowDefinition definition = flowService.getOrCreate(flowCode);
|
||||
return inTransaction(() ->
|
||||
createDraft(type, param, user, definition, organizationId, organizationName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交预检:按单据声明校验必传附件分类,再逐节点逐组解析审批人(ADR 0003/0005)。
|
||||
*
|
||||
* <p>不通过即抛 BusinessException;消息指名缺的是哪一类附件或哪个组。</p>
|
||||
*/
|
||||
public void preflight(ApprovalInstance instance, List<ApprovalNode> nodes) {
|
||||
ApprovalDocumentType type = documentRegistry.find(instance.getFlowCode());
|
||||
String requiredCategory = type == null ? null : type.requiredAttachmentCategory();
|
||||
if (requiredCategory != null) {
|
||||
if (attachmentService.countCategory(instance.getId(), requiredCategory) == 0) {
|
||||
throw new BusinessException("请至少上传一张" + ApprovalConstants.categoryName(requiredCategory));
|
||||
}
|
||||
} else if (attachmentService.count(instance.getId()) == 0) {
|
||||
throw new BusinessException("请至少上传一张附件");
|
||||
}
|
||||
String missing = approverResolver.preflight(instance, nodes);
|
||||
if (missing != null) {
|
||||
throw new BusinessException(missing);
|
||||
}
|
||||
if (directoryService.listRoleMembers(ApprovalConstants.ROLE_ARCHIVIST).isEmpty()) {
|
||||
throw new BusinessException("收文员角色缺配");
|
||||
}
|
||||
}
|
||||
|
||||
/** 单据实现;未知流程返回 null。 */
|
||||
public ApprovalDocumentType documentType(ApprovalInstance instance) {
|
||||
return documentRegistry.find(instance.getFlowCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交/重提前刷新列表标题与检索文本(业务表可能刚被编辑过)。
|
||||
*/
|
||||
public void refreshIndex(ApprovalInstance instance) {
|
||||
ApprovalDocumentType type = documentRegistry.find(instance.getFlowCode());
|
||||
if (type == null) {
|
||||
return;
|
||||
}
|
||||
instance.setTitle(type.title(instance));
|
||||
instance.setSearchText(truncate(type.searchText(instance)));
|
||||
instanceMapper.updateById(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按实例当前轮次装配会签进度(仅会签节点有组)。
|
||||
*/
|
||||
public void fillGroupProgress(ApprovalInstance instance, ApprovalDetail detail) {
|
||||
List<ApprovalGroupProgressVO> progress = new ArrayList<>();
|
||||
for (ApprovalNode node : flowService.parseNodes(instance.getDefinitionId())) {
|
||||
if (node.getGroups() == null || node.getGroups().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Map<String, List<ApprovalTask>> grouped =
|
||||
engineService.tasksByGroup(instance.getId(), instance.getRound(), node.getKey());
|
||||
for (List<ApprovalTask> tasks : grouped.values()) {
|
||||
if (tasks.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ApprovalTask first = tasks.get(0);
|
||||
ApprovalGroupProgressVO vo = new ApprovalGroupProgressVO();
|
||||
vo.setNodeKey(node.getKey());
|
||||
vo.setNodeName(node.getName());
|
||||
vo.setGroupKey(first.getGroupKey());
|
||||
vo.setGroupName(first.getGroupName());
|
||||
vo.setGroupSeq(first.getGroupSeq());
|
||||
List<ApprovalGroupProgressVO.Member> members = new ArrayList<>(tasks.size());
|
||||
int signed = 0;
|
||||
for (ApprovalTask task : tasks) {
|
||||
ApprovalGroupProgressVO.Member member = new ApprovalGroupProgressVO.Member();
|
||||
member.setUserId(task.getAssigneeId());
|
||||
member.setUserName(task.getAssigneeName());
|
||||
member.setStatus(task.getStatus());
|
||||
member.setStatusName(groupTaskStatusName(task.getStatus()));
|
||||
members.add(member);
|
||||
if (ApprovalConstants.TASK_DONE.equals(task.getStatus())) {
|
||||
signed++;
|
||||
}
|
||||
}
|
||||
vo.setMembers(members);
|
||||
vo.setSigned(signed);
|
||||
vo.setTotal(members.size());
|
||||
progress.add(vo);
|
||||
}
|
||||
}
|
||||
detail.setGroupProgress(progress);
|
||||
}
|
||||
|
||||
private String groupTaskStatusName(String status) {
|
||||
if (ApprovalConstants.TASK_PENDING.equals(status)) {
|
||||
return "待审批";
|
||||
}
|
||||
if (ApprovalConstants.TASK_DONE.equals(status)) {
|
||||
return "已通过";
|
||||
}
|
||||
if (ApprovalConstants.TASK_CANCELLED.equals(status)) {
|
||||
return "已取消";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private Long createDraft(ApprovalDocumentType type, ApprovalDocumentParam param, User user,
|
||||
ApprovalFlowDefinition definition, Integer organizationId,
|
||||
String organizationName) {
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setFlowCode(type.flowCode());
|
||||
instance.setDefinitionId(definition.getId());
|
||||
instance.setDefinitionVersion(definition.getVersion());
|
||||
instance.setDocType(type.docType());
|
||||
instance.setBizId(0L);
|
||||
instance.setStatus(ApprovalConstants.STATUS_DRAFT);
|
||||
instance.setRound(0);
|
||||
instance.setReporterId(user.getUserId());
|
||||
instance.setReporterName(displayName(user));
|
||||
instance.setReportOrgId(organizationId);
|
||||
instance.setReportOrgName(organizationName);
|
||||
instanceMapper.insert(instance);
|
||||
|
||||
Long bizId = type.persist(instance.getId(), null, param, user);
|
||||
instance.setBizId(bizId == null ? 0L : bizId);
|
||||
instance.setTitle(type.title(instance));
|
||||
instance.setSearchText(truncate(type.searchText(instance)));
|
||||
instanceMapper.updateById(instance);
|
||||
return instance.getId();
|
||||
}
|
||||
|
||||
private Long updateDraft(ApprovalDocumentType type, ApprovalDocumentParam param, User user,
|
||||
Integer organizationId, String organizationName) {
|
||||
ApprovalInstance instance = instanceMapper.selectById(param.getId());
|
||||
if (instance == null) {
|
||||
throw new BusinessException("单据不存在");
|
||||
}
|
||||
if (!type.flowCode().equals(instance.getFlowCode())) {
|
||||
throw new BusinessException("单据类型不匹配");
|
||||
}
|
||||
if (!instance.getReporterId().equals(user.getUserId())) {
|
||||
throw new BusinessException("无权操作该单据");
|
||||
}
|
||||
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
|
||||
throw new BusinessException(ApprovalConstants.writeBlockedMessage(instance.getStatus()));
|
||||
}
|
||||
Long bizId = type.persist(instance.getId(), instance.getBizId(), param, user);
|
||||
if (bizId != null) {
|
||||
instance.setBizId(bizId);
|
||||
}
|
||||
instance.setReportOrgId(organizationId);
|
||||
instance.setReportOrgName(organizationName);
|
||||
instance.setTitle(type.title(instance));
|
||||
instance.setSearchText(truncate(type.searchText(instance)));
|
||||
instanceMapper.updateById(instance);
|
||||
return instance.getId();
|
||||
}
|
||||
|
||||
private String truncate(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return text.length() <= MAX_SEARCH_TEXT ? text : text.substring(0, MAX_SEARCH_TEXT);
|
||||
}
|
||||
|
||||
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 <T> T inTransaction(Supplier<T> action) {
|
||||
return new TransactionTemplate(transactionManager).execute(status -> action.get());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gxwebsoft.approval.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
@@ -16,13 +17,17 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 审批引擎起步:实例加锁、首节点激活、任务快照与处理记录。
|
||||
* 审批引擎:实例加锁、节点激活、任务快照、会签分组与处理记录。
|
||||
*
|
||||
* <p>本类只写 master 库,必须在调用方的事务内执行;后续迭代在此扩展
|
||||
* 通过/驳回/撤回/存档/归档/派单的节点推进,不再改动已发布的方法签名。</p>
|
||||
* <p>本类只写 master 库,必须在调用方的事务内执行;审批人解析(core 库)
|
||||
* 必须由调用方在事务外完成后传入。</p>
|
||||
*/
|
||||
@Service
|
||||
public class ApprovalEngineService {
|
||||
@@ -37,7 +42,7 @@ public class ApprovalEngineService {
|
||||
private ApprovalFlowService flowService;
|
||||
|
||||
/**
|
||||
* 提交/重提:激活流程首节点,为解析出的审批人生成或签任务。
|
||||
* 提交/重提:激活流程首节点,为解析出的审批人生成任务。
|
||||
*
|
||||
* @return 生成的任务数;0 表示节点缺配,实例停留在当前节点等待管理员补配置
|
||||
*/
|
||||
@@ -91,7 +96,7 @@ public class ApprovalEngineService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消同一轮同一节点的其余待办(或签处理完成后调用)。
|
||||
* 取消同一轮同一节点的其余待办(或签/派单/归档处理完成后调用)。
|
||||
*/
|
||||
public int cancelPendingTasks(Long instanceId, Integer round, Long exceptTaskId) {
|
||||
UpdateWrapper<ApprovalTask> wrapper = new UpdateWrapper<ApprovalTask>()
|
||||
@@ -105,6 +110,88 @@ public class ApprovalEngineService {
|
||||
return taskMapper.update(null, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消同节点同轮同组的其余待办(会签节点组内或签)。
|
||||
*
|
||||
* <p>会签节点不能像或签那样取消整轮:那会让第一组通过就杀掉其余组的待办(ADR 0003)。</p>
|
||||
*/
|
||||
public int cancelPendingTasksInGroup(Long instanceId, Integer round, String nodeKey,
|
||||
String groupKey, Long exceptTaskId) {
|
||||
UpdateWrapper<ApprovalTask> wrapper = new UpdateWrapper<ApprovalTask>()
|
||||
.eq("instance_id", instanceId)
|
||||
.eq("round", round)
|
||||
.eq("node_key", nodeKey)
|
||||
.eq("group_key", groupKey)
|
||||
.eq("status", ApprovalConstants.TASK_PENDING)
|
||||
.set("status", ApprovalConstants.TASK_CANCELLED);
|
||||
if (exceptTaskId != null) {
|
||||
wrapper.ne("id", exceptTaskId);
|
||||
}
|
||||
return taskMapper.update(null, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成指定用户在本节点本轮的全部待办(一人同属多个会签组时一次点击生效)。
|
||||
*
|
||||
* @return 实际更新的任务数
|
||||
*/
|
||||
public int completeAssigneeTasks(Long instanceId, Integer round, String nodeKey, Integer assigneeId,
|
||||
Long excludeTaskId, String opinion) {
|
||||
UpdateWrapper<ApprovalTask> wrapper = new UpdateWrapper<ApprovalTask>()
|
||||
.eq("instance_id", instanceId)
|
||||
.eq("round", round)
|
||||
.eq("node_key", nodeKey)
|
||||
.eq("assignee_id", assigneeId)
|
||||
.eq("status", ApprovalConstants.TASK_PENDING)
|
||||
.set("status", ApprovalConstants.TASK_DONE)
|
||||
.set("handle_time", LocalDateTime.now());
|
||||
if (excludeTaskId != null) {
|
||||
wrapper.ne("id", excludeTaskId);
|
||||
}
|
||||
if (opinion != null) {
|
||||
wrapper.set("opinion", opinion);
|
||||
}
|
||||
return taskMapper.update(null, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 该节点该轮仍待处理的组标识集合(按 group_seq 顺序)。
|
||||
*/
|
||||
public List<String> pendingGroupKeys(Long instanceId, Integer round, String nodeKey) {
|
||||
List<ApprovalTask> pending = taskMapper.selectList(new LambdaQueryWrapper<ApprovalTask>()
|
||||
.eq(ApprovalTask::getInstanceId, instanceId)
|
||||
.eq(ApprovalTask::getRound, round)
|
||||
.eq(ApprovalTask::getNodeKey, nodeKey)
|
||||
.eq(ApprovalTask::getStatus, ApprovalConstants.TASK_PENDING)
|
||||
.orderByAsc(ApprovalTask::getGroupSeq)
|
||||
.orderByAsc(ApprovalTask::getId));
|
||||
Map<String, Boolean> ordered = new LinkedHashMap<>();
|
||||
for (ApprovalTask task : pending) {
|
||||
if (task.getGroupKey() != null) {
|
||||
ordered.putIfAbsent(task.getGroupKey(), Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(ordered.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点下非空的会签组(按 group_seq),供详情分组展示。
|
||||
*/
|
||||
public Map<String, List<ApprovalTask>> tasksByGroup(Long instanceId, Integer round, String nodeKey) {
|
||||
List<ApprovalTask> tasks = taskMapper.selectList(new LambdaQueryWrapper<ApprovalTask>()
|
||||
.eq(ApprovalTask::getInstanceId, instanceId)
|
||||
.eq(ApprovalTask::getRound, round)
|
||||
.eq(ApprovalTask::getNodeKey, nodeKey)
|
||||
.orderByAsc(ApprovalTask::getGroupSeq)
|
||||
.orderByAsc(ApprovalTask::getId));
|
||||
Map<String, List<ApprovalTask>> grouped = new LinkedHashMap<>();
|
||||
for (ApprovalTask task : tasks) {
|
||||
String key = task.getGroupKey() == null ? "" : task.getGroupKey();
|
||||
grouped.computeIfAbsent(key, ignored -> new ArrayList<>()).add(task);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加处理记录。
|
||||
*/
|
||||
@@ -136,6 +223,10 @@ public class ApprovalEngineService {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ApprovalNode findNode(ApprovalInstance instance, String nodeKey) {
|
||||
return flowService.findNode(instance.getDefinitionId(), nodeKey);
|
||||
}
|
||||
|
||||
public String nodeName(ApprovalInstance instance, String nodeKey) {
|
||||
return flowService.nodeName(instance.getDefinitionId(), nodeKey);
|
||||
}
|
||||
@@ -155,6 +246,10 @@ public class ApprovalEngineService {
|
||||
task.setTaskType(node.getType());
|
||||
task.setAssigneeId(approver.getUserId());
|
||||
task.setAssigneeName(approver.getUserName());
|
||||
// 非会签节点三列为空(ADR 0003)
|
||||
task.setGroupKey(approver.getGroupKey());
|
||||
task.setGroupName(approver.getGroupName());
|
||||
task.setGroupSeq(approver.getGroupSeq());
|
||||
task.setStatus(ApprovalConstants.TASK_PENDING);
|
||||
task.setCreateTime(LocalDateTime.now());
|
||||
taskMapper.insert(task);
|
||||
@@ -172,4 +267,9 @@ public class ApprovalEngineService {
|
||||
}
|
||||
return user.getUsername();
|
||||
}
|
||||
|
||||
/** 便于文档实现者复用:去重并保留顺序。 */
|
||||
public static List<ApprovalApprover> distinctApprovers(List<ApprovalApprover> approvers) {
|
||||
return approvers.stream().distinct().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.gxwebsoft.approval.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentRegistry;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalExportMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
|
||||
import com.gxwebsoft.approval.param.ApprovalQueryParam;
|
||||
import com.gxwebsoft.approval.support.ApprovalExportResult;
|
||||
import com.gxwebsoft.approval.support.ApprovalExportRow;
|
||||
import com.gxwebsoft.approval.support.ApprovalLineSpec;
|
||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
@@ -25,15 +31,16 @@ import java.math.RoundingMode;
|
||||
import java.nio.file.Files;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 单据导出:按列表同一套筛选与数据权限同步生成 xlsx,并写文件记录。
|
||||
* 单据导出:跨单据总表与按单据类型的明细导出。
|
||||
*
|
||||
* <p>独立查询一次取 5001 行判断是否超限,不经过分页插件的 2000 条上限;
|
||||
* 超过 5000 行直接拒绝,不生成文件。导出内容不含附件。</p>
|
||||
* <p>总表保持「一行单据一行」,只多「明细行数」「合计金额」与「会签进度」三列(ADR 0004);
|
||||
* 逐行数据另做按 flow_code 的明细导出,供对账与成本归集。导出内容不含附件。</p>
|
||||
*/
|
||||
@Service
|
||||
public class ApprovalExportService {
|
||||
@@ -43,33 +50,35 @@ public class ApprovalExportService {
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/** 一期 16 列顺序保持不变,二期三列追加在末尾(ADR 0004 / 0003)。 */
|
||||
private static final String[] HEADERS = {
|
||||
"单号", "单据类型", "标题或名称", "报送部门", "报送人", "受理部门", "实施单位", "费用(元)",
|
||||
"服务内容或协调内容", "备注", "状态", "当前节点", "提交时间", "审批完成时间", "归档时间", "归档人"
|
||||
"服务内容或协调内容", "备注", "状态", "当前节点", "提交时间", "审批完成时间", "归档时间", "归档人",
|
||||
"明细行数", "合计金额", "会签进度"
|
||||
};
|
||||
|
||||
/** 列宽(字符数),长文本列加宽但不截断内容。 */
|
||||
private static final int[] COLUMN_WIDTHS = {
|
||||
20, 16, 28, 20, 12, 20, 22, 12, 50, 28, 12, 16, 20, 20, 20, 12
|
||||
20, 16, 28, 20, 12, 20, 22, 12, 50, 28, 12, 16, 20, 20, 20, 12, 10, 14, 18
|
||||
};
|
||||
|
||||
@Resource
|
||||
private ApprovalExportMapper exportMapper;
|
||||
|
||||
@Resource
|
||||
private ApprovalInstanceMapper instanceMapper;
|
||||
@Resource
|
||||
private ApprovalDocumentRegistry documentRegistry;
|
||||
@Resource
|
||||
private ApprovalFlowService flowService;
|
||||
|
||||
@Resource
|
||||
private ApprovalDirectoryService directoryService;
|
||||
|
||||
@Resource
|
||||
private ApprovalFileService fileService;
|
||||
|
||||
@Resource
|
||||
private ConfigProperties config;
|
||||
|
||||
/**
|
||||
* 导出当前筛选结果。
|
||||
* 导出跨单据总表。
|
||||
*
|
||||
* @throws BusinessException 结果超过 {@link #MAX_EXPORT_ROWS} 行时拒绝,不生成文件
|
||||
*/
|
||||
@@ -83,6 +92,57 @@ public class ApprovalExportService {
|
||||
throw new BusinessException("请缩小筛选范围");
|
||||
}
|
||||
String filename = "excel/审批单据-" + System.currentTimeMillis() + ".xlsx";
|
||||
return write(user, rows.size(), filename, sheet -> fillSummarySheet(sheet, rows));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按单据类型的明细导出:明细列 + 单号/报送部门/状态,含逐行数据。
|
||||
*/
|
||||
public ApprovalExportResult exportLines(String flowCode, ApprovalQueryParam param, User user)
|
||||
throws IOException {
|
||||
ApprovalDocumentType type = documentRegistry.require(flowCode);
|
||||
List<ApprovalLineSpec> columns = type.lineColumns();
|
||||
if (columns == null || columns.isEmpty()) {
|
||||
throw new BusinessException("该单据类型没有明细行");
|
||||
}
|
||||
int allVisible = directoryService.hasAnyRole(user.getUserId(),
|
||||
ApprovalConstants.ROLE_SUPER_ADMIN, ApprovalConstants.ROLE_ARCHIVIST) ? 1 : 0;
|
||||
int assignVisible = hasAuthority(user, ApprovalConstants.PERMISSION_ASSIGN) ? 1 : 0;
|
||||
List<ApprovalExportRow> instances = exportMapper.selectExportRows(param, user.getUserId(), allVisible,
|
||||
assignVisible);
|
||||
if (instances.size() > MAX_EXPORT_ROWS) {
|
||||
throw new BusinessException("请缩小筛选范围");
|
||||
}
|
||||
List<ExportLine> lines = new ArrayList<>();
|
||||
for (ApprovalExportRow row : instances) {
|
||||
if (!flowCode.equals(row.getFlowCode())) {
|
||||
continue;
|
||||
}
|
||||
ApprovalInstance instance = instanceMapper.selectById(row.getId());
|
||||
if (instance == null) {
|
||||
continue;
|
||||
}
|
||||
for (Map<String, Object> line : type.lines(instance)) {
|
||||
lines.add(new ExportLine(row, line));
|
||||
}
|
||||
}
|
||||
String filename = "excel/审批单据明细-" + flowCode + "-" + System.currentTimeMillis() + ".xlsx";
|
||||
return write(user, lines.size(), filename, sheet -> fillLineSheet(sheet, type, columns, lines));
|
||||
}
|
||||
|
||||
/** 明细导出的一行:所属单据 + 明细行。 */
|
||||
private static final class ExportLine {
|
||||
private final ApprovalExportRow instance;
|
||||
private final Map<String, Object> line;
|
||||
|
||||
private ExportLine(ApprovalExportRow instance, Map<String, Object> line) {
|
||||
this.instance = instance;
|
||||
this.line = line;
|
||||
}
|
||||
}
|
||||
|
||||
private ApprovalExportResult write(User user, int rowCount, String filename,
|
||||
WorkbookFiller filler) throws IOException {
|
||||
String path = config.getUploadPath() + filename;
|
||||
File target = new File(path);
|
||||
try {
|
||||
@@ -90,9 +150,17 @@ public class ApprovalExportService {
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent.toPath());
|
||||
}
|
||||
writeWorkbook(rows, target);
|
||||
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
|
||||
try {
|
||||
filler.fill(workbook);
|
||||
try (OutputStream output = Files.newOutputStream(target.toPath())) {
|
||||
workbook.write(output);
|
||||
}
|
||||
} finally {
|
||||
workbook.dispose();
|
||||
workbook.close();
|
||||
}
|
||||
} catch (IOException | RuntimeException e) {
|
||||
// 生成失败时不留下半成品文件
|
||||
Files.deleteIfExists(target.toPath());
|
||||
throw e;
|
||||
}
|
||||
@@ -106,57 +174,125 @@ public class ApprovalExportService {
|
||||
record.setUrl(config.getServerUrl() + "/file/download/" + filename);
|
||||
record.setDownloadUrl(record.getUrl());
|
||||
fileService.save(record);
|
||||
return new ApprovalExportResult(record, rows.size());
|
||||
return new ApprovalExportResult(record, rowCount);
|
||||
}
|
||||
|
||||
private boolean hasAuthority(User user, String authority) {
|
||||
if (user.getAuthorities() == null) {
|
||||
return false;
|
||||
private void fillSummarySheet(SXSSFWorkbook workbook, List<ApprovalExportRow> rows) {
|
||||
SXSSFSheet sheet = workbook.createSheet("审批单据");
|
||||
for (int i = 0; i < COLUMN_WIDTHS.length; i++) {
|
||||
sheet.setColumnWidth(i, COLUMN_WIDTHS[i] * 256);
|
||||
}
|
||||
SXSSFRow header = sheet.createRow(0);
|
||||
for (int i = 0; i < HEADERS.length; i++) {
|
||||
header.createCell(i).setCellValue(HEADERS[i]);
|
||||
}
|
||||
Map<Long, List<ApprovalNode>> nodeCache = new HashMap<>();
|
||||
// 批量取实例,避免逐行往返(5000 行导出时 N+1 会拖到分钟级)
|
||||
Map<Long, ApprovalInstance> instances = new HashMap<>();
|
||||
List<Long> instanceIds = new ArrayList<>();
|
||||
for (ApprovalExportRow row : rows) {
|
||||
instanceIds.add(row.getId());
|
||||
}
|
||||
if (!instanceIds.isEmpty()) {
|
||||
for (ApprovalInstance instance : instanceMapper.selectBatchIds(instanceIds)) {
|
||||
instances.put(instance.getId(), instance);
|
||||
}
|
||||
}
|
||||
Map<String, int[]> lineCountCache = new HashMap<>();
|
||||
Map<String, BigDecimal> amountCache = new HashMap<>();
|
||||
Map<String, String> progressCache = new HashMap<>();
|
||||
int rowNum = 1;
|
||||
for (ApprovalExportRow row : rows) {
|
||||
ApprovalInstance instance = instances.get(row.getId());
|
||||
ApprovalDocumentType type = instance == null ? null
|
||||
: documentRegistry.find(instance.getFlowCode());
|
||||
String cacheKey = type == null ? "" : type.flowCode() + ":" + row.getId();
|
||||
int lineCount = type == null ? 0
|
||||
: lineCountCache.computeIfAbsent(cacheKey, k -> new int[]{documentRegistry.lineCount(type, instance)})[0];
|
||||
BigDecimal amountTotal = type == null ? null : amountCache.computeIfAbsent(cacheKey,
|
||||
k -> documentRegistry.lineAmountTotal(type, instance));
|
||||
String progress = instance == null ? "" : progressCache.computeIfAbsent(cacheKey,
|
||||
k -> countersignProgress(instance));
|
||||
SXSSFRow dataRow = sheet.createRow(rowNum++);
|
||||
String[] values = toValues(row, nodeCache, lineCount, amountTotal, progress);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
dataRow.createCell(i).setCellValue(values[i]);
|
||||
}
|
||||
}
|
||||
return user.getAuthorities().stream()
|
||||
.map(Menu::getAuthority)
|
||||
.anyMatch(authority::equals);
|
||||
}
|
||||
|
||||
private void writeWorkbook(List<ApprovalExportRow> rows, File target) throws IOException {
|
||||
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
|
||||
try {
|
||||
SXSSFSheet sheet = workbook.createSheet("审批单据");
|
||||
for (int i = 0; i < COLUMN_WIDTHS.length; i++) {
|
||||
sheet.setColumnWidth(i, COLUMN_WIDTHS[i] * 256);
|
||||
private void fillLineSheet(SXSSFWorkbook workbook, ApprovalDocumentType type,
|
||||
List<ApprovalLineSpec> columns, List<ExportLine> lines) {
|
||||
SXSSFSheet sheet = workbook.createSheet(type.flowName());
|
||||
List<String> headers = new ArrayList<>();
|
||||
headers.add("单号");
|
||||
headers.add("状态");
|
||||
headers.add("报送部门");
|
||||
headers.add("报送人");
|
||||
for (ApprovalLineSpec column : columns) {
|
||||
headers.add(column.getLabel());
|
||||
}
|
||||
for (int i = 0; i < headers.size(); i++) {
|
||||
sheet.setColumnWidth(i, 18 * 256);
|
||||
}
|
||||
SXSSFRow header = sheet.createRow(0);
|
||||
for (int i = 0; i < headers.size(); i++) {
|
||||
header.createCell(i).setCellValue(headers.get(i));
|
||||
}
|
||||
int rowNum = 1;
|
||||
for (ExportLine exportLine : lines) {
|
||||
SXSSFRow dataRow = sheet.createRow(rowNum++);
|
||||
int col = 0;
|
||||
dataRow.createCell(col++).setCellValue(text(exportLine.instance.getBizNo()));
|
||||
dataRow.createCell(col++).setCellValue(text(ApprovalConstants.statusName(exportLine.instance.getStatus())));
|
||||
dataRow.createCell(col++).setCellValue(text(exportLine.instance.getReportOrgName()));
|
||||
dataRow.createCell(col++).setCellValue(text(exportLine.instance.getReporterName()));
|
||||
for (ApprovalLineSpec column : columns) {
|
||||
dataRow.createCell(col++).setCellValue(lineValue(exportLine.line.get(column.getKey())));
|
||||
}
|
||||
SXSSFRow header = sheet.createRow(0);
|
||||
for (int i = 0; i < HEADERS.length; i++) {
|
||||
header.createCell(i).setCellValue(HEADERS[i]);
|
||||
}
|
||||
Map<Long, List<ApprovalNode>> nodeCache = new HashMap<>();
|
||||
int rowNum = 1;
|
||||
for (ApprovalExportRow row : rows) {
|
||||
SXSSFRow dataRow = sheet.createRow(rowNum++);
|
||||
String[] values = toValues(row, nodeCache);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
dataRow.createCell(i).setCellValue(values[i]);
|
||||
}
|
||||
}
|
||||
try (OutputStream output = Files.newOutputStream(target.toPath())) {
|
||||
workbook.write(output);
|
||||
}
|
||||
} finally {
|
||||
workbook.dispose();
|
||||
workbook.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 固定列顺序:单号、单据类型、标题或名称、报送部门、报送人、受理部门、实施单位、费用(元)、
|
||||
* 服务内容或协调内容、备注、状态、当前节点、提交时间、审批完成时间、归档时间、归档人。
|
||||
* 会签进度列:仅会签节点有组时填「已签 N/M」,其余为空。
|
||||
*/
|
||||
private String[] toValues(ApprovalExportRow row, Map<Long, List<ApprovalNode>> nodeCache) {
|
||||
private String countersignProgress(ApprovalInstance instance) {
|
||||
List<ApprovalNode> nodes = flowService.parseNodes(instance.getDefinitionId());
|
||||
List<String> parts = new ArrayList<>();
|
||||
for (ApprovalNode node : nodes) {
|
||||
if (node.getGroups() == null || node.getGroups().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// 用详情装配同一套进度,避免导出与详情口径分叉
|
||||
com.gxwebsoft.approval.vo.ApprovalDetail detail = new com.gxwebsoft.approval.vo.ApprovalDetail();
|
||||
documentService.fillGroupProgress(instance, detail);
|
||||
List<com.gxwebsoft.approval.vo.ApprovalGroupProgressVO> progress = detail.getGroupProgress();
|
||||
int signedGroups = 0;
|
||||
int totalGroups = 0;
|
||||
for (com.gxwebsoft.approval.vo.ApprovalGroupProgressVO group : progress) {
|
||||
if (!node.getKey().equals(group.getNodeKey())) {
|
||||
continue;
|
||||
}
|
||||
totalGroups++;
|
||||
if (group.getSigned() >= group.getTotal() && group.getTotal() > 0) {
|
||||
signedGroups++;
|
||||
}
|
||||
}
|
||||
if (totalGroups > 0) {
|
||||
parts.add(node.getName() + " 已签 " + signedGroups + "/" + totalGroups);
|
||||
}
|
||||
}
|
||||
return String.join(";", parts);
|
||||
}
|
||||
|
||||
private String[] toValues(ApprovalExportRow row, Map<Long, List<ApprovalNode>> nodeCache,
|
||||
int lineCount, BigDecimal amountTotal, String progress) {
|
||||
boolean completion = ApprovalConstants.DOC_COMPLETION.equals(row.getDocType());
|
||||
String flowName = flowService.flowName(row.getDefinitionId(), row.getFlowCode());
|
||||
return new String[]{
|
||||
text(row.getBizNo()),
|
||||
text(ApprovalConstants.docTypeName(row.getDocType())),
|
||||
completion ? text(row.getName()) : text(row.getTitle()),
|
||||
text(flowName == null ? ApprovalConstants.docTypeName(row.getDocType()) : flowName),
|
||||
text(row.getTitle()),
|
||||
text(row.getReportOrgName()),
|
||||
text(row.getReporterName()),
|
||||
text(row.getAcceptOrgName()),
|
||||
@@ -169,10 +305,32 @@ public class ApprovalExportService {
|
||||
time(row.getSubmitTime()),
|
||||
time(row.getApproveTime()),
|
||||
time(row.getArchiveTime()),
|
||||
text(row.getArchivistName())
|
||||
text(row.getArchivistName()),
|
||||
lineCount > 0 ? String.valueOf(lineCount) : "",
|
||||
amountTotal == null ? "" : fee(amountTotal),
|
||||
text(progress)
|
||||
};
|
||||
}
|
||||
|
||||
private String lineValue(Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
if (value instanceof BigDecimal) {
|
||||
return ((BigDecimal) value).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
if (value instanceof LocalDateTime) {
|
||||
return TIME_FORMAT.format((LocalDateTime) value);
|
||||
}
|
||||
if (value instanceof java.time.LocalDate) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value ? "是" : "否";
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
private String nodeName(Map<Long, List<ApprovalNode>> cache, Long definitionId, String nodeKey) {
|
||||
if (definitionId == null || nodeKey == null) {
|
||||
return null;
|
||||
@@ -196,4 +354,21 @@ public class ApprovalExportService {
|
||||
private String text(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private boolean hasAuthority(User user, String authority) {
|
||||
if (user.getAuthorities() == null) {
|
||||
return false;
|
||||
}
|
||||
return user.getAuthorities().stream()
|
||||
.map(Menu::getAuthority)
|
||||
.anyMatch(authority::equals);
|
||||
}
|
||||
|
||||
/** 工作簿填充回调。 */
|
||||
private interface WorkbookFiller {
|
||||
void fill(SXSSFWorkbook workbook);
|
||||
}
|
||||
|
||||
@Resource
|
||||
private ApprovalDocumentService documentService;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.gxwebsoft.approval.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentRegistry;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFlowDefinition;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalFlowDefinitionMapper;
|
||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||
@@ -14,34 +15,25 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 流程定义服务:按租户幂等发布各流程的最新版本,并解析节点 JSON。
|
||||
*
|
||||
* <p>版本不可变:流程调整只新增版本行,已创建实例继续引用其 definition_id;
|
||||
* {@link #getOrCreate(String)} 始终返回最新版本。</p>
|
||||
* {@link #getOrCreate(String)} 始终返回最新版本。节点 JSON、流程名称与最新版本号
|
||||
* 全部来自 {@link ApprovalDocumentType},新增单据不需要改本类。</p>
|
||||
*/
|
||||
@Service
|
||||
public class ApprovalFlowService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApprovalFlowService.class);
|
||||
private static final int INITIAL_VERSION = 1;
|
||||
/** 各流程当前发布版本;调整节点时必须递增并保留旧版本行。 */
|
||||
private static final Map<String, Integer> LATEST_VERSIONS;
|
||||
|
||||
static {
|
||||
Map<String, Integer> versions = new LinkedHashMap<>();
|
||||
versions.put(ApprovalConstants.FLOW_COMPLETION, 1);
|
||||
versions.put(ApprovalConstants.FLOW_COORDINATION_INTERNAL, 2);
|
||||
LATEST_VERSIONS = Collections.unmodifiableMap(versions);
|
||||
}
|
||||
|
||||
@Resource
|
||||
private ApprovalFlowDefinitionMapper flowDefinitionMapper;
|
||||
@Resource
|
||||
private ApprovalDocumentRegistry documentRegistry;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
@@ -50,7 +42,8 @@ public class ApprovalFlowService {
|
||||
* <p>并发首次创建依赖唯一键 uk_flow_ver 兜底,冲突后重新读取最新版本。</p>
|
||||
*/
|
||||
public ApprovalFlowDefinition getOrCreate(String flowCode) {
|
||||
int latestVersion = latestVersion(flowCode);
|
||||
ApprovalDocumentType type = documentRegistry.require(flowCode);
|
||||
int latestVersion = type.latestVersion();
|
||||
ApprovalFlowDefinition definition = findLatest(flowCode);
|
||||
if (definition != null && definition.getVersion() >= latestVersion) {
|
||||
return definition;
|
||||
@@ -58,8 +51,8 @@ public class ApprovalFlowService {
|
||||
ApprovalFlowDefinition created = new ApprovalFlowDefinition();
|
||||
created.setFlowCode(flowCode);
|
||||
created.setVersion(latestVersion);
|
||||
created.setName(flowName(flowCode));
|
||||
created.setNodesJson(flowNodesJson(flowCode));
|
||||
created.setName(type.flowName());
|
||||
created.setNodesJson(type.nodesJson());
|
||||
try {
|
||||
flowDefinitionMapper.insert(created);
|
||||
return created;
|
||||
@@ -107,35 +100,21 @@ public class ApprovalFlowService {
|
||||
return node == null ? null : node.getName();
|
||||
}
|
||||
|
||||
/** 流程定义名称(单据类型显示名,ADR 0007);定义缺失时回退实现名称。 */
|
||||
public String flowName(Long definitionId, String flowCode) {
|
||||
ApprovalFlowDefinition definition =
|
||||
definitionId == null ? null : flowDefinitionMapper.selectById(definitionId);
|
||||
if (definition != null && definition.getName() != null) {
|
||||
return definition.getName();
|
||||
}
|
||||
ApprovalDocumentType type = documentRegistry.find(flowCode);
|
||||
return type == null ? null : type.flowName();
|
||||
}
|
||||
|
||||
private ApprovalFlowDefinition findLatest(String flowCode) {
|
||||
return flowDefinitionMapper.selectOne(new LambdaQueryWrapper<ApprovalFlowDefinition>()
|
||||
.eq(ApprovalFlowDefinition::getFlowCode, flowCode)
|
||||
.orderByDesc(ApprovalFlowDefinition::getVersion)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private int latestVersion(String flowCode) {
|
||||
Integer version = LATEST_VERSIONS.get(flowCode);
|
||||
return version == null ? INITIAL_VERSION : version;
|
||||
}
|
||||
|
||||
private String flowName(String flowCode) {
|
||||
if (ApprovalConstants.FLOW_COMPLETION.equals(flowCode)) {
|
||||
return "业务完成确认单";
|
||||
}
|
||||
if (ApprovalConstants.FLOW_COORDINATION_INTERNAL.equals(flowCode)) {
|
||||
return "内部协调单";
|
||||
}
|
||||
throw new BusinessException("未知的审批流程: " + flowCode);
|
||||
}
|
||||
|
||||
private String flowNodesJson(String flowCode) {
|
||||
if (ApprovalConstants.FLOW_COMPLETION.equals(flowCode)) {
|
||||
return ApprovalConstants.COMPLETION_NODES_JSON;
|
||||
}
|
||||
if (ApprovalConstants.FLOW_COORDINATION_INTERNAL.equals(flowCode)) {
|
||||
return ApprovalConstants.COORDINATION_NODES_JSON;
|
||||
}
|
||||
throw new BusinessException("未知的审批流程: " + flowCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,26 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.entity.ApprovalCompletionForm;
|
||||
import com.gxwebsoft.approval.entity.ApprovalCoordinationForm;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFlowDefinition;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentRegistry;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFieldChange;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.entity.ApprovalRecord;
|
||||
import com.gxwebsoft.approval.entity.ApprovalTask;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalCompletionFormMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalCoordinationFormMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalFieldChangeMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalRecordMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalTaskMapper;
|
||||
import com.gxwebsoft.approval.param.ApprovalCompletionParam;
|
||||
import com.gxwebsoft.approval.param.ApprovalCoordinationParam;
|
||||
import com.gxwebsoft.approval.param.ApprovalMineParam;
|
||||
import com.gxwebsoft.approval.param.ApprovalTaskListParam;
|
||||
import com.gxwebsoft.approval.support.ApprovalApprover;
|
||||
import com.gxwebsoft.approval.support.ApprovalItemRow;
|
||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||
import com.gxwebsoft.approval.vo.ApprovalConfirmVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import com.gxwebsoft.approval.vo.ApprovalFieldChangeVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalFieldVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalInstanceVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalItemVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalMyTaskVO;
|
||||
@@ -30,7 +31,7 @@ import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalPendingTaskVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalPermissionsVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalRecordVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalUserVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalWritableFieldVO;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.system.entity.Menu;
|
||||
@@ -41,17 +42,15 @@ import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 业务完成确认单草稿、提交与详情服务。
|
||||
* 审批实例服务:草稿保存入口、提交预检、详情装配与列表查询。
|
||||
*
|
||||
* <p>跨库约束:服务方法本身不开启事务;需要写 master 的片段放进
|
||||
* TransactionTemplate,任何 @DS("server") 的目录查询都在事务外完成,
|
||||
@@ -60,7 +59,6 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class ApprovalInstanceService {
|
||||
|
||||
private static final BigDecimal MAX_FEE = new BigDecimal("99999999.99");
|
||||
private static final String PERMISSION_ASSIGN = ApprovalConstants.PERMISSION_ASSIGN;
|
||||
/** 待办范围:全租户待办(仅 assign 权限可用)。 */
|
||||
private static final String SCOPE_ALL = "all";
|
||||
@@ -68,64 +66,35 @@ public class ApprovalInstanceService {
|
||||
@Resource
|
||||
private ApprovalInstanceMapper instanceMapper;
|
||||
@Resource
|
||||
private ApprovalCompletionFormMapper completionFormMapper;
|
||||
@Resource
|
||||
private ApprovalCoordinationFormMapper coordinationFormMapper;
|
||||
@Resource
|
||||
private ApprovalRecordMapper recordMapper;
|
||||
@Resource
|
||||
private ApprovalTaskMapper taskMapper;
|
||||
@Resource
|
||||
private ApprovalFieldChangeMapper fieldChangeMapper;
|
||||
@Resource
|
||||
private ApprovalFlowService flowService;
|
||||
@Resource
|
||||
private ApprovalNoService noService;
|
||||
@Resource
|
||||
private ApprovalEngineService engineService;
|
||||
@Resource
|
||||
private ApprovalAttachmentService attachmentService;
|
||||
private ApprovalDocumentService documentService;
|
||||
@Resource
|
||||
private ApprovalDocumentRegistry documentRegistry;
|
||||
@Resource
|
||||
private ApprovalDirectoryService directoryService;
|
||||
@Resource
|
||||
private ApprovalAttachmentService attachmentService;
|
||||
@Resource
|
||||
private ApprovalApproverResolver approverResolver;
|
||||
@Resource
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
/**
|
||||
* 新建或更新业务完成确认单草稿,返回实例id。
|
||||
* 新建或更新任意单据草稿,返回实例 id。
|
||||
*/
|
||||
public Long saveCompletion(ApprovalCompletionParam param, User user) {
|
||||
validateCompletion(param);
|
||||
Integer organizationId = user.getOrganizationId();
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganization(organizationId);
|
||||
String organizationName = organization == null ? null : organization.getOrganizationName();
|
||||
|
||||
if (param.getId() != null) {
|
||||
return inTransaction(() -> updateDraft(param, user, organizationId, organizationName));
|
||||
}
|
||||
ApprovalFlowDefinition definition = flowService.getOrCreate(ApprovalConstants.FLOW_COMPLETION);
|
||||
return inTransaction(() -> createDraft(param, user, definition, organizationId, organizationName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建或更新内部协调单草稿,返回实例id。
|
||||
*
|
||||
* <p>报送部门与报送人取登录态;受理部门必须是启用组织,保存时即校验。</p>
|
||||
*/
|
||||
public Long saveCoordination(ApprovalCoordinationParam param, User user) {
|
||||
validateCoordination(param);
|
||||
ApprovalOrganizationVO acceptOrganization = directoryService.getOrganization(param.getAcceptOrgId());
|
||||
if (acceptOrganization == null) {
|
||||
throw new BusinessException("受理部门不存在或已停用");
|
||||
}
|
||||
Integer organizationId = user.getOrganizationId();
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganization(organizationId);
|
||||
String organizationName = organization == null ? null : organization.getOrganizationName();
|
||||
|
||||
if (param.getId() != null) {
|
||||
return inTransaction(() ->
|
||||
updateCoordinationDraft(param, user, organizationId, organizationName, acceptOrganization));
|
||||
}
|
||||
ApprovalFlowDefinition definition = flowService.getOrCreate(ApprovalConstants.FLOW_COORDINATION_INTERNAL);
|
||||
return inTransaction(() ->
|
||||
createCoordinationDraft(param, user, definition, organizationId, organizationName, acceptOrganization));
|
||||
public Long save(String flowCode, ApprovalDocumentParam param, User user) {
|
||||
return documentService.save(flowCode, param, user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,15 +111,9 @@ public class ApprovalInstanceService {
|
||||
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
|
||||
throw new BusinessException(submitBlockedMessage(instance.getStatus()));
|
||||
}
|
||||
boolean coordination = ApprovalConstants.DOC_COORDINATION.equals(instance.getDocType());
|
||||
ApprovalCoordinationForm coordinationForm = null;
|
||||
if (coordination) {
|
||||
coordinationForm = coordinationForm(instanceId);
|
||||
if (coordinationForm == null) {
|
||||
throw new BusinessException("单据内容不存在");
|
||||
}
|
||||
} else if (completionForm(instanceId) == null) {
|
||||
throw new BusinessException("单据内容不存在");
|
||||
ApprovalDocumentType type = documentRegistry.find(instance.getFlowCode());
|
||||
if (type == null) {
|
||||
throw new BusinessException("单据类型不支持");
|
||||
}
|
||||
|
||||
Integer organizationId = user.getOrganizationId();
|
||||
@@ -165,76 +128,81 @@ public class ApprovalInstanceService {
|
||||
if (leaderRoleCode == null) {
|
||||
throw new BusinessException("部门未配置编码,无法提交");
|
||||
}
|
||||
List<ApprovalUserVO> leaders = directoryService.listRoleMembers(leaderRoleCode);
|
||||
if (leaders.isEmpty()) {
|
||||
if (directoryService.listRoleMembers(leaderRoleCode).isEmpty()) {
|
||||
throw new BusinessException("报送部门负责人角色缺配");
|
||||
}
|
||||
ApprovalOrganizationVO acceptOrganization = null;
|
||||
if (coordination) {
|
||||
acceptOrganization = preflightAcceptOrganization(coordinationForm);
|
||||
}
|
||||
if (directoryService.listRoleMembers(ApprovalConstants.ROLE_ARCHIVIST).isEmpty()) {
|
||||
throw new BusinessException("收文员角色缺配");
|
||||
}
|
||||
if (attachmentService.count(instanceId) == 0) {
|
||||
throw new BusinessException("请至少上传一张附件");
|
||||
}
|
||||
// 协调单的受理部门在提交时快照到实例;外部流程的分管领导按受理部门解析
|
||||
ApprovalOrganizationVO acceptOrganization = preflightAcceptOrganization(type, instance);
|
||||
// 附件分类与逐节点逐组审批人解析(ADR 0003 / 0005)
|
||||
documentService.preflight(instance, flowService.parseNodes(instance.getDefinitionId()));
|
||||
|
||||
List<ApprovalApprover> approvers = leaders.stream()
|
||||
.map(this::toApprover)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ApprovalOrganizationVO acceptOrg = acceptOrganization;
|
||||
return inTransaction(() -> doSubmit(instanceId, user, organization, acceptOrg, approvers));
|
||||
return inTransaction(() -> doSubmit(instanceId, user, organization, acceptOrganization, type));
|
||||
}
|
||||
|
||||
/**
|
||||
* 协调单提交预检:受理部门负责人角色、受理部门停用。
|
||||
* 协调单提交预检:受理部门仍启用且负责人角色有成员;返回受理部门供提交时快照。
|
||||
*/
|
||||
private ApprovalOrganizationVO preflightAcceptOrganization(ApprovalCoordinationForm form) {
|
||||
Integer acceptOrgId = form.getAcceptOrgId();
|
||||
if (acceptOrgId == null || acceptOrgId <= 0) {
|
||||
throw new BusinessException("请选择受理部门");
|
||||
private ApprovalOrganizationVO preflightAcceptOrganization(ApprovalDocumentType type,
|
||||
ApprovalInstance instance) {
|
||||
if (!ApprovalConstants.DOC_COORDINATION.equals(type.docType())) {
|
||||
return null;
|
||||
}
|
||||
ApprovalOrganizationVO acceptOrganization = directoryService.getOrganization(acceptOrgId);
|
||||
if (acceptOrganization == null) {
|
||||
ApprovalCoordinationHolder holder = coordinationAcceptOrg(instance.getId());
|
||||
if (holder == null) {
|
||||
throw new BusinessException("单据内容不存在");
|
||||
}
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganization(holder.acceptOrgId);
|
||||
if (organization == null) {
|
||||
throw new BusinessException("受理部门已停用");
|
||||
}
|
||||
String leaderRoleCode = ApprovalConstants.departmentLeaderRoleCode(acceptOrganization.getOrganizationCode());
|
||||
String leaderRoleCode = ApprovalConstants.departmentLeaderRoleCode(organization.getOrganizationCode());
|
||||
if (leaderRoleCode == null) {
|
||||
throw new BusinessException("受理部门未配置编码");
|
||||
}
|
||||
if (directoryService.listRoleMembers(leaderRoleCode).isEmpty()) {
|
||||
throw new BusinessException("受理部门负责人角色缺配");
|
||||
}
|
||||
return acceptOrganization;
|
||||
return organization;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读协调单业务表的受理部门 id(仅协调单需要,走 engine 侧 master 查询)。
|
||||
*/
|
||||
private ApprovalCoordinationHolder coordinationAcceptOrg(Long instanceId) {
|
||||
var row = instanceMapper.selectCoordinationAcceptOrg(instanceId);
|
||||
if (row == null || row.get("accept_org_id") == null) {
|
||||
return null;
|
||||
}
|
||||
return new ApprovalCoordinationHolder(((Number) row.get("accept_org_id")).intValue());
|
||||
}
|
||||
|
||||
private String doSubmit(Long instanceId, User user, ApprovalOrganizationVO organization,
|
||||
ApprovalOrganizationVO acceptOrganization, List<ApprovalApprover> approvers) {
|
||||
ApprovalOrganizationVO acceptOrganization, ApprovalDocumentType type) {
|
||||
ApprovalInstance locked = engineService.lockInstance(instanceId);
|
||||
if (!locked.getReporterId().equals(user.getUserId())
|
||||
|| !ApprovalConstants.EDITABLE_STATUSES.contains(locked.getStatus())) {
|
||||
throw new BusinessException("单据状态已变化,请刷新后重试");
|
||||
}
|
||||
boolean firstSubmit = StrUtil.isBlank(locked.getBizNo());
|
||||
locked.setReportOrgId(organization.getOrganizationId());
|
||||
locked.setReportOrgName(organization.getOrganizationName());
|
||||
if (acceptOrganization != null) {
|
||||
locked.setAcceptOrgId(acceptOrganization.getOrganizationId());
|
||||
locked.setAcceptOrgName(acceptOrganization.getOrganizationName());
|
||||
}
|
||||
boolean firstSubmit = StrUtil.isBlank(locked.getBizNo());
|
||||
if (firstSubmit) {
|
||||
boolean coordination = ApprovalConstants.DOC_COORDINATION.equals(locked.getDocType());
|
||||
locked.setBizNo(noService.nextNo(coordination
|
||||
? ApprovalConstants.NO_PREFIX_COORDINATION
|
||||
: ApprovalConstants.NO_PREFIX_COMPLETION));
|
||||
locked.setBizNo(noService.nextNo(type.noPrefix()));
|
||||
}
|
||||
locked.setStatus(ApprovalConstants.STATUS_PENDING);
|
||||
locked.setRound((locked.getRound() == null ? 0 : locked.getRound()) + 1);
|
||||
if (locked.getSubmitTime() == null) {
|
||||
locked.setSubmitTime(LocalDateTime.now());
|
||||
}
|
||||
List<ApprovalNode> nodes = flowService.parseNodes(locked.getDefinitionId());
|
||||
if (nodes.isEmpty()) {
|
||||
throw new BusinessException("审批流程未配置节点");
|
||||
}
|
||||
List<ApprovalApprover> approvers = approverResolver.resolve(locked, nodes.get(0)).getApprovers();
|
||||
int tasks = engineService.startFlow(locked, approvers, user,
|
||||
firstSubmit ? ApprovalConstants.ACTION_SUBMIT : ApprovalConstants.ACTION_RESUBMIT);
|
||||
if (tasks == 0) {
|
||||
@@ -260,33 +228,108 @@ public class ApprovalInstanceService {
|
||||
}
|
||||
|
||||
ApprovalInstanceVO vo = baseVO(instance);
|
||||
ApprovalCompletionForm completion = completionForm(instanceId);
|
||||
if (completion != null) {
|
||||
vo.setName(completion.getName());
|
||||
vo.setImplementOrg(completion.getImplementOrg());
|
||||
vo.setFee(completion.getFee());
|
||||
vo.setServiceContent(completion.getServiceContent());
|
||||
}
|
||||
ApprovalCoordinationForm coordination = coordinationForm(instanceId);
|
||||
if (coordination != null) {
|
||||
vo.setAcceptOrgId(coordination.getAcceptOrgId());
|
||||
vo.setAcceptOrgName(coordination.getAcceptOrgName());
|
||||
vo.setTitle(coordination.getTitle());
|
||||
vo.setContent(coordination.getContent());
|
||||
vo.setRemark(coordination.getRemark());
|
||||
ApprovalDocumentType type = documentRegistry.find(instance.getFlowCode());
|
||||
ApprovalDetail document = new ApprovalDetail();
|
||||
if (type != null) {
|
||||
type.load(instance, document);
|
||||
if (type.lineColumns() != null) {
|
||||
document.setLineColumns(type.lineColumns());
|
||||
document.setLineSummary(lineSummary(type, instance));
|
||||
}
|
||||
documentService.fillGroupProgress(instance, document);
|
||||
applyLegacyFields(vo, document);
|
||||
}
|
||||
vo.setDocument(document);
|
||||
vo.setAttachments(attachmentService.listVos(instanceId));
|
||||
vo.setRecords(records(instance));
|
||||
vo.setConfirms(confirms(instance));
|
||||
vo.setFieldChanges(fieldChanges(instance));
|
||||
ApprovalMyTaskVO myTask = myTask(instance, user.getUserId());
|
||||
vo.setMyTask(myTask);
|
||||
vo.setPendingTasks(pendingTasks(instance));
|
||||
vo.setPermissions(permissions(instance, user, myTask));
|
||||
vo.setPermissions(permissions(instance, user, myTask, type));
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 我发起的列表:按提交时间倒序,草稿排最后。
|
||||
* 兼容一期的扁平字段:详情仍返回 name/implementOrg/fee/serviceContent/title/content/remark,
|
||||
* 两端既有页面与用例不用改;新页面统一读 `document.fields`。
|
||||
*/
|
||||
private void applyLegacyFields(ApprovalInstanceVO vo, ApprovalDetail document) {
|
||||
for (ApprovalFieldVO field : document.getFields()) {
|
||||
switch (field.getKey()) {
|
||||
case "name":
|
||||
vo.setName(field.getValue());
|
||||
break;
|
||||
case "implementOrg":
|
||||
vo.setImplementOrg(field.getValue());
|
||||
break;
|
||||
case "fee":
|
||||
vo.setFee(field.getValue() == null ? null : new java.math.BigDecimal(field.getValue()));
|
||||
break;
|
||||
case "serviceContent":
|
||||
vo.setServiceContent(field.getValue());
|
||||
break;
|
||||
case "title":
|
||||
vo.setTitle(field.getValue());
|
||||
break;
|
||||
case "content":
|
||||
vo.setContent(field.getValue());
|
||||
break;
|
||||
case "remark":
|
||||
vo.setRemark(field.getValue());
|
||||
break;
|
||||
case "acceptOrgName":
|
||||
vo.setAcceptOrgName(field.getValue());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 明细汇总:行数 + 合计金额(金额由查询时 SUM,不落库)。
|
||||
*/
|
||||
private com.gxwebsoft.approval.vo.ApprovalLineSummaryVO lineSummary(ApprovalDocumentType type,
|
||||
ApprovalInstance instance) {
|
||||
var summary = new com.gxwebsoft.approval.vo.ApprovalLineSummaryVO();
|
||||
summary.setLineCount(type.lineCount(instance));
|
||||
summary.setAmountTotal(type.lineAmountTotal(instance));
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户可回填字段:仅当他是当前节点处理人且该节点声明了可写字段时非空(ADR 0006)。
|
||||
*/
|
||||
private List<ApprovalWritableFieldVO> writableFields(ApprovalInstance instance, ApprovalDocumentType type,
|
||||
ApprovalMyTaskVO myTask) {
|
||||
List<ApprovalWritableFieldVO> result = new ArrayList<>();
|
||||
if (myTask == null || type == null) {
|
||||
return result;
|
||||
}
|
||||
ApprovalNode node = flowService.findNode(instance.getDefinitionId(), myTask.getNodeKey());
|
||||
if (node == null || node.getWritableFields() == null || node.getWritableFields().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
Map<String, com.gxwebsoft.approval.support.ApprovalFieldSpec> specs = new HashMap<>();
|
||||
for (var spec : type.fieldSpecs()) {
|
||||
specs.put(spec.getKey(), spec);
|
||||
}
|
||||
for (ApprovalNode.WritableField writable : node.getWritableFields()) {
|
||||
ApprovalWritableFieldVO vo = new ApprovalWritableFieldVO();
|
||||
vo.setField(writable.getField());
|
||||
var spec = specs.get(writable.getField());
|
||||
vo.setLabel(spec == null ? writable.getField() : spec.getLabel());
|
||||
vo.setRequired(writable.isRequired());
|
||||
vo.setValue(type.readField(instance, writable.getField()));
|
||||
result.add(vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 我发起的列表。
|
||||
*/
|
||||
public PageResult<ApprovalItemVO> mine(ApprovalMineParam param, User user) {
|
||||
long current = param.getPage() == null ? 1 : Math.max(1, param.getPage());
|
||||
@@ -319,9 +362,6 @@ public class ApprovalInstanceService {
|
||||
return taskPage(param, user, ApprovalConstants.TASK_DONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全租户待办:任一 PENDING 任务对应的实例,按实例去重。
|
||||
*/
|
||||
private PageResult<ApprovalItemVO> pendingPage(ApprovalTaskListParam param) {
|
||||
long current = param.getPage() == null ? 1 : Math.max(1, param.getPage());
|
||||
long size = param.getLimit() == null ? 10 : Math.max(1, param.getLimit());
|
||||
@@ -340,13 +380,16 @@ public class ApprovalInstanceService {
|
||||
|
||||
private List<ApprovalItemVO> toItems(List<ApprovalItemRow> rows) {
|
||||
Map<Long, List<ApprovalNode>> nodeCache = new HashMap<>();
|
||||
Map<Long, String> flowNameCache = new HashMap<>();
|
||||
List<ApprovalItemVO> list = new ArrayList<>();
|
||||
for (ApprovalItemRow row : rows) {
|
||||
ApprovalItemVO item = new ApprovalItemVO();
|
||||
item.setId(row.getId());
|
||||
item.setBizNo(row.getBizNo());
|
||||
item.setDocType(row.getDocType());
|
||||
item.setDocTypeName(ApprovalConstants.docTypeName(row.getDocType()));
|
||||
item.setDocTypeName(flowNameCache.computeIfAbsent(row.getDefinitionId(),
|
||||
id -> flowService.flowName(id, row.getFlowCode())));
|
||||
item.setFlowCode(row.getFlowCode());
|
||||
item.setStatus(row.getStatus());
|
||||
item.setStatusName(ApprovalConstants.statusName(row.getStatus()));
|
||||
item.setTitle(row.getTitle());
|
||||
@@ -362,142 +405,12 @@ public class ApprovalInstanceService {
|
||||
return list;
|
||||
}
|
||||
|
||||
private Long createDraft(ApprovalCompletionParam param, User user, ApprovalFlowDefinition definition,
|
||||
Integer organizationId, String organizationName) {
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setFlowCode(ApprovalConstants.FLOW_COMPLETION);
|
||||
instance.setDefinitionId(definition.getId());
|
||||
instance.setDefinitionVersion(definition.getVersion());
|
||||
instance.setDocType(ApprovalConstants.DOC_COMPLETION);
|
||||
instance.setBizId(0L);
|
||||
instance.setStatus(ApprovalConstants.STATUS_DRAFT);
|
||||
instance.setRound(0);
|
||||
instance.setReporterId(user.getUserId());
|
||||
instance.setReporterName(displayName(user));
|
||||
instance.setReportOrgId(organizationId);
|
||||
instance.setReportOrgName(organizationName);
|
||||
instanceMapper.insert(instance);
|
||||
|
||||
ApprovalCompletionForm form = new ApprovalCompletionForm();
|
||||
form.setInstanceId(instance.getId());
|
||||
form.setName(param.getName().trim());
|
||||
form.setImplementOrg(param.getImplementOrg().trim());
|
||||
form.setFee(param.getFee());
|
||||
form.setServiceContent(param.getServiceContent().trim());
|
||||
completionFormMapper.insert(form);
|
||||
|
||||
instance.setBizId(form.getId());
|
||||
instanceMapper.updateById(instance);
|
||||
return instance.getId();
|
||||
}
|
||||
|
||||
private Long updateDraft(ApprovalCompletionParam param, User user,
|
||||
Integer organizationId, String organizationName) {
|
||||
ApprovalInstance instance = instanceMapper.selectById(param.getId());
|
||||
if (instance == null) {
|
||||
throw new BusinessException("单据不存在");
|
||||
}
|
||||
if (!instance.getReporterId().equals(user.getUserId())) {
|
||||
throw new BusinessException("无权操作该单据");
|
||||
}
|
||||
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
|
||||
throw new BusinessException(ApprovalConstants.writeBlockedMessage(instance.getStatus()));
|
||||
}
|
||||
ApprovalCompletionForm form = completionForm(instance.getId());
|
||||
if (form == null) {
|
||||
throw new BusinessException("单据内容不存在");
|
||||
}
|
||||
form.setName(param.getName().trim());
|
||||
form.setImplementOrg(param.getImplementOrg().trim());
|
||||
form.setFee(param.getFee());
|
||||
form.setServiceContent(param.getServiceContent().trim());
|
||||
completionFormMapper.updateById(form);
|
||||
|
||||
instance.setReportOrgId(organizationId);
|
||||
instance.setReportOrgName(organizationName);
|
||||
instanceMapper.updateById(instance);
|
||||
return instance.getId();
|
||||
}
|
||||
|
||||
private Long createCoordinationDraft(ApprovalCoordinationParam param, User user,
|
||||
ApprovalFlowDefinition definition,
|
||||
Integer organizationId, String organizationName,
|
||||
ApprovalOrganizationVO acceptOrganization) {
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setFlowCode(ApprovalConstants.FLOW_COORDINATION_INTERNAL);
|
||||
instance.setDefinitionId(definition.getId());
|
||||
instance.setDefinitionVersion(definition.getVersion());
|
||||
instance.setDocType(ApprovalConstants.DOC_COORDINATION);
|
||||
instance.setBizId(0L);
|
||||
instance.setStatus(ApprovalConstants.STATUS_DRAFT);
|
||||
instance.setRound(0);
|
||||
instance.setReporterId(user.getUserId());
|
||||
instance.setReporterName(displayName(user));
|
||||
instance.setReportOrgId(organizationId);
|
||||
instance.setReportOrgName(organizationName);
|
||||
instanceMapper.insert(instance);
|
||||
|
||||
ApprovalCoordinationForm form = new ApprovalCoordinationForm();
|
||||
form.setInstanceId(instance.getId());
|
||||
form.setAcceptOrgId(acceptOrganization.getOrganizationId());
|
||||
form.setAcceptOrgName(acceptOrganization.getOrganizationName());
|
||||
form.setTitle(param.getTitle().trim());
|
||||
form.setContent(param.getContent().trim());
|
||||
form.setRemark(normalizeRemark(param.getRemark()));
|
||||
coordinationFormMapper.insert(form);
|
||||
|
||||
instance.setBizId(form.getId());
|
||||
instanceMapper.updateById(instance);
|
||||
return instance.getId();
|
||||
}
|
||||
|
||||
private Long updateCoordinationDraft(ApprovalCoordinationParam param, User user,
|
||||
Integer organizationId, String organizationName,
|
||||
ApprovalOrganizationVO acceptOrganization) {
|
||||
ApprovalInstance instance = instanceMapper.selectById(param.getId());
|
||||
if (instance == null) {
|
||||
throw new BusinessException("单据不存在");
|
||||
}
|
||||
if (!ApprovalConstants.DOC_COORDINATION.equals(instance.getDocType())) {
|
||||
throw new BusinessException("单据类型不匹配");
|
||||
}
|
||||
if (!instance.getReporterId().equals(user.getUserId())) {
|
||||
throw new BusinessException("无权操作该单据");
|
||||
}
|
||||
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
|
||||
throw new BusinessException("当前状态不可修改");
|
||||
}
|
||||
ApprovalCoordinationForm form = coordinationForm(instance.getId());
|
||||
if (form == null) {
|
||||
throw new BusinessException("单据内容不存在");
|
||||
}
|
||||
form.setAcceptOrgId(acceptOrganization.getOrganizationId());
|
||||
form.setAcceptOrgName(acceptOrganization.getOrganizationName());
|
||||
form.setTitle(param.getTitle().trim());
|
||||
form.setContent(param.getContent().trim());
|
||||
form.setRemark(normalizeRemark(param.getRemark()));
|
||||
coordinationFormMapper.updateById(form);
|
||||
|
||||
instance.setReportOrgId(organizationId);
|
||||
instance.setReportOrgName(organizationName);
|
||||
instanceMapper.updateById(instance);
|
||||
return instance.getId();
|
||||
}
|
||||
|
||||
private String normalizeRemark(String remark) {
|
||||
if (remark == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = remark.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private ApprovalInstanceVO baseVO(ApprovalInstance instance) {
|
||||
ApprovalInstanceVO vo = new ApprovalInstanceVO();
|
||||
vo.setId(instance.getId());
|
||||
vo.setBizNo(instance.getBizNo());
|
||||
vo.setDocType(instance.getDocType());
|
||||
vo.setDocTypeName(ApprovalConstants.docTypeName(instance.getDocType()));
|
||||
vo.setDocTypeName(flowService.flowName(instance.getDefinitionId(), instance.getFlowCode()));
|
||||
vo.setFlowCode(instance.getFlowCode());
|
||||
vo.setStatus(instance.getStatus());
|
||||
vo.setStatusName(ApprovalConstants.statusName(instance.getStatus()));
|
||||
@@ -545,6 +458,30 @@ public class ApprovalInstanceService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ApprovalFieldChangeVO> fieldChanges(ApprovalInstance instance) {
|
||||
List<ApprovalFieldChange> changes = fieldChangeMapper.selectList(
|
||||
new LambdaQueryWrapper<ApprovalFieldChange>()
|
||||
.eq(ApprovalFieldChange::getInstanceId, instance.getId())
|
||||
.orderByAsc(ApprovalFieldChange::getCreateTime)
|
||||
.orderByAsc(ApprovalFieldChange::getId));
|
||||
List<ApprovalFieldChangeVO> result = new ArrayList<>(changes.size());
|
||||
for (ApprovalFieldChange change : changes) {
|
||||
ApprovalFieldChangeVO vo = new ApprovalFieldChangeVO();
|
||||
vo.setId(change.getId());
|
||||
vo.setNodeKey(change.getNodeKey());
|
||||
vo.setNodeName(flowService.nodeName(instance.getDefinitionId(), change.getNodeKey()));
|
||||
vo.setFieldKey(change.getFieldKey());
|
||||
vo.setFieldLabel(change.getFieldLabel());
|
||||
vo.setOldValue(change.getOldValue());
|
||||
vo.setNewValue(change.getNewValue());
|
||||
vo.setOperatorName(change.getOperatorName());
|
||||
vo.setRound(change.getRound());
|
||||
vo.setCreateTime(change.getCreateTime());
|
||||
result.add(vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ApprovalConfirmVO> confirms(ApprovalInstance instance) {
|
||||
List<ApprovalTask> tasks = taskMapper.selectList(new LambdaQueryWrapper<ApprovalTask>()
|
||||
.eq(ApprovalTask::getInstanceId, instance.getId())
|
||||
@@ -556,6 +493,8 @@ public class ApprovalInstanceService {
|
||||
ApprovalConfirmVO vo = new ApprovalConfirmVO();
|
||||
vo.setUserId(task.getAssigneeId());
|
||||
vo.setUserName(task.getAssigneeName());
|
||||
vo.setGroupKey(task.getGroupKey());
|
||||
vo.setGroupName(task.getGroupName());
|
||||
vo.setStatus(task.getStatus());
|
||||
vo.setStatusName(taskStatusName(task.getStatus()));
|
||||
vo.setHandleTime(task.getHandleTime());
|
||||
@@ -582,6 +521,8 @@ public class ApprovalInstanceService {
|
||||
vo.setId(task.getId());
|
||||
vo.setNodeKey(task.getNodeKey());
|
||||
vo.setNodeType(task.getNodeType());
|
||||
vo.setGroupKey(task.getGroupKey());
|
||||
vo.setGroupName(task.getGroupName());
|
||||
vo.setStatus(task.getStatus());
|
||||
return vo;
|
||||
}
|
||||
@@ -601,6 +542,8 @@ public class ApprovalInstanceService {
|
||||
vo.setNodeKey(task.getNodeKey());
|
||||
vo.setNodeName(flowService.nodeName(instance.getDefinitionId(), task.getNodeKey()));
|
||||
vo.setNodeType(task.getNodeType());
|
||||
vo.setGroupKey(task.getGroupKey());
|
||||
vo.setGroupName(task.getGroupName());
|
||||
vo.setAssigneeId(task.getAssigneeId());
|
||||
vo.setAssigneeName(task.getAssigneeName());
|
||||
result.add(vo);
|
||||
@@ -608,7 +551,8 @@ public class ApprovalInstanceService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private ApprovalPermissionsVO permissions(ApprovalInstance instance, User user, ApprovalMyTaskVO myTask) {
|
||||
private ApprovalPermissionsVO permissions(ApprovalInstance instance, User user, ApprovalMyTaskVO myTask,
|
||||
ApprovalDocumentType type) {
|
||||
ApprovalPermissionsVO permissions = new ApprovalPermissionsVO();
|
||||
boolean reporter = instance.getReporterId().equals(user.getUserId());
|
||||
boolean editable = reporter && ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus());
|
||||
@@ -625,12 +569,17 @@ public class ApprovalInstanceService {
|
||||
&& ApprovalConstants.STATUS_PENDING.equals(instance.getStatus()));
|
||||
if (myTask != null) {
|
||||
boolean approveNode = ApprovalConstants.NODE_APPROVE.equals(myTask.getNodeType());
|
||||
boolean rejectableNode = approveNode || ApprovalConstants.NODE_DISPATCH.equals(myTask.getNodeType());
|
||||
permissions.setCanApprove(approveNode);
|
||||
boolean countersignNode = ApprovalConstants.NODE_COUNTERSIGN.equals(myTask.getNodeType());
|
||||
boolean rejectableNode = approveNode || countersignNode
|
||||
|| ApprovalConstants.NODE_DISPATCH.equals(myTask.getNodeType());
|
||||
permissions.setCanApprove(approveNode || countersignNode);
|
||||
permissions.setCanReject(rejectableNode);
|
||||
permissions.setCanDispatch(ApprovalConstants.NODE_DISPATCH.equals(myTask.getNodeType()));
|
||||
permissions.setCanConfirm(ApprovalConstants.NODE_CONFIRM.equals(myTask.getNodeType()));
|
||||
permissions.setCanArchive(ApprovalConstants.NODE_ARCHIVE.equals(myTask.getNodeType()));
|
||||
permissions.setWritableFields(writableFields(instance, type, myTask));
|
||||
} else {
|
||||
permissions.setWritableFields(new ArrayList<>());
|
||||
}
|
||||
return permissions;
|
||||
}
|
||||
@@ -644,129 +593,6 @@ public class ApprovalInstanceService {
|
||||
.anyMatch(authority::equals);
|
||||
}
|
||||
|
||||
private ApprovalCompletionForm completionForm(Long instanceId) {
|
||||
List<ApprovalCompletionForm> forms = completionFormMapper.selectList(
|
||||
new LambdaQueryWrapper<ApprovalCompletionForm>()
|
||||
.eq(ApprovalCompletionForm::getInstanceId, instanceId)
|
||||
.last("LIMIT 1"));
|
||||
return forms.isEmpty() ? null : forms.get(0);
|
||||
}
|
||||
|
||||
private ApprovalCoordinationForm coordinationForm(Long instanceId) {
|
||||
List<ApprovalCoordinationForm> forms = coordinationFormMapper.selectList(
|
||||
new LambdaQueryWrapper<ApprovalCoordinationForm>()
|
||||
.eq(ApprovalCoordinationForm::getInstanceId, instanceId)
|
||||
.last("LIMIT 1"));
|
||||
return forms.isEmpty() ? null : forms.get(0);
|
||||
}
|
||||
|
||||
private void validateCompletion(ApprovalCompletionParam param) {
|
||||
if (param == null) {
|
||||
throw new BusinessException("参数不能为空");
|
||||
}
|
||||
String name = param.getName() == null ? "" : param.getName().trim();
|
||||
if (name.isEmpty()) {
|
||||
throw new BusinessException("名称不能为空");
|
||||
}
|
||||
if (name.length() > 100) {
|
||||
throw new BusinessException("名称不能超过100字");
|
||||
}
|
||||
String implementOrg = param.getImplementOrg() == null ? "" : param.getImplementOrg().trim();
|
||||
if (implementOrg.isEmpty()) {
|
||||
throw new BusinessException("实施单位不能为空");
|
||||
}
|
||||
if (implementOrg.length() > 100) {
|
||||
throw new BusinessException("实施单位不能超过100字");
|
||||
}
|
||||
BigDecimal fee = param.getFee();
|
||||
if (fee == null) {
|
||||
throw new BusinessException("费用不能为空");
|
||||
}
|
||||
if (fee.scale() > 2 || fee.compareTo(BigDecimal.ZERO) < 0 || fee.compareTo(MAX_FEE) > 0) {
|
||||
throw new BusinessException("费用必须为0到99999999.99之间的两位小数");
|
||||
}
|
||||
String serviceContent = param.getServiceContent() == null ? "" : param.getServiceContent().trim();
|
||||
if (serviceContent.isEmpty()) {
|
||||
throw new BusinessException("服务内容不能为空");
|
||||
}
|
||||
if (serviceContent.length() > 2000) {
|
||||
throw new BusinessException("服务内容不能超过2000字");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCoordination(ApprovalCoordinationParam param) {
|
||||
if (param == null) {
|
||||
throw new BusinessException("参数不能为空");
|
||||
}
|
||||
if (param.getAcceptOrgId() == null || param.getAcceptOrgId() <= 0) {
|
||||
throw new BusinessException("请选择受理部门");
|
||||
}
|
||||
String title = param.getTitle() == null ? "" : param.getTitle().trim();
|
||||
if (title.isEmpty()) {
|
||||
throw new BusinessException("标题不能为空");
|
||||
}
|
||||
if (title.length() > 100) {
|
||||
throw new BusinessException("标题不能超过100字");
|
||||
}
|
||||
String content = param.getContent() == null ? "" : param.getContent().trim();
|
||||
if (content.isEmpty()) {
|
||||
throw new BusinessException("内容不能为空");
|
||||
}
|
||||
if (content.length() > 2000) {
|
||||
throw new BusinessException("内容不能超过2000字");
|
||||
}
|
||||
String remark = normalizeRemark(param.getRemark());
|
||||
if (remark != null && remark.length() > 500) {
|
||||
throw new BusinessException("备注不能超过500字");
|
||||
}
|
||||
}
|
||||
|
||||
private String submitBlockedMessage(String status) {
|
||||
if (ApprovalConstants.STATUS_PENDING.equals(status)) {
|
||||
return "单据正在审批中,不能重复提交";
|
||||
}
|
||||
if (ApprovalConstants.STATUS_ARCHIVED.equals(status)) {
|
||||
return "单据已归档,不能修改";
|
||||
}
|
||||
return "当前状态不可提交";
|
||||
}
|
||||
|
||||
private ApprovalApprover toApprover(ApprovalUserVO user) {
|
||||
return new ApprovalApprover(user.getUserId(), userDisplayName(user));
|
||||
}
|
||||
|
||||
private String userDisplayName(ApprovalUserVO user) {
|
||||
if (StrUtil.isNotBlank(user.getRealName())) {
|
||||
return user.getRealName();
|
||||
}
|
||||
if (StrUtil.isNotBlank(user.getNickname())) {
|
||||
return user.getNickname();
|
||||
}
|
||||
return user.getUsername();
|
||||
}
|
||||
|
||||
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 nodeName(Map<Long, List<ApprovalNode>> cache, Long definitionId, String nodeKey) {
|
||||
if (definitionId == null || nodeKey == null) {
|
||||
return null;
|
||||
}
|
||||
List<ApprovalNode> nodes = cache.computeIfAbsent(definitionId, flowService::parseNodes);
|
||||
return nodes.stream()
|
||||
.filter(node -> nodeKey.equals(node.getKey()))
|
||||
.map(ApprovalNode::getName)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private String taskStatusName(String status) {
|
||||
if (ApprovalConstants.TASK_PENDING.equals(status)) {
|
||||
return "待确认";
|
||||
@@ -780,7 +606,38 @@ public class ApprovalInstanceService {
|
||||
return status;
|
||||
}
|
||||
|
||||
private String submitBlockedMessage(String status) {
|
||||
if (ApprovalConstants.STATUS_PENDING.equals(status)) {
|
||||
return "单据正在审批中,不能重复提交";
|
||||
}
|
||||
if (ApprovalConstants.STATUS_ARCHIVED.equals(status)) {
|
||||
return "单据已归档,不能修改";
|
||||
}
|
||||
return "当前状态不可提交";
|
||||
}
|
||||
|
||||
private String nodeName(Map<Long, List<ApprovalNode>> cache, Long definitionId, String nodeKey) {
|
||||
if (definitionId == null || nodeKey == null) {
|
||||
return null;
|
||||
}
|
||||
List<ApprovalNode> nodes = cache.computeIfAbsent(definitionId, flowService::parseNodes);
|
||||
return nodes.stream()
|
||||
.filter(node -> nodeKey.equals(node.getKey()))
|
||||
.map(ApprovalNode::getName)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private <T> T inTransaction(Supplier<T> action) {
|
||||
return new TransactionTemplate(transactionManager).execute(status -> action.get());
|
||||
}
|
||||
|
||||
/** 协调单受理部门快照载体。 */
|
||||
private static final class ApprovalCoordinationHolder {
|
||||
private final Integer acceptOrgId;
|
||||
|
||||
private ApprovalCoordinationHolder(Integer acceptOrgId) {
|
||||
this.acceptOrgId = acceptOrgId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentRegistry;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.document.ApprovalFieldUpdate;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFieldChange;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.entity.ApprovalTask;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalFieldChangeMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalTaskMapper;
|
||||
import com.gxwebsoft.approval.param.ApprovalTaskAssignParam;
|
||||
@@ -13,7 +19,6 @@ import com.gxwebsoft.approval.param.ApprovalTaskDispatchParam;
|
||||
import com.gxwebsoft.approval.support.ApprovalApprover;
|
||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||
import com.gxwebsoft.approval.vo.ApprovalAssignVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalUserVO;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
@@ -26,25 +31,19 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 审批任务动作:通过、驳回、撤回与节点推进。
|
||||
* 审批任务动作:通过、会签通过、回填、驳回、撤回与节点推进。
|
||||
*
|
||||
* <p>跨库约束:审批人解析必须走 {@link ApprovalDirectoryService}(core 专用连接),
|
||||
* <p>跨库约束:审批人解析必须走 {@link ApprovalApproverResolver}(core 专用连接),
|
||||
* 在事务外完成;写 master 的部分放进 TransactionTemplate。任务处理使用
|
||||
* WHERE status='PENDING' 的条件更新,抢先后到者收到「任务已处理」。</p>
|
||||
*/
|
||||
@Service
|
||||
public class ApprovalTaskService {
|
||||
|
||||
private static final String RESOLVER_REPORTER_ORG_LEADER = "reporterOrgLeader";
|
||||
private static final String RESOLVER_ACCEPT_ORG_LEADER = "acceptOrgLeader";
|
||||
private static final String RESOLVER_ARCHIVIST = "archivist";
|
||||
private static final String RESOLVER_REPORTER = "reporter";
|
||||
private static final String RESOLVER_REPORTER_AND_DISPATCH_TARGET = "reporterAndDispatchTarget";
|
||||
private static final String RESOLVER_ACCEPT_ORG_MEMBERS = "acceptOrgMembers";
|
||||
private static final int MAX_OPINION_LENGTH = 500;
|
||||
|
||||
@Resource
|
||||
@@ -52,18 +51,28 @@ public class ApprovalTaskService {
|
||||
@Resource
|
||||
private ApprovalTaskMapper taskMapper;
|
||||
@Resource
|
||||
private ApprovalFieldChangeMapper fieldChangeMapper;
|
||||
@Resource
|
||||
private ApprovalEngineService engineService;
|
||||
@Resource
|
||||
private ApprovalDirectoryService directoryService;
|
||||
@Resource
|
||||
private ApprovalApproverResolver approverResolver;
|
||||
@Resource
|
||||
private ApprovalDocumentRegistry documentRegistry;
|
||||
@Resource
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
/**
|
||||
* 通过:APPROVE 节点或签,处理后取消同节点同轮其余待办并推进下一节点。
|
||||
* 通过:APPROVE 节点或签、COUNTERSIGN 节点组内或签。
|
||||
*
|
||||
* <p>会签节点一人同属多组时,一次通过把他该节点本轮全部待办置完成;
|
||||
* 只取消同组剩余待办,全部组完成后才推进(ADR 0003)。</p>
|
||||
*/
|
||||
public Result approve(Long taskId, String opinion, User user) {
|
||||
public Result approve(Long taskId, String opinion, Map<String, String> fields, User user) {
|
||||
ApprovalTask task = requireTask(taskId, user);
|
||||
if (!ApprovalConstants.NODE_APPROVE.equals(task.getNodeType())) {
|
||||
boolean countersign = ApprovalConstants.NODE_COUNTERSIGN.equals(task.getNodeType());
|
||||
if (!ApprovalConstants.NODE_APPROVE.equals(task.getNodeType()) && !countersign) {
|
||||
throw new BusinessException("当前任务不支持通过");
|
||||
}
|
||||
String normalized = normalizeOpinion(opinion);
|
||||
@@ -71,14 +80,52 @@ public class ApprovalTaskService {
|
||||
if (instance == null) {
|
||||
throw new BusinessException("单据不存在");
|
||||
}
|
||||
ApprovalDocumentType documentType = documentRegistry.find(instance.getFlowCode());
|
||||
// 回填字段在事务外校验(可能涉及唯一性查询与业务表读取)
|
||||
List<ApprovalFieldUpdate> changes = writeFields(instance, documentType, task, fields, true, user);
|
||||
ApprovalNode next = engineService.nextNode(instance, task.getNodeKey());
|
||||
List<ApprovalApprover> approvers =
|
||||
next == null ? Collections.emptyList() : resolveApprovers(instance, next);
|
||||
ApprovalApproverResolver.Resolution resolution = next == null
|
||||
? ApprovalApproverResolver.Resolution.resolved(Collections.emptyList())
|
||||
: approverResolver.resolve(instance, next);
|
||||
|
||||
String warning = inTransaction(() -> doApprove(taskId, task, normalized, user, next, approvers));
|
||||
String warning = inTransaction(() -> countersign
|
||||
? doCountersignApprove(taskId, task, normalized, user, next, resolution, changes)
|
||||
: doApprove(taskId, task, normalized, user, next, resolution, changes));
|
||||
return new Result(instance.getId(), warning);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填:只写字段不推进,允许必填缺项;仅任务处理人本人、任务仍待办、实例仍审批中。
|
||||
*/
|
||||
public Result backfill(Long taskId, Map<String, String> fields, User user) {
|
||||
ApprovalTask task = requireTask(taskId, user);
|
||||
if (fields == null || fields.isEmpty()) {
|
||||
throw new BusinessException("请填写回填内容");
|
||||
}
|
||||
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
|
||||
if (instance == null) {
|
||||
throw new BusinessException("单据不存在");
|
||||
}
|
||||
if (!ApprovalConstants.STATUS_PENDING.equals(instance.getStatus())) {
|
||||
throw new BusinessException("单据状态已变化,请刷新后重试");
|
||||
}
|
||||
ensureTaskCurrent(task, instance);
|
||||
requireWritableNode(instance, task);
|
||||
ApprovalDocumentType documentType = documentRegistry.find(instance.getFlowCode());
|
||||
List<ApprovalFieldUpdate> changes = writeFields(instance, documentType, task, fields, false, user);
|
||||
if (changes.isEmpty()) {
|
||||
return new Result(instance.getId(), null);
|
||||
}
|
||||
inTransaction(() -> {
|
||||
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
|
||||
ensureTaskHandleable(task, locked);
|
||||
persistChanges(locked, task, changes, user);
|
||||
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
|
||||
ApprovalConstants.ACTION_BACKFILL, backfillSummary(changes));
|
||||
});
|
||||
return new Result(instance.getId(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 存档确认:CONFIRM 任务只能确认(无意见),同节点同轮全部完成后实例进入审批完成,
|
||||
* 并激活归档节点;归档节点缺配时实例停在审批中并提示管理员补配置。
|
||||
@@ -94,10 +141,11 @@ public class ApprovalTaskService {
|
||||
}
|
||||
ensureNotArchived(instance);
|
||||
ApprovalNode next = engineService.nextNode(instance, task.getNodeKey());
|
||||
List<ApprovalApprover> approvers =
|
||||
next == null ? Collections.emptyList() : resolveApprovers(instance, next);
|
||||
ApprovalApproverResolver.Resolution resolution = next == null
|
||||
? ApprovalApproverResolver.Resolution.resolved(Collections.emptyList())
|
||||
: approverResolver.resolve(instance, next);
|
||||
|
||||
String warning = inTransaction(() -> doConfirm(taskId, task, user, next, approvers));
|
||||
String warning = inTransaction(() -> doConfirm(taskId, task, user, next, resolution));
|
||||
return new Result(instance.getId(), warning);
|
||||
}
|
||||
|
||||
@@ -120,16 +168,25 @@ public class ApprovalTaskService {
|
||||
|
||||
/**
|
||||
* 驳回:意见必填,同轮待办全部取消,实例回到报送人。
|
||||
*
|
||||
* <p>会签节点任一组驳回即整节点结束、round+1,本轮已完成的组任务保持 DONE 留痕。
|
||||
* 单据侧需要清空的字段由 {@link ApprovalDocumentType#onRoundReset} 处理(ADR 0014)。</p>
|
||||
*/
|
||||
public Result reject(Long taskId, String opinion, User user) {
|
||||
ApprovalTask task = requireTask(taskId, user);
|
||||
boolean rejectable = ApprovalConstants.NODE_APPROVE.equals(task.getNodeType())
|
||||
|| ApprovalConstants.NODE_COUNTERSIGN.equals(task.getNodeType())
|
||||
|| ApprovalConstants.NODE_DISPATCH.equals(task.getNodeType());
|
||||
if (!rejectable) {
|
||||
throw new BusinessException("当前任务不支持驳回");
|
||||
}
|
||||
String required = requireOpinion(opinion);
|
||||
inTransaction(() -> doReject(taskId, task, required, user));
|
||||
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
|
||||
if (instance == null) {
|
||||
throw new BusinessException("单据不存在");
|
||||
}
|
||||
ApprovalDocumentType documentType = documentRegistry.find(instance.getFlowCode());
|
||||
inTransaction(() -> doReject(taskId, task, required, user, documentType));
|
||||
return new Result(task.getInstanceId(), null);
|
||||
}
|
||||
|
||||
@@ -151,14 +208,13 @@ public class ApprovalTaskService {
|
||||
throw new BusinessException("当前状态不可撤回");
|
||||
}
|
||||
ensureNoHandledTask(instance);
|
||||
inTransaction(() -> doWithdraw(instanceId, user));
|
||||
ApprovalDocumentType documentType = documentRegistry.find(instance.getFlowCode());
|
||||
inTransaction(() -> doWithdraw(instanceId, user, documentType));
|
||||
return new Result(instanceId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派:管理员把待处理任务换给同租户其他启用用户,原因必填并写 ASSIGN 记录。
|
||||
*
|
||||
* <p>改派不改变任务状态,只替换处理人;操作人不必是任务当前处理人。</p>
|
||||
*/
|
||||
public ApprovalAssignVO assign(Long taskId, ApprovalTaskAssignParam param, User user) {
|
||||
if (param == null || param.getTargetUserId() == null) {
|
||||
@@ -166,20 +222,18 @@ public class ApprovalTaskService {
|
||||
}
|
||||
String reason = requireAssignReason(param.getReason());
|
||||
ApprovalTask task = requireAssignableTask(taskId);
|
||||
// 事务外解析目标用户:启用、未删除且属于当前租户(@DS("server") 查询显式带租户)
|
||||
List<ApprovalUserVO> targets =
|
||||
directoryService.listActiveUsersByIds(Collections.singletonList(param.getTargetUserId()));
|
||||
if (targets.isEmpty()) {
|
||||
throw new BusinessException("改派对象不存在或已停用");
|
||||
}
|
||||
ApprovalUserVO target = targets.get(0);
|
||||
String targetName = displayName(target);
|
||||
String targetName = approverResolver.displayName(target);
|
||||
return inTransaction(() -> doAssign(taskId, task, target.getUserId(), targetName, reason, user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单候选池:仅派单节点、本人待办的任务可查询;优先受理部门启用成员,
|
||||
* 部门无成员时回退为租户非普通消费用户。
|
||||
* 派单候选池:仅派单节点、本人待办的任务可查询。
|
||||
*/
|
||||
public List<ApprovalUserVO> candidates(Long taskId, User user) {
|
||||
ApprovalTask task = requireDispatchTask(taskId, user);
|
||||
@@ -192,8 +246,7 @@ public class ApprovalTaskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单:选定候选池中的 1 人即完成派单节点,激活存档节点并为报送人与被派单人生成两条
|
||||
* 存档任务。目标校验与审批人解析在事务外完成(core 库),写 master 的部分在事务内。
|
||||
* 派单:选定候选池中的 1 人即完成派单节点,激活存档节点并为存档人生成任务。
|
||||
*/
|
||||
public Result dispatch(Long taskId, ApprovalTaskDispatchParam param, User user) {
|
||||
if (param == null || param.getTargetUserId() == null) {
|
||||
@@ -206,7 +259,7 @@ public class ApprovalTaskService {
|
||||
}
|
||||
ensureNotArchived(instance);
|
||||
ApprovalUserVO target = requireDispatchTarget(instance, param.getTargetUserId());
|
||||
String targetName = displayName(target);
|
||||
String targetName = approverResolver.displayName(target);
|
||||
ApprovalNode next = engineService.nextNode(instance, task.getNodeKey());
|
||||
List<ApprovalApprover> approvers = dispatchNextApprovers(instance, next, target);
|
||||
String warning = inTransaction(() ->
|
||||
@@ -219,25 +272,66 @@ public class ApprovalTaskService {
|
||||
}
|
||||
|
||||
private String doApprove(Long taskId, ApprovalTask task, String opinion, User user,
|
||||
ApprovalNode next, List<ApprovalApprover> approvers) {
|
||||
// 先锁实例串行化同单据的处理,再做任务条件更新,避免并发或签互相等待行锁
|
||||
ApprovalNode next, ApprovalApproverResolver.Resolution resolution,
|
||||
List<ApprovalFieldUpdate> changes) {
|
||||
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
|
||||
ensureTaskHandleable(task, locked);
|
||||
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, opinion) == 0) {
|
||||
throw new BusinessException("任务已处理");
|
||||
}
|
||||
persistChanges(locked, task, changes, user);
|
||||
engineService.cancelPendingTasks(locked.getId(), locked.getRound(), taskId);
|
||||
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
|
||||
ApprovalConstants.ACTION_APPROVE, opinion);
|
||||
return advance(locked, next, resolution);
|
||||
}
|
||||
|
||||
/**
|
||||
* 会签通过:完成本人在该节点本轮的全部待办 → 只取消同组剩余 → 全部组完成才推进。
|
||||
*/
|
||||
private String doCountersignApprove(Long taskId, ApprovalTask task, String opinion, User user,
|
||||
ApprovalNode next, ApprovalApproverResolver.Resolution resolution,
|
||||
List<ApprovalFieldUpdate> changes) {
|
||||
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
|
||||
ensureTaskHandleable(task, locked);
|
||||
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, opinion) == 0) {
|
||||
throw new BusinessException("任务已处理");
|
||||
}
|
||||
persistChanges(locked, task, changes, user);
|
||||
// 一人同属多组:一次点击完成他在本节点本轮的全部待办
|
||||
engineService.completeAssigneeTasks(locked.getId(), locked.getRound(), task.getNodeKey(),
|
||||
user.getUserId(), taskId, opinion);
|
||||
if (task.getGroupKey() != null) {
|
||||
engineService.cancelPendingTasksInGroup(locked.getId(), locked.getRound(), task.getNodeKey(),
|
||||
task.getGroupKey(), null);
|
||||
}
|
||||
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
|
||||
ApprovalConstants.ACTION_APPROVE, opinion);
|
||||
List<String> pendingGroups = engineService.pendingGroupKeys(locked.getId(), locked.getRound(),
|
||||
task.getNodeKey());
|
||||
if (!pendingGroups.isEmpty()) {
|
||||
// 还有组未签:节点停在当前节点,不推进
|
||||
return null;
|
||||
}
|
||||
return advance(locked, next, resolution);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推进到下一节点;仓库内所有调用点共用同一套缺配提示。
|
||||
*/
|
||||
private String advance(ApprovalInstance locked, ApprovalNode next,
|
||||
ApprovalApproverResolver.Resolution resolution) {
|
||||
if (next == null) {
|
||||
return null;
|
||||
}
|
||||
int created = engineService.moveToNode(locked, next, locked.getRound(), approvers);
|
||||
// 缺配也要把当前节点推进到该节点(一期口径:停在缺配节点等管理员补人,不留在上一节点)
|
||||
int created = engineService.moveToNode(locked, next, locked.getRound(),
|
||||
resolution.isResolved() ? resolution.getApprovers() : java.util.Collections.emptyList());
|
||||
return created == 0 ? "当前节点缺少审批人,请联系管理员补配置" : null;
|
||||
}
|
||||
|
||||
private String doConfirm(Long taskId, ApprovalTask task, User user,
|
||||
ApprovalNode next, List<ApprovalApprover> approvers) {
|
||||
ApprovalNode next, ApprovalApproverResolver.Resolution resolution) {
|
||||
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
|
||||
ensureNotArchived(locked);
|
||||
ensureTaskHandleable(task, locked);
|
||||
@@ -246,6 +340,7 @@ public class ApprovalTaskService {
|
||||
}
|
||||
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
|
||||
ApprovalConstants.ACTION_CONFIRM, null);
|
||||
// 存档进度按「节点 + 轮次」聚合,同轮两个 CONFIRM 节点不会串进度(ADR 0003 修正)
|
||||
Integer remaining = taskMapper.selectCount(new LambdaQueryWrapper<ApprovalTask>()
|
||||
.eq(ApprovalTask::getInstanceId, locked.getId())
|
||||
.eq(ApprovalTask::getRound, locked.getRound())
|
||||
@@ -257,7 +352,8 @@ public class ApprovalTaskService {
|
||||
if (next == null) {
|
||||
return "当前节点缺少审批人,请联系管理员补配置";
|
||||
}
|
||||
int created = engineService.moveToNode(locked, next, locked.getRound(), approvers);
|
||||
int created = engineService.moveToNode(locked, next, locked.getRound(),
|
||||
resolution.isResolved() ? resolution.getApprovers() : java.util.Collections.emptyList());
|
||||
if (created == 0) {
|
||||
return "当前节点缺少审批人,请联系管理员补配置";
|
||||
}
|
||||
@@ -297,7 +393,8 @@ public class ApprovalTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void doReject(Long taskId, ApprovalTask task, String opinion, User user) {
|
||||
private void doReject(Long taskId, ApprovalTask task, String opinion, User user,
|
||||
ApprovalDocumentType documentType) {
|
||||
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
|
||||
ensureTaskHandleable(task, locked);
|
||||
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, opinion) == 0) {
|
||||
@@ -307,6 +404,9 @@ public class ApprovalTaskService {
|
||||
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
|
||||
ApprovalConstants.ACTION_REJECT, opinion);
|
||||
engineService.cancelPendingTasks(locked.getId(), round, null);
|
||||
if (documentType != null) {
|
||||
documentType.onRoundReset(locked, user);
|
||||
}
|
||||
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
|
||||
.eq("id", locked.getId())
|
||||
.eq("status", ApprovalConstants.STATUS_PENDING)
|
||||
@@ -317,7 +417,7 @@ public class ApprovalTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void doWithdraw(Long instanceId, User user) {
|
||||
private void doWithdraw(Long instanceId, User user, ApprovalDocumentType documentType) {
|
||||
ApprovalInstance locked = engineService.lockInstance(instanceId);
|
||||
if (!ApprovalConstants.STATUS_PENDING.equals(locked.getStatus())) {
|
||||
throw new BusinessException("当前状态不可撤回");
|
||||
@@ -327,6 +427,9 @@ public class ApprovalTaskService {
|
||||
engineService.addRecord(locked, null, locked.getCurrentNodeKey(), user,
|
||||
ApprovalConstants.ACTION_WITHDRAW, null);
|
||||
engineService.cancelPendingTasks(locked.getId(), round, null);
|
||||
if (documentType != null) {
|
||||
documentType.onRoundReset(locked, user);
|
||||
}
|
||||
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
|
||||
.eq("id", instanceId)
|
||||
.eq("status", ApprovalConstants.STATUS_PENDING)
|
||||
@@ -337,9 +440,6 @@ public class ApprovalTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务内改派:锁实例后条件更新处理人,并追加 ASSIGN 记录。
|
||||
*/
|
||||
private ApprovalAssignVO doAssign(Long taskId, ApprovalTask task, Integer targetUserId,
|
||||
String targetName, String reason, User user) {
|
||||
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
|
||||
@@ -365,9 +465,6 @@ public class ApprovalTaskService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务内派单:锁实例后条件更新任务、快照派单对象、写派单记录并激活存档节点。
|
||||
*/
|
||||
private String doDispatch(Long taskId, ApprovalTask task, ApprovalUserVO target, String targetName,
|
||||
User user, ApprovalNode next, List<ApprovalApprover> approvers) {
|
||||
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
|
||||
@@ -397,8 +494,67 @@ public class ApprovalTaskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单前校验:任务存在、属于本人、是派单节点且仍待处理。
|
||||
* 回填字段:委托单据实现校验并落库,返回改值记录(事务外执行,可能访问 core 库)。
|
||||
*/
|
||||
private List<ApprovalFieldUpdate> writeFields(ApprovalInstance instance, ApprovalDocumentType type,
|
||||
ApprovalTask task, Map<String, String> fields,
|
||||
boolean requireComplete, User user) {
|
||||
if (type == null || fields == null || fields.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
requireWritableNode(instance, task);
|
||||
return type.applyWritableFields(instance, fields, user, requireComplete);
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点必须声明了可写字段,否则拒绝回填。
|
||||
*/
|
||||
private void requireWritableNode(ApprovalInstance instance, ApprovalTask task) {
|
||||
ApprovalNode node = engineService.findNode(instance, task.getNodeKey());
|
||||
if (node == null || node.getWritableFields() == null || node.getWritableFields().isEmpty()) {
|
||||
throw new BusinessException("当前节点不支持回填");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 落改值历史;每次回填/通过按字段写一行。
|
||||
*/
|
||||
private void persistChanges(ApprovalInstance instance, ApprovalTask task,
|
||||
List<ApprovalFieldUpdate> changes, User user) {
|
||||
if (changes == null || changes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (ApprovalFieldUpdate change : changes) {
|
||||
ApprovalFieldChange entity = new ApprovalFieldChange();
|
||||
entity.setInstanceId(instance.getId());
|
||||
entity.setTaskId(task.getId());
|
||||
entity.setNodeKey(task.getNodeKey());
|
||||
entity.setFieldKey(change.getFieldKey());
|
||||
entity.setFieldLabel(change.getFieldLabel());
|
||||
entity.setOldValue(change.getOldValue());
|
||||
entity.setNewValue(change.getNewValue());
|
||||
entity.setOperatorId(user.getUserId());
|
||||
entity.setOperatorName(displayName(user));
|
||||
entity.setRound(instance.getRound());
|
||||
entity.setCreateTime(LocalDateTime.now());
|
||||
fieldChangeMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private String backfillSummary(List<ApprovalFieldUpdate> changes) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
for (ApprovalFieldUpdate change : changes) {
|
||||
parts.add(change.getFieldLabel() + ":" + text(change.getOldValue()) + " → "
|
||||
+ text(change.getNewValue()));
|
||||
}
|
||||
String summary = String.join(";", parts);
|
||||
return summary.length() > MAX_OPINION_LENGTH ? summary.substring(0, MAX_OPINION_LENGTH) : summary;
|
||||
}
|
||||
|
||||
private String text(String value) {
|
||||
return value == null || value.isEmpty() ? "空" : value;
|
||||
}
|
||||
|
||||
private ApprovalTask requireDispatchTask(Long taskId, User user) {
|
||||
ApprovalTask task = requireTask(taskId, user);
|
||||
if (!ApprovalConstants.NODE_DISPATCH.equals(task.getNodeType())) {
|
||||
@@ -407,9 +563,6 @@ public class ApprovalTaskService {
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单目标必须来自候选池(受理部门启用成员,或部门无成员时的租户回退候选)。
|
||||
*/
|
||||
private ApprovalUserVO requireDispatchTarget(ApprovalInstance instance, Integer targetUserId) {
|
||||
for (ApprovalUserVO candidate : directoryService.listDispatchCandidates(instance.getAcceptOrgId())) {
|
||||
if (targetUserId.equals(candidate.getUserId())) {
|
||||
@@ -419,16 +572,13 @@ public class ApprovalTaskService {
|
||||
throw new BusinessException("派单对象不在候选范围内");
|
||||
}
|
||||
|
||||
/**
|
||||
* 存档节点审批人:报送人 + 本次派单对象(实例快照尚未写入,需显式解析)。
|
||||
*/
|
||||
private List<ApprovalApprover> dispatchNextApprovers(ApprovalInstance instance, ApprovalNode next,
|
||||
ApprovalUserVO target) {
|
||||
if (next == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (!RESOLVER_REPORTER_AND_DISPATCH_TARGET.equals(next.getResolver())) {
|
||||
return resolveApprovers(instance, next);
|
||||
if (!ApprovalConstants.RESOLVER_REPORTER_AND_DISPATCH_TARGET.equals(next.getResolver())) {
|
||||
return approverResolver.resolve(instance, next).getApprovers();
|
||||
}
|
||||
List<Integer> userIds = new ArrayList<>();
|
||||
if (instance.getReporterId() != null) {
|
||||
@@ -437,12 +587,11 @@ public class ApprovalTaskService {
|
||||
if (!userIds.contains(target.getUserId())) {
|
||||
userIds.add(target.getUserId());
|
||||
}
|
||||
return userApprovers(userIds);
|
||||
return directoryService.listActiveUsersByIds(userIds).stream()
|
||||
.map(user -> new ApprovalApprover(user.getUserId(), approverResolver.displayName(user)))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单任务条件更新:完成并快照派单对象。
|
||||
*/
|
||||
private int updateDispatchTask(Long taskId, Integer targetUserId, String targetName) {
|
||||
return taskMapper.update(null, new UpdateWrapper<ApprovalTask>()
|
||||
.eq("id", taskId)
|
||||
@@ -453,9 +602,6 @@ public class ApprovalTaskService {
|
||||
.set("dispatch_target_name", targetName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派前的基本校验:任务存在、待处理且单据仍在审批中。
|
||||
*/
|
||||
private ApprovalTask requireAssignableTask(Long taskId) {
|
||||
if (taskId == null) {
|
||||
throw new BusinessException("任务不存在");
|
||||
@@ -477,9 +623,6 @@ public class ApprovalTaskService {
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务必须属于实例当前轮次、当前节点,否则视为已被处理或已失效。
|
||||
*/
|
||||
private void ensureTaskHandleable(ApprovalTask task, ApprovalInstance instance) {
|
||||
if (!ApprovalConstants.STATUS_PENDING.equals(instance.getStatus())) {
|
||||
throw new BusinessException("单据状态已变化,请刷新后重试");
|
||||
@@ -536,85 +679,6 @@ public class ApprovalTaskService {
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务外按节点解析器解析审批人;缺配时返回空列表,由调用方提示并停留当前节点。
|
||||
*/
|
||||
private List<ApprovalApprover> resolveApprovers(ApprovalInstance instance, ApprovalNode node) {
|
||||
String resolver = node.getResolver();
|
||||
if (StrUtil.isBlank(resolver)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
switch (resolver) {
|
||||
case RESOLVER_REPORTER_ORG_LEADER:
|
||||
return departmentLeaderApprovers(instance.getReportOrgId());
|
||||
case RESOLVER_ACCEPT_ORG_LEADER:
|
||||
return departmentLeaderApprovers(instance.getAcceptOrgId());
|
||||
case RESOLVER_ARCHIVIST:
|
||||
return roleApprovers(ApprovalConstants.ROLE_ARCHIVIST);
|
||||
case RESOLVER_REPORTER:
|
||||
return userApprovers(Collections.singletonList(instance.getReporterId()));
|
||||
case RESOLVER_REPORTER_AND_DISPATCH_TARGET:
|
||||
return userApprovers(confirmCandidateIds(instance));
|
||||
case RESOLVER_ACCEPT_ORG_MEMBERS:
|
||||
return organizationApprovers(instance.getAcceptOrgId());
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门负责人审批人:按部门 organization_code 解析角色;部门或编码缺失时返回空列表,
|
||||
* 节点保持缺配状态,等待管理员补配置。
|
||||
*/
|
||||
private List<ApprovalApprover> departmentLeaderApprovers(Integer organizationId) {
|
||||
ApprovalOrganizationVO organization = directoryService.getOrganization(organizationId);
|
||||
String roleCode = organization == null
|
||||
? null : ApprovalConstants.departmentLeaderRoleCode(organization.getOrganizationCode());
|
||||
return roleCode == null ? Collections.emptyList() : roleApprovers(roleCode);
|
||||
}
|
||||
|
||||
private List<Integer> confirmCandidateIds(ApprovalInstance instance) {
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
if (instance.getReporterId() != null) {
|
||||
ids.add(instance.getReporterId());
|
||||
}
|
||||
if (instance.getDispatchTargetId() != null && !ids.contains(instance.getDispatchTargetId())) {
|
||||
ids.add(instance.getDispatchTargetId());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> roleApprovers(String roleCode) {
|
||||
if (StrUtil.isBlank(roleCode)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return toApprovers(directoryService.listRoleMembers(roleCode));
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> userApprovers(List<Integer> userIds) {
|
||||
return toApprovers(directoryService.listActiveUsersByIds(userIds));
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> organizationApprovers(Integer organizationId) {
|
||||
return toApprovers(directoryService.listOrganizationMembers(organizationId));
|
||||
}
|
||||
|
||||
private List<ApprovalApprover> toApprovers(List<ApprovalUserVO> users) {
|
||||
return users.stream()
|
||||
.map(user -> new ApprovalApprover(user.getUserId(), displayName(user)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private String displayName(ApprovalUserVO user) {
|
||||
if (StrUtil.isNotBlank(user.getRealName())) {
|
||||
return user.getRealName();
|
||||
}
|
||||
if (StrUtil.isNotBlank(user.getNickname())) {
|
||||
return user.getNickname();
|
||||
}
|
||||
return user.getUsername();
|
||||
}
|
||||
|
||||
private String displayName(User user) {
|
||||
if (StrUtil.isNotBlank(user.getRealName())) {
|
||||
return user.getRealName();
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
package com.gxwebsoft.approval.support;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 节点激活时快照的审批人。
|
||||
*
|
||||
* <p>会签节点额外带上组标识、组显示名与展示顺序;非会签节点这三项为空。</p>
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class ApprovalApprover {
|
||||
|
||||
private Integer userId;
|
||||
|
||||
private String userName;
|
||||
|
||||
/** 会签组标识;非会签节点为空。 */
|
||||
private String groupKey;
|
||||
|
||||
/** 会签组显示名;非会签节点为空。 */
|
||||
private String groupName;
|
||||
|
||||
/** 会签组展示顺序,从 1 开始;非会签节点为空。 */
|
||||
private Integer groupSeq;
|
||||
|
||||
public ApprovalApprover() {
|
||||
}
|
||||
|
||||
public ApprovalApprover(Integer userId, String userName) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public ApprovalApprover(Integer userId, String userName, String groupKey, String groupName,
|
||||
Integer groupSeq) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
this.groupKey = groupKey;
|
||||
this.groupName = groupName;
|
||||
this.groupSeq = groupSeq;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package com.gxwebsoft.approval.support;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 导出查询数据库行:两类单据合并列,未命中的表单字段为空。
|
||||
* 导出查询数据库行:跨单据总表一行单据一行。
|
||||
*
|
||||
* <p>标题与检索文本取 approval_instance 的落库列,单据差异不再靠 LEFT JOIN 各业务表
|
||||
* 拼列(ADR 0007);明细行数与合计金额由导出服务按单据实现补算。</p>
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalExportRow implements Serializable {
|
||||
@@ -16,7 +18,9 @@ public class ApprovalExportRow implements Serializable {
|
||||
private Long id;
|
||||
private String bizNo;
|
||||
private String docType;
|
||||
private String flowCode;
|
||||
private String status;
|
||||
private String title;
|
||||
private String reporterName;
|
||||
private String reportOrgName;
|
||||
private String acceptOrgName;
|
||||
@@ -27,19 +31,14 @@ public class ApprovalExportRow implements Serializable {
|
||||
private LocalDateTime archiveTime;
|
||||
private String archivistName;
|
||||
|
||||
/** 业务完成确认单:名称。 */
|
||||
private String name;
|
||||
/** 业务完成确认单:实施单位。 */
|
||||
/** 业务完成确认单:实施单位(仅一期单据保留的固定列)。 */
|
||||
private String implementOrg;
|
||||
/** 业务完成确认单:费用(元)。 */
|
||||
private BigDecimal fee;
|
||||
private java.math.BigDecimal fee;
|
||||
/** 业务完成确认单:服务内容。 */
|
||||
private String serviceContent;
|
||||
|
||||
/** 内部协调单:标题。 */
|
||||
private String title;
|
||||
/** 内部协调单:协调内容。 */
|
||||
/** 工作协调单:协调内容。 */
|
||||
private String content;
|
||||
/** 内部协调单:备注。 */
|
||||
/** 工作协调单:备注。 */
|
||||
private String remark;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.gxwebsoft.approval.support;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 可写字段元数据(ADR 0006):长度、格式、精度与唯一性由后端注册表承担,
|
||||
* 流程 JSON 里只有「哪些字段可写、是否必填」的策略。
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalFieldSpec implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 字段键(驼峰,与业务表实体一致)。 */
|
||||
private String key;
|
||||
|
||||
/** 字段显示名。 */
|
||||
private String label;
|
||||
|
||||
/** 最大长度;null 表示不限制。 */
|
||||
private Integer maxLength;
|
||||
|
||||
/** 是否在同类单据内唯一(同租户、同 flow_code、实例未作废)。 */
|
||||
private boolean unique;
|
||||
|
||||
/** 字段类型:TEXT / DECIMAL / DATE / INT。 */
|
||||
private String type;
|
||||
|
||||
public static ApprovalFieldSpec text(String key, String label, int maxLength) {
|
||||
ApprovalFieldSpec spec = new ApprovalFieldSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(ApprovalLineSpec.TYPE_TEXT);
|
||||
spec.setMaxLength(maxLength);
|
||||
return spec;
|
||||
}
|
||||
|
||||
public static ApprovalFieldSpec uniqueText(String key, String label, int maxLength) {
|
||||
ApprovalFieldSpec spec = text(key, label, maxLength);
|
||||
spec.setUnique(true);
|
||||
return spec;
|
||||
}
|
||||
|
||||
public static ApprovalFieldSpec decimal(String key, String label) {
|
||||
ApprovalFieldSpec spec = new ApprovalFieldSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(ApprovalLineSpec.TYPE_DECIMAL);
|
||||
return spec;
|
||||
}
|
||||
|
||||
public static ApprovalFieldSpec date(String key, String label) {
|
||||
ApprovalFieldSpec spec = new ApprovalFieldSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(ApprovalLineSpec.TYPE_DATE);
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
@@ -25,4 +25,5 @@ public class ApprovalItemRow implements Serializable {
|
||||
private LocalDateTime archiveTime;
|
||||
private String currentNodeKey;
|
||||
private Long definitionId;
|
||||
private String flowCode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.gxwebsoft.approval.support;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 明细列元数据(ADR 0004):由后端注册表按 flow_code 提供,驱动服务端校验与前端列渲染。
|
||||
*
|
||||
* <p>列结构随表结构走,不放进流程定义 JSON——改列就是改表。</p>
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalLineSpec implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 列键(驼峰,与返回的明细行字段一致)。 */
|
||||
private String key;
|
||||
|
||||
/** 列显示名。 */
|
||||
private String label;
|
||||
|
||||
/** 列类型:TEXT / DECIMAL / INT / DATE / BOOL / ENUM。 */
|
||||
private String type;
|
||||
|
||||
private boolean required;
|
||||
|
||||
/** 数值列小数位。 */
|
||||
private Integer scale;
|
||||
|
||||
/** 文本列最大长度。 */
|
||||
private Integer maxLength;
|
||||
|
||||
/** 是否参与金额(服务端按数量 × 单价重算)。 */
|
||||
private boolean money;
|
||||
|
||||
/** ENUM 列的可选值。 */
|
||||
private List<Option> options;
|
||||
|
||||
public static ApprovalLineSpec text(String key, String label, boolean required, int maxLength) {
|
||||
ApprovalLineSpec spec = new ApprovalLineSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(TYPE_TEXT);
|
||||
spec.setRequired(required);
|
||||
spec.setMaxLength(maxLength);
|
||||
return spec;
|
||||
}
|
||||
|
||||
public static ApprovalLineSpec decimal(String key, String label, boolean required, int scale) {
|
||||
ApprovalLineSpec spec = new ApprovalLineSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(TYPE_DECIMAL);
|
||||
spec.setRequired(required);
|
||||
spec.setScale(scale);
|
||||
return spec;
|
||||
}
|
||||
|
||||
public static ApprovalLineSpec integer(String key, String label, boolean required) {
|
||||
ApprovalLineSpec spec = new ApprovalLineSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(TYPE_INT);
|
||||
spec.setRequired(required);
|
||||
return spec;
|
||||
}
|
||||
|
||||
public static ApprovalLineSpec date(String key, String label, boolean required) {
|
||||
ApprovalLineSpec spec = new ApprovalLineSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(TYPE_DATE);
|
||||
spec.setRequired(required);
|
||||
return spec;
|
||||
}
|
||||
|
||||
public static ApprovalLineSpec bool(String key, String label, boolean required) {
|
||||
ApprovalLineSpec spec = new ApprovalLineSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(TYPE_BOOL);
|
||||
spec.setRequired(required);
|
||||
return spec;
|
||||
}
|
||||
|
||||
public static ApprovalLineSpec enumOf(String key, String label, boolean required, List<Option> options) {
|
||||
ApprovalLineSpec spec = new ApprovalLineSpec();
|
||||
spec.setKey(key);
|
||||
spec.setLabel(label);
|
||||
spec.setType(TYPE_ENUM);
|
||||
spec.setRequired(required);
|
||||
spec.setOptions(options);
|
||||
return spec;
|
||||
}
|
||||
|
||||
/** 标记为金额列:值由服务端按 quantity × unitPrice 重算。 */
|
||||
public ApprovalLineSpec asMoney() {
|
||||
this.money = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static final String TYPE_TEXT = "TEXT";
|
||||
public static final String TYPE_DECIMAL = "DECIMAL";
|
||||
public static final String TYPE_INT = "INT";
|
||||
public static final String TYPE_DATE = "DATE";
|
||||
public static final String TYPE_BOOL = "BOOL";
|
||||
public static final String TYPE_ENUM = "ENUM";
|
||||
|
||||
/** 金额精度:DECIMAL(12,2),0~99999999.99。 */
|
||||
public static final BigDecimal MAX_MONEY = new BigDecimal("99999999.99");
|
||||
|
||||
/** 枚举列选项:`value` 为存储编码,`label` 为中文文案。 */
|
||||
@Data
|
||||
public static class Option implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String value;
|
||||
private String label;
|
||||
|
||||
public Option() {
|
||||
}
|
||||
|
||||
public Option(String value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,13 @@ package com.gxwebsoft.approval.support;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流程定义中的节点描述(nodes_json 数组元素)。
|
||||
*
|
||||
* <p>{@code @JsonIgnoreProperties(ignoreUnknown = true)} 意味着配置里多写的字段会被静默忽略,
|
||||
* 因此节点上新增任何流程配置都必须同步在这里加字段(ADR 0003 / ADR 0006 的坑)。</p>
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -21,4 +26,29 @@ public class ApprovalNode {
|
||||
|
||||
/** 节点显示名称。 */
|
||||
private String name;
|
||||
|
||||
/** 会签组(仅 COUNTERSIGN);为空时按或签处理。 */
|
||||
private List<ApprovalGroup> groups;
|
||||
|
||||
/** 可写字段(APPROVE/COUNTERSIGN/CONFIRM 可声明);为空表示不可回填。 */
|
||||
private List<WritableField> writableFields;
|
||||
|
||||
/** 会签组定义:组标识、显示名、解析器与可选固定组织编码。 */
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ApprovalGroup {
|
||||
private String key;
|
||||
private String name;
|
||||
private String resolver;
|
||||
/** 固定组织编码(orgLeaderByCode / orgDeputyByCode 使用)。 */
|
||||
private String orgCode;
|
||||
}
|
||||
|
||||
/** 节点可写字段声明;长度与唯一性由 ApprovalFieldSpec 承担。 */
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class WritableField {
|
||||
private String field;
|
||||
private boolean required;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ public class ApprovalConfirmVO implements Serializable {
|
||||
|
||||
private String userName;
|
||||
|
||||
/** 双存档时区分存档方(ADR 0003 / 0012)。 */
|
||||
private String groupKey;
|
||||
|
||||
private String groupName;
|
||||
|
||||
private String status;
|
||||
|
||||
private String statusName;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.gxwebsoft.approval.vo;
|
||||
|
||||
import com.gxwebsoft.approval.support.ApprovalLineSpec;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单据业务内容:业务字段、明细行/列元数据、明细汇总与会签进度。
|
||||
*
|
||||
* <p>前端按 `fields` 通用渲染;明细类单据再按 `lineColumns` 渲染表格。</p>
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "单据业务内容")
|
||||
public class ApprovalDetail implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 业务字段(有序)。 */
|
||||
private List<ApprovalFieldVO> fields = new ArrayList<>();
|
||||
|
||||
/** 明细行:每行是以列 key 为键的 map。 */
|
||||
private List<java.util.Map<String, Object>> lines = new ArrayList<>();
|
||||
|
||||
/** 明细列元数据;无明细为空。 */
|
||||
private List<ApprovalLineSpec> lineColumns = new ArrayList<>();
|
||||
|
||||
/** 明细汇总;无明细为 null。 */
|
||||
private ApprovalLineSummaryVO lineSummary;
|
||||
|
||||
/** 会签节点进度(当前轮),非会签单据为空。 */
|
||||
private List<ApprovalGroupProgressVO> groupProgress = new ArrayList<>();
|
||||
|
||||
/** 字段改值历史(最新在后)。 */
|
||||
private List<ApprovalFieldChangeVO> fieldChanges = new ArrayList<>();
|
||||
|
||||
public void addField(String key, String label, Object value) {
|
||||
fields.add(new ApprovalFieldVO(key, label, value == null ? null : String.valueOf(value)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.gxwebsoft.approval.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 详情中的字段改值历史(ADR 0006)。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "字段改值历史")
|
||||
public class ApprovalFieldChangeVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
|
||||
private String nodeKey;
|
||||
|
||||
private String nodeName;
|
||||
|
||||
private String fieldKey;
|
||||
|
||||
private String fieldLabel;
|
||||
|
||||
private String oldValue;
|
||||
|
||||
private String newValue;
|
||||
|
||||
private String operatorName;
|
||||
|
||||
private Integer round;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.gxwebsoft.approval.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 详情中的业务字段(有序,中文标签),前端通用渲染。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "审批业务字段")
|
||||
public class ApprovalFieldVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String key;
|
||||
|
||||
private String label;
|
||||
|
||||
/** 已格式化好的展示值;空值为 null。 */
|
||||
private String value;
|
||||
|
||||
public ApprovalFieldVO() {
|
||||
}
|
||||
|
||||
public ApprovalFieldVO(String key, String label, String value) {
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.gxwebsoft.approval.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会签节点进度:按组展示已签 N/M(ADR 0003)。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "会签进度")
|
||||
public class ApprovalGroupProgressVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String nodeKey;
|
||||
|
||||
private String nodeName;
|
||||
|
||||
private String groupKey;
|
||||
|
||||
private String groupName;
|
||||
|
||||
private Integer groupSeq;
|
||||
|
||||
/** 该组已完成人数。 */
|
||||
private int signed;
|
||||
|
||||
/** 该组任务总人数。 */
|
||||
private int total;
|
||||
|
||||
private List<Member> members;
|
||||
|
||||
/** 组内成员状态。 */
|
||||
@Data
|
||||
@ApiModel(description = "会签组成员")
|
||||
public static class Member implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer userId;
|
||||
|
||||
private String userName;
|
||||
|
||||
private String status;
|
||||
|
||||
private String statusName;
|
||||
}
|
||||
}
|
||||
@@ -91,5 +91,11 @@ public class ApprovalInstanceVO implements Serializable {
|
||||
/** 实例当前全部待办任务,供有 assign 权限的 PC 用户按任务改派。 */
|
||||
private List<ApprovalPendingTaskVO> pendingTasks;
|
||||
|
||||
/** 单据业务内容(业务字段、明细、会签进度、改值历史)。 */
|
||||
private ApprovalDetail document;
|
||||
|
||||
/** 字段改值历史(ADR 0006)。 */
|
||||
private List<ApprovalFieldChangeVO> fieldChanges;
|
||||
|
||||
private ApprovalPermissionsVO permissions;
|
||||
}
|
||||
|
||||
@@ -43,4 +43,7 @@ public class ApprovalItemVO implements Serializable {
|
||||
private String acceptOrgName;
|
||||
|
||||
private LocalDateTime archiveTime;
|
||||
|
||||
/** 流程编码(按流程类型筛选,ADR 0007)。 */
|
||||
private String flowCode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.gxwebsoft.approval.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 明细汇总:行数(必有)与合计金额(金额类单据才填)。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "明细汇总")
|
||||
public class ApprovalLineSummaryVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int lineCount;
|
||||
|
||||
private BigDecimal amountTotal;
|
||||
}
|
||||
@@ -19,5 +19,9 @@ public class ApprovalMyTaskVO implements Serializable {
|
||||
|
||||
private String nodeType;
|
||||
|
||||
private String groupKey;
|
||||
|
||||
private String groupName;
|
||||
|
||||
private String status;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ public class ApprovalPendingTaskVO implements Serializable {
|
||||
|
||||
private String nodeType;
|
||||
|
||||
private String groupKey;
|
||||
|
||||
private String groupName;
|
||||
|
||||
private Integer assigneeId;
|
||||
|
||||
private String assigneeName;
|
||||
|
||||
@@ -30,4 +30,7 @@ public class ApprovalPermissionsVO implements Serializable {
|
||||
private boolean canDispatch;
|
||||
|
||||
private boolean canAssign;
|
||||
|
||||
/** 当前用户在该节点可回填的字段(ADR 0006);非节点处理人为空。 */
|
||||
private java.util.List<ApprovalWritableFieldVO> writableFields = new java.util.ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.gxwebsoft.approval.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 当前用户在该节点可回填的字段(仅当他是该节点处理人时返回)。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "可写字段")
|
||||
public class ApprovalWritableFieldVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String field;
|
||||
|
||||
private String label;
|
||||
|
||||
private boolean required;
|
||||
|
||||
/** 当前值。 */
|
||||
private String value;
|
||||
}
|
||||
@@ -63,7 +63,8 @@ class ApprovalExportIT {
|
||||
|
||||
private static final String[] HEADERS = {
|
||||
"单号", "单据类型", "标题或名称", "报送部门", "报送人", "受理部门", "实施单位", "费用(元)",
|
||||
"服务内容或协调内容", "备注", "状态", "当前节点", "提交时间", "审批完成时间", "归档时间", "归档人"
|
||||
"服务内容或协调内容", "备注", "状态", "当前节点", "提交时间", "审批完成时间", "归档时间", "归档人",
|
||||
"明细行数", "合计金额", "会签进度"
|
||||
};
|
||||
|
||||
@Autowired
|
||||
|
||||
Reference in New Issue
Block a user