简道云fastapi

This commit is contained in:
z66
2025-11-07 17:48:49 +08:00
commit 073f0646a1
30 changed files with 5933 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="fastapi_app" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>
+225
View File
@@ -0,0 +1,225 @@
# FastAPI项目架构说明文档
## 项目架构概览
本项目采用模块化、可扩展的架构设计,主要包含以下核心组件:
```
fastapi_app/
├── app/
│ ├── core/ # 核心模块(可选)
│ │ ├── header_manager.py # 请求头管理器
│ │ ├── module_registry.py # 模块注册表
│ │ └── __init__.py # 核心模块导出
│ ├── module/ # 业务模块
│ │ ├── F6_Plugin_module.py
│ │ ├── module.py
│ │ └── other_module.py
│ ├── tasks/ # 后台任务模块
│ │ ├── common.py
│ │ ├── brand_tasks.py
│ │ ├── delete_tasks.py
│ │ └── customer_tasks.py
│ ├── utils/ # 工具模块
│ │ └── app_tools.py
│ └── api.py # API接口
├── main.py # FastAPI应用入口
└── requirements_fastapi.txt # 依赖清单
```
## 核心组件说明
### 1. 请求头管理器(HeaderManager
统一管理不同模块的请求头配置。
预定义配置:
- `default`: 默认请求头
- `f6_login`: F6系统登录请求头
- `jiandaoyun_api`: 简道云API请求头
使用示例:
```python
from app.core.header_manager import HeaderManager
# 获取默认请求头
headers = HeaderManager.get_headers()
# 获取F6登录请求头
headers = HeaderManager.get_headers('f6_login')
# 自定义请求头
headers = HeaderManager.get_headers(
'default',
referer='https://example.com',
custom_headers={'X-Custom-Header': 'value'}
)
# 注册新模块的请求头
from app.core.header_manager import HeaderConfig
custom_config = HeaderConfig(
referer='https://new-module.com',
user_agent='Custom Agent'
)
HeaderManager.register_module_headers('new_module', custom_config)
```
### 2. 模块注册表(ModuleRegistry
统一注册和管理所有功能模块和操作(可选使用,主要用于请求头管理)。
注册操作:
```python
from app.core import core_manager
core_manager.register_action(
action_name='my_action',
handler=my_handler_function,
module_name='my_module',
description='操作描述',
requires_auth=True,
header_module='default'
)
```
获取操作:
```python
action_config = core_manager.registry.get_action('my_action')
if action_config:
result = action_config.handler(data)
```
## 添加新功能模块
### 步骤1:创建模块文件
`app/module/` 目录下创建新模块文件,例如 `new_module.py`
```python
class NewModule:
def my_action(self, data):
"""即刻执行的操作"""
# 处理逻辑
return {'msg': '执行成功'}
def my_background_action(self, data):
"""后台执行的操作"""
# 这个函数会启动后台线程
from app import back_ground_tasks
import threading
thread = threading.Thread(
target=back_ground_tasks.my_background_task,
args=(data,)
)
thread.start()
return {'msg': '正在执行中'}
```
### 步骤2:创建后台任务(如果需要)
`app/tasks/` 目录下创建任务文件,例如 `new_tasks.py`
```python
from app.tasks.common import update_jiandaoyun, approve_workflow
def my_background_task(data):
"""后台任务函数"""
# 执行耗时操作
result = "执行结果"
# 更新简道云表单
msg = update_jiandaoyun(data, result)
# 提交工作流
if msg.get('msg'):
approve_workflow(data)
```
### 步骤3:在 main.py 中注册操作
`main.py``get_action_map()` 函数中添加:
```python
def get_action_map() -> dict:
# ... 现有代码 ...
new_module = NewModule()
return {
# ... 现有操作 ...
'my_action': new_module.my_action,
'my_background_action': new_module.my_background_action,
}
```
### 步骤4:注册新的请求头(如果需要)
`app/core/header_manager.py` 中添加:
```python
# 在 HeaderManager 类中添加
NEW_MODULE_HEADERS = HeaderConfig(
referer='https://new-module.com',
user_agent='Custom User Agent',
custom_headers={'X-API-Key': 'your-api-key'}
)
_module_headers: Dict[str, HeaderConfig] = {
# ... 现有配置 ...
'new_module': NEW_MODULE_HEADERS,
}
```
## 执行方式说明
### 调度机制
项目使用原有的任务队列调度方式:
1. **所有操作**都通过 `app_tools.enqueue_task()` 放入任务队列
2. **同步等待**结果通过 `response_queue.get()` 获取
3. **后台任务**在模块函数内部使用 `threading.Thread` 启动线程
### 即刻执行 vs 后台执行
- **即刻执行**:函数直接返回结果,通过任务队列同步执行
- **后台执行**:函数内部启动线程执行耗时操作,立即返回"正在执行中"的提示
所有操作都通过统一的任务队列处理,后台任务由模块函数内部管理。
## API接口
### POST /webhook
统一处理所有请求的主接口。
请求头:
- `Action`: 操作名称
- `Check`: (可选)F6_Plugin 操作时的检查标志
请求体:
```json
{
"api_key": "...",
"entry_id": "...",
"data_id": "...",
"Action": "..." // F6_Plugin 操作时需要
}
```
## 最佳实践
1. **模块职责单一**:每个模块只负责一类功能
2. **统一注册**:所有操作都在 `main.py``get_action_map()` 中统一注册
3. **请求头管理**:使用 HeaderManager 统一管理请求头,避免硬编码(可选)
4. **错误处理**:在模块函数中统一处理异常
5. **日志记录**:使用统一的日志记录器 `logging.getLogger('app')`
6. **向后兼容**:保持 `app.back_ground_tasks` 的向后兼容性
7. **任务队列**:所有操作都通过 `app_tools.enqueue_task()` 放入任务队列统一处理
## 后续扩展建议
1. **添加中间件**:用于请求验证、日志记录等
2. **添加配置管理**:使用配置文件管理不同环境的设置
3. **添加任务队列**:使用 Celery 或 RQ 管理后台任务
4. **添加API文档**:使用 FastAPI 的自动文档功能
5. **添加单元测试**:为每个模块添加测试用例
+506
View File
@@ -0,0 +1,506 @@
# Bug 报告
## 严重程度说明
- 🔴 **严重**: 可能导致程序崩溃或数据丢失
- 🟡 **中等**: 可能导致功能异常或错误处理不当
- 🟢 **轻微**: 代码质量问题,不影响功能但需要改进
---
## 1. api.py (根目录) - 错误日志键名错误
### 位置
- 第540行:`entry_data_batch_delete` 方法
- 第654行:`workflow_task_hand_over` 方法
- 第701行:`get_upload_token` 方法
- 第745行:`upload_file` 方法
### 问题描述
🟡 **中等** - 在错误日志中使用了错误的键名 `data['data_list']`,但这些方法中可能不存在该键。
### 代码示例
```python
# 第540行 - entry_data_batch_delete
error_task_logger.error(
f"任务 {data['data_list'][start_index:end_index]} 连续{max_retries}次请求失败,放弃此次请求。")
# 应该是 data['data_ids']
# 第654行 - workflow_task_hand_over
error_task_logger.error(
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
# 该方法没有 data_list 键
# 第701行 - get_upload_token
error_task_logger.error(
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
# 该方法没有 data_list 键
# 第745行 - upload_file
error_task_logger.error(
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
# 该方法没有 data_list 键
```
### 修复建议
使用正确的键名或使用 `data.get()` 方法安全访问。
---
## 2. main.py - Handler 调用问题
### 位置
第76行和第80行
### 问题描述
🟡 **中等** - 当 `action_map.get()` 返回默认的 lambda 函数时,handler 是一个函数对象,但代码中直接将其传递给 `enqueue_task`,这应该是正确的。但如果 handler 是 lambda,需要确保它能被正确调用。
### 代码示例
```python
# 第76行
handler = action_map.get(sub_action, lambda x: {'msg': '未执行'})
# 第80行
handler = action_map.get(action, lambda x: {'msg': '未知的操作'})
```
### 修复建议
确保所有 handler 都能正确处理数据参数。如果 handler 可能返回 None,需要添加检查。
---
## 3. main.py - 队列阻塞风险
### 位置
第86行
### 问题描述
🟡 **中等** - 使用 `response_queue.get()` 会无限期阻塞,如果任务处理线程出现问题,可能导致请求永远挂起。
### 代码示例
```python
result = await anyio.to_thread.run_sync(response_queue.get)
```
### 修复建议
添加超时机制或使用 `response_queue.get(timeout=...)`
---
## 4. app/api.py - 数组访问未检查
### 位置
第712-713行:`get_upload_token` 方法
### 问题描述
🔴 **严重** - 直接访问 `res_j['token_and_url_list'][0]` 可能导致 KeyError 或 IndexError。
### 代码示例
```python
res_j = res.json()
upload_url = res_j['token_and_url_list'][0]['url']
upload_token = res_j['token_and_url_list'][0]['token']
```
### 修复建议
添加检查:
```python
token_list = res_j.get('token_and_url_list', [])
if not token_list:
raise ValueError("未获取到上传凭证")
upload_url = token_list[0].get('url')
upload_token = token_list[0].get('token')
```
---
## 5. app/module/F6_Plugin_module.py - 数组访问未检查
### 位置
第40行:`accept_file` 方法
### 问题描述
🔴 **严重** - 直接访问 `data['data']['附件'][0]['url']` 可能导致 KeyError 或 IndexError。
### 代码示例
```python
url = data['data']['附件'][0]['url']
```
### 修复建议
添加检查:
```python
attachments = data.get('data', {}).get('附件', [])
if not attachments:
return None, data
url = attachments[0].get('url')
```
---
## 6. app/module/F6_Plugin_module.py - 键访问未检查
### 位置
第96行:`check_file` 方法
### 问题描述
🔴 **严重** - 直接访问 `data1['data']['Action(隐藏)']` 可能导致 KeyError。
### 代码示例
```python
action = data1['data']['Action(隐藏)']
```
### 修复建议
使用安全访问:
```python
action = data1.get('data', {}).get('Action(隐藏)')
if not action:
return {'msg': '缺少Action字段'}
```
---
## 7. app/module/module.py - 临时文件未清理
### 位置
第21-31行:`get_captcha` 方法
### 问题描述
🟢 **轻微** - 创建了临时文件 `captcha.png``preprocessed_captcha.png`,但没有清理。
### 代码示例
```python
with open('captcha.png', 'wb') as f:
f.write(response.content)
# ... 处理文件 ...
image.save('preprocessed_captcha.png')
```
### 修复建议
使用临时文件或处理完后删除:
```python
import tempfile
import os
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
tmp.write(response.content)
tmp_path = tmp.name
try:
# 处理文件
...
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
```
---
## 8. app/module/module.py - group_id 可能为空
### 位置
第65-73行:`login_in` 方法
### 问题描述
🟡 **中等** - 如果找不到匹配的 `company_name``group_id` 为空字符串,后续请求可能失败。
### 代码示例
```python
group_id = ''
for group in res_json.get('data', []):
if group["groupName"] == company_name:
group_id = group.get("groupId")
token = quote(res_json['token'])
url = (f'https://yunxiu.f6car.cn/kzf6/user/loginAfterChooseGroup?'
f'token={token}&groupId={group_id}&macAddress=')
```
### 修复建议
添加检查:
```python
if not group_id:
logger.error(f"未找到公司名称: {company_name}")
return None
```
---
## 9. app/module/module.py - 时间判断逻辑错误
### 位置
第119行:`delete_customer_background` 方法(在 delete_tasks.py 中)
第246行:`delete_car_background` 方法(在 delete_tasks.py 中)
### 问题描述
🟡 **中等** - 时间判断逻辑错误:`if 20 <= now.hour <= 8:` 永远不会为 True(20 不可能小于等于 8)。
### 代码示例
```python
now = datetime.now()
if 20 <= now.hour <= 8:
time.sleep(1)
else:
time.sleep(3)
```
### 修复建议
应该是:
```python
now = datetime.now()
if 8 <= now.hour <= 20: # 8点到20点之间
time.sleep(3.5)
else: # 其他时间
time.sleep(1.5)
```
---
## 10. app/tasks/common.py - 数组访问未检查
### 位置
第56行:`approve_workflow` 方法
### 问题描述
🔴 **严重** - 直接访问 `json['tasks']``json['tasks'][0]` 可能导致 KeyError 或 IndexError。
### 代码示例
```python
for task in json['tasks']:
if task['status'] == 0:
username = task['assignee']['username']
instance_id = task['instance_id']
task_id = task['task_id']
```
### 修复建议
添加检查:
```python
tasks = json.get('tasks', [])
if not tasks:
logger.error("未找到待处理任务")
return
for task in tasks:
if task.get('status') == 0:
assignee = task.get('assignee', {})
username = assignee.get('username')
instance_id = task.get('instance_id')
task_id = task.get('task_id')
if username and instance_id and task_id:
break
```
---
## 11. app/tasks/delete_tasks.py - 数组访问未检查
### 位置
第62行:`delete_customer_background` 方法
第154行:`delete_car_background` 方法
### 问题描述
🔴 **严重** - 直接访问 `org_res.json().get("data").get("list")[0]` 可能导致 IndexError。
### 代码示例
```python
operate_org_id = org_res.json().get("data").get("list")[0].get("orgId")
```
### 修复建议
添加检查:
```python
org_data = org_res.json().get("data", {}).get("list", [])
if not org_data:
logger.error("未获取到门店信息")
return
operate_org_id = org_data[0].get("orgId")
```
---
## 12. app/tasks/customer_tasks.py - 重试逻辑错误
### 位置
第52-67行:`modify_customer_info_background` 方法
### 问题描述
🟡 **中等** - `retry_count` 在循环外部初始化,导致每次页面请求都使用相同的重试计数,而不是每页独立重试。
### 代码示例
```python
retry_count = 0
for page in range(1, total_pages + 1):
while retry_count < max_retries:
# ...
if response.status_code == 200:
break
else:
retry_count += 1
```
### 修复建议
`retry_count` 移到循环内部:
```python
for page in range(1, total_pages + 1):
retry_count = 0
while retry_count < max_retries:
# ...
```
---
## 13. app/tasks/delete_tasks.py - 条件判断错误
### 位置
第115-116行:`delete_customer_background` 方法
### 问题描述
🟡 **中等** - `if success + fail < len(json_data):` 这个条件判断逻辑有问题,应该检查是否还有未处理的项。
### 代码示例
```python
if success + fail < len(json_data):
continue
```
### 修复建议
这个条件判断似乎是想检查是否处理完所有项,但逻辑不正确。应该移除或修正。
---
## 14. app/tasks/delete_tasks.py - 变量作用域问题
### 位置
第215行:`delete_car_background` 方法
### 问题描述
🟡 **中等** - 在循环外部使用了 `page` 变量,但 `page` 只在循环内部定义。
### 代码示例
```python
for page in range(1, all_page + 1):
# ...
for item in items:
# ...
if not car_id or not customer_id:
logger.info(f"页码 {page} 中缺少必要的ID信息") # page 在这里可用
# ...
# ...
logger.error(f"删除失败: 页码 {page}, ...") # page 在这里可用
```
实际上这个不是bug`page` 在循环内部是可用的。但如果在循环外部使用就会有问题。
---
## 15. api.py (根目录) - 文件资源泄漏
### 位置
第721-747行:`upload_file` 方法
### 问题描述
🔴 **严重** - 文件在循环外部打开,如果循环中发生异常或提前返回,文件可能不会被关闭,导致资源泄漏。
### 代码示例
```python
f = open(file_path, 'rb')
files = {"file": f}
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10)
# ...
if res.status_code == 200:
return data_get # 提前返回,文件未关闭!
# ...
except requests.exceptions.RequestException as e:
# 如果异常发生,文件可能不会被关闭
# ...
f.close() # 只有在循环结束后才会执行
```
### 修复建议
使用 `with` 语句确保文件总是被关闭:
```python
retries = 0
result = None
while retries <= max_retries:
try:
with open(file_path, 'rb') as f:
files = {"file": f}
res = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
return data_get
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_task_logger.error(f"上传文件失败,已重试{max_retries}")
return None
```
---
## 16. api.py (根目录) - 重试时文件指针问题
### 位置
第721-747行:`upload_file` 方法
### 问题描述
🟡 **中等** - 文件在循环外部打开,重试时文件指针已经移动到文件末尾,后续重试会读取空内容。
### 代码示例
```python
f = open(file_path, 'rb')
files = {"file": f}
retries = 0
while retries <= max_retries:
res = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10)
# 第一次请求后,文件指针已经移动到末尾
# 重试时会读取空内容
```
### 修复建议
每次重试时重新打开文件,或使用 `with` 语句在每次循环中打开文件。
---
## 总结
### 按严重程度分类
**严重 (🔴)** - 需要立即修复:
1. app/api.py - 数组访问未检查(get_upload_token
2. app/module/F6_Plugin_module.py - 数组访问未检查(accept_file
3. app/module/F6_Plugin_module.py - 键访问未检查(check_file
4. app/tasks/common.py - 数组访问未检查(approve_workflow
5. app/tasks/delete_tasks.py - 数组访问未检查(两处)
6. api.py (根目录) - 文件资源泄漏(upload_file
**中等 (🟡)** - 建议尽快修复:
1. api.py - 错误日志键名错误(多处)
2. api.py - 重试时文件指针问题(upload_file
3. main.py - Handler 调用和队列阻塞问题
4. app/module/module.py - group_id 可能为空
5. app/module/module.py - 时间判断逻辑错误(两处)
6. app/tasks/customer_tasks.py - 重试逻辑错误
7. app/tasks/delete_tasks.py - 条件判断错误
**轻微 (🟢)** - 代码质量改进:
1. app/module/module.py - 临时文件未清理
### 建议的修复优先级
1. **立即修复**:所有严重级别的bug,特别是可能导致程序崩溃的数组访问问题
2. **尽快修复**:中等级别的bug,特别是逻辑错误和错误处理问题
3. **代码改进**:轻微级别的bug,在时间允许时进行改进
+710
View File
@@ -0,0 +1,710 @@
# FastAPI 学习文档
## 📚 目录
1. [FastAPI 简介](#fastapi-简介)
2. [Flask vs FastAPI 对比](#flask-vs-fastapi-对比)
3. [FastAPI 核心概念](#fastapi-核心概念)
4. [项目中的 FastAPI 使用](#项目中的-fastapi-使用)
5. [学习路径建议](#学习路径建议)
6. [常见问题解答](#常见问题解答)
---
## FastAPI 简介
### 什么是 FastAPI
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,基于 Python 3.6+ 的类型提示(type hints)。
### 主要特点
1. **高性能**:与 NodeJS 和 Go 相当,是最快的 Python 框架之一
2. **快速开发**:开发速度提升约 200% 到 300%
3. **自动文档**:自动生成交互式 API 文档(Swagger UI
4. **类型提示**:基于 Python 类型提示,提供更好的 IDE 支持
5. **异步支持**:原生支持 async/await
6. **数据验证**:自动进行请求和响应数据验证
---
## Flask vs FastAPI 对比
### 1. 基本语法对比
#### Flask 写法
```python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
data = request.get_json()
return jsonify({'msg': 'success'})
```
#### FastAPI 写法
```python
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
app = FastAPI()
class RequestData(BaseModel):
name: str
age: int
@app.post("/webhook")
async def webhook(data: RequestData):
return JSONResponse({'msg': 'success'})
```
### 2. 主要区别
| 特性 | Flask | FastAPI |
|------|-------|---------|
| **异步支持** | 需要额外库(Flask-AsyncIO | 原生支持 async/await |
| **类型验证** | 手动验证 | 自动验证(基于 Pydantic |
| **API 文档** | 需要手动编写 | 自动生成(Swagger/OpenAPI |
| **性能** | 中等 | 高性能(接近 NodeJS |
| **依赖注入** | 需要额外库 | 内置支持 |
| **数据验证** | 手动处理 | 自动验证和转换 |
### 3. 请求处理对比
#### Flask
```python
from flask import request
@app.route('/api', methods=['POST'])
def api():
# 手动获取 JSON 数据
data = request.get_json()
# 手动验证
if not data or 'name' not in data:
return jsonify({'error': '缺少 name 字段'}), 400
return jsonify({'result': data['name']})
```
#### FastAPI
```python
from pydantic import BaseModel
class Item(BaseModel):
name: str
age: int
@app.post("/api")
async def api(item: Item):
# 自动验证,如果 name 缺失会自动返回 422 错误
return {'result': item.name}
```
---
## FastAPI 核心概念
### 1. 应用实例(FastAPI App
```python
from fastapi import FastAPI
# 创建 FastAPI 应用实例
app = FastAPI(
title="简道云FastAPI服务", # API 文档标题
description="简道云 API 服务", # API 描述
version="1.0.0" # 版本号
)
```
**项目中的使用**`main.py`):
```python
app = FastAPI(title="简道云FastAPI服务")
```
### 2. 路由装饰器(Route Decorators
FastAPI 使用装饰器定义路由,类似于 Flask:
```python
@app.get("/") # GET 请求
@app.post("/") # POST 请求
@app.put("/") # PUT 请求
@app.delete("/") # DELETE 请求
```
**项目中的使用**`main.py`):
```python
@app.post("/webhook")
async def webhook(request: Request):
# 处理逻辑
pass
```
### 3. 请求对象(Request
FastAPI 的 `Request` 对象提供了访问请求信息的方法:
```python
from fastapi import Request
@app.post("/webhook")
async def webhook(request: Request):
# 获取 JSON 数据
data = await request.json()
# 获取请求头
headers = request.headers
# 获取查询参数
query_params = request.query_params
# 获取路径参数
path_params = request.path_params
```
**项目中的使用**`main.py`):
```python
@app.post("/webhook")
async def webhook(request: Request):
# 获取请求数据
data = await request.json()
header = request.headers
# 解码请求头
decoded_header = app_tools.decode_headers(header)
```
### 4. 响应对象(Response
FastAPI 提供了多种响应类型:
```python
from fastapi.responses import JSONResponse, HTMLResponse, PlainTextResponse
@app.post("/api")
async def api():
# JSON 响应
return JSONResponse({'msg': 'success'})
# 或者直接返回字典(FastAPI 会自动转换为 JSON
return {'msg': 'success'}
```
**项目中的使用**`main.py`):
```python
return JSONResponse(result)
```
### 5. 异步函数(Async Functions
FastAPI 支持异步函数,使用 `async``await`
```python
@app.post("/webhook")
async def webhook(request: Request):
# 异步获取 JSON 数据
data = await request.json()
# 异步调用其他函数
result = await some_async_function(data)
return result
```
**项目中的使用**`main.py`):
```python
@app.post("/webhook")
async def webhook(request: Request):
data = await request.json() # 异步获取数据
# ...
result = await anyio.to_thread.run_sync(response_queue.get) # 异步执行同步函数
```
### 6. 生命周期事件(Lifecycle Events
FastAPI 提供了应用启动和关闭事件:
```python
@app.on_event("startup")
async def startup_event():
"""应用启动时执行"""
print("应用启动")
@app.on_event("shutdown")
async def shutdown_event():
"""应用关闭时执行"""
print("应用关闭")
```
**项目中的使用**`main.py`):
```python
@app.on_event("startup")
def on_startup():
"""应用启动时初始化"""
app.state.app_tools = AppTools(Config)
app.state.logger = setup_global_logger(Config)
app.state.f6_module = F6Module()
app.state.f6_plugin_module = F6PluginModule()
app.state.other_module = OtherPluginModule()
```
### 7. 应用状态(Application State
FastAPI 允许在应用实例上存储状态:
```python
# 设置状态
app.state.my_data = "some value"
# 获取状态
my_data = app.state.my_data
```
**项目中的使用**`main.py`):
```python
# 在启动时设置
app.state.app_tools = AppTools(Config)
app.state.logger = setup_global_logger(Config)
# 在路由中使用
logger = app.state.logger
app_tools: AppTools = app.state.app_tools
```
### 8. 数据验证(Pydantic Models
FastAPI 使用 Pydantic 进行数据验证:
```python
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
email: str = None # 可选字段
@app.post("/users")
async def create_user(user: User):
# user 已经自动验证和转换
return {"name": user.name, "age": user.age}
```
**项目中的潜在使用**(可以改进):
```python
# 可以定义请求模型
class WebhookRequest(BaseModel):
api_key: str
entry_id: str
data_id: str
Action: str = None # 可选字段
@app.post("/webhook")
async def webhook(request: Request, data: WebhookRequest):
# data 已经自动验证
pass
```
---
## 项目中的 FastAPI 使用
### 1. 项目结构分析
```
fastapi_app/
├── main.py # FastAPI 应用入口
├── api.py # API 工具类(简道云 API 封装)
├── app/
│ ├── api.py # API 模块(简道云 API 封装)
│ ├── config.py # 配置管理
│ ├── module/ # 业务模块
│ ├── tasks/ # 后台任务
│ └── utils/ # 工具函数
└── requirements.txt # 依赖列表
```
### 2. main.py 详解
让我们逐步分析 `main.py` 文件:
#### 2.1 导入和初始化
```python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import json
import anyio
from app.utils.app_tools import AppTools, setup_global_logger
from app.module.F6_Plugin_module import F6PluginModule
from app.module.module import F6Module
from app.module.other_module import OtherPluginModule
from app.config import Config
# 创建 FastAPI 应用实例
app = FastAPI(title="简道云FastAPI服务")
```
**说明**
- `FastAPI`:主应用类
- `Request`:请求对象,用于访问请求信息
- `JSONResponse`JSON 响应类
#### 2.2 启动事件
```python
@app.on_event("startup")
def on_startup():
"""应用启动时初始化"""
app.state.app_tools = AppTools(Config)
app.state.logger = setup_global_logger(Config)
app.state.f6_module = F6Module()
app.state.f6_plugin_module = F6PluginModule()
app.state.other_module = OtherPluginModule()
```
**说明**
- `@app.on_event("startup")`:应用启动时执行
- `app.state`:存储应用级别的状态
- 初始化工具类、日志记录器和业务模块
#### 2.3 路由处理
```python
@app.post("/webhook")
async def webhook(request: Request):
"""
接受前端请求后将任务放入消息队列
Returns:
any: 返回任务处理的结果
"""
logger = app.state.logger
app_tools: AppTools = app.state.app_tools
# 获取请求数据
data = await request.json()
header = request.headers
# 解码请求头
decoded_header = app_tools.decode_headers(header)
# 获取操作映射表
action_map = get_action_map()
action = decoded_header.get('Action')
# 处理逻辑...
# 将任务放入消息队列
response_queue = app_tools.enqueue_task(handler, data)
# 等待任务处理结果
result = await anyio.to_thread.run_sync(response_queue.get)
logger.info(json.dumps(result, ensure_ascii=False, indent=4))
return JSONResponse(result)
```
**关键点**
1. `async def`:异步函数定义
2. `await request.json()`:异步获取 JSON 数据
3. `await anyio.to_thread.run_sync()`:在线程池中执行同步函数
4. `JSONResponse`:返回 JSON 响应
### 3. 任务队列机制
项目使用自定义的任务队列来处理请求:
```python
# 在 app_tools.py 中
class AppTools:
def __init__(self, config):
self.task_queue = Queue()
self._start_task_thread()
def enqueue_task(self, handler, data):
response_queue = Queue()
self.task_queue.put({
'handler': handler,
'data': data,
'response': response_queue
})
return response_queue
```
**工作流程**
1. 请求到达 `/webhook` 端点
2. 将任务放入队列
3. 后台线程处理任务
4. 等待结果返回
### 4. 操作映射机制
项目使用操作映射表来路由不同的操作:
```python
def get_action_map() -> dict:
"""获取操作映射表"""
f6_module = app.state.f6_module
f6_plugin_module = app.state.f6_plugin_module
other_module = app.state.other_module
return {
'login_in': f6_module.accept_login_message,
'get_company_information': f6_module.get_company_information,
'create_brand': f6_plugin_module.create_brand,
# ... 更多操作
}
```
**说明**
- 通过请求头中的 `Action` 字段确定要执行的操作
- 使用字典映射操作名到处理函数
---
## 学习路径建议
### 阶段 1:基础理解(1-2 天)
1. **理解 FastAPI 基本概念**
- 阅读官方文档:https://fastapi.tiangolo.com/
- 理解路由、请求、响应的基本用法
2. **运行项目**
```bash
# 安装依赖
pip install -r requirements.txt
# 运行项目
python main.py
# 或
uvicorn main:app --reload
```
3. **访问 API 文档**
- 启动后访问:http://localhost:5003/docs
- 查看自动生成的 Swagger UI 文档
### 阶段 2:代码分析(2-3 天)
1. **分析 main.py**
- 理解应用初始化流程
- 理解路由处理逻辑
- 理解任务队列机制
2. **分析业务模块**
- 查看 `app/module/` 目录下的模块
- 理解操作映射机制
- 理解模块间的交互
3. **分析工具类**
- 查看 `app/utils/app_tools.py`
- 理解任务队列实现
- 理解日志记录机制
### 阶段 3:实践练习(3-5 天)
1. **添加新路由**
```python
@app.get("/health")
async def health_check():
return {"status": "ok"}
```
2. **添加数据验证**
```python
from pydantic import BaseModel
class WebhookData(BaseModel):
api_key: str
entry_id: str
data_id: str
@app.post("/webhook")
async def webhook(data: WebhookData):
return {"received": data.dict()}
```
3. **添加错误处理**
```python
from fastapi import HTTPException
@app.post("/webhook")
async def webhook(request: Request):
try:
data = await request.json()
# 处理逻辑
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
```
### 阶段 4:深入学习(1-2 周)
1. **学习异步编程**
- 理解 `async/await`
- 理解 `asyncio`
- 理解并发处理
2. **学习 Pydantic**
- 数据验证
- 数据序列化
- 自定义验证器
3. **学习依赖注入**
- FastAPI 的依赖注入系统
- 数据库连接管理
- 认证和授权
---
## 常见问题解答
### Q1: FastAPI 和 Flask 的主要区别是什么?
**A:** 主要区别:
1. **性能**FastAPI 性能更高,支持异步
2. **类型验证**FastAPI 自动验证,Flask 需要手动
3. **API 文档**FastAPI 自动生成,Flask 需要手动编写
4. **异步支持**FastAPI 原生支持,Flask 需要额外库
### Q2: 为什么使用 `async def` 而不是 `def`
**A:**
- `async def` 允许使用 `await` 关键字
- 可以异步处理 I/O 操作(如网络请求、数据库查询)
- 提高并发性能
**示例**
```python
# 同步(阻塞)
def sync_function():
data = requests.get("https://api.example.com") # 阻塞
return data
# 异步(非阻塞)
async def async_function():
async with httpx.AsyncClient() as client:
data = await client.get("https://api.example.com") # 非阻塞
return data
```
### Q3: `app.state` 是什么?为什么要用它?
**A:**
- `app.state` 是 FastAPI 提供的应用级状态存储
- 用于存储需要在多个路由之间共享的数据
- 类似于 Flask 的 `g` 对象,但作用域是整个应用
**项目中的使用**
```python
# 启动时设置
app.state.logger = setup_global_logger(Config)
# 在路由中使用
logger = app.state.logger
```
### Q4: 如何处理同步函数?
**A:**
使用 `anyio.to_thread.run_sync()` 在线程池中执行同步函数:
```python
import anyio
# 同步函数
def sync_function(data):
# 耗时操作
return result
# 在异步函数中调用
@app.post("/api")
async def api(request: Request):
result = await anyio.to_thread.run_sync(sync_function, data)
return result
```
### Q5: 如何添加中间件?
**A:**
使用 `@app.middleware()` 装饰器:
```python
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
```
### Q6: 如何添加 CORS 支持?
**A:**
使用 `CORSMiddleware`
```python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有源
allow_credentials=True,
allow_methods=["*"], # 允许所有方法
allow_headers=["*"], # 允许所有头
)
```
### Q7: 如何添加认证?
**A:**
使用依赖注入:
```python
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer
security = HTTPBearer()
async def verify_token(token: str = Depends(security)):
if token != "valid_token":
raise HTTPException(status_code=401, detail="Invalid token")
return token
@app.post("/api")
async def api(token: str = Depends(verify_token)):
return {"message": "Authenticated"}
```
---
## 推荐资源
1. **官方文档**https://fastapi.tiangolo.com/
2. **中文文档**https://fastapi.tiangolo.com/zh/
3. **Pydantic 文档**https://docs.pydantic.dev/
4. **Uvicorn 文档**https://www.uvicorn.org/
---
## 总结
FastAPI 是一个现代、高性能的 Web 框架,特别适合构建 API。相比 Flask,它提供了:
1. **更好的性能**:异步支持,高性能
2. **自动验证**:基于类型提示的自动数据验证
3. **自动文档**:自动生成 API 文档
4. **更好的开发体验**:类型提示、IDE 支持
通过这个项目,你可以学习到:
- FastAPI 的基本用法
- 异步编程
- 任务队列机制
- 模块化架构设计
建议按照学习路径逐步深入,多实践、多思考,逐步掌握 FastAPI 的精髓。
+496
View File
@@ -0,0 +1,496 @@
# FastAPI 快速参考指南
## 🚀 快速开始
### 最小示例
```python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
```
### 运行应用
```bash
# 方式 1:使用 uvicorn
uvicorn main:app --reload
# 方式 2:在代码中运行
if __name__ == '__main__':
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
```
---
## 📝 路由定义
### HTTP 方法
```python
@app.get("/items") # GET 请求
@app.post("/items") # POST 请求
@app.put("/items/{id}") # PUT 请求
@app.delete("/items/{id}") # DELETE 请求
@app.patch("/items/{id}") # PATCH 请求
```
### 路径参数
```python
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
# 多个路径参数
@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(user_id: int, item_id: int):
return {"user_id": user_id, "item_id": item_id}
```
### 查询参数
```python
@app.get("/items")
async def read_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
# 可选参数
@app.get("/items")
async def read_items(q: str = None):
return {"q": q}
```
### 请求体
```python
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
description: str = None # 可选字段
@app.post("/items")
async def create_item(item: Item):
return item
```
---
## 🔧 请求和响应
### 获取请求数据
```python
from fastapi import Request
@app.post("/webhook")
async def webhook(request: Request):
# JSON 数据
data = await request.json()
# 请求头
headers = request.headers
# 查询参数
query_params = request.query_params
# 表单数据
form_data = await request.form()
# 文件上传
files = await request.form()
```
### 响应类型
```python
from fastapi.responses import JSONResponse, HTMLResponse, PlainTextResponse
@app.get("/json")
async def json_response():
return JSONResponse({"message": "Hello"})
@app.get("/html")
async def html_response():
return HTMLResponse("<h1>Hello</h1>")
@app.get("/text")
async def text_response():
return PlainTextResponse("Hello")
```
### 状态码
```python
from fastapi import status
@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
return item
```
---
## ✅ 数据验证(Pydantic
### 基本模型
```python
from pydantic import BaseModel, Field
class User(BaseModel):
name: str
age: int = Field(gt=0, le=120) # 年龄必须大于 0,小于等于 120
email: str = Field(..., regex="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
```
### 嵌套模型
```python
class Address(BaseModel):
street: str
city: str
class User(BaseModel):
name: str
address: Address
```
### 列表和字典
```python
class Item(BaseModel):
tags: list[str] = []
metadata: dict[str, str] = {}
```
---
## 🔐 依赖注入
### 基本依赖
```python
from fastapi import Depends
def get_db():
db = "database"
yield db
# 清理代码
@app.get("/items")
async def read_items(db: str = Depends(get_db)):
return {"db": db}
```
### 类依赖
```python
class Database:
def get_data(self):
return "data"
def get_database():
return Database()
@app.get("/items")
async def read_items(db: Database = Depends(get_database)):
return db.get_data()
```
---
## 🛡️ 错误处理
### HTTP 异常
```python
from fastapi import HTTPException
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id}
```
### 自定义异常处理器
```python
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
return JSONResponse(
status_code=400,
content={"message": str(exc)}
)
```
---
## 🔄 异步编程
### 异步函数
```python
import asyncio
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(1) # 模拟异步操作
return {"message": "Done"}
```
### 在线程池中运行同步函数
```python
import anyio
def sync_function(data):
# 同步耗时操作
return result
@app.post("/sync-in-async")
async def sync_in_async(data: dict):
result = await anyio.to_thread.run_sync(sync_function, data)
return result
```
---
## 📊 应用状态和生命周期
### 应用状态
```python
# 设置状态
app.state.my_data = "value"
# 获取状态
my_data = app.state.my_data
```
### 启动和关闭事件
```python
@app.on_event("startup")
async def startup_event():
# 启动时执行
print("应用启动")
@app.on_event("shutdown")
async def shutdown_event():
# 关闭时执行
print("应用关闭")
```
---
## 🎯 中间件
### 添加中间件
```python
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
```
### CORS 中间件
```python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
```
---
## 📚 项目中的实际应用
### 1. 应用初始化(main.py
```python
app = FastAPI(title="简道云FastAPI服务")
@app.on_event("startup")
def on_startup():
app.state.app_tools = AppTools(Config)
app.state.logger = setup_global_logger(Config)
app.state.f6_module = F6Module()
```
### 2. 路由处理
```python
@app.post("/webhook")
async def webhook(request: Request):
# 获取状态
logger = app.state.logger
app_tools = app.state.app_tools
# 获取请求数据
data = await request.json()
header = request.headers
# 处理逻辑
# ...
# 返回响应
return JSONResponse(result)
```
### 3. 任务队列集成
```python
# 将任务放入队列
response_queue = app_tools.enqueue_task(handler, data)
# 等待结果(在线程池中执行)
result = await anyio.to_thread.run_sync(response_queue.get)
```
---
## 🔍 调试技巧
### 1. 查看自动生成的文档
启动应用后访问:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
### 2. 打印请求信息
```python
@app.post("/webhook")
async def webhook(request: Request):
print(f"Method: {request.method}")
print(f"URL: {request.url}")
print(f"Headers: {dict(request.headers)}")
data = await request.json()
print(f"Body: {data}")
```
### 3. 使用日志
```python
import logging
logger = logging.getLogger(__name__)
@app.post("/webhook")
async def webhook(request: Request):
logger.info("收到请求")
logger.debug(f"请求数据: {data}")
```
---
## 📖 常用模式
### 1. 操作映射模式(项目中使用)
```python
def get_action_map() -> dict:
return {
'action1': handler1,
'action2': handler2,
}
@app.post("/api")
async def api(request: Request):
action_map = get_action_map()
action = request.headers.get('Action')
handler = action_map.get(action)
if handler:
result = handler(data)
return JSONResponse(result)
```
### 2. 统一错误处理
```python
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(f"未处理的异常: {exc}")
return JSONResponse(
status_code=500,
content={"message": "内部服务器错误"}
)
```
### 3. 请求验证
```python
from pydantic import BaseModel, validator
class WebhookData(BaseModel):
api_key: str
entry_id: str
data_id: str
@validator('api_key')
def validate_api_key(cls, v):
if not v:
raise ValueError('api_key 不能为空')
return v
@app.post("/webhook")
async def webhook(data: WebhookData):
# 数据已自动验证
return {"received": data.dict()}
```
---
## 🎓 学习检查清单
- [ ] 理解 FastAPI 基本概念
- [ ] 能够创建简单的路由
- [ ] 理解异步编程(async/await
- [ ] 掌握 Pydantic 数据验证
- [ ] 理解依赖注入
- [ ] 能够处理错误
- [ ] 理解应用状态和生命周期
- [ ] 能够添加中间件
- [ ] 理解项目中的任务队列机制
- [ ] 能够添加新的路由和功能
---
## 📚 相关资源
- **FastAPI 官方文档**: https://fastapi.tiangolo.com/
- **FastAPI 中文文档**: https://fastapi.tiangolo.com/zh/
- **Pydantic 文档**: https://docs.pydantic.dev/
- **Uvicorn 文档**: https://www.uvicorn.org/
- **Python 异步编程**: https://docs.python.org/3/library/asyncio.html
---
**提示**: 这个快速参考指南可以作为日常开发的速查手册。建议结合 `FASTAPI_LEARNING.md` 深入学习。
+352
View File
@@ -0,0 +1,352 @@
# FastAPI 学习资源集合
欢迎!这个目录包含了学习 FastAPI 的完整资源,特别适合从 Flask 迁移到 FastAPI 的开发者。
## 📚 文档列表
### 1. [FastAPI 学习文档](./FASTAPI_LEARNING.md) 📖
**推荐首先阅读**
全面的 FastAPI 学习指南,包括:
- FastAPI 简介和特点
- Flask vs FastAPI 详细对比
- FastAPI 核心概念详解
- 项目中的实际应用分析
- 学习路径建议
- 常见问题解答
**适合**:想要系统学习 FastAPI 的初学者
### 2. [FastAPI 快速参考指南](./FASTAPI_QUICK_REFERENCE.md) ⚡
**日常开发速查手册**
快速查找常用功能的参考指南:
- 路由定义
- 请求和响应处理
- 数据验证
- 依赖注入
- 错误处理
- 异步编程
- 项目中的实际应用模式
**适合**:日常开发时快速查找语法和用法
### 3. [Flask 到 FastAPI 迁移指南](./FLASK_TO_FASTAPI_MIGRATION.md) 🔄
**迁移项目必读**
详细的迁移指南,包括:
- 迁移概览和步骤
- 核心概念对比表
- 代码迁移示例
- 当前项目的迁移分析
- 常见迁移问题解答
- 迁移检查清单
**适合**:正在从 Flask 迁移到 FastAPI 的开发者
---
## 🎯 学习路径
### 阶段 1:基础入门(1-2 天)
1. **阅读 [FastAPI 学习文档](./FASTAPI_LEARNING.md)**
- 理解 FastAPI 的基本概念
- 了解 Flask vs FastAPI 的区别
- 理解项目中的 FastAPI 使用
2. **运行项目**
```bash
# 安装依赖
pip install -r requirements.txt
# 运行项目
python main.py
# 或
uvicorn main:app --reload
```
3. **访问 API 文档**
- 启动后访问:http://localhost:5003/docs
- 查看自动生成的 Swagger UI 文档
- 尝试调用 API 端点
### 阶段 2:深入理解(2-3 天)
1. **分析项目代码**
- 阅读 `main.py`,理解应用初始化
- 阅读 `app/module/` 下的业务模块
- 理解任务队列机制
2. **参考 [Flask 到 FastAPI 迁移指南](./FLASK_TO_FASTAPI_MIGRATION.md)**
- 理解迁移过程
- 对比 Flask 和 FastAPI 的写法
- 理解项目中的迁移实现
3. **实践练习**
- 添加新的 API 端点
- 添加数据验证
- 添加错误处理
### 阶段 3:熟练应用(3-5 天)
1. **使用 [快速参考指南](./FASTAPI_QUICK_REFERENCE.md)**
- 在日常开发中参考
- 尝试不同的 FastAPI 特性
- 优化现有代码
2. **深入学习**
- 学习异步编程
- 学习 Pydantic 高级特性
- 学习依赖注入系统
3. **项目改进**
- 重构代码使用 FastAPI 最佳实践
- 添加更多类型提示
- 优化性能
---
## 🚀 快速开始
### 1. 安装依赖
```bash
pip install fastapi uvicorn
```
### 2. 最小示例
```python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
```
### 3. 运行应用
```bash
uvicorn main:app --reload
```
### 4. 访问文档
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
---
## 📖 项目结构
```
fastapi_app/
├── main.py # FastAPI 应用入口
├── api.py # API 工具类
├── app/
│ ├── api.py # 简道云 API 封装
│ ├── config.py # 配置管理
│ ├── module/ # 业务模块
│ │ ├── F6_Plugin_module.py
│ │ ├── module.py
│ │ └── other_module.py
│ ├── tasks/ # 后台任务
│ └── utils/ # 工具函数
│ └── app_tools.py
├── requirements.txt # 依赖列表
├── FASTAPI_LEARNING.md # 📖 学习文档
├── FASTAPI_QUICK_REFERENCE.md # ⚡ 快速参考
└── FLASK_TO_FASTAPI_MIGRATION.md # 🔄 迁移指南
```
---
## 🔑 关键概念
### 1. 应用实例
```python
from fastapi import FastAPI
app = FastAPI(title="简道云FastAPI服务")
```
### 2. 路由定义
```python
@app.post("/webhook")
async def webhook(request: Request):
data = await request.json()
return JSONResponse(result)
```
### 3. 应用状态
```python
# 启动时设置
app.state.logger = setup_global_logger(Config)
# 路由中使用
logger = app.state.logger
```
### 4. 生命周期事件
```python
@app.on_event("startup")
def on_startup():
# 初始化代码
pass
```
---
## 💡 项目中的实际应用
### 1. 应用初始化
```python
# main.py
app = FastAPI(title="简道云FastAPI服务")
@app.on_event("startup")
def on_startup():
app.state.app_tools = AppTools(Config)
app.state.logger = setup_global_logger(Config)
app.state.f6_module = F6Module()
```
### 2. 路由处理
```python
@app.post("/webhook")
async def webhook(request: Request):
logger = app.state.logger
app_tools = app.state.app_tools
data = await request.json()
header = request.headers
# 处理逻辑
result = await anyio.to_thread.run_sync(response_queue.get)
return JSONResponse(result)
```
### 3. 任务队列
```python
# 将任务放入队列
response_queue = app_tools.enqueue_task(handler, data)
# 在线程池中执行同步函数
result = await anyio.to_thread.run_sync(response_queue.get)
```
---
## 📚 推荐资源
### 官方文档
- **FastAPI 官方文档**: https://fastapi.tiangolo.com/
- **FastAPI 中文文档**: https://fastapi.tiangolo.com/zh/
- **Pydantic 文档**: https://docs.pydantic.dev/
- **Uvicorn 文档**: https://www.uvicorn.org/
### 学习资源
- **FastAPI 教程**: https://fastapi.tiangolo.com/tutorial/
- **Python 异步编程**: https://docs.python.org/3/library/asyncio.html
- **类型提示**: https://docs.python.org/3/library/typing.html
---
## ❓ 常见问题
### Q: 我应该先读哪个文档?
**A:** 建议按以下顺序:
1. 先读 [FastAPI 学习文档](./FASTAPI_LEARNING.md) 了解基础
2. 再读 [Flask 到 FastAPI 迁移指南](./FLASK_TO_FASTAPI_MIGRATION.md) 理解迁移
3. 日常开发时参考 [快速参考指南](./FASTAPI_QUICK_REFERENCE.md)
### Q: 如何理解项目中的异步代码?
**A:**
- FastAPI 使用 `async/await` 处理异步操作
- `await request.json()` 异步获取 JSON 数据
- `await anyio.to_thread.run_sync()` 在线程池中执行同步函数
- 详细说明见 [FastAPI 学习文档](./FASTAPI_LEARNING.md#异步编程)
### Q: 项目中的任务队列是如何工作的?
**A:**
- 使用 Python 的 `Queue` 和 `threading` 实现
- 请求到达后,任务放入队列
- 后台线程处理任务
- 使用 `anyio.to_thread.run_sync()` 等待结果
- 详细说明见 [FastAPI 学习文档](./FASTAPI_LEARNING.md#项目中的-fastapi-使用)
### Q: 如何添加新的 API 端点?
**A:**
1. 在 `main.py` 中添加路由函数
2. 在 `get_action_map()` 中注册操作(如果需要)
3. 参考 [快速参考指南](./FASTAPI_QUICK_REFERENCE.md#路由定义)
---
## 🎓 学习检查清单
### 基础理解
- [ ] 理解 FastAPI 的基本概念
- [ ] 理解 Flask vs FastAPI 的区别
- [ ] 能够创建简单的路由
- [ ] 理解异步编程(async/await
### 项目理解
- [ ] 理解项目结构
- [ ] 理解应用初始化流程
- [ ] 理解路由处理逻辑
- [ ] 理解任务队列机制
### 实践能力
- [ ] 能够添加新的路由
- [ ] 能够使用 Pydantic 进行数据验证
- [ ] 能够处理错误
- [ ] 能够使用应用状态
### 深入学习
- [ ] 理解依赖注入
- [ ] 理解中间件
- [ ] 理解生命周期事件
- [ ] 能够优化代码性能
---
## 📝 更新日志
- **2024-01-XX**: 创建 FastAPI 学习文档集合
- 添加 FastAPI 学习文档
- 添加快速参考指南
- 添加 Flask 到 FastAPI 迁移指南
---
## 🤝 贡献
如果你发现文档中有错误或需要改进的地方,欢迎提出建议!
---
**祝你学习愉快!** 🎉
如有问题,请参考相应的文档或查看 FastAPI 官方文档。
+620
View File
@@ -0,0 +1,620 @@
# Flask 到 FastAPI 迁移指南
## 📋 目录
1. [迁移概览](#迁移概览)
2. [核心概念对比](#核心概念对比)
3. [代码迁移示例](#代码迁移示例)
4. [项目迁移分析](#项目迁移分析)
5. [常见迁移问题](#常见迁移问题)
---
## 迁移概览
### 为什么迁移到 FastAPI
1. **性能提升**FastAPI 性能接近 NodeJS 和 Go
2. **自动文档**:自动生成 API 文档
3. **类型安全**:基于 Python 类型提示
4. **异步支持**:原生支持 async/await
5. **现代特性**:符合现代 Python 开发标准
### 迁移步骤
1.**安装 FastAPI**`pip install fastapi uvicorn`
2.**替换应用实例**`Flask()``FastAPI()`
3.**更新路由装饰器**:基本语法相同
4.**处理异步**:添加 `async/await`
5.**数据验证**:使用 Pydantic 模型
6.**更新响应**:使用 FastAPI 响应类
---
## 核心概念对比
### 1. 应用实例
#### Flask
```python
from flask import Flask
app = Flask(__name__)
```
#### FastAPI
```python
from fastapi import FastAPI
app = FastAPI()
```
### 2. 路由定义
#### Flask
```python
@app.route('/items', methods=['GET'])
def get_items():
return jsonify({'items': []})
```
#### FastAPI
```python
@app.get('/items')
async def get_items():
return {'items': []}
```
**区别**
- FastAPI 使用 `@app.get()` 而不是 `@app.route(methods=['GET'])`
- FastAPI 函数通常是 `async`
- FastAPI 直接返回字典,自动转换为 JSON
### 3. 获取请求数据
#### Flask
```python
from flask import request
@app.route('/items', methods=['POST'])
def create_item():
data = request.get_json()
name = data.get('name')
return jsonify({'name': name})
```
#### FastAPI
```python
from fastapi import Request
from pydantic import BaseModel
class Item(BaseModel):
name: str
@app.post('/items')
async def create_item(item: Item):
return {'name': item.name}
```
**区别**
- FastAPI 使用 Pydantic 模型自动验证
- 不需要手动获取 JSON 数据
- 类型自动验证和转换
### 4. 路径参数
#### Flask
```python
@app.route('/items/<int:item_id>')
def get_item(item_id):
return jsonify({'item_id': item_id})
```
#### FastAPI
```python
@app.get('/items/{item_id}')
async def get_item(item_id: int):
return {'item_id': item_id}
```
**区别**
- FastAPI 使用 `{item_id}` 而不是 `<int:item_id>`
- 类型在函数参数中指定
### 5. 查询参数
#### Flask
```python
from flask import request
@app.route('/items')
def get_items():
skip = request.args.get('skip', 0, type=int)
limit = request.args.get('limit', 10, type=int)
return jsonify({'skip': skip, 'limit': limit})
```
#### FastAPI
```python
@app.get('/items')
async def get_items(skip: int = 0, limit: int = 10):
return {'skip': skip, 'limit': limit}
```
**区别**
- FastAPI 查询参数直接在函数参数中定义
- 默认值自动处理
- 类型自动验证
### 6. 请求头
#### Flask
```python
from flask import request
@app.route('/items')
def get_items():
user_agent = request.headers.get('User-Agent')
return jsonify({'user_agent': user_agent})
```
#### FastAPI
```python
from fastapi import Request, Header
@app.get('/items')
async def get_items(request: Request):
user_agent = request.headers.get('user-agent')
return {'user_agent': user_agent}
# 或者使用 Header
@app.get('/items')
async def get_items(user_agent: str = Header(None)):
return {'user_agent': user_agent}
```
### 7. 响应
#### Flask
```python
from flask import jsonify, Response
@app.route('/items')
def get_items():
return jsonify({'items': []})
# 或
return Response('text', mimetype='text/plain')
```
#### FastAPI
```python
from fastapi.responses import JSONResponse, PlainTextResponse
@app.get('/items')
async def get_items():
return {'items': []} # 自动转换为 JSON
# 或
return JSONResponse({'items': []})
# 或
return PlainTextResponse('text')
```
### 8. 错误处理
#### Flask
```python
from flask import abort
@app.route('/items/<int:item_id>')
def get_item(item_id):
if item_id not in items:
abort(404, description="Item not found")
return jsonify({'item_id': item_id})
```
#### FastAPI
```python
from fastapi import HTTPException
@app.get('/items/{item_id}')
async def get_item(item_id: int):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {'item_id': item_id}
```
### 9. 应用状态
#### Flask
```python
from flask import g
@app.before_request
def before_request():
g.db = get_database()
@app.route('/items')
def get_items():
db = g.db
return jsonify({'items': []})
```
#### FastAPI
```python
@app.on_event("startup")
def startup():
app.state.db = get_database()
@app.get('/items')
async def get_items():
db = app.state.db
return {'items': []}
```
### 10. 中间件
#### Flask
```python
@app.before_request
def before_request():
# 请求前处理
pass
@app.after_request
def after_request(response):
# 请求后处理
response.headers['X-Custom'] = 'value'
return response
```
#### FastAPI
```python
@app.middleware("http")
async def add_custom_header(request: Request, call_next):
# 请求前处理
response = await call_next(request)
# 请求后处理
response.headers['X-Custom'] = 'value'
return response
```
---
## 代码迁移示例
### 示例 1:简单的 API 端点
#### Flask 版本
```python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
if not data or 'name' not in data:
return jsonify({'error': '缺少 name 字段'}), 400
name = data['name']
age = data.get('age', 0)
return jsonify({'name': name, 'age': age}), 201
```
#### FastAPI 版本
```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
age: int = 0
@app.post('/api/users', status_code=201)
async def create_user(user: User):
return {'name': user.name, 'age': user.age}
```
**改进点**
- ✅ 自动数据验证
- ✅ 类型安全
- ✅ 更简洁的代码
- ✅ 自动生成 API 文档
### 示例 2:带路径参数的端点
#### Flask 版本
```python
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
if user_id not in users:
return jsonify({'error': '用户不存在'}), 404
return jsonify(users[user_id])
```
#### FastAPI 版本
```python
@app.get('/api/users/{user_id}')
async def get_user(user_id: int):
if user_id not in users:
raise HTTPException(status_code=404, detail='用户不存在')
return users[user_id]
```
### 示例 3:文件上传
#### Flask 版本
```python
from flask import request
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': '没有文件'}), 400
file = request.files['file']
# 处理文件
return jsonify({'filename': file.filename})
```
#### FastAPI 版本
```python
from fastapi import UploadFile, File
@app.post('/upload')
async def upload_file(file: UploadFile = File(...)):
# 处理文件
return {'filename': file.filename}
```
---
## 项目迁移分析
### 当前项目的迁移情况
#### 1. 应用初始化(main.py
**Flask 版本(推测)**
```python
from flask import Flask
app = Flask(__name__)
@app.before_first_request
def initialize():
app.config['app_tools'] = AppTools(Config)
# ...
```
**FastAPI 版本(当前)**
```python
from fastapi import FastAPI
app = FastAPI(title="简道云FastAPI服务")
@app.on_event("startup")
def on_startup():
app.state.app_tools = AppTools(Config)
app.state.logger = setup_global_logger(Config)
# ...
```
**迁移要点**
-`Flask()``FastAPI()`
-`@app.before_first_request``@app.on_event("startup")`
-`app.config``app.state`
#### 2. 路由处理(main.py
**Flask 版本(推测)**
```python
@app.route('/webhook', methods=['POST'])
def webhook():
data = request.get_json()
header = request.headers
# 处理逻辑
return jsonify(result)
```
**FastAPI 版本(当前)**
```python
@app.post("/webhook")
async def webhook(request: Request):
data = await request.json()
header = request.headers
# 处理逻辑
return JSONResponse(result)
```
**迁移要点**
-`@app.route(methods=['POST'])``@app.post()`
-`def``async def`
-`request.get_json()``await request.json()`
-`jsonify()``JSONResponse()` 或直接返回字典
#### 3. 任务队列处理
**项目特点**
- 使用自定义任务队列(`Queue` + `threading`
- 在异步函数中调用同步函数
**FastAPI 处理方式**
```python
# 在线程池中执行同步函数
result = await anyio.to_thread.run_sync(response_queue.get)
```
**迁移要点**
- ✅ 使用 `anyio.to_thread.run_sync()` 在线程池中执行同步代码
- ✅ 保持原有的任务队列机制
---
## 常见迁移问题
### Q1: 如何处理 Flask 的 `g` 对象?
**A:** 使用 `app.state` 或依赖注入:
```python
# Flask
from flask import g
g.db = get_db()
# FastAPI 方式 1:使用 app.state
app.state.db = get_db()
db = app.state.db
# FastAPI 方式 2:使用依赖注入(推荐)
def get_db():
db = "database"
yield db
@app.get("/items")
async def get_items(db: str = Depends(get_db)):
return {"db": db}
```
### Q2: 如何处理 Flask 的 `session`
**A:** FastAPI 不内置 session,需要手动实现或使用第三方库:
```python
from fastapi import FastAPI, Request
from starlette.middleware.sessions import SessionMiddleware
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="secret")
@app.post("/login")
async def login(request: Request):
request.session['user'] = 'username'
return {"message": "Logged in"}
```
### Q3: 如何处理 Flask 的 `url_for`
**A:** 使用 `Request` 对象构建 URL
```python
from fastapi import Request
@app.get("/items/{item_id}")
async def get_item(item_id: int, request: Request):
url = str(request.url_for('get_item', item_id=item_id))
return {"url": url}
```
### Q4: 如何迁移 Flask 的蓝图(Blueprint)?
**A:** 使用 FastAPI 的 `APIRouter`
```python
from fastapi import APIRouter
router = APIRouter()
@router.get("/items")
async def get_items():
return {"items": []}
# 在主应用中注册
app.include_router(router, prefix="/api")
```
### Q5: 如何处理 Flask 的 `current_app`
**A:** 使用依赖注入或直接访问 `app`
```python
# Flask
from flask import current_app
config = current_app.config
# FastAPI
from fastapi import Request
@app.get("/config")
async def get_config(request: Request):
app = request.app
# 访问应用配置
return {"config": "value"}
```
### Q6: 如何迁移 Flask 的 `before_request` 和 `after_request`
**A:** 使用中间件:
```python
@app.middleware("http")
async def middleware(request: Request, call_next):
# before_request 逻辑
print("请求前")
response = await call_next(request)
# after_request 逻辑
print("请求后")
response.headers["X-Custom"] = "value"
return response
```
---
## 迁移检查清单
### 基础迁移
- [ ] 替换 `Flask()``FastAPI()`
- [ ] 更新路由装饰器(`@app.route()``@app.get()` 等)
- [ ] 添加 `async` 关键字
- [ ] 更新请求数据获取方式
- [ ] 更新响应返回方式
### 高级迁移
- [ ] 使用 Pydantic 模型进行数据验证
- [ ] 迁移应用状态(`app.config``app.state`
- [ ] 更新生命周期事件(`before_first_request``on_event("startup")`
- [ ] 迁移中间件
- [ ] 更新错误处理
### 测试和优化
- [ ] 测试所有 API 端点
- [ ] 验证数据验证功能
- [ ] 检查性能提升
- [ ] 更新文档
- [ ] 更新依赖项
---
## 总结
### 迁移优势
1. **性能提升**FastAPI 性能显著优于 Flask
2. **开发效率**:自动文档、自动验证减少开发时间
3. **类型安全**:类型提示提供更好的 IDE 支持
4. **现代特性**:异步支持、依赖注入等现代特性
### 注意事项
1. **异步编程**:需要理解 `async/await`
2. **Pydantic**:需要学习 Pydantic 模型定义
3. **依赖注入**:FastAPI 的依赖注入系统需要适应
4. **生态系统**:Flask 的某些扩展可能需要替代方案
### 建议
1. **逐步迁移**:不要一次性迁移所有代码
2. **保持兼容**:在迁移过程中保持 API 兼容性
3. **充分测试**:迁移后充分测试所有功能
4. **学习资源**:参考 FastAPI 官方文档和示例
---
**提示**:这个迁移指南帮助你理解从 Flask 到 FastAPI 的迁移过程。结合 `FASTAPI_LEARNING.md``FASTAPI_QUICK_REFERENCE.md` 一起学习效果更好。
+2
View File
@@ -0,0 +1,2 @@
__all__ = []
+186
View File
@@ -0,0 +1,186 @@
# API模块更新说明
## 更新内容
根据 `fastapi_app/api.py`(最新版)更新了 `fastapi_app/app/api.py`,主要更新包括:
### 1. 添加失败重试机制
所有API函数都已添加重试机制,参考 `entry_data_list` 的实现模式:
- **默认重试次数**:20次(部分函数为10次)
- **重试逻辑**
- 捕获 `requests.exceptions.RequestException` 异常
- 重试前等待时间:0.1秒(快速请求)或3-10秒(慢速请求)
- 超过最大重试次数后记录错误日志并返回None或抛出异常
### 2. 新增功能函数
添加了以下新函数(原版本中没有的):
- `entry_data_banch_update()` - 批量修改数据
- `entry_data_delete()` - 删除单条数据
- `entry_data_batch_delete()` - 批量删除数据
- `workflow_task_hand_over()` - 流程待办转交
- `get_upload_token()` - 获取文件上传凭证
- `upload_file()` - 上传文件
### 3. 功能增强
- **`entry_data_get()`**
- 添加 `replace` 参数(默认True,保持向后兼容)
- 添加 `max_retries` 参数
- 添加重试机制
- **`entry_data_list()`**
- 添加 `replace` 参数(默认True,保持向后兼容)
- 添加 `max_retries` 参数
- 改进重试逻辑,支持分页重试
- **`entry_widget_list()`**
- 添加 `max_retries` 参数
- 添加重试机制
- **`data_batch_create()`**
- 添加 `max_retries` 参数
- 添加重试机制
- 支持 `is_start_workflow``is_start_trigger``transaction_id` 参数
- **`entry_data_batch_create()`**
- 添加 `max_retries` 参数
- 添加重试机制
- 支持 `is_start_workflow``is_start_trigger` 参数
- 使用 `NpEncoder` 处理NumPy数据类型
- **`entry_data_update()`**
- 添加 `max_retries` 参数
- 添加重试机制
- **`workflow_instance_get()`**
- 添加 `max_retries` 参数
- 添加重试机制
- **`workflow_task_approve()`**
- 添加 `max_retries` 参数
- 添加重试机制
- `comment` 参数支持自定义(默认"自动转交")
### 4. 工具函数
- **`NpEncoder`**:JSON编码器,处理NumPy数据类型
- **`replace_decimals()`**:递归替换Decimal类型为float
### 5. 字段替换优化
`field_replacement()` 方法使用递归实现,更优雅地处理嵌套数据结构。
## 兼容性保证
### 向后兼容
1. **默认参数**
- `entry_data_get()``entry_data_list()``replace` 参数默认为 `True`,保持原有行为
- 所有新增的 `max_retries` 参数都有合理的默认值
2. **导入路径**
- 使用 `from app.config import Config`(而非 `from config import Config`
- 使用 `logging.getLogger('app')`(而非 `log_config`
3. **函数签名**
- 所有原有函数的调用方式保持不变
- 新增参数都是可选参数
### 日志系统
- 使用 `logging.getLogger('app')` 作为常规日志记录器
- 使用 `logging.getLogger('app.error')` 作为错误日志记录器
- 与 fastapi_app 项目的日志系统完全兼容
## 使用示例
### 基本使用(保持原有方式)
```python
from app.api import API
api = API()
# 获取单条数据(自动替换字段)
data = api.entry_data_get({
'api_key': 'xxx',
'entry_id': 'xxx',
'data_id': 'xxx'
})
# 获取多条数据(自动替换字段)
data_list = api.entry_data_list({
'api_key': 'xxx',
'entry_id': 'xxx'
})
```
### 使用新功能
```python
# 不替换字段
data = api.entry_data_get({
'api_key': 'xxx',
'entry_id': 'xxx',
'data_id': 'xxx'
}, replace=False)
# 自定义重试次数
data = api.entry_data_get({
'api_key': 'xxx',
'entry_id': 'xxx',
'data_id': 'xxx'
}, max_retries=10)
# 批量删除
result = api.entry_data_batch_delete({
'api_key': 'xxx',
'entry_id': 'xxx',
'data_ids': ['id1', 'id2', 'id3']
}, chunk_size=90, max_retries=20)
```
## 重试机制说明
### 重试策略
1. **快速请求**0.1秒延迟):
- `entry_data_list()` - 分页请求
- `entry_data_batch_create()` - 批量创建
- `entry_data_batch_delete()` - 批量删除
- `workflow_instance_get()` - 流程查询
2. **慢速请求**3-10秒延迟):
- `data_batch_create()` - 3秒延迟
- `entry_data_update()` - 10秒延迟
- `entry_data_banch_update()` - 10秒延迟
- `entry_data_delete()` - 10秒延迟
- `workflow_task_approve()` - 3秒延迟
- `workflow_task_hand_over()` - 3秒延迟
- `get_upload_token()` - 3秒延迟
- `upload_file()` - 3秒延迟
### 错误处理
- 所有重试失败的操作都会记录到错误日志
- 部分函数在重试失败后返回 `None`,部分会抛出异常
- 错误日志包含失败的任务标识信息
## 注意事项
1. **超时设置**:所有请求都设置了 `timeout=10`
2. **状态码检查**:使用 `res.raise_for_status()` 检查HTTP状态码
3. **文件上传**`upload_file()` 使用 `with` 语句确保文件正确关闭
4. **数据类型处理**:批量操作函数使用 `NpEncoder``replace_decimals()` 处理特殊数据类型
## 测试建议
1. 测试所有原有功能的兼容性
2. 测试新添加的函数
3. 测试重试机制(可以模拟网络错误)
4. 测试 `replace=False` 参数的使用
+2
View File
@@ -0,0 +1,2 @@
__all__ = []
+793
View File
@@ -0,0 +1,793 @@
"""
API 模块 - 简道云API接口封装
支持失败重试机制,兼容现有代码
"""
import requests
import json
import time
import logging
from typing import Optional, List, Dict, Any
from decimal import Decimal
import numpy as np
from app.config import Config
# 获取日志记录器
logger = logging.getLogger('app')
error_logger = logging.getLogger('app.error') # 错误日志记录器
class NpEncoder(json.JSONEncoder):
"""NumPy数据类型JSON编码器"""
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
else:
return super(NpEncoder, self).default(obj)
def replace_decimals(obj):
"""递归替换Decimal类型为float"""
if isinstance(obj, dict):
return {k: replace_decimals(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [replace_decimals(item) for item in obj]
elif isinstance(obj, Decimal):
return float(obj)
return obj
class API:
def entry_data_get(self, data: dict, replace: bool = True, max_retries: int = 20) -> Dict:
"""
获取单条表单数据
:param replace: 是否替换字段,默认为True(保持向后兼容)
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息
:return: 表单数据
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/get'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_id": data['data_id']
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
print(data_get)
if replace:
data_get = self.field_replacement(data, data_get)
return data_get
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
if retries <= max_retries:
time.sleep(0.1)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_id')} 连续{max_retries}次请求失败,放弃此次请求。")
raise Exception(f"获取单条表单数据失败,已重试{max_retries}")
def entry_data_list(self, data: dict, replace: bool = True, max_retries: int = 20) -> Dict:
"""
获取多条表单数据
:param max_retries: 最大重试次数
:param replace: 是否替换字段,默认为True(保持向后兼容)
:param data: 简道云插件发送过来的data,包含应用id、表单id等信息
:return: 表单数据列表
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
all_data_batches = []
last_data_id = None
exit_flag = False
while True:
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"limit": 100,
"data_id": last_data_id
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if data_get.get("data"):
all_data_batches.extend(data_get['data'])
last_data_id = data_get['data'][-1].get('_id')
print(f"已获取 {len(all_data_batches)} 条数据")
break
else:
if 'data' not in data_get or len(data_get['data']) == 0:
exit_flag = True
break
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(0.1)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1)
if retries > max_retries:
error_logger.error(f"任务 {last_data_id}组 连续{max_retries}次请求失败,放弃此次请求。")
all_data_batches.append(None)
if exit_flag:
break
final_data = {
'data': all_data_batches
}
logger.info(f"获取了{len(all_data_batches)}条数据")
if replace:
print("进行了替换")
return self.field_replacement(data, final_data)
else:
return final_data
@staticmethod
def entry_widget_list(data: dict, max_retries: int = 20) -> Optional[Dict[str, Any]]:
"""
获取表单字段
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id、表单id等信息
:return: 表单字段信息
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/widget/list'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
return res.json()
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
if retries <= max_retries:
time.sleep(0.1)
if retries > max_retries:
error_logger.error(f"获取表单字段失败,已重试{max_retries}")
return None
def field_replacement(self, data: dict, data_get: dict) -> dict:
"""
字段替换,将id替换为标签名,即唯一值替换为表单中显示字段的名字
:param data: 简道云插件发送过来的data,包含表单id、数据id、应用id
:param data_get: 简道云请求的数据,一般是根据数据id获取到表单的数据
:return: 替换后的数据
"""
widget_list = self.entry_widget_list(data)
if not widget_list or 'widgets' not in widget_list or not isinstance(widget_list['widgets'], list):
raise ValueError("映射表没有接受到数据")
name_to_label = {widget['name']: widget['label'] for widget in widget_list['widgets']}
def replace_keys(obj):
"""递归替换字典中的键名"""
if isinstance(obj, dict):
new_dict = {}
for key, value in obj.items():
new_key = name_to_label.get(key, key)
new_dict[new_key] = replace_keys(value)
return new_dict
elif isinstance(obj, list):
return [replace_keys(item) for item in obj]
else:
return obj
data_get_copy = json.loads(json.dumps(data_get))
if 'data' in data_get_copy:
data_get_copy['data'] = replace_keys(data_get_copy['data'])
return data_get_copy
@staticmethod
def data_batch_create(data: dict, max_retries: int = 20) -> Optional[Dict]:
"""
新建单条表单数据
:param max_retries: 最大重试次数
:param data: 应该包含应用id、表单id,以及新建的数据data['data']
:return: 返回创建后简道云返回的信息
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/create'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data": data['data'],
"is_start_workflow": data.get('is_start_workflow', "false"),
"is_start_trigger": data.get('is_start_trigger', "false"),
"transaction_id": data.get('transaction_id', "")
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
return data_get
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data')} 连续{max_retries}次请求失败,放弃此次请求。")
return None
@staticmethod
def entry_data_batch_create(
data: dict,
chunk_size: int = 90,
max_retries: int = 20
) -> List[Optional[Dict]]:
"""
新建多条数据
:param max_retries: 最大重试次数
:param data: 应包含数据id、表单id、以及需要新建的信息,新建信息应该是一个列表
:param chunk_size: 简道云限制批量新建一次最多100条,这里默认值设置为90条一次
:return: 返回请求后的结果
"""
data = replace_decimals(data)
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_create'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
total_length = len(data['data_list'])
logger.info(f"多数据写入行数: {total_length}")
num_chunks = (total_length + chunk_size - 1) // chunk_size
data_get_list = []
for i in range(num_chunks):
start_index = i * chunk_size
end_index = min(start_index + chunk_size, total_length)
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_list": data['data_list'][start_index:end_index],
"is_start_workflow": data.get('is_start_workflow', "false"),
"is_start_trigger": data.get('is_start_trigger', "false"),
}, cls=NpEncoder)
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if data_get.get("status") == "success":
data_get_list.append(data_get)
break
else:
logger.warning(f"请求异常,将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1)
if retries > max_retries:
error_logger.error(
f"任务 {data['data_list'][start_index:end_index]} 连续{max_retries}次请求失败,放弃此次请求。")
data_get_list.append(None)
return data_get_list
@staticmethod
def entry_data_update(data: dict, max_retries: int = 20) -> dict:
"""
修改数据
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息
:return: 修改数据后简道云返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/update'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_id": data['data_id'],
"data": data['data']
})
data_get = None
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(10)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_id')} 连续{max_retries}次请求失败,放弃此次请求。")
return data_get
@staticmethod
def entry_data_banch_update(data: dict, max_retries: int = 20, chunk_size: int = 90) -> List[dict]:
"""
批量修改数据
:param chunk_size: 批量修改块大小
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息
:return: 修改数据后简道云返回的结果列表
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_update'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
total_length = len(data['data_ids'])
logger.info(f"多数据修改行数: {total_length}")
num_chunks = (total_length + chunk_size - 1) // chunk_size
data_get_list = []
for i in range(num_chunks):
start_index = i * chunk_size
end_index = min(start_index + chunk_size, total_length)
payload = {
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_ids": data['data_ids'][start_index:end_index],
"data": data['data']
}
if "transaction_id" in data:
payload["transaction_id"] = data["transaction_id"]
payload = json.dumps(payload, cls=NpEncoder)
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
data_get_list.append(data_get)
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(10)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_ids')} 连续{max_retries}次请求失败,放弃此次请求。")
continue
return data_get_list
@staticmethod
def entry_data_delete(data: dict, max_retries: int = 20) -> dict:
"""
删除单条数据
:param data: 应包含应用ID、表单ID、数据ID
:param max_retries: 最大重试次数,默认20
:return: 删除结果
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/delete'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_id": data['data_id'],
})
retries = 0
delete_status = None
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
delete_status = res.json()
# 手动处理状态码 4001(数据不存在)
if delete_status == {
"code": 4001,
"msg": "Data does not exist."
}:
logger.info(f"返回结果: {delete_status}")
break
res.raise_for_status()
if res.status_code == 200:
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(10)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_id')} 连续{max_retries}次请求失败,放弃此次请求。")
continue
return delete_status
@staticmethod
def entry_data_batch_delete(
data: dict,
chunk_size: int = 90,
max_retries: int = 20
) -> List[Optional[Dict]]:
"""
批量删除数据
:param data: 应包含应用ID、表单ID、数据ID列表
:param chunk_size: 单次删除最大条数,默认90
:param max_retries: 重试次数,默认20
:return: 删除结果列表
"""
data = replace_decimals(data)
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_delete'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
total_length = len(data['data_ids'])
logger.info(f"多数据删除行数: {total_length}")
num_chunks = (total_length + chunk_size - 1) // chunk_size
data_get_list = []
for i in range(num_chunks):
start_index = i * chunk_size
end_index = min(start_index + chunk_size, total_length)
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_ids": data['data_ids'][start_index:end_index],
}, cls=NpEncoder)
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
logger.info(f"{i}页 返回结果: {data_get}")
if data_get.get("status") == "success":
data_get_list.append(data_get)
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1)
if retries > max_retries:
error_logger.error(
f"批量删除任务第{i+1}批 连续{max_retries}次请求失败,放弃此次请求。")
data_get_list.append(None)
return data_get_list
@staticmethod
def workflow_instance_get(data: dict, max_retries: int = 20) -> dict:
"""
查询实例流程信息
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id
:return: 查询简道云流程实例信息返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v5/workflow/instance/get'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"instance_id": data['data_id'],
"tasks_type": 1
})
data_get = None
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_id')} 连续{max_retries}次请求失败,放弃此次请求。")
return data_get
@staticmethod
def workflow_task_approve(data: dict, max_retries: int = 20) -> dict:
"""
流程待办提交
:param max_retries: 最大重试次数
:param data: 应包含username、instance_id(data_id)、task_id等信息
:return: 返回简道云流程待办提交的结果
"""
url = 'https://api.jiandaoyun.com/api/v1/workflow/task/approve'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"username": data["username"],
"instance_id": data["instance_id"],
"task_id": data['task_id'],
"comment": data.get("comment", "自动转交")
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
if res.status_code == 200:
return res.json()
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"任务 {data.get('task_id')} 连续{max_retries}次请求失败,放弃此次请求。")
return {}
@staticmethod
def workflow_task_hand_over(data: dict, max_retries: int = 10) -> Optional[dict]:
"""
流程待办转交
:param max_retries: 最大重试次数
:param data: 应包含username、instance_id(data_id)、task_id、transfer_username等信息
:return: 返回简道云流程待办转交的结果
"""
url = 'https://api.jiandaoyun.com/api/v1/workflow/task/transfer'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"username": data["username"],
"instance_id": data["instance_id"],
"task_id": data['task_id'],
"transfer_username": data['transfer_username'],
"comment": data.get("comment", "转交")
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
if res.status_code == 200:
return res.json()
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"任务转交失败,已重试{max_retries}")
return None
@staticmethod
def get_upload_token(data: dict, max_retries: int = 10) -> Optional[Dict[str, Any]]:
"""
获取文件上传凭证
:param max_retries: 最大重试次数
:param data: 应包含应用ID、表单ID、事务ID
:return: 返回upload_url、upload_token
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/file/get_upload_token'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"transaction_id": data['transaction_id'],
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
res_j = res.json()
# 检查 token_and_url_list 是否存在且不为空
token_list = res_j.get('token_and_url_list', [])
if not token_list or len(token_list) == 0:
logger.warning(f"未获取到上传凭证列表,将重新请求")
retries += 1
time.sleep(3)
continue
token_item = token_list[0]
upload_url = token_item.get('url')
upload_token = token_item.get('token')
if not upload_url or not upload_token:
logger.warning(f"上传凭证信息不完整,将重新请求")
retries += 1
time.sleep(3)
continue
logger.info(f"返回结果: {upload_url}, {upload_token}")
if res.status_code == 200:
return {
'upload_url': upload_url,
'upload_token': upload_token
}
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except (requests.exceptions.RequestException, KeyError, IndexError, TypeError) as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"获取上传凭证失败,已重试{max_retries}")
return None
@staticmethod
def upload_file(data: dict, max_retries: int = 10) -> Optional[Any]:
"""
上传文件
:param max_retries: 最大重试次数
:param data: 应包含上传文件路径、上传文件url、上传文件token
:return: 返回上传文件结果
"""
url = data['upload_url']
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
}
file_path = data['file_path']
payload = {
"token": data['upload_token'],
}
retries = 0
result = None
while retries <= max_retries:
try:
with open(file_path, 'rb') as f:
files = {"file": f}
res = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10)
res.raise_for_status()
data_get = res.json()
logger.info(f"返回结果: {data_get}")
if res.status_code == 200:
result = data_get
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"上传文件失败,已重试{max_retries}")
return result
+24
View File
@@ -0,0 +1,24 @@
"""
后台任务模块 - 向后兼容入口
此文件保持向后兼容实际功能已拆分到 app.tasks 模块中
"""
# 从新的 tasks 模块导入所有函数,保持向后兼容
from app.tasks import (
update_jiandaoyun,
approve_workflow,
create_brand_background,
delete_history_background,
delete_customer_background,
delete_car_background,
modify_customer_info_background,
)
__all__ = [
'update_jiandaoyun',
'approve_workflow',
'create_brand_background',
'delete_history_background',
'delete_customer_background',
'delete_car_background',
'modify_customer_info_background',
]
+34
View File
@@ -0,0 +1,34 @@
from pathlib import Path
# 获取当前文件所在的目录
BASE_DIR = Path(__file__).resolve().parent.parent # 项目根目录
# 构建保存下载文件的目录路径
SAVE_DIRECTORY = BASE_DIR / '下载文件'
# 构建保存模板文件的目录路径
MODE_DIRECTORY = BASE_DIR / '模板文件'
# 构建日志文件的目录路径
LOGS_DIRECTORY = BASE_DIR / 'logs'
LOG_FILE = LOGS_DIRECTORY / '简道云.log'
# 确保目录存在,如果不存在则创建
SAVE_DIRECTORY.mkdir(parents=True, exist_ok=True)
MODE_DIRECTORY.mkdir(parents=True, exist_ok=True)
# API 配置
JIANDAOYUN_API_TOKEN = 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN' # 曹伟应用api测试 app_key
# 导出配置
class Config:
BASE_DIR = BASE_DIR
SAVE_DIRECTORY = SAVE_DIRECTORY
MODE_DIRECTORY = MODE_DIRECTORY
JIANDAOYUN_API_TOKEN = JIANDAOYUN_API_TOKEN
LOGS_DIRECTORY = LOGS_DIRECTORY
LOG_FILE = LOG_FILE
+60
View File
@@ -0,0 +1,60 @@
"""
核心模块初始化
统一初始化请求头管理器模块注册表等核心组件
"""
from typing import Dict, Any, Callable
from app.core.header_manager import HeaderManager
from app.core.module_registry import ModuleRegistry, registry
class CoreManager:
"""核心管理器 - 统一管理所有核心组件"""
def __init__(self):
self.header_manager = HeaderManager
self.registry = registry
def initialize_modules(self, modules: Dict[str, Any]):
"""
初始化并注册所有模块
Args:
modules: 模块字典格式为 {模块名: 模块实例}
"""
for module_name, module_instance in modules.items():
self.registry.register_module(module_name, module_instance)
def register_action(
self,
action_name: str,
handler: Callable,
module_name: str = "default",
**kwargs
):
"""
便捷方法注册操作
Args:
action_name: 操作名称
handler: 处理函数
module_name: 模块名称
**kwargs: 其他配置参数
"""
self.registry.register_action(
action_name=action_name,
handler=handler,
module_name=module_name,
**kwargs
)
# 全局核心管理器实例
core_manager = CoreManager()
# 导出常用类和函数
__all__ = [
'core_manager',
'HeaderManager',
'ModuleRegistry',
'registry',
]
+118
View File
@@ -0,0 +1,118 @@
"""
请求头管理器
统一管理不同模块的请求头配置
"""
from typing import Dict, Optional
from dataclasses import dataclass, field
@dataclass
class HeaderConfig:
"""请求头配置"""
referer: Optional[str] = None
user_agent: Optional[str] = None
content_type: Optional[str] = None
authorization: Optional[str] = None
custom_headers: Dict[str, str] = field(default_factory=dict)
def to_dict(self) -> Dict[str, str]:
"""转换为字典格式"""
headers = {}
if self.referer:
headers['Referer'] = self.referer
if self.user_agent:
headers['User-Agent'] = self.user_agent
if self.content_type:
headers['Content-Type'] = self.content_type
if self.authorization:
headers['Authorization'] = self.authorization
# 添加自定义请求头
headers.update(self.custom_headers)
return headers
class HeaderManager:
"""请求头管理器"""
# 默认请求头配置
DEFAULT_HEADERS = HeaderConfig(
referer='https://yunxiu.f6car.cn/erp/view/index.html',
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'
)
# F6系统登录请求头
F6_LOGIN_HEADERS = HeaderConfig(
referer='https://yunxiu.f6car.com/kzf6/login/confirm',
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.0.0'
)
# 简道云API请求头
JIANDAOYUN_API_HEADERS = HeaderConfig(
content_type='application/json',
# authorization 应该从配置中获取,这里只是示例
)
# 模块特定的请求头配置
_module_headers: Dict[str, HeaderConfig] = {
'default': DEFAULT_HEADERS,
'f6_login': F6_LOGIN_HEADERS,
'jiandaoyun_api': JIANDAOYUN_API_HEADERS,
}
@classmethod
def get_headers(cls, module_name: str = 'default', **overrides) -> Dict[str, str]:
"""
获取指定模块的请求头
Args:
module_name: 模块名称
**overrides: 覆盖的请求头配置
Returns:
请求头字典
"""
# 获取基础配置
config = cls._module_headers.get(module_name, cls.DEFAULT_HEADERS)
# 创建配置副本
header_config = HeaderConfig(
referer=overrides.get('referer', config.referer),
user_agent=overrides.get('user_agent', config.user_agent),
content_type=overrides.get('content_type', config.content_type),
authorization=overrides.get('authorization', config.authorization),
custom_headers={**config.custom_headers, **overrides.get('custom_headers', {})}
)
return header_config.to_dict()
@classmethod
def register_module_headers(cls, module_name: str, config: HeaderConfig):
"""
注册模块的请求头配置
Args:
module_name: 模块名称
config: 请求头配置
"""
cls._module_headers[module_name] = config
@classmethod
def update_module_headers(cls, module_name: str, **updates):
"""
更新模块的请求头配置
Args:
module_name: 模块名称
**updates: 要更新的配置项
"""
if module_name in cls._module_headers:
config = cls._module_headers[module_name]
for key, value in updates.items():
if hasattr(config, key):
setattr(config, key, value)
else:
config.custom_headers[key] = value
+116
View File
@@ -0,0 +1,116 @@
"""
模块注册和路由管理
提供统一的模块注册机制方便添加新功能模块
"""
from typing import Dict, Callable, Optional, Any
from dataclasses import dataclass
@dataclass
class ActionConfig:
"""操作配置"""
handler: Callable # 处理函数
module_name: str # 所属模块名称
description: Optional[str] = None # 描述
requires_auth: bool = True # 是否需要认证
header_module: Optional[str] = None # 使用的请求头模块名称
class ModuleRegistry:
"""模块注册表"""
def __init__(self):
self._actions: Dict[str, ActionConfig] = {}
self._modules: Dict[str, Dict[str, Any]] = {}
def register_action(
self,
action_name: str,
handler: Callable,
module_name: str = "default",
description: Optional[str] = None,
requires_auth: bool = True,
header_module: Optional[str] = None
):
"""
注册操作
Args:
action_name: 操作名称
handler: 处理函数
module_name: 模块名称
description: 描述
requires_auth: 是否需要认证
header_module: 请求头模块名称
"""
config = ActionConfig(
handler=handler,
module_name=module_name,
description=description,
requires_auth=requires_auth,
header_module=header_module
)
self._actions[action_name] = config
def get_action(self, action_name: str) -> Optional[ActionConfig]:
"""
获取操作配置
Args:
action_name: 操作名称
Returns:
操作配置如果不存在返回None
"""
return self._actions.get(action_name)
def get_all_actions(self) -> Dict[str, ActionConfig]:
"""获取所有注册的操作"""
return self._actions.copy()
def register_module(self, module_name: str, module_instance: Any, **metadata):
"""
注册模块实例
Args:
module_name: 模块名称
module_instance: 模块实例
**metadata: 模块元数据
"""
self._modules[module_name] = {
'instance': module_instance,
**metadata
}
def get_module(self, module_name: str) -> Optional[Any]:
"""
获取模块实例
Args:
module_name: 模块名称
Returns:
模块实例如果不存在返回None
"""
module_info = self._modules.get(module_name)
return module_info['instance'] if module_info else None
def get_actions_by_module(self, module_name: str) -> Dict[str, ActionConfig]:
"""
获取指定模块的所有操作
Args:
module_name: 模块名称
Returns:
操作字典
"""
return {
name: config
for name, config in self._actions.items()
if config.module_name == module_name
}
# 全局模块注册表实例
registry = ModuleRegistry()
+298
View File
@@ -0,0 +1,298 @@
import requests
from urllib.parse import quote
import pandas as pd
import os
import urllib.parse
from datetime import datetime
from app.api import API
from typing import Optional, Dict, Any, Tuple
from app.config import Config
from app.module import F6Module
import threading
from app import back_ground_tasks
api_instance = API()
class F6PluginModule:
@staticmethod
def accept_file(data: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any]]: # 接收文件
"""
接收文件
此方法用于处理前端上传的文件下载文件并保存到指定目录主要步骤包括
1. 处理前端传递的数据获取文件的URL
2. 解析URL以获取文件名
3. 根据当前时间生成新的文件名以避免文件名冲突
4. 下载文件并保存到指定目录
5. 返回文件保存路径和处理后的数据
Args:
data (dict): 包含文件URL和其他必要信息的字典
Returns:
tuple: 包含文件保存路径和处理后的数据的元组如果文件保存成功返回保存路径和数据如果失败返回 None 和数据
"""
data = api_instance.entry_data_get(data=data)
print(data)
try:
# 安全地访问附件信息
data_dict = data.get('data', {})
attachments = data_dict.get('附件', [])
if not attachments or len(attachments) == 0:
print('上传url未读取到,或无上传文件: 附件列表为空')
save_path = ''
return save_path, data
first_attachment = attachments[0]
url = first_attachment.get('url')
if not url:
print('上传url未读取到,或无上传文件: URL为空')
save_path = ''
return save_path, data
print(url)
except (KeyError, IndexError, TypeError) as e:
print(f'上传url未读取到,或无上传文件:{e}')
save_path = ''
return save_path, data
parsed_url = urllib.parse.urlparse(url)
query_params = urllib.parse.parse_qs(parsed_url.query)
attname = query_params.get('attname', [''])[0]
filename = urllib.parse.unquote(attname)
# 获取当前时间并格式化为指定格式的字符串
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
# 分离文件名和扩展名
name_part, ext_part = filename.rsplit('.', 1) if '.' in filename else (filename, '')
# 构建新文件名
new_filename = f"{name_part}{timestamp}.{ext_part}" if ext_part else f"{name_part}{timestamp}"
save_path = os.path.join(Config.SAVE_DIRECTORY, new_filename)
print(save_path)
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(save_path, 'wb') as file:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
file.write(chunk)
return save_path, data
else:
return None, data
def check_file(self, data: Dict[str, Any]) -> Dict[str, str]: # 校验上传文件
"""
校验上传文件
此方法负责接收前端上传的文件并根据文件类型和操作指令进行相应的校验主要步骤包括
1. 调用 `accept_file` 方法处理前端传递的数据获取文件保存路径和处理后的数据
2. 根据数据中的 `Action` 字段判断需要执行的操作类型
3. 如果文件保存路径有效继续执行以下步骤
- 如果操作类型为 `create_brand`则读取文件和模板文件校验文件格式是否正确
- 如果文件格式正确返回成功消息否则返回错误消息
4. 如果文件保存路径无效返回相应的错误消息
5. 如果读取文件过程中发生异常捕获异常并返回错误消息
Args:
data (dict): 前端请求发送过来的数据包含文件信息和其他必要参数
Returns:
dict: 包含文件校验结果的消息字典如果校验成功则返回文件路径和校验标志如果失败则返回错误消息
"""
save_path, data1 = self.accept_file(data)
# 安全地获取 Action 字段
data_dict = data1.get('data', {})
action = data_dict.get('Action(隐藏)')
if not action:
return {'msg': '缺少Action字段,无法校验文件'}
if save_path:
try:
if action == 'create_brand':
df1 = pd.read_excel(save_path, sheet_name=0)
if "品牌" in df1.columns[0]: # 校验表头名字
print('文件校验成功')
return {'msg': f'{save_path}', 'check': ''}
else:
print("'msg':'文件上传格式错误'")
return {'msg': '文件上传格式错误'}
elif action == 'modify_customer_info':
df = pd.read_excel(save_path, sheet_name=0)
if "客户手机号" in df.columns[0]: # 校验表头名字
print('文件校验成功')
return {'msg': f'{save_path}', 'check': ''}
else:
print("'msg':'文件上传格式错误'")
return {'msg': '文件上传格式错误'}
elif action == 'delete_cars':
pass
else:
pass
except Exception as e:
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
else:
return {'msg': '当前节点无附件上传', 'check': ''}
@staticmethod
def create_brand(data: Dict[str, Any]) -> Dict[str, str]: # 创建品牌
entry_data = api_instance.entry_data_get(data=data)
print('执行 品牌批量新建')
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
save_path = entry_data['data']['文件保存地址']
login_response = F6Module.login_in(username, password, company_name)
if login_response is None:
return {'msg': '登录失败'}
try:
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
except Exception as e:
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
try:
thread = threading.Thread(target=back_ground_tasks.create_brand_background,
args=(data, cookies, df, save_path))
thread.start()
except Exception as e:
print(f'创建线程失败: {str(e)}')
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
@staticmethod
def delete_history(data: Dict[str, Any]) -> Dict[str, str]:
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
org_name = entry_data['data']['门店名称']
login_response = F6Module.login_in(username, password, company_name)
if login_response is None:
return {'msg': '未执行', 'msg_details': '登录失败'}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
url = 'https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=1000&name='
res = requests.get(url=url, cookies=cookies)
store_data = res.json()
org_lists = store_data['data']['list']
org_id = ''
org_name1 = ''
for org in org_lists:
org_name1 = org['orgName']
if org_name == org['orgName']:
org_id = org['orgId']
if org_id:
thread = threading.Thread(target=back_ground_tasks.delete_history_background,
args=(data, cookies, org_id, org_name1))
thread.start()
return {'msg': '正在执行中', 'msg_details': '请稍后查看结果'}
else:
return {'msg': '未执行', 'msg_details': '门店信息错误'}
@staticmethod
def delete_customer(data):
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
res = F6Module.login_in(username, password, company_name)
if res is not None:
cookies = requests.utils.dict_from_cookiejar(res.cookies)
url = "https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=50000&pageNo=1"
res = requests.get(url, cookies=cookies)
json = res.json()
if json:
thread = threading.Thread(target=back_ground_tasks.delete_customer_background,
args=(data, cookies, json['data']['data'],))
thread.start()
return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'}
else:
return {'msg': '未执行', 'msg_details': '无客户信息'}
else:
return {'msg': '未执行', 'msg_details': '登录失败'}
@staticmethod
def delete_cars(data):
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
res = F6Module.login_in(username, password, company_name)
if res is not None:
cookies = requests.utils.dict_from_cookiejar(res.cookies)
url = "https://yunxiu.f6car.cn/member/car/carListForPermission"
header = {
'Referer': 'https://yunxiu.f6car.cn/erp/view/index.html',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'
}
json_data = {"pageSize": 100, "pageNo": 1}
res = requests.post(url=url, cookies=cookies, json=json_data, headers=header)
res_data = res.json()
total_items = int(res_data["data"]['total'])
all_page = total_items // 100 + (total_items % 100 > 0)
if res_data:
thread = threading.Thread(target=back_ground_tasks.delete_car_background,
args=(data, url, cookies, header, all_page))
thread.start()
return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'}
else:
return {'msg': '未执行', 'msg_details': '无客户车辆信息'}
else:
return {'msg': '未执行', 'msg_details': '登录失败'}
def modify_customer_info(self, data: Dict[str, str]):
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
save_path = entry_data['data']['文件保存地址']
login_response = F6Module.login_in(username, password, company_name)
if login_response is None:
return {'msg': '未执行', 'msg_details': '登录失败'}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
try:
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
except Exception as e:
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
if cookies:
thread = threading.Thread(target=back_ground_tasks.modify_customer_info_background,
args=(data, cookies, df, save_path))
thread.start()
return {'msg': '正在执行中', 'msg_details': '请稍后查看结果'}
else:
return {'msg': '未执行', 'msg_details': 'cookies获取失败'}
+2
View File
@@ -0,0 +1,2 @@
__all__ = []
+226
View File
@@ -0,0 +1,226 @@
import requests
import hashlib
from urllib.parse import quote
from datetime import datetime
from app.api import API
from typing import Optional, Dict, AnyStr
from PIL import Image, ImageEnhance
import pytesseract
import logging
from datetime import datetime
api_instance = API()
logger = logging.getLogger('app')
class F6Module:
@staticmethod
def get_captcha() -> AnyStr:
captcha_url = 'https://yunxiu.f6car.cn/kzf6/login/captcha-image'
response = requests.get(captcha_url)
with open('captcha.png', 'wb') as f:
f.write(response.content)
image = Image.open('captcha.png')
enhancer = ImageEnhance.Contrast(image)
image = enhancer.enhance(2.0)
enhancer = ImageEnhance.Brightness(image)
image = enhancer.enhance(1.5)
image = image.convert('L')
image = image.point(lambda x: 0 if x < 128 else 255, '1')
image.save('preprocessed_captcha.png')
captcha_text = pytesseract.image_to_string(image)
print(f"识别的验证码为: {captcha_text}")
return captcha_text
@staticmethod
def login_in(username: str, password: str, company_name: str = '默认门店',) -> Optional[requests.Response]:
url = "https://yunxiu.f6car.com/kzf6/login/confirm"
session = requests.Session()
header = {
'Referer': url,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/130.0.0.0 Safari/537.36 Edg/129.0.0.0'
}
data = {
'username': username,
'password': hashlib.md5(password.encode('utf-8')).hexdigest(),
}
try:
res = session.post(url=url, headers=header, data=data)
res_json = res.json()
if res_json.get('message') == '请输入图形验证码':
logger.warning("触发图形验证码")
captcha_text = F6Module.get_captcha()
data.update({'imageCode': captcha_text})
res = session.post(url=url, headers=header, data=data)
res_json = res.json()
if res_json.get("data") is None:
return res
else:
group_id = ''
for group in res_json.get('data', []):
if group.get("groupName") == company_name:
group_id = group.get("groupId")
break
if not group_id:
logger.error(f"未找到公司名称: {company_name}")
return None
token = quote(res_json['token']) # URL 编码
url = (f'https://yunxiu.f6car.cn/kzf6/user/loginAfterChooseGroup?'
f'token={token}&groupId={group_id}&macAddress=')
res1 = session.get(url, cookies=res.cookies)
return res1
except Exception as e:
print(f"Error during login: {e}")
return None
def accept_login_message(self, data: Dict[str, str]) -> Dict[str, str]:
username = data['username']
password = data['password']
company_name = data['company_name']
res = self.login_in(username, password, company_name)
if res is not None:
cookies = requests.utils.dict_from_cookiejar(res.cookies)
json = res.json()
url = 'https://yunxiu.f6car.cn/hive/company/getGroupName'
res1 = requests.get(url=url, cookies=cookies)
data1 = res1.json()
if data1['code'] == 200:
if data1['data'] == company_name:
if json['status'] == 'success':
json['status'] = '登录成功'
elif json['status'] == 'Error':
json['status'] = '登录失败,请检查账号密码'
else:
json['status'] = '公司名称不正确或未选择公司名称,请重试'
else:
json['status'] = '请输入正确的账号密码并选择公司名称'
return json
else:
return {"status": "登录失败,请检查公司名称"}
def get_company_information(self, data: Dict[str, str]) -> Dict[str, str]:
username = data['username']
password = data['password']
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
print(username)
url = "https://yunxiu.f6car.com/kzf6/login/confirm"
session = requests.Session()
header = {
'Referer': url,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'
}
data = {
'username': username,
'password': hashlib.md5(password.encode('utf-8')).hexdigest(),
}
try:
res = session.post(url=url, headers=header, data=data)
res_json = res.json()
if res_json.get('message') == '请输入图形验证码':
pass
jiandaoyun_data = {'api_key': '6694d3c4fcb69ca9a111a6c4', 'entry_id': '6736e2112ad50045f041a827'}
if res_json.get("data") is None:
print('单店')
res = self.login_in(username, password)
if res is not None:
cookies = requests.utils.dict_from_cookiejar(res.cookies)
url = 'https://yunxiu.f6car.cn/hive/company/getGroupName'
res = requests.get(url=url, cookies=cookies)
data = res.json()
store_name = data['data']
jiandaoyun_data['data_list'] = [
{"_widget_1731650067055": {"value": f'{username}{password}{timestamp}'},
"_widget_1731650067056": {"value": f"{store_name}"}}]
api_instance.entry_data_batch_create(jiandaoyun_data)
res = {'msg': f'{username}{password}{timestamp}'}
else:
jiandaoyun_data_list = []
for group in res_json.get('data', []):
append_data = {"_widget_1731650067055": {"value": f'{username}{password}{timestamp}'},
"_widget_1731650067056": {"value": f"{group['groupName']}"}}
jiandaoyun_data_list.append(append_data)
jiandaoyun_data['data_list'] = jiandaoyun_data_list
res = api_instance.entry_data_batch_create(jiandaoyun_data)
print(res)
res = {'msg': f'{username}{password}{timestamp}'}
return res
except Exception as e:
print(f"获取公司名称失败: {e}")
res = {'msg': '获取公司名称失败,请重新获取'}
return res
def get_store_information(self, data: Dict[str, str]) -> Dict[str, dict[str, str]]:
username = data['username']
password = data['password']
company_name = data['company_name']
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
login_response = self.login_in(username, password, company_name)
if login_response is None:
return {"msg": {'msg': '未执行', 'msg_details': '登录失败'}}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
url = 'https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=100&name='
res = requests.get(url=url, cookies=cookies)
data = res.json()
org_lists = data['data']['list']
url = 'https://yunxiu.f6car.cn/member/car/carListForPermission'
json = {"pageSize": 10, "pageNo": 1}
car_res = requests.post(url=url, json=json, cookies=cookies)
total_cars_data = car_res.json()
total_cars = total_cars_data['data']['total']
url = 'https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=10&pageNo=1'
customer_res = requests.get(url=url, cookies=cookies)
total_customer_data = customer_res.json()
total_customer = total_customer_data['data']['total']
jiandaoyun_data = {'api_key': '6694d3c4fcb69ca9a111a6c4',
'entry_id': '673c38ccca57a5cf266eb18c'}
jiandaoyun_data_list = []
for org in org_lists:
append_data = {"_widget_1731999948708": {"value": f'{username}{password}{company_name}{timestamp}'},
"_widget_1731999948709": {"value": f"{org['orgName']}"}}
jiandaoyun_data_list.append(append_data)
jiandaoyun_data['data_list'] = jiandaoyun_data_list
api_instance.entry_data_batch_create(jiandaoyun_data)
res = {'msg': {"msg": f'{username}{password}{company_name}{timestamp}',
"total_cars": f"{total_cars}条客户车辆",
"total_customer": f"{total_customer}条客户"}}
return res
@staticmethod
def get_keep_heart(data: Dict[str, str]) -> Dict[str, str]:
return data
+22
View File
@@ -0,0 +1,22 @@
import requests
from urllib.parse import quote
import pandas as pd
import os
import urllib.parse
from datetime import datetime
from app.api import API
from typing import Optional, Dict, Any, Tuple
from app.config import Config
from app.module import F6Module
import threading
from app import back_ground_tasks
api_instance = API()
class OtherPluginModule:
def sms_signature_status(self):
pass
+89
View File
@@ -0,0 +1,89 @@
# 后台任务模块结构说明
## 模块结构
后台任务已按功能拆分为以下模块:
```
app/tasks/
├── __init__.py # 统一导出入口
├── common.py # 通用功能模块(简道云表单更新、工作流审批)
├── brand_tasks.py # 品牌相关任务
├── delete_tasks.py # 删除相关任务
└── customer_tasks.py # 客户相关任务
```
## 模块说明
### common.py - 通用功能模块
包含所有任务共用的功能:
- `update_jiandaoyun()` - 更新简道云表单
- `approve_workflow()` - 工作流审批
### brand_tasks.py - 品牌任务模块
品牌相关的后台任务:
- `create_brand_background()` - 品牌批量创建
### delete_tasks.py - 删除任务模块
删除相关的后台任务:
- `delete_history_background()` - 删除历史维修记录
- `delete_customer_background()` - 删除客户信息
- `delete_car_background()` - 删除客户车辆信息
### customer_tasks.py - 客户任务模块
客户相关的后台任务:
- `modify_customer_info_background()` - 修改客户信息
## 向后兼容
原有的 `app.back_ground_tasks` 模块仍然可用,它现在作为向后兼容的入口,实际功能已拆分到 `app.tasks` 模块中。
## 添加新功能模块
如需添加新的功能模块,请按以下步骤:
1. 在 `app/tasks/` 目录下创建新的模块文件,例如 `new_feature_tasks.py`
2. 在新模块中实现相关功能函数
3. 在 `app/tasks/__init__.py` 中导入并导出新函数
4. 在 `app/back_ground_tasks.py` 中导入新函数以保持向后兼容
示例:
```python
# app/tasks/new_feature_tasks.py
from app.tasks.common import update_jiandaoyun, approve_workflow
def new_feature_background(data, cookies):
# 实现新功能
result = "执行结果"
msg = update_jiandaoyun(data, result)
if msg.get('msg'):
approve_workflow(data)
```
```python
# app/tasks/__init__.py 中添加
from app.tasks.new_feature_tasks import new_feature_background
__all__ = [
# ... 其他函数
'new_feature_background',
]
```
## 使用方式
### 方式一:使用新的模块结构(推荐)
```python
from app.tasks import create_brand_background
from app.tasks.brand_tasks import create_brand_background # 也可以直接导入
```
### 方式二:使用向后兼容的导入方式
```python
from app import back_ground_tasks
back_ground_tasks.create_brand_background(...)
```
两种方式都可以正常工作,代码执行逻辑完全一致。
+34
View File
@@ -0,0 +1,34 @@
"""
后台任务模块统一导出入口
保持向后兼容所有原有导入方式仍然有效
"""
# 通用功能
from app.tasks.common import update_jiandaoyun, approve_workflow
# 品牌相关任务
from app.tasks.brand_tasks import create_brand_background
# 删除相关任务
from app.tasks.delete_tasks import (
delete_history_background,
delete_customer_background,
delete_car_background
)
# 客户相关任务
from app.tasks.customer_tasks import modify_customer_info_background
__all__ = [
# 通用功能
'update_jiandaoyun',
'approve_workflow',
# 品牌任务
'create_brand_background',
# 删除任务
'delete_history_background',
'delete_customer_background',
'delete_car_background',
# 客户任务
'modify_customer_info_background',
]
+55
View File
@@ -0,0 +1,55 @@
"""
品牌相关后台任务模块
包含品牌批量创建等功能
"""
import logging
import os
import requests
import pandas as pd
from typing import Dict, Any
from tqdm import tqdm
from app.tasks.common import update_jiandaoyun, approve_workflow
logger = logging.getLogger('app')
def create_brand_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str):
"""
品牌批量创建后台运行函数
:param data: 包含表单id数据id等的字典
:param cookies: 用户登录f6系统的cookies信息
:param df: 表格读取到的内容DataFrame格式
:param save_path: 文件保存的地址
:return: None
"""
df = df.where(pd.notnull(df), None)
# 定义请求URL
url = 'https://yunxiu.f6car.cn/camaro/brand/getOrCreate'
# 遍历DataFrame中的每一行数据
results = []
for index, row in tqdm(df.iterrows(), total=df.shape[0], desc="创建品牌"):
brand_name = row[df.columns[0]]
if brand_name is None or pd.isna(brand_name) or str(brand_name).strip() == '':
results.append({f'{brand_name}': '无效品牌名', '状态': '跳过'})
logger.warning(f"跳过无效品牌名: {brand_name}")
continue
try:
response = requests.post(url, cookies=cookies, json={"brandName": brand_name})
response.raise_for_status() # 抛出HTTP错误
results.append({'品牌名': brand_name, '状态': '创建成功'})
except requests.exceptions.RequestException as e:
results.append({'品牌名': brand_name, '状态': f'创建失败: {str(e)}'})
print({'msg': '已执行', 'msg_details': f'{results}'})
logger.info(f"品牌创建结果: {results}")
os.remove(save_path)
print(f'{save_path}已删除')
print(data)
# 调用api回写改掉 执行明细与执行状态
msg = update_jiandaoyun(data, f'{results}')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
+94
View File
@@ -0,0 +1,94 @@
"""
通用后台任务模块
包含简道云表单更新和工作流审批等通用功能
"""
import logging
import time
from typing import Dict, Any
from app.api import API
api_instance = API()
logger = logging.getLogger('app')
def update_jiandaoyun(data: Dict[str, Any], results: str):
"""
更新简道云表单
:param data: 包含表单id应用id数据id的字典
:param results: 执行结果信息
:return: 更新结果字典
"""
# 定义简道云数据配置
jiandaoyun_data = {
'api_key': data['api_key'],
'entry_id': data['entry_id'],
'data_id': data['data_id'],
"data": {
'_widget_1731379774828': {"value": "已执行"}, # f6系统批量操作测试 是否执行成功
'_widget_1731381334870': {"value": results} # f6系统批量操作测试 执行明细
}
}
time.sleep(1)
print(jiandaoyun_data)
try:
response = api_instance.entry_data_update(jiandaoyun_data)
logger.info(f"简道云表单更新成功: {response}")
return {'msg': True}
except Exception as e:
logger.error(f"简道云表单更新失败: {e}")
return {'msg': False}
def approve_workflow(data: Dict[str, Any]):
"""
获取简道云当前流程节点并直接提交
:param data: 包含表单id应用id数据id的字典
:return: None
"""
# 获取简道云当前流程列表
json = api_instance.workflow_instance_get(data)
# 检查返回数据是否有效
if not json:
logger.error("未获取到工作流实例信息")
return
# 安全地获取任务列表
tasks = json.get('tasks', [])
if not tasks:
logger.error("未找到待处理任务")
return
# 将JSON字符串转换为Python字典
username = ''
instance_id = ''
task_id = ''
for task in tasks:
if task.get('status') == 0:
assignee = task.get('assignee', {})
username = assignee.get('username', '')
instance_id = task.get('instance_id', '')
task_id = task.get('task_id', '')
if username and instance_id and task_id:
break
if not username or not instance_id or not task_id:
logger.error("未找到有效的待处理任务信息")
return
task_data = {
"username": username,
"instance_id": instance_id,
"task_id": task_id,
}
try:
response = api_instance.workflow_task_approve(task_data)
logger.info(f"简道云工作流任务提交成功: {response}")
except Exception as e:
logger.error(f"简道云工作流任务提交失败: {e}")
+261
View File
@@ -0,0 +1,261 @@
"""
客户相关后台任务模块
包含修改客户信息等功能
"""
import logging
import requests
import pandas as pd
import time
import re
from typing import Dict, Any, Optional
from app.tasks.common import update_jiandaoyun, approve_workflow
logger = logging.getLogger('app')
def modify_customer_info_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str):
"""
修改客户信息后台任务
此函数用于后台任务用于修改会员信息
Args:
data (Dict[str, Any]): 前端请求发送过来的数据包含文件信息和其他必要参数
cookies (Dict[str, str]): 登录用户的Cookies
Returns:
None
"""
df = df.where(pd.notnull(df), None)
params = {
'pageSize': 100,
'pageNo': '1',
}
res = requests.get(
'https://yunxiu.f6car.cn/member/customer/listForPermission',
params=params,
cookies=cookies,
)
total = int(res.json().get("data").get("total"))
total_pages = (total // params["pageSize"]) + (1 if total % params["pageSize"] > 0 else 0)
print(f"总计{total_pages}")
all_customers = []
max_retries = 10
retry_count = 0
for page in range(1, total_pages + 1):
print(f"正在请求第 {page} 页...")
params["pageNo"] = page
while retry_count < max_retries:
response = requests.get(
'https://yunxiu.f6car.cn/member/customer/listForPermission',
params=params,
cookies=cookies,
timeout=10
)
time.sleep(1)
if response.status_code == 200:
suppliers = response.json().get("data", {}).get("data", [])
all_customers.extend(suppliers)
break
else:
retry_count += 1
print(f"请求第 {page} 页失败,正在重试(第 {retry_count} 次)...")
time.sleep(3)
# 获取专属运营顾问列表
json_data = {
'includeStopedEmployee': False,
'pageSize': 1000,
'filterNullUser': False,
'keyword': '',
'idOwnOrgList': [],
}
response = requests.post(
'https://yunxiu.f6car.cn/hive/employee/searchStaffInGroup',
cookies=cookies,
json=json_data,
)
staff_list = response.json().get("data").get("list")
name_to_userid = {
emp['name']: emp['userId']
for emp in staff_list
if emp['userId'] is not None
}
df['userId'] = df['专属运营顾问'].map(name_to_userid)
def extract_province_city_district(address: Optional[str]) -> Dict[str, Optional[str]]:
"""安全解析省市区信息,所有返回值都可能为None"""
if not address:
return {'': None, '': None, '': None}
try:
pattern = r'(?P<省>(?:[\u4e00-\u9fa5]+(?:省|自治区|特别行政区))?)' \
r'(?P<市>(?:[\u4e00-\u9fa5]+(?:市|自治州|地区|盟))?)' \
r'(?P<区>(?:[\u4e00-\u9fa5]+区|[\u4e00-\u9fa5]+县|[\u4e00-\u9fa5]+旗)?)'
match = re.match(pattern, address.strip())
return {k: v if v else None for k, v in match.groupdict().items()} if match else {'': None, '': None,
'': None}
except Exception:
return {'': None, '': None, '': None}
def safe_get(d: Optional[Dict], *keys, default=None):
"""多层字典安全获取值,始终返回可能为None的值"""
if not isinstance(d, dict):
return default
for key in keys:
d = d.get(key, {})
if not isinstance(d, dict):
break
return d if d != {} else default
def convert_to_request_data(original_data: Optional[Dict[str, Any]], df: pd.DataFrame) -> Dict[str, Any]:
"""
完全安全的字典转换函数
特点
1. 每个字段的值都可能为None
2. 不会因为任何字段为空而报错
3. 不使用任何默认值完全保留原始数据的空值状态
"""
customer_info = safe_get(original_data, 'data', 'customerInfo') if original_data else None
address_parts = extract_province_city_district(
safe_get(customer_info, 'provinceCityAreaName') if customer_info else None
)
cell_phone = safe_get(customer_info, 'cellPhone')
exclusive_info = None
df_row = None
if cell_phone and not df.empty:
matched_rows = df[df['客户手机号'] == cell_phone]
if not matched_rows.empty:
df_row = matched_rows.iloc[0]
exclusive_info = {
'userId': df_row.get('userId'),
'name': df_row.get('专属运营顾问')
}
request_data = {
"pkId": safe_get(customer_info, 'idCustomer'),
"idCustomer": safe_get(customer_info, 'idCustomer'),
"name": df_row.get('客户姓名') if df_row is not None and pd.notna(df_row.get('客户姓名')) else safe_get(
customer_info, 'name'),
"sex": safe_get(customer_info, 'sex'),
"customerType": df_row.get('客户类型') if df_row is not None and pd.notna(
df_row.get('客户类型')) else safe_get(
customer_info, 'customerType'),
"customerSource": safe_get(customer_info, 'customerSource'),
"customerSourceName": df_row.get('客户来源') if df_row is not None and pd.notna(
df_row.get('客户来源')) else safe_get(customer_info, 'customerSourceName'),
"companyName": df_row.get('单位名称') if df_row is not None and pd.notna(
df_row.get('单位名称')) else safe_get(
customer_info, 'companyName'),
"cellPhone": cell_phone,
"wechart": safe_get(customer_info, 'wechart'),
"qq": safe_get(customer_info, 'qq'),
"contacts": safe_get(customer_info, 'contacts'),
"contactTelephone": safe_get(customer_info, 'contactTelephone'),
"province": safe_get(customer_info, 'province'),
"city": safe_get(customer_info, 'city'),
"area": safe_get(customer_info, 'area'),
"street": safe_get(customer_info, 'street'),
"address": safe_get(customer_info, 'address'),
"detailAddress": safe_get(customer_info, 'detailAddress'),
"pId": safe_get(customer_info, 'province'),
"cId": safe_get(customer_info, 'city'),
"aId": safe_get(customer_info, 'area'),
"provinceName": address_parts.get(''),
"cityName": address_parts.get(''),
"areaName": address_parts.get(''),
"provinceCityAreaName": safe_get(customer_info, 'provinceCityAreaName'),
"birthday": safe_get(customer_info, 'birthday'),
"creationtime": safe_get(customer_info, 'creationtime'),
"modifiedtime": safe_get(customer_info, 'modifiedtime'),
"creator": safe_get(customer_info, 'creator'),
"creatorName": safe_get(customer_info, 'creatorName'),
"modifier": safe_get(customer_info, 'modifier'),
"idOwnOrg": safe_get(customer_info, 'idOwnOrg'),
"idOwnGroup": safe_get(customer_info, 'idOwnGroup'),
"insuranceCompany": safe_get(customer_info, 'insuranceCompany'),
"maritalStatus": safe_get(customer_info, 'maritalStatus'),
"monthlyIncome": safe_get(customer_info, 'monthlyIncome'),
"idNumber": safe_get(customer_info, 'idNumber'),
"personHobby": safe_get(customer_info, 'personHobby'),
"credentialsType": safe_get(customer_info, 'credentialsType'),
"points": safe_get(customer_info, 'points'),
"maxAccountAmount": safe_get(customer_info, 'maxAccountAmount'),
"pointsEnable": safe_get(customer_info, 'pointsEnable'),
"level": safe_get(customer_info, 'level'),
"memberCardNo": safe_get(customer_info, 'memberCardNo'),
"customerLevel": safe_get(customer_info, 'customerLevel'),
"exclusiveConsultantId": exclusive_info['userId'] if exclusive_info else safe_get(customer_info,
'exclusiveConsultantId'),
"exclusiveConsultantName": exclusive_info['name'] if exclusive_info else safe_get(customer_info,
'exclusiveConsultantName'),
"exclusiveOrgId": safe_get(customer_info, 'exclusiveOrgId'),
"exclusiveOrgName": safe_get(customer_info, 'exclusiveOrgName'),
"customerMemo": df_row.get('客户备注') if df_row is not None and pd.notna(
df_row.get('客户备注')) else safe_get(
customer_info, 'customerMemo'),
"isDel": safe_get(customer_info, 'isDel'),
"idFrom": safe_get(customer_info, 'idFrom'),
"mnemonic": safe_get(customer_info, 'mnemonic'),
"idOrgSource": safe_get(customer_info, 'idOrgSource'),
"firstArrivalIdSourceBill": safe_get(customer_info, 'firstArrivalIdSourceBill'),
"lastArrivalIdSourceBill": safe_get(customer_info, 'lastArrivalIdSourceBill'),
"customerInfoType": safe_get(customer_info, 'customerInfoType'),
"customerInfoCompleteDate": safe_get(customer_info, 'customerInfoCompleteDate'),
"customerInfoCompleteType": safe_get(customer_info, 'customerInfoCompleteType'),
"xczUserId": safe_get(customer_info, 'xczUserId'),
"xczUuid": safe_get(customer_info, 'xczUuid'),
"idWxbCustomer": safe_get(customer_info, 'idWxbCustomer'),
"promoteEmployeeId": safe_get(customer_info, 'promoteEmployeeId'),
"promoteEmployeeName": safe_get(customer_info, 'promoteEmployeeName'),
"promoteMemberId": safe_get(customer_info, 'promoteMemberId'),
"promoteMemberName": safe_get(customer_info, 'promoteMemberName'),
"driverExpiryDate": safe_get(customer_info, 'driverExpiryDate'),
"crmDeleteExclusiveFlag": safe_get(customer_info, 'crmDeleteExclusiveFlag'),
"totalObtainPoints": safe_get(customer_info, 'totalObtainPoints'),
"totalUsedPoints": safe_get(customer_info, 'totalUsedPoints'),
"orgName": safe_get(customer_info, 'orgName'),
"weChatFollower": safe_get(customer_info, 'weChatFollower'),
"pointsEnableConfig": safe_get(customer_info, 'pointsEnableConfig'),
"personalPointsEnableConfig": safe_get(customer_info, 'personalPointsEnableConfig'),
"pointsButtonStatus": safe_get(customer_info, 'pointsButtonStatus'),
"tmallInstallMember": safe_get(customer_info, 'tmallInstallMember'),
"corpId": safe_get(customer_info, 'corpId'),
"thirdCorpId": safe_get(customer_info, 'thirdCorpId'),
}
return request_data
for customer in all_customers:
phone = customer.get("cellPhone")
if phone in df["客户手机号"].tolist():
print("开始修改")
cus_id = customer.get("idCustomer", {})
cus_response = requests.get(f'https://yunxiu.f6car.cn/member/customer/{cus_id}', cookies=cookies)
original_data = cus_response.json()
final_json_data = convert_to_request_data(original_data, df)
response = requests.post(
'https://yunxiu.f6car.cn/member/customer/modifyCustomer',
cookies=cookies,
json=final_json_data,
)
print("修改完成")
time.sleep(1)
msg = update_jiandaoyun(data, f'修改完成')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
+304
View File
@@ -0,0 +1,304 @@
"""
删除相关后台任务模块
包含删除历史维修记录删除客户信息删除客户车辆信息等功能
"""
import logging
import traceback
import requests
import time
from typing import Dict, Any, List
from datetime import datetime
from tqdm import tqdm
from app.tasks.common import update_jiandaoyun, approve_workflow
logger = logging.getLogger('app')
def delete_history_background(data: Dict[str, Any], cookies: Dict[str, str], org_id: str, org_name: str):
"""
删除历史维修数据后台运行函数
:param data: 包含表单id数据id等的字典
:param cookies: 用户登录F6系统的cookies信息
:param org_id: 需要删除历史维修记录的门店id
:param org_name: 需要删除历史维修记录的门店名称
:return: None
"""
url = f'https://yunxiu.f6car.cn/maintain-dump/maintainHistory/?orgid={org_id}' # 删除url
res = requests.delete(url=url, cookies=cookies)
res_data = res.json()
if res.status_code == 200 and res_data.get('code') == 200:
results = f'{org_name} 历史维修记录已删除'
print(results)
logger.info(f"删除 {org_name} 历史维修记录成功")
else:
results = f'删除 {org_name} 历史维修记录失败: {res_data.get("message")}'
print(results)
logger.error(f"删除 {org_name} 历史维修记录失败: {res_data.get('message')}")
# 调用api回写改掉 执行明细与执行状态
time.sleep(1)
msg = update_jiandaoyun(data, f'{results}')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], json_data: List[Dict[str, Any]]):
"""
删除客户信息后台运行函数
:param data: 包含表单id数据id等字典
:param cookies: 用户登录f6系统的cookies信息
:param json_data: 获取到的客户信息列表列表最大值取决url里面的值
:return: None
"""
success = 0
fail = 0
# 获取门店ID
org_url = "https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=10&name="
org_res = requests.get(url=org_url, cookies=cookies)
# 安全地获取门店ID
org_data = org_res.json().get("data", {})
org_list = org_data.get("list", [])
if not org_list or len(org_list) == 0:
logger.error("未获取到门店信息")
msg = update_jiandaoyun(data, '删除失败: 未获取到门店信息')
if msg.get('msg'):
approve_workflow(data)
return
operate_org_id = org_list[0].get("orgId")
if not operate_org_id:
logger.error("门店ID为空")
msg = update_jiandaoyun(data, '删除失败: 门店ID为空')
if msg.get('msg'):
approve_workflow(data)
return
print(operate_org_id)
# 获取会员卡列表
card_url = f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}&pageSize=100&pageNo=1"
card_res = requests.get(url=card_url, cookies=cookies)
total_card = int(card_res.json().get("data").get("total"))
print(total_card)
total_page = total_card // 100 + (total_card % 100 > 0)
card_list_customers = []
for page in tqdm(range(1, total_page + 1), desc="查询会员卡"):
card_url = (f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}"
f"&pageSize=100&pageNo={page}")
card_res = requests.get(url=card_url, cookies=cookies)
card_cars_list = card_res.json().get("data").get("data")
for card_customer in card_cars_list:
if card_customer.get("idCustomer") is None:
continue
else:
card_list_customers.append(card_customer.get("idCustomer", None))
time.sleep(0.2)
for item in tqdm(json_data, desc="删除客户"):
id_customer = item['idCustomer']
phone = item['cellPhone']
consume_last_time = item['consumeLastTime']
if consume_last_time:
print(f'{id_customer}最近消费时间: {consume_last_time},跳过删除')
logger.warning(f"{id_customer}最近消费时间: {consume_last_time},跳过删除")
continue
if id_customer in card_list_customers:
logger.info(f"{id_customer} 存在会员卡,跳过删除")
fail += 1
continue
try:
url = f"https://yunxiu.f6car.cn/member/customer/{id_customer}" # 客户信息删除url
res = requests.delete(url, cookies=cookies) # 客户信息删除
res_data = res.json()
if res_data.get('success'):
success += 1
logger.info(f"客户删除成功: ID={id_customer}, 手机号={phone}")
else:
fail += 1
logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, 错误信息: {res_data.get('message')}")
time.sleep(0.2)
except Exception as e:
fail += 1
print("删除失败,", item, id_customer, phone, e)
logger.error(f"删除客户时发生错误: ID={id_customer}, 手机号={phone}, 错误信息: {e}")
if success + fail < len(json_data):
continue
now = datetime.now()
if 20 <= now.hour <= 8:
time.sleep(1)
else:
time.sleep(3)
logger.info(f"客户删除结果: 成功次数={success}, 失败次数={fail}")
msg = update_jiandaoyun(data, f'成功次数{success},失败次数{fail}')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
def delete_car_background(data: Dict[str, Any], url: str, cookies: Dict[str, str], header: Dict[str, Any],
all_page: str):
"""
删除客户车辆信息后台运行函数
:param header: 应包含账号登录的请求头
:param url: 包含请求客户车辆信息的url
:param all_page: 客户车辆信息的页数
:param data: 包含表单id数据id等的字典
:param cookies: 登录F6系统后的请求信息
:return: None
"""
print(cookies)
success = 0
fail = 0
try:
# 确保 all_page 是一个整数
all_page = int(all_page)
# 获取门店ID
org_url = "https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=10&name="
org_res = requests.get(url=org_url, cookies=cookies)
# 安全地获取门店ID
org_data = org_res.json().get("data", {})
org_list = org_data.get("list", [])
if not org_list or len(org_list) == 0:
logger.error("未获取到门店信息")
msg = update_jiandaoyun(data, '删除失败: 未获取到门店信息')
if msg.get('msg'):
approve_workflow(data)
return
operate_org_id = org_list[0].get("orgId")
if not operate_org_id:
logger.error("门店ID为空")
msg = update_jiandaoyun(data, '删除失败: 门店ID为空')
if msg.get('msg'):
approve_workflow(data)
return
print(operate_org_id)
# 获取会员卡列表
card_url = (
f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}&pageSize=100&pageNo=1"
)
card_res = requests.get(url=card_url, cookies=cookies)
total_card = int(card_res.json().get("data").get("total"))
print(total_card)
total_page = total_card // 100 + (total_card % 100 > 0)
card_list_cars = []
for page in tqdm(range(1, total_page + 1), desc="查询会员卡"):
card_url = (f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}"
f"&pageSize=100&pageNo={page}")
card_res = requests.get(url=card_url, cookies=cookies)
card_cars_list = card_res.json().get("data").get("data")
for card_car in card_cars_list:
if card_car.get("cars") is None:
continue
for car in card_car.get("cars", []):
card_list_cars.append(car.get("idCar", None))
time.sleep(0.2)
itemlist = []
# 使用 range() 创建一个可迭代的对象
for page in range(1, all_page + 1):
json_data = {
"pageSize": 100,
"pageNo": page
}
# 获取当前页的数据
res = requests.post(url=url, cookies=cookies, json=json_data, headers=header)
res.raise_for_status() # 检查请求是否成功
response_data = res.json()
if 'data' not in response_data or 'data' not in response_data['data']:
print(f"警告: 页码 {page} 返回的数据格式不正确")
continue
items = response_data['data']['data']
for item in items:
itemlist.append(item)
for item in tqdm(itemlist, desc="删除车辆信息"):
car_id = item.get('tmCarInfo', {}).get('pkId')
customer_id = item.get('tmCustomerInfo', {}).get('pkId')
consume_last_time = item.get('tmCustomerInfo', {}).get('consumeLastTime')
if consume_last_time:
logger.info(f"{customer_id}最近消费时间: {consume_last_time},跳过删除")
fail += 1
continue
if car_id in card_list_cars:
logger.info(f"{customer_id} 存在会员卡,跳过删除")
fail += 1
continue
if not car_id or not customer_id:
logger.info(f"页码 {page} 中缺少必要的ID信息")
fail += 1
continue
try:
delete_url = (
f"https://yunxiu.f6car.cn/member/car/deleteCar/{car_id}/{customer_id}"
)
delete_res = requests.delete(delete_url, cookies=cookies)
delete_res.raise_for_status() # 检查删除请求是否成功
delete_data = delete_res.json()
if delete_data.get('data'):
success += 1
else:
fail += 1
logger.error(
f"删除失败: 页码 {page}, 车辆ID {car_id}, 客户ID {customer_id},"
f" 错误信息: {delete_data.get('message', '未知错误')}")
print(
f"删除失败: 页码 {page}, 车辆ID {car_id}, 客户ID {customer_id},"
f" 错误信息: {delete_data.get('message', '未知错误')}")
time.sleep(0.2) # 避免过快请求
except requests.exceptions.RequestException as e:
fail += 1
print(f"删除失败: 页码 {page}, 车辆ID {car_id}, 客户ID {customer_id}, 错误信息: {e}")
logger.error(f"删除失败: 页码 {page}, 车辆ID {car_id}, 客户ID {customer_id}, 错误信息: {e}")
continue
now = datetime.now()
if 20 <= now.hour <= 8:
time.sleep(1)
else:
time.sleep(3)
print(f"完成: 成功删除 {success} 辆车, 失败 {fail} 辆车")
logger.info(f"完成: 成功删除 {success} 辆车, 失败 {fail} 辆车")
except ValueError as e:
print(f"Error converting all_page to integer: {e}")
traceback.print_exc() # 打印堆栈跟踪信息
except Exception as e:
print(f"An unexpected error occurred: {e}")
traceback.print_exc() # 打印堆栈跟踪信息
msg = update_jiandaoyun(data, f'成功次数{success},失败次数{fail}')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
+88
View File
@@ -0,0 +1,88 @@
import logging
import os
from logging.handlers import RotatingFileHandler
from queue import Queue
import threading
from apscheduler.schedulers.background import BackgroundScheduler
from urllib.parse import unquote
class AppTools:
def __init__(self, config):
self.config = config
self.task_queue = Queue()
self.logger = self._setup_logger()
self.scheduler = self._setup_scheduler()
self._start_task_thread()
def _setup_logger(self):
log_dir = self.config.LOGS_DIRECTORY
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = self.config.LOG_FILE
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
if not any(isinstance(h, RotatingFileHandler) and getattr(h, 'baseFilename', None) == str(log_file)
for h in logger.handlers):
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024 * 5, backupCount=5, encoding='utf-8')
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s:%(name)s:%(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
def _setup_scheduler(self):
scheduler = BackgroundScheduler()
import atexit
atexit.register(lambda: scheduler.shutdown(wait=False))
return scheduler
def _start_task_thread(self):
task_thread = threading.Thread(target=self.process_tasks, daemon=True)
task_thread.start()
def process_tasks(self):
while True:
task = self.task_queue.get()
if task is None:
self.logger.error("任务处理线程已终止")
break
try:
result = task['handler'](task['data'])
task['response'].put(result)
except Exception as e:
self.logger.error(f"任务执行失败: {str(e)}")
task['response'].put({'msg': f'任务执行失败: {str(e)}'})
finally:
self.task_queue.task_done()
self.logger.info("任务处理完成")
def enqueue_task(self, handler, data):
response_queue = Queue()
self.task_queue.put({
'handler': handler,
'data': data,
'response': response_queue
})
return response_queue
@staticmethod
def decode_headers(headers):
return {key: unquote(value, encoding='utf-8') for key, value in headers.items()}
logger = None
def setup_global_logger(config):
global logger
if logger is None:
app_tools = AppTools(config)
logger = app_tools.logger
return logger
+96
View File
@@ -0,0 +1,96 @@
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import json
import anyio
from app.utils.app_tools import AppTools, setup_global_logger
from app.module.F6_Plugin_module import F6PluginModule
from app.module.module import F6Module
from app.module.other_module import OtherPluginModule
from app.config import Config
app = FastAPI(title="简道云FastAPI服务")
@app.on_event("startup")
def on_startup():
"""应用启动时初始化"""
app.state.app_tools = AppTools(Config)
app.state.logger = setup_global_logger(Config)
app.state.f6_module = F6Module()
app.state.f6_plugin_module = F6PluginModule()
app.state.other_module = OtherPluginModule()
def get_action_map() -> dict:
"""获取操作映射表"""
f6_module = app.state.f6_module
f6_plugin_module = app.state.f6_plugin_module
other_module = app.state.other_module
return {
'login_in': f6_module.accept_login_message,
'get_company_information': f6_module.get_company_information,
'get_store_information': f6_module.get_store_information,
"keep_alive": f6_module.get_keep_heart,
'check_file': f6_plugin_module.check_file,
'create_brand': f6_plugin_module.create_brand,
'delete_history': f6_plugin_module.delete_history,
'delete_customer': f6_plugin_module.delete_customer,
'delete_cars': f6_plugin_module.delete_cars,
'sms_signature_status': other_module.sms_signature_status,
'modify_customer_info': f6_plugin_module.modify_customer_info,
}
@app.post("/webhook")
async def webhook(request: Request):
"""
接受前端请求后将任务放入消息队列
Returns:
any: 返回任务处理的结果
"""
logger = app.state.logger
app_tools: AppTools = app.state.app_tools
# 获取请求数据
data = await request.json()
header = request.headers
# 解码请求头
decoded_header = app_tools.decode_headers(header)
# 获取操作映射表
action_map = get_action_map()
action = decoded_header.get('Action')
# 处理 F6_Plugin 特殊逻辑
if action == 'F6_Plugin':
check = decoded_header.get('Check')
if check == '':
handler = app.state.f6_plugin_module.check_file
elif check == '':
print(data)
sub_action = data.get('Action')
print(sub_action)
handler = action_map.get(sub_action, lambda x: {'msg': '未执行'})
else:
return JSONResponse({'msg': '未知的操作'})
else:
handler = action_map.get(action, lambda x: {'msg': '未知的操作'})
# 将任务放入消息队列
response_queue = app_tools.enqueue_task(handler, data)
# 等待任务处理结果
result = await anyio.to_thread.run_sync(response_queue.get)
print(handler)
logger.info(json.dumps(result, ensure_ascii=False, indent=4))
return JSONResponse(result)
if __name__ == '__main__':
import uvicorn
uvicorn.run("fastapi_app.main:app", host="0.0.0.0", port=5003, reload=False)
+11
View File
@@ -0,0 +1,11 @@
anyio==4.11.0
apscheduler==3.11.1
fastapi==0.121.0
log_config==2.1.1
numpy==2.3.4
pandas==2.3.3
Pillow==12.0.0
pytesseract==0.3.13
Requests==2.32.5
tqdm==4.67.1
uvicorn==0.38.0
+97
View File
@@ -0,0 +1,97 @@
import logging
import os
from logging.handlers import RotatingFileHandler
from queue import Queue
import threading
from apscheduler.schedulers.background import BackgroundScheduler
from urllib.parse import unquote
class AppTools:
def __init__(self, config):
self.config = config
self.task_queue = Queue()
self.logger = self._setup_logger()
self.scheduler = self._setup_scheduler()
self._start_task_thread()
def _setup_logger(self):
"""配置日志记录器(不依赖 Flask)。"""
log_dir = self.config.LOGS_DIRECTORY
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = self.config.LOG_FILE
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
# 防止重复添加 handler
if not any(isinstance(h, RotatingFileHandler) and getattr(h, 'baseFilename', None) == str(log_file)
for h in logger.handlers):
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024 * 5, backupCount=5, encoding='utf-8')
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s:%(name)s:%(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
def _setup_scheduler(self):
"""配置后台调度器"""
scheduler = BackgroundScheduler()
# 如需定时任务可在此添加
import atexit
atexit.register(lambda: scheduler.shutdown(wait=False))
return scheduler
def _start_task_thread(self):
"""启动任务处理线程"""
task_thread = threading.Thread(target=self.process_tasks, daemon=True)
task_thread.start()
def process_tasks(self):
"""处理任务队列中的任务"""
while True:
task = self.task_queue.get()
if task is None:
self.logger.error("任务处理线程已终止")
break
try:
result = task['handler'](task['data'])
task['response'].put(result)
except Exception as e:
self.logger.error(f"任务执行失败: {str(e)}")
task['response'].put({'msg': f'任务执行失败: {str(e)}'})
finally:
self.task_queue.task_done()
self.logger.info("任务处理完成")
def enqueue_task(self, handler, data):
"""将任务放入消息队列"""
response_queue = Queue()
self.task_queue.put({
'handler': handler,
'data': data,
'response': response_queue
})
return response_queue
@staticmethod
def decode_headers(headers):
"""解码包含中文字符的 HTTP 请求头"""
return {key: unquote(value, encoding='utf-8') for key, value in headers.items()}
# 简易的全局日志记录器设置(与原项目解耦)
logger = None
def setup_global_logger(config):
global logger
if logger is None:
app_tools = AppTools(config)
logger = app_tools.logger
return logger