Browse Source

feat(common-online): 升级common-orm依赖并增强文件字段处理功能

- 将common-orm版本从1.0.2升级到1.0.3
- 新增FieldKind.isFileKind方法判断文件上传字段类型
- 移除OnlineAsyncConfig异步配置类
- 新增OnlineFile实体类、映射器和相关服务接口及实现
- 实现附件上传/图片上传字段的文件数据统一存储到zz_online_file表
- 添加列表查询时文件字段的回填功能
- 新增ormCountByDatasourceId和ormSumByDatasourceId接口支持统计功能
- 完善文件字段的数据验证和保存逻辑
- 实现删除操作时级联清理附件数据的功能
feature/2026-08/0812-ccc-dev
chenchuchuan 3 days ago
parent
commit
cf09cac37d
  1. 2
      common/common-flow-online/pom.xml
  2. 2
      common/common-generator/pom.xml
  3. 2
      common/common-online/pom.xml
  4. 79
      common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java
  5. 36
      common/common-online/src/main/java/apelet/common/online/config/OnlineAsyncConfig.java
  6. 2
      common/common-online/src/main/java/apelet/common/online/config/OnlineAutoConfig.java
  7. 105
      common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java
  8. 13
      common/common-online/src/main/java/apelet/common/online/dao/OnlineFileMapper.java
  9. 26
      common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermCodeMapper.xml
  10. 24
      common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermMapper.xml
  11. 24
      common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermModuleMapper.xml
  12. 104
      common/common-online/src/main/java/apelet/common/online/model/OnlineFile.java
  13. 10
      common/common-online/src/main/java/apelet/common/online/model/constant/FieldKind.java
  14. 54
      common/common-online/src/main/java/apelet/common/online/service/OnlineFileService.java
  15. 9
      common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java
  16. 93
      common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFileServiceImpl.java
  17. 544
      common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java
  18. 29
      common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodeMapper.java
  19. 5
      common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodePermMapper.java
  20. 27
      common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermMapper.java
  21. 27
      common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermModuleMapper.java
  22. 14
      common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodeMapper.java
  23. 14
      common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermMapper.java
  24. 14
      common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermModuleMapper.java
  25. 122
      common/common-online/src/main/java/apelet/common/online/upms/service/impl/OnlinePermCodeServiceImpl.java
  26. 3
      common/common-online/src/main/java/apelet/common/online/util/OnlineOperationHelper.java

2
common/common-flow-online/pom.xml

@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-orm</artifactId>
<version>1.0.2</version>
<version>1.0.3</version>
</dependency>
</dependencies>
</project>

2
common/common-generator/pom.xml

@ -59,7 +59,7 @@ @@ -59,7 +59,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-orm</artifactId>
<version>1.0.2</version>
<version>1.0.3</version>
<scope>compile</scope>
</dependency>
<dependency>

2
common/common-online/pom.xml

@ -76,7 +76,7 @@ @@ -76,7 +76,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-orm</artifactId>
<version>1.0.2</version>
<version>1.0.3</version>
</dependency>
</dependencies>

79
common/common-online/src/main/java/apelet/common/online/abstractplugin/ListDataOrmService.java

@ -21,6 +21,7 @@ import com.alibaba.fastjson.JSON; @@ -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 { @@ -71,34 +72,11 @@ public class ListDataOrmService extends ListDataService {
List<Map<String, Object>> resList = new ArrayList<>();
OnlineTable masterTable = datasourceResult.getData().getMasterTable();
OnlineForm onlineForm = onlineFormService.getOnlineFormFromCache(dto.getFormId());
// 根据当前用户角色 + 当前表单匹配权限规则:非空→基础数据权限,为空→列表权限
List<PermissionDataRuleSchema> ruleSchemaList = this.getRuleSchemaListByCurrentRole();
String conditionExpression = null;
if (CollUtil.isNotEmpty(ruleSchemaList)) {
List<PermissionDataRuleSchema> 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<String> 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 { @@ -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 { @@ -133,6 +113,57 @@ public class ListDataOrmService extends ListDataService {
}
/**
* 构建列表查询过滤条件用户过滤条件 + 数据权限规则 loadGridData 共用
*/
private Filter buildFilter() {
// 根据当前用户角色 + 当前表单匹配权限规则:非空→基础数据权限,为空→列表权限
List<PermissionDataRuleSchema> ruleSchemaList = this.getRuleSchemaListByCurrentRole();
String conditionExpression = null;
if (CollUtil.isNotEmpty(ruleSchemaList)) {
List<PermissionDataRuleSchema> 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
*/

36
common/common-online/src/main/java/apelet/common/online/config/OnlineAsyncConfig.java

@ -1,36 +0,0 @@ @@ -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异步任务线程池配置
* <p>供在线表单发布生成权限字等异步任务使用</p>
*
* @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;
}
}

2
common/common-online/src/main/java/apelet/common/online/config/OnlineAutoConfig.java

@ -1,7 +1,6 @@ @@ -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; @@ -10,6 +9,5 @@ import org.springframework.context.annotation.Import;
* @date 2023-08-04
*/
@EnableConfigurationProperties({OnlineProperties.class})
@Import(OnlineAsyncConfig.class)
public class OnlineAutoConfig {
}

105
common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java

@ -858,6 +858,101 @@ public class OnlineOperationController { @@ -858,6 +858,101 @@ public class OnlineOperationController {
// }
}
/**
* 根据表头Id以及接口参数统计动态表单数据记录数过滤逻辑与 ormListByDatasourceId 一致
* 前端不再传 datasourceId/datasourceVariableName后端直接按表头Idonl_form_head.id zz_online_table 查询不依赖数据源
*
* @param formId 表头Id onl_form_head.id
* @param filterDtoList 多虑数据对象列表
* @param metaCode 透传给数据权限规则的元编码
* @return 符合过滤条件的记录总数
*/
@PostMapping("/ormCountByDatasourceId")
public ResponseResult<Object> ormCountByDatasourceId(
@MyRequestBody(required = true) Long formId,
@MyRequestBody List<OnlineFilterDto> 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<Void> 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后端直接按表头Idonl_form_head.id zz_online_table 查询不依赖数据源
*
* @param formId 表头Id onl_form_head.id
* @param sumColumn 要求和的字段名支持 表名.字段名 或直接字段名
* @param filterDtoList 多虑数据对象列表
* @param metaCode 透传给数据权限规则的元编码
* @return 符合条件的字段合计值
*/
@PostMapping("/ormSumByDatasourceId")
public ResponseResult<Object> ormSumByDatasourceId(
@MyRequestBody(required = true) Long formId,
@MyRequestBody(required = true) String sumColumn,
@MyRequestBody List<OnlineFilterDto> 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<Void> 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透传 metaCodeonlFormHead主表信息供过滤与数据权限使用
* 求和/统计不依赖数据源直接用 zz_online_table 主表组装轻量数据源即可
*/
private ListDataOrmService buildListDataOrmService(Long formId, List<OnlineFilterDto> filterDtoList, String metaCode, OnlineTable masterTable) {
OnlineEventPluginExecuteDto onlineEventPluginExecuteDto = new OnlineEventPluginExecuteDto();
Map<String, Object> 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 根据表头Idonl_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<OnlineTable>().eq(OnlineTable::getTableName, onlFormHead.getTableName()));
}
private GridData exeListPlugin(
Long formId,
List<OnlineFilterDto> filterDtoList,
@ -984,6 +1079,8 @@ public class OnlineOperationController { @@ -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<String, Object> map = resultList.get(0);
map.keySet().forEach(k -> {
@ -997,6 +1094,14 @@ public class OnlineOperationController { @@ -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));

13
common/common-online/src/main/java/apelet/common/online/dao/OnlineFileMapper.java

@ -0,0 +1,13 @@ @@ -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<OnlineFile> {
}

26
common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermCodeMapper.xml

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="apelet.common.online.upms.dao.OnlineSysPermCodeMapper">
<resultMap id="BaseResultMap" type="apelet.common.online.upms.model.SysPermCode">
<id column="perm_code_id" jdbcType="BIGINT" property="permCodeId"/>
<result column="parent_id" jdbcType="BIGINT" property="parentId"/>
<result column="perm_code" jdbcType="VARCHAR" property="permCode"/>
<result column="perm_code_type" jdbcType="INTEGER" property="permCodeType"/>
<result column="show_name" jdbcType="VARCHAR" property="showName"/>
<result column="show_order" jdbcType="INTEGER" property="showOrder"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
<result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
<result column="deleted_flag" jdbcType="INTEGER" property="deletedFlag"/>
</resultMap>
<!-- 物理删除,绕开实体上的 @TableLogic。xy_sys_perm_code 有唯一索引 uk_perm_code(perm_code, deleted_flag),
逻辑删除只把 deleted_flag 置为 -1,同一表单重复发布时第二次清理会撞 Duplicate entry。 -->
<delete id="physicalDeleteByIdList">
DELETE FROM xy_sys_perm_code WHERE perm_code_id IN
<foreach collection="permCodeIdList" item="permCodeId" separator="," open="(" close=")">
#{permCodeId}
</foreach>
</delete>
</mapper>

24
common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermMapper.xml

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="apelet.common.online.upms.dao.OnlineSysPermMapper">
<resultMap id="BaseResultMap" type="apelet.common.online.upms.model.SysPerm">
<id column="perm_id" jdbcType="BIGINT" property="permId"/>
<result column="module_id" jdbcType="BIGINT" property="moduleId"/>
<result column="perm_name" jdbcType="VARCHAR" property="permName"/>
<result column="url" jdbcType="VARCHAR" property="url"/>
<result column="show_order" jdbcType="INTEGER" property="showOrder"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
<result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
<result column="deleted_flag" jdbcType="INTEGER" property="deletedFlag"/>
</resultMap>
<!-- 物理删除,绕开实体上的 @TableLogic。发布时自动生成的权限资源属于派生数据,逻辑删除只会堆积 deleted_flag = -1 的垃圾行。 -->
<delete id="physicalDeleteByIdList">
DELETE FROM xy_sys_perm WHERE perm_id IN
<foreach collection="permIdList" item="permId" separator="," open="(" close=")">
#{permId}
</foreach>
</delete>
</mapper>

24
common/common-online/src/main/java/apelet/common/online/dao/mapper/OnlineSysPermModuleMapper.xml

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="apelet.common.online.upms.dao.OnlineSysPermModuleMapper">
<resultMap id="BaseResultMap" type="apelet.common.online.upms.model.SysPermModule">
<id column="module_id" jdbcType="BIGINT" property="moduleId"/>
<result column="parent_id" jdbcType="BIGINT" property="parentId"/>
<result column="module_name" jdbcType="VARCHAR" property="moduleName"/>
<result column="module_type" jdbcType="INTEGER" property="moduleType"/>
<result column="show_order" jdbcType="INTEGER" property="showOrder"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
<result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
<result column="deleted_flag" jdbcType="INTEGER" property="deletedFlag"/>
</resultMap>
<!-- 物理删除,绕开实体上的 @TableLogic。发布时自动生成的权限模块属于派生数据,逻辑删除只会堆积 deleted_flag = -1 的垃圾行。 -->
<delete id="physicalDeleteByIdList">
DELETE FROM xy_sys_perm_module WHERE module_id IN
<foreach collection="moduleIdList" item="moduleId" separator="," open="(" close=")">
#{moduleId}
</foreach>
</delete>
</mapper>

104
common/common-online/src/main/java/apelet/common/online/model/OnlineFile.java

@ -0,0 +1,104 @@ @@ -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;
/**
* 在线表单文件实体对象
* <p>附件上传/图片上传字段(file_kind=1/2)的文件数据统一落在此表业务表中该字段仅作查询回填载体</p>
*
* @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;
}

10
common/common-online/src/main/java/apelet/common/online/model/constant/FieldKind.java

@ -92,6 +92,16 @@ public final class FieldKind { @@ -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 待验证的参数值

54
common/common-online/src/main/java/apelet/common/online/service/OnlineFileService.java

@ -0,0 +1,54 @@ @@ -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<OnlineFile> 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<OnlineFile> getOnlineFileList(Long onlineTableId, String fieldName, Long businessId);
/**
* 批量获取多个业务记录的文件数据按业务Id分组返回
*
* @param onlineTableId 在线表Id(zz_online_table.table_id)
* @param fieldName 字段名称
* @param businessIdList 业务Id列表
* @return key为businessIdvalue为该业务的文件数据列表
*/
Map<Long, List<OnlineFile>> getOnlineFileMapByBusinessIds(
Long onlineTableId, String fieldName, List<Long> businessIdList);
}

9
common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java

@ -313,4 +313,13 @@ public interface OnlineOperationService { @@ -313,4 +313,13 @@ public interface OnlineOperationService {
List<JSONObject> collectionToJson(ObjectCollection collection, List<Map<String, Object>> resultList);
/**
* 查询结果回填将附件上传/图片上传字段(field_kind=1/2)对应的 zz_online_file 数据
* 组装成文件JSON数组回填到查询结果的对应字段上
*
* @param resultList 查询结果列表会被原地修改
* @param table 结果列表所属的在线数据表主表或分录表
*/
void fillFileColumn(List<Map<String, Object>> resultList, OnlineTable table);
}

93
common/common-online/src/main/java/apelet/common/online/service/impl/OnlineFileServiceImpl.java

@ -0,0 +1,93 @@ @@ -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<OnlineFile> 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<OnlineFile> 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<Long, List<OnlineFile>> getOnlineFileMapByBusinessIds(Long onlineTableId, String fieldName, List<Long> businessIdList) {
if (CollUtil.isEmpty(businessIdList)) {
return new HashMap<>();
}
List<OnlineFile> fileList = onlineFileMapper.selectList(new QueryWrapper<OnlineFile>()
.eq("online_table_id", onlineTableId)
.eq("field_name", fieldName)
.in("business_id", businessIdList));
return fileList.stream().collect(Collectors.groupingBy(OnlineFile::getBusinessId));
}
}

544
common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java

@ -1,63 +1,65 @@ @@ -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; @@ -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 { @@ -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 { @@ -197,6 +198,8 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
}
}
}
this.saveFileColumn(table, id, data);
this.nullifyFileColumnValues(columnDataList, columnValueList);
this.doInsert(table, columnNames, columnValueList);
// List<String> list = Arrays.asList(columnNames.split(","));
// ObjectValue objectValue = new ObjectValue(table.getTableName());
@ -245,6 +248,8 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { @@ -245,6 +248,8 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
}
}
// this.doInsert(table, columnNames, columnValueList);
this.saveFileColumn(table, id, data);
this.nullifyFileColumnValues(columnDataList, columnValueList);
List<String> 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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -494,11 +511,27 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
}
List<OnlineFilterDto> slaveFilterList =
this.makeDefaultFilter(relation.getSlaveTable(), slaveColumn, columnValue);
// 先查出从表受影响记录的主键,用于附件清理。
List<Long> slaveBusinessIdList = new LinkedList<>();
OnlineColumn slavePkColumn = slaveTable.getPrimaryKeyColumn();
if (slavePkColumn != null) {
List<Map<String, Object>> 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 { @@ -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 { @@ -575,6 +609,7 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
slaveTable, null, selectFields, filterList, dataPermFilter, null, null);
List<Map<String, Object>> 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 { @@ -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 { @@ -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 { @@ -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<PendingFileColumn> pendingFileList = new LinkedList<>();
if (onlFormHead.getIsomerismMark() != null) {
finalObjectValue = this.cntEasValue(onlinePluginExecuteDto,
masterData,
@ -1031,12 +1073,294 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { @@ -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<OnlinePluginExecuteSortDto> list
= sortDtoList.stream().sorted(Comparator.comparing(OnlinePluginExecuteSortDto::getOrder)).collect(Collectors.toList());
return exePlugin(list, onlinePluginExecuteDto.getBillId(), onlineTable.getTableName(), finalObjectValue, onlineForm);
// 删除会被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;
}
/**
* 判断当前按钮绑定的插件中是否包含指定插件
* <p>按钮与插件的绑定是 widgetJson pluginList[].class 的全限定类名( QuickFormWidgetUtil#setSaveButtonEnabled)
* 用类名判断比用中文 buttonName 可靠</p>
*/
private boolean containsPlugin(List<OnlinePluginExecuteSortDto> sortDtoList, Class<?> pluginClass) {
if (CollUtil.isEmpty(sortDtoList)) {
return false;
}
return sortDtoList.stream().anyMatch(dto -> pluginClass.getName().equals(dto.getClassPath()));
}
/**
* 删除前收集待清理附件的业务主键
* <p>表头主键取自billId分录主键必须在删除前查出因为ORM删除表头时会级联删除分录行
* 删除后再查就拿不到附件会在 zz_online_file 里变成孤儿记录</p>
*
* @param masterTable 表头对应的在线数据表
* @param billId 表头主键批量删除时为逗号分隔的多个主键
* @return 在线表tableId -> 该表下待清理附件的业务主键列表
*/
private Map<Long, List<Long>> collectFileBusinessIdForRemove(OnlineTable masterTable, String billId) {
Map<Long, List<Long>> businessIdMap = new LinkedHashMap<>();
if (masterTable == null || StrUtil.isBlank(billId)) {
return businessIdMap;
}
List<Long> 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<Long> masterIdList, Map<Long, List<Long>> businessIdMap) {
EntityTable entityTable = ormGenDataSourceUtil.getEntityTable(masterTable.getTableName());
if (entityTable == null || CollUtil.isEmpty(entityTable.getEntityColumnList())) {
return;
}
// 分录表名 -> 在线表tableId,用于把查询结果里的分录行归到对应的在线表。
Map<String, Long> 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<OnlineTable>().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<String, Object> 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<Long, List<Long>> businessIdMap) {
if (MapUtil.isEmpty(businessIdMap)) {
return;
}
businessIdMap.forEach((onlineTableId, businessIdList) -> {
for (Long businessId : businessIdList) {
onlineFileService.removeByBusinessId(onlineTableId, businessId);
}
});
}
/**
* 附件字段暂存对象
* <p>持有表头/分录行的ObjectValue实例引用新增时主键由ORM在保存过程中生成并回写到该实例上
* 保存前拿不到主键只能保存后按实例读回</p>
*/
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
* <p>按全量覆盖语义表中所有 field_kind 1/2 的字段一律以入参为准入参未包含该字段视为清空</p>
*
* @param objectValue 待保存的表头对象分录以分录表名为key挂在其上
* @param masterTable 表头对应的在线数据表
* @param slaveData 分录入参key为分录表的tableId
* @return 待落库的附件字段列表
*/
private List<PendingFileColumn> extractFileColumn(ObjectValue objectValue, OnlineTable masterTable, JSONObject slaveData) {
List<PendingFileColumn> 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<PendingFileColumn> 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<PendingFileColumn> 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<OnlineFile> fileList = this.parseOnlineFileList(pending.fileJson, pending.columnName);
onlineFileService.replaceFileList(pending.table.getTableId(), pending.columnName, businessId, fileList);
}
}
/**
* 把文件字段的JSON数组字符串解析为 OnlineFile 列表
* <p>值为空或解析失败时返回空列表即按清空该字段附件处理</p>
*/
private List<OnlineFile> parseOnlineFileList(String fileJson, String columnName) {
List<OnlineFile> 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 { @@ -2203,6 +2527,152 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
return sb.substring(0, sb.length() - 1);
}
/**
* 处理附件上传/图片上传字段将入参中的文件JSON数组解析后存入 zz_online_file并把业务字段置空
* <p>约定入参未包含该字段或值为null时保留原附件数据空数组时清空该字段的全部附件</p>
*
* @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<OnlineFile> 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<Map<String, Object>> resultList, OnlineTable table) {
if (CollUtil.isEmpty(resultList)) {
return;
}
List<OnlineColumn> 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<Long> 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<Long, List<OnlineFile>> fileMap =
onlineFileService.getOnlineFileMapByBusinessIds(table.getTableId(), column.getColumnName(), businessIdList);
if (MapUtil.isEmpty(fileMap)) {
continue;
}
for (Map<String, Object> result : resultList) {
Object idValue = result.get(primaryKeyColumn.getColumnName());
if (ObjectUtil.isEmpty(idValue)) {
continue;
}
Long businessId = this.convertToLongId(idValue.toString());
List<OnlineFile> 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写入业务表
* <p>columnValueList与columnDataList对齐(均跳过自增列)</p>
*/
private void nullifyFileColumnValues(List<ColumnData> columnDataList, List<Object> 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;

29
common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodeMapper.java

@ -0,0 +1,29 @@ @@ -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;
/**
* 权限字数据访问操作接口
* <p>在线表单发布自动生成权限字使用精简自 tenant-admin 的同名 Mapper为避免与 tenant-admin 同名 Mapper
* bean 名冲突采用 Online 前缀命名</p>
*
* @author guifc
* @date 2023-08-04
*/
public interface OnlineSysPermCodeMapper extends BaseDaoMapper<SysPermCode> {
/**
* 按主键Id集合物理删除权限字
* <p>xy_sys_perm_code 上有 (perm_code, deleted_flag) 唯一索引逻辑删除只是把 deleted_flag 置为 -1
* 同一表单重复发布时第二次清理会与已存在的 -1 行冲突报 Duplicate entry发布时自动生成的权限字属于派生数据
* 无需保留删除痕迹因此走物理删除自定义 SQL 不受实体上 {@code @TableLogic} 的影响</p>
*
* @param permCodeIdList 权限字主键Id集合不能为空
* @return 删除行数
*/
int physicalDeleteByIdList(@Param("permCodeIdList") Collection<Long> permCodeIdList);
}

5
common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodePermMapper.java → common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermCodePermMapper.java

@ -5,10 +5,11 @@ import apelet.common.online.upms.model.SysPermCodePerm; @@ -5,10 +5,11 @@ import apelet.common.online.upms.model.SysPermCodePerm;
/**
* 权限字与权限资源关系数据访问操作接口
* <p>在线表单发布自动生成权限字使用精简自 tenant-admin 的同名 Mapper</p>
* <p>在线表单发布自动生成权限字使用精简自 tenant-admin 的同名 Mapper为避免与 tenant-admin 同名 Mapper
* bean 名冲突采用 Online 前缀命名</p>
*
* @author guifc
* @date 2023-08-04
*/
public interface SysPermCodePermMapper extends BaseDaoMapper<SysPermCodePerm> {
public interface OnlineSysPermCodePermMapper extends BaseDaoMapper<SysPermCodePerm> {
}

27
common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermMapper.java

@ -0,0 +1,27 @@ @@ -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;
/**
* 权限资源数据访问操作接口
* <p>在线表单发布自动生成权限字使用精简自 tenant-admin 的同名 Mapper为避免与 tenant-admin 同名 Mapper
* bean 名冲突采用 Online 前缀命名</p>
*
* @author guifc
* @date 2023-08-04
*/
public interface OnlineSysPermMapper extends BaseDaoMapper<SysPerm> {
/**
* 按主键Id集合物理删除权限资源
* <p>与权限字保持一致发布时自动生成的权限资源属于派生数据逻辑删除只会不断堆积 deleted_flag = -1 的垃圾行</p>
*
* @param permIdList 权限资源主键Id集合不能为空
* @return 删除行数
*/
int physicalDeleteByIdList(@Param("permIdList") Collection<Long> permIdList);
}

27
common/common-online/src/main/java/apelet/common/online/upms/dao/OnlineSysPermModuleMapper.java

@ -0,0 +1,27 @@ @@ -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;
/**
* 权限模块数据访问操作接口
* <p>在线表单发布自动生成权限字使用精简自 tenant-admin 的同名 Mapper为避免与 tenant-admin 同名 Mapper
* bean 名冲突采用 Online 前缀命名</p>
*
* @author guifc
* @date 2023-08-04
*/
public interface OnlineSysPermModuleMapper extends BaseDaoMapper<SysPermModule> {
/**
* 按主键Id集合物理删除权限模块
* <p>与权限字保持一致发布时自动生成的权限模块属于派生数据逻辑删除只会不断堆积 deleted_flag = -1 的垃圾行</p>
*
* @param moduleIdList 权限模块主键Id集合不能为空
* @return 删除行数
*/
int physicalDeleteByIdList(@Param("moduleIdList") Collection<Long> moduleIdList);
}

14
common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermCodeMapper.java

@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
package apelet.common.online.upms.dao;
import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.online.upms.model.SysPermCode;
/**
* 权限字数据访问操作接口
* <p>在线表单发布自动生成权限字使用精简自 tenant-admin 的同名 Mapper</p>
*
* @author guifc
* @date 2023-08-04
*/
public interface SysPermCodeMapper extends BaseDaoMapper<SysPermCode> {
}

14
common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermMapper.java

@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
package apelet.common.online.upms.dao;
import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.online.upms.model.SysPerm;
/**
* 权限资源数据访问操作接口
* <p>在线表单发布自动生成权限字使用精简自 tenant-admin 的同名 Mapper</p>
*
* @author guifc
* @date 2023-08-04
*/
public interface SysPermMapper extends BaseDaoMapper<SysPerm> {
}

14
common/common-online/src/main/java/apelet/common/online/upms/dao/SysPermModuleMapper.java

@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
package apelet.common.online.upms.dao;
import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.online.upms.model.SysPermModule;
/**
* 权限模块数据访问操作接口
* <p>在线表单发布自动生成权限字使用精简自 tenant-admin 的同名 Mapper</p>
*
* @author guifc
* @date 2023-08-04
*/
public interface SysPermModuleMapper extends BaseDaoMapper<SysPermModule> {
}

122
common/common-online/src/main/java/apelet/common/online/upms/service/impl/OnlinePermCodeServiceImpl.java

@ -11,10 +11,10 @@ import apelet.common.online.dao.OnlinePageMapper; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -196,8 +223,7 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService {
if (StrUtil.isBlank(pageCode)) {
return;
}
SysPermCode root = sysPermCodeMapper.selectOne(new QueryWrapper<SysPermCode>()
.eq("perm_code", pageCode).eq("perm_code_type", 0));
SysPermCode root = onlineSysPermCodeMapper.selectOne(new QueryWrapper<SysPermCode>().eq("perm_code", pageCode).eq("perm_code_type", 0));
if (root == null) {
return;
}
@ -207,33 +233,31 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { @@ -207,33 +233,31 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService {
Set<Long> codeIdSet = new HashSet<>(codeIdList);
List<Long> parentIdList = new ArrayList<>(codeIdList);
while (CollUtil.isNotEmpty(parentIdList)) {
List<SysPermCode> childList = sysPermCodeMapper.selectList(
new QueryWrapper<SysPermCode>().in("parent_id", parentIdList));
List<SysPermCode> childList = onlineSysPermCodeMapper.selectList(new QueryWrapper<SysPermCode>().in("parent_id", parentIdList));
parentIdList = childList.stream().map(SysPermCode::getPermCodeId).collect(Collectors.toList());
codeIdSet.addAll(parentIdList);
}
// 权限字与权限关联、权限资源及其模块。
List<SysPermCodePerm> relationList = sysPermCodePermMapper.selectList(
new QueryWrapper<SysPermCodePerm>().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<SysPermCodePerm> relationList = onlineSysPermCodePermMapper.selectList(new QueryWrapper<SysPermCodePerm>().in("perm_code_id", codeIdSet));
if (CollUtil.isNotEmpty(relationList)) {
sysPermCodePermMapper.delete(new QueryWrapper<SysPermCodePerm>().in("perm_code_id", codeIdSet));
onlineSysPermCodePermMapper.delete(new QueryWrapper<SysPermCodePerm>().in("perm_code_id", codeIdSet));
}
Set<Long> permIdSet = relationList.stream().map(SysPermCodePerm::getPermId).collect(Collectors.toSet());
Set<Long> moduleIdSet = new HashSet<>();
if (CollUtil.isNotEmpty(permIdSet)) {
List<SysPerm> permList = sysPermMapper.selectList(new QueryWrapper<SysPerm>().in("perm_id", permIdSet));
List<SysPerm> permList = onlineSysPermMapper.selectList(new QueryWrapper<SysPerm>().in("perm_id", permIdSet));
moduleIdSet = permList.stream().map(SysPerm::getModuleId).collect(Collectors.toSet());
sysPermMapper.delete(new QueryWrapper<SysPerm>().in("perm_id", permIdSet));
onlineSysPermMapper.physicalDeleteByIdList(permIdSet);
}
sysPermCodeMapper.delete(new QueryWrapper<SysPermCode>().in("perm_code_id", codeIdSet));
onlineSysPermCodeMapper.physicalDeleteByIdList(codeIdSet);
// 删除权限模块(子模块及其上级根模块)。
if (CollUtil.isNotEmpty(moduleIdSet)) {
List<SysPermModule> moduleList = sysPermModuleMapper.selectList(
new QueryWrapper<SysPermModule>().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<SysPermModule> moduleList = onlineSysPermModuleMapper.selectList(new QueryWrapper<SysPermModule>().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<SysPermModule>().in("module_id", moduleIdSet));
onlineSysPermModuleMapper.physicalDeleteByIdList(moduleIdSet);
}
}
log.info("在线表单删除清理权限字完成,pageCode={}, 权限字数={}", pageCode, codeIdSet.size());
@ -300,6 +324,6 @@ public class OnlinePermCodeServiceImpl implements OnlinePermCodeService { @@ -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);
}
}

3
common/common-online/src/main/java/apelet/common/online/util/OnlineOperationHelper.java

@ -167,7 +167,8 @@ public class OnlineOperationHelper { @@ -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);

Loading…
Cancel
Save