diff --git a/sql/02-approval_menu.sql b/sql/02-approval_menu.sql
index 9c6c397..2ee1ee3 100644
--- a/sql/02-approval_menu.sql
+++ b/sql/02-approval_menu.sql
@@ -65,19 +65,6 @@ WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE tenant_id = 10150 AND authority = 'approval:instance:export' AND menu_type = 1 AND deleted = 0
);
--- 授权 superAdmin(role_id = 586, tenant 10150)
-INSERT INTO sys_role_menu (role_id, menu_id, tenant_id)
-SELECT 586, m.menu_id, 10150
-FROM sys_menu m
-WHERE m.tenant_id = 10150
- AND m.deleted = 0
- AND (m.path IN ('/approval', '/approval/todo', '/approval/instance') OR m.authority LIKE 'approval:instance:%')
- AND NOT EXISTS (
- SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 586 AND rm.menu_id = m.menu_id AND rm.tenant_id = 10150
- );
-
-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);
@@ -94,3 +81,18 @@ 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
);
+
+-- 授权 superAdmin(role_id = 586, tenant 10150)
+INSERT INTO sys_role_menu (role_id, menu_id, tenant_id)
+SELECT 586, m.menu_id, 10150
+FROM sys_menu m
+WHERE m.tenant_id = 10150
+ AND m.deleted = 0
+ AND (m.path IN ('/approval', '/approval/todo', '/approval/instance', '/approval/price')
+ OR m.authority LIKE 'approval:%')
+ 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);
+
diff --git a/sql/05-approval_phase2.sql b/sql/05-approval_phase2.sql
index 06316da..47b2941 100644
--- a/sql/05-approval_phase2.sql
+++ b/sql/05-approval_phase2.sql
@@ -90,18 +90,5 @@ CREATE TABLE IF NOT EXISTS approval_field_change (
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;
diff --git a/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java b/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java
index a683ad4..7f76cda 100644
--- a/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java
+++ b/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java
@@ -65,6 +65,19 @@ public final class ApprovalConstants {
public static final String FLOW_COUPON = "coupon";
public static final String FLOW_FACILITY = "facility";
+ /**
+ * 单据类型(doc_type,决定用哪张业务表单)。
+ *
+ *
与 flow_code 分开是 ADR 0007 的核心区分:工作协调单两个流程共用同一个 doc_type。
+ * 每单一张表单的流程取值与 flow_code 相同,但常量分开命名,避免把两个概念混用。
+ */
+ public static final String DOC_SETTLEMENT = "settlement";
+ public static final String DOC_RECEIPT = "receipt";
+ public static final String DOC_VEHICLE = "vehicle";
+ public static final String DOC_VENUE = "venue";
+ public static final String DOC_COUPON = "coupon";
+ public static final String DOC_FACILITY = "facility";
+
// 单号前缀
public static final String NO_PREFIX_COMPLETION = "WG";
public static final String NO_PREFIX_COORDINATION = "XT";
diff --git a/src/main/java/com/gxwebsoft/approval/controller/ApprovalTaskController.java b/src/main/java/com/gxwebsoft/approval/controller/ApprovalTaskController.java
index 9190c13..e252c98 100644
--- a/src/main/java/com/gxwebsoft/approval/controller/ApprovalTaskController.java
+++ b/src/main/java/com/gxwebsoft/approval/controller/ApprovalTaskController.java
@@ -98,9 +98,11 @@ public class ApprovalTaskController extends BaseController {
@PreAuthorize("hasAuthority('approval:instance:approve')")
@ApiOperation("存档确认")
@PostMapping("/task/{id}/confirm")
- public ApiResult confirm(@PathVariable("id") Long id) {
+ public ApiResult confirm(@PathVariable("id") Long id,
+ @RequestBody(required = false) ApprovalTaskActionParam param) {
User user = getLoginUser();
- ApprovalTaskService.Result result = taskService.confirm(id, user);
+ ApprovalTaskService.Result result = taskService.confirm(id,
+ param == null ? null : param.getFields(), user);
return success(result.getWarning() == null ? "存档成功" : result.getWarning(),
instanceService.detail(result.getInstanceId(), user));
}
diff --git a/src/main/java/com/gxwebsoft/approval/doc/completion/CompletionDocumentType.java b/src/main/java/com/gxwebsoft/approval/doc/completion/CompletionDocumentType.java
index cf54949..f5febe2 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/completion/CompletionDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/completion/CompletionDocumentType.java
@@ -79,7 +79,8 @@ public class CompletionDocumentType extends AbstractApprovalDocumentType {
}
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
ApprovalCompletionParam completion = cast(param);
ApprovalCompletionForm form = bizId == null ? null : completionFormMapper.selectById(bizId);
if (form == null) {
diff --git a/src/main/java/com/gxwebsoft/approval/doc/coordination/CoordinationDocumentType.java b/src/main/java/com/gxwebsoft/approval/doc/coordination/CoordinationDocumentType.java
index 82e8e17..0e8989f 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/coordination/CoordinationDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/coordination/CoordinationDocumentType.java
@@ -94,13 +94,27 @@ public class CoordinationDocumentType extends AbstractApprovalDocumentType {
optionalText("备注", coordination.getRemark(), 500);
}
+ /** 受理部门快照在事务外解析:persist 在事务内,@DS("server") 会失效。 */
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Object prepare(ApprovalDocumentParam param, User user) {
ApprovalCoordinationParam coordination = cast(param);
ApprovalOrganizationVO accept = directoryService.getOrganization(coordination.getAcceptOrgId());
if (accept == null) {
throw new BusinessException("受理部门不存在或已停用");
}
+ return accept;
+ }
+
+ @Override
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
+ ApprovalCoordinationParam coordination = cast(param);
+ ApprovalOrganizationVO accept = prepared instanceof ApprovalOrganizationVO
+ ? (ApprovalOrganizationVO) prepared
+ : directoryService.getOrganization(coordination.getAcceptOrgId());
+ if (accept == null) {
+ throw new BusinessException("受理部门不存在或已停用");
+ }
ApprovalCoordinationForm form = bizId == null ? null : coordinationFormMapper.selectById(bizId);
if (form == null) {
form = new ApprovalCoordinationForm();
diff --git a/src/main/java/com/gxwebsoft/approval/doc/coupon/CouponDocumentType.java b/src/main/java/com/gxwebsoft/approval/doc/coupon/CouponDocumentType.java
index b35b7fb..0ffd223 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/coupon/CouponDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/coupon/CouponDocumentType.java
@@ -88,7 +88,7 @@ public class CouponDocumentType extends AbstractApprovalDocumentType {
@Override
public String docType() {
- return ApprovalConstants.FLOW_COUPON;
+ return ApprovalConstants.DOC_COUPON;
}
@Override
@@ -137,7 +137,8 @@ public class CouponDocumentType extends AbstractApprovalDocumentType {
* 调用方(ApprovalDocumentService)已在外层事务内调用,跨库解析不在这里做。
*/
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
ApprovalCouponParam coupon = cast(param);
List> rows = ApprovalLineSupport.normalize(coupon.getLines(), LINE_COLUMNS);
validateRows(rows);
diff --git a/src/main/java/com/gxwebsoft/approval/doc/facility/FacilityDocumentType.java b/src/main/java/com/gxwebsoft/approval/doc/facility/FacilityDocumentType.java
index 3e8c145..c301c27 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/facility/FacilityDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/facility/FacilityDocumentType.java
@@ -76,7 +76,7 @@ public class FacilityDocumentType extends AbstractApprovalDocumentType {
@Override
public String docType() {
- return ApprovalConstants.FLOW_FACILITY;
+ return ApprovalConstants.DOC_FACILITY;
}
@Override
@@ -130,7 +130,8 @@ public class FacilityDocumentType extends AbstractApprovalDocumentType {
}
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
ApprovalFacilityParam facility = cast(param);
LocalDate effectiveDate = requireEffectiveDate(facility.getEffectiveDate());
List> rows = ApprovalLineSupport.normalize(facility.getLines(), lineColumns());
diff --git a/src/main/java/com/gxwebsoft/approval/doc/facility/FacilityPriceQueryService.java b/src/main/java/com/gxwebsoft/approval/doc/facility/FacilityPriceQueryService.java
index 8fea5df..06f069b 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/facility/FacilityPriceQueryService.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/facility/FacilityPriceQueryService.java
@@ -53,8 +53,9 @@ public class FacilityPriceQueryService implements ApprovalPriceProvider {
if (tenantId == null) {
return null;
}
+ // 当前生效版本:只看到期已生效的版本
List rows = priceMapper.selectArchivedVersions(
- tenantId, LocalDate.now(), 0, 1);
+ tenantId, false, LocalDate.now(), 0, 1);
return rows.isEmpty() ? null : toVersion(tenantId, rows.get(0));
}
@@ -76,8 +77,9 @@ public class FacilityPriceQueryService implements ApprovalPriceProvider {
}
int size = limit <= 0 ? DEFAULT_HISTORY_LIMIT : limit;
int pageNo = page <= 0 ? 1 : page;
+ // 历史版本:包含未来生效的已归档版本(#58 验收要求历史可查)
List rows = priceMapper.selectArchivedVersions(
- tenantId, LocalDate.now(), (pageNo - 1) * size, size);
+ tenantId, true, LocalDate.now(), (pageNo - 1) * size, size);
List versions = new ArrayList<>(rows.size());
for (FacilityPriceVersionRow row : rows) {
ApprovalPriceVersion version = toVersion(tenantId, row);
diff --git a/src/main/java/com/gxwebsoft/approval/doc/receipt/ReceiptDocumentType.java b/src/main/java/com/gxwebsoft/approval/doc/receipt/ReceiptDocumentType.java
index de63230..cb9e2a9 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/receipt/ReceiptDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/receipt/ReceiptDocumentType.java
@@ -81,7 +81,7 @@ public class ReceiptDocumentType extends AbstractApprovalDocumentType {
@Override
public String docType() {
- return DOC_TYPE;
+ return ApprovalConstants.DOC_RECEIPT;
}
@Override
@@ -100,6 +100,12 @@ public class ReceiptDocumentType extends AbstractApprovalDocumentType {
}
/** 转账凭证至少一张:只要求整单有附件会让别的材料顶替掉凭证(ADR 0014)。 */
+ /** 受理部门固定为财务部(ADR 0014)。 */
+ @Override
+ public String acceptOrgCode() {
+ return "APPROVAL_FINANCE";
+ }
+
@Override
public String requiredAttachmentCategory() {
return ApprovalConstants.CATEGORY_VOUCHER;
@@ -125,7 +131,8 @@ public class ReceiptDocumentType extends AbstractApprovalDocumentType {
}
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
ApprovalReceiptParam receipt = cast(param);
ApprovalReceiptForm form = bizId == null ? null : receiptFormMapper.selectById(bizId);
if (form == null) {
diff --git a/src/main/java/com/gxwebsoft/approval/doc/settlement/SettlementDocumentType.java b/src/main/java/com/gxwebsoft/approval/doc/settlement/SettlementDocumentType.java
index 38d66cb..ebba566 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/settlement/SettlementDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/settlement/SettlementDocumentType.java
@@ -70,7 +70,7 @@ public class SettlementDocumentType extends AbstractApprovalDocumentType {
@Override
public String docType() {
- return ApprovalConstants.FLOW_SETTLEMENT;
+ return ApprovalConstants.DOC_SETTLEMENT;
}
@Override
@@ -121,7 +121,8 @@ public class SettlementDocumentType extends AbstractApprovalDocumentType {
* 调用方(ApprovalDocumentService)已在外层事务内调用,跨库解析不在这里做。
*/
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
ApprovalSettlementParam settlement = cast(param);
List> rows = ApprovalLineSupport.normalize(settlement.getLines(), LINE_COLUMNS);
diff --git a/src/main/java/com/gxwebsoft/approval/doc/vehicle/VehicleDocumentType.java b/src/main/java/com/gxwebsoft/approval/doc/vehicle/VehicleDocumentType.java
index 2de51f5..050ed8b 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/vehicle/VehicleDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/vehicle/VehicleDocumentType.java
@@ -111,7 +111,7 @@ public class VehicleDocumentType extends AbstractApprovalDocumentType {
@Override
public String docType() {
- return ApprovalConstants.FLOW_VEHICLE;
+ return ApprovalConstants.DOC_VEHICLE;
}
@Override
@@ -158,7 +158,8 @@ public class VehicleDocumentType extends AbstractApprovalDocumentType {
* 调用方(ApprovalDocumentService)已在外层事务内调用,跨库解析不在这里做。
*/
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
ApprovalVehicleParam vehicle = cast(param);
List> rows = ApprovalLineSupport.normalize(vehicle.getLines(), LINE_COLUMNS);
validateRows(rows);
diff --git a/src/main/java/com/gxwebsoft/approval/doc/venue/VenueDocumentType.java b/src/main/java/com/gxwebsoft/approval/doc/venue/VenueDocumentType.java
index 8321102..fe09e73 100644
--- a/src/main/java/com/gxwebsoft/approval/doc/venue/VenueDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/doc/venue/VenueDocumentType.java
@@ -59,7 +59,7 @@ public class VenueDocumentType extends AbstractApprovalDocumentType {
/** doc_type 与 flow_code 同值(本单只有一个流程);复用流程常量避免两处写字面量。 */
@Override
public String docType() {
- return ApprovalConstants.FLOW_VENUE;
+ return ApprovalConstants.DOC_VENUE;
}
@Override
@@ -97,7 +97,8 @@ public class VenueDocumentType extends AbstractApprovalDocumentType {
}
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
ApprovalVenueParam venue = cast(param);
ApprovalVenueForm form = bizId == null || bizId <= 0 ? null : venueFormMapper.selectById(bizId);
if (form == null) {
diff --git a/src/main/java/com/gxwebsoft/approval/document/AbstractApprovalDocumentType.java b/src/main/java/com/gxwebsoft/approval/document/AbstractApprovalDocumentType.java
index b8790b6..ac1eaa8 100644
--- a/src/main/java/com/gxwebsoft/approval/document/AbstractApprovalDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/document/AbstractApprovalDocumentType.java
@@ -44,6 +44,12 @@ public abstract class AbstractApprovalDocumentType implements ApprovalDocumentTy
return new ArrayList<>();
}
+ /** 默认无预解析。 */
+ @Override
+ public Object prepare(ApprovalDocumentParam param, User user) {
+ return null;
+ }
+
/** 必填文本校验,返回去掉首尾空格的值。 */
protected String requireText(String label, String value, int maxLength) {
String trimmed = value == null ? "" : value.trim();
diff --git a/src/main/java/com/gxwebsoft/approval/document/ApprovalDocumentRegistry.java b/src/main/java/com/gxwebsoft/approval/document/ApprovalDocumentRegistry.java
index 1949f40..14fb67c 100644
--- a/src/main/java/com/gxwebsoft/approval/document/ApprovalDocumentRegistry.java
+++ b/src/main/java/com/gxwebsoft/approval/document/ApprovalDocumentRegistry.java
@@ -20,7 +20,6 @@ import java.util.Map;
public class ApprovalDocumentRegistry {
private final Map byFlowCode = new LinkedHashMap<>();
- private final Map byDocType = new LinkedHashMap<>();
public ApprovalDocumentRegistry(List documentTypes) {
for (ApprovalDocumentType type : documentTypes) {
@@ -28,8 +27,6 @@ public class ApprovalDocumentRegistry {
if (previous != null) {
throw new IllegalStateException("审批流程编码重复注册: " + type.flowCode());
}
- // doc_type 可被多个流程共用(内部/外部协调单),以第一个注册的为准
- byDocType.putIfAbsent(type.docType(), type);
}
}
@@ -46,10 +43,6 @@ public class ApprovalDocumentRegistry {
return flowCode == null ? null : byFlowCode.get(flowCode);
}
- public ApprovalDocumentType findByDocType(String docType) {
- return docType == null ? null : byDocType.get(docType);
- }
-
public List all() {
return Collections.unmodifiableList(new ArrayList<>(byFlowCode.values()));
}
diff --git a/src/main/java/com/gxwebsoft/approval/document/ApprovalDocumentType.java b/src/main/java/com/gxwebsoft/approval/document/ApprovalDocumentType.java
index 9adf768..abe4ced 100644
--- a/src/main/java/com/gxwebsoft/approval/document/ApprovalDocumentType.java
+++ b/src/main/java/com/gxwebsoft/approval/document/ApprovalDocumentType.java
@@ -51,6 +51,15 @@ public interface ApprovalDocumentType {
return Collections.emptyList();
}
+ /**
+ * 固定受理部门组织编码:单据的业务受理方写死在流程里(如收据单的财务部)。
+ *
+ * 返回非空时,提交会把该部门快照到实例的受理部门,列表与导出因此能显示。
+ */
+ default String acceptOrgCode() {
+ return null;
+ }
+
/** 提交预检要求的附件分类;null 沿用「整单至少一张附件」。 */
default String requiredAttachmentCategory() {
return null;
@@ -59,15 +68,33 @@ public interface ApprovalDocumentType {
/** 草稿参数类型。 */
Class extends ApprovalDocumentParam> paramClass();
- /** 字段校验,失败抛 BusinessException。 */
+ /**
+ * 字段校验,失败抛 BusinessException。
+ *
+ * 在事务外执行,可以访问 core 库(组织、角色)。
+ */
void validate(ApprovalDocumentParam param, User user);
+ /**
+ * 写库前的预解析:把需要 core 库的数据(如受理部门快照)解析好交给 {@link #persist}。
+ *
+ * {@code persist} 在事务内执行,不能调 {@code @DS("server")} 的服务;
+ * 需要跨库数据时必须在这里解析。默认无操作,返回 null。
+ */
+ default Object prepare(ApprovalDocumentParam param, User user) {
+ return null;
+ }
+
/**
* 写业务表:新建时 bizId 为 null,更新时传入已有 bizId。
*
+ * 由 {@code ApprovalDocumentService} 在事务内调用,因此实现不得访问 core 库;
+ * 需要的跨库数据由 {@link #prepare} 提前解析后经 {@code prepared} 传入。
+ *
+ * @param prepared {@link #prepare} 的返回值
* @return 业务表主键
*/
- Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user);
+ Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user, Object prepared);
/** 把业务字段与明细写入详情。 */
void load(ApprovalInstance instance, ApprovalDetail detail);
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalFacilityPriceMapper.java b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalFacilityPriceMapper.java
index 2fd925b..5a16887 100644
--- a/src/main/java/com/gxwebsoft/approval/mapper/ApprovalFacilityPriceMapper.java
+++ b/src/main/java/com/gxwebsoft/approval/mapper/ApprovalFacilityPriceMapper.java
@@ -25,6 +25,7 @@ public interface ApprovalFacilityPriceMapper {
*/
@InterceptorIgnore(tenantLine = "true")
List selectArchivedVersions(@Param("tenantId") Integer tenantId,
+ @Param("includeFuture") boolean includeFuture,
@Param("today") LocalDate today,
@Param("offset") int offset,
@Param("limit") int limit);
diff --git a/src/main/java/com/gxwebsoft/approval/mapper/xml/ApprovalFacilityPriceMapper.xml b/src/main/java/com/gxwebsoft/approval/mapper/xml/ApprovalFacilityPriceMapper.xml
index 9dd2792..1a39721 100644
--- a/src/main/java/com/gxwebsoft/approval/mapper/xml/ApprovalFacilityPriceMapper.xml
+++ b/src/main/java/com/gxwebsoft/approval/mapper/xml/ApprovalFacilityPriceMapper.xml
@@ -2,7 +2,11 @@
-
+
SELECT i.id AS instance_id,
i.biz_no AS biz_no,
@@ -14,7 +18,9 @@
WHERE i.tenant_id = #{tenantId}
AND i.flow_code = 'facility'
AND i.status = 'ARCHIVED'
+
AND f.effective_date <= #{today}
+
ORDER BY f.effective_date DESC, i.archive_time DESC, i.id DESC
LIMIT #{limit} OFFSET #{offset}
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalApproverResolver.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalApproverResolver.java
index 7f87830..605f2b6 100644
--- a/src/main/java/com/gxwebsoft/approval/service/ApprovalApproverResolver.java
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalApproverResolver.java
@@ -210,7 +210,8 @@ public class ApprovalApproverResolver {
if (ApprovalConstants.RESOLVER_ORG_LEADER_BY_CODE.equals(resolver)
|| ApprovalConstants.RESOLVER_REPORTER_ORG_LEADER.equals(resolver)
|| ApprovalConstants.RESOLVER_ACCEPT_ORG_LEADER.equals(resolver)) {
- return groupName + "负责人角色缺配";
+ // 组名本身已含部门名(如「财务部」);避免拼成「财务部负责人负责人角色缺配」
+ return groupName.endsWith("负责人") ? groupName + "角色缺配" : groupName + "负责人角色缺配";
}
return groupName + "审批人缺配";
}
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalDocumentService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalDocumentService.java
index 05ca62c..d2d0cc8 100644
--- a/src/main/java/com/gxwebsoft/approval/service/ApprovalDocumentService.java
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalDocumentService.java
@@ -63,17 +63,18 @@ public class ApprovalDocumentService {
throw new BusinessException("参数不能为空");
}
type.validate(param, user);
+ Object prepared = type.prepare(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));
+ updateDraft(type, param, user, organizationId, organizationName, prepared));
}
ApprovalFlowDefinition definition = flowService.getOrCreate(flowCode);
return inTransaction(() ->
- createDraft(type, param, user, definition, organizationId, organizationName));
+ createDraft(type, param, user, definition, organizationId, organizationName, prepared));
}
/**
@@ -181,7 +182,7 @@ public class ApprovalDocumentService {
private Long createDraft(ApprovalDocumentType type, ApprovalDocumentParam param, User user,
ApprovalFlowDefinition definition, Integer organizationId,
- String organizationName) {
+ String organizationName, Object prepared) {
ApprovalInstance instance = new ApprovalInstance();
instance.setFlowCode(type.flowCode());
instance.setDefinitionId(definition.getId());
@@ -196,7 +197,7 @@ public class ApprovalDocumentService {
instance.setReportOrgName(organizationName);
instanceMapper.insert(instance);
- Long bizId = type.persist(instance.getId(), null, param, user);
+ Long bizId = type.persist(instance.getId(), null, param, user, prepared);
instance.setBizId(bizId == null ? 0L : bizId);
instance.setTitle(type.title(instance));
instance.setSearchText(truncate(type.searchText(instance)));
@@ -205,7 +206,7 @@ public class ApprovalDocumentService {
}
private Long updateDraft(ApprovalDocumentType type, ApprovalDocumentParam param, User user,
- Integer organizationId, String organizationName) {
+ Integer organizationId, String organizationName, Object prepared) {
ApprovalInstance instance = instanceMapper.selectById(param.getId());
if (instance == null) {
throw new BusinessException("单据不存在");
@@ -219,7 +220,7 @@ public class ApprovalDocumentService {
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
throw new BusinessException(ApprovalConstants.writeBlockedMessage(instance.getStatus()));
}
- Long bizId = type.persist(instance.getId(), instance.getBizId(), param, user);
+ Long bizId = type.persist(instance.getId(), instance.getBizId(), param, user, prepared);
if (bizId != null) {
instance.setBizId(bizId);
}
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java
index 3ba4e8f..4d372c7 100644
--- a/src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java
@@ -154,6 +154,30 @@ public class ApprovalEngineService {
return taskMapper.update(null, wrapper);
}
+ /**
+ * 指定用户在节点本轮所属的会签组标识(去重,按出现顺序)。
+ *
+ * 一人同属多组时,一次通过同时签了他所在的所有组,因此这些组的其余成员待办
+ * 也要一并取消——否则那些组会永远停在 PENDING,节点无法推进。
+ */
+ public List assigneeGroupKeys(Long instanceId, Integer round, String nodeKey,
+ Integer assigneeId) {
+ List tasks = taskMapper.selectList(new LambdaQueryWrapper()
+ .eq(ApprovalTask::getInstanceId, instanceId)
+ .eq(ApprovalTask::getRound, round)
+ .eq(ApprovalTask::getNodeKey, nodeKey)
+ .eq(ApprovalTask::getAssigneeId, assigneeId)
+ .orderByAsc(ApprovalTask::getGroupSeq)
+ .orderByAsc(ApprovalTask::getId));
+ Map ordered = new LinkedHashMap<>();
+ for (ApprovalTask task : tasks) {
+ if (task.getGroupKey() != null) {
+ ordered.putIfAbsent(task.getGroupKey(), Boolean.TRUE);
+ }
+ }
+ return new ArrayList<>(ordered.keySet());
+ }
+
/**
* 该节点该轮仍待处理的组标识集合(按 group_seq 顺序)。
*/
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalExportService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalExportService.java
index 58c9875..6855027 100644
--- a/src/main/java/com/gxwebsoft/approval/service/ApprovalExportService.java
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalExportService.java
@@ -252,7 +252,7 @@ public class ApprovalExportService {
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())));
+ dataRow.createCell(col++).setCellValue(lineValue(column, exportLine.line.get(column.getKey())));
}
}
}
@@ -268,14 +268,14 @@ public class ApprovalExportService {
if (nodes.stream().noneMatch(node -> node.getGroups() != null && !node.getGroups().isEmpty())) {
return "";
}
+ // 一次装配全部会签进度,避免按节点重复查询(导出批量时是 N 倍开销)
+ com.gxwebsoft.approval.vo.ApprovalDetail detail = new com.gxwebsoft.approval.vo.ApprovalDetail();
+ documentService.fillGroupProgress(instance, detail);
+ List progress = detail.getGroupProgress();
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 progress = detail.getGroupProgress();
int signedGroups = 0;
int totalGroups = 0;
for (com.gxwebsoft.approval.vo.ApprovalGroupProgressVO group : progress) {
@@ -321,10 +321,22 @@ public class ApprovalExportService {
};
}
- private String lineValue(Object value) {
+ /**
+ * 明细列取值:ENUM 列输出中文文案而不是存储编码(导出给人看,不是给程序看)。
+ */
+ private String lineValue(ApprovalLineSpec column, Object value) {
if (value == null) {
return "";
}
+ if (ApprovalLineSpec.TYPE_ENUM.equals(column.getType()) && column.getOptions() != null) {
+ String code = String.valueOf(value);
+ for (ApprovalLineSpec.Option option : column.getOptions()) {
+ if (code.equals(option.getValue())) {
+ return option.getLabel();
+ }
+ }
+ return code;
+ }
if (value instanceof BigDecimal) {
return ((BigDecimal) value).stripTrailingZeros().toPlainString();
}
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java
index cbeed15..6286a65 100644
--- a/src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java
@@ -133,16 +133,32 @@ public class ApprovalInstanceService {
}
// 协调单的受理部门在提交时快照到实例;外部流程的分管领导按受理部门解析
ApprovalOrganizationVO acceptOrganization = preflightAcceptOrganization(type, instance);
+ if (acceptOrganization == null && type.acceptOrgCode() != null) {
+ // 业务受理方写死在流程里的单据(如收据单的财务部):快照到实例供列表/导出显示
+ acceptOrganization = directoryService.getOrganizationByCode(type.acceptOrgCode());
+ if (acceptOrganization == null) {
+ throw new BusinessException("受理部门不存在或已停用");
+ }
+ }
// 预检要在内存里先带上受理部门快照:节点解析器(acceptOrgLeader/acceptOrgDeputy)读的是实例字段,
// 而快照在 doSubmit 才落库
if (acceptOrganization != null) {
instance.setAcceptOrgId(acceptOrganization.getOrganizationId());
instance.setAcceptOrgName(acceptOrganization.getOrganizationName());
}
- // 附件分类与逐节点逐组审批人解析(ADR 0003 / 0005)
- documentService.preflight(instance, flowService.parseNodes(instance.getDefinitionId()));
+ ApprovalOrganizationVO resolvedAcceptOrg = acceptOrganization;
+ List nodes = flowService.parseNodes(instance.getDefinitionId());
+ // 附件分类与逐节点逐组审批人解析(ADR 0003 / 0005),必须在事务外完成
+ documentService.preflight(instance, nodes);
+ if (nodes.isEmpty()) {
+ throw new BusinessException("审批流程未配置节点");
+ }
+ // 首节点审批人同样在事务外解析:TransactionTemplate 内 @DS("server") 不生效
+ ApprovalApproverResolver.Resolution firstNode =
+ approverResolver.resolve(instance, nodes.get(0));
- return inTransaction(() -> doSubmit(instanceId, user, organization, acceptOrganization, type));
+ return inTransaction(() ->
+ doSubmit(instanceId, user, organization, resolvedAcceptOrg, type, firstNode));
}
/**
@@ -183,7 +199,8 @@ public class ApprovalInstanceService {
}
private String doSubmit(Long instanceId, User user, ApprovalOrganizationVO organization,
- ApprovalOrganizationVO acceptOrganization, ApprovalDocumentType type) {
+ ApprovalOrganizationVO acceptOrganization, ApprovalDocumentType type,
+ ApprovalApproverResolver.Resolution firstNode) {
ApprovalInstance locked = engineService.lockInstance(instanceId);
if (!locked.getReporterId().equals(user.getUserId())
|| !ApprovalConstants.EDITABLE_STATUSES.contains(locked.getStatus())) {
@@ -204,11 +221,7 @@ public class ApprovalInstanceService {
if (locked.getSubmitTime() == null) {
locked.setSubmitTime(LocalDateTime.now());
}
- List nodes = flowService.parseNodes(locked.getDefinitionId());
- if (nodes.isEmpty()) {
- throw new BusinessException("审批流程未配置节点");
- }
- List approvers = approverResolver.resolve(locked, nodes.get(0)).getApprovers();
+ List approvers = firstNode.getApprovers();
int tasks = engineService.startFlow(locked, approvers, user,
firstSubmit ? ApprovalConstants.ACTION_SUBMIT : ApprovalConstants.ACTION_RESUBMIT);
if (tasks == 0) {
diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalTaskService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalTaskService.java
index a730fe3..d2fe08b 100644
--- a/src/main/java/com/gxwebsoft/approval/service/ApprovalTaskService.java
+++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalTaskService.java
@@ -112,6 +112,8 @@ public class ApprovalTaskService {
}
ensureTaskCurrent(task, instance);
ApprovalDocumentType documentType = documentRegistry.find(instance.getFlowCode());
+ // 字段校验在事务外(可能查询 core 库做唯一性判断),落库前在事务内重新确认任务仍可处理;
+ // 任务若已被处理则整体回滚,不会留下没有 field_change 的字段值
List changes = writeFields(instance, documentType, task, fields, false, user);
if (changes.isEmpty()) {
return new Result(instance.getId(), null);
@@ -130,7 +132,7 @@ public class ApprovalTaskService {
* 存档确认:CONFIRM 任务只能确认(无意见),同节点同轮全部完成后实例进入审批完成,
* 并激活归档节点;归档节点缺配时实例停在审批中并提示管理员补配置。
*/
- public Result confirm(Long taskId, User user) {
+ public Result confirm(Long taskId, Map fields, User user) {
ApprovalTask task = requireTask(taskId, user);
if (!ApprovalConstants.NODE_CONFIRM.equals(task.getNodeType())) {
throw new BusinessException("当前任务不支持存档确认");
@@ -140,12 +142,15 @@ public class ApprovalTaskService {
throw new BusinessException("单据不存在");
}
ensureNotArchived(instance);
+ // ADR 0006:CONFIRM 节点也可声明可写字段(如设施价格表的运营管理部存档人)
+ ApprovalDocumentType documentType = documentRegistry.find(instance.getFlowCode());
+ List changes = writeFields(instance, documentType, task, fields, true, user);
ApprovalNode next = engineService.nextNode(instance, task.getNodeKey());
ApprovalApproverResolver.Resolution resolution = next == null
? ApprovalApproverResolver.Resolution.resolved(Collections.emptyList())
: approverResolver.resolve(instance, next);
- String warning = inTransaction(() -> doConfirm(taskId, task, user, next, resolution));
+ String warning = inTransaction(() -> doConfirm(taskId, task, user, next, resolution, changes));
return new Result(instance.getId(), warning);
}
@@ -298,12 +303,18 @@ public class ApprovalTaskService {
throw new BusinessException("任务已处理");
}
persistChanges(locked, task, changes, user);
- // 一人同属多组:一次点击完成他在本节点本轮的全部待办
+ // 一人同属多组:一次点击完成他在本节点本轮的全部待办,并签掉他所在的**每个**组,
+ // 因此这些组的其余成员待办都要取消(只取消点击任务所在组会让另一组永远卡住)
+ List signedGroups = engineService.assigneeGroupKeys(locked.getId(), locked.getRound(),
+ task.getNodeKey(), user.getUserId());
+ if (signedGroups.isEmpty() && task.getGroupKey() != null) {
+ signedGroups = java.util.Collections.singletonList(task.getGroupKey());
+ }
engineService.completeAssigneeTasks(locked.getId(), locked.getRound(), task.getNodeKey(),
user.getUserId(), taskId, opinion);
- if (task.getGroupKey() != null) {
+ for (String groupKey : signedGroups) {
engineService.cancelPendingTasksInGroup(locked.getId(), locked.getRound(), task.getNodeKey(),
- task.getGroupKey(), null);
+ groupKey, null);
}
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_APPROVE, opinion);
@@ -331,13 +342,15 @@ public class ApprovalTaskService {
}
private String doConfirm(Long taskId, ApprovalTask task, User user,
- ApprovalNode next, ApprovalApproverResolver.Resolution resolution) {
+ ApprovalNode next, ApprovalApproverResolver.Resolution resolution,
+ List changes) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureNotArchived(locked);
ensureTaskHandleable(task, locked);
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, null) == 0) {
throw new BusinessException("任务已处理");
}
+ persistChanges(locked, task, changes, user);
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_CONFIRM, null);
// 存档进度按「节点 + 轮次」聚合,同轮两个 CONFIRM 节点不会串进度(ADR 0003 修正)
diff --git a/src/main/java/com/gxwebsoft/approval/vo/ApprovalDetail.java b/src/main/java/com/gxwebsoft/approval/vo/ApprovalDetail.java
index 65a3ad4..bdab617 100644
--- a/src/main/java/com/gxwebsoft/approval/vo/ApprovalDetail.java
+++ b/src/main/java/com/gxwebsoft/approval/vo/ApprovalDetail.java
@@ -33,9 +33,6 @@ public class ApprovalDetail implements Serializable {
/** 会签节点进度(当前轮),非会签单据为空。 */
private List groupProgress = new ArrayList<>();
- /** 字段改值历史(最新在后)。 */
- private List fieldChanges = new ArrayList<>();
-
public void addField(String key, String label, Object value) {
fields.add(new ApprovalFieldVO(key, label, value == null ? null : String.valueOf(value)));
}
diff --git a/src/test/java/com/gxwebsoft/approval/ApprovalAttachmentFileRoundTripIT.java b/src/test/java/com/gxwebsoft/approval/ApprovalAttachmentFileRoundTripIT.java
index 25d80a2..36eacf8 100644
--- a/src/test/java/com/gxwebsoft/approval/ApprovalAttachmentFileRoundTripIT.java
+++ b/src/test/java/com/gxwebsoft/approval/ApprovalAttachmentFileRoundTripIT.java
@@ -93,6 +93,42 @@ class ApprovalAttachmentFileRoundTripIT {
assertThat(download.getResponse().getContentAsByteArray()).isEqualTo(payload);
}
+ /**
+ * 附件分类三档(#50):VOUCHER 上传成功、详情按三档显示名称、白名单外的值被拒。
+ */
+ @Test
+ void voucherCategoryIsAcceptedAndUnknownCategoryRejected() throws Exception {
+ long instanceId = createCompletionDraft("附件分类三档");
+
+ MockMultipartFile file = new MockMultipartFile("file", "voucher.pdf", "application/pdf",
+ new byte[]{'%', 'P', 'D', 'F', '-', '1', '.', '4'});
+ String response = mockMvc.perform(multipart("/api/approval/attachment/upload")
+ .file(file)
+ .param("instanceId", String.valueOf(instanceId))
+ .param("category", "VOUCHER")
+ .header("tenantId", TENANT)
+ .with(authentication(TestAuthentication.user(reporter.userId, reporter.username,
+ organizationId, TENANT, SUBMIT))))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.category").value("VOUCHER"))
+ .andExpect(jsonPath("$.data.categoryName").value("凭证"))
+ .andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
+ assertThat(objectMapper.readTree(response).path("data").path("id").asLong()).isPositive();
+
+ MockMultipartFile again = new MockMultipartFile("file", "sign.png", "image/png", pngBytes());
+ mockMvc.perform(multipart("/api/approval/attachment/upload")
+ .file(again)
+ .param("instanceId", String.valueOf(instanceId))
+ .param("category", "EXTERNAL")
+ .header("tenantId", TENANT)
+ .with(authentication(TestAuthentication.user(reporter.userId, reporter.username,
+ organizationId, TENANT, SUBMIT))))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(1))
+ .andExpect(jsonPath("$.message").value("附件分类不正确"));
+ }
+
// ---------- helpers ----------
private long createCompletionDraft(String name) throws Exception {
diff --git a/src/test/java/com/gxwebsoft/approval/ApprovalCountersignEngineIT.java b/src/test/java/com/gxwebsoft/approval/ApprovalCountersignEngineIT.java
index 87e0256..9517b7f 100644
--- a/src/test/java/com/gxwebsoft/approval/ApprovalCountersignEngineIT.java
+++ b/src/test/java/com/gxwebsoft/approval/ApprovalCountersignEngineIT.java
@@ -135,24 +135,31 @@ class ApprovalCountersignEngineIT {
@Test
void onePersonInTwoGroupsCompletesBothWithSingleClick() throws Exception {
- // 让第一组负责人同时挂上第二组的部门负责人角色:他会拿到两条会签任务
+ // 让第一组负责人同时挂上第二组负责人角色:他会拿到两条会签任务,
+ // 而第二组另有一名成员(groupLeaders.get(1))——该成员的待办必须随这次点击一并取消,
+ // 否则第二组永远有 PENDING,节点无法推进。
Integer secondRoleId = data.server().queryForObject(
"SELECT role_id FROM sys_role WHERE tenant_id = ? AND deleted = 0 AND role_code = ?",
Integer.class, TENANT, data.leaderRoleCode(groupOrgs.get(1)));
data.bind(groupLeaders.get(0).userId, secondRoleId);
- // 第二组只留这一个成员,使总任务数仍是 4,其中该用户占 2 条
- data.server().update("DELETE FROM sys_user_role WHERE user_id = ? AND role_id = ?",
- groupLeaders.get(1).userId, secondRoleId);
long instanceId = createAndSubmit();
- assertThat(countersignTaskCount(instanceId)).isEqualTo(4);
+ assertThat(countersignTaskCount(instanceId)).isEqualTo(5);
approve(taskFor(instanceId, groupLeaders.get(0))).andExpect(jsonPath("$.code").value(0));
+ // 本人两条任务都完成
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
+ "AND assignee_id = ? AND status = 'DONE'", instanceId, groupLeaders.get(0).userId)).isEqualTo(2);
- assertThat(currentNodeKey(instanceId)).isEqualTo("countersign");
+ // 他所在两个组的其余成员待办被取消,只剩另外两组共 2 条待办
assertThat(countersignPendingCount(instanceId)).isEqualTo(2);
+ assertThat(currentNodeKey(instanceId)).isEqualTo("countersign");
+
+ // 另外两组签完后节点正常推进:证明一人双组没有把节点卡死
+ approve(taskFor(instanceId, groupLeaders.get(2))).andExpect(jsonPath("$.code").value(0));
+ approve(taskFor(instanceId, groupLeaders.get(3)))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.currentNodeName").value("存档"));
}
@Test
diff --git a/src/test/java/com/gxwebsoft/approval/support/CountersignProbeDocumentType.java b/src/test/java/com/gxwebsoft/approval/support/CountersignProbeDocumentType.java
index 470189f..05544a4 100644
--- a/src/test/java/com/gxwebsoft/approval/support/CountersignProbeDocumentType.java
+++ b/src/test/java/com/gxwebsoft/approval/support/CountersignProbeDocumentType.java
@@ -114,7 +114,8 @@ public class CountersignProbeDocumentType extends AbstractApprovalDocumentType {
}
@Override
- public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
+ public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user,
+ Object prepared) {
return bizId == null ? instanceId : bizId;
}