Commit 87efa3b4 by 陈皓

添加接口服务系统

parent ee256af6
...@@ -6,26 +6,8 @@ redis.password: ...@@ -6,26 +6,8 @@ redis.password:
#数据库源配置 #数据库源配置
db: db:
fypt: fypt:
username: oa_system username: root
password: GXfy/2014!@#$ password: Dk2019!23456
driver-class-name: com.kingbase8.Driver driver-class-name: com.mysql.cj.jdbc.Driver
url: url:
sales: jdbc:kingbase8://172.28.1.68:54321/SALES image: jdbc:mysql://119.45.183.210:13308/image?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
#jwt
jwt:
header: Authorization
# 令牌前缀
token-start-with: Bearer
# 必须使用最少88位的Base64对该令牌进行编码
base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=
# 令牌过期时间 此处单位/毫秒 ,默认2小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html
token-validity-in-seconds: 7200000
# 在线用户key
online-key: online-token-
# 验证码
code-key: code-key-
# token 续期检查时间范围(默认30分钟,单位默认毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期
detect: 1800000
# 续期时间范围,默认 1小时,这里单位毫秒
renew: 3600000
#Redis配置
redis.url: 147.1.5.135
redis.port: 6379
redis.password: Gxfy2022!@#$
#数据库源配置
db:
fypt:
username: ymts1
password: gxfy123456
driver-class-name: com.mysql.cj.jdbc.Driver
url:
image: jdbc:mysql://147.1.3.70:3306/image?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
\ No newline at end of file
...@@ -136,6 +136,7 @@ public class ImgProcService { ...@@ -136,6 +136,7 @@ public class ImgProcService {
// 检测图片的黑边 // 检测图片的黑边
res.setBlack(blackDetection(image)); res.setBlack(blackDetection(image));
image.release(); image.release();
FileUtil.del(filePath);
return res; return res;
} }
......
...@@ -18,12 +18,12 @@ spring: ...@@ -18,12 +18,12 @@ spring:
ip: ip:
local-parsing: true local-parsing: true
imgconfig:
opencv: /opt/tianjin/lib/opencv_java460.so
deskew: /opt/tianjin/lib/Deskew/Bin/deskew
deskewpy: /opt/tianjin/lib/correct.py
#imgconfig: #imgconfig:
# opencv: D:/project/imgproc/lib/opencv_java460.dll # opencv: /opt/services/tianjin-backend/lib/opencv_java460.so
# deskew: C:/Users/11419/Desktop/Deskew/Bin/deskew.exe # deskew: /opt/services/tianjin-backend/lib/Deskew/Bin/deskew
# deskewpy: D:/project/imgproc/lib/correct.py # deskewpy: /opt/tianjin/lib/correct.py
imgconfig:
opencv: D:/project/imgproc/lib/opencv_java460.dll
deskew: C:/Users/11419/Desktop/Deskew/Bin/deskew.exe
deskewpy: D:/project/imgproc/lib/correct.py
package com.zq.processing.controller;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zq.processing.service.ImgProcService;
import com.zq.processing.utils.*;
import com.zq.processing.vo.DetectionResVo;
import com.zq.processing.vo.OptimizationReq;
import com.zq.processing.vo.OptimizationVO;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
/**
* <p>
* 图片处理API
* </p>
*
* @author chenhao
* @since 2023/3/18 9:32
*/
@io.swagger.annotations.Api(tags = "图片处理API")
@RequestMapping("/imgproc/v1")
@RestController
public class ApiController {
@Value("${imgconfig.opencv}")
String opencvUrl;
@Value("${imgconfig.deskew}")
String deskewUrl;
private final ImgProcService service;
@Autowired
public ApiController(ImgProcService service) {
this.service = service;
}
@ApiOperation("测试")
@PostMapping("/ping")
public ResultVo<?> ping() {
return ResultVo.success("测试成功");
}
@ApiOperation("图片检测")
@PostMapping ("/detection")
public ResultVo<DetectionResVo> detection(@RequestBody OptimizationReq req) throws Exception {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.detection(req));
}
@ApiOperation("图片校正(deskew工具)")
@PostMapping("/imageCorrection")
public ResultVo<?> imageCorrection(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 图片矫正
ImageCorrectionUtil.deskew(imgPath, savePath, deskewUrl, false);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("去黑边")
@PostMapping("/removeBlack")
public ResultVo<?> removeBlack(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 去黑边
RemoveBlackUtil.remove(imgPath, savePath);
// 图片弯曲矫正
HashMap<String, Object> map = new HashMap<>();
map.put("file", FileUtil.file(savePath));
String response = HttpUtil.post("http://ddns.gxmailu.com:18888/api/correct", map);
JSONObject res = JSONUtil.parseObj(response);
if (res.get("success", Boolean.class)) {
String base64 = res.get("data", String.class);
return ResultVo.success(base64);
} else {
return ResultVo.fail(res.get("msg", String.class));
}
}
@ApiOperation("图片灰度化")
@PostMapping("/gray")
public ResultVo<?> gray(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 灰度化
ImageUtil.gray(imgPath, savePath);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("图片边缘检测")
@PostMapping("/canny")
public ResultVo<?> canny(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
System.load(opencvUrl);
String imgPath = UploadUtils.saveTempFile(file, "api");
String ext = FileUtil.extName(file.getOriginalFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/file/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
// 边缘检测
ImageUtil.canny(imgPath, savePath);
return ResultVo.success(Base64Encoder.encode(FileUtil.readBytes(savePath)));
}
@ApiOperation("图片弯曲矫正")
@PostMapping("/correct")
public ResultVo<?> correct(@RequestPart MultipartFile file) {
if (file.isEmpty()) {
return ResultVo.fail("文件不能为空");
}
String imgPath = UploadUtils.saveTempFile(file, "api");
// 图片弯曲矫正
HashMap<String, Object> map = new HashMap<>();
map.put("file", FileUtil.file(imgPath));
String response = HttpUtil.post("http://ddns.gxmailu.com:18888/api/correct", map);
JSONObject res = JSONUtil.parseObj(response);
if (res.get("success", Boolean.class)) {
String base64 = res.get("data", String.class);
return ResultVo.success(base64);
} else {
return ResultVo.fail(res.get("msg", String.class));
}
}
@ApiOperation("图片优化")
@PostMapping("/imageOptimization")
public ResultVo<OptimizationVO> imageOptimization(@RequestBody OptimizationReq req) throws IOException {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.optimization(req));
}
}
package com.zq.processing.service;
import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import com.zq.processing.utils.ImageCorrectionUtil;
import com.zq.processing.utils.ImageUtil;
import com.zq.processing.utils.RemoveBlackUtil;
import com.zq.processing.utils.UuidUtils;
import com.zq.processing.vo.DetectionResVo;
import com.zq.processing.vo.OptimizationReq;
import com.zq.processing.vo.OptimizationVO;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.opencv.opencv_core.Mat;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imwrite;
/**
* <p>
* 图片检测服务
* </p>
*
* @author chenhao
* @since 2022/10/24 15:02
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class ImgProcService {
@Value("${imgconfig.deskew}")
String deskewUrl;
@Value("${imgconfig.deskewpy}")
String deskewpyUrl;
public DetectionResVo detection(OptimizationReq req) throws Exception {
// 1. 临时保存图片
String ext = FileUtil.extName(req.getFilename());
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String filePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
FileUtil.writeBytes(Base64Decoder.decode(req.getFileContent()), filePath);
if ("heic".equals(ext)) {
BufferedImage bufferedImage = ImageIO.read(new File(filePath));
ext = "jpg";
filePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
ImageIO.write(bufferedImage, "jpg", new File(filePath));
}
DetectionResVo res = new DetectionResVo();
@Cleanup
Mat image = imread(filePath);
// 检测图片的分辨率
res.setWidthResolution(image.cols());
res.setHeightResolution(image.rows());
// 检测图片的DPI
res.setDpi(ImageUtil.getDpi(FileUtil.file(filePath)));
// 检测图片清晰度
res.setClarity(ImageUtil.imageSharpnessDetector(filePath));
// 检测图片的亮度
res.setBrightness(ImageUtil.brightness(filePath));
// 检测图片倾斜角度
res.setAngle(getAngle(filePath));
// 检测图片的黑边
res.setBlack(RemoveBlackUtil.blackDetection(image));
image.release();
return res;
}
/**
* 通过矫正工具获取的倾斜角度
*
* @param imageUrl 图片路径
* @return 图片倾斜角度
*/
private Double getAngle(String imageUrl) {
// 纠正文件的保存路径
String ext = FileUtil.extName(imageUrl);
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "." + ext;
return 0.0;
}
public OptimizationVO optimization(OptimizationReq req) throws IOException {
OptimizationVO res = new OptimizationVO();
// 1. 临时保存图片
String ext = FileUtil.extName(req.getFilename());
String filename = req.getFilename();
String yyyyMMdd = new SimpleDateFormat("/yyyyMM/dd/").format(new Date());
String savePath = "/data/temp" + yyyyMMdd + UuidUtils.uuidNoDash() + "/";
String filePath = savePath + filename;
FileUtil.writeBytes(Base64Decoder.decode(req.getFileContent()), filePath);
log.info("{}图片进行优化", filePath);
// 处理heic图片
if ("heic".equals(ext)) {
BufferedImage bufferedImage = ImageIO.read(new File(filePath));
ext = "jpg";
filename = FileUtil.mainName(filename) + ".jpg";
filePath = savePath + filename;
ImageIO.write(bufferedImage, "jpg", new File(filePath));
log.info("{}图片从heic转换为jpg", filePath);
}
String newPath = filePath;
// 2. 执行纠偏
long start = System.currentTimeMillis();
String deskerFile = savePath + "(1)." + ext;
String resMsg = ImageCorrectionUtil.deskew2(filePath, deskerFile, deskewpyUrl);
Double skewAngle = Convert.toDouble(resMsg);
if (skewAngle == null) {
log.error(resMsg);
String deskewRes = ImageCorrectionUtil.deskew(filePath, deskerFile, deskewUrl, false);
Pattern pattern = Pattern.compile("Skew angle found \\[deg\\]: (-?\\d+(?:\\.\\d+)?)");
Matcher matcher = pattern.matcher(deskewRes);
if (matcher.find()) {
skewAngle = Double.parseDouble(matcher.group(1));
log.info("矫正工具矫正角度【{}】", skewAngle);
} else {
res.setDeskewAngel(0);
}
}
if (Double.compare(skewAngle, 1) < 0 && Double.compare(skewAngle, -1) > 0) {
res.setDeskewAngel(0);
} else {
newPath = deskerFile;
res.setDeskewAngel(skewAngle);
}
log.info("纠偏所需时间【{}】", System.currentTimeMillis() - start);
// 3. 黑边处理
start = System.currentTimeMillis();
String blackUrl = savePath + "(2)." + ext;
@Cleanup
Mat bMat = imread(newPath);
if (RemoveBlackUtil.blackDetection(bMat)) {
res.setRemoveBlack(true);
RemoveBlackUtil.remove(newPath, blackUrl);
newPath = blackUrl;
} else {
res.setRemoveBlack(false);
}
res.setCorrect(false);
log.info("黑边处理消耗的时间【{}】", System.currentTimeMillis() - start);
// 4。 图片亮度检测
// 计算图像的平均亮度
start = System.currentTimeMillis();
double brightness = ImageUtil.brightness(newPath);
res.setOriginalBrightnessVal(brightness);
// 亮度异常执行亮度矫正
if (brightness < 100 || brightness > 500) {
res.setBrightness(true);
// 计算亮度调整值
double alpha = 300 / brightness;
// 执行亮度调整
@Cleanup
Mat grayImage = imread(newPath);
@Cleanup
Mat adjustedImage = new Mat();
grayImage.convertTo(adjustedImage, -1, alpha, 0);
// 保存调整后的图像
imwrite(newPath, adjustedImage);
res.setBrightnessVal(ImageUtil.brightness(newPath));
} else {
res.setBrightness(false);
res.setBrightnessVal(0);
}
log.info("亮度检测所需时间【{}】", System.currentTimeMillis() - start);
// 5. 图片清晰度检测
start = System.currentTimeMillis();
double laplacianScore = ImageUtil.imageSharpnessDetector(newPath);
res.setOriginalClarityVal(laplacianScore);
if (laplacianScore < 500) {
res.setClarity(true);
Mat cMat = ImageUtil.unsharpMasking(newPath);
// 保存调整后的图像
imwrite(newPath, cMat);
res.setClarityVal(ImageUtil.imageSharpnessDetector(newPath));
cMat.release();
} else {
res.setClarity(false);
res.setClarityVal(0);
}
log.info("图片清晰度所需时间【{}】", System.currentTimeMillis() - start);
res.setFilename(filename);
res.setFileContent(Base64Encoder.encode(FileUtil.readBytes(newPath)));
return res;
// 6. 图片弯曲矫正
// String correctUrl = savePath + "(3)." + ext;
// HashMap<String, Object> map = new HashMap<>();
// map.put("file", FileUtil.file(newPath));
// String response = HttpUtil.post("http://ddns.gxmailu.com:18888/api/correct", map);
// JSONObject correctRes = JSONUtil.parseObj(response);
// if (ObjectUtil.isNotNull(correctRes) && correctRes.get("success", Boolean.class)) {
// String base64 = correctRes.get("data", String.class);
// res.setCorrect(true);
// res.setFileContent(base64);
// res.setFilename(filename);
// } else {
// res.setCorrect(false);
// res.setFilename(filename);
// res.setFileContent(Base64Encoder.encode(FileUtil.readBytes(newPath)));
// }
// return res;
}
}
package com.zq.processing.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zq.processing.vo.DetectionResVo;
import com.zq.processing.vo.OptimizationReq;
import com.zq.processing.vo.OptimizationVO;
public class DemoUtil {
final static String URL = "localhost:9100/imgproc/v1/";
public static void main(String[] args) {
detection();
// String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/4.png";
// String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res4.jpg";
// System.out.println(Convert.toInt(-6.25));
// ImgUtil.rotate(FileUtil.file(testImg), Convert.toInt(-6.25), FileUtil.file(resImg));
}
public static void detection() {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/3.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.jpg";
String url = URL + "detection";
OptimizationReq req = new OptimizationReq();
req.setFilename("3.png");
req.setFileContent(Base64.encode(FileUtil.file(testImg)));
long start = System.currentTimeMillis();
HttpRequest request = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(req));
try (HttpResponse response = request.execute()) {
System.out.println(System.currentTimeMillis() - start);
String res = response.body();
JSONObject object = JSONUtil.parseObj(res);
DetectionResVo detectionRes = object.get("data", DetectionResVo.class);
System.out.println(JSONUtil.toJsonStr(detectionRes));
}
}
public static void imageOptimization() {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/2.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.jpg";
String url = URL + "imageOptimization";
OptimizationReq req = new OptimizationReq();
req.setFilename("3.png");
req.setFileContent(Base64.encode(FileUtil.file(testImg)));
long start = System.currentTimeMillis();
HttpRequest request = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(req));
try (HttpResponse response = request.execute()) {
System.out.println(System.currentTimeMillis()- start);
String res = response.body();
JSONObject object = JSONUtil.parseObj(res);
OptimizationVO optimizationVO = object.get("data", OptimizationVO.class);
FileUtil.writeBytes(Base64.decode(optimizationVO.getFileContent()), FileUtil.file(resImg));
optimizationVO.setFileContent(null);
System.out.println(JSONUtil.toJsonStr(optimizationVO));
}
}
}
package com.zq.processing.utils;
import cn.hutool.core.convert.Convert;
import lombok.Cleanup;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Point2f;
import org.bytedeco.opencv.opencv_core.Scalar4i;
import org.bytedeco.opencv.opencv_core.Size;
import org.bytedeco.opencv.opencv_imgproc.Vec4iVector;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
public class Deskew {
public static void main(String[] args) {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/4.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.png";
long start = System.currentTimeMillis();
@Cleanup
Mat mat = imread(testImg);
System.out.println(getDeskewAngle(mat));
System.out.println(System.currentTimeMillis() - start);
}
public static Integer getDeskewAngle(Mat src) {
// 图片灰度化
@Cleanup
Mat gray = ImageUtil.gray(src);
// // 高斯模糊(?)
// @Cleanup
// UMat blur = gray.clone();
// GaussianBlur(gray, blur, new Size(9, 9), 0);
// // 二值化(?)
// @Cleanup
// UMat thresh = blur.clone();
// threshold(blur, thresh, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
@Cleanup
Mat kernel = getStructuringElement(MORPH_RECT, new Size(5, 5));
// 图片膨胀
Mat erode = gray.clone();
erode(gray, erode, kernel);
// 图片腐蚀
@Cleanup
Mat dilate = erode.clone();
dilate(erode, dilate, kernel);
// 边缘检测
@Cleanup
Mat canny = dilate.clone();
Canny(dilate, canny, 50, 150);
// 霍夫变换得到线条
@Cleanup
Vec4iVector lines = new Vec4iVector();
//累加器阈值参数,小于设置值不返回
int threshold = 30;
//最低线段长度,低于设置值则不返回
double minLineLength = 0;
//间距小于该值的线当成同一条线
double maxLineGap = 200;
// 霍夫变换,通过步长为1,角度为PI/180来搜索可能的直线
HoughLinesP(canny, lines, 1, Math.PI / 180, threshold, minLineLength, maxLineGap);
// 计算倾斜角度
List<Integer> angelList = new ArrayList<>();
for (int i = 0; i < lines.size(); i++) {
Scalar4i scalar4i = lines.get(0);
int k = calculateAngle(scalar4i.get(0), scalar4i.get(1), scalar4i.get(2), scalar4i.get(3));
angelList.add(k);
}
if (angelList.isEmpty()) {
return 0;
}
return most(angelList);
}
/**
* 求数组众数
*
* @param angelList 数组
* @return 数组众数
*/
private static int most(List<Integer> angelList) {
if (angelList.isEmpty()) {
return 0;
}
int res = 0;
int max = Integer.MIN_VALUE;
Map<Integer, Integer> map = new HashMap<>();
for (int i : angelList) {
map.put(i, map.getOrDefault(i, 0) + 1);
}
for (Integer i : map.keySet()) {
int count = map.get(i);
if (count > max) {
max = count;
res = i;
}
}
return res;
}
/**
* 计算直线的倾斜角
*/
private static int calculateAngle(double x1, double y1, double x2, double y2) {
if (Math.abs(x2 - x1) < 1e-4) {
return 90;
} else if (Math.abs(y2 - y1) < 1e-4) {
return 0;
} else {
double k = -(y2 - y1) / (x2 - x1);
double res = Math.atan(k) * 57.29577;
return Convert.toInt(Math.round(res));
}
}
/**
* 图片旋转
*
* @param image 输入图片
* @param angle 旋转角度
* @return 输出图片
*/
public static Mat rotate(Mat image, int angle) {
int w = image.cols();
int h = image.rows();
// 定义参数
double scale = 1.0;
@Cleanup
Point2f center = new Point2f((float) w / 2, (float) h / 2);
// 定义旋转矩阵
@Cleanup
Mat rotationMatrix = getRotationMatrix2D(center, -angle, scale);
// 旋转矩阵
Mat rotatedImage = new Mat();
warpAffine(image, rotatedImage, rotationMatrix, new Size(w, h));
return rotatedImage;
}
}
package com.zq.processing.utils;
import cn.hutool.core.util.RuntimeUtil;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
/**
* <p>
* 纠偏工具Deskew
* </p>
*
* @author chenhao
* @since 2023/3/8 10:23
*/
public class ImageCorrectionUtil {
/**
* 纠正图片旋转
*
* @param srcImgPath 图片地址
*/
public static void correctImg(String srcImgPath) {
FileOutputStream fos = null;
try {
// 原始图片
File srcFile = new File(srcImgPath);
// 获取偏转角度
int angle = getAngle(srcFile);
if (angle != 90 && angle != 270) {
return;
}
// 原始图片缓存
BufferedImage srcImg = ImageIO.read(srcFile);
// 宽高互换
// 原始宽度
int imgWidth = srcImg.getHeight();
// 原始高度
int imgHeight = srcImg.getWidth();
// 中心点位置
double centerWidth = ((double) imgWidth) / 2;
double centerHeight = ((double) imgHeight) / 2;
// 图片缓存
BufferedImage targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
// 旋转对应角度
Graphics2D g = targetImg.createGraphics();
g.rotate(Math.toRadians(angle), centerWidth, centerHeight);
g.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);
g.rotate(Math.toRadians(-angle), centerWidth, centerHeight);
g.dispose();
// 输出图片
fos = new FileOutputStream(srcFile);
ImageIO.write(targetImg, "jpg", fos);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 获取图片旋转角度
*
* @param file 上传图片
* @return 图片旋转角度
*/
private static int getAngle(File file) throws Exception {
Metadata metadata = ImageMetadataReader.readMetadata(file);
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
if ("Orientation".equals(tag.getTagName())) {
String orientation = tag.getDescription();
if (orientation.contains("90")) {
return 90;
} else if (orientation.contains("180")) {
return 180;
} else if (orientation.contains("270")) {
return 270;
}
}
}
}
return 0;
}
/**
* 调用deskew工具
* Usage:
* deskew [-o output] [-a angle] [-b color] [..] input
* input: Input image file
* Options:
* -o output: 输出图像文件路径(默认:out.png)
* -a angle: 最大期望倾斜角度(两个方向)(默认:10度)
* -b color: 背景颜色十六进制格式RRGGBB|LL|AARRGGBB(默认为黑色)
* Ext. options:
* -q filter: 用于旋转的重采样过滤器 (default: linear,
* values: nearest|linear|cubic|lanczos)
* -t a|treshold: 自动阈值或0..255的值 (default: a)
* -r rect: 仅在内容矩形中进行倾斜检测 (pixels):
* left,top,right,bottom (default: whole page)
* -f format: 输出像素格式 (values: b1|g8|rgb24|rgba32)
* -l angle: 如果倾斜角度较小,则跳过倾斜步骤 (default: 0.01)
* -g flags: Operational flags (any combination of):
* c - auto crop, d - detect only (no output to file)
* -s info: 信息转储 (any combination of):
* s - skew detection stats, p - program parameters, t - timings
* -c specs: 某些文件格式的输出压缩规格。可以定义几个规格-用逗号分隔。支持规格:
* jXX - JPEG compression quality, XX is in range [1,100(best)]
* tSCHEME - TIFF compression scheme: none|lzw|rle|deflate|jpeg|g4
* Supported file formats
* Input: BMP, JPG, PNG, JNG, GIF, DDS, TGA, PBM, PGM, PPM, PAM, PFM, TIF, PSD
* Output: BMP, JPG, PNG, JNG, GIF, DDS, TGA, PGM, PPM, PAM, PFM, TIF, PSD
*
* @return 调用结果
*/
public static String deskew(String src, String dst, String deskewUrl, boolean deskewAngle) {
StringBuilder str = new StringBuilder();
// 命令行位置
str.append(deskewUrl).append(" ");
// 背景颜色
str.append("-b").append(" ").append("FFFFFF").append(" ");
if (deskewAngle) {
// 图片旋转耗时很多,跳过图片旋转
str.append("-l").append(" ").append("80").append(" ");
}
// 输出路径
str.append("-o").append(" ").append(dst).append(" ");
// 输入路径
str.append(src);
return RuntimeUtil.execForStr(str.toString());
}
public static String deskew2(String src, String dst, String deskewpyUrl) {
StringBuilder str = new StringBuilder();
// python启动
str.append("python").append(" ");
// python文件位置
str.append(deskewpyUrl).append(" ");
// 输入文件
str.append("-i").append(" ").append(src).append(" ");
// 输出文件
str.append("-o").append(" ").append(dst).append(" ");
return RuntimeUtil.execForStr(str.toString());
}
public static void main(String[] args) {
String path1 = "C:\\Users\\11419\\Desktop\\Deskew\\TestImages\\3.png";
String path2 = "C:\\Users\\11419\\Desktop\\Deskew\\TestImages\\res3.png";
String du = "C:\\Users\\11419\\Desktop\\lib\\correct.py";
String res = deskew2(path1, path2, du);
System.out.println(res);
}
}
package com.zq.processing.utils;
import cn.hutool.core.convert.Convert;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import lombok.Cleanup;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.Imaging;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Scalar;
import org.bytedeco.opencv.opencv_core.Size;
import org.bytedeco.opencv.opencv_core.UMat;
import java.io.File;
import static org.bytedeco.opencv.global.opencv_core.*;
import static org.bytedeco.opencv.global.opencv_imgcodecs.*;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
public class ImageUtil {
/**
* 图片灰度化
*
* @param mat 图片地址
*/
public static Mat gray(Mat mat) {
// 灰度化
Mat gray = mat.clone();
if (mat.channels() == 4 || mat.channels() == 3) {
cvtColor(mat, gray, COLOR_BGR2GRAY);
} else if (mat.channels() == 2) {
cvtColor(mat, gray, COLOR_BGR5652GRAY);
} else {
gray = mat;
}
return gray;
}
/**
* 图片灰度化
*
* @param mat 图片地址
*/
public static UMat gray(UMat mat) {
// 灰度化
UMat gray = mat.clone();
if (mat.channels() == 4 || mat.channels() == 3) {
cvtColor(mat, gray, COLOR_BGR2GRAY);
} else if (mat.channels() == 2) {
cvtColor(mat, gray, COLOR_BGR5652GRAY);
} else {
gray = mat;
}
return gray;
}
/**
* 图片灰度化
*
* @param src 图片地址
* @param dst 灰度化图片保存地址
*/
public static void gray(String src, String dst) {
@Cleanup
Mat mat = imread(src);
// 灰度化
@Cleanup
Mat gray = gray(mat);
imwrite(dst, gray);
}
/**
* 边缘检测
*
* @param src 图片地址
* @param dst 边缘检测图片保存地址
*/
public static void canny(String src, String dst) {
@Cleanup
Mat mat = imread(src);
@Cleanup
Mat cannyImg = canny(mat);
imwrite(dst, cannyImg);
}
/**
* 灰度化图片后进行canny边缘检测
*
* @param src 图片矩阵
* @return 边缘检测之后的图片矩阵
*/
private static Mat canny(Mat src) {
// 灰度化
@Cleanup
Mat gray = gray(src);
@Cleanup
Mat mat = gray.clone();
Canny(gray, mat, 60, 200);
return mat;
}
/**
* 获取图片DPI
*
* @param file 图片文件
* @return [水平DPI,垂直DPI]
*/
public static Integer getDpi(File file) throws Exception {
int res;
ImageInfo imageInfo = Imaging.getImageInfo(file);
res = imageInfo.getPhysicalWidthDpi();
if (res == -1) {
Metadata metadata = ImageMetadataReader.readMetadata(file);
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
if ("X Resolution".equals(tag.getTagName())) {
res = Convert.toInt(tag.getDescription());
}
}
}
}
return res;
}
/**
* 计算图片清晰度
*
* @param path 图片路径
* @return 图片清晰度
*/
public static double imageSharpnessDetector(String path) {
@Cleanup
Mat image = imread(path);
@Cleanup
Mat grayImage = gray(image);
// 计算拉普拉斯
@Cleanup
Mat laplacian = new Mat();
Laplacian(grayImage, laplacian, CV_64F);
// 计算清晰度
@Cleanup
Mat mean = new Mat();
@Cleanup
Mat stdDev = new Mat();
meanStdDev(laplacian, mean, stdDev);
return Math.pow(stdDev.createIndexer().getDouble(0, 0, 0), 2.0);
}
/**
* 计算图片平均亮度
*
* @param path 图片路径
* @return 图片平均亮度
*/
public static double brightness(String path) {
@Cleanup
Mat grayImage = imread(path, IMREAD_GRAYSCALE);
// 计算图像的平均亮度
Scalar mean = mean(grayImage);
return mean.get(0);
}
/**
* 执行高斯模糊, UnsharpMasking
*
* @param path 图片路径
* @return 执行后的结果
*/
public static Mat unsharpMasking(String path) {
@Cleanup
Mat image = imread(path);
// 高斯模糊
Mat blurred = new Mat();
GaussianBlur(image, blurred, new Size(3, 3), 0);
// UnsharpMasking
@Cleanup
Mat unsharpMasked = new Mat();
addWeighted(image, 1.5, blurred, -0.5, 0, unsharpMasked);
return unsharpMasked;
}
}
package com.zq.processing.utils;
import lombok.Cleanup;
import org.bytedeco.javacpp.indexer.UByteIndexer;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Rect;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imwrite;
import static org.bytedeco.opencv.global.opencv_imgproc.COLOR_BGR2GRAY;
import static org.bytedeco.opencv.global.opencv_imgproc.cvtColor;
/**
* 去除黑边
*/
public class RemoveBlackUtil {
/**
* 去黑边"全黑"阈值
*/
private static final Integer BLACK_VALUE = 40;
/**
* 去除黑边
*
* @param src 原图片路径
* @param dst 处理后图片路径
*/
public static void remove(String src, String dst) {
@Cleanup
Mat mat = imread(src);
Mat res = removeBlackEdge(mat);
imwrite(dst, res);
}
/**
* 去除图片黑边,若无黑边,则原图返回。默认“全黑”阈值为 {@code BLACK_VALUE}
*
* @param srcMat 预去除黑边的Mat
* @return 去除黑边之后的Mat
*/
private static Mat removeBlackEdge(Mat srcMat) {
return removeBlackEdge(srcMat, BLACK_VALUE);
}
private static Mat removeBlackEdge(Mat srcMat, int blackValue) {
// 图片进行灰度化
@Cleanup
Mat grayMat = srcMat.clone();
cvtColor(srcMat, grayMat, COLOR_BGR2GRAY);
// 定义边界
int topRow = 0;
int leftCol = 0;
int rightCol = grayMat.cols() - 1;
int bottomRow = grayMat.rows() - 1;
UByteIndexer indexer = grayMat.createIndexer();
// 上方黑边判断
for (int row = 0; row < grayMat.rows(); row++) {
if (sum(grayMat.row(row), indexer) / grayMat.cols() < blackValue) {
topRow = row;
} else {
break;
}
}
// 左边黑边判断
for (int col = 0; col < grayMat.cols(); col++) {
if (sum(grayMat.col(col), indexer) / grayMat.rows() < blackValue) {
leftCol = col;
} else {
break;
}
}
// 右边黑边判断
for (int col = grayMat.cols() - 1; col > 0; col--) {
if (sum(grayMat.col(col), indexer) / grayMat.rows() < blackValue) {
rightCol = col;
} else {
break;
}
}
// 下方黑边判断
for (int row = grayMat.rows() - 1; row > 0; row--) {
if (sum(grayMat.row(row), indexer) / grayMat.cols() < blackValue) {
bottomRow = row;
} else {
break;
}
}
int x = leftCol;
int y = topRow;
int width = rightCol - leftCol;
int height = bottomRow - topRow;
if (leftCol == 0 && rightCol == grayMat.cols() - 1 && topRow == 0 && bottomRow == grayMat.rows() - 1) {
return srcMat;
}
return cut(srcMat, x, y, width, height);
}
/**
* 按照指定的尺寸截取Mat,坐标原点为左上角
*
* @param src 源Mat
* @param x x
* @param y y
* @param width width
* @param height height
* @return 截取后的Mat
*/
private static Mat cut(Mat src, int x, int y, int width, int height) {
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (width > src.cols()) {
width = src.cols();
}
if (height > src.rows()) {
height = src.rows();
}
// 截取尺寸
Rect rect = new Rect(x, y, width, height);
return new Mat(src, rect);
}
/**
* 像素求和
*
* @param mat mat
* @return sum
*/
private static int sum(Mat mat, UByteIndexer indexer) {
int sum = 0;
for (int row = 0; row < mat.rows(); row++) {
for (int col = 0; col < mat.cols(); col++) {
sum += indexer.get(row, col, 0);
}
}
return sum;
}
/**
* 黑边检测
*
* @param src 图片矩阵
* @return true表示可能存在黑边
*/
public static boolean blackDetection(Mat src) {
// 图片进行灰度化
@Cleanup
Mat grayMat = src.clone();
cvtColor(src, grayMat, COLOR_BGR2GRAY);
int width = src.cols();
int height = src.rows();
// 定义初始边界
int top = 0;
int left = 0;
int right = width - 1;
int bottom = height - 1;
UByteIndexer indexer = grayMat.createIndexer();
// 上方黑边判断
for (int row = 0; row < height; row++) {
if (sum(grayMat.row(row), indexer) / width < BLACK_VALUE) {
top = row;
} else {
break;
}
}
// 左边黑边判断
for (int col = 0; col < width; col++) {
if (sum(grayMat.col(col), indexer) / height < BLACK_VALUE) {
left = col;
} else {
break;
}
}
// 右边黑边判断
for (int col = width - 1; col > 0; col--) {
if (sum(grayMat.col(col), indexer) / height < BLACK_VALUE) {
right = col;
} else {
break;
}
}
// 下方黑边判断
for (int row = height - 1; row > 0; row--) {
if (sum(grayMat.row(row), indexer) / width < BLACK_VALUE) {
bottom = row;
} else {
break;
}
}
// 若是边界没有被更新,认为不存在黑边
return top != 0 || left != 0 || right != width - 1 || bottom != height - 1;
}
}
package com.zq.processing.utils;
public class TestUtil {
public static void main(String[] args) {
String testImg = "C:/Users/11419/Desktop/Deskew/TestImages/6.png";
String resImg = "C:/Users/11419/Desktop/Deskew/TestImages/res.png";
}
}
package com.zq.processing.utils;
import cn.hutool.core.io.FileUtil;
import com.zq.processing.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
/**
* @author wilmiam
* @since 2021-07-09 18:05
*/
@Slf4j
public class UploadUtils {
private final static String DATE_FORMAT = "/yyyyMM/dd/";
/**
* 保存临时文件
*
* @return 保存地址
*/
public static String saveTempFile(MultipartFile file, String systemName) {
try {
String originalFilename = file.getOriginalFilename();
//取文件扩展名
String ext = Objects.requireNonNull(originalFilename).substring(originalFilename.lastIndexOf(".") + 1);
//生成新文件名
String name = UuidUtils.uuidNoDash() + "." + ext;
String yyyyMMdd = new SimpleDateFormat(DATE_FORMAT).format(new Date());
File dest = new File("/data/temp/" + systemName + yyyyMMdd + name);
FileUtil.writeBytes(file.getBytes(), dest);
return "/data/temp/" + systemName + yyyyMMdd + name;
} catch (IOException e) {
log.error("文件保存失败:{}", e.getMessage());
throw new BusinessException("文件保存失败");
}
}
}
package com.zq.processing.vo;
import lombok.Data;
/**
* <p>
*
* </p>
*
* @author chenhao
* @since 2023/4/20
*/
@Data
public class OptimizationReq {
private String fileContent;
private String filename;
}
package com.zq.processing.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <p>
* 图片优化结果类
* </p>
*
* @author chenhao
* @since 2023/4/20
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class OptimizationVO {
@ApiModelProperty("base64图片")
private String fileContent;
@ApiModelProperty("文件名称")
private String filename;
@ApiModelProperty("是否经过亮度调整")
private boolean isBrightness;
@ApiModelProperty("图片原始平均亮度值")
private double originalBrightnessVal;
@ApiModelProperty("图片修正后的平均亮度值")
private double brightnessVal;
@ApiModelProperty("是否经过清晰度调整")
private boolean isClarity;
@ApiModelProperty("图片原始清晰度值")
private double originalClarityVal;
@ApiModelProperty("图片清晰度调整过后的清晰度值")
private double clarityVal;
@ApiModelProperty("是否经过黑边处理")
private boolean isRemoveBlack;
@ApiModelProperty("图片纠偏的角度,纠偏出错返回-1")
private double deskewAngel;
@ApiModelProperty("图片是否经过弯曲矫正")
private boolean isCorrect;
}
server:
port: 9100
spring:
application:
name: IMGPROCEING-SERVER
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mvc:
format:
date-time: yyyy-MM-dd HH:mm:ss
servlet:
multipart:
max-file-size: 100MB
max-request-size: 150MB
# IP 本地解析
ip:
local-parsing: true
#imgconfig:
# opencv: /opt/tianjin/lib/opencv_java460.so
# deskew: /opt/tianjin/lib/Deskew/Bin/deskew
# deskewpy: /opt/tianjin/lib/correct.py
imgconfig:
opencv: C:/Users/11419/Desktop/lib/opencv_java460.dll
deskew: C:/Users/11419/Desktop/Deskew/Bin/deskew.exe
deskewpy: C:/Users/11419/Desktop/lib/correct.py
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>com.zq</groupId> <groupId>com.zq</groupId>
<artifactId>imgproc</artifactId> <artifactId>imgproc</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
</parent> </parent>
<artifactId>imgprocessing-server</artifactId> <modelVersion>4.0.0</modelVersion>
<artifactId>interface</artifactId>
<version>1.0.0</version>
<name>interface-server</name>
<description>接口服务</description>
<properties> <properties>
<java.version>1.8</java.version> <maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.4.2</spring-boot.version>
</properties> </properties>
<dependencies> <dependencies>
...@@ -27,48 +30,88 @@ ...@@ -27,48 +30,88 @@
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId> <artifactId>spring-cloud-starter-config</artifactId>
</dependency> </dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--Spring boot Web容器--> <!--Spring boot Web容器-->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- 注解执行器 -->
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>hutool-all</artifactId> <artifactId>spring-boot-configuration-processor</artifactId>
<version>5.8.19</version> <optional>true</optional>
</dependency> </dependency>
<!-- redis -->
<dependency> <dependency>
<groupId>org.bytedeco</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>javacv-platform</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
<version>1.5.9</version>
</dependency> </dependency>
<!-- 搭配redis的对象池依赖 -->
<dependency> <dependency>
<groupId>org.bytedeco</groupId> <groupId>org.apache.commons</groupId>
<artifactId>opencv-platform</artifactId> <artifactId>commons-pool2</artifactId>
<version>4.7.0-1.5.9</version> <version>${commons-pool2.version}</version>
</dependency> </dependency>
<!-- druid数据源驱动 -->
<!-- Additional dependencies required to use CUDA and cuDNN -->
<dependency> <dependency>
<groupId>org.bytedeco</groupId> <groupId>com.alibaba</groupId>
<artifactId>opencv-platform-gpu</artifactId> <artifactId>druid-spring-boot-starter</artifactId>
<version>4.7.0-1.5.9</version> <version>1.2.6</version>
</dependency> </dependency>
<!--mybatis-plus-->
<dependency> <dependency>
<groupId>com.drewnoakes</groupId> <groupId>com.baomidou</groupId>
<artifactId>metadata-extractor</artifactId> <artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.18.0</version> <version>3.5.3.1</version>
</dependency> </dependency>
<!-- mysql -->
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>mysql</groupId>
<artifactId>commons-imaging</artifactId> <artifactId>mysql-connector-java</artifactId>
<version>1.0-alpha3</version> <scope>runtime</scope>
</dependency> </dependency>
<!-- swageer -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>io.springfox</groupId>
<artifactId>lombok</artifactId> <artifactId>springfox-swagger2</artifactId>
<version>1.18.28</version> <version>2.9.2</version>
</dependency>
<!-- commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.41</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
......
package com.zq.processing; package com.zq.im;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* <p>
* 服务主启动类
* </p>
*
* @author chenhao
* @since 2022/10/12 20:57
*/
@EnableFeignClients
@EnableDiscoveryClient @EnableDiscoveryClient
@SpringBootApplication @SpringBootApplication
public class ImgprocessingApplication { public class InterfaceApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(ImgprocessingApplication.class, args); SpringApplication.run(InterfaceApplication.class, args);
} }
} }
package com.zq.im.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <p>
* 用于标记记录用户操作日志的方法
* </P>
*
* @author yww
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiLog {
String value() default "";
boolean save() default false;
}
\ No newline at end of file
package com.zq.im.annotation.rest;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* <p>
* 用户操作日志处理切面
* </p>
*
* @author chenhao
* @since 2022/10/15 17:23
*/
@Order(1)
@Aspect
@Component
public class ApiLogAspect {
// private static final Logger LOGGER = LoggerFactory.getLogger(com.zq.im.annotation.ApiLog.class);
// private final LogServiceImpl logService;
//
// @Autowired
// public ApiLogAspect(LogServiceImpl logService) {
// this.logService = logService;
// }
//
// /**
// * 声明切入点
// */
// @Pointcut("@annotation(com.zq.im.annotation.ApiLog)")
// public void pointCut() {}
//
// /**
// * 环绕通知
// */
// @Around("pointCut()")
// public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// // 记录接口调用时间
// long startTime = System.currentTimeMillis();
// Object result = joinPoint.proceed();
// long endTime = System.currentTimeMillis();
// // 获取方法
// Signature signature = joinPoint.getSignature();
// MethodSignature methodSignature = (MethodSignature) signature;
// Method method = methodSignature.getMethod();
// // 获取Log注解的信息,查看是否需要保存操作记录
// boolean isSave = false;
// String value = "";
// if (method.isAnnotationPresent(com.yww.admin.annotation.Log.class)) {
// com.yww.admin.annotation.Log annotation = method.getAnnotation(com.yww.admin.annotation.Log.class);
// isSave = annotation.save();
// value = annotation.value();
// }
// // 打印注解上的消息
// if (StrUtil.isNotBlank(value)) {
// LOGGER.info(value);
// }
// // 若是选择保存到数据库,则获取信息后保存
// if (!isSave) {
// return result;
// }
// // 获取当前请求对象
// ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// if (attributes == null) {
// return result;
// }
// HttpServletRequest request = attributes.getRequest();
// //记录请求信息
// Log.LogBuilder builder = Log.builder();
// // 记录Operation的注解信息
// if (method.isAnnotationPresent(Operation.class)) {
// Operation operation = method.getAnnotation(Operation.class);
// builder.summary(operation.summary()).description(operation.description());
// }
// String urlStr = request.getRequestURL().toString();
// builder.basePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()))
// .uri(request.getRequestURI())
// .url(request.getRequestURL().toString())
// .browser(IpUtil.getBrowser(request).getBrowser().getName())
// .ip(IpUtil.getLongIpAddr(request))
// .method(request.getMethod())
// .parameter(JSONUtil.parse(getParameter(method, joinPoint.getArgs())).toString())
// .result(JSONUtil.parse(result).toString())
// .spendTime((int) (endTime - startTime))
// .startTime(LocalDateTimeUtil.of(startTime));
// Log log = builder.build();
// LOGGER.info(log.toString());
// logService.save(log);
// return result;
// }
//
// /**
// * 根据方法和传入的参数获取请求参数
// *
// * @param method 方法
// * @param args 传入参数
// * @return 请求参数
// */
// private Object getParameter(Method method, Object[] args) {
// List<Object> argsList = new ArrayList<>();
// Parameter[] parameters = method.getParameters();
// for (int i = 0; i < parameters.length; i++) {
// // 将RequestBody注解修饰的参数作为请求参数
// RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
// if (requestBody != null) {
// argsList.add(args[i]);
// }
// // 将RequestParam注解修饰的参数作为请求参数
// RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
// if (requestParam != null) {
// Map<String, Object> map = new HashMap<>(16);
// String key = parameters[i].getName();
// if (StrUtil.isNotBlank(requestParam.value())) {
// key = requestParam.value();
// }
// map.put(key, args[i]);
// argsList.add(map);
// }
// }
// if (argsList.size() == 0) {
// return null;
// } else if (argsList.size() == 1) {
// return argsList.get(0);
// } else {
// return argsList;
// }
// }
}
package com.zq.im.config;
import cn.hutool.core.lang.Assert;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.SerializationException;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import reactor.util.annotation.Nullable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* Redis配置类
* </p>
*
* @author chenhao
* @since 2022/10/12 20:57
*/
@Slf4j
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig extends CachingConfigurerSupport {
/**
* RedisTemplate的配置
*/
@Bean(name = "redisTemplate")
@ConditionalOnMissingBean(name = "redisTemplate")
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用FastJson2重写序列化
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
// value值的序列化采用FastJson2重写的序列化
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
/**
* 缓存配置
* 设置 redis 数据默认过期时间,默认3小时
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
FastJson2JsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.
SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(3));
return configuration;
}
/**
* 自定义缓存key生成策略,默认将使用该策略
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
Map<String, Object> container = new HashMap<>(4);
Class<?> targetClassClass = target.getClass();
// 类地址
container.put("class", targetClassClass.toGenericString());
// 方法名称
container.put("methodName", method.getName());
// 包名称
container.put("package", targetClassClass.getPackage());
// 参数列表
for (int i = 0; i < params.length; i++) {
container.put(String.valueOf(i), params[i]);
}
// 转为JSON字符串
String jsonString = JSON.toJSONString(container);
// 做SHA256 Hash计算,得到一个SHA256摘要作为Key
return DigestUtil.sha256Hex(jsonString);
};
}
/**
* Redis的异常处理,当Redis发生异常时,打印日志
*/
@Bean
@Override
@SuppressWarnings(value = {"all"})
public CacheErrorHandler errorHandler() {
log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.error("Redis occur handleCacheClearError:", e);
}
};
}
}
/**
* Value 序列化
*
* @param <T>
*/
class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private final Class<T> clazz;
public FastJson2JsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length == 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
}
}
/**
* 重写序列化器
*/
class StringRedisSerializer implements RedisSerializer<Object> {
private final Charset charset;
StringRedisSerializer() {
this(StandardCharsets.UTF_8);
}
private StringRedisSerializer(Charset charset) {
Assert.notNull(charset, "Charset must not be null!");
this.charset = charset;
}
@Override
public String deserialize(byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public @Nullable byte[] serialize(Object object) {
String string = JSON.toJSONString(object);
if (StringUtils.isBlank(string)) {
return null;
}
string = string.replace("\"", "");
return string.getBytes(charset);
}
}
\ No newline at end of file
package com.zq.im.config;
import com.zq.im.interceptor.ApiMethodInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* <p>
* 注册拦截器
* </p>
*
* @author chenhao
* @since 2023/11/9
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Lazy
@Resource
private ApiMethodInterceptor apiMethodInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiMethodInterceptor).addPathPatterns("/api/v1/**");
}
}
package com.zq.im.config.feign;
import feign.RequestInterceptor;
import feign.codec.Encoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
/**
* @author wilmiam
* @since 2021-07-09 10:34
*/
@Configuration
public class FeignConfig {
/**
* 转发请求头
*/
private static final List<String> FORWARD_HEADERS = Arrays.asList(
"AUTHORIZATION",
"X-FORWARDED-FOR",
"X-FORWARDED-PROTO",
"X-FORWARDED-PORT",
"X-FORWARDED-HOST",
"FORWARDED",
"PROXY-CLIENT-IP",
"WL-PROXY-CLIENT-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR",
"X-REAL-IP",
"HOST"
);
private final ObjectFactory<HttpMessageConverters> messageConverters;
public FeignConfig(ObjectFactory<HttpMessageConverters> messageConverters) {
this.messageConverters = messageConverters;
}
public static HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return null;
}
return requestAttributes.getRequest();
}
/**
* 解决fein远程调用丢失请求头
*/
@Bean
public RequestInterceptor requestInterceptor() {
return template -> {
HttpServletRequest request = getRequest();
if (request == null) {
return;
}
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 不要设置content-length
if ("content-length".equals(name)) {
continue;
}
if (FORWARD_HEADERS.contains(name.toUpperCase())) {
String values = request.getHeader(name);
template.header(name, values);
}
}
}
};
}
@Bean
public Encoder feignEncoder() {
return new FeignSpringFormEncoder(new SpringEncoder(messageConverters));
}
}
package com.zq.im.config.feign;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.form.ContentType;
import feign.form.FormEncoder;
import feign.form.MultipartFormContentProcessor;
import feign.form.spring.SpringManyMultipartFilesWriter;
import feign.form.spring.SpringSingleMultipartFileWriter;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Map;
/**
* feign 多文件上传配置
*
* @author Lander
*/
public class FeignSpringFormEncoder extends FormEncoder {
public FeignSpringFormEncoder() {
this(new Default());
}
public FeignSpringFormEncoder(Encoder delegate) {
super(delegate);
MultipartFormContentProcessor processor = (MultipartFormContentProcessor) this.getContentProcessor(ContentType.MULTIPART);
processor.addFirstWriter(new SpringSingleMultipartFileWriter());
processor.addFirstWriter(new SpringManyMultipartFilesWriter());
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
if (bodyType != null && bodyType.equals(MultipartFile[].class)) {
MultipartFile[] file = (MultipartFile[]) object;
if (file != null) {
Map<String, Object> data = Collections.singletonMap(file.length == 0 ? "" : file[0].getName(), object);
super.encode(data, MAP_STRING_WILDCARD, template);
return;
}
} else if (bodyType != null && bodyType.equals(MultipartFile.class)) {
MultipartFile file = (MultipartFile) object;
if (file != null) {
Map<String, Object> data = Collections.singletonMap(file.getName(), object);
super.encode(data, MAP_STRING_WILDCARD, template);
return;
}
}
super.encode(object, bodyType, template);
}
}
package com.zq.im.config.mybatis;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;
import java.util.List;
/**
* <p>
* 批量插入SQL注入器
* </P>
*
* @author chenhao
* @since 2022/10/12 20:57
*/
public class InsertBatchSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
// 获取MybatisPlus的自带方法
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
// 添加自定义批量插入方法,名称为insertBatchSomeColumn
methodList.add(new InsertBatchSomeColumn());
return methodList;
}
}
\ No newline at end of file
package com.zq.im.config.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* <p>
* Mybatis-plus的自动填充处理处理
* 注意是对象属性名不是表的字段名
* </p>
*
* @author chenhao
* @since 2022/10/12 20:57
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入数据时填充创建和修改时间
*/
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
/**
* 修改数据时更新修改时间
*/
@Override
public void updateFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
package com.zq.im.config.mybatis;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* <p>
* Mybatis-Plus配置类
* </p>
*
* @author chenhao
* @since 2022/10/12 20:57
*/
@Configuration
@EnableTransactionManagement
@MapperScan("com.zq.im.modules.system.dao")
public class MybatisPlusConfig {
/**
* 插件配置
* 1. 防全表更新与删除插件
* 2. 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 防全表更新与删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// 分页插件,指定数据库为MYSQL
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
/**
* 自定义批量插入 SQL 注入器
*/
@Bean
public InsertBatchSqlInjector insertBatchSqlInjector() {
return new InsertBatchSqlInjector();
}
}
package com.zq.im.constant;
/**
* API响应码
*
* @author wilmiam
* @since 2021-07-14 11:37
*/
public enum ApiCodeEnum {
/**
* 成功
*/
SUCCESS("200", "成功"),
/**
* 未知错误
*/
UNKNOWN_ERROR("100", "未知错误"),
/**
* 版本号错误
*/
VERSION_ERROR("101", "版本号错误"),
/**
* 调用方法不存在
*/
METHOD_ERROR("102", "调用方法不存在"),
/**
* 调用方法异常
*/
METHOD_HANDLER_ERROR("103", "调用方法异常"),
/**
* 传递参数异常
*/
PARAM_ERROR("104", "传递参数异常"),
/**
* IP黑名单拦截
*/
IP_BLACK("105", "IP黑名单拦截"),
/**
* API服务维护中
*/
SERVER_MAINTAIN("106", "API服务维护中"),
/**
* 签名校验失败
*/
CHECK_SIGN_VALID_ERROR("107", "签名校验失败"),
/**
* 服务不可用
*/
SERVICE_NOT_AVAILABLE("108", "服务不可用"),
/**
* 业务处理失败
*/
BUSINESS_ERROR("400", "业务处理失败"),
/**
* 登陆验证失败
*/
LOGIN_VALID_ERROR("401", "登陆验证失败"),
/**
* 服务器繁忙
*/
SERVER_ERROR("500", "服务器繁忙"),
;
private final String code;
private final String msg;
ApiCodeEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String code() {
return code;
}
public String msg() {
return msg;
}
}
package com.zq.im.constant;
/**
* API版本
*
* @author wilmiam
* @since 2021-07-14 11:37
*/
public enum ApiVersionEnum {
/**
* V1
*/
v1,
/**
* V2
*/
v2
}
package com.zq.im.constant;
/**
* <p>
* Token相关常量类
* </p>
*
* @author chenhao
*/
public class TokenConstant {
/**
* Token的密钥
*/
public static final String TOKEN_SECRET = "HWTJ";
/**
* Token在请求头部的名称
*/
public static final String TOKEN_HEADER = "Authorization";
/**
* Token的前缀
*/
public static final String TOKEN_PREFIX = "ApiToken.";
/**
* header的头部加密算法声明
*/
public static final String TOKEN_ALG = "HMAC512";
/**
* header的头部Token类型
*/
public static final String TOKEN_TYP = "JWT";
/**
* Token的签发者
*/
public static final String TOKEN_ISSUER = "HWTJ";
/**
* Token面向的主体
*/
public static final String TOKEN_SUBJECT = "api";
/**
* Token的接收方
*/
public static final String TOKEN_AUDIENCE = "api";
/**
* Token解析后当前用户的信息
*/
public static final String ADMIN_TOKEN = "current_token";
}
package com.zq.processing.exception; package com.zq.im.exception;
/** /**
* 业务错误 * 业务错误
* *
* @author wilmiam * @author chenhao
* @since 2021-07-09 17:58 * @since 2022/10/12 20:57
*/ */
public class BusinessException extends RuntimeException { public class BusinessException extends RuntimeException {
......
package com.zq.im.interceptor;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONUtil;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zq.im.constant.ApiCodeEnum;
import com.zq.im.modules.api.form.ApiForm;
import com.zq.im.modules.api.service.ApiManage;
import com.zq.im.modules.api.utils.ApiUtils;
import com.zq.im.utils.IpUtil;
import com.zq.im.utils.RedisUtil;
import com.zq.im.utils.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 方法拦截器
* 主要是校验Token信息,校验成功线后程保存Token信息
*
* @author chenhao
* @since 2023-11-09
*/
@Slf4j
@Component
public class ApiMethodInterceptor implements HandlerInterceptor {
@Autowired
private RedisUtil redisUtil;
/**
* 当前环境
*/
@Value("${spring.profiles.active:product}")
private String active;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String ip = IpUtil.getIpAddr(request);
ApiForm form = ServletUtil.toBean(request, ApiForm.class, true);
log.info("from [{}] to [{}]", ip, request.getRequestURI() + "#" + form.getMethod());
// 判断是否是可以在未登录状态下执行的方法名
boolean notValid = ApiManage.isNotValid(form.getMethod());
if (notValid) {
return true;
}
// 验证认证信息
String apiToken = redisUtil.getStr(TokenUtil.getApiTokenKey(form.getAppId()));
if (apiToken == null) {
response.setStatus(HttpStatus.HTTP_BAD_REQUEST);
ServletUtil.write(response, JSONUtil.toJsonStr(ApiUtils.of(form, ApiCodeEnum.LOGIN_VALID_ERROR)), "application/json;charset=utf-8");
return false;
}
DecodedJWT decodedJWT;
try {
decodedJWT = TokenUtil.parse(apiToken);
} catch (Exception e) {
response.setStatus(HttpStatus.HTTP_BAD_REQUEST);
ServletUtil.write(response, JSONUtil.toJsonStr(ApiUtils.of(form, ApiCodeEnum.LOGIN_VALID_ERROR)), "application/json;charset=utf-8");
return false;
}
TokenUtil.setUserContext(decodedJWT);
return true;
}
}
package com.zq.im.modules.api.controller;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.zq.im.constant.ApiCodeEnum;
import com.zq.im.constant.ApiVersionEnum;
import com.zq.im.exception.BusinessException;
import com.zq.im.modules.api.form.ApiForm;
import com.zq.im.modules.api.form.ApiResp;
import com.zq.im.modules.api.service.ApiManage;
import com.zq.im.modules.api.utils.ApiUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
/**
* API方法总入口
*
* @author chenhao
* @since 2023/11/9
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1")
public class ApiV1 {
private final ApiManage apiManage;
@PostMapping(value = "/action")
public ResponseEntity<ApiResp> action(ApiForm form, HttpServletResponse response) {
// 设置版本号
form.setVersion(ApiVersionEnum.v1.name());
//解析业务参数
if (!form.parseBizContent()) {
ApiResp apiResp = ApiUtils.of(form, ApiCodeEnum.PARAM_ERROR);
return ResponseEntity.badRequest().body(apiResp);
}
// 调用接口方法
ApiResp resp;
try {
// 调用接口方法
resp = apiManage.action(form);
} catch (BusinessException e) {
// 捕抓业务发生的异常
log.error("v1调用方法业务发生错误", e);
resp = ApiUtils.fail(form, e.getMessage());
} catch (Exception e) {
// 转化指定异常为来自或者包含指定异常
BusinessException businessException = ExceptionUtil.convertFromOrSuppressedThrowable(e, BusinessException.class, true);
if (businessException == null) {
// 不是业务异常才打印错误日志
log.error("v1调用方法发生错误", e);
}
if (businessException != null) {
resp = ApiUtils.fail(form, businessException.getMessage());
} else {
resp = ApiUtils.of(form, ApiCodeEnum.METHOD_HANDLER_ERROR);
}
}
// 没有数据输出空
if (resp == null) {
resp = ApiUtils.of(form, ApiCodeEnum.UNKNOWN_ERROR);
}
// 如果接口返回数据为输入流,则返回输入
Object data = resp.getData();
if (data instanceof InputStream) {
InputStream inputStream = (InputStream) data;
ServletUtil.write(response, inputStream);
return null;
}
return resp.isSuccess() ? ResponseEntity.ok(resp) : ResponseEntity.badRequest().body(resp);
}
}
package com.zq.im.modules.api.form;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zq.im.exception.BusinessException;
import com.zq.im.modules.api.utils.ApiUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.TreeMap;
/**
* api 基础form
*
* @author wilmiam
* @since 2021-07-13 09:59
*/
@Slf4j
@Data
public class ApiForm {
private String appId;
private String method;// 请求方法
private String sign;// 签名
private String timestamp;// 时间戳, 单位: 毫秒
private String version;// 接口版本
private String apiNo;// 接口码
private String bizContent;// 请求业务参数
private JSONObject bizContentJson;// 请求业务的json对象
private MultipartFile file; // 上传文件用
private MultipartFile[] fileList; // 上传文件用
/**
* 解析业务参数,将业务参数转为JSON字符串
*/
public boolean parseBizContent() {
try {
// 业务参数不为空,便给参数进行BASE64解码
if (StrUtil.isNotBlank(bizContent)) {
bizContent = ApiUtils.decode(bizContent, "BASE64");
}
// 参数为空就设置为空
if (bizContent == null) {
bizContent = "";
}
// 参数转换为字符串
bizContentJson = JSON.parseObject(bizContent);
if (bizContentJson == null) {
bizContentJson = new JSONObject();
}
return true;
} catch (Exception e) {
log.error("bizContent解析失败:{}", e.getMessage());
return false;
}
}
public <T> T toBean(Class<T> beanClass) {
JSONObject contentJson = getContentJson();
return contentJson.toJavaObject(beanClass);
}
/**
* 获取参数
*/
public String getString(String key) {
String value = getContentJson().getString(key);
return value == null ? "" : value;
}
/**
* 获取业务参数的JSON字符串
*/
public JSONObject getContentJson() {
if (bizContentJson != null) {
return bizContentJson;
}
parseBizContent();
return bizContentJson;
}
/**
* 加签,其实就是拼接加上Key
*/
public String getSignStr(String key) {
// 参数进行排序
TreeMap<String, String> treeMap = getSignTreeMap();
// 参数拼接
StringBuilder src = new StringBuilder();
for (Map.Entry<String, String> entry : treeMap.entrySet()) {
src.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
src.append("key=").append(key);
return src.toString();
}
/**
* 获取签名所需参数,并进行字典序排序
*/
public TreeMap<String, String> getSignTreeMap() {
TreeMap<String, String> treeMap = new TreeMap<>();
treeMap.put("apiNo", this.apiNo);
treeMap.put("timestamp", this.timestamp);
treeMap.put("method", this.method);
String bizContent = StrUtil.isBlank(this.bizContent) ? "" : this.bizContent;
treeMap.put("bizContent", bizContent);
// 如果有文件传入,并设置文件的MD5
if (file != null) {
InputStream inputStream;
try {
inputStream = file.getInputStream();
} catch (IOException e) {
log.error("获取文件流发生错误", e);
throw new BusinessException("获取文件流发生错误:" + e.getMessage());
}
treeMap.put("fileMd5", SecureUtil.md5(inputStream));
}
// 如果有文件列表,拼接所有文件的MD5
if (ArrayUtil.isNotEmpty(fileList)) {
StringBuilder md5 = new StringBuilder();
for (MultipartFile multipartFile : fileList) {
InputStream inputStream;
try {
inputStream = multipartFile.getInputStream();
} catch (IOException e) {
log.error("获取文件流发生错误", e);
throw new BusinessException("获取文件流发生错误:" + e.getMessage());
}
md5.append(SecureUtil.md5(inputStream));
}
treeMap.put("fileMd5", md5.toString());
}
return treeMap;
}
}
package com.zq.im.modules.api.form;
import com.zq.im.constant.ApiCodeEnum;
import lombok.Getter;
/**
* @author wilmiam
* @since 2022-11-04 11:29
*/
@Getter
public class ApiResp {
private String apiNo = "";
private String code = ApiCodeEnum.SUCCESS.code();
private String msg = ApiCodeEnum.SUCCESS.msg();
private Long timestamp = System.currentTimeMillis();
private Object data;
public ApiResp(ApiForm form) {
this.apiNo = form.getApiNo() == null ? "" : form.getApiNo();
}
public ApiResp(ApiCodeEnum apiCodeEnum) {
this.code = apiCodeEnum.code();
this.msg = apiCodeEnum.msg();
}
public ApiResp(ApiForm form, ApiCodeEnum apiCodeEnum) {
this.code = apiCodeEnum.code();
this.msg = apiCodeEnum.msg();
this.apiNo = form.getApiNo();
}
public ApiResp setApiNo(String apiNo) {
this.apiNo = apiNo;
return this;
}
public ApiResp setCode(String code) {
this.code = code;
return this;
}
public ApiResp setMsg(String msg) {
this.msg = msg;
return this;
}
public ApiResp setTimestamp(Long timestamp) {
this.timestamp = timestamp;
return this;
}
public ApiResp setData(Object data) {
this.data = data;
return this;
}
public Boolean isSuccess() {
return this.getCode().equals(ApiCodeEnum.SUCCESS.code());
}
}
package com.zq.im.modules.api.service;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zq.im.constant.ApiCodeEnum;
import com.zq.im.modules.api.form.ApiForm;
import com.zq.im.modules.api.form.ApiResp;
import com.zq.im.modules.api.utils.ApiUtils;
import com.zq.im.modules.api.utils.ReflectionUtils;
import com.zq.im.utils.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author wilmiam
* @since 2022/10/28 16:02
*/
@Slf4j
@Service
public class ApiManage {
/**
* 允许用户未登录状态下执行的方法名
*/
private static final List<String> ALLOW_METHOD = Arrays.asList("getApiToken");
private static final List<String> METHOD_LIST;
static {
METHOD_LIST = methodList();
}
public static List<String> methodList() {
List<String> methodList = new ArrayList<>();
Method[] methods = IApiLogic.class.getMethods();
for (Method method : methods) {
Class<?>[] params = method.getParameterTypes();
if (params.length == 1 && (params[0] == ApiForm.class)) {
methodList.add(method.getName());
}
}
return methodList;
}
public static boolean isNotValid(String method) {
return ALLOW_METHOD.contains(method);
}
public ApiResp action(ApiForm form) throws Exception {
// 如果接口方法不存在,则返回异常
if (!METHOD_LIST.contains(form.getMethod())) {
return ApiUtils.of(form, ApiCodeEnum.METHOD_ERROR);
}
// 校验签名,校验失败直接返回
ApiResp authResp = signValid(form);
if (!authResp.isSuccess()) {
return authResp;
}
IApiLogic apiLogic = ApiUtils.getApiLogic(form);
// 调用接口方法,利用反射更简洁
Object result = ReflectionUtils.invokeMethod(apiLogic, form.getMethod(), new Class<?>[]{ApiForm.class}, new Object[]{form});
return (ApiResp) result;
}
/**
* 签名验证
*/
public ApiResp signValid(ApiForm form) {
// 白名单存在该方法,直接放行
boolean contains = ALLOW_METHOD.contains(form.getMethod());
if (contains) {
return ApiUtils.success(form);
}
// 获取Token中的sessionKey
DecodedJWT apiToken = TokenUtil.getUserContext();
String sessionKey = TokenUtil.getSessionkey(apiToken);
// 通过sessionKey签名,并进行校验
String sign = ApiUtils.getSign(form.getSignStr(sessionKey == null ? "" : sessionKey));
if (!sign.equals(form.getSign())) {
return ApiUtils.of(form, ApiCodeEnum.CHECK_SIGN_VALID_ERROR);
}
return ApiUtils.success(form);
}
}
package com.zq.im.modules.api.service;
import com.zq.im.modules.api.form.ApiForm;
import com.zq.im.modules.api.form.ApiResp;
/**
* API通用接口
*
* @author wilmiam
* @since 2022-11-04 11:35
*/
public interface IApiCommon {
ApiResp signValid(ApiForm form);
}
package com.zq.im.modules.api.service;
import com.zq.im.modules.api.form.ApiForm;
import com.zq.im.modules.api.form.ApiResp;
/**
* API接口类
* 这里的注解没有用,只是为了注释说明方法
*
* @author wilmiam
* @since 2022-11-01 09:53
*/
public interface IApiLogic extends IApiCommon {
/**
* 获取第三方调用token
*/
ApiResp getApiToken(ApiForm form);
/**
* 图片质量检测
*/
ApiResp detection(ApiForm form);
//
// /**
// * 图片纠偏
// *
// * @param form
// * @return
// */
// ApiResp deskew(ApiForm form);
//
// /**
// * 去黑边
// *
// * @param form
// * @return
// */
// ApiResp removeBlack(ApiForm form);
//
// /**
// * 图片灰度化
// *
// * @param form
// * @return
// */
// ApiResp gray(ApiForm form);
//
// /**
// * 图片边缘检测
// *
// * @param form
// * @return
// */
// ApiResp canny(ApiForm form);
//
// /**
// * 图片弯曲矫正
// *
// * @param form
// * @return
// */
// ApiResp correct(ApiForm form);
//
// /**
// * 图片优化
// *
// * @param form
// * @return
// */
// ApiResp imageOptimization(ApiForm form);
}
package com.zq.im.modules.api.service.impl;
import com.zq.im.modules.api.form.ApiForm;
import com.zq.im.modules.api.form.ApiResp;
import com.zq.im.modules.api.service.IApiLogic;
import com.zq.im.modules.api.utils.ApiUtils;
import com.zq.im.modules.im.req.ImageReq;
import com.zq.im.modules.im.service.ImgProcService;
import com.zq.im.modules.system.service.ApiUserInfoService;
import com.zq.im.utils.AssertUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* API实现类
*
* @author wilmiam
* @since 2022-10-31 17:16
*/
@Slf4j
@Component
public class ApiV1Logic extends BaseApiLogic implements IApiLogic {
@Autowired
ApiUserInfoService apiUserInfoService;
@Autowired
private ImgProcService imgProcService;
@Override
public ApiResp getApiToken(ApiForm form) {
String appId = form.getString("appId");
String appSecret = form.getString("appSecret");
AssertUtils.hasText(appId, "缺少参数appId");
AssertUtils.hasText(appSecret, "缺少参数secret");
return ApiUtils.toApiResp(form, apiUserInfoService.getApiToken(appId, appSecret));
}
@Override
public ApiResp detection(ApiForm form) {
ImageReq req = form.toBean(ImageReq.class);
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ApiUtils.toApiResp(form, imgProcService.detection(req));
}
//
// @ApiMethod(name = "图片纠偏", filterRequestField = "fileContent")
// @Override
// public ApiResp deskew(ApiForm form) {
// DetectionVo vo = form.toBean(DetectionVo.class);
// AssertUtils.hasText(vo.getFileContent(), "缺少文件内容");
// AssertUtils.hasText(vo.getFilename(), "缺少文件名");
// return ApiUtils.toApiResp(form, imgProcService.apiService(vo, "imageCorrection"));
// }
//
// @ApiMethod(name = "去黑边", filterRequestField = "fileContent")
// @Override
// public ApiResp removeBlack(ApiForm form) {
// DetectionVo vo = form.toBean(DetectionVo.class);
// AssertUtils.hasText(vo.getFileContent(), "缺少文件内容");
// AssertUtils.hasText(vo.getFilename(), "缺少文件名");
// return ApiUtils.toApiResp(form, imgProcService.apiService(vo, "removeBlack"));
// }
//
// @ApiMethod(name = "图片灰度化", filterRequestField = "fileContent")
// @Override
// public ApiResp gray(ApiForm form) {
// DetectionVo vo = form.toBean(DetectionVo.class);
// AssertUtils.hasText(vo.getFileContent(), "缺少文件内容");
// AssertUtils.hasText(vo.getFilename(), "缺少文件名");
// return ApiUtils.toApiResp(form, imgProcService.apiService(vo, "gray"));
// }
//
// @ApiMethod(name = "图片边缘检测", filterRequestField = "fileContent")
// @Override
// public ApiResp canny(ApiForm form) {
// DetectionVo vo = form.toBean(DetectionVo.class);
// AssertUtils.hasText(vo.getFileContent(), "缺少文件内容");
// AssertUtils.hasText(vo.getFilename(), "缺少文件名");
// return ApiUtils.toApiResp(form, imgProcService.apiService(vo, "gray"));
// }
//
// @ApiMethod(name = "图片弯曲矫正", filterRequestField = "fileContent")
// @Override
// public ApiResp correct(ApiForm form) {
// DetectionVo vo = form.toBean(DetectionVo.class);
// AssertUtils.hasText(vo.getFileContent(), "缺少文件内容");
// AssertUtils.hasText(vo.getFilename(), "缺少文件名");
// return ApiUtils.toApiResp(form, imgProcService.apiService(vo, "correct"));
// }
//
// @ApiMethod(name = "图片优化", filterRequestField = "fileContent")
// @Override
// public ApiResp imageOptimization(ApiForm form) {
// DetectionVo vo = form.toBean(DetectionVo.class);
// AssertUtils.hasText(vo.getFileContent(), "缺少文件内容");
// AssertUtils.hasText(vo.getFilename(), "缺少文件名");
// return ApiUtils.toApiResp(form, imgProcService.imageOptimization(vo, "imageOptimization"));
// }
}
package com.zq.im.modules.api.service.impl;
import com.zq.im.modules.api.service.IApiLogic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* API实现类
*
* @author wilmiam
* @since 2022-10-31 17:16
*/
@Slf4j
@Component
public class ApiV2Logic extends ApiV1Logic implements IApiLogic {
}
package com.zq.im.modules.api.service.impl;
import com.zq.im.modules.api.form.ApiForm;
import com.zq.im.modules.api.form.ApiResp;
import com.zq.im.modules.api.service.IApiLogic;
import com.zq.im.modules.api.utils.ApiUtils;
/**
* API基础类
* <p>
* 2016年11月15日 下午9:48:27
*
* @author wilmiam
*/
public abstract class BaseApiLogic implements IApiLogic {
@Override
public ApiResp signValid(ApiForm form) {
return ApiUtils.success(form);
}
}
package com.zq.im.modules.api.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.zq.im.constant.ApiCodeEnum;
import com.zq.im.modules.api.form.ApiForm;
import com.zq.im.modules.api.form.ApiResp;
import com.zq.im.modules.api.service.IApiLogic;
import com.zq.im.modules.api.service.impl.ApiV1Logic;
import com.zq.im.modules.api.service.impl.ApiV2Logic;
import com.zq.im.modules.system.vo.ResultVo;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author wilmiam
* @since 2021-07-22 10:18
*/
@Component
public class ApiUtils {
private static final Map<String, IApiLogic> MAP = new HashMap<>();
public ApiUtils(ApiV1Logic apiV1Logic, ApiV2Logic apiV2Logic) {
addApi("v1", apiV1Logic);
addApi("v2", apiV2Logic);
}
public static void addApi(String version, IApiLogic apiLogic) {
MAP.put(version, apiLogic);
}
public static IApiLogic getApiLogic(ApiForm form) {
return MAP.get(form.getVersion());
}
/**
* 获取成功响应
*/
public static ApiResp success(ApiForm form) {
return new ApiResp(form, ApiCodeEnum.SUCCESS);
}
/**
* 获取失败的响应
*/
public static ApiResp fail(ApiForm form, String errMsg) {
ApiResp apiResp = new ApiResp(form, ApiCodeEnum.BUSINESS_ERROR);
apiResp.setMsg(errMsg);
return apiResp;
}
public static ApiResp of(ApiForm form, ApiCodeEnum apiCodeEnum) {
return new ApiResp(form, apiCodeEnum);
}
public static ApiResp toApiResp(ApiForm form, ResultVo resultVo) {
ApiResp apiResp = new ApiResp(form);
if (resultVo.isSuccess()) {
apiResp.setData(resultVo.getData() == null ? "" : resultVo.getData());
} else {
return apiResp.setCode(String.valueOf(resultVo.getErrCode())).setMsg(resultVo.getErrMsg());
}
return apiResp;
}
public static ApiResp toApiResp(ApiForm form, Object data) {
ApiResp apiResp = new ApiResp(form);
apiResp.setData(data);
return apiResp;
}
/**
* 解码
*/
public static String decode(String params, String encryptType) {
if (StrUtil.isBlank(params)) {
return "";
}
// params = EncryptUtils.urlDecode(params, "UTF-8");
if ("RSA".equals(encryptType)) {
params = EncryptUtils.rsaDecodeByPrivateKey(params, RsaUtils.privateKey);
} else {
params = Base64.decodeStr(params);
}
return params;
}
/**
* 编码
* <p>
* 2017年3月15日 下午1:49:09
*
* @param params
* @return
*/
public static String encode(String params, String encryptType) {
if (StrUtil.isBlank(params)) {
return "";
}
if ("RSA".equals(encryptType)) {
params = EncryptUtils.rsaDecodeByPrivateKey(params, RsaUtils.publicKey);
} else {
params = EncryptUtils.base64Decode(params);
}
params = EncryptUtils.urlEncode(params, "UTF-8");
return params;
}
/**
* 获取验证sign
* <p>
* 2017年3月15日 下午3:14:27
*
* @param content
* @return
*/
public static String getSign(String content) {
return SecureUtil.md5(content);
}
}
package com.zq.im.modules.api.utils;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.server.ServerErrorException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* 加密工具类
*
* @author wilmiam
* @since 2021-07-09 17:55
*/
public class EncryptUtils {
private static final Logger log = LoggerFactory.getLogger(EncryptUtils.class);
/**
* 字符编码
*/
private static final String CHARSET_NAME = "UTF-8";
/**
* 用来将字节转换成 16 进制表示的字符
*/
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static final int HEX_RADIUS = 16;
/**
* MD5 加密字符串
*/
public static String md5Encrypt(final String sourceStr) {
return md5Encrypt(sourceStr, CHARSET_NAME);
}
/**
* MD5加密字符串
*/
public static String md5Encrypt(final String sourceStr, String coding) {
if (StrUtil.isBlank(sourceStr)) {
return null;
}
byte[] sourceByte;
try {
sourceByte = sourceStr.getBytes(coding);
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage(), e);
sourceByte = sourceStr.getBytes();
}
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
md.update(sourceByte);
// MD5 的计算结果是一个 128 位的长整数,用字节表示就是 16 个字节
final byte[] tmp = md.digest();
// 每个字节用 16 进制表示的话,使用两个字符,所以表示成 16 进制需要 32 个字符
// 16 << 1 相当于 16*2
final char[] str = new char[16 << 1];
// 表示转换结果中对应的字符位置
int k = 0;
// 从第一个字节开始,对 MD5 的每一个字节转换成 16 进制字符的转换
for (int i = 0; i < HEX_RADIUS; i++) {
// 取第 i 个字节
final byte byte0 = tmp[i];
// 取字节中高 4 位的数字转换, >>> 为逻辑右移,将符号位一起右移
str[k++] = HEX_DIGITS[byte0 >>> 4 & 0xf];
// 取字节中低 4 位的数字转换
str[k++] = HEX_DIGITS[byte0 & 0xf];
}
// 换后的结果转换为字符串
return new String(str);
} catch (final NoSuchAlgorithmException e) {
log.error(">> MD5加密错误", e);
}
return null;
}
/**
* 私钥解密(注意:encodedData是base64加密后的才行)
*/
public static String rsaDecodeByPrivateKey(String encodedData, String privateKey) {
try {
byte[] decodedData = RsaUtils.decryptByPrivateKey(Base64.getDecoder().decode(encodedData), privateKey);
return new String(decodedData, CHARSET_NAME);
} catch (Exception e) {
log.error("rsaDecodeByPrivateKey error", e);
return null;
}
}
/**
* 使用BASE64进行解密
*
* @param base64Str base64字符串
* @return 解密字符串
*/
public static String base64Decode(final String base64Str) {
byte[] decode = Base64.getDecoder().decode(base64Str);
try {
return new String(decode, CHARSET_NAME);
} catch (UnsupportedEncodingException e) {
log.error("base64 decode error", e);
throw new ServerErrorException("编码失败");
}
}
/**
* 对字符串进行URL编码
*
* @param sourceStr 需要编码的字符串
* @param enc 编码格式
* @return
*/
public static String urlEncode(String sourceStr, String enc) {
try {
return URLEncoder.encode(sourceStr, enc);
} catch (UnsupportedEncodingException e) {
log.error("urlEncode error", e);
return null;
}
}
}
/**
* Copyright 2015-2025 .
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zq.im.modules.api.utils;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 方法类
*
* @author syh
*/
@Slf4j
public class ReflectionUtils {
/**
* 循环向上转型, 获取对象的 DeclaredMethod
*
* @param object : 子类对象
* @param methodName : 父类中的方法名
* @param parameterTypes : 父类中的方法参数类型
* @return 父类中的方法对象
*/
private static Method getDeclaredMethod(Object object, String methodName, Class<?>... parameterTypes) {
Method method;
Class<?> clazz = object.getClass();
while (clazz != Object.class) {
try {
method = clazz.getDeclaredMethod(methodName, parameterTypes);
return method;
} catch (Exception e) {
// 未获取到就往上找
clazz = clazz.getSuperclass();
}
}
return null;
}
/**
* 直接调用对象方法, 而忽略修饰符(private, protected, default)
*
* @param object : 子类对象
* @param methodName : 父类中的方法名
* @param parameterTypes : 父类中的方法参数类型
* @param parameters : 父类中的方法参数
* @return 父类中方法的执行结果
*/
public static Object invokeMethod(Object object, String methodName, Class<?>[] parameterTypes, Object[] parameters) throws InvocationTargetException, IllegalAccessException {
// 根据 对象、方法名和对应的方法参数 通过反射 调用上面的方法获取 Method 对象
Method method = getDeclaredMethod(object, methodName, parameterTypes);
if (method == null) {
log.warn(object.getClass() + "未获取到" + methodName + "方法!");
return null;
}
// 抑制Java对方法进行检查,主要是针对私有方法而言
method.setAccessible(true);
// 调用object 的 method 所代表的方法,其方法的参数是 parameters
return method.invoke(object, parameters);
}
/**
* 循环向上转型, 获取对象的 DeclaredField
*
* @param object : 子类对象
* @param fieldName : 父类中的属性名
* @return 父类中的属性对象
*/
private static Field getDeclaredField(Object object, String fieldName) {
Field field;
Class<?> clazz = object.getClass();
while (clazz != Object.class) {
try {
field = clazz.getDeclaredField(fieldName);
return field;
} catch (Exception e) {
// 未获取到就往上找
clazz = clazz.getSuperclass();
}
}
return null;
}
/**
* 直接设置对象属性值, 忽略 private/protected 修饰符, 也不经过 setter
*
* @param object : 子类对象
* @param fieldName : 父类中的属性名
* @param value : 将要设置的值
*/
public static void setFieldValue(Object object, String fieldName, Object value) {
// 根据 对象和属性名通过反射 调用上面的方法获取 Field对象
Field field = getDeclaredField(object, fieldName);
if (field == null) {
System.err.println(object.getClass() + "未获取到" + fieldName + "属性!");
return;
}
// 抑制Java对其的检查
field.setAccessible(true);
try {
// 将 object 中 field 所代表的值 设置为 value
field.set(object, value);
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 直接读取对象的属性值, 忽略 private/protected 修饰符, 也不经过 getter
*
* @param object : 子类对象
* @param fieldName : 父类中的属性名
* @return : 父类中的属性值
*/
public static Object getFieldValue(Object object, String fieldName) {
// 根据 对象和属性名通过反射 调用上面的方法获取 Field对象
Field field = getDeclaredField(object, fieldName);
if (field == null) {
System.err.println(object.getClass() + "未获取到" + fieldName + "属性!");
return null;
}
// 抑制Java对其的检查
field.setAccessible(true);
try {
// 获取 object 中 field 所代表的属性值
return field.get(object);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
package com.zq.im.modules.api.utils;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* @author wilmiam
* @since 2021-07-09 17:56
*/
public class RsaUtils {
/**
* 获取公钥的key
*/
public static final String PUBLIC_KEY = "RSAPublicKey";
/**
* 获取私钥的key
*/
public static final String PRIVATE_KEY = "RSAPrivateKey";
/**
* 加密算法RSA
*/
private static final String KEY_ALGORITHM = "RSA";
/**
* 签名算法
*/
private static final String SIGNATURE_ALGORITHM = "MD5withRSA";
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;
public static String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMTaoTuj4LU9WAMeaVWcpwgyMcdvAA3JRDcG0+pWG086c+WPdSggNNZaVw3szCKOTnWvNc6SoqjpjpbQpC57uag67VzKWLmsZoF6SXjCARyRaEkfK2VRHTfkVpyd8FF16gebVhhyjbkkja9JVEekwqOGzfmnfSKfx5LwvcSxdiSrAgMBAAECgYBZgGHQQPk4zhRHDrurnhbfhhrV5yTqH7kxH5yYLeAqzJPHKsuEm+gKEXcFMMW7bGJF5YycSFVGYTJgZapQLBbDlrZdM8SjxsNyrCKI3v3LNQDsqs5x751HfFVvTme7wroN/uJszUaQJPagEUckMkHvpv7XWoL3Wbz7oy94T3ENoQJBAPAhj2yo9jRZv5JRlYy5BFwqYpxSWqGjzr2k2YiGqB9/y/pDpDx3q42FaBcOlOOeh/My+iVNLcezqgj+U0yx79ECQQDR3Oz9ckCm2q7AMCLFmp9cs4dws6DLim35awOvLIXtm/Z1tRNyuLqb6g2VM4O/QiTu64F3+ljKiOWHAcgxqUe7AkEArTuYy4vs6gFhCb6fg8Cp24+cSifDSF7zM67sW+jA+tBoJ+iKYDD46wS1/gQ/9yGT9Cfve998ylfbr9dB4s9vMQJAOH/uHd3gogtF+N/8vI6AUQjUcfcqVyIRsZCqEUM/W1Ud6VqyvbQWKVu+BGk2EwvPvbMRzCdOOFja0pocN6KHeQJAQPwlDo1IHJI5F60CvfIG8dIwtGexMnd4NNHQ4KH0peK9jUCPkkpW0No5ZEtKNgfdPk23erfyx5cGqocvnoUpoQ==";
public static String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDE2qE7o+C1PVgDHmlVnKcIMjHHbwANyUQ3BtPqVhtPOnPlj3UoIDTWWlcN7Mwijk51rzXOkqKo6Y6W0KQue7moOu1cyli5rGaBekl4wgEckWhJHytlUR035FacnfBRdeoHm1YYco25JI2vSVRHpMKjhs35p30in8eS8L3EsXYkqwIDAQAB";
/**
* 生成密钥对(公钥和私钥)
*
* @return
* @throws Exception
*/
public static Map<String, Key> genKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String, Key> keyMap = new HashMap<>(4);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
/**
* 用私钥对信息生成数字签名
*
* @param data 已加密数据
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static String sign(byte[] data, String privateKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateK);
signature.update(data);
return Base64.getEncoder().encodeToString(signature.sign());
}
/**
* 校验数字签名
*
* @param data 已加密数据
* @param publicKey 公钥(BASE64编码)
* @param sign 数字签名
* @return
* @throws Exception
*/
public static boolean verify(byte[] data, String publicKey, String sign)
throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey publicK = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicK);
signature.update(data);
return signature.verify(Base64.getDecoder().decode(sign));
}
/**
* <p>
* 公钥解密
* </p>
*
* @param encryptedData 已加密数据
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey) throws Exception {
return byteTransfer(getCipherByRsaPublicKey(Cipher.DECRYPT_MODE, publicKey), encryptedData, MAX_ENCRYPT_BLOCK);
}
/**
* <p>
* 公钥加密
* </p>
*
* @param data 源数据
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception {
return byteTransfer(getCipherByRsaPublicKey(Cipher.ENCRYPT_MODE, publicKey), data, MAX_ENCRYPT_BLOCK);
}
private static Cipher getCipherByRsaPublicKey(int mode, String publicKey) throws GeneralSecurityException {
byte[] keyBytes = Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
// 对数据加密
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicK);
return cipher;
}
/**
* <P>
* 私钥解密
* </p>
*
* @param encryptedData 已加密数据
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) throws Exception {
return byteTransfer(getCipherByRsaPrivateKey(Cipher.DECRYPT_MODE, privateKey), encryptedData, MAX_DECRYPT_BLOCK);
}
/**
* 私钥加密
*
* @param data 源数据
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, String privateKey) throws Exception {
return byteTransfer(getCipherByRsaPrivateKey(Cipher.ENCRYPT_MODE, privateKey), data, MAX_ENCRYPT_BLOCK);
}
private static Cipher getCipherByRsaPrivateKey(int mode, String key) throws GeneralSecurityException {
byte[] keyBytes = Base64.getDecoder().decode(key);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(mode, privateK);
return cipher;
}
/**
* <p>
* 获取私钥
* </p>
*
* @param keyMap 密钥对
* @return
*/
public static String getPrivateKey(Map<String, Key> keyMap) {
Key key = keyMap.get(PRIVATE_KEY);
return Base64.getEncoder().encodeToString(key.getEncoded());
}
/**
* 获取公钥
*
* @param keyMap 密钥对
* @return
*/
public static String getPublicKey(Map<String, Key> keyMap) {
Key key = keyMap.get(PUBLIC_KEY);
return Base64.getEncoder().encodeToString(key.getEncoded());
}
/**
* 根据cipher的模式, 对数据进行分段加密/解密
*
* @param cipher
* @param data
* @return
* @throws GeneralSecurityException,IOException
*/
private static byte[] byteTransfer(Cipher cipher, byte[] data, int maxBlock) throws GeneralSecurityException, IOException {
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 根据cipher的模式, 对数据分段加密/解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > maxBlock) {
cache = cipher.doFinal(data, offSet, maxBlock);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * maxBlock;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
}
package com.zq.im.modules.api.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Objects;
/**
* @author wilmiam
* @since 2022/1/27 12:30
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MethodVo {
private String name;
private String service;
private String value;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MethodVo methodVo = (MethodVo) o;
return Objects.equals(value, methodVo.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
package com.zq.im.modules.im.controller;
import com.zq.im.modules.im.req.ImageReq;
import com.zq.im.modules.im.service.ImgProcService;
import com.zq.im.modules.system.vo.ResultVo;
import com.zq.im.utils.AssertUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 图片处理相关接口
* </p>
*
* @ClassName ImgApi
* @Author chenhao
* @Date 2022/10/27 17:55
*/
@Slf4j
@RestController
@RequestMapping("/interface/api/imgproc")
@RequiredArgsConstructor
@Api(tags = "图片处理相关接口")
public class ImgProcController {
private final ImgProcService service;
@ApiOperation("检测图片信息")
@PostMapping(value = "/detection")
public ResultVo detection(@RequestBody ImageReq req) {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.detection(req));
}
@ApiOperation("图片纠偏")
@PostMapping(value = "/deskew")
public ResultVo deskew(@RequestBody ImageReq req) {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.apiService(req, "imageCorrection"));
}
@ApiOperation("去黑边")
@PostMapping(value = "/removeBlack")
public ResultVo removeBlack(@RequestBody ImageReq req) {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.apiService(req, "removeBlack"));
}
@ApiOperation("图片灰度化")
@PostMapping(value = "/gray")
public ResultVo gray(@RequestBody ImageReq req) {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.apiService(req, "gray"));
}
@ApiOperation("图片边缘检测")
@PostMapping(value = "/canny")
public ResultVo canny(@RequestBody ImageReq req) {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.apiService(req, "canny"));
}
@ApiOperation("图片弯曲矫正")
@PostMapping(value = "/correct")
public ResultVo correct(@RequestBody ImageReq req) {
AssertUtils.hasText(req.getFileContent(), "缺少文件内容");
AssertUtils.hasText(req.getFilename(), "缺少文件名");
return ResultVo.success(service.apiService(req, "correct"));
}
}
package com.zq.im.modules.im.feign;
import com.zq.im.modules.im.req.ImageReq;
import com.zq.im.modules.im.vo.DetectionResVo;
import com.zq.im.modules.system.vo.ResultVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
/**
* @author chenhao
* @since 2022/4/24 10:26
*/
@FeignClient(name = "IMGPROC-SERVER", path = "/imgproc/v1")
public interface ImgprocFeign {
/**
* 图片质量检测接口
*/
@PostMapping(value = "/detection", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
ResultVo<DetectionResVo> detection(ImageReq req);
}
package com.zq.im.modules.im.req;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <p>
* 图片请求类
* </p>
*
* @author chenhao
* @since 2023/11/9
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImageReq {
/**
* 图片名称
*/
String filename;
/**
* 图片的Base64字符串
*/
String fileContent;
}
package com.zq.im.modules.im.service;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zq.im.exception.BusinessException;
import com.zq.im.modules.im.feign.ImgprocFeign;
import com.zq.im.modules.im.req.ImageReq;
import com.zq.im.modules.im.vo.DetectionResVo;
import com.zq.im.modules.system.vo.ResultVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
/**
* <p>
* 图片处理Service
* </p>
*
* @author chenhao
* @since 2022/10/27 17:56
*/
@RequiredArgsConstructor
@Service
@Slf4j
public class ImgProcService {
private final String URL = "http://147.1.5.135:8510/imgproc/v1/";
private final String TEST_URL = "http://localhost:8510/imgproc/v1/";
@Autowired
ImgprocFeign feign;
public Object detection(ImageReq req) {
ResultVo<DetectionResVo> resultVo = feign.detection(req);
return resultVo.getData();
}
public Object detection1(ImageReq req) {
String url = URL + "detection";
HttpRequest request = HttpUtil.createPost(url)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(req))
.timeout(120 * 1000);
String body;
try (HttpResponse response = request.execute()) {
body = response.body();
}
JSONObject obj = JSONUtil.parseObj(body);
Integer code = obj.getInt("code");
if (code != null && code != 200) {
throw new BusinessException(obj.getStr("msg"));
}
return obj.get("data");
}
public Object apiService(ImageReq req, String method) {
String url = URL + method;
String dir = IdUtil.fastSimpleUUID();
File file = new File(dir + "/" + req.getFilename());
Base64.decodeToFile(req.getFileContent(), file);
HttpRequest request = HttpUtil.createPost(url)
.form("file", file)
.timeout(120 * 1000);
String body;
try (HttpResponse response = request.execute()) {
body = response.body();
}
JSONObject obj = JSONUtil.parseObj(body);
Integer code = obj.getInt("code");
if (code != null && code != 200) {
throw new BusinessException(obj.getStr("msg"));
}
// 删除目录及文件
FileUtil.del(file.getAbsoluteFile().getParent());
return obj.get("data");
}
}
package com.zq.processing.vo; package com.zq.im.modules.im.vo;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
...@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor; ...@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
* *
* </p> * </p>
* *
* @author chenhao * @author yww
* @since 2023/4/21 * @since 2023/4/21
*/ */
@NoArgsConstructor @NoArgsConstructor
......
package com.zq.im.modules.system.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zq.im.modules.system.entity.ApiUserInfo;
import org.springframework.stereotype.Repository;
/**
* 第三方接口调用用户信息(ApiUserInfo)表数据库访问层
*
* @author makejava
* @since 2021-12-09 17:20:44
*/
@Repository
public interface ApiUserInfoDao extends BaseMapper<ApiUserInfo> {
}
package com.zq.im.modules.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 第三方接口调用用户信息(ApiUserInfo)实体类
*
* @author makejava
* @since 2021-12-09 17:20:44
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "api_user_info")
public class ApiUserInfo {
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_UUID)
private String id;
/**
* APP_ID
*/
private String appId;
/**
* APP_SECRET
*/
private String appSecret;
/**
* 申请人
*/
private String applyer;
/**
* 状态
*/
private Integer state;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
package com.zq.im.modules.system.service;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.zq.im.modules.system.dao.ApiUserInfoDao;
import com.zq.im.modules.system.entity.ApiUserInfo;
import com.zq.im.utils.AssertUtils;
import com.zq.im.utils.RedisUtil;
import com.zq.im.utils.TokenUtil;
import com.zq.im.utils.UuidUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author chenhao
*/
@Slf4j
@Service
public class ApiUserInfoService {
@Autowired
ApiUserInfoDao apiUserInfoDao;
@Resource
RedisUtil redisUtil;
public static void main(String[] args) {
// 创建用户
ApiUserInfo apiUserInfo = new ApiUserInfo();
apiUserInfo.setId(UuidUtils.uuidNoDash());
apiUserInfo.setAppId(UuidUtils.uuidNoDash());
apiUserInfo.setAppSecret(UuidUtils.uuidNoDash());
apiUserInfo.setApplyer("测试用户");
apiUserInfo.setState(1);
System.out.println(DateUtil.now());
System.out.println(JSONUtil.toJsonStr(apiUserInfo));
}
public Map<String, Object> getApiToken(String appId, String appSecret) {
ApiUserInfo apiAppInfo = apiUserInfoDao.selectOne(
Wrappers.lambdaQuery(ApiUserInfo.builder().appId(appId).appSecret(appSecret).build())
);
AssertUtils.notNull(apiAppInfo, "账号或密码错误");
AssertUtils.isTrue(apiAppInfo.getState() == 1, "账号未激活");
String sessionKey = IdUtil.simpleUUID();
String apiToken = TokenUtil.createToken(apiAppInfo.getAppId(), sessionKey);
log.debug(">> [session-key]:{}", sessionKey);
redisUtil.setStr(TokenUtil.getApiTokenKey(apiAppInfo.getAppId()), apiToken, 120L);
Map<String, Object> data = new HashMap<>();
data.put("userId", apiAppInfo.getId());
data.put("username", apiAppInfo.getApplyer());
data.put("key", sessionKey);
data.put("expireTime", TimeUnit.MINUTES.toSeconds(TokenUtil.API_TOKEN_EXPIRE_MINUTES));
data.put("unit", "秒");
return data;
}
}
package com.zq.processing.utils; package com.zq.im.modules.system.vo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
......
package com.zq.processing.utils; package com.zq.im.utils;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.zq.processing.exception.BusinessException; import com.zq.im.exception.BusinessException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
......
package com.zq.im.utils;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>
* IP相关工具类
* </p>
*
* @author chenhao
* @since 2022/10/12 20:57
*/
public class IpUtil {
public static long getLongIpAddr(HttpServletRequest request) {
String ip = getIpAddr(request);
return ipv4ToLong(ip);
}
/**
* 获取客户端IP
*
* @return IP
*/
public static String getIpAddr() {
return getIpAddr(getRequest());
}
/**
* 获取客户端IP
*
* @param request 请求
* @return IP
*/
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
// 判断x-forwarded-for是否有IP
String ip = request.getHeader("x-forwarded-for");
if (isNotUnknown(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
// 判断Proxy-Client-IP是否有IP
if (isNotUnknown(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
// 判断X-Forwarded-For是否有IP
if (isNotUnknown(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
// 判断WL-Proxy-Client-IP是否有IP
if (isNotUnknown(ip)) {
ip = request.getHeader("X-Real-IP");
}
// 判断X-Real-IP是否有IP
if (isNotUnknown(ip)) {
ip = request.getRemoteAddr();
}
// 若是IP最后为0:0:0:0:0:0:0:1,就是本地回环地址,否则第一个非unknown的IP地址就是真实地址
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
}
/**
* 从多级反向代理中获得第一个非unknown IP地址
*
* @param ip 获得的IP地址
* @return 第一个非unknown IP地址
*/
@SuppressWarnings("all")
private static String getMultistageReverseProxyIp(String ip) {
// 多级反向代理检测
if (ip != null && ip.indexOf(",") > 0) {
final String[] ips = ip.trim().split(",");
for (String subIp : ips) {
if (isNotUnknown(subIp)) {
ip = subIp;
break;
}
}
}
return ip;
}
/**
* 检测给定字符串是否为未知,多用于检测HTTP请求相关
*
* @param checkString 被检测的字符串
* @return 是否未知
*/
private static boolean isNotUnknown(String checkString) {
return StrUtil.isNotBlank(checkString) && StrUtil.isNotEmpty(checkString) && !"unknown".equalsIgnoreCase(checkString);
}
/**
* 获取UserAgent信息对象
*
* @param request 请求
* @return UserAgent
*/
public static UserAgent getBrowser(HttpServletRequest request) {
return UserAgentUtil.parse(request.getHeader("User-Agent"));
}
/**
* 获取当前请求
*
* @return 当前请求
*/
private static HttpServletRequest getRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
/**
* 将ipv4地址转换为long类型
* 等同于MySQL的inet_aton方法
*
* @param ipStr ipv4地址
* @return long类型的ipv地址
*/
public static long ipv4ToLong(String ipStr) {
String regex = "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(ipStr);
if (matcher.matches()) {
long addr = 0;
for (int i = 1; i <= 4; i++) {
addr |= Long.parseLong(matcher.group(i)) << 8 * (4 - i);
}
return addr;
} else {
throw new RuntimeException("ipv4地址格式出错!");
}
}
/**
* 将long类型的ip转为ipv4字符串
* 等同于MySQL的inet_ntoa方法
*
* @param longIp long类型IP
* @return IPV4字符串
*/
public static String longToIpv4(long longIp) {
StringBuilder sb = new StringBuilder();
// 直接右移24位
sb.append(longIp >> 24 & 0xFF);
sb.append(CharUtil.DOT);
// 将高8位置0,然后右移16位
sb.append(longIp >> 16 & 0xFF);
sb.append(CharUtil.DOT);
sb.append(longIp >> 8 & 0xFF);
sb.append(CharUtil.DOT);
sb.append(longIp & 0xFF);
return sb.toString();
}
}
package com.zq.im.utils;
import org.apache.logging.log4j.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* 基于ThreadLocal的线程相关上下文帮助类, 用于在同一线程下传递变量.
* </p>
*
* @author zq
* @since 2023/11/8
*/
public class ThreadContextUtil {
private static ThreadLocal<Map<String, Object>> threadLocalMap = ThreadLocal.withInitial(HashMap::new);
/**
* 根据指定的key获取当前线程相关的变量值
*
* @param key 变量的key
* @param <T> 变量值的具体类型
* @return 若无此key对应的变量值, 则返回{@code null}
* @throws ClassCastException 若接收此返回值的变量类型与上下文保存的值的实际类型不匹配, 则抛出异常
*/
@SuppressWarnings("unchecked")
public static <T> T get(String key) {
return (T) threadLocalMap.get().get(key);
}
/**
* 根据指定的key获取当前线程相关的变量值, 若为{@code null}则返回指定的默认值
*
* @param key 变量的key
* @param defaultValue 默认值
* @param <T> 变量值的具体类型
* @return 若无此key对应的变量值, 则返回defaultValue
* @throws ClassCastException 若接收此返回值的变量类型与上下文保存的值的实际类型不匹配, 则抛出异常
*/
public static <T> T get(String key, T defaultValue) {
T value = get(key);
return value == null ? defaultValue : value;
}
/**
* 设置线程相关上下文的变量值
*
* @param key 变量的key
* @param value 变量值
*/
public static void set(String key, Object value) {
threadLocalMap.get().put(key, value);
}
/**
* 删除指定key的变量
*
* @param key 变量的key
*/
public static void remove(String key) {
threadLocalMap.get().remove(key);
}
/**
* 清除当前线程相关的上下文
*/
public static void close() {
threadLocalMap.remove();
}
/**
* Don't let anyone instantiate this class
*/
private void ThreadContext() {
}
}
package com.zq.im.utils;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.zq.im.constant.TokenConstant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* Token工具类
* 1. Header,记录令牌类型和签名算法
* 2. payload,携带用户信息
* (1) iss(issuer), 签发者
* (2) sub(subject), 面向的主体
* (3) aud(audience), 接收方
* (4) nbf(notBefore), 开始生效生效时间戳
* (5) exp(expiresAt), 过期时间戳
* (6) iat(issuedAt ), 签发时间
* (7) jti(jwtId), 唯一标识
* 3. signature, 签名,防止Token被篡改
* </p>
*
* @ClassName TokenUtil
* @Author yww
* @Date 2022/10/15 14:31
*/
public class TokenUtil {
/**
* 用户名
*/
public static final String USER_NAME = "userName";
/**
* sessionKey
*/
public static final String SESSIONKEY = "sessionKey";
/**
* 过期时长
*/
public static final long API_TOKEN_EXPIRE_MINUTES = 120L;
/**
* 线程变量的键值
*/
private static final String APP_TOKEN_CONTEXT_KEY = "app-token";
/**
* 生成Token
* 当前使用HMAC512的加密算法
*
* @return Token
*/
public static String createToken(String username, String sessionKey) {
// 设置Token头部(不设置也会默认有这两个值)
Map<String, Object> header = new HashMap<String, Object>(2) {
private static final long serialVersionUID = 1L;
{
put("alg", TokenConstant.TOKEN_ALG);
put("typ", TokenConstant.TOKEN_TYP);
}
};
// 设置负载
Map<String, Object> payload = new HashMap<String, Object>(1) {
private static final long serialVersionUID = 1L;
{
put(USER_NAME, username);
put(SESSIONKEY, sessionKey);
}
};
// 过期时间三小时
long now = DateUtil.current();
long exp = now + 1000 * API_TOKEN_EXPIRE_MINUTES * 60;
return JWT.create()
// 设置header
.withHeader(header)
// 设置payload
.withIssuer(TokenConstant.TOKEN_ISSUER)
.withSubject(TokenConstant.TOKEN_SUBJECT)
.withAudience(TokenConstant.TOKEN_AUDIENCE)
.withNotBefore(new Date(now))
.withExpiresAt(new Date(exp))
.withIssuedAt(new Date(now))
.withJWTId(IdUtil.fastSimpleUUID())
.withPayload(payload)
// 签名
.sign(Algorithm.HMAC512(TokenConstant.TOKEN_SECRET));
}
/**
* 解析Token
* 当前使用HMAC512的加密算法
*
* @param token Token
*/
public static DecodedJWT parse(String token) {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512(TokenConstant.TOKEN_SECRET)).build();
return jwtVerifier.verify(token);
}
/**
* 获取用户名
*
* @param decoded 解析后的Token
* @return 用户名
*/
public static String getUserName(DecodedJWT decoded) {
return decoded.getClaim(USER_NAME).asString();
}
/**
* 获取用户名
*
* @param decoded 解析后的Token
* @return 用户名
*/
public static String getSessionkey(DecodedJWT decoded) {
return decoded.getClaim(SESSIONKEY).asString();
}
public static String getApiTokenKey(String tokenKey) {
return TokenConstant.TOKEN_PREFIX + tokenKey;
}
public static DecodedJWT getUserContext() {
return ThreadContextUtil.get(APP_TOKEN_CONTEXT_KEY);
}
public static void setUserContext(DecodedJWT decodedJWT) {
if (decodedJWT != null) {
ThreadContextUtil.set(APP_TOKEN_CONTEXT_KEY, decodedJWT);
}
}
public static void main(String[] args) {
// String token = createToken("ec3f54b398e64fbeb9ed00bb0144a91b",
// "4c0d6c2a99e541c8a1affc3c8d8dcddb");
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhcGkiLCJhdWQiOiJhcGkiLCJuYmYiOjE2OTk0OTc1MTIsInNlc3Npb25LZXkiOiI5MjhkMmM0YjE2NTY0YTY3OTkwZjY4ZmE2ZmExZmIxNSIsImlzcyI6IkhXVEoiLCJleHAiOjE2OTk1MDQ3MTIsInVzZXJOYW1lIjoiZWMzZjU0YjM5OGU2NGZiZWI5ZWQwMGJiMDE0NGE5MWIiLCJpYXQiOjE2OTk0OTc1MTIsImp0aSI6ImNmYTBhNzNmYmM0NTRlYmE4NzNlNmY3ODA3ZDE1Y2QyIn0.0ZoVFzZWFYmUEwbymR7M75hbt6RMDlpyWlh9NzmXgwATaPZGsGCFSou5RvjQWa2vKFOOdUV065pXX3uyWJ3c0Q";
DecodedJWT decodedJWT = parse(token);
setUserContext(decodedJWT);
DecodedJWT jwt = getUserContext();
System.out.println(jwt.getToken());
System.out.println(getSessionkey(decodedJWT));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment