<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Project on XEDCZQ Blog</title><link>https://xedczq.cn/en/categories/project/</link><description>Recent content in Project on XEDCZQ Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 15 May 2026 21:55:13 +0800</lastBuildDate><atom:link href="https://xedczq.cn/en/categories/project/index.xml" rel="self" type="application/rss+xml"/><item><title>AI Resume Analysis: Knowledgebase Module</title><link>https://xedczq.cn/en/post/aiinterview_knowledgebase/</link><pubDate>Fri, 15 May 2026 21:55:13 +0800</pubDate><guid>https://xedczq.cn/en/post/aiinterview_knowledgebase/</guid><description>&lt;h2 id="knowledgebase-module-design-and-implementation"&gt;&lt;a href="#knowledgebase-module-design-and-implementation" class="header-anchor"&gt;&lt;/a&gt;Knowledgebase Module Design and Implementation
&lt;/h2&gt;&lt;p&gt;This note records how I implemented the &lt;code&gt;Knowledgebase&lt;/code&gt; module in the &lt;code&gt;interview-guide&lt;/code&gt; project. The goal is to connect document upload, vectorization, RAG query, and session association into a sustainable knowledge service workflow.&lt;/p&gt;
&lt;h2 id="module-capability-overview"&gt;&lt;a href="#module-capability-overview" class="header-anchor"&gt;&lt;/a&gt;Module Capability Overview
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Document management: supports upload, download, deletion, categorization, keyword search, and statistics.&lt;/li&gt;
&lt;li&gt;Vectorization capability: stores vectors with &lt;code&gt;pgvector&lt;/code&gt;, and processes chunking/storage through async tasks.&lt;/li&gt;
&lt;li&gt;RAG Q&amp;amp;A: supports both non-streaming and streaming (SSE) multi-knowledgebase query.&lt;/li&gt;
&lt;li&gt;Session coordination: automatically removes associated session references when deleting a knowledgebase to reduce inconsistency risk.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="state-transitions"&gt;&lt;a href="#state-transitions" class="header-anchor"&gt;&lt;/a&gt;State Transitions
&lt;/h2&gt;&lt;h3 id="diagram-1-knowledgebase-main-state-machine"&gt;&lt;a href="#diagram-1-knowledgebase-main-state-machine" class="header-anchor"&gt;&lt;/a&gt;Diagram 1: KnowledgeBase Main State Machine
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["Call POST /api/knowledgebase/upload to upload file"] --&gt; B["File validation + type detection + dedup check"]

B --&gt; C{"Is file duplicated (fileHash exists)?"}
C --&gt;|Yes| D["Return existing knowledgebase record\nduplicate=true\nno vectorization triggered"]
C --&gt;|No| E["Parse text content + upload file to storage"]

E --&gt; F["Save KnowledgeBaseEntity\ninitial vectorStatus=PENDING"]
F --&gt; G["Send vectorization task to Redis Stream"]

G --&gt; H["VectorizeStreamConsumer consumes task"]
H --&gt; I["markProcessing\nvectorStatus=PROCESSING"]

I --&gt; J["vectorizeAndStore\nchunk text and write to pgvector"]
J --&gt; K{"Did vectorization succeed?"}

K --&gt;|Yes| L["markCompleted\nvectorStatus=COMPLETED\nvectorError=null"]
K --&gt;|No| M{"retryCount &lt; 3 ?"}
M --&gt;|Yes| N["Requeue task (retry+1)"]
N --&gt; H
M --&gt;|No| O["markFailed\nvectorStatus=FAILED\nwrite vectorError"]

P["Call POST /api/knowledgebase/{id}/revectorize"] --&gt; Q["Reset status to PENDING\nclear vectorError"]
Q --&gt; G

R["Call DELETE /api/knowledgebase/{id} to delete knowledgebase"] --&gt; S["Remove RAG session associations"]
S --&gt; T["Delete vector data (best effort) + delete storage file (best effort)"]
T --&gt; U["Delete knowledgebase DB record\nlifecycle ends"]&lt;/pre&gt;&lt;h3 id="diagram-2-chunked-knowledgebase-vectorization-flow"&gt;&lt;a href="#diagram-2-chunked-knowledgebase-vectorization-flow" class="header-anchor"&gt;&lt;/a&gt;Diagram 2: Chunked Knowledgebase Vectorization Flow
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["Knowledgebase upload succeeds"] --&gt; B["Save knowledgebase record vectorStatus=PENDING"]
B --&gt; C["Send vectorization task to Redis Stream"]
C --&gt; D["VectorizeStreamConsumer starts polling"]
D --&gt; E["Read one message: kbId + content + retryCount"]
E --&gt; F["Set status to PROCESSING"]
F --&gt; G["Execute vectorizeAndStore"]

G --&gt; H["Delete old vectors for this kbId"]
H --&gt; I["Text chunking via TokenTextSplitter"]
I --&gt; J["Add metadata kb_id to each chunk"]
J --&gt; K["Batch call vectorStore.add to write vectors"]

K --&gt; L["Set status to COMPLETED"]
L --&gt; M["ACK message"]

G --&gt; N{"Processing exception?"}
N --&gt;|Yes| O{"retryCount &lt; 3"}
O --&gt;|Yes| P["retryCount+1 and requeue"]
P --&gt; M
O --&gt;|No| Q["Set status to FAILED and record error"]
Q --&gt; M&lt;/pre&gt;&lt;h2 id="key-api-design"&gt;&lt;a href="#key-api-design" class="header-anchor"&gt;&lt;/a&gt;Key API Design
&lt;/h2&gt;&lt;h3 id="get-apiknowledgebaselist-get-knowledgebase-list-status-filter--sorting"&gt;&lt;a href="#get-apiknowledgebaselist-get-knowledgebase-list-status-filter--sorting" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/list&lt;/code&gt; Get Knowledgebase List (Status Filter + Sorting)
&lt;/h3&gt;&lt;p&gt;Call chain:&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-get-knowledgebase-detail"&gt;&lt;a href="#get-apiknowledgebaseid-get-knowledgebase-detail" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/{id}&lt;/code&gt; Get Knowledgebase Detail
&lt;/h3&gt;&lt;p&gt;Call chain:&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-delete-knowledgebase"&gt;&lt;a href="#delete-apiknowledgebaseid-delete-knowledgebase" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/knowledgebase/{id}&lt;/code&gt; Delete Knowledgebase
&lt;/h3&gt;&lt;p&gt;Core flow:&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;Notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Removes RAG session associations first, then deletes vectors/storage files, then DB record.&lt;/li&gt;
&lt;li&gt;Vector/storage deletion failures are logged as &lt;code&gt;warn&lt;/code&gt; and do not block the main delete flow.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiknowledgebasequery-non-streaming-qa-multi-knowledgebase"&gt;&lt;a href="#post-apiknowledgebasequery-non-streaming-qa-multi-knowledgebase" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/knowledgebase/query&lt;/code&gt; Non-Streaming Q&amp;amp;A (Multi-Knowledgebase)
&lt;/h3&gt;&lt;p&gt;Rate limits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLOBAL/IP: 10 each&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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;Processing highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;knowledgeBaseIds&lt;/code&gt; and &lt;code&gt;question&lt;/code&gt; are required.&lt;/li&gt;
&lt;li&gt;If no hit, returns fixed fallback text: &amp;ldquo;No information retrieved&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;If hit exists, builds context + prompts and calls default ChatClient for answer generation.&lt;/li&gt;
&lt;li&gt;Returns &lt;code&gt;QueryResponse(answer, primaryKbId, kbNamesStr)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiknowledgebasequerystream-streaming-qa-sse-multi-knowledgebase"&gt;&lt;a href="#post-apiknowledgebasequerystream-streaming-qa-sse-multi-knowledgebase" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/knowledgebase/query/stream&lt;/code&gt; Streaming Q&amp;amp;A (SSE, Multi-Knowledgebase)
&lt;/h3&gt;&lt;p&gt;Rate limits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLOBAL/IP: 5 each&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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;Processing highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Returns &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;Empty input or no hit returns fallback text stream directly.&lt;/li&gt;
&lt;li&gt;Both stream-time and outer exceptions are downgraded to safe fallback output.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apiknowledgebasecategories-get-all-category-names"&gt;&lt;a href="#get-apiknowledgebasecategories-get-all-category-names" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/categories&lt;/code&gt; Get All Category Names
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Return:&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-get-knowledgebase-list-by-category"&gt;&lt;a href="#get-apiknowledgebasecategorycategory-get-knowledgebase-list-by-category" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/category/{category}&lt;/code&gt; Get Knowledgebase List by Category
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Return:&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-get-uncategorized-knowledgebase-list"&gt;&lt;a href="#get-apiknowledgebaseuncategorized-get-uncategorized-knowledgebase-list" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/uncategorized&lt;/code&gt; Get Uncategorized Knowledgebase List
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Current implementation reuses category-query path and distinguishes uncategorized by specific category value.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put-apiknowledgebaseidcategory-update-knowledgebase-category"&gt;&lt;a href="#put-apiknowledgebaseidcategory-update-knowledgebase-category" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/knowledgebase/{id}/category&lt;/code&gt; Update Knowledgebase Category
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Processing highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queries by &lt;code&gt;id&lt;/code&gt; first and throws business exception if not found.&lt;/li&gt;
&lt;li&gt;Updates &lt;code&gt;category&lt;/code&gt; and persists record when found.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiknowledgebaseupload-upload-knowledgebase-file-multipart"&gt;&lt;a href="#post-apiknowledgebaseupload-upload-knowledgebase-file-multipart" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/knowledgebase/upload&lt;/code&gt; Upload Knowledgebase File (multipart)
&lt;/h3&gt;&lt;p&gt;Parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file&lt;/code&gt; (required)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt; (optional)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;category&lt;/code&gt; (optional)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rate limits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLOBAL/IP: 3 each&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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;Processing flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Validate file presence and size (max 50MB).&lt;/li&gt;
&lt;li&gt;Validate type by MIME + extension whitelist (PDF/DOCX/DOC/TXT/MD).&lt;/li&gt;
&lt;li&gt;Compute &lt;code&gt;SHA-256&lt;/code&gt; for dedup check.&lt;/li&gt;
&lt;li&gt;Parse text content; fail directly on empty text.&lt;/li&gt;
&lt;li&gt;Upload file to RustFS (S3-compatible), generate &lt;code&gt;fileKey/fileUrl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Save &lt;code&gt;KnowledgeBaseEntity&lt;/code&gt; with initial vector status &lt;code&gt;PENDING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enqueue async vectorization task to Redis Stream (&lt;code&gt;knowledgebase:vectorize:stream&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;knowledgeBase + storage + duplicate=false&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="get-apiknowledgebaseiddownload-download-original-knowledgebase-file"&gt;&lt;a href="#get-apiknowledgebaseiddownload-download-original-knowledgebase-file" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/{id}/download&lt;/code&gt; Download Original Knowledgebase File
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Return:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ResponseEntity&amp;lt;byte[]&amp;gt;&lt;/code&gt; (with &lt;code&gt;Content-Disposition&lt;/code&gt; and &lt;code&gt;Content-Type&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apiknowledgebasesearchkeyword-keyword-search-knowledgebase"&gt;&lt;a href="#get-apiknowledgebasesearchkeyword-keyword-search-knowledgebase" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/search?keyword=...&lt;/code&gt; Keyword Search Knowledgebase
&lt;/h3&gt;&lt;p&gt;Call chain:&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-get-knowledgebase-statistics"&gt;&lt;a href="#get-apiknowledgebasestats-get-knowledgebase-statistics" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/knowledgebase/stats&lt;/code&gt; Get Knowledgebase Statistics
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Return:&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-manual-re-vectorization"&gt;&lt;a href="#post-apiknowledgebaseidrevectorize-manual-re-vectorization" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/knowledgebase/{id}/revectorize&lt;/code&gt; Manual Re-Vectorization
&lt;/h3&gt;&lt;p&gt;Rate limits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLOBAL/IP: 2 each&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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;Processing flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Query knowledgebase by &lt;code&gt;id&lt;/code&gt;, throw exception if missing.&lt;/li&gt;
&lt;li&gt;Download source file from object storage and re-parse text.&lt;/li&gt;
&lt;li&gt;Fail directly if parsing fails or returns empty text.&lt;/li&gt;
&lt;li&gt;Reset vector status to &lt;code&gt;PENDING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enqueue vectorization task to Redis Stream.&lt;/li&gt;
&lt;li&gt;Return success immediately; frontend polls status afterward.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="async-vectorization-processing-flow-core-implementation"&gt;&lt;a href="#async-vectorization-processing-flow-core-implementation" class="header-anchor"&gt;&lt;/a&gt;Async Vectorization Processing Flow (Core Implementation)
&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) Delete old vectors&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) Text chunking (default no overlap)&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) Add 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) Batch vector write (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="summary"&gt;&lt;a href="#summary" class="header-anchor"&gt;&lt;/a&gt;Summary
&lt;/h2&gt;&lt;p&gt;The core value of the &lt;code&gt;Knowledgebase&lt;/code&gt; module is connecting file asset management with retrieval-augmented Q&amp;amp;A. For me, the real value is not just successful upload, but making sure documents reliably enter the vectorization pipeline and finally provide reusable, traceable knowledge support in Q&amp;amp;A scenarios.&lt;/p&gt;</description></item><item><title>AI Resume Analysis: Voice Interview Module</title><link>https://xedczq.cn/en/post/aiinterview_voiceinterview/</link><pubDate>Thu, 14 May 2026 22:34:43 +0800</pubDate><guid>https://xedczq.cn/en/post/aiinterview_voiceinterview/</guid><description>&lt;h2 id="voiceinterview-module-design-and-implementation"&gt;&lt;a href="#voiceinterview-module-design-and-implementation" class="header-anchor"&gt;&lt;/a&gt;VoiceInterview Module Design and Implementation
&lt;/h2&gt;&lt;p&gt;This note records how I implemented the &lt;code&gt;VoiceInterview&lt;/code&gt; module in the &lt;code&gt;interview-guide&lt;/code&gt; project. The core goal is to make voice interviews deliver a complete experience of real-time interaction, resumable sessions, and traceable evaluation.&lt;/p&gt;
&lt;h2 id="module-capability-overview"&gt;&lt;a href="#module-capability-overview" class="header-anchor"&gt;&lt;/a&gt;Module Capability Overview
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Real-time voice interaction: built on &lt;code&gt;WebSocket + Qwen3 Voice Model&lt;/code&gt; (shared API key for ASR/TTS/LLM).&lt;/li&gt;
&lt;li&gt;Streaming experience optimization: sentence-level concurrent TTS, generation/synthesis/playback in parallel, first-packet latency around 200ms.&lt;/li&gt;
&lt;li&gt;Server-side VAD: automatic segmentation with real-time subtitles (including intermediate results).&lt;/li&gt;
&lt;li&gt;Echo protection: supports manual submission to avoid AI playback being captured as user input.&lt;/li&gt;
&lt;li&gt;Session continuity: supports pause/resume and multi-turn context memory, with auto-pause on timeout.&lt;/li&gt;
&lt;li&gt;Observability metrics: Micrometer metrics for TTS/ASR latency, session duration, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="state-transitions"&gt;&lt;a href="#state-transitions" class="header-anchor"&gt;&lt;/a&gt;State Transitions
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["Create Session&lt;br/&gt;POST /api/voice-interview/sessions"] --&gt; B["IN_PROGRESS"]

B --&gt; C{"Session Events"}
C -- "Pause / Timeout" --&gt; D["PAUSED"]
D -- "Resume" --&gt; B

C -- "End Interview" --&gt; E["COMPLETED"]
E --&gt; F["evaluateStatus = PENDING"]
F --&gt; G["evaluateStatus = PROCESSING"]

G --&gt; H{"Evaluation Result"}
H -- "Success" --&gt; I["EVALUATED&lt;br/&gt;evaluateStatus = COMPLETED"]
H -- "Failure" --&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="key-api-design"&gt;&lt;a href="#key-api-design" class="header-anchor"&gt;&lt;/a&gt;Key API Design
&lt;/h2&gt;&lt;h3 id="post-apivoice-interviewsessions-create-voice-interview-session"&gt;&lt;a href="#post-apivoice-interviewsessions-create-voice-interview-session" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/voice-interview/sessions&lt;/code&gt; Create Voice Interview Session
&lt;/h3&gt;&lt;p&gt;Controller entry:&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;Core call chain:&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;Implementation highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fallback &lt;code&gt;skillId&lt;/code&gt; (use default skill when missing).&lt;/li&gt;
&lt;li&gt;Fallback &lt;code&gt;llmProvider&lt;/code&gt; (use default provider when empty).&lt;/li&gt;
&lt;li&gt;Build &lt;code&gt;VoiceInterviewSessionEntity&lt;/code&gt; (phase switches, difficulty, resume ID, JD text, planned duration, etc.).&lt;/li&gt;
&lt;li&gt;Default &lt;code&gt;userId = &amp;quot;default&amp;quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set initial phase (the first enabled one in &lt;code&gt;intro/tech/project/hr&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Persist to &lt;code&gt;voice_interview_sessions&lt;/code&gt; and cache in Redis (with TTL).&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;SessionResponseDTO&lt;/code&gt; (session ID, status, phase, config, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apivoice-interviewsessionssessionid-get-session-detail-by-id"&gt;&lt;a href="#get-apivoice-interviewsessionssessionid-get-session-detail-by-id" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/voice-interview/sessions/{sessionId}&lt;/code&gt; Get Session Detail by ID
&lt;/h3&gt;&lt;p&gt;Controller call:&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;Implementation highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read Redis first, then DB fallback.&lt;/li&gt;
&lt;li&gt;Build &lt;code&gt;SessionResponseDTO&lt;/code&gt; when found.&lt;/li&gt;
&lt;li&gt;Return unified error when not found: &lt;code&gt;Session not found: {sessionId}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apivoice-interviewsessionssessionidend-end-session-and-trigger-async-evaluation"&gt;&lt;a href="#post-apivoice-interviewsessionssessionidend-end-session-and-trigger-async-evaluation" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/voice-interview/sessions/{sessionId}/end&lt;/code&gt; End Session and Trigger Async Evaluation
&lt;/h3&gt;&lt;p&gt;Controller call:&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;End + evaluation logic:&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;Notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API returns &lt;code&gt;Result.success()&lt;/code&gt; immediately without waiting for evaluation completion.&lt;/li&gt;
&lt;li&gt;Frontend polls &lt;code&gt;GET /api/voice-interview/sessions/{sessionId}/evaluation&lt;/code&gt; for progress.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put-apivoice-interviewsessionssessionidpause-pause-session"&gt;&lt;a href="#put-apivoice-interviewsessionssessionidpause-pause-session" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/voice-interview/sessions/{sessionId}/pause&lt;/code&gt; Pause Session
&lt;/h3&gt;&lt;p&gt;Core call:&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;Implementation highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only &lt;code&gt;IN_PROGRESS&lt;/code&gt; sessions can be paused.&lt;/li&gt;
&lt;li&gt;Set status to &lt;code&gt;PAUSED&lt;/code&gt;, record reason, update &lt;code&gt;updatedAt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Persist DB and sync Redis cache.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put-apivoice-interviewsessionssessionidresume-resume-session"&gt;&lt;a href="#put-apivoice-interviewsessionssessionidresume-resume-session" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/voice-interview/sessions/{sessionId}/resume&lt;/code&gt; Resume Session
&lt;/h3&gt;&lt;p&gt;Core call:&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;Implementation highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only &lt;code&gt;PAUSED&lt;/code&gt; sessions can be resumed.&lt;/li&gt;
&lt;li&gt;After resume, status becomes &lt;code&gt;IN_PROGRESS&lt;/code&gt; without resetting phase/progress.&lt;/li&gt;
&lt;li&gt;Persist DB, sync Redis, and return latest &lt;code&gt;SessionResponseDTO&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apivoice-interviewsessions-get-session-list-filter-by-useridstatus"&gt;&lt;a href="#get-apivoice-interviewsessions-get-session-list-filter-by-useridstatus" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/voice-interview/sessions&lt;/code&gt; Get Session List (Filter by userId/status)
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Return:&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-delete-voice-interview-session"&gt;&lt;a href="#delete-apivoice-interviewsessionssessionid-delete-voice-interview-session" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/voice-interview/sessions/{sessionId}&lt;/code&gt; Delete Voice Interview Session
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Implementation highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validate session existence.&lt;/li&gt;
&lt;li&gt;Delete session and related data (messages/evaluation, depending on repository implementation).&lt;/li&gt;
&lt;li&gt;Clear Redis cache.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="get-apivoice-interviewsessionssessionidmessages-get-conversation-history"&gt;&lt;a href="#get-apivoice-interviewsessionssessionidmessages-get-conversation-history" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/voice-interview/sessions/{sessionId}/messages&lt;/code&gt; Get Conversation History
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Return:&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-get-async-evaluation-status-and-result"&gt;&lt;a href="#get-apivoice-interviewsessionssessionidevaluation-get-async-evaluation-status-and-result" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/voice-interview/sessions/{sessionId}/evaluation&lt;/code&gt; Get Async Evaluation Status and Result
&lt;/h3&gt;&lt;p&gt;Implementation highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validate session first (throw &lt;code&gt;VOICE_SESSION_NOT_FOUND&lt;/code&gt; if missing).&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;evaluateStatus&lt;/code&gt; and &lt;code&gt;evaluateError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If status is &lt;code&gt;COMPLETED&lt;/code&gt;, load evaluation details:&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;Return &lt;code&gt;VoiceEvaluationStatusDTO&lt;/code&gt; (includes status and result when completed).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apivoice-interviewsessionssessionidevaluation-manually-trigger-async-evaluation"&gt;&lt;a href="#post-apivoice-interviewsessionssessionidevaluation-manually-trigger-async-evaluation" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/voice-interview/sessions/{sessionId}/evaluation&lt;/code&gt; Manually Trigger Async Evaluation
&lt;/h3&gt;&lt;p&gt;Processing logic:&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;Rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If already &lt;code&gt;COMPLETED&lt;/code&gt;: return existing evaluation result directly.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;PENDING/PROCESSING&lt;/code&gt;: return current status without duplicate triggering.&lt;/li&gt;
&lt;li&gt;For other triggerable states: enqueue evaluation task and return &lt;code&gt;PENDING&lt;/code&gt;, then frontend continues polling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary"&gt;&lt;a href="#summary" class="header-anchor"&gt;&lt;/a&gt;Summary
&lt;/h2&gt;&lt;p&gt;The key value of the &lt;code&gt;VoiceInterview&lt;/code&gt; module is not just making voice interaction work, but making the entire real-time pipeline and session lifecycle robustly connected. For me, only when the full chain (create, pause, resume, end, evaluate) works reliably can voice interviews become a truly evolvable product capability.&lt;/p&gt;</description></item><item><title>AI Resume Analysis: Interview Schedule Module</title><link>https://xedczq.cn/en/post/aiinterview_interviewschedule/</link><pubDate>Thu, 14 May 2026 17:10:42 +0800</pubDate><guid>https://xedczq.cn/en/post/aiinterview_interviewschedule/</guid><description>&lt;h2 id="interviewschedule-module-design-and-implementation"&gt;&lt;a href="#interviewschedule-module-design-and-implementation" class="header-anchor"&gt;&lt;/a&gt;InterviewSchedule Module Design and Implementation
&lt;/h2&gt;&lt;p&gt;This note records how I implemented the &lt;code&gt;InterviewSchedule&lt;/code&gt; module in the &lt;code&gt;interview-guide&lt;/code&gt; project. The goal is to integrate invitation parsing, record management, status maintenance, and reminder coordination into one stable and maintainable workflow.&lt;/p&gt;
&lt;h2 id="module-capability-overview"&gt;&lt;a href="#module-capability-overview" class="header-anchor"&gt;&lt;/a&gt;Module Capability Overview
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Invitation parsing: dual-channel parsing with rule engine + AI, supports Feishu/Tencent Meeting/Zoom text formats, automatically extracts company, role, interview time, and meeting link.&lt;/li&gt;
&lt;li&gt;Calendar management: supports day/week/month view, drag-and-drop adjustment, and list view collaboration.&lt;/li&gt;
&lt;li&gt;Status maintenance: supports manual status updates and scheduled auto-expiration.&lt;/li&gt;
&lt;li&gt;Reminder mechanism: supports configurable reminders to reduce missed interviews.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="state-transitions"&gt;&lt;a href="#state-transitions" class="header-anchor"&gt;&lt;/a&gt;State Transitions
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["Call POST /api/interview-schedule/parse to parse invitation text"] --&gt; B{"Did rule parsing succeed?"}
B --&gt;|Yes| C["Return ParseResponse\nparseMethod = rule"]
B --&gt;|No| D["Call LLM parsing"]
D --&gt; E{"Did AI parsing succeed?"}
E --&gt;|Yes| F["Return ParseResponse\nparseMethod = ai"]
E --&gt;|No| G["Return parse failure\nsuccess = false"]

H["Call POST /api/interview-schedule to create record"] --&gt; I["create(): force status = PENDING"]
I --&gt; J["Write to DB\nstatus: PENDING"]

J --&gt; K["Call GET /api/interview-schedule or /{id} to query record"]

J --&gt; L["Call PUT /api/interview-schedule/{id} to update base info"]
L --&gt; M["Only update company/role/time fields\nwithout changing status"]
M --&gt; J

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

O --&gt; P{"Target status"}
P --&gt;|COMPLETED| Q["Status -&gt; COMPLETED"]
P --&gt;|CANCELLED| R["Status -&gt; CANCELLED"]
P --&gt;|RESCHEDULED| S["Status -&gt; RESCHEDULED"]
P --&gt;|PENDING| T["Status -&gt; PENDING"]

Q --&gt; U["Record can still be rewritten via status API"]
R --&gt; U
S --&gt; U
T --&gt; U
U --&gt; N

J --&gt; V["Scheduled task ScheduleStatusUpdater\nruns every hour"]
V --&gt; W{"Condition met?\nstatus=PENDING and interviewTime &lt; now"}
W --&gt;|Yes| X["Batch update to CANCELLED"]
W --&gt;|No| Y["No change"]

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

J --&gt; Z["Call DELETE /api/interview-schedule/{id}"]
Z --&gt; AA["Delete record (lifecycle ends)"]&lt;/pre&gt;&lt;h2 id="key-api-design"&gt;&lt;a href="#key-api-design" class="header-anchor"&gt;&lt;/a&gt;Key API Design
&lt;/h2&gt;&lt;h3 id="post-apiinterview-scheduleparse-parse-interview-invitation-text"&gt;&lt;a href="#post-apiinterview-scheduleparse-parse-interview-invitation-text" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview-schedule/parse&lt;/code&gt; Parse Interview Invitation Text
&lt;/h3&gt;&lt;p&gt;Core logic:&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;Rule parsing handles structured patterns from Feishu/Tencent/Zoom first.&lt;/li&gt;
&lt;li&gt;AI parsing acts as a fallback channel for non-standard text.&lt;/li&gt;
&lt;li&gt;Input boundary constraints and prompt-injection protection are applied before AI parsing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiinterview-schedule-create-interview-record"&gt;&lt;a href="#post-apiinterview-schedule-create-interview-record" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview-schedule&lt;/code&gt; Create Interview Record
&lt;/h3&gt;&lt;p&gt;Purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Allows users to directly create an interview schedule record from manual input.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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;Request body (core fields):&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;Company name cannot be empty&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;Position cannot be empty&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;Interview time cannot be empty&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-get-interview-record-by-id"&gt;&lt;a href="#get-apiinterview-scheduleid-get-interview-record-by-id" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview-schedule/{id}&lt;/code&gt; Get Interview Record by ID
&lt;/h3&gt;&lt;p&gt;Processing flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller receives &lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;scheduleService.getById(id)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service queries repository for one record and throws business exception if not found&lt;/li&gt;
&lt;li&gt;Returns &lt;code&gt;Result&amp;lt;InterviewScheduleDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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-get-interview-record-list"&gt;&lt;a href="#get-apiinterview-schedule-get-interview-record-list" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview-schedule&lt;/code&gt; Get Interview Record List
&lt;/h3&gt;&lt;p&gt;Processing flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller accepts optional filters: &lt;code&gt;status/start/end&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;scheduleService.getAll(status, start, end)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service queries by conditions and converts to DTO&lt;/li&gt;
&lt;li&gt;Returns &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;Call chain:&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-update-interview-record"&gt;&lt;a href="#put-apiinterview-scheduleid-update-interview-record" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PUT /api/interview-schedule/{id}&lt;/code&gt; Update Interview Record
&lt;/h3&gt;&lt;p&gt;Processing flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller receives &lt;code&gt;id + CreateInterviewRequest&lt;/code&gt; (with &lt;code&gt;@Valid&lt;/code&gt; validation)&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;scheduleService.update(id, request)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service loads existing record, updates fields, and saves&lt;/li&gt;
&lt;li&gt;Returns updated &lt;code&gt;Result&amp;lt;InterviewScheduleDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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-delete-interview-record"&gt;&lt;a href="#delete-apiinterview-scheduleid-delete-interview-record" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/interview-schedule/{id}&lt;/code&gt; Delete Interview Record
&lt;/h3&gt;&lt;p&gt;Processing flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller receives &lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;scheduleService.delete(id)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service deletes when found, throws exception when missing&lt;/li&gt;
&lt;li&gt;Returns &lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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-update-interview-status"&gt;&lt;a href="#patchput-apiinterview-scheduleidstatus-update-interview-status" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;PATCH/PUT /api/interview-schedule/{id}/status&lt;/code&gt; Update Interview Status
&lt;/h3&gt;&lt;p&gt;API implementation:&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;Update interview status: 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;Core call:&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="summary"&gt;&lt;a href="#summary" class="header-anchor"&gt;&lt;/a&gt;Summary
&lt;/h2&gt;&lt;p&gt;The core value of the &lt;code&gt;InterviewSchedule&lt;/code&gt; module is connecting invitation understanding with interview process management. For me, this layer is what enables frontend calendar interaction, reminder strategy, and downstream interview evaluation to form a continuous user experience, instead of scattering information across chats and manual notes.&lt;/p&gt;</description></item><item><title>AI Resume Analysis: Interview Module</title><link>https://xedczq.cn/en/post/aiinterview_interview/</link><pubDate>Thu, 14 May 2026 15:00:53 +0800</pubDate><guid>https://xedczq.cn/en/post/aiinterview_interview/</guid><description>&lt;h2 id="interview-mock-interview-module-design-and-implementation"&gt;&lt;a href="#interview-mock-interview-module-design-and-implementation" class="header-anchor"&gt;&lt;/a&gt;Interview Mock Interview Module Design and Implementation
&lt;/h2&gt;&lt;p&gt;This note records how I implemented the &lt;code&gt;Interview&lt;/code&gt; module in the &lt;code&gt;interview-guide&lt;/code&gt; project, including the core APIs and evaluation pipeline. The main goal is to build a complete closed loop for question generation, answering, evaluation, and report export, while keeping text interviews and voice interviews aligned under the same evaluation logic.&lt;/p&gt;
&lt;h2 id="module-capability-overview"&gt;&lt;a href="#module-capability-overview" class="header-anchor"&gt;&lt;/a&gt;Module Capability Overview
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Skill-driven question generation: supports 10+ interview tracks (Java backend, major-company tracks, frontend, Python, algorithms, system design, test development, AI Agent, etc.). Each track is defined by &lt;code&gt;SKILL.md&lt;/code&gt; for scope and difficulty distribution.&lt;/li&gt;
&lt;li&gt;Historical question deduplication: previously asked questions in historical sessions are excluded during session creation to reduce repeated assessment.&lt;/li&gt;
&lt;li&gt;Interview stage duration linkage: after total duration changes, each stage (self-introduction, technical assessment, project deep-dive, reverse Q&amp;amp;A) is auto-allocated by ratio.&lt;/li&gt;
&lt;li&gt;Intelligent follow-up flow: supports multi-round follow-up configuration (default: 1 round) to simulate realistic interview interactions.&lt;/li&gt;
&lt;li&gt;Unified evaluation engine: text and voice interviews share the same evaluation architecture (batch evaluation + structured output + summarization + fallback).&lt;/li&gt;
&lt;li&gt;Report export: supports asynchronous generation and export of PDF interview reports.&lt;/li&gt;
&lt;li&gt;Interview center: unified entry for continue/restart/history operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="core-state-flow"&gt;&lt;a href="#core-state-flow" class="header-anchor"&gt;&lt;/a&gt;Core State Flow
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["Call POST /api/interview/sessions to create session"] --&gt; B{"Any unfinished session\nand forceCreate != true?"}
B --&gt;|Yes| C["Return existing session"]
B --&gt;|No| D["Generate questions and save session"]

D --&gt; E["Session state: CREATED\nCache in Redis + persist in DB"]
C --&gt; E

E --&gt; F["Call GET /api/interview/sessions/{sessionId}/question"]
F --&gt; G{"Is current state CREATED?"}
G --&gt;|Yes| H["Switch to IN_PROGRESS"]
G --&gt;|No| I["Keep current state"]
H --&gt; J["Return current question"]
I --&gt; J

J --&gt; K["Call POST /api/interview/sessions/{sessionId}/answers to submit answer"]
K --&gt; L["Save answer"]
L --&gt; M{"Any next question?"}
M --&gt;|Yes| N["currentIndex + 1\nState remains IN_PROGRESS"]
M --&gt;|No| O["Switch state to COMPLETED"]

N --&gt; F
O --&gt; P["Set evaluateStatus to PENDING"]
P --&gt; Q["Send evaluation task to Redis Stream"]

R["Call POST /api/interview/sessions/{sessionId}/complete for early submit"] --&gt; O

Q --&gt; S["Evaluation consumer processes task"]
S --&gt; T["evaluateStatus = PROCESSING"]
T --&gt; U{"Evaluation successful?"}
U --&gt;|Yes| V["Save evaluation report"]
V --&gt; W["Session state = EVALUATED\nevaluateStatus = COMPLETED"]
U --&gt;|No| X{"Retry count &lt; 3 ?"}
X --&gt;|Yes| Q
X --&gt;|No| Y["evaluateStatus = FAILED\nRecord evaluateError"]

Z["Call DELETE /api/interview/sessions/{sessionId}"] --&gt; AA["Delete DB session and answers"]
AA --&gt; AB["Session ended"]&lt;/pre&gt;&lt;h2 id="key-api-design"&gt;&lt;a href="#key-api-design" class="header-anchor"&gt;&lt;/a&gt;Key API Design
&lt;/h2&gt;&lt;h3 id="get-apiinterviewsessions-list-interview-sessions"&gt;&lt;a href="#get-apiinterviewsessions-list-interview-sessions" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions&lt;/code&gt; List Interview Sessions
&lt;/h3&gt;&lt;p&gt;Purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Used by the interview history page, returns session list in reverse creation order.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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-create-interview-session"&gt;&lt;a href="#post-apiinterviewsessions-create-interview-session" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview/sessions&lt;/code&gt; Create Interview Session
&lt;/h3&gt;&lt;p&gt;Rate limiting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Global limit + IP limit (5)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Core logic:&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-get-session-info"&gt;&lt;a href="#get-apiinterviewsessionssessionid-get-session-info" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}&lt;/code&gt; Get Session Info
&lt;/h3&gt;&lt;p&gt;Core logic:&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-get-current-question"&gt;&lt;a href="#get-apiinterviewsessionssessionidquestion-get-current-question" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}/question&lt;/code&gt; Get Current Question
&lt;/h3&gt;&lt;p&gt;Core logic:&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;If session is in &lt;code&gt;CREATED&lt;/code&gt; state, return question by &lt;code&gt;currentIndex&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiinterviewsessionssessionidanswers-submit-answer-and-move-forward"&gt;&lt;a href="#post-apiinterviewsessionssessionidanswers-submit-answer-and-move-forward" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview/sessions/{sessionId}/answers&lt;/code&gt; Submit Answer and Move Forward
&lt;/h3&gt;&lt;p&gt;Rate limiting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Global limit (10)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Core logic:&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;Updates answer, session state, cache, and DB.&lt;/li&gt;
&lt;li&gt;If this is the last question:&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-save-draft-answer-no-progress"&gt;&lt;a href="#post-apiinterviewsessionssessionidanswers-save-draft-answer-no-progress" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview/sessions/{sessionId}/answers&lt;/code&gt; Save Draft Answer (No Progress)
&lt;/h3&gt;&lt;p&gt;Core logic:&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;Syncs both Redis and DB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="post-apiinterviewsessionssessionidcomplete-early-submit"&gt;&lt;a href="#post-apiinterviewsessionssessionidcomplete-early-submit" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;POST /api/interview/sessions/{sessionId}/complete&lt;/code&gt; Early Submit
&lt;/h3&gt;&lt;p&gt;Core logic:&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;Persists DB status.&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-find-unfinished-session"&gt;&lt;a href="#get-apiinterviewsessionsunfinishedresumeid-find-unfinished-session" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/unfinished/{resumeId}&lt;/code&gt; Find Unfinished Session
&lt;/h3&gt;&lt;p&gt;Core logic:&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-generate-interview-evaluation-report"&gt;&lt;a href="#get-apiinterviewsessionssessionidreport-generate-interview-evaluation-report" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}/report&lt;/code&gt; Generate Interview Evaluation Report
&lt;/h3&gt;&lt;p&gt;Core logic:&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;Uses anti-injection instruction to reduce prompt contamination risk from user input.&lt;/p&gt;
&lt;h3 id="get-apiinterviewsessionssessioniddetails-get-interview-detail"&gt;&lt;a href="#get-apiinterviewsessionssessioniddetails-get-interview-detail" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}/details&lt;/code&gt; Get Interview Detail
&lt;/h3&gt;&lt;p&gt;Call chain:&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-export-interview-report-as-pdf"&gt;&lt;a href="#get-apiinterviewsessionssessionidexport-export-interview-report-as-pdf" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;GET /api/interview/sessions/{sessionId}/export&lt;/code&gt; Export Interview Report as PDF
&lt;/h3&gt;&lt;p&gt;Call chain:&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-delete-interview-session"&gt;&lt;a href="#delete-apiinterviewsessionssessionid-delete-interview-session" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;DELETE /api/interview/sessions/{sessionId}&lt;/code&gt; Delete Interview Session
&lt;/h3&gt;&lt;p&gt;Call chain:&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="evaluation-engine-implementation-highlights"&gt;&lt;a href="#evaluation-engine-implementation-highlights" class="header-anchor"&gt;&lt;/a&gt;Evaluation Engine Implementation Highlights
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;A single evaluation pipeline supports both text and voice interviews, reducing branch complexity.&lt;/li&gt;
&lt;li&gt;Batch-first then summarize strategy balances long-context stability and structured output quality.&lt;/li&gt;
&lt;li&gt;Anti-injection prompt composition is applied to reduce malicious-input interference.&lt;/li&gt;
&lt;li&gt;In failure scenarios, unified invoker + fallback fields avoid hard report failures.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary"&gt;&lt;a href="#summary" class="header-anchor"&gt;&lt;/a&gt;Summary
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;Interview&lt;/code&gt; module now covers the full workflow from session creation, dynamic question generation, answer progression, asynchronous evaluation, to report export. For me, the key value is separating interview process management from evaluation result production into two evolvable layers, so future changes to question strategy or model upgrades can stay controlled.&lt;/p&gt;</description></item><item><title>AI Resume Analysis: Resume Module</title><link>https://xedczq.cn/en/post/aiinterview_resume/</link><pubDate>Thu, 14 May 2026 11:31:10 +0800</pubDate><guid>https://xedczq.cn/en/post/aiinterview_resume/</guid><description>&lt;h2 id="resume-module-design-and-implementation"&gt;&lt;a href="#resume-module-design-and-implementation" class="header-anchor"&gt;&lt;/a&gt;Resume Module Design and Implementation
&lt;/h2&gt;&lt;p&gt;This note records the core design, API responsibilities, async processing pipeline, and practical considerations of the &lt;code&gt;Resume&lt;/code&gt; module in the &lt;code&gt;interview-guide&lt;/code&gt; project.&lt;/p&gt;
&lt;h2 id="module-capabilities"&gt;&lt;a href="#module-capabilities" class="header-anchor"&gt;&lt;/a&gt;Module Capabilities
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Multi-format parsing: supports &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;, and &lt;code&gt;MD&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Async processing: uses &lt;code&gt;Redis Stream&lt;/code&gt; for asynchronous resume analysis with status tracking.&lt;/li&gt;
&lt;li&gt;Stability: built-in auto-retry on analysis failure (up to 3 times) + duplicate detection based on file hash.&lt;/li&gt;
&lt;li&gt;Report export: supports one-click export of AI analysis results as a structured PDF report.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="core-status-flow"&gt;&lt;a href="#core-status-flow" class="header-anchor"&gt;&lt;/a&gt;Core Status Flow
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
A["Call /api/resumes/upload"] --&gt; B["Validate file and type"]
B --&gt; C{"Is duplicate resume?"}

C --&gt;|Yes| D["Return historical result or status (duplicate=true)"]
C --&gt;|No| E["Parse text + upload object storage + save ResumeEntity"]

E --&gt; F["Set analyzeStatus = PENDING"]
F --&gt; G["Send Redis Stream analyze task"]

G --&gt; H{"Task queued successfully?"}
H --&gt;|No| I["Set FAILED (queue failed)"]
H --&gt;|Yes| J["Consumer pulls task"]

J --&gt; K["Set PROCESSING"]
K --&gt; L["Call ResumeGradingService for AI analysis"]

L --&gt; M{"Any exception in this round?"}
M --&gt;|No| N["Save analysis result"]
N --&gt; O["Set COMPLETED"]

M --&gt;|Yes| P{"retryCount &lt; 3 ?"}
P --&gt;|Yes| Q["retryCount + 1, requeue task"]
Q --&gt; J
P --&gt;|No| R["Set FAILED (final failure)"]

S["Manual retry /api/resumes/{id}/reanalyze"] --&gt; T["Set PENDING and requeue"]
T --&gt; J&lt;/pre&gt;&lt;h2 id="key-api-design"&gt;&lt;a href="#key-api-design" class="header-anchor"&gt;&lt;/a&gt;Key API Design
&lt;/h2&gt;&lt;h3 id="apiresumesupload-upload-resume-async-analysis"&gt;&lt;a href="#apiresumesupload-upload-resume-async-analysis" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/upload&lt;/code&gt; Upload Resume (Async Analysis)
&lt;/h3&gt;&lt;p&gt;Rate limit strategy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Global limit: &lt;code&gt;@RateLimit(dimension = RateLimit.Dimension.GLOBAL, count = 5)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IP limit: &lt;code&gt;@RateLimit(dimension = RateLimit.Dimension.IP, count = 5)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Entry call:&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;Processing flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Basic file validation&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;Resume&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;Includes: null check, file size limit, and logging.
2. File type detection&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;Supports: &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. Duplicate file detection&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;Internal flow:&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;Resume parsing and text cleaning&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;Parse to plain text using &lt;code&gt;Apache Tika&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textCleaningService.cleanText(content)&lt;/code&gt; to reduce excessive line breaks and token usage&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="5"&gt;
&lt;li&gt;File storage (unstructured data)&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;Uploads to &lt;code&gt;RustFS/MinIO&lt;/code&gt; for unstructured file storage.
6. Metadata persistence&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;Send async analysis task&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;Uses &lt;code&gt;Redis Stream&lt;/code&gt; as the message queue
8. Return upload response&lt;br&gt;
Frontend checks subsequent APIs for async processing status.&lt;/p&gt;
&lt;h3 id="apiresumes-get-resume-list"&gt;&lt;a href="#apiresumes-get-resume-list" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes&lt;/code&gt; Get Resume List
&lt;/h3&gt;&lt;p&gt;Call chain:&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;Current issue:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User-level isolation is not implemented yet, so it currently returns the full list.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="apiresumesiddetail-get-resume-detail"&gt;&lt;a href="#apiresumesiddetail-get-resume-detail" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/{id}/detail&lt;/code&gt; Get Resume Detail
&lt;/h3&gt;&lt;p&gt;Call chain:&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-export-analysis-report-as-pdf"&gt;&lt;a href="#apiresumesidexport-export-analysis-report-as-pdf" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/{id}/export&lt;/code&gt; Export Analysis Report as PDF
&lt;/h3&gt;&lt;p&gt;Call chain:&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-delete-resume"&gt;&lt;a href="#apiresumesid-delete-resume" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/{id}&lt;/code&gt; Delete Resume
&lt;/h3&gt;&lt;p&gt;Call chain:&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-reanalyze-resume"&gt;&lt;a href="#apiresumesidreanalyze-reanalyze-resume" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/{id}/reanalyze&lt;/code&gt; Reanalyze Resume
&lt;/h3&gt;&lt;p&gt;Rate limit strategy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Global limit: &lt;code&gt;@RateLimit(dimension = RateLimit.Dimension.GLOBAL, count = 2)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IP limit: &lt;code&gt;@RateLimit(dimension = RateLimit.Dimension.IP, count = 2)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call chain:&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;Then update and persist status in the processing step.&lt;/p&gt;
&lt;h3 id="apiresumeshealth-health-check"&gt;&lt;a href="#apiresumeshealth-health-check" class="header-anchor"&gt;&lt;/a&gt;&lt;code&gt;/api/resumes/health&lt;/code&gt; Health Check
&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;For service liveness checks.&lt;/p&gt;
&lt;h2 id="stability-design-points"&gt;&lt;a href="#stability-design-points" class="header-anchor"&gt;&lt;/a&gt;Stability Design Points
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Async decoupling: upload and analysis are separated to improve responsiveness.&lt;/li&gt;
&lt;li&gt;Auto-retry: failed analysis retries up to 3 times to reduce transient failures.&lt;/li&gt;
&lt;li&gt;Hash-based dedup: &lt;code&gt;SHA-256&lt;/code&gt; content hash avoids repeated analysis of identical files.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary"&gt;&lt;a href="#summary" class="header-anchor"&gt;&lt;/a&gt;Summary
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;Resume&lt;/code&gt; module already forms a complete loop: upload, parse, async analyze, export, and delete. The current implementation is stable enough for iterative feature expansion and production hardening.&lt;/p&gt;</description></item></channel></rss>