简道云V2.0

This commit is contained in:
z66
2025-11-14 11:04:01 +08:00
parent 073f0646a1
commit 49fc75214f
33 changed files with 1811 additions and 4454 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="fastapi_app" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
-225
View File
@@ -1,225 +0,0 @@
# 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
@@ -1,506 +0,0 @@
# 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
@@ -1,710 +0,0 @@
# 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
@@ -1,496 +0,0 @@
# 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
@@ -1,352 +0,0 @@
# 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
@@ -1,620 +0,0 @@
# 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` 一起学习效果更好。
+391
View File
@@ -0,0 +1,391 @@
# 简道云 FastAPI 服务
简道云插件后端服务,提供数据同步和处理功能。
## 📋 项目简介
本项目是一个基于 FastAPI 框架的简道云插件后端服务,主要用于处理简道云插件发送的请求,执行各种业务操作。系统与 F6 汽车维修管理系统集成,提供以下核心功能:
- **F6系统集成**: 登录认证、公司信息获取、门店信息管理
- **文件处理**: 文件上传、校验、Excel批量处理
- **数据管理**: 品牌批量创建、客户信息管理、车辆信息管理
- **数据清理**: 历史记录删除、客户删除、车辆删除
- **BI任务**: 数据处理和报表生成
- **工作流自动化**: 自动提交简道云工作流
## 🏗️ 项目结构
```
fastapi_app/
├── main.py # 应用入口,生命周期管理
├── requirements.txt # Python依赖清单
├── README.md # 项目文档(本文件)
├── app/ # 应用主目录
│ ├── __init__.py
│ ├── config.py # 配置管理(目录、API Token等)
│ ├── schemas.py # Pydantic数据模型定义
│ │
│ ├── api/ # API路由模块
│ │ ├── __init__.py # 简道云API封装类
│ │ ├── routes.py # FastAPI路由定义
│ │ └── dependencies.py # 依赖注入函数
│ │
│ ├── core/ # 核心功能模块
│ │ ├── __init__.py # 核心管理器
│ │ └── module_registry.py # 模块注册表
│ │
│ ├── module/ # 业务模块
│ │ ├── __init__.py
│ │ ├── module.py # F6Module - F6系统相关功能
│ │ ├── f6_plugin_module.py # F6PluginModule - F6插件功能
│ │ └── other_module.py # OtherPluginModule - 其他功能
│ │
│ ├── tasks/ # 后台任务模块
│ │ ├── __init__.py # 任务统一导出
│ │ ├── common.py # 通用任务函数
│ │ ├── brand_tasks.py # 品牌相关任务
│ │ ├── customer_tasks.py # 客户相关任务
│ │ ├── delete_tasks.py # 删除相关任务
│ │ └── bi_tasks.py # BI相关任务
│ │
│ └── utils/ # 工具模块
│ └── app_tools.py # 应用工具类(日志、任务队列等)
├── logs/ # 日志目录
├── 下载文件/ # 下载文件存储目录
└── 模板文件/ # 模板文件存储目录
```
## 🚀 快速开始
### 环境要求
- Python 3.8+
- pip
- Tesseract OCR(用于验证码识别,可选)
### 安装依赖
```bash
pip install -r requirements.txt
```
### 配置说明
编辑 `app/config.py` 文件,配置以下内容:
- `JIANDAOYUN_API_TOKEN`: 简道云API Token(生产环境建议使用环境变量)
### 运行应用
```bash
# 方式1:直接运行
python main.py
# 方式2:使用 uvicorn(推荐)
uvicorn main:app --host 0.0.0.0 --port 5003 --reload
```
### 访问API文档
启动应用后,访问以下地址查看API文档:
- **Swagger UI**: http://localhost:5003/docs
- **ReDoc**: http://localhost:5003/redoc
## 📡 API 接口
### 健康检查
```http
GET /health
```
**响应示例:**
```json
{
"status": "ok",
"version": "1.0.0"
}
```
### Webhook 接口
```http
POST /webhook
```
**请求头:**
- `Action`: 操作类型(必需)
- `Check`: 检查标志(F6_Plugin 操作时需要,值为"是"或"否"
**请求体:**
```json
{
"api_key": "简道云应用ID",
"entry_id": "简道云表单ID",
"data_id": "简道云数据ID",
"Action": "操作类型(F6_Plugin 操作时需要)",
"username": "用户名",
"password": "密码",
"company_name": "公司名称"
}
```
**响应示例:**
```json
{
"msg": "操作完成",
"msg_details": "详细信息",
"status": "success"
}
```
## 🔧 支持的操作
### F6Module 操作
| 操作名 | 描述 | 说明 |
|--------|------|------|
| `login_in` | F6系统登录 | 使用用户名、密码登录F6系统,支持验证码识别 |
| `get_company_information` | 获取公司信息 | 获取F6系统中的公司列表,保存到简道云 |
| `get_store_information` | 获取门店信息 | 获取指定公司的门店列表及统计数据 |
| `keep_alive` | 保持连接 | 心跳检测,保持连接活跃 |
### F6PluginModule 操作
| 操作名 | 描述 | 说明 |
|--------|------|------|
| `check_file` | 校验上传文件 | 校验Excel文件格式,支持品牌创建、客户修改等场景 |
| `create_brand` | 创建品牌 | 批量创建品牌,从Excel文件读取品牌名称 |
| `delete_history` | 删除历史记录 | 删除指定门店的历史维修记录 |
| `delete_customer` | 删除客户 | 批量删除客户信息(会检查会员卡和消费记录) |
| `delete_cars` | 删除车辆 | 批量删除客户车辆信息(会检查会员卡和消费记录) |
| `modify_customer_info` | 修改客户信息 | 批量修改客户信息,从Excel文件读取修改内容 |
| `bi_task` | BI任务 | 执行BI相关任务(数据处理、报表生成等) |
### OtherPluginModule 操作
| 操作名 | 描述 | 说明 |
|--------|------|------|
| `sms_signature_status` | 短信签名状态 | 查询短信签名状态(待实现) |
## 🛠️ 技术栈
- **FastAPI**: 现代、快速的 Web 框架,支持异步处理
- **Pydantic**: 数据验证和设置管理
- **Uvicorn**: ASGI 服务器
- **APScheduler**: 后台任务调度
- **Requests**: HTTP 请求库
- **Pandas**: Excel文件处理和数据分析
- **Pillow**: 图像处理
- **Pytesseract**: OCR 识别(用于验证码识别)
## 📦 依赖列表
```
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
```
## 🏛️ 架构设计
### 核心特性
1. **模块化设计**: 业务逻辑按模块分离,易于维护和扩展
2. **依赖注入**: 使用 FastAPI 的依赖注入系统,减少耦合
3. **模块注册表**: 使用 `module_registry` 统一管理所有操作
4. **路由分离**: API 路由分离到独立文件,职责清晰
5. **数据验证**: 使用 Pydantic schemas 进行请求和响应验证
6. **异常处理**: 完善的异常处理机制,统一的错误响应格式
7. **日志记录**: 规范的日志记录,支持日志轮转(单文件5MB,保留5个备份)
### 生命周期管理
应用使用 FastAPI 的 `lifespan` 功能管理应用生命周期:
- **启动时**:
- 初始化工具类(AppTools
- 设置全局日志记录器
- 初始化业务模块(F6Module、F6PluginModule、OtherPluginModule
- 注册所有操作到模块注册表
- **运行时**: 处理请求,执行业务逻辑
- **关闭时**: 清理资源,关闭后台调度器
### 任务处理机制
系统支持两种任务处理方式:
1. **同步任务**:
- 通过任务队列同步执行,等待结果返回
- 适用于快速操作(如登录、信息获取)
- 超时时间:55秒(简道云默认60秒超时)
2. **后台任务**:
- 在模块函数内部启动线程执行耗时操作
- 立即返回"正在执行中"的提示
- 适用于批量操作(如品牌创建、数据删除)
- 执行完成后自动更新简道云表单并提交工作流
### 简道云API封装
`app/api/__init__.py` 中的 `API` 类封装了简道云的所有API接口:
- **表单数据操作**: 获取、创建、更新、删除(单条/批量)
- **表单字段获取**: 获取表单字段信息,支持字段ID到标签名的替换
- **工作流操作**: 获取实例、审批、转交
- **文件操作**: 获取上传凭证、上传文件
- **重试机制**: 所有API调用都支持失败重试(默认最多20次)
## 🔐 配置说明
配置文件位于 `app/config.py`,主要配置项:
| 配置项 | 说明 | 默认值 |
|--------|------|--------|
| `BASE_DIR` | 项目根目录 | 自动获取 |
| `SAVE_DIRECTORY` | 下载文件保存目录 | `下载文件/` |
| `MODE_DIRECTORY` | 模板文件保存目录 | `模板文件/` |
| `LOGS_DIRECTORY` | 日志文件目录 | `logs/` |
| `LOG_FILE` | 日志文件路径 | `logs/简道云.log` |
| `JIANDAOYUN_API_TOKEN` | 简道云API Token | 需配置 |
## 📝 开发指南
### 添加新操作
1. **在模块中实现方法**:
```python
# app/module/your_module.py
class YourModule:
def your_action(self, data: Dict[str, Any]) -> Dict[str, str]:
# 实现业务逻辑
return {'msg': '操作完成'}
```
2. **在 main.py 中注册**:
```python
# 在 lifespan 函数中
core_manager.register_action(
'your_action',
your_module.your_action,
'your_module',
description='操作描述'
)
```
### 添加新路由
在 `app/api/routes.py` 中添加新的路由:
```python
@router.post("/your-endpoint", tags=["业务"])
async def your_endpoint(
request: Request,
logger: logging.Logger = Depends(get_logger)
):
# 实现路由逻辑
return {"message": "success"}
```
### 添加新的数据模型
在 `app/schemas.py` 中添加新的 Pydantic 模型:
```python
class YourRequest(BaseModel):
field1: str = Field(..., description="字段1")
field2: Optional[int] = Field(None, description="字段2")
```
### 添加后台任务
1. **在 tasks 目录下创建任务文件**:
```python
# app/tasks/your_tasks.py
def your_task_background(data, ...):
# 实现后台任务逻辑
# 完成后调用 update_jiandaoyun 和 approve_workflow
```
2. **在模块中调用**:
```python
import threading
from app.tasks.your_tasks import your_task_background
thread = threading.Thread(target=your_task_background, args=(...))
thread.start()
return {'msg': '正在执行中'}
```
## 🧪 测试
### 健康检查测试
```bash
curl http://localhost:5003/health
```
### Webhook 测试
```bash
curl -X POST http://localhost:5003/webhook \
-H "Content-Type: application/json" \
-H "Action: login_in" \
-d '{
"username": "test",
"password": "test",
"company_name": "测试公司"
}'
```
## 📊 日志
日志文件位于 `logs/简道云.log`,支持日志轮转:
- 单个日志文件最大 5MB
- 保留 5 个备份文件
- 使用 UTF-8 编码
- 日志格式:`时间戳 级别:模块名:消息`
## ⚠️ 注意事项
1. **API Token**: 当前 API Token 硬编码在配置文件中,生产环境建议使用环境变量管理
2. **CORS**: 当前配置允许所有来源,生产环境建议限制允许的来源
3. **超时时间**: 任务执行超时时间默认为 55 秒,可根据需要调整
4. **文件存储**: 下载的文件存储在 `下载文件/` 目录,注意定期清理
5. **验证码识别**: 需要系统安装 Tesseract OCR 才能正常识别验证码
6. **删除操作**: 删除客户和车辆时会检查会员卡和消费记录,有记录的数据会被跳过
7. **批量操作**: 批量操作在后台线程中执行,不会阻塞主请求
## 🔄 版本历史
- **v1.0.0**: 初始版本
- 实现基本的 Webhook 接口
- 支持 F6 系统相关操作
- 支持文件上传和校验
- 支持品牌、客户、车辆管理
- 支持数据删除操作
- 支持BI任务处理
## 📄 许可证
本项目为内部项目,仅供内部使用。
## 📞 联系方式
如有问题,请联系数据组。
---
**最后更新**: 2025年
-186
View File
@@ -1,186 +0,0 @@
# 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
@@ -1,2 +0,0 @@
__all__ = []
-793
View File
@@ -1,793 +0,0 @@
"""
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
+148
View File
@@ -0,0 +1,148 @@
"""
FastAPI 依赖注入模块
本模块提供 FastAPI 的依赖注入函数,用于在路由中注入可复用的依赖项。
使用依赖注入可以减少代码重复,提高可测试性和可维护性。
提供的依赖项:
- get_logger: 获取日志记录器
- get_app_tools: 获取应用工具实例
- get_f6_module: 获取 F6Module 实例
- get_f6_plugin_module: 获取 F6PluginModule 实例
- get_other_module: 获取 OtherPluginModule 实例
- get_action_map: 获取操作映射表
"""
from fastapi import Request
from typing import Optional
import logging
def get_logger(request: Request) -> logging.Logger:
"""
获取日志记录器依赖项
从应用状态中获取日志记录器,如果未初始化则返回一个基本的 logger。
Args:
request: FastAPI 请求对象
Returns:
logging.Logger: 日志记录器实例
"""
logger = getattr(request.app.state, 'logger', None)
if logger is None:
# 如果 logger 未初始化,返回一个基本的 logger
logger = logging.getLogger('app')
return logger
def get_app_tools(request: Request):
"""
获取应用工具实例依赖项
从应用状态中获取 AppTools 实例,用于任务队列管理等操作。
Args:
request: FastAPI 请求对象
Returns:
AppTools: 应用工具实例
Raises:
RuntimeError: 如果 AppTools 未初始化
"""
from app.utils.app_tools import AppTools
app_tools = getattr(request.app.state, 'app_tools', None)
if app_tools is None:
raise RuntimeError("AppTools 未初始化")
return app_tools
def get_f6_module(request: Request):
"""
获取 F6Module 实例依赖项
从应用状态中获取 F6Module 实例,用于 F6 系统相关操作。
Args:
request: FastAPI 请求对象
Returns:
F6Module: F6Module 实例
Raises:
RuntimeError: 如果 F6Module 未初始化
"""
f6_module = getattr(request.app.state, 'f6_module', None)
if f6_module is None:
raise RuntimeError("F6Module 未初始化")
return f6_module
def get_f6_plugin_module(request: Request):
"""
获取 F6PluginModule 实例依赖项
从应用状态中获取 F6PluginModule 实例,用于 F6 插件相关操作。
Args:
request: FastAPI 请求对象
Returns:
F6PluginModule: F6PluginModule 实例
Raises:
RuntimeError: 如果 F6PluginModule 未初始化
"""
f6_plugin_module = getattr(request.app.state, 'f6_plugin_module', None)
if f6_plugin_module is None:
raise RuntimeError("F6PluginModule 未初始化")
return f6_plugin_module
def get_other_module(request: Request):
"""
获取 OtherPluginModule 实例依赖项
从应用状态中获取 OtherPluginModule 实例,用于其他插件相关操作。
Args:
request: FastAPI 请求对象
Returns:
OtherPluginModule: OtherPluginModule 实例
Raises:
RuntimeError: 如果 OtherPluginModule 未初始化
"""
other_module = getattr(request.app.state, 'other_module', None)
if other_module is None:
raise RuntimeError("OtherPluginModule 未初始化")
return other_module
def get_action_map(request: Request) -> dict:
"""
获取操作映射表依赖项
从模块注册表中获取所有注册的操作,并转换为字典格式(操作名 -> 处理函数)。
Args:
request: FastAPI 请求对象
Returns:
dict: 操作映射表,格式为 {操作名: 处理函数}
"""
from app.core.module_registry import registry
# 从 registry 获取所有注册的操作
actions = registry.get_all_actions()
# 转换为字典格式(handler 函数)
action_map = {
action_name: config.handler
for action_name, config in actions.items()
}
return action_map
+171
View File
@@ -0,0 +1,171 @@
"""
API 路由定义模块
本模块定义所有 API 路由端点,包括:
- /health: 健康检查端点
- /webhook: Webhook 端点,处理简道云插件的请求
所有路由都使用 FastAPI 的依赖注入系统,通过 dependencies.py 中的函数注入依赖项。
"""
from fastapi import APIRouter, Request, HTTPException, status, Depends
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from typing import Dict, Any
import json
import anyio
import asyncio
import logging
from app.schemas import WebhookRequest, WebhookResponse, HealthResponse
from app.api.dependencies import (
get_logger,
get_app_tools,
get_f6_plugin_module,
get_action_map
)
from app.utils.app_tools import AppTools
# 创建路由器
# 使用 APIRouter 分离路由,便于管理和维护
router = APIRouter()
@router.get("/health", response_model=HealthResponse, tags=["系统"])
async def healthcheck():
"""
健康检查端点
用于检查服务是否正常运行
"""
return HealthResponse(status="ok", version="1.0.0")
@router.post("/webhook", response_model=WebhookResponse, tags=["业务"])
async def webhook(
request: Request,
logger: logging.Logger = Depends(get_logger),
app_tools: AppTools = Depends(get_app_tools),
f6_plugin_module = Depends(get_f6_plugin_module),
action_map: Dict[str, Any] = Depends(get_action_map)
):
"""
接受前端请求后将任务放入消息队列
此端点接收简道云插件的请求,根据请求头中的 Action 字段路由到相应的处理函数。
支持的操作包括:登录、获取公司信息、文件校验、品牌创建等。
Args:
request: FastAPI 请求对象,包含请求体和请求头
logger: 日志记录器
app_tools: 应用工具实例
f6_plugin_module: F6插件模块实例
action_map: 操作映射表
Returns:
WebhookResponse: 任务处理结果
Raises:
HTTPException: 当操作类型无效或任务执行超时时抛出
"""
try:
# 获取请求数据并验证
try:
raw_data = await request.json()
# 使用 Pydantic 进行数据验证(允许额外字段)
webhook_data = WebhookRequest(**raw_data)
data = webhook_data.dict(exclude_none=True)
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="请求体必须是有效的 JSON 格式"
)
except Exception as e:
logger.warning(f"请求数据验证失败: {str(e)}")
# 如果验证失败,仍然尝试使用原始数据(向后兼容)
data = raw_data if 'raw_data' in locals() else {}
# 获取并解码请求头
header = request.headers
decoded_header = app_tools.decode_headers(header)
# 验证 Action 字段
action = decoded_header.get('Action')
if not action:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="请求头中缺少必需的 Action 字段"
)
# 处理 F6_Plugin 特殊逻辑
if action == 'F6_Plugin':
check = decoded_header.get('Check')
if check == '':
handler = f6_plugin_module.check_file
elif check == '':
sub_action = data.get('Action')
if not sub_action:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="F6_Plugin 操作需要提供 Action 字段"
)
handler = action_map.get(sub_action)
if not handler:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"未知的子操作类型: {sub_action}"
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"F6_Plugin 操作需要提供有效的 Check 字段(是/否),当前值: {check}"
)
else:
handler = action_map.get(action)
if not handler:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"未知的操作类型: {action}。支持的操作: {', '.join(action_map.keys())}"
)
logger.info(f"接收到操作请求: {action}, 数据ID: {data.get('data_id', 'N/A')}")
# 将任务放入消息队列
response_queue = app_tools.enqueue_task(handler, data)
# 等待任务处理结果(添加超时保护,简道云默认60秒)
try:
# 使用 asyncio.wait_for 添加超时
result = await asyncio.wait_for(
anyio.to_thread.run_sync(response_queue.get),
timeout=55.0
)
except asyncio.TimeoutError:
logger.error(f"任务执行超时: {action}, 数据ID: {data.get('data_id', 'N/A')}")
raise HTTPException(
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
detail="任务执行超时,请稍后重试"
)
# 验证返回结果格式
if not isinstance(result, dict):
result = {"msg": str(result)}
if "msg" not in result:
result["msg"] = "操作完成"
logger.info(f"操作完成: {action}, 结果: {json.dumps(result, ensure_ascii=False)}")
# 返回响应(使用 Pydantic 模型验证)
return WebhookResponse(**result)
except HTTPException:
# 重新抛出 HTTP 异常
raise
except Exception as e:
# 捕获其他未预期的异常
logger.error(f"处理请求时发生未预期的错误: {type(e).__name__} - {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"处理请求时发生错误: {str(e)}"
)
-24
View File
@@ -1,24 +0,0 @@
"""
后台任务模块 - 向后兼容入口
此文件保持向后兼容,实际功能已拆分到 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',
]
+31 -1
View File
@@ -1,29 +1,59 @@
"""
应用配置模块
本模块负责管理应用的所有配置项,包括:
- 目录路径配置
- API Token 配置
- 日志配置
注意:生产环境建议将敏感信息(如 API Token)移至环境变量。
"""
from pathlib import Path
# 获取当前文件所在的目录
# 当前文件位于 app/config.pyparent.parent 获取项目根目录
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 配置
# 简道云 API Token,用于调用简道云 API
# 注意:生产环境建议使用环境变量管理此配置
JIANDAOYUN_API_TOKEN = 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN' # 曹伟应用api测试 app_key
# 导出配置
class Config:
"""
应用配置类
统一管理应用的所有配置项,方便在应用中使用。
属性:
BASE_DIR: 项目根目录路径
SAVE_DIRECTORY: 下载文件保存目录
MODE_DIRECTORY: 模板文件保存目录
JIANDAOYUN_API_TOKEN: 简道云 API Token
LOGS_DIRECTORY: 日志文件目录
LOG_FILE: 日志文件路径
"""
BASE_DIR = BASE_DIR
SAVE_DIRECTORY = SAVE_DIRECTORY
MODE_DIRECTORY = MODE_DIRECTORY
+16 -5
View File
@@ -1,17 +1,28 @@
"""
核心模块初始化
统一初始化请求头管理器、模块注册表等核心组件
本模块统一初始化和管理核心组件,包括:
- ModuleRegistry: 模块注册表
- CoreManager: 核心管理器
提供统一的接口来管理这些核心组件。
"""
from typing import Dict, Any, Callable
from app.core.header_manager import HeaderManager
from app.core.module_registry import ModuleRegistry, registry
class CoreManager:
"""核心管理器 - 统一管理所有核心组件"""
"""
核心管理器
统一管理所有核心组件,提供便捷的方法来初始化和注册模块。
属性:
registry: 模块注册表实例
"""
def __init__(self):
self.header_manager = HeaderManager
"""初始化核心管理器"""
self.registry = registry
def initialize_modules(self, modules: Dict[str, Any]):
@@ -49,12 +60,12 @@ class CoreManager:
# 全局核心管理器实例
# 在应用启动时使用此实例来注册模块和操作
core_manager = CoreManager()
# 导出常用类和函数
__all__ = [
'core_manager',
'HeaderManager',
'ModuleRegistry',
'registry',
]
-118
View File
@@ -1,118 +0,0 @@
"""
请求头管理器
统一管理不同模块的请求头配置
"""
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
+38 -5
View File
@@ -1,6 +1,12 @@
"""
模块注册和路由管理
提供统一的模块注册机制,方便添加新功能模块
模块注册表模块
本模块提供统一的模块注册机制,用于管理所有业务模块和操作。
使用注册表模式可以:
- 统一管理所有操作
- 方便添加新功能模块
- 支持动态路由和操作查找
- 提供操作元数据管理
"""
from typing import Dict, Callable, Optional, Any
from dataclasses import dataclass
@@ -8,7 +14,18 @@ from dataclasses import dataclass
@dataclass
class ActionConfig:
"""操作配置"""
"""
操作配置数据类
存储操作的配置信息,包括处理函数、所属模块、描述等。
属性:
handler: 处理函数,执行具体业务逻辑
module_name: 所属模块名称
description: 操作描述,用于文档和日志
requires_auth: 是否需要认证,默认 True
header_module: 使用的请求头模块名称,可选
"""
handler: Callable # 处理函数
module_name: str # 所属模块名称
description: Optional[str] = None # 描述
@@ -17,9 +34,19 @@ class ActionConfig:
class ModuleRegistry:
"""模块注册表"""
"""
模块注册表类
统一管理所有业务模块和操作的注册表。
支持注册模块实例和操作,并提供查询功能。
属性:
_actions: 操作字典,格式为 {操作名: ActionConfig}
_modules: 模块字典,格式为 {模块名: 模块信息}
"""
def __init__(self):
"""初始化模块注册表"""
self._actions: Dict[str, ActionConfig] = {}
self._modules: Dict[str, Dict[str, Any]] = {}
@@ -65,7 +92,12 @@ class ModuleRegistry:
return self._actions.get(action_name)
def get_all_actions(self) -> Dict[str, ActionConfig]:
"""获取所有注册的操作"""
"""
获取所有注册的操作
Returns:
Dict[str, ActionConfig]: 所有注册的操作字典的副本
"""
return self._actions.copy()
def register_module(self, module_name: str, module_instance: Any, **metadata):
@@ -113,4 +145,5 @@ class ModuleRegistry:
# 全局模块注册表实例
# 在应用启动时使用此实例来注册所有模块和操作
registry = ModuleRegistry()
+154 -13
View File
@@ -1,26 +1,57 @@
"""
F6 插件模块
本模块提供 F6 插件相关的功能,包括:
- 文件上传和校验
- 品牌批量创建
- 历史记录删除
- 客户信息管理
- 车辆信息管理
依赖:
- requests: HTTP 请求
- pandas: Excel 文件处理
- threading: 后台任务处理
"""
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
from app.api import API
from app.config import Config
from app.module.module import F6Module
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
from app.tasks.bi_tasks import bi_task_background
# 简道云 API 实例,用于调用简道云 API
api_instance = API()
class F6PluginModule:
"""
F6 插件模块类
提供 F6 插件相关的所有功能,包括文件处理、品牌管理、数据删除等。
"""
@staticmethod
def accept_file(data: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any]]: # 接收文件
def accept_file(data: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any]]:
"""
接收文件
接收文件
处理前端上传的文件,下载文件并保存到指定目录。
此方法用于处理前端上传的文件,下载文件并保存到指定目录。主要步骤包括:
1. 处理前端传递的数据,获取文件的URL。
2. 解析URL以获取文件名。
@@ -147,7 +178,19 @@ class F6PluginModule:
@staticmethod
def create_brand(data: Dict[str, Any]) -> Dict[str, str]: # 创建品牌
def create_brand(data: Dict[str, Any]) -> Dict[str, str]:
"""
创建品牌
从简道云获取品牌创建请求,读取 Excel 文件,并在后台线程中批量创建品牌。
立即返回"正在执行"的提示,实际创建在后台线程中执行。
Args:
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
Returns:
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
"""
entry_data = api_instance.entry_data_get(data=data)
print('执行 品牌批量新建')
username = entry_data['data']['账号']
@@ -167,7 +210,7 @@ class F6PluginModule:
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
try:
thread = threading.Thread(target=back_ground_tasks.create_brand_background,
thread = threading.Thread(target=create_brand_background,
args=(data, cookies, df, save_path))
thread.start()
except Exception as e:
@@ -177,6 +220,18 @@ class F6PluginModule:
@staticmethod
def delete_history(data: Dict[str, Any]) -> Dict[str, str]:
"""
删除历史记录
从简道云获取删除历史记录请求,在后台线程中删除指定门店的历史维修记录。
立即返回"正在执行中"的提示,实际删除在后台线程中执行。
Args:
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
Returns:
Dict[str, str]: 包含执行状态的字典
"""
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
@@ -202,7 +257,7 @@ class F6PluginModule:
org_id = org['orgId']
if org_id:
thread = threading.Thread(target=back_ground_tasks.delete_history_background,
thread = threading.Thread(target=delete_history_background,
args=(data, cookies, org_id, org_name1))
thread.start()
return {'msg': '正在执行中', 'msg_details': '请稍后查看结果'}
@@ -211,6 +266,18 @@ class F6PluginModule:
@staticmethod
def delete_customer(data):
"""
删除客户
从简道云获取删除客户请求,在后台线程中批量删除客户信息。
立即返回"正在执行中"的提示,实际删除在后台线程中执行。
Args:
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
Returns:
Dict[str, str]: 包含执行状态的字典
"""
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
@@ -225,7 +292,7 @@ class F6PluginModule:
json = res.json()
if json:
thread = threading.Thread(target=back_ground_tasks.delete_customer_background,
thread = threading.Thread(target=delete_customer_background,
args=(data, cookies, json['data']['data'],))
thread.start()
return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'}
@@ -236,6 +303,18 @@ class F6PluginModule:
@staticmethod
def delete_cars(data):
"""
删除车辆
从简道云获取删除车辆请求,在后台线程中批量删除客户车辆信息。
立即返回"正在执行中"的提示,实际删除在后台线程中执行。
Args:
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
Returns:
Dict[str, str]: 包含执行状态的字典
"""
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
@@ -259,7 +338,7 @@ class F6PluginModule:
all_page = total_items // 100 + (total_items % 100 > 0)
if res_data:
thread = threading.Thread(target=back_ground_tasks.delete_car_background,
thread = threading.Thread(target=delete_car_background,
args=(data, url, cookies, header, all_page))
thread.start()
return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'}
@@ -270,6 +349,18 @@ class F6PluginModule:
return {'msg': '未执行', 'msg_details': '登录失败'}
def modify_customer_info(self, data: Dict[str, str]):
"""
修改客户信息
从简道云获取修改客户信息请求,读取 Excel 文件,并在后台线程中批量修改客户信息。
立即返回"正在执行中"的提示,实际修改在后台线程中执行。
Args:
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
Returns:
Dict[str, str]: 包含执行状态的字典
"""
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
@@ -288,11 +379,61 @@ class F6PluginModule:
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
if cookies:
thread = threading.Thread(target=back_ground_tasks.modify_customer_info_background,
thread = threading.Thread(target=modify_customer_info_background,
args=(data, cookies, df, save_path))
thread.start()
return {'msg': '正在执行中', 'msg_details': '请稍后查看结果'}
else:
return {'msg': '未执行', 'msg_details': 'cookies获取失败'}
@staticmethod
def bi_task(data: Dict[str, Any]) -> Dict[str, str]:
"""
BI任务
从简道云获取BI任务请求,读取 Excel 文件(如果需要),并在后台线程中执行BI任务。
立即返回"正在执行"的提示,实际执行在后台线程中完成。
Args:
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
Returns:
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
"""
entry_data = api_instance.entry_data_get(data=data)
print('执行 BI任务')
# 获取必要的参数(根据实际需求调整)
username = entry_data['data'].get('账号')
password = entry_data['data'].get('密码')
company_name = entry_data['data'].get('公司名称')
save_path = entry_data['data'].get('文件保存地址')
# 如果需要登录F6系统
cookies = None
if username and password and company_name:
login_response = F6Module.login_in(username, password, company_name)
if login_response is None:
return {'msg': '登录失败', 'msg_details': '无法登录F6系统'}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
# 如果需要读取Excel文件
df = None
if save_path:
try:
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
except Exception as e:
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
# 启动后台线程执行BI任务
try:
thread = threading.Thread(target=bi_task_background,
args=(data, cookies, df, save_path))
thread.start()
except Exception as e:
print(f'创建线程失败: {str(e)}')
return {'msg': '任务启动失败', 'msg_details': f'无法启动后台任务: {str(e)}'}
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
+8
View File
@@ -1,2 +1,10 @@
"""
业务模块包
本包包含所有业务模块包括
- module.py: F6Module - F6系统相关功能
- f6_plugin_module.py: F6PluginModule - F6插件功能
- other_module.py: OtherPluginModule - 其他功能模块
"""
__all__ = []
+96 -1
View File
@@ -1,3 +1,18 @@
"""
F6 系统模块
本模块提供 F6 系统相关的功能包括
- 登录和认证
- 验证码识别
- 公司信息获取
- 门店信息获取
- 保持连接
依赖:
- requests: HTTP 请求
- PIL: 图像处理
- pytesseract: OCR 识别
"""
import requests
import hashlib
from urllib.parse import quote
@@ -7,15 +22,33 @@ from typing import Optional, Dict, AnyStr
from PIL import Image, ImageEnhance
import pytesseract
import logging
from datetime import datetime
# 简道云 API 实例,用于调用简道云 API
api_instance = API()
# 日志记录器
logger = logging.getLogger('app')
class F6Module:
"""
F6 系统模块类
提供 F6 系统相关的所有功能包括登录信息获取等
"""
@staticmethod
def get_captcha() -> AnyStr:
"""
获取并识别验证码
F6 系统获取验证码图片使用 OCR 识别验证码文本
Returns:
AnyStr: 识别出的验证码文本
注意:
需要系统安装 Tesseract OCR 才能正常工作
"""
captcha_url = 'https://yunxiu.f6car.cn/kzf6/login/captcha-image'
response = requests.get(captcha_url)
with open('captcha.png', 'wb') as f:
@@ -37,6 +70,23 @@ class F6Module:
@staticmethod
def login_in(username: str, password: str, company_name: str = '默认门店',) -> Optional[requests.Response]:
"""
F6 系统登录
使用用户名和密码登录 F6 系统并选择指定的公司
如果触发验证码会自动识别并重试登录
Args:
username: 用户名
password: 密码明文方法内部会进行 MD5 加密
company_name: 公司名称默认为'默认门店'
Returns:
Optional[requests.Response]: 登录响应对象登录失败返回 None
注意:
密码会在方法内部进行 MD5 加密处理
"""
url = "https://yunxiu.f6car.com/kzf6/login/confirm"
session = requests.Session()
header = {
@@ -82,6 +132,17 @@ class F6Module:
return None
def accept_login_message(self, data: Dict[str, str]) -> Dict[str, str]:
"""
接受登录消息并处理
处理简道云插件发送的登录请求执行登录并返回结果
Args:
data: 包含用户名密码公司名称的字典
Returns:
Dict[str, str]: 登录结果包含状态信息
"""
username = data['username']
password = data['password']
company_name = data['company_name']
@@ -110,6 +171,17 @@ class F6Module:
return {"status": "登录失败,请检查公司名称"}
def get_company_information(self, data: Dict[str, str]) -> Dict[str, str]:
"""
获取公司信息
根据用户名和密码获取 F6 系统中的公司信息并将结果保存到简道云
Args:
data: 包含用户名密码的字典
Returns:
Dict[str, str]: 包含时间戳的消息用于后续查询
"""
username = data['username']
password = data['password']
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
@@ -173,6 +245,18 @@ class F6Module:
return res
def get_store_information(self, data: Dict[str, str]) -> Dict[str, dict[str, str]]:
"""
获取门店信息
根据用户名密码和公司名称获取 F6 系统中的门店信息
包括门店列表客户车辆数量客户数量等
Args:
data: 包含用户名密码公司名称的字典
Returns:
Dict[str, dict[str, str]]: 包含时间戳门店信息统计数据的结果
"""
username = data['username']
password = data['password']
company_name = data['company_name']
@@ -221,6 +305,17 @@ class F6Module:
@staticmethod
def get_keep_heart(data: Dict[str, str]) -> Dict[str, str]:
"""
保持连接
用于保持连接的心跳检测直接返回接收到的数据
Args:
data: 接收到的数据字典
Returns:
Dict[str, str]: 原样返回接收到的数据
"""
return data
+18 -14
View File
@@ -1,22 +1,26 @@
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):
"""
短信签名状态
查询短信签名状态待实现
Returns:
待实现
"""
pass
+77
View File
@@ -0,0 +1,77 @@
"""
Pydantic 数据模型定义
用于 FastAPI 请求和响应的数据验证
"""
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field
from enum import Enum
class ActionType(str, Enum):
"""支持的操作类型枚举"""
LOGIN_IN = "login_in"
GET_COMPANY_INFORMATION = "get_company_information"
GET_STORE_INFORMATION = "get_store_information"
KEEP_ALIVE = "keep_alive"
CHECK_FILE = "check_file"
CREATE_BRAND = "create_brand"
DELETE_HISTORY = "delete_history"
DELETE_CUSTOMER = "delete_customer"
DELETE_CARS = "delete_cars"
SMS_SIGNATURE_STATUS = "sms_signature_status"
MODIFY_CUSTOMER_INFO = "modify_customer_info"
F6_PLUGIN = "F6_Plugin"
class WebhookRequest(BaseModel):
"""Webhook 请求体数据模型"""
# 通用字段
api_key: Optional[str] = Field(None, description="简道云应用ID")
entry_id: Optional[str] = Field(None, description="简道云表单ID")
data_id: Optional[str] = Field(None, description="简道云数据ID")
Action: Optional[str] = Field(None, description="操作类型")
# 登录相关字段
username: Optional[str] = Field(None, description="用户名")
password: Optional[str] = Field(None, description="密码")
company_name: Optional[str] = Field(None, description="公司名称")
# 文件相关字段
file_path: Optional[str] = Field(None, description="文件保存路径")
# 其他字段(允许任意额外字段)
class Config:
extra = "allow" # 允许额外字段,因为简道云可能传递其他字段
class WebhookHeader(BaseModel):
"""Webhook 请求头数据模型"""
Action: Optional[str] = Field(None, description="操作类型")
Check: Optional[str] = Field(None, description="检查标志(是/否)")
class Config:
extra = "allow" # 允许额外请求头
class WebhookResponse(BaseModel):
"""Webhook 响应数据模型"""
msg: str = Field(..., description="响应消息")
msg_details: Optional[str] = Field(None, description="详细信息")
check: Optional[str] = Field(None, description="检查结果")
status: Optional[str] = Field(None, description="状态")
class Config:
extra = "allow" # 允许额外字段
class HealthResponse(BaseModel):
"""健康检查响应模型"""
status: str = Field("ok", description="服务状态")
version: Optional[str] = Field(None, description="服务版本")
class ErrorResponse(BaseModel):
"""错误响应模型"""
detail: str = Field(..., description="错误详情")
error_code: Optional[str] = Field(None, description="错误代码")
-89
View File
@@ -1,89 +0,0 @@
# 后台任务模块结构说明
## 模块结构
后台任务已按功能拆分为以下模块:
```
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(...)
```
两种方式都可以正常工作,代码执行逻辑完全一致。
+14 -1
View File
@@ -1,6 +1,14 @@
"""
后台任务模块统一导出入口
保持向后兼容所有原有导入方式仍然有效
本模块统一导出所有后台任务函数保持向后兼容
所有原有导入方式仍然有效
导出的任务包括
- 通用功能: update_jiandaoyun, approve_workflow
- 品牌任务: create_brand_background
- 删除任务: delete_history_background, delete_customer_background, delete_car_background
- 客户任务: modify_customer_info_background
"""
# 通用功能
from app.tasks.common import update_jiandaoyun, approve_workflow
@@ -18,6 +26,9 @@ from app.tasks.delete_tasks import (
# 客户相关任务
from app.tasks.customer_tasks import modify_customer_info_background
# BI相关任务
from app.tasks.bi_tasks import bi_task_background
__all__ = [
# 通用功能
'update_jiandaoyun',
@@ -30,5 +41,7 @@ __all__ = [
'delete_car_background',
# 客户任务
'modify_customer_info_background',
# BI任务
'bi_task_background',
]
+86
View File
@@ -0,0 +1,86 @@
"""
BI相关后台任务模块
本模块包含BI相关的后台任务包括
- BI数据处理
- BI报表生成
这些任务在后台线程中执行不会阻塞主请求
执行完成后会更新简道云表单并自动提交工作流
"""
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 bi_task_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame = None, save_path: str = None):
"""
BI任务后台执行函数
在后台线程中执行BI相关任务如数据处理报表生成等
执行完成后会更新简道云表单并自动提交工作流
Args:
data: 包含表单ID(api_key)表单ID(entry_id)数据ID(data_id)的字典
cookies: 用户登录 F6 系统的 cookies 信息如果需要
df: Excel 文件读取的内容DataFrame 格式如果需要
save_path: Excel 文件保存的地址执行完成后会删除此文件如果需要
Returns:
None
注意:
- 这是一个示例函数需要根据实际BI任务需求进行实现
- 执行完成后会自动删除上传的文件如果提供了save_path
- 执行结果会更新到简道云表单
"""
try:
# TODO: 在这里实现具体的BI任务逻辑
# 例如:数据处理、报表生成、数据同步等
# 示例:处理数据
results = []
if df is not None:
df = df.where(pd.notnull(df), None)
for index, row in tqdm(df.iterrows(), total=df.shape[0], desc="处理BI数据"):
# TODO: 实现具体的数据处理逻辑
# 例如:调用BI API、生成报表、数据转换等
result_item = {
'行号': index + 1,
'状态': '处理成功'
}
results.append(result_item)
else:
# 如果没有DataFrame,执行其他BI任务
# TODO: 实现其他BI任务逻辑
results.append({'状态': 'BI任务执行成功'})
# 删除文件(如果提供了save_path)
if save_path and os.path.exists(save_path):
os.remove(save_path)
logger.info(f'{save_path}已删除')
# 格式化结果
results_str = f'{results}' if results else 'BI任务执行完成'
logger.info(f"BI任务执行结果: {results_str}")
# 调用api回写改掉 执行明细与执行状态
msg = update_jiandaoyun(data, results_str)
if msg.get('msg'):
approve_workflow(data)
logger.info('表单已自动提交至下一步')
except Exception as e:
error_msg = f'BI任务执行失败: {str(e)}'
logger.error(error_msg, exc_info=True)
msg = update_jiandaoyun(data, error_msg)
if msg.get('msg'):
approve_workflow(data)
+23 -7
View File
@@ -1,6 +1,10 @@
"""
品牌相关后台任务模块
包含品牌批量创建等功能
本模块包含品牌相关的后台任务包括
- 品牌批量创建
这些任务在后台线程中执行不会阻塞主请求
"""
import logging
import os
@@ -15,12 +19,24 @@ 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
品牌批量创建后台任务
在后台线程中批量创建品牌 Excel 文件中读取品牌名称并创建
执行完成后会更新简道云表单并自动提交工作流
Args:
data: 包含表单ID(api_key)表单ID(entry_id)数据ID(data_id)的字典
cookies: 用户登录 F6 系统的 cookies 信息
df: Excel 文件读取的内容DataFrame 格式第一列为品牌名称
save_path: Excel 文件保存的地址执行完成后会删除此文件
Returns:
None
注意:
- 无效的品牌名None空字符串会被跳过
- 执行完成后会自动删除上传的文件
- 执行结果会更新到简道云表单
"""
df = df.where(pd.notnull(df), None)
# 定义请求URL
+136 -7
View File
@@ -1,10 +1,19 @@
"""
通用后台任务模块
包含简道云表单更新和工作流审批等通用功能
本模块包含所有后台任务通用的功能包括
- 简道云表单更新
- 工作流审批
- 获取门店ID
- 获取会员卡列表
这些功能被多个后台任务模块复用
"""
import logging
import time
from typing import Dict, Any
import requests
from typing import Dict, Any, List, Optional, Callable
from tqdm import tqdm
from app.api import API
api_instance = API()
@@ -14,9 +23,15 @@ logger = logging.getLogger('app')
def update_jiandaoyun(data: Dict[str, Any], results: str):
"""
更新简道云表单
:param data: 包含表单id应用id数据id的字典
:param results: 执行结果信息
:return: 更新结果字典
将后台任务的执行结果更新到简道云表单中
Args:
data: 包含表单ID(api_key)表单ID(entry_id)数据ID(data_id)的字典
results: 执行结果信息将写入到表单的执行明细字段
Returns:
Dict: 更新结果字典{'msg': True} 表示成功{'msg': False} 表示失败
"""
# 定义简道云数据配置
jiandaoyun_data = {
@@ -44,8 +59,17 @@ def update_jiandaoyun(data: Dict[str, Any], results: str):
def approve_workflow(data: Dict[str, Any]):
"""
获取简道云当前流程节点并直接提交
:param data: 包含表单id应用id数据id的字典
:return: None
获取简道云工作流的当前待处理任务并自动提交到下一步
Args:
data: 包含表单ID(api_key)表单ID(entry_id)数据ID(data_id)的字典
Returns:
None
注意:
如果未找到待处理任务函数会记录错误并返回不会抛出异常
"""
# 获取简道云当前流程列表
json = api_instance.workflow_instance_get(data)
@@ -92,3 +116,108 @@ def approve_workflow(data: Dict[str, Any]):
except Exception as e:
logger.error(f"简道云工作流任务提交失败: {e}")
def get_operate_org_id(cookies: Dict[str, str]) -> Optional[str]:
"""
获取操作门店ID
从F6系统获取第一个门店的组织ID用于后续操作
Args:
cookies: 用户登录 F6 系统的 cookies 信息
Returns:
Optional[str]: 门店ID如果获取失败返回 None
注意:
如果未获取到门店信息或门店ID为空会记录错误日志并返回 None
"""
org_url = "https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=10&name="
try:
org_res = requests.get(url=org_url, cookies=cookies)
org_data = org_res.json().get("data", {})
org_list = org_data.get("list", [])
if not org_list or len(org_list) == 0:
logger.error("未获取到门店信息")
return None
operate_org_id = org_list[0].get("orgId")
if not operate_org_id:
logger.error("门店ID为空")
return None
logger.info(f"获取门店ID成功: {operate_org_id}")
return operate_org_id
except Exception as e:
logger.error(f"获取门店ID时发生错误: {e}")
return None
def get_card_list(
cookies: Dict[str, str],
operate_org_id: str,
extract_func: Callable[[Dict], Optional[str]] = None
) -> List[str]:
"""
获取会员卡列表
从F6系统获取指定门店的会员卡列表支持自定义提取逻辑
Args:
cookies: 用户登录 F6 系统的 cookies 信息
operate_org_id: 门店ID
extract_func: 自定义提取函数用于从会员卡数据中提取ID
如果不提供默认提取 idCustomer 字段
Returns:
List[str]: 会员卡ID列表
注意:
- 默认每页100条数据会自动分页获取所有数据
- 每页请求间隔0.2避免请求过快
"""
card_list = []
try:
# 获取第一页,确定总页数
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", 0))
if total_card == 0:
logger.info("未找到会员卡数据")
return card_list
total_page = total_card // 100 + (total_card % 100 > 0)
logger.info(f"会员卡总数: {total_card}, 总页数: {total_page}")
# 定义默认提取函数(提取客户ID
if extract_func is None:
def default_extract(card_item: Dict) -> Optional[str]:
return card_item.get("idCustomer")
extract_func = default_extract
# 分页获取所有会员卡数据
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_data_list = card_res.json().get("data", {}).get("data", [])
# 使用提取函数提取ID
for card_item in card_data_list:
extracted_id = extract_func(card_item)
if extracted_id is not None:
card_list.append(extracted_id)
time.sleep(0.2)
logger.info(f"获取会员卡列表成功,共 {len(card_list)}")
return card_list
except Exception as e:
logger.error(f"获取会员卡列表时发生错误: {e}")
return card_list
+20 -8
View File
@@ -1,6 +1,10 @@
"""
客户相关后台任务模块
包含修改客户信息等功能
本模块包含客户相关的后台任务包括
- 客户信息批量修改
这些任务在后台线程中执行不会阻塞主请求
"""
import logging
import requests
@@ -15,16 +19,24 @@ logger = logging.getLogger('app')
def modify_customer_info_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str):
"""
修改客户信息后台任务
此函数用于后台任务用于修改会员信息
修改客户信息后台任务
在后台线程中批量修改客户信息 Excel 文件中读取客户手机号和修改信息
执行完成后会更新简道云表单并自动提交工作流
Args:
data (Dict[str, Any]): 前端请求发送过来的数据包含文件信息和其他必要参数
cookies (Dict[str, str]): 登录用户的Cookies
data: 包含表单ID(api_key)表单ID(entry_id)数据ID(data_id)的字典
cookies: 用户登录 F6 系统的 cookies 信息
df: Excel 文件读取的内容DataFrame 格式第一列为客户手机号
save_path: Excel 文件保存的地址执行完成后会删除此文件
Returns:
None
注意:
- 根据客户手机号匹配客户信息
- 执行完成后会自动删除上传的文件
- 执行结果会更新到简道云表单
"""
df = df.where(pd.notnull(df), None)
params = {
+94 -98
View File
@@ -1,27 +1,41 @@
"""
删除相关后台任务模块
包含删除历史维修记录删除客户信息删除客户车辆信息等功能
本模块包含删除相关的后台任务包括
- 删除历史维修记录
- 删除客户信息
- 删除客户车辆信息
这些任务在后台线程中执行不会阻塞主请求
执行完成后会更新简道云表单并自动提交工作流
"""
import logging
import traceback
import requests
import time
from typing import Dict, Any, List
from typing import Dict, Any, List, Optional
from datetime import datetime
from tqdm import tqdm
from app.tasks.common import update_jiandaoyun, approve_workflow
from app.tasks.common import update_jiandaoyun, approve_workflow, get_operate_org_id, get_card_list
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
删除历史维修记录后台任务
在后台线程中删除指定门店的历史维修记录
执行完成后会更新简道云表单并自动提交工作流
Args:
data: 包含表单ID(api_key)表单ID(entry_id)数据ID(data_id)的字典
cookies: 用户登录 F6 系统的 cookies 信息
org_id: 需要删除历史维修记录的门店ID
org_name: 需要删除历史维修记录的门店名称
Returns:
None
"""
url = f'https://yunxiu.f6car.cn/maintain-dump/maintainHistory/?orgid={org_id}' # 删除url
res = requests.delete(url=url, cookies=cookies)
@@ -47,60 +61,37 @@ def delete_history_background(data: Dict[str, Any], cookies: Dict[str, str], org
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
删除客户信息后台任务
在后台线程中批量删除客户信息
执行完成后会更新简道云表单并自动提交工作流
Args:
data: 包含表单ID(api_key)表单ID(entry_id)数据ID(data_id)的字典
cookies: 用户登录 F6 系统的 cookies 信息
json_data: 获取到的客户信息列表包含要删除的客户信息
Returns:
None
注意:
- 8-20点之间每3.5秒删除一条数据其余时间每1.5秒删除一条数据
- 执行结果会更新到简道云表单
"""
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("未获取到门店信息")
operate_org_id = get_operate_org_id(cookies)
if not operate_org_id:
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)
# 获取会员卡列表(提取客户ID
card_list_customers = get_card_list(cookies, operate_org_id)
for item in tqdm(json_data, desc="删除客户"):
id_customer = item['idCustomer']
@@ -135,10 +126,10 @@ def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], js
continue
now = datetime.now()
if 20 <= now.hour <= 8:
time.sleep(1)
if 8 <= now.hour <= 20:
time.sleep(3.5)
else:
time.sleep(3)
time.sleep(1.5)
logger.info(f"客户删除结果: 成功次数={success}, 失败次数={fail}")
@@ -152,13 +143,26 @@ def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], js
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
删除客户车辆信息后台任务
在后台线程中批量删除客户车辆信息
会检查车辆是否有会员卡或最近消费记录有则跳过删除
执行完成后会更新简道云表单并自动提交工作流
Args:
data: 包含表单ID(api_key)表单ID(entry_id)数据ID(data_id)的字典
url: 获取车辆列表的 API URL
cookies: 用户登录 F6 系统的 cookies 信息
header: HTTP 请求头字典应包含账号登录的请求头
all_page: 总页数字符串或整数用于分页获取车辆列表
Returns:
None
注意:
- 8-20点之间每3.5秒删除一条数据其余时间每1.5秒删除一条数据
- 有会员卡或最近消费记录的车辆会被跳过
- 执行结果会更新到简道云表单
"""
print(cookies)
success = 0
@@ -168,50 +172,42 @@ def delete_car_background(data: Dict[str, Any], url: str, cookies: Dict[str, str
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("未获取到门店信息")
operate_org_id = get_operate_org_id(cookies)
if not operate_org_id:
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)
# 获取会员卡列表(提取车辆ID
# 注意:需要获取所有车辆的ID,所以不能直接使用 get_card_list
# 需要自定义提取逻辑,返回所有车辆的ID列表
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}")
try:
# 获取第一页,确定总页数
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)
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)
total_card = int(card_res.json().get("data", {}).get("total", 0))
if total_card > 0:
total_page = total_card // 100 + (total_card % 100 > 0)
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", []):
car_id = car.get("idCar")
if car_id:
card_list_cars.append(car_id)
time.sleep(0.2)
except Exception as e:
logger.error(f"获取会员卡列表时发生错误: {e}")
itemlist = []
# 使用 range() 创建一个可迭代的对象
+107
View File
@@ -1,3 +1,14 @@
"""
应用工具模块
本模块提供应用级的工具类包括
- 日志记录器配置支持日志轮转
- 后台任务调度器
- 任务队列管理
- 请求头解码工具
这些工具在整个应用中被广泛使用
"""
import logging
import os
from logging.handlers import RotatingFileHandler
@@ -8,7 +19,27 @@ from urllib.parse import unquote
class AppTools:
"""
应用级工具集合类
提供应用级别的工具功能包括
- 初始化轮转日志记录器
- 初始化后台调度器进程退出时自动关闭
- 维护一个简单的任务队列与后台处理线程
属性:
config: 配置对象
task_queue: 任务队列
logger: 日志记录器
scheduler: 后台调度器
"""
def __init__(self, config):
"""
初始化应用工具
Args:
config: 配置对象包含日志目录等配置信息
"""
self.config = config
self.task_queue = Queue()
self.logger = self._setup_logger()
@@ -16,6 +47,19 @@ class AppTools:
self._start_task_thread()
def _setup_logger(self):
"""
配置带轮转的文件日志记录器
创建支持日志轮转的文件日志记录器避免重复添加相同文件处理器
Returns:
logging.Logger: 配置好的日志记录器
注意:
- 日志文件最大 5MB
- 保留 5 个备份文件
- 使用 UTF-8 编码
"""
log_dir = self.config.LOGS_DIRECTORY
if not os.path.exists(log_dir):
os.makedirs(log_dir)
@@ -25,6 +69,7 @@ class AppTools:
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
# 若未绑定目标日志文件的 RotatingFileHandler,则创建并绑定
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')
@@ -36,19 +81,43 @@ class AppTools:
return logger
def _setup_scheduler(self):
"""
初始化后台调度器
创建后台调度器并在进程退出时优雅关闭防止资源泄漏
Returns:
BackgroundScheduler: 后台调度器实例
注意:
调度器会在进程退出时自动关闭
"""
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):
"""
后台消费队列中的任务
任务结构为 {'handler': callable, 'data': any, 'response': Queue}
- 正常执行时将 handler(data) 的结果放入 response 队列
- 发生异常时记录错误并将失败信息放入 response 队列
- 每次任务完成后调用 task_done()
"""
while True:
task = self.task_queue.get()
if task is None:
# 外部以 None 作为结束信号
self.logger.error("任务处理线程已终止")
break
try:
@@ -62,6 +131,18 @@ class AppTools:
self.logger.info("任务处理完成")
def enqueue_task(self, handler, data):
"""
将任务入队
将任务放入任务队列并返回一个响应队列调用方可以从响应队列中获取执行结果
Args:
handler: 处理函数执行具体业务逻辑
data: 传递给处理函数的数据
Returns:
Queue: 响应队列用于获取任务执行结果
"""
response_queue = Queue()
self.task_queue.put({
'handler': handler,
@@ -72,13 +153,39 @@ class AppTools:
@staticmethod
def decode_headers(headers):
"""
解码请求头
对请求头字典进行 URL 解码UTF-8返回解码后的副本
主要用于处理包含中文字符的请求头
Args:
headers: 请求头字典
Returns:
dict: 解码后的请求头字典
"""
return {key: unquote(value, encoding='utf-8') for key, value in headers.items()}
# 全局日志记录器变量
# 用于存储全局日志记录器实例,避免重复初始化
logger = None
def setup_global_logger(config):
"""
设置全局日志记录器
懒加载并返回全局 logger若未初始化则构建 AppTools 并复用其中的 logger
避免重复初始化日志处理器
Args:
config: 配置对象
Returns:
logging.Logger: 全局日志记录器实例
"""
global logger
if logger is None:
app_tools = AppTools(config)
+180 -75
View File
@@ -1,96 +1,201 @@
from fastapi import FastAPI, Request
"""
简道云 FastAPI 服务 - 主应用入口
本文件是 FastAPI 应用的主入口文件负责
1. 应用初始化和生命周期管理
2. 模块注册和路由配置
3. 异常处理器配置
4. 中间件配置
作者: 项目团队
版本: 1.0.0
"""
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.responses import JSONResponse
import json
import anyio
from fastapi.exceptions import RequestValidationError
import logging
from app.utils.app_tools import AppTools, setup_global_logger
from app.module.F6_Plugin_module import F6PluginModule
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
from app.schemas import ErrorResponse
from app.core import core_manager
from app.api.routes import router
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(title="简道云FastAPI服务")
@app.on_event("startup")
def on_startup():
"""应用启动时初始化"""
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理 - 启动和关闭"""
# 启动时初始化
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: 返回任务处理的结果
# 初始化业务模块
f6_module = F6Module()
f6_plugin_module = F6PluginModule()
other_module = OtherPluginModule()
# 将模块实例存储到 app.state(用于依赖注入)
app.state.f6_module = f6_module
app.state.f6_plugin_module = f6_plugin_module
app.state.other_module = other_module
# 注册模块到 registry
core_manager.initialize_modules({
'f6_module': f6_module,
'f6_plugin_module': f6_plugin_module,
'other_module': other_module
})
# 注册所有操作到 module_registry
core_manager.register_action('login_in', f6_module.accept_login_message, 'f6_module',
description='F6系统登录')
core_manager.register_action('get_company_information', f6_module.get_company_information, 'f6_module',
description='获取公司信息')
core_manager.register_action('get_store_information', f6_module.get_store_information, 'f6_module',
description='获取门店信息')
core_manager.register_action('keep_alive', f6_module.get_keep_heart, 'f6_module',
description='保持连接')
core_manager.register_action('check_file', f6_plugin_module.check_file, 'f6_plugin_module',
description='校验上传文件')
core_manager.register_action('create_brand', f6_plugin_module.create_brand, 'f6_plugin_module',
description='创建品牌')
core_manager.register_action('delete_history', f6_plugin_module.delete_history, 'f6_plugin_module',
description='删除历史记录')
core_manager.register_action('delete_customer', f6_plugin_module.delete_customer, 'f6_plugin_module',
description='删除客户')
core_manager.register_action('delete_cars', f6_plugin_module.delete_cars, 'f6_plugin_module',
description='删除车辆')
core_manager.register_action('sms_signature_status', other_module.sms_signature_status, 'other_module',
description='短信签名状态')
core_manager.register_action('modify_customer_info', f6_plugin_module.modify_customer_info, 'f6_plugin_module',
description='修改客户信息')
core_manager.register_action('bi_task', f6_plugin_module.bi_task, 'f6_plugin_module',
description='BI任务')
app.state.logger.info("应用启动完成,已注册所有操作")
yield
# 关闭时清理资源
if hasattr(app.state, 'app_tools') and hasattr(app.state.app_tools, 'scheduler'):
app.state.app_tools.scheduler.shutdown(wait=False)
app.state.logger.info("应用关闭")
app = FastAPI(
title="简道云FastAPI服务",
description="简道云插件后端服务,提供数据同步和处理功能",
version="1.0.0",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(router)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""
logger = app.state.logger
app_tools: AppTools = app.state.app_tools
HTTP 异常处理器
处理所有 HTTPException 异常返回统一的错误响应格式
Args:
request: FastAPI 请求对象
exc: HTTPException 异常对象
Returns:
JSONResponse: 包含错误详情的 JSON 响应
"""
logger = getattr(app.state, 'logger', None)
if logger:
logger.error(f"HTTP异常: {exc.status_code} - {exc.detail}")
return JSONResponse(
status_code=exc.status_code,
content=ErrorResponse(
detail=exc.detail or "HTTP error",
error_code=f"HTTP_{exc.status_code}"
).dict(),
)
# 获取请求数据
data = await request.json()
header = request.headers
# 解码请求头
decoded_header = app_tools.decode_headers(header)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
请求验证异常处理器
处理 Pydantic 数据验证失败的情况返回详细的验证错误信息
Args:
request: FastAPI 请求对象
exc: RequestValidationError 异常对象
Returns:
JSONResponse: 包含验证错误详情的 JSON 响应
"""
logger = getattr(app.state, 'logger', None)
if logger:
logger.warning(f"请求验证失败: {exc.errors()}")
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=ErrorResponse(
detail="请求数据验证失败",
error_code="VALIDATION_ERROR"
).dict(),
)
# 获取操作映射表
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': '未知的操作'})
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""
通用异常处理器
捕获所有未处理的异常防止应用崩溃并记录详细的错误信息
Args:
request: FastAPI 请求对象
exc: 异常对象
Returns:
JSONResponse: 包含错误详情的 JSON 响应
"""
logger = getattr(app.state, 'logger', None)
if logger:
logger.error(f"未处理的异常: {type(exc).__name__} - {str(exc)}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=ErrorResponse(
detail="服务器内部错误",
error_code="INTERNAL_ERROR"
).dict(),
)
# 将任务放入消息队列
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)
# 路由已移动到 app/api/routes.py
if __name__ == '__main__':
"""
直接运行入口
当直接运行此文件时启动 uvicorn 服务器
默认配置
- 主机: 0.0.0.0 (监听所有网络接口)
- 端口: 5003
- 热重载: 关闭 (生产环境建议关闭)
"""
import uvicorn
uvicorn.run("fastapi_app.main:app", host="0.0.0.0", port=5003, reload=False)
uvicorn.run(app, host="0.0.0.0", port=5003, reload=False)
-97
View File
@@ -1,97 +0,0 @@
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