24 KiB
gaokao-site 故障排查模式
2026-06-20 新增,来自合并分数定位图+API 500错误+裸JS代码修复 2026-06-20 更新:追加学校类型/标签排查模式
模式 A:API 返回 500 错误
症状:前端功能(如录取定位推荐→图谱)点击后不显示,浏览器控制台有 500 错误。
排查流程:
- 复现请求:SSH 后用
curl直接调接口验证 - 查错误日志:
/tmp/gaokao_error.log(gunicorn error-logfile 配置),或/www/wwwroot/gaokao/gaokao.log - 根因模式:本项目最常见 NameError 陷阱:
- 变量名不一致:代码中同时存在
bao和bao_shallow/bao_deep两组变量,是"浅保/深保"拆分引入时未统一更新所有引用 - 多处传播:
prob_calc、_calc_prob_data、prob_detail三处函数都有同样模式,修了一处别处可能还坏着
- 变量名不一致:代码中同时存在
- 修复后验证:重启 gunicorn + curl 验证 JSON 正常返回
curl 验证:
curl -s -X POST http://localhost:8080/api/prob \
-H 'Content-Type: application/json' \
-d '{"score":520,"subject":"物理类","major":"不限","region":"不限"}'
curl -s -X POST http://localhost:8080/api/prob/detail \
-H 'Content-Type: application/json' \
-d '{"score":520,"subject":"物理类"}'
模式 B:页面底部出现裸 JS 代码
症状:页面底部(或其他位置)显示 JavaScript 源码文本,被浏览器当 HTML 渲染。
根因:<script> 标签被提前闭合,其后的 JS 代码暴露在 script 标签外 → 浏览器解释为纯文本。
排查:
# 查看 page source 中 script 标签的数量是否匹配
grep -n '<script>\|</script>' /www/wwwroot/gaokao/index.html | head -30
模式 C:功能改名后引用遗漏
改任何 UI 文本时必须全局搜索所有 .html 文件。
检查范围:所有 .html 文件,不只是 index.html:
grep -rn '旧名称' /www/wwwroot/gaokao/*.html
模式 D:API 返回字段名与前端读取字段不一致
症状:ECharts 图表中名称显示为空、undefined、或英文。
排查流程:
- curl 调 API,检查实际返回的字段名
- 查看前端渲染函数中
.map/.forEach读取的字段名 - 确认前后端字段名一致
模式 E:AI对话/plan.html 偏好不生效
症状:用户选了地区、专业、风险偏好,但推荐结果没变化。
排查流程:
- 确认缓存:
redis-cli keys "aichat:*"— 检查 key 是否含偏好后缀 - 确认传递:POST body 是否含 region/risk/major
- 确认后端接收:路由是否读取参数
- 确认函数签名:
_calc_prob_data/recommend是否有 risk 参数
模式 F:major/region 过滤遗漏学校
症状:选广东只显示少量学校,或选计算机但看到师范学校。
根因:majors 表 2025 年只有 820/1220 所学校。原代码的 missing_from_majors & loc_schools 是永远空集的逻辑错误。
修复方案:用 school_basic.json (2859 所) 的 province 和 recommended_majors 字段补充过滤。
模式 G:视觉重复的页脚/底部元素
症状:页面底部出现两个看起来像页脚的区块。
根因:法律声明横幅(独立全宽 <div>)紧贴 Footer 上方,视觉上完全像另一个页脚。
修复:将法律声明链接合并到 footer-bottom 内,删除独立横幅 HTML。
模式 H:AI推荐数量与实际描述数不匹配
症状:AI 对话中承诺推荐 X 所学校,但实际只介绍了 Y 所,后续院校直接断掉。
根因:数据库推荐上限数值过大,AI 给每所院校做详尽分析,内容量远超 AI 一次回复的能力上限。
关键陷阱:上限分三层,必须全改:
| 层 | 位置 | 检查命令 |
|---|---|---|
后端 recommend() |
gaokao_chat_server_v2.py |
grep -n '\[:|n=' gaokao_chat_server_v2.py | grep -v 'import|date|time|slice' |
后端 _calc_prob_data() |
gaokao_chat_server_v2.py |
同上 |
| 前端 plan.html 切片 | plan.html |
grep -n 'slice(0,' plan.html | grep -v 'slice(0,200)' |
| 前端 prompt 描述文字 | plan.html |
grep -n '建议.*所' plan.html |
模式 I:偏好筛选不生效("选什么都一样")
症状:plan.html 第二页选不同专业/地区,返回结果完全相同。
详细文档:references/preference-filtering.md
模式 J:学校类型判定错误
症状:plan.html 表格/导出 TXT 中所有学校显示为「公办」,或独立学院被标记为【公办】,或独立学院继承了985/211标签。
排查流程:
# 1. 确认 API 返回的 school_type 和 tags
curl -s http://127.0.0.1:8080/api/prob -X POST -H 'Content-Type: application/json' \
-d '{"score":517,"subject":"物理类"}' | python3 -c '
import json,sys; d=json.load(sys.stdin)
# 检查第一个学校是否有 school_type
for s in ["chong","wen","bao_shallow","bao_deep"]:
items = d.get(s,[])
if items: print(f"{s}: has school_type={items[0].get(\"school_type\",\"MISSING\")}"); break
# 检查独立学院
for s in ["bao_deep"]:
for i in d.get(s,[]):
if "嘉庚" in i.get("school",""):
print(f'独立学院 {i[\"school\"]}: type={i.get(\"school_type\",\"?\")} tags={i.get(\"tags\",\"?\")}')
'
已知根因:
/api/prob返回 items 无school_type字段 → plan.html 默认显示「公办」convert()中未正确检测独立学院(只用 tags,没用 description)get_school_tags()模糊匹配让独立学院继承母校标签
修复参考:详见 references/school-type-pipeline.md
模式 K:部署后旧代码仍在运行
症状:修改代码后重启服务,但表现和改之前一样。
排查链条:
gunicorn HUP 信号经常失效— 需完整 kill+restart__pycache__ 残留旧 .pyc— 必须删除重复文件— 下划线和连字符两个版本,可能改错文件Redis 缓存— 旧回复仍在缓存中
pkill -9 -f gunicorn
rm -rf /www/wwwroot/gaokao/__pycache__
cd /www/wwwroot/gaokao
export DEEPSEEK_API_KEY='sk-b1212066094d4e319784f23d5b2c6bbd'
nohup /www/server/panel/pyenv/bin/gunicorn --workers 2 --bind 127.0.0.1:8080 --timeout 180 \
--access-logfile /tmp/gaokao_access.log --error-logfile /tmp/gaokao_error.log \
gaokao_chat_server_v2:app > /dev/null 2>&1 &
sleep 3
curl -s http://localhost:8080/health
redis-cli KEYS 'aichat:*' | xargs -r redis-cli DEL
常见陷阱:
__pycache__/*.cpython-37.pyc残留旧 .pyc- 目录下有
gaokao_chat_server_v2.py(下划线)和gaokao-chat-server-v2.py(连字符)两个文件 - Redis 缓存的旧回复不会被重启清除 — 用户会提醒你「缓存没清」。改完推荐逻辑/AI prompt 后必须手动清缓存
# 标准部署三步:scp → restart → clear cache
sshpass -p 'xue.2538' scp gaokao_chat_server_v2.py root@192.144.179.11:/www/wwwroot/gaokao/
sshpass -p 'xue.2538' ssh root@192.144.179.11 "pkill -f gunicorn && sleep 2 && cd /www/wwwroot/gaokao && nohup /www/server/panel/pyenv/bin/gunicorn ... &"
sshpass -p 'xue.2538' ssh root@192.144.179.11 "redis-cli keys 'aichat:*' | xargs -r redis-cli DEL"
# 再验证
sshpass -p 'xue.2538' ssh root@192.144.179.11 "curl -s http://127.0.0.1:8080/health"
# 最后推送到 Gitea(用户会问"代码推送到gitea了么?")
cd ~/mc/gaokao-site && git add ... && git commit -m "..." && git push origin master
模式 L:AI 分析截断(回复不完整)
症状:AI 对话/志愿草表分析只显示了部分学校(如只写了冲的8所,稳写一半就断了),浅保/深保完全没写。
根因:call_deepseek() 中 max_tokens=6000 太小。42所学校每所需100-200 token分析 → 总输出约5000-10000 token。
排查:
grep 'max_tokens' /www/wwwroot/gaokao/gaokao_chat_server_v2.py
修复:增大 max_tokens 到 20000:
"max_tokens": 20000,
验证:curl 调 /api/prob 后,观察 AI 回复长度是否完整覆盖所有学校。
陷阱:只改 max_tokens 不够时,还需检查 DeepSeek model 是否支持大输出(deepseek-v4-flash 支持 32K context)。
模式 M:跨梯队学校重复(同校出现在冲+稳)
症状:同一所学校出现在多个梯队(如河南农业大学在冲志愿第1和稳志愿第2都有),但计划类型相同(都是普通类),用户混淆。
根因:schools 表中同一学校有多个 plan_type+subject_req 组合,分数不同。比如河南农业大学 普通类 再选化学有522分和516分两个记录 → 522在冲范围,516在稳范围。
排查:
sqlite3 /www/wwwroot/gaokao/gaokao_henan.db 'SELECT plan_type, min_score, subject_req FROM schools WHERE school_name="河南农业大学" AND year=2025 AND subject="物理类" ORDER BY min_score'
修复:在 _calc_prob_data() 返回值前加跨梯队去重逻辑:
- 遍历梯队从高到低(冲→稳→浅保→深保)
- 记录已出现的学校名
- 低梯队中已出现过的学校自动跳过
- 去重后梯队数量下降 → 必须从原始池补充新学校填充
验证:
curl -s http://localhost:8080/api/prob -X POST -H 'Content-Type: application/json' \
-d '{"score":517,"subject":"物理类","major":"不限","region":"不限","risk":"冲","mode":"学校优先"}' | python3 -c '
import json,sys; d=json.load(sys.stdin)
schools=[]
for tn in ["chong","wen","bao_shallow","bao_deep"]:
schools.extend([s.get("学校","") for s in d.get(tn,[])])
from collections import Counter
dups=[(s,c) for s,c in Counter(schools).items() if c>1]
print(f"重复: {dups if dups else chr(10004)}")
'
模式 N:深保全是公办,无民办/独立学院
症状:深保推荐的所有学校都标注「公办」,没有民办或独立学院。
根因:默认 top() 排序按质量分取离分数最近的学校,公办学校质量分普遍高于民办。
修复:在深保候选池中扫描民办/独立学院,强制注入至少2所:
- 扫描 deep保的原始候选池,找 school_type='民办' 或 tuition_category 含"独立学院"的学校
- 如果当前深保选中列表民办数 < 2,从池中替换
- 替换策略:移除质量最低的公办 → 加入质量最高的民办
数据验证:
sqlite3 /www/wwwroot/gaokao/gaokao_henan.db 'SELECT school_name, min_score, plan_type FROM schools WHERE year=2025 AND subject="物理类" AND batch="本科批" AND min_score BETWEEN 400 AND 490 AND tuition LIKE "%独立学院%" ORDER BY min_score DESC LIMIT 10'
模式 O:测试端点选错导致误判
症状:用 /api/recommend 测试得到6+6+0+0结果,但前端显示42+学校。
根因:有两个推荐端点,参数名不同:
| 端点 | 前端使用 | 科类参数名 |
|---|---|---|
/api/prob |
✅ 志愿草表 plan.html | subject |
/api/recommend |
❌ 未使用 | category |
规则:永远用前端实际调用的端点测试。前端调 /api/prob 就不用 /api/recommend 验证。
模式 P:推荐逻辑改完后 Redis 缓存命中旧结果
症状:改了推荐算法(阈值、排序、去重逻辑),重启服务后结果还是老的。
修复:
redis-cli KEYS 'rec_cache:*' | xargs -r redis-cli DEL
redis-cli KEYS 'aichat:*' | xargs -r redis-cli DEL
模式 Q:去重后梯队数量不足目标
症状:跨梯队去重后,稳/浅保/深保数量远低于目标。
修复:去重后加补新学校逻辑:记录去重前原始列表,从原始列表中选尚未在更高梯队出现的学校补充,按质量分排序。
模式 R:学校类型判定——数据库优先策略
症状:plan.html 表格中学校类型误标(公办标民办/民办标公办),尤其是:
- 山西农业大学等 type=农林 的学校被标为民办(正则误配历史文本"创办的私立铭贤学堂")
- 武汉东湖学院等实际民办但 tags 错标"双一流"的学校被标为公办
根因:_determine_school_type() 只用 description 正则分析,存在两个问题:
- 正则误匹配历史语境(如"1907年孔祥熙创办的私立铭贤学堂" →
创办的.{0,30}私立命中) - school_basic.json 的
tags字段可能错误(3所民办学校错标"双一流")
正确方案:数据库优先——majors 表的 school_type 字段是原始数据源,覆盖 2302 所学校,准确率 100%。description 分析只作为后备。
实现方式:
# 模块级缓存
_SCHOOL_TYPE_CACHE = {}
# 在 _calc_prob_data 开头加载
if not _SCHOOL_TYPE_CACHE:
rows = conn.execute(
"SELECT school_name, school_type FROM majors WHERE school_type IS NOT NULL AND school_type != '' GROUP BY school_name"
).fetchall()
for row in rows:
_SCHOOL_TYPE_CACHE[row[0]] = row[1]
# 在 with_prob() 和 convert() 中使用
'school_type': _SCHOOL_TYPE_CACHE.get(it['school']) or _determine_school_type(...)
数据质量修复:school_basic.json 中 tags 错标需手动修正:
- 武汉东湖学院: [双一流] → [民办]
- 河北科技学院: [双一流] → [民办]
- 齐鲁理工学院: [双一流] → [民办]
排查:
# 查某校在 majors 表中的类型
sqlite3 /www/wwwroot/gaokao/gaokao_henan.db "SELECT school_name, school_type FROM majors WHERE school_name='山西农业大学' AND school_type IS NOT NULL GROUP BY school_name"
# 查 school_basic.json 中的 tags
python3 -c "import json; d=json.load(open('/www/wwwroot/gaokao/school_basic.json')); print(d.get('武汉东湖学院',{}).get('tags',''))"
模式 S:2024 年回退查询 batch 命名不匹配
症状:AI 回复显示"暂无分专业录取数据",但数据库中实际有该校的 majors 数据。
根因:2025 年新高考使用 batch='本科批',但 2024 年旧高考使用 batch='本科一批'/本科二批'。_get_recommended_majors_for_student() 的回退查询写死了 batch=? 传 '本科批',导致 2024 年数据查不到。
修复:回退查询用 OR 覆盖所有可能 batch 名:
rows = conn.execute(
"SELECT ... FROM majors WHERE school_name=? AND year=2024 AND subject='理科' AND "
"(batch='本科一批' OR batch='本科二批' OR batch='本科批') "
"ORDER BY min_score DESC LIMIT 10",
(school_name,)).fetchall()
排查:
# 查某校 2024 年的 batch 名
sqlite3 /www/wwwroot/gaokao/gaokao_henan.db "SELECT DISTINCT batch FROM majors WHERE school_name='河南科技大学' AND year=2024 AND subject='理科'"
模式 U:专业推荐排序——分层限流 + 二级排序
症状:学校推荐中有大量专业,但 AI 只看到了一端的专业(全是保底或全是冲刺),看不到另一端的可选范围。
根因:_get_recommended_majors_for_student() 的 LIMIT 10 是全局截断——如果学校有 300+ 专业且保底层占了前 10 条,冲刺层一条都看不到。
修复:分层排序:SQL 用 CASE WHEN 分两层,各层内按规则排序:
# 保底层(≤考生分)先出,冲刺层(>考生分)后出
ORDER BY CASE WHEN min_score <= ? THEN 0 ELSE 1 END, [层内排序]
修复:分层限流:SQL 取 30 条,Python 层切保底 5 条 + 冲刺 5 条:
_safe = [m for m in result if m['tag'] == '可报'][:5]
_stretch = [m for m in result if m['tag'] in ('冲刺', '不稳')][:5]
return _safe + _stretch, year_used
修复:二级排序:同分专业按 major ASC 稳定排序,避免数据库物理顺序导致结果抖动:
-- 学校优先:层内 min_score ASC, major ASC
-- 专业优先/均衡:层内 ABS(min_score - ?) ASC, major ASC
验证:
# 东北林业大学(300+专业)
sqlite3 /www/wwwroot/gaokao/gaokao_henan.db "SELECT major, min_score FROM majors WHERE school_name='东北林业大学' AND year=2025 AND subject='物理类' AND batch='本科批' AND min_score IS NOT NULL ORDER BY CASE WHEN min_score <= 517 THEN 0 ELSE 1 END, min_score ASC, major ASC LIMIT 10"
# 应输出: 保底3条(434/459/515) + 冲刺5条(562-585)
覆盖范围:_get_recommended_majors_for_student 中有6个 SQL(2025物理优先+2024理科回退+2024文科回退 × 学校优先+其他模式),每个 ORDER BY 和 LIMIT 都要同步改。
模式 V:AI 回复 token 上限不足 / 上下文截断
症状:AI 只分析了部分学校,或分析到一半截断。
根因:有三层截断——max_tokens、_major_lines 截断、上下文总长度。只改一个不够。
修复(五层同步):
call_deepseek()中:max_tokens: 20000 → 32000call_deepseek()中:timeout=150 → timeout=300- nginx:
proxy_read_timeout 150s → 360s - gunicorn:
--timeout 180 → --timeout 360 _major_lines[:120] → _major_lines[:300](format_recommend 中,每校展示6个专业时24校×7行=168行,120不够)
模式 W:DeepSeek 忽略上下文中的 2024 年专业数据
症状:_get_recommended_majors_for_student 能正常返回 2024 年的专业数据(含分数/位次),数据也在上下文中标注了 (2024年数据),但 AI 分析仍写"暂无分专业录取数据"或"数据中未提供具体专业录取分"。
根因:DeepSeek 对其训练知识过于自信(尤其是清华/北大/上交等顶尖学校),会优先使用自身知识覆盖上下文数据,即便上下文中有明确标注。
修复方案:
- prompt 标注
(最高优先级):DeepSeek 对标注了"最高优先级"的规则会强制遵循,无视自身知识覆盖。新加规则:
★ 数据年份说明(最高优先级):如果某校仅有2024年专业录取数据,**必须使用该2024年数据的分数、位次进行专业推荐分析**,不得因为数据是2024年就说"数据中未提供具体专业录取分"
-
强化数据段标题:将标题从
【各院校专业录取数据(用于推荐专业+分数匹配分析)】改为【各院校专业录取数据(必须逐校提取用于推荐分析,含2024年数据)】 -
注意事项:即使加了最高优先级标签,个别学校的 AI 行为仍不可控(上海交大仍有概率忽略数据)。清华和北大已正常。
模式 X:同校同计划类型在同梯队重复
症状:同一个学校+同一个计划类型在同一个梯队中出现两次(如深保中有两个"南昌医学院 普通类",仅分数不同)。
根因:_calc_prob_data() 中 with_entry_points() 按学校名分组去重,但后面的民办补充逻辑(lines 1598-1616)从原始 bao_deep 取数据时绕过了 with_entry_points 的去重。
修复:在 _calc_prob_data() return 前加最终去重层:
def _dedup_tier(items):
seen = set()
result = []
for it in items:
key = (it.get('school'), it.get('plan_type', '普通类'))
if key not in seen:
seen.add(key)
result.append(it)
return result
chong_w = _dedup_tier(chong_w)
wen_w = _dedup_tier(wen_w)
bao_shallow_w = _dedup_tier(bao_shallow_w)
bao_deep_w = _dedup_tier(bao_deep_w)
模式 AA:schools 表 rows_dict 去重丢失低分入口(同校多 plan_type)
症状:低分段(如 180/190 分)推荐结果极少,某校在数据库实际有低分计划类型(如较高收费专业 185 分)但未出现在任何梯队中。
根因:_calc_prob_data() 中将 schools 表查询结果存入 rows_dict 时,key 是 school_name。同一学校有多个 plan_type(如普通类 222 分 + 较高收费专业 185 分)时,ORDER BY min_score DESC 下高分行先入 dict,低分行因 key 重复被跳过 → 最终只保留最高分。
示例:鹤壁职业技术学院 2024 年有两条数据:
- 普通类 min_score=222
- 较高收费专业 min_score=185
rows_dict['鹤壁职业技术学院'] 最终 = 222(第一条 222 先入 → 第二条 185 因 if r[0] not in rows_dict 已存在被跳过)
对于考生 190 分:gap=222-190=32 → 不在任何梯队范围 → 学校完全消失。
修复:key 改为 (school_name, plan_type) 元组,保留所有计划类型:
# 2025 年数据
for r in cur.fetchall():
rows_dict[(r[0], r[5])] = r # (school_name, plan_type) -> row
# 2024 年回退
for r in cur2.fetchall():
key = (r[0], r[5])
if key not in rows_dict:
rows_dict[key] = r
下游影响:rows = list(rows_dict.values()) 现在包含更多行(同校不同 plan_type 各占一行)。下游 with_entry_points() 会按学校分组取离考生分数最近的入口,正确展示学校。
排查:
# 查某校在 schools 表中有哪些 plan_type
sqlite3 /www/wwwroot/gaokao/gaokao_henan.db "SELECT school_name, plan_type, min_score FROM schools WHERE school_name LIKE '%鹤壁%' AND year=2024 AND subject='理科' ORDER BY min_score"
# 验证修复后该校正确定位到梯队
curl -s -X POST http://127.0.0.1:8080/api/prob -H 'Content-Type: application/json' -d '{"score":190,"subject":"物理类","region":"河南"}' | python3 -c 'import json,sys;d=json.load(sys.stdin);[print(f"{t}: {[(x.get(\"学校\",\"\"),x.get(\"分数\",\"\"),x.get(\"计划类型\",\"\")) for x in d.get(t,[]) if "鹤壁" in x.get("学校","")]}") for t in ["chong","wen","bao_shallow","bao_deep"]]'
同步检查:修复此模式后,用 grep -n 'rows_dict\[r\[0\]\] = r\|rows_dict\[r\[0\]\]' 确认代码中无其他同名重复 pattern。
模式 Y:format_recommend 中同校不同 plan_type 展示遗漏
症状:同一学校同时有"普通类"和"中外合作办学"两种计划类型出现在推荐中,但专业录取数据只展示了一个计划类型的。
根因:format_recommend 中按学校名去重(if _sn in _all_schools: continue),同一学校的第二个计划类型被跳过。
修复:去重键改为 (校名, 计划类型):
_key_dup = (_sn, _it.get('计划类型', '普通类'))
if _key_dup in _seen_schools: continue
_seen_schools.add(_key_dup)
同时在展示时标注计划类型后缀 [中外合作办学]。
模式 Z:部署后遗留问题检查清单
改完代码部署后,按以下顺序检查:
# 1. 语法检查
ssh root@192.144.179.11 "python3 -c 'import py_compile; py_compile.compile(\"/www/wwwroot/gaokao/gaokao_chat_server_v2.py\", doraise=True)'"
# 2. 重启 gunicorn(HUP 可能失效,用 pkill -f 确保完全重启)
ssh root@192.144.179.11 "pkill -f gunicorn && sleep 2 && cd /www/wwwroot/gaokao && DEEPSEEK_API_KEY=... nohup ... &"
# 3. 清缓存(**必须做**,否则旧回复一直命中)
ssh root@192.144.179.11 "redis-cli keys 'aichat:*' | xargs -r redis-cli del && redis-cli keys 'rec_cache:*' | xargs -r redis-cli del"
# 4. 验证 API
curl -s -X POST http://127.0.0.1:8080/api/prob -H 'Content-Type: application/json' -d '{"score":517,"subject":"物理类"}'
# 5. 推送到 Gitea(服务器无法访问内网 Gitea,必须从本地推送)
cd ~/mc/gaokao-site && git add . && git commit -m "vX.X: 描述" && git push
# 6. 人肉测试:打开 plan.html 跑一个分数验证
模式 T:diff 字段含义——批次线差非考生线差
症状:AI 正文中「线差95分」明显错误(实际应为+5分:522-517=5)。
根因:schools 表的 batch_diff 列存储的是学校投档分与本科线的差值(522-427=95),不是与考生分数的差值。_calc_prob_data 将其读为 diff 字段,format_recommend 又直接输出为「线差」,AI 按字面意思理解为考生线差。
修复:在 format_recommend 中,用实际计算的 score_gap 替换 it['线差']:
score_gap = it['分数'] - r['student_score'] # 这才是正确的考生线差
f" 线差={score_gap}{gap_str}{_life_str}" # 而不是 f" 线差={it['线差']}..."
注意:前端 plan.html 表格中的"分差"是在前端用 school_score - student_score 计算的,不受此问题影响。只有 AI 上下文中的 db_context 受影响。
| 日期 | 范围 | 详见 |
|---|---|---|
| 2026-06-20 | 导航改名、API 500、偏好修复、专业地区筛选 | 本文件下方 |
| 2026-06-20 (2) | 11维分析、专业匹配、排名就业数据 | recommendation-engine.md |
| 2026-06-20 (3) | 学校类型、独立学院、标签继承 | school-type-pipeline.md |