From 78443e96a6b02853b3f7869ededc459a558dacf0 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期一, 16 三月 2026 12:49:36 +0800
Subject: [PATCH] #
---
src/main/java/com/zy/asrs/domain/path/StationPathProfileConfig.java | 49
src/main/java/com/zy/asrs/service/BasStationPathProfileService.java | 7
src/main/resources/sql/20260313_add_station_path_policy_menu.sql | 71
src/main/webapp/static/js/stationPathPolicy/stationPathPolicy.js | 1303 +++++++++++++++
src/main/java/com/zy/asrs/service/impl/BasStationPathRuleServiceImpl.java | 11
src/main/java/com/zy/asrs/entity/BasStationPathRule.java | 61
src/main/java/com/zy/asrs/domain/path/StationPathResolvedPolicy.java | 27
src/main/java/com/zy/common/utils/NavigateUtils.java | 450 +++++
tmp/docs/wcs_wms_plan_check.html | 922 ++++++++++
src/main/java/com/zy/asrs/mapper/BasStationPathRuleMapper.java | 11
src/main/java/com/zy/asrs/entity/BasStationPathProfile.java | 40
src/main/webapp/views/stationPathPolicy/stationPathPolicy.html | 1300 +++++++++++++++
src/main/java/com/zy/asrs/service/BasStationPathRuleService.java | 7
src/main/java/com/zy/asrs/service/impl/StationPathPolicyServiceImpl.java | 264 +++
src/main/java/com/zy/asrs/controller/BasStationPathPolicyController.java | 420 ++++
src/main/java/com/zy/asrs/service/StationPathPolicyService.java | 10
src/main/resources/sql/20260313_create_station_path_policy_tables.sql | 65
src/main/java/com/zy/asrs/mapper/BasStationPathProfileMapper.java | 11
src/main/java/com/zy/asrs/domain/path/StationPathRuleConfig.java | 48
src/main/java/com/zy/asrs/service/impl/BasStationPathProfileServiceImpl.java | 11
20 files changed, 5,086 insertions(+), 2 deletions(-)
diff --git a/src/main/java/com/zy/asrs/controller/BasStationPathPolicyController.java b/src/main/java/com/zy/asrs/controller/BasStationPathPolicyController.java
new file mode 100644
index 0000000..17fffc3
--- /dev/null
+++ b/src/main/java/com/zy/asrs/controller/BasStationPathPolicyController.java
@@ -0,0 +1,420 @@
+package com.zy.asrs.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.annotations.ManagerAuth;
+import com.core.common.R;
+import com.zy.asrs.domain.path.StationPathResolvedPolicy;
+import com.zy.asrs.entity.BasMap;
+import com.zy.asrs.entity.BasStation;
+import com.zy.asrs.entity.BasStationPathProfile;
+import com.zy.asrs.entity.BasStationPathRule;
+import com.zy.asrs.service.BasMapService;
+import com.zy.asrs.service.BasStationService;
+import com.zy.asrs.service.BasStationPathProfileService;
+import com.zy.asrs.service.BasStationPathRuleService;
+import com.zy.asrs.service.StationPathPolicyService;
+import com.zy.common.model.NavigateNode;
+import com.zy.common.utils.NavigateUtils;
+import com.zy.common.utils.RedisUtil;
+import com.zy.common.web.BaseController;
+import com.zy.core.enums.RedisKeyType;
+import com.zy.system.entity.Config;
+import com.zy.system.service.ConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+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.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RestController
+@RequestMapping("/basStationPathPolicy")
+public class BasStationPathPolicyController extends BaseController {
+
+ @Autowired
+ private BasStationPathProfileService basStationPathProfileService;
+ @Autowired
+ private BasStationPathRuleService basStationPathRuleService;
+ @Autowired
+ private ConfigService configService;
+ @Autowired
+ private RedisUtil redisUtil;
+ @Autowired
+ private StationPathPolicyService stationPathPolicyService;
+ @Autowired
+ private BasStationService basStationService;
+ @Autowired
+ private BasMapService basMapService;
+ @Autowired
+ private NavigateUtils navigateUtils;
+
+ @RequestMapping("/data/auth")
+ @ManagerAuth
+ public R data() {
+ Map<String, Object> data = new HashMap<>();
+ data.put("profiles", basStationPathProfileService.list(new QueryWrapper<BasStationPathProfile>().orderByAsc("priority", "id")));
+ data.put("rules", basStationPathRuleService.list(new QueryWrapper<BasStationPathRule>().orderByAsc("priority", "id")));
+ data.put("scoreMode", getSystemConfig("stationPathScoreMode", "legacy"));
+ data.put("defaultProfileCode", getSystemConfig("stationPathDefaultProfileCode", "default"));
+ data.put("stations", buildStationSummaryList());
+ data.put("levList", basMapService.getLevList());
+ return R.ok(data);
+ }
+
+ @RequestMapping("/save/auth")
+ @ManagerAuth
+ public R save(@RequestBody JSONObject payload) {
+ JSONArray profiles = payload.getJSONArray("profiles");
+ JSONArray rules = payload.getJSONArray("rules");
+
+ upsertSystemConfig("绔欑偣璺緞璇勫垎妯″紡", "stationPathScoreMode", defaultIfBlank(payload.getString("scoreMode"), "legacy"), "String");
+ upsertSystemConfig("绔欑偣璺緞榛樿妯℃澘缂栫爜", "stationPathDefaultProfileCode", defaultIfBlank(payload.getString("defaultProfileCode"), "default"), "String");
+
+ basStationPathProfileService.remove(new QueryWrapper<>());
+ basStationPathRuleService.remove(new QueryWrapper<>());
+
+ List<BasStationPathProfile> profileList = new ArrayList<>();
+ if (profiles != null) {
+ for (int i = 0; i < profiles.size(); i++) {
+ JSONObject item = profiles.getJSONObject(i);
+ if (item == null) {
+ continue;
+ }
+ if (isBlank(item.getString("profileCode"))) {
+ continue;
+ }
+ BasStationPathProfile profile = new BasStationPathProfile();
+ profile.setProfileCode(item.getString("profileCode"));
+ profile.setProfileName(defaultIfBlank(item.getString("profileName"), item.getString("profileCode")));
+ profile.setPriority(item.getInteger("priority") == null ? 100 : item.getInteger("priority"));
+ profile.setStatus(item.getShort("status") == null ? (short) 1 : item.getShort("status"));
+ profile.setMemo(item.getString("memo"));
+ Object configObj = item.get("config");
+ if (configObj != null) {
+ profile.setConfigJson(JSON.toJSONString(configObj));
+ } else {
+ profile.setConfigJson(item.getString("configJson"));
+ }
+ profileList.add(profile);
+ }
+ }
+ if (!profileList.isEmpty()) {
+ basStationPathProfileService.saveBatch(profileList);
+ }
+
+ List<BasStationPathRule> ruleList = new ArrayList<>();
+ if (rules != null) {
+ for (int i = 0; i < rules.size(); i++) {
+ JSONObject item = rules.getJSONObject(i);
+ if (item == null) {
+ continue;
+ }
+ if (isBlank(item.getString("ruleCode"))) {
+ continue;
+ }
+ BasStationPathRule rule = new BasStationPathRule();
+ rule.setRuleCode(item.getString("ruleCode"));
+ rule.setRuleName(defaultIfBlank(item.getString("ruleName"), item.getString("ruleCode")));
+ rule.setPriority(item.getInteger("priority") == null ? 100 : item.getInteger("priority"));
+ rule.setStatus(item.getShort("status") == null ? (short) 1 : item.getShort("status"));
+ rule.setSceneType(item.getString("sceneType"));
+ rule.setStartStationId(item.getInteger("startStationId"));
+ rule.setEndStationId(item.getInteger("endStationId"));
+ rule.setProfileCode(item.getString("profileCode"));
+ rule.setMemo(item.getString("memo"));
+ rule.setHardJson(toJson(item.get("hard"), item.getString("hardJson")));
+ rule.setWaypointJson(toJson(item.get("waypoint"), item.getString("waypointJson")));
+ rule.setSoftJson(toJson(item.get("soft"), item.getString("softJson")));
+ rule.setFallbackJson(toJson(item.get("fallback"), item.getString("fallbackJson")));
+ ruleList.add(rule);
+ }
+ }
+ if (!ruleList.isEmpty()) {
+ basStationPathRuleService.saveBatch(ruleList);
+ }
+
+ refreshSystemConfigCache();
+ stationPathPolicyService.evictCache();
+ return R.ok();
+ }
+
+ @RequestMapping("/resolve/auth")
+ @ManagerAuth
+ public R resolve(@RequestParam Integer startStationId, @RequestParam Integer endStationId) {
+ StationPathResolvedPolicy resolved = stationPathPolicyService.resolvePolicy(startStationId, endStationId);
+ return R.ok(resolved);
+ }
+
+ @RequestMapping("/preview/auth")
+ @ManagerAuth
+ public R preview(@RequestParam Integer startStationId,
+ @RequestParam Integer endStationId,
+ @RequestParam(required = false, defaultValue = "false") Boolean includeMapData) {
+ StationPathResolvedPolicy resolved = stationPathPolicyService.resolvePolicy(startStationId, endStationId);
+ List<NavigateNode> nodes = navigateUtils.calcByStationId(startStationId, endStationId);
+ List<Integer> stationIdList = new ArrayList<>();
+ List<Map<String, Object>> nodeList = new ArrayList<>();
+ Set<Integer> seen = new HashSet<>();
+ for (NavigateNode node : nodes) {
+ JSONObject value = parseNodeValue(node == null ? null : node.getNodeValue());
+ Integer stationId = value == null ? null : value.getInteger("stationId");
+ if (stationId != null && seen.add(stationId)) {
+ stationIdList.add(stationId);
+ }
+ Map<String, Object> item = new HashMap<>();
+ item.put("stationId", stationId);
+ item.put("x", node == null ? null : node.getX());
+ item.put("y", node == null ? null : node.getY());
+ item.put("direction", node == null ? null : node.getDirection());
+ item.put("isInflectionPoint", node != null && Boolean.TRUE.equals(node.getIsInflectionPoint()));
+ item.put("isLiftTransferPoint", node != null && Boolean.TRUE.equals(node.getIsLiftTransferPoint()));
+ nodeList.add(item);
+ }
+
+ BasStation startStation = basStationService.getById(startStationId);
+ Integer lev = startStation == null ? null : startStation.getStationLev();
+ BasMap basMap = Boolean.TRUE.equals(includeMapData) && lev != null
+ ? basMapService.getOne(new QueryWrapper<BasMap>().eq("lev", lev))
+ : null;
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("resolvedPolicy", resolved);
+ result.put("pathStationIds", stationIdList);
+ result.put("pathNodes", nodeList);
+ result.put("pathLength", stationIdList.size());
+ result.put("turnCount", countTurnCount(nodeList));
+ result.put("liftTransferCount", countLiftTransferCount(nodeList));
+ result.put("lev", lev);
+ result.put("mapData", basMap == null ? null : basMap.getData());
+ result.put("previewTime", new Date());
+ return R.ok(result);
+ }
+
+ @RequestMapping("/expandSoftPath/auth")
+ @ManagerAuth
+ public R expandSoftPath(@RequestBody JSONObject payload) {
+ Integer startStationId = payload.getInteger("startStationId");
+ Integer endStationId = payload.getInteger("endStationId");
+ if (startStationId == null || endStationId == null) {
+ return R.error("璧风偣鍜岀粓鐐逛笉鑳戒负绌�");
+ }
+
+ List<Integer> routePoints = new ArrayList<>();
+ routePoints.add(startStationId);
+ routePoints.addAll(parseStationIdArray(payload.getJSONArray("keyStations")));
+ routePoints.add(endStationId);
+
+ List<Integer> fullPathStationIds = new ArrayList<>();
+ for (int i = 0; i < routePoints.size() - 1; i++) {
+ Integer segmentStart = routePoints.get(i);
+ Integer segmentEnd = routePoints.get(i + 1);
+ if (segmentStart == null || segmentEnd == null) {
+ continue;
+ }
+ if (segmentStart.equals(segmentEnd)) {
+ if (fullPathStationIds.isEmpty()) {
+ fullPathStationIds.add(segmentStart);
+ }
+ continue;
+ }
+ List<NavigateNode> segmentNodes = navigateUtils.calcByStationId(segmentStart, segmentEnd);
+ List<Integer> segmentStationIds = extractStationIds(segmentNodes);
+ if (segmentStationIds.isEmpty()) {
+ return R.error("鏈壘鍒� " + segmentStart + " 鍒� " + segmentEnd + " 鐨勫彲琛岃矾寰�");
+ }
+ appendSegmentPath(fullPathStationIds, segmentStationIds);
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("startStationId", startStationId);
+ result.put("endStationId", endStationId);
+ result.put("keyStations", routePoints.size() <= 2 ? new ArrayList<>() : routePoints.subList(1, routePoints.size() - 1));
+ result.put("pathStationIds", fullPathStationIds);
+ result.put("pathLength", fullPathStationIds.size());
+ result.put("segmentCount", Math.max(routePoints.size() - 1, 0));
+ result.put("previewTime", new Date());
+ return R.ok(result);
+ }
+
+ private List<Map<String, Object>> buildStationSummaryList() {
+ List<Map<String, Object>> result = new ArrayList<>();
+ List<BasStation> stationList = basStationService.list(new QueryWrapper<BasStation>()
+ .eq("status", 1)
+ .orderByAsc("station_lev", "station_id"));
+ for (BasStation station : stationList) {
+ if (station == null || station.getStationId() == null) {
+ continue;
+ }
+ Map<String, Object> item = new HashMap<>();
+ item.put("stationId", station.getStationId());
+ item.put("stationLev", station.getStationLev());
+ item.put("stationAlias", station.getStationAlias());
+ result.add(item);
+ }
+ return result;
+ }
+
+ private void upsertSystemConfig(String name, String code, String value, String selectType) {
+ String finalValue = value == null ? "" : value.trim();
+ Config config = configService.getOne(new QueryWrapper<Config>().eq("code", code));
+ if (config == null) {
+ config = new Config(name, code, finalValue, (short) 1, (short) 1);
+ config.setSelectType(selectType);
+ configService.save(config);
+ } else {
+ config.setName(name);
+ config.setValue(finalValue);
+ config.setType((short) 1);
+ config.setStatus((short) 1);
+ config.setSelectType(selectType);
+ configService.updateById(config);
+ }
+ }
+
+ private void refreshSystemConfigCache() {
+ HashMap<String, String> systemConfigMap = new HashMap<>();
+ List<Config> configList = configService.list(new QueryWrapper<>());
+ for (Config config : configList) {
+ systemConfigMap.put(config.getCode(), config.getValue());
+ }
+ redisUtil.set(RedisKeyType.SYSTEM_CONFIG_MAP.key, systemConfigMap);
+ }
+
+ private String getSystemConfig(String code, String defaultValue) {
+ Object mapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
+ if (mapObj instanceof Map) {
+ Object value = ((Map<?, ?>) mapObj).get(code);
+ if (value != null) {
+ String text = String.valueOf(value).trim();
+ if (!text.isEmpty()) {
+ return text;
+ }
+ }
+ }
+ return defaultValue;
+ }
+
+ private List<Integer> parseStationIdArray(JSONArray array) {
+ List<Integer> result = new ArrayList<>();
+ if (array == null || array.isEmpty()) {
+ return result;
+ }
+ Set<Integer> seen = new HashSet<>();
+ for (int i = 0; i < array.size(); i++) {
+ Integer stationId = toInteger(array.get(i));
+ if (stationId != null && seen.add(stationId)) {
+ result.add(stationId);
+ }
+ }
+ return result;
+ }
+
+ private List<Integer> extractStationIds(List<NavigateNode> nodes) {
+ List<Integer> stationIdList = new ArrayList<>();
+ Set<Integer> seen = new HashSet<>();
+ for (NavigateNode node : nodes) {
+ JSONObject value = parseNodeValue(node == null ? null : node.getNodeValue());
+ Integer stationId = value == null ? null : value.getInteger("stationId");
+ if (stationId != null && seen.add(stationId)) {
+ stationIdList.add(stationId);
+ }
+ }
+ return stationIdList;
+ }
+
+ private void appendSegmentPath(List<Integer> fullPathStationIds, List<Integer> segmentStationIds) {
+ if (segmentStationIds == null || segmentStationIds.isEmpty()) {
+ return;
+ }
+ for (Integer stationId : segmentStationIds) {
+ if (stationId == null) {
+ continue;
+ }
+ if (fullPathStationIds.isEmpty() || !stationId.equals(fullPathStationIds.get(fullPathStationIds.size() - 1))) {
+ fullPathStationIds.add(stationId);
+ }
+ }
+ }
+
+ private String toJson(Object obj, String fallbackText) {
+ if (obj != null) {
+ return JSON.toJSONString(obj);
+ }
+ return fallbackText;
+ }
+
+ private JSONObject parseNodeValue(String text) {
+ if (isBlank(text)) {
+ return null;
+ }
+ try {
+ return JSON.parseObject(text);
+ } catch (Exception ignore) {
+ return null;
+ }
+ }
+
+ private int countTurnCount(List<Map<String, Object>> nodeList) {
+ if (nodeList == null || nodeList.size() < 3) {
+ return 0;
+ }
+ int count = 0;
+ for (int i = 1; i < nodeList.size() - 1; i++) {
+ Map<String, Object> prev = nodeList.get(i - 1);
+ Map<String, Object> next = nodeList.get(i + 1);
+ Integer prevX = toInteger(prev.get("x"));
+ Integer prevY = toInteger(prev.get("y"));
+ Integer nextX = toInteger(next.get("x"));
+ Integer nextY = toInteger(next.get("y"));
+ if (prevX == null || prevY == null || nextX == null || nextY == null) {
+ continue;
+ }
+ if (!prevX.equals(nextX) && !prevY.equals(nextY)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private int countLiftTransferCount(List<Map<String, Object>> nodeList) {
+ int count = 0;
+ for (Map<String, Object> item : nodeList) {
+ if (Boolean.TRUE.equals(item.get("isLiftTransferPoint"))) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private Integer toInteger(Object value) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Integer) {
+ return (Integer) value;
+ }
+ try {
+ return Integer.parseInt(String.valueOf(value));
+ } catch (Exception ignore) {
+ return null;
+ }
+ }
+
+ private String defaultIfBlank(String text, String defaultValue) {
+ return isBlank(text) ? defaultValue : text.trim();
+ }
+
+ private boolean isBlank(String text) {
+ return text == null || text.trim().isEmpty();
+ }
+}
diff --git a/src/main/java/com/zy/asrs/domain/path/StationPathProfileConfig.java b/src/main/java/com/zy/asrs/domain/path/StationPathProfileConfig.java
new file mode 100644
index 0000000..bfc066c
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/path/StationPathProfileConfig.java
@@ -0,0 +1,49 @@
+package com.zy.asrs.domain.path;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class StationPathProfileConfig implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private Integer calcMaxDepth = 120;
+ private Integer calcMaxPaths = 500;
+ private Integer calcMaxCost = 300;
+
+ private Integer s1TopK = 5;
+ private Double s1LenWeight = 1.0d;
+ private Double s1TurnWeight = 3.0d;
+ private Double s1LiftWeight = 8.0d;
+ private Double s1SoftDeviationWeight = 4.0d;
+ private Double s1MaxLenRatio = 1.15d;
+ private Integer s1MaxTurnDiff = 1;
+
+ private Double s2BusyWeight = 2.0d;
+ private Double s2RunBlockWeight = 10.0d;
+ private Double s2LoopLoadWeight = 12.0d;
+
+ public static StationPathProfileConfig defaultConfig() {
+ return new StationPathProfileConfig();
+ }
+
+ public void mergeFrom(StationPathProfileConfig source) {
+ if (source == null) {
+ return;
+ }
+ if (source.calcMaxDepth != null) this.calcMaxDepth = source.calcMaxDepth;
+ if (source.calcMaxPaths != null) this.calcMaxPaths = source.calcMaxPaths;
+ if (source.calcMaxCost != null) this.calcMaxCost = source.calcMaxCost;
+ if (source.s1TopK != null) this.s1TopK = source.s1TopK;
+ if (source.s1LenWeight != null) this.s1LenWeight = source.s1LenWeight;
+ if (source.s1TurnWeight != null) this.s1TurnWeight = source.s1TurnWeight;
+ if (source.s1LiftWeight != null) this.s1LiftWeight = source.s1LiftWeight;
+ if (source.s1SoftDeviationWeight != null) this.s1SoftDeviationWeight = source.s1SoftDeviationWeight;
+ if (source.s1MaxLenRatio != null) this.s1MaxLenRatio = source.s1MaxLenRatio;
+ if (source.s1MaxTurnDiff != null) this.s1MaxTurnDiff = source.s1MaxTurnDiff;
+ if (source.s2BusyWeight != null) this.s2BusyWeight = source.s2BusyWeight;
+ if (source.s2RunBlockWeight != null) this.s2RunBlockWeight = source.s2RunBlockWeight;
+ if (source.s2LoopLoadWeight != null) this.s2LoopLoadWeight = source.s2LoopLoadWeight;
+ }
+}
diff --git a/src/main/java/com/zy/asrs/domain/path/StationPathResolvedPolicy.java b/src/main/java/com/zy/asrs/domain/path/StationPathResolvedPolicy.java
new file mode 100644
index 0000000..a5d18b9
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/path/StationPathResolvedPolicy.java
@@ -0,0 +1,27 @@
+package com.zy.asrs.domain.path;
+
+import com.zy.asrs.entity.BasStationPathProfile;
+import com.zy.asrs.entity.BasStationPathRule;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class StationPathResolvedPolicy implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private String scoreMode = "legacy";
+ private String defaultProfileCode = "default";
+ private BasStationPathProfile profileEntity;
+ private BasStationPathRule ruleEntity;
+ private StationPathProfileConfig profileConfig = StationPathProfileConfig.defaultConfig();
+ private StationPathRuleConfig ruleConfig = new StationPathRuleConfig();
+
+ public boolean useTwoStage() {
+ return "twoStage".equalsIgnoreCase(scoreMode);
+ }
+
+ public boolean matchedRule() {
+ return ruleEntity != null;
+ }
+}
diff --git a/src/main/java/com/zy/asrs/domain/path/StationPathRuleConfig.java b/src/main/java/com/zy/asrs/domain/path/StationPathRuleConfig.java
new file mode 100644
index 0000000..67533fb
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/path/StationPathRuleConfig.java
@@ -0,0 +1,48 @@
+package com.zy.asrs.domain.path;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class StationPathRuleConfig implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private HardConstraint hard = new HardConstraint();
+ private WaypointConstraint waypoint = new WaypointConstraint();
+ private SoftPreference soft = new SoftPreference();
+ private FallbackPolicy fallback = new FallbackPolicy();
+
+ @Data
+ public static class HardConstraint implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private List<Integer> mustPassStations = new ArrayList<>();
+ private List<Integer> forbidStations = new ArrayList<>();
+ private List<String> mustPassEdges = new ArrayList<>();
+ private List<String> forbidEdges = new ArrayList<>();
+ }
+
+ @Data
+ public static class WaypointConstraint implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private List<Integer> stations = new ArrayList<>();
+ }
+
+ @Data
+ public static class SoftPreference implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private List<Integer> keyStations = new ArrayList<>();
+ private List<Integer> preferredPath = new ArrayList<>();
+ private Double deviationWeight = 6.0d;
+ private Integer maxOffPathCount = 2;
+ }
+
+ @Data
+ public static class FallbackPolicy implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private Boolean strictWaypoint = false;
+ private Boolean allowSoftDegrade = true;
+ }
+}
diff --git a/src/main/java/com/zy/asrs/entity/BasStationPathProfile.java b/src/main/java/com/zy/asrs/entity/BasStationPathProfile.java
new file mode 100644
index 0000000..18a0fd4
--- /dev/null
+++ b/src/main/java/com/zy/asrs/entity/BasStationPathProfile.java
@@ -0,0 +1,40 @@
+package com.zy.asrs.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("asr_bas_station_path_profile")
+public class BasStationPathProfile implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @TableField("profile_code")
+ private String profileCode;
+
+ @TableField("profile_name")
+ private String profileName;
+
+ private Integer priority;
+
+ private Short status;
+
+ @TableField("config_json")
+ private String configJson;
+
+ private String memo;
+
+ @TableField("create_time")
+ private Date createTime;
+
+ @TableField("update_time")
+ private Date updateTime;
+}
diff --git a/src/main/java/com/zy/asrs/entity/BasStationPathRule.java b/src/main/java/com/zy/asrs/entity/BasStationPathRule.java
new file mode 100644
index 0000000..bb047c8
--- /dev/null
+++ b/src/main/java/com/zy/asrs/entity/BasStationPathRule.java
@@ -0,0 +1,61 @@
+package com.zy.asrs.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("asr_bas_station_path_rule")
+public class BasStationPathRule implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @TableField("rule_code")
+ private String ruleCode;
+
+ @TableField("rule_name")
+ private String ruleName;
+
+ private Integer priority;
+
+ private Short status;
+
+ @TableField("scene_type")
+ private String sceneType;
+
+ @TableField("start_station_id")
+ private Integer startStationId;
+
+ @TableField("end_station_id")
+ private Integer endStationId;
+
+ @TableField("profile_code")
+ private String profileCode;
+
+ @TableField("hard_json")
+ private String hardJson;
+
+ @TableField("waypoint_json")
+ private String waypointJson;
+
+ @TableField("soft_json")
+ private String softJson;
+
+ @TableField("fallback_json")
+ private String fallbackJson;
+
+ private String memo;
+
+ @TableField("create_time")
+ private Date createTime;
+
+ @TableField("update_time")
+ private Date updateTime;
+}
diff --git a/src/main/java/com/zy/asrs/mapper/BasStationPathProfileMapper.java b/src/main/java/com/zy/asrs/mapper/BasStationPathProfileMapper.java
new file mode 100644
index 0000000..a5743f6
--- /dev/null
+++ b/src/main/java/com/zy/asrs/mapper/BasStationPathProfileMapper.java
@@ -0,0 +1,11 @@
+package com.zy.asrs.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zy.asrs.entity.BasStationPathProfile;
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface BasStationPathProfileMapper extends BaseMapper<BasStationPathProfile> {
+}
diff --git a/src/main/java/com/zy/asrs/mapper/BasStationPathRuleMapper.java b/src/main/java/com/zy/asrs/mapper/BasStationPathRuleMapper.java
new file mode 100644
index 0000000..6892acb
--- /dev/null
+++ b/src/main/java/com/zy/asrs/mapper/BasStationPathRuleMapper.java
@@ -0,0 +1,11 @@
+package com.zy.asrs.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zy.asrs.entity.BasStationPathRule;
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface BasStationPathRuleMapper extends BaseMapper<BasStationPathRule> {
+}
diff --git a/src/main/java/com/zy/asrs/service/BasStationPathProfileService.java b/src/main/java/com/zy/asrs/service/BasStationPathProfileService.java
new file mode 100644
index 0000000..9a0d788
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/BasStationPathProfileService.java
@@ -0,0 +1,7 @@
+package com.zy.asrs.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zy.asrs.entity.BasStationPathProfile;
+
+public interface BasStationPathProfileService extends IService<BasStationPathProfile> {
+}
diff --git a/src/main/java/com/zy/asrs/service/BasStationPathRuleService.java b/src/main/java/com/zy/asrs/service/BasStationPathRuleService.java
new file mode 100644
index 0000000..67ee6b1
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/BasStationPathRuleService.java
@@ -0,0 +1,7 @@
+package com.zy.asrs.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zy.asrs.entity.BasStationPathRule;
+
+public interface BasStationPathRuleService extends IService<BasStationPathRule> {
+}
diff --git a/src/main/java/com/zy/asrs/service/StationPathPolicyService.java b/src/main/java/com/zy/asrs/service/StationPathPolicyService.java
new file mode 100644
index 0000000..c6097c1
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/StationPathPolicyService.java
@@ -0,0 +1,10 @@
+package com.zy.asrs.service;
+
+import com.zy.asrs.domain.path.StationPathResolvedPolicy;
+
+public interface StationPathPolicyService {
+
+ StationPathResolvedPolicy resolvePolicy(Integer startStationId, Integer endStationId);
+
+ void evictCache();
+}
diff --git a/src/main/java/com/zy/asrs/service/impl/BasStationPathProfileServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/BasStationPathProfileServiceImpl.java
new file mode 100644
index 0000000..4ceb56f
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/impl/BasStationPathProfileServiceImpl.java
@@ -0,0 +1,11 @@
+package com.zy.asrs.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zy.asrs.entity.BasStationPathProfile;
+import com.zy.asrs.mapper.BasStationPathProfileMapper;
+import com.zy.asrs.service.BasStationPathProfileService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class BasStationPathProfileServiceImpl extends ServiceImpl<BasStationPathProfileMapper, BasStationPathProfile> implements BasStationPathProfileService {
+}
diff --git a/src/main/java/com/zy/asrs/service/impl/BasStationPathRuleServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/BasStationPathRuleServiceImpl.java
new file mode 100644
index 0000000..85a1de5
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/impl/BasStationPathRuleServiceImpl.java
@@ -0,0 +1,11 @@
+package com.zy.asrs.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zy.asrs.entity.BasStationPathRule;
+import com.zy.asrs.mapper.BasStationPathRuleMapper;
+import com.zy.asrs.service.BasStationPathRuleService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class BasStationPathRuleServiceImpl extends ServiceImpl<BasStationPathRuleMapper, BasStationPathRule> implements BasStationPathRuleService {
+}
diff --git a/src/main/java/com/zy/asrs/service/impl/StationPathPolicyServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/StationPathPolicyServiceImpl.java
new file mode 100644
index 0000000..2e82a25
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/impl/StationPathPolicyServiceImpl.java
@@ -0,0 +1,264 @@
+package com.zy.asrs.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.zy.asrs.domain.path.StationPathProfileConfig;
+import com.zy.asrs.domain.path.StationPathResolvedPolicy;
+import com.zy.asrs.domain.path.StationPathRuleConfig;
+import com.zy.asrs.entity.BasStationPathProfile;
+import com.zy.asrs.entity.BasStationPathRule;
+import com.zy.asrs.service.BasStationPathProfileService;
+import com.zy.asrs.service.BasStationPathRuleService;
+import com.zy.asrs.service.StationPathPolicyService;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.enums.RedisKeyType;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service("stationPathPolicyService")
+@Slf4j
+public class StationPathPolicyServiceImpl implements StationPathPolicyService {
+ private static final long CACHE_TTL_MILLIS = 5_000L;
+
+ @Autowired
+ private BasStationPathProfileService basStationPathProfileService;
+ @Autowired
+ private BasStationPathRuleService basStationPathRuleService;
+ @Autowired
+ private RedisUtil redisUtil;
+
+ private volatile CacheSnapshot cacheSnapshot = new CacheSnapshot();
+ private volatile long cacheTime = 0L;
+
+ @Override
+ public StationPathResolvedPolicy resolvePolicy(Integer startStationId, Integer endStationId) {
+ StationPathResolvedPolicy resolved = new StationPathResolvedPolicy();
+ resolved.setScoreMode(getSystemConfig("stationPathScoreMode", "legacy"));
+ resolved.setDefaultProfileCode(getSystemConfig("stationPathDefaultProfileCode", "default"));
+
+ CacheSnapshot snapshot = getCacheSnapshot();
+ BasStationPathRule matchedRule = matchRule(snapshot.ruleList, startStationId, endStationId);
+ BasStationPathProfile matchedProfile = null;
+ if (matchedRule != null && notBlank(matchedRule.getProfileCode())) {
+ matchedProfile = snapshot.profileMap.get(matchedRule.getProfileCode());
+ }
+ if (matchedProfile == null && notBlank(resolved.getDefaultProfileCode())) {
+ matchedProfile = snapshot.profileMap.get(resolved.getDefaultProfileCode());
+ }
+ if (matchedProfile == null) {
+ matchedProfile = snapshot.profileMap.get("default");
+ }
+ if (matchedProfile == null && !snapshot.profileList.isEmpty()) {
+ matchedProfile = snapshot.profileList.get(0);
+ }
+
+ resolved.setRuleEntity(matchedRule);
+ resolved.setProfileEntity(matchedProfile);
+ resolved.setProfileConfig(parseProfileConfig(matchedProfile == null ? null : matchedProfile.getConfigJson()));
+ resolved.setRuleConfig(parseRuleConfig(matchedRule));
+ return resolved;
+ }
+
+ @Override
+ public void evictCache() {
+ cacheSnapshot = new CacheSnapshot();
+ cacheTime = 0L;
+ }
+
+ private CacheSnapshot getCacheSnapshot() {
+ long now = System.currentTimeMillis();
+ CacheSnapshot local = cacheSnapshot;
+ if (local.loaded && now - cacheTime < CACHE_TTL_MILLIS) {
+ return local;
+ }
+ synchronized (this) {
+ local = cacheSnapshot;
+ if (local.loaded && now - cacheTime < CACHE_TTL_MILLIS) {
+ return local;
+ }
+ cacheSnapshot = loadSnapshot();
+ cacheTime = System.currentTimeMillis();
+ return cacheSnapshot;
+ }
+ }
+
+ private CacheSnapshot loadSnapshot() {
+ CacheSnapshot snapshot = new CacheSnapshot();
+ try {
+ List<BasStationPathProfile> profiles = basStationPathProfileService.list(new QueryWrapper<BasStationPathProfile>()
+ .eq("status", 1)
+ .orderByAsc("priority", "id"));
+ if (profiles != null) {
+ snapshot.profileList.addAll(profiles);
+ for (BasStationPathProfile profile : profiles) {
+ if (profile != null && notBlank(profile.getProfileCode())) {
+ snapshot.profileMap.put(profile.getProfileCode(), profile);
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.warn("鍔犺浇绔欑偣璺緞妯℃澘澶辫触锛屽洖閫�榛樿閰嶇疆: {}", e.getMessage());
+ }
+
+ try {
+ List<BasStationPathRule> rules = basStationPathRuleService.list(new QueryWrapper<BasStationPathRule>()
+ .eq("status", 1)
+ .orderByAsc("priority", "id"));
+ if (rules != null) {
+ snapshot.ruleList.addAll(rules);
+ }
+ } catch (Exception e) {
+ log.warn("鍔犺浇绔欑偣璺緞瑙勫垯澶辫触锛屽拷鐣ヤ汉宸ヨ鍒�: {}", e.getMessage());
+ }
+
+ snapshot.loaded = true;
+ return snapshot;
+ }
+
+ private BasStationPathRule matchRule(List<BasStationPathRule> ruleList, Integer startStationId, Integer endStationId) {
+ if (ruleList == null || ruleList.isEmpty()) {
+ return null;
+ }
+
+ List<BasStationPathRule> candidates = new ArrayList<>();
+ for (BasStationPathRule rule : ruleList) {
+ if (rule == null) {
+ continue;
+ }
+ if (!matchNullable(rule.getStartStationId(), startStationId)) {
+ continue;
+ }
+ if (!matchNullable(rule.getEndStationId(), endStationId)) {
+ continue;
+ }
+ candidates.add(rule);
+ }
+ if (candidates.isEmpty()) {
+ return null;
+ }
+
+ candidates.sort(Comparator
+ .comparingInt(this::specificity).reversed()
+ .thenComparingInt(rule -> safeInt(rule.getPriority(), 100))
+ .thenComparingLong(rule -> rule.getId() == null ? Long.MAX_VALUE : rule.getId()));
+ return candidates.get(0);
+ }
+
+ private boolean matchNullable(Integer expected, Integer actual) {
+ return expected == null || expected.equals(actual);
+ }
+
+ private int specificity(BasStationPathRule rule) {
+ int score = 0;
+ if (rule.getStartStationId() != null) {
+ score += 1;
+ }
+ if (rule.getEndStationId() != null) {
+ score += 1;
+ }
+ return score;
+ }
+
+ private StationPathProfileConfig parseProfileConfig(String configJson) {
+ StationPathProfileConfig config = StationPathProfileConfig.defaultConfig();
+ if (!notBlank(configJson)) {
+ return config;
+ }
+ try {
+ StationPathProfileConfig parsed = JSON.parseObject(configJson, StationPathProfileConfig.class);
+ config.mergeFrom(parsed);
+ } catch (Exception e) {
+ log.warn("瑙f瀽绔欑偣璺緞妯℃澘閰嶇疆澶辫触锛屼娇鐢ㄩ粯璁ゅ��: {}", e.getMessage());
+ }
+ return config;
+ }
+
+ private StationPathRuleConfig parseRuleConfig(BasStationPathRule rule) {
+ StationPathRuleConfig config = new StationPathRuleConfig();
+ if (rule == null) {
+ return config;
+ }
+
+ try {
+ if (notBlank(rule.getHardJson())) {
+ StationPathRuleConfig.HardConstraint hard = JSON.parseObject(rule.getHardJson(), StationPathRuleConfig.HardConstraint.class);
+ if (hard != null) {
+ config.setHard(hard);
+ }
+ }
+ } catch (Exception e) {
+ log.warn("瑙f瀽纭害鏉熼厤缃け璐�, ruleCode={}", rule.getRuleCode());
+ }
+ try {
+ if (notBlank(rule.getWaypointJson())) {
+ StationPathRuleConfig.WaypointConstraint waypoint = JSON.parseObject(rule.getWaypointJson(), StationPathRuleConfig.WaypointConstraint.class);
+ if (waypoint != null) {
+ config.setWaypoint(waypoint);
+ }
+ }
+ } catch (Exception e) {
+ log.warn("瑙f瀽閫旂粡鐐归厤缃け璐�, ruleCode={}", rule.getRuleCode());
+ }
+ try {
+ if (notBlank(rule.getSoftJson())) {
+ StationPathRuleConfig.SoftPreference soft = JSON.parseObject(rule.getSoftJson(), StationPathRuleConfig.SoftPreference.class);
+ if (soft != null) {
+ config.setSoft(soft);
+ }
+ }
+ } catch (Exception e) {
+ log.warn("瑙f瀽杞亸濂介厤缃け璐�, ruleCode={}", rule.getRuleCode());
+ }
+ try {
+ if (notBlank(rule.getFallbackJson())) {
+ StationPathRuleConfig.FallbackPolicy fallback = JSON.parseObject(rule.getFallbackJson(), StationPathRuleConfig.FallbackPolicy.class);
+ if (fallback != null) {
+ config.setFallback(fallback);
+ }
+ }
+ } catch (Exception e) {
+ log.warn("瑙f瀽瑙勫垯闄嶇骇閰嶇疆澶辫触, ruleCode={}", rule.getRuleCode());
+ }
+ return config;
+ }
+
+ private String getSystemConfig(String code, String defaultValue) {
+ try {
+ Object mapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
+ if (mapObj instanceof Map) {
+ Object value = ((Map<?, ?>) mapObj).get(code);
+ if (value != null) {
+ String text = String.valueOf(value).trim();
+ if (!text.isEmpty()) {
+ return text;
+ }
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ return defaultValue;
+ }
+
+ private boolean notBlank(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ private int safeInt(Integer value, int defaultValue) {
+ return value == null ? defaultValue : value;
+ }
+
+ private static class CacheSnapshot {
+ private boolean loaded = false;
+ private final List<BasStationPathProfile> profileList = new ArrayList<>();
+ private final List<BasStationPathRule> ruleList = new ArrayList<>();
+ private final Map<String, BasStationPathProfile> profileMap = new HashMap<>();
+ }
+}
diff --git a/src/main/java/com/zy/common/utils/NavigateUtils.java b/src/main/java/com/zy/common/utils/NavigateUtils.java
index 620f8f7..1529d69 100644
--- a/src/main/java/com/zy/common/utils/NavigateUtils.java
+++ b/src/main/java/com/zy/common/utils/NavigateUtils.java
@@ -8,10 +8,17 @@
import java.util.Map;
import java.util.Set;
+import com.zy.asrs.domain.path.StationPathProfileConfig;
+import com.zy.asrs.domain.path.StationPathResolvedPolicy;
+import com.zy.asrs.domain.path.StationPathRuleConfig;
+import com.zy.asrs.domain.vo.StationCycleCapacityVo;
+import com.zy.asrs.domain.vo.StationCycleLoopVo;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasStation;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.BasStationService;
+import com.zy.asrs.service.StationCycleCapacityService;
+import com.zy.asrs.service.StationPathPolicyService;
import com.zy.core.News;
import com.zy.core.model.StationObjModel;
import org.springframework.beans.factory.annotation.Autowired;
@@ -42,6 +49,10 @@
@Autowired
private BasStationService basStationService;
+ @Autowired
+ private StationPathPolicyService stationPathPolicyService;
+ @Autowired
+ private StationCycleCapacityService stationCycleCapacityService;
public synchronized List<NavigateNode> calcByStationId(Integer startStationId, Integer endStationId) {
BasStation startStation = basStationService.getById(startStationId);
@@ -63,9 +74,17 @@
throw new CoolException("鏈壘鍒拌 缁堢偣 瀵瑰簲鐨勮妭鐐�");
}
+ StationPathResolvedPolicy resolvedPolicy = resolveStationPathPolicy(startStationId, endStationId);
+ StationPathProfileConfig profileConfig = resolvedPolicy.getProfileConfig() == null
+ ? StationPathProfileConfig.defaultConfig()
+ : resolvedPolicy.getProfileConfig();
+
long startTime = System.currentTimeMillis();
News.info("[WCS Debug] 绔欑偣璺緞寮�濮嬭绠�,startStationId={},endStationId={}", startStationId, endStationId);
- List<List<NavigateNode>> allList = navigateSolution.allSimplePaths(stationMap, startNode, endNode, 120, 500, 300);
+ int calcMaxDepth = resolvedPolicy.useTwoStage() ? safeInt(profileConfig.getCalcMaxDepth(), 120) : 120;
+ int calcMaxPaths = resolvedPolicy.useTwoStage() ? safeInt(profileConfig.getCalcMaxPaths(), 500) : 500;
+ int calcMaxCost = resolvedPolicy.useTwoStage() ? safeInt(profileConfig.getCalcMaxCost(), 300) : 300;
+ List<List<NavigateNode>> allList = navigateSolution.allSimplePaths(stationMap, startNode, endNode, calcMaxDepth, calcMaxPaths, calcMaxCost);
if (allList.isEmpty()) {
// throw new CoolException("鏈壘鍒拌璺緞");
return new ArrayList<>();
@@ -74,7 +93,9 @@
startTime = System.currentTimeMillis();
News.info("[WCS Debug] 绔欑偣璺緞鏉冮噸寮�濮嬪垎鏋�,startStationId={},endStationId={}", startStationId, endStationId);
- List<NavigateNode> list = findStationBestPath(allList);
+ List<NavigateNode> list = resolvedPolicy.useTwoStage()
+ ? findStationBestPathTwoStage(allList, resolvedPolicy)
+ : findStationBestPath(allList);
News.info("[WCS Debug] 绔欑偣璺緞鏉冮噸鍒嗘瀽瀹屾垚锛岃�楁椂锛歿}ms", System.currentTimeMillis() - startTime);
//鍘婚噸
@@ -218,6 +239,431 @@
return liftStationList;
}
+ private StationPathResolvedPolicy resolveStationPathPolicy(Integer startStationId, Integer endStationId) {
+ try {
+ if (stationPathPolicyService != null) {
+ StationPathResolvedPolicy resolved = stationPathPolicyService.resolvePolicy(startStationId, endStationId);
+ if (resolved != null) {
+ return resolved;
+ }
+ }
+ } catch (Exception e) {
+ News.warn("绔欑偣璺緞绛栫暐鍔犺浇澶辫触锛屽洖閫� legacy: {}", e.getMessage());
+ }
+ return new StationPathResolvedPolicy();
+ }
+
+ private List<NavigateNode> findStationBestPathTwoStage(List<List<NavigateNode>> allList, StationPathResolvedPolicy resolvedPolicy) {
+ if (allList == null || allList.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ StationPathRuleConfig ruleConfig = resolvedPolicy.getRuleConfig() == null
+ ? new StationPathRuleConfig()
+ : resolvedPolicy.getRuleConfig();
+ StationPathProfileConfig profileConfig = resolvedPolicy.getProfileConfig() == null
+ ? StationPathProfileConfig.defaultConfig()
+ : resolvedPolicy.getProfileConfig();
+
+ List<List<NavigateNode>> filteredCandidates = applyRuleFilters(allList, ruleConfig, true);
+ if (filteredCandidates.isEmpty() && hasWaypoint(ruleConfig) && !strictWaypoint(ruleConfig)) {
+ filteredCandidates = applyRuleFilters(allList, ruleConfig, false);
+ News.info("[WCS Debug] 绔欑偣璺緞瑙勫垯宸查檷绾э紝蹇界暐鍏抽敭閫旂粡鐐圭害鏉熷悗閲嶈瘯");
+ }
+ if (filteredCandidates.isEmpty()) {
+ if (resolvedPolicy.matchedRule()) {
+ News.warn("绔欑偣璺緞瑙勫垯鍛戒腑浣嗘棤鍙璺緞锛宺uleCode={}",
+ resolvedPolicy.getRuleEntity() == null ? "" : resolvedPolicy.getRuleEntity().getRuleCode());
+ return new ArrayList<>();
+ }
+ filteredCandidates = allList;
+ }
+
+ Map<Integer, StationProtocol> statusMap = loadStationStatusMap();
+ Map<Integer, Double> stationLoopLoadMap = loadStationLoopLoadMap();
+ List<PathCandidateMetrics> metricsList = new ArrayList<>();
+ for (List<NavigateNode> path : filteredCandidates) {
+ if (path == null || path.isEmpty()) {
+ continue;
+ }
+ metricsList.add(buildCandidateMetrics(path, statusMap, stationLoopLoadMap, profileConfig, ruleConfig));
+ }
+ if (metricsList.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ metricsList.sort((a, b) -> compareDouble(a.staticCost, b.staticCost, a.turnCount, b.turnCount, a.pathLen, b.pathLen));
+ PathCandidateMetrics preferred = metricsList.get(0);
+ int maxLen = (int) Math.ceil(preferred.pathLen * safeDouble(profileConfig.getS1MaxLenRatio(), 1.15d));
+ int maxTurns = preferred.turnCount + safeInt(profileConfig.getS1MaxTurnDiff(), 1);
+
+ List<PathCandidateMetrics> stage1Selected = new ArrayList<>();
+ for (PathCandidateMetrics metrics : metricsList) {
+ if (metrics.pathLen <= maxLen && metrics.turnCount <= maxTurns) {
+ stage1Selected.add(metrics);
+ }
+ }
+ if (stage1Selected.isEmpty()) {
+ stage1Selected.addAll(metricsList);
+ }
+
+ int topK = safeInt(profileConfig.getS1TopK(), 5);
+ if (topK > 0 && stage1Selected.size() > topK) {
+ stage1Selected = new ArrayList<>(stage1Selected.subList(0, topK));
+ }
+
+ stage1Selected.sort((a, b) -> compareDouble(a.dynamicCost, b.dynamicCost, a.pathLen, b.pathLen, a.turnCount, b.turnCount));
+ return stage1Selected.get(0).path;
+ }
+
+ private List<List<NavigateNode>> applyRuleFilters(List<List<NavigateNode>> allList,
+ StationPathRuleConfig ruleConfig,
+ boolean includeWaypoint) {
+ if (allList == null || allList.isEmpty()) {
+ return new ArrayList<>();
+ }
+ if (ruleConfig == null) {
+ return allList;
+ }
+
+ List<List<NavigateNode>> result = new ArrayList<>();
+ for (List<NavigateNode> path : allList) {
+ if (path == null || path.isEmpty()) {
+ continue;
+ }
+ List<Integer> stationIdList = extractStationIdList(path);
+ if (!matchHardConstraint(stationIdList, ruleConfig.getHard())) {
+ continue;
+ }
+ if (includeWaypoint && !matchWaypointConstraint(stationIdList, ruleConfig.getWaypoint())) {
+ continue;
+ }
+ result.add(path);
+ }
+ return result;
+ }
+
+ private boolean matchHardConstraint(List<Integer> stationIdList, StationPathRuleConfig.HardConstraint hard) {
+ if (stationIdList == null) {
+ return false;
+ }
+ if (hard == null) {
+ return true;
+ }
+ Set<Integer> stationIdSet = new HashSet<>(stationIdList);
+ for (Integer stationId : safeList(hard.getMustPassStations())) {
+ if (stationId != null && !stationIdSet.contains(stationId)) {
+ return false;
+ }
+ }
+ for (Integer stationId : safeList(hard.getForbidStations())) {
+ if (stationId != null && stationIdSet.contains(stationId)) {
+ return false;
+ }
+ }
+ for (String edge : safeList(hard.getMustPassEdges())) {
+ if (notBlank(edge) && !containsEdge(stationIdList, edge)) {
+ return false;
+ }
+ }
+ for (String edge : safeList(hard.getForbidEdges())) {
+ if (notBlank(edge) && containsEdge(stationIdList, edge)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean matchWaypointConstraint(List<Integer> stationIdList, StationPathRuleConfig.WaypointConstraint waypoint) {
+ if (waypoint == null || waypoint.getStations() == null || waypoint.getStations().isEmpty()) {
+ return true;
+ }
+ int cursor = 0;
+ for (Integer stationId : stationIdList) {
+ Integer expected = waypoint.getStations().get(cursor);
+ if (expected != null && expected.equals(stationId)) {
+ cursor++;
+ if (cursor >= waypoint.getStations().size()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean hasWaypoint(StationPathRuleConfig ruleConfig) {
+ return ruleConfig != null
+ && ruleConfig.getWaypoint() != null
+ && ruleConfig.getWaypoint().getStations() != null
+ && !ruleConfig.getWaypoint().getStations().isEmpty();
+ }
+
+ private boolean strictWaypoint(StationPathRuleConfig ruleConfig) {
+ return ruleConfig != null
+ && ruleConfig.getFallback() != null
+ && Boolean.TRUE.equals(ruleConfig.getFallback().getStrictWaypoint());
+ }
+
+ private boolean containsEdge(List<Integer> stationIdList, String edgeText) {
+ int[] edge = parseEdge(edgeText);
+ if (edge == null) {
+ return false;
+ }
+ for (int i = 0; i < stationIdList.size() - 1; i++) {
+ Integer current = stationIdList.get(i);
+ Integer next = stationIdList.get(i + 1);
+ if (current != null && next != null && current == edge[0] && next == edge[1]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private int[] parseEdge(String edgeText) {
+ if (!notBlank(edgeText)) {
+ return null;
+ }
+ String normalized = edgeText.replace(" ", "");
+ String[] parts = normalized.split("->");
+ if (parts.length != 2) {
+ return null;
+ }
+ try {
+ return new int[]{Integer.parseInt(parts[0]), Integer.parseInt(parts[1])};
+ } catch (Exception ignore) {
+ return null;
+ }
+ }
+
+ private PathCandidateMetrics buildCandidateMetrics(List<NavigateNode> path,
+ Map<Integer, StationProtocol> statusMap,
+ Map<Integer, Double> stationLoopLoadMap,
+ StationPathProfileConfig profileConfig,
+ StationPathRuleConfig ruleConfig) {
+ PathCandidateMetrics metrics = new PathCandidateMetrics();
+ metrics.path = path;
+ metrics.pathLen = path.size();
+ metrics.turnCount = countTurnCount(path);
+ metrics.liftTransferCount = countLiftTransferCount(path);
+
+ List<Integer> stationIdList = extractStationIdList(path);
+ metrics.busyStationCount = countBusyStationCount(stationIdList, statusMap);
+ metrics.runBlockCount = countRunBlockCount(stationIdList, statusMap);
+ metrics.loopPenalty = calcLoopPenalty(stationIdList, stationLoopLoadMap);
+ metrics.softDeviationCount = calcSoftDeviationCount(stationIdList,
+ ruleConfig == null || ruleConfig.getSoft() == null ? null : ruleConfig.getSoft().getPreferredPath());
+
+ double softDeviationWeight = safeDouble(profileConfig.getS1SoftDeviationWeight(), 4.0d);
+ if (ruleConfig != null && ruleConfig.getSoft() != null && ruleConfig.getSoft().getDeviationWeight() != null) {
+ softDeviationWeight = ruleConfig.getSoft().getDeviationWeight();
+ }
+
+ metrics.staticCost =
+ safeDouble(profileConfig.getS1LenWeight(), 1.0d) * metrics.pathLen
+ + safeDouble(profileConfig.getS1TurnWeight(), 3.0d) * metrics.turnCount
+ + safeDouble(profileConfig.getS1LiftWeight(), 8.0d) * metrics.liftTransferCount
+ + softDeviationWeight * metrics.softDeviationCount;
+
+ metrics.dynamicCost =
+ safeDouble(profileConfig.getS2BusyWeight(), 2.0d) * metrics.busyStationCount
+ + safeDouble(profileConfig.getS2RunBlockWeight(), 10.0d) * metrics.runBlockCount
+ + safeDouble(profileConfig.getS2LoopLoadWeight(), 12.0d) * metrics.loopPenalty;
+ return metrics;
+ }
+
+ private int countTurnCount(List<NavigateNode> path) {
+ if (path == null || path.size() < 3) {
+ return 0;
+ }
+ int count = 0;
+ for (int i = 1; i < path.size() - 1; i++) {
+ NavigateNode prev = path.get(i - 1);
+ NavigateNode next = path.get(i + 1);
+ if (prev == null || next == null) {
+ continue;
+ }
+ if (prev.getX() != next.getX() && prev.getY() != next.getY()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private int countLiftTransferCount(List<NavigateNode> path) {
+ int count = 0;
+ for (NavigateNode node : safeList(path)) {
+ try {
+ JSONObject valueObject = JSON.parseObject(node.getNodeValue());
+ if (valueObject == null) {
+ continue;
+ }
+ Object isLiftTransfer = valueObject.get("isLiftTransfer");
+ if (isLiftTransfer != null) {
+ String text = String.valueOf(isLiftTransfer);
+ if ("1".equals(text) || "true".equalsIgnoreCase(text)) {
+ count++;
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ }
+ return count;
+ }
+
+ private int countBusyStationCount(List<Integer> stationIdList, Map<Integer, StationProtocol> statusMap) {
+ int count = 0;
+ for (Integer stationId : stationIdList) {
+ StationProtocol protocol = statusMap.get(stationId);
+ if (protocol != null && protocol.getTaskNo() != null && protocol.getTaskNo() > 0) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private int countRunBlockCount(List<Integer> stationIdList, Map<Integer, StationProtocol> statusMap) {
+ int count = 0;
+ for (Integer stationId : stationIdList) {
+ StationProtocol protocol = statusMap.get(stationId);
+ if (protocol != null && protocol.isRunBlock()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private double calcLoopPenalty(List<Integer> stationIdList, Map<Integer, Double> stationLoopLoadMap) {
+ double maxLoad = 0.0d;
+ for (Integer stationId : stationIdList) {
+ Double load = stationLoopLoadMap.get(stationId);
+ if (load != null && load > maxLoad) {
+ maxLoad = load;
+ }
+ }
+ return maxLoad;
+ }
+
+ private int calcSoftDeviationCount(List<Integer> stationIdList, List<Integer> preferredPath) {
+ if (preferredPath == null || preferredPath.isEmpty() || stationIdList == null || stationIdList.isEmpty()) {
+ return 0;
+ }
+ Set<Integer> preferredSet = new HashSet<>(preferredPath);
+ int count = 0;
+ for (int i = 1; i < stationIdList.size() - 1; i++) {
+ Integer stationId = stationIdList.get(i);
+ if (stationId != null && !preferredSet.contains(stationId)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private List<Integer> extractStationIdList(List<NavigateNode> path) {
+ List<Integer> stationIdList = new ArrayList<>();
+ Set<Integer> seen = new HashSet<>();
+ for (NavigateNode node : safeList(path)) {
+ Integer stationId = extractStationId(node);
+ if (stationId == null) {
+ continue;
+ }
+ if (seen.add(stationId)) {
+ stationIdList.add(stationId);
+ }
+ }
+ return stationIdList;
+ }
+
+ private Map<Integer, StationProtocol> loadStationStatusMap() {
+ Map<Integer, StationProtocol> statusMap = new HashMap<>();
+ try {
+ DeviceConfigService deviceConfigService = SpringUtils.getBean(DeviceConfigService.class);
+ if (deviceConfigService == null) {
+ return statusMap;
+ }
+ List<DeviceConfig> devpList = deviceConfigService.list(new QueryWrapper<DeviceConfig>()
+ .eq("device_type", String.valueOf(SlaveType.Devp)));
+ for (DeviceConfig deviceConfig : devpList) {
+ StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, deviceConfig.getDeviceNo());
+ if (stationThread == null) {
+ continue;
+ }
+ Map<Integer, StationProtocol> map = stationThread.getStatusMap();
+ if (map != null && !map.isEmpty()) {
+ statusMap.putAll(map);
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ return statusMap;
+ }
+
+ private Map<Integer, Double> loadStationLoopLoadMap() {
+ Map<Integer, Double> stationLoopLoadMap = new HashMap<>();
+ try {
+ if (stationCycleCapacityService == null) {
+ return stationLoopLoadMap;
+ }
+ StationCycleCapacityVo capacityVo = stationCycleCapacityService.getLatestSnapshot();
+ if (capacityVo == null || capacityVo.getLoopList() == null) {
+ return stationLoopLoadMap;
+ }
+ for (StationCycleLoopVo loopVo : capacityVo.getLoopList()) {
+ if (loopVo == null || loopVo.getStationIdList() == null) {
+ continue;
+ }
+ double currentLoad = loopVo.getCurrentLoad() == null ? 0.0d : loopVo.getCurrentLoad();
+ for (Integer stationId : loopVo.getStationIdList()) {
+ if (stationId != null) {
+ stationLoopLoadMap.put(stationId, currentLoad);
+ }
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ return stationLoopLoadMap;
+ }
+
+ private int compareDouble(double left, double right, int thenLeft1, int thenRight1, int thenLeft2, int thenRight2) {
+ int result = Double.compare(left, right);
+ if (result != 0) {
+ return result;
+ }
+ result = Integer.compare(thenLeft1, thenRight1);
+ if (result != 0) {
+ return result;
+ }
+ return Integer.compare(thenLeft2, thenRight2);
+ }
+
+ private int safeInt(Integer value, int defaultValue) {
+ return value == null ? defaultValue : value;
+ }
+
+ private double safeDouble(Double value, double defaultValue) {
+ return value == null ? defaultValue : value;
+ }
+
+ private boolean notBlank(String text) {
+ return text != null && !text.trim().isEmpty();
+ }
+
+ private <T> List<T> safeList(List<T> list) {
+ return list == null ? Collections.emptyList() : list;
+ }
+
+ private static class PathCandidateMetrics {
+ private List<NavigateNode> path;
+ private int pathLen;
+ private int turnCount;
+ private int liftTransferCount;
+ private int busyStationCount;
+ private int runBlockCount;
+ private int softDeviationCount;
+ private double loopPenalty;
+ private double staticCost;
+ private double dynamicCost;
+ }
+
public synchronized List<NavigateNode> findStationBestPath(List<List<NavigateNode>> allList) {
if (allList == null || allList.isEmpty()) {
return new ArrayList<>();
diff --git a/src/main/resources/sql/20260313_add_station_path_policy_menu.sql b/src/main/resources/sql/20260313_add_station_path_policy_menu.sql
new file mode 100644
index 0000000..d6363a6
--- /dev/null
+++ b/src/main/resources/sql/20260313_add_station_path_policy_menu.sql
@@ -0,0 +1,71 @@
+-- 灏� 杈撻�佽矾寰勭瓥鐣ョ鐞� 鑿滃崟鎸傝浇鍒帮細鍩虹璧勬枡锛堜紭鍏堬級鎴栧紑鍙戜笓鐢�
+-- 璇存槑锛氭墽琛屾湰鑴氭湰鍚庯紝璇峰湪鈥滆鑹叉巿鏉冣�濋噷缁欏搴旇鑹插嬀閫夋柊鑿滃崟鍜屸�滄煡鐪嬧�濇潈闄愩��
+
+SET @station_path_policy_parent_id := COALESCE(
+ (
+ SELECT id
+ FROM sys_resource
+ WHERE code = 'base' 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 'stationPathPolicy/stationPathPolicy.html', '杈撻�佽矾寰勭瓥鐣�', @station_path_policy_parent_id, 2, 996, 1
+FROM dual
+WHERE @station_path_policy_parent_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_resource
+ WHERE code = 'stationPathPolicy/stationPathPolicy.html' AND level = 2
+ );
+
+UPDATE sys_resource
+SET name = '杈撻�佽矾寰勭瓥鐣�',
+ resource_id = @station_path_policy_parent_id,
+ level = 2,
+ sort = 996,
+ status = 1
+WHERE code = 'stationPathPolicy/stationPathPolicy.html' AND level = 2;
+
+SET @station_path_policy_id := (
+ SELECT id
+ FROM sys_resource
+ WHERE code = 'stationPathPolicy/stationPathPolicy.html' AND level = 2
+ ORDER BY id
+ LIMIT 1
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'stationPathPolicy/stationPathPolicy.html#view', '鏌ョ湅', @station_path_policy_id, 3, 1, 1
+FROM dual
+WHERE @station_path_policy_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_resource
+ WHERE code = 'stationPathPolicy/stationPathPolicy.html#view' AND level = 3
+ );
+
+UPDATE sys_resource
+SET name = '鏌ョ湅',
+ resource_id = @station_path_policy_id,
+ level = 3,
+ sort = 1,
+ status = 1
+WHERE code = 'stationPathPolicy/stationPathPolicy.html#view' AND level = 3;
+
+SELECT id, code, name, resource_id, level, sort, status
+FROM sys_resource
+WHERE code IN (
+ 'stationPathPolicy/stationPathPolicy.html',
+ 'stationPathPolicy/stationPathPolicy.html#view'
+)
+ORDER BY level, sort, id;
diff --git a/src/main/resources/sql/20260313_create_station_path_policy_tables.sql b/src/main/resources/sql/20260313_create_station_path_policy_tables.sql
new file mode 100644
index 0000000..7ec0595
--- /dev/null
+++ b/src/main/resources/sql/20260313_create_station_path_policy_tables.sql
@@ -0,0 +1,65 @@
+CREATE TABLE IF NOT EXISTS `asr_bas_station_path_profile` (
+ `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+ `profile_code` VARCHAR(64) NOT NULL COMMENT '妯℃澘缂栫爜',
+ `profile_name` VARCHAR(128) NOT NULL COMMENT '妯℃澘鍚嶇О',
+ `priority` INT NOT NULL DEFAULT 100 COMMENT '浼樺厛绾э紝瓒婂皬瓒婁紭鍏�',
+ `status` TINYINT NOT NULL DEFAULT 1 COMMENT '鐘舵�� 1鍚敤 0绂佺敤',
+ `config_json` LONGTEXT NULL COMMENT '妯℃澘鍙傛暟JSON',
+ `memo` VARCHAR(255) NULL COMMENT '澶囨敞',
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_asr_bas_station_path_profile_code` (`profile_code`),
+ KEY `idx_asr_bas_station_path_profile_status_priority` (`status`, `priority`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='杈撻�佺珯鐐硅矾寰勮瘎鍒嗘ā鏉�';
+
+CREATE TABLE IF NOT EXISTS `asr_bas_station_path_rule` (
+ `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+ `rule_code` VARCHAR(64) NOT NULL COMMENT '瑙勫垯缂栫爜',
+ `rule_name` VARCHAR(128) NOT NULL COMMENT '瑙勫垯鍚嶇О',
+ `priority` INT NOT NULL DEFAULT 100 COMMENT '浼樺厛绾э紝瓒婂皬瓒婁紭鍏�',
+ `status` TINYINT NOT NULL DEFAULT 1 COMMENT '鐘舵�� 1鍚敤 0绂佺敤',
+ `scene_type` VARCHAR(32) NULL COMMENT '鍦烘櫙绫诲瀷',
+ `start_station_id` INT NULL COMMENT '璧风偣绔欑偣ID锛岀┖琛ㄧず閫氶厤',
+ `end_station_id` INT NULL COMMENT '缁堢偣绔欑偣ID锛岀┖琛ㄧず閫氶厤',
+ `profile_code` VARCHAR(64) NULL COMMENT '缁戝畾妯℃澘缂栫爜',
+ `hard_json` LONGTEXT NULL COMMENT '纭害鏉烰SON',
+ `waypoint_json` LONGTEXT NULL COMMENT '鍏抽敭閫旂粡鐐笿SON',
+ `soft_json` LONGTEXT NULL COMMENT '杞亸濂絁SON',
+ `fallback_json` LONGTEXT NULL COMMENT '闄嶇骇绛栫暐JSON',
+ `memo` VARCHAR(255) NULL COMMENT '澶囨敞',
+ `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_asr_bas_station_path_rule_code` (`rule_code`),
+ KEY `idx_asr_bas_station_path_rule_match` (`status`, `start_station_id`, `end_station_id`, `priority`),
+ KEY `idx_asr_bas_station_path_rule_profile` (`profile_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='杈撻�佺珯鐐硅矾寰勪汉宸ヨ鍒�';
+
+INSERT INTO `asr_bas_station_path_profile`
+(`profile_code`, `profile_name`, `priority`, `status`, `config_json`, `memo`)
+SELECT
+ 'default',
+ '榛樿涓ら樁娈佃瘎鍒嗘ā鏉�',
+ 100,
+ 1,
+ '{"calcMaxDepth":120,"calcMaxPaths":500,"calcMaxCost":300,"s1TopK":5,"s1LenWeight":1.0,"s1TurnWeight":3.0,"s1LiftWeight":8.0,"s1SoftDeviationWeight":4.0,"s1MaxLenRatio":1.15,"s1MaxTurnDiff":1,"s2BusyWeight":2.0,"s2RunBlockWeight":10.0,"s2LoopLoadWeight":12.0}',
+ '榛樿妯℃澘锛屾湭鍛戒腑瑙勫垯鏃跺厹搴�'
+FROM dual
+WHERE NOT EXISTS (
+ SELECT 1 FROM `asr_bas_station_path_profile` WHERE `profile_code` = 'default'
+);
+
+INSERT INTO `sys_config`(`name`, `code`, `value`, `type`, `status`, `select_type`)
+SELECT '绔欑偣璺緞璇勫垎妯″紡', 'stationPathScoreMode', 'legacy', 1, 1, 'String'
+FROM dual
+WHERE NOT EXISTS (
+ SELECT 1 FROM `sys_config` WHERE `code` = 'stationPathScoreMode'
+);
+
+INSERT INTO `sys_config`(`name`, `code`, `value`, `type`, `status`, `select_type`)
+SELECT '绔欑偣璺緞榛樿妯℃澘缂栫爜', 'stationPathDefaultProfileCode', 'default', 1, 1, 'String'
+FROM dual
+WHERE NOT EXISTS (
+ SELECT 1 FROM `sys_config` WHERE `code` = 'stationPathDefaultProfileCode'
+);
diff --git a/src/main/webapp/static/js/stationPathPolicy/stationPathPolicy.js b/src/main/webapp/static/js/stationPathPolicy/stationPathPolicy.js
new file mode 100644
index 0000000..eb9db67
--- /dev/null
+++ b/src/main/webapp/static/js/stationPathPolicy/stationPathPolicy.js
@@ -0,0 +1,1303 @@
+function createDefaultProfileConfig() {
+ return {
+ calcMaxDepth: 120,
+ calcMaxPaths: 500,
+ calcMaxCost: 300,
+ s1TopK: 5,
+ s1LenWeight: 1.0,
+ s1TurnWeight: 3.0,
+ s1LiftWeight: 8.0,
+ s1SoftDeviationWeight: 4.0,
+ s1MaxLenRatio: 1.15,
+ s1MaxTurnDiff: 1,
+ s2BusyWeight: 2.0,
+ s2RunBlockWeight: 10.0,
+ s2LoopLoadWeight: 12.0
+ }
+}
+
+function createDefaultProfile() {
+ return {
+ id: null,
+ profileCode: '',
+ profileName: '',
+ priority: 100,
+ status: 1,
+ memo: '',
+ config: createDefaultProfileConfig(),
+ _originCode: null
+ }
+}
+
+function createDefaultRule(defaultProfileCode) {
+ return {
+ id: null,
+ ruleCode: '',
+ ruleName: '',
+ priority: 100,
+ status: 1,
+ sceneType: 'station',
+ startStationId: null,
+ endStationId: null,
+ profileCode: defaultProfileCode || 'default',
+ memo: '',
+ hard: {
+ mustPassStations: [],
+ forbidStations: [],
+ mustPassEdges: [],
+ forbidEdges: [],
+ mustPassEdgesText: '',
+ forbidEdgesText: ''
+ },
+ waypoint: {
+ stations: []
+ },
+ soft: {
+ keyStations: [],
+ preferredPath: [],
+ deviationWeight: 6.0,
+ maxOffPathCount: 2
+ },
+ fallback: {
+ strictWaypoint: false,
+ allowSoftDegrade: true
+ },
+ _originCode: null
+ }
+}
+
+var app = new Vue({
+ el: '#app',
+ data: function () {
+ return {
+ loading: false,
+ saving: false,
+ scoreMode: 'legacy',
+ defaultProfileCode: 'default',
+ profiles: [],
+ rules: [],
+ stations: [],
+ levList: [],
+ selectedProfileCode: '',
+ selectedRuleCode: '',
+ profileDialogVisible: false,
+ ruleDialogVisible: false,
+ profileForm: createDefaultProfile(),
+ ruleForm: createDefaultRule('default'),
+ previewForm: {
+ startStationId: null,
+ endStationId: null
+ },
+ previewLoading: false,
+ previewResult: null,
+ activeMapLev: null,
+ mapContext: {
+ lev: null,
+ width: 0,
+ height: 0,
+ nodes: [],
+ nodeMap: {}
+ },
+ mapZoomPercent: 100,
+ pickedStationId: null,
+ showRuleJson: false,
+ showAllPathTags: false,
+ softExpandLoading: false,
+ mapDragActive: false,
+ mapDragMoved: false,
+ mapDragStartX: 0,
+ mapDragStartY: 0,
+ mapDragOriginPanX: 0,
+ mapDragOriginPanY: 0,
+ suppressNodeClick: false,
+ mapPanX: 20,
+ mapPanY: 20
+ }
+ },
+ computed: {
+ pickedStation: function () {
+ return this.findStation(this.pickedStationId)
+ },
+ hasPickedStation: function () {
+ return this.pickedStationId != null
+ },
+ stationMapById: function () {
+ var map = {}
+ ;(this.stations || []).forEach(function (station) {
+ if (station && station.stationId != null) {
+ map[String(station.stationId)] = station
+ }
+ })
+ return map
+ },
+ stationOptions: function () {
+ return (this.stations || []).map(function (station) {
+ return {
+ stationId: station.stationId,
+ label: this.stationOptionLabel(station)
+ }
+ }.bind(this))
+ },
+ selectedRule: function () {
+ var code = this.selectedRuleCode
+ if (!code) {
+ return null
+ }
+ for (var i = 0; i < this.rules.length; i++) {
+ if (this.rules[i].ruleCode === code) {
+ return this.rules[i]
+ }
+ }
+ return null
+ },
+ activeRuleForVisual: function () {
+ if (this.ruleDialogVisible && this.ruleForm) {
+ return this.ruleForm
+ }
+ if (this.selectedRule) {
+ return this.selectedRule
+ }
+ if (this.previewResult && this.previewResult.resolvedPolicy && this.previewResult.resolvedPolicy.ruleConfig) {
+ return {
+ startStationId: this.previewForm.startStationId,
+ endStationId: this.previewForm.endStationId,
+ hard: this.previewResult.resolvedPolicy.ruleConfig.hard || this.defaultRule().hard,
+ waypoint: this.previewResult.resolvedPolicy.ruleConfig.waypoint || this.defaultRule().waypoint,
+ soft: this.previewResult.resolvedPolicy.ruleConfig.soft || this.defaultRule().soft,
+ fallback: this.previewResult.resolvedPolicy.ruleConfig.fallback || this.defaultRule().fallback
+ }
+ }
+ return null
+ },
+ previewPathTags: function () {
+ var result = this.previewResult
+ if (!result || !result.pathStationIds) {
+ return []
+ }
+ return result.pathStationIds.map(function (stationId) {
+ return {
+ stationId: stationId,
+ label: this.stationLabel(stationId)
+ }
+ }.bind(this))
+ },
+ visiblePreviewPathTags: function () {
+ if (this.showAllPathTags) {
+ return this.previewPathTags
+ }
+ return this.previewPathTags.slice(0, 14)
+ },
+ hiddenPathTagCount: function () {
+ return Math.max(this.previewPathTags.length - this.visiblePreviewPathTags.length, 0)
+ },
+ hasActualPath: function () {
+ return !!this.actualPathPolyline
+ },
+ hasPreviewPath: function () {
+ return !!(this.previewResult && this.previewResult.pathStationIds && this.previewResult.pathStationIds.length)
+ },
+ pathStationLookup: function () {
+ return this.buildLookup(this.previewResult && this.previewResult.pathStationIds)
+ },
+ preferredStationLookup: function () {
+ return this.buildLookup(this.activeRuleForVisual && this.activeRuleForVisual.soft ? this.activeRuleForVisual.soft.preferredPath : [])
+ },
+ waypointStationLookup: function () {
+ return this.buildLookup(this.activeRuleForVisual && this.activeRuleForVisual.waypoint ? this.activeRuleForVisual.waypoint.stations : [])
+ },
+ forbidStationLookup: function () {
+ return this.buildLookup(this.activeRuleForVisual && this.activeRuleForVisual.hard ? this.activeRuleForVisual.hard.forbidStations : [])
+ },
+ mustPassStationLookup: function () {
+ return this.buildLookup(this.activeRuleForVisual && this.activeRuleForVisual.hard ? this.activeRuleForVisual.hard.mustPassStations : [])
+ },
+ renderedMapNodes: function () {
+ var stationMap = this.stationMapById
+ var pickedId = String(this.pickedStationId == null ? '' : this.pickedStationId)
+ var startId = String(this.previewForm.startStationId == null ? '' : this.previewForm.startStationId)
+ var endId = String(this.previewForm.endStationId == null ? '' : this.previewForm.endStationId)
+ var pathLookup = this.pathStationLookup
+ var preferredLookup = this.preferredStationLookup
+ var waypointLookup = this.waypointStationLookup
+ var forbidLookup = this.forbidStationLookup
+ var mustPassLookup = this.mustPassStationLookup
+ return (this.mapContext.nodes || []).map(function (node) {
+ var stationId = node.stationId
+ var key = String(stationId == null ? '' : stationId)
+ var classes = []
+ if (pickedId && pickedId === key) {
+ classes.push('is-picked')
+ }
+ if (startId && startId === key) {
+ classes.push('is-start')
+ }
+ if (endId && endId === key) {
+ classes.push('is-end')
+ }
+ if (pathLookup[key]) {
+ classes.push('is-path')
+ }
+ if (preferredLookup[key]) {
+ classes.push('is-preferred')
+ }
+ if (waypointLookup[key]) {
+ classes.push('is-waypoint')
+ }
+ if (forbidLookup[key]) {
+ classes.push('is-forbid')
+ }
+ if (mustPassLookup[key]) {
+ classes.push('is-must-pass')
+ }
+ var station = stationMap[key]
+ return {
+ stationId: stationId,
+ x: node.x,
+ y: node.y,
+ left: node.x + 'px',
+ top: node.y + 'px',
+ classes: classes,
+ title: station ? this.stationOptionLabel(station) : String(stationId || ''),
+ showLabel: !!(startId === key || endId === key || pathLookup[key] || waypointLookup[key] || forbidLookup[key] || mustPassLookup[key] || pickedId === key)
+ }
+ }.bind(this))
+ },
+ mapStageStyle: function () {
+ return {
+ width: this.mapContext.width + 'px',
+ height: this.mapContext.height + 'px',
+ transform: 'translate(' + this.mapPanX + 'px, ' + this.mapPanY + 'px) scale(' + (this.mapZoomPercent / 100) + ')'
+ }
+ },
+ actualPathPolyline: function () {
+ var stationIds = this.previewResult && this.previewResult.pathStationIds ? this.previewResult.pathStationIds : []
+ return this.buildPolyline(stationIds)
+ },
+ preferredPathPolyline: function () {
+ var rule = this.activeRuleForVisual
+ var preferredPath = rule && rule.soft ? rule.soft.preferredPath : []
+ return this.buildPolyline(preferredPath)
+ },
+ activeRulePreviewJson: function () {
+ if (!this.activeRuleForVisual) {
+ return ''
+ }
+ return JSON.stringify(this.sanitizeRuleForSave(this.activeRuleForVisual), null, 2)
+ },
+ ruleDialogPickedHint: function () {
+ if (!this.ruleDialogVisible) {
+ return ''
+ }
+ if (!this.pickedStation) {
+ return '瑙勫垯寮圭獥寮�鍚椂锛屼粛鍙湪鍙充晶鍦板浘鐐瑰嚮绔欑偣锛屽啀涓�閿姞鍏ュ繀缁�/绂佺敤/閫旂粡/鍋忓ソ銆�'
+ }
+ return '褰撳墠鍦板浘閫変腑锛�' + this.stationOptionLabel(this.pickedStation)
+ }
+ },
+ mounted: function () {
+ this.loadData()
+ },
+ beforeDestroy: function () {
+ this.detachMapDragListeners()
+ },
+ methods: {
+ loadData: function () {
+ var that = this
+ this.loading = true
+ $.ajax({
+ url: baseUrl + '/basStationPathPolicy/data/auth',
+ method: 'GET',
+ headers: { token: localStorage.getItem('token') },
+ success: function (res) {
+ that.loading = false
+ if (res.code !== 200) {
+ that.$message.error('鍔犺浇澶辫触: ' + res.msg)
+ return
+ }
+ var data = res.data || {}
+ that.scoreMode = data.scoreMode || 'legacy'
+ that.defaultProfileCode = data.defaultProfileCode || 'default'
+ that.showRuleJson = false
+ that.showAllPathTags = false
+ that.stations = (data.stations || []).sort(function (a, b) {
+ if ((a.stationLev || 0) !== (b.stationLev || 0)) {
+ return (a.stationLev || 0) - (b.stationLev || 0)
+ }
+ return (a.stationId || 0) - (b.stationId || 0)
+ })
+ that.levList = data.levList || []
+ that.profiles = (data.profiles || []).map(that.normalizeProfile)
+ that.rules = (data.rules || []).map(that.normalizeRule)
+ if (!that.defaultProfileCode && that.profiles.length) {
+ that.defaultProfileCode = that.profiles[0].profileCode
+ }
+ if (!that.selectedProfileCode && that.profiles.length) {
+ that.selectedProfileCode = that.defaultProfileCode || that.profiles[0].profileCode
+ }
+ if (!that.selectedRuleCode && that.rules.length) {
+ that.selectedRuleCode = that.rules[0].ruleCode
+ }
+ if (that.selectedRule) {
+ that.loadMapByRule(that.selectedRule)
+ } else if (that.levList.length && !that.activeMapLev) {
+ that.loadMapByLev(that.levList[0])
+ }
+ },
+ error: function () {
+ that.loading = false
+ that.$message.error('鍔犺浇璇锋眰寮傚父')
+ }
+ })
+ },
+ saveAll: function () {
+ if (!this.profiles.length) {
+ this.$message.warning('鑷冲皯闇�瑕佷繚鐣欎竴涓ā鏉�')
+ return
+ }
+ var hasDefault = this.profiles.some(function (item) {
+ return item.profileCode === this.defaultProfileCode
+ }.bind(this))
+ if (!hasDefault) {
+ this.$message.warning('榛樿妯℃澘缂栫爜娌℃湁瀵瑰簲妯℃澘')
+ return
+ }
+ var payload = {
+ scoreMode: this.scoreMode,
+ defaultProfileCode: this.defaultProfileCode,
+ profiles: this.profiles.map(this.sanitizeProfileForSave),
+ rules: this.rules.map(this.sanitizeRuleForSave)
+ }
+ var that = this
+ this.saving = true
+ $.ajax({
+ url: baseUrl + '/basStationPathPolicy/save/auth',
+ method: 'POST',
+ headers: { token: localStorage.getItem('token') },
+ contentType: 'application/json',
+ data: JSON.stringify(payload),
+ success: function (res) {
+ that.saving = false
+ if (res.code !== 200) {
+ that.$message.error('淇濆瓨澶辫触: ' + res.msg)
+ return
+ }
+ that.$message.success('淇濆瓨鎴愬姛')
+ that.loadData()
+ },
+ error: function () {
+ that.saving = false
+ that.$message.error('淇濆瓨璇锋眰寮傚父')
+ }
+ })
+ },
+ openProfileDialog: function (item) {
+ this.profileForm = item ? this.cloneProfileModel(item) : this.defaultProfile()
+ this.profileDialogVisible = true
+ },
+ confirmProfileDialog: function () {
+ var form = this.profileForm
+ if (this.isBlank(form.profileCode)) {
+ this.$message.warning('妯℃澘缂栫爜涓嶈兘涓虹┖')
+ return
+ }
+ if (this.isBlank(form.profileName)) {
+ this.$message.warning('妯℃澘鍚嶇О涓嶈兘涓虹┖')
+ return
+ }
+ var existsIndex = this.findProfileIndex(form.profileCode)
+ if (existsIndex >= 0 && (!form._originCode || form._originCode !== form.profileCode)) {
+ this.$message.warning('妯℃澘缂栫爜宸插瓨鍦�')
+ return
+ }
+ var profile = this.cloneProfileModel(form)
+ delete profile._originCode
+ if (form._originCode) {
+ var originIndex = this.findProfileIndex(form._originCode)
+ if (originIndex >= 0) {
+ this.$set(this.profiles, originIndex, profile)
+ if (this.defaultProfileCode === form._originCode) {
+ this.defaultProfileCode = profile.profileCode
+ }
+ if (this.selectedProfileCode === form._originCode) {
+ this.selectedProfileCode = profile.profileCode
+ }
+ this.rules.forEach(function (rule) {
+ if (rule.profileCode === form._originCode) {
+ rule.profileCode = profile.profileCode
+ }
+ })
+ }
+ } else {
+ this.profiles.push(profile)
+ this.selectedProfileCode = profile.profileCode
+ if (!this.defaultProfileCode) {
+ this.defaultProfileCode = profile.profileCode
+ }
+ }
+ this.profileDialogVisible = false
+ },
+ cloneProfile: function (item) {
+ var copy = this.cloneProfileModel(item)
+ copy.profileCode = item.profileCode + '_copy'
+ copy.profileName = item.profileName + ' - 鍓湰'
+ copy._originCode = null
+ this.profileForm = copy
+ this.profileDialogVisible = true
+ },
+ removeProfile: function (item) {
+ if (!item) {
+ return
+ }
+ if (this.profiles.length <= 1) {
+ this.$message.warning('鑷冲皯淇濈暀涓�涓ā鏉�')
+ return
+ }
+ var used = this.rules.some(function (rule) {
+ return rule.profileCode === item.profileCode
+ })
+ if (used) {
+ this.$message.warning('璇ユā鏉夸粛琚鍒欏紩鐢紝涓嶈兘鍒犻櫎')
+ return
+ }
+ var that = this
+ this.$confirm('纭鍒犻櫎妯℃澘 ' + item.profileCode + ' 鍚楋紵', '鎻愮ず', { type: 'warning' })
+ .then(function () {
+ that.profiles = that.profiles.filter(function (profile) {
+ return profile.profileCode !== item.profileCode
+ })
+ if (that.defaultProfileCode === item.profileCode) {
+ that.defaultProfileCode = that.profiles[0] ? that.profiles[0].profileCode : ''
+ }
+ if (that.selectedProfileCode === item.profileCode) {
+ that.selectedProfileCode = that.defaultProfileCode
+ }
+ })
+ .catch(function () {})
+ },
+ openRuleDialog: function (item) {
+ this.ruleForm = item ? this.cloneRuleModel(item) : this.defaultRule()
+ this.ruleDialogVisible = true
+ },
+ confirmRuleDialog: function () {
+ var form = this.ruleForm
+ if (this.isBlank(form.ruleCode)) {
+ this.$message.warning('瑙勫垯缂栫爜涓嶈兘涓虹┖')
+ return
+ }
+ if (this.isBlank(form.ruleName)) {
+ this.$message.warning('瑙勫垯鍚嶇О涓嶈兘涓虹┖')
+ return
+ }
+ var existsIndex = this.findRuleIndex(form.ruleCode)
+ if (existsIndex >= 0 && (!form._originCode || form._originCode !== form.ruleCode)) {
+ this.$message.warning('瑙勫垯缂栫爜宸插瓨鍦�')
+ return
+ }
+ var rule = this.cloneRuleModel(form)
+ delete rule._originCode
+ if (form._originCode) {
+ var originIndex = this.findRuleIndex(form._originCode)
+ if (originIndex >= 0) {
+ this.$set(this.rules, originIndex, rule)
+ if (this.selectedRuleCode === form._originCode) {
+ this.selectedRuleCode = rule.ruleCode
+ }
+ }
+ } else {
+ this.rules.push(rule)
+ this.selectedRuleCode = rule.ruleCode
+ }
+ this.ruleDialogVisible = false
+ this.loadMapByRule(rule)
+ },
+ importPreviewPathToRule: function () {
+ if (!this.hasPreviewPath) {
+ this.$message.warning('璇峰厛鍦ㄥ彸渚у畬鎴愪竴娆¤矾寰勯瑙�')
+ return
+ }
+ if (!this.ruleForm || !this.ruleForm.soft) {
+ return
+ }
+ this.ruleForm.soft.preferredPath = (this.previewResult.pathStationIds || []).slice()
+ this.ruleForm.soft.keyStations = []
+ if (!this.ruleForm.startStationId && this.previewForm.startStationId) {
+ this.ruleForm.startStationId = this.previewForm.startStationId
+ }
+ if (!this.ruleForm.endStationId && this.previewForm.endStationId) {
+ this.ruleForm.endStationId = this.previewForm.endStationId
+ }
+ this.$message.success('宸插鍏ュ綋鍓嶉瑙堣矾寰勶紝鍏� ' + this.ruleForm.soft.preferredPath.length + ' 涓珯鐐�')
+ },
+ expandRuleSoftPreferredPath: function () {
+ if (this.softExpandLoading) {
+ return
+ }
+ if (!this.ruleForm || !this.ruleForm.soft) {
+ return
+ }
+ var startStationId = this.toNumberSafe(this.ruleForm.startStationId) || this.toNumberSafe(this.previewForm.startStationId)
+ var endStationId = this.toNumberSafe(this.ruleForm.endStationId) || this.toNumberSafe(this.previewForm.endStationId)
+ if (startStationId == null || endStationId == null) {
+ this.$message.warning('璇峰厛涓鸿鍒欒缃捣鐐瑰拰缁堢偣锛屾垨鍏堝湪鍙充晶棰勮涓�鏉¤矾寰�')
+ return
+ }
+ var keyStations = this.uniqueNumbers((this.ruleForm.soft.keyStations || []).slice())
+ var that = this
+ this.softExpandLoading = true
+ $.ajax({
+ url: baseUrl + '/basStationPathPolicy/expandSoftPath/auth',
+ method: 'POST',
+ headers: { token: localStorage.getItem('token') },
+ contentType: 'application/json',
+ data: JSON.stringify({
+ startStationId: startStationId,
+ endStationId: endStationId,
+ keyStations: keyStations
+ }),
+ success: function (res) {
+ that.softExpandLoading = false
+ if (res.code !== 200) {
+ that.$message.error('灞曞紑澶辫触: ' + res.msg)
+ return
+ }
+ var data = res.data || {}
+ var pathStationIds = data.pathStationIds || []
+ if (!pathStationIds.length) {
+ that.$message.warning('娌℃湁鐢熸垚鍙敤鐨勮蒋鍋忓ソ璺緞')
+ return
+ }
+ that.ruleForm.startStationId = startStationId
+ that.ruleForm.endStationId = endStationId
+ that.ruleForm.soft.keyStations = keyStations
+ that.ruleForm.soft.preferredPath = pathStationIds.slice()
+ that.$message.success(keyStations.length
+ ? '宸叉寜鍏抽敭鐐瑰睍寮�瀹屾暣杞亸濂借矾寰�'
+ : '宸叉寜璧风粓鐐圭敓鎴愬畬鏁磋蒋鍋忓ソ璺緞')
+ },
+ error: function () {
+ that.softExpandLoading = false
+ that.$message.error('灞曞紑杞亸濂借矾寰勮姹傚紓甯�')
+ }
+ })
+ },
+ clearSoftPreferredPath: function () {
+ if (!this.ruleForm || !this.ruleForm.soft) {
+ return
+ }
+ this.ruleForm.soft.keyStations = []
+ this.ruleForm.soft.preferredPath = []
+ },
+ cloneRule: function (item) {
+ var copy = this.cloneRuleModel(item)
+ copy.ruleCode = item.ruleCode + '_copy'
+ copy.ruleName = item.ruleName + ' - 鍓湰'
+ copy._originCode = null
+ this.ruleForm = copy
+ this.ruleDialogVisible = true
+ },
+ removeRule: function (item) {
+ var that = this
+ this.$confirm('纭鍒犻櫎瑙勫垯 ' + item.ruleCode + ' 鍚楋紵', '鎻愮ず', { type: 'warning' })
+ .then(function () {
+ that.rules = that.rules.filter(function (rule) {
+ return rule.ruleCode !== item.ruleCode
+ })
+ if (that.selectedRuleCode === item.ruleCode) {
+ that.selectedRuleCode = that.rules[0] ? that.rules[0].ruleCode : ''
+ }
+ })
+ .catch(function () {})
+ },
+ selectRule: function (item) {
+ this.selectedRuleCode = item.ruleCode
+ this.loadMapByRule(item)
+ },
+ previewRule: function (item) {
+ this.selectRule(item)
+ this.previewForm.startStationId = item.startStationId
+ this.previewForm.endStationId = item.endStationId
+ this.showRuleJson = false
+ this.showAllPathTags = false
+ if (item.startStationId && item.endStationId) {
+ this.loadPreview()
+ }
+ },
+ loadPreview: function () {
+ if (!this.previewForm.startStationId || !this.previewForm.endStationId) {
+ this.$message.warning('璇烽�夋嫨璧风偣鍜岀粓鐐�')
+ return
+ }
+ var that = this
+ this.previewLoading = true
+ $.ajax({
+ url: baseUrl + '/basStationPathPolicy/preview/auth',
+ method: 'GET',
+ headers: { token: localStorage.getItem('token') },
+ data: {
+ startStationId: this.previewForm.startStationId,
+ endStationId: this.previewForm.endStationId
+ },
+ success: function (res) {
+ that.previewLoading = false
+ if (res.code !== 200) {
+ that.$message.error('棰勮澶辫触: ' + res.msg)
+ return
+ }
+ that.showRuleJson = false
+ that.showAllPathTags = false
+ that.previewResult = res.data || null
+ if (that.previewResult && that.previewResult.lev) {
+ that.activeMapLev = that.previewResult.lev
+ }
+ if (that.previewResult && that.previewResult.mapData) {
+ that.applyMapData(that.previewResult.mapData, that.previewResult.lev)
+ } else if (that.activeMapLev && that.mapContext.lev !== that.activeMapLev) {
+ that.loadMapByLev(that.activeMapLev)
+ }
+ that.$nextTick(function () {
+ that.centerOnPath()
+ })
+ if (!that.hasActualPath) {
+ that.$message.warning('褰撳墠璧风粓鐐规湭璁$畻鍒板彲琛岃矾寰勶紝璇锋鏌ヨ鍒欐垨妤煎眰鍦板浘')
+ }
+ },
+ error: function () {
+ that.previewLoading = false
+ that.$message.error('棰勮璇锋眰寮傚父')
+ }
+ })
+ },
+ loadMapByRule: function (rule) {
+ if (!rule) {
+ return
+ }
+ var station = this.findStation(rule.startStationId || rule.endStationId)
+ if (station && station.stationLev && this.mapContext.lev !== station.stationLev) {
+ this.loadMapByLev(station.stationLev)
+ }
+ },
+ loadMapByLev: function (lev) {
+ if (!lev) {
+ return
+ }
+ if (this.mapContext.lev === lev && this.mapContext.nodes && this.mapContext.nodes.length) {
+ return
+ }
+ var that = this
+ $.ajax({
+ url: baseUrl + '/basMap/lev/' + lev + '/auth',
+ method: 'GET',
+ headers: { token: localStorage.getItem('token') },
+ success: function (res) {
+ if (res.code !== 200) {
+ that.$message.error('鍔犺浇妤煎眰鍦板浘澶辫触: ' + res.msg)
+ return
+ }
+ that.applyMapData(res.data, lev)
+ },
+ error: function () {
+ that.$message.error('鍔犺浇妤煎眰鍦板浘璇锋眰寮傚父')
+ }
+ })
+ },
+ applyMapData: function (mapData, lev) {
+ var parsed = mapData
+ if (typeof mapData === 'string') {
+ try {
+ parsed = JSON.parse(mapData)
+ } catch (e) {
+ parsed = []
+ }
+ }
+ if (!Array.isArray(parsed)) {
+ this.mapContext = { lev: lev, width: 0, height: 0, nodes: [], nodeMap: {} }
+ return
+ }
+ var cellStep = 26
+ var margin = 22
+ var nodes = []
+ var nodeMap = {}
+ var rows = parsed.length
+ var cols = 0
+ for (var r = 0; r < parsed.length; r++) {
+ var row = parsed[r] || []
+ cols = Math.max(cols, row.length)
+ for (var c = 0; c < row.length; c++) {
+ var cell = row[c] || {}
+ var type = cell.type
+ var mergeType = cell.mergeType
+ if (!(type === 'devp' || (type === 'merge' && mergeType === 'devp'))) {
+ continue
+ }
+ var value = this.parseJson(cell.value)
+ var stationId = value ? value.stationId : null
+ if (!stationId) {
+ continue
+ }
+ var node = {
+ stationId: stationId,
+ row: r,
+ col: c,
+ x: margin + c * cellStep,
+ y: margin + r * cellStep,
+ stationAlias: value.stationAlias || '',
+ isLiftTransfer: value.isLiftTransfer === 1 || value.isLiftTransfer === true
+ }
+ nodes.push(node)
+ nodeMap[String(stationId)] = node
+ }
+ }
+ this.mapContext = {
+ lev: lev,
+ width: margin * 2 + Math.max(cols - 1, 0) * cellStep + 40,
+ height: margin * 2 + Math.max(rows - 1, 0) * cellStep + 40,
+ nodes: nodes,
+ nodeMap: nodeMap
+ }
+ this.fitMap()
+ },
+ fitMap: function () {
+ var wrap = this.$refs.mapCanvasWrap
+ if (!wrap || !this.mapContext.width || !this.mapContext.height) {
+ return
+ }
+ var bounds = this.getMapContentBounds()
+ var contentWidth = Math.max((bounds.maxX - bounds.minX), 1)
+ var contentHeight = Math.max((bounds.maxY - bounds.minY), 1)
+ var usableWidth = Math.max(wrap.clientWidth - 30, 320)
+ var usableHeight = Math.max(wrap.clientHeight - 30, 240)
+ var scaleX = usableWidth / contentWidth
+ var scaleY = usableHeight / contentHeight
+ var scale = Math.min(scaleX, scaleY, 1.7)
+ var zoomPercent = Math.max(60, Math.min(220, Math.floor(scale * 100)))
+ var centerX = (bounds.minX + bounds.maxX) / 2
+ var centerY = (bounds.minY + bounds.maxY) / 2
+ this.mapZoomPercent = zoomPercent
+ this.mapPanX = Math.round(wrap.clientWidth / 2 - centerX * (zoomPercent / 100))
+ this.mapPanY = Math.round(wrap.clientHeight / 2 - centerY * (zoomPercent / 100))
+ },
+ centerOnPath: function () {
+ var wrap = this.$refs.mapCanvasWrap
+ var stage = this.$refs.mapStage
+ if (!wrap || !stage || !this.actualPathPolyline) {
+ return
+ }
+ var stationIds = this.previewResult && this.previewResult.pathStationIds ? this.previewResult.pathStationIds : []
+ var points = this.resolvePoints(stationIds)
+ if (!points.length) {
+ return
+ }
+ var minX = points[0].x
+ var maxX = points[0].x
+ var minY = points[0].y
+ var maxY = points[0].y
+ points.forEach(function (point) {
+ minX = Math.min(minX, point.x)
+ maxX = Math.max(maxX, point.x)
+ minY = Math.min(minY, point.y)
+ maxY = Math.max(maxY, point.y)
+ })
+ var scale = this.mapZoomPercent / 100
+ this.mapPanX = Math.round(wrap.clientWidth / 2 - ((minX + maxX) / 2) * scale)
+ this.mapPanY = Math.round(wrap.clientHeight / 2 - ((minY + maxY) / 2) * scale)
+ },
+ resetPreview: function () {
+ this.previewForm.startStationId = null
+ this.previewForm.endStationId = null
+ this.previewResult = null
+ this.pickedStationId = null
+ this.showRuleJson = false
+ this.showAllPathTags = false
+ },
+ updateMapZoom: function (nextPercent) {
+ var wrap = this.$refs.mapCanvasWrap
+ var zoomPercent = this.toNumberSafe(nextPercent)
+ if (zoomPercent == null) {
+ return
+ }
+ zoomPercent = Math.max(60, Math.min(220, zoomPercent))
+ if (!wrap || !this.mapContext.width || !this.mapContext.height) {
+ this.mapZoomPercent = zoomPercent
+ return
+ }
+ this.setMapZoomAroundPoint(zoomPercent, wrap.clientWidth / 2, wrap.clientHeight / 2)
+ },
+ setMapZoomAroundPoint: function (nextPercent, anchorX, anchorY) {
+ var currentPercent = this.mapZoomPercent
+ if (!currentPercent || currentPercent === nextPercent) {
+ this.mapZoomPercent = nextPercent
+ return
+ }
+ var currentScale = currentPercent / 100
+ var nextScale = nextPercent / 100
+ if (!currentScale || !nextScale) {
+ this.mapZoomPercent = nextPercent
+ return
+ }
+ var mapX = (anchorX - this.mapPanX) / currentScale
+ var mapY = (anchorY - this.mapPanY) / currentScale
+ this.mapZoomPercent = nextPercent
+ this.mapPanX = Math.round(anchorX - mapX * nextScale)
+ this.mapPanY = Math.round(anchorY - mapY * nextScale)
+ },
+ handleMapWheel: function (event) {
+ if (!this.mapContext.nodes.length) {
+ return
+ }
+ if (event.ctrlKey || event.metaKey) {
+ var wrap = this.$refs.mapCanvasWrap
+ if (!wrap) {
+ return
+ }
+ var rect = wrap.getBoundingClientRect()
+ var delta = event.deltaY < 0 ? 10 : -10
+ var nextPercent = Math.max(60, Math.min(220, this.mapZoomPercent + delta))
+ this.setMapZoomAroundPoint(nextPercent, event.clientX - rect.left, event.clientY - rect.top)
+ return
+ }
+ this.mapPanX -= event.deltaX
+ this.mapPanY -= event.deltaY
+ },
+ beginMapDrag: function (event) {
+ var wrap = this.$refs.mapCanvasWrap
+ if (!wrap || !this.mapContext.nodes.length) {
+ return
+ }
+ if (event && event.button != null && event.button !== 0) {
+ return
+ }
+ this.mapDragActive = true
+ this.mapDragMoved = false
+ this.mapDragStartX = event.clientX
+ this.mapDragStartY = event.clientY
+ this.mapDragOriginPanX = this.mapPanX
+ this.mapDragOriginPanY = this.mapPanY
+ document.addEventListener('mousemove', this.handleMapDragMove)
+ document.addEventListener('mouseup', this.endMapDrag)
+ if (event.preventDefault) {
+ event.preventDefault()
+ }
+ },
+ handleMapDragMove: function (event) {
+ if (!this.mapDragActive) {
+ return
+ }
+ var wrap = this.$refs.mapCanvasWrap
+ if (!wrap) {
+ this.endMapDrag()
+ return
+ }
+ var deltaX = event.clientX - this.mapDragStartX
+ var deltaY = event.clientY - this.mapDragStartY
+ if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
+ this.mapDragMoved = true
+ }
+ this.mapPanX = this.mapDragOriginPanX + deltaX
+ this.mapPanY = this.mapDragOriginPanY + deltaY
+ },
+ endMapDrag: function () {
+ if (!this.mapDragActive) {
+ this.detachMapDragListeners()
+ return
+ }
+ this.mapDragActive = false
+ this.detachMapDragListeners()
+ if (this.mapDragMoved) {
+ this.suppressNodeClick = true
+ var that = this
+ window.setTimeout(function () {
+ that.suppressNodeClick = false
+ }, 0)
+ }
+ },
+ detachMapDragListeners: function () {
+ document.removeEventListener('mousemove', this.handleMapDragMove)
+ document.removeEventListener('mouseup', this.endMapDrag)
+ },
+ pickNode: function (node) {
+ if (this.suppressNodeClick) {
+ return
+ }
+ this.pickedStationId = node.stationId
+ },
+ applyPickedStation: function (field) {
+ if (!this.pickedStationId) {
+ return
+ }
+ if (field === 'start') {
+ this.previewForm.startStationId = this.pickedStationId
+ } else if (field === 'end') {
+ this.previewForm.endStationId = this.pickedStationId
+ } else if (field === 'ruleStart' && this.ruleForm) {
+ this.ruleForm.startStationId = this.pickedStationId
+ } else if (field === 'ruleEnd' && this.ruleForm) {
+ this.ruleForm.endStationId = this.pickedStationId
+ } else if (field === 'mustPass' && this.ruleForm) {
+ this.pushUniqueStation(this.ruleForm.hard.mustPassStations, this.pickedStationId)
+ } else if (field === 'forbid' && this.ruleForm) {
+ this.pushUniqueStation(this.ruleForm.hard.forbidStations, this.pickedStationId)
+ } else if (field === 'waypoint' && this.ruleForm) {
+ this.pushUniqueStation(this.ruleForm.waypoint.stations, this.pickedStationId)
+ } else if (field === 'preferred' && this.ruleForm) {
+ this.pushUniqueStation(this.ruleForm.soft.preferredPath, this.pickedStationId)
+ }
+ },
+ pushUniqueStation: function (list, stationId) {
+ if (!Array.isArray(list)) {
+ return
+ }
+ var value = this.toNumberSafe(stationId)
+ if (value == null) {
+ return
+ }
+ var exists = list.some(function (item) {
+ return String(item) === String(value)
+ })
+ if (!exists) {
+ list.push(value)
+ }
+ },
+ moveListItem: function (list, index, offset) {
+ if (!Array.isArray(list)) {
+ return
+ }
+ var targetIndex = index + offset
+ if (index < 0 || targetIndex < 0 || index >= list.length || targetIndex >= list.length) {
+ return
+ }
+ var moved = list.splice(index, 1)[0]
+ list.splice(targetIndex, 0, moved)
+ },
+ removeListItem: function (list, index) {
+ if (!Array.isArray(list) || index < 0 || index >= list.length) {
+ return
+ }
+ list.splice(index, 1)
+ },
+ buildLookup: function (list) {
+ var lookup = {}
+ ;(list || []).forEach(function (item) {
+ if (item != null && item !== '') {
+ lookup[String(item)] = true
+ }
+ })
+ return lookup
+ },
+ nodeStyle: function (node) {
+ return {
+ left: node.x + 'px',
+ top: node.y + 'px'
+ }
+ },
+ nodeClasses: function (node) {
+ var stationId = node.stationId
+ var classes = []
+ if (String(this.pickedStationId || '') === String(stationId)) {
+ classes.push('is-picked')
+ }
+ if (this.previewForm.startStationId === stationId) {
+ classes.push('is-start')
+ }
+ if (this.previewForm.endStationId === stationId) {
+ classes.push('is-end')
+ }
+ if (this.pathStationSet()[stationId]) {
+ classes.push('is-path')
+ }
+ if (this.preferredStationSet()[stationId]) {
+ classes.push('is-preferred')
+ }
+ if (this.waypointStationSet()[stationId]) {
+ classes.push('is-waypoint')
+ }
+ if (this.forbidStationSet()[stationId]) {
+ classes.push('is-forbid')
+ }
+ if (this.mustPassStationSet()[stationId]) {
+ classes.push('is-must-pass')
+ }
+ return classes
+ },
+ showNodeLabel: function (node) {
+ var stationId = node.stationId
+ return !!(
+ this.previewForm.startStationId === stationId
+ || this.previewForm.endStationId === stationId
+ || this.pathStationSet()[stationId]
+ || this.waypointStationSet()[stationId]
+ || this.forbidStationSet()[stationId]
+ || this.mustPassStationSet()[stationId]
+ || String(this.pickedStationId || '') === String(stationId)
+ )
+ },
+ stationNodeTitle: function (node) {
+ return this.stationLabel(node.stationId)
+ },
+ stationOptionLabel: function (station) {
+ if (!station) {
+ return ''
+ }
+ var alias = station.stationAlias ? ' 路 ' + station.stationAlias : ''
+ return 'L' + (station.stationLev || '-') + ' 路 ' + station.stationId + alias
+ },
+ stationLabel: function (stationId) {
+ var station = this.findStation(stationId)
+ return station ? this.stationOptionLabel(station) : String(stationId || '')
+ },
+ findStation: function (stationId) {
+ if (stationId == null) {
+ return null
+ }
+ return this.stationMapById[String(stationId)] || null
+ },
+ routeLabel: function (rule) {
+ if (!rule.startStationId && !rule.endStationId) {
+ return '閫氶厤瑙勫垯'
+ }
+ return (rule.startStationId || '*') + ' 鈫� ' + (rule.endStationId || '*')
+ },
+ ruleSummaryCount: function (hard) {
+ hard = hard || {}
+ return (hard.mustPassStations || []).length
+ + (hard.forbidStations || []).length
+ + (hard.mustPassEdges || []).length
+ + (hard.forbidEdges || []).length
+ },
+ pathStationSet: function () {
+ return this.pathStationLookup
+ },
+ preferredStationSet: function () {
+ return this.preferredStationLookup
+ },
+ waypointStationSet: function () {
+ return this.waypointStationLookup
+ },
+ forbidStationSet: function () {
+ return this.forbidStationLookup
+ },
+ mustPassStationSet: function () {
+ return this.mustPassStationLookup
+ },
+ buildPolyline: function (stationIds) {
+ var points = this.resolvePoints(stationIds)
+ if (!points.length) {
+ return ''
+ }
+ return points.map(function (point) {
+ return point.x + ',' + point.y
+ }).join(' ')
+ },
+ resolvePoints: function (stationIds) {
+ var points = []
+ var map = this.mapContext.nodeMap || {}
+ ;(stationIds || []).forEach(function (stationId) {
+ var node = map[String(stationId)]
+ if (node) {
+ points.push({ x: node.x, y: node.y })
+ }
+ })
+ return points
+ },
+ getMapContentBounds: function () {
+ var nodes = this.mapContext && this.mapContext.nodes ? this.mapContext.nodes : []
+ if (!nodes.length) {
+ return {
+ minX: 0,
+ maxX: this.mapContext.width || 0,
+ minY: 0,
+ maxY: this.mapContext.height || 0
+ }
+ }
+ var minX = null
+ var maxX = null
+ var minY = null
+ var maxY = null
+ nodes.forEach(function (node) {
+ if (!node) {
+ return
+ }
+ minX = minX == null ? node.x : Math.min(minX, node.x)
+ maxX = maxX == null ? node.x : Math.max(maxX, node.x)
+ minY = minY == null ? node.y : Math.min(minY, node.y)
+ maxY = maxY == null ? node.y : Math.max(maxY, node.y)
+ })
+ var padding = 48
+ return {
+ minX: Math.max((minX == null ? 0 : minX) - padding, 0),
+ maxX: Math.min((maxX == null ? 0 : maxX) + padding, this.mapContext.width || Number.MAX_SAFE_INTEGER),
+ minY: Math.max((minY == null ? 0 : minY) - padding, 0),
+ maxY: Math.min((maxY == null ? 0 : maxY) + padding, this.mapContext.height || Number.MAX_SAFE_INTEGER)
+ }
+ },
+ defaultProfileConfig: function () {
+ return createDefaultProfileConfig()
+ },
+ defaultProfile: function () {
+ return createDefaultProfile()
+ },
+ defaultRule: function () {
+ return createDefaultRule(this.defaultProfileCode || 'default')
+ },
+ normalizeProfile: function (raw) {
+ var config = Object.assign({}, this.defaultProfileConfig(), this.parseJson(raw.configJson) || raw.config || {})
+ return {
+ id: raw.id || null,
+ profileCode: raw.profileCode || '',
+ profileName: raw.profileName || raw.profileCode || '',
+ priority: raw.priority == null ? 100 : Number(raw.priority),
+ status: raw.status == null ? 1 : Number(raw.status),
+ memo: raw.memo || '',
+ config: config
+ }
+ },
+ normalizeRule: function (raw) {
+ var rule = this.defaultRule()
+ var hard = Object.assign({}, rule.hard, this.parseJson(raw.hardJson) || raw.hard || {})
+ var waypoint = Object.assign({}, rule.waypoint, this.parseJson(raw.waypointJson) || raw.waypoint || {})
+ var soft = Object.assign({}, rule.soft, this.parseJson(raw.softJson) || raw.soft || {})
+ var fallback = Object.assign({}, rule.fallback, this.parseJson(raw.fallbackJson) || raw.fallback || {})
+ hard.mustPassEdgesText = (hard.mustPassEdges || []).join('\n')
+ hard.forbidEdgesText = (hard.forbidEdges || []).join('\n')
+ rule.id = raw.id || null
+ rule.ruleCode = raw.ruleCode || ''
+ rule.ruleName = raw.ruleName || raw.ruleCode || ''
+ rule.priority = raw.priority == null ? 100 : Number(raw.priority)
+ rule.status = raw.status == null ? 1 : Number(raw.status)
+ rule.sceneType = raw.sceneType || 'station'
+ rule.startStationId = raw.startStationId == null ? null : Number(raw.startStationId)
+ rule.endStationId = raw.endStationId == null ? null : Number(raw.endStationId)
+ rule.profileCode = raw.profileCode || ''
+ rule.memo = raw.memo || ''
+ rule.hard = hard
+ rule.waypoint = waypoint
+ rule.soft = soft
+ rule.fallback = fallback
+ return rule
+ },
+ cloneProfileModel: function (item) {
+ var model = this.normalizeProfile(item)
+ model._originCode = item.profileCode || item._originCode || null
+ return JSON.parse(JSON.stringify(model))
+ },
+ cloneRuleModel: function (item) {
+ var model = this.normalizeRule(item)
+ model._originCode = item.ruleCode || item._originCode || null
+ return JSON.parse(JSON.stringify(model))
+ },
+ sanitizeProfileForSave: function (item) {
+ return {
+ id: item.id || null,
+ profileCode: item.profileCode,
+ profileName: item.profileName,
+ priority: Number(item.priority || 100),
+ status: Number(item.status || 0),
+ memo: item.memo || '',
+ config: Object.assign({}, item.config || {})
+ }
+ },
+ sanitizeRuleForSave: function (item) {
+ var hard = Object.assign({}, item.hard || {})
+ hard.mustPassEdges = this.parseLines(hard.mustPassEdgesText || hard.mustPassEdges || [])
+ hard.forbidEdges = this.parseLines(hard.forbidEdgesText || hard.forbidEdges || [])
+ delete hard.mustPassEdgesText
+ delete hard.forbidEdgesText
+ return {
+ id: item.id || null,
+ ruleCode: item.ruleCode,
+ ruleName: item.ruleName,
+ priority: Number(item.priority || 100),
+ status: Number(item.status || 0),
+ sceneType: item.sceneType || '',
+ startStationId: item.startStationId == null ? null : Number(item.startStationId),
+ endStationId: item.endStationId == null ? null : Number(item.endStationId),
+ profileCode: item.profileCode || '',
+ memo: item.memo || '',
+ hard: {
+ mustPassStations: this.uniqueNumbers(hard.mustPassStations || []),
+ forbidStations: this.uniqueNumbers(hard.forbidStations || []),
+ mustPassEdges: hard.mustPassEdges,
+ forbidEdges: hard.forbidEdges
+ },
+ waypoint: {
+ stations: this.uniqueNumbers((item.waypoint && item.waypoint.stations) || [])
+ },
+ soft: {
+ keyStations: this.uniqueNumbers((item.soft && item.soft.keyStations) || []),
+ preferredPath: this.uniqueNumbers((item.soft && item.soft.preferredPath) || []),
+ deviationWeight: this.toNumberSafe(item.soft && item.soft.deviationWeight) || 0,
+ maxOffPathCount: this.toNumberSafe(item.soft && item.soft.maxOffPathCount) || 0
+ },
+ fallback: {
+ strictWaypoint: !!(item.fallback && item.fallback.strictWaypoint),
+ allowSoftDegrade: !(item.fallback && item.fallback.allowSoftDegrade === false)
+ }
+ }
+ },
+ findProfileIndex: function (profileCode) {
+ for (var i = 0; i < this.profiles.length; i++) {
+ if (this.profiles[i].profileCode === profileCode) {
+ return i
+ }
+ }
+ return -1
+ },
+ findRuleIndex: function (ruleCode) {
+ for (var i = 0; i < this.rules.length; i++) {
+ if (this.rules[i].ruleCode === ruleCode) {
+ return i
+ }
+ }
+ return -1
+ },
+ parseJson: function (value) {
+ if (!value) {
+ return null
+ }
+ if (typeof value === 'object') {
+ return value
+ }
+ try {
+ return JSON.parse(value)
+ } catch (e) {
+ return null
+ }
+ },
+ parseLines: function (value) {
+ if (Array.isArray(value)) {
+ return value.filter(function (item) { return !!String(item || '').trim() })
+ }
+ return String(value || '')
+ .split('\n')
+ .map(function (item) { return item.trim() })
+ .filter(function (item) { return !!item })
+ },
+ toNumberSafe: function (value) {
+ if (value == null || value === '') {
+ return null
+ }
+ var num = Number(value)
+ return isNaN(num) ? null : num
+ },
+ notNull: function (value) {
+ return value != null
+ },
+ uniqueNumbers: function (list) {
+ var result = []
+ var seen = {}
+ ;(list || []).forEach(function (item) {
+ var num = this.toNumberSafe(item)
+ if (num == null) {
+ return
+ }
+ if (!seen[String(num)]) {
+ seen[String(num)] = true
+ result.push(num)
+ }
+ }.bind(this))
+ return result
+ },
+ isBlank: function (text) {
+ return text == null || String(text).trim() === ''
+ }
+ }
+})
diff --git a/src/main/webapp/views/stationPathPolicy/stationPathPolicy.html b/src/main/webapp/views/stationPathPolicy/stationPathPolicy.html
new file mode 100644
index 0000000..1bbd1a0
--- /dev/null
+++ b/src/main/webapp/views/stationPathPolicy/stationPathPolicy.html
@@ -0,0 +1,1300 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="utf-8">
+ <title>杈撻�佽矾寰勭瓥鐣ョ鐞�</title>
+ <meta name="renderer" content="webkit">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+ <link rel="stylesheet" href="../../static/vue/element/element.css">
+ <link rel="stylesheet" href="../../static/css/cool.css">
+ <style>
+ :root {
+ --page-bg:
+ radial-gradient(1200px 560px at -10% -20%, rgba(53, 117, 200, 0.14), transparent 58%),
+ radial-gradient(900px 420px at 110% -10%, rgba(38, 164, 138, 0.12), transparent 56%),
+ linear-gradient(180deg, #eff4fa 0%, #f7fafc 100%);
+ --card-bg: rgba(255, 255, 255, 0.94);
+ --card-border: rgba(216, 226, 238, 0.95);
+ --text-main: #223449;
+ --text-sub: #607286;
+ --primary: #2f79d6;
+ --accent: #20a98a;
+ --warn: #ef8b3b;
+ --danger: #dc5c5c;
+ --path: #1f9f89;
+ --preferred: #2f79d6;
+ --forbid: #d9534f;
+ --must-pass: #9b59b6;
+ --waypoint: #ff9f1c;
+ }
+
+ [v-cloak] { display: none; }
+
+ html, body {
+ margin: 0;
+ min-height: 100%;
+ height: auto;
+ font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
+ background: var(--page-bg);
+ color: var(--text-main);
+ }
+
+ .page-shell {
+ min-height: 100%;
+ height: auto;
+ box-sizing: border-box;
+ width: min(1880px, calc(100% - 24px));
+ margin: 0 auto;
+ padding: 20px 8px 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+ }
+
+ .hero-card,
+ .panel-card {
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 24px;
+ box-shadow: 0 16px 32px rgba(39, 62, 92, 0.08);
+ }
+
+ .hero-card {
+ padding: 24px 28px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+ flex-wrap: wrap;
+ }
+
+ .hero-title {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ .hero-title h1 {
+ margin: 0;
+ font-size: 24px;
+ font-weight: 700;
+ letter-spacing: 0.4px;
+ }
+
+ .hero-actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ }
+
+ .hero-grid {
+ display: grid;
+ grid-template-columns: minmax(340px, 380px) minmax(0, 1fr);
+ grid-template-areas:
+ "profile rule"
+ "profile preview";
+ gap: 16px;
+ align-items: start;
+ }
+
+ .profile-panel {
+ grid-area: profile;
+ min-width: 0;
+ }
+
+ .rule-panel {
+ grid-area: rule;
+ min-width: 0;
+ }
+
+ .panel-card {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ .preview-panel {
+ grid-area: preview;
+ min-width: 0;
+ }
+
+ .panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 20px 22px 16px;
+ border-bottom: 1px solid rgba(221, 230, 239, 0.94);
+ }
+
+ .panel-head h2 {
+ margin: 0;
+ font-size: 17px;
+ font-weight: 700;
+ }
+
+ .panel-body {
+ padding: 18px 20px 20px;
+ flex: 0 0 auto;
+ overflow: visible;
+ }
+
+ .setting-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
+ }
+
+ .hint-card {
+ border-radius: 18px;
+ background: linear-gradient(180deg, rgba(243, 248, 255, 0.94) 0%, rgba(248, 251, 255, 0.9) 100%);
+ border: 1px solid rgba(212, 223, 236, 0.92);
+ padding: 14px 16px;
+ }
+
+ .hint-card strong {
+ display: block;
+ margin-bottom: 6px;
+ font-size: 13px;
+ }
+
+ .hint-card span {
+ color: var(--text-sub);
+ font-size: 12px;
+ line-height: 1.7;
+ }
+
+ .stat-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ }
+
+ .stat-card {
+ border: 1px solid rgba(217, 227, 236, 0.94);
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 18px;
+ padding: 16px;
+ }
+
+ .stat-card .label {
+ color: var(--text-sub);
+ font-size: 12px;
+ }
+
+ .stat-card .value {
+ margin-top: 6px;
+ font-size: 24px;
+ font-weight: 700;
+ }
+
+ .entity-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .entity-empty.el-empty {
+ padding: 18px 0 4px;
+ }
+
+ .entity-empty .el-empty__image {
+ height: 96px;
+ }
+
+ .entity-empty .el-empty__description p {
+ color: var(--text-sub);
+ font-size: 13px;
+ }
+
+ .entity-item {
+ border: 1px solid rgba(215, 226, 238, 0.94);
+ background: rgba(255, 255, 255, 0.96);
+ border-radius: 18px;
+ padding: 16px 16px 14px;
+ transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
+ }
+
+ .entity-item.is-active {
+ border-color: rgba(47, 121, 214, 0.65);
+ box-shadow: 0 10px 24px rgba(47, 121, 214, 0.12);
+ transform: translateY(-1px);
+ }
+
+ .entity-item-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ }
+
+ .entity-title {
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 1.5;
+ }
+
+ .entity-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 8px;
+ }
+
+ .meta-pill {
+ display: inline-flex;
+ align-items: center;
+ padding: 4px 8px;
+ font-size: 12px;
+ border-radius: 999px;
+ background: rgba(242, 246, 252, 0.94);
+ color: var(--text-sub);
+ border: 1px solid rgba(220, 229, 238, 0.9);
+ }
+
+ .entity-desc {
+ margin-top: 10px;
+ color: var(--text-sub);
+ font-size: 12px;
+ line-height: 1.7;
+ }
+
+ .entity-actions {
+ margin-top: 12px;
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .entity-actions .el-button {
+ padding: 7px 11px;
+ border-radius: 10px;
+ }
+
+ .empty-shell {
+ border: 1px dashed rgba(193, 207, 221, 0.96);
+ background: rgba(248, 251, 254, 0.84);
+ border-radius: 18px;
+ padding: 22px 16px;
+ }
+
+ .preview-toolbar {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-bottom: 0;
+ }
+
+ .preview-toolbar-row {
+ display: grid;
+ gap: 12px;
+ align-items: center;
+ }
+
+ .preview-toolbar-row-main {
+ grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.2fr) minmax(180px, 220px);
+ }
+
+ .preview-toolbar-row-secondary {
+ grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
+ }
+
+ .preview-toolbar-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+
+ .preview-zoom-card {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 10px 14px;
+ border-radius: 18px;
+ background: linear-gradient(180deg, rgba(247, 250, 255, 0.96) 0%, rgba(243, 248, 254, 0.96) 100%);
+ border: 1px solid rgba(217, 227, 236, 0.96);
+ }
+
+ .preview-zoom-meta {
+ min-width: 74px;
+ }
+
+ .preview-zoom-meta strong {
+ display: block;
+ font-size: 12px;
+ margin-bottom: 2px;
+ }
+
+ .preview-zoom-meta span {
+ font-size: 12px;
+ color: var(--text-sub);
+ }
+
+ .preview-panel-body {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ }
+
+ .preview-info-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.95fr);
+ gap: 12px;
+ align-items: start;
+ }
+
+ .preview-summary-card {
+ border-radius: 16px;
+ border: 1px solid rgba(217, 227, 236, 0.96);
+ background: rgba(255, 255, 255, 0.96);
+ padding: 12px 14px;
+ }
+
+ .preview-summary {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 0;
+ }
+
+ .preview-summary-actions {
+ margin-top: 10px;
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ .summary-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 10px;
+ border-radius: 999px;
+ background: rgba(244, 248, 252, 0.96);
+ border: 1px solid rgba(220, 229, 238, 0.96);
+ font-size: 12px;
+ }
+
+ .summary-chip strong {
+ color: var(--text-main);
+ }
+
+ .route-strip {
+ border-radius: 18px;
+ border: 1px solid rgba(217, 227, 236, 0.94);
+ background: rgba(255, 255, 255, 0.94);
+ padding: 12px;
+ margin-bottom: 12px;
+ }
+
+ .route-strip-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 10px;
+ }
+
+ .route-tag-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ max-height: 98px;
+ overflow-y: auto;
+ padding-right: 4px;
+ }
+
+ .route-tag {
+ border-radius: 999px;
+ padding: 6px 10px;
+ background: rgba(47, 121, 214, 0.08);
+ color: #245a95;
+ font-size: 12px;
+ border: 1px solid rgba(47, 121, 214, 0.12);
+ }
+
+ .route-strip-more {
+ margin-top: 8px;
+ color: var(--text-sub);
+ font-size: 12px;
+ }
+
+ .map-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 10px;
+ }
+
+ .legend-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--text-sub);
+ }
+
+ .legend-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ display: inline-block;
+ }
+
+ .map-shell {
+ position: relative;
+ flex: 1;
+ min-height: 620px;
+ border-radius: 22px;
+ border: 1px solid rgba(216, 226, 238, 0.95);
+ background:
+ linear-gradient(180deg, rgba(251, 253, 255, 0.96) 0%, rgba(247, 250, 253, 0.94) 100%);
+ overflow: hidden;
+ }
+
+ .map-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 12px 14px;
+ border-bottom: 1px solid rgba(221, 230, 239, 0.94);
+ background: rgba(249, 251, 254, 0.9);
+ }
+
+ .map-toolbar-left,
+ .map-toolbar-right {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+
+ .map-canvas-wrap {
+ position: absolute;
+ top: 56px;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ padding: 20px;
+ box-sizing: border-box;
+ cursor: grab;
+ user-select: none;
+ touch-action: none;
+ }
+
+ .map-stage {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform-origin: left top;
+ will-change: transform;
+ }
+
+ .map-stage svg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: visible;
+ }
+
+ .map-node {
+ position: absolute;
+ width: 18px;
+ height: 18px;
+ margin-left: -9px;
+ margin-top: -9px;
+ border-radius: 50%;
+ border: 2px solid rgba(165, 181, 198, 0.88);
+ background: rgba(255, 255, 255, 0.95);
+ cursor: pointer;
+ transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ }
+
+ .map-node:hover {
+ transform: scale(1.14);
+ box-shadow: 0 8px 18px rgba(58, 82, 111, 0.16);
+ z-index: 4;
+ }
+
+ .map-canvas-wrap.is-dragging {
+ cursor: grabbing;
+ }
+
+ .map-canvas-wrap.is-dragging .map-node {
+ cursor: grabbing;
+ }
+
+ .map-node.is-picked {
+ width: 26px;
+ height: 26px;
+ margin-left: -13px;
+ margin-top: -13px;
+ border-color: #101828;
+ box-shadow: 0 10px 24px rgba(16, 24, 40, 0.16);
+ z-index: 6;
+ }
+
+ .map-node.is-path {
+ border-color: rgba(31, 159, 137, 0.92);
+ background: rgba(31, 159, 137, 0.12);
+ }
+
+ .map-node.is-preferred {
+ border-color: rgba(47, 121, 214, 0.92);
+ background: rgba(47, 121, 214, 0.09);
+ }
+
+ .map-node.is-waypoint {
+ box-shadow: 0 0 0 4px rgba(255, 159, 28, 0.16);
+ }
+
+ .map-node.is-forbid {
+ border-color: rgba(220, 92, 92, 0.94);
+ background: rgba(220, 92, 92, 0.12);
+ }
+
+ .map-node.is-must-pass {
+ box-shadow: 0 0 0 4px rgba(155, 89, 182, 0.14);
+ }
+
+ .map-node.is-start,
+ .map-node.is-end {
+ width: 28px;
+ height: 28px;
+ margin-left: -14px;
+ margin-top: -14px;
+ }
+
+ .map-node.is-start {
+ border-color: rgba(47, 121, 214, 0.92);
+ background: rgba(47, 121, 214, 0.18);
+ }
+
+ .map-node.is-end {
+ border-color: rgba(32, 169, 138, 0.92);
+ background: rgba(32, 169, 138, 0.16);
+ }
+
+ .map-node-label {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ transform: translateX(-50%);
+ white-space: nowrap;
+ font-size: 11px;
+ padding: 2px 6px;
+ border-radius: 999px;
+ background: rgba(16, 24, 40, 0.76);
+ color: #fff;
+ pointer-events: none;
+ }
+
+ .rule-json {
+ margin-top: 12px;
+ border-radius: 18px;
+ background: rgba(16, 24, 40, 0.92);
+ color: #d6e5ff;
+ padding: 12px 14px;
+ font-family: Menlo, Monaco, Consolas, monospace;
+ font-size: 12px;
+ line-height: 1.7;
+ max-height: 220px;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ }
+
+ .mono {
+ font-family: Menlo, Monaco, Consolas, monospace;
+ }
+
+ .sequence-card {
+ margin-top: 10px;
+ border-radius: 16px;
+ border: 1px solid rgba(218, 228, 238, 0.96);
+ background: rgba(255, 255, 255, 0.92);
+ padding: 8px;
+ }
+
+ .sequence-row {
+ display: grid;
+ grid-template-columns: 28px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 10px;
+ border-radius: 12px;
+ padding: 6px 8px;
+ }
+
+ .sequence-row + .sequence-row {
+ margin-top: 6px;
+ }
+
+ .sequence-row:hover {
+ background: rgba(243, 247, 252, 0.92);
+ }
+
+ .sequence-index {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ color: #245a95;
+ background: rgba(47, 121, 214, 0.08);
+ border: 1px solid rgba(47, 121, 214, 0.14);
+ }
+
+ .sequence-label {
+ min-width: 0;
+ font-size: 12px;
+ line-height: 1.6;
+ color: var(--text-main);
+ word-break: break-all;
+ }
+
+ .sequence-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .sequence-actions .el-button {
+ padding: 5px 7px;
+ border-radius: 10px;
+ }
+
+ .dialog-picked-bar {
+ margin: 0 0 12px;
+ border-radius: 16px;
+ border: 1px solid rgba(217, 227, 236, 0.96);
+ background: rgba(247, 250, 254, 0.95);
+ padding: 10px 12px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+
+ .dialog-picked-bar .help {
+ color: var(--text-sub);
+ font-size: 12px;
+ line-height: 1.6;
+ }
+
+ .dialog-panel .el-dialog {
+ border-radius: 24px;
+ overflow: hidden;
+ }
+
+ .dialog-panel .el-dialog__header {
+ padding: 22px 24px 14px;
+ background: linear-gradient(180deg, #f7fbff 0%, #f2f7fb 100%);
+ border-bottom: 1px solid rgba(223, 231, 240, 0.94);
+ }
+
+ .dialog-panel .el-dialog__body {
+ padding: 18px 24px 10px;
+ }
+
+ .dialog-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+ }
+
+ .dialog-grid .span-2 {
+ grid-column: span 2;
+ }
+
+ .section-card {
+ border: 1px solid rgba(219, 229, 238, 0.96);
+ border-radius: 18px;
+ padding: 12px 14px 2px;
+ background: rgba(250, 252, 254, 0.9);
+ margin-bottom: 12px;
+ }
+
+ .section-card h3 {
+ margin: 0 0 10px;
+ font-size: 13px;
+ font-weight: 700;
+ }
+
+ .section-title-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+ }
+
+ .section-title-row h3 {
+ margin-bottom: 0;
+ }
+
+ .section-inline-actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .section-help {
+ color: var(--text-sub);
+ font-size: 12px;
+ margin: -2px 0 10px;
+ line-height: 1.6;
+ }
+
+ .selected-station-bar {
+ border-radius: 16px;
+ border: 1px solid rgba(217, 227, 236, 0.96);
+ background: rgba(255, 255, 255, 0.95);
+ padding: 10px 12px;
+ margin-bottom: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+
+ @media (max-width: 1480px) {
+ .hero-grid {
+ grid-template-columns: minmax(320px, 360px) minmax(0, 1fr);
+ }
+
+ .preview-info-grid {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ @media (max-width: 1280px) {
+ .hero-grid {
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ "profile"
+ "rule"
+ "preview";
+ }
+ .map-shell {
+ min-height: 560px;
+ }
+ .preview-toolbar-row,
+ .preview-toolbar-row-main,
+ .preview-toolbar-row-secondary {
+ grid-template-columns: 1fr;
+ }
+ .preview-toolbar-actions {
+ justify-content: flex-start;
+ }
+ }
+ </style>
+</head>
+<body>
+<div id="app" v-cloak class="page-shell">
+ <div class="hero-card">
+ <div class="hero-title">
+ <h1>杈撻�佽矾寰勭瓥鐣ョ鐞�</h1>
+ </div>
+ <div class="hero-actions">
+ <el-button icon="el-icon-refresh" @click="loadData">鍒锋柊</el-button>
+ <el-button type="primary" icon="el-icon-check" :loading="saving" @click="saveAll">淇濆瓨鍏ㄩ儴</el-button>
+ </div>
+ </div>
+
+ <div class="hero-grid">
+ <div class="panel-card profile-panel">
+ <div class="panel-head">
+ <div><h2>妯℃澘涓庢ā寮�</h2></div>
+ <el-button type="primary" plain size="small" icon="el-icon-plus" @click="openProfileDialog()">鏂板妯℃澘</el-button>
+ </div>
+ <div class="panel-body">
+ <div class="setting-grid">
+ <div class="section-card">
+ <h3>鍏ㄥ眬寮�鍏�</h3>
+ <el-form label-position="top">
+ <div class="dialog-grid">
+ <el-form-item label="璇勫垎妯″紡" class="span-2">
+ <el-radio-group v-model="scoreMode">
+ <el-radio-button label="legacy">legacy</el-radio-button>
+ <el-radio-button label="twoStage">twoStage</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="榛樿妯℃澘" class="span-2">
+ <el-select v-model="defaultProfileCode" placeholder="璇烽�夋嫨榛樿妯℃澘" filterable style="width: 100%;">
+ <el-option v-for="item in profiles" :key="item.profileCode" :label="item.profileName + ' (' + item.profileCode + ')'" :value="item.profileCode"></el-option>
+ </el-select>
+ </el-form-item>
+ </div>
+ </el-form>
+ </div>
+
+ <div class="stat-grid">
+ <div class="stat-card">
+ <div class="label">妯℃澘鏁伴噺</div>
+ <div class="value">{{ profiles.length }}</div>
+ </div>
+ <div class="stat-card">
+ <div class="label">瑙勫垯鏁伴噺</div>
+ <div class="value">{{ rules.length }}</div>
+ </div>
+ </div>
+
+ <div class="entity-list" v-if="profiles.length">
+ <div class="entity-item" v-for="item in profiles" :key="item.profileCode" :class="{ 'is-active': selectedProfileCode === item.profileCode }" @click="selectedProfileCode = item.profileCode">
+ <div class="entity-item-head">
+ <div>
+ <div class="entity-title">{{ item.profileName }}</div>
+ <div class="entity-meta">
+ <span class="meta-pill mono">{{ item.profileCode }}</span>
+ <span class="meta-pill">浼樺厛绾� {{ item.priority }}</span>
+ <span class="meta-pill">{{ item.status === 1 ? '鍚敤' : '绂佺敤' }}</span>
+ </div>
+ </div>
+ <el-tag size="mini" :type="defaultProfileCode === item.profileCode ? 'success' : 'info'">{{ defaultProfileCode === item.profileCode ? '榛樿' : '妯℃澘' }}</el-tag>
+ </div>
+ <div class="entity-desc">
+ S1: 闀垮害 {{ item.config.s1LenWeight }} / 鎷愮偣 {{ item.config.s1TurnWeight }} / 椤跺崌 {{ item.config.s1LiftWeight }}<br>
+ S2: 蹇欑珯 {{ item.config.s2BusyWeight }} / 鍫靛 {{ item.config.s2RunBlockWeight }} / 鐜嚎 {{ item.config.s2LoopLoadWeight }}
+ </div>
+ <div class="entity-actions">
+ <el-button size="mini" @click.stop="openProfileDialog(item)">缂栬緫</el-button>
+ <el-button size="mini" type="primary" plain @click.stop="cloneProfile(item)">澶嶅埗</el-button>
+ <el-button size="mini" type="danger" plain @click.stop="removeProfile(item)">鍒犻櫎</el-button>
+ </div>
+ </div>
+ </div>
+ <el-empty v-else class="entity-empty" description="杩樻病鏈夎矾寰勬ā鏉�"></el-empty>
+ </div>
+ </div>
+ </div>
+
+ <div class="panel-card rule-panel">
+ <div class="panel-head">
+ <div><h2>浜哄伐瑙勫垯</h2></div>
+ <el-button type="primary" plain size="small" icon="el-icon-plus" @click="openRuleDialog()">鏂板瑙勫垯</el-button>
+ </div>
+ <div class="panel-body">
+ <div class="entity-list" v-if="rules.length">
+ <div class="entity-item" v-for="item in rules" :key="item.ruleCode" :class="{ 'is-active': selectedRuleCode === item.ruleCode }" @click="selectRule(item)">
+ <div class="entity-item-head">
+ <div>
+ <div class="entity-title">{{ item.ruleName }}</div>
+ <div class="entity-meta">
+ <span class="meta-pill mono">{{ item.ruleCode }}</span>
+ <span class="meta-pill">浼樺厛绾� {{ item.priority }}</span>
+ <span class="meta-pill">{{ item.status === 1 ? '鍚敤' : '绂佺敤' }}</span>
+ <span class="meta-pill">{{ item.profileCode || '鏈粦瀹氭ā鏉�' }}</span>
+ </div>
+ </div>
+ <el-tag size="mini" :type="item.startStationId && item.endStationId ? 'success' : 'warning'">{{ routeLabel(item) }}</el-tag>
+ </div>
+ <div class="entity-desc">
+ 纭害鏉� {{ ruleSummaryCount(item.hard) }} 椤� 路 閫旂粡鐐� {{ (item.waypoint.stations || []).length }} 涓� 路 杞亸濂� {{ (item.soft.preferredPath || []).length }} 娈�
+ </div>
+ <div class="entity-actions">
+ <el-button size="mini" @click.stop="openRuleDialog(item)">缂栬緫</el-button>
+ <el-button size="mini" type="primary" plain @click.stop="cloneRule(item)">澶嶅埗</el-button>
+ <el-button size="mini" @click.stop="previewRule(item)" :disabled="!item.startStationId || !item.endStationId">璇曠畻</el-button>
+ <el-button size="mini" type="danger" plain @click.stop="removeRule(item)">鍒犻櫎</el-button>
+ </div>
+ </div>
+ </div>
+ <el-empty v-else class="entity-empty" description="杩樻病鏈変汉宸ヨ鍒�"></el-empty>
+ </div>
+ </div>
+
+ <div class="panel-card preview-panel">
+ <div class="panel-head">
+ <div><h2>鍙鍖栭瑙�</h2></div>
+ </div>
+ <div class="panel-body preview-panel-body">
+ <div class="preview-toolbar">
+ <div class="preview-toolbar-row preview-toolbar-row-main">
+ <el-select v-model="previewForm.startStationId" filterable clearable placeholder="璧风偣绔欑偣">
+ <el-option v-for="item in stationOptions" :key="'s-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ <el-select v-model="previewForm.endStationId" filterable clearable placeholder="缁堢偣绔欑偣">
+ <el-option v-for="item in stationOptions" :key="'e-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ <el-select v-model="activeMapLev" placeholder="妤煎眰" clearable @change="loadMapByLev" :disabled="previewLoading">
+ <el-option v-for="lev in levList" :key="lev" :label="'妤煎眰 ' + lev" :value="lev"></el-option>
+ </el-select>
+ </div>
+ <div class="preview-toolbar-row preview-toolbar-row-secondary">
+ <div class="preview-zoom-card">
+ <div class="preview-zoom-meta">
+ <strong>鍦板浘缂╂斁</strong>
+ <span>{{ mapZoomPercent }}%</span>
+ </div>
+ <el-slider :value="mapZoomPercent" @input="updateMapZoom" :min="60" :max="220" :step="10" :show-tooltip="false" style="flex: 1;"></el-slider>
+ </div>
+ <div class="preview-toolbar-actions">
+ <el-button @click="fitMap" :disabled="!mapContext.nodes.length">閫傞厤鍦板浘</el-button>
+ <el-button @click="resetPreview">娓呯┖</el-button>
+ <el-button type="primary" :loading="previewLoading" @click="loadPreview" :disabled="!previewForm.startStationId || !previewForm.endStationId">瑙f瀽棰勮</el-button>
+ </div>
+ </div>
+ </div>
+
+ <div class="preview-info-grid">
+ <div class="selected-station-bar">
+ <div>
+ <strong>褰撳墠閫変腑绔欑偣锛�</strong>
+ <span v-if="pickedStation">{{ stationOptionLabel(pickedStation) }}</span>
+ <span v-else style="color: var(--text-sub);">鐐瑰嚮鍦板浘涓婄殑杈撻�佺珯鐐�</span>
+ </div>
+ <div style="display: flex; gap: 8px; flex-wrap: wrap;">
+ <el-button size="mini" :disabled="!pickedStation" @click="applyPickedStation('start')">璁句负棰勮璧风偣</el-button>
+ <el-button size="mini" :disabled="!pickedStation" @click="applyPickedStation('end')">璁句负棰勮缁堢偣</el-button>
+ </div>
+ </div>
+ <div class="preview-summary-card">
+ <div class="preview-summary">
+ <span class="summary-chip"><strong>妯″紡</strong> {{ scoreMode }}</span>
+ <span class="summary-chip"><strong>榛樿妯℃澘</strong> {{ defaultProfileCode || '-' }}</span>
+ <span class="summary-chip" v-if="previewResult"><strong>鍛戒腑妯℃澘</strong> {{ previewResult.resolvedPolicy && previewResult.resolvedPolicy.profileEntity ? previewResult.resolvedPolicy.profileEntity.profileCode : '-' }}</span>
+ <span class="summary-chip" v-if="previewResult"><strong>鍛戒腑瑙勫垯</strong> {{ previewResult.resolvedPolicy && previewResult.resolvedPolicy.ruleEntity ? previewResult.resolvedPolicy.ruleEntity.ruleCode : '鏃�' }}</span>
+ <span class="summary-chip" v-if="previewResult"><strong>璺緞闀垮害</strong> {{ previewResult.pathLength }}</span>
+ <span class="summary-chip" v-if="previewResult"><strong>鎷愮偣鏁�</strong> {{ previewResult.turnCount }}</span>
+ <span class="summary-chip" v-if="previewResult"><strong>椤跺崌鐐�</strong> {{ previewResult.liftTransferCount }}</span>
+ </div>
+ <div class="preview-summary-actions" v-if="activeRulePreviewJson">
+ <el-button type="text" @click="showRuleJson = !showRuleJson">{{ showRuleJson ? '鏀惰捣瑙勫垯 JSON' : '鏌ョ湅瑙勫垯 JSON' }}</el-button>
+ </div>
+ </div>
+ </div>
+
+ <div class="route-strip" v-if="previewPathTags.length">
+ <div class="route-strip-head">
+ <strong>璺緞绔欑偣搴忓垪</strong>
+ <div style="display: flex; align-items: center; gap: 12px;">
+ <span style="color: var(--text-sub); font-size: 12px;">{{ previewPathTags.length }} 涓珯鐐�</span>
+ <el-button type="text" v-if="hiddenPathTagCount || showAllPathTags" @click="showAllPathTags = !showAllPathTags">{{ showAllPathTags ? '鏀惰捣' : '灞曞紑鍏ㄩ儴' }}</el-button>
+ </div>
+ </div>
+ <div class="route-tag-row">
+ <span class="route-tag" v-for="item in visiblePreviewPathTags" :key="'path-' + item.stationId">{{ item.label }}</span>
+ </div>
+ <div class="route-strip-more" v-if="hiddenPathTagCount && !showAllPathTags">杩樻湁 {{ hiddenPathTagCount }} 涓珯鐐规湭灞曞紑</div>
+ </div>
+
+ <div class="map-legend">
+ <span class="legend-item"><i class="legend-dot" style="background: rgba(255,255,255,0.95); border: 2px solid rgba(165,181,198,0.88);"></i>杈撻�佺珯鐐�</span>
+ <span class="legend-item"><i class="legend-dot" style="background: rgba(31,159,137,0.12); border: 2px solid rgba(31,159,137,0.92);"></i>瀹為檯璺緞</span>
+ <span class="legend-item"><i class="legend-dot" style="background: rgba(47,121,214,0.09); border: 2px solid rgba(47,121,214,0.92);"></i>杞亸濂�</span>
+ <span class="legend-item"><i class="legend-dot" style="background: rgba(220,92,92,0.12); border: 2px solid rgba(220,92,92,0.94);"></i>绂佺敤绔欑偣</span>
+ <span class="legend-item"><i class="legend-dot" style="background: rgba(255,159,28,0.16); border: 2px solid rgba(255,159,28,0.92);"></i>閫旂粡鐐�</span>
+ <span class="legend-item"><i class="legend-dot" style="background: rgba(155,89,182,0.14); border: 2px solid rgba(155,89,182,0.92);"></i>蹇呯粡绔欑偣</span>
+ </div>
+
+ <div class="map-shell">
+ <div class="map-toolbar">
+ <div class="map-toolbar-left">
+ <strong>妤煎眰 {{ activeMapLev || '-' }}</strong>
+ <span style="color: var(--text-sub); font-size: 12px;">鑺傜偣 {{ mapContext.nodes.length }}</span>
+ </div>
+ <div class="map-toolbar-right">
+ <el-button size="mini" @click="centerOnPath" :disabled="!hasActualPath">鑱氱劍璺緞</el-button>
+ </div>
+ </div>
+ <div class="map-canvas-wrap"
+ ref="mapCanvasWrap"
+ :class="{ 'is-dragging': mapDragActive }"
+ @mousedown.left="beginMapDrag"
+ @wheel.prevent="handleMapWheel"
+ @dragstart.prevent>
+ <div v-if="!mapContext.nodes.length" class="empty-shell" style="margin: 14px;">璇烽�夋嫨妤煎眰鎴栨墽琛屼竴娆¤矾寰勯瑙堛��</div>
+ <div v-else class="map-stage" :style="mapStageStyle" ref="mapStage">
+ <svg :width="mapContext.width" :height="mapContext.height">
+ <polyline v-if="preferredPathPolyline" :points="preferredPathPolyline" fill="none" stroke="var(--preferred)" stroke-width="4" stroke-dasharray="10 8" opacity="0.72"></polyline>
+ <polyline v-if="actualPathPolyline" :points="actualPathPolyline" fill="none" stroke="var(--path)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" opacity="0.92"></polyline>
+ </svg>
+ <div v-for="node in renderedMapNodes"
+ :key="'node-' + node.stationId"
+ class="map-node"
+ :class="node.classes"
+ :style="{ left: node.left, top: node.top }"
+ :title="node.title"
+ @click="pickNode(node)">
+ <div class="map-node-label" v-if="node.showLabel">{{ node.stationId }}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="hint-card" v-if="previewResult && !hasActualPath" style="margin-top: 12px;">
+ <strong>褰撳墠鏃犲彲琛岃矾寰�</strong>
+ <span>绯荤粺宸茬粡瑙f瀽鍑哄懡涓鍒欏拰妯℃澘锛屼絾鍦ㄧ幇鏈夊湴鍥俱�佺‖绾︽潫銆侀�旂粡鐐逛笌瀹炴椂鏉′欢涓嬫病鏈夊緱鍒板彲琛岃矾绾裤�傚彲浠ュ厛妫�鏌ョ鐢ㄧ珯鐐广�侀�旂粡鐐归『搴忓拰妤煎眰閫夋嫨銆�</span>
+ </div>
+
+ <div class="rule-json" v-if="showRuleJson && activeRulePreviewJson">
+{{ activeRulePreviewJson }}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <el-dialog title="璺緞妯℃澘" :visible.sync="profileDialogVisible" width="820px" class="dialog-panel" append-to-body :destroy-on-close="true">
+ <div class="dialog-grid">
+ <el-form label-position="top" label-width="120px" style="width: 100%;">
+ <div class="section-card">
+ <h3>鍩虹淇℃伅</h3>
+ <div class="dialog-grid">
+ <el-form-item label="妯℃澘缂栫爜">
+ <el-input v-model.trim="profileForm.profileCode" placeholder="濡� default / loop_safe"></el-input>
+ </el-form-item>
+ <el-form-item label="妯℃澘鍚嶇О">
+ <el-input v-model.trim="profileForm.profileName" placeholder="妯℃澘鍚嶇О"></el-input>
+ </el-form-item>
+ <el-form-item label="浼樺厛绾�">
+ <el-input-number v-model="profileForm.priority" :min="1" :max="9999" style="width: 100%;"></el-input-number>
+ </el-form-item>
+ <el-form-item label="鐘舵��">
+ <el-switch v-model="profileForm.status" :active-value="1" :inactive-value="0"></el-switch>
+ </el-form-item>
+ <el-form-item label="澶囨敞" class="span-2">
+ <el-input v-model.trim="profileForm.memo" placeholder="鍙~鍐欓�傜敤鍦烘櫙"></el-input>
+ </el-form-item>
+ </div>
+ </div>
+
+ <div class="section-card">
+ <h3>鍊欓�夌敓鎴�</h3>
+ <div class="dialog-grid">
+ <el-form-item label="鏈�澶ф繁搴�"><el-input-number v-model="profileForm.config.calcMaxDepth" :min="20" :max="500" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="鏈�澶ц矾寰勬暟"><el-input-number v-model="profileForm.config.calcMaxPaths" :min="10" :max="3000" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="鏈�澶т唬浠�"><el-input-number v-model="profileForm.config.calcMaxCost" :min="10" :max="5000" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="S1 淇濈暀 TopK"><el-input-number v-model="profileForm.config.s1TopK" :min="1" :max="50" style="width: 100%;"></el-input-number></el-form-item>
+ </div>
+ </div>
+
+ <div class="section-card">
+ <h3>绗竴闃舵闈欐�佽瘎鍒�</h3>
+ <div class="dialog-grid">
+ <el-form-item label="闀垮害鏉冮噸"><el-input-number v-model="profileForm.config.s1LenWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="鎷愮偣鏉冮噸"><el-input-number v-model="profileForm.config.s1TurnWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="椤跺崌鏉冮噸"><el-input-number v-model="profileForm.config.s1LiftWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="鍋忕浜哄伐璺緞鏉冮噸"><el-input-number v-model="profileForm.config.s1SoftDeviationWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="闀垮害鏀惧姣斾緥"><el-input-number v-model="profileForm.config.s1MaxLenRatio" :min="1" :step="0.05" :precision="2" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="鍏佽澶氭嫄鐐规暟"><el-input-number v-model="profileForm.config.s1MaxTurnDiff" :min="0" :max="20" style="width: 100%;"></el-input-number></el-form-item>
+ </div>
+ </div>
+
+ <div class="section-card">
+ <h3>绗簩闃舵鍔ㄦ�佽瘎鍒�</h3>
+ <div class="dialog-grid">
+ <el-form-item label="蹇欑珯鏉冮噸"><el-input-number v-model="profileForm.config.s2BusyWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="鍫靛鏉冮噸"><el-input-number v-model="profileForm.config.s2RunBlockWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
+ <el-form-item label="鐜嚎璐熻浇鏉冮噸"><el-input-number v-model="profileForm.config.s2LoopLoadWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number></el-form-item>
+ </div>
+ </div>
+ </el-form>
+ </div>
+ <span slot="footer" class="dialog-footer">
+ <el-button @click="profileDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="confirmProfileDialog">纭畾</el-button>
+ </span>
+ </el-dialog>
+
+ <el-dialog title="浜哄伐瑙勫垯" :visible.sync="ruleDialogVisible" width="980px" class="dialog-panel" append-to-body :modal="false" :close-on-click-modal="false" :lock-scroll="false" :destroy-on-close="true" top="24px">
+ <el-form label-position="top" label-width="120px">
+ <div class="dialog-picked-bar">
+ <div class="help">{{ ruleDialogPickedHint }}</div>
+ <div style="display: flex; gap: 8px; flex-wrap: wrap;">
+ <el-button size="mini" :disabled="!hasPickedStation" @click="applyPickedStation('ruleStart')">甯﹀叆瑙勫垯璧风偣</el-button>
+ <el-button size="mini" :disabled="!hasPickedStation" @click="applyPickedStation('ruleEnd')">甯﹀叆瑙勫垯缁堢偣</el-button>
+ <el-button size="mini" type="primary" plain :disabled="!hasPickedStation" @click="applyPickedStation('mustPass')">鍔犲叆蹇呯粡</el-button>
+ <el-button size="mini" type="danger" plain :disabled="!hasPickedStation" @click="applyPickedStation('forbid')">鍔犲叆绂佺敤</el-button>
+ <el-button size="mini" type="warning" plain :disabled="!hasPickedStation" @click="applyPickedStation('waypoint')">鍔犲叆閫旂粡鐐�</el-button>
+ <el-button size="mini" type="success" plain :disabled="!hasPickedStation" @click="applyPickedStation('preferred')">鍔犲叆鍋忓ソ璺緞</el-button>
+ </div>
+ </div>
+
+ <div class="section-card">
+ <h3>鍩虹淇℃伅</h3>
+ <div class="dialog-grid">
+ <el-form-item label="瑙勫垯缂栫爜">
+ <el-input v-model.trim="ruleForm.ruleCode" placeholder="濡� R-IN-101-221"></el-input>
+ </el-form-item>
+ <el-form-item label="瑙勫垯鍚嶇О">
+ <el-input v-model.trim="ruleForm.ruleName" placeholder="瑙勫垯鍚嶇О"></el-input>
+ </el-form-item>
+ <el-form-item label="浼樺厛绾�">
+ <el-input-number v-model="ruleForm.priority" :min="1" :max="9999" style="width: 100%;"></el-input-number>
+ </el-form-item>
+ <el-form-item label="鐘舵��">
+ <el-switch v-model="ruleForm.status" :active-value="1" :inactive-value="0"></el-switch>
+ </el-form-item>
+ <el-form-item label="璧风偣绔欑偣">
+ <el-select v-model="ruleForm.startStationId" filterable clearable placeholder="鍙暀绌鸿〃绀洪�氶厤">
+ <el-option v-for="item in stationOptions" :key="'rs-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="缁堢偣绔欑偣">
+ <el-select v-model="ruleForm.endStationId" filterable clearable placeholder="鍙暀绌鸿〃绀洪�氶厤">
+ <el-option v-for="item in stationOptions" :key="'re-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="缁戝畾妯℃澘">
+ <el-select v-model="ruleForm.profileCode" filterable clearable placeholder="涓虹┖鏃惰蛋榛樿妯℃澘">
+ <el-option v-for="item in profiles" :key="'rp-' + item.profileCode" :label="item.profileName + ' (' + item.profileCode + ')'" :value="item.profileCode"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍦烘櫙绫诲瀷">
+ <el-input v-model.trim="ruleForm.sceneType" placeholder="鍙�夛紝濡� inbound / outbound"></el-input>
+ </el-form-item>
+ <el-form-item label="澶囨敞" class="span-2">
+ <el-input v-model.trim="ruleForm.memo" placeholder="琛ュ厖璇存槑瑙勫垯鐩殑"></el-input>
+ </el-form-item>
+ </div>
+ </div>
+
+ <div class="section-card">
+ <h3>纭害鏉�</h3>
+ <div class="section-help">纭害鏉熶笉浼氳鍔ㄦ�佽瘎鍒嗘帹缈汇�傛敮鎸佸繀缁忕珯鐐广�佺鐢ㄧ珯鐐广�佸繀缁忚竟銆佺鐢ㄨ竟銆�</div>
+ <div class="dialog-grid">
+ <el-form-item label="蹇呯粡绔欑偣" class="span-2">
+ <el-select v-model="ruleForm.hard.mustPassStations" multiple filterable collapse-tags placeholder="閫夋嫨蹇呯粡绔欑偣">
+ <el-option v-for="item in stationOptions" :key="'must-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ <div class="sequence-card" v-if="ruleForm.hard.mustPassStations.length">
+ <div class="sequence-row" v-for="(stationId, index) in ruleForm.hard.mustPassStations" :key="'must-row-' + stationId + '-' + index">
+ <span class="sequence-index">{{ index + 1 }}</span>
+ <span class="sequence-label">{{ stationLabel(stationId) }}</span>
+ <div class="sequence-actions">
+ <el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.hard.mustPassStations, index)"></el-button>
+ </div>
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="绂佺敤绔欑偣" class="span-2">
+ <el-select v-model="ruleForm.hard.forbidStations" multiple filterable collapse-tags placeholder="閫夋嫨绂佺敤绔欑偣">
+ <el-option v-for="item in stationOptions" :key="'forbid-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ <div class="sequence-card" v-if="ruleForm.hard.forbidStations.length">
+ <div class="sequence-row" v-for="(stationId, index) in ruleForm.hard.forbidStations" :key="'forbid-row-' + stationId + '-' + index">
+ <span class="sequence-index">{{ index + 1 }}</span>
+ <span class="sequence-label">{{ stationLabel(stationId) }}</span>
+ <div class="sequence-actions">
+ <el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.hard.forbidStations, index)"></el-button>
+ </div>
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="蹇呯粡杈�" class="span-2">
+ <el-input type="textarea" :rows="3" v-model="ruleForm.hard.mustPassEdgesText" placeholder="姣忚涓�鏉★紝濡� 101->102"></el-input>
+ </el-form-item>
+ <el-form-item label="绂佺敤杈�" class="span-2">
+ <el-input type="textarea" :rows="3" v-model="ruleForm.hard.forbidEdgesText" placeholder="姣忚涓�鏉★紝濡� 215->216"></el-input>
+ </el-form-item>
+ </div>
+ </div>
+
+ <div class="section-card">
+ <h3>鍏抽敭閫旂粡鐐�</h3>
+ <div class="section-help">鎸夐『搴忕粡杩囪繖浜涚珯鐐广�傝嫢寮�鍚弗鏍兼ā寮忥紝鏈懡涓�旂粡鐐规椂浼氱洿鎺ュ垽瀹氭棤璺緞銆�</div>
+ <div class="dialog-grid">
+ <el-form-item label="閫旂粡鐐瑰簭鍒�" class="span-2">
+ <el-select v-model="ruleForm.waypoint.stations" multiple filterable collapse-tags placeholder="鎸夐『搴忛�夋嫨閫旂粡鐐�">
+ <el-option v-for="item in stationOptions" :key="'wp-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ <div class="sequence-card" v-if="ruleForm.waypoint.stations.length">
+ <div class="sequence-row" v-for="(stationId, index) in ruleForm.waypoint.stations" :key="'waypoint-row-' + stationId + '-' + index">
+ <span class="sequence-index">{{ index + 1 }}</span>
+ <span class="sequence-label">{{ stationLabel(stationId) }}</span>
+ <div class="sequence-actions">
+ <el-button size="mini" plain icon="el-icon-top" :disabled="index === 0" @click="moveListItem(ruleForm.waypoint.stations, index, -1)"></el-button>
+ <el-button size="mini" plain icon="el-icon-bottom" :disabled="index === ruleForm.waypoint.stations.length - 1" @click="moveListItem(ruleForm.waypoint.stations, index, 1)"></el-button>
+ <el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.waypoint.stations, index)"></el-button>
+ </div>
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="涓ユ牸閫旂粡鐐规ā寮�">
+ <el-switch v-model="ruleForm.fallback.strictWaypoint"></el-switch>
+ </el-form-item>
+ </div>
+ </div>
+
+ <div class="section-card">
+ <div class="section-title-row">
+ <h3>杞亸濂借矾寰�</h3>
+ <div class="section-inline-actions">
+ <el-button size="mini" plain @click="importPreviewPathToRule" :disabled="!hasPreviewPath">瀵煎叆褰撳墠棰勮</el-button>
+ <el-button size="mini" type="primary" plain @click="expandRuleSoftPreferredPath" :loading="softExpandLoading">鎸夊叧閿偣灞曞紑</el-button>
+ <el-button size="mini" @click="clearSoftPreferredPath" :disabled="!(ruleForm.soft.keyStations.length || ruleForm.soft.preferredPath.length)">娓呯┖</el-button>
+ </div>
+ </div>
+ <div class="section-help">鐢ㄤ簬琛ㄨ揪鈥滀汉瑙夊緱姝e父搴旇繖鏍疯蛋鈥濈殑鎺ㄨ崘绾胯矾锛岀畻娉曞厑璁稿亸绂伙紝浣嗗亸绂讳細琚綒鍒嗐��</div>
+ <div class="dialog-grid">
+ <el-form-item label="鍏抽敭鐐瑰簭鍒�" class="span-2">
+ <el-select v-model="ruleForm.soft.keyStations" multiple filterable collapse-tags placeholder="鍙�夊叧閿偣锛岀郴缁熶細鑷姩琛ュ叏鏁存潯鍋忓ソ璺緞">
+ <el-option v-for="item in stationOptions" :key="'soft-key-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ <div class="sequence-card" v-if="ruleForm.soft.keyStations.length">
+ <div class="sequence-row" v-for="(stationId, index) in ruleForm.soft.keyStations" :key="'soft-key-row-' + stationId + '-' + index">
+ <span class="sequence-index">{{ index + 1 }}</span>
+ <span class="sequence-label">{{ stationLabel(stationId) }}</span>
+ <div class="sequence-actions">
+ <el-button size="mini" plain icon="el-icon-top" :disabled="index === 0" @click="moveListItem(ruleForm.soft.keyStations, index, -1)"></el-button>
+ <el-button size="mini" plain icon="el-icon-bottom" :disabled="index === ruleForm.soft.keyStations.length - 1" @click="moveListItem(ruleForm.soft.keyStations, index, 1)"></el-button>
+ <el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.soft.keyStations, index)"></el-button>
+ </div>
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="瀹屾暣鍋忓ソ璺緞" class="span-2">
+ <el-select v-model="ruleForm.soft.preferredPath" multiple filterable collapse-tags placeholder="鎸夐『搴忛�夋嫨鍋忓ソ璺緞">
+ <el-option v-for="item in stationOptions" :key="'pref-' + item.stationId" :label="item.label" :value="item.stationId"></el-option>
+ </el-select>
+ <div class="sequence-card" v-if="ruleForm.soft.preferredPath.length">
+ <div class="sequence-row" v-for="(stationId, index) in ruleForm.soft.preferredPath" :key="'pref-row-' + stationId + '-' + index">
+ <span class="sequence-index">{{ index + 1 }}</span>
+ <span class="sequence-label">{{ stationLabel(stationId) }}</span>
+ <div class="sequence-actions">
+ <el-button size="mini" plain icon="el-icon-top" :disabled="index === 0" @click="moveListItem(ruleForm.soft.preferredPath, index, -1)"></el-button>
+ <el-button size="mini" plain icon="el-icon-bottom" :disabled="index === ruleForm.soft.preferredPath.length - 1" @click="moveListItem(ruleForm.soft.preferredPath, index, 1)"></el-button>
+ <el-button size="mini" type="danger" plain icon="el-icon-delete" @click="removeListItem(ruleForm.soft.preferredPath, index)"></el-button>
+ </div>
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="鍋忕缃氬垎鏉冮噸">
+ <el-input-number v-model="ruleForm.soft.deviationWeight" :min="0" :step="0.5" style="width: 100%;"></el-input-number>
+ </el-form-item>
+ <el-form-item label="鍏佽鍋忕绔欑偣鏁�">
+ <el-input-number v-model="ruleForm.soft.maxOffPathCount" :min="0" :max="50" style="width: 100%;"></el-input-number>
+ </el-form-item>
+ <el-form-item label="鍏佽杞亸濂介檷绾�">
+ <el-switch v-model="ruleForm.fallback.allowSoftDegrade"></el-switch>
+ </el-form-item>
+ </div>
+ </div>
+ </el-form>
+ <span slot="footer" class="dialog-footer">
+ <el-button @click="ruleDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="confirmRuleDialog">纭畾</el-button>
+ </span>
+ </el-dialog>
+</div>
+<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></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/common.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/stationPathPolicy/stationPathPolicy.js?v=20260316e"></script>
+</body>
+</html>
diff --git a/tmp/docs/wcs_wms_plan_check.html b/tmp/docs/wcs_wms_plan_check.html
new file mode 100644
index 0000000..a00678b
--- /dev/null
+++ b/tmp/docs/wcs_wms_plan_check.html
@@ -0,0 +1,922 @@
+<!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