<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>项目 on XEDCZQ的博客</title><link>https://xedczq.cn/categories/%E9%A1%B9%E7%9B%AE/</link><description>Recent content in 项目 on XEDCZQ的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Fri, 15 May 2026 21:55:13 +0800</lastBuildDate><atom:link href="https://xedczq.cn/categories/%E9%A1%B9%E7%9B%AE/index.xml" rel="self" type="application/rss+xml"/><item><title>Ai面试项目：knowledgebase模块</title><link>https://xedczq.cn/post/aiinterview_knowledgebase/</link><pubDate>Fri, 15 May 2026 21:55:13 +0800</pubDate><guid>https://xedczq.cn/post/aiinterview_knowledgebase/</guid><description>&lt;h2 id="knowledgebase-知识库模块设计与实现"&gt;&lt;a href="#knowledgebase-%e7%9f%a5%e8%af%86%e5%ba%93%e6%a8%a1%e5%9d%97%e8%ae%be%e8%ae%a1%e4%b8%8e%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;Knowledgebase 知识库模块设计与实现
&lt;/h2&gt;&lt;p&gt;这篇笔记记录我在 &lt;code&gt;interview-guide&lt;/code&gt; 项目中对 &lt;code&gt;Knowledgebase&lt;/code&gt; 模块的实现。目标是把“文档上传、向量化、RAG 查询、会话关联”打通成一个可持续迭代的知识服务能力。&lt;/p&gt;
&lt;h2 id="模块能力概览"&gt;&lt;a href="#%e6%a8%a1%e5%9d%97%e8%83%bd%e5%8a%9b%e6%a6%82%e8%a7%88" class="header-anchor"&gt;&lt;/a&gt;模块能力概览
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;文档管理：支持上传、下载、删除、分类、关键字检索与统计。&lt;/li&gt;
&lt;li&gt;向量化能力：基于 &lt;code&gt;pgvector&lt;/code&gt; 存储向量，使用异步任务进行切片与入库。&lt;/li&gt;
&lt;li&gt;RAG 问答：支持非流式与流式（SSE）多知识库检索问答。&lt;/li&gt;
&lt;li&gt;会话协同：删除知识库时自动清理关联会话引用，降低数据不一致风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="状态转换"&gt;&lt;a href="#%e7%8a%b6%e6%80%81%e8%bd%ac%e6%8d%a2" class="header-anchor"&gt;&lt;/a&gt;状态转换
&lt;/h2&gt;&lt;h3 id="图1knowledgebase-主状态机"&gt;&lt;a href="#%e5%9b%be1knowledgebase-%e4%b8%bb%e7%8a%b6%e6%80%81%e6%9c%ba" class="header-anchor"&gt;&lt;/a&gt;图1：KnowledgeBase 主状态机
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["调用 POST /api/knowledgebase/upload 上传文件"] --&gt; B["文件校验 + 类型检测 + 去重检查"]

B --&gt; C{"是否重复文件(fileHash已存在)"}
C --&gt;|是| D["返回已有知识库记录\nduplicate=true\n不触发向量化"]
C --&gt;|否| E["解析文本内容 + 上传文件到存储"]

E --&gt; F["保存 KnowledgeBaseEntity\n初始 vectorStatus=PENDING"]
F --&gt; G["发送向量化任务到 Redis Stream"]

G --&gt; H["VectorizeStreamConsumer 消费任务"]
H --&gt; I["markProcessing\nvectorStatus=PROCESSING"]

I --&gt; J["vectorizeAndStore\n切分文档并写入 pgvector"]
J --&gt; K{"向量化是否成功"}

K --&gt;|是| L["markCompleted\nvectorStatus=COMPLETED\nvectorError=null"]
K --&gt;|否| M{"retryCount &lt; 3 ?"}
M --&gt;|是| N["任务重新入队(retry+1)"]
N --&gt; H
M --&gt;|否| O["markFailed\nvectorStatus=FAILED\n写入vectorError"]

P["调用 POST /api/knowledgebase/{id}/revectorize"] --&gt; Q["状态重置为 PENDING\n清空vectorError"]
Q --&gt; G

R["调用 DELETE /api/knowledgebase/{id} 删除知识库"] --&gt; S["移除RAG会话关联"]
S --&gt; T["删除向量数据(尽力) + 删除存储文件(尽力)"]
T --&gt; U["删除知识库DB记录\n生命周期结束"]&lt;/pre&gt;&lt;h3 id="图2分片上传知识库流程"&gt;&lt;a href="#%e5%9b%be2%e5%88%86%e7%89%87%e4%b8%8a%e4%bc%a0%e7%9f%a5%e8%af%86%e5%ba%93%e6%b5%81%e7%a8%8b" class="header-anchor"&gt;&lt;/a&gt;图2：分片上传知识库流程
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["上传知识库成功"] --&gt; B["保存知识库记录 vectorStatus=PENDING"]
B --&gt; C["发送向量化任务到 Redis Stream"]
C --&gt; D["VectorizeStreamConsumer 启动并轮询"]
D --&gt; E["读取一条消息 kbId + content + retryCount"]
E --&gt; F["更新状态 PROCESSING"]
F --&gt; G["执行 vectorizeAndStore"]

G --&gt; H["删除该 kbId 的旧向量"]
H --&gt; I["文本分块 TokenTextSplitter"]
I --&gt; J["给每个分块打 metadata kb_id"]
J --&gt; K["分批调用 vectorStore.add 写入向量库"]

K --&gt; L["更新状态 COMPLETED"]
L --&gt; M["ACK 消息"]

G --&gt; N{"处理异常"}
N --&gt;|是| O{"retryCount &lt; 3"}
O --&gt;|是| P["retryCount+1 重新入队"]
P --&gt; M
O --&gt;|否| Q["更新状态 FAILED 并记录错误"]
Q --&gt; M&lt;/pre&gt;&lt;h2 id="关键接口设计"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e6%8e%a5%e5%8f%a3%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;关键接口设计
&lt;/h2&gt;&lt;h3 id="get-apiknowledgebaselist-获取知识库列表状态过滤--排序"&gt;&lt;a href="#get-apiknowledgebaselist-%e8%8e%b7%e5%8f%96%e7%9f%a5%e8%af%86%e5%ba%93%e5%88%97%e8%a1%a8%e7%8a%b6%e6%80%81%e8%bf%87%e6%bb%a4--%e6%8e%92%e5%ba%8f" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/list&lt;/code&gt; 获取知识库列表（状态过滤 + 排序）
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listKnowledgeBases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;knowledgeBaseRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByVectorStatusOrderByUploadedAtDesc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectorStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;knowledgeBaseRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAllByOrderByUploadedAtDesc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sortEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiknowledgebaseid-获取知识库详情"&gt;&lt;a href="#get-apiknowledgebaseid-%e8%8e%b7%e5%8f%96%e7%9f%a5%e8%af%86%e5%ba%93%e8%af%a6%e6%83%85" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/{id}&lt;/code&gt; 获取知识库详情
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getKnowledgeBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;knowledgeBaseRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="delete-apiknowledgebaseid-删除知识库"&gt;&lt;a href="#delete-apiknowledgebaseid-%e5%88%a0%e9%99%a4%e7%9f%a5%e8%af%86%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/knowledgebase/{id}&lt;/code&gt; 删除知识库
&lt;/h3&gt;&lt;p&gt;核心流程：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;deleteService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteKnowledgeBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;knowledgeBaseRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByKnowledgeBaseIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vectorService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteByKnowledgeBaseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;storageService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteKnowledgeBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStorageKey&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;knowledgeBaseRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先清理 RAG 会话关联，再删除向量与对象存储文件，最后删除数据库记录。&lt;/li&gt;
&lt;li&gt;向量删除和对象存储删除失败仅 &lt;code&gt;warn&lt;/code&gt;，不阻断主删除流程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiknowledgebasequery-非流式问答多知识库"&gt;&lt;a href="#post-apiknowledgebasequery-%e9%9d%9e%e6%b5%81%e5%bc%8f%e9%97%ae%e7%ad%94%e5%a4%9a%e7%9f%a5%e8%af%86%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/knowledgebase/query&lt;/code&gt; 非流式问答（多知识库）
&lt;/h3&gt;&lt;p&gt;限流：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLOBAL/IP 各 10&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;queryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queryKnowledgeBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;answerQuestion&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;countService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateQuestionCounts&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vectorService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;similaritySearch&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;入参 &lt;code&gt;knowledgeBaseIds&lt;/code&gt; 与 &lt;code&gt;question&lt;/code&gt; 必填。&lt;/li&gt;
&lt;li&gt;无命中返回固定文案“未检索到信息”。&lt;/li&gt;
&lt;li&gt;有命中则拼接 context，构建提示词后调用默认 ChatClient 生成答案。&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;QueryResponse(answer, primaryKbId, kbNamesStr)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiknowledgebasequerystream-流式问答sse多知识库"&gt;&lt;a href="#post-apiknowledgebasequerystream-%e6%b5%81%e5%bc%8f%e9%97%ae%e7%ad%94sse%e5%a4%9a%e7%9f%a5%e8%af%86%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/knowledgebase/query/stream&lt;/code&gt; 流式问答（SSE，多知识库）
&lt;/h3&gt;&lt;p&gt;限流：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLOBAL/IP 各 5&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;queryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;answerQuestionStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kbIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;countService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateQuestionCounts&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vectorService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;similaritySearch&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;normalizeStreamOutput&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回类型为 &lt;code&gt;Flux&amp;lt;String&amp;gt;&lt;/code&gt;（&lt;code&gt;text/event-stream&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;空输入或无检索命中时返回固定降级文本流。&lt;/li&gt;
&lt;li&gt;流内异常与外层异常都做统一降级输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apiknowledgebasecategories-获取所有分类名"&gt;&lt;a href="#get-apiknowledgebasecategories-%e8%8e%b7%e5%8f%96%e6%89%80%e6%9c%89%e5%88%86%e7%b1%bb%e5%90%8d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/categories&lt;/code&gt; 获取所有分类名
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAllCategories&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;List&amp;lt;String&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apiknowledgebasecategorycategory-按分类获取知识库列表"&gt;&lt;a href="#get-apiknowledgebasecategorycategory-%e6%8c%89%e5%88%86%e7%b1%bb%e8%8e%b7%e5%8f%96%e7%9f%a5%e8%af%86%e5%ba%93%e5%88%97%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/category/{category}&lt;/code&gt; 按分类获取知识库列表
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listByCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;List&amp;lt;KnowledgeBaseListItemDTO&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apiknowledgebaseuncategorized-获取未分类知识库列表"&gt;&lt;a href="#get-apiknowledgebaseuncategorized-%e8%8e%b7%e5%8f%96%e6%9c%aa%e5%88%86%e7%b1%bb%e7%9f%a5%e8%af%86%e5%ba%93%e5%88%97%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/uncategorized&lt;/code&gt; 获取未分类知识库列表
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listByCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前实现沿用分类查询路径，通过特定分类值区分未分类集合。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put-apiknowledgebaseidcategory-更新知识库分类"&gt;&lt;a href="#put-apiknowledgebaseidcategory-%e6%9b%b4%e6%96%b0%e7%9f%a5%e8%af%86%e5%ba%93%e5%88%86%e7%b1%bb" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/knowledgebase/{id}/category&lt;/code&gt; 更新知识库分类
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;category&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先按 &lt;code&gt;id&lt;/code&gt; 查记录，不存在抛业务异常。&lt;/li&gt;
&lt;li&gt;存在则更新 &lt;code&gt;category&lt;/code&gt; 字段并保存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiknowledgebaseupload-上传知识库文件multipart"&gt;&lt;a href="#post-apiknowledgebaseupload-%e4%b8%8a%e4%bc%a0%e7%9f%a5%e8%af%86%e5%ba%93%e6%96%87%e4%bb%b6multipart" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/knowledgebase/upload&lt;/code&gt; 上传知识库文件（multipart）
&lt;/h3&gt;&lt;p&gt;参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file&lt;/code&gt;（必填）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;（可选）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;category&lt;/code&gt;（可选）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;限流：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLOBAL/IP 各 3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;uploadService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uploadKnowledgeBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;findByFileHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;校验文件非空、大小（最大 50MB）。&lt;/li&gt;
&lt;li&gt;按 MIME + 扩展名白名单校验类型（PDF/DOCX/DOC/TXT/MD）。&lt;/li&gt;
&lt;li&gt;计算 &lt;code&gt;SHA-256&lt;/code&gt; 并做去重。&lt;/li&gt;
&lt;li&gt;解析正文，空文本直接失败。&lt;/li&gt;
&lt;li&gt;上传原文件到 RustFS（S3 兼容），生成 &lt;code&gt;fileKey/fileUrl&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;落库 &lt;code&gt;KnowledgeBaseEntity&lt;/code&gt;，初始向量状态 &lt;code&gt;PENDING&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;投递异步向量化任务到 Redis Stream（&lt;code&gt;knowledgebase:vectorize:stream&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;knowledgeBase + storage + duplicate=false&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="get-apiknowledgebaseiddownload-下载原始知识库文件"&gt;&lt;a href="#get-apiknowledgebaseiddownload-%e4%b8%8b%e8%bd%bd%e5%8e%9f%e5%a7%8b%e7%9f%a5%e8%af%86%e5%ba%93%e6%96%87%e4%bb%b6" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/{id}/download&lt;/code&gt; 下载原始知识库文件
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEntityForDownload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;downloadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ResponseEntity&amp;lt;byte[]&amp;gt;&lt;/code&gt;（包含 &lt;code&gt;Content-Disposition&lt;/code&gt; 与 &lt;code&gt;Content-Type&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apiknowledgebasesearchkeyword-关键字搜索知识库"&gt;&lt;a href="#get-apiknowledgebasesearchkeyword-%e5%85%b3%e9%94%ae%e5%ad%97%e6%90%9c%e7%b4%a2%e7%9f%a5%e8%af%86%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/search?keyword=...&lt;/code&gt; 关键字搜索知识库
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiknowledgebasestats-获取知识库统计信息"&gt;&lt;a href="#get-apiknowledgebasestats-%e8%8e%b7%e5%8f%96%e7%9f%a5%e8%af%86%e5%ba%93%e7%bb%9f%e8%ae%a1%e4%bf%a1%e6%81%af" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/stats&lt;/code&gt; 获取知识库统计信息
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatistics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KnowledgeBaseStatsDTO&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiknowledgebaseidrevectorize-手动重新向量化"&gt;&lt;a href="#post-apiknowledgebaseidrevectorize-%e6%89%8b%e5%8a%a8%e9%87%8d%e6%96%b0%e5%90%91%e9%87%8f%e5%8c%96" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/knowledgebase/{id}/revectorize&lt;/code&gt; 手动重新向量化
&lt;/h3&gt;&lt;p&gt;限流：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLOBAL/IP 各 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;uploadService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;revectorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按 &lt;code&gt;id&lt;/code&gt; 查知识库，不存在抛异常。&lt;/li&gt;
&lt;li&gt;从对象存储下载原文件并重新解析文本。&lt;/li&gt;
&lt;li&gt;解析失败或空文本直接失败。&lt;/li&gt;
&lt;li&gt;更新向量状态为 &lt;code&gt;PENDING&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;投递向量化任务到 Redis Stream。&lt;/li&gt;
&lt;li&gt;立即返回成功，前端轮询状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="异步向量化处理流程核心实现"&gt;&lt;a href="#%e5%bc%82%e6%ad%a5%e5%90%91%e9%87%8f%e5%8c%96%e5%a4%84%e7%90%86%e6%b5%81%e7%a8%8b%e6%a0%b8%e5%bf%83%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;异步向量化处理流程（核心实现）
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 1) 删除旧向量&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;deleteByKnowledgeBaseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;knowledgeBaseId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 2) 文本切片（默认无重叠）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;textSplitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 3) 写入 metadata（kb_id）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;kb_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;knowledgeBaseId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 4) 分批向量化写入（DashScope batch &amp;lt;= 10）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;batchCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MAX_BATCH_SIZE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MAX_BATCH_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;totalChunks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Knowledgebase&lt;/code&gt; 模块的核心是把“文件资产管理”和“检索增强问答”打通。对我来说，真正有价值的不只是上传成功，而是文档能够稳定进入向量化链路，并最终在问答场景里提供可复用、可追踪的知识支持。&lt;/p&gt;</description></item><item><title>Ai面试项目：voice interview模块</title><link>https://xedczq.cn/post/aiinterview_voiceinterview/</link><pubDate>Thu, 14 May 2026 22:34:43 +0800</pubDate><guid>https://xedczq.cn/post/aiinterview_voiceinterview/</guid><description>&lt;h2 id="voiceinterview-语音面试模块设计与实现"&gt;&lt;a href="#voiceinterview-%e8%af%ad%e9%9f%b3%e9%9d%a2%e8%af%95%e6%a8%a1%e5%9d%97%e8%ae%be%e8%ae%a1%e4%b8%8e%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;VoiceInterview 语音面试模块设计与实现
&lt;/h2&gt;&lt;p&gt;这篇笔记记录我在 &lt;code&gt;interview-guide&lt;/code&gt; 项目中对 &lt;code&gt;VoiceInterview&lt;/code&gt; 模块的实现。核心目标是让语音面试具备“实时交互 + 可恢复会话 + 可追踪评估”的完整体验。&lt;/p&gt;
&lt;h2 id="模块能力概览"&gt;&lt;a href="#%e6%a8%a1%e5%9d%97%e8%83%bd%e5%8a%9b%e6%a6%82%e8%a7%88" class="header-anchor"&gt;&lt;/a&gt;模块能力概览
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;实时语音对话：基于 &lt;code&gt;WebSocket + 千问3语音模型&lt;/code&gt;（ASR/TTS/LLM 统一 API Key）。&lt;/li&gt;
&lt;li&gt;流式体验优化：句子级并发 TTS，边生成边合成边播放，首包延迟约 200ms。&lt;/li&gt;
&lt;li&gt;服务端 VAD：自动断句，提供实时字幕（含中间结果）。&lt;/li&gt;
&lt;li&gt;回声防护：支持手动提交机制，避免 AI 播报被误录入。&lt;/li&gt;
&lt;li&gt;会话连续性：支持暂停/恢复与多轮上下文记忆，超时可自动暂停。&lt;/li&gt;
&lt;li&gt;监控埋点：通过 Micrometer 采集 TTS/ASR 延迟、会话时长等指标。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="状态转换"&gt;&lt;a href="#%e7%8a%b6%e6%80%81%e8%bd%ac%e6%8d%a2" class="header-anchor"&gt;&lt;/a&gt;状态转换
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;
flowchart TD
A["创建会话&lt;br/&gt;POST /api/voice-interview/sessions"] --&gt; B["IN_PROGRESS"]

B --&gt; C{"会话中事件"}
C -- "暂停/超时" --&gt; D["PAUSED"]
D -- "恢复" --&gt; B

C -- "结束面试" --&gt; E["COMPLETED"]
E --&gt; F["evaluateStatus = PENDING"]
F --&gt; G["evaluateStatus = PROCESSING"]

G --&gt; H{"评估结果"}
H -- "成功" --&gt; I["EVALUATED&lt;br/&gt;evaluateStatus = COMPLETED"]
H -- "失败" --&gt; J["evaluateStatus = FAILED"]

B --&gt; K["DELETE /api/voice-interview/sessions/{id}"]
D --&gt; K
E --&gt; K
I --&gt; K
J --&gt; K&lt;/pre&gt;&lt;h2 id="关键接口设计"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e6%8e%a5%e5%8f%a3%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;关键接口设计
&lt;/h2&gt;&lt;h3 id="post-apivoice-interviewsessions-创建语音面试会话"&gt;&lt;a href="#post-apivoice-interviewsessions-%e5%88%9b%e5%bb%ba%e8%af%ad%e9%9f%b3%e9%9d%a2%e8%af%95%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/voice-interview/sessions&lt;/code&gt; 创建语音面试会话
&lt;/h3&gt;&lt;p&gt;Controller 入口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;VoiceInterviewController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Valid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreateSessionRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;核心调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实现要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;兜底 &lt;code&gt;skillId&lt;/code&gt;（未传则使用默认技能）。&lt;/li&gt;
&lt;li&gt;兜底 &lt;code&gt;llmProvider&lt;/code&gt;（空值走默认 provider）。&lt;/li&gt;
&lt;li&gt;组装 &lt;code&gt;VoiceInterviewSessionEntity&lt;/code&gt;（阶段开关、难度、简历 ID、JD 文本、计划时长等）。&lt;/li&gt;
&lt;li&gt;默认 &lt;code&gt;userId = &amp;quot;default&amp;quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;设置初始阶段（&lt;code&gt;intro/tech/project/hr&lt;/code&gt; 中第一个启用阶段）。&lt;/li&gt;
&lt;li&gt;持久化到数据库 &lt;code&gt;voice_interview_sessions&lt;/code&gt;，并写入 Redis（带 TTL）。&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;SessionResponseDTO&lt;/code&gt;（会话 ID、状态、阶段、配置等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apivoice-interviewsessionssessionid-根据会话-id-获取详情"&gt;&lt;a href="#get-apivoice-interviewsessionssessionid-%e6%a0%b9%e6%8d%ae%e4%bc%9a%e8%af%9d-id-%e8%8e%b7%e5%8f%96%e8%af%a6%e6%83%85" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/voice-interview/sessions/{sessionId}&lt;/code&gt; 根据会话 ID 获取详情
&lt;/h3&gt;&lt;p&gt;Controller 调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSessionDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实现要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先查 Redis 缓存，未命中再查数据库。&lt;/li&gt;
&lt;li&gt;查到后组装 &lt;code&gt;SessionResponseDTO&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;查不到返回统一错误：&lt;code&gt;Session not found: {sessionId}&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apivoice-interviewsessionssessionidend-结束会话并触发异步评估"&gt;&lt;a href="#post-apivoice-interviewsessionssessionidend-%e7%bb%93%e6%9d%9f%e4%bc%9a%e8%af%9d%e5%b9%b6%e8%a7%a6%e5%8f%91%e5%bc%82%e6%ad%a5%e8%af%84%e4%bc%b0" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/voice-interview/sessions/{sessionId}/end&lt;/code&gt; 结束会话并触发异步评估
&lt;/h3&gt;&lt;p&gt;Controller 调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;endSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;结束与评估逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEndTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCurrentPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COMPLETED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COMPLETED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEvaluateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceEvaluateStreamProducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendEvaluateTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;redisService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;streamAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;streamKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;buildMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AsyncTaskStreamConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STREAM_MAX_LEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接口立即返回 &lt;code&gt;Result.success()&lt;/code&gt;，不阻塞等待评估结果。&lt;/li&gt;
&lt;li&gt;前端通过 &lt;code&gt;GET /api/voice-interview/sessions/{sessionId}/evaluation&lt;/code&gt; 轮询评估状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put-apivoice-interviewsessionssessionidpause-暂停会话"&gt;&lt;a href="#put-apivoice-interviewsessionssessionidpause-%e6%9a%82%e5%81%9c%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/voice-interview/sessions/{sessionId}/pause&lt;/code&gt; 暂停会话
&lt;/h3&gt;&lt;p&gt;核心调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pauseSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实现要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅 &lt;code&gt;IN_PROGRESS&lt;/code&gt; 状态允许暂停。&lt;/li&gt;
&lt;li&gt;更新状态为 &lt;code&gt;PAUSED&lt;/code&gt;，记录原因并刷新 &lt;code&gt;updatedAt&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;同步持久化数据库与 Redis 缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put-apivoice-interviewsessionssessionidresume-恢复会话"&gt;&lt;a href="#put-apivoice-interviewsessionssessionidresume-%e6%81%a2%e5%a4%8d%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/voice-interview/sessions/{sessionId}/resume&lt;/code&gt; 恢复会话
&lt;/h3&gt;&lt;p&gt;核心调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resumeSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实现要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅 &lt;code&gt;PAUSED&lt;/code&gt; 状态允许恢复。&lt;/li&gt;
&lt;li&gt;恢复后状态改为 &lt;code&gt;IN_PROGRESS&lt;/code&gt;，不重置题目进度与阶段。&lt;/li&gt;
&lt;li&gt;保存数据库并同步 Redis，返回最新 &lt;code&gt;SessionResponseDTO&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apivoice-interviewsessions-获取会话列表按-useridstatus-可过滤"&gt;&lt;a href="#get-apivoice-interviewsessions-%e8%8e%b7%e5%8f%96%e4%bc%9a%e8%af%9d%e5%88%97%e8%a1%a8%e6%8c%89-useridstatus-%e5%8f%af%e8%bf%87%e6%bb%a4" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/voice-interview/sessions&lt;/code&gt; 获取会话列表（按 userId/status 可过滤）
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAllSessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByUserIdAndStatusOrderByUpdatedAtDesc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;statusEnum&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;List&amp;lt;SessionMetaDTO&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="delete-apivoice-interviewsessionssessionid-删除语音面试会话"&gt;&lt;a href="#delete-apivoice-interviewsessionssessionid-%e5%88%a0%e9%99%a4%e8%af%ad%e9%9f%b3%e9%9d%a2%e8%af%95%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/voice-interview/sessions/{sessionId}&lt;/code&gt; 删除语音面试会话
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实现要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;校验会话存在性。&lt;/li&gt;
&lt;li&gt;删除会话与关联数据（消息/评估等）。&lt;/li&gt;
&lt;li&gt;清理 Redis 缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apivoice-interviewsessionssessionidmessages-获取对话历史"&gt;&lt;a href="#get-apivoice-interviewsessionssessionidmessages-%e8%8e%b7%e5%8f%96%e5%af%b9%e8%af%9d%e5%8e%86%e5%8f%b2" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/voice-interview/sessions/{sessionId}/messages&lt;/code&gt; 获取对话历史
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConversationHistoryDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;返回：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Result&amp;lt;List&amp;lt;VoiceInterviewMessageDTO&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apivoice-interviewsessionssessionidevaluation-获取异步评估状态与结果"&gt;&lt;a href="#get-apivoice-interviewsessionssessionidevaluation-%e8%8e%b7%e5%8f%96%e5%bc%82%e6%ad%a5%e8%af%84%e4%bc%b0%e7%8a%b6%e6%80%81%e4%b8%8e%e7%bb%93%e6%9e%9c" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/voice-interview/sessions/{sessionId}/evaluation&lt;/code&gt; 获取异步评估状态与结果
&lt;/h3&gt;&lt;p&gt;实现要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先校验会话存在（不存在抛 &lt;code&gt;VOICE_SESSION_NOT_FOUND&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;读取 &lt;code&gt;evaluateStatus&lt;/code&gt;、&lt;code&gt;evaluateError&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;若状态为 &lt;code&gt;COMPLETED&lt;/code&gt;，再读取评估详情：&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;evaluationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEvaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;返回 &lt;code&gt;VoiceEvaluationStatusDTO&lt;/code&gt;（包含状态，完成时附评估结果）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apivoice-interviewsessionssessionidevaluation-手动触发异步评估"&gt;&lt;a href="#post-apivoice-interviewsessionssessionidevaluation-%e6%89%8b%e5%8a%a8%e8%a7%a6%e5%8f%91%e5%bc%82%e6%ad%a5%e8%af%84%e4%bc%b0" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/voice-interview/sessions/{sessionId}/evaluation&lt;/code&gt; 手动触发异步评估
&lt;/h3&gt;&lt;p&gt;处理逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;evaluationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEvaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;voiceInterviewService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;triggerEvaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若已 &lt;code&gt;COMPLETED&lt;/code&gt;：直接返回已有评估结果。&lt;/li&gt;
&lt;li&gt;若为 &lt;code&gt;PENDING/PROCESSING&lt;/code&gt;：返回当前状态，不重复触发。&lt;/li&gt;
&lt;li&gt;其他可触发状态：入队评估任务，立即返回 &lt;code&gt;PENDING&lt;/code&gt;，前端继续轮询。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;VoiceInterview&lt;/code&gt; 模块的关键不是“把语音跑通”，而是把实时链路和会话生命周期稳定地串起来。对我来说，只有在创建、暂停、恢复、结束、评估这一整条链都能可靠协同时，语音面试才是真正可持续迭代的产品能力。&lt;/p&gt;</description></item><item><title>Ai简历分析：interview schedule模块</title><link>https://xedczq.cn/post/aiinterview_interviewschedule/</link><pubDate>Thu, 14 May 2026 17:10:42 +0800</pubDate><guid>https://xedczq.cn/post/aiinterview_interviewschedule/</guid><description>&lt;h2 id="interviewschedule-面试安排模块设计与实现"&gt;&lt;a href="#interviewschedule-%e9%9d%a2%e8%af%95%e5%ae%89%e6%8e%92%e6%a8%a1%e5%9d%97%e8%ae%be%e8%ae%a1%e4%b8%8e%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;InterviewSchedule 面试安排模块设计与实现
&lt;/h2&gt;&lt;p&gt;这篇笔记记录我在 &lt;code&gt;interview-guide&lt;/code&gt; 项目中对 &lt;code&gt;InterviewSchedule&lt;/code&gt; 模块的实现思路。目标是把“邀约解析、记录管理、状态维护、提醒协同”整合成一条稳定可维护的链路。&lt;/p&gt;
&lt;h2 id="模块能力概览"&gt;&lt;a href="#%e6%a8%a1%e5%9d%97%e8%83%bd%e5%8a%9b%e6%a6%82%e8%a7%88" class="header-anchor"&gt;&lt;/a&gt;模块能力概览
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;邀请解析：规则引擎 + AI 双通道，支持飞书/腾讯会议/Zoom 文本格式，自动提取公司、岗位、时间、会议链接。&lt;/li&gt;
&lt;li&gt;日历管理：支持日/周/月视图、拖拽调整与列表视图协同。&lt;/li&gt;
&lt;li&gt;状态维护：支持手动标记与定时任务自动过期。&lt;/li&gt;
&lt;li&gt;提醒机制：支持可配置提醒，降低漏面试风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="状态转换"&gt;&lt;a href="#%e7%8a%b6%e6%80%81%e8%bd%ac%e6%8d%a2" class="header-anchor"&gt;&lt;/a&gt;状态转换
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["调用 POST /api/interview-schedule/parse 解析邀约文本"] --&gt; B{"规则解析是否成功"}
B --&gt;|是| C["返回 ParseResponse\nparseMethod = rule"]
B --&gt;|否| D["调用 LLM 解析"]
D --&gt; E{"AI 解析是否成功"}
E --&gt;|是| F["返回 ParseResponse\nparseMethod = ai"]
E --&gt;|否| G["返回解析失败\nsuccess = false"]

H["调用 POST /api/interview-schedule 创建记录"] --&gt; I["create(): 强制 status = PENDING"]
I --&gt; J["写入 DB\n状态: PENDING"]

J --&gt; K["调用 GET /api/interview-schedule 或 /{id} 查询记录"]

J --&gt; L["调用 PUT /api/interview-schedule/{id} 更新基础信息"]
L --&gt; M["仅更新公司/岗位/时间等字段\n不改 status"]
M --&gt; J

J --&gt; N["调用 PATCH|PUT /api/interview-schedule/{id}/status?status=..."]
N --&gt; O["updateStatus(): entity.setStatus(status)"]

O --&gt; P{"目标状态"}
P --&gt;|COMPLETED| Q["状态 -&gt; COMPLETED"]
P --&gt;|CANCELLED| R["状态 -&gt; CANCELLED"]
P --&gt;|RESCHEDULED| S["状态 -&gt; RESCHEDULED"]
P --&gt;|PENDING| T["状态 -&gt; PENDING"]

Q --&gt; U["记录继续可被状态接口改写"]
R --&gt; U
S --&gt; U
T --&gt; U
U --&gt; N

J --&gt; V["定时任务 ScheduleStatusUpdater\n每小时执行一次"]
V --&gt; W{"是否满足\nstatus=PENDING 且 interviewTime &lt; now"}
W --&gt;|是| X["批量更新为 CANCELLED"]
W --&gt;|否| Y["不变"]

X --&gt; R
Y --&gt; J

J --&gt; Z["调用 DELETE /api/interview-schedule/{id}"]
Z --&gt; AA["删除记录（生命周期结束）"]&lt;/pre&gt;&lt;h2 id="关键接口设计"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e6%8e%a5%e5%8f%a3%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;关键接口设计
&lt;/h2&gt;&lt;h3 id="post-apiinterview-scheduleparse-解析面试邀约文本"&gt;&lt;a href="#post-apiinterview-scheduleparse-%e8%a7%a3%e6%9e%90%e9%9d%a2%e8%af%95%e9%82%80%e7%ba%a6%e6%96%87%e6%9c%ac" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview-schedule/parse&lt;/code&gt; 解析面试邀约文本
&lt;/h3&gt;&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;parseService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRawText&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSource&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tryRuleParsing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;parseWithAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;规则解析优先处理飞书/腾讯/Zoom 结构化片段。&lt;/li&gt;
&lt;li&gt;AI 解析作为补充通道，增强对非标准文本的适配能力。&lt;/li&gt;
&lt;li&gt;在 AI 解析前做输入边界约束与注入防护。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiinterview-schedule-创建面试记录"&gt;&lt;a href="#post-apiinterview-schedule-%e5%88%9b%e5%bb%ba%e9%9d%a2%e8%af%95%e8%ae%b0%e5%bd%95" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview-schedule&lt;/code&gt; 创建面试记录
&lt;/h3&gt;&lt;p&gt;用途：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持用户直接输入信息创建面试安排。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;scheduleService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;请求体（核心字段）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateInterviewRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@NotBlank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;公司名称不能为空&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@NotBlank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;岗位不能为空&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;面试时间不能为空&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@com.fasterxml.jackson.annotation.JsonFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;yyyy-MM-dd&amp;#39;T&amp;#39;HH:mm[:ss]&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;LocalDateTime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;interviewTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;interviewType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ONSITE, VIDEO, PHONE&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meetingLink&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;roundNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;interviewer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiinterview-scheduleid-根据-id-获取面试记录"&gt;&lt;a href="#get-apiinterview-scheduleid-%e6%a0%b9%e6%8d%ae-id-%e8%8e%b7%e5%8f%96%e9%9d%a2%e8%af%95%e8%ae%b0%e5%bd%95" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview-schedule/{id}&lt;/code&gt; 根据 ID 获取面试记录
&lt;/h3&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller 接收 &lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;scheduleService.getById(id)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service 从 Repository 查询单条记录，不存在则抛业务异常&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;Result&amp;lt;InterviewScheduleDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;scheduleService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiinterview-schedule-获取面试记录列表"&gt;&lt;a href="#get-apiinterview-schedule-%e8%8e%b7%e5%8f%96%e9%9d%a2%e8%af%95%e8%ae%b0%e5%bd%95%e5%88%97%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview-schedule&lt;/code&gt; 获取面试记录列表
&lt;/h3&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller 接收可选筛选参数：&lt;code&gt;status/start/end&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;scheduleService.getAll(status, start, end)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service 按条件查询并转换 DTO&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;Result&amp;lt;List&amp;lt;InterviewScheduleDTO&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;scheduleService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="put-apiinterview-scheduleid-更新面试记录"&gt;&lt;a href="#put-apiinterview-scheduleid-%e6%9b%b4%e6%96%b0%e9%9d%a2%e8%af%95%e8%ae%b0%e5%bd%95" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/interview-schedule/{id}&lt;/code&gt; 更新面试记录
&lt;/h3&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller 接收 &lt;code&gt;id + CreateInterviewRequest&lt;/code&gt;（&lt;code&gt;@Valid&lt;/code&gt; 校验）&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;scheduleService.update(id, request)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service 查询旧记录，更新字段并保存&lt;/li&gt;
&lt;li&gt;返回更新后的 &lt;code&gt;Result&amp;lt;InterviewScheduleDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;scheduleService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="delete-apiinterview-scheduleid-删除面试记录"&gt;&lt;a href="#delete-apiinterview-scheduleid-%e5%88%a0%e9%99%a4%e9%9d%a2%e8%af%95%e8%ae%b0%e5%bd%95" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/interview-schedule/{id}&lt;/code&gt; 删除面试记录
&lt;/h3&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller 接收 &lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;scheduleService.delete(id)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service 查到后删除，不存在则抛异常&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;scheduleService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="patchput-apiinterview-scheduleidstatus-更新面试状态"&gt;&lt;a href="#patchput-apiinterview-scheduleidstatus-%e6%9b%b4%e6%96%b0%e9%9d%a2%e8%af%95%e7%8a%b6%e6%80%81" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PATCH/PUT /api/interview-schedule/{id}/status&lt;/code&gt; 更新面试状态
&lt;/h3&gt;&lt;p&gt;接口实现：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/{id}/status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RequestMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PATCH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RequestMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PUT&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InterviewScheduleDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;InterviewStatus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;更新面试状态: ID={}, status={}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;InterviewScheduleDTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;scheduleService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;核心调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;scheduleService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;InterviewSchedule&lt;/code&gt; 模块的核心价值在于把“邀约文本理解”和“面试过程管理”连接起来。对我来说，这一层做好之后，前端日历交互、提醒策略和后续面试评估才能形成连续体验，避免信息散落在聊天记录和手工备忘里。&lt;/p&gt;</description></item><item><title>Ai简历分析：interview模块</title><link>https://xedczq.cn/post/aiinterview_interview/</link><pubDate>Thu, 14 May 2026 15:00:53 +0800</pubDate><guid>https://xedczq.cn/post/aiinterview_interview/</guid><description>&lt;h2 id="interview-模拟面试模块设计与实现"&gt;&lt;a href="#interview-%e6%a8%a1%e6%8b%9f%e9%9d%a2%e8%af%95%e6%a8%a1%e5%9d%97%e8%ae%be%e8%ae%a1%e4%b8%8e%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;Interview 模拟面试模块设计与实现
&lt;/h2&gt;&lt;p&gt;这篇笔记记录我在 &lt;code&gt;interview-guide&lt;/code&gt; 项目里对 &lt;code&gt;Interview&lt;/code&gt; 模块的实现思路、核心接口和评估链路。重点是把“出题、答题、评估、导出”做成一个完整闭环，并且保证文字面试与语音面试的评估逻辑保持一致。&lt;/p&gt;
&lt;h2 id="模块能力概览"&gt;&lt;a href="#%e6%a8%a1%e5%9d%97%e8%83%bd%e5%8a%9b%e6%a6%82%e8%a7%88" class="header-anchor"&gt;&lt;/a&gt;模块能力概览
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Skill 驱动出题：内置 10+ 面试方向（Java 后端、大厂专项、前端、Python、算法、系统设计、测开、AI Agent 等），每个方向由 &lt;code&gt;SKILL.md&lt;/code&gt; 约束考察范围与难度分布。&lt;/li&gt;
&lt;li&gt;历史题目去重：创建会话时优先排除历史会话中已问过的题，减少重复考察。&lt;/li&gt;
&lt;li&gt;面试时长联动：总时长变更后，各阶段（自我介绍、技术考察、项目深挖、反问）按比例自动分配。&lt;/li&gt;
&lt;li&gt;智能追问流：支持多轮追问配置（默认 1 条），模拟真实面试追问过程。&lt;/li&gt;
&lt;li&gt;统一评估引擎：文字面试和语音面试复用同一套评估架构（分批评估 + 结构化输出 + 汇总 + 兜底）。&lt;/li&gt;
&lt;li&gt;报告导出：支持异步生成并导出 PDF 面试报告。&lt;/li&gt;
&lt;li&gt;面试中心：统一入口，支持继续面试、重新面试和历史面试查看。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="核心状态流转"&gt;&lt;a href="#%e6%a0%b8%e5%bf%83%e7%8a%b6%e6%80%81%e6%b5%81%e8%bd%ac" class="header-anchor"&gt;&lt;/a&gt;核心状态流转
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["调用 POST /api/interview/sessions 创建会话"] --&gt; B{"是否有未完成会话\n且 forceCreate != true"}
B --&gt;|是| C["返回已有会话"]
B --&gt;|否| D["生成题目并保存会话"]

D --&gt; E["会话状态: CREATED\n缓存 Redis + 落库 DB"]
C --&gt; E

E --&gt; F["调用 GET /api/interview/sessions/{sessionId}/question"]
F --&gt; G{"当前状态是否 CREATED"}
G --&gt;|是| H["切换为 IN_PROGRESS"]
G --&gt;|否| I["保持原状态"]
H --&gt; J["返回当前题"]
I --&gt; J

J --&gt; K["调用 POST /api/interview/sessions/{sessionId}/answers 提交答案"]
K --&gt; L["保存答案"]
L --&gt; M{"是否还有下一题"}
M --&gt;|是| N["currentIndex + 1\n状态保持 IN_PROGRESS"]
M --&gt;|否| O["状态切换为 COMPLETED"]

N --&gt; F
O --&gt; P["evaluateStatus 置为 PENDING"]
P --&gt; Q["发送评估任务到 Redis Stream"]

R["调用 POST /api/interview/sessions/{sessionId}/complete 提前交卷"] --&gt; O

Q --&gt; S["评估消费者处理"]
S --&gt; T["evaluateStatus = PROCESSING"]
T --&gt; U{"评估是否成功"}
U --&gt;|是| V["保存评估报告"]
V --&gt; W["会话状态 = EVALUATED\nevaluateStatus = COMPLETED"]
U --&gt;|否| X{"重试次数 &lt; 3 ?"}
X --&gt;|是| Q
X --&gt;|否| Y["evaluateStatus = FAILED\n记录 evaluateError"]

Z["调用 DELETE /api/interview/sessions/{sessionId}"] --&gt; AA["删除 DB 会话与答案"]
AA --&gt; AB["会话结束"]&lt;/pre&gt;&lt;h2 id="关键接口设计"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e6%8e%a5%e5%8f%a3%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;关键接口设计
&lt;/h2&gt;&lt;h3 id="get-apiinterviewsessions-列出面试会话"&gt;&lt;a href="#get-apiinterviewsessions-%e5%88%97%e5%87%ba%e9%9d%a2%e8%af%95%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions&lt;/code&gt; 列出面试会话
&lt;/h3&gt;&lt;p&gt;用途：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;面试记录页展示，按创建时间倒序返回会话列表。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="post-apiinterviewsessions-创建面试会话"&gt;&lt;a href="#post-apiinterviewsessions-%e5%88%9b%e5%bb%ba%e9%9d%a2%e8%af%95%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview/sessions&lt;/code&gt; 创建面试会话
&lt;/h3&gt;&lt;p&gt;限流策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局限流 + IP 限流（5 次）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHistoricalQuestions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skillId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findTop10ByResumeIdAndSkillIdOrderByCreatedAtDesc&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findTop10BySkillIdOrderByCreatedAtDesc&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;questionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generateQuestionsBySkill&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveSession&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveSession&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiinterviewsessionssessionid-获取会话信息"&gt;&lt;a href="#get-apiinterviewsessionssessionid-%e8%8e%b7%e5%8f%96%e4%bc%9a%e8%af%9d%e4%bf%a1%e6%81%af" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}&lt;/code&gt; 获取会话信息
&lt;/h3&gt;&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;restoreSessionFromDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiinterviewsessionssessionidquestion-获取当前问题"&gt;&lt;a href="#get-apiinterviewsessionssessionidquestion-%e8%8e%b7%e5%8f%96%e5%bd%93%e5%89%8d%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}/question&lt;/code&gt; 获取当前问题
&lt;/h3&gt;&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrentQuestionResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;getCurrentQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;getOrRestoreSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;当会话为 &lt;code&gt;CREATED&lt;/code&gt; 状态时，按 &lt;code&gt;currentIndex&lt;/code&gt; 返回当前题目。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiinterviewsessionssessionidanswers-提交答案并推进"&gt;&lt;a href="#post-apiinterviewsessionssessionidanswers-%e6%8f%90%e4%ba%a4%e7%ad%94%e6%a1%88%e5%b9%b6%e6%8e%a8%e8%bf%9b" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview/sessions/{sessionId}/answers&lt;/code&gt; 提交答案并推进
&lt;/h3&gt;&lt;p&gt;限流策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局限流（10 次）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submitAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;更新题目答案、会话状态、缓存与数据库。&lt;/li&gt;
&lt;li&gt;如果是最后一题：&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateEvaluateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AsyncTaskStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;evaluateStreamProducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendEvaluateTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="post-apiinterviewsessionssessionidanswers-暂存答案不推进"&gt;&lt;a href="#post-apiinterviewsessionssessionidanswers-%e6%9a%82%e5%ad%98%e7%ad%94%e6%a1%88%e4%b8%8d%e6%8e%a8%e8%bf%9b" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview/sessions/{sessionId}/answers&lt;/code&gt; 暂存答案（不推进）
&lt;/h3&gt;&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;同步更新 Redis 与数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiinterviewsessionssessionidcomplete-提前交卷"&gt;&lt;a href="#post-apiinterviewsessionssessionidcomplete-%e6%8f%90%e5%89%8d%e4%ba%a4%e5%8d%b7" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview/sessions/{sessionId}/complete&lt;/code&gt; 提前交卷
&lt;/h3&gt;&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completeInterview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateSessionStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SessionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;COMPLETED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;持久化数据库状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;evaluateStreamProducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendEvaluateTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiinterviewsessionsunfinishedresumeid-查找未完成会话"&gt;&lt;a href="#get-apiinterviewsessionsunfinishedresumeid-%e6%9f%a5%e6%89%be%e6%9c%aa%e5%ae%8c%e6%88%90%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/unfinished/{resumeId}&lt;/code&gt; 查找未完成会话
&lt;/h3&gt;&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findUnfinishedSessionOrThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;findUnfinishedSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findUnfinishedSessionId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findUnfinishedSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiinterviewsessionssessionidreport-生成面试评估报告"&gt;&lt;a href="#get-apiinterviewsessionssessionidreport-%e7%94%9f%e6%88%90%e9%9d%a2%e8%af%95%e8%af%84%e4%bc%b0%e6%8a%a5%e5%91%8a" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}/report&lt;/code&gt; 生成面试评估报告
&lt;/h3&gt;&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generateReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;evaluationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;evaluateInterview&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;unifiedEvaluationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;evaluateInBatches&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;summarizeBatchResults&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;structuredOutputInvoker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;securedSystemPrompt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;systemPromptWithFormat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ANTI_INJECTION_INSTRUCTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;通过反注入指令降低用户输入污染模型行为的风险。&lt;/p&gt;
&lt;h3 id="get-apiinterviewsessionssessioniddetails-获取面试详情"&gt;&lt;a href="#get-apiinterviewsessionssessioniddetails-%e8%8e%b7%e5%8f%96%e9%9d%a2%e8%af%95%e8%af%a6%e6%83%85" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}/details&lt;/code&gt; 获取面试详情
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;historyService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInterviewDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;interviewPersistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findBySessionId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="get-apiinterviewsessionssessionidexport-导出面试报告-pdf"&gt;&lt;a href="#get-apiinterviewsessionssessionidexport-%e5%af%bc%e5%87%ba%e9%9d%a2%e8%af%95%e6%8a%a5%e5%91%8a-pdf" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}/export&lt;/code&gt; 导出面试报告 PDF
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;historyService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exportInterviewPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;interviewPersistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findBySessionId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pdfExportService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exportInterviewReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="delete-apiinterviewsessionssessionid-删除面试会话"&gt;&lt;a href="#delete-apiinterviewsessionssessionid-%e5%88%a0%e9%99%a4%e9%9d%a2%e8%af%95%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/interview/sessions/{sessionId}&lt;/code&gt; 删除面试会话
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteSessionBySessionId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findBySessionId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sessionRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="评估引擎实现要点"&gt;&lt;a href="#%e8%af%84%e4%bc%b0%e5%bc%95%e6%93%8e%e5%ae%9e%e7%8e%b0%e8%a6%81%e7%82%b9" class="header-anchor"&gt;&lt;/a&gt;评估引擎实现要点
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;同一条评估链路兼容文字面试与语音面试，减少分叉实现成本。&lt;/li&gt;
&lt;li&gt;分批评估后再汇总，兼顾长上下文稳定性与结构化输出质量。&lt;/li&gt;
&lt;li&gt;引入反注入提示词拼接，降低恶意输入对评估结果的干扰。&lt;/li&gt;
&lt;li&gt;异常场景通过统一调用器与兜底字段输出，避免报告直接失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Interview&lt;/code&gt; 模块目前已经实现了从创建会话、动态出题、过程作答、异步评估到报告导出的完整流程。对我来说，这一模块最关键的价值是把“面试过程管理”和“评估结果生产”拆成可演进的两个层次，后续无论替换题库策略还是升级评估模型，整体改动都能保持可控。&lt;/p&gt;</description></item><item><title>Ai简历分析：resume模块</title><link>https://xedczq.cn/post/aiinterview_resume/</link><pubDate>Thu, 14 May 2026 11:31:10 +0800</pubDate><guid>https://xedczq.cn/post/aiinterview_resume/</guid><description>&lt;h2 id="resume-简历管理模块设计与实现"&gt;&lt;a href="#resume-%e7%ae%80%e5%8e%86%e7%ae%a1%e7%90%86%e6%a8%a1%e5%9d%97%e8%ae%be%e8%ae%a1%e4%b8%8e%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;Resume 简历管理模块设计与实现
&lt;/h2&gt;&lt;p&gt;本文记录 &lt;code&gt;interview-guide&lt;/code&gt; 项目中 &lt;code&gt;Resume&lt;/code&gt; 模块的核心设计、接口职责、异步处理链路与当前问题。&lt;/p&gt;
&lt;h2 id="模块能力概览"&gt;&lt;a href="#%e6%a8%a1%e5%9d%97%e8%83%bd%e5%8a%9b%e6%a6%82%e8%a7%88" class="header-anchor"&gt;&lt;/a&gt;模块能力概览
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;多格式解析：支持 &lt;code&gt;PDF&lt;/code&gt;、&lt;code&gt;DOCX&lt;/code&gt;、&lt;code&gt;DOC&lt;/code&gt;、&lt;code&gt;TXT&lt;/code&gt;、&lt;code&gt;MD&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;异步处理流：基于 &lt;code&gt;Redis Stream&lt;/code&gt; 异步分析简历，支持进度状态跟踪。&lt;/li&gt;
&lt;li&gt;稳定性保障：失败自动重试（最多 3 次）+ 基于文件哈希的重复检测。&lt;/li&gt;
&lt;li&gt;报告导出：支持将 AI 分析结果导出为结构化 PDF 报告。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="核心状态流转"&gt;&lt;a href="#%e6%a0%b8%e5%bf%83%e7%8a%b6%e6%80%81%e6%b5%81%e8%bd%ac" class="header-anchor"&gt;&lt;/a&gt;核心状态流转
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["调用 /api/resumes/upload"] --&gt; B["文件校验与类型校验"]
B --&gt; C{"是否重复简历?"}

C --&gt;|是| D["返回历史结果或当前状态 (duplicate=true)"]
C --&gt;|否| E["解析文本 + 上传对象存储 + 保存 ResumeEntity"]

E --&gt; F["设置 analyzeStatus = PENDING"]
F --&gt; G["发送 Redis Stream 分析任务"]

G --&gt; H{"任务是否成功入队?"}
H --&gt;|否| I["置为 FAILED (任务入队失败)"]
H --&gt;|是| J["消费者拉取任务"]

J --&gt; K["置为 PROCESSING"]
K --&gt; L["调用 ResumeGradingService 进行 AI 分析"]

L --&gt; M{"本次处理是否异常?"}
M --&gt;|否| N["保存分析结果"]
N --&gt; O["置为 COMPLETED"]

M --&gt;|是| P{"retryCount &lt; 3 ?"}
P --&gt;|是| Q["retryCount + 1，重新入队"]
Q --&gt; J
P --&gt;|否| R["置为 FAILED (最终失败)"]

S["手动重试 /api/resumes/{id}/reanalyze"] --&gt; T["置为 PENDING 并重新入队"]
T --&gt; J&lt;/pre&gt;&lt;h2 id="关键接口设计"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e6%8e%a5%e5%8f%a3%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;关键接口设计
&lt;/h2&gt;&lt;h3 id="apiresumesupload-简历上传异步分析"&gt;&lt;a href="#apiresumesupload-%e7%ae%80%e5%8e%86%e4%b8%8a%e4%bc%a0%e5%bc%82%e6%ad%a5%e5%88%86%e6%9e%90" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/upload&lt;/code&gt; 简历上传（异步分析）
&lt;/h3&gt;&lt;p&gt;限流策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局限流：&lt;code&gt;@RateLimit(dimension = RateLimit.Dimension.GLOBAL, count = 5)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IP 限流：&lt;code&gt;@RateLimit(dimension = RateLimit.Dimension.IP, count = 5)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;入口调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;uploadService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uploadAndAnalyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;文件基础校验&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fileValidationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validateFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MAX_FILE_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;简历&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;包含：判空、大小限制、日志记录。
2. 文件类型识别&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;contentType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;parseService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;detectContentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;支持：&lt;code&gt;PDF&lt;/code&gt;、&lt;code&gt;DOCX&lt;/code&gt;、&lt;code&gt;DOC&lt;/code&gt;、&lt;code&gt;TXT&lt;/code&gt;、&lt;code&gt;MD&lt;/code&gt;。
3. 重复文件检测&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findExistingResume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;内部流程：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fileHashService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;resumeRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByFileHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="4"&gt;
&lt;li&gt;简历解析与文本清洗&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;parseService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parseResume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Apache Tika&lt;/code&gt; 解析纯文本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textCleaningService.cleanText(content)&lt;/code&gt; 清理多余换行，减少 Token 消耗&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="5"&gt;
&lt;li&gt;文件存储（非结构化）&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;storageService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uploadResume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;storageService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFileUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上传到 &lt;code&gt;RustFS/MinIO&lt;/code&gt;，用于非结构化文件存储。
6. 元数据入库&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveResume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;resumeText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fileKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fileUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="7"&gt;
&lt;li&gt;投递异步分析任务&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;analyzeStreamProducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendAnalyzeTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedResume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;resumeText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用 &lt;code&gt;Redis Stream&lt;/code&gt; 作为消息队列
8. 返回上传响应&lt;br&gt;
前端通过后续查询接口观察异步分析状态。&lt;/p&gt;
&lt;h3 id="apiresumes-获取简历列表"&gt;&lt;a href="#apiresumes-%e8%8e%b7%e5%8f%96%e7%ae%80%e5%8e%86%e5%88%97%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes&lt;/code&gt; 获取简历列表
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;historyService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAllResumes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;resumePersistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAllResumes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尚未区分用户数据，默认返回全量列表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="apiresumesiddetail-获取简历详情"&gt;&lt;a href="#apiresumesiddetail-%e8%8e%b7%e5%8f%96%e7%ae%80%e5%8e%86%e8%af%a6%e6%83%85" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/{id}/detail&lt;/code&gt; 获取简历详情
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;historyService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResumeDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;resumePersistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;resumeRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="apiresumesidexport-导出分析报告-pdf"&gt;&lt;a href="#apiresumesidexport-%e5%af%bc%e5%87%ba%e5%88%86%e6%9e%90%e6%8a%a5%e5%91%8a-pdf" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/{id}/export&lt;/code&gt; 导出分析报告 PDF
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;historyService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exportAnalysisPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;resumePersistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;resumePersistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLatestAnalysisAsDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pdfExportService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exportResumeAnalysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;analysisDTO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="apiresumesid-删除简历"&gt;&lt;a href="#apiresumesid-%e5%88%a0%e9%99%a4%e7%ae%80%e5%8e%86" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/{id}&lt;/code&gt; 删除简历
&lt;/h3&gt;&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;deleteService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteResume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;storageService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteResume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStorageKey&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;interviewPersistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteSessionsByResumeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;persistenceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteResume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="apiresumesidreanalyze-重新分析"&gt;&lt;a href="#apiresumesidreanalyze-%e9%87%8d%e6%96%b0%e5%88%86%e6%9e%90" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/{id}/reanalyze&lt;/code&gt; 重新分析
&lt;/h3&gt;&lt;p&gt;限流策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局限流：&lt;code&gt;@RateLimit(dimension = RateLimit.Dimension.GLOBAL, count = 2)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IP 限流：&lt;code&gt;@RateLimit(dimension = RateLimit.Dimension.IP, count = 2)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;uploadService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reanalyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;resumeRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;analyzeStreamProducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendAnalyzeTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resumeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;resumeText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;并在处理中更新状态后保存。&lt;/p&gt;
&lt;h3 id="apiresumeshealth-健康检查"&gt;&lt;a href="#apiresumeshealth-%e5%81%a5%e5%ba%b7%e6%a3%80%e6%9f%a5" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/health&lt;/code&gt; 健康检查
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;用于服务可用性探测。&lt;/p&gt;
&lt;h2 id="稳定性设计点"&gt;&lt;a href="#%e7%a8%b3%e5%ae%9a%e6%80%a7%e8%ae%be%e8%ae%a1%e7%82%b9" class="header-anchor"&gt;&lt;/a&gt;稳定性设计点
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;异步解耦：上传与分析分离，削峰并提升接口响应速度。&lt;/li&gt;
&lt;li&gt;自动重试：分析失败后最多重试 3 次，降低瞬时故障影响。&lt;/li&gt;
&lt;li&gt;哈希去重：基于内容 &lt;code&gt;SHA-256&lt;/code&gt; 快速识别重复简历，避免重复计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Resume&lt;/code&gt; 模块目前已经具备上传、解析、异步分析、导出与删除的完整闭环。下一阶段重点是用户隔离与可观测性建设，确保在多人使用和高并发场景下依旧稳定可控。&lt;/p&gt;</description></item></channel></rss>