From cf09cac37d457169c7d5382837fa8e86740e71d4 Mon Sep 17 00:00:00 2001 From: chenchuchuan <13554600537@163.com> Date: Wed, 26 Aug 2026 13:53:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(common-online):=20=E5=8D=87=E7=BA=A7common?= =?UTF-8?q?-orm=E4=BE=9D=E8=B5=96=E5=B9=B6=E5=A2=9E=E5=BC=BA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=AD=97=E6=AE=B5=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将common-orm版本从1.0.2升级到1.0.3 - 新增FieldKind.isFileKind方法判断文件上传字段类型 - 移除OnlineAsyncConfig异步配置类 - 新增OnlineFile实体类、映射器和相关服务接口及实现 - 实现附件上传/图片上传字段的文件数据统一存储到zz_online_file表 - 添加列表查询时文件字段的回填功能 - 新增ormCountByDatasourceId和ormSumByDatasourceId接口支持统计功能 - 完善文件字段的数据验证和保存逻辑 - 实现删除操作时级联清理附件数据的功能 --- common/common-flow-online/pom.xml | 2 +- common/common-generator/pom.xml | 2 +- common/common-online/pom.xml | 2 +- .../online/abstractplugin/ListDataOrmService.java | 79 ++- .../common/online/config/OnlineAsyncConfig.java | 36 -- .../common/online/config/OnlineAutoConfig.java | 2 - .../controller/OnlineOperationController.java | 105 ++++ .../apelet/common/online/dao/OnlineFileMapper.java | 13 + .../online/dao/mapper/OnlineSysPermCodeMapper.xml | 26 + .../online/dao/mapper/OnlineSysPermMapper.xml | 24 + .../dao/mapper/OnlineSysPermModuleMapper.xml | 24 + .../apelet/common/online/model/OnlineFile.java | 104 ++++ .../common/online/model/constant/FieldKind.java | 10 + .../common/online/service/OnlineFileService.java | 54 ++ .../online/service/OnlineOperationService.java | 9 + .../online/service/impl/OnlineFileServiceImpl.java | 93 ++++ .../service/impl/OnlineOperationServiceImpl.java | 544 +++++++++++++++++++-- .../online/upms/dao/OnlineSysPermCodeMapper.java | 29 ++ ...apper.java => OnlineSysPermCodePermMapper.java} | 5 +- .../online/upms/dao/OnlineSysPermMapper.java | 27 + .../online/upms/dao/OnlineSysPermModuleMapper.java | 27 + .../common/online/upms/dao/SysPermCodeMapper.java | 14 - .../common/online/upms/dao/SysPermMapper.java | 14 - .../online/upms/dao/SysPermModuleMapper.java | 14 - .../service/impl/OnlinePermCodeServiceImpl.java | 122 +++-- .../common/online/util/OnlineOperationHelper.java | 3 +- 26 files changed, 1188 insertions(+), 196 deletions(-) delete mode 100644 common/common-online/src/main/java/apelet/common/online/config/OnlineAsyncConfig.java create mode 100644 common/common-online/src/main/java/apelet/common/online/dao/OnlineFileMapper.java create mode 100644 common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermCodeMapper.xml create mode 100644 common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermMapper.xml create mode 100644 common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermModuleMapper.xml create mode 100644 common/common-online/src/main/java/apelet/common/online/model/OnlineFile.java create mode 100644 common/common-online/src/main/java/apelet/common/online/service/OnlineFileService.java create mode 100644 common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFileServiceImpl.java create mode 100644 common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodeMapper.java rename common/common-online/src/main/java/apelet/common/online/upms/dao/{SysPermCodePermMapper.java => OnlineSysPermCodePermMapper.java} (59%) create mode 100644 common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermMapper.java create mode 100644 common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermModuleMapper.java delete mode 100644 common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodeMapper.java delete mode 100644 common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermMapper.java delete mode 100644 common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermModuleMapper.java diff --git a/common/common-flow-online/pom.xml b/common/common-flow-online/pom.xml index c89858f..9700ed9 100644 --- a/common/common-flow-online/pom.xml +++ b/common/common-flow-online/pom.xml @@ -28,7 +28,7 @@ apelet common-orm - 1.0.2 + 1.0.3 \ No newline at end of file diff --git a/common/common-generator/pom.xml b/common/common-generator/pom.xml index 0133288..9437dcd 100644 --- a/common/common-generator/pom.xml +++ b/common/common-generator/pom.xml @@ -59,7 +59,7 @@ apelet common-orm - 1.0.2 + 1.0.3 compile diff --git a/common/common-online/pom.xml b/common/common-online/pom.xml index 7d3b28b..d6785df 100644 --- a/common/common-online/pom.xml +++ b/common/common-online/pom.xml @@ -76,7 +76,7 @@ apelet common-orm - 1.0.2 + 1.0.3 diff --git a/common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java b/common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java index c6a2f5d..724926e 100644 --- a/common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java +++ b/common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java @@ -21,6 +21,7 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; +import java.math.BigDecimal; import java.util.*; import java.util.stream.Collectors; @@ -71,34 +72,11 @@ public class ListDataOrmService extends ListDataService { List> resList = new ArrayList<>(); OnlineTable masterTable = datasourceResult.getData().getMasterTable(); OnlineForm onlineForm = onlineFormService.getOnlineFormFromCache(dto.getFormId()); - // 根据当前用户角色 + 当前表单匹配权限规则:非空→基础数据权限,为空→列表权限 - List ruleSchemaList = this.getRuleSchemaListByCurrentRole(); - String conditionExpression = null; - if (CollUtil.isNotEmpty(ruleSchemaList)) { - List filtered = new ArrayList<>(); - if (StrUtil.isNotBlank(metaCode)) { - filtered = ruleSchemaList.stream().filter(s -> DataRuleType.BASIC_DATA == s.getRuleType() && metaCode.equals(s.getMetaCode())).collect(Collectors.toList()); - } - if (CollUtil.isEmpty(filtered)) { - filtered = ruleSchemaList.stream().filter(s -> DataRuleType.LIST == s.getRuleType()).collect(Collectors.toList()); - } - if (CollUtil.isNotEmpty(filtered)) { - conditionExpression = filtered.stream().map(PermissionDataRuleSchema::getConditionExpression).collect(Collectors.joining(") AND (", "(", ")")); - } - } - JSONObject widgetJson = JSON.parseObject(onlineForm.getWidgetJson()); JSONObject widgetList = widgetJson.getJSONObject("pc").getJSONObject("tableWidget"); Set selectorSet = new TreeSet<>(); getOrmSelector(widgetList, selectorSet, masterTable, new HashMap<>()); - Filter filter = getFilter(); - if (StrUtil.isNotBlank(conditionExpression)) { - TokenData tokenData = TokenData.takeFromRequest(); - // 解析 conditionExpression 中的 ${字段名} 占位符,从 tokenData 中取值填充 - conditionExpression = resolvePlaceholders(conditionExpression, tokenData); - Filter parsed = FilterParser.parse(conditionExpression); - filter = filter.and(parsed); - } + Filter filter = buildFilter(); Sorter sorter = new Sorter(); if (orderParam != null) { @@ -124,6 +102,8 @@ public class ListDataOrmService extends ListDataService { .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); onlineOperationService.buildDataListWithDict(masterTable, oneToOneRelationList, resList); onlineOperationHelper.maskFieldDataList(resList, masterTable, null, null); + // 附件上传/图片上传字段的值不落业务表,这里从 zz_online_file 回填成文件JSON数组。 + onlineOperationService.fillFileColumn(resList, masterTable); GridData gridData = new GridData(); gridData.setDataList(resList); gridData.setTotalCount(ormDataSourceUtil.count(masterTable.getTableName(), filter)); @@ -133,6 +113,57 @@ public class ListDataOrmService extends ListDataService { } /** + * 构建列表查询过滤条件:用户过滤条件 + 数据权限规则(与 loadGridData 共用)。 + */ + private Filter buildFilter() { + // 根据当前用户角色 + 当前表单匹配权限规则:非空→基础数据权限,为空→列表权限 + List ruleSchemaList = this.getRuleSchemaListByCurrentRole(); + String conditionExpression = null; + if (CollUtil.isNotEmpty(ruleSchemaList)) { + List filtered = new ArrayList<>(); + if (StrUtil.isNotBlank(metaCode)) { + filtered = ruleSchemaList.stream().filter(s -> DataRuleType.BASIC_DATA == s.getRuleType() && metaCode.equals(s.getMetaCode())).collect(Collectors.toList()); + } + if (CollUtil.isEmpty(filtered)) { + filtered = ruleSchemaList.stream().filter(s -> DataRuleType.LIST == s.getRuleType()).collect(Collectors.toList()); + } + if (CollUtil.isNotEmpty(filtered)) { + conditionExpression = filtered.stream().map(PermissionDataRuleSchema::getConditionExpression).collect(Collectors.joining(") AND (", "(", ")")); + } + } + Filter filter = getFilter(); + if (StrUtil.isNotBlank(conditionExpression)) { + TokenData tokenData = TokenData.takeFromRequest(); + // 解析 conditionExpression 中的 ${字段名} 占位符,从 tokenData 中取值填充 + conditionExpression = resolvePlaceholders(conditionExpression, tokenData); + Filter parsed = FilterParser.parse(conditionExpression); + filter = filter.and(parsed); + } + return filter; + } + + /** + * 按列表查询条件统计主表记录数。 + * + * @return 符合条件的记录总数。 + */ + public int countGridData() { + OnlineTable masterTable = datasourceResult.getData().getMasterTable(); + return ormDataSourceUtil.count(masterTable.getTableName(), buildFilter()); + } + + /** + * 按列表查询条件对指定字段求和。 + * + * @param sumColumn 要求和的字段(支持 表名.字段名 或直接字段名)。 + * @return 求和结果。 + */ + public BigDecimal sumGridData(String sumColumn) { + OnlineTable masterTable = datasourceResult.getData().getMasterTable(); + return ormDataSourceUtil.sum(masterTable.getTableName(), sumColumn, buildFilter()); + } + + /** * 解析 conditionExpression 中的 ${字段名} 占位符,从 tokenData 中取值填充。 * 如 ${loginName} → 当前登录用户名,${deptId} → 当前用户部门ID。 */ diff --git a/common/common-online/src/main/java/apelet/common/online/config/OnlineAsyncConfig.java b/common/common-online/src/main/java/apelet/common/online/config/OnlineAsyncConfig.java deleted file mode 100644 index c9cf094..0000000 --- a/common/common-online/src/main/java/apelet/common/online/config/OnlineAsyncConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package apelet.common.online.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import java.util.concurrent.ThreadPoolExecutor; - -/** - * common-online异步任务线程池配置。 - *

供在线表单发布生成权限字等异步任务使用。

- * - * @author guifc - * @date 2023-08-04 - */ -@Configuration -@EnableAsync -public class OnlineAsyncConfig { - - /** - * 权限字生成/清理专用线程池。 - */ - @Bean("permCodeTaskExecutor") - public ThreadPoolTaskExecutor permCodeTaskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(2); - executor.setMaxPoolSize(4); - executor.setQueueCapacity(100); - executor.setThreadNamePrefix("perm-code-"); - // 队列满时由调用线程执行,避免丢弃任务。 - executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); - executor.initialize(); - return executor; - } -} diff --git a/common/common-online/src/main/java/apelet/common/online/config/OnlineAutoConfig.java b/common/common-online/src/main/java/apelet/common/online/config/OnlineAutoConfig.java index ba86a99..45338b2 100644 --- a/common/common-online/src/main/java/apelet/common/online/config/OnlineAutoConfig.java +++ b/common/common-online/src/main/java/apelet/common/online/config/OnlineAutoConfig.java @@ -1,7 +1,6 @@ package apelet.common.online.config; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Import; /** * common-online模块的自动配置引导类。 @@ -10,6 +9,5 @@ import org.springframework.context.annotation.Import; * @date 2023-08-04 */ @EnableConfigurationProperties({OnlineProperties.class}) -@Import(OnlineAsyncConfig.class) public class OnlineAutoConfig { } diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java index 536f7ce..3874e13 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java @@ -858,6 +858,101 @@ public class OnlineOperationController { // } } + /** + * 根据表头Id,以及接口参数,统计动态表单数据记录数(过滤逻辑与 ormListByDatasourceId 一致)。 + * 前端不再传 datasourceId/datasourceVariableName,后端直接按表头Id(onl_form_head.id)取 zz_online_table 查询,不依赖数据源。 + * + * @param formId 表头Id,即 onl_form_head.id。 + * @param filterDtoList 多虑数据对象列表。 + * @param metaCode 透传给数据权限规则的元编码。 + * @return 符合过滤条件的记录总数。 + */ + @PostMapping("/ormCountByDatasourceId") + public ResponseResult ormCountByDatasourceId( + @MyRequestBody(required = true) Long formId, + @MyRequestBody List filterDtoList, + @MyRequestBody String metaCode + ) { + // 前端不再传 datasourceId/datasourceVariableName,直接按表头Id(onl_form_head.id)取 zz_online_table,无需查数据源。 + OnlineTable masterTable = this.getMasterTableByFormId(formId); + if (masterTable == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "数据查询失败, 请检查表单Id"); + } + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + return ResponseResult.errorFrom(filterDtoListResult); + } + ListDataOrmService listDataService = this.buildListDataOrmService(formId, filterDtoList, metaCode, masterTable); + return ResponseResult.success(listDataService.countGridData()); + } + + /** + * 根据表头Id,以及接口参数,统计动态表单数据某字段的合计值(过滤逻辑与 ormListByDatasourceId 一致)。 + * 前端不再传 datasourceId/datasourceVariableName,后端直接按表头Id(onl_form_head.id)取 zz_online_table 查询,不依赖数据源。 + * + * @param formId 表头Id,即 onl_form_head.id。 + * @param sumColumn 要求和的字段名,支持 表名.字段名 或直接字段名。 + * @param filterDtoList 多虑数据对象列表。 + * @param metaCode 透传给数据权限规则的元编码。 + * @return 符合条件的字段合计值。 + */ + @PostMapping("/ormSumByDatasourceId") + public ResponseResult ormSumByDatasourceId( + @MyRequestBody(required = true) Long formId, + @MyRequestBody(required = true) String sumColumn, + @MyRequestBody List filterDtoList, + @MyRequestBody String metaCode + ) { + // 求和字段支持 表名.字段名 或直接字段名,逐段校验防注入。 + boolean sumColumnValid = StrUtil.isNotBlank(sumColumn) && Arrays.stream(sumColumn.split("\\.")).allMatch(this::checkTableAndColumnName); + if (!sumColumnValid) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败,求和字段名非法!"); + } + // 前端不再传 datasourceId/datasourceVariableName,直接按表头Id(onl_form_head.id)取 zz_online_table,无需查数据源。 + OnlineTable masterTable = this.getMasterTableByFormId(formId); + if (masterTable == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "数据查询失败, 请检查表单Id"); + } + ResponseResult filterDtoListResult = this.verifyFilterDtoList(filterDtoList); + if (!filterDtoListResult.isSuccess()) { + return ResponseResult.errorFrom(filterDtoListResult); + } + ListDataOrmService listDataService = this.buildListDataOrmService(formId, filterDtoList, metaCode, masterTable); + return ResponseResult.success(listDataService.sumGridData(sumColumn)); + } + + /** + * 按 ormListByDatasourceId 的规则组装 ListDataOrmService:透传 metaCode、onlFormHead、主表信息,供过滤与数据权限使用。 + * 求和/统计不依赖数据源,直接用 zz_online_table 主表组装轻量数据源即可。 + */ + private ListDataOrmService buildListDataOrmService(Long formId, List filterDtoList, String metaCode, OnlineTable masterTable) { + OnlineEventPluginExecuteDto onlineEventPluginExecuteDto = new OnlineEventPluginExecuteDto(); + Map listParams = new HashMap<>(); + listParams.put("metaCode", metaCode); + listParams.put("filterDtoList", filterDtoList); + listParams.put("onlFormHead", onlFormHeadService.getOnlFormHeadFromCache(formId)); + OnlineDatasource datasource = new OnlineDatasource(); + datasource.setMasterTable(masterTable); + listParams.put("datasourceResult", ResponseResult.success(datasource)); + onlineEventPluginExecuteDto.setFormId(formId); + onlineEventPluginExecuteDto.setListParams(listParams); + return new ListDataOrmService(onlineEventPluginExecuteDto); + } + + /** + * 前端不再传 datasourceId/datasourceVariableName 时,根据表头Id(onl_form_head.id)直接取主表 zz_online_table。 + * + * @param formId 表头Id,即 onl_form_head.id。 + * @return 主表对象,表头或表不存在时返回null。 + */ + private OnlineTable getMasterTableByFormId(Long formId) { + OnlFormHead onlFormHead = onlFormHeadService.getOnlFormHeadFromCache(formId); + if (onlFormHead == null) { + return null; + } + return onlineTableService.getOne(new LambdaQueryWrapper().eq(OnlineTable::getTableName, onlFormHead.getTableName())); + } + private GridData exeListPlugin( Long formId, List filterDtoList, @@ -984,6 +1079,8 @@ public class OnlineOperationController { .filter(r -> r.getRelationType().equals(RelationType.ONE_TO_ONE)).collect(Collectors.toList()); onlineOperationService.buildDataListWithDict(masterTable, oneToOneRelationList, resultList); onlineOperationHelper.maskFieldDataList(resultList, masterTable, null, null); + // 表头的附件上传/图片上传字段从 zz_online_file 回填成文件JSON数组。 + onlineOperationService.fillFileColumn(resultList, masterTable); Map map = resultList.get(0); map.keySet().forEach(k -> { @@ -997,6 +1094,14 @@ public class OnlineOperationController { ? Double.valueOf(m.get(sort).toString()) : null, Comparator.nullsLast(Double::compareTo))) .collect(Collectors.toList()); + // 分录的附件上传/图片上传字段同样回填,online_table_id 用分录表自己的tableId。 + Object entryTableId = entryIdMap.get(k); + if (entryTableId != null) { + OnlineTable entryTable = onlineTableService.getOnlineTableFromCache(Long.valueOf(entryTableId.toString())); + if (entryTable != null) { + onlineOperationService.fillFileColumn(entryList, entryTable); + } + } listData.put(String.valueOf(entryIdMap.get(k)), entryList); } else { rowData.put(k, map.get(k)); diff --git a/common/common-online/src/main/java/apelet/common/online/dao/OnlineFileMapper.java b/common/common-online/src/main/java/apelet/common/online/dao/OnlineFileMapper.java new file mode 100644 index 0000000..a670294 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/dao/OnlineFileMapper.java @@ -0,0 +1,13 @@ +package apelet.common.online.dao; + +import apelet.common.core.base.dao.BaseDaoMapper; +import apelet.common.online.model.OnlineFile; + +/** + * 在线表单文件数据访问操作接口。 + * + * @author guifc + * @date 2023-08-04 + */ +public interface OnlineFileMapper extends BaseDaoMapper { +} diff --git a/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermCodeMapper.xml b/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermCodeMapper.xml new file mode 100644 index 0000000..bb8ac60 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermCodeMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + DELETE FROM xy_sys_perm_code WHERE perm_code_id IN + + #{permCodeId} + + + diff --git a/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermMapper.xml b/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermMapper.xml new file mode 100644 index 0000000..b9d9a0e --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + DELETE FROM xy_sys_perm WHERE perm_id IN + + #{permId} + + + diff --git a/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermModuleMapper.xml b/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermModuleMapper.xml new file mode 100644 index 0000000..a128706 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermModuleMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + DELETE FROM xy_sys_perm_module WHERE module_id IN + + #{moduleId} + + + diff --git a/common/common-online/src/main/java/apelet/common/online/model/OnlineFile.java b/common/common-online/src/main/java/apelet/common/online/model/OnlineFile.java new file mode 100644 index 0000000..e42a75b --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/model/OnlineFile.java @@ -0,0 +1,104 @@ +package apelet.common.online.model; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * 在线表单文件实体对象。 + *

附件上传/图片上传字段(file_kind=1/2)的文件数据统一落在此表,业务表中该字段仅作查询回填载体。

+ * + * @author guifc + * @date 2023-08-04 + */ +@Data +@TableName(value = "zz_online_file") +public class OnlineFile { + + /** + * 主键Id。 + */ + @TableId(value = "id") + private Long id; + + /** + * 在线表Id(zz_online_table.table_id)。 + */ + @TableField(value = "online_table_id") + private Long onlineTableId; + + /** + * 字段名称。 + */ + @TableField(value = "field_name") + private String fieldName; + + /** + * 业务Id(所在表记录主键值)。 + */ + @TableField(value = "business_id") + private Long businessId; + + /** + * 基础URL。 + */ + @TableField(value = "base_url") + private String baseUrl; + + /** + * 下载URI。 + */ + @TableField(value = "download_uri") + private String downloadUri; + + /** + * 文件名称(带后缀)。 + */ + @TableField(value = "file_name") + private String fileName; + + /** + * 上传地址。 + */ + @TableField(value = "upload_path") + private String uploadPath; + + /** + * 名称(带后缀)。 + */ + @TableField(value = "name") + private String name; + + /** + * 文件全地址。 + */ + @TableField(value = "url") + private String url; + + /** + * 创建时间。 + */ + @TableField(value = "create_time") + private Date createTime; + + /** + * 创建者。 + */ + @TableField(value = "create_user_id") + private Long createUserId; + + /** + * 修改时间。 + */ + @TableField(value = "update_time") + private Date updateTime; + + /** + * 修改人。 + */ + @TableField(value = "update_user_id") + private Long updateUserId; +} diff --git a/common/common-online/src/main/java/apelet/common/online/model/constant/FieldKind.java b/common/common-online/src/main/java/apelet/common/online/model/constant/FieldKind.java index 0470dcc..dcb8c42 100644 --- a/common/common-online/src/main/java/apelet/common/online/model/constant/FieldKind.java +++ b/common/common-online/src/main/java/apelet/common/online/model/constant/FieldKind.java @@ -92,6 +92,16 @@ public final class FieldKind { } /** + * 判断字段类别是否为附件上传/图片上传字段。 + * + * @param fieldKind 字段类别值。 + * @return 是上传字段返回true,否则false。 + */ + public static boolean isFileKind(Integer fieldKind) { + return fieldKind != null && (fieldKind == UPLOAD || fieldKind == UPLOAD_IMAGE); + } + + /** * 判断参数是否为当前常量字典的合法值。 * * @param value 待验证的参数值。 diff --git a/common/common-online/src/main/java/apelet/common/online/service/OnlineFileService.java b/common/common-online/src/main/java/apelet/common/online/service/OnlineFileService.java new file mode 100644 index 0000000..e37c2db --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/service/OnlineFileService.java @@ -0,0 +1,54 @@ +package apelet.common.online.service; + +import apelet.common.online.model.OnlineFile; + +import java.util.List; +import java.util.Map; + +/** + * 在线表单文件数据操作服务接口。 + * + * @author guifc + * @date 2023-08-04 + */ +public interface OnlineFileService { + + /** + * 全量替换指定业务记录、指定字段的文件数据:先删旧再插入。 + * + * @param onlineTableId 在线表Id(zz_online_table.table_id)。 + * @param fieldName 字段名称。 + * @param businessId 业务Id(所在表记录主键值)。 + * @param fileList 文件数据列表。 + */ + void replaceFileList(Long onlineTableId, String fieldName, Long businessId, List fileList); + + /** + * 删除指定业务记录的全部文件数据。 + * + * @param onlineTableId 在线表Id(zz_online_table.table_id)。 + * @param businessId 业务Id(所在表记录主键值)。 + */ + void removeByBusinessId(Long onlineTableId, Long businessId); + + /** + * 获取指定业务记录、指定字段的文件数据列表。 + * + * @param onlineTableId 在线表Id(zz_online_table.table_id)。 + * @param fieldName 字段名称。 + * @param businessId 业务Id(所在表记录主键值)。 + * @return 文件数据列表。 + */ + List getOnlineFileList(Long onlineTableId, String fieldName, Long businessId); + + /** + * 批量获取多个业务记录的文件数据,按业务Id分组返回。 + * + * @param onlineTableId 在线表Id(zz_online_table.table_id)。 + * @param fieldName 字段名称。 + * @param businessIdList 业务Id列表。 + * @return key为businessId,value为该业务的文件数据列表。 + */ + Map> getOnlineFileMapByBusinessIds( + Long onlineTableId, String fieldName, List businessIdList); +} diff --git a/common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java b/common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java index 84239c6..ad2bc45 100644 --- a/common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java +++ b/common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java @@ -313,4 +313,13 @@ public interface OnlineOperationService { List collectionToJson(ObjectCollection collection, List> resultList); + /** + * 查询结果回填:将附件上传/图片上传字段(field_kind=1/2)对应的 zz_online_file 数据 + * 组装成文件JSON数组,回填到查询结果的对应字段上。 + * + * @param resultList 查询结果列表,会被原地修改。 + * @param table 结果列表所属的在线数据表(主表或分录表)。 + */ + void fillFileColumn(List> resultList, OnlineTable table); + } diff --git a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFileServiceImpl.java b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFileServiceImpl.java new file mode 100644 index 0000000..9f19f8b --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFileServiceImpl.java @@ -0,0 +1,93 @@ +package apelet.common.online.service.impl; + +import apelet.common.core.annotation.MyDataSourceResolver; +import apelet.common.core.constant.ApplicationConstant; +import apelet.common.core.util.DefaultDataSourceResolver; +import apelet.common.online.dao.OnlineFileMapper; +import apelet.common.online.model.OnlineFile; +import apelet.common.online.service.OnlineFileService; +import apelet.common.sequence.wrapper.IdGeneratorWrapper; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 在线表单文件数据操作服务实现类。 + * + * @author guifc + * @date 2023-08-04 + */ +@MyDataSourceResolver( + resolver = DefaultDataSourceResolver.class, + intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) +@Service("onlineFileService") +public class OnlineFileServiceImpl implements OnlineFileService { + + @Autowired + private OnlineFileMapper onlineFileMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + @Transactional(rollbackFor = Exception.class) + @Override + public void replaceFileList(Long onlineTableId, String fieldName, Long businessId, List fileList) { + OnlineFile filter = new OnlineFile(); + filter.setOnlineTableId(onlineTableId); + filter.setFieldName(fieldName); + filter.setBusinessId(businessId); + onlineFileMapper.delete(new QueryWrapper<>(filter)); + if (CollUtil.isEmpty(fileList)) { + return; + } + Date now = new Date(); + for (OnlineFile onlineFile : fileList) { + onlineFile.setId(idGenerator.nextLongId()); + onlineFile.setOnlineTableId(onlineTableId); + onlineFile.setFieldName(fieldName); + onlineFile.setBusinessId(businessId); + if (onlineFile.getCreateTime() == null) { + onlineFile.setCreateTime(now); + } + onlineFile.setUpdateTime(now); + onlineFileMapper.insert(onlineFile); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void removeByBusinessId(Long onlineTableId, Long businessId) { + OnlineFile filter = new OnlineFile(); + filter.setOnlineTableId(onlineTableId); + filter.setBusinessId(businessId); + onlineFileMapper.delete(new QueryWrapper<>(filter)); + } + + @Override + public List getOnlineFileList(Long onlineTableId, String fieldName, Long businessId) { + OnlineFile filter = new OnlineFile(); + filter.setOnlineTableId(onlineTableId); + filter.setFieldName(fieldName); + filter.setBusinessId(businessId); + return onlineFileMapper.selectList(new QueryWrapper<>(filter)); + } + + @Override + public Map> getOnlineFileMapByBusinessIds(Long onlineTableId, String fieldName, List businessIdList) { + if (CollUtil.isEmpty(businessIdList)) { + return new HashMap<>(); + } + List fileList = onlineFileMapper.selectList(new QueryWrapper() + .eq("online_table_id", onlineTableId) + .eq("field_name", fieldName) + .in("business_id", businessIdList)); + return fileList.stream().collect(Collectors.groupingBy(OnlineFile::getBusinessId)); + } +} diff --git a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java index 0645aeb..a2d03ae 100644 --- a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java +++ b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java @@ -1,63 +1,65 @@ package apelet.common.online.service.impl; +import apelet.common.core.annotation.MultiDatabaseWriteMethod; +import apelet.common.core.annotation.MyDataSourceResolver; +import apelet.common.core.cache.CacheConfig; +import apelet.common.core.constant.*; +import apelet.common.core.exception.MyRuntimeException; +import apelet.common.core.exception.NoDataPermException; +import apelet.common.core.object.*; import apelet.common.core.util.*; +import apelet.common.datafilter.config.DataFilterProperties; +import apelet.common.dbutil.constant.DblinkType; +import apelet.common.dbutil.provider.DataSourceProvider; +import apelet.common.dict.model.TenantGlobalDict; +import apelet.common.dict.service.GlobalDictService; +import apelet.common.dict.service.TenantGlobalDictService; import apelet.common.generator.model.OnlFormField; 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.dao.ConvertRelationFOnLineMapper; +import apelet.common.online.dao.OnlineOperationMapper; +import apelet.common.online.dto.OnlineFilterDto; 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.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; +import apelet.common.online.object.TransactionalBusinessData; import apelet.common.online.plugin.OperationResult; import apelet.common.online.plugin.OperationServicePlugIn; import apelet.common.online.plugin.PlugInArgs; +import apelet.common.online.plugin.pre.DeleteOperationServicePlugIn; +import apelet.common.online.plugin.pre.SaveOperationServicePlugIn; +import apelet.common.online.service.*; +import apelet.common.online.util.*; 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.io.FileUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.text.StrFormatter; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.EnumUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.core.map.MapUtil; import cn.hutool.http.HttpUtil; import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.TypeReference; -import apelet.common.core.annotation.MultiDatabaseWriteMethod; -import apelet.common.core.annotation.MyDataSourceResolver; -import apelet.common.core.cache.CacheConfig; -import apelet.common.core.constant.*; -import apelet.common.core.exception.MyRuntimeException; -import apelet.common.core.exception.NoDataPermException; -import apelet.common.core.object.*; -import apelet.common.datafilter.config.DataFilterProperties; -import apelet.common.dbutil.constant.DblinkType; -import apelet.common.dbutil.provider.DataSourceProvider; -import apelet.common.dict.model.TenantGlobalDict; -import apelet.common.dict.service.GlobalDictService; -import apelet.common.dict.service.TenantGlobalDictService; -import apelet.common.online.config.OnlineProperties; -import apelet.common.online.dao.OnlineOperationMapper; -import apelet.common.online.dto.OnlineFilterDto; -import apelet.common.online.exception.OnlineRuntimeException; -import apelet.common.online.model.*; -import apelet.common.online.model.constant.FieldFilterType; -import apelet.common.online.model.constant.*; -import apelet.common.online.object.ConstDictInfo; -import apelet.common.online.object.*; -import apelet.common.online.service.*; -import apelet.common.online.util.*; -import apelet.common.orm.impl.EntityColumn; -import apelet.common.orm.impl.EntityTable; -import apelet.common.orm.impl.LinkEntityColumn; -import apelet.common.redis.util.CommonRedisUtil; -import apelet.common.sequence.wrapper.IdGeneratorWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; import lombok.Data; @@ -73,12 +75,9 @@ import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.io.File; -import java.io.IOException; import java.io.Serializable; -import java.nio.file.Files; import java.sql.Connection; import java.time.LocalDateTime; -import java.time.temporal.ValueRange; import java.util.*; import java.util.stream.Collectors; @@ -135,6 +134,8 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { private OnlineOperationService onlineOperationService; @Autowired private IOnlFormFieldService onlFormFieldService; + @Autowired + private OnlineFileService onlineFileService; private static final String DICT_MAP_SUFFIX = "DictMap"; private static final String DICT_MAP_LIST_SUFFIX = "DictMapList"; @@ -197,6 +198,8 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { } } } + this.saveFileColumn(table, id, data); + this.nullifyFileColumnValues(columnDataList, columnValueList); this.doInsert(table, columnNames, columnValueList); // List list = Arrays.asList(columnNames.split(",")); // ObjectValue objectValue = new ObjectValue(table.getTableName()); @@ -245,6 +248,8 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { } } // this.doInsert(table, columnNames, columnValueList); + this.saveFileColumn(table, id, data); + this.nullifyFileColumnValues(columnDataList, columnValueList); List column = Arrays.asList(columnNames.split(",")); for (int i = 0; i < columnValueList.size(); i++) { if (columnValueList.get(i) != null) { @@ -371,6 +376,10 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { } continue; } + if (FieldKind.isFileKind(columnData.getColumn().getFieldKind())) { + // 文件字段值已写入 zz_online_file,业务列统一置NULL。 + columnData.setColumnValue(null); + } if (!MyCommonUtil.equalsAny(columnData.getColumn().getFieldKind(), FieldKind.CREATE_TIME, FieldKind.CREATE_USER_ID, FieldKind.CREATE_DEPT_ID, FieldKind.TENANT_FILTER)) { updateColumnList.add(columnData); @@ -380,6 +389,7 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { } } if (CollUtil.isEmpty(updateColumnList)) { + this.saveFileColumn(table, this.convertToLongId(dataId), data); return true; } if (CollUtil.isNotEmpty(maskFieldDataList)) { @@ -388,7 +398,9 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { this.compareAndSetMaskFieldData(table, maskFieldDataList, originalData); } String dataPermFilter = this.buildDataPermFilter(table); - return this.doUpdate(table, updateColumnList, filterList, dataPermFilter); + boolean updated = this.doUpdate(table, updateColumnList, filterList, dataPermFilter); + this.saveFileColumn(table, this.convertToLongId(dataId), data); + return updated; } @MultiDatabaseWriteMethod @@ -476,6 +488,11 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { } else { this.doLogicDelete(table, table.getPrimaryKeyColumn(), dataId, dataPermFilter); } + Long businessId = this.convertToLongId(dataId); + if (businessId != null) { + // 级联清理该业务记录对应的附件数据。 + onlineFileService.removeByBusinessId(table.getTableId(), businessId); + } if (CollUtil.isEmpty(relationList)) { return true; } @@ -494,11 +511,27 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { } List slaveFilterList = this.makeDefaultFilter(relation.getSlaveTable(), slaveColumn, columnValue); + // 先查出从表受影响记录的主键,用于附件清理。 + List slaveBusinessIdList = new LinkedList<>(); + OnlineColumn slavePkColumn = slaveTable.getPrimaryKeyColumn(); + if (slavePkColumn != null) { + List> slaveRows = onlineOperationMapper.getList(slaveTable.getTableName(), null, slavePkColumn.getColumnName(), slaveFilterList, null, null); + slaveBusinessIdList = slaveRows.stream() + .map(r -> r.get(slavePkColumn.getColumnName())) + .filter(ObjectUtil::isNotEmpty) + .map(v -> this.convertToLongId(v.toString())) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } if (slaveTable.getLogicDeleteColumn() == null) { this.doDelete(slaveTable, slaveFilterList, null); } else { this.doLogicDelete(slaveTable, slaveColumn, columnValue, null); } + // 级联清理从表业务记录对应的附件数据。 + for (Long slaveBusinessId : slaveBusinessIdList) { + onlineFileService.removeByBusinessId(slaveTable.getTableId(), slaveBusinessId); + } } return true; } @@ -560,6 +593,7 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { this.buildVirtualColumn(resultList, table, toManyRelationList); } this.reformatResultListWithOneToOneRelation(resultList, oneToOneRelationList); + this.fillFileColumn(resultList, table); return resultList.get(0); } @@ -575,6 +609,7 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { slaveTable, null, selectFields, filterList, dataPermFilter, null, null); List> resultList = pageData.getDataList(); this.buildDataListWithDict(resultList, slaveTable); + this.fillFileColumn(resultList, slaveTable); return CollUtil.isEmpty(resultList) ? null : resultList.get(0); } @@ -608,6 +643,7 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { this.buildVirtualColumn(resultList, table, toManyRelationList); } this.reformatResultListWithOneToOneRelation(resultList, oneToOneRelationList); + this.fillFileColumn(resultList, table); return pageData; } @@ -694,6 +730,7 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { this.buildDataListWithDict(resultList, slaveTable); this.reformatResultListWithOneToOneRelation(resultList, oneToOneRelationList); reformatResultListWithOneToOneRelation2(resultList, slaveTable.getTableName()); + this.fillFileColumn(resultList, slaveTable); return pageData; } @@ -1021,7 +1058,12 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { } OnlineTable onlineTable = onlineTableService.getOnlineTableFromCache(onlineForm.getMasterTableId()); OnlFormHead onlFormHead = onlFormHeadService.getHeadTableNameCache(onlineTable.getTableName()); + // executePlugin 是所有按钮的统一入口,附件的写入/清理必须按插件类型分别设闸, + // 否则「审核」这类只提交部分字段的按钮会把附件全量覆盖掉,「删除」则不会清理附件。 + boolean saveOperation = this.containsPlugin(sortDtoList, SaveOperationServicePlugIn.class); + boolean deleteOperation = this.containsPlugin(sortDtoList, DeleteOperationServicePlugIn.class); ObjectValue finalObjectValue = null; + List pendingFileList = new LinkedList<>(); if (onlFormHead.getIsomerismMark() != null) { finalObjectValue = this.cntEasValue(onlinePluginExecuteDto, masterData, @@ -1031,12 +1073,294 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { } else { if(masterData != null){ finalObjectValue = onlineFormService.getObjectValue(masterData, slaveData, onlineTable.getTableName()); + if (saveOperation) { + // 附件上传/图片上传字段:先摘出入参值并把业务列置NULL,等ORM保存回写主键后再落 zz_online_file。 + pendingFileList = this.extractFileColumn(finalObjectValue, onlineTable, slaveData); + } } } List list = sortDtoList.stream().sorted(Comparator.comparing(OnlinePluginExecuteSortDto::getOrder)).collect(Collectors.toList()); - return exePlugin(list, onlinePluginExecuteDto.getBillId(), onlineTable.getTableName(), finalObjectValue, onlineForm); + // 删除会被ORM级联删掉分录行,附件归属的分录主键必须在删除前查出来,删完就查不到了。 + Map> 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; + } + + /** + * 判断当前按钮绑定的插件中是否包含指定插件。 + *

按钮与插件的绑定是 widgetJson 中 pluginList[].class 的全限定类名(见 QuickFormWidgetUtil#setSaveButtonEnabled), + * 用类名判断比用中文 buttonName 可靠。

+ */ + private boolean containsPlugin(List sortDtoList, Class pluginClass) { + if (CollUtil.isEmpty(sortDtoList)) { + return false; + } + return sortDtoList.stream().anyMatch(dto -> pluginClass.getName().equals(dto.getClassPath())); + } + + /** + * 删除前收集待清理附件的业务主键。 + *

表头主键取自billId;分录主键必须在删除前查出,因为ORM删除表头时会级联删除分录行, + * 删除后再查就拿不到,附件会在 zz_online_file 里变成孤儿记录。

+ * + * @param masterTable 表头对应的在线数据表。 + * @param billId 表头主键,批量删除时为逗号分隔的多个主键。 + * @return 在线表tableId -> 该表下待清理附件的业务主键列表。 + */ + private Map> collectFileBusinessIdForRemove(OnlineTable masterTable, String billId) { + Map> businessIdMap = new LinkedHashMap<>(); + if (masterTable == null || StrUtil.isBlank(billId)) { + return businessIdMap; + } + List masterIdList = new LinkedList<>(); + for (String idStr : StrUtil.split(billId, ',')) { + Long businessId = this.convertToLongId(StrUtil.trim(idStr)); + if (businessId != null) { + masterIdList.add(businessId); + } + } + if (CollUtil.isEmpty(masterIdList)) { + return businessIdMap; + } + businessIdMap.put(masterTable.getTableId(), masterIdList); + try { + this.collectEntryBusinessIdForRemove(masterTable, masterIdList, businessIdMap); + } catch (Exception e) { + // 分录主键查询失败不影响表头附件清理,仅记录日志。 + log.error("删除前查询分录主键失败,分录附件可能残留,table={}, billId={}", masterTable.getTableName(), billId, e); + } + return businessIdMap; + } + + /** + * 从ORM实体元数据中找出分录表(relationType=1),按表头主键查出各分录行主键。 + */ + private void collectEntryBusinessIdForRemove(OnlineTable masterTable, List masterIdList, Map> businessIdMap) { + EntityTable entityTable = ormGenDataSourceUtil.getEntityTable(masterTable.getTableName()); + if (entityTable == null || CollUtil.isEmpty(entityTable.getEntityColumnList())) { + return; + } + // 分录表名 -> 在线表tableId,用于把查询结果里的分录行归到对应的在线表。 + Map entryTableIdMap = new LinkedHashMap<>(); + Selector selector = new Selector(); + selector.getList().add(new SelectorItem("id")); + for (EntityColumn entityColumn : entityTable.getEntityColumnList()) { + if (!(entityColumn instanceof LinkEntityColumn)) { + continue; + } + EntityLinkRelation relation = ((LinkEntityColumn) entityColumn).getEntityLinkRelation(); + if (relation == null || relation.getRelationType() == null || !relation.getRelationType().equals(1)) { + continue; + } + String entryTableName = relation.getSuperTable().getTableName(); + OnlineTable entryTable = onlineTableService.getOne(new LambdaQueryWrapper().eq(OnlineTable::getTableName, entryTableName)); + if (entryTable == null) { + continue; + } + entryTableIdMap.put(entryTableName, entryTable.getTableId()); + // 只取分录主键,不需要其它字段。 + selector.getList().add(new SelectorItem(relation.getRelationName() + ".id")); + } + if (MapUtil.isEmpty(entryTableIdMap)) { + return; + } + Filter filter = new Filter(); + filter.add(new FilterItem("id", FilterItem.INNER, + masterIdList.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")))); + ObjectCollection collection = ormGenDataSourceUtil.query(masterTable.getTableName(), filter, selector); + for (int i = 0; i < collection.size(); i++) { + Map rowValues = collection.getObject(i).getValues(); + if (MapUtil.isEmpty(rowValues)) { + continue; + } + for (Object value : rowValues.values()) { + if (!(value instanceof ObjectCollection)) { + continue; + } + ObjectCollection entryCollection = (ObjectCollection) value; + for (int j = 0; j < entryCollection.size(); j++) { + ObjectValue entryRow = entryCollection.getObject(j); + // 分录行自带表名,按表名归类比猜ObjectValue上挂载的key名可靠。 + Long entryTableId = entryTableIdMap.get(entryRow.getTableName()); + if (entryTableId == null) { + continue; + } + Object entryIdValue = entryRow.get("id"); + Long entryId = entryIdValue == null ? null : this.convertToLongId(entryIdValue.toString()); + if (entryId != null) { + businessIdMap.computeIfAbsent(entryTableId, k -> new LinkedList<>()).add(entryId); + } + } + } + } + } + + /** + * 按收集到的业务主键清理 zz_online_file 中的附件数据。 + */ + private void removeFileByBusinessIdMap(Map> businessIdMap) { + if (MapUtil.isEmpty(businessIdMap)) { + return; + } + businessIdMap.forEach((onlineTableId, businessIdList) -> { + for (Long businessId : businessIdList) { + onlineFileService.removeByBusinessId(onlineTableId, businessId); + } + }); + } + + /** + * 附件字段暂存对象。 + *

持有表头/分录行的ObjectValue实例引用:新增时主键由ORM在保存过程中生成并回写到该实例上, + * 保存前拿不到主键,只能保存后按实例读回。

+ */ + private static class PendingFileColumn { + /** 附件所属的行对象,ORM保存后主键会回写到该实例上。 */ + private final ObjectValue row; + /** 行所属的在线数据表。 */ + private final OnlineTable table; + /** 附件字段的列名。 */ + private final String columnName; + /** 入参中该字段的文件JSON数组字符串,为空表示清空该字段附件。 */ + private final String fileJson; + + private PendingFileColumn(ObjectValue row, OnlineTable table, String columnName, String fileJson) { + this.row = row; + this.table = table; + this.columnName = columnName; + this.fileJson = fileJson; + } + } + + /** + * 摘出表头及各分录中附件上传/图片上传字段的入参值,并把业务列统一置NULL。 + *

按全量覆盖语义:表中所有 field_kind 为 1/2 的字段一律以入参为准,入参未包含该字段视为清空。

+ * + * @param objectValue 待保存的表头对象,分录以分录表名为key挂在其上。 + * @param masterTable 表头对应的在线数据表。 + * @param slaveData 分录入参,key为分录表的tableId。 + * @return 待落库的附件字段列表。 + */ + private List extractFileColumn(ObjectValue objectValue, OnlineTable masterTable, JSONObject slaveData) { + List pendingList = new LinkedList<>(); + if (objectValue == null) { + return pendingList; + } + this.extractFileColumnFromRow(objectValue, masterTable, pendingList); + if (slaveData == null) { + return pendingList; + } + // 分录:slaveData的key为分录表tableId,ObjectValue上以分录表名挂ObjectCollection(见OnlineFormServiceImpl#getObjectValue)。 + for (String tableIdStr : slaveData.keySet()) { + Long entryTableId = this.convertToLongId(tableIdStr); + if (entryTableId == null) { + continue; + } + OnlineTable entryTable = onlineTableService.getOnlineTableFromCache(entryTableId); + if (entryTable == null) { + continue; + } + Object collectionObj = objectValue.get(entryTable.getTableName()); + if (!(collectionObj instanceof ObjectCollection)) { + continue; + } + ObjectCollection collection = (ObjectCollection) collectionObj; + for (int i = 0; i < collection.size(); i++) { + this.extractFileColumnFromRow(collection.getObject(i), entryTable, pendingList); + } + } + return pendingList; + } + + /** + * 摘出单行中所有附件字段的值,并把该行对应列置NULL。 + */ + private void extractFileColumnFromRow(ObjectValue row, OnlineTable table, List pendingList) { + if (row == null || table == null || MapUtil.isEmpty(table.getColumnMap())) { + return; + } + for (OnlineColumn column : table.getColumnMap().values()) { + if (!FieldKind.isFileKind(column.getFieldKind())) { + continue; + } + String columnName = column.getColumnName(); + Object value = row.get(columnName); + pendingList.add(new PendingFileColumn(row, table, columnName, value == null ? null : value.toString())); + // 业务列仅作查询回填载体,不落库,统一置NULL。 + row.put(columnName, null); + } + } + + /** + * 保存完成后把暂存的附件数据落 zz_online_file,业务主键从ORM回写后的行对象上读取。 + */ + private void flushPendingFileColumn(List pendingList) { + if (CollUtil.isEmpty(pendingList)) { + return; + } + for (PendingFileColumn pending : pendingList) { + OnlineColumn primaryKeyColumn = pending.table.getPrimaryKeyColumn(); + if (primaryKeyColumn == null) { + log.warn("附件落库失败,表 [{}] 未配置主键字段", pending.table.getTableName()); + continue; + } + Object idValue = pending.row.get(primaryKeyColumn.getColumnName()); + Long businessId = idValue == null ? null : this.convertToLongId(idValue.toString()); + if (businessId == null) { + log.warn("附件落库失败,保存后未取到业务主键,table={}, column={}", pending.table.getTableName(), pending.columnName); + continue; + } + List fileList = this.parseOnlineFileList(pending.fileJson, pending.columnName); + onlineFileService.replaceFileList(pending.table.getTableId(), pending.columnName, businessId, fileList); + } + } + + /** + * 把文件字段的JSON数组字符串解析为 OnlineFile 列表。 + *

值为空或解析失败时返回空列表,即按清空该字段附件处理。

+ */ + private List parseOnlineFileList(String fileJson, String columnName) { + List fileList = new LinkedList<>(); + if (StrUtil.isBlank(fileJson)) { + return fileList; + } + JSONArray fileArray; + try { + fileArray = JSON.parseArray(fileJson); + } catch (Exception e) { + log.warn("解析文件字段 [{}] 数据失败,按清空处理,原值={}", columnName, fileJson); + return fileList; + } + if (fileArray == null) { + return fileList; + } + TokenData tokenData = TokenData.takeFromRequest(); + Long operatorUserId = tokenData == null ? null : tokenData.getUserId(); + for (int i = 0; i < fileArray.size(); i++) { + JSONObject fileItem = fileArray.getJSONObject(i); + if (fileItem == null) { + continue; + } + OnlineFile onlineFile = new OnlineFile(); + onlineFile.setBaseUrl(fileItem.getString("baseUrl")); + onlineFile.setDownloadUri(fileItem.getString("downloadUri")); + onlineFile.setFileName(fileItem.getString("filename")); + onlineFile.setUploadPath(fileItem.getString("uploadPath")); + onlineFile.setName(fileItem.getString("name")); + onlineFile.setUrl(fileItem.getString("url")); + onlineFile.setCreateUserId(operatorUserId); + onlineFile.setUpdateUserId(operatorUserId); + fileList.add(onlineFile); + } + return fileList; } @@ -2203,6 +2527,152 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { return sb.substring(0, sb.length() - 1); } + /** + * 处理附件上传/图片上传字段:将入参中的文件JSON数组解析后存入 zz_online_file,并把业务字段置空。 + *

约定:入参未包含该字段或值为null时保留原附件数据;空数组时清空该字段的全部附件。

+ * + * @param table 在线数据表对象。 + * @param id 业务记录主键值。 + * @param data 入参数据。 + */ + private void saveFileColumn(OnlineTable table, Object id, JSONObject data) { + if (id == null) { + return; + } + Long businessId = this.convertToLongId(id.toString()); + if (businessId == null) { + return; + } + for (OnlineColumn column : table.getColumnMap().values()) { + if (!FieldKind.isFileKind(column.getFieldKind())) { + continue; + } + String columnName = column.getColumnName(); + // 入参没有该字段时不做处理,保留原有附件。 + if (!data.containsKey(columnName) || data.get(columnName) == null) { + continue; + } + JSONArray fileArray; + try { + fileArray = JSON.parseArray(data.getString(columnName)); + } catch (Exception e) { + // 解析失败说明入参格式异常,保留原有附件,不做清理。 + log.warn("解析文件字段 [{}] 数据失败,保留原有附件", columnName); + continue; + } + List fileList = new LinkedList<>(); + if (fileArray != null) { + for (int i = 0; i < fileArray.size(); i++) { + OnlineFile onlineFile = new OnlineFile(); + JSONObject fileJson = fileArray.getJSONObject(i); + onlineFile.setBaseUrl(fileJson.getString("baseUrl")); + onlineFile.setDownloadUri(fileJson.getString("downloadUri")); + onlineFile.setFileName(fileJson.getString("filename")); + onlineFile.setUploadPath(fileJson.getString("uploadPath")); + onlineFile.setName(fileJson.getString("name")); + onlineFile.setUrl(fileJson.getString("url")); + onlineFile.setCreateUserId(TokenData.takeFromRequest().getUserId()); + onlineFile.setUpdateUserId(TokenData.takeFromRequest().getUserId()); + fileList.add(onlineFile); + } + } + // 全量替换:先删旧再插入,空数组即清空。 + onlineFileService.replaceFileList(table.getTableId(), columnName, businessId, fileList); + // 业务表字段仅作为查询回填载体,不落库,统一置NULL。 + data.put(columnName, null); + } + } + + /** + * 查询结果回填:将附件上传/图片上传字段对应的 zz_online_file 数据组装成文件JSON数组回填到字段上。 + * + * @param resultList 查询结果列表。 + * @param table 在线数据表对象。 + */ + @Override + public void fillFileColumn(List> resultList, OnlineTable table) { + if (CollUtil.isEmpty(resultList)) { + return; + } + List fileColumnList = table.getColumnMap().values().stream() + .filter(c -> FieldKind.isFileKind(c.getFieldKind())) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(fileColumnList)) { + return; + } + OnlineColumn primaryKeyColumn = table.getPrimaryKeyColumn(); + if (primaryKeyColumn == null) { + return; + } + List businessIdList = resultList.stream() + .map(r -> r.get(primaryKeyColumn.getColumnName())) + .filter(ObjectUtil::isNotEmpty) + .map(v -> this.convertToLongId(v.toString())) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + for (OnlineColumn column : fileColumnList) { + Map> fileMap = + onlineFileService.getOnlineFileMapByBusinessIds(table.getTableId(), column.getColumnName(), businessIdList); + if (MapUtil.isEmpty(fileMap)) { + continue; + } + for (Map result : resultList) { + Object idValue = result.get(primaryKeyColumn.getColumnName()); + if (ObjectUtil.isEmpty(idValue)) { + continue; + } + Long businessId = this.convertToLongId(idValue.toString()); + List fileList = fileMap.get(businessId); + if (CollUtil.isEmpty(fileList)) { + continue; + } + JSONArray fileArray = new JSONArray(); + for (OnlineFile onlineFile : fileList) { + JSONObject fileJson = new JSONObject(); + fileJson.put("baseUrl", onlineFile.getBaseUrl()); + fileJson.put("downloadUri", onlineFile.getDownloadUri()); + fileJson.put("filename", onlineFile.getFileName()); + fileJson.put("uploadPath", onlineFile.getUploadPath()); + fileJson.put("name", onlineFile.getName()); + fileJson.put("url", onlineFile.getUrl()); + fileJson.put("uploadFailed", false); + fileArray.add(fileJson); + } + result.put(column.getColumnName(), fileArray); + } + } + } + + /** + * 将文件字段的值在insert/update的值列表中统一置NULL,避免文件JSON写入业务表。 + *

columnValueList与columnDataList对齐(均跳过自增列)。

+ */ + private void nullifyFileColumnValues(List columnDataList, List columnValueList) { + int valueIndex = 0; + for (ColumnData columnData : columnDataList) { + if (BooleanUtil.isTrue(columnData.getColumn().getAutoIncrement())) { + continue; + } + if (FieldKind.isFileKind(columnData.getColumn().getFieldKind())) { + columnData.setColumnValue(null); + columnValueList.set(valueIndex, null); + } + valueIndex++; + } + } + + private Long convertToLongId(String dataId) { + if (StrUtil.isBlank(dataId)) { + return null; + } + try { + return Long.valueOf(dataId); + } catch (NumberFormatException e) { + return null; + } + } + private void makeupColumnValue(ColumnData columnData) { if (BooleanUtil.isTrue(columnData.getColumn().getAutoIncrement())) { return; diff --git a/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodeMapper.java b/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodeMapper.java new file mode 100644 index 0000000..fbf01b9 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodeMapper.java @@ -0,0 +1,29 @@ +package apelet.common.online.upms.dao; + +import apelet.common.core.base.dao.BaseDaoMapper; +import apelet.common.online.upms.model.SysPermCode; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; + +/** + * 权限字数据访问操作接口。 + *

在线表单发布自动生成权限字使用,精简自 tenant-admin 的同名 Mapper。为避免与 tenant-admin 同名 Mapper + * 的 bean 名冲突,采用 Online 前缀命名。

+ * + * @author guifc + * @date 2023-08-04 + */ +public interface OnlineSysPermCodeMapper extends BaseDaoMapper { + + /** + * 按主键Id集合物理删除权限字。 + *

xy_sys_perm_code 上有 (perm_code, deleted_flag) 唯一索引,逻辑删除只是把 deleted_flag 置为 -1, + * 同一表单重复发布时第二次清理会与已存在的 -1 行冲突报 Duplicate entry。发布时自动生成的权限字属于派生数据, + * 无需保留删除痕迹,因此走物理删除。自定义 SQL 不受实体上 {@code @TableLogic} 的影响。

+ * + * @param permCodeIdList 权限字主键Id集合,不能为空。 + * @return 删除行数。 + */ + int physicalDeleteByIdList(@Param("permCodeIdList") Collection permCodeIdList); +} diff --git a/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodePermMapper.java b/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodePermMapper.java similarity index 59% rename from common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodePermMapper.java rename to common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodePermMapper.java index d4075b3..aeb7736 100644 --- a/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodePermMapper.java +++ b/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodePermMapper.java @@ -5,10 +5,11 @@ import apelet.common.online.upms.model.SysPermCodePerm; /** * 权限字与权限资源关系数据访问操作接口。 - *

在线表单发布自动生成权限字使用,精简自 tenant-admin 的同名 Mapper。

+ *

在线表单发布自动生成权限字使用,精简自 tenant-admin 的同名 Mapper。为避免与 tenant-admin 同名 Mapper + * 的 bean 名冲突,采用 Online 前缀命名。

* * @author guifc * @date 2023-08-04 */ -public interface SysPermCodePermMapper extends BaseDaoMapper { +public interface OnlineSysPermCodePermMapper extends BaseDaoMapper { } diff --git a/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermMapper.java b/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermMapper.java new file mode 100644 index 0000000..0c04505 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermMapper.java @@ -0,0 +1,27 @@ +package apelet.common.online.upms.dao; + +import apelet.common.core.base.dao.BaseDaoMapper; +import apelet.common.online.upms.model.SysPerm; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; + +/** + * 权限资源数据访问操作接口。 + *

在线表单发布自动生成权限字使用,精简自 tenant-admin 的同名 Mapper。为避免与 tenant-admin 同名 Mapper + * 的 bean 名冲突,采用 Online 前缀命名。

+ * + * @author guifc + * @date 2023-08-04 + */ +public interface OnlineSysPermMapper extends BaseDaoMapper { + + /** + * 按主键Id集合物理删除权限资源。 + *

与权限字保持一致:发布时自动生成的权限资源属于派生数据,逻辑删除只会不断堆积 deleted_flag = -1 的垃圾行。

+ * + * @param permIdList 权限资源主键Id集合,不能为空。 + * @return 删除行数。 + */ + int physicalDeleteByIdList(@Param("permIdList") Collection permIdList); +} diff --git a/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermModuleMapper.java b/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermModuleMapper.java new file mode 100644 index 0000000..4264c63 --- /dev/null +++ b/common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermModuleMapper.java @@ -0,0 +1,27 @@ +package apelet.common.online.upms.dao; + +import apelet.common.core.base.dao.BaseDaoMapper; +import apelet.common.online.upms.model.SysPermModule; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; + +/** + * 权限模块数据访问操作接口。 + *

在线表单发布自动生成权限字使用,精简自 tenant-admin 的同名 Mapper。为避免与 tenant-admin 同名 Mapper + * 的 bean 名冲突,采用 Online 前缀命名。

+ * + * @author guifc + * @date 2023-08-04 + */ +public interface OnlineSysPermModuleMapper extends BaseDaoMapper { + + /** + * 按主键Id集合物理删除权限模块。 + *

与权限字保持一致:发布时自动生成的权限模块属于派生数据,逻辑删除只会不断堆积 deleted_flag = -1 的垃圾行。

+ * + * @param moduleIdList 权限模块主键Id集合,不能为空。 + * @return 删除行数。 + */ + int physicalDeleteByIdList(@Param("moduleIdList") Collection moduleIdList); +} diff --git a/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodeMapper.java b/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodeMapper.java deleted file mode 100644 index d3225e7..0000000 --- a/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodeMapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package apelet.common.online.upms.dao; - -import apelet.common.core.base.dao.BaseDaoMapper; -import apelet.common.online.upms.model.SysPermCode; - -/** - * 权限字数据访问操作接口。 - *

在线表单发布自动生成权限字使用,精简自 tenant-admin 的同名 Mapper。

- * - * @author guifc - * @date 2023-08-04 - */ -public interface SysPermCodeMapper extends BaseDaoMapper { -} diff --git a/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermMapper.java b/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermMapper.java deleted file mode 100644 index d290163..0000000 --- a/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermMapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package apelet.common.online.upms.dao; - -import apelet.common.core.base.dao.BaseDaoMapper; -import apelet.common.online.upms.model.SysPerm; - -/** - * 权限资源数据访问操作接口。 - *

在线表单发布自动生成权限字使用,精简自 tenant-admin 的同名 Mapper。

- * - * @author guifc - * @date 2023-08-04 - */ -public interface SysPermMapper extends BaseDaoMapper { -} diff --git a/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermModuleMapper.java b/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermModuleMapper.java deleted file mode 100644 index 2cb8969..0000000 --- a/common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermModuleMapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package apelet.common.online.upms.dao; - -import apelet.common.core.base.dao.BaseDaoMapper; -import apelet.common.online.upms.model.SysPermModule; - -/** - * 权限模块数据访问操作接口。 - *

在线表单发布自动生成权限字使用,精简自 tenant-admin 的同名 Mapper。

- * - * @author guifc - * @date 2023-08-04 - */ -public interface SysPermModuleMapper extends BaseDaoMapper { -} diff --git a/common/common-online/src/main/java/apelet/common/online/upms/service/impl/OnlinePermCodeServiceImpl.java b/common/common-online/src/main/java/apelet/common/online/upms/service/impl/OnlinePermCodeServiceImpl.java index 7266b79..bf1659c 100644 --- a/common/common-online/src/main/java/apelet/common/online/upms/service/impl/OnlinePermCodeServiceImpl.java +++ b/common/common-online/src/main/java/apelet/common/online/upms/service/impl/OnlinePermCodeServiceImpl.java @@ -11,10 +11,10 @@ import apelet.common.online.dao.OnlinePageMapper; import apelet.common.online.dao.OnlineTableMapper; import apelet.common.online.model.OnlinePage; import apelet.common.online.model.OnlineTable; -import apelet.common.online.upms.dao.SysPermCodeMapper; -import apelet.common.online.upms.dao.SysPermCodePermMapper; -import apelet.common.online.upms.dao.SysPermMapper; -import apelet.common.online.upms.dao.SysPermModuleMapper; +import apelet.common.online.upms.dao.OnlineSysPermCodeMapper; +import apelet.common.online.upms.dao.OnlineSysPermCodePermMapper; +import apelet.common.online.upms.dao.OnlineSysPermMapper; +import apelet.common.online.upms.dao.OnlineSysPermModuleMapper; import apelet.common.online.upms.model.SysPerm; import apelet.common.online.upms.model.SysPermCode; import apelet.common.online.upms.model.SysPermCodePerm; @@ -27,11 +27,12 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import javax.annotation.PreDestroy; import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -61,6 +62,16 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { @Value("${common-online.urlPrefix:}") private String urlPrefix; + /** + * 权限字生成/清理专用线程池,不依赖Spring异步体系,避免影响应用原有线程池配置。 + */ + private final ExecutorService permCodeExecutor = Executors.newFixedThreadPool(2, r -> { + Thread thread = new Thread(r); + thread.setName("perm-code-" + thread.getId()); + thread.setDaemon(true); + return thread; + }); + @Autowired private OnlinePageMapper onlinePageMapper; @Autowired @@ -68,42 +79,58 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { @Autowired private OnlineTableMapper onlineTableMapper; @Autowired - private SysPermModuleMapper sysPermModuleMapper; + private OnlineSysPermModuleMapper onlineSysPermModuleMapper; @Autowired - private SysPermMapper sysPermMapper; + private OnlineSysPermMapper onlineSysPermMapper; @Autowired - private SysPermCodeMapper sysPermCodeMapper; + private OnlineSysPermCodeMapper onlineSysPermCodeMapper; @Autowired - private SysPermCodePermMapper sysPermCodePermMapper; + private OnlineSysPermCodePermMapper onlineSysPermCodePermMapper; @Autowired private IdGeneratorWrapper idGenerator; /** * 发布后异步生成权限字,先生成前先清理旧数据保证幂等。 */ - @Async("permCodeTaskExecutor") - @Transactional(rollbackFor = Exception.class) @Override public void genPermCodeByPageId(Long pageId, Long userId) { - try { - this.doGenPermCode(pageId, userId); - } catch (Exception e) { - log.error("在线表单发布生成权限字失败,pageId={}", pageId, e); - } + permCodeExecutor.execute(() -> { + // 异步线程没有请求上下文,需显式指定在线表单数据源。 + Integer originalType = DefaultDataSourceResolver.setDataSourceType(ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE); + try { + this.doGenPermCode(pageId, userId); + } catch (Exception e) { + log.error("在线表单发布生成权限字失败,pageId={}", pageId, e); + } finally { + DefaultDataSourceResolver.unset(originalType); + } + }); } /** * 删除表单后异步清理权限字相关数据。 */ - @Async("permCodeTaskExecutor") - @Transactional(rollbackFor = Exception.class) @Override public void removePermCodeByPageCode(String pageCode) { - try { - this.doRemovePermCode(pageCode); - } catch (Exception e) { - log.error("在线表单删除清理权限字失败,pageCode={}", pageCode, e); - } + permCodeExecutor.execute(() -> { + // 异步线程没有请求上下文,需显式指定在线表单数据源。 + Integer originalType = DefaultDataSourceResolver.setDataSourceType(ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE); + try { + this.doRemovePermCode(pageCode); + } catch (Exception e) { + log.error("在线表单删除清理权限字失败,pageCode={}", pageCode, e); + } finally { + DefaultDataSourceResolver.unset(originalType); + } + }); + } + + /** + * 应用关闭时释放线程池。 + */ + @PreDestroy + public void shutdownExecutor() { + permCodeExecutor.shutdown(); } private void doGenPermCode(Long pageId, Long userId) { @@ -149,15 +176,15 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { Long operatorUserId = userId != null ? userId : 0L; // 权限模块:moduleType=0 根模块 + moduleType=1 子模块,所有权限挂在子模块下。 SysPermModule rootModule = this.buildModule(0L, 0, pageName, 1, operatorUserId, now); - sysPermModuleMapper.insert(rootModule); + onlineSysPermModuleMapper.insert(rootModule); SysPermModule childModule = this.buildModule(rootModule.getModuleId(), 1, pageName, 1, operatorUserId, now); - sysPermModuleMapper.insert(childModule); + onlineSysPermModuleMapper.insert(childModule); // 权限字树:type=0 根 -> type=1 片段 -> type=2 操作。 SysPermCode code0 = this.buildPermCode(0L, pageCode, 0, pageName, 1, operatorUserId, now); - sysPermCodeMapper.insert(code0); + onlineSysPermCodeMapper.insert(code0); String code1Str = pageCode + ":" + pageCode; SysPermCode code1 = this.buildPermCode(code0.getPermCodeId(), code1Str, 1, pageName, 1, operatorUserId, now); - sysPermCodeMapper.insert(code1); + onlineSysPermCodeMapper.insert(code1); // 每个表(主表+附表)各生成下载/上传两条 perm、两条操作权限字及其关联。 for (OnlFormHead head : headList) { if (StrUtil.isBlank(head.getTableName())) { @@ -176,13 +203,13 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { String uploadUrl = this.makeUrl(isMain ? UPLOAD_DATASOURCE : UPLOAD_ONE_TO_MANY, mainModelName); String code2Base = code1Str + ":" + modelName; SysPerm downloadPerm = this.buildPerm(childModule.getModuleId(), permNameBase + "下载文件", downloadUrl, 1, operatorUserId, now); - sysPermMapper.insert(downloadPerm); + onlineSysPermMapper.insert(downloadPerm); SysPerm uploadPerm = this.buildPerm(childModule.getModuleId(), permNameBase + "上传文件", uploadUrl, 2, operatorUserId, now); - sysPermMapper.insert(uploadPerm); + onlineSysPermMapper.insert(uploadPerm); SysPermCode code2Download = this.buildPermCode(code1.getPermCodeId(), code2Base + "download", 2, permNameBase + "下载文件", 1, operatorUserId, now); - sysPermCodeMapper.insert(code2Download); + onlineSysPermCodeMapper.insert(code2Download); SysPermCode code2Upload = this.buildPermCode(code1.getPermCodeId(), code2Base + "upload", 2, permNameBase + "上传文件", 2, operatorUserId, now); - sysPermCodeMapper.insert(code2Upload); + onlineSysPermCodeMapper.insert(code2Upload); this.insertPermCodePerm(code2Download.getPermCodeId(), downloadPerm.getPermId()); this.insertPermCodePerm(code2Upload.getPermCodeId(), uploadPerm.getPermId()); } @@ -196,8 +223,7 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { if (StrUtil.isBlank(pageCode)) { return; } - SysPermCode root = sysPermCodeMapper.selectOne(new QueryWrapper() - .eq("perm_code", pageCode).eq("perm_code_type", 0)); + SysPermCode root = onlineSysPermCodeMapper.selectOne(new QueryWrapper().eq("perm_code", pageCode).eq("perm_code_type", 0)); if (root == null) { return; } @@ -207,33 +233,31 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { Set codeIdSet = new HashSet<>(codeIdList); List parentIdList = new ArrayList<>(codeIdList); while (CollUtil.isNotEmpty(parentIdList)) { - List childList = sysPermCodeMapper.selectList( - new QueryWrapper().in("parent_id", parentIdList)); + List childList = onlineSysPermCodeMapper.selectList(new QueryWrapper().in("parent_id", parentIdList)); parentIdList = childList.stream().map(SysPermCode::getPermCodeId).collect(Collectors.toList()); codeIdSet.addAll(parentIdList); } - // 权限字与权限关联、权限资源及其模块。 - List relationList = sysPermCodePermMapper.selectList( - new QueryWrapper().in("perm_code_id", codeIdSet)); + // 权限字与权限关联、权限资源及其模块。注意 SysPermCode/SysPerm/SysPermModule 都带 @TableLogic, + // mapper.delete 只会把 deleted_flag 置为 -1,而 xy_sys_perm_code 上有 (perm_code, deleted_flag) 唯一索引, + // 同一表单重复发布时第二次清理必然撞 Duplicate entry,因此这里统一走物理删除。 + List relationList = onlineSysPermCodePermMapper.selectList(new QueryWrapper().in("perm_code_id", codeIdSet)); if (CollUtil.isNotEmpty(relationList)) { - sysPermCodePermMapper.delete(new QueryWrapper().in("perm_code_id", codeIdSet)); + onlineSysPermCodePermMapper.delete(new QueryWrapper().in("perm_code_id", codeIdSet)); } Set permIdSet = relationList.stream().map(SysPermCodePerm::getPermId).collect(Collectors.toSet()); Set moduleIdSet = new HashSet<>(); if (CollUtil.isNotEmpty(permIdSet)) { - List permList = sysPermMapper.selectList(new QueryWrapper().in("perm_id", permIdSet)); + List permList = onlineSysPermMapper.selectList(new QueryWrapper().in("perm_id", permIdSet)); moduleIdSet = permList.stream().map(SysPerm::getModuleId).collect(Collectors.toSet()); - sysPermMapper.delete(new QueryWrapper().in("perm_id", permIdSet)); + onlineSysPermMapper.physicalDeleteByIdList(permIdSet); } - sysPermCodeMapper.delete(new QueryWrapper().in("perm_code_id", codeIdSet)); + onlineSysPermCodeMapper.physicalDeleteByIdList(codeIdSet); // 删除权限模块(子模块及其上级根模块)。 if (CollUtil.isNotEmpty(moduleIdSet)) { - List moduleList = sysPermModuleMapper.selectList( - new QueryWrapper().in("module_id", moduleIdSet)); - moduleIdSet = moduleList.stream().flatMap(m -> Stream.of(m.getModuleId(), m.getParentId())) - .filter(Objects::nonNull).filter(id -> id != 0).collect(Collectors.toSet()); + List moduleList = onlineSysPermModuleMapper.selectList(new QueryWrapper().in("module_id", moduleIdSet)); + moduleIdSet = moduleList.stream().flatMap(m -> Stream.of(m.getModuleId(), m.getParentId())).filter(Objects::nonNull).filter(id -> id != 0).collect(Collectors.toSet()); if (CollUtil.isNotEmpty(moduleIdSet)) { - sysPermModuleMapper.delete(new QueryWrapper().in("module_id", moduleIdSet)); + onlineSysPermModuleMapper.physicalDeleteByIdList(moduleIdSet); } } log.info("在线表单删除清理权限字完成,pageCode={}, 权限字数={}", pageCode, codeIdSet.size()); @@ -300,6 +324,6 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { SysPermCodePerm permCodePerm = new SysPermCodePerm(); permCodePerm.setPermCodeId(permCodeId); permCodePerm.setPermId(permId); - sysPermCodePermMapper.insert(permCodePerm); + onlineSysPermCodePermMapper.insert(permCodePerm); } } diff --git a/common/common-online/src/main/java/apelet/common/online/util/OnlineOperationHelper.java b/common/common-online/src/main/java/apelet/common/online/util/OnlineOperationHelper.java index 3d8fd37..1482d4e 100644 --- a/common/common-online/src/main/java/apelet/common/online/util/OnlineOperationHelper.java +++ b/common/common-online/src/main/java/apelet/common/online/util/OnlineOperationHelper.java @@ -167,7 +167,8 @@ public class OnlineOperationHelper { return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); } } else { - if (value == null && !column.getNullable() && StrUtil.isBlank(column.getEncodedRule())) { + if (value == null && !column.getNullable() && StrUtil.isBlank(column.getEncodedRule()) + && !FieldKind.isFileKind(column.getFieldKind())) { errorMessage = "数据验证失败,数据表 [" + table.getTableName() + "] 字段 [" + column.getColumnName() + "] 不能为空值!"; return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);