Compare commits

..

15 Commits

Author SHA1 Message Date
panda 0f9971b7d2 trae-git 2026-04-09 09:53:47 +08:00
panda 976753d3c0 异常待办时间更改 2026-04-02 09:09:28 +08:00
panda 8e57195033 应续约日与过期日对调
更新续约代表数据一致性
2026-03-31 10:41:17 +08:00
panda 25225ce136 续约代办历史记录迁移
展会线索登记
2026-03-25 09:34:48 +08:00
panda ab0813c5ec 续约代办历史记录迁移 2026-03-09 09:24:10 +08:00
panda 69390fd080 异常回访、续约回访增加默认值
新签、续约回访增加权限唯一值不存在报错
2026-03-06 17:50:17 +08:00
panda be3af8cf51 增加注释 2026-02-28 10:58:18 +08:00
panda 95ae2c864a api重试间隔由0.1改为0.5
修改任务结束后向服务器发送的日期格式
2026-02-25 09:43:01 +08:00
panda 6a002240cf requirements 2026-01-21 09:18:53 +08:00
panda 50b4a92f96 续约待办历史记录迁移 2026-01-21 09:17:09 +08:00
panda d28d4c5c97 修复换源导致的续约日常回访异常 2026-01-16 17:40:17 +08:00
panda 25795f4a2d NGV换源 2026-01-14 15:13:44 +08:00
panda 1ef81def0f 修复因无新增客户导致NGV数据新增异常 2026-01-12 16:27:15 +08:00
panda 923c035fd5 分子分母归属月份字段更新 2026-01-06 16:24:35 +08:00
panda cf3814b3c2 校验唯一任务添加时间
优化续约代办请求次数
2026-01-04 13:53:39 +08:00
71 changed files with 70836 additions and 12234 deletions
+10 -1
View File
@@ -6,4 +6,13 @@
*.iml
out
gen
.logs
.logs
.log
.csv
.excel
.xlsx
output/
__pycache__/
.env
.vscode/
+58 -10
View File
@@ -99,7 +99,9 @@ class API:
"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会抛出异常
@@ -115,11 +117,11 @@ class API:
break
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
time.sleep(0.5) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
logger.warning(f"请求异常: {e}, 将重新请求,{data_get}")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
time.sleep(0.5) # 在重试之间稍作停顿
if retries > max_retries:
error_task_logger.error(f"任务 {last_data_id}组 连续{max_retries}次请求失败,放弃此次请求。")
all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理
@@ -297,7 +299,7 @@ class API:
retries = 0
while retries <= max_retries:
try:
res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10)
res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=15)
# print(res.json())
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
@@ -311,7 +313,7 @@ class API:
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
time.sleep(0.5) # 在重试之间稍作停顿
if retries > max_retries:
error_task_logger.error(
f"任务 {data['data_list'][start_index:end_index]} 连续{max_retries}次请求失败,放弃此次请求。")
@@ -338,9 +340,12 @@ 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": data.get('is_start_trigger', True),
}
)
# print(payload)
data_get = None
retries = 0
@@ -349,7 +354,6 @@ class API:
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:
@@ -535,7 +539,7 @@ class API:
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
time.sleep(0.5) # 在重试之间稍作停顿
if retries > max_retries:
error_task_logger.error(
f"任务 {data['data_list'][start_index:end_index]} 连续{max_retries}次请求失败,放弃此次请求。")
@@ -581,7 +585,7 @@ class API:
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
time.sleep(0.5) # 在重试之间稍作停顿
if retries > max_retries:
error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。")
@@ -624,7 +628,51 @@ class API:
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
time.sleep(0.5) # 在重试之间稍作停顿
if retries > max_retries:
error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。")
return data_get
@staticmethod
def workflow_instance_start(data: dict, max_retries: int = 20) -> dict:
"""
激活流程
:param max_retries:
:param data: 简道云插件发送过来的data,包含应用id
:return: 查询简道云流程实例信息返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v1/workflow/instance/activate'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey
'Content-Type': 'application/json'
}
payload = json.dumps({
"instance_id": data['data_id'],
"flow_id": data['flow_id'], # 节点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:
logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.5) # 在重试之间稍作停顿
if retries > max_retries:
error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。")
+97 -33
View File
@@ -19,6 +19,7 @@ error_task_logger = configure_error_task_logger()
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
class NewExceptionTask:
"""
SaaS异常回访
@@ -175,7 +176,9 @@ class NewExceptionTask:
# 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] }]}}
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)
@@ -225,7 +228,7 @@ class NewExceptionTask:
task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
all_data =[]
all_data = []
try:
self.load_all_data()
@@ -240,6 +243,7 @@ class NewExceptionTask:
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):
@@ -248,7 +252,7 @@ class NewExceptionTask:
# 对整个DataFrame的所有列应用替换函数
data_yichang = data_yichang.apply(replace_values)
error_data= []
error_data = []
for index_num, row in data_yichang.iterrows(): # 对过滤后的每一条进行派发
try:
# 每次循环前清空省市区变量
@@ -257,7 +261,7 @@ class NewExceptionTask:
area_name = None
is_pass = False
for exception_service in self.exception_service_todo :
for exception_service in self.exception_service_todo:
# 通过查询筛选进行中的逻辑
if exception_service['_widget_1748241895842'] == row['org_code']:
is_pass = True
@@ -297,35 +301,36 @@ class NewExceptionTask:
NGV_data_id = None
reason = None
create_exception =None
create_date = 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']):
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}")
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}")
@@ -333,8 +338,56 @@ class NewExceptionTask:
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
@@ -343,29 +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
# 定义可能的日期格式(灵活应对不同格式)
date_formats = [
"%Y-%m-%d %H:%M:%S", # 含时间
"%Y-%m-%d", # 仅日期
"%Y/%m/%d",
"%Y/%m/%d %H:%M:%S"
]
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 # 解析失败,跳过
# 使用解析后的日期进行判断
@@ -380,15 +441,14 @@ class NewExceptionTask:
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']):
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}")
@@ -457,6 +517,8 @@ class NewExceptionTask:
"_widget_1748512176655": {"value": "未处理"}, # 跟进状态
"_widget_1772761760440":{"value": "客服跟进节点"}, # 当前跟进节点
})
routine_follow_up_payload = {
@@ -478,7 +540,9 @@ class NewExceptionTask:
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)
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, "异常服务待办派发")
+81 -36
View File
@@ -81,50 +81,77 @@ class CommonModule:
def get_ngv_details(self, days_back=1):
"""
从固定的数据库中获取前几天的NGV明细。
参数 `days_back` 表示相对于今天的天数偏移量,默认为1前一天)
返回包含NGV明细的pandas DataFrame
重构后适配MySQL的NGV明细获取方法(仅处理saas_create_time字段,全字段保留文本类型)
参数 `days_back`相对于今天的天数偏移量,默认为1(前一天)
返回pandas DataFrame(所有字段为文本类型,仅saas_create_time做日期格式化),失败返回None
"""
conn = None
cursor = None
try:
# 获得连接
conn = psycopg2.connect(**self.conn)
# 1. 建立MySQL连接(仅适配MySQL,参数与原逻辑对齐)
conn = pymysql.connect(
host=Config.BI_CONN_host,
database=Config.BI_CONN_INFO_database,
user=Config.BI_CONN_INFO_user,
password=Config.BI_CONN_INFO_password,
charset='utf8mb4', # MySQL中文兼容
cursorclass=pymysql.cursors.DictCursor # 保持字典游标,字段名映射一致
)
cursor = conn.cursor()
# 获取指定天数前的日期
# 2. 日期计算逻辑(完全复用原始逻辑)
now_time = datetime.now()
target_time = now_time + timedelta(days=-days_back)
target_date_id = int(target_time.strftime('%Y%m%d')) # 获取目标日期
target_time = now_time - timedelta(days=days_back)
target_date_id = int(target_time.strftime('%Y%m%d'))
# sql语句查询
sql = f"""
SELECT * FROM "public"."holo_ads_report_saas_profile_ngv_detail_d" WHERE "date_id" = '{target_date_id}' ;
"""
# 执行语句并获取结果集
cursor.execute(sql)
# 3. MySQL兼容的SQL(仅替换语法,逻辑不变)
sql = """
SELECT *
FROM `jdy_ngv_data_source`
WHERE `date_id` = %s;
"""
cursor.execute(sql, (target_date_id,))
rows = cursor.fetchall()
all_fields = cursor.description
# 执行结果转化为dataframe
col = [i[0] for i in all_fields]
data_NGV = pd.DataFrame(rows, columns=col)
# 4. 数据转换:强制全字段为文本类型(匹配原始数据源特性)
if rows:
# 核心:所有字段转字符串,空值统一为'',避免后续处理异常
data_NGV = pd.DataFrame(rows).fillna('').replace('None', '')
else:
data_NGV = pd.DataFrame()
# 尝试自动解析日期时间字符串
# 5. 仅处理saas_create_time字段(完全复用原始转换逻辑)
time_format = "%Y-%m-%d %H:%M:%S"
if 'saas_create_time' in data_NGV.columns:
data_NGV['saas_create_time'] = pd.to_datetime(data_NGV['saas_create_time'], format=time_format,
errors='coerce')
data_NGV['saas_create_time'] = data_NGV['saas_create_time'].dt.strftime('%Y-%m-%d')
# 步骤1:解析为datetime(消除格式警告)
temp_dt = pd.to_datetime(
data_NGV['saas_create_time'],
format=time_format, # 指定格式,消除UserWarning
errors='coerce' # 解析失败设为NaT
)
# 步骤2:转换为YYYY-MM-DD格式的字符串,覆盖原始列(与原逻辑一致)
data_NGV['saas_create_time'] = temp_dt.dt.strftime('%Y-%m-%d').fillna('')
# 关闭游标和连接
cursor.close()
conn.close()
# 6. 其他时间字段完全保留原始文本格式(不做任何处理)
# date_fmt/expiry_time等字段仅保留从数据库读取的原始字符串)
return data_NGV
except Exception as e:
error_task_logger.error(f"获取NGV明细失败: {e}")
error_task_logger.error(f"获取NGV明细失败(MySQL适配): {str(e)}", exc_info=True)
return None
finally:
# 确保MySQL连接/游标关闭(资源释放)
if cursor:
try:
cursor.close()
except Exception as e:
error_task_logger.warning(f"关闭MySQL游标失败: {str(e)}")
if conn:
try:
conn.close()
except Exception as e:
error_task_logger.warning(f"关闭MySQL连接失败: {str(e)}")
def get_yichang_details(self, days_back=1):
"""
@@ -180,12 +207,19 @@ class CommonModule:
"""
try:
# 获得连接
conn = psycopg2.connect(**self.conn)
conn = pymysql.connect(
host=Config.BI_CONN_host,
database=Config.BI_CONN_INFO_database,
user=Config.BI_CONN_INFO_user,
password=Config.BI_CONN_INFO_password,
charset='utf8mb4', # MySQL中文兼容
cursorclass=pymysql.cursors.DictCursor # 保持字典游标,字段名映射一致
)
cursor = conn.cursor()
# 获取指定天数前的日期
now_time = datetime.now()
yes_time = now_time + timedelta(days=-2)
yes_time = now_time + timedelta(days=-2) # 防止NGV没更新
yes_time_nyr = int(yes_time.strftime('%Y%m%d')) # 获取前两天日期
# 获取指定天数前的日期
@@ -195,15 +229,26 @@ class CommonModule:
print("距离今天还有{}天的日期是:{}".format(days_to_add, future_date))
sql = f"""SELECT * FROM "public"."holo_ads_report_saas_profile_ngv_detail_d" WHERE "date_id" = '{yes_time_nyr}' and "expiry_time" like '%{future_date}%';"""
sql = """
SELECT *
FROM `jdy_ngv_data_source`
WHERE `date_id` = %s \
AND `expiry_time` LIKE %s; \
"""
# 执行语句并获取结果集
cursor.execute(sql)
like_pattern = f"%{future_date}%"
cursor.execute(sql, (yes_time_nyr, like_pattern))
rows = cursor.fetchall()
all_fields = cursor.description # 获取所有字段名
# 执行结果转化为dataframe
col = [i[0] for i in all_fields]
data_NGV = pd.DataFrame(list(rows), columns=col)
if rows:
# data_NGV = pd.DataFrame(rows).astype(str).replace({'nan': '', 'NaT': ''})
all_fields = cursor.description # 获取所有字段名
# 执行结果转化为dataframe
col = [i[0] for i in all_fields]
data_NGV = pd.DataFrame(list(rows), columns=col).astype(str).replace({'nan': '', 'NaT': ''})
else:
data_NGV = pd.DataFrame()
# 关闭数据库连接
cursor.close()
@@ -701,7 +746,7 @@ class CommonModule:
run_time_sec = int(run_time.total_seconds())
# 5. 格式化时间为 UTC 的 ISO 8601 格式(带 "Z"
today_utc = end_time_utc.strftime("%Y-%m-%d")
today_utc = end_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
task_end_iso = end_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
task_start_iso = task_start_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
+2
View File
@@ -10,6 +10,8 @@ from decimal import Decimal
import time
import numpy as np
import json
import os
os.chdir(Path(__file__).parent)
def replace_decimals(obj):
+29
View File
@@ -141,3 +141,32 @@ Traceback (most recent call last):
import numpy.typing as typing
^^^^^^^^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'numpy.rec'
2026-01-06 16:04:19,673 - log_config.py - error_task_logger - ERROR - 任务执行失败: Can only use .dt accessor with datetimelike values
Traceback (most recent call last):
File "D:\Idea Project\SaaS_V1.7\back_ground_module\update_molecule_reporting_adjustment_to_bi.py", line 207, in main
df['归属月份'] = df['归属月份'].dt.tz_localize(None).dt.strftime('%Y-%m-%d %H:%M:%S')
^^^^^^^^^^^^^^^^^
File "D:\Program Files\anaconda3\envs\SaaS\Lib\site-packages\pandas\core\generic.py", line 6321, in __getattr__
return object.__getattribute__(self, name)
~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
File "D:\Program Files\anaconda3\envs\SaaS\Lib\site-packages\pandas\core\accessor.py", line 224, in __get__
accessor_obj = self._accessor(obj)
File "D:\Program Files\anaconda3\envs\SaaS\Lib\site-packages\pandas\core\indexes\accessors.py", line 643, in __new__
raise AttributeError("Can only use .dt accessor with datetimelike values")
AttributeError: Can only use .dt accessor with datetimelike values. Did you mean: 'at'?
2026-01-06 16:14:24,812 - log_config.py - error_task_logger - ERROR - 写入数据时发生错误: overflow encountered in multiply
Traceback (most recent call last):
File "D:\Idea Project\SaaS_V1.7\back_ground_module\update_denominator_reporting_adjustment_to_bi.py", line 157, in write_to_bi
dt_utc = pd.to_datetime(timestamp_ms, unit='ms', utc=True)
File "D:\Program Files\anaconda3\envs\SaaS\Lib\site-packages\pandas\core\tools\datetimes.py", line 1072, in to_datetime
values = convert_listlike(arg._values, format)
File "D:\Program Files\anaconda3\envs\SaaS\Lib\site-packages\pandas\core\tools\datetimes.py", line 409, in _convert_listlike_datetimes
return _to_datetime_with_unit(arg, unit, name, utc, errors)
File "D:\Program Files\anaconda3\envs\SaaS\Lib\site-packages\pandas\core\tools\datetimes.py", line 517, in _to_datetime_with_unit
arr = cast_from_unit_vectorized(arg, unit=unit)
File "pandas/_libs/tslibs/conversion.pyx", line 149, in pandas._libs.tslibs.conversion.cast_from_unit_vectorized
File "D:\Program Files\anaconda3\envs\SaaS\Lib\site-packages\numpy\_core\fromnumeric.py", line 3674, in round
return _wrapfunc(a, 'round', decimals=decimals, out=out)
File "D:\Program Files\anaconda3\envs\SaaS\Lib\site-packages\numpy\_core\fromnumeric.py", line 54, in _wrapfunc
return bound(*args, **kwds)
FloatingPointError: overflow encountered in multiply
@@ -24,6 +24,7 @@ os.makedirs(output_dir, exist_ok=True)
class NonStandardPerformanceToBI:
""" 非标业绩提报转BI"""
def __init__(self):
self.dealer_service_data = None
self.field_mapping = {
@@ -54,27 +55,27 @@ class NonStandardPerformanceToBI:
"新签提成比例-首年": "_widget_1753778922503",
"新签提成比例-非首年": "_widget_1753778922548",
"新签阶段及提成比例": "_widget_1753778656359",
"业绩动作":"_widget_1756708722933",
"提成动作":"_widget_1756708722932",
"业绩动作": "_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",
"业绩类型": "_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"
@@ -134,10 +135,20 @@ class NonStandardPerformanceToBI:
df.columns = [reverse_mapping.get(col, col) for col in df.columns]
# 只保留流程是否结束为是的内容
df = df[df["流程是否结束"] == ""]
target_col = "流程是否结束"
if target_col in df.columns:
# 只有当列存在时才进行过滤,且 pandas 会自动处理 NaN != "是" 的情况
df = df[df[target_col] == ""]
else:
logger.warning(f"字段 '{target_col}' 不存在,跳过过滤步骤,保留所有数据或根据业务需求处理。")
if df.empty:
logger.info("过滤后数据为空,无需后续处理。")
return df
# 2.成员字段取值
user_columns = ["报备业绩归属小六", "报备业绩归属区域经理", "原业绩归属人", "原业绩归属区域经理", "运营专家","业绩归属小六-区域提交"]
user_columns = ["报备业绩归属小六", "报备业绩归属区域经理", "原业绩归属人", "原业绩归属区域经理", "运营专家",
"业绩归属小六-区域提交"]
for col in user_columns:
df[col] = df[col].map(lambda x: x.get("name", "") if isinstance(x, dict) else "")
@@ -318,7 +329,7 @@ class NonStandardPerformanceToBI:
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))
common_module.send_task_error(task_start_time, "非标业绩提报转BI", str(e))
if __name__ == '__main__':
@@ -23,6 +23,7 @@ api_instance = API()
class ProvinceCityPersonRelationToBI:
'''省市区人员关系表转BI'''
def __init__(self):
self.pvc_data = None
self.field_mapping = {
+3
View File
@@ -351,6 +351,9 @@ class RenewalToDo:
data_NGV['120天是否跟进'] = "主动"
data_NGV['60天是否跟进']= "主动"
data_NGV['30天是否跟进']= "主动"
# 新增当前所处节点默认值
data_NGV['当前所处节点'] = "120天节点"
# 格式化为字符串(去掉时区)
for col in ['过期日', '120天自动流转时间', '60天自动流转时间', '30天自动流转时间', '0天自动流转时间']:
data_NGV[col] = data_NGV[col].dt.strftime('%Y-%m-%d %H:%M:%S')
+18 -13
View File
@@ -622,7 +622,10 @@ class NewServicesRevisit:
continue
if not Billing:
logger.warning(f"权限表请求失败或者权限表无对应关系,权限唯一值是:{NGV_store_level_key}")
logger.warning(f"权限表请求失败或者权限表无对应关系,权限唯一值是:{NGV_store_level_key},跳过该条派发")
error_msg = f"门店编码:{row['org_code']},权限唯一值:{NGV_store_level_key}"
common_module.send_task_error(task_start_time, "新签客户回访-权限表无匹配", error_msg)
continue
if row["active_status_fmt"] == "活跃": # 开单 是否使用
payload_dict.update({"_widget_1735004315765": {"value": ""}})
@@ -635,7 +638,7 @@ class NewServicesRevisit:
payload_dict.update({"_widget_1735106258036": {"value": ""}})
except Exception as e:
error_task_logger.error(f"会员卡拥有识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-会员卡拥有识别", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-会员卡拥有识别", str(e))
try:
if row["card_bill_day_count_last_30_day"] != "0": # 会员卡 是否使用
payload_dict.update({"_widget_1735106258038": {"value": ""}})
@@ -643,7 +646,7 @@ class NewServicesRevisit:
payload_dict.update({"_widget_1735106258038": {"value": ""}})
except Exception as e:
error_task_logger.error(f"会员卡使用识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-会员卡使用识别", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-会员卡使用识别", str(e))
# print(self.service_remind.get("_widget_1735112637045"))
payload_dict["_widget_1735106258018"] = {"value": ""}
@@ -675,11 +678,13 @@ class NewServicesRevisit:
# 近30天业务单量=0 则其它所有模块均不推荐
try:
for feature_module, feature_value in feature_dict.items(): # 模块
if row["bill_count_last_30_day"] == '0' and payload_dict[feature_value]["value"] == '':
if feature_value not in payload_dict:
continue
if row["bill_count_last_30_day"] == '0' and payload_dict[feature_value].get("value") == '':
payload_dict.update({f"{feature_value}": {"value": "×"}})
except Exception as e:
error_task_logger.error(f"不开单识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-不开单识别", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-不开单识别", str(e))
# 保单识别:从系统中抽取目标门店,针对门店抽取修改是否推荐
try:
@@ -687,7 +692,7 @@ class NewServicesRevisit:
payload_dict.update({'_widget_1735004315746': {"value": ""}})
except Exception as e:
error_task_logger.error(f"保单识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-保单识别", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-保单识别", str(e))
# 私域小程序:根据是否开通微信小程序判断是否使用,旗舰版及以上算拥有
try:
for item in self.private_domain:
@@ -700,7 +705,7 @@ class NewServicesRevisit:
break
except Exception as e:
error_task_logger.error(f"小程序识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-小程序识别", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-小程序识别", str(e))
try:
high_version = ['皇冠版', '至尊版', '尊享版', '旗舰版']
if row["saas_edition_fmt"] in high_version:
@@ -709,7 +714,7 @@ class NewServicesRevisit:
payload_dict.update({'_widget_1735106258141': {"value": ""}}) # SYXCX:是否拥有
except Exception as e:
error_task_logger.error(f"私域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-私域小程序", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-私域小程序", str(e))
# 公域小程序:根据是否开通微信小程序判断是否使用,旗舰版及以上算拥有
try:
@@ -723,7 +728,7 @@ class NewServicesRevisit:
break
except Exception as e:
error_task_logger.error(f"公域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-公域小程序", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-公域小程序", str(e))
try:
if row["id_own_org"] in self.public_domain_list:
@@ -732,7 +737,7 @@ class NewServicesRevisit:
payload_dict.update({'_widget_1735106258112': {"value": ""}}) # GYXCX:是否拥有
except Exception as e:
error_task_logger.error(f"公域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-公域小程序", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-公域小程序", str(e))
# 异业合作:根据是否存在判断是否拥有,过滤条件 商品名称包含异业两个字
try:
@@ -742,7 +747,7 @@ class NewServicesRevisit:
payload_dict.update({'_widget_1735107355618': {"value": ""}}) # YYHZ:是否拥有
except Exception as e:
error_task_logger.error(f"异业合作:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-异业合作", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-异业合作", str(e))
# 短信:根据是否启动短信功能判断是否拥有,根据
try:
@@ -756,7 +761,7 @@ class NewServicesRevisit:
break
except Exception as e:
error_task_logger.error(f"短信是否使用:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-短信是否使用", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-短信是否使用", str(e))
try:
for item in self.groupnotification:
@@ -769,7 +774,7 @@ class NewServicesRevisit:
break
except Exception as e:
error_task_logger.error(f"短信是否使用:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-短信是否使用", str(e))
common_module.send_task_error(task_start_time, "新签客户回访-短信是否使用", str(e))
NGV_data_id = None
# 获取关联数据
@@ -326,16 +326,29 @@ class RenewServicesRevisit:
# 将最佳值合并回原数据集
data_NGV = data_NGV.merge(best_values, on='id_own_group', how='left')
condition = (data_NGV['is_main_org'] == 1) & (data_NGV['org_status'] == '过期') # 步骤2: 过滤条件
# 修复:处理 is_main_org 可能是字符串类型的情况
if data_NGV['is_main_org'].dtype == 'object':
is_main_org_numeric = pd.to_numeric(data_NGV['is_main_org'], errors='coerce')
condition = (is_main_org_numeric == 1) & (data_NGV['org_status'] == '过期') # 步骤2: 过滤条件
else:
condition = (data_NGV['is_main_org'] == 1) & (data_NGV['org_status'] == '过期') # 步骤2: 过滤条件
ngvv2 = data_NGV[condition]
# ngvv2.to_excel(r"C:\Users\Administrator.DESKTOP-7IC2USJ\Desktop\NGVV2.xlsx")
data_NGV_V2 = data_NGV.copy() # 步骤3: 检查id_own_group是否存在于ngvv2中
data_NGV_V2['条件'] = ((data_NGV_V2['org_type'] == "一般") & (data_NGV_V2['org_status'] == '留存') &
(data_NGV_V2['area_manager'] != '殷昊') & (
data_NGV_V2['area_manager'] != '孙玉蕾') & (
data_NGV_V2['is_main_org'] != 1))
# 修复:处理 is_main_org 可能是字符串类型的情况
if data_NGV_V2['is_main_org'].dtype == 'object':
is_main_org_numeric_v2 = pd.to_numeric(data_NGV_V2['is_main_org'], errors='coerce')
data_NGV_V2['条件'] = ((data_NGV_V2['org_type'] == "一般") & (data_NGV_V2['org_status'] == '留存') &
(data_NGV_V2['area_manager'] != '殷昊') & (
data_NGV_V2['area_manager'] != '孙玉蕾') & (
is_main_org_numeric_v2 != 1))
else:
data_NGV_V2['条件'] = ((data_NGV_V2['org_type'] == "一般") & (data_NGV_V2['org_status'] == '留存') &
(data_NGV_V2['area_manager'] != '殷昊') & (
data_NGV_V2['area_manager'] != '孙玉蕾') & (
data_NGV_V2['is_main_org'] != 1))
data_NGV_V2 = data_NGV_V2.loc[data_NGV_V2["条件"]]
# 步骤4: 过滤存在的记录
data_NGV_V2['exists_in_ngvv2'] = data_NGV_V2['id_own_group'].isin(ngvv2['id_own_group'])
@@ -350,10 +363,18 @@ class RenewServicesRevisit:
result = filtered_data.drop_duplicates(subset='id_own_group', keep='first')
data_NGV['条件'] = (data_NGV['org_type'] == "一般") & (data_NGV['org_status'] == '留存') & (
data_NGV['area_manager'] != '殷昊') & (
data_NGV['area_manager'] != '孙玉蕾') & (
data_NGV['is_main_org'] == 1)
# 修复:处理 is_main_org 可能是字符串类型的情况
if data_NGV['is_main_org'].dtype == 'object':
is_main_org_numeric_main = pd.to_numeric(data_NGV['is_main_org'], errors='coerce')
data_NGV['条件'] = (data_NGV['org_type'] == "一般") & (data_NGV['org_status'] == '留存') & (
data_NGV['area_manager'] != '殷昊') & (
data_NGV['area_manager'] != '孙玉蕾') & (
is_main_org_numeric_main == 1)
else:
data_NGV['条件'] = (data_NGV['org_type'] == "一般") & (data_NGV['org_status'] == '留存') & (
data_NGV['area_manager'] != '殷昊') & (
data_NGV['area_manager'] != '孙玉蕾') & (
data_NGV['is_main_org'] == 1)
data_NGV = data_NGV.loc[data_NGV["条件"]]
data_NGV = pd.concat([data_NGV, result], axis=0)
@@ -715,7 +736,10 @@ class RenewServicesRevisit:
continue
if not Billing:
logger.warning(f"权限表请求失败或者权限表无对应关系,权限唯一值是:{NGV_store_level_key}")
logger.warning(f"权限表请求失败或者权限表无对应关系,权限唯一值是:{NGV_store_level_key},跳过该条派发")
error_msg = f"门店编码:{row['org_code']},权限唯一值:{NGV_store_level_key}"
common_module.send_task_error(task_start_time, "续约客户回访-权限表无匹配", error_msg)
continue # 无权限匹配时 payload_dict 缺少 _widget_1734073342350 等字段,后续会 KeyError,直接跳过
if row["active_status_fmt"] == "活跃": # 开单 是否使用
payload_dict.update({"_widget_1735004315765": {"value": ""}})
@@ -729,7 +753,7 @@ class RenewServicesRevisit:
payload_dict.update({"_widget_1735106258036": {"value": ""}})
except Exception as e:
error_task_logger.error(f"会员卡拥有识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-会员卡拥有识别", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-会员卡拥有识别", str(e))
try:
if row["card_bill_day_count_last_30_day"] != "0": # 会员卡 是否使用
@@ -738,7 +762,7 @@ class RenewServicesRevisit:
payload_dict.update({"_widget_1735106258038": {"value": ""}})
except Exception as e:
error_task_logger.error(f"会员卡使用识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-会员卡使用识别", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-会员卡使用识别", str(e))
# print(self.service_remind.get("_widget_1735112637045"))
payload_dict["_widget_1735106258018"] = {"value": ""}
@@ -771,11 +795,13 @@ class RenewServicesRevisit:
# 近30天业务单量=0 则其它所有模块均不推荐
try:
for feature_module, feature_value in feature_dict.items(): # 模块
if row["bill_count_last_30_day"] == '0' and payload_dict[feature_value]["value"] == '':
if feature_value not in payload_dict:
continue # 权限未匹配时该 key 不存在,避免 KeyError
if row["bill_count_last_30_day"] == '0' and payload_dict[feature_value].get("value") == '':
payload_dict.update({f"{feature_value}": {"value": "×"}})
except Exception as e:
error_task_logger.error(f"不开单识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-不开单识别", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-不开单识别", str(e))
# 保单识别:从系统中抽取目标门店,针对门店抽取修改是否推荐
try:
@@ -783,7 +809,7 @@ class RenewServicesRevisit:
payload_dict.update({'_widget_1735004315746': {"value": ""}})
except Exception as e:
error_task_logger.error(f"保单识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-保单识别", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-保单识别", str(e))
# 私域小程序:根据是否开通微信小程序判断是否使用,旗舰版及以上算拥有
try:
@@ -797,7 +823,7 @@ class RenewServicesRevisit:
break
except Exception as e:
error_task_logger.error(f"小程序识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-小程序识别", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-小程序识别", str(e))
try:
high_version = ['皇冠版', '至尊版', '尊享版', '旗舰版']
@@ -807,7 +833,7 @@ class RenewServicesRevisit:
payload_dict.update({'_widget_1735106258141': {"value": ""}}) # SYXCX:是否拥有
except Exception as e:
error_task_logger.error(f"私域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-私域小程序", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-私域小程序", str(e))
# 公域小程序:根据是否开通微信小程序判断是否使用,旗舰版及以上算拥有
try:
@@ -821,7 +847,7 @@ class RenewServicesRevisit:
break
except Exception as e:
error_task_logger.error(f"公域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-公域小程序", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-公域小程序", str(e))
try:
if row["id_own_org"] in self.public_domain_list:
@@ -830,7 +856,7 @@ class RenewServicesRevisit:
payload_dict.update({'_widget_1735106258112': {"value": ""}}) # GYXCX:是否拥有
except Exception as e:
error_task_logger.error(f"公域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-公域小程序", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-公域小程序", str(e))
# 异业合作:根据是否存在判断是否拥有,过滤条件 商品名称包含异业两个字
try:
@@ -840,7 +866,7 @@ class RenewServicesRevisit:
payload_dict.update({'_widget_1735107355618': {"value": ""}}) # YYHZ:是否拥有
except Exception as e:
error_task_logger.error(f"异业合作:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-异业合作", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-异业合作", str(e))
# 短信:根据是否启动短信功能判断是否拥有,根据
try:
@@ -854,7 +880,7 @@ class RenewServicesRevisit:
break
except Exception as e:
error_task_logger.error(f"短信是否使用:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-短信是否使用", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-短信是否使用", str(e))
try:
for item in self.groupnotification:
@@ -867,7 +893,7 @@ class RenewServicesRevisit:
break
except Exception as e:
error_task_logger.error(f"短信是否使用:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-短信是否使用", str(e))
common_module.send_task_error(task_start_time, "续约客户回访-短信是否使用", str(e))
NGV_data_id = None
# 获取关联数据
Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

+12 -9
View File
@@ -54,8 +54,8 @@ class UpdateNGVData:
timestamp = time.time()
# data_NGV_j.to_csv(os.path.join(output_dir, f"{timestamp}up_NGV_j.csv"))
# data_NGV_j1.to_csv(os.path.join(output_dir, f"{timestamp}up_NGV_j1.csv"))
data_NGV_j.to_csv(os.path.join(output_dir, f"up_NGV_j.csv"))
data_NGV_j1.to_csv(os.path.join(output_dir, f"up_NGV_j1.csv"))
# 找出在 data_NGV_j 中存在但在 data_NGV_j1 中不存在的 data_id
unique_data_ids = data_NGV_j[~data_NGV_j['org_code'].isin(data_NGV_j1['org_code'])]
@@ -111,9 +111,11 @@ class UpdateNGVData:
logger.info(f"人员转换完成")
# 数字保留3位小数
filtered_df['g_month_percentage'] = (pd.to_numeric(data_NGV_j['g_month_percentage'], errors='coerce')
.round(3)
.apply(lambda x: f"{x:.3f}" if pd.notna(x) else ''))
filtered_df['g_month_percentage'] = (
pd.to_numeric(filtered_df['g_month_percentage'], errors='coerce')
.round(3)
.apply(lambda x: f"{x:.3f}" if pd.notna(x) else '')
)
# filtered_df.to_csv(r"D:\Idea Project\SaaS_V1.3\back_ground_module\output\NGV.csv")
@@ -138,17 +140,18 @@ class UpdateNGVData:
# print(result_str[:500])
# 保存到Excel文件
# output_path = r'D:\Idea Project\F6+宜搭+其它(1)\new\文件输出\ngv明细1.xlsx'
# output_path = r'D:\Idea Project\SaaS_V1.7\back_ground_module\output\ngv明细1.xlsx'
# filtered_df.to_excel(output_path, index=False)
# data_NGV_j1.to_excel( r'D:\Idea Project\F6+宜搭+其它(1)\new\文件输出\ngv明细j1.xlsx', index=False)
# data_NGV_j.to_excel( r'D:\Idea Project\F6+宜搭+其它(1)\new\文件输出\ngv明细j.xlsx', index=False)
# new_df.to_excel(r'D:\Idea Project\F6+宜搭+其它(1)\new\文件输出\ngv明细ndf.xlsx', index=False)
# data_NGV_j1.to_excel( r'D:\Idea Project\SaaS_V1.7\back_ground_module\output\ngv明细j1.xlsx', index=False)
# data_NGV_j.to_excel( r'D:\Idea Project\SaaS_V1.7\back_ground_module\output\ngv明细j.xlsx', index=False)
# new_df.to_excel(r'D:\Idea Project\SaaS_V1.7\back_ground_module\output\ngv明细ndf.xlsx', index=False)
common_module.send_task_status(task_start_time, "NGV新增数据")
logger.info(f"任务完成。")
except Exception as e:
error_task_logger.error(f"任务执行时发生异常: {e}")
common_module.send_task_error(task_start_time, "NGV新增数据", str(e))
# pass
@staticmethod
def row_to_dict(row, field_mapping):
@@ -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字段
@@ -63,7 +63,8 @@ class DenominatorReportingAdjustment:
"总部调整结果": "selectField_lfqwg05y",
"总部核对结果": "selectField_lfqwg05x",
"是否上传衡石": "selectField_mca5shoz",
"是否计入应续约数": "selectField_mdnwwvyo"
"是否计入应续约数": "selectField_mdnwwvyo",
"归属月份": "dateField_mjtprnxl",
}
def get_yida_data(self):
@@ -146,15 +147,31 @@ class DenominatorReportingAdjustment:
# 处理日期字段 - 新增部分
date_fields = ['开户日期', '开始时间', '结束时间']
date_fields = ['开户日期', '开始时间', '结束时间', "归属月份"]
# 处理日期字段 - 安全版本
for field in date_fields:
if field in df.columns:
# 转换为整数类型
df[field] = pd.to_numeric(df[field], errors='coerce').astype('Int64')
# 转换为datetime对象
df[field] = pd.to_datetime(df[field], unit='ms')
# 转换为MySQL兼容的字符串格式
df[field] = df[field].dt.strftime('%Y-%m-%d %H:%M:%S')
# 1. 先确保是数值类型,非数字转为 NaN
numeric_series = pd.to_numeric(df[field], errors='coerce')
# 2. 设置合理的时间戳范围(毫秒)
# 1970-01-01 到 2100-12-31
min_ts = 0
max_ts = 4102444799999 # 2100-12-31 23:59:59.999 UTC 毫秒
# 3. 过滤掉超出范围的值(设为 NaN)
valid_mask = (numeric_series >= min_ts) & (numeric_series <= max_ts)
safe_timestamps = numeric_series.where(valid_mask)
# 4. 转换为 datetime(只对有效值转换)
try:
dt_utc = pd.to_datetime(safe_timestamps, unit='ms', utc=True)
dt_shanghai = dt_utc.dt.tz_convert('Asia/Shanghai')
dt_naive = dt_shanghai.dt.tz_localize(None)
df[field] = dt_naive.dt.strftime('%Y-%m-%d %H:%M:%S')
except Exception as e:
error_task_logger.warning(f"字段 '{field}' 时间转换失败,全部置空: {e}")
df[field] = None
df = df.replace([None, np.nan, pd.NA, 'nan', 'NaN', 'NAN', ''], None)
@@ -321,7 +321,6 @@ class EmailProcessor:
charset='utf8mb4',
)
with connection.cursor() as cursor:
# 处理数据
df = df.where(pd.notna(df), None) # 将NaN转换为None
@@ -193,11 +193,21 @@ class MoleculeReportingAdjustment:
df = pd.DataFrame(self.molecule_data_list)
# df.to_csv('molecule_data.csv', index=False)
if '归属月份' in df.columns:
# 确保是整数类型
df['归属月份'] = df['归属月份'].astype('Int64')
# 转换为datetime对象
df['归属月份'] = pd.to_datetime(df['归属月份'], unit='ms')
# 转换为MySQL兼容的字符串格式
# 1. 先将所有值转为数值,无法转换的变成 NaN
timestamp_ms = pd.to_numeric(df['归属月份'], errors='coerce')
# 2. 只对有效数值(非 NaN)进行 datetime 转换
# unit='ms', origin='unix' (默认), utc=True 表示输入是 UTC 毫秒时间戳
df['归属月份'] = pd.to_datetime(timestamp_ms, unit='ms', utc=True)
# 3. 转换时区到 UTC+8Asia/Shanghai
df['归属月份'] = df['归属月份'].dt.tz_convert('Asia/Shanghai')
# 4. 移除时区信息(因为 MySQL DATETIME 不支持时区)
df['归属月份'] = df['归属月份'].dt.tz_localize(None)
# 5. 格式化为字符串(可选:如果你写入的是 DATETIME 字段,其实可以保持 datetime 类型,pymysql 会自动处理)
# 但你当前 write_to_bi 用的是 %s 插入,所以需要字符串
df['归属月份'] = df['归属月份'].dt.strftime('%Y-%m-%d %H:%M:%S')
# step2:清空BI数据表
Binary file not shown.
+147
View File
@@ -2554,3 +2554,150 @@
2026-01-04 10:29:26,171 - log_config.py - error_task_logger - ERROR - 任务 高德匹配手机号 超过执行窗口5分钟以上,标记为过期。
2026-01-04 10:29:26,171 - log_config.py - error_task_logger - ERROR - 任务 省市区人员关系表转BI 超过执行窗口5分钟以上,标记为过期。
2026-01-04 10:29:26,172 - log_config.py - error_task_logger - ERROR - 任务 续约回访待办 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,807 - log_config.py - error_task_logger - ERROR - 任务 NGV新增数据 (09:00) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,808 - log_config.py - error_task_logger - ERROR - 任务 NGV更新数据 (12:30) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,809 - log_config.py - error_task_logger - ERROR - 任务 新签客户回访 (09:05) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,810 - log_config.py - error_task_logger - ERROR - 任务 续约客户回访 (09:08) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,811 - log_config.py - error_task_logger - ERROR - 任务 接车宝日常派发 (09:10) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,811 - log_config.py - error_task_logger - ERROR - 任务 私域小程序数据支撑 (04:40) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,812 - log_config.py - error_task_logger - ERROR - 任务 小六提成数据支撑 (04:40) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,813 - log_config.py - error_task_logger - ERROR - 任务 异业合作数据支撑 (04:20) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,814 - log_config.py - error_task_logger - ERROR - 任务 短信数据支撑 (04:10) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,815 - log_config.py - error_task_logger - ERROR - 任务 海外邮件推送 (08:28) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,815 - log_config.py - error_task_logger - ERROR - 任务 异常服务待办派发 (10:00) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,816 - log_config.py - error_task_logger - ERROR - 任务 简道云海外项目CRM客户档案迁移BI (08:37) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,817 - log_config.py - error_task_logger - ERROR - 任务 分母报备调整 (09:05) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,817 - log_config.py - error_task_logger - ERROR - 任务 分子报备调整 (09:03) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,818 - log_config.py - error_task_logger - ERROR - 任务 履约表数据支撑 (09:10) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,819 - log_config.py - error_task_logger - ERROR - 任务 字段监控 (06:25) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,820 - log_config.py - error_task_logger - ERROR - 任务 经销商新签服务单转BI (08:05) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,820 - log_config.py - error_task_logger - ERROR - 任务 非标业绩提报转BI (08:01) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,821 - log_config.py - error_task_logger - ERROR - 任务 合伙人结算登记同步到BI (08:02) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,821 - log_config.py - error_task_logger - ERROR - 任务 高德匹配手机号 (05:00) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,822 - log_config.py - error_task_logger - ERROR - 任务 非标业绩提报转BI (12:01) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,823 - log_config.py - error_task_logger - ERROR - 任务 合伙人结算登记同步到BI (12:02) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,824 - log_config.py - error_task_logger - ERROR - 任务 省市区人员关系表转BI (08:00) 超过执行窗口5分钟以上,标记为过期。
2026-01-04 13:45:42,824 - log_config.py - error_task_logger - ERROR - 任务 续约回访待办 (09:35) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,451 - log_config.py - error_task_logger - ERROR - 任务 NGV新增数据 (09:00) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,452 - log_config.py - error_task_logger - ERROR - 任务 新签客户回访 (09:05) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,453 - log_config.py - error_task_logger - ERROR - 任务 续约客户回访 (09:08) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,454 - log_config.py - error_task_logger - ERROR - 任务 接车宝日常派发 (09:10) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,455 - log_config.py - error_task_logger - ERROR - 任务 私域小程序数据支撑 (04:40) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,456 - log_config.py - error_task_logger - ERROR - 任务 小六提成数据支撑 (04:40) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,457 - log_config.py - error_task_logger - ERROR - 任务 异业合作数据支撑 (04:20) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,459 - log_config.py - error_task_logger - ERROR - 任务 短信数据支撑 (04:10) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,460 - log_config.py - error_task_logger - ERROR - 任务 海外邮件推送 (08:28) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,461 - log_config.py - error_task_logger - ERROR - 任务 异常服务待办派发 (10:00) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,462 - log_config.py - error_task_logger - ERROR - 任务 简道云海外项目CRM客户档案迁移BI (08:37) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,463 - log_config.py - error_task_logger - ERROR - 任务 分母报备调整 (09:05) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,465 - log_config.py - error_task_logger - ERROR - 任务 分子报备调整 (09:03) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,466 - log_config.py - error_task_logger - ERROR - 任务 履约表数据支撑 (09:10) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,467 - log_config.py - error_task_logger - ERROR - 任务 字段监控 (06:25) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,468 - log_config.py - error_task_logger - ERROR - 任务 经销商新签服务单转BI (08:05) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,470 - log_config.py - error_task_logger - ERROR - 任务 非标业绩提报转BI (08:01) 超过执行窗口5分钟以上,标记为过期。
2026-01-06 10:22:45,471 - log_config.py - error_task_logger - ERROR - 任务 合伙人结算登记同步到BI (08:02) 超过执行窗口5分钟以上,标记为过期。
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'
+11815
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
chardet==5.2.0
holidays==0.87
mysql_connector_repackaged==0.3.1
numpy==2.4.1
pandas==2.3.3
psycopg2==2.9.11
pydes==2.0.1
pymysql==1.1.2
python_dateutil==2.9.0.post0
pytz==2025.2
Requests==2.32.5
schedule==1.2.2
tqdm==4.67.1
+7 -3
View File
@@ -18,15 +18,19 @@ NGV更新数据,12:30,True,待执行
宜搭流程耗时写入BI,07:22,False,已禁用
简道云员工ID表更新,07:23,False,已禁用
简道云海外项目CRM客户档案迁移BI,08:37,True,过期
安装服务历史派发,09:00,True,过期
安装服务历史派发,09:00,False,已禁用
新签客户回访测试,09:00,False,已禁用
分母报备调整,09:05,True,过期
分子报备调整,09:03,True,过期
履约表数据支撑,09:10,True,过期
字段监控,06:25,True,过期
经销商新签服务单转BI,08:05,True,过期
非标业绩提报转BI,17:01,True,待执行
合伙人结算登记同步到BI,17:02,True,待执行
非标业绩提报转BI,08:01,True,过期
合伙人结算登记同步到BI,08:02,True,过期
高德匹配手机号,05:00,True,过期
非标业绩提报转BI,12:01,True,待执行
非标业绩提报转BI,17:01,True,待执行
合伙人结算登记同步到BI,12:02,True,待执行
合伙人结算登记同步到BI,17:02,True,待执行
省市区人员关系表转BI,08:00,True,过期
续约回访待办,09:35,True,过期
1 unique_id exec_time is_switch_on status
18 宜搭流程耗时写入BI 07:22 False 已禁用
19 简道云员工ID表更新 07:23 False 已禁用
20 简道云海外项目CRM客户档案迁移BI 08:37 True 过期
21 安装服务历史派发 09:00 True False 过期 已禁用
22 新签客户回访测试 09:00 False 已禁用
23 分母报备调整 09:05 True 过期
24 分子报备调整 09:03 True 过期
25 履约表数据支撑 09:10 True 过期
26 字段监控 06:25 True 过期
27 经销商新签服务单转BI 08:05 True 过期
28 非标业绩提报转BI 17:01 08:01 True 待执行 过期
29 合伙人结算登记同步到BI 17:02 08:02 True 待执行 过期
30 高德匹配手机号 05:00 True 过期
31 非标业绩提报转BI 12:01 True 待执行
32 非标业绩提报转BI 17:01 True 待执行
33 合伙人结算登记同步到BI 12:02 True 待执行
34 合伙人结算登记同步到BI 17:02 True 待执行
35 省市区人员关系表转BI 08:00 True 过期
36 续约回访待办 09:35 True 过期
+323
View File
@@ -0,0 +1,323 @@
{
"cells": [
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2025-11-05T09:03:45.525420Z",
"start_time": "2025-11-05T09:03:44.127181Z"
}
},
"source": [
"# -*- coding: utf-8 -*-\n",
"import pandas as pd\n",
"import datetime\n",
"from config import Config\n",
"from api import API\n",
"from back_ground_module import CommonModule\n",
"from log_config import configure_task_logger, configure_error_task_logger\n",
"import time\n",
"timestamp = time.time() # 返回 float,单位:秒\n",
"\n",
"logger = configure_task_logger()\n",
"# 获取已经配置好的错误任务日志记录器\n",
"error_task_logger = configure_error_task_logger()\n",
"start_time = datetime.datetime.now()\n",
"api_instance = API()\n",
"common_module = CommonModule()\n",
"output_dir = \"output\" # 设置输出目录\n",
"# 创建输出目录(如果不存在)\n",
"import os\n",
"\n",
"os.makedirs(output_dir, exist_ok=True)\n",
"\n",
"\n",
"class UpdateNGVData:\n",
" \"\"\"NGV数据每日新增\"\"\"\n",
"\n",
" def __init__(self):\n",
" self.staff_id_list = None\n",
" self.field_mapping = {}\n",
" self.fields()\n",
"\n",
" def load_all_data(self):\n",
" # 获取简道云员工id\n",
" payload = {\"api_key\": \"6694d3c4fcb69ca9a111a6c4\",\n",
" \"entry_id\": \"6769204a1902c9341340a1bc\",\n",
" }\n",
" staff_id = api_instance.entry_data_list(payload)\n",
" self.staff_id_list = staff_id.get(\"data\") # api请求格式,将数据封装在data字典里\n",
"\n",
" @staticmethod\n",
" def get_staff_id(row_item, name):\n",
" \"\"\"辅助函数,用于获取员工ID\"\"\"\n",
" if str(row_item[\"_widget_1734942794144\"]) == str(name): # 检查姓名是否匹配\n",
" return row_item[\"_widget_1734942794145\"] # 返回员工ID\n",
" return None\n",
"\n",
" def main(self):\n",
" task_start_time = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n",
" try:\n",
" self.load_all_data()\n",
" logger.info(f\"数据加载完成\")\n",
" #\n",
" # data_NGV_j = common_module.get_ngv_details(days_back=1)\n",
" # data_NGV_j1 = common_module.get_ngv_details(days_back=2)\n",
" timestamp = time.time() # 返回 float,单位:秒\n",
" #\n",
" # data_NGV_j.to_csv(os.path.join(output_dir, f\"{timestamp}up_NGV_j.csv\"))\n",
" # data_NGV_j1.to_csv(os.path.join(output_dir, f\"{timestamp}up_NGV_j1.csv\"))\n",
" #\n",
" # # 找出在 data_NGV_j 中存在但在 data_NGV_j1 中不存在的 data_id\n",
" # unique_data_ids = data_NGV_j[~data_NGV_j['org_code'].isin(data_NGV_j1['org_code'])]\n",
" #\n",
" # # 创建一个新的 DataFrame 保存这些唯一的 data_id 及其对应的数据\n",
" # new_df = data_NGV_j[data_NGV_j['org_code'].isin(unique_data_ids['org_code'])]\n",
" #\n",
" # # 对 new_df 进行进一步的过滤,只保留 org_type 为 \"一般\" 的记录\n",
" # data_NGV_j = data_NGV_j[data_NGV_j['org_type'] == '一般']\n",
" # data_NGV_j1 = data_NGV_j1[data_NGV_j1['org_type'] == '一般']\n",
" # filtered_df = new_df[new_df['org_type'] == '一般']\n",
" # 默认未删除\n",
" filtered_df = pd.read_excel(r\"C:\\Users\\zy187\\Downloads\\异常服务跟进待办_20251105170140.xlsx\",sheet_name=\"Sheet1\")\n",
" filtered_df['源ngv是否已删除'] = '未删除'\n",
"\n",
" # 日期字段转换为日期格式\n",
" time_columns = ['date_fmt', 'saas_create_time', 'expiry_time', 'install_create_time', \"last_end_date\",\n",
" \"renew_date\"]\n",
" new_filtered_df = filtered_df.copy() # 复制df,以调整时间\n",
" for col in time_columns:\n",
" # 1. 转换为datetime类型(带错误处理)\n",
" # 使用.loc安全赋值\n",
" new_filtered_df[col] = pd.to_datetime(filtered_df[col], errors='coerce', utc=False)\n",
"\n",
" # 2. 优化后的时区转换(高效向量化操作)\n",
" filtered_df[col + '_date'] = (\n",
" new_filtered_df[col]\n",
" # 本地化为北京时间(东八区)\n",
" .dt.tz_localize('Asia/Shanghai', ambiguous='infer', nonexistent='NaT')\n",
" # 转换为UTC时区\n",
" .dt.tz_convert('UTC')\n",
" # 格式化为ISO8601字符串\n",
" .dt.strftime('%Y-%m-%dT%H:%M:%SZ')\n",
" )\n",
" logger.info(f\"时间转换完成\")\n",
"\n",
" # 人员字段转换为人员字段\n",
" staff_columns = ['area_manager', 'service_impl_principal', \"service_salesmen\", \"technician\"]\n",
" # 将员工列表转为DataFrame\n",
" # 三重循环临时方案(确保可写入)\n",
" for col in staff_columns:\n",
" staff_ids = []\n",
" for _, row in filtered_df.iterrows():\n",
" matched = False\n",
" for staff in self.staff_id_list:\n",
" if str(staff['_widget_1734942794144']) == str(row[col]):\n",
" staff_ids.append(staff['_widget_1734942794145'])\n",
" matched = True\n",
" break\n",
" if not matched:\n",
" staff_ids.append(None)\n",
" filtered_df[col + \"_staff_id\"] = staff_ids\n",
" logger.info(f\"人员转换完成\")\n",
"\n",
" # filtered_df.to_csv(r\"D:\\Idea Project\\SaaS_V1.3\\back_ground_module\\output\\NGV.csv\")\n",
"\n",
" # 生成包含所有行转换后的字典列表\n",
" # all_data = [self.row_to_dict(row, self.field_mapping) for index, row in data_NGV_j1.iterrows()] # 前两天的全部数据\n",
" # all_data = [self.row_to_dict(row, self.field_mapping) for index, row in data_NGV_j.iterrows()] # 前一天的全部数据\n",
" all_data = [self.row_to_dict(row, self.field_mapping) for index, row in filtered_df.iterrows()] # 增量数据\n",
"\n",
" try:\n",
" filtered_df.to_csv(os.path.join(output_dir, f\"{timestamp}NGV.csv\"))\n",
" except Exception as e:\n",
" error_task_logger.error(f\"NGV过滤后数据保存异常: {e}\")\n",
" pass\n",
"\n",
" #\n",
" data = {'api_key': Config.SaaS_Tasks_APP_ID, 'entry_id': Config.NGV_TASKS_ENTRY_ID, \"data_list\": all_data,\n",
" \"is_start_trigger\": \"true\"}\n",
"\n",
" result = api_instance.entry_data_batch_create(data)\n",
" logger.info(f\"数据已推送:{result}\")\n",
" # result_str = str(result)\n",
" # print(result_str[:500])\n",
"\n",
" # 保存到Excel文件\n",
" # output_path = r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细1.xlsx'\n",
" # filtered_df.to_excel(output_path, index=False)\n",
" # data_NGV_j1.to_excel( r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细j1.xlsx', index=False)\n",
" # data_NGV_j.to_excel( r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细j.xlsx', index=False)\n",
" # new_df.to_excel(r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细ndf.xlsx', index=False)\n",
"\n",
" common_module.send_task_status(task_start_time, \"NGV新增数据\")\n",
" logger.info(f\"任务完成。\")\n",
" except Exception as e:\n",
" error_task_logger.error(f\"任务执行时发生异常: {e}\")\n",
" # common_module.send_task_error(task_start_time, \"NGV新增数据\", str(e))\n",
"\n",
" @staticmethod\n",
" def row_to_dict(row, field_mapping):\n",
" \"\"\"将一行数据转换为指定格式的字典,并确保时间类型可JSON序列化\"\"\"\n",
" result = {}\n",
" for col_name, widget_id in field_mapping.items():\n",
" if col_name in row:\n",
" value = row[col_name]\n",
" if pd.isna(value):\n",
" clean_value = None\n",
" elif isinstance(value, (pd.Timestamp, pd.Timedelta)):\n",
" clean_value = value.isoformat() # 或 str(value)\n",
" elif hasattr(value, 'strftime'): # 兼容 datetime.datetime\n",
" clean_value = value.strftime('%Y-%m-%dT%H:%M:%SZ')\n",
" else:\n",
" clean_value = value\n",
" result[widget_id] = {\"value\": clean_value}\n",
" return result\n",
"\n",
" def fields(self):\n",
" self.field_mapping = dict(date_id='_widget_1734062123065', date_fmt='_widget_1734062123066',\n",
" id_own_group='_widget_1734062123067', group_name='_widget_1734062123068',\n",
" id_own_org='_widget_1734062123069', org_name='_widget_1734062123070',\n",
" org_code='_widget_1734062123071', group_grade='_widget_1734062123072',\n",
" org_type='_widget_1734062123073', org_status='_widget_1734062123074',\n",
" saas_version='_widget_1734062123075', is_wechat='_widget_1734062123076',\n",
" is_mini_app='_widget_1734062123077', is_wx_shop='_widget_1734062123078',\n",
" is_camera_service='_widget_1734062123079',\n",
" is_maintenance_service='_widget_1734062123080',\n",
" saas_create_time='_widget_1734062123081', expiry_time='_widget_1734062123082',\n",
" saas_use_days='_widget_1734062123083', saas_use_year='_widget_1734062123084',\n",
" is_main_org='_widget_1734062123085', license_code='_widget_1734062123086',\n",
" license_name='_widget_1734062123087', org_crm_id='_widget_1734062123088',\n",
" province_id='_widget_1734062123089', province_name='_widget_1734062123090',\n",
" city_id='_widget_1734062123091', city_name='_widget_1734062123092',\n",
" area_id='_widget_1734062123093', area_name='_widget_1734062123094',\n",
" region_name='_widget_1734062123095', region_short_name='_widget_1734062123096',\n",
" branch_name='_widget_1734062123097', carzone_store_id='_widget_1734062123098',\n",
" carzone_store_name='_widget_1734062123099',\n",
" customer_carzone_id='_widget_1734062123100', salesmen='_widget_1734062123101',\n",
" area_manager='_widget_1734062123102', service_salesmen='_widget_1734062123103',\n",
" impl_principal='_widget_1734062123104',\n",
" service_impl_principal='_widget_1734062123105',\n",
" active_user_count='_widget_1734062123106', active_user_type='_widget_1734062123107',\n",
" limit_user_count='_widget_1734062123108', limit_user_type='_widget_1734062123109',\n",
" is_n='_widget_1734062123110', is_g='_widget_1734062123111',\n",
" is_v='_widget_1734062123112', is_visited='_widget_1734062123113',\n",
" is_active='_widget_1734062123114', active_status_fmt='_widget_1734062123115',\n",
" bill_count_last_30_day='_widget_1734062123116',\n",
" bill_day_count_last_30_day='_widget_1734062123117',\n",
" bill_day_count_this_month='_widget_1734062123118',\n",
" bill_count_last_7_day='_widget_1734062123119',\n",
" bill_day_count_last_7_day='_widget_1734062123120', pv_count='_widget_1734062123121',\n",
" uv_count='_widget_1734062123122', bill_count_1d='_widget_1734062123123',\n",
" bill_count_2d='_widget_1734062123124', bill_count_3d='_widget_1734062123125',\n",
" bill_count_4d='_widget_1734062123126', bill_count_5d='_widget_1734062123127',\n",
" bill_count_6d='_widget_1734062123128', bill_count_7d='_widget_1734062123129',\n",
" bill_count_8d='_widget_1734062123130', bill_count_9d='_widget_1734062123131',\n",
" bill_count_10d='_widget_1734062123132', bill_count_11d='_widget_1734062123133',\n",
" bill_count_12d='_widget_1734062123134', bill_count_13d='_widget_1734062123135',\n",
" bill_count_14d='_widget_1734062123136', bill_count_15d='_widget_1734062123137',\n",
" bill_count_16d='_widget_1734062123138', bill_count_17d='_widget_1734062123139',\n",
" bill_count_18d='_widget_1734062123140', bill_count_19d='_widget_1734062123141',\n",
" bill_count_20d='_widget_1734062123142', bill_count_21d='_widget_1734062123143',\n",
" bill_count_22d='_widget_1734062123144', bill_count_23d='_widget_1734062123145',\n",
" bill_count_24d='_widget_1734062123146', bill_count_25d='_widget_1734062123147',\n",
" bill_count_26d='_widget_1734062123148', bill_count_27d='_widget_1734062123149',\n",
" bill_count_28d='_widget_1734062123150', bill_count_29d='_widget_1734062123151',\n",
" bill_count_30d='_widget_1734062123152', bill_count_31d='_widget_1734062123153',\n",
" etl_time='_widget_1734062123154',\n",
" maintain_bill_count_last_30_day='_widget_1734062123155',\n",
" washing_bill_count_last_30_day='_widget_1734062123156',\n",
" maintain_bill_day_count_last_30_day='_widget_1734062123157',\n",
" washing_bill_day_count_last_30_day='_widget_1734062123158',\n",
" retail_bill_count_last_30_day='_widget_1734062123159',\n",
" retail_bill_day_count_last_30_day='_widget_1734062123160',\n",
" purchase_bill_count_last_30_day='_widget_1734062123161',\n",
" purchase_bill_day_count_last_30_day='_widget_1734062123162',\n",
" card_bill_count_last_30_day='_widget_1734062123163',\n",
" card_bill_day_count_last_30_day='_widget_1734062123164',\n",
" gd_sales_bill_count_last_30_day='_widget_1734062123165',\n",
" gd_sales_bill_day_count_last_30_day='_widget_1734062123166',\n",
" g_change_flag='_widget_1734062123167', saas_package='_widget_1734062123168',\n",
" manage_model='_widget_1734062123169', contacts='_widget_1734062123170',\n",
" contact_number='_widget_1734062123171', contact_mobile='_widget_1734062123172',\n",
" g_month_count='_widget_1734062123173', g_month_percentage='_widget_1734062123174',\n",
" is_install_service='_widget_1734062123175',\n",
" install_create_time='_widget_1734062123176', last_end_date='_widget_1734062123177',\n",
" renew_date='_widget_1734062123178', is_chain_owner='_widget_1734062123179',\n",
" group_org_count='_widget_1734062123180',\n",
" recent_bill_warning_days='_widget_1734062123181',\n",
" g_change_flag_d='_widget_1734062123182', g_lost_warning_days='_widget_1734062123183',\n",
" saas_edition_fmt='_widget_1734062123184', g_flag_1m='_widget_1734062123185',\n",
" g_flag_2m='_widget_1734062123186', g_flag_3m='_widget_1734062123187',\n",
" g_flag_4m='_widget_1734062123188', g_flag_5m='_widget_1734062123189',\n",
" g_flag_6m='_widget_1734062123190', g_flag_day_count='_widget_1734062123191',\n",
" add_org_flag='_widget_1734062123192', pt='_widget_1734062123193',\n",
" org_size='_widget_1734062123194', qualification_type_fmt='_widget_1734062123195',\n",
" business_scope_fmt='_widget_1734062123196', store_type_fmt='_widget_1734062123197',\n",
" area='_widget_1734062123198', station_number='_widget_1734062123199',\n",
" header_type_fmt='_widget_1734062123200', org_stage='_widget_1734062123201',\n",
" g_count_this_month='_widget_1734062123202',\n",
" saas_customer_type='_widget_1734062123203', technician='_widget_1734062123204',\n",
" tmall_maintain_service_status_desc='_widget_1734062123205',\n",
" date_fmt_date='_widget_1749000071375',\n",
" area_manager_staff_id='_widget_1748496855779',\n",
" service_impl_principal_staff_id=\"_widget_1748496855780\",\n",
" service_salesmen_staff_id=\"_widget_1748496855778\",\n",
" technician_staff_id=\"_widget_1751877712235\",\n",
" saas_create_time_date=\"_widget_1749000071377\",\n",
" expiry_time_date=\"_widget_1749000071382\",\n",
" install_create_time_date=\"_widget_1749000071384\",\n",
" last_end_date_date=\"_widget_1749000071389\", renew_date_date=\"_widget_1749000071391\"\n",
" , 源NGV是否已删除=\"_widget_1754285499851\")\n",
"\n",
"\n",
"if __name__ == '__main__':\n",
" start = UpdateNGVData()\n",
" start.main()\n"
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"已获取 100 条数据\n",
"已获取 146 条数据\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\u001B[92m2025-11-05 17:03:45,497 - api.py - task_logger - INFO - 获取了146条数据\u001B[0m\n",
"\u001B[92m2025-11-05 17:03:45,498 - 4224831806.py - task_logger - INFO - 数据加载完成\u001B[0m\n",
"\u001B[91m2025-11-05 17:03:45,523 - 4224831806.py - error_task_logger - ERROR - 任务执行时发生异常: 'date_fmt'\u001B[0m\n"
]
}
],
"execution_count": 6
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,93 @@
import argparse
from pathlib import Path
import pandas as pd
def keep_latest_by_time(df: pd.DataFrame, store_col: str, time_col: str) -> pd.DataFrame:
if store_col not in df.columns:
raise ValueError(f"缺少列: {store_col}")
if time_col not in df.columns:
raise ValueError(f"缺少列: {time_col}")
working = df.copy()
working[store_col] = working[store_col].astype(str).fillna("")
working[time_col] = working[time_col].astype(str).fillna("")
working["_parsed_time"] = pd.to_datetime(working[time_col], errors="coerce")
working["_row_order"] = range(len(working))
working = working.sort_values(
by=["_parsed_time", "_row_order"],
ascending=[True, True],
kind="mergesort",
na_position="first",
)
latest = working.groupby(store_col, sort=False).tail(1)
latest = latest.drop(columns=["_parsed_time", "_row_order"]).reset_index(drop=True)
return latest
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument("--input", "-i", required=False, help="输入 Excel 路径(.xlsx")
parser.add_argument("--output", "-o", required=False, help="输出 Excel 路径(.xlsx")
parser.add_argument("--sheet", default="需要保留一条", help="Sheet 名称")
parser.add_argument("--store-col", default="门店编码", help="门店编码列名")
parser.add_argument("--time-col", default="创建时间", help="创建时间列名")
parser.add_argument("--codes-output", required=False, help="可选:输出门店编码清单(.txt 或 .csv)")
parser.add_argument("--demo", action="store_true", help="运行内置示例(不读写 Excel")
return parser
def main() -> int:
args = build_parser().parse_args()
if args.demo:
demo_df = pd.DataFrame(
[
{"门店编码": "A001", "创建时间": "2026-03-01 10:00:00", "其他": "x"},
{"门店编码": "A001", "创建时间": "2026-03-05 09:00:00", "其他": "y"},
{"门店编码": "B002", "创建时间": "2026/03/02 12:00", "其他": "m"},
{"门店编码": "B002", "创建时间": "无效时间", "其他": "n"},
]
)
result = keep_latest_by_time(demo_df, store_col=args.store_col, time_col=args.time_col)
print(result)
print("门店编码:", ",".join(result[args.store_col].astype(str).tolist()))
return 0
if not args.input:
raise SystemExit("缺少参数 --input")
input_path = Path(args.input).expanduser().resolve()
if not input_path.exists():
raise SystemExit(f"输入文件不存在: {input_path}")
df = pd.read_excel(input_path, sheet_name=args.sheet, dtype=str).fillna("")
latest = keep_latest_by_time(df, store_col=args.store_col, time_col=args.time_col)
output_path = Path(args.output).expanduser().resolve() if args.output else None
if output_path is None:
output_path = input_path.with_name(f"{input_path.stem}_保留最新.xlsx")
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
latest.to_excel(writer, sheet_name="保留最新", index=False)
store_codes = latest[args.store_col].astype(str).tolist()
print(f"保留行数: {len(latest)}")
print(f"门店编码数量: {len(store_codes)}")
if args.codes_output:
codes_path = Path(args.codes_output).expanduser().resolve()
if codes_path.suffix.lower() == ".csv":
pd.DataFrame({args.store_col: store_codes}).to_csv(codes_path, index=False, encoding="utf-8-sig")
else:
codes_path.write_text("\n".join(store_codes), encoding="utf-8")
print(f"输出文件: {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
-104
View File
@@ -1,104 +0,0 @@
{
"result": [
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-02-28T09:07Z",
"showName": "提交申请",
"operateType": "NEW_PROCESS",
"remark": "",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "宜搭平台",
"actionExit": "submit",
"operatorUserId": "yida_pub_account",
"activityId": "sid-restartevent",
"size": 1,
"dataId": 52359309345,
"domainList": [],
"operatorDisplayName": "宜搭平台",
"action": "提交申请",
"taskId": "null",
"operatorPhotoUrl": "//img.alicdn.com/tfs/TB1mKVJSpXXXXcwaXXXXXXXXXXX-78-80.jpg"
},
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-03-18T10:14Z",
"showName": "120天联系情况",
"operateType": "EXECUTE_TASK_NORMAL",
"remark": "提交",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "孟祥宇",
"actionExit": "agree",
"operatorUserId": "224616673723465569",
"activityId": "sid-6470221a-82ec-4bdd-a873-245ee47a5605",
"size": 1,
"dataId": 53850072345,
"domainList": [],
"operatorDisplayName": "孟祥宇",
"action": "提交",
"taskId": "52359293348",
"operatorPhotoUrl": "https://static.dingtalk.com/media/lADPDh0cUUaSfHrNAvDNAnA_624_752.jpg"
},
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-04-01T14:08Z",
"showName": "60天联系情况",
"operateType": "EXECUTE_TASK_NORMAL",
"remark": "系统提交",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "魏子淇",
"actionExit": "agree",
"operatorUserId": "195159084238961158",
"activityId": "sid-ab6374fd-7580-66d5-1628-6b0666bb38ff",
"size": 1,
"dataId": 55294608170,
"domainList": [],
"operatorDisplayName": "魏子淇",
"action": "提交",
"taskId": "53850072349",
"operatorPhotoUrl": "https://static.dingtalk.com/media/lADPD2sQvHoyUEbNAtHNAtA_720_721.jpg"
},
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-04-01T14:08Z",
"showName": "30天联系情况",
"operateType": "EXECUTE_TASK_NORMAL",
"remark": "系统提交",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "魏子淇",
"actionExit": "agree",
"operatorUserId": "195159084238961158",
"activityId": "sid-e5928800-154e-4e20-6019-1364274afc49",
"size": 1,
"dataId": 55294609293,
"domainList": [],
"operatorDisplayName": "魏子淇",
"action": "提交",
"taskId": "55294608175",
"operatorPhotoUrl": "https://static.dingtalk.com/media/lADPD2sQvHoyUEbNAtHNAtA_720_721.jpg"
},
{
"processInstanceId": "fa83e004-64d5-40a8-b2a9-122cc24a548f",
"operateTimeGMT": "2025-04-01T14:08Z",
"showName": "0天处理情况",
"operateType": "EXECUTE_TASK_NORMAL",
"remark": "系统提交",
"taskHoldTimeGMT": 0,
"type": "HISTORY",
"operatorName": "魏子淇",
"actionExit": "agree",
"operatorUserId": "195159084238961158",
"activityId": "sid-ba12125f-bc3a-2663-ebf0-43b5aeb8c32c",
"size": 1,
"dataId": 55294558885,
"domainList": [],
"operatorDisplayName": "魏子淇",
"action": "同意",
"taskId": "55294609301",
"operatorPhotoUrl": "https://static.dingtalk.com/media/lADPD2sQvHoyUEbNAtHNAtA_720_721.jpg"
}
]
}
-116
View File
@@ -1,116 +0,0 @@
{
"app_id": "6694d3c4fcb69ca9a111a6c4",
"form_id": "693778ee287cfdcc2df85ece",
"form_title": "流程表单结构测试",
"instance_id": "6937dc8d2cbcdd4c466a8381",
"url": "https://dingtalk.jiandaoyun.com/workflow/process_instance/6937dc8d2cbcdd4c466a8381",
"update_time": "2025-12-09T08:23:41.095Z",
"create_time": "2025-12-09T08:23:41.093Z",
"finish_time": None,
"status": 0,
"creator": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"tasks": [
{
"app_id": "6694d3c4fcb69ca9a111a6c4",
"form_id": "693778ee287cfdcc2df85ece",
"form_title": "流程表单结构测试",
"title": "流程发起节点",
"instance_id": "6937dc8d2cbcdd4c466a8381",
"task_id": "6937dc8d2cbcdd4c466a8398",
"flow_id": 0,
"flow_name": "流程发起节点",
"url": "https://dingtalk.jiandaoyun.com/workflow/process_instance/6937dc8d2cbcdd4c466a8381/task/6937dc8d2cbcdd4c466a8398",
"assignee": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"creator": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"create_time": "2025-12-09T08:23:41.094Z",
"create_action": "forward",
"finish_time": "2025-12-09T08:23:41.094Z",
"finish_action": "forward",
"status": 1
},
{
"app_id": "6694d3c4fcb69ca9a111a6c4",
"form_id": "693778ee287cfdcc2df85ece",
"form_title": "流程表单结构测试",
"title": "多跟进人节点",
"instance_id": "6937dc8d2cbcdd4c466a8381",
"task_id": "6937dc8d2cbcdd4c466a83a6",
"flow_id": 2,
"flow_name": "多跟进人节点",
"url": "https://dingtalk.jiandaoyun.com/workflow/process_instance/6937dc8d2cbcdd4c466a8381/task/6937dc8d2cbcdd4c466a83a6",
"assignee": {
"username": "4210192048793363",
"name": "张阳",
"departments": [
449008196
],
"type": 0,
"status": 1,
"integrate_id": "4210192048793363"
},
"creator": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"create_time": "2025-12-09T08:23:41.095Z",
"create_action": "forward",
"finish_time": None,
"finish_action": None,
"status": 0
},
{
"app_id": "6694d3c4fcb69ca9a111a6c4",
"form_id": "693778ee287cfdcc2df85ece",
"form_title": "流程表单结构测试",
"title": "多跟进人节点",
"instance_id": "6937dc8d2cbcdd4c466a8381",
"task_id": "6937dc8d2cbcdd4c466a83aa",
"flow_id": 2,
"flow_name": "多跟进人节点",
"url": "https://dingtalk.jiandaoyun.com/workflow/process_instance/6937dc8d2cbcdd4c466a8381/task/6937dc8d2cbcdd4c466a83aa",
"assignee": {
"username": "2268275546837446",
"name": "曹伟",
"departments": [
449008196
],
"type": 0,
"status": 1,
"integrate_id": "2268275546837446"
},
"creator": {
"username": "#admin",
"name": "F6汽车科技",
"type": 0,
"status": 1,
"integrate_id": "#admin"
},
"create_time": "2025-12-09T08:23:41.095Z",
"create_action": "forward",
"finish_time": None,
"finish_action": None,
"status": 0
}
]
}
-9312
View File
File diff suppressed because one or more lines are too long
+314
View File
@@ -0,0 +1,314 @@
{
"cells": [
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2026-01-13T07:56:05.597737600Z",
"start_time": "2026-01-13T07:50:57.192717400Z"
}
},
"source": [
"import time\n",
"\n",
"import pandas as pd\n",
"import psycopg2\n",
"import mysql.connector\n",
"from mysql.connector import Error\n",
"from datetime import datetime, timedelta\n",
"import numpy as np\n",
"\n",
"# ========== 配置 ==========\n",
"PG_CONN_INFO = {\n",
" \"database\": \"f6_bi\",\n",
" \"user\": \"LTAI5tMJsijFA9BS1R6uBpUT\",\n",
" \"password\": \"PajEQMIRWNRcipd8mYvlud2KHWJr6N\",\n",
" \"host\": \"hgpostcn-cn-m1e4gikbu00l-cn-shanghai.hologres.aliyuncs.com\",\n",
" \"port\": \"80\"\n",
"}\n",
"\n",
"MYSQL_CONFIG = {\n",
" 'host': \"f6-public.rwlb.rds.aliyuncs.com\",\n",
" 'user': \"rw_operation_data_relay\",\n",
" 'password': \"m+q5Z4%IVuF9bf\",\n",
" 'database': \"f6operation_data_relay\"\n",
"}\n",
"\n",
"SOURCE_TABLE = '\"public\".\"holo_ads_report_saas_profile_ngv_detail_d\"'\n",
"PARTITION_COLUMN = \"date_id\"\n",
"TARGET_TABLE_MYSQL = \"jdy_ngv_data_source\"\n",
"BATCH_SIZE = 2000\n",
"\n",
"# ========== 辅助函数 ==========\n",
"def is_datetime_type(pg_type: str) -> bool:\n",
" if not pg_type:\n",
" return False\n",
" pg_type = pg_type.lower()\n",
" return any(kw in pg_type for kw in ['timestamp', 'datetime', 'date'])\n",
"\n",
"def clean_column_name(name, index):\n",
" \"\"\"将列名转为合法字符串,处理 None / nan / 空值\"\"\"\n",
" if name is None:\n",
" return f\"unknown_col_{index}\"\n",
" if isinstance(name, float) and pd.isna(name):\n",
" return f\"unknown_col_{index}\"\n",
" name_str = str(name).strip()\n",
" if not name_str or name_str.lower() in ('nan', 'none', 'null', ''):\n",
" return f\"unknown_col_{index}\"\n",
" return name_str\n",
"\n",
"def get_source_schema():\n",
" conn = psycopg2.connect(**PG_CONN_INFO)\n",
" cur = conn.cursor()\n",
" cur.execute(\"\"\"\n",
" SELECT column_name, data_type\n",
" FROM information_schema.columns\n",
" WHERE table_schema = 'public'\n",
" AND table_name = 'holo_ads_report_saas_profile_ngv_detail_d'\n",
" ORDER BY ordinal_position;\n",
" \"\"\")\n",
" raw_schema = cur.fetchall()\n",
" cur.close()\n",
" conn.close()\n",
"\n",
" # 清洗列名\n",
" cleaned_schema = []\n",
" for i, (col_name, data_type) in enumerate(raw_schema):\n",
" clean_name = clean_column_name(col_name, i)\n",
" cleaned_schema.append((clean_name, data_type or 'text'))\n",
" return cleaned_schema\n",
"\n",
"def create_ngv_table(cursor, schema):\n",
" col_defs = []\n",
" for col_name, pg_type in schema:\n",
" if is_datetime_type(pg_type):\n",
" col_defs.append(f\"`{col_name}` DATETIME\")\n",
" else:\n",
" col_defs.append(f\"`{col_name}` TEXT\") # ✅ 关键:用 TEXT 避免行大小超限\n",
" create_sql = f\"\"\"\n",
" CREATE TABLE IF NOT EXISTS `{TARGET_TABLE_MYSQL}` (\n",
" {\",\\n \".join(col_defs)}\n",
" ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;\n",
" \"\"\"\n",
" cursor.execute(create_sql)\n",
" print(\"✅ MySQL 表 NGV 已创建(时间字段为 DATETIME,其余为 TEXT\")\n",
"\n",
"def normalize_datetime_cols(df, datetime_cols):\n",
" df = df.copy()\n",
" for col in datetime_cols:\n",
" if col in df.columns:\n",
" df[col] = pd.to_datetime(df[col], errors='coerce')\n",
" df[col] = df[col].dt.strftime('%Y-%m-%d %H:%M:%S').where(df[col].notnull(), None)\n",
" return df.where(pd.notnull(df), None)\n",
"\n",
"# ========== 主流程 ==========\n",
"def main():\n",
" # 1. 生成最近10天的 date_id(字符串格式)\n",
" date_ids = [\n",
" (datetime.now().date() - timedelta(days=i)).strftime(\"%Y%m%d\")\n",
" for i in range(3)\n",
" ]\n",
" print(f\"将同步以下 date_id 分区: {date_ids}\")\n",
"\n",
" # 2. 获取并清洗源表结构\n",
" schema = get_source_schema()\n",
" column_names = [col for col, _ in schema]\n",
" datetime_cols = [col for col, typ in schema if is_datetime_type(typ)]\n",
"\n",
" print(f\"检测到 {len(column_names)} 个字段,其中时间字段: {datetime_cols[:3]}...\")\n",
"\n",
" # 3. 连接 MySQL\n",
" mysql_conn = mysql.connector.connect(**MYSQL_CONFIG)\n",
" mysql_cursor = mysql_conn.cursor()\n",
"\n",
" try:\n",
" # 4. 创建目标表\n",
" create_ngv_table(mysql_cursor, schema)\n",
"\n",
" # 5. 构造插入 SQL\n",
" placeholders = \", \".join([\"%s\"] * len(column_names))\n",
" cols_str = \", \".join([f\"`{c}`\" for c in column_names])\n",
" insert_sql = f\"INSERT INTO `{TARGET_TABLE_MYSQL}` ({cols_str}) VALUES ({placeholders})\"\n",
"\n",
" # 6. 清空目标表\n",
" mysql_cursor.execute(f\"TRUNCATE TABLE `{TARGET_TABLE_MYSQL}`;\")\n",
" print(\"🗑️ 已清空 NGV 表\")\n",
"\n",
" # 7. 按 date_id 分批处理\n",
" # -- 新增:固定列名用于 SELECT --\n",
" fixed_columns = [col for col, _ in schema]\n",
" quoted_fixed_columns = \", \".join([f'\"{c}\"' for c in fixed_columns])\n",
"\n",
" # 动态选择排序字段(必须在 fixed_columns 中)\n",
" exclude_cols = {PARTITION_COLUMN} | set(datetime_cols)\n",
" candidates = [col for col in fixed_columns if col not in exclude_cols]\n",
" order_col = f'\"{candidates[0]}\"' if candidates else f'\"{PARTITION_COLUMN}\"'\n",
"\n",
" if \"org_code\" not in column_names:\n",
" raise ValueError(\"❌ 源表中未找到唯一字段 'org_code'\")\n",
"\n",
" for date_id in date_ids:\n",
" print(f\"\\n>>> 处理 date_id = {date_id}\")\n",
" last_org_code = None # 游标:上一批最大的 org_code\n",
" i = 1\n",
"\n",
" while True:\n",
" time.sleep(3)\n",
" pg_conn = psycopg2.connect(**PG_CONN_INFO)\n",
" pg_cur = pg_conn.cursor()\n",
"\n",
" # 构造 WHERE 条件\n",
" if last_org_code is None:\n",
" where_clause = f'\"{PARTITION_COLUMN}\" = %s'\n",
" params = (date_id,)\n",
" else:\n",
" where_clause = f'\"{PARTITION_COLUMN}\" = %s AND \"org_code\" > %s'\n",
" params = (date_id, last_org_code)\n",
"\n",
" sql = f\"\"\"\n",
" SELECT {quoted_fixed_columns}\n",
" FROM {SOURCE_TABLE}\n",
" WHERE {where_clause}\n",
" ORDER BY \"org_code\"\n",
" LIMIT {BATCH_SIZE};\n",
" \"\"\"\n",
" pg_cur.execute(sql, params)\n",
" rows = pg_cur.fetchall()\n",
" pg_cur.close()\n",
" pg_conn.close()\n",
"\n",
" if not rows:\n",
" break\n",
"\n",
" df_batch = pd.DataFrame(rows, columns=fixed_columns)\n",
" df_batch = normalize_datetime_cols(df_batch, datetime_cols)\n",
" df_batch.to_csv(f\"输出查看{i}.csv\", index=False)\n",
" i += 1\n",
"\n",
" # 更新游标:取本批最后一条的 org_code\n",
" last_org_code = df_batch.iloc[-1][\"org_code\"]\n",
"\n",
" # 清洗并插入 MySQL\n",
" def sanitize_row(row):\n",
" return tuple(\n",
" None if (isinstance(x, float) and pd.isna(x)) or pd.isna(x) else x\n",
" for x in row\n",
" )\n",
"\n",
" data_tuples = [sanitize_row(row) for row in df_batch.values]\n",
" mysql_cursor.executemany(insert_sql, data_tuples)\n",
" mysql_conn.commit()\n",
"\n",
" inserted = len(data_tuples)\n",
" print(f\" date_id={date_id} 已插入 {inserted} 行 (last_org_code={last_org_code})\")\n",
"\n",
" print(f\"✅ date_id={date_id} 同步完成\")\n",
"\n",
" print(f\"\\n🎉 同步完成!数据已写入 MySQL 表 `{TARGET_TABLE_MYSQL}`\")\n",
"\n",
" except Exception as e:\n",
" print(f\"❌ 错误: {e}\")\n",
" mysql_conn.rollback()\n",
" finally:\n",
" mysql_cursor.close()\n",
" mysql_conn.close()\n",
"\n",
"if __name__ == \"__main__\":\n",
" main()"
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"将同步以下 date_id 分区: ['20260113', '20260112', '20260111']\n",
"检测到 141 个字段,其中时间字段: []...\n",
"✅ MySQL 表 NGV 已创建(时间字段为 DATETIME,其余为 TEXT\n",
"🗑️ 已清空 NGV 表\n",
"\n",
">>> 处理 date_id = 20260113\n",
"✅ date_id=20260113 同步完成\n",
"\n",
">>> 处理 date_id = 20260112\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS201812070004175)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS201903250025112)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS201907240033962)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS201910150040257)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS201912160046642)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202003230057861)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202006080088028)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202010040108419)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202104090119587)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202106210130145)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202108300139429)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202112050146822)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202204250175005)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202209300189703)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202303230219406)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202308210240694)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202402260259380)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202406260273963)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202411040284912)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202503300294788)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202507150304118)\n",
" date_id=20260112 已插入 2000 行 (last_org_code=CHS202511120312981)\n",
" date_id=20260112 已插入 1278 行 (last_org_code=TQB201509180060)\n",
"✅ date_id=20260112 同步完成\n",
"\n",
">>> 处理 date_id = 20260111\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS201812070004175)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS201903250025112)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS201907240033962)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS201910150040257)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS201912160046642)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202003230057861)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202006080088042)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202010050108424)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202104090119621)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202106210130146)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202108300139429)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202112050146822)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202204250175005)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202209300189703)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202303230219406)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202308210240694)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202402260259380)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202406260273963)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202411040284912)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202503300294779)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202507150304098)\n",
" date_id=20260111 已插入 2000 行 (last_org_code=CHS202511120312975)\n",
" date_id=20260111 已插入 1259 行 (last_org_code=TQB201509180060)\n",
"✅ date_id=20260111 同步完成\n",
"\n",
"🎉 同步完成!数据已写入 MySQL 表 `jdy_ngv_data_source`\n"
]
}
],
"execution_count": 7
}
],
"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
}
-1639
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "## 全量同步",
"id": "69bf37484b68b727"
},
{
"cell_type": "code",
"execution_count": null,
"id": "initial_id",
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
""
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+1
View File
@@ -401,6 +401,7 @@ try:
ts = int(round(t * 1000))
randint = random.randint(100000000, 999999999)
req = res_new['result'] + "|" + formData['textField_kuntp6fk'] + "|" + formData['textField_kuntp6fl']+ "|" + formData['employeeField_kykw5ege'] + "_" + str(ts) + "_" + str(randint)
# 实例ID|门ID|服务单号|专属运营顾问
str_en = des_encrypt(req)
print(str_en.decode('utf-8'))
req_new = str_en.decode('utf-8')
+39
View File
@@ -0,0 +1,39 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "initial_id",
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import pandas as pd\n",
"\n",
"df = pd.read_excel(fr\"C:\\Users\\hp_z66\\Downloads\\商机问题跟进表_20260331114857.xlsx\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+43
View File
@@ -0,0 +1,43 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "## 重复派发",
"id": "2d5eea6406e5bd27"
},
{
"cell_type": "code",
"execution_count": null,
"id": "initial_id",
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
""
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,524 @@
import os
import pandas as pd
import json
import ast
import re
from datetime import datetime
from chardet import detect
import unicodedata
# ==============================
# 核心修复:处理数组类型数据的工具函数
# ==============================
def is_empty_value(value):
"""判断值是否为空(支持数组、字符串、数值等所有类型)"""
if pd.isna(value):
return True
if value is None:
return True
if isinstance(value, str) and value.strip() == '':
return True
if isinstance(value, (list, tuple, set)) and len(value) == 0:
return True
return False
def clean_special_characters(value):
"""清理特殊字符(支持数组类型,避免歧义错误)"""
# 处理空值
if is_empty_value(value):
return value
# 处理数组类型(如 ['a', 'b']
if isinstance(value, (list, tuple)):
cleaned_list = []
for item in value:
if isinstance(item, str):
cleaned_list.append(_clean_single_string(item))
else:
cleaned_list.append(item)
return cleaned_list
# 处理字符串类型
elif isinstance(value, str):
return _clean_single_string(value)
# 其他类型(数值、布尔等)直接返回
else:
return value
def _clean_single_string(text):
"""清理单个字符串的特殊字符(内部调用)"""
try:
# 移除控制字符(保留换行、制表符)
cleaned = ''.join(
char for char in text
if unicodedata.category(char)[0] != 'C' or char in '\n\t'
)
# 移除特定无法编码的字符(如右至左标记)
special_chars = ['\u202d', '\u202c', '\u202a', '\u202b', '\u200b']
for char in special_chars:
cleaned = cleaned.replace(char, '')
# 替换全角空格为半角空格
cleaned = cleaned.replace('\u3000', ' ')
return cleaned
except:
return text
def get_safe_encoding(file_path=None):
"""获取安全的编码格式(优先UTF-8-SIG)"""
if file_path and os.path.exists(file_path):
with open(file_path, 'rb') as f:
raw_data = f.read(10000)
result = detect(raw_data)
detected = result['encoding']
if detected in ['utf-8', 'utf-8-sig', 'gbk', 'gb2312']:
return 'utf-8-sig'
return 'utf-8-sig'
# ==============================
# 第一步:生成 expanded_yd_data.csv(彻底修复数组问题)
# ==============================
def generate_expanded_csv(output_dir):
"""生成展开后的源文件(支持数组类型数据)"""
os.makedirs(output_dir, exist_ok=True)
converted_csv_path = os.path.join(output_dir, "converted_yd_data.csv")
if not os.path.exists(converted_csv_path):
print(f"❌ 错误:converted_yd_data.csv 不存在,路径:{converted_csv_path}")
return None
# 读取文件(兼容不同编码)
encoding = get_safe_encoding(converted_csv_path)
try:
df = pd.read_csv(converted_csv_path, encoding=encoding)
print(f"✅ 成功读取 converted_yd_data.csv(编码:{encoding}),数据规模:{df.shape[0]}× {df.shape[1]}")
except Exception as e:
print(f"❌ 读取 converted_yd_data.csv 失败:{str(e)}")
return None
if 'data' not in df.columns:
print(f"❌ 错误:converted_yd_data.csv 中缺少 'data'")
return None
# 清理非data列的特殊字符(仅处理字符串列)
non_data_cols = [col for col in df.columns if col != 'data']
for col in non_data_cols:
if df[col].dtype == 'object': # 仅处理字符串类型列
df[col] = df[col].apply(clean_special_characters)
# 检查并解析 data 列(核心:处理数组类型)
sample = df['data'].dropna().iloc[0] if not df['data'].dropna().empty else ""
print("Sample of 'data' column (after cleaning):")
print(repr(str(sample))[:200]) # 转为字符串避免数组打印过长
if isinstance(sample, str):
print("Detected string format, parsing with ast.literal_eval...")
def safe_literal_eval(x):
if is_empty_value(x):
return {}
try:
# 先清理字符串,再解析
cleaned_x = clean_special_characters(x) if isinstance(x, str) else str(x)
parsed = ast.literal_eval(cleaned_x)
# 解析后再次清理(处理数组中的特殊字符)
return clean_special_characters(parsed)
except (ValueError, SyntaxError, TypeError) as e:
print(f"Parse error on: {repr(str(x))[:100]}... Error: {str(e)[:50]}")
return {}
df['data'] = df['data'].apply(safe_literal_eval)
# 展开 data 列(处理数组类型,转为字符串存储)
def flatten_expanded_data(expanded_df):
"""展平数据,将数组转为字符串"""
for col in expanded_df.columns:
if expanded_df[col].dtype == 'object':
expanded_df[col] = expanded_df[col].apply(
lambda x: ','.join(map(str, x)) if isinstance(x, (list, tuple)) else x
)
return expanded_df
expanded = pd.json_normalize(df['data'])
expanded = flatten_expanded_data(expanded) # 数组转字符串(如 ['a','b'] → "a,b"
# 清理展开后的数据
for col in expanded.columns:
expanded[col] = expanded[col].apply(clean_special_characters)
# 合并数据
other_cols = df.drop(columns=['data'])
new_df = pd.concat([other_cols.reset_index(drop=True), expanded.reset_index(drop=True)], axis=1)
# 数据过滤(修复数组转字符串后的判断逻辑)
if 'instanceStatus' in new_df.columns:
# 确保过滤条件处理字符串
new_df['instanceStatus'] = new_df['instanceStatus'].astype(str)
new_df = new_df[new_df["instanceStatus"].str.strip() == "RUNNING"]
print(f"✅ 过滤后(instanceStatus=RUNNING):{new_df.shape[0]}")
else:
print(f"⚠️ 警告:缺少 'instanceStatus' 列,跳过状态过滤")
col = "textField_kto3q3ev"
if col in new_df.columns:
# 处理数组转字符串后的空值判断
new_df[col] = new_df[col].apply(
lambda x: ','.join(map(str, x)) if isinstance(x, (list, tuple)) else x
).astype(str)
mask2 = (new_df[col].str.strip() == "") | (new_df[col].str.strip() == "nan")
new_df = new_df[mask2]
print(f"✅ 过滤后({col}为空):{new_df.shape[0]}")
else:
print(f"⚠️ 警告:缺少 '{col}' 列,跳过订单编码过滤")
# 保存文件(UTF-8-SIG编码)
expanded_csv_path = os.path.join(output_dir, "expanded_yd_data.csv")
try:
new_df.to_csv(
expanded_csv_path,
index=False,
encoding='utf-8-sig',
na_rep='',
errors='replace'
)
print(f"✅ Expanded data saved to: {expanded_csv_path}(编码:utf-8-sig")
return expanded_csv_path
except Exception as e:
print(f"❌ 保存 expanded_yd_data.csv 失败:{str(e)}")
return None
# ==============================
# 第二步:格式转换核心函数(兼容数组处理结果)
# ==============================
def convert_to_data_ngv_format(
source_csv_path,
target_csv_path,
main_output_path,
additional_output_path,
doc_output_path
):
print("\n=== 开始格式转换(目标格式:data_NGV.csv===")
try:
# 读取目标文件
if not os.path.exists(target_csv_path):
raise FileNotFoundError(f"目标文件不存在:{target_csv_path}")
# 兼容目标文件的编码
try:
df_target = pd.read_csv(target_csv_path, encoding='utf-8-sig')
target_encoding = 'utf-8-sig'
except:
df_target = pd.read_csv(target_csv_path, encoding='gbk')
target_encoding = 'gbk'
print(f"✅ 成功读取目标文件:{target_csv_path}(编码:{target_encoding}")
print(f" 数据规模:{df_target.shape[0]}× {df_target.shape[1]}")
# 读取源文件(已处理数组问题)
df_source = pd.read_csv(source_csv_path, encoding='utf-8-sig')
print(f"✅ 成功读取源文件:{source_csv_path}(编码:utf-8-sig")
print(f" 数据规模:{df_source.shape[0]}× {df_source.shape[1]}")
except Exception as e:
print(f"❌ 文件读取失败:{str(e)}")
return False
# 字段映射(适配 data_NGV 格式)
print("\n=== 建立字段映射关系 ===")
field_mapping = {
'createTimeGMT': 'saas_create_time',
'modifiedTimeGMT': 'etl_time',
'title': 'org_remark',
'instanceStatus': 'active_status_fmt',
'processInstanceId': 'id_own_org',
'actionExecutor': 'technician',
'originator': 'salesmen',
'textField_kuj8nx00': 'province_name',
'textField_kuj8nx01': 'city_name'
}
# 筛选有效映射
valid_mapping = {}
for source_field, target_field in field_mapping.items():
if target_field in df_target.columns and source_field in df_source.columns:
valid_mapping[source_field] = target_field
print(f" {source_field}{target_field}")
if len(valid_mapping) == 0:
print(f"⚠️ 警告:未找到有效字段映射,将仅填充默认值")
# 字段分类
target_columns = set(df_target.columns)
source_columns = set(df_source.columns)
additional_columns = list(source_columns - target_columns - set(valid_mapping.keys()))
print(f"\n=== 字段统计 ===")
print(f" 目标文件总字段数:{len(target_columns)}")
print(f" 源文件总字段数:{len(source_columns)}")
print(f" 有效映射字段数:{len(valid_mapping)}")
print(f" 额外字段数(放入单独文件):{len(additional_columns)}")
# 创建结果数据结构
df_main = df_target.iloc[0:0].copy()
df_main.insert(0, 'main_file_id', '') # 关联ID列
df_additional = pd.DataFrame(columns=['main_file_id'] + additional_columns)
# 数据处理工具函数(兼容字符串化的数组)
def extract_org_info(title_text):
if is_empty_value(title_text):
return '', ''
title_str = str(title_text).strip()
org_name_match = re.search(r'门店名称:([^,\n]+)', title_str)
org_code_match = re.search(r'门店编码:([^,\n]+)', title_str)
org_name = org_name_match.group(1).strip() if org_name_match else ''
org_code = org_code_match.group(1).strip() if org_code_match else ''
return org_name, org_code
def extract_technician(executor_text):
if is_empty_value(executor_text):
return ''
executor_str = str(executor_text).strip()
# 匹配字符串化的数组中的中文姓名(如 "{'nameInChinese':'何钊'}"
name_match = re.search(r"'nameInChinese':\s*'([^']+)'", executor_str)
return name_match.group(1).strip() if name_match else ''
def convert_gmt_time(gmt_str):
if is_empty_value(gmt_str):
return None
try:
time_str = str(gmt_str).replace('T', ' ').replace('Z', '').strip()
return pd.to_datetime(time_str)
except:
return None
# 逐行处理数据
print(f"\n=== 开始数据处理(共{len(df_source)}行) ===")
for idx, source_row in df_source.iterrows():
main_file_id = f"REC-{idx:06d}"
# 处理主文件数据
main_row = pd.Series(index=df_main.columns, dtype='object')
main_row['main_file_id'] = main_file_id
# 填充映射字段(处理字符串化的数组)
for source_field, target_field in valid_mapping.items():
value = source_row[source_field]
if not is_empty_value(value):
# 将字符串化的数组转为普通字符串(如 "a,b" → "a,b"
if isinstance(value, (list, tuple)):
main_row[target_field] = ','.join(map(str, value))
else:
main_row[target_field] = str(value).strip()
else:
main_row[target_field] = ''
# 处理关键业务字段
create_time = convert_gmt_time(source_row.get('createTimeGMT'))
if create_time:
main_row['date_fmt'] = create_time.strftime('%Y/%m/%d')
main_row['date_id'] = int(create_time.strftime('%Y%m%d'))
main_row['pt'] = main_row['date_id']
# 提取门店信息
org_name, org_code = extract_org_info(source_row.get('title'))
if org_name:
main_row['org_name'] = org_name
main_row['group_name'] = org_name
if org_code:
main_row['org_code'] = org_code
# 提取处理人
technician = extract_technician(source_row.get('actionExecutor'))
if technician:
main_row['technician'] = technician
# 填充默认值
default_values = {
'org_type': '一般',
'org_status': '留存',
'group_grade': '普通客户(VIP',
'is_active': 1,
'active_status_fmt': '活跃',
'province_name': main_row.get('province_name', '未知'),
'city_name': main_row.get('city_name', '未知'),
'area_name': '未知',
'is_wechat': 0,
'is_mini_app': 0,
'id_own_group': 0,
'org_code': main_row.get('org_code', '')
}
for col, default_val in default_values.items():
if col in main_row.index and is_empty_value(main_row[col]):
main_row[col] = default_val
df_main.loc[idx] = main_row
# 处理额外字段文件
additional_row = pd.Series(index=df_additional.columns, dtype='object')
additional_row['main_file_id'] = main_file_id
for col in additional_columns:
if col in source_row.index:
value = source_row[col]
if not is_empty_value(value):
# 统一转为字符串存储(兼容数组)
if isinstance(value, (list, tuple)):
additional_row[col] = ','.join(map(str, value))
else:
additional_row[col] = str(value).strip()
else:
additional_row[col] = ''
df_additional.loc[idx] = additional_row
# 进度提示(大数据量优化)
if (idx + 1) % 500 == 0 or (idx + 1) == len(df_source):
print(f" 已处理 {idx + 1}/{len(df_source)}")
# 数据类型优化
print(f"\n=== 优化数据类型 ===")
numeric_cols = ['date_id', 'pt', 'is_active', 'is_wechat', 'is_mini_app',
'id_own_group', 'active_user_count', 'limit_user_count']
for col in numeric_cols:
if col in df_main.columns:
# 处理字符串格式的数值
df_main[col] = pd.to_numeric(
df_main[col].astype(str).str.replace(',', ''), # 移除数组转字符串的逗号
errors='coerce'
).fillna(0).astype(int)
# 填充空值
df_main = df_main.fillna('')
df_additional = df_additional.fillna('')
# 保存文件
print(f"\n=== 保存结果文件 ===")
try:
# 保存主文件(匹配 data_NGV 格式)
df_main.to_csv(
main_output_path,
index=False,
encoding='utf-8-sig',
na_rep='',
errors='replace'
)
print(f"✅ 主文件保存成功: {main_output_path}(编码:utf-8-sig")
# 保存额外字段文件
df_additional.to_csv(
additional_output_path,
index=False,
encoding='utf-8-sig',
na_rep='',
errors='replace'
)
print(f"✅ 额外字段文件保存成功: {additional_output_path}(编码:utf-8-sig")
# 生成说明文档
generate_relation_doc(doc_output_path, main_output_path, additional_output_path,
len(target_columns), len(source_columns), len(valid_mapping))
print(f"✅ 关联说明文档保存成功: {doc_output_path}")
return True
except Exception as e:
print(f"❌ 文件保存失败: {str(e)}")
return False
# ==============================
# 辅助函数:生成关联说明文档
# ==============================
def generate_relation_doc(doc_path, main_file, additional_file, target_col_count, source_col_count, mapping_count):
doc_content = f"""# 数据格式转换结果说明
## 目标格式文件:data_NGV.csv
### 一、文件概述
1. **主文件匹配目标格式**
- 文件名{os.path.basename(main_file)}
- 格式来源完全匹配 data_NGV.csv 结构
- 数据规模{pd.read_csv(main_file, encoding='utf-8-sig').shape[0]} × {target_col_count + 1}
- 编码格式UTF-8-SIG兼容所有字符和数组数据
2. **额外字段文件**
- 文件名{os.path.basename(additional_file)}
- 包含内容源文件中 data_NGV.csv 没有的字段
- 数据规模{pd.read_csv(additional_file, encoding='utf-8-sig').shape[0]} × {pd.read_csv(additional_file, encoding='utf-8-sig').shape[1]}
- 编码格式UTF-8-SIG
### 二、关键处理说明
1. **数组数据处理**源文件中的数组 ['a','b']已转为字符串"a,b"存储避免格式错误
2. **特殊字符清理**自动移除无法编码的控制字符如右至左标记 \u202d
3. **编码统一**所有文件使用 UTF-8-SIG 编码兼容中文和特殊字符
### 三、关联方法
1. **关联字段**`main_file_id`格式REC-000000
2. **Excel导入**数据自文本/CSV选择文件编码选择"UTF-8"完成
### 四、使用建议
1. 主文件可直接用于业务系统 data_NGV.csv 格式完全兼容
2. 额外字段文件用于原始数据追溯通过 main_file_id 关联
3. 数组转字符串后的数据可通过 Excel "文本分列"功能恢复为数组
"""
with open(doc_path, 'w', encoding='utf-8') as f:
f.write(doc_content)
# ==============================
# 主执行函数
# ==============================
def main():
# 配置文件路径(请根据您的实际位置修改!)
base_dir = "D:\\Idea Project\\SaaS_V1.7\\test\\output"
target_csv_path = "D:\\Idea Project\\SaaS_V1.7\\test\\output\\data_NGV.csv"
# 生成 expanded_yd_data.csv(解决数组问题)
expanded_csv_path = generate_expanded_csv(base_dir)
if not expanded_csv_path:
print("❌ 生成 expanded_yd_data.csv 失败,终止转换")
return
# 配置输出路径
main_output_path = os.path.join(base_dir, "主文件_匹配data_NGV格式.csv")
additional_output_path = os.path.join(base_dir, "额外字段文件_源文件特有列.csv")
doc_output_path = os.path.join(base_dir, "格式转换说明.md")
# 执行转换
success = convert_to_data_ngv_format(
source_csv_path=expanded_csv_path,
target_csv_path=target_csv_path,
main_output_path=main_output_path,
additional_output_path=additional_output_path,
doc_output_path=doc_output_path
)
if success:
print(f"\n🎉 格式转换全部完成!")
print(f"📁 输出目录:{base_dir}")
print(f"🔔 重要:Excel导入时需选择'UTF-8'编码,数组数据已转为逗号分隔字符串")
else:
print(f"\n❌ 格式转换失败,请查看日志信息排查问题")
# ==============================
# 执行入口
# ==============================
if __name__ == "__main__":
# API模块导入(不影响核心功能)
try:
from yd_api import YDAPI
from api import API
print("✅ 成功导入 API 模块")
except ImportError:
print("⚠️ 警告:未找到 yd_api 或 api 模块,跳过API初始化(不影响格式转换)")
# 执行主流程
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,241 @@
import os
import sys
import pandas as pd
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)
# 加载数据
# 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"
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)
ndf = pd.DataFrame(all_instance_data)
ndf.to_csv(r"D:\Idea Project\SaaS_V1.7\\test\output\yd_process_details.csv", index=False)
# 读取宜搭流程详情(已提前导出)
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",
}
# 宜搭字段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天是否跟进": {"小六": "主动", "系统": "自动"},
}
# ========================
# 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", [])
# 构建映射字典:姓名 -> 员工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
logger.info(f"加载 {len(name_to_staff_id)} 名员工信息")
# ========================
# 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)
+200
View File
@@ -0,0 +1,200 @@
import os
from datetime import datetime, timezone, timedelta
import pandas as pd
from holidays.countries import saint_martin as record
from tqdm import tqdm
import json
from yd_api import YDAPI
from api import API
import time
output_dir = "output"
os.makedirs(output_dir, exist_ok=True)
api_instance = API()
yd_api_instance = YDAPI()
def generate_monthly_ranges(start: str, end: str):
"""
生成按自然月划分的时间段列表左闭右开
例如: [('2025-11-01T00:00:00Z', '2025-12-01T00:00:00Z'), ...]
"""
start_dt = datetime.fromisoformat(start.replace("Z", "+00:00"))
end_dt = datetime.fromisoformat(end.replace("Z", "+00:00"))
ranges = []
current = start_dt
while current < end_dt:
# 下一个月的第一天
if current.month == 12:
next_month = current.replace(year=current.year + 1, month=1, day=1)
else:
next_month = current.replace(month=current.month + 1, day=1)
# 不超过 end_dt
segment_end = min(next_month, end_dt)
ranges.append((
current.strftime("%Y-%m-%dT00:00:00Z"),
segment_end.strftime("%Y-%m-%dT00:00:00Z")
))
current = next_month
return ranges
class GetYDData:
def __init__(self):
self.FORMID = "FORM-PE866MD1MJMU0WGLYRFLYEN5YN9L1I55Z7ZUK22"
self.appType = "APP_UYZ0KG6L0CCNV80GZ66O"
self.systemToken = "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2"
# 第一段:2025-01-01 到 2025-11-01
first_segment = ("2025-01-01T00:00:00Z", "2025-02-01T00:00:00Z")
# 第二段:2025-11-01 到当前时间(按月拆分)
now_utc_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
monthly_segments = generate_monthly_ranges("2025-02-01T00:00:00Z", now_utc_str)
# 合并所有时间段
self.time_ranges = [first_segment] + monthly_segments
print("📅 计划拉取以下时间段:")
for i, (s, e) in enumerate(self.time_ranges, 1):
print(f" {i}. {s}{e}")
def build_value_to_label_map(self, form_structure):
value_to_label_map = {}
fields = form_structure.get("result", [])
for field in fields:
field_id = field.get("fieldId")
component = field.get("componentName")
props = field.get("props", {})
data_source = props.get("dataSource", [])
if component in ["SelectField", "RadioField"] and data_source:
option_map = {}
for opt in data_source:
val = opt.get("value")
if val is None:
continue
text_obj = opt.get("text", {})
if isinstance(text_obj, dict):
zh_text = text_obj.get("zh_CN")
if zh_text is None and "value" in text_obj:
raw = text_obj["value"]
if isinstance(raw, str) and raw.startswith('"') and raw.endswith('"'):
zh_text = raw[1:-1]
else:
zh_text = str(val)
elif zh_text is None:
zh_text = str(val)
else:
zh_text = str(text_obj)
option_map[str(val)] = zh_text
if option_map:
value_to_label_map[field_id] = option_map
return value_to_label_map
def convert_record_values(self, record, value_map):
converted = {}
for key, val in record.items():
if key in value_map and val is not None:
str_val = str(val)
converted[key] = value_map[key].get(str_val, val)
else:
converted[key] = val
return converted
def fetch_records_in_range(self, token, start_time, end_time):
"""拉取指定时间范围内的所有记录"""
try:
first_page = yd_api_instance.read_processes_instances(
token=token,
formUuid=self.FORMID,
page=1,
n=100,
appType=self.appType,
systemToken=self.systemToken,
instanceStatus="",
modifiedFromTimeGMT=start_time,
modifiedToTimeGMT=end_time,
)
except Exception as e:
print(f"❌ 首页请求失败 ({start_time} {end_time}): {e}")
return []
total_count = first_page.get("totalCount", 0)
total_pages = (total_count // 100) + (1 if total_count % 100 else 0)
print(f"📊 [{start_time[:10]} {end_time[:10]}] 总记录数: {total_count}, 共 {total_pages}")
all_records = []
if total_count > 0:
all_records.extend(first_page.get("data", []))
for page in tqdm(range(2, total_pages + 1), desc=f"{start_time[:7]}"):
try:
resp = yd_api_instance.read_processes_instances(
token=token,
formUuid=self.FORMID,
page=page,
n=100,
appType=self.appType,
systemToken=self.systemToken,
instanceStatus="",
modifiedFromTimeGMT=start_time,
modifiedToTimeGMT=end_time,
)
page_data = resp.get("data", [])
all_records.extend(page_data)
time.sleep(0.15) # 稍微增加间隔,更安全
except Exception as e:
print(f"⚠️ 第 {page} 页失败 ({start_time[:10]}): {e}")
continue
return all_records
def main(self):
# Step 1: 获取表单结构
token = yd_api_instance.generateToken()
form_struct = yd_api_instance.get_form_structures(
token=token,
formUuid=self.FORMID
)
value_map = self.build_value_to_label_map(form_struct)
print("\n✅ 表单选项映射构建完成")
# Step 2: 按时间段拉取
all_records = []
all_records_detils = []
for start_time, end_time in self.time_ranges:
print(f"\n⏳ 拉取: {start_time}{end_time}")
records = self.fetch_records_in_range(token, start_time, end_time)
all_records.extend(records)
try:
record_data = record.get("data", [])
all_records_detils.extend(record_data)
except Exception as e:
continue
print(f"\n📥 总共获取 {len(all_records)} 条流程实例")
# # Step 3: 转换 formData
converted_records = []
for inst in all_records:
form_data = inst.get("formData", {})
converted = self.convert_record_values(form_data, value_map)
converted_records.append(converted)
# Step 4: 保存
if all_records:
df = pd.DataFrame(all_records)
output_path = os.path.join(output_dir, "converted_yd_data.csv")
df.to_csv(output_path, index=False)
df1 = pd.DataFrame(all_records_detils)
output_path1 = os.path.join(output_dir, "converted_yd_data_detail.csv")
df1.to_csv(output_path1, index=False)
print(f"\n✅ 成功保存 {len(all_records)} 条记录至: {output_path}")
else:
print("\n❌ 无有效数据")
if __name__ == "__main__":
GetYDData().main()
+445
View File
@@ -0,0 +1,445 @@
# -*- coding: utf-8 -*-
import sys
import io
import imaplib
import email
import re
from datetime import datetime, timedelta
from email.header import decode_header
import pandas as pd
# 假设 api.py 在当前目录下,且包含 API 类
import requests
from typing import Optional, List, Dict, Any
from decimal import Decimal
import time
import numpy as np
from log_config import configure_task_logger, configure_error_task_logger
import json
# === 强制标准输出为 UTF-8 (兼容不同运行环境) ===
# 注意:在部分 IDE 中重新包装 sys.stdout 可能会导致乱码,若报错可注释掉以下两行
try:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
except AttributeError:
pass
# ================= 配置区域 =================
EMAIL_ACCOUNT = "zhangyang@f6car.cn"
PASSWORD = "RGBdMggmJ4s2FzZK" # ⚠️ 生产环境建议使用环境变量,不要硬编码
IMAP_SERVER = "imap.qiye.aliyun.com"
IMAP_PORT = 993
SUBJECT_KEYWORD = "展会线索登记"
DAYS_TO_SCAN = 30 # 扫描最近30天
OUTPUT_FILE = f"展会线索_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
# 定义标准字段顺序
FIELD_KEYS = ["姓名", "手机号", "", "", "", "公司名称", "备注"]
class API:
def entry_data_list(self, data: dict, replace: bool = False, max_retries: int = 20) -> Dict: # 获取多条表单数据
"""
获取多条表单数据
:param max_retries: 最大重试次数
:param replace: 是否替换字段
:param data:
api_key: 应用id
entry_id: 表单id
:return:
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list'
headers = {
'Authorization': "Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN", # 曹伟应用api测试 app_key
'Content-Type': 'application/json'
}
all_data_batches = [] # 用于存储每次请求返回的数据批次
last_data_id = None
exit_flag = False
while True:
payload = json.dumps({
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"limit": 90,
"data_id": last_data_id,
"filter": data.get('filter', None)
})
retries = 0
while retries <= max_retries:
data_get = None
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
if data_get["data"]:
all_data_batches.extend(data_get['data'])
last_data_id = data_get['data'][-1].get('_id')
print(f"已获取 {len(all_data_batches)} 条数据")
break # 成功则跳出循环
else:
if 'data' not in data_get or len(data_get['data']) == 0:
exit_flag = True
break
retries += 1
time.sleep(0.5) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
retries += 1
time.sleep(0.5) # 在重试之间稍作停顿
if retries > max_retries:
all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理
if exit_flag:
break
# 构建最终返回的字典
final_data = {
'data': all_data_batches # 'data' 键对应的值是列表的列表
}
return final_data
@staticmethod
def data_batch_create(data: dict, max_retries: int = 20) -> Optional[requests.Response]: # 新建单条数据
"""
新建单条表单数据
:param max_retries: 最大重试次数
:param data: 应该包含应用id表单id以及新建的数据data['data']
:return: 返回创建后简道云返回的信息
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/create'
headers = {
'Authorization': "Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN", # 曹伟应用api测试 app_key
'Content-Type': 'application/json'
}
# noinspection DuplicatedCode
payload = json.dumps({
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"data": data['data'],
"is_start_workflow": data.get('is_start_workflow', "false"),
"is_start_trigger": data.get('is_start_trigger', "false"),
"transaction_id": data.get('transaction_id', "")
}
)
retries = 0
while retries <= max_retries:
try:
res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
if res.status_code == 200:
return data_get
else:
retries += 1
time.sleep(3) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
retries += 1
time.sleep(3) # 在重试之间稍作停顿
if retries > max_retries:
print(
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
return None
# ===========================================
def decode_mime_words(s):
if not s:
return ""
decoded_parts = []
# decode_header 返回的是 list of (bytes/str, encoding)
for part, encoding in decode_header(s):
if isinstance(part, bytes):
decoded_parts.append(part.decode(encoding or 'utf-8', errors='ignore'))
else:
decoded_parts.append(str(part))
return "".join(decoded_parts)
def extract_data_from_body(body_text):
"""
从邮件正文中提取线索数据
格式姓名 | 手机号 | | | | 公司 | 备注
"""
if not body_text:
return []
data_list = []
# 【修复点 1】splitlines() 是方法,需要加括号
lines = body_text.splitlines()
for line in lines:
line = line.strip()
# 如果行中没有分隔符,跳过
if '|' not in line:
continue
# 按 '|' 分割并去除首尾空格
parts = [p.strip() for p in line.split('|')]
# 【关键校验】至少需要前两个字段(姓名、手机号)非空
if len(parts) < 2 or not parts[0] or not parts[1]:
continue
# 构建字典,动态映射
record = {}
for i, key in enumerate(FIELD_KEYS):
if i < len(parts):
record[key] = parts[i]
else:
record[key] = "" # 缺失的字段填空字符串
data_list.append(record)
return data_list
def save_to_excel(leads, filename):
if not leads:
return None
df = pd.DataFrame(leads)
# 定义期望的列顺序
cols = ["姓名", "手机号", "", "", "", "公司名称", "备注", "来源邮件时间"]
# 确保列存在且顺序正确
# 先保留所有现有列中在 cols 里的,按 cols 顺序
ordered_cols = [c for c in cols if c in df.columns]
# 再加上可能存在的其他列(虽然逻辑上不应该有,但以防万一)
other_cols = [c for c in df.columns if c not in cols]
final_cols = ordered_cols + other_cols
df = df[final_cols]
# df.to_excel(filename, index=False)
return df
def main():
print(f"正在连接 IMAP 服务器:{IMAP_SERVER} ...")
mail = None
start_date = datetime.now() - timedelta(days=DAYS_TO_SCAN)
date_str = start_date.strftime("%d-%b-%Y").upper()
all_leads = []
count_processed = 0
try:
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
mail.login(EMAIL_ACCOUNT, PASSWORD)
mail.select("INBOX")
print(f"正在搜索 [{date_str}] 之后的邮件...")
search_query = f'(SINCE "{date_str}")'
status, messages = mail.search(None, search_query)
if status != "OK":
print("❌ 搜索失败")
return
mail_ids = messages[0].split()
if not mail_ids:
print(f"✅ 未找到 {date_str} 之后的新邮件。")
return
print(f"📩 找到 {len(mail_ids)} 封近期邮件,开始详细扫描...")
for mail_id in mail_ids:
try:
status, msg_data = mail.fetch(mail_id, "(RFC822)")
if status != "OK":
continue
raw_email = msg_data[0][1]
if isinstance(raw_email, bytes):
mime_msg = email.message_from_bytes(raw_email)
else:
mime_msg = email.message_from_string(raw_email.decode('utf-8', errors='ignore'))
subject = decode_mime_words(mime_msg.get("Subject"))
if SUBJECT_KEYWORD not in subject:
continue
count_processed += 1
date_str_full = mime_msg.get("Date")
body_content = ""
if mime_msg.is_multipart():
for part in mime_msg.walk():
content_disposition = part.get_content_disposition()
if content_disposition and "attachment" in str(content_disposition):
continue
content_type = part.get_content_type()
if content_type in ["text/plain", "text/html"]:
try:
charset = part.get_content_charset() or 'utf-8'
payload = part.get_payload(decode=True)
if payload:
text = payload.decode(charset, errors='ignore') if isinstance(payload,
bytes) else str(
payload)
if content_type == "text/html":
text = re.sub(r'<[^>]+>', ' ', text)
body_content += text + "\n"
except Exception:
pass
else:
try:
charset = mime_msg.get_content_charset() or 'utf-8'
payload = mime_msg.get_payload(decode=True)
if payload:
body_content = payload.decode(charset, errors='ignore') if isinstance(payload,
bytes) else str(
payload)
except Exception:
pass
leads = extract_data_from_body(body_content)
for lead in leads:
lead["来源邮件时间"] = date_str_full
if leads:
print(f"[{subject}] -> 提取 {len(leads)}")
all_leads.extend(leads)
except Exception as e:
print(f"处理邮件 ID {mail_id} 时出错:{e}")
continue
# ================= 新增:本地数据去重逻辑 =================
original_count = len(all_leads)
if original_count > 0:
seen_phones = set()
unique_leads = []
for lead in all_leads:
phone = str(lead.get("手机号", "")).strip()
# 如果手机号为空,或者已经出现过,则跳过
if not phone or phone in seen_phones:
continue
seen_phones.add(phone)
unique_leads.append(lead)
all_leads = unique_leads
removed_count = original_count - len(all_leads)
if removed_count > 0:
print(
f"\n⚠️ 检测到重复数据,已根据【手机号】去重:原始 {original_count} 条 -> 去重后 {len(all_leads)} 条 (移除 {removed_count} 条)")
else:
print(f"\n✅ 数据检查完成,无重复手机号。共 {len(all_leads)} 条。")
# =======================================================
df = save_to_excel(all_leads, OUTPUT_FILE)
if df is not None:
print(f"\n✅ 成功!共扫描 {count_processed} 封匹配邮件,最终有效线索 {len(all_leads)} 条。")
print(f"文件已保存至:{OUTPUT_FILE}")
else:
print(f"\n⚠️ 扫描完成,但在 {count_processed} 封近期邮件中未找到符合格式的数据。")
return # 如果没有数据,后续同步逻辑无需执行
# 同步至简道云
if all_leads:
print("\n开始同步至简道云...")
api_instance = API()
payload_query = {
"api_key": "66b9678280b37f8a276b1d01",
"entry_id": "69b22dc5434e05c7b6b4b5b2",
}
try:
response = api_instance.entry_data_list(payload_query)
now_data = response.get("data", []) if response else []
existing_phones = set()
phone_widget_id = "_widget_1692928669587"
for item in now_data:
phone_val = item.get(phone_widget_id)
if phone_val:
existing_phones.add(str(phone_val).strip())
print(f"简道云现有手机号数量:{len(existing_phones)}")
new_count = 0
# 此时 df 已经是去重后的数据,且 all_leads 也是去重后的
# 再次遍历 df 确保只提交本地没有的(防止简道云已有但本地没查到的情况,虽然逻辑上上面已经过滤了)
# 为了代码健壮性,这里保留原有的 existing_phones 检查逻辑
for index, row in df.iterrows():
current_phone = str(row["手机号"]).strip()
if not current_phone:
continue
# 双重保险:如果简道云里已经有了,跳过
if current_phone in existing_phones:
print(f"跳过 (云端已存在): {current_phone}")
continue
new_payload = {
"api_key": "66b9678280b37f8a276b1d01",
"entry_id": "69b22dc5434e05c7b6b4b5b2",
"data": {
"_widget_1690785229260": {"value": row.get("姓名", "")},
"_widget_1690785229261": {"value": row.get("公司名称", "")},
"_widget_1692928669587": {"value": row.get("手机号", "")},
"_widget_1690785229266": {"value": row.get("备注", "")},
"_widget_1690785326597": {"value": {"province": row.get("", ""),
"city": row.get("", ""),
"district": row.get("", ""),
"detail": row.get("", "") + row.get("",
"") + row.get(
"", ""),
}
},
"_widget_1690785229279": {"value": row.get("", "")},
"_widget_1773381838511": {"value": row.get("", "")},
"_widget_1692070309987": {"value": row.get("", "")},
}
}
result = api_instance.data_batch_create(new_payload)
if result and (result.get("success") or result.get("code") == 200 or "error" not in result):
new_count += 1
print(f"新增成功:{current_phone} ({row.get('姓名')})")
else:
print(f"提交结果:{current_phone}, 返回:{result}")
# 如果提交成功但返回格式奇怪,也可以考虑计入成功,视具体API文档而定
# 这里保守处理,只有明确成功才算
print(f"\n✅ 同步完成,本次新增 {new_count} 条数据。")
except Exception as api_err:
print(f"\n❌ 简道云 API 交互错误:{api_err}")
import traceback
traceback.print_exc()
except imaplib.IMAP4.error as e:
print("\n❌ IMAP 协议错误:")
print(f"错误详情:{e}")
except Exception as e:
print("\n❌ 发生严重错误:")
import traceback
traceback.print_exc()
finally:
if mail:
try:
mail.close()
mail.logout()
except:
pass
if __name__ == "__main__":
main()
+158
View File
@@ -0,0 +1,158 @@
{
"cells": [
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2026-01-07T02:17:11.661841100Z",
"start_time": "2026-01-07T02:17:11.600589500Z"
}
},
"source": [
"from back_ground_module import CommonModule\n",
"from api import API\n",
"from log_config import configure_task_logger, configure_error_task_logger\n",
"from datetime import datetime, timedelta, timezone\n",
"import pandas as pd\n",
"import os\n",
"\n",
"# 获取已经配置好的常规日志记录器\n",
"logger = configure_task_logger()\n",
"# 获取已经配置好的错误任务日志记录器\n",
"error_task_logger = configure_error_task_logger()\n",
"# 保存为CSV文件\n",
"output_dir = \"output\" # 设置输出目录\n",
"# 创建输出目录(如果不存在)\n",
"import os\n",
"\n",
"os.makedirs(output_dir, exist_ok=True)\n",
"common_module = CommonModule()\n",
"api_instance = API()\n",
"\n",
"data_JCB = common_module.get_jcb_details()\n",
"current_local = datetime.now() + timedelta(days=-1) # tz-naive,代表本地时间\n",
"current_date_str = current_local.strftime(\"%Y-%m-%d\")\n",
"# 计算30天前的本地日期(用于开户日判断)\n",
"thirty_days_ago_local = (current_local - timedelta(days=30)).date()\n",
"payload = {\"api_key\": \"6717470a0b3975ef583c6df1\",\n",
" \"entry_id\": \"67174710da507490d8ac12c1\",\n",
" }\n",
"daily_revisit = api_instance.entry_data_list(payload)\n",
"daily_revisit_list = daily_revisit.get(\"data\") # api请求格式,将数据封装在data字典里\n",
"abnormal_data = []\n",
"for index, row in data_JCB.iterrows():\n",
" try:\n",
" # 开户日是本地日期字符串,解析为 date 对象\n",
" open_date = datetime.strptime(str(row['开户日']), \"%Y-%m-%d\").date()\n",
" except (ValueError, TypeError):\n",
" continue # 跳过无效日期\n",
"\n",
" if (\n",
" open_date < thirty_days_ago_local\n",
" and row['近30天开单天数'] == 0\n",
" and row['客户状态'] == \"留存\"\n",
" ):\n",
" new_row = row.copy()\n",
" new_row[\"日期\"] = open_date.strftime(\"%Y-%m-%d\")\n",
" abnormal_data.append(new_row)\n",
"\n",
"abnormal_data = pd.DataFrame(abnormal_data) if abnormal_data else pd.DataFrame()\n",
"\n",
"if not abnormal_data.empty:\n",
" abnormal_data[\"表单类型\"] = \"异常待办\"\n",
" abnormal_data[\"派发日期\"] = current_date_str\n",
"\n",
" # 清洗手机号(仅去除浮点型 .0)\n",
" def clean_phone(x):\n",
" if pd.isna(x) or x == \"\" or x == \"None\":\n",
" return \"\"\n",
" s = str(x)\n",
" if s.endswith('.0') and s[:-2].isdigit():\n",
" return s[:-2]\n",
" return s\n",
"\n",
" abnormal_data['联系手机号'] = abnormal_data['联系手机号'].apply(clean_phone)\n",
"\n",
"# 构建云端已派发记录 DataFrame\n",
"df_cloud = pd.DataFrame([\n",
" {\n",
" \"数据id\": item.get(\"_id\", \"\"),\n",
" \"账号\": item.get(\"_widget_1739258942667\", \"\"),\n",
" \"提交时间\": item.get(\"createTime\", \"\"),\n",
" \"表单类型\": item.get(\"_widget_1739951204545\", \"\")\n",
" }\n",
" for item in daily_revisit_list\n",
"])\n",
"\n",
"recent_accounts = set()\n",
"if not df_cloud.empty and not abnormal_data.empty:\n",
" # 将 createTime 转为 UTC 时间(强制统一时区)\n",
" df_cloud[\"提交时间\"] = pd.to_datetime(df_cloud[\"提交时间\"], utc=True, errors=\"coerce\")\n",
" df_cloud = df_cloud.dropna(subset=[\"提交时间\"])\n",
"\n",
" # 筛选“异常待办”\n",
" df_abnormal_cloud = df_cloud[df_cloud[\"表单类型\"] == \"异常待办\"]\n",
"\n",
" if not df_abnormal_cloud.empty:\n",
" # 每个账号保留最新一条\n",
" df_recent = df_abnormal_cloud.sort_values(\"提交时间\").groupby(\"账号\", as_index=False).tail(1)\n",
"\n",
" current_utc = datetime.now(timezone.utc)\n",
" cutoff_utc = pd.Timestamp(current_utc) - pd.Timedelta(days=30)\n",
"\n",
" # 安全比较:两边都是 UTC\n",
" recent_accounts = set(df_recent[df_recent[\"提交时间\"] > cutoff_utc][\"账号\"])\n",
"\n",
"# 剔除已派发账号 + 过滤有效手机号\n",
"if not abnormal_data.empty:\n",
" abnormal_data = abnormal_data[\n",
" (~abnormal_data[\"账号\"].isin(recent_accounts)) &\n",
" (abnormal_data[\"联系手机号\"].notna()) &\n",
" (abnormal_data[\"联系手机号\"] != \"\") &\n",
" (abnormal_data[\"联系手机号\"] != \"None\")\n",
" ]\n",
"\n",
"# # 保存结果\n",
"output_path = os.path.join(output_dir, \"异常待办1.csv\")\n",
"abnormal_data.to_csv(output_path, index=False)"
],
"outputs": [
{
"ename": "ModuleNotFoundError",
"evalue": "No module named 'back_ground_module'",
"output_type": "error",
"traceback": [
"\u001B[31m---------------------------------------------------------------------------\u001B[39m",
"\u001B[31mModuleNotFoundError\u001B[39m Traceback (most recent call last)",
"\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[3]\u001B[39m\u001B[32m, line 1\u001B[39m\n\u001B[32m----> \u001B[39m\u001B[32m1\u001B[39m \u001B[38;5;28;01mfrom\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34;01mback_ground_module\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;28;01mimport\u001B[39;00m CommonModule\n\u001B[32m 2\u001B[39m \u001B[38;5;28;01mfrom\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34;01mapi\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;28;01mimport\u001B[39;00m API\n\u001B[32m 3\u001B[39m \u001B[38;5;28;01mfrom\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34;01mlog_config\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;28;01mimport\u001B[39;00m configure_task_logger, configure_error_task_logger\n",
"\u001B[31mModuleNotFoundError\u001B[39m: No module named 'back_ground_module'"
]
}
],
"execution_count": 3
}
],
"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
}
+112
View File
@@ -0,0 +1,112 @@
from back_ground_module import CommonModule
from api import API
from log_config import configure_task_logger, configure_error_task_logger
from datetime import datetime, timedelta, timezone
import pandas as pd
import os
# 获取已经配置好的常规日志记录器
logger = configure_task_logger()
# 获取已经配置好的错误任务日志记录器
error_task_logger = configure_error_task_logger()
# 保存为CSV文件
output_dir = "output" # 设置输出目录
# 创建输出目录(如果不存在)
import os
os.makedirs(output_dir, exist_ok=True)
common_module = CommonModule()
api_instance = API()
data_JCB = common_module.get_jcb_details()
output_path = os.path.join(output_dir, "借车包明细.csv")
data_JCB.to_csv(output_path, index=False)
current_local = datetime.now() + timedelta(days=-1) # tz-naive,代表本地时间
current_date_str = current_local.strftime("%Y-%m-%d")
# 计算30天前的本地日期(用于开户日判断)
thirty_days_ago_local = (current_local - timedelta(days=30)).date()
payload = {"api_key": "6717470a0b3975ef583c6df1",
"entry_id": "67174710da507490d8ac12c1",
}
daily_revisit = api_instance.entry_data_list(payload)
daily_revisit_list = daily_revisit.get("data") # api请求格式,将数据封装在data字典里
abnormal_data = []
for index, row in data_JCB.iterrows():
try:
# 开户日是本地日期字符串,解析为 date 对象
open_date = datetime.strptime(str(row['开户日']), "%Y-%m-%d").date()
except (ValueError, TypeError):
continue # 跳过无效日期
if (
open_date < thirty_days_ago_local
and row['近30天开单天数'] == 0
and row['客户状态'] == "留存"
):
new_row = row.copy()
new_row["日期"] = open_date.strftime("%Y-%m-%d")
abnormal_data.append(new_row)
abnormal_data = pd.DataFrame(abnormal_data) if abnormal_data else pd.DataFrame()
output_path = os.path.join(output_dir, "异常待办.csv")
abnormal_data.to_csv(output_path, index=False)
if not abnormal_data.empty:
abnormal_data["表单类型"] = "异常待办"
abnormal_data["派发日期"] = current_date_str
# 清洗手机号(仅去除浮点型 .0
def clean_phone(x):
if pd.isna(x) or x == "" or x == "None":
return ""
s = str(x)
if s.endswith('.0') and s[:-2].isdigit():
return s[:-2]
return s
abnormal_data['联系手机号'] = abnormal_data['联系手机号'].apply(clean_phone)
# 构建云端已派发记录 DataFrame
df_cloud = pd.DataFrame([
{
"数据id": item.get("_id", ""),
"账号": item.get("_widget_1739258942667", ""),
"提交时间": item.get("createTime", ""),
"表单类型": item.get("_widget_1739951204545", "")
}
for item in daily_revisit_list
])
output_path = os.path.join(output_dir, "异常待办云端.csv")
df_cloud.to_csv(output_path, index=False)
recent_accounts = set()
if not df_cloud.empty and not abnormal_data.empty:
# 将 createTime 转为 UTC 时间(强制统一时区)
df_cloud["提交时间"] = pd.to_datetime(df_cloud["提交时间"], utc=True, errors="coerce")
df_cloud = df_cloud.dropna(subset=["提交时间"])
# 筛选“异常待办”
df_abnormal_cloud = df_cloud[df_cloud["表单类型"] == "异常待办"]
if not df_abnormal_cloud.empty:
# 每个账号保留最新一条
df_recent = df_abnormal_cloud.sort_values("提交时间").groupby("账号", as_index=False).tail(1)
current_utc = datetime.now(timezone.utc)
cutoff_utc = pd.Timestamp(current_utc) - pd.Timedelta(days=30)
# 安全比较:两边都是 UTC
recent_accounts = set(df_recent[df_recent["提交时间"] > cutoff_utc]["账号"])
# 剔除已派发账号 + 过滤有效手机号
if not abnormal_data.empty:
abnormal_data = abnormal_data[
(~abnormal_data["账号"].isin(recent_accounts)) &
(abnormal_data["联系手机号"].notna()) &
(abnormal_data["联系手机号"] != "") &
(abnormal_data["联系手机号"] != "None")
]
# # 保存结果
output_path = os.path.join(output_dir, "异常待办1.csv")
abnormal_data.to_csv(output_path, index=False)
@@ -0,0 +1,556 @@
import datetime
import os
import time
import requests
from api import API
from back_ground_module import CommonModule
import pandas as pd
from log_config import configure_task_logger, configure_error_task_logger
api_instance = API()
common_module = CommonModule()
# start_time = datetime.datetime.now()
# 获取已经配置好的常规日志记录器
logger = configure_task_logger()
# 获取已经配置好的错误任务日志记录器
error_task_logger = configure_error_task_logger()
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
class NewExceptionTask:
"""
SaaS异常回访
"""
def __init__(self):
self.exception_service_todo = None
self.get_feature_usage = None
self.saas_create_time = None
self.index = None
self.date_one = None
self.data_yichang_S = None
self.date_list = None
self.Smart_detection = None
self.service_remind = None
self.NGV_data_list = None
self.permissions_table = None
self.staff_id_list = None
self.json_list = []
self.policy_recognition = None
self.widget_list = None
self.private_domain = None
self.public_domain = None
self.public_domain_list = None
self.different_industries = None
self.different_industries_list = None
self.groupnotification = None
self.fields_mapping = {
"门店名称": "_widget_1748241895830",
"联系人": "_widget_1748241895831",
"开户时间": "_widget_1748241895839",
"门店编码": "_widget_1748241895842",
"联系方式": "_widget_1748241895832",
"系统版本": "_widget_1748241895850",
"公司名称": "_widget_1748241895844",
"运营顾问": "_widget_1748246808679",
"区域经理": "_widget_1748246808682",
"公司等级": "_widget_1748241895846",
"运营专家": "_widget_1748246808681",
"操作模式E.L/E.S": "_widget_1748241895853",
"活跃健康状态变化": "_widget_1748241895829",
"初始日": "_widget_1748241895833",
"推进日": "_widget_1748241895834",
"异常跟进情况描述": "_widget_1748512176640",
"异常变化原因": "_widget_1748512176641",
"正常使用": "_widget_1748512176643",
"门店原因": "_widget_1748512176645",
"服务原因": "_widget_1748512176647",
"产品原因": "_widget_1748512176649",
"未正式切换": "_widget_1748512176651",
"跟进状态": "_widget_1748512176655",
"是否可激活": "_widget_1758615839701",
"是否有续约风险": "_widget_1758615839703",
"当前跟进人": "_widget_1748246808678",
"激活策略": "_widget_1758615839717",
"跟进时间": "_widget_1748512176654",
"是否跟进完成": "_widget_1751273412737",
"区域客服": "_widget_1748246808680",
"大区": "_widget_1748241895847",
"": "_widget_1748241895848",
"城市": "_widget_1748241895855",
"门店类型": "_widget_1748241895849",
"saas客户类型": "_widget_1748241895851",
"门店阶段": "_widget_1748241895852",
"提交人": "creator",
"提交时间": "createTime",
"更新时间": "updateTime"
}
def calculate_date_one(self, start_offset=0):
"""
计算从当前日期或指定偏移量的日期开始往前遍历遇到date_list中日期的次数
参数:
- start_offset: 从当前日期起始的天数偏移量默认为0即今天负数表示过去正数表示未来
返回:
- date_one: 遍历到date_list中日期的次数
"""
jdy_date = datetime.datetime.now().strftime("%Y-%m-%d")
jdy_start_time = datetime.datetime.now().strftime("%Y-%m-%d ")
# 设置起始日期
now_time = datetime.datetime.now() + datetime.timedelta(days=start_offset)
# 初始化计数器
date_one = 1
print("当前日期:", now_time.strftime("%Y-%m-%d"))
# 检查起始日期是否在date_list中
if now_time.strftime("%Y-%m-%d") in self.date_list:
date_one = 0
print("开始次数:", date_one)
else:
# 遍历日期
for i in range(1, 10):
new_date = now_time + datetime.timedelta(days=-i)
new_date_str = new_date.strftime("%Y-%m-%d")
print("遍历日期:", new_date_str)
if new_date_str in self.date_list:
date_one += 1
print("节假日期:", new_date_str)
else:
break
print("遍历次数:", date_one)
return date_one
@staticmethod
def download_url_content(url, save_path):
"""
下载指定 URL 的内容并保存到本地文件
:param url: 要下载内容的 URL
:param save_path: 保存文件的路径
"""
try:
# 发送 GET 请求以获取内容
response = requests.get(url, stream=True)
response.raise_for_status() # 如果响应状态码不是 200,抛出异常
# 确保保存目录存在
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# 将内容写入文件
with open(save_path, 'wb') as file:
for chunk in response.iter_content(chunk_size=8192): # 分块写入,避免占用过多内存
if chunk: # 过滤掉空块
file.write(chunk)
print(f"文件已成功保存到 {save_path}")
except requests.exceptions.RequestException as e:
print(f"下载失败: {e}")
except Exception as e:
print(f"发生错误: {e}")
def load_all_data(self):
"""加载所有必要的数据表"""
# 省市区人员关系表
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "676512ac3e54dc3159460c0a"}
json_dict = api_instance.entry_data_list(payload)
self.json_list = json_dict.get("data")
# 获取简道云员工id
payload = {"api_key": "6694d3c4fcb69ca9a111a6c4",
"entry_id": "6769204a1902c9341340a1bc",
}
staff_id = api_instance.entry_data_list(payload)
self.staff_id_list = staff_id.get("data") # api请求格式,将数据封装在data字典里
# 获取NGV数据
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "675bb02bd2d53c2034c665e4"}
self.NGV_data_list = api_instance.entry_data_list(payload).get("data", [])
# print("NGV获取后的类型:", type(self.NGV_data_list))
# 获取异常服务待办(添加过滤进行中的订单)
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "68340de79f116c0b66b6b0cc",
"filter": {"rel": "and",
"cond": [{"field": "flowState", "type": "flowstate", "method": "eq", "value": [0]}]}}
self.exception_service_todo = api_instance.entry_data_list(payload).get("data", [])
# print(self.exception_service_todo)
@staticmethod
def build_index(json_list):
index = {}
for json_item in json_list:
try:
key = (json_item['_widget_1734677164861'], json_item['_widget_1734677164862'],
json_item['_widget_1734677164863']) # 省市区
if '_widget_1734677164870' not in json_item: # 异常回访客服
raise KeyError("缺少 '异常回访客服'")
index[key] = json_item
except KeyError as e:
print(f"警告:{e},跳过该条记录: {json_item}")
continue
print('index', index)
return index
@staticmethod
def find_customer_service(province_name, city_name, area_name, index):
key = (province_name, city_name, area_name)
# print(index)
if key not in index:
return "数据缺失: 未找到对应的异常回访客服"
return index[key]
@staticmethod
def get_staff_id(row_item, name):
"""辅助函数,用于获取员工ID"""
if str(row_item["_widget_1734942794144"]) == str(name): # 检查姓名是否匹配
return row_item["_widget_1734942794145"] # 返回员工ID
return None
def assign_customer_service(self, province_name, city_name, area_name, index):
"""根据省市区派发给异常回访客服"""
# try:
customer_service_info = self.find_customer_service(province_name, city_name, area_name, index)
customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服
return customer_service
# except Exception as e:
# print(f"Error finding customer service: {e}")
# return "分配失败,请检查", "分配失败,请检查", "分配失败,请检查"
def main(self):
task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
all_data = []
try:
self.load_all_data()
data = common_module.get_yichang_details(days_back=1)
self.data_yichang_S = pd.DataFrame() if data is None or data.empty else data.astype(str)
self.index = self.build_index(self.json_list)
logger.info("开始运行SaaS异常回访")
if self.data_yichang_S.empty:
logger.info("未获取到数据或数据为空")
common_module.send_task_status(task_start_time, "异常服务待办派发")
return
data_yichang = self.data_yichang_S.copy()
# data_yichang.to_csv(os.path.join(output_dir,"data_yichang.csv"), index=False)
def replace_values(series):
# 使用条件判断来进行替换
return series.apply(lambda x: '' if pd.isna(x) or x in ['NA', 'None', ''] else x)
# 对整个DataFrame的所有列应用替换函数
data_yichang = data_yichang.apply(replace_values)
error_data = []
for index_num, row in data_yichang.iterrows(): # 对过滤后的每一条进行派发
try:
# 每次循环前清空省市区变量
province_name = None
city_name = None
area_name = None
is_pass = False
for exception_service in self.exception_service_todo:
# 通过查询筛选进行中的逻辑
if exception_service['_widget_1748241895842'] == row['org_code']:
is_pass = True
break
if is_pass:
logger.info(f"已存在待办,跳过该条记录: {row}")
continue
payload_dict = {}
distribution_date = datetime.datetime.now(datetime.timezone.utc)
distribution_date = distribution_date.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
date_obj1 = datetime.datetime.strptime(row["init_day"], "%Y%m%d").strftime("%Y-%m-%d")
date_obj2 = datetime.datetime.strptime(row["push_day"], "%Y%m%d").strftime("%Y-%m-%d")
NGV_roles = {
'service_impl_principal': row['service_impl_principal'], # 运营负责人
'area_manager': row['area_manager'], # 区域经理
'technician': row['technician'], # 运营专家
}
for role, name in NGV_roles.items(): # 寻找对应的员工ID
for row_item in self.staff_id_list:
staff_id = self.get_staff_id(row_item, name)
if staff_id:
NGV_roles[role] = staff_id
break # 找到后退出循环
else:
NGV_roles[role] = None # 如果没有找到对应的员工ID
relationship_manager, area_manager, technician = [NGV_roles[role] for role in
['service_impl_principal',
'area_manager',
'technician']]
UUid = time.strftime("%Y%m%d%H%M%S", time.localtime())
NGV_data_id = None
reason = None
create_exception = None
create_date = None
# 优先从 data_yichang_S 获取省市区信息
province_name = row.get('province_name')
city_name = row.get('city_name')
area_name = row.get('area_name') if 'area_name' in row else row.get('district_name')
# 检查省市区是否完整(省市区是一体的,任意一个缺失就需要从NGV获取)
use_ngv_location = False
if (not province_name or province_name in ['', 'None', 'NA'] or
not city_name or city_name in ['', 'None', 'NA'] or
not area_name or area_name in ['', 'None', 'NA']):
use_ngv_location = True
logger.info(f"门店 {row['org_code']} 的省市区信息不完整,将从NGV_data_list获取")
stop_date = None
# 获取关联数据
for NGV_Data in self.NGV_data_list:
# NGV_Data = NGV_Data.get("data")
if row["org_code"] == NGV_Data.get("_widget_1734062123071"): # 门店编码
NGV_data_id = NGV_Data.get("_id")
# 如果需要从 NGV_data_list 获取省市区信息
if use_ngv_location:
province_name = NGV_Data.get("_widget_1734062123090")
city_name = NGV_Data.get("_widget_1734062123092")
area_name = NGV_Data.get("_widget_1734062123094")
logger.info(
f"【从NGV获取省市区】门店 {row['org_code']}: {province_name}, {city_name}, {area_name}")
# 门店原因
reason = NGV_Data.get("_widget_1758617393828")
logger.info(f"获取关联数据成功:{NGV_data_id}, {province_name}, {city_name}, {area_name}")
# 是否生成异常待办
create_exception = NGV_Data.get("_widget_1758769279995")
# 获取上线日期(文本)# 202512.3改为开户日
create_date = NGV_Data.get("_widget_1734062123081")
# 获取暂停派发日期
stop_date = NGV_Data.get("_widget_1772610343227", None)
break # 找到匹配的数据后退出循环
# 定义可能的日期格式(灵活应对不同格式)
date_formats = [
"%Y-%m-%d %H:%M:%S", # 含时间
"%Y-%m-%d", # 仅日期
"%Y/%m/%d",
"%Y/%m/%d %H:%M:%S"
]
if stop_date:
# 解析暂停派发日期
parsed_stop_date = None
stop_value = stop_date.get("value") if isinstance(stop_date, dict) else stop_date
if isinstance(stop_value, (int, float)):
parsed_stop_date = datetime.datetime.fromtimestamp(
stop_value / 1000, tz=datetime.timezone.utc
).replace(tzinfo=None)
elif isinstance(stop_value, str):
stop_str = stop_value.strip()
iso_candidate = stop_str[:-1] + "+00:00" if stop_str.endswith("Z") else stop_str
try:
iso_dt = datetime.datetime.fromisoformat(iso_candidate)
except ValueError:
iso_dt = None
if iso_dt is not None:
parsed_stop_date = iso_dt.astimezone(datetime.timezone.utc).replace(tzinfo=None) if iso_dt.tzinfo else iso_dt
else:
for fmt in date_formats:
try:
parsed_stop_date = datetime.datetime.strptime(stop_str, fmt)
logger.debug(f"使用格式 {fmt} 成功解析暂停派发日期: {parsed_stop_date}")
break
except ValueError:
continue
if parsed_stop_date:
# 获取当前UTC时间
current_utc_time = datetime.datetime.utcnow()
logger.debug(f"当前UTC时间: {current_utc_time}")
logger.debug(f"暂停派发日期: {parsed_stop_date}")
# 比较时间
if current_utc_time < parsed_stop_date:
logger.info(f"当前UTC时间低于暂停派发日期,跳过派发")
continue
# 判断门店原因
# if reason in ["门店倒闭", "门店转让", "加盟其他连锁","切换竞品","虚拟门店","重新开户","已退款","二套系统"]:
# continue
# 判断是否继续生成异常待办
if create_exception == "":
continue
# 新增:检查 create_date_str 是否存在且有效
create_date_value = create_date.get("value") if isinstance(create_date, dict) else create_date
if not create_date_value:
logger.warning("上线日期为空,跳过该记录")
continue
parsed_date = None
if isinstance(create_date_value, (int, float)):
local_tz = datetime.timezone(datetime.timedelta(hours=8))
parsed_date = datetime.datetime.fromtimestamp(create_date_value / 1000, tz=local_tz).date()
elif isinstance(create_date_value, str):
create_str = create_date_value.strip()
iso_candidate = create_str[:-1] + "+00:00" if create_str.endswith("Z") else create_str
try:
iso_dt = datetime.datetime.fromisoformat(iso_candidate)
except ValueError:
iso_dt = None
if iso_dt is not None:
local_tz = datetime.timezone(datetime.timedelta(hours=8))
parsed_date = iso_dt.date() if iso_dt.tzinfo is None else iso_dt.astimezone(local_tz).date()
else:
for fmt in date_formats:
try:
parsed_date = datetime.datetime.strptime(create_str, fmt).date()
logger.debug(f"使用格式 {fmt} 成功解析日期: {parsed_date}")
break
except ValueError:
continue
if parsed_date is None:
logger.error(f"无法解析上线日期: '{create_date_value}',支持的格式: %Y-%m-%d, %Y-%m-%d %H:%M:%S 等")
continue # 解析失败,跳过
# 使用解析后的日期进行判断
now_date = datetime.date.today()
delta = now_date - parsed_date
days_diff = delta.days
if days_diff > 30:
logger.info(f"上线日期 {parsed_date} 超过30天({days_diff}天),生成待办")
# ✅ 继续后续待办创建逻辑
else:
logger.info(f"上线日期 {parsed_date} 在30天内,跳过处理")
continue
if not NGV_data_id:
logger.warning(f"未找到关联数据,请检查门店编码: {row['org_code']}")
# 根据省市区派发给异常回访客服
# 检查省市区是否都有值,如果有任何一个为空,则客服为空
if (not province_name or province_name in ['', 'None', 'NA'] or
not city_name or city_name in ['', 'None', 'NA'] or
not area_name or area_name in ['', 'None', 'NA']):
customer_service = None
logger.warning(f"【省市区信息缺失】门店 {row['org_code']} 省市区信息不完整,异常回访客服设置为空")
logger.warning(f"省: {province_name}, 市: {city_name}, 区: {area_name}")
else:
customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index)
logger.info(f"【派发客服】门店 {row['org_code']} 派发给客服: {customer_service}")
payload_dict.update({
"_widget_1748241895829": {"value": row["health_warning_info"]}, # 活跃健康状态变化
"_widget_1748241895830": {"value": row["org_name"]}, # 门店名称
"_widget_1748241895831": {"value": row["contacts"]}, # 联系人
"_widget_1748241895832": {"value": row['contact_mobile']}, # 联系方式
"_widget_1748241895833": {
"value": int(time.mktime(time.strptime(date_obj1, "%Y-%m-%d")) * 1000) if row[
"init_day"] != '' else ''},
# 初始日
"_widget_1748241895834": {
"value": int(time.mktime(time.strptime(date_obj2, "%Y-%m-%d")) * 1000) if row[
"push_day"] != '' else ''},
# 推进日
"_widget_1748246808678": {"value": customer_service}, # 当前跟进人
# "_widget_1748246808678": {"value": "083726094935447433"}, # 当前跟进人
"_widget_1748246808679": {"value": relationship_manager}, # 运营负责人
"_widget_1748246808680": {"value": customer_service}, # 区域客服
"_widget_1748241895839": {
"value": int(time.mktime(time.strptime(row["saas_create_time"], "%Y-%m-%d")) * 1000) if row[
"saas_create_time"] != '' else ''},
# 开户时间
"_widget_1748246808681": {"value": technician}, # 技术专家
"_widget_1748246808682": {"value": area_manager}, # 区域经理
"_widget_1748241895842": {"value": row['org_code']}, # 门店编码
"_widget_1748241895844": {"value": row['group_name']}, # 公司名称
"_widget_1748241895846": {"value": row['group_grade']}, # 公司等级
"_widget_1748241895847": {"value": row['region_name']}, # 大区
"_widget_1748241895848": {"value": row['province_name']}, # 省
"_widget_1748241895849": {"value": row['org_type']}, # 门店类型
"_widget_1748241895850": {"value": row['saas_edition_fmt']}, # 系统版本
"_widget_1748241895851": {"value": row['saas_customer_type']}, # saas客户类型
"_widget_1748241895852": {"value": row['org_stage']}, # 门店阶段
"_widget_1748241895853": {"value": row['contact_mobile']}, # 操作模式E.L/E.S
"_widget_1748241895855": {"value": row['city_name']}, # 城市
"_widget_1748247754304": {"value": NGV_data_id}, # 数据id
"_widget_1748512176655": {"value": "未处理"}, # 跟进状态
"_widget_1772761760440":{"value": "客服跟进节点"}, # 当前跟进节点
})
routine_follow_up_payload = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "68340de79f116c0b66b6b0cc", # 异常服务跟进待办
"is_start_workflow": "true",
"data": payload_dict,
"transaction_id": UUid
}
all_data.append(routine_follow_up_payload)
# res = api_instance.data_batch_create(routine_follow_up_payload)
# logger.info(f"创建结果:{res}")
except Exception as e:
error_task_logger.exception(f"异常服务待办派发执行时发生异常: {e}")
error_data.append(row)
pass
if error_data:
error_df = pd.DataFrame(error_data)
error_df.to_csv(os.path.join(output_dir, "异常派发错误数据.csv"))
common_module.send_task_error(task_start_time=task_start_time, task_name="异常服务待办派发",
error_message="失败文件中省市区匹配不到,需要通过门店编码在客户资料表中查询正确的省市区,并更新到省市区人员关系表中",
df=error_df)
ndf = pd.DataFrame(all_data)
ndf.to_csv(os.path.join(output_dir, "异常派发.csv"))
common_module.send_task_status(task_start_time, "异常服务待办派发")
except Exception as e:
error_task_logger.error(f"异常服务待办派发执行时发生异常: {e}")
common_module.send_task_error(task_start_time, "异常服务待办派发", str(e))
if __name__ == '__main__':
start = NewExceptionTask()
start.main()
+139
View File
@@ -0,0 +1,139 @@
import pymysql
import sys
import time
# ================== 配置信息 ==================
SOURCE_CONFIG = {
'host': "f6-public.rwlb.rds.aliyuncs.com",
'user': "rw_operation_data_relay",
'password': "m+q5Z4%IVuF9bf",
'database': "f6operation_data_relay",
'connect_timeout': 30,
'read_timeout': 600,
'write_timeout': 600
}
TARGET_CONFIG = {
'host': "db-f6operation-sst.f6car.org",
'user': "rw_operation",
'password': "tDm45eBj@upzLydHc",
'database': "f6operation_data_relay",
'connect_timeout': 30,
'read_timeout': 600,
'write_timeout': 600
}
TABLE_NAME = 'rpt_customized_maintain_detail'
READ_BATCH_SIZE = 1000 # 每次从源库读 5000 行
WRITE_BATCH_SIZE = 1000 # 每次向目标库写 5000 行
# ==============================================
def main():
print("🔧 正在从源数据库读取表结构...")
# === 第一步:获取建表语句 ===
source_conn = None
try:
source_conn = pymysql.connect(**SOURCE_CONFIG)
with source_conn.cursor() as cursor:
cursor.execute(f"SHOW CREATE TABLE `{TABLE_NAME}`")
result = cursor.fetchone()
if not result:
raise Exception(f"{TABLE_NAME} 不存在于源数据库")
create_table_sql = result[1]
except Exception as e:
print(f"❌ 读取表结构失败: {e}")
sys.exit(1)
finally:
if source_conn:
source_conn.close()
# === 第二步:获取总行数(用于进度)===
total_rows = 0
try:
source_conn = pymysql.connect(**SOURCE_CONFIG)
with source_conn.cursor() as cursor:
cursor.execute(f"SELECT COUNT(*) FROM `{TABLE_NAME}`")
total_rows = cursor.fetchone()[0]
except Exception as e:
print(f"⚠️ 无法获取总行数(不影响迁移): {e}")
finally:
if source_conn:
source_conn.close()
print(f"📊 表共约 {total_rows} 行,将分批读取和写入...")
# === 第三步:重建目标表 ===
target_conn = None
columns = []
try:
target_conn = pymysql.connect(**TARGET_CONFIG)
with target_conn.cursor() as cursor:
cursor.execute(f"DROP TABLE IF EXISTS `{TABLE_NAME}`")
new_create_sql = create_table_sql.replace(f"`{SOURCE_CONFIG['database']}`.", "")
cursor.execute(new_create_sql)
# 获取列名(从建表语句解析较复杂,改用查一次空结果)
cursor.execute(f"SELECT * FROM `{TABLE_NAME}` LIMIT 0")
columns = [desc[0] for desc in cursor.description]
target_conn.commit()
print("✅ 目标表已重建")
except Exception as e:
print(f"❌ 重建目标表失败: {e}")
if target_conn:
target_conn.close()
sys.exit(1)
# === 第四步:分批读取 + 分批写入 ===
offset = 0
inserted_total = 0
while True:
time.sleep(5)
# 从源库读取一批
batch_rows = []
try:
source_conn = pymysql.connect(**SOURCE_CONFIG)
with source_conn.cursor(pymysql.cursors.DictCursor) as cursor:
cursor.execute(
f"SELECT * FROM `{TABLE_NAME}` LIMIT %s OFFSET %s",
(READ_BATCH_SIZE, offset)
)
batch_rows = cursor.fetchall()
source_conn.close()
except Exception as e:
print(f"❌ 读取第 {offset//READ_BATCH_SIZE + 1} 批数据失败: {e}")
break
if not batch_rows:
print("🔚 数据读取完成")
break
# 转换为元组
data_tuples = [tuple(row[col] for col in columns) for row in batch_rows]
# 写入目标库(可再分小批,但这里 batch_rows 已是 5000
try:
with target_conn.cursor() as cursor:
placeholders = ', '.join(['%s'] * len(columns))
insert_sql = f"INSERT INTO `{TABLE_NAME}` (`{'`, `'.join(columns)}`) VALUES ({placeholders})"
cursor.executemany(insert_sql, data_tuples)
target_conn.commit()
inserted_total += len(data_tuples)
print(f"📤 已写入 {inserted_total} / {total_rows}")
except Exception as e:
print(f"❌ 写入失败(批次 offset={offset}: {e}")
target_conn.rollback()
break
offset += READ_BATCH_SIZE
# === 清理 ===
if target_conn and target_conn.open:
target_conn.close()
print(f"🎉 迁移完成!共写入 {inserted_total}")
if __name__ == "__main__":
main()
+973
View File
@@ -0,0 +1,973 @@
import os
import time
import requests
from api import API
from back_ground_module import CommonModule
import pandas as pd
import datetime
import re
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()
class RenewServicesRevisit:
"""续约回访90-180"""
def __init__(self):
self.index = None
self.data_NGV = None
self.date_list = None
self.Smart_detection = None
self.service_remind = None
self.json_list = []
self.NGV_data_list = None
self.permissions_table = None
self.staff_id_list = None
self.get_feature_usage = None
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
def load_all_data(self):
"""加载所有必要的数据表"""
# 省市区人员关系表
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "676512ac3e54dc3159460c0a"}
json_dict = api_instance.entry_data_list(payload)
if json_dict and "data" in json_dict:
self.json_list = json_dict.get("data")
else:
print("加载省市区人员关系表失败")
self.json_list = []
# 获取简道云员工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字典里
# 获取权限表信息
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "675b96c14e839f90fef1647c"}
self.permissions_table = api_instance.entry_data_list(payload).get("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": "676bb7bda3029720f1083e99"}
self.service_remind = api_instance.entry_data_list(payload).get("data")
# 获取智能检测-数据支持表单数据
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "676bb99649ab3ac975af6e39"}
self.Smart_detection = api_instance.entry_data_list(payload).get("data")
# 获取功能使用情况表
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "6763bbf657bd8fb76fcb41b2"}
self.get_feature_usage = api_instance.entry_data_list(payload).get("data", [])
# 获取保单识别表
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "6773a60d30ed87ff9f68d3c5"}
self.policy_recognition = api_instance.entry_data_list(payload).get("data")
# 提取 _widget_1735632397600 的值并存储在列表中
self.widget_list = [item['_widget_1735632397600'] for item in self.policy_recognition]
# 获取私域小程序-数据支持表单数据
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "67e0f0fae622896749ba5087"}
self.private_domain = api_instance.entry_data_list(payload).get("data", [])
# 提取 _widget_1742795002375 的值并存储在列表中
# self.private_domain_list = [item['_widget_1742795002375'] for item in self.private_domain]
# 获取公域小程序-数据支持表单数据
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "67e0c702c8f603b997980999"}
self.public_domain = api_instance.entry_data_list(payload).get("data", [])
# 提取 _widget_1742784257506 的值并存储在列表中
self.public_domain_list = [item['_widget_1742784257506'] for item in self.public_domain]
# 获取异业合作-数据支持表单数据
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "67e24fdd8dfcfa918e17c30b"}
self.different_industries = api_instance.entry_data_list(payload).get("data", [])
# 提取 _widget_1742784257506 的值并存储在列表中
self.different_industries_list = [item['_widget_1742884829007'] for item in self.different_industries]
# 获取短信-数据支持表单数据
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "67e5107198ba1b20d5df3974"}
self.groupnotification = api_instance.entry_data_list(payload).get("data", [])
# 获取多公司过滤表
payload = {"api_key": "675b900991ad2491c69389ca", "entry_id": "689bf5f8ba88a28cb0679ec9"}
self.get_filter_company_list = api_instance.entry_data_list(payload).get("data", [])
@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}")
@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_1734677164871' not in json_item: # 日常回访客服
raise KeyError("缺少 '日常回访客服'")
index[key] = json_item
except KeyError as e:
print(f"警告:{e},跳过该条记录: {json_item}")
continue
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 remove_parentheses(text: str) -> str:
# 使用正则表达式匹配并去除括号及其内容
# \s* 表示匹配零个或多个空白字符(处理括号前后可能存在的空格)
# $ 和 $ 分别表示匹配左括号和右括号
# 中间的 .*? 表示非贪婪地匹配任意数量的字符(包括没有字符的情况)
cleaned_text = re.sub(r'\s*$.*?$\s*', '', text)
# 为了确保同时处理中文括号,再进行一次替换
cleaned_text = re.sub(r'\s*.*?\s*', '', cleaned_text)
return cleaned_text.strip() # 去除两端多余的空白字符
@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)
# 定义一个辅助函数,用于安全地获取多层字段中的 username
def safe_get_username(data, key):
try:
if isinstance(data, dict):
return data.get(key, {}).get('username', "")
return ""
except:
return ""
relationship_manager = safe_get_username(customer_service_info, '_widget_1734677164864')
customer_service = safe_get_username(customer_service_info, '_widget_1734677164871')
technician = safe_get_username(customer_service_info, '_widget_1734677164866')
area_manager = safe_get_username(customer_service_info, '_widget_1734677164865')
return relationship_manager, customer_service, technician, area_manager
except Exception as e:
print(f"Error finding customer service: {e}")
return "分配失败,请检查", "分配失败,请检查", "分配失败,请检查"
def calculate_date_one(self, start_offset=0):
"""
计算从当前日期或指定偏移量的日期开始往前遍历遇到date_list中日期的次数
参数:
- start_offset: 从当前日期起始的天数偏移量默认为0即今天负数表示过去正数表示未来
返回:
- date_one: 遍历到date_list中日期的次数
"""
# 设置起始日期
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
def main(self):
import datetime
task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
logger.info("开始执行任务")
# 主店过期,分店设置为主店
global png_url, upload_key
self.load_all_data()
logger.info("数据加载完成")
self.date_list = common_module.get_holiday_list() # 获取一年中的节假日
self.date_one = self.calculate_date_one(start_offset=0)
self.data_NGV = common_module.get_ngv_details(days_back=1) # 获取data_NGV 并转为str
self.index = self.build_index(self.json_list)
logger.info("获取多公司过滤公司id")
all_filter_company_list = [] # 获取多公司过滤公司id
for company in self.get_filter_company_list:
company_list = company.get("_widget_1755052002491")
for company_item in company_list:
if company_item.get("_widget_1755052002496") == "":
all_filter_company_list.append(company_item.get("_widget_1755052002495"))
logger.info(f"过滤公司条数:{len(all_filter_company_list)}")
# 将A列和B列的日期字符串转换为日期格式
data_NGV = self.data_NGV.copy()
data_NGV['A'] = pd.to_datetime(data_NGV['expiry_time'])
data_NGV['B'] = pd.to_datetime(data_NGV['renew_date'])
def replace_values(series):
# 使用条件判断来进行替换
return series.apply(lambda x: '' if pd.isna(x) or x in ['NA', 'None', ''] else x)
# 对整个DataFrame的所有列应用替换函数
# 处理字符串数据并显式指定数据类型
data_NGV = data_NGV.apply(replace_values)
# 针对公司主店过期,取公司最高等级版本派发
# 过滤多公司
data_NGV = data_NGV[~data_NGV['id_own_group'].isin(all_filter_company_list)]
# 定义优先级顺序
edition_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']
customer_type_order = ["F", "E", "D", "C", "B", "A"] # 索引越小优先级越高
group_grade_order = ['全国KAFMVP', '区域KAMVP', '重要客户(SVIP', '普通客户(VIP']
# 创建映射字典,并为不在列表中的值设置默认值
edition_map = {edition: idx for idx, edition in enumerate(edition_order)}
customer_type_map = {ctype: idx for idx, ctype in enumerate(customer_type_order)}
group_grade_map = {grade: idx for idx, grade in enumerate(group_grade_order)}
# 添加用于排序的新列,并处理不在映射字典中的值
data_NGV['edition_rank'] = data_NGV['saas_edition_fmt'].map(edition_map).fillna(0).astype(
int) # 缺失值用最高优先级填充
data_NGV['customer_type_rank'] = data_NGV['saas_customer_type'].map(customer_type_map).fillna(0).astype(int)
data_NGV['group_grade_rank'] = data_NGV['group_grade'].map(group_grade_map).fillna(0).astype(int)
# data_NGV.to_csv("88855.csv")
# 找到每组中 edition_rank 最小值对应的行
best_edition_idx = data_NGV.groupby('id_own_group')['edition_rank'].idxmin()
best_edition_rows = data_NGV.loc[best_edition_idx]
best_edition_rows['max_saas_edition'] = best_edition_rows['saas_edition_fmt']
# 找到每组中 customer_type_rank 最小值对应的行
best_customer_type_idx = data_NGV.groupby('id_own_group')['customer_type_rank'].idxmin()
best_customer_type_rows = data_NGV.loc[best_customer_type_idx]
best_customer_type_rows['max_saas_customer_type'] = best_customer_type_rows['customer_type_rank'].apply(
lambda x: customer_type_order[x])
# 找到每组中 group_grade_rank 最小值对应的行
best_group_grade_idx = data_NGV.groupby('id_own_group')['group_grade_rank'].idxmin()
best_group_grade_rows = data_NGV.loc[best_group_grade_idx]
best_group_grade_rows['max_group_grade'] = best_group_grade_rows['group_grade']
# 合并最佳值回到原数据集
best_values = (
best_edition_rows[['id_own_group', 'max_saas_edition']]
.merge(best_customer_type_rows[['id_own_group', 'max_saas_customer_type']], on='id_own_group',
how='outer')
.merge(best_group_grade_rows[['id_own_group', 'max_group_grade']], on='id_own_group', how='outer')
)
# 将最佳值合并回原数据集
data_NGV = data_NGV.merge(best_values, on='id_own_group', how='left')
condition = (data_NGV['is_main_org'] == 1) & (data_NGV['org_status'] == '过期') # 步骤2: 过滤条件
ngvv2 = data_NGV[condition]
# ngvv2.to_excel(r"C:\Users\Administrator.DESKTOP-7IC2USJ\Desktop\NGVV2.xlsx")
data_NGV_V2 = data_NGV.copy() # 步骤3: 检查id_own_group是否存在于ngvv2中
data_NGV_V2['条件'] = ((data_NGV_V2['org_type'] == "一般") & (data_NGV_V2['org_status'] == '留存') &
(data_NGV_V2['area_manager'] != '殷昊') & (
data_NGV_V2['area_manager'] != '孙玉蕾') & (
data_NGV_V2['is_main_org'] != 1))
data_NGV_V2 = data_NGV_V2.loc[data_NGV_V2["条件"]]
# 步骤4: 过滤存在的记录
data_NGV_V2['exists_in_ngvv2'] = data_NGV_V2['id_own_group'].isin(ngvv2['id_own_group'])
filtered_data = data_NGV_V2[data_NGV_V2['exists_in_ngvv2']]
fixed_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']
# sorted_items = sorted(filtered_data, key=lambda x: fixed_order.index(x))
fixed_order_map = {edition: index for index, edition in enumerate(fixed_order)}
filtered_data['sort_key'] = filtered_data['saas_edition_fmt'].map(fixed_order_map)
filtered_data = filtered_data.sort_values(by='sort_key').drop('sort_key', axis=1)
result = filtered_data.drop_duplicates(subset='id_own_group', keep='first')
data_NGV['条件'] = (data_NGV['org_type'] == "一般") & (data_NGV['org_status'] == '留存') & (
data_NGV['area_manager'] != '殷昊') & (
data_NGV['area_manager'] != '孙玉蕾') & (
data_NGV['is_main_org'] == 1)
data_NGV = data_NGV.loc[data_NGV["条件"]]
data_NGV = pd.concat([data_NGV, result], axis=0)
data_details = data_NGV.copy()
# 重置索引
data_details = data_details.reset_index(drop=True)
# data_details.to_csv("dayin.csv")
# 判断A列的日期是否大于B列的日期730天,如果是的话,将B列的值设置为1
data_details['条件'] = data_details.apply(
lambda row: (
(pd.to_datetime(row['A']) - pd.to_datetime(row['B'])).days
if pd.to_datetime(row['A']) - pd.to_datetime(row['B']) >= pd.Timedelta(days=730)
else 0
),
axis=1
)
data_details = data_details.loc[data_details["条件"] > 0]
# 定义一个函数,用于将数字除以365并取整数
def divide_by_365(x):
if isinstance(x, (int, float)):
return int(x / 365)
else:
return x
# 使用applymap()函数将divide_by_365函数应用到DataFrame的每个元素
data_details[''] = data_details['条件'].apply(divide_by_365)
# 重置索引
data_details = data_details.reset_index(drop=True)
# data_details.to_excel(r"C:\Users\admin\Downloads\2.xlsx")
# 创建一个新的空的DataFrame
new_df = pd.DataFrame()
# 遍历原始DataFrame的每一行
from datetime import datetime
for index, row in data_details.iterrows():
# 根据A列的值来决定复制的次数
if row["renew_date"] != "2024-02-29":
for i_new in range(1, row['']):
# 修改日期
row_new = row.copy()
c = row_new["renew_date"]
date_obj = datetime.strptime(c, "%Y-%m-%d")
new_year = date_obj.year + i_new
new_date_obj = date_obj.replace(year=new_year)
new_c = new_date_obj.strftime("%Y-%m-%d")
row_new["renew_date"] = new_c
# 将当前行添加到新的DataFrame中
# new_df = new_df.append(row_new, ignore_index=True)
new_df = pd.concat([new_df, pd.DataFrame([row_new])], ignore_index=True)
# 合并两个DataFrame
merged_df = pd.concat([data_NGV, new_df], axis=0, ignore_index=True)
data_details = merged_df.copy() # 替换名称
data_details_not_null = data_details[data_details['renew_date'].notnull()]
# data_details_not_null = data_details_not_null[data_details_not_null['renew_date'].str.contains('2023')]
# data_details_not_null = data_details_not_null.sort_values(by='renew_date', ascending=True).drop_duplicates(
# subset='id_own_group') 重置索引
data_details_not_null = data_details_not_null.reset_index(drop=True)
data_details = data_details_not_null.copy() # 替换名称 v2
data_details['saas_create_time'] = data_details['saas_create_time'].str[:4] # 截取前10位
data_details['renew_date_new'] = data_details['renew_date'].str[:4] # 截取前10位
data_details = data_details[
data_details['saas_create_time'] != data_details['renew_date_new']] # 过滤掉等于renew_date的行
data_details = data_details.reset_index(drop=True)
logger.info(f"过滤后的数据长度为: {len(data_details)}")
# data_details.to_excel(r"C:\Users\admin\Downloads\4.xlsx")
import datetime
start_time = datetime.datetime.now()
date_90 = 83
date_120 = 113
date_180 = 173
# self.date_one = 1
now_time = start_time.replace()
if now_time.strftime("%Y-%m-%d") in self.date_list:
self.date_one = 0
print("开始次数:", self.date_one)
print("当前日期:", now_time)
logger.info(f"遍历次数:{self.date_one}")
now_time = start_time.replace()
for i in range(0, self.date_one):
logger.info(f"这是第{i}次遍历")
import datetime
now_time = datetime.datetime.now() + datetime.timedelta(days=-(i + 1))
today = now_time + datetime.timedelta(days=-date_90)
formatted_today_90 = today.strftime("%Y-%m-%d")
today = now_time + datetime.timedelta(days=-date_120)
formatted_today_120 = today.strftime("%Y-%m-%d")
today = now_time + datetime.timedelta(days=-date_180)
formatted_today_180 = today.strftime("%Y-%m-%d")
logger.info(f"90天为{formatted_today_90}180天为{formatted_today_180}")
# 获取数据
data_details_90 = data_details.copy()
data_details_90['条件'] = (data_details_90['renew_date'] == formatted_today_90) & (data_details_90[
'group_grade'] != "普通客户(VIP") # & (data_details_90['saas_edition_fmt'] != '基础版') & (data_details_90['saas_edition_fmt'] != '入门版')
data_details_90 = data_details_90.loc[data_details_90["条件"]]
data_details_120 = data_details.copy()
data_details_120['条件'] = (data_details_120['renew_date'] == formatted_today_120) & (
(data_details_120['saas_edition_fmt'] == '基础版') | (
data_details_120['saas_edition_fmt'] == '入门版'))
data_details_120 = data_details_120.loc[data_details_120["条件"]]
data_details_180 = data_details.copy()
data_details_180['条件'] = (data_details_180[
'renew_date'] == formatted_today_180) # & (data_details_180[
# 'saas_edition_fmt'] != '基础版') & (data_details_180['saas_edition_fmt'] != '入门版')
data_details_180 = data_details_180.loc[data_details_180["条件"]]
data_details_90["跟进阶段"] = "续约后90天回访"
data_details_90["主要目的"] = "关怀使用情况,促进更多功能使用,提升系统使用深度。"
data_details_120["跟进阶段"] = "续约后120天回访"
data_details_120["主要目的"] = "暂无"
data_details_180["跟进阶段"] = "续约后180天回访"
data_details_180["主要目的"] = "关怀使用情况,促进增购商机转化,识别潜在风险,及时提报。"
# 合并三个DataFrame
data_result = pd.concat([data_details_90, data_details_180],
ignore_index=True) # 去除续约120天回访 data_details_120
logger.info(f"派发数据长度:{len(data_result)}")
data_result = data_result.astype(str)
data_NGV = data_result.copy()
for index_num, row in data_NGV.iterrows(): # 对过滤后的每一条进行派发
try:
# print(row["org_code"]) # 数据验证
# print(row["service_impl_principal"])
# print(row["area_manager"])
# print(row["technician"])
# print("销售负责人是:", row["salesmen"])
payload_dict = {}
saas_use_year = re.findall(r'第([0-9]+)年', row["saas_use_year"])[0]
NGV_roles = {
'relationship_manager': row['service_impl_principal'], # 运营负责人
# 'relationship_manager': "张阳", # 运营负责人
'area_manager': row['area_manager'], # 区域经理
'technician': row['technician'], # 技术专家
'salesmen': row['salesmen'], # 销售负责人
}
for role, name in NGV_roles.items():
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
# 回访人员: 需确认 四年以下 technician
if int(saas_use_year) < 4:
relationship_manager, area_manager, technician, salesmen = [NGV_roles[role] for role in
['relationship_manager',
'area_manager',
'technician', 'salesmen']]
# 如果未找到运营负责人,则根据省市区派发给日常回访客服
if not relationship_manager:
relationship_manager = self.assign_customer_service(
row['province_name'], row['city_name'], row['area_name'], self.index
)[0]
if not technician:
technician = self.assign_customer_service(
row['province_name'], row['city_name'], row['area_name'], self.index
)[2]
if row["group_grade"] == "普通客户(VIP" or row["group_grade"] == "重要客户(SVIP":
payload_dict.update({
"_widget_1734590278288": {"value": relationship_manager}, # 跟进人是运营负责人
})
else:
payload_dict.update({
"_widget_1734590278288": {"value": technician}, # 跟进人是技术专家
})
else:
logger.info(
f"超过4年的客户,派发给客服,省市区:{row['province_name']} {row['city_name']} {row['area_name']}")
salesmen = [NGV_roles[role] for role in ['salesmen']][0]
# 直接根据省市区派发给日常回访客服
relationship_manager, customer_service, technician, area_manager = self.assign_customer_service(
row['province_name'], row['city_name'], row['area_name'], self.index
)
logger.info(f"派发给 {relationship_manager} {customer_service} {technician} {area_manager}")
if row["group_grade"] == "普通客户(VIP" or row["group_grade"] == "重要客户(SVIP":
payload_dict.update({
"_widget_1734590278288": {"value": customer_service} # 跟进人是日常回访客服
})
else:
payload_dict.update({
"_widget_1734590278288": {"value": technician} # 跟进人是技术专家
})
payload_dict.update({
# "_widget_1734590278288": {"value": relationship_manager}, # 跟进人是运营负责人
"_widget_1734590278289": {"value": relationship_manager}, # 运营负责人
"_widget_1734590278290": {"value": area_manager}, # 区域经理
"_widget_1734590278291": {"value": technician}, # 技术专家
"_widget_1735290738545": {"value": salesmen} # 销售负责人
})
logger.info(f"请求输出:{payload_dict}")
if payload_dict.get("_widget_1734590278288") == "02414917880947": # 如果跟进人是殷浩
payload_dict.update({
"_widget_1734590278288": {"value": "051612246035720178"}, # 跟进人是赵柄诚
})
# 输出结果
logger.info(f"SaaS开户回访人员:{relationship_manager}")
logger.info(f"SaaS技术专家:{technician}")
logger.info(f"SaaS区域经理:{area_manager}")
# 判断权限唯一值
group_grade = re.sub(r'[^]*', '', row['max_group_grade'])
if not row['saas_customer_type'] or row['saas_customer_type'] == 'NA' or row[
'saas_customer_type'] == 'None':
row['saas_customer_type'] = "F"
NGV_store_level_key = group_grade + row['max_saas_edition'] + row['max_saas_customer_type']
logger.info(f"权限唯一值: {NGV_store_level_key}")
Billing = None
for item in self.permissions_table:
if NGV_store_level_key == item.get("_widget_1734056507963"): # 合并(等级-类型-分层)
print("该门店开单的权限是:", item.get("_widget_1734055617039"))
Billing = item.get("_widget_1734055617039") # 开单
Service_Alerts = item.get("_widget_1734055617040") # 服务提醒
membership = item.get("_widget_1734055617041") # 会员卡
SMS = item.get("_widget_1734055617042") # 短信
Public_domain_applets = item.get("_widget_1734055617043") # 公域小程序
Private_domain_applets = item.get("_widget_1734055617044") # 私域小程序
Test_sheet = item.get("_widget_1734055617045") # 检测单
AI_poster = item.get("_widget_1734055617046") # AI海报
Business_wallets = item.get("_widget_1734055617047") # 企业钱包
Precision_marketing = item.get("_widget_1734055617049") # 精准营销
Paid_memberships = item.get("_widget_1734055617051") # 付费会员
business_WeCom = item.get("_widget_1734055617052") # 企业微信
Insurance_policy_identification = item.get("_widget_1734055617053") # 保险单识别
Insurance_bots = item.get("_widget_1734055617054") # 保险机器人
Camera_pick_up = item.get("_widget_1734055617055") # 摄像头接车
Camera_billing = item.get("_widget_1734055617056") # 摄像头开单
Transparent_workshop = item.get("_widget_1734055617057") # 透明车间
Cross_industry_cooperation = item.get("_widget_1734055617058") # 异业合作
BI_Insights = item.get("_widget_1734055617059") # BI洞察
payload_dict.update(
{
"_widget_1734073342350": {"value": Billing},
"_widget_1735004315757": {"value": Service_Alerts},
"_widget_1735004315756": {"value": membership},
"_widget_1735004315755": {"value": SMS},
"_widget_1735004315754": {"value": Public_domain_applets},
"_widget_1735004315753": {"value": Private_domain_applets},
"_widget_1735004315752": {"value": Test_sheet},
"_widget_1735004315751": {"value": AI_poster},
"_widget_1735004315750": {"value": Business_wallets},
"_widget_1735004315749": {"value": Precision_marketing},
"_widget_1735004315748": {"value": Paid_memberships},
"_widget_1735004315747": {"value": business_WeCom},
"_widget_1735004315746": {"value": Insurance_policy_identification},
"_widget_1735004315745": {"value": Insurance_bots},
"_widget_1735004315744": {"value": Camera_pick_up},
"_widget_1735004315743": {"value": Camera_billing},
"_widget_1735004315742": {"value": Transparent_workshop},
"_widget_1735004315741": {"value": Cross_industry_cooperation},
"_widget_1734073342352": {"value": BI_Insights},
}
)
feature_dict = {
"开单": "_widget_1734073342350",
"服务提醒": "_widget_1735004315757",
"会员卡": "_widget_1735004315756",
"短信": "_widget_1735004315755",
"公域小程序": "_widget_1735004315754",
"私域小程序": "_widget_1735004315753",
"检测单": "_widget_1735004315752",
"AI海报": "_widget_1735004315751",
"企业钱包": "_widget_1735004315750",
"精准营销": "_widget_1735004315749",
"付费会员": "_widget_1735004315748",
"企业微信": "_widget_1735004315747",
"保险单识别": "_widget_1735004315746",
"保险机器人": "_widget_1735004315745",
"摄像头接车": "_widget_1735004315744",
"摄像头开单": "_widget_1735004315743",
"透明车间": "_widget_1735004315742",
"异业合作": "_widget_1735004315741",
"BI洞察": "_widget_1734073342352",
}
# _widget_1735527329557 下次是否推荐
for new_item in self.get_feature_usage:
for feature_module, feature_value in feature_dict.items(): # 模块
if new_item.get("_widget_1735268263079") == feature_module and new_item.get(
"_widget_1735527329557") == "" and new_item.get(
"_widget_1735280720550") == \
row["id_own_org"]: # 下次是否推荐 功能使用情况表
logger.info(f"{feature_value}进行了更改")
payload_dict.update({f"{feature_value}": {"value": "×"}})
if new_item.get("_widget_1735268263079") == feature_module and new_item.get(
"_widget_1736414617462") == "" and new_item.get(
"_widget_1735280720550") == \
row["id_own_org"]: # 下次是否跟进
logger.info(f"{feature_value}进行了更改")
payload_dict.update({f"{feature_value}": {"value": "×"}})
fields_to_check = {
"_widget_1735004315763": Billing, # 开单
"_widget_1735106258016": Service_Alerts, # 服务提醒
"_widget_1735106258036": membership, # 会员卡
"_widget_1735106258086": SMS, # 短信
"_widget_1735106258112": Public_domain_applets, # 公域小程序
"_widget_1735106258141": Private_domain_applets, # 私域小程序
"_widget_1735107354648": Test_sheet, # 检测单
"_widget_1735107354725": AI_poster, # AI海报
"_widget_1735107354811": Business_wallets, # 企业钱包
"_widget_1735107354906": Precision_marketing, # 精准营销
"_widget_1735107354980": Paid_memberships, # 付费会员
"_widget_1735107355093": business_WeCom, # 企业微信
"_widget_1735107355143": Insurance_policy_identification, # 保险单识别
"_widget_1735107355235": Insurance_bots, # 保险机器人
"_widget_1735107355333": Camera_pick_up, # 摄像头接车
"_widget_1735107355392": Camera_billing, # 摄像头开单
"_widget_1735107355502": Transparent_workshop, # 透明车间
"_widget_1735107355618": Cross_industry_cooperation, # 异业合作
"_widget_1735107355740": BI_Insights # BI洞察
}
# 遍历每个字段,检查其值并更新payload_dict
for widget_id, field_name in fields_to_check.items():
if field_name == "":
payload_dict.update({widget_id: {"value": ""}})
break
is_continue = False
# 如果门店倒闭则不派发
for store in self.NGV_data_list:
if row["id_own_org"] == store.get("_widget_1734062123069") and store.get(
"_widget_1752027386523") == "门店倒闭":
is_continue = True
logger.info(f"{row["id_own_org"]}门店已关闭,不派发")
break
if is_continue:
continue
if not Billing:
logger.warning(f"权限表请求失败或者权限表无对应关系,权限唯一值是:{NGV_store_level_key}")
if row["active_status_fmt"] == "活跃": # 开单 是否使用
payload_dict.update({"_widget_1735004315765": {"value": ""}})
else:
payload_dict.update({"_widget_1735004315765": {"value": ""}})
try:
if row["saas_edition_fmt"] not in ["基础版", "入门版"]: # 会员卡 是否拥有
payload_dict.update({"_widget_1735106258036": {"value": ""}})
else:
payload_dict.update({"_widget_1735106258036": {"value": ""}})
except Exception as e:
error_task_logger.error(f"会员卡拥有识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-会员卡拥有识别", str(e))
try:
if row["card_bill_day_count_last_30_day"] != "0": # 会员卡 是否使用
payload_dict.update({"_widget_1735106258038": {"value": ""}})
else:
payload_dict.update({"_widget_1735106258038": {"value": ""}})
except Exception as e:
error_task_logger.error(f"会员卡使用识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-会员卡使用识别", str(e))
# print(self.service_remind.get("_widget_1735112637045"))
payload_dict["_widget_1735106258018"] = {"value": ""}
for item in self.service_remind:
if row["id_own_group"] == item.get("_widget_1735112637043"):
if int(item.get("_widget_1735112637045")) < 180 and int(
item.get("_widget_1735112637046")) == 1: # 服务提醒 是否使用
payload_dict.update({"_widget_1735106258018": {"value": ""}})
break
elif int(item.get("_widget_1735112637048")) > 0:
payload_dict.update({"_widget_1735106258018": {"value": ""}})
break
keys_to_check = [
"_widget_1735113110155"
] # 智能检测 是否使用
# 初始化默认值为"否"
payload_dict["_widget_1735107354650"] = {"value": ""}
# 检查每个键,如果有一个大于0,则更新为"是"并停止检查
for key in keys_to_check:
for item in self.Smart_detection:
if row["id_own_org"] == item.get("_widget_1735113110147"):
if int(item.get(key, 0)) > 0: # 使用get方法并提供默认值0防止键不存在的情况
payload_dict["_widget_1735107354650"]["value"] = ""
break
# 近30天业务单量=0 则其它所有模块均不推荐
try:
for feature_module, feature_value in feature_dict.items(): # 模块
if row["bill_count_last_30_day"] == '0' and payload_dict[feature_value]["value"] == '':
payload_dict.update({f"{feature_value}": {"value": "×"}})
except Exception as e:
error_task_logger.error(f"不开单识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-不开单识别", str(e))
# 保单识别:从系统中抽取目标门店,针对门店抽取修改是否推荐
try:
if row["org_code"] in self.widget_list:
payload_dict.update({'_widget_1735004315746': {"value": ""}})
except Exception as e:
error_task_logger.error(f"保单识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-保单识别", str(e))
# 私域小程序:根据是否开通微信小程序判断是否使用,旗舰版及以上算拥有
try:
for item in self.private_domain:
if row["id_own_group"] == item.get("_widget_1742795002375"): # 公司id
if int(item.get("_widget_1742795002379")) > 0: # 上架商品数
payload_dict.update({"_widget_1735106258143": {"value": ""}}) # DX:是否拥有
break
else:
payload_dict.update({"_widget_1735106258143": {"value": ""}}) # DX:是否拥有
break
except Exception as e:
error_task_logger.error(f"小程序识别:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-小程序识别", str(e))
try:
high_version = ['皇冠版', '至尊版', '尊享版', '旗舰版']
if row["saas_edition_fmt"] in high_version:
payload_dict.update({'_widget_1735106258141': {"value": ""}}) # SYXCX:是否拥有
else:
payload_dict.update({'_widget_1735106258141': {"value": ""}}) # SYXCX:是否拥有
except Exception as e:
error_task_logger.error(f"私域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-私域小程序", str(e))
# 公域小程序:根据是否开通微信小程序判断是否使用,旗舰版及以上算拥有
try:
for item in self.public_domain:
if row["id_own_org"] == item.get("_widget_1742784257506"): # 门店id
if int(item.get("_widget_1742784257509")) == 1: # 发布商品数量
payload_dict.update({"_widget_1735106258114": {"value": ""}}) # GYXCX:是否使用
break
else:
payload_dict.update({"_widget_1735106258114": {"value": ""}}) # GYXCX:是否使用
break
except Exception as e:
error_task_logger.error(f"公域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-公域小程序", str(e))
try:
if row["id_own_org"] in self.public_domain_list:
payload_dict.update({'_widget_1735106258112': {"value": ""}}) # GYXCX:是否拥有
else:
payload_dict.update({'_widget_1735106258112': {"value": ""}}) # GYXCX:是否拥有
except Exception as e:
error_task_logger.error(f"公域小程序:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-公域小程序", str(e))
# 异业合作:根据是否存在判断是否拥有,过滤条件 商品名称包含异业两个字
try:
if row["id_own_org"] in self.different_industries_list:
payload_dict.update({'_widget_1735107355618': {"value": ""}}) # YYHZ:是否拥有
else:
payload_dict.update({'_widget_1735107355618': {"value": ""}}) # YYHZ:是否拥有
except Exception as e:
error_task_logger.error(f"异业合作:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-异业合作", str(e))
# 短信:根据是否启动短信功能判断是否拥有,根据
try:
for item in self.groupnotification:
if row["id_own_group"] == item.get("_widget_1743065201885"): # 公司id
if int(item.get("_widget_1743065201886")) == 1: # 是否启动短信功能
payload_dict.update({"_widget_1735106258086": {"value": ""}}) # DX:是否拥有
break
else:
payload_dict.update({"_widget_1735106258086": {"value": ""}}) # DX:是否拥有
break
except Exception as e:
error_task_logger.error(f"短信是否使用:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-短信是否使用", str(e))
try:
for item in self.groupnotification:
if row["id_own_group"] == item.get("_widget_1743065201885"): # 公司id
if int(item.get("_widget_1743065201889")) > 0: # 累计发送成功总人数
payload_dict.update({"_widget_1735106258088": {"value": ""}}) # DX:是否使用
break
else:
payload_dict.update({"_widget_1735106258088": {"value": ""}}) # DX:是否使用
break
except Exception as e:
error_task_logger.error(f"短信是否使用:Error finding customer service: {e}")
common_module.send_task_error(task_start_time, "手动添加日常回访-短信是否使用", str(e))
NGV_data_id = 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")
logger.info(f"关联数据数据id:{NGV_data_id}")
try:
png_url = NGV_Data.get('_widget_1742890765211', {})[0].get('url', "")
except:
png_url = ""
logger.info(f"关联数据图片:{png_url}")
if not NGV_data_id:
logger.warning("未找到数据ID")
distribution_date = datetime.datetime.now(datetime.timezone.utc)
distribution_date = distribution_date.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
upload_key = None
UUid = time.strftime("%Y%m%d%H%M%S", time.localtime())
if png_url:
save_dir = "sampleCloud" # 设置输出目录
# 创建输出目录(如果不存在)
import os
os.makedirs(save_dir, exist_ok=True)
save_path = fr'{save_dir}\png\{time.strftime("%Y%m%d%H%M%S", time.localtime())}.png'
RenewServicesRevisit.download_url_content(png_url, save_path)
logger.info(f"已保存图片到 {save_path}")
up_data = api_instance.get_upload_token(
{"api_key": "675b900991ad2491c69389ca", "entry_id": "675b9c63925cd404038a6b86",
"transaction_id": UUid})
upload_url = up_data.get("upload_url")
upload_token = up_data.get("upload_token")
upload_result = api_instance.upload_file(
{"upload_url": upload_url, "upload_token": upload_token, "file_path": save_path})
upload_key = upload_result.get("key")
logger.info("已上传文件")
payload_dict.update({
"_widget_1734590278279": {"value": row["group_name"]}, # 公司名称
"_widget_1735112931760": {"value": row["id_own_group"]}, # 公司id
"_widget_1735112931761": {"value": row["id_own_org"]}, # 门店id
"_widget_1734590278281": {"value": row['org_name']}, # 门店名称
"_widget_1734590278292": {"value": row["跟进阶段"]}, # 跟进阶段
"_widget_1734321349021": {"value": NGV_data_id}, # 关data_get联数据
"_widget_1742548684369": {"value": row['主要目的']}, # 主要目的
"_widget_1734590278266": {"value": row['region_name']}, # 大区
"_widget_1734590278285": {"value": row['branch_name']}, # 小区
"_widget_1734590278284": {"value": row['province_name']}, # 省
"_widget_1734590278283": {"value": row['city_name']}, # 市
"_widget_1734590278282": {"value": row['area_name']}, # 区
"_widget_1734590278278": {"value": row['saas_customer_type']}, # 门店分层
"_widget_1734590278277": {"value": row['group_grade']}, # 公司等级
"_widget_1734590278276": {"value": row['limit_user_type']}, # 限制账户类型
"_widget_1734590278275": {"value": row['active_user_type']}, # 有效账户类型
"_widget_1734590278274": {"value": row['saas_version']}, # ERP操作模式
"_widget_1734590278273": {"value": row['saas_use_year']}, # 使用时长
"_widget_1734590278272": {"value": row['org_stage']}, # 门店阶段
"_widget_1734590278271": {"value": row['manage_model']}, # 经营模式
"_widget_1734590278267": {"value": row['contacts']}, # 联系人
"_widget_1734590278287": {"value": row['contact_mobile']}, # 联系手机号
"_widget_1734590278286": {"value": row['saas_edition_fmt']}, # SaaS版本
"_widget_1734590278280": {"value": row['org_code']}, # 门店编码
# "_widget_1735287791875": {"value": row['salesmen']}, # 销售负责人
"_widget_1735096489244": {"value": distribution_date}, # 派发时间
"_widget_1742895342914": {"value": row['business_scope_fmt']}, # 经营范围
"_widget_1742895342915": {"value": row['station_number']}, # 工位数
"_widget_1742895342916": {"value": [upload_key]} # 门头照片
})
routine_follow_up_payload = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "675b9c63925cd404038a6b86",
"is_start_workflow": "true",
"data": payload_dict,
"transaction_id": UUid
}
# res = api_instance.data_batch_create(routine_follow_up_payload)
# logger.info(f"创建结果:{res}")
time.sleep(1)
# print(res)
except:
pass
# common_module.send_task_status(task_start_time, "续约客户回访")
logger.info("续约客户回访任务完成")
except Exception as e:
# common_module.send_task_error(task_start_time, "续约客户回访", str(e))
error_task_logger.error(f"续约客户回访任务执行时发生异常: {e}")
if __name__ == '__main__':
start = RenewServicesRevisit()
start.main()
@@ -0,0 +1,899 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 数据库验证脚本 - 数据处理部分\n",
"\n",
"本notebook用于调试和验证数据库验证脚本的数据处理逻辑(260-425行)\n",
"\n",
"## 使用说明\n",
"1. 先执行数据加载部分(第2个单元格),这部分比较耗时\n",
"2. 数据加载完成后,再执行后续的数据处理单元格\n",
"3. 每个单元格都可以单独执行和调试\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T06:53:03.604128900Z",
"start_time": "2026-01-16T06:53:01.840121200Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"库导入完成\n",
"项目根目录: D:\\Idea Project\\SaaS_V1.7\n"
]
}
],
"source": [
"# 导入必要的库\n",
"import os\n",
"import sys\n",
"import pandas as pd\n",
"import datetime\n",
"from datetime import datetime, timedelta\n",
"import re\n",
"\n",
"# 添加项目根目录到路径(notebook文件在test目录下,需要添加父目录)\n",
"current_dir = os.getcwd()\n",
"# 如果当前目录是test,则添加父目录;否则添加当前目录\n",
"if os.path.basename(current_dir) == 'test':\n",
" project_root = os.path.dirname(current_dir)\n",
"else:\n",
" project_root = current_dir\n",
"sys.path.insert(0, project_root)\n",
"\n",
"from api import API\n",
"from back_ground_module import CommonModule\n",
"from log_config import configure_task_logger, configure_error_task_logger\n",
"\n",
"# 初始化API和CommonModule\n",
"api_instance = API()\n",
"common_module = CommonModule()\n",
"\n",
"# 获取日志记录器\n",
"logger = configure_task_logger()\n",
"error_task_logger = configure_error_task_logger()\n",
"\n",
"print(\"库导入完成\")\n",
"print(f\"项目根目录: {project_root}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 步骤1: 数据加载(耗时操作,可单独执行)\n",
"\n",
"这部分会加载所有必要的数据,包括:\n",
"- 省市区人员关系表\n",
"- 员工ID列表\n",
"- 权限表\n",
"- NGV数据列表\n",
"- 服务提醒数据\n",
"- 智能检测数据\n",
"- 功能使用情况表\n",
"- 保单识别表\n",
"- 私域/公域小程序数据\n",
"- 异业合作数据\n",
"- 短信数据\n",
"- 多公司过滤表\n",
"- NGV明细数据(从数据库获取)\n",
"- 节假日列表\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 数据加载部分 ==========\n",
"# 这部分比较耗时,可以单独执行\n",
"\n",
"print(\"开始加载数据...\")\n",
"\n",
"# 省市区人员关系表\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676512ac3e54dc3159460c0a\"}\n",
"json_dict = api_instance.entry_data_list(payload)\n",
"if json_dict and \"data\" in json_dict:\n",
" json_list = json_dict.get(\"data\")\n",
"else:\n",
" print(\"加载省市区人员关系表失败\")\n",
" json_list = []\n",
"print(f\"省市区人员关系表: {len(json_list)} 条\")\n",
"\n",
"# 获取简道云员工id\n",
"payload = {\"api_key\": \"6694d3c4fcb69ca9a111a6c4\", \"entry_id\": \"6769204a1902c9341340a1bc\"}\n",
"staff_id = api_instance.entry_data_list(payload)\n",
"staff_id_list = staff_id.get(\"data\")\n",
"print(f\"员工ID列表: {len(staff_id_list)} 条\")\n",
"\n",
"# 获取权限表信息\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"675b96c14e839f90fef1647c\"}\n",
"permissions_table = api_instance.entry_data_list(payload).get(\"data\")\n",
"print(f\"权限表: {len(permissions_table)} 条\")\n",
"\n",
"# 获取NGV数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"675bb02bd2d53c2034c665e4\"}\n",
"NGV_data_list = api_instance.entry_data_list(payload).get(\"data\")\n",
"print(f\"NGV数据列表: {len(NGV_data_list)} 条\")\n",
"\n",
"# 获取服务提醒-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676bb7bda3029720f1083e99\"}\n",
"service_remind = api_instance.entry_data_list(payload).get(\"data\")\n",
"print(f\"服务提醒数据: {len(service_remind)} 条\")\n",
"\n",
"# 获取智能检测-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676bb99649ab3ac975af6e39\"}\n",
"Smart_detection = api_instance.entry_data_list(payload).get(\"data\")\n",
"print(f\"智能检测数据: {len(Smart_detection)} 条\")\n",
"\n",
"# 获取功能使用情况表\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"6763bbf657bd8fb76fcb41b2\"}\n",
"get_feature_usage = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"print(f\"功能使用情况表: {len(get_feature_usage)} 条\")\n",
"\n",
"# 获取保单识别表\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"6773a60d30ed87ff9f68d3c5\"}\n",
"policy_recognition = api_instance.entry_data_list(payload).get(\"data\")\n",
"widget_list = [item['_widget_1735632397600'] for item in policy_recognition]\n",
"print(f\"保单识别表: {len(policy_recognition)} 条\")\n",
"\n",
"# 获取私域小程序-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e0f0fae622896749ba5087\"}\n",
"private_domain = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"print(f\"私域小程序数据: {len(private_domain)} 条\")\n",
"\n",
"# 获取公域小程序-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e0c702c8f603b997980999\"}\n",
"public_domain = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"public_domain_list = [item['_widget_1742784257506'] for item in public_domain]\n",
"print(f\"公域小程序数据: {len(public_domain)} 条\")\n",
"\n",
"# 获取异业合作-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e24fdd8dfcfa918e17c30b\"}\n",
"different_industries = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"different_industries_list = [item['_widget_1742884829007'] for item in different_industries]\n",
"print(f\"异业合作数据: {len(different_industries)} 条\")\n",
"\n",
"# 获取短信-数据支持表单数据\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e5107198ba1b20d5df3974\"}\n",
"groupnotification = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"print(f\"短信数据: {len(groupnotification)} 条\")\n",
"\n",
"# 获取多公司过滤表\n",
"payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"689bf5f8ba88a28cb0679ec9\"}\n",
"get_filter_company_list = api_instance.entry_data_list(payload).get(\"data\", [])\n",
"print(f\"多公司过滤表: {len(get_filter_company_list)} 条\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\n",
"# 获取节假日列表\n",
"date_list = common_module.get_holiday_list()\n",
"print(f\"节假日列表: {len(date_list)} 个日期\")\n",
"\n",
"# 获取NGV明细数据(从数据库获取,比较耗时)\n",
"print(\"\\n开始从数据库获取NGV明细数据(这可能需要一些时间)...\")\n",
"data_NGV = common_module.get_ngv_details(days_back=1)\n",
"print(f\"NGV明细数据: {len(data_NGV)} 条\")\n",
"print(f\"NGV明细数据列数: {len(data_NGV.columns)}\")\n",
"\n",
"# 构建省市区索引\n",
"def build_index(json_list):\n",
" index = {}\n",
" for json_item in json_list:\n",
" try:\n",
" key = (json_item['_widget_1734677164861'], json_item['_widget_1734677164862'],\n",
" json_item['_widget_1734677164863']) # 省市区\n",
" if '_widget_1734677164871' not in json_item: # 日常回访客服\n",
" raise KeyError(\"缺少 '日常回访客服' 键\")\n",
" index[key] = json_item\n",
" except KeyError as e:\n",
" print(f\"警告:{e},跳过该条记录\")\n",
" continue\n",
" return index\n",
"\n",
"index = build_index(json_list)\n",
"print(f\"省市区索引构建完成: {len(index)} 条\")\n",
"\n",
"print(\"\\n========== 数据加载完成 ==========\")\n",
"print(f\"数据加载时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 步骤2: 数据处理逻辑(260-425行)\n",
"\n",
"这部分包含主要的数据处理逻辑,包括:\n",
"- 获取多公司过滤公司id\n",
"- 数据清洗和转换\n",
"- 优先级排序\n",
"- 数据过滤和合并\n",
"- 日期计算和扩展\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 获取多公司过滤公司id ==========\n",
"logger.info(\"获取多公司过滤公司id\")\n",
"all_filter_company_list = [] # 获取多公司过滤公司id\n",
"for company in get_filter_company_list:\n",
" company_list = company.get(\"_widget_1755052002491\")\n",
" if company_list:\n",
" for company_item in company_list:\n",
" if company_item.get(\"_widget_1755052002496\") == \"否\":\n",
" all_filter_company_list.append(company_item.get(\"_widget_1755052002495\"))\n",
"logger.info(f\"过滤公司条数:{len(all_filter_company_list)}\")\n",
"print(f\"过滤公司条数: {len(all_filter_company_list)}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:27:13.175060200Z",
"start_time": "2026-01-16T07:27:09.857076900Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"数据预处理完成,当前数据量: 45672 条\n",
"数据列数: 143\n"
]
}
],
"source": [
"# ========== 数据预处理:日期转换和数据清洗 ==========\n",
"# 将A列和B列的日期字符串转换为日期格式\n",
"data_NGV = data_NGV.copy()\n",
"data_NGV['A'] = pd.to_datetime(data_NGV['expiry_time'])\n",
"data_NGV['B'] = pd.to_datetime(data_NGV['renew_date'])\n",
"\n",
"def replace_values(series):\n",
" # 使用条件判断来进行替换\n",
" return series.apply(lambda x: '' if pd.isna(x) or x in ['NA', 'None', ''] else x)\n",
"\n",
"# 处理字符串数据并显式指定数据类型\n",
"data_NGV = data_NGV.apply(replace_values)\n",
"\n",
"print(f\"数据预处理完成,当前数据量: {len(data_NGV)} 条\")\n",
"print(f\"数据列数: {len(data_NGV.columns)}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:28:01.298494800Z",
"start_time": "2026-01-16T07:28:01.106113700Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"过滤多公司后数据量: 45653 条\n"
]
}
],
"source": [
"# ========== 过滤多公司 ==========\n",
"# 针对公司主店过期,取公司最高等级版本派发\n",
"# 过滤多公司\n",
"data_NGV = data_NGV[~data_NGV['id_own_group'].isin(all_filter_company_list)]\n",
"print(f\"过滤多公司后数据量: {len(data_NGV)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:28:04.235079300Z",
"start_time": "2026-01-16T07:28:04.185820400Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"优先级映射完成\n"
]
}
],
"source": [
"# ========== 定义优先级顺序和创建映射字典 ==========\n",
"# 定义优先级顺序\n",
"edition_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']\n",
"customer_type_order = [\"F\", \"E\", \"D\", \"C\", \"B\", \"A\"] # 索引越小优先级越高\n",
"group_grade_order = ['全国KAFMVP', '区域KAMVP', '重要客户(SVIP', '普通客户(VIP']\n",
"\n",
"# 创建映射字典,并为不在列表中的值设置默认值\n",
"edition_map = {edition: idx for idx, edition in enumerate(edition_order)}\n",
"customer_type_map = {ctype: idx for idx, ctype in enumerate(customer_type_order)}\n",
"group_grade_map = {grade: idx for idx, grade in enumerate(group_grade_order)}\n",
"\n",
"# 添加用于排序的新列,并处理不在映射字典中的值\n",
"data_NGV['edition_rank'] = data_NGV['saas_edition_fmt'].map(edition_map).fillna(0).astype(int) # 缺失值用最高优先级填充\n",
"data_NGV['customer_type_rank'] = data_NGV['saas_customer_type'].map(customer_type_map).fillna(0).astype(int)\n",
"data_NGV['group_grade_rank'] = data_NGV['group_grade'].map(group_grade_map).fillna(0).astype(int)\n",
"\n",
"print(\"优先级映射完成\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:28:08.790102700Z",
"start_time": "2026-01-16T07:28:08.399298Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"最佳值查找完成\n"
]
}
],
"source": [
"# ========== 找到每组中的最佳值 ==========\n",
"# 找到每组中 edition_rank 最小值对应的行\n",
"best_edition_idx = data_NGV.groupby('id_own_group')['edition_rank'].idxmin()\n",
"best_edition_rows = data_NGV.loc[best_edition_idx]\n",
"best_edition_rows['max_saas_edition'] = best_edition_rows['saas_edition_fmt']\n",
"\n",
"# 找到每组中 customer_type_rank 最小值对应的行\n",
"best_customer_type_idx = data_NGV.groupby('id_own_group')['customer_type_rank'].idxmin()\n",
"best_customer_type_rows = data_NGV.loc[best_customer_type_idx]\n",
"best_customer_type_rows['max_saas_customer_type'] = best_customer_type_rows['customer_type_rank'].apply(\n",
" lambda x: customer_type_order[x])\n",
"\n",
"# 找到每组中 group_grade_rank 最小值对应的行\n",
"best_group_grade_idx = data_NGV.groupby('id_own_group')['group_grade_rank'].idxmin()\n",
"best_group_grade_rows = data_NGV.loc[best_group_grade_idx]\n",
"best_group_grade_rows['max_group_grade'] = best_group_grade_rows['group_grade']\n",
"\n",
"print(\"最佳值查找完成\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:28:11.374632Z",
"start_time": "2026-01-16T07:28:11.141730700Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"最佳值合并完成,当前数据量: 45653 条\n"
]
}
],
"source": [
"# ========== 合并最佳值回到原数据集 ==========\n",
"# 合并最佳值回到原数据集\n",
"best_values = (\n",
" best_edition_rows[['id_own_group', 'max_saas_edition']]\n",
" .merge(best_customer_type_rows[['id_own_group', 'max_saas_customer_type']], on='id_own_group',\n",
" how='outer')\n",
" .merge(best_group_grade_rows[['id_own_group', 'max_group_grade']], on='id_own_group', how='outer')\n",
")\n",
"\n",
"# 将最佳值合并回原数据集\n",
"data_NGV = data_NGV.merge(best_values, on='id_own_group', how='left')\n",
"\n",
"print(f\"最佳值合并完成,当前数据量: {len(data_NGV)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:30:06.358604500Z",
"start_time": "2026-01-16T07:30:05.752861600Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"============================================================\n",
"调试信息:处理主店过期情况\n",
"============================================================\n",
"\n",
"当前 data_NGV 数据量: 45653 条\n",
"\n",
"【字段检查】\n",
"is_main_org 数据类型: object\n",
"is_main_org 唯一值: ['0', '1']\n",
"is_main_org 值分布:\n",
"is_main_org\n",
"1 37628\n",
"0 8025\n",
"Name: count, dtype: int64\n",
"\n",
"org_status 数据类型: object\n",
"org_status 唯一值: ['留存', '过期']\n",
"org_status 值分布:\n",
"org_status\n",
"留存 27985\n",
"过期 17668\n",
"Name: count, dtype: int64\n",
"\n",
"org_type 数据类型: object\n",
"org_type 唯一值: ['一般', '天猫']\n",
"org_type 值分布:\n",
"org_type\n",
"一般 42985\n",
"天猫 2668\n",
"Name: count, dtype: int64\n",
"\n",
"【步骤1: 筛选主店过期】\n",
"警告: is_main_org 是字符串类型,尝试转换为数值\n",
"条件筛选结果数量: 15065 条\n",
"主店过期数据量 (ngvv2): 15065 条\n",
"ngvv2 中的 id_own_group 数量: 15065 个\n",
"ngvv2 中的 id_own_group 示例: ['10545055917999655906', '10545055917999678943', '10545055917999702656', '10545055917999726421', '10545055917999791008', '10545055917999907421', '10545055917999958815', '10545055917999963314', '10545055917999973061', '10545055918000062921']\n",
"\n",
"【步骤2: 筛选分店留存】\n",
"data_NGV_V2 初始数据量: 45653 条\n",
"\n",
"area_manager 唯一值数量: 16\n",
"area_manager 值分布(前10:\n",
"area_manager\n",
"肖军 10824\n",
"景东强 8408\n",
"陈庆伟 8322\n",
"张凯 8269\n",
"关磊 7028\n",
"孙玉蕾 2006\n",
"殷昊 556\n",
"王涛 161\n",
"刘伟 52\n",
" 8\n",
"Name: count, dtype: int64\n",
"\n",
"各条件筛选结果:\n",
" org_type == '一般': 42985 条\n",
" org_status == '留存': 27985 条\n",
" area_manager != '殷昊': 45097 条\n",
" area_manager != '孙玉蕾': 43647 条\n",
" is_main_org != 1: 8025 条\n",
"\n",
"所有条件合并后数据量: 4882 条\n",
"data_NGV_V2_filtered 中的 id_own_group 数量: 1564 个\n",
"data_NGV_V2_filtered 中的 id_own_group 示例: ['10545055917999659357', '10545055917999659357', '10545055917999688607', '10545055917999688607', '10545055917999688607', '10545055917999719687', '10545055917999791008', '10545055917999791008', '10545055917999995278', '10545055918000106937']\n",
"\n",
"【步骤3: 检查 id_own_group 交集】\n",
"ngvv2 中的 id_own_group 数量: 15065\n",
"data_NGV_V2_filtered 中的 id_own_group 数量: 1564\n",
"交集数量: 316\n",
"交集中的 id_own_group 示例: ['11240984669917478021', '10546172455175803787', '10546172455166018835', '10546172455220322161', '10546443563657780587', '10546172455213602644', '10546443563816611858', '11240984669917430021', '11240984669917329620', '11240984669917352563']\n",
"\n",
"【步骤4: 最终过滤】\n",
"过滤后的数据量: 468 条\n",
"\n",
"============================================================\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"C:\\Users\\hp_z66\\AppData\\Local\\Temp\\ipykernel_14280\\4275068286.py:100: SettingWithCopyWarning: \n",
"A value is trying to be set on a copy of a slice from a DataFrame.\n",
"Try using .loc[row_indexer,col_indexer] = value instead\n",
"\n",
"See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
" data_NGV_V2_filtered['exists_in_ngvv2'] = data_NGV_V2_filtered['id_own_group'].isin(ngvv2['id_own_group'])\n"
]
}
],
"source": [
"# ========== 处理主店过期的情况 ==========\n",
"# 调试信息:检查数据状态\n",
"print(\"=\" * 60)\n",
"print(\"调试信息:处理主店过期情况\")\n",
"print(\"=\" * 60)\n",
"print(f\"\\n当前 data_NGV 数据量: {len(data_NGV)} 条\")\n",
"\n",
"# 检查关键字段的数据类型和唯一值\n",
"print(f\"\\n【字段检查】\")\n",
"print(f\"is_main_org 数据类型: {data_NGV['is_main_org'].dtype}\")\n",
"print(f\"is_main_org 唯一值: {sorted(data_NGV['is_main_org'].unique())}\")\n",
"print(f\"is_main_org 值分布:\\n{data_NGV['is_main_org'].value_counts()}\")\n",
"\n",
"print(f\"\\norg_status 数据类型: {data_NGV['org_status'].dtype}\")\n",
"print(f\"org_status 唯一值: {sorted(data_NGV['org_status'].unique())}\")\n",
"print(f\"org_status 值分布:\\n{data_NGV['org_status'].value_counts()}\")\n",
"\n",
"print(f\"\\norg_type 数据类型: {data_NGV['org_type'].dtype}\")\n",
"print(f\"org_type 唯一值: {sorted(data_NGV['org_type'].unique())}\")\n",
"print(f\"org_type 值分布:\\n{data_NGV['org_type'].value_counts()}\")\n",
"\n",
"# 步骤1: 筛选主店过期的情况\n",
"print(f\"\\n【步骤1: 筛选主店过期】\")\n",
"# 确保 is_main_org 是数值类型\n",
"if data_NGV['is_main_org'].dtype == 'object':\n",
" print(\"警告: is_main_org 是字符串类型,尝试转换为数值\")\n",
" data_NGV['is_main_org'] = pd.to_numeric(data_NGV['is_main_org'], errors='coerce')\n",
"\n",
"condition = (data_NGV['is_main_org'] == 1) & (data_NGV['org_status'] == '过期')\n",
"print(f\"条件筛选结果数量: {condition.sum()} 条\")\n",
"\n",
"ngvv2 = data_NGV[condition]\n",
"print(f\"主店过期数据量 (ngvv2): {len(ngvv2)} 条\")\n",
"\n",
"if len(ngvv2) > 0:\n",
" print(f\"ngvv2 中的 id_own_group 数量: {ngvv2['id_own_group'].nunique()} 个\")\n",
" print(f\"ngvv2 中的 id_own_group 示例: {ngvv2['id_own_group'].head(10).tolist()}\")\n",
"else:\n",
" print(\"⚠️ 警告: ngvv2 为空,没有主店过期的情况!\")\n",
"\n",
"# 步骤2: 检查分店留存的情况\n",
"print(f\"\\n【步骤2: 筛选分店留存】\")\n",
"# 在合并最佳值之前保存原始数据副本(重要!)\n",
"data_NGV_V2 = data_NGV.copy()\n",
"print(f\"data_NGV_V2 初始数据量: {len(data_NGV_V2)} 条\")\n",
"\n",
"# 检查 area_manager 字段\n",
"print(f\"\\narea_manager 唯一值数量: {data_NGV_V2['area_manager'].nunique()}\")\n",
"print(f\"area_manager 值分布(前10:\\n{data_NGV_V2['area_manager'].value_counts().head(10)}\")\n",
"\n",
"# 确保 is_main_org 是数值类型\n",
"if data_NGV_V2['is_main_org'].dtype == 'object':\n",
" data_NGV_V2['is_main_org'] = pd.to_numeric(data_NGV_V2['is_main_org'], errors='coerce')\n",
"\n",
"# 逐步检查每个条件\n",
"cond1 = (data_NGV_V2['org_type'] == \"一般\")\n",
"cond2 = (data_NGV_V2['org_status'] == '留存')\n",
"cond3 = (data_NGV_V2['area_manager'] != '殷昊')\n",
"cond4 = (data_NGV_V2['area_manager'] != '孙玉蕾')\n",
"cond5 = (data_NGV_V2['is_main_org'] != 1)\n",
"\n",
"print(f\"\\n各条件筛选结果:\")\n",
"print(f\" org_type == '一般': {cond1.sum()} 条\")\n",
"print(f\" org_status == '留存': {cond2.sum()} 条\")\n",
"print(f\" area_manager != '殷昊': {cond3.sum()} 条\")\n",
"print(f\" area_manager != '孙玉蕾': {cond4.sum()} 条\")\n",
"print(f\" is_main_org != 1: {cond5.sum()} 条\")\n",
"\n",
"data_NGV_V2['条件'] = cond1 & cond2 & cond3 & cond4 & cond5\n",
"data_NGV_V2_filtered = data_NGV_V2.loc[data_NGV_V2[\"条件\"]]\n",
"print(f\"\\n所有条件合并后数据量: {len(data_NGV_V2_filtered)} 条\")\n",
"\n",
"if len(data_NGV_V2_filtered) > 0:\n",
" print(f\"data_NGV_V2_filtered 中的 id_own_group 数量: {data_NGV_V2_filtered['id_own_group'].nunique()} 个\")\n",
" print(f\"data_NGV_V2_filtered 中的 id_own_group 示例: {data_NGV_V2_filtered['id_own_group'].head(10).tolist()}\")\n",
"\n",
"# 步骤3: 检查交集\n",
"print(f\"\\n【步骤3: 检查 id_own_group 交集】\")\n",
"if len(ngvv2) > 0 and len(data_NGV_V2_filtered) > 0:\n",
" ngvv2_groups = set(ngvv2['id_own_group'].unique())\n",
" v2_groups = set(data_NGV_V2_filtered['id_own_group'].unique())\n",
" intersection = ngvv2_groups & v2_groups\n",
" \n",
" print(f\"ngvv2 中的 id_own_group 数量: {len(ngvv2_groups)}\")\n",
" print(f\"data_NGV_V2_filtered 中的 id_own_group 数量: {len(v2_groups)}\")\n",
" print(f\"交集数量: {len(intersection)}\")\n",
" \n",
" if len(intersection) > 0:\n",
" print(f\"交集中的 id_own_group 示例: {list(intersection)[:10]}\")\n",
" else:\n",
" print(\"⚠️ 警告: 没有交集!这可能是问题所在。\")\n",
" print(f\"ngvv2 中的前10个 id_own_group: {list(ngvv2_groups)[:10]}\")\n",
" print(f\"data_NGV_V2_filtered 中的前10个 id_own_group: {list(v2_groups)[:10]}\")\n",
"else:\n",
" print(\"⚠️ 警告: ngvv2 或 data_NGV_V2_filtered 为空,无法检查交集\")\n",
"\n",
"# 步骤4: 过滤存在的记录\n",
"print(f\"\\n【步骤4: 最终过滤】\")\n",
"if len(ngvv2) > 0:\n",
" data_NGV_V2_filtered['exists_in_ngvv2'] = data_NGV_V2_filtered['id_own_group'].isin(ngvv2['id_own_group'])\n",
" filtered_data = data_NGV_V2_filtered[data_NGV_V2_filtered['exists_in_ngvv2']]\n",
" print(f\"过滤后的数据量: {len(filtered_data)} 条\")\n",
" \n",
" if len(filtered_data) == 0:\n",
" print(\"\\n❌ 问题诊断:\")\n",
" print(\" 过滤后数据为空,可能的原因:\")\n",
" print(\" 1. ngvv2 为空(没有主店过期的情况)\")\n",
" print(\" 2. data_NGV_V2_filtered 为空(没有满足条件的分店留存数据)\")\n",
" print(\" 3. 两者的 id_own_group 没有交集\")\n",
" print(\"\\n建议:\")\n",
" print(\" - 检查数据源是否正确\")\n",
" print(\" - 检查字段值是否匹配(注意数据类型和格式)\")\n",
" print(\" - 检查是否有主店过期但分店留存的情况\")\n",
"else:\n",
" print(\"⚠️ 警告: ngvv2 为空,无法进行过滤\")\n",
" filtered_data = pd.DataFrame() # 创建空DataFrame\n",
"\n",
"print(\"\\n\" + \"=\" * 60)\n"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:30:16.487181500Z",
"start_time": "2026-01-16T07:30:16.440099400Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"排序去重后数据量: 316 条\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"C:\\Users\\hp_z66\\AppData\\Local\\Temp\\ipykernel_14280\\2835892650.py:5: SettingWithCopyWarning: \n",
"A value is trying to be set on a copy of a slice from a DataFrame.\n",
"Try using .loc[row_indexer,col_indexer] = value instead\n",
"\n",
"See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
" filtered_data['sort_key'] = filtered_data['saas_edition_fmt'].map(fixed_order_map)\n"
]
}
],
"source": [
"# ========== 对过滤数据进行排序和去重 ==========\n",
"fixed_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']\n",
"\n",
"fixed_order_map = {edition: index for index, edition in enumerate(fixed_order)}\n",
"filtered_data['sort_key'] = filtered_data['saas_edition_fmt'].map(fixed_order_map)\n",
"filtered_data = filtered_data.sort_values(by='sort_key').drop('sort_key', axis=1)\n",
"\n",
"result = filtered_data.drop_duplicates(subset='id_own_group', keep='first')\n",
"\n",
"print(f\"排序去重后数据量: {len(result)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 合并主店留存数据和分店数据 ==========\n",
"data_NGV['条件'] = (data_NGV['org_type'] == \"一般\") & (data_NGV['org_status'] == '留存') & (\n",
" data_NGV['area_manager'] != '殷昊') & (\n",
" data_NGV['area_manager'] != '孙玉蕾') & (\n",
" data_NGV['is_main_org'] == 1)\n",
"data_NGV = data_NGV.loc[data_NGV[\"条件\"]]\n",
"\n",
"data_NGV = pd.concat([data_NGV, result], axis=0)\n",
"data_details = data_NGV.copy()\n",
"\n",
"# 重置索引\n",
"data_details = data_details.reset_index(drop=True)\n",
"\n",
"print(f\"合并后数据量: {len(data_details)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:30:21.600199200Z",
"start_time": "2026-01-16T07:30:20.828225500Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"日期计算后数据量: 9845 条\n",
"需要扩展的数据行数: 9845 条\n"
]
}
],
"source": [
"# ========== 判断日期差并计算年数 ==========\n",
"# 判断A列的日期是否大于B列的日期730天,如果是的话,将B列的值设置为天数差\n",
"data_details['条件'] = data_details.apply(\n",
" lambda row: (\n",
" (pd.to_datetime(row['A']) - pd.to_datetime(row['B'])).days\n",
" if pd.to_datetime(row['A']) - pd.to_datetime(row['B']) >= pd.Timedelta(days=730)\n",
" else 0\n",
" ),\n",
" axis=1\n",
")\n",
"data_details = data_details.loc[data_details[\"条件\"] > 0]\n",
"\n",
"# 定义一个函数,用于将数字除以365并取整数\n",
"def divide_by_365(x):\n",
" if isinstance(x, (int, float)):\n",
" return int(x / 365)\n",
" else:\n",
" return x\n",
"\n",
"# 使用apply函数将divide_by_365函数应用到DataFrame的列\n",
"data_details['年'] = data_details['条件'].apply(divide_by_365)\n",
"\n",
"# 重置索引\n",
"data_details = data_details.reset_index(drop=True)\n",
"\n",
"print(f\"日期计算后数据量: {len(data_details)} 条\")\n",
"print(f\"需要扩展的数据行数: {len(data_details[data_details['年'] > 1])} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 扩展数据:根据年数复制行并修改日期 ==========\n",
"# 创建一个新的空的DataFrame\n",
"new_df = pd.DataFrame()\n",
"\n",
"# 遍历原始DataFrame的每一行\n",
"for index, row in data_details.iterrows():\n",
" # 根据年数来决定复制的次数\n",
" if row[\"renew_date\"] != \"2024-02-29\":\n",
" for i_new in range(1, row['年']):\n",
" # 修改日期\n",
" row_new = row.copy()\n",
" c = row_new[\"renew_date\"]\n",
" date_obj = datetime.strptime(c, \"%Y-%m-%d\")\n",
" new_year = date_obj.year + i_new\n",
" new_date_obj = date_obj.replace(year=new_year)\n",
" new_c = new_date_obj.strftime(\"%Y-%m-%d\")\n",
" row_new[\"renew_date\"] = new_c\n",
" # 将当前行添加到新的DataFrame中\n",
" new_df = pd.concat([new_df, pd.DataFrame([row_new])], ignore_index=True)\n",
"\n",
"print(f\"扩展后的新数据量: {len(new_df)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-16T07:39:28.813848800Z",
"start_time": "2026-01-16T07:39:28.255902200Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"合并后总数据量: 39599 条\n"
]
}
],
"source": [
"# ========== 合并原始数据和扩展数据 ==========\n",
"# 合并两个DataFrame\n",
"merged_df = pd.concat([data_NGV, new_df], axis=0, ignore_index=True)\n",
"data_details = merged_df.copy() # 替换名称\n",
"\n",
"data_details_not_null = data_details[data_details['renew_date'].notnull()]\n",
"# 重置索引\n",
"data_details_not_null = data_details_not_null.reset_index(drop=True)\n",
"data_details = data_details_not_null.copy() # 替换名称 v2\n",
"\n",
"print(f\"合并后总数据量: {len(data_details)} 条\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ========== 最终过滤:排除创建时间等于续约时间的记录 ==========\n",
"data_details['saas_create_time'] = data_details['saas_create_time'].str[:4] # 截取前4位(年份)\n",
"data_details['renew_date_new'] = data_details['renew_date'].str[:4] # 截取前4位(年份)\n",
"data_details = data_details[\n",
" data_details['saas_create_time'] != data_details['renew_date_new']] # 过滤掉等于renew_date的行\n",
"\n",
"data_details = data_details.reset_index(drop=True)\n",
"\n",
"logger.info(f\"过滤后的数据长度为: {len(data_details)}\")\n",
"print(f\"\\n========== 数据处理完成 ==========\")\n",
"print(f\"最终数据量: {len(data_details)} 条\")\n",
"print(f\"处理完成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 数据验证和检查\n",
"\n",
"可以在这里添加数据验证代码,检查处理结果的正确性\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"start_time": "2026-01-16T09:00:49.656903800Z"
}
},
"outputs": [],
"source": [
"# ========== 数据验证 ==========\n",
"# 查看数据基本信息\n",
"print(\"数据基本信息:\")\n",
"print(f\"数据形状: {data_details.shape}\")\n",
"print(f\"\\n数据列名:\")\n",
"print(data_details.columns.tolist())\n",
"\n",
"# 查看前几行数据\n",
"print(\"\\n前5行数据:\")\n",
"print(data_details.head())\n",
"\n",
"# 检查关键字段的数据分布\n",
"if 'saas_edition_fmt' in data_details.columns:\n",
" print(\"\\n版本分布:\")\n",
" print(data_details['saas_edition_fmt'].value_counts())\n",
"\n",
"if 'org_status' in data_details.columns:\n",
" print(\"\\n组织状态分布:\")\n",
" print(data_details['org_status'].value_counts())\n",
"\n",
"# 可以保存到CSV文件进行进一步检查\n",
"# data_details.to_csv(\"处理后的数据.csv\", index=False, encoding='utf-8-sig')\n",
"# print(\"\\n数据已保存到: 处理后的数据.csv\")\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+53
View File
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
payload ={
"app_id": "675b900991ad2491c69389ca",
"data": {
"_widget_1767851302591": 1772508166000,
"_widget_1767863644355": "何文娟",
"_widget_1767851302590": "1772507997862",
"_widget_1767851302593": [
"064863221620345741"
],
"_widget_1767851302592": "15651815921",
"_widget_1767851302595": [
"122744083333090577"
],
"_widget_1767851302594": "064863221620345741",
"_widget_1767851302597": "废弃测试三月分店",
"_widget_1767851302596": "CHS202603030049192",
"_widget_1767952153422": [
"洗车",
"美容"
],
"_widget_1772264444051": "19999.0",
"_widget_1767851302599": "普通客户(VIP",
"_widget_1767851302598": "zyc测试公司废弃",
"_widget_1768290159749": [
"122744083333090577"
],
"_widget_1768290159748": "华南区域",
"_widget_1768290159747": "15870306745529522117",
"_widget_1767863644358": "15651815921",
"_widget_1768290159746": "16338827489080131642",
"_widget_1767863644357": "何文娟",
"_widget_1767863644356": "15651815921",
"_widget_1767863644582": [
"064863221620345741"
],
"_widget_1767851302585": "XQFWD20260303001",
"_widget_1772264443873": "台湾省",
"_widget_1772703526239": "1",
"_widget_1772264443872": "其它",
"_widget_1767851302600": 1772508361000,
"_widget_1768290159737": 1773112966000,
"_widget_1767851302602": "皇冠版",
"_widget_1772264443811": "C",
"_widget_1772264443812": "5",
"_widget_1767851302604": "30"
}
,
"entry_id": "695f439e3e910f09190d8e99",
"is_start_workflow": true
}
+208
View File
@@ -0,0 +1,208 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "新签节点调用格式",
"id": "671b10d308af2bdc"
},
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2026-03-12T09:25:38.245095300Z",
"start_time": "2026-03-12T09:25:36.441415900Z"
}
},
"source": [
"import requests\n",
"url = \"https://manage-pre.f6yc.com/hive-admin/yida/updateNode\"\n",
"\n",
"\n",
"payload = {\n",
" \"nodeCode\": \"ORG_RESEARCH\",\n",
" \"needTraining\": \"是\",\n",
" \"impPrincipal\":\"['171408516124043808']\",\n",
" \"instanceId\": \"69b269d6c193f83cc27f65e7\"\n",
"}\n",
"\n",
"# [{\"_id\":\"69a285f3fa0d9fb1984da9bf\",\"name\":\"张乐乐\",\"username\":\"171408516124043808\",\"status\":1,\"type\":0}]\n",
"\n",
"response = requests.post(url, data=payload)\n",
"response.json()"
],
"outputs": [
{
"data": {
"text/plain": [
"{'code': 200, 'data': None, 'message': 'SUCCESS'}"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 14
},
{
"metadata": {},
"cell_type": "markdown",
"source": "# 续约联调",
"id": "6ce335625a1c1fbb"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-26T07:12:43.283568100Z",
"start_time": "2026-03-26T07:12:42.653902400Z"
}
},
"cell_type": "code",
"source": [
"# 可以引用一些第三方库.\n",
"import json\n",
"import requests\n",
"import time\n",
"import random\n",
"import time\n",
"import binascii # 【修正1】添加缺失的 binascii 导入\n",
"from pyDes import des, CBC, PAD_PKCS5\n",
"\n",
"data_id = \"69c4dc44986b303c2ef69d46\" # 数据id\n",
"orgid = \"16058986391127773239\" # 门店id\n",
"order_id = \"XYFWD20260326002\" # 服务单号\n",
"operation_consultant_str = '[{\"_id\":\"69a285f3fa0d9fb1984da9bf\",\"name\":\"张乐乐\",\"username\":\"171408516124043808\",\"status\":1,\"type\":0}]'# 专属人员顾问\n",
"\n",
"url = \"https://manage-pre.f6yc.com/hive-admin/py/yida/renewal/insertRenewalFormsData\"\n",
"\n",
"def des_encrypt(s):\n",
" \"\"\"\n",
" DES 加密\n",
" :param s: 原始字符串\n",
" :return: 加密后字符串,16进制\n",
" \"\"\"\n",
" secret_key = 'HwdMBW8o'\n",
" iv = secret_key\n",
" k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n",
" en = k.encrypt(s, padmode=PAD_PKCS5)\n",
" return binascii.b2a_base64(en, newline=False)\n",
"\n",
"\n",
"def des_descrypt(s):\n",
" \"\"\"\n",
" DES 解密\n",
" :param s: 加密后的字符串,16进制\n",
" :return: 解密后的字符串\n",
" \"\"\"\n",
" secret_key = 'HwdMBW8o'\n",
" iv = secret_key\n",
" k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n",
" de = k.decrypt(binascii.a2b_base64(s), padmode=PAD_PKCS5)\n",
" return de\n",
"\n",
"\n",
"impPrincipal_list = []\n",
"\n",
"if operation_consultant_str:\n",
" try:\n",
" # 将字符串 '[{...}]' 转换为 Python 列表 [{...}]\n",
" operation_list = json.loads(operation_consultant_str)\n",
"\n",
" # 确保解析出来的是列表\n",
" if isinstance(operation_list, list):\n",
" for operate in operation_list:\n",
" # 确保每一项是字典且包含 username\n",
" if isinstance(operate, dict) and \"username\" in operate:\n",
" impPrincipal_list.append(operate[\"username\"])\n",
" else:\n",
" # 如果解析出来不是列表(比如是个单对象),做兼容处理\n",
" if isinstance(operation_list, dict) and \"username\" in operation_list:\n",
" impPrincipal_list.append(operation_list[\"username\"])\n",
"\n",
" except json.JSONDecodeError:\n",
" # 如果解析失败,记录错误或保持列表为空\n",
" print(f\"JSON 解析失败: {operation_consultant_str}\")\n",
" impPrincipal_list = []\n",
"else:\n",
" operation_list = []\n",
"\n",
"impPrincipal_value=impPrincipal_list[0]\n",
"\n",
"t = time.time()\n",
"ts = int(round(t * 1000))\n",
"randint = random.randint(100000000, 999999999)\n",
"req = data_id + \"|\" + orgid + \"|\" +order_id+ \"|\" + impPrincipal_value + \"_\" + str(ts) + \"_\" + str(randint)\n",
"# 实例ID|门ID|服务单号|专属运营顾问\n",
"str_en = des_encrypt(req)\n",
"print(str_en.decode('utf-8'))\n",
"req_new = str_en.decode('utf-8')\n",
"payload = {\n",
" 'req':req_new,\n",
" 't':ts,\n",
" 'r':randint\n",
"}\n",
"\n",
"res = requests.post(url,data=payload)\n"
],
"id": "fc98d87aa8b19ce2",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Lmj1kXumBcGLR5IKAtgFDP2meGj6FgLzq7gaQ/6wDuZ3d43P0mtQCyUdQXkeC4kYxOT8w7n1xNxYhIy9PVq/xUR9emnP5CQweBFe8P3S9tRZdNFVOdipnD+xiof/q9b//C87kgUEIH7JT00nlBBkaQ==\n"
]
}
],
"execution_count": 2
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-13T08:12:33.285136400Z",
"start_time": "2026-03-13T08:12:33.246808900Z"
}
},
"cell_type": "code",
"source": "impPrincipal_value",
"id": "8edd45ef4657d8ed",
"outputs": [
{
"data": {
"text/plain": [
"'171408516124043808'"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 26
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+10
View File
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
import pandas as pd
import requests
url = "https://manage-pre.f6yc.com/hive-admin/yida/renewal/updateNode"
payload = {"instanceId":"15348c27-57b7-4285-b93b-87b3d41f5a28","nodeCode":"SIXTY","impPrincipal":"[\"053052302136860181\"]"}
result = requests.post(url, data=payload)
print(result.text)
+200
View File
@@ -0,0 +1,200 @@
import pandas as pd
import datetime
from config import Config
from api import API
import pymysql # 使用 pymysql 替代 mysql.connector
from back_ground_module import CommonModule
import os
import mysql.connector
import pandas as pd
import json
import numpy as np
import mysql.connector
from mysql.connector import Error
from log_config import configure_task_logger, configure_error_task_logger
import math
logger = configure_task_logger()
error_task_logger = configure_error_task_logger()
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
common_module = CommonModule()
api_instance = API()
class ProvinceCityPersonRelationToBI:
def __init__(self):
self.pvc_data = None
self.field_mapping = {
"": "_widget_1734677164861",
"": "_widget_1734677164862",
"运营顾问": "_widget_1734677164864",
"区域经理": "_widget_1734677164865",
"运营专家": "_widget_1734677164866",
"战区": "_widget_1734677164867",
"新签回访客服": "_widget_1734677164868",
"续约回访客服": "_widget_1734677164869",
"异常待办客服": "_widget_1734677164870",
"日常回访客服": "_widget_1734677164871",
}
def load_all_data(self):
payload = {"api_key": "675b900991ad2491c69389ca",
"entry_id": "676512ac3e54dc3159460c0a",
}
pvc_data = api_instance.entry_data_list(payload)
self.pvc_data = pvc_data.get("data") # api请求格式,将数据封装在data字典里
def data_process(self):
df = pd.DataFrame(self.pvc_data)
# 反转映射字典
reverse_mapping = {v: k for k, v in self.field_mapping.items()}
# 1.列明替换
df.columns = [reverse_mapping.get(col, col) for col in df.columns]
# 2.成员字段取值
user_columns = ["运营顾问", "区域经理", "运营专家", "新签回访客服", "续约回访客服",
"异常待办客服", "日常回访客服"]
for col in user_columns:
df[col] = df[col].map(lambda x: x.get("name", "") if isinstance(x, dict) else "")
# 3.根据省市去重
df = df.drop_duplicates(subset=['', ''])
return df
def clear_table_data(self):
"""
清空指定 MySQL 表的数据
参数已写死在函数内部直接调用即可
"""
# 数据库连接信息
HS_DB_Config = {
'host': "f6-public.rwlb.rds.aliyuncs.com",
'user': "rw_operation_data_relay",
'password': "m+q5Z4%IVuF9bf",
'database': "f6operation_data_relay"
}
table_name = "province_city_person_relation_to_bi" # 要清空的表名
connection = None
try:
# 建立数据库连接
connection = mysql.connector.connect(
host=HS_DB_Config["host"],
user=HS_DB_Config["user"],
password=HS_DB_Config["password"],
database=HS_DB_Config["database"]
)
if connection.is_connected():
cursor = connection.cursor()
# 使用TRUNCATE清空表数据
cursor.execute(f"TRUNCATE TABLE {table_name}")
connection.commit()
logger.info(f"成功清空表 {table_name} 中的所有数据")
except Error as e:
error_task_logger.error(f"清空表时发生错误: {e}")
if connection and connection.is_connected():
connection.rollback()
finally:
if connection and connection.is_connected():
cursor.close()
connection.close()
logger.info("数据库连接已关闭")
def write_to_bi(self, df):
HS_DB_Config = Config.HS_DB_Config
table_name = "province_city_person_relation_to_bi"
chunk_size = 1000 # 每批插入 1000 行
# 清理 DataFrame 中的 NaN/None 等值
df = df.replace([None, np.nan, pd.NA, 'nan', 'NaN', 'NAN', ''], None)
connection = mysql.connector.connect(
host=HS_DB_Config["host"],
user=HS_DB_Config["user"],
password=HS_DB_Config["password"],
database=HS_DB_Config["database"]
)
cursor = connection.cursor()
try:
# 获取数据库表的列名
cursor.execute(f"SHOW COLUMNS FROM `{table_name}`")
db_columns = [col[0] for col in cursor.fetchall()]
# 保留与数据库匹配的列
filtered_df = df[df.columns.intersection(db_columns)]
if filtered_df.empty:
print("DataFrame 中没有与数据库表结构匹配的列。")
return
# 处理 dict/list 类型字段:转为 JSON 字符串
filtered_df = filtered_df.copy()
for col in filtered_df.columns:
if filtered_df[col].apply(lambda x: isinstance(x, (dict, list)) if x is not None else False).any():
filtered_df[col] = filtered_df[col].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else x
)
# 构建 INSERT 语句(只构建一次)
columns = [f"`{col}`" for col in filtered_df.columns]
placeholders = ', '.join(['%s'] * len(columns))
insert_sql = f"INSERT INTO `{table_name}` ({', '.join(columns)}) VALUES ({placeholders})"
total_rows = len(filtered_df)
num_chunks = math.ceil(total_rows / chunk_size)
for i in range(num_chunks):
start_idx = i * chunk_size
end_idx = min(start_idx + chunk_size, total_rows)
chunk_df = filtered_df.iloc[start_idx:end_idx]
# 转为元组列表
data_to_insert = [
tuple(row) for row in chunk_df.values
]
# 批量执行(executemany 更高效)
cursor.executemany(insert_sql, data_to_insert)
connection.commit()
logger.info(f"成功写入 {total_rows} 条记录到 {table_name} 表中(分 {num_chunks} 批)。")
except Exception as e:
error_task_logger.error(f"写入数据库时发生错误: {e}", exc_info=True)
connection.rollback()
finally:
cursor.close()
connection.close()
def main(self):
task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
logger.info("任务开始")
# step1: 获取数据
self.load_all_data()
logger.info("加载数据完成")
# step2:数据处理
df = self.data_process()
# df.to_csv(os.path.join(output_dir, "new_dealer_service_order_to_bi.csv"))
logger.info("数据处理完成")
# step3:数据库删除
self.clear_table_data()
logger.info("目标数据库已清空")
# step4:数据写入BI
self.write_to_bi(df)
logger.info("数据已写入数据库中")
common_module.send_task_status(task_start_time, "省市区人员关系表转BI")
except Exception as e:
error_task_logger.error(f"省市区人员关系表转BI发生错误{e}")
common_module.send_task_error(task_start_time, "省市区人员关系表转BI", str(e))
if __name__ == '__main__':
province_city_person_relation_to_bi = ProvinceCityPersonRelationToBI()
province_city_person_relation_to_bi.main()
File diff suppressed because it is too large Load Diff
+249
View File
@@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
import os
import re
import sys
import pandas as pd
from tqdm import tqdm
# 让 test 脚本可以 import 到项目根目录的模块(api.py / yd_api.py / log_config.py
# 将项目根目录加入模块搜索路径,确保可以 import 根目录下的 api.py/yd_api.py
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
from api import API
from log_config import configure_error_task_logger, configure_task_logger
from yd_api import YDAPI
logger = configure_task_logger()
error_task_logger = configure_error_task_logger()
api_instance = API()
yd_api_instance = YDAPI()
# =========================
# 需要你关注/可能要改的配置
# =========================
# 简道云:续约待办表单(目标表单)
APP_ID = "675b900991ad2491c69389ca"
ENTRY_ID = "6931063d64187eaf6b927557"
# 宜搭:续约服务流程(来源流程)
APP_TYPE = "APP_UYZ0KG6L0CCNV80GZ66O"
SYSTEM_TOKEN = "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2"
# 简道云:员工表(用于把“区域经理姓名”转成“员工ID”,如果你简道云字段是人员控件,必须传ID)
STAFF_APP_ID = "6694d3c4fcb69ca9a111a6c4"
STAFF_ENTRY_ID = "6769204a1902c9341340a1bc"
STAFF_NAME_WIDGET = "_widget_1734942794144"
STAFF_ID_WIDGET = "_widget_1734942794145"
# 输入数据源必须包含这三列:
# - data_id:简道云待办数据的 data_id
# - 门店编码:用于简单校验(对不上会提示)
# - 宜搭实例ID:用于拉取宜搭实例详情
REQUIRED_INPUT_COLUMNS = ("data_id", "门店编码", "宜搭实例ID")
if __name__ == "__main__":
# 第 1 个命令行参数:你的数据源文件路径(csv/xlsx/xls
df = pd.read_excel(fr"C:\Users\hp_z66\Downloads\续约服务流程_20260326133517.xlsx",sheet_name="简道云-宜搭实例id", dtype=str).fillna("")
# 兼容列名:有的表叫“数据ID/实例ID”,统一成脚本内部使用的列名
df = df.rename(columns={"数据ID": "data_id", "dataId": "data_id", "DataID": "data_id", "实例ID": "宜搭实例ID"})
for c in REQUIRED_INPUT_COLUMNS:
if c not in df.columns:
raise SystemExit(f"缺少必需列: {c}")
for c in REQUIRED_INPUT_COLUMNS:
df[c] = df[c].astype(str).str.strip()
df = df[(df["data_id"] != "") & (df["宜搭实例ID"] != "")]
df = df.drop_duplicates(subset=["data_id"]).reset_index(drop=True)
# 读取员工表:构建 “姓名 -> 员工ID” 映射
staff_resp = api_instance.entry_data_list({"api_key": STAFF_APP_ID, "entry_id": STAFF_ENTRY_ID}) or {}
staff_list = staff_resp.get("data", []) or []
name_to_staff_id = {}
for item in staff_list:
n = str(item.get("_widget_1734942794144", "")).strip()
i = str(item.get("_widget_1734942794145", "")).strip()
if n and i:
name_to_staff_id[n] = i
logger.info(f"员工数: {len(name_to_staff_id)}")
# 读取简道云表单字段:label(中文名) -> name(_widget_xxx)
# 这样你只要改 labels 里的中文字段名,脚本会自动找到对应 widget_id
widget_list = api_instance.entry_widget_list({"api_key": APP_ID, "entry_id": ENTRY_ID}) or {}
widgets = widget_list.get("widgets", []) or []
label_to_name = {}
for w in widgets:
l = str(w.get("label", "")).strip()
n = str(w.get("name", "")).strip()
if l and n:
label_to_name[l] = n
# 需要同步到简道云的字段(中文 label)
# 如果你简道云字段名不是这些(比如“省份/城市/区县”),就在这里改成你表单里实际的 label
labels = ["120天是否联系上客户", "60天是否联系上客户", "30天是否联系上客户", "区域经理", "服务单号", "门店ID", "公司id", "", "",]
jdy_map = {l: label_to_name.get(l) for l in labels if label_to_name.get(l)}
miss = [l for l in labels if l not in jdy_map]
if miss:
logger.warning(f"简道云缺少字段: {miss}")
logger.warning(f"已匹配字段: {list(jdy_map.keys())} | 门店ID_widget={jdy_map.get('门店ID')} 公司id_widget={jdy_map.get('公司id')}")
# 获取宜搭 token(后续拉取实例详情使用)
token = yd_api_instance.generateToken()
ok = 0
fail = 0
skip = 0
for _, row in tqdm(df.iterrows(), total=len(df)):
data_id = str(row.get("data_id", "")).strip()
instance_id = str(row.get("宜搭实例ID", "")).strip()
store_code = str(row.get("门店编码", "")).strip()
if not data_id or not instance_id:
logger.warning(f"跳过:data_id/实例ID 为空 data_id={data_id} instance_id={instance_id} 行数据={row.to_dict()}")
skip += 1
continue
try:
# 拉取宜搭实例详情(里面的 formData 才是字段数据)
info = yd_api_instance.processes_instancesInfos(token, instance_id, APP_TYPE, SYSTEM_TOKEN) or {}
container = info.get("data") if isinstance(info, dict) else None
form = {}
if isinstance(container, dict):
form = container.get("formData") or container
if not isinstance(form, dict) or not form:
logger.warning(f"跳过:宜搭实例无表单数据 instance_id={instance_id} data_id={data_id}")
skip += 1
# 简单校验:输入的门店编码 vs 宜搭表单里的门店编码,不一致就打日志提醒
# 宜搭门店编码字段IDtextField_ksydghqw(来自你旧脚本/导出的 yd_process_details.csv
yd_store = str(form.get("textField_ksydghqw", "")).strip()
if yd_store and store_code and yd_store != store_code:
logger.warning(f"门店编码不一致: {store_code} vs {yd_store} 实例:{instance_id} data_id:{data_id}")
# 从 formData 中按字段ID取值:取到第一个非空值就返回
def pick(keys):
for k in keys:
v = form.get(k)
if v is None:
continue
s = str(v).strip()
if s and s.lower() not in {"nan", "null", "none"}:
return s
return ""
# 宜搭人员字段常见是:["张三(123)"] 这种结构,这里做一个“只拿姓名”的处理
def first_name(v):
s = str(v).strip()
if s.startswith("[") and s.endswith("]"):
inner = s[1:-1].split(",", 1)[0].strip().strip("'").strip('"')
s = inner
m = re.split(r"\(", s, maxsplit=1)
return m[0].strip() if m else s
# =========================
# 下面是“宜搭字段ID -> 业务字段”的取值逻辑(你最常修改的区域)
# =========================
# 说明:
# - 120/60/30 是否联系上客户:优先各节点字段,取不到就回退用统一字段 radioField_l85ppdia
# - 区域经理:employeeField_ksydghre
# - 服务单号:textField_kuntp6fl(你历史数据是 XYFWDxxxx
# - 省/市:textField_kuj8nx00 / textField_kuj8nx01
# - 区:优先 textField_kuhnydmk;取不到就从地址 textField_ksydghrm 里截取
v120 = pick(["radioField_ksydghrf",])
v60 = pick(["radioField_kuhnydmd", ])
v30 = pick(["radioField_kuhnydn0", ])
region_name = first_name(form.get("employeeField_ksydghre", ""))
# 如果简道云“区域经理”字段是人员控件:应传员工ID;否则传姓名也能写入(取决于你表单控件类型)
region_value = name_to_staff_id.get(region_name, region_name) if region_name else ""
service_no = pick(["textField_kuntp6fl"])
# 门店ID(强烈建议你把下面 keys 改成你宜搭里“门店ID/公司id”的真实字段ID)
# 示例:store_id = pick(["textField_orgid", "textField_id_own_org"])
store_id = pick(["textField_kuntp6fk"])
prov = pick(["textField_kuj8nx00"])
city = pick(["textField_kuj8nx01"])
# 拼装简道云更新 payload:每个字段必须是 {"value": 值}
data_dict = {}
if v120 and jdy_map.get("120天是否联系上客户"):
data_dict[jdy_map["120天是否联系上客户"]] = {"value": v120}
if v60 and jdy_map.get("60天是否联系上客户"):
data_dict[jdy_map["60天是否联系上客户"]] = {"value": v60}
if v30 and jdy_map.get("30天是否联系上客户"):
data_dict[jdy_map["30天是否联系上客户"]] = {"value": v30}
if region_value and jdy_map.get("区域经理"):
data_dict[jdy_map["区域经理"]] = {"value": region_value}
if service_no and jdy_map.get("服务单号"):
data_dict[jdy_map["服务单号"]] = {"value": service_no}
# 门店ID优先写入“门店ID”,如果表单没有此字段则回退写入“公司id”
if store_id:
if jdy_map.get("门店ID"):
data_dict[jdy_map["门店ID"]] = {"value": store_id}
elif jdy_map.get("公司id"):
data_dict[jdy_map["公司id"]] = {"value": store_id}
if prov and jdy_map.get(""):
data_dict[jdy_map[""]] = {"value": prov}
if city and jdy_map.get(""):
data_dict[jdy_map[""]] = {"value": city}
if not data_dict:
reasons = []
if not v120:
reasons.append("120天:值空")
elif not jdy_map.get("120天是否联系上客户"):
reasons.append("120天:未映射")
if not v60:
reasons.append("60天:值空")
elif not jdy_map.get("60天是否联系上客户"):
reasons.append("60天:未映射")
if not v30:
reasons.append("30天:值空")
elif not jdy_map.get("30天是否联系上客户"):
reasons.append("30天:未映射")
if not region_value:
reasons.append("区域经理:值空")
elif not jdy_map.get("区域经理"):
reasons.append("区域经理:未映射")
if not service_no:
reasons.append("服务单号:值空")
elif not jdy_map.get("服务单号"):
reasons.append("服务单号:未映射")
if not store_id:
reasons.append("门店ID:值空")
elif not (jdy_map.get("门店ID") or jdy_map.get("公司id")):
reasons.append("门店ID:表单无对应字段(门店ID/公司id)")
if not prov:
reasons.append("省:值空")
elif not jdy_map.get(""):
reasons.append("省:未映射")
if not city:
reasons.append("市:值空")
elif not jdy_map.get(""):
reasons.append("市:未映射")
logger.warning(
f"跳过 data_id:{data_id} 实例:{instance_id} | 原因: {', '.join(reasons)} | "
f"取值: v120={v120} v60={v60} v30={v30} region_name={region_name} region_value={region_value} "
f"service_no={service_no} store_id={store_id} prov={prov} city={city}"
)
skip += 1
continue
# 更新简道云数据(同你旧脚本)
payload = {
"api_key": APP_ID,
"entry_id": ENTRY_ID,
"data_id": data_id,
"data": data_dict,
"is_start_trigger": False,## 目前宜搭有通知
}
res = api_instance.entry_data_update(payload)
# 兼容两种返回格式:有的返回 {'status':'success',...},有的直接返回 {'data':{...}}
if isinstance(res, dict) and (res.get("status") == "success" or isinstance(res.get("data"), dict)):
ok += 1
else:
fail += 1
logger.warning(f"更新失败 data_id:{data_id} 实例:{instance_id} 返回:{res}")
except Exception as e:
fail += 1
error_task_logger.error(f"同步异常 data_id:{data_id} 实例:{instance_id} 错误:{e}", exc_info=True)
logger.info(f"完成 同步成功:{ok} 失败:{fail} 跳过:{skip}")
@@ -0,0 +1,58 @@
import requests
import pandas as pd
cookies = {
'auth_token': 's%3A.9uztgExtmqUJXHCi00hv9SGq6eVYSvH%2BxQSwrox1Yls',
'fx-lang': 'zh_cn',
'tenantId': 'agndqbuttb7ipfciraxcokgqyu',
'AGL_USER_ID': 'a50da526-dd43-4a78-ace1-ba810a6f2168',
'_ga': 'GA1.1.626243428.1772260541',
'_clck': 'y7ldwu%5E2%5Eg3y%5E0%5E2250',
'_ga_JTDW9M3LHZ': 'GS2.1.s1772266909$o3$g0$t1772266909$j60$l0$h0',
'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D',
'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D',
'_csrf': 's%3AE3ildFF7aOT874ca1lQaPpJh.M4vLvJ9TdzyukOLVlNNYO9ysSRLlssqxQh1%2FwMawRCY',
'Hm_lvt_de47dd1629940fe88b02865de93dd9fe': '1774410585,1774417748,1774487358,1774503122',
'HMACCOUNT': 'A6A0585E8C70051D',
'Hm_lpvt_de47dd1629940fe88b02865de93dd9fe': '1774503273',
'GSuvNKHqfvX2r6v7P8HkZv2bow': 's%3ALj5VHNEzUxprmpIJHWpNg7SzUp7GR3H1.yp8HeYMA0ARAeFC0CqxucSfBxvudd4xO76ZwAACWvTc',
'JDY_SID': 's%3AZrD40YQAsP9Ui53M1j0lud3AvwbdgzEl.ljjCkitsu2tDtbx0kJTROsKeNh8QfX2mz6SehH%2B%2FD5E',
}
headers = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'content-type': 'application/json',
'origin': 'https://www.jiandaoyun.com',
'priority': 'u=1, i',
'referer': 'https://www.jiandaoyun.com/dashboard/app/675b900991ad2491c69389ca/form/6931063d64187eaf6b927557/edit',
'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0',
'x-csrf-token': 'Mape7z8g-8iFP_vxFm8qSzVprTb4XsGfvs1o',
'x-jdy-ver': '10.19.6',
'x-request-id': '7bb1d48a-0fdb-46b7-bf67-ebd52c1a1f9a',
# 'cookie': 'auth_token=s%3A.9uztgExtmqUJXHCi00hv9SGq6eVYSvH%2BxQSwrox1Yls; fx-lang=zh_cn; tenantId=agndqbuttb7ipfciraxcokgqyu; AGL_USER_ID=a50da526-dd43-4a78-ace1-ba810a6f2168; _ga=GA1.1.626243428.1772260541; _clck=y7ldwu%5E2%5Eg3y%5E0%5E2250; _ga_JTDW9M3LHZ=GS2.1.s1772266909$o3$g0$t1772266909$j60$l0$h0; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D; _csrf=s%3AE3ildFF7aOT874ca1lQaPpJh.M4vLvJ9TdzyukOLVlNNYO9ysSRLlssqxQh1%2FwMawRCY; Hm_lvt_de47dd1629940fe88b02865de93dd9fe=1774410585,1774417748,1774487358,1774503122; HMACCOUNT=A6A0585E8C70051D; Hm_lpvt_de47dd1629940fe88b02865de93dd9fe=1774503273; GSuvNKHqfvX2r6v7P8HkZv2bow=s%3ALj5VHNEzUxprmpIJHWpNg7SzUp7GR3H1.yp8HeYMA0ARAeFC0CqxucSfBxvudd4xO76ZwAACWvTc; JDY_SID=s%3AZrD40YQAsP9Ui53M1j0lud3AvwbdgzEl.ljjCkitsu2tDtbx0kJTROsKeNh8QfX2mz6SehH%2B%2FD5E',
}
df = pd.read_excel(fr"C:\Users\hp_z66\Downloads\续约服务流程_20260327115935.xlsx",sheet_name="需要查询补充120天节点数据", dtype=str).fillna("")
json_data = {
'skip': 0,
'limit': 20,
'appId': '675b900991ad2491c69389ca',
'entryId': '6931063d64187eaf6b927557',
'formId': '6931063d64187eaf6b927557',
'dataId': '69ab8f89837038e4e81bff21',
}
response = requests.post(
'https://www.jiandaoyun.com/_/admin/data/log/list_changes',
cookies=cookies,
headers=headers,
json=json_data,
)
@@ -0,0 +1,791 @@
import os
from datetime import datetime, timedelta, timezone
import pandas as pd
from tqdm import tqdm
from datetime import datetime, timezone
import pandas as pd
import os
from typing import Dict
import requests
import json
import time
import numpy as np # 导入numpy库用于处理numpy数组
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
class Config:
JIANDAOYUN_API_TOKEN = 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN' # token
class API:
def entry_data_list(self, data: dict, replace: bool = False, max_retries: int = 20) -> Dict: # 获取多条表单数据
"""
获取多条表单数据
:param max_retries: 最大重试次数
:param replace: 是否替换字段
:param data:
api_key: 应用id
entry_id: 表单id
:return:
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 app_key
'Content-Type': 'application/json'
}
all_data_batches = [] # 用于存储每次请求返回的数据批次
last_data_id = None
exit_flag = False
while True:
payload = json.dumps({
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"limit": 90,
"data_id": last_data_id,
"filter": data.get('filter', None)
})
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
if data_get["data"]:
all_data_batches.extend(data_get['data'])
last_data_id = data_get['data'][-1].get('_id')
print(f"已获取 {len(all_data_batches)} 条数据")
break # 成功则跳出循环
else:
if 'data' not in data_get or len(data_get['data']) == 0:
exit_flag = True
break
# logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
# logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
if retries > max_retries:
# error_task_logger.error(f"任务 {last_data_id}组 连续{max_retries}次请求失败,放弃此次请求。")
all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理
if exit_flag:
break
# 构建最终返回的字典
final_data = {
'data': all_data_batches # 'data' 键对应的值是列表的列表
}
# logger.info(f"获取了{len(all_data_batches)}条数据")
if replace:
print("进行了替换")
return_data = self.field_replacement(data, final_data) # 字段替换,由id替换为标签名
return return_data
else:
return final_data
def field_replacement(self, data: dict, data_get: dict) -> dict:
"""
字段替换将id替换为标签名即唯一值替换为表单中显示字段的名字
:param data: 简道云插件发送过来的data包含表单id数据id应用id
:param data_get: 简道云请求的数据一般是根据数据id获取到表单的数据
:return: 将根据数据id获取到的表单数据进行替换返回替换后的数据
"""
# 获取表单对应字段标签名称
widget_list = self.entry_widget_list(data)
# 检查widget_list是否有效
if not widget_list or 'widgets' not in widget_list or not isinstance(widget_list['widgets'], list):
raise ValueError("映射表没有接受到数据")
# 创建一个映射表,将_widget_名称映射到label
name_to_label = {widget['name']: widget['label'] for widget in widget_list['widgets']}
def replace_keys(obj):
"""递归替换字典中的键名"""
if isinstance(obj, dict):
new_dict = {}
for key, value in obj.items():
new_key = name_to_label.get(key, key)
new_dict[new_key] = replace_keys(value)
return new_dict
elif isinstance(obj, list):
return [replace_keys(item) for item in obj]
else:
return obj
# 复制 data_get,避免修改原始数据
data_get_copy = json.loads(json.dumps(data_get)) # 深拷贝
if 'data' in data_get_copy:
data_get_copy['data'] = replace_keys(data_get_copy['data'])
return data_get_copy
@staticmethod
def workflow_instance_get(data: dict, max_retries: int = 20) -> dict:
"""
查询实例流程信息
:param max_retries:
:param data: 简道云插件发送过来的data包含应用id
:return: 查询简道云流程实例信息返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v6/workflow/instance/get'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey
'Content-Type': 'application/json'
}
payload = json.dumps({
"instance_id": data['data_id'],
"tasks_type": 1
}
)
print("payload:", payload)
data_get = None
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
# res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
# print( "返回结果:", data_get)
if res.status_code == 200:
break # 成功则跳出循环
else:
# logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
# logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
if retries > max_retries:
# error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。")
print("请求失败")
return data_get
@staticmethod
def entry_data_update(data: dict, max_retries: int = 20) -> dict: # 修改数据
"""
修改数据
:param max_retries: 最大重试次数此处设置100次
:param data: 简道云插件发送过来的data包含应用id表单id数据id等信息
:return: 修改数据后简道云返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/update'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey
'Content-Type': 'application/json'
}
payload = json.dumps({
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"data_id": data['data_id'], # 数据ID
"data": data['data'],
"is_start_trigger": True
}
)
data_get = None
retries = 0
while retries <= max_retries:
try:
res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10)
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
# print(data_get)
if res.status_code == 200:
break # 成功则跳出循环
else:
# logger.warning(f"请求异常, 将重新请求")
retries += 1
time.sleep(3) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
# logger.warning(f"请求异常: {e}, 将重新请求")
retries += 1
time.sleep(10) # 在重试之间稍作停顿
if retries > max_retries:
# error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。")
continue
return data_get
@staticmethod
def workflow_instance_end(data: dict, max_retries: int = 20) -> dict:
"""
关闭流程
:param max_retries:
:param data: 简道云插件发送过来的data包含应用id
:return: 查询简道云流程实例信息返回的结果
"""
url = 'https://api.jiandaoyun.com/api/v1/workflow/instance/close'
headers = {
'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey
'Content-Type': 'application/json'
}
payload = json.dumps({
"instance_id": data['data_id'],
}
)
print("payload:", payload)
data_get = None
retries = 0
while retries <= max_retries:
try:
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
# res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
data_get = res.json()
print("返回结果:", data_get)
if res.status_code == 200:
break # 成功则跳出循环
else:
retries += 1
time.sleep(3) # 在重试之间稍作停顿
except requests.exceptions.RequestException as e:
retries += 1
time.sleep(0.1) # 在重试之间稍作停顿
return data_get
class NpEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
else:
return super(NpEncoder, self).default(obj)
class YDAPI:
appKey = "ding5kqocon5s9oph5uq"
appSecret = "HL1jgsIIfLAC0eTH0A1m4mwxUDqbgsiPeCCGGE3ocM6qJBTIW7Ivt9drxF_Z4Kb_"
@staticmethod
def get_ids_query(token, formUuid, appType, systemToken, formInstanceIdList=None, max_retries=10, delay=2):
"""
函数功能读取表单的所有数据并加入重试机制
Args:
token (str): 登录验证token用于API调用的身份验证
formUuid (str): 表单唯一标识符用于指定需要读取哪个表单的实例数据
page (int): 分页参数指定请求的数据页码
n (int): 每页显示的数据条数
appType (str): 应用类型标识符默认为 "APP_UYZ0KG6L0CCNV80GZ66O"
systemToken (str): 系统token默认为固定值
instanceStatus (str): 流程实例状态默认为"RUNNING"
max_retries (int): 最大重试次数默认为10次
delay (int): 每次重试之间的延迟秒数默认为2秒
Returns:
dict: 返回从API获取的流程表单实例数据的JSON解析结果
Raises:
Exception: 如果达到最大重试次数仍未成功则抛出异常
"""
attempt = 0
api = f'https://api.dingtalk.com/v1.0/yida/forms/instances/ids/query'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
formData = {
"appType": appType,
"systemToken": systemToken,
"userId": "yida_pub_account", # 超级管理员账号
"language": "zh_CN",
"formUuid": formUuid,
"formInstanceIdList": formInstanceIdList,
}
# print(formData)
while True:
if attempt >= max_retries:
break
try:
res = requests.post(api, headers=headers, json=formData)
# print(res.json())
res.raise_for_status() # 如果返回状态码不是2xx,抛出异常
return res.json()
except requests.exceptions.RequestException as e:
time.sleep(delay)
attempt += 1
def generateToken(self) -> str:
"""
函数功能生成访问令牌token
Returns:
str: 返回生成的访问令牌字符串此token用于后续API调用的身份验证
"""
token_api = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
data = {
"appKey": f"{self.appKey}",
"appSecret": f'{self.appSecret}'
}
res = requests.post(token_api, json=data)
token = res.json().get('accessToken')
return token
def read_processes_instances(self, token, formUuid, page, n, appType="APP_UYZ0KG6L0CCNV80GZ66O",
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", instanceStatus="RUNNING",
max_retries=10, delay=2, createFromTimeGMT=None, createToTimeGMT=None,
modifiedFromTimeGMT=None,
modifiedToTimeGMT=None, searchFieldJson={}):
"""
函数功能读取流程表单的所有数据并加入重试机制
Args:
token (str): 登录验证token用于API调用的身份验证
formUuid (str): 表单唯一标识符用于指定需要读取哪个表单的实例数据
page (int): 分页参数指定请求的数据页码
n (int): 每页显示的数据条数
appType (str): 应用类型标识符默认为 "APP_UYZ0KG6L0CCNV80GZ66O"
systemToken (str): 系统token默认为固定值
instanceStatus (str): 流程实例状态默认为"RUNNING"
max_retries (int): 最大重试次数默认为10次
delay (int): 每次重试之间的延迟秒数默认为2秒
Returns:
dict: 返回从API获取的流程表单实例数据的JSON解析结果
Raises:
Exception: 如果达到最大重试次数仍未成功则抛出异常
"""
attempt = 0
api = f'https://api.dingtalk.com/v1.0/yida/processes/instances?pageNumber={page}&pageSize={n}'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
formData = {
"appType": appType,
"systemToken": systemToken,
"userId": "yida_pub_account", # 超级管理员账号
"language": "zh_CN",
"formUuid": formUuid,
"instanceStatus": instanceStatus, # 运行中
"createFromTimeGMT": createFromTimeGMT,
"createToTimeGMT": createToTimeGMT,
"modifiedFromTimeGMT": modifiedFromTimeGMT,
"modifiedToTimeGMT": modifiedToTimeGMT,
"searchFieldJson": json.dumps(
searchFieldJson
)
}
# print(formData)
while True:
if attempt >= max_retries:
# error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取流程实例数据,跳过本次请求。")
break
try:
res = requests.post(api, headers=headers, json=formData)
# print(res.json())
res.raise_for_status() # 如果返回状态码不是2xx,抛出异常
return res.json()
except requests.exceptions.RequestException as e:
# logger.warning(f"请求异常: {e},正在尝试第 {attempt + 1} 次重试...")
time.sleep(delay)
attempt += 1
def update_from(self, token, formInstanceId, data_new):
"""
函数功能更新表单内容
Args:
token (str): 登录验证token用于API调用的身份验证
formInstanceId (str): 表单实例ID读文件获取
data_new (dict): 新的数据内容用于替换现有表单实例中的数据读文件获取
Returns:
Response: 返回API请求的响应对象
"""
api = f'https://api.dingtalk.com//v1.0/yida/forms/instances'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
payload = {
"appType": "APP_UYZ0KG6L0CCNV80GZ66O",
"systemToken": "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2",
"userId": "yida_pub_account", # 曹伟 id
"language": "zh_CN",
"useLatestVersion": "false",
"formInstanceId": formInstanceId,
"updateFormDataJson": json.dumps(data_new, cls=NpEncoder),
}
res = requests.put(api, headers=headers, json=payload)
return res
def get_approval_records(self, token: str, processInstanceId: str, appType="APP_UYZ0KG6L0CCNV80GZ66O",
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", max_retries=10, delay=2):
"""
函数功能获取流程表单的审批记录适用于"F6客户服务"应用并且包含重试机制
Args:
token (str): 登录验证token用于API调用的身份验证
processInstanceId (str): 流程实例ID用于标识需要获取审批记录的具体流程实例
appType (str): 应用类型标识符默认为 "APP_UYZ0KG6L0CCNV80GZ66O"
systemToken (str): 系统token默认为固定值
max_retries (int): 最大重试次数默认为10次
delay (int): 每次重试之间的延迟秒数默认为2秒
Returns:
dict: 返回从API获取的审批记录的JSON解析结果通常包括审批步骤审批人审批时间等信息
"""
attempt = 0
userId = "yida_pub_account"
api = f'https://api.dingtalk.com/v1.0/yida/processes/operationRecords?appType={appType}&systemToken={systemToken}&userId={userId}&language=zh_CN&processInstanceId={processInstanceId}'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
while True:
if attempt >= max_retries:
# error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取审批数据,跳过本次请求。")
break
try:
res = requests.get(api, headers=headers)
res.raise_for_status() # 如果响应状态码不是2xx,则抛出HTTPError
return res.json()
except (requests.exceptions.RequestException, Exception) as e:
# logger.warning(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...")
time.sleep(delay) # 等待指定的延迟时间后再次尝试
attempt += 1
def aggree_approval(self, token: str, taskId: str, processInstanceId: str, formData: dict, res_new):
"""_summary_
函数功能同意审批节点 --F6客户服务 应用
Args:
token (str): 登录验证token
taskId (str): 获取到的审批节点ID
processInstanceId (str): 读取文件获得的实例ID
formData (dict): 数据样式
res_new (响应值): 从员工ID表里获取到员工名对应的员工ID
Returns:
响应值: 返回请求结果
"""
api = 'https://api.dingtalk.com/v1.0/yida/tasks/execute'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
payload = {
"outResult": "AGREE",
"appType": "APP_UYZ0KG6L0CCNV80GZ66O",
"systemToken": "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2",
"remark": "同意(接口自动)",
"formDataJson": json.dumps(formData, cls=NpEncoder),
"processInstanceId": processInstanceId,
"userId": res_new,
"language": "zh_CN",
"taskId": int(taskId)
}
res = requests.post(api, headers=headers, json=payload)
return res
api_instance = API()
yd_api_instance = YDAPI()
class YDToJDYRenewalToDo(object):
def __init__(self):
self.FORMID = "FORM-PE866MD1MJMU0WGLYRFLYEN5YN9L1I55Z7ZUK22"
self.appType = "APP_UYZ0KG6L0CCNV80GZ66O"
self.systemToken = "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2"
self.token = yd_api_instance.generateToken()
def load_all_data(self):
end_dt = datetime.now() + timedelta(days=1)
start_dt = end_dt - timedelta(days=90)
start_time = start_dt.strftime("%Y-%m-%d")
end_time = end_dt.strftime("%Y-%m-%d")
yd_data = yd_api_instance.read_processes_instances(
token=self.token,
formUuid=self.FORMID,
page=1,
n=100,
appType=self.appType,
systemToken=self.systemToken,
instanceStatus="",
modifiedFromTimeGMT=start_time,
modifiedToTimeGMT=end_time,
)
all_process_list = []
PAGES_two = yd_data.get('totalCount') // 100 + 1
for a in tqdm(range(1, PAGES_two + 1)):
try:
yd_data = yd_api_instance.read_processes_instances(
token=self.token,
formUuid=self.FORMID,
page=a,
n=100,
appType=self.appType,
systemToken=self.systemToken,
instanceStatus="",
modifiedFromTimeGMT=start_time,
modifiedToTimeGMT=end_time,
)
all_process_list = all_process_list + yd_data.get("data")
except Exception as e:
print(f"获取流程实例数据时出错: {e}")
continue
df_current = pd.DataFrame(all_process_list)
current_file = f"{output_dir}/{start_time}_{end_time}_all_process_list.csv"
# === 新增:读取上次文件并计算差值 ===
if os.path.exists(current_file):
try:
df_last = pd.read_csv(current_file)
except Exception as e:
print(f"读取历史文件失败: {e}")
df_last = pd.DataFrame()
else:
df_last = None # 明确标记:无历史文件
id_col = 'processInstanceId'
if df_last is not None and not df_last.empty and not df_current.empty and id_col in df_current.columns and id_col in df_last.columns:
# 有历史文件,计算新增
last_ids = set(df_last[id_col].astype(str))
current_ids = set(df_current[id_col].astype(str))
new_ids = current_ids - last_ids
diff_records = df_current[df_current[id_col].astype(str).isin(new_ids)].to_dict('records')
else:
# 没有历史文件 或 无法比对 → 返回全部当前数据
diff_records = df_current.to_dict('records') # ← 关键修改点
# 保存当前全量数据(覆盖)
df_current.to_csv(current_file, index=False)
return diff_records
def filter_renewal_data(self, all_process_list):
update_data_list = []
for item in all_process_list:
if item.get("data").get("textField_kto3q3ev"):
org_id = item.get("data").get("textField_ksydghqw")
order_code = item.get("data").get("textField_kto3q3ev") # 订单编码
pay_time = item.get("data").get("dateField_kto3q3ex") # 订单支付日期
res = yd_api_instance.get_ids_query(
token=self.token,
formInstanceIdList=[item.get("processInstanceId")],
formUuid="FORM-PE866MD1MJMU0WGLYRFLYEN5YN9L1I55Z7ZUK22",
appType="APP_UYZ0KG6L0CCNV80GZ66O",
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2",
)
payment_amount = None
bussiness_type = None
# print(res)
if res.get("result"):
form_data = res["result"][0]["formData"]
payment_amount = form_data.get("textField_kyjy1kkm") # "9987"
bussiness_type = form_data.get("textField_kyjy1kkn") # "续约"
# payment_amount = res.get("data").get("textField_kyjy1kkm") # 支付金额
# bussiness_type = res.get("data").get("textField_kyjy1kkn") # 业务类型
if pay_time:
# 确保是整数
timestamp_ms = int(pay_time)
# 转为 UTC datetime 对象
pay_datetime_utc = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
print(pay_datetime_utc) # 例如: 2024-04-05 12:34:38.901000+00:00
else:
pay_datetime_utc = None
update_data_list.append({
"order_code": order_code,
"pay_time": pay_time,
"payment_amount": payment_amount,
"bussiness_type": bussiness_type,
"org_id": org_id
})
return update_data_list
def check_jd_ydy_data(self, update_data_list):
def extract_widget_value(record, widget_id):
value = record.get(widget_id)
if isinstance(value, dict) and "value" in value:
return value.get("value")
return value
def has_value(value):
if value is None:
return False
if isinstance(value, str):
return value.strip() != ""
if isinstance(value, (list, dict)):
return len(value) > 0
return True
def pick_best_record(records):
if not records:
return None
time_fields = [
"_updateTime",
"_updated_at",
"_createTime",
"_created_at",
"updateTime",
"updated_at",
"createTime",
"created_at",
]
def extract_sort_value(record):
for field in time_fields:
value = record.get(field)
if value is None:
continue
try:
return int(value)
except Exception:
try:
return int(float(value))
except Exception:
continue
return 0
return max(records, key=extract_sort_value)
def query_jdy_by_org_id(org_id, flow_state=None):
cond = [{
"field": "_widget_1764820541661",
"type": "text",
"method": "eq",
"value": [org_id]
}]
if flow_state is not None:
cond.append({
"field": "flowState",
"type": "flowstate",
"method": "eq",
"value": [flow_state]
})
payload = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "6931063d64187eaf6b927557",
"filter": {
"rel": "and",
"cond": cond
}
}
return api_instance.entry_data_list(payload).get("data", [])
for item in update_data_list:
data_list = query_jdy_by_org_id(item.get("org_id"), flow_state=0)
if not data_list:
data_list = query_jdy_by_org_id(item.get("org_id"), flow_state=None)
result = pick_best_record(data_list)
data_id = result.get("_id") if result else None
if not data_id:
# print(f"未找到订单 {item.get('order_code')} 的简道云数据,跳过。")
continue
# 查询实例状态
instance_status = api_instance.workflow_instance_get({"data_id": data_id})
task_status = instance_status.get("status", -1)
print(f"订单 {item.get('order_code')} 的简道云流程状态为 {task_status}")
if task_status == 0:
print("简道云流程正在进行中,执行流程关闭")
api_instance.workflow_instance_end({"data_id": data_id})
target_values = {
"_widget_1764820541674": item.get("order_code"),
"_widget_1764820541679": item.get("pay_time"),
"_widget_1764820541676": item.get("payment_amount"),
"_widget_1764820541680": item.get("bussiness_type"),
}
data_to_update = {}
for widget_id, new_value in target_values.items():
existing_value = extract_widget_value(result, widget_id)
if has_value(existing_value):
continue
if not has_value(new_value):
continue
data_to_update[widget_id] = {"value": new_value}
if not data_to_update:
print(f"订单 {item.get('order_code')} 简道云字段已有值或无可写入内容,跳过同步")
continue
print("开始同步数据")
update_payload = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "6931063d64187eaf6b927557",
"data_id": data_id,
"data": data_to_update
}
print(update_payload)
api_instance.entry_data_update(update_payload)
print("数据同步完成")
def main(self):
task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
# step1 获取简道云与宜搭数据
jd_ydy_data = self.load_all_data()
if jd_ydy_data:
# step2 过滤已经续约的单子
update_data_list = self.filter_renewal_data(jd_ydy_data)
# step3 校验简道云是否有进行中的单子并关闭
self.check_jd_ydy_data(update_data_list)
else:
print("本次执行无处理数据")
except Exception as e:
print(e)
if __name__ == '__main__':
jd_ydy_renewal_to_do = YDToJDYRenewalToDo()
jd_ydy_renewal_to_do.main()
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import os
from datetime import datetime, timedelta, timezone
import pandas as pd
@@ -193,7 +194,8 @@ class API:
"app_id": data['api_key'], # 应用ID
"entry_id": data['entry_id'], # 表单ID
"data_id": data['data_id'], # 数据ID
"data": data['data']
"data": data['data'],
"is_start_trigger": True
}
)
@@ -583,20 +585,19 @@ class YDToJDYRenewalToDo(object):
print(f"读取历史文件失败: {e}")
df_last = pd.DataFrame()
else:
df_last = pd.DataFrame()
df_last = None # 明确标记:无历史文件
# 假设唯一标识字段为 'instanceId',请根据实际字段名调整
id_col = 'processInstanceId'
if not df_last.empty and not df_current.empty and id_col in df_current.columns and id_col in df_last.columns:
# 转为字符串确保可比
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:
# 没有历史文件 或 无唯一ID → 返回空 list(按你要求)
diff_records = []
# 没有历史文件 或 无法比对 → 返回全部当前数据
diff_records = df_current.to_dict('records') # ← 关键修改点
# 保存当前全量数据(覆盖)
df_current.to_csv(current_file, index=False)
-33
View File
@@ -1,33 +0,0 @@
from datetime import datetime
import os
from config import Config
import pandas as pd
from back_ground_module import CommonModule
from api import API
from log_config import configure_task_logger, configure_error_task_logger
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 YdToJDYRenewalToDo:
def __init__(self):
pass
def load_all_data(self):
"""
从各类来源加载数据上加载数据
:return:
"""
def main(self):
pass
if __name__ == '__main__':
yd_to_jd_renewal_to_do = YdToJDYRenewalToDo()
yd_to_jd_renewal_to_do.main()
-1
View File
@@ -122,7 +122,6 @@ class RenewalToDo:
def load_all_data(self):
"""
从各类来源加载数据上加载数据
:return:
"""
# 数据库获取续约回访数据
+114 -18
View File
@@ -5,7 +5,8 @@ from api import API
from back_ground_module import CommonModule
from log_config import configure_task_logger, configure_error_task_logger
from collections import defaultdict
from datetime import datetime, timezone, timedelta, date, UTC
from config import Config
logger = configure_task_logger()
error_task_logger = configure_error_task_logger()
api_instance = API()
@@ -14,7 +15,81 @@ output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
import pandas as pd
import psycopg2
from datetime import datetime, timedelta, date
import os
def get_renewal_details():
"""
从固定的数据库中获取续约待办数据先拉全量指定 date_id再用 CSV 中的门店编码在内存中过滤
"""
try:
# 1. 从 CSV 文件中读取门店编码
csv_path = r"D:\Idea Project\SaaS_V1.7\test\output\expanded_yd_data.csv"
if not os.path.exists(csv_path):
error_task_logger.error(f"CSV 文件不存在: {csv_path}")
return pd.DataFrame()
store_df = pd.read_csv(csv_path, dtype=str)
if "textField_ksydghqw" not in store_df.columns:
error_task_logger.error("CSV 文件中缺少列 'textField_ksydghqw'")
return pd.DataFrame()
store_codes = set(store_df["textField_ksydghqw"].dropna().unique()) # 转为 set 提升查找效率
if not store_codes:
error_task_logger.warning("CSV 中未找到有效的门店编码")
return pd.DataFrame()
# 2. 连接数据库
conn = psycopg2.connect(**Config.CONN_INFO)
cursor = conn.cursor()
# 获取前两天的 date_id(整数格式 YYYYMMDD
now_time = datetime.now()
yes_time = now_time + timedelta(days=-2)
yes_time_nyr = int(yes_time.strftime('%Y%m%d'))
# 3. 构造 SQL 查询:不再包含 org_code 过滤
sql = """
SELECT *
FROM "public"."holo_ads_report_saas_profile_ngv_detail_d"
WHERE "date_id" = %s
"""
# 执行查询(只传 date_id
cursor.execute(sql, (yes_time_nyr,))
rows = cursor.fetchall()
all_fields = cursor.description
# 转换为 DataFrame
col = [i[0] for i in all_fields]
data_NGV = pd.DataFrame(rows, columns=col) if rows else pd.DataFrame(columns=col)
# 关闭连接
cursor.close()
conn.close()
# 4. 在内存中用 store_codes 过滤 org_code
if "org_code" not in data_NGV.columns:
error_task_logger.error("数据库结果中缺少 'org_code' 字段")
return pd.DataFrame()
# 确保 org_code 是字符串类型(与 store_codes 一致)
data_NGV = data_NGV.copy() # 避免 SettingWithCopyWarning
data_NGV["org_code"] = data_NGV["org_code"].astype(str)
# 过滤:只保留 org_code 在 store_codes 中的行
filtered_data = data_NGV[data_NGV["org_code"].isin(store_codes)].reset_index(drop=True)
return filtered_data
except Exception as e:
error_task_logger.error(f"获取续约待办数据时出错: {e}", exc_info=True)
return pd.DataFrame()
class RenewalToDo:
"""续约回访待办派发"""
def __init__(self):
self.renewal_data_list = None
self.cyclic_increasing = None
@@ -84,6 +159,8 @@ class RenewalToDo:
"流程状态": "_widget_1765352838610",
"经营模式": "_widget_1765964381952",
"公司等级": "_widget_1766130435561",
"公司id": "_widget_1766631811839",
"订单商品名称": "_widget_1766730385209",
"提交人": "creator",
"提交时间": "createTime",
"更新时间": "updateTime"
@@ -101,6 +178,7 @@ class RenewalToDo:
"group_grade": "公司等级",
"technician": "运营专家",
"manage_model": "经营模式",
"id_own_group": "公司id",
}
self.subform_field_map = {
"商品名称": "_widget_1764820541719",
@@ -112,7 +190,7 @@ class RenewalToDo:
"续约后订单编码": "_widget_1764820541725",
# 根据实际需要添加更多字段
}
self.renewal_list_map ={
self.renewal_list_map = {
}
@@ -123,7 +201,9 @@ class RenewalToDo:
"""
# 数据库获取续约回访数据
self.data_NGV = pd.read_csv(os.path.join(output_dir, "data_NGV.csv"), encoding="gbk")
self.data_NGV = common_module.get_renewal_details()
# self.data_NGV = get_renewal_details() # 历史数据
self.data_NGV.to_csv("D:\\Idea Project\\SaaS_V1.7\\test\\output\data_NGV1.csv")
# 获取加盟商信息
self.franchisee = common_module.get_renewal_franchisee_details()
@@ -344,6 +424,10 @@ class RenewalToDo:
data_NGV['60天自动流转时间'] = data_NGV['过期日'] - pd.Timedelta(days=30)
data_NGV['30天自动流转时间'] = data_NGV['过期日'] - pd.Timedelta(days=0)
data_NGV['0天自动流转时间'] = data_NGV['过期日'] + pd.Timedelta(days=90)
data_NGV['120天是否跟进'] = "主动"
data_NGV['60天是否跟进'] = "主动"
data_NGV['30天是否跟进'] = "主动"
# 格式化为字符串(去掉时区)
for col in ['过期日', '120天自动流转时间', '60天自动流转时间', '30天自动流转时间', '0天自动流转时间']:
data_NGV[col] = data_NGV[col].dt.strftime('%Y-%m-%d %H:%M:%S')
@@ -356,24 +440,39 @@ class RenewalToDo:
)
# 新增上次购买价格列
# 1. 清洗并拼接类型+价格
df_lp = self.last_price[['门店编码', '类型', '价格']].copy()
# 1. 清洗数据
df_lp = self.last_price[['门店编码', '类型', '订单商品名称', '价格']].copy()
# 处理“类型”和“订单商品名称”的缺失值
df_lp['类型'] = df_lp['类型'].fillna('').astype(str)
df_lp['订单商品名称'] = df_lp['订单商品名称'].fillna('').astype(str)
# 处理价格:转数字、四舍五入、填0、转字符串
df_lp['价格'] = (
pd.to_numeric(df_lp['价格'], errors='coerce')
.round().fillna(0).astype(int).astype(str)
)
df_lp['类型_价格'] = df_lp['类型'] + df_lp['价格']
# 2. 按门店聚合,分号连接
agg_df = df_lp.groupby('门店编码', as_index=False)['类型_价格'].apply(';'.join)
# 2. 拼接“类型:价格”
df_lp['类型_价格'] = df_lp['类型'] + ':' + df_lp['价格']
# 3. 合并回主表
data_NGV = data_NGV.merge(agg_df, on='门店编码', how='left').fillna({'类型_价格': ''})
data_NGV.rename(columns={'类型_价格': '上次购买价格'}, inplace=True)
# 3. 按门店聚合两列
agg_df = df_lp.groupby('门店编码', as_index=False).agg({
'类型_价格': lambda x: ';'.join(x),
'订单商品名称': lambda x: ';'.join(x)
})
# 4. 处理没有匹配记录的门店(填空或默认值)
data_NGV['上次购买价格'] = data_NGV['上次购买价格'].fillna('')
# 4. 合并回主表
data_NGV = data_NGV.merge(agg_df, on='门店编码', how='left')
# 5. 填充缺失值为空字符串,并重命名列
data_NGV['类型_价格'] = data_NGV['类型_价格'].fillna('')
data_NGV['订单商品名称'] = data_NGV['订单商品名称'].fillna('')
data_NGV.rename(columns={
'类型_价格': '上次购买价格',
'订单商品名称': '订单商品名称'
}, inplace=True)
# 成员字段替换(现在列名是中文)
staff_name_cols = [
@@ -482,7 +581,7 @@ class RenewalToDo:
payload = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "6931063d64187eaf6b927557",
"entry_id": "6965eec36b73376aa0b5bff8",
"data_list": records
}
print(payload)
@@ -502,11 +601,8 @@ class RenewalToDo:
data_NGV = self.process_data()
# step3:数据派发
self.dispatch_task(data_NGV)
# step4:过期日发生变化更新已有表单
# step5:自动同意原表单
common_module.send_task_status(task_start_time, "续约回访待办")
# 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))
+69 -16
View File
@@ -233,6 +233,31 @@ class YDAPI:
appKey = "ding5kqocon5s9oph5uq"
appSecret = "HL1jgsIIfLAC0eTH0A1m4mwxUDqbgsiPeCCGGE3ocM6qJBTIW7Ivt9drxF_Z4Kb_"
@staticmethod
def _to_int_task_id(task_id):
if task_id is None:
raise ValueError("taskId is None")
if isinstance(task_id, bool):
raise ValueError(f"taskId is bool: {task_id}")
if isinstance(task_id, int):
return task_id
if isinstance(task_id, float):
if task_id.is_integer():
return int(task_id)
raise ValueError(f"taskId is non-integer float: {task_id}")
if isinstance(task_id, str):
s = task_id.strip()
if s.isdigit() or (s.startswith("-") and s[1:].isdigit()):
return int(s)
try:
f = float(s)
except ValueError as e:
raise ValueError(f"taskId is not numeric: {task_id}") from e
if f.is_integer():
return int(f)
raise ValueError(f"taskId is non-integer numeric string: {task_id}")
raise ValueError(f"taskId has unsupported type: {type(task_id).__name__}")
def generateToken(self) -> str:
"""
函数功能生成访问令牌token
@@ -301,10 +326,10 @@ class YDAPI:
while True:
if attempt >= max_retries:
# error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取流程实例数据,跳过本次请求。")
break
return {"data": []}
try:
res = requests.post(api, headers=headers, json=formData)
res = requests.post(api, headers=headers, json=formData, timeout=15)
# print(res.json())
res.raise_for_status() # 如果返回状态码不是2xx,抛出异常
return res.json()
@@ -313,6 +338,11 @@ class YDAPI:
# logger.warning(f"请求异常: {e},正在尝试第 {attempt + 1} 次重试...")
time.sleep(delay)
attempt += 1
except Exception:
time.sleep(delay)
attempt += 1
return {"data": []}
def update_from(self, token, formInstanceId, data_new):
"""
@@ -345,7 +375,7 @@ class YDAPI:
}
res = requests.put(api, headers=headers, json=payload)
res = requests.put(api, headers=headers, json=payload, timeout=15)
return res
def get_approval_records(self, token: str, processInstanceId: str, appType="APP_UYZ0KG6L0CCNV80GZ66O",
@@ -375,16 +405,17 @@ class YDAPI:
while True:
if attempt >= max_retries:
# error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取审批数据,跳过本次请求。")
break
return {"result": []}
try:
res = requests.get(api, headers=headers)
res = requests.get(api, headers=headers, timeout=15)
res.raise_for_status() # 如果响应状态码不是2xx,则抛出HTTPError
return res.json()
except (requests.exceptions.RequestException, Exception) as e:
# logger.warning(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...")
time.sleep(delay) # 等待指定的延迟时间后再次尝试
attempt += 1
return {"result": []}
def aggree_approval(self, token: str, taskId: str, processInstanceId: str, formData: dict, res_new):
"""_summary_
@@ -415,10 +446,10 @@ class YDAPI:
"processInstanceId": processInstanceId,
"userId": res_new,
"language": "zh_CN",
"taskId": int(taskId)
"taskId": self._to_int_task_id(taskId)
}
res = requests.post(api, headers=headers, json=payload)
res = requests.post(api, headers=headers, json=payload, timeout=15)
return res
@@ -449,9 +480,22 @@ class JDYToYDRenewalToDo(object):
"30天是否跟进": "_widget_1764820541632",
"30天处理人": "_widget_1764820541636",
"30天跟进时间": "_widget_1765352838633",
"是否联系上":"_widget_1764820541638",
"数据ID": "_id"
}
@staticmethod
def _as_list(val):
if val is None:
return []
if isinstance(val, list):
return val
if isinstance(val, tuple):
return list(val)
if isinstance(val, dict):
return [val]
return []
def load_all_data(self):
# 获取简道云已派发续约待办,若无数据直接返回
today_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d")
@@ -463,7 +507,7 @@ class JDYToYDRenewalToDo(object):
"value": [""]}]},
}
renewal = api_instance.entry_data_list(payload)
self.renewal_data_list = renewal.get("data") or []
self.renewal_data_list = self._as_list((renewal or {}).get("data"))
if not self.renewal_data_list:
self.renewal_data_df = pd.DataFrame()
return
@@ -478,7 +522,7 @@ class JDYToYDRenewalToDo(object):
all_data = []
for _, row in self.renewal_data_df.iterrows():
yd_data = yd_api_instance.read_processes_instances(
yd_resp = yd_api_instance.read_processes_instances(
token=self.token,
formUuid=self.FORMID,
page=1,
@@ -487,9 +531,12 @@ class JDYToYDRenewalToDo(object):
systemToken=self.systemToken,
instanceStatus="",
searchFieldJson={"textField_ksydghqw": row["_widget_1764820541661"]},
).get("data", [])
)
yd_data = self._as_list((yd_resp or {}).get("data"))
for record in yd_data:
if not isinstance(record, dict):
continue
enriched = {**record}
enriched.update({k: row.get(v, "") for k, v in self.follow_up_fields.items()})
all_data.append(enriched)
@@ -704,20 +751,20 @@ class JDYToYDRenewalToDo(object):
data = {"data_id": item["数据ID"]}
jdy_workflow_data = api_instance.workflow_instance_get(data) or {}
print("简道云流程日志:", jdy_workflow_data)
jdy_logs = jdy_workflow_data.get("logs", [])
jdy_logs = self._as_list(jdy_workflow_data.get("logs"))
jdy_logs = sorted(
jdy_logs,
key=lambda x: parse_dt(x.get("finish_time") or x.get("create_time")),
reverse=True,
)
tasks = jdy_workflow_data.get("tasks", []) or []
tasks = self._as_list(jdy_workflow_data.get("tasks"))
pending = [t for t in tasks if t.get("status") == 0]
task_candidate = (pending[0] if pending else None) or (sorted(
tasks,
key=lambda x: parse_dt(x.get("create_time")),
reverse=True
)[0] if tasks else {})
jdy_result_records = jdy_workflow_data.get("result", []) or []
jdy_result_records = self._as_list(jdy_workflow_data.get("result"))
jdy_stage_candidates = [
extract_stage_from_text(str(jdy_logs[0].get("flow_name", ""))) if jdy_logs else None,
@@ -740,7 +787,7 @@ class JDYToYDRenewalToDo(object):
systemToken=self.systemToken
) or {}
print("宜搭流程日志:", yd_workflow_data)
yd_results = yd_workflow_data.get("result", []) or []
yd_results = self._as_list(yd_workflow_data.get("result"))
# 展开宜搭 domainList 以获取所有动作
yd_records = [
@@ -753,7 +800,7 @@ class JDYToYDRenewalToDo(object):
key=lambda x: parse_dt(x.get("operateTimeGMT") or x.get("activeTimeGMT")),
reverse=True,
)
# 优先使用当前待办(type == TODO),否则用最新一条
# 优先使用当前待办(type == "TODO"),否则用最新一条
yd_todo = next((r for r in yd_records if str(r.get("type")).upper() == "TODO"), None)
yd_latest = yd_todo or (yd_records[0] if yd_records else {})
yd_stage = extract_stage_from_text(
@@ -784,6 +831,11 @@ class JDYToYDRenewalToDo(object):
if not task_id or not operator_user_id:
print(f"缺少 taskId 或 operatorUserId,无法自动同意 processInstanceId={item['processInstanceId']}")
continue
try:
task_id = yd_api_instance._to_int_task_id(task_id)
except Exception as e:
print(f"taskId 非法,跳过自动同意 processInstanceId={item['processInstanceId']} taskId={task_id} err={e}")
continue
try:
yd_api_instance.aggree_approval(
token=self.token,
@@ -798,10 +850,11 @@ class JDYToYDRenewalToDo(object):
def retrun_jdy(self, yd_update_list):
for item in yd_update_list:
data_id= str(item.get('数据ID'))
data = {
"api_key": "675b900991ad2491c69389ca",
"entry_id": "6931063d64187eaf6b927557",
"data_id": item.get('数据ID'),
"data_id": data_id,
"data":
{
"_widget_1766469131897": {"value": ""},
File diff suppressed because one or more lines are too long
+138
View File
@@ -0,0 +1,138 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "## 保存boss请求结果",
"id": "311a82d4faf8e2d"
},
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2025-11-10T06:24:23.858755Z",
"start_time": "2025-11-10T06:24:22.994108Z"
}
},
"source": [
"# 标准库\n",
"import os\n",
"import time\n",
"import random\n",
"import json\n",
"import binascii\n",
"from datetime import date, timedelta, datetime\n",
"from urllib.parse import quote\n",
"from pathlib import Path\n",
"\n",
"# 第三方库\n",
"import numpy as np\n",
"import pandas as pd\n",
"import requests\n",
"from pyDes import des, CBC, PAD_PKCS5\n",
"import mysql.connector\n",
"from mysql.connector import Error\n",
"\n",
"# PostgreSQL(如果你用到了)\n",
"import psycopg2\n",
"\n",
"# 自定义模块\n",
"from config import Config\n",
"from api import API\n",
"from back_ground_module import CommonModule\n",
"from log_config import configure_task_logger, configure_error_task_logger\n",
"\n",
"\n",
"logger = configure_task_logger()\n",
"error_task_logger = configure_error_task_logger()\n",
"api_instance = API()\n",
"common_module = CommonModule()\n",
"output_dir = \"output\" # 设置输出目录\n",
"os.makedirs(output_dir, exist_ok=True)\n",
"\n",
"def des_encrypt(s):\n",
" \"\"\"\n",
" DES 加密\n",
" :param s: 原始字符串\n",
" :return: 加密后字符串,16进制\n",
" \"\"\"\n",
" secret_key = 'HwdMBW8o'\n",
" iv = secret_key\n",
" k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n",
" en = k.encrypt(s, padmode=PAD_PKCS5)\n",
" return binascii.b2a_base64(en, newline=False)\n",
"\n",
"\n",
"def des_descrypt(s):\n",
" \"\"\"\n",
" DES 解密\n",
" :param s: 加密后的字符串,16进制\n",
" :return: 解密后的字符串\n",
" \"\"\"\n",
" secret_key = 'HwdMBW8o'\n",
" iv = secret_key\n",
" k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n",
" de = k.decrypt(binascii.a2b_base64(s), padmode=PAD_PKCS5)\n",
" return de\n",
"\n",
"data_NGV = common_module.get_renewal_details()\n",
"\n",
"\n",
"for i in range(0,len(data_NGV[\"date_fmt\"])):\n",
" t = time.time()\n",
" ts = int(round(t * 1000))\n",
" randint = random.randint(100000000, 999999999)\n",
" req = data_NGV['id_own_org'][i] + \"_\" + str(ts) + \"_\" + str(randint)\n",
" str_en = des_encrypt(req)\n",
" req_new = str_en.decode('utf-8')\n",
"\n",
" url = f\"http://manage.f6yc.com/hive-admin/py/yida/renewal/orgInfo\"\n",
" data = {\n",
" 'req':req_new,\n",
" 't':ts,\n",
" 'r':randint\n",
" }\n",
" res = requests.post(url,data=data)\n",
" # print(res.json.json())\n",
"\n",
" break\n",
"\n",
"print(len(data_NGV))"
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"距离今天还有120天的日期是:2026-03-10\n",
"29\n"
]
}
],
"execution_count": 2
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
,_id,提交人,updater,deleter,提交时间,更新时间,deleteTime,flowState,报备类型,协作内容,情况说明,订单编号,年限,版本,实付金额,商品名称,履约金额,门店编码,门店名称,支付日期,开户/处理日期,业绩归属日期,业绩类型,公司名称,公司ID,报备业绩金额-区域提交,业绩归属小六-区域提交,业绩归属月,是否同步衡石,小六业绩金额,区域业绩金额,报备业绩归属小六,报备业绩归属区域经理,报备业绩归属大区,原业绩归属人,原业绩归属区域经理,原业绩归属大区,小六业绩比例,区域业绩比例,运营专家,业绩动作,提成类型,新签阶段及提成比例,提成金额,SaaS新签提成比例,服务包提成比例,新签提成比例-首年,新签提成比例-非首年,提成动作,业绩类型-聚合,业绩分组,流程是否结束,appId,entryId
0,68d1039a408016fe13556b06,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:06:50,2025-12-25 15:32:02,,2,多年补差价,,6月订单,9月补差价2年,1757735155184,2,入门版,999,,,CHS202506010300195,上海汨晨汽车服务有限公司,2025-09-13 00:00:00,2025-06-01 00:00:00,2025-09-13 00:00:00,新签,,,,,,是,999.0,999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
1,68d103cdb8f662bfdf1b7ea2,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:07:41,2025-12-25 15:32:02,,2,新签超3年,,新签5年,1758076816064,2,标准版,3000,,,CHS202302130204882,江阴市华士爱驹汽车养护店,2025-09-17 00:00:00,2025-09-17 00:00:00,2025-09-17 00:00:00,新签,,,,,,是,3000.0,3000.0,赵旭伟,肖军,江苏,赵旭伟,肖军,江苏,1.0,1.0,陈博,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
2,68d103f17f34705c8dbe360a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:08:17,2025-12-25 15:32:02,,2,新签超3年,,新签5年,1758181826462,2,基础版,1600,,,CHS202504240297202,古城区鸿远轮胎服务中心,2025-09-18 00:00:00,2025-09-18 00:00:00,2025-09-18 00:00:00,新签,,,,,,是,1600.0,1600.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
3,68d10415f3728b4cd791630e,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:08:53,2025-12-25 15:32:02,,2,新签超3年,,5年订单,1758259644403,2,基础版,1759,,,CHS202509190309704,金堂县赵镇四达汽修厂,2025-09-19 00:00:00,2025-09-19 00:00:00,2025-09-19 00:00:00,新签,,,,,,是,1759.0,1759.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
4,68d1042ee3ee1af6d0d7f8ee,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:09:18,2025-12-25 15:32:02,,2,新签超3年,,,1756814144233,2,入门版,1000,,,CHS202509020309092,天津市滨海新区安驰汽车修理服务部,2025-09-03 00:00:00,2025-09-02 00:00:00,2025-09-03 00:00:00,新签,,,,,,是,1000.0,1000.0,王鑫,关磊,华北,王鑫,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
5,68d104502618b5ea53f4264f,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:09:52,2025-12-25 15:32:02,,2,新签超3年,,,1757245487196,2,基础版,1400,,,CHS202509070309251,车广角盘锦店,2025-09-08 00:00:00,2025-09-07 00:00:00,2025-09-08 00:00:00,新签,,,,,,是,1400.0,1400.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
6,68d10471c1d4a4211d2ce420,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:10:25,2025-12-25 15:32:02,,2,新签超3年,,,1757388589517,1,进阶版,1000,,,CHS202509090309331,上海德伽汽车服务中心,2025-09-09 00:00:00,2025-09-09 00:00:00,2025-09-09 00:00:00,新签,,,,,,是,1000.0,1000.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
7,68d104a081bf67abc88ce650,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:11:12,2025-12-25 15:32:02,,2,新签超3年,,,1757752521945,2,标准版,3000,,,CHS202509130309507,腾冲诚亿汽车修理有限公司,2025-09-14 00:00:00,2025-09-13 00:00:00,2025-09-14 00:00:00,新签,,,,,,是,3000.0,3000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
8,68d104e0304ea14df52c1128,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:12:16,2025-12-25 15:32:02,,2,品牌方协作,电子目录,电子目录,,,,12000,,,,,,,2025-09-22 16:12:16,,,,,,,是,0.0,0.0,杜浩,肖军,江苏,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
9,68d104fce01701d04e9ba14d,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:12:44,2025-12-25 15:32:02,,2,品牌方协作,电子目录,,,,,8000,,,,,,,2025-09-22 16:12:44,,,,,,,是,0.0,0.0,韩皞,陈庆伟,东北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
10,68d105177771c4e88d61d725,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:13:11,2025-12-25 15:32:02,,2,品牌方协作,电子目录,,,,,9000,,,,,,,2025-09-22 16:13:11,,,,,,,是,0.0,0.0,胡楠,景东强,西北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
11,68d2342ba541a358893d61ef,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-23 13:46:19,2025-12-25 15:32:02,,2,多年补差价,,,1758459953833,2,入门版,1000,,,CHS202505070297933,上海义诚汽车服务有限公司,2025-09-22 00:00:00,2025-05-07 00:00:00,2025-09-22 00:00:00,新签,,,,,,是,1000.0,1000.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
12,68d3bc8d99f9d49450ed8b1a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-24 17:40:29,2025-12-25 15:32:02,,2,品牌方协作,电子目录,电子目录,小六未参与,,,,9000,,,,,,,2025-09-24 17:40:29,,,,,,,是,0.0,0.0,胡楠,景东强,西北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
13,68d494d8c6f070c9f68a4e94,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-25 09:03:20,2025-12-25 15:32:02,,2,新签超3年,,,1758612872486,1,基础版,799,,,CHS202509230309865,回民区青辰宝悦汽车维修中心(个体工商户),2025-09-23 00:00:00,2025-09-23 00:00:00,2025-09-23 00:00:00,新签,,,,,,是,799.0,799.0,张宏伟,关磊,华北,张宏伟,关磊,华北,1.0,1.0,武宏超,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
14,68d5f2a0b3bc5add3c3d7a23,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:55:44,2025-12-25 15:32:02,,2,新签超3年,,,1758789984175,2,入门版,1000,,,CHS202509250310033,济南双江汽车服务有限公司,2025-09-26 00:00:00,2025-09-25 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,1000.0,1000.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
15,68d5f2c3f7765d3eddb8506a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:56:19,2025-12-25 15:32:02,,2,多年补差价,,,1758787369355,2,旗舰版,4000,,,CHS202505190299143,政德汽修,2025-09-26 00:00:00,2025-05-19 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,4000.0,4000.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
16,68d5f2eb8eb300e7d401f7f9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:56:59,2025-12-25 15:32:02,,2,新签超3年,,,1758786185157,2,标准版,3120,,,CHS202509250310019,红河州秀林工贸有限公司,2025-09-26 00:00:00,2025-09-25 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
17,68d8882a40f51cce582eddb5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-28 08:58:18,2025-12-25 15:32:02,,2,多年补差价,,,1758894296058,2,进阶版,2501,,,CHS202107030131629,灵山县车益汽车维修厂,2025-09-27 00:00:00,2025-08-09 00:00:00,2025-09-27 00:00:00,新签,,,,,,是,2501.0,2501.0,黄环宇,张凯,华南沪,黄环宇,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],125.05,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
18,68d9e2d9710265914f554379,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-29 09:37:29,2025-12-25 15:32:02,,2,跨区新签,,,1759047159482,3,旗舰版,7000,,,CHS202509280310174,荟星行汽车服务有限公司,2025-09-29 00:00:00,2025-09-28 00:00:00,2025-09-29 00:00:00,新签,,,,,,是,3500.0,3500.0,韩皞,陈庆伟,东北,张宏伟,关磊,华北,0.5,0.5,孙旭亮,拆单,,[],315.0,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
19,68d9f2549d0de29d680f6403,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-29 10:43:32,2025-12-25 15:32:02,,2,新签超3年,,,1759063075745,2,入门版,920,,,CHS202509280310180,宏运汽修,2025-09-29 00:00:00,2025-09-28 00:00:00,2025-09-29 00:00:00,新签,,,,,,是,920.0,920.0,柴铁峰,陈庆伟,东北,柴铁峰,陈庆伟,东北,1.0,1.0,刘立,新增,,[],46.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
20,68db3e73890706b7678f34ea,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-30 10:20:35,2025-12-25 15:32:02,,2,新签超3年,,,1757909706794,2,旗舰版,4400,,,CHS202509150309534,监利市壹加汽车服务有限公司,2025-09-15 00:00:00,2025-09-15 00:00:00,2025-09-15 00:00:00,新签,,,,,,是,4400.0,4400.0,陈煜,景东强,华中,陈煜,景东强,华中,1.0,1.0,刘光春,新增,,[],220.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
21,68db50ff8e50807e1e84e8f8,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-30 11:39:43,2025-12-25 15:32:02,,2,新签超3年,,,1759039606751,1,标准版,1500,,,CHS202509280310149,鄂尔多斯市心成泰汽车维修服务有限公司,2025-09-28 00:00:00,2025-09-28 00:00:00,2025-09-28 00:00:00,新签,,,,,,是,1500.0,1500.0,张宏伟,关磊,华北,张宏伟,关磊,华北,1.0,1.0,武宏超,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
22,68e71b9216dd2af696d3c489,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-09 10:18:58,2025-12-25 15:32:02,,2,新签超3年,,,1759573927652,1,基础版,800,,,CHS202303010210121,德系专修,2025-10-05 00:00:00,2025-10-04 00:00:00,2025-10-05 00:00:00,新签,,,,,,是,800.0,800.0,刘磊,关磊,华北,刘磊,关磊,华北,1.0,1.0,,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
23,68e768c27a4eb1ea616434b0,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-09 15:48:18,2025-12-25 15:32:02,,2,新签超3年,,,1759395920361,2,进阶版,2000,,,CHS202510020310341,官渡区众名汽车维修服务经营部,2025-10-03 00:00:00,2025-10-03 00:00:00,2025-10-03 00:00:00,新签,,,,,,是,2000.0,2000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],100.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
24,68e9ba0cd69c47d29c2e0972,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-11 09:59:40,2025-12-25 15:32:02,,2,新签超3年,,,1760012463416,1,进阶版,1000,,,CHS202510090310521,新乡骏享汽车销售服务有限公司,2025-10-10 00:00:00,2025-10-10 00:00:00,2025-10-10 00:00:00,新签,,,,,,是,1000.0,1000.0,王兵帅,张凯,河南,王兵帅,张凯,河南,1.0,1.0,邢恒岭,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
25,68eda452872200f9abed2d5d,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-14 09:16:02,2025-12-25 15:32:02,,2,多年补差价,,,1760180223661,2,进阶版,1999,,,CHS202507090303811,上海洗事临门汽车服务有限公司,2025-10-12 00:00:00,2025-07-09 00:00:00,2025-10-12 00:00:00,新签,,,,,,是,1999.0,1999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],99.95,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
26,68f830d486f984a8f5d58949,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-22 09:18:12,2025-12-25 15:32:02,,2,电销业绩统计,,,1760341341750,3,基础版,2900,,,CHS202411020284735,深圳康得新KDX大膜王星级甄选店,2025-10-13 00:00:00,2025-10-13 00:00:00,2025-10-13 00:00:00,新签,,,,,,是,0.0,2900.0,严冬延,张凯,华南沪,耿渝淇,张凯,,,1.0,,新增,,[],130.5,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
27,68f98a2a9cdcf060fcebf670,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-23 09:51:38,2025-12-25 15:32:02,,2,新签超3年,,,1761122509436,2,进阶版,2000,,,CHS202409190281922,海口小拇指汽车服务,2025-10-23 00:00:00,2025-10-22 00:00:00,2025-10-23 00:00:00,新签,,,,,,是,2000.0,2000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],100.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
28,68fecdec710ccbf6abfdcf9c,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-27 09:42:04,2025-12-25 15:32:02,,2,新签超3年,,,1761365304042,2,标准版,3200,,,CHS202510250311422,博越汽修,2025-10-25 00:00:00,2025-10-25 00:00:00,2025-10-25 00:00:00,新签,,,,,,是,3200.0,3200.0,王有军,陈庆伟,浙皖,王有军,陈庆伟,浙皖,1.0,1.0,,新增,,[],160.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
29,6901795327c25049c38f1e2b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-29 10:17:55,2025-12-25 15:32:02,,1,新签超3年,,,1761644690009,2,基础版,1600,,,CHS202510280311584,宜良捷驰汽车修理厂,2025-10-29 00:00:00,2025-10-29 00:00:00,2025-10-29 00:00:00,新签,,,,,,是,1600.0,1600.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],80.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
30,690179cf34ed36233a8166fd,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-29 10:19:59,2025-12-25 15:32:02,,1,跨区新签,,,1761611436641,1,旗舰版,4499,,,CHS202510280311527,易道大咖乌鲁木齐东坪店,2025-10-28 00:00:00,2025-10-28 00:00:00,2025-10-28 00:00:00,新签,,,,,,是,2249.5,2249.5,孙振华,景东强,西北,潘志强,张凯,华南沪,0.5,0.5,,拆单,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
31,69041f3e38fb7000e0ffc32e,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-31 10:30:22,2025-12-25 15:32:02,,1,新签超3年,,,1761816948126,2,至尊版,8000,,,CHS202510300312383,意嘉易春天里店,2025-10-31 00:00:00,2025-10-31 00:00:00,2025-10-31 00:00:00,新签,,,,,,是,8000.0,8000.0,范启超,肖军,西南,范启超,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
32,69095a58a9520f1eeaf2afcb,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-04 09:43:52,2025-12-25 15:32:02,,1,新签超3年,,,1762158897286,2,基础版,5000,续约旗舰版-门店管理系统2年,2500.0,CHS202511030312575,宣威市龙泉汽车有限公司,2025-11-04 00:00:00,2025-11-04 00:00:00,2025-11-04 00:00:00,新签,,,,,,是,5000.0,5000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
33,690aae75afd572c006769a86,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:55:01,2025-12-25 15:32:02,,1,新签超3年,,,1762245527558,2,进阶版,2240,续约进阶版2年,1120.0,CHS202312020253454,乐山郭建军,2025-11-05 00:00:00,2025-11-05 00:00:00,2025-11-05 00:00:00,新签,,,,,,是,2240.0,2240.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
34,690aae91ff9575eb8a758dc3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:55:29,2025-12-25 15:32:02,,1,多年补差价,,,1761960739508,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202509230309853,都江堰市爱都汽车维修有限责任公司,2025-11-01 00:00:00,2025-09-23 00:00:00,2025-11-01 00:00:00,新签,,,,,,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
35,690aaec3f23eb35e6e394e54,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:56:19,2025-12-25 15:32:02,,1,多年补差价,,,1761955976569,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202509150309531,三越汽车音响,2025-11-01 00:00:00,2025-09-12 00:00:00,2025-11-01 00:00:00,新签,,,,,,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
36,690c00df267ca78f0a86da76,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-06 09:58:55,2025-12-25 15:32:02,,1,新签超3年,,,1762263761814,2,入门版,1600,续约入门版2年,800.0,CHS202105140124767,茂名市电白区华南汽车维修有限公司,2025-11-05 00:00:00,2025-11-04 00:00:00,2025-11-05 00:00:00,新签,,,,,,是,1600.0,1600.0,严冬延,张凯,华南沪,严冬延,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
37,690d4feafeb41d02c491646b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-07 09:48:26,2025-12-25 15:32:02,,1,多年补差价,,,1762418825828,2,旗舰版,3599,续约旗舰版-门店管理系统2年,1799.5,CHS202508090305351,山海车服,2025-11-07 00:00:00,2025-08-09 00:00:00,2025-11-07 00:00:00,新签,上海山高海深汽车服务有限公司,15975930111903944708,,,,是,3599.0,3599.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
38,690d5114ebf892f67c83c3d9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-07 09:53:24,2025-12-25 15:32:02,,1,新签超3年,,,1762401310694,2,进阶版,2240,续约进阶版2年,1120.0,CHS202003250058491,博伟汽修(新南路),2025-11-06 00:00:00,2025-11-06 00:00:00,2025-11-06 00:00:00,新签,博伟汽修,10546443563986851726,,,,是,2240.0,2240.0,胡仲远,肖军,西南,胡仲远,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
39,6915529caca516e7c6c28b71,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-13 11:38:04,2025-12-25 15:32:02,,1,多年补差价,,,1762933140216,2,基础版,1998,续约基础版-门店管理系统2年,999.0,CHS202209140188566,德奥养车,2025-11-12 00:00:00,2025-11-08 00:00:00,2025-11-12 00:00:00,新签,德奥养车,11240984669917483539,,,,是,1998.0,1998.0,陈晨,关磊,华北,陈晨,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
40,691a76c508e9a99cb7e65565,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-17 09:13:41,2025-12-25 15:32:02,,1,新签超3年,,,1763100107052,2,标准版,3120,续约标准版-门店管理系统2年,1560.0,CHS202511150313111,云南锡业集团汽车技术服务有限公司,2025-11-14 00:00:00,2025-11-14 00:00:00,2025-11-14 00:00:00,新签,云南锡业集团汽车技术服务(文山服务中心),16011444960305905673,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
41,691c0a70d6f09daa8e0af427,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:00,2025-12-25 15:32:02,,1,多年补差价,,,1763366190883,2,入门版,999,续约入门版2年,499.5,CHS202509020309077,宜嘉养车,2025-11-17 00:00:00,2025-09-02 00:00:00,2025-11-17 00:00:00,新签,宜嘉养车,15984698892155383884,,,,是,999.0,999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
42,691c0a845f80e95f659f9b26,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:20,2025-12-25 15:32:02,,1,新签超3年,,,1763351228539,1,进阶版,1000,续约进阶版,1000.0,CHS202511170313156,郑州欧汇汽车维修有限公司,2025-11-17 00:00:00,2025-11-17 00:00:00,2025-11-17 00:00:00,新签,郑州欧汇汽车维修有限公司,16012185257256194131,,,,是,1000.0,1000.0,王兵帅,张凯,河南,王兵帅,张凯,河南,1.0,1.0,邢恒岭,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
43,691c0a9576f2783074f62089,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:37,2025-12-25 15:32:02,,1,新签超3年,,,1763370516248,2,标准版,3120,续约标准版-门店管理系统2年,1560.0,CHS202511170313182,文山万霆新能源科技有限责任公司,2025-11-18 00:00:00,2025-11-18 00:00:00,2025-11-18 00:00:00,新签,文山万霆新能源科技有限责任公司,16012268807246610438,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
44,691d39d9de406a40ae9c1859,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-19 11:30:33,2025-12-25 15:32:02,,1,多年补差价,,,1763447333680,2,入门版,800,续约入门版2年,400.0,CHS202510070310394,盛发汽车维修保养中心,2025-11-18 00:00:00,2025-10-07 00:00:00,2025-11-18 00:00:00,新签,滨海凯盛汽修养护中心,15997329870740811799,,,,是,800.0,800.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
45,691e88e504528289184a3d00,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-20 11:20:05,2025-12-25 15:32:02,,1,多年补差价,,,1763522175031,2,基础版,2300,续约基础版-门店管理系统2年,1150.0,CHS202511190313271,徐宝行汽车服务有限公司,2025-11-19 00:00:00,2025-11-19 00:00:00,2025-11-19 00:00:00,新签,徐州徐宝行汽车服务有限公司,16012883105031421976,,,,是,2300.0,2300.0,赵涛,肖军,江苏,赵涛,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
46,691fdb3878dc061019100add,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-21 11:23:36,2025-12-25 15:32:02,,1,多年补差价,,,1763619899692,2,入门版,700,续约入门版2年,350.0,CHS202510230311351,梵远汽车服务,2025-11-20 00:00:00,2025-10-23 00:00:00,2025-11-20 00:00:00,新签,南京市雨花台区梵远汽车配件销售中心,16003167838416171074,,,,是,700.0,700.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
47,6927e49b21b02c48ca7bb1f5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-27 13:41:47,2025-12-25 15:32:02,,1,多年补差价,,,1764149126832,2,进阶版,1799,续约进阶版2年,899.5,CHS202511080312789,鼎鹏汽车服务中心,2025-11-27 00:00:00,2025-11-08 00:00:00,2025-11-27 00:00:00,新签,贵阳市云岩区鼎鹏汽车养护中心,16008912692744061003,,,,是,1799.0,1799.0,熊斌,肖军,西南,熊斌,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
48,6928ffbe82767255aabb44be,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:49:50,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,18000,,,,,,,2025-11-27 00:00:00,新签,,,,,,是,9000.0,18000.0,梁柱,张凯,华南沪,,,,0.5,1.0,黄宗祥,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
49,6928fff076d8fe5abdd61266,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:50:40,2025-12-25 15:32:02,,1,新签超3年,,,1762590806733,2,基础版,1600,续约基础版-门店管理系统2年,800.0,CHS202511080312804,华营昌检车线店,2025-11-09 00:00:00,2025-11-08 00:00:00,2025-11-09 00:00:00,新签,阳泉华营昌汽修连锁,10907434497378123878,,,,是,1600.0,1600.0,刘剑桥,关磊,华北,刘剑桥,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
50,692900560e8ab9d6604193f2,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:52:22,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,5000,,,,,,,2025-11-07 00:00:00,新签,,,,,,是,2500.0,5000.0,赵旭伟,肖军,江苏,,,,0.5,1.0,陈博,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
51,6929055f9e94d391e9cd53dd,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 10:13:51,2025-12-25 15:32:02,,1,新签超3年,,,1762739543304,2,入门版,1200,续约入门版2年,600.0,CHS202511090312841,小欢汽车维修,2025-11-10 00:00:00,2025-11-10 00:00:00,2025-11-10 00:00:00,新签,义县稍户营子镇小欢汽车维修处(个体工商户),16009461720019927075,,,,是,1200.0,1200.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
52,692cfbd8da792279e268feef,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:16,2025-12-25 15:32:02,,1,多年补差价,,,1764382941584,2,入门版,800,续约入门版2年,400.0,CHS202510170310927,盐城市大丰区辰煜汽车服务有限公司,2025-11-29 00:00:00,2025-10-18 00:00:00,2025-11-29 00:00:00,新签,盐城市大丰区辰煜汽车服务有限公司,10546443563782178538,,,,是,800.0,800.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
53,692cfbe7ff1b578004611ddc,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:31,2025-12-25 15:32:02,,1,新签超3年,,,1764333612264,2,标准版,3600,续约标准版-门店管理系统2年,1800.0,CHS202511280313670,厦门市鑫车宝汽车服务有限公司,2025-11-29 00:00:00,2025-11-28 00:00:00,2025-11-29 00:00:00,新签,厦门市鑫车宝汽车服务有限公司,16016206505594359823,,,,是,3600.0,3600.0,郭锦城,张凯,华南沪,郭锦城,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
54,692cfc00bc219a1741c27ffc,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:56,2025-12-25 15:32:02,,1,多年补差价,,,1764502026835,2,进阶版,2001,续约进阶版2年,1000.5,CHS202511250313539,天门市小拇指汽车维修服务有限公司,2025-12-01 00:00:00,2025-11-26 00:00:00,2025-12-01 00:00:00,新签,天门市小拇指汽车维修服务有限公司,16015175963579027532,,,,是,2001.0,2001.0,陈煜,景东强,华中,陈煜,景东强,华中,1.0,1.0,刘光春,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
55,6938df09cc655df92928b5b3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-10 10:46:33,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,8000,,,,,,,2025-12-10 00:00:00,新签,,,8000,,2025-12-01 00:00:00,是,4000.0,8000.0,孙旭亮,陈庆伟,东北,,,,0.5,1.0,孙旭亮,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
56,69391bbdcdfe09bce4c98cd7,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-10 15:05:33,2025-12-25 15:32:02,,1,新签超3年,,,1764473175419,2,进阶版,2000,续约进阶版2年,1000.0,CHS202511300313735,唐山市路南新腾云汽车维修服务站,2025-11-30 00:00:00,2025-11-30 00:00:00,2025-11-30 00:00:00,新签,唐山市路南新腾云汽车维修服务站,16016880445115371607,,,,是,2000.0,2000.0,王鑫,关磊,华北,王鑫,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
57,693b7c9a25d3e9c841729ed3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-12 10:23:22,2025-12-25 15:32:02,,1,新签超3年,,,1765420934693,2,入门版,1000,续约入门版2年,500.0,CHS202512110314232,西昌安宁美车度汽修,2025-12-11 00:00:00,2025-12-11 00:00:00,2025-12-11 00:00:00,新签,西昌安宁美车度汽车喷漆中心,16020868221515104344,,,,是,1000.0,1000.0,杨君毅,肖军,西南,杨君毅,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
58,693f6a268691ed316215299b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:53:42,2025-12-25 15:32:02,,1,跨区新签,,,1765158079178,3,基础版,3000,基础版-门店管理系统3年,1000.0,CHS202512080314089,鹊大师(大同店),2025-12-08 00:00:00,2025-12-08 00:00:00,2025-12-08 00:00:00,新签,广之源汽车一站式服务中心,16000651004727029792,,,,是,1500.0,1500.0,张宏伟,关磊,华北,韩皞,关磊,东北,0.5,0.5,杨挺,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
59,693f6a41b051e5517a47eed5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:54:09,2025-12-25 15:32:02,,1,跨区新签,,,1765358368741,3,至尊版,11997,至尊版-门店管理系统3年,3999.0,CHS202512100314217,鞍山尊荣万顺汽车销售有限公司,2025-12-10 00:00:00,2025-12-10 00:00:00,2025-12-10 00:00:00,新签,鞍山尊荣万顺汽车销售有限公司,16020612536361586766,,,,是,5998.5,5998.5,宋小涛,陈庆伟,东北,刘磊,陈庆伟,华北,0.5,0.5,孙旭亮,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
60,693f6a522b2bc18aef5bec96,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:54:26,2025-12-25 15:32:02,,1,多年补差价,,,1765259877744,2,进阶版,2500,续约进阶版2年,1250.0,CHS202509170309603,平湖市万里汽车大修厂,2025-12-09 00:00:00,2025-09-17 00:00:00,2025-12-09 00:00:00,新签,平湖市万里汽车大修厂,15990048390801023028,,,,是,2500.0,2500.0,王有军,陈庆伟,浙皖,王有军,陈庆伟,浙皖,1.0,1.0,魏子淇,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
61,6948aa20b9b289e0b6b0a8d1,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-22 10:17:04,2025-12-25 15:32:02,,1,新签超3年,,,1766292766298,2,入门版,800,续约入门版2年,400.0,CHS202512210314689,博晏爱玩车维修中心,2025-12-21 00:00:00,2025-12-21 00:00:00,2025-12-21 00:00:00,新签,义县稍户营子镇博晏爱玩车汽车服务维修中心,16024514498417168447,,,,是,800.0,800.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
62,6948abbc111dbba0cb0d4fd9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-22 10:23:56,2025-12-25 15:32:02,,1,多年补差价,,,1766125609196,2,至尊版,8999,续约至尊版-门店管理系统2年,4499.5,CHS202506260302952,烟台福泰汽车服务有限公司,2025-12-19 00:00:00,2025-06-26 00:00:00,2025-12-19 00:00:00,新签,福泰汽车服务有限公司,11240984669918394572,,,,是,8999.0,8999.0,宗川涵,关磊,山东,宗川涵,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
63,6949f9571fc4fc20b158497f,"{'name': '陈庆伟', 'username': '025366033037741985', 'status': 1, 'type': 0, 'departments': [122311528, 122229573], 'integrate_id': '025366033037741985'}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-23 10:07:19,2025-12-30 17:35:22,,1,跨区新签,,该单是刘磊跨区浙江的客户,王蔚东培训的,关磊已经让小六把业绩和提成都给王蔚东了,100%算王蔚东的,1765263063272,1,基础版,1199,基础版-门店管理系统,1199.0,CHS202512090314165,舟山市于瑞汽车修理有限公司,2025-12-09 00:00:00,2025-12-09 00:00:00,2025-12-09 00:00:00,新签,舟山市于瑞汽车修理有限公司,16020236055869423684,1199,,2025-12-01 00:00:00,否,599.5,599.5,刘磊,关磊,华北,刘磊,陈庆伟,华北,0.5,0.5,魏子淇,拆单,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
64,694a09358daf16de8df90b12,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-23 11:15:01,2025-12-25 15:32:02,,1,新签超3年,,,1766389436606,1,旗舰版,2125,续约旗舰版-门店管理系统,2125.0,CHS202512220314748,PM汽车服务,2025-12-22 00:00:00,2025-12-22 00:00:00,2025-12-22 00:00:00,新签,PM汽车服务,16024929481844097081,2125,,2025-12-01 00:00:00,是,2125.0,2125.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
65,694b80373f7c91e02f438916,"{'name': '张凯', 'username': '1525590028775887', 'status': 1, 'type': 0, 'departments': [122283546, 127595486, 122514491], 'integrate_id': '1525590028775887'}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-24 13:55:03,2025-12-25 15:32:02,,1,多年补差价,,新签一个月补1599两年,共计3198三年基础版,1766474568560,2,基础版,1599,续约基础版-门店管理系统2年,799.5,CHS202512070314056,贺州市德宝汽车养护有限公司,2025-12-23 00:00:00,2025-12-07 00:00:00,2025-12-23 00:00:00,新签,贺州市德宝汽车养护有限公司,16019465729355059235,1599,黄宗祥,2025-12-01 00:00:00,是,1599.0,1599.0,黄宗祥,张凯,华南沪,黄宗祥,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
66,694cfd1d990c750feb7392f8,"{'name': '景东强', 'username': '232229053125844557', 'status': 1, 'type': 0, 'departments': [122472424, 122503482], 'integrate_id': '232229053125844557'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-25 17:00:13,2025-12-26 09:45:45,,1,多年补差价,,此客户约定进阶版3年3899元;11月8号付款一年进阶版1999元,12月25日补尾款1900元。胡冰区域,1766652296888,2,进阶版,1900,续约进阶版2年,950.0,CHS202511080312811,宜春晴天汽车服务有限公司,2025-12-25 00:00:00,2025-11-08 00:00:00,2025-12-25 00:00:00,新签,宜春晴天汽车服务有限公司,16009000270293930057,1900,胡冰,2025-12-01 00:00:00,是,1900.0,1900.0,胡冰,景东强,华中,胡冰,景东强,华中,1.0,1.0,金华斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
67,69536f0a92ee436c9af5e238,"{'name': '肖军', 'username': '311003461041349', 'status': 1, 'type': 0, 'departments': [122314630, 122323520], 'integrate_id': '311003461041349'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 14:19:54,2025-12-31 10:05:48,,1,多年补差价,,,1767075482440,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202511170313151,成都鑫城南汽车,2025-12-30 00:00:00,2025-11-17 00:00:00,2025-12-30 00:00:00,新签,成都鑫城南汽车,16012170912208031813,1101,陈致欣,2025-12-01 00:00:00,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
68,6953889d92e8e1b524429a46,"{'name': '景东强', 'username': '232229053125844557', 'status': 1, 'type': 0, 'departments': [122472424, 122503482], 'integrate_id': '232229053125844557'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 16:09:01,2025-12-30 16:15:23,,1,跨区新签,,此客户在孙婷婷区域,但由于客户单店体量较大,于是让陈煜介入洽谈,故业绩陈煜一半9000元。孙婷婷一半9000元。,1765503409986,3,至尊版,17997,至尊版-门店管理系统3年,5999.0,CHS202512120314279,武汉酷卡驰汽车科技有限公司,2025-12-12 00:00:00,2025-12-12 00:00:00,2025-12-12 00:00:00,新签,武汉酷卡驰汽车科技有限公司,16021219284734738438,18000,陈煜,2025-12-01 00:00:00,是,8998.5,8998.5,孙婷婷,景东强,华中,陈煜,景东强,华中,0.5,0.5,刘光春,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
69,6953bd5a65e053d760df4b80,"{'name': '陈庆伟', 'username': '025366033037741985', 'status': 1, 'type': 0, 'departments': [122311528, 122229573], 'integrate_id': '025366033037741985'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 19:54:02,2025-12-31 09:04:54,,1,多年补差价,,新签3个月内补差价,多年购,1766986879209001,2,进阶版,1600,续约进阶版2年,800.0,CHS202510120310664,鹤岗市南山区鑫宏海中外汽车维修站,2025-12-29 00:00:00,2025-10-12 00:00:00,2025-12-29 00:00:00,新签,鹤岗市南山区鑫宏海中外汽车维修站,15999259585890246734,1600,韩皞,2025-12-01 00:00:00,是,1600.0,1600.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,刘立,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6
@@ -0,0 +1,326 @@
# -*- coding: utf-8 -*-
import pandas as pd
import datetime
from config import Config
from api import API
import pymysql # 使用 pymysql 替代 mysql.connector
from back_ground_module import CommonModule
import os
import mysql.connector
import pandas as pd
import json
import numpy as np
import mysql.connector
from mysql.connector import Error
from log_config import configure_task_logger, configure_error_task_logger
logger = configure_task_logger()
error_task_logger = configure_error_task_logger()
api_instance = API()
common_module = CommonModule()
output_dir = "output" # 设置输出目录
os.makedirs(output_dir, exist_ok=True)
class NonStandardPerformanceToBI:
""" 非标业绩提报转BI"""
def __init__(self):
self.dealer_service_data = None
self.field_mapping = {
"报备类型": "_widget_1753770875899",
"协作内容": "_widget_1753770875915",
"情况说明": "_widget_1753770875944",
"订单编号": "_widget_1753770875887",
"实付金额": "_widget_1753770875889",
"门店编码": "_widget_1753770875890",
"门店名称": "_widget_1753770875888",
"版本": "_widget_1753770875891",
"年限": "_widget_1753948745953",
"支付日期": "_widget_1753770875893",
"开户/处理日期": "_widget_1753770875894",
"小六业绩金额": "_widget_1753770875898",
"区域业绩金额": "_widget_1753770875937",
"报备业绩归属区域经理": "_widget_1753770875903",
"报备业绩归属大区": "_widget_1753866196486",
"原业绩归属人": "_widget_1753856032683",
"原业绩归属区域经理": "_widget_1753866196485",
"小六业绩比例": "_widget_1753770875917",
"区域业绩比例": "_widget_1753770875921",
"运营专家": "_widget_1753770875902",
"提成类型": "_widget_1753778922504",
"SaaS新签提成比例": "_widget_1753770875949",
"服务包提成比例": "_widget_1753778922567",
"提成金额": "_widget_1753770875948",
"新签提成比例-首年": "_widget_1753778922503",
"新签提成比例-非首年": "_widget_1753778922548",
"新签阶段及提成比例": "_widget_1753778656359",
"业绩动作":"_widget_1756708722933",
"提成动作":"_widget_1756708722932",
"新签阶段及提成比例.选择提成阶段": "_widget_1753778656359._widget_1753778656361",
"新签阶段及提成比例.新签阶段": "_widget_1753778656359._widget_1753948745962",
"新签阶段及提成比例.提成比例": "_widget_1753778656359._widget_1753778656362",
"业绩类型":"_widget_1753770875966",
"报备业绩归属小六":"_widget_1753770875901",
"原业绩归属大区":"_widget_1755159216098",
"业绩分类":"_widget_1758706882564",
"流程是否结束":"_widget_1761633418013",
"业绩类型-聚合":"_widget_1758706882564",
"业绩分组":"_widget_1762417447169",
"商品名称":"_widget_1762219744898",
"履约金额":"_widget_1762220516367",
"业绩归属日期":"_widget_1762417447127",
"公司名称":"_widget_1762420723743",
"公司ID":"_widget_1762420723744",
"报备业绩金额-区域提交":"_widget_1766375035236",
"业绩归属小六-区域提交":"_widget_1766461143813",
"业绩归属月":"_widget_1766375035265",
"是否同步衡石":"_widget_1766484337844",
"提交人": "creator",
"提交时间": "createTime",
"更新时间": "updateTime"
}
# 定义需要特殊处理的列表字段及其内部字段映射
self.list_fields_config = {
"新签阶段及提成比例": {
"_widget_1753778656361": "选择提成阶段",
"_widget_1753948745962": "新签阶段",
"_widget_1753778656362": "提成比例"
},
# 可以在这里添加其他列表字段的配置
# "另一个列表字段": {
# "原始字段名1": "映射后字段名1",
# "原始字段名2": "映射后字段名2"
# }
}
def load_all_data(self):
# 获取非标业绩提报数据
payload = {"api_key": "66b9678280b37f8a276b1d01",
"entry_id": "68886b7c0382a7249ae0b5d6",
}
dealer_service = api_instance.entry_data_list(payload)
self.dealer_service_data = dealer_service.get("data") # api请求格式,将数据封装在data字典里
def process_list_field(self, field_value, field_config):
"""通用方法:处理列表类型的字段"""
if not isinstance(field_value, (list, np.ndarray)):
return field_value
processed_list = []
for item in field_value:
if not isinstance(item, dict):
processed_list.append(item)
continue
processed_item = {}
for original_key, mapped_key in field_config.items():
if original_key in item:
# 处理包含id的字典字段
if isinstance(item[original_key], dict) and "id" in item[original_key]:
processed_item[mapped_key] = item[original_key]["id"]
else:
processed_item[mapped_key] = item[original_key]
else:
processed_item[mapped_key] = None
processed_list.append(processed_item)
return processed_list
def data_process(self):
df = pd.DataFrame(self.dealer_service_data)
# 反转映射字典
reverse_mapping = {v: k for k, v in self.field_mapping.items()}
# 1.列明替换
df.columns = [reverse_mapping.get(col, col) for col in df.columns]
# 只保留流程是否结束为是的内容
df = df[df["流程是否结束"] == ""]
# 2.成员字段取值
user_columns = ["报备业绩归属小六", "报备业绩归属区域经理", "原业绩归属人", "原业绩归属区域经理", "运营专家","业绩归属小六-区域提交"]
for col in user_columns:
df[col] = df[col].map(lambda x: x.get("name", "") if isinstance(x, dict) else "")
# 3.日期字段转为北京时间
time_columns = ["支付日期", "开户/处理日期", "提交时间", "更新时间", "业绩归属月", "业绩归属日期"]
for col in time_columns:
# 1. 解析为 datetime,并明确指定为 UTC(即使原始字符串无时区)
dt_utc = pd.to_datetime(df[col], errors='coerce', utc=True)
# 2. 转换为北京时间
dt_beijing = dt_utc.dt.tz_convert('Asia/Shanghai')
# 3. 去掉时区信息(变成 naive datetime),然后格式化为字符串
df[col] = dt_beijing.dt.tz_localize(None).dt.strftime('%Y-%m-%d %H:%M:%S')
# 4.业绩动作等于拆单做复制
# 4.1. 定义条件
mask = df['业绩动作'] == '拆单'
# 4.2. 复制满足条件的行
new_rows = df[mask].copy() # ⚠️ 一定要用 .copy() 避免 SettingWithCopyWarning
# 3. 修改新行中的某些列
new_rows['小六业绩金额'] = -new_rows['小六业绩金额']
new_rows['区域业绩金额'] = -new_rows['区域业绩金额']
new_rows['报备业绩归属小六'] = new_rows['原业绩归属人']
new_rows['报备业绩归属区域经理'] = new_rows['原业绩归属区域经理']
new_rows['报备业绩归属大区'] = new_rows['原业绩归属大区']
# 4. 合并回原 DataFrame
df = pd.concat([df, new_rows], ignore_index=True)
# 5.处理所有配置的列表字段
if "新签阶段及提成比例" in df.columns:
# 先处理订单登记表字段
df["新签阶段及提成比例"] = df["新签阶段及提成比例"].apply(
lambda x: self.process_list_field(x, self.list_fields_config["新签阶段及提成比例"])
if x is not None and (isinstance(x, (list, dict, np.ndarray)) or not pd.isna(x))
else None
)
# 拆分行
df_exploded = df.explode("新签阶段及提成比例")
# 将订单登记表中的字段提取到主表中
order_fields = self.list_fields_config["新签阶段及提成比例"].values()
for field in order_fields:
df_exploded[field] = df_exploded["新签阶段及提成比例"].apply(
lambda x: x.get(field) if isinstance(x, dict) else None
)
# 删除原始的订单登记表列
df_exploded = df_exploded.drop(columns=["新签阶段及提成比例"])
# 重置索引
df = df_exploded.reset_index(drop=True)
return df
def write_to_bi(self, df):
# 数据库连接信息
HS_DB_Config = Config.HS_DB_Config
table_name = "non_standard_performance_to_BI" # 替换为你的实际表名
# 建立数据库连接
connection = mysql.connector.connect(
host=HS_DB_Config["host"],
user=HS_DB_Config["user"],
password=HS_DB_Config["password"],
database=HS_DB_Config["database"]
)
cursor = connection.cursor()
try:
# 查询表列名
cursor.execute(f"SHOW COLUMNS FROM {table_name}")
columns_info = cursor.fetchall()
db_columns = [col[0] for col in columns_info] # 提取列名
df = df.replace([None, np.nan, pd.NA, 'nan', 'NaN', 'NAN', ''], None)
# 保留 DataFrame 中与数据库列名匹配的列
filtered_df = df[df.columns.intersection(db_columns)]
# 如果没有匹配的列,直接返回
if filtered_df.empty:
print("DataFrame 中没有与数据库表结构匹配的列。")
return
# 筛选列之后,插入前处理 dict 类型
filtered_df = filtered_df.copy()
for col in filtered_df.columns:
if filtered_df[col].apply(lambda x: isinstance(x, (dict, list)) if x is not None else False).any():
filtered_df.loc[:, col] = filtered_df[col].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else x
)
# 构建插入语句
placeholders = ', '.join(['%s'] * len(filtered_df.columns))
# 使用反引号避免特殊列明
columns = ', '.join([f"`{col}`" for col in filtered_df.columns])
insert_sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
# 将 DataFrame 写入数据库
for _, row in filtered_df.iterrows():
cursor.execute(insert_sql, tuple(row))
connection.commit()
logger.info(f"成功写入 {len(filtered_df)} 条记录到 {table_name} 表中。")
except Exception as e:
error_task_logger.error(f"写入数据库时发生错误: {e}")
connection.rollback()
finally:
cursor.close()
connection.close()
def clear_table_data(self):
"""
清空指定 MySQL 表的数据
参数已写死在函数内部直接调用即可
"""
# 数据库连接信息
HS_DB_Config = {
'host': "f6-public.rwlb.rds.aliyuncs.com",
'user': "rw_operation_data_relay",
'password': "m+q5Z4%IVuF9bf",
'database': "f6operation_data_relay"
}
table_name = "non_standard_performance_to_BI" # 要清空的表名
connection = None
try:
# 建立数据库连接
connection = mysql.connector.connect(
host=HS_DB_Config["host"],
user=HS_DB_Config["user"],
password=HS_DB_Config["password"],
database=HS_DB_Config["database"]
)
if connection.is_connected():
cursor = connection.cursor()
# 使用TRUNCATE清空表数据
cursor.execute(f"TRUNCATE TABLE {table_name}")
connection.commit()
logger.info(f"成功清空表 {table_name} 中的所有数据")
except Error as e:
error_task_logger.error(f"清空表时发生错误: {e}")
if connection and connection.is_connected():
connection.rollback()
finally:
if connection and connection.is_connected():
cursor.close()
connection.close()
logger.info("数据库连接已关闭")
def main(self):
task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
logger.info("任务开始")
# step1: 获取数据
self.load_all_data()
logger.info("加载数据完成")
# step2:数据处理
df = self.data_process()
# df.to_csv(os.path.join(output_dir, "new_dealer_service_order_to_bi.csv"))
logger.info("数据处理完成")
# step3:数据库删除
self.clear_table_data()
logger.info("目标数据库已清空")
# step4:数据写入BI
self.write_to_bi(df)
logger.info("数据已写入数据库中")
common_module.send_task_status(task_start_time, "非标业绩提报转BI")
except Exception as e:
error_task_logger.error(f"非标业绩提报转BI发生错误{e}")
common_module.send_task_error(task_start_time,"非标业绩提报转BI", str(e))
if __name__ == '__main__':
start = NonStandardPerformanceToBI()
start.main()
+26 -27
View File
@@ -237,8 +237,8 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-12-25T06:47:43.233398500Z",
"start_time": "2025-12-25T06:47:42.837753600Z"
"end_time": "2026-01-06T08:14:03.891128500Z",
"start_time": "2026-01-06T08:14:03.535415Z"
}
},
"cell_type": "code",
@@ -254,10 +254,10 @@
" } # 衡时数据库链接配置-mysql\n",
"# table_name = \"new_dealer_service_order_to_bi\" # 替换为你的实际表名\n",
"\n",
"table_name = \"partner_settlement_to_BI\"\n",
"column_name = \"是否同步\"\n",
"new_column_type = \"VARCHAR(255)\" # 目标数据类型\n",
"# new_column_type = \"DATETIME\" # 目标数据类型\n",
"table_name = \"f6_denominator_adjustment\"\n",
"column_name = \"归属月份\"\n",
"# new_column_type = \"VARCHAR(255)\" # 目标数据类型\n",
"new_column_type = \"DATETIME\" # 目标数据类型\n",
"\n",
"try:\n",
" # 连接数据库\n",
@@ -316,12 +316,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
"✅ 成功添加字段: `是否同步`\n",
"✅ 成功添加字段: `归属月份`\n",
"数据库连接已关闭\n"
]
}
],
"execution_count": 24
"execution_count": 1
},
{
"metadata": {},
@@ -421,8 +421,8 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-06-11T01:39:55.579785Z",
"start_time": "2025-06-11T01:39:55.295469Z"
"end_time": "2026-04-03T09:13:22.881255Z",
"start_time": "2026-04-03T09:13:20.171270200Z"
}
},
"cell_type": "code",
@@ -432,31 +432,31 @@
"from mysql.connector import Error\n",
"\n",
"# 数据库连接信息\n",
"host = \"rm-uf6r230vbtxf5gdz63o.mysql.rds.aliyuncs.com\"\n",
"user = \"rw_operation_data_relay\"\n",
"password = \"m+q5Z4%IVuF9bf\"\n",
"database = \"f6operation_data_relay\"\n",
"table_name = \"yida_process_time_statistics\" # 要操作的表名\n",
"BI_CONN_host = \"f6-public.rwlb.rds.aliyuncs.com\"\n",
"BI_CONN_INFO_database = \"f6operation_data_relay\"\n",
"BI_CONN_INFO_user = \"rw_operation_data_relay\"\n",
"BI_CONN_INFO_password = \"m+q5Z4%IVuF9bf\"\n",
"table_name = \"gp_monthly_renewal_rate_new\" # 要操作的表名\n",
"# table_name = \"thailand_store_data_email\" # 要操作的表名\n",
"min_id_to_delete = 127821 # 要删除的最小ID值\n",
"\n",
"# 连接数据库\n",
"try:\n",
" connection = mysql.connector.connect(\n",
" host=host,\n",
" user=user,\n",
" password=password,\n",
" database=database\n",
" host=BI_CONN_host,\n",
" user=BI_CONN_INFO_user,\n",
" password=BI_CONN_INFO_password,\n",
" database=BI_CONN_INFO_database\n",
" )\n",
"\n",
" if connection.is_connected():\n",
" cursor = connection.cursor()\n",
"\n",
" # 使用DELETE删除ID大于等于127821的数据\n",
" cursor.execute(f\"DELETE FROM {table_name} WHERE id >= {min_id_to_delete}\")\n",
" # cursor.execute(f\"DELETE FROM {table_name} WHERE id >= {min_id_to_delete}\")\n",
" cursor.execute(f\"DELETE FROM GP_monthly_renewal_rate_new WHERE 月分区(仅用于存储每月最后一天截至数据) = '202603';\")\n",
"\n",
" connection.commit()\n",
" print(f\"成功删除表 {table_name} 中ID大于等于{min_id_to_delete}的所有数据\")\n",
"\n",
"except Error as e:\n",
" print(f\"删除数据时发生错误: {e}\")\n",
@@ -474,12 +474,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
"成功删除表 yida_process_time_statistics 中ID大于等于127821的所有数据\n",
"数据库连接已关闭\n"
]
}
],
"execution_count": 3
"execution_count": 2
},
{
"metadata": {},
@@ -552,8 +551,8 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-08-21T02:06:22.956060Z",
"start_time": "2025-08-21T02:06:22.797879Z"
"end_time": "2026-01-13T03:20:16.053485200Z",
"start_time": "2026-01-13T03:20:15.845522200Z"
}
},
"cell_type": "code",
@@ -578,7 +577,7 @@
"} # 衡时数据库链接配置-mysql\n",
"\n",
"# 表名\n",
"table_name = \"test\" # 请替换为实际的表名\n",
"table_name = \"jdy_ngv_data_source\" # 请替换为实际的表名\n",
"\n",
"# 连接数据库\n",
"connection = mysql.connector.connect(\n",
@@ -606,7 +605,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"成功删除表 test\n"
"成功删除表 jdy_ngv_data_source\n"
]
}
],
@@ -25,6 +25,7 @@ def save_data_yichang_to_csv():
# data_yichang_S = common_module.get_yichang_details(days_back=1)
data_yichang_S = common_module.get_ngv_details(days_back=1) # ngv
if data_yichang_S is None or data_yichang_S.empty:
print("未获取到数据或数据为空")
return
@@ -34,7 +35,7 @@ def save_data_yichang_to_csv():
# 生成文件名(包含日期)
current_date = datetime.datetime.now().strftime("%Y%m%d")
filename = f"data_yichang_{current_date}.csv"
filename = f"data_NGV_{current_date}.csv"
filepath = os.path.join(output_dir, filename)
# 保存到CSV
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
+59 -764
View File
@@ -12,8 +12,8 @@
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2025-07-17T08:03:51.919981Z",
"start_time": "2025-07-17T08:03:39.331174Z"
"end_time": "2026-02-25T05:45:52.998049100Z",
"start_time": "2026-02-25T05:45:49.628616100Z"
}
},
"source": [
@@ -21,7 +21,7 @@
"from api import API\n",
"\n",
"api_instance = API()\n",
"df = pd.read_excel(r\"C:\\Users\\Administrator.DESKTOP-7IC2USJ\\Downloads\\客户资料_20250801111301.xlsx\",sheet_name=\"id为空\")\n",
"df = pd.read_excel(r\"C:\\Users\\hp_z66\\Downloads\\客户资料_20260225113103.xlsx\",sheet_name=\"Sheet7\")\n",
"for index, row in df.iterrows():\n",
" data_id = row[\"data_id\"]\n",
" payload = {\n",
@@ -33,37 +33,25 @@
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n"
"ename": "KeyboardInterrupt",
"evalue": "",
"output_type": "error",
"traceback": [
"\u001B[31m---------------------------------------------------------------------------\u001B[39m",
"\u001B[31mKeyboardInterrupt\u001B[39m Traceback (most recent call last)",
"\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[1]\u001B[39m\u001B[32m, line 5\u001B[39m\n\u001B[32m 2\u001B[39m \u001B[38;5;28;01mfrom\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34;01mapi\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;28;01mimport\u001B[39;00m API\n\u001B[32m 4\u001B[39m api_instance = API()\n\u001B[32m----> \u001B[39m\u001B[32m5\u001B[39m df = \u001B[43mpd\u001B[49m\u001B[43m.\u001B[49m\u001B[43mread_excel\u001B[49m\u001B[43m(\u001B[49m\u001B[33;43mr\u001B[39;49m\u001B[33;43m\"\u001B[39;49m\u001B[33;43mC:\u001B[39;49m\u001B[33;43m\\\u001B[39;49m\u001B[33;43mUsers\u001B[39;49m\u001B[33;43m\\\u001B[39;49m\u001B[33;43mhp_z66\u001B[39;49m\u001B[33;43m\\\u001B[39;49m\u001B[33;43mDownloads\u001B[39;49m\u001B[33;43m\\\u001B[39;49m\u001B[33;43m客户资料_20260225113103.xlsx\u001B[39;49m\u001B[33;43m\"\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43msheet_name\u001B[49m\u001B[43m=\u001B[49m\u001B[33;43m\"\u001B[39;49m\u001B[33;43mSheet7\u001B[39;49m\u001B[33;43m\"\u001B[39;49m\u001B[43m)\u001B[49m\n\u001B[32m 6\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m index, row \u001B[38;5;129;01min\u001B[39;00m df.iterrows():\n\u001B[32m 7\u001B[39m data_id = row[\u001B[33m\"\u001B[39m\u001B[33mdata_id\u001B[39m\u001B[33m\"\u001B[39m]\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\pandas\\io\\excel\\_base.py:495\u001B[39m, in \u001B[36mread_excel\u001B[39m\u001B[34m(io, sheet_name, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skiprows, nrows, na_values, keep_default_na, na_filter, verbose, parse_dates, date_parser, date_format, thousands, decimal, comment, skipfooter, storage_options, dtype_backend, engine_kwargs)\u001B[39m\n\u001B[32m 493\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m \u001B[38;5;28misinstance\u001B[39m(io, ExcelFile):\n\u001B[32m 494\u001B[39m should_close = \u001B[38;5;28;01mTrue\u001B[39;00m\n\u001B[32m--> \u001B[39m\u001B[32m495\u001B[39m io = \u001B[43mExcelFile\u001B[49m\u001B[43m(\u001B[49m\n\u001B[32m 496\u001B[39m \u001B[43m \u001B[49m\u001B[43mio\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 497\u001B[39m \u001B[43m \u001B[49m\u001B[43mstorage_options\u001B[49m\u001B[43m=\u001B[49m\u001B[43mstorage_options\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 498\u001B[39m \u001B[43m \u001B[49m\u001B[43mengine\u001B[49m\u001B[43m=\u001B[49m\u001B[43mengine\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 499\u001B[39m \u001B[43m \u001B[49m\u001B[43mengine_kwargs\u001B[49m\u001B[43m=\u001B[49m\u001B[43mengine_kwargs\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 500\u001B[39m \u001B[43m \u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 501\u001B[39m \u001B[38;5;28;01melif\u001B[39;00m engine \u001B[38;5;129;01mand\u001B[39;00m engine != io.engine:\n\u001B[32m 502\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\n\u001B[32m 503\u001B[39m \u001B[33m\"\u001B[39m\u001B[33mEngine should not be specified when passing \u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 504\u001B[39m \u001B[33m\"\u001B[39m\u001B[33man ExcelFile - ExcelFile already has the engine set\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 505\u001B[39m )\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\pandas\\io\\excel\\_base.py:1567\u001B[39m, in \u001B[36mExcelFile.__init__\u001B[39m\u001B[34m(self, path_or_buffer, engine, storage_options, engine_kwargs)\u001B[39m\n\u001B[32m 1564\u001B[39m \u001B[38;5;28mself\u001B[39m.engine = engine\n\u001B[32m 1565\u001B[39m \u001B[38;5;28mself\u001B[39m.storage_options = storage_options\n\u001B[32m-> \u001B[39m\u001B[32m1567\u001B[39m \u001B[38;5;28mself\u001B[39m._reader = \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43m_engines\u001B[49m\u001B[43m[\u001B[49m\u001B[43mengine\u001B[49m\u001B[43m]\u001B[49m\u001B[43m(\u001B[49m\n\u001B[32m 1568\u001B[39m \u001B[43m \u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43m_io\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 1569\u001B[39m \u001B[43m \u001B[49m\u001B[43mstorage_options\u001B[49m\u001B[43m=\u001B[49m\u001B[43mstorage_options\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 1570\u001B[39m \u001B[43m \u001B[49m\u001B[43mengine_kwargs\u001B[49m\u001B[43m=\u001B[49m\u001B[43mengine_kwargs\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 1571\u001B[39m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\pandas\\io\\excel\\_openpyxl.py:553\u001B[39m, in \u001B[36mOpenpyxlReader.__init__\u001B[39m\u001B[34m(self, filepath_or_buffer, storage_options, engine_kwargs)\u001B[39m\n\u001B[32m 541\u001B[39m \u001B[38;5;250m\u001B[39m\u001B[33;03m\"\"\"\u001B[39;00m\n\u001B[32m 542\u001B[39m \u001B[33;03mReader using openpyxl engine.\u001B[39;00m\n\u001B[32m 543\u001B[39m \n\u001B[32m (...)\u001B[39m\u001B[32m 550\u001B[39m \u001B[33;03m Arbitrary keyword arguments passed to excel engine.\u001B[39;00m\n\u001B[32m 551\u001B[39m \u001B[33;03m\"\"\"\u001B[39;00m\n\u001B[32m 552\u001B[39m import_optional_dependency(\u001B[33m\"\u001B[39m\u001B[33mopenpyxl\u001B[39m\u001B[33m\"\u001B[39m)\n\u001B[32m--> \u001B[39m\u001B[32m553\u001B[39m \u001B[38;5;28;43msuper\u001B[39;49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\u001B[43m.\u001B[49m\u001B[34;43m__init__\u001B[39;49m\u001B[43m(\u001B[49m\n\u001B[32m 554\u001B[39m \u001B[43m \u001B[49m\u001B[43mfilepath_or_buffer\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 555\u001B[39m \u001B[43m \u001B[49m\u001B[43mstorage_options\u001B[49m\u001B[43m=\u001B[49m\u001B[43mstorage_options\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 556\u001B[39m \u001B[43m \u001B[49m\u001B[43mengine_kwargs\u001B[49m\u001B[43m=\u001B[49m\u001B[43mengine_kwargs\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 557\u001B[39m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\pandas\\io\\excel\\_base.py:573\u001B[39m, in \u001B[36mBaseExcelReader.__init__\u001B[39m\u001B[34m(self, filepath_or_buffer, storage_options, engine_kwargs)\u001B[39m\n\u001B[32m 571\u001B[39m \u001B[38;5;28mself\u001B[39m.handles.handle.seek(\u001B[32m0\u001B[39m)\n\u001B[32m 572\u001B[39m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[32m--> \u001B[39m\u001B[32m573\u001B[39m \u001B[38;5;28mself\u001B[39m.book = \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43mload_workbook\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43mhandles\u001B[49m\u001B[43m.\u001B[49m\u001B[43mhandle\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mengine_kwargs\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 574\u001B[39m \u001B[38;5;28;01mexcept\u001B[39;00m \u001B[38;5;167;01mException\u001B[39;00m:\n\u001B[32m 575\u001B[39m \u001B[38;5;28mself\u001B[39m.close()\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\pandas\\io\\excel\\_openpyxl.py:572\u001B[39m, in \u001B[36mOpenpyxlReader.load_workbook\u001B[39m\u001B[34m(self, filepath_or_buffer, engine_kwargs)\u001B[39m\n\u001B[32m 568\u001B[39m \u001B[38;5;28;01mfrom\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34;01mopenpyxl\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;28;01mimport\u001B[39;00m load_workbook\n\u001B[32m 570\u001B[39m default_kwargs = {\u001B[33m\"\u001B[39m\u001B[33mread_only\u001B[39m\u001B[33m\"\u001B[39m: \u001B[38;5;28;01mTrue\u001B[39;00m, \u001B[33m\"\u001B[39m\u001B[33mdata_only\u001B[39m\u001B[33m\"\u001B[39m: \u001B[38;5;28;01mTrue\u001B[39;00m, \u001B[33m\"\u001B[39m\u001B[33mkeep_links\u001B[39m\u001B[33m\"\u001B[39m: \u001B[38;5;28;01mFalse\u001B[39;00m}\n\u001B[32m--> \u001B[39m\u001B[32m572\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mload_workbook\u001B[49m\u001B[43m(\u001B[49m\n\u001B[32m 573\u001B[39m \u001B[43m \u001B[49m\u001B[43mfilepath_or_buffer\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 574\u001B[39m \u001B[43m \u001B[49m\u001B[43m*\u001B[49m\u001B[43m*\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdefault_kwargs\u001B[49m\u001B[43m \u001B[49m\u001B[43m|\u001B[49m\u001B[43m \u001B[49m\u001B[43mengine_kwargs\u001B[49m\u001B[43m)\u001B[49m\u001B[43m,\u001B[49m\n\u001B[32m 575\u001B[39m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\openpyxl\\reader\\excel.py:348\u001B[39m, in \u001B[36mload_workbook\u001B[39m\u001B[34m(filename, read_only, keep_vba, data_only, keep_links, rich_text)\u001B[39m\n\u001B[32m 318\u001B[39m \u001B[38;5;250m\u001B[39m\u001B[33;03m\"\"\"Open the given filename and return the workbook\u001B[39;00m\n\u001B[32m 319\u001B[39m \n\u001B[32m 320\u001B[39m \u001B[33;03m:param filename: the path to open or a file-like object\u001B[39;00m\n\u001B[32m (...)\u001B[39m\u001B[32m 344\u001B[39m \n\u001B[32m 345\u001B[39m \u001B[33;03m\"\"\"\u001B[39;00m\n\u001B[32m 346\u001B[39m reader = ExcelReader(filename, read_only, keep_vba,\n\u001B[32m 347\u001B[39m data_only, keep_links, rich_text)\n\u001B[32m--> \u001B[39m\u001B[32m348\u001B[39m \u001B[43mreader\u001B[49m\u001B[43m.\u001B[49m\u001B[43mread\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 349\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m reader.wb\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\openpyxl\\reader\\excel.py:291\u001B[39m, in \u001B[36mExcelReader.read\u001B[39m\u001B[34m(self)\u001B[39m\n\u001B[32m 289\u001B[39m \u001B[38;5;28mself\u001B[39m.read_manifest()\n\u001B[32m 290\u001B[39m action = \u001B[33m\"\u001B[39m\u001B[33mread strings\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m--> \u001B[39m\u001B[32m291\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43mread_strings\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 292\u001B[39m action = \u001B[33m\"\u001B[39m\u001B[33mread workbook\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 293\u001B[39m \u001B[38;5;28mself\u001B[39m.read_workbook()\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\openpyxl\\reader\\excel.py:147\u001B[39m, in \u001B[36mExcelReader.read_strings\u001B[39m\u001B[34m(self)\u001B[39m\n\u001B[32m 145\u001B[39m strings_path = ct.PartName[\u001B[32m1\u001B[39m:]\n\u001B[32m 146\u001B[39m \u001B[38;5;28;01mwith\u001B[39;00m \u001B[38;5;28mself\u001B[39m.archive.open(strings_path,) \u001B[38;5;28;01mas\u001B[39;00m src:\n\u001B[32m--> \u001B[39m\u001B[32m147\u001B[39m \u001B[38;5;28mself\u001B[39m.shared_strings = \u001B[43mreader\u001B[49m\u001B[43m(\u001B[49m\u001B[43msrc\u001B[49m\u001B[43m)\u001B[49m\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\openpyxl\\reader\\strings.py:18\u001B[39m, in \u001B[36mread_string_table\u001B[39m\u001B[34m(xml_source)\u001B[39m\n\u001B[32m 16\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m _, node \u001B[38;5;129;01min\u001B[39;00m iterparse(xml_source):\n\u001B[32m 17\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m node.tag == STRING_TAG:\n\u001B[32m---> \u001B[39m\u001B[32m18\u001B[39m text = \u001B[43mText\u001B[49m\u001B[43m.\u001B[49m\u001B[43mfrom_tree\u001B[49m\u001B[43m(\u001B[49m\u001B[43mnode\u001B[49m\u001B[43m)\u001B[49m.content\n\u001B[32m 19\u001B[39m text = text.replace(\u001B[33m'\u001B[39m\u001B[33mx005F_\u001B[39m\u001B[33m'\u001B[39m, \u001B[33m'\u001B[39m\u001B[33m'\u001B[39m)\n\u001B[32m 20\u001B[39m node.clear()\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\openpyxl\\descriptors\\serialisable.py:74\u001B[39m, in \u001B[36mSerialisable.from_tree\u001B[39m\u001B[34m(cls, node)\u001B[39m\n\u001B[32m 71\u001B[39m attrib[\u001B[33m\"\u001B[39m\u001B[33mattr_text\u001B[39m\u001B[33m\"\u001B[39m] = node.text\n\u001B[32m 73\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m el \u001B[38;5;129;01min\u001B[39;00m node:\n\u001B[32m---> \u001B[39m\u001B[32m74\u001B[39m tag = \u001B[43mlocalname\u001B[49m\u001B[43m(\u001B[49m\u001B[43mel\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 75\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m tag \u001B[38;5;129;01min\u001B[39;00m KEYWORDS:\n\u001B[32m 76\u001B[39m tag = \u001B[33m\"\u001B[39m\u001B[33m_\u001B[39m\u001B[33m\"\u001B[39m + tag\n",
"\u001B[36mFile \u001B[39m\u001B[32mD:\\Program Files\\anaconda3\\envs\\SaaS\\Lib\\site-packages\\openpyxl\\xml\\functions.py:81\u001B[39m, in \u001B[36mlocalname\u001B[39m\u001B[34m(node)\u001B[39m\n\u001B[32m 79\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[33m\"\u001B[39m\u001B[33mcomment\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 80\u001B[39m m = NS_REGEX.match(node.tag)\n\u001B[32m---> \u001B[39m\u001B[32m81\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mm\u001B[49m\u001B[43m.\u001B[49m\u001B[43mgroup\u001B[49m\u001B[43m(\u001B[49m\u001B[33;43m'\u001B[39;49m\u001B[33;43mlocalname\u001B[39;49m\u001B[33;43m'\u001B[39;49m\u001B[43m)\u001B[49m\n",
"\u001B[31mKeyboardInterrupt\u001B[39m: "
]
}
],
@@ -1528,8 +1516,8 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-08-01T08:12:57.996465Z",
"start_time": "2025-08-01T08:10:21.860850Z"
"end_time": "2026-02-26T02:48:25.972319100Z",
"start_time": "2026-02-26T02:34:28.428344700Z"
}
},
"cell_type": "code",
@@ -1538,7 +1526,7 @@
"from api import API\n",
"\n",
"api_instance = API()\n",
"df = pd.read_excel(r\"C:\\Users\\Administrator.DESKTOP-7IC2USJ\\Downloads\\客户资料_20250801111301.xlsx\",sheet_name=\"id为空\")\n",
"df = pd.read_excel(r\"C:\\Users\\hp_z66\\Downloads\\客户资料_20260226092528.xlsx\",sheet_name=\"Sheet3\")\n",
"for index, row in df.iterrows():\n",
" data_id = row[\"data_id\"]\n",
" payload = {\n",
@@ -1549,733 +1537,7 @@
" api_instance.entry_data_delete(payload)"
],
"id": "8a06c19850c522b3",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n",
"返回结果: {'status': 'success'}\n"
]
}
],
"outputs": [],
"execution_count": 1
},
{
@@ -2317,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": {
+33 -29
View File
@@ -97,7 +97,6 @@ class YDAPI:
break
try:
res = requests.get(api, headers=headers, params=formData)
return res.json()
except (requests.exceptions.RequestException, Exception) as e:
print(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...")
@@ -155,7 +154,7 @@ class YDAPI:
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", instanceStatus="RUNNING",
max_retries=10, delay=2, createFromTimeGMT=None, createToTimeGMT=None,
modifiedFromTimeGMT=None,
modifiedToTimeGMT=None, searchFieldJson={}):
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解析结果
@@ -196,8 +196,11 @@ class YDAPI:
"modifiedToTimeGMT": modifiedToTimeGMT,
"searchFieldJson": json.dumps(
searchFieldJson
)
),
"useAlias": useAlias,
}
if instanceIdList:
formData["instanceIdList"] = instanceIdList
# print(formData)
while True:
@@ -460,39 +463,40 @@ class YDAPI:
def get_form_structures(self, token, formUuid, appType="APP_UYZ0KG6L0CCNV80GZ66O",
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", max_retries=20):
"""
函数功能获取表单结构 # 宜搭废弃
Args:
token (str): 登录验证token用于API调用的身份验证
formUuid (str): 表单的UUID标识要获取结构的表单
appType
Returns:
响应值: 如果请求成功则返回服务器的JSON响应如果请求失败或无响应则返回一个表示获取成功信息的字符串
获取表单结构宜搭
注意该接口必须使用 GET 方法参数通过 query string 传递
"""
api = f'https://api.dingtalk.com/v1.0/yida/forms/formFields'
headers = {
"Content-Type": "application/json",
"x-acs-dingtalk-access-token": token
}
retries = 0
data_get = {}
payload = {
"formUuid": formUuid,
api_base = 'https://api.dingtalk.com/v1.0/yida/forms/formFields'
# 构造查询参数
params = {
"appType": appType,
"systemToken": systemToken,
"formUuid": formUuid,
"userId": "2268275546837446",
}
headers = {
"x-acs-dingtalk-access-token": token
}
retries = 0
while retries < max_retries:
res = requests.post(api, headers=headers, json=payload)
if res.status_code == 200:
data_get = res.json()
break
else:
retries += 1
time.sleep(0.1)
return data_get
try:
# 使用 GET,并传入 params(自动编码为 query string
res = requests.get(api_base, headers=headers, params=params, timeout=10)
if res.status_code == 200:
return res.json()
else:
print(f"请求失败,状态码: {res.status_code}, 响应: {res.text}")
except Exception as e:
print(f"请求异常: {e}")
retries += 1
time.sleep(0.1)
return {} # 或抛出异常
class NpEncoder(json.JSONEncoder):