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 wrapper = new EntityWrapper<>(); wrapper.orderBy("priority", true).orderBy("id", true); List 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 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 wrapper = new EntityWrapper<>(); wrapper.orderBy("priority", true).orderBy("id", true); List list = llmRouteConfigService.selectList(wrapper); List> routes = new ArrayList<>(); if (list != null) { for (LlmRouteConfig cfg : list) { routes.add(exportRow(cfg)); } } HashMap 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 errors = new ArrayList<>(); List 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()); } HashMap dbById = new HashMap<>(); if (!replace) { List 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 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 exportRow(LlmRouteConfig cfg) { LinkedHashMap 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(); } }