Commit cca6d880 authored by 李文光's avatar 李文光

docs: 补充项目文档并清理失效启动脚本

- 新增 AGENTS.md(AI 编码助手规范)、docs/project_handoff.md(交接文档)
- 启动统一走 backend/scripts/start_server.py,支持 --with-frontend 构建同步前端;
  删除 backend/package.json 及失效脚本 sync_frontend_static.py / legacy-server.mjs
- 学校/专业/技能词典抽为共享数据 shared/resume-dictionaries.json,
  后端 resume_parser_core.py 与前端 constants.js 均引用同一份
- 更新各 README、.gitignore(忽略 .codegraph/);测试断言改为 Vue 前端资源
parent 2e37711d
......@@ -12,6 +12,12 @@
*.sqlite
*.sqlite3
# ---------- 共享数据(词典等,应提交到 git,勿忽略) ----------
# shared/resume-dictionaries.json 由前后端共同引用,需入库
# ---------- 工具索引 ----------
.codegraph/
# ---------- 日志 ----------
*.log
......
# AGENTS.md — AI 编码助手规范
本文件指导 AI 编码助手在本仓库内工作:改代码前先读,改完必须自检。所有规则以「必须 / 禁止 / 建议」的强制性表述给出。详细背景见 [docs/project_handoff.md](docs/project_handoff.md)[README.md](README.md)
## 1. 项目概述
招聘管理系统(recruit-sys,v0.5.0),覆盖「岗位需求梳理 → JD 生成 → 简历采集/解析 → 候选人评估 → Offer 流程」。三端分离:
- **backend/**:Python 3.11+ / FastAPI / SQLAlchemy 2 / Alembic / SQLite(可切 MySQL)
- **vue-app/**:Vue 3(Composition API)/ Vite / Pinia / Element Plus / SCSS,**无 TypeScript**
- **browser-extension/**:Chrome MV3 扩展(原生 JS),采集 BOSS 直聘/猎聘简历
业务数据以「整包 JSON 状态」为事实源(见 §5.1),不是事件溯源。UI 与注释均为中文。
## 2. 环境与常用命令
**必须使用虚拟环境 `backend/.venv`,禁止直接调用系统 `python`。** 所有命令在仓库根目录执行(前端命令在 `vue-app/`)。
| 用途 | 命令 |
| --- | --- |
| 启动后端 | `backend\.venv\Scripts\python.exe backend/scripts/start_server.py` |
| 后端开发模式(自动重载) | `... start_server.py --reload` |
| 构建+同步前端后启动(生产部署) | `... start_server.py --with-frontend` |
| 后端测试 | `... -m pytest -c backend/pyproject.toml` |
| 数据库迁移 | `... -m alembic -c backend/alembic.ini upgrade head` |
| 简历解析 CLI | `... backend/scripts/parse_resume.py <简历文件>` |
| 前端开发 | `cd vue-app && npm run dev`(http://127.0.0.1:5173,/api 代理到 4177) |
| 前端构建 / 检查 | `npm run build` / `npm run lint` / `npm run format` |
端口约定:后端 **4177**,前端 dev **5173**(Vite 已配代理)。
## 3. 编码规范
### Python(backend/)
- 遵循 [backend/pyproject.toml](backend/pyproject.toml) 的 ruff 配置:`line-length=100``target-version=py311``select=E,F,I,UP,B``ignore=E501,B008`。提交前跑 `ruff check backend/`
- 允许 Python 3.11 语法(`X | None``dict[str, ...]` 等)。
- 包导入一律从根开始:`from backend.app.xxx import yyy`;服务/仓储分层:routers(参数与响应)→ services(业务)→ repositories(持久化)。
- 字符串/JSON 输出保持 UTF-8;序列化用 `backend.app.repositories.json_utils.dump_json``ensure_ascii=False`)。
### 前端(vue-app/)
- 遵循 [.prettierrc.json](vue-app/.prettierrc.json)**无分号、单引号、printWidth 120、trailingComma es5、LF 换行**
- Vue 3 用 `<script setup>` + Composition API;不引入 TypeScript。
- 分层:`views/`(页面)→ `components/`(可复用)→ `stores/`(状态+动作)→ `utils/`(纯业务逻辑,不调 API)→ `api/`(HTTP)。
- 字段命名 **camelCase**`jobId``matchScore``jdStatus`…);UI 文案中文。
## 4. 修改必须遵守的架构规则
### 4.1 状态同步是「整包覆盖」
`PUT /api/state` 会先**清空全部业务表再重写**[state_repository.py 的 replace_state](backend/app/repositories/state_repository.py))。新增/修改业务实体时:**models.py(表)与 state_repository.py(读写映射)必须同步改**;前端 `utils/normalize.js``utils/constants.js` 的字段也要对应。禁止绕过此机制另搞一套增量同步。
### 4.2 共享词典只有一份
学校/专业/技能词典唯一数据源是 **`shared/resume-dictionaries.json`**
- 后端:[resume_parser_core.py](backend/app/services/resume_parser_core.py) 启动时加载(`_DICTIONARIES`);
- 前端:[constants.js](vue-app/src/utils/constants.js) `import` 同一文件。
**改词典只允许改共享 JSON**,禁止在任一端代码里内嵌重复列表。
### 4.3 AI 调用前的脱敏是硬性要求
- 候选人信息发往 LLM 前必须脱敏([llm.py 的 sanitize_candidate_for_llm / sanitize_resume_for_llm](backend/app/services/llm.py)):姓名、手机、邮箱、年龄、出生年月等替换为占位符。
- 岗位**对内字段**`sensitive``genderRestriction``ageRestriction``probationPeriod``probationCriteria``nonCompete``internalFeasibility``verification`,即 `llm.py``_INTERNAL_ONLY_JOB_KEYS`**严禁**进入对外 JD 或 LLM prompt;生成 JD 前必须经 `_sanitize_job_for_external` 剥离。
- 能力回退链不可破坏:`generate_jd_draft` = QwenPaw → LLM → 本地结构化;`analyze_resume` = LLM → 本地规则。无 Key 时必须能回退到 `local-structured`,不得 500。
### 4.4 JD 逼问式访谈是无状态状态机
[jd_grill.py](backend/app/services/jd_grill.py):前端持有会话 `state`,每次 POST `{state, answer}` 推进。敏感项只写入 `job.sensitive`。新增问题 = 在 `QUESTION_BANK` 加条目(注意 `skip_when`/`tech_only`),并同步更新 `test_ai.py` 的推进用例。
### 4.5 采集幂等与队列
`/api/ingest` 幂等键 = sha256(source|sourceUrl|pdfUrl|fileHash|name|jobTitle)([ingest_repository.py](backend/app/repositories/ingest_repository.py))。改动采集 payload 字段时注意幂等键是否需扩展;入库后走「待确认队列」→ 前端确认,不要直接改状态。
### 4.6 前端产物目录
`backend/static/` 是构建产物(由 `start_server.py --with-frontend` 生成,已 gitignore)。**禁止手动增删该目录或把它当源码**;页面/资源文件请改 `vue-app/src/` 后重新构建。
### 4.7 浏览器扩展
`browser-extension/` 是原生 JS(MV3),无构建步骤。`reference/` 是外部参考副本,**禁止改动或引用**
## 5. 测试要求
- **改完后端代码必须跑 `backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml`,期望 11 passed**
- 测试必须自包含:`conftest.py` 已隔离(`RECRUITMENT_SKIP_ENV_FILES=1` + 临时 SQLite/目录),**禁止依赖真实 `.env`、网络、LLM Key**
- 新增测试放 `backend/tests/`,用 `client` fixture;改动访谈题单/状态机时同步更新 `test_ai.py`
- `test_static_security.py` 断言 Vue `index.html` 实际引用的 JS 资源——若 `backend/static/` 为空,先 `start_server.py --with-frontend` 生成再跑。
- 前端目前无自动化测试;改 `utils/` 纯逻辑时至少保持现有行为不被破坏(npm run build 能过)。
## 6. 常见陷阱(高频踩坑点)
- **禁止**在 backend 里用 `npm start` / 引用 `backend/package.json`(已删除);启动统一走 `start_server.py`
- **禁止**提交 `.env` / `.env.local`(已 gitignore);`backend/.env` 内有一个内网 LLM Key,别把它写进代码、注释或文档。
- 后端命令一律走 `backend/.venv`,否则可能因缺依赖或版本不符失败。
- 字段命名:前端 camelCase ↔ 后端 DB 列 snake_case;`data` 列存整个实体 JSON(camelCase)——两边别混用。
- 时间统一 ISO 字符串(`utc_now_iso`);ID 用 `前缀-uuid` 形式(job-、cand-、offer-、task-、evt-…)。
-`models.py` 后:本地开发由 `create_all` 自动建表,**不需要**手写迁移;正式迁移需 `alembic revision`(对齐 `0001_initial` 全量建表风格)。
- 文件必须 UTF-8;Windows 环境注意路径分隔(代码内用 pathlib,命令用反斜杠)。
......@@ -28,28 +28,39 @@ recruit-sys/
## 快速开始
### 1. 启动后端
> 本项目后端统一使用虚拟环境 `backend/.venv`(Python 3.11+),所有 Python 命令都在该环境下执行。当前仓库已创建该环境;**尚未创建时按下方「创建虚拟环境」步骤创建**。下文统一用 `backend\.venv\Scripts\python.exe` 直接调用,避免依赖 shell 激活方式;也可以先激活环境再使用 `python`:`backend\.venv\Scripts\Activate.ps1`(PowerShell)或 `backend\.venv\Scripts\activate.bat`(cmd)。
要求:Python 3.11+(首次运行需安装依赖)。
### 1. 启动后端
```powershell
# 项目根目录(D:\Hgny\Py\recruit-sys)
python -m pip install -e backend\[dev]
# 复制配置模板并按需修改(数据库、密钥、LLM Key 等)
# 首次:创建虚拟环境(如尚未创建;要求系统已安装 Python 3.11+)
python -m venv backend\.venv
# 首次:安装依赖 + 复制配置模板并按需修改(数据库、密钥、LLM Key 等)
backend\.venv\Scripts\python.exe -m pip install -e "backend[dev]"
copy backend\.env.example backend\.env
# 启动服务,默认 http://127.0.0.1:4177
python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 4177 --reload
backend\.venv\Scripts\python.exe backend/scripts/start_server.py
```
首次启动会自动创建 `data/`(SQLite 数据库)与 `uploads/`(简历文件)目录,无需手工建表。也可用 Windows 脚本:
首次启动会自动创建 `data/`(SQLite 数据库)与 `uploads/`(简历文件)目录,无需手工建表。也可用 Windows 脚本(已配置为使用 `.venv` 启动)
```powershell
backend\start-server.cmd
```
或进入 `backend/` 执行 `npm start`(会先尝试同步旧前端发布文件,见下文「已知问题」)。
后端启动脚本说明(`backend/scripts/start_server.py`,替代已删除的 `backend/package.json`):
| 命令 | 说明 |
| --- | --- |
| `backend\.venv\Scripts\python.exe backend/scripts/start_server.py` | 仅启动后端(使用 `backend/static` 现有内容) |
| `... start_server.py --with-frontend` | 先 `npm run build` 构建前端并同步到 `backend/static`,再启动后端(生产/演示部署用) |
| `... start_server.py --reload` | 开发模式,后端代码变更自动重载 |
> 前端构建依赖 `vue-app/node_modules`,首次需先 `cd vue-app && npm install`。
### 2. 启动前端(开发模式)
......@@ -65,18 +76,15 @@ Vite 开发服务器默认运行在 `http://127.0.0.1:5173`,已配置 `/api`
### 3. 生产部署(前端 + 后端同源)
构建前端并让 FastAPI 直接托管页面,最终只需运行一个后端进程:
用启动脚本一次完成「构建前端 → 同步到 `backend/static` → 启动后端」,最终只需运行一个后端进程:
```powershell
# 构建前端,产物输出到 vue-app/dist/
cd vue-app
npm run build
# 把 dist/ 内容复制到后端静态目录(FastAPI 挂载在 /)
# 然后只需启动后端,访问 http://127.0.0.1:4177 即可
backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend
```
> 注意:`vue-app/dist/` 目前不会自动同步到 `backend/static/`,需手动复制或接入 CI。API 基地址默认留空(同源),可通过注入全局变量 `window.RECRUITMENT_API_BASE_URL` 指定后端地址。
访问 `http://127.0.0.1:4177` 即可。`/` 会 307 跳转到 `/index.html`(由 FastAPI 从 `backend/static` 托管)。
> `vue-app/dist/` 不会自动同步到 `backend/static/`,需通过上面的 `--with-frontend` 或手动复制。API 基地址默认留空(同源),可通过注入全局变量 `window.RECRUITMENT_API_BASE_URL` 指定后端地址。
### 4. 安装浏览器扩展(可选)
......@@ -133,7 +141,7 @@ npm run build
| `HOST` / `PORT` | `127.0.0.1` / `4177` | 服务监听地址与端口 |
| `DATA_DIR` | `./data` | 运行时数据目录(生产建议放源码目录外) |
| `FILES_DIR` | `./uploads` | 简历等上传文件目录 |
| `DATABASE_URL` | `sqlite:///./data/recruitment.sqlite` | 数据库;可切换 MySQL(`mysql+pymysql://...`,需 `pip install -e backend[mysql]`) |
| `DATABASE_URL` | `sqlite:///./data/recruitment.sqlite` | 数据库;可切换 MySQL(`mysql+pymysql://...`,需 `backend\.venv\Scripts\python.exe -m pip install -e "backend[mysql]"`) |
| `ADMIN_PASSWORD` | 空 | 设置后启用 HTTP Basic 登录(除 ping / 采集接口外) |
| `SESSION_SECRET` | 开发默认值 | 部署时请修改 |
| `INGEST_TOKEN` | 空 | 设置后 `/api/ingest``/api/jd/sync-external``X-Ingest-Token``?token=` |
......@@ -147,27 +155,28 @@ npm run build
项目默认 `create_all` 自动建表;已有 Alembic 迁移支持,需要时执行:
```powershell
python -m alembic -c backend/alembic.ini upgrade head
backend\.venv\Scripts\python.exe -m alembic -c backend/alembic.ini upgrade head
```
## 测试
```powershell
python -m pytest -c backend/pyproject.toml
backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml
```
## 常用命令
```powershell
# 后端(项目根目录)
python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 4177 --reload # 开发
python -m pytest -c backend/pyproject.toml # 测试
python -m alembic -c backend/alembic.ini upgrade head # 迁移
# 后端(项目根目录,全部使用 backend/.venv 环境)
backend\.venv\Scripts\python.exe backend/scripts/start_server.py # 启动后端
backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend # 构建+同步前端后启动(生产部署)
backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml # 测试
backend\.venv\Scripts\python.exe -m alembic -c backend/alembic.ini upgrade head # 迁移
# 简历解析 CLI(不启动服务,直接解析简历文件)
python backend/scripts/parse_resume.py <简历文件>
backend\.venv\Scripts\python.exe backend/scripts/parse_resume.py <简历文件>
# 前端(vue-app/ 目录)
# 前端(vue-app/ 目录,不依赖 Python 环境
npm run dev # 开发服务器 http://127.0.0.1:5173
npm run build # 生产构建到 dist/
npm run preview # 预览生产构建
......@@ -177,8 +186,9 @@ npm run format # Prettier 格式化
## 已知问题
- 后端 `package.json``start` / `dev` / `sync:static` 脚本调用 `backend/scripts/sync_frontend_static.py`,该脚本依赖仓库根目录的旧 `frontend/` 模块;该目录已不存在,脚本现在会报「Required frontend asset is missing」。因此请直接用上文的 `uvicorn` 命令启动,不要依赖 `npm start`。旧脚本保留用于兼容历史流程,可随时清理。
- `backend/static/` 目录当前为空,需要「生产部署(同源托管)」时请按上文手动同步 `vue-app/dist/`
- `backend/static/` 是前端构建产物的同步目录(已 gitignore),初始为空;使用 `start_server.py --with-frontend` 或手动同步 `vue-app/dist/` 后才会提供页面。未同步时 `/` 会跳转但 `/index.html` 返回 404,属正常状态。
- 后端启动统一走 `backend/scripts/start_server.py`;历史 `backend/package.json` 与失效脚本(`sync_frontend_static.py``legacy-server.mjs`)已删除。
- 学校/专业/技能词典为前后端共享的一份数据:`shared/resume-dictionaries.json`(前端经 `vue-app/src/utils/constants.js` 引用,后端经 `backend/app/services/resume_parser_core.py` 加载)。修改词典请改共享文件。
## 更多文档
......
......@@ -4,31 +4,38 @@
## 启动
要求:Python 3.11+。在项目根目录(`recruit-sys/`)执行:
要求:Python 3.11+**后端统一使用虚拟环境 `backend/.venv`**。在项目根目录(`recruit-sys/`)执行:
```powershell
python -m pip install -e backend\[dev]
# 首次:创建虚拟环境(如尚未创建;要求系统已安装 Python 3.11+)
python -m venv backend\.venv
# 首次:安装依赖 + 复制配置模板
backend\.venv\Scripts\python.exe -m pip install -e "backend[dev]"
copy backend\.env.example backend\.env # 首次:复制并按需修改配置
python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 4177 --reload
# 启动后端(默认 http://127.0.0.1:4177)
backend\.venv\Scripts\python.exe backend/scripts/start_server.py
```
- 启动脚本 `backend/scripts/start_server.py` 统一负责启动:
- 默认仅启动后端(使用 `backend/static` 现有内容);
- `--with-frontend`:先 `npm run build` 构建前端并同步到 `backend/static`,再启动(生产/演示部署);
- `--reload`:开发模式,后端代码变更自动重载。
- 激活方式(可选):PowerShell 用 `backend\.venv\Scripts\Activate.ps1`,cmd 用 `backend\.venv\Scripts\activate.bat`;激活后可直接用 `python`
- 首次启动自动创建 `data/`(SQLite 数据库)与 `uploads/`(简历文件)目录,并自动建表。
- 启动后访问 `http://127.0.0.1:4177/`;API 位于 `/api/*`,交互式文档在 `/docs`
- Windows 下也可直接运行 `backend\start-server.cmd`启动并输出日志到 `backend/server.out.log` / `server.err.log`)。
- 启动后访问 `http://127.0.0.1:4177/`(307 跳转 `/index.html`;API 位于 `/api/*`,交互式文档在 `/docs`
- Windows 下也可直接运行 `backend\start-server.cmd`已配置使用 `.venv` + 启动脚本,输出日志到 `backend/server.out.log` / `server.err.log`)。
## 前端发布文件
`static/` 是网页前端发布目录,由 FastAPI 挂载在 `/`当前 `static/` 为空:构建后的 Vue 前端产物在 `vue-app/dist/`,需要同源部署时手动复制过去:
`static/` 是网页前端发布目录,由 FastAPI 挂载在 `/`**默认为空**,使用 `start_server.py --with-frontend` 会自动构建 `vue-app/dist` 并同步到该目录;未同步时 `/index.html` 返回 404,属正常状态。
```powershell
# 构建前端
cd vue-app
npm run build
# 将 dist/ 内容复制到 backend/static/(此后只需运行后端即可访问页面)
backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend
```
> 历史脚本 `scripts/sync_frontend_static.py`(及 `backend/package.json` 的 `start` / `dev` / `sync:static`)依赖仓库根目录的旧 `frontend/` 模块,该目录已不存在,脚本会报「Required frontend asset is missing」,请勿使用;如需删除可自行清理
> 学校/专业/技能词典与前端共享一份数据:`../shared/resume-dictionaries.json`(本模块 `resume_parser_core.py` 启动时加载;前端 `vue-app/src/utils/constants.js` 引用同一文件)。修改词典请改共享文件,保持两端一致
## 配置文件
......@@ -41,9 +48,10 @@ npm run build
## 常用命令
```powershell
# 在项目根目录执行
python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 4177 --reload
python -m pytest -c backend/pyproject.toml
python -m alembic -c backend/alembic.ini upgrade head
python backend/scripts/parse_resume.py <简历文件> # 简历解析 CLI,不启动服务
# 在项目根目录执行(统一使用 backend/.venv)
backend\.venv\Scripts\python.exe backend/scripts/start_server.py # 启动后端
backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend # 构建+同步前端后启动
backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml
backend\.venv\Scripts\python.exe -m alembic -c backend/alembic.ini upgrade head
backend\.venv\Scripts\python.exe backend/scripts/parse_resume.py <简历文件> # 简历解析 CLI,不启动服务
```
......@@ -10,6 +10,18 @@ if hasattr(sys.stdout, "reconfigure"):
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8")
# 学校/专业/技能词典与前端 vue-app/src/utils/constants.js 共享同一份数据
# (shared/resume-dictionaries.json),修改词典请改共享文件,避免两处漂移。
_DICTIONARIES_PATH = Path(__file__).resolve().parents[3] / "shared" / "resume-dictionaries.json"
def _load_dictionaries() -> dict:
with open(_DICTIONARIES_PATH, encoding="utf-8") as file:
return json.load(file)
_DICTIONARIES = _load_dictionaries()
def read_pdf(path: Path) -> str:
from pypdf import PdfReader
......@@ -261,49 +273,18 @@ def infer_source(text: str, filename: str) -> str:
def extract_skills(text: str):
dictionary = [
"电力交易", "电力市场", "现货", "中长期", "售电", "新能源", "储能", "负荷预测",
"交易策略", "电价", "价差", "结算", "风控", "政策研究", "数据分析",
"Python", "SQL", "Excel", "React", "Node", "SaaS", "AI", "产品规划",
"需求分析", "销售策略", "大客户", "团队管理", "招聘", "员工关系"
]
return [skill for skill in dictionary if skill.lower() in text.lower()]
PROJECT_985 = {
"清华大学", "北京大学", "中国人民大学", "北京航空航天大学", "北京理工大学", "中国农业大学", "北京师范大学", "中央民族大学",
"南开大学", "天津大学", "大连理工大学", "东北大学", "吉林大学", "哈尔滨工业大学", "复旦大学", "同济大学", "上海交通大学",
"华东师范大学", "南京大学", "东南大学", "浙江大学", "中国科学技术大学", "厦门大学", "山东大学", "中国海洋大学",
"武汉大学", "华中科技大学", "湖南大学", "中南大学", "国防科技大学", "中山大学", "华南理工大学", "四川大学",
"电子科技大学", "重庆大学", "西安交通大学", "西北工业大学", "西北农林科技大学", "兰州大学"
}
PROJECT_211_EXTRA = {
"北京交通大学", "北京工业大学", "北京科技大学", "北京化工大学", "北京邮电大学", "北京林业大学", "北京中医药大学",
"北京外国语大学", "中国传媒大学", "中央财经大学", "对外经济贸易大学", "北京体育大学", "中央音乐学院",
"华北电力大学", "中国政法大学", "中国矿业大学", "中国石油大学", "中国地质大学", "上海财经大学", "上海大学",
"东华大学", "华东理工大学", "上海外国语大学", "第二军医大学", "苏州大学", "南京航空航天大学", "南京理工大学",
"河海大学", "江南大学", "南京农业大学", "中国药科大学", "南京师范大学", "安徽大学", "合肥工业大学", "福州大学",
"南昌大学", "郑州大学", "武汉理工大学", "华中师范大学", "华中农业大学", "中南财经政法大学", "湖南师范大学",
"暨南大学", "华南师范大学", "广西大学", "海南大学", "西南交通大学", "四川农业大学", "西南财经大学",
"西南大学", "云南大学", "贵州大学", "西藏大学", "西北大学", "西安电子科技大学", "长安大学",
"陕西师范大学", "青海大学", "宁夏大学", "新疆大学", "石河子大学", "内蒙古大学", "辽宁大学", "大连海事大学",
"东北师范大学", "哈尔滨工程大学", "东北林业大学", "东北农业大学", "河北工业大学", "太原理工大学",
"延边大学"
}
INDUSTRY_SCHOOLS = {
"东北电力大学", "上海电力大学", "华北水利水电大学", "长沙理工大学", "三峡大学",
"沈阳化工大学", "沈阳化工学院", "沈阳工业大学", "辽宁石油化工大学", "南京工程学院", "浙江水利水电学院"
}
COMMON_MAJORS = [
"电气工程及其自动化", "机械设计制造及其自动化", "新能源科学与工程", "能源与动力工程",
"数据科学与大数据技术", "计算机科学与技术", "化学工程与工艺", "电子信息工程",
"新能源材料与器件", "能源化学工程", "电力系统及其自动化", "电气工程与智能控制",
"人力资源管理", "信息管理与信息系统", "电气工程", "软件工程", "自动化", "应用化学",
"通信工程", "工商管理", "市场营销", "财务管理", "会计学"
]
return [skill for skill in _SKILL_DICTIONARY if skill.lower() in text.lower()]
PROJECT_985 = set(_DICTIONARIES["schools985"])
PROJECT_211_EXTRA = set(_DICTIONARIES["schools211Extra"])
INDUSTRY_SCHOOLS = set(_DICTIONARIES["industrySchools"])
COMMON_MAJORS = _DICTIONARIES["majors"]
_SKILL_DICTIONARY = _DICTIONARIES["skills"]
def clean_school_candidate(value: str) -> str:
......
{
"name": "recruitment-backend",
"version": "0.5.0",
"description": "招聘系统后端与集成静态前端的命令包装。",
"private": true,
"scripts": {
"start": "cd .. && python backend/scripts/sync_frontend_static.py && python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 4177",
"dev": "cd .. && python backend/scripts/sync_frontend_static.py && python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 4177 --reload",
"migrate": "cd .. && python -m alembic -c backend/alembic.ini upgrade head",
"test": "cd .. && python -m pytest -c backend/pyproject.toml",
"check:parser": "cd .. && python backend/scripts/parse_resume.py --help",
"sync:static": "cd .. && python backend/scripts/sync_frontend_static.py",
"check:static": "cd .. && python backend/scripts/sync_frontend_static.py --check"
}
}
console.error("后端已迁移到 Python + FastAPI。请在项目根目录执行:");
console.error("python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 4177");
process.exit(1);
"""启动招聘系统后端(FastAPI),可选先构建并同步前端产物到 backend/static。
用法(项目根目录执行,推荐使用 backend/.venv 环境):
backend\\.venv\\Scripts\\python.exe backend/scripts/start_server.py # 仅启动后端(不打包前端)
backend\\.venv\\Scripts\\python.exe backend/scripts/start_server.py --with-frontend # 先构建前端并同步到 backend/static,再启动
backend\\.venv\\Scripts\\python.exe backend/scripts/start_server.py --reload # 开发模式,代码变更自动重载
说明:
- 前端构建 = 在 vue-app 下执行 `npm run build`,再把 dist/ 复制到 backend/static/。
- 不加 --with-frontend 时使用 backend/static 现有内容(为空时页面 404,属正常状态)。
- host/port 默认读取 backend 配置(.env 的 HOST/PORT,默认 127.0.0.1:4177)。
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
BACKEND_DIR = PROJECT_ROOT / "backend"
STATIC_DIR = BACKEND_DIR / "static"
VUE_APP_DIR = PROJECT_ROOT / "vue-app"
def _npm_run_build() -> None:
"""在 vue-app 下执行 npm run build(Windows 用 cmd /c 包装 npm)。"""
if os.name == "nt":
command = ["cmd", "/c", "npm", "run", "build"]
else:
command = ["npm", "run", "build"]
subprocess.run(command, cwd=VUE_APP_DIR, check=True)
def build_frontend() -> None:
"""构建前端并把 dist/ 同步到 backend/static/(先清空旧目录)。"""
if not (VUE_APP_DIR / "package.json").is_file():
sys.exit("未找到 vue-app/package.json,无法构建前端。")
print("[start-server] 构建前端(npm run build)...")
try:
_npm_run_build()
except subprocess.CalledProcessError:
sys.exit("前端构建失败(npm run build 非零退出)。请先在 vue-app 下执行 npm install。")
dist = VUE_APP_DIR / "dist"
if not dist.is_dir():
sys.exit("前端构建完成但未生成 vue-app/dist/。")
if STATIC_DIR.exists():
shutil.rmtree(STATIC_DIR)
STATIC_DIR.mkdir(parents=True)
for item in dist.iterdir():
if item.is_dir():
shutil.copytree(item, STATIC_DIR / item.name)
else:
shutil.copy2(item, STATIC_DIR / item.name)
print(f"[start-server] 前端产物已同步到 {STATIC_DIR.relative_to(PROJECT_ROOT)}")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="启动招聘系统后端。")
parser.add_argument(
"--with-frontend",
action="store_true",
help="先构建前端并同步到 backend/static,再启动后端(生产/演示部署用)",
)
parser.add_argument("--host", default=None, help="监听地址(默认读配置 HOST)")
parser.add_argument("--port", type=int, default=None, help="监听端口(默认读配置 PORT)")
parser.add_argument("--reload", action="store_true", help="开发模式:代码变更自动重载")
args = parser.parse_args(argv)
if args.with_frontend:
build_frontend()
os.chdir(PROJECT_ROOT)
from backend.app.config import get_settings
settings = get_settings()
host = args.host or settings.host
port = args.port or settings.port
print(f"[start-server] 启动后端 http://{host}:{port} (reload={args.reload})")
import uvicorn
uvicorn.run(
"backend.app.main:app",
host=host,
port=port,
reload=args.reload,
reload_dirs=[str(BACKEND_DIR)] if args.reload else None,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
"""Publish selected frontend source assets into backend/static."""
from __future__ import annotations
import argparse
import shutil
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
FRONTEND_ROOT = PROJECT_ROOT / "frontend"
STATIC_ROOT = PROJECT_ROOT / "backend" / "static"
ASSET_MAP = {
"app.html": "app.html",
"app.html:index": "index.html",
"app.css": "app.css",
"app.js": "app.js",
"auto-ingest.js": "auto-ingest.js",
"config.js": "config.js",
"prototype.html": "prototype.html",
}
def assets_match() -> bool:
for source_key, target_name in ASSET_MAP.items():
source_name = source_key.split(":", 1)[0]
source = FRONTEND_ROOT / source_name
target = STATIC_ROOT / target_name
if not source.is_file() or not target.is_file() or source.read_bytes() != target.read_bytes():
return False
return True
def sync() -> None:
STATIC_ROOT.mkdir(parents=True, exist_ok=True)
for source_key, target_name in ASSET_MAP.items():
source_name = source_key.split(":", 1)[0]
source = FRONTEND_ROOT / source_name
if not source.is_file():
raise FileNotFoundError(f"Required frontend asset is missing: {source}")
shutil.copy2(source, STATIC_ROOT / target_name)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="同步前端发布文件到 backend/static。")
parser.add_argument("--check", action="store_true", help="只检查静态副本是否与前端源码一致")
args = parser.parse_args(argv)
if args.check:
if assets_match():
print("Frontend static assets are current.")
return 0
print("Frontend static assets are missing or out of date.", file=sys.stderr)
return 1
sync()
print(f"Published frontend assets to {STATIC_ROOT}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@echo off
setlocal
cd /d "%~dp0.."
REM ??????? backend/**/*.py ?????????? backend/static?????????
REM ???????? JD ??? QwenPaw????? QwenPaw ???start-qwenpaw.cmd??
python -m uvicorn backend.app.main:app --host 127.0.0.1 --port 4177 --reload --reload-dir backend >> "backend\server.out.log" 2>> "backend\server.err.log"
REM 启动招聘系统后端(使用 backend/.venv 虚拟环境,Python 3.11)。
REM 通过 backend/scripts/start_server.py 启动:
REM - 默认仅启动后端;需要同时构建/同步前端到 backend/static 时,把下面的
REM --reload 改为 --reload --with-frontend。
REM 依赖:请先在 backend/.venv 中安装依赖:
REM backend\.venv\Scripts\python.exe -m pip install -e "backend[dev]"
REM 日志输出到 backend\server.out.log / backend\server.err.log
set "PYTHON=%~dp0.venv\Scripts\python.exe"
if not exist "%PYTHON%" (
echo [ERROR] 未找到虚拟环境解释器: %PYTHON%
echo 请先创建 backend\.venv 并安装依赖,再运行本脚本。
exit /b 1
)
"%PYTHON%" backend\scripts\start_server.py --reload >> "backend\server.out.log" 2>> "backend\server.err.log"
......@@ -6,7 +6,13 @@ def test_homepage_redirects_to_published_frontend(client):
homepage = client.get("/index.html")
assert homepage.status_code == 200
assert "招聘系统" in homepage.text
assert client.get("/app.js").status_code == 200
# Vue 前端入口:index.html 引用 assets/ 下的 JS/CSS,抽查其中首个 JS 资源可访问
import re
match = re.search(r'src="([^"]+\.js)"', homepage.text) or re.search(r'href="([^"]+\.js)"', homepage.text)
assert match, "index.html 未引用任何 JS 资源"
assert client.get(match.group(1)).status_code == 200
def test_static_host_does_not_expose_sensitive_paths(client):
......
# 招聘系统项目交接文档
- 版本:0.5.0
- 位置:`D:\Hgny\Py\recruit-sys`
- 生成日期:2026-08-31(基于当前代码实际分析)
- 技术栈:FastAPI(Python 3.11+)+ SQLAlchemy 2 / SQLite + Vue 3 / Vite + Chrome MV3 扩展
本文档基于对当前代码的实际阅读与运行验证编写。所有行为描述均可回溯到代码位置(文末列出的文件路径即为代码位置);对未能验证的部分已明确标注「未验证」。
---
## 1. 项目目标
面向招聘 HR / 用人部门的一体化招聘管理工具,覆盖从「一句话需求」到「Offer 审批」的全流程:
1. **岗位需求梳理**:用「逼问式访谈」把模糊的一句话需求一步步逼成结构化岗位字段(定位、硬门槛、命脉、排除信号、入职约束),产出可直接生成 JD 的岗位信息。
2. **JD 生成与迭代**:基于结构化岗位信息生成 / 迭代 JD 草稿(本地结构化规则 → LLM → QwenPaw 智能体三级能力),敏感信息(性别/年龄/试用期/竞业)不落入对外 JD。
3. **简历采集与解析**:浏览器扩展从 BOSS 直聘 / 猎聘采集简历(PDF URL 或页面摘要),后端自动下载、解析(PDF/DOCX/TXT)、字段提取(姓名/电话/邮箱/学校/学历/年限等)。
4. **候选人评估与匹配**:本地规则或 LLM 对候选人做匹配度评分,生成评估报告。
5. **流程管理**:待办、岗位、简历库、Offer 四个模块 + 招聘数据看板。
整体形态是「单机/内网可跑的本地工具」:后端同源托管前端页面,浏览器扩展连本机 4177 端口采集简历。
---
## 2. 当前能力(按代码实测)
### 后端 API(全部位于 `backend/app/routers/`)
| 能力 | 端点 | 代码位置 |
| --- | --- | --- |
| 健康检查 | `GET /api/ping`(含待确认采集数) | [backend/app/routers/health.py](backend/app/routers/health.py) |
| 全量状态读写 | `GET/PUT /api/state``POST /api/import/local-state``GET /api/export/state` | [backend/app/routers/state.py](backend/app/routers/state.py) |
| 简历解析 / 上传 | `POST /api/parse-resume``POST /api/upload-resume``GET /api/resumes/{filename}` | [backend/app/routers/resume.py](backend/app/routers/resume.py) |
| AI 简历分析 | `POST /api/analyze-resume`(LLM 优先,回退本地规则) | [backend/app/routers/ai.py](backend/app/routers/ai.py)[backend/app/services/llm.py](backend/app/services/llm.py) |
| JD 生成 | `POST /api/generate-jd`(QwenPaw → LLM → 本地结构化) | [backend/app/routers/ai.py](backend/app/routers/ai.py)[backend/app/services/llm.py](backend/app/services/llm.py) |
| JD 信息完整度 | `POST /api/jd/completeness` | [backend/app/services/qwenpaw_client.py](backend/app/services/qwenpaw_client.py) |
| 逼问式访谈 | `POST /api/jd/grill/start``POST /api/jd/grill/advance` | [backend/app/services/jd_grill.py](backend/app/services/jd_grill.py) |
| QwenPaw 流式聊天 | `POST /api/qwenpaw/chat`(SSE) | [backend/app/services/qwenpaw_client.py](backend/app/services/qwenpaw_client.py) |
| 对外 JD 同步 | `POST /api/jd/sync-external` | [backend/app/services/external_sync.py](backend/app/services/external_sync.py) |
| 简历采集 | `POST /api/ingest``GET /api/ingest-queue``POST /api/ingest-queue/clear` | [backend/app/routers/ingest.py](backend/app/routers/ingest.py) |
### 简历解析器
- PDF / DOCX / TXT 三种格式;纯规则实现,无 OCR,无 LLM 参与。
- 字段提取:姓名、求职意向、电话、邮箱、出生日期/年龄、工作年限(按时间区间合并计算)、学历、学校(985/211/行业院校表匹配)、专业(词典 + 教育经历块)、城市、来源(Boss/猎聘/智联/内推/官网)、技能(词典命中)。
- 实现:`backend/app/services/resume_parser_core.py`(单文件,约 465 行)。
- 有独立的 CLI 入口:`python backend/scripts/parse_resume.py <文件>`
### AI 能力分级(按 `backend/app/services/llm.py` 实测)
- **配置优先级**`LLM_*` > `DEEPSEEK_*` > `OPENAI_*`(见 [config.py](backend/app/config.py)`effective_*` 属性)。
- **未配置 Key 时**:自动回退「本地结构化」分析/生成(`provider: local-structured`),不报错。
- **敏感信息处理**
- 候选人发送给 LLM 前会本地脱敏(姓名/手机/邮箱/年龄/出生年月 → 占位符),见 [llm.py 的 sanitize_*](backend/app/services/llm.py)
- 岗位对内字段(sensitive/genderRestriction/ageRestriction/试用期/竞业等)在生成对外 JD 前被剥离,见 [llm.py 的 `_sanitize_job_for_external`](backend/app/services/llm.py)
- 逼问式访谈的敏感项(性别/年龄)只写入 `job.sensitive`,见 [jd_grill.py 的 `_apply_answer`](backend/app/services/jd_grill.py)
- **QwenPaw 接入**:默认关闭(`QWENPAW_ENABLED=false`);启用后 JD 生成优先走它,服务不可用/超时回退 LLM/本地(见 [qwenpaw_client.py](backend/app/services/qwenpaw_client.py))。
### 浏览器扩展(`browser-extension/`)
- Manifest V3;在 BOSS 直聘(zhipin.com)和猎聘(liepin.com)页面注入一个可拖拽的柴犬宠物按钮,点击后采集当前简历。
- 采集方式:优先找简历 PDF 下载链接(BOSS 走 `docdownload.zhipin.com` 转换,见 [content.js](browser-extension/content.js)),带 cookie 请求 PDF;取不到则采集页面摘要文本。
- 通过 `chrome.cookies` API 拿 HttpOnly cookie(BOSS 鉴权 cookie 是 HttpOnly),见 [background.js](browser-extension/background.js)
- 提交到 `http://127.0.0.1:4177/api/ingest`(可在弹窗修改地址,存 chrome.storage.local)。
---
## 3. 目录结构
```
recruit-sys/
├── backend/ # FastAPI 后端(Python 包,根目录下以 backend.app.* 导入)
│ ├── app/
│ │ ├── main.py # 应用工厂:CORS、Basic Auth、路由挂载、静态托管、异常处理
│ │ ├── config.py # Settings(.env / .env.local 读取,Pydantic)
│ │ ├── db.py # SQLAlchemy engine/session,create_all 自动建表
│ │ ├── models.py # 14 张表 ORM 模型
│ │ ├── security.py # Basic Auth 中间件 + ingest token 校验
│ │ ├── routers/ # 5 个路由模块(health/state/resume/ai/ingest)
│ │ ├── services/ # llm / jd_grill / qwenpaw_client / resume_parser(_core) /
│ │ │ # file_storage / external_sync
│ │ └── repositories/ # state_repository / ingest_repository / json_utils
│ ├── alembic/ # 迁移(versions/0001_initial.py 全量建表)
│ ├── scripts/ # parse_resume.py(简历解析 CLI)、start_server.py(启动后端,可选构建/同步前端)、start-deepseek.example.ps1(历史示例)
│ ├── tests/ # 5 个测试文件(详见 §6)
│ ├── static/ # 前端发布目录(FastAPI 挂载 /;由 start_server.py --with-frontend 同步,初始为空)
│ ├── pyproject.toml # 依赖 + pytest/ruff 配置
│ ├── requirements.txt # 入口:-e .[dev]
│ ├── .env / .env.example / .env.local
│ └── start-server.cmd # Windows 一键启动(含日志输出)
├── vue-app/ # Vue 3 前端
│ ├── src/
│ │ ├── api/ # axios 实例 + recruitment.js(9 个 API 封装)
│ │ ├── stores/recruitment.js # Pinia store(领域动作、事件回填、任务关联)
│ │ ├── router/ # 路由 + 导航配置(5 个模块)
│ │ ├── layouts/AppShell.vue # 侧边栏 + 顶部栏 + 页面容器
│ │ ├── views/ # 7 个视图(todo/dashboard/jobs 列表+详情+编辑/resumes/offer)
│ │ ├── components/ # common / todo / dashboard / jobs / resumes / offer
│ │ └── utils/ # 纯业务逻辑(匹配、评分、推断、流程等 22 个文件)
│ ├── dist/ # 生产构建产物(当前不在 git 跟踪范围)
│ └── vite.config.js # dev 5173,/api 代理到 4177
└── browser-extension/ # Chrome/Edge MV3 扩展(可直接「加载已解压的扩展程序」)
├── manifest.json # 权限:activeTab/storage/cookies/scripting + zhipin/liepin/本机 4177
├── background.js # service worker:chrome.cookies 获取 HttpOnly cookie
├── content.js # 页面注入(柴犬宠物按钮 + 采集逻辑)
├── inject.js # 主世界脚本(MAIN world,用于拦截 PDF 下载链接)
├── popup.html / popup.js # 弹窗:API 地址配置 + 连接测试
└── reference/ # 外部参考副本(非运行时,已 gitignore)
```
---
## 4. 核心架构
### 4.1 分层
- **路由层**(routers/):只做参数解析与响应包装,业务在 services。
- **服务层**(services/):核心业务。`llm.py` 是 AI 主入口(分析/生成 + 三级回退 + 脱敏);`jd_grill.py` 是无状态访谈状态机(题单 + 推进逻辑);`qwenpaw_client.py` 是 QwenPaw SSE 客户端;`resume_parser_core.py` 是纯规则解析器;`file_storage.py` 是文件落盘;`external_sync.py` 是对外 JD 落库。
- **仓储层**(repositories/):`state_repository.py` 负责「整包状态」与表的双向映射(PUT /api/state 时先全表 delete 再插入,见 `replace_state`[state_repository.py](backend/app/repositories/state_repository.py));`ingest_repository.py` 负责采集幂等与入库。
### 4.2 数据模型与状态同步机制(重点)
- 全库共 14 张表([models.py](backend/app/models.py)):SchemaMigration、AppStateMeta、User、Job、JdVersion、Candidate、CandidateApplication、ResumeFile、ResumeText、ResumeProfile、CandidateMatchReport、Interview、Offer、Task、RecruitmentEvent、IngestItem。
- **核心机制**:业务数据以「整包 JSON 状态」为事实源。`GET /api/state` 把各表读回合并为一个 `{jobs, candidates, offers, tasks, eventLog, updatedAt}` 对象;`PUT /api/state` 则先清空再全量重写。`AppStateMeta.raw_snapshot` 存整包快照,供无数据时回读。
- 这意味着:**这不是增量/事件溯源架构,而是「快照全量替换」**。多端并发写同一库会互相覆盖(last-write-wins)。
- 简历文件走独立存储(uploads/resumes/,hash 命名),与状态快照分离,见 [file_storage.py](backend/app/services/file_storage.py)
- 采集链路(`/api/ingest`[ingest_repository.py](backend/app/repositories/ingest_repository.py)):幂等键 = sha256(source|sourceUrl|pdfUrl|fileHash|name|jobTitle),重复提交返回 `skipped`;每条采集产生 Candidate + ResumeFile + ResumeText + ResumeProfile + RecruitmentEvent + IngestItem(未确认),前端看板通过 `/api/ingest-queue` 确认后进正式列表。
### 4.3 前端架构
- Vue 3 Composition API + Pinia + Vue Router + Element Plus;无 TypeScript,无组件测试。
- **状态同步策略**`stores/recruitment.js` 以 localStorage(key `recruitment-system-mvp-v1`,见 [constants.js](vue-app/src/utils/constants.js))为主事实源,向后端 `/api/state` 全量保存;启动时尝试读取后端状态,两者有冲突时以后端为准(`meta.persisted` 为 true 时用后端)。
- 大量业务逻辑在 `src/utils/`(22 个纯函数模块):匹配打分、字段推断、JD 质量、任务流程、Offer 模板等。规则与后端 `resume_parser_core.py` 的学校/专业/技能词典**是两套独立副本**(前端 `constants.js``project985Schools` 等与后端 [resume_parser_core.py](backend/app/services/resume_parser_core.py) 中集合重复),改一处需同步另一处。
---
## 5. 启动与部署
### 5.1 环境要求
- Python ≥ 3.11(后端依赖 `requires-python = ">=3.11"`
- Node.js ≥ 18(vue-app/package.json engines)
- **后端统一使用虚拟环境 `backend/.venv`(含 Python 3.11 与依赖),所有后端 Python 命令一律走该环境**,不再直接调用系统 python。创建方式见 §5.2 第 1 步。
- 本机测试时用该环境可跑通(见 §6)
### 5.2 首次安装
```powershell
# 项目根目录
cd D:\Hgny\Py\recruit-sys
# ── 第 1 步:创建后端虚拟环境(如尚未创建)──
# 要求系统已安装 Python 3.11+(python --version 确认)
python -m venv backend\.venv
# 结果:backend\.venv\Scripts\python.exe 即该环境解释器。
# 激活方式(可选):PowerShell 用 backend\.venv\Scripts\Activate.ps1,
# cmd 用 backend\.venv\Scripts\activate.bat;激活后可直接用 python。
# .venv 已被 backend/.gitignore 忽略,不会提交到 git。
# ── 第 2 步:安装后端依赖(-e .[dev] 含 pytest/ruff;requirements.txt 只是入口)──
backend\.venv\Scripts\python.exe -m pip install -e "backend[dev]"
# ── 第 3 步:配置 ──
copy backend\.env.example backend\.env
# 必改项(首次部门部署):
# ADMIN_PASSWORD=强密码 # 设置后启用 HTTP Basic 登录
# SESSION_SECRET=随机长字符串 # 部署环境务必改
# INGEST_TOKEN=随机字符串 # 设置后扩展采集接口需要 token
# LLM_API_KEY / LLM_BASE_URL / LLM_MODEL # 如需要 AI 能力
# ── 第 4 步:前端依赖(如需开发前端)──
cd vue-app
npm install
```
> 依赖说明:`backend/requirements.txt` 内容仅为 `-e .[dev]`,实际依赖清单在 [pyproject.toml](backend/pyproject.toml) 的 `[project.dependencies]`。
### 5.3 启动后端(开发/日常)
```powershell
cd D:\Hgny\Py\recruit-sys
# 启动后端(使用 backend/.venv)。默认读取配置 host/port(.env 或默认 127.0.0.1:4177)。
backend\.venv\Scripts\python.exe backend/scripts/start_server.py
# 开发模式:代码变更自动重载
backend\.venv\Scripts\python.exe backend/scripts/start_server.py --reload
# 生产/演示部署:先构建前端并同步到 backend/static,再启动(见 §5.5)
backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend
```
- 首次启动自动创建 `data/`(SQLite)与 `uploads/` 目录并自动建表([db.py 的 create_all](backend/app/db.py)),**不需要**先跑迁移。
- 访问 `http://127.0.0.1:4177/` 会 307 跳转到 `/index.html``/docs` 是 FastAPI 交互文档。
- 启动脚本说明与参数见 [backend/scripts/start_server.py](backend/scripts/start_server.py)
- Windows 一键启动脚本:[backend/start-server.cmd](backend/start-server.cmd)(同样 4177,`--reload`**已配置为使用 `.venv` 启动**,日志写入 `backend/server.out.log` / `server.err.log`)。
### 5.4 启动前端(开发模式)
```powershell
cd D:\Hgny\Py\recruit-sys\vue-app
npm install # 首次
npm run dev # http://127.0.0.1:5173
```
- Vite 把 `/api` 代理到 `http://127.0.0.1:4177`(可用环境变量 `RECRUITMENT_BACKEND_PORT` 改后端端口,见 [vite.config.js](vue-app/vite.config.js))。
- 开发时浏览器访问 5173,后端必须已启动。
### 5.5 生产部署(同源托管)
```powershell
# 一条命令完成:构建前端 → 同步到 backend/static → 启动后端
cd D:\Hgny\Py\recruit-sys
backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend
# 访问 http://127.0.0.1:4177 即完整系统(/ 307 跳转到 /index.html)
```
- 前端 API 地址默认空(同源);如需指向其他后端,注入全局变量 `window.RECRUITMENT_API_BASE_URL`(见 [vue-app/src/api/index.js](vue-app/src/api/index.js))。
- 也可以手动同步:`cd vue-app && npm run build` 后把 `dist/` 复制到 `backend/static/`
### 5.6 浏览器扩展(可选)
1. 启动后端(扩展连 4177);
2. Chrome/Edge 扩展页开启开发者模式 → 加载已解压的扩展程序 → 选 `browser-extension/`
3. 在 BOSS 直聘 / 猎聘候选人页面点击右下角宠物按钮采集;弹窗可改 API 地址并测试连接。
### 5.7 数据库迁移(如需要)
项目默认 `create_all` 自动建表;Alembic 迁移已配置([alembic/env.py](backend/alembic/env.py) 从 Settings 读 DATABASE_URL),切换 MySQL 或需要版本化管理时:
```powershell
backend\.venv\Scripts\python.exe -m alembic -c backend/alembic.ini upgrade head
```
---
## 6. 测试现状(已实测)
```powershell
cd D:\Hgny\Py\recruit-sys
backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml
```
**当前结果:11 通过,0 失败**(已实测)。
- 说明:曾有一个失败(`test_static_security.py`),原因是后端静态目录为空且该测试断言旧版 `app.js` 存在。已把静态托管链路改为 `start_server.py --with-frontend` 自动同步,并将该测试改为断言 Vue 前端 `index.html` 实际引用的 JS 资源,现已全绿。
- 测试覆盖:AI 本地回退([test_ai.py](backend/tests/test_ai.py))、访谈状态机全流程 14 题推进 + 已有字段跳过、采集幂等去重 + 队列确认([test_ingest.py](backend/tests/test_ingest.py))、状态整包 roundtrip + localStorage 导入([test_state.py](backend/tests/test_state.py))、简历解析字段提取 + CLI --help([test_resume_parser.py](backend/tests/test_resume_parser.py))、首页跳转 + Vue 静态资源 + CORS 白名单 + .env 不可被静态托管泄露([test_static_security.py](backend/tests/test_static_security.py))。
- 测试环境隔离:conftest 用临时目录的 SQLite + 清空 env,见 [backend/tests/conftest.py](backend/tests/conftest.py)
---
## 7. 数据存储
| 内容 | 位置 | 说明 |
| --- | --- | --- |
| SQLite 数据库 | `./data/recruitment.sqlite`(相对项目根目录;`DATABASE_URL` 可改) | 14 张表;业务数据以整包 JSON 快照为准 |
| 简历文件 | `./uploads/resumes/``FILES_DIR` 可改) | hash 命名,`GET /api/resumes/{storedName}` 下载 |
| 状态快照 | `app_state_meta.raw_snapshot` | 整包 JSON 备份,空库时回读 |
| 导出备份 | `GET /api/export/state` | 下载 `recruitment-backup-YYYY-MM-DD.json`,可经 `/api/import/local-state` 导回 |
| 前端 localStorage | key `recruitment-system-mvp-v1` | 前端主事实源,向后端全量同步 |
---
## 8. 已知问题与设计边界
### 8.1 已知问题(已修复后剩余)
1. **`backend/static/` 默认为空 → 未同步前页面 404**`/` 只做 307 跳转,`/index.html` 在 static 为空时返回 404。这是「未执行前端同步」的正常状态,不是代码 bug;用 `start_server.py --with-frontend`(§5.5)或手动复制 `vue-app/dist/` 后即恢复。测试套件已改为不依赖该目录(见 §6)。
2. **`backend/.env` 中残留真实 Key**`backend/.env`(非 .env.example)里 `LLM_API_KEY`/`LLM_BASE_URL` 指向 `http://172.18.102.220:13000` 的内部 LLM 服务,属本机配置;`.env` 已在 .gitignore 中,但**交接时注意不要把它带到仓库/分享出去**
### 8.2 设计边界(非缺陷,使用前须知)
- **状态同步是「整包覆盖」而非增量**`PUT /api/state` 会全表删后重建([state_repository.py 的 replace_state](backend/app/repositories/state_repository.py))。多端同时使用会互相覆盖,**不适合多人并发操作**;单机/单 HR 使用无问题。
- **`localStorage` 与后端状态的一致性**:前端先写 localStorage 再同步后端;若 localStorage 有旧数据且后端空库,会以 localStorage 为准导入(`/api/import/local-state` 逻辑存在)。**清浏览器数据前请先导出备份**`GET /api/export/state`)。
- **简历解析是纯规则、无 OCR**:扫描件/图片型 PDF 提取不到文本,`parse_status` 会标记 `needs_manual`,需人工补录(见 [ingest_repository.py](backend/app/repositories/ingest_repository.py)`parse_status` 判断)。解析器面向中文文本简历优化,英文简历字段提取能力弱。
- **`reference/` 是外部参考副本**`browser-extension/reference/` 下的 boss-sync-plugin 是别家方案的复制品,仅作参考,不参与运行,也不应被当作本项目代码维护(已进 .gitignore)。
### 8.3 部署安全清单(部署前必须知道)
- 不设 `ADMIN_PASSWORD` 时系统**完全开放**(无登录),仅适合本机使用;部署到内网/公网必须设密码 + 改 `SESSION_SECRET` + 设 `INGEST_TOKEN`
- 扩展采集会把**带 cookie 的 PDF URL** 发给后端(后端用这些 cookie 下载 PDF)——cookie 只在本机 4177 传输,但请确认网络环境可信。
- CORS 默认只放行 5173 来源;同源部署时页面自己调自己不受 CORS 限制。
---
## 9. 下一步建议
按优先级排序(基于当前代码缺口):
1. **并发写入保护**`replace_state` 的整包覆盖对多端是隐患;如要多人使用,应改为按实体增量 upsert(现在 `replace_state` 已经是按实体拆表写入,可在此基础上做单实体 PUT),或明确「单用户使用」边界并写进文档。
2. **补充前端测试**:目前只有后端 pytest,前端无任何测试(`vue-app/package.json` 无 test script);至少对 `src/utils/` 的纯逻辑(匹配、评分)加单元测试。
3. **简历解析增强**:OCR(扫描件)、更多格式(doc/xls)、英文简历;`parse_status=needs_manual` 时提供「重新解析 / 人工修正」的 UI 入口(目前仅打标,无前端引导)。
4. **QwenPaw 与 LLM 链路验证**`QWENPAW_ENABLED` 与真实 Key 的端到端未在本机验证(本机只有 local-structured 路径有测试覆盖);部署 AI 能力前应先手动验证 `/api/analyze-resume``/api/generate-jd` 在配置 Key 后的行为。
5. **部署文档化**:补一份部署检查单(改密码/secret/token、数据目录外置、备份策略),把 §8.3 的注意事项固化为流程。
6. **前端构建依赖**`start_server.py --with-frontend` 依赖 `vue-app/node_modules`(已 gitignore),新机器首次需先 `cd vue-app && npm install`;脚本已给出明确报错提示。
> 已完成的修复:启动脚本统一(`backend/scripts/start_server.py`,删除 `backend/package.json` 与失效脚本 `sync_frontend_static.py`/`legacy-server.mjs`);静态托管链路(`--with-frontend` 构建+同步);词典去重(前后端共享 `shared/resume-dictionaries.json`);测试断言更新为 Vue 前端资源(11 过 0 挂)。
---
## 附:关键代码索引
| 关注点 | 文件 |
| --- | --- |
| 应用入口 / 路由挂载 / 中间件 | [backend/app/main.py](backend/app/main.py) |
| 配置与 env 读取 | [backend/app/config.py](backend/app/config.py) |
| 数据库与自动建表 | [backend/app/db.py](backend/app/db.py) |
| ORM 模型(14 表) | [backend/app/models.py](backend/app/models.py) |
| 认证 / 采集令牌 | [backend/app/security.py](backend/app/security.py) |
| 状态整包同步 | [backend/app/repositories/state_repository.py](backend/app/repositories/state_repository.py) |
| 采集入库链路 | [backend/app/repositories/ingest_repository.py](backend/app/repositories/ingest_repository.py) |
| AI 分析 / JD 生成 / 脱敏 | [backend/app/services/llm.py](backend/app/services/llm.py) |
| 逼问式访谈状态机 | [backend/app/services/jd_grill.py](backend/app/services/jd_grill.py) |
| QwenPaw 客户端 | [backend/app/services/qwenpaw_client.py](backend/app/services/qwenpaw_client.py) |
| 简历解析器(纯规则) | [backend/app/services/resume_parser_core.py](backend/app/services/resume_parser_core.py) |
| 文件存储 | [backend/app/services/file_storage.py](backend/app/services/file_storage.py) |
| 对外 JD 同步 | [backend/app/services/external_sync.py](backend/app/services/external_sync.py) |
| 前端入口 / 路由 / store | [vue-app/src/main.js](vue-app/src/main.js) / [router/index.js](vue-app/src/router/index.js) / [stores/recruitment.js](vue-app/src/stores/recruitment.js) |
| 前端 API 封装 | [vue-app/src/api/recruitment.js](vue-app/src/api/recruitment.js) |
| 前端常量(词典/种子数据) | [vue-app/src/utils/constants.js](vue-app/src/utils/constants.js)(词典来自共享 [shared/resume-dictionaries.json](shared/resume-dictionaries.json)) |
| 扩展采集逻辑 | [browser-extension/content.js](browser-extension/content.js) / [background.js](browser-extension/background.js) |
| 扩展清单 | [browser-extension/manifest.json](browser-extension/manifest.json) |
| 测试 | [backend/tests/](backend/tests/) |
{
"schools985": [
"清华大学",
"北京大学",
"中国人民大学",
"北京航空航天大学",
"北京理工大学",
"中国农业大学",
"北京师范大学",
"中央民族大学",
"南开大学",
"天津大学",
"大连理工大学",
"东北大学",
"吉林大学",
"哈尔滨工业大学",
"复旦大学",
"同济大学",
"上海交通大学",
"华东师范大学",
"南京大学",
"东南大学",
"浙江大学",
"中国科学技术大学",
"厦门大学",
"山东大学",
"中国海洋大学",
"武汉大学",
"华中科技大学",
"湖南大学",
"中南大学",
"国防科技大学",
"中山大学",
"华南理工大学",
"四川大学",
"电子科技大学",
"重庆大学",
"西安交通大学",
"西北工业大学",
"西北农林科技大学",
"兰州大学"
],
"schools211Extra": [
"北京交通大学",
"北京工业大学",
"北京科技大学",
"北京化工大学",
"北京邮电大学",
"北京林业大学",
"北京中医药大学",
"北京外国语大学",
"中国传媒大学",
"中央财经大学",
"对外经济贸易大学",
"北京体育大学",
"中央音乐学院",
"华北电力大学",
"中国政法大学",
"中国矿业大学",
"中国石油大学",
"中国地质大学",
"上海财经大学",
"上海大学",
"东华大学",
"华东理工大学",
"上海外国语大学",
"第二军医大学",
"苏州大学",
"南京航空航天大学",
"南京理工大学",
"河海大学",
"江南大学",
"南京农业大学",
"中国药科大学",
"南京师范大学",
"安徽大学",
"合肥工业大学",
"福州大学",
"南昌大学",
"郑州大学",
"武汉理工大学",
"华中师范大学",
"华中农业大学",
"中南财经政法大学",
"湖南师范大学",
"暨南大学",
"华南师范大学",
"广西大学",
"海南大学",
"西南交通大学",
"四川农业大学",
"西南财经大学",
"西南大学",
"云南大学",
"贵州大学",
"西藏大学",
"西北大学",
"西安电子科技大学",
"长安大学",
"陕西师范大学",
"青海大学",
"宁夏大学",
"新疆大学",
"石河子大学",
"内蒙古大学",
"辽宁大学",
"大连海事大学",
"东北师范大学",
"哈尔滨工程大学",
"东北林业大学",
"东北农业大学",
"河北工业大学",
"太原理工大学",
"延边大学"
],
"industrySchools": [
"东北电力大学",
"上海电力大学",
"华北水利水电大学",
"长沙理工大学",
"三峡大学",
"沈阳化工大学",
"沈阳化工学院",
"沈阳工业大学",
"辽宁石油化工大学",
"南京工程学院",
"浙江水利水电学院"
],
"majors": [
"电气工程及其自动化",
"机械设计制造及其自动化",
"新能源科学与工程",
"能源与动力工程",
"数据科学与大数据技术",
"计算机科学与技术",
"化学工程与工艺",
"电子信息工程",
"新能源材料与器件",
"能源化学工程",
"电力系统及其自动化",
"电气工程与智能控制",
"人力资源管理",
"信息管理与信息系统",
"电气工程",
"软件工程",
"自动化",
"应用化学",
"通信工程",
"工商管理",
"市场营销",
"财务管理",
"会计学"
],
"skills": [
"电力交易",
"电力市场",
"现货",
"中长期",
"售电",
"新能源",
"储能",
"负荷预测",
"交易策略",
"电价",
"价差",
"结算",
"风控",
"政策研究",
"数据分析",
"Python",
"SQL",
"Excel",
"React",
"Node",
"SaaS",
"AI",
"产品规划",
"需求分析",
"销售策略",
"大客户",
"团队管理",
"招聘",
"员工关系"
]
}
\ No newline at end of file
import dictionaries from '../../../shared/resume-dictionaries.json'
export const STORAGE_KEY = 'recruitment-system-mvp-v1'
export const OFFER_TEMPLATE_VERSION = 'offer-template-v2'
......@@ -175,187 +177,20 @@ export const seedState = {
updatedAt: new Date().toISOString(),
}
export const project985Schools = new Set([
'清华大学',
'北京大学',
'中国人民大学',
'北京航空航天大学',
'北京理工大学',
'中国农业大学',
'北京师范大学',
'中央民族大学',
'南开大学',
'天津大学',
'大连理工大学',
'东北大学',
'吉林大学',
'哈尔滨工业大学',
'复旦大学',
'同济大学',
'上海交通大学',
'华东师范大学',
'南京大学',
'东南大学',
'浙江大学',
'中国科学技术大学',
'厦门大学',
'山东大学',
'中国海洋大学',
'武汉大学',
'华中科技大学',
'湖南大学',
'中南大学',
'国防科技大学',
'中山大学',
'华南理工大学',
'四川大学',
'电子科技大学',
'重庆大学',
'西安交通大学',
'西北工业大学',
'西北农林科技大学',
'兰州大学',
])
// 学校/专业/技能词典与后端共享同一份数据(shared/resume-dictionaries.json),
// 修改词典请改共享文件,避免与后端 resume_parser_core.py 两处漂移。
export const project985Schools = new Set(dictionaries.schools985)
export const project211Schools = new Set([
...project985Schools,
'北京交通大学',
'北京工业大学',
'北京科技大学',
'北京化工大学',
'北京邮电大学',
'北京林业大学',
'北京中医药大学',
'北京外国语大学',
'中国传媒大学',
'中央财经大学',
'对外经济贸易大学',
'北京体育大学',
'中央音乐学院',
'华北电力大学',
'中国政法大学',
'中国矿业大学',
'中国石油大学',
'中国地质大学',
'上海财经大学',
'上海大学',
'东华大学',
'华东理工大学',
'上海外国语大学',
'苏州大学',
'南京航空航天大学',
'南京理工大学',
'河海大学',
'江南大学',
'南京农业大学',
'中国药科大学',
'南京师范大学',
'安徽大学',
'合肥工业大学',
'福州大学',
'南昌大学',
'郑州大学',
'武汉理工大学',
'华中师范大学',
'华中农业大学',
'中南财经政法大学',
'湖南师范大学',
'暨南大学',
'华南师范大学',
'广西大学',
'海南大学',
'西南交通大学',
'四川农业大学',
'西南财经大学',
'西南大学',
'云南大学',
'贵州大学',
'西北大学',
'西安电子科技大学',
'长安大学',
'陕西师范大学',
'内蒙古大学',
'辽宁大学',
'大连海事大学',
'东北师范大学',
'哈尔滨工程大学',
'东北林业大学',
'东北农业大学',
'河北工业大学',
'太原理工大学',
'延边大学',
...dictionaries.schools211Extra,
])
export const industrySchools = new Set([
'东北电力大学',
'上海电力大学',
'华北水利水电大学',
'长沙理工大学',
'三峡大学',
'沈阳化工大学',
'沈阳化工学院',
'沈阳工业大学',
'辽宁石油化工大学',
'南京工程学院',
'浙江水利水电学院',
])
export const industrySchools = new Set(dictionaries.industrySchools)
export const commonMajors = [
'电气工程及其自动化',
'机械设计制造及其自动化',
'新能源科学与工程',
'能源与动力工程',
'数据科学与大数据技术',
'计算机科学与技术',
'化学工程与工艺',
'电子信息工程',
'新能源材料与器件',
'能源化学工程',
'电力系统及其自动化',
'电气工程与智能控制',
'人力资源管理',
'信息管理与信息系统',
'电气工程',
'软件工程',
'自动化',
'应用化学',
'通信工程',
'工商管理',
'市场营销',
'财务管理',
'会计学',
]
export const commonMajors = dictionaries.majors
export const STAGE_ORDER = ['未筛选', '初筛通过', '面试中', 'Offer 中', '待入职', '已入职']
export const SKILL_DICTIONARY = [
'电力交易',
'电力市场',
'现货',
'中长期',
'售电',
'新能源',
'储能',
'负荷预测',
'交易策略',
'电价',
'价差',
'结算',
'风控',
'政策研究',
'数据分析',
'Python',
'SQL',
'Excel',
'React',
'Node',
'SaaS',
'AI',
'产品规划',
'需求分析',
'销售策略',
'大客户',
'团队管理',
'招聘',
'员工关系',
]
export const SKILL_DICTIONARY = dictionaries.skills
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment