文件管理体系优化,升级文件管理功能

96 阅读7分钟

由于不能上传视频,跳转观看实现效果

此前,我已在框架内对文件做了简要的管理。在日常的使用中,我发现此前的设计还是没有达到想要的效果,

故,欲对文件管理体系做一次调整,从而达到配置化文件功能与业务关联。

此前,我已设计过文件管理的表结构,此下,简述一下,此前设计思路。

可见,以上存储的字段,基本都是文件常见字段。

除此之外,在其新增了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);

至此,业已实现文件动态绑定、拖拽上传元素等功能。此后,只需对各个所需的上传功能做前端元素封装即可。