diff --git a/src/main/java/com/rj/common/DictItemConstants.java b/src/main/java/com/rj/common/DictItemConstants.java
new file mode 100644
index 0000000..cce92a3
--- /dev/null
+++ b/src/main/java/com/rj/common/DictItemConstants.java
@@ -0,0 +1,13 @@
+package com.rj.common;
+
+/**
+ * dict_item 相关常量
+ */
+public final class DictItemConstants {
+
+ private DictItemConstants() {
+ }
+
+ public static final String QIWEI_CONFIG = "qiwei_config";
+}
+
diff --git a/src/main/java/com/rj/common/PasswordUtil.java b/src/main/java/com/rj/common/PasswordUtil.java
index 3f110a4..a22a839 100644
--- a/src/main/java/com/rj/common/PasswordUtil.java
+++ b/src/main/java/com/rj/common/PasswordUtil.java
@@ -2,6 +2,15 @@ package com.rj.common;
import org.springframework.util.DigestUtils;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Base64;
+
/**
* 密码工具类
*
@@ -15,6 +24,13 @@ public class PasswordUtil {
*/
public static final String DEFAULT_SALT = "rj_system_2025";
+ /**
+ * 可逆加密标记,避免重复加密
+ */
+ private static final String ENC_PREFIX = "ENC$";
+
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
/**
* MD5加密密码
*
@@ -47,6 +63,79 @@ public class PasswordUtil {
return encryptPassword(password, DEFAULT_SALT);
}
+ /**
+ * 可逆加密(AES-GCM),输出格式:ENC$base64(iv).base64(cipherText)
+ *
+ * 说明:此方法用于需要可解密的配置值(例如企微配置),不要用于用户密码。
+ */
+ public static String encryptReversibleWithDefaultSalt(String plainText) {
+ if (plainText == null) {
+ plainText = "";
+ }
+ if (isEncryptedReversible(plainText)) {
+ return plainText;
+ }
+ try {
+ byte[] iv = new byte[12]; // GCM recommended IV length
+ SECURE_RANDOM.nextBytes(iv);
+
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ GCMParameterSpec spec = new GCMParameterSpec(128, iv);
+ cipher.init(Cipher.ENCRYPT_MODE, deriveAesKey(DEFAULT_SALT), spec);
+
+ byte[] cipherBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
+ String ivB64 = Base64.getEncoder().encodeToString(iv);
+ String ctB64 = Base64.getEncoder().encodeToString(cipherBytes);
+ return ENC_PREFIX + ivB64 + "." + ctB64;
+ } catch (Exception e) {
+ throw new IllegalStateException("encrypt reversible failed: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 可逆解密(AES-GCM),支持未加密值直接原样返回。
+ */
+ public static String decryptReversibleWithDefaultSalt(String encryptedOrPlain) {
+ if (encryptedOrPlain == null) {
+ return null;
+ }
+ if (!isEncryptedReversible(encryptedOrPlain)) {
+ return encryptedOrPlain;
+ }
+ try {
+ String payload = encryptedOrPlain.substring(ENC_PREFIX.length());
+ int dot = payload.indexOf('.');
+ if (dot <= 0 || dot >= payload.length() - 1) {
+ throw new IllegalArgumentException("bad encrypted format");
+ }
+ byte[] iv = Base64.getDecoder().decode(payload.substring(0, dot));
+ byte[] cipherBytes = Base64.getDecoder().decode(payload.substring(dot + 1));
+
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ GCMParameterSpec spec = new GCMParameterSpec(128, iv);
+ cipher.init(Cipher.DECRYPT_MODE, deriveAesKey(DEFAULT_SALT), spec);
+ byte[] plain = cipher.doFinal(cipherBytes);
+ return new String(plain, StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new IllegalStateException("decrypt reversible failed: " + e.getMessage(), e);
+ }
+ }
+
+ public static boolean isEncryptedReversible(String value) {
+ return value != null && value.startsWith(ENC_PREFIX);
+ }
+
+ private static SecretKey deriveAesKey(String salt) {
+ try {
+ MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+ byte[] keyBytes = sha256.digest(String.valueOf(salt).getBytes(StandardCharsets.UTF_8));
+ // 256-bit AES key
+ return new SecretKeySpec(keyBytes, "AES");
+ } catch (Exception e) {
+ throw new IllegalStateException("derive key failed: " + e.getMessage(), e);
+ }
+ }
+
/**
* 验证密码
*
diff --git a/src/main/java/com/rj/controller/DictItemController.java b/src/main/java/com/rj/controller/DictItemController.java
index 79b4d2d..a32a773 100644
--- a/src/main/java/com/rj/controller/DictItemController.java
+++ b/src/main/java/com/rj/controller/DictItemController.java
@@ -1,5 +1,7 @@
package com.rj.controller;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.rj.common.DictItemConstants;
import com.rj.dto.QiWeiConfig;
import com.rj.entity.DictItem;
import com.rj.service.IDictItemService;
@@ -26,6 +28,8 @@ public class DictItemController {
@Autowired
private IDictItemService dictItemService;
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
@PostMapping("/add")
@Operation(summary = "新增")
public ResponseEntity