2026-05-12 11:06:16 +08:00
|
|
|
|
package com.multictrl.common.utils;
|
|
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.collection.CollUtil;
|
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
|
|
|
import cn.hutool.json.JSONArray;
|
|
|
|
|
|
import cn.hutool.json.JSONObject;
|
|
|
|
|
|
import cn.hutool.json.JSONUtil;
|
2026-06-10 17:47:16 +08:00
|
|
|
|
import com.alibaba.fastjson2.JSON;
|
|
|
|
|
|
import com.alibaba.fastjson2.filter.NameFilter;
|
2026-05-12 11:06:16 +08:00
|
|
|
|
import com.drew.imaging.jpeg.JpegMetadataReader;
|
|
|
|
|
|
import com.drew.imaging.jpeg.JpegProcessingException;
|
|
|
|
|
|
import com.drew.imaging.mp4.Mp4MetadataReader;
|
|
|
|
|
|
import com.drew.metadata.Directory;
|
|
|
|
|
|
import com.drew.metadata.Metadata;
|
|
|
|
|
|
import com.drew.metadata.Tag;
|
|
|
|
|
|
import com.multictrl.common.exception.ExceptionUtils;
|
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
|
import org.apache.commons.io.FileUtils;
|
|
|
|
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
|
|
|
import java.math.RoundingMode;
|
|
|
|
|
|
import java.security.MessageDigest;
|
|
|
|
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
|
|
|
import java.text.ParseException;
|
|
|
|
|
|
import java.text.SimpleDateFormat;
|
|
|
|
|
|
import java.util.Date;
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
import java.util.Locale;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通用工具
|
|
|
|
|
|
*
|
|
|
|
|
|
* @author Sdy
|
|
|
|
|
|
* @since 1.0.0 2026/4/24
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Slf4j
|
|
|
|
|
|
public class Utils {
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 线程休息n毫秒
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static void sleep(long millis) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Thread.sleep(millis);
|
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
|
log.error("线程睡眠异常:{}", ExceptionUtils.getErrorStackTrace(e));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
* 驼峰转下划线
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static JSONObject beanToSnakeJson(Object obj) {
|
|
|
|
|
|
JSONObject camelJson = JSONUtil.parseObj(obj);
|
|
|
|
|
|
JSONObject snakeJson = new JSONObject();
|
|
|
|
|
|
for (String key : camelJson.keySet()) {
|
|
|
|
|
|
String snakeKey = StrUtil.toUnderlineCase(key); // 驼峰转下划线
|
|
|
|
|
|
Object value = camelJson.get(key);
|
|
|
|
|
|
// 递归处理嵌套 JSONObject 或 JSONArray
|
|
|
|
|
|
if (value instanceof JSONObject) {
|
|
|
|
|
|
value = beanToSnakeJson(value); // 递归转换
|
|
|
|
|
|
} else if (value instanceof JSONArray) {
|
|
|
|
|
|
JSONArray arr = new JSONArray();
|
|
|
|
|
|
for (Object item : (JSONArray) value) {
|
|
|
|
|
|
if (item instanceof JSONObject) {
|
|
|
|
|
|
arr.add(beanToSnakeJson(item));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
arr.add(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
value = arr;
|
|
|
|
|
|
}
|
|
|
|
|
|
snakeJson.set(snakeKey, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
return snakeJson;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 17:47:16 +08:00
|
|
|
|
/*
|
|
|
|
|
|
* 阿里FastJson2驼峰转下划线
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static com.alibaba.fastjson2.JSONObject beanToSnakeJsonFastJson(Object o) {
|
|
|
|
|
|
NameFilter snakeCaseFilter = (object, name, value)
|
|
|
|
|
|
-> name.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
|
|
|
|
|
|
return com.alibaba.fastjson2.JSONObject.parseObject(JSON.toJSONString(o, snakeCaseFilter));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 11:06:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取最后两个字符
|
|
|
|
|
|
* "a/b/c/d" "c/d"
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static String getLastTwoSegments(String str, String separator) {
|
|
|
|
|
|
if (StrUtil.isBlank(str)) return str;
|
|
|
|
|
|
// 按分隔符切割
|
|
|
|
|
|
List<String> parts = StrUtil.split(str, separator);
|
|
|
|
|
|
if (parts.size() <= 2) {
|
|
|
|
|
|
return str; // 不足两段则原样返回
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 取最后两段
|
|
|
|
|
|
List<String> lastTwo = parts.subList(parts.size() - 2, parts.size());
|
|
|
|
|
|
return CollUtil.join(lastTwo, String.valueOf(separator));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 文件转md5
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static String md5File(String path) {
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
|
try {
|
|
|
|
|
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
|
|
|
|
|
md.update(FileUtils.readFileToByteArray(new File(path)));
|
|
|
|
|
|
byte[] b = md.digest();
|
|
|
|
|
|
int d;
|
|
|
|
|
|
for (byte value : b) {
|
|
|
|
|
|
d = value;
|
|
|
|
|
|
if (d < 0) {
|
|
|
|
|
|
d = value & 0xff;
|
|
|
|
|
|
// 与上一行效果等同
|
|
|
|
|
|
// i += 256;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d < 16) {
|
|
|
|
|
|
sb.append("0");
|
|
|
|
|
|
}
|
|
|
|
|
|
sb.append(Integer.toHexString(d));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (NoSuchAlgorithmException | IOException e) {
|
|
|
|
|
|
log.error("md5 fail", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
return sb.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取文本的md5
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static String md5Txt(String context) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
|
|
|
|
|
byte[] buffer = context.getBytes();
|
|
|
|
|
|
md.update(buffer, 0, buffer.length);
|
|
|
|
|
|
byte[] digest = md.digest();
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
|
for (byte b : digest) {
|
|
|
|
|
|
sb.append(String.format("%02x", b));
|
|
|
|
|
|
}
|
|
|
|
|
|
return sb.toString();
|
|
|
|
|
|
} catch (NoSuchAlgorithmException exception) {
|
|
|
|
|
|
log.error("md5 fail", exception);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取图片的Windows XP Keywords
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static String getImageWindowsXpKeywords(String mediaPath) {
|
|
|
|
|
|
|
|
|
|
|
|
Metadata metadata;
|
|
|
|
|
|
try {
|
|
|
|
|
|
metadata = JpegMetadataReader.readMetadata(new File(mediaPath));
|
|
|
|
|
|
for (Directory exif : metadata.getDirectories()) {
|
|
|
|
|
|
for (Tag tag : exif.getTags()) {
|
|
|
|
|
|
if ("Windows XP Keywords".equals(tag.getTagName())) {
|
|
|
|
|
|
return tag.getDescription();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (JpegProcessingException | IOException e) {
|
|
|
|
|
|
log.error("error", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static long getImageTime(String mediaPath) {
|
|
|
|
|
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
|
|
|
|
|
|
Metadata metadata;
|
|
|
|
|
|
try {
|
|
|
|
|
|
metadata = JpegMetadataReader.readMetadata(new File(mediaPath));
|
|
|
|
|
|
|
|
|
|
|
|
for (Directory exif : metadata.getDirectories()) {
|
|
|
|
|
|
for (Tag tag : exif.getTags()) {
|
|
|
|
|
|
if ("Date/Time".equals(tag.getTagName())) {
|
|
|
|
|
|
return sdf.parse(tag.getDescription()).getTime();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (JpegProcessingException | IOException e) {
|
|
|
|
|
|
log.error("error", e);
|
|
|
|
|
|
} catch (ParseException e) {
|
|
|
|
|
|
throw new RuntimeException(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static long getVideoTime(String videoPath) {
|
|
|
|
|
|
Metadata metadata;
|
|
|
|
|
|
try {
|
|
|
|
|
|
metadata = Mp4MetadataReader.readMetadata(new File(videoPath));
|
|
|
|
|
|
for (Directory exif : metadata.getDirectories()) {
|
|
|
|
|
|
for (Tag tag : exif.getTags()) {
|
|
|
|
|
|
if ("Creation Time".equals(tag.getTagName())) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy",
|
|
|
|
|
|
Locale.US);
|
|
|
|
|
|
Date d = sdf.parse(tag.getDescription());
|
|
|
|
|
|
return d.getTime();
|
|
|
|
|
|
} catch (ParseException ignored) {
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
|
log.error("error", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 格式化double,保留指定位小数(四舍五入)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param value 原始数值
|
|
|
|
|
|
* @param scale 小数位数
|
|
|
|
|
|
* @return 格式化后的字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static String format(double value, int scale) {
|
|
|
|
|
|
if (Double.isNaN(value) || Double.isInfinite(value)) {
|
|
|
|
|
|
return "0";
|
|
|
|
|
|
}
|
|
|
|
|
|
BigDecimal bd = BigDecimal.valueOf(value);
|
|
|
|
|
|
bd = bd.setScale(scale, RoundingMode.HALF_UP);
|
|
|
|
|
|
return bd.stripTrailingZeros().toPlainString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 字节数格式化为人性化字符串(自动选择B, KB, MB, GB, TB)
|
|
|
|
|
|
* 1024进制
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param bytes 字节数
|
|
|
|
|
|
* @return 如 "1.45 GB"
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static String formatBytes(long bytes) {
|
|
|
|
|
|
if (bytes < 0) {
|
|
|
|
|
|
return "0 B";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (bytes < 1024) {
|
|
|
|
|
|
return bytes + " B";
|
|
|
|
|
|
}
|
|
|
|
|
|
int exp = (int) (Math.log(bytes) / Math.log(1024));
|
|
|
|
|
|
char pre = "KMGTPE".charAt(exp - 1);
|
|
|
|
|
|
double result = bytes / Math.pow(1024, exp);
|
|
|
|
|
|
return format(result, 2) + " " + pre + "B";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|