336 lines
13 KiB
Markdown
336 lines
13 KiB
Markdown
# xiaozhaoya.com 招聘接口说明
|
||
|
||
> 基础域名:`https://api.xiaozhaoya.com`
|
||
> 所有接口的响应体结构统一为 `{ "code": <int>, "message": <string>, "data": <string|null> }`。
|
||
> `data` 字段在成功时是一段 **AES 加密后再 Base64 编码的密文**,需解密后才能得到明文 JSON(解密方案与完整代码见文末第三节)。
|
||
|
||
## 一、接口总览
|
||
|
||
| # | 接口 | 方法 | 功能 | 是否需要授权 |
|
||
|---|------|------|------|-------------|
|
||
| 1 | `/api/recruitment/list` | GET | 招聘列表:分页返回岗位概要(含公司、行业、城市、投递方式等) | **部分**:未登录只能访问第 1 页;翻页(page ≥ 2)返回 401 |
|
||
| 2 | `/api/recruitment/detail?id={id}` | GET | 招聘详情:返回单条岗位的完整信息(含岗位职责、工作地址、薪资等) | **否**:可裸调。仅"招聘公告地址、投递方式"两个字段脱敏为"登陆后可查看" |
|
||
| 3 | `/api/recruitment/{id}/getUrl` | POST | 获取该岗位的**招聘公告原文地址**(`announcementLink`) | **是**:必须携带有效登录态,否则返回 401「请先登录」 |
|
||
|
||
### 关于 id 的重要说明
|
||
|
||
- **`id` 是自增 ID**:`recruitmentId` 随发布顺序连续递增(例如 41785、41786 … 41796),越大越新。可据此推断数据总量与遍历范围。
|
||
- **详情接口查询不存在的 id**:`/api/recruitment/detail?id={不存在的id}` 时,服务端返回 `code:500, message:"ok"`,且 **`data` 为 `null`**(没有可解密内容)。调用方应先判断 `data` 是否为 null,再进行解密。
|
||
- 特例:`id` 为负数时会返回 `401 请登录后继续浏览`。
|
||
|
||
---
|
||
|
||
## 二、请求示例、参数与返回值
|
||
|
||
### 接口 1:招聘列表 `GET /api/recruitment/list`
|
||
|
||
**请求参数(Query)**
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `page` | int | 是 | 页码,从 1 开始。未登录时仅 `page=1` 可用 |
|
||
| `size` | int | 是 | 每页条数。**未登录时服务端强制为 12**,传更大值(如 200)也只返回 12 条 |
|
||
| `sortRule` | string | 否 | 排序规则,如 `updateDate`(按更新时间倒序)。为默认值,去掉不影响结果 |
|
||
| `userId` | string | 否 | 登录用户 ID。登录态下随请求携带 |
|
||
|
||
**请求示例**
|
||
|
||
```bash
|
||
curl 'https://api.xiaozhaoya.com/api/recruitment/list?page=1&size=12&sortRule=updateDate' \
|
||
-H 'accept: application/json' \
|
||
-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36'
|
||
```
|
||
|
||
**返回值(`data` 解密后)**
|
||
|
||
```json
|
||
{
|
||
"total": "33872",
|
||
"data": [
|
||
{
|
||
"recruitmentId": 41796,
|
||
"updateDate": "2026-07-21",
|
||
"companyName": "同花顺-AIME顶尖人才计划",
|
||
"companyTypeName": "民企",
|
||
"fullName": "浙江核新同花顺网络信息股份有限公司",
|
||
"shortName": "同花顺",
|
||
"logoUrl": "https://xiaozhaoya.oss-cn-shanghai.aliyuncs.com/company/logo/....jpg",
|
||
"batchNameList": "秋招专场",
|
||
"industryName": "IT/互联网/游戏",
|
||
"jobCategoryNameList": "设计, 人工智能, 工程师",
|
||
"jobTitle": "多模态算法工程师;大模型算法工程师;...",
|
||
"educationLevelNameList": "硕士, 博士",
|
||
"graduationYearList": "2026, 2027",
|
||
"majorRequirements": "未明确",
|
||
"cityNameList": "杭州市",
|
||
"applicationDeadline": "尽快投递",
|
||
"sourceName": "offer雷达",
|
||
"announcementTitle": "同花顺AIME顶尖人才计划|全球招募",
|
||
"announcementLink": "/",
|
||
"applicationMethod": "https://campus.10jqka.com.cn/job/list?type=school",
|
||
"hasWrittenTest": "未明确",
|
||
"companyProfile": "国内领先的互联网金融信息服务商,...",
|
||
"isShow": true
|
||
}
|
||
// ... 共 12 条;jobContent、workAddress、salary、viewAmount 等字段在列表中为 null
|
||
],
|
||
"isMembershipValid": false,
|
||
"isLoggedIn": false
|
||
}
|
||
```
|
||
|
||
**说明**
|
||
|
||
- 列表中的 `applicationMethod`(投递方式)为**明文真实链接**;`announcementLink` 为 `/`(占位)。
|
||
- `jobContent`、`workAddress`、`salary`、`viewAmount`、`likeAmount`、`recruitmentCount` 等字段在列表接口中均为 `null`,需调用详情接口获取。
|
||
- 翻页未授权时的返回:`{ "code": 401, "message": "请登录后查看更多信息", "data": null }`。
|
||
|
||
---
|
||
|
||
### 接口 2:招聘详情 `GET /api/recruitment/detail`
|
||
|
||
**请求参数(Query)**
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `id` | int | 是 | 招聘 ID(自增)。不存在时 `data` 返回 null |
|
||
|
||
**请求示例**
|
||
|
||
```bash
|
||
curl 'https://api.xiaozhaoya.com/api/recruitment/detail?id=41794' \
|
||
-H 'accept: application/json' \
|
||
-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36'
|
||
```
|
||
|
||
**返回值(`data` 解密后)**
|
||
|
||
```json
|
||
{
|
||
"recruitmentId": 41794,
|
||
"updateDate": "2026-07-21",
|
||
"companyName": "网易游戏-互娱",
|
||
"companyTypeName": "民企",
|
||
"logoUrl": "https://xiaozhaoya.oss-cn-shanghai.aliyuncs.com/company/logo/....jpg",
|
||
"batchNameList": "秋招专场",
|
||
"industryName": "IT/互联网/游戏",
|
||
"jobCategoryNameList": "运营, 设计, IT技术, 人工智能, ...",
|
||
"jobTitle": "游戏策划类;游戏程序类;游戏艺术类;...",
|
||
"educationLevelNameList": "本科, 硕士, 博士",
|
||
"graduationYearList": "2027",
|
||
"cityNameList": "上海市, 杭州市, 广州市",
|
||
"announcementDate": "2026-07-21",
|
||
"applicationDeadline": "尽快投递",
|
||
"sourceName": "网易游戏互娱招聘",
|
||
"announcementTitle": "网易游戏(互娱)2027届校园招聘正式启动!",
|
||
"announcementLink": "登陆后可查看",
|
||
"applicationMethod": "登陆后可查看",
|
||
"hasWrittenTest": "有笔试",
|
||
"companyProfile": "网易游戏(互娱)是国内领先的游戏厂商,...",
|
||
"tip": "每位同学最多投递2个岗位,表现优秀可转正",
|
||
"tags": "大厂校招,多岗热招,福利丰厚",
|
||
"viewAmount": "10",
|
||
"likeAmount": "1",
|
||
"recruitmentCount": "若干",
|
||
"jobContent": "#### 游戏策划类\n- 负责游戏玩法规则设计...(Markdown 格式的完整岗位职责)",
|
||
"workAddress": "广州市天河区思成路1号,杭州市滨江区网商路399号,上海市静安区江场路1228弄",
|
||
"salary": "面议",
|
||
"isMembershipValid": false,
|
||
"isLoggedIn": false
|
||
}
|
||
```
|
||
|
||
**说明**
|
||
|
||
- 详情接口未登录也可获取几乎全部字段(岗位职责、工作地址、薪资等)。
|
||
- 仅 `announcementLink`(招聘公告地址)与 `applicationMethod`(投递方式)两个字段被脱敏为 `"登陆后可查看"`。
|
||
- **不存在的 id** 的返回:`{ "code": 500, "message": "ok", "data": null }`。
|
||
|
||
---
|
||
|
||
### 接口 3:获取招聘公告地址 `POST /api/recruitment/{id}/getUrl`
|
||
|
||
功能:返回该岗位的**招聘公告原文地址**(即详情中被脱敏的 `announcementLink`)。
|
||
|
||
**路径参数**
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `id` | int | 是 | 招聘 ID,写在 URL 路径中 |
|
||
|
||
**请求体**:空对象 `{}`(`Content-Type: application/json`)。
|
||
|
||
**请求示例(需登录态)**
|
||
|
||
```bash
|
||
curl -X POST 'https://api.xiaozhaoya.com/api/recruitment/41794/getUrl' \
|
||
-H 'accept: application/json' \
|
||
-H 'content-type: application/json' \
|
||
-H 'authorization: <有效的登录 token>' \
|
||
-H 'origin: https://www.xiaozhaoya.com' \
|
||
-H 'referer: https://www.xiaozhaoya.com/' \
|
||
-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36' \
|
||
--data '{}'
|
||
```
|
||
|
||
**返回值**
|
||
|
||
- 未授权(无有效登录态):
|
||
|
||
```json
|
||
{ "code": 401, "message": "请先登录", "data": null }
|
||
```
|
||
|
||
- 授权成功:`data` 为加密字符串,解密后为招聘公告地址(结构以实际返回为准)。
|
||
|
||
**授权说明**:登录 token 通过 `authorization` 请求头传递(站点使用 Sa-Token 框架,响应头 `access-control-expose-headers: satoken`)。token 存在有效期,失效后即便请求头格式正确,服务端仍判定为未登录(列表接口返回中 `isLoggedIn` 为 `false`)。
|
||
|
||
---
|
||
|
||
## 三、`data` 字段解密方法
|
||
|
||
站点对响应体的 `data` 字段做了对称加密,须解密后才能得到明文 JSON。
|
||
|
||
**加密方案**
|
||
|
||
| 项目 | 值 |
|
||
|------|-----|
|
||
| 算法 | AES-128 |
|
||
| 模式 | CBC |
|
||
| 填充 | PKCS#7(CryptoJS 兼容) |
|
||
| Key | `version 2.2.1bak`(UTF-8,16 字节) |
|
||
| IV | `version 2.2.1bak`(UTF-8,16 字节,与 Key 相同) |
|
||
| 传输编码 | Base64 |
|
||
|
||
**解密流程**:Base64 解码 → AES-128-CBC 解密 → 去除 PKCS#7 填充 → UTF-8 解码 → JSON 解析。
|
||
|
||
**依赖**
|
||
|
||
```
|
||
cryptography>=41
|
||
```
|
||
|
||
**解密代码(完整源码)**
|
||
|
||
```python
|
||
"""Decode the encrypted payload returned by xiaozhaoya.com APIs.
|
||
|
||
The site currently uses AES-128-CBC with PKCS#7 padding. The key and IV are
|
||
part of the public frontend bundle, so keep them configurable in case the site
|
||
rotates them later.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import json
|
||
from collections.abc import Mapping
|
||
from typing import Any
|
||
|
||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||
|
||
|
||
DEFAULT_KEY = "version 2.2.1bak"
|
||
DEFAULT_IV = "version 2.2.1bak"
|
||
|
||
|
||
def decode_ciphertext(
|
||
ciphertext: str,
|
||
*,
|
||
key: str = DEFAULT_KEY,
|
||
iv: str = DEFAULT_IV,
|
||
parse_json: bool = True,
|
||
) -> Any:
|
||
"""Decrypt one Base64 payload and optionally parse its UTF-8 JSON.
|
||
|
||
Args:
|
||
ciphertext: The API's Base64-encoded ``data`` value.
|
||
key: AES key encoded as UTF-8 (16 bytes by default).
|
||
iv: CBC initialization vector encoded as UTF-8 (16 bytes by default).
|
||
parse_json: Return the decoded JSON value when true; otherwise return
|
||
the decoded UTF-8 string.
|
||
"""
|
||
|
||
if not isinstance(ciphertext, str) or not ciphertext.strip():
|
||
raise ValueError("ciphertext must be a non-empty Base64 string")
|
||
|
||
key_bytes = key.encode("utf-8")
|
||
iv_bytes = iv.encode("utf-8")
|
||
if len(key_bytes) not in (16, 24, 32):
|
||
raise ValueError("AES key must be 16, 24, or 32 UTF-8 bytes")
|
||
if len(iv_bytes) != 16:
|
||
raise ValueError("AES-CBC IV must be 16 UTF-8 bytes")
|
||
|
||
try:
|
||
encrypted = base64.b64decode(ciphertext, validate=True)
|
||
except ValueError as exc:
|
||
raise ValueError("ciphertext is not valid Base64") from exc
|
||
|
||
if not encrypted or len(encrypted) % 16:
|
||
raise ValueError("decoded ciphertext length must be a non-zero AES block multiple")
|
||
|
||
decryptor = Cipher(
|
||
algorithms.AES(key_bytes),
|
||
modes.CBC(iv_bytes),
|
||
).decryptor()
|
||
padded = decryptor.update(encrypted) + decryptor.finalize()
|
||
|
||
# CryptoJS.pad.Pkcs7 compatibility.
|
||
padding_length = padded[-1]
|
||
if not 1 <= padding_length <= 16 or padded[-padding_length:] != bytes([padding_length]) * padding_length:
|
||
raise ValueError("invalid PKCS#7 padding")
|
||
plaintext = padded[:-padding_length].decode("utf-8")
|
||
return json.loads(plaintext) if parse_json else plaintext
|
||
|
||
|
||
def decode_api_data(
|
||
response: Mapping[str, Any],
|
||
*,
|
||
key: str = DEFAULT_KEY,
|
||
iv: str = DEFAULT_IV,
|
||
) -> Any:
|
||
"""Extract and decode an API response's encrypted ``data`` field."""
|
||
|
||
try:
|
||
ciphertext = response["data"]
|
||
except (KeyError, TypeError) as exc:
|
||
raise ValueError("API response does not contain a data field") from exc
|
||
return decode_ciphertext(ciphertext, key=key, iv=iv)
|
||
```
|
||
|
||
上面提供两个函数:
|
||
|
||
- `decode_ciphertext(ciphertext, *, key=..., iv=..., parse_json=True)`:解密单个 Base64 密文字符串。
|
||
- `decode_api_data(response, *, key=..., iv=...)`:从完整响应对象中取出 `data` 字段并解密。
|
||
|
||
**调用示例**
|
||
|
||
```python
|
||
import json
|
||
import urllib.request
|
||
import gzip
|
||
# 将上面的解密函数保存为 decoder.py 后即可导入
|
||
from decoder import decode_api_data
|
||
|
||
url = "https://api.xiaozhaoya.com/api/recruitment/detail?id=41794"
|
||
headers = {
|
||
"accept": "application/json",
|
||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36",
|
||
}
|
||
|
||
resp = urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=30)
|
||
raw = resp.read()
|
||
if resp.headers.get("content-encoding") == "gzip":
|
||
raw = gzip.decompress(raw)
|
||
|
||
payload = json.loads(raw.decode("utf-8"))
|
||
# 注意:先判断 data 是否为 null(如 id 不存在、未授权等场景)
|
||
if isinstance(payload.get("data"), str):
|
||
data = decode_api_data(payload)
|
||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||
else:
|
||
print("无数据:", payload.get("code"), payload.get("message"))
|
||
```
|
||
|
||
> 备注:Key/IV 硬编码在前端 JS 中且公开,此加密本质是轻量混淆/反爬,并非真正的机密性保护。若站点后续轮换密钥,`decode_ciphertext` 支持通过 `key`/`iv` 参数覆盖默认值。
|