@ -16,6 +16,8 @@ import apelet.common.botp.service.IConvertPluginEntryService;
@@ -16,6 +16,8 @@ import apelet.common.botp.service.IConvertPluginEntryService;
import apelet.common.botp.service.IReverseWritingBusinessService ;
import apelet.common.botp.service.IReverseWritingFormulaService ;
import apelet.common.botp.service.IReverseWritingRulesService ;
import apelet.common.botp.service.execute.entity.ResolvedWriteBackRule ;
import apelet.common.botp.util.RuleSnapshotUtil ;
import apelet.common.botp.util.RuleValueCalculator ;
import apelet.common.core.botp.BotpReverseWriteFacade ;
import apelet.common.core.botp.BotpWriteBackRequest ;
@ -32,8 +34,10 @@ import apelet.common.online.rule.model.RuleEvalContext;
@@ -32,8 +34,10 @@ import apelet.common.online.rule.model.RuleEvalContext;
import apelet.common.orm.impl.Selector ;
import apelet.common.orm.impl.SelectorItem ;
import com.alibaba.fastjson.JSON ;
import com.alibaba.fastjson.JSONArray ;
import com.alibaba.fastjson.JSONObject ;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper ;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper ;
import lombok.extern.slf4j.Slf4j ;
import org.apache.commons.lang3.StringUtils ;
import org.springframework.beans.factory.annotation.Autowired ;
@ -44,6 +48,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
@@ -44,6 +48,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
import java.math.BigDecimal ;
import java.util.* ;
import java.util.stream.Collectors ;
/ * *
* 反写编排器 : 保存 / 审核 / 撤销 / 弃审四个入口共用的唯一实现 。
@ -149,6 +154,11 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -149,6 +154,11 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
private void executeRule ( ReverseWritingRules rule , ObjectValue targetBill , OnlFormHead targetHead , BotpWriteBackRequest request ) {
List < ConvertRelation > relations = locateRelations ( targetHead , request ) ;
List < ConvertRelationEntry > entries = relations . isEmpty ( ) ? Collections . emptyList ( ) : loadRelationEntries ( relations ) ;
if ( entries . isEmpty ( ) & & ! relations . isEmpty ( ) ) {
// 单表单据(无分录)不会产生分录级关联关系,退化成「单据 ↔ 单据」配对:
// 此时分行 id 就是单据 id,公式里的 表.字段 直接落在单据表上。
entries = toBillLevelPairs ( relations ) ;
}
if ( entries . isEmpty ( ) ) {
// 关系记录缺失时回退读单据上的 srcbillid / srcbillentryid(迁移期兼容)
entries = fallbackBySrcBillId ( targetBill ) ;
@ -159,13 +169,161 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -159,13 +169,161 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
rule . getId ( ) , request . getTargetTableName ( ) , request . getTargetBillId ( ) ) ;
return ;
}
List < WriteBackPlugIn > plugins = loadPlugins ( rule . getId ( ) ) ;
WriteBackContext context = buildContext ( rule , targetBill , request . isRollback ( ) ) ;
// 回滚时优先按关联关系上记录的版本取快照执行,保证「怎么加的就怎么退」
ResolvedWriteBackRule resolved = resolveRule ( rule , entries , request . isRollback ( ) ) ;
List < WriteBackPlugIn > plugins = loadPlugins ( resolved . getRule ( ) . getId ( ) ) ;
WriteBackContext context = buildContext ( resolved . getRule ( ) , targetBill , request . isRollback ( ) ) ;
invoke ( plugins , context , WriteBackHookEnum . BEFORE_RULE ) ;
execBusinessRules ( rule , entries , context ) ;
execFormulas ( rule , entries , context ) ;
closeSourceRows ( rule , entries , context ) ;
execBusinessRules ( resolved , entries , context ) ;
execFormulas ( resolved , entries , context , plugins ) ;
closeSourceRows ( resolved . getR ule ( ) , entries , context ) ;
registerAfterCommit ( plugins , context ) ;
if ( ! request . isRollback ( ) ) {
recordRuleVersion ( resolved , entries ) ;
}
}
/ * *
* 解析本次执行用的规则 。
*
* < p > 回滚时若关联关系上记录了版本 、 且与当前版本不同 , 就按记录版本取快照执行 — —
* 否则反写规则改版后 , 老单据的撤销会用新公式去退旧数据 。 < / p >
*
* @param current 当前规则 ( 库里最新版本 ) 。
* @param entries 关联关系 。
* @param rollback 是否回滚方向 。
* @return 解析结果 。
* /
private ResolvedWriteBackRule resolveRule ( ReverseWritingRules current , List < ConvertRelationEntry > entries , boolean rollback ) {
Integer recorded = firstRecordedVersion ( entries ) ;
if ( rollback & & recorded ! = null & & ! recorded . equals ( current . getRuleVersion ( ) ) ) {
ResolvedWriteBackRule fromSnapshot = loadSnapshot ( current . getId ( ) , recorded ) ;
if ( fromSnapshot ! = null ) {
log . info ( "反写回滚按记录版本执行。规则id={},记录版本={},当前版本={}" , current . getId ( ) , recorded , current . getRuleVersion ( ) ) ;
return fromSnapshot ;
}
}
ResolvedWriteBackRule resolved = new ResolvedWriteBackRule ( ) ;
resolved . setRule ( current ) ;
resolved . setVersion ( current . getRuleVersion ( ) ) ;
resolved . setFormulas ( reverseWritingFormulaService . list ( new LambdaQueryWrapper < ReverseWritingFormula > ( )
. eq ( ReverseWritingFormula : : getEnable , 1 )
. eq ( ReverseWritingFormula : : getRulesId , current . getId ( ) )
. orderByAsc ( ReverseWritingFormula : : getExecOrder ) ) ) ;
resolved . setBusinesses ( reverseWritingBusinessService . list ( new LambdaQueryWrapper < ReverseWritingBusiness > ( )
. eq ( ReverseWritingBusiness : : getEnable , 1 )
. eq ( ReverseWritingBusiness : : getRulesId , current . getId ( ) ) ) ) ;
return resolved ;
}
/ * *
* 按版本读反写规则快照 。
*
* @param ruleId 规则 id 。
* @param version 版本号 。
* @return 解析结果 ; 快照缺失或内容不完整时返回 null ( 调用方退回当前版本 ) 。
* /
private ResolvedWriteBackRule loadSnapshot ( Long ruleId , Integer version ) {
ReverseWritingRulesVer snapshot = reverseWritingRulesService . getVersionSnapshot ( ruleId , version ) ;
if ( snapshot = = null | | StringUtils . isBlank ( snapshot . getSnapshotJson ( ) ) ) {
log . warn ( "反写规则快照不存在,退回当前版本执行。规则id={},版本={}" , ruleId , version ) ;
return null ;
}
JSONObject json = JSON . parseObject ( snapshot . getSnapshotJson ( ) ) ;
ReverseWritingRules rule = JSON . parseObject ( json . getString ( RuleSnapshotUtil . SECTION_RULE ) , ReverseWritingRules . class ) ;
if ( rule = = null ) {
log . warn ( "反写规则快照内容不完整(缺少 rule 分区),退回当前版本执行。规则id={},版本={}" , ruleId , version ) ;
return null ;
}
ResolvedWriteBackRule resolved = new ResolvedWriteBackRule ( ) ;
resolved . setRule ( rule ) ;
resolved . setFormulas ( parseSection ( json . getJSONArray ( RuleSnapshotUtil . SECTION_FORMULAS ) , ReverseWritingFormula . class ) . stream ( )
. filter ( f - > f . getEnable ( ) = = 1 )
. sorted ( Comparator . comparing ( ReverseWritingFormula : : getExecOrder , Comparator . nullsLast ( Comparator . naturalOrder ( ) ) ) )
. collect ( Collectors . toList ( ) ) ) ;
resolved . setBusinesses ( parseSection ( json . getJSONArray ( RuleSnapshotUtil . SECTION_BUSINESSES ) , ReverseWritingBusiness . class ) . stream ( )
. filter ( b - > b . getEnable ( ) = = 1 )
. collect ( Collectors . toList ( ) ) ) ;
resolved . setVersion ( snapshot . getRuleVersion ( ) ) ;
return resolved ;
}
/ * *
* 解析快照里的子表分区 。
*
* @param array 分区数组 , 可空 。
* @param clazz 目标类型 。
* @param < T > 类型 。
* @return 列表 , 不会返回 null 。
* /
private < T > List < T > parseSection ( JSONArray array , Class < T > clazz ) {
return array = = null ? new ArrayList < > ( ) : array . toJavaList ( clazz ) ;
}
/ * *
* 取关联关系上记录的反写规则版本 。
*
* @param entries 关联关系 。
* @return 版本号 ; 没有记录时返回 null 。
* /
private Integer firstRecordedVersion ( List < ConvertRelationEntry > entries ) {
for ( ConvertRelationEntry entry : entries ) {
if ( entry . getWritebackRuleVersion ( ) ! = null ) {
return entry . getWritebackRuleVersion ( ) ;
}
}
return null ;
}
/ * *
* 记录本次反写实际使用的规则版本 ( 正向执行时 ) 。
*
* < p > 按目标单一次性更新 , 不逐行 update 。 < / p >
*
* @param resolved 解析结果 。
* @param entries 关联关系 。
* /
private void recordRuleVersion ( ResolvedWriteBackRule resolved , List < ConvertRelationEntry > entries ) {
if ( resolved . getVersion ( ) = = null | | entries . isEmpty ( ) ) {
return ;
}
Set < String > targetIds = new LinkedHashSet < > ( ) ;
for ( ConvertRelationEntry entry : entries ) {
if ( StringUtils . isNotBlank ( entry . getTargetId ( ) ) ) {
targetIds . add ( entry . getTargetId ( ) ) ;
}
}
if ( targetIds . isEmpty ( ) ) {
return ;
}
convertRelationEntryMapper . update ( null , new LambdaUpdateWrapper < ConvertRelationEntry > ( )
. in ( ConvertRelationEntry : : getTargetId , targetIds )
. set ( ConvertRelationEntry : : getWritebackRuleVersion , resolved . getVersion ( ) ) ) ;
}
/ * *
* 把单据级关联关系转成 「 单据 ↔ 单据 」 的配对 。
*
* < p > 单表单据没有分录 , 转换时不会写 convert_relation_entry 。 这里把单据 id 同时当作
* 「 源行 id / 目标行 id 」 , 后续按行取数的逻辑就能原样复用 。 < / p >
*
* @param relations 单据级关联关系 。
* @return 配对列表 。
* /
private List < ConvertRelationEntry > toBillLevelPairs ( List < ConvertRelation > relations ) {
List < ConvertRelationEntry > pairs = new ArrayList < > ( relations . size ( ) ) ;
for ( ConvertRelation relation : relations ) {
if ( StringUtils . isBlank ( relation . getSourceId ( ) ) ) {
continue ;
}
ConvertRelationEntry pair = new ConvertRelationEntry ( ) ;
pair . setSourceId ( relation . getSourceId ( ) ) ;
pair . setSourceEntryId ( relation . getSourceId ( ) ) ;
pair . setTargetId ( relation . getTargetId ( ) ) ;
pair . setTargeEntryId ( relation . getTargetId ( ) ) ;
pairs . add ( pair ) ;
}
return pairs ;
}
/ * *
@ -215,15 +373,12 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -215,15 +373,12 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
* < p > 分支表达式形如 { @code 源单表 . 源单表 . 字段 = $fun$ ( 参数 ) } 或 { @code 源单表 . 字段 = ' 常量 ' } :
* 等号左边是回写位置 ( 源单 ) , 右边是取值 ( 函数 / 字段引用 / 字面量 ) 。 < / p >
*
* @param rule 反写规则 。
* @param resolved 解析后的规则 ( 含业务规则分录 ) 。
* @param entries 关联关系 。
* @param context 反写上下文 。
* /
private void execBusinessRules ( ReverseWritingRules rule , List < ConvertRelationEntry > entries , WriteBackContext context ) {
List < ReverseWritingBusiness > businesses = reverseWritingBusinessService . list ( new LambdaQueryWrapper < ReverseWritingBusiness > ( )
. eq ( ReverseWritingBusiness : : getEnable , 1 )
. eq ( ReverseWritingBusiness : : getRulesId , rule . getId ( ) ) ) ;
for ( ReverseWritingBusiness business : businesses ) {
private void execBusinessRules ( ResolvedWriteBackRule resolved , List < ConvertRelationEntry > entries , WriteBackContext context ) {
for ( ReverseWritingBusiness business : resolved . getBusinesses ( ) ) {
boolean hit = business . getPreconditionsEnglish ( ) = = null
| | conditionHit ( business . getPreconditionsEnglish ( ) , business . getPreconditionsJson ( ) , context . getTargetBillData ( ) ) ;
String expression = hit ? business . getEstablishedEnglish ( ) : business . getNotEstablishedEnglish ( ) ;
@ -259,29 +414,34 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -259,29 +414,34 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
if ( value = = null ) {
return ;
}
// 回写位置可能是源单【表头】或【分录】:表头写源单 id,分录写源分录行 id。
// 原先一律用 sourceEntryId,目标是表头时会按分录行 id 去更新表头行,写错行或压根更新不到。
String sourceHeaderTable = resolveTableName ( String . valueOf ( context . getRule ( ) . getSourceDocuments ( ) ) ) ;
boolean headerTarget = sourceHeaderTable ! = null & & sourceHeaderTable . equals ( targetRef [ 0 ] ) ;
for ( ConvertRelationEntry entry : entries ) {
writeSourceRow ( targetRef [ 0 ] , entry . getSourceEntryId ( ) , targetRef [ 1 ] , value ) ;
String rowId = headerTarget ? entry . getSourceId ( ) : entry . getSourceEntryId ( ) ;
if ( StringUtils . isBlank ( rowId ) ) {
continue ;
}
writeSourceRow ( targetRef [ 0 ] , rowId , targetRef [ 1 ] , value ) ;
}
}
/ * *
* 逐条执行反写公式 , 按 exec_order 升序 。
*
* @param rule 反写规则 。
* @param resolved 解析后的规则 ( 含公式分录 ) 。
* @param entries 关联关系 。
* @param context 反写上下文 。
* @param plugins 已加载的插件实例 ( 复用 , 避免每条公式都查一次库 ) 。
* /
private void execFormulas ( ReverseWritingRules rule , List < ConvertRelationEntry > entries , WriteBackContext context ) {
List < ReverseWritingFormula > formulas = reverseWritingFormulaService . list ( new LambdaQueryWrapper < ReverseWritingFormula > ( )
. eq ( ReverseWritingFormula : : getEnable , 1 )
. eq ( ReverseWritingFormula : : getRulesId , rule . getId ( ) )
. orderByAsc ( ReverseWritingFormula : : getExecOrder ) ) ;
for ( ReverseWritingFormula formula : formulas ) {
private void execFormulas ( ResolvedWriteBackRule resolved , List < ConvertRelationEntry > entries , WriteBackContext context , List < WriteBackPlugIn > plugins ) {
for ( ReverseWritingFormula formula : resolved . getFormulas ( ) ) {
context . setFormula ( formula ) ;
if ( ! conditionHit ( formula . getConditionEnglish ( ) , formula . getConditionJson ( ) , context . getTargetBillData ( ) ) ) {
continue ;
}
applyFormula ( formula , entries , context ) ;
applyFormula ( formula , entries , context , plugins ) ;
}
}
@ -295,8 +455,9 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -295,8 +455,9 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
* @param formula 反写公式 。
* @param entries 关联关系 。
* @param context 反写上下文 。
* @param plugins 已加载的插件实例 。
* /
private void applyFormula ( ReverseWritingFormula formula , List < ConvertRelationEntry > entries , WriteBackContext context ) {
private void applyFormula ( ReverseWritingFormula formula , List < ConvertRelationEntry > entries , WriteBackContext context , List < WriteBackPlugIn > plugins ) {
ReverseWriteModeEnum mode = ReverseWriteModeEnum . ofNullable ( formula . getMode ( ) ) ;
String [ ] targetRef = splitRef ( formula . getFormulaEnglish ( ) ) ;
String [ ] sourceRef = splitRef ( formula . getSourceSingleField ( ) ) ;
@ -304,7 +465,6 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -304,7 +465,6 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
throw new BotpMappingException ( "反写公式配置不完整,公式=" + formula . getFormulaEnglish ( )
+ ",源单字段=" + formula . getSourceSingleField ( ) ) ;
}
List < WriteBackPlugIn > plugins = loadPlugins ( context . getRule ( ) . getId ( ) ) ;
int scale = resolveScale ( sourceRef [ 0 ] , sourceRef [ 1 ] ) ;
for ( ConvertRelationEntry entry : entries ) {
ObjectValue targetRow = queryRow ( targetRef [ 0 ] , entry . getTargeEntryId ( ) ) ;
@ -328,7 +488,7 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -328,7 +488,7 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
context . setCommitted ( false ) ;
invoke ( plugins , context , WriteBackHookEnum . AFTER_CALC ) ;
invoke ( plugins , context , WriteBackHookEnum . BEFORE_EXCESS ) ;
context . setExcess ( excessHit ( context ) ) ;
context . setExcess ( excessHit ( context , sourceRow , sourceRef [ 1 ] ) ) ;
invoke ( plugins , context , WriteBackHookEnum . AFTER_EXCESS ) ;
if ( Boolean . TRUE . equals ( context . getExcess ( ) ) & & ExcessActionEnum . ofNullable ( context . getRule ( ) . getExcessAction ( ) ) = = ExcessActionEnum . REJECT ) {
throw new BotpMappingException ( "反写超额,操作已取消。规则=" + context . getRule ( ) . getName ( )
@ -349,12 +509,28 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -349,12 +509,28 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
* @param context 反写上下文 ( 已填 newValue ) 。
* @return 超额返回 true ; 未配置检查公式时返回 false 。
* /
private boolean excessHit ( WriteBackContext context ) {
/ * *
* 超额检查 : 对 「 本次写入后的值 」 求值 , 为真即判定超额 。
*
* < p > 求值上下文是 < b > 源分录行 < / b > , 并且在求值前把算出的新值放进去 — — 这样条件里
* 「 已下推数量 + 本次数量 & gt ; 申请数量 」 这类引用才能拿到写入后的结果 。
* 原先拿目标单求值 , 源单字段一个都取不到 , 条件永远不成立 。 < / p >
*
* < p > 条件里的字段引用请写成 { @code 分录表 . 分录表 . 字段 } 形式 , 与反写公式的
* { @code sourceSingleField } 保持一致 。 < / p >
*
* @param context 反写上下文 ( 已填 newValue ) 。
* @param sourceRow 源分录行 。
* @param sourceField 本次写入的源单字段名 。
* @return 超额返回 true ; 未配置检查公式时返回 false 。
* /
private boolean excessHit ( WriteBackContext context , ObjectValue sourceRow , String sourceField ) {
String expression = context . getRule ( ) . getExcessCheckJson ( ) ;
if ( StringUtils . isBlank ( expression ) ) {
if ( StringUtils . isBlank ( expression ) | | sourceRow = = null ) {
return false ;
}
return conditionHit ( expression , null , context . getTargetBillData ( ) ) ;
sourceRow . put ( sourceField , context . getNewValue ( ) ) ;
return conditionHit ( expression , null , sourceRow ) ;
}
/ * *
@ -451,6 +627,11 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
@@ -451,6 +627,11 @@ public class WriteBackOrchestrator implements BotpReverseWriteFacade {
* @param beforeValue 反写前的原值 。
* /
private void recordBeforeValue ( ConvertRelationEntry entry , BigDecimal beforeValue ) {
if ( entry . getId ( ) = = null ) {
// 单据级配对(单表单据)没有真实的 convert_relation_entry 行,无处记录原值
log . warn ( "单表单据没有分录级关联记录,覆盖方式的原值未记录,撤销时无法恢复。源单id={}" , entry . getSourceId ( ) ) ;
return ;
}
ConvertRelationEntry update = new ConvertRelationEntry ( ) ;
update . setId ( entry . getId ( ) ) ;
update . setBeforeValue ( beforeValue ) ;