Browse Source
- 移除转换时的反写触发:删除 ConvertOrchestrator#triggerSaveWriteBack 及其 WriteBackOrchestrator 依赖。转换只负责「源单 → 目标单」的数据复制,原实现在 autoSave 落库后补触发反写,等于把刚复制过去的值按规则再写回来,累加类公式让源单数量指数增长 (1→2→4→8→24),覆盖类公式把源单字段改成复制值。反写改由目标单被真正操作时触发, 入口已存在:SaveOperationServicePlugIn(保存)、AuditOperationServicePlugIn(审核), 时机由 reverse_writing_rules.trigger_timing 决定('1' 保存 / '2' 审核) - 转换关系改用落库后的真实主键:BotpService#addNew 落库前收集分录「预发主键 → 对象实例」, 落库后读真实主键建立映射,saveRelation 用它覆盖 targe_entry_id。根因是 initDestBillData 建骨架时预发主键,而 ormGenDataSourceUtil.addNew 插入分录时会重新发号, 关系表存的是库里不存在的 id,反写按 targe_entry_id 查目标分录必然为空而静默跳过。 新增 BtpTransformResult#entryIdMap 承载映射,校正时打 INFO 记录「计划id / 实际id」 - 批量保存回写真实主键:新增 syncSavedIds,exePlugin 落库后把 ORM 回写的真实主键同步回 入参 JSON(表头直接覆盖,分录按 key 顺序 + 行下标对齐,与 getObjectValue 的入栈顺序一致)。 不回写的话 executeBatchSaveInTransaction 补写 convert_relation 时用预发 id, 关系指向不存在的记录,用户改单保存触发反写就会「找不到关联关系」。 实测:payload 里 header id=...101824 / entry id=...101825, 库里实际 ...04651520 / ...34011648 - 新增转换关系补写门面:BotpConvertRelationFacade(common-core 接口)+ ConvertRelationFacade(common-botp 实现),按 BillRelation JSON 补写 convert_relation / convert_relation_entry,target_id 以落库后真实主键覆盖,按主键幂等。 为什么需要:convert_relation 只在 BotpService#transform 的 autoSave 分支写入, formStatus=2(未落库、前端手动保存)时转换阶段不写关系,保存后反写找不到来源单。 ConvertedBill 新增 billRelation,buildBatchSavePayload 内嵌到 masterData[i].billRelation, 父子关系由结构而非下标保证;executeBatchSave 摘出该键(不能当表头字段交给 ORM)。 executeBatchSaveInTransaction 增加 relationList 参数,落库后调 saveRelationQuietly 补写, 与保存在同一事务内。BotpReverseWriteSupport 泛化 getFacadeQuietly 支持按类型取门面, 未挂载 botp 的应用静默跳过 - 修正反写公式读写方向:formula(formulaEnglish) 指回写位置(源单), sourceSingleField 指取值位置(目标单),原实现写反了,表现为「反写执行了但源单没变」。 变量统一改为 read(目标单,取值)/ write(源单,回写),避免后续再被读反 - 新增 isHeadTable:公式引用表头字段时用单据 id 查,引用分录字段时用分录行 id 查。 原先一律用分录 id,配成 请购单.请购单.供应商 这类表头字段时永远查不到行 - 新增 syncNonNumericValue:供应商、名称等文本字段无法做 BigDecimal 运算 (toBigDecimal 返回 null,nullToZero 后变成 0),改为正向直接把目标单值写到源单, 回滚按 before_value 恢复;目标单字段为空时跳过并告警,不把源单写空 - recordBeforeValue 改用 LambdaUpdateWrapper 显式 set:beforeValue 为 null 时 MyBatis-Plus 会跳过 null 字段,生成 UPDATE ... WHERE id=? 这种无 SET 的非法 SQL - 新增 resolveEntryTableQuietly(key 兼容 tableId 与表名,解析不到返回 null 不抛) 与 toJsonRowList(分录值兼容数组与单行对象),用于主键回写 - 反写跳过日志补上表名与行 id,原先只打 id,无法判断查的是表头还是分录 - 新增 zz-resource/db-scripts/2026-10-27-convert-relation-entry-writeback-script.sql: 补 convert_relation_entry 的 before_value(decimal(24,8))与 writeback_rule_version(int)。 实体早已声明这两个字段(注释也写明「需要新增列」)但 DDL 未跟上,MyBatis-Plus 按实体 拼 SELECT,任何查询该表的 SQL 都会报 Unknown column 'before_value' in 'field list', 反写与转换落库都会直接失败。脚本用 information_schema 判断列是否存在,可重复执行dev-lml
11 changed files with 532 additions and 52 deletions
@ -0,0 +1,121 @@
@@ -0,0 +1,121 @@
|
||||
package apelet.common.botp.service.execute; |
||||
|
||||
import apelet.common.botp.dao.ConvertRelationEntryMapper; |
||||
import apelet.common.botp.dao.ConvertRelationMapper; |
||||
import apelet.common.botp.model.ConvertRelation; |
||||
import apelet.common.botp.model.ConvertRelationEntry; |
||||
import apelet.common.botp.service.execute.entity.BillRelation; |
||||
import apelet.common.botp.service.execute.entity.BillRelationEntry; |
||||
import apelet.common.core.botp.BotpConvertRelationFacade; |
||||
import apelet.common.core.object.TokenData; |
||||
import com.alibaba.fastjson.JSON; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.lang3.StringUtils; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Component; |
||||
import org.springframework.transaction.annotation.Transactional; |
||||
|
||||
import java.util.Date; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* 转换关系补写实现。 |
||||
* |
||||
* <p><b>背景</b>:{@code convert_relation} / {@code convert_relation_entry} 原来只在 |
||||
* {@code BotpService#transform} 的 autoSave 分支写入。{@code formStatus=2}(未落库、前端手动保存) |
||||
* 时不写,保存后反写就找不到来源单。本实现负责在「保存落库之后」把关系补上。</p> |
||||
* |
||||
* <p><b>幂等</b>:按关联关系主键判重,重复保存不会写出重复关系。</p> |
||||
* |
||||
*/ |
||||
@Slf4j |
||||
@Component |
||||
public class ConvertRelationFacade implements BotpConvertRelationFacade { |
||||
|
||||
@Autowired |
||||
private ConvertRelationMapper convertRelationMapper; |
||||
|
||||
@Autowired |
||||
private ConvertRelationEntryMapper convertRelationEntryMapper; |
||||
|
||||
@Override |
||||
@Transactional(rollbackFor = Exception.class) |
||||
public void saveBillRelation(String billRelationJson, Long targetBillId) { |
||||
if (StringUtils.isBlank(billRelationJson) || targetBillId == null) { |
||||
return; |
||||
} |
||||
BillRelation billRelation; |
||||
try { |
||||
billRelation = JSON.parseObject(billRelationJson, BillRelation.class); |
||||
} catch (Exception e) { |
||||
log.warn("转换关系 JSON 解析失败,跳过补写。目标单id={}", targetBillId, e); |
||||
return; |
||||
} |
||||
if (billRelation == null || billRelation.getId() == null) { |
||||
log.warn("转换关系缺少主键,跳过补写。目标单id={}", targetBillId); |
||||
return; |
||||
} |
||||
// 幂等:同一张目标单可能被多次保存,关系已存在就不再写。
|
||||
if (convertRelationMapper.selectById(billRelation.getId()) != null) { |
||||
log.debug("转换关系已存在,跳过补写。关系id={},目标单id={}", billRelation.getId(), targetBillId); |
||||
return; |
||||
} |
||||
ConvertRelation relation = new ConvertRelation(); |
||||
relation.setId(billRelation.getId()); |
||||
relation.setConvertRuleId(billRelation.getConvertRuleId()); |
||||
relation.setSourceId(billRelation.getSourceId()); |
||||
relation.setSourceNumber(billRelation.getSourceNumber()); |
||||
// 目标单 id 以落库后的真实主键为准,覆盖下发的值。
|
||||
relation.setTargetId(String.valueOf(targetBillId)); |
||||
relation.setTargetNumber(billRelation.getTargetNumber()); |
||||
fillAudit(relation); |
||||
convertRelationMapper.insert(relation); |
||||
|
||||
List<BillRelationEntry> entryList = billRelation.getBillRelationEntryList(); |
||||
if (entryList == null || entryList.isEmpty()) { |
||||
return; |
||||
} |
||||
for (BillRelationEntry billRelationEntry : entryList) { |
||||
ConvertRelationEntry entry = new ConvertRelationEntry(); |
||||
entry.setId(billRelationEntry.getId()); |
||||
entry.setConvertRuleId(billRelationEntry.getConvertRuleId()); |
||||
entry.setSourceEntryId(billRelationEntry.getSourceEntryId()); |
||||
entry.setSourceEntryNumber(billRelationEntry.getSourceEntryNumber()); |
||||
entry.setSourceId(billRelationEntry.getSourceId()); |
||||
entry.setSourceNumber(billRelationEntry.getSourceNumber()); |
||||
entry.setTargeEntryId(billRelationEntry.getTargeEntryId()); |
||||
entry.setTargetId(String.valueOf(targetBillId)); |
||||
entry.setTargeEntryNumber(billRelationEntry.getTargeEntryNumber()); |
||||
entry.setTargetNumber(billRelationEntry.getTargetNumber()); |
||||
fillAudit(entry); |
||||
convertRelationEntryMapper.insert(entry); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 统一填审计字段。 |
||||
* |
||||
* @param target 待填对象。 |
||||
*/ |
||||
private void fillAudit(Object target) { |
||||
Long userId = null; |
||||
TokenData tokenData = TokenData.takeFromRequest(); |
||||
if (tokenData != null) { |
||||
userId = tokenData.getUserId(); |
||||
} |
||||
Date now = new Date(); |
||||
if (target instanceof ConvertRelation) { |
||||
ConvertRelation relation = (ConvertRelation) target; |
||||
relation.setCreateUserId(userId); |
||||
relation.setCreateTime(now); |
||||
relation.setUpdateUserId(userId); |
||||
relation.setUpdateTime(now); |
||||
} else if (target instanceof ConvertRelationEntry) { |
||||
ConvertRelationEntry entry = (ConvertRelationEntry) target; |
||||
entry.setCreateUserId(userId); |
||||
entry.setCreateTime(now); |
||||
entry.setUpdateUserId(userId); |
||||
entry.setUpdateTime(now); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
package apelet.common.core.botp; |
||||
|
||||
/** |
||||
* 转换关系落库门面(依赖反转用)。 |
||||
* |
||||
* <p><b>为什么需要</b>:转换关系表 {@code convert_relation} / {@code convert_relation_entry} 只在 |
||||
* BOTP 转换的 autoSave 分支写入。当 {@code formStatus=2}(未落库、由前端手动保存)时, |
||||
* 转换阶段不会写关系,保存接口落库后必须补写,否则后续「保存即反写」「上查下查」都找不到来源单。</p> |
||||
* |
||||
* <p><b>为什么放在 common-core</b>:与 {@link BotpReverseWriteFacade} 同理——补写实现与实体都在 |
||||
* common-botp,而补写时机在 common-online,直接依赖会成环。</p> |
||||
* |
||||
* <p>调用方应通过 {@link BotpReverseWriteSupport#saveRelationQuietly} 调用,未挂载 botp 时静默跳过。</p> |
||||
* |
||||
*/ |
||||
public interface BotpConvertRelationFacade { |
||||
|
||||
/** |
||||
* 按转换结果下发的关联关系补写转换关系表。 |
||||
* |
||||
* <p>已存在的关联关系(按关联关系主键判重)直接跳过,保证重复保存不会产生重复关系。</p> |
||||
* |
||||
* @param billRelationJson 单张目标单的关联关系 JSON 串,结构见 BillRelation。 |
||||
* @param targetBillId 目标单落库后的真实主键,会覆盖 JSON 里的 targetId。 |
||||
*/ |
||||
void saveBillRelation(String billRelationJson, Long targetBillId); |
||||
} |
||||
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
-- ---------------------------- |
||||
-- convert_relation_entry 补反写回滚所需的两个字段。 |
||||
-- |
||||
-- 背景:ConvertRelationEntry 实体上声明了 beforeValue / writebackRuleVersion, |
||||
-- 但 DDL 没跟上,MyBatis-Plus 查询会带上这两列,导致 |
||||
-- Unknown column 'before_value' in 'field list' |
||||
-- 反写(WriteBackOrchestrator#loadRelationEntries)与转换落库都会直接报错。 |
||||
-- |
||||
-- 请在 tenant-admin 服务指向的默认数据库执行该脚本;脚本可重复执行。 |
||||
-- ---------------------------- |
||||
SET NAMES utf8mb4; |
||||
SET FOREIGN_KEY_CHECKS = 0; |
||||
|
||||
-- ---------------------------- |
||||
-- 1. before_value:反写前的源单字段原值。 |
||||
-- 只有「覆盖」方式需要——累加/扣减天然可逆,覆盖把原值冲掉了,不记下来撤销时就回不去。 |
||||
-- ---------------------------- |
||||
SET @ddl = (SELECT IF(COUNT(*) = 0, |
||||
'ALTER TABLE `convert_relation_entry` ADD COLUMN `before_value` decimal(24,8) DEFAULT NULL COMMENT ''反写前的源单字段原值,仅覆盖方式使用,供撤销回滚恢复''', |
||||
'DO 0') |
||||
FROM information_schema.columns |
||||
WHERE table_schema = DATABASE() AND table_name = 'convert_relation_entry' AND column_name = 'before_value'); |
||||
PREPARE stmt FROM @ddl; |
||||
EXECUTE stmt; |
||||
DEALLOCATE PREPARE stmt; |
||||
|
||||
-- ---------------------------- |
||||
-- 2. writeback_rule_version:本次反写实际使用的反写规则版本。 |
||||
-- 撤销/弃审时按该版本取快照执行,保证「怎么加的就怎么退」。 |
||||
-- ---------------------------- |
||||
SET @ddl = (SELECT IF(COUNT(*) = 0, |
||||
'ALTER TABLE `convert_relation_entry` ADD COLUMN `writeback_rule_version` int DEFAULT NULL COMMENT ''本次反写使用的反写规则版本,供撤销回滚按同一版本执行''', |
||||
'DO 0') |
||||
FROM information_schema.columns |
||||
WHERE table_schema = DATABASE() AND table_name = 'convert_relation_entry' AND column_name = 'writeback_rule_version'); |
||||
PREPARE stmt FROM @ddl; |
||||
EXECUTE stmt; |
||||
DEALLOCATE PREPARE stmt; |
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1; |
||||
Loading…
Reference in new issue