commit 1438117ba7f6a44f20f3e701b144e67f6cc00570 Author: Jeason <1710884619@qq.com> Date: Wed Mar 18 15:13:48 2026 +0800 欧洲分国家车辆激活数查询脚本 diff --git a/aa b/aa new file mode 100644 index 0000000..c44e375 --- /dev/null +++ b/aa @@ -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 \ No newline at end of file diff --git a/hang/0630.py b/hang/0630.py new file mode 100644 index 0000000..7b8be89 --- /dev/null +++ b/hang/0630.py @@ -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) \ No newline at end of file diff --git a/hang/Rewrite/active_number_daily.py b/hang/Rewrite/active_number_daily.py new file mode 100644 index 0000000..2648d97 --- /dev/null +++ b/hang/Rewrite/active_number_daily.py @@ -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) \ No newline at end of file diff --git a/hang/Rewrite/active_vin_check.py b/hang/Rewrite/active_vin_check.py new file mode 100644 index 0000000..4eb4bcd --- /dev/null +++ b/hang/Rewrite/active_vin_check.py @@ -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) diff --git a/hang/Rewrite/add_vehicle_type.py b/hang/Rewrite/add_vehicle_type.py new file mode 100644 index 0000000..303c1fb --- /dev/null +++ b/hang/Rewrite/add_vehicle_type.py @@ -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) diff --git a/hang/Rewrite/car_active_seek_UK.py b/hang/Rewrite/car_active_seek_UK.py new file mode 100644 index 0000000..427802d --- /dev/null +++ b/hang/Rewrite/car_active_seek_UK.py @@ -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") diff --git a/hang/Rewrite/car_active_seek_by_area.py b/hang/Rewrite/car_active_seek_by_area.py new file mode 100644 index 0000000..f741040 --- /dev/null +++ b/hang/Rewrite/car_active_seek_by_area.py @@ -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']}") \ No newline at end of file diff --git a/hang/active_time_analysis.py b/hang/active_time_analysis.py new file mode 100644 index 0000000..c7e4657 --- /dev/null +++ b/hang/active_time_analysis.py @@ -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}") diff --git a/hang/chery_active_car.py b/hang/chery_active_car.py new file mode 100644 index 0000000..c0b51df --- /dev/null +++ b/hang/chery_active_car.py @@ -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处理完成!按任意键退出...") diff --git a/hang/daily-active-check-eu-oj.py b/hang/daily-active-check-eu-oj.py new file mode 100644 index 0000000..80becdc --- /dev/null +++ b/hang/daily-active-check-eu-oj.py @@ -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)) \ No newline at end of file diff --git a/hang/demo060904.py b/hang/demo060904.py new file mode 100644 index 0000000..523901b --- /dev/null +++ b/hang/demo060904.py @@ -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() \ No newline at end of file diff --git a/hang/demo06092.py b/hang/demo06092.py new file mode 100644 index 0000000..2746bd0 --- /dev/null +++ b/hang/demo06092.py @@ -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) \ No newline at end of file diff --git a/hang/demo06093.py b/hang/demo06093.py new file mode 100644 index 0000000..f531e16 --- /dev/null +++ b/hang/demo06093.py @@ -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) \ No newline at end of file diff --git a/hang/demo0610.py b/hang/demo0610.py new file mode 100644 index 0000000..4bf9c76 --- /dev/null +++ b/hang/demo0610.py @@ -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) \ No newline at end of file diff --git a/hang/demo061002.py b/hang/demo061002.py new file mode 100644 index 0000000..2648d97 --- /dev/null +++ b/hang/demo061002.py @@ -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) \ No newline at end of file diff --git a/hang/demo061003.py b/hang/demo061003.py new file mode 100644 index 0000000..808685b --- /dev/null +++ b/hang/demo061003.py @@ -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) \ No newline at end of file diff --git a/hang/demo2.py b/hang/demo2.py new file mode 100644 index 0000000..b83a7cb --- /dev/null +++ b/hang/demo2.py @@ -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("✅ 统计报告已发送至飞书") \ No newline at end of file diff --git a/hang/italy-seek.py b/hang/italy-seek.py new file mode 100644 index 0000000..240e2f4 --- /dev/null +++ b/hang/italy-seek.py @@ -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} +''') diff --git a/hang/output/active_car_number.xlsx b/hang/output/active_car_number.xlsx new file mode 100644 index 0000000..7a97493 Binary files /dev/null and b/hang/output/active_car_number.xlsx differ diff --git a/hang/output/demo0611.py b/hang/output/demo0611.py new file mode 100644 index 0000000..808685b --- /dev/null +++ b/hang/output/demo0611.py @@ -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) \ No newline at end of file diff --git a/lion-ououmeng9.py b/lion-ououmeng9.py new file mode 100644 index 0000000..d5186a4 --- /dev/null +++ b/lion-ououmeng9.py @@ -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() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5596b44 --- /dev/null +++ b/main.py @@ -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/ diff --git a/reports/Activated_Vehicles_2025-04-22.xlsx b/reports/Activated_Vehicles_2025-04-22.xlsx new file mode 100644 index 0000000..7ef6a80 Binary files /dev/null and b/reports/Activated_Vehicles_2025-04-22.xlsx differ diff --git a/reports/Activated_Vehicles_2025-04-23.xlsx b/reports/Activated_Vehicles_2025-04-23.xlsx new file mode 100644 index 0000000..88d728c Binary files /dev/null and b/reports/Activated_Vehicles_2025-04-23.xlsx differ diff --git a/reports/Inactive_Vehicles_2025-04-23.xlsx b/reports/Inactive_Vehicles_2025-04-23.xlsx new file mode 100644 index 0000000..370fa33 Binary files /dev/null and b/reports/Inactive_Vehicles_2025-04-23.xlsx differ diff --git a/reports/Inactive_Vehicles_2025-04-24.xlsx b/reports/Inactive_Vehicles_2025-04-24.xlsx new file mode 100644 index 0000000..381e76c Binary files /dev/null and b/reports/Inactive_Vehicles_2025-04-24.xlsx differ diff --git a/reports/Inactive_Vehicles_2025-04-25.xlsx b/reports/Inactive_Vehicles_2025-04-25.xlsx new file mode 100644 index 0000000..202e410 Binary files /dev/null and b/reports/Inactive_Vehicles_2025-04-25.xlsx differ diff --git a/reports/Inactive_Vehicles_2025-04-28.xlsx b/reports/Inactive_Vehicles_2025-04-28.xlsx new file mode 100644 index 0000000..793d216 Binary files /dev/null and b/reports/Inactive_Vehicles_2025-04-28.xlsx differ diff --git a/reports/Inactive_Vehicles_2025-04-29.xlsx b/reports/Inactive_Vehicles_2025-04-29.xlsx new file mode 100644 index 0000000..622e211 Binary files /dev/null and b/reports/Inactive_Vehicles_2025-04-29.xlsx differ diff --git a/reports/Inactive_Vehicles_2025-05-05.xlsx b/reports/Inactive_Vehicles_2025-05-05.xlsx new file mode 100644 index 0000000..dc0a4c7 Binary files /dev/null and b/reports/Inactive_Vehicles_2025-05-05.xlsx differ diff --git a/reports/Inactive_Vehicles_2025-05-19.xlsx b/reports/Inactive_Vehicles_2025-05-19.xlsx new file mode 100644 index 0000000..6360b52 Binary files /dev/null and b/reports/Inactive_Vehicles_2025-05-19.xlsx differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5f8e284 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file