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