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 @@ ...@@ -12,6 +12,12 @@
*.sqlite *.sqlite
*.sqlite3 *.sqlite3
# ---------- 共享数据(词典等,应提交到 git,勿忽略) ----------
# shared/resume-dictionaries.json 由前后端共同引用,需入库
# ---------- 工具索引 ----------
.codegraph/
# ---------- 日志 ---------- # ---------- 日志 ----------
*.log *.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/ ...@@ -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 ```powershell
# 项目根目录(D:\Hgny\Py\recruit-sys) # 项目根目录(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 copy backend\.env.example backend\.env
# 启动服务,默认 http://127.0.0.1:4177 # 启动服务,默认 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 ```powershell
backend\start-server.cmd 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. 启动前端(开发模式) ### 2. 启动前端(开发模式)
...@@ -65,18 +76,15 @@ Vite 开发服务器默认运行在 `http://127.0.0.1:5173`,已配置 `/api` ...@@ -65,18 +76,15 @@ Vite 开发服务器默认运行在 `http://127.0.0.1:5173`,已配置 `/api`
### 3. 生产部署(前端 + 后端同源) ### 3. 生产部署(前端 + 后端同源)
构建前端并让 FastAPI 直接托管页面,最终只需运行一个后端进程: 用启动脚本一次完成「构建前端 → 同步到 `backend/static` → 启动后端」,最终只需运行一个后端进程:
```powershell ```powershell
# 构建前端,产物输出到 vue-app/dist/ backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend
cd vue-app
npm run build
# 把 dist/ 内容复制到后端静态目录(FastAPI 挂载在 /)
# 然后只需启动后端,访问 http://127.0.0.1:4177 即可
``` ```
> 注意:`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. 安装浏览器扩展(可选) ### 4. 安装浏览器扩展(可选)
...@@ -133,7 +141,7 @@ npm run build ...@@ -133,7 +141,7 @@ npm run build
| `HOST` / `PORT` | `127.0.0.1` / `4177` | 服务监听地址与端口 | | `HOST` / `PORT` | `127.0.0.1` / `4177` | 服务监听地址与端口 |
| `DATA_DIR` | `./data` | 运行时数据目录(生产建议放源码目录外) | | `DATA_DIR` | `./data` | 运行时数据目录(生产建议放源码目录外) |
| `FILES_DIR` | `./uploads` | 简历等上传文件目录 | | `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 / 采集接口外) | | `ADMIN_PASSWORD` | 空 | 设置后启用 HTTP Basic 登录(除 ping / 采集接口外) |
| `SESSION_SECRET` | 开发默认值 | 部署时请修改 | | `SESSION_SECRET` | 开发默认值 | 部署时请修改 |
| `INGEST_TOKEN` | 空 | 设置后 `/api/ingest``/api/jd/sync-external``X-Ingest-Token``?token=` | | `INGEST_TOKEN` | 空 | 设置后 `/api/ingest``/api/jd/sync-external``X-Ingest-Token``?token=` |
...@@ -147,27 +155,28 @@ npm run build ...@@ -147,27 +155,28 @@ npm run build
项目默认 `create_all` 自动建表;已有 Alembic 迁移支持,需要时执行: 项目默认 `create_all` 自动建表;已有 Alembic 迁移支持,需要时执行:
```powershell ```powershell
python -m alembic -c backend/alembic.ini upgrade head backend\.venv\Scripts\python.exe -m alembic -c backend/alembic.ini upgrade head
``` ```
## 测试 ## 测试
```powershell ```powershell
python -m pytest -c backend/pyproject.toml backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml
``` ```
## 常用命令 ## 常用命令
```powershell ```powershell
# 后端(项目根目录) # 后端(项目根目录,全部使用 backend/.venv 环境)
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 # 启动后端
python -m pytest -c backend/pyproject.toml # 测试 backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend # 构建+同步前端后启动(生产部署)
python -m alembic -c backend/alembic.ini upgrade head # 迁移 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(不启动服务,直接解析简历文件) # 简历解析 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 dev # 开发服务器 http://127.0.0.1:5173
npm run build # 生产构建到 dist/ npm run build # 生产构建到 dist/
npm run preview # 预览生产构建 npm run preview # 预览生产构建
...@@ -177,8 +186,9 @@ npm run format # Prettier 格式化 ...@@ -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/` 是前端构建产物的同步目录(已 gitignore),初始为空;使用 `start_server.py --with-frontend` 或手动同步 `vue-app/dist/` 后才会提供页面。未同步时 `/` 会跳转但 `/index.html` 返回 404,属正常状态。
- `backend/static/` 目录当前为空,需要「生产部署(同源托管)」时请按上文手动同步 `vue-app/dist/` - 后端启动统一走 `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 @@ ...@@ -4,31 +4,38 @@
## 启动 ## 启动
要求:Python 3.11+。在项目根目录(`recruit-sys/`)执行: 要求:Python 3.11+**后端统一使用虚拟环境 `backend/.venv`**。在项目根目录(`recruit-sys/`)执行:
```powershell ```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 # 首次:复制并按需修改配置 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/`(简历文件)目录,并自动建表。 - 首次启动自动创建 `data/`(SQLite 数据库)与 `uploads/`(简历文件)目录,并自动建表。
- 启动后访问 `http://127.0.0.1:4177/`;API 位于 `/api/*`,交互式文档在 `/docs` - 启动后访问 `http://127.0.0.1:4177/`(307 跳转 `/index.html`;API 位于 `/api/*`,交互式文档在 `/docs`
- Windows 下也可直接运行 `backend\start-server.cmd`启动并输出日志到 `backend/server.out.log` / `server.err.log`)。 - 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 ```powershell
# 构建前端 backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend
cd vue-app
npm run build
# 将 dist/ 内容复制到 backend/static/(此后只需运行后端即可访问页面)
``` ```
> 历史脚本 `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 ...@@ -41,9 +48,10 @@ npm run build
## 常用命令 ## 常用命令
```powershell ```powershell
# 在项目根目录执行 # 在项目根目录执行(统一使用 backend/.venv)
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 # 启动后端
python -m pytest -c backend/pyproject.toml backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend # 构建+同步前端后启动
python -m alembic -c backend/alembic.ini upgrade head backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml
python backend/scripts/parse_resume.py <简历文件> # 简历解析 CLI,不启动服务 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"): ...@@ -10,6 +10,18 @@ if hasattr(sys.stdout, "reconfigure"):
if hasattr(sys.stderr, "reconfigure"): if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8") 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: def read_pdf(path: Path) -> str:
from pypdf import PdfReader from pypdf import PdfReader
...@@ -261,49 +273,18 @@ def infer_source(text: str, filename: str) -> str: ...@@ -261,49 +273,18 @@ def infer_source(text: str, filename: str) -> str:
def extract_skills(text: str): def extract_skills(text: str):
dictionary = [ return [skill for skill in _SKILL_DICTIONARY if skill.lower() in text.lower()]
"电力交易", "电力市场", "现货", "中长期", "售电", "新能源", "储能", "负荷预测",
"交易策略", "电价", "价差", "结算", "风控", "政策研究", "数据分析",
"Python", "SQL", "Excel", "React", "Node", "SaaS", "AI", "产品规划", PROJECT_985 = set(_DICTIONARIES["schools985"])
"需求分析", "销售策略", "大客户", "团队管理", "招聘", "员工关系"
] PROJECT_211_EXTRA = set(_DICTIONARIES["schools211Extra"])
return [skill for skill in dictionary if skill.lower() in text.lower()]
INDUSTRY_SCHOOLS = set(_DICTIONARIES["industrySchools"])
PROJECT_985 = { COMMON_MAJORS = _DICTIONARIES["majors"]
"清华大学", "北京大学", "中国人民大学", "北京航空航天大学", "北京理工大学", "中国农业大学", "北京师范大学", "中央民族大学",
"南开大学", "天津大学", "大连理工大学", "东北大学", "吉林大学", "哈尔滨工业大学", "复旦大学", "同济大学", "上海交通大学", _SKILL_DICTIONARY = _DICTIONARIES["skills"]
"华东师范大学", "南京大学", "东南大学", "浙江大学", "中国科学技术大学", "厦门大学", "山东大学", "中国海洋大学",
"武汉大学", "华中科技大学", "湖南大学", "中南大学", "国防科技大学", "中山大学", "华南理工大学", "四川大学",
"电子科技大学", "重庆大学", "西安交通大学", "西北工业大学", "西北农林科技大学", "兰州大学"
}
PROJECT_211_EXTRA = {
"北京交通大学", "北京工业大学", "北京科技大学", "北京化工大学", "北京邮电大学", "北京林业大学", "北京中医药大学",
"北京外国语大学", "中国传媒大学", "中央财经大学", "对外经济贸易大学", "北京体育大学", "中央音乐学院",
"华北电力大学", "中国政法大学", "中国矿业大学", "中国石油大学", "中国地质大学", "上海财经大学", "上海大学",
"东华大学", "华东理工大学", "上海外国语大学", "第二军医大学", "苏州大学", "南京航空航天大学", "南京理工大学",
"河海大学", "江南大学", "南京农业大学", "中国药科大学", "南京师范大学", "安徽大学", "合肥工业大学", "福州大学",
"南昌大学", "郑州大学", "武汉理工大学", "华中师范大学", "华中农业大学", "中南财经政法大学", "湖南师范大学",
"暨南大学", "华南师范大学", "广西大学", "海南大学", "西南交通大学", "四川农业大学", "西南财经大学",
"西南大学", "云南大学", "贵州大学", "西藏大学", "西北大学", "西安电子科技大学", "长安大学",
"陕西师范大学", "青海大学", "宁夏大学", "新疆大学", "石河子大学", "内蒙古大学", "辽宁大学", "大连海事大学",
"东北师范大学", "哈尔滨工程大学", "东北林业大学", "东北农业大学", "河北工业大学", "太原理工大学",
"延边大学"
}
INDUSTRY_SCHOOLS = {
"东北电力大学", "上海电力大学", "华北水利水电大学", "长沙理工大学", "三峡大学",
"沈阳化工大学", "沈阳化工学院", "沈阳工业大学", "辽宁石油化工大学", "南京工程学院", "浙江水利水电学院"
}
COMMON_MAJORS = [
"电气工程及其自动化", "机械设计制造及其自动化", "新能源科学与工程", "能源与动力工程",
"数据科学与大数据技术", "计算机科学与技术", "化学工程与工艺", "电子信息工程",
"新能源材料与器件", "能源化学工程", "电力系统及其自动化", "电气工程与智能控制",
"人力资源管理", "信息管理与信息系统", "电气工程", "软件工程", "自动化", "应用化学",
"通信工程", "工商管理", "市场营销", "财务管理", "会计学"
]
def clean_school_candidate(value: str) -> str: 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 @echo off
setlocal setlocal
cd /d "%~dp0.." cd /d "%~dp0.."
REM ??????? backend/**/*.py ?????????? backend/static????????? REM 启动招聘系统后端(使用 backend/.venv 虚拟环境,Python 3.11)。
REM ???????? JD ??? QwenPaw????? QwenPaw ???start-qwenpaw.cmd?? REM 通过 backend/scripts/start_server.py 启动:
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/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): ...@@ -6,7 +6,13 @@ def test_homepage_redirects_to_published_frontend(client):
homepage = client.get("/index.html") homepage = client.get("/index.html")
assert homepage.status_code == 200 assert homepage.status_code == 200
assert "招聘系统" in homepage.text 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): 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 STORAGE_KEY = 'recruitment-system-mvp-v1'
export const OFFER_TEMPLATE_VERSION = 'offer-template-v2' export const OFFER_TEMPLATE_VERSION = 'offer-template-v2'
...@@ -175,187 +177,20 @@ export const seedState = { ...@@ -175,187 +177,20 @@ export const seedState = {
updatedAt: new Date().toISOString(), 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([ export const project211Schools = new Set([
...project985Schools, ...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 STAGE_ORDER = ['未筛选', '初筛通过', '面试中', 'Offer 中', '待入职', '已入职']
export const SKILL_DICTIONARY = [ export const SKILL_DICTIONARY = dictionaries.skills
'电力交易',
'电力市场',
'现货',
'中长期',
'售电',
'新能源',
'储能',
'负荷预测',
'交易策略',
'电价',
'价差',
'结算',
'风控',
'政策研究',
'数据分析',
'Python',
'SQL',
'Excel',
'React',
'Node',
'SaaS',
'AI',
'产品规划',
'需求分析',
'销售策略',
'大客户',
'团队管理',
'招聘',
'员工关系',
]
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