1.客户信息修改,将硬编码改为动态取值
2.新增项目批量停用、材料批量修改功能
This commit is contained in:
+103
-15
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
F6 插件模块
|
F6 后台执行模块
|
||||||
|
|
||||||
本模块提供 F6 插件相关的功能,包括:
|
本模块提供 F6 插件相关的功能,包括:
|
||||||
- 文件上传和校验
|
- 文件上传和校验
|
||||||
@@ -7,6 +7,8 @@ F6 插件模块
|
|||||||
- 历史记录删除
|
- 历史记录删除
|
||||||
- 客户信息管理
|
- 客户信息管理
|
||||||
- 车辆信息管理
|
- 车辆信息管理
|
||||||
|
- 项目信息批量启停
|
||||||
|
- 材料信息批量修改
|
||||||
|
|
||||||
依赖:
|
依赖:
|
||||||
- requests: HTTP 请求
|
- requests: HTTP 请求
|
||||||
@@ -31,6 +33,10 @@ from app.tasks.delete_tasks import (
|
|||||||
delete_customer_background,
|
delete_customer_background,
|
||||||
delete_car_background
|
delete_car_background
|
||||||
)
|
)
|
||||||
|
from app.tasks.material_tasks import (
|
||||||
|
batch_disable_projects,
|
||||||
|
batch_modify_materials,
|
||||||
|
)
|
||||||
from app.tasks.customer_tasks import modify_customer_info_background
|
from app.tasks.customer_tasks import modify_customer_info_background
|
||||||
from app.tasks.bi_tasks import bi_task_background
|
from app.tasks.bi_tasks import bi_task_background
|
||||||
|
|
||||||
@@ -65,7 +71,7 @@ class F6PluginModule:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple: 包含文件保存路径和处理后的数据的元组。如果文件保存成功,返回保存路径和数据;如果失败,返回 None 和数据。
|
tuple: 包含文件保存路径和处理后的数据的元组。如果文件保存成功,返回保存路径和数据;如果失败,返回 None 和数据。
|
||||||
"""
|
"""
|
||||||
data = api_instance.entry_data_get(data=data,replace= True)
|
data = api_instance.entry_data_get(data=data, replace=True)
|
||||||
print(data)
|
print(data)
|
||||||
try:
|
try:
|
||||||
# 安全地访问附件信息
|
# 安全地访问附件信息
|
||||||
@@ -118,8 +124,7 @@ class F6PluginModule:
|
|||||||
else:
|
else:
|
||||||
return None, data
|
return None, data
|
||||||
|
|
||||||
|
def check_file(self, data: Dict[str, Any]) -> dict[str, str] | None: # 校验上传文件
|
||||||
def check_file(self, data: Dict[str, Any]) -> Dict[str, str]: # 校验上传文件
|
|
||||||
"""
|
"""
|
||||||
校验上传文件。
|
校验上传文件。
|
||||||
|
|
||||||
@@ -176,7 +181,6 @@ class F6PluginModule:
|
|||||||
else:
|
else:
|
||||||
return {'msg': '当前节点无附件上传', 'check': '是'}
|
return {'msg': '当前节点无附件上传', 'check': '是'}
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_brand(data: Dict[str, Any]) -> Dict[str, str]:
|
def create_brand(data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
@@ -191,7 +195,7 @@ class F6PluginModule:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||||
"""
|
"""
|
||||||
entry_data = api_instance.entry_data_get(data=data,replace= True)
|
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||||
print('执行 品牌批量新建')
|
print('执行 品牌批量新建')
|
||||||
username = entry_data['data']['账号']
|
username = entry_data['data']['账号']
|
||||||
password = entry_data['data']['密码']
|
password = entry_data['data']['密码']
|
||||||
@@ -232,7 +236,7 @@ class F6PluginModule:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, str]: 包含执行状态的字典
|
Dict[str, str]: 包含执行状态的字典
|
||||||
"""
|
"""
|
||||||
entry_data = api_instance.entry_data_get(data=data,replace= True)
|
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||||
username = entry_data['data']['账号']
|
username = entry_data['data']['账号']
|
||||||
password = entry_data['data']['密码']
|
password = entry_data['data']['密码']
|
||||||
company_name = entry_data['data']['公司名称']
|
company_name = entry_data['data']['公司名称']
|
||||||
@@ -279,7 +283,7 @@ class F6PluginModule:
|
|||||||
Dict[str, str]: 包含执行状态的字典
|
Dict[str, str]: 包含执行状态的字典
|
||||||
"""
|
"""
|
||||||
print('执行 删除客户')
|
print('执行 删除客户')
|
||||||
entry_data = api_instance.entry_data_get(data=data,replace= True)
|
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||||
username = entry_data['data']['账号']
|
username = entry_data['data']['账号']
|
||||||
password = entry_data['data']['密码']
|
password = entry_data['data']['密码']
|
||||||
company_name = entry_data['data']['公司名称']
|
company_name = entry_data['data']['公司名称']
|
||||||
@@ -298,7 +302,8 @@ class F6PluginModule:
|
|||||||
thread = threading.Thread(target=delete_customer_background,
|
thread = threading.Thread(target=delete_customer_background,
|
||||||
args=(data, cookies, total,))
|
args=(data, cookies, total,))
|
||||||
thread.start()
|
thread.start()
|
||||||
return {'msg': '正在执行中', 'msg_details': f'总计{total}条数据,8-20点3.5s一条数据,其余时间1.5s一条数据'}
|
return {'msg': '正在执行中',
|
||||||
|
'msg_details': f'总计{total}条数据,8-20点3.5s一条数据,其余时间1.5s一条数据'}
|
||||||
else:
|
else:
|
||||||
return {'msg': '未执行', 'msg_details': '无客户信息'}
|
return {'msg': '未执行', 'msg_details': '无客户信息'}
|
||||||
else:
|
else:
|
||||||
@@ -318,7 +323,7 @@ class F6PluginModule:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, str]: 包含执行状态的字典
|
Dict[str, str]: 包含执行状态的字典
|
||||||
"""
|
"""
|
||||||
entry_data = api_instance.entry_data_get(data=data,replace= True)
|
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||||
username = entry_data['data']['账号']
|
username = entry_data['data']['账号']
|
||||||
password = entry_data['data']['密码']
|
password = entry_data['data']['密码']
|
||||||
company_name = entry_data['data']['公司名称']
|
company_name = entry_data['data']['公司名称']
|
||||||
@@ -351,7 +356,8 @@ class F6PluginModule:
|
|||||||
else:
|
else:
|
||||||
return {'msg': '未执行', 'msg_details': '登录失败'}
|
return {'msg': '未执行', 'msg_details': '登录失败'}
|
||||||
|
|
||||||
def modify_customer_info(self, data: Dict[str, str]):
|
@staticmethod
|
||||||
|
def modify_customer_info(data: Dict[str, str]):
|
||||||
"""
|
"""
|
||||||
修改客户信息
|
修改客户信息
|
||||||
|
|
||||||
@@ -364,7 +370,7 @@ class F6PluginModule:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, str]: 包含执行状态的字典
|
Dict[str, str]: 包含执行状态的字典
|
||||||
"""
|
"""
|
||||||
entry_data = api_instance.entry_data_get(data=data,replace= True)
|
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||||
username = entry_data['data']['账号']
|
username = entry_data['data']['账号']
|
||||||
password = entry_data['data']['密码']
|
password = entry_data['data']['密码']
|
||||||
company_name = entry_data['data']['公司名称']
|
company_name = entry_data['data']['公司名称']
|
||||||
@@ -389,6 +395,90 @@ class F6PluginModule:
|
|||||||
else:
|
else:
|
||||||
return {'msg': '未执行', 'msg_details': 'cookies获取失败'}
|
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': '正在执行,请稍后看结果'}
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
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=batch_disable_projects,
|
||||||
|
args=(data, cookies, df, save_path,option))
|
||||||
|
thread.start()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'创建线程失败: {str(e)}')
|
||||||
|
|
||||||
|
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def disable_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': '正在执行,请稍后看结果'}
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
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=batch_modify_materials,
|
||||||
|
args=(data, cookies, df, save_path, option))
|
||||||
|
thread.start()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'创建线程失败: {str(e)}')
|
||||||
|
|
||||||
|
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bi_task(data: Dict[str, Any]) -> Dict[str, str]:
|
def bi_task(data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
@@ -403,7 +493,7 @@ class F6PluginModule:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||||
"""
|
"""
|
||||||
entry_data = api_instance.entry_data_get(data=data,replace= True)
|
entry_data = api_instance.entry_data_get(data=data, replace=True)
|
||||||
print('执行 BI任务')
|
print('执行 BI任务')
|
||||||
|
|
||||||
# 获取必要的参数(根据实际需求调整)
|
# 获取必要的参数(根据实际需求调整)
|
||||||
@@ -438,5 +528,3 @@ class F6PluginModule:
|
|||||||
return {'msg': '任务启动失败', 'msg_details': f'无法启动后台任务: {str(e)}'}
|
return {'msg': '任务启动失败', 'msg_details': f'无法启动后台任务: {str(e)}'}
|
||||||
|
|
||||||
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
F6 系统模块
|
F6 前端即时响应模块
|
||||||
|
|
||||||
本模块提供 F6 系统相关的功能,包括:
|
本模块提供 F6 系统相关的功能,包括:
|
||||||
- 登录和认证
|
- 登录和认证
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ from app.tasks.customer_tasks import modify_customer_info_background
|
|||||||
# BI相关任务
|
# BI相关任务
|
||||||
from app.tasks.bi_tasks import bi_task_background
|
from app.tasks.bi_tasks import bi_task_background
|
||||||
|
|
||||||
|
from app.tasks.material_tasks import ( \
|
||||||
|
batch_modify_materials,
|
||||||
|
batch_disable_projects
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# 通用功能
|
# 通用功能
|
||||||
'update_jiandaoyun',
|
'update_jiandaoyun',
|
||||||
@@ -43,5 +48,7 @@ __all__ = [
|
|||||||
'modify_customer_info_background',
|
'modify_customer_info_background',
|
||||||
# BI任务
|
# BI任务
|
||||||
'bi_task_background',
|
'bi_task_background',
|
||||||
|
# 项目材料任务
|
||||||
|
'batch_disable_projects',
|
||||||
|
'batch_modify_materials',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
BI相关后台任务模块
|
BI相关后台任务模块
|
||||||
|
|
||||||
本模块包含BI相关的后台任务,包括:
|
本模块包含BI相关的后台任务,包括:
|
||||||
- BI数据处理
|
- TODO BI数据处理
|
||||||
- BI报表生成
|
|
||||||
|
|
||||||
这些任务在后台线程中执行,不会阻塞主请求。
|
这些任务在后台线程中执行,不会阻塞主请求。
|
||||||
执行完成后会更新简道云表单并自动提交工作流。
|
执行完成后会更新简道云表单并自动提交工作流。
|
||||||
|
|||||||
+2
-3
@@ -137,8 +137,6 @@ def execute_failure_handler(data: Dict[str, Any]):
|
|||||||
api_instance.data_batch_create(pay_load)
|
api_instance.data_batch_create(pay_load)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_operate_org_id(cookies: Dict[str, str]) -> Optional[str]:
|
def get_operate_org_id(cookies: Dict[str, str]) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
获取操作门店ID
|
获取操作门店ID
|
||||||
@@ -154,7 +152,7 @@ def get_operate_org_id(cookies: Dict[str, str]) -> Optional[str]:
|
|||||||
注意:
|
注意:
|
||||||
如果未获取到门店信息或门店ID为空,会记录错误日志并返回 None
|
如果未获取到门店信息或门店ID为空,会记录错误日志并返回 None
|
||||||
"""
|
"""
|
||||||
org_url = "https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=10&name="
|
org_url = "https://yunxiu.f6car.cn/hive/org/getPageOrgGroupMembers?currentPage=1&pageSize=100&name="
|
||||||
|
|
||||||
try:
|
try:
|
||||||
org_res = requests.get(url=org_url, cookies=cookies)
|
org_res = requests.get(url=org_url, cookies=cookies)
|
||||||
@@ -243,3 +241,4 @@ def get_card_list(
|
|||||||
logger.error(f"获取会员卡列表时发生错误: {e}")
|
logger.error(f"获取会员卡列表时发生错误: {e}")
|
||||||
return card_list
|
return card_list
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+115
-179
@@ -10,7 +10,7 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import time
|
import time
|
||||||
import re
|
import os
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
from app.tasks.common import update_jiandaoyun, approve_workflow
|
from app.tasks.common import update_jiandaoyun, approve_workflow
|
||||||
|
|
||||||
@@ -38,48 +38,49 @@ def modify_customer_info_background(data: Dict[str, Any], cookies: Dict[str, str
|
|||||||
- 执行完成后会自动删除上传的文件
|
- 执行完成后会自动删除上传的文件
|
||||||
- 执行结果会更新到简道云表单
|
- 执行结果会更新到简道云表单
|
||||||
"""
|
"""
|
||||||
df.where(pd.notnull(df), None)
|
try:
|
||||||
|
# 替换 NaN 为 None,便于后续判断
|
||||||
|
df = df.where(pd.notnull(df), None)
|
||||||
|
|
||||||
logger.info("获取当前客户下所有客户信息")
|
logger.info("开始获取当前客户下所有客户信息")
|
||||||
|
|
||||||
params = {
|
|
||||||
'pageSize': 100,
|
|
||||||
'pageNo': '1',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# 分页获取全部客户
|
||||||
|
params = {'pageSize': 100, 'pageNo': '1'}
|
||||||
res = requests.get(
|
res = requests.get(
|
||||||
'https://yunxiu.f6car.cn/member/customer/listForPermission',
|
'https://yunxiu.f6car.cn/member/customer/listForPermission',
|
||||||
params=params,
|
params=params,
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
|
timeout=10
|
||||||
)
|
)
|
||||||
|
res.raise_for_status()
|
||||||
total = int(res.json().get("data").get("total"))
|
total = int(res.json().get("data", {}).get("total", 0))
|
||||||
total_pages = (total // params["pageSize"]) + (1 if total % params["pageSize"] > 0 else 0)
|
total_pages = (total // params["pageSize"]) + (1 if total % params["pageSize"] > 0 else 0)
|
||||||
print(f"总计{total_pages}页")
|
logger.info(f"总计 {total_pages} 页,共 {total} 个客户")
|
||||||
|
|
||||||
all_customers = []
|
all_customers = []
|
||||||
max_retries = 10
|
|
||||||
retry_count = 0
|
|
||||||
for page in range(1, total_pages + 1):
|
for page in range(1, total_pages + 1):
|
||||||
print(f"正在请求第 {page} 页...")
|
logger.debug(f"正在请求第 {page} 页...")
|
||||||
params["pageNo"] = page
|
params["pageNo"] = page
|
||||||
|
retry_count = 0
|
||||||
|
max_retries = 5
|
||||||
while retry_count < max_retries:
|
while retry_count < max_retries:
|
||||||
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
'https://yunxiu.f6car.cn/member/customer/listForPermission',
|
'https://yunxiu.f6car.cn/member/customer/listForPermission',
|
||||||
params=params,
|
params=params,
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
time.sleep(1)
|
response.raise_for_status()
|
||||||
if response.status_code == 200:
|
page_data = response.json().get("data", {}).get("data", [])
|
||||||
suppliers = response.json().get("data", {}).get("data", [])
|
all_customers.extend(page_data)
|
||||||
all_customers.extend(suppliers)
|
|
||||||
break
|
break
|
||||||
else:
|
except Exception as e:
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
print(f"请求第 {page} 页失败,正在重试(第 {retry_count} 次)...")
|
logger.warning(f"请求第 {page} 页失败(第 {retry_count} 次重试): {e}")
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
else:
|
||||||
|
logger.error(f"第 {page} 页请求失败超过最大重试次数,跳过")
|
||||||
|
|
||||||
# 获取专属运营顾问列表
|
# 获取专属运营顾问列表
|
||||||
json_data = {
|
json_data = {
|
||||||
@@ -89,188 +90,123 @@ def modify_customer_info_background(data: Dict[str, Any], cookies: Dict[str, str
|
|||||||
'keyword': '',
|
'keyword': '',
|
||||||
'idOwnOrgList': [],
|
'idOwnOrgList': [],
|
||||||
}
|
}
|
||||||
|
staff_resp = requests.post(
|
||||||
response = requests.post(
|
|
||||||
'https://yunxiu.f6car.cn/hive/employee/searchStaffInGroup',
|
'https://yunxiu.f6car.cn/hive/employee/searchStaffInGroup',
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
json=json_data,
|
json=json_data,
|
||||||
|
timeout=10
|
||||||
)
|
)
|
||||||
|
staff_resp.raise_for_status()
|
||||||
staff_list = response.json().get("data").get("list")
|
staff_list = staff_resp.json().get("data", {}).get("list", [])
|
||||||
name_to_userid = {
|
name_to_userid = {
|
||||||
emp['name']: emp['userId']
|
emp['name']: emp['userId']
|
||||||
for emp in staff_list
|
for emp in staff_list
|
||||||
if emp['userId'] is not None
|
if emp.get('userId') is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 在 df 中添加 userId 列
|
||||||
df['userId'] = df['专属运营顾问'].map(name_to_userid)
|
df['userId'] = df['专属运营顾问'].map(name_to_userid)
|
||||||
|
|
||||||
def extract_province_city_district(address: Optional[str]) -> Dict[str, Optional[str]]:
|
# 字段映射:Excel 列名 -> F6 字段名
|
||||||
"""安全解析省市区信息,所有返回值都可能为None"""
|
FIELD_MAPPING = {
|
||||||
if not address:
|
'客户姓名': 'name',
|
||||||
return {'省': None, '市': None, '区': None}
|
'客户类型': 'customerType',
|
||||||
|
'客户来源': 'customerSourceName',
|
||||||
try:
|
'单位名称': 'companyName',
|
||||||
pattern = r'(?P<省>(?:[\u4e00-\u9fa5]+(?:省|自治区|特别行政区))?)' \
|
'客户备注': 'customerMemo',
|
||||||
r'(?P<市>(?:[\u4e00-\u9fa5]+(?:市|自治州|地区|盟))?)' \
|
'专属运营顾问': 'exclusiveConsultantName', # userId 单独处理
|
||||||
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 = {
|
def convert_to_request_data(original_data: dict, df_row: pd.Series) -> dict:
|
||||||
"pkId": safe_get(customer_info, 'idCustomer'),
|
"""以原始客户数据为基础,仅覆盖 Excel 中非空字段"""
|
||||||
"idCustomer": safe_get(customer_info, 'idCustomer'),
|
customer_info = original_data.get("data", {}).get("customerInfo", {}) or {}
|
||||||
"name": df_row.get('客户姓名') if df_row is not None and pd.notna(df_row.get('客户姓名')) else safe_get(
|
request_data = dict(customer_info) # 浅拷贝,足够用
|
||||||
customer_info, 'name'),
|
|
||||||
"sex": safe_get(customer_info, 'sex'),
|
# 覆盖指定字段
|
||||||
"customerType": df_row.get('客户类型') if df_row is not None and pd.notna(
|
for excel_col, f6_field in FIELD_MAPPING.items():
|
||||||
df_row.get('客户类型')) else safe_get(
|
value = df_row.get(excel_col)
|
||||||
customer_info, 'customerType'),
|
if pd.notna(value):
|
||||||
"customerSource": safe_get(customer_info, 'customerSource'),
|
request_data[f6_field] = str(value).strip() if isinstance(value, str) else value
|
||||||
"customerSourceName": df_row.get('客户来源') if df_row is not None and pd.notna(
|
|
||||||
df_row.get('客户来源')) else safe_get(customer_info, 'customerSourceName'),
|
# 处理专属顾问 ID
|
||||||
"companyName": df_row.get('单位名称') if df_row is not None and pd.notna(
|
if pd.notna(df_row.get('userId')):
|
||||||
df_row.get('单位名称')) else safe_get(
|
request_data['exclusiveConsultantId'] = df_row['userId']
|
||||||
customer_info, 'companyName'),
|
|
||||||
"cellPhone": cell_phone,
|
# 确保必要字段
|
||||||
"wechart": safe_get(customer_info, 'wechart'),
|
if 'idCustomer' in customer_info:
|
||||||
"qq": safe_get(customer_info, 'qq'),
|
request_data['pkId'] = customer_info['idCustomer']
|
||||||
"contacts": safe_get(customer_info, 'contacts'),
|
request_data['idCustomer'] = customer_info['idCustomer']
|
||||||
"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
|
return request_data
|
||||||
|
|
||||||
|
# 执行批量更新
|
||||||
|
updated_count = 0
|
||||||
for customer in all_customers:
|
for customer in all_customers:
|
||||||
phone = customer.get("cellPhone")
|
phone = customer.get("cellPhone")
|
||||||
if phone in df["客户手机号"].tolist():
|
if not phone:
|
||||||
print("开始修改")
|
continue
|
||||||
cus_id = customer.get("idCustomer", {})
|
|
||||||
cus_response = requests.get(f'https://yunxiu.f6car.cn/member/customer/{cus_id}', cookies=cookies)
|
matched_rows = df[df['客户手机号'] == phone]
|
||||||
original_data = cus_response.json()
|
if matched_rows.empty:
|
||||||
final_json_data = convert_to_request_data(original_data, df)
|
continue
|
||||||
response = requests.post(
|
|
||||||
|
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',
|
'https://yunxiu.f6car.cn/member/customer/modifyCustomer',
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
json=final_json_data,
|
json=final_json_data,
|
||||||
|
timeout=10
|
||||||
)
|
)
|
||||||
print("修改完成")
|
|
||||||
|
|
||||||
time.sleep(1)
|
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}")
|
||||||
|
|
||||||
msg = update_jiandaoyun(data, f'修改完成')
|
time.sleep(1) # 避免触发限流
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"处理客户 {phone} 时发生异常: {e}")
|
||||||
|
|
||||||
|
# 更新简道云状态
|
||||||
|
msg = update_jiandaoyun(data, f'批量修改完成,共更新 {updated_count} 个客户')
|
||||||
if msg.get('msg'):
|
if msg.get('msg'):
|
||||||
approve_workflow(data)
|
approve_workflow(data)
|
||||||
print('表单已自动提交至下一步')
|
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}")
|
||||||
+7
-7
@@ -1,11 +1,11 @@
|
|||||||
anyio==4.11.0
|
anyio==4.12.0
|
||||||
apscheduler==3.11.1
|
apscheduler==3.11.2
|
||||||
fastapi==0.121.0
|
fastapi==0.128.0
|
||||||
log_config==2.1.1
|
numpy==2.4.0
|
||||||
numpy==2.3.4
|
|
||||||
pandas==2.3.3
|
pandas==2.3.3
|
||||||
Pillow==12.0.0
|
Pillow==12.1.0
|
||||||
|
pydantic==2.12.5
|
||||||
pytesseract==0.3.13
|
pytesseract==0.3.13
|
||||||
Requests==2.32.5
|
Requests==2.32.5
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
uvicorn==0.38.0
|
uvicorn==0.40.0
|
||||||
|
|||||||
Reference in New Issue
Block a user