diff --git a/app/module/F6_Plugin_module.py b/app/module/F6_Plugin_module.py index 2094535..e9ea27c 100644 --- a/app/module/F6_Plugin_module.py +++ b/app/module/F6_Plugin_module.py @@ -1,5 +1,5 @@ """ -F6 插件模块 +F6 后台执行模块 本模块提供 F6 插件相关的功能,包括: - 文件上传和校验 @@ -7,6 +7,8 @@ F6 插件模块 - 历史记录删除 - 客户信息管理 - 车辆信息管理 +- 项目信息批量启停 +- 材料信息批量修改 依赖: - requests: HTTP 请求 @@ -31,6 +33,10 @@ from app.tasks.delete_tasks import ( delete_customer_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.bi_tasks import bi_task_background @@ -65,26 +71,26 @@ class F6PluginModule: Returns: tuple: 包含文件保存路径和处理后的数据的元组。如果文件保存成功,返回保存路径和数据;如果失败,返回 None 和数据。 """ - data = api_instance.entry_data_get(data=data,replace= True) + data = api_instance.entry_data_get(data=data, replace=True) print(data) try: # 安全地访问附件信息 data_dict = data.get('data', {}) attachments = data_dict.get('附件', []) - + if not attachments or len(attachments) == 0: print('上传url未读取到,或无上传文件: 附件列表为空') save_path = '' return save_path, data - + first_attachment = attachments[0] url = first_attachment.get('url') - + if not url: print('上传url未读取到,或无上传文件: URL为空') save_path = '' return save_path, data - + print(url) except (KeyError, IndexError, TypeError) as e: print(f'上传url未读取到,或无上传文件:{e}') @@ -118,8 +124,7 @@ class F6PluginModule: else: return None, data - - def check_file(self, data: Dict[str, Any]) -> Dict[str, str]: # 校验上传文件 + def check_file(self, data: Dict[str, Any]) -> dict[str, str] | None: # 校验上传文件 """ 校验上传文件。 @@ -143,7 +148,7 @@ class F6PluginModule: # 安全地获取 Action 字段 data_dict = data1.get('data', {}) action = data_dict.get('Action(隐藏)') - + if not action: return {'msg': '缺少Action字段,无法校验文件'} @@ -176,7 +181,6 @@ class F6PluginModule: else: return {'msg': '当前节点无附件上传', 'check': '是'} - @staticmethod def create_brand(data: Dict[str, Any]) -> Dict[str, str]: """ @@ -191,7 +195,7 @@ class F6PluginModule: Returns: 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('执行 品牌批量新建') username = entry_data['data']['账号'] password = entry_data['data']['密码'] @@ -232,7 +236,7 @@ class F6PluginModule: Returns: 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']['账号'] password = entry_data['data']['密码'] company_name = entry_data['data']['公司名称'] @@ -279,7 +283,7 @@ class F6PluginModule: Dict[str, str]: 包含执行状态的字典 """ 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']['账号'] password = entry_data['data']['密码'] company_name = entry_data['data']['公司名称'] @@ -298,7 +302,8 @@ class F6PluginModule: thread = threading.Thread(target=delete_customer_background, args=(data, cookies, total,)) 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: return {'msg': '未执行', 'msg_details': '无客户信息'} else: @@ -318,7 +323,7 @@ class F6PluginModule: Returns: 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']['账号'] password = entry_data['data']['密码'] company_name = entry_data['data']['公司名称'] @@ -351,20 +356,21 @@ class F6PluginModule: else: return {'msg': '未执行', 'msg_details': '登录失败'} - def modify_customer_info(self, data: Dict[str, str]): + @staticmethod + def modify_customer_info(data: Dict[str, str]): """ 修改客户信息 - + 从简道云获取修改客户信息请求,读取 Excel 文件,并在后台线程中批量修改客户信息。 立即返回"正在执行中"的提示,实际修改在后台线程中执行。 - + Args: data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典 - + Returns: Dict[str, str]: 包含执行状态的字典 """ - entry_data = api_instance.entry_data_get(data=data,replace= True) + entry_data = api_instance.entry_data_get(data=data, replace=True) username = entry_data['data']['账号'] password = entry_data['data']['密码'] company_name = entry_data['data']['公司名称'] @@ -389,6 +395,90 @@ class F6PluginModule: else: return {'msg': '未执行', 'msg_details': 'cookies获取失败'} + @staticmethod + def disable_projects(data: Dict[str, Any]) -> Dict[str, str]: + """ + 项目批量启停 + + 从简道云获取项目批量启停请求,读取 Excel 文件,并在后台线程中批量启停项目。 + 立即返回"正在执行"的提示,实际创建在后台线程中执行。 + + Args: + data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典 + + Returns: + Dict[str, str]: 包含执行状态的字典,{'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'} + """ + 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 def bi_task(data: Dict[str, Any]) -> Dict[str, str]: """ @@ -403,15 +493,15 @@ class F6PluginModule: Returns: 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任务') - + # 获取必要的参数(根据实际需求调整) username = entry_data['data'].get('账号') password = entry_data['data'].get('密码') company_name = entry_data['data'].get('公司名称') save_path = entry_data['data'].get('文件保存地址') - + # 如果需要登录F6系统 cookies = None if username and password and company_name: @@ -419,7 +509,7 @@ class F6PluginModule: if login_response is None: return {'msg': '登录失败', 'msg_details': '无法登录F6系统'} cookies = requests.utils.dict_from_cookiejar(login_response.cookies) - + # 如果需要读取Excel文件 df = None if save_path: @@ -427,7 +517,7 @@ class F6PluginModule: df = pd.read_excel(save_path, sheet_name=0, dtype='string') except Exception as e: return {'msg': f'读取Excel文件失败: {str(e)},文件路径:{save_path}'} - + # 启动后台线程执行BI任务 try: thread = threading.Thread(target=bi_task_background, @@ -438,5 +528,3 @@ class F6PluginModule: return {'msg': '任务启动失败', 'msg_details': f'无法启动后台任务: {str(e)}'} return {'msg': '正在执行', 'msg_details': '正在执行,请稍后看结果'} - - diff --git a/app/module/module.py b/app/module/module.py index 67f7613..5f57f69 100644 --- a/app/module/module.py +++ b/app/module/module.py @@ -1,5 +1,5 @@ """ -F6 系统模块 +F6 前端即时响应模块 本模块提供 F6 系统相关的功能,包括: - 登录和认证 diff --git a/app/tasks/__init__.py b/app/tasks/__init__.py index 03546e5..169065b 100644 --- a/app/tasks/__init__.py +++ b/app/tasks/__init__.py @@ -29,6 +29,11 @@ from app.tasks.customer_tasks import modify_customer_info_background # BI相关任务 from app.tasks.bi_tasks import bi_task_background +from app.tasks.material_tasks import ( \ + batch_modify_materials, + batch_disable_projects +) + __all__ = [ # 通用功能 'update_jiandaoyun', @@ -43,5 +48,7 @@ __all__ = [ 'modify_customer_info_background', # BI任务 'bi_task_background', + # 项目材料任务 + 'batch_disable_projects', + 'batch_modify_materials', ] - diff --git a/app/tasks/bi_tasks.py b/app/tasks/bi_tasks.py index 432cec3..21bad9d 100644 --- a/app/tasks/bi_tasks.py +++ b/app/tasks/bi_tasks.py @@ -2,8 +2,8 @@ BI相关后台任务模块 本模块包含BI相关的后台任务,包括: -- BI数据处理 -- BI报表生成 +- TODO BI数据处理 + 这些任务在后台线程中执行,不会阻塞主请求。 执行完成后会更新简道云表单并自动提交工作流。 diff --git a/app/tasks/common.py b/app/tasks/common.py index fc29c25..c329523 100644 --- a/app/tasks/common.py +++ b/app/tasks/common.py @@ -137,8 +137,6 @@ def execute_failure_handler(data: Dict[str, Any]): api_instance.data_batch_create(pay_load) - - def get_operate_org_id(cookies: Dict[str, str]) -> Optional[str]: """ 获取操作门店ID @@ -154,7 +152,7 @@ def get_operate_org_id(cookies: Dict[str, str]) -> Optional[str]: 注意: 如果未获取到门店信息或门店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: org_res = requests.get(url=org_url, cookies=cookies) @@ -243,3 +241,4 @@ def get_card_list( logger.error(f"获取会员卡列表时发生错误: {e}") return card_list + diff --git a/app/tasks/customer_tasks.py b/app/tasks/customer_tasks.py index ab522ab..8551296 100644 --- a/app/tasks/customer_tasks.py +++ b/app/tasks/customer_tasks.py @@ -10,7 +10,7 @@ import logging import requests import pandas as pd import time -import re +import os from typing import Dict, Any, Optional from app.tasks.common import update_jiandaoyun, approve_workflow @@ -20,257 +20,193 @@ logger = logging.getLogger('app') def modify_customer_info_background(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str): """ 修改客户信息后台任务 - + 在后台线程中批量修改客户信息,从 Excel 文件中读取客户手机号和修改信息。 执行完成后会更新简道云表单并自动提交工作流。 - + Args: data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典 cookies: 用户登录 F6 系统的 cookies 信息 df: Excel 文件读取的内容,DataFrame 格式,第一列为客户手机号 save_path: Excel 文件保存的地址,执行完成后会删除此文件 - + Returns: None - + 注意: - 根据客户手机号匹配客户信息 - 执行完成后会自动删除上传的文件 - 执行结果会更新到简道云表单 """ - 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', - } - - 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 + # 分页获取全部客户 + params = {'pageSize': 100, 'pageNo': '1'} + res = requests.get( + 'https://yunxiu.f6car.cn/member/customer/listForPermission', + params=params, + cookies=cookies, + timeout=10 ) + res.raise_for_status() + total = int(res.json().get("data", {}).get("total", 0)) + total_pages = (total // params["pageSize"]) + (1 if total % params["pageSize"] > 0 else 0) + logger.info(f"总计 {total_pages} 页,共 {total} 个客户") - cell_phone = safe_get(customer_info, 'cellPhone') + all_customers = [] + for page in range(1, total_pages + 1): + logger.debug(f"正在请求第 {page} 页...") + params["pageNo"] = page + retry_count = 0 + max_retries = 5 + while retry_count < max_retries: + try: + response = requests.get( + 'https://yunxiu.f6car.cn/member/customer/listForPermission', + params=params, + cookies=cookies, + timeout=10 + ) + response.raise_for_status() + page_data = response.json().get("data", {}).get("data", []) + all_customers.extend(page_data) + break + except Exception as e: + retry_count += 1 + logger.warning(f"请求第 {page} 页失败(第 {retry_count} 次重试): {e}") + time.sleep(3) + else: + logger.error(f"第 {page} 页请求失败超过最大重试次数,跳过") - exclusive_info = None - df_row = None - if cell_phone and not df.empty: - matched_rows = df[df['客户手机号'] == cell_phone] - if not matched_rows.empty: - df_row = matched_rows.iloc[0] - exclusive_info = { - 'userId': df_row.get('userId'), - 'name': df_row.get('专属运营顾问') - } - - request_data = { - "pkId": safe_get(customer_info, 'idCustomer'), - "idCustomer": safe_get(customer_info, 'idCustomer'), - "name": df_row.get('客户姓名') if df_row is not None and pd.notna(df_row.get('客户姓名')) else safe_get( - customer_info, 'name'), - "sex": safe_get(customer_info, 'sex'), - "customerType": df_row.get('客户类型') if df_row is not None and pd.notna( - df_row.get('客户类型')) else safe_get( - customer_info, 'customerType'), - "customerSource": safe_get(customer_info, 'customerSource'), - "customerSourceName": df_row.get('客户来源') if df_row is not None and pd.notna( - df_row.get('客户来源')) else safe_get(customer_info, 'customerSourceName'), - "companyName": df_row.get('单位名称') if df_row is not None and pd.notna( - df_row.get('单位名称')) else safe_get( - customer_info, 'companyName'), - "cellPhone": cell_phone, - "wechart": safe_get(customer_info, 'wechart'), - "qq": safe_get(customer_info, 'qq'), - "contacts": safe_get(customer_info, 'contacts'), - "contactTelephone": safe_get(customer_info, 'contactTelephone'), - "province": safe_get(customer_info, 'province'), - "city": safe_get(customer_info, 'city'), - "area": safe_get(customer_info, 'area'), - "street": safe_get(customer_info, 'street'), - "address": safe_get(customer_info, 'address'), - "detailAddress": safe_get(customer_info, 'detailAddress'), - "pId": safe_get(customer_info, 'province'), - "cId": safe_get(customer_info, 'city'), - "aId": safe_get(customer_info, 'area'), - "provinceName": address_parts.get('省'), - "cityName": address_parts.get('市'), - "areaName": address_parts.get('区'), - "provinceCityAreaName": safe_get(customer_info, 'provinceCityAreaName'), - "birthday": safe_get(customer_info, 'birthday'), - "creationtime": safe_get(customer_info, 'creationtime'), - "modifiedtime": safe_get(customer_info, 'modifiedtime'), - "creator": safe_get(customer_info, 'creator'), - "creatorName": safe_get(customer_info, 'creatorName'), - "modifier": safe_get(customer_info, 'modifier'), - "idOwnOrg": safe_get(customer_info, 'idOwnOrg'), - "idOwnGroup": safe_get(customer_info, 'idOwnGroup'), - "insuranceCompany": safe_get(customer_info, 'insuranceCompany'), - "maritalStatus": safe_get(customer_info, 'maritalStatus'), - "monthlyIncome": safe_get(customer_info, 'monthlyIncome'), - "idNumber": safe_get(customer_info, 'idNumber'), - "personHobby": safe_get(customer_info, 'personHobby'), - "credentialsType": safe_get(customer_info, 'credentialsType'), - "points": safe_get(customer_info, 'points'), - "maxAccountAmount": safe_get(customer_info, 'maxAccountAmount'), - "pointsEnable": safe_get(customer_info, 'pointsEnable'), - "level": safe_get(customer_info, 'level'), - "memberCardNo": safe_get(customer_info, 'memberCardNo'), - "customerLevel": safe_get(customer_info, 'customerLevel'), - "exclusiveConsultantId": exclusive_info['userId'] if exclusive_info else safe_get(customer_info, - 'exclusiveConsultantId'), - "exclusiveConsultantName": exclusive_info['name'] if exclusive_info else safe_get(customer_info, - 'exclusiveConsultantName'), - "exclusiveOrgId": safe_get(customer_info, 'exclusiveOrgId'), - "exclusiveOrgName": safe_get(customer_info, 'exclusiveOrgName'), - "customerMemo": df_row.get('客户备注') if df_row is not None and pd.notna( - df_row.get('客户备注')) else safe_get( - customer_info, 'customerMemo'), - "isDel": safe_get(customer_info, 'isDel'), - "idFrom": safe_get(customer_info, 'idFrom'), - "mnemonic": safe_get(customer_info, 'mnemonic'), - "idOrgSource": safe_get(customer_info, 'idOrgSource'), - "firstArrivalIdSourceBill": safe_get(customer_info, 'firstArrivalIdSourceBill'), - "lastArrivalIdSourceBill": safe_get(customer_info, 'lastArrivalIdSourceBill'), - "customerInfoType": safe_get(customer_info, 'customerInfoType'), - "customerInfoCompleteDate": safe_get(customer_info, 'customerInfoCompleteDate'), - "customerInfoCompleteType": safe_get(customer_info, 'customerInfoCompleteType'), - "xczUserId": safe_get(customer_info, 'xczUserId'), - "xczUuid": safe_get(customer_info, 'xczUuid'), - "idWxbCustomer": safe_get(customer_info, 'idWxbCustomer'), - "promoteEmployeeId": safe_get(customer_info, 'promoteEmployeeId'), - "promoteEmployeeName": safe_get(customer_info, 'promoteEmployeeName'), - "promoteMemberId": safe_get(customer_info, 'promoteMemberId'), - "promoteMemberName": safe_get(customer_info, 'promoteMemberName'), - "driverExpiryDate": safe_get(customer_info, 'driverExpiryDate'), - "crmDeleteExclusiveFlag": safe_get(customer_info, 'crmDeleteExclusiveFlag'), - "totalObtainPoints": safe_get(customer_info, 'totalObtainPoints'), - "totalUsedPoints": safe_get(customer_info, 'totalUsedPoints'), - "orgName": safe_get(customer_info, 'orgName'), - "weChatFollower": safe_get(customer_info, 'weChatFollower'), - "pointsEnableConfig": safe_get(customer_info, 'pointsEnableConfig'), - "personalPointsEnableConfig": safe_get(customer_info, 'personalPointsEnableConfig'), - "pointsButtonStatus": safe_get(customer_info, 'pointsButtonStatus'), - "tmallInstallMember": safe_get(customer_info, 'tmallInstallMember'), - "corpId": safe_get(customer_info, 'corpId'), - "thirdCorpId": safe_get(customer_info, 'thirdCorpId'), + # 获取专属运营顾问列表 + json_data = { + 'includeStopedEmployee': False, + 'pageSize': 1000, + 'filterNullUser': False, + 'keyword': '', + 'idOwnOrgList': [], + } + staff_resp = requests.post( + 'https://yunxiu.f6car.cn/hive/employee/searchStaffInGroup', + cookies=cookies, + json=json_data, + timeout=10 + ) + staff_resp.raise_for_status() + staff_list = staff_resp.json().get("data", {}).get("list", []) + name_to_userid = { + emp['name']: emp['userId'] + for emp in staff_list + if emp.get('userId') is not None } - return request_data + # 在 df 中添加 userId 列 + df['userId'] = df['专属运营顾问'].map(name_to_userid) - for customer in all_customers: - phone = customer.get("cellPhone") - if phone in df["客户手机号"].tolist(): - print("开始修改") - cus_id = customer.get("idCustomer", {}) - cus_response = requests.get(f'https://yunxiu.f6car.cn/member/customer/{cus_id}', cookies=cookies) - original_data = cus_response.json() - final_json_data = convert_to_request_data(original_data, df) - response = requests.post( - 'https://yunxiu.f6car.cn/member/customer/modifyCustomer', - cookies=cookies, - json=final_json_data, - ) - print("修改完成") + # 字段映射:Excel 列名 -> F6 字段名 + FIELD_MAPPING = { + '客户姓名': 'name', + '客户类型': 'customerType', + '客户来源': 'customerSourceName', + '单位名称': 'companyName', + '客户备注': 'customerMemo', + '专属运营顾问': 'exclusiveConsultantName', # userId 单独处理 + } - time.sleep(1) + def convert_to_request_data(original_data: dict, df_row: pd.Series) -> dict: + """以原始客户数据为基础,仅覆盖 Excel 中非空字段""" + customer_info = original_data.get("data", {}).get("customerInfo", {}) or {} + request_data = dict(customer_info) # 浅拷贝,足够用 - msg = update_jiandaoyun(data, f'修改完成') + # 覆盖指定字段 + for excel_col, f6_field in FIELD_MAPPING.items(): + value = df_row.get(excel_col) + if pd.notna(value): + request_data[f6_field] = str(value).strip() if isinstance(value, str) else value - if msg.get('msg'): - approve_workflow(data) - print('表单已自动提交至下一步') + # 处理专属顾问 ID + if pd.notna(df_row.get('userId')): + request_data['exclusiveConsultantId'] = df_row['userId'] + # 确保必要字段 + if 'idCustomer' in customer_info: + request_data['pkId'] = customer_info['idCustomer'] + request_data['idCustomer'] = customer_info['idCustomer'] + + return request_data + + # 执行批量更新 + updated_count = 0 + for customer in all_customers: + phone = customer.get("cellPhone") + if not phone: + continue + + matched_rows = df[df['客户手机号'] == phone] + if matched_rows.empty: + continue + + df_row = matched_rows.iloc[0] + cus_id = customer.get("idCustomer") + if not cus_id: + logger.warning(f"客户手机号 {phone} 缺少 idCustomer,跳过") + continue + + try: + # 获取完整客户信息 + detail_resp = requests.get( + f'https://yunxiu.f6car.cn/member/customer/{cus_id}', + cookies=cookies, + timeout=10 + ) + detail_resp.raise_for_status() + original_data = detail_resp.json() + + # 构造更新数据 + final_json_data = convert_to_request_data(original_data, df_row) + + # 发送更新 + update_resp = requests.post( + 'https://yunxiu.f6car.cn/member/customer/modifyCustomer', + cookies=cookies, + json=final_json_data, + timeout=10 + ) + + if update_resp.status_code == 200 and update_resp.json().get('success'): + logger.info(f"✅ 客户 {phone} 更新成功") + updated_count += 1 + else: + logger.error(f"❌ 客户 {phone} 更新失败: {update_resp.text}") + + time.sleep(1) # 避免触发限流 + + except Exception as e: + logger.exception(f"处理客户 {phone} 时发生异常: {e}") + + # 更新简道云状态 + msg = update_jiandaoyun(data, f'批量修改完成,共更新 {updated_count} 个客户') + if msg.get('msg'): + approve_workflow(data) + logger.info('✅ 表单已自动提交至下一步') + + except Exception as e: + logger.exception("modify_customer_info_background 执行出错:") + # 即使出错也尝试通知简道云 + try: + update_jiandaoyun(data, f'执行失败: {str(e)[:200]}') + except: + pass + + finally: + # 清理临时文件 + try: + if os.path.exists(save_path): + os.remove(save_path) + logger.info(f"临时文件已删除: {save_path}") + except Exception as e: + logger.warning(f"删除临时文件失败: {e}") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6914390..2fce1ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ -anyio==4.11.0 -apscheduler==3.11.1 -fastapi==0.121.0 -log_config==2.1.1 -numpy==2.3.4 +anyio==4.12.0 +apscheduler==3.11.2 +fastapi==0.128.0 +numpy==2.4.0 pandas==2.3.3 -Pillow==12.0.0 +Pillow==12.1.0 +pydantic==2.12.5 pytesseract==0.3.13 Requests==2.32.5 tqdm==4.67.1 -uvicorn==0.38.0 +uvicorn==0.40.0