欧洲分国家车辆激活数查询脚本

This commit is contained in:
2026-03-18 15:13:48 +08:00
commit 1438117ba7
32 changed files with 5102 additions and 0 deletions

View File

@@ -0,0 +1,411 @@
import requests
import logging
import time
from datetime import datetime, timedelta, date
# ====================== 配置区域 ======================
# 飞书应用凭证
APP_ID = "cli_a8b50ec0eed1500d"
APP_SECRET = "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"
# 日统计表格配置
DAILY_SPREADSHEET_ID = "MIqssvuB3h3XmgtkI26cBX7Angc" # 日统计电子表格ID
DAILY_SHEET_ID = "46ea20" # 日统计工作表ID
DAILY_URL = "https://my-ichery.feishu.cn/sheets/MIqssvuB3h3XmgtkI26cBX7Angc?sheet=46ea20" # 日统计表格链接
# 周统计表格配置
WEEKLY_BASE_TOKEN = "T3QLbmpqma014ussjy9cgitqnSh" # 周统计多维表格ID
WEEKLY_TABLE_ID = "tbloB11YuVOSdi9e" # 周统计表ID
WEEKLY_URL = "https://my-ichery.feishu.cn/base/T3QLbmpqma014ussjy9cgitqnSh?table=tbloB11YuVOSdi9e" # 周统计表格链接
# 飞书机器人配置
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/3bc45247-c469-47a0-bfe9-c62e46c5aca0"
# ====================== 日志配置 ======================
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
def get_access_token():
"""获取飞书访问令牌[1,3](@ref)"""
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
api_post_data = {"app_id": APP_ID, "app_secret": APP_SECRET}
try:
response = requests.post(api_token_url, json=api_post_data, timeout=10)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") == 0:
return resp_data["tenant_access_token"]
else:
raise Exception(f"获取Token失败: {resp_data.get('msg')}")
except Exception as e:
logging.error(f"获取飞书Token异常: {str(e)}")
raise
def read_daily_data(retry=3):
"""从飞书电子表格读取日统计数据[1,3](@ref)"""
access_token = get_access_token()
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{DAILY_SPREADSHEET_ID}/values/{DAILY_SHEET_ID}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
for attempt in range(retry):
try:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") != 0:
raise Exception(f"API错误: {resp_data.get('msg')}")
# 解析数据:跳过标题行,只取日期和激活数量
values = resp_data['data']['valueRange']['values'][1:]
daily_data = []
for row in values:
try:
date_str = row[0]
count = int(row[1])
date_obj = datetime.strptime(date_str, "%Y-%m-%d").date()
daily_data.append({"date": date_obj, "count": count})
except (IndexError, ValueError) as e:
logging.warning(f"数据格式错误: {row} - {str(e)}")
return daily_data
except Exception as e:
logging.warning(f"读取飞书表格失败(尝试 {attempt + 1}/{retry}): {str(e)}")
if attempt < retry - 1:
time.sleep(2 ** attempt) # 指数退避重试
raise Exception(f"读取飞书表格失败,重试{retry}次后仍不成功")
def clear_weekly_table(retry=3):
"""清空周统计表(覆盖写入前的准备)[3,5](@ref)"""
access_token = get_access_token()
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{WEEKLY_BASE_TOKEN}/tables/{WEEKLY_TABLE_ID}/records"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
# 获取所有记录ID
record_ids = []
page_token = ""
while True:
params = {"page_size": 100}
if page_token:
params["page_token"] = page_token
response = requests.get(url, headers=headers, params=params, timeout=30)
resp_data = response.json()
if resp_data.get("code") != 0:
raise Exception(f"获取记录失败: {resp_data.get('msg')}")
# 收集所有记录的ID
for record in resp_data.get("data", {}).get("items", []):
record_ids.append(record["record_id"])
# 检查是否有更多数据
if resp_data["data"].get("has_more"):
page_token = resp_data["data"].get("page_token", "")
else:
break
if not record_ids:
logging.info("周统计表为空,无需清除")
return True
logging.info(f"获取到{len(record_ids)}条记录待删除")
# 批量删除所有记录
delete_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{WEEKLY_BASE_TOKEN}/tables/{WEEKLY_TABLE_ID}/records/batch_delete"
# 分批删除每次最多100条[3](@ref)
batch_size = 100
for i in range(0, len(record_ids), batch_size):
batch_ids = record_ids[i:i + batch_size]
payload = {"records": batch_ids}
for attempt in range(retry):
try:
response = requests.post(delete_url, json=payload, headers=headers, timeout=30)
resp_data = response.json()
if response.status_code == 200 and resp_data.get("code") == 0:
logging.info(f"成功删除批次 {i // batch_size + 1}: {len(batch_ids)}条记录")
break
else:
error_msg = resp_data.get('msg', '未知错误')
logging.warning(f"删除记录失败(尝试 {attempt + 1}/{retry}): {error_msg}")
time.sleep(2 ** attempt)
except Exception as e:
logging.warning(f"删除记录网络错误(尝试 {attempt + 1}/{retry}): {str(e)}")
time.sleep(2 ** attempt)
return True
def write_weekly_data(records, retry=3):
"""覆盖写入数据到飞书多维表格(周统计)[3,5](@ref)"""
access_token = get_access_token()
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{WEEKLY_BASE_TOKEN}/tables/{WEEKLY_TABLE_ID}/records/batch_create"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
# 构建API请求体
payload = {"records": []}
for row in records:
try:
# 使用毫秒时间戳格式(飞书官方要求)[3](@ref)
start_dt = datetime.combine(row['week_start'], datetime.min.time())
end_dt = datetime.combine(row['week_end'], datetime.min.time())
start_timestamp = int(start_dt.timestamp() * 1000)
end_timestamp = int(end_dt.timestamp() * 1000)
payload["records"].append({
"fields": {
"周数": str(row['week_number']),
"周开始日期": start_timestamp,
"周结束日期": end_timestamp,
"激活数量": int(row['active_count'])
}
})
except Exception as e:
logging.error(f"记录格式错误: {row} - {str(e)}")
# 处理空记录情况
if not payload["records"]:
logging.warning("没有有效记录可写入多维表格")
return 0
# 分批写入每次最多100条[3](@ref)
batch_size = 100
success_count = 0
for i in range(0, len(payload["records"]), batch_size):
batch = payload["records"][i:i + batch_size]
batch_payload = {"records": batch}
batch_success = False
for attempt in range(retry):
try:
response = requests.post(url, json=batch_payload, headers=headers, timeout=30)
resp_data = response.json()
if response.status_code == 200 and resp_data.get("code") == 0:
if not batch_success:
success_count += len(batch)
batch_success = True
logging.info(f"成功写入批次 {i // batch_size + 1}: {len(batch)}条记录")
break
else:
error_msg = resp_data.get('msg', '未知错误')
error_code = resp_data.get("code", "")
# 错误诊断[3](@ref)
if error_code == "DatetimeFieldConvFail":
logging.error("日期字段转换失败: 请检查多维表格中日期字段类型配置")
elif error_code == "TextFieldConvFail":
logging.error("文本字段转换失败: 请检查数字字段是否包含非数字字符")
logging.warning(f"飞书API错误(尝试 {attempt + 1}/{retry}): {error_msg}")
time.sleep(2 ** attempt)
except Exception as e:
logging.warning(f"网络错误(尝试 {attempt + 1}/{retry}): {str(e)}")
time.sleep(2 ** attempt)
if not batch_success:
logging.error(f"批次 {i // batch_size + 1} 写入失败")
return success_count
def calculate_iso_week(date):
"""计算ISO周数处理跨年"""
iso_year, iso_week, iso_day = date.isocalendar()
return f"{iso_year}-W{iso_week:02d}"
def calculate_week_range(date):
"""计算所在周的周一和周日日期ISO标准"""
start_date = date - timedelta(days=date.weekday())
end_date = start_date + timedelta(days=6)
return start_date, end_date
def send_feishu_message(message, retry=2):
"""发送飞书通知[6,7,8](@ref)"""
headers = {'Content-Type': 'application/json'}
# 构建交互式卡片
payload = {
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": True},
"elements": [
{
"tag": "div",
"text": {"content": message, "tag": "lark_md"}
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"content": "查看日统计", "tag": "plain_text"},
"type": "default",
"url": DAILY_URL
},
{
"tag": "button",
"text": {"content": "查看周统计", "tag": "plain_text"},
"type": "primary",
"url": WEEKLY_URL
}
]
}
],
"header": {
"title": {"content": "数据同步完成通知", "tag": "plain_text"},
"template": "green"
}
}
}
for attempt in range(retry):
try:
response = requests.post(WEBHOOK_URL, headers=headers, json=payload, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
logging.warning(f"飞书消息发送失败(尝试 {attempt + 1}/{retry}): {str(e)}")
if attempt < retry - 1:
time.sleep(2) # 等待后重试
raise Exception(f"飞书消息发送失败,重试{retry}次后仍不成功")
if __name__ == '__main__':
try:
logging.info("开始周统计计算流程...")
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# 1. 从飞书日统计表读取数据
daily_data = read_daily_data()
logging.info(f"成功读取{len(daily_data)}条日统计记录")
# 2. 计算周统计数据(处理跨年)
weekly_stats = {}
for entry in daily_data:
date = entry['date']
count = entry['count']
week_number = calculate_iso_week(date)
week_start, week_end = calculate_week_range(date)
if week_number not in weekly_stats:
weekly_stats[week_number] = {
'week_number': week_number,
'week_start': week_start,
'week_end': week_end,
'active_count': 0
}
weekly_stats[week_number]['active_count'] += count
# 转换为列表并排序
weekly_list = sorted(weekly_stats.values(), key=lambda x: x['week_start'])
logging.info(f"生成{len(weekly_list)}条周统计记录")
# 3. 覆盖写入周统计表
# 先清空表
if not clear_weekly_table():
raise Exception("清空周统计表失败")
logging.info("成功清空周统计表")
# 再写入新数据
success_count = write_weekly_data(weekly_list)
if success_count != len(weekly_list):
raise Exception(f"周统计写入失败: 成功写入{success_count}条,总记录{len(weekly_list)}")
logging.info(f"成功覆盖写入{success_count}条周统计记录")
# 4. 构建通知消息
today = datetime.now().date()
current_week_number = calculate_iso_week(today)
stats_summary = f"• 本周({current_week_number})激活车辆: {weekly_stats.get(current_week_number, {}).get('active_count', 0)}\n"
stats_summary += f"• 累计激活车辆: {sum(week['active_count'] for week in weekly_list)}\n"
if weekly_list:
latest_week = weekly_list[-1]
stats_summary += f"• 最新一周({latest_week['week_number']}): {latest_week['week_start'].strftime('%Y-%m-%d')}{latest_week['week_end'].strftime('%Y-%m-%d')}, 激活{latest_week['active_count']}"
message = f"📅 周统计计算完成 ✅\n"
message += f"⏰ 时间: {current_time}\n"
message += f"📊 统计摘要:\n{stats_summary}\n"
message += f"📝 处理结果: 成功处理{len(daily_data)}条日记录 → 生成{len(weekly_list)}条周记录"
# 5. 构建通知消息
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# 新增统计维度计算
today = datetime.now().date()
yesterday = today - timedelta(days=1)
current_week_start = today - timedelta(days=today.weekday())
current_week_end = current_week_start + timedelta(days=6)
last_week_start = current_week_start - timedelta(days=7)
last_week_end = last_week_start + timedelta(days=6)
three_months_ago = today - timedelta(days=90)
# 计算昨日数据
yesterday_count = next((d['count'] for d in daily_data if d['date'] == yesterday), 0)
# 计算今日数据
today_count = next((d['count'] for d in daily_data if d['date'] == today), 0)
# 计算本周数据(当前周)
current_week_number = calculate_iso_week(today)
current_week_total = weekly_stats.get(current_week_number, {}).get('active_count', 0)
# 计算上周数据
last_week_number = calculate_iso_week(last_week_start)
last_week_total = weekly_stats.get(last_week_number, {}).get('active_count', 0)
# 计算最近三个月数据
recent_three_months = sum(
d['count'] for d in daily_data
if three_months_ago <= d['date'] <= today
)
# 计算累计数据
total_count = sum(d['count'] for d in daily_data)
# 构建统计摘要
stats_summary = f"• 今日激活: {today_count}\n"
stats_summary += f"• 昨日激活: {yesterday_count}\n"
stats_summary += f"• 本周激活: {current_week_total}\n"
stats_summary += f"• 上周激活: {last_week_total}\n"
stats_summary += f"• 近三月激活: {recent_three_months}\n"
stats_summary += f"• 累计激活: {total_count}\n"
# # 添加最新周统计摘要
# if weekly_list:
# latest_week = weekly_list[-1]
# stats_summary += f"• 最新一周({latest_week['week_number']}): {latest_week['active_count']}辆"
message = f"📅欧盟O&J统计计算完成 ✅\n"
message += f"⏰ 时间: {current_time}\n"
message += f"📊 统计摘要:\n{stats_summary}\n"
message += f"📝 处理结果: 成功处理{len(daily_data)}条日记录 → 生成{len(weekly_list)}条周记录"
# 5. 发送通知
result = send_feishu_message(message)
logging.info(f"飞书消息发送成功: {result}")
except Exception as e:
error_message = f"❌ 周统计计算异常: {str(e)}"
logging.exception("程序执行异常")
send_feishu_message(error_message)

View File

@@ -0,0 +1,362 @@
import pandas as pd
import requests
import logging
import time
import json
from sqlalchemy import create_engine
from urllib.parse import quote_plus
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(essage)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# ====================== 全局配置 ======================
# 数据库连接配置
DB_CONFIG = {
'host': 'eutsp-prod.mysql.germany.rds.aliyuncs.com',
'port': 3306,
'user': 'international_tsp_eu_r',
'password': 'ZXhBgo1TB2XbF3kP',
'database': 'chery_international_tsp_eu'
}
# 飞书多维表格配置
FEISHU_APP_TOKEN = "T3QLbmpqma014ussjy9cgitqnSh" # 应用Token
FEISHU_TABLE_ID = "tbloPJHLc05MHIAY" # 表格ID
VIN_FIELD_NAME = "车架号" # VIN字段名称
ACTIVE_FIELD_NAME = "激活状态" # 激活状态字段名称
VEHICLE_NAME_FIELD = "车型"# 车型字段名称
# 飞书应用凭证
APP_ID = "cli_a8b50ec0eed1500d"
APP_SECRET = "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"
# 飞书机器人配置
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/3bc45247-c469-47a0-bfe9-c62e46c5aca0"
MULTI_TABLE_URL = "https://my-ichery.feishu.cn/base/T3QLbmpqma014ussjy9cgitqnSh?from=from_copylink"
# ====================== 通用函数 ======================
def get_access_token():
"""获取飞书访问令牌"""
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
api_post_data = {"app_id": APP_ID, "app_secret": APP_SECRET}
try:
response = requests.post(api_token_url, json=api_post_data, timeout=10)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") == 0:
return resp_data["tenant_access_token"]
else:
raise Exception(f"获取Token失败: {resp_data.get('msg')}")
except Exception as e:
logging.error(f"获取飞书Token异常: {str(e)}")
raise
def send_feishu_message(message):
"""发送飞书通知[9,10](@ref)"""
headers = {'Content-Type': 'application/json'}
# 构建交互式卡片
payload = {
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": True},
"elements": [
{
"tag": "div",
"text": {"content": message, "tag": "lark_md"}
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"content": "查看多维表格", "tag": "plain_text"},
"type": "primary",
"url": MULTI_TABLE_URL
}
]
}
],
"header": {
"title": {"content": "车辆状态同步报告", "tag": "plain_text"},
"template": "blue"
}
}
}
try:
response = requests.post(WEBHOOK_URL, headers=headers, json=payload, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
logging.error(f"飞书消息发送失败: {str(e)}")
return None
# ====================== 车辆激活状态同步 ======================
def read_feishu_records(app_token, table_id, retry=3):
"""从飞书多维表格读取车辆记录"""
access_token = get_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {"page_size": 500} # 单次请求最大记录数
all_records = []
page_token = None
while True:
if page_token:
params["page_token"] = page_token
records_fetched = False
for attempt in range(retry):
try:
response = requests.get(url, headers=headers, params=params, timeout=30)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") != 0:
raise Exception(f"API错误: {resp_data.get('msg')}")
records = resp_data["data"]["items"]
all_records.extend(records)
# 检查是否有下一页
if "page_token" in resp_data["data"] and resp_data["data"]["has_more"]:
page_token = resp_data["data"]["page_token"]
else:
page_token = None
records_fetched = True
break
except Exception as e:
logging.warning(f"读取多维表格失败(尝试 {attempt + 1}/{retry}): {str(e)}")
if attempt < retry - 1:
time.sleep(2 ** attempt)
if not records_fetched:
raise Exception("无法读取多维表格数据")
if not page_token:
break
return all_records
def fetch_activation_data():
"""从数据库查询车辆激活状态"""
try:
# 创建SQLAlchemy引擎
safe_password = quote_plus(DB_CONFIG['password'])
engine = create_engine(
f"mysql+pymysql://{DB_CONFIG['user']}:{safe_password}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}",
pool_recycle=3600
)
sql_query = """
SELECT
v.VIN AS car_vins,
CASE s.STATE
WHEN 2 THEN 'active'
ELSE 'no_active'
END AS active_status,
iv.`NAME` AS vehicle_name
FROM v_vehicle_info v
JOIN v_tbox_info t USING (VIN)
JOIN v_sim_info s USING (SN)
LEFT JOIN v_material_info m ON m.MATERIAL_ID = v.MATERIAL_ID
LEFT JOIN v_outside_vehicle_type ov ON ov.TYPE_CODE = m.OUTSIDE_TYPE_CODE
LEFT JOIN v_inside_vehicle_type iv ON iv.TYPE_CODE = ov.INSIDE_TYPE_CODE
"""
# 执行查询并转换为DataFrame
df = pd.read_sql(sql_query, engine)
return df.set_index('car_vins')['active_status'].to_dict()
except Exception as e:
logging.error(f"数据库查询失败: {str(e)}")
raise
def compare_and_update_records(app_token, table_id, activation_data):
"""比较并更新飞书多维表格记录"""
access_token = get_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
# 1. 读取多维表格所有记录
feishu_records = read_feishu_records(app_token, table_id)
logging.info(f"成功读取{len(feishu_records)}条多维表格记录")
# 统计变量
total_updated = 0
activated_count = 0
not_activated_count = 0
# 2. 准备需要更新的记录
records_to_update = []
for record in feishu_records:
try:
record_id = record["record_id"]
fields = record.get("fields", {})
# 获取车架号(VIN)
vin = fields.get(VIN_FIELD_NAME)
if not vin:
logging.warning(f"记录 {record_id} 缺少VIN字段")
continue
# 获取多维表格中的激活状态
current_status = fields.get(ACTIVE_FIELD_NAME, "").lower()
# 获取数据库中的激活状态
db_status = activation_data.get(vin)
if not db_status:
logging.warning(f"VIN {vin} 在数据库中未找到")
continue
# 比较状态是否需要更新
if db_status != current_status:
records_to_update.append({
"record_id": record_id,
"fields": {
ACTIVE_FIELD_NAME: db_status
}
})
logging.info(f"VIN {vin} 状态需更新: {current_status}{db_status}")
# 统计更新状态
if db_status == 'active':
activated_count += 1
else:
not_activated_count += 1
except Exception as e:
logging.error(f"处理记录异常: {record} - {str(e)}")
total_updated = len(records_to_update)
logging.info(f"需要更新{total_updated}条记录")
logging.info(f"更新统计: 激活车辆: {activated_count}台, 未激活车辆: {not_activated_count}")
# 3. 批量更新记录 (每批次50条)
if not records_to_update:
logging.info("没有需要更新的记录")
return total_updated, activated_count, not_activated_count
batch_size = 50
success_count = 0
for i in range(0, len(records_to_update), batch_size):
batch = records_to_update[i:i + batch_size]
batch_payload = {"records": batch}
update_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update"
for attempt in range(3): # 最多重试3次
try:
response = requests.post(update_url, headers=headers, json=batch_payload, timeout=30)
resp_data = response.json()
if response.status_code == 200 and resp_data.get("code") == 0:
success_count += len(batch)
logging.info(f"成功更新批次 {i // batch_size + 1}: {len(batch)}条记录")
break
else:
error_msg = resp_data.get('msg', '未知错误')
logging.warning(f"更新失败(尝试 {attempt + 1}/3): {error_msg}")
time.sleep(2 ** attempt)
except Exception as e:
logging.warning(f"网络错误(尝试 {attempt + 1}/3): {str(e)}")
time.sleep(2 ** attempt)
logging.info(f"总计更新成功{success_count}条记录")
return total_updated, activated_count, not_activated_count
def sync_vehicle_activation():
"""执行车辆激活状态同步流程"""
try:
start_time = time.time()
logging.info("开始车辆激活状态同步流程...")
# 1. 从数据库获取激活状态数据
activation_data = fetch_activation_data()
logging.info(f"从数据库获取{len(activation_data)}条激活状态记录")
# 统计数据库中的激活状态
db_activated = sum(1 for status in activation_data.values() if status == 'active')
db_not_activated = sum(1 for status in activation_data.values() if status == 'no_active')
logging.info(f"数据库统计: 激活车辆: {db_activated}台, 未激活车辆: {db_not_activated}")
# 2. 与飞书多维表格数据比对并更新
total_updated, activated_count, not_activated_count = compare_and_update_records(
FEISHU_APP_TOKEN, FEISHU_TABLE_ID, activation_data
)
# 3. 输出最终统计结果
logging.info("=" * 50)
logging.info(f"本次更新统计结果:")
logging.info(f" - 更新车辆总数: {total_updated}")
logging.info(f" - 激活车辆数量: {activated_count}")
logging.info(f" - 未激活车辆数量: {not_activated_count}")
logging.info("=" * 50)
# 4. 完成日志
elapsed_time = time.time() - start_time
logging.info(f"流程完成! 耗时: {elapsed_time:.2f}秒, 更新记录数: {total_updated}")
return {
"total_updated": total_updated,
"activated_count": activated_count,
"not_activated_count": not_activated_count,
"elapsed_time": elapsed_time
}
except Exception as e:
logging.error(f"流程执行异常: {str(e)}")
logging.exception("详细错误信息")
return None
# ====================== 主执行流程 ======================
if __name__ == '__main__':
# 执行车辆激活状态同步
sync_result = sync_vehicle_activation()
if sync_result:
# 构建飞书消息
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
message = (
f"🚗 **车辆激活状态同步完成** ✅\n"
f"⏰ 同步时间: {current_time}\n"
f"⏱️ 处理耗时: {sync_result['elapsed_time']:.2f}\n\n"
f"📊 **同步统计结果:**\n"
f"• 更新车辆总数: {sync_result['total_updated']}\n"
f"• ✅ 激活车辆数量: {sync_result['activated_count']}\n"
f"• ❌ 未激活车辆数量: {sync_result['not_activated_count']}\n\n"
f"🔗 查看多维表格: [点击访问]({MULTI_TABLE_URL})"
)
# 发送飞书通知
send_result = send_feishu_message(message)
if send_result:
logging.info(f"飞书消息发送成功: {send_result}")
else:
logging.error("飞书消息发送失败")
else:
error_message = "❌ 车辆激活状态同步失败,请检查日志"
send_feishu_message(error_message)
logging.error(error_message)

View File

@@ -0,0 +1,322 @@
import pandas as pd
import requests
import logging
import time
import json
from sqlalchemy import create_engine
from urllib.parse import quote_plus
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s %(message)s'
)
# ====================== 全局配置 ======================
# 数据库连接配置
DB_CONFIG = {
'host': 'eutsp-prod.mysql.germany.rds.aliyuncs.com',
'port': 3306,
'user': 'international_tsp_eu_r',
'password': 'ZXhBgo1TB2XbF3kP',
'database': 'chery_international_tsp_eu'
}
# 飞书多维表格配置
FEISHU_APP_TOKEN = "T3QLbmpqma014ussjy9cgitqnSh" # 应用Token
FEISHU_TABLE_ID = "tbloPJHLc05MHIAY" # 表格ID
VIN_FIELD_NAME = "车架号" # VIN字段名称
ACTIVE_FIELD_NAME = "激活状态" # 激活状态字段名称
VEHICLE_NAME_FIELD = "车型"
# 飞书应用凭证
APP_ID = "cli_a8b50ec0eed1500d"
APP_SECRET = "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"
# 飞书机器人配置
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/3bc45247-c469-47a0-bfe9-c62e46c5aca0"
MULTI_TABLE_URL = "https://my-ichery.feishu.cn/base/T3QLbmpqma014ussjy9cgitqnSh?from=from_copylink"
# ====================== 通用函数 ======================
def get_access_token():
"""获取飞书访问令牌"""
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
api_post_data = {"app_id": APP_ID, "app_secret": APP_SECRET}
try:
response = requests.post(api_token_url, json=api_post_data, timeout=10)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") == 0:
return resp_data["tenant_access_token"]
else:
raise Exception(f"获取Token失败: {resp_data.get('msg')}")
except Exception as e:
logging.error(f"获取飞书Token异常: {e}")
raise
def send_feishu_message(message):
"""发送飞书通知[9,10](@ref)"""
headers = {'Content-Type': 'application/json'}
# 构建交互式卡片
payload = {
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": True},
"elements": [
{
"tag": "div",
"text": {"content": message, "tag": "lark_md"}
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"content": "查看多维表格", "tag": "plain_text"},
"type": "primary",
"url": MULTI_TABLE_URL
}
]
}
],
"header": {
"title": {"content": "车辆状态同步报告", "tag": "plain_text"},
"template": "blue"
}
}
}
try:
response = requests.post(WEBHOOK_URL, headers=headers, json=payload, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
logging.error(f"飞书消息发送失败: {str(e)}")
return None
def read_feishu_records(app_token, table_id, retry=3):
"""从飞书多维表格读取车辆记录"""
access_token = get_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {"page_size": 500}
all_records = []
page_token = None
while True:
if page_token:
params["page_token"] = page_token
for attempt in range(retry):
try:
response = requests.get(url, headers=headers, params=params, timeout=30)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") != 0:
raise Exception(f"API错误: {resp_data.get('msg')}")
records = resp_data["data"]["items"]
all_records.extend(records)
has_more = resp_data["data"].get("has_more")
page_token = resp_data["data"].get("page_token") if has_more else None
break
except Exception as e:
logging.warning(f"读取多维表格失败(尝试 {attempt+1}/{retry}): {e}")
time.sleep(2 ** attempt)
else:
raise Exception("无法读取多维表格数据")
if not page_token:
break
return all_records
def fetch_activation_data():
"""从数据库查询车辆激活状态和车型"""
try:
safe_password = quote_plus(DB_CONFIG['password'])
engine = create_engine(
f"mysql+pymysql://{DB_CONFIG['user']}:{safe_password}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}",
pool_recycle=3600
)
sql_query = """
SELECT
v.VIN AS car_vins,
CASE s.STATE
WHEN 2 THEN 'active'
ELSE 'no_active'
END AS active_status,
iv.`NAME` AS `车型`
FROM v_vehicle_info v
JOIN v_tbox_info t USING (VIN)
JOIN v_sim_info s USING (SN)
LEFT JOIN v_material_info m ON m.MATERIAL_ID = v.MATERIAL_ID
LEFT JOIN v_outside_vehicle_type ov ON ov.TYPE_CODE = m.OUTSIDE_TYPE_CODE
LEFT JOIN v_inside_vehicle_type iv ON iv.TYPE_CODE = ov.INSIDE_TYPE_CODE
"""
df = pd.read_sql(sql_query, engine)
# --- 新增:按 VIN 去重,保留第一条记录 ---
df = df.drop_duplicates(subset=['car_vins'], keep='first')
# 校验列是否都在
expected = ['car_vins', 'active_status', VEHICLE_NAME_FIELD]
missing = [c for c in expected if c not in df.columns]
if missing:
logging.error(f"查询结果缺少列: {missing}")
raise KeyError(f"Missing columns: {missing}")
# 设索引并返回字典
df2 = df.set_index('car_vins')[['active_status', VEHICLE_NAME_FIELD]]
return df2.to_dict(orient='index')
except Exception as e:
logging.error(f"数据库查询失败: {e}")
raise
def compare_and_update_records(app_token, table_id, activation_data):
"""比较并更新飞书多维表格记录:既更新激活状态,也写入车型"""
access_token = get_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
feishu_records = read_feishu_records(app_token, table_id)
logging.info(f"成功读取{len(feishu_records)}条多维表格记录")
records_to_update = []
activated_count = not_activated_count = 0
for record in feishu_records:
record_id = record.get("record_id")
fields = record.get("fields", {})
vin = fields.get(VIN_FIELD_NAME)
if not vin:
logging.warning(f"记录 {record_id} 缺少VIN字段")
continue
db = activation_data.get(vin)
if not db:
logging.warning(f"VIN {vin} 在数据库中未找到")
continue
db_status = db['active_status']
db_name = db[VEHICLE_NAME_FIELD]
current_status = fields.get(ACTIVE_FIELD_NAME, "").lower()
current_name = fields.get(VEHICLE_NAME_FIELD, "")
if db_status != current_status or db_name != current_name:
update_fields = {
ACTIVE_FIELD_NAME: db_status,
VEHICLE_NAME_FIELD: db_name
}
records_to_update.append({
"record_id": record_id,
"fields": update_fields
})
logging.debug(f"VIN {vin} 需要更新: 状态 {current_status}{db_status}, 车型 {current_name}{db_name}")
if db_status == "active":
activated_count += 1
else:
not_activated_count += 1
total_updated = len(records_to_update)
logging.info(f"需要更新 {total_updated} 条记录(激活:{activated_count},未激活:{not_activated_count}")
# 批量更新
batch_size = 50
for i in range(0, total_updated, batch_size):
batch = records_to_update[i:i+batch_size]
payload = {"records": batch}
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update"
for attempt in range(3):
try:
r = requests.post(url, headers=headers, json=payload, timeout=30)
data = r.json()
if r.status_code == 200 and data.get("code") == 0:
logging.info(f"{i//batch_size+1}批更新成功,{len(batch)}")
break
else:
logging.warning(f"{i//batch_size+1}批更新失败(尝试{attempt+1}/3){data.get('msg')}")
except Exception as e:
logging.warning(f"网络异常(尝试{attempt+1}/3){e}")
time.sleep(2 ** attempt)
return total_updated, activated_count, not_activated_count
def sync_vehicle_activation():
"""执行车辆激活状态同步流程"""
try:
start_time = time.time()
logging.info("开始车辆激活状态同步流程...")
activation_data = fetch_activation_data()
logging.info(f"从数据库获取{len(activation_data)}条激活状态记录")
db_activated = sum(1 for v in activation_data.values() if v['active_status']=='active')
db_not_activated = sum(1 for v in activation_data.values() if v['active_status']=='no_active')
logging.info(f"数据库统计: 激活车辆 {db_activated}台, 未激活车辆 {db_not_activated}")
total_updated, activated_count, not_activated_count = compare_and_update_records(
FEISHU_APP_TOKEN, FEISHU_TABLE_ID, activation_data
)
logging.info("="*50)
logging.info(f"本次更新统计: 总更新 {total_updated}台, 激活 {activated_count}台, 未激活 {not_activated_count}")
logging.info("="*50)
elapsed = time.time()-start_time
logging.info(f"流程完成! 耗时 {elapsed:.2f}")
return {
"total_updated": total_updated,
"activated_count": activated_count,
"not_activated_count": not_activated_count,
"elapsed_time": elapsed
}
except Exception as e:
logging.error(f"流程执行异常: {e}")
logging.exception("详细错误信息")
return None
if __name__ == '__main__':
sync_result=sync_vehicle_activation()
if sync_result:
# 构建飞书消息
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
message = (
f"🚗 **车辆激活状态同步完成** ✅\n"
f"⏰ 同步时间: {current_time}\n"
f"⏱️ 处理耗时: {sync_result['elapsed_time']:.2f}\n\n"
f"📊 **同步统计结果:**\n"
f"• 更新车辆总数: {sync_result['total_updated']}\n"
f"• ✅ 激活车辆数量: {sync_result['activated_count']}\n"
f"• ❌ 未激活车辆数量: {sync_result['not_activated_count']}\n\n"
f"🔗 查看多维表格: [点击访问]({MULTI_TABLE_URL})"
)
# 发送飞书通知
send_result = send_feishu_message(message)
if send_result:
logging.info(f"飞书消息发送成功: {send_result}")
else:
logging.error("飞书消息发送失败")
else:
error_message = "❌ 车辆激活状态同步失败,请检查日志"
send_feishu_message(error_message)
logging.error(error_message)

View File

@@ -0,0 +1,347 @@
import pandas as pd
import requests
import logging
import time
import json
from sqlalchemy import create_engine
from urllib.parse import quote_plus
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s %(message)s'
)
# ====================== 全局配置 ======================
# 数据库连接配置
DB_CONFIG = {
'host': 'eutsp-prod.mysql.germany.rds.aliyuncs.com',
'port': 3306,
'user': 'international_tsp_eu_r',
'password': 'ZXhBgo1TB2XbF3kP',
'database': 'chery_international_tsp_eu'
}
# 飞书多维表格配置 (多市场表格配置)
FEISHU_APP_TOKEN = "T3QLbmpqma014ussjy9cgitqnSh" # 应用Token (恢复此关键变量)
MARKET_TABLES = [
{"name": "英国", "table_id": "tblbfEHrsojQ4D5w", "view_id": "vewBvcYDAa"}
]
# 通用字段映射
VIN_FIELD_NAME = "车架号"
ACTIVE_FIELD_NAME = "激活状态"
VEHICLE_NAME_FIELD = "车型"
# 飞书应用凭证
APP_ID = "cli_a8b50ec0eed1500d"
APP_SECRET = "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"
# 飞书机器人配置
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/3bc45247-c469-47a0-bfe9-c62e46c5aca0"
MULTI_TABLE_URL = "https://my-ichery.feishu.cn/base/T3QLbmpqma014ussjy9cgitqnSh?from=from_copylink"
# ====================== 通用函数 ======================
def get_access_token():
"""获取飞书访问令牌"""
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
api_post_data = {"app_id": APP_ID, "app_secret": APP_SECRET}
try:
response = requests.post(api_token_url, json=api_post_data, timeout=10)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") == 0:
return resp_data["tenant_access_token"]
else:
raise Exception(f"获取Token失败: {resp_data.get('msg')}")
except Exception as e:
logging.error(f"获取飞书Token异常: {e}")
raise
def send_feishu_message(message):
"""发送飞书通知"""
headers = {'Content-Type': 'application/json'}
payload = {
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": True},
"elements": [
{
"tag": "div",
"text": {"content": message, "tag": "lark_md"}
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"content": "查看多维表格", "tag": "plain_text"},
"type": "primary",
"url": MULTI_TABLE_URL
}
]
}
],
"header": {
"title": {"content": "车辆状态同步报告", "tag": "plain_text"},
"template": "blue"
}
}
}
try:
response = requests.post(WEBHOOK_URL, headers=headers, json=payload, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
logging.error(f"飞书消息发送失败: {str(e)}")
return None
def read_feishu_records(app_token, table_id, view_id, retry=3):
"""从飞书多维表格读取车辆记录"""
access_token = get_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {
"page_size": 500,
"view_id": view_id # 添加视图ID参数
}
all_records = []
page_token = None
while True:
if page_token:
params["page_token"] = page_token
for attempt in range(retry):
try:
response = requests.get(url, headers=headers, params=params, timeout=30)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") != 0:
raise Exception(f"API错误: {resp_data.get('msg')}")
records = resp_data["data"]["items"]
all_records.extend(records)
has_more = resp_data["data"].get("has_more")
page_token = resp_data["data"].get("page_token") if has_more else None
break
except Exception as e:
logging.warning(f"读取多维表格失败(尝试 {attempt + 1}/{retry}): {e}")
time.sleep(2 ** attempt)
else:
raise Exception("无法读取多维表格数据")
if not page_token:
break
return all_records
def fetch_activation_data():
"""从数据库查询车辆激活状态和车型"""
try:
safe_password = quote_plus(DB_CONFIG['password'])
engine = create_engine(
f"mysql+pymysql://{DB_CONFIG['user']}:{safe_password}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}",
pool_recycle=3600
)
sql_query = """
SELECT
v.VIN AS car_vins,
CASE s.STATE
WHEN 2 THEN 'active'
ELSE 'no_active'
END AS active_status,
iv.`NAME` AS `车型`
FROM v_vehicle_info v
JOIN v_tbox_info t USING (VIN)
JOIN v_sim_info s USING (SN)
LEFT JOIN v_material_info m ON m.MATERIAL_ID = v.MATERIAL_ID
LEFT JOIN v_outside_vehicle_type ov ON ov.TYPE_CODE = m.OUTSIDE_TYPE_CODE
LEFT JOIN v_inside_vehicle_type iv ON iv.TYPE_CODE = ov.INSIDE_TYPE_CODE
"""
df = pd.read_sql(sql_query, engine)
df = df.drop_duplicates(subset=['car_vins'], keep='first')
expected = ['car_vins', 'active_status', VEHICLE_NAME_FIELD]
missing = [c for c in expected if c not in df.columns]
if missing:
logging.error(f"查询结果缺少列: {missing}")
raise KeyError(f"Missing columns: {missing}")
df2 = df.set_index('car_vins')[['active_status', VEHICLE_NAME_FIELD]]
return df2.to_dict(orient='index')
except Exception as e:
logging.error(f"数据库查询失败: {e}")
raise
def compare_and_update_records(app_token, table_id, view_id, activation_data):
"""比较并更新飞书多维表格记录"""
access_token = get_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
feishu_records = read_feishu_records(app_token, table_id, view_id)
logging.info(f"读取 {len(feishu_records)} 条多维表格记录完成")
# 初始化计数器和待更新列表
records_to_update = []
activated_count = 0
not_activated_count = 0
# Streamline record processing and validation upfront
valid_records = [
record for record in feishu_records
if record.get("fields", {}).get(VIN_FIELD_NAME)
]
logging.info(f"有效记录数: {len(valid_records)} (过滤无效记录)")
for record in valid_records:
record_id = record["record_id"]
fields = record["fields"]
vin = fields[VIN_FIELD_NAME]
# 数据库信息匹配
db_data = activation_data.get(vin)
if not db_data:
continue # 如果VIN不存在于数据库则跳过
db_status = db_data['active_status']
db_name = db_data[VEHICLE_NAME_FIELD]
current_status = fields.get(ACTIVE_FIELD_NAME, "").lower()
current_name = fields.get(VEHICLE_NAME_FIELD, "")
# 需要更新的记录
if db_status != current_status or db_name != current_name:
records_to_update.append({
"record_id": record_id,
"fields": {
ACTIVE_FIELD_NAME: db_status,
VEHICLE_NAME_FIELD: db_name
}
})
if db_status == "active":
activated_count += 1
else:
not_activated_count += 1
total_updated = len(records_to_update)
logging.info(f"准备更新 {total_updated} 条记录 (激活: {activated_count}, 未激活: {not_activated_count})")
# 批量更新记录
batch_size = 100
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update"
def batch_update(batch):
payload = {"records": batch}
for attempt in range(3): # 网络重试机制
try:
response = requests.post(url, headers=headers, json=payload, timeout=30)
if response.status_code == 200 and response.json().get("code") == 0:
return True # 更新成功
else:
logging.warning(f"更新失败,第{attempt + 1}次尝试: {response.json().get('msg')}")
except Exception as e:
logging.warning(f"网络异常,第{attempt + 1}次尝试: {e}")
time.sleep(2 ** attempt) # 指数级等待
return False # 所有尝试均失败
# 批次处理记录更新
for i in range(0, total_updated, batch_size):
batch = records_to_update[i:i + batch_size]
if batch_update(batch):
logging.info(f"批次 {i // batch_size + 1} 更新成功 ({len(batch)} 条记录)")
else:
logging.error(f"批次 {i // batch_size + 1} 更新失败 ({len(batch)} 条记录)")
return total_updated, activated_count, not_activated_count
# ====================== 多表格处理流程 ======================
def sync_all_markets():
"""执行所有市场的车辆激活状态同步"""
start_time = time.time()
all_results = []
try:
# 一次性获取所有激活数据(避免多次查询数据库)
logging.info("开始获取数据库激活数据...")
activation_data = fetch_activation_data()
logging.info(f"获取到 {len(activation_data)} 条车辆激活数据")
# 按市场依次处理
for market in MARKET_TABLES:
market_name = market["name"]
table_id = market["table_id"]
view_id = market["view_id"]
logging.info(f"开始同步市场: {market_name} (表格ID: {table_id})")
try:
# 使用FEISHU_APP_TOKEN修复NameError问题 ⭐⭐关键修复
updated, activated, not_activated = compare_and_update_records(
FEISHU_APP_TOKEN,
table_id,
view_id,
activation_data
)
result = {
"market": market_name,
"table_id": table_id,
"total_updated": updated,
"activated_count": activated,
"not_activated_count": not_activated,
"success": True
}
logging.info(f"{market_name}同步完成: 更新{updated}台 (✅激活{activated}台,❌未激活{not_activated}台)")
except Exception as e:
result = {
"market": market_name,
"table_id": table_id,
"error": str(e),
"success": False
}
logging.error(f"{market_name}同步失败: {str(e)}", exc_info=True)
all_results.append(result)
time.sleep(1) # 每个市场处理间隔
# 统计总体数据
total_elapsed = time.time() - start_time
return {
"success": True,
"results": all_results,
"elapsed_time": total_elapsed,
"start_time": start_time
}
except Exception as e:
logging.error(f"整体同步流程失败: {str(e)}", exc_info=True)
return {
"success": False,
"error": str(e),
"elapsed_time": time.time() - start_time
}
if __name__ == '__main__':
# 执行多市场同步
sync_result = sync_all_markets()
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")

View File

@@ -0,0 +1,387 @@
import pandas as pd
import requests
import logging
import time
import json
from sqlalchemy import create_engine
from urllib.parse import quote_plus
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s %(message)s'
)
# ====================== 全局配置 ======================
# 数据库连接配置
DB_CONFIG = {
'host': 'eutsp-prod.mysql.germany.rds.aliyuncs.com',
'port': 3306,
'user': 'international_tsp_eu_r',
'password': 'ZXhBgo1TB2XbF3kP',
'database': 'chery_international_tsp_eu'
}
# 飞书多维表格配置 (多市场表格配置)
FEISHU_APP_TOKEN = "T3QLbmpqma014ussjy9cgitqnSh" # 应用Token (恢复此关键变量)
MARKET_TABLES = [
{"name": "欧洲所有", "table_id": "tbloPJHLc05MHIAY", "view_id": "vewBvcYDAa"},
{"name": "意大利", "table_id": "tbl4ZXTTgcPvdZUD", "view_id": "vewBvcYDAa"},
{"name": "西班牙", "table_id": "tbl0KBWQT1ZqiT2g", "view_id": "vewBvcYDAa"},
{"name": "英国", "table_id": "tblbfEHrsojQ4D5w", "view_id": "vewBvcYDAa"},
{"name": "比利时", "table_id": "tblYN3eEEumBMlgB", "view_id": "vewBvcYDAa"}
]
# 通用字段映射
VIN_FIELD_NAME = "车架号"
ACTIVE_FIELD_NAME = "激活状态"
VEHICLE_NAME_FIELD = "车型"
# 飞书应用凭证
APP_ID = "cli_a8b50ec0eed1500d"
APP_SECRET = "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"
# 飞书机器人配置
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/3bc45247-c469-47a0-bfe9-c62e46c5aca0"
MULTI_TABLE_URL = "https://my-ichery.feishu.cn/base/T3QLbmpqma014ussjy9cgitqnSh?from=from_copylink"
# ====================== 通用函数 ======================
def get_access_token():
"""获取飞书访问令牌"""
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
api_post_data = {"app_id": APP_ID, "app_secret": APP_SECRET}
try:
response = requests.post(api_token_url, json=api_post_data, timeout=10)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") == 0:
return resp_data["tenant_access_token"]
else:
raise Exception(f"获取Token失败: {resp_data.get('msg')}")
except Exception as e:
logging.error(f"获取飞书Token异常: {e}")
raise
def send_feishu_message(message):
"""发送飞书通知"""
headers = {'Content-Type': 'application/json'}
payload = {
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": True},
"elements": [
{
"tag": "div",
"text": {"content": message, "tag": "lark_md"}
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"content": "查看多维表格", "tag": "plain_text"},
"type": "primary",
"url": MULTI_TABLE_URL
}
]
}
],
"header": {
"title": {"content": "车辆状态同步报告", "tag": "plain_text"},
"template": "blue"
}
}
}
try:
response = requests.post(WEBHOOK_URL, headers=headers, json=payload, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
logging.error(f"飞书消息发送失败: {str(e)}")
return None
def read_feishu_records(app_token, table_id, view_id, retry=3):
"""从飞书多维表格读取车辆记录"""
access_token = get_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {
"page_size": 500,
"view_id": view_id # 添加视图ID参数
}
all_records = []
page_token = None
while True:
if page_token:
params["page_token"] = page_token
for attempt in range(retry):
try:
response = requests.get(url, headers=headers, params=params, timeout=30)
response.raise_for_status()
resp_data = response.json()
if resp_data.get("code") != 0:
raise Exception(f"API错误: {resp_data.get('msg')}")
records = resp_data["data"]["items"]
all_records.extend(records)
has_more = resp_data["data"].get("has_more")
page_token = resp_data["data"].get("page_token") if has_more else None
break
except Exception as e:
logging.warning(f"读取多维表格失败(尝试 {attempt + 1}/{retry}): {e}")
time.sleep(2 ** attempt)
else:
raise Exception("无法读取多维表格数据")
if not page_token:
break
return all_records
def fetch_activation_data():
"""从数据库查询车辆激活状态和车型"""
try:
safe_password = quote_plus(DB_CONFIG['password'])
engine = create_engine(
f"mysql+pymysql://{DB_CONFIG['user']}:{safe_password}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}",
pool_recycle=3600
)
sql_query = """
SELECT
v.VIN AS car_vins,
CASE s.STATE
WHEN 2 THEN 'active'
ELSE 'no_active'
END AS active_status,
iv.`NAME` AS `车型`
FROM v_vehicle_info v
JOIN v_tbox_info t USING (VIN)
JOIN v_sim_info s USING (SN)
LEFT JOIN v_material_info m ON m.MATERIAL_ID = v.MATERIAL_ID
LEFT JOIN v_outside_vehicle_type ov ON ov.TYPE_CODE = m.OUTSIDE_TYPE_CODE
LEFT JOIN v_inside_vehicle_type iv ON iv.TYPE_CODE = ov.INSIDE_TYPE_CODE
"""
df = pd.read_sql(sql_query, engine)
df = df.drop_duplicates(subset=['car_vins'], keep='first')
expected = ['car_vins', 'active_status', VEHICLE_NAME_FIELD]
missing = [c for c in expected if c not in df.columns]
if missing:
logging.error(f"查询结果缺少列: {missing}")
raise KeyError(f"Missing columns: {missing}")
df2 = df.set_index('car_vins')[['active_status', VEHICLE_NAME_FIELD]]
return df2.to_dict(orient='index')
except Exception as e:
logging.error(f"数据库查询失败: {e}")
raise
def compare_and_update_records(app_token, table_id, view_id, activation_data):
"""比较并更新飞书多维表格记录"""
access_token = get_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json; charset=utf-8",
}
feishu_records = read_feishu_records(app_token, table_id, view_id)
logging.info(f"成功读取{len(feishu_records)}条多维表格记录")
records_to_update = []
activated_count = not_activated_count = 0
for record in feishu_records:
record_id = record.get("record_id")
fields = record.get("fields", {})
vin = fields.get(VIN_FIELD_NAME)
if not vin:
logging.warning(f"记录 {record_id} 缺少VIN字段")
continue
db = activation_data.get(vin)
if not db:
logging.warning(f"VIN {vin} 在数据库中未找到")
continue
db_status = db['active_status']
db_name = db[VEHICLE_NAME_FIELD]
current_status = fields.get(ACTIVE_FIELD_NAME, "").lower()
current_name = fields.get(VEHICLE_NAME_FIELD, "")
if db_status != current_status or db_name != current_name:
update_fields = {
ACTIVE_FIELD_NAME: db_status,
VEHICLE_NAME_FIELD: db_name
}
records_to_update.append({
"record_id": record_id,
"fields": update_fields
})
logging.debug(f"VIN {vin} 需要更新: 状态 {current_status}{db_status}, 车型 {current_name}{db_name}")
if db_status == "active":
activated_count += 1
else:
not_activated_count += 1
total_updated = len(records_to_update)
logging.info(f"需要更新 {total_updated} 条记录(激活:{activated_count},未激活:{not_activated_count}")
# 批量更新
batch_size = 50
for i in range(0, total_updated, batch_size):
batch = records_to_update[i:i + batch_size]
payload = {"records": batch}
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update"
for attempt in range(3):
try:
r = requests.post(url, headers=headers, json=payload, timeout=30)
data = r.json()
if r.status_code == 200 and data.get("code") == 0:
logging.info(f"{i // batch_size + 1}批更新成功,{len(batch)}")
break
else:
logging.warning(f"{i // batch_size + 1}批更新失败(尝试{attempt + 1}/3){data.get('msg')}")
except Exception as e:
logging.warning(f"网络异常(尝试{attempt + 1}/3){e}")
time.sleep(2 ** attempt)
return total_updated, activated_count, not_activated_count
# ====================== 多表格处理流程 ======================
def sync_all_markets():
"""执行所有市场的车辆激活状态同步"""
start_time = time.time()
all_results = []
try:
# 一次性获取所有激活数据(避免多次查询数据库)
logging.info("开始获取数据库激活数据...")
activation_data = fetch_activation_data()
logging.info(f"获取到 {len(activation_data)} 条车辆激活数据")
# 按市场依次处理
for market in MARKET_TABLES:
market_name = market["name"]
table_id = market["table_id"]
view_id = market["view_id"]
logging.info(f"开始同步市场: {market_name} (表格ID: {table_id})")
try:
# 使用FEISHU_APP_TOKEN修复NameError问题 ⭐⭐关键修复
updated, activated, not_activated = compare_and_update_records(
FEISHU_APP_TOKEN,
table_id,
view_id,
activation_data
)
result = {
"market": market_name,
"table_id": table_id,
"total_updated": updated,
"activated_count": activated,
"not_activated_count": not_activated,
"success": True
}
logging.info(f"{market_name}同步完成: 更新{updated}台 (✅激活{activated}台,❌未激活{not_activated}台)")
except Exception as e:
result = {
"market": market_name,
"table_id": table_id,
"error": str(e),
"success": False
}
logging.error(f"{market_name}同步失败: {str(e)}", exc_info=True)
all_results.append(result)
time.sleep(1) # 每个市场处理间隔
# 统计总体数据
total_elapsed = time.time() - start_time
return {
"success": True,
"results": all_results,
"elapsed_time": total_elapsed,
"start_time": start_time
}
except Exception as e:
logging.error(f"整体同步流程失败: {str(e)}", exc_info=True)
return {
"success": False,
"error": str(e),
"elapsed_time": time.time() - start_time
}
if __name__ == '__main__':
# 执行多市场同步
sync_result = sync_all_markets()
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
if sync_result.get("success"):
# 构建详细的飞书消息
message_header = f"🚗 **车辆激活状态同步完成** ✅\n" \
f"⏰ 同步时间: {current_time}\n" \
f"⏱️ 总耗时: {sync_result['elapsed_time']:.2f}\n\n" \
f"📊 **各市场同步详情:**\n"
message_details = []
success_count = 0
for result in sync_result["results"]:
if result["success"]:
success_count += 1
detail = (
f"• **{result['market']}**: "
f"更新 {result['total_updated']}台 | "
f"✅激活 {result['activated_count']}台 | "
f"❌未激活 {result['not_activated_count']}"
)
else:
detail = (
f"• **{result['market']}**: ❌同步失败!错误信息: `{result['error']}`"
)
message_details.append(detail)
message_summary = f"\n✅ 成功处理 {success_count}/{len(MARKET_TABLES)} 个市场\n" \
f"🔗 查看多维表格: [点击访问]({MULTI_TABLE_URL})"
full_message = message_header + "\n".join(message_details) + message_summary
# 发送飞书通知
send_result = send_feishu_message(full_message)
if send_result:
logging.info("飞书消息发送成功")
else:
logging.error("飞书消息发送失败")
else:
error_message = f"❌ **车辆激活状态同步失败**\n" \
f"⏰ 时间: {current_time}\n" \
f"⏱️ 耗时: {sync_result['elapsed_time']:.2f}\n" \
f"错误信息: `{sync_result['error']}`\n\n" \
f"请检查系统日志获取详细信息"
send_feishu_message(error_message)
logging.error(f"整体同步失败: {sync_result['error']}")