fix: 代码审查修复

- 会签:一人多组时取消其所在**每个组**的其余待办(原先只取消点击任务所在组,另一组永久 PENDING 卡单)
- 跨库:提交首节点审批人解析移出事务;协调单受理部门改为 prepare 事务外解析
- 回填:CONFIRM 节点支持 writableFields(ADR 0006 三类节点均可声明)
- 收据单:受理部门(财务部)快照到实例,列表/导出可见
- 明细导出:ENUM 列输出中文文案而非存储编码
- 价格表:历史版本包含未来生效的已归档版本
- 导出会签进度一次装配,去掉按节点重复查询
- 菜单种子:价格菜单在 superAdmin 授权之前,授权条件覆盖 approval:%
- doc_type 与 flow_code 常量分开;删除未使用的 findByDocType / ApprovalDetail.fieldChanges
- 补附件三档白名单用例(VOUCHER 通过、EXTERNAL 被拒)
This commit is contained in:
weicw
2026-10-31 08:22:04 +08:00
parent 25b40d83c6
commit 599d85b7da
28 changed files with 263 additions and 92 deletions
@@ -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 {
@@ -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
@@ -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;
}