From 95b33ee3d50afa5c25068f5da93c72530a52b37f Mon Sep 17 00:00:00 2001
From: yangyufu
Date: Tue, 19 May 2026 14:37:56 +0800
Subject: [PATCH] =?UTF-8?q?MDM=20-SAP=20CRM=20=E6=96=B0=E7=B3=BB=E7=BB=9F?=
=?UTF-8?q?=E4=B8=8A=E7=BA=BF=20=E8=B0=83=E6=95=B4=E5=86=85=E5=AE=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../impl/PdmBiContainerinfoServiceImpl.java | 2 +-
.../PdmBiSubpackagerelationServiceImpl.java | 2 +-
.../util/SlitterTaskUtilBakup0519.java | 707 ++++++++++++++++++
.../config/thread/ApiLogExecutorConfig.java | 35 +
.../nl/modules/logging/annotation/ApiLog.java | 22 +
.../modules/logging/aspect/ApiLogAspect.java | 121 +++
.../sysapi/SysApiLogController.java | 159 ++++
.../service/quartz/config/JobRunner.java | 22 +-
.../sysapi/entity/dto/ApiLogQuery.java | 34 +
.../sysapi/mapper/SysApiLogMapper.java | 16 +
.../sysapi/service/ISysApiLogService.java | 17 +
.../sysapi/service/OutboundApiLogger.java | 71 ++
.../sysapi/service/SysApiLogServiceImpl.java | 38 +
.../java/org/nl/wms/basedata/st/wql/stivt.xls | Bin 300032 -> 300544 bytes
.../wms/ext/crm/rest/CrmToLmsController.java | 17 +-
.../wms/ext/mdm/rest/MdmToLmsController.java | 25 +-
.../mdm/service/impl/MdmToLmsServiceImpl.java | 8 +-
.../wms/ext/mes/rest/MesToLmsController.java | 176 ++++-
.../mes/service/impl/LmsToMesServiceImpl.java | 71 +-
.../wms/ext/sap/rest/LmsToSapController.java | 8 +-
.../wms/ext/sap/rest/SapToLmsController.java | 49 +-
.../wms/ext/sap/service/LmsToSapService.java | 17 +
.../sap/service/impl/LmsToSapServiceImpl.java | 80 +-
.../impl/ProductInstorServiceImpl.java | 10 +-
.../nl/wms/pda/st/wql/PDA_PRODUVTIONOUT.wql | 4 +-
.../java/org/nl/wms/sch/AutoQueryEnum.java | 6 +-
.../wms/sch/manage/AutoQueryProudDayData.java | 10 +-
.../nl/wms/sch/manage/AutoQueryUpload.java | 2 +-
.../service/impl/ProductScrapServiceImpl.java | 9 +-
.../st/instor/wql/QST_IVT_PRODUCTSCRAP.wql | 2 +-
.../service/impl/CheckOutBillServiceImpl.java | 2 +-
.../st/outbill/wql/QST_IVT_CHECKOUTBILL.wql | 3 +-
.../impl/InAndOutRetrunServiceImpl.java | 203 +++--
.../main/resources/config/application-dev.yml | 8 +-
lms/nladmin-ui/public/config.js | 2 +-
lms/nladmin-ui/src/api/monitor/sysapilog.js | 17 +
.../src/views/monitor/sysapiLog/index.vue | 366 +++++++++
.../src/views/system/notice/NoticeIcon.vue | 3 -
.../wms/st/inStor/productscrap/AddDialog.vue | 20 +-
.../src/views/wms/st/inbill/DivDialog.vue | 1 -
40 files changed, 2196 insertions(+), 169 deletions(-)
create mode 100644 lms/nladmin-system/src/main/java/org/nl/b_lms/sch/tasks/slitter/util/SlitterTaskUtilBakup0519.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/config/thread/ApiLogExecutorConfig.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/modules/logging/annotation/ApiLog.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/modules/logging/aspect/ApiLogAspect.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/system/controller/sysapi/SysApiLogController.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/system/service/sysapi/entity/dto/ApiLogQuery.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/system/service/sysapi/mapper/SysApiLogMapper.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/system/service/sysapi/service/ISysApiLogService.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/system/service/sysapi/service/OutboundApiLogger.java
create mode 100644 lms/nladmin-system/src/main/java/org/nl/system/service/sysapi/service/SysApiLogServiceImpl.java
create mode 100644 lms/nladmin-ui/src/api/monitor/sysapilog.js
create mode 100644 lms/nladmin-ui/src/views/monitor/sysapiLog/index.vue
diff --git a/lms/nladmin-system/src/main/java/org/nl/b_lms/pdm/info/service/impl/PdmBiContainerinfoServiceImpl.java b/lms/nladmin-system/src/main/java/org/nl/b_lms/pdm/info/service/impl/PdmBiContainerinfoServiceImpl.java
index 62f8f24ce..6fb9f2f4e 100644
--- a/lms/nladmin-system/src/main/java/org/nl/b_lms/pdm/info/service/impl/PdmBiContainerinfoServiceImpl.java
+++ b/lms/nladmin-system/src/main/java/org/nl/b_lms/pdm/info/service/impl/PdmBiContainerinfoServiceImpl.java
@@ -95,7 +95,7 @@ public class PdmBiContainerinfoServiceImpl extends ServiceImpl纸制筒管|纸管|6英寸|1300 or 纸制筒管|纸管|3英寸|12|650 or 玻璃纤维及其制品|FRP管|6英寸|15-20|1700|阶梯
+ * 长度:1300mm
+ * 外径:6*25.4mm+15*2mm=182.4mm
+ * 内径:6英寸(25.4mm/英寸)
+ * 壁厚:15mm(常规)、特殊12mm
+ * 材质:纸管
+ */
+ public static String getPaperTubeInformation(PdmBiSlittingproductionplan plan) {
+ // 纸管描述
+ String tubeDescription;
+ if (SlitterConstant.SLITTER_TYPE_PAPER.equals(plan.getPaper_tube_or_FRP())) {
+ tubeDescription = plan.getPaper_tube_description();
+ } else {
+ tubeDescription = plan.getFRP_description();
+ }
+ // 材质
+ return getComposePaperTubeInformation(tubeDescription, plan.getPaper_tube_or_FRP());
+ }
+
+ /**
+ * 获取组成信息
+ * @param tubeDescription 纸管信息
+ * @param paperOrFrp 材质(1纸管,2FRP管)
+ * @return 长*外径*内径*壁厚*重量*薄壁厚*阶梯长度*材质(1纸管,2FRP管)
+ */
+ public static String getComposePaperTubeInformation(String tubeDescription, String paperOrFrp) {
+ if (ObjectUtil.isEmpty(tubeDescription)) {
+ return "";
+ }
+ return getComposePaperTubeInformation(tubeDescription) + "*" + paperOrFrp;
+ }
+
+ /**
+ * 获取组成信息
+ * @param tubeDescription 纸管信息
+ * @return 长*外径*内径*壁厚*重量*薄壁厚*阶梯长度
+ */
+ public static String getComposePaperTubeInformation(String tubeDescription) {
+ if (ObjectUtil.isEmpty(tubeDescription)) {
+ return "";
+ }
+ boolean flag = tubeDescription.contains("阶梯");
+ tubeDescription = tubeDescription.replaceAll("\\|[\\u4e00-\\u9fa5]+$", "");
+ // 解析描述数组
+ String[] tubeArray = tubeDescription.split("\\|");
+ // 定义尺寸与长度
+ double dia = Double.parseDouble(Character.toString(tubeArray[2].charAt(0)));
+
+ // 假设壁厚默认值为15,如果描述数组长度为4,则重新赋值
+ String th = tubeArray.length == 5 ? tubeArray[3] : "15";
+ String th2 = "0";
+ String jtLen = "0";
+ if (tubeArray[3].contains("-")) {
+ th = tubeArray[3].split("-")[1];
+ th2 = tubeArray[3].split("-")[0];
+ }
+ if (flag) {
+ switch (tubeArray[tubeArray.length - 1]) {
+ case "1400":
+ case "1700":
+ jtLen = "75";
+ break;
+ case "1500":
+ case "1600":
+ jtLen = "150";
+ break;
+ default:
+ jtLen = "0";
+ break;
+ }
+ }
+ // 计算外径和内径
+ double externalDiameter = dia * 25.4 + Double.parseDouble(th) * 2;
+ double internalDiameter = dia * 25.4;
+
+ StringBuilder sb = new StringBuilder();
+ // 长*外径*内径*壁厚*重量*材质(1纸管,2FRP管)
+ // 长
+ sb.append(tubeArray[tubeArray.length - 1]);
+ sb.append("*");
+ // 外径
+ sb.append(NumberUtil.round(externalDiameter, 1).doubleValue());
+ sb.append("*");
+ // 内径
+ sb.append(NumberUtil.round(internalDiameter, 1).doubleValue());
+ sb.append("*");
+ // 壁厚
+ sb.append(th);
+ sb.append("*");
+ // 重量
+ sb.append(0);
+ sb.append("*");
+ // 薄壁厚
+ sb.append(th2);
+ sb.append("*");
+ // 阶梯长度
+ sb.append(jtLen);
+ return sb.toString();
+ }
+
+ public static void main(String[] args) {
+// System.out.println(getComposePaperTubeInformation("玻璃纤维及其制品|FRP管|6英寸|15|1100", "1"));
+ System.out.println(isNumeric(",3000"));
+ System.out.println(isNumeric("3000"));
+ System.out.println(isNumeric("3000.32"));
+ System.out.println(isNumeric("-3000.32"));
+ boolean b = checkComplete("1", "2", null);
+ }
+
+ /**
+ * 设置所需的套管纸管信息
+ * @param param 任务参数
+ * @param needPlans 所需的分切计划
+ */
+ public static void putNeedPaperSpec(JSONObject param, List needPlans) {
+ log.info("正在设置所需的套管纸管信息...");
+ // 纸制筒管|纸管|6英寸|1300 or 纸制筒管|纸管|3英寸|12|650
+ for (PdmBiSlittingproductionplan plan : needPlans) {
+ if (SlitterConstant.SLITTER_SUB_VOLUME_LEFT.equals(plan.getLeft_or_right())) {
+ if (SlitterConstant.SLITTER_TYPE_PAPER.equals(plan.getPaper_tube_or_FRP())) {
+ param.put("left", plan.getPaper_tube_material());
+ param.put("leftSize", plan.getPaper_tube_model().split("\\|")[2].charAt(0));
+ } else {
+ param.put("left", plan.getFRP_material());
+ param.put("leftSize", plan.getFRP_model().split("\\|")[2].charAt(0));
+ }
+ param.put("leftSpec", SlitterTaskUtilBakup0519.getPaperTubeInformation(plan));
+ } else {
+ if (SlitterConstant.SLITTER_TYPE_PAPER.equals(plan.getPaper_tube_or_FRP())) {
+ param.put("right", plan.getPaper_tube_material());
+ param.put("rightSize", plan.getPaper_tube_model().split("\\|")[2].charAt(0));
+ } else {
+ param.put("right", plan.getFRP_material());
+ param.put("rightSize", plan.getFRP_model().split("\\|")[2].charAt(0));
+ }
+ param.put("rightSpec", SlitterTaskUtilBakup0519.getPaperTubeInformation(plan));
+ }
+ }
+ }
+
+ /**
+ * 设置当前的拔管纸管信息
+ * @param param 任务参数
+ * @param oldPlans 老的分切计划
+ */
+ public static void putCurrentPaperSpec(JSONObject param, List oldPlans) {
+ for (PdmBiSlittingproductionplan plan : oldPlans) {
+ if (SlitterConstant.SLITTER_SUB_VOLUME_LEFT.equals(plan.getLeft_or_right())) {
+ if (SlitterConstant.SLITTER_TYPE_PAPER.equals(plan.getPaper_tube_or_FRP())) {
+ param.put("currentLeft", plan.getPaper_tube_material());
+ param.put("currentLeftSize", plan.getPaper_tube_model().split("\\|")[2].charAt(0));
+ } else {
+ param.put("currentLeft", plan.getFRP_material());
+ param.put("currentLeftSize", plan.getFRP_model().split("\\|")[2].charAt(0));
+ }
+ param.put("currentLeftSpec", SlitterTaskUtilBakup0519.getPaperTubeInformation(plan));
+ } else {
+ if (SlitterConstant.SLITTER_TYPE_PAPER.equals(plan.getPaper_tube_or_FRP())) {
+ param.put("currentRight", plan.getPaper_tube_material());
+ param.put("currentRightSize", plan.getPaper_tube_model().split("\\|")[2].charAt(0));
+ } else {
+ param.put("currentRight", plan.getFRP_material());
+ param.put("currentRightSize", plan.getFRP_model().split("\\|")[2].charAt(0));
+ }
+ param.put("currentRightSpec", SlitterTaskUtilBakup0519.getPaperTubeInformation(plan));
+ }
+ }
+ }
+
+ /**
+ * 获取设备号
+ * @param resourceCode /
+ * @return /
+ */
+ public static int getNumberByResourceCode(String resourceCode) {
+ if (ObjectUtil.isEmpty(resourceCode)) {
+ throw new BadRequestException("输入的设备号编码不能为空!");
+ }
+ String trimStr = resourceCode.trim();
+ // 提取最后两位作为字符串
+ String lastTwoDigitsString = trimStr.substring(trimStr.length() - 2);
+ // 将整数再转换回字符串以供返回
+ return Integer.parseInt(lastTwoDigitsString);
+ }
+
+ /**
+ * 获取上下区域
+ * @param num 编码
+ * @param area 区域
+ * @return /
+ */
+ public static String getPointLocationInCutDevice(int num, String area) {
+ if (area.equals(B1_AREA_CODE)) {
+ if (num >= 1 && num <= 6) {
+ return "0";
+ }
+ }
+ if (area.equals(B2_AREA_CODE)) {
+ if (num >= 1 && num <= 5) {
+ return "0";
+ }
+ }
+ return "1";
+ }
+
+ /**
+ * 查询是否包含在内
+ * @param names 数组
+ * @param name 自负床
+ * @return Boolean 是 {@code true} or 否{@code false}
+ */
+ public static boolean containscode(String[] names, String name) {
+ for (String n : names) {
+ if (name.equals(n)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 获取name在数组中的索引
+ * @param names 数组
+ * @param name 校验字符
+ * @return 索引
+ */
+ public static int getIndex(String[] names, String name) {
+ for (int i = 0; i < names.length; i++) {
+ if (name.equals(names[i])) {
+ return i;
+ }
+ }
+ // 如果找不到返回-1
+ return -1;
+ }
+
+ /**
+ * 业务:备货区送纸管托盘时候,将纸管信息带给任务中的参数
+ * @param paperList 备货区纸管信息
+ * @param param 任务参数json
+ */
+ public static void doSavePaperInfos(List paperList, JSONObject param) {
+ String[] material_codes = new String[paperList.size()];
+ String[] material_specs = new String[paperList.size()];
+ int[] qtys = new int[paperList.size()];
+ String[] material_codes1 = {null,null,null,null,null};
+ String[] material_specs1 = {null,null,null,null,null};
+ int[] qtys1 = {0,0,0,0,0};
+ for (int i = 0; i < paperList.size(); i++) {
+ MdPbPapervehicle vehicle = paperList.get(i);
+ String materialCode = vehicle.getMaterial_code();
+ int qty = vehicle.getQty().intValue();
+ if (containscode(material_codes, materialCode)) {
+ int index = getIndex(material_codes, materialCode);
+ qtys[index] += qty;
+ } else {
+ material_codes[i] = vehicle.getMaterial_code();
+ String spec = getComposePaperTubeInformation(vehicle.getMaterial_name(), vehicle.getMaterial_code().startsWith("4") ? "1" : "2");
+ material_specs[i] = spec;
+ qtys[i] = qty;
+ }
+ //新规格数据处理
+ int row_num = Integer.parseInt(vehicle.getRow_num());
+ String spec = getComposePaperTubeInformation(vehicle.getMaterial_name(), vehicle.getMaterial_code().startsWith("4") ? "1" : "2");
+ material_codes1[row_num-1] = materialCode;
+ material_specs1[row_num-1] = spec;
+ qtys1[row_num-1] = qty;
+ }
+ // 转成String
+ String[] qtysStr = Arrays.stream(qtys)
+ .mapToObj(String::valueOf)
+ .toArray(String[]::new);
+ // 转成String
+ String[] qtysStr1 = Arrays.stream(qtys1)
+ .mapToObj(String::valueOf)
+ .toArray(String[]::new);
+ param.put("to_material", String.join(",", material_codes));
+ param.put("to_spec", String.join(",", material_specs));
+ param.put("to_qty", String.join(",", qtysStr));
+
+ param.put("to_material1", String.join(",", material_codes1));
+ param.put("to_spec1", String.join(",", material_specs1));
+ param.put("to_qty1", String.join(",", qtysStr1));
+ param.put("device_code", material_specs);
+ }
+
+ /**
+ * 传入JSONArray返回筛选字符串
+ * @param array /
+ * @param name /
+ * @return /
+ */
+ public static List getAllStringByName(JSONArray array, String name) {
+ List res = new ArrayList<>();
+ for (int i = 0; i < array.size(); i++) {
+ JSONObject jsonObject = array.getJSONObject(i);
+ res.add(jsonObject.getString(name));
+ }
+ return res;
+ }
+
+ /**
+ * 通过分切计划的上下轴返回对应的气胀轴编码
+ * todo: 是否存在问题?
+ * @param plan 分切机
+ * @return 气胀轴编码或者"-"
+ */
+ public static String getQzzNoByUpOrDown(PdmBiSlittingproductionplan plan) {
+ if (SlitterConstant.SLITTER_SHAFT_UP.equals(plan.getUp_or_down())) {
+ return plan.getQzzno();
+ } else {
+ return "-";
+ }
+ }
+
+ /**
+ * 获取纸管长度
+ * @param plan 分切计划
+ * @return /
+ */
+ public static String getPaperLength(PdmBiSlittingproductionplan plan) {
+// String s = "玻璃纤维及其制品|FRP管|6英寸|15|1700";
+ String paperStr = "";
+ if (plan.getPaper_tube_or_FRP().equals(SlitterConstant.SLITTER_TYPE_PAPER)) {
+ paperStr = plan.getPaper_tube_model();
+ } else {
+ paperStr = plan.getFRP_model();
+ }
+ String[] split = paperStr.replaceAll("\\|[\\u4e00-\\u9fa5]+$", "").split("\\|");
+ return split[split.length - 1];
+ }
+
+ /**
+ * 获取: 长*外径*内径*壁厚*重量*薄壁厚*阶梯长度
+ * @param plan
+ * @return
+ */
+ public static String getTubeConvertInfo(PdmBiSlittingproductionplan plan) {
+// String s = "玻璃纤维及其制品|FRP管|6英寸|15|1700";
+ String paperStr = "";
+ if (plan.getPaper_tube_or_FRP().equals(SlitterConstant.SLITTER_TYPE_PAPER)) {
+ paperStr = plan.getPaper_tube_model();
+ } else {
+ paperStr = plan.getFRP_model();
+ }
+ return getComposePaperTubeInformation(paperStr);
+ }
+
+ /**
+ * 获取纸管长度
+ * @param plan 分切计划
+ * @return /
+ */
+ public static String getPaperLengthByCode(String name) {
+// String name = "玻璃纤维及其制品|FRP管|6英寸|15|1700";
+ String[] split = name.replaceAll("\\|[\\u4e00-\\u9fa5]+$", "").split("\\|");
+ return split[split.length - 1];
+ }
+ /**
+ * 获取纸管长度
+ * @param plan 分切计划
+ * @return /
+ */
+ public static Integer getPaperLengthByCodeInt(String name) {
+// String name = "玻璃纤维及其制品|FRP管|6英寸|15|1700";
+ return Integer.valueOf(getPaperLengthByCode(name));
+ }
+ public static String getPaperSize(PdmBiSlittingproductionplan plan) {
+// String s = "玻璃纤维及其制品|FRP管|6英寸|15|1700";
+ String paperStr = "";
+ if (plan.getPaper_tube_or_FRP().equals(SlitterConstant.SLITTER_TYPE_PAPER)) {
+ paperStr = plan.getPaper_tube_model();
+ } else {
+ paperStr = plan.getFRP_model();
+ }
+ String[] split = paperStr.replaceAll("\\|[\\u4e00-\\u9fa5]+$", "").split("\\|");
+ return split[2].replaceAll("英寸","");
+ }
+
+ public static Integer getPaperLengthInt(PdmBiSlittingproductionplan plan) {
+ return Integer.valueOf(getPaperLength(plan));
+ }
+
+ /**
+ * 设置重量
+ * @param plans 分切计划
+ * @return 0,0,0,0
+ */
+ public static void setPaperWeightStr(String weightStr, List plans) {
+ // 1. 校验输入格式
+ String[] parts = weightStr.split(",");
+ if (parts.length != 4) {
+ // 没有数据则不修改
+ return;
+ }
+
+ // 2. 遍历四个位置
+ for (int index = 0; index < 4; index++) {
+ // 解析重量值(自动四舍五入到两位小数)
+ String strValue = parts[index];
+ BigDecimal value;
+ try {
+ value = new BigDecimal(strValue).setScale(2, RoundingMode.HALF_UP);
+ } catch (NumberFormatException e) {
+ continue; // 跳过无效数值
+ }
+
+ // 3. 根据索引确定位置规则
+ String expectedUp, expectedLeft;
+ switch (index) {
+ case 0: // 上左
+ expectedUp = "1";
+ expectedLeft = "1";
+ break;
+ case 1: // 上右
+ expectedUp = "1";
+ expectedLeft = "2";
+ break;
+ case 2: // 下左
+ expectedUp = "2";
+ expectedLeft = "1";
+ break;
+ case 3: // 下右
+ expectedUp = "2";
+ expectedLeft = "2";
+ break;
+ default:
+ throw new IllegalStateException("非法索引: " + index);
+ }
+
+ // 4. 在集合中查找匹配项并更新
+ for (PdmBiSlittingproductionplan plan : plans) {
+ if (expectedUp.equals(plan.getUp_or_down()) &&
+ expectedLeft.equals(plan.getLeft_or_right())) {
+ plan.setPaper_weight(value.toString());
+ break; // 找到后跳出循环
+ }
+ }
+ }
+ }
+
+ /**
+ * 获取重量
+ * @param plans 分切计划
+ * @return 0,0,0,0
+ */
+ public static String getPaperWeightStr(List plans) {
+ String[] weights = new String[4];
+ Arrays.fill(weights, "0");
+
+ for (PdmBiSlittingproductionplan plan : plans) {
+ // 1. 获取重量并四舍五入
+ String weightStr = plan.getPaper_weight();
+ BigDecimal weight;
+ try {
+ weight = new BigDecimal(weightStr);
+ } catch (Exception e) {
+ continue;
+ }
+ BigDecimal rounded = weight.setScale(2, RoundingMode.HALF_UP);
+
+ // 2. 根据位置确定数组索引
+ String upDown = plan.getUp_or_down();
+ String leftRight = plan.getLeft_or_right();
+ int index = -1;
+
+ if ("1".equals(upDown)) {
+ index = ("1".equals(leftRight)) ? 0 : 1;
+ } else {
+ index = ("1".equals(leftRight)) ? 2 : 3;
+ }
+
+ // 3. 更新对应位置的重量(格式化为两位小数)
+ if (index >= 0 && index < 4) {
+ weights[index] = rounded.toString();
+ }
+ }
+
+ // 4. 拼接结果字符串
+ return String.join(",", weights);
+ }
+
+ /**
+ * 固定输入与输出气胀轴库点位
+ * @param input
+ * @return
+ */
+ public static String getQzzkMappedValue(String input) {
+ switch (input) {
+ case "B_QZZK01": return "B_QZZK02";
+ case "B_QZZK02": return "B_QZZK01";
+ case "B_QZZK03": return "B_QZZK04";
+ case "B_QZZK04": return "B_QZZK03";
+ default: throw new BadRequestException("站点输入错误: " + input);
+ }
+ }
+
+ /**
+ * 下卷前置校验,判断卷的上下轴属性与实际是否一致 - 校验子卷属性
+ * @param jsonArray 子卷数组
+ * @param plans 计划
+ */
+ public static void validateConsistency(JSONArray jsonArray, List plans) {
+ if (plans.size() == 0) {
+ throw new BadRequestException("计划未找到,请确保MES已推送。");
+ }
+ // 1. 将 List 转为 Map
+ Map planMap = plans.stream()
+ .collect(Collectors.toMap(
+ p -> p.getContainer_name(),
+ p -> p.getUp_or_down(),
+ // 如果有重复 key,保留第一个(按需调整)
+ (existing, replacement) -> existing
+ ));
+
+ // 2. 遍历 JSONArray
+ for (int i = 0; i < jsonArray.size(); i++) {
+ JSONObject item = jsonArray.getJSONObject(i);
+ String containerName = item.getString("container_name");
+ String site = item.getString("site");
+
+ // 3. 检查 Plan 中是否存在对应的 container_name
+ if (!planMap.containsKey(containerName)) {
+ throw new BadRequestException("校验失败: container_name " + containerName + " 在计划列表中不存在");
+ }
+
+ // 4. 比较 site 和 up_or_down 是否一致
+ String expectedUpDown = planMap.get(containerName);
+ if (!expectedUpDown.equals(site)) {
+ throw new BadRequestException("校验失败: 子卷号 " + containerName
+ + " 的上下轴位置"
+ + "与MES分切计划中的上下轴位置" + expectedUpDown + "不一致!");
+ }
+ }
+ log.info("分切下卷计划位置校验通过!");
+ }
+
+ /**
+ * 判断管芯行家对接位是否有所需的管芯、并且数量是符合的。
+ * @param tubeCodes
+ * @param needTubes
+ * @param papers
+ * @return
+ */
+ public static boolean containsAllTubes(List tubeCodes, List needTubes, List papers) {
+ Set tubeSet = new HashSet<>(tubeCodes);
+ for (String tube : needTubes) {
+ if (!tubeSet.contains(tube)) {
+ return false;
+ }
+ }
+ int num = 0;
+ for (String needTube : needTubes) {
+ for (MdPbPapervehicle paper : papers) {
+ if (needTube.equals(paper.getMaterial_code())) {
+ num += paper.getQty().intValue();
+ }
+ }
+ if (num == 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 组成映射Map
+ * @param tubes
+ * @return
+ */
+ public static Map countTubes(List tubes) {
+ if (tubes == null) {
+ return new HashMap<>();
+ }
+ return tubes.stream()
+ .filter(tube -> tube != null)
+ .collect(Collectors.toMap(
+ Function.identity(),
+ value -> 1,
+ Integer::sum
+ ));
+ }
+
+
+ public static List mapList(Collection from, Function func) {
+ if (CollUtil.isEmpty(from)) {
+ return new ArrayList<>();
+ }
+ return from.stream().map(func).collect(Collectors.toList());
+ }
+
+ /**
+ * 转换List
+ * @param obj
+ * @return
+ */
+ public static List objectToList(Object obj) {
+ // 安全转换为List
+ List errorList = new ArrayList<>();
+ if (obj instanceof List) {
+ for (Object item : (List>) obj) {
+ if (item instanceof String) {
+ errorList.add((String) item);
+ } else {
+ // 非字符串元素处理(按需调整)
+ errorList.add(item.toString());
+ }
+ }
+ } else if (obj != null) {
+ // 如果存储的不是List(如JSON字符串),需额外处理
+ throw new IllegalStateException("Expected List type from Redis, but got: " + obj.getClass());
+ }
+ return errorList;
+ }
+
+ /**
+ * 判断字符串是否为数字(整数、小数、负数)
+ */
+ public static boolean isNumeric(String str) {
+ if (str == null || str.isEmpty() || str.trim().isEmpty()) {
+ return false;
+ }
+ String s = str.trim();
+ return s.matches("-?\\d+(\\.\\d+)?");
+ }
+
+ /**
+ * 单根轴的数据判断:根据指定的尺寸类型,校验左侧或右侧电流数据是否完整。
+ * 当 size 为 1 时,只需一侧数据完整即返回 true;
+ * 当 size 为 2 时,要求左右两侧数据均完整才返回 true。
+ *
+ * @param param 包含相关字段的 JSON 对象,必须包含以下键:
+ * "currentLeft", "currentLeftSize", "currentLeftSpec",
+ * "currentRight", "currentRightSize", "currentRightSpec"
+ * @param size 校验模式大小,决定校验逻辑:
+ * - 1:表示任一轴(左或右)数据完整即可(逻辑或)
+ * - 2:表示左右两轴数据都必须完整(逻辑与)
+ * @return 符合校验规则时返回 true,否则返回 false
+ */
+ public static boolean singleShaftCheck(JSONObject param, int size) {
+ String currentLeft = param.getString("currentLeft");
+ String currentLeftSize = param.getString("currentLeftSize");
+ String currentLeftSpec = param.getString("currentLeftSpec");
+ String currentRight = param.getString("currentRight");
+ String currentRightSize = param.getString("currentRightSize");
+ String currentRightSpec = param.getString("currentRightSpec");
+
+ // 根据 size 值执行不同的校验策略:1 表示任一轴有效即可,2 表示双轴均需有效
+ switch (size) {
+ case 1:
+ return checkComplete(currentLeft, currentLeftSpec, currentLeftSize)
+ || checkComplete(currentRight, currentRightSpec, currentRightSize);
+ case 2:
+ return checkComplete(currentLeft, currentLeftSpec, currentLeftSize)
+ && checkComplete(currentRight, currentRightSpec, currentRightSize);
+ }
+ return false;
+ }
+
+
+ public static boolean checkComplete(String... checks) {
+ if (checks == null) {
+ return false;
+ }
+ for (String check : checks) {
+ if (ObjectUtil.isEmpty(check)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/lms/nladmin-system/src/main/java/org/nl/config/thread/ApiLogExecutorConfig.java b/lms/nladmin-system/src/main/java/org/nl/config/thread/ApiLogExecutorConfig.java
new file mode 100644
index 000000000..80927a4c3
--- /dev/null
+++ b/lms/nladmin-system/src/main/java/org/nl/config/thread/ApiLogExecutorConfig.java
@@ -0,0 +1,35 @@
+package org.nl.config.thread;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * @author ManMan.Yang
+ * @version V1.1
+ * @date 2026/5/14
+ * @description 外部系统接口执行异步线程池配置
+ */
+
+@Configuration
+@Slf4j
+public class ApiLogExecutorConfig {
+
+ @Bean("apiLogExecutor")
+ public Executor apiLogExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(5);
+ executor.setMaxPoolSize(10);
+ executor.setQueueCapacity(200);
+ executor.setKeepAliveSeconds(60);
+ executor.setThreadNamePrefix("sys-api-log-");
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ executor.setWaitForTasksToCompleteOnShutdown(true);
+ executor.setAwaitTerminationSeconds(60);
+ executor.initialize();
+ return executor;
+ }
+}
diff --git a/lms/nladmin-system/src/main/java/org/nl/modules/logging/annotation/ApiLog.java b/lms/nladmin-system/src/main/java/org/nl/modules/logging/annotation/ApiLog.java
new file mode 100644
index 000000000..5e0545e25
--- /dev/null
+++ b/lms/nladmin-system/src/main/java/org/nl/modules/logging/annotation/ApiLog.java
@@ -0,0 +1,22 @@
+package org.nl.modules.logging.annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author ManMan.Yang
+ * @version V1.1
+ * @date 2026/5/14
+ * @description 注解
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiLog {
+
+ String bizCode() default "";
+
+ String bizDesc() default "";
+
+ String systemFlag() default "";
+}
diff --git a/lms/nladmin-system/src/main/java/org/nl/modules/logging/aspect/ApiLogAspect.java b/lms/nladmin-system/src/main/java/org/nl/modules/logging/aspect/ApiLogAspect.java
new file mode 100644
index 000000000..7f87244a6
--- /dev/null
+++ b/lms/nladmin-system/src/main/java/org/nl/modules/logging/aspect/ApiLogAspect.java
@@ -0,0 +1,121 @@
+package org.nl.modules.logging.aspect;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.nl.common.utils.IdUtil;
+import org.nl.modules.logging.annotation.ApiLog;
+import org.nl.system.service.sysapi.entity.SysApiLog;
+import org.nl.system.service.sysapi.service.ISysApiLogService;
+import org.slf4j.MDC;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author ManMan.Yang
+ * @version V1.1
+ * @date 2026/5/14
+ * @description aop
+ */
+
+
+@Aspect
+@Component
+@Slf4j
+public class ApiLogAspect {
+
+ private final ISysApiLogService apiLogService;
+
+ public ApiLogAspect(ISysApiLogService apiLogService) {
+ this.apiLogService = apiLogService;
+ }
+
+ @Around("@annotation(apiLog)")
+ public Object around(ProceedingJoinPoint joinPoint, ApiLog apiLog) throws Throwable {
+ long startTime = System.currentTimeMillis();
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+
+ SysApiLog logEntity = new SysApiLog();
+ logEntity.setLogId(IdUtil.getStringId());
+ logEntity.setDirection(1);
+ logEntity.setSystemFlag(apiLog.systemFlag());
+ logEntity.setBizCode(apiLog.bizCode());
+ logEntity.setBizDesc(apiLog.bizDesc());
+ logEntity.setTraceId(MDC.get("traceId"));
+ logEntity.setCreateTime(DateUtil.now());
+
+ try {
+ if (attributes != null) {
+ HttpServletRequest request = attributes.getRequest();
+ logEntity.setApiUrl(request.getRequestURI());
+ logEntity.setRequestMethod(request.getMethod());
+ logEntity.setRequestIp(getIpAddress(request));
+ logEntity.setRequestHeaders(JSONUtil.toJsonStr(getRequestHeaders(request)));
+ }
+
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ Method method = signature.getMethod();
+ Object[] args = joinPoint.getArgs();
+ logEntity.setRequestParams(buildRequestParams(method, args));
+
+ Object result = joinPoint.proceed();
+
+ long costTime = System.currentTimeMillis() - startTime;
+ logEntity.setCostTime(costTime);
+ logEntity.setResponseStatus(200);
+ logEntity.setStatus("SUCCESS");
+ logEntity.setResponseBody(JSONUtil.toJsonStr(result));
+
+ return result;
+
+ } catch (Throwable throwable) {
+ long costTime = System.currentTimeMillis() - startTime;
+ logEntity.setCostTime(costTime);
+ logEntity.setStatus("FAIL");
+ logEntity.setErrorMsg(throwable.getMessage());
+
+ throw throwable;
+
+ } finally {
+ apiLogService.saveAsync(logEntity);
+ }
+ }
+
+ private String getIpAddress(HttpServletRequest request) {
+ String ip = request.getHeader("X-Forwarded-For");
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("X-Real-IP");
+ }
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getRemoteAddr();
+ }
+ return ip;
+ }
+
+ private Map getRequestHeaders(HttpServletRequest request) {
+ Map headers = new HashMap<>();
+ headers.put("Content-Type", request.getContentType());
+ headers.put("User-Agent", request.getHeader("User-Agent"));
+ return headers;
+ }
+
+ private String buildRequestParams(Method method, Object[] args) {
+ try {
+ if (args == null || args.length == 0) {
+ return "{}";
+ }
+ return JSONUtil.toJsonStr(args);
+ } catch (Exception e) {
+ return "参数序列化失败";
+ }
+ }
+}
diff --git a/lms/nladmin-system/src/main/java/org/nl/system/controller/sysapi/SysApiLogController.java b/lms/nladmin-system/src/main/java/org/nl/system/controller/sysapi/SysApiLogController.java
new file mode 100644
index 000000000..4d7e65e5c
--- /dev/null
+++ b/lms/nladmin-system/src/main/java/org/nl/system/controller/sysapi/SysApiLogController.java
@@ -0,0 +1,159 @@
+package org.nl.system.controller.sysapi;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.nl.common.domain.query.PageQuery;
+import org.nl.modules.logging.annotation.Log;
+import org.nl.system.service.sysapi.entity.SysApiLog;
+import org.nl.system.service.sysapi.entity.dto.ApiLogQuery;
+import org.nl.system.service.sysapi.service.ISysApiLogService;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author ManMan.Yang
+ * @version V1.1
+ * @date 2026/5/14
+ * @description 外部系统接口日志控制器
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/sysApiLog")
+@Slf4j
+public class SysApiLogController {
+
+ private final ISysApiLogService apiLogService;
+
+ /**
+ * 分页查询接口日志列表
+ */
+ @GetMapping
+ @Log("查询接口日志列表")
+ public ResponseEntity