1.存储结构设计##
1.1后端mongodb中存储结构
由于mongodb数据库中每条记录存储不得超过16M,所以不能以工程Id存储一条记录,否则工程中上传的图片超过16M之后数据库就崩溃。
到mongodb的另一个存储机制 ---- 无返回码. 在 < mongodb 权威指南> 一书中, 作者称之为离弦之箭. 什么意思呢, 就是mongodb的插入,删除等操作, 客户端向数据库发出请求之后, 是不需要等待数据库返回操作是否成功的返回结果. 这也是mongodb插入,更新等操作速度快的原因. 这就导致, 当单个文件超过16M之后, 程序并不会报错, 但此时, 数据已经无法插入数据库了.
所以我们确定为一张图片作为一个文档,文档中包括 的字段"_id", "image" : {"contenType" : "image/png", "description" , "content":{} , "originName"},"bqItemID" , "id" , "userID", "projectID"
其中"image"对应的也是一个文档,在后端可以理解为一个JSON对象(图片对象),content存储的为图片转化为二进制后的内容,"_id"为mongo自动生成的,"id"在后端生成,(UUID生成的sId),由于我们设计的图片上传功能是一条清单可上传多张图片,所以为每张图片生成一个Id后将其放到数组中(idArray.add(sID);)并返回给前端idArray。
1.1.1后端上传代码如下,
public String submitReferFile(MultipartFile[] files, String userID, String projectID, String bqItemID) {
JSONObject ret = new JSONObject();
try {
// 构造提交对象数组
JSONArray arrayItems = new JSONArray();
JSONArray idArray = new JSONArray();
for (int j = 0; j < files.length; j++) {
JSONObject item = new JSONObject();
item.put(EBQEditOtherConst.pfnUserID, userID);
item.put(EBQEditOtherConst.pfnProjectID, projectID);
item.put(EBQEditOtherConst.pfnBqItemID, bqItemID);
String sID = UUID.randomUUID().toString();
item.put(EBQEditOtherConst.pfnID, sID);
idArray.add(sID);
JSONObject image = new JSONObject();
image.put(EBQEditOtherConst.pfnDescription, files[j].getName());
image.put(EBQEditOtherConst.pfnOriginName, files[j].getOriginalFilename());
image.put(EBQEditOtherConst.pfnContentType, files[j].getContentType());
image.put(EBQEditOtherConst.pfnContent, new Binary(files[j].getBytes()));
item.put(EBQEditOtherConst.pfnImage, image);
arrayItems.add(item);
}
boolean bRet = mongoDao.insertMany(EBQEditOtherConst.ptnBqItemReferFile, arrayItems);
if(bRet){
return EBQApiRetBuilder.buildResult(EBQHttpStatus.OK, true, idArray,"提交成功!");
} else {
return EBQApiRetBuilder.buildResult(EBQHttpStatus.FORBIDDEN, false,"提交失败,请重试!!");
}
}
catch (IOException e) {
return EBQApiRetBuilder.buildResult(EBQHttpStatus.INTERNAL_SERVER_ERROR, false, "提交失败,请重试!");
}
}
上面代码注意图片转化为二进制的方式(new Binary)
image.put(EBQEditOtherConst.pfnContent, new Binary(files[j].getBytes()));
存储之后调试代码发现"content"字段对应的也是一个对象其中有两个属性“data”和“type”,data里面存储的是真正的图片二进制数据
1.1.2 后端返回图片二进制代码
本文将图片二进制放在HttpServletResponse的响应正文里
@ResponseBody
@GetMapping(value = "referFileData/{id}")
public void getReferFile(@PathVariable("id") String id, HttpServletRequest request, HttpServletResponse response) {
referFileService.getReferFile(response, id);
}
referFileService中的方法
@Override
public void getReferFile(HttpServletResponse response, String sID) {
Document document = mongoDao.findByID(EBQEditOtherConst.ptnBqItemReferFile, sID);
JSONObject item = JSONObject.parseObject(document.toJson());
JSONObject image = item.getJSONObject(EBQEditOtherConst.pfnImage);
JSONObject imageContent = image.getJSONObject(EBQEditOtherConst.pfnContent);
byte[] imageBytes = "".getBytes();
if (imageContent.containsKey(EBQEditOtherConst.pfnBianry)) {
imageBytes = imageContent.getBytes(EBQEditOtherConst.pfnBianry);
} else if (imageContent.containsKey(EBQEditOtherConst.pfnData)) {
imageBytes = imageContent.getBytes(EBQEditOtherConst.pfnData);
}
response.setContentType(image.getString(EBQEditOtherConst.pfnContentType)); //设置图片格式
boolean bRet = false;
try {
OutputStream out = response.getOutputStream(); // 打开输出流
out.write(imageBytes); //输出图片
out.flush(); //输出
out.close(); //关闭输出
}
catch (IOException e) {
}
}
1.2前端indexdb存储结构
由于本项目中一个前端工程对应indexdb数据库中一个对象仓库(project)的一条记录(indexdb学习请移步www.ruanyifeng.com/blog/2018/0… ,并且大部分数据保存在data中,所以我们将工程所属的图片存储在data里面的bqItem_referFile对应的数组中,数组中的每一个元素是一个json数组结构,每个json对象有两个属性,其中一个为Id,等于清单的的bqItemId,另外一个属性为values,其对应的是数组,数组元素为后端生成的图片sId(参见前面后端代码),下图为前端工程一条工程记录的结构图
2.el -upload 在前端的使用##
2.1上传与读取
<el-dialog custom-class="bqItemReferFile" :title="bqItemReferFile.description"
:visible.sync="dialogShowFlg" v-if="dialogShowFlg" :closeOnClickModal="false" v-drag>
<div class="dialog-center">
<p>注:图片支持jpg、jpeg、png格式;图片大小不能超过512KB;每条清单最多上传4张图片</p><br><br>
<el-upload
:multiple="multiple"
:limit="4"
:on-exceed="handleExceed"
:file-list="images"
action="http://127.0.0.1:9001/edit/other/referFileData/"
accept='image/jpg,image/jpeg,image/png'
list-type="picture-card"
:on-preview="handlePictureCardPreview"
:before-remove="onBeforeRemove"
:on-remove="handleRemove"
:auto-upload="true"
:before-upload="onBeforeUpload"
:http-request="uploadFile"
:disabled="fbfxItem.Locked"
v-bind:class="{ 'upload-hide': curReferFile.value.length >= 4 || fbfxItem.Locked}"
ref="upload">
<i class="el-icon-plus"></i>
</el-upload>
</div>
<div slot="footer" class="dialog-footer">
<el-button size="large" @click="closeDialog">取消</el-button>
</div>
</el-dialog>
<el-dialog :visible.sync="dialogVisible" v-drag>
<img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
:file-list="images"其中的images 里面可以存储图片的name和URL 信息(本项目使用时直接将后端生成的sId作为name对应的信息),所以我们选择该属性主动操作images数组
:http-request="uploadFile",我们在uploadFile方法中写上传代码如下
uploadFile(file) {
let vueObj = this;
vueObj.formData = new FormData();
vueObj.formData.append('file', file.file);
vueObj.formData.append('userID', vueObj.userID);
vueObj.formData.append('projectID', vueObj.projectID);
vueObj.formData.append('bqItemID', vueObj.bqItemID);
let image = {};
vueObj.images.push(image);//由于下面操作是异步的所以在此先装入空的image,否则上传图片会抖动
request().submitReferFile(vueObj.formData, function(retData){//上传到后端
let ret = JSON.parse(retData);
ret.data.forEach(pictureId => {
image.name = pictureId;
image.url = process.env.API_EDIT_ROOT + 'other/referFileData/' + pictureId;
});
vueObj.$message(ret.message);
vueObj.submitFileIDToProject(ret.data);//保存到前端工程
});
},
请注意如下两行代码(预防图片抖动):
let image = {}; vueObj.images.push(image);//由于下面操作是异步的所以在此先装入空的image,否则上传图片会抖动 保存到前端工程的方法如下:
submitFileIDToProject: function(imageIdList) {
let vueObj = this;
if (vueObj.curProject.data == null) {
return;
}
if (vueObj.curProject.data.bqItem_referFile == undefined) {
vueObj.curProject.data.bqItem_referFile = [];
}
let bqItemData = vueObj.curProject.data.bqItem_referFile.find(p => p.id == vueObj.bqItemID);
if (bqItemData == null) {
let bqItem = {};
bqItem.id = vueObj.bqItemID;
bqItem.value = imageIdList;
vueObj.curProject.data.bqItem_referFile.push(bqItem);
vueObj.curReferFile = bqItem;
} else {
bqItemData.value = bqItemData.value.concat(imageIdList);
}
db().save();//保存到前端数据库
},
后端生成的imageId(sId)保存在了前端工程数据库中,以后点击清单直接从前端数据库中读取image的Id信息就可以了,(当然工程第一次所有前端工程里面的内容(data)也是从对应的后端数据库中取出来的。)详情见如下:
created: function() {
let vueObj = this;
vueObj.$on('show-dialog',function(record, onSuccess){
vueObj.onSuccess = onSuccess;
vueObj.fbfxItem = record;
vueObj.bqItemID = vueObj.fbfxItem.id;
vueObj.projectID = db().getProjectID();
vueObj.Locked = record.Locked;
vueObj.images = [];
vueObj.curProject = db().getCurProject();
vueObj.init();
vueObj.dialogShowFlg = true;
});
},
主要是在init方法里面加载图片Id(用图片的Id构造每张图片唯一的URL)
init: function() {
let vueObj = this;
vueObj.curReferFile= {value: []};
if (vueObj.curProject == null || vueObj.curProject.data.bqItem_referFile == undefined) {
return;
}
let bqItemData = vueObj.curProject.data.bqItem_referFile.find(p => p.id == vueObj.bqItemID);
if (bqItemData == undefined) {
return;
}
vueObj.curReferFile = bqItemData;
if (bqItemData.value) {
bqItemData.value.forEach(pictureId => {
let image = {name : pictureId, url : process.env.API_EDIT_ROOT + 'other/referFileData/' + pictureId};
vueObj.images.push(image);
});
}
},
2.2 el-upload钩子函数
这个控件最坑的是在onBeforeUpload钩子函数返回false后会进入onBeforeRemove,而onBeforeRemove返回true 后竟然会进入handleRemove钩子删除images数组里面的元素造成显示图片无故消失。所以我们在删除钩子做了一个判断如下
handleRemove(file, fileList) {
let vueObj = this;
if(vueObj.images.indexOf(file) < 0){//不做此判断会无故删除images元素
return;
}
if(!vueObj.Locked) {
let param = {'ids' : [file.name]};
let oldLength = vueObj.images.length;
vueObj.images = vueObj.images.filter(item => item.name != file.name);
request().deleteReferFile(param, function() {
vueObj.deleteFileIDFromProject(file.name);
});
}
},
2.3 el-upload图片显示问题
图片上的预览符号变成了方块,需要主动在css文件加上
.el-icon-zoom-in:before{ content: "\e62a"; }