|
|
@@ -0,0 +1,343 @@
|
|
|
+# 稳定性与易用性分析与优化报告(ai-server)
|
|
|
+
|
|
|
+> 审计日期:2026-09-30
|
|
|
+> 范围:`ai-server/src/main/java`(约 860 文件、53 个 Controller、216 个端点)+ 配置 + 文档
|
|
|
+> 方法:3 轮全仓扫描(容错与资源管理 / 异常处理与可观测性 / API 易用性与开发者体验),Top 发现经人工源码核实。
|
|
|
+> 前置说明:**并发问题已在 `concurrency-issues-and-fixes.md` 闭环(29 项,27 项已修复),本报告不含并发类问题。**
|
|
|
+> 结论计数:**稳定性 17 项(高 3 / 中 8 / 低 6)| 可观测性 12 项(高 3 / 中 6 / 低 3)| 易用性 22 项(高 4 / 中高 3 / 中 8 / 低 7)**。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 0.1 修复实施记录(2026-09-30,用户指定三项)
|
|
|
+
|
|
|
+本报告中以下三项已实施并测试通过(Top10 表中 #1、#5、#10 的稳定性部分):
|
|
|
+
|
|
|
+| 项 | 实施 | 改动 |
|
|
|
+|---|---|---|
|
|
|
+| **O-1 PII 日志** | 已修复 | `AbstractDataCleaner`:提取结果日志改为脱敏输出(新增 `maskForLog`:保留前4后2、短值留首字符),"预处理后文本"整段内容不再打印(只留长度,降 debug);`GraphService` 的 `System.out.println(sql)`(含手机号/卡号节点值)移除;删除整文件注释的死代码 `GraphServicecopy.java`(613 行,内含同类打印) |
|
|
|
+| **S-2 ES 写入主链路** | 已修复 | `SearchDocIndexService`:`add()/flush()` 全部容错化(ES 故障记日志+丢弃计数,绝不上抛清洗链路);新增熔断——连续 3 批失败后熔断 60s,期间 `add()` 直接跳过(不再每批承受 60s 超时),成功即复位。缺失数据按案件重建索引补偿。新增 `SearchDocIndexServiceTest`(4 用例:不上抛/熔断开/成功复位/flush 降级) |
|
|
|
+| **S-4 附件 RAG** | 已修复 | ① `ChatAttachmentService` 路径 C 检索失败降级为"无 RAG 片段的普通回答"(fail-open,不阻断整条消息);② `AttachmentDocIndexer.index()` 增加单附件总预算 `zsjz.chat-attachment.index-budget-seconds`(默认 600s),超时后剩余 chunk 跳过并置 truncated,杜绝上传请求被占住几十分钟。新增 `AttachmentDocIndexerTest`(3 用例) |
|
|
|
+
|
|
|
+新增测试 7 个全部通过;全量 370 个测试仅剩 HEAD 既有的 8 个过时断言(工具注册数,与本轮无关),零新增回归。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 0. 结论摘要
|
|
|
+
|
|
|
+**一句话**:系统的"骨架"是健康的(异常处理框架、流式解析、AI 清洗看门狗、LLM 失败语义都是样板级),但存在三类系统性风险——
|
|
|
+
|
|
|
+1. **稳定性:外部依赖 failure mode 不对等**。直连 LLM 有超时重试、ES 查询侧 fail-open,但**意图识别 `.block()` 无超时**(配置项存在却没接线,挂死会永久卡住会话锁)、**ES 在清洗写入主链路上无任何容错**(ES 挂 = 文件级清洗失败 + 半截数据)、**对话主流无应用层超时**。这三处都是"好的模式已经存在,只是没铺到位"。
|
|
|
+2. **稳定性:静默失败仍然存在**。治理"创建时序图"吞掉 SQLException 还向用户报完成;预处理整批失败返回空列表(精心设计的报错被自己的 catch 吞掉);`file_ai_profile` 的 PENDING 态无看门狗,重启后永久卡死;dm 清洗与治理**没有 ai_clean 那样的重启自愈**,且全项目无优雅停机配置。
|
|
|
+3. **合规级问题:测绘核心 PII(姓名/卡号/证件号)逐条打进 info 日志**,且 logback `root=debug` 仅控制台输出、无文件无滚动——量大、无档、敏感。这是全报告优先级最高的一条。
|
|
|
+
|
|
|
+易用性侧的系统性问题只有一个:**新模块(agent/insight/aiclean)与旧模块像两代人所写**——前者 REST + VO + @Valid + 测试,后者 RPC 动词 + 实体直出 + 零校验;同一语义"删除"有 5 种接口形状、分页 4 种形状、"每页条数"3 个名字。逐个端点修补无意义,需要一次契约层统一。
|
|
|
+
|
|
|
+**Top 10 行动项(按性价比排序)**:
|
|
|
+
|
|
|
+| # | 问题 | 严重度 | 类型 | 工作量 |
|
|
|
+|---|---|---|---|---|
|
|
|
+| 1 | PII 逐条进 info 日志 + root=debug 无文件日志 | 高 | 合规/稳定 | 0.5 天 |
|
|
|
+| 2 | 意图识别 `.block()` 无超时(配置项未接线)→ 卡死会话锁 | 高 | 稳定 | 0.5 天 |
|
|
|
+| 3 | 治理 createTimeSeries 吞 SQLException 还报完成 | 高 | 稳定 | 0.5 天 |
|
|
|
+| 4 | 预处理整批失败吞 ServerException 返回空列表 | 高 | 稳定 | 0.5 天 |
|
|
|
+| 5 | ES 移出清洗写入主链路(旁路化 + 失败降级) | 高 | 稳定 | 1-2 天 |
|
|
|
+| 6 | SheetProbe POI 全量加载改 SAX(OOM 风险) | 中高 | 稳定 | 1 天 |
|
|
|
+| 7 | dm 清洗/govern 重启自愈(对齐 ai_clean 看门狗)+ 优雅停机 | 中高 | 稳定 | 2 天 |
|
|
|
+| 8 | `file_ai_profile` PENDING 态补看门狗 | 中 | 稳定 | 0.5 天 |
|
|
|
+| 9 | SSE 断线丢进度:事件落缓冲支持回放(或 /dm、/govern 补轮询通道) | 中高 | 可观测 | 1-2 天 |
|
|
|
+| 10 | API 契约统一(错误码枚举、统一分页 VO、写操作改 POST) | 中高 | 易用 | 渐进 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 稳定性
|
|
|
+
|
|
|
+### 1.1 外部调用容错
|
|
|
+
|
|
|
+#### S-1(高)意图识别 LLM 调用无超时——超时配置项存在但从未接线
|
|
|
+
|
|
|
+- **位置**:`module/agent/intent/IntentService.java:84`
|
|
|
+- **现状**:`agent.call(userMsg, IntentResult.class).block()` 无任何 timeout。而 `IntentProperties.java:17` 定义了 `timeoutSeconds = 5`(注释写明"超时即降级为原始问题直发"),但 IntentService/IntentMiddleware 全程未引用该配置——**配置是死的**。对照:FollowupService 有 `blockLast(Duration.ofSeconds(8))` + 全 catch(`FollowupService.java:85-91`),Intent 漏了同款。
|
|
|
+- **触发**:模型端点半开连接(TCP 建立但不吐数据不 RST)——反代黑洞、厂商网关 hang。
|
|
|
+- **后果**:`IntentMiddleware.onAgent` 在主对话 Flux 管线内**同步**调用;fail-open 只覆盖"抛异常",不覆盖"永不返回"——整条 SSE 无任何输出,且 chat 会话锁在 doFinally 释放,流不终结 → **该会话此后永远收到"正在回复中"**。
|
|
|
+- **修复**:`.block(Duration.ofSeconds(props.getTimeoutSeconds()))` + TimeoutException 并入 fail-open 分支;补一条单测覆盖"配置超时生效"。
|
|
|
+
|
|
|
+#### S-2(高)Elasticsearch 在批量清洗写入主链路上,无容错——ES 挂 = 文件级清洗失败 + 半截数据
|
|
|
+
|
|
|
+- **位置**:`module/dm/clean/AbstractCallRecordDataLoader.java:142`(AbstractTransRecordDataLoader.java:107 及各 External*Loader 同型);写入服务 `module/search/service/SearchDocIndexService.java:79-95`
|
|
|
+- **现状**:每个 loader 的 `doWrite` 逐行调 `searchDocIndexService.add(...)`,**无 try/catch**;add 攒满 1000 条时同步 `repository.saveAll(batch)` 直连 ES。异常沿链路抛到 `CleanTask.java:108-119` → 文件标 `FILE_READ_FAIL`。
|
|
|
+- **后果**:清洗过程中 ES 宕机/滚动重启 → 该文件清洗中断,DuckDB 业务表留下前 N 行半截数据,多用户大批量导入时 ES 单点拖垮数据管理主链路。讽刺的是**查询侧反而是 fail-open 的**(SearchQueryService.java:75-78 返回空列表)——写入侧比查询侧脆。ES 假死时 socket-timeout 60s(application-dev.yaml:27-28),每批先挂 60s。失败后 `SearchDocIndexService.buffers` 里该案件尾批(≤999 条)滞留内存。
|
|
|
+- **修复**:全文检索定位为**旁路能力**——`add()` 内部 catch(ES 失败只 log + 计数,绝不上抛清洗链路),另加"ES 连续失败 N 批后熔断 60s"防止 60s 超时反复拖慢清洗;搜索缺失数据靠文件级重建索引补偿(`/search` 已有按案件删除重建的底座)。
|
|
|
+
|
|
|
+#### S-3(中高)对话主流无整体超时、无应用层心跳
|
|
|
+
|
|
|
+- **位置**:`module/agent/service/impl/AgentChatServiceImpl.java`(doStreamLocked 的 `agent.streamEvents` 无 timeout 操作符);`AgentModelFactory.create`(未设置连接/读超时);chat SSE 无心跳(SseService 的 5s 心跳只覆盖清洗/治理通道)。
|
|
|
+- **后果**:模型流建连后长时间不发首事件 → SSE 挂住无 error 帧(onErrorResume 只接"抛异常"),会话锁到客户端断连才释放;经代理部署时空闲连接被掐,前端表现"永久转圈后断开"。
|
|
|
+- **修复**:`events.timeout(Duration.ofMinutes(2), fallbackError)`(首事件超时)+ 给 chat SSE 加与清洗通道同款心跳帧。
|
|
|
+
|
|
|
+#### S-4(中)附件大文档 RAG:检索失败阻断整条消息;上传同步索引无总预算
|
|
|
+
|
|
|
+- **位置**:`module/agent/attachment/ChatAttachmentService.java:290`(DOCUMENT 分支 `docIndexer.search(...)` 无 try/catch);`AttachmentDocIndexer.java:96-153`(500 chunk × 每块 60s 超时,无总预算)。
|
|
|
+- **后果**:嵌入模型 API 故障时,用户对带大文档附件的会话提问**整条消息失败**,而不是降级为"不带 RAG 片段的普通回答";上传侧最坏 500/8×60s ≈ 62 分钟占住 Tomcat 请求线程。
|
|
|
+- **修复**:① buildBlocks 的 DOCUMENT 分支整体 try/catch,失败 log.warn 后返回空 RAG 块继续对话(对齐 ES 查询侧的 fail-open);② 上传索引改为后台虚拟线程任务 + indexStatus 轮询(上传请求立即返回),或至少加总预算(如 10 分钟)超时。
|
|
|
+
|
|
|
+#### S-5(中·已知决策)Redis 是全站认证硬依赖,无降级
|
|
|
+
|
|
|
+- **位置**:`application-dev.yaml:19-22`;application.yaml:53-54 注释自认"Redis 不可用则所有 StpUtil.* 抛异常,无降级"。
|
|
|
+- **后果**:Redis 抖动 → 全站请求 500(且不是 NotLoginException,落不到 401 处理,用户看到"系统异常")。这是注释里的显式架构选择,如实记录;**建议至少做一件事**:GlobalExceptionHandler 对 `org.springframework.data.redis.RedisConnectionFailureException` 给出专属文案"认证服务暂不可用,请稍后重试",把 500 变成可理解的提示。
|
|
|
+
|
|
|
+### 1.2 资源与内存
|
|
|
+
|
|
|
+#### S-6(中高·OOM 风险)SheetProbe 用 POI usermodel 全量加载 workbook——全链路唯一的全量内存点
|
|
|
+
|
|
|
+- **位置**:`module/aiclean/probe/SheetProbe.java:62`(`WorkbookFactory.create(file)`,文件上限 80MB)
|
|
|
+- **现状**:80MB xlsx 解压后 XML 数百 MB,POI DOM 模式内存放大 20-50 倍;AI 清洗判定并发闸门默认 3 → 最坏 3 份 usermodel 同时在堆上。主链路预览/清洗都是 Fesod 流式 + DuckDBAppender,**唯独这条探测路径不是**。
|
|
|
+- **修复**:探测只需要前 200 行 → 改 POI SAX(XSSFSheetXMLHandler)或复用 Fesod 流式读,堆占用从数百 MB 降到 KB 级。
|
|
|
+
|
|
|
+#### S-7(中)上传源文件目录无 TTL/孤儿清扫,磁盘增长无上界
|
|
|
+
|
|
|
+- **位置**:`DmService.java`(saveUploadedFile 落 `workspace/{caseId}/upload/{batchId}/`;sweepStaleTmpBatches 只扫 `PathConst.TMP_PATH`;clearUploadBatch 仅用户手动触发)。
|
|
|
+- **场景**:上传后不点清洗就关浏览器 → 200MB×N 源文件永久留存。tmp 批次有 24h TTL、chat 附件有 7 天孤儿清扫,**唯独 DM 上传目录没有**。
|
|
|
+- **修复**:与"重新清洗需要源文件"的语义协调——清扫"无关联 file_info 活动批次且超过 7 天"的 upload 目录(对齐附件孤儿清扫的条件删除范式)。
|
|
|
+
|
|
|
+#### S-8(低)PG 主库 Hikari 零调优
|
|
|
+
|
|
|
+- **位置**:`application-dev.yaml`(无 hikari 节,默认 maximum-pool-size=10),被请求线程 + boundedElastic 异步落库 + GlobalCache.initCache 全表读共享。
|
|
|
+- **修复**:显式配置(如 maximum-pool-size=20、connection-timeout=10s)并压测;顺带给 MyBatis 加慢 SQL 日志(见 O-7)。
|
|
|
+
|
|
|
+#### S-9(低)graph_his 图谱历史 JSON 只增不清(GraphService.java:632);`SearchQueryService.java:62` leading-wildcard 查询仅性能提示。
|
|
|
+
|
|
|
+### 1.3 停机、自愈与状态机
|
|
|
+
|
|
|
+#### S-10(中高)无优雅停机 + dm 清洗/govern 无重启自愈
|
|
|
+
|
|
|
+- **位置**:application.yaml 无 `server.shutdown: graceful` / `spring.lifecycle.timeout-per-shutdown-phase`(Boot 3 默认 IMMEDIATE);`AppStopEndEventListener` 停机只 flush RocksDB。
|
|
|
+- **后果**:SIGTERM/kill -9 时 dm 清洗与治理(单阶段 10-40 分钟)被硬杀:DuckDB 留部分行、govern 的 call_record 可能 truncate 后未重算完。ai_clean 有看门狗启动恢复(好的),**dm 清洗与治理没有**——`FileSheetStateEnum` 没有"清洗中/治理中"中间态,重启后前端看不出"上次没跑完",全靠用户记得重跑。AI 回复落库在停机窗口静默丢。
|
|
|
+- **修复**:① yaml 加 graceful shutdown + 30s timeout;② `FileSheetStateEnum` 增加 `CLEANING/GOVERNING` 中间态,启动时把中间态标为 `FAIL(原因:服务重启中断)`——对齐 AiCleanWatchdog 的启动 sweep 范式,用户可一键重跑。
|
|
|
+
|
|
|
+#### S-11(中)`file_ai_profile` 的 PENDING/RUNNING 态无看门狗,重启后永久卡死
|
|
|
+
|
|
|
+- **位置**:`FileRecognitionService`(内存队列无启动重扫);`FileAiProfileService.java:142`(建档即 PENDING)。
|
|
|
+- **后果**:上传 docx/pdf 落 PENDING → 服务重启 → 档案永远"待识别",前端轮询等不到,唯一恢复方式是删文件重传。这是状态机里唯一没被看门狗覆盖的态(ai_clean 有、清洗失败有 FAIL 态)。
|
|
|
+- **修复**:启动时(或并入现有看门狗)把 PENDING/RUNNING 且 update_time 超过 30 分钟的档案标 FAIL(原因"服务重启中断,请删除后重新上传")。
|
|
|
+
|
|
|
+#### S-12(中)清洗完成回调链路非原子:initCache 失败会跳过人员库落库
|
|
|
+
|
|
|
+- **位置**:`DmService.java` createDefaultCallback(`GlobalCache.initCache()` 在 drain/flush 之前,且无 try/catch)。
|
|
|
+- **后果**:全部 sheet 洗完的瞬间 PG 抖动 → initCache 抛异常 → 本批人员缓冲不落库不清空、SSE 停摆。
|
|
|
+- **修复**:调整顺序为"先 drain+flush(业务数据落库优先)→ 再 initCache",且 initCache 失败只告警不阻断回调其余步骤。
|
|
|
+
|
|
|
+#### S-13(高)治理"创建时序图"吞 SQLException,失败伪装成成功
|
|
|
+
|
|
|
+- **位置**:`GovernService.java` createTimeSeries:`catch (SQLException e)` 只 `log.error("数据库执行错误: {}", e.getMessage())`(无堆栈),随后照样 `sendProgress("创建时序图完成!")`——流水线继续、最终报"全部执行完成"。
|
|
|
+- **后果**:时序图数据静默缺失,用户拿到"治理成功"的错误结论。这是治理链路 fail-fast 改造后**漏网的最后一处**(与 awaitStage 的修复形成对照)。
|
|
|
+- **修复**:抛出让流水线失败中断(对齐其他阶段),SSE 推"创建时序图失败:…";日志带堆栈。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 可观测性(异常处理 / 日志 / 健康 / 指标 / 进度)
|
|
|
+
|
|
|
+### 2.1 总体判断
|
|
|
+
|
|
|
+`GlobalExceptionHandler`(common/exception)是**样板级的好**:15 类异常全覆盖、业务异常 cause 链拆包(救回被 MyBatisSystemException 包住的"请先打开案件")、兜底 500 不泄堆栈且服务端留全栈、401 按前端约定返回 HTTP 200+code 401 且写明理由。问题都在"框架没铺到的地方"。
|
|
|
+
|
|
|
+### 2.2 问题清单
|
|
|
+
|
|
|
+#### O-1(高·合规)测绘核心 PII 逐条打进 info 日志
|
|
|
+
|
|
|
+- **位置**:`module/dm/clean/AbstractDataCleaner.java:211`(`log.info("预处理后文本:{}", cleanText)`——整段原文含开户人姓名/卡号)、`:244`(`log.info("数据提取完成,结果:姓名={}, 卡号={}, 证件号={}", ...)`);`module/graph/service/GraphService.java:112`(`System.out.println(sql)`——图谱查询 SQL 含手机号/卡号节点值,绕过 logback 直打 stdout);放大器:MyBatis `log-impl: Slf4jImpl` + root=debug 会把 WHERE 子句里的手机号/卡号字面量全量输出。
|
|
|
+- **后果**:清洗是每文件必经链路,等于把案件核心敏感数据全量复制到日志流;无文件滚动则日志留存策略无从谈起。**这是本报告合规优先级最高的一条。**
|
|
|
+- **修复**:两处改 debug + 掩码(卡号/证件号中间段打星);GraphService 改 log.debug 并删除 `GraphServicecopy.java`(613 行死代码文件);见 O-2 调 root。
|
|
|
+
|
|
|
+#### O-2(高·运维)logback:root=debug、仅 CONSOLE、无文件无滚动
|
|
|
+
|
|
|
+- **位置**:`src/main/resources/logback-spring.xml`(全文 8 行)。
|
|
|
+- **后果**:日志全靠 stdout 重定向;debug 级让 DuckDB/Reactor/Hikari/MyBatis 全量输出,量级放大;重启即丢,无从排障。
|
|
|
+- **修复**:root=info + RollingFileAppender(按天+大小滚动、保留 30 天)+ 按包分级(com.zsjz=info, org.springframework=warn);用 `<springProfile>` 区分 dev(可 debug)/ prod。
|
|
|
+
|
|
|
+#### O-3(高)SSE 流内异常把 `ex.getMessage()` 原样透传前端,未过兜底滤网
|
|
|
+
|
|
|
+- **位置**:`AgentChatServiceImpl.java:305-317`、`InsightServiceImpl.java:354-357`(`onErrorResume` 直接 `sse("error", Map.of("error", ex.getMessage()))`)。
|
|
|
+- **后果**:SSE 流不经过 GlobalExceptionHandler——DataAccessException 等底层异常的 message(含 SQL 片段/表名/驱动细节)直接上屏;NPE 变成"未知错误"用户无从判断。
|
|
|
+- **修复**:仿 GlobalExceptionHandler——unwrap 出 ServerException 用其 message,其余统一"对话处理失败,请稍后重试"并 log.error 带堆栈。**注意与既有"错误文案可行动"的好例子共存**(并发锁"该会话正在回复中"这类是 ServerException,会走 unwrap 保留)。
|
|
|
+
|
|
|
+#### O-4(中高)预处理整批失败吞 ServerException 返回空列表——精心设计的用户报错被自己的 catch 吃掉
|
|
|
+
|
|
|
+- **位置**:`DmService.java:142-146`(`catch (Exception e) { log.error(...) } return List.of()`);同型 `:459-462`(resolvePreviewFiles,try 块内 :454 刻意抛出的"所有文件不存在或格式错误!"被吞)。
|
|
|
+- **后果**:预处理批量链路整体失败时前端只显示 0 个文件,无任何提示。
|
|
|
+- **修复**:catch 分支只兜底非业务异常——先 unwrap ServerException 上抛,其余异常返回空列表 + log。
|
|
|
+
|
|
|
+#### O-5(中高)SSE 断线即丢进度,无回放;/dm 与 /govern 只有 SSE 一条进度通道
|
|
|
+
|
|
|
+- **位置**:`SseService.java:97-111`(emitter 不存在直接跳过推送;事件 id 是随机串,无 Last-Event-ID 回放)。
|
|
|
+- **后果**:治理 10-40 分钟长任务里,用户浏览器断网/休眠/代理踢线期间的所有进度帧永久丢失,重连后干等下一条心跳。
|
|
|
+- **修复**(按成本递增):a) 最小改动——给 /dm 与 /govern 各补一个查询当前批次的轮询接口(对齐 /aiClean/smartClean/status 已有范式);b) 完整方案——SseService 按用户保留最近 N 条事件的环形缓冲,`see()` 带 Last-Event-ID 回放。
|
|
|
+
|
|
|
+#### O-6(中)智能清洗轮询接口不暴露失败原因
|
|
|
+
|
|
|
+- **位置**:`AiSmartCleanService.SmartCleanStatusVO`(返回 stage/计数/needReview,**不含 lastError**)。
|
|
|
+- **后果**:看门狗与 run() 失败时精心写入 lastError("批次心跳超时…请重新发起智能清洗"),前端轮询不到,用户只见 FAILED 不知道为什么、该干什么。job 粒度 failReason 经 /aiClean/jobs 可查(好),批次级出口缺这一列。
|
|
|
+- **修复**:SmartCleanStatusVO 补 `lastError` 字段(脱敏截断)。
|
|
|
+
|
|
|
+#### O-7(中)无 traceId/MDC;GlobalExceptionHandler 兜底日志无请求上下文
|
|
|
+
|
|
|
+- 全仓 0 处 MDC;清洗/治理链路有"【数据治理】+caseId"前缀(好),但一次对话(sessionKey→agent→工具→落库)、一次智能清洗(batchId→jobId)跨多行无关联 ID;兜底"系统异常"日志只能靠线程名反推。
|
|
|
+- **修复**:Servlet Filter 里 `MDC.put("traceId", ...)`(有 caseId/userId 也一并放入)+ logback pattern 输出;成本一天以内。
|
|
|
+
|
|
|
+#### O-8(中)健康检查是假活:`/sys/health` 不探测任何依赖;无 Actuator
|
|
|
+
|
|
|
+- **位置**:`SystemController.java:47-50`(`return Result.succeed()`);pom.xml 无 actuator。
|
|
|
+- **后果**:PG/Redis/ES/LLM 网关任何一个挂掉,/sys/health 依然 200,网关/前端就绪探测被误导。
|
|
|
+- **修复**:升级 /sys/health 为依赖汇总自检(PG `SELECT 1`、Redis ping、ES exists、LLM 配置存在性,任一失败返回 code 非 0 + 各依赖状态明细);不引入 actuator 也够用,引入则加 micrometer 导出更好。
|
|
|
+
|
|
|
+#### O-9(中)无指标、无慢 SQL 观测
|
|
|
+
|
|
|
+- 全仓无 Micrometer/自定义指标;LLM token 用量散落(ai_clean_job 表有 tokenIn/tokenOut——好;UsageStore 仅内存轮次);唯一查询耗时日志在被吞异常的 createTimeSeries 里;DuckDB 大 SQL(治理 30 分钟级语句)无耗时统计。
|
|
|
+- **修复**:① 一个 MyBatis Interceptor 打印 >3s 的慢 SQL(带 caseId 上下文);② LLM 调用计数器(次数/token/耗时/失败)落一张 metrics 表或日志聚合,替代纯内存 UsageStore。
|
|
|
+
|
|
|
+#### O-10(中)日志级别滥用污染信噪比
|
|
|
+
|
|
|
+- GlobalExceptionHandler 里 404/400 类客户端错误全用 `log.error`(:118-192 共 8 处)——污染 error 告警;`GovernService.calcTreePersonData` 循环级 info("根据卡号统计数据!")大案件成倍刷屏;`IntentService.java:89` 预期内的 fail-open 降级用 error(同文件 :141 用 warn,不一致)。
|
|
|
+- **修复**:客户端错误降 warn;循环级 info 降 debug;降级分支统一 warn。
|
|
|
+
|
|
|
+#### O-11(中)"进度条"是假进度 + 失败也推 100%
|
|
|
+
|
|
|
+- `SlowDownProgress.java`:按"越接近 100 越慢"的随机步进模拟,与真实完成量无关;叠加 `GovernService` 失败分支推 `SseDTO(102, 100, "治理失败:…")`——前端按"进度=100 即成功"渲染会误判。
|
|
|
+- **修复**:失败事件的 progress 用当前值而非 100;前端协议增加显式 `status: FAILED` 字段(与文案解耦)。
|
|
|
+
|
|
|
+#### O-12(低)错误码体系形同虚设
|
|
|
+
|
|
|
+- `ErrorEnum` 只有 9 个条目,全仓 `new ServerException(400, ...)` 裸 int 遍地(AgentChatServiceImpl 5 处、ChatAttachmentService、AiSmartCleanService 等),前端只能匹配 message 文本。
|
|
|
+- **修复**:见 E-1(并入易用性契约治理)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 易用性
|
|
|
+
|
|
|
+### 3.1 API 设计一致性(53 Controller / 216 端点)
|
|
|
+
|
|
|
+#### E-1(高)GET 做删除/写操作大面积存在(约 25 处)
|
|
|
+
|
|
|
+- 最危险:`AgentChatController.java:126-129`——**`GET /chat/messages/{messageId}` 实际是删除消息**,URL 无 delete 字样、返回 void,浏览器预取/curl 即删数据。其余:`GET /chat/sessions/{id}/delete`、`/title`(改标题)、`/pin`、`/messages/{id}/star`;`GET /insight/sessions/{id}/delete`;`GET /capability/skills/{name}/delete`、`/toggle`;旧模块 `GET /sdc/delete`、`/pbi/...`、`/case/...`、`/tpl/...`、`/dm/cancelClean`、`/govern/calcTask`(启动任务)等。
|
|
|
+- **后果**:浏览器预取/链接分享/日志重放会误触发删除与任务;无幂等语义可定义。
|
|
|
+- **修复**:写操作改 POST/DELETE。前端 electron 客户端同步改造;对无法马上改的前端,先给这批 GET 加"仅 POST 允许写"不现实——**现实路径**:至少把"URL 不含 delete 字样的 GET 删除"(`/chat/messages/{id}`)改名,其余在契约版本化时一并处理。
|
|
|
+
|
|
|
+#### E-2(高)同一语义"删除"有 5 种接口形状、分页 4 种返回体、"每页条数"3 个名字
|
|
|
+
|
|
|
+- 删除:`GET /sessions/{id}/delete` vs `GET /xxx/delete?id=` vs `POST /user/delete`(body)vs `POST /dm/deletedFiles`(ids 数组)vs `GET /chat/messages/{id}`(无字样)。
|
|
|
+- 分页返回:MP `Page<T>` / `Map{list,total}`(GovernTreeController 7 个端点)/ `IPage`(InsightController)/ 纯 List。
|
|
|
+- 分页入参:`page/limit`(Query 基类,46 类继承)vs `page/size`(AiCleanController)vs `pageSize`(agent 工具层)。
|
|
|
+- **修复**:定义统一契约——`Result<PageVO<T>>{records,total,page,size}` + 删除一律 `POST /xxx/delete`(body 传 id(s))+ 入参统一 Query 基类;**新代码立即执行,旧代码随版本化迁移**。
|
|
|
+
|
|
|
+#### E-3(高)响应包络不统一:agent 系 5 个 Controller 完全裸 VO
|
|
|
+
|
|
|
+- `AgentChatController.java:63` 返回裸 `ChatSessionVO`、多处返回 void;`InsightController.java:94` 直接返回 MP `IPage`;其余 185/216 端点返回 `Result<T>`。前端 defHttp 维护两套 transform(GlobalExceptionHandler 注释自己承认 `/chat/**` 走 `isTransformResponse:false` 特殊路径容易漏处理 401)。
|
|
|
+- **修复**:/chat 系列补 Result 包络(SSE 流除外),前端删掉特殊分支。
|
|
|
+
|
|
|
+#### E-4(中高)实体直接出参,VO 层缺失
|
|
|
+
|
|
|
+- `CallRecordController` `Result<Page<CallRecord>>`、`TransRecordController`、`GovernTreeController` `Result<List<GovernTree>>`、`DmController` `Result<List<GovernConf>>`、`AiCleanController` 返回 `AiCleanJob` 实体。表加字段即破坏前端契约,内部字段无遮蔽。
|
|
|
+- **修复**:出参一律 VO(MapStruct 已在项目内用于 agent 模块,范式现成)。
|
|
|
+
|
|
|
+#### E-5(中高)参数校验覆盖率 8/53 个 Controller
|
|
|
+
|
|
|
+- @Valid 与约束注解全部集中在 agent/embedding 新模块约 12 个 DTO;52 个旧 Controller 的 DTO/Query **零约束注解**;null 直接穿透进 service(`UserController.java:77` `dto == null ? null : dto.getId()` 手写兜底);`SpecialDateService.java:37/70` 注释自我承认"来自请求体(无 @Valid 校验),缺参时不能直接 == 1L(自动拆箱 NPE)"——用防御性补偿代替校验。
|
|
|
+- **修复**:GlobalExceptionHandler 的校验异常处理链已就绪(MethodArgumentNotValid/Bind/ConstraintViolation 全有 handler),只差 DTO 上加注解。优先给写操作 DTO 补 @NotNull/@NotBlank。
|
|
|
+
|
|
|
+#### E-6(中)错误码与文案
|
|
|
+
|
|
|
+- SSE error 帧无错误码字段(只有 message);`LlmService.java:180` 把上游 401/429 原始报文透传终端用户(timeout 已修成 504,说明只修了一半);好的对照:401 文案按 sa-token 类型细分、"请先打开案件后再执行数据治理"可行动。
|
|
|
+- **修复**:SSE error 帧补 `code` 字段并与 Result.code 同枚举(见 O-12);LlmService 按上游状态码映射为面向用户的文案("模型服务认证失败,请检查模型配置")。
|
|
|
+
|
|
|
+#### E-7(中)URL 命名三分天下 + 缩写谜语
|
|
|
+
|
|
|
+- RESTful 派(/agents、/chat/sessions)、RPC 动词派(/call/night/statFirstCallNight,15 个 stat* 控制器同一模板)、缩写谜语派(`/sdc`、`/pbi`、`/pg`、`/gt`、`/tpl`、`/qry`、`/cr`);大小写混用;部分映射缺前导斜杠、注解内多余空格。
|
|
|
+- **修复**:不做大规模重命名(前端改动面太大);**定一条规范写进 AGENTS/CODEX 规则**:新接口必须 `/模块/资源/动作` kebab 或 camel 二选一,禁新缩写;15 个 stat 控制器是同一模板,可抽公共基类一次收敛。
|
|
|
+
|
|
|
+### 3.2 配置易用性
|
|
|
+
|
|
|
+#### E-8(高)无 prod profile + 明文凭证入库
|
|
|
+
|
|
|
+- `application.yaml:10` 写死 `active: dev`;`application-dev.yaml` 内 postgres/postgres、neo4j/neo4j123、注释掉的 ES 密码全部在 git 里。
|
|
|
+- **修复**:`active: ${SPRING_PROFILES_ACTIVE:dev}` + 新增 application-prod.yaml(凭证走环境变量);已泄露的密码轮换。
|
|
|
+
|
|
|
+#### E-9(中)配置注释与实际不一致 + 三处联动硬编码
|
|
|
+
|
|
|
+- `application-dev.yaml:54` 注释写 Redis 是 .109,实际配的 .103;QUICK_START.md 同错且引用不存在的 `ai-frontend/` 目录;`server.port/context-path/storage.local.base-url` 三处必须手工同步改(注释自己警告了)。
|
|
|
+- **修复**:修注释;base-url 用配置引用或启动时校验三者一致性。
|
|
|
+
|
|
|
+### 3.3 开发者体验
|
|
|
+
|
|
|
+#### E-10(中高)死代码与复制粘贴代际
|
|
|
+
|
|
|
+- `GraphServicecopy.java`:613 行整文件块注释的死代码(文件名就叫 copy);`WebConfig.java:28-38` 注释掉的 db1 bean(含个人本机路径);`App.java` 的 init() 死代码。
|
|
|
+- 清洗器带序号复制品:`TransInOutDataCleaner2~6`、`CallDataCleaner1`、`TransStdDataCleaner1`、`TransCoreHisBalanceCleaner2` 等——序号即复制代际;15 个 stat* Controller 同模板复制 15 份;三胞胎 KeyController 逐行同构;`GovernTreeController` 7 个 getTreeXxxPage 仅表名不同。
|
|
|
+- **修复**:死代码直接删(git 有历史);stat 控制器抽基类;清洗器复制品登记到治理清单(行为各异不敢贸然合并,但至少命名与注释标注来源差异)。
|
|
|
+
|
|
|
+#### E-11(中)命名拼写错误与超大类
|
|
|
+
|
|
|
+- 拼写:包名 `govern/serivce`(已知)、`SseService.see()`(sse→see,连锁传播到调用点与注释)、`TrackMeetSummeryDTO`、`/saveGovernCon`、`getMaplocal`、`SUCCEED_CODE`。
|
|
|
+- 超大类(>800 行):`DmService`(967 行且无接口的单体 @Service)、`TransAnalysisTool`(942)、`AgentChatServiceImpl`(874);800 边缘还有 5 个。
|
|
|
+- **修复**:拼写在下一轮触碰这些文件时顺手修(包名迁移除外,动静大);超大类只拆"下一步必改"的 DmService(预览/清洗/删除/上传四块职责天然可分)。
|
|
|
+
|
|
|
+#### E-12(中高)数据库 schema 无版本化迁移
|
|
|
+
|
|
|
+- `sql/` 下 20+ 个无版本脚本(`aasd.sql`、`case_table_1.sql`、`testSql/交易共同.sql`…),无 Flyway/Liquibase,QUICK_START.md:102 承认手工执行且混有 Solon 时代旧表。多人环境无法可重复建库。
|
|
|
+- **修复**:引入 Flyway(baseline 现状)+ 新增变更一律走迁移脚本;测试 SQL 移出 sql/ 目录。
|
|
|
+
|
|
|
+#### E-13(中)文档错位
|
|
|
+
|
|
|
+- `readme.md` 是个人便签(许可证工具 + pmtiles 命令);`ai-server/CODE_WIKI.md`(849 行)是 Solon 时代遗留且未删(QUICK_START 里加了 ⚠ 但没删文件);QUICK_START 本身质量不错。
|
|
|
+- **修复**:readme 重写为"项目是什么 + 怎么起 + 指向 QUICK_START";CODE_WIKI 归档到 docs/legacy 或删除。
|
|
|
+
|
|
|
+### 3.4 前端契约稳定性
|
|
|
+
|
|
|
+#### E-14(高·业务正确性风险)BigDecimal 全局序列化为"千分位字符串"且 null→"0.00"
|
|
|
+
|
|
|
+- **位置**:`common/config/BigDecimalSerializer.java:24-26`(null 序列化为 `"0.00"`)、`:31`(经 DataUtil.convertAmount 输出 `"1,234.00"` 带千分位)。
|
|
|
+- **后果**:① 空值与真实零金额在 UI 上不可区分——研判业务里这是"假数据"(用户会认为该账户余额为 0 而非无数据);② 前端做数值运算必须先去逗号,每处都要记得。
|
|
|
+- **修复**:null 输出 `null`(前端空态展示);千分位格式化移到前端展示层(后端输出纯数字字符串或数值)。**这是接口行为变更,需与前端同步排期**。
|
|
|
+
|
|
|
+#### E-15(低)日期双轨但一致;Redis 缓存的 ObjectMapper 未配全局日期格式(缓存内时间戳格式与 HTTP 出参不同)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 修复路线图
|
|
|
+
|
|
|
+**第一批:止血(1-2 天,全部是局部小改)**
|
|
|
+1. O-1 PII 日志脱敏 + O-2 logback info/滚动/分文件(合规,最高优先)
|
|
|
+2. S-1 意图识别 block 超时接线(IntentProperties.timeoutSeconds 已有)
|
|
|
+3. S-13 createTimeSeries 抛异常中断流水线
|
|
|
+4. O-4 预处理 unwrap ServerException
|
|
|
+5. O-3 SSE 兜底错误不泄内部 message
|
|
|
+6. S-12 回调顺序调整(先 flush 人员库再 initCache)
|
|
|
+
|
|
|
+**第二批:稳定性加固(3-5 天)**
|
|
|
+7. S-2 ES 移出清洗主链路(add 内部 catch + 熔断)
|
|
|
+8. S-6 SheetProbe 改 SAX
|
|
|
+9. S-10 graceful shutdown + FileSheetStateEnum 中间态 + 启动 sweep
|
|
|
+10. S-11 file_ai_profile 看门狗;S-3 对话流超时 + 心跳;S-4 RAG 检索降级
|
|
|
+11. O-5 /dm、/govern 补轮询通道;O-6 SmartCleanStatusVO 补 lastError
|
|
|
+
|
|
|
+**第三批:可观测性与运维(2-3 天)**
|
|
|
+12. O-7 MDC traceId;O-8 /sys/health 依赖自检;O-9 慢 SQL Interceptor
|
|
|
+13. O-10 级别治理;O-11 失败不推 100%;E-8 prod profile + 凭证出库
|
|
|
+
|
|
|
+**第四批:契约治理(渐进,随版本)**
|
|
|
+14. E-14 BigDecimal 序列化变更(与前端联排)
|
|
|
+15. E-1/E-2/E-3 写操作 POST 化、统一分页 VO、/chat 补 Result 包络
|
|
|
+16. E-5 写 DTO 补校验;E-12 Flyway 落地;E-10 死代码清理
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 已做得好的部分(防止误伤,后续改动请保持)
|
|
|
+
|
|
|
+1. **GlobalExceptionHandler** 样板级:15 类异常、cause 拆包、500 不泄堆栈、401 按前端约定 200+401 且写明理由。
|
|
|
+2. **AI 清洗(aiclean)模块的观测设计是全仓标杆**:job 状态机落库、costMs/tokenIn/tokenOut/failReason、批次原子计数、needReview 明细落库、看门狗双 sweep(启动孤儿+心跳超时)、失败原因写 lastError。
|
|
|
+3. **LLM 失败语义干净**:LlmService 绝不返回 null、400/404/500/504 分明、timeout 用 Reactor 操作符、LlmRetry 只对 429/5xx 指数退避且仅批处理链路使用。
|
|
|
+4. **大文件解析全流式**:预览/清洗 Fesod 流式 + DuckDBAppender(200MB csv 不进堆);附件表格扫描同款;行级脏数据有 CleanErrorLog 台账(带行号)。
|
|
|
+5. **DuckDB 资源预算**:memory_limit 按 max-open-cases 分摊物理内存 60%、超 8 案拒绝开案、threads=2、快照指纹缓存。
|
|
|
+6. **PythonExecutor 防护完备**:子进程超时强杀、输出截断、沙箱清理、py-out LRU 跳过在途、快照导出重试后显式报错。
|
|
|
+7. **ES 查询侧 fail-open + 启动不阻断**(但写入侧需按 S-2 补齐——两侧要对称)。
|
|
|
+8. **上下文传播系统化**:CaseContextHolder(ScopedValue)+ Reactor 钩子,每处异步提交显式带 caseId/userId——SSE 能推到正确的人,这是最容易系统性坏掉的地方。
|
|
|
+9. **application.yaml 的"为什么"注释是示范级**(调度线程池与心跳、MCP base-url 404 陷阱、DuckDB 预算分摊)。
|
|
|
+10. **对话错误持久化**:错误/中断/部分回复落库为 message,刷新可见;研判流 doFinally 无条件复位状态位防会话卡死。
|