From ee15a3415c5120dcc135debb5c293c00b594df6a Mon Sep 17 00:00:00 2001 From: cjh <949661474@qq.com> Date: Wed, 10 Jun 2026 17:02:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5mq=E5=92=8C=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=AE=A1=E7=90=86=E5=92=8C=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E6=9E=84=E4=BB=B6,=E6=9E=84=E5=BB=BA=E6=A0=91=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 11 +- .../api/controller/PartCodeController.java | 18 +- .../com/bim/api/entity/PartCodeRelation.java | 2 + .../api/service/PartCodeRelationService.java | 40 ++- src/main/java/com/bim/api/util/SignUtil.java | 327 ++++++++++++++++++ src/main/resources/application-dev.yml | 10 +- src/main/resources/application.yml | 17 +- 7 files changed, 413 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/bim/api/util/SignUtil.java diff --git a/pom.xml b/pom.xml index 2ccefc6..70aac75 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,10 @@ org.springframework.boot spring-boot-starter-data-redis + + org.springframework.boot + spring-boot-starter-amqp + com.fasterxml.jackson.core jackson-databind @@ -88,6 +92,11 @@ fastjson2 2.0.32 + + software.amazon.awssdk + s3 + 2.28.22 + @@ -124,4 +133,4 @@ - \ No newline at end of file + diff --git a/src/main/java/com/bim/api/controller/PartCodeController.java b/src/main/java/com/bim/api/controller/PartCodeController.java index 4ae3d63..5a94620 100644 --- a/src/main/java/com/bim/api/controller/PartCodeController.java +++ b/src/main/java/com/bim/api/controller/PartCodeController.java @@ -20,10 +20,11 @@ public class PartCodeController { @GetMapping("/byPart/{partId}") @ResponseBody - public Map getByPartId(@PathVariable String partId) { + public Map getByPartId(@PathVariable String partId, + @RequestParam(required = false) Long modelId) { Map result = new HashMap<>(); try { - List list = service.findByPartId(partId); + List list = service.findByPartId(partId, modelId); result.put("code", 200); result.put("data", list); } catch (Exception e) { @@ -69,6 +70,7 @@ public class PartCodeController { try { Object partIdsObj = params.get("partIds"); Object codeIdsObj = params.get("codeIds"); + Long modelId = parseLong(params.get("modelId")); if (partIdsObj == null || codeIdsObj == null) { result.put("code", 500); @@ -100,7 +102,7 @@ public class PartCodeController { codeDatas[i] = data != null ? data.toString() : ""; } - List list = service.saveBatchWithData(partInfoList, codeIds, codeDatas); + List list = service.saveBatchWithData(partInfoList, codeIds, codeDatas, modelId); result.put("code", 200); result.put("data", list); } catch (Exception e) { @@ -117,6 +119,7 @@ public class PartCodeController { try { Object partIdsObj = params.get("partIds"); Object codeIdsObj = params.get("codeIds"); + Long modelId = parseLong(params.get("modelId")); if (partIdsObj == null || codeIdsObj == null) { result.put("code", 500); @@ -148,7 +151,7 @@ public class PartCodeController { codeDatas[i] = data != null ? data.toString() : ""; } - List list = service.deleteBatch(partInfoList, codeIds, codeDatas); + List list = service.deleteBatch(partInfoList, codeIds, codeDatas, modelId); result.put("code", 200); result.put("data", list); } catch (Exception e) { @@ -157,4 +160,11 @@ public class PartCodeController { } return result; } + + private Long parseLong(Object value) { + if (value == null || String.valueOf(value).isBlank()) { + return null; + } + return Long.valueOf(String.valueOf(value)); + } } diff --git a/src/main/java/com/bim/api/entity/PartCodeRelation.java b/src/main/java/com/bim/api/entity/PartCodeRelation.java index ed2c69a..ec69e81 100644 --- a/src/main/java/com/bim/api/entity/PartCodeRelation.java +++ b/src/main/java/com/bim/api/entity/PartCodeRelation.java @@ -21,4 +21,6 @@ public class PartCodeRelation { private Boolean isLeaf; private LocalDateTime createTime; + + private Long modelId; } diff --git a/src/main/java/com/bim/api/service/PartCodeRelationService.java b/src/main/java/com/bim/api/service/PartCodeRelationService.java index 0f374ec..1827ef1 100644 --- a/src/main/java/com/bim/api/service/PartCodeRelationService.java +++ b/src/main/java/com/bim/api/service/PartCodeRelationService.java @@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -34,6 +35,12 @@ public class PartCodeRelationService extends ServiceImpl().eq(PartCodeRelation::getPartId, partId)); } + public List findByPartId(String partId, Long modelId) { + return list(new LambdaQueryWrapper() + .eq(PartCodeRelation::getPartId, partId) + .eq(modelId != null, PartCodeRelation::getModelId, modelId)); + } + public List> findCodeWbsMappings() { List relations = list(new LambdaQueryWrapper() .orderByAsc(PartCodeRelation::getCodeId) @@ -207,6 +214,11 @@ public class PartCodeRelationService extends ServiceImpl saveBatchWithData(List> partInfoList, String[] codeIds, String[] codeDatas) { + return saveBatchWithData(partInfoList, codeIds, codeDatas, null); + } + + @Transactional + public List saveBatchWithData(List> partInfoList, String[] codeIds, String[] codeDatas, Long modelId) { List result = new ArrayList<>(); if (partInfoList == null || partInfoList.isEmpty()) { return result; @@ -222,12 +234,14 @@ public class PartCodeRelationService extends ServiceImpl() .eq(PartCodeRelation::getPartId, partId) - .eq(PartCodeRelation::getCodeId, codeId)); + .eq(PartCodeRelation::getCodeId, codeId) + .eq(modelId != null, PartCodeRelation::getModelId, modelId)); if (cnt == 0) { PartCodeRelation relation = new PartCodeRelation(); relation.setPartId(partId); relation.setCodeId(codeId); relation.setCodeData(codeData); + relation.setModelId(modelId); relation.setCreateTime(LocalDateTime.now()); String createDate = safeString(partInfo.get("createDate")); if (createDate != null && !createDate.isEmpty()) { @@ -246,6 +260,11 @@ public class PartCodeRelationService extends ServiceImpl deleteBatch(List> partInfoList, String[] codeIds, String[] codeDatas) { + return deleteBatch(partInfoList, codeIds, codeDatas, null); + } + + @Transactional + public List deleteBatch(List> partInfoList, String[] codeIds, String[] codeDatas, Long modelId) { if (partInfoList == null || partInfoList.isEmpty()) { return List.of(); } @@ -261,10 +280,23 @@ public class PartCodeRelationService extends ServiceImpl() - .in(PartCodeRelation::getPartId, partIds)); + List targetCodeIds = Arrays.stream(codeIds == null ? new String[0] : codeIds) + .map(this::safeString) + .filter(codeId -> codeId != null && !codeId.isBlank()) + .distinct() + .toList(); + if (targetCodeIds.isEmpty()) { + return List.of(); + } - return saveBatchWithData(partInfoList, codeIds, codeDatas); + remove(new LambdaQueryWrapper() + .in(PartCodeRelation::getPartId, partIds) + .in(PartCodeRelation::getCodeId, targetCodeIds) + .eq(modelId != null, PartCodeRelation::getModelId, modelId)); + + return list(new LambdaQueryWrapper() + .in(PartCodeRelation::getPartId, partIds) + .eq(modelId != null, PartCodeRelation::getModelId, modelId)); } private String safeString(Object value) { diff --git a/src/main/java/com/bim/api/util/SignUtil.java b/src/main/java/com/bim/api/util/SignUtil.java new file mode 100644 index 0000000..2cf5764 --- /dev/null +++ b/src/main/java/com/bim/api/util/SignUtil.java @@ -0,0 +1,327 @@ +package com.bim.api.util; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.lang.reflect.Array; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * 转换服务签名工具 + *

+ * 按转换服务接口规范生成 HMAC-SHA256 签名: + * 1. 排除 sign 字段 + * 2. 字段名按 ASCII 升序排序 + * 3. 按 key=value 格式拼接,& 连接 + * 4. 对象/数组使用稳定 JSON(无多余空格,字段 ASCII 升序排序) + * 5. 签名算法:Hex(HMAC-SHA256(signString, secretKey)) + * + * @author bim-engine + */ +public class SignUtil { + + private SignUtil() { + } + + /** + * 生成签名 + * + * @param params 请求参数(必须包含 sign 占位字段,值为空字符串或 null) + * @param secretKey 共享密钥 + * @return 十六进制小写签名 + */ + public static String generateSign(Map params, String secretKey) { + try { + String signString = buildSignString(params); + return hmacSha256Hex(signString, secretKey); + } catch (Exception e) { + throw new RuntimeException("生成签名失败: " + e.getMessage(), e); + } + } + + /** + * 验证签名 + * + * @param params 请求参数(包含 sign 字段) + * @param secretKey 共享密钥 + * @param sign 待验证的签名 + * @return true 表示验证通过 + */ + public static boolean verifySign(Map params, String secretKey, String sign) { + if (sign == null || sign.isEmpty()) { + return false; + } + try { + String expectedSign = generateSign(params, secretKey); + return expectedSign.equals(sign); + } catch (Exception e) { + return false; + } + } + + /** + * 验证签名,并允许指定部分字符串字段按原始 JSON 转义值参与签名。 + *

+ * 适用于回调消息中存在 JSON 字符串字段的场景,例如 extraData 在原始消息中为 + * {\"id\":1},解析后会变成 {"id":1},直接用解析值会改变签名串。 + * + * @param params 请求参数(包含 sign 字段) + * @param rawJson 原始 JSON 消息 + * @param rawStringFields 需要保留原始转义内容的顶层字符串字段名 + * @param secretKey 共享密钥 + * @param sign 待验证的签名 + * @return true 表示验证通过 + */ + public static boolean verifySignWithRawStringFields(Map params, String rawJson, + Set rawStringFields, String secretKey, String sign) { + if (sign == null || sign.isEmpty()) { + return false; + } + try { + String signString = buildSignString(params, rawJson, rawStringFields, false); + String expectedSign = hmacSha256Hex(signString, secretKey); + return expectedSign.equals(sign); + } catch (Exception e) { + return false; + } + } + + /** + * 构建待签名字符串 + */ + public static String buildSignString(Map params) { + TreeMap sortedParams = new TreeMap<>(params); + sortedParams.remove("sign"); + + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : sortedParams.entrySet()) { + if (builder.length() > 0) { + builder.append("&"); + } + builder.append(entry.getKey()) + .append("=") + .append(toSignValue(entry.getValue())); + } + return builder.toString(); + } + + /** + * 构建待签名字符串,并对指定顶层字符串字段使用原始 JSON 中的转义内容。 + */ + public static String buildSignString(Map params, String rawJson, Set rawStringFields) { + return buildSignString(params, rawJson, rawStringFields, false); + } + + /** + * 构建待签名字符串 + */ + public static String buildSignString(Map params, String rawJson, Set rawStringFields, boolean nullAsEmpty) { + TreeMap sortedParams = new TreeMap<>(params); + sortedParams.remove("sign"); + + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : sortedParams.entrySet()) { + if (builder.length() > 0) { + builder.append("&"); + } + builder.append(entry.getKey()).append("="); + + String rawStringValue = null; + if (rawStringFields != null && rawStringFields.contains(entry.getKey()) && entry.getValue() instanceof String) { + rawStringValue = extractRawJsonStringValue(rawJson, entry.getKey()); + } + builder.append(rawStringValue == null ? toSignValue(entry.getValue(), nullAsEmpty) : rawStringValue); + } + return builder.toString(); + } + + /** + * 提取顶层 JSON 字符串字段的原始内容,不反转义,保留 \"、\\ 等字符。 + */ + private static String extractRawJsonStringValue(String json, String fieldName) { + if (json == null || fieldName == null) { + return null; + } + String fieldToken = "\"" + fieldName + "\""; + int fieldIndex = json.indexOf(fieldToken); + if (fieldIndex < 0) { + return null; + } + + int colonIndex = json.indexOf(':', fieldIndex + fieldToken.length()); + if (colonIndex < 0) { + return null; + } + + int valueStart = colonIndex + 1; + while (valueStart < json.length() && Character.isWhitespace(json.charAt(valueStart))) { + valueStart++; + } + if (valueStart >= json.length() || json.charAt(valueStart) != '"') { + return null; + } + + StringBuilder rawValue = new StringBuilder(); + for (int i = valueStart + 1; i < json.length(); i++) { + char current = json.charAt(i); + if (current == '\\') { + if (i + 1 < json.length()) { + rawValue.append(current).append(json.charAt(i + 1)); + i++; + } else { + rawValue.append(current); + } + continue; + } + if (current == '"') { + return rawValue.toString(); + } + rawValue.append(current); + } + return null; + } + + /** + * 将值转换为签名字符串中的表示形式 + */ + private static String toSignValue(Object value) { + return toSignValue(value, false); + } + + /** + * 将值转换为签名字符串中的表示形式。 + */ + private static String toSignValue(Object value, boolean nullAsEmpty) { + if (value == null) { + return nullAsEmpty ? "" : "null"; + } + if (value instanceof Map || value instanceof Iterable || value.getClass().isArray()) { + return toStableJson(value); + } + if (value instanceof Boolean) { + return Boolean.TRUE.equals(value) ? "true" : "false"; + } + return String.valueOf(value); + } + + /** + * 生成稳定 JSON 字符串 + *

+ * 规则: + * - 无多余空格 + * - 对象字段按 ASCII 升序排序 + * - 嵌套对象也按 ASCII 升序排序 + * - 数组顺序保持不变 + */ + private static String toStableJson(Object value) { + if (value == null) { + return "null"; + } + if (value instanceof String || value instanceof Character) { + return "\"" + escapeJson(String.valueOf(value)) + "\""; + } + if (value instanceof Number || value instanceof Boolean) { + return String.valueOf(value); + } + if (value instanceof Map) { + TreeMap sortedMap = new TreeMap<>(); + for (Map.Entry entry : ((Map) value).entrySet()) { + sortedMap.put(String.valueOf(entry.getKey()), entry.getValue()); + } + + StringBuilder builder = new StringBuilder("{"); + Iterator> iterator = sortedMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + builder.append("\"") + .append(escapeJson(entry.getKey())) + .append("\":") + .append(toStableJson(entry.getValue())); + if (iterator.hasNext()) { + builder.append(","); + } + } + return builder.append("}").toString(); + } + if (value instanceof Iterable) { + StringBuilder builder = new StringBuilder("["); + Iterator iterator = ((Iterable) value).iterator(); + while (iterator.hasNext()) { + builder.append(toStableJson(iterator.next())); + if (iterator.hasNext()) { + builder.append(","); + } + } + return builder.append("]").toString(); + } + if (value.getClass().isArray()) { + List list = new ArrayList<>(); + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + list.add(Array.get(value, i)); + } + return toStableJson(list); + } + return "\"" + escapeJson(String.valueOf(value)) + "\""; + } + + /** + * JSON 字符串转义 + */ + private static String escapeJson(String value) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '"': + builder.append("\\\""); + break; + case '\\': + builder.append("\\\\"); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (c < 0x20) { + builder.append(String.format("\\u%04x", (int) c)); + } else { + builder.append(c); + } + } + } + return builder.toString(); + } + + /** + * HMAC-SHA256 签名,返回十六进制小写字符串 + */ + private static String hmacSha256Hex(String data, String secretKey) throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + + StringBuilder hex = new StringBuilder(); + for (byte b : bytes) { + hex.append(String.format("%02x", b & 0xff)); + } + return hex.toString(); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index e6b6c99..b408a23 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -5,9 +5,15 @@ spring: password: root data: redis: - host: localhost + host: 113.45.226.10 + # 端口,默认为6379 port: 6379 - database: 0 + # 数据库索引 + database: 13 + # redis 密码必须配置 + password: lyzbim2016 + # 连接超时时间 + timeout: 10s mybatis-plus: configuration: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b6dfc8e..4b24638 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,6 +21,15 @@ spring: multipart: max-file-size: 100MB max-request-size: 100MB + rabbitmq: + host: 38.54.88.163 + port: 5672 + username: iflow + password: iflow + virtual-host: bim_engine + listener: + simple: + acknowledge-mode: auto mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml @@ -31,4 +40,10 @@ mybatis-plus: logging: level: - com.bim.api: debug \ No newline at end of file + com.bim.api: debug + +file-convert: + secret-key: bim-engine-secret + model-queue-name: bim_engine_queue_file_convert + callback-queue-name: system_file_convert_callback_queue + current-language: zh-CN