前言
图片上传和预览在移动端应用非常广泛和频繁,vant组件库van-uploader组件已经帮我们实现了大部分功能,但是在系统中频繁使用还是有点麻烦,我们根据自身的业务系统重新封装了一下简化我们的开发。后端使用springboot集成jcifs实现文件管理微服务。
附件上传
附件预览
前端组件
组件介绍
前端只要在视图中使用组件,传入需要的参数即可
businessid | 业务id,用于把业务单号和附件关联 |
tmp_id | 临时业务id,在一开始业务单号未产生的时候和附件关联 |
name | 用于把不通类型的附件归类到不同的文件夹 |
businesstype | 用于区分同一个业务单号下不通类型的附件组 |
readonly | 判断组件是预览还是上传 |
<bdog-uploader-image
id="visit_uploader"
businessid="{{id}}"
tmp_id="{{tmp_id}}"
name="customerVisit"
businesstype="visit"
readonly ="{{readonly}}"
/>
组件js部分代码
const util = require('../../utils/util.js')
var request = require('../../utils/request.js')
var { config } = require('../../utils/config.js')
import Toast from '@vant/weapp/toast/toast';
const app = getApp();
Component({
properties: {
businessid: {
type: String
},
tmp_id: {
type: String
},
name: {
type: String
},
businesstype: {
type: String
},
readonly:{
type:Boolean
}
},
data: {
fileList: [],
},
attached:function(){
//this.getFileList()
},
methods: {
afterRead(event) {
Toast.loading({
duration: 0, // 持续展示 toast
forbidClick: true,
message: "上传中"
})
var that = this
const { file } = event.detail
wx.uploadFile({
url: config.baseUrl +'/MpAttachment/uploadFile',
filePath: file.url,
name: 'file',
header: {
"Content-Type": "multipart/form-data",
"id":that.data.businessid,
"tmpId":that.data.tmp_id,
"name":that.data.name,
"businesstype":that.data.businesstype,
"token":app.globalData.userInfo.token,
},
success(res) {
const data = JSON.parse(res.data)
if(data.code == 200){
// 上传完成需要更新 fileList
const { fileList = [] } = that.data;
const url = config.baseUrl +'/MpAttachment/getImage?id=' + data.data.id
fileList.push({ ...file, url: url, id: data.data.id })
that.setData({ fileList })
Toast.clear();
Toast({ type: 'success',message: '上传成功',duration:500, })
}else{
Toast({ type: 'fail',message: '上传失败',duration:500, })
}
},
fail:function(res){
Toast({ type: 'fail',message: '上传失败',duration:500, })
}
});
},
delete(event) {
Toast.loading({
duration: 0, // 持续展示 toast
forbidClick: true,
message: "删除中"
})
var that = this
var data = {}
data['id'] = event.detail.file.id
request.get('/MpAttachment/delete',data)
.then(function (res) {
if(res.code == 200){
const { fileList } = that.data;
const newFileList = fileList.filter((items) =>{
return items.id != event.detail.file.id
})
that.setData({ fileList : newFileList, })
Toast.clear();
Toast({ type: 'success',message: '删除成功',duration:500, })
}else{
Toast({ type: 'fail',message: '删除失败',duration:500, })
}
}, function (error) {
Toast({ type: 'fail',message: '删除失败',duration:500, })
})
},
getFileList() {
var that = this
var data = {}
data['businessid'] = that.data.businessid
data['businesstype'] = that.data.businesstype
request.get('/MpAttachment/getList',data)
.then(function (res) {
if(res.code == 200){
const fileList = res.data;
fileList.forEach(function(items){
items.url = config.baseUrl + '/MpAttachment/getImage?id=' + items.id
items.type = 'image'
})
that.setData({ fileList : fileList, })
}else{
Toast({ type: 'fail',message: '附件加载失败',duration:500, })
}
}, function (error) {
Toast({ type: 'fail',message: '附件加载失败',duration:500, })
})
}
}
})
组件视图部分代码
<van-cell title="" >
<van-uploader
slot="right-icon"
file-list="{{ fileList }}"
max-count="9"
bind:after-read="afterRead"
bind:delete="delete"
show-upload="{{ !readonly }}"
deletable="{{ !readonly }}"
/>
</van-cell>
<van-toast id="van-toast" />
后端微服务
后端微服务
微服务总归包含了附件上传、删除、获取图片、获取列表、附件上传个服务
微服务代码
@RestController
@RequestMapping("/MpAttachment")
@Api(tags = { Swagger2Config.TAG_MpAttachment })
public class MpAttachmentController implements ServletContextAware {
protected HttpServletRequest request;
protected HttpServletResponse response;
protected HttpSession session;
protected ServletContext servletContext;
String FileConnect ="/";
@Autowired
protected UserService userService;
@Autowired
@Qualifier("dispJdbcTemplate")
protected JdbcTemplate dispJdbcTemplate;
@Autowired
protected MpAttachmentService mpAttachmentService;
@ApiOperation(value = "获取列表", notes = "")
@GetMapping(value="/getList")
public Result getList(@ApiParam(value = "businessid" , required=true ) @RequestParam String businessid,
@ApiParam(value = "businesstype" , required=false ) @RequestParam String businesstype) throws ParseException {
List list = mpAttachmentService.getViewList(businessid,businesstype);
return Result.success(list,"成功!");
}
@CrossOrigin
@ApiOperation(value = "附件上传", notes = "")
@PostMapping("/uploadFile")
public Result uploadFile(@RequestParam("file") MultipartFile file, @RequestHeader("name") String name, @RequestHeader String id, @RequestHeader String tmpId, @RequestHeader String businesstype) {
if (file.isEmpty()) {
return Result.failed("上传文件为空");
}
String uuid = UUID.randomUUID().toString();
// 获取文件名
String fileName = file.getOriginalFilename();
String newFileName = uuid + "."+ fileName.split("\\.")[1];
MpAttachment attachment = new MpAttachment();
attachment.setBusinessid(id);
attachment.setTmp_businessid(tmpId);
attachment.setBusinesstype(businesstype);
attachment.setFilename(fileName);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyMMdd");
String uploadPath = name + FileConnect + LocalDate.now().format(fmt);
attachment.setFilepath(uploadPath + FileConnect + newFileName);
try {
//文件上传
SmbFileUtils.save(file.getBytes(),uploadPath,newFileName);
attachment.setCreatetime(DateUtils.getNow());
attachment.setId(UUID.randomUUID().toString());
mpAttachmentService.add(attachment);
return Result.success(mpAttachmentService.getView(attachment.getId()),"成功!");
} catch (IOException e) {
e.printStackTrace();
return Result.failed("文件上传失败");
}
}
@CrossOrigin
@ApiOperation(value = "附件上传并添加水印", notes = "")
@PostMapping("/uploadImageFile")
public Result uploadImageFile(@RequestParam("file") MultipartFile file, @RequestHeader("name") String name, @RequestHeader String id, @RequestHeader String tmpId, @RequestHeader String businesstype) {
User user = userService.findCueernt();
if (file.isEmpty()) {
return Result.failed("上传文件为空");
}
String uuid = UUID.randomUUID().toString();
// 获取文件名
String fileName = file.getOriginalFilename();
String newFileName = uuid + "."+ fileName.split("\\.")[1];
MpAttachment attachment = new MpAttachment();
attachment.setBusinessid(id);
attachment.setTmp_businessid(tmpId);
attachment.setBusinesstype(businesstype);
attachment.setFilename(fileName);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyMMdd");
String uploadPath = name + FileConnect + LocalDate.now().format(fmt);
attachment.setFilepath(uploadPath + FileConnect + newFileName);
try {
//添加水印
InputStream input = new ByteArrayInputStream((file.getBytes()));
/**给图片添加文字水印**/
ArrayList<String> watermarkList =new ArrayList<String>();
watermarkList.add("现场拍照[客户照片]");
watermarkList.add(user.getName() +" " + DateUtils.dateToStr(new Date(),"yyyy-MM-dd HH:mm"));
InputStream output = ImageWatermarkUtils.markImageByText(watermarkList,input,fileName.split("\\.")[1]);
//文件上传
SmbFileUtils.save(FileUtils.StreamToByte(output),uploadPath,newFileName);
attachment.setCreatetime(DateUtils.getNow());
attachment.setId(UUID.randomUUID().toString());
mpAttachmentService.add(attachment);
return Result.success(mpAttachmentService.getView(attachment.getId()),"成功!");
} catch (IOException e) {
e.printStackTrace();
return Result.failed("文件上传失败");
}
}
@CrossOrigin
@ApiOperation(value = "base64附件上传", notes = "")
@PostMapping("/base64UploadFile")
public Result base64UploadFile(@RequestBody String base64Image, @RequestHeader("fileName") String fileName, @RequestHeader("name") String name, @RequestHeader String id, @RequestHeader String tmpId, @RequestHeader String businesstype) throws UnsupportedEncodingException {
String uuid = UUID.randomUUID().toString();
base64Image = java.net.URLDecoder.decode(base64Image,"UTF-8");
fileName = java.net.URLDecoder.decode(fileName,"UTF-8");
id = java.net.URLDecoder.decode(id,"UTF-8");
String newFileName = uuid + "."+ fileName.split("\\.")[1];
MpAttachment attachment = new MpAttachment();
attachment.setBusinessid(id);
attachment.setTmp_businessid(tmpId);
attachment.setBusinesstype(businesstype);
attachment.setFilename(fileName);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyMMdd");
String uploadPath = name + FileConnect + LocalDate.now().format(fmt);
attachment.setFilepath(uploadPath + FileConnect + newFileName);
try {
byte[] imageByte = ImageUtils.base64ImageToByte(base64Image);
SmbFileUtils.save(imageByte,uploadPath,newFileName);
attachment.setCreatetime(DateUtils.getNow());
attachment.setId(UUID.randomUUID().toString());
mpAttachmentService.add(attachment);
return Result.success(mpAttachmentService.getView(attachment.getId()),"成功!");
} catch (Exception e) {
e.printStackTrace();
return Result.failed("文件上传失败");
}
}
@ApiOperation(value = "获取图片", notes = "")
@GetMapping(value="/getImage", produces = {MediaType.IMAGE_PNG_VALUE})
public BufferedImage getImage(@ApiParam(value = "id" , required=true ) @RequestParam String id) throws IOException {
MpAttachment attachment = mpAttachmentService.get(id);
if(attachment !=null)
{
InputStream imageInputStream = SmbFileUtils.getFile(attachment.getFilepath());
return ImageIO.read(imageInputStream);
}
return null;
}
@ApiOperation(value = "删除", notes = "")
@GetMapping(value="/delete")
public Result delete(@ApiParam(value = "id" , required=true ) @RequestParam String id) {
MpAttachment attachment = mpAttachmentService.get(id);
try {
SmbFileUtils.delete(attachment.getFilepath());
int result = mpAttachmentService.delete(id);
if(result >0){
return Result.success(attachment,"删除成功!");
}else {
return Result.success(attachment,"删除失败!");
}
} catch (Exception e) {
e.printStackTrace();
return Result.failed("失败");
}
}
@ModelAttribute
public void setReqAndRes(HttpServletRequest request, HttpServletResponse response){
this.request = request;
this.response = response;
this.session = request.getSession();
}
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
}
jcifs文件管理帮助类
package com.brickdog.common.utils;
import jcifs.CIFSContext;
import jcifs.CIFSException;
import jcifs.context.SingletonContext;
import jcifs.smb.*;
import java.io.*;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;
public class SmbFileUtils {
static String ip = "127.0.0.1";
static String domain = "127.0.0.1/upload";
static String userName = "admin";
static String password = "admin";
static void SmbFileUtils(){
}
//根据账号密码登录
private static CIFSContext withNTLMCredentials(CIFSContext ctx) {
return ctx.withCredentials(new NtlmPasswordAuthenticator(domain,
userName, password));
}
//保存文件
public static String save(byte[] byteArr, String url,String fileName) throws IOException {
InputStream in = new ByteArrayInputStream(byteArr);
String status = "";
try {
CIFSContext context = withNTLMCredentials(SingletonContext.getInstance());
SmbFileWriter.createDirectory("smb://" + domain +"/" + url, context);
boolean result = SmbFileWriter.writeSmbFile(in, "smb://" + domain +"/" + url +"/" + fileName, context);
status = "success";
} catch (Exception e) {
e.printStackTrace();
status = "error";
} finally {
in.close();
return status;
}
}
//获取文件
public static InputStream getFile(String filePath) throws IOException {
String url = "smb://" + domain + "/" + filePath;
try {
CIFSContext context = withNTLMCredentials(SingletonContext.getInstance());
SmbFileReader reader = new SmbFileReader();
byte[] byteArr = reader.readSmbFile(url, context);
InputStream input = new ByteArrayInputStream(byteArr);
return input;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//删除文件
public static String delete(String filePath) throws IOException {
String status = "";
String url = "smb://" + domain + "/" + filePath;
try {
CIFSContext context = withNTLMCredentials(SingletonContext.getInstance());
SmbFile file = new SmbFile(url, context);
if (file.exists()) {
file.delete();
status = "success";
}
} catch (Exception e) {
e.printStackTrace();
status = "error";
}
return status;
}
static class SmbFileReader {
public byte[] readSmbFile(String path, CIFSContext context) throws IOException {
try {
SmbFile smbFile = new SmbFile(path, context);
long fileSize = smbFile.length();
if (fileSize > Integer.MAX_VALUE) {
System.out.println("file too big...");
return null;
}
InputStream fi = smbFile.getInputStream();
byte[] buffer = new byte[(int) fileSize];
int offset = 0;
int numRead = 0;
while (offset < buffer.length
&& (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) {
offset += numRead;
}
// 确保所有数据均被读取
if (offset != buffer.length) {
throw new IOException("Could not completely read file "
+ smbFile.getName());
}
fi.close();
return buffer;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
static class SmbFileWriter {
static boolean writeSmbFile(String source, String target, CIFSContext context) throws IOException {
if (StrUtils.isEmpty(source) || StrUtils.isEmpty(target)) {
return false;
}
return writeSmbFile(Files.newInputStream(Paths.get(source)),
target, context);
}
static boolean writeSmbFile(InputStream in, String target, CIFSContext context) throws IOException {
if (Objects.nonNull(in) && StrUtils.isNotEmpty(target)) {
try (SmbFile file = new SmbFile(target, context)) {
try (SmbFile parent = new SmbFile(file.getParent(), context)) {
if (!parent.exists()) {
createDirectory(file.getParent(), context);
}
if (!file.exists()) {
file.createNewFile();
}
}
try (OutputStream os = file.getOutputStream()) {
byte[] bytes = new byte[1024];
while (in.read(bytes) != -1) {
os.write(bytes);
}
return true;
}
}finally {
in.close();
}
}
return false;
}
static SmbFile createDirectory(String targetDir, CIFSContext context) throws MalformedURLException,
CIFSException, MalformedURLException {
try (SmbFile dir = new SmbFile(targetDir, context)) {
if (!dir.exists()) {
dir.mkdir();
}
return dir;
}
}
}
}
pom文件
这边jcifs包我们一定要使用2.0以上的,2.0以下经常会出现网盘权限认证卡住导致读取或者上传附件特别慢
<dependency>
<groupId>eu.agno3.jcifs</groupId>
<artifactId>jcifs-ng</artifactId>
<version>2.1.3</version>
</dependency>
生成文件
我们对每类文件进行文件加归类,每天一个文件夹分开存放附件
表结构
添加水印
使用案列
//添加水印
InputStream input = new ByteArrayInputStream((file.getBytes()));
/**给图片添加文字水印**/
ArrayList<String> watermarkList =new ArrayList<String>();
watermarkList.add("现场拍照[客户照片]");
watermarkList.add(user.getName() +" " + DateUtils.dateToStr(new Date(),"yyyy-MM-dd HH:mm"));
InputStream output = ImageWatermarkUtils.markImageByText(watermarkList,input,fileName.split("\\.")[1]);
//文件上传
SmbFileUtils.save(FileUtils.StreamToByte(output),uploadPath,newFileName);
图片帮助类
package com.brickdog.common.utils;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
/**
* 图片添加水印工具类
* 文字水印 图片水印 利用jdk ,不依赖第三方
*/
public class ImageWatermarkUtils {
static final String NEW_IMAGE_NAME_PRE_STR = "_water";
// 水印透明度
private static float alpha = 0.5f;
// 水印文字字体
private static Font font = new Font("宋体", Font.BOLD, 12);
// 水印文字颜色
private static Color color = Color.white;
/**
* 给图片添加水印、可设置水印图片旋转角度
*
* @param iconPath 水印图片路径
* @param srcImgPath 源图片路径
* @param targerPath 目标图片路径
* @param degree 水印图片旋转角度
*/
public static void markImageByIcon(String iconPath, String srcImgPath, String targerPath, Integer degree) {
OutputStream os = null;
try {
if (StrUtils.isBlank(targerPath)) {
targerPath = srcImgPath.substring(0, srcImgPath.lastIndexOf(".")) + NEW_IMAGE_NAME_PRE_STR + srcImgPath.substring(srcImgPath.lastIndexOf("."));
}
Image srcImg = ImageIO.read(new File(srcImgPath));
BufferedImage buffImg = new BufferedImage(srcImg.getWidth(null), srcImg.getHeight(null), BufferedImage.TYPE_INT_RGB);
// 得到画笔对象
// Graphics g= buffImg.getGraphics();
Graphics2D g = buffImg.createGraphics();
// 设置对线段的锯齿状边缘处理
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(srcImg.getScaledInstance(srcImg.getWidth(null), srcImg.getHeight(null), Image.SCALE_SMOOTH), 0, 0, null);
if (null != degree) {
// 设置水印旋转
g.rotate(Math.toRadians(degree),
(double) buffImg.getWidth() / 2, (double) buffImg
.getHeight() / 2);
}
// 水印图象的路径 水印一般为gif或者png的,这样可设置透明度
ImageIcon imgIcon = new ImageIcon(iconPath);
// 得到Image对象。
Image img = imgIcon.getImage();
float alpha = 1f; // 透明度
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
/**
* 以下一算水印图位置,右下角
*/
int width = srcImg.getWidth(null);
int height = srcImg.getHeight(null);
int iconWidth = img.getWidth(null);
int iconHeight = img.getHeight(null);
int x = width - iconWidth;
int y = height - iconHeight;
x = x < 0 ? 0 : x;
y = y < 0 ? 0 : y;
// 表示水印图片的位置
g.drawImage(img, x, y, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
g.dispose();
os = new FileOutputStream(targerPath);
// 生成图片
ImageIO.write(buffImg, "JPG", os);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != os)
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 给图片添加水印文字、可设置水印文字的旋转角度
*
*/
public static InputStream markImageByText(ArrayList<String> watermarkList, InputStream imageInputStream,String formatName) {
try {
// 1、源图片
Image srcImg = ImageIO.read(imageInputStream);
int srcImgWidth = srcImg.getWidth(null);
int srcImgHeight = srcImg.getHeight(null);
BufferedImage buffImg = new BufferedImage(srcImgWidth, srcImgHeight, BufferedImage.TYPE_INT_RGB);
// 2、得到画笔对象
Graphics2D g = buffImg.createGraphics();
// 3、设置对线段的锯齿状边缘处理
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(
srcImg.getScaledInstance(srcImg.getWidth(null),
srcImg.getHeight(null), Image.SCALE_SMOOTH), 0, 0, null);
// 4、设置黑色遮罩
int rowHeight = 20;
int padding = 6;
int height = rowHeight * watermarkList.size() + padding;
int x = padding;
int y = srcImgHeight - height;
g.setColor(Color.black);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.3f));
g.fillRect(0,y,srcImgWidth,height);
// 5、设置水印文字颜色
g.setColor(color);
// 6、设置水印文字Font
g.setFont(font);
// 7、设置水印文字透明度
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1f));
// 8、第一参数->设置的内容,后面两个参数->文字在图片上的坐标位置(x,y)
for(int i=0;i<watermarkList.size();i++)
{
g.drawString(watermarkList.get(i), x, y + rowHeight);
y =y+rowHeight;
}
// 9、释放资源
g.dispose();
// 10、生成图片
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(buffImg, formatName, os);
InputStream outStream = new ByteArrayInputStream(os.toByteArray());
return outStream;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
} catch (Exception e) {
e.printStackTrace();
}
try {
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 获取水印文字总长度
*
* @param waterMarkContent
* 水印的文字
* @param g
* @return 水印文字总长度
*/
private static int getWatermarkLength(String waterMarkContent, Graphics2D g) {
return g.getFontMetrics(g.getFont()).charsWidth(waterMarkContent.toCharArray(), 0, waterMarkContent.length());
}
}
参考文献
https://github.com/codelibs/jcifs
https://github.com/xuanyiying/jcifs-ng-smb2-demo