From 2e7dbd705fc82e8db74b073e55af938d67d8c19f Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期二, 17 三月 2026 09:05:49 +0800
Subject: [PATCH] #
---
src/main/webapp/static/js/devicePingLog/devicePingLog.js | 466 +++++++
src/main/webapp/static/js/dashboard/dashboard.js | 173 ++
src/main/java/com/zy/asrs/controller/DevicePingLogController.java | 118 +
src/main/java/com/zy/asrs/service/DevicePingFileStorageService.java | 550 ++++++++
src/main/java/com/zy/core/task/DevicePingScheduler.java | 414 ++++++
src/main/webapp/views/dashboard/dashboard.html | 308 +++
src/main/resources/sql/20260316_add_device_ping_log_menu.sql | 71 +
/dev/null | 922 --------------
src/main/webapp/views/index.html | 113 +
src/main/java/com/zy/asrs/domain/DevicePingSample.java | 25
src/main/java/com/zy/system/controller/DashboardController.java | 169 ++
src/main/resources/application.yml | 17
src/main/webapp/views/devicePingLog/devicePingLog.html | 480 +++++++
13 files changed, 2,802 insertions(+), 1,024 deletions(-)
diff --git a/src/main/java/com/zy/asrs/controller/DevicePingLogController.java b/src/main/java/com/zy/asrs/controller/DevicePingLogController.java
new file mode 100644
index 0000000..bfce3d5
--- /dev/null
+++ b/src/main/java/com/zy/asrs/controller/DevicePingLogController.java
@@ -0,0 +1,118 @@
+package com.zy.asrs.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.annotations.ManagerAuth;
+import com.core.common.R;
+import com.zy.asrs.entity.DeviceConfig;
+import com.zy.asrs.service.DeviceConfigService;
+import com.zy.asrs.service.DevicePingFileStorageService;
+import com.zy.common.web.BaseController;
+import com.zy.core.enums.SlaveType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+public class DevicePingLogController extends BaseController {
+
+ @Autowired
+ private DeviceConfigService deviceConfigService;
+
+ @Autowired
+ private DevicePingFileStorageService devicePingFileStorageService;
+
+ @Value("${devicePingStorage.intervalMs:1000}")
+ private int intervalMs;
+
+ @Value("${devicePingStorage.timeoutMs:800}")
+ private int timeoutMs;
+
+ @Value("${devicePingStorage.probeCount:3}")
+ private int probeCount;
+
+ @Value("${devicePingStorage.packetSize:-1}")
+ private int packetSize;
+
+ @RequestMapping("/devicePingLog/options/auth")
+ @ManagerAuth
+ public R options() {
+ List<DeviceConfig> configs = listConfigs();
+ if (configs == null) {
+ return R.error("璇诲彇璁惧閰嶇疆澶辫触");
+ }
+ List<Map<String, Object>> list = new ArrayList<>();
+ for (DeviceConfig config : configs) {
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("deviceType", config.getDeviceType());
+ item.put("deviceNo", config.getDeviceNo());
+ item.put("ip", config.getIp());
+ item.put("port", config.getPort());
+ item.put("label", config.getDeviceType() + "-" + config.getDeviceNo() + " (" + config.getIp() + ")");
+ list.add(item);
+ }
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("devices", list);
+ result.put("days", devicePingFileStorageService.listDays());
+ Map<String, Object> samplingConfig = new LinkedHashMap<>();
+ samplingConfig.put("intervalMs", intervalMs);
+ samplingConfig.put("timeoutMs", timeoutMs);
+ samplingConfig.put("probeCount", probeCount);
+ samplingConfig.put("packetSize", packetSize);
+ result.put("samplingConfig", samplingConfig);
+ return R.ok(result);
+ }
+
+ @RequestMapping("/devicePingLog/overview/auth")
+ @ManagerAuth
+ public R overview() {
+ List<DeviceConfig> configs = listConfigs();
+ if (configs == null) {
+ return R.error("璇诲彇璁惧閰嶇疆澶辫触");
+ }
+ return R.ok(devicePingFileStorageService.queryOverview(configs));
+ }
+
+ @RequestMapping("/devicePingLog/trend/auth")
+ @ManagerAuth
+ public R trend(@RequestParam String deviceType,
+ @RequestParam Integer deviceNo,
+ @RequestParam Long startTime,
+ @RequestParam Long endTime,
+ @RequestParam(required = false) Integer bucketSec) {
+ if (SlaveType.findInstance(deviceType) == null) {
+ return R.error("璁惧绫诲瀷閿欒");
+ }
+ if (deviceNo == null) {
+ return R.error("璁惧缂栧彿涓嶈兘涓虹┖");
+ }
+ if (startTime == null || endTime == null || startTime <= 0 || endTime <= 0 || endTime < startTime) {
+ return R.error("鏃堕棿鑼冨洿閿欒");
+ }
+ DeviceConfig config = deviceConfigService.getOne(new QueryWrapper<DeviceConfig>()
+ .eq("device_type", deviceType)
+ .eq("device_no", deviceNo)
+ .last("limit 1"));
+ if (config == null) {
+ return R.error("鏈壘鍒板搴旇澶囬厤缃�");
+ }
+ return R.ok(devicePingFileStorageService.queryTrend(config, startTime, endTime, bucketSec));
+ }
+
+ private List<DeviceConfig> listConfigs() {
+ try {
+ return deviceConfigService.list(new QueryWrapper<DeviceConfig>()
+ .isNotNull("ip")
+ .ne("ip", "")
+ .orderBy(true, true, "device_type", "device_no"));
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/zy/asrs/domain/DevicePingSample.java b/src/main/java/com/zy/asrs/domain/DevicePingSample.java
new file mode 100644
index 0000000..bfcb268
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/DevicePingSample.java
@@ -0,0 +1,25 @@
+package com.zy.asrs.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class DevicePingSample {
+
+ private String deviceType;
+ private Integer deviceNo;
+ private String ip;
+ private Integer port;
+ private Date createTime;
+ private Boolean reachable;
+ private Long latencyMs;
+ private Long avgLatencyMs;
+ private Long minLatencyMs;
+ private Long maxLatencyMs;
+ private Integer packetSize;
+ private Integer probeCount;
+ private Integer successProbeCount;
+ private String status;
+ private String message;
+}
diff --git a/src/main/java/com/zy/asrs/service/DevicePingFileStorageService.java b/src/main/java/com/zy/asrs/service/DevicePingFileStorageService.java
new file mode 100644
index 0000000..5c6f686
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/DevicePingFileStorageService.java
@@ -0,0 +1,550 @@
+package com.zy.asrs.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.zy.asrs.domain.DevicePingSample;
+import com.zy.asrs.entity.DeviceConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Stream;
+
+@Service
+public class DevicePingFileStorageService {
+
+ private static final ZoneId ZONE_ID = ZoneId.systemDefault();
+ private static final DateTimeFormatter DAY_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
+ private static final DateTimeFormatter HOUR_FORMAT = DateTimeFormatter.ofPattern("HH");
+ private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ @Value("${devicePingStorage.loggingPath}")
+ private String loggingPath;
+
+ @Value("${devicePingStorage.expireDays:7}")
+ private Integer expireDays;
+
+ @Value("${devicePingStorage.packetSize:-1}")
+ private Integer packetSize;
+
+ public void appendSamples(List<DevicePingSample> samples) {
+ if (samples == null || samples.isEmpty()) {
+ return;
+ }
+ Map<Path, List<DevicePingSample>> grouped = new LinkedHashMap<>();
+ for (DevicePingSample sample : samples) {
+ if (sample == null || sample.getCreateTime() == null) {
+ continue;
+ }
+ grouped.computeIfAbsent(resolveFilePath(sample), k -> new ArrayList<>()).add(sample);
+ }
+ for (Map.Entry<Path, List<DevicePingSample>> entry : grouped.entrySet()) {
+ Path path = entry.getKey();
+ List<DevicePingSample> fileSamples = entry.getValue();
+ fileSamples.sort(Comparator.comparing(DevicePingSample::getCreateTime, Comparator.nullsLast(Date::compareTo)));
+ try {
+ Files.createDirectories(path.getParent());
+ try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
+ for (DevicePingSample sample : fileSamples) {
+ writer.write(JSON.toJSONStringWithDateFormat(sample, "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteDateUseDateFormat));
+ writer.newLine();
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ public List<String> listDays() {
+ Path baseDir = Paths.get(loggingPath);
+ if (!Files.exists(baseDir)) {
+ return new ArrayList<>();
+ }
+ try {
+ List<String> days = new ArrayList<>();
+ try (Stream<Path> stream = Files.list(baseDir)) {
+ stream
+ .filter(Files::isDirectory)
+ .forEach(path -> {
+ String day = path.getFileName().toString();
+ if (day.length() == 8 && day.chars().allMatch(Character::isDigit)) {
+ days.add(day);
+ }
+ });
+ }
+ days.sort(Comparator.reverseOrder());
+ return days;
+ } catch (Exception ignored) {
+ return new ArrayList<>();
+ }
+ }
+
+ public Map<String, Object> queryTrend(DeviceConfig deviceConfig, long startTime, long endTime, Integer bucketSec) {
+ if (deviceConfig == null || startTime <= 0 || endTime <= 0 || endTime < startTime) {
+ return Collections.emptyMap();
+ }
+ TrendAccumulator summary = new TrendAccumulator();
+ List<Map<String, Object>> series = new ArrayList<>();
+ List<Map<String, Object>> alerts = new ArrayList<>();
+
+ for (Path file : resolveRangeFiles(deviceConfig.getDeviceType(), deviceConfig.getDeviceNo(), startTime, endTime)) {
+ if (!Files.exists(file)) {
+ continue;
+ }
+ try {
+ try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
+ lines.forEach(line -> {
+ if (line == null || line.trim().isEmpty()) {
+ return;
+ }
+ DevicePingSample sample;
+ try {
+ sample = JSON.parseObject(line, DevicePingSample.class);
+ } catch (Exception ex) {
+ return;
+ }
+ if (sample == null || sample.getCreateTime() == null) {
+ return;
+ }
+ long ts = sample.getCreateTime().getTime();
+ if (ts < startTime || ts > endTime) {
+ return;
+ }
+ summary.add(sample);
+ Map<String, Object> point = new LinkedHashMap<>();
+ point.put("time", ts);
+ point.put("timeLabel", formatDateTime(ts));
+ point.put("reachable", Boolean.TRUE.equals(sample.getReachable()));
+ point.put("status", sample.getStatus());
+ point.put("message", sample.getMessage());
+ point.put("latencyMs", sample.getLatencyMs());
+ point.put("avgLatencyMs", sample.getAvgLatencyMs());
+ point.put("minLatencyMs", sample.getMinLatencyMs());
+ point.put("maxLatencyMs", sample.getMaxLatencyMs());
+ point.put("packetSize", resolvePacketSize(sample.getPacketSize()));
+ point.put("probeCount", sample.getProbeCount());
+ point.put("successProbeCount", sample.getSuccessProbeCount());
+ point.put("successRate", round2(summaryRate(sample.getSuccessProbeCount(), sample.getProbeCount())));
+ point.put("failProbeCount", Math.max(0, safeInt(sample.getProbeCount()) - safeInt(sample.getSuccessProbeCount())));
+ series.add(point);
+ if (!"OK".equalsIgnoreCase(safeText(sample.getStatus(), "")) && alerts.size() < 120) {
+ Map<String, Object> alert = new LinkedHashMap<>();
+ alert.put("time", ts);
+ alert.put("timeLabel", formatDateTime(ts));
+ alert.put("status", sample.getStatus());
+ alert.put("message", safeText(sample.getMessage(), "鎺㈡祴澶辫触"));
+ alert.put("ip", sample.getIp());
+ alerts.add(alert);
+ }
+ });
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ series.sort(Comparator.comparingLong(item -> Long.parseLong(String.valueOf(item.get("time")))));
+
+ Map<String, Object> device = new LinkedHashMap<>();
+ device.put("deviceType", deviceConfig.getDeviceType());
+ device.put("deviceNo", deviceConfig.getDeviceNo());
+ device.put("ip", deviceConfig.getIp());
+ device.put("port", deviceConfig.getPort());
+ device.put("label", buildDeviceLabel(deviceConfig));
+ device.put("packetSize", resolvePacketSize(summary.latestPacketSize));
+
+ Map<String, Object> summaryMap = new LinkedHashMap<>();
+ summaryMap.put("totalSamples", summary.totalCount);
+ summaryMap.put("successSamples", summary.successCount);
+ summaryMap.put("failSamples", summary.failCount);
+ summaryMap.put("successRate", round2(summary.successRate()));
+ summaryMap.put("avgLatencyMs", summary.latestAvgLatency);
+ summaryMap.put("minLatencyMs", summary.latestMinLatency);
+ summaryMap.put("maxLatencyMs", summary.latestMaxLatency);
+ summaryMap.put("latestStatus", summary.latestStatus);
+ summaryMap.put("latestTime", summary.latestTime);
+ summaryMap.put("latestTimeLabel", summary.latestTime <= 0 ? "" : formatDateTime(summary.latestTime));
+ summaryMap.put("packetSize", resolvePacketSize(summary.latestPacketSize));
+ summaryMap.put("bucketSec", 1);
+ summaryMap.put("startTime", startTime);
+ summaryMap.put("endTime", endTime);
+ summaryMap.put("startTimeLabel", formatDateTime(startTime));
+ summaryMap.put("endTimeLabel", formatDateTime(endTime));
+
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("device", device);
+ result.put("summary", summaryMap);
+ result.put("series", series);
+ result.put("alerts", alerts);
+ return result;
+ }
+
+ public Map<String, Object> queryOverview(List<DeviceConfig> deviceConfigs) {
+ List<Map<String, Object>> devices = new ArrayList<>();
+ if (deviceConfigs == null || deviceConfigs.isEmpty()) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("summary", buildOverviewSummary(devices));
+ result.put("devices", devices);
+ return result;
+ }
+
+ for (DeviceConfig config : deviceConfigs) {
+ DevicePingSample latestSample = findLatestSample(config);
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("deviceType", config.getDeviceType());
+ item.put("deviceNo", config.getDeviceNo());
+ item.put("ip", config.getIp());
+ item.put("port", config.getPort());
+ item.put("label", buildDeviceLabel(config));
+ item.put("packetSize", resolvePacketSize(latestSample == null ? null : latestSample.getPacketSize()));
+ if (latestSample == null) {
+ item.put("status", "NO_DATA");
+ item.put("statusText", "鏆傛棤鏁版嵁");
+ item.put("statusLevel", 3);
+ item.put("reachable", false);
+ item.put("successRate", null);
+ item.put("avgLatencyMs", null);
+ item.put("minLatencyMs", null);
+ item.put("maxLatencyMs", null);
+ item.put("latestTime", null);
+ item.put("latestTimeLabel", "--");
+ item.put("message", "");
+ } else {
+ item.put("status", safeText(latestSample.getStatus(), "UNKNOWN"));
+ item.put("statusText", resolveStatusText(latestSample.getStatus()));
+ item.put("statusLevel", resolveStatusLevel(latestSample.getStatus()));
+ item.put("reachable", Boolean.TRUE.equals(latestSample.getReachable()));
+ item.put("successRate", round2(summaryRate(latestSample.getSuccessProbeCount(), latestSample.getProbeCount())));
+ item.put("avgLatencyMs", latestSample.getAvgLatencyMs());
+ item.put("minLatencyMs", latestSample.getMinLatencyMs());
+ item.put("maxLatencyMs", latestSample.getMaxLatencyMs());
+ item.put("latestTime", latestSample.getCreateTime() == null ? null : latestSample.getCreateTime().getTime());
+ item.put("latestTimeLabel", latestSample.getCreateTime() == null ? "--" : formatDateTime(latestSample.getCreateTime().getTime()));
+ item.put("message", safeText(latestSample.getMessage(), ""));
+ }
+ devices.add(item);
+ }
+
+ devices.sort((a, b) -> {
+ int levelDiff = toInt(a.get("statusLevel")) - toInt(b.get("statusLevel"));
+ if (levelDiff != 0) {
+ return levelDiff;
+ }
+ int typeDiff = safeText(String.valueOf(a.get("deviceType")), "").compareTo(safeText(String.valueOf(b.get("deviceType")), ""));
+ if (typeDiff != 0) {
+ return typeDiff;
+ }
+ return Integer.compare(toInt(a.get("deviceNo")), toInt(b.get("deviceNo")));
+ });
+
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("summary", buildOverviewSummary(devices));
+ result.put("devices", devices);
+ return result;
+ }
+
+ public void cleanupExpired() {
+ Path baseDir = Paths.get(loggingPath);
+ if (!Files.exists(baseDir) || expireDays == null || expireDays <= 0) {
+ return;
+ }
+ long cutoff = System.currentTimeMillis() - expireDays * 24L * 60L * 60L * 1000L;
+ try {
+ try (Stream<Path> stream = Files.list(baseDir)) {
+ stream
+ .filter(Files::isDirectory)
+ .forEach(path -> {
+ String day = path.getFileName().toString();
+ if (day.length() != 8 || !day.chars().allMatch(Character::isDigit)) {
+ return;
+ }
+ try {
+ LocalDate date = LocalDate.parse(day, DAY_FORMAT);
+ long startOfDay = date.atStartOfDay(ZONE_ID).toInstant().toEpochMilli();
+ if (startOfDay >= cutoff) {
+ return;
+ }
+ try (Stream<Path> walk = Files.walk(path)) {
+ walk.sorted(Comparator.reverseOrder())
+ .forEach(p -> {
+ try {
+ Files.deleteIfExists(p);
+ } catch (Exception ignored) {
+ }
+ });
+ }
+ } catch (Exception ignored) {
+ }
+ });
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ private Path resolveFilePath(DevicePingSample sample) {
+ LocalDateTime dateTime = LocalDateTime.ofInstant(sample.getCreateTime().toInstant(), ZONE_ID);
+ String day = DAY_FORMAT.format(dateTime);
+ String hour = HOUR_FORMAT.format(dateTime);
+ String fileName = sample.getDeviceType() + "_" + sample.getDeviceNo() + "_" + day + "_" + hour + ".log";
+ return Paths.get(loggingPath, day, fileName);
+ }
+
+ private List<Path> resolveRangeFiles(String deviceType, Integer deviceNo, long startTime, long endTime) {
+ List<Path> paths = new ArrayList<>();
+ LocalDateTime current = LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZONE_ID)
+ .withMinute(0).withSecond(0).withNano(0);
+ LocalDateTime end = LocalDateTime.ofInstant(Instant.ofEpochMilli(endTime), ZONE_ID)
+ .withMinute(0).withSecond(0).withNano(0);
+ while (!current.isAfter(end)) {
+ String day = DAY_FORMAT.format(current);
+ String hour = HOUR_FORMAT.format(current);
+ String fileName = deviceType + "_" + deviceNo + "_" + day + "_" + hour + ".log";
+ paths.add(Paths.get(loggingPath, day, fileName));
+ current = current.plusHours(1);
+ }
+ return paths;
+ }
+
+ private String formatDateTime(long timestamp) {
+ return TIME_FORMAT.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZONE_ID));
+ }
+
+ private String buildDeviceLabel(DeviceConfig config) {
+ return String.format(Locale.ROOT, "%s-%s (%s)", safeText(config.getDeviceType(), "-"),
+ config.getDeviceNo() == null ? "-" : String.valueOf(config.getDeviceNo()),
+ safeText(config.getIp(), "-"));
+ }
+
+ private DevicePingSample findLatestSample(DeviceConfig config) {
+ if (config == null || config.getDeviceNo() == null || safeText(config.getDeviceType(), "").isEmpty()) {
+ return null;
+ }
+ List<String> days = listDays();
+ for (String day : days) {
+ Path dayDir = Paths.get(loggingPath, day);
+ if (!Files.exists(dayDir)) {
+ continue;
+ }
+ String prefix = config.getDeviceType() + "_" + config.getDeviceNo() + "_" + day + "_";
+ List<Path> candidates = new ArrayList<>();
+ try (Stream<Path> stream = Files.list(dayDir)) {
+ stream.filter(path -> !Files.isDirectory(path))
+ .filter(path -> {
+ String name = path.getFileName().toString();
+ return name.startsWith(prefix) && name.endsWith(".log");
+ })
+ .forEach(candidates::add);
+ } catch (Exception ignored) {
+ }
+ candidates.sort((a, b) -> b.getFileName().toString().compareTo(a.getFileName().toString()));
+ for (Path candidate : candidates) {
+ DevicePingSample sample = readLastSample(candidate);
+ if (sample != null) {
+ return sample;
+ }
+ }
+ }
+ return null;
+ }
+
+ private DevicePingSample readLastSample(Path file) {
+ try {
+ List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
+ for (int i = lines.size() - 1; i >= 0; i--) {
+ String line = lines.get(i);
+ if (line == null || line.trim().isEmpty()) {
+ continue;
+ }
+ try {
+ DevicePingSample sample = JSON.parseObject(line, DevicePingSample.class);
+ if (sample != null && sample.getCreateTime() != null) {
+ return sample;
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ return null;
+ }
+
+ private Map<String, Object> buildOverviewSummary(List<Map<String, Object>> devices) {
+ Map<String, Object> summary = new LinkedHashMap<>();
+ long total = devices == null ? 0L : devices.size();
+ long okCount = 0L;
+ long unstableCount = 0L;
+ long offlineCount = 0L;
+ long noDataCount = 0L;
+ long latencyCount = 0L;
+ long latencySum = 0L;
+ Long maxLatency = null;
+ if (devices != null) {
+ for (Map<String, Object> item : devices) {
+ String status = String.valueOf(item.get("status"));
+ if ("OK".equals(status)) {
+ okCount++;
+ } else if ("UNSTABLE".equals(status)) {
+ unstableCount++;
+ } else if ("NO_DATA".equals(status)) {
+ noDataCount++;
+ } else {
+ offlineCount++;
+ }
+ Object avgLatency = item.get("avgLatencyMs");
+ if (avgLatency instanceof Number) {
+ latencySum += ((Number) avgLatency).longValue();
+ latencyCount++;
+ }
+ Object peakLatency = item.get("maxLatencyMs");
+ if (peakLatency instanceof Number) {
+ long candidate = ((Number) peakLatency).longValue();
+ if (maxLatency == null || candidate > maxLatency) {
+ maxLatency = candidate;
+ }
+ }
+ }
+ }
+ summary.put("totalDevices", total);
+ summary.put("okDevices", okCount);
+ summary.put("unstableDevices", unstableCount);
+ summary.put("offlineDevices", offlineCount);
+ summary.put("noDataDevices", noDataCount);
+ summary.put("avgLatencyMs", latencyCount <= 0 ? null : Math.round(latencySum * 100D / latencyCount) / 100D);
+ summary.put("maxLatencyMs", maxLatency);
+ return summary;
+ }
+
+ private String safeText(String value, String defaultValue) {
+ if (value == null || value.trim().isEmpty()) {
+ return defaultValue;
+ }
+ return value.trim();
+ }
+
+ private Double round2(double value) {
+ return Math.round(value * 100D) / 100D;
+ }
+
+ private Integer resolvePacketSize(Integer samplePacketSize) {
+ if (samplePacketSize != null) {
+ return Math.max(-1, samplePacketSize);
+ }
+ return packetSize == null ? -1 : Math.max(-1, packetSize);
+ }
+
+ private double summaryRate(Integer successCount, Integer probeCount) {
+ int total = safeInt(probeCount);
+ if (total <= 0) {
+ return 0D;
+ }
+ return safeInt(successCount) * 100D / total;
+ }
+
+ private int safeInt(Integer value) {
+ return value == null ? 0 : value;
+ }
+
+ private int toInt(Object value) {
+ if (value == null) {
+ return 0;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ try {
+ return Integer.parseInt(String.valueOf(value));
+ } catch (Exception ignored) {
+ return 0;
+ }
+ }
+
+ private String resolveStatusText(String status) {
+ if ("OK".equalsIgnoreCase(status)) {
+ return "姝e父";
+ }
+ if ("UNSTABLE".equalsIgnoreCase(status)) {
+ return "娉㈠姩";
+ }
+ if ("TIMEOUT".equalsIgnoreCase(status)) {
+ return "瓒呮椂";
+ }
+ if ("ERROR".equalsIgnoreCase(status)) {
+ return "寮傚父";
+ }
+ if ("NO_DATA".equalsIgnoreCase(status)) {
+ return "鏆傛棤鏁版嵁";
+ }
+ return safeText(status, "鏈煡");
+ }
+
+ private int resolveStatusLevel(String status) {
+ if ("TIMEOUT".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) {
+ return 0;
+ }
+ if ("UNSTABLE".equalsIgnoreCase(status)) {
+ return 1;
+ }
+ if ("OK".equalsIgnoreCase(status)) {
+ return 2;
+ }
+ return 3;
+ }
+
+ private static class TrendAccumulator {
+
+ private long totalCount;
+ private long successCount;
+ private long failCount;
+ private Long latestAvgLatency;
+ private Long latestMinLatency;
+ private Long latestMaxLatency;
+ private Integer latestPacketSize;
+ private String latestStatus;
+ private long latestTime;
+
+ private void add(DevicePingSample sample) {
+ totalCount++;
+ if (Boolean.TRUE.equals(sample.getReachable())) {
+ successCount++;
+ } else {
+ failCount++;
+ }
+ if (sample.getCreateTime() != null) {
+ long currentTime = sample.getCreateTime().getTime();
+ if (currentTime >= latestTime) {
+ latestTime = currentTime;
+ latestAvgLatency = sample.getAvgLatencyMs();
+ latestMinLatency = sample.getMinLatencyMs();
+ latestMaxLatency = sample.getMaxLatencyMs();
+ latestPacketSize = sample.getPacketSize();
+ latestStatus = sample.getStatus();
+ }
+ }
+ }
+
+ private Double successRate() {
+ if (totalCount <= 0) {
+ return 0D;
+ }
+ return (successCount * 100D) / totalCount;
+ }
+ }
+}
diff --git a/src/main/java/com/zy/core/task/DevicePingScheduler.java b/src/main/java/com/zy/core/task/DevicePingScheduler.java
new file mode 100644
index 0000000..af8fbf7
--- /dev/null
+++ b/src/main/java/com/zy/core/task/DevicePingScheduler.java
@@ -0,0 +1,414 @@
+package com.zy.core.task;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.common.Cools;
+import com.zy.asrs.domain.DevicePingSample;
+import com.zy.asrs.entity.DeviceConfig;
+import com.zy.asrs.service.DeviceConfigService;
+import com.zy.asrs.service.DevicePingFileStorageService;
+import jakarta.annotation.PreDestroy;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Component
+public class DevicePingScheduler {
+
+ private static final Pattern LATENCY_PATTERN = Pattern.compile("(?:time|鏃堕棿)\\s*[=<]?\\s*([0-9]+(?:[\\.,][0-9]+)?)\\s*(?:ms|姣)", Pattern.CASE_INSENSITIVE);
+ private static final int OUTPUT_MESSAGE_LIMIT = 120;
+
+ @Value("${devicePingStorage.enabled:true}")
+ private boolean enabled;
+
+ @Value("${devicePingStorage.timeoutMs:800}")
+ private int timeoutMs;
+
+ @Value("${devicePingStorage.probeCount:3}")
+ private int probeCount;
+
+ @Value("${devicePingStorage.maxParallel:8}")
+ private int maxParallel;
+
+ @Value("${devicePingStorage.packetSize:-1}")
+ private int packetSize;
+
+ @Autowired
+ private DeviceConfigService deviceConfigService;
+
+ @Autowired
+ private DevicePingFileStorageService devicePingFileStorageService;
+
+ private final AtomicBoolean running = new AtomicBoolean(false);
+ private volatile ExecutorService executorService;
+ private volatile ExecutorService probeExecutorService;
+ private volatile long lastCleanupAt = 0L;
+
+ @Scheduled(fixedDelayString = "${devicePingStorage.intervalMs:1000}")
+ public void schedule() {
+ if (!enabled) {
+ return;
+ }
+ if (!running.compareAndSet(false, true)) {
+ return;
+ }
+ ensureExecutor().submit(() -> {
+ try {
+ collectOnce();
+ } finally {
+ running.set(false);
+ }
+ });
+ }
+
+ private void collectOnce() {
+ List<DeviceConfig> configs = loadConfigs();
+ if (configs.isEmpty()) {
+ maybeCleanup();
+ return;
+ }
+
+ List<Future<DevicePingSample>> futures = new ArrayList<>();
+ for (DeviceConfig config : configs) {
+ futures.add(ensureProbeExecutor().submit(() -> probe(config)));
+ }
+
+ List<DevicePingSample> samples = new ArrayList<>();
+ for (Future<DevicePingSample> future : futures) {
+ try {
+ DevicePingSample sample = future.get();
+ if (sample != null) {
+ samples.add(sample);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ devicePingFileStorageService.appendSamples(samples);
+ maybeCleanup();
+ }
+
+ private List<DeviceConfig> loadConfigs() {
+ try {
+ QueryWrapper<DeviceConfig> wrapper = new QueryWrapper<DeviceConfig>()
+ .isNotNull("ip")
+ .ne("ip", "")
+ .orderBy(true, true, "device_type", "device_no");
+ return deviceConfigService.list(wrapper);
+ } catch (Exception ignored) {
+ return new ArrayList<>();
+ }
+ }
+
+ private DevicePingSample probe(DeviceConfig config) {
+ DevicePingSample sample = new DevicePingSample();
+ sample.setCreateTime(new Date());
+ sample.setDeviceType(config.getDeviceType());
+ sample.setDeviceNo(config.getDeviceNo());
+ sample.setIp(config.getIp());
+ sample.setPort(config.getPort());
+ sample.setPacketSize(packetSize);
+ int actualProbeCount = Math.max(1, probeCount);
+ sample.setProbeCount(actualProbeCount);
+
+ List<Long> successLatencies = new ArrayList<>();
+ String lastError = "";
+ int successCount = 0;
+ int perProbeTimeoutMs = Math.max(100, timeoutMs / actualProbeCount);
+
+ for (int i = 0; i < actualProbeCount; i++) {
+ try {
+ PingResult pingResult = systemPing(config.getIp(), perProbeTimeoutMs);
+ if (pingResult.success) {
+ successLatencies.add(pingResult.latencyMs);
+ successCount++;
+ } else {
+ lastError = "绗�" + (i + 1) + "娆℃帰娴嬪け璐�";
+ if (!Cools.isEmpty(pingResult.message)) {
+ lastError = lastError + "锛�" + pingResult.message;
+ }
+ }
+ } catch (Exception ex) {
+ lastError = Cools.isEmpty(ex.getMessage()) ? ex.getClass().getSimpleName() : ex.getMessage();
+ }
+ }
+
+ sample.setSuccessProbeCount(successCount);
+ sample.setReachable(successCount > 0);
+ if (!successLatencies.isEmpty()) {
+ long minLatency = Long.MAX_VALUE;
+ long maxLatency = Long.MIN_VALUE;
+ long latencySum = 0L;
+ for (Long latency : successLatencies) {
+ if (latency == null) {
+ continue;
+ }
+ latencySum += latency;
+ if (latency < minLatency) {
+ minLatency = latency;
+ }
+ if (latency > maxLatency) {
+ maxLatency = latency;
+ }
+ }
+ long avgLatency = Math.round(latencySum * 1.0D / successLatencies.size());
+ sample.setLatencyMs(avgLatency);
+ sample.setAvgLatencyMs(avgLatency);
+ sample.setMinLatencyMs(minLatency);
+ sample.setMaxLatencyMs(maxLatency);
+ } else {
+ sample.setLatencyMs(null);
+ sample.setAvgLatencyMs(null);
+ sample.setMinLatencyMs(null);
+ sample.setMaxLatencyMs(null);
+ }
+
+ if (successCount == actualProbeCount) {
+ sample.setStatus("OK");
+ sample.setMessage(actualProbeCount + "/" + actualProbeCount + " 娆℃帰娴嬫垚鍔�");
+ } else if (successCount > 0) {
+ sample.setStatus("UNSTABLE");
+ sample.setMessage(successCount + "/" + actualProbeCount + " 娆℃帰娴嬫垚鍔�" + (Cools.isEmpty(lastError) ? "" : "锛�" + lastError));
+ } else {
+ sample.setStatus("TIMEOUT");
+ sample.setMessage(Cools.isEmpty(lastError) ? "鍏ㄩ儴鎺㈡祴鍧囪秴鏃�" : lastError);
+ }
+ return sample;
+ }
+
+ private PingResult systemPing(String ip, int timeoutMs) throws Exception {
+ List<String> command = buildPingCommand(ip, timeoutMs);
+ ProcessBuilder processBuilder = new ProcessBuilder(command);
+ processBuilder.redirectErrorStream(true);
+ if (!isWindows()) {
+ processBuilder.environment().put("LC_ALL", "C");
+ processBuilder.environment().put("LANG", "C");
+ }
+
+ long start = System.nanoTime();
+ Process process = processBuilder.start();
+ String output;
+ try (InputStream inputStream = process.getInputStream()) {
+ boolean finished = process.waitFor(Math.max(1L, timeoutMs + 500L), TimeUnit.MILLISECONDS);
+ if (!finished) {
+ process.destroyForcibly();
+ return new PingResult(false, null, "瓒呮椂");
+ }
+ output = readFully(inputStream);
+ }
+
+ int exitCode = process.exitValue();
+ Long latencyMs = parseLatencyMs(output);
+ if (exitCode == 0) {
+ if (latencyMs == null) {
+ latencyMs = Math.max(0L, (System.nanoTime() - start) / 1_000_000L);
+ }
+ return new PingResult(true, latencyMs, "鎴愬姛");
+ }
+ return new PingResult(false, null, extractFailureMessage(output, exitCode));
+ }
+
+ private List<String> buildPingCommand(String ip, int timeoutMs) {
+ int actualPacketSize = Math.max(-1, packetSize);
+ if (isWindows()) {
+ List<String> command = new ArrayList<>(Arrays.asList("ping", "-n", "1", "-w", String.valueOf(Math.max(1, timeoutMs))));
+ if (actualPacketSize >= 0) {
+ command.add("-l");
+ command.add(String.valueOf(actualPacketSize));
+ }
+ command.add(ip);
+ return command;
+ }
+ if (isMac()) {
+ List<String> command = new ArrayList<>(Arrays.asList("ping", "-n", "-c", "1", "-W", String.valueOf(Math.max(1, timeoutMs))));
+ if (actualPacketSize >= 0) {
+ command.add("-s");
+ command.add(String.valueOf(actualPacketSize));
+ }
+ command.add(ip);
+ return command;
+ }
+ int timeoutSec = Math.max(1, (int) Math.ceil(timeoutMs / 1000.0D));
+ List<String> command = new ArrayList<>(Arrays.asList("ping", "-n", "-c", "1", "-W", String.valueOf(timeoutSec)));
+ if (actualPacketSize >= 0) {
+ command.add("-s");
+ command.add(String.valueOf(actualPacketSize));
+ }
+ command.add(ip);
+ return command;
+ }
+
+ private boolean isWindows() {
+ return System.getProperty("os.name", "").toLowerCase().startsWith("windows");
+ }
+
+ private boolean isMac() {
+ String osName = System.getProperty("os.name", "").toLowerCase();
+ return osName.startsWith("mac") || osName.startsWith("darwin");
+ }
+
+ private Long parseLatencyMs(String output) {
+ if (Cools.isEmpty(output)) {
+ return null;
+ }
+ Matcher matcher = LATENCY_PATTERN.matcher(output.replace(',', '.'));
+ if (matcher.find()) {
+ try {
+ return Math.round(Double.parseDouble(matcher.group(1)));
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ private String extractFailureMessage(String output, int exitCode) {
+ if (Cools.isEmpty(output)) {
+ return "exit=" + exitCode;
+ }
+ String[] lines = output.split("\\R");
+ for (String line : lines) {
+ String value = line == null ? "" : line.trim();
+ if (value.isEmpty()) {
+ continue;
+ }
+ String lower = value.toLowerCase();
+ if (lower.contains("request timeout")
+ || lower.contains("100.0% packet loss")
+ || lower.contains("100% packet loss")
+ || lower.contains("destination host unreachable")
+ || lower.contains("could not find host")
+ || lower.contains("name or service not known")
+ || lower.contains("temporary failure in name resolution")
+ || value.contains("璇锋眰瓒呮椂")
+ || value.contains("鎵句笉鍒颁富鏈�")
+ || value.contains("鏃犳硶璁块棶鐩爣涓绘満")
+ || value.contains("100.0% 涓㈠け")) {
+ return trimMessage(value);
+ }
+ }
+ for (String line : lines) {
+ String value = line == null ? "" : line.trim();
+ if (!value.isEmpty()) {
+ return trimMessage(value);
+ }
+ }
+ return "exit=" + exitCode;
+ }
+
+ private String trimMessage(String message) {
+ if (Cools.isEmpty(message)) {
+ return "";
+ }
+ String value = message.trim();
+ if (value.length() > OUTPUT_MESSAGE_LIMIT) {
+ return value.substring(0, OUTPUT_MESSAGE_LIMIT);
+ }
+ return value;
+ }
+
+ private String readFully(InputStream inputStream) throws Exception {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, length);
+ }
+ return outputStream.toString(Charset.defaultCharset());
+ }
+
+ private ExecutorService ensureExecutor() {
+ ExecutorService current = executorService;
+ if (current != null && !current.isShutdown()) {
+ return current;
+ }
+ synchronized (this) {
+ current = executorService;
+ if (current == null || current.isShutdown()) {
+ executorService = Executors.newSingleThreadExecutor(new NamedThreadFactory("device-ping-schedule-"));
+ }
+ return executorService;
+ }
+ }
+
+ private ExecutorService ensureProbeExecutor() {
+ ExecutorService current = probeExecutorService;
+ if (current != null && !current.isShutdown()) {
+ return current;
+ }
+ synchronized (this) {
+ current = probeExecutorService;
+ if (current == null || current.isShutdown()) {
+ probeExecutorService = Executors.newFixedThreadPool(Math.max(1, maxParallel), new NamedThreadFactory("device-ping-probe-"));
+ }
+ return probeExecutorService;
+ }
+ }
+
+ private void maybeCleanup() {
+ long now = System.currentTimeMillis();
+ if (now - lastCleanupAt < 60L * 60L * 1000L) {
+ return;
+ }
+ lastCleanupAt = now;
+ devicePingFileStorageService.cleanupExpired();
+ }
+
+ @PreDestroy
+ public void shutdown() {
+ ExecutorService current = executorService;
+ if (current != null) {
+ current.shutdownNow();
+ }
+ ExecutorService probeCurrent = probeExecutorService;
+ if (probeCurrent != null) {
+ probeCurrent.shutdownNow();
+ }
+ }
+
+ private static class NamedThreadFactory implements ThreadFactory {
+
+ private final String prefix;
+ private final AtomicInteger index = new AtomicInteger(1);
+
+ private NamedThreadFactory(String prefix) {
+ this.prefix = prefix;
+ }
+
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable, prefix + index.getAndIncrement());
+ thread.setDaemon(true);
+ return thread;
+ }
+ }
+
+ private static class PingResult {
+
+ private final boolean success;
+ private final Long latencyMs;
+ private final String message;
+
+ private PingResult(boolean success, Long latencyMs, String message) {
+ this.success = success;
+ this.latencyMs = latencyMs;
+ this.message = message;
+ }
+ }
+}
diff --git a/src/main/java/com/zy/system/controller/DashboardController.java b/src/main/java/com/zy/system/controller/DashboardController.java
index fa97e4d..de5cdc9 100644
--- a/src/main/java/com/zy/system/controller/DashboardController.java
+++ b/src/main/java/com/zy/system/controller/DashboardController.java
@@ -25,6 +25,7 @@
import com.zy.core.thread.StationThread;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -59,12 +60,24 @@
private LlmCallLogService llmCallLogService;
@Autowired
private AiChatSessionMapper aiChatSessionMapper;
+ @Autowired
+ private DevicePingFileStorageService devicePingFileStorageService;
+
+ @Value("${devicePingStorage.intervalMs:1000}")
+ private int devicePingIntervalMs;
+ @Value("${devicePingStorage.timeoutMs:800}")
+ private int devicePingTimeoutMs;
+ @Value("${devicePingStorage.probeCount:3}")
+ private int devicePingProbeCount;
+ @Value("${devicePingStorage.packetSize:-1}")
+ private int devicePingPacketSize;
@GetMapping("/summary/auth")
@ManagerAuth(memo = "绯荤粺浠〃鐩樼粺璁�")
public R summary() {
Map<String, Object> tasks = buildTaskStats();
Map<String, Object> devices = buildDeviceStats();
+ Map<String, Object> network = buildNetworkStats();
Map<String, Object> ai = buildAiStats();
Map<String, Object> overview = new LinkedHashMap<>();
@@ -82,6 +95,7 @@
result.put("overview", overview);
result.put("tasks", tasks);
result.put("devices", devices);
+ result.put("network", network);
result.put("ai", ai);
return R.ok(result);
}
@@ -424,6 +438,74 @@
return result;
}
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> buildNetworkStats() {
+ Map<String, Object> result = new LinkedHashMap<>();
+ Map<String, Object> overview = new LinkedHashMap<>();
+ overview.put("totalDevices", 0L);
+ overview.put("okDevices", 0L);
+ overview.put("unstableDevices", 0L);
+ overview.put("offlineDevices", 0L);
+ overview.put("noDataDevices", 0L);
+ overview.put("attentionDevices", 0L);
+ overview.put("avgLatencyMs", null);
+ overview.put("maxLatencyMs", null);
+
+ Map<String, Object> samplingConfig = new LinkedHashMap<>();
+ samplingConfig.put("intervalMs", devicePingIntervalMs);
+ samplingConfig.put("timeoutMs", devicePingTimeoutMs);
+ samplingConfig.put("probeCount", devicePingProbeCount);
+ samplingConfig.put("packetSize", Math.max(devicePingPacketSize, -1));
+
+ List<Map<String, Object>> statusStats = new ArrayList<>();
+ statusStats.add(metric("姝e父", 0L));
+ statusStats.add(metric("娉㈠姩", 0L));
+ statusStats.add(metric("瓒呮椂/寮傚父", 0L));
+ statusStats.add(metric("鏆傛棤鏁版嵁", 0L));
+
+ List<Map<String, Object>> focusDevices = new ArrayList<>();
+
+ try {
+ Map<String, Object> overviewResult = devicePingFileStorageService.queryOverview(listPingConfigs());
+ Map<String, Object> summary = overviewResult.get("summary") instanceof Map
+ ? (Map<String, Object>) overviewResult.get("summary")
+ : Collections.emptyMap();
+ List<Map<String, Object>> devices = overviewResult.get("devices") instanceof List
+ ? (List<Map<String, Object>>) overviewResult.get("devices")
+ : Collections.emptyList();
+
+ long okDevices = toLong(summary.get("okDevices"));
+ long unstableDevices = toLong(summary.get("unstableDevices"));
+ long offlineDevices = toLong(summary.get("offlineDevices"));
+ long noDataDevices = toLong(summary.get("noDataDevices"));
+
+ overview.put("totalDevices", toLong(summary.get("totalDevices")));
+ overview.put("okDevices", okDevices);
+ overview.put("unstableDevices", unstableDevices);
+ overview.put("offlineDevices", offlineDevices);
+ overview.put("noDataDevices", noDataDevices);
+ overview.put("attentionDevices", unstableDevices + offlineDevices + noDataDevices);
+ overview.put("avgLatencyMs", summary.get("avgLatencyMs"));
+ overview.put("maxLatencyMs", summary.get("maxLatencyMs"));
+
+ statusStats = new ArrayList<>();
+ statusStats.add(metric("姝e父", okDevices));
+ statusStats.add(metric("娉㈠姩", unstableDevices));
+ statusStats.add(metric("瓒呮椂/寮傚父", offlineDevices));
+ statusStats.add(metric("鏆傛棤鏁版嵁", noDataDevices));
+
+ focusDevices = buildNetworkFocusDevices(devices);
+ } catch (Exception e) {
+ log.warn("dashboard network stats load failed: {}", safeMessage(e));
+ }
+
+ result.put("overview", overview);
+ result.put("samplingConfig", samplingConfig);
+ result.put("statusStats", statusStats);
+ result.put("focusDevices", focusDevices);
+ return result;
+ }
+
private List<DeviceConfig> listDeviceConfig(SlaveType type) {
try {
return deviceConfigService.list(new QueryWrapper<DeviceConfig>()
@@ -434,6 +516,93 @@
}
}
+ private List<DeviceConfig> listPingConfigs() {
+ try {
+ return deviceConfigService.list(new QueryWrapper<DeviceConfig>()
+ .isNotNull("ip")
+ .ne("ip", "")
+ .orderBy(true, true, "device_type", "device_no"));
+ } catch (Exception e) {
+ log.warn("dashboard ping device config load failed: {}", safeMessage(e));
+ return Collections.emptyList();
+ }
+ }
+
+ private List<Map<String, Object>> buildNetworkFocusDevices(List<Map<String, Object>> devices) {
+ if (devices == null || devices.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<Map<String, Object>> candidates = new ArrayList<>();
+ for (Map<String, Object> row : devices) {
+ if (resolveNetworkFocusRank(row) < 3) {
+ candidates.add(row);
+ }
+ }
+ if (candidates.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ candidates.sort((left, right) -> {
+ int rankDiff = Integer.compare(resolveNetworkFocusRank(left), resolveNetworkFocusRank(right));
+ if (rankDiff != 0) {
+ return rankDiff;
+ }
+ int latencyDiff = Long.compare(toLong(right.get("avgLatencyMs")), toLong(left.get("avgLatencyMs")));
+ if (latencyDiff != 0) {
+ return latencyDiff;
+ }
+ return Long.compare(toLong(right.get("latestTime")), toLong(left.get("latestTime")));
+ });
+
+ List<Map<String, Object>> result = new ArrayList<>();
+ for (Map<String, Object> item : candidates) {
+ if (item == null) {
+ continue;
+ }
+ Map<String, Object> focus = new LinkedHashMap<>();
+ focus.put("name", toText(item.get("deviceType")) + "-" + toText(item.get("deviceNo")));
+ focus.put("ip", toText(item.get("ip")));
+ focus.put("statusText", defaultText(toText(item.get("statusText")), "鏈煡"));
+ focus.put("statusType", resolveNetworkStatusTagType(toText(item.get("status"))));
+ focus.put("avgLatencyMs", item.get("avgLatencyMs"));
+ focus.put("latestTimeLabel", defaultText(toText(item.get("latestTimeLabel")), "--"));
+ focus.put("message", defaultText(toText(item.get("message")), "鏆傛棤棰濆璇存槑"));
+ result.add(focus);
+ if (result.size() >= 4) {
+ break;
+ }
+ }
+ return result;
+ }
+
+ private int resolveNetworkFocusRank(Map<String, Object> row) {
+ String status = row == null ? "" : toText(row.get("status"));
+ if ("TIMEOUT".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) {
+ return 0;
+ }
+ if ("UNSTABLE".equalsIgnoreCase(status)) {
+ return 1;
+ }
+ if ("NO_DATA".equalsIgnoreCase(status)) {
+ return 2;
+ }
+ return 3;
+ }
+
+ private String resolveNetworkStatusTagType(String status) {
+ if ("TIMEOUT".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) {
+ return "danger";
+ }
+ if ("UNSTABLE".equalsIgnoreCase(status)) {
+ return "warning";
+ }
+ if ("OK".equalsIgnoreCase(status)) {
+ return "success";
+ }
+ return "info";
+ }
+
private boolean isInboundTask(Long wrkSts) {
return wrkSts != null && wrkSts > 0 && wrkSts < 100;
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 99d3d5e..e4008be 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -114,6 +114,23 @@
# 鏃ュ織杩囨湡鏃堕棿 鍗曚綅澶�
expireDays: 7
+devicePingStorage:
+ enabled: true
+ # 绉掔骇璁惧缃戠粶鎺㈡祴鏃ュ織瀛樺偍鍦板潃
+ loggingPath: ./stock/out/@pom.build.finalName@/devicePingLogs
+ # 鏃ュ織杩囨湡鏃堕棿 鍗曚綅澶�
+ expireDays: 7
+ # 閲囨牱鍛ㄦ湡锛堟绉掞級
+ intervalMs: 1000
+ # 鍗曟鎺㈡祴瓒呮椂锛堟绉掞級
+ timeoutMs: 800
+ # 姣忎釜鏍锋湰鍐呰繛缁帰娴嬫鏁帮紝鐢ㄤ簬鐩存帴寰楀埌 min/avg/max 涓変釜鎸囨爣
+ probeCount: 3
+ # ping 鏁版嵁鍖呭ぇ灏忥紙瀛楄妭锛夛紝< 0 鏃舵部鐢ㄧ郴缁熼粯璁わ紱Windows 瀵瑰簲 -l锛宮acOS/Linux 瀵瑰簲 -s
+ packetSize: 1024
+ # 骞惰鎺㈡祴绾跨▼鏁�
+ maxParallel: 8
+
llm:
# 鐜板凡杩佺Щ鍒版暟鎹簱琛� sys_llm_route 缁存姢锛堟敮鎸佸API/澶氭ā鍨�/澶欿ey鑷姩鍒囨崲锛�
# 浠ヤ笅浠呬綔涓烘暟鎹簱涓虹┖鏃剁殑鍏煎鍥為��閰嶇疆
diff --git a/src/main/resources/sql/20260316_add_device_ping_log_menu.sql b/src/main/resources/sql/20260316_add_device_ping_log_menu.sql
new file mode 100644
index 0000000..254ec01
--- /dev/null
+++ b/src/main/resources/sql/20260316_add_device_ping_log_menu.sql
@@ -0,0 +1,71 @@
+-- 灏� 璁惧缃戠粶鍒嗘瀽 鑿滃崟鎸傝浇鍒帮細鏃ュ織鎶ヨ〃锛堜紭鍏堬級鎴栧紑鍙戜笓鐢�
+-- 璇存槑锛氭墽琛屾湰鑴氭湰鍚庯紝璇峰湪鈥滆鑹叉巿鏉冣�濋噷缁欏搴旇鑹插嬀閫夋柊鑿滃崟鍜屸�滄煡鐪嬧�濇潈闄愩��
+
+SET @device_ping_parent_id := COALESCE(
+ (
+ SELECT id
+ FROM sys_resource
+ WHERE code = 'logReport' AND level = 1
+ ORDER BY id
+ LIMIT 1
+ ),
+ (
+ SELECT id
+ FROM sys_resource
+ WHERE code = 'develop' AND level = 1
+ ORDER BY id
+ LIMIT 1
+ )
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'devicePingLog/devicePingLog.html', '璁惧缃戠粶鍒嗘瀽', @device_ping_parent_id, 2, 997, 1
+FROM dual
+WHERE @device_ping_parent_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_resource
+ WHERE code = 'devicePingLog/devicePingLog.html' AND level = 2
+ );
+
+UPDATE sys_resource
+SET name = '璁惧缃戠粶鍒嗘瀽',
+ resource_id = @device_ping_parent_id,
+ level = 2,
+ sort = 997,
+ status = 1
+WHERE code = 'devicePingLog/devicePingLog.html' AND level = 2;
+
+SET @device_ping_id := (
+ SELECT id
+ FROM sys_resource
+ WHERE code = 'devicePingLog/devicePingLog.html' AND level = 2
+ ORDER BY id
+ LIMIT 1
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'devicePingLog/devicePingLog.html#view', '鏌ョ湅', @device_ping_id, 3, 1, 1
+FROM dual
+WHERE @device_ping_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_resource
+ WHERE code = 'devicePingLog/devicePingLog.html#view' AND level = 3
+ );
+
+UPDATE sys_resource
+SET name = '鏌ョ湅',
+ resource_id = @device_ping_id,
+ level = 3,
+ sort = 1,
+ status = 1
+WHERE code = 'devicePingLog/devicePingLog.html#view' AND level = 3;
+
+SELECT id, code, name, resource_id, level, sort, status
+FROM sys_resource
+WHERE code IN (
+ 'devicePingLog/devicePingLog.html',
+ 'devicePingLog/devicePingLog.html#view'
+)
+ORDER BY level, sort, id;
diff --git a/src/main/webapp/static/js/dashboard/dashboard.js b/src/main/webapp/static/js/dashboard/dashboard.js
index dc8cf57..db6676a 100644
--- a/src/main/webapp/static/js/dashboard/dashboard.js
+++ b/src/main/webapp/static/js/dashboard/dashboard.js
@@ -20,6 +20,7 @@
taskDirection: null,
taskStage: null,
deviceType: null,
+ networkStatus: null,
aiRoute: null
},
overview: {
@@ -55,6 +56,26 @@
onlineRate: 0
},
typeStats: []
+ },
+ network: {
+ overview: {
+ totalDevices: 0,
+ okDevices: 0,
+ unstableDevices: 0,
+ offlineDevices: 0,
+ noDataDevices: 0,
+ attentionDevices: 0,
+ avgLatencyMs: null,
+ maxLatencyMs: null
+ },
+ samplingConfig: {
+ intervalMs: 1000,
+ timeoutMs: 800,
+ probeCount: 3,
+ packetSize: -1
+ },
+ statusStats: [],
+ focusDevices: []
},
ai: {
overview: {
@@ -120,6 +141,7 @@
this.overview = payload.overview || this.overview;
this.tasks = payload.tasks || this.tasks;
this.devices = payload.devices || this.devices;
+ this.network = payload.network || this.network;
this.ai = payload.ai || this.ai;
this.updateCharts();
},
@@ -128,16 +150,18 @@
this.charts.taskDirection = echarts.init(this.$refs.taskDirectionChart);
this.charts.taskStage = echarts.init(this.$refs.taskStageChart);
this.charts.deviceType = echarts.init(this.$refs.deviceTypeChart);
+ this.charts.networkStatus = echarts.init(this.$refs.networkStatusChart);
this.charts.aiRoute = echarts.init(this.$refs.aiRouteChart);
},
updateCharts: function () {
- if (!this.charts.taskDirection || !this.charts.taskStage || !this.charts.deviceType || !this.charts.aiRoute) {
+ if (!this.charts.taskDirection || !this.charts.taskStage || !this.charts.deviceType || !this.charts.networkStatus || !this.charts.aiRoute) {
return;
}
this.charts.taskDirection.setOption(this.buildTaskDirectionOption());
this.charts.taskStage.setOption(this.buildTaskStageOption());
this.charts.deviceType.setOption(this.buildDeviceTypeOption());
+ this.charts.networkStatus.setOption(this.buildNetworkStatusOption());
this.charts.aiRoute.setOption(this.buildAiRouteOption());
this.resizeCharts();
},
@@ -323,6 +347,51 @@
}]
};
},
+ buildNetworkStatusOption: function () {
+ var data = cloneMetricList(this.network.statusStats);
+ return {
+ color: ["#2fa38e", "#f59a4a", "#de5c5c", "#c8d4e1"],
+ tooltip: {
+ trigger: "item",
+ formatter: "{b}<br/>{c} ({d}%)"
+ },
+ graphic: [{
+ type: "text",
+ left: "center",
+ top: "38%",
+ style: {
+ text: this.formatNumber(this.network.overview.attentionDevices || 0),
+ fill: "#1f3142",
+ fontSize: 26,
+ fontWeight: 700
+ }
+ }, {
+ type: "text",
+ left: "center",
+ top: "54%",
+ style: {
+ text: "闇�鍏虫敞璁惧",
+ fill: "#7c8fa4",
+ fontSize: 12
+ }
+ }],
+ legend: {
+ bottom: 0,
+ itemWidth: 10,
+ itemHeight: 10,
+ textStyle: { color: "#60778d", fontSize: 12 }
+ },
+ series: [{
+ type: "pie",
+ radius: ["55%", "75%"],
+ center: ["50%", "42%"],
+ avoidLabelOverlap: false,
+ label: { show: false },
+ labelLine: { show: false },
+ data: data
+ }]
+ };
+ },
formatNumber: function (value) {
var num = Number(value || 0);
if (!isFinite(num)) {
@@ -330,8 +399,31 @@
}
return num.toLocaleString("zh-CN");
},
+ formatLatency: function (value) {
+ var num;
+ if (value == null || value === "") {
+ return "--";
+ }
+ num = Number(value);
+ if (!isFinite(num)) {
+ return "--";
+ }
+ return num.toLocaleString("zh-CN", { maximumFractionDigits: 2 }) + " ms";
+ },
+ formatPacketSize: function (value) {
+ var num = Number(value);
+ if (!isFinite(num) || num < 0) {
+ return "绯荤粺榛樿";
+ }
+ return num + " B";
+ },
displayText: function (value, fallback) {
return value == null || value === "" ? (fallback || "") : value;
+ },
+ networkSamplingText: function () {
+ var config = this.network && this.network.samplingConfig ? this.network.samplingConfig : {};
+ return "閲囨牱 " + this.displayText(config.intervalMs, 0) + " ms / 瓒呮椂 " + this.displayText(config.timeoutMs, 0) +
+ " ms / 姣忔牱鏈� " + this.displayText(config.probeCount, 0) + " 娆� / 鍖呭ぇ灏� " + this.formatPacketSize(config.packetSize);
},
startAutoRefresh: function () {
var self = this;
@@ -381,15 +473,86 @@
}
}
},
- openMonitor: function () {
+ resolveParentMenuApp: function () {
+ var parentDocument;
+ var parentRoot;
+ try {
+ if (!window.parent || window.parent === window || !window.parent.document) {
+ return null;
+ }
+ parentDocument = window.parent.document;
+ parentRoot = parentDocument.getElementById("app");
+ return parentRoot && parentRoot.__vue__ ? parentRoot.__vue__ : null;
+ } catch (e) {
+ return null;
+ }
+ },
+ resolveAbsoluteViewPath: function (path) {
+ if (!path) {
+ return "";
+ }
+ if (/^https?:\/\//.test(path) || path.indexOf(baseUrl) === 0) {
+ return path;
+ }
+ if (path.indexOf("/views/") === 0) {
+ return baseUrl + path;
+ }
+ if (path.indexOf("views/") === 0) {
+ return baseUrl + "/" + path;
+ }
+ return baseUrl + "/views/" + path.replace(/^\/+/, "");
+ },
+ findParentMenuByPath: function (targetPath, preferredName, preferredGroup) {
+ var parentApp = this.resolveParentMenuApp();
+ var targetBasePath = this.resolveAbsoluteViewPath(targetPath).split("#")[0].split("?")[0];
+ var fallback = null;
+ var i;
+ var j;
+ var group;
+ var item;
+ var itemBasePath;
+
+ if (!parentApp || !Array.isArray(parentApp.menus)) {
+ return null;
+ }
+
+ for (i = 0; i < parentApp.menus.length; i++) {
+ group = parentApp.menus[i];
+ for (j = 0; j < (group.subMenu || []).length; j++) {
+ item = group.subMenu[j];
+ if (!item || !item.url) {
+ continue;
+ }
+ itemBasePath = item.url.split("#")[0].split("?")[0];
+ if (!fallback && ((preferredName && item.name === preferredName) || itemBasePath === targetBasePath)) {
+ fallback = item;
+ }
+ if ((!preferredGroup || group.menu === preferredGroup) && itemBasePath === targetBasePath) {
+ return item;
+ }
+ }
+ }
+
+ return fallback;
+ },
+ openParentMenuView: function (targetPath, fallbackName, preferredName, preferredGroup) {
+ var targetMenu = this.findParentMenuByPath(targetPath, preferredName || fallbackName, preferredGroup);
+ var menuPath = targetMenu && targetMenu.url ? targetMenu.url : targetPath;
+ var menuName = targetMenu && targetMenu.name ? targetMenu.name : (fallbackName || preferredName || "");
if (window.parent && window.parent.index && typeof window.parent.index.loadView === "function") {
window.parent.index.loadView({
- menuPath: "/views/watch/console.html",
- menuName: "鐩戞帶宸ヤ綔鍙�"
+ menuPath: menuPath,
+ menuName: menuName
});
return;
}
- window.open(baseUrl + "/views/watch/console.html", "_blank");
+ window.open(targetMenu && targetMenu.url ? targetMenu.url : this.resolveAbsoluteViewPath(targetPath), "_blank");
+ },
+ openMonitor: function () {
+ this.openParentMenuView("/views/watch/console.html", "鐩戞帶鐢婚潰", "鐩戞帶鐢婚潰", "鐩戞帶绯荤粺");
+ },
+ openDevicePingAnalysis: function () {
+ this.openParentMenuView("/views/devicePingLog/devicePingLog.html", "璁惧缃戠粶鍒嗘瀽", "璁惧缃戠粶鍒嗘瀽");
}
}
});
diff --git a/src/main/webapp/static/js/devicePingLog/devicePingLog.js b/src/main/webapp/static/js/devicePingLog/devicePingLog.js
new file mode 100644
index 0000000..5e002d9
--- /dev/null
+++ b/src/main/webapp/static/js/devicePingLog/devicePingLog.js
@@ -0,0 +1,466 @@
+(function () {
+ "use strict";
+
+ function nowDate() {
+ return new Date();
+ }
+
+ function minutesAgo(minutes) {
+ return new Date(Date.now() - minutes * 60 * 1000);
+ }
+
+ function createEmptyOverviewSummary() {
+ return {
+ totalDevices: 0,
+ okDevices: 0,
+ unstableDevices: 0,
+ offlineDevices: 0,
+ noDataDevices: 0,
+ avgLatencyMs: null,
+ maxLatencyMs: null
+ };
+ }
+
+ function createEmptyDetailSummary() {
+ return {
+ totalSamples: 0,
+ successSamples: 0,
+ failSamples: 0,
+ successRate: 0,
+ avgLatencyMs: null,
+ minLatencyMs: null,
+ maxLatencyMs: null,
+ packetSize: -1,
+ latestStatus: "",
+ latestTimeLabel: ""
+ };
+ }
+
+ function createEmptySamplingConfig() {
+ return {
+ intervalMs: 1000,
+ timeoutMs: 800,
+ probeCount: 3,
+ packetSize: -1
+ };
+ }
+
+ new Vue({
+ el: "#app",
+ data: function () {
+ return {
+ overviewLoading: false,
+ detailLoading: false,
+ devices: [],
+ availableDays: [],
+ samplingConfig: createEmptySamplingConfig(),
+ overviewSummary: createEmptyOverviewSummary(),
+ overviewRows: [],
+ overviewFilters: {
+ deviceType: "",
+ keyword: ""
+ },
+ detailFilters: {
+ deviceKey: "",
+ range: [minutesAgo(30), nowDate()]
+ },
+ detailSummary: createEmptyDetailSummary(),
+ series: [],
+ alerts: [],
+ charts: {
+ latency: null,
+ availability: null
+ },
+ resizeHandler: null
+ };
+ },
+ computed: {
+ currentDevice: function () {
+ var key = this.detailFilters.deviceKey;
+ if (!key) {
+ return null;
+ }
+ for (var i = 0; i < this.devices.length; i++) {
+ var item = this.devices[i];
+ if ((item.deviceType + "#" + item.deviceNo) === key) {
+ return item;
+ }
+ }
+ for (var j = 0; j < this.overviewRows.length; j++) {
+ var row = this.overviewRows[j];
+ if ((row.deviceType + "#" + row.deviceNo) === key) {
+ return row;
+ }
+ }
+ return null;
+ },
+ filteredOverviewRows: function () {
+ var type = this.overviewFilters.deviceType;
+ var keyword = (this.overviewFilters.keyword || "").toLowerCase();
+ return this.overviewRows.filter(function (item) {
+ if (type && item.deviceType !== type) {
+ return false;
+ }
+ if (!keyword) {
+ return true;
+ }
+ var deviceText = (item.deviceType + "-" + item.deviceNo).toLowerCase();
+ var ipText = (item.ip || "").toLowerCase();
+ return deviceText.indexOf(keyword) >= 0 || ipText.indexOf(keyword) >= 0;
+ });
+ },
+ samplingConfigText: function () {
+ var config = this.samplingConfig || createEmptySamplingConfig();
+ return "閲囨牱 " + config.intervalMs + " ms / 瓒呮椂 " + config.timeoutMs + " ms / 姣忔牱鏈� " + config.probeCount + " 娆�";
+ }
+ },
+ mounted: function () {
+ var self = this;
+ this.$nextTick(function () {
+ self.loadOptions();
+ self.loadOverview();
+ self.resizeHandler = function () {
+ self.resizeCharts();
+ };
+ window.addEventListener("resize", self.resizeHandler);
+ });
+ },
+ beforeDestroy: function () {
+ if (this.resizeHandler) {
+ window.removeEventListener("resize", this.resizeHandler);
+ }
+ this.disposeCharts();
+ },
+ methods: {
+ loadOptions: function () {
+ var self = this;
+ $.ajax({
+ url: baseUrl + "/devicePingLog/options/auth",
+ headers: { token: localStorage.getItem("token") },
+ method: "GET",
+ success: function (res) {
+ if (res && res.code === 200) {
+ var data = res.data || {};
+ self.devices = data.devices || [];
+ self.availableDays = data.days || [];
+ self.samplingConfig = Object.assign(createEmptySamplingConfig(), data.samplingConfig || {});
+ return;
+ }
+ self.$message.error((res && res.msg) || "璁惧閰嶇疆鍔犺浇澶辫触");
+ },
+ error: function () {
+ self.$message.error("璁惧閰嶇疆鍔犺浇澶辫触");
+ }
+ });
+ },
+ loadOverview: function () {
+ var self = this;
+ this.overviewLoading = true;
+ $.ajax({
+ url: baseUrl + "/devicePingLog/overview/auth",
+ headers: { token: localStorage.getItem("token") },
+ method: "GET",
+ success: function (res) {
+ if (res && res.code === 200) {
+ var data = res.data || {};
+ self.overviewSummary = Object.assign(createEmptyOverviewSummary(), data.summary || {});
+ self.overviewRows = data.devices || [];
+ return;
+ }
+ self.overviewSummary = createEmptyOverviewSummary();
+ self.overviewRows = [];
+ self.$message.error((res && res.msg) || "鎬昏鏁版嵁鍔犺浇澶辫触");
+ },
+ error: function () {
+ self.overviewSummary = createEmptyOverviewSummary();
+ self.overviewRows = [];
+ self.$message.error("鎬昏鏁版嵁鍔犺浇澶辫触");
+ },
+ complete: function () {
+ self.overviewLoading = false;
+ }
+ });
+ },
+ openDetail: function (row) {
+ var self = this;
+ if (!row) {
+ return;
+ }
+ this.detailFilters.deviceKey = row.deviceType + "#" + row.deviceNo;
+ this.setQuickRange(30, false);
+ this.$nextTick(function () {
+ self.ensureCharts();
+ self.queryTrend();
+ });
+ },
+ setQuickRange: function (minutes, autoQuery) {
+ this.detailFilters.range = [minutesAgo(minutes), nowDate()];
+ if (autoQuery !== false && this.currentDevice) {
+ this.queryTrend();
+ }
+ },
+ queryTrend: function () {
+ if (!this.currentDevice) {
+ return;
+ }
+ if (!this.detailFilters.range || this.detailFilters.range.length !== 2) {
+ this.$message.warning("璇烽�夋嫨鏃堕棿鑼冨洿");
+ return;
+ }
+ var parts = this.detailFilters.deviceKey.split("#");
+ if (parts.length !== 2) {
+ this.$message.warning("璁惧淇℃伅鏃犳晥");
+ return;
+ }
+ var self = this;
+ this.detailLoading = true;
+ $.ajax({
+ url: baseUrl + "/devicePingLog/trend/auth",
+ headers: { token: localStorage.getItem("token") },
+ method: "GET",
+ data: {
+ deviceType: parts[0],
+ deviceNo: parts[1],
+ startTime: this.detailFilters.range[0].getTime(),
+ endTime: this.detailFilters.range[1].getTime()
+ },
+ success: function (res) {
+ if (res && res.code === 200) {
+ var data = res.data || {};
+ self.detailSummary = Object.assign(createEmptyDetailSummary(), data.summary || {});
+ self.series = data.series || [];
+ self.alerts = data.alerts || [];
+ self.updateCharts();
+ return;
+ }
+ self.detailSummary = createEmptyDetailSummary();
+ self.series = [];
+ self.alerts = [];
+ self.updateCharts();
+ self.$message.error((res && res.msg) || "璁惧璇︽儏鍔犺浇澶辫触");
+ },
+ error: function () {
+ self.detailSummary = createEmptyDetailSummary();
+ self.series = [];
+ self.alerts = [];
+ self.updateCharts();
+ self.$message.error("璁惧璇︽儏鍔犺浇澶辫触");
+ },
+ complete: function () {
+ self.detailLoading = false;
+ }
+ });
+ },
+ ensureCharts: function () {
+ if (this.$refs.latencyChart && !this.charts.latency) {
+ this.charts.latency = echarts.init(this.$refs.latencyChart);
+ }
+ if (this.$refs.availabilityChart && !this.charts.availability) {
+ this.charts.availability = echarts.init(this.$refs.availabilityChart);
+ }
+ },
+ disposeCharts: function () {
+ if (this.charts.latency) {
+ this.charts.latency.dispose();
+ this.charts.latency = null;
+ }
+ if (this.charts.availability) {
+ this.charts.availability.dispose();
+ this.charts.availability = null;
+ }
+ },
+ resizeCharts: function () {
+ if (this.charts.latency) {
+ this.charts.latency.resize();
+ }
+ if (this.charts.availability) {
+ this.charts.availability.resize();
+ }
+ },
+ updateCharts: function () {
+ this.ensureCharts();
+ if (!this.charts.latency || !this.charts.availability) {
+ return;
+ }
+ if (!this.series.length) {
+ this.charts.latency.clear();
+ this.charts.availability.clear();
+ return;
+ }
+ this.charts.latency.setOption(this.buildLatencyOption(), true);
+ this.charts.availability.setOption(this.buildAvailabilityOption(), true);
+ this.resizeCharts();
+ },
+ buildLatencyOption: function () {
+ var xAxisData = this.series.map(function (item) { return item.timeLabel; });
+ return {
+ color: ["#1f6fb2", "#2fa38e", "#f59a4a"],
+ tooltip: {
+ trigger: "axis"
+ },
+ legend: {
+ top: 0,
+ textStyle: { color: "#5e738a" }
+ },
+ grid: {
+ left: 54,
+ right: 18,
+ top: 40,
+ bottom: 54
+ },
+ xAxis: {
+ type: "category",
+ data: xAxisData,
+ boundaryGap: false,
+ axisLine: { lineStyle: { color: "#d5dfeb" } },
+ axisLabel: {
+ color: "#74879a",
+ formatter: function (value) {
+ return value.slice(11);
+ }
+ }
+ },
+ yAxis: {
+ type: "value",
+ name: "ms",
+ splitLine: { lineStyle: { color: "#edf2f7" } },
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: "#7f92a5" }
+ },
+ dataZoom: [{
+ type: "inside"
+ }, {
+ type: "slider",
+ height: 18,
+ bottom: 10
+ }],
+ series: [{
+ name: "骞冲潎",
+ type: "line",
+ showSymbol: false,
+ sampling: "lttb",
+ data: this.series.map(function (item) { return item.avgLatencyMs; }),
+ lineStyle: { width: 2 }
+ }, {
+ name: "鏈�灏�",
+ type: "line",
+ showSymbol: false,
+ sampling: "lttb",
+ data: this.series.map(function (item) { return item.minLatencyMs; }),
+ lineStyle: { width: 1.5 }
+ }, {
+ name: "鏈�澶�",
+ type: "line",
+ showSymbol: false,
+ sampling: "lttb",
+ data: this.series.map(function (item) { return item.maxLatencyMs; }),
+ lineStyle: { width: 1.5, type: "dashed" }
+ }]
+ };
+ },
+ buildAvailabilityOption: function () {
+ var xAxisData = this.series.map(function (item) { return item.timeLabel; });
+ return {
+ color: ["#de5c5c", "#2fa38e"],
+ tooltip: {
+ trigger: "axis"
+ },
+ legend: {
+ top: 0,
+ textStyle: { color: "#5e738a" }
+ },
+ grid: {
+ left: 48,
+ right: 50,
+ top: 40,
+ bottom: 54
+ },
+ xAxis: {
+ type: "category",
+ data: xAxisData,
+ axisLine: { lineStyle: { color: "#d5dfeb" } },
+ axisLabel: {
+ color: "#74879a",
+ formatter: function (value) {
+ return value.slice(11);
+ }
+ }
+ },
+ yAxis: [{
+ type: "value",
+ name: "澶辫触",
+ splitLine: { lineStyle: { color: "#edf2f7" } },
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: "#7f92a5" }
+ }, {
+ type: "value",
+ name: "鎴愬姛鐜�%",
+ min: 0,
+ max: 100,
+ splitLine: { show: false },
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: "#7f92a5" }
+ }],
+ dataZoom: [{
+ type: "inside"
+ }, {
+ type: "slider",
+ height: 18,
+ bottom: 10
+ }],
+ series: [{
+ name: "澶辫触娆℃暟",
+ type: "bar",
+ yAxisIndex: 0,
+ barMaxWidth: 18,
+ data: this.series.map(function (item) { return item.failProbeCount; }),
+ itemStyle: {
+ borderRadius: [6, 6, 0, 0]
+ }
+ }, {
+ name: "鎴愬姛鐜�",
+ type: "line",
+ yAxisIndex: 1,
+ showSymbol: false,
+ sampling: "lttb",
+ data: this.series.map(function (item) { return item.successRate; }),
+ lineStyle: { width: 2 }
+ }]
+ };
+ },
+ statusClass: function (status) {
+ if (status === "OK") {
+ return "ok";
+ }
+ if (status === "UNSTABLE") {
+ return "unstable";
+ }
+ if (status === "NO_DATA") {
+ return "nodata";
+ }
+ return "offline";
+ },
+ formatLatency: function (value) {
+ if (value === null || value === undefined || value === "") {
+ return "--";
+ }
+ return value + " ms";
+ },
+ formatPercent: function (value) {
+ if (value === null || value === undefined || value === "") {
+ return "--";
+ }
+ return value + "%";
+ },
+ formatPacketSize: function (value) {
+ if (value === null || value === undefined || value === "" || value < 0) {
+ return "绯荤粺榛樿";
+ }
+ return value + " B";
+ }
+ }
+ });
+})();
diff --git a/src/main/webapp/views/dashboard/dashboard.html b/src/main/webapp/views/dashboard/dashboard.html
index 857578d..3c2b87e 100644
--- a/src/main/webapp/views/dashboard/dashboard.html
+++ b/src/main/webapp/views/dashboard/dashboard.html
@@ -271,6 +271,14 @@
font-weight: 700;
}
+ .panel-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
.mini-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -324,6 +332,26 @@
border-color: rgba(151, 110, 204, 0.18);
}
+ .network-mini-ok {
+ background: linear-gradient(180deg, rgba(47, 163, 142, 0.11) 0%, rgba(47, 163, 142, 0.03) 100%);
+ border-color: rgba(47, 163, 142, 0.18);
+ }
+
+ .network-mini-warning {
+ background: linear-gradient(180deg, rgba(245, 154, 74, 0.12) 0%, rgba(245, 154, 74, 0.03) 100%);
+ border-color: rgba(245, 154, 74, 0.18);
+ }
+
+ .network-mini-offline {
+ background: linear-gradient(180deg, rgba(222, 92, 92, 0.10) 0%, rgba(222, 92, 92, 0.03) 100%);
+ border-color: rgba(222, 92, 92, 0.18);
+ }
+
+ .network-mini-latency {
+ background: linear-gradient(180deg, rgba(31, 111, 178, 0.09) 0%, rgba(31, 111, 178, 0.02) 100%);
+ border-color: rgba(31, 111, 178, 0.16);
+ }
+
.chart-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -344,12 +372,20 @@
margin-bottom: 8px;
}
+ .chart-subtitle {
+ margin-bottom: 10px;
+ font-size: 12px;
+ color: #7d90a4;
+ line-height: 1.6;
+ }
+
.chart-box {
width: 100%;
height: 280px;
}
.panel-device .mini-grid,
+ .panel-network .mini-grid,
.panel-ai .mini-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -387,6 +423,7 @@
}
.device-chart-box,
+ .network-chart-box,
.ai-chart-box {
width: 100%;
height: 250px;
@@ -466,8 +503,80 @@
padding: 8px 10px;
}
+ .network-note {
+ margin-top: 6px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 12px;
+ padding: 8px 10px;
+ }
+
+ .network-note-danger {
+ color: #c15b5b;
+ background: rgba(222, 92, 92, 0.08);
+ }
+
+ .network-note-warning {
+ color: #b67632;
+ background: rgba(245, 154, 74, 0.12);
+ }
+
+ .network-note-info {
+ color: #657d95;
+ background: rgba(125, 144, 164, 0.10);
+ }
+
+ .network-healthy-state {
+ margin-top: 14px;
+ padding: 14px 16px;
+ border-radius: 16px;
+ border: 1px solid rgba(87, 186, 128, 0.20);
+ background: linear-gradient(135deg, rgba(87, 186, 128, 0.10) 0%, rgba(87, 186, 128, 0.03) 100%);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+ }
+
+ .network-healthy-main {
+ min-width: 0;
+ flex: 1;
+ }
+
+ .network-healthy-title {
+ font-size: 14px;
+ font-weight: 700;
+ color: #2d7f56;
+ line-height: 1.5;
+ }
+
+ .network-healthy-desc {
+ margin-top: 4px;
+ font-size: 12px;
+ color: #698399;
+ line-height: 1.6;
+ }
+
+ .network-healthy-tags {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .network-healthy-tag {
+ padding: 6px 10px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(87, 186, 128, 0.18);
+ font-size: 12px;
+ color: #557160;
+ white-space: nowrap;
+ }
+
.recent-panel {
- min-height: 100%;
+ min-height: 0;
}
.recent-table {
@@ -553,6 +662,7 @@
}
.panel-device .mini-grid,
+ .panel-network .mini-grid,
.panel-ai .mini-grid {
grid-template-columns: 1fr;
}
@@ -579,6 +689,20 @@
.route-row-side {
align-items: flex-start;
text-align: left;
+ }
+
+ .network-healthy-state {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .network-healthy-tags {
+ justify-content: flex-start;
+ }
+
+ .panel-actions {
+ width: 100%;
+ justify-content: flex-start;
}
}
</style>
@@ -732,60 +856,6 @@
</el-table>
</div>
</section>
- </div>
-
- <div class="dashboard-column">
- <section class="panel panel-device">
- <div class="panel-header">
- <div>
- <div class="panel-kicker">Devices</div>
- <h2 class="panel-title">璁惧鎬佸娍</h2>
- <div class="panel-desc">姹囨�昏緭閫佺珯鐐广�佸爢鍨涙満銆佸弻宸ヤ綅鍫嗗灈鏈轰笌 RGV 鐨勫湪绾裤�佸繖纰屽拰鍛婅鎯呭喌銆�</div>
- </div>
- <el-tag size="small" type="info">鍦ㄧ嚎鐜� {{ devices.overview.onlineRate || 0 }}%</el-tag>
- </div>
-
- <div class="mini-grid">
- <div class="mini-card">
- <div class="mini-label">璁惧鎬绘暟</div>
- <div class="mini-value">{{ formatNumber(devices.overview.total) }}</div>
- <div class="mini-hint">宸插惎鐢ㄩ厤缃澶�</div>
- </div>
- <div class="mini-card">
- <div class="mini-label">鍦ㄧ嚎璁惧</div>
- <div class="mini-value">{{ formatNumber(devices.overview.online) }}</div>
- <div class="mini-hint">瀹炴椂杩為�氳澶囨暟閲�</div>
- </div>
- <div class="mini-card">
- <div class="mini-label">蹇欑璁惧</div>
- <div class="mini-value">{{ formatNumber(devices.overview.busy) }}</div>
- <div class="mini-hint">褰撳墠鎵胯浇浠诲姟鐨勮澶�</div>
- </div>
- <div class="mini-card">
- <div class="mini-label">鍛婅璁惧</div>
- <div class="mini-value">{{ formatNumber(devices.overview.alarm) }}</div>
- <div class="mini-hint">鍚樆濉炴垨鎶ヨ鐘舵��</div>
- </div>
- </div>
-
- <div class="chart-card">
- <div class="chart-title">璁惧鍦ㄧ嚎鍒嗗竷</div>
- <div ref="deviceTypeChart" class="device-chart-box"></div>
- </div>
-
- <div class="type-list">
- <div v-for="item in devices.typeStats" :key="item.name" class="type-row">
- <div class="type-row-main">
- <div class="type-row-name">{{ item.name }}</div>
- <div class="type-row-desc">鍦ㄧ嚎 {{ formatNumber(item.online) }} / 鎬绘暟 {{ formatNumber(item.total) }}锛岀绾� {{ formatNumber(item.offline) }}</div>
- </div>
- <div class="type-row-side">
- <el-tag size="mini" type="success">蹇欑 {{ formatNumber(item.busy) }}</el-tag>
- <el-tag size="mini" :type="item.alarm > 0 ? 'danger' : 'info'">鍛婅 {{ formatNumber(item.alarm) }}</el-tag>
- </div>
- </div>
- </div>
- </section>
<section class="panel panel-ai">
<div class="panel-header">
@@ -842,6 +912,132 @@
<el-empty v-else description="鏆傛棤 AI 璺敱鏁版嵁"></el-empty>
</section>
</div>
+
+ <div class="dashboard-column">
+ <section class="panel panel-device">
+ <div class="panel-header">
+ <div>
+ <div class="panel-kicker">Devices</div>
+ <h2 class="panel-title">璁惧鎬佸娍</h2>
+ <div class="panel-desc">姹囨�昏緭閫佺珯鐐广�佸爢鍨涙満銆佸弻宸ヤ綅鍫嗗灈鏈轰笌 RGV 鐨勫湪绾裤�佸繖纰屽拰鍛婅鎯呭喌銆�</div>
+ </div>
+ <el-tag size="small" type="info">鍦ㄧ嚎鐜� {{ devices.overview.onlineRate || 0 }}%</el-tag>
+ </div>
+
+ <div class="mini-grid">
+ <div class="mini-card">
+ <div class="mini-label">璁惧鎬绘暟</div>
+ <div class="mini-value">{{ formatNumber(devices.overview.total) }}</div>
+ <div class="mini-hint">宸插惎鐢ㄩ厤缃澶�</div>
+ </div>
+ <div class="mini-card">
+ <div class="mini-label">鍦ㄧ嚎璁惧</div>
+ <div class="mini-value">{{ formatNumber(devices.overview.online) }}</div>
+ <div class="mini-hint">瀹炴椂杩為�氳澶囨暟閲�</div>
+ </div>
+ <div class="mini-card">
+ <div class="mini-label">蹇欑璁惧</div>
+ <div class="mini-value">{{ formatNumber(devices.overview.busy) }}</div>
+ <div class="mini-hint">褰撳墠鎵胯浇浠诲姟鐨勮澶�</div>
+ </div>
+ <div class="mini-card">
+ <div class="mini-label">鍛婅璁惧</div>
+ <div class="mini-value">{{ formatNumber(devices.overview.alarm) }}</div>
+ <div class="mini-hint">鍚樆濉炴垨鎶ヨ鐘舵��</div>
+ </div>
+ </div>
+
+ <div class="chart-card">
+ <div class="chart-title">璁惧鍦ㄧ嚎鍒嗗竷</div>
+ <div ref="deviceTypeChart" class="device-chart-box"></div>
+ </div>
+
+ <div class="type-list">
+ <div v-for="item in devices.typeStats" :key="item.name" class="type-row">
+ <div class="type-row-main">
+ <div class="type-row-name">{{ item.name }}</div>
+ <div class="type-row-desc">鍦ㄧ嚎 {{ formatNumber(item.online) }} / 鎬绘暟 {{ formatNumber(item.total) }}锛岀绾� {{ formatNumber(item.offline) }}</div>
+ </div>
+ <div class="type-row-side">
+ <el-tag size="mini" type="success">蹇欑 {{ formatNumber(item.busy) }}</el-tag>
+ <el-tag size="mini" :type="item.alarm > 0 ? 'danger' : 'info'">鍛婅 {{ formatNumber(item.alarm) }}</el-tag>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="panel panel-network">
+ <div class="panel-header">
+ <div>
+ <div class="panel-kicker">Network</div>
+ <h2 class="panel-title">璁惧缃戠粶鍒嗘瀽</h2>
+ <div class="panel-desc">姹囨�绘渶鏂� Ping 鏍锋湰鐨勮繛閫氭�с�佸欢杩熶笌寮傚父璁惧锛屽府鍔╁揩閫熷彂鐜扮綉缁滄尝鍔ㄣ��</div>
+ </div>
+ <div class="panel-actions">
+ <el-tag size="small" :type="network.overview.attentionDevices > 0 ? 'warning' : 'success'">
+ 闇�鍏虫敞 {{ formatNumber(network.overview.attentionDevices) }}
+ </el-tag>
+ <el-button size="mini" plain @click="openDevicePingAnalysis">鏌ョ湅鏄庣粏</el-button>
+ </div>
+ </div>
+
+ <div class="mini-grid">
+ <div class="mini-card network-mini-ok">
+ <div class="mini-label">姝e父</div>
+ <div class="mini-value">{{ formatNumber(network.overview.okDevices) }}</div>
+ <div class="mini-hint">鏈�鏂版牱鏈姸鎬� OK</div>
+ </div>
+ <div class="mini-card network-mini-warning">
+ <div class="mini-label">娉㈠姩</div>
+ <div class="mini-value">{{ formatNumber(network.overview.unstableDevices) }}</div>
+ <div class="mini-hint">閮ㄥ垎鎺㈡祴鎴愬姛</div>
+ </div>
+ <div class="mini-card network-mini-offline">
+ <div class="mini-label">瓒呮椂/寮傚父</div>
+ <div class="mini-value">{{ formatNumber(network.overview.offlineDevices) }}</div>
+ <div class="mini-hint">鏆傛棤鏁版嵁 {{ formatNumber(network.overview.noDataDevices) }}</div>
+ </div>
+ <div class="mini-card network-mini-latency">
+ <div class="mini-label">骞冲潎寤惰繜</div>
+ <div class="mini-value">{{ formatLatency(network.overview.avgLatencyMs) }}</div>
+ <div class="mini-hint">宄板�� {{ formatLatency(network.overview.maxLatencyMs) }}</div>
+ </div>
+ </div>
+
+ <div class="chart-card">
+ <div class="chart-title">杩為�氱姸鎬佸垎甯�</div>
+ <div class="chart-subtitle">{{ networkSamplingText() }}</div>
+ <div ref="networkStatusChart" class="network-chart-box"></div>
+ </div>
+
+ <div class="route-list" v-if="network.focusDevices.length">
+ <div v-for="item in network.focusDevices" :key="item.name + '-' + item.ip" class="route-row">
+ <div class="route-row-main">
+ <div class="route-row-name">{{ item.name }}</div>
+ <div class="route-row-desc">{{ displayText(item.ip, '-') }}</div>
+ <div v-if="item.message" :class="['network-note', 'network-note-' + (item.statusType || 'info')]">{{ item.message }}</div>
+ </div>
+ <div class="route-row-side">
+ <el-tag size="mini" :type="item.statusType">{{ item.statusText }}</el-tag>
+ <div class="route-extra">骞冲潎 {{ formatLatency(item.avgLatencyMs) }}</div>
+ <div class="route-extra">鏈�杩戞牱鏈� {{ displayText(item.latestTimeLabel, '-') }}</div>
+ </div>
+ </div>
+ </div>
+ <div v-else-if="network.overview.totalDevices > 0" class="network-healthy-state">
+ <div class="network-healthy-main">
+ <div class="network-healthy-title">褰撳墠缃戠粶鎺㈡祴绋冲畾</div>
+ <div class="network-healthy-desc">宸茬撼鍏� {{ formatNumber(network.overview.totalDevices) }} 鍙拌澶囷紝鏈�杩戜竴杞湭鍙戠幇瓒呮椂鎴栨尝鍔ㄣ��</div>
+ </div>
+ <div class="network-healthy-tags">
+ <div class="network-healthy-tag">姝e父 {{ formatNumber(network.overview.okDevices) }}</div>
+ <div class="network-healthy-tag">骞冲潎 {{ formatLatency(network.overview.avgLatencyMs) }}</div>
+ <div class="network-healthy-tag">宄板�� {{ formatLatency(network.overview.maxLatencyMs) }}</div>
+ </div>
+ </div>
+ <el-empty v-else description="鏆傛棤璁惧缃戠粶鏍锋湰"></el-empty>
+ </section>
+ </div>
</div>
<div v-if="loading" class="loading-mask">
@@ -858,6 +1054,6 @@
<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
<script type="text/javascript" src="../../static/vue/element/element.js"></script>
<script type="text/javascript" src="../../static/js/echarts/echarts.min.js"></script>
-<script type="text/javascript" src="../../static/js/dashboard/dashboard.js"></script>
+<script type="text/javascript" src="../../static/js/dashboard/dashboard.js?v=20260317-dashboard-network-focus"></script>
</body>
</html>
diff --git a/src/main/webapp/views/devicePingLog/devicePingLog.html b/src/main/webapp/views/devicePingLog/devicePingLog.html
new file mode 100644
index 0000000..6e8b7aa
--- /dev/null
+++ b/src/main/webapp/views/devicePingLog/devicePingLog.html
@@ -0,0 +1,480 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>璁惧缃戠粶鍒嗘瀽</title>
+ <link rel="stylesheet" href="../../static/vue/element/element.css" />
+ <style>
+ [v-cloak] {
+ display: none;
+ }
+
+ html,
+ body {
+ margin: 0;
+ min-height: 100%;
+ font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
+ background: linear-gradient(180deg, #f4f7fb 0%, #edf2f7 100%);
+ color: #243447;
+ }
+
+ * {
+ box-sizing: border-box;
+ }
+
+ .page-shell {
+ max-width: 1680px;
+ margin: 0 auto;
+ padding: 16px;
+ }
+
+ .page-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+ flex-wrap: wrap;
+ }
+
+ .page-title {
+ font-size: 28px;
+ font-weight: 700;
+ color: #1f3142;
+ }
+
+ .page-meta {
+ font-size: 12px;
+ color: #7d8ea2;
+ }
+
+ .summary-grid {
+ display: grid;
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ gap: 12px;
+ margin-bottom: 16px;
+ }
+
+ .summary-card {
+ padding: 14px 16px;
+ border-radius: 18px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 251, 255, 0.92) 100%);
+ border: 1px solid rgba(216, 226, 235, 0.96);
+ box-shadow: 0 14px 28px rgba(39, 63, 92, 0.06);
+ min-height: 90px;
+ }
+
+ .summary-label {
+ font-size: 12px;
+ color: #7d8ea2;
+ }
+
+ .summary-value {
+ margin-top: 8px;
+ font-size: 28px;
+ line-height: 1.1;
+ font-weight: 700;
+ color: #22364a;
+ }
+
+ .summary-sub {
+ margin-top: 8px;
+ font-size: 12px;
+ color: #8a99ab;
+ }
+
+ .panel {
+ margin-top: 16px;
+ border-radius: 22px;
+ border: 1px solid rgba(216, 226, 235, 0.96);
+ background: rgba(255, 255, 255, 0.9);
+ box-shadow: 0 16px 32px rgba(39, 63, 92, 0.06);
+ overflow: hidden;
+ }
+
+ .panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 18px 18px 0;
+ flex-wrap: wrap;
+ }
+
+ .panel-title {
+ font-size: 18px;
+ font-weight: 700;
+ color: #22364a;
+ }
+
+ .panel-body {
+ padding: 16px 18px 18px;
+ }
+
+ .toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin-bottom: 12px;
+ }
+
+ .toolbar-right {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+
+ .detail-shell {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .detail-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+
+ .detail-selected {
+ font-size: 14px;
+ color: #50657b;
+ font-weight: 600;
+ }
+
+ .detail-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ gap: 12px;
+ }
+
+ .detail-card {
+ padding: 14px 16px;
+ border-radius: 16px;
+ background: #f7fafc;
+ border: 1px solid #e1e9f2;
+ min-height: 84px;
+ }
+
+ .detail-card-label {
+ font-size: 12px;
+ color: #8091a4;
+ }
+
+ .detail-card-value {
+ margin-top: 8px;
+ font-size: 24px;
+ line-height: 1.1;
+ font-weight: 700;
+ color: #23364a;
+ }
+
+ .chart-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr);
+ gap: 16px;
+ }
+
+ .chart-card {
+ padding: 16px;
+ border-radius: 18px;
+ border: 1px solid #e2eaf2;
+ background: #fbfdff;
+ }
+
+ .chart-title {
+ margin-bottom: 12px;
+ font-size: 16px;
+ font-weight: 700;
+ color: #243447;
+ }
+
+ .chart-box {
+ height: 340px;
+ width: 100%;
+ }
+
+ .empty-shell {
+ padding: 48px 16px;
+ text-align: center;
+ font-size: 14px;
+ color: #8a99ab;
+ border: 1px dashed #d8e2ec;
+ border-radius: 16px;
+ background: #fafcff;
+ }
+
+ .status-chip {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 74px;
+ height: 26px;
+ padding: 0 10px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 700;
+ }
+
+ .status-chip.ok {
+ color: #177d5a;
+ background: rgba(30, 170, 112, 0.14);
+ }
+
+ .status-chip.unstable {
+ color: #b56d05;
+ background: rgba(245, 154, 74, 0.16);
+ }
+
+ .status-chip.offline {
+ color: #bb3d3d;
+ background: rgba(222, 92, 92, 0.14);
+ }
+
+ .status-chip.nodata {
+ color: #6f8194;
+ background: rgba(176, 190, 204, 0.2);
+ }
+
+ @media (max-width: 1360px) {
+ .summary-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
+ .detail-summary-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
+ .chart-grid {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ @media (max-width: 768px) {
+ .page-shell {
+ padding: 12px;
+ }
+
+ .summary-grid,
+ .detail-summary-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .page-title {
+ font-size: 24px;
+ }
+ }
+ </style>
+</head>
+<body>
+<div id="app" v-cloak class="page-shell">
+ <div class="page-head">
+ <div class="page-title">璁惧缃戠粶鍒嗘瀽</div>
+ <div class="page-meta">鍖呭ぇ灏� {{ formatPacketSize(samplingConfig.packetSize) }}锛寋{ samplingConfigText }}</div>
+ </div>
+
+ <section class="summary-grid">
+ <div class="summary-card">
+ <div class="summary-label">璁惧鎬绘暟</div>
+ <div class="summary-value">{{ overviewSummary.totalDevices }}</div>
+ <div class="summary-sub">宸查厤缃� IP 鐨勮澶�</div>
+ </div>
+ <div class="summary-card">
+ <div class="summary-label">姝e父</div>
+ <div class="summary-value">{{ overviewSummary.okDevices }}</div>
+ <div class="summary-sub">鏈�杩戞牱鏈姸鎬� OK</div>
+ </div>
+ <div class="summary-card">
+ <div class="summary-label">娉㈠姩</div>
+ <div class="summary-value">{{ overviewSummary.unstableDevices }}</div>
+ <div class="summary-sub">閮ㄥ垎鎺㈡祴鎴愬姛</div>
+ </div>
+ <div class="summary-card">
+ <div class="summary-label">瓒呮椂/寮傚父</div>
+ <div class="summary-value">{{ overviewSummary.offlineDevices }}</div>
+ <div class="summary-sub">鏈�杩戞牱鏈笉鍙揪</div>
+ </div>
+ <div class="summary-card">
+ <div class="summary-label">鏆傛棤鏁版嵁</div>
+ <div class="summary-value">{{ overviewSummary.noDataDevices }}</div>
+ <div class="summary-sub">杩樻病鏈夎惤鐩樻牱鏈�</div>
+ </div>
+ <div class="summary-card">
+ <div class="summary-label">鏁翠綋骞冲潎寤惰繜</div>
+ <div class="summary-value">{{ formatLatency(overviewSummary.avgLatencyMs) }}</div>
+ <div class="summary-sub">宄板�� {{ formatLatency(overviewSummary.maxLatencyMs) }}</div>
+ </div>
+ </section>
+
+ <section class="panel">
+ <div class="panel-head">
+ <div class="panel-title">璁惧鎬昏</div>
+ <el-button size="small" :loading="overviewLoading" @click="loadOverview">鍒锋柊</el-button>
+ </div>
+ <div class="panel-body">
+ <div class="toolbar">
+ <el-form :inline="true" size="small" :model="overviewFilters" style="margin-bottom: -18px;">
+ <el-form-item label="璁惧绫诲瀷">
+ <el-select v-model="overviewFilters.deviceType" clearable style="width: 120px;">
+ <el-option label="鍏ㄩ儴" value=""></el-option>
+ <el-option label="Crn" value="Crn"></el-option>
+ <el-option label="DualCrn" value="DualCrn"></el-option>
+ <el-option label="Devp" value="Devp"></el-option>
+ <el-option label="Rgv" value="Rgv"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏抽敭瀛�">
+ <el-input v-model.trim="overviewFilters.keyword" clearable style="width: 220px;" placeholder="璁惧鍙� / IP"></el-input>
+ </el-form-item>
+ </el-form>
+ <div class="toolbar-right">
+ <span class="page-meta">鎬昏璁惧 {{ filteredOverviewRows.length }} 鍙�</span>
+ </div>
+ </div>
+
+ <el-table :data="filteredOverviewRows" border stripe size="mini" v-loading="overviewLoading" style="width: 100%;">
+ <el-table-column label="璁惧" min-width="170">
+ <template slot-scope="scope">
+ {{ scope.row.deviceType }}-{{ scope.row.deviceNo }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="ip" label="IP" min-width="150"></el-table-column>
+ <el-table-column label="鐘舵��" width="110" align="center">
+ <template slot-scope="scope">
+ <span class="status-chip" :class="statusClass(scope.row.status)">{{ scope.row.statusText }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎴愬姛鐜�" width="90" align="right">
+ <template slot-scope="scope">
+ {{ formatPercent(scope.row.successRate) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="骞冲潎" width="90" align="right">
+ <template slot-scope="scope">
+ {{ formatLatency(scope.row.avgLatencyMs) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈�灏�" width="90" align="right">
+ <template slot-scope="scope">
+ {{ formatLatency(scope.row.minLatencyMs) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈�澶�" width="90" align="right">
+ <template slot-scope="scope">
+ {{ formatLatency(scope.row.maxLatencyMs) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="latestTimeLabel" label="鏇存柊鏃堕棿" width="170"></el-table-column>
+ <el-table-column prop="message" label="璇存槑" min-width="200" show-overflow-tooltip></el-table-column>
+ <el-table-column label="鎿嶄綔" width="110" align="center" fixed="right">
+ <template slot-scope="scope">
+ <el-button size="mini" type="primary" plain @click="openDetail(scope.row)">鏌ョ湅璇︽儏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </section>
+
+ <section class="panel">
+ <div class="panel-head">
+ <div class="panel-title">璁惧璇︽儏</div>
+ </div>
+ <div class="panel-body">
+ <div v-if="!currentDevice" class="empty-shell">浠庝笂鏂硅澶囨�昏閫夋嫨涓�鍙拌澶囨煡鐪嬬绾ф槑缁�</div>
+ <div v-else class="detail-shell">
+ <div class="detail-toolbar">
+ <div class="detail-selected">{{ currentDevice.label }}</div>
+ <div class="toolbar-right">
+ <el-form :inline="true" size="small" :model="detailFilters" style="margin-bottom: -18px;">
+ <el-form-item label="鏃堕棿鑼冨洿">
+ <el-date-picker
+ v-model="detailFilters.range"
+ type="datetimerange"
+ unlink-panels
+ range-separator="鑷�"
+ start-placeholder="寮�濮�"
+ end-placeholder="缁撴潫"
+ style="width: 360px;">
+ </el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="setQuickRange(30)">30 鍒嗛挓</el-button>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="setQuickRange(60)">1 灏忔椂</el-button>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="setQuickRange(360)">6 灏忔椂</el-button>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" :loading="detailLoading" @click="queryTrend">鏌ヨ</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </div>
+
+ <div class="detail-summary-grid">
+ <div class="detail-card">
+ <div class="detail-card-label">鐘舵��</div>
+ <div class="detail-card-value">{{ detailSummary.latestStatus || '--' }}</div>
+ </div>
+ <div class="detail-card">
+ <div class="detail-card-label">鍖呭ぇ灏�</div>
+ <div class="detail-card-value">{{ formatPacketSize(detailSummary.packetSize) }}</div>
+ </div>
+ <div class="detail-card">
+ <div class="detail-card-label">鎴愬姛鐜�</div>
+ <div class="detail-card-value">{{ formatPercent(detailSummary.successRate) }}</div>
+ </div>
+ <div class="detail-card">
+ <div class="detail-card-label">骞冲潎</div>
+ <div class="detail-card-value">{{ formatLatency(detailSummary.avgLatencyMs) }}</div>
+ </div>
+ <div class="detail-card">
+ <div class="detail-card-label">鏈�灏�</div>
+ <div class="detail-card-value">{{ formatLatency(detailSummary.minLatencyMs) }}</div>
+ </div>
+ <div class="detail-card">
+ <div class="detail-card-label">鏈�澶�</div>
+ <div class="detail-card-value">{{ formatLatency(detailSummary.maxLatencyMs) }}</div>
+ </div>
+ </div>
+
+ <div class="chart-grid">
+ <div class="chart-card">
+ <div class="chart-title">寤惰繜</div>
+ <div v-if="!series.length && !detailLoading" class="empty-shell">褰撳墠鑼冨洿鏆傛棤绉掔骇鏍锋湰</div>
+ <div v-show="series.length || detailLoading" ref="latencyChart" class="chart-box"></div>
+ </div>
+ <div class="chart-card">
+ <div class="chart-title">鎴愬姛鐜� / 澶辫触娆℃暟</div>
+ <div v-if="!series.length && !detailLoading" class="empty-shell">褰撳墠鑼冨洿鏆傛棤绉掔骇鏍锋湰</div>
+ <div v-show="series.length || detailLoading" ref="availabilityChart" class="chart-box"></div>
+ </div>
+ </div>
+
+ <el-table :data="alerts" border stripe size="mini" style="width: 100%;">
+ <el-table-column prop="timeLabel" label="鏃堕棿" width="180"></el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="110"></el-table-column>
+ <el-table-column prop="ip" label="IP" width="150"></el-table-column>
+ <el-table-column prop="message" label="璇存槑" min-width="240" show-overflow-tooltip></el-table-column>
+ </el-table>
+ </div>
+ </div>
+ </section>
+</div>
+
+<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+<script type="text/javascript" src="../../static/vue/element/element.js"></script>
+<script type="text/javascript" src="../../static/js/echarts/echarts.min.js"></script>
+<script type="text/javascript" src="../../static/js/devicePingLog/devicePingLog.js?v=20260316_device_ping_packet_size_detail" charset="utf-8"></script>
+</body>
+</html>
diff --git a/src/main/webapp/views/index.html b/src/main/webapp/views/index.html
index 12d725e..2fdcc81 100644
--- a/src/main/webapp/views/index.html
+++ b/src/main/webapp/views/index.html
@@ -862,7 +862,7 @@
<script type="text/javascript" src="../static/vue/js/vue.min.js"></script>
<script type="text/javascript" src="../static/vue/element/element.js"></script>
<script>
- var DASHBOARD_VIEW_VERSION = "20260316-hero-two-rows-flat-compact";
+ var DASHBOARD_VIEW_VERSION = "20260317-dashboard-network-balanced-columns";
var HOME_TAB_CONFIG = {
title: "绯荤粺浠〃鐩�",
url: baseUrl + "/views/dashboard/dashboard.html?layoutVersion=" + encodeURIComponent(DASHBOARD_VIEW_VERSION),
@@ -1177,7 +1177,7 @@
return this.tl(title);
},
findMenuMeta: function (tab) {
- var normalizedUrl;
+ var menuEntry;
var i;
var j;
var group;
@@ -1185,20 +1185,69 @@
if (!tab) {
return null;
}
- normalizedUrl = this.resolveViewSrc(tab.url);
+ if (tab.menuKey) {
+ for (i = 0; i < this.menus.length; i++) {
+ group = this.menus[i];
+ for (j = 0; j < group.subMenu.length; j++) {
+ item = group.subMenu[j];
+ if (item.tabKey === tab.menuKey) {
+ return {
+ group: group,
+ item: item
+ };
+ }
+ }
+ }
+ }
+ menuEntry = this.findMenuEntryByUrl(tab.url);
+ if (menuEntry) {
+ return menuEntry;
+ }
+ return null;
+ },
+ normalizeMenuMatchUrl: function (url, stripQuery) {
+ var normalized = this.resolveViewSrc(url || "");
+ var hashIndex = normalized.indexOf("#");
+ var queryIndex;
+ if (hashIndex > -1) {
+ normalized = normalized.substring(0, hashIndex);
+ }
+ if (stripQuery) {
+ queryIndex = normalized.indexOf("?");
+ if (queryIndex > -1) {
+ normalized = normalized.substring(0, queryIndex);
+ }
+ }
+ return normalized;
+ },
+ findMenuEntryByUrl: function (url) {
+ var normalized = this.normalizeMenuMatchUrl(url, false);
+ var normalizedBase = this.normalizeMenuMatchUrl(url, true);
+ var fallback = null;
+ var i;
+ var j;
+ var group;
+ var item;
+
for (i = 0; i < this.menus.length; i++) {
group = this.menus[i];
for (j = 0; j < group.subMenu.length; j++) {
item = group.subMenu[j];
- if ((tab.menuKey && item.tabKey === tab.menuKey) || item.url === normalizedUrl) {
+ if (item.url === normalized) {
return {
+ group: group,
+ item: item
+ };
+ }
+ if (!fallback && this.normalizeMenuMatchUrl(item.url, true) === normalizedBase) {
+ fallback = {
group: group,
item: item
};
}
}
}
- return null;
+ return fallback;
},
syncTabMeta: function (tab, homeConfig, profileConfig) {
var menuMeta;
@@ -1602,38 +1651,16 @@
});
},
findMenuKeyByUrl: function (url) {
- var normalized = this.resolveViewSrc(url);
- var i;
- var j;
- var group;
- var item;
-
- for (i = 0; i < this.menus.length; i++) {
- group = this.menus[i];
- for (j = 0; j < group.subMenu.length; j++) {
- item = group.subMenu[j];
- if (item.url === normalized) {
- return item.tabKey;
- }
- }
+ var entry = this.findMenuEntryByUrl(url);
+ if (entry && entry.item) {
+ return entry.item.tabKey || "";
}
return "";
},
findMenuGroupIndexByUrl: function (url) {
- var normalized = this.resolveViewSrc(url);
- var i;
- var j;
- var group;
- var item;
-
- for (i = 0; i < this.menus.length; i++) {
- group = this.menus[i];
- for (j = 0; j < group.subMenu.length; j++) {
- item = group.subMenu[j];
- if (item.url === normalized) {
- return "group-" + group.menuId;
- }
- }
+ var entry = this.findMenuEntryByUrl(url);
+ if (entry && entry.group) {
+ return "group-" + entry.group.menuId;
}
return "";
},
@@ -2027,31 +2054,35 @@
window.index = window.index || {};
window.index.loadView = function (param) {
var url;
+ var menuEntry;
if (!param || !param.menuPath) {
return;
}
url = that.resolveViewSrc(param.menuPath);
+ menuEntry = that.findMenuEntryByUrl(url);
that.addOrActivateTab({
- title: that.stripTags(param.menuName) || that.t("common.workPage"),
- url: url,
+ title: that.stripTags(param.menuName) || (menuEntry && menuEntry.item ? menuEntry.item.name : that.t("common.workPage")),
+ url: menuEntry && menuEntry.item ? menuEntry.item.url : url,
home: false,
- group: that.t("common.businessPage"),
- menuKey: that.findMenuKeyByUrl(url)
+ group: menuEntry && menuEntry.group ? menuEntry.group.menu : that.t("common.businessPage"),
+ menuKey: menuEntry && menuEntry.item ? menuEntry.item.tabKey : that.findMenuKeyByUrl(url)
});
};
window.index.loadHome = function (param) {
var url;
+ var menuEntry;
if (!param || !param.menuPath) {
that.openHomeTab();
return;
}
url = that.resolveViewSrc(param.menuPath);
+ menuEntry = that.findMenuEntryByUrl(url);
that.addOrActivateTab({
- title: that.stripTags(param.menuName) || that.resolveHomeConfig().title,
- url: url,
+ title: that.stripTags(param.menuName) || (menuEntry && menuEntry.item ? menuEntry.item.name : that.resolveHomeConfig().title),
+ url: menuEntry && menuEntry.item ? menuEntry.item.url : url,
home: true,
- group: that.resolveHomeConfig().group,
- menuKey: that.findMenuKeyByUrl(url)
+ group: menuEntry && menuEntry.group ? menuEntry.group.menu : that.resolveHomeConfig().group,
+ menuKey: menuEntry && menuEntry.item ? menuEntry.item.tabKey : that.findMenuKeyByUrl(url)
});
};
diff --git a/tmp/docs/wcs_wms_plan_check.html b/tmp/docs/wcs_wms_plan_check.html
deleted file mode 100644
index a00678b..0000000
--- a/tmp/docs/wcs_wms_plan_check.html
+++ /dev/null
@@ -1,922 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
-<head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
- <meta http-equiv="Content-Style-Type" content="text/css">
- <title></title>
- <meta name="Author" content="python-docx">
- <meta name="LastAuthor" content="Junjie Xie">
- <meta name="Description" content="generated by python-docx">
- <meta name="CreationTime" content="2013-12-25T07:15:00Z">
- <meta name="ModificationTime" content="2013-12-25T07:15:00Z">
- <meta name="Generator" content="Cocoa HTML Writer">
- <meta name="CocoaVersion" content="2685.4">
- <style type="text/css">
- p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px 'Songti SC'}
- p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; text-align: justify; font: 11.0px 'Songti SC'; min-height: 16.0px}
- p.p3 {margin: 12.0px 0.0px 8.0px 0.0px; text-align: center; font: 20.0px 'Heiti SC Light'}
- p.p4 {margin: 12.0px 0.0px 8.0px 0.0px; text-align: center; font: 20.0px 'Heiti SC Light'; min-height: 21.0px}
- p.p5 {margin: 8.0px 0.0px 4.0px 0.0px; font: 20.0px 'Heiti SC Light'; min-height: 21.0px}
- p.p6 {margin: 8.0px 0.0px 4.0px 0.0px; font: 13.0px 'Songti SC'}
- p.p7 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Times}
- p.p8 {margin: 8.0px 0.0px 6.0px 0.0px; font: 14.0px 'Songti SC'}
- p.p9 {margin: 0.0px 0.0px 0.0px 0.0px; text-align: justify; font: 12.0px 'Songti SC'}
- p.p10 {margin: 0.0px 0.0px 0.0px 0.0px; text-align: center; font: 10.5px 'Songti SC'}
- p.p11 {margin: 0.0px 0.0px 0.0px 0.0px; font: 10.5px 'Songti SC'}
- p.p12 {margin: 0.0px 0.0px 0.0px 0.0px; text-align: justify; font: 10.5px 'Songti SC'; min-height: 15.0px}
- p.p13 {margin: 0.0px 0.0px 2.0px 0.0px; font: 12.0px 'Songti SC'}
- p.p14 {margin: 0.0px 0.0px 2.0px 0.0px; font: 12.0px 'Songti SC'; min-height: 17.0px}
- p.p15 {margin: 0.0px 0.0px 0.0px 0.0px; text-align: justify; font: 10.5px 'Songti SC'}
- p.p16 {margin: 0.0px 0.0px 0.0px 0.0px; text-align: justify; font: 12.0px Times}
- p.p17 {margin: 0.0px 0.0px 0.0px 0.0px; font: 10.5px Times}
- p.p18 {margin: 0.0px 0.0px 0.0px 0.0px; font: 10.5px 'Songti SC'; min-height: 15.0px}
- span.s1 {text-decoration: underline}
- span.s2 {font: 13.0px Times}
- span.s3 {font: 10.5px Times}
- span.s4 {font: 12.0px Times}
- span.s5 {font: 10.5px 'Songti SC'}
- table.t1 {border-collapse: collapse}
- td.td1 {border-style: solid; border-width: 1.0px 1.0px 1.0px 1.0px; border-color: #bfbfbf #bfbfbf #bfbfbf #bfbfbf; padding: 0.0px 5.0px 0.0px 5.0px}
- </style>
-</head>
-<body>
-<p class="p1"><b>鎻愭缂栧彿锛歘_____锛堟湰浜轰笉鐢ㄥ~鍐欙紝鎻愪氦鍚庣敱鐮旂┒闄㈢粺涓�缂栧彿锛�</b></p>
-<p class="p2"><b></b><br></p>
-<p class="p2"><b></b><br></p>
-<p class="p2"><b></b><br></p>
-<p class="p3"><b>闆嗗洟鍏徃 2026 骞村害鎶�鏈垱鏂�<br>
-鐮斿彂椤圭洰鎻愭涔�</b></p>
-<p class="p4"><b></b><br></p>
-<p class="p4"><b></b><br></p>
-<p class="p4"><b></b><br></p>
-<p class="p4"><b></b><br></p>
-<p class="p4"><b></b><br></p>
-<p class="p4"><b></b><br></p>
-<p class="p4"><b></b><br></p>
-<p class="p5"><b></b><br></p>
-<p class="p6"><b>椤圭洰鍚嶇О锛�</b><span class="s1"><b>闈㈠悜 WCS/WMS 鍗忓悓鐨勫Agent AI鏅鸿兘浣撲笌Prompt鑷涔犲叧閿妧鏈爺鍙�</b></span></p>
-<p class="p6"><b>鎻愭瀹屾垚浜猴細</b></p>
-<p class="p6"><b>鎻愭鍙備笌浜�</b><span class="s2"><b>锛�</b></span></p>
-<p class="p7"></p>
-<p class="p8"><b>涓�銆佺珛椤硅儗鏅笌鎰忎箟</b></p>
-<p class="p9"><b>闆嗗洟鍏徃 2026 骞村害鎶�鏈垱鏂扮爺鍙戦」鐩緛闆嗘槑纭紦鍔卞洿缁曚汉宸ユ櫤鑳姐�佹満鍣ㄤ汉璋冨害涓庣畻娉曘�佸畨鍏ㄧ洃鎺с�佺豢鑹叉櫤鑳戒粨鍌ㄧ墿娴佺瓑鏂瑰悜寮�灞曠爺鍙戯紝閲嶇偣鏀寔鑳藉瑙e喅鐜版湁浜у搧涓庣郴缁熷叡鎬ч棶棰樸�佹敮鎾戜笟鍔″彂灞曠棝鐐广�佸苟鍏峰鍓嶇灮鎬х殑鎶�鏈垱鏂伴」鐩�傛湰鎻愭涓庤鏂瑰悜楂樺害涓�鑷达紝鑱氱劍浠撳偍鐗╂祦鍦烘櫙涓� WCS 涓� WMS 鍗忓悓鏁堢巼銆佸紓甯稿缃兘鍔涖�佹帶鍒跺畨鍏ㄦ�у拰浜у搧鏅鸿兘鍖栨按骞虫彁鍗囥��</b></p>
-<p class="p9"><b>褰撳墠鍏徃鍦� WCS/WMS 椤圭洰浜や粯鍜岃繍缁磋繃绋嬩腑锛屽凡绉疮浜嗗ぇ閲忎笟鍔¤鍒欍�佹帴鍙g粡楠屻�佸紓甯稿鐞嗙粡楠屽拰鐜板満璋冭瘯鐭ヨ瘑锛屼絾杩欎簺鑳藉姏浠嶄富瑕佷緷璧栦汉宸ョ粡楠屻�佸浐瀹氳鍒欏拰鍒嗘暎鏂囨。锛屽瓨鍦ㄦ煡璇㈠垎鏋愭晥鐜囦笉楂樸�佽法绯荤粺鍗忓悓涓嶈冻銆佺粡楠岄毦娌夋穩銆丄I 鎺у埗杈圭晫涓嶆竻鏅扮瓑鍏辨�ч棶棰樸��</b></p>
-<p class="p9"><b>鏈」鐩嫙鍦ㄧ幇鏈夌郴缁熷熀纭�涓婃瀯寤哄彲鎺с�佸彲瀹¤銆佸彲澶嶇敤鐨勬櫤鑳戒綋骞冲彴锛氬湪涓氬姟灞傚疄鐜板鏅鸿兘浣撳崗鍚屽垎鏋愩�佸缓璁拰杈呭姪鍐崇瓥锛屽湪鎵ц灞備互 MCP 涓烘爣鍑嗗寲鎺ュ叆鍗忚瀹炵幇璧勬簮璇诲彇銆佸伐鍏疯皟鐢ㄤ笌 Prompt 缂栨帓锛屽湪娌荤悊灞傚紩鍏� PromptOps銆佹暟瀛楀鐢熶豢鐪熷拰鍏ㄩ摼璺璁℃満鍒讹紝閫愭鎵撻�氫粠鈥滃彧璇昏緟鍔┾�濆埌鈥滃彈鎺у啓鍏モ�濆啀鍒扳�滃眬閮ㄨ嚜鍔ㄥ寲鈥濈殑鑳藉姏婕旇繘璺緞銆�</b></p>
-<p class="p9"><b>椤圭洰鐨勭珛椤规剰涔変富瑕佷綋鐜板湪鍥涗釜鏂归潰锛氫竴鏄矇娣�褰㈡垚鍙鐢ㄧ殑 AI 鑳藉姏搴曞骇锛屾彁鍗囧叕鍙镐骇鍝佺珵浜夊姏鍜岄」鐩氦浠樿兘鍔涳紱浜屾槸鎶婁笓瀹剁粡楠屻�丼OP 鍜屽巻鍙叉棩蹇楄浆鍖栦负绯荤粺鍖栫煡璇嗚祫浜э紝闄嶄綆浜哄憳渚濊禆锛涗笁鏄湪瀹夊叏鍙帶鍓嶆彁涓嬫彁鍗囪鍒掔敓鎴愩�佸紓甯稿缃�佹姤琛ㄧ敓鎴愬拰杩愮淮杈呭姪鏁堢巼锛涘洓鏄负鍚庣画鏈哄櫒浜鸿皟搴︺�佸畨鍏ㄧ洃鎺с�佺豢鑹叉櫤鑳戒粨鍌ㄧ瓑鍦烘櫙鎻愪緵鍙鍒剁殑鍏辨�ф妧鏈祫浜с��</b></p>
-<p class="p8"><b>浜屻�侀」鐩洰鏍囦笌鎷熻В鍐崇殑鍏抽敭闂</b></p>
-<p class="p9"><b>鏈」鐩嫙鍦� 12 涓湀鍐呭畬鎴愬Agent AI 鏅鸿兘浣撳钩鍙版牳蹇冭兘鍔涚爺鍙戯紝骞跺湪 WCS/WMS 鍏稿瀷鍦烘櫙瀹屾垚璇曠偣楠岃瘉锛屽舰鎴愨�滃钩鍙板簳搴� + 瀹夊叏娌荤悊 + 鍦烘櫙搴旂敤 + 璇勬祴浣撶郴鈥濈殑瀹屾暣鑳藉姏闂幆銆�</b></p>
-<p class="p9"><b>2.1 椤圭洰鐩爣</b></p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p10"><b>搴忓彿</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>鐩爣椤�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>棰勬湡鎸囨爣</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>1</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛澶欰gent鍗忓悓搴曞骇</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>瑕嗙洊鏌ヨ瑙i噴銆佽鍒掑缓璁�佸紓甯歌瘖鏂�佸彈鎺ф搷浣� 4 绫绘牳蹇冨満鏅�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>2</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛 Prompt 鑷涔犱笌娌荤悊鏈哄埗</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛 Prompt 妯℃澘锛屾敮鎸佺増鏈鐞嗐�佺绾胯瘎娴嬨�佺伆搴﹀彂甯冨拰鍥炴粴</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>3</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛 MCP 鍙帶闆嗘垚鑳藉姏</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>瀹屾垚 WMS/WCS/SOP/鏃ュ織绛夎祫婧愪笌宸ュ叿鐨勬爣鍑嗗寲鎺ュ叆锛屽舰鎴愮粺涓�璋冪敤鍏ュ彛</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>4</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛瀹夊叏鎺у埗闂幆</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>楂橀闄╂搷浣滃叏閮ㄧ粡杩囨潈闄愭牎楠屻�佷豢鐪熼獙璇併�佸弻閲嶇‘璁ゅ拰瀹¤鐣欑棔</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>5</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>瀹屾垚璇曠偣涓庤瘎娴嬮獙璇�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>褰㈡垚鏈湴妯″瀷涓庝簯绔ā鍨嬪姣旂粨璁猴紝骞跺湪鍏稿瀷鍦烘櫙瀹屾垚璇曠偣钀藉湴</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p12"><b></b><br></p>
-<p class="p9"><b>2.2 鎷熻В鍐崇殑鍏抽敭闂</b></p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p10"><b>鍏抽敭闂</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>鐜扮姸琛ㄧ幇</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>鎷熻В鍐虫�濊矾</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍦烘櫙鐞嗚В涓庤矾鐢变笉缁熶竴</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏌ヨ銆佸垎鏋愩�佹帶鍒剁被璇锋眰娣锋潅锛屼緷璧栦汉宸ュ垽鏂�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>閫氳繃鍦烘櫙璺敱 Agent 鍒嗙被鍒嗘祦锛岀粺涓�璇嗗埆椋庨櫓绛夌骇涓庡伐鍏疯竟鐣�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>涓氬姟鐭ヨ瘑鍒嗘暎銆佹洿鏂板洶闅�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>SOP銆佹帴鍙h鏄庛�佸紓甯哥粡楠屽垎鏁e湪鏂囨。鍜屼釜浜虹粡楠屼腑</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤鸿鐭ヨ瘑妫�绱� Agent 涓庣煡璇嗗簱锛屽鏂囨。銆佹棩蹇椼�佸伐鍗曡繘琛岀粨鏋勫寲娌夋穩</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>宸ュ叿璋冪敤涓嶇ǔ瀹氥�佷笉鍙帶</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鑷劧璇█鐩存帴涓嬫矇鍒扮郴缁熻皟鐢ㄥ瓨鍦ㄩ闄�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>閫氳繃缁撴瀯鍖� Prompt銆丼chema 鏍¢獙涓� MCP Tools 瀹炵幇鏍囧噯鍖栬皟鐢�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>AI 杩涘叆鎺у埗鐜妭鐨勫畨鍏ㄩ闄╅珮</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>璇搷浣溿�佽秺鏉冦�佹彁绀烘敞鍏ャ�佺己灏戝洖婊氭満鍒�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寮曞叆鏉冮檺鍒嗙骇銆丄BAC 绛栫暐銆佹暟瀛楀鐢熶豢鐪熴�佸鎵瑰拰鍛戒护璐︽湰瀹¤</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>Prompt 浣跨敤缁忛獙闅炬矇娣�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>Prompt 闈欐�佺淮鎶わ紝缂哄皯鏁堟灉鏁版嵁闂幆</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛 PromptOps 娴佺▼锛屼互鏃ュ織銆佸弽棣堜笌璇勬祴椹卞姩鎸佺画浼樺寲</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>妯″瀷閫夊瀷缂哄皯缁熶竴渚濇嵁</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏈湴妯″瀷涓庝簯绔ā鍨嬪湪鏁堟灉銆佹垚鏈�佹椂寤朵笂宸紓澶�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛缁熶竴璇勬祴鐭╅樀锛屽舰鎴愭ā鍨嬮�夊瀷鍜屾贩鍚堥儴缃茬瓥鐣�</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p8"><b>涓夈�佸垵姝ユ妧鏈�濊矾鎴栧疄鏂芥柟妗�</b></p>
-<p class="p9"><b>3.1 鎬讳綋鎶�鏈矾绾�</b></p>
-<p class="p9"><b>椤圭洰鎬讳綋閲囩敤鈥滃Agent鍗忓悓 + PromptOps 鎸佺画浼樺寲 + MCP 鍙帶闆嗘垚 + 浠跨湡瀹¤瀹夊叏闂幆鈥濈殑鎶�鏈矾绾裤�傜郴缁熷垎涓轰氦浜掍笌缂栨帓灞傘�佽兘鍔涗笌鐭ヨ瘑灞傘�侀泦鎴愪笌鎵ц灞傘�佸畨鍏ㄦ不鐞嗗眰鍥涗釜灞傛銆�</b></p>
-<p class="p13"><b>锛�1锛変氦浜掍笌缂栨帓灞傦細鐢辩紪鎺掑櫒鍗忚皟鍦烘櫙璺敱 Agent銆佺煡璇嗘绱� Agent銆佽鍒掑垎瑙� Agent銆佹墽琛� Agent銆佸畨鍏ㄥ鏌� Agent 鍜� PromptOps Agent锛岀粺涓�鎺ユ敹涓氬姟璇锋眰骞惰緭鍑哄彲瑙i噴缁撴灉銆�</b></p>
-<p class="p13"><b>锛�2锛夎兘鍔涗笌鐭ヨ瘑灞傦細寤鸿 Prompt 妯℃澘搴撱�佺煡璇嗗簱銆佽瘎娴嬮泦銆佹棩蹇楀弽棣堝簱鍜岀増鏈簱锛屽疄鐜� Prompt 鍒濆鍖栥�佸鐩樸�佷紭鍖栧拰鐏板害鍙戝竷銆�</b></p>
-<p class="p13"><b>锛�3锛夐泦鎴愪笌鎵ц灞傦細閫氳繃 MCP Server 瀵� WMS銆乄CS銆佹棩蹇楃郴缁熴�佸伐鍗曠郴缁熴�佷豢鐪熺郴缁熺瓑杩涜鏍囧噯鍖栨帴鍏ワ紝缁熶竴灏佽 Resources銆乀ools 鍜� Prompts 鑳藉姏銆�</b></p>
-<p class="p13"><b>锛�4锛夊畨鍏ㄦ不鐞嗗眰锛氬缓绔� OAuth Scope + ABAC 绛栫暐鎺у埗銆佹暟瀛楀鐢熶豢鐪熴�佷汉宸ュ鎵广�佹搷浣滃璁°�佽ˉ鍋垮洖婊氬拰寮傚父鐔旀柇鏈哄埗锛屼繚璇� AI 搴旂敤鑼冨洿鍙帶銆侀闄╁彲杩芥函銆�</b></p>
-<p class="p9"><b>3.2 鏍稿績瀹炴柦鍐呭</b></p>
-<p class="p13"><b>锛�1锛夊Agent 鍗忓悓鑳藉姏鐮斿彂锛氬洿缁� WCS/WMS 鍏稿瀷涓氬姟锛岀爺鍙戝満鏅矾鐢便�佺煡璇嗘绱€�佽鍒掑垎瑙c�佸伐鍏锋墽琛屻�佸畨鍏ㄥ鏌ュ拰澶嶇洏浼樺寲绛夋牳蹇� Agent锛屼娇澶嶆潅浠诲姟浠庘�滃崟娆¢棶绛斺�濆崌绾т负鈥滃垎宸ュ崗浣溿�侀�愭楠岃瘉銆佺粨鏋滃彲瑙i噴鈥濈殑宸ヤ綔娴併��</b></p>
-<p class="p13"><b>锛�2锛塒rompt 妯℃澘浣撶郴涓� PromptOps 寤鸿锛氬缓璁惧満鏅瘑鍒�佷笟鍔$瓥鐣ャ�佽澶囨帶鍒躲�佸紓甯歌瘖鏂�佹姤琛ㄥ鐩樸�佸畨鍏ㄥ悎瑙勭瓑鍏被 Prompt 妯℃澘鏃忥紱寤虹珛鏃ュ織閲囬泦銆佹牱鏈矇娣�銆佺绾胯瘎娴嬨�佸�欓�� Prompt 鐢熸垚銆佺伆搴﹀彂甯冨拰鐗堟湰鍥炴粴鏈哄埗锛屽疄鐜� Prompt 鐨勬寔缁紭鍖栥��</b></p>
-<p class="p13"><b>锛�3锛塎CP 鍙帶闆嗘垚鐮斿彂锛氬鍏徃鐜版湁 WCS/WMS 鐨勫叧閿祫婧愬拰宸ュ叿杩涜灏佽锛屼紭鍏堟帴鍏ュ彧璇荤被鏁版嵁锛屽啀閫愭鎵╁睍鍒� dry-run 鍐欏叆銆佸伐鍗曠敓鎴愩�佹尝娆″缓璁�佸紓甯稿缃缓璁瓑宸ュ叿锛屾渶缁堝舰鎴愮粺涓�鐨� AI 璋冪敤鎺ュ彛灞傘��</b></p>
-<p class="p13"><b>锛�4锛夊畨鍏ㄦ帶鍒舵満鍒剁爺鍙戯細瀵逛簬娑夊強绯荤粺鍐欏叆鍜屾帶鍒剁殑鍦烘櫙锛屽己鍒跺紩鍏ユ渶灏忔潈闄愩�佸鎵规祦銆佷豢鐪熷厛琛屻�佺粨鏋滃璁″拰琛ュ伩鏈哄埗锛岄伩鍏� AI 鐩存帴瀵圭敓浜х郴缁熻繘琛屾棤绾︽潫鎿嶄綔銆�</b></p>
-<p class="p13"><b>锛�5锛夋ā鍨嬭瘎娴嬩笌閮ㄧ讲绛栫暐鐮斿彂锛氶�夊彇鏈湴鍙儴缃叉ā鍨嬩笌浜戠妯″瀷杩涜缁熶竴娴嬭瘯锛屽舰鎴愰�傞厤 WCS/WMS 鍦烘櫙鐨勬ā鍨嬮�夊瀷绛栫暐锛屽苟鏍规嵁鏃跺欢銆佹垚鏈�侀闄╃瓑绾у疄鐜版贩鍚堣矾鐢便��</b></p>
-<p class="p9"><b>3.3 闃舵瀹炴柦璁″垝</b></p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p10"><b>闃舵</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>鏃堕棿</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>閲嶇偣浠诲姟</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>闃舵浜や粯</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>绗竴闃舵</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>T0-T3 鏈�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>闇�姹傛⒊鐞嗐�佺煡璇嗚祫浜х洏鐐广�佸彧璇昏祫婧愭帴鍏ャ�佸Agent鍘熷瀷鎼缓</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍘熷瀷绯荤粺銆侀鎵� Prompt 妯℃澘銆佺煡璇嗗簱涓庡彧璇� MCP 鎺ュ彛</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>绗簩闃舵</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>T4-T8 鏈�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>PromptOps 寤鸿銆佽瘎娴嬩綋绯诲缓璁俱�乨ry-run 鍐欏叆鑳藉姏銆佸畨鍏ㄥ鎵逛笌瀹¤鏈哄埗鐮斿彂</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>Prompt 鐗堟湰娌荤悊骞冲彴銆佽瘎娴嬫姤鍛娿�佷腑椋庨櫓鍦烘櫙璇曠偣</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>绗笁闃舵</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>T9-T12 鏈�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>灞�閮ㄥ彈鎺ч棴鐜瘯鐐广�佷豢鐪熼獙璇併�佷笟鍔℃寚鏍囬獙璇併�佹垚鏋滃浐鍖栦笌鎺ㄥ箍鍑嗗</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>璇曠偣搴旂敤銆侀」鐩�荤粨鎶ュ憡銆佹帹骞挎柟妗堛�佺煡璇嗕骇鏉冩潗鏂�</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p12"><b></b><br></p>
-<p class="p9"><b>3.4 棣栨壒钀藉湴鍦烘櫙</b></p>
-<p class="p13"><b>1. 娉㈡/浠诲姟鐢熸垚涓庝笅鍙戝缓璁細鎻愬崌璁″垝鏁堢巼锛屽厛浠� dry-run 鏂瑰紡楠岃瘉銆�</b></p>
-<p class="p13"><b>2. 寮傚父鑷瘖鏂笌搴旀�ュ缃缓璁細鍥寸粫鍛婅銆佸崱绠便�佸仠鏈恒�佹嫢鍫电瓑闂杈撳嚭澶勭疆寤鸿鍜岀‘璁ら」銆�</b></p>
-<p class="p13"><b>3. 瀹夊叏鑱旈攣涓庡嵄闄╂搷浣滄嫤鎴細瀵归珮椋庨櫓鎿嶄綔澧炲姞 AI 浜屾鏍¢獙涓庢嫤鎴兘鍔涖��</b></p>
-<p class="p13"><b>4. 宸ュ崟銆佹棩鎶ュ拰澶嶇洏鎶ュ憡鑷姩鐢熸垚锛氶檷浣庝汉宸ユ枃涔︽垚鏈紝褰㈡垚缁忛獙娌夋穩銆�</b></p>
-<p class="p13"><b>5. 鍩硅涓� SOP 鏅鸿兘闂瓟锛氭湇鍔′竴绾夸汉鍛樺拰瀹炴柦浜哄憳鐨勫揩閫熸煡璇笌瀛︿範銆�</b></p>
-<p class="p9"><b>3.5 鍙鎬у垎鏋�</b></p>
-<p class="p9"><b>鍏徃鐜版湁 WCS/WMS 浜у搧銆佹帴鍙d綋绯诲拰椤圭洰缁忛獙涓烘湰椤圭洰鎻愪緵浜嗚壇濂界殑钀藉湴鍩虹銆傞」鐩垵鏈熶互鈥滃彧璇昏緟鍔┾�濆拰鈥渄ry-run 寤鸿鈥濅负涓伙紝涓嶆敼鍙樼幇鏈夋牳蹇冩帶鍒堕�昏緫锛屽彲鍦ㄨ緝浣庨闄╀笅蹇�熼獙璇佷环鍊硷紱涓悗鏈熷啀鍦ㄥ鎵广�佷豢鐪熷拰瀹¤鏈哄埗鎴愮啛鐨勫墠鎻愪笅鎺ㄨ繘鍙楁帶鍐欏叆鍜屽眬閮ㄨ嚜鍔ㄥ寲銆�</b></p>
-<p class="p8"><b>鍥涖�侀鏈熸垚鏋滀笌浠峰��</b></p>
-<p class="p9"><b>4.1 棰勬湡鎴愭灉褰㈠紡</b></p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p10"><b>鎴愭灉绫诲埆</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>棰勬湡鎴愭灉</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>骞冲彴鎴愭灉</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>褰㈡垚 1 濂楅潰鍚� WCS/WMS 鐨勫Agent AI 鏅鸿兘浣撳钩鍙板師鍨�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>闆嗘垚鎴愭灉</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>褰㈡垚 WMS/WCS绛夋爣鍑嗗寲 MCP 璧勬簮鎴栧伐鍏锋帴鍏ヨ兘鍔�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>绠楁硶涓庢不鐞嗘垚鏋�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>褰㈡垚 Prompt 妯℃澘銆�</b><span class="s3"><b>P</b></span><b>romptOps 娴佺▼鍜屾ā鍨嬭瘎娴嬩綋绯�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍦烘櫙鎴愭灉</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>瀹屾垚鍏稿瀷涓氬姟鍦烘櫙璇曠偣楠岃瘉锛屽舰鎴愬彲澶嶇敤瀹炴柦妯℃澘</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鐭ヨ瘑浜ф潈鎴愭灉</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>璁″垝鐢宠杞欢钁椾綔鏉�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>绠$悊鎴愭灉</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>褰㈡垚瀹夊叏绛栫暐銆佸鎵规満鍒躲�佸璁℃満鍒跺拰椤圭洰鎺ㄥ箍瑙勮寖鏂囨。</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p12"><b></b><br></p>
-<p class="p9"><b>4.2 鎶�鏈笌涓氬姟浠峰��</b></p>
-<p class="p13"><b>1. 鎻愬崌浜у搧鏅鸿兘鍖栨按骞筹紝閫氳繃鎶婂Agent銆丳romptOps 鍜� MCP 闆嗘垚鍒板簳灞備骇鍝佽兘鍔涗腑锛屽舰鎴愬叕鍙搁潰鍚戞櫤鑳戒粨鍌ㄩ」鐩殑宸紓鍖栫珵浜夊姏銆�</b></p>
-<p class="p13"><span class="s4"><b>2</b></span><b>. 闄嶄綆缁忛獙渚濊禆涓庡煿璁垚鏈紝鎶婁笓瀹剁粡楠屻�丼OP 鍜屽巻鍙插缃渚嬫矇娣�涓哄彲澶嶇敤鐭ヨ瘑璧勪骇锛岄檷浣庢柊浜哄煿鍏绘垚鏈拰璺ㄩ」鐩鍒舵垚鏈��</b></p>
-<p class="p13"><span class="s4"><b>3</b></span><b>. 寮哄寲瀹夊叏鍙帶鑳藉姏锛岄�氳繃瀹℃壒銆佷豢鐪熴�佸璁″拰琛ュ伩鏈哄埗锛屽皢 AI 浠庘�滀笉鍙帶榛戠鈥濊浆鍙樹负鈥滆竟鐣屾竻鏅般�佽繃绋嬪彲杩芥函鈥濈殑宸ョ▼鑳藉姏銆�</b></p>
-<p class="p13"><span class="s4"><b>4</b></span><b>. 褰㈡垚闆嗗洟鍏辨�ф妧鏈祫浜э紝椤圭洰鎴愭灉鍙悜鏈哄櫒浜鸿皟搴︺�佸畨鍏ㄧ洃鎺с�佸洯鍖烘櫤鑳借繍缁村拰缁胯壊浠撳偍绛夋柟鍚戝鍒舵帹骞裤��</b></p>
-<p class="p14"><b></b><br></p>
-<p class="p9"><b>4.3 椋庨櫓璇勪及涓庡簲瀵�</b></p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p10"><b>椋庨櫓椤�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>椋庨櫓璇存槑</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>搴斿鎺柦</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鎶�鏈泦鎴愰闄�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>WCS/WMS 鎺ュ彛宸紓澶с�佸巻鍙茬郴缁熸爣鍑嗕笉缁熶竴</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>閲囩敤鍒嗗眰閫傞厤鍜屾爣鍑嗗寲灏佽锛屽厛鎺ュ叆鍏辨�ц兘鍔涘啀閫愭鎵╁睍</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏁版嵁璐ㄩ噺椋庨櫓</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>SOP銆佹棩蹇椼�佸伐鍗曟暟鎹瓨鍦ㄧ己澶便�佽繃鏈熸垨鍣0</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛鏁版嵁娓呮礂銆佺増鏈帶鍒跺拰浜哄伐鎶芥鏈哄埗</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>瀹夊叏椋庨櫓</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>楂橀闄╂搷浣滃瓨鍦ㄨ秺鏉冦�佽璋冪敤銆佹彁绀烘敞鍏ョ瓑闂</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寮哄埗瀹℃壒銆佷豢鐪熴�丄BAC 绛栫暐銆佸璁$暀鐥曞拰鍥炴粴琛ュ伩</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>妯″瀷鏁堟灉娉㈠姩椋庨櫓</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>妯″瀷鍦ㄤ笉鍚屽満鏅笅绋冲畾鎬у拰鎴愭湰宸紓杈冨ぇ</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>寤虹珛缁熶竴璇勬祴闆嗭紝閲囩敤鏈湴妯″瀷涓庝簯妯″瀷娣峰悎璺敱</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鎺ㄥ箍搴旂敤椋庨櫓</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>涓�绾夸汉鍛樹娇鐢ㄤ範鎯拰淇′换搴﹂渶瑕佸煿鍏�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>閫氳繃鍙杈呭姪鍏堣惤鍦帮紝閫愭鎵╁ぇ搴旂敤杈圭晫锛岄厤濂楀煿璁笌澶嶇洏</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p8"><b>浜斻�佽祫婧愰渶姹傚垵姝ヤ及绠�</b></p>
-<p class="p9"><b>5.1 鎵�闇�宸ユ椂澶╂暟浼扮畻</b></p>
-<p class="p15"><b>鎸夐」鐩缓璁懆鏈� 12 涓湀銆佹湀鍧� 22 涓伐浣滄棩娴嬬畻锛�1.0 FTE 绾︽姌绠椾负 264 浜哄ぉ锛屼互涓嬩负椤圭洰鏍稿績瑙掕壊鎵�闇�宸ユ椂浼扮畻銆�</b></p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p10"><b>瑙掕壊</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鎵�闇�宸ユ椂锛堜汉澶╋級</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>涓昏鑱岃矗</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>椤圭洰璐熻矗浜�/鏋舵瀯甯�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>264</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鎬讳綋鏂规璁捐銆佸叧閿矾寰勬帹杩涖�佽法鍥㈤槦鍗忚皟</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>AI绠楁硶涓庢櫤鑳戒綋鐮斿彂</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>396</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>澶欰gent銆丳romptOps銆佽瘎娴嬩綋绯讳笌妯″瀷璺敱鐮斿彂</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>WCS鍚庣鐮斿彂</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>264</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>WCS 渚ф帴鍙e皝瑁呫�佹帶鍒跺伐鍏枫�佸璁″拰浠跨湡鑱斿姩</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>WMS鍚庣鐮斿彂</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>264</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>WMS 渚ц祫婧愪笌宸ュ叿鎺ュ叆銆佷笟鍔$瓥鐣ヨ仈鍔�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍓嶇涓庝氦浜掔爺鍙�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>132</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏅鸿兘浣撴帶鍒跺彴銆佺粨鏋滃睍绀轰笌瀹℃壒浜や簰</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>娴嬭瘯涓庤繍缁�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>132</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>娴嬭瘯鐢ㄤ緥銆佺幆澧冮儴缃层�佺洃鎺у憡璀︺�佸洖褰掗獙璇�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>涓氬姟涓撳/鐜板満椤鹃棶</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>132</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍦烘櫙瀹氫箟銆佹晥鏋滆瘎瀹°�佽瘯鐐规帹杩�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍚堣</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>1584</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鎸� 12 涓湀銆�22 涓伐浣滄棩/鏈堜及绠楋紝鏍稿績鍥㈤槦鍗忓悓鎶曞叆</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p12"><b></b><br></p>
-<p class="p9"><b>5.2 鏂规涓�锛氭湰鍦伴儴缃茬‖浠舵垚鏈槑缁�</b></p>
-<p class="p16">鏂规涓�浠呯粺璁℃湰鍦版ā鍨嬮儴缃叉墍闇�鐨勬牳蹇冪‖浠舵垚鏈紝涓嶅惈缃戠粶閰嶅銆佷緵鐢典繚闅溿�佸浠藉瓨鍌ㄣ�佸熀纭�閮ㄧ讲銆佸畨鍏ㄥ姞鍥哄強棰勫璐圭瓑鎵╁睍椤癸紝渚夸簬鍦ㄦ彁妗堜腑鍗曠嫭璇存槑璇曠偣鐜鐨勬渶灏忔姇鍏ヨ妯°��</p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p10"><b>绫诲埆</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>閰嶇疆寤鸿</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>鍗曚环锛堝厓锛�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>鏁伴噺</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>灏忚锛堝厓锛�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>澶囨敞</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>GPU</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>NVIDIA RTX 6000 Ada 48GB</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>53000</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>2</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>106000</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>涓绘帹鐞嗚妭鐐�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏈嶅姟鍣ㄥ簳搴�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>4U GPU鏈嶅姟鍣ㄦ満绠� + 涓绘澘 + 鍐椾綑鐢垫簮</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>28000</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>1</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>28000</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍙� GPU 鏈嶅姟鍣�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>CPU</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>Intel Xeon 涓鍙岃矾</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>9000</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>2</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>18000</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>缂栨帓銆佹绱€�佽皟搴�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍐呭瓨</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>ECC DDR5 RECC 64GB x 4 = 256GB</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>15599</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>4</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>62396</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鎸夊崟鏉� 64GB 浠锋牸娴嬬畻</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>绯荤粺鐩�/鏁版嵁鐩�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>浼佷笟绾� NVMe 3.84TB</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>2800</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>2</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>5600</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>妯″瀷涓庡悜閲忓簱</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>Mac Studio</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>Apple Mac Studio M3 Ultra 96GB/1TB</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>32999</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>1</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>32999</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>楂橀厤鏈湴妯″瀷寮�鍙戜笌婕旂ず鏈�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏈湴瀹㈡埛绔瘯楠屾満</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>AMD Ryzen 9 9950X3D + RTX 5090 + 256GB DDR5 + 2TB SSD</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>50200</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>1</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p17"><span class="s5"><b>5</b></span><b>240</b><span class="s5"><b>0</b></span></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鎸� CPU 5300 + GPU 20600 + 鍐呭瓨 19000 + 涓绘澘 3000 + 鐢垫簮 1500 + 2TB SSD </b><span class="s3"><b>3000</b></span><b>浼扮畻</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>鍚堣</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p18"><b></b><br></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p18"><b></b><br></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p18"><b></b><br></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p17"><b>305395</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>绾� 30.</b><span class="s3"><b>5</b></span><b>涓囧厓</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p12"><b></b><br></p>
-<p class="p9"><b>5.3 鏂规浜岋細涓绘祦浜戠 API 浣跨敤鎴愭湰娴嬬畻</b></p>
-<p class="p9"><b>鏂规浜屼负鍏奸【澶嶆潅鎺ㄧ悊銆佷綆鏃跺欢闂瓟涓庡閮ㄦā鍨嬭兘鍔涜ˉ鍏咃紝椤圭洰璇曠偣鏈熷缓璁鐣欎富娴佷簯绔ぇ妯″瀷 API 棰勭畻銆備互涓嬫祴绠楁寜 1 USD 绾﹀悎 7.00 CNY 浼扮畻锛屼笖浠呯粺璁℃爣鍑嗘枃鏈� Token 璐圭敤锛屼笉鍚仈缃戞悳绱€�佷唬鐮佹墽琛屻�佸悜閲忓瓨鍌ㄣ�佸伐鍏疯皟鐢ㄧ瓑闄勫姞璐圭敤銆�</b></p>
-<p class="p9"><b>娴嬬畻鍙e緞濡備笅锛�</b></p>
-<p class="p13"><b>- 鏈堣姹傞噺锛�20000 娆�</b></p>
-<p class="p13"><b>- 骞冲潎鍗曟杈撳叆锛�8000 Tokens</b></p>
-<p class="p13"><b>- 骞冲潎鍗曟杈撳嚭锛�2000 Tokens</b></p>
-<p class="p13"><b>- 鏈堣緭鍏ユ�婚噺锛�160000000 Tokens</b></p>
-<p class="p13"><b>- 鏈堣緭鍑烘�婚噺锛�40000000 Tokens</b></p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p10"><b>鍘傚晢</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>妯″瀷</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>杈撳叆鍗曚环锛圲SD/鐧句竾Tokens锛�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>杈撳嚭鍗曚环锛圲SD/鐧句竾Tokens锛�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>鏈堣垂鐢紙USD锛�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>鏈堣垂鐢紙CNY锛�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p10"><b>閫傜敤寤鸿</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>OpenAI</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>GPT-5 mini</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>0.25</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>2.00</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>120</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>840</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>甯歌闂瓟銆佽交閲� Agent 缂栨帓銆佹枃鏈敓鎴�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>OpenAI</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>GPT-5.4</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>2.50</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>15.00</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>1000</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>7000</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>澶嶆潅鎺ㄧ悊銆佹柟妗堢敓鎴愩�佸叧閿换鍔″鏍�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>Google</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>Gemini 2.5 Flash</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>0.30</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>2.50</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>148</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>1036</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>楂橀浣庢椂寤跺満鏅�佸疄鏃堕棶绛斻�佹壒閲忓鐞�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>Google</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>Gemini 2.5 Pro</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>1.25</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>10.00</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>600</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>4200</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>澶氭楠ゅ垎鏋愩�侀暱涓婁笅鏂囩悊瑙c�佸鏉傜爺鍙戣緟鍔�</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p9"><b>5.4 鏂规涓�涓庢柟妗堜簩浼樺姡鍔垮垎鏋�</b></p>
-<table cellspacing="0" cellpadding="0" class="t1">
- <tbody>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>瀵规瘮缁村害</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏂规涓�锛氭湰鍦伴儴缃�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏂规浜岋細浜戠 API</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>涓昏浼樺娍</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏁版嵁鐣欏瓨鍦ㄥ唴缃戯紝瀹夊叏杈圭晫鏇存竻鏅帮紱鍐呯綉璋冪敤绋冲畾锛岄�傚悎楂樻晱鎰熸垨寮哄悎瑙勫満鏅紱鍙寜涓氬姟闇�瑕佸畾鍒堕儴缃蹭笌鎺ㄧ悊閾捐矾銆�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏃犻渶涓�娆℃�ч噰璐‖浠讹紝鎸夐噺浠樿垂锛屽垵鏈熸姇鍏ヤ綆锛涘彲蹇�熸帴鍏ラ珮鎬ц兘妯″瀷锛岄�傚悎 PoC 涓庤瘯鐐癸紱寮规�ф墿瀹规柟渚匡紝鍩虹璁炬柦杩愮淮鍘嬪姏杈冨皬銆�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>涓昏鍔e娍</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>涓�娆℃�х‖浠跺拰閮ㄧ讲鎶曞叆楂橈紝寤鸿鍛ㄦ湡杈冮暱锛涙ā鍨嬪崌绾с�佹帹鐞嗕紭鍖栧拰鏁呴殰澶勭悊渚濊禆鍐呴儴鍥㈤槦锛涘鏉傛帹鐞嗚兘鍔涘彈鏈湴绠楀姏涓庢ā鍨嬬増鏈檺鍒躲��</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>瀛樺湪澶栫綉渚濊禆锛岄渶瑕佽瘎浼版暟鎹嚭鍩熴�佽劚鏁忓拰瀹¤瑕佹眰锛涢暱鏈熼珮棰戣皟鐢ㄥ悗绱璐圭敤鍙兘涓婂崌锛涢珮鏁忔劅鎺у埗鍦烘櫙闇�瑕佹洿涓ユ牸鐨勬潈闄愪笌椋庢帶鎺柦銆�</b></p>
- </td>
- </tr>
- <tr>
- <td valign="middle" class="td1">
- <p class="p11"><b>閫傜敤闃舵</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏇撮�傚悎璇曠偣鎴愮啛鍚庛�佽皟鐢ㄨ妯$ǔ瀹氫笖瀵规暟鎹湰鍦板寲鍜岄暱鏈熷彲鎺ф�ц姹傝緝楂樼殑闃舵銆�</b></p>
- </td>
- <td valign="middle" class="td1">
- <p class="p11"><b>鏇撮�傚悎椤圭洰鍒濇湡蹇�熼獙璇佷笟鍔′环鍊笺�佹ā鍨嬫晥鏋滃拰鍦烘櫙杈圭晫鐨勯樁娈点��</b></p>
- </td>
- </tr>
- </tbody>
-</table>
-<p class="p9"><b>5.5 鍒濇湡寤鸿閲囩敤鏂规浜岀殑鍘熷洜</b></p>
-<p class="p15"><b>缁煎悎椤圭洰褰撳墠澶勪簬 PoC 涓庤瘯鐐归獙璇侀樁娈碉紝寤鸿椤圭洰鍒濇湡浼樺厛閲囩敤鏂规浜岋紙浜戠 API锛変綔涓轰富鏂规銆傛牳蹇冨師鍥犲湪浜庢柟妗堜簩鏃犻渶涓�娆℃�ф姇鍏ョ害 30.5 涓囧厓鐨勬湰鍦扮‖浠堕噰璐垚鏈嵆鍙揩閫熷惎鍔ㄩ獙璇侊紱鎸変笂琛ㄦ祴绠楋紝浜戠 API 鏈堝害鎴愭湰绾︿负 840 鍏冭嚦 7000 鍏冿紝鍗充娇鎸夎緝楂樻。妯″瀷鍏ㄥ勾娴嬬畻绾� 8.4 涓囧厓锛屽垵鏈熸�讳綋鎶曞叆浠嶆槑鏄句綆浜庢柟妗堜竴銆�</b></p>
-<p class="p15"><b>姝ゅ锛屾柟妗堜簩鍙洿鎺ヨ幏寰楁洿寮虹殑閫氱敤妯″瀷鑳藉姏鍜屾洿蹇殑鐗堟湰杩唬閫熷害锛屽噺灏戞湇鍔″櫒閲囪喘銆侀儴缃层�侀┍鍔ㄩ�傞厤鍜屾ā鍨嬭繍缁寸瓑鍓嶇疆宸ヤ綔锛屼娇鐮斿彂璧勬簮闆嗕腑鎶曞叆鍒板Agent 缂栨帓銆丳rompt 鑷涔犲拰 WCS/WMS 鍦烘櫙鎵撶(涓�傚緟璇曠偣鍦烘櫙绋冲畾銆佽皟鐢ㄨ妯℃斁澶т笖鏁版嵁鏈湴鍖栬姹傝繘涓�姝ユ槑纭悗锛屽啀璇勪及寮曞叆鏂规涓�鎴栧舰鎴愨�滀簯绔� API 鍏堣楠岃瘉 + 鏈湴閮ㄧ讲鎸夐渶钀藉湴鈥濈殑娣峰悎璺嚎銆�</b></p>
-<p class="p12"><b></b><br></p>
-<p class="p9"><b>5.6 鍛ㄦ湡璁″垝</b></p>
-<p class="p9"><b>椤圭洰寤鸿鍛ㄦ湡涓� 12 涓湀锛屾寜鐓р�淧oC 楠岃瘉 -> Pilot 璇曠偣 -> Production 鍑嗙敓浜ч獙璇佲�濈殑鏂瑰紡鎺ㄨ繘銆傚墠 3 涓湀瀹屾垚鍩虹搴曞骇鍜岄鎵瑰満鏅獙璇侊紱绗� 4 鑷� 8 涓湀瀹屾垚瀹夊叏娌荤悊銆丳romptOps 鍜屼腑椋庨櫓鍦烘櫙璇曠偣锛涚 9 鑷� 12 涓湀瀹屾垚璇曠偣闂幆銆佹寚鏍囧鐩樺拰鎴愭灉鍥哄寲锛岀‘淇濋」鐩湪 1 骞村唴褰㈡垚鍙獙鏀舵垚鏋溿��</b></p>
-</body>
-</html>
--
Gitblit v1.9.1