# 招聘信息采集与智能匹配系统 **Repository Path**: AJiangS/jobradar ## Basic Information - **Project Name**: 招聘信息采集与智能匹配系统 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-14 - **Last Updated**: 2026-10-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 上岸通 — 事业单位/国企招聘信息聚合平台(原 JobRadar) > 后端迁移 (FastAPI → Spring Boot) 聚合 136+ 官方招聘渠道(各级人社厅局、人事考试网、央企国企、银行、高校就业网)的 招聘公告与职位信息:**Python 爬虫采集 → DeepSeek 大模型结构化分析 → Spring Boot 入库 与智能匹配**,全流程文件流水线自动化。 把原 FastAPI 后端整体迁移到 Spring Boot 3 (Spring Boot 3.5.5,**JDK 25**)。数据库改用 **MySQL** (本地实例,root/root),持久层为 **MyBatis(注解 SQL 风格)**,表结构由项目根目录的 `schema.sql` 单文件管理(全部 `CREATE TABLE IF NOT EXISTS`, 启动时自动执行,已有库不受影响);API 路径、请求/响应 JSON 形状、状态码与 原后端一一对应,Vue 前端无需任何改动。 架构(**爬虫抓取 → 立即 AI 分析 → 落盘 → 后端入库** 的文件流水线): ``` Vue 前端(独立进程 :5173 开发 / :4173 预览) │ fetch /api/v1/**(credentials: include) ▼ Spring Boot :8080(纯 JSON API + CORS 放行前端源) │ │ 轮询主目录(复用 meta.ai_analysis 直接入库; ├── MySQL :3306 ▼ 无分析结果 → 宽限期内等爬虫侧写回,超时兜底) │ 主目录 crawler-data/ └── Python 爬虫服务 :18091 └─ 每个网址一个子目录(*.txt + *.meta.json + 附件) (POST /crawl/sources/{id}/run,源配置由 Spring 传入) (GET /crawl/health) ``` **前后端彻底分离**:后端只提供 `/api/v1/**` JSON 接口,不再托管前端构建产物; 前端是独立进程(Vite),`/api` 请求由 Vite proxy 转发到 `http://127.0.0.1:8080`, 跨源访问由后端 CORS 配置(`WebConfig#addCorsMappings`)放行并允许携带 Cookie。 数据流水线: ``` 爬虫(独立进程) 后端(Spring,每 5 分钟轮询,可配) 按源表顺序逐个网站抓取 主目录里有文件? ──► 内容落盘 主目录/<网站名>/xxx.txt ├─ meta 带 ai_analysis ──► 直接入库(不再调模型) (+ .meta.json 元数据 + 附件归档) ├─ 无分析且 < 宽限期(2h) ──► 等待爬虫侧写回 只写文件,不写数据库 └─ 无分析且超宽限期 ──► 后端兜底 DeepSeek 分析 抓取全部完成后立即调 DeepSeek (兼容旧文件/爬虫未配 key) 结构化分析(tools/ai_process.py), 结果写回 .meta.json 的 ai_analysis ──► 落盘完成(meta 带分析,原子改名) ``` - 爬虫负责抓取 + **抓完立即调 DeepSeek 分析**(`jobradar/ai.py`,prompt 与后端一致):结果写进 `.meta.json` 的 `ai_analysis` 字段后原子改名落盘——**后端轮询只会看到"已分析完"的完整文件**, 根治"没爬完就被轮询"的竞态(txt/meta 均 tmp+rename 原子提交); - 后端固定间隔轮询主目录:**优先复用 meta.ai_analysis 直接入库**(公告 + 公告版本 + 岗位,正式数据 ACTIVE),不再重复调大模型;文件无分析结果时在宽限期(`jobradar.crawler.ai-grace-millis`, 默认 2 小时)内跳过等爬虫侧写回,超时后兜底自己分析(兼容爬虫未配 DeepSeek key 的旧部署); - 去重:公告按 `official_url` 指纹、岗位按内容指纹——重复抓取/重复分析不会产生重复数据; 已入库的文件在下一轮轮询时直接移入 `processed/`; - 管理后台状态接口包含 `pending_crawled_files`(主目录待处理文件数)。 - **Spring Boot 后端** `backend/`:全部业务逻辑(认证、职位、推荐、通知、定时任务、Web Push、 学校合作岗位(录入/推送/在线申请审核)、以及本流水线的目录轮询入库 `CrawledFileProcessor`)。 - **Python 爬虫服务** `crawler/main.py`:薄 HTTP 壳,**纯文件写入器——不连数据库**(源配置由 Spring 放在请求体里传入,爬完返回统计)。抓取与落盘逻辑在 `source_runner` / `site_crawlers` / `sources`;`tools/run_all.py` 全量抓取后自动接 `tools/ai_process.py`(**抓完立即调 DeepSeek 结构化**,结果写 meta.ai_analysis); 任务状态机(QUEUED/RUNNING/SUCCEEDED/ RETRY/DEAD_LETTER/BLOCKED)与源健康度**已迁到后端 `CrawlerDispatchService`**。 - **AI 全部走 DeepSeek API**(OpenAI 兼容协议),替代原来的本地 AI 网关。 --- ## 1. 构建 Spring Boot 后端 需要 **JDK 25**(含 Maven;编译由你本机进行,仓库不要求本地装有 Java)。 Docker 构建(服务器上可用): ```bash cd backend docker build -t jobradar-backend . ``` 或者服务器上直接用 Maven(JDK 25 + Maven 3.9+): ```bash cd backend mvn -DskipTests package # 产物 backend/target/jobradar-backend.jar ``` ## 2. 配置 服务器上创建两个环境文件(键值见下方配置表;也支持直接放 `application.yml`): ```bash vi /home/cjj/services/jobradar/backend/env # Spring Boot 环境变量 vi /home/cjj/services/jobradar/crawler/env # 爬虫环境变量 ``` **DeepSeek API Key**:默认写在 `backend/src/main/resources/application.yml` 的 `jobradar.ai.deepseek.api-key`(支持 `${DEEPSEEK_API_KEY:}` 环境变量覆盖): ```yaml jobradar: ai: deepseek: base-url: https://api.deepseek.com api-key: sk-xxxx # ← 在这里粘贴你的 DeepSeek Key daily-model: deepseek-chat ultra-model: deepseek-reasoner ``` 关键配置(均在 application.yml / env): > **数据库 = MySQL**(root/root 本地实例),持久层 = MyBatis 注解 Mapper > (`com.jobradar.mapper`,无 XML;`save` 为 `INSERT ... ON DUPLICATE KEY UPDATE` 全列 upsert, > createdAt/updatedAt 由 MyBatis 拦截器自动补)。启动时执行 `schema.sql` > (`createDatabaseIfNotExist` 自动建库);所有 TEXT 列按 MySQL `longtext` 创建 > (公告原文可能超过 64KB),`JSON` 列用 MySQL 原生 JSON 类型 > (TypeHandler:`com.jobradar.mybatis`),MySQL 保留字列(`before`/`after`/`read`/ > `key`/`rank`)已按反引号转义。爬虫是纯文件写入器,不再连接数据库。 | 项 | 默认 | 说明 | | --- | --- | --- | | `DATABASE_URL` | `jdbc:mysql://127.0.0.1:3306/jobradar?...` | MySQL,root/root,自动建库建表 | | `DATABASE_USER/PASSWORD` | `root` / `root` | 本机 MySQL 凭据 | | `CRAWLER_BASE_URL` | `http://127.0.0.1:18091` | 爬虫服务地址 | | `CRAWLER_INPUT_DIR` | `/home/cjj/services/jobradar/crawler-data` | 爬虫落盘主目录(每站一个子目录),后端轮询它 | | `CRAWLER_POLL_MILLIS` | `300000` | 后端轮询主目录间隔(毫秒;默认 5 分钟) | | `CRAWLER_AI_GRACE_MILLIS` | `7200000` | meta 无爬虫侧 ai_analysis 时的宽限期(毫秒;默认 2 小时,超时后端兜底分析) | | `DEEPSEEK_API_KEY` | 空 | 爬虫侧也读同一变量:抓完立即调 DeepSeek 分析(未配置则跳过,交给后端兜底) | | `SMTP_*` | 空 | 邮件通知(可选) | | `VAPID_*` | 空 | Web Push(可选) | | `SECURE_COOKIES` | `true` | 生产走 HTTPS 时保持 true | 爬虫 env(`crawler/env`)很简单——爬虫**不需要**数据库连接: ``` JOBRADAR_HOME=/home/cjj/services/jobradar # 可选:落盘主目录,默认 /crawler-data # 必须与后端 CRAWLER_INPUT_DIR 指向同一目录 CRAWLER_OUTPUT_ROOT=/home/cjj/services/jobradar/crawler-data ``` ## 3. 启动 **前后端是两个独立进程**(前后分离): Spring Boot 后端(JDK 25 + Maven 构建后直接跑,或 Docker)——只提供 `/api/v1/**` JSON API: ```bash cd backend && mvn -DskipTests package java -XX:MaxRAMPercentage=75 -jar target/jobradar-backend.jar # 默认监听 127.0.0.1:8080 ``` Vue 前端(独立进程,Node 18+): ```bash cd frontend && npm install npm run dev # 开发:http://127.0.0.1:5173,/api 由 Vite proxy 转发到 :8080 # 或生产预览: npm run build && npm run preview # 预览:http://127.0.0.1:4173 ``` 爬虫服务(独立进程,依赖 `crawler/requirements.txt`,只有 fastapi/uvicorn/httpx/bs4): ```bash cd crawler python -m venv .venv && .venv/bin/pip install -r requirements.txt # 爬虫 env(无数据库): # JOBRADAR_HOME=/home/cjj/services/jobradar .venv/bin/python -m uvicorn main:app --host 127.0.0.1 --port 18091 ``` 需要对外提供访问时,用任意反向代理(nginx/caddy)把 80 端口反代到 `127.0.0.1:4173`(前端预览/静态服务)即可;API 同理由前端 proxy 转发。 ## 3.5 打包成手机 App(安卓 / iOS) 前端用 **Capacitor** 打包成原生 App——`frontend/dist` 就是 App 的 Web 部分, 外面套一个原生壳(安卓 = Android Studio 工程,iOS = Xcode 工程), 已生成 `frontend/android/` 与 `frontend/ios/`。 ```bash cd frontend npm install npx cap add android # 已执行过:生成 android/ 工程( Gradle ) npx cap add ios # 已执行过:生成 ios/ 工程(需要 macOS + Xcode 才能编译) npm run build && npx cap sync # 重新构建 dist 并同步进两个原生工程 ``` **关键差异:原生壳里没有 Vite proxy**,`/api/v1` 相对路径会指向 App 本地, 所以打包前要用环境变量指定后端的绝对地址(见 `frontend/.env.example`): ```bash # 安卓模拟器(10.0.2.2 = 模拟器眼中的宿主机): VITE_API_BASE_URL=http://10.0.2.2:8080/api/v1 npm run build && npx cap sync android # 真机与电脑同一 WiFi:后端要允许局域网访问,前端指向电脑的局域网 IP SERVER_ADDRESS=0.0.0.0 java -jar backend/target/jobradar-backend.jar VITE_API_BASE_URL=http://192.168.x.x:8080/api/v1 npm run build && npx cap sync android # 生产:后端上 HTTPS 后指向正式域名 VITE_API_BASE_URL=https://api.example.com/api/v1 npm run build && npx cap sync ``` 编译 / 安装: ```bash npm run android:open # 打开 Android Studio,Run 到模拟器或真机 npm run ios:open # 打开 Xcode(仅 macOS),选择真机/模拟器运行 # 或命令行直接出包: cd android && ./gradlew assembleRelease # 产物 android/app/build/outputs/apk/release/ ``` 注意: - **跨域已放行**:后端 CORS 允许 `https://localhost`(安卓 WebView)和 `capacitor://localhost`(iOS WebView)携带 Cookie,登录态与 CSRF 与浏览器一致。 - **PWA 的 Service Worker 只在浏览器注册**(`src/main.ts` 判断 `Capacitor.isNativePlatform()`),原生 App 由系统管理更新,避免 SW 缓存拖住版本。 - 原生推送需要 `@capacitor/push-notifications`(未接入);目前 App 内用 通知中心(铃铛页)查看提醒。 - 后端默认只听 `127.0.0.1`,真机调试时用 `SERVER_ADDRESS=0.0.0.0` 放开; `secure-cookies` 在明文 HTTP 下要设 `false`(HTTPS 部署保持 `true`)。 ## 4. 验证 ```bash curl http://127.0.0.1:8080/api/v1/health # 后端 API:{"status":"ok","database":"ok"} curl http://127.0.0.1:18091/crawl/health # 爬虫服务:{"status":"ok",...} # 浏览器打开 http://127.0.0.1:5173 # 前端(开发);生产预览用 :4173 ``` 端到端(流水线冒烟):手动触发一个源抓取 → 爬虫把内容落到 `CRAWLER_INPUT_DIR/<网站名>/` → **爬虫抓完立即调 DeepSeek 分析**(结果写 `.meta.json` 的 `ai_analysis`)→ 下一次轮询(默认 5 分钟内)后端**复用分析直接 入库**公告/岗位 → 文件移入 `processed/`;管理后台状态里 `pending_crawled_files` 回落到 0。爬虫未配置 key 时,文件在宽限期(2 小时)后由 后端兜底分析入库。 ## 5. 定时任务(替代 Celery beat) 全部在 Spring 内实现,且仅在 `automation_state.status == RUNNING` 时执行,并通过 JVM 内任务锁保证单实例不并发: | 时间 (Asia/Shanghai) | 任务 | | --- | --- | | `0 0 2 * * *` | 数据源抓取:按源表顺序(行政级别+名称)一个一个派发抓取(爬虫落盘) | | 每 5 分钟(`jobradar.crawler.poll-millis`) | 目录轮询:复用 meta.ai_analysis 直接入库 → 移入 processed/;无分析结果在宽限期内等待爬虫侧写回,超时后端兜底分析 | | `0 0 4 * * *` | 推荐刷新 | | `0 0 6 * * *` | 过期清理(ACTIVE 过期行及其公告) | | `0 */15 * * * *` | 通知投递 | | 每 5 分钟 | RETRY 任务重派(抓取失败自动重试,最多 3 次,超限 DEAD_LETTER) | > 抓取并发:`jobradar.crawler.max-concurrent`(默认 1)控制同时抓几个源, > 1 = 严格顺序;源多且互相独立时可调大(如 3-5)加快全量抓取。 ## 6. 数据源与爬虫方法(每站一个 py 文件,按目录自动分发) `sources` 表就是登记表(schema.sql 已内置全部 136 个站点的 `INSERT IGNORE` 种子数据, 实测不可爬的 34 个自动置 enabled=0):`name`(网站名)+ `base_url`(网址)+ `adapter_type`(爬虫方法名),管理后台 `POST /api/v1/admin/sources` 添加。 **`adapter_type` = `crawler/sites/` 下的文件名**(如 `guangdong_rsks`)。 爬虫启动时自动扫描 `crawler/sites/*.py` 建立注册表,抓取时按 `adapter_type` 找对应文件调用;找不到则该源报 BLOCKED 并自动停用。 新增一个人社局官网: 1. 在 `crawler/sites/` 下新建 `<方法名>.py`(复制 `_template.py` 改 `SITE` 里的 name/base_url,再在 `crawl()` 里填该站的选择器/翻页/关键词); 静态列表页直接调 `runner.crawl_list(...)`;特殊结构(跳转壳/接口/JS/表单) 在 `crawl()` 里写自己的逻辑(用 `runner.public_get` / `write_article` / `download_attachments` / `save_html_snapshot` 等公共工具); 2. `sources` 表加一行:`name`=网站名、`base_url`=网址、 `adapter_type`=上一步的文件名; 3. 启用后每日 02:00 按源表顺序依次抓取(含翻页),内容落盘到 `主目录/<网站名>/`(`*.txt` + `*.meta.json` + 附件),后端轮询**复用 meta.ai_analysis 入库**。 内置方法:`html-list`(通用静态列表页,选择器从 `source.config` 读,兼容旧数据源)。 已收录 **135 个官方招聘网站**:6 中央(人社部/人事考试网/公务员局/事业单位平台/公共招聘网/教育部就业平台)+ 31 省级人社厅 + 22 省级人事考试网 + 兵团 + 20 重点地市人社 + 12 高校官方就业网 + 6 央企官方招聘 + **三大运营商与 14 家银行(含校招入口)** + 更多央企与国聘平台 + 14 新疆地州政府,不含商业平台;清单见 `crawler/tools/sites_catalog.py`, 实测结果见 `crawler/sites_verified.md`;站点增删改后跑 `crawler/tools/gen_site_files.py` 重新生成文件、`crawler/tools/test_sites.py` 重新实测。 目前 **105/135 站能爬到真实招聘信息**;其余 ❌ 分类记录在 sites_verified.md: JS 渲染 SPA(银行/国企/高校就业平台多为 React/Vue,静态爬虫拿不到列表,部分有公开 JSON 接口可做 custom)、 WAF/验证码(瑞数动态防护 412/521、人社部 JS 混淆 cookie)、登录墙(统一身份认证/学信网)、 域名失效或网络不通(DNS/超时/证书,含证书链不完整的自动降级处理)。 **自定义接口爬虫(custom=True)**:对 JS 渲染/反爬站逆向其前端接口直取职位数据—— 已实装:国聘(gp-api 免登录接口)、工行(qryPostList 免登录)、建行(WCCMainPlatV5 AJAX)、 交行(GTMS 网关接口:社招职位 + 招聘公告)、民生(careerrecruitment/search.view 社招职位)、 中石化(/api/upgrade/homepage 免登录,职位受招聘批次时间锁)、中国移动(getHotJobs.do md5+RSA 签名接口)、国家能源集团(服务端渲染 JSP 列表直取)、人社部(JS cookie 反爬已逆向, 中央事业单位公开招聘平台直取)、国家大学生就业服务平台(ncss 职位 JSON)、江苏人社 (dataproxy.jsp)、新疆人事考试中心(跳转壳解析)。 农行(career.abchina.com)接口为全链路 AES-128-CBC+RSA-1024 信封加密,明文请求被拒 (CL00003),加密方案已逆向(pellosetq=SM4-ECB 校验值)但 key 转换细节未复现,暂未实装。 落盘目录示例: ``` /home/cjj/services/jobradar/crawler-data/ ├── 汉中市人社局/ │ ├── 3f9a1c...24.txt # 公告正文(纯文本) │ ├── 3f9a1c...24.meta.json # 元数据(url/标题/发布时间/来源) │ └── 3f9a1c...24.岗位表.xlsx # 附件归档(后端只分析 .txt,附件存档) ├── 陕西省人社厅/… └── processed/ # 后端处理完的文件移到这里 ``` ## 7. 与原后端的行为差异(有意为之) - **AI 提供方**:所有 AI 调用(职位分析入库、推荐解释)统一走 DeepSeek API; 原 `jobradar-ai` / `private-ai-gateway` 不再需要。 - **Python 爬虫**:`crawler/sites/` 每站一个 py 文件(100+ 官方站,自动扫描 分发);统一骨架 `runner.crawl_list` + 公共工具(public_get/write_article/ download_attachments/save_html_snapshot),站点差异(URL/选择器/翻页/特殊 结构)写在各自文件里;抓取结果**落盘主目录**(每站一个子目录), 岗位解析由后端 DeepSeek 完成;爬虫**不再连接数据库**——源配置由 Spring 经 HTTP 请求体传入,任务状态机(QUEUED/RUNNING/SUCCEEDED/RETRY/DEAD_LETTER/BLOCKED) 与源健康度迁到后端 `CrawlerDispatchService`(HTTP 423 → 源 BLOCKED 并停用; 5xx/网络错误 → RETRY/DEAD_LETTER)。 - **会话/CSRF/权限模型**:`jobradar_session` + `jobradar_csrf` 双 cookie、 `X-CSRF-Token` 校验、`@RequireRole`(ADMIN/SUPER_ADMIN)与原来完全一致。 - 未匹配的 `/api/**` 返回统一错误包 `{code, message, details, request_id}`。 ## 8. 目录速览 ``` backend/ src/main/java/com/jobradar/ controller/ # 所有 REST 控制器(与 FastAPI 路由一一对应) service/ # 业务逻辑(推荐/清理 + CrawledFileProcessor 目录轮询入库 + CrawlerDispatchService 爬取调度) notification/ # 邮件 + Web Push (RFC 8291) + 通知分发 crawler/ # 爬虫 HTTP 客户端 scheduler/ # 定时任务 + JVM 内任务锁 security/ # 密码 (Argon2id) / 会话 / CSRF / 角色 matching/ # 资格匹配引擎 + 专业知识库 ai/ # DeepSeek 客户端 entity/ mapper/ mybatis/ config/ common/ src/main/resources/application.yml crawler/ main.py # 爬虫薄壳 (FastAPI :18091,无数据库:源配置走 HTTP 请求体) jobradar/ # 爬虫核心(纯文件写入器): source_runner.py # 抓取引擎(sites/ 自动扫描分发;crawl_list 通用骨架 + 落盘工具) sources.py # 列表适配器(HtmlListAdapter) sites/ # ★ 每站一个爬虫文件(135 个官方站;文件名 = adapter_type) _template.py # 新增站点模板(复制改名) html_list.py # 内置:config 驱动通用列表页(兼容旧数据源) xjrsks.py # 新疆人事考试中心(自定义:跳转壳 → 正文在 rst.xinjiang.gov.cn) ncss.py # 国家大学生就业服务平台(自定义:职位 JSON 接口直取) jiangsu_rsj.py # 江苏人社(自定义:公示公告 dataproxy.jsp 接口直取) iguopin.py # 国聘(自定义:gp-api 职位接口直取,无需登录,含职位描述全文) icbc_bank.py # 工行招聘(自定义:qryPostList/qryPostById 接口直取,无需登录) ccb_bank.py # 建行招聘(自定义:WCCMainPlatV5 AJAX 接口直取) bcm_bank.py # 交行招聘(自定义:GTMS 网关接口直取,社招职位+公告) cmbc_bank.py # 民生招聘(自定义:search.view 接口直取,社招职位) sinopec_zhaopin.py # 中石化(自定义:/api/upgrade/homepage 公告+职位接口直取) cmcc_zhaopin.py # 中国移动(自定义:getHotJobs.do md5+RSA 签名接口直取) guangdong_rsks.py / mohrss.py / ... # 其余由 tools/gen_site_files.py 生成 tools/ # 开发辅助 sites_catalog.py # 135 个官方站清单(增删改后重新生成;custom=True 站手工维护) gen_site_files.py # 由清单生成 sites/*.py(支持 href_contains / next_url_format / exclude_href) test_sites.py # 逐个实测 → 写 sites_verified.md inspect_site.py # 单站检查:打印列表页链接,精修选择器用 sites_verified.md # 实测结果:能爬到招聘信息的网址清单(✅/❌) requirements.txt # 只有 fastapi / uvicorn / httpx / beautifulsoup4 Dockerfile / pytest.ini tests/ # test_source_runner.py(无数据库的落盘行为测试) ```