在物资管理中,我们经常需要对物资通过二维码,一码一物的方式进行管理,这样的管理方式非常方便。每个二维码都记录了整个物资设备的生命周期。
具体的业务场景可能有如下几个场景:
当物资进场后,我们可以通过物资清单Excel实现数据导入,批量制作二维码标签,贴在对应的物资设备上,一物一码,手机扫码即可查看物资类型,物资名称,物资型号,以及物资的当前使用状态(进场,入库,出库,已使用等等)。
1.首先提供Excel的数据批量导入功能接口
2.然后对导入的资产数据信息进行落库。
3.开始批量制作二维码标签,二维码的内容为当前每个资产的详细信息以及初始化的状态,比如已检阅状态。app扫码后,会根据自己固定的跳转路由地址,并携带扫码得到的参数,跳转到指定的路由地址。
4.将其制作完毕后,将所有的二维码图片上传至文件服务器minio或者oss,同时给用户返回所有的URL列表,供用户下载打印。
当二维码标签信息需要修改,例如物资使用状态发生变化时,不需要重新制作打印标签,要么在后台修改对应的二维码中的内容即可;同时也需要支持手机端现场扫码,直接修改内容即可,不用登录后台操作。
1.提供物资状态或者信息变更接口,管理员扫码后,首先回显物资所有信息,以及当前的状态,
2.管理员可以直接修改当前物资信息和状态,并进行提交。
当物资或者设备出现损坏或者故障的时候,使用者可以通过对该物资或者设备上的二维码进行扫码,选择故障申报,填写故障信息,并上传故障图片,完成申报,形成任务下发,同时系统将向责任人进行提醒通知,维修人员维修完毕后,扫码后,在当前任务下完成故障维修说明,并结束任务,形成任务闭环。
今天我给大家做一个实现,具体用到的技术:
SpringBoot+Minio+Hutool
<hutool.version>5.8.15hutool.version>
引入如下:
<dependency>
<groupId>io.miniogroupId>
<artifactId>minioartifactId>
<version>8.5.2version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-bomartifactId>
<version>${hutool.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.15version>
dependency>
<dependency>
<groupId>com.google.zxinggroupId>
<artifactId>coreartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>com.google.zxinggroupId>
<artifactId>javaseartifactId>
<version>3.2.0version>
dependency>
qrCode:
# 二维码中间的logo地址
logoUrl:
# 二维码宽度
width: 300
# 二维码高度
height: 300
# 获取二维码中的Logo缩放的比例系数,如5表示长宽最小值的1/5
ratio: 7
minio:
endpoint:
accessKey:
secretKey:
bucketName:
expires: 7200 # 单位秒
package com.wuk.uaa.utils;
import cn.hutool.core.io.FileUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 文件处理工具类
*/
@Slf4j
@Component
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FileUtils extends FileUtil {
/**
* 获取封装得MultipartFile
* @param inputStream inputStream
* @param fileName fileName
* @return MultipartFile
*/
public MultipartFile getMultipartFile(InputStream inputStream, String fileName) {
FileItem fileItem = createFileItem(inputStream, fileName);
return new CommonsMultipartFile(fileItem);
}
/**
* FileItem类对象创建
*/
public FileItem createFileItem(InputStream inputStream, String fileName) {
FileItemFactory factory = new DiskFileItemFactory(16, null);
String textFieldName = "file";
FileItem item = factory.createItem(textFieldName, MediaType.MULTIPART_FORM_DATA_VALUE, true, fileName);
int bytesRead;
byte[] buffer = new byte[8192];
OutputStream os = null;
//使用输出流输出输入流的字节
try {
os = item.getOutputStream();
while ((bytesRead = inputStream.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
inputStream.close();
} catch (IOException e) {
log.error("Stream copy exception", e);
throw new IllegalArgumentException("文件上传失败");
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
log.error("Stream close exception", e);
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error("Stream close exception", e);
}
}
}
return item;
}
}
package com.wuk.uaa.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Data
public class MinIoClientConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
/**
* 注入minio 客户端
* @return
*/
@Bean
public MinioClient minioClient(){
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
package com.wuk.uaa.utils;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
@Component
@Slf4j
public class MinioUtil {
private static final String FILE_NAME_PATTERN = "{}_{}";
@Value("${minio.expires}")
private Integer fileExpires;
@Resource
private MinioClient minioClient;
/**
* description: 判断bucket是否存在
* @param bucketName 桶名称
* @return: bool值
*/
public boolean bucketExists(String bucketName) throws Exception{
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* description: 创建bucket
* @param bucketName 桶名称
*/
public void createBucket(String bucketName) throws Exception {
boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!isExist) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* description: 获取全部bucket
*/
public List<Bucket> getAllBuckets() throws Exception{
return minioClient.listBuckets();
}
/***
* @author: wuk
* @date: 2023/7/11 11:05
* @description: 上传文件并返回文件名称
* * @return: java.lang.String 返回
*/
public String upload(MultipartFile file, String bucketName) throws Exception {
//判断桶是否存在
boolean b = this.bucketExists(bucketName);
if (!b) {
this.createBucket(bucketName);
}
// 上传文件的原始文件名
String originalFilename = file.getOriginalFilename();
// 文件大小
long fileSize = file.getSize();
log.info("上传文件的原始文件名:{},文件大小:{},文件类型为:{}", originalFilename, fileSize, file.getContentType());
// 文件名:原始文件名_日期
String fileName = StrUtil.format(FILE_NAME_PATTERN,DateUtils.dateTimeNow(),originalFilename);
log.info("格式化后的文件名:{}", fileName);
try {
minioClient.putObject(PutObjectArgs
.builder()
.bucket(bucketName)
.object(fileName)
.stream(file.getInputStream(), fileSize, -1)
.contentType(file.getContentType()).build());
} catch (Exception e) {
log.info("上传文件失败!{}",e.getMessage());
}
// 返回可访问的图片链接
String fileUrl = getFileUrl(bucketName, fileName, fileExpires);
log.info("上传的图片链接为:{}",fileUrl);
return fileUrl;
}
/**
* description: 获取文件url并设置过期时间
* @param bucketName 桶名称
* @param fileName 文件名称
* @param expires 过期时间(不指定默认为7200秒)
* @return: url 文件路径
*/
public String getFileUrl(String bucketName, String fileName, Integer expires) throws Exception {
BucketExistsArgs bucketArgs = BucketExistsArgs.builder().bucket(bucketName).build();
boolean bucketExists = minioClient.bucketExists(bucketArgs);
if (ObjectUtil.isNull(expires)) {
expires = fileExpires;
}
String url = "";
if (bucketExists) {
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(fileName)
.expiry(expires)
.build();
url = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
}
return url;
}
/**
* description: 下载文件
* @param bucketName 桶名称
* @param fileName 文件名称
* @return: byte[] 文件数组
*/
public byte[] download(String bucketName, String fileName) throws Exception{
BucketExistsArgs bucketArgs = BucketExistsArgs.builder().bucket(bucketName).build();
boolean bucketExists = minioClient.bucketExists(bucketArgs);
log.info("bucketExists:{}", bucketExists);
InputStream stream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buff = new byte[100];
int rc;
while((rc=stream.read(buff, 0, 100))>0) {
baos.write(buff, 0, rc);
}
return baos.toByteArray();
}
}
package com.wuk.uaa.utils;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.wuk.uaa.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* @BelongsProject: base-uaa-api
* @BelongsPackage: com.wuk.uaa.utils
* @Author: wuk
* @Date: 2023/7/10 8:47
* @Description:
*/
@Slf4j
@Component
public class QRCodeUtils {
@Value("${qrCode.logoUrl}")
private String logoUrl;
@Value("${qrCode.width}")
private Integer width;
@Value("${qrCode.height}")
private Integer height;
@Value("${qrCode.ratio}")
private Integer ratio;
@Value("${minio.bucketName}")
private String bucketName;
@Resource
private MinioUtil minioUtil;
@Resource
private FileUtils fileUtils;
public QrConfig getQrConfig(){
try {
// 将URL转为BufferedImage
BufferedImage bufferedImage = ImageIO.read(new URL(logoUrl));
// 二维码自定义参数对象
QrConfig qrConfig = QrConfig.create();
// 设置二维码的宽度
qrConfig.setWidth(width);
// 设置二维码的高度
qrConfig.setHeight(height);
// 设置二维码中LOGO图片
qrConfig.setImg(bufferedImage);
// 设置二维码中的Logo缩放的比例系数,如4表示长宽最小值的1/4
qrConfig.setRatio(ratio);
qrConfig.setCharset(StandardCharsets.UTF_8);
qrConfig.setErrorCorrection(ErrorCorrectionLevel.H);
return qrConfig;
}catch (Exception ex){
ex.printStackTrace();
log.error("错误信息为:{}",ex.getMessage());
throw new ServiceException("LOGO的Url地址无法解析");
}
}
/**
* 将文件对象列表上传至文件服务器,然后得到所有的二维码标签图片URL
*/
public List<String> getOrCodeUrlList(List<?> contents) throws Exception{
List<String> fileUrls = new ArrayList<>();
for (Object content : contents) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
QrCodeUtil
.generate(JsonUtils.toJsonString(content),
getQrConfig(), ImgUtil.IMAGE_TYPE_JPG, outputStream);
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
//文件上传并获取地址
fileUrls.add(minioUtil.upload(fileUtils.getMultipartFile(inputStream, IdUtil.getSnowflake().nextIdStr()+"."+ImgUtil.IMAGE_TYPE_JPG), bucketName));
}
return fileUrls;
}
}
package com.wuk.uaa.controller;
import com.wuk.uaa.core.controller.BaseController;
import com.wuk.uaa.core.domain.R;
import com.wuk.uaa.entity.vo.QRCodeVo;
import com.wuk.uaa.entity.vo.SysUserImportVo;
import com.wuk.uaa.excel.ExcelResult;
import com.wuk.uaa.service.ISysUserService;
import com.wuk.uaa.utils.ExcelUtil;
import com.wuk.uaa.utils.QRCodeUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
/**
* @BelongsProject: base-oauth-api
* @BelongsPackage: com.longi.baseoauthapi.controller
* @Author: wuk
* @Date: 2023/7/7 14:21
* @Description: 二维码应用
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/tdCode")
public class QRCodeController extends BaseController {
@Resource
private QRCodeUtils qrCodeUtils;
@Resource
private ISysUserService userService;
@PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<QRCodeVo> importData(@RequestPart("file") MultipartFile file) throws Exception {
ExcelResult<SysUserImportVo> result = ExcelUtil.importExcel(file.getInputStream(),
SysUserImportVo.class,true);
List<SysUserImportVo> userImportVos = result.getList();
List<String> qrCodeUrlList = qrCodeUtils.getOrCodeUrlList(userImportVos);
QRCodeVo qrCodeVo = new QRCodeVo();
qrCodeVo.setAnalysis(result.getAnalysis());
qrCodeVo.setUrlList(qrCodeUrlList);
//保存业务库
userService.insertISUser(userImportVos);
return R.ok(qrCodeVo);
}
}