diff --git a/API文档.md b/API文档.md new file mode 100644 index 0000000..2846cae --- /dev/null +++ b/API文档.md @@ -0,0 +1,643 @@ +# BOSS 直聘自动化平台 - API 接口文档 + +**Base URL**: `http://{服务器IP}:9000` + +**认证方式**: Cookie(`auth_token`),登录成功后自动通过 `Set-Cookie` 设置,后续请求自动携带。 + +**请求格式**: 支持 `application/json` 和 `multipart/form-data` + +**响应规范**: 所有接口的 JSON 响应体均包含 **`code`** 字段,与 HTTP 状态码一致。成功时如 `code: 200`、`code: 201`;错误时如 `code: 401`、`code: 404`。原返回数组的接口会包装为 `{"code": 状态码, "data": 原数组}`。 + +--- + +## 一、健康检查 + +### GET /health + +**说明**: 检查服务器是否正常运行,无需登录。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回服务状态与在线 Worker 数 | +| 500 | 服务器内部错误(少见) | + +**请求参数**: 无 + +**响应示例**: +```json +{ + "status": "ok", + "workers_online": 2 +} +``` + +| 返回字段 | 类型 | 说明 | +|---------|------|------| +| status | string | 服务状态,固定 `"ok"` | +| workers_online | int | 当前在线 Worker 数量 | + +--- + +## 二、认证 + +### POST /api/auth/login + +**说明**: 管理员登录,无需认证。登录成功后通过 `Set-Cookie` 返回 `auth_token`。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回 token 并设置 Cookie | +| 401 | 用户名或密码错误 | + +**请求参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| username | string | 是 | 用户名 | +| password | string | 是 | 密码 | + +**请求示例**: +```json +{ + "username": "admin", + "password": "boss_dp_admin" +} +``` + +**成功响应** (200): +```json +{ + "token": "a1b2c3d4e5f6..." +} +``` + +**失败响应** (401): +```json +{ + "detail": "用户名或密码错误" +} +``` + +| 返回字段 | 类型 | 说明 | +|---------|------|------| +| token | string | 登录 token(同时通过 Cookie 设置) | + +**响应 Header**: +``` +Set-Cookie: auth_token=a1b2c3d4e5f6...; HttpOnly; Max-Age=31536000; SameSite=Lax +``` + +--- + +## 三、Worker 管理 + +> 以下接口均需登录(携带 `auth_token` Cookie) + +### GET /api/workers + +**说明**: 获取所有已注册 Worker 列表。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回 Worker 数组 | +| 401 | 未登录或 token 失效 | + +**请求参数**: 无 + +**响应示例**: +```json +[ + { + "worker_id": "worker-1", + "worker_name": "本机", + "browsers": [ + { + "id": "abd63190eea84dc49429962f7bb330a4", + "name": "第一个", + "remark": "" + } + ], + "online": true, + "current_task_id": null + } +] +``` + +| 返回字段 | 类型 | 说明 | +|---------|------|------| +| worker_id | string | Worker 唯一标识 | +| worker_name | string | Worker 名称(电脑名) | +| browsers | array | 该电脑上的比特浏览器环境列表 | +| browsers[].id | string | 浏览器窗口 ID | +| browsers[].name | string | 浏览器窗口名称(环境名) | +| browsers[].remark | string | 备注 | +| online | boolean | 是否在线 | +| current_task_id | string/null | 当前正在执行的任务 ID | + +--- + +### GET /api/workers/{worker_id} + +**说明**: 获取指定 Worker 的详细信息。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回单个 Worker 对象 | +| 401 | 未登录或 token 失效 | +| 404 | Worker 不存在 | + +**路径参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| worker_id | string | 是 | Worker ID | + +**成功响应** (200): 同上单个 Worker 对象 + +**失败响应** (404): +```json +{ + "detail": "Worker worker-99 不存在" +} +``` + +--- + +## 四、账号管理 + +> 以下接口均需登录(携带 `auth_token` Cookie) + +### POST /api/accounts + +**说明**: 添加账号(将浏览器环境名称绑定到指定电脑)。若已存在相同绑定则直接返回已有记录。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 201 | 成功创建或返回已有记录 | +| 400 | 请求参数错误(如缺少 browser_name、worker_id) | +| 401 | 未登录或 token 失效 | + +**请求参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| browser_name | string | 是 | 浏览器环境名称 | +| worker_id | string | 是 | 目标电脑的 Worker ID | + +**请求示例**: +```json +{ + "browser_name": "第一个", + "worker_id": "worker-1" +} +``` + +**成功响应** (201): +```json +{ + "id": 1, + "worker_id": "worker-1", + "browser_id": "name:第一个", + "browser_name": "第一个", + "boss_username": "", + "is_logged_in": false, + "current_task_id": null, + "current_task_status": null, + "checked_at": null, + "created_at": "2026-02-14T16:00:00", + "updated_at": "2026-02-14T16:00:00", + "worker_name": "本机", + "worker_online": true +} +``` + +--- + +### GET /api/accounts + +**说明**: 查询所有账号列表,包含电脑名称、在线状态、任务执行状态。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回账号数组 | +| 401 | 未登录或 token 失效 | + +**查询参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| worker_id | string | 否 | 按 Worker ID 过滤 | + +**响应示例**: +```json +[ + { + "id": 1, + "worker_id": "worker-1", + "browser_id": "abd63190eea84dc49429962f7bb330a4", + "browser_name": "第一个", + "boss_username": "某用户", + "is_logged_in": true, + "current_task_id": "3f113d90bb32", + "current_task_status": "success", + "checked_at": "2026-02-14T16:05:00", + "created_at": "2026-02-14T16:00:00", + "updated_at": "2026-02-14T16:05:00", + "worker_name": "本机", + "worker_online": true + } +] +``` + +| 返回字段 | 类型 | 说明 | +|---------|------|------| +| id | int | 账号记录 ID | +| worker_id | string | 绑定的 Worker ID | +| browser_id | string | 比特浏览器窗口 ID | +| browser_name | string | 浏览器环境名称 | +| boss_username | string | BOSS 直聘用户名(检测后填充) | +| is_logged_in | boolean | 是否已登录 BOSS | +| current_task_id | string/null | 当前关联的任务 ID | +| current_task_status | string/null | 当前任务状态:`pending` / `dispatched` / `running` / `success` / `failed` | +| checked_at | string/null | 最近一次检测时间(ISO 格式) | +| created_at | string | 创建时间 | +| updated_at | string | 更新时间 | +| worker_name | string | 电脑名称(运行时补充) | +| worker_online | boolean | 电脑是否在线(运行时补充) | + +--- + +### GET /api/accounts/{id} + +**说明**: 查询单个账号详情。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回单个账号对象 | +| 401 | 未登录或 token 失效 | +| 404 | 账号不存在 | + +**路径参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | int | 是 | 账号记录 ID | + +**成功响应** (200): 同上单个账号对象 + +**失败响应** (404): +```json +{ + "detail": "账号不存在" +} +``` + +--- + +### DELETE /api/accounts/{id} + +**说明**: 删除指定账号。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功删除 | +| 401 | 未登录或 token 失效 | +| 404 | 账号不存在 | + +**路径参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | int | 是 | 账号记录 ID | + +**成功响应** (200): +```json +{ + "message": "账号已删除" +} +``` + +**失败响应** (404): +```json +{ + "detail": "账号不存在" +} +``` + +--- + +## 五、任务管理 + +> 以下接口均需登录(携带 `auth_token` Cookie) +> +> **统一任务入口**:所有任务(检查登录、招聘等)都通过 `POST /api/tasks` 提交,通过 `task_type` 字段区分任务类型。 + +### POST /api/tasks + +**说明**: 提交新任务。通过 `task_type` 指定任务类型,系统自动路由到目标 Worker 执行。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 201 | 成功,任务已创建并派发 | +| 400 | 未指定 worker_id 或 account_name | +| 401 | 未登录或 token 失效 | +| 404 | 未找到拥有该浏览器环境的在线 Worker | +| 503 | Worker 不在线 / WebSocket 连接不存在 / 派发失败 | + +**请求参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| task_type | string | 是 | 任务类型:`check_login`(检查登录)、`boss_recruit`(招聘) | +| worker_id | string | 否 | 目标 Worker ID(与 account_name 二选一) | +| account_name | string | 否 | 浏览器环境名称(系统自动查找对应 Worker) | +| params | object | 否 | 任务附加参数,默认 `{}` | + +**路由规则**: `worker_id` 优先;若未指定则通过 `account_name` 自动查找对应的在线 Worker。 + +**请求示例 — 检查登录**: +```json +{ + "task_type": "check_login", + "account_name": "第一个" +} +``` + +**请求示例 — 招聘任务**: +```json +{ + "task_type": "boss_recruit", + "worker_id": "worker-1", + "params": { + "keyword": "Python开发", + "count": 10 + } +} +``` + +**成功响应** (201): +```json +{ + "task_id": "3f113d90bb32", + "task_type": "check_login", + "status": "dispatched", + "worker_id": "worker-1", + "account_name": "第一个", + "params": {}, + "progress": null, + "result": null, + "error": null, + "created_at": 1739520000.123, + "updated_at": 1739520000.456 +} +``` + +**失败响应**: + +| 状态码 | 说明 | +|--------|------| +| 400 | 未指定 worker_id 或 account_name | +| 401 | 未登录或 token 失效 | +| 404 | 未找到拥有该浏览器环境的在线 Worker | +| 503 | Worker 不在线 / WebSocket 连接不存在 / 派发失败 | + +--- + +### GET /api/tasks + +**说明**: 查询任务列表(内存中的任务)。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回任务数组 | +| 401 | 未登录或 token 失效 | + +**查询参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| worker_id | string | 否 | 按 Worker ID 过滤 | +| status | string | 否 | 按状态过滤:`pending` / `dispatched` / `running` / `success` / `failed` / `cancelled` | +| limit | int | 否 | 返回数量限制,默认 50 | + +**响应示例**: +```json +[ + { + "task_id": "3f113d90bb32", + "task_type": "check_login", + "status": "success", + "worker_id": "worker-1", + "account_name": "第一个", + "params": {}, + "progress": "检测完成: 第一个 → 未登录 ()", + "result": { + "browser_id": "abd63190eea84dc49429962f7bb330a4", + "browser_name": "第一个", + "boss_username": "", + "is_logged_in": false + }, + "error": null, + "created_at": 1739520000.123, + "updated_at": 1739520030.789 + } +] +``` + +| 返回字段 | 类型 | 说明 | +|---------|------|------| +| task_id | string | 任务唯一 ID(12 位十六进制) | +| task_type | string | 任务类型 | +| status | string | 任务状态:`pending` → `dispatched` → `running` → `success`/`failed` | +| worker_id | string/null | 执行任务的 Worker ID | +| account_name | string/null | 关联的浏览器环境名称 | +| params | object | 任务参数 | +| progress | string/null | 执行进度描述 | +| result | object/null | 任务结果(成功时返回) | +| error | string/null | 错误信息(失败时返回) | +| created_at | float | 创建时间(Unix 时间戳) | +| updated_at | float | 最后更新时间(Unix 时间戳) | + +--- + +### GET /api/tasks/{task_id} + +**说明**: 查询指定任务的状态和结果。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回单个任务对象 | +| 401 | 未登录或 token 失效 | +| 404 | 任务不存在 | + +**路径参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| task_id | string | 是 | 任务 ID | + +**成功响应** (200): 同上单个任务对象 + +**失败响应** (404): +```json +{ + "detail": "任务 xxx 不存在" +} +``` + +--- + +## 六、任务类型说明 + +| task_type | 说明 | 必要参数 | 返回 result | +|-----------|------|---------|-------------| +| `check_login` | 检测浏览器环境中 BOSS 账号是否已登录 | `account_name`(环境名) | `{ "browser_id", "browser_name", "boss_username", "is_logged_in" }` | +| `boss_recruit` | 执行 BOSS 直聘自动招聘流程 | `worker_id` + `params` | 根据任务定义返回 | + +--- + +## 七、任务状态流转 + +``` +pending → dispatched → running → success + → failed + → cancelled +``` + +| 状态 | 说明 | +|------|------| +| pending | 任务已创建,等待派发 | +| dispatched | 已通过 WebSocket 发送给 Worker | +| running | Worker 正在执行中 | +| success | 执行成功 | +| failed | 执行失败 | +| cancelled | 已取消 | + +--- + +## 八、联系记录(数据展示) + +> 以下接口均需登录(携带 `auth_token` Cookie) + +### GET /api/contacts/query(按电话/微信号查询) + +**说明**:根据已获取的电话号码或微信号查询联系记录,用于数据展示。对 `contact` 字段做模糊匹配。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回分页结果 | +| 400 | 未提供 contact 参数 | +| 401 | 未登录或 token 失效 | + +**查询参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| contact | string | 是 | 电话号码或微信号(支持部分匹配) | +| page | int | 否 | 页码,默认 1 | +| page_size | int | 否 | 每页条数,默认 20 | + +**请求示例**: +``` +GET /api/contacts/query?contact=13800138000 +GET /api/contacts/query?contact=wxid_abc&page=1&page_size=10 +``` + +**成功响应** (200): +```json +{ + "total": 2, + "page": 1, + "page_size": 20, + "contact_keyword": "13800138000", + "results": [ + { + "id": 1, + "name": "张三", + "position": "Python开发", + "contact": "13800138000", + "reply_status": "已回复", + "wechat_exchanged": true, + "account_id": 1, + "worker_id": "worker-1", + "notes": "", + "contacted_at": "2026-02-24T10:00:00", + "created_at": "2026-02-24T10:00:00" + } + ] +} +``` + +| 返回字段 | 类型 | 说明 | +|---------|------|------| +| total | int | 匹配总数 | +| page | int | 当前页 | +| page_size | int | 每页条数 | +| contact_keyword | string | 本次查询使用的关键词 | +| results | array | 联系记录列表(字段同联系记录模型) | + +**失败响应** (400):未提供 contact 参数时 +```json +{ + "detail": "请提供 contact 参数(电话号码或微信号)" +} +``` + +--- + +### GET /api/contacts + +**说明**:分页查询联系记录,支持按姓名、岗位、联系方式关键词搜索,以及按回复状态、是否交换微信筛选。 + +**状态码**: + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功,返回 total、page、page_size、results | +| 401 | 未登录或 token 失效 | + +**查询参数**:`search`(关键词)、`reply_status`、`wechat_exchanged`、`page`、`page_size`。返回格式同上(含 `total`、`page`、`page_size`、`results`)。 + +--- + +## 九、通用错误格式 + +所有接口的错误响应统一为: + +```json +{ + "detail": "错误描述信息" +} +``` + +| 状态码 | 说明 | +|--------|------| +| 400 | 请求参数错误 | +| 401 | 未登录或 token 失效 | +| 403 | 无权限 | +| 404 | 资源不存在 | +| 503 | Worker 不在线或服务不可用 | diff --git a/server/core/exception_handler.py b/server/core/exception_handler.py index 52fce66..0be8280 100644 --- a/server/core/exception_handler.py +++ b/server/core/exception_handler.py @@ -13,9 +13,10 @@ def custom_exception_handler(exc, context): # 确保使用异常自带的 HTTP 状态码(如 401 登录失效、403 无权限) if isinstance(exc, APIException) and getattr(exc, "status_code", None): response.status_code = exc.status_code - # 统一格式:{"detail": "..."} + # 统一格式:{"detail": "...", "code": 状态码} if isinstance(response.data, list): response.data = {"detail": response.data[0] if response.data else str(exc)} elif isinstance(response.data, dict) and "detail" not in response.data: response.data = {"detail": str(response.data)} + response.data["code"] = response.status_code return response diff --git a/server/middleware.py b/server/middleware.py index 4f479a5..80df48c 100644 --- a/server/middleware.py +++ b/server/middleware.py @@ -2,6 +2,34 @@ """ 自定义中间件。 """ +import json + + +class ResponseCodeMiddleware: + """为所有 JSON 响应 body 统一注入 code 字段(与 HTTP 状态码一致)。""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + if "application/json" not in response.get("Content-Type", ""): + return response + try: + content = response.content.decode(response.charset or "utf-8") + data = json.loads(content) + code = response.status_code + if isinstance(data, dict): + if "code" not in data: + data["code"] = code + elif isinstance(data, list): + data = {"code": code, "data": data} + else: + data = {"code": code, "data": data} + response.content = json.dumps(data, ensure_ascii=False).encode(response.charset or "utf-8") + except (ValueError, TypeError, UnicodeDecodeError): + pass + return response class CorsMiddleware: diff --git a/server/settings.py b/server/settings.py index 15bd156..f3a8b67 100644 --- a/server/settings.py +++ b/server/settings.py @@ -25,6 +25,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ "django.middleware.common.CommonMiddleware", "server.middleware.CorsMiddleware", + "server.middleware.ResponseCodeMiddleware", ] # ─── URL / ASGI ───