236 lines
6.7 KiB
Markdown
236 lines
6.7 KiB
Markdown
# ERP 自动化数据采集 — 技术说明
|
||
|
||
> 目标系统:[腾一工业互联网平台](https://yunmes.tftykj.cn/#)
|
||
> 框架:DrissionPage(Python)
|
||
> 更新:2026-04-16
|
||
|
||
---
|
||
|
||
## 一、ERP 接口如何鉴权
|
||
|
||
### 1.1 基础机制
|
||
|
||
该 ERP 基于 **ASP.NET Boilerplate (ABP) 框架**,接口鉴权通过以下两种凭据组合完成:
|
||
|
||
| 凭据 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `auth_token` | Cookie / JWT | 登录后由服务端颁发,携带用户ID、角色、租户ID等声明 |
|
||
| `ASP.NET_SessionId` | Cookie | 服务端会话标识 |
|
||
| `Abp.TenantId` / `Abp.TenantCode` | Cookie | 多租户标识 |
|
||
| `x-sig` | 请求Header | 客户端动态生成的请求签名,每次请求不同 |
|
||
|
||
### 1.2 `auth_token` 解析(JWT)
|
||
|
||
登录成功后的 JWT 结构(Base64 解码 Payload):
|
||
|
||
```json
|
||
{
|
||
"nameid": "103",
|
||
"unique_name": ["DTCS", "DTCS"],
|
||
"role": "0099",
|
||
"http://www.aspnetboilerplate.com/identity/claims/tenantId": "18",
|
||
"iss": "gdtykj",
|
||
"aud": "TfTechApi",
|
||
"exp": 1776330877,
|
||
"nbf": 1776323677
|
||
}
|
||
```
|
||
|
||
该 Token 有效期约 **2 小时**(`exp - nbf = 7200s`)。
|
||
|
||
### 1.3 `x-sig` 签名
|
||
|
||
每次 POST 请求都携带一个动态值,例:
|
||
```
|
||
x-sig: QpE4yJ/TNqPJvIkYjww5QM.w/cn1wtipR2zQZSWlrF9L8
|
||
```
|
||
|
||
该签名由**浏览器端 JavaScript 在运行时生成**,算法未公开,无法离线复现。
|
||
⚠️ 这意味着**无法绕过浏览器直接用 `requests` 调用 API**。
|
||
|
||
### 1.4 典型 API 请求示例(BOM 查询)
|
||
|
||
```
|
||
POST https://yunmes.tftykj.cn/api/services/TfTechApi/Material/MaterialBom_SearchList_Proxy
|
||
Content-Type: application/x-www-form-urlencoded
|
||
x-sig: <动态签名>
|
||
Cookie: auth_token=<JWT>; Abp.TenantId=18; ...
|
||
|
||
page=1&rows=50&MaterialCode=&MaterialName=&...
|
||
```
|
||
|
||
---
|
||
|
||
## 二、我们的程序如何绕过 `x-sig` 获取数据
|
||
|
||
### 核心思路:让真实浏览器替我们发请求
|
||
|
||
由于 `x-sig` 无法离线生成,我们**不直接调用 API**,而是:
|
||
|
||
```
|
||
Python 控制真实 Chrome 浏览器
|
||
→ 浏览器完成登录(cookies 自动注入)
|
||
→ 浏览器跳转到目标页面(页面自动发起 API 请求)
|
||
→ Python 拦截浏览器网络层的原始响应
|
||
→ 提取响应 JSON,保存到本地
|
||
```
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ Python 程序 │
|
||
│ │
|
||
│ DrissionPage.listen.start() ←──────┐ │
|
||
│ ↓ │ │
|
||
│ page.refresh() → Chrome 发出请求 │ │
|
||
│ ↓ │ │
|
||
│ 服务器响应(含数据)│ │
|
||
│ ↓ │ │
|
||
│ packet = listen.wait() ────────────┘ │
|
||
│ ↓ │
|
||
│ packet.response.body → JSON 数据 │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
### Vue 表单填写的特殊处理
|
||
|
||
该 ERP 前端使用 **Vue.js 双向绑定**。普通的 `clear() + input()` 会留下残值导致登录失败。
|
||
我们使用 JS 原生 setter 强制清空后再模拟键盘输入:
|
||
|
||
```python
|
||
# 用 JS 原生 setter 清空(触发 Vue 响应式)
|
||
page.run_js("""
|
||
var el = arguments[0];
|
||
Object.getOwnPropertyDescriptor(
|
||
window.HTMLInputElement.prototype, 'value'
|
||
).set.call(el, '');
|
||
el.dispatchEvent(new Event('input', {bubbles: true}));
|
||
""", ele)
|
||
time.sleep(0.05) # 等 Vue 处理 clear 事件
|
||
ele.input(value) # 模拟键盘输入,触发 keydown/change
|
||
```
|
||
|
||
---
|
||
|
||
## 三、使用方式
|
||
|
||
### 3.1 环境准备
|
||
|
||
```bash
|
||
# 安装依赖
|
||
pip install DrissionPage python-dotenv
|
||
|
||
# 配置账号(自动模式使用,手动模式不需要)
|
||
cp .env.example .env # 或直接编辑 .env
|
||
```
|
||
|
||
`.env` 文件内容:
|
||
```env
|
||
ERP_URL=https://yunmes.tftykj.cn/#
|
||
ERP_TENANT=gddtsk
|
||
ERP_USERNAME=DTCS
|
||
ERP_PASSWORD=123456
|
||
```
|
||
|
||
> ⚠️ `.env` 已加入 `.gitignore`,不会被提交到代码仓库。
|
||
|
||
---
|
||
|
||
### 3.2 自动模式(适合生产部署)
|
||
|
||
从 `.env` 读取账号自动完成登录,无需人工干预。
|
||
|
||
```bash
|
||
python bom_query.py
|
||
```
|
||
|
||
**执行流程:**
|
||
1. 打开 Chrome,访问 ERP 登录页
|
||
2. 自动填写租户代码 / 账号 / 密码
|
||
3. 点击登录按钮,等待跳转(约 2s)
|
||
4. 导航到 BOM 表查询页,触发数据加载
|
||
5. 拦截 API 响应,保存 JSON 到 `output/` 目录
|
||
|
||
**典型日志:**
|
||
```
|
||
[15:58:21] ℹ️ BOM 查询启动 [自动登录]
|
||
[15:58:23] ℹ️ 填写登录信息...
|
||
[15:58:25] ✅ 登录成功(2s)
|
||
[15:58:43] ✅ 拦截成功 → HTTP 200
|
||
[15:58:43] ℹ️ 本页记录: 50 条 | 总计: 1494 条
|
||
[15:58:43] ✅ 已保存: output/bom_page1_20260416_155843.json
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 手动模式(适合涉密账号,不留存密码)
|
||
|
||
程序只负责打开浏览器,由**人工在浏览器中完成登录**,程序检测到登录成功后自动继续执行数据采集。
|
||
|
||
```bash
|
||
python bom_query.py --manual
|
||
```
|
||
|
||
**执行流程:**
|
||
1. 打开 Chrome,访问 ERP 登录页(字段为空,不填写任何内容)
|
||
2. 终端提示:`请在浏览器中手动输入账号并登录`
|
||
3. 用户在浏览器中自行输入账号、密码并点击登录
|
||
4. 程序检测到登录成功后,自动导航到 BOM 页面并采集数据
|
||
5. 采集完成,保存 JSON 到 `output/` 目录
|
||
|
||
**特点:**
|
||
- 密码不出现在任何配置文件或代码中
|
||
- 程序等待最长 **120 秒**供人工操作
|
||
- 每 10 秒打印一次进度提示
|
||
|
||
---
|
||
|
||
### 3.4 输出文件
|
||
|
||
数据保存在 `output/` 目录,文件名格式:
|
||
|
||
```
|
||
output/bom_page1_YYYYMMDD_HHMMSS.json
|
||
```
|
||
|
||
JSON 结构(ABP 标准响应):
|
||
```json
|
||
{
|
||
"result": {
|
||
"totalCount": 1494,
|
||
"items": [
|
||
{
|
||
"materialCode": "1ADAA00001",
|
||
"materialName": "DA400大锥度中走丝",
|
||
...
|
||
}
|
||
]
|
||
},
|
||
"success": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、项目文件结构
|
||
|
||
```
|
||
browser_login/
|
||
├── login.py # 核心登录模块(自动 / 手动 / 共用等待逻辑)
|
||
├── bom_query.py # BOM 数据采集(支持 --manual 参数)
|
||
├── .env # 账号配置(自动模式,不提交 git)
|
||
├── .gitignore # 忽略 .env 等敏感文件
|
||
├── output/ # 采集结果 JSON 文件
|
||
└── README.md # 本文档
|
||
```
|
||
|
||
---
|
||
|
||
## 五、已知限制
|
||
|
||
| 限制 | 说明 |
|
||
|---|---|
|
||
| 必须使用真实浏览器 | `x-sig` 由 JS 动态生成,无法离线复现 |
|
||
| 每次只获取 50 条 | 当前固定取第 1 页,翻页逻辑待开发 |
|
||
| JWT 有效期 ~2h | 长时间运行需重新登录 |
|
||
| 表格等待逻辑待优化 | 当前用刷新触发请求,偶发 WARN 提示 |
|