Compare commits

...

5 Commits

Author SHA1 Message Date
panda 0f9971b7d2 trae-git 2026-04-09 09:53:47 +08:00
panda 976753d3c0 异常待办时间更改 2026-04-02 09:09:28 +08:00
panda 8e57195033 应续约日与过期日对调
更新续约代表数据一致性
2026-03-31 10:41:17 +08:00
panda 25225ce136 续约代办历史记录迁移
展会线索登记
2026-03-25 09:34:48 +08:00
panda ab0813c5ec 续约代办历史记录迁移 2026-03-09 09:24:10 +08:00
39 changed files with 21677 additions and 10308 deletions
+10 -1
View File
@@ -6,4 +6,13 @@
*.iml
out
gen
.logs
.logs
.log
.csv
.excel
.xlsx
output/
__pycache__/
.env
.vscode/
+44 -12
View File
@@ -353,13 +353,29 @@ class NewExceptionTask:
if stop_date:
# 解析暂停派发日期
parsed_stop_date = None
for fmt in date_formats:
stop_value = stop_date.get("value") if isinstance(stop_date, dict) else stop_date
if isinstance(stop_value, (int, float)):
parsed_stop_date = datetime.datetime.fromtimestamp(
stop_value / 1000, tz=datetime.timezone.utc
).replace(tzinfo=None)
elif isinstance(stop_value, str):
stop_str = stop_value.strip()
iso_candidate = stop_str[:-1] + "+00:00" if stop_str.endswith("Z") else stop_str
try:
parsed_stop_date = datetime.datetime.strptime(stop_date.strip(), fmt)
logger.debug(f"使用格式 {fmt} 成功解析暂停派发日期: {parsed_stop_date}")
break
iso_dt = datetime.datetime.fromisoformat(iso_candidate)
except ValueError:
continue
iso_dt = None
if iso_dt is not None:
parsed_stop_date = iso_dt.astimezone(datetime.timezone.utc).replace(tzinfo=None) if iso_dt.tzinfo else iso_dt
else:
for fmt in date_formats:
try:
parsed_stop_date = datetime.datetime.strptime(stop_str, fmt)
logger.debug(f"使用格式 {fmt} 成功解析暂停派发日期: {parsed_stop_date}")
break
except ValueError:
continue
if parsed_stop_date:
# 获取当前UTC时间
@@ -380,21 +396,37 @@ class NewExceptionTask:
if create_exception == "":
continue
# 新增:检查 create_date_str 是否存在且有效
if not create_date:
create_date_value = create_date.get("value") if isinstance(create_date, dict) else create_date
if not create_date_value:
logger.warning("上线日期为空,跳过该记录")
continue
parsed_date = None
for fmt in date_formats:
if isinstance(create_date_value, (int, float)):
local_tz = datetime.timezone(datetime.timedelta(hours=8))
parsed_date = datetime.datetime.fromtimestamp(create_date_value / 1000, tz=local_tz).date()
elif isinstance(create_date_value, str):
create_str = create_date_value.strip()
iso_candidate = create_str[:-1] + "+00:00" if create_str.endswith("Z") else create_str
try:
parsed_date = datetime.datetime.strptime(create_date.strip(), fmt).date()
logger.debug(f"使用格式 {fmt} 成功解析日期: {parsed_date}")
break
iso_dt = datetime.datetime.fromisoformat(iso_candidate)
except ValueError:
continue
iso_dt = None
if iso_dt is not None:
local_tz = datetime.timezone(datetime.timedelta(hours=8))
parsed_date = iso_dt.date() if iso_dt.tzinfo is None else iso_dt.astimezone(local_tz).date()
else:
for fmt in date_formats:
try:
parsed_date = datetime.datetime.strptime(create_str, fmt).date()
logger.debug(f"使用格式 {fmt} 成功解析日期: {parsed_date}")
break
except ValueError:
continue
if parsed_date is None:
logger.error(f"无法解析上线日期: '{create_date}',支持的格式: %Y-%m-%d, %Y-%m-%d %H:%M:%S 等")
logger.error(f"无法解析上线日期: '{create_date_value}',支持的格式: %Y-%m-%d, %Y-%m-%d %H:%M:%S 等")
continue # 解析失败,跳过
# 使用解析后的日期进行判断
@@ -835,7 +835,7 @@ class UpdateAllNGVDataDaily:
'is_camera_service': '_widget_1734062123079',
'is_maintenance_service': '_widget_1734062123080',
'saas_create_time': '_widget_1734062123081',
'expiry_time': '_widget_1734062123082',
'expiry_time': '_widget_1734062123177', # 改过期日
'saas_use_days': '_widget_1734062123083',
'saas_use_year': '_widget_1734062123084',
'is_main_org': '_widget_1734062123085',
@@ -958,7 +958,7 @@ class UpdateAllNGVDataDaily:
# 安装服务
'is_install_service': '_widget_1734062123175',
'install_create_time': '_widget_1734062123176',
'last_end_date': '_widget_1734062123177',
'last_end_date': '_widget_1734062123082',
'renew_date': '_widget_1734062123178',
# 连锁信息
@@ -1005,9 +1005,9 @@ class UpdateAllNGVDataDaily:
# 日期字段(UTC格式)
'date_fmt_date': '_widget_1749000071375',
'saas_create_time_date': '_widget_1749000071377',
'expiry_time_date': '_widget_1749000071382',
'expiry_time_date': '_widget_1749000071389', # 过期日
'install_create_time_date': '_widget_1749000071384',
'last_end_date_date': '_widget_1749000071389',
'last_end_date_date': '_widget_1749000071382',
'renew_date_date': '_widget_1749000071391',
# 人员ID字段
Binary file not shown.
+102
View File
@@ -2599,3 +2599,105 @@
2026-01-06 10:22:45,472 - log_config.py - error_task_logger - ERROR - 任务 高德匹配手机号 (05:00) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,473 - log_config.py - error_task_logger - ERROR - 任务 省市区人员关系表转BI (08:00) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,475 - log_config.py - error_task_logger - ERROR - 任务 续约回访待办 (09:35) 超过执行窗口5分钟以上,标记为过期。
2026-03-26 16:52:07,168 - log_config.py - error_task_logger - ERROR - 同步异常 data_id:69c48f57805eb4de7a4f42c4 实例:fde35fef-ce35-4521-9f01-139b3d15efd0 错误:name 'data' is not defined
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\续约待办一致性-全量同步.py", line 111, in <module>
form = data.get("formData")
^^^^
NameError: name 'data' is not defined
2026-04-01 14:23:45,579 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:23:49,233 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:23:51,145 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:02,662 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:12,213 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:19,850 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:20,919 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:21,410 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:29,458 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:30,227 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:35,451 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
2026-04-01 14:24:36,797 - log_config.py - error_task_logger - ERROR - 异常服务待办派发执行时发生异常: 'str' object has no attribute 'get'
Traceback (most recent call last):
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 475, in main
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
File "d:\Idea Project\SaaS_V1.7\test\异常服务代办暂停派发不生效问题排查.py", line 227, in assign_customer_service
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
+11709
View File
File diff suppressed because it is too large Load Diff
+323
View File
@@ -0,0 +1,323 @@
{
"cells": [
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2025-11-05T09:03:45.525420Z",
"start_time": "2025-11-05T09:03:44.127181Z"
}
},
"source": [
"# -*- coding: utf-8 -*-\n",
"import pandas as pd\n",
"import datetime\n",
"from config import Config\n",
"from api import API\n",
"from back_ground_module import CommonModule\n",
"from log_config import configure_task_logger, configure_error_task_logger\n",
"import time\n",
"timestamp = time.time() # 返回 float,单位:秒\n",
"\n",
"logger = configure_task_logger()\n",
"# 获取已经配置好的错误任务日志记录器\n",
"error_task_logger = configure_error_task_logger()\n",
"start_time = datetime.datetime.now()\n",
"api_instance = API()\n",
"common_module = CommonModule()\n",
"output_dir = \"output\" # 设置输出目录\n",
"# 创建输出目录(如果不存在)\n",
"import os\n",
"\n",
"os.makedirs(output_dir, exist_ok=True)\n",
"\n",
"\n",
"class UpdateNGVData:\n",
" \"\"\"NGV数据每日新增\"\"\"\n",
"\n",
" def __init__(self):\n",
" self.staff_id_list = None\n",
" self.field_mapping = {}\n",
" self.fields()\n",
"\n",
" def load_all_data(self):\n",
" # 获取简道云员工id\n",
" payload = {\"api_key\": \"6694d3c4fcb69ca9a111a6c4\",\n",
" \"entry_id\": \"6769204a1902c9341340a1bc\",\n",
" }\n",
" staff_id = api_instance.entry_data_list(payload)\n",
" self.staff_id_list = staff_id.get(\"data\") # api请求格式,将数据封装在data字典里\n",
"\n",
" @staticmethod\n",
" def get_staff_id(row_item, name):\n",
" \"\"\"辅助函数,用于获取员工ID\"\"\"\n",
" if str(row_item[\"_widget_1734942794144\"]) == str(name): # 检查姓名是否匹配\n",
" return row_item[\"_widget_1734942794145\"] # 返回员工ID\n",
" return None\n",
"\n",
" def main(self):\n",
" task_start_time = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n",
" try:\n",
" self.load_all_data()\n",
" logger.info(f\"数据加载完成\")\n",
" #\n",
" # data_NGV_j = common_module.get_ngv_details(days_back=1)\n",
" # data_NGV_j1 = common_module.get_ngv_details(days_back=2)\n",
" timestamp = time.time() # 返回 float,单位:秒\n",
" #\n",
" # data_NGV_j.to_csv(os.path.join(output_dir, f\"{timestamp}up_NGV_j.csv\"))\n",
" # data_NGV_j1.to_csv(os.path.join(output_dir, f\"{timestamp}up_NGV_j1.csv\"))\n",
" #\n",
" # # 找出在 data_NGV_j 中存在但在 data_NGV_j1 中不存在的 data_id\n",
" # unique_data_ids = data_NGV_j[~data_NGV_j['org_code'].isin(data_NGV_j1['org_code'])]\n",
" #\n",
" # # 创建一个新的 DataFrame 保存这些唯一的 data_id 及其对应的数据\n",
" # new_df = data_NGV_j[data_NGV_j['org_code'].isin(unique_data_ids['org_code'])]\n",
" #\n",
" # # 对 new_df 进行进一步的过滤,只保留 org_type 为 \"一般\" 的记录\n",
" # data_NGV_j = data_NGV_j[data_NGV_j['org_type'] == '一般']\n",
" # data_NGV_j1 = data_NGV_j1[data_NGV_j1['org_type'] == '一般']\n",
" # filtered_df = new_df[new_df['org_type'] == '一般']\n",
" # 默认未删除\n",
" filtered_df = pd.read_excel(r\"C:\\Users\\zy187\\Downloads\\异常服务跟进待办_20251105170140.xlsx\",sheet_name=\"Sheet1\")\n",
" filtered_df['源ngv是否已删除'] = '未删除'\n",
"\n",
" # 日期字段转换为日期格式\n",
" time_columns = ['date_fmt', 'saas_create_time', 'expiry_time', 'install_create_time', \"last_end_date\",\n",
" \"renew_date\"]\n",
" new_filtered_df = filtered_df.copy() # 复制df,以调整时间\n",
" for col in time_columns:\n",
" # 1. 转换为datetime类型(带错误处理)\n",
" # 使用.loc安全赋值\n",
" new_filtered_df[col] = pd.to_datetime(filtered_df[col], errors='coerce', utc=False)\n",
"\n",
" # 2. 优化后的时区转换(高效向量化操作)\n",
" filtered_df[col + '_date'] = (\n",
" new_filtered_df[col]\n",
" # 本地化为北京时间(东八区)\n",
" .dt.tz_localize('Asia/Shanghai', ambiguous='infer', nonexistent='NaT')\n",
" # 转换为UTC时区\n",
" .dt.tz_convert('UTC')\n",
" # 格式化为ISO8601字符串\n",
" .dt.strftime('%Y-%m-%dT%H:%M:%SZ')\n",
" )\n",
" logger.info(f\"时间转换完成\")\n",
"\n",
" # 人员字段转换为人员字段\n",
" staff_columns = ['area_manager', 'service_impl_principal', \"service_salesmen\", \"technician\"]\n",
" # 将员工列表转为DataFrame\n",
" # 三重循环临时方案(确保可写入)\n",
" for col in staff_columns:\n",
" staff_ids = []\n",
" for _, row in filtered_df.iterrows():\n",
" matched = False\n",
" for staff in self.staff_id_list:\n",
" if str(staff['_widget_1734942794144']) == str(row[col]):\n",
" staff_ids.append(staff['_widget_1734942794145'])\n",
" matched = True\n",
" break\n",
" if not matched:\n",
" staff_ids.append(None)\n",
" filtered_df[col + \"_staff_id\"] = staff_ids\n",
" logger.info(f\"人员转换完成\")\n",
"\n",
" # filtered_df.to_csv(r\"D:\\Idea Project\\SaaS_V1.3\\back_ground_module\\output\\NGV.csv\")\n",
"\n",
" # 生成包含所有行转换后的字典列表\n",
" # all_data = [self.row_to_dict(row, self.field_mapping) for index, row in data_NGV_j1.iterrows()] # 前两天的全部数据\n",
" # all_data = [self.row_to_dict(row, self.field_mapping) for index, row in data_NGV_j.iterrows()] # 前一天的全部数据\n",
" all_data = [self.row_to_dict(row, self.field_mapping) for index, row in filtered_df.iterrows()] # 增量数据\n",
"\n",
" try:\n",
" filtered_df.to_csv(os.path.join(output_dir, f\"{timestamp}NGV.csv\"))\n",
" except Exception as e:\n",
" error_task_logger.error(f\"NGV过滤后数据保存异常: {e}\")\n",
" pass\n",
"\n",
" #\n",
" data = {'api_key': Config.SaaS_Tasks_APP_ID, 'entry_id': Config.NGV_TASKS_ENTRY_ID, \"data_list\": all_data,\n",
" \"is_start_trigger\": \"true\"}\n",
"\n",
" result = api_instance.entry_data_batch_create(data)\n",
" logger.info(f\"数据已推送:{result}\")\n",
" # result_str = str(result)\n",
" # print(result_str[:500])\n",
"\n",
" # 保存到Excel文件\n",
" # output_path = r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细1.xlsx'\n",
" # filtered_df.to_excel(output_path, index=False)\n",
" # data_NGV_j1.to_excel( r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细j1.xlsx', index=False)\n",
" # data_NGV_j.to_excel( r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细j.xlsx', index=False)\n",
" # new_df.to_excel(r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细ndf.xlsx', index=False)\n",
"\n",
" common_module.send_task_status(task_start_time, \"NGV新增数据\")\n",
" logger.info(f\"任务完成。\")\n",
" except Exception as e:\n",
" error_task_logger.error(f\"任务执行时发生异常: {e}\")\n",
" # common_module.send_task_error(task_start_time, \"NGV新增数据\", str(e))\n",
"\n",
" @staticmethod\n",
" def row_to_dict(row, field_mapping):\n",
" \"\"\"将一行数据转换为指定格式的字典,并确保时间类型可JSON序列化\"\"\"\n",
" result = {}\n",
" for col_name, widget_id in field_mapping.items():\n",
" if col_name in row:\n",
" value = row[col_name]\n",
" if pd.isna(value):\n",
" clean_value = None\n",
" elif isinstance(value, (pd.Timestamp, pd.Timedelta)):\n",
" clean_value = value.isoformat() # 或 str(value)\n",
" elif hasattr(value, 'strftime'): # 兼容 datetime.datetime\n",
" clean_value = value.strftime('%Y-%m-%dT%H:%M:%SZ')\n",
" else:\n",
" clean_value = value\n",
" result[widget_id] = {\"value\": clean_value}\n",
" return result\n",
"\n",
" def fields(self):\n",
" self.field_mapping = dict(date_id='_widget_1734062123065', date_fmt='_widget_1734062123066',\n",
" id_own_group='_widget_1734062123067', group_name='_widget_1734062123068',\n",
" id_own_org='_widget_1734062123069', org_name='_widget_1734062123070',\n",
" org_code='_widget_1734062123071', group_grade='_widget_1734062123072',\n",
" org_type='_widget_1734062123073', org_status='_widget_1734062123074',\n",
" saas_version='_widget_1734062123075', is_wechat='_widget_1734062123076',\n",
" is_mini_app='_widget_1734062123077', is_wx_shop='_widget_1734062123078',\n",
" is_camera_service='_widget_1734062123079',\n",
" is_maintenance_service='_widget_1734062123080',\n",
" saas_create_time='_widget_1734062123081', expiry_time='_widget_1734062123082',\n",
" saas_use_days='_widget_1734062123083', saas_use_year='_widget_1734062123084',\n",
" is_main_org='_widget_1734062123085', license_code='_widget_1734062123086',\n",
" license_name='_widget_1734062123087', org_crm_id='_widget_1734062123088',\n",
" province_id='_widget_1734062123089', province_name='_widget_1734062123090',\n",
" city_id='_widget_1734062123091', city_name='_widget_1734062123092',\n",
" area_id='_widget_1734062123093', area_name='_widget_1734062123094',\n",
" region_name='_widget_1734062123095', region_short_name='_widget_1734062123096',\n",
" branch_name='_widget_1734062123097', carzone_store_id='_widget_1734062123098',\n",
" carzone_store_name='_widget_1734062123099',\n",
" customer_carzone_id='_widget_1734062123100', salesmen='_widget_1734062123101',\n",
" area_manager='_widget_1734062123102', service_salesmen='_widget_1734062123103',\n",
" impl_principal='_widget_1734062123104',\n",
" service_impl_principal='_widget_1734062123105',\n",
" active_user_count='_widget_1734062123106', active_user_type='_widget_1734062123107',\n",
" limit_user_count='_widget_1734062123108', limit_user_type='_widget_1734062123109',\n",
" is_n='_widget_1734062123110', is_g='_widget_1734062123111',\n",
" is_v='_widget_1734062123112', is_visited='_widget_1734062123113',\n",
" is_active='_widget_1734062123114', active_status_fmt='_widget_1734062123115',\n",
" bill_count_last_30_day='_widget_1734062123116',\n",
" bill_day_count_last_30_day='_widget_1734062123117',\n",
" bill_day_count_this_month='_widget_1734062123118',\n",
" bill_count_last_7_day='_widget_1734062123119',\n",
" bill_day_count_last_7_day='_widget_1734062123120', pv_count='_widget_1734062123121',\n",
" uv_count='_widget_1734062123122', bill_count_1d='_widget_1734062123123',\n",
" bill_count_2d='_widget_1734062123124', bill_count_3d='_widget_1734062123125',\n",
" bill_count_4d='_widget_1734062123126', bill_count_5d='_widget_1734062123127',\n",
" bill_count_6d='_widget_1734062123128', bill_count_7d='_widget_1734062123129',\n",
" bill_count_8d='_widget_1734062123130', bill_count_9d='_widget_1734062123131',\n",
" bill_count_10d='_widget_1734062123132', bill_count_11d='_widget_1734062123133',\n",
" bill_count_12d='_widget_1734062123134', bill_count_13d='_widget_1734062123135',\n",
" bill_count_14d='_widget_1734062123136', bill_count_15d='_widget_1734062123137',\n",
" bill_count_16d='_widget_1734062123138', bill_count_17d='_widget_1734062123139',\n",
" bill_count_18d='_widget_1734062123140', bill_count_19d='_widget_1734062123141',\n",
" bill_count_20d='_widget_1734062123142', bill_count_21d='_widget_1734062123143',\n",
" bill_count_22d='_widget_1734062123144', bill_count_23d='_widget_1734062123145',\n",
" bill_count_24d='_widget_1734062123146', bill_count_25d='_widget_1734062123147',\n",
" bill_count_26d='_widget_1734062123148', bill_count_27d='_widget_1734062123149',\n",
" bill_count_28d='_widget_1734062123150', bill_count_29d='_widget_1734062123151',\n",
" bill_count_30d='_widget_1734062123152', bill_count_31d='_widget_1734062123153',\n",
" etl_time='_widget_1734062123154',\n",
" maintain_bill_count_last_30_day='_widget_1734062123155',\n",
" washing_bill_count_last_30_day='_widget_1734062123156',\n",
" maintain_bill_day_count_last_30_day='_widget_1734062123157',\n",
" washing_bill_day_count_last_30_day='_widget_1734062123158',\n",
" retail_bill_count_last_30_day='_widget_1734062123159',\n",
" retail_bill_day_count_last_30_day='_widget_1734062123160',\n",
" purchase_bill_count_last_30_day='_widget_1734062123161',\n",
" purchase_bill_day_count_last_30_day='_widget_1734062123162',\n",
" card_bill_count_last_30_day='_widget_1734062123163',\n",
" card_bill_day_count_last_30_day='_widget_1734062123164',\n",
" gd_sales_bill_count_last_30_day='_widget_1734062123165',\n",
" gd_sales_bill_day_count_last_30_day='_widget_1734062123166',\n",
" g_change_flag='_widget_1734062123167', saas_package='_widget_1734062123168',\n",
" manage_model='_widget_1734062123169', contacts='_widget_1734062123170',\n",
" contact_number='_widget_1734062123171', contact_mobile='_widget_1734062123172',\n",
" g_month_count='_widget_1734062123173', g_month_percentage='_widget_1734062123174',\n",
" is_install_service='_widget_1734062123175',\n",
" install_create_time='_widget_1734062123176', last_end_date='_widget_1734062123177',\n",
" renew_date='_widget_1734062123178', is_chain_owner='_widget_1734062123179',\n",
" group_org_count='_widget_1734062123180',\n",
" recent_bill_warning_days='_widget_1734062123181',\n",
" g_change_flag_d='_widget_1734062123182', g_lost_warning_days='_widget_1734062123183',\n",
" saas_edition_fmt='_widget_1734062123184', g_flag_1m='_widget_1734062123185',\n",
" g_flag_2m='_widget_1734062123186', g_flag_3m='_widget_1734062123187',\n",
" g_flag_4m='_widget_1734062123188', g_flag_5m='_widget_1734062123189',\n",
" g_flag_6m='_widget_1734062123190', g_flag_day_count='_widget_1734062123191',\n",
" add_org_flag='_widget_1734062123192', pt='_widget_1734062123193',\n",
" org_size='_widget_1734062123194', qualification_type_fmt='_widget_1734062123195',\n",
" business_scope_fmt='_widget_1734062123196', store_type_fmt='_widget_1734062123197',\n",
" area='_widget_1734062123198', station_number='_widget_1734062123199',\n",
" header_type_fmt='_widget_1734062123200', org_stage='_widget_1734062123201',\n",
" g_count_this_month='_widget_1734062123202',\n",
" saas_customer_type='_widget_1734062123203', technician='_widget_1734062123204',\n",
" tmall_maintain_service_status_desc='_widget_1734062123205',\n",
" date_fmt_date='_widget_1749000071375',\n",
" area_manager_staff_id='_widget_1748496855779',\n",
" service_impl_principal_staff_id=\"_widget_1748496855780\",\n",
" service_salesmen_staff_id=\"_widget_1748496855778\",\n",
" technician_staff_id=\"_widget_1751877712235\",\n",
" saas_create_time_date=\"_widget_1749000071377\",\n",
" expiry_time_date=\"_widget_1749000071382\",\n",
" install_create_time_date=\"_widget_1749000071384\",\n",
" last_end_date_date=\"_widget_1749000071389\", renew_date_date=\"_widget_1749000071391\"\n",
" , 源NGV是否已删除=\"_widget_1754285499851\")\n",
"\n",
"\n",
"if __name__ == '__main__':\n",
" start = UpdateNGVData()\n",
" start.main()\n"
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"已获取 100 条数据\n",
"已获取 146 条数据\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\u001B[92m2025-11-05 17:03:45,497 - api.py - task_logger - INFO - 获取了146条数据\u001B[0m\n",
"\u001B[92m2025-11-05 17:03:45,498 - 4224831806.py - task_logger - INFO - 数据加载完成\u001B[0m\n",
"\u001B[91m2025-11-05 17:03:45,523 - 4224831806.py - error_task_logger - ERROR - 任务执行时发生异常: 'date_fmt'\u001B[0m\n"
]
}
],
"execution_count": 6
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,93 @@
import argparse
from pathlib import Path
import pandas as pd
def keep_latest_by_time(df: pd.DataFrame, store_col: str, time_col: str) -> pd.DataFrame:
if store_col not in df.columns:
raise ValueError(f"缺少列: {store_col}")
if time_col not in df.columns:
raise ValueError(f"缺少列: {time_col}")
working = df.copy()
working[store_col] = working[store_col].astype(str).fillna("")
working[time_col] = working[time_col].astype(str).fillna("")
working["_parsed_time"] = pd.to_datetime(working[time_col], errors="coerce")
working["_row_order"] = range(len(working))
working = working.sort_values(
by=["_parsed_time", "_row_order"],
ascending=[True, True],
kind="mergesort",
na_position="first",
)
latest = working.groupby(store_col, sort=False).tail(1)
latest = latest.drop(columns=["_parsed_time", "_row_order"]).reset_index(drop=True)
return latest
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument("--input", "-i", required=False, help="输入 Excel 路径(.xlsx")
parser.add_argument("--output", "-o", required=False, help="输出 Excel 路径(.xlsx")
parser.add_argument("--sheet", default="需要保留一条", help="Sheet 名称")
parser.add_argument("--store-col", default="门店编码", help="门店编码列名")
parser.add_argument("--time-col", default="创建时间", help="创建时间列名")
parser.add_argument("--codes-output", required=False, help="可选:输出门店编码清单(.txt 或 .csv)")
parser.add_argument("--demo", action="store_true", help="运行内置示例(不读写 Excel")
return parser
def main() -> int:
args = build_parser().parse_args()
if args.demo:
demo_df = pd.DataFrame(
[
{"门店编码": "A001", "创建时间": "2026-03-01 10:00:00", "其他": "x"},
{"门店编码": "A001", "创建时间": "2026-03-05 09:00:00", "其他": "y"},
{"门店编码": "B002", "创建时间": "2026/03/02 12:00", "其他": "m"},
{"门店编码": "B002", "创建时间": "无效时间", "其他": "n"},
]
)
result = keep_latest_by_time(demo_df, store_col=args.store_col, time_col=args.time_col)
print(result)
print("门店编码:", ",".join(result[args.store_col].astype(str).tolist()))
return 0
if not args.input:
raise SystemExit("缺少参数 --input")
input_path = Path(args.input).expanduser().resolve()
if not input_path.exists():
raise SystemExit(f"输入文件不存在: {input_path}")
df = pd.read_excel(input_path, sheet_name=args.sheet, dtype=str).fillna("")
latest = keep_latest_by_time(df, store_col=args.store_col, time_col=args.time_col)
output_path = Path(args.output).expanduser().resolve() if args.output else None
if output_path is None:
output_path = input_path.with_name(f"{input_path.stem}_保留最新.xlsx")
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
latest.to_excel(writer, sheet_name="保留最新", index=False)
store_codes = latest[args.store_col].astype(str).tolist()
print(f"保留行数: {len(latest)}")
print(f"门店编码数量: {len(store_codes)}")
if args.codes_output:
codes_path = Path(args.codes_output).expanduser().resolve()
if codes_path.suffix.lower() == ".csv":
pd.DataFrame({args.store_col: store_codes}).to_csv(codes_path, index=False, encoding="utf-8-sig")
else:
codes_path.write_text("\n".join(store_codes), encoding="utf-8")
print(f"输出文件: {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
-104
View File
@@ -1,104 +0,0 @@
{
"result": [
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-02-28T09:07Z",
"showName": "提交申请",
"operateType": "NEW_PROCESS",
"remark": "",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "宜搭平台",
"actionExit": "submit",
"operatorUserId": "yida_pub_account",
"activityId": "sid-restartevent",
"size": 1,
"dataId": 52359309345,
"domainList": [],
"operatorDisplayName": "宜搭平台",
"action": "提交申请",
"taskId": "null",
"operatorPhotoUrl": "//img.alicdn.com/tfs/TB1mKVJSpXXXXcwaXXXXXXXXXXX-78-80.jpg"
},
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-03-18T10:14Z",
"showName": "120天联系情况",
"operateType": "EXECUTE_TASK_NORMAL",
"remark": "提交",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "孟祥宇",
"actionExit": "agree",
"operatorUserId": "224616673723465569",
"activityId": "sid-6470221a-82ec-4bdd-a873-245ee47a5605",
"size": 1,
"dataId": 53850072345,
"domainList": [],
"operatorDisplayName": "孟祥宇",
"action": "提交",
"taskId": "52359293348",
"operatorPhotoUrl": "https://static.dingtalk.com/media/lADPDh0cUUaSfHrNAvDNAnA_624_752.jpg"
},
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-04-01T14:08Z",
"showName": "60天联系情况",
"operateType": "EXECUTE_TASK_NORMAL",
"remark": "系统提交",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "魏子淇",
"actionExit": "agree",
"operatorUserId": "195159084238961158",
"activityId": "sid-ab6374fd-7580-66d5-1628-6b0666bb38ff",
"size": 1,
"dataId": 55294608170,
"domainList": [],
"operatorDisplayName": "魏子淇",
"action": "提交",
"taskId": "53850072349",
"operatorPhotoUrl": "https://static.dingtalk.com/media/lADPD2sQvHoyUEbNAtHNAtA_720_721.jpg"
},
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-04-01T14:08Z",
"showName": "30天联系情况",
"operateType": "EXECUTE_TASK_NORMAL",
"remark": "系统提交",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "魏子淇",
"actionExit": "agree",
"operatorUserId": "195159084238961158",
"activityId": "sid-e5928800-154e-4e20-6019-1364274afc49",
"size": 1,
"dataId": 55294609293,
"domainList": [],
"operatorDisplayName": "魏子淇",
"action": "提交",
"taskId": "55294608175",
"operatorPhotoUrl": "https://static.dingtalk.com/media/lADPD2sQvHoyUEbNAtHNAtA_720_721.jpg"
},
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-04-01T14:08Z",
"showName": "0天处理情况",
"operateType": "EXECUTE_TASK_NORMAL",
"remark": "系统提交",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "魏子淇",
"actionExit": "agree",
"operatorUserId": "195159084238961158",
"activityId": "sid-ba12125f-bc3a-2663-ebf0-43b5aeb8c32c",
"size": 1,
"dataId": 55294558885,
"domainList": [],
"operatorDisplayName": "魏子淇",
"action": "同意",
"taskId": "55294609301",
"operatorPhotoUrl": "https://static.dingtalk.com/media/lADPD2sQvHoyUEbNAtHNAtA_720_721.jpg"
}
]
}
-116
View File
@@ -1,116 +0,0 @@
{
"app_id": "6694d3c4fcb69ca9a111a6c4",
"form_id": "693778ee287cfdcc2df85ece",
"form_title": "流程表单结构测试",
"instance_id": "6937dc8d2cbcdd4c466a8381",
"url": "https://dingtalk.jiandaoyun.com/workflow/process_instance/6937dc8d2cbcdd4c466a8381",
"update_time": "2025-12-09T08:23:41.095Z",
"create_time": "2025-12-09T08:23:41.093Z",
"finish_time": None,
"status": 0,
"creator": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"tasks": [
{
"app_id": "6694d3c4fcb69ca9a111a6c4",
"form_id": "693778ee287cfdcc2df85ece",
"form_title": "流程表单结构测试",
"title": "流程发起节点",
"instance_id": "6937dc8d2cbcdd4c466a8381",
"task_id": "6937dc8d2cbcdd4c466a8398",
"flow_id": 0,
"flow_name": "流程发起节点",
"url": "https://dingtalk.jiandaoyun.com/workflow/process_instance/6937dc8d2cbcdd4c466a8381/task/6937dc8d2cbcdd4c466a8398",
"assignee": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"creator": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"create_time": "2025-12-09T08:23:41.094Z",
"create_action": "forward",
"finish_time": "2025-12-09T08:23:41.094Z",
"finish_action": "forward",
"status": 1
},
{
"app_id": "6694d3c4fcb69ca9a111a6c4",
"form_id": "693778ee287cfdcc2df85ece",
"form_title": "流程表单结构测试",
"title": "多跟进人节点",
"instance_id": "6937dc8d2cbcdd4c466a8381",
"task_id": "6937dc8d2cbcdd4c466a83a6",
"flow_id": 2,
"flow_name": "多跟进人节点",
"url": "https://dingtalk.jiandaoyun.com/workflow/process_instance/6937dc8d2cbcdd4c466a8381/task/6937dc8d2cbcdd4c466a83a6",
"assignee": {
"username": "4210192048793363",
"name": "张阳",
"departments": [
449008196
],
"type": 0,
"status": 1,
"integrate_id": "4210192048793363"
},
"creator": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"create_time": "2025-12-09T08:23:41.095Z",
"create_action": "forward",
"finish_time": None,
"finish_action": None,
"status": 0
},
{
"app_id": "6694d3c4fcb69ca9a111a6c4",
"form_id": "693778ee287cfdcc2df85ece",
"form_title": "流程表单结构测试",
"title": "多跟进人节点",
"instance_id": "6937dc8d2cbcdd4c466a8381",
"task_id": "6937dc8d2cbcdd4c466a83aa",
"flow_id": 2,
"flow_name": "多跟进人节点",
"url": "https://dingtalk.jiandaoyun.com/workflow/process_instance/6937dc8d2cbcdd4c466a8381/task/6937dc8d2cbcdd4c466a83aa",
"assignee": {
"username": "2268275546837446",
"name": "曹伟",
"departments": [
449008196
],
"type": 0,
"status": 1,
"integrate_id": "2268275546837446"
},
"creator": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"create_time": "2025-12-09T08:23:41.095Z",
"create_action": "forward",
"finish_time": None,
"finish_action": None,
"status": 0
}
]
}
-1639
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "## 全量同步",
"id": "69bf37484b68b727"
},
{
"cell_type": "code",
"execution_count": null,
"id": "initial_id",
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
""
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+1
View File
@@ -401,6 +401,7 @@ try:
ts = int(round(t * 1000))
randint = random.randint(100000000, 999999999)
req = res_new['result'] + "|" + formData['textField_kuntp6fk'] + "|" + formData['textField_kuntp6fl']+ "|" + formData['employeeField_kykw5ege'] + "_" + str(ts) + "_" + str(randint)
# 实例ID|门ID|服务单号|专属运营顾问
str_en = des_encrypt(req)
print(str_en.decode('utf-8'))
req_new = str_en.decode('utf-8')
+39
View File
@@ -0,0 +1,39 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "initial_id",
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import pandas as pd\n",
"\n",
"df = pd.read_excel(fr\"C:\\Users\\hp_z66\\Downloads\\商机问题跟进表_20260331114857.xlsx\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+43
View File
@@ -0,0 +1,43 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "## 重复派发",
"id": "2d5eea6406e5bd27"
},
{
"cell_type": "code",
"execution_count": null,
"id": "initial_id",
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
""
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because it is too large Load Diff
@@ -1,59 +1,241 @@
import os
import sys
import pandas as pd
import json
import ast # 👈 新增导入
from datetime import datetime
# 获取上级目录并加入路径
nb_path = os.path.abspath('')
parent_dir = os.path.dirname(nb_path)
sys.path.append(parent_dir)
from back_ground_module import CommonModule
from log_config import configure_task_logger, configure_error_task_logger
from yd_api import YDAPI
from api import API
from tqdm import tqdm
logger = configure_task_logger()
error_task_logger = configure_error_task_logger()
api_instance = API()
yd_api_instance = YDAPI()
common_module = CommonModule()
output_dir = "output"
os.makedirs(output_dir, exist_ok=True)
api_instance = API()
yd_api_instance = YDAPI()
# 加载数据
# df = pd.read_csv(r"D:\Idea Project\SaaS_V1.7\test\output\expanded_yd_data.csv",encoding="gbk").astype(str)
df = pd.read_excel(r"C:\Users\hp_z66\OneDrive\Desktop\门店分析新.xlsx",sheet_name="新建").astype(str)
df2 = pd.read_excel(
r"D:\Idea Project\SaaS_V1.7\test\output\续约服务流程_20260324165743.xlsx"
).fillna('').astype(str)
# 从df中获取流程编码获取流程详细信息 # 测试注释
token = yd_api_instance.generateToken()
FORMID = "FORM-PE866MD1MJMU0WGLYRFLYEN5YN9L1I55Z7ZUK22"
appType = "APP_UYZ0KG6L0CCNV80GZ66O"
systemToken = "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2"
df = pd.read_csv(os.path.join(output_dir, "converted_yd_data.csv"))
all_instance_data = []
for index, row in tqdm(df.iterrows(), total=len(df)):
instance_id = row["实例ID"]
instance_info = yd_api_instance.processes_instancesInfos(token, instance_id, appType, systemToken)
data = instance_info.get("data")
if data:
# 提取 formData 中的字段并合并到外层
form_data = data.get("formData", {})
if isinstance(form_data, dict):
data.update(form_data)
# 手动注入实例 ID,确保映射能找到
data["实例ID"] = instance_id
all_instance_data.append(data)
# 检查 data 列的第一个非空值类型
sample = df['data'].dropna().iloc[0] if not df['data'].dropna().empty else ""
print("Sample of 'data' column:")
print(repr(sample)[:200]) # 打印前200字符,看是单引号还是双引号
ndf = pd.DataFrame(all_instance_data)
ndf.to_csv(r"D:\Idea Project\SaaS_V1.7\\test\output\yd_process_details.csv", index=False)
# 如果是字符串(且是 Python dict 格式,单引号),用 ast.literal_eval
if isinstance(sample, str):
print("Detected string format, parsing with ast.literal_eval...")
# 读取宜搭流程详情(已提前导出)
ndf = pd.read_csv(r"D:\Idea Project\SaaS_V1.7\test\output\yd_process_details.csv")
# 简道云字段中文名 → 字段ID 映射
jdy_map = {
"门店编码": "_widget_1764820541661",
"120天是否跟进": "_widget_1764820541628",
"120天处理人": "_widget_1764820541634",
"120天跟进时间": "_widget_1765352838631",
"60天是否跟进": "_widget_1764820541630",
"60天处理人": "_widget_1764820541635",
"60天跟进时间": "_widget_1765352838632",
"30天是否跟进": "_widget_1764820541632",
"30天处理人": "_widget_1764820541636",
"30天跟进时间": "_widget_1765352838633",
"是否联系上": "_widget_1764820541638",
"现阶段问题": "_widget_1764820541641",
"联系情况及问题说明": "_widget_1764820541653",
"潜在商机": "_widget_1764820541657",
"商机详情": "_widget_1764820541659",
"不续约原因": "_widget_1764820541700",
"产品问题": "_widget_1764820541707",
"服务问题": "_widget_1764820541709",
"门店问题": "_widget_1764820541711",
"价格问题": "_widget_1764820541713",
"不续约具体情况说明": "_widget_1764820541702",
"宜搭实例ID": "_widget_1774339442956",
}
# 安全地将字符串转为字典
def safe_literal_eval(x):
if pd.isna(x) or x == "":
return {}
try:
return ast.literal_eval(x)
except (ValueError, SyntaxError) as e:
print(f"Parse error on: {repr(x)[:100]}... Error: {e}")
return {}
# 宜搭字段ID → 简道云中文名 映射
yd_field_id_to_jdy_chinese = {
"textField_ksydghqw": "门店编码",
"radioField_kuntp6fm": "120天是否跟进",
"textField_livc8bjj": "120天处理人",
"dateField_lifr1fdv": "120天跟进时间",
"radioField_kurxyhvp": "60天是否跟进",
"textField_livc8bjl": "60天处理人",
"dateField_lifr1fdx": "60天跟进时间",
"radioField_kurxyhvq": "30天是否跟进",
"textField_livc8bjm": "30天处理人",
"dateField_lifr1fdy": "30天跟进时间",
"radioField_l85ppdia": "是否联系上",
"radioField_r3yeqvd": "现阶段问题",
"textAreaField_972lhkt": "联系情况及问题说明",
"radioField_ljqi5we3": "潜在商机",
"textareaField_liviovx0": "商机详情",
"selectField_l31clxfy": "不续约原因",
"selectField_l31clxfz": "产品问题",
"selectField_l31clxg0": "服务问题",
"selectField_l31clxg1": "门店问题",
"selectField_l31clxg2": "价格问题",
"textareaField_l31clxg4": "不续约具体情况说明",
"radioField_l85ppdie": "续约意愿",
"实例ID":"宜搭实例ID"
}
# 值映射(用于标准化选项值)
value_mapping = {
"现阶段问题": {"暂时没有问题": "暂时无问题"},
"不续约原因": {"产品原因": "产品问题", "门店原因": "门店问题"},
"服务问题": {
"联系不上小六": "联系不上运营顾问",
"小六态度问题": "运营顾问态度问题",
"小六业务不专业": "运营顾问业务不专业",
"小六离职未能获取不续约原因": "运营顾问离职未能获取不续约原因"
},
"120天是否跟进": {"小六": "主动", "系统": "自动"},
"60天是否跟进": {"小六": "主动", "系统": "自动"},
"30天是否跟进": {"小六": "主动", "系统": "自动"},
}
df['data'] = df['data'].apply(safe_literal_eval)
# ========================
# 1. 获取员工姓名 → ID 映射
# ========================
payload_staff = {
"api_key": "6694d3c4fcb69ca9a111a6c4", # 注意:应为 app_id,不是 api_key(根据你实际接口调整)
"entry_id": "6769204a1902c9341340a1bc",
}
staff_resp = api_instance.entry_data_list(payload_staff)
staff_id_list = staff_resp.get("data", [])
# 展开 data 列
expanded = pd.json_normalize(df['data'])
# 构建映射字典:姓名 -> 员工ID
name_to_staff_id = {}
for item in staff_id_list:
name = item.get("_widget_1734942794144", "").strip()
staff_id = item.get("_widget_1734942794145", "").strip()
if name and staff_id:
name_to_staff_id[name] = staff_id
# 保留其他列(注意:原列是 'data',不是 'raw_data'
other_cols = df.drop(columns=['data'])
logger.info(f"加载 {len(name_to_staff_id)} 名员工信息")
# 合并
new_df = pd.concat([other_cols.reset_index(drop=True), expanded.reset_index(drop=True)], axis=1).astype(str)
# 过滤进行中
new_df = new_df[new_df["instanceStatus"] == "RUNNING"]
# 订单编码为空
col = "textField_kto3q3ev"
mask2 = (
new_df[col].isnull() |
(new_df[col].astype(str).str.strip() == "")
)
new_df = new_df[mask2]
# 保存结果
new_df.to_csv(os.path.join(output_dir, "expanded_yd_data.csv"), index=False, encoding='utf-8-sig')
print("✅ Expanded data saved to 'expanded_yd_data.csv'")
# ========================
# 2. 定义哪些字段是“人员字段”(需替换为ID)
# ========================
STAFF_COLUMNS_CHINESE = {
"120天处理人",
"60天处理人",
"30天处理人",
"运营顾问",
"运营专家",
"区域客服",
}
# 构建门店编码 → data_id 映射
df2["门店编码_clean"] = df2["门店编码"].astype(str).str.strip().replace('nan', '')
jdy_store_map = df2.set_index("门店编码_clean")["data_id"].to_dict()
date_fields_chinese = {"120天跟进时间", "60天跟进时间", "30天跟进时间"}
update_records = []
for idx, row in ndf.iterrows():
yd_store_code = str(row.get("textField_ksydghqw", "")).strip()
if not yd_store_code or yd_store_code == "nan":
continue
jdy_id = jdy_store_map.get(yd_store_code)
if not jdy_id:
continue
# 构造 data 字段:每个字段必须是 { "value": ... }
data_dict = {}
for yd_field_id, jdy_chinese in yd_field_id_to_jdy_chinese.items():
raw_val = row.get(yd_field_id, "")
if pd.isna(raw_val) or str(raw_val).strip().lower() in {"", "nan", "-", "", "null"}:
continue
# 处理日期字段
if jdy_chinese in date_fields_chinese:
try:
if isinstance(raw_val, (int, float)) or (
isinstance(raw_val, str) and raw_val.replace('.', '', 1).isdigit()):
ts = float(raw_val)
if ts < 1e12:
ts *= 1000
final_value = int(ts)
else:
dt = pd.to_datetime([str(raw_val)], errors='coerce')[0]
if pd.isna(dt):
raise ValueError("Invalid date")
if dt.tz is None:
dt = dt.tz_localize('Asia/Shanghai')
final_value = int(dt.tz_convert('UTC').timestamp() * 1000)
if not (1577836800000 <= final_value <= 1900000000000):
continue
except Exception as e:
logger.error(f"日期转换失败 [{jdy_chinese}]: {raw_val}, {e}")
continue
else:
str_val = str(raw_val)
final_value = value_mapping.get(jdy_chinese, {}).get(str_val, str_val)
# 如果是人员字段,尝试替换为员工ID
if jdy_chinese in STAFF_COLUMNS_CHINESE:
staff_id = name_to_staff_id.get(str_val)
if staff_id:
final_value = staff_id
else:
logger.warning(f"未找到员工ID,保留原姓名 [{jdy_chinese}]: {str_val}")
jdy_field_id = jdy_map.get(jdy_chinese)
if jdy_field_id:
data_dict[jdy_field_id] = {"value": final_value}
if data_dict:
update_records.append({
"data_id": jdy_id,
"data": data_dict
})
# 批量发送更新请求
logger.info(f"共构造 {len(update_records)} 条更新记录")
APP_ID = "675b900991ad2491c69389ca"
# ENTRY_ID = "6965eec36b73376aa0b5bff8"
ENTRY_ID = "6931063d64187eaf6b927557"
for record in tqdm(update_records):
payload = {
"api_key": APP_ID,
"entry_id": ENTRY_ID,
# "transaction_id": str(uuid.uuid4()), # 推荐:保证幂等
"data_id": record["data_id"],
"data": record["data"],
"is_start_trigger": False
}
res = api_instance.entry_data_update(payload)
# print(res)
+445
View File
@@ -0,0 +1,445 @@
# -*- coding: utf-8 -*-
import sys
import io
import imaplib
import email
import re
from datetime import datetime, timedelta
from email.header import decode_header
import pandas as pd
# 假设 api.py 在当前目录下,且包含 API 类
import requests
from typing import Optional, List, Dict, Any
from decimal import Decimal
import time
import numpy as np
from log_config import configure_task_logger, configure_error_task_logger
import json
# === 强制标准输出为 UTF-8 (兼容不同运行环境) ===
# 注意:在部分 IDE 中重新包装 sys.stdout 可能会导致乱码,若报错可注释掉以下两行
try:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
except AttributeError:
pass
# ================= 配置区域 =================
EMAIL_ACCOUNT = "zhangyang@f6car.cn"
PASSWORD = "RGBdMggmJ4s2FzZK" # ⚠️ 生产环境建议使用环境变量,不要硬编码
IMAP_SERVER = "imap.qiye.aliyun.com"
IMAP_PORT = 993
SUBJECT_KEYWORD = "展会线索登记"
DAYS_TO_SCAN = 30 # 扫描最近30天
OUTPUT_FILE = f"展会线索_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
# 定义标准字段顺序
FIELD_KEYS = ["姓名", "手机号", "", "", "", "公司名称", "备注"]
class API:
def entry_data_list(self, data: dict, replace: bool = False, max_retries: int = 20) -> Dict: # 获取多条表单数据
"""
获取多条表单数据
:param max_retries: 最大重试次数
:param replace: 是否替换字段
:param data:
api_key: 应用id
entry_id: 表单id
:return:
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list'
headers = {
'Authorization': "Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN", # 曹伟应用api测试 app_key
'Content-Type': 'application/json'
}
all_data_batches = [] # 用于存储每次请求返回的数据批次
last_data_id = None
exit_flag = False
while True:
payload = json.dumps({
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"limit": 90,
"data_id": last_data_id,
"filter": data.get('filter', None)
})
retries = 0
while retries <= max_retries:
data_get = None
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
if data_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
retries += 1
time.sleep(0.5) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
retries += 1
time.sleep(0.5) # 在重试之间稍作停顿
if retries > max_retries:
all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理
if exit_flag:
break
# 构建最终返回的字典
final_data = {
'data': all_data_batches # 'data' 键对应的值是列表的列表
}
return final_data
@staticmethod
def data_batch_create(data: dict, max_retries: int = 20) -> Optional[requests.Response]: # 新建单条数据
"""
新建单条表单数据
:param max_retries: 最大重试次数
:param data: 应该包含应用id表单id以及新建的数据data['data']
:return: 返回创建后简道云返回的信息
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/create'
headers = {
'Authorization': "Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN", # 曹伟应用api测试 app_key
'Content-Type': 'application/json'
}
# noinspection DuplicatedCode
payload = json.dumps({
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单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.Response = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
if res.status_code == 200:
return data_get
else:
retries += 1
time.sleep(3) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
retries += 1
time.sleep(3) # 在重试之间稍作停顿
if retries > max_retries:
print(
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
return None
# ===========================================
def decode_mime_words(s):
if not s:
return ""
decoded_parts = []
# decode_header 返回的是 list of (bytes/str, encoding)
for part, encoding in decode_header(s):
if isinstance(part, bytes):
decoded_parts.append(part.decode(encoding or 'utf-8', errors='ignore'))
else:
decoded_parts.append(str(part))
return "".join(decoded_parts)
def extract_data_from_body(body_text):
"""
从邮件正文中提取线索数据
格式姓名 | 手机号 | | | | 公司 | 备注
"""
if not body_text:
return []
data_list = []
# 【修复点 1】splitlines() 是方法,需要加括号
lines = body_text.splitlines()
for line in lines:
line = line.strip()
# 如果行中没有分隔符,跳过
if '|' not in line:
continue
# 按 '|' 分割并去除首尾空格
parts = [p.strip() for p in line.split('|')]
# 【关键校验】至少需要前两个字段(姓名、手机号)非空
if len(parts) < 2 or not parts[0] or not parts[1]:
continue
# 构建字典,动态映射
record = {}
for i, key in enumerate(FIELD_KEYS):
if i < len(parts):
record[key] = parts[i]
else:
record[key] = "" # 缺失的字段填空字符串
data_list.append(record)
return data_list
def save_to_excel(leads, filename):
if not leads:
return None
df = pd.DataFrame(leads)
# 定义期望的列顺序
cols = ["姓名", "手机号", "", "", "", "公司名称", "备注", "来源邮件时间"]
# 确保列存在且顺序正确
# 先保留所有现有列中在 cols 里的,按 cols 顺序
ordered_cols = [c for c in cols if c in df.columns]
# 再加上可能存在的其他列(虽然逻辑上不应该有,但以防万一)
other_cols = [c for c in df.columns if c not in cols]
final_cols = ordered_cols + other_cols
df = df[final_cols]
# df.to_excel(filename, index=False)
return df
def main():
print(f"正在连接 IMAP 服务器:{IMAP_SERVER} ...")
mail = None
start_date = datetime.now() - timedelta(days=DAYS_TO_SCAN)
date_str = start_date.strftime("%d-%b-%Y").upper()
all_leads = []
count_processed = 0
try:
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
mail.login(EMAIL_ACCOUNT, PASSWORD)
mail.select("INBOX")
print(f"正在搜索 [{date_str}] 之后的邮件...")
search_query = f'(SINCE "{date_str}")'
status, messages = mail.search(None, search_query)
if status != "OK":
print("❌ 搜索失败")
return
mail_ids = messages[0].split()
if not mail_ids:
print(f"✅ 未找到 {date_str} 之后的新邮件。")
return
print(f"📩 找到 {len(mail_ids)} 封近期邮件,开始详细扫描...")
for mail_id in mail_ids:
try:
status, msg_data = mail.fetch(mail_id, "(RFC822)")
if status != "OK":
continue
raw_email = msg_data[0][1]
if isinstance(raw_email, bytes):
mime_msg = email.message_from_bytes(raw_email)
else:
mime_msg = email.message_from_string(raw_email.decode('utf-8', errors='ignore'))
subject = decode_mime_words(mime_msg.get("Subject"))
if SUBJECT_KEYWORD not in subject:
continue
count_processed += 1
date_str_full = mime_msg.get("Date")
body_content = ""
if mime_msg.is_multipart():
for part in mime_msg.walk():
content_disposition = part.get_content_disposition()
if content_disposition and "attachment" in str(content_disposition):
continue
content_type = part.get_content_type()
if content_type in ["text/plain", "text/html"]:
try:
charset = part.get_content_charset() or 'utf-8'
payload = part.get_payload(decode=True)
if payload:
text = payload.decode(charset, errors='ignore') if isinstance(payload,
bytes) else str(
payload)
if content_type == "text/html":
text = re.sub(r'<[^>]+>', ' ', text)
body_content += text + "\n"
except Exception:
pass
else:
try:
charset = mime_msg.get_content_charset() or 'utf-8'
payload = mime_msg.get_payload(decode=True)
if payload:
body_content = payload.decode(charset, errors='ignore') if isinstance(payload,
bytes) else str(
payload)
except Exception:
pass
leads = extract_data_from_body(body_content)
for lead in leads:
lead["来源邮件时间"] = date_str_full
if leads:
print(f"[{subject}] -> 提取 {len(leads)}")
all_leads.extend(leads)
except Exception as e:
print(f"处理邮件 ID {mail_id} 时出错:{e}")
continue
# ================= 新增:本地数据去重逻辑 =================
original_count = len(all_leads)
if original_count > 0:
seen_phones = set()
unique_leads = []
for lead in all_leads:
phone = str(lead.get("手机号", "")).strip()
# 如果手机号为空,或者已经出现过,则跳过
if not phone or phone in seen_phones:
continue
seen_phones.add(phone)
unique_leads.append(lead)
all_leads = unique_leads
removed_count = original_count - len(all_leads)
if removed_count > 0:
print(
f"\n⚠️ 检测到重复数据,已根据【手机号】去重:原始 {original_count} 条 -> 去重后 {len(all_leads)} 条 (移除 {removed_count} 条)")
else:
print(f"\n✅ 数据检查完成,无重复手机号。共 {len(all_leads)} 条。")
# =======================================================
df = save_to_excel(all_leads, OUTPUT_FILE)
if df is not None:
print(f"\n✅ 成功!共扫描 {count_processed} 封匹配邮件,最终有效线索 {len(all_leads)} 条。")
print(f"文件已保存至:{OUTPUT_FILE}")
else:
print(f"\n⚠️ 扫描完成,但在 {count_processed} 封近期邮件中未找到符合格式的数据。")
return # 如果没有数据,后续同步逻辑无需执行
# 同步至简道云
if all_leads:
print("\n开始同步至简道云...")
api_instance = API()
payload_query = {
"api_key": "66b9678280b37f8a276b1d01",
"entry_id": "69b22dc5434e05c7b6b4b5b2",
}
try:
response = api_instance.entry_data_list(payload_query)
now_data = response.get("data", []) if response else []
existing_phones = set()
phone_widget_id = "_widget_1692928669587"
for item in now_data:
phone_val = item.get(phone_widget_id)
if phone_val:
existing_phones.add(str(phone_val).strip())
print(f"简道云现有手机号数量:{len(existing_phones)}")
new_count = 0
# 此时 df 已经是去重后的数据,且 all_leads 也是去重后的
# 再次遍历 df 确保只提交本地没有的(防止简道云已有但本地没查到的情况,虽然逻辑上上面已经过滤了)
# 为了代码健壮性,这里保留原有的 existing_phones 检查逻辑
for index, row in df.iterrows():
current_phone = str(row["手机号"]).strip()
if not current_phone:
continue
# 双重保险:如果简道云里已经有了,跳过
if current_phone in existing_phones:
print(f"跳过 (云端已存在): {current_phone}")
continue
new_payload = {
"api_key": "66b9678280b37f8a276b1d01",
"entry_id": "69b22dc5434e05c7b6b4b5b2",
"data": {
"_widget_1690785229260": {"value": row.get("姓名", "")},
"_widget_1690785229261": {"value": row.get("公司名称", "")},
"_widget_1692928669587": {"value": row.get("手机号", "")},
"_widget_1690785229266": {"value": row.get("备注", "")},
"_widget_1690785326597": {"value": {"province": row.get("", ""),
"city": row.get("", ""),
"district": row.get("", ""),
"detail": row.get("", "") + row.get("",
"") + row.get(
"", ""),
}
},
"_widget_1690785229279": {"value": row.get("", "")},
"_widget_1773381838511": {"value": row.get("", "")},
"_widget_1692070309987": {"value": row.get("", "")},
}
}
result = api_instance.data_batch_create(new_payload)
if result and (result.get("success") or result.get("code") == 200 or "error" not in result):
new_count += 1
print(f"新增成功:{current_phone} ({row.get('姓名')})")
else:
print(f"提交结果:{current_phone}, 返回:{result}")
# 如果提交成功但返回格式奇怪,也可以考虑计入成功,视具体API文档而定
# 这里保守处理,只有明确成功才算
print(f"\n✅ 同步完成,本次新增 {new_count} 条数据。")
except Exception as api_err:
print(f"\n❌ 简道云 API 交互错误:{api_err}")
import traceback
traceback.print_exc()
except imaplib.IMAP4.error as e:
print("\n❌ IMAP 协议错误:")
print(f"错误详情:{e}")
except Exception as e:
print("\n❌ 发生严重错误:")
import traceback
traceback.print_exc()
finally:
if mail:
try:
mail.close()
mail.logout()
except:
pass
if __name__ == "__main__":
main()
@@ -0,0 +1,556 @@
import datetime
import os
import time
import requests
from api import API
from back_ground_module import CommonModule
import pandas as pd
from log_config import configure_task_logger, configure_error_task_logger
api_instance = API()
common_module = CommonModule()
# start_time = datetime.datetime.now()
# 获取已经配置好的常规日志记录器
logger = configure_task_logger()
# 获取已经配置好的错误任务日志记录器
error_task_logger = configure_error_task_logger()
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
class NewExceptionTask:
"""
SaaS异常回访
"""
def __init__(self):
self.exception_service_todo = None
self.get_feature_usage = None
self.saas_create_time = None
self.index = None
self.date_one = None
self.data_yichang_S = None
self.date_list = None
self.Smart_detection = None
self.service_remind = None
self.NGV_data_list = None
self.permissions_table = None
self.staff_id_list = None
self.json_list = []
self.policy_recognition = None
self.widget_list = None
self.private_domain = None
self.public_domain = None
self.public_domain_list = None
self.different_industries = None
self.different_industries_list = None
self.groupnotification = None
self.fields_mapping = {
"门店名称": "_widget_1748241895830",
"联系人": "_widget_1748241895831",
"开户时间": "_widget_1748241895839",
"门店编码": "_widget_1748241895842",
"联系方式": "_widget_1748241895832",
"系统版本": "_widget_1748241895850",
"公司名称": "_widget_1748241895844",
"运营顾问": "_widget_1748246808679",
"区域经理": "_widget_1748246808682",
"公司等级": "_widget_1748241895846",
"运营专家": "_widget_1748246808681",
"操作模式E.L/E.S": "_widget_1748241895853",
"活跃健康状态变化": "_widget_1748241895829",
"初始日": "_widget_1748241895833",
"推进日": "_widget_1748241895834",
"异常跟进情况描述": "_widget_1748512176640",
"异常变化原因": "_widget_1748512176641",
"正常使用": "_widget_1748512176643",
"门店原因": "_widget_1748512176645",
"服务原因": "_widget_1748512176647",
"产品原因": "_widget_1748512176649",
"未正式切换": "_widget_1748512176651",
"跟进状态": "_widget_1748512176655",
"是否可激活": "_widget_1758615839701",
"是否有续约风险": "_widget_1758615839703",
"当前跟进人": "_widget_1748246808678",
"激活策略": "_widget_1758615839717",
"跟进时间": "_widget_1748512176654",
"是否跟进完成": "_widget_1751273412737",
"区域客服": "_widget_1748246808680",
"大区": "_widget_1748241895847",
"": "_widget_1748241895848",
"城市": "_widget_1748241895855",
"门店类型": "_widget_1748241895849",
"saas客户类型": "_widget_1748241895851",
"门店阶段": "_widget_1748241895852",
"提交人": "creator",
"提交时间": "createTime",
"更新时间": "updateTime"
}
def calculate_date_one(self, start_offset=0):
"""
计算从当前日期或指定偏移量的日期开始往前遍历遇到date_list中日期的次数
参数:
- start_offset: 从当前日期起始的天数偏移量默认为0即今天负数表示过去正数表示未来
返回:
- date_one: 遍历到date_list中日期的次数
"""
jdy_date = datetime.datetime.now().strftime("%Y-%m-%d")
jdy_start_time = datetime.datetime.now().strftime("%Y-%m-%d ")
# 设置起始日期
now_time = datetime.datetime.now() + datetime.timedelta(days=start_offset)
# 初始化计数器
date_one = 1
print("当前日期:", now_time.strftime("%Y-%m-%d"))
# 检查起始日期是否在date_list中
if now_time.strftime("%Y-%m-%d") in self.date_list:
date_one = 0
print("开始次数:", date_one)
else:
# 遍历日期
for i in range(1, 10):
new_date = now_time + datetime.timedelta(days=-i)
new_date_str = new_date.strftime("%Y-%m-%d")
print("遍历日期:", new_date_str)
if new_date_str in self.date_list:
date_one += 1
print("节假日期:", new_date_str)
else:
break
print("遍历次数:", date_one)
return date_one
@staticmethod
def download_url_content(url, save_path):
"""
下载指定 URL 的内容并保存到本地文件
:param url: 要下载内容的 URL
:param save_path: 保存文件的路径
"""
try:
# 发送 GET 请求以获取内容
response = requests.get(url, stream=True)
response.raise_for_status() # 如果响应状态码不是 200,抛出异常
# 确保保存目录存在
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# 将内容写入文件
with open(save_path, 'wb') as file:
for chunk in response.iter_content(chunk_size=8192): # 分块写入,避免占用过多内存
if chunk: # 过滤掉空块
file.write(chunk)
print(f"文件已成功保存到 {save_path}")
except requests.exceptions.RequestException as e:
print(f"下载失败: {e}")
except Exception as e:
print(f"发生错误: {e}")
def load_all_data(self):
"""加载所有必要的数据表"""
# 省市区人员关系表
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "676512ac3e54dc3159460c0a"}
json_dict = api_instance.entry_data_list(payload)
self.json_list = json_dict.get("data")
# 获取简道云员工id
payload = {"api_key": "6694d3c4fcb69ca9a111a6c4",
"entry_id": "6769204a1902c9341340a1bc",
}
staff_id = api_instance.entry_data_list(payload)
self.staff_id_list = staff_id.get("data") # api请求格式,将数据封装在data字典里
# 获取NGV数据
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "675bb02bd2d53c2034c665e4"}
self.NGV_data_list = api_instance.entry_data_list(payload).get("data", [])
# print("NGV获取后的类型:", type(self.NGV_data_list))
# 获取异常服务待办(添加过滤进行中的订单)
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "68340de79f116c0b66b6b0cc",
"filter": {"rel": "and",
"cond": [{"field": "flowState", "type": "flowstate", "method": "eq", "value": [0]}]}}
self.exception_service_todo = api_instance.entry_data_list(payload).get("data", [])
# print(self.exception_service_todo)
@staticmethod
def build_index(json_list):
index = {}
for json_item in json_list:
try:
key = (json_item['_widget_1734677164861'], json_item['_widget_1734677164862'],
json_item['_widget_1734677164863']) # 省市区
if '_widget_1734677164870' not in json_item: # 异常回访客服
raise KeyError("缺少 '异常回访客服'")
index[key] = json_item
except KeyError as e:
print(f"警告:{e},跳过该条记录: {json_item}")
continue
print('index', index)
return index
@staticmethod
def find_customer_service(province_name, city_name, area_name, index):
key = (province_name, city_name, area_name)
# print(index)
if key not in index:
return "数据缺失: 未找到对应的异常回访客服"
return index[key]
@staticmethod
def get_staff_id(row_item, name):
"""辅助函数,用于获取员工ID"""
if str(row_item["_widget_1734942794144"]) == str(name): # 检查姓名是否匹配
return row_item["_widget_1734942794145"] # 返回员工ID
return None
def assign_customer_service(self, province_name, city_name, area_name, index):
"""根据省市区派发给异常回访客服"""
# try:
customer_service_info = self.find_customer_service(province_name, city_name, area_name, index)
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
return customer_service
# except Exception as e:
# print(f"Error finding customer service: {e}")
# return "分配失败,请检查", "分配失败,请检查", "分配失败,请检查"
def main(self):
task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
all_data = []
try:
self.load_all_data()
data = common_module.get_yichang_details(days_back=1)
self.data_yichang_S = pd.DataFrame() if data is None or data.empty else data.astype(str)
self.index = self.build_index(self.json_list)
logger.info("开始运行SaaS异常回访")
if self.data_yichang_S.empty:
logger.info("未获取到数据或数据为空")
common_module.send_task_status(task_start_time, "异常服务待办派发")
return
data_yichang = self.data_yichang_S.copy()
# data_yichang.to_csv(os.path.join(output_dir,"data_yichang.csv"), index=False)
def replace_values(series):
# 使用条件判断来进行替换
return series.apply(lambda x: '' if pd.isna(x) or x in ['NA', 'None', ''] else x)
# 对整个DataFrame的所有列应用替换函数
data_yichang = data_yichang.apply(replace_values)
error_data = []
for index_num, row in data_yichang.iterrows(): # 对过滤后的每一条进行派发
try:
# 每次循环前清空省市区变量
province_name = None
city_name = None
area_name = None
is_pass = False
for exception_service in self.exception_service_todo:
# 通过查询筛选进行中的逻辑
if exception_service['_widget_1748241895842'] == row['org_code']:
is_pass = True
break
if is_pass:
logger.info(f"已存在待办,跳过该条记录: {row}")
continue
payload_dict = {}
distribution_date = datetime.datetime.now(datetime.timezone.utc)
distribution_date = distribution_date.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
date_obj1 = datetime.datetime.strptime(row["init_day"], "%Y%m%d").strftime("%Y-%m-%d")
date_obj2 = datetime.datetime.strptime(row["push_day"], "%Y%m%d").strftime("%Y-%m-%d")
NGV_roles = {
'service_impl_principal': row['service_impl_principal'], # 运营负责人
'area_manager': row['area_manager'], # 区域经理
'technician': row['technician'], # 运营专家
}
for role, name in NGV_roles.items(): # 寻找对应的员工ID
for row_item in self.staff_id_list:
staff_id = self.get_staff_id(row_item, name)
if staff_id:
NGV_roles[role] = staff_id
break # 找到后退出循环
else:
NGV_roles[role] = None # 如果没有找到对应的员工ID
relationship_manager, area_manager, technician = [NGV_roles[role] for role in
['service_impl_principal',
'area_manager',
'technician']]
UUid = time.strftime("%Y%m%d%H%M%S", time.localtime())
NGV_data_id = None
reason = None
create_exception = None
create_date = None
# 优先从 data_yichang_S 获取省市区信息
province_name = row.get('province_name')
city_name = row.get('city_name')
area_name = row.get('area_name') if 'area_name' in row else row.get('district_name')
# 检查省市区是否完整(省市区是一体的,任意一个缺失就需要从NGV获取)
use_ngv_location = False
if (not province_name or province_name in ['', 'None', 'NA'] or
not city_name or city_name in ['', 'None', 'NA'] or
not area_name or area_name in ['', 'None', 'NA']):
use_ngv_location = True
logger.info(f"门店 {row['org_code']} 的省市区信息不完整,将从NGV_data_list获取")
stop_date = None
# 获取关联数据
for NGV_Data in self.NGV_data_list:
# NGV_Data = NGV_Data.get("data")
if row["org_code"] == NGV_Data.get("_widget_1734062123071"): # 门店编码
NGV_data_id = NGV_Data.get("_id")
# 如果需要从 NGV_data_list 获取省市区信息
if use_ngv_location:
province_name = NGV_Data.get("_widget_1734062123090")
city_name = NGV_Data.get("_widget_1734062123092")
area_name = NGV_Data.get("_widget_1734062123094")
logger.info(
f"【从NGV获取省市区】门店 {row['org_code']}: {province_name}, {city_name}, {area_name}")
# 门店原因
reason = NGV_Data.get("_widget_1758617393828")
logger.info(f"获取关联数据成功:{NGV_data_id}, {province_name}, {city_name}, {area_name}")
# 是否生成异常待办
create_exception = NGV_Data.get("_widget_1758769279995")
# 获取上线日期(文本)# 202512.3改为开户日
create_date = NGV_Data.get("_widget_1734062123081")
# 获取暂停派发日期
stop_date = NGV_Data.get("_widget_1772610343227", None)
break # 找到匹配的数据后退出循环
# 定义可能的日期格式(灵活应对不同格式)
date_formats = [
"%Y-%m-%d %H:%M:%S", # 含时间
"%Y-%m-%d", # 仅日期
"%Y/%m/%d",
"%Y/%m/%d %H:%M:%S"
]
if stop_date:
# 解析暂停派发日期
parsed_stop_date = None
stop_value = stop_date.get("value") if isinstance(stop_date, dict) else stop_date
if isinstance(stop_value, (int, float)):
parsed_stop_date = datetime.datetime.fromtimestamp(
stop_value / 1000, tz=datetime.timezone.utc
).replace(tzinfo=None)
elif isinstance(stop_value, str):
stop_str = stop_value.strip()
iso_candidate = stop_str[:-1] + "+00:00" if stop_str.endswith("Z") else stop_str
try:
iso_dt = datetime.datetime.fromisoformat(iso_candidate)
except ValueError:
iso_dt = None
if iso_dt is not None:
parsed_stop_date = iso_dt.astimezone(datetime.timezone.utc).replace(tzinfo=None) if iso_dt.tzinfo else iso_dt
else:
for fmt in date_formats:
try:
parsed_stop_date = datetime.datetime.strptime(stop_str, fmt)
logger.debug(f"使用格式 {fmt} 成功解析暂停派发日期: {parsed_stop_date}")
break
except ValueError:
continue
if parsed_stop_date:
# 获取当前UTC时间
current_utc_time = datetime.datetime.utcnow()
logger.debug(f"当前UTC时间: {current_utc_time}")
logger.debug(f"暂停派发日期: {parsed_stop_date}")
# 比较时间
if current_utc_time < parsed_stop_date:
logger.info(f"当前UTC时间低于暂停派发日期,跳过派发")
continue
# 判断门店原因
# if reason in ["门店倒闭", "门店转让", "加盟其他连锁","切换竞品","虚拟门店","重新开户","已退款","二套系统"]:
# continue
# 判断是否继续生成异常待办
if create_exception == "":
continue
# 新增:检查 create_date_str 是否存在且有效
create_date_value = create_date.get("value") if isinstance(create_date, dict) else create_date
if not create_date_value:
logger.warning("上线日期为空,跳过该记录")
continue
parsed_date = None
if isinstance(create_date_value, (int, float)):
local_tz = datetime.timezone(datetime.timedelta(hours=8))
parsed_date = datetime.datetime.fromtimestamp(create_date_value / 1000, tz=local_tz).date()
elif isinstance(create_date_value, str):
create_str = create_date_value.strip()
iso_candidate = create_str[:-1] + "+00:00" if create_str.endswith("Z") else create_str
try:
iso_dt = datetime.datetime.fromisoformat(iso_candidate)
except ValueError:
iso_dt = None
if iso_dt is not None:
local_tz = datetime.timezone(datetime.timedelta(hours=8))
parsed_date = iso_dt.date() if iso_dt.tzinfo is None else iso_dt.astimezone(local_tz).date()
else:
for fmt in date_formats:
try:
parsed_date = datetime.datetime.strptime(create_str, fmt).date()
logger.debug(f"使用格式 {fmt} 成功解析日期: {parsed_date}")
break
except ValueError:
continue
if parsed_date is None:
logger.error(f"无法解析上线日期: '{create_date_value}',支持的格式: %Y-%m-%d, %Y-%m-%d %H:%M:%S 等")
continue # 解析失败,跳过
# 使用解析后的日期进行判断
now_date = datetime.date.today()
delta = now_date - parsed_date
days_diff = delta.days
if days_diff > 30:
logger.info(f"上线日期 {parsed_date} 超过30天({days_diff}天),生成待办")
# ✅ 继续后续待办创建逻辑
else:
logger.info(f"上线日期 {parsed_date} 在30天内,跳过处理")
continue
if not NGV_data_id:
logger.warning(f"未找到关联数据,请检查门店编码: {row['org_code']}")
# 根据省市区派发给异常回访客服
# 检查省市区是否都有值,如果有任何一个为空,则客服为空
if (not province_name or province_name in ['', 'None', 'NA'] or
not city_name or city_name in ['', 'None', 'NA'] or
not area_name or area_name in ['', 'None', 'NA']):
customer_service = None
logger.warning(f"【省市区信息缺失】门店 {row['org_code']} 省市区信息不完整,异常回访客服设置为空")
logger.warning(f"省: {province_name}, 市: {city_name}, 区: {area_name}")
else:
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
logger.info(f"【派发客服】门店 {row['org_code']} 派发给客服: {customer_service}")
payload_dict.update({
"_widget_1748241895829": {"value": row["health_warning_info"]}, # 活跃健康状态变化
"_widget_1748241895830": {"value": row["org_name"]}, # 门店名称
"_widget_1748241895831": {"value": row["contacts"]}, # 联系人
"_widget_1748241895832": {"value": row['contact_mobile']}, # 联系方式
"_widget_1748241895833": {
"value": int(time.mktime(time.strptime(date_obj1, "%Y-%m-%d")) * 1000) if row[
"init_day"] != '' else ''},
# 初始日
"_widget_1748241895834": {
"value": int(time.mktime(time.strptime(date_obj2, "%Y-%m-%d")) * 1000) if row[
"push_day"] != '' else ''},
# 推进日
"_widget_1748246808678": {"value": customer_service}, # 当前跟进人
# "_widget_1748246808678": {"value": "083726094935447433"}, # 当前跟进人
"_widget_1748246808679": {"value": relationship_manager}, # 运营负责人
"_widget_1748246808680": {"value": customer_service}, # 区域客服
"_widget_1748241895839": {
"value": int(time.mktime(time.strptime(row["saas_create_time"], "%Y-%m-%d")) * 1000) if row[
"saas_create_time"] != '' else ''},
# 开户时间
"_widget_1748246808681": {"value": technician}, # 技术专家
"_widget_1748246808682": {"value": area_manager}, # 区域经理
"_widget_1748241895842": {"value": row['org_code']}, # 门店编码
"_widget_1748241895844": {"value": row['group_name']}, # 公司名称
"_widget_1748241895846": {"value": row['group_grade']}, # 公司等级
"_widget_1748241895847": {"value": row['region_name']}, # 大区
"_widget_1748241895848": {"value": row['province_name']}, # 省
"_widget_1748241895849": {"value": row['org_type']}, # 门店类型
"_widget_1748241895850": {"value": row['saas_edition_fmt']}, # 系统版本
"_widget_1748241895851": {"value": row['saas_customer_type']}, # saas客户类型
"_widget_1748241895852": {"value": row['org_stage']}, # 门店阶段
"_widget_1748241895853": {"value": row['contact_mobile']}, # 操作模式E.L/E.S
"_widget_1748241895855": {"value": row['city_name']}, # 城市
"_widget_1748247754304": {"value": NGV_data_id}, # 数据id
"_widget_1748512176655": {"value": "未处理"}, # 跟进状态
"_widget_1772761760440":{"value": "客服跟进节点"}, # 当前跟进节点
})
routine_follow_up_payload = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "68340de79f116c0b66b6b0cc", # 异常服务跟进待办
"is_start_workflow": "true",
"data": payload_dict,
"transaction_id": UUid
}
all_data.append(routine_follow_up_payload)
# res = api_instance.data_batch_create(routine_follow_up_payload)
# logger.info(f"创建结果:{res}")
except Exception as e:
error_task_logger.exception(f"异常服务待办派发执行时发生异常: {e}")
error_data.append(row)
pass
if error_data:
error_df = pd.DataFrame(error_data)
error_df.to_csv(os.path.join(output_dir, "异常派发错误数据.csv"))
common_module.send_task_error(task_start_time=task_start_time, task_name="异常服务待办派发",
error_message="失败文件中省市区匹配不到,需要通过门店编码在客户资料表中查询正确的省市区,并更新到省市区人员关系表中",
df=error_df)
ndf = pd.DataFrame(all_data)
ndf.to_csv(os.path.join(output_dir, "异常派发.csv"))
common_module.send_task_status(task_start_time, "异常服务待办派发")
except Exception as e:
error_task_logger.error(f"异常服务待办派发执行时发生异常: {e}")
common_module.send_task_error(task_start_time, "异常服务待办派发", str(e))
if __name__ == '__main__':
start = NewExceptionTask()
start.main()
+139
View File
@@ -0,0 +1,139 @@
import pymysql
import sys
import time
# ================== 配置信息 ==================
SOURCE_CONFIG = {
'host': "f6-public.rwlb.rds.aliyuncs.com",
'user': "rw_operation_data_relay",
'password': "m+q5Z4%IVuF9bf",
'database': "f6operation_data_relay",
'connect_timeout': 30,
'read_timeout': 600,
'write_timeout': 600
}
TARGET_CONFIG = {
'host': "db-f6operation-sst.f6car.org",
'user': "rw_operation",
'password': "tDm45eBj@upzLydHc",
'database': "f6operation_data_relay",
'connect_timeout': 30,
'read_timeout': 600,
'write_timeout': 600
}
TABLE_NAME = 'rpt_customized_maintain_detail'
READ_BATCH_SIZE = 1000 # 每次从源库读 5000 行
WRITE_BATCH_SIZE = 1000 # 每次向目标库写 5000 行
# ==============================================
def main():
print("🔧 正在从源数据库读取表结构...")
# === 第一步:获取建表语句 ===
source_conn = None
try:
source_conn = pymysql.connect(**SOURCE_CONFIG)
with source_conn.cursor() as cursor:
cursor.execute(f"SHOW CREATE TABLE `{TABLE_NAME}`")
result = cursor.fetchone()
if not result:
raise Exception(f"{TABLE_NAME} 不存在于源数据库")
create_table_sql = result[1]
except Exception as e:
print(f"❌ 读取表结构失败: {e}")
sys.exit(1)
finally:
if source_conn:
source_conn.close()
# === 第二步:获取总行数(用于进度)===
total_rows = 0
try:
source_conn = pymysql.connect(**SOURCE_CONFIG)
with source_conn.cursor() as cursor:
cursor.execute(f"SELECT COUNT(*) FROM `{TABLE_NAME}`")
total_rows = cursor.fetchone()[0]
except Exception as e:
print(f"⚠️ 无法获取总行数(不影响迁移): {e}")
finally:
if source_conn:
source_conn.close()
print(f"📊 表共约 {total_rows} 行,将分批读取和写入...")
# === 第三步:重建目标表 ===
target_conn = None
columns = []
try:
target_conn = pymysql.connect(**TARGET_CONFIG)
with target_conn.cursor() as cursor:
cursor.execute(f"DROP TABLE IF EXISTS `{TABLE_NAME}`")
new_create_sql = create_table_sql.replace(f"`{SOURCE_CONFIG['database']}`.", "")
cursor.execute(new_create_sql)
# 获取列名(从建表语句解析较复杂,改用查一次空结果)
cursor.execute(f"SELECT * FROM `{TABLE_NAME}` LIMIT 0")
columns = [desc[0] for desc in cursor.description]
target_conn.commit()
print("✅ 目标表已重建")
except Exception as e:
print(f"❌ 重建目标表失败: {e}")
if target_conn:
target_conn.close()
sys.exit(1)
# === 第四步:分批读取 + 分批写入 ===
offset = 0
inserted_total = 0
while True:
time.sleep(5)
# 从源库读取一批
batch_rows = []
try:
source_conn = pymysql.connect(**SOURCE_CONFIG)
with source_conn.cursor(pymysql.cursors.DictCursor) as cursor:
cursor.execute(
f"SELECT * FROM `{TABLE_NAME}` LIMIT %s OFFSET %s",
(READ_BATCH_SIZE, offset)
)
batch_rows = cursor.fetchall()
source_conn.close()
except Exception as e:
print(f"❌ 读取第 {offset//READ_BATCH_SIZE + 1} 批数据失败: {e}")
break
if not batch_rows:
print("🔚 数据读取完成")
break
# 转换为元组
data_tuples = [tuple(row[col] for col in columns) for row in batch_rows]
# 写入目标库(可再分小批,但这里 batch_rows 已是 5000
try:
with target_conn.cursor() as cursor:
placeholders = ', '.join(['%s'] * len(columns))
insert_sql = f"INSERT INTO `{TABLE_NAME}` (`{'`, `'.join(columns)}`) VALUES ({placeholders})"
cursor.executemany(insert_sql, data_tuples)
target_conn.commit()
inserted_total += len(data_tuples)
print(f"📤 已写入 {inserted_total} / {total_rows}")
except Exception as e:
print(f"❌ 写入失败(批次 offset={offset}: {e}")
target_conn.rollback()
break
offset += READ_BATCH_SIZE
# === 清理 ===
if target_conn and target_conn.open:
target_conn.close()
print(f"🎉 迁移完成!共写入 {inserted_total}")
if __name__ == "__main__":
main()
@@ -0,0 +1,899 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 数据库验证脚本 - 数据处理部分\n",
"\n",
"本notebook用于调试和验证数据库验证脚本的数据处理逻辑(260-425行)\n",
"\n",
"## 使用说明\n",
"1. 先执行数据加载部分(第2个单元格),这部分比较耗时\n",
"2. 数据加载完成后,再执行后续的数据处理单元格\n",
"3. 每个单元格都可以单独执行和调试\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T06:53:03.604128900Z",
"start_time": "2026-01-16T06:53:01.840121200Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"库导入完成\n",
"项目根目录: D:\\Idea Project\\SaaS_V1.7\n"
]
}
],
"source": [
"# 导入必要的库\n",
"import os\n",
"import sys\n",
"import pandas as pd\n",
"import datetime\n",
"from datetime import datetime, timedelta\n",
"import re\n",
"\n",
"# 添加项目根目录到路径(notebook文件在test目录下,需要添加父目录)\n",
"current_dir = os.getcwd()\n",
"# 如果当前目录是test,则添加父目录;否则添加当前目录\n",
"if os.path.basename(current_dir) == 'test':\n",
" project_root = os.path.dirname(current_dir)\n",
"else:\n",
" project_root = current_dir\n",
"sys.path.insert(0, project_root)\n",
"\n",
"from api import API\n",
"from back_ground_module import CommonModule\n",
"from log_config import configure_task_logger, configure_error_task_logger\n",
"\n",
"# 初始化API和CommonModule\n",
"api_instance = API()\n",
"common_module = CommonModule()\n",
"\n",
"# 获取日志记录器\n",
"logger = configure_task_logger()\n",
"error_task_logger = configure_error_task_logger()\n",
"\n",
"print(\"库导入完成\")\n",
"print(f\"项目根目录: {project_root}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 步骤1: 数据加载(耗时操作,可单独执行)\n",
"\n",
"这部分会加载所有必要的数据,包括:\n",
"- 省市区人员关系表\n",
"- 员工ID列表\n",
"- 权限表\n",
"- NGV数据列表\n",
"- 服务提醒数据\n",
"- 智能检测数据\n",
"- 功能使用情况表\n",
"- 保单识别表\n",
"- 私域/公域小程序数据\n",
"- 异业合作数据\n",
"- 短信数据\n",
"- 多公司过滤表\n",
"- NGV明细数据(从数据库获取)\n",
"- 节假日列表\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 数据加载部分 ==========\n",
"# 这部分比较耗时,可以单独执行\n",
"\n",
"print(\"开始加载数据...\")\n",
"\n",
"# 省市区人员关系表\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676512ac3e54dc3159460c0a\"}\n",
"json_dict = api_instance.entry_data_list(payload)\n",
"if json_dict and \"data\" in json_dict:\n",
" json_list = json_dict.get(\"data\")\n",
"else:\n",
" print(\"加载省市区人员关系表失败\")\n",
" json_list = []\n",
"print(f\"省市区人员关系表: {len(json_list)} 条\")\n",
"\n",
"# 获取简道云员工id\n",
"payload = {\"api_key\": \"6694d3c4fcb69ca9a111a6c4\", \"entry_id\": \"6769204a1902c9341340a1bc\"}\n",
"staff_id = api_instance.entry_data_list(payload)\n",
"staff_id_list = staff_id.get(\"data\")\n",
"print(f\"员工ID列表: {len(staff_id_list)} 条\")\n",
"\n",
"# 获取权限表信息\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"675b96c14e839f90fef1647c\"}\n",
"permissions_table = api_instance.entry_data_list(payload).get(\"data\")\n",
"print(f\"权限表: {len(permissions_table)} 条\")\n",
"\n",
"# 获取NGV数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"675bb02bd2d53c2034c665e4\"}\n",
"NGV_data_list = api_instance.entry_data_list(payload).get(\"data\")\n",
"print(f\"NGV数据列表: {len(NGV_data_list)} 条\")\n",
"\n",
"# 获取服务提醒-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676bb7bda3029720f1083e99\"}\n",
"service_remind = api_instance.entry_data_list(payload).get(\"data\")\n",
"print(f\"服务提醒数据: {len(service_remind)} 条\")\n",
"\n",
"# 获取智能检测-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676bb99649ab3ac975af6e39\"}\n",
"Smart_detection = api_instance.entry_data_list(payload).get(\"data\")\n",
"print(f\"智能检测数据: {len(Smart_detection)} 条\")\n",
"\n",
"# 获取功能使用情况表\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"6763bbf657bd8fb76fcb41b2\"}\n",
"get_feature_usage = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"print(f\"功能使用情况表: {len(get_feature_usage)} 条\")\n",
"\n",
"# 获取保单识别表\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"6773a60d30ed87ff9f68d3c5\"}\n",
"policy_recognition = api_instance.entry_data_list(payload).get(\"data\")\n",
"widget_list = [item['_widget_1735632397600'] for item in policy_recognition]\n",
"print(f\"保单识别表: {len(policy_recognition)} 条\")\n",
"\n",
"# 获取私域小程序-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e0f0fae622896749ba5087\"}\n",
"private_domain = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"print(f\"私域小程序数据: {len(private_domain)} 条\")\n",
"\n",
"# 获取公域小程序-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e0c702c8f603b997980999\"}\n",
"public_domain = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"public_domain_list = [item['_widget_1742784257506'] for item in public_domain]\n",
"print(f\"公域小程序数据: {len(public_domain)} 条\")\n",
"\n",
"# 获取异业合作-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e24fdd8dfcfa918e17c30b\"}\n",
"different_industries = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"different_industries_list = [item['_widget_1742884829007'] for item in different_industries]\n",
"print(f\"异业合作数据: {len(different_industries)} 条\")\n",
"\n",
"# 获取短信-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e5107198ba1b20d5df3974\"}\n",
"groupnotification = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"print(f\"短信数据: {len(groupnotification)} 条\")\n",
"\n",
"# 获取多公司过滤表\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"689bf5f8ba88a28cb0679ec9\"}\n",
"get_filter_company_list = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"print(f\"多公司过滤表: {len(get_filter_company_list)} 条\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\n",
"# 获取节假日列表\n",
"date_list = common_module.get_holiday_list()\n",
"print(f\"节假日列表: {len(date_list)} 个日期\")\n",
"\n",
"# 获取NGV明细数据(从数据库获取,比较耗时)\n",
"print(\"\\n开始从数据库获取NGV明细数据(这可能需要一些时间)...\")\n",
"data_NGV = common_module.get_ngv_details(days_back=1)\n",
"print(f\"NGV明细数据: {len(data_NGV)} 条\")\n",
"print(f\"NGV明细数据列数: {len(data_NGV.columns)}\")\n",
"\n",
"# 构建省市区索引\n",
"def build_index(json_list):\n",
" index = {}\n",
" for json_item in json_list:\n",
" try:\n",
" key = (json_item['_widget_1734677164861'], json_item['_widget_1734677164862'],\n",
" json_item['_widget_1734677164863']) # 省市区\n",
" if '_widget_1734677164871' not in json_item: # 日常回访客服\n",
" raise KeyError(\"缺少 '日常回访客服' 键\")\n",
" index[key] = json_item\n",
" except KeyError as e:\n",
" print(f\"警告:{e},跳过该条记录\")\n",
" continue\n",
" return index\n",
"\n",
"index = build_index(json_list)\n",
"print(f\"省市区索引构建完成: {len(index)} 条\")\n",
"\n",
"print(\"\\n========== 数据加载完成 ==========\")\n",
"print(f\"数据加载时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 步骤2: 数据处理逻辑(260-425行)\n",
"\n",
"这部分包含主要的数据处理逻辑,包括:\n",
"- 获取多公司过滤公司id\n",
"- 数据清洗和转换\n",
"- 优先级排序\n",
"- 数据过滤和合并\n",
"- 日期计算和扩展\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 获取多公司过滤公司id ==========\n",
"logger.info(\"获取多公司过滤公司id\")\n",
"all_filter_company_list = [] # 获取多公司过滤公司id\n",
"for company in get_filter_company_list:\n",
" company_list = company.get(\"_widget_1755052002491\")\n",
" if company_list:\n",
" for company_item in company_list:\n",
" if company_item.get(\"_widget_1755052002496\") == \"否\":\n",
" all_filter_company_list.append(company_item.get(\"_widget_1755052002495\"))\n",
"logger.info(f\"过滤公司条数:{len(all_filter_company_list)}\")\n",
"print(f\"过滤公司条数: {len(all_filter_company_list)}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:27:13.175060200Z",
"start_time": "2026-01-16T07:27:09.857076900Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"数据预处理完成,当前数据量: 45672 条\n",
"数据列数: 143\n"
]
}
],
"source": [
"# ========== 数据预处理:日期转换和数据清洗 ==========\n",
"# 将A列和B列的日期字符串转换为日期格式\n",
"data_NGV = data_NGV.copy()\n",
"data_NGV['A'] = pd.to_datetime(data_NGV['expiry_time'])\n",
"data_NGV['B'] = pd.to_datetime(data_NGV['renew_date'])\n",
"\n",
"def replace_values(series):\n",
" # 使用条件判断来进行替换\n",
" return series.apply(lambda x: '' if pd.isna(x) or x in ['NA', 'None', ''] else x)\n",
"\n",
"# 处理字符串数据并显式指定数据类型\n",
"data_NGV = data_NGV.apply(replace_values)\n",
"\n",
"print(f\"数据预处理完成,当前数据量: {len(data_NGV)} 条\")\n",
"print(f\"数据列数: {len(data_NGV.columns)}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:28:01.298494800Z",
"start_time": "2026-01-16T07:28:01.106113700Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"过滤多公司后数据量: 45653 条\n"
]
}
],
"source": [
"# ========== 过滤多公司 ==========\n",
"# 针对公司主店过期,取公司最高等级版本派发\n",
"# 过滤多公司\n",
"data_NGV = data_NGV[~data_NGV['id_own_group'].isin(all_filter_company_list)]\n",
"print(f\"过滤多公司后数据量: {len(data_NGV)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:28:04.235079300Z",
"start_time": "2026-01-16T07:28:04.185820400Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"优先级映射完成\n"
]
}
],
"source": [
"# ========== 定义优先级顺序和创建映射字典 ==========\n",
"# 定义优先级顺序\n",
"edition_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']\n",
"customer_type_order = [\"F\", \"E\", \"D\", \"C\", \"B\", \"A\"] # 索引越小优先级越高\n",
"group_grade_order = ['全国KAFMVP', '区域KAMVP', '重要客户(SVIP', '普通客户(VIP']\n",
"\n",
"# 创建映射字典,并为不在列表中的值设置默认值\n",
"edition_map = {edition: idx for idx, edition in enumerate(edition_order)}\n",
"customer_type_map = {ctype: idx for idx, ctype in enumerate(customer_type_order)}\n",
"group_grade_map = {grade: idx for idx, grade in enumerate(group_grade_order)}\n",
"\n",
"# 添加用于排序的新列,并处理不在映射字典中的值\n",
"data_NGV['edition_rank'] = data_NGV['saas_edition_fmt'].map(edition_map).fillna(0).astype(int) # 缺失值用最高优先级填充\n",
"data_NGV['customer_type_rank'] = data_NGV['saas_customer_type'].map(customer_type_map).fillna(0).astype(int)\n",
"data_NGV['group_grade_rank'] = data_NGV['group_grade'].map(group_grade_map).fillna(0).astype(int)\n",
"\n",
"print(\"优先级映射完成\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:28:08.790102700Z",
"start_time": "2026-01-16T07:28:08.399298Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"最佳值查找完成\n"
]
}
],
"source": [
"# ========== 找到每组中的最佳值 ==========\n",
"# 找到每组中 edition_rank 最小值对应的行\n",
"best_edition_idx = data_NGV.groupby('id_own_group')['edition_rank'].idxmin()\n",
"best_edition_rows = data_NGV.loc[best_edition_idx]\n",
"best_edition_rows['max_saas_edition'] = best_edition_rows['saas_edition_fmt']\n",
"\n",
"# 找到每组中 customer_type_rank 最小值对应的行\n",
"best_customer_type_idx = data_NGV.groupby('id_own_group')['customer_type_rank'].idxmin()\n",
"best_customer_type_rows = data_NGV.loc[best_customer_type_idx]\n",
"best_customer_type_rows['max_saas_customer_type'] = best_customer_type_rows['customer_type_rank'].apply(\n",
" lambda x: customer_type_order[x])\n",
"\n",
"# 找到每组中 group_grade_rank 最小值对应的行\n",
"best_group_grade_idx = data_NGV.groupby('id_own_group')['group_grade_rank'].idxmin()\n",
"best_group_grade_rows = data_NGV.loc[best_group_grade_idx]\n",
"best_group_grade_rows['max_group_grade'] = best_group_grade_rows['group_grade']\n",
"\n",
"print(\"最佳值查找完成\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:28:11.374632Z",
"start_time": "2026-01-16T07:28:11.141730700Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"最佳值合并完成,当前数据量: 45653 条\n"
]
}
],
"source": [
"# ========== 合并最佳值回到原数据集 ==========\n",
"# 合并最佳值回到原数据集\n",
"best_values = (\n",
" best_edition_rows[['id_own_group', 'max_saas_edition']]\n",
" .merge(best_customer_type_rows[['id_own_group', 'max_saas_customer_type']], on='id_own_group',\n",
" how='outer')\n",
" .merge(best_group_grade_rows[['id_own_group', 'max_group_grade']], on='id_own_group', how='outer')\n",
")\n",
"\n",
"# 将最佳值合并回原数据集\n",
"data_NGV = data_NGV.merge(best_values, on='id_own_group', how='left')\n",
"\n",
"print(f\"最佳值合并完成,当前数据量: {len(data_NGV)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:30:06.358604500Z",
"start_time": "2026-01-16T07:30:05.752861600Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"============================================================\n",
"调试信息:处理主店过期情况\n",
"============================================================\n",
"\n",
"当前 data_NGV 数据量: 45653 条\n",
"\n",
"【字段检查】\n",
"is_main_org 数据类型: object\n",
"is_main_org 唯一值: ['0', '1']\n",
"is_main_org 值分布:\n",
"is_main_org\n",
"1 37628\n",
"0 8025\n",
"Name: count, dtype: int64\n",
"\n",
"org_status 数据类型: object\n",
"org_status 唯一值: ['留存', '过期']\n",
"org_status 值分布:\n",
"org_status\n",
"留存 27985\n",
"过期 17668\n",
"Name: count, dtype: int64\n",
"\n",
"org_type 数据类型: object\n",
"org_type 唯一值: ['一般', '天猫']\n",
"org_type 值分布:\n",
"org_type\n",
"一般 42985\n",
"天猫 2668\n",
"Name: count, dtype: int64\n",
"\n",
"【步骤1: 筛选主店过期】\n",
"警告: is_main_org 是字符串类型,尝试转换为数值\n",
"条件筛选结果数量: 15065 条\n",
"主店过期数据量 (ngvv2): 15065 条\n",
"ngvv2 中的 id_own_group 数量: 15065 个\n",
"ngvv2 中的 id_own_group 示例: ['10545055917999655906', '10545055917999678943', '10545055917999702656', '10545055917999726421', '10545055917999791008', '10545055917999907421', '10545055917999958815', '10545055917999963314', '10545055917999973061', '10545055918000062921']\n",
"\n",
"【步骤2: 筛选分店留存】\n",
"data_NGV_V2 初始数据量: 45653 条\n",
"\n",
"area_manager 唯一值数量: 16\n",
"area_manager 值分布(前10:\n",
"area_manager\n",
"肖军 10824\n",
"景东强 8408\n",
"陈庆伟 8322\n",
"张凯 8269\n",
"关磊 7028\n",
"孙玉蕾 2006\n",
"殷昊 556\n",
"王涛 161\n",
"刘伟 52\n",
" 8\n",
"Name: count, dtype: int64\n",
"\n",
"各条件筛选结果:\n",
" org_type == '一般': 42985 条\n",
" org_status == '留存': 27985 条\n",
" area_manager != '殷昊': 45097 条\n",
" area_manager != '孙玉蕾': 43647 条\n",
" is_main_org != 1: 8025 条\n",
"\n",
"所有条件合并后数据量: 4882 条\n",
"data_NGV_V2_filtered 中的 id_own_group 数量: 1564 个\n",
"data_NGV_V2_filtered 中的 id_own_group 示例: ['10545055917999659357', '10545055917999659357', '10545055917999688607', '10545055917999688607', '10545055917999688607', '10545055917999719687', '10545055917999791008', '10545055917999791008', '10545055917999995278', '10545055918000106937']\n",
"\n",
"【步骤3: 检查 id_own_group 交集】\n",
"ngvv2 中的 id_own_group 数量: 15065\n",
"data_NGV_V2_filtered 中的 id_own_group 数量: 1564\n",
"交集数量: 316\n",
"交集中的 id_own_group 示例: ['11240984669917478021', '10546172455175803787', '10546172455166018835', '10546172455220322161', '10546443563657780587', '10546172455213602644', '10546443563816611858', '11240984669917430021', '11240984669917329620', '11240984669917352563']\n",
"\n",
"【步骤4: 最终过滤】\n",
"过滤后的数据量: 468 条\n",
"\n",
"============================================================\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"C:\\Users\\hp_z66\\AppData\\Local\\Temp\\ipykernel_14280\\4275068286.py:100: SettingWithCopyWarning: \n",
"A value is trying to be set on a copy of a slice from a DataFrame.\n",
"Try using .loc[row_indexer,col_indexer] = value instead\n",
"\n",
"See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
" data_NGV_V2_filtered['exists_in_ngvv2'] = data_NGV_V2_filtered['id_own_group'].isin(ngvv2['id_own_group'])\n"
]
}
],
"source": [
"# ========== 处理主店过期的情况 ==========\n",
"# 调试信息:检查数据状态\n",
"print(\"=\" * 60)\n",
"print(\"调试信息:处理主店过期情况\")\n",
"print(\"=\" * 60)\n",
"print(f\"\\n当前 data_NGV 数据量: {len(data_NGV)} 条\")\n",
"\n",
"# 检查关键字段的数据类型和唯一值\n",
"print(f\"\\n【字段检查】\")\n",
"print(f\"is_main_org 数据类型: {data_NGV['is_main_org'].dtype}\")\n",
"print(f\"is_main_org 唯一值: {sorted(data_NGV['is_main_org'].unique())}\")\n",
"print(f\"is_main_org 值分布:\\n{data_NGV['is_main_org'].value_counts()}\")\n",
"\n",
"print(f\"\\norg_status 数据类型: {data_NGV['org_status'].dtype}\")\n",
"print(f\"org_status 唯一值: {sorted(data_NGV['org_status'].unique())}\")\n",
"print(f\"org_status 值分布:\\n{data_NGV['org_status'].value_counts()}\")\n",
"\n",
"print(f\"\\norg_type 数据类型: {data_NGV['org_type'].dtype}\")\n",
"print(f\"org_type 唯一值: {sorted(data_NGV['org_type'].unique())}\")\n",
"print(f\"org_type 值分布:\\n{data_NGV['org_type'].value_counts()}\")\n",
"\n",
"# 步骤1: 筛选主店过期的情况\n",
"print(f\"\\n【步骤1: 筛选主店过期】\")\n",
"# 确保 is_main_org 是数值类型\n",
"if data_NGV['is_main_org'].dtype == 'object':\n",
" print(\"警告: is_main_org 是字符串类型,尝试转换为数值\")\n",
" data_NGV['is_main_org'] = pd.to_numeric(data_NGV['is_main_org'], errors='coerce')\n",
"\n",
"condition = (data_NGV['is_main_org'] == 1) & (data_NGV['org_status'] == '过期')\n",
"print(f\"条件筛选结果数量: {condition.sum()} 条\")\n",
"\n",
"ngvv2 = data_NGV[condition]\n",
"print(f\"主店过期数据量 (ngvv2): {len(ngvv2)} 条\")\n",
"\n",
"if len(ngvv2) > 0:\n",
" print(f\"ngvv2 中的 id_own_group 数量: {ngvv2['id_own_group'].nunique()} 个\")\n",
" print(f\"ngvv2 中的 id_own_group 示例: {ngvv2['id_own_group'].head(10).tolist()}\")\n",
"else:\n",
" print(\"⚠️ 警告: ngvv2 为空,没有主店过期的情况!\")\n",
"\n",
"# 步骤2: 检查分店留存的情况\n",
"print(f\"\\n【步骤2: 筛选分店留存】\")\n",
"# 在合并最佳值之前保存原始数据副本(重要!)\n",
"data_NGV_V2 = data_NGV.copy()\n",
"print(f\"data_NGV_V2 初始数据量: {len(data_NGV_V2)} 条\")\n",
"\n",
"# 检查 area_manager 字段\n",
"print(f\"\\narea_manager 唯一值数量: {data_NGV_V2['area_manager'].nunique()}\")\n",
"print(f\"area_manager 值分布(前10:\\n{data_NGV_V2['area_manager'].value_counts().head(10)}\")\n",
"\n",
"# 确保 is_main_org 是数值类型\n",
"if data_NGV_V2['is_main_org'].dtype == 'object':\n",
" data_NGV_V2['is_main_org'] = pd.to_numeric(data_NGV_V2['is_main_org'], errors='coerce')\n",
"\n",
"# 逐步检查每个条件\n",
"cond1 = (data_NGV_V2['org_type'] == \"一般\")\n",
"cond2 = (data_NGV_V2['org_status'] == '留存')\n",
"cond3 = (data_NGV_V2['area_manager'] != '殷昊')\n",
"cond4 = (data_NGV_V2['area_manager'] != '孙玉蕾')\n",
"cond5 = (data_NGV_V2['is_main_org'] != 1)\n",
"\n",
"print(f\"\\n各条件筛选结果:\")\n",
"print(f\" org_type == '一般': {cond1.sum()} 条\")\n",
"print(f\" org_status == '留存': {cond2.sum()} 条\")\n",
"print(f\" area_manager != '殷昊': {cond3.sum()} 条\")\n",
"print(f\" area_manager != '孙玉蕾': {cond4.sum()} 条\")\n",
"print(f\" is_main_org != 1: {cond5.sum()} 条\")\n",
"\n",
"data_NGV_V2['条件'] = cond1 & cond2 & cond3 & cond4 & cond5\n",
"data_NGV_V2_filtered = data_NGV_V2.loc[data_NGV_V2[\"条件\"]]\n",
"print(f\"\\n所有条件合并后数据量: {len(data_NGV_V2_filtered)} 条\")\n",
"\n",
"if len(data_NGV_V2_filtered) > 0:\n",
" print(f\"data_NGV_V2_filtered 中的 id_own_group 数量: {data_NGV_V2_filtered['id_own_group'].nunique()} 个\")\n",
" print(f\"data_NGV_V2_filtered 中的 id_own_group 示例: {data_NGV_V2_filtered['id_own_group'].head(10).tolist()}\")\n",
"\n",
"# 步骤3: 检查交集\n",
"print(f\"\\n【步骤3: 检查 id_own_group 交集】\")\n",
"if len(ngvv2) > 0 and len(data_NGV_V2_filtered) > 0:\n",
" ngvv2_groups = set(ngvv2['id_own_group'].unique())\n",
" v2_groups = set(data_NGV_V2_filtered['id_own_group'].unique())\n",
" intersection = ngvv2_groups & v2_groups\n",
" \n",
" print(f\"ngvv2 中的 id_own_group 数量: {len(ngvv2_groups)}\")\n",
" print(f\"data_NGV_V2_filtered 中的 id_own_group 数量: {len(v2_groups)}\")\n",
" print(f\"交集数量: {len(intersection)}\")\n",
" \n",
" if len(intersection) > 0:\n",
" print(f\"交集中的 id_own_group 示例: {list(intersection)[:10]}\")\n",
" else:\n",
" print(\"⚠️ 警告: 没有交集!这可能是问题所在。\")\n",
" print(f\"ngvv2 中的前10个 id_own_group: {list(ngvv2_groups)[:10]}\")\n",
" print(f\"data_NGV_V2_filtered 中的前10个 id_own_group: {list(v2_groups)[:10]}\")\n",
"else:\n",
" print(\"⚠️ 警告: ngvv2 或 data_NGV_V2_filtered 为空,无法检查交集\")\n",
"\n",
"# 步骤4: 过滤存在的记录\n",
"print(f\"\\n【步骤4: 最终过滤】\")\n",
"if len(ngvv2) > 0:\n",
" data_NGV_V2_filtered['exists_in_ngvv2'] = data_NGV_V2_filtered['id_own_group'].isin(ngvv2['id_own_group'])\n",
" filtered_data = data_NGV_V2_filtered[data_NGV_V2_filtered['exists_in_ngvv2']]\n",
" print(f\"过滤后的数据量: {len(filtered_data)} 条\")\n",
" \n",
" if len(filtered_data) == 0:\n",
" print(\"\\n❌ 问题诊断:\")\n",
" print(\" 过滤后数据为空,可能的原因:\")\n",
" print(\" 1. ngvv2 为空(没有主店过期的情况)\")\n",
" print(\" 2. data_NGV_V2_filtered 为空(没有满足条件的分店留存数据)\")\n",
" print(\" 3. 两者的 id_own_group 没有交集\")\n",
" print(\"\\n建议:\")\n",
" print(\" - 检查数据源是否正确\")\n",
" print(\" - 检查字段值是否匹配(注意数据类型和格式)\")\n",
" print(\" - 检查是否有主店过期但分店留存的情况\")\n",
"else:\n",
" print(\"⚠️ 警告: ngvv2 为空,无法进行过滤\")\n",
" filtered_data = pd.DataFrame() # 创建空DataFrame\n",
"\n",
"print(\"\\n\" + \"=\" * 60)\n"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:30:16.487181500Z",
"start_time": "2026-01-16T07:30:16.440099400Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"排序去重后数据量: 316 条\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"C:\\Users\\hp_z66\\AppData\\Local\\Temp\\ipykernel_14280\\2835892650.py:5: SettingWithCopyWarning: \n",
"A value is trying to be set on a copy of a slice from a DataFrame.\n",
"Try using .loc[row_indexer,col_indexer] = value instead\n",
"\n",
"See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
" filtered_data['sort_key'] = filtered_data['saas_edition_fmt'].map(fixed_order_map)\n"
]
}
],
"source": [
"# ========== 对过滤数据进行排序和去重 ==========\n",
"fixed_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']\n",
"\n",
"fixed_order_map = {edition: index for index, edition in enumerate(fixed_order)}\n",
"filtered_data['sort_key'] = filtered_data['saas_edition_fmt'].map(fixed_order_map)\n",
"filtered_data = filtered_data.sort_values(by='sort_key').drop('sort_key', axis=1)\n",
"\n",
"result = filtered_data.drop_duplicates(subset='id_own_group', keep='first')\n",
"\n",
"print(f\"排序去重后数据量: {len(result)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 合并主店留存数据和分店数据 ==========\n",
"data_NGV['条件'] = (data_NGV['org_type'] == \"一般\") & (data_NGV['org_status'] == '留存') & (\n",
" data_NGV['area_manager'] != '殷昊') & (\n",
" data_NGV['area_manager'] != '孙玉蕾') & (\n",
" data_NGV['is_main_org'] == 1)\n",
"data_NGV = data_NGV.loc[data_NGV[\"条件\"]]\n",
"\n",
"data_NGV = pd.concat([data_NGV, result], axis=0)\n",
"data_details = data_NGV.copy()\n",
"\n",
"# 重置索引\n",
"data_details = data_details.reset_index(drop=True)\n",
"\n",
"print(f\"合并后数据量: {len(data_details)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:30:21.600199200Z",
"start_time": "2026-01-16T07:30:20.828225500Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"日期计算后数据量: 9845 条\n",
"需要扩展的数据行数: 9845 条\n"
]
}
],
"source": [
"# ========== 判断日期差并计算年数 ==========\n",
"# 判断A列的日期是否大于B列的日期730天,如果是的话,将B列的值设置为天数差\n",
"data_details['条件'] = data_details.apply(\n",
" lambda row: (\n",
" (pd.to_datetime(row['A']) - pd.to_datetime(row['B'])).days\n",
" if pd.to_datetime(row['A']) - pd.to_datetime(row['B']) >= pd.Timedelta(days=730)\n",
" else 0\n",
" ),\n",
" axis=1\n",
")\n",
"data_details = data_details.loc[data_details[\"条件\"] > 0]\n",
"\n",
"# 定义一个函数,用于将数字除以365并取整数\n",
"def divide_by_365(x):\n",
" if isinstance(x, (int, float)):\n",
" return int(x / 365)\n",
" else:\n",
" return x\n",
"\n",
"# 使用apply函数将divide_by_365函数应用到DataFrame的列\n",
"data_details['年'] = data_details['条件'].apply(divide_by_365)\n",
"\n",
"# 重置索引\n",
"data_details = data_details.reset_index(drop=True)\n",
"\n",
"print(f\"日期计算后数据量: {len(data_details)} 条\")\n",
"print(f\"需要扩展的数据行数: {len(data_details[data_details['年'] > 1])} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 扩展数据:根据年数复制行并修改日期 ==========\n",
"# 创建一个新的空的DataFrame\n",
"new_df = pd.DataFrame()\n",
"\n",
"# 遍历原始DataFrame的每一行\n",
"for index, row in data_details.iterrows():\n",
" # 根据年数来决定复制的次数\n",
" if row[\"renew_date\"] != \"2024-02-29\":\n",
" for i_new in range(1, row['年']):\n",
" # 修改日期\n",
" row_new = row.copy()\n",
" c = row_new[\"renew_date\"]\n",
" date_obj = datetime.strptime(c, \"%Y-%m-%d\")\n",
" new_year = date_obj.year + i_new\n",
" new_date_obj = date_obj.replace(year=new_year)\n",
" new_c = new_date_obj.strftime(\"%Y-%m-%d\")\n",
" row_new[\"renew_date\"] = new_c\n",
" # 将当前行添加到新的DataFrame中\n",
" new_df = pd.concat([new_df, pd.DataFrame([row_new])], ignore_index=True)\n",
"\n",
"print(f\"扩展后的新数据量: {len(new_df)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:39:28.813848800Z",
"start_time": "2026-01-16T07:39:28.255902200Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"合并后总数据量: 39599 条\n"
]
}
],
"source": [
"# ========== 合并原始数据和扩展数据 ==========\n",
"# 合并两个DataFrame\n",
"merged_df = pd.concat([data_NGV, new_df], axis=0, ignore_index=True)\n",
"data_details = merged_df.copy() # 替换名称\n",
"\n",
"data_details_not_null = data_details[data_details['renew_date'].notnull()]\n",
"# 重置索引\n",
"data_details_not_null = data_details_not_null.reset_index(drop=True)\n",
"data_details = data_details_not_null.copy() # 替换名称 v2\n",
"\n",
"print(f\"合并后总数据量: {len(data_details)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 最终过滤:排除创建时间等于续约时间的记录 ==========\n",
"data_details['saas_create_time'] = data_details['saas_create_time'].str[:4] # 截取前4位(年份)\n",
"data_details['renew_date_new'] = data_details['renew_date'].str[:4] # 截取前4位(年份)\n",
"data_details = data_details[\n",
" data_details['saas_create_time'] != data_details['renew_date_new']] # 过滤掉等于renew_date的行\n",
"\n",
"data_details = data_details.reset_index(drop=True)\n",
"\n",
"logger.info(f\"过滤后的数据长度为: {len(data_details)}\")\n",
"print(f\"\\n========== 数据处理完成 ==========\")\n",
"print(f\"最终数据量: {len(data_details)} 条\")\n",
"print(f\"处理完成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 数据验证和检查\n",
"\n",
"可以在这里添加数据验证代码,检查处理结果的正确性\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"start_time": "2026-01-16T09:00:49.656903800Z"
}
},
"outputs": [],
"source": [
"# ========== 数据验证 ==========\n",
"# 查看数据基本信息\n",
"print(\"数据基本信息:\")\n",
"print(f\"数据形状: {data_details.shape}\")\n",
"print(f\"\\n数据列名:\")\n",
"print(data_details.columns.tolist())\n",
"\n",
"# 查看前几行数据\n",
"print(\"\\n前5行数据:\")\n",
"print(data_details.head())\n",
"\n",
"# 检查关键字段的数据分布\n",
"if 'saas_edition_fmt' in data_details.columns:\n",
" print(\"\\n版本分布:\")\n",
" print(data_details['saas_edition_fmt'].value_counts())\n",
"\n",
"if 'org_status' in data_details.columns:\n",
" print(\"\\n组织状态分布:\")\n",
" print(data_details['org_status'].value_counts())\n",
"\n",
"# 可以保存到CSV文件进行进一步检查\n",
"# data_details.to_csv(\"处理后的数据.csv\", index=False, encoding='utf-8-sig')\n",
"# print(\"\\n数据已保存到: 处理后的数据.csv\")\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+53
View File
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
payload ={
"app_id": "675b900991ad2491c69389ca",
"data": {
"_widget_1767851302591": 1772508166000,
"_widget_1767863644355": "何文娟",
"_widget_1767851302590": "1772507997862",
"_widget_1767851302593": [
"064863221620345741"
],
"_widget_1767851302592": "15651815921",
"_widget_1767851302595": [
"122744083333090577"
],
"_widget_1767851302594": "064863221620345741",
"_widget_1767851302597": "废弃测试三月分店",
"_widget_1767851302596": "CHS202603030049192",
"_widget_1767952153422": [
"洗车",
"美容"
],
"_widget_1772264444051": "19999.0",
"_widget_1767851302599": "普通客户(VIP",
"_widget_1767851302598": "zyc测试公司废弃",
"_widget_1768290159749": [
"122744083333090577"
],
"_widget_1768290159748": "华南区域",
"_widget_1768290159747": "15870306745529522117",
"_widget_1767863644358": "15651815921",
"_widget_1768290159746": "16338827489080131642",
"_widget_1767863644357": "何文娟",
"_widget_1767863644356": "15651815921",
"_widget_1767863644582": [
"064863221620345741"
],
"_widget_1767851302585": "XQFWD20260303001",
"_widget_1772264443873": "台湾省",
"_widget_1772703526239": "1",
"_widget_1772264443872": "其它",
"_widget_1767851302600": 1772508361000,
"_widget_1768290159737": 1773112966000,
"_widget_1767851302602": "皇冠版",
"_widget_1772264443811": "C",
"_widget_1772264443812": "5",
"_widget_1767851302604": "30"
}
,
"entry_id": "695f439e3e910f09190d8e99",
"is_start_workflow": true
}
+208
View File
@@ -0,0 +1,208 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "新签节点调用格式",
"id": "671b10d308af2bdc"
},
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2026-03-12T09:25:38.245095300Z",
"start_time": "2026-03-12T09:25:36.441415900Z"
}
},
"source": [
"import requests\n",
"url = \"https://manage-pre.f6yc.com/hive-admin/yida/updateNode\"\n",
"\n",
"\n",
"payload = {\n",
" \"nodeCode\": \"ORG_RESEARCH\",\n",
" \"needTraining\": \"是\",\n",
" \"impPrincipal\":\"['171408516124043808']\",\n",
" \"instanceId\": \"69b269d6c193f83cc27f65e7\"\n",
"}\n",
"\n",
"# [{\"_id\":\"69a285f3fa0d9fb1984da9bf\",\"name\":\"张乐乐\",\"username\":\"171408516124043808\",\"status\":1,\"type\":0}]\n",
"\n",
"response = requests.post(url, data=payload)\n",
"response.json()"
],
"outputs": [
{
"data": {
"text/plain": [
"{'code': 200, 'data': None, 'message': 'SUCCESS'}"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 14
},
{
"metadata": {},
"cell_type": "markdown",
"source": "# 续约联调",
"id": "6ce335625a1c1fbb"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-26T07:12:43.283568100Z",
"start_time": "2026-03-26T07:12:42.653902400Z"
}
},
"cell_type": "code",
"source": [
"# 可以引用一些第三方库.\n",
"import json\n",
"import requests\n",
"import time\n",
"import random\n",
"import time\n",
"import binascii # 【修正1】添加缺失的 binascii 导入\n",
"from pyDes import des, CBC, PAD_PKCS5\n",
"\n",
"data_id = \"69c4dc44986b303c2ef69d46\" # 数据id\n",
"orgid = \"16058986391127773239\" # 门店id\n",
"order_id = \"XYFWD20260326002\" # 服务单号\n",
"operation_consultant_str = '[{\"_id\":\"69a285f3fa0d9fb1984da9bf\",\"name\":\"张乐乐\",\"username\":\"171408516124043808\",\"status\":1,\"type\":0}]'# 专属人员顾问\n",
"\n",
"url = \"https://manage-pre.f6yc.com/hive-admin/py/yida/renewal/insertRenewalFormsData\"\n",
"\n",
"def des_encrypt(s):\n",
" \"\"\"\n",
" DES 加密\n",
" :param s: 原始字符串\n",
" :return: 加密后字符串,16进制\n",
" \"\"\"\n",
" secret_key = 'HwdMBW8o'\n",
" iv = secret_key\n",
" k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n",
" en = k.encrypt(s, padmode=PAD_PKCS5)\n",
" return binascii.b2a_base64(en, newline=False)\n",
"\n",
"\n",
"def des_descrypt(s):\n",
" \"\"\"\n",
" DES 解密\n",
" :param s: 加密后的字符串,16进制\n",
" :return: 解密后的字符串\n",
" \"\"\"\n",
" secret_key = 'HwdMBW8o'\n",
" iv = secret_key\n",
" k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n",
" de = k.decrypt(binascii.a2b_base64(s), padmode=PAD_PKCS5)\n",
" return de\n",
"\n",
"\n",
"impPrincipal_list = []\n",
"\n",
"if operation_consultant_str:\n",
" try:\n",
" # 将字符串 '[{...}]' 转换为 Python 列表 [{...}]\n",
" operation_list = json.loads(operation_consultant_str)\n",
"\n",
" # 确保解析出来的是列表\n",
" if isinstance(operation_list, list):\n",
" for operate in operation_list:\n",
" # 确保每一项是字典且包含 username\n",
" if isinstance(operate, dict) and \"username\" in operate:\n",
" impPrincipal_list.append(operate[\"username\"])\n",
" else:\n",
" # 如果解析出来不是列表(比如是个单对象),做兼容处理\n",
" if isinstance(operation_list, dict) and \"username\" in operation_list:\n",
" impPrincipal_list.append(operation_list[\"username\"])\n",
"\n",
" except json.JSONDecodeError:\n",
" # 如果解析失败,记录错误或保持列表为空\n",
" print(f\"JSON 解析失败: {operation_consultant_str}\")\n",
" impPrincipal_list = []\n",
"else:\n",
" operation_list = []\n",
"\n",
"impPrincipal_value=impPrincipal_list[0]\n",
"\n",
"t = time.time()\n",
"ts = int(round(t * 1000))\n",
"randint = random.randint(100000000, 999999999)\n",
"req = data_id + \"|\" + orgid + \"|\" +order_id+ \"|\" + impPrincipal_value + \"_\" + str(ts) + \"_\" + str(randint)\n",
"# 实例ID|门ID|服务单号|专属运营顾问\n",
"str_en = des_encrypt(req)\n",
"print(str_en.decode('utf-8'))\n",
"req_new = str_en.decode('utf-8')\n",
"payload = {\n",
" 'req':req_new,\n",
" 't':ts,\n",
" 'r':randint\n",
"}\n",
"\n",
"res = requests.post(url,data=payload)\n"
],
"id": "fc98d87aa8b19ce2",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Lmj1kXumBcGLR5IKAtgFDP2meGj6FgLzq7gaQ/6wDuZ3d43P0mtQCyUdQXkeC4kYxOT8w7n1xNxYhIy9PVq/xUR9emnP5CQweBFe8P3S9tRZdNFVOdipnD+xiof/q9b//C87kgUEIH7JT00nlBBkaQ==\n"
]
}
],
"execution_count": 2
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-13T08:12:33.285136400Z",
"start_time": "2026-03-13T08:12:33.246808900Z"
}
},
"cell_type": "code",
"source": "impPrincipal_value",
"id": "8edd45ef4657d8ed",
"outputs": [
{
"data": {
"text/plain": [
"'171408516124043808'"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 26
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+10
View File
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
import pandas as pd
import requests
url = "https://manage-pre.f6yc.com/hive-admin/yida/renewal/updateNode"
payload = {"instanceId":"15348c27-57b7-4285-b93b-87b3d41f5a28","nodeCode":"SIXTY","impPrincipal":"[\"053052302136860181\"]"}
result = requests.post(url, data=payload)
print(result.text)
+200
View File
@@ -0,0 +1,200 @@
import pandas as pd
import datetime
from config import Config
from api import API
import pymysql # 使用 pymysql 替代 mysql.connector
from back_ground_module import CommonModule
import os
import mysql.connector
import pandas as pd
import json
import numpy as np
import mysql.connector
from mysql.connector import Error
from log_config import configure_task_logger, configure_error_task_logger
import math
logger = configure_task_logger()
error_task_logger = configure_error_task_logger()
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
common_module = CommonModule()
api_instance = API()
class ProvinceCityPersonRelationToBI:
def __init__(self):
self.pvc_data = None
self.field_mapping = {
"": "_widget_1734677164861",
"": "_widget_1734677164862",
"运营顾问": "_widget_1734677164864",
"区域经理": "_widget_1734677164865",
"运营专家": "_widget_1734677164866",
"战区": "_widget_1734677164867",
"新签回访客服": "_widget_1734677164868",
"续约回访客服": "_widget_1734677164869",
"异常待办客服": "_widget_1734677164870",
"日常回访客服": "_widget_1734677164871",
}
def load_all_data(self):
payload = {"api_key": "675b900991ad2491c69389ca",
"entry_id": "676512ac3e54dc3159460c0a",
}
pvc_data = api_instance.entry_data_list(payload)
self.pvc_data = pvc_data.get("data") # api请求格式,将数据封装在data字典里
def data_process(self):
df = pd.DataFrame(self.pvc_data)
# 反转映射字典
reverse_mapping = {v: k for k, v in self.field_mapping.items()}
# 1.列明替换
df.columns = [reverse_mapping.get(col, col) for col in df.columns]
# 2.成员字段取值
user_columns = ["运营顾问", "区域经理", "运营专家", "新签回访客服", "续约回访客服",
"异常待办客服", "日常回访客服"]
for col in user_columns:
df[col] = df[col].map(lambda x: x.get("name", "") if isinstance(x, dict) else "")
# 3.根据省市去重
df = df.drop_duplicates(subset=['', ''])
return df
def clear_table_data(self):
"""
清空指定 MySQL 表的数据
参数已写死在函数内部直接调用即可
"""
# 数据库连接信息
HS_DB_Config = {
'host': "f6-public.rwlb.rds.aliyuncs.com",
'user': "rw_operation_data_relay",
'password': "m+q5Z4%IVuF9bf",
'database': "f6operation_data_relay"
}
table_name = "province_city_person_relation_to_bi" # 要清空的表名
connection = None
try:
# 建立数据库连接
connection = mysql.connector.connect(
host=HS_DB_Config["host"],
user=HS_DB_Config["user"],
password=HS_DB_Config["password"],
database=HS_DB_Config["database"]
)
if connection.is_connected():
cursor = connection.cursor()
# 使用TRUNCATE清空表数据
cursor.execute(f"TRUNCATE TABLE {table_name}")
connection.commit()
logger.info(f"成功清空表 {table_name} 中的所有数据")
except Error as e:
error_task_logger.error(f"清空表时发生错误: {e}")
if connection and connection.is_connected():
connection.rollback()
finally:
if connection and connection.is_connected():
cursor.close()
connection.close()
logger.info("数据库连接已关闭")
def write_to_bi(self, df):
HS_DB_Config = Config.HS_DB_Config
table_name = "province_city_person_relation_to_bi"
chunk_size = 1000 # 每批插入 1000 行
# 清理 DataFrame 中的 NaN/None 等值
df = df.replace([None, np.nan, pd.NA, 'nan', 'NaN', 'NAN', ''], None)
connection = mysql.connector.connect(
host=HS_DB_Config["host"],
user=HS_DB_Config["user"],
password=HS_DB_Config["password"],
database=HS_DB_Config["database"]
)
cursor = connection.cursor()
try:
# 获取数据库表的列名
cursor.execute(f"SHOW COLUMNS FROM `{table_name}`")
db_columns = [col[0] for col in cursor.fetchall()]
# 保留与数据库匹配的列
filtered_df = df[df.columns.intersection(db_columns)]
if filtered_df.empty:
print("DataFrame 中没有与数据库表结构匹配的列。")
return
# 处理 dict/list 类型字段:转为 JSON 字符串
filtered_df = filtered_df.copy()
for col in filtered_df.columns:
if filtered_df[col].apply(lambda x: isinstance(x, (dict, list)) if x is not None else False).any():
filtered_df[col] = filtered_df[col].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else x
)
# 构建 INSERT 语句(只构建一次)
columns = [f"`{col}`" for col in filtered_df.columns]
placeholders = ', '.join(['%s'] * len(columns))
insert_sql = f"INSERT INTO `{table_name}` ({', '.join(columns)}) VALUES ({placeholders})"
total_rows = len(filtered_df)
num_chunks = math.ceil(total_rows / chunk_size)
for i in range(num_chunks):
start_idx = i * chunk_size
end_idx = min(start_idx + chunk_size, total_rows)
chunk_df = filtered_df.iloc[start_idx:end_idx]
# 转为元组列表
data_to_insert = [
tuple(row) for row in chunk_df.values
]
# 批量执行(executemany 更高效)
cursor.executemany(insert_sql, data_to_insert)
connection.commit()
logger.info(f"成功写入 {total_rows} 条记录到 {table_name} 表中(分 {num_chunks} 批)。")
except Exception as e:
error_task_logger.error(f"写入数据库时发生错误: {e}", exc_info=True)
connection.rollback()
finally:
cursor.close()
connection.close()
def main(self):
task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
logger.info("任务开始")
# step1: 获取数据
self.load_all_data()
logger.info("加载数据完成")
# step2:数据处理
df = self.data_process()
# df.to_csv(os.path.join(output_dir, "new_dealer_service_order_to_bi.csv"))
logger.info("数据处理完成")
# step3:数据库删除
self.clear_table_data()
logger.info("目标数据库已清空")
# step4:数据写入BI
self.write_to_bi(df)
logger.info("数据已写入数据库中")
common_module.send_task_status(task_start_time, "省市区人员关系表转BI")
except Exception as e:
error_task_logger.error(f"省市区人员关系表转BI发生错误{e}")
common_module.send_task_error(task_start_time, "省市区人员关系表转BI", str(e))
if __name__ == '__main__':
province_city_person_relation_to_bi = ProvinceCityPersonRelationToBI()
province_city_person_relation_to_bi.main()
File diff suppressed because it is too large Load Diff
+249
View File
@@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
import os
import re
import sys
import pandas as pd
from tqdm import tqdm
# 让 test 脚本可以 import 到项目根目录的模块(api.py / yd_api.py / log_config.py
# 将项目根目录加入模块搜索路径,确保可以 import 根目录下的 api.py/yd_api.py
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
from api import API
from log_config import configure_error_task_logger, configure_task_logger
from yd_api import YDAPI
logger = configure_task_logger()
error_task_logger = configure_error_task_logger()
api_instance = API()
yd_api_instance = YDAPI()
# =========================
# 需要你关注/可能要改的配置
# =========================
# 简道云:续约待办表单(目标表单)
APP_ID = "675b900991ad2491c69389ca"
ENTRY_ID = "6931063d64187eaf6b927557"
# 宜搭:续约服务流程(来源流程)
APP_TYPE = "APP_UYZ0KG6L0CCNV80GZ66O"
SYSTEM_TOKEN = "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2"
# 简道云:员工表(用于把“区域经理姓名”转成“员工ID”,如果你简道云字段是人员控件,必须传ID)
STAFF_APP_ID = "6694d3c4fcb69ca9a111a6c4"
STAFF_ENTRY_ID = "6769204a1902c9341340a1bc"
STAFF_NAME_WIDGET = "_widget_1734942794144"
STAFF_ID_WIDGET = "_widget_1734942794145"
# 输入数据源必须包含这三列:
# - data_id:简道云待办数据的 data_id
# - 门店编码:用于简单校验(对不上会提示)
# - 宜搭实例ID:用于拉取宜搭实例详情
REQUIRED_INPUT_COLUMNS = ("data_id", "门店编码", "宜搭实例ID")
if __name__ == "__main__":
# 第 1 个命令行参数:你的数据源文件路径(csv/xlsx/xls
df = pd.read_excel(fr"C:\Users\hp_z66\Downloads\续约服务流程_20260326133517.xlsx",sheet_name="简道云-宜搭实例id", dtype=str).fillna("")
# 兼容列名:有的表叫“数据ID/实例ID”,统一成脚本内部使用的列名
df = df.rename(columns={"数据ID": "data_id", "dataId": "data_id", "DataID": "data_id", "实例ID": "宜搭实例ID"})
for c in REQUIRED_INPUT_COLUMNS:
if c not in df.columns:
raise SystemExit(f"缺少必需列: {c}")
for c in REQUIRED_INPUT_COLUMNS:
df[c] = df[c].astype(str).str.strip()
df = df[(df["data_id"] != "") & (df["宜搭实例ID"] != "")]
df = df.drop_duplicates(subset=["data_id"]).reset_index(drop=True)
# 读取员工表:构建 “姓名 -> 员工ID” 映射
staff_resp = api_instance.entry_data_list({"api_key": STAFF_APP_ID, "entry_id": STAFF_ENTRY_ID}) or {}
staff_list = staff_resp.get("data", []) or []
name_to_staff_id = {}
for item in staff_list:
n = str(item.get("_widget_1734942794144", "")).strip()
i = str(item.get("_widget_1734942794145", "")).strip()
if n and i:
name_to_staff_id[n] = i
logger.info(f"员工数: {len(name_to_staff_id)}")
# 读取简道云表单字段:label(中文名) -> name(_widget_xxx)
# 这样你只要改 labels 里的中文字段名,脚本会自动找到对应 widget_id
widget_list = api_instance.entry_widget_list({"api_key": APP_ID, "entry_id": ENTRY_ID}) or {}
widgets = widget_list.get("widgets", []) or []
label_to_name = {}
for w in widgets:
l = str(w.get("label", "")).strip()
n = str(w.get("name", "")).strip()
if l and n:
label_to_name[l] = n
# 需要同步到简道云的字段(中文 label)
# 如果你简道云字段名不是这些(比如“省份/城市/区县”),就在这里改成你表单里实际的 label
labels = ["120天是否联系上客户", "60天是否联系上客户", "30天是否联系上客户", "区域经理", "服务单号", "门店ID", "公司id", "", "",]
jdy_map = {l: label_to_name.get(l) for l in labels if label_to_name.get(l)}
miss = [l for l in labels if l not in jdy_map]
if miss:
logger.warning(f"简道云缺少字段: {miss}")
logger.warning(f"已匹配字段: {list(jdy_map.keys())} | 门店ID_widget={jdy_map.get('门店ID')} 公司id_widget={jdy_map.get('公司id')}")
# 获取宜搭 token(后续拉取实例详情使用)
token = yd_api_instance.generateToken()
ok = 0
fail = 0
skip = 0
for _, row in tqdm(df.iterrows(), total=len(df)):
data_id = str(row.get("data_id", "")).strip()
instance_id = str(row.get("宜搭实例ID", "")).strip()
store_code = str(row.get("门店编码", "")).strip()
if not data_id or not instance_id:
logger.warning(f"跳过:data_id/实例ID 为空 data_id={data_id} instance_id={instance_id} 行数据={row.to_dict()}")
skip += 1
continue
try:
# 拉取宜搭实例详情(里面的 formData 才是字段数据)
info = yd_api_instance.processes_instancesInfos(token, instance_id, APP_TYPE, SYSTEM_TOKEN) or {}
container = info.get("data") if isinstance(info, dict) else None
form = {}
if isinstance(container, dict):
form = container.get("formData") or container
if not isinstance(form, dict) or not form:
logger.warning(f"跳过:宜搭实例无表单数据 instance_id={instance_id} data_id={data_id}")
skip += 1
# 简单校验:输入的门店编码 vs 宜搭表单里的门店编码,不一致就打日志提醒
# 宜搭门店编码字段IDtextField_ksydghqw(来自你旧脚本/导出的 yd_process_details.csv
yd_store = str(form.get("textField_ksydghqw", "")).strip()
if yd_store and store_code and yd_store != store_code:
logger.warning(f"门店编码不一致: {store_code} vs {yd_store} 实例:{instance_id} data_id:{data_id}")
# 从 formData 中按字段ID取值:取到第一个非空值就返回
def pick(keys):
for k in keys:
v = form.get(k)
if v is None:
continue
s = str(v).strip()
if s and s.lower() not in {"nan", "null", "none"}:
return s
return ""
# 宜搭人员字段常见是:["张三(123)"] 这种结构,这里做一个“只拿姓名”的处理
def first_name(v):
s = str(v).strip()
if s.startswith("[") and s.endswith("]"):
inner = s[1:-1].split(",", 1)[0].strip().strip("'").strip('"')
s = inner
m = re.split(r"\(", s, maxsplit=1)
return m[0].strip() if m else s
# =========================
# 下面是“宜搭字段ID -> 业务字段”的取值逻辑(你最常修改的区域)
# =========================
# 说明:
# - 120/60/30 是否联系上客户:优先各节点字段,取不到就回退用统一字段 radioField_l85ppdia
# - 区域经理:employeeField_ksydghre
# - 服务单号:textField_kuntp6fl(你历史数据是 XYFWDxxxx
# - 省/市:textField_kuj8nx00 / textField_kuj8nx01
# - 区:优先 textField_kuhnydmk;取不到就从地址 textField_ksydghrm 里截取
v120 = pick(["radioField_ksydghrf",])
v60 = pick(["radioField_kuhnydmd", ])
v30 = pick(["radioField_kuhnydn0", ])
region_name = first_name(form.get("employeeField_ksydghre", ""))
# 如果简道云“区域经理”字段是人员控件:应传员工ID;否则传姓名也能写入(取决于你表单控件类型)
region_value = name_to_staff_id.get(region_name, region_name) if region_name else ""
service_no = pick(["textField_kuntp6fl"])
# 门店ID(强烈建议你把下面 keys 改成你宜搭里“门店ID/公司id”的真实字段ID)
# 示例:store_id = pick(["textField_orgid", "textField_id_own_org"])
store_id = pick(["textField_kuntp6fk"])
prov = pick(["textField_kuj8nx00"])
city = pick(["textField_kuj8nx01"])
# 拼装简道云更新 payload:每个字段必须是 {"value": 值}
data_dict = {}
if v120 and jdy_map.get("120天是否联系上客户"):
data_dict[jdy_map["120天是否联系上客户"]] = {"value": v120}
if v60 and jdy_map.get("60天是否联系上客户"):
data_dict[jdy_map["60天是否联系上客户"]] = {"value": v60}
if v30 and jdy_map.get("30天是否联系上客户"):
data_dict[jdy_map["30天是否联系上客户"]] = {"value": v30}
if region_value and jdy_map.get("区域经理"):
data_dict[jdy_map["区域经理"]] = {"value": region_value}
if service_no and jdy_map.get("服务单号"):
data_dict[jdy_map["服务单号"]] = {"value": service_no}
# 门店ID优先写入“门店ID”,如果表单没有此字段则回退写入“公司id”
if store_id:
if jdy_map.get("门店ID"):
data_dict[jdy_map["门店ID"]] = {"value": store_id}
elif jdy_map.get("公司id"):
data_dict[jdy_map["公司id"]] = {"value": store_id}
if prov and jdy_map.get(""):
data_dict[jdy_map[""]] = {"value": prov}
if city and jdy_map.get(""):
data_dict[jdy_map[""]] = {"value": city}
if not data_dict:
reasons = []
if not v120:
reasons.append("120天:值空")
elif not jdy_map.get("120天是否联系上客户"):
reasons.append("120天:未映射")
if not v60:
reasons.append("60天:值空")
elif not jdy_map.get("60天是否联系上客户"):
reasons.append("60天:未映射")
if not v30:
reasons.append("30天:值空")
elif not jdy_map.get("30天是否联系上客户"):
reasons.append("30天:未映射")
if not region_value:
reasons.append("区域经理:值空")
elif not jdy_map.get("区域经理"):
reasons.append("区域经理:未映射")
if not service_no:
reasons.append("服务单号:值空")
elif not jdy_map.get("服务单号"):
reasons.append("服务单号:未映射")
if not store_id:
reasons.append("门店ID:值空")
elif not (jdy_map.get("门店ID") or jdy_map.get("公司id")):
reasons.append("门店ID:表单无对应字段(门店ID/公司id)")
if not prov:
reasons.append("省:值空")
elif not jdy_map.get(""):
reasons.append("省:未映射")
if not city:
reasons.append("市:值空")
elif not jdy_map.get(""):
reasons.append("市:未映射")
logger.warning(
f"跳过 data_id:{data_id} 实例:{instance_id} | 原因: {', '.join(reasons)} | "
f"取值: v120={v120} v60={v60} v30={v30} region_name={region_name} region_value={region_value} "
f"service_no={service_no} store_id={store_id} prov={prov} city={city}"
)
skip += 1
continue
# 更新简道云数据(同你旧脚本)
payload = {
"api_key": APP_ID,
"entry_id": ENTRY_ID,
"data_id": data_id,
"data": data_dict,
"is_start_trigger": False,## 目前宜搭有通知
}
res = api_instance.entry_data_update(payload)
# 兼容两种返回格式:有的返回 {'status':'success',...},有的直接返回 {'data':{...}}
if isinstance(res, dict) and (res.get("status") == "success" or isinstance(res.get("data"), dict)):
ok += 1
else:
fail += 1
logger.warning(f"更新失败 data_id:{data_id} 实例:{instance_id} 返回:{res}")
except Exception as e:
fail += 1
error_task_logger.error(f"同步异常 data_id:{data_id} 实例:{instance_id} 错误:{e}", exc_info=True)
logger.info(f"完成 同步成功:{ok} 失败:{fail} 跳过:{skip}")
@@ -0,0 +1,58 @@
import requests
import pandas as pd
cookies = {
'auth_token': 's%3A.9uztgExtmqUJXHCi00hv9SGq6eVYSvH%2BxQSwrox1Yls',
'fx-lang': 'zh_cn',
'tenantId': 'agndqbuttb7ipfciraxcokgqyu',
'AGL_USER_ID': 'a50da526-dd43-4a78-ace1-ba810a6f2168',
'_ga': 'GA1.1.626243428.1772260541',
'_clck': 'y7ldwu%5E2%5Eg3y%5E0%5E2250',
'_ga_JTDW9M3LHZ': 'GS2.1.s1772266909$o3$g0$t1772266909$j60$l0$h0',
'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D',
'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D',
'_csrf': 's%3AE3ildFF7aOT874ca1lQaPpJh.M4vLvJ9TdzyukOLVlNNYO9ysSRLlssqxQh1%2FwMawRCY',
'Hm_lvt_de47dd1629940fe88b02865de93dd9fe': '1774410585,1774417748,1774487358,1774503122',
'HMACCOUNT': 'A6A0585E8C70051D',
'Hm_lpvt_de47dd1629940fe88b02865de93dd9fe': '1774503273',
'GSuvNKHqfvX2r6v7P8HkZv2bow': 's%3ALj5VHNEzUxprmpIJHWpNg7SzUp7GR3H1.yp8HeYMA0ARAeFC0CqxucSfBxvudd4xO76ZwAACWvTc',
'JDY_SID': 's%3AZrD40YQAsP9Ui53M1j0lud3AvwbdgzEl.ljjCkitsu2tDtbx0kJTROsKeNh8QfX2mz6SehH%2B%2FD5E',
}
headers = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'content-type': 'application/json',
'origin': 'https://www.jiandaoyun.com',
'priority': 'u=1, i',
'referer': 'https://www.jiandaoyun.com/dashboard/app/675b900991ad2491c69389ca/form/6931063d64187eaf6b927557/edit',
'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0',
'x-csrf-token': 'Mape7z8g-8iFP_vxFm8qSzVprTb4XsGfvs1o',
'x-jdy-ver': '10.19.6',
'x-request-id': '7bb1d48a-0fdb-46b7-bf67-ebd52c1a1f9a',
# 'cookie': 'auth_token=s%3A.9uztgExtmqUJXHCi00hv9SGq6eVYSvH%2BxQSwrox1Yls; fx-lang=zh_cn; tenantId=agndqbuttb7ipfciraxcokgqyu; AGL_USER_ID=a50da526-dd43-4a78-ace1-ba810a6f2168; _ga=GA1.1.626243428.1772260541; _clck=y7ldwu%5E2%5Eg3y%5E0%5E2250; _ga_JTDW9M3LHZ=GS2.1.s1772266909$o3$g0$t1772266909$j60$l0$h0; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D; _csrf=s%3AE3ildFF7aOT874ca1lQaPpJh.M4vLvJ9TdzyukOLVlNNYO9ysSRLlssqxQh1%2FwMawRCY; Hm_lvt_de47dd1629940fe88b02865de93dd9fe=1774410585,1774417748,1774487358,1774503122; HMACCOUNT=A6A0585E8C70051D; Hm_lpvt_de47dd1629940fe88b02865de93dd9fe=1774503273; GSuvNKHqfvX2r6v7P8HkZv2bow=s%3ALj5VHNEzUxprmpIJHWpNg7SzUp7GR3H1.yp8HeYMA0ARAeFC0CqxucSfBxvudd4xO76ZwAACWvTc; JDY_SID=s%3AZrD40YQAsP9Ui53M1j0lud3AvwbdgzEl.ljjCkitsu2tDtbx0kJTROsKeNh8QfX2mz6SehH%2B%2FD5E',
}
df = pd.read_excel(fr"C:\Users\hp_z66\Downloads\续约服务流程_20260327115935.xlsx",sheet_name="需要查询补充120天节点数据", dtype=str).fillna("")
json_data = {
'skip': 0,
'limit': 20,
'appId': '675b900991ad2491c69389ca',
'entryId': '6931063d64187eaf6b927557',
'formId': '6931063d64187eaf6b927557',
'dataId': '69ab8f89837038e4e81bff21',
}
response = requests.post(
'https://www.jiandaoyun.com/_/admin/data/log/list_changes',
cookies=cookies,
headers=headers,
json=json_data,
)
@@ -0,0 +1,791 @@
import os
from datetime import datetime, timedelta, timezone
import pandas as pd
from tqdm import tqdm
from datetime import datetime, timezone
import pandas as pd
import os
from typing import Dict
import requests
import json
import time
import numpy as np # 导入numpy库用于处理numpy数组
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
class Config:
JIANDAOYUN_API_TOKEN = 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN' # token
class API:
def entry_data_list(self, data: dict, replace: bool = False, max_retries: int = 20) -> Dict: # 获取多条表单数据
"""
获取多条表单数据
:param max_retries: 最大重试次数
:param replace: 是否替换字段
:param data:
api_key: 应用id
entry_id: 表单id
:return:
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 app_key
'Content-Type': 'application/json'
}
all_data_batches = [] # 用于存储每次请求返回的数据批次
last_data_id = None
exit_flag = False
while True:
payload = json.dumps({
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"limit": 90,
"data_id": last_data_id,
"filter": data.get('filter', None)
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
if data_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_task_logger.error(f"任务 {last_data_id}组 连续{max_retries}次请求失败,放弃此次请求。")
all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理
if exit_flag:
break
# 构建最终返回的字典
final_data = {
'data': all_data_batches # 'data' 键对应的值是列表的列表
}
# logger.info(f"获取了{len(all_data_batches)}条数据")
if replace:
print("进行了替换")
return_data = self.field_replacement(data, final_data) # 字段替换,由id替换为标签名
return return_data
else:
return final_data
def field_replacement(self, data: dict, data_get: dict) -> dict:
"""
字段替换将id替换为标签名即唯一值替换为表单中显示字段的名字
:param data: 简道云插件发送过来的data包含表单id数据id应用id
:param data_get: 简道云请求的数据一般是根据数据id获取到表单的数据
:return: 将根据数据id获取到的表单数据进行替换返回替换后的数据
"""
# 获取表单对应字段标签名称
widget_list = self.entry_widget_list(data)
# 检查widget_list是否有效
if not widget_list or 'widgets' not in widget_list or not isinstance(widget_list['widgets'], list):
raise ValueError("映射表没有接受到数据")
# 创建一个映射表,将_widget_名称映射到label
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,避免修改原始数据
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 workflow_instance_get(data: dict, max_retries: int = 20) -> dict:
"""
查询实例流程信息
:param max_retries:
:param data: 简道云插件发送过来的data包含应用id
:return: 查询简道云流程实例信息返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v6/workflow/instance/get'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey
'Content-Type': 'application/json'
}
payload = json.dumps({
"instance_id": data['data_id'],
"tasks_type": 1
}
)
print("payload:", payload)
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() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
# print( "返回结果:", data_get)
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_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。")
print("请求失败")
return data_get
@staticmethod
def entry_data_update(data: dict, max_retries: int = 20) -> dict: # 修改数据
"""
修改数据
:param max_retries: 最大重试次数此处设置100次
:param data: 简道云插件发送过来的data包含应用id表单id数据id等信息
:return: 修改数据后简道云返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/update'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"data_id": data['data_id'], # 数据ID
"data": data['data'],
"is_start_trigger": True
}
)
data_get = None
retries = 0
while retries <= max_retries:
try:
res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
# print(data_get)
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_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。")
continue
return data_get
@staticmethod
def workflow_instance_end(data: dict, max_retries: int = 20) -> dict:
"""
关闭流程
:param max_retries:
:param data: 简道云插件发送过来的data包含应用id
:return: 查询简道云流程实例信息返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v1/workflow/instance/close'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey
'Content-Type': 'application/json'
}
payload = json.dumps({
"instance_id": data['data_id'],
}
)
print("payload:", payload)
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() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
print("返回结果:", data_get)
if res.status_code == 200:
break # 成功则跳出循环
else:
retries += 1
time.sleep(3) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
return data_get
class NpEncoder(json.JSONEncoder):
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)
class YDAPI:
appKey = "ding5kqocon5s9oph5uq"
appSecret = "HL1jgsIIfLAC0eTH0A1m4mwxUDqbgsiPeCCGGE3ocM6qJBTIW7Ivt9drxF_Z4Kb_"
@staticmethod
def get_ids_query(token, formUuid, appType, systemToken, formInstanceIdList=None, max_retries=10, delay=2):
"""
函数功能读取表单的所有数据并加入重试机制
Args:
token (str): 登录验证token用于API调用的身份验证
formUuid (str): 表单唯一标识符用于指定需要读取哪个表单的实例数据
page (int): 分页参数指定请求的数据页码
n (int): 每页显示的数据条数
appType (str): 应用类型标识符默认为 "APP_UYZ0KG6L0CCNV80GZ66O"
systemToken (str): 系统token默认为固定值
instanceStatus (str): 流程实例状态默认为"RUNNING"
max_retries (int): 最大重试次数默认为10次
delay (int): 每次重试之间的延迟秒数默认为2秒
Returns:
dict: 返回从API获取的流程表单实例数据的JSON解析结果
Raises:
Exception: 如果达到最大重试次数仍未成功则抛出异常
"""
attempt = 0
api = f'https://api.dingtalk.com/v1.0/yida/forms/instances/ids/query'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
formData = {
"appType": appType,
"systemToken": systemToken,
"userId": "yida_pub_account", # 超级管理员账号
"language": "zh_CN",
"formUuid": formUuid,
"formInstanceIdList": formInstanceIdList,
}
# print(formData)
while True:
if attempt >= max_retries:
break
try:
res = requests.post(api, headers=headers, json=formData)
# print(res.json())
res.raise_for_status() # 如果返回状态码不是2xx,抛出异常
return res.json()
except requests.exceptions.RequestException as e:
time.sleep(delay)
attempt += 1
def generateToken(self) -> str:
"""
函数功能生成访问令牌token
Returns:
str: 返回生成的访问令牌字符串此token用于后续API调用的身份验证
"""
token_api = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
data = {
"appKey": f"{self.appKey}",
"appSecret": f'{self.appSecret}'
}
res = requests.post(token_api, json=data)
token = res.json().get('accessToken')
return token
def read_processes_instances(self, token, formUuid, page, n, appType="APP_UYZ0KG6L0CCNV80GZ66O",
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", instanceStatus="RUNNING",
max_retries=10, delay=2, createFromTimeGMT=None, createToTimeGMT=None,
modifiedFromTimeGMT=None,
modifiedToTimeGMT=None, searchFieldJson={}):
"""
函数功能读取流程表单的所有数据并加入重试机制
Args:
token (str): 登录验证token用于API调用的身份验证
formUuid (str): 表单唯一标识符用于指定需要读取哪个表单的实例数据
page (int): 分页参数指定请求的数据页码
n (int): 每页显示的数据条数
appType (str): 应用类型标识符默认为 "APP_UYZ0KG6L0CCNV80GZ66O"
systemToken (str): 系统token默认为固定值
instanceStatus (str): 流程实例状态默认为"RUNNING"
max_retries (int): 最大重试次数默认为10次
delay (int): 每次重试之间的延迟秒数默认为2秒
Returns:
dict: 返回从API获取的流程表单实例数据的JSON解析结果
Raises:
Exception: 如果达到最大重试次数仍未成功则抛出异常
"""
attempt = 0
api = f'https://api.dingtalk.com/v1.0/yida/processes/instances?pageNumber={page}&pageSize={n}'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
formData = {
"appType": appType,
"systemToken": systemToken,
"userId": "yida_pub_account", # 超级管理员账号
"language": "zh_CN",
"formUuid": formUuid,
"instanceStatus": instanceStatus, # 运行中
"createFromTimeGMT": createFromTimeGMT,
"createToTimeGMT": createToTimeGMT,
"modifiedFromTimeGMT": modifiedFromTimeGMT,
"modifiedToTimeGMT": modifiedToTimeGMT,
"searchFieldJson": json.dumps(
searchFieldJson
)
}
# print(formData)
while True:
if attempt >= max_retries:
# error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取流程实例数据,跳过本次请求。")
break
try:
res = requests.post(api, headers=headers, json=formData)
# print(res.json())
res.raise_for_status() # 如果返回状态码不是2xx,抛出异常
return res.json()
except requests.exceptions.RequestException as e:
# logger.warning(f"请求异常: {e},正在尝试第 {attempt + 1} 次重试...")
time.sleep(delay)
attempt += 1
def update_from(self, token, formInstanceId, data_new):
"""
函数功能更新表单内容
Args:
token (str): 登录验证token用于API调用的身份验证
formInstanceId (str): 表单实例ID读文件获取
data_new (dict): 新的数据内容用于替换现有表单实例中的数据读文件获取
Returns:
Response: 返回API请求的响应对象
"""
api = f'https://api.dingtalk.com//v1.0/yida/forms/instances'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
payload = {
"appType": "APP_UYZ0KG6L0CCNV80GZ66O",
"systemToken": "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2",
"userId": "yida_pub_account", # 曹伟 id
"language": "zh_CN",
"useLatestVersion": "false",
"formInstanceId": formInstanceId,
"updateFormDataJson": json.dumps(data_new, cls=NpEncoder),
}
res = requests.put(api, headers=headers, json=payload)
return res
def get_approval_records(self, token: str, processInstanceId: str, appType="APP_UYZ0KG6L0CCNV80GZ66O",
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", max_retries=10, delay=2):
"""
函数功能获取流程表单的审批记录适用于"F6客户服务"应用并且包含重试机制
Args:
token (str): 登录验证token用于API调用的身份验证
processInstanceId (str): 流程实例ID用于标识需要获取审批记录的具体流程实例
appType (str): 应用类型标识符默认为 "APP_UYZ0KG6L0CCNV80GZ66O"
systemToken (str): 系统token默认为固定值
max_retries (int): 最大重试次数默认为10次
delay (int): 每次重试之间的延迟秒数默认为2秒
Returns:
dict: 返回从API获取的审批记录的JSON解析结果通常包括审批步骤审批人审批时间等信息
"""
attempt = 0
userId = "yida_pub_account"
api = f'https://api.dingtalk.com/v1.0/yida/processes/operationRecords?appType={appType}&systemToken={systemToken}&userId={userId}&language=zh_CN&processInstanceId={processInstanceId}'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
while True:
if attempt >= max_retries:
# error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取审批数据,跳过本次请求。")
break
try:
res = requests.get(api, headers=headers)
res.raise_for_status() # 如果响应状态码不是2xx,则抛出HTTPError
return res.json()
except (requests.exceptions.RequestException, Exception) as e:
# logger.warning(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...")
time.sleep(delay) # 等待指定的延迟时间后再次尝试
attempt += 1
def aggree_approval(self, token: str, taskId: str, processInstanceId: str, formData: dict, res_new):
"""_summary_
函数功能同意审批节点 --F6客户服务 应用
Args:
token (str): 登录验证token
taskId (str): 获取到的审批节点ID
processInstanceId (str): 读取文件获得的实例ID
formData (dict): 数据样式
res_new (响应值): 从员工ID表里获取到员工名对应的员工ID
Returns:
响应值: 返回请求结果
"""
api = 'https://api.dingtalk.com/v1.0/yida/tasks/execute'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
payload = {
"outResult": "AGREE",
"appType": "APP_UYZ0KG6L0CCNV80GZ66O",
"systemToken": "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2",
"remark": "同意(接口自动)",
"formDataJson": json.dumps(formData, cls=NpEncoder),
"processInstanceId": processInstanceId,
"userId": res_new,
"language": "zh_CN",
"taskId": int(taskId)
}
res = requests.post(api, headers=headers, json=payload)
return res
api_instance = API()
yd_api_instance = YDAPI()
class YDToJDYRenewalToDo(object):
def __init__(self):
self.FORMID = "FORM-PE866MD1MJMU0WGLYRFLYEN5YN9L1I55Z7ZUK22"
self.appType = "APP_UYZ0KG6L0CCNV80GZ66O"
self.systemToken = "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2"
self.token = yd_api_instance.generateToken()
def load_all_data(self):
end_dt = datetime.now() + timedelta(days=1)
start_dt = end_dt - timedelta(days=90)
start_time = start_dt.strftime("%Y-%m-%d")
end_time = end_dt.strftime("%Y-%m-%d")
yd_data = yd_api_instance.read_processes_instances(
token=self.token,
formUuid=self.FORMID,
page=1,
n=100,
appType=self.appType,
systemToken=self.systemToken,
instanceStatus="",
modifiedFromTimeGMT=start_time,
modifiedToTimeGMT=end_time,
)
all_process_list = []
PAGES_two = yd_data.get('totalCount') // 100 + 1
for a in tqdm(range(1, PAGES_two + 1)):
try:
yd_data = yd_api_instance.read_processes_instances(
token=self.token,
formUuid=self.FORMID,
page=a,
n=100,
appType=self.appType,
systemToken=self.systemToken,
instanceStatus="",
modifiedFromTimeGMT=start_time,
modifiedToTimeGMT=end_time,
)
all_process_list = all_process_list + yd_data.get("data")
except Exception as e:
print(f"获取流程实例数据时出错: {e}")
continue
df_current = pd.DataFrame(all_process_list)
current_file = f"{output_dir}/{start_time}_{end_time}_all_process_list.csv"
# === 新增:读取上次文件并计算差值 ===
if os.path.exists(current_file):
try:
df_last = pd.read_csv(current_file)
except Exception as e:
print(f"读取历史文件失败: {e}")
df_last = pd.DataFrame()
else:
df_last = None # 明确标记:无历史文件
id_col = 'processInstanceId'
if df_last is not None and not df_last.empty and not df_current.empty and id_col in df_current.columns and id_col in df_last.columns:
# 有历史文件,计算新增
last_ids = set(df_last[id_col].astype(str))
current_ids = set(df_current[id_col].astype(str))
new_ids = current_ids - last_ids
diff_records = df_current[df_current[id_col].astype(str).isin(new_ids)].to_dict('records')
else:
# 没有历史文件 或 无法比对 → 返回全部当前数据
diff_records = df_current.to_dict('records') # ← 关键修改点
# 保存当前全量数据(覆盖)
df_current.to_csv(current_file, index=False)
return diff_records
def filter_renewal_data(self, all_process_list):
update_data_list = []
for item in all_process_list:
if item.get("data").get("textField_kto3q3ev"):
org_id = item.get("data").get("textField_ksydghqw")
order_code = item.get("data").get("textField_kto3q3ev") # 订单编码
pay_time = item.get("data").get("dateField_kto3q3ex") # 订单支付日期
res = yd_api_instance.get_ids_query(
token=self.token,
formInstanceIdList=[item.get("processInstanceId")],
formUuid="FORM-PE866MD1MJMU0WGLYRFLYEN5YN9L1I55Z7ZUK22",
appType="APP_UYZ0KG6L0CCNV80GZ66O",
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2",
)
payment_amount = None
bussiness_type = None
# print(res)
if res.get("result"):
form_data = res["result"][0]["formData"]
payment_amount = form_data.get("textField_kyjy1kkm") # "9987"
bussiness_type = form_data.get("textField_kyjy1kkn") # "续约"
# payment_amount = res.get("data").get("textField_kyjy1kkm") # 支付金额
# bussiness_type = res.get("data").get("textField_kyjy1kkn") # 业务类型
if pay_time:
# 确保是整数
timestamp_ms = int(pay_time)
# 转为 UTC datetime 对象
pay_datetime_utc = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
print(pay_datetime_utc) # 例如: 2024-04-05 12:34:38.901000+00:00
else:
pay_datetime_utc = None
update_data_list.append({
"order_code": order_code,
"pay_time": pay_time,
"payment_amount": payment_amount,
"bussiness_type": bussiness_type,
"org_id": org_id
})
return update_data_list
def check_jd_ydy_data(self, update_data_list):
def extract_widget_value(record, widget_id):
value = record.get(widget_id)
if isinstance(value, dict) and "value" in value:
return value.get("value")
return value
def has_value(value):
if value is None:
return False
if isinstance(value, str):
return value.strip() != ""
if isinstance(value, (list, dict)):
return len(value) > 0
return True
def pick_best_record(records):
if not records:
return None
time_fields = [
"_updateTime",
"_updated_at",
"_createTime",
"_created_at",
"updateTime",
"updated_at",
"createTime",
"created_at",
]
def extract_sort_value(record):
for field in time_fields:
value = record.get(field)
if value is None:
continue
try:
return int(value)
except Exception:
try:
return int(float(value))
except Exception:
continue
return 0
return max(records, key=extract_sort_value)
def query_jdy_by_org_id(org_id, flow_state=None):
cond = [{
"field": "_widget_1764820541661",
"type": "text",
"method": "eq",
"value": [org_id]
}]
if flow_state is not None:
cond.append({
"field": "flowState",
"type": "flowstate",
"method": "eq",
"value": [flow_state]
})
payload = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "6931063d64187eaf6b927557",
"filter": {
"rel": "and",
"cond": cond
}
}
return api_instance.entry_data_list(payload).get("data", [])
for item in update_data_list:
data_list = query_jdy_by_org_id(item.get("org_id"), flow_state=0)
if not data_list:
data_list = query_jdy_by_org_id(item.get("org_id"), flow_state=None)
result = pick_best_record(data_list)
data_id = result.get("_id") if result else None
if not data_id:
# print(f"未找到订单 {item.get('order_code')} 的简道云数据,跳过。")
continue
# 查询实例状态
instance_status = api_instance.workflow_instance_get({"data_id": data_id})
task_status = instance_status.get("status", -1)
print(f"订单 {item.get('order_code')} 的简道云流程状态为 {task_status}")
if task_status == 0:
print("简道云流程正在进行中,执行流程关闭")
api_instance.workflow_instance_end({"data_id": data_id})
target_values = {
"_widget_1764820541674": item.get("order_code"),
"_widget_1764820541679": item.get("pay_time"),
"_widget_1764820541676": item.get("payment_amount"),
"_widget_1764820541680": item.get("bussiness_type"),
}
data_to_update = {}
for widget_id, new_value in target_values.items():
existing_value = extract_widget_value(result, widget_id)
if has_value(existing_value):
continue
if not has_value(new_value):
continue
data_to_update[widget_id] = {"value": new_value}
if not data_to_update:
print(f"订单 {item.get('order_code')} 简道云字段已有值或无可写入内容,跳过同步")
continue
print("开始同步数据")
update_payload = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "6931063d64187eaf6b927557",
"data_id": data_id,
"data": data_to_update
}
print(update_payload)
api_instance.entry_data_update(update_payload)
print("数据同步完成")
def main(self):
task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
# step1 获取简道云与宜搭数据
jd_ydy_data = self.load_all_data()
if jd_ydy_data:
# step2 过滤已经续约的单子
update_data_list = self.filter_renewal_data(jd_ydy_data)
# step3 校验简道云是否有进行中的单子并关闭
self.check_jd_ydy_data(update_data_list)
else:
print("本次执行无处理数据")
except Exception as e:
print(e)
if __name__ == '__main__':
jd_ydy_renewal_to_do = YDToJDYRenewalToDo()
jd_ydy_renewal_to_do.main()
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import os
from datetime import datetime, timedelta, timezone
import pandas as pd
@@ -193,7 +194,8 @@ class API:
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"data_id": data['data_id'], # 数据ID
"data": data['data']
"data": data['data'],
"is_start_trigger": True
}
)
+69 -16
View File
@@ -233,6 +233,31 @@ class YDAPI:
appKey = "ding5kqocon5s9oph5uq"
appSecret = "HL1jgsIIfLAC0eTH0A1m4mwxUDqbgsiPeCCGGE3ocM6qJBTIW7Ivt9drxF_Z4Kb_"
@staticmethod
def _to_int_task_id(task_id):
if task_id is None:
raise ValueError("taskId is None")
if isinstance(task_id, bool):
raise ValueError(f"taskId is bool: {task_id}")
if isinstance(task_id, int):
return task_id
if isinstance(task_id, float):
if task_id.is_integer():
return int(task_id)
raise ValueError(f"taskId is non-integer float: {task_id}")
if isinstance(task_id, str):
s = task_id.strip()
if s.isdigit() or (s.startswith("-") and s[1:].isdigit()):
return int(s)
try:
f = float(s)
except ValueError as e:
raise ValueError(f"taskId is not numeric: {task_id}") from e
if f.is_integer():
return int(f)
raise ValueError(f"taskId is non-integer numeric string: {task_id}")
raise ValueError(f"taskId has unsupported type: {type(task_id).__name__}")
def generateToken(self) -> str:
"""
函数功能生成访问令牌token
@@ -301,10 +326,10 @@ class YDAPI:
while True:
if attempt >= max_retries:
# error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取流程实例数据,跳过本次请求。")
break
return {"data": []}
try:
res = requests.post(api, headers=headers, json=formData)
res = requests.post(api, headers=headers, json=formData, timeout=15)
# print(res.json())
res.raise_for_status() # 如果返回状态码不是2xx,抛出异常
return res.json()
@@ -313,6 +338,11 @@ class YDAPI:
# logger.warning(f"请求异常: {e},正在尝试第 {attempt + 1} 次重试...")
time.sleep(delay)
attempt += 1
except Exception:
time.sleep(delay)
attempt += 1
return {"data": []}
def update_from(self, token, formInstanceId, data_new):
"""
@@ -345,7 +375,7 @@ class YDAPI:
}
res = requests.put(api, headers=headers, json=payload)
res = requests.put(api, headers=headers, json=payload, timeout=15)
return res
def get_approval_records(self, token: str, processInstanceId: str, appType="APP_UYZ0KG6L0CCNV80GZ66O",
@@ -375,16 +405,17 @@ class YDAPI:
while True:
if attempt >= max_retries:
# error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取审批数据,跳过本次请求。")
break
return {"result": []}
try:
res = requests.get(api, headers=headers)
res = requests.get(api, headers=headers, timeout=15)
res.raise_for_status() # 如果响应状态码不是2xx,则抛出HTTPError
return res.json()
except (requests.exceptions.RequestException, Exception) as e:
# logger.warning(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...")
time.sleep(delay) # 等待指定的延迟时间后再次尝试
attempt += 1
return {"result": []}
def aggree_approval(self, token: str, taskId: str, processInstanceId: str, formData: dict, res_new):
"""_summary_
@@ -415,10 +446,10 @@ class YDAPI:
"processInstanceId": processInstanceId,
"userId": res_new,
"language": "zh_CN",
"taskId": int(taskId)
"taskId": self._to_int_task_id(taskId)
}
res = requests.post(api, headers=headers, json=payload)
res = requests.post(api, headers=headers, json=payload, timeout=15)
return res
@@ -449,9 +480,22 @@ class JDYToYDRenewalToDo(object):
"30天是否跟进": "_widget_1764820541632",
"30天处理人": "_widget_1764820541636",
"30天跟进时间": "_widget_1765352838633",
"是否联系上":"_widget_1764820541638",
"数据ID": "_id"
}
@staticmethod
def _as_list(val):
if val is None:
return []
if isinstance(val, list):
return val
if isinstance(val, tuple):
return list(val)
if isinstance(val, dict):
return [val]
return []
def load_all_data(self):
# 获取简道云已派发续约待办,若无数据直接返回
today_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d")
@@ -463,7 +507,7 @@ class JDYToYDRenewalToDo(object):
"value": [""]}]},
}
renewal = api_instance.entry_data_list(payload)
self.renewal_data_list = renewal.get("data") or []
self.renewal_data_list = self._as_list((renewal or {}).get("data"))
if not self.renewal_data_list:
self.renewal_data_df = pd.DataFrame()
return
@@ -478,7 +522,7 @@ class JDYToYDRenewalToDo(object):
all_data = []
for _, row in self.renewal_data_df.iterrows():
yd_data = yd_api_instance.read_processes_instances(
yd_resp = yd_api_instance.read_processes_instances(
token=self.token,
formUuid=self.FORMID,
page=1,
@@ -487,9 +531,12 @@ class JDYToYDRenewalToDo(object):
systemToken=self.systemToken,
instanceStatus="",
searchFieldJson={"textField_ksydghqw": row["_widget_1764820541661"]},
).get("data", [])
)
yd_data = self._as_list((yd_resp or {}).get("data"))
for record in yd_data:
if not isinstance(record, dict):
continue
enriched = {**record}
enriched.update({k: row.get(v, "") for k, v in self.follow_up_fields.items()})
all_data.append(enriched)
@@ -704,20 +751,20 @@ class JDYToYDRenewalToDo(object):
data = {"data_id": item["数据ID"]}
jdy_workflow_data = api_instance.workflow_instance_get(data) or {}
print("简道云流程日志:", jdy_workflow_data)
jdy_logs = jdy_workflow_data.get("logs", [])
jdy_logs = self._as_list(jdy_workflow_data.get("logs"))
jdy_logs = sorted(
jdy_logs,
key=lambda x: parse_dt(x.get("finish_time") or x.get("create_time")),
reverse=True,
)
tasks = jdy_workflow_data.get("tasks", []) or []
tasks = self._as_list(jdy_workflow_data.get("tasks"))
pending = [t for t in tasks if t.get("status") == 0]
task_candidate = (pending[0] if pending else None) or (sorted(
tasks,
key=lambda x: parse_dt(x.get("create_time")),
reverse=True
)[0] if tasks else {})
jdy_result_records = jdy_workflow_data.get("result", []) or []
jdy_result_records = self._as_list(jdy_workflow_data.get("result"))
jdy_stage_candidates = [
extract_stage_from_text(str(jdy_logs[0].get("flow_name", ""))) if jdy_logs else None,
@@ -740,7 +787,7 @@ class JDYToYDRenewalToDo(object):
systemToken=self.systemToken
) or {}
print("宜搭流程日志:", yd_workflow_data)
yd_results = yd_workflow_data.get("result", []) or []
yd_results = self._as_list(yd_workflow_data.get("result"))
# 展开宜搭 domainList 以获取所有动作
yd_records = [
@@ -753,7 +800,7 @@ class JDYToYDRenewalToDo(object):
key=lambda x: parse_dt(x.get("operateTimeGMT") or x.get("activeTimeGMT")),
reverse=True,
)
# 优先使用当前待办(type == TODO),否则用最新一条
# 优先使用当前待办(type == "TODO"),否则用最新一条
yd_todo = next((r for r in yd_records if str(r.get("type")).upper() == "TODO"), None)
yd_latest = yd_todo or (yd_records[0] if yd_records else {})
yd_stage = extract_stage_from_text(
@@ -784,6 +831,11 @@ class JDYToYDRenewalToDo(object):
if not task_id or not operator_user_id:
print(f"缺少 taskId 或 operatorUserId,无法自动同意 processInstanceId={item['processInstanceId']}")
continue
try:
task_id = yd_api_instance._to_int_task_id(task_id)
except Exception as e:
print(f"taskId 非法,跳过自动同意 processInstanceId={item['processInstanceId']} taskId={task_id} err={e}")
continue
try:
yd_api_instance.aggree_approval(
token=self.token,
@@ -798,10 +850,11 @@ class JDYToYDRenewalToDo(object):
def retrun_jdy(self, yd_update_list):
for item in yd_update_list:
data_id= str(item.get('数据ID'))
data = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "6931063d64187eaf6b927557",
"data_id": item.get('数据ID'),
"data_id": data_id,
"data":
{
"_widget_1766469131897": {"value": ""},
File diff suppressed because one or more lines are too long
+138
View File
@@ -0,0 +1,138 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "## 保存boss请求结果",
"id": "311a82d4faf8e2d"
},
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2025-11-10T06:24:23.858755Z",
"start_time": "2025-11-10T06:24:22.994108Z"
}
},
"source": [
"# 标准库\n",
"import os\n",
"import time\n",
"import random\n",
"import json\n",
"import binascii\n",
"from datetime import date, timedelta, datetime\n",
"from urllib.parse import quote\n",
"from pathlib import Path\n",
"\n",
"# 第三方库\n",
"import numpy as np\n",
"import pandas as pd\n",
"import requests\n",
"from pyDes import des, CBC, PAD_PKCS5\n",
"import mysql.connector\n",
"from mysql.connector import Error\n",
"\n",
"# PostgreSQL(如果你用到了)\n",
"import psycopg2\n",
"\n",
"# 自定义模块\n",
"from config import Config\n",
"from api import API\n",
"from back_ground_module import CommonModule\n",
"from log_config import configure_task_logger, configure_error_task_logger\n",
"\n",
"\n",
"logger = configure_task_logger()\n",
"error_task_logger = configure_error_task_logger()\n",
"api_instance = API()\n",
"common_module = CommonModule()\n",
"output_dir = \"output\" # 设置输出目录\n",
"os.makedirs(output_dir, exist_ok=True)\n",
"\n",
"def des_encrypt(s):\n",
" \"\"\"\n",
" DES 加密\n",
" :param s: 原始字符串\n",
" :return: 加密后字符串,16进制\n",
" \"\"\"\n",
" secret_key = 'HwdMBW8o'\n",
" iv = secret_key\n",
" k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n",
" en = k.encrypt(s, padmode=PAD_PKCS5)\n",
" return binascii.b2a_base64(en, newline=False)\n",
"\n",
"\n",
"def des_descrypt(s):\n",
" \"\"\"\n",
" DES 解密\n",
" :param s: 加密后的字符串,16进制\n",
" :return: 解密后的字符串\n",
" \"\"\"\n",
" secret_key = 'HwdMBW8o'\n",
" iv = secret_key\n",
" k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n",
" de = k.decrypt(binascii.a2b_base64(s), padmode=PAD_PKCS5)\n",
" return de\n",
"\n",
"data_NGV = common_module.get_renewal_details()\n",
"\n",
"\n",
"for i in range(0,len(data_NGV[\"date_fmt\"])):\n",
" t = time.time()\n",
" ts = int(round(t * 1000))\n",
" randint = random.randint(100000000, 999999999)\n",
" req = data_NGV['id_own_org'][i] + \"_\" + str(ts) + \"_\" + str(randint)\n",
" str_en = des_encrypt(req)\n",
" req_new = str_en.decode('utf-8')\n",
"\n",
" url = f\"http://manage.f6yc.com/hive-admin/py/yida/renewal/orgInfo\"\n",
" data = {\n",
" 'req':req_new,\n",
" 't':ts,\n",
" 'r':randint\n",
" }\n",
" res = requests.post(url,data=data)\n",
" # print(res.json.json())\n",
"\n",
" break\n",
"\n",
"print(len(data_NGV))"
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"距离今天还有120天的日期是:2026-03-10\n",
"29\n"
]
}
],
"execution_count": 2
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+71
View File
@@ -0,0 +1,71 @@
,_id,提交人,updater,deleter,提交时间,更新时间,deleteTime,flowState,报备类型,协作内容,情况说明,订单编号,年限,版本,实付金额,商品名称,履约金额,门店编码,门店名称,支付日期,开户/处理日期,业绩归属日期,业绩类型,公司名称,公司ID,报备业绩金额-区域提交,业绩归属小六-区域提交,业绩归属月,是否同步衡石,小六业绩金额,区域业绩金额,报备业绩归属小六,报备业绩归属区域经理,报备业绩归属大区,原业绩归属人,原业绩归属区域经理,原业绩归属大区,小六业绩比例,区域业绩比例,运营专家,业绩动作,提成类型,新签阶段及提成比例,提成金额,SaaS新签提成比例,服务包提成比例,新签提成比例-首年,新签提成比例-非首年,提成动作,业绩类型-聚合,业绩分组,流程是否结束,appId,entryId
0,68d1039a408016fe13556b06,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:06:50,2025-12-25 15:32:02,,2,多年补差价,,6月订单,9月补差价2年,1757735155184,2,入门版,999,,,CHS202506010300195,上海汨晨汽车服务有限公司,2025-09-13 00:00:00,2025-06-01 00:00:00,2025-09-13 00:00:00,新签,,,,,,是,999.0,999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
1,68d103cdb8f662bfdf1b7ea2,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:07:41,2025-12-25 15:32:02,,2,新签超3年,,新签5年,1758076816064,2,标准版,3000,,,CHS202302130204882,江阴市华士爱驹汽车养护店,2025-09-17 00:00:00,2025-09-17 00:00:00,2025-09-17 00:00:00,新签,,,,,,是,3000.0,3000.0,赵旭伟,肖军,江苏,赵旭伟,肖军,江苏,1.0,1.0,陈博,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
2,68d103f17f34705c8dbe360a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:08:17,2025-12-25 15:32:02,,2,新签超3年,,新签5年,1758181826462,2,基础版,1600,,,CHS202504240297202,古城区鸿远轮胎服务中心,2025-09-18 00:00:00,2025-09-18 00:00:00,2025-09-18 00:00:00,新签,,,,,,是,1600.0,1600.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
3,68d10415f3728b4cd791630e,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:08:53,2025-12-25 15:32:02,,2,新签超3年,,5年订单,1758259644403,2,基础版,1759,,,CHS202509190309704,金堂县赵镇四达汽修厂,2025-09-19 00:00:00,2025-09-19 00:00:00,2025-09-19 00:00:00,新签,,,,,,是,1759.0,1759.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
4,68d1042ee3ee1af6d0d7f8ee,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:09:18,2025-12-25 15:32:02,,2,新签超3年,,,1756814144233,2,入门版,1000,,,CHS202509020309092,天津市滨海新区安驰汽车修理服务部,2025-09-03 00:00:00,2025-09-02 00:00:00,2025-09-03 00:00:00,新签,,,,,,是,1000.0,1000.0,王鑫,关磊,华北,王鑫,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
5,68d104502618b5ea53f4264f,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:09:52,2025-12-25 15:32:02,,2,新签超3年,,,1757245487196,2,基础版,1400,,,CHS202509070309251,车广角盘锦店,2025-09-08 00:00:00,2025-09-07 00:00:00,2025-09-08 00:00:00,新签,,,,,,是,1400.0,1400.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
6,68d10471c1d4a4211d2ce420,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:10:25,2025-12-25 15:32:02,,2,新签超3年,,,1757388589517,1,进阶版,1000,,,CHS202509090309331,上海德伽汽车服务中心,2025-09-09 00:00:00,2025-09-09 00:00:00,2025-09-09 00:00:00,新签,,,,,,是,1000.0,1000.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
7,68d104a081bf67abc88ce650,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:11:12,2025-12-25 15:32:02,,2,新签超3年,,,1757752521945,2,标准版,3000,,,CHS202509130309507,腾冲诚亿汽车修理有限公司,2025-09-14 00:00:00,2025-09-13 00:00:00,2025-09-14 00:00:00,新签,,,,,,是,3000.0,3000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
8,68d104e0304ea14df52c1128,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:12:16,2025-12-25 15:32:02,,2,品牌方协作,电子目录,电子目录,,,,12000,,,,,,,2025-09-22 16:12:16,,,,,,,是,0.0,0.0,杜浩,肖军,江苏,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
9,68d104fce01701d04e9ba14d,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:12:44,2025-12-25 15:32:02,,2,品牌方协作,电子目录,,,,,8000,,,,,,,2025-09-22 16:12:44,,,,,,,是,0.0,0.0,韩皞,陈庆伟,东北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
10,68d105177771c4e88d61d725,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:13:11,2025-12-25 15:32:02,,2,品牌方协作,电子目录,,,,,9000,,,,,,,2025-09-22 16:13:11,,,,,,,是,0.0,0.0,胡楠,景东强,西北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
11,68d2342ba541a358893d61ef,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-23 13:46:19,2025-12-25 15:32:02,,2,多年补差价,,,1758459953833,2,入门版,1000,,,CHS202505070297933,上海义诚汽车服务有限公司,2025-09-22 00:00:00,2025-05-07 00:00:00,2025-09-22 00:00:00,新签,,,,,,是,1000.0,1000.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
12,68d3bc8d99f9d49450ed8b1a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-24 17:40:29,2025-12-25 15:32:02,,2,品牌方协作,电子目录,电子目录,小六未参与,,,,9000,,,,,,,2025-09-24 17:40:29,,,,,,,是,0.0,0.0,胡楠,景东强,西北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
13,68d494d8c6f070c9f68a4e94,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-25 09:03:20,2025-12-25 15:32:02,,2,新签超3年,,,1758612872486,1,基础版,799,,,CHS202509230309865,回民区青辰宝悦汽车维修中心(个体工商户),2025-09-23 00:00:00,2025-09-23 00:00:00,2025-09-23 00:00:00,新签,,,,,,是,799.0,799.0,张宏伟,关磊,华北,张宏伟,关磊,华北,1.0,1.0,武宏超,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
14,68d5f2a0b3bc5add3c3d7a23,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:55:44,2025-12-25 15:32:02,,2,新签超3年,,,1758789984175,2,入门版,1000,,,CHS202509250310033,济南双江汽车服务有限公司,2025-09-26 00:00:00,2025-09-25 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,1000.0,1000.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
15,68d5f2c3f7765d3eddb8506a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:56:19,2025-12-25 15:32:02,,2,多年补差价,,,1758787369355,2,旗舰版,4000,,,CHS202505190299143,政德汽修,2025-09-26 00:00:00,2025-05-19 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,4000.0,4000.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
16,68d5f2eb8eb300e7d401f7f9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:56:59,2025-12-25 15:32:02,,2,新签超3年,,,1758786185157,2,标准版,3120,,,CHS202509250310019,红河州秀林工贸有限公司,2025-09-26 00:00:00,2025-09-25 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
17,68d8882a40f51cce582eddb5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-28 08:58:18,2025-12-25 15:32:02,,2,多年补差价,,,1758894296058,2,进阶版,2501,,,CHS202107030131629,灵山县车益汽车维修厂,2025-09-27 00:00:00,2025-08-09 00:00:00,2025-09-27 00:00:00,新签,,,,,,是,2501.0,2501.0,黄环宇,张凯,华南沪,黄环宇,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],125.05,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
18,68d9e2d9710265914f554379,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-29 09:37:29,2025-12-25 15:32:02,,2,跨区新签,,,1759047159482,3,旗舰版,7000,,,CHS202509280310174,荟星行汽车服务有限公司,2025-09-29 00:00:00,2025-09-28 00:00:00,2025-09-29 00:00:00,新签,,,,,,是,3500.0,3500.0,韩皞,陈庆伟,东北,张宏伟,关磊,华北,0.5,0.5,孙旭亮,拆单,,[],315.0,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
19,68d9f2549d0de29d680f6403,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-29 10:43:32,2025-12-25 15:32:02,,2,新签超3年,,,1759063075745,2,入门版,920,,,CHS202509280310180,宏运汽修,2025-09-29 00:00:00,2025-09-28 00:00:00,2025-09-29 00:00:00,新签,,,,,,是,920.0,920.0,柴铁峰,陈庆伟,东北,柴铁峰,陈庆伟,东北,1.0,1.0,刘立,新增,,[],46.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
20,68db3e73890706b7678f34ea,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-30 10:20:35,2025-12-25 15:32:02,,2,新签超3年,,,1757909706794,2,旗舰版,4400,,,CHS202509150309534,监利市壹加汽车服务有限公司,2025-09-15 00:00:00,2025-09-15 00:00:00,2025-09-15 00:00:00,新签,,,,,,是,4400.0,4400.0,陈煜,景东强,华中,陈煜,景东强,华中,1.0,1.0,刘光春,新增,,[],220.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
21,68db50ff8e50807e1e84e8f8,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-30 11:39:43,2025-12-25 15:32:02,,2,新签超3年,,,1759039606751,1,标准版,1500,,,CHS202509280310149,鄂尔多斯市心成泰汽车维修服务有限公司,2025-09-28 00:00:00,2025-09-28 00:00:00,2025-09-28 00:00:00,新签,,,,,,是,1500.0,1500.0,张宏伟,关磊,华北,张宏伟,关磊,华北,1.0,1.0,武宏超,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
22,68e71b9216dd2af696d3c489,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-09 10:18:58,2025-12-25 15:32:02,,2,新签超3年,,,1759573927652,1,基础版,800,,,CHS202303010210121,德系专修,2025-10-05 00:00:00,2025-10-04 00:00:00,2025-10-05 00:00:00,新签,,,,,,是,800.0,800.0,刘磊,关磊,华北,刘磊,关磊,华北,1.0,1.0,,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
23,68e768c27a4eb1ea616434b0,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-09 15:48:18,2025-12-25 15:32:02,,2,新签超3年,,,1759395920361,2,进阶版,2000,,,CHS202510020310341,官渡区众名汽车维修服务经营部,2025-10-03 00:00:00,2025-10-03 00:00:00,2025-10-03 00:00:00,新签,,,,,,是,2000.0,2000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],100.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
24,68e9ba0cd69c47d29c2e0972,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-11 09:59:40,2025-12-25 15:32:02,,2,新签超3年,,,1760012463416,1,进阶版,1000,,,CHS202510090310521,新乡骏享汽车销售服务有限公司,2025-10-10 00:00:00,2025-10-10 00:00:00,2025-10-10 00:00:00,新签,,,,,,是,1000.0,1000.0,王兵帅,张凯,河南,王兵帅,张凯,河南,1.0,1.0,邢恒岭,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
25,68eda452872200f9abed2d5d,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-14 09:16:02,2025-12-25 15:32:02,,2,多年补差价,,,1760180223661,2,进阶版,1999,,,CHS202507090303811,上海洗事临门汽车服务有限公司,2025-10-12 00:00:00,2025-07-09 00:00:00,2025-10-12 00:00:00,新签,,,,,,是,1999.0,1999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],99.95,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
26,68f830d486f984a8f5d58949,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-22 09:18:12,2025-12-25 15:32:02,,2,电销业绩统计,,,1760341341750,3,基础版,2900,,,CHS202411020284735,深圳康得新KDX大膜王星级甄选店,2025-10-13 00:00:00,2025-10-13 00:00:00,2025-10-13 00:00:00,新签,,,,,,是,0.0,2900.0,严冬延,张凯,华南沪,耿渝淇,张凯,,,1.0,,新增,,[],130.5,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
27,68f98a2a9cdcf060fcebf670,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-23 09:51:38,2025-12-25 15:32:02,,2,新签超3年,,,1761122509436,2,进阶版,2000,,,CHS202409190281922,海口小拇指汽车服务,2025-10-23 00:00:00,2025-10-22 00:00:00,2025-10-23 00:00:00,新签,,,,,,是,2000.0,2000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],100.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
28,68fecdec710ccbf6abfdcf9c,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-27 09:42:04,2025-12-25 15:32:02,,2,新签超3年,,,1761365304042,2,标准版,3200,,,CHS202510250311422,博越汽修,2025-10-25 00:00:00,2025-10-25 00:00:00,2025-10-25 00:00:00,新签,,,,,,是,3200.0,3200.0,王有军,陈庆伟,浙皖,王有军,陈庆伟,浙皖,1.0,1.0,,新增,,[],160.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
29,6901795327c25049c38f1e2b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-29 10:17:55,2025-12-25 15:32:02,,1,新签超3年,,,1761644690009,2,基础版,1600,,,CHS202510280311584,宜良捷驰汽车修理厂,2025-10-29 00:00:00,2025-10-29 00:00:00,2025-10-29 00:00:00,新签,,,,,,是,1600.0,1600.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],80.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
30,690179cf34ed36233a8166fd,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-29 10:19:59,2025-12-25 15:32:02,,1,跨区新签,,,1761611436641,1,旗舰版,4499,,,CHS202510280311527,易道大咖乌鲁木齐东坪店,2025-10-28 00:00:00,2025-10-28 00:00:00,2025-10-28 00:00:00,新签,,,,,,是,2249.5,2249.5,孙振华,景东强,西北,潘志强,张凯,华南沪,0.5,0.5,,拆单,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
31,69041f3e38fb7000e0ffc32e,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-31 10:30:22,2025-12-25 15:32:02,,1,新签超3年,,,1761816948126,2,至尊版,8000,,,CHS202510300312383,意嘉易春天里店,2025-10-31 00:00:00,2025-10-31 00:00:00,2025-10-31 00:00:00,新签,,,,,,是,8000.0,8000.0,范启超,肖军,西南,范启超,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
32,69095a58a9520f1eeaf2afcb,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-04 09:43:52,2025-12-25 15:32:02,,1,新签超3年,,,1762158897286,2,基础版,5000,续约旗舰版-门店管理系统2年,2500.0,CHS202511030312575,宣威市龙泉汽车有限公司,2025-11-04 00:00:00,2025-11-04 00:00:00,2025-11-04 00:00:00,新签,,,,,,是,5000.0,5000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
33,690aae75afd572c006769a86,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:55:01,2025-12-25 15:32:02,,1,新签超3年,,,1762245527558,2,进阶版,2240,续约进阶版2年,1120.0,CHS202312020253454,乐山郭建军,2025-11-05 00:00:00,2025-11-05 00:00:00,2025-11-05 00:00:00,新签,,,,,,是,2240.0,2240.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
34,690aae91ff9575eb8a758dc3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:55:29,2025-12-25 15:32:02,,1,多年补差价,,,1761960739508,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202509230309853,都江堰市爱都汽车维修有限责任公司,2025-11-01 00:00:00,2025-09-23 00:00:00,2025-11-01 00:00:00,新签,,,,,,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
35,690aaec3f23eb35e6e394e54,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:56:19,2025-12-25 15:32:02,,1,多年补差价,,,1761955976569,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202509150309531,三越汽车音响,2025-11-01 00:00:00,2025-09-12 00:00:00,2025-11-01 00:00:00,新签,,,,,,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
36,690c00df267ca78f0a86da76,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-06 09:58:55,2025-12-25 15:32:02,,1,新签超3年,,,1762263761814,2,入门版,1600,续约入门版2年,800.0,CHS202105140124767,茂名市电白区华南汽车维修有限公司,2025-11-05 00:00:00,2025-11-04 00:00:00,2025-11-05 00:00:00,新签,,,,,,是,1600.0,1600.0,严冬延,张凯,华南沪,严冬延,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
37,690d4feafeb41d02c491646b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-07 09:48:26,2025-12-25 15:32:02,,1,多年补差价,,,1762418825828,2,旗舰版,3599,续约旗舰版-门店管理系统2年,1799.5,CHS202508090305351,山海车服,2025-11-07 00:00:00,2025-08-09 00:00:00,2025-11-07 00:00:00,新签,上海山高海深汽车服务有限公司,15975930111903944708,,,,是,3599.0,3599.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
38,690d5114ebf892f67c83c3d9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-07 09:53:24,2025-12-25 15:32:02,,1,新签超3年,,,1762401310694,2,进阶版,2240,续约进阶版2年,1120.0,CHS202003250058491,博伟汽修(新南路),2025-11-06 00:00:00,2025-11-06 00:00:00,2025-11-06 00:00:00,新签,博伟汽修,10546443563986851726,,,,是,2240.0,2240.0,胡仲远,肖军,西南,胡仲远,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
39,6915529caca516e7c6c28b71,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-13 11:38:04,2025-12-25 15:32:02,,1,多年补差价,,,1762933140216,2,基础版,1998,续约基础版-门店管理系统2年,999.0,CHS202209140188566,德奥养车,2025-11-12 00:00:00,2025-11-08 00:00:00,2025-11-12 00:00:00,新签,德奥养车,11240984669917483539,,,,是,1998.0,1998.0,陈晨,关磊,华北,陈晨,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
40,691a76c508e9a99cb7e65565,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-17 09:13:41,2025-12-25 15:32:02,,1,新签超3年,,,1763100107052,2,标准版,3120,续约标准版-门店管理系统2年,1560.0,CHS202511150313111,云南锡业集团汽车技术服务有限公司,2025-11-14 00:00:00,2025-11-14 00:00:00,2025-11-14 00:00:00,新签,云南锡业集团汽车技术服务(文山服务中心),16011444960305905673,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
41,691c0a70d6f09daa8e0af427,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:00,2025-12-25 15:32:02,,1,多年补差价,,,1763366190883,2,入门版,999,续约入门版2年,499.5,CHS202509020309077,宜嘉养车,2025-11-17 00:00:00,2025-09-02 00:00:00,2025-11-17 00:00:00,新签,宜嘉养车,15984698892155383884,,,,是,999.0,999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
42,691c0a845f80e95f659f9b26,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:20,2025-12-25 15:32:02,,1,新签超3年,,,1763351228539,1,进阶版,1000,续约进阶版,1000.0,CHS202511170313156,郑州欧汇汽车维修有限公司,2025-11-17 00:00:00,2025-11-17 00:00:00,2025-11-17 00:00:00,新签,郑州欧汇汽车维修有限公司,16012185257256194131,,,,是,1000.0,1000.0,王兵帅,张凯,河南,王兵帅,张凯,河南,1.0,1.0,邢恒岭,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
43,691c0a9576f2783074f62089,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:37,2025-12-25 15:32:02,,1,新签超3年,,,1763370516248,2,标准版,3120,续约标准版-门店管理系统2年,1560.0,CHS202511170313182,文山万霆新能源科技有限责任公司,2025-11-18 00:00:00,2025-11-18 00:00:00,2025-11-18 00:00:00,新签,文山万霆新能源科技有限责任公司,16012268807246610438,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
44,691d39d9de406a40ae9c1859,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-19 11:30:33,2025-12-25 15:32:02,,1,多年补差价,,,1763447333680,2,入门版,800,续约入门版2年,400.0,CHS202510070310394,盛发汽车维修保养中心,2025-11-18 00:00:00,2025-10-07 00:00:00,2025-11-18 00:00:00,新签,滨海凯盛汽修养护中心,15997329870740811799,,,,是,800.0,800.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
45,691e88e504528289184a3d00,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-20 11:20:05,2025-12-25 15:32:02,,1,多年补差价,,,1763522175031,2,基础版,2300,续约基础版-门店管理系统2年,1150.0,CHS202511190313271,徐宝行汽车服务有限公司,2025-11-19 00:00:00,2025-11-19 00:00:00,2025-11-19 00:00:00,新签,徐州徐宝行汽车服务有限公司,16012883105031421976,,,,是,2300.0,2300.0,赵涛,肖军,江苏,赵涛,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
46,691fdb3878dc061019100add,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-21 11:23:36,2025-12-25 15:32:02,,1,多年补差价,,,1763619899692,2,入门版,700,续约入门版2年,350.0,CHS202510230311351,梵远汽车服务,2025-11-20 00:00:00,2025-10-23 00:00:00,2025-11-20 00:00:00,新签,南京市雨花台区梵远汽车配件销售中心,16003167838416171074,,,,是,700.0,700.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
47,6927e49b21b02c48ca7bb1f5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-27 13:41:47,2025-12-25 15:32:02,,1,多年补差价,,,1764149126832,2,进阶版,1799,续约进阶版2年,899.5,CHS202511080312789,鼎鹏汽车服务中心,2025-11-27 00:00:00,2025-11-08 00:00:00,2025-11-27 00:00:00,新签,贵阳市云岩区鼎鹏汽车养护中心,16008912692744061003,,,,是,1799.0,1799.0,熊斌,肖军,西南,熊斌,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
48,6928ffbe82767255aabb44be,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:49:50,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,18000,,,,,,,2025-11-27 00:00:00,新签,,,,,,是,9000.0,18000.0,梁柱,张凯,华南沪,,,,0.5,1.0,黄宗祥,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
49,6928fff076d8fe5abdd61266,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:50:40,2025-12-25 15:32:02,,1,新签超3年,,,1762590806733,2,基础版,1600,续约基础版-门店管理系统2年,800.0,CHS202511080312804,华营昌检车线店,2025-11-09 00:00:00,2025-11-08 00:00:00,2025-11-09 00:00:00,新签,阳泉华营昌汽修连锁,10907434497378123878,,,,是,1600.0,1600.0,刘剑桥,关磊,华北,刘剑桥,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
50,692900560e8ab9d6604193f2,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:52:22,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,5000,,,,,,,2025-11-07 00:00:00,新签,,,,,,是,2500.0,5000.0,赵旭伟,肖军,江苏,,,,0.5,1.0,陈博,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
51,6929055f9e94d391e9cd53dd,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 10:13:51,2025-12-25 15:32:02,,1,新签超3年,,,1762739543304,2,入门版,1200,续约入门版2年,600.0,CHS202511090312841,小欢汽车维修,2025-11-10 00:00:00,2025-11-10 00:00:00,2025-11-10 00:00:00,新签,义县稍户营子镇小欢汽车维修处(个体工商户),16009461720019927075,,,,是,1200.0,1200.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
52,692cfbd8da792279e268feef,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:16,2025-12-25 15:32:02,,1,多年补差价,,,1764382941584,2,入门版,800,续约入门版2年,400.0,CHS202510170310927,盐城市大丰区辰煜汽车服务有限公司,2025-11-29 00:00:00,2025-10-18 00:00:00,2025-11-29 00:00:00,新签,盐城市大丰区辰煜汽车服务有限公司,10546443563782178538,,,,是,800.0,800.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
53,692cfbe7ff1b578004611ddc,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:31,2025-12-25 15:32:02,,1,新签超3年,,,1764333612264,2,标准版,3600,续约标准版-门店管理系统2年,1800.0,CHS202511280313670,厦门市鑫车宝汽车服务有限公司,2025-11-29 00:00:00,2025-11-28 00:00:00,2025-11-29 00:00:00,新签,厦门市鑫车宝汽车服务有限公司,16016206505594359823,,,,是,3600.0,3600.0,郭锦城,张凯,华南沪,郭锦城,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
54,692cfc00bc219a1741c27ffc,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:56,2025-12-25 15:32:02,,1,多年补差价,,,1764502026835,2,进阶版,2001,续约进阶版2年,1000.5,CHS202511250313539,天门市小拇指汽车维修服务有限公司,2025-12-01 00:00:00,2025-11-26 00:00:00,2025-12-01 00:00:00,新签,天门市小拇指汽车维修服务有限公司,16015175963579027532,,,,是,2001.0,2001.0,陈煜,景东强,华中,陈煜,景东强,华中,1.0,1.0,刘光春,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
55,6938df09cc655df92928b5b3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-10 10:46:33,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,8000,,,,,,,2025-12-10 00:00:00,新签,,,8000,,2025-12-01 00:00:00,是,4000.0,8000.0,孙旭亮,陈庆伟,东北,,,,0.5,1.0,孙旭亮,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
56,69391bbdcdfe09bce4c98cd7,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-10 15:05:33,2025-12-25 15:32:02,,1,新签超3年,,,1764473175419,2,进阶版,2000,续约进阶版2年,1000.0,CHS202511300313735,唐山市路南新腾云汽车维修服务站,2025-11-30 00:00:00,2025-11-30 00:00:00,2025-11-30 00:00:00,新签,唐山市路南新腾云汽车维修服务站,16016880445115371607,,,,是,2000.0,2000.0,王鑫,关磊,华北,王鑫,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
57,693b7c9a25d3e9c841729ed3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-12 10:23:22,2025-12-25 15:32:02,,1,新签超3年,,,1765420934693,2,入门版,1000,续约入门版2年,500.0,CHS202512110314232,西昌安宁美车度汽修,2025-12-11 00:00:00,2025-12-11 00:00:00,2025-12-11 00:00:00,新签,西昌安宁美车度汽车喷漆中心,16020868221515104344,,,,是,1000.0,1000.0,杨君毅,肖军,西南,杨君毅,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
58,693f6a268691ed316215299b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:53:42,2025-12-25 15:32:02,,1,跨区新签,,,1765158079178,3,基础版,3000,基础版-门店管理系统3年,1000.0,CHS202512080314089,鹊大师(大同店),2025-12-08 00:00:00,2025-12-08 00:00:00,2025-12-08 00:00:00,新签,广之源汽车一站式服务中心,16000651004727029792,,,,是,1500.0,1500.0,张宏伟,关磊,华北,韩皞,关磊,东北,0.5,0.5,杨挺,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
59,693f6a41b051e5517a47eed5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:54:09,2025-12-25 15:32:02,,1,跨区新签,,,1765358368741,3,至尊版,11997,至尊版-门店管理系统3年,3999.0,CHS202512100314217,鞍山尊荣万顺汽车销售有限公司,2025-12-10 00:00:00,2025-12-10 00:00:00,2025-12-10 00:00:00,新签,鞍山尊荣万顺汽车销售有限公司,16020612536361586766,,,,是,5998.5,5998.5,宋小涛,陈庆伟,东北,刘磊,陈庆伟,华北,0.5,0.5,孙旭亮,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
60,693f6a522b2bc18aef5bec96,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:54:26,2025-12-25 15:32:02,,1,多年补差价,,,1765259877744,2,进阶版,2500,续约进阶版2年,1250.0,CHS202509170309603,平湖市万里汽车大修厂,2025-12-09 00:00:00,2025-09-17 00:00:00,2025-12-09 00:00:00,新签,平湖市万里汽车大修厂,15990048390801023028,,,,是,2500.0,2500.0,王有军,陈庆伟,浙皖,王有军,陈庆伟,浙皖,1.0,1.0,魏子淇,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
61,6948aa20b9b289e0b6b0a8d1,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-22 10:17:04,2025-12-25 15:32:02,,1,新签超3年,,,1766292766298,2,入门版,800,续约入门版2年,400.0,CHS202512210314689,博晏爱玩车维修中心,2025-12-21 00:00:00,2025-12-21 00:00:00,2025-12-21 00:00:00,新签,义县稍户营子镇博晏爱玩车汽车服务维修中心,16024514498417168447,,,,是,800.0,800.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
62,6948abbc111dbba0cb0d4fd9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-22 10:23:56,2025-12-25 15:32:02,,1,多年补差价,,,1766125609196,2,至尊版,8999,续约至尊版-门店管理系统2年,4499.5,CHS202506260302952,烟台福泰汽车服务有限公司,2025-12-19 00:00:00,2025-06-26 00:00:00,2025-12-19 00:00:00,新签,福泰汽车服务有限公司,11240984669918394572,,,,是,8999.0,8999.0,宗川涵,关磊,山东,宗川涵,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
63,6949f9571fc4fc20b158497f,"{'name': '陈庆伟', 'username': '025366033037741985', 'status': 1, 'type': 0, 'departments': [122311528, 122229573], 'integrate_id': '025366033037741985'}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-23 10:07:19,2025-12-30 17:35:22,,1,跨区新签,,该单是刘磊跨区浙江的客户,王蔚东培训的,关磊已经让小六把业绩和提成都给王蔚东了,100%算王蔚东的,1765263063272,1,基础版,1199,基础版-门店管理系统,1199.0,CHS202512090314165,舟山市于瑞汽车修理有限公司,2025-12-09 00:00:00,2025-12-09 00:00:00,2025-12-09 00:00:00,新签,舟山市于瑞汽车修理有限公司,16020236055869423684,1199,,2025-12-01 00:00:00,否,599.5,599.5,刘磊,关磊,华北,刘磊,陈庆伟,华北,0.5,0.5,魏子淇,拆单,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
64,694a09358daf16de8df90b12,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-23 11:15:01,2025-12-25 15:32:02,,1,新签超3年,,,1766389436606,1,旗舰版,2125,续约旗舰版-门店管理系统,2125.0,CHS202512220314748,PM汽车服务,2025-12-22 00:00:00,2025-12-22 00:00:00,2025-12-22 00:00:00,新签,PM汽车服务,16024929481844097081,2125,,2025-12-01 00:00:00,是,2125.0,2125.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
65,694b80373f7c91e02f438916,"{'name': '张凯', 'username': '1525590028775887', 'status': 1, 'type': 0, 'departments': [122283546, 127595486, 122514491], 'integrate_id': '1525590028775887'}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-24 13:55:03,2025-12-25 15:32:02,,1,多年补差价,,新签一个月补1599两年,共计3198三年基础版,1766474568560,2,基础版,1599,续约基础版-门店管理系统2年,799.5,CHS202512070314056,贺州市德宝汽车养护有限公司,2025-12-23 00:00:00,2025-12-07 00:00:00,2025-12-23 00:00:00,新签,贺州市德宝汽车养护有限公司,16019465729355059235,1599,黄宗祥,2025-12-01 00:00:00,是,1599.0,1599.0,黄宗祥,张凯,华南沪,黄宗祥,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
66,694cfd1d990c750feb7392f8,"{'name': '景东强', 'username': '232229053125844557', 'status': 1, 'type': 0, 'departments': [122472424, 122503482], 'integrate_id': '232229053125844557'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-25 17:00:13,2025-12-26 09:45:45,,1,多年补差价,,此客户约定进阶版3年3899元;11月8号付款一年进阶版1999元,12月25日补尾款1900元。胡冰区域,1766652296888,2,进阶版,1900,续约进阶版2年,950.0,CHS202511080312811,宜春晴天汽车服务有限公司,2025-12-25 00:00:00,2025-11-08 00:00:00,2025-12-25 00:00:00,新签,宜春晴天汽车服务有限公司,16009000270293930057,1900,胡冰,2025-12-01 00:00:00,是,1900.0,1900.0,胡冰,景东强,华中,胡冰,景东强,华中,1.0,1.0,金华斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
67,69536f0a92ee436c9af5e238,"{'name': '肖军', 'username': '311003461041349', 'status': 1, 'type': 0, 'departments': [122314630, 122323520], 'integrate_id': '311003461041349'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 14:19:54,2025-12-31 10:05:48,,1,多年补差价,,,1767075482440,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202511170313151,成都鑫城南汽车,2025-12-30 00:00:00,2025-11-17 00:00:00,2025-12-30 00:00:00,新签,成都鑫城南汽车,16012170912208031813,1101,陈致欣,2025-12-01 00:00:00,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
68,6953889d92e8e1b524429a46,"{'name': '景东强', 'username': '232229053125844557', 'status': 1, 'type': 0, 'departments': [122472424, 122503482], 'integrate_id': '232229053125844557'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 16:09:01,2025-12-30 16:15:23,,1,跨区新签,,此客户在孙婷婷区域,但由于客户单店体量较大,于是让陈煜介入洽谈,故业绩陈煜一半9000元。孙婷婷一半9000元。,1765503409986,3,至尊版,17997,至尊版-门店管理系统3年,5999.0,CHS202512120314279,武汉酷卡驰汽车科技有限公司,2025-12-12 00:00:00,2025-12-12 00:00:00,2025-12-12 00:00:00,新签,武汉酷卡驰汽车科技有限公司,16021219284734738438,18000,陈煜,2025-12-01 00:00:00,是,8998.5,8998.5,孙婷婷,景东强,华中,陈煜,景东强,华中,0.5,0.5,刘光春,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
69,6953bd5a65e053d760df4b80,"{'name': '陈庆伟', 'username': '025366033037741985', 'status': 1, 'type': 0, 'departments': [122311528, 122229573], 'integrate_id': '025366033037741985'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 19:54:02,2025-12-31 09:04:54,,1,多年补差价,,新签3个月内补差价,多年购,1766986879209001,2,进阶版,1600,续约进阶版2年,800.0,CHS202510120310664,鹤岗市南山区鑫宏海中外汽车维修站,2025-12-29 00:00:00,2025-10-12 00:00:00,2025-12-29 00:00:00,新签,鹤岗市南山区鑫宏海中外汽车维修站,15999259585890246734,1600,韩皞,2025-12-01 00:00:00,是,1600.0,1600.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,刘立,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
@@ -0,0 +1,326 @@
# -*- coding: utf-8 -*-
import pandas as pd
import datetime
from config import Config
from api import API
import pymysql # 使用 pymysql 替代 mysql.connector
from back_ground_module import CommonModule
import os
import mysql.connector
import pandas as pd
import json
import numpy as np
import mysql.connector
from mysql.connector import Error
from log_config import configure_task_logger, configure_error_task_logger
logger = configure_task_logger()
error_task_logger = configure_error_task_logger()
api_instance = API()
common_module = CommonModule()
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
class NonStandardPerformanceToBI:
""" 非标业绩提报转BI"""
def __init__(self):
self.dealer_service_data = None
self.field_mapping = {
"报备类型": "_widget_1753770875899",
"协作内容": "_widget_1753770875915",
"情况说明": "_widget_1753770875944",
"订单编号": "_widget_1753770875887",
"实付金额": "_widget_1753770875889",
"门店编码": "_widget_1753770875890",
"门店名称": "_widget_1753770875888",
"版本": "_widget_1753770875891",
"年限": "_widget_1753948745953",
"支付日期": "_widget_1753770875893",
"开户/处理日期": "_widget_1753770875894",
"小六业绩金额": "_widget_1753770875898",
"区域业绩金额": "_widget_1753770875937",
"报备业绩归属区域经理": "_widget_1753770875903",
"报备业绩归属大区": "_widget_1753866196486",
"原业绩归属人": "_widget_1753856032683",
"原业绩归属区域经理": "_widget_1753866196485",
"小六业绩比例": "_widget_1753770875917",
"区域业绩比例": "_widget_1753770875921",
"运营专家": "_widget_1753770875902",
"提成类型": "_widget_1753778922504",
"SaaS新签提成比例": "_widget_1753770875949",
"服务包提成比例": "_widget_1753778922567",
"提成金额": "_widget_1753770875948",
"新签提成比例-首年": "_widget_1753778922503",
"新签提成比例-非首年": "_widget_1753778922548",
"新签阶段及提成比例": "_widget_1753778656359",
"业绩动作":"_widget_1756708722933",
"提成动作":"_widget_1756708722932",
"新签阶段及提成比例.选择提成阶段": "_widget_1753778656359._widget_1753778656361",
"新签阶段及提成比例.新签阶段": "_widget_1753778656359._widget_1753948745962",
"新签阶段及提成比例.提成比例": "_widget_1753778656359._widget_1753778656362",
"业绩类型":"_widget_1753770875966",
"报备业绩归属小六":"_widget_1753770875901",
"原业绩归属大区":"_widget_1755159216098",
"业绩分类":"_widget_1758706882564",
"流程是否结束":"_widget_1761633418013",
"业绩类型-聚合":"_widget_1758706882564",
"业绩分组":"_widget_1762417447169",
"商品名称":"_widget_1762219744898",
"履约金额":"_widget_1762220516367",
"业绩归属日期":"_widget_1762417447127",
"公司名称":"_widget_1762420723743",
"公司ID":"_widget_1762420723744",
"报备业绩金额-区域提交":"_widget_1766375035236",
"业绩归属小六-区域提交":"_widget_1766461143813",
"业绩归属月":"_widget_1766375035265",
"是否同步衡石":"_widget_1766484337844",
"提交人": "creator",
"提交时间": "createTime",
"更新时间": "updateTime"
}
# 定义需要特殊处理的列表字段及其内部字段映射
self.list_fields_config = {
"新签阶段及提成比例": {
"_widget_1753778656361": "选择提成阶段",
"_widget_1753948745962": "新签阶段",
"_widget_1753778656362": "提成比例"
},
# 可以在这里添加其他列表字段的配置
# "另一个列表字段": {
# "原始字段名1": "映射后字段名1",
# "原始字段名2": "映射后字段名2"
# }
}
def load_all_data(self):
# 获取非标业绩提报数据
payload = {"api_key": "66b9678280b37f8a276b1d01",
"entry_id": "68886b7c0382a7249ae0b5d6",
}
dealer_service = api_instance.entry_data_list(payload)
self.dealer_service_data = dealer_service.get("data") # api请求格式,将数据封装在data字典里
def process_list_field(self, field_value, field_config):
"""通用方法:处理列表类型的字段"""
if not isinstance(field_value, (list, np.ndarray)):
return field_value
processed_list = []
for item in field_value:
if not isinstance(item, dict):
processed_list.append(item)
continue
processed_item = {}
for original_key, mapped_key in field_config.items():
if original_key in item:
# 处理包含id的字典字段
if isinstance(item[original_key], dict) and "id" in item[original_key]:
processed_item[mapped_key] = item[original_key]["id"]
else:
processed_item[mapped_key] = item[original_key]
else:
processed_item[mapped_key] = None
processed_list.append(processed_item)
return processed_list
def data_process(self):
df = pd.DataFrame(self.dealer_service_data)
# 反转映射字典
reverse_mapping = {v: k for k, v in self.field_mapping.items()}
# 1.列明替换
df.columns = [reverse_mapping.get(col, col) for col in df.columns]
# 只保留流程是否结束为是的内容
df = df[df["流程是否结束"] == ""]
# 2.成员字段取值
user_columns = ["报备业绩归属小六", "报备业绩归属区域经理", "原业绩归属人", "原业绩归属区域经理", "运营专家","业绩归属小六-区域提交"]
for col in user_columns:
df[col] = df[col].map(lambda x: x.get("name", "") if isinstance(x, dict) else "")
# 3.日期字段转为北京时间
time_columns = ["支付日期", "开户/处理日期", "提交时间", "更新时间", "业绩归属月", "业绩归属日期"]
for col in time_columns:
# 1. 解析为 datetime,并明确指定为 UTC(即使原始字符串无时区)
dt_utc = pd.to_datetime(df[col], errors='coerce', utc=True)
# 2. 转换为北京时间
dt_beijing = dt_utc.dt.tz_convert('Asia/Shanghai')
# 3. 去掉时区信息(变成 naive datetime),然后格式化为字符串
df[col] = dt_beijing.dt.tz_localize(None).dt.strftime('%Y-%m-%d %H:%M:%S')
# 4.业绩动作等于拆单做复制
# 4.1. 定义条件
mask = df['业绩动作'] == '拆单'
# 4.2. 复制满足条件的行
new_rows = df[mask].copy() # ⚠️ 一定要用 .copy() 避免 SettingWithCopyWarning
# 3. 修改新行中的某些列
new_rows['小六业绩金额'] = -new_rows['小六业绩金额']
new_rows['区域业绩金额'] = -new_rows['区域业绩金额']
new_rows['报备业绩归属小六'] = new_rows['原业绩归属人']
new_rows['报备业绩归属区域经理'] = new_rows['原业绩归属区域经理']
new_rows['报备业绩归属大区'] = new_rows['原业绩归属大区']
# 4. 合并回原 DataFrame
df = pd.concat([df, new_rows], ignore_index=True)
# 5.处理所有配置的列表字段
if "新签阶段及提成比例" in df.columns:
# 先处理订单登记表字段
df["新签阶段及提成比例"] = df["新签阶段及提成比例"].apply(
lambda x: self.process_list_field(x, self.list_fields_config["新签阶段及提成比例"])
if x is not None and (isinstance(x, (list, dict, np.ndarray)) or not pd.isna(x))
else None
)
# 拆分行
df_exploded = df.explode("新签阶段及提成比例")
# 将订单登记表中的字段提取到主表中
order_fields = self.list_fields_config["新签阶段及提成比例"].values()
for field in order_fields:
df_exploded[field] = df_exploded["新签阶段及提成比例"].apply(
lambda x: x.get(field) if isinstance(x, dict) else None
)
# 删除原始的订单登记表列
df_exploded = df_exploded.drop(columns=["新签阶段及提成比例"])
# 重置索引
df = df_exploded.reset_index(drop=True)
return df
def write_to_bi(self, df):
# 数据库连接信息
HS_DB_Config = Config.HS_DB_Config
table_name = "non_standard_performance_to_BI" # 替换为你的实际表名
# 建立数据库连接
connection = mysql.connector.connect(
host=HS_DB_Config["host"],
user=HS_DB_Config["user"],
password=HS_DB_Config["password"],
database=HS_DB_Config["database"]
)
cursor = connection.cursor()
try:
# 查询表列名
cursor.execute(f"SHOW COLUMNS FROM {table_name}")
columns_info = cursor.fetchall()
db_columns = [col[0] for col in columns_info] # 提取列名
df = df.replace([None, np.nan, pd.NA, 'nan', 'NaN', 'NAN', ''], None)
# 保留 DataFrame 中与数据库列名匹配的列
filtered_df = df[df.columns.intersection(db_columns)]
# 如果没有匹配的列,直接返回
if filtered_df.empty:
print("DataFrame 中没有与数据库表结构匹配的列。")
return
# 筛选列之后,插入前处理 dict 类型
filtered_df = filtered_df.copy()
for col in filtered_df.columns:
if filtered_df[col].apply(lambda x: isinstance(x, (dict, list)) if x is not None else False).any():
filtered_df.loc[:, col] = filtered_df[col].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else x
)
# 构建插入语句
placeholders = ', '.join(['%s'] * len(filtered_df.columns))
# 使用反引号避免特殊列明
columns = ', '.join([f"`{col}`" for col in filtered_df.columns])
insert_sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
# 将 DataFrame 写入数据库
for _, row in filtered_df.iterrows():
cursor.execute(insert_sql, tuple(row))
connection.commit()
logger.info(f"成功写入 {len(filtered_df)} 条记录到 {table_name} 表中。")
except Exception as e:
error_task_logger.error(f"写入数据库时发生错误: {e}")
connection.rollback()
finally:
cursor.close()
connection.close()
def clear_table_data(self):
"""
清空指定 MySQL 表的数据
参数已写死在函数内部直接调用即可
"""
# 数据库连接信息
HS_DB_Config = {
'host': "f6-public.rwlb.rds.aliyuncs.com",
'user': "rw_operation_data_relay",
'password': "m+q5Z4%IVuF9bf",
'database': "f6operation_data_relay"
}
table_name = "non_standard_performance_to_BI" # 要清空的表名
connection = None
try:
# 建立数据库连接
connection = mysql.connector.connect(
host=HS_DB_Config["host"],
user=HS_DB_Config["user"],
password=HS_DB_Config["password"],
database=HS_DB_Config["database"]
)
if connection.is_connected():
cursor = connection.cursor()
# 使用TRUNCATE清空表数据
cursor.execute(f"TRUNCATE TABLE {table_name}")
connection.commit()
logger.info(f"成功清空表 {table_name} 中的所有数据")
except Error as e:
error_task_logger.error(f"清空表时发生错误: {e}")
if connection and connection.is_connected():
connection.rollback()
finally:
if connection and connection.is_connected():
cursor.close()
connection.close()
logger.info("数据库连接已关闭")
def main(self):
task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
logger.info("任务开始")
# step1: 获取数据
self.load_all_data()
logger.info("加载数据完成")
# step2:数据处理
df = self.data_process()
# df.to_csv(os.path.join(output_dir, "new_dealer_service_order_to_bi.csv"))
logger.info("数据处理完成")
# step3:数据库删除
self.clear_table_data()
logger.info("目标数据库已清空")
# step4:数据写入BI
self.write_to_bi(df)
logger.info("数据已写入数据库中")
common_module.send_task_status(task_start_time, "非标业绩提报转BI")
except Exception as e:
error_task_logger.error(f"非标业绩提报转BI发生错误{e}")
common_module.send_task_error(task_start_time,"非标业绩提报转BI", str(e))
if __name__ == '__main__':
start = NonStandardPerformanceToBI()
start.main()
+3 -3
View File
@@ -421,8 +421,8 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-05T06:03:19.996979900Z",
"start_time": "2026-03-05T06:03:19.335172700Z"
"end_time": "2026-04-03T09:13:22.881255Z",
"start_time": "2026-04-03T09:13:20.171270200Z"
}
},
"cell_type": "code",
@@ -454,7 +454,7 @@
"\n",
" # 使用DELETE删除ID大于等于127821的数据\n",
" # cursor.execute(f\"DELETE FROM {table_name} WHERE id >= {min_id_to_delete}\")\n",
" cursor.execute(f\"DELETE FROM GP_annual_renewal_rate_new WHERE 月分区(仅用于存储每月最后一天截至数据) = '202602';\")\n",
" cursor.execute(f\"DELETE FROM GP_monthly_renewal_rate_new WHERE 月分区(仅用于存储每月最后一天截至数据) = '202603';\")\n",
"\n",
" connection.commit()\n",
"\n",
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
+33
View File
@@ -1579,6 +1579,39 @@
}
],
"execution_count": 1
},
{
"metadata": {},
"cell_type": "markdown",
"source": "## 商机问题跟进表",
"id": "9e39df3c1ef0ccbc"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-31T06:24:52.996465800Z",
"start_time": "2026-03-31T06:21:23.287329300Z"
}
},
"cell_type": "code",
"source": [
"import pandas as pd\n",
"from api import API\n",
"\n",
"api_instance = API()\n",
"df = pd.read_excel(r\"C:\\Users\\hp_z66\\Downloads\\商机问题跟进表_20260331114857.xlsx\",sheet_name=\"Sheet8\")\n",
"for index, row in df.iterrows():\n",
" data_id = row[\"data_id\"]\n",
" payload = {\n",
" \"api_key\": \"675b900991ad2491c69389ca\",\n",
" \"entry_id\": \"67f8b1d3307bad317abc3a9a\",\n",
" \"data_id\": data_id,\n",
" }\n",
" api_instance.entry_data_delete(payload)"
],
"id": "9982ace96792b53c",
"outputs": [],
"execution_count": 1
}
],
"metadata": {
+4 -2
View File
@@ -97,7 +97,6 @@ class YDAPI:
break
try:
res = requests.get(api, headers=headers, params=formData)
return res.json()
except (requests.exceptions.RequestException, Exception) as e:
print(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...")
@@ -155,7 +154,7 @@ class YDAPI:
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", instanceStatus="RUNNING",
max_retries=10, delay=2, createFromTimeGMT=None, createToTimeGMT=None,
modifiedFromTimeGMT=None,
modifiedToTimeGMT=None, searchFieldJson={},useAlias=False):
modifiedToTimeGMT=None, searchFieldJson={},useAlias=False, instanceIdList=None):
"""
函数功能读取流程表单的所有数据并加入重试机制
@@ -169,6 +168,7 @@ class YDAPI:
instanceStatus (str): 流程实例状态默认为"RUNNING"
max_retries (int): 最大重试次数默认为10次
delay (int): 每次重试之间的延迟秒数默认为2秒
instanceIdList (list): 实例ID列表用于精确查询
Returns:
dict: 返回从API获取的流程表单实例数据的JSON解析结果
@@ -199,6 +199,8 @@ class YDAPI:
),
"useAlias": useAlias,
}
if instanceIdList:
formData["instanceIdList"] = instanceIdList
# print(formData)
while True: