實作效果視訊如下
視訊加載中...
此前,我已在架構内對檔案做了簡要的管理。在日常的使用中,我發現此前的設計還是沒有達到想要的效果,
故,欲對檔案管理體系做一次調整,進而達到配置化檔案功能與業務關聯。
此前,我已設計過檔案管理的表結構,此下,簡述一下,此前設計思路。
可見,以上存儲的字段,基本都是檔案常見字段。
除此之外,在其新增了biz_type、biz_key兩個字段,此兩個字段,用于檔案和業務之前的關聯使用。
進而,可以使檔案體系,脫離業務,又與業務完全綁定在一起。
本次調整,意在修改檔案上傳元件,使其徹底和業務脫離關聯。
開發思路
後端需要提供兩個接口,上傳和擷取檔案清單接口。
上傳接口需要支援多檔案傳輸,并接入bizKey和bieType。
不同的檔案元件可以通過bizKey和bieType擷取業務對應的檔案資料。
後端還需内置定義兩個參數,saveFileIds,deleteFileIds用于對業務與檔案之間的動态綁定。
我準備在新增和更新時,在架構内部進行自動綁定檔案主鍵,故此下,分兩種邏輯梳理開發思路。
新增
對于此種邏輯,直接綁定主鍵即可。但,我欲在架構内部自動綁定業務主鍵(bizKey)。
當此種情況下,有種情況不可避免,當一個處理邏輯内部有多個新增邏輯時,即會産生多個新增業務主鍵,那麼,以那個業務主鍵為準,又該綁定那個新增業務主鍵?
若,通過業務主鍵編碼進行記錄的話,勢必需要再次記錄一個編碼字段,反而造成了複雜性。
故,我欲取第一個新增主鍵為主,其他主鍵不做操作。在此基礎上,新增擴充函數進行攔截處理,相容特殊業務邏輯即可。
更新
對于此種邏輯,我需相容兩種處理邏輯。
1、已傳入bizKey,直接綁定更新檔案資料。
2、未傳入bizKey時,我會在檔案元件中定義bizKeyCode(業務編碼),進行自動綁定值。
參數傳遞
由于該邏輯需要統一處理,故需定義統一參數傳遞邏輯。
我計劃在header中新增key:值為fileContent,傳遞儲存、删除等檔案資料資訊,同時通過加密為字元串傳遞,後端在需要的地方統一處理功能邏輯。
擷取檔案清單
此前,也提過徹底拆解與業務之間的關聯。故,需封裝擷取檔案清單接口,用來滿足業務元件内容的資料回顯,與檔案編輯功能。
寫止于此,開發思路業已明晰,此下,将正式實作具體功能。為保證代碼清晰,特将前後端分開進行表述。
後端
1、新增業務主鍵入線程同步功能
由于,需要徹底脫離具體的業務邏輯,我欲在此前實作的線程同步中,存儲“新增業務主鍵”進行資料同步。因,功能與api請求均需要用到此邏輯,我計劃在“RequestAttributes”中新增屬性“insertBizKey”用來存儲新增後産生的業務主鍵,後期若有其他業務需求,再做更多設計。
/**
* 插入腳本業務主鍵
*/
private String insertBizKey;
2、統一插入入口設定業務主鍵入線程同步
此上,已定義了臨時屬性,此下将在“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;
}
3、前後端資料傳輸格式與對象
為相容多個元件的情況,我将未加密的傳輸資料定義為以下格式。
[
{
"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;
}
}
4、處理新增、更新業務主鍵綁定
/**
* 更新檔案業務資料
*
* @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;
}
5、提供查詢檔案清單接口
此接口,由于是給元件提供使用,我需要定義必須傳入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 {
}
至此後端部分功能就開發完成了,接下來,就對前端功能進行改造。
前端
1、檔案工廠
在與後端進行互動過程中,勢必要對所需傳遞的參數進行綁定。
為避免代碼備援,故有此封裝。此工廠,需實作對新增、删除檔案主鍵記錄,并在接口需要傳遞時,傳回加密處理後的資料。
/**
* 檔案工廠類
*
* @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;
}
}
2、元素實作動态綁定
本篇,将完善我此前定義的拖拽上傳元素。
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);
至此,業已實作檔案動态綁定、拖拽上傳元素等功能。此後,隻需對各個所需的上傳功能做前端元素封裝即可。