headers) {
+ try (HttpResponse response = HttpRequest.get(url)
+ .addHeaders(headers)
+ .execute()) {
+ return response.body();
+ }
+ }
+
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/io/FileUtils.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/io/FileUtils.java
new file mode 100644
index 0000000..51aa860
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/io/FileUtils.java
@@ -0,0 +1,84 @@
+package cd.casic.framework.commons.util.io;
+
+import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import lombok.SneakyThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+
+/**
+ * 文件工具类
+ *
+ * @author mianbin
+ */
+public class FileUtils {
+
+ /**
+ * 创建临时文件
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @param data 文件内容
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile(String data) {
+ File file = createTempFile();
+ // 写入内容
+ FileUtil.writeUtf8String(data, file);
+ return file;
+ }
+
+ /**
+ * 创建临时文件
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @param data 文件内容
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile(byte[] data) {
+ File file = createTempFile();
+ // 写入内容
+ FileUtil.writeBytes(data, file);
+ return file;
+ }
+
+ /**
+ * 创建临时文件,无内容
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile() {
+ // 创建文件,通过 UUID 保证唯一
+ File file = File.createTempFile(IdUtil.simpleUUID(), null);
+ // 标记 JVM 退出时,自动删除
+ file.deleteOnExit();
+ return file;
+ }
+
+ /**
+ * 生成文件路径
+ *
+ * @param content 文件内容
+ * @param originalName 原始文件名
+ * @return path,唯一不可重复
+ */
+ public static String generatePath(byte[] content, String originalName) {
+ String sha256Hex = DigestUtil.sha256Hex(content);
+ // 情况一:如果存在 name,则优先使用 name 的后缀
+ if (StrUtil.isNotBlank(originalName)) {
+ String extName = FileNameUtil.extName(originalName);
+ return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
+ }
+ // 情况二:基于 content 计算
+ return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
+ }
+
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/io/IoUtils.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/io/IoUtils.java
new file mode 100644
index 0000000..7879f16
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/io/IoUtils.java
@@ -0,0 +1,28 @@
+package cd.casic.framework.commons.util.io;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.io.InputStream;
+
+/**
+ * IO 工具类,用于 {@link IoUtil} 缺失的方法
+ *
+ * @author mianbin
+ */
+public class IoUtils {
+
+ /**
+ * 从流中读取 UTF8 编码的内容
+ *
+ * @param in 输入流
+ * @param isClose 是否关闭
+ * @return 内容
+ * @throws IORuntimeException IO 异常
+ */
+ public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
+ return StrUtil.utf8Str(IoUtil.read(in, isClose));
+ }
+
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/json/JsonUtils.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/json/JsonUtils.java
new file mode 100644
index 0000000..4e98c81
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/json/JsonUtils.java
@@ -0,0 +1,202 @@
+package cd.casic.framework.commons.util.json;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JSON 工具类
+ *
+ * @author mianbin
+ */
+@Slf4j
+public class JsonUtils {
+
+ private static ObjectMapper objectMapper = new ObjectMapper();
+
+ static {
+ objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
+ objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
+ }
+
+ /**
+ * 初始化 objectMapper 属性
+ *
+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
+ *
+ * @param objectMapper ObjectMapper 对象
+ */
+ public static void init(ObjectMapper objectMapper) {
+ JsonUtils.objectMapper = objectMapper;
+ }
+
+ @SneakyThrows
+ public static String toJsonString(Object object) {
+ return objectMapper.writeValueAsString(object);
+ }
+
+ @SneakyThrows
+ public static byte[] toJsonByte(Object object) {
+ return objectMapper.writeValueAsBytes(object);
+ }
+
+ @SneakyThrows
+ public static String toJsonPrettyString(Object object) {
+ return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+ }
+
+ public static T parseObject(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(text, clazz);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static T parseObject(String text, String path, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ JsonNode treeNode = objectMapper.readTree(text);
+ JsonNode pathNode = treeNode.path(path);
+ return objectMapper.readValue(pathNode.toString(), clazz);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static T parseObject(String text, Type type) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 将字符串解析成指定类型的对象
+ * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
+ * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
+ *
+ * @param text 字符串
+ * @param clazz 类型
+ * @return 对象
+ */
+ public static T parseObject2(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ return JSONUtil.toBean(text, clazz);
+ }
+
+ public static T parseObject(byte[] bytes, Class clazz) {
+ if (ArrayUtil.isEmpty(bytes)) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(bytes, clazz);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", bytes, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static T parseObject(String text, TypeReference typeReference) {
+ try {
+ return objectMapper.readValue(text, typeReference);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
+ *
+ * @param text 字符串
+ * @param typeReference 类型引用
+ * @return 指定类型的对象
+ */
+ public static T parseObjectQuietly(String text, TypeReference typeReference) {
+ try {
+ return objectMapper.readValue(text, typeReference);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ public static List parseArray(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return new ArrayList<>();
+ }
+ try {
+ return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static List parseArray(String text, String path, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ JsonNode treeNode = objectMapper.readTree(text);
+ JsonNode pathNode = treeNode.path(path);
+ return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static JsonNode parseTree(String text) {
+ try {
+ return objectMapper.readTree(text);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static JsonNode parseTree(byte[] text) {
+ try {
+ return objectMapper.readTree(text);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static boolean isJson(String text) {
+ return JSONUtil.isTypeJSON(text);
+ }
+
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/NumberSerializer.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/NumberSerializer.java
new file mode 100644
index 0000000..42373db
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/NumberSerializer.java
@@ -0,0 +1,37 @@
+package cd.casic.framework.commons.util.json.databind;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
+
+import java.io.IOException;
+
+/**
+ * Long 序列化规则
+ *
+ * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题
+ *
+ * @author 星语
+ */
+@JacksonStdImpl
+public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
+
+ private static final long MAX_SAFE_INTEGER = 9007199254740991L;
+ private static final long MIN_SAFE_INTEGER = -9007199254740991L;
+
+ public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
+
+ public NumberSerializer(Class extends Number> rawType) {
+ super(rawType);
+ }
+
+ @Override
+ public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ // 超出范围 序列化位字符串
+ if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
+ super.serialize(value, gen, serializers);
+ } else {
+ gen.writeString(value.toString());
+ }
+ }
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/TimestampLocalDateTimeDeserializer.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/TimestampLocalDateTimeDeserializer.java
new file mode 100644
index 0000000..81fd135
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/TimestampLocalDateTimeDeserializer.java
@@ -0,0 +1,27 @@
+package cd.casic.framework.commons.util.json.databind;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+/**
+ * 基于时间戳的 LocalDateTime 反序列化器
+ *
+ * @author 老五
+ */
+public class TimestampLocalDateTimeDeserializer extends JsonDeserializer {
+
+ public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
+
+ @Override
+ public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ // 将 Long 时间戳,转换为 LocalDateTime 对象
+ return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
+ }
+
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/TimestampLocalDateTimeSerializer.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/TimestampLocalDateTimeSerializer.java
new file mode 100644
index 0000000..394fc66
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/json/databind/TimestampLocalDateTimeSerializer.java
@@ -0,0 +1,26 @@
+package cd.casic.framework.commons.util.json.databind;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+/**
+ * 基于时间戳的 LocalDateTime 序列化器
+ *
+ * @author 老五
+ */
+public class TimestampLocalDateTimeSerializer extends JsonSerializer {
+
+ public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
+
+ @Override
+ public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ // 将 LocalDateTime 对象,转换为 Long 时间戳
+ gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+ }
+
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/monitor/TracerUtils.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/monitor/TracerUtils.java
new file mode 100644
index 0000000..eb5f87b
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/monitor/TracerUtils.java
@@ -0,0 +1,30 @@
+package cd.casic.framework.commons.util.monitor;
+
+import org.apache.skywalking.apm.toolkit.trace.TraceContext;
+
+/**
+ * 链路追踪工具类
+ *
+ * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
+ *
+ * @author mianbin
+ */
+public class TracerUtils {
+
+ /**
+ * 私有化构造方法
+ */
+ private TracerUtils() {
+ }
+
+ /**
+ * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
+ * 如果不存在的话为空字符串!!!
+ *
+ * @return 链路追踪编号
+ */
+ public static String getTraceId() {
+ return TraceContext.traceId();
+ }
+
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/number/MoneyUtils.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/number/MoneyUtils.java
new file mode 100644
index 0000000..7e5316c
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/number/MoneyUtils.java
@@ -0,0 +1,131 @@
+package cd.casic.framework.commons.util.number;
+
+import cn.hutool.core.math.Money;
+import cn.hutool.core.util.NumberUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 金额工具类
+ *
+ * @author mianbin
+ */
+public class MoneyUtils {
+
+ /**
+ * 金额的小数位数
+ */
+ private static final int PRICE_SCALE = 2;
+
+ /**
+ * 百分比对应的 BigDecimal 对象
+ */
+ public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100);
+
+ /**
+ * 计算百分比金额,四舍五入
+ *
+ * @param price 金额
+ * @param rate 百分比,例如说 56.77% 则传入 56.77
+ * @return 百分比金额
+ */
+ public static Integer calculateRatePrice(Integer price, Double rate) {
+ return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue();
+ }
+
+ /**
+ * 计算百分比金额,向下传入
+ *
+ * @param price 金额
+ * @param rate 百分比,例如说 56.77% 则传入 56.77
+ * @return 百分比金额
+ */
+ public static Integer calculateRatePriceFloor(Integer price, Double rate) {
+ return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue();
+ }
+
+ /**
+ * 计算百分比金额
+ *
+ * @param price 金额(单位分)
+ * @param count 数量
+ * @param percent 折扣(单位分),列如 60.2%,则传入 6020
+ * @return 商品总价
+ */
+ public static Integer calculator(Integer price, Integer count, Integer percent) {
+ price = price * count;
+ if (percent == null) {
+ return price;
+ }
+ return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100));
+ }
+
+ /**
+ * 计算百分比金额
+ *
+ * @param price 金额
+ * @param rate 百分比,例如说 56.77% 则传入 56.77
+ * @param scale 保留小数位数
+ * @param roundingMode 舍入模式
+ */
+ public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) {
+ return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
+ .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
+ }
+
+ /**
+ * 分转元
+ *
+ * @param fen 分
+ * @return 元
+ */
+ public static BigDecimal fenToYuan(int fen) {
+ return new Money(0, fen).getAmount();
+ }
+
+ /**
+ * 分转元(字符串)
+ *
+ * 例如说 fen 为 1 时,则结果为 0.01
+ *
+ * @param fen 分
+ * @return 元
+ */
+ public static String fenToYuanStr(int fen) {
+ return new Money(0, fen).toString();
+ }
+
+ /**
+ * 金额相乘,默认进行四舍五入
+ *
+ * 位数:{@link #PRICE_SCALE}
+ *
+ * @param price 金额
+ * @param count 数量
+ * @return 金额相乘结果
+ */
+ public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) {
+ if (price == null || count == null) {
+ return null;
+ }
+ return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * 金额相乘(百分比),默认进行四舍五入
+ *
+ * 位数:{@link #PRICE_SCALE}
+ *
+ * @param price 金额
+ * @param percent 百分比
+ * @return 金额相乘结果
+ */
+ public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) {
+ if (price == null || percent == null) {
+ return null;
+ }
+ return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP);
+ }
+
+}
diff --git a/framework/commons/src/main/java/cd/casic/framework/commons/util/number/NumberUtils.java b/framework/commons/src/main/java/cd/casic/framework/commons/util/number/NumberUtils.java
new file mode 100644
index 0000000..6287f60
--- /dev/null
+++ b/framework/commons/src/main/java/cd/casic/framework/commons/util/number/NumberUtils.java
@@ -0,0 +1,64 @@
+package cd.casic.framework.commons.util.number;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.math.BigDecimal;
+
+/**
+ * 数字的工具类,补全 {@link NumberUtil} 的功能
+ *
+ * @author mianbin
+ */
+public class NumberUtils {
+
+ public static Long parseLong(String str) {
+ return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
+ }
+
+ public static Integer parseInt(String str) {
+ return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null;
+ }
+
+ /**
+ * 通过经纬度获取地球上两点之间的距离
+ *
+ * 参考 <