Compare commits
5 Commits
69390fd080
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f9971b7d2 | |||
| 976753d3c0 | |||
| 8e57195033 | |||
| 25225ce136 | |||
| ab0813c5ec |
+10
-1
@@ -6,4 +6,13 @@
|
||||
*.iml
|
||||
out
|
||||
gen
|
||||
.logs
|
||||
.logs
|
||||
.log
|
||||
.csv
|
||||
.excel
|
||||
.xlsx
|
||||
output/
|
||||
__pycache__/
|
||||
.env
|
||||
.vscode/
|
||||
|
||||
|
||||
@@ -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.
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
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
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+2648
-8370
File diff suppressed because it is too large
Load Diff
+222
-40
@@ -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
@@ -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
@@ -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 = ['全国KA(FMVP)', '区域KA(MVP)', '重要客户(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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 宜搭表单里的门店编码,不一致就打日志提醒
|
||||
# 宜搭门店编码字段ID:textField_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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 -*-
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user