xiaowei-system/skills/devops/devops-umbrella/references/gaokao-site/debug-patterns.md

24 KiB
Raw Blame History

gaokao-site 故障排查模式

2026-06-20 新增,来自合并分数定位图+API 500错误+裸JS代码修复 2026-06-20 更新:追加学校类型/标签排查模式


模式 AAPI 返回 500 错误

症状:前端功能(如录取定位推荐→图谱)点击后不显示,浏览器控制台有 500 错误。

排查流程

  1. 复现请求SSH 后用 curl 直接调接口验证
  2. 查错误日志/tmp/gaokao_error.loggunicorn error-logfile 配置),或 /www/wwwroot/gaokao/gaokao.log
  3. 根因模式:本项目最常见 NameError 陷阱:
    • 变量名不一致:代码中同时存在 baobao_shallow/bao_deep 两组变量,是"浅保/深保"拆分引入时未统一更新所有引用
    • 多处传播prob_calc_calc_prob_dataprob_detail 三处函数都有同样模式,修了一处别处可能还坏着
  4. 修复后验证:重启 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

模式 DAPI 返回字段名与前端读取字段不一致

症状ECharts 图表中名称显示为空、undefined、或英文。

排查流程

  1. curl 调 API检查实际返回的字段名
  2. 查看前端渲染函数中 .map / .forEach 读取的字段名
  3. 确认前后端字段名一致

模式 EAI对话/plan.html 偏好不生效

症状:用户选了地区、专业、风险偏好,但推荐结果没变化。

排查流程

  1. 确认缓存redis-cli keys "aichat:*" — 检查 key 是否含偏好后缀
  2. 确认传递POST body 是否含 region/risk/major
  3. 确认后端接收:路由是否读取参数
  4. 确认函数签名_calc_prob_data / recommend 是否有 risk 参数

模式 Fmajor/region 过滤遗漏学校

症状:选广东只显示少量学校,或选计算机但看到师范学校。

根因majors 表 2025 年只有 820/1220 所学校。原代码的 missing_from_majors & loc_schools 是永远空集的逻辑错误。

修复方案:用 school_basic.json (2859 所) 的 provincerecommended_majors 字段补充过滤。


模式 G视觉重复的页脚/底部元素

症状:页面底部出现两个看起来像页脚的区块。

根因:法律声明横幅(独立全宽 <div>)紧贴 Footer 上方,视觉上完全像另一个页脚。

修复:将法律声明链接合并到 footer-bottom 内,删除独立横幅 HTML。


模式 HAI推荐数量与实际描述数不匹配

症状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\",\"?\")}')
'

已知根因

  1. /api/prob 返回 items 无 school_type 字段 → plan.html 默认显示「公办」
  2. convert() 中未正确检测独立学院(只用 tags没用 description
  3. get_school_tags() 模糊匹配让独立学院继承母校标签

修复参考:详见 references/school-type-pipeline.md


模式 K部署后旧代码仍在运行

症状:修改代码后重启服务,但表现和改之前一样。

排查链条

  1. gunicorn HUP 信号经常失效 — 需完整 kill+restart
  2. __pycache__ 残留旧 .pyc — 必须删除
  3. 重复文件 — 下划线和连字符两个版本,可能改错文件
  4. 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


模式 LAI 分析截断(回复不完整)

症状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() 返回值前加跨梯队去重逻辑:

  1. 遍历梯队从高到低(冲→稳→浅保→深保)
  2. 记录已出现的学校名
  3. 低梯队中已出现过的学校自动跳过
  4. 去重后梯队数量下降 → 必须从原始池补充新学校填充

验证

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所

  1. 扫描 deep保的原始候选池找 school_type='民办' 或 tuition_category 含"独立学院"的学校
  2. 如果当前深保选中列表民办数 < 2从池中替换
  3. 替换策略:移除质量最低的公办 → 加入质量最高的民办

数据验证

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 正则分析,存在两个问题:

  1. 正则误匹配历史语境(如"1907年孔祥熙创办的私立铭贤学堂" → 创办的.{0,30}私立 命中)
  2. 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',''))"

模式 S2024 年回退查询 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个 SQL2025物理优先+2024理科回退+2024文科回退 × 学校优先+其他模式),每个 ORDER BY 和 LIMIT 都要同步改。


模式 VAI 回复 token 上限不足 / 上下文截断

症状AI 只分析了部分学校,或分析到一半截断。

根因有三层截断——max_tokens、_major_lines 截断、上下文总长度。只改一个不够。

修复(五层同步):

  1. call_deepseek() 中:max_tokens: 20000 → 32000
  2. call_deepseek() 中:timeout=150 → timeout=300
  3. nginx: proxy_read_timeout 150s → 360s
  4. gunicorn: --timeout 180 → --timeout 360
  5. _major_lines[:120] → _major_lines[:300]format_recommend 中每校展示6个专业时24校×7行=168行120不够

模式 WDeepSeek 忽略上下文中的 2024 年专业数据

症状_get_recommended_majors_for_student 能正常返回 2024 年的专业数据(含分数/位次),数据也在上下文中标注了 2024年数据,但 AI 分析仍写"暂无分专业录取数据"或"数据中未提供具体专业录取分"。

根因DeepSeek 对其训练知识过于自信(尤其是清华/北大/上交等顶尖学校),会优先使用自身知识覆盖上下文数据,即便上下文中有明确标注。

修复方案

  1. prompt 标注 (最高优先级)DeepSeek 对标注了"最高优先级"的规则会强制遵循,无视自身知识覆盖。新加规则:
★ 数据年份说明最高优先级如果某校仅有2024年专业录取数据**必须使用该2024年数据的分数、位次进行专业推荐分析**不得因为数据是2024年就说"数据中未提供具体专业录取分"
  1. 强化数据段标题:将标题从 【各院校专业录取数据(用于推荐专业+分数匹配分析)】 改为 【各院校专业录取数据必须逐校提取用于推荐分析含2024年数据

  2. 注意事项:即使加了最高优先级标签,个别学校的 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)

模式 AAschools 表 rows_dict 去重丢失低分入口(同校多 plan_type

症状:低分段(如 180/190 分)推荐结果极少,某校在数据库实际有低分计划类型(如较高收费专业 185 分)但未出现在任何梯队中。

根因_calc_prob_data() 中将 schools 表查询结果存入 rows_dictkey 是 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。


模式 Yformat_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. 重启 gunicornHUP 可能失效,用 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 跑一个分数验证

模式 Tdiff 字段含义——批次线差非考生线差

症状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