简道云fastapi

This commit is contained in:
z66
2025-11-07 17:48:49 +08:00
commit 073f0646a1
30 changed files with 5933 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
# API模块更新说明
## 更新内容
根据 `fastapi_app/api.py`(最新版)更新了 `fastapi_app/app/api.py`,主要更新包括:
### 1. 添加失败重试机制
所有API函数都已添加重试机制,参考 `entry_data_list` 的实现模式:
- **默认重试次数**:20次(部分函数为10次)
- **重试逻辑**
- 捕获 `requests.exceptions.RequestException` 异常
- 重试前等待时间:0.1秒(快速请求)或3-10秒(慢速请求)
- 超过最大重试次数后记录错误日志并返回None或抛出异常
### 2. 新增功能函数
添加了以下新函数(原版本中没有的):
- `entry_data_banch_update()` - 批量修改数据
- `entry_data_delete()` - 删除单条数据
- `entry_data_batch_delete()` - 批量删除数据
- `workflow_task_hand_over()` - 流程待办转交
- `get_upload_token()` - 获取文件上传凭证
- `upload_file()` - 上传文件
### 3. 功能增强
- **`entry_data_get()`**
- 添加 `replace` 参数(默认True,保持向后兼容)
- 添加 `max_retries` 参数
- 添加重试机制
- **`entry_data_list()`**
- 添加 `replace` 参数(默认True,保持向后兼容)
- 添加 `max_retries` 参数
- 改进重试逻辑,支持分页重试
- **`entry_widget_list()`**
- 添加 `max_retries` 参数
- 添加重试机制
- **`data_batch_create()`**
- 添加 `max_retries` 参数
- 添加重试机制
- 支持 `is_start_workflow``is_start_trigger``transaction_id` 参数
- **`entry_data_batch_create()`**
- 添加 `max_retries` 参数
- 添加重试机制
- 支持 `is_start_workflow``is_start_trigger` 参数
- 使用 `NpEncoder` 处理NumPy数据类型
- **`entry_data_update()`**
- 添加 `max_retries` 参数
- 添加重试机制
- **`workflow_instance_get()`**
- 添加 `max_retries` 参数
- 添加重试机制
- **`workflow_task_approve()`**
- 添加 `max_retries` 参数
- 添加重试机制
- `comment` 参数支持自定义(默认"自动转交")
### 4. 工具函数
- **`NpEncoder`**:JSON编码器,处理NumPy数据类型
- **`replace_decimals()`**:递归替换Decimal类型为float
### 5. 字段替换优化
`field_replacement()` 方法使用递归实现,更优雅地处理嵌套数据结构。
## 兼容性保证
### 向后兼容
1. **默认参数**
- `entry_data_get()``entry_data_list()``replace` 参数默认为 `True`,保持原有行为
- 所有新增的 `max_retries` 参数都有合理的默认值
2. **导入路径**
- 使用 `from app.config import Config`(而非 `from config import Config`
- 使用 `logging.getLogger('app')`(而非 `log_config`
3. **函数签名**
- 所有原有函数的调用方式保持不变
- 新增参数都是可选参数
### 日志系统
- 使用 `logging.getLogger('app')` 作为常规日志记录器
- 使用 `logging.getLogger('app.error')` 作为错误日志记录器
- 与 fastapi_app 项目的日志系统完全兼容
## 使用示例
### 基本使用(保持原有方式)
```python
from app.api import API
api = API()
# 获取单条数据(自动替换字段)
data = api.entry_data_get({
'api_key': 'xxx',
'entry_id': 'xxx',
'data_id': 'xxx'
})
# 获取多条数据(自动替换字段)
data_list = api.entry_data_list({
'api_key': 'xxx',
'entry_id': 'xxx'
})
```
### 使用新功能
```python
# 不替换字段
data = api.entry_data_get({
'api_key': 'xxx',
'entry_id': 'xxx',
'data_id': 'xxx'
}, replace=False)
# 自定义重试次数
data = api.entry_data_get({
'api_key': 'xxx',
'entry_id': 'xxx',
'data_id': 'xxx'
}, max_retries=10)
# 批量删除
result = api.entry_data_batch_delete({
'api_key': 'xxx',
'entry_id': 'xxx',
'data_ids': ['id1', 'id2', 'id3']
}, chunk_size=90, max_retries=20)
```
## 重试机制说明
### 重试策略
1. **快速请求**0.1秒延迟):
- `entry_data_list()` - 分页请求
- `entry_data_batch_create()` - 批量创建
- `entry_data_batch_delete()` - 批量删除
- `workflow_instance_get()` - 流程查询
2. **慢速请求**3-10秒延迟):
- `data_batch_create()` - 3秒延迟
- `entry_data_update()` - 10秒延迟
- `entry_data_banch_update()` - 10秒延迟
- `entry_data_delete()` - 10秒延迟
- `workflow_task_approve()` - 3秒延迟
- `workflow_task_hand_over()` - 3秒延迟
- `get_upload_token()` - 3秒延迟
- `upload_file()` - 3秒延迟
### 错误处理
- 所有重试失败的操作都会记录到错误日志
- 部分函数在重试失败后返回 `None`,部分会抛出异常
- 错误日志包含失败的任务标识信息
## 注意事项
1. **超时设置**:所有请求都设置了 `timeout=10`
2. **状态码检查**:使用 `res.raise_for_status()` 检查HTTP状态码
3. **文件上传**`upload_file()` 使用 `with` 语句确保文件正确关闭
4. **数据类型处理**:批量操作函数使用 `NpEncoder``replace_decimals()` 处理特殊数据类型
## 测试建议
1. 测试所有原有功能的兼容性
2. 测试新添加的函数
3. 测试重试机制(可以模拟网络错误)
4. 测试 `replace=False` 参数的使用
+2
View File
@@ -0,0 +1,2 @@
__all__ = []
+793
View File
@@ -0,0 +1,793 @@
"""
API 模块 - 简道云API接口封装
支持失败重试机制,兼容现有代码
"""
import requests
import json
import time
import logging
from typing import Optional, List, Dict, Any
from decimal import Decimal
import numpy as np
from app.config import Config
# 获取日志记录器
logger = logging.getLogger('app')
error_logger = logging.getLogger('app.error') # 错误日志记录器
class NpEncoder(json.JSONEncoder):
"""NumPy数据类型JSON编码器"""
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
else:
return super(NpEncoder, self).default(obj)
def replace_decimals(obj):
"""递归替换Decimal类型为float"""
if isinstance(obj, dict):
return {k: replace_decimals(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [replace_decimals(item) for item in obj]
elif isinstance(obj, Decimal):
return float(obj)
return obj
class API:
def entry_data_get(self, data: dict, replace: bool = True, max_retries: int = 20) -> Dict:
"""
获取单条表单数据
:param replace: 是否替换字段,默认为True(保持向后兼容)
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息
:return: 表单数据
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/get'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_id": data['data_id']
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
print(data_get)
if replace:
data_get = self.field_replacement(data, data_get)
return data_get
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
if retries <= max_retries:
time.sleep(0.1)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_id')} 连续{max_retries}次请求失败,放弃此次请求。")
raise Exception(f"获取单条表单数据失败,已重试{max_retries}")
def entry_data_list(self, data: dict, replace: bool = True, max_retries: int = 20) -> Dict:
"""
获取多条表单数据
:param max_retries: 最大重试次数
:param replace: 是否替换字段,默认为True(保持向后兼容)
:param data: 简道云插件发送过来的data,包含应用id、表单id等信息
:return: 表单数据列表
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
all_data_batches = []
last_data_id = None
exit_flag = False
while True:
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"limit": 100,
"data_id": last_data_id
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if data_get.get("data"):
all_data_batches.extend(data_get['data'])
last_data_id = data_get['data'][-1].get('_id')
print(f"已获取 {len(all_data_batches)} 条数据")
break
else:
if 'data' not in data_get or len(data_get['data']) == 0:
exit_flag = True
break
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(0.1)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1)
if retries > max_retries:
error_logger.error(f"任务 {last_data_id}组 连续{max_retries}次请求失败,放弃此次请求。")
all_data_batches.append(None)
if exit_flag:
break
final_data = {
'data': all_data_batches
}
logger.info(f"获取了{len(all_data_batches)}条数据")
if replace:
print("进行了替换")
return self.field_replacement(data, final_data)
else:
return final_data
@staticmethod
def entry_widget_list(data: dict, max_retries: int = 20) -> Optional[Dict[str, Any]]:
"""
获取表单字段
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id、表单id等信息
:return: 表单字段信息
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/widget/list'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
return res.json()
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
if retries <= max_retries:
time.sleep(0.1)
if retries > max_retries:
error_logger.error(f"获取表单字段失败,已重试{max_retries}")
return None
def field_replacement(self, data: dict, data_get: dict) -> dict:
"""
字段替换,将id替换为标签名,即唯一值替换为表单中显示字段的名字
:param data: 简道云插件发送过来的data,包含表单id、数据id、应用id
:param data_get: 简道云请求的数据,一般是根据数据id获取到表单的数据
:return: 替换后的数据
"""
widget_list = self.entry_widget_list(data)
if not widget_list or 'widgets' not in widget_list or not isinstance(widget_list['widgets'], list):
raise ValueError("映射表没有接受到数据")
name_to_label = {widget['name']: widget['label'] for widget in widget_list['widgets']}
def replace_keys(obj):
"""递归替换字典中的键名"""
if isinstance(obj, dict):
new_dict = {}
for key, value in obj.items():
new_key = name_to_label.get(key, key)
new_dict[new_key] = replace_keys(value)
return new_dict
elif isinstance(obj, list):
return [replace_keys(item) for item in obj]
else:
return obj
data_get_copy = json.loads(json.dumps(data_get))
if 'data' in data_get_copy:
data_get_copy['data'] = replace_keys(data_get_copy['data'])
return data_get_copy
@staticmethod
def data_batch_create(data: dict, max_retries: int = 20) -> Optional[Dict]:
"""
新建单条表单数据
:param max_retries: 最大重试次数
:param data: 应该包含应用id、表单id,以及新建的数据data['data']
:return: 返回创建后简道云返回的信息
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/create'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data": data['data'],
"is_start_workflow": data.get('is_start_workflow', "false"),
"is_start_trigger": data.get('is_start_trigger', "false"),
"transaction_id": data.get('transaction_id', "")
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
return data_get
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data')} 连续{max_retries}次请求失败,放弃此次请求。")
return None
@staticmethod
def entry_data_batch_create(
data: dict,
chunk_size: int = 90,
max_retries: int = 20
) -> List[Optional[Dict]]:
"""
新建多条数据
:param max_retries: 最大重试次数
:param data: 应包含数据id、表单id、以及需要新建的信息,新建信息应该是一个列表
:param chunk_size: 简道云限制批量新建一次最多100条,这里默认值设置为90条一次
:return: 返回请求后的结果
"""
data = replace_decimals(data)
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_create'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
total_length = len(data['data_list'])
logger.info(f"多数据写入行数: {total_length}")
num_chunks = (total_length + chunk_size - 1) // chunk_size
data_get_list = []
for i in range(num_chunks):
start_index = i * chunk_size
end_index = min(start_index + chunk_size, total_length)
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_list": data['data_list'][start_index:end_index],
"is_start_workflow": data.get('is_start_workflow', "false"),
"is_start_trigger": data.get('is_start_trigger', "false"),
}, cls=NpEncoder)
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if data_get.get("status") == "success":
data_get_list.append(data_get)
break
else:
logger.warning(f"请求异常,将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1)
if retries > max_retries:
error_logger.error(
f"任务 {data['data_list'][start_index:end_index]} 连续{max_retries}次请求失败,放弃此次请求。")
data_get_list.append(None)
return data_get_list
@staticmethod
def entry_data_update(data: dict, max_retries: int = 20) -> dict:
"""
修改数据
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息
:return: 修改数据后简道云返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/update'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_id": data['data_id'],
"data": data['data']
})
data_get = None
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(10)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_id')} 连续{max_retries}次请求失败,放弃此次请求。")
return data_get
@staticmethod
def entry_data_banch_update(data: dict, max_retries: int = 20, chunk_size: int = 90) -> List[dict]:
"""
批量修改数据
:param chunk_size: 批量修改块大小
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息
:return: 修改数据后简道云返回的结果列表
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_update'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
total_length = len(data['data_ids'])
logger.info(f"多数据修改行数: {total_length}")
num_chunks = (total_length + chunk_size - 1) // chunk_size
data_get_list = []
for i in range(num_chunks):
start_index = i * chunk_size
end_index = min(start_index + chunk_size, total_length)
payload = {
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_ids": data['data_ids'][start_index:end_index],
"data": data['data']
}
if "transaction_id" in data:
payload["transaction_id"] = data["transaction_id"]
payload = json.dumps(payload, cls=NpEncoder)
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
data_get_list.append(data_get)
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(10)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_ids')} 连续{max_retries}次请求失败,放弃此次请求。")
continue
return data_get_list
@staticmethod
def entry_data_delete(data: dict, max_retries: int = 20) -> dict:
"""
删除单条数据
:param data: 应包含应用ID、表单ID、数据ID
:param max_retries: 最大重试次数,默认20
:return: 删除结果
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/delete'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_id": data['data_id'],
})
retries = 0
delete_status = None
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
delete_status = res.json()
# 手动处理状态码 4001(数据不存在)
if delete_status == {
"code": 4001,
"msg": "Data does not exist."
}:
logger.info(f"返回结果: {delete_status}")
break
res.raise_for_status()
if res.status_code == 200:
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(10)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_id')} 连续{max_retries}次请求失败,放弃此次请求。")
continue
return delete_status
@staticmethod
def entry_data_batch_delete(
data: dict,
chunk_size: int = 90,
max_retries: int = 20
) -> List[Optional[Dict]]:
"""
批量删除数据
:param data: 应包含应用ID、表单ID、数据ID列表
:param chunk_size: 单次删除最大条数,默认90
:param max_retries: 重试次数,默认20
:return: 删除结果列表
"""
data = replace_decimals(data)
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_delete'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
total_length = len(data['data_ids'])
logger.info(f"多数据删除行数: {total_length}")
num_chunks = (total_length + chunk_size - 1) // chunk_size
data_get_list = []
for i in range(num_chunks):
start_index = i * chunk_size
end_index = min(start_index + chunk_size, total_length)
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"data_ids": data['data_ids'][start_index:end_index],
}, cls=NpEncoder)
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
logger.info(f"{i}页 返回结果: {data_get}")
if data_get.get("status") == "success":
data_get_list.append(data_get)
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1)
if retries > max_retries:
error_logger.error(
f"批量删除任务第{i+1}批 连续{max_retries}次请求失败,放弃此次请求。")
data_get_list.append(None)
return data_get_list
@staticmethod
def workflow_instance_get(data: dict, max_retries: int = 20) -> dict:
"""
查询实例流程信息
:param max_retries: 最大重试次数
:param data: 简道云插件发送过来的data,包含应用id
:return: 查询简道云流程实例信息返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v5/workflow/instance/get'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"instance_id": data['data_id'],
"tasks_type": 1
})
data_get = None
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
data_get = res.json()
if res.status_code == 200:
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1)
if retries > max_retries:
error_logger.error(f"任务 {data.get('data_id')} 连续{max_retries}次请求失败,放弃此次请求。")
return data_get
@staticmethod
def workflow_task_approve(data: dict, max_retries: int = 20) -> dict:
"""
流程待办提交
:param max_retries: 最大重试次数
:param data: 应包含username、instance_id(data_id)、task_id等信息
:return: 返回简道云流程待办提交的结果
"""
url = 'https://api.jiandaoyun.com/api/v1/workflow/task/approve'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"username": data["username"],
"instance_id": data["instance_id"],
"task_id": data['task_id'],
"comment": data.get("comment", "自动转交")
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
if res.status_code == 200:
return res.json()
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"任务 {data.get('task_id')} 连续{max_retries}次请求失败,放弃此次请求。")
return {}
@staticmethod
def workflow_task_hand_over(data: dict, max_retries: int = 10) -> Optional[dict]:
"""
流程待办转交
:param max_retries: 最大重试次数
:param data: 应包含username、instance_id(data_id)、task_id、transfer_username等信息
:return: 返回简道云流程待办转交的结果
"""
url = 'https://api.jiandaoyun.com/api/v1/workflow/task/transfer'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"username": data["username"],
"instance_id": data["instance_id"],
"task_id": data['task_id'],
"transfer_username": data['transfer_username'],
"comment": data.get("comment", "转交")
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
if res.status_code == 200:
return res.json()
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"任务转交失败,已重试{max_retries}")
return None
@staticmethod
def get_upload_token(data: dict, max_retries: int = 10) -> Optional[Dict[str, Any]]:
"""
获取文件上传凭证
:param max_retries: 最大重试次数
:param data: 应包含应用ID、表单ID、事务ID
:return: 返回upload_url、upload_token
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/file/get_upload_token'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'],
"entry_id": data['entry_id'],
"transaction_id": data['transaction_id'],
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status()
res_j = res.json()
# 检查 token_and_url_list 是否存在且不为空
token_list = res_j.get('token_and_url_list', [])
if not token_list or len(token_list) == 0:
logger.warning(f"未获取到上传凭证列表,将重新请求")
retries += 1
time.sleep(3)
continue
token_item = token_list[0]
upload_url = token_item.get('url')
upload_token = token_item.get('token')
if not upload_url or not upload_token:
logger.warning(f"上传凭证信息不完整,将重新请求")
retries += 1
time.sleep(3)
continue
logger.info(f"返回结果: {upload_url}, {upload_token}")
if res.status_code == 200:
return {
'upload_url': upload_url,
'upload_token': upload_token
}
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except (requests.exceptions.RequestException, KeyError, IndexError, TypeError) as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"获取上传凭证失败,已重试{max_retries}")
return None
@staticmethod
def upload_file(data: dict, max_retries: int = 10) -> Optional[Any]:
"""
上传文件
:param max_retries: 最大重试次数
:param data: 应包含上传文件路径、上传文件url、上传文件token
:return: 返回上传文件结果
"""
url = data['upload_url']
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN,
}
file_path = data['file_path']
payload = {
"token": data['upload_token'],
}
retries = 0
result = None
while retries <= max_retries:
try:
with open(file_path, 'rb') as f:
files = {"file": f}
res = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10)
res.raise_for_status()
data_get = res.json()
logger.info(f"返回结果: {data_get}")
if res.status_code == 200:
result = data_get
break
else:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3)
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(3)
if retries > max_retries:
error_logger.error(f"上传文件失败,已重试{max_retries}")
return result
+24
View File
@@ -0,0 +1,24 @@
"""
后台任务模块 - 向后兼容入口
此文件保持向后兼容,实际功能已拆分到 app.tasks 模块中
"""
# 从新的 tasks 模块导入所有函数,保持向后兼容
from app.tasks import (
update_jiandaoyun,
approve_workflow,
create_brand_background,
delete_history_background,
delete_customer_background,
delete_car_background,
modify_customer_info_background,
)
__all__ = [
'update_jiandaoyun',
'approve_workflow',
'create_brand_background',
'delete_history_background',
'delete_customer_background',
'delete_car_background',
'modify_customer_info_background',
]
+34
View File
@@ -0,0 +1,34 @@
from pathlib import Path
# 获取当前文件所在的目录
BASE_DIR = Path(__file__).resolve().parent.parent # 项目根目录
# 构建保存下载文件的目录路径
SAVE_DIRECTORY = BASE_DIR / '下载文件'
# 构建保存模板文件的目录路径
MODE_DIRECTORY = BASE_DIR / '模板文件'
# 构建日志文件的目录路径
LOGS_DIRECTORY = BASE_DIR / 'logs'
LOG_FILE = LOGS_DIRECTORY / '简道云.log'
# 确保目录存在,如果不存在则创建
SAVE_DIRECTORY.mkdir(parents=True, exist_ok=True)
MODE_DIRECTORY.mkdir(parents=True, exist_ok=True)
# API 配置
JIANDAOYUN_API_TOKEN = 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN' # 曹伟应用api测试 app_key
# 导出配置
class Config:
BASE_DIR = BASE_DIR
SAVE_DIRECTORY = SAVE_DIRECTORY
MODE_DIRECTORY = MODE_DIRECTORY
JIANDAOYUN_API_TOKEN = JIANDAOYUN_API_TOKEN
LOGS_DIRECTORY = LOGS_DIRECTORY
LOG_FILE = LOG_FILE
+60
View File
@@ -0,0 +1,60 @@
"""
核心模块初始化
统一初始化请求头管理器、模块注册表等核心组件
"""
from typing import Dict, Any, Callable
from app.core.header_manager import HeaderManager
from app.core.module_registry import ModuleRegistry, registry
class CoreManager:
"""核心管理器 - 统一管理所有核心组件"""
def __init__(self):
self.header_manager = HeaderManager
self.registry = registry
def initialize_modules(self, modules: Dict[str, Any]):
"""
初始化并注册所有模块
Args:
modules: 模块字典,格式为 {模块名: 模块实例}
"""
for module_name, module_instance in modules.items():
self.registry.register_module(module_name, module_instance)
def register_action(
self,
action_name: str,
handler: Callable,
module_name: str = "default",
**kwargs
):
"""
便捷方法:注册操作
Args:
action_name: 操作名称
handler: 处理函数
module_name: 模块名称
**kwargs: 其他配置参数
"""
self.registry.register_action(
action_name=action_name,
handler=handler,
module_name=module_name,
**kwargs
)
# 全局核心管理器实例
core_manager = CoreManager()
# 导出常用类和函数
__all__ = [
'core_manager',
'HeaderManager',
'ModuleRegistry',
'registry',
]
+118
View File
@@ -0,0 +1,118 @@
"""
请求头管理器
统一管理不同模块的请求头配置
"""
from typing import Dict, Optional
from dataclasses import dataclass, field
@dataclass
class HeaderConfig:
"""请求头配置"""
referer: Optional[str] = None
user_agent: Optional[str] = None
content_type: Optional[str] = None
authorization: Optional[str] = None
custom_headers: Dict[str, str] = field(default_factory=dict)
def to_dict(self) -> Dict[str, str]:
"""转换为字典格式"""
headers = {}
if self.referer:
headers['Referer'] = self.referer
if self.user_agent:
headers['User-Agent'] = self.user_agent
if self.content_type:
headers['Content-Type'] = self.content_type
if self.authorization:
headers['Authorization'] = self.authorization
# 添加自定义请求头
headers.update(self.custom_headers)
return headers
class HeaderManager:
"""请求头管理器"""
# 默认请求头配置
DEFAULT_HEADERS = HeaderConfig(
referer='https://yunxiu.f6car.cn/erp/view/index.html',
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'
)
# F6系统登录请求头
F6_LOGIN_HEADERS = HeaderConfig(
referer='https://yunxiu.f6car.com/kzf6/login/confirm',
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.0.0'
)
# 简道云API请求头
JIANDAOYUN_API_HEADERS = HeaderConfig(
content_type='application/json',
# authorization 应该从配置中获取,这里只是示例
)
# 模块特定的请求头配置
_module_headers: Dict[str, HeaderConfig] = {
'default': DEFAULT_HEADERS,
'f6_login': F6_LOGIN_HEADERS,
'jiandaoyun_api': JIANDAOYUN_API_HEADERS,
}
@classmethod
def get_headers(cls, module_name: str = 'default', **overrides) -> Dict[str, str]:
"""
获取指定模块的请求头
Args:
module_name: 模块名称
**overrides: 覆盖的请求头配置
Returns:
请求头字典
"""
# 获取基础配置
config = cls._module_headers.get(module_name, cls.DEFAULT_HEADERS)
# 创建配置副本
header_config = HeaderConfig(
referer=overrides.get('referer', config.referer),
user_agent=overrides.get('user_agent', config.user_agent),
content_type=overrides.get('content_type', config.content_type),
authorization=overrides.get('authorization', config.authorization),
custom_headers={**config.custom_headers, **overrides.get('custom_headers', {})}
)
return header_config.to_dict()
@classmethod
def register_module_headers(cls, module_name: str, config: HeaderConfig):
"""
注册模块的请求头配置
Args:
module_name: 模块名称
config: 请求头配置
"""
cls._module_headers[module_name] = config
@classmethod
def update_module_headers(cls, module_name: str, **updates):
"""
更新模块的请求头配置
Args:
module_name: 模块名称
**updates: 要更新的配置项
"""
if module_name in cls._module_headers:
config = cls._module_headers[module_name]
for key, value in updates.items():
if hasattr(config, key):
setattr(config, key, value)
else:
config.custom_headers[key] = value
+116
View File
@@ -0,0 +1,116 @@
"""
模块注册和路由管理
提供统一的模块注册机制,方便添加新功能模块
"""
from typing import Dict, Callable, Optional, Any
from dataclasses import dataclass
@dataclass
class ActionConfig:
"""操作配置"""
handler: Callable # 处理函数
module_name: str # 所属模块名称
description: Optional[str] = None # 描述
requires_auth: bool = True # 是否需要认证
header_module: Optional[str] = None # 使用的请求头模块名称
class ModuleRegistry:
"""模块注册表"""
def __init__(self):
self._actions: Dict[str, ActionConfig] = {}
self._modules: Dict[str, Dict[str, Any]] = {}
def register_action(
self,
action_name: str,
handler: Callable,
module_name: str = "default",
description: Optional[str] = None,
requires_auth: bool = True,
header_module: Optional[str] = None
):
"""
注册操作
Args:
action_name: 操作名称
handler: 处理函数
module_name: 模块名称
description: 描述
requires_auth: 是否需要认证
header_module: 请求头模块名称
"""
config = ActionConfig(
handler=handler,
module_name=module_name,
description=description,
requires_auth=requires_auth,
header_module=header_module
)
self._actions[action_name] = config
def get_action(self, action_name: str) -> Optional[ActionConfig]:
"""
获取操作配置
Args:
action_name: 操作名称
Returns:
操作配置,如果不存在返回None
"""
return self._actions.get(action_name)
def get_all_actions(self) -> Dict[str, ActionConfig]:
"""获取所有注册的操作"""
return self._actions.copy()
def register_module(self, module_name: str, module_instance: Any, **metadata):
"""
注册模块实例
Args:
module_name: 模块名称
module_instance: 模块实例
**metadata: 模块元数据
"""
self._modules[module_name] = {
'instance': module_instance,
**metadata
}
def get_module(self, module_name: str) -> Optional[Any]:
"""
获取模块实例
Args:
module_name: 模块名称
Returns:
模块实例,如果不存在返回None
"""
module_info = self._modules.get(module_name)
return module_info['instance'] if module_info else None
def get_actions_by_module(self, module_name: str) -> Dict[str, ActionConfig]:
"""
获取指定模块的所有操作
Args:
module_name: 模块名称
Returns:
操作字典
"""
return {
name: config
for name, config in self._actions.items()
if config.module_name == module_name
}
# 全局模块注册表实例
registry = ModuleRegistry()
+298
View File
@@ -0,0 +1,298 @@
import requests
from urllib.parse import quote
import pandas as pd
import os
import urllib.parse
from datetime import datetime
from app.api import API
from typing import Optional, Dict, Any, Tuple
from app.config import Config
from app.module import F6Module
import threading
from app import back_ground_tasks
api_instance = API()
class F6PluginModule:
@staticmethod
def accept_file(data: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any]]: # 接收文件
"""
接收文件。
此方法用于处理前端上传的文件,下载文件并保存到指定目录。主要步骤包括:
1. 处理前端传递的数据,获取文件的URL。
2. 解析URL以获取文件名。
3. 根据当前时间生成新的文件名,以避免文件名冲突。
4. 下载文件并保存到指定目录。
5. 返回文件保存路径和处理后的数据。
Args:
data (dict): 包含文件URL和其他必要信息的字典。
Returns:
tuple: 包含文件保存路径和处理后的数据的元组。如果文件保存成功,返回保存路径和数据;如果失败,返回 None 和数据。
"""
data = api_instance.entry_data_get(data=data)
print(data)
try:
# 安全地访问附件信息
data_dict = data.get('data', {})
attachments = data_dict.get('附件', [])
if not attachments or len(attachments) == 0:
print('上传url未读取到,或无上传文件: 附件列表为空')
save_path = ''
return save_path, data
first_attachment = attachments[0]
url = first_attachment.get('url')
if not url:
print('上传url未读取到,或无上传文件: URL为空')
save_path = ''
return save_path, data
print(url)
except (KeyError, IndexError, TypeError) as e:
print(f'上传url未读取到,或无上传文件:{e}')
save_path = ''
return save_path, data
parsed_url = urllib.parse.urlparse(url)
query_params = urllib.parse.parse_qs(parsed_url.query)
attname = query_params.get('attname', [''])[0]
filename = urllib.parse.unquote(attname)
# 获取当前时间并格式化为指定格式的字符串
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
# 分离文件名和扩展名
name_part, ext_part = filename.rsplit('.', 1) if '.' in filename else (filename, '')
# 构建新文件名
new_filename = f"{name_part}{timestamp}.{ext_part}" if ext_part else f"{name_part}{timestamp}"
save_path = os.path.join(Config.SAVE_DIRECTORY, new_filename)
print(save_path)
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(save_path, 'wb') as file:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
file.write(chunk)
return save_path, data
else:
return None, data
def check_file(self, data: Dict[str, Any]) -> Dict[str, str]: # 校验上传文件
"""
校验上传文件。
此方法负责接收前端上传的文件,并根据文件类型和操作指令进行相应的校验。主要步骤包括:
1. 调用 `accept_file` 方法处理前端传递的数据,获取文件保存路径和处理后的数据。
2. 根据数据中的 `Action` 字段判断需要执行的操作类型。
3. 如果文件保存路径有效,继续执行以下步骤:
- 如果操作类型为 `create_brand`,则读取文件和模板文件,校验文件格式是否正确。
- 如果文件格式正确,返回成功消息;否则返回错误消息。
4. 如果文件保存路径无效,返回相应的错误消息。
5. 如果读取文件过程中发生异常,捕获异常并返回错误消息。
Args:
data (dict): 前端请求发送过来的数据,包含文件信息和其他必要参数。
Returns:
dict: 包含文件校验结果的消息字典。如果校验成功,则返回文件路径和校验标志;如果失败,则返回错误消息。
"""
save_path, data1 = self.accept_file(data)
# 安全地获取 Action 字段
data_dict = data1.get('data', {})
action = data_dict.get('Action(隐藏)')
if not action:
return {'msg': '缺少Action字段,无法校验文件'}
if save_path:
try:
if action == 'create_brand':
df1 = pd.read_excel(save_path, sheet_name=0)
if "品牌" in df1.columns[0]: # 校验表头名字
print('文件校验成功')
return {'msg': f'{save_path}', 'check': ''}
else:
print("'msg':'文件上传格式错误'")
return {'msg': '文件上传格式错误'}
elif action == 'modify_customer_info':
df = pd.read_excel(save_path, sheet_name=0)
if "客户手机号" in df.columns[0]: # 校验表头名字
print('文件校验成功')
return {'msg': f'{save_path}', 'check': ''}
else:
print("'msg':'文件上传格式错误'")
return {'msg': '文件上传格式错误'}
elif action == 'delete_cars':
pass
else:
pass
except Exception as e:
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
else:
return {'msg': '当前节点无附件上传', 'check': ''}
@staticmethod
def create_brand(data: Dict[str, Any]) -> Dict[str, str]: # 创建品牌
entry_data = api_instance.entry_data_get(data=data)
print('执行 品牌批量新建')
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
save_path = entry_data['data']['文件保存地址']
login_response = F6Module.login_in(username, password, company_name)
if login_response is None:
return {'msg': '登录失败'}
try:
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
except Exception as e:
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
try:
thread = threading.Thread(target=back_ground_tasks.create_brand_background,
args=(data, cookies, df, save_path))
thread.start()
except Exception as e:
print(f'创建线程失败: {str(e)}')
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
@staticmethod
def delete_history(data: Dict[str, Any]) -> Dict[str, str]:
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
org_name = entry_data['data']['门店名称']
login_response = F6Module.login_in(username, password, company_name)
if login_response is None:
return {'msg': '未执行', 'msg_details': '登录失败'}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
url = 'https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=1000&name='
res = requests.get(url=url, cookies=cookies)
store_data = res.json()
org_lists = store_data['data']['list']
org_id = ''
org_name1 = ''
for org in org_lists:
org_name1 = org['orgName']
if org_name == org['orgName']:
org_id = org['orgId']
if org_id:
thread = threading.Thread(target=back_ground_tasks.delete_history_background,
args=(data, cookies, org_id, org_name1))
thread.start()
return {'msg': '正在执行中', 'msg_details': '请稍后查看结果'}
else:
return {'msg': '未执行', 'msg_details': '门店信息错误'}
@staticmethod
def delete_customer(data):
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
res = F6Module.login_in(username, password, company_name)
if res is not None:
cookies = requests.utils.dict_from_cookiejar(res.cookies)
url = "https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=50000&pageNo=1"
res = requests.get(url, cookies=cookies)
json = res.json()
if json:
thread = threading.Thread(target=back_ground_tasks.delete_customer_background,
args=(data, cookies, json['data']['data'],))
thread.start()
return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'}
else:
return {'msg': '未执行', 'msg_details': '无客户信息'}
else:
return {'msg': '未执行', 'msg_details': '登录失败'}
@staticmethod
def delete_cars(data):
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
res = F6Module.login_in(username, password, company_name)
if res is not None:
cookies = requests.utils.dict_from_cookiejar(res.cookies)
url = "https://yunxiu.f6car.cn/member/car/carListForPermission"
header = {
'Referer': 'https://yunxiu.f6car.cn/erp/view/index.html',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'
}
json_data = {"pageSize": 100, "pageNo": 1}
res = requests.post(url=url, cookies=cookies, json=json_data, headers=header)
res_data = res.json()
total_items = int(res_data["data"]['total'])
all_page = total_items // 100 + (total_items % 100 > 0)
if res_data:
thread = threading.Thread(target=back_ground_tasks.delete_car_background,
args=(data, url, cookies, header, all_page))
thread.start()
return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'}
else:
return {'msg': '未执行', 'msg_details': '无客户车辆信息'}
else:
return {'msg': '未执行', 'msg_details': '登录失败'}
def modify_customer_info(self, data: Dict[str, str]):
entry_data = api_instance.entry_data_get(data=data)
username = entry_data['data']['账号']
password = entry_data['data']['密码']
company_name = entry_data['data']['公司名称']
save_path = entry_data['data']['文件保存地址']
login_response = F6Module.login_in(username, password, company_name)
if login_response is None:
return {'msg': '未执行', 'msg_details': '登录失败'}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
try:
df = pd.read_excel(save_path, sheet_name=0, dtype='string')
except Exception as e:
return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'}
if cookies:
thread = threading.Thread(target=back_ground_tasks.modify_customer_info_background,
args=(data, cookies, df, save_path))
thread.start()
return {'msg': '正在执行中', 'msg_details': '请稍后查看结果'}
else:
return {'msg': '未执行', 'msg_details': 'cookies获取失败'}
+2
View File
@@ -0,0 +1,2 @@
__all__ = []
+226
View File
@@ -0,0 +1,226 @@
import requests
import hashlib
from urllib.parse import quote
from datetime import datetime
from app.api import API
from typing import Optional, Dict, AnyStr
from PIL import Image, ImageEnhance
import pytesseract
import logging
from datetime import datetime
api_instance = API()
logger = logging.getLogger('app')
class F6Module:
@staticmethod
def get_captcha() -> AnyStr:
captcha_url = 'https://yunxiu.f6car.cn/kzf6/login/captcha-image'
response = requests.get(captcha_url)
with open('captcha.png', 'wb') as f:
f.write(response.content)
image = Image.open('captcha.png')
enhancer = ImageEnhance.Contrast(image)
image = enhancer.enhance(2.0)
enhancer = ImageEnhance.Brightness(image)
image = enhancer.enhance(1.5)
image = image.convert('L')
image = image.point(lambda x: 0 if x < 128 else 255, '1')
image.save('preprocessed_captcha.png')
captcha_text = pytesseract.image_to_string(image)
print(f"识别的验证码为: {captcha_text}")
return captcha_text
@staticmethod
def login_in(username: str, password: str, company_name: str = '默认门店',) -> Optional[requests.Response]:
url = "https://yunxiu.f6car.com/kzf6/login/confirm"
session = requests.Session()
header = {
'Referer': url,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/130.0.0.0 Safari/537.36 Edg/129.0.0.0'
}
data = {
'username': username,
'password': hashlib.md5(password.encode('utf-8')).hexdigest(),
}
try:
res = session.post(url=url, headers=header, data=data)
res_json = res.json()
if res_json.get('message') == '请输入图形验证码':
logger.warning("触发图形验证码")
captcha_text = F6Module.get_captcha()
data.update({'imageCode': captcha_text})
res = session.post(url=url, headers=header, data=data)
res_json = res.json()
if res_json.get("data") is None:
return res
else:
group_id = ''
for group in res_json.get('data', []):
if group.get("groupName") == company_name:
group_id = group.get("groupId")
break
if not group_id:
logger.error(f"未找到公司名称: {company_name}")
return None
token = quote(res_json['token']) # URL 编码
url = (f'https://yunxiu.f6car.cn/kzf6/user/loginAfterChooseGroup?'
f'token={token}&groupId={group_id}&macAddress=')
res1 = session.get(url, cookies=res.cookies)
return res1
except Exception as e:
print(f"Error during login: {e}")
return None
def accept_login_message(self, data: Dict[str, str]) -> Dict[str, str]:
username = data['username']
password = data['password']
company_name = data['company_name']
res = self.login_in(username, password, company_name)
if res is not None:
cookies = requests.utils.dict_from_cookiejar(res.cookies)
json = res.json()
url = 'https://yunxiu.f6car.cn/hive/company/getGroupName'
res1 = requests.get(url=url, cookies=cookies)
data1 = res1.json()
if data1['code'] == 200:
if data1['data'] == company_name:
if json['status'] == 'success':
json['status'] = '登录成功'
elif json['status'] == 'Error':
json['status'] = '登录失败,请检查账号密码'
else:
json['status'] = '公司名称不正确或未选择公司名称,请重试'
else:
json['status'] = '请输入正确的账号密码并选择公司名称'
return json
else:
return {"status": "登录失败,请检查公司名称"}
def get_company_information(self, data: Dict[str, str]) -> Dict[str, str]:
username = data['username']
password = data['password']
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
print(username)
url = "https://yunxiu.f6car.com/kzf6/login/confirm"
session = requests.Session()
header = {
'Referer': url,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'
}
data = {
'username': username,
'password': hashlib.md5(password.encode('utf-8')).hexdigest(),
}
try:
res = session.post(url=url, headers=header, data=data)
res_json = res.json()
if res_json.get('message') == '请输入图形验证码':
pass
jiandaoyun_data = {'api_key': '6694d3c4fcb69ca9a111a6c4', 'entry_id': '6736e2112ad50045f041a827'}
if res_json.get("data") is None:
print('单店')
res = self.login_in(username, password)
if res is not None:
cookies = requests.utils.dict_from_cookiejar(res.cookies)
url = 'https://yunxiu.f6car.cn/hive/company/getGroupName'
res = requests.get(url=url, cookies=cookies)
data = res.json()
store_name = data['data']
jiandaoyun_data['data_list'] = [
{"_widget_1731650067055": {"value": f'{username}{password}{timestamp}'},
"_widget_1731650067056": {"value": f"{store_name}"}}]
api_instance.entry_data_batch_create(jiandaoyun_data)
res = {'msg': f'{username}{password}{timestamp}'}
else:
jiandaoyun_data_list = []
for group in res_json.get('data', []):
append_data = {"_widget_1731650067055": {"value": f'{username}{password}{timestamp}'},
"_widget_1731650067056": {"value": f"{group['groupName']}"}}
jiandaoyun_data_list.append(append_data)
jiandaoyun_data['data_list'] = jiandaoyun_data_list
res = api_instance.entry_data_batch_create(jiandaoyun_data)
print(res)
res = {'msg': f'{username}{password}{timestamp}'}
return res
except Exception as e:
print(f"获取公司名称失败: {e}")
res = {'msg': '获取公司名称失败,请重新获取'}
return res
def get_store_information(self, data: Dict[str, str]) -> Dict[str, dict[str, str]]:
username = data['username']
password = data['password']
company_name = data['company_name']
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
login_response = self.login_in(username, password, company_name)
if login_response is None:
return {"msg": {'msg': '未执行', 'msg_details': '登录失败'}}
cookies = requests.utils.dict_from_cookiejar(login_response.cookies)
url = 'https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=100&name='
res = requests.get(url=url, cookies=cookies)
data = res.json()
org_lists = data['data']['list']
url = 'https://yunxiu.f6car.cn/member/car/carListForPermission'
json = {"pageSize": 10, "pageNo": 1}
car_res = requests.post(url=url, json=json, cookies=cookies)
total_cars_data = car_res.json()
total_cars = total_cars_data['data']['total']
url = 'https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=10&pageNo=1'
customer_res = requests.get(url=url, cookies=cookies)
total_customer_data = customer_res.json()
total_customer = total_customer_data['data']['total']
jiandaoyun_data = {'api_key': '6694d3c4fcb69ca9a111a6c4',
'entry_id': '673c38ccca57a5cf266eb18c'}
jiandaoyun_data_list = []
for org in org_lists:
append_data = {"_widget_1731999948708": {"value": f'{username}{password}{company_name}{timestamp}'},
"_widget_1731999948709": {"value": f"{org['orgName']}"}}
jiandaoyun_data_list.append(append_data)
jiandaoyun_data['data_list'] = jiandaoyun_data_list
api_instance.entry_data_batch_create(jiandaoyun_data)
res = {'msg': {"msg": f'{username}{password}{company_name}{timestamp}',
"total_cars": f"{total_cars}条客户车辆",
"total_customer": f"{total_customer}条客户"}}
return res
@staticmethod
def get_keep_heart(data: Dict[str, str]) -> Dict[str, str]:
return data
+22
View File
@@ -0,0 +1,22 @@
import requests
from urllib.parse import quote
import pandas as pd
import os
import urllib.parse
from datetime import datetime
from app.api import API
from typing import Optional, Dict, Any, Tuple
from app.config import Config
from app.module import F6Module
import threading
from app import back_ground_tasks
api_instance = API()
class OtherPluginModule:
def sms_signature_status(self):
pass
+89
View File
@@ -0,0 +1,89 @@
# 后台任务模块结构说明
## 模块结构
后台任务已按功能拆分为以下模块:
```
app/tasks/
├── __init__.py # 统一导出入口
├── common.py # 通用功能模块(简道云表单更新、工作流审批)
├── brand_tasks.py # 品牌相关任务
├── delete_tasks.py # 删除相关任务
└── customer_tasks.py # 客户相关任务
```
## 模块说明
### common.py - 通用功能模块
包含所有任务共用的功能:
- `update_jiandaoyun()` - 更新简道云表单
- `approve_workflow()` - 工作流审批
### brand_tasks.py - 品牌任务模块
品牌相关的后台任务:
- `create_brand_background()` - 品牌批量创建
### delete_tasks.py - 删除任务模块
删除相关的后台任务:
- `delete_history_background()` - 删除历史维修记录
- `delete_customer_background()` - 删除客户信息
- `delete_car_background()` - 删除客户车辆信息
### customer_tasks.py - 客户任务模块
客户相关的后台任务:
- `modify_customer_info_background()` - 修改客户信息
## 向后兼容
原有的 `app.back_ground_tasks` 模块仍然可用,它现在作为向后兼容的入口,实际功能已拆分到 `app.tasks` 模块中。
## 添加新功能模块
如需添加新的功能模块,请按以下步骤:
1.`app/tasks/` 目录下创建新的模块文件,例如 `new_feature_tasks.py`
2. 在新模块中实现相关功能函数
3.`app/tasks/__init__.py` 中导入并导出新函数
4.`app/back_ground_tasks.py` 中导入新函数以保持向后兼容
示例:
```python
# app/tasks/new_feature_tasks.py
from app.tasks.common import update_jiandaoyun, approve_workflow
def new_feature_background(data, cookies):
# 实现新功能
result = "执行结果"
msg = update_jiandaoyun(data, result)
if msg.get('msg'):
approve_workflow(data)
```
```python
# app/tasks/__init__.py 中添加
from app.tasks.new_feature_tasks import new_feature_background
__all__ = [
# ... 其他函数
'new_feature_background',
]
```
## 使用方式
### 方式一:使用新的模块结构(推荐)
```python
from app.tasks import create_brand_background
from app.tasks.brand_tasks import create_brand_background # 也可以直接导入
```
### 方式二:使用向后兼容的导入方式
```python
from app import back_ground_tasks
back_ground_tasks.create_brand_background(...)
```
两种方式都可以正常工作,代码执行逻辑完全一致。
+34
View File
@@ -0,0 +1,34 @@
"""
后台任务模块统一导出入口
保持向后兼容,所有原有导入方式仍然有效
"""
# 通用功能
from app.tasks.common import update_jiandaoyun, approve_workflow
# 品牌相关任务
from app.tasks.brand_tasks import create_brand_background
# 删除相关任务
from app.tasks.delete_tasks import (
delete_history_background,
delete_customer_background,
delete_car_background
)
# 客户相关任务
from app.tasks.customer_tasks import modify_customer_info_background
__all__ = [
# 通用功能
'update_jiandaoyun',
'approve_workflow',
# 品牌任务
'create_brand_background',
# 删除任务
'delete_history_background',
'delete_customer_background',
'delete_car_background',
# 客户任务
'modify_customer_info_background',
]
+55
View File
@@ -0,0 +1,55 @@
"""
品牌相关后台任务模块
包含品牌批量创建等功能
"""
import logging
import os
import requests
import pandas as pd
from typing import Dict, Any
from tqdm import tqdm
from app.tasks.common import update_jiandaoyun, approve_workflow
logger = logging.getLogger('app')
def create_brand_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str):
"""
品牌批量创建后台运行函数
:param data: 包含表单id、数据id等的字典
:param cookies: 用户登录f6系统的cookies信息
:param df: 表格读取到的内容,DataFrame格式
:param save_path: 文件保存的地址
:return: None
"""
df = df.where(pd.notnull(df), None)
# 定义请求URL
url = 'https://yunxiu.f6car.cn/camaro/brand/getOrCreate'
# 遍历DataFrame中的每一行数据
results = []
for index, row in tqdm(df.iterrows(), total=df.shape[0], desc="创建品牌"):
brand_name = row[df.columns[0]]
if brand_name is None or pd.isna(brand_name) or str(brand_name).strip() == '':
results.append({f'{brand_name}': '无效品牌名', '状态': '跳过'})
logger.warning(f"跳过无效品牌名: {brand_name}")
continue
try:
response = requests.post(url, cookies=cookies, json={"brandName": brand_name})
response.raise_for_status() # 抛出HTTP错误
results.append({'品牌名': brand_name, '状态': '创建成功'})
except requests.exceptions.RequestException as e:
results.append({'品牌名': brand_name, '状态': f'创建失败: {str(e)}'})
print({'msg': '已执行', 'msg_details': f'{results}'})
logger.info(f"品牌创建结果: {results}")
os.remove(save_path)
print(f'{save_path}已删除')
print(data)
# 调用api回写改掉 执行明细与执行状态
msg = update_jiandaoyun(data, f'{results}')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
+94
View File
@@ -0,0 +1,94 @@
"""
通用后台任务模块
包含简道云表单更新和工作流审批等通用功能
"""
import logging
import time
from typing import Dict, Any
from app.api import API
api_instance = API()
logger = logging.getLogger('app')
def update_jiandaoyun(data: Dict[str, Any], results: str):
"""
更新简道云表单
:param data: 包含表单id、应用id、数据id的字典
:param results: 执行结果信息
:return: 更新结果字典
"""
# 定义简道云数据配置
jiandaoyun_data = {
'api_key': data['api_key'],
'entry_id': data['entry_id'],
'data_id': data['data_id'],
"data": {
'_widget_1731379774828': {"value": "已执行"}, # f6系统批量操作测试 是否执行成功
'_widget_1731381334870': {"value": results} # f6系统批量操作测试 执行明细
}
}
time.sleep(1)
print(jiandaoyun_data)
try:
response = api_instance.entry_data_update(jiandaoyun_data)
logger.info(f"简道云表单更新成功: {response}")
return {'msg': True}
except Exception as e:
logger.error(f"简道云表单更新失败: {e}")
return {'msg': False}
def approve_workflow(data: Dict[str, Any]):
"""
获取简道云当前流程节点并直接提交
:param data: 包含表单id、应用id、数据id的字典
:return: None
"""
# 获取简道云当前流程列表
json = api_instance.workflow_instance_get(data)
# 检查返回数据是否有效
if not json:
logger.error("未获取到工作流实例信息")
return
# 安全地获取任务列表
tasks = json.get('tasks', [])
if not tasks:
logger.error("未找到待处理任务")
return
# 将JSON字符串转换为Python字典
username = ''
instance_id = ''
task_id = ''
for task in tasks:
if task.get('status') == 0:
assignee = task.get('assignee', {})
username = assignee.get('username', '')
instance_id = task.get('instance_id', '')
task_id = task.get('task_id', '')
if username and instance_id and task_id:
break
if not username or not instance_id or not task_id:
logger.error("未找到有效的待处理任务信息")
return
task_data = {
"username": username,
"instance_id": instance_id,
"task_id": task_id,
}
try:
response = api_instance.workflow_task_approve(task_data)
logger.info(f"简道云工作流任务提交成功: {response}")
except Exception as e:
logger.error(f"简道云工作流任务提交失败: {e}")
+261
View File
@@ -0,0 +1,261 @@
"""
客户相关后台任务模块
包含修改客户信息等功能
"""
import logging
import requests
import pandas as pd
import time
import re
from typing import Dict, Any, Optional
from app.tasks.common import update_jiandaoyun, approve_workflow
logger = logging.getLogger('app')
def modify_customer_info_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str):
"""
修改客户信息后台任务。
此函数用于后台任务,用于修改会员信息。
Args:
data (Dict[str, Any]): 前端请求发送过来的数据,包含文件信息和其他必要参数。
cookies (Dict[str, str]): 登录用户的Cookies。
Returns:
None
"""
df = df.where(pd.notnull(df), None)
params = {
'pageSize': 100,
'pageNo': '1',
}
res = requests.get(
'https://yunxiu.f6car.cn/member/customer/listForPermission',
params=params,
cookies=cookies,
)
total = int(res.json().get("data").get("total"))
total_pages = (total // params["pageSize"]) + (1 if total % params["pageSize"] > 0 else 0)
print(f"总计{total_pages}")
all_customers = []
max_retries = 10
retry_count = 0
for page in range(1, total_pages + 1):
print(f"正在请求第 {page} 页...")
params["pageNo"] = page
while retry_count < max_retries:
response = requests.get(
'https://yunxiu.f6car.cn/member/customer/listForPermission',
params=params,
cookies=cookies,
timeout=10
)
time.sleep(1)
if response.status_code == 200:
suppliers = response.json().get("data", {}).get("data", [])
all_customers.extend(suppliers)
break
else:
retry_count += 1
print(f"请求第 {page} 页失败,正在重试(第 {retry_count} 次)...")
time.sleep(3)
# 获取专属运营顾问列表
json_data = {
'includeStopedEmployee': False,
'pageSize': 1000,
'filterNullUser': False,
'keyword': '',
'idOwnOrgList': [],
}
response = requests.post(
'https://yunxiu.f6car.cn/hive/employee/searchStaffInGroup',
cookies=cookies,
json=json_data,
)
staff_list = response.json().get("data").get("list")
name_to_userid = {
emp['name']: emp['userId']
for emp in staff_list
if emp['userId'] is not None
}
df['userId'] = df['专属运营顾问'].map(name_to_userid)
def extract_province_city_district(address: Optional[str]) -> Dict[str, Optional[str]]:
"""安全解析省市区信息,所有返回值都可能为None"""
if not address:
return {'': None, '': None, '': None}
try:
pattern = r'(?P<省>(?:[\u4e00-\u9fa5]+(?:省|自治区|特别行政区))?)' \
r'(?P<市>(?:[\u4e00-\u9fa5]+(?:市|自治州|地区|盟))?)' \
r'(?P<区>(?:[\u4e00-\u9fa5]+区|[\u4e00-\u9fa5]+县|[\u4e00-\u9fa5]+旗)?)'
match = re.match(pattern, address.strip())
return {k: v if v else None for k, v in match.groupdict().items()} if match else {'': None, '': None,
'': None}
except Exception:
return {'': None, '': None, '': None}
def safe_get(d: Optional[Dict], *keys, default=None):
"""多层字典安全获取值,始终返回可能为None的值"""
if not isinstance(d, dict):
return default
for key in keys:
d = d.get(key, {})
if not isinstance(d, dict):
break
return d if d != {} else default
def convert_to_request_data(original_data: Optional[Dict[str, Any]], df: pd.DataFrame) -> Dict[str, Any]:
"""
完全安全的字典转换函数
特点:
1. 每个字段的值都可能为None
2. 不会因为任何字段为空而报错
3. 不使用任何默认值,完全保留原始数据的空值状态
"""
customer_info = safe_get(original_data, 'data', 'customerInfo') if original_data else None
address_parts = extract_province_city_district(
safe_get(customer_info, 'provinceCityAreaName') if customer_info else None
)
cell_phone = safe_get(customer_info, 'cellPhone')
exclusive_info = None
df_row = None
if cell_phone and not df.empty:
matched_rows = df[df['客户手机号'] == cell_phone]
if not matched_rows.empty:
df_row = matched_rows.iloc[0]
exclusive_info = {
'userId': df_row.get('userId'),
'name': df_row.get('专属运营顾问')
}
request_data = {
"pkId": safe_get(customer_info, 'idCustomer'),
"idCustomer": safe_get(customer_info, 'idCustomer'),
"name": df_row.get('客户姓名') if df_row is not None and pd.notna(df_row.get('客户姓名')) else safe_get(
customer_info, 'name'),
"sex": safe_get(customer_info, 'sex'),
"customerType": df_row.get('客户类型') if df_row is not None and pd.notna(
df_row.get('客户类型')) else safe_get(
customer_info, 'customerType'),
"customerSource": safe_get(customer_info, 'customerSource'),
"customerSourceName": df_row.get('客户来源') if df_row is not None and pd.notna(
df_row.get('客户来源')) else safe_get(customer_info, 'customerSourceName'),
"companyName": df_row.get('单位名称') if df_row is not None and pd.notna(
df_row.get('单位名称')) else safe_get(
customer_info, 'companyName'),
"cellPhone": cell_phone,
"wechart": safe_get(customer_info, 'wechart'),
"qq": safe_get(customer_info, 'qq'),
"contacts": safe_get(customer_info, 'contacts'),
"contactTelephone": safe_get(customer_info, 'contactTelephone'),
"province": safe_get(customer_info, 'province'),
"city": safe_get(customer_info, 'city'),
"area": safe_get(customer_info, 'area'),
"street": safe_get(customer_info, 'street'),
"address": safe_get(customer_info, 'address'),
"detailAddress": safe_get(customer_info, 'detailAddress'),
"pId": safe_get(customer_info, 'province'),
"cId": safe_get(customer_info, 'city'),
"aId": safe_get(customer_info, 'area'),
"provinceName": address_parts.get(''),
"cityName": address_parts.get(''),
"areaName": address_parts.get(''),
"provinceCityAreaName": safe_get(customer_info, 'provinceCityAreaName'),
"birthday": safe_get(customer_info, 'birthday'),
"creationtime": safe_get(customer_info, 'creationtime'),
"modifiedtime": safe_get(customer_info, 'modifiedtime'),
"creator": safe_get(customer_info, 'creator'),
"creatorName": safe_get(customer_info, 'creatorName'),
"modifier": safe_get(customer_info, 'modifier'),
"idOwnOrg": safe_get(customer_info, 'idOwnOrg'),
"idOwnGroup": safe_get(customer_info, 'idOwnGroup'),
"insuranceCompany": safe_get(customer_info, 'insuranceCompany'),
"maritalStatus": safe_get(customer_info, 'maritalStatus'),
"monthlyIncome": safe_get(customer_info, 'monthlyIncome'),
"idNumber": safe_get(customer_info, 'idNumber'),
"personHobby": safe_get(customer_info, 'personHobby'),
"credentialsType": safe_get(customer_info, 'credentialsType'),
"points": safe_get(customer_info, 'points'),
"maxAccountAmount": safe_get(customer_info, 'maxAccountAmount'),
"pointsEnable": safe_get(customer_info, 'pointsEnable'),
"level": safe_get(customer_info, 'level'),
"memberCardNo": safe_get(customer_info, 'memberCardNo'),
"customerLevel": safe_get(customer_info, 'customerLevel'),
"exclusiveConsultantId": exclusive_info['userId'] if exclusive_info else safe_get(customer_info,
'exclusiveConsultantId'),
"exclusiveConsultantName": exclusive_info['name'] if exclusive_info else safe_get(customer_info,
'exclusiveConsultantName'),
"exclusiveOrgId": safe_get(customer_info, 'exclusiveOrgId'),
"exclusiveOrgName": safe_get(customer_info, 'exclusiveOrgName'),
"customerMemo": df_row.get('客户备注') if df_row is not None and pd.notna(
df_row.get('客户备注')) else safe_get(
customer_info, 'customerMemo'),
"isDel": safe_get(customer_info, 'isDel'),
"idFrom": safe_get(customer_info, 'idFrom'),
"mnemonic": safe_get(customer_info, 'mnemonic'),
"idOrgSource": safe_get(customer_info, 'idOrgSource'),
"firstArrivalIdSourceBill": safe_get(customer_info, 'firstArrivalIdSourceBill'),
"lastArrivalIdSourceBill": safe_get(customer_info, 'lastArrivalIdSourceBill'),
"customerInfoType": safe_get(customer_info, 'customerInfoType'),
"customerInfoCompleteDate": safe_get(customer_info, 'customerInfoCompleteDate'),
"customerInfoCompleteType": safe_get(customer_info, 'customerInfoCompleteType'),
"xczUserId": safe_get(customer_info, 'xczUserId'),
"xczUuid": safe_get(customer_info, 'xczUuid'),
"idWxbCustomer": safe_get(customer_info, 'idWxbCustomer'),
"promoteEmployeeId": safe_get(customer_info, 'promoteEmployeeId'),
"promoteEmployeeName": safe_get(customer_info, 'promoteEmployeeName'),
"promoteMemberId": safe_get(customer_info, 'promoteMemberId'),
"promoteMemberName": safe_get(customer_info, 'promoteMemberName'),
"driverExpiryDate": safe_get(customer_info, 'driverExpiryDate'),
"crmDeleteExclusiveFlag": safe_get(customer_info, 'crmDeleteExclusiveFlag'),
"totalObtainPoints": safe_get(customer_info, 'totalObtainPoints'),
"totalUsedPoints": safe_get(customer_info, 'totalUsedPoints'),
"orgName": safe_get(customer_info, 'orgName'),
"weChatFollower": safe_get(customer_info, 'weChatFollower'),
"pointsEnableConfig": safe_get(customer_info, 'pointsEnableConfig'),
"personalPointsEnableConfig": safe_get(customer_info, 'personalPointsEnableConfig'),
"pointsButtonStatus": safe_get(customer_info, 'pointsButtonStatus'),
"tmallInstallMember": safe_get(customer_info, 'tmallInstallMember'),
"corpId": safe_get(customer_info, 'corpId'),
"thirdCorpId": safe_get(customer_info, 'thirdCorpId'),
}
return request_data
for customer in all_customers:
phone = customer.get("cellPhone")
if phone in df["客户手机号"].tolist():
print("开始修改")
cus_id = customer.get("idCustomer", {})
cus_response = requests.get(f'https://yunxiu.f6car.cn/member/customer/{cus_id}', cookies=cookies)
original_data = cus_response.json()
final_json_data = convert_to_request_data(original_data, df)
response = requests.post(
'https://yunxiu.f6car.cn/member/customer/modifyCustomer',
cookies=cookies,
json=final_json_data,
)
print("修改完成")
time.sleep(1)
msg = update_jiandaoyun(data, f'修改完成')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
+304
View File
@@ -0,0 +1,304 @@
"""
删除相关后台任务模块
包含删除历史维修记录、删除客户信息、删除客户车辆信息等功能
"""
import logging
import traceback
import requests
import time
from typing import Dict, Any, List
from datetime import datetime
from tqdm import tqdm
from app.tasks.common import update_jiandaoyun, approve_workflow
logger = logging.getLogger('app')
def delete_history_background(data: Dict[str, Any], cookies: Dict[str, str], org_id: str, org_name: str):
"""
删除历史维修数据后台运行函数
:param data: 包含表单id、数据id等的字典
:param cookies: 用户登录F6系统的cookies信息
:param org_id: 需要删除历史维修记录的门店id
:param org_name: 需要删除历史维修记录的门店名称
:return: None
"""
url = f'https://yunxiu.f6car.cn/maintain-dump/maintainHistory/?orgid={org_id}' # 删除url
res = requests.delete(url=url, cookies=cookies)
res_data = res.json()
if res.status_code == 200 and res_data.get('code') == 200:
results = f'{org_name} 历史维修记录已删除'
print(results)
logger.info(f"删除 {org_name} 历史维修记录成功")
else:
results = f'删除 {org_name} 历史维修记录失败: {res_data.get("message")}'
print(results)
logger.error(f"删除 {org_name} 历史维修记录失败: {res_data.get('message')}")
# 调用api回写改掉 执行明细与执行状态
time.sleep(1)
msg = update_jiandaoyun(data, f'{results}')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], json_data: List[Dict[str, Any]]):
"""
删除客户信息后台运行函数
:param data: 包含表单id、数据id等字典
:param cookies: 用户登录f6系统的cookies信息
:param json_data: 获取到的客户信息列表,列表最大值取决url里面的值
:return: None
"""
success = 0
fail = 0
# 获取门店ID
org_url = "https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=10&name="
org_res = requests.get(url=org_url, cookies=cookies)
# 安全地获取门店ID
org_data = org_res.json().get("data", {})
org_list = org_data.get("list", [])
if not org_list or len(org_list) == 0:
logger.error("未获取到门店信息")
msg = update_jiandaoyun(data, '删除失败: 未获取到门店信息')
if msg.get('msg'):
approve_workflow(data)
return
operate_org_id = org_list[0].get("orgId")
if not operate_org_id:
logger.error("门店ID为空")
msg = update_jiandaoyun(data, '删除失败: 门店ID为空')
if msg.get('msg'):
approve_workflow(data)
return
print(operate_org_id)
# 获取会员卡列表
card_url = f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}&pageSize=100&pageNo=1"
card_res = requests.get(url=card_url, cookies=cookies)
total_card = int(card_res.json().get("data").get("total"))
print(total_card)
total_page = total_card // 100 + (total_card % 100 > 0)
card_list_customers = []
for page in tqdm(range(1, total_page + 1), desc="查询会员卡"):
card_url = (f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}"
f"&pageSize=100&pageNo={page}")
card_res = requests.get(url=card_url, cookies=cookies)
card_cars_list = card_res.json().get("data").get("data")
for card_customer in card_cars_list:
if card_customer.get("idCustomer") is None:
continue
else:
card_list_customers.append(card_customer.get("idCustomer", None))
time.sleep(0.2)
for item in tqdm(json_data, desc="删除客户"):
id_customer = item['idCustomer']
phone = item['cellPhone']
consume_last_time = item['consumeLastTime']
if consume_last_time:
print(f'{id_customer}最近消费时间: {consume_last_time},跳过删除')
logger.warning(f"{id_customer}最近消费时间: {consume_last_time},跳过删除")
continue
if id_customer in card_list_customers:
logger.info(f"{id_customer} 存在会员卡,跳过删除")
fail += 1
continue
try:
url = f"https://yunxiu.f6car.cn/member/customer/{id_customer}" # 客户信息删除url
res = requests.delete(url, cookies=cookies) # 客户信息删除
res_data = res.json()
if res_data.get('success'):
success += 1
logger.info(f"客户删除成功: ID={id_customer}, 手机号={phone}")
else:
fail += 1
logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, 错误信息: {res_data.get('message')}")
time.sleep(0.2)
except Exception as e:
fail += 1
print("删除失败,", item, id_customer, phone, e)
logger.error(f"删除客户时发生错误: ID={id_customer}, 手机号={phone}, 错误信息: {e}")
if success + fail < len(json_data):
continue
now = datetime.now()
if 20 <= now.hour <= 8:
time.sleep(1)
else:
time.sleep(3)
logger.info(f"客户删除结果: 成功次数={success}, 失败次数={fail}")
msg = update_jiandaoyun(data, f'成功次数{success},失败次数{fail}')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
def delete_car_background(data: Dict[str, Any], url: str, cookies: Dict[str, str], header: Dict[str, Any],
all_page: str):
"""
删除客户车辆信息后台运行函数
:param header: 应包含账号登录的请求头
:param url: 包含请求客户车辆信息的url
:param all_page: 客户车辆信息的页数
:param data: 包含表单id、数据id等的字典
:param cookies: 登录F6系统后的请求信息
:return: None
"""
print(cookies)
success = 0
fail = 0
try:
# 确保 all_page 是一个整数
all_page = int(all_page)
# 获取门店ID
org_url = "https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=10&name="
org_res = requests.get(url=org_url, cookies=cookies)
# 安全地获取门店ID
org_data = org_res.json().get("data", {})
org_list = org_data.get("list", [])
if not org_list or len(org_list) == 0:
logger.error("未获取到门店信息")
msg = update_jiandaoyun(data, '删除失败: 未获取到门店信息')
if msg.get('msg'):
approve_workflow(data)
return
operate_org_id = org_list[0].get("orgId")
if not operate_org_id:
logger.error("门店ID为空")
msg = update_jiandaoyun(data, '删除失败: 门店ID为空')
if msg.get('msg'):
approve_workflow(data)
return
print(operate_org_id)
# 获取会员卡列表
card_url = (
f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}&pageSize=100&pageNo=1"
)
card_res = requests.get(url=card_url, cookies=cookies)
total_card = int(card_res.json().get("data").get("total"))
print(total_card)
total_page = total_card // 100 + (total_card % 100 > 0)
card_list_cars = []
for page in tqdm(range(1, total_page + 1), desc="查询会员卡"):
card_url = (f"https://yunxiu.f6car.cn/marketing/card/paging?useStationIdOwnOrgList={operate_org_id}"
f"&pageSize=100&pageNo={page}")
card_res = requests.get(url=card_url, cookies=cookies)
card_cars_list = card_res.json().get("data").get("data")
for card_car in card_cars_list:
if card_car.get("cars") is None:
continue
for car in card_car.get("cars", []):
card_list_cars.append(car.get("idCar", None))
time.sleep(0.2)
itemlist = []
# 使用 range() 创建一个可迭代的对象
for page in range(1, all_page + 1):
json_data = {
"pageSize": 100,
"pageNo": page
}
# 获取当前页的数据
res = requests.post(url=url, cookies=cookies, json=json_data, headers=header)
res.raise_for_status() # 检查请求是否成功
response_data = res.json()
if 'data' not in response_data or 'data' not in response_data['data']:
print(f"警告: 页码 {page} 返回的数据格式不正确")
continue
items = response_data['data']['data']
for item in items:
itemlist.append(item)
for item in tqdm(itemlist, desc="删除车辆信息"):
car_id = item.get('tmCarInfo', {}).get('pkId')
customer_id = item.get('tmCustomerInfo', {}).get('pkId')
consume_last_time = item.get('tmCustomerInfo', {}).get('consumeLastTime')
if consume_last_time:
logger.info(f"{customer_id}最近消费时间: {consume_last_time},跳过删除")
fail += 1
continue
if car_id in card_list_cars:
logger.info(f"{customer_id} 存在会员卡,跳过删除")
fail += 1
continue
if not car_id or not customer_id:
logger.info(f"页码 {page} 中缺少必要的ID信息")
fail += 1
continue
try:
delete_url = (
f"https://yunxiu.f6car.cn/member/car/deleteCar/{car_id}/{customer_id}"
)
delete_res = requests.delete(delete_url, cookies=cookies)
delete_res.raise_for_status() # 检查删除请求是否成功
delete_data = delete_res.json()
if delete_data.get('data'):
success += 1
else:
fail += 1
logger.error(
f"删除失败: 页码 {page}, 车辆ID {car_id}, 客户ID {customer_id},"
f" 错误信息: {delete_data.get('message', '未知错误')}")
print(
f"删除失败: 页码 {page}, 车辆ID {car_id}, 客户ID {customer_id},"
f" 错误信息: {delete_data.get('message', '未知错误')}")
time.sleep(0.2) # 避免过快请求
except requests.exceptions.RequestException as e:
fail += 1
print(f"删除失败: 页码 {page}, 车辆ID {car_id}, 客户ID {customer_id}, 错误信息: {e}")
logger.error(f"删除失败: 页码 {page}, 车辆ID {car_id}, 客户ID {customer_id}, 错误信息: {e}")
continue
now = datetime.now()
if 20 <= now.hour <= 8:
time.sleep(1)
else:
time.sleep(3)
print(f"完成: 成功删除 {success} 辆车, 失败 {fail} 辆车")
logger.info(f"完成: 成功删除 {success} 辆车, 失败 {fail} 辆车")
except ValueError as e:
print(f"Error converting all_page to integer: {e}")
traceback.print_exc() # 打印堆栈跟踪信息
except Exception as e:
print(f"An unexpected error occurred: {e}")
traceback.print_exc() # 打印堆栈跟踪信息
msg = update_jiandaoyun(data, f'成功次数{success},失败次数{fail}')
if msg.get('msg'):
approve_workflow(data)
print('表单已自动提交至下一步')
+88
View File
@@ -0,0 +1,88 @@
import logging
import os
from logging.handlers import RotatingFileHandler
from queue import Queue
import threading
from apscheduler.schedulers.background import BackgroundScheduler
from urllib.parse import unquote
class AppTools:
def __init__(self, config):
self.config = config
self.task_queue = Queue()
self.logger = self._setup_logger()
self.scheduler = self._setup_scheduler()
self._start_task_thread()
def _setup_logger(self):
log_dir = self.config.LOGS_DIRECTORY
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = self.config.LOG_FILE
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
if not any(isinstance(h, RotatingFileHandler) and getattr(h, 'baseFilename', None) == str(log_file)
for h in logger.handlers):
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024 * 5, backupCount=5, encoding='utf-8')
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s:%(name)s:%(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
def _setup_scheduler(self):
scheduler = BackgroundScheduler()
import atexit
atexit.register(lambda: scheduler.shutdown(wait=False))
return scheduler
def _start_task_thread(self):
task_thread = threading.Thread(target=self.process_tasks, daemon=True)
task_thread.start()
def process_tasks(self):
while True:
task = self.task_queue.get()
if task is None:
self.logger.error("任务处理线程已终止")
break
try:
result = task['handler'](task['data'])
task['response'].put(result)
except Exception as e:
self.logger.error(f"任务执行失败: {str(e)}")
task['response'].put({'msg': f'任务执行失败: {str(e)}'})
finally:
self.task_queue.task_done()
self.logger.info("任务处理完成")
def enqueue_task(self, handler, data):
response_queue = Queue()
self.task_queue.put({
'handler': handler,
'data': data,
'response': response_queue
})
return response_queue
@staticmethod
def decode_headers(headers):
return {key: unquote(value, encoding='utf-8') for key, value in headers.items()}
logger = None
def setup_global_logger(config):
global logger
if logger is None:
app_tools = AppTools(config)
logger = app_tools.logger
return logger