#
Administrator
2026-04-25 2378049bb786d9fa6451d43692eb36e17f659f0b
#
26个文件已添加
3828 ■■■■■ 已修改文件
src/main/java/com/zy/ai/controller/LlmCallLogController.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/controller/LlmRouteConfigController.java 330 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/entity/LlmCallLog.java 219 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/entity/LlmRouteConfig.java 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/mapper/LlmCallLogMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/mapper/LlmRouteConfigMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/LlmCallLogService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/LlmRouteConfigService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/LlmRoutingService.java 286 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/impl/LlmCallLogServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/impl/LlmRouteConfigServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/param/DualCrnUpdateTaskNoParam.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/param/StationCommandBarcodeParam.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/vo/StationCycleCapacityVo.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/vo/StationCycleLoopVo.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/StationCycleCapacityService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/StationCycleCapacityServiceImpl.java 623 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/StationCycleCapacityScheduler.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/ZyStationV4FakeSegConnect.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/real/ZyStationV4RealConnect.java 335 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/ZyStationV4Thread.java 478 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseUtils.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/docs/~$S外部HTTP API接口V1.7.docx 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/map/~$小松地图.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260109084743.nb3 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/ai/llm_config.html 1014 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/controller/LlmCallLogController.java
New file
@@ -0,0 +1,65 @@
package com.zy.ai.controller;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.plugins.Page;
import com.core.annotations.ManagerAuth;
import com.core.common.R;
import com.zy.ai.entity.LlmCallLog;
import com.zy.ai.service.LlmCallLogService;
import com.zy.common.web.BaseController;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/ai/llm/log")
@RequiredArgsConstructor
public class LlmCallLogController extends BaseController {
    private final LlmCallLogService llmCallLogService;
    @GetMapping("/list/auth")
    @ManagerAuth
    public R list(@RequestParam(defaultValue = "1") Integer curr,
                  @RequestParam(defaultValue = "20") Integer limit,
                  @RequestParam(required = false) String scene,
                  @RequestParam(required = false) Integer success,
                  @RequestParam(required = false) Long routeId,
                  @RequestParam(required = false) String traceId) {
        EntityWrapper<LlmCallLog> wrapper = new EntityWrapper<>();
        if (!isBlank(scene)) {
            wrapper.eq("scene", scene.trim());
        }
        if (success != null) {
            wrapper.eq("success", success);
        }
        if (routeId != null) {
            wrapper.eq("route_id", routeId);
        }
        if (!isBlank(traceId)) {
            wrapper.eq("trace_id", traceId.trim());
        }
        wrapper.orderBy("id", false);
        return R.ok(llmCallLogService.selectPage(new Page<>(curr, limit), wrapper));
    }
    @PostMapping("/delete/auth")
    @ManagerAuth
    public R delete(@RequestParam("id") Long id) {
        if (id == null) {
            return R.error("id不能为空");
        }
        llmCallLogService.deleteById(id);
        return R.ok();
    }
    @PostMapping("/clear/auth")
    @ManagerAuth
    public R clear() {
        llmCallLogService.delete(new EntityWrapper<LlmCallLog>());
        return R.ok();
    }
    private boolean isBlank(String s) {
        return s == null || s.trim().isEmpty();
    }
}
src/main/java/com/zy/ai/controller/LlmRouteConfigController.java
New file
@@ -0,0 +1,330 @@
package com.zy.ai.controller;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.annotations.ManagerAuth;
import com.core.common.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zy.ai.entity.LlmRouteConfig;
import com.zy.ai.service.LlmRouteConfigService;
import com.zy.ai.service.LlmRoutingService;
import com.zy.common.web.BaseController;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/ai/llm/config")
@RequiredArgsConstructor
public class LlmRouteConfigController extends BaseController {
    private final LlmRouteConfigService llmRouteConfigService;
    private final LlmRoutingService llmRoutingService;
    private final ObjectMapper objectMapper;
    @GetMapping("/list/auth")
    @ManagerAuth
    public R list() {
        EntityWrapper<LlmRouteConfig> wrapper = new EntityWrapper<>();
        wrapper.orderBy("priority", true).orderBy("id", true);
        List<LlmRouteConfig> list = llmRouteConfigService.selectList(wrapper);
        return R.ok(list);
    }
    @PostMapping("/save/auth")
    @ManagerAuth
    public R save(@RequestBody LlmRouteConfig config) {
        if (config == null) {
            return R.error("参数不能为空");
        }
        if (isBlank(config.getBaseUrl()) || isBlank(config.getApiKey()) || isBlank(config.getModel())) {
            return R.error("必须填写 baseUrl/apiKey/model");
        }
        if (config.getId() == null) {
            llmRoutingService.fillAndNormalize(config, true);
            llmRouteConfigService.insert(config);
        } else {
            LlmRouteConfig db = llmRouteConfigService.selectById(config.getId());
            if (db == null) {
                return R.error("配置不存在");
            }
            // ä¿ç•™ç»Ÿè®¡å­—段,避免前端误覆盖
            Integer failCount = db.getFailCount();
            Integer successCount = db.getSuccessCount();
            Integer consecutiveFailCount = db.getConsecutiveFailCount();
            Date lastFailTime = db.getLastFailTime();
            Date lastUsedTime = db.getLastUsedTime();
            String lastError = db.getLastError();
            llmRoutingService.fillAndNormalize(config, false);
            config.setFailCount(failCount);
            config.setSuccessCount(successCount);
            config.setConsecutiveFailCount(consecutiveFailCount);
            config.setLastFailTime(lastFailTime);
            config.setLastUsedTime(lastUsedTime);
            config.setLastError(lastError);
            config.setCreateTime(db.getCreateTime());
            llmRouteConfigService.updateById(config);
        }
        llmRoutingService.evictCache();
        return R.ok(config);
    }
    @PostMapping("/delete/auth")
    @ManagerAuth
    public R delete(@RequestParam("id") Long id) {
        if (id == null) {
            return R.error("id不能为空");
        }
        llmRouteConfigService.deleteById(id);
        llmRoutingService.evictCache();
        return R.ok();
    }
    @PostMapping("/clearCooldown/auth")
    @ManagerAuth
    public R clearCooldown(@RequestParam("id") Long id) {
        if (id == null) {
            return R.error("id不能为空");
        }
        LlmRouteConfig cfg = llmRouteConfigService.selectById(id);
        if (cfg == null) {
            return R.error("配置不存在");
        }
        cfg.setCooldownUntil(null);
        cfg.setConsecutiveFailCount(0);
        cfg.setUpdateTime(new Date());
        llmRouteConfigService.updateById(cfg);
        llmRoutingService.evictCache();
        return R.ok();
    }
    @PostMapping("/test/auth")
    @ManagerAuth
    public R test(@RequestBody LlmRouteConfig config) {
        if (config == null) {
            return R.error("参数不能为空");
        }
        if (isBlank(config.getBaseUrl()) || isBlank(config.getApiKey()) || isBlank(config.getModel())) {
            return R.error("测试失败:必须填写 baseUrl/apiKey/model");
        }
        Map<String, Object> data = llmRoutingService.testRoute(config);
        if (Boolean.TRUE.equals(data.get("ok")) && config.getId() != null) {
            LlmRouteConfig db = llmRouteConfigService.selectById(config.getId());
            if (db != null) {
                db.setCooldownUntil(null);
                db.setConsecutiveFailCount(0);
                db.setUpdateTime(new Date());
                llmRouteConfigService.updateById(db);
                llmRoutingService.evictCache();
            }
        }
        return R.ok(data);
    }
    @GetMapping("/export/auth")
    @ManagerAuth
    public R exportConfig() {
        EntityWrapper<LlmRouteConfig> wrapper = new EntityWrapper<>();
        wrapper.orderBy("priority", true).orderBy("id", true);
        List<LlmRouteConfig> list = llmRouteConfigService.selectList(wrapper);
        List<Map<String, Object>> routes = new ArrayList<>();
        if (list != null) {
            for (LlmRouteConfig cfg : list) {
                routes.add(exportRow(cfg));
            }
        }
        HashMap<String, Object> result = new HashMap<>();
        result.put("version", "1.0");
        result.put("exportTime", new Date());
        result.put("count", routes.size());
        result.put("routes", routes);
        return R.ok(result);
    }
    @PostMapping("/import/auth")
    @ManagerAuth
    @Transactional(rollbackFor = Exception.class)
    public R importConfig(@RequestBody Object body) {
        boolean replace = false;
        List<?> rawRoutes = null;
        if (body instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) body;
            replace = parseBoolean(map.get("replace"));
            Object routesObj = map.get("routes");
            if (routesObj instanceof List) {
                rawRoutes = (List<?>) routesObj;
            }
        } else if (body instanceof List) {
            rawRoutes = (List<?>) body;
        }
        if (rawRoutes == null || rawRoutes.isEmpty()) {
            return R.error("导入数据为空或格式不正确,必须包含 routes æ•°ç»„");
        }
        int inserted = 0;
        int updated = 0;
        int skipped = 0;
        List<String> errors = new ArrayList<>();
        List<LlmRouteConfig> validRoutes = new ArrayList<>();
        for (int i = 0; i < rawRoutes.size(); i++) {
            Object row = rawRoutes.get(i);
            LlmRouteConfig cfg;
            try {
                cfg = objectMapper.convertValue(row, LlmRouteConfig.class);
            } catch (Exception e) {
                skipped++;
                errors.add("第" + (i + 1) + "条解析失败: " + safeMsg(e.getMessage()));
                continue;
            }
            if (cfg == null) {
                skipped++;
                errors.add("第" + (i + 1) + "条为空");
                continue;
            }
            cfg.setName(trim(cfg.getName()));
            cfg.setBaseUrl(trim(cfg.getBaseUrl()));
            cfg.setApiKey(trim(cfg.getApiKey()));
            cfg.setModel(trim(cfg.getModel()));
            cfg.setMemo(trim(cfg.getMemo()));
            if (isBlank(cfg.getBaseUrl()) || isBlank(cfg.getApiKey()) || isBlank(cfg.getModel())) {
                skipped++;
                errors.add("第" + (i + 1) + "条缺少必填字段 baseUrl/apiKey/model");
                continue;
            }
            validRoutes.add(cfg);
        }
        if (validRoutes.isEmpty()) {
            String firstError = errors.isEmpty() ? "" : (",首条原因:" + errors.get(0));
            return R.error("导入失败:没有可用配置" + firstError);
        }
        if (replace) {
            llmRouteConfigService.delete(new EntityWrapper<LlmRouteConfig>());
        }
        HashMap<Long, LlmRouteConfig> dbById = new HashMap<>();
        if (!replace) {
            List<LlmRouteConfig> current = llmRouteConfigService.selectList(new EntityWrapper<>());
            if (current != null) {
                for (LlmRouteConfig item : current) {
                    if (item != null && item.getId() != null) {
                        dbById.put(item.getId(), item);
                    }
                }
            }
        }
        for (LlmRouteConfig cfg : validRoutes) {
            if (!replace && cfg.getId() != null && dbById.containsKey(cfg.getId())) {
                LlmRouteConfig db = dbById.get(cfg.getId());
                Long keepId = db.getId();
                Date createTime = db.getCreateTime();
                Integer failCount = db.getFailCount();
                Integer successCount = db.getSuccessCount();
                Integer consecutiveFailCount = db.getConsecutiveFailCount();
                Date lastFailTime = db.getLastFailTime();
                Date lastUsedTime = db.getLastUsedTime();
                String lastError = db.getLastError();
                Date cooldownUntil = db.getCooldownUntil();
                llmRoutingService.fillAndNormalize(cfg, false);
                cfg.setId(keepId);
                cfg.setCreateTime(createTime);
                cfg.setFailCount(failCount);
                cfg.setSuccessCount(successCount);
                cfg.setConsecutiveFailCount(consecutiveFailCount);
                cfg.setLastFailTime(lastFailTime);
                cfg.setLastUsedTime(lastUsedTime);
                cfg.setLastError(lastError);
                cfg.setCooldownUntil(cooldownUntil);
                llmRouteConfigService.updateById(cfg);
                updated++;
                continue;
            }
            cfg.setId(null);
            cfg.setCooldownUntil(null);
            cfg.setFailCount(0);
            cfg.setSuccessCount(0);
            cfg.setConsecutiveFailCount(0);
            cfg.setLastFailTime(null);
            cfg.setLastUsedTime(null);
            cfg.setLastError(null);
            llmRoutingService.fillAndNormalize(cfg, true);
            llmRouteConfigService.insert(cfg);
            inserted++;
        }
        llmRoutingService.evictCache();
        HashMap<String, Object> result = new HashMap<>();
        result.put("replace", replace);
        result.put("total", rawRoutes.size());
        result.put("inserted", inserted);
        result.put("updated", updated);
        result.put("skipped", skipped);
        result.put("errorCount", errors.size());
        if (!errors.isEmpty()) {
            int max = Math.min(errors.size(), 20);
            result.put("errors", errors.subList(0, max));
        }
        log.info("LLM路由导入完成, replace={}, total={}, inserted={}, updated={}, skipped={}",
                replace, rawRoutes.size(), inserted, updated, skipped);
        return R.ok(result);
    }
    private Map<String, Object> exportRow(LlmRouteConfig cfg) {
        LinkedHashMap<String, Object> row = new LinkedHashMap<>();
        row.put("id", cfg.getId());
        row.put("name", cfg.getName());
        row.put("baseUrl", cfg.getBaseUrl());
        row.put("apiKey", cfg.getApiKey());
        row.put("model", cfg.getModel());
        row.put("thinking", cfg.getThinking());
        row.put("priority", cfg.getPriority());
        row.put("status", cfg.getStatus());
        row.put("switchOnQuota", cfg.getSwitchOnQuota());
        row.put("switchOnError", cfg.getSwitchOnError());
        row.put("cooldownSeconds", cfg.getCooldownSeconds());
        row.put("memo", cfg.getMemo());
        return row;
    }
    private boolean parseBoolean(Object x) {
        if (x instanceof Boolean) return (Boolean) x;
        if (x == null) return false;
        String s = String.valueOf(x).trim();
        return "1".equals(s) || "true".equalsIgnoreCase(s) || "yes".equalsIgnoreCase(s);
    }
    private String trim(String s) {
        return s == null ? null : s.trim();
    }
    private String safeMsg(String s) {
        if (s == null) return "";
        return s.length() > 200 ? s.substring(0, 200) : s;
    }
    private boolean isBlank(String s) {
        return s == null || s.trim().isEmpty();
    }
}
src/main/java/com/zy/ai/entity/LlmCallLog.java
New file
@@ -0,0 +1,219 @@
package com.zy.ai.entity;
import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableName;
import com.baomidou.mybatisplus.enums.IdType;
import java.io.Serializable;
import java.util.Date;
@TableName("sys_llm_call_log")
public class LlmCallLog implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @TableField("trace_id")
    private String traceId;
    private String scene;
    private Short stream;
    @TableField("attempt_no")
    private Integer attemptNo;
    @TableField("route_id")
    private Long routeId;
    @TableField("route_name")
    private String routeName;
    @TableField("base_url")
    private String baseUrl;
    private String model;
    private Short success;
    @TableField("http_status")
    private Integer httpStatus;
    @TableField("latency_ms")
    private Long latencyMs;
    @TableField("switch_mode")
    private String switchMode;
    @TableField("request_content")
    private String requestContent;
    @TableField("response_content")
    private String responseContent;
    @TableField("error_type")
    private String errorType;
    @TableField("error_message")
    private String errorMessage;
    private String extra;
    @TableField("create_time")
    private Date createTime;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getTraceId() {
        return traceId;
    }
    public void setTraceId(String traceId) {
        this.traceId = traceId;
    }
    public String getScene() {
        return scene;
    }
    public void setScene(String scene) {
        this.scene = scene;
    }
    public Short getStream() {
        return stream;
    }
    public void setStream(Short stream) {
        this.stream = stream;
    }
    public Integer getAttemptNo() {
        return attemptNo;
    }
    public void setAttemptNo(Integer attemptNo) {
        this.attemptNo = attemptNo;
    }
    public Long getRouteId() {
        return routeId;
    }
    public void setRouteId(Long routeId) {
        this.routeId = routeId;
    }
    public String getRouteName() {
        return routeName;
    }
    public void setRouteName(String routeName) {
        this.routeName = routeName;
    }
    public String getBaseUrl() {
        return baseUrl;
    }
    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }
    public String getModel() {
        return model;
    }
    public void setModel(String model) {
        this.model = model;
    }
    public Short getSuccess() {
        return success;
    }
    public void setSuccess(Short success) {
        this.success = success;
    }
    public Integer getHttpStatus() {
        return httpStatus;
    }
    public void setHttpStatus(Integer httpStatus) {
        this.httpStatus = httpStatus;
    }
    public Long getLatencyMs() {
        return latencyMs;
    }
    public void setLatencyMs(Long latencyMs) {
        this.latencyMs = latencyMs;
    }
    public String getSwitchMode() {
        return switchMode;
    }
    public void setSwitchMode(String switchMode) {
        this.switchMode = switchMode;
    }
    public String getRequestContent() {
        return requestContent;
    }
    public void setRequestContent(String requestContent) {
        this.requestContent = requestContent;
    }
    public String getResponseContent() {
        return responseContent;
    }
    public void setResponseContent(String responseContent) {
        this.responseContent = responseContent;
    }
    public String getErrorType() {
        return errorType;
    }
    public void setErrorType(String errorType) {
        this.errorType = errorType;
    }
    public String getErrorMessage() {
        return errorMessage;
    }
    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }
    public String getExtra() {
        return extra;
    }
    public void setExtra(String extra) {
        this.extra = extra;
    }
    public Date getCreateTime() {
        return createTime;
    }
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}
src/main/java/com/zy/ai/entity/LlmRouteConfig.java
New file
@@ -0,0 +1,249 @@
package com.zy.ai.entity;
import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableName;
import com.baomidou.mybatisplus.enums.IdType;
import java.io.Serializable;
import java.util.Date;
@TableName("sys_llm_route")
public class LlmRouteConfig implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String name;
    @TableField("base_url")
    private String baseUrl;
    @TableField("api_key")
    private String apiKey;
    private String model;
    /**
     * 1 å¼€å¯æ·±åº¦æ€è€ƒ 0 å…³é—­
     */
    private Short thinking;
    /**
     * æ•°å­—越小优先级越高
     */
    private Integer priority;
    /**
     * 1 å¯ç”¨ 0 ç¦ç”¨
     */
    private Short status;
    @TableField("switch_on_quota")
    private Short switchOnQuota;
    @TableField("switch_on_error")
    private Short switchOnError;
    @TableField("cooldown_seconds")
    private Integer cooldownSeconds;
    @TableField("cooldown_until")
    private Date cooldownUntil;
    @TableField("fail_count")
    private Integer failCount;
    @TableField("success_count")
    private Integer successCount;
    @TableField("consecutive_fail_count")
    private Integer consecutiveFailCount;
    @TableField("last_error")
    private String lastError;
    @TableField("last_used_time")
    private Date lastUsedTime;
    @TableField("last_fail_time")
    private Date lastFailTime;
    @TableField("create_time")
    private Date createTime;
    @TableField("update_time")
    private Date updateTime;
    private String memo;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getBaseUrl() {
        return baseUrl;
    }
    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }
    public String getApiKey() {
        return apiKey;
    }
    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }
    public String getModel() {
        return model;
    }
    public void setModel(String model) {
        this.model = model;
    }
    public Short getThinking() {
        return thinking;
    }
    public void setThinking(Short thinking) {
        this.thinking = thinking;
    }
    public Integer getPriority() {
        return priority;
    }
    public void setPriority(Integer priority) {
        this.priority = priority;
    }
    public Short getStatus() {
        return status;
    }
    public void setStatus(Short status) {
        this.status = status;
    }
    public Short getSwitchOnQuota() {
        return switchOnQuota;
    }
    public void setSwitchOnQuota(Short switchOnQuota) {
        this.switchOnQuota = switchOnQuota;
    }
    public Short getSwitchOnError() {
        return switchOnError;
    }
    public void setSwitchOnError(Short switchOnError) {
        this.switchOnError = switchOnError;
    }
    public Integer getCooldownSeconds() {
        return cooldownSeconds;
    }
    public void setCooldownSeconds(Integer cooldownSeconds) {
        this.cooldownSeconds = cooldownSeconds;
    }
    public Date getCooldownUntil() {
        return cooldownUntil;
    }
    public void setCooldownUntil(Date cooldownUntil) {
        this.cooldownUntil = cooldownUntil;
    }
    public Integer getFailCount() {
        return failCount;
    }
    public void setFailCount(Integer failCount) {
        this.failCount = failCount;
    }
    public Integer getSuccessCount() {
        return successCount;
    }
    public void setSuccessCount(Integer successCount) {
        this.successCount = successCount;
    }
    public Integer getConsecutiveFailCount() {
        return consecutiveFailCount;
    }
    public void setConsecutiveFailCount(Integer consecutiveFailCount) {
        this.consecutiveFailCount = consecutiveFailCount;
    }
    public String getLastError() {
        return lastError;
    }
    public void setLastError(String lastError) {
        this.lastError = lastError;
    }
    public Date getLastUsedTime() {
        return lastUsedTime;
    }
    public void setLastUsedTime(Date lastUsedTime) {
        this.lastUsedTime = lastUsedTime;
    }
    public Date getLastFailTime() {
        return lastFailTime;
    }
    public void setLastFailTime(Date lastFailTime) {
        this.lastFailTime = lastFailTime;
    }
    public Date getCreateTime() {
        return createTime;
    }
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
    public Date getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
    public String getMemo() {
        return memo;
    }
    public void setMemo(String memo) {
        this.memo = memo;
    }
}
src/main/java/com/zy/ai/mapper/LlmCallLogMapper.java
New file
@@ -0,0 +1,11 @@
package com.zy.ai.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.zy.ai.entity.LlmCallLog;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface LlmCallLogMapper extends BaseMapper<LlmCallLog> {
}
src/main/java/com/zy/ai/mapper/LlmRouteConfigMapper.java
New file
@@ -0,0 +1,11 @@
package com.zy.ai.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.zy.ai.entity.LlmRouteConfig;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface LlmRouteConfigMapper extends BaseMapper<LlmRouteConfig> {
}
src/main/java/com/zy/ai/service/LlmCallLogService.java
New file
@@ -0,0 +1,8 @@
package com.zy.ai.service;
import com.baomidou.mybatisplus.service.IService;
import com.zy.ai.entity.LlmCallLog;
public interface LlmCallLogService extends IService<LlmCallLog> {
    void saveIgnoreError(LlmCallLog log);
}
src/main/java/com/zy/ai/service/LlmRouteConfigService.java
New file
@@ -0,0 +1,7 @@
package com.zy.ai.service;
import com.baomidou.mybatisplus.service.IService;
import com.zy.ai.entity.LlmRouteConfig;
public interface LlmRouteConfigService extends IService<LlmRouteConfig> {
}
src/main/java/com/zy/ai/service/LlmRoutingService.java
New file
@@ -0,0 +1,286 @@
package com.zy.ai.service;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.zy.ai.entity.LlmRouteConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class LlmRoutingService {
    private static final long CACHE_TTL_MS = 3000L;
    private final LlmRouteConfigService llmRouteConfigService;
    private volatile List<LlmRouteConfig> allRouteCache = Collections.emptyList();
    private volatile long cacheExpireAt = 0L;
    private static final Comparator<LlmRouteConfig> ROUTE_ORDER = (a, b) -> {
        int pa = a == null || a.getPriority() == null ? Integer.MAX_VALUE : a.getPriority();
        int pb = b == null || b.getPriority() == null ? Integer.MAX_VALUE : b.getPriority();
        if (pa != pb) return Integer.compare(pa, pb);
        long ia = a == null || a.getId() == null ? Long.MAX_VALUE : a.getId();
        long ib = b == null || b.getId() == null ? Long.MAX_VALUE : b.getId();
        return Long.compare(ia, ib);
    };
    public void evictCache() {
        cacheExpireAt = 0L;
    }
    public List<LlmRouteConfig> listAllOrdered() {
        return new ArrayList<>(loadAllRoutes());
    }
    public List<LlmRouteConfig> listAvailableRoutes() {
        Date now = new Date();
        List<LlmRouteConfig> result = new ArrayList<>();
        List<LlmRouteConfig> coolingRoutes = new ArrayList<>();
        int total = 0;
        int disabled = 0;
        int invalid = 0;
        for (LlmRouteConfig c : loadAllRoutes()) {
            total++;
            if (!isEnabled(c)) {
                disabled++;
                continue;
            }
            if (isBlank(c.getBaseUrl()) || isBlank(c.getApiKey()) || isBlank(c.getModel())) {
                invalid++;
                continue;
            }
            if (isCooling(c, now)) {
                coolingRoutes.add(c);
                continue;
            }
            result.add(c);
        }
        if (result.isEmpty() && !coolingRoutes.isEmpty()) {
            // é¿å…æ‰€æœ‰è·¯ç”±éƒ½å¤„于冷却时系统完全不可用,降级允许使用冷却路由
            coolingRoutes.sort(ROUTE_ORDER);
            log.warn("LLM è·¯ç”±å‡å¤„于冷却,降级启用冷却路由。cooling={}, total={}", coolingRoutes.size(), total);
            return coolingRoutes;
        }
        result.sort(ROUTE_ORDER);
        if (result.isEmpty()) {
            log.warn("未找到可用 LLM è·¯ç”±ã€‚total={}, disabled={}, invalid={}", total, disabled, invalid);
        }
        return result;
    }
    public void markSuccess(Long routeId) {
        if (routeId == null) return;
        try {
            LlmRouteConfig db = llmRouteConfigService.selectById(routeId);
            if (db == null) return;
            db.setSuccessCount(nvl(db.getSuccessCount()) + 1);
            db.setConsecutiveFailCount(0);
            db.setLastUsedTime(new Date());
            db.setUpdateTime(new Date());
            llmRouteConfigService.updateById(db);
            evictCache();
        } catch (Exception e) {
            log.warn("更新路由成功状态失败, routeId={}", routeId, e);
        }
    }
    public void markFailure(Long routeId, String errorText, boolean enterCooldown, Integer cooldownSeconds) {
        if (routeId == null) return;
        try {
            LlmRouteConfig db = llmRouteConfigService.selectById(routeId);
            if (db == null) return;
            Date now = new Date();
            db.setFailCount(nvl(db.getFailCount()) + 1);
            db.setConsecutiveFailCount(nvl(db.getConsecutiveFailCount()) + 1);
            db.setLastFailTime(now);
            db.setLastError(trimError(errorText));
            if (enterCooldown) {
                int sec = cooldownSeconds != null && cooldownSeconds > 0
                        ? cooldownSeconds
                        : defaultCooldown(db.getCooldownSeconds());
                db.setCooldownUntil(new Date(now.getTime() + sec * 1000L));
            }
            db.setUpdateTime(now);
            llmRouteConfigService.updateById(db);
            evictCache();
        } catch (Exception e) {
            log.warn("更新路由失败状态失败, routeId={}", routeId, e);
        }
    }
    private int defaultCooldown(Integer sec) {
        return sec == null || sec <= 0 ? 300 : sec;
    }
    private String trimError(String err) {
        if (err == null) return null;
        String x = err.replace("\n", " ").replace("\r", " ");
        return x.length() > 500 ? x.substring(0, 500) : x;
    }
    private Integer nvl(Integer x) {
        return x == null ? 0 : x;
    }
    private boolean isEnabled(LlmRouteConfig c) {
        return c != null && c.getStatus() != null && c.getStatus() == 1;
    }
    private boolean isCooling(LlmRouteConfig c, Date now) {
        return c != null && c.getCooldownUntil() != null && c.getCooldownUntil().after(now);
    }
    private List<LlmRouteConfig> loadAllRoutes() {
        long now = System.currentTimeMillis();
        if (now < cacheExpireAt && allRouteCache != null) {
            return allRouteCache;
        }
        synchronized (this) {
            now = System.currentTimeMillis();
            if (now < cacheExpireAt && allRouteCache != null) {
                return allRouteCache;
            }
            EntityWrapper<LlmRouteConfig> wrapper = new EntityWrapper<>();
            wrapper.orderBy("priority", true).orderBy("id", true);
            List<LlmRouteConfig> list = llmRouteConfigService.selectList(wrapper);
            if (list == null) {
                allRouteCache = Collections.emptyList();
            } else {
                list.sort(ROUTE_ORDER);
                allRouteCache = list;
            }
            cacheExpireAt = System.currentTimeMillis() + CACHE_TTL_MS;
            return allRouteCache;
        }
    }
    private String safe(String s) {
        return s == null ? "" : s.trim();
    }
    private boolean isBlank(String s) {
        return s == null || s.trim().isEmpty();
    }
    public LlmRouteConfig fillAndNormalize(LlmRouteConfig cfg, boolean isCreate) {
        Date now = new Date();
        if (isBlank(cfg.getName())) {
            cfg.setName("LLM_ROUTE_" + now.getTime());
        }
        if (cfg.getThinking() == null) {
            cfg.setThinking((short) 0);
        }
        if (cfg.getPriority() == null) {
            cfg.setPriority(100);
        }
        if (cfg.getStatus() == null) {
            cfg.setStatus((short) 1);
        }
        if (cfg.getSwitchOnQuota() == null) {
            cfg.setSwitchOnQuota((short) 1);
        }
        if (cfg.getSwitchOnError() == null) {
            cfg.setSwitchOnError((short) 1);
        }
        if (cfg.getCooldownSeconds() == null || cfg.getCooldownSeconds() < 0) {
            cfg.setCooldownSeconds(300);
        }
        if (cfg.getFailCount() == null) {
            cfg.setFailCount(0);
        }
        if (cfg.getSuccessCount() == null) {
            cfg.setSuccessCount(0);
        }
        if (cfg.getConsecutiveFailCount() == null) {
            cfg.setConsecutiveFailCount(0);
        }
        if (isCreate) {
            cfg.setCreateTime(now);
        }
        cfg.setUpdateTime(now);
        return cfg;
    }
    public Map<String, Object> testRoute(LlmRouteConfig cfg) {
        HashMap<String, Object> result = new HashMap<>();
        long start = System.currentTimeMillis();
        try {
            TestHttpResult raw = testJavaRoute(cfg);
            fillTestResult(result, raw, start);
        } catch (Exception e) {
            result.put("ok", false);
            result.put("statusCode", -1);
            result.put("latencyMs", System.currentTimeMillis() - start);
            result.put("message", "测试异常: " + safe(e.getMessage()));
            result.put("responseSnippet", "");
        }
        return result;
    }
    private void fillTestResult(HashMap<String, Object> result, TestHttpResult raw, long start) {
        boolean ok = raw.statusCode >= 200 && raw.statusCode < 300;
        result.put("ok", ok);
        result.put("statusCode", raw.statusCode);
        result.put("latencyMs", System.currentTimeMillis() - start);
        result.put("message", ok ? "测试成功" : "测试失败");
        result.put("responseSnippet", trimBody(raw.body));
    }
    private TestHttpResult testJavaRoute(LlmRouteConfig cfg) {
        HashMap<String, Object> req = new HashMap<>();
        req.put("model", cfg.getModel());
        List<Map<String, String>> messages = new ArrayList<>();
        HashMap<String, String> msg = new HashMap<>();
        msg.put("role", "user");
        msg.put("content", "ping");
        messages.add(msg);
        req.put("messages", messages);
        req.put("stream", false);
        req.put("max_tokens", 8);
        req.put("temperature", 0);
        WebClient client = WebClient.builder().baseUrl(cfg.getBaseUrl()).build();
        return client.post()
                .uri("/chat/completions")
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + cfg.getApiKey())
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)
                .bodyValue(req)
                .exchangeToMono(resp -> resp.bodyToMono(String.class)
                        .defaultIfEmpty("")
                        .map(body -> new TestHttpResult(resp.rawStatusCode(), body)))
                .timeout(Duration.ofSeconds(12))
                .onErrorResume(ex -> Mono.just(new TestHttpResult(-1, safe(ex.getMessage()))))
                .block();
    }
    private String trimBody(String body) {
        String x = safe(body).replace("\r", " ").replace("\n", " ");
        return x.length() > 300 ? x.substring(0, 300) : x;
    }
    private static class TestHttpResult {
        private final int statusCode;
        private final String body;
        private TestHttpResult(int statusCode, String body) {
            this.statusCode = statusCode;
            this.body = body;
        }
    }
}
src/main/java/com/zy/ai/service/impl/LlmCallLogServiceImpl.java
New file
@@ -0,0 +1,33 @@
package com.zy.ai.service.impl;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.zy.ai.entity.LlmCallLog;
import com.zy.ai.mapper.LlmCallLogMapper;
import com.zy.ai.service.LlmCallLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service("llmCallLogService")
@Slf4j
public class LlmCallLogServiceImpl extends ServiceImpl<LlmCallLogMapper, LlmCallLog> implements LlmCallLogService {
    private volatile boolean disabled = false;
    @Override
    public void saveIgnoreError(LlmCallLog logItem) {
        if (logItem == null || disabled) {
            return;
        }
        try {
            insert(logItem);
        } catch (Exception e) {
            String msg = e.getMessage() == null ? "" : e.getMessage();
            if (msg.contains("doesn't exist") || msg.contains("不存在")) {
                disabled = true;
                log.warn("LLM调用日志表不存在,日志记录已自动关闭,请先执行建表SQL");
                return;
            }
            log.warn("写入LLM调用日志失败: {}", msg);
        }
    }
}
src/main/java/com/zy/ai/service/impl/LlmRouteConfigServiceImpl.java
New file
@@ -0,0 +1,11 @@
package com.zy.ai.service.impl;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.zy.ai.entity.LlmRouteConfig;
import com.zy.ai.mapper.LlmRouteConfigMapper;
import com.zy.ai.service.LlmRouteConfigService;
import org.springframework.stereotype.Service;
@Service("llmRouteConfigService")
public class LlmRouteConfigServiceImpl extends ServiceImpl<LlmRouteConfigMapper, LlmRouteConfig> implements LlmRouteConfigService {
}
src/main/java/com/zy/asrs/domain/param/DualCrnUpdateTaskNoParam.java
New file
@@ -0,0 +1,10 @@
package com.zy.asrs.domain.param;
import lombok.Data;
@Data
public class DualCrnUpdateTaskNoParam {
    private Integer crnNo;
    private Integer station;
    private Integer taskNo;
}
src/main/java/com/zy/asrs/domain/param/StationCommandBarcodeParam.java
New file
@@ -0,0 +1,14 @@
package com.zy.asrs.domain.param;
import lombok.Data;
@Data
public class StationCommandBarcodeParam {
    // ç«™ç‚¹ç¼–号
    private Integer stationId;
    // æ¡ç å€¼ï¼ˆå…è®¸ä¸ºç©ºå­—符串)
    private String barcode;
}
src/main/java/com/zy/asrs/domain/vo/StationCycleCapacityVo.java
New file
@@ -0,0 +1,29 @@
package com.zy.asrs.domain.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Data
public class StationCycleCapacityVo {
    // å¾ªçŽ¯åœˆæ˜Žç»†
    private List<StationCycleLoopVo> loopList = new ArrayList<>();
    // å¾ªçŽ¯åœˆæ•°é‡
    private Integer loopCount = 0;
    // å¾ªçŽ¯åœˆç«™ç‚¹æ€»æ•°
    private Integer totalStationCount = 0;
    // å¾ªçŽ¯åœˆä¸­æœ‰ä»»åŠ¡ç«™ç‚¹æ€»æ•°
    private Integer taskStationCount = 0;
    // å½“前承载量(0-1):当前任务数 / å¾ªçŽ¯åœˆæ€»ç«™ç‚¹æ•°
    private Double currentLoad = 0.0;
    // æœ€æ–°åˆ·æ–°æ—¶é—´
    private Date refreshTime;
}
src/main/java/com/zy/asrs/domain/vo/StationCycleLoopVo.java
New file
@@ -0,0 +1,28 @@
package com.zy.asrs.domain.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class StationCycleLoopVo {
    // å¾ªçŽ¯åœˆåºå·ï¼ˆä»Ž1开始)
    private Integer loopNo;
    // å¾ªçŽ¯åœˆå†…ç«™ç‚¹ç¼–å·
    private List<Integer> stationIdList = new ArrayList<>();
    // å¾ªçŽ¯åœˆå†…å­˜åœ¨çš„å·¥ä½œå·
    private List<Integer> workNoList = new ArrayList<>();
    // å¾ªçŽ¯åœˆç«™ç‚¹æ€»æ•°
    private Integer stationCount = 0;
    // å¾ªçŽ¯åœˆå†…æœ‰ä»»åŠ¡ç«™ç‚¹æ•°
    private Integer taskCount = 0;
    // å½“前承载量(0-1):当前任务数 / å½“前循环圈总站点数
    private Double currentLoad = 0.0;
}
src/main/java/com/zy/asrs/service/StationCycleCapacityService.java
New file
@@ -0,0 +1,13 @@
package com.zy.asrs.service;
import com.zy.asrs.domain.vo.StationCycleCapacityVo;
public interface StationCycleCapacityService {
    // ç«‹å³åˆ·æ–°å¾ªçŽ¯åœˆä¸Žæ‰¿è½½é‡å¿«ç…§
    void refreshSnapshot();
    // èŽ·å–æœ€æ–°å¾ªçŽ¯åœˆä¸Žæ‰¿è½½é‡å¿«ç…§
    StationCycleCapacityVo getLatestSnapshot();
}
src/main/java/com/zy/asrs/service/impl/StationCycleCapacityServiceImpl.java
New file
@@ -0,0 +1,623 @@
package com.zy.asrs.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
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.DeviceConfig;
import com.zy.asrs.service.BasMapService;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.DeviceConfigService;
import com.zy.asrs.service.StationCycleCapacityService;
import com.zy.common.model.NavigateNode;
import com.zy.common.utils.RedisUtil;
import com.zy.common.utils.NavigateSolution;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.thread.StationThread;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@Service("stationCycleCapacityService")
@Slf4j
public class StationCycleCapacityServiceImpl implements StationCycleCapacityService {
    private static final long LOOP_LOAD_RESERVE_EXPIRE_MILLIS = 120_000L;
    @Autowired
    private BasMapService basMapService;
    @Autowired
    private DeviceConfigService deviceConfigService;
    @Autowired
    private BasDevpService basDevpService;
    @Autowired
    private RedisUtil redisUtil;
    private final AtomicReference<StationCycleCapacityVo> snapshotRef = new AtomicReference<>(new StationCycleCapacityVo());
    @Override
    public synchronized void refreshSnapshot() {
        try {
            StationCycleCapacityVo snapshot = buildSnapshot();
            snapshotRef.set(snapshot);
        } catch (Exception e) {
            log.error("刷新循环圈承载量失败", e);
        }
    }
    @Override
    public StationCycleCapacityVo getLatestSnapshot() {
        StationCycleCapacityVo snapshot = snapshotRef.get();
        if (snapshot == null || snapshot.getRefreshTime() == null) {
            refreshSnapshot();
            snapshot = snapshotRef.get();
        }
        return snapshot == null ? new StationCycleCapacityVo() : snapshot;
    }
    private StationCycleCapacityVo buildSnapshot() {
        GraphContext context = buildStationGraph();
        Map<Integer, Integer> workNoMap = buildStationWorkNoMap();
        Set<Integer> availableStationSet = new HashSet<>(context.graph.keySet());
        availableStationSet.removeAll(context.excludeStationSet);
        Map<Integer, Set<Integer>> filteredGraph = new HashMap<>();
        for (Integer stationId : availableStationSet) {
            Set<Integer> nextSet = context.graph.getOrDefault(stationId, Collections.emptySet());
            Set<Integer> filteredNext = new HashSet<>();
            for (Integer nextId : nextSet) {
                if (availableStationSet.contains(nextId)) {
                    filteredNext.add(nextId);
                }
            }
            filteredGraph.put(stationId, filteredNext);
        }
        List<Set<Integer>> sccList = findStrongConnectedComponents(filteredGraph);
        List<StationCycleLoopVo> loopList = new ArrayList<>();
        int loopNo = 1;
        int totalStationCount = 0;
        int taskStationCount = 0;
        Set<Integer> actualWorkNoSet = new HashSet<>();
        for (Set<Integer> scc : sccList) {
            if (!isCycleScc(scc, filteredGraph)) {
                continue;
            }
            // å¯¹ SCC å†åšä¸€æ¬¡â€œçŽ¯æ ¸å¿ƒâ€å‰¥ç¦»ï¼Œå‰”é™¤æžæˆ/死胡同节点
            List<Set<Integer>> coreLoopList = extractCoreLoopComponents(scc, filteredGraph);
            for (Set<Integer> coreLoop : coreLoopList) {
                List<Integer> stationIdList = new ArrayList<>(coreLoop);
                Collections.sort(stationIdList);
                List<Integer> workNoList = new ArrayList<>();
                int currentLoopTaskCount = 0;
                for (Integer stationId : stationIdList) {
                    Integer workNo = workNoMap.get(stationId);
                    if (workNo != null && workNo > 0) {
                        workNoList.add(workNo);
                        currentLoopTaskCount++;
                        actualWorkNoSet.add(workNo);
                    }
                }
                StationCycleLoopVo loopVo = new StationCycleLoopVo();
                loopVo.setLoopNo(loopNo++);
                loopVo.setStationIdList(stationIdList);
                loopVo.setWorkNoList(workNoList);
                loopVo.setStationCount(stationIdList.size());
                loopVo.setTaskCount(currentLoopTaskCount);
                loopVo.setCurrentLoad(calcCurrentLoad(currentLoopTaskCount, stationIdList.size()));
                loopList.add(loopVo);
                totalStationCount += stationIdList.size();
                taskStationCount += currentLoopTaskCount;
            }
        }
        int reserveTaskCount = mergeReserveTaskCount(loopList, actualWorkNoSet);
        taskStationCount += reserveTaskCount;
        StationCycleCapacityVo vo = new StationCycleCapacityVo();
        vo.setLoopList(loopList);
        vo.setLoopCount(loopList.size());
        vo.setTotalStationCount(totalStationCount);
        vo.setTaskStationCount(taskStationCount);
        vo.setCurrentLoad(calcCurrentLoad(taskStationCount, totalStationCount));
        vo.setRefreshTime(new Date());
        return vo;
    }
    private int mergeReserveTaskCount(List<StationCycleLoopVo> loopList, Set<Integer> actualWorkNoSet) {
        if (loopList == null || loopList.isEmpty()) {
            return 0;
        }
        Map<Object, Object> reserveMap = redisUtil.hmget(RedisKeyType.STATION_CYCLE_LOAD_RESERVE.key);
        if (reserveMap == null || reserveMap.isEmpty()) {
            return 0;
        }
        Map<Integer, StationCycleLoopVo> loopMap = new HashMap<>();
        Map<Integer, StationCycleLoopVo> stationLoopMap = new HashMap<>();
        for (StationCycleLoopVo loopVo : loopList) {
            if (loopVo != null && loopVo.getLoopNo() != null) {
                loopMap.put(loopVo.getLoopNo(), loopVo);
            }
            if (loopVo == null || loopVo.getStationIdList() == null) {
                continue;
            }
            for (Integer stationId : loopVo.getStationIdList()) {
                if (stationId != null) {
                    stationLoopMap.put(stationId, loopVo);
                }
            }
        }
        long now = System.currentTimeMillis();
        int mergedCount = 0;
        List<Object> removeFieldList = new ArrayList<>();
        for (Map.Entry<Object, Object> entry : reserveMap.entrySet()) {
            ReserveRecord record = parseReserveRecord(entry.getKey(), entry.getValue());
            if (record == null) {
                removeFieldList.add(entry.getKey());
                continue;
            }
            if (actualWorkNoSet.contains(record.wrkNo)) {
                removeFieldList.add(entry.getKey());
                continue;
            }
            if (record.createTime <= 0 || now - record.createTime > LOOP_LOAD_RESERVE_EXPIRE_MILLIS) {
                removeFieldList.add(entry.getKey());
                continue;
            }
            StationCycleLoopVo loopVo = loopMap.get(record.loopNo);
            if (loopVo == null && record.hitStationId != null) {
                loopVo = stationLoopMap.get(record.hitStationId);
            }
            if (loopVo == null) {
                removeFieldList.add(entry.getKey());
                continue;
            }
            List<Integer> workNoList = loopVo.getWorkNoList();
            if (workNoList == null) {
                workNoList = new ArrayList<>();
                loopVo.setWorkNoList(workNoList);
            }
            if (workNoList.contains(record.wrkNo)) {
                continue;
            }
            workNoList.add(record.wrkNo);
            Collections.sort(workNoList);
            int mergedTaskCount = toNonNegative(loopVo.getTaskCount()) + 1;
            loopVo.setTaskCount(mergedTaskCount);
            loopVo.setCurrentLoad(calcCurrentLoad(mergedTaskCount, toNonNegative(loopVo.getStationCount())));
            mergedCount++;
        }
        if (!removeFieldList.isEmpty()) {
            redisUtil.hdel(RedisKeyType.STATION_CYCLE_LOAD_RESERVE.key, removeFieldList.toArray());
        }
        return mergedCount;
    }
    private ReserveRecord parseReserveRecord(Object fieldObj, Object valueObj) {
        if (fieldObj == null || valueObj == null) {
            return null;
        }
        Integer fieldWrkNo = parseInteger(String.valueOf(fieldObj));
        if (fieldWrkNo == null || fieldWrkNo <= 0) {
            return null;
        }
        JSONObject jsonObject;
        try {
            jsonObject = JSON.parseObject(String.valueOf(valueObj));
        } catch (Exception e) {
            return null;
        }
        if (jsonObject == null) {
            return null;
        }
        Integer wrkNo = jsonObject.getInteger("wrkNo");
        Integer loopNo = jsonObject.getInteger("loopNo");
        Integer hitStationId = jsonObject.getInteger("hitStationId");
        Long createTime = jsonObject.getLong("createTime");
        if (wrkNo == null || wrkNo <= 0) {
            wrkNo = fieldWrkNo;
        }
        if ((loopNo == null || loopNo <= 0) && (hitStationId == null || hitStationId <= 0)) {
            return null;
        }
        if (createTime == null || createTime <= 0) {
            return null;
        }
        ReserveRecord record = new ReserveRecord();
        record.wrkNo = wrkNo;
        record.loopNo = loopNo;
        record.hitStationId = hitStationId;
        record.createTime = createTime;
        return record;
    }
    private Integer parseInteger(String value) {
        if (value == null || value.trim().isEmpty()) {
            return null;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (Exception e) {
            return null;
        }
    }
    private int toNonNegative(Integer value) {
        if (value == null || value < 0) {
            return 0;
        }
        return value;
    }
    private static class ReserveRecord {
        private Integer wrkNo;
        private Integer loopNo;
        private Integer hitStationId;
        private Long createTime;
    }
    private double calcCurrentLoad(int taskCount, int stationCount) {
        if (stationCount <= 0 || taskCount <= 0) {
            return 0.0;
        }
        double value = (double) taskCount / (double) stationCount;
        if (value < 0.0) {
            return 0.0;
        }
        if (value > 1.0) {
            return 1.0;
        }
        return value;
    }
    private GraphContext buildStationGraph() {
        GraphContext context = new GraphContext();
        List<Integer> levList = basMapService.getLevList();
        if (levList == null || levList.isEmpty()) {
            return context;
        }
        NavigateSolution navigateSolution = new NavigateSolution();
        List<Integer> sortedLevList = new ArrayList<>(levList);
        sortedLevList = new ArrayList<>(new HashSet<>(sortedLevList));
        Collections.sort(sortedLevList);
//        for (Integer lev : sortedLevList) {
//            List<List<NavigateNode>> stationMap;
//            try {
//                stationMap = navigateSolution.getStationMap(lev);
//            } catch (Exception e) {
//                log.warn("加载楼层{}地图失败,跳过循环圈计算", lev);
//                continue;
//            }
//            if (stationMap == null || stationMap.isEmpty()) {
//                continue;
//            }
//
//            for (List<NavigateNode> row : stationMap) {
//                for (NavigateNode node : row) {
//                    JSONObject valueObj = parseNodeValue(node.getNodeValue());
//                    if (valueObj == null) {
//                        continue;
//                    }
//                    Integer stationId = valueObj.getInteger("stationId");
//                    if (stationId == null) {
//                        continue;
//                    }
//
//                    context.graph.computeIfAbsent(stationId, k -> new HashSet<>());
//                    if (isExcludeStation(valueObj)) {
//                        context.excludeStationSet.add(stationId);
//                    }
//
//                    List<NavigateNode> nextNodeList = navigateSolution.extend_current_node(stationMap, node);
//                    if (nextNodeList == null || nextNodeList.isEmpty()) {
//                        continue;
//                    }
//                    for (NavigateNode nextNode : nextNodeList) {
//                        JSONObject nextValueObj = parseNodeValue(nextNode.getNodeValue());
//                        if (nextValueObj == null) {
//                            continue;
//                        }
//                        Integer nextStationId = nextValueObj.getInteger("stationId");
//                        if (nextStationId == null || stationId.equals(nextStationId)) {
//                            continue;
//                        }
//
//                        context.graph.computeIfAbsent(nextStationId, k -> new HashSet<>());
//                        context.graph.get(stationId).add(nextStationId);
//                    }
//                }
//            }
//        }
        appendExcludeStationsFromDeviceConfig(context.excludeStationSet);
        return context;
    }
    private void appendExcludeStationsFromDeviceConfig(Set<Integer> excludeStationSet) {
        List<BasDevp> basDevpList = basDevpService.selectList(new EntityWrapper<>());
        if (basDevpList == null || basDevpList.isEmpty()) {
            return;
        }
        for (BasDevp basDevp : basDevpList) {
            List<StationObjModel> inStationList = basDevp.getInStationList$();
            for (StationObjModel stationObjModel : inStationList) {
                if (stationObjModel != null && stationObjModel.getStationId() != null) {
                    excludeStationSet.add(stationObjModel.getStationId());
                }
            }
            List<StationObjModel> barcodeStationList = basDevp.getBarcodeStationList$();
            for (StationObjModel stationObjModel : barcodeStationList) {
                if (stationObjModel != null && stationObjModel.getStationId() != null) {
                    excludeStationSet.add(stationObjModel.getStationId());
                }
            }
        }
    }
    private JSONObject parseNodeValue(String nodeValue) {
        if (nodeValue == null || nodeValue.trim().isEmpty()) {
            return null;
        }
        try {
            return JSON.parseObject(nodeValue);
        } catch (Exception ignore) {
            return null;
        }
    }
    private boolean isExcludeStation(JSONObject valueObj) {
        Integer isInStation = valueObj.getInteger("isInStation");
        Integer isBarcodeStation = valueObj.getInteger("isBarcodeStation");
        return (isInStation != null && isInStation == 1)
                || (isBarcodeStation != null && isBarcodeStation == 1);
    }
    private Map<Integer, Integer> buildStationWorkNoMap() {
        Map<Integer, Integer> workNoMap = new HashMap<>();
        List<DeviceConfig> devpList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>()
                .eq("device_type", String.valueOf(SlaveType.Devp)));
        if (devpList == null || devpList.isEmpty()) {
            return workNoMap;
        }
        for (DeviceConfig deviceConfig : devpList) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, deviceConfig.getDeviceNo());
            if (stationThread == null) {
                continue;
            }
            Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
            if (statusMap == null || statusMap.isEmpty()) {
                continue;
            }
            for (StationProtocol protocol : statusMap.values()) {
                if (protocol == null || protocol.getStationId() == null) {
                    continue;
                }
                Integer taskNo = protocol.getTaskNo();
                if (taskNo != null && taskNo > 0) {
                    workNoMap.put(protocol.getStationId(), taskNo);
                }
            }
        }
        return workNoMap;
    }
    private List<Set<Integer>> findStrongConnectedComponents(Map<Integer, Set<Integer>> graph) {
        List<Set<Integer>> result = new ArrayList<>();
        if (graph == null || graph.isEmpty()) {
            return result;
        }
        Map<Integer, Integer> indexMap = new HashMap<>();
        Map<Integer, Integer> lowLinkMap = new HashMap<>();
        Deque<Integer> stack = new ArrayDeque<>();
        Set<Integer> inStack = new HashSet<>();
        int[] index = new int[]{0};
        List<Integer> nodeList = new ArrayList<>(graph.keySet());
        Collections.sort(nodeList);
        for (Integer node : nodeList) {
            if (!indexMap.containsKey(node)) {
                strongConnect(node, graph, indexMap, lowLinkMap, stack, inStack, index, result);
            }
        }
        return result;
    }
    private void strongConnect(Integer node,
                               Map<Integer, Set<Integer>> graph,
                               Map<Integer, Integer> indexMap,
                               Map<Integer, Integer> lowLinkMap,
                               Deque<Integer> stack,
                               Set<Integer> inStack,
                               int[] index,
                               List<Set<Integer>> result) {
        indexMap.put(node, index[0]);
        lowLinkMap.put(node, index[0]);
        index[0]++;
        stack.push(node);
        inStack.add(node);
        List<Integer> nextList = new ArrayList<>(graph.getOrDefault(node, Collections.emptySet()));
        Collections.sort(nextList);
        for (Integer next : nextList) {
            if (!indexMap.containsKey(next)) {
                strongConnect(next, graph, indexMap, lowLinkMap, stack, inStack, index, result);
                lowLinkMap.put(node, Math.min(lowLinkMap.get(node), lowLinkMap.get(next)));
            } else if (inStack.contains(next)) {
                lowLinkMap.put(node, Math.min(lowLinkMap.get(node), indexMap.get(next)));
            }
        }
        if (!lowLinkMap.get(node).equals(indexMap.get(node))) {
            return;
        }
        Set<Integer> scc = new HashSet<>();
        while (!stack.isEmpty()) {
            Integer top = stack.pop();
            inStack.remove(top);
            scc.add(top);
            if (top.equals(node)) {
                break;
            }
        }
        result.add(scc);
    }
    private boolean isCycleScc(Set<Integer> scc, Map<Integer, Set<Integer>> graph) {
        if (scc == null || scc.isEmpty()) {
            return false;
        }
        if (scc.size() > 1) {
            return true;
        }
        Integer onlyNode = scc.iterator().next();
        Set<Integer> nextSet = graph.getOrDefault(onlyNode, Collections.emptySet());
        return nextSet.contains(onlyNode);
    }
    /**
     * ä»Ž SCC ä¸­æå–循环核心:
     * 1) è½¬æ— å‘图
     * 2) é€’归剥离度数<2的节点(2-core)
     * 3) å°†å‰©ä½™èŠ‚ç‚¹æ‹†æˆè¿žé€šåˆ†é‡ï¼Œæ¯ä¸ªåˆ†é‡>=3才认定为循环圈
     */
    private List<Set<Integer>> extractCoreLoopComponents(Set<Integer> scc, Map<Integer, Set<Integer>> graph) {
        List<Set<Integer>> result = new ArrayList<>();
        if (scc == null || scc.isEmpty()) {
            return result;
        }
        // æž„建 SCC å†…无向邻接
        Map<Integer, Set<Integer>> undirectedMap = new HashMap<>();
        for (Integer node : scc) {
            undirectedMap.put(node, new HashSet<>());
        }
        for (Integer from : scc) {
            Set<Integer> nextSet = graph.getOrDefault(from, Collections.emptySet());
            for (Integer to : nextSet) {
                if (!scc.contains(to) || from.equals(to)) {
                    continue;
                }
                undirectedMap.get(from).add(to);
                undirectedMap.get(to).add(from);
            }
        }
        // 2-core å‰¥ç¦»
        Set<Integer> alive = new HashSet<>(scc);
        Map<Integer, Integer> degreeMap = new HashMap<>();
        ArrayDeque<Integer> queue = new ArrayDeque<>();
        for (Integer node : scc) {
            int degree = undirectedMap.getOrDefault(node, Collections.emptySet()).size();
            degreeMap.put(node, degree);
            if (degree < 2) {
                queue.offer(node);
            }
        }
        while (!queue.isEmpty()) {
            Integer node = queue.poll();
            if (!alive.remove(node)) {
                continue;
            }
            for (Integer next : undirectedMap.getOrDefault(node, Collections.emptySet())) {
                if (!alive.contains(next)) {
                    continue;
                }
                int newDegree = degreeMap.getOrDefault(next, 0) - 1;
                degreeMap.put(next, newDegree);
                if (newDegree < 2) {
                    queue.offer(next);
                }
            }
        }
        if (alive.size() < 3) {
            return result;
        }
        // æ‹†åˆ†è¿žé€šåˆ†é‡
        Set<Integer> visited = new HashSet<>();
        List<Integer> sortedAlive = new ArrayList<>(alive);
        Collections.sort(sortedAlive);
        for (Integer start : sortedAlive) {
            if (!visited.add(start)) {
                continue;
            }
            Set<Integer> component = new HashSet<>();
            ArrayDeque<Integer> bfs = new ArrayDeque<>();
            bfs.offer(start);
            component.add(start);
            while (!bfs.isEmpty()) {
                Integer node = bfs.poll();
                for (Integer next : undirectedMap.getOrDefault(node, Collections.emptySet())) {
                    if (!alive.contains(next) || !visited.add(next)) {
                        continue;
                    }
                    component.add(next);
                    bfs.offer(next);
                }
            }
            // è‡³å°‘3个点才认为是真正“圈”
            if (component.size() >= 3) {
                result.add(component);
            }
        }
        return result;
    }
    private static class GraphContext {
        private final Map<Integer, Set<Integer>> graph = new HashMap<>();
        private final Set<Integer> excludeStationSet = new HashSet<>();
    }
}
src/main/java/com/zy/asrs/task/StationCycleCapacityScheduler.java
New file
@@ -0,0 +1,20 @@
package com.zy.asrs.task;
import com.zy.asrs.service.StationCycleCapacityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class StationCycleCapacityScheduler {
    @Autowired
    private StationCycleCapacityService stationCycleCapacityService;
//    // æ¯ç§’刷新一次循环圈承载量
//    @Scheduled(cron = "0/1 * * * * ? ")
//    public void refreshStationCycleCapacity() {
//        stationCycleCapacityService.refreshSnapshot();
//    }
}
src/main/java/com/zy/core/network/fake/ZyStationV4FakeSegConnect.java
New file
@@ -0,0 +1,8 @@
package com.zy.core.network.fake;
/**
 * è¾“送站 V4 ä»¿çœŸåˆ†æ®µè¿žæŽ¥å®žçŽ°ã€‚
 * å½“前复用 V3 åˆ†æ®µä»¿çœŸé€»è¾‘,保留独立类用于后续 V4 ä»¿çœŸç­–略演进。
 */
public class ZyStationV4FakeSegConnect extends ZyStationFakeSegConnect {
}
src/main/java/com/zy/core/network/real/ZyStationV4RealConnect.java
New file
@@ -0,0 +1,335 @@
package com.zy.core.network.real;
import HslCommunication.Core.Types.OperateResult;
import HslCommunication.Core.Types.OperateResultExOne;
import HslCommunication.Profinet.Siemens.SiemensPLCS;
import HslCommunication.Profinet.Siemens.SiemensS7Net;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.DateUtils;
import com.core.common.SpringUtils;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.DeviceConfig;
import com.zy.asrs.service.BasDevpService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.OutputQueue;
import com.zy.core.model.CommandResponse;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.command.StationCommand;
import com.zy.core.network.api.ZyStationConnectApi;
import com.zy.core.network.entity.ZyStationStatusEntity;
import lombok.extern.slf4j.Slf4j;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
/**
 * è¾“送站真实连接(PLC)
 */
@Slf4j
public class ZyStationV4RealConnect implements ZyStationConnectApi {
    private List<ZyStationStatusEntity> statusList;
    private List<StationObjModel> barcodeOriginList;
    private SiemensS7Net siemensNet;
    private DeviceConfig deviceConfig;
    private RedisUtil redisUtil;
    public ZyStationV4RealConnect(DeviceConfig deviceConfig, RedisUtil redisUtil) {
        this.deviceConfig = deviceConfig;
        this.redisUtil = redisUtil;
    }
    @Override
    public boolean connect() {
        boolean connected = false;
        siemensNet = new SiemensS7Net(SiemensPLCS.S1200, deviceConfig.getIp());
        OperateResult connect = siemensNet.ConnectServer();
        if (connect.IsSuccess) {
            connected = true;
            OutputQueue.DEVP.offer(MessageFormat.format("【{0}】输送站plc连接成功 ===>> [id:{1}] [ip:{2}] [port:{3}]",
                    DateUtils.convert(new Date()), deviceConfig.getDeviceNo(), deviceConfig.getIp(),
                    deviceConfig.getPort()));
            News.info("输送站plc连接成功 ===>> [id:{}] [ip:{}] [port:{}]",
                    deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort());
        } else {
            OutputQueue.DEVP.offer(MessageFormat.format("【{0}】输送站plc连接失败!!! ===>> [id:{1}] [ip:{2}] [port:{3}]",
                    DateUtils.convert(new Date()), deviceConfig.getDeviceNo(), deviceConfig.getIp(),
                    deviceConfig.getPort()));
            News.error("输送站plc连接失败!!! ===>> [id:{}] [ip:{}] [port:{}]",
                    deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort());
        }
//        siemensNet.ConnectClose();
        return connected;
    }
    @Override
    public boolean disconnect() {
        siemensNet.ConnectClose();
        return true;
    }
    @Override
    public List<ZyStationStatusEntity> getStatus(Integer deviceNo) {
        if (statusList == null) {
            BasDevpService basDevpService = SpringUtils.getBean(BasDevpService.class);
            if (basDevpService == null) {
                return Collections.emptyList();
            }
            BasDevp basDevp = basDevpService
                    .selectOne(new EntityWrapper<BasDevp>().eq("devp_no", deviceConfig.getDeviceNo()));
            if (basDevp == null) {
                return Collections.emptyList();
            }
            statusList = JSONObject.parseArray(basDevp.getStationList(), ZyStationStatusEntity.class);
            if (statusList != null) {
                statusList.sort(Comparator.comparing(ZyStationStatusEntity::getStationId));
            }
            barcodeOriginList = basDevp.getBarcodeStationList$();
        }
        if (siemensNet == null) {
            return statusList;
        }
        OperateResultExOne<byte[]> result = siemensNet.Read("DB100.0", (short) (statusList.size() * 10));
        if (result.IsSuccess) {
            for (int i = 0; i < statusList.size(); i++) {
                ZyStationStatusEntity statusEntity = statusList.get(i); // ç«™ç‚¹ç¼–号
                statusEntity.setTaskNo(siemensNet.getByteTransform().TransInt32(result.Content, i * 10)); // å·¥ä½œå·
                statusEntity.setTargetStaNo((int) siemensNet.getByteTransform().TransInt16(result.Content, i * 10 + 4)); // ç›®æ ‡ç«™
                boolean[] status = siemensNet.getByteTransform().TransBool(result.Content, i * 10 + 6, 1);
                statusEntity.setAutoing(status[0]); // è‡ªåЍ
                statusEntity.setLoading(status[1]); // æœ‰ç‰©
                statusEntity.setInEnable(status[2]); // å¯å…¥
                statusEntity.setOutEnable(status[3]);// å¯å‡º
                statusEntity.setEmptyMk(status[4]); // ç©ºæ‰˜ç›˜
                statusEntity.setFullPlt(status[5]); // æ»¡æ‰˜ç›˜
                boolean[] status2 = siemensNet.getByteTransform().TransBool(result.Content, i * 10 + 7, 1);
                statusEntity.setEnableIn(status2[1]);//启动入库
                Integer palletHeight = null;
                if (status[7]) {
                    palletHeight = 1;//低
                }
                if (status2[0]) {
                    palletHeight = 2;//中
                }
                if (status[6]) {
                    palletHeight = 3;//高
                }
                statusEntity.setPalletHeight(palletHeight);//高低信号
                statusEntity.setError(0);//默认无报警
                statusEntity.setTaskWriteIdx((int) siemensNet.getByteTransform().TransInt16(result.Content, i * 10 + 8));//任务可写区
            }
        }
        // æ¡ç æ‰«æå™¨
        OperateResultExOne<byte[]> result2 = siemensNet.Read("DB101.16", (short) (barcodeOriginList.size() * 16));
        if (result2.IsSuccess) {
            for (int i = 0; i < barcodeOriginList.size(); i++) {
                ZyStationStatusEntity barcodeEntity = findStatusEntityByBarcodeIdx(i + 1);
                if (barcodeEntity == null) {
                    continue;
                }
                String barcode = siemensNet.getByteTransform().TransString(result2.Content, i * 16 + 2, 14, "UTF-8");
                barcode = barcode.trim();
                barcodeEntity.setBarcode(barcode);
            }
        }
        // ç§°é‡
        OperateResultExOne<byte[]> result3 = siemensNet.Read("DB102.4", (short) (barcodeOriginList.size() * 4));
        if (result3.IsSuccess) {
            for (int i = 0; i < barcodeOriginList.size(); i++) {
                ZyStationStatusEntity barcodeEntity = findStatusEntityByBarcodeIdx(i + 1);
                if (barcodeEntity == null) {
                    continue;
                }
                double weight = (double) siemensNet.getByteTransform().TransSingle(result3.Content, i * 4);
                barcodeEntity.setWeight(weight);
            }
        }
        // æŠ¥è­¦ä¿¡æ¯
        OperateResultExOne<byte[]> result4 = siemensNet.Read("DB103.2", (short) (barcodeOriginList.size() * 2));
        if (result4.IsSuccess) {
            for (int i = 0; i < barcodeOriginList.size(); i++) {
                ZyStationStatusEntity barcodeEntity = findStatusEntityByBarcodeIdx(i + 1);
                if (barcodeEntity == null) {
                    continue;
                }
                StringBuilder sb = new StringBuilder();
                boolean[] status1 = siemensNet.getByteTransform().TransBool(result4.Content, i * 2, 1);
                boolean[] status2 = siemensNet.getByteTransform().TransBool(result4.Content, i * 2 + 1, 1);
                if(status1[0]){
                    sb.append("左超宽报警;");
                }
                if(status1[1]) {
                    sb.append("右超宽报警;");
                }
                if(status1[2]) {
                    sb.append("前超长报警;");
                }
                if(status1[3]) {
                    sb.append("后超长报警;");
                }
                if(status1[4]) {
                    sb.append("超高报警;");
                }
                if(status1[5]) {
                    sb.append("有货报警,空托入库时检测托盘上有无货物;");
                }
                if(status1[6]) {
                    sb.append("重量异常报警;");
                }
                if(status1[7]) {
                    sb.append("扫码异常;");
                }
                if(sb.length() > 0) {
                    barcodeEntity.setError(1);
                }else {
                    barcodeEntity.setError(0);
                }
                barcodeEntity.setErrorMsg(sb.toString());
            }
        }
        return statusList;
    }
    @Override
    public CommandResponse sendCommand(Integer deviceNo, StationCommand command) {
        CommandResponse commandResponse = new CommandResponse(false);
        if (null == command) {
            commandResponse.setMessage("命令为空");
            return commandResponse;
        }
        int taskWriteIdx = getTaskWriteIdx(command.getStationId());
        if (taskWriteIdx == -1) {
            commandResponse.setMessage("命令下发超时,无法找到可用下发区域");
            return commandResponse;
        }
        int stationIdx = findIndex(command.getStationId());
        short[] data = new short[2];
        data[0] = (short) 0;
        data[1] = command.getTargetStaNo().shortValue();
        OperateResult writeTaskNo = siemensNet.Write("DB13." + (stationIdx * 48 + (taskWriteIdx * 12)), command.getTaskNo());
        if (!writeTaskNo.IsSuccess) {
            log.error("写入输送线命令失败。站点编号={},站点数据={}", command.getTaskNo(), JSON.toJSON(command));
            commandResponse.setResult(false);
            commandResponse.setMessage("命令下发失败,写入工作号失败");
            return commandResponse;
        }
        OperateResult writeData = siemensNet.Write("DB13." + (stationIdx * 48 + (taskWriteIdx * 12 + 4)), data);
        if (!writeData.IsSuccess) {
            log.error("写入输送线命令失败。站点编号={},站点数据={}", command.getTaskNo(), JSON.toJSON(command));
            commandResponse.setResult(false);
            commandResponse.setMessage("命令下发失败,写入数据区域失败");
            return commandResponse;
        }
        log.info("写入输送线命令成功。任务号={},站点数据={}", command.getTaskNo(), JSON.toJSON(command));
        commandResponse.setResult(true);
        return commandResponse;
    }
    @Override
    public synchronized CommandResponse sendOriginCommand(String address, short[] data) {
        CommandResponse commandResponse = new CommandResponse(false);
        if (null == data || data.length == 0) {
            commandResponse.setMessage("数据为空");
            return commandResponse;
        }
        OperateResult write = siemensNet.Write(address, data);
        if (write.IsSuccess) {
            log.info("写入原始命令成功。地址={},数据={}", address, JSON.toJSON(data));
            commandResponse.setResult(true);
        } else {
            log.error("写入原始命令失败。地址={},数据={}", address, JSON.toJSON(data));
            commandResponse.setResult(false);
        }
        return commandResponse;
    }
    @Override
    public byte[] readOriginCommand(String address, int length) {
        OperateResultExOne<byte[]> result = siemensNet.Read(address, (short) length);
        if (result.IsSuccess) {
            return result.Content;
        }
        return new byte[0];
    }
    private ZyStationStatusEntity findStatusEntityByBarcodeIdx(Integer barcodeIdx) {
        Integer stationId = null;
        for (StationObjModel stationObjModel : barcodeOriginList) {
            if (stationObjModel.getBarcodeIdx().equals(barcodeIdx)) {
                stationId = stationObjModel.getStationId();
                break;
            }
        }
        for (ZyStationStatusEntity zyStationStatusEntity : statusList) {
            if(zyStationStatusEntity.getStationId().equals(stationId)) {
                return zyStationStatusEntity;
            }
        }
        return null;
    }
    private int getTaskWriteIdx(int stationId) {
        int useIdx = -1;
        int stationIdx = findIndex(stationId);
        if (stationIdx != -1) {
            ZyStationStatusEntity statusEntity = statusList.get(stationIdx);
            Integer taskWriteIdx = statusEntity.getTaskWriteIdx();
            if (taskWriteIdx > 0) {
                OperateResultExOne<byte[]> resultTask = siemensNet.Read("DB13." + (stationId * 48), (short) 48);
                if (resultTask.IsSuccess) {
                    int taskNo = siemensNet.getByteTransform().TransInt32(resultTask.Content, taskWriteIdx * 12);
                    int startPoint = siemensNet.getByteTransform().TransInt16(resultTask.Content, taskWriteIdx * 12 + 4);
                    int targetPoint = siemensNet.getByteTransform().TransInt16(resultTask.Content, taskWriteIdx * 12 + 6);
                    if (taskNo == 0 && startPoint == 0 && targetPoint == 0) {
                        useIdx = taskWriteIdx;
                    }
                }
            }
        }
        return useIdx;
    }
    private int findIndex(Integer stationId) {
        for (int i = 0; i < statusList.size(); i++) {
            ZyStationStatusEntity statusEntity = statusList.get(i);
            if (statusEntity.getStationId().equals(stationId)) {
                return i;
            }
        }
        return -1;
    }
}
src/main/java/com/zy/core/thread/impl/ZyStationV4Thread.java
New file
@@ -0,0 +1,478 @@
package com.zy.core.thread.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.Cools;
import com.core.common.DateUtils;
import com.core.common.SpringUtils;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasStationOpt;
import com.zy.asrs.entity.DeviceConfig;
import com.zy.asrs.entity.DeviceDataLog;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.BasStationOptService;
import com.zy.asrs.utils.Utils;
import com.zy.common.model.NavigateNode;
import com.zy.common.utils.NavigateUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.OutputQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.model.CommandResponse;
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.network.DeviceConnectPool;
import com.zy.core.network.ZyStationConnectDriver;
import com.zy.core.network.entity.ZyStationStatusEntity;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Data
@Slf4j
public class ZyStationV4Thread implements Runnable, com.zy.core.thread.StationThread {
    private List<StationProtocol> statusList = new ArrayList<>();
    private DeviceConfig deviceConfig;
    private RedisUtil redisUtil;
    private ZyStationConnectDriver zyStationConnectDriver;
    private int deviceLogCollectTime = 200;
    private long deviceDataLogTime = System.currentTimeMillis();
    private ExecutorService executor = Executors.newFixedThreadPool(9999);
    public ZyStationV4Thread(DeviceConfig deviceConfig, RedisUtil redisUtil) {
        this.deviceConfig = deviceConfig;
        this.redisUtil = redisUtil;
    }
    @Override
    @SuppressWarnings("InfiniteLoopStatement")
    public void run() {
        this.connect();
        deviceLogCollectTime = Utils.getDeviceLogCollectTime();
        Thread readThread = new Thread(() -> {
            while (true) {
                try {
                    deviceLogCollectTime = Utils.getDeviceLogCollectTime();
                    readStatus();
                    Thread.sleep(100);
                } catch (Exception e) {
                    log.error("StationV4Thread Fail", e);
                }
            }
        });
        readThread.start();
        Thread processThread = new Thread(() -> {
            while (true) {
                try {
                    int step = 1;
                    Task task = MessageQueue.poll(SlaveType.Devp, deviceConfig.getDeviceNo());
                    if (task != null) {
                        step = task.getStep();
                    }
                    if (step == 2) {
                        StationCommand cmd = (StationCommand) task.getData();
                        executor.submit(() -> executeMoveWithSeg(cmd));
                    }
                    Thread.sleep(100);
                } catch (Exception e) {
                    log.error("StationV4Process Fail", e);
                }
            }
        });
        processThread.start();
    }
    private void readStatus() {
        if (zyStationConnectDriver == null) {
            return;
        }
        if (statusList.isEmpty()) {
            BasDevpService basDevpService = null;
            try {
                basDevpService = SpringUtils.getBean(BasDevpService.class);
            } catch (Exception e) {
            }
            if (basDevpService == null) {
                return;
            }
            BasDevp basDevp = basDevpService
                    .selectOne(new EntityWrapper<BasDevp>().eq("devp_no", deviceConfig.getDeviceNo()));
            if (basDevp == null) {
                return;
            }
            List<ZyStationStatusEntity> list = JSONObject.parseArray(basDevp.getStationList(), ZyStationStatusEntity.class);
            for (ZyStationStatusEntity entity : list) {
                StationProtocol stationProtocol = new StationProtocol();
                stationProtocol.setStationId(entity.getStationId());
                statusList.add(stationProtocol);
            }
        }
        List<ZyStationStatusEntity> zyStationStatusEntities = zyStationConnectDriver.getStatus();
        for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) {
            for (StationProtocol stationProtocol : statusList) {
                if (stationProtocol.getStationId().equals(statusEntity.getStationId())) {
                    stationProtocol.setTaskNo(statusEntity.getTaskNo());
                    stationProtocol.setTargetStaNo(statusEntity.getTargetStaNo());
                    stationProtocol.setAutoing(statusEntity.isAutoing());
                    stationProtocol.setLoading(statusEntity.isLoading());
                    stationProtocol.setInEnable(statusEntity.isInEnable());
                    stationProtocol.setOutEnable(statusEntity.isOutEnable());
                    stationProtocol.setEmptyMk(statusEntity.isEmptyMk());
                    stationProtocol.setFullPlt(statusEntity.isFullPlt());
                    stationProtocol.setPalletHeight(statusEntity.getPalletHeight());
                    stationProtocol.setError(statusEntity.getError());
                    stationProtocol.setErrorMsg(statusEntity.getErrorMsg());
                    stationProtocol.setBarcode(statusEntity.getBarcode());
                    stationProtocol.setRunBlock(statusEntity.isRunBlock());
                    stationProtocol.setEnableIn(statusEntity.isEnableIn());
                    stationProtocol.setWeight(statusEntity.getWeight());
                    stationProtocol.setTaskWriteIdx(statusEntity.getTaskWriteIdx());
                }
                if (!Cools.isEmpty(stationProtocol.getSystemWarning())) {
                    if (stationProtocol.isAutoing()
                            && !stationProtocol.isLoading()
                    ) {
                        stationProtocol.setSystemWarning("");
                    }
                }
            }
        }
        OutputQueue.DEVP.offer(MessageFormat.format("【{0}】[id:{1}] <<<<< å®žæ—¶æ•°æ®æ›´æ–°æˆåŠŸ", DateUtils.convert(new Date()), deviceConfig.getDeviceNo()));
        if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) {
            DeviceDataLog deviceDataLog = new DeviceDataLog();
            deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities));
            deviceDataLog.setWcsData(JSON.toJSONString(statusList));
            deviceDataLog.setType(String.valueOf(SlaveType.Devp));
            deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo());
            deviceDataLog.setCreateTime(new Date());
            redisUtil.set(RedisKeyType.DEVICE_LOG_KEY.key + System.currentTimeMillis(), deviceDataLog, 60 * 60 * 24);
            deviceDataLogTime = System.currentTimeMillis();
        }
    }
    @Override
    public boolean connect() {
        zyStationConnectDriver = new ZyStationConnectDriver(deviceConfig, redisUtil);
        zyStationConnectDriver.start();
        DeviceConnectPool.put(SlaveType.Devp, deviceConfig.getDeviceNo(), zyStationConnectDriver);
        return true;
    }
    @Override
    public void close() {
        if (zyStationConnectDriver != null) {
            zyStationConnectDriver.close();
        }
        if (executor != null) {
            try { executor.shutdownNow(); } catch (Exception ignore) {}
        }
    }
    @Override
    public List<StationProtocol> getStatus() {
        return statusList;
    }
    @Override
    public Map<Integer, StationProtocol> getStatusMap() {
        Map<Integer, StationProtocol> map = new HashMap<>();
        for (StationProtocol stationProtocol : statusList) {
            map.put(stationProtocol.getStationId(), stationProtocol);
        }
        return map;
    }
    @Override
    public StationCommand getCommand(StationCommandType commandType, Integer taskNo, Integer stationId, Integer targetStationId, Integer palletSize) {
        StationCommand stationCommand = new StationCommand();
        stationCommand.setTaskNo(taskNo);
        stationCommand.setStationId(stationId);
        stationCommand.setTargetStaNo(targetStationId);
        stationCommand.setPalletSize(palletSize);
        stationCommand.setCommandType(commandType);
        if (commandType == StationCommandType.MOVE) {
            if (!stationId.equals(targetStationId)) {
                List<NavigateNode> nodes = calcPathNavigateNodes(stationId, targetStationId);
                List<Integer> path = new ArrayList<>();
                List<Integer> liftTransferPath = new ArrayList<>();
                for (NavigateNode n : nodes) {
                    JSONObject v = JSONObject.parseObject(n.getNodeValue());
                    if (v == null) {
                        continue;
                    }
                    Integer stationNo = v.getInteger("stationId");
                    if (stationNo == null) {
                        continue;
                    }
                    path.add(stationNo);
                    if (Boolean.TRUE.equals(n.getIsLiftTransferPoint())) {
                        liftTransferPath.add(stationNo);
                    }
                }
                stationCommand.setNavigatePath(path);
                stationCommand.setLiftTransferPath(liftTransferPath);
            }
        }
        return stationCommand;
    }
    @Override
    public CommandResponse sendCommand(StationCommand command) {
        CommandResponse commandResponse = null;
        try {
            commandResponse = zyStationConnectDriver.sendCommand(command);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            BasStationOptService optService = SpringUtils.getBean(BasStationOptService.class);
            List<ZyStationStatusEntity> statusListEntity = zyStationConnectDriver.getStatus();
            ZyStationStatusEntity matched = null;
            if (statusListEntity != null) {
                for (ZyStationStatusEntity e : statusListEntity) {
                    if (e.getStationId() != null && e.getStationId().equals(command.getStationId())) {
                        matched = e;
                        break;
                    }
                }
            }
            BasStationOpt basStationOpt = new BasStationOpt(
                    command.getTaskNo(),
                    command.getStationId(),
                    new Date(),
                    String.valueOf(command.getCommandType()),
                    command.getStationId(),
                    command.getTargetStaNo(),
                    null,
                    null,
                    null,
                    JSON.toJSONString(command),
                    JSON.toJSONString(matched),
                    1,
                    JSON.toJSONString(commandResponse)
            );
            if (optService != null) {
                optService.insert(basStationOpt);
            }
        }
        return commandResponse;
    }
    @Override
    public CommandResponse sendOriginCommand(String address, short[] data) {
        return zyStationConnectDriver.sendOriginCommand(address, data);
    }
    @Override
    public byte[] readOriginCommand(String address, int length) {
        return zyStationConnectDriver.readOriginCommand(address, length);
    }
    private List<NavigateNode> calcPathNavigateNodes(Integer startStationId, Integer targetStationId) {
        NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class);
        if (navigateUtils == null) {
            return new ArrayList<>();
        }
        return navigateUtils.calcByStationId(startStationId, targetStationId);
    }
    private void executeMoveWithSeg(StationCommand original) {
        if(original.getCommandType() == StationCommandType.MOVE){
            List<Integer> path = JSON.parseArray(JSON.toJSONString(original.getNavigatePath(), SerializerFeature.DisableCircularReferenceDetect), Integer.class);
            List<Integer> liftTransferPath = JSON.parseArray(JSON.toJSONString(original.getLiftTransferPath(), SerializerFeature.DisableCircularReferenceDetect), Integer.class);
            if (path == null || path.isEmpty()) {
                // åŒç«™ç‚¹ä»»åŠ¡ä¸ä¼šç”Ÿæˆè·¯å¾„ï¼Œä½†ä»éœ€ä¸‹å‘å‘½ä»¤å†™å…¥ä»»åŠ¡æ•°æ®
                if (Objects.equals(original.getStationId(), original.getTargetStaNo())) {
                    while (true) {
                        CommandResponse commandResponse = sendCommand(original);
                        if (commandResponse != null && commandResponse.getResult()) {
                            break;
                        }
                        try {
                            Thread.sleep(200);
                        } catch (Exception ignore) {}
                    }
                }
                return;
            }
            int total = path.size();
            List<Integer> segmentEndIndices = new ArrayList<>();
            if (liftTransferPath != null) {
                for (Integer liftTransferStationId : liftTransferPath) {
                    int endIndex = path.indexOf(liftTransferStationId);
                    // é¿å…ä»¥èµ·ç‚¹ä½œä¸ºåˆ‡ç‚¹å¯¼è‡´ç©ºåˆ†æ®µ
                    if (endIndex <= 0) {
                        continue;
                    }
                    if (segmentEndIndices.isEmpty() || endIndex > segmentEndIndices.get(segmentEndIndices.size() - 1)) {
                        segmentEndIndices.add(endIndex);
                    }
                }
            }
            if (segmentEndIndices.isEmpty() || segmentEndIndices.get(segmentEndIndices.size() - 1) != total - 1) {
                segmentEndIndices.add(total - 1);
            }
            List<StationCommand> segmentCommands = new ArrayList<>();
            int buildStartIdx = 0;
            for (Integer endIdx : segmentEndIndices) {
                if (endIdx == null || endIdx < buildStartIdx) {
                    continue;
                }
                List<Integer> segmentPath = new ArrayList<>(path.subList(buildStartIdx, endIdx + 1));
                if (segmentPath.isEmpty()) {
                    buildStartIdx = endIdx + 1;
                    continue;
                }
                StationCommand segmentCommand = new StationCommand();
                segmentCommand.setTaskNo(original.getTaskNo());
                segmentCommand.setCommandType(original.getCommandType());
                segmentCommand.setPalletSize(original.getPalletSize());
                segmentCommand.setBarcode(original.getBarcode());
                segmentCommand.setOriginalNavigatePath(path);
                segmentCommand.setNavigatePath(segmentPath);
                // æ¯æ®µå‘½ä»¤ï¼šèµ·ç‚¹=当前段首站点,终点=当前段末站点
                segmentCommand.setStationId(segmentPath.get(0));
                segmentCommand.setTargetStaNo(segmentPath.get(segmentPath.size() - 1));
                segmentCommands.add(segmentCommand);
                // åˆ†æ®µè¾¹ç•Œç‚¹éœ€è¦åŒæ—¶ä½œä¸ºä¸‹ä¸€æ®µçš„起点(例如 [221,220,219] + [219,213,212])
                buildStartIdx = endIdx;
            }
            if (segmentCommands.isEmpty()) {
                return;
            }
            int segCursor = 0;
            while (true) {
                CommandResponse commandResponse = sendCommand(segmentCommands.get(segCursor));
                if (commandResponse == null) {
                    try {
                        Thread.sleep(200);
                    } catch (Exception ignore) {}
                    continue;
                }
                if (commandResponse.getResult()) {
                    break;
                }
                try {
                    Thread.sleep(200);
                } catch (Exception ignore) {}
            }
            long runTime = System.currentTimeMillis();
            boolean firstRun = true;
            while (true) {
                try {
                    Object cancel = redisUtil.get(RedisKeyType.DEVICE_STATION_MOVE_RESET.key + original.getTaskNo());
                    if (cancel != null) {
                        break;//收到中断信号
                    }
                    StationProtocol currentStation = findCurrentStationByTask(original.getTaskNo());
                    if (currentStation == null) {
                        if(System.currentTimeMillis() - runTime > 1000 * 60){
                            break;
                        }
                        Thread.sleep(500);
                        continue;
                    }
                    runTime = System.currentTimeMillis();
                    if (!firstRun && currentStation.isRunBlock()) {
                        break;
                    }
                    int currentIndex = path.indexOf(currentStation.getStationId());
                    if (currentIndex < 0) {
                        Thread.sleep(500);
                        continue;
                    }
                    int remaining = total - currentIndex - 1;
                    if (remaining <= 0) {
                        break;
                    }
                    int currentSegEndIndex = segmentEndIndices.get(segCursor);
                    int currentSegStartIndex = segCursor == 0 ? 0 : segmentEndIndices.get(segCursor - 1);
                    int segLen = currentSegEndIndex - currentSegStartIndex + 1;
                    int remainingSegment = Math.max(0, currentSegEndIndex - currentIndex);
                    int thresholdSegment = (int) Math.ceil(segLen * 0.3);
                    if (remainingSegment <= thresholdSegment && segCursor < segmentCommands.size() - 1) {
                        segCursor++;
                        while (true) {
                            CommandResponse commandResponse = sendCommand(segmentCommands.get(segCursor));
                            if (commandResponse == null) {
                                Thread.sleep(200);
                                continue;
                            }
                            if (commandResponse.getResult()) {
                                break;
                            }
                            Thread.sleep(200);
                        }
                    }
                    Thread.sleep(500);
                } catch (Exception e) {
                    break;
                }
                firstRun = false;
            }
        }else {
            sendCommand(original);
        }
    }
    private StationProtocol findCurrentStationByTask(Integer taskNo) {
        try {
            com.zy.asrs.service.DeviceConfigService deviceConfigService = SpringUtils.getBean(com.zy.asrs.service.DeviceConfigService.class);
            if (deviceConfigService == null) {
                return null;
            }
            List<DeviceConfig> devpList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>()
                    .eq("device_type", String.valueOf(SlaveType.Devp)));
            for (DeviceConfig dc : devpList) {
                com.zy.core.thread.StationThread t = (com.zy.core.thread.StationThread) SlaveConnection.get(SlaveType.Devp, dc.getDeviceNo());
                if (t == null) {
                    continue;
                }
                Map<Integer, StationProtocol> m = t.getStatusMap();
                if (m == null || m.isEmpty()) {
                    continue;
                }
                for (StationProtocol sp : m.values()) {
                    if (sp.getTaskNo() != null && sp.getTaskNo().equals(taskNo) && sp.isLoading()) {
                        return sp;
                    }
                }
            }
        } catch (Exception e) {
            return null;
        }
        return null;
    }
}
src/main/java/com/zy/system/entity/license/LicenseUtils.java
New file
@@ -0,0 +1,26 @@
package com.zy.system.entity.license;
public class LicenseUtils {
    /**
     * èŽ·å–å½“å‰æœåŠ¡å™¨éœ€è¦é¢å¤–æ ¡éªŒçš„License参数
     */
    public static LicenseCheck getServerInfos(){
        //操作系统类型
        String osName = System.getProperty("os.name").toLowerCase();
        osName = osName.toLowerCase();
        AbstractServerInfos abstractServerInfos = null;
        //根据不同操作系统类型选择不同的数据获取方法
        if (osName.startsWith("windows")) {
            abstractServerInfos = new WindowsServerInfos();
        } else if (osName.startsWith("linux")) {
            abstractServerInfos = new LinuxServerInfos();
        }else{//其他服务器类型
            abstractServerInfos = new LinuxServerInfos();
        }
        return abstractServerInfos.getServerInfos();
    }
}
src/main/resources/docs/~$SÍⲿHTTP API½Ó¿ÚV1.7.docx
Binary files differ
src/main/resources/map/~$СËɵØÍ¼.xlsx
Binary files differ
src/main/resources/sql/20260109084743.nb3
Binary files differ
src/main/webapp/views/ai/llm_config.html
New file
@@ -0,0 +1,1014 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI配置</title>
  <link rel="stylesheet" href="../../static/vue/element/element.css" />
  <style>
    body {
      margin: 0;
      font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
      background:
        radial-gradient(1200px 500px at 10% -10%, rgba(26, 115, 232, 0.14), transparent 50%),
        radial-gradient(900px 450px at 100% 0%, rgba(38, 166, 154, 0.11), transparent 55%),
        #f4f7fb;
    }
    .container {
      max-width: 1640px;
      margin: 16px auto;
      padding: 0 14px;
    }
    .hero {
      background: linear-gradient(135deg, #0f4c81 0%, #1f6fb2 45%, #2aa198 100%);
      color: #fff;
      border-radius: 14px;
      padding: 14px 16px;
      margin-bottom: 10px;
      box-shadow: 0 10px 28px rgba(23, 70, 110, 0.22);
    }
    .hero-top {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }
    .hero-title {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .hero-title .main {
      font-size: 16px;
      font-weight: 700;
      letter-spacing: 0.2px;
    }
    .hero-title .sub {
      font-size: 12px;
      opacity: 0.9;
    }
    .hero-actions {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
      justify-content: flex-end;
    }
    .summary-grid {
      margin-top: 10px;
      display: grid;
      grid-template-columns: repeat(5, minmax(0, 1fr));
      gap: 8px;
    }
    .summary-card {
      border-radius: 10px;
      background: rgba(255, 255, 255, 0.16);
      border: 1px solid rgba(255, 255, 255, 0.24);
      padding: 8px 10px;
      min-height: 56px;
      backdrop-filter: blur(3px);
    }
    .summary-card .k {
      font-size: 11px;
      opacity: 0.88;
    }
    .summary-card .v {
      margin-top: 4px;
      font-size: 22px;
      font-weight: 700;
      line-height: 1.1;
    }
    .route-board {
      border-radius: 14px;
      border: 1px solid #dbe5f2;
      background:
        radial-gradient(800px 200px at -10% 0, rgba(52, 119, 201, 0.06), transparent 55%),
        radial-gradient(700px 220px at 110% 20%, rgba(39, 154, 136, 0.08), transparent 58%),
        #f9fbff;
      box-shadow: 0 8px 30px rgba(26, 53, 84, 0.10);
      padding: 12px;
      min-height: 64vh;
    }
    .route-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(390px, 1fr));
      gap: 12px;
    }
    .route-card {
      border-radius: 14px;
      border: 1px solid #e4ebf5;
      background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
      box-shadow: 0 10px 24px rgba(14, 38, 68, 0.08);
      padding: 12px;
      transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
      animation: card-in 0.24s ease both;
    }
    .route-card:hover {
      transform: translateY(-2px);
      box-shadow: 0 14px 26px rgba(14, 38, 68, 0.12);
      border-color: #d4e2f2;
    }
    .route-card.cooling {
      border-color: #f2d8a2;
      background: linear-gradient(180deg, #fffdf6 0%, #fffaf0 100%);
    }
    .route-card.disabled {
      opacity: 0.84;
    }
    .route-head {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 8px;
      margin-bottom: 10px;
    }
    .route-title {
      display: flex;
      flex-direction: column;
      gap: 5px;
      min-width: 0;
      flex: 1;
    }
    .route-id-line {
      color: #8294aa;
      font-size: 11px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .route-state {
      display: flex;
      gap: 6px;
      align-items: center;
      flex-wrap: wrap;
      justify-content: flex-end;
      max-width: 46%;
    }
    .route-fields {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
    }
    .field-full {
      grid-column: 1 / -1;
    }
    .field-label {
      font-size: 11px;
      color: #6f8094;
      margin-bottom: 4px;
    }
    .switch-line {
      margin-top: 10px;
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 8px;
    }
    .switch-item {
      border: 1px solid #e7edf7;
      border-radius: 10px;
      padding: 6px 8px;
      background: #fff;
      display: flex;
      align-items: center;
      justify-content: space-between;
      font-size: 12px;
      color: #2f3f53;
    }
    .stats-box {
      margin-top: 10px;
      border: 1px solid #e8edf6;
      border-radius: 10px;
      background: linear-gradient(180deg, #fcfdff 0%, #f7faff 100%);
      padding: 8px 10px;
      font-size: 12px;
      color: #4c5f76;
      line-height: 1.6;
    }
    .stats-box .light {
      color: #7f91a8;
    }
    .route-actions {
      margin-top: 10px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
    }
    .action-left, .action-right {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .empty-shell {
      min-height: 48vh;
      border-radius: 12px;
      border: 1px dashed #cfd8e5;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      color: #7d8ea4;
      gap: 8px;
      background: rgba(255, 255, 255, 0.55);
    }
    .log-toolbar {
      display: flex;
      align-items: center;
      gap: 8px;
      margin-bottom: 10px;
      flex-wrap: wrap;
    }
    .log-text {
      max-width: 360px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      color: #6c7f95;
      font-size: 12px;
    }
    .log-detail-body {
      max-height: 62vh;
      overflow: auto;
      border: 1px solid #dfe8f3;
      border-radius: 8px;
      background: #f8fbff;
      padding: 10px 12px;
      white-space: pre-wrap;
      word-break: break-word;
      line-height: 1.55;
      color: #2e3c4f;
      font-size: 12px;
      font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
    }
    @keyframes card-in {
      from { opacity: 0; transform: translateY(8px); }
      to { opacity: 1; transform: translateY(0); }
    }
    .mono {
      font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
      font-size: 12px;
    }
    @media (max-width: 1280px) {
      .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .route-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
      .route-fields { grid-template-columns: 1fr; }
      .switch-line { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
<div id="app" class="container">
  <div class="hero">
    <div class="hero-top">
      <div class="hero-title">
        <div v-html="headerIcon" style="display:flex;"></div>
        <div>
          <div class="main">AI配置 - LLM路由</div>
          <div class="sub">支持多API、多模型、多Key,额度耗尽或故障自动切换</div>
        </div>
      </div>
      <div class="hero-actions">
        <el-button type="primary" size="mini" @click="addRoute">新增路由</el-button>
        <el-button size="mini" @click="exportRoutes">导出JSON</el-button>
        <el-button size="mini" @click="triggerImport">导入JSON</el-button>
        <el-button size="mini" @click="loadRoutes">刷新</el-button>
        <el-button size="mini" @click="openLogDialog">调用日志</el-button>
      </div>
    </div>
    <div class="summary-grid">
      <div class="summary-card">
        <div class="k">总路由</div>
        <div class="v">{{ summary.total }}</div>
      </div>
      <div class="summary-card">
        <div class="k">启用</div>
        <div class="v">{{ summary.enabled }}</div>
      </div>
      <div class="summary-card">
        <div class="k">故障切换开启</div>
        <div class="v">{{ summary.errorSwitch }}</div>
      </div>
      <div class="summary-card">
        <div class="k">额度切换开启</div>
        <div class="v">{{ summary.quotaSwitch }}</div>
      </div>
      <div class="summary-card">
        <div class="k">冷却中</div>
        <div class="v">{{ summary.cooling }}</div>
      </div>
    </div>
  </div>
  <input ref="importFileInput" type="file" accept="application/json,.json" style="display:none;" @change="handleImportFileChange" />
  <div class="route-board" v-loading="loading">
    <div v-if="!routes || routes.length === 0" class="empty-shell">
      <div style="font-size:14px;font-weight:600;">暂无路由配置</div>
      <div style="font-size:12px;">点击右上角“新增路由”创建第一条配置</div>
    </div>
    <div v-else class="route-grid">
      <div class="route-card" :class="routeCardClass(route)" v-for="(route, idx) in routes" :key="route.id ? ('route_' + route.id) : ('new_' + idx)">
        <div class="route-head">
          <div class="route-title">
            <el-input v-model="route.name" size="mini" placeholder="路由名称"></el-input>
            <div class="route-id-line">#{{ route.id || 'new' }} Â· ä¼˜å…ˆçº§ {{ route.priority || 0 }}</div>
          </div>
          <div class="route-state">
            <el-tag size="mini" :type="route.status === 1 ? 'success' : 'info'">{{ route.status === 1 ? '启用' : '禁用' }}</el-tag>
            <el-tag size="mini" type="warning" v-if="isRouteCooling(route)">冷却中</el-tag>
          </div>
        </div>
        <div class="route-fields">
          <div class="field-full">
            <div class="field-label">Base URL</div>
            <el-input v-model="route.baseUrl" class="mono" size="mini" placeholder="必填,例如: https://dashscope.aliyuncs.com/compatible-mode/v1"></el-input>
          </div>
          <div>
            <div class="field-label">模型</div>
            <el-input v-model="route.model" class="mono" size="mini" placeholder="必填"></el-input>
          </div>
          <div>
            <div class="field-label">优先级(越小越优先)</div>
            <el-input-number v-model="route.priority" size="mini" :min="0" :max="99999" :controls="false" style="width:100%;"></el-input-number>
          </div>
          <div class="field-full">
            <div class="field-label">API Key</div>
            <el-input v-model="route.apiKey" class="mono" type="password" size="mini" placeholder="必填">
              <template slot="append">
                <el-button type="text" style="padding:0 8px;" @click="copyApiKey(route)">复制</el-button>
              </template>
            </el-input>
          </div>
          <div>
            <div class="field-label">冷却秒数</div>
            <el-input-number v-model="route.cooldownSeconds" size="mini" :min="0" :max="86400" :controls="false" style="width:100%;"></el-input-number>
          </div>
        </div>
        <div class="switch-line">
          <div class="switch-item">
            <span>状态</span>
            <el-switch v-model="route.status" :active-value="1" :inactive-value="0"></el-switch>
          </div>
          <div class="switch-item">
            <span>思考</span>
            <el-switch v-model="route.thinking" :active-value="1" :inactive-value="0"></el-switch>
          </div>
          <div class="switch-item">
            <span>额度切换</span>
            <el-switch v-model="route.switchOnQuota" :active-value="1" :inactive-value="0"></el-switch>
          </div>
          <div class="switch-item">
            <span>故障切换</span>
            <el-switch v-model="route.switchOnError" :active-value="1" :inactive-value="0"></el-switch>
          </div>
        </div>
        <div class="stats-box">
          <div>成功 {{ route.successCount || 0 }} / å¤±è´¥ {{ route.failCount || 0 }} / è¿žç»­å¤±è´¥ {{ route.consecutiveFailCount || 0 }}</div>
          <div class="light">冷却到: {{ formatDateTime(route.cooldownUntil) }}</div>
          <div class="light">最近错误: {{ route.lastError || '-' }}</div>
        </div>
        <div class="route-actions">
          <div class="action-left">
            <el-button type="primary" size="mini" @click="saveRoute(route)">保存</el-button>
            <el-button size="mini" :loading="route.__testing === true" @click="testRoute(route)">
              {{ route.__testing === true ? '测试中...' : '测试' }}
            </el-button>
          </div>
          <div class="action-right">
            <el-dropdown trigger="click" @command="function(cmd){ handleRouteCommand(cmd, route, idx); }">
              <el-button size="mini" plain>
                æ›´å¤š<i class="el-icon-arrow-down el-icon--right"></i>
              </el-button>
              <el-dropdown-menu slot="dropdown">
                <el-dropdown-item command="cooldown" :disabled="!route.id">清冷却</el-dropdown-item>
                <el-dropdown-item command="delete" divided>删除</el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </div>
        </div>
      </div>
    </div>
  </div>
  <el-dialog title="LLM调用日志" :visible.sync="logDialogVisible" width="88%" :close-on-click-modal="false">
    <div class="log-toolbar">
      <el-select v-model="logQuery.scene" size="mini" clearable placeholder="场景" style="width:180px;">
        <el-option label="chat" value="chat"></el-option>
        <el-option label="chat_completion" value="chat_completion"></el-option>
        <el-option label="chat_completion_tools" value="chat_completion_tools"></el-option>
        <el-option label="chat_stream" value="chat_stream"></el-option>
        <el-option label="chat_stream_tools" value="chat_stream_tools"></el-option>
      </el-select>
      <el-select v-model="logQuery.success" size="mini" clearable placeholder="结果" style="width:120px;">
        <el-option label="成功" :value="1"></el-option>
        <el-option label="失败" :value="0"></el-option>
      </el-select>
      <el-input v-model="logQuery.traceId" size="mini" placeholder="traceId" style="width:260px;"></el-input>
      <el-button type="primary" size="mini" @click="loadLogs(1)">查询</el-button>
      <el-button size="mini" @click="resetLogQuery">重置</el-button>
      <el-button type="danger" plain size="mini" @click="clearLogs">清空日志</el-button>
    </div>
    <el-table :data="logPage.records" border stripe height="56vh" v-loading="logLoading" :header-cell-style="{background:'#f7f9fc', color:'#2e3a4d', fontWeight:600}">
      <el-table-column label="时间" width="165">
        <template slot-scope="scope">
          {{ formatDateTime(scope.row.createTime) }}
        </template>
      </el-table-column>
      <el-table-column prop="scene" label="场景" width="165"></el-table-column>
      <el-table-column prop="attemptNo" label="尝试" width="70"></el-table-column>
      <el-table-column prop="routeName" label="路由" width="170"></el-table-column>
      <el-table-column prop="model" label="模型" width="150"></el-table-column>
      <el-table-column label="结果" width="85">
        <template slot-scope="scope">
          <el-tag size="mini" :type="scope.row.success === 1 ? 'success' : 'danger'">
            {{ scope.row.success === 1 ? '成功' : '失败' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="httpStatus" label="状态码" width="90"></el-table-column>
      <el-table-column prop="latencyMs" label="耗时(ms)" width="95"></el-table-column>
      <el-table-column prop="traceId" label="TraceId" width="230"></el-table-column>
      <el-table-column label="错误" min-width="220">
        <template slot-scope="scope">
          <div class="log-text">{{ scope.row.errorMessage || '-' }}</div>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="120" fixed="right">
        <template slot-scope="scope">
          <el-button type="text" size="mini" @click="showLogDetail(scope.row)">详情</el-button>
          <el-button type="text" size="mini" style="color:#F56C6C;" @click="deleteLog(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <div style="margin-top:10px;text-align:right;">
      <el-pagination
        background
        layout="total, prev, pager, next"
        :current-page="logPage.curr"
        :page-size="logPage.limit"
        :total="logPage.total"
        @current-change="loadLogs">
      </el-pagination>
    </div>
  </el-dialog>
  <el-dialog :title="logDetailTitle || '日志详情'" :visible.sync="logDetailVisible" width="82%" :close-on-click-modal="false" append-to-body>
    <div class="log-detail-body">{{ logDetailText || '-' }}</div>
    <span slot="footer" class="dialog-footer">
      <el-button size="mini" @click="copyText(logDetailText)">复制全文</el-button>
      <el-button type="primary" size="mini" @click="logDetailVisible = false">关闭</el-button>
    </span>
  </el-dialog>
</div>
<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>
  new Vue({
    el: '#app',
    data: function() {
      return {
        headerIcon: getAiIconHtml(34, 34),
        loading: false,
        routes: [],
        logDialogVisible: false,
        logLoading: false,
        logDetailVisible: false,
        logDetailTitle: '',
        logDetailText: '',
        logQuery: {
          scene: '',
          success: '',
          traceId: ''
        },
        logPage: {
          records: [],
          curr: 1,
          limit: 20,
          total: 0
        }
      };
    },
    computed: {
      summary: function() {
        var now = Date.now();
        var total = this.routes.length;
        var enabled = 0, quotaSwitch = 0, errorSwitch = 0, cooling = 0;
        for (var i = 0; i < this.routes.length; i++) {
          var x = this.routes[i];
          if (x.status === 1) enabled++;
          if (x.switchOnQuota === 1) quotaSwitch++;
          if (x.switchOnError === 1) errorSwitch++;
          if (x.cooldownUntil && new Date(x.cooldownUntil).getTime() > now) cooling++;
        }
        return { total: total, enabled: enabled, quotaSwitch: quotaSwitch, errorSwitch: errorSwitch, cooling: cooling };
      }
    },
    methods: {
      formatDateTime: function(input) {
        if (!input) return '-';
        var d = input instanceof Date ? input : new Date(input);
        if (isNaN(d.getTime())) return String(input);
        var pad = function(n) { return n < 10 ? ('0' + n) : String(n); };
        var y = d.getFullYear();
        var m = pad(d.getMonth() + 1);
        var day = pad(d.getDate());
        var h = pad(d.getHours());
        var mm = pad(d.getMinutes());
        var s = pad(d.getSeconds());
        return y + '-' + m + '-' + day + ' ' + h + ':' + mm + ':' + s;
      },
      isRouteCooling: function(route) {
        if (!route || !route.cooldownUntil) return false;
        var x = new Date(route.cooldownUntil).getTime();
        return !isNaN(x) && x > Date.now();
      },
      routeCardClass: function(route) {
        return {
          cooling: this.isRouteCooling(route),
          disabled: route && route.status !== 1
        };
      },
      copyApiKey: function(route) {
        var self = this;
        var text = route && route.apiKey ? String(route.apiKey) : '';
        if (!text) {
          self.$message.warning('API Key ä¸ºç©º');
          return;
        }
        var afterCopy = function(ok) {
          if (ok) self.$message.success('API Key å·²å¤åˆ¶');
          else self.$message.error('复制失败,请手动复制');
        };
        if (navigator && navigator.clipboard && window.isSecureContext) {
          navigator.clipboard.writeText(text)
            .then(function(){ afterCopy(true); })
            .catch(function(){ afterCopy(false); });
          return;
        }
        var ta = document.createElement('textarea');
        ta.value = text;
        ta.setAttribute('readonly', 'readonly');
        ta.style.position = 'fixed';
        ta.style.left = '-9999px';
        document.body.appendChild(ta);
        ta.focus();
        ta.select();
        var ok = false;
        try {
          ok = document.execCommand('copy');
        } catch (e) {
          ok = false;
        }
        document.body.removeChild(ta);
        afterCopy(ok);
      },
      copyText: function(text) {
        var self = this;
        var val = text ? String(text) : '';
        if (!val) {
          self.$message.warning('没有可复制内容');
          return;
        }
        var done = function(ok) {
          if (ok) self.$message.success('已复制');
          else self.$message.error('复制失败,请手动复制');
        };
        if (navigator && navigator.clipboard && window.isSecureContext) {
          navigator.clipboard.writeText(val).then(function(){ done(true); }).catch(function(){ done(false); });
          return;
        }
        var ta = document.createElement('textarea');
        ta.value = val;
        ta.setAttribute('readonly', 'readonly');
        ta.style.position = 'fixed';
        ta.style.left = '-9999px';
        document.body.appendChild(ta);
        ta.focus();
        ta.select();
        var ok = false;
        try {
          ok = document.execCommand('copy');
        } catch (e) {
          ok = false;
        }
        document.body.removeChild(ta);
        done(ok);
      },
      authHeaders: function() {
        return { 'token': localStorage.getItem('token') };
      },
      exportRoutes: function() {
        var self = this;
        fetch(baseUrl + '/ai/llm/config/export/auth', { headers: self.authHeaders() })
          .then(function(r){ return r.json(); })
          .then(function(res){
            if (!res || res.code !== 200) {
              self.$message.error((res && res.msg) ? res.msg : '导出失败');
              return;
            }
            var payload = res.data || {};
            var text = JSON.stringify(payload, null, 2);
            var name = 'llm_routes_' + self.buildExportTimestamp() + '.json';
            var blob = new Blob([text], { type: 'application/json;charset=utf-8' });
            var a = document.createElement('a');
            a.href = URL.createObjectURL(blob);
            a.download = name;
            document.body.appendChild(a);
            a.click();
            setTimeout(function() {
              URL.revokeObjectURL(a.href);
              document.body.removeChild(a);
            }, 0);
            self.$message.success('导出成功');
          })
          .catch(function(){
            self.$message.error('导出失败');
          });
      },
      buildExportTimestamp: function() {
        var d = new Date();
        var pad = function(n) { return n < 10 ? ('0' + n) : String(n); };
        return d.getFullYear()
          + pad(d.getMonth() + 1)
          + pad(d.getDate())
          + '_'
          + pad(d.getHours())
          + pad(d.getMinutes())
          + pad(d.getSeconds());
      },
      triggerImport: function() {
        var input = this.$refs.importFileInput;
        if (!input) return;
        input.value = '';
        input.click();
      },
      handleImportFileChange: function(evt) {
        var self = this;
        var files = evt && evt.target && evt.target.files ? evt.target.files : null;
        var file = files && files.length > 0 ? files[0] : null;
        if (!file) return;
        var reader = new FileReader();
        reader.onload = function(e) {
          var text = e && e.target ? e.target.result : '';
          var parsed;
          try {
            parsed = JSON.parse(text || '{}');
          } catch (err) {
            self.$message.error('JSON æ ¼å¼ä¸æ­£ç¡®');
            return;
          }
          var routes = self.extractImportRoutes(parsed);
          if (!routes || routes.length === 0) {
            self.$message.warning('未找到可导入的 routes');
            return;
          }
          self.$confirm(
            '请选择导入方式:覆盖导入会先清空现有路由;点击“合并导入”则按ID更新或新增。',
            '导入确认',
            {
              type: 'warning',
              distinguishCancelAndClose: true,
              confirmButtonText: '覆盖导入',
              cancelButtonText: '合并导入',
              closeOnClickModal: false
            }
          ).then(function() {
            self.doImportRoutes(routes, true);
          }).catch(function(action) {
            if (action === 'cancel') {
              self.doImportRoutes(routes, false);
            }
          });
        };
        reader.onerror = function() {
          self.$message.error('读取文件失败');
        };
        reader.readAsText(file, 'utf-8');
      },
      extractImportRoutes: function(parsed) {
        if (Array.isArray(parsed)) return parsed;
        if (!parsed || typeof parsed !== 'object') return [];
        if (Array.isArray(parsed.routes)) return parsed.routes;
        if (parsed.data && Array.isArray(parsed.data.routes)) return parsed.data.routes;
        if (Array.isArray(parsed.data)) return parsed.data;
        return [];
      },
      doImportRoutes: function(routes, replace) {
        var self = this;
        fetch(baseUrl + '/ai/llm/config/import/auth', {
          method: 'POST',
          headers: Object.assign({ 'Content-Type': 'application/json' }, self.authHeaders()),
          body: JSON.stringify({
            replace: replace === true,
            routes: routes
          })
        })
          .then(function(r){ return r.json(); })
          .then(function(res){
            if (!res || res.code !== 200) {
              self.$message.error((res && res.msg) ? res.msg : '导入失败');
              return;
            }
            var d = res.data || {};
            var msg = '导入完成:新增 ' + (d.inserted || 0)
              + ',更新 ' + (d.updated || 0)
              + ',跳过 ' + (d.skipped || 0);
            if (d.errorCount && d.errorCount > 0) {
              msg += ',异常 ' + d.errorCount;
            }
            self.$message.success(msg);
            if (Array.isArray(d.errors) && d.errors.length > 0) {
              self.$alert(d.errors.join('\n'), '导入异常明细(最多20条)', {
                confirmButtonText: '确定',
                type: 'warning'
              });
            }
            self.loadRoutes();
          })
          .catch(function(){
            self.$message.error('导入失败');
          });
      },
      handleRouteCommand: function(command, route, idx) {
        if (command === 'test') return this.testRoute(route);
        if (command === 'save') return this.saveRoute(route);
        if (command === 'cooldown') return this.clearCooldown(route);
        if (command === 'delete') return this.deleteRoute(route, idx);
      },
      openLogDialog: function() {
        this.logDialogVisible = true;
        this.loadLogs(1);
      },
      resetLogQuery: function() {
        this.logQuery.scene = '';
        this.logQuery.success = '';
        this.logQuery.traceId = '';
        this.loadLogs(1);
      },
      buildLogQuery: function(curr) {
        var q = [];
        q.push('curr=' + encodeURIComponent(curr || 1));
        q.push('limit=' + encodeURIComponent(this.logPage.limit));
        if (this.logQuery.scene) q.push('scene=' + encodeURIComponent(this.logQuery.scene));
        if (this.logQuery.success !== '' && this.logQuery.success !== null && this.logQuery.success !== undefined) {
          q.push('success=' + encodeURIComponent(this.logQuery.success));
        }
        if (this.logQuery.traceId) q.push('traceId=' + encodeURIComponent(this.logQuery.traceId));
        return q.join('&');
      },
      loadLogs: function(curr) {
        var self = this;
        self.logLoading = true;
        fetch(baseUrl + '/ai/llm/log/list/auth?' + self.buildLogQuery(curr), { headers: self.authHeaders() })
          .then(function(r){ return r.json(); })
          .then(function(res){
            self.logLoading = false;
            if (!res || res.code !== 200) {
              self.$message.error((res && res.msg) ? res.msg : '日志加载失败');
              return;
            }
            var p = res.data || {};
            self.logPage.records = Array.isArray(p.records) ? p.records : [];
            self.logPage.curr = p.current || curr || 1;
            self.logPage.limit = p.size || self.logPage.limit;
            self.logPage.total = p.total || 0;
          })
          .catch(function(){
            self.logLoading = false;
            self.$message.error('日志加载失败');
          });
      },
      showLogDetail: function(row) {
        var text = ''
          + '时间: ' + this.formatDateTime(row.createTime) + '\n'
          + 'TraceId: ' + (row.traceId || '-') + '\n'
          + '场景: ' + (row.scene || '-') + '\n'
          + '路由: ' + (row.routeName || '-') + '\n'
          + '模型: ' + (row.model || '-') + '\n'
          + '状态码: ' + (row.httpStatus != null ? row.httpStatus : '-') + '\n'
          + '耗时: ' + (row.latencyMs != null ? row.latencyMs : '-') + ' ms\n'
          + '结果: ' + (row.success === 1 ? '成功' : '失败') + '\n'
          + '错误: ' + (row.errorMessage || '-') + '\n\n'
          + '请求:\n' + (row.requestContent || '-') + '\n\n'
          + '响应:\n' + (row.responseContent || '-');
        this.logDetailTitle = '日志详情 - ' + (row.traceId || row.id || '');
        this.logDetailText = text;
        this.logDetailVisible = true;
      },
      deleteLog: function(row) {
        var self = this;
        if (!row || !row.id) return;
        self.$confirm('确定删除该日志吗?', '提示', { type: 'warning' }).then(function() {
          fetch(baseUrl + '/ai/llm/log/delete/auth?id=' + encodeURIComponent(row.id), {
            method: 'POST',
            headers: self.authHeaders()
          })
            .then(function(r){ return r.json(); })
            .then(function(res){
              if (res && res.code === 200) {
                self.$message.success('删除成功');
                self.loadLogs(self.logPage.curr);
              } else {
                self.$message.error((res && res.msg) ? res.msg : '删除失败');
              }
            })
            .catch(function(){
              self.$message.error('删除失败');
            });
        }).catch(function(){});
      },
      clearLogs: function() {
        var self = this;
        self.$confirm('确定清空全部LLM调用日志吗?', '提示', { type: 'warning' }).then(function() {
          fetch(baseUrl + '/ai/llm/log/clear/auth', {
            method: 'POST',
            headers: self.authHeaders()
          })
            .then(function(r){ return r.json(); })
            .then(function(res){
              if (res && res.code === 200) {
                self.$message.success('已清空');
                self.loadLogs(1);
              } else {
                self.$message.error((res && res.msg) ? res.msg : '清空失败');
              }
            })
            .catch(function(){
              self.$message.error('清空失败');
            });
        }).catch(function(){});
      },
      loadRoutes: function() {
        var self = this;
        self.loading = true;
        fetch(baseUrl + '/ai/llm/config/list/auth', { headers: self.authHeaders() })
          .then(function(r){ return r.json(); })
          .then(function(res){
            self.loading = false;
            if (res && res.code === 200) {
              self.routes = Array.isArray(res.data) ? res.data : [];
            } else {
              self.$message.error((res && res.msg) ? res.msg : '加载失败');
            }
          })
          .catch(function(){
            self.loading = false;
            self.$message.error('加载失败');
          });
      },
      addRoute: function() {
        this.routes.unshift({
          id: null,
          name: '',
          baseUrl: '',
          apiKey: '',
          model: '',
          thinking: 0,
          priority: 100,
          status: 1,
          switchOnQuota: 1,
          switchOnError: 1,
          cooldownSeconds: 300,
          successCount: 0,
          failCount: 0,
          consecutiveFailCount: 0,
          cooldownUntil: null,
          lastError: null
        });
      },
      buildPayload: function(route) {
        return {
          id: route.id,
          name: route.name,
          baseUrl: route.baseUrl,
          apiKey: route.apiKey,
          model: route.model,
          thinking: route.thinking,
          priority: route.priority,
          status: route.status,
          switchOnQuota: route.switchOnQuota,
          switchOnError: route.switchOnError,
          cooldownSeconds: route.cooldownSeconds,
          memo: route.memo
        };
      },
      saveRoute: function(route) {
        var self = this;
        fetch(baseUrl + '/ai/llm/config/save/auth', {
          method: 'POST',
          headers: Object.assign({ 'Content-Type': 'application/json' }, self.authHeaders()),
          body: JSON.stringify(self.buildPayload(route))
        })
          .then(function(r){ return r.json(); })
          .then(function(res){
            if (res && res.code === 200) {
              self.$message.success('保存成功');
              self.loadRoutes();
            } else {
              self.$message.error((res && res.msg) ? res.msg : '保存失败');
            }
          })
          .catch(function(){
            self.$message.error('保存失败');
          });
      },
      deleteRoute: function(route, idx) {
        var self = this;
        if (!route.id) {
          self.routes.splice(idx, 1);
          return;
        }
        self.$confirm('确定删除该路由吗?', '提示', { type: 'warning' }).then(function() {
        fetch(baseUrl + '/ai/llm/config/delete/auth?id=' + encodeURIComponent(route.id), {
          method: 'POST',
          headers: self.authHeaders()
        })
          .then(function(r){ return r.json(); })
          .then(function(res){
            if (res && res.code === 200) {
              self.$message.success('删除成功');
              self.loadRoutes();
            } else {
              self.$message.error((res && res.msg) ? res.msg : '删除失败');
            }
          })
          .catch(function(){
            self.$message.error('删除失败');
          });
        }).catch(function(){});
      },
      clearCooldown: function(route) {
        var self = this;
        if (!route.id) return;
        fetch(baseUrl + '/ai/llm/config/clearCooldown/auth?id=' + encodeURIComponent(route.id), {
          method: 'POST',
          headers: self.authHeaders()
        })
          .then(function(r){ return r.json(); })
          .then(function(res){
            if (res && res.code === 200) {
              self.$message.success('已清除冷却');
              self.loadRoutes();
            } else {
              self.$message.error((res && res.msg) ? res.msg : '操作失败');
            }
          })
          .catch(function(){
            self.$message.error('操作失败');
          });
      },
      testRoute: function(route) {
        var self = this;
        if (route.__testing === true) return;
        if (!route.id) {
          self.$message.warning('当前是未保存配置,测试通过后仍需先保存才会生效');
        }
        self.$set(route, '__testing', true);
        fetch(baseUrl + '/ai/llm/config/test/auth', {
          method: 'POST',
          headers: Object.assign({ 'Content-Type': 'application/json' }, self.authHeaders()),
          body: JSON.stringify(self.buildPayload(route))
        })
          .then(function(r){ return r.json(); })
          .then(function(res){
            if (!res || res.code !== 200) {
              self.$message.error((res && res.msg) ? res.msg : '测试失败');
              return;
            }
            var data = res.data || {};
            var ok = data.ok === true;
            var title = ok ? '测试成功' : '测试失败';
            var msg = ''
              + '路由: ' + (route.name || '-') + '\n'
              + 'Base URL: ' + (route.baseUrl || '-') + '\n'
              + '状态码: ' + (data.statusCode != null ? data.statusCode : '-') + '\n'
              + '耗时: ' + (data.latencyMs != null ? data.latencyMs : '-') + ' ms\n'
              + '结果: ' + (data.message || '-') + '\n'
              + '返回片段: ' + (data.responseSnippet || '-');
            self.$alert(msg, title, { confirmButtonText: '确定', type: ok ? 'success' : 'error' });
          })
          .catch(function(){
            self.$message.error('测试失败');
          })
          .finally(function(){
            self.$set(route, '__testing', false);
          });
      }
    },
    mounted: function() {
      this.loadRoutes();
    }
  });
</script>
</body>
</html>