Compare commits
11 Commits
073f0646a1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f0fcea03bb | |||
| 838453b88f | |||
| 98944ecbdc | |||
| 283f7849f8 | |||
| 5cde7f852a | |||
| 3938c820b5 | |||
| c4c4ccc7e9 | |||
| 11e4151395 | |||
| 70c375a34e | |||
| 1e83d5b19a | |||
| 49fc75214f |
+179
@@ -0,0 +1,179 @@
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### 默认 template
|
||||
# IntelliJ 文件
|
||||
.idea
|
||||
*.iml
|
||||
out
|
||||
gen
|
||||
|
||||
# 数据文件
|
||||
*.csv
|
||||
*.xlsx
|
||||
*.xls
|
||||
|
||||
# 环境文件
|
||||
.vscode
|
||||
.conda
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+3
-1
@@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="fastapi_app" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
|
||||
-225
@@ -1,225 +0,0 @@
|
||||
# FastAPI项目架构说明文档
|
||||
|
||||
## 项目架构概览
|
||||
|
||||
本项目采用模块化、可扩展的架构设计,主要包含以下核心组件:
|
||||
|
||||
```
|
||||
fastapi_app/
|
||||
├── app/
|
||||
│ ├── core/ # 核心模块(可选)
|
||||
│ │ ├── header_manager.py # 请求头管理器
|
||||
│ │ ├── module_registry.py # 模块注册表
|
||||
│ │ └── __init__.py # 核心模块导出
|
||||
│ ├── module/ # 业务模块
|
||||
│ │ ├── F6_Plugin_module.py
|
||||
│ │ ├── module.py
|
||||
│ │ └── other_module.py
|
||||
│ ├── tasks/ # 后台任务模块
|
||||
│ │ ├── common.py
|
||||
│ │ ├── brand_tasks.py
|
||||
│ │ ├── delete_tasks.py
|
||||
│ │ └── customer_tasks.py
|
||||
│ ├── utils/ # 工具模块
|
||||
│ │ └── app_tools.py
|
||||
│ └── api.py # API接口
|
||||
├── main.py # FastAPI应用入口
|
||||
└── requirements_fastapi.txt # 依赖清单
|
||||
```
|
||||
|
||||
## 核心组件说明
|
||||
|
||||
### 1. 请求头管理器(HeaderManager)
|
||||
|
||||
统一管理不同模块的请求头配置。
|
||||
|
||||
预定义配置:
|
||||
- `default`: 默认请求头
|
||||
- `f6_login`: F6系统登录请求头
|
||||
- `jiandaoyun_api`: 简道云API请求头
|
||||
|
||||
使用示例:
|
||||
```python
|
||||
from app.core.header_manager import HeaderManager
|
||||
|
||||
# 获取默认请求头
|
||||
headers = HeaderManager.get_headers()
|
||||
|
||||
# 获取F6登录请求头
|
||||
headers = HeaderManager.get_headers('f6_login')
|
||||
|
||||
# 自定义请求头
|
||||
headers = HeaderManager.get_headers(
|
||||
'default',
|
||||
referer='https://example.com',
|
||||
custom_headers={'X-Custom-Header': 'value'}
|
||||
)
|
||||
|
||||
# 注册新模块的请求头
|
||||
from app.core.header_manager import HeaderConfig
|
||||
custom_config = HeaderConfig(
|
||||
referer='https://new-module.com',
|
||||
user_agent='Custom Agent'
|
||||
)
|
||||
HeaderManager.register_module_headers('new_module', custom_config)
|
||||
```
|
||||
|
||||
### 2. 模块注册表(ModuleRegistry)
|
||||
|
||||
统一注册和管理所有功能模块和操作(可选使用,主要用于请求头管理)。
|
||||
|
||||
注册操作:
|
||||
```python
|
||||
from app.core import core_manager
|
||||
|
||||
core_manager.register_action(
|
||||
action_name='my_action',
|
||||
handler=my_handler_function,
|
||||
module_name='my_module',
|
||||
description='操作描述',
|
||||
requires_auth=True,
|
||||
header_module='default'
|
||||
)
|
||||
```
|
||||
|
||||
获取操作:
|
||||
```python
|
||||
action_config = core_manager.registry.get_action('my_action')
|
||||
if action_config:
|
||||
result = action_config.handler(data)
|
||||
```
|
||||
|
||||
## 添加新功能模块
|
||||
|
||||
### 步骤1:创建模块文件
|
||||
|
||||
在 `app/module/` 目录下创建新模块文件,例如 `new_module.py`:
|
||||
|
||||
```python
|
||||
class NewModule:
|
||||
def my_action(self, data):
|
||||
"""即刻执行的操作"""
|
||||
# 处理逻辑
|
||||
return {'msg': '执行成功'}
|
||||
|
||||
def my_background_action(self, data):
|
||||
"""后台执行的操作"""
|
||||
# 这个函数会启动后台线程
|
||||
from app import back_ground_tasks
|
||||
import threading
|
||||
|
||||
thread = threading.Thread(
|
||||
target=back_ground_tasks.my_background_task,
|
||||
args=(data,)
|
||||
)
|
||||
thread.start()
|
||||
return {'msg': '正在执行中'}
|
||||
```
|
||||
|
||||
### 步骤2:创建后台任务(如果需要)
|
||||
|
||||
在 `app/tasks/` 目录下创建任务文件,例如 `new_tasks.py`:
|
||||
|
||||
```python
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow
|
||||
|
||||
def my_background_task(data):
|
||||
"""后台任务函数"""
|
||||
# 执行耗时操作
|
||||
result = "执行结果"
|
||||
|
||||
# 更新简道云表单
|
||||
msg = update_jiandaoyun(data, result)
|
||||
|
||||
# 提交工作流
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
```
|
||||
|
||||
### 步骤3:在 main.py 中注册操作
|
||||
|
||||
在 `main.py` 的 `get_action_map()` 函数中添加:
|
||||
|
||||
```python
|
||||
def get_action_map() -> dict:
|
||||
# ... 现有代码 ...
|
||||
new_module = NewModule()
|
||||
return {
|
||||
# ... 现有操作 ...
|
||||
'my_action': new_module.my_action,
|
||||
'my_background_action': new_module.my_background_action,
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤4:注册新的请求头(如果需要)
|
||||
|
||||
在 `app/core/header_manager.py` 中添加:
|
||||
|
||||
```python
|
||||
# 在 HeaderManager 类中添加
|
||||
NEW_MODULE_HEADERS = HeaderConfig(
|
||||
referer='https://new-module.com',
|
||||
user_agent='Custom User Agent',
|
||||
custom_headers={'X-API-Key': 'your-api-key'}
|
||||
)
|
||||
|
||||
_module_headers: Dict[str, HeaderConfig] = {
|
||||
# ... 现有配置 ...
|
||||
'new_module': NEW_MODULE_HEADERS,
|
||||
}
|
||||
```
|
||||
|
||||
## 执行方式说明
|
||||
|
||||
### 调度机制
|
||||
|
||||
项目使用原有的任务队列调度方式:
|
||||
|
||||
1. **所有操作**都通过 `app_tools.enqueue_task()` 放入任务队列
|
||||
2. **同步等待**结果通过 `response_queue.get()` 获取
|
||||
3. **后台任务**在模块函数内部使用 `threading.Thread` 启动线程
|
||||
|
||||
### 即刻执行 vs 后台执行
|
||||
|
||||
- **即刻执行**:函数直接返回结果,通过任务队列同步执行
|
||||
- **后台执行**:函数内部启动线程执行耗时操作,立即返回"正在执行中"的提示
|
||||
|
||||
所有操作都通过统一的任务队列处理,后台任务由模块函数内部管理。
|
||||
|
||||
## API接口
|
||||
|
||||
### POST /webhook
|
||||
统一处理所有请求的主接口。
|
||||
|
||||
请求头:
|
||||
- `Action`: 操作名称
|
||||
- `Check`: (可选)F6_Plugin 操作时的检查标志
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"api_key": "...",
|
||||
"entry_id": "...",
|
||||
"data_id": "...",
|
||||
"Action": "..." // F6_Plugin 操作时需要
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **模块职责单一**:每个模块只负责一类功能
|
||||
2. **统一注册**:所有操作都在 `main.py` 的 `get_action_map()` 中统一注册
|
||||
3. **请求头管理**:使用 HeaderManager 统一管理请求头,避免硬编码(可选)
|
||||
4. **错误处理**:在模块函数中统一处理异常
|
||||
5. **日志记录**:使用统一的日志记录器 `logging.getLogger('app')`
|
||||
6. **向后兼容**:保持 `app.back_ground_tasks` 的向后兼容性
|
||||
7. **任务队列**:所有操作都通过 `app_tools.enqueue_task()` 放入任务队列统一处理
|
||||
|
||||
## 后续扩展建议
|
||||
|
||||
1. **添加中间件**:用于请求验证、日志记录等
|
||||
2. **添加配置管理**:使用配置文件管理不同环境的设置
|
||||
3. **添加任务队列**:使用 Celery 或 RQ 管理后台任务
|
||||
4. **添加API文档**:使用 FastAPI 的自动文档功能
|
||||
5. **添加单元测试**:为每个模块添加测试用例
|
||||
|
||||
-506
@@ -1,506 +0,0 @@
|
||||
# Bug 报告
|
||||
|
||||
## 严重程度说明
|
||||
- 🔴 **严重**: 可能导致程序崩溃或数据丢失
|
||||
- 🟡 **中等**: 可能导致功能异常或错误处理不当
|
||||
- 🟢 **轻微**: 代码质量问题,不影响功能但需要改进
|
||||
|
||||
---
|
||||
|
||||
## 1. api.py (根目录) - 错误日志键名错误
|
||||
|
||||
### 位置
|
||||
- 第540行:`entry_data_batch_delete` 方法
|
||||
- 第654行:`workflow_task_hand_over` 方法
|
||||
- 第701行:`get_upload_token` 方法
|
||||
- 第745行:`upload_file` 方法
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - 在错误日志中使用了错误的键名 `data['data_list']`,但这些方法中可能不存在该键。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
# 第540行 - entry_data_batch_delete
|
||||
error_task_logger.error(
|
||||
f"任务 {data['data_list'][start_index:end_index]} 连续{max_retries}次请求失败,放弃此次请求。")
|
||||
# 应该是 data['data_ids']
|
||||
|
||||
# 第654行 - workflow_task_hand_over
|
||||
error_task_logger.error(
|
||||
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
|
||||
# 该方法没有 data_list 键
|
||||
|
||||
# 第701行 - get_upload_token
|
||||
error_task_logger.error(
|
||||
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
|
||||
# 该方法没有 data_list 键
|
||||
|
||||
# 第745行 - upload_file
|
||||
error_task_logger.error(
|
||||
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
|
||||
# 该方法没有 data_list 键
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
使用正确的键名或使用 `data.get()` 方法安全访问。
|
||||
|
||||
---
|
||||
|
||||
## 2. main.py - Handler 调用问题
|
||||
|
||||
### 位置
|
||||
第76行和第80行
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - 当 `action_map.get()` 返回默认的 lambda 函数时,handler 是一个函数对象,但代码中直接将其传递给 `enqueue_task`,这应该是正确的。但如果 handler 是 lambda,需要确保它能被正确调用。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
# 第76行
|
||||
handler = action_map.get(sub_action, lambda x: {'msg': '未执行'})
|
||||
|
||||
# 第80行
|
||||
handler = action_map.get(action, lambda x: {'msg': '未知的操作'})
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
确保所有 handler 都能正确处理数据参数。如果 handler 可能返回 None,需要添加检查。
|
||||
|
||||
---
|
||||
|
||||
## 3. main.py - 队列阻塞风险
|
||||
|
||||
### 位置
|
||||
第86行
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - 使用 `response_queue.get()` 会无限期阻塞,如果任务处理线程出现问题,可能导致请求永远挂起。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
result = await anyio.to_thread.run_sync(response_queue.get)
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
添加超时机制或使用 `response_queue.get(timeout=...)`。
|
||||
|
||||
---
|
||||
|
||||
## 4. app/api.py - 数组访问未检查
|
||||
|
||||
### 位置
|
||||
第712-713行:`get_upload_token` 方法
|
||||
|
||||
### 问题描述
|
||||
🔴 **严重** - 直接访问 `res_j['token_and_url_list'][0]` 可能导致 KeyError 或 IndexError。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
res_j = res.json()
|
||||
upload_url = res_j['token_and_url_list'][0]['url']
|
||||
upload_token = res_j['token_and_url_list'][0]['token']
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
添加检查:
|
||||
```python
|
||||
token_list = res_j.get('token_and_url_list', [])
|
||||
if not token_list:
|
||||
raise ValueError("未获取到上传凭证")
|
||||
upload_url = token_list[0].get('url')
|
||||
upload_token = token_list[0].get('token')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. app/module/F6_Plugin_module.py - 数组访问未检查
|
||||
|
||||
### 位置
|
||||
第40行:`accept_file` 方法
|
||||
|
||||
### 问题描述
|
||||
🔴 **严重** - 直接访问 `data['data']['附件'][0]['url']` 可能导致 KeyError 或 IndexError。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
url = data['data']['附件'][0]['url']
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
添加检查:
|
||||
```python
|
||||
attachments = data.get('data', {}).get('附件', [])
|
||||
if not attachments:
|
||||
return None, data
|
||||
url = attachments[0].get('url')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. app/module/F6_Plugin_module.py - 键访问未检查
|
||||
|
||||
### 位置
|
||||
第96行:`check_file` 方法
|
||||
|
||||
### 问题描述
|
||||
🔴 **严重** - 直接访问 `data1['data']['Action(隐藏)']` 可能导致 KeyError。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
action = data1['data']['Action(隐藏)']
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
使用安全访问:
|
||||
```python
|
||||
action = data1.get('data', {}).get('Action(隐藏)')
|
||||
if not action:
|
||||
return {'msg': '缺少Action字段'}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. app/module/module.py - 临时文件未清理
|
||||
|
||||
### 位置
|
||||
第21-31行:`get_captcha` 方法
|
||||
|
||||
### 问题描述
|
||||
🟢 **轻微** - 创建了临时文件 `captcha.png` 和 `preprocessed_captcha.png`,但没有清理。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
with open('captcha.png', 'wb') as f:
|
||||
f.write(response.content)
|
||||
# ... 处理文件 ...
|
||||
image.save('preprocessed_captcha.png')
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
使用临时文件或处理完后删除:
|
||||
```python
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
||||
tmp.write(response.content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
try:
|
||||
# 处理文件
|
||||
...
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. app/module/module.py - group_id 可能为空
|
||||
|
||||
### 位置
|
||||
第65-73行:`login_in` 方法
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - 如果找不到匹配的 `company_name`,`group_id` 为空字符串,后续请求可能失败。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
group_id = ''
|
||||
for group in res_json.get('data', []):
|
||||
if group["groupName"] == company_name:
|
||||
group_id = group.get("groupId")
|
||||
|
||||
token = quote(res_json['token'])
|
||||
url = (f'https://yunxiu.f6car.cn/kzf6/user/loginAfterChooseGroup?'
|
||||
f'token={token}&groupId={group_id}&macAddress=')
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
添加检查:
|
||||
```python
|
||||
if not group_id:
|
||||
logger.error(f"未找到公司名称: {company_name}")
|
||||
return None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. app/module/module.py - 时间判断逻辑错误
|
||||
|
||||
### 位置
|
||||
第119行:`delete_customer_background` 方法(在 delete_tasks.py 中)
|
||||
第246行:`delete_car_background` 方法(在 delete_tasks.py 中)
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - 时间判断逻辑错误:`if 20 <= now.hour <= 8:` 永远不会为 True(20 不可能小于等于 8)。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
now = datetime.now()
|
||||
if 20 <= now.hour <= 8:
|
||||
time.sleep(1)
|
||||
else:
|
||||
time.sleep(3)
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
应该是:
|
||||
```python
|
||||
now = datetime.now()
|
||||
if 8 <= now.hour <= 20: # 8点到20点之间
|
||||
time.sleep(3.5)
|
||||
else: # 其他时间
|
||||
time.sleep(1.5)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. app/tasks/common.py - 数组访问未检查
|
||||
|
||||
### 位置
|
||||
第56行:`approve_workflow` 方法
|
||||
|
||||
### 问题描述
|
||||
🔴 **严重** - 直接访问 `json['tasks']` 和 `json['tasks'][0]` 可能导致 KeyError 或 IndexError。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
for task in json['tasks']:
|
||||
if task['status'] == 0:
|
||||
username = task['assignee']['username']
|
||||
instance_id = task['instance_id']
|
||||
task_id = task['task_id']
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
添加检查:
|
||||
```python
|
||||
tasks = json.get('tasks', [])
|
||||
if not tasks:
|
||||
logger.error("未找到待处理任务")
|
||||
return
|
||||
|
||||
for task in tasks:
|
||||
if task.get('status') == 0:
|
||||
assignee = task.get('assignee', {})
|
||||
username = assignee.get('username')
|
||||
instance_id = task.get('instance_id')
|
||||
task_id = task.get('task_id')
|
||||
if username and instance_id and task_id:
|
||||
break
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. app/tasks/delete_tasks.py - 数组访问未检查
|
||||
|
||||
### 位置
|
||||
第62行:`delete_customer_background` 方法
|
||||
第154行:`delete_car_background` 方法
|
||||
|
||||
### 问题描述
|
||||
🔴 **严重** - 直接访问 `org_res.json().get("data").get("list")[0]` 可能导致 IndexError。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
operate_org_id = org_res.json().get("data").get("list")[0].get("orgId")
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
添加检查:
|
||||
```python
|
||||
org_data = org_res.json().get("data", {}).get("list", [])
|
||||
if not org_data:
|
||||
logger.error("未获取到门店信息")
|
||||
return
|
||||
operate_org_id = org_data[0].get("orgId")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. app/tasks/customer_tasks.py - 重试逻辑错误
|
||||
|
||||
### 位置
|
||||
第52-67行:`modify_customer_info_background` 方法
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - `retry_count` 在循环外部初始化,导致每次页面请求都使用相同的重试计数,而不是每页独立重试。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
retry_count = 0
|
||||
for page in range(1, total_pages + 1):
|
||||
while retry_count < max_retries:
|
||||
# ...
|
||||
if response.status_code == 200:
|
||||
break
|
||||
else:
|
||||
retry_count += 1
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
将 `retry_count` 移到循环内部:
|
||||
```python
|
||||
for page in range(1, total_pages + 1):
|
||||
retry_count = 0
|
||||
while retry_count < max_retries:
|
||||
# ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. app/tasks/delete_tasks.py - 条件判断错误
|
||||
|
||||
### 位置
|
||||
第115-116行:`delete_customer_background` 方法
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - `if success + fail < len(json_data):` 这个条件判断逻辑有问题,应该检查是否还有未处理的项。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
if success + fail < len(json_data):
|
||||
continue
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
这个条件判断似乎是想检查是否处理完所有项,但逻辑不正确。应该移除或修正。
|
||||
|
||||
---
|
||||
|
||||
## 14. app/tasks/delete_tasks.py - 变量作用域问题
|
||||
|
||||
### 位置
|
||||
第215行:`delete_car_background` 方法
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - 在循环外部使用了 `page` 变量,但 `page` 只在循环内部定义。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
for page in range(1, all_page + 1):
|
||||
# ...
|
||||
for item in items:
|
||||
# ...
|
||||
if not car_id or not customer_id:
|
||||
logger.info(f"页码 {page} 中缺少必要的ID信息") # page 在这里可用
|
||||
# ...
|
||||
# ...
|
||||
logger.error(f"删除失败: 页码 {page}, ...") # page 在这里可用
|
||||
```
|
||||
|
||||
实际上这个不是bug,`page` 在循环内部是可用的。但如果在循环外部使用就会有问题。
|
||||
|
||||
---
|
||||
|
||||
## 15. api.py (根目录) - 文件资源泄漏
|
||||
|
||||
### 位置
|
||||
第721-747行:`upload_file` 方法
|
||||
|
||||
### 问题描述
|
||||
🔴 **严重** - 文件在循环外部打开,如果循环中发生异常或提前返回,文件可能不会被关闭,导致资源泄漏。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
f = open(file_path, 'rb')
|
||||
files = {"file": f}
|
||||
|
||||
retries = 0
|
||||
while retries <= max_retries:
|
||||
try:
|
||||
res = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10)
|
||||
# ...
|
||||
if res.status_code == 200:
|
||||
return data_get # 提前返回,文件未关闭!
|
||||
# ...
|
||||
except requests.exceptions.RequestException as e:
|
||||
# 如果异常发生,文件可能不会被关闭
|
||||
# ...
|
||||
f.close() # 只有在循环结束后才会执行
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
使用 `with` 语句确保文件总是被关闭:
|
||||
```python
|
||||
retries = 0
|
||||
result = None
|
||||
while retries <= max_retries:
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
files = {"file": f}
|
||||
res = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10)
|
||||
res.raise_for_status()
|
||||
data_get = res.json()
|
||||
if res.status_code == 200:
|
||||
return data_get
|
||||
retries += 1
|
||||
time.sleep(3)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"请求异常: {e}, 将重新请求")
|
||||
retries += 1
|
||||
time.sleep(3)
|
||||
|
||||
if retries > max_retries:
|
||||
error_task_logger.error(f"上传文件失败,已重试{max_retries}次")
|
||||
return None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. api.py (根目录) - 重试时文件指针问题
|
||||
|
||||
### 位置
|
||||
第721-747行:`upload_file` 方法
|
||||
|
||||
### 问题描述
|
||||
🟡 **中等** - 文件在循环外部打开,重试时文件指针已经移动到文件末尾,后续重试会读取空内容。
|
||||
|
||||
### 代码示例
|
||||
```python
|
||||
f = open(file_path, 'rb')
|
||||
files = {"file": f}
|
||||
|
||||
retries = 0
|
||||
while retries <= max_retries:
|
||||
res = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10)
|
||||
# 第一次请求后,文件指针已经移动到末尾
|
||||
# 重试时会读取空内容
|
||||
```
|
||||
|
||||
### 修复建议
|
||||
每次重试时重新打开文件,或使用 `with` 语句在每次循环中打开文件。
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
### 按严重程度分类
|
||||
|
||||
**严重 (🔴)** - 需要立即修复:
|
||||
1. app/api.py - 数组访问未检查(get_upload_token)
|
||||
2. app/module/F6_Plugin_module.py - 数组访问未检查(accept_file)
|
||||
3. app/module/F6_Plugin_module.py - 键访问未检查(check_file)
|
||||
4. app/tasks/common.py - 数组访问未检查(approve_workflow)
|
||||
5. app/tasks/delete_tasks.py - 数组访问未检查(两处)
|
||||
6. api.py (根目录) - 文件资源泄漏(upload_file)
|
||||
|
||||
**中等 (🟡)** - 建议尽快修复:
|
||||
1. api.py - 错误日志键名错误(多处)
|
||||
2. api.py - 重试时文件指针问题(upload_file)
|
||||
3. main.py - Handler 调用和队列阻塞问题
|
||||
4. app/module/module.py - group_id 可能为空
|
||||
5. app/module/module.py - 时间判断逻辑错误(两处)
|
||||
6. app/tasks/customer_tasks.py - 重试逻辑错误
|
||||
7. app/tasks/delete_tasks.py - 条件判断错误
|
||||
|
||||
**轻微 (🟢)** - 代码质量改进:
|
||||
1. app/module/module.py - 临时文件未清理
|
||||
|
||||
### 建议的修复优先级
|
||||
|
||||
1. **立即修复**:所有严重级别的bug,特别是可能导致程序崩溃的数组访问问题
|
||||
2. **尽快修复**:中等级别的bug,特别是逻辑错误和错误处理问题
|
||||
3. **代码改进**:轻微级别的bug,在时间允许时进行改进
|
||||
|
||||
@@ -1,710 +0,0 @@
|
||||
# FastAPI 学习文档
|
||||
|
||||
## 📚 目录
|
||||
|
||||
1. [FastAPI 简介](#fastapi-简介)
|
||||
2. [Flask vs FastAPI 对比](#flask-vs-fastapi-对比)
|
||||
3. [FastAPI 核心概念](#fastapi-核心概念)
|
||||
4. [项目中的 FastAPI 使用](#项目中的-fastapi-使用)
|
||||
5. [学习路径建议](#学习路径建议)
|
||||
6. [常见问题解答](#常见问题解答)
|
||||
|
||||
---
|
||||
|
||||
## FastAPI 简介
|
||||
|
||||
### 什么是 FastAPI?
|
||||
|
||||
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,基于 Python 3.6+ 的类型提示(type hints)。
|
||||
|
||||
### 主要特点
|
||||
|
||||
1. **高性能**:与 NodeJS 和 Go 相当,是最快的 Python 框架之一
|
||||
2. **快速开发**:开发速度提升约 200% 到 300%
|
||||
3. **自动文档**:自动生成交互式 API 文档(Swagger UI)
|
||||
4. **类型提示**:基于 Python 类型提示,提供更好的 IDE 支持
|
||||
5. **异步支持**:原生支持 async/await
|
||||
6. **数据验证**:自动进行请求和响应数据验证
|
||||
|
||||
---
|
||||
|
||||
## Flask vs FastAPI 对比
|
||||
|
||||
### 1. 基本语法对比
|
||||
|
||||
#### Flask 写法
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
data = request.get_json()
|
||||
return jsonify({'msg': 'success'})
|
||||
```
|
||||
|
||||
#### FastAPI 写法
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class RequestData(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(data: RequestData):
|
||||
return JSONResponse({'msg': 'success'})
|
||||
```
|
||||
|
||||
### 2. 主要区别
|
||||
|
||||
| 特性 | Flask | FastAPI |
|
||||
|------|-------|---------|
|
||||
| **异步支持** | 需要额外库(Flask-AsyncIO) | 原生支持 async/await |
|
||||
| **类型验证** | 手动验证 | 自动验证(基于 Pydantic) |
|
||||
| **API 文档** | 需要手动编写 | 自动生成(Swagger/OpenAPI) |
|
||||
| **性能** | 中等 | 高性能(接近 NodeJS) |
|
||||
| **依赖注入** | 需要额外库 | 内置支持 |
|
||||
| **数据验证** | 手动处理 | 自动验证和转换 |
|
||||
|
||||
### 3. 请求处理对比
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
from flask import request
|
||||
|
||||
@app.route('/api', methods=['POST'])
|
||||
def api():
|
||||
# 手动获取 JSON 数据
|
||||
data = request.get_json()
|
||||
# 手动验证
|
||||
if not data or 'name' not in data:
|
||||
return jsonify({'error': '缺少 name 字段'}), 400
|
||||
return jsonify({'result': data['name']})
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
@app.post("/api")
|
||||
async def api(item: Item):
|
||||
# 自动验证,如果 name 缺失会自动返回 422 错误
|
||||
return {'result': item.name}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FastAPI 核心概念
|
||||
|
||||
### 1. 应用实例(FastAPI App)
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
# 创建 FastAPI 应用实例
|
||||
app = FastAPI(
|
||||
title="简道云FastAPI服务", # API 文档标题
|
||||
description="简道云 API 服务", # API 描述
|
||||
version="1.0.0" # 版本号
|
||||
)
|
||||
```
|
||||
|
||||
**项目中的使用**(`main.py`):
|
||||
```python
|
||||
app = FastAPI(title="简道云FastAPI服务")
|
||||
```
|
||||
|
||||
### 2. 路由装饰器(Route Decorators)
|
||||
|
||||
FastAPI 使用装饰器定义路由,类似于 Flask:
|
||||
|
||||
```python
|
||||
@app.get("/") # GET 请求
|
||||
@app.post("/") # POST 请求
|
||||
@app.put("/") # PUT 请求
|
||||
@app.delete("/") # DELETE 请求
|
||||
```
|
||||
|
||||
**项目中的使用**(`main.py`):
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
# 处理逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. 请求对象(Request)
|
||||
|
||||
FastAPI 的 `Request` 对象提供了访问请求信息的方法:
|
||||
|
||||
```python
|
||||
from fastapi import Request
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
# 获取 JSON 数据
|
||||
data = await request.json()
|
||||
|
||||
# 获取请求头
|
||||
headers = request.headers
|
||||
|
||||
# 获取查询参数
|
||||
query_params = request.query_params
|
||||
|
||||
# 获取路径参数
|
||||
path_params = request.path_params
|
||||
```
|
||||
|
||||
**项目中的使用**(`main.py`):
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
# 获取请求数据
|
||||
data = await request.json()
|
||||
header = request.headers
|
||||
|
||||
# 解码请求头
|
||||
decoded_header = app_tools.decode_headers(header)
|
||||
```
|
||||
|
||||
### 4. 响应对象(Response)
|
||||
|
||||
FastAPI 提供了多种响应类型:
|
||||
|
||||
```python
|
||||
from fastapi.responses import JSONResponse, HTMLResponse, PlainTextResponse
|
||||
|
||||
@app.post("/api")
|
||||
async def api():
|
||||
# JSON 响应
|
||||
return JSONResponse({'msg': 'success'})
|
||||
|
||||
# 或者直接返回字典(FastAPI 会自动转换为 JSON)
|
||||
return {'msg': 'success'}
|
||||
```
|
||||
|
||||
**项目中的使用**(`main.py`):
|
||||
```python
|
||||
return JSONResponse(result)
|
||||
```
|
||||
|
||||
### 5. 异步函数(Async Functions)
|
||||
|
||||
FastAPI 支持异步函数,使用 `async` 和 `await`:
|
||||
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
# 异步获取 JSON 数据
|
||||
data = await request.json()
|
||||
|
||||
# 异步调用其他函数
|
||||
result = await some_async_function(data)
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
**项目中的使用**(`main.py`):
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
data = await request.json() # 异步获取数据
|
||||
# ...
|
||||
result = await anyio.to_thread.run_sync(response_queue.get) # 异步执行同步函数
|
||||
```
|
||||
|
||||
### 6. 生命周期事件(Lifecycle Events)
|
||||
|
||||
FastAPI 提供了应用启动和关闭事件:
|
||||
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""应用启动时执行"""
|
||||
print("应用启动")
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""应用关闭时执行"""
|
||||
print("应用关闭")
|
||||
```
|
||||
|
||||
**项目中的使用**(`main.py`):
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
"""应用启动时初始化"""
|
||||
app.state.app_tools = AppTools(Config)
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
app.state.f6_module = F6Module()
|
||||
app.state.f6_plugin_module = F6PluginModule()
|
||||
app.state.other_module = OtherPluginModule()
|
||||
```
|
||||
|
||||
### 7. 应用状态(Application State)
|
||||
|
||||
FastAPI 允许在应用实例上存储状态:
|
||||
|
||||
```python
|
||||
# 设置状态
|
||||
app.state.my_data = "some value"
|
||||
|
||||
# 获取状态
|
||||
my_data = app.state.my_data
|
||||
```
|
||||
|
||||
**项目中的使用**(`main.py`):
|
||||
```python
|
||||
# 在启动时设置
|
||||
app.state.app_tools = AppTools(Config)
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
|
||||
# 在路由中使用
|
||||
logger = app.state.logger
|
||||
app_tools: AppTools = app.state.app_tools
|
||||
```
|
||||
|
||||
### 8. 数据验证(Pydantic Models)
|
||||
|
||||
FastAPI 使用 Pydantic 进行数据验证:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: str = None # 可选字段
|
||||
|
||||
@app.post("/users")
|
||||
async def create_user(user: User):
|
||||
# user 已经自动验证和转换
|
||||
return {"name": user.name, "age": user.age}
|
||||
```
|
||||
|
||||
**项目中的潜在使用**(可以改进):
|
||||
```python
|
||||
# 可以定义请求模型
|
||||
class WebhookRequest(BaseModel):
|
||||
api_key: str
|
||||
entry_id: str
|
||||
data_id: str
|
||||
Action: str = None # 可选字段
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request, data: WebhookRequest):
|
||||
# data 已经自动验证
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目中的 FastAPI 使用
|
||||
|
||||
### 1. 项目结构分析
|
||||
|
||||
```
|
||||
fastapi_app/
|
||||
├── main.py # FastAPI 应用入口
|
||||
├── api.py # API 工具类(简道云 API 封装)
|
||||
├── app/
|
||||
│ ├── api.py # API 模块(简道云 API 封装)
|
||||
│ ├── config.py # 配置管理
|
||||
│ ├── module/ # 业务模块
|
||||
│ ├── tasks/ # 后台任务
|
||||
│ └── utils/ # 工具函数
|
||||
└── requirements.txt # 依赖列表
|
||||
```
|
||||
|
||||
### 2. main.py 详解
|
||||
|
||||
让我们逐步分析 `main.py` 文件:
|
||||
|
||||
#### 2.1 导入和初始化
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
import json
|
||||
import anyio
|
||||
from app.utils.app_tools import AppTools, setup_global_logger
|
||||
from app.module.F6_Plugin_module import F6PluginModule
|
||||
from app.module.module import F6Module
|
||||
from app.module.other_module import OtherPluginModule
|
||||
from app.config import Config
|
||||
|
||||
# 创建 FastAPI 应用实例
|
||||
app = FastAPI(title="简道云FastAPI服务")
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- `FastAPI`:主应用类
|
||||
- `Request`:请求对象,用于访问请求信息
|
||||
- `JSONResponse`:JSON 响应类
|
||||
|
||||
#### 2.2 启动事件
|
||||
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
"""应用启动时初始化"""
|
||||
app.state.app_tools = AppTools(Config)
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
app.state.f6_module = F6Module()
|
||||
app.state.f6_plugin_module = F6PluginModule()
|
||||
app.state.other_module = OtherPluginModule()
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- `@app.on_event("startup")`:应用启动时执行
|
||||
- `app.state`:存储应用级别的状态
|
||||
- 初始化工具类、日志记录器和业务模块
|
||||
|
||||
#### 2.3 路由处理
|
||||
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
"""
|
||||
接受前端请求后将任务放入消息队列
|
||||
|
||||
Returns:
|
||||
any: 返回任务处理的结果
|
||||
"""
|
||||
logger = app.state.logger
|
||||
app_tools: AppTools = app.state.app_tools
|
||||
|
||||
# 获取请求数据
|
||||
data = await request.json()
|
||||
header = request.headers
|
||||
|
||||
# 解码请求头
|
||||
decoded_header = app_tools.decode_headers(header)
|
||||
|
||||
# 获取操作映射表
|
||||
action_map = get_action_map()
|
||||
action = decoded_header.get('Action')
|
||||
|
||||
# 处理逻辑...
|
||||
|
||||
# 将任务放入消息队列
|
||||
response_queue = app_tools.enqueue_task(handler, data)
|
||||
|
||||
# 等待任务处理结果
|
||||
result = await anyio.to_thread.run_sync(response_queue.get)
|
||||
|
||||
logger.info(json.dumps(result, ensure_ascii=False, indent=4))
|
||||
|
||||
return JSONResponse(result)
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
1. `async def`:异步函数定义
|
||||
2. `await request.json()`:异步获取 JSON 数据
|
||||
3. `await anyio.to_thread.run_sync()`:在线程池中执行同步函数
|
||||
4. `JSONResponse`:返回 JSON 响应
|
||||
|
||||
### 3. 任务队列机制
|
||||
|
||||
项目使用自定义的任务队列来处理请求:
|
||||
|
||||
```python
|
||||
# 在 app_tools.py 中
|
||||
class AppTools:
|
||||
def __init__(self, config):
|
||||
self.task_queue = Queue()
|
||||
self._start_task_thread()
|
||||
|
||||
def enqueue_task(self, handler, data):
|
||||
response_queue = Queue()
|
||||
self.task_queue.put({
|
||||
'handler': handler,
|
||||
'data': data,
|
||||
'response': response_queue
|
||||
})
|
||||
return response_queue
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. 请求到达 `/webhook` 端点
|
||||
2. 将任务放入队列
|
||||
3. 后台线程处理任务
|
||||
4. 等待结果返回
|
||||
|
||||
### 4. 操作映射机制
|
||||
|
||||
项目使用操作映射表来路由不同的操作:
|
||||
|
||||
```python
|
||||
def get_action_map() -> dict:
|
||||
"""获取操作映射表"""
|
||||
f6_module = app.state.f6_module
|
||||
f6_plugin_module = app.state.f6_plugin_module
|
||||
other_module = app.state.other_module
|
||||
return {
|
||||
'login_in': f6_module.accept_login_message,
|
||||
'get_company_information': f6_module.get_company_information,
|
||||
'create_brand': f6_plugin_module.create_brand,
|
||||
# ... 更多操作
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 通过请求头中的 `Action` 字段确定要执行的操作
|
||||
- 使用字典映射操作名到处理函数
|
||||
|
||||
---
|
||||
|
||||
## 学习路径建议
|
||||
|
||||
### 阶段 1:基础理解(1-2 天)
|
||||
|
||||
1. **理解 FastAPI 基本概念**
|
||||
- 阅读官方文档:https://fastapi.tiangolo.com/
|
||||
- 理解路由、请求、响应的基本用法
|
||||
|
||||
2. **运行项目**
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 运行项目
|
||||
python main.py
|
||||
# 或
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
3. **访问 API 文档**
|
||||
- 启动后访问:http://localhost:5003/docs
|
||||
- 查看自动生成的 Swagger UI 文档
|
||||
|
||||
### 阶段 2:代码分析(2-3 天)
|
||||
|
||||
1. **分析 main.py**
|
||||
- 理解应用初始化流程
|
||||
- 理解路由处理逻辑
|
||||
- 理解任务队列机制
|
||||
|
||||
2. **分析业务模块**
|
||||
- 查看 `app/module/` 目录下的模块
|
||||
- 理解操作映射机制
|
||||
- 理解模块间的交互
|
||||
|
||||
3. **分析工具类**
|
||||
- 查看 `app/utils/app_tools.py`
|
||||
- 理解任务队列实现
|
||||
- 理解日志记录机制
|
||||
|
||||
### 阶段 3:实践练习(3-5 天)
|
||||
|
||||
1. **添加新路由**
|
||||
```python
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok"}
|
||||
```
|
||||
|
||||
2. **添加数据验证**
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class WebhookData(BaseModel):
|
||||
api_key: str
|
||||
entry_id: str
|
||||
data_id: str
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(data: WebhookData):
|
||||
return {"received": data.dict()}
|
||||
```
|
||||
|
||||
3. **添加错误处理**
|
||||
```python
|
||||
from fastapi import HTTPException
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
# 处理逻辑
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
```
|
||||
|
||||
### 阶段 4:深入学习(1-2 周)
|
||||
|
||||
1. **学习异步编程**
|
||||
- 理解 `async/await`
|
||||
- 理解 `asyncio`
|
||||
- 理解并发处理
|
||||
|
||||
2. **学习 Pydantic**
|
||||
- 数据验证
|
||||
- 数据序列化
|
||||
- 自定义验证器
|
||||
|
||||
3. **学习依赖注入**
|
||||
- FastAPI 的依赖注入系统
|
||||
- 数据库连接管理
|
||||
- 认证和授权
|
||||
|
||||
---
|
||||
|
||||
## 常见问题解答
|
||||
|
||||
### Q1: FastAPI 和 Flask 的主要区别是什么?
|
||||
|
||||
**A:** 主要区别:
|
||||
1. **性能**:FastAPI 性能更高,支持异步
|
||||
2. **类型验证**:FastAPI 自动验证,Flask 需要手动
|
||||
3. **API 文档**:FastAPI 自动生成,Flask 需要手动编写
|
||||
4. **异步支持**:FastAPI 原生支持,Flask 需要额外库
|
||||
|
||||
### Q2: 为什么使用 `async def` 而不是 `def`?
|
||||
|
||||
**A:**
|
||||
- `async def` 允许使用 `await` 关键字
|
||||
- 可以异步处理 I/O 操作(如网络请求、数据库查询)
|
||||
- 提高并发性能
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
# 同步(阻塞)
|
||||
def sync_function():
|
||||
data = requests.get("https://api.example.com") # 阻塞
|
||||
return data
|
||||
|
||||
# 异步(非阻塞)
|
||||
async def async_function():
|
||||
async with httpx.AsyncClient() as client:
|
||||
data = await client.get("https://api.example.com") # 非阻塞
|
||||
return data
|
||||
```
|
||||
|
||||
### Q3: `app.state` 是什么?为什么要用它?
|
||||
|
||||
**A:**
|
||||
- `app.state` 是 FastAPI 提供的应用级状态存储
|
||||
- 用于存储需要在多个路由之间共享的数据
|
||||
- 类似于 Flask 的 `g` 对象,但作用域是整个应用
|
||||
|
||||
**项目中的使用**:
|
||||
```python
|
||||
# 启动时设置
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
|
||||
# 在路由中使用
|
||||
logger = app.state.logger
|
||||
```
|
||||
|
||||
### Q4: 如何处理同步函数?
|
||||
|
||||
**A:**
|
||||
使用 `anyio.to_thread.run_sync()` 在线程池中执行同步函数:
|
||||
|
||||
```python
|
||||
import anyio
|
||||
|
||||
# 同步函数
|
||||
def sync_function(data):
|
||||
# 耗时操作
|
||||
return result
|
||||
|
||||
# 在异步函数中调用
|
||||
@app.post("/api")
|
||||
async def api(request: Request):
|
||||
result = await anyio.to_thread.run_sync(sync_function, data)
|
||||
return result
|
||||
```
|
||||
|
||||
### Q5: 如何添加中间件?
|
||||
|
||||
**A:**
|
||||
使用 `@app.middleware()` 装饰器:
|
||||
|
||||
```python
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
```
|
||||
|
||||
### Q6: 如何添加 CORS 支持?
|
||||
|
||||
**A:**
|
||||
使用 `CORSMiddleware`:
|
||||
|
||||
```python
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # 允许所有源
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"], # 允许所有方法
|
||||
allow_headers=["*"], # 允许所有头
|
||||
)
|
||||
```
|
||||
|
||||
### Q7: 如何添加认证?
|
||||
|
||||
**A:**
|
||||
使用依赖注入:
|
||||
|
||||
```python
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import HTTPBearer
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
async def verify_token(token: str = Depends(security)):
|
||||
if token != "valid_token":
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
return token
|
||||
|
||||
@app.post("/api")
|
||||
async def api(token: str = Depends(verify_token)):
|
||||
return {"message": "Authenticated"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 推荐资源
|
||||
|
||||
1. **官方文档**:https://fastapi.tiangolo.com/
|
||||
2. **中文文档**:https://fastapi.tiangolo.com/zh/
|
||||
3. **Pydantic 文档**:https://docs.pydantic.dev/
|
||||
4. **Uvicorn 文档**:https://www.uvicorn.org/
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
FastAPI 是一个现代、高性能的 Web 框架,特别适合构建 API。相比 Flask,它提供了:
|
||||
|
||||
1. **更好的性能**:异步支持,高性能
|
||||
2. **自动验证**:基于类型提示的自动数据验证
|
||||
3. **自动文档**:自动生成 API 文档
|
||||
4. **更好的开发体验**:类型提示、IDE 支持
|
||||
|
||||
通过这个项目,你可以学习到:
|
||||
- FastAPI 的基本用法
|
||||
- 异步编程
|
||||
- 任务队列机制
|
||||
- 模块化架构设计
|
||||
|
||||
建议按照学习路径逐步深入,多实践、多思考,逐步掌握 FastAPI 的精髓。
|
||||
|
||||
@@ -1,496 +0,0 @@
|
||||
# FastAPI 快速参考指南
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 最小示例
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
async def read_root():
|
||||
return {"Hello": "World"}
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int):
|
||||
return {"item_id": item_id}
|
||||
```
|
||||
|
||||
### 运行应用
|
||||
|
||||
```bash
|
||||
# 方式 1:使用 uvicorn
|
||||
uvicorn main:app --reload
|
||||
|
||||
# 方式 2:在代码中运行
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 路由定义
|
||||
|
||||
### HTTP 方法
|
||||
|
||||
```python
|
||||
@app.get("/items") # GET 请求
|
||||
@app.post("/items") # POST 请求
|
||||
@app.put("/items/{id}") # PUT 请求
|
||||
@app.delete("/items/{id}") # DELETE 请求
|
||||
@app.patch("/items/{id}") # PATCH 请求
|
||||
```
|
||||
|
||||
### 路径参数
|
||||
|
||||
```python
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int):
|
||||
return {"item_id": item_id}
|
||||
|
||||
# 多个路径参数
|
||||
@app.get("/users/{user_id}/items/{item_id}")
|
||||
async def read_user_item(user_id: int, item_id: int):
|
||||
return {"user_id": user_id, "item_id": item_id}
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
```python
|
||||
@app.get("/items")
|
||||
async def read_items(skip: int = 0, limit: int = 10):
|
||||
return {"skip": skip, "limit": limit}
|
||||
|
||||
# 可选参数
|
||||
@app.get("/items")
|
||||
async def read_items(q: str = None):
|
||||
return {"q": q}
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
description: str = None # 可选字段
|
||||
|
||||
@app.post("/items")
|
||||
async def create_item(item: Item):
|
||||
return item
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 请求和响应
|
||||
|
||||
### 获取请求数据
|
||||
|
||||
```python
|
||||
from fastapi import Request
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
# JSON 数据
|
||||
data = await request.json()
|
||||
|
||||
# 请求头
|
||||
headers = request.headers
|
||||
|
||||
# 查询参数
|
||||
query_params = request.query_params
|
||||
|
||||
# 表单数据
|
||||
form_data = await request.form()
|
||||
|
||||
# 文件上传
|
||||
files = await request.form()
|
||||
```
|
||||
|
||||
### 响应类型
|
||||
|
||||
```python
|
||||
from fastapi.responses import JSONResponse, HTMLResponse, PlainTextResponse
|
||||
|
||||
@app.get("/json")
|
||||
async def json_response():
|
||||
return JSONResponse({"message": "Hello"})
|
||||
|
||||
@app.get("/html")
|
||||
async def html_response():
|
||||
return HTMLResponse("<h1>Hello</h1>")
|
||||
|
||||
@app.get("/text")
|
||||
async def text_response():
|
||||
return PlainTextResponse("Hello")
|
||||
```
|
||||
|
||||
### 状态码
|
||||
|
||||
```python
|
||||
from fastapi import status
|
||||
|
||||
@app.post("/items", status_code=status.HTTP_201_CREATED)
|
||||
async def create_item(item: Item):
|
||||
return item
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 数据验证(Pydantic)
|
||||
|
||||
### 基本模型
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int = Field(gt=0, le=120) # 年龄必须大于 0,小于等于 120
|
||||
email: str = Field(..., regex="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
|
||||
```
|
||||
|
||||
### 嵌套模型
|
||||
|
||||
```python
|
||||
class Address(BaseModel):
|
||||
street: str
|
||||
city: str
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
address: Address
|
||||
```
|
||||
|
||||
### 列表和字典
|
||||
|
||||
```python
|
||||
class Item(BaseModel):
|
||||
tags: list[str] = []
|
||||
metadata: dict[str, str] = {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 依赖注入
|
||||
|
||||
### 基本依赖
|
||||
|
||||
```python
|
||||
from fastapi import Depends
|
||||
|
||||
def get_db():
|
||||
db = "database"
|
||||
yield db
|
||||
# 清理代码
|
||||
|
||||
@app.get("/items")
|
||||
async def read_items(db: str = Depends(get_db)):
|
||||
return {"db": db}
|
||||
```
|
||||
|
||||
### 类依赖
|
||||
|
||||
```python
|
||||
class Database:
|
||||
def get_data(self):
|
||||
return "data"
|
||||
|
||||
def get_database():
|
||||
return Database()
|
||||
|
||||
@app.get("/items")
|
||||
async def read_items(db: Database = Depends(get_database)):
|
||||
return db.get_data()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 错误处理
|
||||
|
||||
### HTTP 异常
|
||||
|
||||
```python
|
||||
from fastapi import HTTPException
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int):
|
||||
if item_id not in items:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return {"item_id": item_id}
|
||||
```
|
||||
|
||||
### 自定义异常处理器
|
||||
|
||||
```python
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
@app.exception_handler(ValueError)
|
||||
async def value_error_handler(request: Request, exc: ValueError):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"message": str(exc)}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 异步编程
|
||||
|
||||
### 异步函数
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
@app.get("/async")
|
||||
async def async_endpoint():
|
||||
await asyncio.sleep(1) # 模拟异步操作
|
||||
return {"message": "Done"}
|
||||
```
|
||||
|
||||
### 在线程池中运行同步函数
|
||||
|
||||
```python
|
||||
import anyio
|
||||
|
||||
def sync_function(data):
|
||||
# 同步耗时操作
|
||||
return result
|
||||
|
||||
@app.post("/sync-in-async")
|
||||
async def sync_in_async(data: dict):
|
||||
result = await anyio.to_thread.run_sync(sync_function, data)
|
||||
return result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 应用状态和生命周期
|
||||
|
||||
### 应用状态
|
||||
|
||||
```python
|
||||
# 设置状态
|
||||
app.state.my_data = "value"
|
||||
|
||||
# 获取状态
|
||||
my_data = app.state.my_data
|
||||
```
|
||||
|
||||
### 启动和关闭事件
|
||||
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
# 启动时执行
|
||||
print("应用启动")
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
# 关闭时执行
|
||||
print("应用关闭")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 中间件
|
||||
|
||||
### 添加中间件
|
||||
|
||||
```python
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
```
|
||||
|
||||
### CORS 中间件
|
||||
|
||||
```python
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 项目中的实际应用
|
||||
|
||||
### 1. 应用初始化(main.py)
|
||||
|
||||
```python
|
||||
app = FastAPI(title="简道云FastAPI服务")
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
app.state.app_tools = AppTools(Config)
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
app.state.f6_module = F6Module()
|
||||
```
|
||||
|
||||
### 2. 路由处理
|
||||
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
# 获取状态
|
||||
logger = app.state.logger
|
||||
app_tools = app.state.app_tools
|
||||
|
||||
# 获取请求数据
|
||||
data = await request.json()
|
||||
header = request.headers
|
||||
|
||||
# 处理逻辑
|
||||
# ...
|
||||
|
||||
# 返回响应
|
||||
return JSONResponse(result)
|
||||
```
|
||||
|
||||
### 3. 任务队列集成
|
||||
|
||||
```python
|
||||
# 将任务放入队列
|
||||
response_queue = app_tools.enqueue_task(handler, data)
|
||||
|
||||
# 等待结果(在线程池中执行)
|
||||
result = await anyio.to_thread.run_sync(response_queue.get)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 调试技巧
|
||||
|
||||
### 1. 查看自动生成的文档
|
||||
|
||||
启动应用后访问:
|
||||
- Swagger UI: http://localhost:8000/docs
|
||||
- ReDoc: http://localhost:8000/redoc
|
||||
|
||||
### 2. 打印请求信息
|
||||
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
print(f"Method: {request.method}")
|
||||
print(f"URL: {request.url}")
|
||||
print(f"Headers: {dict(request.headers)}")
|
||||
data = await request.json()
|
||||
print(f"Body: {data}")
|
||||
```
|
||||
|
||||
### 3. 使用日志
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
logger.info("收到请求")
|
||||
logger.debug(f"请求数据: {data}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 常用模式
|
||||
|
||||
### 1. 操作映射模式(项目中使用)
|
||||
|
||||
```python
|
||||
def get_action_map() -> dict:
|
||||
return {
|
||||
'action1': handler1,
|
||||
'action2': handler2,
|
||||
}
|
||||
|
||||
@app.post("/api")
|
||||
async def api(request: Request):
|
||||
action_map = get_action_map()
|
||||
action = request.headers.get('Action')
|
||||
handler = action_map.get(action)
|
||||
if handler:
|
||||
result = handler(data)
|
||||
return JSONResponse(result)
|
||||
```
|
||||
|
||||
### 2. 统一错误处理
|
||||
|
||||
```python
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
logger.error(f"未处理的异常: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"message": "内部服务器错误"}
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 请求验证
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
class WebhookData(BaseModel):
|
||||
api_key: str
|
||||
entry_id: str
|
||||
data_id: str
|
||||
|
||||
@validator('api_key')
|
||||
def validate_api_key(cls, v):
|
||||
if not v:
|
||||
raise ValueError('api_key 不能为空')
|
||||
return v
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(data: WebhookData):
|
||||
# 数据已自动验证
|
||||
return {"received": data.dict()}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 学习检查清单
|
||||
|
||||
- [ ] 理解 FastAPI 基本概念
|
||||
- [ ] 能够创建简单的路由
|
||||
- [ ] 理解异步编程(async/await)
|
||||
- [ ] 掌握 Pydantic 数据验证
|
||||
- [ ] 理解依赖注入
|
||||
- [ ] 能够处理错误
|
||||
- [ ] 理解应用状态和生命周期
|
||||
- [ ] 能够添加中间件
|
||||
- [ ] 理解项目中的任务队列机制
|
||||
- [ ] 能够添加新的路由和功能
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关资源
|
||||
|
||||
- **FastAPI 官方文档**: https://fastapi.tiangolo.com/
|
||||
- **FastAPI 中文文档**: https://fastapi.tiangolo.com/zh/
|
||||
- **Pydantic 文档**: https://docs.pydantic.dev/
|
||||
- **Uvicorn 文档**: https://www.uvicorn.org/
|
||||
- **Python 异步编程**: https://docs.python.org/3/library/asyncio.html
|
||||
|
||||
---
|
||||
|
||||
**提示**: 这个快速参考指南可以作为日常开发的速查手册。建议结合 `FASTAPI_LEARNING.md` 深入学习。
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
# FastAPI 学习资源集合
|
||||
|
||||
欢迎!这个目录包含了学习 FastAPI 的完整资源,特别适合从 Flask 迁移到 FastAPI 的开发者。
|
||||
|
||||
## 📚 文档列表
|
||||
|
||||
### 1. [FastAPI 学习文档](./FASTAPI_LEARNING.md) 📖
|
||||
**推荐首先阅读**
|
||||
|
||||
全面的 FastAPI 学习指南,包括:
|
||||
- FastAPI 简介和特点
|
||||
- Flask vs FastAPI 详细对比
|
||||
- FastAPI 核心概念详解
|
||||
- 项目中的实际应用分析
|
||||
- 学习路径建议
|
||||
- 常见问题解答
|
||||
|
||||
**适合**:想要系统学习 FastAPI 的初学者
|
||||
|
||||
### 2. [FastAPI 快速参考指南](./FASTAPI_QUICK_REFERENCE.md) ⚡
|
||||
**日常开发速查手册**
|
||||
|
||||
快速查找常用功能的参考指南:
|
||||
- 路由定义
|
||||
- 请求和响应处理
|
||||
- 数据验证
|
||||
- 依赖注入
|
||||
- 错误处理
|
||||
- 异步编程
|
||||
- 项目中的实际应用模式
|
||||
|
||||
**适合**:日常开发时快速查找语法和用法
|
||||
|
||||
### 3. [Flask 到 FastAPI 迁移指南](./FLASK_TO_FASTAPI_MIGRATION.md) 🔄
|
||||
**迁移项目必读**
|
||||
|
||||
详细的迁移指南,包括:
|
||||
- 迁移概览和步骤
|
||||
- 核心概念对比表
|
||||
- 代码迁移示例
|
||||
- 当前项目的迁移分析
|
||||
- 常见迁移问题解答
|
||||
- 迁移检查清单
|
||||
|
||||
**适合**:正在从 Flask 迁移到 FastAPI 的开发者
|
||||
|
||||
---
|
||||
|
||||
## 🎯 学习路径
|
||||
|
||||
### 阶段 1:基础入门(1-2 天)
|
||||
|
||||
1. **阅读 [FastAPI 学习文档](./FASTAPI_LEARNING.md)**
|
||||
- 理解 FastAPI 的基本概念
|
||||
- 了解 Flask vs FastAPI 的区别
|
||||
- 理解项目中的 FastAPI 使用
|
||||
|
||||
2. **运行项目**
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 运行项目
|
||||
python main.py
|
||||
# 或
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
3. **访问 API 文档**
|
||||
- 启动后访问:http://localhost:5003/docs
|
||||
- 查看自动生成的 Swagger UI 文档
|
||||
- 尝试调用 API 端点
|
||||
|
||||
### 阶段 2:深入理解(2-3 天)
|
||||
|
||||
1. **分析项目代码**
|
||||
- 阅读 `main.py`,理解应用初始化
|
||||
- 阅读 `app/module/` 下的业务模块
|
||||
- 理解任务队列机制
|
||||
|
||||
2. **参考 [Flask 到 FastAPI 迁移指南](./FLASK_TO_FASTAPI_MIGRATION.md)**
|
||||
- 理解迁移过程
|
||||
- 对比 Flask 和 FastAPI 的写法
|
||||
- 理解项目中的迁移实现
|
||||
|
||||
3. **实践练习**
|
||||
- 添加新的 API 端点
|
||||
- 添加数据验证
|
||||
- 添加错误处理
|
||||
|
||||
### 阶段 3:熟练应用(3-5 天)
|
||||
|
||||
1. **使用 [快速参考指南](./FASTAPI_QUICK_REFERENCE.md)**
|
||||
- 在日常开发中参考
|
||||
- 尝试不同的 FastAPI 特性
|
||||
- 优化现有代码
|
||||
|
||||
2. **深入学习**
|
||||
- 学习异步编程
|
||||
- 学习 Pydantic 高级特性
|
||||
- 学习依赖注入系统
|
||||
|
||||
3. **项目改进**
|
||||
- 重构代码使用 FastAPI 最佳实践
|
||||
- 添加更多类型提示
|
||||
- 优化性能
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install fastapi uvicorn
|
||||
```
|
||||
|
||||
### 2. 最小示例
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
async def read_root():
|
||||
return {"Hello": "World"}
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int):
|
||||
return {"item_id": item_id}
|
||||
```
|
||||
|
||||
### 3. 运行应用
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
### 4. 访问文档
|
||||
|
||||
- Swagger UI: http://localhost:8000/docs
|
||||
- ReDoc: http://localhost:8000/redoc
|
||||
|
||||
---
|
||||
|
||||
## 📖 项目结构
|
||||
|
||||
```
|
||||
fastapi_app/
|
||||
├── main.py # FastAPI 应用入口
|
||||
├── api.py # API 工具类
|
||||
├── app/
|
||||
│ ├── api.py # 简道云 API 封装
|
||||
│ ├── config.py # 配置管理
|
||||
│ ├── module/ # 业务模块
|
||||
│ │ ├── F6_Plugin_module.py
|
||||
│ │ ├── module.py
|
||||
│ │ └── other_module.py
|
||||
│ ├── tasks/ # 后台任务
|
||||
│ └── utils/ # 工具函数
|
||||
│ └── app_tools.py
|
||||
├── requirements.txt # 依赖列表
|
||||
│
|
||||
├── FASTAPI_LEARNING.md # 📖 学习文档
|
||||
├── FASTAPI_QUICK_REFERENCE.md # ⚡ 快速参考
|
||||
└── FLASK_TO_FASTAPI_MIGRATION.md # 🔄 迁移指南
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 关键概念
|
||||
|
||||
### 1. 应用实例
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI(title="简道云FastAPI服务")
|
||||
```
|
||||
|
||||
### 2. 路由定义
|
||||
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
data = await request.json()
|
||||
return JSONResponse(result)
|
||||
```
|
||||
|
||||
### 3. 应用状态
|
||||
|
||||
```python
|
||||
# 启动时设置
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
|
||||
# 路由中使用
|
||||
logger = app.state.logger
|
||||
```
|
||||
|
||||
### 4. 生命周期事件
|
||||
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
# 初始化代码
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 项目中的实际应用
|
||||
|
||||
### 1. 应用初始化
|
||||
|
||||
```python
|
||||
# main.py
|
||||
app = FastAPI(title="简道云FastAPI服务")
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
app.state.app_tools = AppTools(Config)
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
app.state.f6_module = F6Module()
|
||||
```
|
||||
|
||||
### 2. 路由处理
|
||||
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
logger = app.state.logger
|
||||
app_tools = app.state.app_tools
|
||||
|
||||
data = await request.json()
|
||||
header = request.headers
|
||||
|
||||
# 处理逻辑
|
||||
result = await anyio.to_thread.run_sync(response_queue.get)
|
||||
|
||||
return JSONResponse(result)
|
||||
```
|
||||
|
||||
### 3. 任务队列
|
||||
|
||||
```python
|
||||
# 将任务放入队列
|
||||
response_queue = app_tools.enqueue_task(handler, data)
|
||||
|
||||
# 在线程池中执行同步函数
|
||||
result = await anyio.to_thread.run_sync(response_queue.get)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 推荐资源
|
||||
|
||||
### 官方文档
|
||||
- **FastAPI 官方文档**: https://fastapi.tiangolo.com/
|
||||
- **FastAPI 中文文档**: https://fastapi.tiangolo.com/zh/
|
||||
- **Pydantic 文档**: https://docs.pydantic.dev/
|
||||
- **Uvicorn 文档**: https://www.uvicorn.org/
|
||||
|
||||
### 学习资源
|
||||
- **FastAPI 教程**: https://fastapi.tiangolo.com/tutorial/
|
||||
- **Python 异步编程**: https://docs.python.org/3/library/asyncio.html
|
||||
- **类型提示**: https://docs.python.org/3/library/typing.html
|
||||
|
||||
---
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 我应该先读哪个文档?
|
||||
|
||||
**A:** 建议按以下顺序:
|
||||
1. 先读 [FastAPI 学习文档](./FASTAPI_LEARNING.md) 了解基础
|
||||
2. 再读 [Flask 到 FastAPI 迁移指南](./FLASK_TO_FASTAPI_MIGRATION.md) 理解迁移
|
||||
3. 日常开发时参考 [快速参考指南](./FASTAPI_QUICK_REFERENCE.md)
|
||||
|
||||
### Q: 如何理解项目中的异步代码?
|
||||
|
||||
**A:**
|
||||
- FastAPI 使用 `async/await` 处理异步操作
|
||||
- `await request.json()` 异步获取 JSON 数据
|
||||
- `await anyio.to_thread.run_sync()` 在线程池中执行同步函数
|
||||
- 详细说明见 [FastAPI 学习文档](./FASTAPI_LEARNING.md#异步编程)
|
||||
|
||||
### Q: 项目中的任务队列是如何工作的?
|
||||
|
||||
**A:**
|
||||
- 使用 Python 的 `Queue` 和 `threading` 实现
|
||||
- 请求到达后,任务放入队列
|
||||
- 后台线程处理任务
|
||||
- 使用 `anyio.to_thread.run_sync()` 等待结果
|
||||
- 详细说明见 [FastAPI 学习文档](./FASTAPI_LEARNING.md#项目中的-fastapi-使用)
|
||||
|
||||
### Q: 如何添加新的 API 端点?
|
||||
|
||||
**A:**
|
||||
1. 在 `main.py` 中添加路由函数
|
||||
2. 在 `get_action_map()` 中注册操作(如果需要)
|
||||
3. 参考 [快速参考指南](./FASTAPI_QUICK_REFERENCE.md#路由定义)
|
||||
|
||||
---
|
||||
|
||||
## 🎓 学习检查清单
|
||||
|
||||
### 基础理解
|
||||
- [ ] 理解 FastAPI 的基本概念
|
||||
- [ ] 理解 Flask vs FastAPI 的区别
|
||||
- [ ] 能够创建简单的路由
|
||||
- [ ] 理解异步编程(async/await)
|
||||
|
||||
### 项目理解
|
||||
- [ ] 理解项目结构
|
||||
- [ ] 理解应用初始化流程
|
||||
- [ ] 理解路由处理逻辑
|
||||
- [ ] 理解任务队列机制
|
||||
|
||||
### 实践能力
|
||||
- [ ] 能够添加新的路由
|
||||
- [ ] 能够使用 Pydantic 进行数据验证
|
||||
- [ ] 能够处理错误
|
||||
- [ ] 能够使用应用状态
|
||||
|
||||
### 深入学习
|
||||
- [ ] 理解依赖注入
|
||||
- [ ] 理解中间件
|
||||
- [ ] 理解生命周期事件
|
||||
- [ ] 能够优化代码性能
|
||||
|
||||
---
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
- **2024-01-XX**: 创建 FastAPI 学习文档集合
|
||||
- 添加 FastAPI 学习文档
|
||||
- 添加快速参考指南
|
||||
- 添加 Flask 到 FastAPI 迁移指南
|
||||
|
||||
---
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
如果你发现文档中有错误或需要改进的地方,欢迎提出建议!
|
||||
|
||||
---
|
||||
|
||||
**祝你学习愉快!** 🎉
|
||||
|
||||
如有问题,请参考相应的文档或查看 FastAPI 官方文档。
|
||||
|
||||
@@ -1,620 +0,0 @@
|
||||
# Flask 到 FastAPI 迁移指南
|
||||
|
||||
## 📋 目录
|
||||
|
||||
1. [迁移概览](#迁移概览)
|
||||
2. [核心概念对比](#核心概念对比)
|
||||
3. [代码迁移示例](#代码迁移示例)
|
||||
4. [项目迁移分析](#项目迁移分析)
|
||||
5. [常见迁移问题](#常见迁移问题)
|
||||
|
||||
---
|
||||
|
||||
## 迁移概览
|
||||
|
||||
### 为什么迁移到 FastAPI?
|
||||
|
||||
1. **性能提升**:FastAPI 性能接近 NodeJS 和 Go
|
||||
2. **自动文档**:自动生成 API 文档
|
||||
3. **类型安全**:基于 Python 类型提示
|
||||
4. **异步支持**:原生支持 async/await
|
||||
5. **现代特性**:符合现代 Python 开发标准
|
||||
|
||||
### 迁移步骤
|
||||
|
||||
1. ✅ **安装 FastAPI**:`pip install fastapi uvicorn`
|
||||
2. ✅ **替换应用实例**:`Flask()` → `FastAPI()`
|
||||
3. ✅ **更新路由装饰器**:基本语法相同
|
||||
4. ✅ **处理异步**:添加 `async/await`
|
||||
5. ✅ **数据验证**:使用 Pydantic 模型
|
||||
6. ✅ **更新响应**:使用 FastAPI 响应类
|
||||
|
||||
---
|
||||
|
||||
## 核心概念对比
|
||||
|
||||
### 1. 应用实例
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
```
|
||||
|
||||
### 2. 路由定义
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
@app.route('/items', methods=['GET'])
|
||||
def get_items():
|
||||
return jsonify({'items': []})
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
@app.get('/items')
|
||||
async def get_items():
|
||||
return {'items': []}
|
||||
```
|
||||
|
||||
**区别**:
|
||||
- FastAPI 使用 `@app.get()` 而不是 `@app.route(methods=['GET'])`
|
||||
- FastAPI 函数通常是 `async`
|
||||
- FastAPI 直接返回字典,自动转换为 JSON
|
||||
|
||||
### 3. 获取请求数据
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
from flask import request
|
||||
|
||||
@app.route('/items', methods=['POST'])
|
||||
def create_item():
|
||||
data = request.get_json()
|
||||
name = data.get('name')
|
||||
return jsonify({'name': name})
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
from fastapi import Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
|
||||
@app.post('/items')
|
||||
async def create_item(item: Item):
|
||||
return {'name': item.name}
|
||||
```
|
||||
|
||||
**区别**:
|
||||
- FastAPI 使用 Pydantic 模型自动验证
|
||||
- 不需要手动获取 JSON 数据
|
||||
- 类型自动验证和转换
|
||||
|
||||
### 4. 路径参数
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
@app.route('/items/<int:item_id>')
|
||||
def get_item(item_id):
|
||||
return jsonify({'item_id': item_id})
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
@app.get('/items/{item_id}')
|
||||
async def get_item(item_id: int):
|
||||
return {'item_id': item_id}
|
||||
```
|
||||
|
||||
**区别**:
|
||||
- FastAPI 使用 `{item_id}` 而不是 `<int:item_id>`
|
||||
- 类型在函数参数中指定
|
||||
|
||||
### 5. 查询参数
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
from flask import request
|
||||
|
||||
@app.route('/items')
|
||||
def get_items():
|
||||
skip = request.args.get('skip', 0, type=int)
|
||||
limit = request.args.get('limit', 10, type=int)
|
||||
return jsonify({'skip': skip, 'limit': limit})
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
@app.get('/items')
|
||||
async def get_items(skip: int = 0, limit: int = 10):
|
||||
return {'skip': skip, 'limit': limit}
|
||||
```
|
||||
|
||||
**区别**:
|
||||
- FastAPI 查询参数直接在函数参数中定义
|
||||
- 默认值自动处理
|
||||
- 类型自动验证
|
||||
|
||||
### 6. 请求头
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
from flask import request
|
||||
|
||||
@app.route('/items')
|
||||
def get_items():
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
return jsonify({'user_agent': user_agent})
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
from fastapi import Request, Header
|
||||
|
||||
@app.get('/items')
|
||||
async def get_items(request: Request):
|
||||
user_agent = request.headers.get('user-agent')
|
||||
return {'user_agent': user_agent}
|
||||
|
||||
# 或者使用 Header
|
||||
@app.get('/items')
|
||||
async def get_items(user_agent: str = Header(None)):
|
||||
return {'user_agent': user_agent}
|
||||
```
|
||||
|
||||
### 7. 响应
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
from flask import jsonify, Response
|
||||
|
||||
@app.route('/items')
|
||||
def get_items():
|
||||
return jsonify({'items': []})
|
||||
# 或
|
||||
return Response('text', mimetype='text/plain')
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse
|
||||
|
||||
@app.get('/items')
|
||||
async def get_items():
|
||||
return {'items': []} # 自动转换为 JSON
|
||||
# 或
|
||||
return JSONResponse({'items': []})
|
||||
# 或
|
||||
return PlainTextResponse('text')
|
||||
```
|
||||
|
||||
### 8. 错误处理
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
from flask import abort
|
||||
|
||||
@app.route('/items/<int:item_id>')
|
||||
def get_item(item_id):
|
||||
if item_id not in items:
|
||||
abort(404, description="Item not found")
|
||||
return jsonify({'item_id': item_id})
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
from fastapi import HTTPException
|
||||
|
||||
@app.get('/items/{item_id}')
|
||||
async def get_item(item_id: int):
|
||||
if item_id not in items:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return {'item_id': item_id}
|
||||
```
|
||||
|
||||
### 9. 应用状态
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
from flask import g
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.db = get_database()
|
||||
|
||||
@app.route('/items')
|
||||
def get_items():
|
||||
db = g.db
|
||||
return jsonify({'items': []})
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
def startup():
|
||||
app.state.db = get_database()
|
||||
|
||||
@app.get('/items')
|
||||
async def get_items():
|
||||
db = app.state.db
|
||||
return {'items': []}
|
||||
```
|
||||
|
||||
### 10. 中间件
|
||||
|
||||
#### Flask
|
||||
```python
|
||||
@app.before_request
|
||||
def before_request():
|
||||
# 请求前处理
|
||||
pass
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
# 请求后处理
|
||||
response.headers['X-Custom'] = 'value'
|
||||
return response
|
||||
```
|
||||
|
||||
#### FastAPI
|
||||
```python
|
||||
@app.middleware("http")
|
||||
async def add_custom_header(request: Request, call_next):
|
||||
# 请求前处理
|
||||
response = await call_next(request)
|
||||
# 请求后处理
|
||||
response.headers['X-Custom'] = 'value'
|
||||
return response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码迁移示例
|
||||
|
||||
### 示例 1:简单的 API 端点
|
||||
|
||||
#### Flask 版本
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/api/users', methods=['POST'])
|
||||
def create_user():
|
||||
data = request.get_json()
|
||||
if not data or 'name' not in data:
|
||||
return jsonify({'error': '缺少 name 字段'}), 400
|
||||
|
||||
name = data['name']
|
||||
age = data.get('age', 0)
|
||||
|
||||
return jsonify({'name': name, 'age': age}), 201
|
||||
```
|
||||
|
||||
#### FastAPI 版本
|
||||
```python
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int = 0
|
||||
|
||||
@app.post('/api/users', status_code=201)
|
||||
async def create_user(user: User):
|
||||
return {'name': user.name, 'age': user.age}
|
||||
```
|
||||
|
||||
**改进点**:
|
||||
- ✅ 自动数据验证
|
||||
- ✅ 类型安全
|
||||
- ✅ 更简洁的代码
|
||||
- ✅ 自动生成 API 文档
|
||||
|
||||
### 示例 2:带路径参数的端点
|
||||
|
||||
#### Flask 版本
|
||||
```python
|
||||
@app.route('/api/users/<int:user_id>', methods=['GET'])
|
||||
def get_user(user_id):
|
||||
if user_id not in users:
|
||||
return jsonify({'error': '用户不存在'}), 404
|
||||
return jsonify(users[user_id])
|
||||
```
|
||||
|
||||
#### FastAPI 版本
|
||||
```python
|
||||
@app.get('/api/users/{user_id}')
|
||||
async def get_user(user_id: int):
|
||||
if user_id not in users:
|
||||
raise HTTPException(status_code=404, detail='用户不存在')
|
||||
return users[user_id]
|
||||
```
|
||||
|
||||
### 示例 3:文件上传
|
||||
|
||||
#### Flask 版本
|
||||
```python
|
||||
from flask import request
|
||||
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload_file():
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': '没有文件'}), 400
|
||||
|
||||
file = request.files['file']
|
||||
# 处理文件
|
||||
return jsonify({'filename': file.filename})
|
||||
```
|
||||
|
||||
#### FastAPI 版本
|
||||
```python
|
||||
from fastapi import UploadFile, File
|
||||
|
||||
@app.post('/upload')
|
||||
async def upload_file(file: UploadFile = File(...)):
|
||||
# 处理文件
|
||||
return {'filename': file.filename}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目迁移分析
|
||||
|
||||
### 当前项目的迁移情况
|
||||
|
||||
#### 1. 应用初始化(main.py)
|
||||
|
||||
**Flask 版本(推测)**:
|
||||
```python
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.before_first_request
|
||||
def initialize():
|
||||
app.config['app_tools'] = AppTools(Config)
|
||||
# ...
|
||||
```
|
||||
|
||||
**FastAPI 版本(当前)**:
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI(title="简道云FastAPI服务")
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
app.state.app_tools = AppTools(Config)
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
# ...
|
||||
```
|
||||
|
||||
**迁移要点**:
|
||||
- ✅ `Flask()` → `FastAPI()`
|
||||
- ✅ `@app.before_first_request` → `@app.on_event("startup")`
|
||||
- ✅ `app.config` → `app.state`
|
||||
|
||||
#### 2. 路由处理(main.py)
|
||||
|
||||
**Flask 版本(推测)**:
|
||||
```python
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
data = request.get_json()
|
||||
header = request.headers
|
||||
# 处理逻辑
|
||||
return jsonify(result)
|
||||
```
|
||||
|
||||
**FastAPI 版本(当前)**:
|
||||
```python
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
data = await request.json()
|
||||
header = request.headers
|
||||
# 处理逻辑
|
||||
return JSONResponse(result)
|
||||
```
|
||||
|
||||
**迁移要点**:
|
||||
- ✅ `@app.route(methods=['POST'])` → `@app.post()`
|
||||
- ✅ `def` → `async def`
|
||||
- ✅ `request.get_json()` → `await request.json()`
|
||||
- ✅ `jsonify()` → `JSONResponse()` 或直接返回字典
|
||||
|
||||
#### 3. 任务队列处理
|
||||
|
||||
**项目特点**:
|
||||
- 使用自定义任务队列(`Queue` + `threading`)
|
||||
- 在异步函数中调用同步函数
|
||||
|
||||
**FastAPI 处理方式**:
|
||||
```python
|
||||
# 在线程池中执行同步函数
|
||||
result = await anyio.to_thread.run_sync(response_queue.get)
|
||||
```
|
||||
|
||||
**迁移要点**:
|
||||
- ✅ 使用 `anyio.to_thread.run_sync()` 在线程池中执行同步代码
|
||||
- ✅ 保持原有的任务队列机制
|
||||
|
||||
---
|
||||
|
||||
## 常见迁移问题
|
||||
|
||||
### Q1: 如何处理 Flask 的 `g` 对象?
|
||||
|
||||
**A:** 使用 `app.state` 或依赖注入:
|
||||
|
||||
```python
|
||||
# Flask
|
||||
from flask import g
|
||||
g.db = get_db()
|
||||
|
||||
# FastAPI 方式 1:使用 app.state
|
||||
app.state.db = get_db()
|
||||
db = app.state.db
|
||||
|
||||
# FastAPI 方式 2:使用依赖注入(推荐)
|
||||
def get_db():
|
||||
db = "database"
|
||||
yield db
|
||||
|
||||
@app.get("/items")
|
||||
async def get_items(db: str = Depends(get_db)):
|
||||
return {"db": db}
|
||||
```
|
||||
|
||||
### Q2: 如何处理 Flask 的 `session`?
|
||||
|
||||
**A:** FastAPI 不内置 session,需要手动实现或使用第三方库:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, Request
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(SessionMiddleware, secret_key="secret")
|
||||
|
||||
@app.post("/login")
|
||||
async def login(request: Request):
|
||||
request.session['user'] = 'username'
|
||||
return {"message": "Logged in"}
|
||||
```
|
||||
|
||||
### Q3: 如何处理 Flask 的 `url_for`?
|
||||
|
||||
**A:** 使用 `Request` 对象构建 URL:
|
||||
|
||||
```python
|
||||
from fastapi import Request
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def get_item(item_id: int, request: Request):
|
||||
url = str(request.url_for('get_item', item_id=item_id))
|
||||
return {"url": url}
|
||||
```
|
||||
|
||||
### Q4: 如何迁移 Flask 的蓝图(Blueprint)?
|
||||
|
||||
**A:** 使用 FastAPI 的 `APIRouter`:
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/items")
|
||||
async def get_items():
|
||||
return {"items": []}
|
||||
|
||||
# 在主应用中注册
|
||||
app.include_router(router, prefix="/api")
|
||||
```
|
||||
|
||||
### Q5: 如何处理 Flask 的 `current_app`?
|
||||
|
||||
**A:** 使用依赖注入或直接访问 `app`:
|
||||
|
||||
```python
|
||||
# Flask
|
||||
from flask import current_app
|
||||
config = current_app.config
|
||||
|
||||
# FastAPI
|
||||
from fastapi import Request
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(request: Request):
|
||||
app = request.app
|
||||
# 访问应用配置
|
||||
return {"config": "value"}
|
||||
```
|
||||
|
||||
### Q6: 如何迁移 Flask 的 `before_request` 和 `after_request`?
|
||||
|
||||
**A:** 使用中间件:
|
||||
|
||||
```python
|
||||
@app.middleware("http")
|
||||
async def middleware(request: Request, call_next):
|
||||
# before_request 逻辑
|
||||
print("请求前")
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
# after_request 逻辑
|
||||
print("请求后")
|
||||
response.headers["X-Custom"] = "value"
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 迁移检查清单
|
||||
|
||||
### 基础迁移
|
||||
- [ ] 替换 `Flask()` 为 `FastAPI()`
|
||||
- [ ] 更新路由装饰器(`@app.route()` → `@app.get()` 等)
|
||||
- [ ] 添加 `async` 关键字
|
||||
- [ ] 更新请求数据获取方式
|
||||
- [ ] 更新响应返回方式
|
||||
|
||||
### 高级迁移
|
||||
- [ ] 使用 Pydantic 模型进行数据验证
|
||||
- [ ] 迁移应用状态(`app.config` → `app.state`)
|
||||
- [ ] 更新生命周期事件(`before_first_request` → `on_event("startup")`)
|
||||
- [ ] 迁移中间件
|
||||
- [ ] 更新错误处理
|
||||
|
||||
### 测试和优化
|
||||
- [ ] 测试所有 API 端点
|
||||
- [ ] 验证数据验证功能
|
||||
- [ ] 检查性能提升
|
||||
- [ ] 更新文档
|
||||
- [ ] 更新依赖项
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
### 迁移优势
|
||||
|
||||
1. **性能提升**:FastAPI 性能显著优于 Flask
|
||||
2. **开发效率**:自动文档、自动验证减少开发时间
|
||||
3. **类型安全**:类型提示提供更好的 IDE 支持
|
||||
4. **现代特性**:异步支持、依赖注入等现代特性
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **异步编程**:需要理解 `async/await`
|
||||
2. **Pydantic**:需要学习 Pydantic 模型定义
|
||||
3. **依赖注入**:FastAPI 的依赖注入系统需要适应
|
||||
4. **生态系统**:Flask 的某些扩展可能需要替代方案
|
||||
|
||||
### 建议
|
||||
|
||||
1. **逐步迁移**:不要一次性迁移所有代码
|
||||
2. **保持兼容**:在迁移过程中保持 API 兼容性
|
||||
3. **充分测试**:迁移后充分测试所有功能
|
||||
4. **学习资源**:参考 FastAPI 官方文档和示例
|
||||
|
||||
---
|
||||
|
||||
**提示**:这个迁移指南帮助你理解从 Flask 到 FastAPI 的迁移过程。结合 `FASTAPI_LEARNING.md` 和 `FASTAPI_QUICK_REFERENCE.md` 一起学习效果更好。
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
# 简道云 FastAPI 服务
|
||||
|
||||
简道云插件后端服务,提供数据同步和处理功能。
|
||||
|
||||
## 📋 项目简介
|
||||
|
||||
本项目是一个基于 FastAPI 框架的简道云插件后端服务,主要用于处理简道云插件发送的请求,执行各种业务操作。系统与 F6 汽车维修管理系统集成,提供以下核心功能:
|
||||
|
||||
- **F6系统集成**: 登录认证、公司信息获取、门店信息管理
|
||||
- **文件处理**: 文件上传、校验、Excel批量处理
|
||||
- **数据管理**: 品牌批量创建、客户信息管理、车辆信息管理
|
||||
- **数据清理**: 历史记录删除、客户删除、车辆删除
|
||||
- **BI任务**: 数据处理和报表生成
|
||||
- **工作流自动化**: 自动提交简道云工作流
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
fastapi_app/
|
||||
├── main.py # 应用入口,生命周期管理
|
||||
├── requirements.txt # Python依赖清单
|
||||
├── README.md # 项目文档(本文件)
|
||||
│
|
||||
├── app/ # 应用主目录
|
||||
│ ├── __init__.py
|
||||
│ ├── config.py # 配置管理(目录、API Token等)
|
||||
│ ├── schemas.py # Pydantic数据模型定义
|
||||
│ │
|
||||
│ ├── api/ # API路由模块
|
||||
│ │ ├── __init__.py # 简道云API封装类
|
||||
│ │ ├── routes.py # FastAPI路由定义
|
||||
│ │ └── dependencies.py # 依赖注入函数
|
||||
│ │
|
||||
│ ├── core/ # 核心功能模块
|
||||
│ │ ├── __init__.py # 核心管理器
|
||||
│ │ └── module_registry.py # 模块注册表
|
||||
│ │
|
||||
│ ├── module/ # 业务模块
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── module.py # F6Module - F6系统相关功能
|
||||
│ │ ├── f6_plugin_module.py # F6PluginModule - F6插件功能
|
||||
│ │ └── other_module.py # OtherPluginModule - 其他功能
|
||||
│ │
|
||||
│ ├── tasks/ # 后台任务模块
|
||||
│ │ ├── __init__.py # 任务统一导出
|
||||
│ │ ├── common.py # 通用任务函数
|
||||
│ │ ├── brand_tasks.py # 品牌相关任务
|
||||
│ │ ├── customer_tasks.py # 客户相关任务
|
||||
│ │ ├── delete_tasks.py # 删除相关任务
|
||||
│ │ └── bi_tasks.py # BI相关任务
|
||||
│ │
|
||||
│ └── utils/ # 工具模块
|
||||
│ └── app_tools.py # 应用工具类(日志、任务队列等)
|
||||
│
|
||||
├── logs/ # 日志目录
|
||||
├── 下载文件/ # 下载文件存储目录
|
||||
└── 模板文件/ # 模板文件存储目录
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Python 3.8+
|
||||
- pip
|
||||
- Tesseract OCR(用于验证码识别,可选)
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 配置说明
|
||||
|
||||
编辑 `app/config.py` 文件,配置以下内容:
|
||||
|
||||
- `JIANDAOYUN_API_TOKEN`: 简道云API Token(生产环境建议使用环境变量)
|
||||
|
||||
### 运行应用
|
||||
|
||||
```bash
|
||||
# 方式1:直接运行
|
||||
python main.py
|
||||
|
||||
# 方式2:使用 uvicorn(推荐)
|
||||
uvicorn main:app --host 0.0.0.0 --port 5003 --reload
|
||||
```
|
||||
|
||||
### 访问API文档
|
||||
|
||||
启动应用后,访问以下地址查看API文档:
|
||||
|
||||
- **Swagger UI**: http://localhost:5003/docs
|
||||
- **ReDoc**: http://localhost:5003/redoc
|
||||
|
||||
## 📡 API 接口
|
||||
|
||||
### 健康检查
|
||||
|
||||
```http
|
||||
GET /health
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook 接口
|
||||
|
||||
```http
|
||||
POST /webhook
|
||||
```
|
||||
|
||||
**请求头:**
|
||||
- `Action`: 操作类型(必需)
|
||||
- `Check`: 检查标志(F6_Plugin 操作时需要,值为"是"或"否")
|
||||
|
||||
**请求体:**
|
||||
```json
|
||||
{
|
||||
"api_key": "简道云应用ID",
|
||||
"entry_id": "简道云表单ID",
|
||||
"data_id": "简道云数据ID",
|
||||
"Action": "操作类型(F6_Plugin 操作时需要)",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"company_name": "公司名称"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"msg": "操作完成",
|
||||
"msg_details": "详细信息",
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 支持的操作
|
||||
|
||||
### F6Module 操作
|
||||
|
||||
| 操作名 | 描述 | 说明 |
|
||||
|--------|------|------|
|
||||
| `login_in` | F6系统登录 | 使用用户名、密码登录F6系统,支持验证码识别 |
|
||||
| `get_company_information` | 获取公司信息 | 获取F6系统中的公司列表,保存到简道云 |
|
||||
| `get_store_information` | 获取门店信息 | 获取指定公司的门店列表及统计数据 |
|
||||
| `keep_alive` | 保持连接 | 心跳检测,保持连接活跃 |
|
||||
|
||||
### F6PluginModule 操作
|
||||
|
||||
| 操作名 | 描述 | 说明 |
|
||||
|--------|------|------|
|
||||
| `check_file` | 校验上传文件 | 校验Excel文件格式,支持品牌创建、客户修改等场景 |
|
||||
| `create_brand` | 创建品牌 | 批量创建品牌,从Excel文件读取品牌名称 |
|
||||
| `delete_history` | 删除历史记录 | 删除指定门店的历史维修记录 |
|
||||
| `delete_customer` | 删除客户 | 批量删除客户信息(会检查会员卡和消费记录) |
|
||||
| `delete_cars` | 删除车辆 | 批量删除客户车辆信息(会检查会员卡和消费记录) |
|
||||
| `modify_customer_info` | 修改客户信息 | 批量修改客户信息,从Excel文件读取修改内容 |
|
||||
| `bi_task` | BI任务 | 执行BI相关任务(数据处理、报表生成等) |
|
||||
|
||||
### OtherPluginModule 操作
|
||||
|
||||
| 操作名 | 描述 | 说明 |
|
||||
|--------|------|------|
|
||||
| `sms_signature_status` | 短信签名状态 | 查询短信签名状态(待实现) |
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **FastAPI**: 现代、快速的 Web 框架,支持异步处理
|
||||
- **Pydantic**: 数据验证和设置管理
|
||||
- **Uvicorn**: ASGI 服务器
|
||||
- **APScheduler**: 后台任务调度
|
||||
- **Requests**: HTTP 请求库
|
||||
- **Pandas**: Excel文件处理和数据分析
|
||||
- **Pillow**: 图像处理
|
||||
- **Pytesseract**: OCR 识别(用于验证码识别)
|
||||
|
||||
## 📦 依赖列表
|
||||
|
||||
```
|
||||
anyio==4.11.0
|
||||
apscheduler==3.11.1
|
||||
fastapi==0.121.0
|
||||
log_config==2.1.1
|
||||
numpy==2.3.4
|
||||
pandas==2.3.3
|
||||
Pillow==12.0.0
|
||||
pytesseract==0.3.13
|
||||
Requests==2.32.5
|
||||
tqdm==4.67.1
|
||||
uvicorn==0.38.0
|
||||
```
|
||||
|
||||
## 🏛️ 架构设计
|
||||
|
||||
### 核心特性
|
||||
|
||||
1. **模块化设计**: 业务逻辑按模块分离,易于维护和扩展
|
||||
2. **依赖注入**: 使用 FastAPI 的依赖注入系统,减少耦合
|
||||
3. **模块注册表**: 使用 `module_registry` 统一管理所有操作
|
||||
4. **路由分离**: API 路由分离到独立文件,职责清晰
|
||||
5. **数据验证**: 使用 Pydantic schemas 进行请求和响应验证
|
||||
6. **异常处理**: 完善的异常处理机制,统一的错误响应格式
|
||||
7. **日志记录**: 规范的日志记录,支持日志轮转(单文件5MB,保留5个备份)
|
||||
|
||||
### 生命周期管理
|
||||
|
||||
应用使用 FastAPI 的 `lifespan` 功能管理应用生命周期:
|
||||
|
||||
- **启动时**:
|
||||
- 初始化工具类(AppTools)
|
||||
- 设置全局日志记录器
|
||||
- 初始化业务模块(F6Module、F6PluginModule、OtherPluginModule)
|
||||
- 注册所有操作到模块注册表
|
||||
- **运行时**: 处理请求,执行业务逻辑
|
||||
- **关闭时**: 清理资源,关闭后台调度器
|
||||
|
||||
### 任务处理机制
|
||||
|
||||
系统支持两种任务处理方式:
|
||||
|
||||
1. **同步任务**:
|
||||
- 通过任务队列同步执行,等待结果返回
|
||||
- 适用于快速操作(如登录、信息获取)
|
||||
- 超时时间:55秒(简道云默认60秒超时)
|
||||
|
||||
2. **后台任务**:
|
||||
- 在模块函数内部启动线程执行耗时操作
|
||||
- 立即返回"正在执行中"的提示
|
||||
- 适用于批量操作(如品牌创建、数据删除)
|
||||
- 执行完成后自动更新简道云表单并提交工作流
|
||||
|
||||
### 简道云API封装
|
||||
|
||||
`app/api/__init__.py` 中的 `API` 类封装了简道云的所有API接口:
|
||||
|
||||
- **表单数据操作**: 获取、创建、更新、删除(单条/批量)
|
||||
- **表单字段获取**: 获取表单字段信息,支持字段ID到标签名的替换
|
||||
- **工作流操作**: 获取实例、审批、转交
|
||||
- **文件操作**: 获取上传凭证、上传文件
|
||||
- **重试机制**: 所有API调用都支持失败重试(默认最多20次)
|
||||
|
||||
## 🔐 配置说明
|
||||
|
||||
配置文件位于 `app/config.py`,主要配置项:
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `BASE_DIR` | 项目根目录 | 自动获取 |
|
||||
| `SAVE_DIRECTORY` | 下载文件保存目录 | `下载文件/` |
|
||||
| `MODE_DIRECTORY` | 模板文件保存目录 | `模板文件/` |
|
||||
| `LOGS_DIRECTORY` | 日志文件目录 | `logs/` |
|
||||
| `LOG_FILE` | 日志文件路径 | `logs/简道云.log` |
|
||||
| `JIANDAOYUN_API_TOKEN` | 简道云API Token | 需配置 |
|
||||
|
||||
## 📝 开发指南
|
||||
|
||||
### 添加新操作
|
||||
|
||||
1. **在模块中实现方法**:
|
||||
```python
|
||||
# app/module/your_module.py
|
||||
class YourModule:
|
||||
def your_action(self, data: Dict[str, Any]) -> Dict[str, str]:
|
||||
# 实现业务逻辑
|
||||
return {'msg': '操作完成'}
|
||||
```
|
||||
|
||||
2. **在 main.py 中注册**:
|
||||
```python
|
||||
# 在 lifespan 函数中
|
||||
core_manager.register_action(
|
||||
'your_action',
|
||||
your_module.your_action,
|
||||
'your_module',
|
||||
description='操作描述'
|
||||
)
|
||||
```
|
||||
|
||||
### 添加新路由
|
||||
|
||||
在 `app/api/routes.py` 中添加新的路由:
|
||||
|
||||
```python
|
||||
@router.post("/your-endpoint", tags=["业务"])
|
||||
async def your_endpoint(
|
||||
request: Request,
|
||||
logger: logging.Logger = Depends(get_logger)
|
||||
):
|
||||
# 实现路由逻辑
|
||||
return {"message": "success"}
|
||||
```
|
||||
|
||||
### 添加新的数据模型
|
||||
|
||||
在 `app/schemas.py` 中添加新的 Pydantic 模型:
|
||||
|
||||
```python
|
||||
class YourRequest(BaseModel):
|
||||
field1: str = Field(..., description="字段1")
|
||||
field2: Optional[int] = Field(None, description="字段2")
|
||||
```
|
||||
|
||||
### 添加后台任务
|
||||
|
||||
1. **在 tasks 目录下创建任务文件**:
|
||||
```python
|
||||
# app/tasks/your_tasks.py
|
||||
def your_task_background(data, ...):
|
||||
# 实现后台任务逻辑
|
||||
# 完成后调用 update_jiandaoyun 和 approve_workflow
|
||||
```
|
||||
|
||||
2. **在模块中调用**:
|
||||
```python
|
||||
import threading
|
||||
from app.tasks.your_tasks import your_task_background
|
||||
|
||||
thread = threading.Thread(target=your_task_background, args=(...))
|
||||
thread.start()
|
||||
return {'msg': '正在执行中'}
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 健康检查测试
|
||||
|
||||
```bash
|
||||
curl http://localhost:5003/health
|
||||
```
|
||||
|
||||
### Webhook 测试
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/webhook \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Action: login_in" \
|
||||
-d '{
|
||||
"username": "test",
|
||||
"password": "test",
|
||||
"company_name": "测试公司"
|
||||
}'
|
||||
```
|
||||
|
||||
## 📊 日志
|
||||
|
||||
日志文件位于 `logs/简道云.log`,支持日志轮转:
|
||||
|
||||
- 单个日志文件最大 5MB
|
||||
- 保留 5 个备份文件
|
||||
- 使用 UTF-8 编码
|
||||
- 日志格式:`时间戳 级别:模块名:消息`
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **API Token**: 当前 API Token 硬编码在配置文件中,生产环境建议使用环境变量管理
|
||||
2. **CORS**: 当前配置允许所有来源,生产环境建议限制允许的来源
|
||||
3. **超时时间**: 任务执行超时时间默认为 55 秒,可根据需要调整
|
||||
4. **文件存储**: 下载的文件存储在 `下载文件/` 目录,注意定期清理
|
||||
5. **验证码识别**: 需要系统安装 Tesseract OCR 才能正常识别验证码
|
||||
6. **删除操作**: 删除客户和车辆时会检查会员卡和消费记录,有记录的数据会被跳过
|
||||
7. **批量操作**: 批量操作在后台线程中执行,不会阻塞主请求
|
||||
|
||||
## 🔄 版本历史
|
||||
|
||||
- **v2.0.0**:
|
||||
- fastapi 完全重构
|
||||
- 支持 F6 系统相关操作
|
||||
- 支持BI任务处理
|
||||
|
||||
|
||||
- **1.0.0**: 初始版本
|
||||
- 实现基本的 Webhook 接口
|
||||
- 支持 F6 系统相关操作
|
||||
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目为内部项目,仅供内部使用。
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
如有问题,请联系数据组。
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025年
|
||||
@@ -1,186 +0,0 @@
|
||||
# API模块更新说明
|
||||
|
||||
## 更新内容
|
||||
|
||||
根据 `fastapi_app/api.py`(最新版)更新了 `fastapi_app/app/api.py`,主要更新包括:
|
||||
|
||||
### 1. 添加失败重试机制
|
||||
|
||||
所有API函数都已添加重试机制,参考 `entry_data_list` 的实现模式:
|
||||
|
||||
- **默认重试次数**:20次(部分函数为10次)
|
||||
- **重试逻辑**:
|
||||
- 捕获 `requests.exceptions.RequestException` 异常
|
||||
- 重试前等待时间:0.1秒(快速请求)或3-10秒(慢速请求)
|
||||
- 超过最大重试次数后记录错误日志并返回None或抛出异常
|
||||
|
||||
### 2. 新增功能函数
|
||||
|
||||
添加了以下新函数(原版本中没有的):
|
||||
|
||||
- `entry_data_banch_update()` - 批量修改数据
|
||||
- `entry_data_delete()` - 删除单条数据
|
||||
- `entry_data_batch_delete()` - 批量删除数据
|
||||
- `workflow_task_hand_over()` - 流程待办转交
|
||||
- `get_upload_token()` - 获取文件上传凭证
|
||||
- `upload_file()` - 上传文件
|
||||
|
||||
### 3. 功能增强
|
||||
|
||||
- **`entry_data_get()`**:
|
||||
- 添加 `replace` 参数(默认True,保持向后兼容)
|
||||
- 添加 `max_retries` 参数
|
||||
- 添加重试机制
|
||||
|
||||
- **`entry_data_list()`**:
|
||||
- 添加 `replace` 参数(默认True,保持向后兼容)
|
||||
- 添加 `max_retries` 参数
|
||||
- 改进重试逻辑,支持分页重试
|
||||
|
||||
- **`entry_widget_list()`**:
|
||||
- 添加 `max_retries` 参数
|
||||
- 添加重试机制
|
||||
|
||||
- **`data_batch_create()`**:
|
||||
- 添加 `max_retries` 参数
|
||||
- 添加重试机制
|
||||
- 支持 `is_start_workflow`、`is_start_trigger`、`transaction_id` 参数
|
||||
|
||||
- **`entry_data_batch_create()`**:
|
||||
- 添加 `max_retries` 参数
|
||||
- 添加重试机制
|
||||
- 支持 `is_start_workflow`、`is_start_trigger` 参数
|
||||
- 使用 `NpEncoder` 处理NumPy数据类型
|
||||
|
||||
- **`entry_data_update()`**:
|
||||
- 添加 `max_retries` 参数
|
||||
- 添加重试机制
|
||||
|
||||
- **`workflow_instance_get()`**:
|
||||
- 添加 `max_retries` 参数
|
||||
- 添加重试机制
|
||||
|
||||
- **`workflow_task_approve()`**:
|
||||
- 添加 `max_retries` 参数
|
||||
- 添加重试机制
|
||||
- `comment` 参数支持自定义(默认"自动转交")
|
||||
|
||||
### 4. 工具函数
|
||||
|
||||
- **`NpEncoder`**:JSON编码器,处理NumPy数据类型
|
||||
- **`replace_decimals()`**:递归替换Decimal类型为float
|
||||
|
||||
### 5. 字段替换优化
|
||||
|
||||
`field_replacement()` 方法使用递归实现,更优雅地处理嵌套数据结构。
|
||||
|
||||
## 兼容性保证
|
||||
|
||||
### 向后兼容
|
||||
|
||||
1. **默认参数**:
|
||||
- `entry_data_get()` 和 `entry_data_list()` 的 `replace` 参数默认为 `True`,保持原有行为
|
||||
- 所有新增的 `max_retries` 参数都有合理的默认值
|
||||
|
||||
2. **导入路径**:
|
||||
- 使用 `from app.config import Config`(而非 `from config import Config`)
|
||||
- 使用 `logging.getLogger('app')`(而非 `log_config`)
|
||||
|
||||
3. **函数签名**:
|
||||
- 所有原有函数的调用方式保持不变
|
||||
- 新增参数都是可选参数
|
||||
|
||||
### 日志系统
|
||||
|
||||
- 使用 `logging.getLogger('app')` 作为常规日志记录器
|
||||
- 使用 `logging.getLogger('app.error')` 作为错误日志记录器
|
||||
- 与 fastapi_app 项目的日志系统完全兼容
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本使用(保持原有方式)
|
||||
|
||||
```python
|
||||
from app.api import API
|
||||
|
||||
api = API()
|
||||
|
||||
# 获取单条数据(自动替换字段)
|
||||
data = api.entry_data_get({
|
||||
'api_key': 'xxx',
|
||||
'entry_id': 'xxx',
|
||||
'data_id': 'xxx'
|
||||
})
|
||||
|
||||
# 获取多条数据(自动替换字段)
|
||||
data_list = api.entry_data_list({
|
||||
'api_key': 'xxx',
|
||||
'entry_id': 'xxx'
|
||||
})
|
||||
```
|
||||
|
||||
### 使用新功能
|
||||
|
||||
```python
|
||||
# 不替换字段
|
||||
data = api.entry_data_get({
|
||||
'api_key': 'xxx',
|
||||
'entry_id': 'xxx',
|
||||
'data_id': 'xxx'
|
||||
}, replace=False)
|
||||
|
||||
# 自定义重试次数
|
||||
data = api.entry_data_get({
|
||||
'api_key': 'xxx',
|
||||
'entry_id': 'xxx',
|
||||
'data_id': 'xxx'
|
||||
}, max_retries=10)
|
||||
|
||||
# 批量删除
|
||||
result = api.entry_data_batch_delete({
|
||||
'api_key': 'xxx',
|
||||
'entry_id': 'xxx',
|
||||
'data_ids': ['id1', 'id2', 'id3']
|
||||
}, chunk_size=90, max_retries=20)
|
||||
```
|
||||
|
||||
## 重试机制说明
|
||||
|
||||
### 重试策略
|
||||
|
||||
1. **快速请求**(0.1秒延迟):
|
||||
- `entry_data_list()` - 分页请求
|
||||
- `entry_data_batch_create()` - 批量创建
|
||||
- `entry_data_batch_delete()` - 批量删除
|
||||
- `workflow_instance_get()` - 流程查询
|
||||
|
||||
2. **慢速请求**(3-10秒延迟):
|
||||
- `data_batch_create()` - 3秒延迟
|
||||
- `entry_data_update()` - 10秒延迟
|
||||
- `entry_data_banch_update()` - 10秒延迟
|
||||
- `entry_data_delete()` - 10秒延迟
|
||||
- `workflow_task_approve()` - 3秒延迟
|
||||
- `workflow_task_hand_over()` - 3秒延迟
|
||||
- `get_upload_token()` - 3秒延迟
|
||||
- `upload_file()` - 3秒延迟
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 所有重试失败的操作都会记录到错误日志
|
||||
- 部分函数在重试失败后返回 `None`,部分会抛出异常
|
||||
- 错误日志包含失败的任务标识信息
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **超时设置**:所有请求都设置了 `timeout=10` 秒
|
||||
2. **状态码检查**:使用 `res.raise_for_status()` 检查HTTP状态码
|
||||
3. **文件上传**:`upload_file()` 使用 `with` 语句确保文件正确关闭
|
||||
4. **数据类型处理**:批量操作函数使用 `NpEncoder` 和 `replace_decimals()` 处理特殊数据类型
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. 测试所有原有功能的兼容性
|
||||
2. 测试新添加的函数
|
||||
3. 测试重试机制(可以模拟网络错误)
|
||||
4. 测试 `replace=False` 参数的使用
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
__all__ = []
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
"""
|
||||
API 模块 - 简道云API接口封装
|
||||
支持失败重试机制,兼容现有代码
|
||||
简道云 API 接口封装模块
|
||||
|
||||
本模块封装了简道云的所有 API 接口,包括:
|
||||
- 表单数据操作(获取、创建、更新、删除)
|
||||
- 表单字段获取
|
||||
- 工作流操作(获取实例、审批、转交)
|
||||
- 文件操作(获取上传凭证、上传文件)
|
||||
|
||||
特性:
|
||||
- 支持失败重试机制(默认最多重试20次)
|
||||
- 支持字段替换(将字段ID替换为标签名)
|
||||
- 支持批量操作(批量创建、更新、删除)
|
||||
- 支持数据类型转换(NumPy、Decimal等)
|
||||
- 完善的错误处理和日志记录
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
@@ -17,7 +29,15 @@ error_logger = logging.getLogger('app.error') # 错误日志记录器
|
||||
|
||||
|
||||
class NpEncoder(json.JSONEncoder):
|
||||
"""NumPy数据类型JSON编码器"""
|
||||
"""
|
||||
NumPy 数据类型 JSON 编码器
|
||||
|
||||
用于将 NumPy 数据类型转换为 JSON 可序列化的类型。
|
||||
支持:
|
||||
- np.integer -> int
|
||||
- np.floating -> float
|
||||
- np.ndarray -> list
|
||||
"""
|
||||
def default(self, obj):
|
||||
if isinstance(obj, np.integer):
|
||||
return int(obj)
|
||||
@@ -30,7 +50,18 @@ class NpEncoder(json.JSONEncoder):
|
||||
|
||||
|
||||
def replace_decimals(obj):
|
||||
"""递归替换Decimal类型为float"""
|
||||
"""
|
||||
递归替换 Decimal 类型为 float
|
||||
|
||||
递归遍历数据结构,将所有 Decimal 类型替换为 float 类型,
|
||||
用于 JSON 序列化。
|
||||
|
||||
Args:
|
||||
obj: 要处理的对象(可以是 dict、list 或其他类型)
|
||||
|
||||
Returns:
|
||||
处理后的对象,所有 Decimal 类型已替换为 float
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
return {k: replace_decimals(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
@@ -41,13 +72,29 @@ def replace_decimals(obj):
|
||||
|
||||
|
||||
class API:
|
||||
def entry_data_get(self, data: dict, replace: bool = True, max_retries: int = 20) -> Dict:
|
||||
"""
|
||||
简道云 API 接口封装类
|
||||
|
||||
提供简道云所有 API 接口的封装,包括表单操作、工作流操作、文件操作等。
|
||||
所有方法都支持失败重试机制,提高可靠性。
|
||||
"""
|
||||
|
||||
def entry_data_get(self, data: dict, replace: bool = False, max_retries: int = 20) -> Dict:
|
||||
"""
|
||||
获取单条表单数据
|
||||
:param replace: 是否替换字段,默认为True(保持向后兼容)
|
||||
:param max_retries: 最大重试次数
|
||||
:param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息
|
||||
:return: 表单数据
|
||||
|
||||
根据应用ID、表单ID和数据ID获取单条表单数据。
|
||||
|
||||
Args:
|
||||
data: 包含应用ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
replace: 是否替换字段(将字段ID替换为标签名),默认为False
|
||||
max_retries: 最大重试次数,默认为20次
|
||||
|
||||
Returns:
|
||||
Dict: 表单数据字典
|
||||
|
||||
Raises:
|
||||
Exception: 如果重试max_retries次后仍然失败
|
||||
"""
|
||||
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/get'
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
FastAPI 依赖注入模块
|
||||
|
||||
本模块提供 FastAPI 的依赖注入函数,用于在路由中注入可复用的依赖项。
|
||||
使用依赖注入可以减少代码重复,提高可测试性和可维护性。
|
||||
|
||||
提供的依赖项:
|
||||
- get_logger: 获取日志记录器
|
||||
- get_app_tools: 获取应用工具实例
|
||||
- get_f6_module: 获取 F6Module 实例
|
||||
- get_f6_plugin_module: 获取 F6PluginModule 实例
|
||||
- get_other_module: 获取 OtherPluginModule 实例
|
||||
- get_action_map: 获取操作映射表
|
||||
"""
|
||||
from fastapi import Request
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
|
||||
def get_logger(request: Request) -> logging.Logger:
|
||||
"""
|
||||
获取日志记录器依赖项
|
||||
|
||||
从应用状态中获取日志记录器,如果未初始化则返回一个基本的 logger。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
logging.Logger: 日志记录器实例
|
||||
"""
|
||||
logger = getattr(request.app.state, 'logger', None)
|
||||
if logger is None:
|
||||
# 如果 logger 未初始化,返回一个基本的 logger
|
||||
logger = logging.getLogger('app')
|
||||
return logger
|
||||
|
||||
|
||||
def get_app_tools(request: Request):
|
||||
"""
|
||||
获取应用工具实例依赖项
|
||||
|
||||
从应用状态中获取 AppTools 实例,用于任务队列管理等操作。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
AppTools: 应用工具实例
|
||||
|
||||
Raises:
|
||||
RuntimeError: 如果 AppTools 未初始化
|
||||
"""
|
||||
from app.utils.app_tools import AppTools
|
||||
app_tools = getattr(request.app.state, 'app_tools', None)
|
||||
if app_tools is None:
|
||||
raise RuntimeError("AppTools 未初始化")
|
||||
return app_tools
|
||||
|
||||
|
||||
def get_f6_module(request: Request):
|
||||
"""
|
||||
获取 F6Module 实例依赖项
|
||||
|
||||
从应用状态中获取 F6Module 实例,用于 F6 系统相关操作。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
F6Module: F6Module 实例
|
||||
|
||||
Raises:
|
||||
RuntimeError: 如果 F6Module 未初始化
|
||||
"""
|
||||
f6_module = getattr(request.app.state, 'f6_module', None)
|
||||
if f6_module is None:
|
||||
raise RuntimeError("F6Module 未初始化")
|
||||
return f6_module
|
||||
|
||||
|
||||
def get_f6_plugin_module(request: Request):
|
||||
"""
|
||||
获取 F6PluginModule 实例依赖项
|
||||
|
||||
从应用状态中获取 F6PluginModule 实例,用于 F6 插件相关操作。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
F6PluginModule: F6PluginModule 实例
|
||||
|
||||
Raises:
|
||||
RuntimeError: 如果 F6PluginModule 未初始化
|
||||
"""
|
||||
f6_plugin_module = getattr(request.app.state, 'f6_plugin_module', None)
|
||||
if f6_plugin_module is None:
|
||||
raise RuntimeError("F6PluginModule 未初始化")
|
||||
return f6_plugin_module
|
||||
|
||||
|
||||
def get_other_module(request: Request):
|
||||
"""
|
||||
获取 OtherPluginModule 实例依赖项
|
||||
|
||||
从应用状态中获取 OtherPluginModule 实例,用于其他插件相关操作。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
OtherPluginModule: OtherPluginModule 实例
|
||||
|
||||
Raises:
|
||||
RuntimeError: 如果 OtherPluginModule 未初始化
|
||||
"""
|
||||
other_module = getattr(request.app.state, 'other_module', None)
|
||||
if other_module is None:
|
||||
raise RuntimeError("OtherPluginModule 未初始化")
|
||||
return other_module
|
||||
|
||||
|
||||
def get_action_map(request: Request) -> dict:
|
||||
"""
|
||||
获取操作映射表依赖项
|
||||
|
||||
从模块注册表中获取所有注册的操作,并转换为字典格式(操作名 -> 处理函数)。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
dict: 操作映射表,格式为 {操作名: 处理函数}
|
||||
"""
|
||||
from app.core.module_registry import registry
|
||||
|
||||
# 从 registry 获取所有注册的操作
|
||||
actions = registry.get_all_actions()
|
||||
|
||||
# 转换为字典格式(handler 函数)
|
||||
action_map = {
|
||||
action_name: config.handler
|
||||
for action_name, config in actions.items()
|
||||
}
|
||||
|
||||
return action_map
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
API 路由定义模块
|
||||
|
||||
本模块定义所有 API 路由端点,包括:
|
||||
- /health: 健康检查端点
|
||||
- /webhook: Webhook 端点,处理简道云插件的请求
|
||||
|
||||
所有路由都使用 FastAPI 的依赖注入系统,通过 dependencies.py 中的函数注入依赖项。
|
||||
"""
|
||||
from fastapi import APIRouter, Request, HTTPException, status, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from pydantic import ValidationError
|
||||
from typing import Dict, Any
|
||||
import json
|
||||
import anyio
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from app.schemas import WebhookRequest, WebhookResponse, HealthResponse
|
||||
from app.api.dependencies import (
|
||||
get_logger,
|
||||
get_app_tools,
|
||||
get_f6_plugin_module,
|
||||
get_action_map
|
||||
)
|
||||
from app.utils.app_tools import AppTools
|
||||
|
||||
# 创建路由器
|
||||
# 使用 APIRouter 分离路由,便于管理和维护
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse, tags=["系统"])
|
||||
async def healthcheck():
|
||||
"""
|
||||
健康检查端点
|
||||
|
||||
用于检查服务是否正常运行
|
||||
"""
|
||||
return HealthResponse(status="ok", version="2.0.0")
|
||||
|
||||
|
||||
@router.post("/webhook", response_model=WebhookResponse, tags=["业务"])
|
||||
async def webhook(
|
||||
request: Request,
|
||||
logger: logging.Logger = Depends(get_logger),
|
||||
app_tools: AppTools = Depends(get_app_tools),
|
||||
f6_plugin_module = Depends(get_f6_plugin_module),
|
||||
action_map: Dict[str, Any] = Depends(get_action_map)
|
||||
):
|
||||
"""
|
||||
接受前端请求后将任务放入消息队列
|
||||
|
||||
此端点接收简道云插件的请求,根据请求头中的 Action 字段路由到相应的处理函数。
|
||||
支持的操作包括:登录、获取公司信息、文件校验、品牌创建等。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象,包含请求体和请求头
|
||||
logger: 日志记录器
|
||||
app_tools: 应用工具实例
|
||||
f6_plugin_module: F6插件模块实例
|
||||
action_map: 操作映射表
|
||||
|
||||
Returns:
|
||||
WebhookResponse: 任务处理结果
|
||||
|
||||
Raises:
|
||||
HTTPException: 当操作类型无效或任务执行超时时抛出
|
||||
"""
|
||||
try:
|
||||
# 获取请求数据并验证
|
||||
try:
|
||||
raw_data = await request.json()
|
||||
# 使用 Pydantic 进行数据验证(允许额外字段)
|
||||
webhook_data = WebhookRequest(**raw_data)
|
||||
data = webhook_data.model_dump(exclude_none=True)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="请求体必须是有效的 JSON 格式"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"请求数据验证失败: {str(e)}, 原始数据: {raw_data if 'raw_data' in locals() else 'N/A'}")
|
||||
# 如果验证失败,仍然尝试使用原始数据(向后兼容)
|
||||
data = raw_data if 'raw_data' in locals() else {}
|
||||
|
||||
# 获取并解码请求头
|
||||
header = request.headers
|
||||
decoded_header = app_tools.decode_headers(header)
|
||||
|
||||
# 验证 Action 字段(HTTP头在FastAPI中会被转换为小写)
|
||||
# 同时检查 'Action' 和 'action' 以兼容不同情况
|
||||
action = decoded_header.get('Action') or decoded_header.get('action')
|
||||
if not action:
|
||||
logger.warning(f"请求头中缺少 Action 字段,请求头: {decoded_header}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="请求头中缺少必需的 Action 字段"
|
||||
)
|
||||
|
||||
# 处理 F6_Plugin 特殊逻辑
|
||||
if action == 'F6_Plugin':
|
||||
# 同时检查 'Check' 和 'check' 以兼容不同情况
|
||||
check = decoded_header.get('Check') or decoded_header.get('check')
|
||||
if check == '否':
|
||||
handler = f6_plugin_module.check_file
|
||||
elif check == '是':
|
||||
sub_action = data.get('Action')
|
||||
if not sub_action:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="F6_Plugin 操作需要提供 Action 字段"
|
||||
)
|
||||
handler = action_map.get(sub_action)
|
||||
if not handler:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"未知的子操作类型: {sub_action}"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"F6_Plugin 操作需要提供有效的 Check 字段(是/否),当前值: {check}"
|
||||
)
|
||||
else:
|
||||
handler = action_map.get(action)
|
||||
if not handler:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"未知的操作类型: {action}。支持的操作: {', '.join(action_map.keys())}"
|
||||
)
|
||||
|
||||
logger.info(f"接收到操作请求: {action}, 数据ID: {data.get('data_id', 'N/A')}")
|
||||
|
||||
# 将任务放入消息队列
|
||||
response_queue = app_tools.enqueue_task(handler, data)
|
||||
|
||||
# 等待任务处理结果(添加超时保护,简道云默认60秒)
|
||||
try:
|
||||
# 使用 asyncio.wait_for 添加超时
|
||||
result = await asyncio.wait_for(
|
||||
anyio.to_thread.run_sync(response_queue.get),
|
||||
timeout=55.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"任务执行超时: {action}, 数据ID: {data.get('data_id', 'N/A')}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
|
||||
detail="任务执行超时,请稍后重试"
|
||||
)
|
||||
|
||||
# 验证返回结果格式
|
||||
if not isinstance(result, dict):
|
||||
result = {"msg": str(result)}
|
||||
|
||||
# 处理 msg 字段:如果 msg 是字典,将其内容展开到结果中
|
||||
if "msg" in result and isinstance(result["msg"], dict):
|
||||
msg_dict = result.pop("msg")
|
||||
logger.warning(f"操作 {action} 返回的 msg 字段是字典类型,正在自动转换。原始数据: {json.dumps(msg_dict, ensure_ascii=False)}")
|
||||
# 如果字典中有 msg 字段,使用它;否则使用 JSON 字符串
|
||||
if "msg" in msg_dict:
|
||||
result["msg"] = msg_dict.pop("msg")
|
||||
else:
|
||||
result["msg"] = json.dumps(msg_dict, ensure_ascii=False)
|
||||
# 将字典中的其他字段合并到结果中
|
||||
result.update(msg_dict)
|
||||
|
||||
if "msg" not in result:
|
||||
result["msg"] = "操作完成"
|
||||
|
||||
# 确保 msg 是字符串类型
|
||||
if not isinstance(result.get("msg"), str):
|
||||
logger.warning(f"操作 {action} 返回的 msg 字段类型为 {type(result.get('msg'))},正在转换为字符串")
|
||||
result["msg"] = str(result.get("msg", "操作完成"))
|
||||
|
||||
logger.info(f"操作完成: {action}, 结果: {json.dumps(result, ensure_ascii=False)}")
|
||||
|
||||
# 返回响应(使用 Pydantic 模型验证)
|
||||
try:
|
||||
return WebhookResponse(**result)
|
||||
except ValidationError as validation_error:
|
||||
# 捕获 Pydantic 验证错误,提供更清晰的错误信息
|
||||
error_messages = []
|
||||
for error in validation_error.errors():
|
||||
field = " -> ".join(str(loc) for loc in error.get("loc", []))
|
||||
error_type = error.get("type", "unknown")
|
||||
error_msg = error.get("msg", "验证失败")
|
||||
error_messages.append(f"字段 '{field}': {error_msg} (类型: {error_type})")
|
||||
|
||||
error_detail = "; ".join(error_messages)
|
||||
logger.error(
|
||||
f"响应数据验证失败 - 操作: {action}, "
|
||||
f"错误详情: {error_detail}, "
|
||||
f"原始数据: {json.dumps(result, ensure_ascii=False, default=str)}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"响应数据格式错误: {error_detail}。请检查操作 '{action}' 的返回格式是否符合 API 规范(msg 字段必须是字符串类型)。"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
# 重新抛出 HTTP 异常
|
||||
raise
|
||||
except Exception as e:
|
||||
# 捕获其他未预期的异常
|
||||
logger.error(f"处理请求时发生未预期的错误: {type(e).__name__} - {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"处理请求时发生错误: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""
|
||||
后台任务模块 - 向后兼容入口
|
||||
此文件保持向后兼容,实际功能已拆分到 app.tasks 模块中
|
||||
"""
|
||||
# 从新的 tasks 模块导入所有函数,保持向后兼容
|
||||
from app.tasks import (
|
||||
update_jiandaoyun,
|
||||
approve_workflow,
|
||||
create_brand_background,
|
||||
delete_history_background,
|
||||
delete_customer_background,
|
||||
delete_car_background,
|
||||
modify_customer_info_background,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'update_jiandaoyun',
|
||||
'approve_workflow',
|
||||
'create_brand_background',
|
||||
'delete_history_background',
|
||||
'delete_customer_background',
|
||||
'delete_car_background',
|
||||
'modify_customer_info_background',
|
||||
]
|
||||
+31
-1
@@ -1,29 +1,59 @@
|
||||
"""
|
||||
应用配置模块
|
||||
|
||||
本模块负责管理应用的所有配置项,包括:
|
||||
- 目录路径配置
|
||||
- API Token 配置
|
||||
- 日志配置
|
||||
|
||||
注意:生产环境建议将敏感信息(如 API Token)移至环境变量。
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
# 获取当前文件所在的目录
|
||||
# 当前文件位于 app/config.py,parent.parent 获取项目根目录
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent # 项目根目录
|
||||
|
||||
# 构建保存下载文件的目录路径
|
||||
# 用于存储从简道云下载的文件
|
||||
SAVE_DIRECTORY = BASE_DIR / '下载文件'
|
||||
|
||||
# 构建保存模板文件的目录路径
|
||||
# 用于存储模板文件
|
||||
MODE_DIRECTORY = BASE_DIR / '模板文件'
|
||||
|
||||
# 构建日志文件的目录路径
|
||||
# 用于存储应用日志文件
|
||||
LOGS_DIRECTORY = BASE_DIR / 'logs'
|
||||
|
||||
# 日志文件路径
|
||||
LOG_FILE = LOGS_DIRECTORY / '简道云.log'
|
||||
|
||||
# 确保目录存在,如果不存在则创建
|
||||
# 在应用启动时自动创建必要的目录
|
||||
SAVE_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
||||
MODE_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# API 配置
|
||||
# 简道云 API Token,用于调用简道云 API
|
||||
# 注意:生产环境建议使用环境变量管理此配置
|
||||
JIANDAOYUN_API_TOKEN = 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN' # 曹伟应用api测试 app_key
|
||||
|
||||
|
||||
# 导出配置
|
||||
class Config:
|
||||
"""
|
||||
应用配置类
|
||||
|
||||
统一管理应用的所有配置项,方便在应用中使用。
|
||||
|
||||
属性:
|
||||
BASE_DIR: 项目根目录路径
|
||||
SAVE_DIRECTORY: 下载文件保存目录
|
||||
MODE_DIRECTORY: 模板文件保存目录
|
||||
JIANDAOYUN_API_TOKEN: 简道云 API Token
|
||||
LOGS_DIRECTORY: 日志文件目录
|
||||
LOG_FILE: 日志文件路径
|
||||
"""
|
||||
BASE_DIR = BASE_DIR
|
||||
SAVE_DIRECTORY = SAVE_DIRECTORY
|
||||
MODE_DIRECTORY = MODE_DIRECTORY
|
||||
|
||||
+16
-5
@@ -1,17 +1,28 @@
|
||||
"""
|
||||
核心模块初始化
|
||||
统一初始化请求头管理器、模块注册表等核心组件
|
||||
|
||||
本模块统一初始化和管理核心组件,包括:
|
||||
- ModuleRegistry: 模块注册表
|
||||
- CoreManager: 核心管理器
|
||||
|
||||
提供统一的接口来管理这些核心组件。
|
||||
"""
|
||||
from typing import Dict, Any, Callable
|
||||
from app.core.header_manager import HeaderManager
|
||||
from app.core.module_registry import ModuleRegistry, registry
|
||||
|
||||
|
||||
class CoreManager:
|
||||
"""核心管理器 - 统一管理所有核心组件"""
|
||||
"""
|
||||
核心管理器
|
||||
|
||||
统一管理所有核心组件,提供便捷的方法来初始化和注册模块。
|
||||
|
||||
属性:
|
||||
registry: 模块注册表实例
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.header_manager = HeaderManager
|
||||
"""初始化核心管理器"""
|
||||
self.registry = registry
|
||||
|
||||
def initialize_modules(self, modules: Dict[str, Any]):
|
||||
@@ -49,12 +60,12 @@ class CoreManager:
|
||||
|
||||
|
||||
# 全局核心管理器实例
|
||||
# 在应用启动时使用此实例来注册模块和操作
|
||||
core_manager = CoreManager()
|
||||
|
||||
# 导出常用类和函数
|
||||
__all__ = [
|
||||
'core_manager',
|
||||
'HeaderManager',
|
||||
'ModuleRegistry',
|
||||
'registry',
|
||||
]
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
请求头管理器
|
||||
统一管理不同模块的请求头配置
|
||||
"""
|
||||
from typing import Dict, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class HeaderConfig:
|
||||
"""请求头配置"""
|
||||
referer: Optional[str] = None
|
||||
user_agent: Optional[str] = None
|
||||
content_type: Optional[str] = None
|
||||
authorization: Optional[str] = None
|
||||
custom_headers: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
"""转换为字典格式"""
|
||||
headers = {}
|
||||
|
||||
if self.referer:
|
||||
headers['Referer'] = self.referer
|
||||
if self.user_agent:
|
||||
headers['User-Agent'] = self.user_agent
|
||||
if self.content_type:
|
||||
headers['Content-Type'] = self.content_type
|
||||
if self.authorization:
|
||||
headers['Authorization'] = self.authorization
|
||||
|
||||
# 添加自定义请求头
|
||||
headers.update(self.custom_headers)
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
class HeaderManager:
|
||||
"""请求头管理器"""
|
||||
|
||||
# 默认请求头配置
|
||||
DEFAULT_HEADERS = HeaderConfig(
|
||||
referer='https://yunxiu.f6car.cn/erp/view/index.html',
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'
|
||||
)
|
||||
|
||||
# F6系统登录请求头
|
||||
F6_LOGIN_HEADERS = HeaderConfig(
|
||||
referer='https://yunxiu.f6car.com/kzf6/login/confirm',
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.0.0'
|
||||
)
|
||||
|
||||
# 简道云API请求头
|
||||
JIANDAOYUN_API_HEADERS = HeaderConfig(
|
||||
content_type='application/json',
|
||||
# authorization 应该从配置中获取,这里只是示例
|
||||
)
|
||||
|
||||
# 模块特定的请求头配置
|
||||
_module_headers: Dict[str, HeaderConfig] = {
|
||||
'default': DEFAULT_HEADERS,
|
||||
'f6_login': F6_LOGIN_HEADERS,
|
||||
'jiandaoyun_api': JIANDAOYUN_API_HEADERS,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_headers(cls, module_name: str = 'default', **overrides) -> Dict[str, str]:
|
||||
"""
|
||||
获取指定模块的请求头
|
||||
|
||||
Args:
|
||||
module_name: 模块名称
|
||||
**overrides: 覆盖的请求头配置
|
||||
|
||||
Returns:
|
||||
请求头字典
|
||||
"""
|
||||
# 获取基础配置
|
||||
config = cls._module_headers.get(module_name, cls.DEFAULT_HEADERS)
|
||||
|
||||
# 创建配置副本
|
||||
header_config = HeaderConfig(
|
||||
referer=overrides.get('referer', config.referer),
|
||||
user_agent=overrides.get('user_agent', config.user_agent),
|
||||
content_type=overrides.get('content_type', config.content_type),
|
||||
authorization=overrides.get('authorization', config.authorization),
|
||||
custom_headers={**config.custom_headers, **overrides.get('custom_headers', {})}
|
||||
)
|
||||
|
||||
return header_config.to_dict()
|
||||
|
||||
@classmethod
|
||||
def register_module_headers(cls, module_name: str, config: HeaderConfig):
|
||||
"""
|
||||
注册模块的请求头配置
|
||||
|
||||
Args:
|
||||
module_name: 模块名称
|
||||
config: 请求头配置
|
||||
"""
|
||||
cls._module_headers[module_name] = config
|
||||
|
||||
@classmethod
|
||||
def update_module_headers(cls, module_name: str, **updates):
|
||||
"""
|
||||
更新模块的请求头配置
|
||||
|
||||
Args:
|
||||
module_name: 模块名称
|
||||
**updates: 要更新的配置项
|
||||
"""
|
||||
if module_name in cls._module_headers:
|
||||
config = cls._module_headers[module_name]
|
||||
for key, value in updates.items():
|
||||
if hasattr(config, key):
|
||||
setattr(config, key, value)
|
||||
else:
|
||||
config.custom_headers[key] = value
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
"""
|
||||
模块注册和路由管理
|
||||
提供统一的模块注册机制,方便添加新功能模块
|
||||
模块注册表模块
|
||||
|
||||
本模块提供统一的模块注册机制,用于管理所有业务模块和操作。
|
||||
使用注册表模式可以:
|
||||
- 统一管理所有操作
|
||||
- 方便添加新功能模块
|
||||
- 支持动态路由和操作查找
|
||||
- 提供操作元数据管理
|
||||
"""
|
||||
from typing import Dict, Callable, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
@@ -8,7 +14,18 @@ from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class ActionConfig:
|
||||
"""操作配置"""
|
||||
"""
|
||||
操作配置数据类
|
||||
|
||||
存储操作的配置信息,包括处理函数、所属模块、描述等。
|
||||
|
||||
属性:
|
||||
handler: 处理函数,执行具体业务逻辑
|
||||
module_name: 所属模块名称
|
||||
description: 操作描述,用于文档和日志
|
||||
requires_auth: 是否需要认证,默认 True
|
||||
header_module: 使用的请求头模块名称,可选
|
||||
"""
|
||||
handler: Callable # 处理函数
|
||||
module_name: str # 所属模块名称
|
||||
description: Optional[str] = None # 描述
|
||||
@@ -17,9 +34,19 @@ class ActionConfig:
|
||||
|
||||
|
||||
class ModuleRegistry:
|
||||
"""模块注册表"""
|
||||
"""
|
||||
模块注册表类
|
||||
|
||||
统一管理所有业务模块和操作的注册表。
|
||||
支持注册模块实例和操作,并提供查询功能。
|
||||
|
||||
属性:
|
||||
_actions: 操作字典,格式为 {操作名: ActionConfig}
|
||||
_modules: 模块字典,格式为 {模块名: 模块信息}
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化模块注册表"""
|
||||
self._actions: Dict[str, ActionConfig] = {}
|
||||
self._modules: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
@@ -65,7 +92,12 @@ class ModuleRegistry:
|
||||
return self._actions.get(action_name)
|
||||
|
||||
def get_all_actions(self) -> Dict[str, ActionConfig]:
|
||||
"""获取所有注册的操作"""
|
||||
"""
|
||||
获取所有注册的操作
|
||||
|
||||
Returns:
|
||||
Dict[str, ActionConfig]: 所有注册的操作字典的副本
|
||||
"""
|
||||
return self._actions.copy()
|
||||
|
||||
def register_module(self, module_name: str, module_instance: Any, **metadata):
|
||||
@@ -113,4 +145,5 @@ class ModuleRegistry:
|
||||
|
||||
|
||||
# 全局模块注册表实例
|
||||
# 在应用启动时使用此实例来注册所有模块和操作
|
||||
registry = ModuleRegistry()
|
||||
|
||||
+377
-35
@@ -1,26 +1,69 @@
|
||||
"""
|
||||
F6 后台执行模块
|
||||
|
||||
本模块提供 F6 插件相关的功能,包括:
|
||||
- 文件上传和校验
|
||||
- 品牌批量创建
|
||||
- 历史记录删除
|
||||
- 客户信息管理
|
||||
- 车辆信息管理
|
||||
- 项目信息批量启停
|
||||
- 材料信息批量修改
|
||||
- 项目信息批量修改
|
||||
|
||||
依赖:
|
||||
- requests: HTTP 请求
|
||||
- pandas: Excel 文件处理
|
||||
- threading: 后台任务处理
|
||||
"""
|
||||
import logging
|
||||
import traceback
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
import pandas as pd
|
||||
import os
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
from app.api import API
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
from app.config import Config
|
||||
from app.module import F6Module
|
||||
import threading
|
||||
from app import back_ground_tasks
|
||||
|
||||
from app.api import API
|
||||
from app.config import Config
|
||||
from app.module.module import F6Module
|
||||
from app.tasks.brand_tasks import create_brand_background
|
||||
from app.tasks.delete_tasks import (
|
||||
delete_history_background,
|
||||
delete_customer_background,
|
||||
delete_car_background
|
||||
)
|
||||
from app.tasks.material_tasks import (
|
||||
batch_disable_projects,
|
||||
batch_modify_materials,
|
||||
batch_modify_projects
|
||||
)
|
||||
from app.tasks.customer_tasks import modify_customer_info_background
|
||||
from app.tasks.bi_tasks import bi_task_background
|
||||
|
||||
# 简道云 API 实例,用于调用简道云 API
|
||||
api_instance = API()
|
||||
|
||||
logger = logging.getLogger('app')
|
||||
|
||||
|
||||
class F6PluginModule:
|
||||
"""
|
||||
F6 插件模块类
|
||||
|
||||
提供 F6 插件相关的所有功能,包括文件处理、品牌管理、数据删除等。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def accept_file(data: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any]]: # 接收文件
|
||||
def accept_file(data: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any]]:
|
||||
"""
|
||||
接收文件。
|
||||
|
||||
接收文件
|
||||
|
||||
处理前端上传的文件,下载文件并保存到指定目录。
|
||||
|
||||
此方法用于处理前端上传的文件,下载文件并保存到指定目录。主要步骤包括:
|
||||
1. 处理前端传递的数据,获取文件的URL。
|
||||
2. 解析URL以获取文件名。
|
||||
@@ -34,26 +77,26 @@ class F6PluginModule:
|
||||
Returns:
|
||||
tuple: 包含文件保存路径和处理后的数据的元组。如果文件保存成功,返回保存路径和数据;如果失败,返回 None 和数据。
|
||||
"""
|
||||
data = api_instance.entry_data_get(data=data)
|
||||
data = api_instance.entry_data_get(data=data, replace=True)
|
||||
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}')
|
||||
@@ -87,8 +130,7 @@ class F6PluginModule:
|
||||
else:
|
||||
return None, data
|
||||
|
||||
|
||||
def check_file(self, data: Dict[str, Any]) -> Dict[str, str]: # 校验上传文件
|
||||
def check_file(self, data: Dict[str, Any]) -> dict[str, str] | None: # 校验上传文件
|
||||
"""
|
||||
校验上传文件。
|
||||
|
||||
@@ -112,7 +154,7 @@ class F6PluginModule:
|
||||
# 安全地获取 Action 字段
|
||||
data_dict = data1.get('data', {})
|
||||
action = data_dict.get('Action(隐藏)')
|
||||
|
||||
|
||||
if not action:
|
||||
return {'msg': '缺少Action字段,无法校验文件'}
|
||||
|
||||
@@ -134,8 +176,38 @@ class F6PluginModule:
|
||||
else:
|
||||
print("'msg':'文件上传格式错误'")
|
||||
return {'msg': '文件上传格式错误'}
|
||||
elif action == 'delete_cars':
|
||||
pass
|
||||
elif action == 'disable_project':
|
||||
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 == 'batch_modify_materials':
|
||||
df2 = pd.read_excel(save_path, sheet_name=0)
|
||||
required_columns = {'原材料编码', '新材料编码', '品牌', '名称', '规格'}
|
||||
actual_columns = set(df2.columns)
|
||||
if required_columns.issubset(actual_columns):
|
||||
print('文件校验成功')
|
||||
return {'msg': f'{save_path}', 'check': '是'}
|
||||
else:
|
||||
missing = required_columns - actual_columns
|
||||
print(f"文件上传格式错误:缺少列 {missing}")
|
||||
return {'msg': '文件上传格式错误'}
|
||||
|
||||
elif action == 'batch_modify_projects':
|
||||
df3 = pd.read_excel(save_path, sheet_name=0)
|
||||
required_columns = {'原项目编码', '新项目编码', '项目名称', '业务分类', '销项税率', '项目说明',
|
||||
}
|
||||
actual_columns = set(df3.columns)
|
||||
if required_columns.issubset(actual_columns):
|
||||
print('文件校验成功')
|
||||
return {'msg': f'{save_path}', 'check': '是'}
|
||||
else:
|
||||
missing = required_columns - actual_columns
|
||||
print(f"文件上传格式错误:缺少列 {missing}")
|
||||
return {'msg': '文件上传格式错误'}
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -145,10 +217,21 @@ class F6PluginModule:
|
||||
else:
|
||||
return {'msg': '当前节点无附件上传', 'check': '是'}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_brand(data: Dict[str, Any]) -> Dict[str, str]: # 创建品牌
|
||||
entry_data = api_instance.entry_data_get(data=data)
|
||||
def create_brand(data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
创建品牌
|
||||
|
||||
从简道云获取品牌创建请求,读取 Excel 文件,并在后台线程中批量创建品牌。
|
||||
立即返回"正在执行"的提示,实际创建在后台线程中执行。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
"""
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
print('执行 品牌批量新建')
|
||||
username = entry_data['data']['账号']
|
||||
password = entry_data['data']['密码']
|
||||
@@ -167,7 +250,7 @@ class F6PluginModule:
|
||||
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
|
||||
|
||||
try:
|
||||
thread = threading.Thread(target=back_ground_tasks.create_brand_background,
|
||||
thread = threading.Thread(target=create_brand_background,
|
||||
args=(data, cookies, df, save_path))
|
||||
thread.start()
|
||||
except Exception as e:
|
||||
@@ -177,7 +260,19 @@ class F6PluginModule:
|
||||
|
||||
@staticmethod
|
||||
def delete_history(data: Dict[str, Any]) -> Dict[str, str]:
|
||||
entry_data = api_instance.entry_data_get(data=data)
|
||||
"""
|
||||
删除历史记录
|
||||
|
||||
从简道云获取删除历史记录请求,在后台线程中删除指定门店的历史维修记录。
|
||||
立即返回"正在执行中"的提示,实际删除在后台线程中执行。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典
|
||||
"""
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
username = entry_data['data']['账号']
|
||||
password = entry_data['data']['密码']
|
||||
company_name = entry_data['data']['公司名称']
|
||||
@@ -202,7 +297,7 @@ class F6PluginModule:
|
||||
org_id = org['orgId']
|
||||
|
||||
if org_id:
|
||||
thread = threading.Thread(target=back_ground_tasks.delete_history_background,
|
||||
thread = threading.Thread(target=delete_history_background,
|
||||
args=(data, cookies, org_id, org_name1))
|
||||
thread.start()
|
||||
return {'msg': '正在执行中', 'msg_details': '请稍后查看结果'}
|
||||
@@ -211,24 +306,40 @@ class F6PluginModule:
|
||||
|
||||
@staticmethod
|
||||
def delete_customer(data):
|
||||
entry_data = api_instance.entry_data_get(data=data)
|
||||
"""
|
||||
删除客户
|
||||
|
||||
从简道云获取删除客户请求,在后台线程中批量删除客户信息。
|
||||
立即返回"正在执行中"的提示,实际删除在后台线程中执行。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典
|
||||
"""
|
||||
print('执行 删除客户')
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
username = entry_data['data']['账号']
|
||||
password = entry_data['data']['密码']
|
||||
company_name = entry_data['data']['公司名称']
|
||||
|
||||
res = F6Module.login_in(username, password, company_name)
|
||||
print(res.json())
|
||||
|
||||
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"
|
||||
url = "https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=100&pageNo=1"
|
||||
res = requests.get(url, cookies=cookies)
|
||||
json = res.json()
|
||||
total = res.json().get('data', {}).get('total', 0)
|
||||
|
||||
if json:
|
||||
thread = threading.Thread(target=back_ground_tasks.delete_customer_background,
|
||||
args=(data, cookies, json['data']['data'],))
|
||||
if total:
|
||||
total = int(total)
|
||||
thread = threading.Thread(target=delete_customer_background,
|
||||
args=(data, cookies, total,))
|
||||
thread.start()
|
||||
return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'}
|
||||
return {'msg': '正在执行中',
|
||||
'msg_details': f'总计{total}条数据,8-20点3.5s一条数据,其余时间1.5s一条数据'}
|
||||
else:
|
||||
return {'msg': '未执行', 'msg_details': '无客户信息'}
|
||||
else:
|
||||
@@ -236,7 +347,19 @@ class F6PluginModule:
|
||||
|
||||
@staticmethod
|
||||
def delete_cars(data):
|
||||
entry_data = api_instance.entry_data_get(data=data)
|
||||
"""
|
||||
删除车辆
|
||||
|
||||
从简道云获取删除车辆请求,在后台线程中批量删除客户车辆信息。
|
||||
立即返回"正在执行中"的提示,实际删除在后台线程中执行。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典
|
||||
"""
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
username = entry_data['data']['账号']
|
||||
password = entry_data['data']['密码']
|
||||
company_name = entry_data['data']['公司名称']
|
||||
@@ -259,7 +382,7 @@ class F6PluginModule:
|
||||
all_page = total_items // 100 + (total_items % 100 > 0)
|
||||
|
||||
if res_data:
|
||||
thread = threading.Thread(target=back_ground_tasks.delete_car_background,
|
||||
thread = threading.Thread(target=delete_car_background,
|
||||
args=(data, url, cookies, header, all_page))
|
||||
thread.start()
|
||||
return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'}
|
||||
@@ -269,8 +392,21 @@ class F6PluginModule:
|
||||
else:
|
||||
return {'msg': '未执行', 'msg_details': '登录失败'}
|
||||
|
||||
def modify_customer_info(self, data: Dict[str, str]):
|
||||
entry_data = api_instance.entry_data_get(data=data)
|
||||
@staticmethod
|
||||
def modify_customer_info(data: Dict[str, str]):
|
||||
"""
|
||||
修改客户信息
|
||||
|
||||
从简道云获取修改客户信息请求,读取 Excel 文件,并在后台线程中批量修改客户信息。
|
||||
立即返回"正在执行中"的提示,实际修改在后台线程中执行。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典
|
||||
"""
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
username = entry_data['data']['账号']
|
||||
password = entry_data['data']['密码']
|
||||
company_name = entry_data['data']['公司名称']
|
||||
@@ -288,11 +424,217 @@ class F6PluginModule:
|
||||
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
|
||||
|
||||
if cookies:
|
||||
thread = threading.Thread(target=back_ground_tasks.modify_customer_info_background,
|
||||
thread = threading.Thread(target=modify_customer_info_background,
|
||||
args=(data, cookies, df, save_path))
|
||||
thread.start()
|
||||
return {'msg': '正在执行中', 'msg_details': '请稍后查看结果'}
|
||||
else:
|
||||
return {'msg': '未执行', 'msg_details': 'cookies获取失败'}
|
||||
|
||||
@staticmethod
|
||||
def disable_projects(data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
项目批量启停
|
||||
|
||||
从简道云获取项目批量启停请求,读取 Excel 文件,并在后台线程中批量启停项目。
|
||||
立即返回"正在执行"的提示,实际创建在后台线程中执行。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
"""
|
||||
try:
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
print('执行 项目批量停用/启用')
|
||||
username = entry_data['data']['账号']
|
||||
password = entry_data['data']['密码']
|
||||
company_name = entry_data['data']['公司名称']
|
||||
save_path = entry_data['data']['文件保存地址']
|
||||
option = entry_data['data']['项目材料批量操作']
|
||||
|
||||
login_response = F6Module.login_in(username, password, company_name)
|
||||
if login_response is None:
|
||||
logger.error(f"F6系统登录失败,用户名: {username}")
|
||||
return {'msg': '登录失败'}
|
||||
|
||||
try:
|
||||
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
|
||||
except Exception as e:
|
||||
logger.error(f"读取Excel文件失败: {save_path}, 错误: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
|
||||
|
||||
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
|
||||
logger.info("当前登录cookies:{}".format(cookies))
|
||||
|
||||
try:
|
||||
thread = threading.Thread(target=batch_disable_projects,
|
||||
args=(data, cookies, df, save_path, option))
|
||||
thread.start()
|
||||
except Exception as e:
|
||||
logger.error(f"创建线程失败: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
print(f'创建线程失败: {str(e)}')
|
||||
return {'msg': f'创建后台线程失败: {str(e)}'}
|
||||
|
||||
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
except KeyError as e:
|
||||
logger.error(f"数据字段缺失: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'数据字段缺失: {str(e)}'}
|
||||
except Exception as e:
|
||||
logger.error(f"项目批量启停任务执行失败: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'执行失败: {str(e)}'}
|
||||
|
||||
@staticmethod
|
||||
def modify_material(data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
材料批量修改
|
||||
|
||||
从简道云获取材料批量修改请求,读取 Excel 文件,并在后台线程中批量修改材料。
|
||||
立即返回"正在执行"的提示,实际创建在后台线程中执行。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
"""
|
||||
try:
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
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:
|
||||
logger.error(f"F6系统登录失败,用户名: {username}")
|
||||
return {'msg': '登录失败'}
|
||||
|
||||
try:
|
||||
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
|
||||
except Exception as e:
|
||||
logger.error(f"读取Excel文件失败: {save_path}, 错误: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
|
||||
|
||||
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
|
||||
|
||||
try:
|
||||
thread = threading.Thread(target=batch_modify_materials,
|
||||
args=(data, cookies, df, save_path))
|
||||
thread.start()
|
||||
except Exception as e:
|
||||
logger.error(f"创建线程失败: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
print(f'创建线程失败: {str(e)}')
|
||||
return {'msg': f'创建后台线程失败: {str(e)}'}
|
||||
|
||||
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
except KeyError as e:
|
||||
logger.error(f"数据字段缺失: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'数据字段缺失: {str(e)}'}
|
||||
except Exception as e:
|
||||
logger.error(f"材料批量修改任务执行失败: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'执行失败: {str(e)}'}
|
||||
|
||||
@staticmethod
|
||||
def modify_project(data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
项目批量修改
|
||||
|
||||
从简道云获取项目批量修改请求,读取 Excel 文件,并在后台线程中批量修改项目。
|
||||
立即返回"正在执行"的提示,实际创建在后台线程中执行。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
"""
|
||||
try:
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
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:
|
||||
logger.error(f"F6系统登录失败,用户名: {username}")
|
||||
return {'msg': '登录失败'}
|
||||
|
||||
try:
|
||||
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
|
||||
except Exception as e:
|
||||
logger.error(f"读取Excel文件失败: {save_path}, 错误: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
|
||||
|
||||
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
|
||||
|
||||
try:
|
||||
thread = threading.Thread(target=batch_modify_projects,
|
||||
args=(data, cookies, df, save_path))
|
||||
thread.start()
|
||||
except Exception as e:
|
||||
logger.error(f"创建线程失败: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
print(f'创建线程失败: {str(e)}')
|
||||
return {'msg': f'创建后台线程失败: {str(e)}'}
|
||||
|
||||
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
except KeyError as e:
|
||||
logger.error(f"数据字段缺失: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'数据字段缺失: {str(e)}'}
|
||||
except Exception as e:
|
||||
logger.error(f"项目批量修改任务执行失败: {str(e)}, 堆栈: {traceback.format_exc()}")
|
||||
return {'msg': f'执行失败: {str(e)}'}
|
||||
|
||||
@staticmethod
|
||||
def bi_task(data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
BI任务(示例)
|
||||
|
||||
从简道云获取BI任务请求,读取 Excel 文件(如果需要),并在后台线程中执行BI任务。
|
||||
立即返回"正在执行"的提示,实际执行在后台线程中完成。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
"""
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
print('执行 BI任务')
|
||||
|
||||
# 获取必要的参数(根据实际需求调整)
|
||||
username = entry_data['data'].get('账号')
|
||||
password = entry_data['data'].get('密码')
|
||||
company_name = entry_data['data'].get('公司名称')
|
||||
save_path = entry_data['data'].get('文件保存地址')
|
||||
|
||||
# 如果需要登录F6系统
|
||||
cookies = None
|
||||
if username and password and company_name:
|
||||
login_response = F6Module.login_in(username, password, company_name)
|
||||
if login_response is None:
|
||||
return {'msg': '登录失败', 'msg_details': '无法登录F6系统'}
|
||||
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
|
||||
|
||||
# 如果需要读取Excel文件
|
||||
df = None
|
||||
if save_path:
|
||||
try:
|
||||
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
|
||||
except Exception as e:
|
||||
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
|
||||
|
||||
# 启动后台线程执行BI任务
|
||||
try:
|
||||
thread = threading.Thread(target=bi_task_background,
|
||||
args=(data, cookies, df, save_path))
|
||||
thread.start()
|
||||
except Exception as e:
|
||||
print(f'创建线程失败: {str(e)}')
|
||||
return {'msg': '任务启动失败', 'msg_details': f'无法启动后台任务: {str(e)}'}
|
||||
|
||||
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
"""
|
||||
业务模块包
|
||||
|
||||
本包包含所有业务模块,包括:
|
||||
- module.py: F6Module - F6系统相关功能
|
||||
- f6_plugin_module.py: F6PluginModule - F6插件功能
|
||||
- other_module.py: OtherPluginModule - 其他功能模块
|
||||
"""
|
||||
__all__ = []
|
||||
|
||||
|
||||
+96
-1
@@ -1,3 +1,18 @@
|
||||
"""
|
||||
F6 前端即时响应模块
|
||||
|
||||
本模块提供 F6 系统相关的功能,包括:
|
||||
- 登录和认证
|
||||
- 验证码识别
|
||||
- 公司信息获取
|
||||
- 门店信息获取
|
||||
- 保持连接
|
||||
|
||||
依赖:
|
||||
- requests: HTTP 请求
|
||||
- PIL: 图像处理
|
||||
- pytesseract: OCR 识别
|
||||
"""
|
||||
import requests
|
||||
import hashlib
|
||||
from urllib.parse import quote
|
||||
@@ -7,15 +22,33 @@ from typing import Optional, Dict, AnyStr
|
||||
from PIL import Image, ImageEnhance
|
||||
import pytesseract
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
# 简道云 API 实例,用于调用简道云 API
|
||||
api_instance = API()
|
||||
|
||||
# 日志记录器
|
||||
logger = logging.getLogger('app')
|
||||
|
||||
|
||||
class F6Module:
|
||||
"""
|
||||
F6 系统模块类
|
||||
|
||||
提供 F6 系统相关的所有功能,包括登录、信息获取等。
|
||||
"""
|
||||
@staticmethod
|
||||
def get_captcha() -> AnyStr:
|
||||
"""
|
||||
获取并识别验证码
|
||||
|
||||
从 F6 系统获取验证码图片,使用 OCR 识别验证码文本。
|
||||
|
||||
Returns:
|
||||
AnyStr: 识别出的验证码文本
|
||||
|
||||
注意:
|
||||
需要系统安装 Tesseract OCR 才能正常工作
|
||||
"""
|
||||
captcha_url = 'https://yunxiu.f6car.cn/kzf6/login/captcha-image'
|
||||
response = requests.get(captcha_url)
|
||||
with open('captcha.png', 'wb') as f:
|
||||
@@ -37,6 +70,23 @@ class F6Module:
|
||||
|
||||
@staticmethod
|
||||
def login_in(username: str, password: str, company_name: str = '默认门店',) -> Optional[requests.Response]:
|
||||
"""
|
||||
F6 系统登录
|
||||
|
||||
使用用户名和密码登录 F6 系统,并选择指定的公司。
|
||||
如果触发验证码,会自动识别并重试登录。
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
password: 密码(明文,方法内部会进行 MD5 加密)
|
||||
company_name: 公司名称,默认为'默认门店'
|
||||
|
||||
Returns:
|
||||
Optional[requests.Response]: 登录响应对象,登录失败返回 None
|
||||
|
||||
注意:
|
||||
密码会在方法内部进行 MD5 加密处理
|
||||
"""
|
||||
url = "https://yunxiu.f6car.com/kzf6/login/confirm"
|
||||
session = requests.Session()
|
||||
header = {
|
||||
@@ -82,6 +132,17 @@ class F6Module:
|
||||
return None
|
||||
|
||||
def accept_login_message(self, data: Dict[str, str]) -> Dict[str, str]:
|
||||
"""
|
||||
接受登录消息并处理
|
||||
|
||||
处理简道云插件发送的登录请求,执行登录并返回结果。
|
||||
|
||||
Args:
|
||||
data: 包含用户名、密码、公司名称的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 登录结果,包含状态信息
|
||||
"""
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
company_name = data['company_name']
|
||||
@@ -110,6 +171,17 @@ class F6Module:
|
||||
return {"status": "登录失败,请检查公司名称"}
|
||||
|
||||
def get_company_information(self, data: Dict[str, str]) -> Dict[str, str]:
|
||||
"""
|
||||
获取公司信息
|
||||
|
||||
根据用户名和密码获取 F6 系统中的公司信息,并将结果保存到简道云。
|
||||
|
||||
Args:
|
||||
data: 包含用户名、密码的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含时间戳的消息,用于后续查询
|
||||
"""
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
||||
@@ -173,6 +245,18 @@ class F6Module:
|
||||
return res
|
||||
|
||||
def get_store_information(self, data: Dict[str, str]) -> Dict[str, dict[str, str]]:
|
||||
"""
|
||||
获取门店信息
|
||||
|
||||
根据用户名、密码和公司名称获取 F6 系统中的门店信息,
|
||||
包括门店列表、客户车辆数量、客户数量等。
|
||||
|
||||
Args:
|
||||
data: 包含用户名、密码、公司名称的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, dict[str, str]]: 包含时间戳、门店信息、统计数据的结果
|
||||
"""
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
company_name = data['company_name']
|
||||
@@ -221,6 +305,17 @@ class F6Module:
|
||||
|
||||
@staticmethod
|
||||
def get_keep_heart(data: Dict[str, str]) -> Dict[str, str]:
|
||||
"""
|
||||
保持连接
|
||||
|
||||
用于保持连接的心跳检测,直接返回接收到的数据。
|
||||
|
||||
Args:
|
||||
data: 接收到的数据字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 原样返回接收到的数据
|
||||
"""
|
||||
return data
|
||||
|
||||
|
||||
|
||||
+18
-14
@@ -1,22 +1,26 @@
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
import pandas as pd
|
||||
import os
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
from app.api import API
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
from app.config import Config
|
||||
from app.module import F6Module
|
||||
import threading
|
||||
from app import back_ground_tasks
|
||||
"""
|
||||
其他插件模块
|
||||
|
||||
api_instance = API()
|
||||
本模块提供其他插件相关的功能,目前包括短信签名状态查询等功能。
|
||||
"""
|
||||
|
||||
|
||||
class OtherPluginModule:
|
||||
|
||||
"""
|
||||
其他插件模块类
|
||||
|
||||
提供其他插件相关的功能,如短信签名状态等。
|
||||
"""
|
||||
|
||||
def sms_signature_status(self):
|
||||
"""
|
||||
短信签名状态
|
||||
|
||||
查询短信签名状态(待实现)。
|
||||
|
||||
Returns:
|
||||
待实现
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Pydantic 数据模型定义
|
||||
用于 FastAPI 请求和响应的数据验证
|
||||
"""
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ActionType(str, Enum):
|
||||
"""支持的操作类型枚举"""
|
||||
LOGIN_IN = "login_in"
|
||||
GET_COMPANY_INFORMATION = "get_company_information"
|
||||
GET_STORE_INFORMATION = "get_store_information"
|
||||
KEEP_ALIVE = "keep_alive"
|
||||
CHECK_FILE = "check_file"
|
||||
CREATE_BRAND = "create_brand"
|
||||
DELETE_HISTORY = "delete_history"
|
||||
DELETE_CUSTOMER = "delete_customer"
|
||||
DELETE_CARS = "delete_cars"
|
||||
SMS_SIGNATURE_STATUS = "sms_signature_status"
|
||||
MODIFY_CUSTOMER_INFO = "modify_customer_info"
|
||||
F6_PLUGIN = "F6_Plugin"
|
||||
|
||||
|
||||
class WebhookRequest(BaseModel):
|
||||
"""Webhook 请求体数据模型"""
|
||||
# 通用字段
|
||||
api_key: Optional[str] = Field(None, description="简道云应用ID")
|
||||
entry_id: Optional[str] = Field(None, description="简道云表单ID")
|
||||
data_id: Optional[str] = Field(None, description="简道云数据ID")
|
||||
Action: Optional[str] = Field(None, description="操作类型")
|
||||
|
||||
# 登录相关字段
|
||||
username: Optional[str] = Field(None, description="用户名")
|
||||
password: Optional[str] = Field(None, description="密码")
|
||||
company_name: Optional[str] = Field(None, description="公司名称")
|
||||
|
||||
# 文件相关字段
|
||||
file_path: Optional[str] = Field(None, description="文件保存路径")
|
||||
|
||||
# 其他字段(允许任意额外字段)
|
||||
class Config:
|
||||
extra = "allow" # 允许额外字段,因为简道云可能传递其他字段
|
||||
|
||||
|
||||
class WebhookHeader(BaseModel):
|
||||
"""Webhook 请求头数据模型"""
|
||||
Action: Optional[str] = Field(None, description="操作类型")
|
||||
Check: Optional[str] = Field(None, description="检查标志(是/否)")
|
||||
|
||||
class Config:
|
||||
extra = "allow" # 允许额外请求头
|
||||
|
||||
|
||||
class WebhookResponse(BaseModel):
|
||||
"""Webhook 响应数据模型"""
|
||||
msg: str = Field(..., description="响应消息")
|
||||
msg_details: Optional[str] = Field(None, description="详细信息")
|
||||
check: Optional[str] = Field(None, description="检查结果")
|
||||
status: Optional[str] = Field(None, description="状态")
|
||||
|
||||
class Config:
|
||||
extra = "allow" # 允许额外字段
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""健康检查响应模型"""
|
||||
status: str = Field("ok", description="服务状态")
|
||||
version: Optional[str] = Field(None, description="服务版本")
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""错误响应模型"""
|
||||
detail: str = Field(..., description="错误详情")
|
||||
error_code: Optional[str] = Field(None, description="错误代码")
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# 后台任务模块结构说明
|
||||
|
||||
## 模块结构
|
||||
|
||||
后台任务已按功能拆分为以下模块:
|
||||
|
||||
```
|
||||
app/tasks/
|
||||
├── __init__.py # 统一导出入口
|
||||
├── common.py # 通用功能模块(简道云表单更新、工作流审批)
|
||||
├── brand_tasks.py # 品牌相关任务
|
||||
├── delete_tasks.py # 删除相关任务
|
||||
└── customer_tasks.py # 客户相关任务
|
||||
```
|
||||
|
||||
## 模块说明
|
||||
|
||||
### common.py - 通用功能模块
|
||||
包含所有任务共用的功能:
|
||||
- `update_jiandaoyun()` - 更新简道云表单
|
||||
- `approve_workflow()` - 工作流审批
|
||||
|
||||
### brand_tasks.py - 品牌任务模块
|
||||
品牌相关的后台任务:
|
||||
- `create_brand_background()` - 品牌批量创建
|
||||
|
||||
### delete_tasks.py - 删除任务模块
|
||||
删除相关的后台任务:
|
||||
- `delete_history_background()` - 删除历史维修记录
|
||||
- `delete_customer_background()` - 删除客户信息
|
||||
- `delete_car_background()` - 删除客户车辆信息
|
||||
|
||||
### customer_tasks.py - 客户任务模块
|
||||
客户相关的后台任务:
|
||||
- `modify_customer_info_background()` - 修改客户信息
|
||||
|
||||
## 向后兼容
|
||||
|
||||
原有的 `app.back_ground_tasks` 模块仍然可用,它现在作为向后兼容的入口,实际功能已拆分到 `app.tasks` 模块中。
|
||||
|
||||
## 添加新功能模块
|
||||
|
||||
如需添加新的功能模块,请按以下步骤:
|
||||
|
||||
1. 在 `app/tasks/` 目录下创建新的模块文件,例如 `new_feature_tasks.py`
|
||||
2. 在新模块中实现相关功能函数
|
||||
3. 在 `app/tasks/__init__.py` 中导入并导出新函数
|
||||
4. 在 `app/back_ground_tasks.py` 中导入新函数以保持向后兼容
|
||||
|
||||
示例:
|
||||
|
||||
```python
|
||||
# app/tasks/new_feature_tasks.py
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow
|
||||
|
||||
def new_feature_background(data, cookies):
|
||||
# 实现新功能
|
||||
result = "执行结果"
|
||||
msg = update_jiandaoyun(data, result)
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
```
|
||||
|
||||
```python
|
||||
# app/tasks/__init__.py 中添加
|
||||
from app.tasks.new_feature_tasks import new_feature_background
|
||||
|
||||
__all__ = [
|
||||
# ... 其他函数
|
||||
'new_feature_background',
|
||||
]
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:使用新的模块结构(推荐)
|
||||
```python
|
||||
from app.tasks import create_brand_background
|
||||
from app.tasks.brand_tasks import create_brand_background # 也可以直接导入
|
||||
```
|
||||
|
||||
### 方式二:使用向后兼容的导入方式
|
||||
```python
|
||||
from app import back_ground_tasks
|
||||
back_ground_tasks.create_brand_background(...)
|
||||
```
|
||||
|
||||
两种方式都可以正常工作,代码执行逻辑完全一致。
|
||||
|
||||
+24
-2
@@ -1,6 +1,14 @@
|
||||
"""
|
||||
后台任务模块统一导出入口
|
||||
保持向后兼容,所有原有导入方式仍然有效
|
||||
|
||||
本模块统一导出所有后台任务函数,保持向后兼容。
|
||||
所有原有导入方式仍然有效。
|
||||
|
||||
导出的任务包括:
|
||||
- 通用功能: update_jiandaoyun, approve_workflow
|
||||
- 品牌任务: create_brand_background
|
||||
- 删除任务: delete_history_background, delete_customer_background, delete_car_background
|
||||
- 客户任务: modify_customer_info_background
|
||||
"""
|
||||
# 通用功能
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow
|
||||
@@ -18,6 +26,15 @@ from app.tasks.delete_tasks import (
|
||||
# 客户相关任务
|
||||
from app.tasks.customer_tasks import modify_customer_info_background
|
||||
|
||||
# BI相关任务
|
||||
from app.tasks.bi_tasks import bi_task_background
|
||||
|
||||
from app.tasks.material_tasks import ( \
|
||||
batch_modify_projects,
|
||||
batch_modify_materials,
|
||||
batch_disable_projects
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# 通用功能
|
||||
'update_jiandaoyun',
|
||||
@@ -30,5 +47,10 @@ __all__ = [
|
||||
'delete_car_background',
|
||||
# 客户任务
|
||||
'modify_customer_info_background',
|
||||
# BI任务
|
||||
'bi_task_background',
|
||||
# 项目材料任务
|
||||
'batch_disable_projects',
|
||||
'batch_modify_materials',
|
||||
'batch_modify_projects',
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
BI相关后台任务模块
|
||||
|
||||
本模块包含BI相关的后台任务,包括:
|
||||
- TODO BI数据处理
|
||||
|
||||
|
||||
这些任务在后台线程中执行,不会阻塞主请求。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import pandas as pd
|
||||
from typing import Dict, Any
|
||||
from tqdm import tqdm
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow
|
||||
|
||||
logger = logging.getLogger('app')
|
||||
|
||||
|
||||
def bi_task_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame = None, save_path: str = None):
|
||||
"""
|
||||
BI任务后台执行函数
|
||||
|
||||
在后台线程中执行BI相关任务,如数据处理、报表生成等。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息(如果需要)
|
||||
df: Excel 文件读取的内容,DataFrame 格式(如果需要)
|
||||
save_path: Excel 文件保存的地址,执行完成后会删除此文件(如果需要)
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
注意:
|
||||
- 这是一个示例函数,需要根据实际BI任务需求进行实现
|
||||
- 执行完成后会自动删除上传的文件(如果提供了save_path)
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
try:
|
||||
# TODO: 在这里实现具体的BI任务逻辑
|
||||
# 例如:数据处理、报表生成、数据同步等
|
||||
|
||||
# 示例:处理数据
|
||||
results = []
|
||||
if df is not None:
|
||||
df = df.where(pd.notnull(df), None)
|
||||
for index, row in tqdm(df.iterrows(), total=df.shape[0], desc="处理BI数据"):
|
||||
# TODO: 实现具体的数据处理逻辑
|
||||
# 例如:调用BI API、生成报表、数据转换等
|
||||
result_item = {
|
||||
'行号': index + 1,
|
||||
'状态': '处理成功'
|
||||
}
|
||||
results.append(result_item)
|
||||
else:
|
||||
# 如果没有DataFrame,执行其他BI任务
|
||||
# TODO: 实现其他BI任务逻辑
|
||||
results.append({'状态': 'BI任务执行成功'})
|
||||
|
||||
# 删除文件(如果提供了save_path)
|
||||
if save_path and os.path.exists(save_path):
|
||||
os.remove(save_path)
|
||||
logger.info(f'{save_path}已删除')
|
||||
|
||||
# 格式化结果
|
||||
results_str = f'{results}' if results else 'BI任务执行完成'
|
||||
logger.info(f"BI任务执行结果: {results_str}")
|
||||
|
||||
# 调用api回写改掉 执行明细与执行状态
|
||||
msg = update_jiandaoyun(data, results_str)
|
||||
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
logger.info('表单已自动提交至下一步')
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'BI任务执行失败: {str(e)}'
|
||||
logger.error(error_msg, exc_info=True)
|
||||
msg = update_jiandaoyun(data, error_msg)
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""
|
||||
品牌相关后台任务模块
|
||||
包含品牌批量创建等功能
|
||||
|
||||
本模块包含品牌相关的后台任务,包括:
|
||||
- 品牌批量创建
|
||||
|
||||
这些任务在后台线程中执行,不会阻塞主请求。
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
@@ -15,12 +19,24 @@ logger = logging.getLogger('app')
|
||||
|
||||
def create_brand_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str):
|
||||
"""
|
||||
品牌批量创建后台运行函数
|
||||
:param data: 包含表单id、数据id等的字典
|
||||
:param cookies: 用户登录f6系统的cookies信息
|
||||
:param df: 表格读取到的内容,DataFrame格式
|
||||
:param save_path: 文件保存的地址
|
||||
:return: None
|
||||
品牌批量创建后台任务
|
||||
|
||||
在后台线程中批量创建品牌,从 Excel 文件中读取品牌名称并创建。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
df: Excel 文件读取的内容,DataFrame 格式,第一列为品牌名称
|
||||
save_path: Excel 文件保存的地址,执行完成后会删除此文件
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
注意:
|
||||
- 无效的品牌名(None、空字符串)会被跳过
|
||||
- 执行完成后会自动删除上传的文件
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
df = df.where(pd.notnull(df), None)
|
||||
# 定义请求URL
|
||||
|
||||
+173
-14
@@ -1,10 +1,19 @@
|
||||
"""
|
||||
通用后台任务模块
|
||||
包含简道云表单更新和工作流审批等通用功能
|
||||
|
||||
本模块包含所有后台任务通用的功能,包括:
|
||||
- 简道云表单更新
|
||||
- 工作流审批
|
||||
- 获取门店ID
|
||||
- 获取会员卡列表
|
||||
|
||||
这些功能被多个后台任务模块复用。
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
import requests
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
from tqdm import tqdm
|
||||
from app.api import API
|
||||
|
||||
api_instance = API()
|
||||
@@ -14,9 +23,15 @@ logger = logging.getLogger('app')
|
||||
def update_jiandaoyun(data: Dict[str, Any], results: str):
|
||||
"""
|
||||
更新简道云表单
|
||||
:param data: 包含表单id、应用id、数据id的字典
|
||||
:param results: 执行结果信息
|
||||
:return: 更新结果字典
|
||||
|
||||
将后台任务的执行结果更新到简道云表单中。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
results: 执行结果信息,将写入到表单的执行明细字段
|
||||
|
||||
Returns:
|
||||
Dict: 更新结果字典,{'msg': True} 表示成功,{'msg': False} 表示失败
|
||||
"""
|
||||
# 定义简道云数据配置
|
||||
jiandaoyun_data = {
|
||||
@@ -44,42 +59,51 @@ def update_jiandaoyun(data: Dict[str, Any], results: str):
|
||||
def approve_workflow(data: Dict[str, Any]):
|
||||
"""
|
||||
获取简道云当前流程节点并直接提交
|
||||
:param data: 包含表单id、应用id、数据id的字典
|
||||
:return: None
|
||||
|
||||
获取简道云工作流的当前待处理任务,并自动提交到下一步。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
注意:
|
||||
如果未找到待处理任务,函数会记录错误并返回,不会抛出异常
|
||||
"""
|
||||
# 获取简道云当前流程列表
|
||||
json = api_instance.workflow_instance_get(data)
|
||||
|
||||
|
||||
# 检查返回数据是否有效
|
||||
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,
|
||||
@@ -92,3 +116,138 @@ def approve_workflow(data: Dict[str, Any]):
|
||||
except Exception as e:
|
||||
logger.error(f"简道云工作流任务提交失败: {e}")
|
||||
|
||||
|
||||
def execute_failure_handler(data: Dict[str, Any]):
|
||||
"""
|
||||
简道云失败流程通知
|
||||
|
||||
函数执行失败时调用,通过钉钉webhook通知到指定人员
|
||||
"""
|
||||
now = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
|
||||
pay_load = {
|
||||
"api_key": "6694d3c4fcb69ca9a111a6c4",
|
||||
"entry_id": "6938e011b360a1132522a62a",
|
||||
"data": {
|
||||
"_widget_1765335060501": {"value": now}, # 失败时间
|
||||
"_widget_1765335060502": {"value": data['failure_name']}, # 任务名称
|
||||
"_widget_1765335060503": {"value": data['failure_details']} # 失败明细
|
||||
}
|
||||
}
|
||||
|
||||
api_instance.data_batch_create(pay_load)
|
||||
|
||||
|
||||
def get_operate_org_id(cookies: Dict[str, str], data: Dict[str, str] = None) -> Optional[str]:
|
||||
"""
|
||||
获取操作门店ID
|
||||
|
||||
从F6系统获取第一个门店的组织ID,用于后续操作。
|
||||
|
||||
Args:
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
data:数据id等
|
||||
|
||||
Returns:
|
||||
Optional[str]: 门店ID,如果获取失败返回 None
|
||||
|
||||
注意:
|
||||
如果未获取到门店信息或门店ID为空,会记录错误日志并返回 None
|
||||
"""
|
||||
org_url = "https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=100&name="
|
||||
|
||||
try:
|
||||
org_res = requests.get(url=org_url, cookies=cookies)
|
||||
logger.info(org_res.json())
|
||||
org_data = org_res.json().get("data", {})
|
||||
org_list = org_data.get("list", [])
|
||||
|
||||
if not org_list or len(org_list) == 0:
|
||||
logger.error("未获取到门店信息")
|
||||
return None
|
||||
if data:
|
||||
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||
org_name = entry_data.get("data", {}).get("门店名称")
|
||||
operate_org_id = [
|
||||
item["orgId"]
|
||||
for item in org_list
|
||||
if item.get("abbrName") == org_name
|
||||
]
|
||||
else:
|
||||
operate_org_id = org_list[0].get("orgId")
|
||||
if not operate_org_id:
|
||||
logger.error("门店ID为空")
|
||||
return None
|
||||
|
||||
logger.info(f"获取门店ID成功: {operate_org_id}")
|
||||
return operate_org_id
|
||||
except Exception as e:
|
||||
logger.error(f"获取门店ID时发生错误: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_card_list(
|
||||
cookies: Dict[str, str],
|
||||
operate_org_id: str,
|
||||
extract_func: Callable[[Dict], Optional[str]] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
获取会员卡列表
|
||||
|
||||
从F6系统获取指定门店的会员卡列表,支持自定义提取逻辑。
|
||||
|
||||
Args:
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
operate_org_id: 门店ID
|
||||
extract_func: 自定义提取函数,用于从会员卡数据中提取ID
|
||||
如果不提供,默认提取 idCustomer 字段
|
||||
|
||||
Returns:
|
||||
List[str]: 会员卡ID列表
|
||||
|
||||
注意:
|
||||
- 默认每页100条数据,会自动分页获取所有数据
|
||||
- 每页请求间隔0.2秒,避免请求过快
|
||||
"""
|
||||
card_list = []
|
||||
|
||||
try:
|
||||
# 获取第一页,确定总页数
|
||||
card_url = f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}&pageSize=100&pageNo=1"
|
||||
card_res = requests.get(url=card_url, cookies=cookies)
|
||||
total_card = int(card_res.json().get("data", {}).get("total", 0))
|
||||
|
||||
if total_card == 0:
|
||||
logger.info("未找到会员卡数据")
|
||||
return card_list
|
||||
|
||||
total_page = total_card // 100 + (total_card % 100 > 0)
|
||||
logger.info(f"会员卡总数: {total_card}, 总页数: {total_page}")
|
||||
|
||||
# 定义默认提取函数(提取客户ID)
|
||||
if extract_func is None:
|
||||
def default_extract(card_item: Dict) -> Optional[str]:
|
||||
return card_item.get("idCustomer")
|
||||
|
||||
extract_func = default_extract
|
||||
|
||||
# 分页获取所有会员卡数据
|
||||
for page in tqdm(range(1, total_page + 1), desc="查询会员卡"):
|
||||
card_url = (f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}"
|
||||
f"&pageSize=100&pageNo={page}")
|
||||
card_res = requests.get(url=card_url, cookies=cookies)
|
||||
card_data_list = card_res.json().get("data", {}).get("data", [])
|
||||
|
||||
# 使用提取函数提取ID
|
||||
for card_item in card_data_list:
|
||||
extracted_id = extract_func(card_item)
|
||||
if extracted_id is not None:
|
||||
card_list.append(extracted_id)
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
logger.info(f"获取会员卡列表成功,共 {len(card_list)} 条")
|
||||
return card_list
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取会员卡列表时发生错误: {e}")
|
||||
return card_list
|
||||
|
||||
+178
-227
@@ -1,12 +1,16 @@
|
||||
"""
|
||||
客户相关后台任务模块
|
||||
包含修改客户信息等功能
|
||||
|
||||
本模块包含客户相关的后台任务,包括:
|
||||
- 客户信息批量修改
|
||||
|
||||
这些任务在后台线程中执行,不会阻塞主请求。
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
import pandas as pd
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow
|
||||
|
||||
@@ -15,247 +19,194 @@ logger = logging.getLogger('app')
|
||||
|
||||
def modify_customer_info_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str):
|
||||
"""
|
||||
修改客户信息后台任务。
|
||||
修改客户信息后台任务
|
||||
|
||||
此函数用于后台任务,用于修改会员信息。
|
||||
在后台线程中批量修改客户信息,从 Excel 文件中读取客户手机号和修改信息。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data (Dict[str, Any]): 前端请求发送过来的数据,包含文件信息和其他必要参数。
|
||||
cookies (Dict[str, str]): 登录用户的Cookies。
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
df: Excel 文件读取的内容,DataFrame 格式,第一列为客户手机号
|
||||
save_path: Excel 文件保存的地址,执行完成后会删除此文件
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
注意:
|
||||
- 根据客户手机号匹配客户信息
|
||||
- 执行完成后会自动删除上传的文件
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
df = df.where(pd.notnull(df), None)
|
||||
params = {
|
||||
'pageSize': 100,
|
||||
'pageNo': '1',
|
||||
}
|
||||
try:
|
||||
# 替换 NaN 为 None,便于后续判断
|
||||
df = df.where(pd.notnull(df), None)
|
||||
|
||||
res = requests.get(
|
||||
'https://yunxiu.f6car.cn/member/customer/listForPermission',
|
||||
params=params,
|
||||
cookies=cookies,
|
||||
)
|
||||
logger.info("开始获取当前客户下所有客户信息")
|
||||
|
||||
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
|
||||
# 分页获取全部客户
|
||||
params = {'pageSize': 100, 'pageNo': '1'}
|
||||
res = requests.get(
|
||||
'https://yunxiu.f6car.cn/member/customer/listForPermission',
|
||||
params=params,
|
||||
cookies=cookies,
|
||||
timeout=10
|
||||
)
|
||||
res.raise_for_status()
|
||||
total = int(res.json().get("data", {}).get("total", 0))
|
||||
total_pages = (total // params["pageSize"]) + (1 if total % params["pageSize"] > 0 else 0)
|
||||
logger.info(f"总计 {total_pages} 页,共 {total} 个客户")
|
||||
|
||||
cell_phone = safe_get(customer_info, 'cellPhone')
|
||||
all_customers = []
|
||||
for page in range(1, total_pages + 1):
|
||||
logger.debug(f"正在请求第 {page} 页...")
|
||||
params["pageNo"] = page
|
||||
retry_count = 0
|
||||
max_retries = 5
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
response = requests.get(
|
||||
'https://yunxiu.f6car.cn/member/customer/listForPermission',
|
||||
params=params,
|
||||
cookies=cookies,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
page_data = response.json().get("data", {}).get("data", [])
|
||||
all_customers.extend(page_data)
|
||||
break
|
||||
except Exception as e:
|
||||
retry_count += 1
|
||||
logger.warning(f"请求第 {page} 页失败(第 {retry_count} 次重试): {e}")
|
||||
time.sleep(3)
|
||||
else:
|
||||
logger.error(f"第 {page} 页请求失败超过最大重试次数,跳过")
|
||||
|
||||
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'),
|
||||
# 获取专属运营顾问列表
|
||||
json_data = {
|
||||
'includeStopedEmployee': False,
|
||||
'pageSize': 1000,
|
||||
'filterNullUser': False,
|
||||
'keyword': '',
|
||||
'idOwnOrgList': [],
|
||||
}
|
||||
staff_resp = requests.post(
|
||||
'https://yunxiu.f6car.cn/hive/employee/searchStaffInGroup',
|
||||
cookies=cookies,
|
||||
json=json_data,
|
||||
timeout=10
|
||||
)
|
||||
staff_resp.raise_for_status()
|
||||
staff_list = staff_resp.json().get("data", {}).get("list", [])
|
||||
name_to_userid = {
|
||||
emp['name']: emp['userId']
|
||||
for emp in staff_list
|
||||
if emp.get('userId') is not None
|
||||
}
|
||||
|
||||
return request_data
|
||||
# 在 df 中添加 userId 列
|
||||
df['userId'] = df['专属运营顾问'].map(name_to_userid)
|
||||
|
||||
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("修改完成")
|
||||
# 字段映射:Excel 列名 -> F6 字段名
|
||||
FIELD_MAPPING = {
|
||||
'客户姓名': 'name',
|
||||
'客户类型': 'customerType',
|
||||
'客户来源': 'customerSourceName',
|
||||
'单位名称': 'companyName',
|
||||
'客户备注': 'customerMemo',
|
||||
'专属运营顾问': 'exclusiveConsultantName', # userId 单独处理
|
||||
}
|
||||
|
||||
time.sleep(1)
|
||||
def convert_to_request_data(original_data: dict, df_row: pd.Series) -> dict:
|
||||
"""以原始客户数据为基础,仅覆盖 Excel 中非空字段"""
|
||||
customer_info = original_data.get("data", {}).get("customerInfo", {}) or {}
|
||||
request_data = dict(customer_info) # 浅拷贝,足够用
|
||||
|
||||
msg = update_jiandaoyun(data, f'修改完成')
|
||||
# 覆盖指定字段
|
||||
for excel_col, f6_field in FIELD_MAPPING.items():
|
||||
value = df_row.get(excel_col)
|
||||
if pd.notna(value):
|
||||
request_data[f6_field] = str(value).strip() if isinstance(value, str) else value
|
||||
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
print('表单已自动提交至下一步')
|
||||
# 处理专属顾问 ID
|
||||
if pd.notna(df_row.get('userId')):
|
||||
request_data['exclusiveConsultantId'] = df_row['userId']
|
||||
|
||||
# 确保必要字段
|
||||
if 'idCustomer' in customer_info:
|
||||
request_data['pkId'] = customer_info['idCustomer']
|
||||
request_data['idCustomer'] = customer_info['idCustomer']
|
||||
|
||||
return request_data
|
||||
|
||||
# 执行批量更新
|
||||
updated_count = 0
|
||||
for customer in all_customers:
|
||||
phone = customer.get("cellPhone")
|
||||
if not phone:
|
||||
continue
|
||||
|
||||
matched_rows = df[df['客户手机号'] == phone]
|
||||
if matched_rows.empty:
|
||||
continue
|
||||
|
||||
df_row = matched_rows.iloc[0]
|
||||
cus_id = customer.get("idCustomer")
|
||||
if not cus_id:
|
||||
logger.warning(f"客户手机号 {phone} 缺少 idCustomer,跳过")
|
||||
continue
|
||||
|
||||
try:
|
||||
# 获取完整客户信息
|
||||
detail_resp = requests.get(
|
||||
f'https://yunxiu.f6car.cn/member/customer/{cus_id}',
|
||||
cookies=cookies,
|
||||
timeout=10
|
||||
)
|
||||
detail_resp.raise_for_status()
|
||||
original_data = detail_resp.json()
|
||||
|
||||
# 构造更新数据
|
||||
final_json_data = convert_to_request_data(original_data, df_row)
|
||||
|
||||
# 发送更新
|
||||
update_resp = requests.post(
|
||||
'https://yunxiu.f6car.cn/member/customer/modifyCustomer',
|
||||
cookies=cookies,
|
||||
json=final_json_data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if update_resp.status_code == 200 and update_resp.json().get('success'):
|
||||
logger.info(f"✅ 客户 {phone} 更新成功")
|
||||
updated_count += 1
|
||||
else:
|
||||
logger.error(f"❌ 客户 {phone} 更新失败: {update_resp.text}")
|
||||
|
||||
time.sleep(1) # 避免触发限流
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"处理客户 {phone} 时发生异常: {e}")
|
||||
|
||||
# 更新简道云状态
|
||||
msg = update_jiandaoyun(data, f'批量修改完成,共更新 {updated_count} 个客户')
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
logger.info('✅ 表单已自动提交至下一步')
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("modify_customer_info_background 执行出错:")
|
||||
# 即使出错也尝试通知简道云
|
||||
try:
|
||||
update_jiandaoyun(data, f'执行失败: {str(e)[:200]}')
|
||||
except:
|
||||
pass
|
||||
|
||||
finally:
|
||||
# 清理临时文件
|
||||
try:
|
||||
if os.path.exists(save_path):
|
||||
os.remove(save_path)
|
||||
logger.info(f"临时文件已删除: {save_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"删除临时文件失败: {e}")
|
||||
+253
-194
@@ -1,217 +1,277 @@
|
||||
"""
|
||||
删除相关后台任务模块
|
||||
包含删除历史维修记录、删除客户信息、删除客户车辆信息等功能
|
||||
|
||||
本模块包含删除相关的后台任务,包括:
|
||||
- 删除历史维修记录
|
||||
- 删除客户信息
|
||||
- 删除客户车辆信息
|
||||
|
||||
这些任务在后台线程中执行,不会阻塞主请求。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
"""
|
||||
import logging
|
||||
import traceback
|
||||
import requests
|
||||
import time
|
||||
from typing import Dict, Any, List
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow, get_operate_org_id, get_card_list, \
|
||||
execute_failure_handler
|
||||
|
||||
logger = logging.getLogger('app')
|
||||
|
||||
|
||||
def delete_history_background(data: Dict[str, Any], cookies: Dict[str, str], org_id: str, org_name: str):
|
||||
"""
|
||||
删除历史维修数据后台运行函数
|
||||
:param data: 包含表单id、数据id等的字典
|
||||
:param cookies: 用户登录F6系统的cookies信息
|
||||
:param org_id: 需要删除历史维修记录的门店id
|
||||
:param org_name: 需要删除历史维修记录的门店名称
|
||||
:return: None
|
||||
删除历史维修记录后台任务
|
||||
|
||||
在后台线程中删除指定门店的历史维修记录。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
org_id: 需要删除历史维修记录的门店ID
|
||||
org_name: 需要删除历史维修记录的门店名称
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
url = f'https://yunxiu.f6car.cn/maintain-dump/maintainHistory/?orgid={org_id}' # 删除url
|
||||
res = requests.delete(url=url, cookies=cookies)
|
||||
res_data = res.json()
|
||||
try:
|
||||
|
||||
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')}")
|
||||
url = f'https://yunxiu.f6car.cn/maintain-dump/maintainHistory/?orgid={org_id}' # 删除url
|
||||
res = requests.delete(url=url, cookies=cookies)
|
||||
res_data = res.json()
|
||||
|
||||
# 调用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)
|
||||
if res.status_code == 200 and res_data.get('code') == 200:
|
||||
results = f'{org_name} 历史维修记录已删除'
|
||||
print(results)
|
||||
logger.info(f"删除 {org_name} 历史维修记录成功")
|
||||
else:
|
||||
time.sleep(3)
|
||||
results = f'删除 {org_name} 历史维修记录失败: {res_data.get("message")}'
|
||||
data["failure_name"] = "删除历史维修记录后台任务"
|
||||
data["failure_details"] = results
|
||||
execute_failure_handler(data)
|
||||
logger.error(f"删除 {org_name} 历史维修记录失败: {res_data.get('message')}")
|
||||
|
||||
logger.info(f"客户删除结果: 成功次数={success}, 失败次数={fail}")
|
||||
# 调用api回写改掉 执行明细与执行状态
|
||||
time.sleep(1)
|
||||
msg = update_jiandaoyun(data, f'{results}')
|
||||
|
||||
msg = update_jiandaoyun(data, f'成功次数{success},失败次数{fail}')
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
print('表单已自动提交至下一步')
|
||||
except Exception as e:
|
||||
logger.error(f"删除 {org_name} 历史维修记录失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
data["failure_name"] = "删除历史维修记录后台任务"
|
||||
data["failure_details"] = traceback.format_exc()
|
||||
execute_failure_handler(data)
|
||||
|
||||
|
||||
def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], total: int):
|
||||
"""
|
||||
删除客户信息后台任务
|
||||
|
||||
在后台线程中批量删除客户信息。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
json_data: 获取到的客户信息列表,包含要删除的客户信息
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
注意:
|
||||
- 8-20点之间每3.5秒删除一条数据,其余时间每1.5秒删除一条数据
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
try:
|
||||
logger.info("开始删除客户信息")
|
||||
success = 0
|
||||
fail = 0
|
||||
|
||||
json_data = []
|
||||
total_page = total // 100 + (total % 100 > 0)
|
||||
logger.info("正在加载客户信息")
|
||||
for page in tqdm(range(1, total_page + 1)):
|
||||
url = f"https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=100&pageNo={page}"
|
||||
res = requests.get(url, cookies=cookies)
|
||||
json_data.extend(res.json().get('data', {}).get('data', []))
|
||||
|
||||
# 获取门店ID
|
||||
operate_org_id = get_operate_org_id(cookies)
|
||||
if not operate_org_id:
|
||||
msg = update_jiandaoyun(data, '删除失败: 未获取到门店信息')
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
return
|
||||
|
||||
|
||||
# 获取会员卡列表(提取客户ID)
|
||||
card_list_customers = get_card_list(cookies, operate_org_id)
|
||||
|
||||
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, timeout=10) # 客户信息删除
|
||||
|
||||
# 检查HTTP状态码
|
||||
if res.status_code != 200:
|
||||
fail += 1
|
||||
error_msg = f"HTTP状态码错误: {res.status_code}"
|
||||
logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, {error_msg}")
|
||||
print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}")
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
|
||||
# 解析响应数据
|
||||
try:
|
||||
res_data = res.json()
|
||||
except ValueError as json_error:
|
||||
fail += 1
|
||||
error_msg = f"响应不是有效的JSON格式: {json_error}, 响应内容: {res.text[:200]}"
|
||||
logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, {error_msg}")
|
||||
print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}")
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
|
||||
# 检查多种可能的成功标识
|
||||
# 有些API返回 success 字段,有些返回 code=200,有些返回 data 字段
|
||||
is_success = (
|
||||
res_data.get('success') is True or
|
||||
res_data.get('code') == 200 or
|
||||
(res_data.get('code') is None and res_data.get('data') is not None)
|
||||
)
|
||||
|
||||
if is_success:
|
||||
success += 1
|
||||
logger.info(f"客户删除成功: ID={id_customer}, 手机号={phone}")
|
||||
else:
|
||||
fail += 1
|
||||
error_msg = res_data.get('message') or res_data.get('msg') or '未知错误'
|
||||
error_detail = f"错误信息: {error_msg}, 完整响应: {res_data}"
|
||||
logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, {error_detail}")
|
||||
print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}")
|
||||
time.sleep(0.2)
|
||||
except requests.exceptions.RequestException as e:
|
||||
fail += 1
|
||||
error_msg = f"网络请求异常: {str(e)}"
|
||||
print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}")
|
||||
logger.error(f"删除客户时发生网络错误: ID={id_customer}, 手机号={phone}, {error_msg}")
|
||||
except Exception as e:
|
||||
fail += 1
|
||||
error_msg = f"未知错误: {str(e)}"
|
||||
print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}")
|
||||
logger.error(f"删除客户时发生错误: ID={id_customer}, 手机号={phone}, {error_msg}", exc_info=True)
|
||||
|
||||
now = datetime.now()
|
||||
if 8 <= now.hour <= 20:
|
||||
time.sleep(3.5)
|
||||
else:
|
||||
time.sleep(1.5)
|
||||
|
||||
logger.info(f"客户删除结果: 成功次数={success}, 失败次数={fail}")
|
||||
|
||||
msg = update_jiandaoyun(data, f'成功次数{success},失败次数{fail}')
|
||||
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
print('表单已自动提交至下一步')
|
||||
except Exception as e:
|
||||
logger.error(f"删除客户信息时发生错误: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
data["failure_name"] = "删除客户信息后台任务"
|
||||
data["failure_details"] = traceback.format_exc()
|
||||
execute_failure_handler(data)
|
||||
|
||||
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
|
||||
删除客户车辆信息后台任务
|
||||
|
||||
在后台线程中批量删除客户车辆信息。
|
||||
会检查车辆是否有会员卡或最近消费记录,有则跳过删除。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
url: 获取车辆列表的 API URL
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
header: HTTP 请求头字典,应包含账号登录的请求头
|
||||
all_page: 总页数(字符串或整数),用于分页获取车辆列表
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
注意:
|
||||
- 8-20点之间每3.5秒删除一条数据,其余时间每1.5秒删除一条数据
|
||||
- 有会员卡或最近消费记录的车辆会被跳过
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
print(cookies)
|
||||
success = 0
|
||||
fail = 0
|
||||
try:
|
||||
# 确保 all_page 是一个整数
|
||||
all_page = int(all_page)
|
||||
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}")
|
||||
# 获取门店ID
|
||||
operate_org_id = get_operate_org_id(cookies)
|
||||
if not operate_org_id:
|
||||
msg = update_jiandaoyun(data, '删除失败: 未获取到门店信息')
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
return
|
||||
|
||||
print(operate_org_id)
|
||||
# 获取会员卡列表(提取车辆ID)
|
||||
# 注意:需要获取所有车辆的ID,所以不能直接使用 get_card_list
|
||||
# 需要自定义提取逻辑,返回所有车辆的ID列表
|
||||
card_list_cars = []
|
||||
|
||||
# 获取第一页,确定总页数
|
||||
card_url = f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}&pageSize=100&pageNo=1"
|
||||
card_res = requests.get(url=card_url, cookies=cookies)
|
||||
card_cars_list = card_res.json().get("data").get("data")
|
||||
total_card = int(card_res.json().get("data", {}).get("total", 0))
|
||||
|
||||
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)
|
||||
if total_card > 0:
|
||||
total_page = total_card // 100 + (total_card % 100 > 0)
|
||||
for page in tqdm(range(1, total_page + 1), desc="查询会员卡"):
|
||||
card_url = (f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}"
|
||||
f"&pageSize=100&pageNo={page}")
|
||||
card_res = requests.get(url=card_url, cookies=cookies)
|
||||
card_cars_list = card_res.json().get("data", {}).get("data", [])
|
||||
|
||||
for card_car in card_cars_list:
|
||||
if card_car.get("cars") is None:
|
||||
continue
|
||||
for car in card_car.get("cars", []):
|
||||
car_id = car.get("idCar")
|
||||
if car_id:
|
||||
card_list_cars.append(car_id)
|
||||
time.sleep(0.2)
|
||||
except Exception as e:
|
||||
logger.error(f"获取会员卡列表时发生错误: {e}")
|
||||
|
||||
itemlist = []
|
||||
# 使用 range() 创建一个可迭代的对象
|
||||
@@ -289,16 +349,15 @@ def delete_car_background(data: Dict[str, Any], url: str, cookies: Dict[str, str
|
||||
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() # 打印堆栈跟踪信息
|
||||
|
||||
msg = update_jiandaoyun(data, f'成功次数{success},失败次数{fail}')
|
||||
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
print('表单已自动提交至下一步')
|
||||
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('表单已自动提交至下一步')
|
||||
|
||||
logger.error(f"删除客户车辆信息时发生错误: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
data["failure_name"] = "删除客户车辆信息后台任务"
|
||||
data["failure_details"] = traceback.format_exc()
|
||||
execute_failure_handler(data)
|
||||
|
||||
@@ -0,0 +1,892 @@
|
||||
"""
|
||||
项目材料相关后台任务模块
|
||||
|
||||
本模块包含项目材料相关的后台任务,包括:
|
||||
- 项目信息批量停用
|
||||
- 材料信息批量修改
|
||||
|
||||
这些任务在后台线程中执行,不会阻塞主请求。
|
||||
"""
|
||||
import logging
|
||||
import traceback
|
||||
import requests
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
from tqdm import tqdm
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow, get_operate_org_id
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
logger = logging.getLogger('app')
|
||||
|
||||
|
||||
def batch_disable_projects(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str,
|
||||
option) -> None:
|
||||
"""
|
||||
项目批量启停后台任务
|
||||
|
||||
在后台线程中批量停用项目,从 Excel 文件中读取项目编码。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
df: Excel 文件读取的内容,DataFrame 格式,第一列为项目编码
|
||||
save_path: Excel 文件保存的地址,执行完成后会删除此文件
|
||||
option: 批量启用、批量停用
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
注意:
|
||||
- 无效的项目(None、空字符串)会被跳过
|
||||
- 执行完成后会自动删除上传的文件
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
logger.info(f"开始执行项目批量启停任务,操作类型: {option}, 文件路径: {save_path}, 数据行数: {len(df)}")
|
||||
if option == "批量启用":
|
||||
type_ = 0 # 1 停用,0启用
|
||||
ob_type = 1
|
||||
else:
|
||||
type_ = 1
|
||||
ob_type = 0
|
||||
logger.info(f"操作类型设置完成: type_={type_}, ob_type={ob_type}")
|
||||
df = df.where(pd.notnull(df), None)
|
||||
|
||||
# 获取门店id
|
||||
logger.info("正在获取门店ID...")
|
||||
try:
|
||||
org_id = get_operate_org_id(cookies)
|
||||
logger.info(f"门店ID获取成功: {org_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"获取门店ID失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# 获取项目信息
|
||||
logger.info("开始获取项目列表...")
|
||||
json_data = {
|
||||
'param': '',
|
||||
'name': '',
|
||||
'customCode': '',
|
||||
'currentPage': 1,
|
||||
'pageSize': 100,
|
||||
'isDel': ob_type,
|
||||
'customInvoiceCategory': 0,
|
||||
'idOwnOrg': org_id,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://ids-goods.f6car.cn/f6-ids-goods/service/getServiceList',
|
||||
cookies=cookies,
|
||||
json=json_data,
|
||||
)
|
||||
time.sleep(1)
|
||||
response.raise_for_status()
|
||||
all_project_list = []
|
||||
total_pages = response.json().get("data", {}).get("totalPages", 0)
|
||||
logger.info(f"获取项目列表响应: {response.json()}")
|
||||
try:
|
||||
total_pages = int(total_pages)
|
||||
except (ValueError, TypeError):
|
||||
logger.error(f"无法解析总页数: {total_pages}, 类型: {type(total_pages)}")
|
||||
total_pages = 0
|
||||
logger.info(f"项目列表总页数: {total_pages}")
|
||||
|
||||
for page in tqdm(range(1, total_pages + 1)):
|
||||
json_data['currentPage'] = page
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://ids-goods.f6car.cn/f6-ids-goods/service/getServiceList',
|
||||
cookies=cookies,
|
||||
json=json_data,
|
||||
)
|
||||
time.sleep(1)
|
||||
response.raise_for_status()
|
||||
project_list = response.json().get("data", {}).get("records", [])
|
||||
all_project_list.extend(project_list)
|
||||
logger.debug(f"第{page}页获取到{len(project_list)}条项目")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取第{page}页项目列表失败: {str(e)}, 响应状态码: {getattr(e.response, 'status_code', 'N/A')}")
|
||||
raise
|
||||
logger.info(f"项目列表获取完成,总计: {len(all_project_list)}条项目")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取项目列表失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
raise
|
||||
# 遍历获取到的项目信息停用文件中的项目
|
||||
code_list = df.iloc[:, 0].dropna().astype(str).tolist()
|
||||
logger.info(f"Excel文件中待处理的项目编码数量: {len(code_list)}")
|
||||
results = []
|
||||
consecutive_failures = 0 # 连续失败计数器
|
||||
MAX_CONSECUTIVE_FAILURES = 100 # 最大连续失败次数
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
|
||||
logger.info("开始处理项目启停操作...")
|
||||
for item in tqdm(all_project_list):
|
||||
custom_code = item.get("customCode")
|
||||
if not code_list or not custom_code or str(custom_code) not in code_list:
|
||||
continue
|
||||
|
||||
logger.debug(f"正在处理项目编码: {custom_code}")
|
||||
info_id = item.get("infoId")
|
||||
pk_id = item.get("pkId")
|
||||
if not info_id or not pk_id:
|
||||
logger.warning(f"项目编码 {custom_code} 缺少必要字段: infoId={info_id}, pkId={pk_id}")
|
||||
results.append({'项目编码': custom_code, '状态': '缺少必要字段(infoId或pkId)'})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'项目编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
continue
|
||||
|
||||
json_data = {
|
||||
"orgIdList": [
|
||||
org_id,
|
||||
],
|
||||
"isDel": type_, # 1 停用 0启用
|
||||
"infoId": info_id,
|
||||
"pkId": pk_id,
|
||||
"type": 1,
|
||||
"idOwnOrg": org_id
|
||||
}
|
||||
try:
|
||||
logger.debug(f"发送启停请求,项目编码: {custom_code}, 操作类型: {type_}")
|
||||
response = requests.post(
|
||||
'https://ids-goods.f6car.cn/f6-ids-goods/service/editAttributeByType',
|
||||
cookies=cookies,
|
||||
json=json_data,
|
||||
)
|
||||
time.sleep(2)
|
||||
response.raise_for_status() # 抛出HTTP错误
|
||||
# 检查业务响应码
|
||||
resp_data = response.json()
|
||||
if resp_data.get("code") == 200:
|
||||
results.append({'项目编码': custom_code, '状态': '停用/启用成功'})
|
||||
success_count += 1
|
||||
consecutive_failures = 0 # 成功时重置计数器
|
||||
logger.info(f"项目编码 {custom_code} 启停操作成功")
|
||||
else:
|
||||
msg = resp_data.get("message", "未知错误")
|
||||
results.append({'项目编码': custom_code, '状态': f'停用/启用失败: {msg}'})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"项目编码 {custom_code} 启停操作失败: {msg}, 响应数据: {resp_data}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = str(e)
|
||||
results.append({'项目编码': custom_code, '状态': f'停用/启用失败: {error_msg}'})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"项目编码 {custom_code} 启停操作请求异常: {error_msg}, 堆栈信息: {traceback.format_exc()}")
|
||||
|
||||
# 检查连续失败次数
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'项目编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
|
||||
logger.info(f"项目启停处理完成,成功: {success_count}条, 失败: {failure_count}条, 总计: {len(results)}条")
|
||||
|
||||
print({'msg': '已执行', 'msg_details': f'{results}'})
|
||||
logger.info(f"停用/启用结果: {results}")
|
||||
|
||||
# 删除文件
|
||||
logger.info(f"准备删除文件: {save_path}")
|
||||
try:
|
||||
os.remove(save_path)
|
||||
logger.info(f"文件删除成功: {save_path}")
|
||||
print(f'{save_path}已删除')
|
||||
except Exception as e:
|
||||
logger.error(f"删除文件失败: {save_path}, 错误信息: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
|
||||
# 调用api回写改掉 执行明细与执行状态
|
||||
logger.info("开始回写简道云表单...")
|
||||
try:
|
||||
msg = update_jiandaoyun(data, f'{results}')
|
||||
logger.info(f"简道云表单回写结果: {msg}")
|
||||
if msg.get('msg'):
|
||||
logger.info("开始自动提交工作流...")
|
||||
approve_workflow(data)
|
||||
logger.info("工作流提交成功")
|
||||
print('表单已自动提交至下一步')
|
||||
else:
|
||||
logger.warning(f"简道云表单回写失败: {msg}")
|
||||
except Exception as e:
|
||||
logger.error(f"回写简道云表单失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
|
||||
logger.info(f"项目批量启停任务执行完成,操作类型: {option}")
|
||||
|
||||
|
||||
def batch_modify_materials(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str) -> None:
|
||||
"""
|
||||
材料批量修改后台任务
|
||||
|
||||
在后台线程中批量修改材料,从 Excel 文件中读取材料编码。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
df: Excel 文件读取的内容,DataFrame 格式,列顺序为:
|
||||
[原材料编码, 新材料编码, 品牌, 名称, 规格]
|
||||
save_path: Excel 文件保存的地址,执行完成后会删除此文件
|
||||
|
||||
注意:
|
||||
- 无效的材料编码(None、空字符串)会被跳过
|
||||
- Excel 中某字段为空(NaN 或空字符串)时,保留原材料中的对应字段
|
||||
- 执行完成后会自动删除上传的文件
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
logger.info(f"开始执行材料批量修改任务,文件: {save_path},行数: {len(df)}")
|
||||
|
||||
def safe_str(val):
|
||||
"""将值转为字符串,NaN 返回空字符串"""
|
||||
if pd.isna(val):
|
||||
return ""
|
||||
return str(val).strip()
|
||||
|
||||
def should_update(new_val):
|
||||
"""判断是否应该用 new_val 更新:非 NaN 且非空字符串"""
|
||||
return not (pd.isna(new_val) or (isinstance(new_val, str) and new_val.strip() == ""))
|
||||
|
||||
# 获取门店id
|
||||
logger.info("正在获取门店ID...")
|
||||
try:
|
||||
org_id = get_operate_org_id(cookies)
|
||||
logger.info(f"门店ID获取成功: {org_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"获取门店ID失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# 第一步:获取所有材料列表(分页)
|
||||
json_data = {
|
||||
'keyWord': '',
|
||||
'idOwnOrg': org_id,
|
||||
'currentPage': 1,
|
||||
'pageSize': 100,
|
||||
'name': '',
|
||||
'brand': '',
|
||||
'supplierCode': '',
|
||||
'customCode': '',
|
||||
'categoryName': '',
|
||||
'categoryId': '',
|
||||
'labelId': '',
|
||||
'labelName': '',
|
||||
'spec': '',
|
||||
'applyModel': '',
|
||||
'sellPurchaseStatuses': [2, 3, 4, 5],
|
||||
'customInvoiceCategory': 0,
|
||||
'getThirdPlatformCode': 0,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://ids-goods.f6car.com/f6-ids-goods/part/getExactPartStockInfo',
|
||||
cookies=cookies,
|
||||
json=json_data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
total_pages = response.json().get("data", {}).get("totalPages", 0)
|
||||
logger.info(f"材料列表页数: {total_pages}")
|
||||
|
||||
all_materials_list = []
|
||||
total_pages = int(total_pages)
|
||||
for page in tqdm(range(1, total_pages + 1), desc="获取材料列表"):
|
||||
json_data['currentPage'] = page
|
||||
try:
|
||||
resp = requests.post(
|
||||
'https://ids-goods.f6car.com/f6-ids-goods/part/getExactPartStockInfo',
|
||||
cookies=cookies,
|
||||
json=json_data,
|
||||
)
|
||||
time.sleep(1)
|
||||
resp.raise_for_status()
|
||||
records = resp.json().get("data", {}).get("records", [])
|
||||
all_materials_list.extend(records)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取第{page}页材料列表失败: {str(e)}, 响应状态码: {getattr(e.response, 'status_code', 'N/A')}")
|
||||
raise
|
||||
logger.info(f"材料列表获取完成,总计: {len(all_materials_list)}条")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取材料列表失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# 第二步:构建 update_map(只存原始值,不预处理)
|
||||
update_map = {}
|
||||
skipped_count = 0
|
||||
for _, row in df.iterrows():
|
||||
orig_code = row.iloc[0] # 原材料编码
|
||||
if pd.isna(orig_code) or str(orig_code).strip() == "":
|
||||
skipped_count += 1
|
||||
continue
|
||||
orig_code = str(orig_code).strip()
|
||||
update_map[orig_code] = {
|
||||
"new_customCode": row.iloc[1],
|
||||
"new_brand": row.iloc[2],
|
||||
"new_name": row.iloc[3],
|
||||
"new_spec": row.iloc[4],
|
||||
}
|
||||
logger.info(f"待更新材料数: {len(update_map)},跳过空编码行: {skipped_count}")
|
||||
|
||||
# 第三步:遍历材料,按需更新
|
||||
results = []
|
||||
consecutive_failures = 0 # 连续失败计数器
|
||||
MAX_CONSECUTIVE_FAILURES = 100 # 最大连续失败次数
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
skip_count = 0
|
||||
|
||||
for item in tqdm(all_materials_list, desc="处理材料更新"):
|
||||
custom_code = item.get("customCode")
|
||||
if not custom_code or str(custom_code) not in update_map:
|
||||
continue
|
||||
|
||||
part_id = item.get("partId")
|
||||
if not part_id:
|
||||
error_msg = '缺少 partId'
|
||||
results.append({'材料编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.warning(f"材料编码 {custom_code} 跳过/失败: {error_msg}")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'材料编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
continue
|
||||
|
||||
try:
|
||||
# 获取材料明细
|
||||
params = {
|
||||
'partId': part_id,
|
||||
'customInvoiceCategory': '0',
|
||||
'getThirdPlatformCode': '0',
|
||||
}
|
||||
materials_response = requests.get(
|
||||
'https://ids-goods.f6car.com/f6-ids-goods/part/getPartInfo',
|
||||
params=params,
|
||||
cookies=cookies,
|
||||
)
|
||||
time.sleep(1)
|
||||
if materials_response.status_code != 200:
|
||||
error_msg = f'获取明细失败: {materials_response.status_code}'
|
||||
results.append({'材料编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"材料编码 {custom_code} {error_msg}, 响应内容: {materials_response.text[:200]}")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'材料编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
continue
|
||||
|
||||
detail = materials_response.json().get("data")
|
||||
if not detail:
|
||||
error_msg = '明细为空'
|
||||
results.append({'材料编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"材料编码 {custom_code} {error_msg}, 响应JSON: {materials_response.json()}")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'材料编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
continue
|
||||
|
||||
updates = update_map[str(custom_code)]
|
||||
|
||||
# 安全更新字段:仅当 Excel 提供有效值时才覆盖
|
||||
if should_update(updates["new_customCode"]):
|
||||
detail["customCode"] = safe_str(updates["new_customCode"])
|
||||
if should_update(updates["new_brand"]):
|
||||
detail["brand"] = safe_str(updates["new_brand"])
|
||||
if should_update(updates["new_name"]):
|
||||
name_val = safe_str(updates["new_name"])
|
||||
detail["name"] = name_val
|
||||
detail["showName"] = name_val # 同步 showName
|
||||
if should_update(updates["new_spec"]):
|
||||
detail["spec"] = safe_str(updates["new_spec"])
|
||||
|
||||
# 修复价格格式和数组(必须!)
|
||||
for f in ["purchasePrice", "sellPrice"]:
|
||||
val = detail.get(f)
|
||||
if isinstance(val, (int, float)):
|
||||
detail[f] = f"{val:.2f}"
|
||||
if detail.get("partBarCodeVos") is None:
|
||||
detail["partBarCodeVos"] = []
|
||||
|
||||
# 提交更新
|
||||
update_resp = requests.post(
|
||||
'https://ids-goods.f6car.com/f6-ids-goods/part/updateAreaPartBasicInfo',
|
||||
cookies=cookies,
|
||||
json=detail
|
||||
)
|
||||
time.sleep(2)
|
||||
|
||||
if update_resp.status_code == 200 and update_resp.json().get("code") == 200:
|
||||
results.append({'材料编码': custom_code, '状态': '修改成功'})
|
||||
success_count += 1
|
||||
consecutive_failures = 0 # 成功时重置计数器
|
||||
else:
|
||||
msg = update_resp.json().get("message", "未知错误")
|
||||
error_msg = f'修改失败: {msg}'
|
||||
results.append({'材料编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"材料编码 {custom_code} {error_msg}, 响应数据: {update_resp.json()}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f'请求异常: {str(e)}'
|
||||
results.append({'材料编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"材料编码 {custom_code} {error_msg}, 堆栈信息: {traceback.format_exc()}")
|
||||
except Exception as e:
|
||||
error_msg = f'内部错误: {str(e)}'
|
||||
results.append({'材料编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"材料编码 {custom_code} {error_msg}, 堆栈信息: {traceback.format_exc()}")
|
||||
|
||||
# 检查连续失败次数
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'材料编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
|
||||
# 统计汇总(总行数 / 待处理 / 成功 / 失败 / 跳过)
|
||||
total_rows = len(df)
|
||||
to_process = len(update_map)
|
||||
# results 里可能包含 “缺少 partId” 等失败信息;这里跳过数按空编码行计数即可
|
||||
skip_count = skipped_count
|
||||
summary = {
|
||||
"总行数": total_rows,
|
||||
"待处理": to_process,
|
||||
"成功": success_count,
|
||||
"失败": failure_count,
|
||||
"跳过": skip_count,
|
||||
}
|
||||
results.insert(0, {"汇总": summary})
|
||||
logger.info(f"材料修改汇总: {summary}")
|
||||
|
||||
# 第四步:清理与回写
|
||||
print({'msg': '已执行', 'msg_details': results})
|
||||
# 结果回写包含汇总 + 明细
|
||||
logger.info("材料批量修改完成,开始回写结果")
|
||||
|
||||
# 删除文件
|
||||
logger.info(f"准备删除文件: {save_path}")
|
||||
try:
|
||||
os.remove(save_path)
|
||||
logger.info(f"文件删除成功: {save_path}")
|
||||
print(f'{save_path} 已删除')
|
||||
except Exception as e:
|
||||
logger.error(f"删除文件失败: {save_path}, 错误信息: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
|
||||
# 回写简道云
|
||||
logger.info("开始回写简道云表单...")
|
||||
try:
|
||||
msg = update_jiandaoyun(data, str(results))
|
||||
logger.info(f"简道云表单回写结果: {msg}")
|
||||
if msg.get('msg'):
|
||||
logger.info("开始自动提交工作流...")
|
||||
approve_workflow(data)
|
||||
logger.info("工作流提交成功")
|
||||
print('表单已自动提交至下一步')
|
||||
else:
|
||||
logger.warning(f"简道云表单回写失败: {msg}")
|
||||
except Exception as e:
|
||||
logger.error(f"回写简道云表单失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
|
||||
logger.info("材料批量修改任务执行完成")
|
||||
|
||||
|
||||
def batch_modify_projects(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str) -> None:
|
||||
"""
|
||||
项目批量修改后台任务
|
||||
|
||||
在后台线程中批量修改项目,从 Excel 文件中读取项目编码。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息
|
||||
df: Excel 文件读取的内容,DataFrame 格式,列顺序为:
|
||||
[0:原项目编码, 1:新项目编码, 2:新项目名称, 3:业务分类, 4:销项税率, 5:项目说明]
|
||||
save_path: Excel 文件保存的地址,执行完成后会删除此文件
|
||||
|
||||
注意:
|
||||
- 无效的项目编码(None、空字符串)会被跳过
|
||||
- Excel 中某字段为空(NaN 或空字符串)时,保留原始项目中的对应字段
|
||||
- 如果新项目名称与系统已有项目名称重复,则跳过处理
|
||||
- 如果Excel中多个行的新项目名称重复,只执行第一条数据,后续重复的会被跳过
|
||||
- 执行完成后会自动删除上传的文件
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
logger.info(f"开始执行项目批量修改任务,文件: {save_path},行数: {len(df)}")
|
||||
|
||||
def safe_str(val):
|
||||
"""将值转为字符串,NaN 返回空字符串"""
|
||||
if pd.isna(val):
|
||||
return ""
|
||||
return str(val).strip()
|
||||
|
||||
def safe_float(val):
|
||||
"""安全转换为 float,失败返回 None"""
|
||||
if pd.isna(val):
|
||||
return None
|
||||
try:
|
||||
return float(val)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def should_update(new_val):
|
||||
"""判断是否应该用 new_val 更新:非 NaN 且非空字符串"""
|
||||
return not (pd.isna(new_val) or (isinstance(new_val, str) and new_val.strip() == ""))
|
||||
|
||||
def safe_iloc(row, idx, default=None):
|
||||
"""安全按索引取行值,列数不足时返回 default,避免 IndexError"""
|
||||
try:
|
||||
if idx < len(row):
|
||||
return row.iloc[idx]
|
||||
except (IndexError, KeyError):
|
||||
pass
|
||||
return default
|
||||
|
||||
# 获取门店id
|
||||
logger.info("获取门店ID...")
|
||||
try:
|
||||
org_id = get_operate_org_id(cookies)
|
||||
logger.info(f"门店ID获取成功: {org_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"获取门店ID失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# 获取服务分类(用于映射业务分类名称 → pkId)
|
||||
logger.info("获取服务分类列表...")
|
||||
try:
|
||||
init_add_resp = requests.post(
|
||||
'https://ids-goods.f6car.cn/f6-ids-goods/service/initAdd',
|
||||
cookies=cookies,
|
||||
data={'idOwnOrg': org_id}
|
||||
)
|
||||
time.sleep(1)
|
||||
init_add_resp.raise_for_status()
|
||||
service_category_list = init_add_resp.json().get("data", {}).get("serviceCategory", [])
|
||||
category_name_to_pk = {item["name"]: item["pkId"] for item in service_category_list}
|
||||
logger.info(f"服务分类列表获取成功,共{len(category_name_to_pk)}个分类")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取服务分类列表失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# 第一步:获取所有项目列表(分页)
|
||||
logger.info("获取项目列表...")
|
||||
json_data = {
|
||||
'param': '',
|
||||
'name': '',
|
||||
'customCode': '',
|
||||
'currentPage': 1,
|
||||
'pageSize': 100,
|
||||
'isDel': 0, # 只查启用的
|
||||
'customInvoiceCategory': 0,
|
||||
'idOwnOrg': org_id,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://ids-goods.f6car.cn/f6-ids-goods/service/getServiceList',
|
||||
cookies=cookies,
|
||||
json=json_data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
total_pages = response.json().get("data", {}).get("totalPages", 0)
|
||||
logger.info(f"项目列表总页数: {total_pages}")
|
||||
|
||||
all_project_list = []
|
||||
total_pages = int(total_pages)
|
||||
for page in tqdm(range(1, total_pages + 1), desc="获取项目列表"):
|
||||
json_data['currentPage'] = page
|
||||
try:
|
||||
resp = requests.post(
|
||||
'https://ids-goods.f6car.cn/f6-ids-goods/service/getServiceList',
|
||||
cookies=cookies,
|
||||
json=json_data,
|
||||
)
|
||||
time.sleep(1)
|
||||
resp.raise_for_status()
|
||||
records = resp.json().get("data", {}).get("records", [])
|
||||
all_project_list.extend(records)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取第{page}页项目列表失败: {str(e)}, 响应状态码: {getattr(e.response, 'status_code', 'N/A')}")
|
||||
raise
|
||||
logger.info(f"项目列表获取完成,总计: {len(all_project_list)}条项目")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"获取项目列表失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# 构建系统已有项目名称集合(用于检查重复)
|
||||
existing_project_names = set()
|
||||
for project in all_project_list:
|
||||
project_name = project.get("name")
|
||||
if project_name:
|
||||
existing_project_names.add(str(project_name).strip())
|
||||
logger.info(f"系统已有项目名称数: {len(existing_project_names)}")
|
||||
|
||||
# 第二步:构建 update_map
|
||||
logger.info("构建更新映射表...")
|
||||
update_map = {}
|
||||
skipped_count = 0
|
||||
category_not_found_count = 0
|
||||
duplicate_name_in_df_count = 0 # DataFrame中重复的新项目名称数量
|
||||
duplicate_name_in_system_count = 0 # 与系统已有项目名称重复的数量
|
||||
seen_new_names = {} # 用于跟踪DataFrame中已出现的新项目名称,key为新名称,value为第一次出现的原项目编码
|
||||
results = [] # 提前创建results,用于记录跳过的信息
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
orig_code = safe_iloc(row, 0)
|
||||
if pd.isna(orig_code) or str(orig_code).strip() == "":
|
||||
skipped_count += 1
|
||||
results.append({'项目编码': '', '状态': '跳过: 项目编码为空'})
|
||||
continue
|
||||
orig_code = str(orig_code).strip()
|
||||
|
||||
# 检查新项目名称(第2列,索引为2)
|
||||
new_name = safe_iloc(row, 2)
|
||||
if should_update(new_name):
|
||||
new_name_clean = safe_str(new_name)
|
||||
|
||||
# 检查DataFrame中是否有重复的新项目名称
|
||||
if new_name_clean in seen_new_names:
|
||||
duplicate_name_in_df_count += 1
|
||||
logger.warning(f"DataFrame中项目名称重复,跳过: 原项目编码={orig_code}, 新项目名称={new_name_clean}, 首次出现在原项目编码={seen_new_names[new_name_clean]}")
|
||||
results.append({'项目编码': orig_code, '状态': f'跳过: DataFrame中项目名称重复(首次出现在原项目编码={seen_new_names[new_name_clean]})'})
|
||||
continue
|
||||
|
||||
# 检查新项目名称是否与系统已有项目名称重复
|
||||
if new_name_clean in existing_project_names:
|
||||
duplicate_name_in_system_count += 1
|
||||
logger.warning(f"新项目名称与系统已有项目名称重复,跳过: 原项目编码={orig_code}, 新项目名称={new_name_clean}")
|
||||
results.append({'项目编码': orig_code, '状态': f'跳过: 新项目名称与系统已有项目名称重复({new_name_clean})'})
|
||||
continue
|
||||
|
||||
# 记录这个新名称
|
||||
seen_new_names[new_name_clean] = orig_code
|
||||
|
||||
# 业务分类映射
|
||||
category_name = safe_iloc(row, 3)
|
||||
category_pk = None
|
||||
category_not_found = False
|
||||
if should_update(category_name):
|
||||
cat_name_clean = safe_str(category_name)
|
||||
category_pk = category_name_to_pk.get(cat_name_clean)
|
||||
if category_pk is None:
|
||||
logger.warning(f"业务分类 '{cat_name_clean}' 未在系统中找到,项目编码: {orig_code}")
|
||||
category_not_found = True
|
||||
category_not_found_count += 1
|
||||
|
||||
# 列 6/7/8(车辆分类、工时单价、工时)不再传入,固定为 None,不更新这些字段
|
||||
car_category_name = None
|
||||
price = None
|
||||
work_hour = None
|
||||
amount = None
|
||||
|
||||
update_map[orig_code] = {
|
||||
"new_customCode": safe_iloc(row, 1), # 新项目编码
|
||||
"new_name": safe_iloc(row, 2), # 新项目名称
|
||||
"new_serviceCategoryId": category_pk,
|
||||
"new_taxRate": safe_iloc(row, 4),
|
||||
"new_memo": safe_iloc(row, 5),
|
||||
"new_carCategoryName": car_category_name,
|
||||
"new_price": price,
|
||||
"new_workHour": work_hour,
|
||||
"new_amount": amount,
|
||||
}
|
||||
logger.info(f"映射表完成: 待处理={len(update_map)}, 跳过空编码={skipped_count}, 分类未找到={category_not_found_count}, DF重复名={duplicate_name_in_df_count}, 系统重名={duplicate_name_in_system_count}")
|
||||
|
||||
# 第三步:遍历项目,按需更新
|
||||
logger.info("开始处理项目更新...")
|
||||
consecutive_failures = 0 # 连续失败计数器
|
||||
MAX_CONSECUTIVE_FAILURES = 100 # 最大连续失败次数
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
|
||||
for item in tqdm(all_project_list, desc="处理项目更新"):
|
||||
custom_code = item.get("customCode")
|
||||
if not custom_code or str(custom_code) not in update_map:
|
||||
continue
|
||||
|
||||
service_id = item.get("pkId") # 项目主键
|
||||
if not service_id:
|
||||
error_msg = '缺少 pkId'
|
||||
results.append({'项目编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.warning(f"项目编码 {custom_code} {error_msg}")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'项目编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
continue
|
||||
|
||||
try:
|
||||
# 获取项目明细
|
||||
params = {
|
||||
'serviceId': service_id,
|
||||
'customInvoiceCategory': '0',
|
||||
}
|
||||
detail_resp = requests.get(
|
||||
'https://ids-goods.f6car.cn/f6-ids-goods/service/viewService',
|
||||
params=params,
|
||||
cookies=cookies,
|
||||
)
|
||||
time.sleep(1)
|
||||
if detail_resp.status_code != 200:
|
||||
error_msg = f'获取明细失败: {detail_resp.status_code}'
|
||||
results.append({'项目编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"项目编码 {custom_code} {error_msg}, 响应内容: {detail_resp.text[:200]}")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'项目编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
continue
|
||||
|
||||
detail = detail_resp.json().get("data")
|
||||
if not detail:
|
||||
error_msg = '明细为空'
|
||||
results.append({'项目编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"项目编码 {custom_code} {error_msg}, 响应JSON: {detail_resp.json()}")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'项目编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
continue
|
||||
|
||||
updates = update_map[str(custom_code)]
|
||||
|
||||
# === 安全更新字段(仅非空时覆盖)===
|
||||
if should_update(updates["new_customCode"]):
|
||||
detail["customCode"] = safe_str(updates["new_customCode"])
|
||||
if should_update(updates["new_name"]):
|
||||
name_val = safe_str(updates["new_name"])
|
||||
# 再次检查新项目名称是否与系统已有项目名称重复(防止在构建update_map后系统状态发生变化)
|
||||
if name_val in existing_project_names:
|
||||
error_msg = f'跳过: 新项目名称与系统已有项目名称重复({name_val})'
|
||||
results.append({'项目编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.warning(f"项目编码 {custom_code} {error_msg}")
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'项目编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
continue
|
||||
detail["name"] = name_val
|
||||
if "showName" in detail:
|
||||
detail["showName"] = name_val
|
||||
|
||||
if updates["new_serviceCategoryId"] is not None:
|
||||
detail["serviceCategoryId"] = updates["new_serviceCategoryId"]
|
||||
|
||||
if should_update(updates["new_taxRate"]):
|
||||
detail["taxRate"] = safe_str(updates["new_taxRate"])
|
||||
|
||||
if should_update(updates["new_memo"]):
|
||||
detail["memo"] = safe_str(updates["new_memo"])
|
||||
|
||||
if should_update(updates["new_carCategoryName"]):
|
||||
detail["carCategoryName"] = safe_str(updates["new_carCategoryName"])
|
||||
|
||||
if updates["new_price"] is not None:
|
||||
detail["price"] = f"{updates['new_price']:.2f}"
|
||||
if updates["new_workHour"] is not None:
|
||||
detail["workHour"] = f"{updates['new_workHour']:.2f}"
|
||||
if updates["new_amount"] is not None:
|
||||
detail["amount"] = f"{updates['new_amount']:.2f}"
|
||||
|
||||
# === 提交更新 ===
|
||||
update_resp = requests.post(
|
||||
'https://ids-goods.f6car.cn/f6-ids-goods/service/editService',
|
||||
cookies=cookies,
|
||||
json=detail
|
||||
)
|
||||
time.sleep(2)
|
||||
|
||||
if update_resp.status_code == 200 and update_resp.json().get("code") == 200:
|
||||
results.append({'项目编码': custom_code, '状态': '修改成功'})
|
||||
success_count += 1
|
||||
consecutive_failures = 0 # 成功时重置计数器
|
||||
else:
|
||||
msg = update_resp.json().get("message", "未知错误")
|
||||
error_msg = f'修改失败: {msg}'
|
||||
results.append({'项目编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
logger.error(f"项目编码 {custom_code} {error_msg}, 响应数据: {update_resp.json()}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"请求异常: {str(e)}"
|
||||
logger.error(f"项目编码 {custom_code} {error_msg}, 堆栈信息: {traceback.format_exc()}")
|
||||
results.append({'项目编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
except Exception as e:
|
||||
error_msg = f"异常: {str(e)}"
|
||||
logger.error(f"项目编码 {custom_code} 更新出错: {traceback.format_exc()}")
|
||||
results.append({'项目编码': custom_code, '状态': error_msg})
|
||||
failure_count += 1
|
||||
consecutive_failures += 1
|
||||
|
||||
# 检查连续失败次数
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
results.append({'项目编码': '系统保护', '状态': '连续100次失败已终止'})
|
||||
logger.warning(f"连续失败{MAX_CONSECUTIVE_FAILURES}次,已中止任务执行")
|
||||
break
|
||||
|
||||
# 汇总插入到结果顶部,便于简道云直接看到统计
|
||||
total_rows = len(df)
|
||||
to_process = len(update_map)
|
||||
skip_count = skipped_count + duplicate_name_in_df_count + duplicate_name_in_system_count
|
||||
summary = {
|
||||
"总行数": total_rows,
|
||||
"待处理": to_process,
|
||||
"成功": success_count,
|
||||
"失败": failure_count,
|
||||
"跳过": skip_count,
|
||||
}
|
||||
results.insert(0, {"汇总": summary})
|
||||
logger.info(f"项目修改汇总: {summary}")
|
||||
|
||||
# 第四步:清理与回写
|
||||
print({'msg': '已执行', 'msg_details': results})
|
||||
logger.info("项目批量修改完成,开始回写结果")
|
||||
|
||||
# 删除文件
|
||||
logger.info(f"准备删除文件: {save_path}")
|
||||
try:
|
||||
os.remove(save_path)
|
||||
logger.info(f"文件删除成功: {save_path}")
|
||||
print(f'{save_path} 已删除')
|
||||
except Exception as e:
|
||||
logger.error(f"删除文件失败: {save_path}, 错误信息: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
|
||||
# 回写简道云
|
||||
logger.info("开始回写简道云表单...")
|
||||
try:
|
||||
msg = update_jiandaoyun(data, str(results))
|
||||
logger.info(f"简道云表单回写结果: {msg}")
|
||||
if msg.get('msg'):
|
||||
logger.info("开始自动提交工作流...")
|
||||
approve_workflow(data)
|
||||
logger.info("工作流提交成功")
|
||||
print('表单已自动提交至下一步')
|
||||
else:
|
||||
logger.warning(f"简道云表单回写失败: {msg}")
|
||||
except Exception as e:
|
||||
logger.error(f"回写简道云表单失败: {str(e)}, 堆栈信息: {traceback.format_exc()}")
|
||||
|
||||
logger.info("项目批量修改任务执行完成")
|
||||
@@ -1,3 +1,14 @@
|
||||
"""
|
||||
应用工具模块
|
||||
|
||||
本模块提供应用级的工具类,包括:
|
||||
- 日志记录器配置(支持日志轮转)
|
||||
- 后台任务调度器
|
||||
- 任务队列管理
|
||||
- 请求头解码工具
|
||||
|
||||
这些工具在整个应用中被广泛使用。
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
@@ -8,7 +19,27 @@ from urllib.parse import unquote
|
||||
|
||||
|
||||
class AppTools:
|
||||
"""
|
||||
应用级工具集合类
|
||||
|
||||
提供应用级别的工具功能,包括:
|
||||
- 初始化轮转日志记录器
|
||||
- 初始化后台调度器(进程退出时自动关闭)
|
||||
- 维护一个简单的任务队列与后台处理线程
|
||||
|
||||
属性:
|
||||
config: 配置对象
|
||||
task_queue: 任务队列
|
||||
logger: 日志记录器
|
||||
scheduler: 后台调度器
|
||||
"""
|
||||
def __init__(self, config):
|
||||
"""
|
||||
初始化应用工具
|
||||
|
||||
Args:
|
||||
config: 配置对象,包含日志、目录等配置信息
|
||||
"""
|
||||
self.config = config
|
||||
self.task_queue = Queue()
|
||||
self.logger = self._setup_logger()
|
||||
@@ -16,6 +47,19 @@ class AppTools:
|
||||
self._start_task_thread()
|
||||
|
||||
def _setup_logger(self):
|
||||
"""
|
||||
配置带轮转的文件日志记录器
|
||||
|
||||
创建支持日志轮转的文件日志记录器,避免重复添加相同文件处理器。
|
||||
|
||||
Returns:
|
||||
logging.Logger: 配置好的日志记录器
|
||||
|
||||
注意:
|
||||
- 日志文件最大 5MB
|
||||
- 保留 5 个备份文件
|
||||
- 使用 UTF-8 编码
|
||||
"""
|
||||
log_dir = self.config.LOGS_DIRECTORY
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
@@ -25,6 +69,7 @@ class AppTools:
|
||||
logger = logging.getLogger("app")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# 若未绑定目标日志文件的 RotatingFileHandler,则创建并绑定
|
||||
if not any(isinstance(h, RotatingFileHandler) and getattr(h, 'baseFilename', None) == str(log_file)
|
||||
for h in logger.handlers):
|
||||
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024 * 5, backupCount=5, encoding='utf-8')
|
||||
@@ -36,19 +81,43 @@ class AppTools:
|
||||
return logger
|
||||
|
||||
def _setup_scheduler(self):
|
||||
"""
|
||||
初始化后台调度器
|
||||
|
||||
创建后台调度器,并在进程退出时优雅关闭,防止资源泄漏。
|
||||
|
||||
Returns:
|
||||
BackgroundScheduler: 后台调度器实例
|
||||
|
||||
注意:
|
||||
调度器会在进程退出时自动关闭
|
||||
"""
|
||||
scheduler = BackgroundScheduler()
|
||||
import atexit
|
||||
atexit.register(lambda: scheduler.shutdown(wait=False))
|
||||
return scheduler
|
||||
|
||||
def _start_task_thread(self):
|
||||
"""
|
||||
启动后台任务处理线程
|
||||
|
||||
启动一个守护线程来处理任务队列中的任务,守护模式运行,随主线程退出。
|
||||
"""
|
||||
task_thread = threading.Thread(target=self.process_tasks, daemon=True)
|
||||
task_thread.start()
|
||||
|
||||
def process_tasks(self):
|
||||
"""
|
||||
后台消费队列中的任务:
|
||||
任务结构为 {'handler': callable, 'data': any, 'response': Queue}
|
||||
- 正常执行时将 handler(data) 的结果放入 response 队列
|
||||
- 发生异常时记录错误并将失败信息放入 response 队列
|
||||
- 每次任务完成后调用 task_done()
|
||||
"""
|
||||
while True:
|
||||
task = self.task_queue.get()
|
||||
if task is None:
|
||||
# 外部以 None 作为结束信号
|
||||
self.logger.error("任务处理线程已终止")
|
||||
break
|
||||
try:
|
||||
@@ -62,6 +131,18 @@ class AppTools:
|
||||
self.logger.info("任务处理完成")
|
||||
|
||||
def enqueue_task(self, handler, data):
|
||||
"""
|
||||
将任务入队
|
||||
|
||||
将任务放入任务队列,并返回一个响应队列,调用方可以从响应队列中获取执行结果。
|
||||
|
||||
Args:
|
||||
handler: 处理函数,执行具体业务逻辑
|
||||
data: 传递给处理函数的数据
|
||||
|
||||
Returns:
|
||||
Queue: 响应队列,用于获取任务执行结果
|
||||
"""
|
||||
response_queue = Queue()
|
||||
self.task_queue.put({
|
||||
'handler': handler,
|
||||
@@ -72,13 +153,39 @@ class AppTools:
|
||||
|
||||
@staticmethod
|
||||
def decode_headers(headers):
|
||||
"""
|
||||
解码请求头
|
||||
|
||||
对请求头字典进行 URL 解码(UTF-8),返回解码后的副本。
|
||||
主要用于处理包含中文字符的请求头。
|
||||
|
||||
Args:
|
||||
headers: 请求头字典
|
||||
|
||||
Returns:
|
||||
dict: 解码后的请求头字典
|
||||
"""
|
||||
return {key: unquote(value, encoding='utf-8') for key, value in headers.items()}
|
||||
|
||||
|
||||
# 全局日志记录器变量
|
||||
# 用于存储全局日志记录器实例,避免重复初始化
|
||||
logger = None
|
||||
|
||||
|
||||
def setup_global_logger(config):
|
||||
"""
|
||||
设置全局日志记录器
|
||||
|
||||
懒加载并返回全局 logger,若未初始化则构建 AppTools 并复用其中的 logger。
|
||||
避免重复初始化日志处理器。
|
||||
|
||||
Args:
|
||||
config: 配置对象
|
||||
|
||||
Returns:
|
||||
logging.Logger: 全局日志记录器实例
|
||||
"""
|
||||
global logger
|
||||
if logger is None:
|
||||
app_tools = AppTools(config)
|
||||
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
# 添加新任务类指南
|
||||
|
||||
本文档详细说明如何在项目中添加一个新的任务类,包含立即响应和后台执行功能。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [架构概述](#架构概述)
|
||||
2. [添加步骤](#添加步骤)
|
||||
3. [代码示例](#代码示例)
|
||||
4. [文件结构说明](#文件结构说明)
|
||||
5. [注意事项](#注意事项)
|
||||
|
||||
---
|
||||
|
||||
## 架构概述
|
||||
|
||||
项目采用**立即响应 + 后台执行**的架构模式:
|
||||
|
||||
- **立即响应函数**:位于 `app/module/f6_plugin_handlers.py`,负责快速响应请求,立即返回"正在执行"消息
|
||||
- **后台执行函数**:位于 `app/tasks/` 目录下,负责在后台线程中执行实际任务
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
客户端请求
|
||||
↓
|
||||
立即响应函数(F6PluginHandlers)
|
||||
↓
|
||||
启动后台线程
|
||||
↓
|
||||
后台执行函数(tasks/*.py)
|
||||
↓
|
||||
更新简道云表单
|
||||
↓
|
||||
自动提交工作流
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 添加步骤
|
||||
|
||||
### 步骤 1: 创建后台任务文件
|
||||
|
||||
在 `app/tasks/` 目录下创建新的任务文件,例如 `app/tasks/your_task.py`:
|
||||
|
||||
```python
|
||||
"""
|
||||
你的任务相关后台任务模块
|
||||
|
||||
本模块包含你的任务相关的后台任务,包括:
|
||||
- 任务描述1
|
||||
- 任务描述2
|
||||
|
||||
这些任务在后台线程中执行,不会阻塞主请求。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
"""
|
||||
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 your_task_background(data: Dict[str, Any], cookies: Dict[str, str] = None,
|
||||
df: pd.DataFrame = None, save_path: str = None):
|
||||
"""
|
||||
你的任务后台执行函数
|
||||
|
||||
在后台线程中执行你的任务。
|
||||
执行完成后会更新简道云表单并自动提交工作流。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
cookies: 用户登录 F6 系统的 cookies 信息(可选)
|
||||
df: Excel 文件读取的内容,DataFrame 格式(可选)
|
||||
save_path: Excel 文件保存的地址,执行完成后会删除此文件(可选)
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
注意:
|
||||
- 执行完成后会自动删除上传的文件(如果提供了save_path)
|
||||
- 执行结果会更新到简道云表单
|
||||
"""
|
||||
try:
|
||||
# TODO: 在这里实现具体的任务逻辑
|
||||
results = []
|
||||
|
||||
# 示例:处理数据
|
||||
if df is not None:
|
||||
df = df.where(pd.notnull(df), None)
|
||||
for index, row in tqdm(df.iterrows(), total=df.shape[0], desc="处理数据"):
|
||||
# 实现具体的数据处理逻辑
|
||||
result_item = {
|
||||
'行号': index + 1,
|
||||
'状态': '处理成功'
|
||||
}
|
||||
results.append(result_item)
|
||||
else:
|
||||
# 如果没有DataFrame,执行其他任务
|
||||
results.append({'状态': '任务执行成功'})
|
||||
|
||||
# 删除文件(如果提供了save_path)
|
||||
if save_path and os.path.exists(save_path):
|
||||
os.remove(save_path)
|
||||
logger.info(f'{save_path}已删除')
|
||||
|
||||
# 格式化结果
|
||||
results_str = f'{results}' if results else '任务执行完成'
|
||||
logger.info(f"任务执行结果: {results_str}")
|
||||
|
||||
# 调用api回写改掉 执行明细与执行状态
|
||||
msg = update_jiandaoyun(data, results_str)
|
||||
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
logger.info('表单已自动提交至下一步')
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'任务执行失败: {str(e)}'
|
||||
logger.error(error_msg, exc_info=True)
|
||||
msg = update_jiandaoyun(data, error_msg)
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
```
|
||||
|
||||
### 步骤 2: 在任务导出文件中注册
|
||||
|
||||
在 `app/tasks/__init__.py` 中添加导入和导出:
|
||||
|
||||
```python
|
||||
# 你的任务
|
||||
from app.tasks.your_task import your_task_background
|
||||
|
||||
__all__ = [
|
||||
# ... 其他任务
|
||||
# 你的任务
|
||||
'your_task_background',
|
||||
]
|
||||
```
|
||||
|
||||
### 步骤 3: 添加立即响应函数
|
||||
|
||||
在 `app/module/f6_plugin_handlers.py` 中添加立即响应函数:
|
||||
|
||||
```python
|
||||
from app.tasks.your_task import your_task_background
|
||||
|
||||
class F6PluginHandlers:
|
||||
# ... 其他方法
|
||||
|
||||
@staticmethod
|
||||
def your_task(data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
你的任务
|
||||
|
||||
从简道云获取任务请求,读取 Excel 文件(如果需要),并在后台线程中执行任务。
|
||||
立即返回"正在执行"的提示,实际执行在后台线程中完成。
|
||||
|
||||
Args:
|
||||
data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含执行状态的字典
|
||||
"""
|
||||
entry_data = api_instance.entry_data_get(data=data)
|
||||
print('执行 你的任务')
|
||||
|
||||
# 获取必要的参数(根据实际需求调整)
|
||||
username = entry_data['data'].get('账号')
|
||||
password = entry_data['data'].get('密码')
|
||||
company_name = entry_data['data'].get('公司名称')
|
||||
save_path = entry_data['data'].get('文件保存地址')
|
||||
|
||||
# 如果需要登录F6系统
|
||||
cookies = None
|
||||
if username and password and company_name:
|
||||
login_response = F6Module.login_in(username, password, company_name)
|
||||
if login_response is None:
|
||||
return {'msg': '登录失败', 'msg_details': '无法登录F6系统'}
|
||||
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
|
||||
|
||||
# 如果需要读取Excel文件
|
||||
df = None
|
||||
if save_path:
|
||||
try:
|
||||
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
|
||||
except Exception as e:
|
||||
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
|
||||
|
||||
# 启动后台线程执行任务
|
||||
try:
|
||||
thread = threading.Thread(target=your_task_background,
|
||||
args=(data, cookies, df, save_path))
|
||||
thread.start()
|
||||
except Exception as e:
|
||||
print(f'创建线程失败: {str(e)}')
|
||||
return {'msg': '任务启动失败', 'msg_details': f'无法启动后台任务: {str(e)}'}
|
||||
|
||||
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
```
|
||||
|
||||
### 步骤 4: 注册操作到模块注册表
|
||||
|
||||
在 `main.py` 的 `lifespan` 函数中注册操作:
|
||||
|
||||
```python
|
||||
core_manager.register_action('your_task', f6_plugin_handlers.your_task, 'f6_plugin_module',
|
||||
description='你的任务描述')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 完整示例:BI任务
|
||||
|
||||
以下是一个完整的示例,展示如何添加BI任务类:
|
||||
|
||||
#### 1. 后台任务文件 (`app/tasks/bi_tasks.py`)
|
||||
|
||||
```python
|
||||
"""
|
||||
BI相关后台任务模块
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import pandas as pd
|
||||
from typing import Dict, Any
|
||||
from tqdm import tqdm
|
||||
from app.tasks.common import update_jiandaoyun, approve_workflow
|
||||
|
||||
logger = logging.getLogger('app')
|
||||
|
||||
def bi_task_background(data: Dict[str, Any], cookies: Dict[str, str] = None,
|
||||
df: pd.DataFrame = None, save_path: str = None):
|
||||
"""BI任务后台执行函数"""
|
||||
try:
|
||||
results = []
|
||||
if df is not None:
|
||||
# 处理Excel数据
|
||||
for index, row in tqdm(df.iterrows(), total=df.shape[0], desc="处理BI数据"):
|
||||
results.append({'行号': index + 1, '状态': '处理成功'})
|
||||
else:
|
||||
results.append({'状态': 'BI任务执行成功'})
|
||||
|
||||
if save_path and os.path.exists(save_path):
|
||||
os.remove(save_path)
|
||||
|
||||
msg = update_jiandaoyun(data, f'{results}')
|
||||
if msg.get('msg'):
|
||||
approve_workflow(data)
|
||||
except Exception as e:
|
||||
error_msg = f'BI任务执行失败: {str(e)}'
|
||||
logger.error(error_msg, exc_info=True)
|
||||
update_jiandaoyun(data, error_msg)
|
||||
```
|
||||
|
||||
#### 2. 立即响应函数 (`app/module/f6_plugin_handlers.py`)
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def bi_task(data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""BI任务立即响应函数"""
|
||||
entry_data = api_instance.entry_data_get(data=data)
|
||||
|
||||
# 获取参数
|
||||
username = entry_data['data'].get('账号')
|
||||
password = entry_data['data'].get('密码')
|
||||
company_name = entry_data['data'].get('公司名称')
|
||||
save_path = entry_data['data'].get('文件保存地址')
|
||||
|
||||
# 登录(如果需要)
|
||||
cookies = None
|
||||
if username and password and company_name:
|
||||
login_response = F6Module.login_in(username, password, company_name)
|
||||
if login_response is None:
|
||||
return {'msg': '登录失败'}
|
||||
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
|
||||
|
||||
# 读取文件(如果需要)
|
||||
df = None
|
||||
if save_path:
|
||||
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
|
||||
|
||||
# 启动后台线程
|
||||
thread = threading.Thread(target=bi_task_background,
|
||||
args=(data, cookies, df, save_path))
|
||||
thread.start()
|
||||
|
||||
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||
```
|
||||
|
||||
#### 3. 注册操作 (`main.py`)
|
||||
|
||||
```python
|
||||
core_manager.register_action('bi_task', f6_plugin_handlers.bi_task, 'f6_plugin_module',
|
||||
description='BI任务')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件结构说明
|
||||
|
||||
### 关键文件位置
|
||||
|
||||
```
|
||||
fastapi_app/
|
||||
├── app/
|
||||
│ ├── module/
|
||||
│ │ └── f6_plugin_handlers.py # 立即响应处理器(重命名自 f6_plugin_module.py)
|
||||
│ │
|
||||
│ ├── tasks/
|
||||
│ │ ├── __init__.py # 任务导出文件
|
||||
│ │ ├── common.py # 通用任务函数
|
||||
│ │ ├── bi_tasks.py # BI任务(示例)
|
||||
│ │ ├── brand_tasks.py # 品牌任务
|
||||
│ │ ├── customer_tasks.py # 客户任务
|
||||
│ │ └── delete_tasks.py # 删除任务
|
||||
│ │
|
||||
│ └── api/
|
||||
│ └── routes.py # API路由
|
||||
│
|
||||
└── main.py # 应用入口,注册所有操作
|
||||
```
|
||||
|
||||
### 命名规范
|
||||
|
||||
- **立即响应函数**:位于 `F6PluginHandlers` 类中,使用小写下划线命名(如 `bi_task`)
|
||||
- **后台执行函数**:位于 `app/tasks/` 目录,使用 `{task_name}_background` 命名(如 `bi_task_background`)
|
||||
- **操作名称**:在 `main.py` 中注册时使用,通常与立即响应函数名相同(如 `'bi_task'`)
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 文件命名变更
|
||||
|
||||
**重要**:`f6_plugin_module.py` 已重命名为 `f6_plugin_handlers.py`,类名从 `F6PluginModule` 改为 `F6PluginHandlers`。
|
||||
|
||||
- 文件名更清晰地表达了其功能:处理立即响应的处理器
|
||||
- 所有引用已更新,但 `app.state.f6_plugin_module` 保持向后兼容
|
||||
|
||||
### 2. 参数传递
|
||||
|
||||
后台执行函数通常接收以下参数:
|
||||
|
||||
- `data`: 必需,包含简道云表单信息
|
||||
- `cookies`: 可选,F6系统登录凭证
|
||||
- `df`: 可选,Excel文件数据(DataFrame)
|
||||
- `save_path`: 可选,文件保存路径
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
- 立即响应函数:应捕获登录、文件读取等错误,立即返回错误信息
|
||||
- 后台执行函数:应使用 try-except 包裹整个逻辑,确保错误能更新到简道云表单
|
||||
|
||||
### 4. 文件清理
|
||||
|
||||
如果任务处理了上传的文件,应在执行完成后删除:
|
||||
|
||||
```python
|
||||
if save_path and os.path.exists(save_path):
|
||||
os.remove(save_path)
|
||||
logger.info(f'{save_path}已删除')
|
||||
```
|
||||
|
||||
### 5. 简道云表单更新
|
||||
|
||||
所有后台任务完成后都应:
|
||||
|
||||
1. 调用 `update_jiandaoyun(data, results_str)` 更新执行结果
|
||||
2. 如果更新成功,调用 `approve_workflow(data)` 自动提交工作流
|
||||
|
||||
### 6. 日志记录
|
||||
|
||||
使用项目统一的日志记录器:
|
||||
|
||||
```python
|
||||
import logging
|
||||
logger = logging.getLogger('app')
|
||||
logger.info("信息日志")
|
||||
logger.error("错误日志", exc_info=True) # exc_info=True 记录异常堆栈
|
||||
```
|
||||
|
||||
### 7. 进度显示
|
||||
|
||||
对于批量处理任务,使用 `tqdm` 显示进度:
|
||||
|
||||
```python
|
||||
from tqdm import tqdm
|
||||
|
||||
for index, row in tqdm(df.iterrows(), total=df.shape[0], desc="处理数据"):
|
||||
# 处理逻辑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速检查清单
|
||||
|
||||
添加新任务类时,请确认:
|
||||
|
||||
- [ ] 创建了后台任务文件 `app/tasks/{task_name}_tasks.py`
|
||||
- [ ] 在 `app/tasks/__init__.py` 中导出了后台任务函数
|
||||
- [ ] 在 `app/module/f6_plugin_handlers.py` 中添加了立即响应函数
|
||||
- [ ] 在 `main.py` 中注册了操作
|
||||
- [ ] 实现了错误处理逻辑
|
||||
- [ ] 添加了日志记录
|
||||
- [ ] 实现了文件清理(如果处理了文件)
|
||||
- [ ] 实现了简道云表单更新和工作流提交
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何测试新添加的任务?
|
||||
|
||||
A: 启动应用后,通过API调用测试:
|
||||
- 请求头设置 `Action: your_task`
|
||||
- 请求体包含简道云表单数据
|
||||
|
||||
### Q: 任务执行失败怎么办?
|
||||
|
||||
A: 后台执行函数中的异常会被捕获,错误信息会更新到简道云表单的"执行明细"字段。
|
||||
|
||||
### Q: 如何修改任务执行逻辑?
|
||||
|
||||
A: 只需修改 `app/tasks/{task_name}_tasks.py` 中的后台执行函数即可。
|
||||
|
||||
### Q: 可以添加不需要登录的任务吗?
|
||||
|
||||
A: 可以,在立即响应函数中不调用 `F6Module.login_in`,`cookies` 参数传 `None` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `app/module/f6_plugin_handlers.py` - 立即响应处理器
|
||||
- `app/tasks/common.py` - 通用任务函数(update_jiandaoyun, approve_workflow)
|
||||
- `app/core/module_registry.py` - 模块注册表
|
||||
- `main.py` - 应用入口和操作注册
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025年
|
||||
|
||||
**维护者**: 数据组
|
||||
|
||||
+685
@@ -0,0 +1,685 @@
|
||||
# 简道云 FastAPI 服务系统说明书
|
||||
|
||||
## 1. 系统概述
|
||||
|
||||
### 1.1 系统简介
|
||||
|
||||
简道云 FastAPI 服务是一个基于 FastAPI 框架开发的后端服务系统,主要用于处理简道云插件发送的业务请求,与 F6 汽车维修管理系统进行数据交互,实现数据同步、批量处理、信息管理等核心功能。
|
||||
|
||||
### 1.2 系统定位
|
||||
|
||||
- **服务对象**: 简道云插件前端
|
||||
- **集成系统**: F6 汽车维修管理系统、简道云平台
|
||||
- **主要功能**: 数据同步、批量操作、信息管理、工作流自动化
|
||||
|
||||
### 1.3 系统特点
|
||||
|
||||
- **高性能**: 基于 FastAPI 异步框架,支持高并发请求
|
||||
- **模块化**: 采用模块化设计,易于扩展和维护
|
||||
- **可靠性**: 完善的异常处理和重试机制
|
||||
- **自动化**: 支持后台任务自动执行和工作流自动提交
|
||||
- **可维护性**: 清晰的代码结构和完善的日志记录
|
||||
|
||||
## 2. 系统功能
|
||||
|
||||
### 2.1 F6系统集成功能
|
||||
|
||||
#### 2.1.1 登录认证
|
||||
|
||||
**功能描述**: 实现 F6 系统的登录认证,支持用户名密码登录和验证码识别。
|
||||
|
||||
**主要流程**:
|
||||
1. 接收用户名、密码、公司名称
|
||||
2. 对密码进行 MD5 加密
|
||||
3. 发送登录请求到 F6 系统
|
||||
4. 如果触发验证码,自动识别并重试
|
||||
5. 选择指定公司并完成登录
|
||||
6. 返回登录结果和 Cookie 信息
|
||||
|
||||
**技术实现**:
|
||||
- 使用 `requests` 库发送 HTTP 请求
|
||||
- 使用 `pytesseract` 进行验证码 OCR 识别
|
||||
- 使用 `PIL` 进行图像预处理(对比度、亮度调整)
|
||||
|
||||
#### 2.1.2 公司信息获取
|
||||
|
||||
**功能描述**: 获取 F6 系统中用户可访问的公司列表,并保存到简道云表单。
|
||||
|
||||
**主要流程**:
|
||||
1. 使用用户名密码登录 F6 系统
|
||||
2. 获取公司列表(单店或多店)
|
||||
3. 将公司信息批量写入简道云表单
|
||||
4. 返回时间戳用于后续查询
|
||||
|
||||
**输出格式**:
|
||||
- 单店: 返回门店名称
|
||||
- 多店: 返回所有公司名称列表
|
||||
|
||||
#### 2.1.3 门店信息获取
|
||||
|
||||
**功能描述**: 获取指定公司下的门店列表及统计数据。
|
||||
|
||||
**主要流程**:
|
||||
1. 登录 F6 系统并选择公司
|
||||
2. 获取门店列表
|
||||
3. 统计客户车辆数量和客户数量
|
||||
4. 将门店信息批量写入简道云表单
|
||||
5. 返回时间戳和统计数据
|
||||
|
||||
**输出内容**:
|
||||
- 门店名称列表
|
||||
- 客户车辆总数
|
||||
- 客户总数
|
||||
|
||||
#### 2.1.4 保持连接
|
||||
|
||||
**功能描述**: 心跳检测功能,用于保持连接活跃。
|
||||
|
||||
**实现**: 直接返回接收到的数据,不做任何处理。
|
||||
|
||||
### 2.2 文件处理功能
|
||||
|
||||
#### 2.2.1 文件上传和下载
|
||||
|
||||
**功能描述**: 处理简道云插件上传的文件,下载并保存到本地。
|
||||
|
||||
**主要流程**:
|
||||
1. 从简道云获取文件 URL
|
||||
2. 解析文件名和扩展名
|
||||
3. 根据时间戳生成唯一文件名
|
||||
4. 下载文件到 `下载文件/` 目录
|
||||
5. 返回文件保存路径
|
||||
|
||||
**文件命名规则**: `原文件名_时间戳.扩展名`
|
||||
|
||||
#### 2.2.2 文件校验
|
||||
|
||||
**功能描述**: 校验上传的 Excel 文件格式是否符合要求。
|
||||
|
||||
**支持的操作类型**:
|
||||
- `create_brand`: 校验第一列是否为"品牌"
|
||||
- `modify_customer_info`: 校验第一列是否为"客户手机号"
|
||||
- `delete_cars`: 暂不校验
|
||||
|
||||
**校验流程**:
|
||||
1. 读取 Excel 文件第一列
|
||||
2. 检查表头是否符合要求
|
||||
3. 返回校验结果(成功/失败)
|
||||
|
||||
### 2.3 数据管理功能
|
||||
|
||||
#### 2.3.1 品牌批量创建
|
||||
|
||||
**功能描述**: 从 Excel 文件读取品牌名称,批量创建到 F6 系统。
|
||||
|
||||
**主要流程**:
|
||||
1. 从简道云获取任务数据(账号、密码、公司名称、文件路径)
|
||||
2. 登录 F6 系统
|
||||
3. 读取 Excel 文件(第一列为品牌名称)
|
||||
4. 在后台线程中批量创建品牌
|
||||
5. 立即返回"正在执行"提示
|
||||
6. 后台任务完成后更新简道云表单并自动提交工作流
|
||||
|
||||
**后台任务处理**:
|
||||
- 遍历 Excel 文件中的每一行
|
||||
- 调用 F6 API 创建品牌
|
||||
- 记录成功和失败的结果
|
||||
- 执行完成后删除上传的文件
|
||||
- 更新简道云表单的执行明细
|
||||
- 自动提交工作流到下一步
|
||||
|
||||
#### 2.3.2 客户信息修改
|
||||
|
||||
**功能描述**: 从 Excel 文件读取客户修改信息,批量修改 F6 系统中的客户信息。
|
||||
|
||||
**主要流程**:
|
||||
1. 从简道云获取任务数据
|
||||
2. 登录 F6 系统
|
||||
3. 读取 Excel 文件(包含客户手机号、修改字段等)
|
||||
4. 获取所有客户列表
|
||||
5. 获取员工列表(用于匹配专属运营顾问)
|
||||
6. 在后台线程中批量修改客户信息
|
||||
7. 立即返回"正在执行中"提示
|
||||
|
||||
**Excel 文件格式**:
|
||||
- 第一列: 客户手机号(必需,用于匹配)
|
||||
- 其他列: 需要修改的字段(客户姓名、客户类型、客户来源、单位名称、专属运营顾问、客户备注等)
|
||||
|
||||
**匹配逻辑**:
|
||||
- 根据客户手机号匹配 F6 系统中的客户
|
||||
- 获取客户原始信息
|
||||
- 合并 Excel 中的修改字段
|
||||
- 解析地址信息(省市区)
|
||||
- 匹配专属运营顾问的用户ID
|
||||
|
||||
#### 2.3.3 客户信息删除
|
||||
|
||||
**功能描述**: 批量删除 F6 系统中的客户信息。
|
||||
|
||||
**主要流程**:
|
||||
1. 从简道云获取任务数据
|
||||
2. 登录 F6 系统
|
||||
3. 获取所有客户列表
|
||||
4. 获取会员卡列表(提取客户ID)
|
||||
5. 在后台线程中批量删除客户
|
||||
6. 立即返回"正在执行中"提示
|
||||
|
||||
**删除规则**:
|
||||
- 跳过有最近消费时间的客户
|
||||
- 跳过有会员卡的客户
|
||||
- 8-20点之间每3.5秒删除一条,其余时间每1.5秒删除一条
|
||||
- 记录成功和失败次数
|
||||
|
||||
#### 2.3.4 车辆信息删除
|
||||
|
||||
**功能描述**: 批量删除 F6 系统中的客户车辆信息。
|
||||
|
||||
**主要流程**:
|
||||
1. 从简道云获取任务数据
|
||||
2. 登录 F6 系统
|
||||
3. 分页获取所有车辆列表
|
||||
4. 获取会员卡列表(提取车辆ID)
|
||||
5. 在后台线程中批量删除车辆
|
||||
6. 立即返回"正在执行中"提示
|
||||
|
||||
**删除规则**:
|
||||
- 跳过有最近消费时间的客户车辆
|
||||
- 跳过有会员卡的车辆
|
||||
- 8-20点之间每3秒删除一条,其余时间每1秒删除一条
|
||||
- 记录成功和失败次数
|
||||
|
||||
#### 2.3.5 历史记录删除
|
||||
|
||||
**功能描述**: 删除指定门店的历史维修记录。
|
||||
|
||||
**主要流程**:
|
||||
1. 从简道云获取任务数据(账号、密码、公司名称、门店名称)
|
||||
2. 登录 F6 系统
|
||||
3. 获取门店列表并匹配门店ID
|
||||
4. 在后台线程中删除历史维修记录
|
||||
5. 立即返回"正在执行中"提示
|
||||
|
||||
**删除方式**: 调用 F6 系统的删除接口,删除整个门店的历史维修记录。
|
||||
|
||||
### 2.4 BI任务功能
|
||||
|
||||
**功能描述**: 执行 BI 相关的数据处理和报表生成任务。
|
||||
|
||||
**当前状态**: 框架已搭建,具体业务逻辑待实现。
|
||||
|
||||
**预留接口**:
|
||||
- 支持从 Excel 文件读取数据
|
||||
- 支持 F6 系统登录(如需要)
|
||||
- 支持后台线程执行
|
||||
- 支持结果回写到简道云
|
||||
|
||||
### 2.5 工作流自动化
|
||||
|
||||
**功能描述**: 自动获取简道云工作流的待处理任务并提交到下一步。
|
||||
|
||||
**主要流程**:
|
||||
1. 获取工作流实例信息
|
||||
2. 查找状态为待处理(status=0)的任务
|
||||
3. 提取任务信息(username、instance_id、task_id)
|
||||
4. 调用审批接口自动提交
|
||||
5. 记录执行结果
|
||||
|
||||
**应用场景**: 后台任务执行完成后,自动将简道云表单提交到工作流下一步,无需人工干预。
|
||||
|
||||
## 3. 技术架构
|
||||
|
||||
### 3.1 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ 简道云插件前端 │
|
||||
└────────┬────────┘
|
||||
│ HTTP/JSON
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ FastAPI 服务 │
|
||||
│ ┌───────────┐ │
|
||||
│ │ 路由层 │ │
|
||||
│ └─────┬─────┘ │
|
||||
│ │ │
|
||||
│ ┌─────▼─────┐ │
|
||||
│ │ 业务模块 │ │
|
||||
│ │ - F6Module│ │
|
||||
│ │ - Plugin │ │
|
||||
│ └─────┬─────┘ │
|
||||
│ │ │
|
||||
│ ┌─────▼─────┐ │
|
||||
│ │ 任务队列 │ │
|
||||
│ └─────┬─────┘ │
|
||||
│ │ │
|
||||
│ ┌─────▼─────┐ │
|
||||
│ │ 后台任务 │ │
|
||||
│ └───────────┘ │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
▼ ▼
|
||||
┌────────┐ ┌────────┐
|
||||
│ F6系统 │ │ 简道云 │
|
||||
└────────┘ └────────┘
|
||||
```
|
||||
|
||||
### 3.2 核心组件
|
||||
|
||||
#### 3.2.1 应用入口 (main.py)
|
||||
|
||||
**职责**:
|
||||
- 应用初始化和生命周期管理
|
||||
- 模块注册和路由配置
|
||||
- 异常处理器配置
|
||||
- 中间件配置(CORS)
|
||||
|
||||
**关键功能**:
|
||||
- `lifespan`: 管理应用启动和关闭
|
||||
- 全局异常处理
|
||||
- 模块实例化并注册到注册表
|
||||
|
||||
#### 3.2.2 路由层 (app/api/routes.py)
|
||||
|
||||
**职责**:
|
||||
- 定义 API 端点
|
||||
- 请求验证和参数解析
|
||||
- 调用业务模块处理请求
|
||||
- 返回响应
|
||||
|
||||
**主要端点**:
|
||||
- `GET /health`: 健康检查
|
||||
- `POST /webhook`: 业务请求处理
|
||||
|
||||
#### 3.2.3 业务模块层
|
||||
|
||||
**F6Module** (`app/module/module.py`):
|
||||
- F6 系统登录和认证
|
||||
- 公司信息获取
|
||||
- 门店信息获取
|
||||
- 保持连接
|
||||
|
||||
**F6PluginModule** (`app/module/f6_plugin_module.py`):
|
||||
- 文件上传和校验
|
||||
- 品牌批量创建
|
||||
- 客户信息管理(修改、删除)
|
||||
- 车辆信息删除
|
||||
- 历史记录删除
|
||||
- BI任务
|
||||
|
||||
**OtherPluginModule** (`app/module/other_module.py`):
|
||||
- 其他插件功能(如短信签名状态)
|
||||
|
||||
#### 3.2.4 任务处理层
|
||||
|
||||
**任务队列** (`app/utils/app_tools.py`):
|
||||
- 使用 `Queue` 实现任务队列
|
||||
- 后台线程处理任务
|
||||
- 支持同步等待结果
|
||||
|
||||
**后台任务** (`app/tasks/`):
|
||||
- `common.py`: 通用任务函数(更新简道云、工作流审批等)
|
||||
- `brand_tasks.py`: 品牌相关任务
|
||||
- `customer_tasks.py`: 客户相关任务
|
||||
- `delete_tasks.py`: 删除相关任务
|
||||
- `bi_tasks.py`: BI相关任务
|
||||
|
||||
#### 3.2.5 核心工具层
|
||||
|
||||
**模块注册表** (`app/core/module_registry.py`):
|
||||
- 统一管理所有业务操作
|
||||
- 支持操作查询和路由
|
||||
- 存储操作元数据(描述、模块名等)
|
||||
|
||||
**应用工具** (`app/utils/app_tools.py`):
|
||||
- 日志记录器配置(支持轮转)
|
||||
- 任务队列管理
|
||||
- 请求头解码工具
|
||||
- 后台调度器
|
||||
|
||||
**简道云API封装** (`app/api/__init__.py`):
|
||||
- 封装简道云所有API接口
|
||||
- 支持失败重试机制
|
||||
- 字段ID到标签名的替换
|
||||
- 批量操作支持
|
||||
|
||||
### 3.3 数据流
|
||||
|
||||
#### 3.3.1 同步任务流程
|
||||
|
||||
```
|
||||
简道云插件 → FastAPI路由 → 业务模块 → 任务队列 → 处理线程 → 返回结果 → 简道云插件
|
||||
```
|
||||
|
||||
#### 3.3.2 后台任务流程
|
||||
|
||||
```
|
||||
简道云插件 → FastAPI路由 → 业务模块 → 启动后台线程 → 立即返回
|
||||
↓
|
||||
后台任务执行
|
||||
↓
|
||||
更新简道云表单
|
||||
↓
|
||||
自动提交工作流
|
||||
```
|
||||
|
||||
### 3.4 技术选型
|
||||
|
||||
| 技术 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| FastAPI | 0.121.0 | Web框架 |
|
||||
| Uvicorn | 0.38.0 | ASGI服务器 |
|
||||
| Pydantic | - | 数据验证 |
|
||||
| Requests | 2.32.5 | HTTP请求 |
|
||||
| Pandas | 2.3.3 | Excel处理 |
|
||||
| APScheduler | 3.11.1 | 任务调度 |
|
||||
| Pillow | 12.0.0 | 图像处理 |
|
||||
| Pytesseract | 0.3.13 | OCR识别 |
|
||||
|
||||
## 4. 部署运维
|
||||
|
||||
### 4.1 环境要求
|
||||
|
||||
- **操作系统**: Windows/Linux/macOS
|
||||
- **Python版本**: 3.8+
|
||||
- **内存**: 建议 2GB+
|
||||
- **磁盘**: 根据文件存储需求,建议 10GB+
|
||||
|
||||
### 4.2 安装步骤
|
||||
|
||||
1. **克隆代码**:
|
||||
```bash
|
||||
git clone <repository_url>
|
||||
cd fastapi_app
|
||||
```
|
||||
|
||||
2. **安装依赖**:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **配置环境**:
|
||||
- 编辑 `app/config.py`,配置 API Token
|
||||
- 确保目录权限正确(logs、下载文件、模板文件)
|
||||
|
||||
4. **启动服务**:
|
||||
```bash
|
||||
python main.py
|
||||
# 或
|
||||
uvicorn main:app --host 0.0.0.0 --port 5003
|
||||
```
|
||||
|
||||
### 4.3 生产环境部署
|
||||
|
||||
#### 4.3.1 使用 Gunicorn + Uvicorn Workers
|
||||
|
||||
```bash
|
||||
pip install gunicorn
|
||||
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:5003
|
||||
```
|
||||
|
||||
#### 4.3.2 使用 systemd 管理服务(Linux)
|
||||
|
||||
创建服务文件 `/etc/systemd/system/fastapi-app.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=简道云 FastAPI 服务
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your_user
|
||||
WorkingDirectory=/path/to/fastapi_app
|
||||
Environment="PATH=/path/to/venv/bin"
|
||||
ExecStart=/path/to/venv/bin/uvicorn main:app --host 0.0.0.0 --port 5003
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启动服务:
|
||||
```bash
|
||||
sudo systemctl enable fastapi-app
|
||||
sudo systemctl start fastapi-app
|
||||
```
|
||||
|
||||
#### 4.3.3 使用 Nginx 反向代理
|
||||
|
||||
Nginx 配置示例:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your_domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5003;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 监控和日志
|
||||
|
||||
#### 4.4.1 日志管理
|
||||
|
||||
- **日志位置**: `logs/简道云.log`
|
||||
- **日志轮转**: 单文件最大 5MB,保留 5 个备份
|
||||
- **日志级别**: INFO
|
||||
- **日志格式**: `时间戳 级别:模块名:消息`
|
||||
|
||||
#### 4.4.2 健康检查
|
||||
|
||||
定期访问 `/health` 端点检查服务状态:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5003/health
|
||||
```
|
||||
|
||||
#### 4.4.3 性能监控
|
||||
|
||||
建议使用以下工具监控服务性能:
|
||||
- **APM工具**: New Relic、Datadog 等
|
||||
- **日志分析**: ELK Stack、Loki 等
|
||||
- **指标监控**: Prometheus + Grafana
|
||||
|
||||
### 4.5 备份和恢复
|
||||
|
||||
#### 4.5.1 数据备份
|
||||
|
||||
需要备份的内容:
|
||||
- 配置文件 (`app/config.py`)
|
||||
- 日志文件 (`logs/`)
|
||||
- 下载的文件 (`下载文件/`)
|
||||
- 模板文件 (`模板文件/`)
|
||||
|
||||
#### 4.5.2 恢复步骤
|
||||
|
||||
1. 恢复代码和配置文件
|
||||
2. 恢复数据文件
|
||||
3. 重新安装依赖
|
||||
4. 重启服务
|
||||
|
||||
## 5. 安全说明
|
||||
|
||||
### 5.1 认证和授权
|
||||
|
||||
- **API Token**: 使用简道云 API Token 进行认证
|
||||
- **F6系统**: 使用用户名密码登录,密码进行 MD5 加密
|
||||
|
||||
### 5.2 数据安全
|
||||
|
||||
- **敏感信息**: API Token 建议使用环境变量管理
|
||||
- **密码处理**: 密码在传输前进行 MD5 加密
|
||||
- **文件存储**: 上传的文件在处理完成后自动删除
|
||||
|
||||
### 5.3 网络安全
|
||||
|
||||
- **CORS配置**: 生产环境建议限制允许的来源
|
||||
- **HTTPS**: 建议使用 HTTPS 加密传输
|
||||
- **防火墙**: 限制服务端口访问
|
||||
|
||||
### 5.4 安全建议
|
||||
|
||||
1. 定期更新依赖包,修复安全漏洞
|
||||
2. 使用强密码策略
|
||||
3. 定期审查日志,发现异常访问
|
||||
4. 限制文件上传大小和类型
|
||||
5. 实施请求频率限制
|
||||
|
||||
## 6. 故障排查
|
||||
|
||||
### 6.1 常见问题
|
||||
|
||||
#### 问题1: 服务启动失败
|
||||
|
||||
**可能原因**:
|
||||
- 端口被占用
|
||||
- 依赖包未安装
|
||||
- 配置文件错误
|
||||
|
||||
**解决方法**:
|
||||
- 检查端口占用: `netstat -ano | findstr 5003`
|
||||
- 重新安装依赖: `pip install -r requirements.txt`
|
||||
- 检查配置文件格式
|
||||
|
||||
#### 问题2: 登录失败
|
||||
|
||||
**可能原因**:
|
||||
- 用户名密码错误
|
||||
- 公司名称不正确
|
||||
- 验证码识别失败
|
||||
- F6系统服务异常
|
||||
|
||||
**解决方法**:
|
||||
- 验证用户名密码和公司名称
|
||||
- 检查 Tesseract OCR 是否正确安装
|
||||
- 查看日志获取详细错误信息
|
||||
|
||||
#### 问题3: 文件上传失败
|
||||
|
||||
**可能原因**:
|
||||
- 文件格式不正确
|
||||
- 文件路径不存在
|
||||
- 磁盘空间不足
|
||||
- 网络连接问题
|
||||
|
||||
**解决方法**:
|
||||
- 检查文件格式是否符合要求
|
||||
- 确保目录存在且有写权限
|
||||
- 检查磁盘空间
|
||||
- 查看网络连接状态
|
||||
|
||||
#### 问题4: 后台任务未执行
|
||||
|
||||
**可能原因**:
|
||||
- 线程启动失败
|
||||
- 任务执行异常
|
||||
- 简道云API调用失败
|
||||
|
||||
**解决方法**:
|
||||
- 查看日志中的错误信息
|
||||
- 检查简道云API Token是否有效
|
||||
- 验证任务数据格式是否正确
|
||||
|
||||
### 6.2 日志分析
|
||||
|
||||
查看日志文件:
|
||||
```bash
|
||||
tail -f logs/简道云.log
|
||||
```
|
||||
|
||||
搜索错误:
|
||||
```bash
|
||||
grep -i error logs/简道云.log
|
||||
```
|
||||
|
||||
### 6.3 性能优化
|
||||
|
||||
1. **并发处理**: 调整 Uvicorn workers 数量
|
||||
2. **数据库连接池**: 如使用数据库,配置连接池
|
||||
3. **缓存**: 对频繁访问的数据使用缓存
|
||||
4. **异步处理**: 更多使用异步函数提高性能
|
||||
|
||||
## 7. 扩展开发
|
||||
|
||||
### 7.1 添加新功能模块
|
||||
|
||||
1. 在 `app/module/` 下创建新模块文件
|
||||
2. 实现业务逻辑方法
|
||||
3. 在 `main.py` 中注册模块和操作
|
||||
4. 在 `app/schemas.py` 中添加数据模型(如需要)
|
||||
|
||||
### 7.2 添加新的API端点
|
||||
|
||||
1. 在 `app/api/routes.py` 中添加路由
|
||||
2. 使用依赖注入获取所需服务
|
||||
3. 实现业务逻辑
|
||||
4. 返回响应
|
||||
|
||||
### 7.3 添加新的后台任务
|
||||
|
||||
1. 在 `app/tasks/` 下创建任务文件
|
||||
2. 实现后台任务函数
|
||||
3. 在业务模块中调用,启动线程
|
||||
4. 使用 `update_jiandaoyun` 更新结果
|
||||
5. 使用 `approve_workflow` 自动提交工作流
|
||||
|
||||
## 8. 附录
|
||||
|
||||
### 8.1 API接口清单
|
||||
|
||||
| 端点 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/health` | GET | 健康检查 |
|
||||
| `/webhook` | POST | 业务请求处理 |
|
||||
|
||||
### 8.2 操作类型清单
|
||||
|
||||
| 操作名 | 模块 | 类型 |
|
||||
|--------|------|------|
|
||||
| `login_in` | F6Module | 同步 |
|
||||
| `get_company_information` | F6Module | 同步 |
|
||||
| `get_store_information` | F6Module | 同步 |
|
||||
| `keep_alive` | F6Module | 同步 |
|
||||
| `check_file` | F6PluginModule | 同步 |
|
||||
| `create_brand` | F6PluginModule | 后台 |
|
||||
| `delete_history` | F6PluginModule | 后台 |
|
||||
| `delete_customer` | F6PluginModule | 后台 |
|
||||
| `delete_cars` | F6PluginModule | 后台 |
|
||||
| `modify_customer_info` | F6PluginModule | 后台 |
|
||||
| `bi_task` | F6PluginModule | 后台 |
|
||||
| `sms_signature_status` | OtherPluginModule | 同步 |
|
||||
|
||||
### 8.3 配置文件说明
|
||||
|
||||
**app/config.py**:
|
||||
- `BASE_DIR`: 项目根目录
|
||||
- `SAVE_DIRECTORY`: 下载文件目录
|
||||
- `MODE_DIRECTORY`: 模板文件目录
|
||||
- `LOGS_DIRECTORY`: 日志目录
|
||||
- `LOG_FILE`: 日志文件路径
|
||||
- `JIANDAOYUN_API_TOKEN`: 简道云API Token
|
||||
|
||||
### 8.4 目录结构说明
|
||||
|
||||
- `logs/`: 日志文件存储
|
||||
- `下载文件/`: 从简道云下载的文件临时存储
|
||||
- `模板文件/`: 模板文件存储
|
||||
- `app/`: 应用代码目录
|
||||
- `app/api/`: API相关代码
|
||||
- `app/core/`: 核心功能代码
|
||||
- `app/module/`: 业务模块代码
|
||||
- `app/tasks/`: 后台任务代码
|
||||
- `app/utils/`: 工具类代码
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 2.0.0
|
||||
**最后更新**: 2025年
|
||||
**维护团队**: 数据组
|
||||
|
||||
@@ -1,96 +1,224 @@
|
||||
from fastapi import FastAPI, Request
|
||||
"""
|
||||
简道云 FastAPI 服务 - 主应用入口
|
||||
|
||||
本文件是 FastAPI 应用的主入口文件,负责:
|
||||
1. 应用初始化和生命周期管理
|
||||
2. 模块注册和路由配置
|
||||
3. 异常处理器配置
|
||||
4. 中间件配置
|
||||
|
||||
作者: 项目团队
|
||||
版本: 2.0.0
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request, HTTPException, status
|
||||
from fastapi.responses import JSONResponse
|
||||
import json
|
||||
import anyio
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
import logging
|
||||
|
||||
from app.utils.app_tools import AppTools, setup_global_logger
|
||||
from app.module.F6_Plugin_module import F6PluginModule
|
||||
from app.module.f6_plugin_module import F6PluginModule
|
||||
from app.module.module import F6Module
|
||||
from app.module.other_module import OtherPluginModule
|
||||
from app.config import Config
|
||||
from app.schemas import ErrorResponse
|
||||
from app.core import core_manager
|
||||
from app.api.routes import router
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
|
||||
app = FastAPI(title="简道云FastAPI服务")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
"""应用启动时初始化"""
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理 - 启动和关闭"""
|
||||
# 启动时初始化
|
||||
app.state.app_tools = AppTools(Config)
|
||||
app.state.logger = setup_global_logger(Config)
|
||||
app.state.f6_module = F6Module()
|
||||
app.state.f6_plugin_module = F6PluginModule()
|
||||
app.state.other_module = OtherPluginModule()
|
||||
|
||||
# 初始化业务模块
|
||||
f6_module = F6Module()
|
||||
f6_plugin_module = F6PluginModule()
|
||||
other_module = OtherPluginModule()
|
||||
|
||||
# 将模块实例存储到 app.state(用于依赖注入)
|
||||
app.state.f6_module = f6_module
|
||||
app.state.f6_plugin_module = f6_plugin_module
|
||||
app.state.other_module = other_module
|
||||
|
||||
# 注册模块到 registry
|
||||
core_manager.initialize_modules({
|
||||
'f6_module': f6_module,
|
||||
'f6_plugin_module': f6_plugin_module,
|
||||
'other_module': other_module
|
||||
})
|
||||
|
||||
# 注册所有操作到 module_registry
|
||||
core_manager.register_action('login_in', f6_module.accept_login_message, 'f6_module',
|
||||
description='F6系统登录')
|
||||
core_manager.register_action('get_company_information', f6_module.get_company_information, 'f6_module',
|
||||
description='获取公司信息')
|
||||
core_manager.register_action('get_store_information', f6_module.get_store_information, 'f6_module',
|
||||
description='获取门店信息')
|
||||
core_manager.register_action('keep_alive', f6_module.get_keep_heart, 'f6_module',
|
||||
description='保持连接')
|
||||
core_manager.register_action('check_file', f6_plugin_module.check_file, 'f6_plugin_module',
|
||||
description='校验上传文件')
|
||||
core_manager.register_action('create_brand', f6_plugin_module.create_brand, 'f6_plugin_module',
|
||||
description='创建品牌')
|
||||
core_manager.register_action('delete_history', f6_plugin_module.delete_history, 'f6_plugin_module',
|
||||
description='删除历史记录')
|
||||
core_manager.register_action('delete_customer', f6_plugin_module.delete_customer, 'f6_plugin_module',
|
||||
description='删除客户')
|
||||
core_manager.register_action('delete_cars', f6_plugin_module.delete_cars, 'f6_plugin_module',
|
||||
description='删除车辆')
|
||||
core_manager.register_action('sms_signature_status', other_module.sms_signature_status, 'other_module',
|
||||
description='短信签名状态')
|
||||
core_manager.register_action('modify_customer_info', f6_plugin_module.modify_customer_info, 'f6_plugin_module',
|
||||
description='修改客户信息')
|
||||
core_manager.register_action('disable_project', f6_plugin_module.disable_projects, 'f6_plugin_module',
|
||||
description='项目信息批量启停')
|
||||
core_manager.register_action('batch_modify_materials', f6_plugin_module.modify_material, 'f6_plugin_module',
|
||||
description='材料信息批量修改')
|
||||
core_manager.register_action('batch_modify_projects', f6_plugin_module.modify_project, 'f6_plugin_module',
|
||||
description='项目信息批量修改')
|
||||
core_manager.register_action('bi_task', f6_plugin_module.bi_task, 'f6_plugin_module',
|
||||
description='BI任务')
|
||||
|
||||
app.state.logger.info("应用启动完成,已注册所有操作")
|
||||
|
||||
yield
|
||||
|
||||
# 关闭时清理资源
|
||||
if hasattr(app.state, 'app_tools') and hasattr(app.state.app_tools, 'scheduler'):
|
||||
app.state.app_tools.scheduler.shutdown(wait=False)
|
||||
app.state.logger.info("应用关闭")
|
||||
|
||||
app = FastAPI(
|
||||
title="简道云FastAPI服务",
|
||||
description="简道云插件后端服务,提供数据同步和处理功能",
|
||||
version="2.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 注册路由
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
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
|
||||
@app.get("/", tags=["系统"])
|
||||
async def root():
|
||||
"""
|
||||
根路径端点
|
||||
|
||||
返回服务基本信息和可用端点
|
||||
"""
|
||||
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,
|
||||
"service": "简道云FastAPI服务",
|
||||
"version": "2.0.0",
|
||||
"status": "running",
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"webhook": "/webhook",
|
||||
"docs": "/docs",
|
||||
"redoc": "/redoc"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request):
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
"""
|
||||
接受前端请求后将任务放入消息队列
|
||||
HTTP 异常处理器
|
||||
|
||||
处理所有 HTTPException 异常,返回统一的错误响应格式。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
exc: HTTPException 异常对象
|
||||
|
||||
Returns:
|
||||
any: 返回任务处理的结果
|
||||
JSONResponse: 包含错误详情的 JSON 响应
|
||||
"""
|
||||
logger = app.state.logger
|
||||
app_tools: AppTools = app.state.app_tools
|
||||
logger = getattr(app.state, 'logger', None)
|
||||
if logger:
|
||||
logger.error(f"HTTP异常: {exc.status_code} - {exc.detail}")
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=ErrorResponse(
|
||||
detail=exc.detail or "HTTP error",
|
||||
error_code=f"HTTP_{exc.status_code}"
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
# 获取请求数据
|
||||
data = await request.json()
|
||||
header = request.headers
|
||||
|
||||
# 解码请求头
|
||||
decoded_header = app_tools.decode_headers(header)
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
"""
|
||||
请求验证异常处理器
|
||||
|
||||
处理 Pydantic 数据验证失败的情况,返回详细的验证错误信息。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
exc: RequestValidationError 异常对象
|
||||
|
||||
Returns:
|
||||
JSONResponse: 包含验证错误详情的 JSON 响应
|
||||
"""
|
||||
logger = getattr(app.state, 'logger', None)
|
||||
if logger:
|
||||
logger.warning(f"请求验证失败: {exc.errors()}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content=ErrorResponse(
|
||||
detail="请求数据验证失败",
|
||||
error_code="VALIDATION_ERROR"
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
# 获取操作映射表
|
||||
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)
|
||||
@app.exception_handler(Exception)
|
||||
async def general_exception_handler(request: Request, exc: Exception):
|
||||
"""
|
||||
通用异常处理器
|
||||
|
||||
捕获所有未处理的异常,防止应用崩溃,并记录详细的错误信息。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
exc: 异常对象
|
||||
|
||||
Returns:
|
||||
JSONResponse: 包含错误详情的 JSON 响应
|
||||
"""
|
||||
logger = getattr(app.state, 'logger', None)
|
||||
if logger:
|
||||
logger.error(f"未处理的异常: {type(exc).__name__} - {str(exc)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=ErrorResponse(
|
||||
detail="服务器内部错误",
|
||||
error_code="INTERNAL_ERROR"
|
||||
).model_dump(),
|
||||
)
|
||||
# 路由已移动到 app/api/routes.py
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""
|
||||
直接运行入口
|
||||
|
||||
当直接运行此文件时,启动 uvicorn 服务器。
|
||||
默认配置:
|
||||
- 主机: 0.0.0.0 (监听所有网络接口)
|
||||
- 端口: 5000
|
||||
- 热重载: 关闭 (生产环境建议关闭)
|
||||
"""
|
||||
import uvicorn
|
||||
uvicorn.run("fastapi_app.main:app", host="0.0.0.0", port=5003, reload=False)
|
||||
uvicorn.run(app, host="0.0.0.0", port=5000, reload=False)
|
||||
|
||||
+8
-7
@@ -1,11 +1,12 @@
|
||||
anyio==4.11.0
|
||||
apscheduler==3.11.1
|
||||
fastapi==0.121.0
|
||||
log_config==2.1.1
|
||||
numpy==2.3.4
|
||||
anyio==4.12.0
|
||||
apscheduler==3.11.2
|
||||
fastapi==0.128.0
|
||||
numpy==2.4.0
|
||||
pandas==2.3.3
|
||||
Pillow==12.0.0
|
||||
Pillow==12.1.0
|
||||
pydantic==2.12.5
|
||||
pytesseract==0.3.13
|
||||
Requests==2.32.5
|
||||
tqdm==4.67.1
|
||||
uvicorn==0.38.0
|
||||
uvicorn==0.40.0
|
||||
openpyxl
|
||||
@@ -0,0 +1,54 @@
|
||||
@echo off
|
||||
title 简道云监听服务 端口5000
|
||||
|
||||
:: 切换到应用所在目录(与原始脚本一致)
|
||||
cd /d "C:\Users\Administrator\Desktop\简道云\简道云"
|
||||
|
||||
:: 激活Anaconda基础环境(或你指定的环境)
|
||||
call C:\ProgramData\anaconda3\Scripts\activate.bat
|
||||
if %errorlevel% neq 0 (
|
||||
echo 激活Anaconda环境失败,请检查环境配置。
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 验证Python是否可用
|
||||
python --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 无法找到Python,请检查Anaconda环境是否正确安装。
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 验证uvicorn是否已安装(可选但推荐)
|
||||
python -c "import uvicorn" >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误:未检测到 uvicorn。请运行以下命令安装:
|
||||
echo conda install -c conda-forge uvicorn fastapi
|
||||
echo 或
|
||||
echo pip install "uvicorn[standard]" fastapi
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 设置默认参数(允许通过环境变量覆盖)
|
||||
if "%HOST%"=="" set HOST=0.0.0.0
|
||||
if "%PORT%"=="" set PORT=5000
|
||||
if "%WORKERS%"=="" set WORKERS=1
|
||||
if "%LOG_LEVEL%"=="" set LOG_LEVEL=info
|
||||
|
||||
set APP_MODULE=main:app
|
||||
|
||||
echo.
|
||||
echo 启动简道云 FastAPI 服务...
|
||||
echo 模块: %APP_MODULE%
|
||||
echo 地址: http://%HOST%:%PORT%
|
||||
echo 进程数: %WORKERS% (Windows建议设为1)
|
||||
echo 日志级别: %LOG_LEVEL%
|
||||
echo.
|
||||
|
||||
:: 使用 python -m uvicorn 确保调用当前环境中的 uvicorn
|
||||
python -m uvicorn %APP_MODULE% --host %HOST% --port %PORT% --workers %WORKERS% --log-level %LOG_LEVEL%
|
||||
|
||||
:: 服务退出后暂停,便于查看日志
|
||||
pause
|
||||
@@ -0,0 +1,49 @@
|
||||
import requests
|
||||
|
||||
cookies = {
|
||||
'memberSESSIONID': '43f77327-7a6b-4844-a1c6-d1acd7e0c970',
|
||||
'erpLanguage': 'zh-CN',
|
||||
'prodOrg': '11240984669917217520',
|
||||
'unp': '15865484595890778191',
|
||||
'un': '15865484595890778191',
|
||||
'_up': '-NillNN-qyBEJ--t3vnSknvoOF53y_SJuMkA2n43U-daUfnArpjQjaZJ9Q3d-WrAAGgt60MgQHajHWBHMKKxj0CuWypi1JgKCFP1EPEk-HbqEvcTrYkr0wcI-fBRv-ZNHu3M-GTc1p60EX-sq-RQgeIal1HLPxpurEj9mEe9rIrrcGQ.',
|
||||
'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2215865484595890778191%22%2C%22first_id%22%3A%2219a48e066e68e2-067b1e693596828-4c657b58-2073600-19a48e066e71500%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22%24device_id%22%3A%2219a48e066e68e2-067b1e693596828-4c657b58-2073600-19a48e066e71500%22%7D',
|
||||
'tmall': 'false',
|
||||
'Hm_lvt_25f5e7a3a5dbb293d7dd35d5f1be8d0a': '1764311728,1764643482,1764662823,1764742943',
|
||||
'Hm_lpvt_25f5e7a3a5dbb293d7dd35d5f1be8d0a': '1764742943',
|
||||
'HMACCOUNT': '55F2182717FD6AE6',
|
||||
}
|
||||
|
||||
headers = {
|
||||
'accept': 'application/json, text/plain, */*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
||||
'cache-control': 'no-cache',
|
||||
'pragma': 'no-cache',
|
||||
'priority': 'u=1, i',
|
||||
'referer': 'https://yunxiu.f6car.cn/erp/view/index.html',
|
||||
'sec-ch-ua': '"Chromium";v="142", "Microsoft Edge";v="142", "Not_A Brand";v="99"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'traceparent': '00-b018ff54506f4db58f1c139e9cb30525-22df753b9da6e050-01',
|
||||
'tracestate': 'rum=v2&browser&dz2uw0c5ay@e5930ea8eb782ae&273f4c9d3aeb40c586714fd3270a28e9&uid_48oaftj52d5ybkwt',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0',
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
# 'cookie': 'memberSESSIONID=43f77327-7a6b-4844-a1c6-d1acd7e0c970; erpLanguage=zh-CN; prodOrg=11240984669917217520; unp=15865484595890778191; un=15865484595890778191; _up=-NillNN-qyBEJ--t3vnSknvoOF53y_SJuMkA2n43U-daUfnArpjQjaZJ9Q3d-WrAAGgt60MgQHajHWBHMKKxj0CuWypi1JgKCFP1EPEk-HbqEvcTrYkr0wcI-fBRv-ZNHu3M-GTc1p60EX-sq-RQgeIal1HLPxpurEj9mEe9rIrrcGQ.; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2215865484595890778191%22%2C%22first_id%22%3A%2219a48e066e68e2-067b1e693596828-4c657b58-2073600-19a48e066e71500%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22%24device_id%22%3A%2219a48e066e68e2-067b1e693596828-4c657b58-2073600-19a48e066e71500%22%7D; tmall=false; Hm_lvt_25f5e7a3a5dbb293d7dd35d5f1be8d0a=1764311728,1764643482,1764662823,1764742943; Hm_lpvt_25f5e7a3a5dbb293d7dd35d5f1be8d0a=1764742943; HMACCOUNT=55F2182717FD6AE6',
|
||||
}
|
||||
|
||||
params = {
|
||||
'pageSize': '10',
|
||||
'pageNo': '1',
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://yunxiu.f6car.cn/member/customer/listForPermission',
|
||||
params=params,
|
||||
cookies=cookies,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
print(response.json())
|
||||
@@ -1,97 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from queue import Queue
|
||||
import threading
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from urllib.parse import unquote
|
||||
|
||||
|
||||
class AppTools:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.task_queue = Queue()
|
||||
self.logger = self._setup_logger()
|
||||
self.scheduler = self._setup_scheduler()
|
||||
self._start_task_thread()
|
||||
|
||||
def _setup_logger(self):
|
||||
"""配置日志记录器(不依赖 Flask)。"""
|
||||
log_dir = self.config.LOGS_DIRECTORY
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
log_file = self.config.LOG_FILE
|
||||
|
||||
logger = logging.getLogger("app")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# 防止重复添加 handler
|
||||
if not any(isinstance(h, RotatingFileHandler) and getattr(h, 'baseFilename', None) == str(log_file)
|
||||
for h in logger.handlers):
|
||||
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024 * 5, backupCount=5, encoding='utf-8')
|
||||
file_handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s:%(name)s:%(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
def _setup_scheduler(self):
|
||||
"""配置后台调度器"""
|
||||
scheduler = BackgroundScheduler()
|
||||
# 如需定时任务可在此添加
|
||||
import atexit
|
||||
atexit.register(lambda: scheduler.shutdown(wait=False))
|
||||
return scheduler
|
||||
|
||||
def _start_task_thread(self):
|
||||
"""启动任务处理线程"""
|
||||
task_thread = threading.Thread(target=self.process_tasks, daemon=True)
|
||||
task_thread.start()
|
||||
|
||||
def process_tasks(self):
|
||||
"""处理任务队列中的任务"""
|
||||
while True:
|
||||
task = self.task_queue.get()
|
||||
if task is None:
|
||||
self.logger.error("任务处理线程已终止")
|
||||
break
|
||||
try:
|
||||
result = task['handler'](task['data'])
|
||||
task['response'].put(result)
|
||||
except Exception as e:
|
||||
self.logger.error(f"任务执行失败: {str(e)}")
|
||||
task['response'].put({'msg': f'任务执行失败: {str(e)}'})
|
||||
finally:
|
||||
self.task_queue.task_done()
|
||||
self.logger.info("任务处理完成")
|
||||
|
||||
def enqueue_task(self, handler, data):
|
||||
"""将任务放入消息队列"""
|
||||
response_queue = Queue()
|
||||
self.task_queue.put({
|
||||
'handler': handler,
|
||||
'data': data,
|
||||
'response': response_queue
|
||||
})
|
||||
return response_queue
|
||||
|
||||
@staticmethod
|
||||
def decode_headers(headers):
|
||||
"""解码包含中文字符的 HTTP 请求头"""
|
||||
return {key: unquote(value, encoding='utf-8') for key, value in headers.items()}
|
||||
|
||||
|
||||
# 简易的全局日志记录器设置(与原项目解耦)
|
||||
logger = None
|
||||
|
||||
|
||||
def setup_global_logger(config):
|
||||
global logger
|
||||
if logger is None:
|
||||
app_tools = AppTools(config)
|
||||
logger = app_tools.logger
|
||||
return logger
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user