Pārlūkot izejas kodu

feat(util): 添加AES加解密和HMAC-MD5签名工具类- 新增AESCryptoUtil工具类,支持AES-128-CBC-PKCS5Padding对称加解密
- 实现encrypt和decrypt方法,包含参数校验和异常处理
- 添加generateRandomKey和generateRandomIV方法用于生成随机密钥和向量
- 新增HmacMD5Util工具类,严格按照RFC 2104标准实现HMAC-MD5签名
- 实现hmacMD5核心算法和hmacMD5Hex便捷方法
- 添加签名验证和十六进制转换功能
- 包含完整的测试用例和不同场景验证逻辑

wzq 1 mēnesi atpakaļ
vecāks
revīzija
1668423507

+ 30 - 0
src/main/java/com/zsElectric/boot/common/constant/ConnectivityConstants.java

@@ -0,0 +1,30 @@
+package com.zsElectric.boot.common.constant;
+
+/**
+ * 连接性对接信息常量
+ *
+ * @author Ray.Hao
+ * @since 1.0.0
+ */
+public interface ConnectivityConstants {
+
+    /**
+     * 运营商密钥
+     */
+    String OPERATOR_SECRET = "R47nY0QNSlPb3bRKmUjp20VF";
+
+    /**
+     * 签名密钥
+     */
+    String SIG_SECRET = "U9xFXjjdYAycq30C";
+
+    /**
+     * 数据密钥
+     */
+    String DATA_SECRET = "r5RI5rqRkzZFrHB6";
+
+    /**
+     * 数据密钥向量
+     */
+    String DATA_SECRET_IV = "TQc8akFtf7bpdEuO";
+}

+ 201 - 0
src/main/java/com/zsElectric/boot/common/util/AESCryptoUtil.java

@@ -0,0 +1,201 @@
+package com.zsElectric.boot.common.util;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * AES加解密工具类
+ * 支持AES-128-CBC-PKCS5Padding对称加解密算法
+ * @version 1.0
+ */
+public class AESCryptoUtil {
+    
+    // 加密算法/模式/填充方式
+    private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final String AES = "AES";
+    private static final int KEY_SIZE = 128; // 128位
+    private static final int IV_SIZE = 16;   // 16字节
+    
+    /**
+     * AES加密
+     * @param data 待加密的明文
+     * @param key 密钥(必须为16字节)
+     * @param iv 初始化向量(必须为16字节)
+     * @return Base64编码的加密结果
+     * @throws Exception 加密异常
+     */
+    public static String encrypt(String data, String key, String iv) throws Exception {
+        // 参数校验
+        if (data == null || data.isEmpty()) {
+            throw new IllegalArgumentException("加密数据不能为空");
+        }
+        if (key == null || key.length() != 16) {
+            throw new IllegalArgumentException("密钥必须为16位字符");
+        }
+        if (iv == null || iv.length() != IV_SIZE) {
+            throw new IllegalArgumentException("初始化向量必须为16位字符");
+        }
+        
+        try {
+            // 创建密钥规范
+            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
+            // 创建初始化向量规范
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
+            
+            // 获取加密实例并初始化
+            Cipher cipher = Cipher.getInstance(ALGORITHM);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+            
+            // 执行加密
+            byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
+            
+            // 返回Base64编码的加密结果
+            return Base64.getEncoder().encodeToString(encryptedBytes);
+            
+        } catch (Exception e) {
+            throw new Exception("AES加密失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * AES解密
+     * @param encryptedData Base64编码的加密数据
+     * @param key 密钥(必须为16字节)
+     * @param iv 初始化向量(必须为16字节)
+     * @return 解密后的明文
+     * @throws Exception 解密异常
+     */
+    public static String decrypt(String encryptedData, String key, String iv) throws Exception {
+        // 参数校验
+        if (encryptedData == null || encryptedData.isEmpty()) {
+            throw new IllegalArgumentException("解密数据不能为空");
+        }
+        if (key == null || key.length() != 16) {
+            throw new IllegalArgumentException("密钥必须为16位字符");
+        }
+        if (iv == null || iv.length() != 16) {
+            throw new IllegalArgumentException("初始化向量必须为16位字符");
+        }
+        
+        try {
+            // 创建密钥规范
+            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
+            // 创建初始化向量规范
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
+            
+            // 获取解密实例并初始化
+            Cipher cipher = Cipher.getInstance(ALGORITHM);
+            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
+            
+            // Base64解码并执行解密
+            byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
+            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+            
+            // 返回解密结果
+            return new String(decryptedBytes, StandardCharsets.UTF_8);
+            
+        } catch (Exception e) {
+            throw new Exception("AES解密失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 生成随机密钥(16字节)
+     * @return 16字节的随机密钥
+     */
+    public static String generateRandomKey() {
+        return generateRandomString(16);
+    }
+    
+    /**
+     * 生成随机初始化向量(16字节)
+     * @return 16字节的随机IV
+     */
+    public static String generateRandomIV() {
+        return generateRandomString(16);
+    }
+    
+    /**
+     * 生成指定长度的随机字符串
+     * @param length 字符串长度
+     * @return 随机字符串
+     */
+    private static String generateRandomString(int length) {
+        String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            int index = (int) (characters.length() * Math.random());
+            sb.append(characters.charAt(index));
+        }
+        return sb.toString();
+    }
+    
+    /**
+     * 测试方法
+     */
+    public static void main(String[] args) {
+        try {
+            // 测试数据
+            String originalData = "{\n" +
+                    "    \"total\": 1,\n" +
+                    "    \"stationStatusInfo\": {\n" +
+                    "        \"operationlD\": \"123456789\",\n" +
+                    "        \"stationlD\": \"111111111111111\",\n" +
+                    "        \"connectorStatusInfos\":{\"connectorD\":1,\"equipmentD\":\"10000000000000000000001\",\n" +
+                    "        \"status\":4,\n" +
+                    "        \"curentA\":0,\n" +
+                    "        \"currentB\":0,\n" +
+                    "        \"curentC\":0,\n" +
+                    "        \"voltageA\":0,\n" +
+                    "        \"voltageB\":0,\n" +
+                    "        \"voltageC\":0,\n" +
+                    "        \"soc\":10\n" +
+                    "        }\n" +
+                    "    }\n" +
+                    "}";
+            String key = "1234567890abcdef";   // 16字节密钥
+            String iv = "1234567890abcdef";   // 16字节初始化向量
+            
+            System.out.println("=== AES-128-CBC-PKCS5Padding 加解密测试 ===");
+            System.out.println("原始数据: " + originalData);
+            System.out.println("密钥: " + key);
+            System.out.println("初始化向量: " + iv);
+            
+            // 加密
+            long startTime = System.currentTimeMillis();
+            String encryptedData = encrypt(originalData, key, iv);
+            long encryptTime = System.currentTimeMillis() - startTime;
+            System.out.println("加密结果: " + encryptedData);
+            System.out.println("加密耗时: " + encryptTime + "ms");
+            
+            // 解密
+            startTime = System.currentTimeMillis();
+            String decryptedData = decrypt(encryptedData, key, iv);
+            long decryptTime = System.currentTimeMillis() - startTime;
+            System.out.println("解密结果: " + decryptedData);
+            System.out.println("解密耗时: " + decryptTime + "ms");
+            
+            // 验证加解密一致性
+            boolean isSuccess = originalData.equals(decryptedData);
+            System.out.println("加解密验证: " + (isSuccess ? "成功" : "失败"));
+            
+            // 测试随机密钥生成
+            System.out.println("\n=== 随机密钥生成测试 ===");
+            String randomKey = generateRandomKey();
+            String randomIV = generateRandomIV();
+            System.out.println("随机密钥: " + randomKey);
+            System.out.println("随机IV: " + randomIV);
+            
+            // 使用随机密钥进行加解密测试
+            String testEncrypted = encrypt("测试数据", randomKey, randomIV);
+            String testDecrypted = decrypt(testEncrypted, randomKey, randomIV);
+            System.out.println("随机密钥加解密测试: " + ("测试数据".equals(testDecrypted) ? "成功" : "失败"));
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 262 - 0
src/main/java/com/zsElectric/boot/common/util/HmacMD5Util.java

@@ -0,0 +1,262 @@
+package com.zsElectric.boot.common.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * HMAC-MD5参数签名工具类
+ * 严格按照RFC 2104标准的7个步骤实现
+ * @version 1.0
+ */
+public class HmacMD5Util {
+    
+    private static final int BLOCK_SIZE = 64; // 块大小为64字节
+    private static final byte IPAD = 0x36;   // 内部填充常量
+    private static final byte OPAD = 0x5C;   // 外部填充常量
+    
+    /**
+     * HMAC-MD5签名生成
+     * @param data 待签名的消息内容
+     * @param key 签名密钥
+     * @return 16字节的HMAC-MD5签名结果
+     * @throws NoSuchAlgorithmException
+     */
+    public static byte[] hmacMD5(byte[] data, byte[] key) throws NoSuchAlgorithmException {
+        // 步骤1:在签名密钥后面添加0创建长为64字节的字符串
+        byte[] k = prepareKey(key);
+        
+        // 步骤2:将密钥与ipad(0x36)做异或运算
+        byte[] iPadXor = xorWithPad(k, IPAD);
+        
+        // 步骤3:将消息内容附加到第二步的结果字符串的末尾
+        byte[] firstInput = concatenate(iPadXor, data);
+        
+        // 步骤4:对第三步生成的数据流做MD5运算
+        byte[] firstHash = md5(firstInput);
+        
+        // 步骤5:将第一步生成的字符串与opad(0x5c)做异或运算
+        byte[] oPadXor = xorWithPad(k, OPAD);
+        
+        // 步骤6:将第四步的结果附加到第五步的结果字符串的末尾
+        byte[] secondInput = concatenate(oPadXor, firstHash);
+        
+        // 步骤7:对第六步生成的数据流做MD5运算,输出最终结果
+        return md5(secondInput);
+    }
+    
+    /**
+     * 步骤1:准备密钥 - 填充或哈希到64字节
+     * @param key 原始密钥
+     * @return 64字节的密钥
+     * @throws NoSuchAlgorithmException
+     */
+    private static byte[] prepareKey(byte[] key) throws NoSuchAlgorithmException {
+        byte[] result = new byte[BLOCK_SIZE];
+        
+        if (key.length > BLOCK_SIZE) {
+            // 如果密钥长度超过64字节,先进行MD5哈希
+            byte[] hashedKey = md5(key);
+            System.arraycopy(hashedKey, 0, result, 0, hashedKey.length);
+            // 剩余部分填充0
+            for (int i = hashedKey.length; i < BLOCK_SIZE; i++) {
+                result[i] = 0;
+            }
+        } else if (key.length < BLOCK_SIZE) {
+            // 如果密钥长度不足64字节,后面补0
+            System.arraycopy(key, 0, result, 0, key.length);
+            for (int i = key.length; i < BLOCK_SIZE; i++) {
+                result[i] = 0;
+            }
+        } else {
+            // 密钥正好64字节
+            result = key.clone();
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 步骤2/5:密钥与pad常量进行异或运算
+     * @param key 64字节的密钥
+     * @param pad 填充常量(IPAD或OPAD)
+     * @return 异或结果
+     */
+    private static byte[] xorWithPad(byte[] key, byte pad) {
+        byte[] result = new byte[BLOCK_SIZE];
+        for (int i = 0; i < BLOCK_SIZE; i++) {
+            result[i] = (byte) (key[i] ^ pad);
+        }
+        return result;
+    }
+    
+    /**
+     * 字节数组拼接
+     * @param a 第一个字节数组
+     * @param b 第二个字节数组
+     * @return 拼接后的字节数组
+     */
+    private static byte[] concatenate(byte[] a, byte[] b) {
+        byte[] result = new byte[a.length + b.length];
+        System.arraycopy(a, 0, result, 0, a.length);
+        System.arraycopy(b, 0, result, a.length, b.length);
+        return result;
+    }
+    
+    /**
+     * MD5哈希计算
+     * @param input 输入数据
+     * @return MD5哈希结果(16字节)
+     * @throws NoSuchAlgorithmException
+     */
+    private static byte[] md5(byte[] input) throws NoSuchAlgorithmException {
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        return md.digest(input);
+    }
+    
+    /**
+     * 字节数组转换为十六进制字符串
+     * @param bytes 字节数组
+     * @return 十六进制字符串
+     */
+    public static String bytesToHex(byte[] bytes) {
+        StringBuilder hexString = new StringBuilder();
+        for (byte b : bytes) {
+            String hex = Integer.toHexString(0xff & b);
+            if (hex.length() == 1) {
+                hexString.append('0');
+            }
+            hexString.append(hex);
+        }
+        return hexString.toString();
+    }
+    
+    /**
+     * 生成HMAC-MD5签名(十六进制字符串形式)
+     * @param data 消息内容
+     * @param key 密钥
+     * @return 32位十六进制签名字符串
+     * @throws NoSuchAlgorithmException
+     */
+    public static String hmacMD5Hex(String data, String key) throws NoSuchAlgorithmException {
+        byte[] signature = hmacMD5(data.getBytes(StandardCharsets.UTF_8), 
+                                 key.getBytes(StandardCharsets.UTF_8));
+        return bytesToHex(signature);
+    }
+    
+    /**
+     * 生成HMAC-MD5签名(十六进制字符串形式)
+     * @param data 消息内容字节数组
+     * @param key 密钥字节数组
+     * @return 32位十六进制签名字符串
+     * @throws NoSuchAlgorithmException
+     */
+    public static String hmacMD5Hex(byte[] data, byte[] key) throws NoSuchAlgorithmException {
+        byte[] signature = hmacMD5(data, key);
+        return bytesToHex(signature);
+    }
+    
+    /**
+     * 验证HMAC-MD5签名
+     * @param data 原始消息内容
+     * @param key 密钥
+     * @param signature 待验证的签名(十六进制字符串)
+     * @return 验证结果
+     * @throws NoSuchAlgorithmException
+     */
+    public static boolean verify(String data, String key, String signature) throws NoSuchAlgorithmException {
+        String calculatedSignature = hmacMD5Hex(data, key);
+        return calculatedSignature.equalsIgnoreCase(signature);
+    }
+    
+    /**
+     * 测试方法
+     */
+    public static void main(String[] args) {
+        try {
+            // 测试数据
+            String data = "z5rjHHzpZLxYPWO1F8mM9ylLOlllU8w/qB8+RCpmKkA9F9kTzJTNlpeDlXKIL/LpX/6MMFTLfeYIGgzckepOBOZnk5GEb3LsoxprtFFm8ipUMpIBiMldJ5wH0YrMHsMu2zfGwMOI2XNcrCCwgGTTeTqugOFxilC2QDQ7HAFDl0AylUp5WiddJ+Pi7fNC5R+lNf6AaivwpMcq+24Ax0iWFxxhyuKRE+Zk5hDwCEyKmzb2ZWav0cJD0L+v63vjbq6KmyM6NE5QafFKHyQ1Xlm3+jeBNhU4ufkinPWMgw0NEPajPxeE4wp+9YEt8p+fXjPmDr2aGKkdRWUzVslWhZIxPWrhCNrQcas7S5G2hIViw2+Wosxkt52kGTPAnIcTfSVQ0zjUQI45Ywgu4PqZklNT9Uh0d2ktnoL+kpuOLF4NeBy9WTLIGB2WNscBYYPnXZuN6kIc+ML026Z7manQC35qTluemcQ3reMhxWiw+owIbMo/0+kUek2MeHPLK9m512QUUpJDoJU1tAkPNvCiqkBBUg==";
+            String key = "1234567890abcdef";
+            
+            System.out.println("=== HMAC-MD5签名测试 ===");
+            System.out.println("原始数据: " + data);
+            System.out.println("密钥: " + key);
+            
+            // 生成签名
+            long startTime = System.nanoTime();
+            String signature = hmacMD5Hex(data, key);
+            long signTime = System.nanoTime() - startTime;
+            
+            System.out.println("HMAC-MD5签名: " + signature);
+            System.out.println("签名长度: " + signature.length() + "字符(32字节)");
+            System.out.println("签名耗时: " + signTime + "纳秒");
+            
+            // 验证签名
+            startTime = System.nanoTime();
+            boolean isValid = verify(data, key, signature);
+            long verifyTime = System.nanoTime() - startTime;
+            
+            System.out.println("签名验证: " + (isValid ? "成功" : "失败"));
+            System.out.println("验证耗时: " + verifyTime + "纳秒");
+            
+            // 测试不同长度密钥
+            System.out.println("\n=== 不同长度密钥测试 ===");
+            testWithDifferentKeyLengths();
+            
+            // 测试签名一致性
+            System.out.println("\n=== 签名一致性测试 ===");
+            testSignatureConsistency();
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+    
+    /**
+     * 测试不同长度密钥的签名
+     */
+    private static void testWithDifferentKeyLengths() throws NoSuchAlgorithmException {
+        String data = "测试数据";
+        
+        // 短密钥(小于64字节)
+        String shortKey = "short";
+        String signature1 = hmacMD5Hex(data, shortKey);
+        System.out.println("短密钥签名: " + signature1);
+        
+        // 长密钥(等于64字节)
+        String longKey = "thisIsAExactly64BytesLongKeyUsedForHMACMD5SignatureTest123";
+        String signature2 = hmacMD5Hex(data, longKey);
+        System.out.println("64字节密钥签名: " + signature2);
+        
+        // 超长密钥(大于64字节)
+        String veryLongKey = "thisIsAVeryLongKeyThatExceedsThe64BytesBlockSizeUsedForHMACMD5SignatureTest123456789";
+        String signature3 = hmacMD5Hex(data, veryLongKey);
+        System.out.println("超长密钥签名: " + signature3);
+    }
+    
+    /**
+     * 测试签名一致性(相同输入应产生相同输出)
+     */
+    private static void testSignatureConsistency() throws NoSuchAlgorithmException {
+        String data = "一致性测试数据";
+        String key = "testKey";
+        
+        // 多次签名应该结果一致
+        String signature1 = hmacMD5Hex(data, key);
+        String signature2 = hmacMD5Hex(data, key);
+        String signature3 = hmacMD5Hex(data, key);
+        
+        System.out.println("第一次签名: " + signature1);
+        System.out.println("第二次签名: " + signature2);
+        System.out.println("第三次签名: " + signature3);
+        
+        boolean consistent = signature1.equals(signature2) && signature2.equals(signature3);
+        System.out.println("签名一致性: " + (consistent ? "通过" : "失败"));
+        
+        // 测试数据微小变化会导致签名完全不同
+        String similarData = "一致性测试数据 "; // 多一个空格
+        String differentSignature = hmacMD5Hex(similarData, key);
+        System.out.println("微小变化后签名: " + differentSignature);
+        System.out.println("敏感性测试: " + (!signature1.equals(differentSignature) ? "通过" : "失败"));
+    }
+}

+ 104 - 0
src/main/java/com/zsElectric/boot/common/util/OkHttpUtil.java

@@ -0,0 +1,104 @@
+package com.zsElectric.boot.common.util;
+
+import okhttp3.*;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.Map;
+
+@Component
+public class OkHttpUtil {
+    private final OkHttpClient okHttpClient;
+    
+    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+    public OkHttpUtil(OkHttpClient okHttpClient) {
+        this.okHttpClient = okHttpClient;
+    }
+    
+    /**
+     * 执行 GET 请求
+     */
+    public String doGet(String url, Map<String, String> headers) throws IOException {
+        Request.Builder requestBuilder = new Request.Builder().url(url);
+        
+        if (headers != null) {
+            headers.forEach(requestBuilder::addHeader);
+        }
+        
+        Request request = requestBuilder.build();
+        
+        try (Response response = okHttpClient.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                throw new IOException("Unexpected code: " + response);
+            }
+            return response.body().string();
+        }
+    }
+    
+    /**
+     * 执行 POST 请求(JSON 数据)
+     */
+    public String doPostJson(String url, String json, Map<String, String> headers) throws IOException {
+        RequestBody body = RequestBody.create(json, JSON);
+        
+        Request.Builder requestBuilder = new Request.Builder()
+                .url(url)
+                .post(body);
+                
+        if (headers != null) {
+            headers.forEach(requestBuilder::addHeader);
+        }
+        
+        Request request = requestBuilder.build();
+        
+        try (Response response = okHttpClient.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                throw new IOException("Unexpected code: " + response);
+            }
+            return response.body().string();
+        }
+    }
+    
+    /**
+     * 执行 POST 请求(表单数据)
+     */
+    public String doPostForm(String url, Map<String, String> formData, Map<String, String> headers) throws IOException {
+        FormBody.Builder formBodyBuilder = new FormBody.Builder();
+        
+        if (formData != null) {
+            formData.forEach(formBodyBuilder::add);
+        }
+        
+        Request.Builder requestBuilder = new Request.Builder()
+                .url(url)
+                .post(formBodyBuilder.build());
+                
+        if (headers != null) {
+            headers.forEach(requestBuilder::addHeader);
+        }
+        
+        Request request = requestBuilder.build();
+        
+        try (Response response = okHttpClient.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                throw new IOException("Unexpected code: " + response);
+            }
+            return response.body().string();
+        }
+    }
+    
+    /**
+     * 异步 GET 请求
+     */
+    public void doGetAsync(String url, Map<String, String> headers, Callback callback) {
+        Request.Builder requestBuilder = new Request.Builder().url(url);
+        
+        if (headers != null) {
+            headers.forEach(requestBuilder::addHeader);
+        }
+        
+        Request request = requestBuilder.build();
+        okHttpClient.newCall(request).enqueue(callback);
+    }
+}

+ 175 - 0
src/main/java/com/zsElectric/boot/common/util/SequenceGenUtil.java

@@ -0,0 +1,175 @@
+package com.zsElectric.boot.common.util;
+
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 基于时间戳的4位自增序列和时间戳生成器
+ * 规则:同一秒内序列号自增(0001-9999),下一秒序列重置为0001
+ *
+ * @author: wzq
+ * @date: 2025/11/12
+ */
+public class SequenceGenUtil {
+
+    private static final AtomicInteger currentSequence = new AtomicInteger(0);
+    private static volatile long lastSecond = -1L;
+    private static final int MAX_SEQUENCE = 9999;
+
+    // 时间格式化器(线程安全)
+    private static final DateTimeFormatter TIMESTAMP_FORMATTER =
+            DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+    /**
+     * 序列号生成结果对象
+     */
+    @Getter
+    public static class SequenceResult {
+        private final String sequence;      // 4位序列号,如 "0001"
+        private final String timestamp;     // 时间戳,如 "20251112154530"
+
+        public SequenceResult(String sequence, String timestamp) {
+            this.sequence = sequence;
+            this.timestamp = timestamp;
+        }
+
+        /**
+         * 获取完整编号:时间戳 + 序列号
+         */
+        public String getFullNumber() {
+            return timestamp + sequence;
+        }
+
+        @Override
+        public String toString() {
+            return "SequenceResult{sequence='" + sequence + "', timestamp='" + timestamp + "'}";
+        }
+    }
+
+    /**
+     * 生成序列号和时间戳
+     * 使用synchronized确保线程安全
+     */
+    public static synchronized SequenceResult generate() {
+        long currentSecond = getCurrentSecond();
+        String currentTimestamp = formatTimestamp(currentSecond);
+
+        // 检查是否是新的一秒
+        if (currentSecond != lastSecond) {
+            currentSequence.set(0);
+            lastSecond = currentSecond;
+        }
+
+        // 获取下一个序列值
+        int seq = currentSequence.incrementAndGet();
+
+        // 检查序列号是否溢出
+        if (seq > MAX_SEQUENCE) {
+            waitForNextSecond(currentSecond);
+            // 递归调用,进入新秒后重新生成
+            return generate();
+        }
+
+        // 格式化为4位数字并返回结果对象
+        String sequenceStr = String.format("%04d", seq);
+        return new SequenceResult(sequenceStr, currentTimestamp);
+    }
+
+    /**
+     * 仅生成4位序列号(保持原有功能)
+     */
+    public static synchronized String generateSequenceOnly() {
+        return generate().getSequence();
+    }
+
+    /**
+     * 生成完整编号(时间戳+序列号)
+     */
+    public static synchronized String generateFullNumber() {
+        return generate().getFullNumber();
+    }
+
+    /**
+     * 获取当前时间的秒级时间戳
+     */
+    private static long getCurrentSecond() {
+        return System.currentTimeMillis() / 1000;
+    }
+
+    /**
+     * 将秒级时间戳格式化为 yyyyMMddHHmmss
+     */
+    private static String formatTimestamp(long timestampInSeconds) {
+        LocalDateTime dateTime = LocalDateTime.ofEpochSecond(timestampInSeconds, 0,
+                java.time.ZoneOffset.UTC);
+        // 调整时区为系统默认时区
+        return dateTime.atZone(java.time.ZoneId.systemDefault()).format(TIMESTAMP_FORMATTER);
+    }
+
+    /**
+     * 等待下一秒(当序列号溢出时调用)
+     */
+    private static void waitForNextSecond(long currentSecond) {
+        while (getCurrentSecond() <= currentSecond) {
+            try {
+                Thread.sleep(1);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("序列生成器等待被中断", e);
+            }
+        }
+    }
+
+    /**
+     * 重置生成器状态(主要用于测试)
+     */
+    public static synchronized void reset() {
+        lastSecond = -1L;
+        currentSequence.set(0);
+    }
+
+    public static void main(String[] args) {
+        System.out.println("=== 生成序列号和时间戳 ===");
+
+        // 生成完整的序列结果(包含序列号和时间戳)
+        SequenceGenUtil.SequenceResult result1 = SequenceGenUtil.generate();
+        System.out.println("序列号: " + result1.getSequence());     // 输出: 0001
+        System.out.println("时间戳: " + result1.getTimestamp());    // 输出: 20251112154530
+        System.out.println("完整编号: " + result1.getFullNumber()); // 输出: 202511121545300001
+        System.out.println("完整对象: " + result1.toString());
+
+        // 同一秒内连续生成
+        System.out.println("\n=== 同一秒内连续生成 ===");
+        for (int i = 0; i < 3; i++) {
+            SequenceGenUtil.SequenceResult result = SequenceGenUtil.generate();
+            System.out.println("时间戳: " + result.getTimestamp() + ", 序列号: " + result.getSequence());
+        }
+
+        // 仅生成序列号(保持原有功能)
+        System.out.println("\n=== 仅生成序列号 ===");
+        String sequenceOnly = SequenceGenUtil.generateSequenceOnly();
+        System.out.println("仅序列号: " + sequenceOnly);
+
+        // 生成完整编号
+        System.out.println("\n=== 生成完整编号 ===");
+        String fullNumber = SequenceGenUtil.generateFullNumber();
+        System.out.println("完整编号: " + fullNumber);
+
+        // 模拟跨秒生成
+        System.out.println("\n=== 模拟跨秒生成 ===");
+        SequenceGenUtil.SequenceResult beforeSleep = SequenceGenUtil.generate();
+        System.out.println("休眠前: " + beforeSleep.getTimestamp() + " - " + beforeSleep.getSequence());
+
+        try {
+            Thread.sleep(1000); // 等待1秒
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        SequenceGenUtil.SequenceResult afterSleep = SequenceGenUtil.generate();
+        System.out.println("休眠后: " + afterSleep.getTimestamp() + " - " + afterSleep.getSequence());
+    }
+}

+ 47 - 0
src/main/java/com/zsElectric/boot/common/util/electric/ApiToken.java

@@ -0,0 +1,47 @@
+package com.zsElectric.boot.common.util.electric;
+
+import lombok.Data;
+import java.time.LocalDateTime;
+
+/**
+ * Token实体类
+ * 封装Token相关信息及有效性检查[2](@ref)
+ */
+@Data
+public class ApiToken {
+    /**
+     * 获取的凭证
+     */
+    private String accessToken;
+    
+    /**
+     * 凭证有效期(单位:秒)
+     */
+    private Integer tokenAvailableTime;
+    
+    /**
+     * 令牌获取时间
+     */
+    private LocalDateTime obtainTime;
+    
+    /**
+     * 令牌过期时间
+     */
+    private LocalDateTime expireTime;
+    
+    /**
+     * 检查令牌是否有效
+     * 提前60秒过期,避免网络延迟等问题导致的令牌无效[2](@ref)
+     * @return true-有效,false-无效
+     */
+    public boolean isValid() {
+        return LocalDateTime.now().plusSeconds(60).isBefore(expireTime);
+    }
+    
+    /**
+     * 计算剩余有效时间(秒)
+     */
+    public long getRemainingSeconds() {
+        return java.time.Duration.between(LocalDateTime.now(), expireTime).getSeconds();
+    }
+}

+ 185 - 0
src/main/java/com/zsElectric/boot/common/util/electric/TokenManager.java

@@ -0,0 +1,185 @@
+package com.zsElectric.boot.common.util.electric;
+
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.checkerframework.checker.units.qual.A;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Token管理器 - 负责Token的获取、刷新、存储和有效性检查[2](@ref)
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TokenManager {
+    
+    private static final String TOKEN_KEY = "api:local:accessToken";
+    private static final String TOKEN_REFRESH_LOCK_KEY = "api:token:refresh:lock";
+    private static final int DEFAULT_TOKEN_EXPIRE_HOURS = 24;
+    
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    // 本地锁,防止单JVM内重复刷新Token[2](@ref)
+    private final ReentrantLock localLock = new ReentrantLock();
+    
+    /**
+     * 获取有效的访问令牌(主入口方法)
+     */
+    public String getValidAccessToken() {
+        // 尝试从Redis获取已存储的Token[1](@ref)
+        ApiToken apiToken = getStoredToken();
+        
+        // 如果Token不存在或已过期,则刷新Token[1,3](@ref)
+        if (Objects.isNull(apiToken) || !apiToken.isValid()) {
+            apiToken = refreshAccessToken();
+        }
+        
+        return apiToken.getAccessToken();
+    }
+    
+    /**
+     * 从Redis获取存储的Token
+     */
+    private ApiToken getStoredToken() {
+        try {
+            Object tokenObj = redisTemplate.opsForValue().get(TOKEN_KEY);
+            if (Objects.isNull(tokenObj)) {
+                log.debug("Redis中未找到Token,tokenKey: {}", TOKEN_KEY);
+                return null;
+            }
+            return (ApiToken) tokenObj;
+        } catch (Exception e) {
+            log.error("从Redis获取Token失败,tokenKey: {}", TOKEN_KEY, e);
+            // 清除可能的错误数据
+            redisTemplate.delete(TOKEN_KEY);
+            return null;
+        }
+    }
+
+    /**
+     * Token刷新相关方法
+     */
+    private ApiToken refreshAccessToken() {
+        // 先尝试获取本地锁,减少分布式锁的竞争[2](@ref)
+        if (!localLock.tryLock()) {
+            log.debug("本地锁已被占用,等待其他线程刷新Token");
+            return waitAndRetrieve();
+        }
+
+        try {
+            // 获取分布式锁,防止集群环境下多个实例同时刷新Token[2](@ref)
+            String lockKey = TOKEN_REFRESH_LOCK_KEY;
+            Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(
+                    lockKey, "LOCK", 30, TimeUnit.SECONDS);
+
+            if (Boolean.FALSE.equals(lockAcquired)) {
+                log.info("分布式锁已被占用,等待其他实例刷新Token");
+                return waitAndRetrieve();
+            }
+
+            try {
+                // 双重检查,防止重复刷新[2](@ref)
+                ApiToken existingToken = getStoredToken();
+                if (Objects.nonNull(existingToken) && existingToken.isValid()) {
+                    log.info("其他线程已刷新Token,直接使用");
+                    return existingToken;
+                }
+
+                log.info("开始刷新Token");
+                ApiToken newToken = fetchNewTokenFromRemote();
+
+                if (Objects.isNull(newToken) ||
+                        Objects.isNull(newToken.getAccessToken()) ||
+                        newToken.getAccessToken().trim().isEmpty()) {
+                    throw new RuntimeException("刷新Token失败,获取到的Token为空");
+                }
+
+                // 设置Token时间信息并存储
+                setupAndStoreToken(TOKEN_KEY, newToken);
+                log.info("Token刷新成功,tokenKey: {},过期时间: {}", TOKEN_KEY, newToken.getExpireTime());
+                return newToken;
+
+            } finally {
+                // 释放分布式锁
+                redisTemplate.delete(lockKey);
+            }
+        } finally {
+            localLock.unlock();
+        }
+    }
+
+    /**
+     * 等待并重新获取Token(用于锁竞争场景)
+     */
+    private ApiToken waitAndRetrieve() {
+        try {
+            TimeUnit.MILLISECONDS.sleep(200);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("等待锁释放时被中断", e);
+        }
+        // 重试获取Token
+        ApiToken existingToken = getStoredToken();
+        if (Objects.nonNull(existingToken) && existingToken.isValid()) {
+            return existingToken;
+        }
+        // 如果仍然无效,递归刷新(会有锁控制)
+        return refreshAccessToken();
+    }
+
+    /**
+     * 调用第三方接口获取新Token[2](@ref)
+     */
+    private ApiToken fetchNewTokenFromRemote() {
+        try {
+            //todo 根据实际情况调用第三方API获取Token
+
+//            if (Objects.isNull(response) || !response.get) {
+//                log.error("调用第三方接口获取Token失败,apiKey: {}", apiKey);
+//                return null;
+//            }
+
+            ApiToken apiToken = new ApiToken();
+//            apiToken.setAccessToken(response.getAccessToken());
+//            ApiToken.setTokenAvailableTime(response.getExpiresIn());
+//            apiToken.setExpiresIn(response.getExpiresIn());
+
+            return apiToken;
+        } catch (Exception e) {
+            log.error("获取新Token时发生异常,tokenKey: {}", TOKEN_KEY, e);
+            throw new RuntimeException("调用第三方接口失败", e);
+        }
+    }
+
+    /**
+     * 设置Token信息并存储到Redis[5](@ref)
+     */
+    private void setupAndStoreToken(String tokenKey, ApiToken token) {
+        LocalDateTime obtainTime = LocalDateTime.now();
+        token.setObtainTime(obtainTime);
+        token.setExpireTime(obtainTime.plusSeconds(token.getTokenAvailableTime()));
+
+        storeToken(tokenKey, token);
+    }
+
+    /**
+     * 存储Token到Redis[5](@ref)
+     */
+    private void storeToken(String apiKey, ApiToken token) {
+        long expireSeconds = Duration.between(LocalDateTime.now(), token.getExpireTime()).getSeconds();
+
+        // 设置Redis过期时间,取实际过期时间和默认过期时间的较小值
+        long redisExpireSeconds = Math.max(expireSeconds, 60); // 至少保留60秒
+        redisExpireSeconds = Math.min(redisExpireSeconds, DEFAULT_TOKEN_EXPIRE_HOURS * 3600L);
+
+        redisTemplate.opsForValue().set(TOKEN_KEY, token, redisExpireSeconds, TimeUnit.SECONDS);
+        log.debug("Token已存储到Redis,apiKey: {}, 过期时间: {}秒", apiKey, redisExpireSeconds);
+    }
+}

+ 48 - 0
src/main/java/com/zsElectric/boot/config/OkHttpConfig.java

@@ -0,0 +1,48 @@
+package com.zsElectric.boot.config;
+
+import okhttp3.ConnectionPool;
+import okhttp3.OkHttpClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+public class OkHttpConfig {
+    
+    @Value("${okhttp.connect-timeout:10}")
+    private Duration connectTimeout;
+    
+    @Value("${okhttp.read-timeout:30}")
+    private Duration readTimeout;
+    
+    @Value("${okhttp.write-timeout:30}")
+    private Duration writeTimeout;
+    
+    @Value("${okhttp.retry-on-connection-failure:true}")
+    private boolean retryOnConnectionFailure;
+    
+    @Value("${okhttp.connection-pool.max-idle-connections:200}")
+    private int maxIdleConnections;
+    
+    @Value("${okhttp.connection-pool.keep-alive-duration:300}")
+    private Duration keepAliveDuration;
+
+    @Bean
+    public ConnectionPool connectionPool() {
+        return new ConnectionPool(maxIdleConnections, keepAliveDuration.toMillis(), TimeUnit.MILLISECONDS);
+    }
+
+    @Bean
+    public OkHttpClient okHttpClient(ConnectionPool connectionPool) {
+        return new OkHttpClient.Builder()
+                .connectTimeout(connectTimeout)
+                .readTimeout(readTimeout)
+                .writeTimeout(writeTimeout)
+                .retryOnConnectionFailure(retryOnConnectionFailure)
+                .connectionPool(connectionPool)
+                .build();
+    }
+}

+ 9 - 0
src/main/resources/application-dev.yml

@@ -96,6 +96,15 @@ security:
     - /webjars/**
     - /favicon.ico
 
+okhttp:
+  connect-timeout: 10s
+  read-timeout: 30s
+  write-timeout: 30s
+  retry-on-connection-failure: true
+  connection-pool:
+    max-idle-connections: 200
+    keep-alive-duration: 300s
+
 # 文件存储配置
 oss:
   # OSS 类型 (目前支持aliyun、minio、local)