@ -19,11 +19,11 @@ import apelet.common.generator.model.OnlFormHead;
@@ -19,11 +19,11 @@ import apelet.common.generator.model.OnlFormHead;
import apelet.common.generator.service.IOnlFormFieldService ;
import apelet.common.generator.service.IOnlFormHeadService ;
import apelet.common.generator.utils.OrmGenDataSourceUtil ;
import apelet.common.online.config.OnlineProperties ;
import apelet.common.online.abstractplugin.ListDataEasService ;
import apelet.common.online.abstractplugin.ListDataOrmService ;
import apelet.common.online.abstractplugin.ListDataService ;
import apelet.common.online.abstractplugin.model.GridData ;
import apelet.common.online.config.OnlineProperties ;
import apelet.common.online.dao.ConvertRelationFOnLineMapper ;
import apelet.common.online.dao.OnlineOperationMapper ;
import apelet.common.online.dto.OnlineEventPluginExecuteDto ;
@ -32,8 +32,11 @@ import apelet.common.online.dto.OnlinePluginExecuteDto;
@@ -32,8 +32,11 @@ import apelet.common.online.dto.OnlinePluginExecuteDto;
import apelet.common.online.dto.OnlinePluginExecuteSortDto ;
import apelet.common.online.exception.OnlineRuntimeException ;
import apelet.common.online.model.* ;
import apelet.common.online.model.constant.* ;
import apelet.common.online.model.constant.EventEnum ;
import apelet.common.online.model.constant.FieldFilterType ;
import apelet.common.online.model.constant.FieldKind ;
import apelet.common.online.model.constant.RelationType ;
import apelet.common.online.model.constant.VirtualType ;
import apelet.common.online.object.ColumnData ;
import apelet.common.online.object.ConstDictInfo ;
import apelet.common.online.object.JoinTableInfo ;
@ -50,6 +53,7 @@ import apelet.common.orm.impl.*;
@@ -50,6 +53,7 @@ import apelet.common.orm.impl.*;
import apelet.common.redis.util.CommonRedisUtil ;
import apelet.common.sequence.wrapper.IdGeneratorWrapper ;
import cn.hutool.core.collection.CollUtil ;
import cn.hutool.core.collection.CollectionUtil ;
import cn.hutool.core.io.FileUtil ;
import cn.hutool.core.lang.Assert ;
import cn.hutool.core.map.MapUtil ;
@ -1128,319 +1132,6 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
@@ -1128,319 +1132,6 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
}
/ * *
* 批量新增 : 一次提交多张新单据 ( 库里不存在的数据 ) 。
*
* < p > 入参与单条 executePlugin 同名同义 , 只是 masterData 变成数组 、 每个元素一张单据 ;
* 每张单据补齐主键后逐条走同一条单条保存链路 ( 附件 、 反写等逻辑全部复用 ) ,
* 整批一个事务 , 任一条失败整批回滚 。 < / p >
*
* @param onlinePluginExecuteDto 操作按钮等参数 。
* @param masterDataArray 表头数据数组 , 每个元素一张单据 。
* @param slaveData 分录数据对象 ( 整批共享 ) , 或与表头数组同长度的数组 , 或 null 。
* @return 操作结果 。
* /
@Override
public OperationResult executePluginBatch ( OnlinePluginExecuteDto onlinePluginExecuteDto ,
JSONArray masterDataArray , Object slaveData ) {
if ( masterDataArray = = null | | masterDataArray . isEmpty ( ) ) {
return this . makeFailedResult ( "没有可保存的数据!" ) ;
}
OnlineForm onlineForm = onlineFormService . getOnlineFormFromCache ( onlinePluginExecuteDto . getFormId ( ) ) ;
if ( onlineForm = = null ) {
return this . makeFailedResult ( "数据验证失败,表单不存在或已失效,请刷新后重试!" ) ;
}
OnlineTable onlineTable = onlineTableService . getOnlineTableFromCache ( onlineForm . getMasterTableId ( ) ) ;
if ( onlineTable = = null | | onlineTable . getPrimaryKeyColumn ( ) = = null ) {
return this . makeFailedResult ( "数据验证失败,表单主表或主键配置缺失,无法批量新增!" ) ;
}
List < OnlinePluginExecuteSortDto > sortDtoList = this . getSortDtoList ( onlineForm , onlinePluginExecuteDto . getButtonName ( ) ) ;
if ( ! this . containsPlugin ( sortDtoList , SaveOperationServicePlugIn . class ) ) {
return this . makeFailedResult ( "批量新增仅支持绑定保存插件的按钮!" ) ;
}
return this . executeBatchSave ( onlinePluginExecuteDto , masterDataArray , slaveData ,
onlineForm , onlineTable , sortDtoList ) ;
}
/ * *
* 批量新增 : 把多张新单据拆成 N 份单条入参 , 补齐主键后逐条走单条保存链路 。
*
* < p > 补齐主键的原因 : 保存插件里表头主键为空会被当作 「 没有可保存的数据 」 跳过 ,
* 而且分录的 parent_id 也要挂到表头主键上 , 所以先由 IdGeneratorWrapper 统一发号 。
* 分录没有主键时同样补号 , 保证与新主键一起按每张单据各自保存 、 不会互相串 。 < / p >
*
* @param onlinePluginExecuteDto 操作按钮等参数 。
* @param masterArray 表头数据数组 。
* @param slaveData 分录数据对象 , 或与表头数组同长度的数组 , 或 null 。
* @param onlineForm 当前表单 。
* @param onlineTable 表头对应的在线数据表 。
* @param sortDtoList 已解析并按执行顺序排好的插件列表 ( 整批只需解析一次 ) 。
* @return 操作结果 。
* /
private OperationResult executeBatchSave ( OnlinePluginExecuteDto onlinePluginExecuteDto , JSONArray masterArray ,
Object slaveData , OnlineForm onlineForm , OnlineTable onlineTable ,
List < OnlinePluginExecuteSortDto > sortDtoList ) {
if ( masterArray . isEmpty ( ) ) {
return this . makeFailedResult ( "没有可保存的数据!" ) ;
}
JSONArray slaveArray = null ;
if ( slaveData instanceof JSONArray ) {
slaveArray = ( JSONArray ) slaveData ;
if ( slaveArray . size ( ) ! = masterArray . size ( ) ) {
return this . makeFailedResult ( "数据验证失败,分录数据条数与待保存单据条数不一致!" ) ;
}
} else if ( slaveData ! = null & & ! ( slaveData instanceof JSONObject ) ) {
return this . makeFailedResult ( "数据验证失败,分录数据格式不正确!" ) ;
}
List < JSONObject > masterList = new LinkedList < > ( ) ;
List < JSONObject > slaveList = new LinkedList < > ( ) ;
// 同一批次里发号生成的主键集合,用于防御生成器回绕或配置问题导致的重复主键。
Set < Long > newIdSet = new HashSet < > ( ) ;
String primaryKeyName = onlineTable . getPrimaryKeyColumn ( ) . getColumnName ( ) ;
for ( int i = 0 ; i < masterArray . size ( ) ; i + + ) {
Object element = masterArray . get ( i ) ;
if ( ! ( element instanceof JSONObject ) ) {
return this . makeFailedResult ( "数据验证失败,第 " + ( i + 1 ) + " 张单据的数据格式不正确!" ) ;
}
JSONObject master = ( JSONObject ) element ;
master . put ( primaryKeyName , this . generatePrimaryKey ( onlineTable . getPrimaryKeyColumn ( ) , newIdSet ) ) ;
Object slaveElement = slaveArray = = null ? slaveData : slaveArray . get ( i ) ;
JSONObject slave = slaveElement instanceof JSONObject ? ( JSONObject ) slaveElement : null ;
if ( slave ! = null ) {
this . makeupEntryPrimaryKey ( slave , newIdSet ) ;
}
masterList . add ( master ) ;
slaveList . add ( slave ) ;
}
return onlineOperationService . executeBatchSaveInTransaction ( masterList , slaveList ,
onlinePluginExecuteDto , onlineForm , sortDtoList ) ;
}
/ * *
* 批量新增的事务边界 : 整批逐条保存 , 任一条抛异常则整批回滚 。
*
* < p > 必须经 Spring 代理调用 ( 见 { @code executeBatchSave } 里的
* { @code onlineOperationService . executeBatchSaveInTransaction } ) ,
* { @link # executePluginsForBill } 内部调用的 { @code executePluginTransaction } 是自调用 , 其事务注解不生效 。 < / p >
*
* @param masterList 表头数据列表 。
* @param slaveList 分录数据列表 , 下标与表头对齐 。
* @param onlinePluginExecuteDto 操作按钮等参数 。
* @param onlineForm 当前表单 。
* @return 操作结果 。
* /
@Transactional ( rollbackFor = Exception . class )
@Override
public OperationResult executeBatchSaveInTransaction ( List < JSONObject > masterList , List < JSONObject > slaveList ,
OnlinePluginExecuteDto onlinePluginExecuteDto ,
OnlineForm onlineForm ,
List < OnlinePluginExecuteSortDto > sortDtoList ) {
List < String > savedIdList = new LinkedList < > ( ) ;
for ( int i = 0 ; i < masterList . size ( ) ; i + + ) {
JSONObject master = masterList . get ( i ) ;
// 逐条复用单条链路:附件摘取/回写、插件链顺序、反写触发等都与编辑页保存完全一致。
OperationResult singleResult =
this . executePluginsForBill ( onlinePluginExecuteDto , master , slaveList . get ( i ) , sortDtoList ) ;
if ( singleResult ! = null & & ! singleResult . isSuccess ( ) ) {
throw new OnlineRuntimeException ( singleResult . getMessage ( ) ) ;
}
savedIdList . add ( String . valueOf ( master . get ( this . resolvePrimaryKeyName ( master ) ) ) ) ;
}
OperationResult operationResult = new OperationResult ( ) ;
operationResult . setInfo ( String . join ( "," , savedIdList ) ) ;
operationResult . setShowMessage ( true ) ;
operationResult . setMessage ( "保存成功,共保存 " + savedIdList . size ( ) + " 条数据!" ) ;
return operationResult ;
}
/ * *
* 取单据主键在 JSON 中的实际 key 。
* < p > ORM 约定主键列为 id , 这里兼容大小写差异 , 避免前端或配置写成 Id 时取不到结果 。 < / p >
*
* @param master 单据数据 。
* @return 主键 key 。
* /
private String resolvePrimaryKeyName ( JSONObject master ) {
if ( master . containsKey ( "id" ) ) {
return "id" ;
}
for ( String key : master . keySet ( ) ) {
if ( "id" . equalsIgnoreCase ( key ) ) {
return key ;
}
}
return "id" ;
}
/ * *
* 生成主键 : 与 makeupColumnValue 的发号规则保持一致 , Long 列走 nextLongId ,
* 其它类型 ( Integer / String / BigDecimal 等 ) 走 nextStringId 再按列类型转换 ,
* 避免把 Long 直接塞进字符串主键列而引发类型转换失败 。
*
* @param primaryKeyColumn 主键字段 , 为空表示表未配置主键 。
* @param newIdSet 本批次已生成的主键集合 , 用于防重复 。
* @return 新主键 。
* /
private Object generatePrimaryKey ( OnlineColumn primaryKeyColumn , Set < Long > newIdSet ) {
if ( primaryKeyColumn = = null ) {
throw new OnlineRuntimeException ( "数据验证失败,表未配置主键字段,无法批量新增!" ) ;
}
String objectFieldType = primaryKeyColumn . getObjectFieldType ( ) ;
if ( ObjectFieldType . LONG . equals ( objectFieldType ) ) {
long id ;
do {
id = idGenerator . nextLongId ( ) ;
} while ( ! newIdSet . add ( id ) ) ;
return id ;
}
// 非 Long 主键(Integer / String 等):发号后按列类型转换,避免把 Long 塞进字符串主键列。
String textId = idGenerator . nextStringId ( ) ;
if ( ObjectFieldType . INTEGER . equals ( objectFieldType ) ) {
return Integer . valueOf ( textId ) ;
}
return textId ;
}
/ * *
* 给缺少主键的分录行补发主键 , 使每张单据的分录落到各自的表头下 。
*
* @param slaveData 一张单据的分录数据 , key 为分录表 tableId 。
* @param newIdSet 本批次已生成的主键集合 , 用于防重复 。
* /
private void makeupEntryPrimaryKey ( JSONObject slaveData , Set < Long > newIdSet ) {
for ( String tableIdStr : slaveData . keySet ( ) ) {
OnlineTable entryTable = onlineTableService . getOnlineTableFromCache ( this . convertToLongId ( tableIdStr ) ) ;
if ( entryTable = = null | | entryTable . getPrimaryKeyColumn ( ) = = null ) {
continue ;
}
String primaryKeyName = entryTable . getPrimaryKeyColumn ( ) . getColumnName ( ) ;
Object entryValue = slaveData . get ( tableIdStr ) ;
// 分录数据既可能是「行数组」,也可能是「单行对象」(一对一关系),两种都补齐主键。
List < JSONObject > entryRowList = new LinkedList < > ( ) ;
if ( entryValue instanceof JSONArray ) {
for ( Object row : ( JSONArray ) entryValue ) {
if ( row instanceof JSONObject ) {
entryRowList . add ( ( JSONObject ) row ) ;
}
}
} else if ( entryValue instanceof JSONObject ) {
entryRowList . add ( ( JSONObject ) entryValue ) ;
}
for ( JSONObject entryRow : entryRowList ) {
if ( entryRow . get ( primaryKeyName ) = = null ) {
entryRow . put ( primaryKeyName , this . generatePrimaryKey ( entryTable . getPrimaryKeyColumn ( ) , newIdSet ) ) ;
}
}
}
}
/ * *
* 构造失败结果 : 前端统一按红色提示展示 message 。
* /
private OperationResult makeFailedResult ( String message ) {
OperationResult operationResult = new OperationResult ( ) ;
operationResult . setSuccess ( false ) ;
operationResult . setShowMessage ( true ) ;
operationResult . setMessage ( message ) ;
return operationResult ;
}
/ * *
* 从表单 widgetJson 中解析出指定按钮按执行顺序排好的插件列表 。
*
* @param onlineForm 当前表单 。
* @param buttonName 按钮名称 。
* @return 排序后的插件列表 。
* /
private List < OnlinePluginExecuteSortDto > getSortDtoList ( OnlineForm onlineForm , String buttonName ) {
JSONObject jsonObject = JSONObject . parseObject ( onlineForm . getWidgetJson ( ) ) ;
JSONArray jsonArray = jsonObject . getJSONObject ( AppDeviceType . getDeviceType ( ) ) . getJSONArray ( "operationList" ) ;
List < OnlinePluginExecuteSortDto > sortDtoList = new ArrayList < > ( ) ;
for ( int i = 0 ; i < jsonArray . size ( ) ; i + + ) {
JSONObject object = jsonArray . getJSONObject ( i ) ;
if ( object . getString ( "name" ) . equals ( buttonName ) & & object . containsKey ( "pluginList" ) & & ! object . getJSONArray ( "pluginList" ) . isEmpty ( ) ) {
if ( object . getBoolean ( "enabled" ) ) {
JSONArray pluginList = object . getJSONArray ( "pluginList" ) ;
for ( int j = 0 ; j < pluginList . size ( ) ; j + + ) {
JSONObject pluginListJSONObject = pluginList . getJSONObject ( j ) ;
//是否启用
boolean isEnable = pluginListJSONObject . getBoolean ( "isEnable" ) ;
if ( isEnable ) {
String aClass = pluginListJSONObject . getString ( "class" ) ;
String order = pluginListJSONObject . getString ( "order" ) ;
OnlinePluginExecuteSortDto executeSortDto = new OnlinePluginExecuteSortDto ( ) ;
executeSortDto . setClassPath ( aClass ) ;
executeSortDto . setOrder ( order ) ;
sortDtoList . add ( executeSortDto ) ;
}
}
}
}
}
return sortDtoList . stream ( ) . sorted ( Comparator . comparing ( OnlinePluginExecuteSortDto : : getOrder ) ) . collect ( Collectors . toList ( ) ) ;
}
/ * *
* 针对一张单据执行完整的插件链 ( 表头 / 分录构建 、 附件摘取与回写 、 反写触发 ) 。
*
* < p > 批量新增复用此方法 : 保证每张单据走的插件链与 executePlugin 完全一致 ,
* 只是插件列表由调用方传入 , 整批只需解析一次 widgetJson 。 < / p >
*
* @param onlinePluginExecuteDto 操作按钮等参数 。
* @param masterData 表头数据 。
* @param slaveData 分录数据 。
* @param list 按执行顺序排好的插件列表 。
* @return 操作结果 。
* /
private OperationResult executePluginsForBill ( OnlinePluginExecuteDto onlinePluginExecuteDto , JSONObject masterData ,
JSONObject slaveData , List < OnlinePluginExecuteSortDto > list ) {
OnlineForm onlineForm = onlineFormService . getOnlineFormFromCache ( onlinePluginExecuteDto . getFormId ( ) ) ;
OnlineTable onlineTable = onlineTableService . getOnlineTableFromCache ( onlineForm . getMasterTableId ( ) ) ;
OnlFormHead onlFormHead = onlFormHeadService . getHeadTableNameCache ( onlineTable . getTableName ( ) ) ;
if ( BooleanUtil . isTrue ( onlinePluginExecuteDto . getIsSelectAll ( ) ) ) {
List < String > allBillIds = this . queryAllBillIds ( onlineTable , onlinePluginExecuteDto . getQueryParams ( ) ) ;
if ( CollUtil . isEmpty ( allBillIds ) ) {
OperationResult empty = new OperationResult ( ) ;
empty . setMessage ( "没有符合条件的数据!" ) ;
return empty ;
}
onlinePluginExecuteDto . setBillId ( CollUtil . join ( allBillIds , "," ) ) ;
}
// executePlugin 是所有按钮的统一入口,附件的写入/清理必须按插件类型分别设闸,
// 否则「审核」这类只提交部分字段的按钮会把附件全量覆盖掉,「删除」则不会清理附件。
boolean saveOperation = this . containsPlugin ( list , SaveOperationServicePlugIn . class ) ;
boolean deleteOperation = this . containsPlugin ( list , DeleteOperationServicePlugIn . class ) ;
ObjectValue finalObjectValue = null ;
List < PendingFileColumn > pendingFileList = new LinkedList < > ( ) ;
if ( onlFormHead . getIsomerismMark ( ) ! = null ) {
finalObjectValue = this . cntEasValue ( onlinePluginExecuteDto ,
masterData ,
slaveData ,
onlFormHead ,
onlineTable ) ;
} else {
if ( masterData ! = null ) {
finalObjectValue = onlineFormService . getObjectValue ( masterData , slaveData , onlineTable . getTableName ( ) ) ;
if ( saveOperation ) {
// 附件上传/图片上传字段:先摘出入参值并把业务列置NULL,等ORM保存回写主键后再落 zz_online_file。
pendingFileList = this . extractFileColumn ( finalObjectValue , onlineTable , slaveData ) ;
}
}
}
// 删除会被ORM级联删掉分录行,附件归属的分录主键必须在删除前查出来,删完就查不到了。
Map < Long , List < Long > > removeFileBusinessIdMap = new LinkedHashMap < > ( ) ;
if ( deleteOperation & & onlFormHead . getIsomerismMark ( ) = = null ) {
removeFileBusinessIdMap = this . collectFileBusinessIdForRemove ( onlineTable , onlinePluginExecuteDto . getBillId ( ) ) ;
}
OperationResult operationResult = exePlugin ( list , onlinePluginExecuteDto . getBillId ( ) , onlineTable . getTableName ( ) , finalObjectValue , onlineForm ) ;
// ORM保存会把生成的主键回写到入参的ObjectValue实例上,此时才能确定附件归属的业务主键。
this . flushPendingFileColumn ( pendingFileList ) ;
this . removeFileByBusinessIdMap ( removeFileBusinessIdMap ) ;
return operationResult ;
}
/ * *
* 按列表筛选条件查询主表全部主键 , 用于 "选择全部按钮" 。
* queryParams 为 { 列名 : 值 } 的等值条件 ; 为空表示无筛选 ( 即整表 ) 。
* /
@ -1833,7 +1524,7 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
@@ -1833,7 +1524,7 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
boolean isHasValue = false ;
for ( int j = 0 ; j < objectCollection . size ( ) ; j + + ) {
if ( billId . equals ( objectCollection . getObject ( j ) . getPkValue ( ) + "" ) ) {
if ( billId . equals ( objectCollection . getObject ( 0 ) . getPkValue ( ) + "" ) ) {
isHasValue = true ;
break ;
}