欧洲分国家车辆激活数查询脚本
This commit is contained in:
95
aa
Normal file
95
aa
Normal file
@@ -0,0 +1,95 @@
|
||||
config:
|
||||
printCql: true
|
||||
httpTimeout: 10000
|
||||
server:
|
||||
port: 80
|
||||
servlet:
|
||||
context-path: /scls
|
||||
|
||||
spring:
|
||||
main:
|
||||
allow-circular-references: true
|
||||
application:
|
||||
name: tsp-statistics-control-log-service
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
mvc:
|
||||
throw-exception-if-no-handler-found: true
|
||||
pathmatch:
|
||||
matching-strategy: ant_path_matcher
|
||||
datasource:
|
||||
url: jdbc:mysql://eutsp-uat.mysql.germany.rds.aliyuncs.com:3306/CHERY_INTERNATIONAL_TSP_EU?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true&allowMultiQueries=true
|
||||
username: international_tsp_eu
|
||||
password: p%geZgf$$n26Qpcn
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
idle-timeout: 180000
|
||||
maximum-pool-size: 10
|
||||
auto-commit: true
|
||||
connection-timeout: 30000
|
||||
connection-test-query: select 1
|
||||
kafka:
|
||||
bootstrap-servers: 10.95.3.204:9092,10.95.3.205:9092,10.95.3.206:9092
|
||||
producer:
|
||||
bootstrap-servers: 10.95.3.204:9092,10.95.3.205:9092,10.95.3.206:9092
|
||||
# 以下序列方式请根据项目实际情况进行配置,如果和下面相同则项目中可以不用配置
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.apache.kafka.common.serialization.ByteArraySerializer
|
||||
#发送失败,重试次数
|
||||
retries: 3
|
||||
# acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
|
||||
# acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
|
||||
# acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
|
||||
acks: 1
|
||||
properties:
|
||||
linger:
|
||||
ms: 1
|
||||
batch-size: 16384
|
||||
buffer-memory: 33554432
|
||||
consumer:
|
||||
bootstrap-servers: 10.95.3.204:9092,10.95.3.205:9092,10.95.3.206:9092
|
||||
auto-offset-reset: earliest
|
||||
group-id: ${spring.application.name}
|
||||
enable-auto-commit: true
|
||||
auto-commit-interval: 1000
|
||||
# 以下序列方式请根据项目实际情况进行配置,如果和下面相同则项目中可以不用配置
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer
|
||||
listener:
|
||||
ack-mode:
|
||||
|
||||
cache:
|
||||
type: redis
|
||||
data:
|
||||
cassandra:
|
||||
keyspace-name: tsp_eu
|
||||
contact-points:
|
||||
- 10.95.3.201
|
||||
- 10.95.3.202
|
||||
- 10.95.3.203
|
||||
port: 9042
|
||||
username: tsp_eu_rw
|
||||
password: awXKeq6cJIytuS4y
|
||||
session-name: Test Cluster
|
||||
local-datacenter: dc1 #默认的数据中心
|
||||
request:
|
||||
timeout: 30s
|
||||
|
||||
|
||||
mybatis-plus:
|
||||
global-config:
|
||||
sql-parser-cache: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
remote-control-log-kafka-topic: tsp-remote-control-flow-data-eu
|
||||
|
||||
threadPool:
|
||||
main:
|
||||
corePoolSize: 10
|
||||
maximumPoolSize: 20
|
||||
queueCapacity: 50
|
||||
rejectedPolicy: 1
|
||||
411
hang/0630.py
Normal file
411
hang/0630.py
Normal 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/b9c810bd-a9b6-4b5a-8ff9-943b93cadaee"
|
||||
|
||||
# ====================== 日志配置 ======================
|
||||
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)
|
||||
411
hang/Rewrite/active_number_daily.py
Normal file
411
hang/Rewrite/active_number_daily.py
Normal 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)
|
||||
362
hang/Rewrite/active_vin_check.py
Normal file
362
hang/Rewrite/active_vin_check.py
Normal 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)
|
||||
322
hang/Rewrite/add_vehicle_type.py
Normal file
322
hang/Rewrite/add_vehicle_type.py
Normal 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)
|
||||
347
hang/Rewrite/car_active_seek_UK.py
Normal file
347
hang/Rewrite/car_active_seek_UK.py
Normal 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")
|
||||
387
hang/Rewrite/car_active_seek_by_area.py
Normal file
387
hang/Rewrite/car_active_seek_by_area.py
Normal 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']}")
|
||||
54
hang/active_time_analysis.py
Normal file
54
hang/active_time_analysis.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
# 配置参数
|
||||
CONFIG = {
|
||||
'input_csv': r'C:\Users\Administrator\Desktop\European Local O&M\all-active-0522.csv.csv',
|
||||
'output_dir': './output',
|
||||
'required_columns': ['car_vins', 'active_status', 'MATERIAL', 'Area','active_date']
|
||||
}
|
||||
df=pd.read_csv(CONFIG['input_csv'])
|
||||
df['active_date'] = pd.to_datetime(df['active_date'], errors='coerce')
|
||||
active_df = df[df['active_status'] == 'active']
|
||||
daily_count = active_df.groupby(active_df['active_date'].dt.date).size().reset_index(name='active_count')
|
||||
print(daily_count)
|
||||
daily_count['active_date'] = pd.to_datetime(daily_count['active_date'])
|
||||
daily_count.set_index('active_date', inplace=True)
|
||||
weekly_count = daily_count.resample('W-MON').sum().reset_index() # 每周从周一开始
|
||||
|
||||
weekly_count.columns = ['week_start', 'weekly_active_count']
|
||||
weekly_count['week_number'] = pd.to_datetime(weekly_count['week_start']).dt.isocalendar().week
|
||||
weekly_count['year'] = pd.to_datetime(weekly_count['week_start']).dt.isocalendar().year
|
||||
# 转换为 datetime 类型
|
||||
weekly_count['week_start'] = pd.to_datetime(weekly_count['week_start'])
|
||||
|
||||
# 提取年份后两位、月份
|
||||
weekly_count['year'] = weekly_count['week_start'].dt.year % 100
|
||||
weekly_count['month'] = weekly_count['week_start'].dt.month
|
||||
|
||||
# 分组内生成「每月第几周」编号(用 cumcount)
|
||||
weekly_count['week_in_month'] = weekly_count.groupby(['year', 'month']).cumcount() + 1
|
||||
|
||||
# 拼接成格式:25-1-4
|
||||
weekly_count['year_month_week'] = (
|
||||
weekly_count['year'].astype(str) + '-' +
|
||||
weekly_count['month'].astype(str) + '-' +
|
||||
weekly_count['week_in_month'].astype(str)
|
||||
)
|
||||
|
||||
print(weekly_count)
|
||||
|
||||
# 获取输出目录路径
|
||||
output_dir = CONFIG['output_dir']
|
||||
os.makedirs(output_dir, exist_ok=True) # 自动创建目录(如果不存在)
|
||||
|
||||
# 拼接 Excel 文件路径
|
||||
output_path = os.path.join(output_dir, 'active_car_number.xlsx')
|
||||
|
||||
# 导出
|
||||
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
|
||||
daily_count.reset_index().to_excel(writer, sheet_name='Daily', index=False)
|
||||
weekly_count.to_excel(writer, sheet_name='Weekly', index=False)
|
||||
|
||||
print(f"已成功导出至:{output_path}")
|
||||
97
hang/chery_active_car.py
Normal file
97
hang/chery_active_car.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
# 配置参数
|
||||
CONFIG = {
|
||||
'input_csv': r'C:\Users\Administrator\Desktop\European Local O&M\all-active-0519.csv.csv',
|
||||
'output_dir': './reports',
|
||||
'required_columns': ['car_vins', 'active_status', 'MATERIAL', 'Area']
|
||||
}
|
||||
|
||||
|
||||
def validate_data(df):
|
||||
"""校验数据完整性"""
|
||||
missing_cols = [col for col in CONFIG['required_columns'] if col not in df.columns]
|
||||
if missing_cols:
|
||||
raise ValueError(f"CSV文件中缺少必要字段: {','.join(missing_cols)}")
|
||||
|
||||
|
||||
def generate_report():
|
||||
now = datetime.now()
|
||||
today_str = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
filename_date = now.strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
print(f"\n📅 当前时间:{today_str}")
|
||||
print("正在读取原始数据...")
|
||||
df = pd.read_csv(CONFIG['input_csv'], encoding='utf-8-sig')
|
||||
|
||||
print("校验数据结构...")
|
||||
validate_data(df)
|
||||
|
||||
# 拆分激活与未激活
|
||||
active_df = df[df['active_status'] == 'active'].copy()
|
||||
inactive_df = df[df['active_status'] != 'active'].copy()
|
||||
|
||||
print(f"✅ 已激活车辆总数:{len(active_df)}")
|
||||
print(f"❗ 未激活车辆总数:{len(inactive_df)}")
|
||||
|
||||
if inactive_df.empty:
|
||||
print("⚠️ 所有车辆均为激活状态,无需生成未激活清单")
|
||||
else:
|
||||
# 处理未激活车辆数据
|
||||
inactive_df.insert(0, 'ReportDate', filename_date)
|
||||
inactive_df.fillna({'Area': '未分配', 'MATERIAL': '未知物料'}, inplace=True)
|
||||
|
||||
# 保存未激活车辆清单
|
||||
os.makedirs(CONFIG['output_dir'], exist_ok=True)
|
||||
output_path = f"{CONFIG['output_dir']}/Inactive_Vehicles_{filename_date}.xlsx"
|
||||
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
|
||||
inactive_df.to_excel(writer, index=False, sheet_name='InactiveVehicles')
|
||||
print(f"\n📁 未激活车辆清单已生成:{os.path.abspath(output_path)}")
|
||||
|
||||
if active_df.empty:
|
||||
print("⚠️ 无激活车辆,跳过分析")
|
||||
return
|
||||
|
||||
# 分析激活车辆数据
|
||||
active_df.fillna({'Area': '未分配', 'MATERIAL': '未知物料'}, inplace=True)
|
||||
|
||||
area_summary = (
|
||||
active_df.groupby('Area')['car_vins']
|
||||
.count()
|
||||
.reset_index()
|
||||
.rename(columns={'car_vins': 'ActiveCount'})
|
||||
.sort_values(by='ActiveCount', ascending=False)
|
||||
)
|
||||
|
||||
material_summary = (
|
||||
active_df.groupby('MATERIAL')['car_vins']
|
||||
.count()
|
||||
.reset_index()
|
||||
.rename(columns={'car_vins': 'ActiveCount'})
|
||||
.sort_values(by='ActiveCount', ascending=False)
|
||||
)
|
||||
|
||||
# 控制台输出分析结果
|
||||
print("\n📊 区域分布统计(已激活车辆):")
|
||||
print(area_summary.to_string(index=False))
|
||||
|
||||
print("\n📦 销量前10的物料号(已激活车辆):")
|
||||
top10_materials = material_summary.head(10)
|
||||
print(top10_materials.to_string(index=False))
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"❌ 错误:未找到输入文件 {CONFIG['input_csv']}")
|
||||
except Exception as e:
|
||||
print(f"❌ 发生异常:{str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n===== 奇瑞车辆日报生成器 =====")
|
||||
input("请将最新csv放入daily_data文件夹后按回车键开始...")
|
||||
|
||||
generate_report()
|
||||
|
||||
input("\n处理完成!按任意键退出...")
|
||||
183
hang/daily-active-check-eu-oj.py
Normal file
183
hang/daily-active-check-eu-oj.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import pandas as pd
|
||||
import pymysql
|
||||
from sqlalchemy import create_engine
|
||||
import requests
|
||||
import json
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
|
||||
def get_access_token():
|
||||
"""获取飞书访问令牌"""
|
||||
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
|
||||
api_post_data = {"app_id": "cli_a8b50ec0eed1500d", "app_secret": "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"}
|
||||
response = requests.post(api_token_url, json=api_post_data)
|
||||
if response.status_code == 200 and response.json().get("code") == 0:
|
||||
return response.json()["tenant_access_token"]
|
||||
else:
|
||||
raise Exception(f"获取 Token 失败: {response.text}")
|
||||
|
||||
|
||||
def overwrite_data(spreadsheet_token, values, access_token=None):
|
||||
"""覆盖写入数据到飞书多维表格"""
|
||||
if not access_token:
|
||||
access_token = get_access_token()
|
||||
|
||||
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}
|
||||
|
||||
row_count = len(values) + 1
|
||||
range_str = f"46ea20!A1:B{row_count}"
|
||||
|
||||
post_data = {
|
||||
"valueRange": {
|
||||
"range": range_str,
|
||||
"values": values
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.put(url, json=post_data, headers=headers)
|
||||
return response # 返回响应对象以便检查状态
|
||||
|
||||
|
||||
# 数据库连接配置
|
||||
DB_CONFIG = {
|
||||
'host': 'eutsp-prod.mysql.germany.rds.aliyuncs.com',
|
||||
'port': 3306,
|
||||
'user': 'international_tsp_eu_r',
|
||||
'password': 'ZXhBgo1TB2XbF3kP',
|
||||
'database': 'chery_international_tsp_eu'
|
||||
}
|
||||
|
||||
|
||||
def fetch_data_from_db():
|
||||
"""从数据库执行SQL查询获取数据"""
|
||||
connection = pymysql.connect(**DB_CONFIG)
|
||||
try:
|
||||
sql_query = """
|
||||
SELECT
|
||||
v_vehicle_info.VIN AS car_vins,
|
||||
CASE v_sim_info.STATE
|
||||
WHEN 2 THEN 'active'
|
||||
ELSE 'no_active'
|
||||
END AS active_status,
|
||||
v_vehicle_info.MATERIAL_ID as MATERIAL,
|
||||
v_vehicle_info.SALES_TERRITORY as Area,
|
||||
v_sim_info.ACTIVATION_DATE as active_date
|
||||
FROM v_vehicle_info
|
||||
JOIN v_tbox_info USING (VIN)
|
||||
JOIN v_sim_info USING (SN);
|
||||
"""
|
||||
return pd.read_sql(sql_query, connection)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def send_feishu_message(webhook_url, message, spreadsheet_token=None):
|
||||
"""
|
||||
通过Webhook发送飞书消息,支持添加多维表格链接
|
||||
:param webhook_url: 机器人Webhook地址
|
||||
:param message: 要发送的文本内容
|
||||
:param spreadsheet_token: 多维表格ID(可选)
|
||||
"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
# 如果有提供多维表格ID,添加表格链接按钮
|
||||
if spreadsheet_token:
|
||||
# 创建多维表格链接
|
||||
spreadsheet_url = f"https://my-ichery.feishu.cn/sheets/{spreadsheet_token}"
|
||||
|
||||
# 使用交互式消息卡片格式
|
||||
payload = {
|
||||
"msg_type": "interactive",
|
||||
"card": {
|
||||
"config": {
|
||||
"wide_screen_mode": True
|
||||
},
|
||||
"elements": [{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"content": message,
|
||||
"tag": "lark_md"
|
||||
}
|
||||
}, {
|
||||
"actions": [{
|
||||
"tag": "button",
|
||||
"text": {
|
||||
"content": "查看多维表格",
|
||||
"tag": "plain_text"
|
||||
},
|
||||
"type": "primary",
|
||||
"url": spreadsheet_url
|
||||
}],
|
||||
"tag": "action"
|
||||
}],
|
||||
"header": {
|
||||
"title": {
|
||||
"content": "数据同步完成通知",
|
||||
"tag": "plain_text"
|
||||
},
|
||||
"template": "green"
|
||||
}
|
||||
}
|
||||
}
|
||||
else:
|
||||
# 普通文本消息
|
||||
payload = {
|
||||
"msg_type": "text",
|
||||
"content": {"text": message}
|
||||
}
|
||||
|
||||
response = requests.post(webhook_url, headers=headers, data=json.dumps(payload))
|
||||
return response.json()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 配置参数
|
||||
SPREADSHEET_ID = "MIqssvuB3h3XmgtkI26cBX7Angc"
|
||||
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/d82e4ada-8f78-40ae-b98e-75af0c448c95"
|
||||
|
||||
try:
|
||||
# 1. 从数据库获取数据
|
||||
df = fetch_data_from_db()
|
||||
|
||||
# 2. 处理数据
|
||||
df['active_date'] = pd.to_datetime(df['active_date'], errors='coerce')
|
||||
active_df = df[df['active_status'] == 'active']
|
||||
|
||||
# 3. 按天统计活跃数
|
||||
daily_count = active_df.groupby(active_df['active_date'].dt.date).size().reset_index(name='active_count')
|
||||
values = [[str(d), c] for d, c in daily_count.values.tolist()]
|
||||
|
||||
# 4. 覆盖写入多维表格
|
||||
response = overwrite_data(SPREADSHEET_ID, values)
|
||||
|
||||
if response.status_code == 200:
|
||||
# 5. 构建通知消息
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
stats_summary = f"• 今日活跃车辆: {daily_count['active_count'].iloc[-1] if len(daily_count) > 0 else 0}\n"
|
||||
stats_summary += f"• 本周活跃车辆: {daily_count[daily_count['active_date'] >= (datetime.now() - pd.Timedelta(days=7)).date()]['active_count'].sum()}\n"
|
||||
stats_summary += f"• 累计活跃车辆: {len(active_df)}"
|
||||
|
||||
message = f"🚗 车辆数据同步完成 ✅\n"
|
||||
message += f"⏰ 时间: {current_time}\n"
|
||||
message += f"📊 统计摘要:\n{stats_summary}\n"
|
||||
message += "[安全关键词:告警]"
|
||||
|
||||
# 6. 发送包含多维表格链接的通知
|
||||
result = send_feishu_message(WEBHOOK_URL, message, SPREADSHEET_ID)
|
||||
print("飞书消息发送成功:", result)
|
||||
else:
|
||||
error_msg = f"表格写入失败: {response.status_code} - {response.text}"
|
||||
error_message = f"❌ 数据同步失败\n{error_msg}\n[安全关键词:告警]"
|
||||
send_feishu_message(WEBHOOK_URL, error_message)
|
||||
print(error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"❌ 数据同步异常: {str(e)}\n[安全关键词:告警]"
|
||||
send_feishu_message(WEBHOOK_URL, error_message)
|
||||
print("发生异常:", str(e))
|
||||
241
hang/demo060904.py
Normal file
241
hang/demo060904.py
Normal file
@@ -0,0 +1,241 @@
|
||||
import pandas as pd
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from sqlalchemy import create_engine
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)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 = "激活状态" # 激活状态字段名称
|
||||
|
||||
|
||||
def get_access_token():
|
||||
"""获取飞书访问令牌"""
|
||||
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
|
||||
api_post_data = {"app_id": "cli_a8b50ec0eed1500d", "app_secret": "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"}
|
||||
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_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_vehicle_info.VIN AS car_vins,
|
||||
CASE v_sim_info.STATE
|
||||
WHEN 2 THEN 'active'
|
||||
ELSE 'no_active'
|
||||
END AS active_status
|
||||
FROM v_vehicle_info
|
||||
JOIN v_tbox_info USING (VIN)
|
||||
JOIN v_sim_info USING (SN);
|
||||
"""
|
||||
|
||||
# 执行查询并转换为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)}条多维表格记录")
|
||||
|
||||
# 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}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"处理记录异常: {record} - {str(e)}")
|
||||
|
||||
logging.info(f"需要更新{len(records_to_update)}条记录")
|
||||
|
||||
# 3. 批量更新记录 (每批次50条)
|
||||
if not records_to_update:
|
||||
logging.info("没有需要更新的记录")
|
||||
return
|
||||
|
||||
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 success_count
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
start_time = time.time()
|
||||
logging.info("开始车辆激活状态同步流程...")
|
||||
|
||||
# 1. 从数据库获取激活状态数据
|
||||
activation_data = fetch_activation_data()
|
||||
logging.info(f"从数据库获取{len(activation_data)}条激活状态记录")
|
||||
|
||||
# 2. 与飞书多维表格数据比对并更新
|
||||
update_count = compare_and_update_records(FEISHU_APP_TOKEN, FEISHU_TABLE_ID, activation_data)
|
||||
|
||||
# 3. 完成日志
|
||||
elapsed_time = time.time() - start_time
|
||||
logging.info(f"流程完成! 耗时: {elapsed_time:.2f}秒, 更新记录数: {update_count}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"流程执行异常: {str(e)}")
|
||||
logging.exception("详细错误信息")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
207
hang/demo06092.py
Normal file
207
hang/demo06092.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import pandas as pd
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
from sqlalchemy import create_engine
|
||||
from urllib.parse import quote_plus
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
|
||||
def get_access_token():
|
||||
"""获取飞书访问令牌[1,7](@ref)"""
|
||||
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
|
||||
api_post_data = {"app_id": "cli_a8b50ec0eed1500d", "app_secret": "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"}
|
||||
try:
|
||||
response = requests.post(api_token_url, json=api_post_data, timeout=10)
|
||||
response.raise_for_status()
|
||||
if response.json().get("code") == 0:
|
||||
return response.json()["tenant_access_token"]
|
||||
else:
|
||||
raise Exception(f"获取Token失败: {response.text}")
|
||||
except Exception as e:
|
||||
logging.error(f"获取飞书Token异常: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def overwrite_data(spreadsheet_token, values, access_token=None):
|
||||
"""覆盖写入数据到飞书多维表格[1,6](@ref)"""
|
||||
if not access_token:
|
||||
access_token = get_access_token()
|
||||
|
||||
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}
|
||||
|
||||
row_count = len(values) + 1
|
||||
range_str = f"46ea20!A1:B{row_count}"
|
||||
|
||||
post_data = {
|
||||
"valueRange": {
|
||||
"range": range_str,
|
||||
"values": values
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.put(url, json=post_data, headers=headers, timeout=15)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except Exception as e:
|
||||
logging.error(f"写入飞书表格失败: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
# 数据库连接配置
|
||||
DB_CONFIG = {
|
||||
'host': 'eutsp-prod.mysql.germany.rds.aliyuncs.com',
|
||||
'port': 3306,
|
||||
'user': 'international_tsp_eu_r',
|
||||
'password': 'ZXhBgo1TB2XbF3kP',
|
||||
'database': 'chery_international_tsp_eu'
|
||||
}
|
||||
|
||||
# 创建SQLAlchemy引擎(解决pandas警告问题)
|
||||
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,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10
|
||||
)
|
||||
|
||||
|
||||
def fetch_data_from_db():
|
||||
"""从数据库执行SQL查询获取数据(使用SQLAlchemy引擎)"""
|
||||
try:
|
||||
sql_query = """
|
||||
SELECT
|
||||
v_vehicle_info.VIN AS car_vins,
|
||||
CASE v_sim_info.STATE
|
||||
WHEN 2 THEN 'active'
|
||||
ELSE 'no_active'
|
||||
END AS active_status,
|
||||
v_vehicle_info.MATERIAL_ID as MATERIAL,
|
||||
v_vehicle_info.SALES_TERRITORY as Area,
|
||||
v_sim_info.ACTIVATION_DATE as active_date
|
||||
FROM v_vehicle_info
|
||||
JOIN v_tbox_info USING (VIN)
|
||||
JOIN v_sim_info USING (SN);
|
||||
"""
|
||||
return pd.read_sql(sql_query, engine)
|
||||
except Exception as e:
|
||||
logging.error(f"数据库查询失败: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def send_feishu_message(webhook_url, message, spreadsheet_token=None, retry=2):
|
||||
"""
|
||||
通过Webhook发送飞书消息,支持添加多维表格链接[2,8](@ref)
|
||||
:param webhook_url: 机器人Webhook地址
|
||||
:param message: 要发送的文本内容
|
||||
:param spreadsheet_token: 多维表格ID(可选)
|
||||
:param retry: 重试次数
|
||||
"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
# 如果有提供多维表格ID,添加表格链接按钮
|
||||
if spreadsheet_token:
|
||||
spreadsheet_url = f"https://my-ichery.feishu.cn/sheets/{spreadsheet_token}"
|
||||
payload = {
|
||||
"msg_type": "interactive",
|
||||
"card": {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"elements": [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {"content": message, "tag": "lark_md"}
|
||||
},
|
||||
{
|
||||
"actions": [{
|
||||
"tag": "button",
|
||||
"text": {"content": "查看多维表格", "tag": "plain_text"},
|
||||
"type": "primary",
|
||||
"url": spreadsheet_url
|
||||
}],
|
||||
"tag": "action"
|
||||
}
|
||||
],
|
||||
"header": {
|
||||
"title": {"content": "数据同步完成通知", "tag": "plain_text"},
|
||||
"template": "green"
|
||||
}
|
||||
}
|
||||
}
|
||||
else:
|
||||
payload = {"msg_type": "text", "content": {"text": message}}
|
||||
|
||||
for attempt in range(retry):
|
||||
try:
|
||||
response = requests.post(webhook_url, headers=headers, data=json.dumps(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__':
|
||||
# 配置参数
|
||||
SPREADSHEET_ID = "MIqssvuB3h3XmgtkI26cBX7Angc"
|
||||
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/d82e4ada-8f78-40ae-b98e-75af0c448c95"
|
||||
|
||||
try:
|
||||
logging.info("开始数据同步流程...")
|
||||
|
||||
# 1. 从数据库获取数据
|
||||
df = fetch_data_from_db()
|
||||
logging.info(f"成功获取{len(df)}条车辆数据")
|
||||
|
||||
# 2. 处理数据
|
||||
df['active_date'] = pd.to_datetime(df['active_date'], errors='coerce')
|
||||
active_df = df[df['active_status'] == 'active']
|
||||
logging.info(f"有效激活数据: {len(active_df)}条")
|
||||
|
||||
# 3. 按天统计激活数
|
||||
daily_count = active_df.groupby(active_df['active_date'].dt.date).size().reset_index(name='active_count')
|
||||
values = [[str(d), str(c)] for d, c in daily_count.values.tolist()] # 确保值为字符串类型
|
||||
logging.info(f"生成{len(values)}条日期统计记录")
|
||||
|
||||
# 4. 覆盖写入多维表格
|
||||
response = overwrite_data(SPREADSHEET_ID, values)
|
||||
if response.status_code == 200:
|
||||
logging.info("数据成功写入飞书多维表格")
|
||||
|
||||
# 5. 构建通知消息
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
today_count = daily_count[daily_count['active_date'] == datetime.now().date()]
|
||||
weekly_count = daily_count[daily_count['active_date'] >= (datetime.now() - pd.Timedelta(days=7)).date()]
|
||||
|
||||
stats_summary = f"• 今日激活车辆: {today_count['active_count'].values[0] if not today_count.empty else 0}\n"
|
||||
stats_summary += f"• 本周激活车辆: {weekly_count['active_count'].sum()}\n"
|
||||
stats_summary += f"• 累计激活车辆: {len(active_df)}"
|
||||
|
||||
message = f"🚗 车辆数据同步完成 ✅\n"
|
||||
message += f"⏰ 时间: {current_time}\n"
|
||||
message += f"📊 统计摘要:\n{stats_summary}\n"
|
||||
|
||||
# 6. 发送包含多维表格链接的通知
|
||||
result = send_feishu_message(WEBHOOK_URL, message, SPREADSHEET_ID)
|
||||
logging.info(f"飞书消息发送成功: {result}")
|
||||
else:
|
||||
error_msg = f"表格写入失败: {response.status_code} - {response.text}"
|
||||
logging.error(error_msg)
|
||||
error_message = f"❌ 数据同步失败\n{error_msg}"
|
||||
send_feishu_message(WEBHOOK_URL, error_message)
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"❌ 数据同步异常: {str(e)}"
|
||||
logging.exception("程序执行异常")
|
||||
send_feishu_message(WEBHOOK_URL, error_message)
|
||||
336
hang/demo06093.py
Normal file
336
hang/demo06093.py
Normal file
@@ -0,0 +1,336 @@
|
||||
import pandas as pd
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from sqlalchemy import create_engine
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
|
||||
def get_access_token():
|
||||
"""获取飞书访问令牌"""
|
||||
api_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
|
||||
api_post_data = {"app_id": "cli_a8b50ec0eed1500d", "app_secret": "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"}
|
||||
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(spreadsheet_token, sheet_id, retry=3):
|
||||
"""从飞书电子表格读取日统计数据"""
|
||||
access_token = get_access_token()
|
||||
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values/{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 write_weekly_data(base_token, table_id, records, retry=3):
|
||||
"""写入数据到飞书多维表格(周统计)"""
|
||||
access_token = get_access_token()
|
||||
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{base_token}/tables/{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,8](@ref) ===
|
||||
# 注意:需要将date对象转换为datetime对象(设置时间为00:00:00)
|
||||
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, # 时间戳格式[3,8](@ref)
|
||||
"周结束日期": end_timestamp, # 时间戳格式[3,8](@ref)
|
||||
"激活数量": int(row['active_count'])
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logging.error(f"记录格式错误: {row} - {str(e)}")
|
||||
|
||||
# 处理空记录情况
|
||||
if not payload["records"]:
|
||||
logging.warning("没有有效记录可写入多维表格")
|
||||
return None
|
||||
|
||||
# 分批写入(每次最多100条)
|
||||
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,8](@ref)
|
||||
if error_code == "DatetimeFieldConvFail":
|
||||
logging.error(f"日期字段转换失败: 请检查多维表格中日期字段类型配置[3,8](@ref)")
|
||||
# 记录导致错误的字段值
|
||||
logging.error(f"问题记录示例: 周开始日期={start_timestamp}, 周结束日期={end_timestamp}")
|
||||
elif error_code == "TextFieldConvFail":
|
||||
logging.error(f"文本字段转换失败: 请检查数字字段是否包含非数字字符")
|
||||
elif error_code == "FieldNameNotFound":
|
||||
logging.error(f"字段未找到: 请检查字段名称是否与多维表格一致")
|
||||
|
||||
# 添加详细错误日志
|
||||
if 'data' in resp_data and 'errors' in resp_data['data']:
|
||||
logging.error(f"详细错误信息: {resp_data['data']['errors']}")
|
||||
|
||||
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年份和周数
|
||||
iso_year, iso_week, iso_day = date.isocalendar()
|
||||
|
||||
# 处理跨年情况
|
||||
if iso_week == 1 and date.month == 12:
|
||||
return f"{iso_year}-W{iso_week:02d}"
|
||||
elif iso_week >= 52 and date.month == 1:
|
||||
return f"{date.year - 1}-W{iso_week:02d}"
|
||||
else:
|
||||
return f"{date.year}-W{iso_week:02d}"
|
||||
|
||||
|
||||
def calculate_week_range(date):
|
||||
"""计算所在周的周一和周日日期(ISO标准)"""
|
||||
# 计算周一 (0=Monday, 6=Sunday)
|
||||
start_date = date - timedelta(days=date.weekday())
|
||||
end_date = start_date + timedelta(days=6)
|
||||
return start_date, end_date
|
||||
|
||||
|
||||
def send_feishu_message(webhook_url, message, daily_url=None, weekly_url=None, retry=2):
|
||||
"""发送飞书通知"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
# 构建交互式卡片
|
||||
elements = [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {"content": message, "tag": "lark_md"}
|
||||
}
|
||||
]
|
||||
|
||||
actions = []
|
||||
|
||||
# 添加日统计表格按钮
|
||||
if daily_url:
|
||||
actions.append({
|
||||
"tag": "button",
|
||||
"text": {"content": "查看日统计", "tag": "plain_text"},
|
||||
"type": "default",
|
||||
"url": daily_url
|
||||
})
|
||||
|
||||
# 添加周统计表格按钮
|
||||
if weekly_url:
|
||||
actions.append({
|
||||
"tag": "button",
|
||||
"text": {"content": "查看周统计", "tag": "plain_text"},
|
||||
"type": "primary",
|
||||
"url": weekly_url
|
||||
})
|
||||
|
||||
if actions:
|
||||
elements.append({
|
||||
"tag": "action",
|
||||
"actions": actions
|
||||
})
|
||||
|
||||
payload = {
|
||||
"msg_type": "interactive",
|
||||
"card": {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"elements": elements,
|
||||
"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__':
|
||||
# 配置参数
|
||||
DAILY_SPREADSHEET_ID = "MIqssvuB3h3XmgtkI26cBX7Angc" # 日统计电子表格ID
|
||||
DAILY_SHEET_ID = "46ea20" # 日统计工作表ID
|
||||
WEEKLY_BASE_TOKEN = "T3QLbmpqma014ussjy9cgitqnSh" # 周统计多维表格ID
|
||||
WEEKLY_TABLE_ID = "tbloB11YuVOSdi9e" # 周统计表ID
|
||||
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/d82e4ada-8f78-40ae-b98e-75af0c448c95"
|
||||
|
||||
# 表格链接
|
||||
DAILY_URL = "https://my-ichery.feishu.cn/sheets/MIqssvuB3h3XmgtkI26cBX7Angc?sheet=46ea20&table=tblKYOLYYl1CFwn9&view=vews5Ash0A"
|
||||
WEEKLY_URL = "https://my-ichery.feishu.cn/base/T3QLbmpqma014ussjy9cgitqnSh?table=tbloB11YuVOSdi9e&view=vewFl6lbpB"
|
||||
|
||||
try:
|
||||
logging.info("开始周统计计算流程...")
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# 1. 从飞书日统计表读取数据
|
||||
daily_data = read_daily_data(DAILY_SPREADSHEET_ID, DAILY_SHEET_ID)
|
||||
logging.info(f"成功读取{len(daily_data)}条日统计记录")
|
||||
|
||||
# 2. 计算周统计数据(处理跨年)
|
||||
weekly_stats = {}
|
||||
for entry in daily_data:
|
||||
date = entry['date']
|
||||
count = entry['count']
|
||||
|
||||
# 计算ISO周数(处理跨年)
|
||||
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. 准备写入数据
|
||||
weekly_values = []
|
||||
for week in weekly_list:
|
||||
weekly_values.append({
|
||||
'week_number': week['week_number'],
|
||||
'week_start': week['week_start'],
|
||||
'week_end': week['week_end'],
|
||||
'active_count': week['active_count']
|
||||
})
|
||||
|
||||
# 4. 写入周统计表(分批处理)
|
||||
success_count = write_weekly_data(WEEKLY_BASE_TOKEN, WEEKLY_TABLE_ID, weekly_values)
|
||||
if success_count != len(weekly_values):
|
||||
raise Exception(f"周统计写入失败: 成功写入{success_count}条,总记录{len(weekly_values)}条")
|
||||
logging.info(f"成功写入{success_count}条周统计记录到飞书多维表格")
|
||||
|
||||
# 5. 构建通知消息
|
||||
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)}条周记录"
|
||||
|
||||
# 6. 发送通知
|
||||
result = send_feishu_message(
|
||||
WEBHOOK_URL,
|
||||
message,
|
||||
daily_url=DAILY_URL,
|
||||
weekly_url=WEEKLY_URL
|
||||
)
|
||||
logging.info(f"飞书消息发送成功: {result}")
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"❌ 周统计计算异常: {str(e)}"
|
||||
logging.exception("程序执行异常")
|
||||
send_feishu_message(WEBHOOK_URL, error_message)
|
||||
213
hang/demo0610.py
Normal file
213
hang/demo0610.py
Normal file
@@ -0,0 +1,213 @@
|
||||
import pandas as pd
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
from sqlalchemy import create_engine
|
||||
from urllib.parse import quote_plus
|
||||
import logging
|
||||
import time # 新增用于消息重试延迟
|
||||
|
||||
# ================== 飞书配置 ==================
|
||||
# 飞书应用凭证
|
||||
APP_ID = "cli_a8b50ec0eed1500d"
|
||||
APP_SECRET = "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK"
|
||||
|
||||
# 飞书多维表格配置
|
||||
SPREADSHEET_ID = "MIqssvuB3h3XmgtkI26cBX7Angc" # 表格ID
|
||||
SHEET_NAME = "46ea20" # 工作表名称
|
||||
|
||||
# 飞书机器人Webhook
|
||||
WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/3bc45247-c469-47a0-bfe9-c62e46c5aca0"
|
||||
|
||||
# ================== 数据库配置 ==================
|
||||
DB_CONFIG = {
|
||||
'host': 'eutsp-prod.mysql.germany.rds.aliyuncs.com',
|
||||
'port': 3306,
|
||||
'user': 'international_tsp_eu_r',
|
||||
'password': 'ZXhBgo1TB2XbF3kP',
|
||||
'database': 'chery_international_tsp_eu'
|
||||
}
|
||||
|
||||
# ================== 其他配置 ==================
|
||||
# 创建SQLAlchemy引擎(解决pandas警告问题)
|
||||
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,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10
|
||||
)
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# ================== 功能函数 ==================
|
||||
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()
|
||||
if response.json().get("code") == 0:
|
||||
return response.json()["tenant_access_token"]
|
||||
else:
|
||||
raise Exception(f"获取Token失败: {response.text}")
|
||||
except Exception as e:
|
||||
logging.error(f"获取飞书Token异常: {str(e)}")
|
||||
raise
|
||||
|
||||
def overwrite_data(spreadsheet_token, values, access_token=None):
|
||||
"""覆盖写入数据到飞书多维表格"""
|
||||
if not access_token:
|
||||
access_token = get_access_token()
|
||||
|
||||
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}
|
||||
|
||||
row_count = len(values) + 1
|
||||
range_str = f"{SHEET_NAME}!A1:B{row_count}" # 使用配置的工作表名称
|
||||
|
||||
post_data = {
|
||||
"valueRange": {
|
||||
"range": range_str,
|
||||
"values": values
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.put(url, json=post_data, headers=headers, timeout=15)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except Exception as e:
|
||||
logging.error(f"写入飞书表格失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def fetch_data_from_db():
|
||||
"""从数据库执行SQL查询获取数据"""
|
||||
try:
|
||||
sql_query = """
|
||||
SELECT
|
||||
v_vehicle_info.VIN AS car_vins,
|
||||
CASE v_sim_info.STATE
|
||||
WHEN 2 THEN 'active'
|
||||
ELSE 'no_active'
|
||||
END AS active_status,
|
||||
v_vehicle_info.MATERIAL_ID as MATERIAL,
|
||||
v_vehicle_info.SALES_TERRITORY as Area,
|
||||
v_sim_info.ACTIVATION_DATE as active_date
|
||||
FROM v_vehicle_info
|
||||
JOIN v_tbox_info USING (VIN)
|
||||
JOIN v_sim_info USING (SN);
|
||||
"""
|
||||
return pd.read_sql(sql_query, engine)
|
||||
except Exception as e:
|
||||
logging.error(f"数据库查询失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def send_feishu_message(webhook_url, message, spreadsheet_token=None, retry=2):
|
||||
"""
|
||||
通过Webhook发送飞书消息,支持添加多维表格链接
|
||||
:param webhook_url: 机器人Webhook地址
|
||||
:param message: 要发送的文本内容
|
||||
:param spreadsheet_token: 多维表格ID(可选)
|
||||
:param retry: 重试次数
|
||||
"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
# 如果有提供多维表格ID,添加表格链接按钮
|
||||
if spreadsheet_token:
|
||||
spreadsheet_url = f"https://my-ichery.feishu.cn/sheets/{spreadsheet_token}"
|
||||
payload = {
|
||||
"msg_type": "interactive",
|
||||
"card": {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"elements": [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {"content": message, "tag": "lark_md"}
|
||||
},
|
||||
{
|
||||
"actions": [{
|
||||
"tag": "button",
|
||||
"text": {"content": "查看多维表格", "tag": "plain_text"},
|
||||
"type": "primary",
|
||||
"url": spreadsheet_url
|
||||
}],
|
||||
"tag": "action"
|
||||
}
|
||||
],
|
||||
"header": {
|
||||
"title": {"content": "数据同步完成通知", "tag": "plain_text"},
|
||||
"template": "green"
|
||||
}
|
||||
}
|
||||
}
|
||||
else:
|
||||
payload = {"msg_type": "text", "content": {"text": message}}
|
||||
|
||||
for attempt in range(retry):
|
||||
try:
|
||||
response = requests.post(webhook_url, headers=headers, data=json.dumps(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("开始数据同步流程...")
|
||||
|
||||
# 1. 从数据库获取数据
|
||||
df = fetch_data_from_db()
|
||||
logging.info(f"成功获取{len(df)}条车辆数据")
|
||||
|
||||
# 2. 处理数据
|
||||
df['active_date'] = pd.to_datetime(df['active_date'], errors='coerce')
|
||||
active_df = df[df['active_status'] == 'active']
|
||||
logging.info(f"有效激活数据: {len(active_df)}条")
|
||||
|
||||
# 3. 按天统计激活数
|
||||
daily_count = active_df.groupby(active_df['active_date'].dt.date).size().reset_index(name='active_count')
|
||||
values = [[str(d), str(c)] for d, c in daily_count.values.tolist()] # 确保值为字符串类型
|
||||
logging.info(f"生成{len(values)}条日期统计记录")
|
||||
|
||||
# 4. 覆盖写入多维表格
|
||||
response = overwrite_data(SPREADSHEET_ID, values)
|
||||
if response.status_code == 200:
|
||||
logging.info("数据成功写入飞书多维表格")
|
||||
|
||||
# 5. 构建通知消息
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
today_count = daily_count[daily_count['active_date'] == datetime.now().date()]
|
||||
weekly_count = daily_count[daily_count['active_date'] >= (datetime.now() - pd.Timedelta(days=7)).date()]
|
||||
|
||||
stats_summary = f"• 今日激活车辆: {today_count['active_count'].values[0] if not today_count.empty else 0}\n"
|
||||
stats_summary += f"• 本周激活车辆: {weekly_count['active_count'].sum()}\n"
|
||||
stats_summary += f"• 累计激活车辆: {len(active_df)}"
|
||||
|
||||
message = f"🚗 车辆数据同步完成 ✅\n"
|
||||
message += f"⏰ 时间: {current_time}\n"
|
||||
message += f"📊 统计摘要:\n{stats_summary}\n"
|
||||
|
||||
# 6. 发送包含多维表格链接的通知
|
||||
result = send_feishu_message(WEBHOOK_URL, message, SPREADSHEET_ID)
|
||||
logging.info(f"飞书消息发送成功: {result}")
|
||||
else:
|
||||
error_msg = f"表格写入失败: {response.status_code} - {response.text}"
|
||||
logging.error(error_msg)
|
||||
error_message = f"❌ 数据同步失败\n{error_msg}"
|
||||
send_feishu_message(WEBHOOK_URL, error_message)
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"❌ 数据同步异常: {str(e)}"
|
||||
logging.exception("程序执行异常")
|
||||
send_feishu_message(WEBHOOK_URL, error_message)
|
||||
411
hang/demo061002.py
Normal file
411
hang/demo061002.py
Normal 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)
|
||||
358
hang/demo061003.py
Normal file
358
hang/demo061003.py
Normal file
@@ -0,0 +1,358 @@
|
||||
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 = "激活状态" # 激活状态字段名称
|
||||
|
||||
# 飞书应用凭证
|
||||
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_vehicle_info.VIN AS car_vins,
|
||||
CASE v_sim_info.STATE
|
||||
WHEN 2 THEN 'active'
|
||||
ELSE 'no_active'
|
||||
END AS active_status
|
||||
FROM v_vehicle_info
|
||||
JOIN v_tbox_info USING (VIN)
|
||||
JOIN v_sim_info USING (SN);
|
||||
"""
|
||||
|
||||
# 执行查询并转换为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)
|
||||
91
hang/demo2.py
Normal file
91
hang/demo2.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
import pymysql
|
||||
from sqlalchemy import create_engine
|
||||
import requests
|
||||
import json
|
||||
|
||||
|
||||
# 1. 获取当前系统时间[1,3,6](@ref)
|
||||
def get_current_time():
|
||||
"""获取格式化的当前系统时间"""
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
# 2. 数据库查询(核心函数)
|
||||
def fetch_activation_data():
|
||||
"""获取激活车辆数据"""
|
||||
connection_uri = f"mysql+pymysql://{DB_CONFIG['user']}:{DB_CONFIG['password']}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}"
|
||||
engine = create_engine(connection_uri)
|
||||
|
||||
sql_query = """
|
||||
SELECT
|
||||
v_sim_info.ACTIVATION_DATE as active_date
|
||||
FROM v_vehicle_info
|
||||
JOIN v_tbox_info USING (VIN)
|
||||
JOIN v_sim_info USING (SN)
|
||||
WHERE v_sim_info.STATE = 2; -- 只查询激活状态车辆
|
||||
"""
|
||||
return pd.read_sql(sql_query, engine)
|
||||
|
||||
|
||||
# 3. 统计计算
|
||||
def calculate_stats(df):
|
||||
"""计算多维统计指标"""
|
||||
# 基础数据处理[1](@ref)
|
||||
df['active_date'] = pd.to_datetime(df['active_date']).dt.date
|
||||
|
||||
# 本周统计(06-02至06-05)
|
||||
start_week = datetime(2025, 6, 2).date()
|
||||
end_week = datetime(2025, 6, 5).date()
|
||||
weekly_data = df[(df['active_date'] >= start_week) & (df['active_date'] <= end_week)]
|
||||
|
||||
# 近三月统计(03-05至06-05)
|
||||
start_3month = (datetime.now() - timedelta(days=90)).date()
|
||||
monthly_data = df[df['active_date'] >= start_3month]
|
||||
|
||||
return {
|
||||
"本周激活总数": len(weekly_data),
|
||||
"本周每日激活": weekly_data.groupby('active_date').size().to_dict(),
|
||||
"近三月激活总数": len(monthly_data),
|
||||
"累计激活总数": len(df)
|
||||
}
|
||||
|
||||
|
||||
# 4. 飞书机器人消息发送
|
||||
def send_feishu_report(stats):
|
||||
"""发送统计报告"""
|
||||
message = f"🚗 **车辆激活统计报告**({get_current_time()})\n\n"
|
||||
message += f"🔹 **本周激活总数**: {stats['本周激活总数']}辆\n"
|
||||
message += "🔹 **本周每日激活**:\n"
|
||||
for date, count in stats['本周每日激活'].items():
|
||||
message += f" ▸ {date}: {count}辆\n"
|
||||
message += f"\n🔹 **近三月激活总数**: {stats['近三月激活总数']}辆\n"
|
||||
message += f"🔹 **累计激活总数**: {stats['累计激活总数']}辆"
|
||||
|
||||
requests.post(FEISHU_WEBHOOK_URL, json={
|
||||
"msg_type": "text",
|
||||
"content": {"text": message}
|
||||
})
|
||||
|
||||
|
||||
# 主流程
|
||||
if __name__ == "__main__":
|
||||
# 配置参数
|
||||
FEISHU_WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/ff3726f6-d4e3-455b-ae65-bca471305733"
|
||||
# 数据库连接
|
||||
DB_CONFIG = {
|
||||
'host': 'eutsp-prod.mysql.germany.rds.aliyuncs.com',
|
||||
'port': 3306, # MySQL默认端口
|
||||
'user': 'international_tsp_eu_r',
|
||||
'password': 'ZXhBgo1TB2XbF3kP',
|
||||
'database': 'chery_international_tsp_eu'
|
||||
}
|
||||
|
||||
# 执行统计
|
||||
df = fetch_activation_data()
|
||||
stats = calculate_stats(df)
|
||||
|
||||
# 发送报告
|
||||
send_feishu_report(stats)
|
||||
print("✅ 统计报告已发送至飞书")
|
||||
74
hang/italy-seek.py
Normal file
74
hang/italy-seek.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
import csv
|
||||
|
||||
# 设置路径参数
|
||||
base_dir = r'C:\Users\Administrator\Desktop\European Local O&M'
|
||||
result_dir = os.path.join(base_dir, 'italy_result')
|
||||
os.makedirs(result_dir, exist_ok=True) # 自动创建目录
|
||||
|
||||
# 获取日期
|
||||
current_date = datetime.now().strftime("%m%d")
|
||||
|
||||
# 加载激活状态数据
|
||||
active_file = os.path.join(base_dir, f'all-active-{current_date}.csv.csv') # 确保文件名正确
|
||||
active_status = {}
|
||||
|
||||
with open(active_file, encoding='utf-8') as f:
|
||||
csv_reader = csv.reader(f)
|
||||
next(csv_reader)
|
||||
for row in csv_reader:
|
||||
vin = row[0].strip().upper()
|
||||
status = row[1].strip().lower()
|
||||
active_status[vin] = 'active' if status == 'active' else 'NO'
|
||||
|
||||
# 加载需要核实的VIN列表
|
||||
vin_list = []
|
||||
with open(os.path.join(base_dir, 'eu516.csv'), 'r', encoding='utf-8') as f:
|
||||
csv_reader = csv.reader(f)
|
||||
next(csv_reader)
|
||||
for row in csv_reader:
|
||||
vin_list.append(row[0].strip().upper())
|
||||
|
||||
# 输出文件路径
|
||||
output_all = os.path.join(result_dir, f'italy-result{current_date}.csv')
|
||||
output_no = os.path.join(result_dir, f'no-italy-result{current_date}.csv')
|
||||
|
||||
# 写入意大利所有VIN的状态
|
||||
with open(output_all, 'w', newline='', encoding='utf-8') as f:
|
||||
csv_writer = csv.writer(f)
|
||||
csv_writer.writerow(['VIN', 'active-status'])
|
||||
for vin in vin_list:
|
||||
status = active_status.get(vin, 'NO')
|
||||
csv_writer.writerow([vin, status])
|
||||
|
||||
# 统计信息并生成未激活车辆清单
|
||||
activated_count = 0
|
||||
deactivated_count = 0
|
||||
|
||||
# 当前UTC时间
|
||||
current_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
with open(output_all, encoding='utf-8') as f_in, \
|
||||
open(output_no, 'w', newline='', encoding='utf-8') as f_out:
|
||||
|
||||
reader = csv.DictReader(f_in)
|
||||
writer = csv.DictWriter(f_out, fieldnames=reader.fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for row in reader:
|
||||
status = row['active-status'].strip().lower()
|
||||
if status == 'active':
|
||||
activated_count += 1
|
||||
elif status == 'no':
|
||||
deactivated_count += 1
|
||||
writer.writerow(row)
|
||||
|
||||
# 控制台输出统计信息
|
||||
print(f'''
|
||||
统计时间:{current_utc}
|
||||
激活车辆数:{activated_count}
|
||||
未激活车辆数:{deactivated_count}
|
||||
|
||||
📁 所有结果已保存至:{result_dir}
|
||||
''')
|
||||
BIN
hang/output/active_car_number.xlsx
Normal file
BIN
hang/output/active_car_number.xlsx
Normal file
Binary file not shown.
358
hang/output/demo0611.py
Normal file
358
hang/output/demo0611.py
Normal file
@@ -0,0 +1,358 @@
|
||||
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 = "激活状态" # 激活状态字段名称
|
||||
|
||||
# 飞书应用凭证
|
||||
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_vehicle_info.VIN AS car_vins,
|
||||
CASE v_sim_info.STATE
|
||||
WHEN 2 THEN 'active'
|
||||
ELSE 'no_active'
|
||||
END AS active_status
|
||||
FROM v_vehicle_info
|
||||
JOIN v_tbox_info USING (VIN)
|
||||
JOIN v_sim_info USING (SN);
|
||||
"""
|
||||
|
||||
# 执行查询并转换为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)
|
||||
123
lion-ououmeng9.py
Normal file
123
lion-ououmeng9.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from cassandra.cluster import Cluster
|
||||
from cassandra.query import BatchStatement
|
||||
import time
|
||||
import hashlib
|
||||
from cassandra.auth import PlainTextAuthProvider
|
||||
import base64
|
||||
from multiprocessing import Process
|
||||
from multiprocessing import Pool
|
||||
|
||||
def sha256(data):
|
||||
sha256 = hashlib.sha256()
|
||||
sha256.update(data.encode('utf-8'))
|
||||
return sha256.hexdigest()
|
||||
|
||||
def execute():
|
||||
# 定义用户名和密码
|
||||
username = 'tsp_eu_stress'
|
||||
password = 'UaiKeq6cKsm2S4y'
|
||||
|
||||
# 创建身份验证提供程序
|
||||
auth_provider = PlainTextAuthProvider(username=username, password=password)
|
||||
|
||||
# 连接到 Cassandra 集群
|
||||
cluster = Cluster(['10.95.0.81'], auth_provider=auth_provider)
|
||||
session = cluster.connect()
|
||||
|
||||
# # 创建Cassandra集群连接
|
||||
# cluster = Cluster(['172.25.116.188'])
|
||||
session = cluster.connect('tsp_eu')
|
||||
|
||||
# 创建批量操作语句
|
||||
# batch = BatchStatement()
|
||||
query2 = "insert into tsp_eu.realtime_his(v,ct,data,st,type) values(%s, %s,%s, %s,%s)"
|
||||
query3 = "insert into tsp_eu.realtime_hf(v,ct,data,st,type) values(%s, %s,%s, %s,%s)"
|
||||
|
||||
|
||||
|
||||
#j为每个vin插入条数;i为插入的vin数
|
||||
for j in range(1):
|
||||
|
||||
for i in range(1,1000001):
|
||||
|
||||
# time.sleep(1)
|
||||
# 创建插入语句
|
||||
# tbox登陆登出插入语句
|
||||
# query1 = "INSERT INTO tbox_login_logout (v,st,ct,reason,result,tbox_sn,type) VALUES (%s, %s,%s, %s,%s, %s,%s)"
|
||||
# batch = BatchStatement()
|
||||
# batch1 = BatchStatement()
|
||||
|
||||
# batch.add(query1, (
|
||||
# sha256('TESTVIN' + str(i).zfill(10)), int(time.time() * 1000), int(time.time() * 1000), "1", "1", "1111",
|
||||
# "2"))
|
||||
# print('TESTVIN' + str(i).zfill(10))
|
||||
# session.execute(batch)
|
||||
#
|
||||
# # # IHU车机登陆登出插入语句
|
||||
# query2 = "insert into tsp_eu.hu_login_logout(v,st,ct,hu_sn,status,status_msg,type) values(%s, %s,%s, %s,%s, %s,%s)"
|
||||
# batch1.add(query2, (sha256('TESTVIN' + str(i).zfill(10)), int(time.time() * 1000), int(time.time() * 1000),
|
||||
# 'TESTVIN' + str(i).zfill(10), '1', 'ddd', '1'))
|
||||
# session.execute(batch1)
|
||||
|
||||
# 低频数据插入语句
|
||||
# batch2 = BatchStatement()
|
||||
|
||||
# print('TESTVIN' + str(i).zfill(10))
|
||||
# batch2.add(query2, (
|
||||
# sha256('TESTVIN' + str(i).zfill(10)), int(time.time() * 1000), "{\"202F\":255,\"2030\":255,\"2031\":255,\"2032\":255,\"2035\":255,\"2036\":255,\"2039\":255,\"203A\":255,\"203B\":255,\"203C\":255}",int(time.time() * 1000), '0'))
|
||||
# session.execute(batch2)
|
||||
#
|
||||
# # 高频数据插入语句
|
||||
# batch3 = BatchStatement()
|
||||
# query3 = "insert into tsp_eu.realtime_hf(v,ct,data,st,type) values(%s, %s,%s, %s,%s)"
|
||||
# batch2.add(query3, (
|
||||
# sha256('TESTVIN' + str(i).zfill(10)), int(time.time() * 1000), "{\"0001\":46720,\"2005\":0,\"2076\":0,\"2077\":38912,\"2082\":2958360575,\"2084\":111428784,\"2085\":46720,\"2086\":0,\"2087\":2908749824,\"208A\":176,\"208C\":68}",
|
||||
# int(time.time() * 1000), '0'))
|
||||
# session.execute(batch2)
|
||||
|
||||
# 实时数据插入语句-每个vin只能有一条数据
|
||||
batch4 = BatchStatement()
|
||||
query4 = "insert into tsp_eu.realtime_new(v,ct,data,st) values(%s, %s,%s, %s)"
|
||||
print('HESTVIN' + str(i).zfill(10))
|
||||
batch4.add(query4, (
|
||||
sha256('HESTVIN' + str(i).zfill(10)), int(time.time() * 1000), {'2001': '350', '2002': '1', '2003': '2000', '2004': '600', '2005': '60', '2006': '83', '2007': '350', '2008': '0', '2009': '10000', '200A': '600', '200B': '1000', '200C': '3000', '200D': '0', '200E': '1', '200F': '3', '2010': '1', '2011': '0', '2012': '0', '2013': '2', '2014': '1', '2015': '0', '2016': '0', '2017': '3', '2018': '0', '2019': '0', '201A': '0', '201B': '0', '201C': '0', '201D': '0', '201E': '0', '201F': '0', '2020': '0', '2021': '0', '2022': '0', '2023': '0', '2024': '0', '2025': '0', '2026': '230', '2027': '230', '2028': '230', '2029': '0', '202A': '6', '202B': '6', '202C': '1', '202D': '0', '202E': '0', '202F': '0', '2030': '1', '2031': '2', '2032': '0', '2033': '1', '2034': '1', '2035': '1', '2036': '1', '2037': '3', '2038': '1', '2039': '1', '203A': '0', '203B': '0', '203C': '0', '203D': '0', '203E': '150', '203F': '0', '2040': '0', '2041': '3', '2042': '17000', '2043': '1000', '2044': '85', '2045': '0', '2046': '23', '2047': '23', '2048': '23', '2049': '23', '204A': '70', '204B': '70', '204C': '70', '204D': '70', '204E': '0', '204F': '0', '2050': '0', '2051': '0', '2052': '0', '2053': '0', '2054': '0', '2055': '0', '2056': '0', '2057': '0', '2058': '0', '2059': '0', '205A': '0', '205B': '0', '205C': '0', '205D': '0', '205E': '1687590135166', '205F': '1687590135166', '2060': '5000', '2061': '180', '2062': '3000', '2063': '0', '2064': '0', '2065': '0', '2066': '0', '2067': '0', '2068': '0', '2069': '0', '206A': '0', '206B': '0', '206C': '0', '206D': '5', '206E': '0', '206F': '5', '2070': '0', '2071': '0', '2072': '0', '2073': '0', '2076': '3', '2077': '350', '2078': '0', '2080': '255', '2081': '255', '2082': '4294967295', '2083': '255', '2084': '4294967295', '2085': '4294967295', '2086': '4294967295', '2087': '4294967295', '2088': '0', '2089': '255', '208A': '255', '208B': '255', '208C': '1800', '208D': '4294967295', '208E': '4294967295', '208F': '4294967295', '2090': '4294967295', '2091': '255', '2092': '255', '2093': '255', '2094': '255', '2095': '65535'}, int(time.time() * 1000)))
|
||||
session.execute(batch4)
|
||||
|
||||
# # 最新诊断
|
||||
# batch5 = BatchStatement()
|
||||
# query5 = "insert into tsp_eu.diagnosis_new(v,ct,data,out_vehicle_type,st) values(%s,%s, %s,%s, %s)"
|
||||
# batch5.add(query5, (
|
||||
# base64.urlsafe_b64encode(('TESTVIN' + str(i).zfill(10)).encode())[0:23].decode(), int(time.time() * 1000),
|
||||
# {'10': 'null', '11': 'null', '12': 'null', '13': 'null', '15': 'null', '19': 'null',
|
||||
# '2': '[9717033,9717267]', '20': 'null', '21': 'null', '22': 'null', '24': 'null', '32': 'null',
|
||||
# '35': 'null', '36': 'null', '5': 'null', '8': 'null'}, 'T19CEV_OUTSIDE_001_TEST_KB51',
|
||||
# int(time.time() * 1000)))
|
||||
# session.execute(batch5)
|
||||
|
||||
# # 历史诊断
|
||||
# batch6 = BatchStatement()
|
||||
# query6 = "insert into tsp_eu.diagnosis_his(v,ct,data,st) values(%s, %s,%s, %s)"
|
||||
# batch6.add(query6, (
|
||||
# sha256('TESTVIN' + str(i).zfill(10)), int(time.time() * 1000),
|
||||
# "{\"5010\":0,\"5011\":1,\"5001\":4,\"5A01\":[]}",
|
||||
# int(time.time() * 1000)))
|
||||
# session.execute(batch6)
|
||||
|
||||
# 执行批量操作
|
||||
# session.execute(batch)
|
||||
|
||||
# 关闭连接
|
||||
session.shutdown()
|
||||
cluster.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
# poo1=Pool(30)
|
||||
# for i in range(1000):
|
||||
# poo1.apply_async(execute)
|
||||
# poo1.close()
|
||||
# poo1.join()
|
||||
# for i in range(10):
|
||||
# p=Process(target=execute)
|
||||
# p.start()
|
||||
|
||||
execute()
|
||||
16
main.py
Normal file
16
main.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# This is a sample Python script.
|
||||
|
||||
# Press Shift+F10 to execute it or replace it with your code.
|
||||
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
|
||||
|
||||
|
||||
def print_hi(name):
|
||||
# Use a breakpoint in the code line below to debug your script.
|
||||
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
|
||||
|
||||
|
||||
# Press the green button in the gutter to run the script.
|
||||
if __name__ == '__main__':
|
||||
print_hi('PyCharm')
|
||||
|
||||
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
|
||||
BIN
reports/Activated_Vehicles_2025-04-22.xlsx
Normal file
BIN
reports/Activated_Vehicles_2025-04-22.xlsx
Normal file
Binary file not shown.
BIN
reports/Activated_Vehicles_2025-04-23.xlsx
Normal file
BIN
reports/Activated_Vehicles_2025-04-23.xlsx
Normal file
Binary file not shown.
BIN
reports/Inactive_Vehicles_2025-04-23.xlsx
Normal file
BIN
reports/Inactive_Vehicles_2025-04-23.xlsx
Normal file
Binary file not shown.
BIN
reports/Inactive_Vehicles_2025-04-24.xlsx
Normal file
BIN
reports/Inactive_Vehicles_2025-04-24.xlsx
Normal file
Binary file not shown.
BIN
reports/Inactive_Vehicles_2025-04-25.xlsx
Normal file
BIN
reports/Inactive_Vehicles_2025-04-25.xlsx
Normal file
Binary file not shown.
BIN
reports/Inactive_Vehicles_2025-04-28.xlsx
Normal file
BIN
reports/Inactive_Vehicles_2025-04-28.xlsx
Normal file
Binary file not shown.
BIN
reports/Inactive_Vehicles_2025-04-29.xlsx
Normal file
BIN
reports/Inactive_Vehicles_2025-04-29.xlsx
Normal file
Binary file not shown.
BIN
reports/Inactive_Vehicles_2025-05-05.xlsx
Normal file
BIN
reports/Inactive_Vehicles_2025-05-05.xlsx
Normal file
Binary file not shown.
BIN
reports/Inactive_Vehicles_2025-05-19.xlsx
Normal file
BIN
reports/Inactive_Vehicles_2025-05-19.xlsx
Normal file
Binary file not shown.
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
pandas~=2.2.3
|
||||
requests~=2.32.3
|
||||
sqlalchemy~=2.0.41
|
||||
PyMySQL~=1.1.1
|
||||
cassandra-driver~=3.28.0
|
||||
Reference in New Issue
Block a user