Browse Source

refactor(botp): 将源单关闭标记从行级改为整单级别

- 将 closed_flag 字段定义从分录表移至主表,语义改为整单关闭
- 反写执行时将 entries 按源单分组,对每张源单主表分别求值关闭条件
- 新增 resolveTableName 方法,按单元数据 id 解析源单表名
- 简化插件排序逻辑,移除自定义 parseOrder 方法改用自然排序加 nullsLast
- 更新 BotpRuleConstant 注释,明确主表字段定位与整单关闭语义
dev-lml
chenchuchuan 1 week ago
parent
commit
975a710a46
  1. 13
      common/common-botp/src/main/java/apelet/common/botp/model/constant/BotpRuleConstant.java
  2. 30
      common/common-botp/src/main/java/apelet/common/botp/service/execute/BotpService.java
  3. 77
      common/common-botp/src/main/java/apelet/common/botp/service/execute/WriteBackOrchestrator.java
  4. 23
      common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java

13
common/common-botp/src/main/java/apelet/common/botp/model/constant/BotpRuleConstant.java

@ -22,13 +22,16 @@ public final class BotpRuleConstant { @@ -22,13 +22,16 @@ public final class BotpRuleConstant {
// ==================== 单据上的系统标准字段(列名) ====================
/**
* 源单行关闭标记列名。
* 源单关闭标记列名。
*
* <p>分录表的系统标准字段:新建表单由 {@code OnlFormHeadServiceImpl} 与 parent_id / seq
* 同批自动生成;存量表由 {@code zz-resource/db-scripts/botp-source-close-flag.sql} 补列。</p>
* <p><b>主表的系统标准字段</b>:新建表单由 {@code OnlFormHeadServiceImpl} 与
* billstatus / srcbillid 等同批自动生成;存量表由
* {@code zz-resource/db-scripts/botp-source-close-flag.sql} 补列。</p>
*
* <p>反写规则的「源单行关闭条件」成立时写 1、不成立时写 0——
* 撤销/弃审把数量退回后条件不再成立,会写回 0 自动解除关闭。</p>
* <p>语义是<b>整单关闭</b>:反写规则的「源单行关闭条件」成立(下推量与回写量相等)时写 1、
* 不成立时写 0——撤销/弃审把数量退回后条件不再成立,会写回 0 自动解除关闭。</p>
*
* <p>挂在主表而非分录表:条件比较的是单据级的数量,且单表单据没有分录表可挂。</p>
*/
public static final String COLUMN_CLOSED_FLAG = "closed_flag";

30
common/common-botp/src/main/java/apelet/common/botp/service/execute/BotpService.java

@ -3,11 +3,7 @@ package apelet.common.botp.service.execute; @@ -3,11 +3,7 @@ package apelet.common.botp.service.execute;
import apelet.common.botp.dao.ConvertRelationEntryMapper;
import apelet.common.botp.dao.ConvertRelationMapper;
import apelet.common.botp.exception.BotpMappingException;
import apelet.common.botp.model.ConvertFieldmappingEntry;
import apelet.common.botp.model.ConvertPluginEntry;
import apelet.common.botp.model.ConvertRelation;
import apelet.common.botp.model.ConvertRelationEntry;
import apelet.common.botp.model.ConvertRule;
import apelet.common.botp.model.*;
import apelet.common.botp.model.constant.BotpRuleConstant;
import apelet.common.botp.model.constant.MergeMethodEnum;
import apelet.common.botp.model.constant.ValueMethodEnum;
@ -62,13 +58,10 @@ import java.util.*; @@ -62,13 +58,10 @@ import java.util.*;
public class BotpService {
/**
* 插件执行顺序:按 serialNumber 数值升序。
*
* <p>serialNumber 在库里是 varchar,直接按字符串比较会得到 {@code "10" < "2"} 的错误顺序;
* 这里解析成数值再排。解析不了的排在最后。彻底修法是把它改成整型列(需存量数据迁移)。</p>
* 插件执行顺序:按 serialNumber 升序,未配置的排在最后。
*/
private static final Comparator<ConvertPluginEntry> PLUGIN_ORDER =
Comparator.comparingInt(entry -> parseOrder(entry.getSerialNumber()));
Comparator.comparing(ConvertPluginEntry::getSerialNumber, Comparator.nullsLast(Comparator.naturalOrder()));
@Autowired
private ConvertRelationMapper convertRelationMapper;
@ -703,21 +696,4 @@ public class BotpService { @@ -703,21 +696,4 @@ public class BotpService {
String autoSave = convertRule.getAutoSave();
return BotpRuleConstant.SWITCH_ON_NUMERIC_VALUE.equals(autoSave) || BotpRuleConstant.SWITCH_ON_VALUE.equals(autoSave);
}
/**
* 解析插件执行顺序,非数字排在最后。
*
* @param serialNumber 配置里的顺序值。
* @return 可比较的数值。
*/
private static int parseOrder(String serialNumber) {
if (StringUtils.isBlank(serialNumber)) {
return Integer.MAX_VALUE;
}
try {
return Integer.parseInt(serialNumber.trim());
} catch (NumberFormatException e) {
return Integer.MAX_VALUE;
}
}
}

77
common/common-botp/src/main/java/apelet/common/botp/service/execute/WriteBackOrchestrator.java

@ -358,17 +358,14 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade { @@ -358,17 +358,14 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
}
/**
* 源单行关闭:关闭标记始终反映「当前是否满足关闭条件」。
* 源单关闭:关闭标记始终反映「当前是否满足关闭条件」。
*
* <p><b>按源分录行逐行求值</b>,不是拿表头求值再一刀切——「已下推数量 &gt;= 数量」这类条件
* 引用的是分录行上的字段,用表头求值永远取不到值。</p>
* <p><b>单据级语义</b>:条件比较的是整张源单的数量(下推量与回写量相等即关闭),
* 所以写的是<b>源单主表</b>那一行,不是分录行。多对一时一张目标单可能对应多张源单,
* 按源单分别求值、分别关闭。</p>
*
* <p><b>条件成立写 1,不成立写 0</b>:撤销 / 弃审把数量退回来之后条件不再成立,
* 这里会把标记清掉、解除关闭(PRD §6.3 ④)。只写 1 不写 0 的话,被关掉的行永远关着。</p>
*
* <p>条件里的字段引用建议写成 {@code 分录表.分录表.字段} 形式(如
* {@code pur_apply_entry.pur_apply_entry.orderqty &gt;= pur_apply_entry.pur_apply_entry.qty}),
* 这样在行上下文里能直接解析。</p>
* 这里会把标记清掉、解除关闭(PRD §6.3 ④)。只写 1 不写 0 的话,被关掉的单永远关着。</p>
*
* @param rule 反写规则。
* @param entries 关联关系。
@ -379,21 +376,25 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade { @@ -379,21 +376,25 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
if (StringUtils.isBlank(expression)) {
return;
}
String sourceTable = resolveTableName(String.valueOf(rule.getSourceDocuments()));
if (StringUtils.isBlank(sourceTable)) {
log.warn("反写规则未配置源单或源单元数据不存在,跳过源单关闭。规则id={}", rule.getId());
return;
}
Map<String, List<ConvertRelationEntry>> bySourceBill = new LinkedHashMap<>();
for (ConvertRelationEntry entry : entries) {
String table = entry.getSourceEntryNumber();
if (StringUtils.isBlank(table)) {
// 走了 srcbillid 回退分支时拿不到源分录表名,无法安全写回,跳过
log.warn("源分录表名缺失(可能走了 srcbillid 回退分支),跳过源单行关闭。规则id={}", rule.getId());
return;
if (StringUtils.isBlank(entry.getSourceId())) {
continue;
}
ObjectValue sourceRow = queryRow(table, entry.getSourceEntryId());
if (sourceRow == null) {
bySourceBill.computeIfAbsent(entry.getSourceId(), k -> new ArrayList<>()).add(entry);
}
for (String sourceBillId : bySourceBill.keySet()) {
ObjectValue sourceBill = queryRow(sourceTable, sourceBillId);
if (sourceBill == null) {
continue;
}
// 关闭标记始终反映「当前是否满足关闭条件」:正向推满时置 1,
// 撤销/弃审把数量退回来之后条件不再成立,这里会写回 0 解除关闭(PRD §6.3 ④)。
boolean shouldClose = conditionHit(expression, null, sourceRow);
writeSourceRow(table, entry.getSourceEntryId(), BotpRuleConstant.COLUMN_CLOSED_FLAG, shouldClose ? 1 : 0);
boolean shouldClose = conditionHit(expression, null, sourceBill);
writeSourceRow(sourceTable, sourceBillId, BotpRuleConstant.COLUMN_CLOSED_FLAG, shouldClose ? 1 : 0);
}
}
@ -425,6 +426,25 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade { @@ -425,6 +426,25 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
}
/**
* 按单元数据 id 解析表名。
*
* @param headId 单元数据 id。
* @return 表名;解析不到时返回 null。
*/
private String resolveTableName(String headId) {
if (StringUtils.isBlank(headId)) {
return null;
}
try {
OnlFormHead head = onlFormHeadService.getById(Long.valueOf(headId));
return head == null ? null : head.getTableName();
} catch (Exception e) {
log.warn("解析单元数据表名失败,headId={},原因={}", headId, e.getMessage());
return null;
}
}
/**
* 覆盖方式记原值:把反写前的源单字段值写进关联关系,撤销时读出来恢复。
*
* @param entry 关联关系。
@ -563,7 +583,7 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade { @@ -563,7 +583,7 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
if (entries.isEmpty()) {
return Collections.emptyList();
}
entries.sort((a, b) -> Integer.compare(parseOrder(a.getSerialNumber()), parseOrder(b.getSerialNumber())));
entries.sort(Comparator.comparing(ConvertPluginEntry::getSerialNumber, Comparator.nullsLast(Comparator.naturalOrder())));
List<WriteBackPlugIn> plugins = new ArrayList<>(entries.size());
for (ConvertPluginEntry entry : entries) {
plugins.add(plugInLoader.load(entry.getClassName(), WriteBackPlugIn.class));
@ -782,23 +802,6 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade { @@ -782,23 +802,6 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
}
/**
* 解析插件执行顺序,非数字排在最后。
*
* @param serialNumber 配置里的顺序值。
* @return 可比较的数值。
*/
private static int parseOrder(String serialNumber) {
if (StringUtils.isBlank(serialNumber)) {
return Integer.MAX_VALUE;
}
try {
return Integer.parseInt(serialNumber.trim());
} catch (NumberFormatException e) {
return Integer.MAX_VALUE;
}
}
/**
* 校验请求入参。
*
* @param request 反写触发请求。

23
common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java

@ -833,6 +833,18 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo @@ -833,6 +833,18 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo
srcentryid.setIsDefault(1);
list.add(srcentryid);
// 源单关闭标记:反写规则的「源单行关闭条件」成立时置 1,撤销/弃审不满足条件时置回 0。
// 挂在主表上是「整单关闭」语义——下推量与回写量相等即关闭该源单,不再出现在选单候选里
// (可在转换规则的「选单过滤」中引用 closed_flag = 0)。
OnlFormField closedFlag = new OnlFormField();
closedFlag.setFieldName("closed_flag");
closedFlag.setFieldRemark("源单关闭标记(1:已关闭 0:未关闭)");
closedFlag.setFieldType(CustomUtil.FieldType_int);
closedFlag.setRowDisabled(1);
closedFlag.setIsEmpty(1);
closedFlag.setIsDefault(1);
list.add(closedFlag);
}
if (StringUtils.isNotEmpty(metaType) && metaType.equals(CustomUtil.MetaType_Entry)) {
@ -853,17 +865,6 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo @@ -853,17 +865,6 @@ public class OnlFormHeadServiceImpl extends ServiceImpl<OnlFormHeadMapper, OnlFo
seq.setIsEmpty(1);
seq.setIsDefault(1);
list.add(seq);
// 源单行关闭标记:反写规则「源单行关闭条件」成立时置 1,撤销/弃审不满足条件时置回 0。
// 用于让推满的行不再出现在选单候选里(可在转换规则的「选单过滤」中引用 closed_flag = 0)。
OnlFormField closedFlag = new OnlFormField();
closedFlag.setFieldName("closed_flag");
closedFlag.setFieldRemark("源单行关闭标记(1:已关闭 0:未关闭)");
closedFlag.setFieldType(CustomUtil.FieldType_int);
closedFlag.setRowDisabled(1);
closedFlag.setIsEmpty(1);
closedFlag.setIsDefault(1);
list.add(closedFlag);
}
// //流程表单需要添加的参数

Loading…
Cancel
Save