From 7acee5189288bc3cb5be0636e0922f80abf17290 Mon Sep 17 00:00:00 2001 From: lianlonggang Date: Fri, 12 Jun 2026 11:49:44 +0800 Subject: [PATCH] =?UTF-8?q?style(system):=20=E7=99=BB=E5=BD=95=E5=90=8E?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=83=A8=E9=97=A8=E4=BF=A1=E6=81=AF=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/renkang/anyan/utils/StringUtils.java | 131 +++++++++- .../service/impl/LoginAnYanServiceImpl.java | 233 +++++++++++++++++- 2 files changed, 361 insertions(+), 3 deletions(-) diff --git a/functional-modules/xj-anyan-api/src/main/java/com/renkang/anyan/utils/StringUtils.java b/functional-modules/xj-anyan-api/src/main/java/com/renkang/anyan/utils/StringUtils.java index db90bea..e678699 100644 --- a/functional-modules/xj-anyan-api/src/main/java/com/renkang/anyan/utils/StringUtils.java +++ b/functional-modules/xj-anyan-api/src/main/java/com/renkang/anyan/utils/StringUtils.java @@ -3,12 +3,20 @@ package com.renkang.anyan.utils; import cn.hutool.core.util.StrUtil; import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyFactory; import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.KeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; @@ -29,7 +37,7 @@ public class StringUtils { public static String encryptByPublicKey(String publicKeyText, String text) throws Exception { // 返回按照 X.509 标准进行编码的密钥的字节 // x509EncodedKeySpec2 是一种规范、规格 - X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(org.apache.commons.codec.binary.Base64.decodeBase64(publicKeyText)); + X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyText)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // 让密钥工厂按照指定的 规范 生成公钥 PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2); @@ -40,7 +48,8 @@ public class StringUtils { byte[] result = cipher.doFinal(text.getBytes()); // 返回 Base64编码的字符串 - return org.apache.commons.codec.binary.Base64.encodeBase64String(result); + return Base64.getEncoder().encodeToString(result); +// return org.apache.commons.codec.binary.Base64.encodeBase64String(result); } @@ -112,4 +121,122 @@ public class StringUtils { return filePath.getFileName().toString(); } } + + // ------------------- AES 加解密(带盐值处理)------------------- + + /** 内部密钥,仅用于日志脱敏,非高强度安全场景 */ + private static final String AES_PASSWORD = "XJPlatform@2024#Secure"; + /** 盐值长度(字节) */ + private static final int SALT_LENGTH = 8; + /** 初始化向量长度(字节) */ + private static final int IV_LENGTH = 16; + /** AES 密钥长度 */ + private static final int KEY_LENGTH = 128; + /** PBKDF2 迭代次数 */ + private static final int PBKDF2_ITERATIONS = 1000; + /** AES 加密算法 */ + private static final String AES_CIPHER = "AES/CBC/PKCS5Padding"; + + /** + * AES 加密(带随机盐值和 IV,每次加密结果不同) + * + * @param plainText 明文 + * @return Base64 密文(格式:盐值 + IV + 加密数据) + */ + public static String aesEncrypt(String plainText) { + if (plainText == null || plainText.isEmpty()) { + return plainText; + } + try { + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[SALT_LENGTH]; + byte[] iv = new byte[IV_LENGTH]; + random.nextBytes(salt); + random.nextBytes(iv); + + SecretKeySpec keySpec = deriveKey(salt); + Cipher cipher = Cipher.getInstance(AES_CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv)); + byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); + + byte[] combined = new byte[SALT_LENGTH + IV_LENGTH + encrypted.length]; + System.arraycopy(salt, 0, combined, 0, SALT_LENGTH); + System.arraycopy(iv, 0, combined, SALT_LENGTH, IV_LENGTH); + System.arraycopy(encrypted, 0, combined, SALT_LENGTH + IV_LENGTH, encrypted.length); + + return Base64.getEncoder().encodeToString(combined); + } catch (Exception e) { + System.err.println("AES加密失败: " + e.getMessage()); + e.printStackTrace(); + return plainText; + } + } + + /** + * AES 解密(解析盐值 + IV + 加密数据) + * + * @param cipherText Base64 密文 + * @return 明文 + */ + public static String aesDecrypt(String cipherText) { + if (cipherText == null || cipherText.isEmpty()) { + return cipherText; + } + try { + byte[] combined = Base64.getDecoder().decode(cipherText); + + byte[] salt = new byte[SALT_LENGTH]; + byte[] iv = new byte[IV_LENGTH]; + byte[] encrypted = new byte[combined.length - SALT_LENGTH - IV_LENGTH]; + System.arraycopy(combined, 0, salt, 0, SALT_LENGTH); + System.arraycopy(combined, SALT_LENGTH, iv, 0, IV_LENGTH); + System.arraycopy(combined, SALT_LENGTH + IV_LENGTH, encrypted, 0, encrypted.length); + + SecretKeySpec keySpec = deriveKey(salt); + Cipher cipher = Cipher.getInstance(AES_CIPHER); + cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv)); + byte[] decrypted = cipher.doFinal(encrypted); + + return new String(decrypted, StandardCharsets.UTF_8); + } catch (Exception e) { + System.err.println("AES解密失败: " + e.getMessage()); + e.printStackTrace(); + return cipherText; + } + } + + /** + * 通过 PBKDF2 从盐值和内部密钥派生 AES 密钥 + */ + private static SecretKeySpec deriveKey(byte[] salt) { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(AES_PASSWORD.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH); + SecretKey tmp = factory.generateSecret(spec); + return new SecretKeySpec(tmp.getEncoded(), "AES"); + } catch (Exception e) { + throw new RuntimeException("派生AES密钥失败", e); + } + } + + public static void main(String[] args) throws Exception { + // --- RSA 公钥加密测试(安眼平台)--- +// String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJL0JkqsUoK6kt3JyogsgqNp9VDGDp+t3ZAGMbVoMPdHNT2nfiIVh9ZMNHF7g2XiAa8O8AQWyh2PjMR0NiUSVQMCAwEAAQ=="; +// String password = "Aa%13999527523"; +// String rsaEncrypted = encryptByPublicKey(publicKey, password); +// System.out.println("RSA加密: " + rsaEncrypted); +// +// // --- AES 加解密测试(日志脱敏)--- +// String originalText = "Aa%13999527523"; +// String aesEncrypted = aesEncrypt(originalText); + String aesDecrypted = aesDecrypt("RuKy7jFDJR0rEJiwxgcyX6RFq8qP5FEgWZuMNxuVkKlbHhjJ2SzLOA=="); +// System.out.println("AES加密: " + aesEncrypted); + System.out.println("AES解密: " + aesDecrypted); +// System.out.println("匹配: " + originalText.equals(aesDecrypted)); + + // 验证每次加密结果不同(盐值和IV随机) +// String aesEncrypted2 = aesEncrypt(originalText); +// System.out.println("再次加密: " + aesEncrypted2); +// System.out.println("两次密文不同: " + !aesEncrypted.equals(aesEncrypted2)); + } } diff --git a/jeecg-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/LoginAnYanServiceImpl.java b/jeecg-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/LoginAnYanServiceImpl.java index 19fde70..ea1ced0 100644 --- a/jeecg-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/LoginAnYanServiceImpl.java +++ b/jeecg-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/LoginAnYanServiceImpl.java @@ -30,6 +30,7 @@ import org.jeecg.modules.system.entity.HealthUserEmployeeEx; import org.jeecg.modules.system.entity.SysDepart; import org.jeecg.modules.system.entity.SysUser; import org.jeecg.modules.system.manager.DepartCacheManager; +import org.jeecg.modules.system.manager.UserCacheManager; import org.jeecg.modules.system.model.SysLoginModel; import org.jeecg.modules.system.service.*; import org.jeecg.util.RSAEncryptUtils; @@ -37,6 +38,7 @@ import org.jetbrains.annotations.NotNull; import org.springframework.core.task.TaskExecutor; import org.springframework.stereotype.Service; +import javax.json.Json; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; @@ -60,6 +62,7 @@ public class LoginAnYanServiceImpl implements ILoginAnYanService { private final TaskExecutor taskExecutor; private final LoginService loginService; private final DepartCacheManager departCacheManager; + private final UserCacheManager userCacheManager; private static final String CLIENT_TYPE_PC = "pc"; private static final String CLIENT_TYPE_APP = "app"; @@ -107,10 +110,12 @@ public class LoginAnYanServiceImpl implements ILoginAnYanService { */ public Result loginBusiness(SysLoginModel sysLoginModel, String username, String clientType) { // 密码解密 + log.info(sysLoginModel.getPassword()); String decryptedPassword = RSAEncryptUtils.decrypt1(sysLoginModel.getPassword(), CommonConstant.PRIVATE_KEY); if (StrUtil.isBlank(decryptedPassword)) { return Result.error("密码解密失败"); } + log.info("解密密码:"+ com.renkang.anyan.utils.StringUtils.aesEncrypt(decryptedPassword)); // 获取安眼token AuthResult anYanAuthResult = anYanClient.getAuthTokenByPassword(username, decryptedPassword); @@ -307,13 +312,17 @@ public class LoginAnYanServiceImpl implements ILoginAnYanService { * @author PengJ * @date 2025/10/29 9:37 */ - private String checkDepartAndUpdate(AnYanUserInfoResult.EmployeeUnitDTO employeeUnitDTO) { + private String checkDepartAndUpdateOld(AnYanUserInfoResult.EmployeeUnitDTO employeeUnitDTO) { + log.info(JSONObject.toJSONString(employeeUnitDTO)); + //{"directDepartmentCode":"1102134","directDepartmentName":"集输班组","email":"hs_aikepaerg@petrochina.com.cn","employeeNum":"80488058","enabledFlag":"1","gender":0,"levelPath":"00000100|00131100|1100620|1102134","mobile":"13999527523","name":"艾克帕尔·玉素甫","sectionUnitCode":"1100620","sectionUnitName":"运行维护三中心","unitCode":"00131100","unitName":"采油一厂","unitNamePath":"root|采油一厂|运行维护三中心|集输班组"} String levelPath = employeeUnitDTO.getLevelPath(); String orgId = ""; if (StringUtils.isNotBlank(levelPath)) { String parentId = ""; String[] unitCodes = levelPath.split("\\|"); + int index = 0; for (String unitCode : unitCodes) { + index ++; //判断是否为顶级组织 if (StringUtils.isBlank(unitCode) || "00000100".equals(unitCode)) { parentId = sysDepartService.getOne(new LambdaQueryWrapper() @@ -333,6 +342,8 @@ public class LoginAnYanServiceImpl implements ILoginAnYanService { orgName = employeeUnitDTO.getUnitName(); } else if (unitCode.equals(employeeUnitDTO.getSectionUnitCode())) { orgName = employeeUnitDTO.getSectionUnitName(); + } else if (unitCode.equals(employeeUnitDTO.getDirectDepartmentCode())) {//缺失了直属单位 + orgName = employeeUnitDTO.getDirectDepartmentName(); } if (StringUtils.isBlank(orgName)) { continue; @@ -399,14 +410,234 @@ public class LoginAnYanServiceImpl implements ILoginAnYanService { } } } + String sendParentId = unitCodes[index-1]; // 父id赋值 parentId = depart.getId(); + log.info(sendParentId + "----" +parentId); + orgId = depart.getId(); } } return orgId; } + /** + * 检查并更新部门信息,返回最底层部门ID + *

+ * levelPath 按 | 分隔各级单位编码,第一位 00000100 为系统根节点需跳过。 + * unitNamePath 按 | 分隔各级单位名称,与 levelPath 一一对应。 + * depart.id = levelPath 中的编码值,depart.parentId = 前一位编码值(跳过 00000100 后父级为系统根组织)。 + * orgCode 层级格式:一级 A01,二级 A01A01,三级 A01A01A01,新增时查询同级最大值 +1。 + * 已存在部门:若 parentId 或名称有变化则更新,parentId 变更时同步重新生成 orgCode。 + * + * @param employeeUnitDTO 安眼用户信息 + * @return 最底层部门 ID + */ + private String checkDepartAndUpdate(AnYanUserInfoResult.EmployeeUnitDTO employeeUnitDTO) { + log.info(JSONObject.toJSONString(employeeUnitDTO)); + String levelPath = employeeUnitDTO.getLevelPath(); + String unitNamePath = employeeUnitDTO.getUnitNamePath(); + + if (StringUtils.isBlank(levelPath) || StringUtils.isBlank(unitNamePath)) { + log.warn("levelPath或unitNamePath为空,跳过部门更新"); + return ""; + } + + String[] codes = levelPath.split("\\|"); + String[] names = unitNamePath.split("\\|"); + + if (codes.length != names.length) { + log.warn("levelPath与unitNamePath长度不一致,codes={},names={}", codes.length, names.length); + return ""; + } + + // 获取系统根组织ID作为默认父级 + String parentId = getRootDepartId(); + String orgId = ""; + + for (int i = 0; i < codes.length; i++) { + String code = codes[i]; + String name = names[i]; + + // 跳过根节点标识 + if ("00000100".equals(code) || StringUtils.isBlank(code)) { + continue; + } + if (StringUtils.isBlank(name)) { + log.warn("部门名称为空,跳过 code={}", code); + continue; + } + + SysDepart depart = sysDepartService.getById(code); + boolean departmentChanged = false; + + if (depart == null) { + // 新增部门 + depart = new SysDepart(); + depart.setId(code); + depart.setParentId(parentId); + depart.setDepartName(name); + depart.setOrgCode(generateOrgCode(parentId)); + depart.setCreateTime(new Date()); + depart.setStatus("1"); + depart.setDelFlag("0"); + depart.setOrgCategory("2"); + depart.setIzLeaf(CommonConstant.IS_LEAF); + sysDepartService.save(depart); + departmentChanged = true; + } else { + // 检查是否需要更新 + String oldOrgCode = depart.getOrgCode(); + boolean changed = false; + boolean parentChanged = false; + if (!parentId.equals(depart.getParentId())) { + depart.setParentId(parentId); + parentChanged = true; + } + if (!name.equals(depart.getDepartName())) { + depart.setDepartName(name); + changed = true; + } + // parentId 变了,或 orgCode 与父级不一致(如部门被移动但 orgCode 未更新),需重新生成 + if (parentChanged || !orgCodeMatchesParent(depart.getOrgCode(), parentId)) { + depart.setOrgCode(generateOrgCode(parentId)); + changed = true; + } + if (changed) { + depart.setUpdateTime(new Date()); + sysDepartService.updateById(depart); + // 若 orgCode 发生了变化,同步更新 sysUser 中的 orgCode 并刷新用户缓存 + String newOrgCode = depart.getOrgCode(); + if (!newOrgCode.equals(oldOrgCode)) { + log.info("部门orgCode变更,code={}, oldOrgCode={}, newOrgCode={}", code, oldOrgCode, newOrgCode); + // 先查出受影响用户的ID,用于后续刷新缓存 + List affectedUsers = sysUserService.list(new LambdaQueryWrapper() + .select(SysUser::getId) + .eq(SysUser::getOrgCode, oldOrgCode)); + // 批量更新 sys_user 表 orgCode + sysUserService.update(new LambdaUpdateWrapper() + .eq(SysUser::getOrgCode, oldOrgCode) + .set(SysUser::getOrgCode, newOrgCode)); + // 刷新受影响用户的缓存 + if (affectedUsers != null && !affectedUsers.isEmpty()) { + for (SysUser user : affectedUsers) { + userCacheManager.updateUserCache(user.getId()); + } + log.info("已刷新 {} 个用户的缓存", affectedUsers.size()); + } + } + } + departmentChanged = changed; + } + + // 若父级是叶子节点,更新为非叶子节点 + SysDepart parent = sysDepartService.getById(parentId); + if (parent != null && CommonConstant.IS_LEAF.equals(parent.getIzLeaf())) { + boolean updated = sysDepartService.update(new LambdaUpdateWrapper() + .eq(SysDepart::getId, parentId) + .set(SysDepart::getIzLeaf, CommonConstant.NOT_LEAF)); + if (updated) { + departmentChanged = true; + } + } + + // 根据是否有子节点维护本级叶子状态 + long childCount = sysDepartService.count(new LambdaQueryWrapper() + .eq(SysDepart::getParentId, code) + .eq(SysDepart::getDelFlag, "0")); + int expectedLeaf = childCount == 0 ? CommonConstant.IS_LEAF : CommonConstant.NOT_LEAF; + if (!Integer.valueOf(expectedLeaf).equals(depart.getIzLeaf())) { + boolean updated = sysDepartService.update(new LambdaUpdateWrapper() + .eq(SysDepart::getId, code) + .set(SysDepart::getIzLeaf, expectedLeaf)); + if (updated) { + departmentChanged = true; + } + } + + // 仅当部门信息有变更时才更新缓存 + if (departmentChanged) { + updateCache(depart); + } + + parentId = code; + orgId = code; + } + + return orgId; + } + + /** + * 查询系统根组织ID(parentId为空、orgCategory=1、orgCode=A01) + */ + private String getRootDepartId() { + SysDepart root = sysDepartService.getOne(new LambdaQueryWrapper() + .and(w -> w.isNull(SysDepart::getParentId).or().eq(SysDepart::getParentId, "")) + .eq(SysDepart::getOrgCategory, "1") + .eq(SysDepart::getOrgCode, "A01")); + return root != null ? root.getId() : ""; + } + + /** + * 生成部门 orgCode + *

+ * 查询同级兄弟部门中 orgCode 最大值,提取最后一段 AXX 序号 +1。 + * 格式:一级直接返回 A01,二级为 父级orgCode + A01,后续依此类推。 + * + * @param parentId 父部门ID + * @return 新生成的 orgCode + */ + private String generateOrgCode(String parentId) { + String prefix; + if (StringUtils.isBlank(parentId)) { + prefix = ""; + } else { + SysDepart parent = sysDepartService.getById(parentId); + prefix = parent != null ? parent.getOrgCode() : ""; + } + + List siblings = sysDepartService.list(new LambdaQueryWrapper() + .eq(SysDepart::getParentId, parentId) + .eq(SysDepart::getDelFlag, "0") + .orderByDesc(SysDepart::getOrgCode)); + + // 只取 orgCode 与父级前缀一致的兄弟,过滤掉历史脏数据 + if (siblings != null && StringUtils.isNotBlank(prefix)) { + siblings.removeIf(s -> !s.getOrgCode().startsWith(prefix)); + } + + if (siblings == null || siblings.isEmpty()) { + return prefix + "A01"; + } + + // 取 orgCode 最大的,提取最后一段 AXX 序号 + String maxCode = siblings.get(0).getOrgCode(); + String lastSegment = maxCode.substring(maxCode.length() - 3); + int seq = Integer.parseInt(lastSegment.substring(1)); + return prefix + "A" + String.format("%02d", seq + 1); + } + + /** + * 检查部门 orgCode 是否与父级 orgCode 一致 + *

+ * 子级 orgCode 必须以父级 orgCode 为前缀。 + * 例如父级 A01A06,子级应为 A01A06A01、A01A06A02 等。 + * + * @param childOrgCode 子部门 orgCode + * @param parentId 父部门 ID + * @return true=一致,false=不一致需修正 + */ + private boolean orgCodeMatchesParent(String childOrgCode, String parentId) { + if (StringUtils.isBlank(parentId) || StringUtils.isBlank(childOrgCode)) { + return true; + } + SysDepart parent = sysDepartService.getById(parentId); + if (parent == null || StringUtils.isBlank(parent.getOrgCode())) { + return true; + } + return childOrgCode.startsWith(parent.getOrgCode()); + } + /** * @author: pj * @description: 更新部门缓存