2026-09-16.md 14 KB

2026-09-16(下午)

上传/清洗流程:sessionStorage 缓存清理时机(用户要求)

用户要求:「我发起调用清洗,或者从上传文件页面返回时,清空 sessionStore 内容/已上传文件缓存」。 经确认(AskUserQuestion):时机 = 发起清洗任务时 + 上传页点返回时;范围 = 连同清洗流程缓存一起清。

改动(2 个文件)

文件 改动
case/views/data/upload.vue 新增 clearCleanFlowSessionCache():清 case_clean_file_infos(沿用 isOwnBatchStorage() 批次守卫)+ case_clean_import_file_infos + case_clean_reclean_context + resetImportProgressSession()(清 case_clean_import_progress、关 SSE、重置运行时状态)。调用点 3 处:handleBack() 无活动分支、cleanupAndLeave()、onBeforeRouteLeave 兜底(目标不是清洗页/清洗进度页时才清)
case/views/data/cleanProgress.vue startClean():doClean() 成功后清 case_clean_file_infos + case_clean_import_file_infos + case_clean_reclean_context(原第 736 行那行保留)

★ 关键约束(以后改这块必看)

  • 进清洗流程前绝对不能清 case_clean_file_infos:cleaning.vue(loadStoredFileList)与 cleanProgress.vue(loadCleanFiles 的兜底键)都要读它,清了就无法清洗。所以「发起清洗时清」只能落在 startClean() 里、loadCleanFiles()/loadReCleanContext() 之后。
  • startClean() 里 不能 调 resetImportProgressSession():initializeProgress() 的顺序是 resetRuntimeState → connectSse → testSse → startClean,它会把刚建立的 SSE 关掉;且会清掉刚设置的 cleanTaskStarted/currentCleanFiles。要清进度键就用 clearImportProgress(caseId)。
  • 清除放在 doClean() 成功之后而非之前:失败时缓存仍在,可重新发起;否则发起失败即死路(必须重传文件)。
  • case_clean_import_progress 不清:它是当前任务的进度快照(currentCleanFiles 自包含),清了刷新页面就无法恢复进度。
  • 5 个文件各自硬编码了同一批 key(upload/cleaning/cleanProgress/importList + importProgressSession),没有共享常量模块 —— 后续如要改 key 需全量搜 case_clean_。

顺手修(预防性,非本次需求)

  • upload.vue:import { type Key } from 'vue' 报 TS2305(该版本 vue 不导出 Key)→ 去掉导入,expandedRowKeys/onExpandedRowsChange 改用 (string | number)[](与 antd Table 的 Key 一致,代码里只塞字符串)。

校验

  • eslint src/case/views/data/{upload,cleanProgress}.vue --max-warnings 0 → 0 错误 (cleanProgress.vue 原有 1 处 prettier 报错(第 134 行 import 折行)用 --fix 修掉,只动了那一行)
  • vue-tsc --noEmit --skipLibCheck → case/views/data/{upload,cleanProgress,cleaning}.vue 0 错误(全仓 103 个既有错误在 src/core 等,与本次无关)

环境备忘(本机 Bash shim 残缺,绕过方式)

  • 本机 shim 里 ls/head/tail/sed/dirname/grep/uname/rm 全部不可用(dirname: command not found), → ./node_modules/.bin/eslint 这类 shell 包装脚本必然失败(它内部用 sed/dirname)。
  • 可用方式:绝对路径调托管 node 直接执行 JS 入口: /c/Users/cc/.workbuddy/binaries/node/versions/22.22.2-3/node.exe ./node_modules/eslint/bin/eslint.js ... 同理 ./node_modules/vue-tsc/bin/vue-tsc.js。git 可用;rm 用 node 的 fs.unlinkSync 兜底。
  • 类型检查输出重定向到仓库内文件 + Grep 工具过滤,比试图用管道 grep 更可靠(用完记得删)。

★★ 全量 Controller @RequestBody 缺失审计(2026-09-16 下午)

用户要求:「检查项目中所有 controller 中有些接口是不是没有加 @RequestBody」。

结论(报告:controller-request-body-audit.md,仓库根)

ai-server 45 个 controller / 98 个写端点(POST/PUT/PATCH)中,76 个端点参数没有绑定注解, 其中 75 个已被确认为真实缺陷(前端用 defHttp.postJson 即 JSON body 调用 → body 被丢弃、参数全 null), 1 个暂无前端调用方(/trans/cashFlow/statTransCashCallBeforeAfterTimeLine)。 已正确 21 个(含上一轮修的 /chat/**、/dm/preFile、/case/create|open|updatePwd 等)+ 1 个 multipart。

★ 判定标准(这套判据以后可直接复用)

  • 前端 axios 默认 content-type 是 application/x-www-form-urlencoded(core/utils/http/axios/index.ts:275), defHttp.post 的 data 会被 qs.stringify 成表单 —— 不加注解的 POJO 能正常绑定; 只有 defHttp.postJson(Axios.ts:187,显式 application/json + data 作 body)才要求后端有 @RequestBody。
  • 所以「POST 端点没加 @RequestBody」本身不是 bug,必须交叉核对前端调用方式才能定性。
  • 已排除会推翻结论的因素:项目没有自定义 HandlerMethodArgumentResolver/WebMvcConfigurer, 也没有重写 body→parameter 的 Filter/HttpServletRequestWrapper(全仓 grep 0 命中)→ 走 Spring 默认 ModelAttribute 绑定。

需特殊修法的 4 个端点(不能无脑加注解)

端点 现状 说明
/pg/delete 后端 delete(Long id),前端 body {id} 直接加 @RequestBody 会 400(对象→Long);应改 @RequestParam+前端 params,或引入 IdDTO
/intimacy/modify List<Intimacy>,前端发数组 @RequestBody List<Intimacy> 即可
/pbi/personOrder List<PersonBasicInfo>,前端发数组 同上
/call/night/statSecondCallNight 参数误写成 @Param("query")(MyBatis 注解) 去掉 @Param 换 @RequestBody

其它发现

  • ResidentPopulationController 只有类级 @RequestMapping("/rp")、零个方法(空壳类)。
  • 9 个 controller 只有 GET 端点:System/Tower/Govern/Search/CleanErrorLog/AgentResult/AgentPythonFile/TransFastFundFlow/ResidentPopulation。

★ 修复(用户催「一点活也没干」后当日执行完毕)

  • 75 个端点全部补上 @RequestBody:32 个 controller 改动(74 个 POJO 参数前插注解 + /pg/delete 改 DTO + statSecondCallNight 的 MyBatis @Param("query") 替换),新增 DTO common/model/person/dto/DeletePersonGroupDTO.java(前端发 {id},简单类型不能直接加注解)。
  • 清理:删掉被替换后失效的 import org.apache.ibatis.annotations.Param(CallNightController)、 4 处与 annotation.* 通配符重复的显式 RequestBody import、3 个 controller 里未使用的 MyBatis @Delete import。
  • 验证三连:① 结构比对(修复前后:映射注解/URL 零改动,签名除注解外零改动,新增行 = 75 签名行 + 32 import); ② mvn -pl ai-server -B clean compile → BUILD SUCCESS(621 源文件);③ 复扫 audit → broken=0 / missingBindingAnnotation=0 / annotated=97 + multipart=1。
  • 报告 controller-request-body-audit.md 顶部加「〇、修复记录」,第 1~8 节保留修复前状态留档。
  • 备份:C:/Users/cc/AppData/Local/Temp/zsjz-audit/backup-controllers/(45 个 controller 修复前副本)。 ★ 32 个文件中 DataProfileController.java 在我修复前就与 HEAD 不同(import 被改成 annotation.* 通配符, 非我所为,已保留)→ 该文件不能用 git checkout 回退(会连带丢掉那处改动),要用备份副本;其余 31 个可 git checkout。
  • 未做:运行期端到端验证(起服务发 JSON body 请求确认筛选生效)。

★★ 本机 Maven 调用方式(shim 太残,cygpath/bash 都不可用)

mvn.cmd、mvn-run.sh 都跑不起来(shim 里连 bash/cygpath 都 command not found)。可行解:直接调 java + classworlds, 全用 Windows 路径,HOST 用托管 JDK(JAVA_HOME 是 BellSoft JDK 25,能编 release 17 的代码 + Lombok):

cd /e/workspace/zsjz-ai && "/c/Program Files/BellSoft/LibericaJDK-25-Full/bin/java.exe" \
  -classpath "D:\soft\apache-maven-3.9.12-bin\boot\plexus-classworlds-2.9.0.jar" \
  "-Dclassworlds.conf=D:\soft\apache-maven-3.9.12-bin\bin\m2.conf" \
  "-Dmaven.home=D:\soft\apache-maven-3.9.12-bin" \
  "-Dmaven.multiModuleProjectDirectory=E:\workspace\zsjz-ai" \
  org.codehaus.plexus.classworlds.launcher.Launcher -pl ai-server -B clean compile \
  > E:/workspace/zsjz-ai/.build-log.txt 2>&1; echo "EXIT=$?"

(注意 plexus-classworlds 版本是 2.9.0 不是 2.8.0;输出重定向到文件再用 Grep 工具过滤,管道 grep 不可用。)

复用工具(已固化为 skill)

~/.workbuddy/skills/spring-requestbody-audit/: scripts/audit.cjs(审计+交叉比对+生成报告)、scripts/fix.cjs(按精确偏移批量补注解,dry-run/--apply/--skip)、 scripts/verify-fix.cjs(结构比对,强制断言「URL/签名除注解外零改动」)。 SKILL.md 里写了完整 9 步修复流程与本机编译方式。 ★ 写这类脚本踩过的 3 个解析坑(务必照抄规避):

  1. TS 泛型里可能含 {(postJson<{ [key: string]: X }>)→ 找参数对象必须从「( 之后」开始找 {;
  2. 嵌套泛型 <Page<X>> / <Record<string, any>> 会让 <[^>]*> 正则整个失配 → 必须手写「跳过可选泛型块」的扫描;
  3. 后端类级 @RequestMapping("cr") 不带前导斜杠(45 个里只有这 1 个)→ 拼路径时要补 /,否则匹配不上前端 URL。 审计覆盖率自查法:统计前端 defHttp.(postJson|post|put|patch|uploadFile|request) 总数(本次 174)与解析出的调用点数(171), 差值必须能逐个解释(本次 3 个全是注释里的假命中)。

错误提示带上接口地址(2026-09-16 晚,用户要求)

需求:「错误提示加上访问接口地址」→ 所有接口报错时,提示里要能一眼看出是哪个接口。

改动(前端 3 文件,ai-frontend/src/core/utils/http/axios/)

文件 改动
helper.ts 新增 appendApiUrl(message, url):'接口请求出错' + '/js/a/dm/doClean' → '接口请求出错(/js/a/dm/doClean)';url 为空或 message 已含该地址时不重复追加
index.ts ① transformRequestHook 取 res.config.url,业务错误(code !== 200,含 401 与 skipErrorMessage 分支)统一走 appendApiUrl;② responseInterceptorsCatch 的 urlPath 增加 `
checkStatus.ts checkStatus(status, msg, mode, url?) 新增第 4 个可选参数,400/403/404/500 等状态码提示统一 appendApiUrl(errMessage, url)

关键设计点(以后改这里注意)

  • 格式用中文全角括号后缀而不是 \n:antd message 的 content 是 div,\n 会被折叠成空格。
  • 追加位置放在「最终显示/抛出」之前,所以服务端 message 与前端 i18n 文案(sys.api.*)都自动带上地址, 且 throw new Error(errorMessage) 里的 message 也带地址 —— 调用方 error.message / getRequestErrorMessage() 无需改动。
  • 全仓检查过没有任何地方做 message === 'xxx' 的精确匹配(只有 cleanProgress 里一段注释掉的代码),所以拼接安全。
  • 保留 /sys/health 的静默特例(urlPath.includes('/sys/health')),健康检查轮询不会刷屏;其 URL 带 ?_t= 时间戳也能命中。
  • 覆盖范围:defHttp 的所有实例(含 src/ai/api/http.ts 的 aiHttp,它复用 defHttp);errorMessageMode==='none' 的请求仍然不弹提示(未改语义)。
  • 提示内容不含 HTTP method(按「接口地址」字面要求);若以后需要区分同路径不同方法,可在 appendApiUrl 里加。

验证

  • eslint(3 文件,--max-warnings 0)→ 0 错误
  • vue-tsc --noEmit → 这三个文件 0 错误,全仓错误总数仍为 103(与改动前一致,无新增)

Result 包装复核(2026-09-16 晚,用户要求)

需求:「检查所有 controller 的接口返回结果是不是又缺 Result 包裹,有就加上」。

审计口径与结果

全量扫描 45 个 controller → 165 个方法级端点(用「方法级映射注解总数 = 165」做过完整性交叉校验,无遗漏): | 类别 | 数量 | | --- | --- | | 已 Result 包裹 | 151 | | 流式 / ResponseEntity(不适用) | 3(/govern/sse、/chat/stream 的 SseEmitter + /py/files/** 的 ResponseEntity) | | 刻意裸返回(保持不动) | 11(AgentChatController 10 个 /chat/** + AgentResultController 的 /chat/results/{resultId}) | | 漏网缺陷(已修) | 2 |

修掉的 2 个(DataProfileController)

  • GET /dp/callStat:CallDataProfileStatDTO → Result<CallDataProfileStatDTO>
  • GET /dp/transStat:TransDataProfileStatDTO → Result<TransDataProfileStatDTO> 前端 person/api/dpApi.ts 用 defHttp.get<...> 默认 transform 读 {code,message,data}, 裸返回必然走到 code !== 200 分支 → 画像页通话/交易统计直接报「接口请求出错」。

★ 为什么上一轮批量脚本会漏掉这两个

它们的注解与方法签名之间夹了一行注释(Solon 迁移遗留):

@GetMapping("/callStat")
//@Cache(key = "dp_callStat_${personName}", tags = StrConsts.CACHE_TAG_QING_JIAN)
public CallDataProfileStatDTO callInfoStat(String personName) { ... }

「紧跟映射注解去找方法签名」的解析逻辑遇到注释行就失配 → 整个端点被跳过。 教训:任何注解/签名扫描都必须做完整性交叉校验(映射注解总数 vs 解析端点数),否则静默漏项。

刻意裸返回清单的判定依据(不要再"修"它们)

  • /chat/**:前端 aiHttp.postRaw/getRaw/putRaw/deleteRaw({ isTransformResponse: false })
  • /chat/results/{resultId}:同上,且 404 语义=结果过期
  • /py/files/**:ResponseEntity 直出文件
  • /govern/sse、/chat/stream:SseEmitter 流式

验证

  • 复扫:151 + 3 + 11 = 165 ✓
  • mvn -pl ai-server -B clean compile → BUILD SUCCESS
  • 审计脚本已归档到 skill:scripts/audit-result-wrap.cjs