Browse Source

fix(botp): 修复分组合并时源单查询报错与子表明细丢失

- ConvertOrchestrator#loadGroupKeys:orm 的 FilterItem.in 不支持集合,
  传 List<Long> 会生成 id in ('[1,2,3]') 的非法 SQL。改用 id = ? OR id = ?
  组合,仍是单次查询;异常消息补上底层原因。
- BotpService#groupSourceRows:分录归并误用表头级 group_by_field 作行键,
  分录表无该列导致全部源行塌进同一组、目标单只剩一条明细。改为每条源
  分录行对应一条目标分录行,分单字段仅用于决定生成几张目标单。
dev-lml
lml 7 days ago
parent
commit
846569f7f2
  1. 43
      common/common-botp/src/main/java/apelet/common/botp/service/execute/BotpService.java
  2. 32
      common/common-botp/src/main/java/apelet/common/botp/service/execute/ConvertOrchestrator.java

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

@ -103,7 +103,7 @@ public class BotpService {
} }
ConvertMappingPlan plan = buildMappingPlan(convertFieldmappingEntryList, srcFormHead, destFormHead); ConvertMappingPlan plan = buildMappingPlan(convertFieldmappingEntryList, srcFormHead, destFormHead);
List<ObjectValue> sourceBills = loadSourceBills(plan, srcFormHead, srcBillIds); List<ObjectValue> sourceBills = loadSourceBills(plan, srcFormHead, srcBillIds);
Map<String, List<ConvertSourceRowGroup>> entryGroups = buildEntryGroups(plan, sourceBills, convertRule); Map<String, List<ConvertSourceRowGroup>> entryGroups = buildEntryGroups(plan, sourceBills);
BtpTransformResult result = initDestBillData(plan, sourceBills, convertRule, entryGroups); BtpTransformResult result = initDestBillData(plan, sourceBills, convertRule, entryGroups);
ObjectValue destBillData = result.getBillDataList().getObject(0); ObjectValue destBillData = result.getBillDataList().getObject(0);
applyMappings(plan, destBillData, sourceBills, convertRule, entryGroups); applyMappings(plan, destBillData, sourceBills, convertRule, entryGroups);
@ -358,59 +358,52 @@ public class BotpService {
} }
/** /**
* 为每个目标分录表预计算归并结果,供建骨架与逐列映射共用(避免算两遍)。 * 为每个目标分录表预计算源行分组,供建骨架与逐列映射共用(避免算两遍)。
* *
* @param plan 映射计划。 * @param plan 映射计划。
* @param sourceBills 源单数据。 * @param sourceBills 源单数据。
* @param convertRule 转换规则(提供行键字段)。 * @return 目标分录表名 → 源行组。
* @return 目标分录表名 → 归并后的源行组。
*/ */
private Map<String, List<ConvertSourceRowGroup>> buildEntryGroups(ConvertMappingPlan plan, List<ObjectValue> sourceBills, ConvertRule convertRule) { private Map<String, List<ConvertSourceRowGroup>> buildEntryGroups(ConvertMappingPlan plan, List<ObjectValue> sourceBills) {
Map<String, List<ConvertSourceRowGroup>> entryGroups = new LinkedHashMap<>(); Map<String, List<ConvertSourceRowGroup>> entryGroups = new LinkedHashMap<>();
for (String destEntryTable : plan.getDestEntryColumns().keySet()) { for (String destEntryTable : plan.getDestEntryColumns().keySet()) {
entryGroups.put(destEntryTable, groupSourceRows(plan, sourceBills, destEntryTable, convertRule.getGroupByField())); entryGroups.put(destEntryTable, groupSourceRows(plan, sourceBills, destEntryTable));
} }
return entryGroups; return entryGroups;
} }
/** /**
* 把批次内所有源单的某张源分录表摊平后按行键归并。 * 把批次内所有源单的某张源分录表摊平成「一行源行一组」。
*
* <p><b>为什么分录不再按 {@code group_by_field} 归并</b>:分单字段是<b>源单表头</b>级的,
* 只用来决定「生成几张目标单」(见 {@code ConvertOrchestrator#splitByGroup})。
* 分录表上没有这一列,拿它当行键会让每一行都取到 null、全部塌进同一个分组,
* 最终一张目标单只剩一条明细——即使它合并了多张源单的全部源行。</p>
*
* <p>保持「每条源分录行 → 一条目标分录行」:按分组合并时,每张目标单保留该组内
* 全部源单的全部明细行,不丢行、不互相覆盖。同一目标行的多值折算仍由
* {@code merge_method} 在映射阶段决定(当前每条目标行只对应一条源行,故按「直接取」处理)。</p>
* *
* @param plan 映射计划。 * @param plan 映射计划。
* @param sourceBills 源单数据。 * @param sourceBills 源单数据。
* @param destEntryTable 目标分录表名。 * @param destEntryTable 目标分录表名。
* @param mergeKeyField 行键字段,空表示不合并。
* @return 分组结果,每组对应一条目标分录行。 * @return 分组结果,每组对应一条目标分录行。
*/ */
private List<ConvertSourceRowGroup> groupSourceRows(ConvertMappingPlan plan, List<ObjectValue> sourceBills, private List<ConvertSourceRowGroup> groupSourceRows(ConvertMappingPlan plan, List<ObjectValue> sourceBills,
String destEntryTable, String mergeKeyField) { String destEntryTable) {
String srcEntryTable = resolveSourceEntryTable(plan, destEntryTable); String srcEntryTable = resolveSourceEntryTable(plan, destEntryTable);
List<ConvertSourceRow> flatRows = new ArrayList<>(); List<ConvertSourceRowGroup> groups = new ArrayList<>();
for (ObjectValue bill : sourceBills) { for (ObjectValue bill : sourceBills) {
ObjectCollection rows = bill.getObjectCollection(srcEntryTable); ObjectCollection rows = bill.getObjectCollection(srcEntryTable);
if (rows == null) { if (rows == null) {
continue; continue;
} }
for (int i = 0; i < rows.size(); i++) { for (int i = 0; i < rows.size(); i++) {
flatRows.add(new ConvertSourceRow(bill, rows.getObject(i)));
}
}
List<ConvertSourceRowGroup> groups = new ArrayList<>();
if (StringUtils.isBlank(mergeKeyField)) {
for (ConvertSourceRow flat : flatRows) {
ConvertSourceRowGroup group = new ConvertSourceRowGroup(); ConvertSourceRowGroup group = new ConvertSourceRowGroup();
group.add(flat.getBill(), flat.getRow()); group.add(bill, rows.getObject(i));
groups.add(group); groups.add(group);
} }
return groups;
}
Map<String, ConvertSourceRowGroup> grouped = new LinkedHashMap<>();
for (ConvertSourceRow flat : flatRows) {
Object key = flat.getRow().get(mergeKeyField);
grouped.computeIfAbsent(key == null ? "" : String.valueOf(key), k -> new ConvertSourceRowGroup())
.add(flat.getBill(), flat.getRow());
} }
groups.addAll(grouped.values());
return groups; return groups;
} }

32
common/common-botp/src/main/java/apelet/common/botp/service/execute/ConvertOrchestrator.java

@ -309,9 +309,14 @@ public class ConvertOrchestrator {
} }
/** /**
* 批量取源单的分组字段值。 * 批量取源单的分组字段值:一次查询取回本批次全部源单的分组值。
* *
* <p>一次 {@code id in (...)} 查询取回全部源单的分组值,避免逐条 queryOne 的 N 次往返。</p> * <p><b>为什么用 {@code id = ? OR id = ?} 而不是 {@code id in (...)}</b>:
* orm 的 {@code FilterItem.in} 在这个版本里<b>不支持集合参数</b>——
* SQL 生成器只对 Integer/Long/String/日期等标量做处理,对其它对象直接
* {@code toString()} 加引号拼进 SQL。因此传入 {@code List<Long>} 会生成
* {@code id in ('[1, 2, 3]')},方括号是非法 SQL,查询必然失败(线上即此报错)。
* 这里改用同等条件的 OR 组合,仍是单次查询;源单在同一张表内,无需逐条往返。</p>
* *
* @param srcTable 源单表名。 * @param srcTable 源单表名。
* @param billIds 源单 id 集合。 * @param billIds 源单 id 集合。
@ -323,12 +328,20 @@ public class ConvertOrchestrator {
if (billIds == null || billIds.isEmpty()) { if (billIds == null || billIds.isEmpty()) {
return groupKeys; return groupKeys;
} }
List<Long> ids = new ArrayList<>(billIds.size()); Filter filter = new Filter();
StringBuilder format = new StringBuilder();
int index = 0;
for (String billId : billIds) { for (String billId : billIds) {
ids.add(Long.valueOf(billId)); filter.add(new FilterItem("id", FilterItem.equals, Long.valueOf(billId)));
if (index > 0) {
format.append(" or ");
}
format.append("#").append(index++);
}
// 只有一个条件时不必设置组合格式,保持默认即可
if (index > 1) {
filter.setFormat(format.toString());
} }
Filter filter = new Filter();
filter.add(new FilterItem("id", FilterItem.in, ids));
Selector selector = new Selector(); Selector selector = new Selector();
selector.getList().add(new SelectorItem("id")); selector.getList().add(new SelectorItem("id"));
selector.getList().add(new SelectorItem(groupByField)); selector.getList().add(new SelectorItem(groupByField));
@ -336,10 +349,9 @@ public class ConvertOrchestrator {
try { try {
rows = ormGenDataSourceUtil.query(srcTable, filter, selector); rows = ormGenDataSourceUtil.query(srcTable, filter, selector);
} catch (Exception e) { } catch (Exception e) {
throw new BotpMappingException("按分单字段查询源单失败,表名=" + srcTable + ",字段=" + groupByField, e); // 带上底层原因:否则日志里只剩"查询源单失败",要再翻一层堆栈才看得到真实 SQL 错误
} throw new BotpMappingException("按分单字段查询源单失败,表名=" + srcTable
if (rows == null) { + ",字段=" + groupByField + ",id=" + billIds + ",原因=" + e.getMessage(), e);
return groupKeys;
} }
for (int i = 0; i < rows.size(); i++) { for (int i = 0; i < rows.size(); i++) {
ObjectValue row = rows.getObject(i); ObjectValue row = rows.getObject(i);

Loading…
Cancel
Save