续约代办历史记录迁移
展会线索登记
This commit is contained in:
@@ -401,6 +401,7 @@ try:
|
|||||||
ts = int(round(t * 1000))
|
ts = int(round(t * 1000))
|
||||||
randint = random.randint(100000000, 999999999)
|
randint = random.randint(100000000, 999999999)
|
||||||
req = res_new['result'] + "|" + formData['textField_kuntp6fk'] + "|" + formData['textField_kuntp6fl']+ "|" + formData['employeeField_kykw5ege'] + "_" + str(ts) + "_" + str(randint)
|
req = res_new['result'] + "|" + formData['textField_kuntp6fk'] + "|" + formData['textField_kuntp6fl']+ "|" + formData['employeeField_kykw5ege'] + "_" + str(ts) + "_" + str(randint)
|
||||||
|
# 实例ID|门ID|服务单号|专属运营顾问
|
||||||
str_en = des_encrypt(req)
|
str_en = des_encrypt(req)
|
||||||
print(str_en.decode('utf-8'))
|
print(str_en.decode('utf-8'))
|
||||||
req_new = str_en.decode('utf-8')
|
req_new = str_en.decode('utf-8')
|
||||||
|
|||||||
+2240
-1299
File diff suppressed because it is too large
Load Diff
+21
-8
@@ -12,7 +12,7 @@ from back_ground_module import CommonModule
|
|||||||
from log_config import configure_task_logger, configure_error_task_logger
|
from log_config import configure_task_logger, configure_error_task_logger
|
||||||
from yd_api import YDAPI
|
from yd_api import YDAPI
|
||||||
from api import API
|
from api import API
|
||||||
from tqdm.notebook import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
logger = configure_task_logger()
|
logger = configure_task_logger()
|
||||||
error_task_logger = configure_error_task_logger()
|
error_task_logger = configure_error_task_logger()
|
||||||
@@ -23,9 +23,10 @@ output_dir = "output"
|
|||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
# 加载数据
|
# 加载数据
|
||||||
df = pd.read_csv(r"D:\Idea Project\SaaS_V1.7\test\output\expanded_yd_data.csv").astype(str)
|
# 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(
|
df2 = pd.read_excel(
|
||||||
r"D:\Idea Project\SaaS_V1.7\test\output\续约服务流程_历史维修记录迁移测试_20260116110136.xlsx"
|
r"D:\Idea Project\SaaS_V1.7\test\output\续约服务流程_20260324165743.xlsx"
|
||||||
).fillna('').astype(str)
|
).fillna('').astype(str)
|
||||||
# 从df中获取流程编码获取流程详细信息 # 测试注释
|
# 从df中获取流程编码获取流程详细信息 # 测试注释
|
||||||
token = yd_api_instance.generateToken()
|
token = yd_api_instance.generateToken()
|
||||||
@@ -35,12 +36,21 @@ systemToken = "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2"
|
|||||||
|
|
||||||
all_instance_data = []
|
all_instance_data = []
|
||||||
for index, row in tqdm(df.iterrows(), total=len(df)):
|
for index, row in tqdm(df.iterrows(), total=len(df)):
|
||||||
instance_id = row["processInstanceId"]
|
instance_id = row["实例ID"]
|
||||||
instance_info = yd_api_instance.processes_instancesInfos(token, instance_id, appType, systemToken)
|
instance_info = yd_api_instance.processes_instancesInfos(token, instance_id, appType, systemToken)
|
||||||
all_instance_data.append(instance_info.get("data"))
|
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 = pd.DataFrame(all_instance_data)
|
||||||
ndf.to_csv(r"D:\Idea Project\SaaS_V1.7\\test\output\yd_process_details.csv")
|
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")
|
ndf = pd.read_csv(r"D:\Idea Project\SaaS_V1.7\test\output\yd_process_details.csv")
|
||||||
@@ -68,6 +78,7 @@ jdy_map = {
|
|||||||
"门店问题": "_widget_1764820541711",
|
"门店问题": "_widget_1764820541711",
|
||||||
"价格问题": "_widget_1764820541713",
|
"价格问题": "_widget_1764820541713",
|
||||||
"不续约具体情况说明": "_widget_1764820541702",
|
"不续约具体情况说明": "_widget_1764820541702",
|
||||||
|
"宜搭实例ID": "_widget_1774339442956",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 宜搭字段ID → 简道云中文名 映射
|
# 宜搭字段ID → 简道云中文名 映射
|
||||||
@@ -94,6 +105,7 @@ yd_field_id_to_jdy_chinese = {
|
|||||||
"selectField_l31clxg2": "价格问题",
|
"selectField_l31clxg2": "价格问题",
|
||||||
"textareaField_l31clxg4": "不续约具体情况说明",
|
"textareaField_l31clxg4": "不续约具体情况说明",
|
||||||
"radioField_l85ppdie": "续约意愿",
|
"radioField_l85ppdie": "续约意愿",
|
||||||
|
"实例ID":"宜搭实例ID"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 值映射(用于标准化选项值)
|
# 值映射(用于标准化选项值)
|
||||||
@@ -212,8 +224,8 @@ for idx, row in ndf.iterrows():
|
|||||||
# 批量发送更新请求
|
# 批量发送更新请求
|
||||||
logger.info(f"共构造 {len(update_records)} 条更新记录")
|
logger.info(f"共构造 {len(update_records)} 条更新记录")
|
||||||
APP_ID = "675b900991ad2491c69389ca"
|
APP_ID = "675b900991ad2491c69389ca"
|
||||||
ENTRY_ID = "6965eec36b73376aa0b5bff8"
|
# ENTRY_ID = "6965eec36b73376aa0b5bff8"
|
||||||
# ENTRY_ID = "6931063d64187eaf6b927557"
|
ENTRY_ID = "6931063d64187eaf6b927557"
|
||||||
|
|
||||||
for record in tqdm(update_records):
|
for record in tqdm(update_records):
|
||||||
payload = {
|
payload = {
|
||||||
@@ -224,5 +236,6 @@ for record in tqdm(update_records):
|
|||||||
"data": record["data"],
|
"data": record["data"],
|
||||||
"is_start_trigger": False
|
"is_start_trigger": False
|
||||||
}
|
}
|
||||||
|
|
||||||
res = api_instance.entry_data_update(payload)
|
res = api_instance.entry_data_update(payload)
|
||||||
# print(res)
|
# print(res)
|
||||||
+445
@@ -0,0 +1,445 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import imaplib
|
||||||
|
import email
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from email.header import decode_header
|
||||||
|
import pandas as pd
|
||||||
|
# 假设 api.py 在当前目录下,且包含 API 类
|
||||||
|
import requests
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from decimal import Decimal
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
from log_config import configure_task_logger, configure_error_task_logger
|
||||||
|
import json
|
||||||
|
|
||||||
|
# === 强制标准输出为 UTF-8 (兼容不同运行环境) ===
|
||||||
|
# 注意:在部分 IDE 中重新包装 sys.stdout 可能会导致乱码,若报错可注释掉以下两行
|
||||||
|
try:
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ================= 配置区域 =================
|
||||||
|
EMAIL_ACCOUNT = "zhangyang@f6car.cn"
|
||||||
|
PASSWORD = "RGBdMggmJ4s2FzZK" # ⚠️ 生产环境建议使用环境变量,不要硬编码
|
||||||
|
IMAP_SERVER = "imap.qiye.aliyun.com"
|
||||||
|
IMAP_PORT = 993
|
||||||
|
SUBJECT_KEYWORD = "展会线索登记"
|
||||||
|
DAYS_TO_SCAN = 30 # 扫描最近30天
|
||||||
|
OUTPUT_FILE = f"展会线索_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||||
|
|
||||||
|
# 定义标准字段顺序
|
||||||
|
FIELD_KEYS = ["姓名", "手机号", "省", "市", "区", "公司名称", "备注"]
|
||||||
|
|
||||||
|
|
||||||
|
class API:
|
||||||
|
def entry_data_list(self, data: dict, replace: bool = False, max_retries: int = 20) -> Dict: # 获取多条表单数据
|
||||||
|
"""
|
||||||
|
获取多条表单数据
|
||||||
|
:param max_retries: 最大重试次数
|
||||||
|
:param replace: 是否替换字段
|
||||||
|
:param data:
|
||||||
|
api_key: 应用id
|
||||||
|
entry_id: 表单id
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list'
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': "Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN", # 曹伟应用api测试 app_key
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
all_data_batches = [] # 用于存储每次请求返回的数据批次
|
||||||
|
last_data_id = None
|
||||||
|
exit_flag = False
|
||||||
|
while True:
|
||||||
|
payload = json.dumps({
|
||||||
|
"app_id": data['api_key'], # 应用ID
|
||||||
|
"entry_id": data['entry_id'], # 表单ID
|
||||||
|
"limit": 90,
|
||||||
|
"data_id": last_data_id,
|
||||||
|
"filter": data.get('filter', None)
|
||||||
|
})
|
||||||
|
retries = 0
|
||||||
|
|
||||||
|
while retries <= max_retries:
|
||||||
|
data_get = None
|
||||||
|
try:
|
||||||
|
res = requests.post(url=url, data=payload, headers=headers, timeout=10)
|
||||||
|
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
|
||||||
|
data_get = res.json()
|
||||||
|
if data_get["data"]:
|
||||||
|
all_data_batches.extend(data_get['data'])
|
||||||
|
last_data_id = data_get['data'][-1].get('_id')
|
||||||
|
print(f"已获取 {len(all_data_batches)} 条数据")
|
||||||
|
break # 成功则跳出循环
|
||||||
|
else:
|
||||||
|
if 'data' not in data_get or len(data_get['data']) == 0:
|
||||||
|
exit_flag = True
|
||||||
|
break
|
||||||
|
retries += 1
|
||||||
|
time.sleep(0.5) # 在重试之间稍作停顿
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
retries += 1
|
||||||
|
time.sleep(0.5) # 在重试之间稍作停顿
|
||||||
|
if retries > max_retries:
|
||||||
|
all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理
|
||||||
|
|
||||||
|
if exit_flag:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 构建最终返回的字典
|
||||||
|
final_data = {
|
||||||
|
'data': all_data_batches # 'data' 键对应的值是列表的列表
|
||||||
|
}
|
||||||
|
|
||||||
|
return final_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def data_batch_create(data: dict, max_retries: int = 20) -> Optional[requests.Response]: # 新建单条数据
|
||||||
|
"""
|
||||||
|
新建单条表单数据
|
||||||
|
:param max_retries: 最大重试次数
|
||||||
|
:param data: 应该包含应用id、表单id,以及新建的数据data['data']
|
||||||
|
:return: 返回创建后简道云返回的信息
|
||||||
|
"""
|
||||||
|
url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/create'
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': "Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN", # 曹伟应用api测试 app_key
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
# noinspection DuplicatedCode
|
||||||
|
payload = json.dumps({
|
||||||
|
"app_id": data['api_key'], # 应用ID
|
||||||
|
"entry_id": data['entry_id'], # 表单ID
|
||||||
|
"data": data['data'],
|
||||||
|
"is_start_workflow": data.get('is_start_workflow', "false"),
|
||||||
|
"is_start_trigger": data.get('is_start_trigger', "false"),
|
||||||
|
"transaction_id": data.get('transaction_id', "")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
retries = 0
|
||||||
|
while retries <= max_retries:
|
||||||
|
try:
|
||||||
|
res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10)
|
||||||
|
res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常
|
||||||
|
data_get = res.json()
|
||||||
|
if res.status_code == 200:
|
||||||
|
return data_get
|
||||||
|
else:
|
||||||
|
retries += 1
|
||||||
|
time.sleep(3) # 在重试之间稍作停顿
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
retries += 1
|
||||||
|
time.sleep(3) # 在重试之间稍作停顿
|
||||||
|
if retries > max_retries:
|
||||||
|
print(
|
||||||
|
f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
def decode_mime_words(s):
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
decoded_parts = []
|
||||||
|
# decode_header 返回的是 list of (bytes/str, encoding)
|
||||||
|
for part, encoding in decode_header(s):
|
||||||
|
if isinstance(part, bytes):
|
||||||
|
decoded_parts.append(part.decode(encoding or 'utf-8', errors='ignore'))
|
||||||
|
else:
|
||||||
|
decoded_parts.append(str(part))
|
||||||
|
return "".join(decoded_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_data_from_body(body_text):
|
||||||
|
"""
|
||||||
|
从邮件正文中提取线索数据。
|
||||||
|
格式:姓名 | 手机号 | 省 | 市 | 区 | 公司 | 备注
|
||||||
|
"""
|
||||||
|
if not body_text:
|
||||||
|
return []
|
||||||
|
|
||||||
|
data_list = []
|
||||||
|
# 【修复点 1】splitlines() 是方法,需要加括号
|
||||||
|
lines = body_text.splitlines()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
# 如果行中没有分隔符,跳过
|
||||||
|
if '|' not in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 按 '|' 分割并去除首尾空格
|
||||||
|
parts = [p.strip() for p in line.split('|')]
|
||||||
|
|
||||||
|
# 【关键校验】至少需要前两个字段(姓名、手机号)非空
|
||||||
|
if len(parts) < 2 or not parts[0] or not parts[1]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 构建字典,动态映射
|
||||||
|
record = {}
|
||||||
|
for i, key in enumerate(FIELD_KEYS):
|
||||||
|
if i < len(parts):
|
||||||
|
record[key] = parts[i]
|
||||||
|
else:
|
||||||
|
record[key] = "" # 缺失的字段填空字符串
|
||||||
|
|
||||||
|
data_list.append(record)
|
||||||
|
|
||||||
|
return data_list
|
||||||
|
|
||||||
|
|
||||||
|
def save_to_excel(leads, filename):
|
||||||
|
if not leads:
|
||||||
|
return None
|
||||||
|
|
||||||
|
df = pd.DataFrame(leads)
|
||||||
|
|
||||||
|
# 定义期望的列顺序
|
||||||
|
cols = ["姓名", "手机号", "省", "市", "区", "公司名称", "备注", "来源邮件时间"]
|
||||||
|
|
||||||
|
# 确保列存在且顺序正确
|
||||||
|
# 先保留所有现有列中在 cols 里的,按 cols 顺序
|
||||||
|
ordered_cols = [c for c in cols if c in df.columns]
|
||||||
|
# 再加上可能存在的其他列(虽然逻辑上不应该有,但以防万一)
|
||||||
|
other_cols = [c for c in df.columns if c not in cols]
|
||||||
|
final_cols = ordered_cols + other_cols
|
||||||
|
|
||||||
|
df = df[final_cols]
|
||||||
|
# df.to_excel(filename, index=False)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"正在连接 IMAP 服务器:{IMAP_SERVER} ...")
|
||||||
|
mail = None
|
||||||
|
|
||||||
|
start_date = datetime.now() - timedelta(days=DAYS_TO_SCAN)
|
||||||
|
date_str = start_date.strftime("%d-%b-%Y").upper()
|
||||||
|
|
||||||
|
all_leads = []
|
||||||
|
count_processed = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
|
||||||
|
mail.login(EMAIL_ACCOUNT, PASSWORD)
|
||||||
|
mail.select("INBOX")
|
||||||
|
|
||||||
|
print(f"正在搜索 [{date_str}] 之后的邮件...")
|
||||||
|
search_query = f'(SINCE "{date_str}")'
|
||||||
|
status, messages = mail.search(None, search_query)
|
||||||
|
|
||||||
|
if status != "OK":
|
||||||
|
print("❌ 搜索失败")
|
||||||
|
return
|
||||||
|
|
||||||
|
mail_ids = messages[0].split()
|
||||||
|
if not mail_ids:
|
||||||
|
print(f"✅ 未找到 {date_str} 之后的新邮件。")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"📩 找到 {len(mail_ids)} 封近期邮件,开始详细扫描...")
|
||||||
|
|
||||||
|
for mail_id in mail_ids:
|
||||||
|
try:
|
||||||
|
status, msg_data = mail.fetch(mail_id, "(RFC822)")
|
||||||
|
if status != "OK":
|
||||||
|
continue
|
||||||
|
|
||||||
|
raw_email = msg_data[0][1]
|
||||||
|
|
||||||
|
if isinstance(raw_email, bytes):
|
||||||
|
mime_msg = email.message_from_bytes(raw_email)
|
||||||
|
else:
|
||||||
|
mime_msg = email.message_from_string(raw_email.decode('utf-8', errors='ignore'))
|
||||||
|
|
||||||
|
subject = decode_mime_words(mime_msg.get("Subject"))
|
||||||
|
if SUBJECT_KEYWORD not in subject:
|
||||||
|
continue
|
||||||
|
|
||||||
|
count_processed += 1
|
||||||
|
date_str_full = mime_msg.get("Date")
|
||||||
|
body_content = ""
|
||||||
|
|
||||||
|
if mime_msg.is_multipart():
|
||||||
|
for part in mime_msg.walk():
|
||||||
|
content_disposition = part.get_content_disposition()
|
||||||
|
if content_disposition and "attachment" in str(content_disposition):
|
||||||
|
continue
|
||||||
|
content_type = part.get_content_type()
|
||||||
|
if content_type in ["text/plain", "text/html"]:
|
||||||
|
try:
|
||||||
|
charset = part.get_content_charset() or 'utf-8'
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
text = payload.decode(charset, errors='ignore') if isinstance(payload,
|
||||||
|
bytes) else str(
|
||||||
|
payload)
|
||||||
|
if content_type == "text/html":
|
||||||
|
text = re.sub(r'<[^>]+>', ' ', text)
|
||||||
|
body_content += text + "\n"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
charset = mime_msg.get_content_charset() or 'utf-8'
|
||||||
|
payload = mime_msg.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
body_content = payload.decode(charset, errors='ignore') if isinstance(payload,
|
||||||
|
bytes) else str(
|
||||||
|
payload)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
leads = extract_data_from_body(body_content)
|
||||||
|
|
||||||
|
for lead in leads:
|
||||||
|
lead["来源邮件时间"] = date_str_full
|
||||||
|
|
||||||
|
if leads:
|
||||||
|
print(f"[{subject}] -> 提取 {len(leads)} 条")
|
||||||
|
all_leads.extend(leads)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"处理邮件 ID {mail_id} 时出错:{e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ================= 新增:本地数据去重逻辑 =================
|
||||||
|
original_count = len(all_leads)
|
||||||
|
if original_count > 0:
|
||||||
|
seen_phones = set()
|
||||||
|
unique_leads = []
|
||||||
|
|
||||||
|
for lead in all_leads:
|
||||||
|
phone = str(lead.get("手机号", "")).strip()
|
||||||
|
# 如果手机号为空,或者已经出现过,则跳过
|
||||||
|
if not phone or phone in seen_phones:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen_phones.add(phone)
|
||||||
|
unique_leads.append(lead)
|
||||||
|
|
||||||
|
all_leads = unique_leads
|
||||||
|
removed_count = original_count - len(all_leads)
|
||||||
|
if removed_count > 0:
|
||||||
|
print(
|
||||||
|
f"\n⚠️ 检测到重复数据,已根据【手机号】去重:原始 {original_count} 条 -> 去重后 {len(all_leads)} 条 (移除 {removed_count} 条)")
|
||||||
|
else:
|
||||||
|
print(f"\n✅ 数据检查完成,无重复手机号。共 {len(all_leads)} 条。")
|
||||||
|
# =======================================================
|
||||||
|
|
||||||
|
df = save_to_excel(all_leads, OUTPUT_FILE)
|
||||||
|
|
||||||
|
if df is not None:
|
||||||
|
print(f"\n✅ 成功!共扫描 {count_processed} 封匹配邮件,最终有效线索 {len(all_leads)} 条。")
|
||||||
|
print(f"文件已保存至:{OUTPUT_FILE}")
|
||||||
|
else:
|
||||||
|
print(f"\n⚠️ 扫描完成,但在 {count_processed} 封近期邮件中未找到符合格式的数据。")
|
||||||
|
return # 如果没有数据,后续同步逻辑无需执行
|
||||||
|
|
||||||
|
# 同步至简道云
|
||||||
|
if all_leads:
|
||||||
|
print("\n开始同步至简道云...")
|
||||||
|
api_instance = API()
|
||||||
|
|
||||||
|
payload_query = {
|
||||||
|
"api_key": "66b9678280b37f8a276b1d01",
|
||||||
|
"entry_id": "69b22dc5434e05c7b6b4b5b2",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = api_instance.entry_data_list(payload_query)
|
||||||
|
now_data = response.get("data", []) if response else []
|
||||||
|
|
||||||
|
existing_phones = set()
|
||||||
|
phone_widget_id = "_widget_1692928669587"
|
||||||
|
|
||||||
|
for item in now_data:
|
||||||
|
phone_val = item.get(phone_widget_id)
|
||||||
|
if phone_val:
|
||||||
|
existing_phones.add(str(phone_val).strip())
|
||||||
|
|
||||||
|
print(f"简道云现有手机号数量:{len(existing_phones)}")
|
||||||
|
|
||||||
|
new_count = 0
|
||||||
|
# 此时 df 已经是去重后的数据,且 all_leads 也是去重后的
|
||||||
|
# 再次遍历 df 确保只提交本地没有的(防止简道云已有但本地没查到的情况,虽然逻辑上上面已经过滤了)
|
||||||
|
# 为了代码健壮性,这里保留原有的 existing_phones 检查逻辑
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
current_phone = str(row["手机号"]).strip()
|
||||||
|
|
||||||
|
if not current_phone:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 双重保险:如果简道云里已经有了,跳过
|
||||||
|
if current_phone in existing_phones:
|
||||||
|
print(f"跳过 (云端已存在): {current_phone}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_payload = {
|
||||||
|
"api_key": "66b9678280b37f8a276b1d01",
|
||||||
|
"entry_id": "69b22dc5434e05c7b6b4b5b2",
|
||||||
|
"data": {
|
||||||
|
"_widget_1690785229260": {"value": row.get("姓名", "")},
|
||||||
|
"_widget_1690785229261": {"value": row.get("公司名称", "")},
|
||||||
|
"_widget_1692928669587": {"value": row.get("手机号", "")},
|
||||||
|
"_widget_1690785229266": {"value": row.get("备注", "")},
|
||||||
|
"_widget_1690785326597": {"value": {"province": row.get("省", ""),
|
||||||
|
"city": row.get("市", ""),
|
||||||
|
"district": row.get("区", ""),
|
||||||
|
"detail": row.get("省", "") + row.get("市",
|
||||||
|
"") + row.get(
|
||||||
|
"区", ""),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_widget_1690785229279": {"value": row.get("市", "")},
|
||||||
|
"_widget_1773381838511": {"value": row.get("省", "")},
|
||||||
|
"_widget_1692070309987": {"value": row.get("区", "")},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = api_instance.data_batch_create(new_payload)
|
||||||
|
|
||||||
|
if result and (result.get("success") or result.get("code") == 200 or "error" not in result):
|
||||||
|
new_count += 1
|
||||||
|
print(f"新增成功:{current_phone} ({row.get('姓名')})")
|
||||||
|
else:
|
||||||
|
print(f"提交结果:{current_phone}, 返回:{result}")
|
||||||
|
# 如果提交成功但返回格式奇怪,也可以考虑计入成功,视具体API文档而定
|
||||||
|
# 这里保守处理,只有明确成功才算
|
||||||
|
|
||||||
|
print(f"\n✅ 同步完成,本次新增 {new_count} 条数据。")
|
||||||
|
|
||||||
|
except Exception as api_err:
|
||||||
|
print(f"\n❌ 简道云 API 交互错误:{api_err}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
except imaplib.IMAP4.error as e:
|
||||||
|
print("\n❌ IMAP 协议错误:")
|
||||||
|
print(f"错误详情:{e}")
|
||||||
|
except Exception as e:
|
||||||
|
print("\n❌ 发生严重错误:")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if mail:
|
||||||
|
try:
|
||||||
|
mail.close()
|
||||||
|
mail.logout()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
payload ={
|
||||||
|
"app_id": "675b900991ad2491c69389ca",
|
||||||
|
|
||||||
|
"data": {
|
||||||
|
"_widget_1767851302591": 1772508166000,
|
||||||
|
"_widget_1767863644355": "何文娟",
|
||||||
|
"_widget_1767851302590": "1772507997862",
|
||||||
|
"_widget_1767851302593": [
|
||||||
|
"064863221620345741"
|
||||||
|
],
|
||||||
|
"_widget_1767851302592": "15651815921",
|
||||||
|
"_widget_1767851302595": [
|
||||||
|
"122744083333090577"
|
||||||
|
],
|
||||||
|
"_widget_1767851302594": "064863221620345741",
|
||||||
|
"_widget_1767851302597": "废弃测试三月分店",
|
||||||
|
"_widget_1767851302596": "CHS202603030049192",
|
||||||
|
"_widget_1767952153422": [
|
||||||
|
"洗车",
|
||||||
|
"美容"
|
||||||
|
],
|
||||||
|
"_widget_1772264444051": "19999.0",
|
||||||
|
"_widget_1767851302599": "普通客户(VIP)",
|
||||||
|
"_widget_1767851302598": "zyc测试公司废弃",
|
||||||
|
"_widget_1768290159749": [
|
||||||
|
"122744083333090577"
|
||||||
|
],
|
||||||
|
"_widget_1768290159748": "华南区域",
|
||||||
|
"_widget_1768290159747": "15870306745529522117",
|
||||||
|
"_widget_1767863644358": "15651815921",
|
||||||
|
"_widget_1768290159746": "16338827489080131642",
|
||||||
|
"_widget_1767863644357": "何文娟",
|
||||||
|
"_widget_1767863644356": "15651815921",
|
||||||
|
"_widget_1767863644582": [
|
||||||
|
"064863221620345741"
|
||||||
|
],
|
||||||
|
"_widget_1767851302585": "XQFWD20260303001",
|
||||||
|
"_widget_1772264443873": "台湾省",
|
||||||
|
"_widget_1772703526239": "1",
|
||||||
|
"_widget_1772264443872": "其它",
|
||||||
|
"_widget_1767851302600": 1772508361000,
|
||||||
|
"_widget_1768290159737": 1773112966000,
|
||||||
|
"_widget_1767851302602": "皇冠版",
|
||||||
|
"_widget_1772264443811": "C",
|
||||||
|
"_widget_1772264443812": "5",
|
||||||
|
"_widget_1767851302604": "30"
|
||||||
|
}
|
||||||
|
,
|
||||||
|
"entry_id": "695f439e3e910f09190d8e99",
|
||||||
|
"is_start_workflow": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": "新签节点调用格式",
|
||||||
|
"id": "671b10d308af2bdc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"id": "initial_id",
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": true,
|
||||||
|
"ExecuteTime": {
|
||||||
|
"end_time": "2026-03-12T09:25:38.245095300Z",
|
||||||
|
"start_time": "2026-03-12T09:25:36.441415900Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": [
|
||||||
|
"import requests\n",
|
||||||
|
"url = \"https://manage-pre.f6yc.com/hive-admin/yida/updateNode\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"payload = {\n",
|
||||||
|
" \"nodeCode\": \"ORG_RESEARCH\",\n",
|
||||||
|
" \"needTraining\": \"是\",\n",
|
||||||
|
" \"impPrincipal\":\"['171408516124043808']\",\n",
|
||||||
|
" \"instanceId\": \"69b269d6c193f83cc27f65e7\"\n",
|
||||||
|
"}\n",
|
||||||
|
"\n",
|
||||||
|
"# [{\"_id\":\"69a285f3fa0d9fb1984da9bf\",\"name\":\"张乐乐\",\"username\":\"171408516124043808\",\"status\":1,\"type\":0}]\n",
|
||||||
|
"\n",
|
||||||
|
"response = requests.post(url, data=payload)\n",
|
||||||
|
"response.json()"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"{'code': 200, 'data': None, 'message': 'SUCCESS'}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 14,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execution_count": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": "# 续约联调",
|
||||||
|
"id": "6ce335625a1c1fbb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"ExecuteTime": {
|
||||||
|
"end_time": "2026-03-24T10:17:42.894874500Z",
|
||||||
|
"start_time": "2026-03-24T10:17:42.269474800Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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 = \"69c264920c1729f7dd5d6a17\" # 数据id\n",
|
||||||
|
"orgid = \"16058306913393233941\" # 门店id\n",
|
||||||
|
"order_id = \"XYFWD20260324003\" # 服务单号\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": [
|
||||||
|
"ZM6bNebKW1EGUtptuTOYEckre+5e1D71Y8ctpN5lD1xNhI3w5j8nHVuNidcwJ4ocxUj/Bxobdn2vCjpl2/6VA5vu3Z6XrxvJsKGP3FaJIOSB4m3PHQpzsI85uNqNysqVYpkhssITC4SM0OmPjzzdqQ==\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execution_count": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"ExecuteTime": {
|
||||||
|
"end_time": "2026-03-13T08:12:33.285136400Z",
|
||||||
|
"start_time": "2026-03-13T08:12:33.246808900Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": "impPrincipal_value",
|
||||||
|
"id": "8edd45ef4657d8ed",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"'171408516124043808'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 26,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execution_count": 26
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython2",
|
||||||
|
"version": "2.7.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import pandas as pd
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = "https://manage-pre.f6yc.com/hive-admin/yida/renewal/updateNode"
|
||||||
|
|
||||||
|
payload = {"instanceId":"15348c27-57b7-4285-b93b-87b3d41f5a28","nodeCode":"SIXTY","impPrincipal":"[\"053052302136860181\"]"}
|
||||||
|
|
||||||
|
result = requests.post(url, data=payload)
|
||||||
|
print(result.text)
|
||||||
+2
-1
@@ -449,6 +449,7 @@ class JDYToYDRenewalToDo(object):
|
|||||||
"30天是否跟进": "_widget_1764820541632",
|
"30天是否跟进": "_widget_1764820541632",
|
||||||
"30天处理人": "_widget_1764820541636",
|
"30天处理人": "_widget_1764820541636",
|
||||||
"30天跟进时间": "_widget_1765352838633",
|
"30天跟进时间": "_widget_1765352838633",
|
||||||
|
"是否联系上":"_widget_1764820541638",
|
||||||
"数据ID": "_id"
|
"数据ID": "_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +754,7 @@ class JDYToYDRenewalToDo(object):
|
|||||||
key=lambda x: parse_dt(x.get("operateTimeGMT") or x.get("activeTimeGMT")),
|
key=lambda x: parse_dt(x.get("operateTimeGMT") or x.get("activeTimeGMT")),
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
# 优先使用当前待办(type == TODO),否则用最新一条
|
# 优先使用当前待办(type == "TODO"),否则用最新一条
|
||||||
yd_todo = next((r for r in yd_records if str(r.get("type")).upper() == "TODO"), None)
|
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_latest = yd_todo or (yd_records[0] if yd_records else {})
|
||||||
yd_stage = extract_stage_from_text(
|
yd_stage = extract_stage_from_text(
|
||||||
|
|||||||
+660
-67
File diff suppressed because one or more lines are too long
@@ -97,7 +97,6 @@ class YDAPI:
|
|||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
res = requests.get(api, headers=headers, params=formData)
|
res = requests.get(api, headers=headers, params=formData)
|
||||||
|
|
||||||
return res.json()
|
return res.json()
|
||||||
except (requests.exceptions.RequestException, Exception) as e:
|
except (requests.exceptions.RequestException, Exception) as e:
|
||||||
print(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...")
|
print(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...")
|
||||||
@@ -155,7 +154,7 @@ class YDAPI:
|
|||||||
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", instanceStatus="RUNNING",
|
systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", instanceStatus="RUNNING",
|
||||||
max_retries=10, delay=2, createFromTimeGMT=None, createToTimeGMT=None,
|
max_retries=10, delay=2, createFromTimeGMT=None, createToTimeGMT=None,
|
||||||
modifiedFromTimeGMT=None,
|
modifiedFromTimeGMT=None,
|
||||||
modifiedToTimeGMT=None, searchFieldJson={},useAlias=False):
|
modifiedToTimeGMT=None, searchFieldJson={},useAlias=False, instanceIdList=None):
|
||||||
"""
|
"""
|
||||||
函数功能:读取流程表单的所有数据,并加入重试机制。
|
函数功能:读取流程表单的所有数据,并加入重试机制。
|
||||||
|
|
||||||
@@ -169,6 +168,7 @@ class YDAPI:
|
|||||||
instanceStatus (str): 流程实例状态,默认为"RUNNING"
|
instanceStatus (str): 流程实例状态,默认为"RUNNING"
|
||||||
max_retries (int): 最大重试次数,默认为10次
|
max_retries (int): 最大重试次数,默认为10次
|
||||||
delay (int): 每次重试之间的延迟秒数,默认为2秒
|
delay (int): 每次重试之间的延迟秒数,默认为2秒
|
||||||
|
instanceIdList (list): 实例ID列表,用于精确查询
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: 返回从API获取的流程表单实例数据的JSON解析结果。
|
dict: 返回从API获取的流程表单实例数据的JSON解析结果。
|
||||||
@@ -199,6 +199,8 @@ class YDAPI:
|
|||||||
),
|
),
|
||||||
"useAlias": useAlias,
|
"useAlias": useAlias,
|
||||||
}
|
}
|
||||||
|
if instanceIdList:
|
||||||
|
formData["instanceIdList"] = instanceIdList
|
||||||
# print(formData)
|
# print(formData)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
Reference in New Issue
Block a user