此前,我已在框架内对文件做了简要的管理。在日常的使用中,我发现此前的设计还是没有达到想要的效果,
故,欲对文件管理体系做一次调整,从而达到配置化文件功能与业务关联。
此前,我已设计过文件管理的表结构,此下,简述一下,此前设计思路。
可见,以上存储的字段,基本都是文件常见字段。
除此之外,在其新增了biz_type、biz_key两个字段,此两个字段,用于文件和业务之前的关联使用。
从而,可以使文件体系,脱离业务,又与业务完全绑定在一起。
本次调整,意在修改文件上传组件,使其彻底和业务脱离关联。
开发思路
后端需要提供两个接口,上传和获取文件列表接口。
上传接口需要支持多文件传输,并接入bizKey和bieType。
不同的文件组件可以通过bizKey和bieType获取业务对应的文件数据。
后端还需内置定义两个参数,saveFileIds,deleteFileIds用于对业务与文件之间的动态绑定。
我准备在新增和更新时,在框架内部进行自动绑定文件主键,故此下,分两种逻辑梳理开发思路。
新增
对于此种逻辑,直接绑定主键即可。但,我欲在框架内部自动绑定业务主键(bizKey)。
当此种情况下,有种情况不可避免,当一个处理逻辑内部有多个新增逻辑时,即会产生多个新增业务主键,那么,以那个业务主键为准,又该绑定那个新增业务主键?
若,通过业务主键编码进行记录的话,势必需要再次记录一个编码字段,反而造成了复杂性。
故,我欲取第一个新增主键为主,其他主键不做操作。在此基础上,新增扩展函数进行拦截处理,兼容特殊业务逻辑即可。
更新
对于此种逻辑,我需兼容两种处理逻辑。
1、已传入bizKey,直接绑定更新文件数据。
2、未传入bizKey时,我会在文件组件中定义bizKeyCode(业务编码),进行自动绑定值。
参数传递
由于该逻辑需要统一处理,故需定义统一参数传递逻辑。
我计划在header中新增key:值为fileContent,传递保存、删除等文件数据信息,同时通过加密为字符串传递,后端在需要的地方统一处理功能逻辑。
获取文件列表
此前,也提过彻底拆解与业务之间的关联。故,需封装获取文件列表接口,用来满足业务组件内容的数据回显,与文件编辑功能。
写止于此,开发思路业已明晰,此下,将正式实现具体功能。为保证代码清晰,特将前后端分开进行表述。
后端
一、新增业务主键入线程同步功能
由于,需要彻底脱离具体的业务逻辑,我欲在此前实现的线程同步中,存储“新增业务主键”进行数据同步。因,功能与api请求均需要用到此逻辑,我计划在“RequestAttributes”中新增属性“insertBizKey”用来存储新增后产生的业务主键,后期若有其他业务需求,再做更多设计。
/**
* 插入脚本业务主键
*/
private String insertBizKey;
二、统一插入入口设置业务主键入线程同步
此上,已定义了临时属性,此下将在“ExecuteSqlFactory”执行请求工厂中,对数据进行临时绑定。
/**
* 执行新增Sql语句
*
* @param requestInfo
* @param requestParam
* @param <T>
* @return
*/
private <T extends Object> T execInsertSql(ISqlExecutor executor, ISqlRequestMessage requestInfo, RequestParam requestParam) throws Exception {
Object result = null;
if (requestParam != null && requestParam.isEmpty()) {
// 转换插入sql
SqlResultInfo resultInfo = SqlParseUtil.convertInsertSql(requestInfo, requestParam);
if (resultInfo != null) {
GeneratedKeyInfo keyInfo = executor.executeId(builderSql(resultInfo.getSql()), resultInfo.getValues());
// 判断是否有主键Key值
if (StringUtils.isNotEmpty(resultInfo.getPrimaryKeyValue())) {
result = resultInfo.getPrimaryKeyValue();
} else {
result = keyInfo.getResult();
}
} else {
throw new ResponseException(ResponseResult.NOT_INSERT_DATA_EXECUTOR);
}
}
// 将主键存入线程同步中
if (result != null) {
String insertBizKey = RequestContextHolder.getAttributes().getInsertBizKey();
if (StringUtils.isEmpty(insertBizKey)) {
RequestContextHolder.getAttributes().setInsertBizKey(insertBizKey);
}
}
return (T) result;
}
三、前后端数据传输格式与对象
为兼容多个组件的情况,我将未加密的传输数据定义为以下格式。
[
{
"bizKey": "",
"bizKeyCode": "",
"saveFileIds": "",
"deleteFileIds": ""
}
]
package com.threeox.biz.file.entity;
import com.threeox.drivenlibrary.entity.base.BaseObject;
/**
* 文件请求参数
*
* @author 赵屈犇
* @version 1.0
* @date 创建时间: 2022/9/23 20:31
* @Copyright(C): 2022 by 赵屈犇
*/
public class FileParamInfo extends BaseObject {
/**
* 业务主键
*/
private String bizKey;
/**
* 业务主键编码
*/
private String bizKeyCode;
/**
* 保存文件ids,以,号分割
*/
private String saveFileIds;
/**
* 删除文件ids,以,号分割
*/
private String deleteFileIds;
public String getBizKey() {
return bizKey;
}
public void setBizKey(String bizKey) {
this.bizKey = bizKey;
}
public String getBizKeyCode() {
return bizKeyCode;
}
public void setBizKeyCode(String bizKeyCode) {
this.bizKeyCode = bizKeyCode;
}
public String getSaveFileIds() {
return saveFileIds;
}
public void setSaveFileIds(String saveFileIds) {
this.saveFileIds = saveFileIds;
}
public String getDeleteFileIds() {
return deleteFileIds;
}
public void setDeleteFileIds(String deleteFileIds) {
this.deleteFileIds = deleteFileIds;
}
}
四、处理新增、更新业务主键绑定
/**
* 更新文件业务数据
*
* @return a
* @author 赵屈犇
* date 创建时间: 2022/9/23 20:25
* @version 1.0
*/
public void updateFileBiz() throws Exception {
List<FileParamInfo> paramInfos = getParamInfo();
if (ArrayUtils.isNotEmpty(paramInfos)) {
UpdateBuilder updateBuilder = null;
try {
// 初始化执行器并开启事务 防止执行器不包含文件数据库
updateBuilder = UpdateBuilder.builder().executor(DrivenManageSqlExecutor.newInstance())
.beginTransaction();
for (FileParamInfo paramInfo: paramInfos) {
updateFileBiz(paramInfo, updateBuilder);
}
} catch (Exception e) {
if (updateBuilder != null) {
updateBuilder.rollback();
}
throw e;
} finally {
if (updateBuilder != null) {
updateBuilder.commit();
}
}
}
}
/**
* 更新文件业务
*
* @param paramInfo
* @param updateBuilder
* @return a
* @author 赵屈犇
* @date 创建时间: 2022/9/23 21:57
* @version 1.0
*/
private void updateFileBiz(FileParamInfo paramInfo, UpdateBuilder updateBuilder) throws Exception {
String bizKey = paramInfo.getBizKey();
if (StringUtils.isEmpty(bizKey)) {
// 获取主键编码
String bizKeyCode = paramInfo.getBizKeyCode();
RequestAttributes attributes = RequestContextHolder.getAttributes();
if (StringUtils.isNotEmpty(bizKeyCode)) {
if (EmptyUtils.isKeyNotEmpty(attributes.getParamsJSON(), bizKeyCode)) {
bizKey = attributes.getParamsJSON().getString(bizKeyCode);
}
}
// 如果为空,赋值插入业务主键
if (StringUtils.isEmpty(bizKey)) {
bizKey = attributes.getInsertBizKey();
}
}
// 不为空时,处理更新逻辑
if (StringUtils.isNotEmpty(bizKey)) {
// 处理新增文件
String saveFileIds = paramInfo.getSaveFileIds();
if (StringUtils.isNotEmpty(saveFileIds)) {
updateBuilder.table("ox_Sys_File").set("biz_key", bizKey).set(DefaultFieldConstants.DATA_STATE,
DataState.NORMAL.getValue())
.and("file_id", QueryType.IN, saveFileIds.split(",")).execute();
}
// 处理删除文件
String deleteFileIds = paramInfo.getDeleteFileIds();
if (StringUtils.isNotEmpty(deleteFileIds)) {
updateBuilder.table("ox_Sys_File").set("biz_key", bizKey).set(DefaultFieldConstants.DATA_STATE,
DataState.DELETE.getValue())
.and("file_id", QueryType.IN, deleteFileIds.split(",")).execute();
}
}
}
/**
* 获取文件请求参数对象
*
* @return a
* @author 赵屈犇
* @date 创建时间: 2022/9/23 20:58
* @version 1.0
*/
private List<FileParamInfo> getParamInfo() {
try {
// 获取文件请求参数
String fileContent = RequestContextHolder.getRequestHeader("fileContent");
if (StringUtils.isNotEmpty(fileContent)) {
fileContent = EncryptFactory.getInstance().decrypt(fileContent);
if (StringUtils.isNotEmpty(fileContent)) {
return JSON.parseArray(fileContent, FileParamInfo.class);
}
}
} catch (Exception e) {
loggerFactory.error("获取文件请求对象报错了", e);
}
return null;
}
五、提供查询文件列表接口
此接口,由于是给组件提供使用,我需要定义必须传入bizKey、bizType字段,然后,通过bizKey、bizType查询文件列表数据并返回。
package com.threeox.biz.file.api;
import com.alibaba.fastjson.JSONObject;
import com.threeox.dblibrary.annotation.create.Column;
import com.threeox.dblibrary.enums.ValueType;
import com.threeox.dblibrary.enums.WhereAndOr;
import com.threeox.drivenlibrary.engine.annotation.api.Api;
import com.threeox.drivenlibrary.engine.annotation.api.ApiVerifyConfig;
import com.threeox.drivenlibrary.engine.annotation.request.RequestConfig;
import com.threeox.drivenlibrary.engine.annotation.request.SqlRequestConfig;
import com.threeox.drivenlibrary.engine.config.constants.dictionary.ConfigDictionaryConstants;
import com.threeox.drivenlibrary.engine.constants.DefaultFieldConstants;
import com.threeox.drivenlibrary.engine.function.impl.AbstractApiExtend;
import com.threeox.drivenlibrary.manage.entity.FileMessage;
/**
* 获取文件列表接口
*
* @author 赵屈犇
* @version 1.0
* @date 创建时间: 2022/9/23 22:00
* @Copyright(C): 2022 by 赵屈犇
*/
@Api(apiUrl = "list", apiName = "获取文件列表接口", verifyConfigs = {
@ApiVerifyConfig(paramCode = "bizType", emptyHint = "请传入业务类型!"),
@ApiVerifyConfig(paramCode = "bizKey", emptyHint = "请传入业务编码!"),
}, isVerifyLogin = false, isVerifyToken = false, moduleUrl = "file/get", requestConfigs = {
@RequestConfig(requestName = "获取文件列表接口", requestCode = "getFileListByBiz", sqlConfig = @SqlRequestConfig(includeEntitys = FileMessage.class,
columns = {
@Column(columnName = "biz_type", whereValue = "bizType", isSelectWhereUse = true, isSelectUse = false, rightAndOr = WhereAndOr.AND),
@Column(columnName = "biz_key", whereValue = "bizKey", isSelectWhereUse = true, isSelectUse = false, rightAndOr = WhereAndOr.AND),
@Column(columnName = DefaultFieldConstants.DATA_STATE, whereValue = ConfigDictionaryConstants.DataStateDict.NORMAL, whereValueType = ValueType.CUSTOM_VALUE, isSelectWhereUse = true, isSelectUse = false)
}
), isTreeResult = true, seedKey = "value", parentKey = "pValue", childrenKey = "children", parameterClass = JSONObject.class)
})
public class GetFileListExtend extends AbstractApiExtend {
}
至此后端部分功能就开发完成了,接下来,就对前端功能进行改造。
前端
一、文件工厂
在与后端进行交互过程中,势必要对所需传递的参数进行绑定。
为避免代码冗余,故有此封装。此工厂,需实现对新增、删除文件主键记录,并在接口需要传递时,返回加密处理后的数据。
/**
* 文件工厂类
*
* @constructor
*/
let FileFactory = function (engine) {
let self = this;
// 临时存储文件内容
let tempFileContent = null;
/**
* 添加保存文件主键
*
* @param bizKeyCode
* @param bizKey
* @param saveFileId
*/
self.addSaveFileIds = function (bizKeyCode, bizKey, fileInfos) {
if (engineCommon.isListNotEmpty(fileInfos)) {
for (let i = 0, length = fileInfos.length; i < length; i++) {
self.addFileId(bizKeyCode, bizKey, fileInfos[0], 'saveFileIds');
}
}
}
/**
* 添加删除文件主键
*
* @param bizKeyCode
* @param bizKey
* @param fileInfo
*/
self.addDeleteFileIds = function (bizKeyCode, bizKey, fileInfos) {
if (engineCommon.isListNotEmpty(fileInfos)) {
for (let i = 0, length = fileInfos.length; i < length; i++) {
self.addFileId(bizKeyCode, bizKey, fileInfos[0], 'deleteFileIds')
}
};
}
/**
* 添加文件主键
*
* @param bizType
* @param fileInfo
* @param fileIdKey
*/
self.addFileId = function (bizKeyCode, bizKey, fileInfo, fileIdKey) {
if (fileInfo) {
// 初始化临时文件内容变量
if (self.tempFileContent == null) {
self.tempFileContent = new Object();
}
// 根据bizType获取配置
let bizContent = self.tempFileContent[bizKeyCode];
if (bizContent == null) {
bizContent = new Object();
}
// 设置bizKey
if (engineCommon.isNotEmpty(bizKey)) {
bizContent['bizKey'] = bizKey;
}
let fileIds = bizContent[fileIdKey];
if (fileIds == null) {
fileIds = new Array();
}
fileIds.push(fileInfo['fileId']);
bizContent[fileIdKey] = fileIds;
self.tempFileContent[bizKeyCode] = bizContent;
}
}
/**
* 获取文件内容配置
*/
self.getFileContent = function () {
if (self.tempFileContent) {
let fileParams = [];
for (let bizKeyCode in self.tempFileContent) {
let fileParam = {
"bizKeyCode": bizKeyCode
};
let bizContent = self.tempFileContent[bizKeyCode];
if (bizContent) {
if (engineCommon.isNotEmpty(bizContent['bizKey'])) {
fileParam['bizKey'] = bizContent['bizKey'];
}
let savFileIds = bizContent['saveFileIds'];
if (engineCommon.isListNotEmpty(savFileIds)) {
fileParam['saveFileIds'] = savFileIds.join(',');
}
let deleteFileIds = bizContent['deleteFileIds'];
if (engineCommon.isListNotEmpty(deleteFileIds)) {
fileParam['deleteFileIds'] = deleteFileIds.join(',');
}
}
fileParams.push(fileParam);
}
return EncryptUtils.encrypt(JSON.stringify(fileParams));
}
return null;
}
}
二、元素实现动态绑定
本篇,将完善我此前定义的拖拽上传元素。
let upload = layFactory.getLayOption("upload");
// 初始化上传对象
let render = new Object();
render.elem = self.factory.getElementId('${fieldCode}_upload_element');
render.url = RequestConstants.ENGINE_UPLOAD_FILES_PATH;
// 定义文件对象
let fileInfos = new Array();
// 获取业务主键
let bizKey = self.factory.getValueByKey('${fileBizKeyCode}');
if (engineCommon.isNotEmpty(bizKey)) {
// 获取文件列表数据
ApiRequest.getFileList({
params: {
bizKey: bizKey,
bizType: "${fileBizType}"
},
success: function (data, message, result) {
// 获取上传后的文件对象
fileInfos = data;
if (engineCommon.isListNotEmpty(data)) {
self.factory.byId('${fieldCode}_upload_show_view').removeClass('layui-hide').find('img').attr('src', fileInfos[0].accessUrl);
self.factory.byId("${fieldCode}").val(fileInfos[0].fileAccessPath);
}
}
});
}
render.before = function (obj) {
// 拦截layui上传,自己实现上传功能
let files = $('#${fieldCode}_ROOT_NODE .layui-upload-file').prop('files');
FileRequest.upload({
"files": files,
"bizType": "${fileBizType}",
success: function (data, message, result) {
// 保存历史的删除文件
self.factory.fileFactory.addDeleteFileIds('${fileBizKeyCode}', bizKey, fileInfos);
// 替换已删除的文件对象
fileInfos = data;
if (engineCommon.isListNotEmpty(fileInfos)) {
self.factory.byId('${fieldCode}_upload_show_view').removeClass('layui-hide').find('img').attr('src', fileInfos[0].accessUrl);
self.factory.byId("${fieldCode}").val(fileInfos[0].fileAccessPath);
}
self.factory.fileFactory.addSaveFileIds('${fileBizKeyCode}', bizKey, data);
}
});
return false;
}
if (engineCommon.isNotEmpty('${fileSize}')) {
render.size = '${fileSize}';
}
if (engineCommon.isNotEmpty('${fileSuffixes}')) {
render.exts = '${fileSuffixes}';
}
if (engineCommon.isNotEmpty('${fileType}')) {
render.accept = '${fileType}';
}
if ('${isShowFileInput}' === 'true') {
self.factory.byId("${fieldCode}").show();
} else {
self.factory.byId("${fieldCode}").hide();
}
upload.render(render);
至此,业已实现文件动态绑定、拖拽上传元素等功能。此后,只需对各个所需的上传功能做前端元素封装即可。