diff --git a/.gitignore b/.gitignore index 94fe0f0..1093eda 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,13 @@ *.iml out gen -.logs \ No newline at end of file +.logs +.log +.csv +.excel +.xlsx +output/ +__pycache__/ +.env +.vscode/ + diff --git a/back_ground_module/upload_file.xlsx b/back_ground_module/upload_file.xlsx new file mode 100644 index 0000000..a458eb7 Binary files /dev/null and b/back_ground_module/upload_file.xlsx differ diff --git a/test/1105ngv.ipynb b/test/1105ngv.ipynb new file mode 100644 index 0000000..97527f4 --- /dev/null +++ b/test/1105ngv.ipynb @@ -0,0 +1,323 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-11-05T09:03:45.525420Z", + "start_time": "2025-11-05T09:03:44.127181Z" + } + }, + "source": [ + "# -*- coding: utf-8 -*-\n", + "import pandas as pd\n", + "import datetime\n", + "from config import Config\n", + "from api import API\n", + "from back_ground_module import CommonModule\n", + "from log_config import configure_task_logger, configure_error_task_logger\n", + "import time\n", + "timestamp = time.time() # 返回 float,单位:秒\n", + "\n", + "logger = configure_task_logger()\n", + "# 获取已经配置好的错误任务日志记录器\n", + "error_task_logger = configure_error_task_logger()\n", + "start_time = datetime.datetime.now()\n", + "api_instance = API()\n", + "common_module = CommonModule()\n", + "output_dir = \"output\" # 设置输出目录\n", + "# 创建输出目录(如果不存在)\n", + "import os\n", + "\n", + "os.makedirs(output_dir, exist_ok=True)\n", + "\n", + "\n", + "class UpdateNGVData:\n", + " \"\"\"NGV数据每日新增\"\"\"\n", + "\n", + " def __init__(self):\n", + " self.staff_id_list = None\n", + " self.field_mapping = {}\n", + " self.fields()\n", + "\n", + " def load_all_data(self):\n", + " # 获取简道云员工id\n", + " payload = {\"api_key\": \"6694d3c4fcb69ca9a111a6c4\",\n", + " \"entry_id\": \"6769204a1902c9341340a1bc\",\n", + " }\n", + " staff_id = api_instance.entry_data_list(payload)\n", + " self.staff_id_list = staff_id.get(\"data\") # api请求格式,将数据封装在data字典里\n", + "\n", + " @staticmethod\n", + " def get_staff_id(row_item, name):\n", + " \"\"\"辅助函数,用于获取员工ID\"\"\"\n", + " if str(row_item[\"_widget_1734942794144\"]) == str(name): # 检查姓名是否匹配\n", + " return row_item[\"_widget_1734942794145\"] # 返回员工ID\n", + " return None\n", + "\n", + " def main(self):\n", + " task_start_time = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n", + " try:\n", + " self.load_all_data()\n", + " logger.info(f\"数据加载完成\")\n", + " #\n", + " # data_NGV_j = common_module.get_ngv_details(days_back=1)\n", + " # data_NGV_j1 = common_module.get_ngv_details(days_back=2)\n", + " timestamp = time.time() # 返回 float,单位:秒\n", + " #\n", + " # data_NGV_j.to_csv(os.path.join(output_dir, f\"{timestamp}up_NGV_j.csv\"))\n", + " # data_NGV_j1.to_csv(os.path.join(output_dir, f\"{timestamp}up_NGV_j1.csv\"))\n", + " #\n", + " # # 找出在 data_NGV_j 中存在但在 data_NGV_j1 中不存在的 data_id\n", + " # unique_data_ids = data_NGV_j[~data_NGV_j['org_code'].isin(data_NGV_j1['org_code'])]\n", + " #\n", + " # # 创建一个新的 DataFrame 保存这些唯一的 data_id 及其对应的数据\n", + " # new_df = data_NGV_j[data_NGV_j['org_code'].isin(unique_data_ids['org_code'])]\n", + " #\n", + " # # 对 new_df 进行进一步的过滤,只保留 org_type 为 \"一般\" 的记录\n", + " # data_NGV_j = data_NGV_j[data_NGV_j['org_type'] == '一般']\n", + " # data_NGV_j1 = data_NGV_j1[data_NGV_j1['org_type'] == '一般']\n", + " # filtered_df = new_df[new_df['org_type'] == '一般']\n", + " # 默认未删除\n", + " filtered_df = pd.read_excel(r\"C:\\Users\\zy187\\Downloads\\异常服务跟进待办_20251105170140.xlsx\",sheet_name=\"Sheet1\")\n", + " filtered_df['源ngv是否已删除'] = '未删除'\n", + "\n", + " # 日期字段转换为日期格式\n", + " time_columns = ['date_fmt', 'saas_create_time', 'expiry_time', 'install_create_time', \"last_end_date\",\n", + " \"renew_date\"]\n", + " new_filtered_df = filtered_df.copy() # 复制df,以调整时间\n", + " for col in time_columns:\n", + " # 1. 转换为datetime类型(带错误处理)\n", + " # 使用.loc安全赋值\n", + " new_filtered_df[col] = pd.to_datetime(filtered_df[col], errors='coerce', utc=False)\n", + "\n", + " # 2. 优化后的时区转换(高效向量化操作)\n", + " filtered_df[col + '_date'] = (\n", + " new_filtered_df[col]\n", + " # 本地化为北京时间(东八区)\n", + " .dt.tz_localize('Asia/Shanghai', ambiguous='infer', nonexistent='NaT')\n", + " # 转换为UTC时区\n", + " .dt.tz_convert('UTC')\n", + " # 格式化为ISO8601字符串\n", + " .dt.strftime('%Y-%m-%dT%H:%M:%SZ')\n", + " )\n", + " logger.info(f\"时间转换完成\")\n", + "\n", + " # 人员字段转换为人员字段\n", + " staff_columns = ['area_manager', 'service_impl_principal', \"service_salesmen\", \"technician\"]\n", + " # 将员工列表转为DataFrame\n", + " # 三重循环临时方案(确保可写入)\n", + " for col in staff_columns:\n", + " staff_ids = []\n", + " for _, row in filtered_df.iterrows():\n", + " matched = False\n", + " for staff in self.staff_id_list:\n", + " if str(staff['_widget_1734942794144']) == str(row[col]):\n", + " staff_ids.append(staff['_widget_1734942794145'])\n", + " matched = True\n", + " break\n", + " if not matched:\n", + " staff_ids.append(None)\n", + " filtered_df[col + \"_staff_id\"] = staff_ids\n", + " logger.info(f\"人员转换完成\")\n", + "\n", + " # filtered_df.to_csv(r\"D:\\Idea Project\\SaaS_V1.3\\back_ground_module\\output\\NGV.csv\")\n", + "\n", + " # 生成包含所有行转换后的字典列表\n", + " # all_data = [self.row_to_dict(row, self.field_mapping) for index, row in data_NGV_j1.iterrows()] # 前两天的全部数据\n", + " # all_data = [self.row_to_dict(row, self.field_mapping) for index, row in data_NGV_j.iterrows()] # 前一天的全部数据\n", + " all_data = [self.row_to_dict(row, self.field_mapping) for index, row in filtered_df.iterrows()] # 增量数据\n", + "\n", + " try:\n", + " filtered_df.to_csv(os.path.join(output_dir, f\"{timestamp}NGV.csv\"))\n", + " except Exception as e:\n", + " error_task_logger.error(f\"NGV过滤后数据保存异常: {e}\")\n", + " pass\n", + "\n", + " #\n", + " data = {'api_key': Config.SaaS_Tasks_APP_ID, 'entry_id': Config.NGV_TASKS_ENTRY_ID, \"data_list\": all_data,\n", + " \"is_start_trigger\": \"true\"}\n", + "\n", + " result = api_instance.entry_data_batch_create(data)\n", + " logger.info(f\"数据已推送:{result}\")\n", + " # result_str = str(result)\n", + " # print(result_str[:500])\n", + "\n", + " # 保存到Excel文件\n", + " # output_path = r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细1.xlsx'\n", + " # filtered_df.to_excel(output_path, index=False)\n", + " # data_NGV_j1.to_excel( r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细j1.xlsx', index=False)\n", + " # data_NGV_j.to_excel( r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细j.xlsx', index=False)\n", + " # new_df.to_excel(r'D:\\Idea Project\\F6+宜搭+其它(1)\\new\\文件输出\\ngv明细ndf.xlsx', index=False)\n", + "\n", + " common_module.send_task_status(task_start_time, \"NGV新增数据\")\n", + " logger.info(f\"任务完成。\")\n", + " except Exception as e:\n", + " error_task_logger.error(f\"任务执行时发生异常: {e}\")\n", + " # common_module.send_task_error(task_start_time, \"NGV新增数据\", str(e))\n", + "\n", + " @staticmethod\n", + " def row_to_dict(row, field_mapping):\n", + " \"\"\"将一行数据转换为指定格式的字典,并确保时间类型可JSON序列化\"\"\"\n", + " result = {}\n", + " for col_name, widget_id in field_mapping.items():\n", + " if col_name in row:\n", + " value = row[col_name]\n", + " if pd.isna(value):\n", + " clean_value = None\n", + " elif isinstance(value, (pd.Timestamp, pd.Timedelta)):\n", + " clean_value = value.isoformat() # 或 str(value)\n", + " elif hasattr(value, 'strftime'): # 兼容 datetime.datetime\n", + " clean_value = value.strftime('%Y-%m-%dT%H:%M:%SZ')\n", + " else:\n", + " clean_value = value\n", + " result[widget_id] = {\"value\": clean_value}\n", + " return result\n", + "\n", + " def fields(self):\n", + " self.field_mapping = dict(date_id='_widget_1734062123065', date_fmt='_widget_1734062123066',\n", + " id_own_group='_widget_1734062123067', group_name='_widget_1734062123068',\n", + " id_own_org='_widget_1734062123069', org_name='_widget_1734062123070',\n", + " org_code='_widget_1734062123071', group_grade='_widget_1734062123072',\n", + " org_type='_widget_1734062123073', org_status='_widget_1734062123074',\n", + " saas_version='_widget_1734062123075', is_wechat='_widget_1734062123076',\n", + " is_mini_app='_widget_1734062123077', is_wx_shop='_widget_1734062123078',\n", + " is_camera_service='_widget_1734062123079',\n", + " is_maintenance_service='_widget_1734062123080',\n", + " saas_create_time='_widget_1734062123081', expiry_time='_widget_1734062123082',\n", + " saas_use_days='_widget_1734062123083', saas_use_year='_widget_1734062123084',\n", + " is_main_org='_widget_1734062123085', license_code='_widget_1734062123086',\n", + " license_name='_widget_1734062123087', org_crm_id='_widget_1734062123088',\n", + " province_id='_widget_1734062123089', province_name='_widget_1734062123090',\n", + " city_id='_widget_1734062123091', city_name='_widget_1734062123092',\n", + " area_id='_widget_1734062123093', area_name='_widget_1734062123094',\n", + " region_name='_widget_1734062123095', region_short_name='_widget_1734062123096',\n", + " branch_name='_widget_1734062123097', carzone_store_id='_widget_1734062123098',\n", + " carzone_store_name='_widget_1734062123099',\n", + " customer_carzone_id='_widget_1734062123100', salesmen='_widget_1734062123101',\n", + " area_manager='_widget_1734062123102', service_salesmen='_widget_1734062123103',\n", + " impl_principal='_widget_1734062123104',\n", + " service_impl_principal='_widget_1734062123105',\n", + " active_user_count='_widget_1734062123106', active_user_type='_widget_1734062123107',\n", + " limit_user_count='_widget_1734062123108', limit_user_type='_widget_1734062123109',\n", + " is_n='_widget_1734062123110', is_g='_widget_1734062123111',\n", + " is_v='_widget_1734062123112', is_visited='_widget_1734062123113',\n", + " is_active='_widget_1734062123114', active_status_fmt='_widget_1734062123115',\n", + " bill_count_last_30_day='_widget_1734062123116',\n", + " bill_day_count_last_30_day='_widget_1734062123117',\n", + " bill_day_count_this_month='_widget_1734062123118',\n", + " bill_count_last_7_day='_widget_1734062123119',\n", + " bill_day_count_last_7_day='_widget_1734062123120', pv_count='_widget_1734062123121',\n", + " uv_count='_widget_1734062123122', bill_count_1d='_widget_1734062123123',\n", + " bill_count_2d='_widget_1734062123124', bill_count_3d='_widget_1734062123125',\n", + " bill_count_4d='_widget_1734062123126', bill_count_5d='_widget_1734062123127',\n", + " bill_count_6d='_widget_1734062123128', bill_count_7d='_widget_1734062123129',\n", + " bill_count_8d='_widget_1734062123130', bill_count_9d='_widget_1734062123131',\n", + " bill_count_10d='_widget_1734062123132', bill_count_11d='_widget_1734062123133',\n", + " bill_count_12d='_widget_1734062123134', bill_count_13d='_widget_1734062123135',\n", + " bill_count_14d='_widget_1734062123136', bill_count_15d='_widget_1734062123137',\n", + " bill_count_16d='_widget_1734062123138', bill_count_17d='_widget_1734062123139',\n", + " bill_count_18d='_widget_1734062123140', bill_count_19d='_widget_1734062123141',\n", + " bill_count_20d='_widget_1734062123142', bill_count_21d='_widget_1734062123143',\n", + " bill_count_22d='_widget_1734062123144', bill_count_23d='_widget_1734062123145',\n", + " bill_count_24d='_widget_1734062123146', bill_count_25d='_widget_1734062123147',\n", + " bill_count_26d='_widget_1734062123148', bill_count_27d='_widget_1734062123149',\n", + " bill_count_28d='_widget_1734062123150', bill_count_29d='_widget_1734062123151',\n", + " bill_count_30d='_widget_1734062123152', bill_count_31d='_widget_1734062123153',\n", + " etl_time='_widget_1734062123154',\n", + " maintain_bill_count_last_30_day='_widget_1734062123155',\n", + " washing_bill_count_last_30_day='_widget_1734062123156',\n", + " maintain_bill_day_count_last_30_day='_widget_1734062123157',\n", + " washing_bill_day_count_last_30_day='_widget_1734062123158',\n", + " retail_bill_count_last_30_day='_widget_1734062123159',\n", + " retail_bill_day_count_last_30_day='_widget_1734062123160',\n", + " purchase_bill_count_last_30_day='_widget_1734062123161',\n", + " purchase_bill_day_count_last_30_day='_widget_1734062123162',\n", + " card_bill_count_last_30_day='_widget_1734062123163',\n", + " card_bill_day_count_last_30_day='_widget_1734062123164',\n", + " gd_sales_bill_count_last_30_day='_widget_1734062123165',\n", + " gd_sales_bill_day_count_last_30_day='_widget_1734062123166',\n", + " g_change_flag='_widget_1734062123167', saas_package='_widget_1734062123168',\n", + " manage_model='_widget_1734062123169', contacts='_widget_1734062123170',\n", + " contact_number='_widget_1734062123171', contact_mobile='_widget_1734062123172',\n", + " g_month_count='_widget_1734062123173', g_month_percentage='_widget_1734062123174',\n", + " is_install_service='_widget_1734062123175',\n", + " install_create_time='_widget_1734062123176', last_end_date='_widget_1734062123177',\n", + " renew_date='_widget_1734062123178', is_chain_owner='_widget_1734062123179',\n", + " group_org_count='_widget_1734062123180',\n", + " recent_bill_warning_days='_widget_1734062123181',\n", + " g_change_flag_d='_widget_1734062123182', g_lost_warning_days='_widget_1734062123183',\n", + " saas_edition_fmt='_widget_1734062123184', g_flag_1m='_widget_1734062123185',\n", + " g_flag_2m='_widget_1734062123186', g_flag_3m='_widget_1734062123187',\n", + " g_flag_4m='_widget_1734062123188', g_flag_5m='_widget_1734062123189',\n", + " g_flag_6m='_widget_1734062123190', g_flag_day_count='_widget_1734062123191',\n", + " add_org_flag='_widget_1734062123192', pt='_widget_1734062123193',\n", + " org_size='_widget_1734062123194', qualification_type_fmt='_widget_1734062123195',\n", + " business_scope_fmt='_widget_1734062123196', store_type_fmt='_widget_1734062123197',\n", + " area='_widget_1734062123198', station_number='_widget_1734062123199',\n", + " header_type_fmt='_widget_1734062123200', org_stage='_widget_1734062123201',\n", + " g_count_this_month='_widget_1734062123202',\n", + " saas_customer_type='_widget_1734062123203', technician='_widget_1734062123204',\n", + " tmall_maintain_service_status_desc='_widget_1734062123205',\n", + " date_fmt_date='_widget_1749000071375',\n", + " area_manager_staff_id='_widget_1748496855779',\n", + " service_impl_principal_staff_id=\"_widget_1748496855780\",\n", + " service_salesmen_staff_id=\"_widget_1748496855778\",\n", + " technician_staff_id=\"_widget_1751877712235\",\n", + " saas_create_time_date=\"_widget_1749000071377\",\n", + " expiry_time_date=\"_widget_1749000071382\",\n", + " install_create_time_date=\"_widget_1749000071384\",\n", + " last_end_date_date=\"_widget_1749000071389\", renew_date_date=\"_widget_1749000071391\"\n", + " , 源NGV是否已删除=\"_widget_1754285499851\")\n", + "\n", + "\n", + "if __name__ == '__main__':\n", + " start = UpdateNGVData()\n", + " start.main()\n" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "已获取 100 条数据\n", + "已获取 146 条数据\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001B[92m2025-11-05 17:03:45,497 - api.py - task_logger - INFO - 获取了146条数据\u001B[0m\n", + "\u001B[92m2025-11-05 17:03:45,498 - 4224831806.py - task_logger - INFO - 数据加载完成\u001B[0m\n", + "\u001B[91m2025-11-05 17:03:45,523 - 4224831806.py - error_task_logger - ERROR - 任务执行时发生异常: 'date_fmt'\u001B[0m\n" + ] + } + ], + "execution_count": 6 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/test/excel_需要保留一条_按创建时间保留最新门店.py b/test/excel_需要保留一条_按创建时间保留最新门店.py new file mode 100644 index 0000000..083b3a0 --- /dev/null +++ b/test/excel_需要保留一条_按创建时间保留最新门店.py @@ -0,0 +1,93 @@ +import argparse +from pathlib import Path + +import pandas as pd + + +def keep_latest_by_time(df: pd.DataFrame, store_col: str, time_col: str) -> pd.DataFrame: + if store_col not in df.columns: + raise ValueError(f"缺少列: {store_col}") + if time_col not in df.columns: + raise ValueError(f"缺少列: {time_col}") + + working = df.copy() + working[store_col] = working[store_col].astype(str).fillna("") + working[time_col] = working[time_col].astype(str).fillna("") + + working["_parsed_time"] = pd.to_datetime(working[time_col], errors="coerce") + working["_row_order"] = range(len(working)) + + working = working.sort_values( + by=["_parsed_time", "_row_order"], + ascending=[True, True], + kind="mergesort", + na_position="first", + ) + latest = working.groupby(store_col, sort=False).tail(1) + latest = latest.drop(columns=["_parsed_time", "_row_order"]).reset_index(drop=True) + return latest + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + parser.add_argument("--input", "-i", required=False, help="输入 Excel 路径(.xlsx)") + parser.add_argument("--output", "-o", required=False, help="输出 Excel 路径(.xlsx)") + parser.add_argument("--sheet", default="需要保留一条", help="Sheet 名称") + parser.add_argument("--store-col", default="门店编码", help="门店编码列名") + parser.add_argument("--time-col", default="创建时间", help="创建时间列名") + parser.add_argument("--codes-output", required=False, help="可选:输出门店编码清单(.txt 或 .csv)") + parser.add_argument("--demo", action="store_true", help="运行内置示例(不读写 Excel)") + return parser + + +def main() -> int: + args = build_parser().parse_args() + + if args.demo: + demo_df = pd.DataFrame( + [ + {"门店编码": "A001", "创建时间": "2026-03-01 10:00:00", "其他": "x"}, + {"门店编码": "A001", "创建时间": "2026-03-05 09:00:00", "其他": "y"}, + {"门店编码": "B002", "创建时间": "2026/03/02 12:00", "其他": "m"}, + {"门店编码": "B002", "创建时间": "无效时间", "其他": "n"}, + ] + ) + result = keep_latest_by_time(demo_df, store_col=args.store_col, time_col=args.time_col) + print(result) + print("门店编码:", ",".join(result[args.store_col].astype(str).tolist())) + return 0 + + if not args.input: + raise SystemExit("缺少参数 --input") + + input_path = Path(args.input).expanduser().resolve() + if not input_path.exists(): + raise SystemExit(f"输入文件不存在: {input_path}") + + df = pd.read_excel(input_path, sheet_name=args.sheet, dtype=str).fillna("") + latest = keep_latest_by_time(df, store_col=args.store_col, time_col=args.time_col) + + output_path = Path(args.output).expanduser().resolve() if args.output else None + if output_path is None: + output_path = input_path.with_name(f"{input_path.stem}_保留最新.xlsx") + + with pd.ExcelWriter(output_path, engine="openpyxl") as writer: + latest.to_excel(writer, sheet_name="保留最新", index=False) + + store_codes = latest[args.store_col].astype(str).tolist() + print(f"保留行数: {len(latest)}") + print(f"门店编码数量: {len(store_codes)}") + + if args.codes_output: + codes_path = Path(args.codes_output).expanduser().resolve() + if codes_path.suffix.lower() == ".csv": + pd.DataFrame({args.store_col: store_codes}).to_csv(codes_path, index=False, encoding="utf-8-sig") + else: + codes_path.write_text("\n".join(store_codes), encoding="utf-8") + + print(f"输出文件: {output_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/异常服务代办暂停派发不生效问题排查.py b/test/异常服务代办暂停派发不生效问题排查.py index 94563cd..9ab8a93 100644 --- a/test/异常服务代办暂停派发不生效问题排查.py +++ b/test/异常服务代办暂停派发不生效问题排查.py @@ -1,13 +1,7 @@ import datetime import os -import sys import time import requests - -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) -if project_root not in sys.path: - sys.path.insert(0, project_root) - from api import API from back_ground_module import CommonModule import pandas as pd @@ -224,10 +218,6 @@ class NewExceptionTask: """根据省市区派发给异常回访客服""" # try: customer_service_info = self.find_customer_service(province_name, city_name, area_name, index) - if not isinstance(customer_service_info, dict): - logger.warning( - f"【省市区未匹配到客服】省={province_name} 市={city_name} 区={area_name} raw={customer_service_info}") - return None customer_service = customer_service_info.get('_widget_1734677164870', {}).get('username') # 异常回访客服 return customer_service # except Exception as e: @@ -242,18 +232,7 @@ class NewExceptionTask: try: self.load_all_data() - data = None - for days_back in range(1, 15): - target_date_id = int( - (datetime.datetime.now() - datetime.timedelta(days=days_back)).strftime("%Y%m%d") - ) - logger.info(f"尝试获取异常明细:pt={target_date_id} days_back={days_back}") - data = common_module.get_yichang_details(days_back=days_back) - if data is not None and not data.empty: - logger.info(f"获取异常明细成功:pt={target_date_id} rows={len(data)} days_back={days_back}") - break - logger.info(f"异常明细为空:pt={target_date_id} days_back={days_back}") - + data = common_module.get_yichang_details(days_back=1) self.data_yichang_S = pd.DataFrame() if data is None or data.empty else data.astype(str) self.index = self.build_index(self.json_list) @@ -274,24 +253,6 @@ class NewExceptionTask: # 对整个DataFrame的所有列应用替换函数 data_yichang = data_yichang.apply(replace_values) error_data = [] - - def extract_widget_value(widget_value): - if widget_value is None: - return None - if isinstance(widget_value, dict): - return widget_value.get("value") - return widget_value - - existing_org_codes = set() - for exception_service in self.exception_service_todo: - try: - existing_value = extract_widget_value(exception_service.get("_widget_1748241895842")) - if existing_value is not None and str(existing_value).strip() != "": - existing_org_codes.add(str(existing_value).strip()) - except Exception: - continue - - dispatched_org_codes = set() for index_num, row in data_yichang.iterrows(): # 对过滤后的每一条进行派发 try: # 每次循环前清空省市区变量 @@ -299,17 +260,15 @@ class NewExceptionTask: city_name = None area_name = None - org_code = str(row.get("org_code", "")).strip() - if not org_code: - logger.warning(f"数据缺少门店编码,跳过。index={index_num}") - continue + is_pass = False + for exception_service in self.exception_service_todo: + # 通过查询筛选进行中的逻辑 + if exception_service['_widget_1748241895842'] == row['org_code']: + is_pass = True + break - if org_code in dispatched_org_codes: - logger.info(f"同一轮数据中门店编码重复,跳过派发。org_code={org_code} index={index_num}") - continue - - if org_code in existing_org_codes: - logger.info(f"已存在待办,跳过派发。org_code={org_code} index={index_num}") + if is_pass: + logger.info(f"已存在待办,跳过该条记录: {row}") continue payload_dict = {} @@ -361,7 +320,7 @@ class NewExceptionTask: # 获取关联数据 for NGV_Data in self.NGV_data_list: # NGV_Data = NGV_Data.get("data") - if org_code == str(NGV_Data.get("_widget_1734062123071", "")).strip(): # 门店编码 + if row["org_code"] == NGV_Data.get("_widget_1734062123071"): # 门店编码 NGV_data_id = NGV_Data.get("_id") # 如果需要从 NGV_data_list 获取省市区信息 @@ -381,8 +340,6 @@ class NewExceptionTask: create_date = NGV_Data.get("_widget_1734062123081") # 获取暂停派发日期 stop_date = NGV_Data.get("_widget_1772610343227", None) - logger.info( - f"【暂停派发字段】org_code={org_code} stop_date={stop_date} type={type(stop_date).__name__}") break # 找到匹配的数据后退出循环 # 定义可能的日期格式(灵活应对不同格式) @@ -392,15 +349,15 @@ class NewExceptionTask: "%Y/%m/%d", "%Y/%m/%d %H:%M:%S" ] - + if stop_date: + # 解析暂停派发日期 parsed_stop_date = None - stop_value = extract_widget_value(stop_date) - local_tz = datetime.timezone(datetime.timedelta(hours=8)) - now_local = datetime.datetime.now(local_tz) - + stop_value = stop_date.get("value") if isinstance(stop_date, dict) else stop_date if isinstance(stop_value, (int, float)): - parsed_stop_date = datetime.datetime.fromtimestamp(stop_value / 1000, tz=local_tz) + parsed_stop_date = datetime.datetime.fromtimestamp( + stop_value / 1000, tz=datetime.timezone.utc + ).replace(tzinfo=None) elif isinstance(stop_value, str): stop_str = stop_value.strip() iso_candidate = stop_str[:-1] + "+00:00" if stop_str.endswith("Z") else stop_str @@ -410,25 +367,25 @@ class NewExceptionTask: iso_dt = None if iso_dt is not None: - parsed_stop_date = iso_dt.replace(tzinfo=local_tz) if iso_dt.tzinfo is None else iso_dt.astimezone(local_tz) + parsed_stop_date = iso_dt.astimezone(datetime.timezone.utc).replace(tzinfo=None) if iso_dt.tzinfo else iso_dt else: for fmt in date_formats: try: - parsed_stop_date = datetime.datetime.strptime(stop_str, fmt).replace( - tzinfo=local_tz) + parsed_stop_date = datetime.datetime.strptime(stop_str, fmt) + logger.debug(f"使用格式 {fmt} 成功解析暂停派发日期: {parsed_stop_date}") break except ValueError: continue - - if parsed_stop_date is None: - logger.warning( - f"【暂停派发解析失败】org_code={org_code} stop_value={stop_value} type={type(stop_value).__name__}") - else: - logger.info( - f"【暂停派发校验】org_code={org_code} now={now_local} stop_until={parsed_stop_date}") - if now_local < parsed_stop_date: - logger.info( - f"【暂停派发生效】org_code={org_code} 当前时间未到暂停派发截止时间,跳过派发") + + if parsed_stop_date: + # 获取当前UTC时间 + current_utc_time = datetime.datetime.utcnow() + logger.debug(f"当前UTC时间: {current_utc_time}") + logger.debug(f"暂停派发日期: {parsed_stop_date}") + + # 比较时间 + if current_utc_time < parsed_stop_date: + logger.info(f"当前UTC时间低于暂停派发日期,跳过派发") continue # 判断门店原因 @@ -439,36 +396,37 @@ class NewExceptionTask: if create_exception == "否": continue # 新增:检查 create_date_str 是否存在且有效 - create_date_value = extract_widget_value(create_date) + create_date_value = create_date.get("value") if isinstance(create_date, dict) else create_date if not create_date_value: logger.warning("上线日期为空,跳过该记录") continue parsed_date = None - for fmt in date_formats: + if isinstance(create_date_value, (int, float)): + local_tz = datetime.timezone(datetime.timedelta(hours=8)) + parsed_date = datetime.datetime.fromtimestamp(create_date_value / 1000, tz=local_tz).date() + elif isinstance(create_date_value, str): + create_str = create_date_value.strip() + iso_candidate = create_str[:-1] + "+00:00" if create_str.endswith("Z") else create_str try: - if isinstance(create_date_value, (int, float)): - local_tz = datetime.timezone(datetime.timedelta(hours=8)) - parsed_date = datetime.datetime.fromtimestamp(create_date_value / 1000, tz=local_tz).date() - else: - create_str = str(create_date_value).strip() - iso_candidate = create_str[:-1] + "+00:00" if create_str.endswith("Z") else create_str - try: - iso_dt = datetime.datetime.fromisoformat(iso_candidate) - except ValueError: - iso_dt = None - if iso_dt is not None: - parsed_date = (iso_dt.date() if iso_dt.tzinfo is None else iso_dt.astimezone(datetime.timezone(datetime.timedelta(hours=8))).date()) - else: - parsed_date = datetime.datetime.strptime(create_str, fmt).date() - logger.debug(f"使用格式 {fmt} 成功解析日期: {parsed_date}") - break + iso_dt = datetime.datetime.fromisoformat(iso_candidate) except ValueError: - continue + iso_dt = None + + if iso_dt is not None: + local_tz = datetime.timezone(datetime.timedelta(hours=8)) + parsed_date = iso_dt.date() if iso_dt.tzinfo is None else iso_dt.astimezone(local_tz).date() + else: + for fmt in date_formats: + try: + parsed_date = datetime.datetime.strptime(create_str, fmt).date() + logger.debug(f"使用格式 {fmt} 成功解析日期: {parsed_date}") + break + except ValueError: + continue if parsed_date is None: - logger.error( - f"无法解析上线日期: '{create_date_value}',支持的格式: %Y-%m-%d, %Y-%m-%d %H:%M:%S 等") + logger.error(f"无法解析上线日期: '{create_date_value}',支持的格式: %Y-%m-%d, %Y-%m-%d %H:%M:%S 等") continue # 解析失败,跳过 # 使用解析后的日期进行判断 @@ -484,7 +442,7 @@ class NewExceptionTask: continue if not NGV_data_id: - logger.warning(f"未找到关联数据,请检查门店编码: {org_code}") + logger.warning(f"未找到关联数据,请检查门店编码: {row['org_code']}") # 根据省市区派发给异常回访客服 # 检查省市区是否都有值,如果有任何一个为空,则客服为空 @@ -496,11 +454,6 @@ class NewExceptionTask: logger.warning(f"省: {province_name}, 市: {city_name}, 区: {area_name}") else: customer_service = self.assign_customer_service(province_name, city_name, area_name, self.index) - if customer_service is None: - logger.warning( - f"【派发客服失败】门店 {org_code} 省={province_name} 市={city_name} 区={area_name} 未匹配到客服,跳过派发") - error_data.append(row) - continue logger.info(f"【派发客服】门店 {row['org_code']} 派发给客服: {customer_service}") payload_dict.update({ @@ -564,7 +517,7 @@ class NewExceptionTask: "_widget_1748512176655": {"value": "未处理"}, # 跟进状态 - "_widget_1772761760440": {"value": "客服跟进节点"}, # 当前跟进节点 + "_widget_1772761760440":{"value": "客服跟进节点"}, # 当前跟进节点 }) @@ -576,7 +529,6 @@ class NewExceptionTask: "transaction_id": UUid } all_data.append(routine_follow_up_payload) - dispatched_org_codes.add(org_code) # res = api_instance.data_batch_create(routine_follow_up_payload) # logger.info(f"创建结果:{res}") @@ -602,4 +554,3 @@ class NewExceptionTask: if __name__ == '__main__': start = NewExceptionTask() start.main() -# -*- coding: utf-8 -*- diff --git a/test/数据库表迁移.py b/test/数据库表迁移.py new file mode 100644 index 0000000..bd9e9a9 --- /dev/null +++ b/test/数据库表迁移.py @@ -0,0 +1,139 @@ +import pymysql +import sys +import time + +# ================== 配置信息 ================== +SOURCE_CONFIG = { + 'host': "f6-public.rwlb.rds.aliyuncs.com", + 'user': "rw_operation_data_relay", + 'password': "m+q5Z4%IVuF9bf", + 'database': "f6operation_data_relay", + 'connect_timeout': 30, + 'read_timeout': 600, + 'write_timeout': 600 +} + +TARGET_CONFIG = { + 'host': "db-f6operation-sst.f6car.org", + 'user': "rw_operation", + 'password': "tDm45eBj@upzLydHc", + 'database': "f6operation_data_relay", + 'connect_timeout': 30, + 'read_timeout': 600, + 'write_timeout': 600 +} + +TABLE_NAME = 'rpt_customized_maintain_detail' +READ_BATCH_SIZE = 1000 # 每次从源库读 5000 行 +WRITE_BATCH_SIZE = 1000 # 每次向目标库写 5000 行 +# ============================================== + + +def main(): + print("🔧 正在从源数据库读取表结构...") + + # === 第一步:获取建表语句 === + source_conn = None + try: + source_conn = pymysql.connect(**SOURCE_CONFIG) + with source_conn.cursor() as cursor: + cursor.execute(f"SHOW CREATE TABLE `{TABLE_NAME}`") + result = cursor.fetchone() + if not result: + raise Exception(f"表 {TABLE_NAME} 不存在于源数据库") + create_table_sql = result[1] + except Exception as e: + print(f"❌ 读取表结构失败: {e}") + sys.exit(1) + finally: + if source_conn: + source_conn.close() + + # === 第二步:获取总行数(用于进度)=== + total_rows = 0 + try: + source_conn = pymysql.connect(**SOURCE_CONFIG) + with source_conn.cursor() as cursor: + cursor.execute(f"SELECT COUNT(*) FROM `{TABLE_NAME}`") + total_rows = cursor.fetchone()[0] + except Exception as e: + print(f"⚠️ 无法获取总行数(不影响迁移): {e}") + finally: + if source_conn: + source_conn.close() + + print(f"📊 表共约 {total_rows} 行,将分批读取和写入...") + + # === 第三步:重建目标表 === + target_conn = None + columns = [] + try: + target_conn = pymysql.connect(**TARGET_CONFIG) + with target_conn.cursor() as cursor: + cursor.execute(f"DROP TABLE IF EXISTS `{TABLE_NAME}`") + new_create_sql = create_table_sql.replace(f"`{SOURCE_CONFIG['database']}`.", "") + cursor.execute(new_create_sql) + # 获取列名(从建表语句解析较复杂,改用查一次空结果) + cursor.execute(f"SELECT * FROM `{TABLE_NAME}` LIMIT 0") + columns = [desc[0] for desc in cursor.description] + target_conn.commit() + print("✅ 目标表已重建") + except Exception as e: + print(f"❌ 重建目标表失败: {e}") + if target_conn: + target_conn.close() + sys.exit(1) + + # === 第四步:分批读取 + 分批写入 === + offset = 0 + inserted_total = 0 + + while True: + time.sleep(5) + # 从源库读取一批 + batch_rows = [] + try: + source_conn = pymysql.connect(**SOURCE_CONFIG) + with source_conn.cursor(pymysql.cursors.DictCursor) as cursor: + cursor.execute( + f"SELECT * FROM `{TABLE_NAME}` LIMIT %s OFFSET %s", + (READ_BATCH_SIZE, offset) + ) + batch_rows = cursor.fetchall() + source_conn.close() + except Exception as e: + print(f"❌ 读取第 {offset//READ_BATCH_SIZE + 1} 批数据失败: {e}") + break + + if not batch_rows: + print("🔚 数据读取完成") + break + + # 转换为元组 + data_tuples = [tuple(row[col] for col in columns) for row in batch_rows] + + # 写入目标库(可再分小批,但这里 batch_rows 已是 5000) + try: + with target_conn.cursor() as cursor: + placeholders = ', '.join(['%s'] * len(columns)) + insert_sql = f"INSERT INTO `{TABLE_NAME}` (`{'`, `'.join(columns)}`) VALUES ({placeholders})" + cursor.executemany(insert_sql, data_tuples) + target_conn.commit() + inserted_total += len(data_tuples) + print(f"📤 已写入 {inserted_total} / {total_rows} 行") + except Exception as e: + print(f"❌ 写入失败(批次 offset={offset}): {e}") + target_conn.rollback() + break + + offset += READ_BATCH_SIZE + + # === 清理 === + if target_conn and target_conn.open: + target_conn.close() + + print(f"🎉 迁移完成!共写入 {inserted_total} 行") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test/数据库验证脚本_数据处理.ipynb b/test/数据库验证脚本_数据处理.ipynb new file mode 100644 index 0000000..13642f1 --- /dev/null +++ b/test/数据库验证脚本_数据处理.ipynb @@ -0,0 +1,899 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 数据库验证脚本 - 数据处理部分\n", + "\n", + "本notebook用于调试和验证数据库验证脚本的数据处理逻辑(260-425行)\n", + "\n", + "## 使用说明\n", + "1. 先执行数据加载部分(第2个单元格),这部分比较耗时\n", + "2. 数据加载完成后,再执行后续的数据处理单元格\n", + "3. 每个单元格都可以单独执行和调试\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T06:53:03.604128900Z", + "start_time": "2026-01-16T06:53:01.840121200Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "库导入完成\n", + "项目根目录: D:\\Idea Project\\SaaS_V1.7\n" + ] + } + ], + "source": [ + "# 导入必要的库\n", + "import os\n", + "import sys\n", + "import pandas as pd\n", + "import datetime\n", + "from datetime import datetime, timedelta\n", + "import re\n", + "\n", + "# 添加项目根目录到路径(notebook文件在test目录下,需要添加父目录)\n", + "current_dir = os.getcwd()\n", + "# 如果当前目录是test,则添加父目录;否则添加当前目录\n", + "if os.path.basename(current_dir) == 'test':\n", + " project_root = os.path.dirname(current_dir)\n", + "else:\n", + " project_root = current_dir\n", + "sys.path.insert(0, project_root)\n", + "\n", + "from api import API\n", + "from back_ground_module import CommonModule\n", + "from log_config import configure_task_logger, configure_error_task_logger\n", + "\n", + "# 初始化API和CommonModule\n", + "api_instance = API()\n", + "common_module = CommonModule()\n", + "\n", + "# 获取日志记录器\n", + "logger = configure_task_logger()\n", + "error_task_logger = configure_error_task_logger()\n", + "\n", + "print(\"库导入完成\")\n", + "print(f\"项目根目录: {project_root}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤1: 数据加载(耗时操作,可单独执行)\n", + "\n", + "这部分会加载所有必要的数据,包括:\n", + "- 省市区人员关系表\n", + "- 员工ID列表\n", + "- 权限表\n", + "- NGV数据列表\n", + "- 服务提醒数据\n", + "- 智能检测数据\n", + "- 功能使用情况表\n", + "- 保单识别表\n", + "- 私域/公域小程序数据\n", + "- 异业合作数据\n", + "- 短信数据\n", + "- 多公司过滤表\n", + "- NGV明细数据(从数据库获取)\n", + "- 节假日列表\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ========== 数据加载部分 ==========\n", + "# 这部分比较耗时,可以单独执行\n", + "\n", + "print(\"开始加载数据...\")\n", + "\n", + "# 省市区人员关系表\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676512ac3e54dc3159460c0a\"}\n", + "json_dict = api_instance.entry_data_list(payload)\n", + "if json_dict and \"data\" in json_dict:\n", + " json_list = json_dict.get(\"data\")\n", + "else:\n", + " print(\"加载省市区人员关系表失败\")\n", + " json_list = []\n", + "print(f\"省市区人员关系表: {len(json_list)} 条\")\n", + "\n", + "# 获取简道云员工id\n", + "payload = {\"api_key\": \"6694d3c4fcb69ca9a111a6c4\", \"entry_id\": \"6769204a1902c9341340a1bc\"}\n", + "staff_id = api_instance.entry_data_list(payload)\n", + "staff_id_list = staff_id.get(\"data\")\n", + "print(f\"员工ID列表: {len(staff_id_list)} 条\")\n", + "\n", + "# 获取权限表信息\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"675b96c14e839f90fef1647c\"}\n", + "permissions_table = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"权限表: {len(permissions_table)} 条\")\n", + "\n", + "# 获取NGV数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"675bb02bd2d53c2034c665e4\"}\n", + "NGV_data_list = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"NGV数据列表: {len(NGV_data_list)} 条\")\n", + "\n", + "# 获取服务提醒-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676bb7bda3029720f1083e99\"}\n", + "service_remind = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"服务提醒数据: {len(service_remind)} 条\")\n", + "\n", + "# 获取智能检测-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676bb99649ab3ac975af6e39\"}\n", + "Smart_detection = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"智能检测数据: {len(Smart_detection)} 条\")\n", + "\n", + "# 获取功能使用情况表\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"6763bbf657bd8fb76fcb41b2\"}\n", + "get_feature_usage = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "print(f\"功能使用情况表: {len(get_feature_usage)} 条\")\n", + "\n", + "# 获取保单识别表\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"6773a60d30ed87ff9f68d3c5\"}\n", + "policy_recognition = api_instance.entry_data_list(payload).get(\"data\")\n", + "widget_list = [item['_widget_1735632397600'] for item in policy_recognition]\n", + "print(f\"保单识别表: {len(policy_recognition)} 条\")\n", + "\n", + "# 获取私域小程序-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e0f0fae622896749ba5087\"}\n", + "private_domain = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "print(f\"私域小程序数据: {len(private_domain)} 条\")\n", + "\n", + "# 获取公域小程序-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e0c702c8f603b997980999\"}\n", + "public_domain = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "public_domain_list = [item['_widget_1742784257506'] for item in public_domain]\n", + "print(f\"公域小程序数据: {len(public_domain)} 条\")\n", + "\n", + "# 获取异业合作-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e24fdd8dfcfa918e17c30b\"}\n", + "different_industries = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "different_industries_list = [item['_widget_1742884829007'] for item in different_industries]\n", + "print(f\"异业合作数据: {len(different_industries)} 条\")\n", + "\n", + "# 获取短信-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e5107198ba1b20d5df3974\"}\n", + "groupnotification = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "print(f\"短信数据: {len(groupnotification)} 条\")\n", + "\n", + "# 获取多公司过滤表\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"689bf5f8ba88a28cb0679ec9\"}\n", + "get_filter_company_list = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "print(f\"多公司过滤表: {len(get_filter_company_list)} 条\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# 获取节假日列表\n", + "date_list = common_module.get_holiday_list()\n", + "print(f\"节假日列表: {len(date_list)} 个日期\")\n", + "\n", + "# 获取NGV明细数据(从数据库获取,比较耗时)\n", + "print(\"\\n开始从数据库获取NGV明细数据(这可能需要一些时间)...\")\n", + "data_NGV = common_module.get_ngv_details(days_back=1)\n", + "print(f\"NGV明细数据: {len(data_NGV)} 条\")\n", + "print(f\"NGV明细数据列数: {len(data_NGV.columns)}\")\n", + "\n", + "# 构建省市区索引\n", + "def build_index(json_list):\n", + " index = {}\n", + " for json_item in json_list:\n", + " try:\n", + " key = (json_item['_widget_1734677164861'], json_item['_widget_1734677164862'],\n", + " json_item['_widget_1734677164863']) # 省市区\n", + " if '_widget_1734677164871' not in json_item: # 日常回访客服\n", + " raise KeyError(\"缺少 '日常回访客服' 键\")\n", + " index[key] = json_item\n", + " except KeyError as e:\n", + " print(f\"警告:{e},跳过该条记录\")\n", + " continue\n", + " return index\n", + "\n", + "index = build_index(json_list)\n", + "print(f\"省市区索引构建完成: {len(index)} 条\")\n", + "\n", + "print(\"\\n========== 数据加载完成 ==========\")\n", + "print(f\"数据加载时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤2: 数据处理逻辑(260-425行)\n", + "\n", + "这部分包含主要的数据处理逻辑,包括:\n", + "- 获取多公司过滤公司id\n", + "- 数据清洗和转换\n", + "- 优先级排序\n", + "- 数据过滤和合并\n", + "- 日期计算和扩展\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ========== 获取多公司过滤公司id ==========\n", + "logger.info(\"获取多公司过滤公司id\")\n", + "all_filter_company_list = [] # 获取多公司过滤公司id\n", + "for company in get_filter_company_list:\n", + " company_list = company.get(\"_widget_1755052002491\")\n", + " if company_list:\n", + " for company_item in company_list:\n", + " if company_item.get(\"_widget_1755052002496\") == \"否\":\n", + " all_filter_company_list.append(company_item.get(\"_widget_1755052002495\"))\n", + "logger.info(f\"过滤公司条数:{len(all_filter_company_list)}\")\n", + "print(f\"过滤公司条数: {len(all_filter_company_list)}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:27:13.175060200Z", + "start_time": "2026-01-16T07:27:09.857076900Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "数据预处理完成,当前数据量: 45672 条\n", + "数据列数: 143\n" + ] + } + ], + "source": [ + "# ========== 数据预处理:日期转换和数据清洗 ==========\n", + "# 将A列和B列的日期字符串转换为日期格式\n", + "data_NGV = data_NGV.copy()\n", + "data_NGV['A'] = pd.to_datetime(data_NGV['expiry_time'])\n", + "data_NGV['B'] = pd.to_datetime(data_NGV['renew_date'])\n", + "\n", + "def replace_values(series):\n", + " # 使用条件判断来进行替换\n", + " return series.apply(lambda x: '' if pd.isna(x) or x in ['NA', 'None', ''] else x)\n", + "\n", + "# 处理字符串数据并显式指定数据类型\n", + "data_NGV = data_NGV.apply(replace_values)\n", + "\n", + "print(f\"数据预处理完成,当前数据量: {len(data_NGV)} 条\")\n", + "print(f\"数据列数: {len(data_NGV.columns)}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:28:01.298494800Z", + "start_time": "2026-01-16T07:28:01.106113700Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "过滤多公司后数据量: 45653 条\n" + ] + } + ], + "source": [ + "# ========== 过滤多公司 ==========\n", + "# 针对公司主店过期,取公司最高等级版本派发\n", + "# 过滤多公司\n", + "data_NGV = data_NGV[~data_NGV['id_own_group'].isin(all_filter_company_list)]\n", + "print(f\"过滤多公司后数据量: {len(data_NGV)} 条\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:28:04.235079300Z", + "start_time": "2026-01-16T07:28:04.185820400Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "优先级映射完成\n" + ] + } + ], + "source": [ + "# ========== 定义优先级顺序和创建映射字典 ==========\n", + "# 定义优先级顺序\n", + "edition_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']\n", + "customer_type_order = [\"F\", \"E\", \"D\", \"C\", \"B\", \"A\"] # 索引越小优先级越高\n", + "group_grade_order = ['全国KA(FMVP)', '区域KA(MVP)', '重要客户(SVIP)', '普通客户(VIP)']\n", + "\n", + "# 创建映射字典,并为不在列表中的值设置默认值\n", + "edition_map = {edition: idx for idx, edition in enumerate(edition_order)}\n", + "customer_type_map = {ctype: idx for idx, ctype in enumerate(customer_type_order)}\n", + "group_grade_map = {grade: idx for idx, grade in enumerate(group_grade_order)}\n", + "\n", + "# 添加用于排序的新列,并处理不在映射字典中的值\n", + "data_NGV['edition_rank'] = data_NGV['saas_edition_fmt'].map(edition_map).fillna(0).astype(int) # 缺失值用最高优先级填充\n", + "data_NGV['customer_type_rank'] = data_NGV['saas_customer_type'].map(customer_type_map).fillna(0).astype(int)\n", + "data_NGV['group_grade_rank'] = data_NGV['group_grade'].map(group_grade_map).fillna(0).astype(int)\n", + "\n", + "print(\"优先级映射完成\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:28:08.790102700Z", + "start_time": "2026-01-16T07:28:08.399298Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "最佳值查找完成\n" + ] + } + ], + "source": [ + "# ========== 找到每组中的最佳值 ==========\n", + "# 找到每组中 edition_rank 最小值对应的行\n", + "best_edition_idx = data_NGV.groupby('id_own_group')['edition_rank'].idxmin()\n", + "best_edition_rows = data_NGV.loc[best_edition_idx]\n", + "best_edition_rows['max_saas_edition'] = best_edition_rows['saas_edition_fmt']\n", + "\n", + "# 找到每组中 customer_type_rank 最小值对应的行\n", + "best_customer_type_idx = data_NGV.groupby('id_own_group')['customer_type_rank'].idxmin()\n", + "best_customer_type_rows = data_NGV.loc[best_customer_type_idx]\n", + "best_customer_type_rows['max_saas_customer_type'] = best_customer_type_rows['customer_type_rank'].apply(\n", + " lambda x: customer_type_order[x])\n", + "\n", + "# 找到每组中 group_grade_rank 最小值对应的行\n", + "best_group_grade_idx = data_NGV.groupby('id_own_group')['group_grade_rank'].idxmin()\n", + "best_group_grade_rows = data_NGV.loc[best_group_grade_idx]\n", + "best_group_grade_rows['max_group_grade'] = best_group_grade_rows['group_grade']\n", + "\n", + "print(\"最佳值查找完成\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:28:11.374632Z", + "start_time": "2026-01-16T07:28:11.141730700Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "最佳值合并完成,当前数据量: 45653 条\n" + ] + } + ], + "source": [ + "# ========== 合并最佳值回到原数据集 ==========\n", + "# 合并最佳值回到原数据集\n", + "best_values = (\n", + " best_edition_rows[['id_own_group', 'max_saas_edition']]\n", + " .merge(best_customer_type_rows[['id_own_group', 'max_saas_customer_type']], on='id_own_group',\n", + " how='outer')\n", + " .merge(best_group_grade_rows[['id_own_group', 'max_group_grade']], on='id_own_group', how='outer')\n", + ")\n", + "\n", + "# 将最佳值合并回原数据集\n", + "data_NGV = data_NGV.merge(best_values, on='id_own_group', how='left')\n", + "\n", + "print(f\"最佳值合并完成,当前数据量: {len(data_NGV)} 条\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:30:06.358604500Z", + "start_time": "2026-01-16T07:30:05.752861600Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "============================================================\n", + "调试信息:处理主店过期情况\n", + "============================================================\n", + "\n", + "当前 data_NGV 数据量: 45653 条\n", + "\n", + "【字段检查】\n", + "is_main_org 数据类型: object\n", + "is_main_org 唯一值: ['0', '1']\n", + "is_main_org 值分布:\n", + "is_main_org\n", + "1 37628\n", + "0 8025\n", + "Name: count, dtype: int64\n", + "\n", + "org_status 数据类型: object\n", + "org_status 唯一值: ['留存', '过期']\n", + "org_status 值分布:\n", + "org_status\n", + "留存 27985\n", + "过期 17668\n", + "Name: count, dtype: int64\n", + "\n", + "org_type 数据类型: object\n", + "org_type 唯一值: ['一般', '天猫']\n", + "org_type 值分布:\n", + "org_type\n", + "一般 42985\n", + "天猫 2668\n", + "Name: count, dtype: int64\n", + "\n", + "【步骤1: 筛选主店过期】\n", + "警告: is_main_org 是字符串类型,尝试转换为数值\n", + "条件筛选结果数量: 15065 条\n", + "主店过期数据量 (ngvv2): 15065 条\n", + "ngvv2 中的 id_own_group 数量: 15065 个\n", + "ngvv2 中的 id_own_group 示例: ['10545055917999655906', '10545055917999678943', '10545055917999702656', '10545055917999726421', '10545055917999791008', '10545055917999907421', '10545055917999958815', '10545055917999963314', '10545055917999973061', '10545055918000062921']\n", + "\n", + "【步骤2: 筛选分店留存】\n", + "data_NGV_V2 初始数据量: 45653 条\n", + "\n", + "area_manager 唯一值数量: 16\n", + "area_manager 值分布(前10):\n", + "area_manager\n", + "肖军 10824\n", + "景东强 8408\n", + "陈庆伟 8322\n", + "张凯 8269\n", + "关磊 7028\n", + "孙玉蕾 2006\n", + "殷昊 556\n", + "王涛 161\n", + "刘伟 52\n", + " 8\n", + "Name: count, dtype: int64\n", + "\n", + "各条件筛选结果:\n", + " org_type == '一般': 42985 条\n", + " org_status == '留存': 27985 条\n", + " area_manager != '殷昊': 45097 条\n", + " area_manager != '孙玉蕾': 43647 条\n", + " is_main_org != 1: 8025 条\n", + "\n", + "所有条件合并后数据量: 4882 条\n", + "data_NGV_V2_filtered 中的 id_own_group 数量: 1564 个\n", + "data_NGV_V2_filtered 中的 id_own_group 示例: ['10545055917999659357', '10545055917999659357', '10545055917999688607', '10545055917999688607', '10545055917999688607', '10545055917999719687', '10545055917999791008', '10545055917999791008', '10545055917999995278', '10545055918000106937']\n", + "\n", + "【步骤3: 检查 id_own_group 交集】\n", + "ngvv2 中的 id_own_group 数量: 15065\n", + "data_NGV_V2_filtered 中的 id_own_group 数量: 1564\n", + "交集数量: 316\n", + "交集中的 id_own_group 示例: ['11240984669917478021', '10546172455175803787', '10546172455166018835', '10546172455220322161', '10546443563657780587', '10546172455213602644', '10546443563816611858', '11240984669917430021', '11240984669917329620', '11240984669917352563']\n", + "\n", + "【步骤4: 最终过滤】\n", + "过滤后的数据量: 468 条\n", + "\n", + "============================================================\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\hp_z66\\AppData\\Local\\Temp\\ipykernel_14280\\4275068286.py:100: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " data_NGV_V2_filtered['exists_in_ngvv2'] = data_NGV_V2_filtered['id_own_group'].isin(ngvv2['id_own_group'])\n" + ] + } + ], + "source": [ + "# ========== 处理主店过期的情况 ==========\n", + "# 调试信息:检查数据状态\n", + "print(\"=\" * 60)\n", + "print(\"调试信息:处理主店过期情况\")\n", + "print(\"=\" * 60)\n", + "print(f\"\\n当前 data_NGV 数据量: {len(data_NGV)} 条\")\n", + "\n", + "# 检查关键字段的数据类型和唯一值\n", + "print(f\"\\n【字段检查】\")\n", + "print(f\"is_main_org 数据类型: {data_NGV['is_main_org'].dtype}\")\n", + "print(f\"is_main_org 唯一值: {sorted(data_NGV['is_main_org'].unique())}\")\n", + "print(f\"is_main_org 值分布:\\n{data_NGV['is_main_org'].value_counts()}\")\n", + "\n", + "print(f\"\\norg_status 数据类型: {data_NGV['org_status'].dtype}\")\n", + "print(f\"org_status 唯一值: {sorted(data_NGV['org_status'].unique())}\")\n", + "print(f\"org_status 值分布:\\n{data_NGV['org_status'].value_counts()}\")\n", + "\n", + "print(f\"\\norg_type 数据类型: {data_NGV['org_type'].dtype}\")\n", + "print(f\"org_type 唯一值: {sorted(data_NGV['org_type'].unique())}\")\n", + "print(f\"org_type 值分布:\\n{data_NGV['org_type'].value_counts()}\")\n", + "\n", + "# 步骤1: 筛选主店过期的情况\n", + "print(f\"\\n【步骤1: 筛选主店过期】\")\n", + "# 确保 is_main_org 是数值类型\n", + "if data_NGV['is_main_org'].dtype == 'object':\n", + " print(\"警告: is_main_org 是字符串类型,尝试转换为数值\")\n", + " data_NGV['is_main_org'] = pd.to_numeric(data_NGV['is_main_org'], errors='coerce')\n", + "\n", + "condition = (data_NGV['is_main_org'] == 1) & (data_NGV['org_status'] == '过期')\n", + "print(f\"条件筛选结果数量: {condition.sum()} 条\")\n", + "\n", + "ngvv2 = data_NGV[condition]\n", + "print(f\"主店过期数据量 (ngvv2): {len(ngvv2)} 条\")\n", + "\n", + "if len(ngvv2) > 0:\n", + " print(f\"ngvv2 中的 id_own_group 数量: {ngvv2['id_own_group'].nunique()} 个\")\n", + " print(f\"ngvv2 中的 id_own_group 示例: {ngvv2['id_own_group'].head(10).tolist()}\")\n", + "else:\n", + " print(\"⚠️ 警告: ngvv2 为空,没有主店过期的情况!\")\n", + "\n", + "# 步骤2: 检查分店留存的情况\n", + "print(f\"\\n【步骤2: 筛选分店留存】\")\n", + "# 在合并最佳值之前保存原始数据副本(重要!)\n", + "data_NGV_V2 = data_NGV.copy()\n", + "print(f\"data_NGV_V2 初始数据量: {len(data_NGV_V2)} 条\")\n", + "\n", + "# 检查 area_manager 字段\n", + "print(f\"\\narea_manager 唯一值数量: {data_NGV_V2['area_manager'].nunique()}\")\n", + "print(f\"area_manager 值分布(前10):\\n{data_NGV_V2['area_manager'].value_counts().head(10)}\")\n", + "\n", + "# 确保 is_main_org 是数值类型\n", + "if data_NGV_V2['is_main_org'].dtype == 'object':\n", + " data_NGV_V2['is_main_org'] = pd.to_numeric(data_NGV_V2['is_main_org'], errors='coerce')\n", + "\n", + "# 逐步检查每个条件\n", + "cond1 = (data_NGV_V2['org_type'] == \"一般\")\n", + "cond2 = (data_NGV_V2['org_status'] == '留存')\n", + "cond3 = (data_NGV_V2['area_manager'] != '殷昊')\n", + "cond4 = (data_NGV_V2['area_manager'] != '孙玉蕾')\n", + "cond5 = (data_NGV_V2['is_main_org'] != 1)\n", + "\n", + "print(f\"\\n各条件筛选结果:\")\n", + "print(f\" org_type == '一般': {cond1.sum()} 条\")\n", + "print(f\" org_status == '留存': {cond2.sum()} 条\")\n", + "print(f\" area_manager != '殷昊': {cond3.sum()} 条\")\n", + "print(f\" area_manager != '孙玉蕾': {cond4.sum()} 条\")\n", + "print(f\" is_main_org != 1: {cond5.sum()} 条\")\n", + "\n", + "data_NGV_V2['条件'] = cond1 & cond2 & cond3 & cond4 & cond5\n", + "data_NGV_V2_filtered = data_NGV_V2.loc[data_NGV_V2[\"条件\"]]\n", + "print(f\"\\n所有条件合并后数据量: {len(data_NGV_V2_filtered)} 条\")\n", + "\n", + "if len(data_NGV_V2_filtered) > 0:\n", + " print(f\"data_NGV_V2_filtered 中的 id_own_group 数量: {data_NGV_V2_filtered['id_own_group'].nunique()} 个\")\n", + " print(f\"data_NGV_V2_filtered 中的 id_own_group 示例: {data_NGV_V2_filtered['id_own_group'].head(10).tolist()}\")\n", + "\n", + "# 步骤3: 检查交集\n", + "print(f\"\\n【步骤3: 检查 id_own_group 交集】\")\n", + "if len(ngvv2) > 0 and len(data_NGV_V2_filtered) > 0:\n", + " ngvv2_groups = set(ngvv2['id_own_group'].unique())\n", + " v2_groups = set(data_NGV_V2_filtered['id_own_group'].unique())\n", + " intersection = ngvv2_groups & v2_groups\n", + " \n", + " print(f\"ngvv2 中的 id_own_group 数量: {len(ngvv2_groups)}\")\n", + " print(f\"data_NGV_V2_filtered 中的 id_own_group 数量: {len(v2_groups)}\")\n", + " print(f\"交集数量: {len(intersection)}\")\n", + " \n", + " if len(intersection) > 0:\n", + " print(f\"交集中的 id_own_group 示例: {list(intersection)[:10]}\")\n", + " else:\n", + " print(\"⚠️ 警告: 没有交集!这可能是问题所在。\")\n", + " print(f\"ngvv2 中的前10个 id_own_group: {list(ngvv2_groups)[:10]}\")\n", + " print(f\"data_NGV_V2_filtered 中的前10个 id_own_group: {list(v2_groups)[:10]}\")\n", + "else:\n", + " print(\"⚠️ 警告: ngvv2 或 data_NGV_V2_filtered 为空,无法检查交集\")\n", + "\n", + "# 步骤4: 过滤存在的记录\n", + "print(f\"\\n【步骤4: 最终过滤】\")\n", + "if len(ngvv2) > 0:\n", + " data_NGV_V2_filtered['exists_in_ngvv2'] = data_NGV_V2_filtered['id_own_group'].isin(ngvv2['id_own_group'])\n", + " filtered_data = data_NGV_V2_filtered[data_NGV_V2_filtered['exists_in_ngvv2']]\n", + " print(f\"过滤后的数据量: {len(filtered_data)} 条\")\n", + " \n", + " if len(filtered_data) == 0:\n", + " print(\"\\n❌ 问题诊断:\")\n", + " print(\" 过滤后数据为空,可能的原因:\")\n", + " print(\" 1. ngvv2 为空(没有主店过期的情况)\")\n", + " print(\" 2. data_NGV_V2_filtered 为空(没有满足条件的分店留存数据)\")\n", + " print(\" 3. 两者的 id_own_group 没有交集\")\n", + " print(\"\\n建议:\")\n", + " print(\" - 检查数据源是否正确\")\n", + " print(\" - 检查字段值是否匹配(注意数据类型和格式)\")\n", + " print(\" - 检查是否有主店过期但分店留存的情况\")\n", + "else:\n", + " print(\"⚠️ 警告: ngvv2 为空,无法进行过滤\")\n", + " filtered_data = pd.DataFrame() # 创建空DataFrame\n", + "\n", + "print(\"\\n\" + \"=\" * 60)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:30:16.487181500Z", + "start_time": "2026-01-16T07:30:16.440099400Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "排序去重后数据量: 316 条\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\hp_z66\\AppData\\Local\\Temp\\ipykernel_14280\\2835892650.py:5: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " filtered_data['sort_key'] = filtered_data['saas_edition_fmt'].map(fixed_order_map)\n" + ] + } + ], + "source": [ + "# ========== 对过滤数据进行排序和去重 ==========\n", + "fixed_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']\n", + "\n", + "fixed_order_map = {edition: index for index, edition in enumerate(fixed_order)}\n", + "filtered_data['sort_key'] = filtered_data['saas_edition_fmt'].map(fixed_order_map)\n", + "filtered_data = filtered_data.sort_values(by='sort_key').drop('sort_key', axis=1)\n", + "\n", + "result = filtered_data.drop_duplicates(subset='id_own_group', keep='first')\n", + "\n", + "print(f\"排序去重后数据量: {len(result)} 条\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ========== 合并主店留存数据和分店数据 ==========\n", + "data_NGV['条件'] = (data_NGV['org_type'] == \"一般\") & (data_NGV['org_status'] == '留存') & (\n", + " data_NGV['area_manager'] != '殷昊') & (\n", + " data_NGV['area_manager'] != '孙玉蕾') & (\n", + " data_NGV['is_main_org'] == 1)\n", + "data_NGV = data_NGV.loc[data_NGV[\"条件\"]]\n", + "\n", + "data_NGV = pd.concat([data_NGV, result], axis=0)\n", + "data_details = data_NGV.copy()\n", + "\n", + "# 重置索引\n", + "data_details = data_details.reset_index(drop=True)\n", + "\n", + "print(f\"合并后数据量: {len(data_details)} 条\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:30:21.600199200Z", + "start_time": "2026-01-16T07:30:20.828225500Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "日期计算后数据量: 9845 条\n", + "需要扩展的数据行数: 9845 条\n" + ] + } + ], + "source": [ + "# ========== 判断日期差并计算年数 ==========\n", + "# 判断A列的日期是否大于B列的日期730天,如果是的话,将B列的值设置为天数差\n", + "data_details['条件'] = data_details.apply(\n", + " lambda row: (\n", + " (pd.to_datetime(row['A']) - pd.to_datetime(row['B'])).days\n", + " if pd.to_datetime(row['A']) - pd.to_datetime(row['B']) >= pd.Timedelta(days=730)\n", + " else 0\n", + " ),\n", + " axis=1\n", + ")\n", + "data_details = data_details.loc[data_details[\"条件\"] > 0]\n", + "\n", + "# 定义一个函数,用于将数字除以365并取整数\n", + "def divide_by_365(x):\n", + " if isinstance(x, (int, float)):\n", + " return int(x / 365)\n", + " else:\n", + " return x\n", + "\n", + "# 使用apply函数将divide_by_365函数应用到DataFrame的列\n", + "data_details['年'] = data_details['条件'].apply(divide_by_365)\n", + "\n", + "# 重置索引\n", + "data_details = data_details.reset_index(drop=True)\n", + "\n", + "print(f\"日期计算后数据量: {len(data_details)} 条\")\n", + "print(f\"需要扩展的数据行数: {len(data_details[data_details['年'] > 1])} 条\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ========== 扩展数据:根据年数复制行并修改日期 ==========\n", + "# 创建一个新的空的DataFrame\n", + "new_df = pd.DataFrame()\n", + "\n", + "# 遍历原始DataFrame的每一行\n", + "for index, row in data_details.iterrows():\n", + " # 根据年数来决定复制的次数\n", + " if row[\"renew_date\"] != \"2024-02-29\":\n", + " for i_new in range(1, row['年']):\n", + " # 修改日期\n", + " row_new = row.copy()\n", + " c = row_new[\"renew_date\"]\n", + " date_obj = datetime.strptime(c, \"%Y-%m-%d\")\n", + " new_year = date_obj.year + i_new\n", + " new_date_obj = date_obj.replace(year=new_year)\n", + " new_c = new_date_obj.strftime(\"%Y-%m-%d\")\n", + " row_new[\"renew_date\"] = new_c\n", + " # 将当前行添加到新的DataFrame中\n", + " new_df = pd.concat([new_df, pd.DataFrame([row_new])], ignore_index=True)\n", + "\n", + "print(f\"扩展后的新数据量: {len(new_df)} 条\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-16T07:39:28.813848800Z", + "start_time": "2026-01-16T07:39:28.255902200Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "合并后总数据量: 39599 条\n" + ] + } + ], + "source": [ + "# ========== 合并原始数据和扩展数据 ==========\n", + "# 合并两个DataFrame\n", + "merged_df = pd.concat([data_NGV, new_df], axis=0, ignore_index=True)\n", + "data_details = merged_df.copy() # 替换名称\n", + "\n", + "data_details_not_null = data_details[data_details['renew_date'].notnull()]\n", + "# 重置索引\n", + "data_details_not_null = data_details_not_null.reset_index(drop=True)\n", + "data_details = data_details_not_null.copy() # 替换名称 v2\n", + "\n", + "print(f\"合并后总数据量: {len(data_details)} 条\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ========== 最终过滤:排除创建时间等于续约时间的记录 ==========\n", + "data_details['saas_create_time'] = data_details['saas_create_time'].str[:4] # 截取前4位(年份)\n", + "data_details['renew_date_new'] = data_details['renew_date'].str[:4] # 截取前4位(年份)\n", + "data_details = data_details[\n", + " data_details['saas_create_time'] != data_details['renew_date_new']] # 过滤掉等于renew_date的行\n", + "\n", + "data_details = data_details.reset_index(drop=True)\n", + "\n", + "logger.info(f\"过滤后的数据长度为: {len(data_details)}\")\n", + "print(f\"\\n========== 数据处理完成 ==========\")\n", + "print(f\"最终数据量: {len(data_details)} 条\")\n", + "print(f\"处理完成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 数据验证和检查\n", + "\n", + "可以在这里添加数据验证代码,检查处理结果的正确性\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "start_time": "2026-01-16T09:00:49.656903800Z" + } + }, + "outputs": [], + "source": [ + "# ========== 数据验证 ==========\n", + "# 查看数据基本信息\n", + "print(\"数据基本信息:\")\n", + "print(f\"数据形状: {data_details.shape}\")\n", + "print(f\"\\n数据列名:\")\n", + "print(data_details.columns.tolist())\n", + "\n", + "# 查看前几行数据\n", + "print(\"\\n前5行数据:\")\n", + "print(data_details.head())\n", + "\n", + "# 检查关键字段的数据分布\n", + "if 'saas_edition_fmt' in data_details.columns:\n", + " print(\"\\n版本分布:\")\n", + " print(data_details['saas_edition_fmt'].value_counts())\n", + "\n", + "if 'org_status' in data_details.columns:\n", + " print(\"\\n组织状态分布:\")\n", + " print(data_details['org_status'].value_counts())\n", + "\n", + "# 可以保存到CSV文件进行进一步检查\n", + "# data_details.to_csv(\"处理后的数据.csv\", index=False, encoding='utf-8-sig')\n", + "# print(\"\\n数据已保存到: 处理后的数据.csv\")\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/test/省市同步到bi.py b/test/省市同步到bi.py new file mode 100644 index 0000000..5d36681 --- /dev/null +++ b/test/省市同步到bi.py @@ -0,0 +1,200 @@ +import pandas as pd +import datetime +from config import Config +from api import API +import pymysql # 使用 pymysql 替代 mysql.connector +from back_ground_module import CommonModule +import os +import mysql.connector +import pandas as pd +import json +import numpy as np +import mysql.connector +from mysql.connector import Error +from log_config import configure_task_logger, configure_error_task_logger +import math + +logger = configure_task_logger() +error_task_logger = configure_error_task_logger() +output_dir = "output" # 设置输出目录 +os.makedirs(output_dir, exist_ok=True) +common_module = CommonModule() +api_instance = API() + + +class ProvinceCityPersonRelationToBI: + def __init__(self): + self.pvc_data = None + self.field_mapping = { + "省": "_widget_1734677164861", + "市": "_widget_1734677164862", + "运营顾问": "_widget_1734677164864", + "区域经理": "_widget_1734677164865", + "运营专家": "_widget_1734677164866", + "战区": "_widget_1734677164867", + "新签回访客服": "_widget_1734677164868", + "续约回访客服": "_widget_1734677164869", + "异常待办客服": "_widget_1734677164870", + "日常回访客服": "_widget_1734677164871", + } + + def load_all_data(self): + payload = {"api_key": "675b900991ad2491c69389ca", + "entry_id": "676512ac3e54dc3159460c0a", + } + pvc_data = api_instance.entry_data_list(payload) + self.pvc_data = pvc_data.get("data") # api请求格式,将数据封装在data字典里 + + def data_process(self): + df = pd.DataFrame(self.pvc_data) + # 反转映射字典 + reverse_mapping = {v: k for k, v in self.field_mapping.items()} + # 1.列明替换 + df.columns = [reverse_mapping.get(col, col) for col in df.columns] + + # 2.成员字段取值 + user_columns = ["运营顾问", "区域经理", "运营专家", "新签回访客服", "续约回访客服", + "异常待办客服", "日常回访客服"] + + for col in user_columns: + df[col] = df[col].map(lambda x: x.get("name", "") if isinstance(x, dict) else "") + + # 3.根据省市去重 + df = df.drop_duplicates(subset=['省', '市']) + + return df + + def clear_table_data(self): + """ + 清空指定 MySQL 表的数据。 + 参数已写死在函数内部,直接调用即可。 + """ + # 数据库连接信息 + HS_DB_Config = { + 'host': "f6-public.rwlb.rds.aliyuncs.com", + 'user': "rw_operation_data_relay", + 'password': "m+q5Z4%IVuF9bf", + 'database': "f6operation_data_relay" + } + table_name = "province_city_person_relation_to_bi" # 要清空的表名 + + connection = None + try: + # 建立数据库连接 + connection = mysql.connector.connect( + host=HS_DB_Config["host"], + user=HS_DB_Config["user"], + password=HS_DB_Config["password"], + database=HS_DB_Config["database"] + ) + if connection.is_connected(): + cursor = connection.cursor() + + # 使用TRUNCATE清空表数据 + cursor.execute(f"TRUNCATE TABLE {table_name}") + connection.commit() + + logger.info(f"成功清空表 {table_name} 中的所有数据") + + except Error as e: + error_task_logger.error(f"清空表时发生错误: {e}") + if connection and connection.is_connected(): + connection.rollback() + finally: + if connection and connection.is_connected(): + cursor.close() + connection.close() + logger.info("数据库连接已关闭") + + def write_to_bi(self, df): + HS_DB_Config = Config.HS_DB_Config + table_name = "province_city_person_relation_to_bi" + chunk_size = 1000 # 每批插入 1000 行 + + # 清理 DataFrame 中的 NaN/None 等值 + df = df.replace([None, np.nan, pd.NA, 'nan', 'NaN', 'NAN', ''], None) + + connection = mysql.connector.connect( + host=HS_DB_Config["host"], + user=HS_DB_Config["user"], + password=HS_DB_Config["password"], + database=HS_DB_Config["database"] + ) + cursor = connection.cursor() + + try: + # 获取数据库表的列名 + cursor.execute(f"SHOW COLUMNS FROM `{table_name}`") + db_columns = [col[0] for col in cursor.fetchall()] + + # 保留与数据库匹配的列 + filtered_df = df[df.columns.intersection(db_columns)] + if filtered_df.empty: + print("DataFrame 中没有与数据库表结构匹配的列。") + return + + # 处理 dict/list 类型字段:转为 JSON 字符串 + filtered_df = filtered_df.copy() + for col in filtered_df.columns: + if filtered_df[col].apply(lambda x: isinstance(x, (dict, list)) if x is not None else False).any(): + filtered_df[col] = filtered_df[col].apply( + lambda x: json.dumps(x, ensure_ascii=False) if x is not None else x + ) + + # 构建 INSERT 语句(只构建一次) + columns = [f"`{col}`" for col in filtered_df.columns] + placeholders = ', '.join(['%s'] * len(columns)) + insert_sql = f"INSERT INTO `{table_name}` ({', '.join(columns)}) VALUES ({placeholders})" + + total_rows = len(filtered_df) + num_chunks = math.ceil(total_rows / chunk_size) + + for i in range(num_chunks): + start_idx = i * chunk_size + end_idx = min(start_idx + chunk_size, total_rows) + chunk_df = filtered_df.iloc[start_idx:end_idx] + + # 转为元组列表 + data_to_insert = [ + tuple(row) for row in chunk_df.values + ] + + # 批量执行(executemany 更高效) + cursor.executemany(insert_sql, data_to_insert) + + connection.commit() + logger.info(f"成功写入 {total_rows} 条记录到 {table_name} 表中(分 {num_chunks} 批)。") + + except Exception as e: + error_task_logger.error(f"写入数据库时发生错误: {e}", exc_info=True) + connection.rollback() + finally: + cursor.close() + connection.close() + + def main(self): + task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + try: + logger.info("任务开始") + # step1: 获取数据 + self.load_all_data() + logger.info("加载数据完成") + # step2:数据处理 + df = self.data_process() + # df.to_csv(os.path.join(output_dir, "new_dealer_service_order_to_bi.csv")) + logger.info("数据处理完成") + # step3:数据库删除 + self.clear_table_data() + logger.info("目标数据库已清空") + # step4:数据写入BI + self.write_to_bi(df) + logger.info("数据已写入数据库中") + common_module.send_task_status(task_start_time, "省市区人员关系表转BI") + except Exception as e: + error_task_logger.error(f"省市区人员关系表转BI发生错误{e}") + common_module.send_task_error(task_start_time, "省市区人员关系表转BI", str(e)) + + +if __name__ == '__main__': + province_city_person_relation_to_bi = ProvinceCityPersonRelationToBI() + province_city_person_relation_to_bi.main() diff --git a/test/续约回访90-180天调试.ipynb b/test/续约回访90-180天调试.ipynb new file mode 100644 index 0000000..d1518c9 --- /dev/null +++ b/test/续约回访90-180天调试.ipynb @@ -0,0 +1,1217 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 续约回访90-180天调试分析\n", + "\n", + "本notebook用于调试续约回访派发数据为空的问题,每一步都会保存CSV以便分析。\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤1: 导入必要的库和初始化\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "当前工作目录: d:\\Idea Project\\SaaS_V1.7\\test\n", + "项目根目录: d:\\Idea Project\\SaaS_V1.7\n", + "Python路径: ['d:\\\\Idea Project\\\\SaaS_V1.7', 'd:\\\\Program Files\\\\anaconda3\\\\envs\\\\SaaS\\\\python313.zip', 'd:\\\\Program Files\\\\anaconda3\\\\envs\\\\SaaS\\\\DLLs']...\n", + "输出目录: d:\\Idea Project\\SaaS_V1.7\\back_ground_module\\output\\debug_revisit_renew\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "import time\n", + "import requests\n", + "\n", + "# 添加项目根目录到Python路径\n", + "# 方法1: 如果notebook在test目录下,向上找一级\n", + "current_dir = os.getcwd()\n", + "if os.path.basename(current_dir) == 'test':\n", + " project_root = os.path.dirname(current_dir)\n", + "else:\n", + " # 方法2: 向上查找直到找到api.py文件\n", + " project_root = current_dir\n", + " while project_root != os.path.dirname(project_root):\n", + " if os.path.exists(os.path.join(project_root, 'api.py')):\n", + " break\n", + " project_root = os.path.dirname(project_root)\n", + "\n", + "if project_root not in sys.path:\n", + " sys.path.insert(0, project_root)\n", + "print(f\"当前工作目录: {current_dir}\")\n", + "print(f\"项目根目录: {project_root}\")\n", + "print(f\"Python路径: {sys.path[:3]}...\") # 只显示前3个路径\n", + "\n", + "from api import API\n", + "from back_ground_module import CommonModule\n", + "import pandas as pd\n", + "import datetime\n", + "import re\n", + "from log_config import configure_task_logger, configure_error_task_logger\n", + "\n", + "api_instance = API()\n", + "common_module = CommonModule()\n", + "logger = configure_task_logger()\n", + "error_task_logger = configure_error_task_logger()\n", + "\n", + "# 设置输出目录(相对于当前notebook目录)\n", + "output_dir = \"output/debug_revisit_renew\"\n", + "os.makedirs(output_dir, exist_ok=True)\n", + "print(f\"输出目录: {os.path.abspath(output_dir)}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤2: 加载所有数据(单独执行,数据量较大)\n", + "\n", + "**注意:这一步数据量较大,请单独执行此单元**\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"开始加载所有数据...\")\n", + "print(f\"开始时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n", + "\n", + "# 省市区人员关系表\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676512ac3e54dc3159460c0a\"}\n", + "json_dict = api_instance.entry_data_list(payload)\n", + "if json_dict and \"data\" in json_dict:\n", + " json_list = json_dict.get(\"data\")\n", + " print(f\"省市区人员关系表: {len(json_list)} 条\")\n", + " pd.DataFrame(json_list).to_csv(f\"{output_dir}/01_省市区人员关系表.csv\", index=False, encoding='utf-8-sig')\n", + "else:\n", + " print(\"加载省市区人员关系表失败\")\n", + " json_list = []\n", + "\n", + "# 获取简道云员工id\n", + "payload = {\"api_key\": \"6694d3c4fcb69ca9a111a6c4\", \"entry_id\": \"6769204a1902c9341340a1bc\"}\n", + "staff_id = api_instance.entry_data_list(payload)\n", + "staff_id_list = staff_id.get(\"data\")\n", + "print(f\"简道云员工id: {len(staff_id_list)} 条\")\n", + "pd.DataFrame(staff_id_list).to_csv(f\"{output_dir}/02_简道云员工id.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取权限表信息\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"675b96c14e839f90fef1647c\"}\n", + "permissions_table = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"权限表: {len(permissions_table)} 条\")\n", + "pd.DataFrame(permissions_table).to_csv(f\"{output_dir}/03_权限表.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取NGV数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"675bb02bd2d53c2034c665e4\"}\n", + "NGV_data_list = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"NGV数据: {len(NGV_data_list)} 条\")\n", + "pd.DataFrame(NGV_data_list).to_csv(f\"{output_dir}/04_NGV数据.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取服务提醒-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676bb7bda3029720f1083e99\"}\n", + "service_remind = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"服务提醒: {len(service_remind)} 条\")\n", + "pd.DataFrame(service_remind).to_csv(f\"{output_dir}/05_服务提醒.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取智能检测-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"676bb99649ab3ac975af6e39\"}\n", + "Smart_detection = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"智能检测: {len(Smart_detection)} 条\")\n", + "pd.DataFrame(Smart_detection).to_csv(f\"{output_dir}/06_智能检测.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取功能使用情况表\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"6763bbf657bd8fb76fcb41b2\"}\n", + "get_feature_usage = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "print(f\"功能使用情况: {len(get_feature_usage)} 条\")\n", + "pd.DataFrame(get_feature_usage).to_csv(f\"{output_dir}/07_功能使用情况.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取保单识别表\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"6773a60d30ed87ff9f68d3c5\"}\n", + "policy_recognition = api_instance.entry_data_list(payload).get(\"data\")\n", + "print(f\"保单识别: {len(policy_recognition)} 条\")\n", + "widget_list = [item['_widget_1735632397600'] for item in policy_recognition]\n", + "pd.DataFrame(policy_recognition).to_csv(f\"{output_dir}/08_保单识别.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取私域小程序-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e0f0fae622896749ba5087\"}\n", + "private_domain = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "print(f\"私域小程序: {len(private_domain)} 条\")\n", + "pd.DataFrame(private_domain).to_csv(f\"{output_dir}/09_私域小程序.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取公域小程序-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e0c702c8f603b997980999\"}\n", + "public_domain = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "public_domain_list = [item['_widget_1742784257506'] for item in public_domain]\n", + "print(f\"公域小程序: {len(public_domain)} 条\")\n", + "pd.DataFrame(public_domain).to_csv(f\"{output_dir}/10_公域小程序.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取异业合作-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e24fdd8dfcfa918e17c30b\"}\n", + "different_industries = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "different_industries_list = [item['_widget_1742884829007'] for item in different_industries]\n", + "print(f\"异业合作: {len(different_industries)} 条\")\n", + "pd.DataFrame(different_industries).to_csv(f\"{output_dir}/11_异业合作.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取短信-数据支持表单数据\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"67e5107198ba1b20d5df3974\"}\n", + "groupnotification = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "print(f\"短信: {len(groupnotification)} 条\")\n", + "pd.DataFrame(groupnotification).to_csv(f\"{output_dir}/12_短信.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 获取多公司过滤表\n", + "payload = {\"api_key\": \"675b900991ad2491c69389ca\", \"entry_id\": \"689bf5f8ba88a28cb0679ec9\"}\n", + "get_filter_company_list = api_instance.entry_data_list(payload).get(\"data\", [])\n", + "print(f\"多公司过滤表: {len(get_filter_company_list)} 条\")\n", + "pd.DataFrame(get_filter_company_list).to_csv(f\"{output_dir}/13_多公司过滤表.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "print(f\"\\n数据加载完成!\")\n", + "print(f\"结束时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤3: 获取节假日列表和计算date_one\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "节假日列表: 52 个日期\n", + "当前日期: 2026-01-17\n", + "遍历日期: 2026-01-16\n", + "遍历次数: 1\n", + "date_one = 1\n" + ] + } + ], + "source": [ + "def calculate_date_one(date_list, start_offset=0):\n", + " \"\"\"\n", + " 计算从当前日期(或指定偏移量的日期)开始,往前遍历遇到date_list中日期的次数。\n", + " \"\"\"\n", + " now_time = datetime.datetime.now() + datetime.timedelta(days=start_offset)\n", + " date_one = 1\n", + " print(\"当前日期:\", now_time.strftime(\"%Y-%m-%d\"))\n", + " \n", + " if now_time.strftime(\"%Y-%m-%d\") in date_list:\n", + " date_one = 0\n", + " print(\"开始次数:\", date_one)\n", + " else:\n", + " for i in range(1, 10):\n", + " new_date = now_time + datetime.timedelta(days=-i)\n", + " new_date_str = new_date.strftime(\"%Y-%m-%d\")\n", + " print(\"遍历日期:\", new_date_str)\n", + " if new_date_str in date_list:\n", + " date_one += 1\n", + " print(\"节假日期:\", new_date_str)\n", + " else:\n", + " break\n", + " \n", + " print(\"遍历次数:\", date_one)\n", + " return date_one\n", + "\n", + "# 获取节假日列表\n", + "date_list = common_module.get_holiday_list()\n", + "print(f\"节假日列表: {len(date_list)} 个日期\")\n", + "pd.Series(date_list).to_csv(f\"{output_dir}/14_节假日列表.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 计算date_one\n", + "date_one = calculate_date_one(date_list, start_offset=0)\n", + "print(f\"date_one = {date_one}\")\n", + "\n", + "# 保存date_one\n", + "pd.DataFrame([{\"date_one\": date_one}]).to_csv(f\"{output_dir}/15_date_one.csv\", index=False, encoding='utf-8-sig')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤4: 获取NGV明细数据\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "开始获取NGV明细数据...\n", + "开始时间: 2026-01-17 13:34:48\n", + "NGV明细数据: 45686 条\n", + "NGV明细数据列数: 141\n", + "已保存到: output/debug_revisit_renew/16_原始NGV数据.csv\n", + "结束时间: 2026-01-17 13:35:00\n" + ] + } + ], + "source": [ + "print(\"开始获取NGV明细数据...\")\n", + "print(f\"开始时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n", + "\n", + "data_NGV = common_module.get_ngv_details(days_back=1)\n", + "print(f\"NGV明细数据: {len(data_NGV)} 条\")\n", + "print(f\"NGV明细数据列数: {len(data_NGV.columns)}\")\n", + "\n", + "# 保存原始NGV数据\n", + "data_NGV.to_csv(f\"{output_dir}/16_原始NGV数据.csv\", index=False, encoding='utf-8-sig')\n", + "print(f\"已保存到: {output_dir}/16_原始NGV数据.csv\")\n", + "\n", + "print(f\"结束时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤5: 构建省市区索引\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "省市区索引构建完成: 3667 条\n" + ] + } + ], + "source": [ + "def build_index(json_list):\n", + " index = {}\n", + " for json_item in json_list:\n", + " try:\n", + " key = (json_item['_widget_1734677164861'], json_item['_widget_1734677164862'],\n", + " json_item['_widget_1734677164863']) # 省市区\n", + " if '_widget_1734677164871' not in json_item: # 日常回访客服\n", + " raise KeyError(\"缺少 '日常回访客服' 键\")\n", + " index[key] = json_item\n", + " except KeyError as e:\n", + " print(f\"警告:{e},跳过该条记录: {json_item}\")\n", + " continue\n", + " return index\n", + "\n", + "# 从CSV加载省市区人员关系表(步骤2的输出)\n", + "csv_path = f\"{output_dir}/01_省市区人员关系表.csv\"\n", + "if os.path.exists(csv_path):\n", + " print(f\"从CSV文件读取: {csv_path}\")\n", + " json_list_df = pd.read_csv(csv_path, encoding='utf-8-sig')\n", + " json_list = json_list_df.to_dict('records')\n", + " print(f\"读取到 {len(json_list)} 条记录\")\n", + "else:\n", + " print(f\"警告: 文件不存在 {csv_path},请先执行步骤2\")\n", + " json_list = []\n", + "\n", + "# 构建索引\n", + "index = build_index(json_list)\n", + "print(f\"省市区索引构建完成: {len(index)} 条\")\n", + "\n", + "# 保存索引信息\n", + "index_df = pd.DataFrame([{\"省\": k[0], \"市\": k[1], \"区\": k[2], \"数据\": str(v)} for k, v in index.items()])\n", + "index_df.to_csv(f\"{output_dir}/17_省市区索引.csv\", index=False, encoding='utf-8-sig')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤6: 获取多公司过滤列表\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "获取多公司过滤公司id\n", + "过滤公司条数: 19\n" + ] + } + ], + "source": [ + "print(\"获取多公司过滤公司id\")\n", + "# 从CSV加载多公司过滤表(步骤2的输出)\n", + "csv_path = f\"{output_dir}/13_多公司过滤表.csv\"\n", + "if os.path.exists(csv_path):\n", + " print(f\"从CSV文件读取: {csv_path}\")\n", + " get_filter_company_list_df = pd.read_csv(csv_path, encoding='utf-8-sig')\n", + " get_filter_company_list = get_filter_company_list_df.to_dict('records')\n", + " print(f\"读取到 {len(get_filter_company_list)} 条记录\")\n", + "else:\n", + " print(f\"警告: 文件不存在 {csv_path},请先执行步骤2\")\n", + " get_filter_company_list = []\n", + "\n", + "all_filter_company_list = []\n", + "for company in get_filter_company_list:\n", + " company_list = company.get(\"_widget_1755052002491\")\n", + " if company_list:\n", + " # 处理可能是字符串的情况\n", + " if isinstance(company_list, str):\n", + " import ast\n", + " try:\n", + " company_list = ast.literal_eval(company_list)\n", + " except:\n", + " continue\n", + " for company_item in company_list:\n", + " if company_item.get(\"_widget_1755052002496\") == \"否\":\n", + " all_filter_company_list.append(company_item.get(\"_widget_1755052002495\"))\n", + "\n", + "print(f\"过滤公司条数: {len(all_filter_company_list)}\")\n", + "pd.Series(all_filter_company_list).to_csv(f\"{output_dir}/18_过滤公司列表.csv\", index=False, encoding='utf-8-sig')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤7: 数据处理和过滤(第一部分:基础处理)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "过滤前数据量: 45686\n", + "过滤后数据量: 45667\n" + ] + } + ], + "source": [ + "# 从CSV加载NGV数据(步骤4的输出)\n", + "csv_path = f\"{output_dir}/16_原始NGV数据.csv\"\n", + "if os.path.exists(csv_path):\n", + " print(f\"从CSV文件读取: {csv_path}\")\n", + " data_NGV = pd.read_csv(csv_path, encoding='utf-8-sig')\n", + " print(f\"读取到 {len(data_NGV)} 条记录\")\n", + "else:\n", + " print(f\"警告: 文件不存在 {csv_path},请先执行步骤4\")\n", + " data_NGV = pd.DataFrame()\n", + "\n", + "# 将A列和B列的日期字符串转换为日期格式\n", + "data_NGV_processed = data_NGV.copy()\n", + "data_NGV_processed['A'] = pd.to_datetime(data_NGV_processed['expiry_time'])\n", + "data_NGV_processed['B'] = pd.to_datetime(data_NGV_processed['renew_date'])\n", + "\n", + "def replace_values(series):\n", + " return series.apply(lambda x: '' if pd.isna(x) or x in ['NA', 'None', ''] else x)\n", + "\n", + "# 处理字符串数据\n", + "data_NGV_processed = data_NGV_processed.apply(replace_values)\n", + "\n", + "# 过滤多公司\n", + "print(f\"过滤前数据量: {len(data_NGV_processed)}\")\n", + "data_NGV_processed = data_NGV_processed[~data_NGV_processed['id_own_group'].isin(all_filter_company_list)]\n", + "print(f\"过滤后数据量: {len(data_NGV_processed)}\")\n", + "\n", + "# 保存过滤后的数据\n", + "data_NGV_processed.to_csv(f\"{output_dir}/19_过滤多公司后数据.csv\", index=False, encoding='utf-8-sig')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤8: 数据处理和过滤(第二部分:优先级排序和最佳值计算)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "合并最佳值后数据量: 45667\n" + ] + } + ], + "source": [ + "# 从CSV加载前一步的数据(步骤7的输出)\n", + "csv_path = f\"{output_dir}/19_过滤多公司后数据.csv\"\n", + "if os.path.exists(csv_path):\n", + " print(f\"从CSV文件读取: {csv_path}\")\n", + " data_NGV_processed = pd.read_csv(csv_path, encoding='utf-8-sig')\n", + " # 重新转换日期列\n", + " data_NGV_processed['A'] = pd.to_datetime(data_NGV_processed['expiry_time'])\n", + " data_NGV_processed['B'] = pd.to_datetime(data_NGV_processed['renew_date'])\n", + " print(f\"读取到 {len(data_NGV_processed)} 条记录\")\n", + "else:\n", + " print(f\"警告: 文件不存在 {csv_path},请先执行步骤7\")\n", + " data_NGV_processed = pd.DataFrame()\n", + "\n", + "# 定义优先级顺序\n", + "edition_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']\n", + "customer_type_order = [\"F\", \"E\", \"D\", \"C\", \"B\", \"A\"]\n", + "group_grade_order = ['全国KA(FMVP)', '区域KA(MVP)', '重要客户(SVIP)', '普通客户(VIP)']\n", + "\n", + "# 创建映射字典\n", + "edition_map = {edition: idx for idx, edition in enumerate(edition_order)}\n", + "customer_type_map = {ctype: idx for idx, ctype in enumerate(customer_type_order)}\n", + "group_grade_map = {grade: idx for idx, grade in enumerate(group_grade_order)}\n", + "\n", + "# 添加用于排序的新列\n", + "data_NGV_processed['edition_rank'] = data_NGV_processed['saas_edition_fmt'].map(edition_map).fillna(0).astype(int)\n", + "data_NGV_processed['customer_type_rank'] = data_NGV_processed['saas_customer_type'].map(customer_type_map).fillna(0).astype(int)\n", + "data_NGV_processed['group_grade_rank'] = data_NGV_processed['group_grade'].map(group_grade_map).fillna(0).astype(int)\n", + "\n", + "# 找到每组中 edition_rank 最小值对应的行\n", + "best_edition_idx = data_NGV_processed.groupby('id_own_group')['edition_rank'].idxmin()\n", + "best_edition_rows = data_NGV_processed.loc[best_edition_idx]\n", + "best_edition_rows['max_saas_edition'] = best_edition_rows['saas_edition_fmt']\n", + "\n", + "# 找到每组中 customer_type_rank 最小值对应的行\n", + "best_customer_type_idx = data_NGV_processed.groupby('id_own_group')['customer_type_rank'].idxmin()\n", + "best_customer_type_rows = data_NGV_processed.loc[best_customer_type_idx]\n", + "best_customer_type_rows['max_saas_customer_type'] = best_customer_type_rows['customer_type_rank'].apply(\n", + " lambda x: customer_type_order[x])\n", + "\n", + "# 找到每组中 group_grade_rank 最小值对应的行\n", + "best_group_grade_idx = data_NGV_processed.groupby('id_own_group')['group_grade_rank'].idxmin()\n", + "best_group_grade_rows = data_NGV_processed.loc[best_group_grade_idx]\n", + "best_group_grade_rows['max_group_grade'] = best_group_grade_rows['group_grade']\n", + "\n", + "# 合并最佳值回到原数据集\n", + "best_values = (\n", + " best_edition_rows[['id_own_group', 'max_saas_edition']]\n", + " .merge(best_customer_type_rows[['id_own_group', 'max_saas_customer_type']], on='id_own_group', how='outer')\n", + " .merge(best_group_grade_rows[['id_own_group', 'max_group_grade']], on='id_own_group', how='outer')\n", + ")\n", + "\n", + "# 将最佳值合并回原数据集\n", + "data_NGV_processed = data_NGV_processed.merge(best_values, on='id_own_group', how='left')\n", + "\n", + "print(f\"合并最佳值后数据量: {len(data_NGV_processed)}\")\n", + "data_NGV_processed.to_csv(f\"{output_dir}/20_合并最佳值后数据.csv\", index=False, encoding='utf-8-sig')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤8.5: 字段数据类型检查(调试用)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "============================================================\n", + "字段数据类型和值检查\n", + "============================================================\n", + "\n", + "当前 data_NGV_processed 数据量: 45667 条\n", + "\n", + "【is_main_org 字段检查】\n", + "数据类型: object\n", + "唯一值: ['0', '1']\n", + "值分布:\n", + "is_main_org\n", + "1 37637\n", + "0 8030\n", + "Name: count, dtype: int64\n", + "示例值(前5个): ['1', '0', '0', '0', '1']\n", + "\n", + "【org_status 字段检查】\n", + "数据类型: object\n", + "唯一值: ['留存', '过期']\n", + "值分布:\n", + "org_status\n", + "留存 27997\n", + "过期 17670\n", + "Name: count, dtype: int64\n", + "\n", + "【org_type 字段检查】\n", + "数据类型: object\n", + "唯一值: ['一般', '天猫']\n", + "值分布:\n", + "org_type\n", + "一般 42998\n", + "天猫 2669\n", + "Name: count, dtype: int64\n", + "\n", + "【area_manager 字段检查】\n", + "数据类型: object\n", + "唯一值数量: 16\n", + "值分布(前10):\n", + "area_manager\n", + "肖军 10795\n", + "景东强 8353\n", + "陈庆伟 8301\n", + "张凯 8232\n", + "关磊 6994\n", + "孙玉蕾 2007\n", + "殷昊 745\n", + "王涛 161\n", + "刘伟 52\n", + " 8\n", + "Name: count, dtype: int64\n", + "是否包含'殷昊': True\n", + "是否包含'孙玉蕾': True\n", + "\n", + "【条件匹配测试】\n", + "警告: is_main_org 是字符串类型,需要转换为数值或使用字符串比较\n", + "使用数值比较 (== 1): 15066 条\n", + "使用字符串比较 (== '1'): 15066 条\n", + "\n", + "============================================================\n" + ] + } + ], + "source": [ + "# 从CSV加载前一步的数据(步骤8的输出)\n", + "csv_path = f\"{output_dir}/20_合并最佳值后数据.csv\"\n", + "if os.path.exists(csv_path):\n", + " print(f\"从CSV文件读取: {csv_path}\")\n", + " data_NGV_processed = pd.read_csv(csv_path, encoding='utf-8-sig')\n", + " print(f\"读取到 {len(data_NGV_processed)} 条记录\")\n", + "else:\n", + " print(f\"警告: 文件不存在 {csv_path},请先执行步骤8\")\n", + " data_NGV_processed = pd.DataFrame()\n", + "\n", + "# 检查关键字段的数据类型和实际值\n", + "print(\"=\" * 60)\n", + "print(\"字段数据类型和值检查\")\n", + "print(\"=\" * 60)\n", + "print(f\"\\n当前 data_NGV_processed 数据量: {len(data_NGV_processed)} 条\")\n", + "\n", + "# 检查 is_main_org\n", + "print(f\"\\n【is_main_org 字段检查】\")\n", + "print(f\"数据类型: {data_NGV_processed['is_main_org'].dtype}\")\n", + "print(f\"唯一值: {sorted(data_NGV_processed['is_main_org'].unique())}\")\n", + "print(f\"值分布:\\n{data_NGV_processed['is_main_org'].value_counts()}\")\n", + "print(f\"示例值(前5个): {data_NGV_processed['is_main_org'].head().tolist()}\")\n", + "\n", + "# 检查 org_status\n", + "print(f\"\\n【org_status 字段检查】\")\n", + "print(f\"数据类型: {data_NGV_processed['org_status'].dtype}\")\n", + "print(f\"唯一值: {sorted(data_NGV_processed['org_status'].unique())}\")\n", + "print(f\"值分布:\\n{data_NGV_processed['org_status'].value_counts()}\")\n", + "\n", + "# 检查 org_type\n", + "print(f\"\\n【org_type 字段检查】\")\n", + "print(f\"数据类型: {data_NGV_processed['org_type'].dtype}\")\n", + "print(f\"唯一值: {sorted(data_NGV_processed['org_type'].unique())}\")\n", + "print(f\"值分布:\\n{data_NGV_processed['org_type'].value_counts()}\")\n", + "\n", + "# 检查 area_manager\n", + "print(f\"\\n【area_manager 字段检查】\")\n", + "print(f\"数据类型: {data_NGV_processed['area_manager'].dtype}\")\n", + "print(f\"唯一值数量: {data_NGV_processed['area_manager'].nunique()}\")\n", + "print(f\"值分布(前10):\\n{data_NGV_processed['area_manager'].value_counts().head(10)}\")\n", + "print(f\"是否包含'殷昊': {'殷昊' in data_NGV_processed['area_manager'].values}\")\n", + "print(f\"是否包含'孙玉蕾': {'孙玉蕾' in data_NGV_processed['area_manager'].values}\")\n", + "\n", + "# 测试条件匹配\n", + "print(f\"\\n【条件匹配测试】\")\n", + "# 测试主店过期条件\n", + "if data_NGV_processed['is_main_org'].dtype == 'object':\n", + " print(\"警告: is_main_org 是字符串类型,需要转换为数值或使用字符串比较\")\n", + " # 尝试转换为数值\n", + " is_main_org_numeric = pd.to_numeric(data_NGV_processed['is_main_org'], errors='coerce')\n", + " condition1_test = (is_main_org_numeric == 1) & (data_NGV_processed['org_status'] == '过期')\n", + " condition1_test_str = (data_NGV_processed['is_main_org'] == '1') & (data_NGV_processed['org_status'] == '过期')\n", + " print(f\"使用数值比较 (== 1): {condition1_test.sum()} 条\")\n", + " print(f\"使用字符串比较 (== '1'): {condition1_test_str.sum()} 条\")\n", + "else:\n", + " condition1_test = (data_NGV_processed['is_main_org'] == 1) & (data_NGV_processed['org_status'] == '过期')\n", + " print(f\"主店过期条件匹配: {condition1_test.sum()} 条\")\n", + "\n", + "# 保存检查结果\n", + "check_result = {\n", + " 'is_main_org_dtype': str(data_NGV_processed['is_main_org'].dtype),\n", + " 'is_main_org_unique_values': list(data_NGV_processed['is_main_org'].unique()),\n", + " 'org_status_unique_values': list(data_NGV_processed['org_status'].unique()),\n", + " 'org_type_unique_values': list(data_NGV_processed['org_type'].unique()),\n", + " 'area_manager_unique_count': data_NGV_processed['area_manager'].nunique(),\n", + " 'has_殷昊': '殷昊' in data_NGV_processed['area_manager'].values,\n", + " 'has_孙玉蕾': '孙玉蕾' in data_NGV_processed['area_manager'].values,\n", + "}\n", + "pd.DataFrame([check_result]).to_csv(f\"{output_dir}/20.5_字段检查结果.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "print(\"\\n\" + \"=\" * 60)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤9: 数据处理和过滤(第三部分:主店过期处理)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "主店过期数据量: 0\n", + "警告: 主店过期数据为空,请检查 is_main_org 和 org_status 字段\n", + "满足条件的分店数据量: 0\n", + "警告: 主店过期数据为空,无法筛选分店数据\n", + "满足条件的主店数据量: 20159\n", + "警告: 没有分店数据可合并\n", + "合并后总数据量: 20159\n" + ] + } + ], + "source": [ + "# 主店过期,分店设置为主店\n", + "# 修复:处理 is_main_org 可能是字符串类型的情况\n", + "if data_NGV_processed['is_main_org'].dtype == 'object':\n", + " # 如果是字符串类型,转换为数值或使用字符串比较\n", + " is_main_org_numeric = pd.to_numeric(data_NGV_processed['is_main_org'], errors='coerce')\n", + " condition = (is_main_org_numeric == 1) & (data_NGV_processed['org_status'] == '过期')\n", + "else:\n", + " condition = (data_NGV_processed['is_main_org'] == 1) & (data_NGV_processed['org_status'] == '过期')\n", + "\n", + "ngvv2 = data_NGV_processed[condition]\n", + "print(f\"主店过期数据量: {len(ngvv2)}\")\n", + "if len(ngvv2) > 0:\n", + " ngvv2.to_csv(f\"{output_dir}/21_主店过期数据.csv\", index=False, encoding='utf-8-sig')\n", + "else:\n", + " print(\"警告: 主店过期数据为空,请检查 is_main_org 和 org_status 字段\")\n", + "\n", + "# 检查id_own_group是否存在于ngvv2中\n", + "data_NGV_V2 = data_NGV_processed.copy()\n", + "\n", + "# 修复:处理 is_main_org 可能是字符串类型的情况\n", + "if data_NGV_V2['is_main_org'].dtype == 'object':\n", + " is_main_org_numeric_v2 = pd.to_numeric(data_NGV_V2['is_main_org'], errors='coerce')\n", + " data_NGV_V2['条件'] = ((data_NGV_V2['org_type'] == \"一般\") & (data_NGV_V2['org_status'] == '留存') &\n", + " (data_NGV_V2['area_manager'] != '殷昊') & (data_NGV_V2['area_manager'] != '孙玉蕾') &\n", + " (is_main_org_numeric_v2 != 1))\n", + "else:\n", + " data_NGV_V2['条件'] = ((data_NGV_V2['org_type'] == \"一般\") & (data_NGV_V2['org_status'] == '留存') &\n", + " (data_NGV_V2['area_manager'] != '殷昊') & (data_NGV_V2['area_manager'] != '孙玉蕾') &\n", + " (data_NGV_V2['is_main_org'] != 1))\n", + "\n", + "data_NGV_V2 = data_NGV_V2.loc[data_NGV_V2[\"条件\"]]\n", + "print(f\"满足条件的分店数据量: {len(data_NGV_V2)}\")\n", + "\n", + "# 过滤存在的记录\n", + "if len(ngvv2) > 0:\n", + " data_NGV_V2['exists_in_ngvv2'] = data_NGV_V2['id_own_group'].isin(ngvv2['id_own_group'])\n", + " filtered_data = data_NGV_V2[data_NGV_V2['exists_in_ngvv2']]\n", + " print(f\"存在于主店过期列表的分店数据量: {len(filtered_data)}\")\n", + " \n", + " if len(filtered_data) > 0:\n", + " filtered_data.to_csv(f\"{output_dir}/22_分店数据.csv\", index=False, encoding='utf-8-sig')\n", + " \n", + " # 按版本排序并去重\n", + " fixed_order = ['皇冠版', '至尊版', '尊享版', '旗舰版', '标准版', '进阶版', '基础版', '入门版']\n", + " fixed_order_map = {edition: index for index, edition in enumerate(fixed_order)}\n", + " filtered_data['sort_key'] = filtered_data['saas_edition_fmt'].map(fixed_order_map)\n", + " filtered_data = filtered_data.sort_values(by='sort_key').drop('sort_key', axis=1)\n", + " result = filtered_data.drop_duplicates(subset='id_own_group', keep='first')\n", + " print(f\"去重后的分店数据量: {len(result)}\")\n", + " if len(result) > 0:\n", + " result.to_csv(f\"{output_dir}/23_去重后分店数据.csv\", index=False, encoding='utf-8-sig')\n", + " else:\n", + " print(\"警告: 没有分店数据存在于主店过期列表中\")\n", + " result = pd.DataFrame()\n", + "else:\n", + " print(\"警告: 主店过期数据为空,无法筛选分店数据\")\n", + " result = pd.DataFrame()\n", + "\n", + "# 合并主店和分店数据\n", + "# 修复:处理 is_main_org 可能是字符串类型的情况\n", + "if data_NGV_processed['is_main_org'].dtype == 'object':\n", + " is_main_org_numeric_main = pd.to_numeric(data_NGV_processed['is_main_org'], errors='coerce')\n", + " data_NGV_processed['条件'] = ((data_NGV_processed['org_type'] == \"一般\") & (data_NGV_processed['org_status'] == '留存') &\n", + " (data_NGV_processed['area_manager'] != '殷昊') &\n", + " (data_NGV_processed['area_manager'] != '孙玉蕾') &\n", + " (is_main_org_numeric_main == 1))\n", + "else:\n", + " data_NGV_processed['条件'] = ((data_NGV_processed['org_type'] == \"一般\") & (data_NGV_processed['org_status'] == '留存') &\n", + " (data_NGV_processed['area_manager'] != '殷昊') &\n", + " (data_NGV_processed['area_manager'] != '孙玉蕾') &\n", + " (data_NGV_processed['is_main_org'] == 1))\n", + "\n", + "data_NGV_processed = data_NGV_processed.loc[data_NGV_processed[\"条件\"]]\n", + "print(f\"满足条件的主店数据量: {len(data_NGV_processed)}\")\n", + "\n", + "# 合并数据\n", + "if len(result) > 0:\n", + " data_NGV_processed = pd.concat([data_NGV_processed, result], axis=0)\n", + "else:\n", + " print(\"警告: 没有分店数据可合并\")\n", + "print(f\"合并后总数据量: {len(data_NGV_processed)}\")\n", + "\n", + "if len(data_NGV_processed) > 0:\n", + " data_NGV_processed.to_csv(f\"{output_dir}/24_合并主店分店后数据.csv\", index=False, encoding='utf-8-sig')\n", + "else:\n", + " print(\"警告: 合并后数据为空,请检查前面的过滤条件\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤10: 数据处理和过滤(第四部分:续约日期处理)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "计算条件前数据量: 20159\n", + "条件>0的数据量: 9740\n", + "过滤后数据量: 9740\n", + "数据量: 9740\n", + "年数分布: {3: 7972, 2: 1388, 5: 259, 4: 98, 6: 14, 7: 4, 10: 4, 8: 1}\n" + ] + } + ], + "source": [ + "data_details = data_NGV_processed.copy()\n", + "data_details = data_details.reset_index(drop=True)\n", + "\n", + "# 判断A列的日期是否大于B列的日期730天\n", + "data_details['条件'] = data_details.apply(\n", + " lambda row: (\n", + " (pd.to_datetime(row['A']) - pd.to_datetime(row['B'])).days\n", + " if pd.to_datetime(row['A']) - pd.to_datetime(row['B']) >= pd.Timedelta(days=730)\n", + " else 0\n", + " ),\n", + " axis=1\n", + ")\n", + "print(f\"计算条件前数据量: {len(data_details)}\")\n", + "print(f\"条件>0的数据量: {len(data_details[data_details['条件'] > 0])}\")\n", + "data_details = data_details.loc[data_details[\"条件\"] > 0]\n", + "print(f\"过滤后数据量: {len(data_details)}\")\n", + "data_details.to_csv(f\"{output_dir}/25_续约日期过滤后数据.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 计算年数\n", + "def divide_by_365(x):\n", + " if isinstance(x, (int, float)):\n", + " return int(x / 365)\n", + " else:\n", + " return x\n", + "\n", + "data_details['年'] = data_details['条件'].apply(divide_by_365)\n", + "data_details = data_details.reset_index(drop=True)\n", + "print(f\"数据量: {len(data_details)}\")\n", + "print(f\"年数分布: {data_details['年'].value_counts().to_dict()}\")\n", + "data_details.to_csv(f\"{output_dir}/26_计算年数后数据.csv\", index=False, encoding='utf-8-sig')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤11: 数据处理和过滤(第五部分:生成历史续约日期)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 从CSV加载前一步的数据(步骤10的输出)\n", + "csv_path = f\"{output_dir}/26_计算年数后数据.csv\"\n", + "if os.path.exists(csv_path):\n", + " print(f\"从CSV文件读取: {csv_path}\")\n", + " data_details = pd.read_csv(csv_path, encoding='utf-8-sig')\n", + " print(f\"读取到 {len(data_details)} 条记录\")\n", + "else:\n", + " print(f\"警告: 文件不存在 {csv_path},请先执行步骤10\")\n", + " data_details = pd.DataFrame()\n", + "\n", + "# 创建新的DataFrame用于存储历史续约日期\n", + "new_df = pd.DataFrame()\n", + "# 使用 from datetime import datetime 避免冲突\n", + "from datetime import datetime as dt\n", + "\n", + "for index, row in data_details.iterrows():\n", + " if row[\"renew_date\"] != \"2024-02-29\":\n", + " # 修复:确保 '年' 是整数类型,处理可能的NaN或float值\n", + " try:\n", + " year_value = int(row['年']) if pd.notna(row['年']) else 0\n", + " except (ValueError, TypeError):\n", + " year_value = 0\n", + " \n", + " # 只有当年数大于1时才生成历史续约日期\n", + " if year_value > 1:\n", + " for i_new in range(1, year_value):\n", + " row_new = row.copy()\n", + " c = row_new[\"renew_date\"]\n", + " # 使用 dt.strptime (dt 是 datetime.datetime 的别名)\n", + " date_obj = dt.strptime(str(c), \"%Y-%m-%d\")\n", + " new_year = date_obj.year + i_new\n", + " new_date_obj = date_obj.replace(year=new_year)\n", + " new_c = new_date_obj.strftime(\"%Y-%m-%d\")\n", + " row_new[\"renew_date\"] = new_c\n", + " new_df = pd.concat([new_df, pd.DataFrame([row_new])], ignore_index=True)\n", + "\n", + "print(f\"生成的历史续约日期数据量: {len(new_df)}\")\n", + "if len(new_df) > 0:\n", + " new_df.to_csv(f\"{output_dir}/27_生成的历史续约日期数据.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 合并两个DataFrame\n", + "merged_df = pd.concat([data_NGV_processed, new_df], axis=0, ignore_index=True)\n", + "data_details = merged_df.copy()\n", + "print(f\"合并后总数据量: {len(data_details)}\")\n", + "data_details.to_csv(f\"{output_dir}/28_合并历史续约日期后数据.csv\", index=False, encoding='utf-8-sig')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤12: 数据处理和过滤(第六部分:最终过滤)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "renew_date不为空的数据量: 38932\n", + "过滤前数据量: 38932\n", + "创建年份等于续约年份的数据量: 8246\n", + "最终过滤后数据量: 30686\n", + "\n", + "最终数据已保存到: output/debug_revisit_renew/30_最终过滤后数据.csv\n" + ] + } + ], + "source": [ + "# 从CSV加载前一步的数据(步骤11的输出)\n", + "csv_path = f\"{output_dir}/28_合并历史续约日期后数据.csv\"\n", + "if os.path.exists(csv_path):\n", + " print(f\"从CSV文件读取: {csv_path}\")\n", + " data_details = pd.read_csv(csv_path, encoding='utf-8-sig')\n", + " print(f\"读取到 {len(data_details)} 条记录\")\n", + "else:\n", + " print(f\"警告: 文件不存在 {csv_path},请先执行步骤11\")\n", + " data_details = pd.DataFrame()\n", + "\n", + "# 过滤renew_date不为空的数据\n", + "data_details_not_null = data_details[data_details['renew_date'].notnull()]\n", + "data_details_not_null = data_details_not_null.reset_index(drop=True)\n", + "print(f\"renew_date不为空的数据量: {len(data_details_not_null)}\")\n", + "data_details_not_null.to_csv(f\"{output_dir}/29_renew_date不为空数据.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "data_details = data_details_not_null.copy()\n", + "\n", + "# 过滤掉创建年份等于续约年份的数据\n", + "data_details['saas_create_time'] = data_details['saas_create_time'].str[:4]\n", + "data_details['renew_date_new'] = data_details['renew_date'].str[:4]\n", + "print(f\"过滤前数据量: {len(data_details)}\")\n", + "print(f\"创建年份等于续约年份的数据量: {len(data_details[data_details['saas_create_time'] == data_details['renew_date_new']])}\")\n", + "data_details = data_details[data_details['saas_create_time'] != data_details['renew_date_new']]\n", + "data_details = data_details.reset_index(drop=True)\n", + "print(f\"最终过滤后数据量: {len(data_details)}\")\n", + "\n", + "data_details.to_csv(f\"{output_dir}/30_最终过滤后数据.csv\", index=False, encoding='utf-8-sig')\n", + "print(f\"\\n最终数据已保存到: {output_dir}/30_最终过滤后数据.csv\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤13: 日期计算和循环处理(90/120/180天数据筛选)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "type object 'datetime.datetime' has no attribute 'datetime'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[27]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m 2\u001b[39m date_120 = \u001b[32m113\u001b[39m\n\u001b[32m 3\u001b[39m date_180 = \u001b[32m173\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m start_time = \u001b[43mdatetime\u001b[49m\u001b[43m.\u001b[49m\u001b[43mdatetime\u001b[49m.now()\n\u001b[32m 6\u001b[39m now_time = start_time.replace()\n\u001b[32m 8\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m now_time.strftime(\u001b[33m\"\u001b[39m\u001b[33m%\u001b[39m\u001b[33mY-\u001b[39m\u001b[33m%\u001b[39m\u001b[33mm-\u001b[39m\u001b[38;5;132;01m%d\u001b[39;00m\u001b[33m\"\u001b[39m) \u001b[38;5;129;01min\u001b[39;00m date_list:\n", + "\u001b[31mAttributeError\u001b[39m: type object 'datetime.datetime' has no attribute 'datetime'" + ] + } + ], + "source": [ + "# 从CSV加载最终过滤后的数据(步骤12的输出)\n", + "csv_path = f\"{output_dir}/30_最终过滤后数据.csv\"\n", + "if os.path.exists(csv_path):\n", + " print(f\"从CSV文件读取: {csv_path}\")\n", + " data_details = pd.read_csv(csv_path, encoding='utf-8-sig')\n", + " print(f\"读取到 {len(data_details)} 条记录\")\n", + "else:\n", + " print(f\"警告: 文件不存在 {csv_path},请先执行步骤12\")\n", + " data_details = pd.DataFrame()\n", + "\n", + "# 从CSV加载节假日列表和date_one(步骤3的输出)\n", + "date_list_csv = f\"{output_dir}/14_节假日列表.csv\"\n", + "if os.path.exists(date_list_csv):\n", + " date_list = pd.read_csv(date_list_csv, encoding='utf-8-sig').iloc[:, 0].tolist()\n", + "else:\n", + " print(f\"警告: 文件不存在 {date_list_csv},请先执行步骤3\")\n", + " date_list = []\n", + "\n", + "date_one_csv = f\"{output_dir}/15_date_one.csv\"\n", + "if os.path.exists(date_one_csv):\n", + " date_one = pd.read_csv(date_one_csv, encoding='utf-8-sig').iloc[0, 0]\n", + "else:\n", + " print(f\"警告: 文件不存在 {date_one_csv},请先执行步骤3\")\n", + " date_one = 1\n", + "\n", + "date_90 = 83\n", + "date_120 = 113\n", + "date_180 = 173\n", + "\n", + "start_time = datetime.datetime.now()\n", + "now_time = start_time.replace()\n", + "\n", + "if now_time.strftime(\"%Y-%m-%d\") in date_list:\n", + " date_one = 0\n", + " print(\"开始次数:\", date_one)\n", + " print(\"当前日期:\", now_time)\n", + "\n", + "print(f\"遍历次数:{date_one}\")\n", + "\n", + "# 存储所有派发数据\n", + "all_distribution_data = []\n", + "\n", + "for i in range(0, date_one):\n", + " print(f\"\\n========== 这是第{i}次遍历 ==========\")\n", + " now_time = datetime.datetime.now() + datetime.timedelta(days=-(i + 1))\n", + " \n", + " today = now_time + datetime.timedelta(days=-date_90)\n", + " formatted_today_90 = today.strftime(\"%Y-%m-%d\")\n", + " today = now_time + datetime.timedelta(days=-date_120)\n", + " formatted_today_120 = today.strftime(\"%Y-%m-%d\")\n", + " today = now_time + datetime.timedelta(days=-date_180)\n", + " formatted_today_180 = today.strftime(\"%Y-%m-%d\")\n", + " \n", + " print(f\"90天为{formatted_today_90},120天为{formatted_today_120},180天为{formatted_today_180}\")\n", + " \n", + " # 获取90天数据\n", + " data_details_90 = data_details.copy()\n", + " data_details_90['条件'] = ((data_details_90['renew_date'] == formatted_today_90) & \n", + " (data_details_90['group_grade'] != \"普通客户(VIP)\"))\n", + " data_details_90 = data_details_90.loc[data_details_90[\"条件\"]]\n", + " print(f\"90天数据量: {len(data_details_90)}\")\n", + " \n", + " # 获取120天数据\n", + " data_details_120 = data_details.copy()\n", + " data_details_120['条件'] = ((data_details_120['renew_date'] == formatted_today_120) &\n", + " ((data_details_120['saas_edition_fmt'] == '基础版') |\n", + " (data_details_120['saas_edition_fmt'] == '入门版')))\n", + " data_details_120 = data_details_120.loc[data_details_120[\"条件\"]]\n", + " print(f\"120天数据量: {len(data_details_120)}\")\n", + " \n", + " # 获取180天数据\n", + " data_details_180 = data_details.copy()\n", + " data_details_180['条件'] = (data_details_180['renew_date'] == formatted_today_180)\n", + " data_details_180 = data_details_180.loc[data_details_180[\"条件\"]]\n", + " print(f\"180天数据量: {len(data_details_180)}\")\n", + " \n", + " # 添加跟进阶段和主要目的\n", + " data_details_90[\"跟进阶段\"] = \"续约后90天回访\"\n", + " data_details_90[\"主要目的\"] = \"关怀使用情况,促进更多功能使用,提升系统使用深度。\"\n", + " data_details_120[\"跟进阶段\"] = \"续约后120天回访\"\n", + " data_details_120[\"主要目的\"] = \"暂无\"\n", + " data_details_180[\"跟进阶段\"] = \"续约后180天回访\"\n", + " data_details_180[\"主要目的\"] = \"关怀使用情况,促进增购商机转化,识别潜在风险,及时提报。\"\n", + " \n", + " # 合并三个DataFrame(去除续约120天回访)\n", + " data_result = pd.concat([data_details_90, data_details_180], ignore_index=True)\n", + " print(f\"合并后派发数据长度:{len(data_result)}\")\n", + " \n", + " if len(data_result) > 0:\n", + " # 保存每次循环的派发数据\n", + " data_result.to_csv(f\"{output_dir}/31_第{i}次遍历_派发数据.csv\", index=False, encoding='utf-8-sig')\n", + " all_distribution_data.append(data_result)\n", + " else:\n", + " print(f\"警告:第{i}次遍历没有派发数据!\")\n", + " # 保存空数据的原因分析\n", + " analysis = {\n", + " '遍历次数': i,\n", + " '日期': now_time.strftime('%Y-%m-%d'),\n", + " '90天日期': formatted_today_90,\n", + " '180天日期': formatted_today_180,\n", + " '90天匹配数量': len(data_details_90),\n", + " '180天匹配数量': len(data_details_180),\n", + " '总数据量': len(data_details),\n", + " 'renew_date唯一值数量': data_details['renew_date'].nunique() if len(data_details) > 0 else 0\n", + " }\n", + " pd.DataFrame([analysis]).to_csv(f\"{output_dir}/32_第{i}次遍历_空数据原因分析.csv\", index=False, encoding='utf-8-sig')\n", + "\n", + "# 合并所有派发数据\n", + "if all_distribution_data:\n", + " final_distribution_data = pd.concat(all_distribution_data, ignore_index=True)\n", + " print(f\"\\n总派发数据量: {len(final_distribution_data)}\")\n", + " final_distribution_data.to_csv(f\"{output_dir}/33_最终派发数据.csv\", index=False, encoding='utf-8-sig')\n", + " print(f\"最终派发数据已保存到: {output_dir}/33_最终派发数据.csv\")\n", + "else:\n", + " print(\"\\n警告:所有遍历都没有派发数据!\")\n", + " # 分析为什么没有派发数据\n", + " analysis = {\n", + " '总数据量': len(data_details),\n", + " 'renew_date唯一值': data_details['renew_date'].nunique() if len(data_details) > 0 else 0,\n", + " 'renew_date范围': f\"{data_details['renew_date'].min()} 到 {data_details['renew_date'].max()}\" if len(data_details) > 0 else '无数据',\n", + " 'date_one': date_one,\n", + " '当前日期': datetime.datetime.now().strftime('%Y-%m-%d')\n", + " }\n", + " pd.DataFrame([analysis]).to_csv(f\"{output_dir}/34_派发数据为空原因分析.csv\", index=False, encoding='utf-8-sig')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 步骤14: 派发数据为空的原因分析\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 分析派发数据为空的原因\n", + "print(\"========== 派发数据为空原因分析 ==========\")\n", + "\n", + "if len(data_details) == 0:\n", + " print(\"问题1: 最终过滤后数据为空\")\n", + " print(\"请检查步骤12的过滤条件\")\n", + "else:\n", + " print(f\"最终数据量: {len(data_details)}\")\n", + " \n", + " # 检查renew_date的分布\n", + " print(f\"\\nrenew_date唯一值数量: {data_details['renew_date'].nunique()}\")\n", + " print(f\"renew_date范围: {data_details['renew_date'].min()} 到 {data_details['renew_date'].max()}\")\n", + " \n", + " # 计算目标日期范围\n", + " now_time = datetime.datetime.now()\n", + " target_date_90 = (now_time - datetime.timedelta(days=83)).strftime(\"%Y-%m-%d\")\n", + " target_date_180 = (now_time - datetime.timedelta(days=173)).strftime(\"%Y-%m-%d\")\n", + " \n", + " print(f\"\\n目标90天日期: {target_date_90}\")\n", + " print(f\"目标180天日期: {target_date_180}\")\n", + " \n", + " # 检查是否有匹配的数据\n", + " match_90 = data_details[data_details['renew_date'] == target_date_90]\n", + " match_180 = data_details[data_details['renew_date'] == target_date_180]\n", + " \n", + " print(f\"\\n匹配90天日期的数据量: {len(match_90)}\")\n", + " print(f\"匹配180天日期的数据量: {len(match_180)}\")\n", + " \n", + " if len(match_90) > 0:\n", + " # 检查90天数据的group_grade过滤\n", + " match_90_filtered = match_90[match_90['group_grade'] != \"普通客户(VIP)\"]\n", + " print(f\"90天数据过滤后(排除普通客户): {len(match_90_filtered)}\")\n", + " if len(match_90_filtered) == 0:\n", + " print(\"问题: 90天数据全部被group_grade过滤掉\")\n", + " print(f\"90天数据的group_grade分布: {match_90['group_grade'].value_counts().to_dict()}\")\n", + " \n", + " # 检查renew_date的日期分布\n", + " print(f\"\\nrenew_date日期分布(前20个):\")\n", + " print(data_details['renew_date'].value_counts().head(20))\n", + " \n", + " # 保存分析结果\n", + " analysis_result = {\n", + " '最终数据量': len(data_details),\n", + " 'renew_date唯一值数量': data_details['renew_date'].nunique(),\n", + " 'renew_date最小值': data_details['renew_date'].min(),\n", + " 'renew_date最大值': data_details['renew_date'].max(),\n", + " '目标90天日期': target_date_90,\n", + " '目标180天日期': target_date_180,\n", + " '匹配90天日期数量': len(match_90),\n", + " '匹配180天日期数量': len(match_180),\n", + " 'date_one': date_one,\n", + " '当前日期': datetime.datetime.now().strftime('%Y-%m-%d')\n", + " }\n", + " pd.DataFrame([analysis_result]).to_csv(f\"{output_dir}/35_派发数据为空原因分析.csv\", index=False, encoding='utf-8-sig')\n", + " \n", + " # 保存renew_date的详细分布\n", + " renew_date_dist = data_details['renew_date'].value_counts().reset_index()\n", + " renew_date_dist.columns = ['renew_date', 'count']\n", + " renew_date_dist.to_csv(f\"{output_dir}/36_renew_date分布.csv\", index=False, encoding='utf-8-sig')\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "SaaS", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/test/续约待办一致性-逐条同步.py b/test/续约待办一致性-逐条同步.py new file mode 100644 index 0000000..bcb7764 --- /dev/null +++ b/test/续约待办一致性-逐条同步.py @@ -0,0 +1,58 @@ +import requests +import pandas as pd + +cookies = { + 'auth_token': 's%3A.9uztgExtmqUJXHCi00hv9SGq6eVYSvH%2BxQSwrox1Yls', + 'fx-lang': 'zh_cn', + 'tenantId': 'agndqbuttb7ipfciraxcokgqyu', + 'AGL_USER_ID': 'a50da526-dd43-4a78-ace1-ba810a6f2168', + '_ga': 'GA1.1.626243428.1772260541', + '_clck': 'y7ldwu%5E2%5Eg3y%5E0%5E2250', + '_ga_JTDW9M3LHZ': 'GS2.1.s1772266909$o3$g0$t1772266909$j60$l0$h0', + 'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D', + 'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D', + '_csrf': 's%3AE3ildFF7aOT874ca1lQaPpJh.M4vLvJ9TdzyukOLVlNNYO9ysSRLlssqxQh1%2FwMawRCY', + 'Hm_lvt_de47dd1629940fe88b02865de93dd9fe': '1774410585,1774417748,1774487358,1774503122', + 'HMACCOUNT': 'A6A0585E8C70051D', + 'Hm_lpvt_de47dd1629940fe88b02865de93dd9fe': '1774503273', + 'GSuvNKHqfvX2r6v7P8HkZv2bow': 's%3ALj5VHNEzUxprmpIJHWpNg7SzUp7GR3H1.yp8HeYMA0ARAeFC0CqxucSfBxvudd4xO76ZwAACWvTc', + 'JDY_SID': 's%3AZrD40YQAsP9Ui53M1j0lud3AvwbdgzEl.ljjCkitsu2tDtbx0kJTROsKeNh8QfX2mz6SehH%2B%2FD5E', +} + +headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/json', + 'origin': 'https://www.jiandaoyun.com', + 'priority': 'u=1, i', + 'referer': 'https://www.jiandaoyun.com/dashboard/app/675b900991ad2491c69389ca/form/6931063d64187eaf6b927557/edit', + 'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0', + 'x-csrf-token': 'Mape7z8g-8iFP_vxFm8qSzVprTb4XsGfvs1o', + 'x-jdy-ver': '10.19.6', + 'x-request-id': '7bb1d48a-0fdb-46b7-bf67-ebd52c1a1f9a', + # 'cookie': 'auth_token=s%3A.9uztgExtmqUJXHCi00hv9SGq6eVYSvH%2BxQSwrox1Yls; fx-lang=zh_cn; tenantId=agndqbuttb7ipfciraxcokgqyu; AGL_USER_ID=a50da526-dd43-4a78-ace1-ba810a6f2168; _ga=GA1.1.626243428.1772260541; _clck=y7ldwu%5E2%5Eg3y%5E0%5E2250; _ga_JTDW9M3LHZ=GS2.1.s1772266909$o3$g0$t1772266909$j60$l0$h0; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2257956c24ceedab0c48c17b4e%22%2C%22first_id%22%3A%2219b6dd1a10c148a-063e3a033b10ed-4c657b58-2073600-19b6dd1a10d145b%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTliNmRkMWExMGMxNDhhLTA2M2UzYTAzM2IxMGVkLTRjNjU3YjU4LTIwNzM2MDAtMTliNmRkMWExMGQxNDViIiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiNTc5NTZjMjRjZWVkYWIwYzQ4YzE3YjRlIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%2257956c24ceedab0c48c17b4e%22%7D%7D; _csrf=s%3AE3ildFF7aOT874ca1lQaPpJh.M4vLvJ9TdzyukOLVlNNYO9ysSRLlssqxQh1%2FwMawRCY; Hm_lvt_de47dd1629940fe88b02865de93dd9fe=1774410585,1774417748,1774487358,1774503122; HMACCOUNT=A6A0585E8C70051D; Hm_lpvt_de47dd1629940fe88b02865de93dd9fe=1774503273; GSuvNKHqfvX2r6v7P8HkZv2bow=s%3ALj5VHNEzUxprmpIJHWpNg7SzUp7GR3H1.yp8HeYMA0ARAeFC0CqxucSfBxvudd4xO76ZwAACWvTc; JDY_SID=s%3AZrD40YQAsP9Ui53M1j0lud3AvwbdgzEl.ljjCkitsu2tDtbx0kJTROsKeNh8QfX2mz6SehH%2B%2FD5E', +} + +df = pd.read_excel(fr"C:\Users\hp_z66\Downloads\续约服务流程_20260327115935.xlsx",sheet_name="需要查询补充120天节点数据", dtype=str).fillna("") + +json_data = { + 'skip': 0, + 'limit': 20, + 'appId': '675b900991ad2491c69389ca', + 'entryId': '6931063d64187eaf6b927557', + 'formId': '6931063d64187eaf6b927557', + 'dataId': '69ab8f89837038e4e81bff21', +} + +response = requests.post( + 'https://www.jiandaoyun.com/_/admin/data/log/list_changes', + cookies=cookies, + headers=headers, + json=json_data, +) diff --git a/test/续约待办宜搭传给简道云-1day.py b/test/续约待办宜搭传给简道云-1day.py new file mode 100644 index 0000000..25d3f4f --- /dev/null +++ b/test/续约待办宜搭传给简道云-1day.py @@ -0,0 +1,791 @@ +import os +from datetime import datetime, timedelta, timezone +import pandas as pd +from tqdm import tqdm +from datetime import datetime, timezone +import pandas as pd +import os +from typing import Dict +import requests +import json +import time +import numpy as np # 导入numpy库用于处理numpy数组 + +output_dir = "output" # 设置输出目录 +os.makedirs(output_dir, exist_ok=True) + + +class Config: + JIANDAOYUN_API_TOKEN = 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN' # token + + +class API: + def entry_data_list(self, data: dict, replace: bool = False, max_retries: int = 20) -> Dict: # 获取多条表单数据 + """ + 获取多条表单数据 + :param max_retries: 最大重试次数 + :param replace: 是否替换字段 + :param data: + api_key: 应用id + entry_id: 表单id + :return: + """ + + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list' + + headers = { + 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 app_key + 'Content-Type': 'application/json' + } + all_data_batches = [] # 用于存储每次请求返回的数据批次 + last_data_id = None + exit_flag = False + while True: + payload = json.dumps({ + "app_id": data['api_key'], # 应用ID + "entry_id": data['entry_id'], # 表单ID + "limit": 90, + "data_id": last_data_id, + "filter": data.get('filter', None) + }) + retries = 0 + while retries <= max_retries: + try: + res = requests.post(url=url, data=payload, headers=headers, timeout=10) + res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 + data_get = res.json() + if data_get["data"]: + all_data_batches.extend(data_get['data']) + last_data_id = data_get['data'][-1].get('_id') + print(f"已获取 {len(all_data_batches)} 条数据") + break # 成功则跳出循环 + else: + if 'data' not in data_get or len(data_get['data']) == 0: + exit_flag = True + break + # logger.warning(f"请求异常, 将重新请求") + retries += 1 + time.sleep(0.1) # 在重试之间稍作停顿 + except requests.exceptions.RequestException as e: + # logger.warning(f"请求异常: {e}, 将重新请求") + retries += 1 + time.sleep(0.1) # 在重试之间稍作停顿 + if retries > max_retries: + # error_task_logger.error(f"任务 {last_data_id}组 连续{max_retries}次请求失败,放弃此次请求。") + all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理 + + if exit_flag: + break + + # 构建最终返回的字典 + final_data = { + 'data': all_data_batches # 'data' 键对应的值是列表的列表 + } + # logger.info(f"获取了{len(all_data_batches)}条数据") + if replace: + print("进行了替换") + return_data = self.field_replacement(data, final_data) # 字段替换,由id替换为标签名 + + return return_data + else: + return final_data + + def field_replacement(self, data: dict, data_get: dict) -> dict: + """ + 字段替换,将id替换为标签名,即唯一值替换为表单中显示字段的名字 + :param data: 简道云插件发送过来的data,包含表单id、数据id、应用id + :param data_get: 简道云请求的数据,一般是根据数据id获取到表单的数据 + :return: 将根据数据id获取到的表单数据,进行替换,返回替换后的数据 + """ + + # 获取表单对应字段标签名称 + widget_list = self.entry_widget_list(data) + + # 检查widget_list是否有效 + if not widget_list or 'widgets' not in widget_list or not isinstance(widget_list['widgets'], list): + raise ValueError("映射表没有接受到数据") + + # 创建一个映射表,将_widget_名称映射到label + name_to_label = {widget['name']: widget['label'] for widget in widget_list['widgets']} + + def replace_keys(obj): + """递归替换字典中的键名""" + if isinstance(obj, dict): + new_dict = {} + for key, value in obj.items(): + new_key = name_to_label.get(key, key) + new_dict[new_key] = replace_keys(value) + return new_dict + elif isinstance(obj, list): + return [replace_keys(item) for item in obj] + else: + return obj + + # 复制 data_get,避免修改原始数据 + data_get_copy = json.loads(json.dumps(data_get)) # 深拷贝 + + if 'data' in data_get_copy: + data_get_copy['data'] = replace_keys(data_get_copy['data']) + + return data_get_copy + + @staticmethod + def workflow_instance_get(data: dict, max_retries: int = 20) -> dict: + """ + 查询实例流程信息 + :param max_retries: + :param data: 简道云插件发送过来的data,包含应用id + :return: 查询简道云流程实例信息返回的结果 + """ + url = 'https://api.jiandaoyun.com/api/v6/workflow/instance/get' + + headers = { + 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey + 'Content-Type': 'application/json' + } + + payload = json.dumps({ + "instance_id": data['data_id'], + "tasks_type": 1 + } + ) + print("payload:", payload) + data_get = None + retries = 0 + while retries <= max_retries: + try: + res = requests.post(url=url, data=payload, headers=headers, timeout=10) + # res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 + data_get = res.json() + # print( "返回结果:", data_get) + if res.status_code == 200: + break # 成功则跳出循环 + else: + # logger.warning(f"请求异常, 将重新请求") + retries += 1 + time.sleep(3) # 在重试之间稍作停顿 + except requests.exceptions.RequestException as e: + # logger.warning(f"请求异常: {e}, 将重新请求") + retries += 1 + time.sleep(0.1) # 在重试之间稍作停顿 + if retries > max_retries: + # error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。") + print("请求失败") + + return data_get + + @staticmethod + def entry_data_update(data: dict, max_retries: int = 20) -> dict: # 修改数据 + """ + 修改数据 + :param max_retries: 最大重试次数,此处设置100次 + :param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息 + :return: 修改数据后简道云返回的结果 + """ + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/update' + + headers = { + 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey + 'Content-Type': 'application/json' + } + + payload = json.dumps({ + "app_id": data['api_key'], # 应用ID + "entry_id": data['entry_id'], # 表单ID + "data_id": data['data_id'], # 数据ID + "data": data['data'], + "is_start_trigger": True + } + ) + + data_get = None + retries = 0 + while retries <= max_retries: + try: + res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) + res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 + data_get = res.json() + # print(data_get) + if res.status_code == 200: + break # 成功则跳出循环 + else: + # logger.warning(f"请求异常, 将重新请求") + retries += 1 + time.sleep(3) # 在重试之间稍作停顿 + except requests.exceptions.RequestException as e: + # logger.warning(f"请求异常: {e}, 将重新请求") + retries += 1 + time.sleep(10) # 在重试之间稍作停顿 + if retries > max_retries: + # error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。") + continue + return data_get + + @staticmethod + def workflow_instance_end(data: dict, max_retries: int = 20) -> dict: + """ + 关闭流程 + :param max_retries: + :param data: 简道云插件发送过来的data,包含应用id + :return: 查询简道云流程实例信息返回的结果 + """ + url = 'https://api.jiandaoyun.com/api/v1/workflow/instance/close' + + headers = { + 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey + 'Content-Type': 'application/json' + } + + payload = json.dumps({ + "instance_id": data['data_id'], + } + ) + print("payload:", payload) + data_get = None + retries = 0 + while retries <= max_retries: + try: + res = requests.post(url=url, data=payload, headers=headers, timeout=10) + # res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 + data_get = res.json() + print("返回结果:", data_get) + if res.status_code == 200: + break # 成功则跳出循环 + else: + + retries += 1 + time.sleep(3) # 在重试之间稍作停顿 + except requests.exceptions.RequestException as e: + + retries += 1 + time.sleep(0.1) # 在重试之间稍作停顿 + + return data_get + + +class NpEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + else: + return super(NpEncoder, self).default(obj) + + +class YDAPI: + appKey = "ding5kqocon5s9oph5uq" + appSecret = "HL1jgsIIfLAC0eTH0A1m4mwxUDqbgsiPeCCGGE3ocM6qJBTIW7Ivt9drxF_Z4Kb_" + + @staticmethod + def get_ids_query(token, formUuid, appType, systemToken, formInstanceIdList=None, max_retries=10, delay=2): + """ + 函数功能:读取表单的所有数据,并加入重试机制。 + + Args: + token (str): 登录验证token,用于API调用的身份验证。 + formUuid (str): 表单唯一标识符,用于指定需要读取哪个表单的实例数据。 + page (int): 分页参数,指定请求的数据页码。 + n (int): 每页显示的数据条数。 + appType (str): 应用类型标识符,默认为 "APP_UYZ0KG6L0CCNV80GZ66O" + systemToken (str): 系统token,默认为固定值 + instanceStatus (str): 流程实例状态,默认为"RUNNING" + max_retries (int): 最大重试次数,默认为10次 + delay (int): 每次重试之间的延迟秒数,默认为2秒 + + Returns: + dict: 返回从API获取的流程表单实例数据的JSON解析结果。 + + Raises: + Exception: 如果达到最大重试次数仍未成功,则抛出异常。 + """ + + attempt = 0 + api = f'https://api.dingtalk.com/v1.0/yida/forms/instances/ids/query' + headers = { + "Content-Type": "application/json", + "x-acs-dingtalk-access-token": token + } + formData = { + "appType": appType, + "systemToken": systemToken, + "userId": "yida_pub_account", # 超级管理员账号 + "language": "zh_CN", + "formUuid": formUuid, + "formInstanceIdList": formInstanceIdList, + } + # print(formData) + + while True: + if attempt >= max_retries: + break + + try: + res = requests.post(api, headers=headers, json=formData) + # print(res.json()) + res.raise_for_status() # 如果返回状态码不是2xx,抛出异常 + return res.json() + + except requests.exceptions.RequestException as e: + + time.sleep(delay) + attempt += 1 + + def generateToken(self) -> str: + """ + 函数功能:生成访问令牌(token) + + Returns: + str: 返回生成的访问令牌字符串。此token用于后续API调用的身份验证。 + """ + token_api = 'https://api.dingtalk.com/v1.0/oauth2/accessToken' + data = { + "appKey": f"{self.appKey}", + "appSecret": f'{self.appSecret}' + } + res = requests.post(token_api, json=data) + token = res.json().get('accessToken') + return token + + def read_processes_instances(self, token, formUuid, page, n, appType="APP_UYZ0KG6L0CCNV80GZ66O", + systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", instanceStatus="RUNNING", + max_retries=10, delay=2, createFromTimeGMT=None, createToTimeGMT=None, + modifiedFromTimeGMT=None, + modifiedToTimeGMT=None, searchFieldJson={}): + """ + 函数功能:读取流程表单的所有数据,并加入重试机制。 + + Args: + token (str): 登录验证token,用于API调用的身份验证。 + formUuid (str): 表单唯一标识符,用于指定需要读取哪个表单的实例数据。 + page (int): 分页参数,指定请求的数据页码。 + n (int): 每页显示的数据条数。 + appType (str): 应用类型标识符,默认为 "APP_UYZ0KG6L0CCNV80GZ66O" + systemToken (str): 系统token,默认为固定值 + instanceStatus (str): 流程实例状态,默认为"RUNNING" + max_retries (int): 最大重试次数,默认为10次 + delay (int): 每次重试之间的延迟秒数,默认为2秒 + + Returns: + dict: 返回从API获取的流程表单实例数据的JSON解析结果。 + + Raises: + Exception: 如果达到最大重试次数仍未成功,则抛出异常。 + """ + + attempt = 0 + api = f'https://api.dingtalk.com/v1.0/yida/processes/instances?pageNumber={page}&pageSize={n}' + headers = { + "Content-Type": "application/json", + "x-acs-dingtalk-access-token": token + } + formData = { + "appType": appType, + "systemToken": systemToken, + "userId": "yida_pub_account", # 超级管理员账号 + "language": "zh_CN", + "formUuid": formUuid, + "instanceStatus": instanceStatus, # 运行中 + "createFromTimeGMT": createFromTimeGMT, + "createToTimeGMT": createToTimeGMT, + "modifiedFromTimeGMT": modifiedFromTimeGMT, + "modifiedToTimeGMT": modifiedToTimeGMT, + "searchFieldJson": json.dumps( + searchFieldJson + ) + } + # print(formData) + + while True: + if attempt >= max_retries: + # error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取流程实例数据,跳过本次请求。") + break + + try: + res = requests.post(api, headers=headers, json=formData) + # print(res.json()) + res.raise_for_status() # 如果返回状态码不是2xx,抛出异常 + return res.json() + + except requests.exceptions.RequestException as e: + # logger.warning(f"请求异常: {e},正在尝试第 {attempt + 1} 次重试...") + time.sleep(delay) + attempt += 1 + + def update_from(self, token, formInstanceId, data_new): + """ + 函数功能:更新表单内容 + + Args: + token (str): 登录验证token,用于API调用的身份验证。 + formInstanceId (str): 表单实例ID,读文件获取。 + data_new (dict): 新的数据内容,用于替换现有表单实例中的数据。读文件获取。 + + Returns: + Response: 返回API请求的响应对象。 + """ + + api = f'https://api.dingtalk.com//v1.0/yida/forms/instances' + + headers = { + "Content-Type": "application/json", + "x-acs-dingtalk-access-token": token + } + + payload = { + "appType": "APP_UYZ0KG6L0CCNV80GZ66O", + "systemToken": "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", + "userId": "yida_pub_account", # 曹伟 id + "language": "zh_CN", + "useLatestVersion": "false", + "formInstanceId": formInstanceId, + "updateFormDataJson": json.dumps(data_new, cls=NpEncoder), + + } + + res = requests.put(api, headers=headers, json=payload) + return res + + def get_approval_records(self, token: str, processInstanceId: str, appType="APP_UYZ0KG6L0CCNV80GZ66O", + systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", max_retries=10, delay=2): + """ + 函数功能:获取流程表单的审批记录,适用于"F6客户服务"应用,并且包含重试机制。 + + Args: + token (str): 登录验证token,用于API调用的身份验证。 + processInstanceId (str): 流程实例ID,用于标识需要获取审批记录的具体流程实例。 + appType (str): 应用类型标识符,默认为 "APP_UYZ0KG6L0CCNV80GZ66O" + systemToken (str): 系统token,默认为固定值 + max_retries (int): 最大重试次数,默认为10次 + delay (int): 每次重试之间的延迟秒数,默认为2秒 + + Returns: + dict: 返回从API获取的审批记录的JSON解析结果。通常包括审批步骤、审批人、审批时间等信息。 + """ + attempt = 0 + userId = "yida_pub_account" + api = f'https://api.dingtalk.com/v1.0/yida/processes/operationRecords?appType={appType}&systemToken={systemToken}&userId={userId}&language=zh_CN&processInstanceId={processInstanceId}' + headers = { + "Content-Type": "application/json", + "x-acs-dingtalk-access-token": token + } + + while True: + if attempt >= max_retries: + # error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取审批数据,跳过本次请求。") + break + + try: + res = requests.get(api, headers=headers) + res.raise_for_status() # 如果响应状态码不是2xx,则抛出HTTPError + return res.json() + except (requests.exceptions.RequestException, Exception) as e: + # logger.warning(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...") + time.sleep(delay) # 等待指定的延迟时间后再次尝试 + attempt += 1 + + def aggree_approval(self, token: str, taskId: str, processInstanceId: str, formData: dict, res_new): + """_summary_ + + 函数功能:同意审批节点 --F6客户服务 应用 + + Args: + token (str): 登录验证token + taskId (str): 获取到的审批节点ID + processInstanceId (str): 读取文件获得的实例ID + formData (dict): 数据样式 + res_new (响应值): 从员工ID表里获取到员工名对应的员工ID + + Returns: + 响应值: 返回请求结果 + """ + api = 'https://api.dingtalk.com/v1.0/yida/tasks/execute' + headers = { + "Content-Type": "application/json", + "x-acs-dingtalk-access-token": token + } + payload = { + "outResult": "AGREE", + "appType": "APP_UYZ0KG6L0CCNV80GZ66O", + "systemToken": "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", + "remark": "同意(接口自动)", + "formDataJson": json.dumps(formData, cls=NpEncoder), + "processInstanceId": processInstanceId, + "userId": res_new, + "language": "zh_CN", + "taskId": int(taskId) + } + + res = requests.post(api, headers=headers, json=payload) + return res + + +api_instance = API() +yd_api_instance = YDAPI() + + +class YDToJDYRenewalToDo(object): + def __init__(self): + self.FORMID = "FORM-PE866MD1MJMU0WGLYRFLYEN5YN9L1I55Z7ZUK22" + self.appType = "APP_UYZ0KG6L0CCNV80GZ66O" + self.systemToken = "XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2" + self.token = yd_api_instance.generateToken() + + def load_all_data(self): + end_dt = datetime.now() + timedelta(days=1) + start_dt = end_dt - timedelta(days=90) + start_time = start_dt.strftime("%Y-%m-%d") + end_time = end_dt.strftime("%Y-%m-%d") + + yd_data = yd_api_instance.read_processes_instances( + token=self.token, + formUuid=self.FORMID, + page=1, + n=100, + appType=self.appType, + systemToken=self.systemToken, + instanceStatus="", + modifiedFromTimeGMT=start_time, + modifiedToTimeGMT=end_time, + ) + + all_process_list = [] + + PAGES_two = yd_data.get('totalCount') // 100 + 1 + + for a in tqdm(range(1, PAGES_two + 1)): + try: + yd_data = yd_api_instance.read_processes_instances( + token=self.token, + formUuid=self.FORMID, + page=a, + n=100, + appType=self.appType, + systemToken=self.systemToken, + instanceStatus="", + modifiedFromTimeGMT=start_time, + modifiedToTimeGMT=end_time, + ) + all_process_list = all_process_list + yd_data.get("data") + except Exception as e: + print(f"获取流程实例数据时出错: {e}") + continue + + df_current = pd.DataFrame(all_process_list) + current_file = f"{output_dir}/{start_time}_{end_time}_all_process_list.csv" + + # === 新增:读取上次文件并计算差值 === + if os.path.exists(current_file): + try: + df_last = pd.read_csv(current_file) + except Exception as e: + print(f"读取历史文件失败: {e}") + df_last = pd.DataFrame() + else: + df_last = None # 明确标记:无历史文件 + + id_col = 'processInstanceId' + + if df_last is not None and not df_last.empty and not df_current.empty and id_col in df_current.columns and id_col in df_last.columns: + # 有历史文件,计算新增 + last_ids = set(df_last[id_col].astype(str)) + current_ids = set(df_current[id_col].astype(str)) + new_ids = current_ids - last_ids + diff_records = df_current[df_current[id_col].astype(str).isin(new_ids)].to_dict('records') + else: + # 没有历史文件 或 无法比对 → 返回全部当前数据 + diff_records = df_current.to_dict('records') # ← 关键修改点 + # 保存当前全量数据(覆盖) + df_current.to_csv(current_file, index=False) + + return diff_records + + def filter_renewal_data(self, all_process_list): + update_data_list = [] + for item in all_process_list: + if item.get("data").get("textField_kto3q3ev"): + org_id = item.get("data").get("textField_ksydghqw") + order_code = item.get("data").get("textField_kto3q3ev") # 订单编码 + pay_time = item.get("data").get("dateField_kto3q3ex") # 订单支付日期 + + res = yd_api_instance.get_ids_query( + token=self.token, + formInstanceIdList=[item.get("processInstanceId")], + formUuid="FORM-PE866MD1MJMU0WGLYRFLYEN5YN9L1I55Z7ZUK22", + appType="APP_UYZ0KG6L0CCNV80GZ66O", + systemToken="XA966F81JAJOFCVVVKO64E9MIIZV1EWE5SFMKJ2", + ) + + payment_amount = None + bussiness_type = None + # print(res) + if res.get("result"): + form_data = res["result"][0]["formData"] + payment_amount = form_data.get("textField_kyjy1kkm") # "9987" + bussiness_type = form_data.get("textField_kyjy1kkn") # "续约" + # payment_amount = res.get("data").get("textField_kyjy1kkm") # 支付金额 + # bussiness_type = res.get("data").get("textField_kyjy1kkn") # 业务类型 + + if pay_time: + # 确保是整数 + timestamp_ms = int(pay_time) + # 转为 UTC datetime 对象 + pay_datetime_utc = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc) + print(pay_datetime_utc) # 例如: 2024-04-05 12:34:38.901000+00:00 + else: + pay_datetime_utc = None + + update_data_list.append({ + "order_code": order_code, + "pay_time": pay_time, + "payment_amount": payment_amount, + "bussiness_type": bussiness_type, + "org_id": org_id + }) + return update_data_list + + def check_jd_ydy_data(self, update_data_list): + def extract_widget_value(record, widget_id): + value = record.get(widget_id) + if isinstance(value, dict) and "value" in value: + return value.get("value") + return value + + def has_value(value): + if value is None: + return False + if isinstance(value, str): + return value.strip() != "" + if isinstance(value, (list, dict)): + return len(value) > 0 + return True + + def pick_best_record(records): + if not records: + return None + + time_fields = [ + "_updateTime", + "_updated_at", + "_createTime", + "_created_at", + "updateTime", + "updated_at", + "createTime", + "created_at", + ] + + def extract_sort_value(record): + for field in time_fields: + value = record.get(field) + if value is None: + continue + try: + return int(value) + except Exception: + try: + return int(float(value)) + except Exception: + continue + return 0 + + return max(records, key=extract_sort_value) + + def query_jdy_by_org_id(org_id, flow_state=None): + cond = [{ + "field": "_widget_1764820541661", + "type": "text", + "method": "eq", + "value": [org_id] + }] + if flow_state is not None: + cond.append({ + "field": "flowState", + "type": "flowstate", + "method": "eq", + "value": [flow_state] + }) + + payload = { + "api_key": "675b900991ad2491c69389ca", + "entry_id": "6931063d64187eaf6b927557", + "filter": { + "rel": "and", + "cond": cond + } + } + return api_instance.entry_data_list(payload).get("data", []) + + for item in update_data_list: + data_list = query_jdy_by_org_id(item.get("org_id"), flow_state=0) + if not data_list: + data_list = query_jdy_by_org_id(item.get("org_id"), flow_state=None) + + result = pick_best_record(data_list) + data_id = result.get("_id") if result else None + + if not data_id: + # print(f"未找到订单 {item.get('order_code')} 的简道云数据,跳过。") + continue + + # 查询实例状态 + instance_status = api_instance.workflow_instance_get({"data_id": data_id}) + task_status = instance_status.get("status", -1) + print(f"订单 {item.get('order_code')} 的简道云流程状态为 {task_status}") + + if task_status == 0: + print("简道云流程正在进行中,执行流程关闭") + api_instance.workflow_instance_end({"data_id": data_id}) + + target_values = { + "_widget_1764820541674": item.get("order_code"), + "_widget_1764820541679": item.get("pay_time"), + "_widget_1764820541676": item.get("payment_amount"), + "_widget_1764820541680": item.get("bussiness_type"), + } + + data_to_update = {} + for widget_id, new_value in target_values.items(): + existing_value = extract_widget_value(result, widget_id) + if has_value(existing_value): + continue + if not has_value(new_value): + continue + data_to_update[widget_id] = {"value": new_value} + + if not data_to_update: + print(f"订单 {item.get('order_code')} 简道云字段已有值或无可写入内容,跳过同步") + continue + + print("开始同步数据") + update_payload = { + "api_key": "675b900991ad2491c69389ca", + "entry_id": "6931063d64187eaf6b927557", + "data_id": data_id, + "data": data_to_update + } + print(update_payload) + api_instance.entry_data_update(update_payload) + print("数据同步完成") + + def main(self): + task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + try: + # step1 获取简道云与宜搭数据 + jd_ydy_data = self.load_all_data() + if jd_ydy_data: + # step2 过滤已经续约的单子 + update_data_list = self.filter_renewal_data(jd_ydy_data) + # step3 校验简道云是否有进行中的单子并关闭 + self.check_jd_ydy_data(update_data_list) + else: + print("本次执行无处理数据") + except Exception as e: + print(e) + + +if __name__ == '__main__': + jd_ydy_renewal_to_do = YDToJDYRenewalToDo() + jd_ydy_renewal_to_do.main() diff --git a/test/续约待办宜搭传给简道云.py b/test/续约待办宜搭传给简道云-5min.py similarity index 99% rename from test/续约待办宜搭传给简道云.py rename to test/续约待办宜搭传给简道云-5min.py index f7740cf..fb2c024 100644 --- a/test/续约待办宜搭传给简道云.py +++ b/test/续约待办宜搭传给简道云-5min.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os from datetime import datetime, timedelta, timezone import pandas as pd diff --git a/test/续约待办简道云回传宜搭.py b/test/续约待办简道云回传宜搭.py index cb817be..218b189 100644 --- a/test/续约待办简道云回传宜搭.py +++ b/test/续约待办简道云回传宜搭.py @@ -233,6 +233,31 @@ class YDAPI: appKey = "ding5kqocon5s9oph5uq" appSecret = "HL1jgsIIfLAC0eTH0A1m4mwxUDqbgsiPeCCGGE3ocM6qJBTIW7Ivt9drxF_Z4Kb_" + @staticmethod + def _to_int_task_id(task_id): + if task_id is None: + raise ValueError("taskId is None") + if isinstance(task_id, bool): + raise ValueError(f"taskId is bool: {task_id}") + if isinstance(task_id, int): + return task_id + if isinstance(task_id, float): + if task_id.is_integer(): + return int(task_id) + raise ValueError(f"taskId is non-integer float: {task_id}") + if isinstance(task_id, str): + s = task_id.strip() + if s.isdigit() or (s.startswith("-") and s[1:].isdigit()): + return int(s) + try: + f = float(s) + except ValueError as e: + raise ValueError(f"taskId is not numeric: {task_id}") from e + if f.is_integer(): + return int(f) + raise ValueError(f"taskId is non-integer numeric string: {task_id}") + raise ValueError(f"taskId has unsupported type: {type(task_id).__name__}") + def generateToken(self) -> str: """ 函数功能:生成访问令牌(token) @@ -301,10 +326,10 @@ class YDAPI: while True: if attempt >= max_retries: # error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取流程实例数据,跳过本次请求。") - break + return {"data": []} try: - res = requests.post(api, headers=headers, json=formData) + res = requests.post(api, headers=headers, json=formData, timeout=15) # print(res.json()) res.raise_for_status() # 如果返回状态码不是2xx,抛出异常 return res.json() @@ -313,6 +338,11 @@ class YDAPI: # logger.warning(f"请求异常: {e},正在尝试第 {attempt + 1} 次重试...") time.sleep(delay) attempt += 1 + except Exception: + time.sleep(delay) + attempt += 1 + + return {"data": []} def update_from(self, token, formInstanceId, data_new): """ @@ -345,7 +375,7 @@ class YDAPI: } - res = requests.put(api, headers=headers, json=payload) + res = requests.put(api, headers=headers, json=payload, timeout=15) return res def get_approval_records(self, token: str, processInstanceId: str, appType="APP_UYZ0KG6L0CCNV80GZ66O", @@ -375,16 +405,17 @@ class YDAPI: while True: if attempt >= max_retries: # error_task_logger.error(f"请求失败,已达最大重试次数 {max_retries},无法获取审批数据,跳过本次请求。") - break + return {"result": []} try: - res = requests.get(api, headers=headers) + res = requests.get(api, headers=headers, timeout=15) res.raise_for_status() # 如果响应状态码不是2xx,则抛出HTTPError return res.json() except (requests.exceptions.RequestException, Exception) as e: # logger.warning(f"请求出现异常: {e}, 正在重试({attempt + 1}/{max_retries})...") time.sleep(delay) # 等待指定的延迟时间后再次尝试 attempt += 1 + return {"result": []} def aggree_approval(self, token: str, taskId: str, processInstanceId: str, formData: dict, res_new): """_summary_ @@ -415,10 +446,10 @@ class YDAPI: "processInstanceId": processInstanceId, "userId": res_new, "language": "zh_CN", - "taskId": int(taskId) + "taskId": self._to_int_task_id(taskId) } - res = requests.post(api, headers=headers, json=payload) + res = requests.post(api, headers=headers, json=payload, timeout=15) return res @@ -453,6 +484,18 @@ class JDYToYDRenewalToDo(object): "数据ID": "_id" } + @staticmethod + def _as_list(val): + if val is None: + return [] + if isinstance(val, list): + return val + if isinstance(val, tuple): + return list(val) + if isinstance(val, dict): + return [val] + return [] + def load_all_data(self): # 获取简道云已派发续约待办,若无数据直接返回 today_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d") @@ -464,7 +507,7 @@ class JDYToYDRenewalToDo(object): "value": ["否"]}]}, } renewal = api_instance.entry_data_list(payload) - self.renewal_data_list = renewal.get("data") or [] + self.renewal_data_list = self._as_list((renewal or {}).get("data")) if not self.renewal_data_list: self.renewal_data_df = pd.DataFrame() return @@ -479,7 +522,7 @@ class JDYToYDRenewalToDo(object): all_data = [] for _, row in self.renewal_data_df.iterrows(): - yd_data = yd_api_instance.read_processes_instances( + yd_resp = yd_api_instance.read_processes_instances( token=self.token, formUuid=self.FORMID, page=1, @@ -488,9 +531,12 @@ class JDYToYDRenewalToDo(object): systemToken=self.systemToken, instanceStatus="", searchFieldJson={"textField_ksydghqw": row["_widget_1764820541661"]}, - ).get("data", []) + ) + yd_data = self._as_list((yd_resp or {}).get("data")) for record in yd_data: + if not isinstance(record, dict): + continue enriched = {**record} enriched.update({k: row.get(v, "") for k, v in self.follow_up_fields.items()}) all_data.append(enriched) @@ -705,20 +751,20 @@ class JDYToYDRenewalToDo(object): data = {"data_id": item["数据ID"]} jdy_workflow_data = api_instance.workflow_instance_get(data) or {} print("简道云流程日志:", jdy_workflow_data) - jdy_logs = jdy_workflow_data.get("logs", []) + jdy_logs = self._as_list(jdy_workflow_data.get("logs")) jdy_logs = sorted( jdy_logs, key=lambda x: parse_dt(x.get("finish_time") or x.get("create_time")), reverse=True, ) - tasks = jdy_workflow_data.get("tasks", []) or [] + tasks = self._as_list(jdy_workflow_data.get("tasks")) pending = [t for t in tasks if t.get("status") == 0] task_candidate = (pending[0] if pending else None) or (sorted( tasks, key=lambda x: parse_dt(x.get("create_time")), reverse=True )[0] if tasks else {}) - jdy_result_records = jdy_workflow_data.get("result", []) or [] + jdy_result_records = self._as_list(jdy_workflow_data.get("result")) jdy_stage_candidates = [ extract_stage_from_text(str(jdy_logs[0].get("flow_name", ""))) if jdy_logs else None, @@ -741,7 +787,7 @@ class JDYToYDRenewalToDo(object): systemToken=self.systemToken ) or {} print("宜搭流程日志:", yd_workflow_data) - yd_results = yd_workflow_data.get("result", []) or [] + yd_results = self._as_list(yd_workflow_data.get("result")) # 展开宜搭 domainList 以获取所有动作 yd_records = [ @@ -785,6 +831,11 @@ class JDYToYDRenewalToDo(object): if not task_id or not operator_user_id: print(f"缺少 taskId 或 operatorUserId,无法自动同意 processInstanceId={item['processInstanceId']}") continue + try: + task_id = yd_api_instance._to_int_task_id(task_id) + except Exception as e: + print(f"taskId 非法,跳过自动同意 processInstanceId={item['processInstanceId']} taskId={task_id} err={e}") + continue try: yd_api_instance.aggree_approval( token=self.token, @@ -799,10 +850,11 @@ class JDYToYDRenewalToDo(object): def retrun_jdy(self, yd_update_list): for item in yd_update_list: + data_id= str(item.get('数据ID')) data = { "api_key": "675b900991ad2491c69389ca", "entry_id": "6931063d64187eaf6b927557", - "data_id": item.get('数据ID'), + "data_id": data_id, "data": { "_widget_1766469131897": {"value": "是"}, diff --git a/test/续约请求接口结果保存.ipynb b/test/续约请求接口结果保存.ipynb new file mode 100644 index 0000000..70b7ad2 --- /dev/null +++ b/test/续约请求接口结果保存.ipynb @@ -0,0 +1,138 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 保存boss请求结果", + "id": "311a82d4faf8e2d" + }, + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-11-10T06:24:23.858755Z", + "start_time": "2025-11-10T06:24:22.994108Z" + } + }, + "source": [ + "# 标准库\n", + "import os\n", + "import time\n", + "import random\n", + "import json\n", + "import binascii\n", + "from datetime import date, timedelta, datetime\n", + "from urllib.parse import quote\n", + "from pathlib import Path\n", + "\n", + "# 第三方库\n", + "import numpy as np\n", + "import pandas as pd\n", + "import requests\n", + "from pyDes import des, CBC, PAD_PKCS5\n", + "import mysql.connector\n", + "from mysql.connector import Error\n", + "\n", + "# PostgreSQL(如果你用到了)\n", + "import psycopg2\n", + "\n", + "# 自定义模块\n", + "from config import Config\n", + "from api import API\n", + "from back_ground_module import CommonModule\n", + "from log_config import configure_task_logger, configure_error_task_logger\n", + "\n", + "\n", + "logger = configure_task_logger()\n", + "error_task_logger = configure_error_task_logger()\n", + "api_instance = API()\n", + "common_module = CommonModule()\n", + "output_dir = \"output\" # 设置输出目录\n", + "os.makedirs(output_dir, exist_ok=True)\n", + "\n", + "def des_encrypt(s):\n", + " \"\"\"\n", + " DES 加密\n", + " :param s: 原始字符串\n", + " :return: 加密后字符串,16进制\n", + " \"\"\"\n", + " secret_key = 'HwdMBW8o'\n", + " iv = secret_key\n", + " k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n", + " en = k.encrypt(s, padmode=PAD_PKCS5)\n", + " return binascii.b2a_base64(en, newline=False)\n", + "\n", + "\n", + "def des_descrypt(s):\n", + " \"\"\"\n", + " DES 解密\n", + " :param s: 加密后的字符串,16进制\n", + " :return: 解密后的字符串\n", + " \"\"\"\n", + " secret_key = 'HwdMBW8o'\n", + " iv = secret_key\n", + " k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)\n", + " de = k.decrypt(binascii.a2b_base64(s), padmode=PAD_PKCS5)\n", + " return de\n", + "\n", + "data_NGV = common_module.get_renewal_details()\n", + "\n", + "\n", + "for i in range(0,len(data_NGV[\"date_fmt\"])):\n", + " t = time.time()\n", + " ts = int(round(t * 1000))\n", + " randint = random.randint(100000000, 999999999)\n", + " req = data_NGV['id_own_org'][i] + \"_\" + str(ts) + \"_\" + str(randint)\n", + " str_en = des_encrypt(req)\n", + " req_new = str_en.decode('utf-8')\n", + "\n", + " url = f\"http://manage.f6yc.com/hive-admin/py/yida/renewal/orgInfo\"\n", + " data = {\n", + " 'req':req_new,\n", + " 't':ts,\n", + " 'r':randint\n", + " }\n", + " res = requests.post(url,data=data)\n", + " # print(res.json.json())\n", + "\n", + " break\n", + "\n", + "print(len(data_NGV))" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "距离今天还有120天的日期是:2026-03-10\n", + "29\n" + ] + } + ], + "execution_count": 2 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/test/非标 b/test/非标 new file mode 100644 index 0000000..70fd441 --- /dev/null +++ b/test/非标 @@ -0,0 +1,71 @@ +,_id,提交人,updater,deleter,提交时间,更新时间,deleteTime,flowState,报备类型,协作内容,情况说明,订单编号,年限,版本,实付金额,商品名称,履约金额,门店编码,门店名称,支付日期,开户/处理日期,业绩归属日期,业绩类型,公司名称,公司ID,报备业绩金额-区域提交,业绩归属小六-区域提交,业绩归属月,是否同步衡石,小六业绩金额,区域业绩金额,报备业绩归属小六,报备业绩归属区域经理,报备业绩归属大区,原业绩归属人,原业绩归属区域经理,原业绩归属大区,小六业绩比例,区域业绩比例,运营专家,业绩动作,提成类型,新签阶段及提成比例,提成金额,SaaS新签提成比例,服务包提成比例,新签提成比例-首年,新签提成比例-非首年,提成动作,业绩类型-聚合,业绩分组,流程是否结束,appId,entryId +0,68d1039a408016fe13556b06,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:06:50,2025-12-25 15:32:02,,2,多年补差价,,6月订单,9月补差价2年,1757735155184,2,入门版,999,,,CHS202506010300195,上海汨晨汽车服务有限公司,2025-09-13 00:00:00,2025-06-01 00:00:00,2025-09-13 00:00:00,新签,,,,,,是,999.0,999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +1,68d103cdb8f662bfdf1b7ea2,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:07:41,2025-12-25 15:32:02,,2,新签超3年,,新签5年,1758076816064,2,标准版,3000,,,CHS202302130204882,江阴市华士爱驹汽车养护店,2025-09-17 00:00:00,2025-09-17 00:00:00,2025-09-17 00:00:00,新签,,,,,,是,3000.0,3000.0,赵旭伟,肖军,江苏,赵旭伟,肖军,江苏,1.0,1.0,陈博,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +2,68d103f17f34705c8dbe360a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:08:17,2025-12-25 15:32:02,,2,新签超3年,,新签5年,1758181826462,2,基础版,1600,,,CHS202504240297202,古城区鸿远轮胎服务中心,2025-09-18 00:00:00,2025-09-18 00:00:00,2025-09-18 00:00:00,新签,,,,,,是,1600.0,1600.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +3,68d10415f3728b4cd791630e,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:08:53,2025-12-25 15:32:02,,2,新签超3年,,5年订单,1758259644403,2,基础版,1759,,,CHS202509190309704,金堂县赵镇四达汽修厂,2025-09-19 00:00:00,2025-09-19 00:00:00,2025-09-19 00:00:00,新签,,,,,,是,1759.0,1759.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +4,68d1042ee3ee1af6d0d7f8ee,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:09:18,2025-12-25 15:32:02,,2,新签超3年,,,1756814144233,2,入门版,1000,,,CHS202509020309092,天津市滨海新区安驰汽车修理服务部,2025-09-03 00:00:00,2025-09-02 00:00:00,2025-09-03 00:00:00,新签,,,,,,是,1000.0,1000.0,王鑫,关磊,华北,王鑫,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +5,68d104502618b5ea53f4264f,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:09:52,2025-12-25 15:32:02,,2,新签超3年,,,1757245487196,2,基础版,1400,,,CHS202509070309251,车广角盘锦店,2025-09-08 00:00:00,2025-09-07 00:00:00,2025-09-08 00:00:00,新签,,,,,,是,1400.0,1400.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +6,68d10471c1d4a4211d2ce420,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:10:25,2025-12-25 15:32:02,,2,新签超3年,,,1757388589517,1,进阶版,1000,,,CHS202509090309331,上海德伽汽车服务中心,2025-09-09 00:00:00,2025-09-09 00:00:00,2025-09-09 00:00:00,新签,,,,,,是,1000.0,1000.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +7,68d104a081bf67abc88ce650,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:11:12,2025-12-25 15:32:02,,2,新签超3年,,,1757752521945,2,标准版,3000,,,CHS202509130309507,腾冲诚亿汽车修理有限公司,2025-09-14 00:00:00,2025-09-13 00:00:00,2025-09-14 00:00:00,新签,,,,,,是,3000.0,3000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +8,68d104e0304ea14df52c1128,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:12:16,2025-12-25 15:32:02,,2,品牌方协作,电子目录,电子目录,,,,12000,,,,,,,2025-09-22 16:12:16,,,,,,,是,0.0,0.0,杜浩,肖军,江苏,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +9,68d104fce01701d04e9ba14d,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:12:44,2025-12-25 15:32:02,,2,品牌方协作,电子目录,,,,,8000,,,,,,,2025-09-22 16:12:44,,,,,,,是,0.0,0.0,韩皞,陈庆伟,东北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +10,68d105177771c4e88d61d725,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-22 16:13:11,2025-12-25 15:32:02,,2,品牌方协作,电子目录,,,,,9000,,,,,,,2025-09-22 16:13:11,,,,,,,是,0.0,0.0,胡楠,景东强,西北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +11,68d2342ba541a358893d61ef,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-23 13:46:19,2025-12-25 15:32:02,,2,多年补差价,,,1758459953833,2,入门版,1000,,,CHS202505070297933,上海义诚汽车服务有限公司,2025-09-22 00:00:00,2025-05-07 00:00:00,2025-09-22 00:00:00,新签,,,,,,是,1000.0,1000.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +12,68d3bc8d99f9d49450ed8b1a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-24 17:40:29,2025-12-25 15:32:02,,2,品牌方协作,电子目录,电子目录,小六未参与,,,,9000,,,,,,,2025-09-24 17:40:29,,,,,,,是,0.0,0.0,胡楠,景东强,西北,,,,,,,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +13,68d494d8c6f070c9f68a4e94,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-25 09:03:20,2025-12-25 15:32:02,,2,新签超3年,,,1758612872486,1,基础版,799,,,CHS202509230309865,回民区青辰宝悦汽车维修中心(个体工商户),2025-09-23 00:00:00,2025-09-23 00:00:00,2025-09-23 00:00:00,新签,,,,,,是,799.0,799.0,张宏伟,关磊,华北,张宏伟,关磊,华北,1.0,1.0,武宏超,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +14,68d5f2a0b3bc5add3c3d7a23,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:55:44,2025-12-25 15:32:02,,2,新签超3年,,,1758789984175,2,入门版,1000,,,CHS202509250310033,济南双江汽车服务有限公司,2025-09-26 00:00:00,2025-09-25 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,1000.0,1000.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +15,68d5f2c3f7765d3eddb8506a,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:56:19,2025-12-25 15:32:02,,2,多年补差价,,,1758787369355,2,旗舰版,4000,,,CHS202505190299143,政德汽修,2025-09-26 00:00:00,2025-05-19 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,4000.0,4000.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +16,68d5f2eb8eb300e7d401f7f9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-26 09:56:59,2025-12-25 15:32:02,,2,新签超3年,,,1758786185157,2,标准版,3120,,,CHS202509250310019,红河州秀林工贸有限公司,2025-09-26 00:00:00,2025-09-25 00:00:00,2025-09-26 00:00:00,新签,,,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +17,68d8882a40f51cce582eddb5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-28 08:58:18,2025-12-25 15:32:02,,2,多年补差价,,,1758894296058,2,进阶版,2501,,,CHS202107030131629,灵山县车益汽车维修厂,2025-09-27 00:00:00,2025-08-09 00:00:00,2025-09-27 00:00:00,新签,,,,,,是,2501.0,2501.0,黄环宇,张凯,华南沪,黄环宇,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],125.05,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +18,68d9e2d9710265914f554379,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-29 09:37:29,2025-12-25 15:32:02,,2,跨区新签,,,1759047159482,3,旗舰版,7000,,,CHS202509280310174,荟星行汽车服务有限公司,2025-09-29 00:00:00,2025-09-28 00:00:00,2025-09-29 00:00:00,新签,,,,,,是,3500.0,3500.0,韩皞,陈庆伟,东北,张宏伟,关磊,华北,0.5,0.5,孙旭亮,拆单,,[],315.0,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +19,68d9f2549d0de29d680f6403,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-29 10:43:32,2025-12-25 15:32:02,,2,新签超3年,,,1759063075745,2,入门版,920,,,CHS202509280310180,宏运汽修,2025-09-29 00:00:00,2025-09-28 00:00:00,2025-09-29 00:00:00,新签,,,,,,是,920.0,920.0,柴铁峰,陈庆伟,东北,柴铁峰,陈庆伟,东北,1.0,1.0,刘立,新增,,[],46.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +20,68db3e73890706b7678f34ea,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-30 10:20:35,2025-12-25 15:32:02,,2,新签超3年,,,1757909706794,2,旗舰版,4400,,,CHS202509150309534,监利市壹加汽车服务有限公司,2025-09-15 00:00:00,2025-09-15 00:00:00,2025-09-15 00:00:00,新签,,,,,,是,4400.0,4400.0,陈煜,景东强,华中,陈煜,景东强,华中,1.0,1.0,刘光春,新增,,[],220.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +21,68db50ff8e50807e1e84e8f8,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-09-30 11:39:43,2025-12-25 15:32:02,,2,新签超3年,,,1759039606751,1,标准版,1500,,,CHS202509280310149,鄂尔多斯市心成泰汽车维修服务有限公司,2025-09-28 00:00:00,2025-09-28 00:00:00,2025-09-28 00:00:00,新签,,,,,,是,1500.0,1500.0,张宏伟,关磊,华北,张宏伟,关磊,华北,1.0,1.0,武宏超,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +22,68e71b9216dd2af696d3c489,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-09 10:18:58,2025-12-25 15:32:02,,2,新签超3年,,,1759573927652,1,基础版,800,,,CHS202303010210121,德系专修,2025-10-05 00:00:00,2025-10-04 00:00:00,2025-10-05 00:00:00,新签,,,,,,是,800.0,800.0,刘磊,关磊,华北,刘磊,关磊,华北,1.0,1.0,,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +23,68e768c27a4eb1ea616434b0,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-09 15:48:18,2025-12-25 15:32:02,,2,新签超3年,,,1759395920361,2,进阶版,2000,,,CHS202510020310341,官渡区众名汽车维修服务经营部,2025-10-03 00:00:00,2025-10-03 00:00:00,2025-10-03 00:00:00,新签,,,,,,是,2000.0,2000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],100.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +24,68e9ba0cd69c47d29c2e0972,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-11 09:59:40,2025-12-25 15:32:02,,2,新签超3年,,,1760012463416,1,进阶版,1000,,,CHS202510090310521,新乡骏享汽车销售服务有限公司,2025-10-10 00:00:00,2025-10-10 00:00:00,2025-10-10 00:00:00,新签,,,,,,是,1000.0,1000.0,王兵帅,张凯,河南,王兵帅,张凯,河南,1.0,1.0,邢恒岭,新增,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +25,68eda452872200f9abed2d5d,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-14 09:16:02,2025-12-25 15:32:02,,2,多年补差价,,,1760180223661,2,进阶版,1999,,,CHS202507090303811,上海洗事临门汽车服务有限公司,2025-10-12 00:00:00,2025-07-09 00:00:00,2025-10-12 00:00:00,新签,,,,,,是,1999.0,1999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],99.95,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +26,68f830d486f984a8f5d58949,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-22 09:18:12,2025-12-25 15:32:02,,2,电销业绩统计,,,1760341341750,3,基础版,2900,,,CHS202411020284735,深圳康得新KDX大膜王星级甄选店,2025-10-13 00:00:00,2025-10-13 00:00:00,2025-10-13 00:00:00,新签,,,,,,是,0.0,2900.0,严冬延,张凯,华南沪,耿渝淇,张凯,,,1.0,,新增,,[],130.5,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +27,68f98a2a9cdcf060fcebf670,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-23 09:51:38,2025-12-25 15:32:02,,2,新签超3年,,,1761122509436,2,进阶版,2000,,,CHS202409190281922,海口小拇指汽车服务,2025-10-23 00:00:00,2025-10-22 00:00:00,2025-10-23 00:00:00,新签,,,,,,是,2000.0,2000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],100.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +28,68fecdec710ccbf6abfdcf9c,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-27 09:42:04,2025-12-25 15:32:02,,2,新签超3年,,,1761365304042,2,标准版,3200,,,CHS202510250311422,博越汽修,2025-10-25 00:00:00,2025-10-25 00:00:00,2025-10-25 00:00:00,新签,,,,,,是,3200.0,3200.0,王有军,陈庆伟,浙皖,王有军,陈庆伟,浙皖,1.0,1.0,,新增,,[],160.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +29,6901795327c25049c38f1e2b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-29 10:17:55,2025-12-25 15:32:02,,1,新签超3年,,,1761644690009,2,基础版,1600,,,CHS202510280311584,宜良捷驰汽车修理厂,2025-10-29 00:00:00,2025-10-29 00:00:00,2025-10-29 00:00:00,新签,,,,,,是,1600.0,1600.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],80.0,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +30,690179cf34ed36233a8166fd,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-29 10:19:59,2025-12-25 15:32:02,,1,跨区新签,,,1761611436641,1,旗舰版,4499,,,CHS202510280311527,易道大咖乌鲁木齐东坪店,2025-10-28 00:00:00,2025-10-28 00:00:00,2025-10-28 00:00:00,新签,,,,,,是,2249.5,2249.5,孙振华,景东强,西北,潘志强,张凯,华南沪,0.5,0.5,,拆单,,[],0.0,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +31,69041f3e38fb7000e0ffc32e,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-10-31 10:30:22,2025-12-25 15:32:02,,1,新签超3年,,,1761816948126,2,至尊版,8000,,,CHS202510300312383,意嘉易春天里店,2025-10-31 00:00:00,2025-10-31 00:00:00,2025-10-31 00:00:00,新签,,,,,,是,8000.0,8000.0,范启超,肖军,西南,范启超,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +32,69095a58a9520f1eeaf2afcb,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-04 09:43:52,2025-12-25 15:32:02,,1,新签超3年,,,1762158897286,2,基础版,5000,续约旗舰版-门店管理系统2年,2500.0,CHS202511030312575,宣威市龙泉汽车有限公司,2025-11-04 00:00:00,2025-11-04 00:00:00,2025-11-04 00:00:00,新签,,,,,,是,5000.0,5000.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +33,690aae75afd572c006769a86,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:55:01,2025-12-25 15:32:02,,1,新签超3年,,,1762245527558,2,进阶版,2240,续约进阶版2年,1120.0,CHS202312020253454,乐山郭建军,2025-11-05 00:00:00,2025-11-05 00:00:00,2025-11-05 00:00:00,新签,,,,,,是,2240.0,2240.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +34,690aae91ff9575eb8a758dc3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:55:29,2025-12-25 15:32:02,,1,多年补差价,,,1761960739508,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202509230309853,都江堰市爱都汽车维修有限责任公司,2025-11-01 00:00:00,2025-09-23 00:00:00,2025-11-01 00:00:00,新签,,,,,,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +35,690aaec3f23eb35e6e394e54,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-05 09:56:19,2025-12-25 15:32:02,,1,多年补差价,,,1761955976569,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202509150309531,三越汽车音响,2025-11-01 00:00:00,2025-09-12 00:00:00,2025-11-01 00:00:00,新签,,,,,,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +36,690c00df267ca78f0a86da76,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-06 09:58:55,2025-12-25 15:32:02,,1,新签超3年,,,1762263761814,2,入门版,1600,续约入门版2年,800.0,CHS202105140124767,茂名市电白区华南汽车维修有限公司,2025-11-05 00:00:00,2025-11-04 00:00:00,2025-11-05 00:00:00,新签,,,,,,是,1600.0,1600.0,严冬延,张凯,华南沪,严冬延,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +37,690d4feafeb41d02c491646b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-07 09:48:26,2025-12-25 15:32:02,,1,多年补差价,,,1762418825828,2,旗舰版,3599,续约旗舰版-门店管理系统2年,1799.5,CHS202508090305351,山海车服,2025-11-07 00:00:00,2025-08-09 00:00:00,2025-11-07 00:00:00,新签,上海山高海深汽车服务有限公司,15975930111903944708,,,,是,3599.0,3599.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +38,690d5114ebf892f67c83c3d9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-07 09:53:24,2025-12-25 15:32:02,,1,新签超3年,,,1762401310694,2,进阶版,2240,续约进阶版2年,1120.0,CHS202003250058491,博伟汽修(新南路),2025-11-06 00:00:00,2025-11-06 00:00:00,2025-11-06 00:00:00,新签,博伟汽修,10546443563986851726,,,,是,2240.0,2240.0,胡仲远,肖军,西南,胡仲远,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +39,6915529caca516e7c6c28b71,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-13 11:38:04,2025-12-25 15:32:02,,1,多年补差价,,,1762933140216,2,基础版,1998,续约基础版-门店管理系统2年,999.0,CHS202209140188566,德奥养车,2025-11-12 00:00:00,2025-11-08 00:00:00,2025-11-12 00:00:00,新签,德奥养车,11240984669917483539,,,,是,1998.0,1998.0,陈晨,关磊,华北,陈晨,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +40,691a76c508e9a99cb7e65565,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-17 09:13:41,2025-12-25 15:32:02,,1,新签超3年,,,1763100107052,2,标准版,3120,续约标准版-门店管理系统2年,1560.0,CHS202511150313111,云南锡业集团汽车技术服务有限公司,2025-11-14 00:00:00,2025-11-14 00:00:00,2025-11-14 00:00:00,新签,云南锡业集团汽车技术服务(文山服务中心),16011444960305905673,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +41,691c0a70d6f09daa8e0af427,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:00,2025-12-25 15:32:02,,1,多年补差价,,,1763366190883,2,入门版,999,续约入门版2年,499.5,CHS202509020309077,宜嘉养车,2025-11-17 00:00:00,2025-09-02 00:00:00,2025-11-17 00:00:00,新签,宜嘉养车,15984698892155383884,,,,是,999.0,999.0,刘鑫烨,张凯,华南沪,刘鑫烨,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +42,691c0a845f80e95f659f9b26,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:20,2025-12-25 15:32:02,,1,新签超3年,,,1763351228539,1,进阶版,1000,续约进阶版,1000.0,CHS202511170313156,郑州欧汇汽车维修有限公司,2025-11-17 00:00:00,2025-11-17 00:00:00,2025-11-17 00:00:00,新签,郑州欧汇汽车维修有限公司,16012185257256194131,,,,是,1000.0,1000.0,王兵帅,张凯,河南,王兵帅,张凯,河南,1.0,1.0,邢恒岭,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +43,691c0a9576f2783074f62089,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-18 13:56:37,2025-12-25 15:32:02,,1,新签超3年,,,1763370516248,2,标准版,3120,续约标准版-门店管理系统2年,1560.0,CHS202511170313182,文山万霆新能源科技有限责任公司,2025-11-18 00:00:00,2025-11-18 00:00:00,2025-11-18 00:00:00,新签,文山万霆新能源科技有限责任公司,16012268807246610438,,,,是,3120.0,3120.0,李壮壮,肖军,西南,李壮壮,肖军,西南,1.0,1.0,崔智杰,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +44,691d39d9de406a40ae9c1859,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-19 11:30:33,2025-12-25 15:32:02,,1,多年补差价,,,1763447333680,2,入门版,800,续约入门版2年,400.0,CHS202510070310394,盛发汽车维修保养中心,2025-11-18 00:00:00,2025-10-07 00:00:00,2025-11-18 00:00:00,新签,滨海凯盛汽修养护中心,15997329870740811799,,,,是,800.0,800.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +45,691e88e504528289184a3d00,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-20 11:20:05,2025-12-25 15:32:02,,1,多年补差价,,,1763522175031,2,基础版,2300,续约基础版-门店管理系统2年,1150.0,CHS202511190313271,徐宝行汽车服务有限公司,2025-11-19 00:00:00,2025-11-19 00:00:00,2025-11-19 00:00:00,新签,徐州徐宝行汽车服务有限公司,16012883105031421976,,,,是,2300.0,2300.0,赵涛,肖军,江苏,赵涛,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +46,691fdb3878dc061019100add,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-21 11:23:36,2025-12-25 15:32:02,,1,多年补差价,,,1763619899692,2,入门版,700,续约入门版2年,350.0,CHS202510230311351,梵远汽车服务,2025-11-20 00:00:00,2025-10-23 00:00:00,2025-11-20 00:00:00,新签,南京市雨花台区梵远汽车配件销售中心,16003167838416171074,,,,是,700.0,700.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +47,6927e49b21b02c48ca7bb1f5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-27 13:41:47,2025-12-25 15:32:02,,1,多年补差价,,,1764149126832,2,进阶版,1799,续约进阶版2年,899.5,CHS202511080312789,鼎鹏汽车服务中心,2025-11-27 00:00:00,2025-11-08 00:00:00,2025-11-27 00:00:00,新签,贵阳市云岩区鼎鹏汽车养护中心,16008912692744061003,,,,是,1799.0,1799.0,熊斌,肖军,西南,熊斌,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +48,6928ffbe82767255aabb44be,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:49:50,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,18000,,,,,,,2025-11-27 00:00:00,新签,,,,,,是,9000.0,18000.0,梁柱,张凯,华南沪,,,,0.5,1.0,黄宗祥,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +49,6928fff076d8fe5abdd61266,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:50:40,2025-12-25 15:32:02,,1,新签超3年,,,1762590806733,2,基础版,1600,续约基础版-门店管理系统2年,800.0,CHS202511080312804,华营昌检车线店,2025-11-09 00:00:00,2025-11-08 00:00:00,2025-11-09 00:00:00,新签,阳泉华营昌汽修连锁,10907434497378123878,,,,是,1600.0,1600.0,刘剑桥,关磊,华北,刘剑桥,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +50,692900560e8ab9d6604193f2,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 09:52:22,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,5000,,,,,,,2025-11-07 00:00:00,新签,,,,,,是,2500.0,5000.0,赵旭伟,肖军,江苏,,,,0.5,1.0,陈博,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +51,6929055f9e94d391e9cd53dd,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-11-28 10:13:51,2025-12-25 15:32:02,,1,新签超3年,,,1762739543304,2,入门版,1200,续约入门版2年,600.0,CHS202511090312841,小欢汽车维修,2025-11-10 00:00:00,2025-11-10 00:00:00,2025-11-10 00:00:00,新签,义县稍户营子镇小欢汽车维修处(个体工商户),16009461720019927075,,,,是,1200.0,1200.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +52,692cfbd8da792279e268feef,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:16,2025-12-25 15:32:02,,1,多年补差价,,,1764382941584,2,入门版,800,续约入门版2年,400.0,CHS202510170310927,盐城市大丰区辰煜汽车服务有限公司,2025-11-29 00:00:00,2025-10-18 00:00:00,2025-11-29 00:00:00,新签,盐城市大丰区辰煜汽车服务有限公司,10546443563782178538,,,,是,800.0,800.0,杜浩,肖军,江苏,杜浩,肖军,江苏,1.0,1.0,霍创业,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +53,692cfbe7ff1b578004611ddc,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:31,2025-12-25 15:32:02,,1,新签超3年,,,1764333612264,2,标准版,3600,续约标准版-门店管理系统2年,1800.0,CHS202511280313670,厦门市鑫车宝汽车服务有限公司,2025-11-29 00:00:00,2025-11-28 00:00:00,2025-11-29 00:00:00,新签,厦门市鑫车宝汽车服务有限公司,16016206505594359823,,,,是,3600.0,3600.0,郭锦城,张凯,华南沪,郭锦城,张凯,华南沪,1.0,1.0,周聪,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +54,692cfc00bc219a1741c27ffc,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-01 10:22:56,2025-12-25 15:32:02,,1,多年补差价,,,1764502026835,2,进阶版,2001,续约进阶版2年,1000.5,CHS202511250313539,天门市小拇指汽车维修服务有限公司,2025-12-01 00:00:00,2025-11-26 00:00:00,2025-12-01 00:00:00,新签,天门市小拇指汽车维修服务有限公司,16015175963579027532,,,,是,2001.0,2001.0,陈煜,景东强,华中,陈煜,景东强,华中,1.0,1.0,刘光春,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +55,6938df09cc655df92928b5b3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-10 10:46:33,2025-12-25 15:32:02,,1,品牌方协作,电子目录,,,,,8000,,,,,,,2025-12-10 00:00:00,新签,,,8000,,2025-12-01 00:00:00,是,4000.0,8000.0,孙旭亮,陈庆伟,东北,,,,0.5,1.0,孙旭亮,新增,,[],,,,,,无,电子目录,电子目录,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +56,69391bbdcdfe09bce4c98cd7,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-10 15:05:33,2025-12-25 15:32:02,,1,新签超3年,,,1764473175419,2,进阶版,2000,续约进阶版2年,1000.0,CHS202511300313735,唐山市路南新腾云汽车维修服务站,2025-11-30 00:00:00,2025-11-30 00:00:00,2025-11-30 00:00:00,新签,唐山市路南新腾云汽车维修服务站,16016880445115371607,,,,是,2000.0,2000.0,王鑫,关磊,华北,王鑫,关磊,华北,1.0,1.0,杨挺,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +57,693b7c9a25d3e9c841729ed3,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-12 10:23:22,2025-12-25 15:32:02,,1,新签超3年,,,1765420934693,2,入门版,1000,续约入门版2年,500.0,CHS202512110314232,西昌安宁美车度汽修,2025-12-11 00:00:00,2025-12-11 00:00:00,2025-12-11 00:00:00,新签,西昌安宁美车度汽车喷漆中心,16020868221515104344,,,,是,1000.0,1000.0,杨君毅,肖军,西南,杨君毅,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +58,693f6a268691ed316215299b,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:53:42,2025-12-25 15:32:02,,1,跨区新签,,,1765158079178,3,基础版,3000,基础版-门店管理系统3年,1000.0,CHS202512080314089,鹊大师(大同店),2025-12-08 00:00:00,2025-12-08 00:00:00,2025-12-08 00:00:00,新签,广之源汽车一站式服务中心,16000651004727029792,,,,是,1500.0,1500.0,张宏伟,关磊,华北,韩皞,关磊,东北,0.5,0.5,杨挺,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +59,693f6a41b051e5517a47eed5,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:54:09,2025-12-25 15:32:02,,1,跨区新签,,,1765358368741,3,至尊版,11997,至尊版-门店管理系统3年,3999.0,CHS202512100314217,鞍山尊荣万顺汽车销售有限公司,2025-12-10 00:00:00,2025-12-10 00:00:00,2025-12-10 00:00:00,新签,鞍山尊荣万顺汽车销售有限公司,16020612536361586766,,,,是,5998.5,5998.5,宋小涛,陈庆伟,东北,刘磊,陈庆伟,华北,0.5,0.5,孙旭亮,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +60,693f6a522b2bc18aef5bec96,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-15 09:54:26,2025-12-25 15:32:02,,1,多年补差价,,,1765259877744,2,进阶版,2500,续约进阶版2年,1250.0,CHS202509170309603,平湖市万里汽车大修厂,2025-12-09 00:00:00,2025-09-17 00:00:00,2025-12-09 00:00:00,新签,平湖市万里汽车大修厂,15990048390801023028,,,,是,2500.0,2500.0,王有军,陈庆伟,浙皖,王有军,陈庆伟,浙皖,1.0,1.0,魏子淇,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +61,6948aa20b9b289e0b6b0a8d1,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-22 10:17:04,2025-12-25 15:32:02,,1,新签超3年,,,1766292766298,2,入门版,800,续约入门版2年,400.0,CHS202512210314689,博晏爱玩车维修中心,2025-12-21 00:00:00,2025-12-21 00:00:00,2025-12-21 00:00:00,新签,义县稍户营子镇博晏爱玩车汽车服务维修中心,16024514498417168447,,,,是,800.0,800.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,孙旭亮,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +62,6948abbc111dbba0cb0d4fd9,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-22 10:23:56,2025-12-25 15:32:02,,1,多年补差价,,,1766125609196,2,至尊版,8999,续约至尊版-门店管理系统2年,4499.5,CHS202506260302952,烟台福泰汽车服务有限公司,2025-12-19 00:00:00,2025-06-26 00:00:00,2025-12-19 00:00:00,新签,福泰汽车服务有限公司,11240984669918394572,,,,是,8999.0,8999.0,宗川涵,关磊,山东,宗川涵,关磊,山东,1.0,1.0,王斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +63,6949f9571fc4fc20b158497f,"{'name': '陈庆伟', 'username': '025366033037741985', 'status': 1, 'type': 0, 'departments': [122311528, 122229573], 'integrate_id': '025366033037741985'}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-23 10:07:19,2025-12-30 17:35:22,,1,跨区新签,,该单是刘磊跨区浙江的客户,王蔚东培训的,关磊已经让小六把业绩和提成都给王蔚东了,100%算王蔚东的,1765263063272,1,基础版,1199,基础版-门店管理系统,1199.0,CHS202512090314165,舟山市于瑞汽车修理有限公司,2025-12-09 00:00:00,2025-12-09 00:00:00,2025-12-09 00:00:00,新签,舟山市于瑞汽车修理有限公司,16020236055869423684,1199,,2025-12-01 00:00:00,否,599.5,599.5,刘磊,关磊,华北,刘磊,陈庆伟,华北,0.5,0.5,魏子淇,拆单,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +64,694a09358daf16de8df90b12,"{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-23 11:15:01,2025-12-25 15:32:02,,1,新签超3年,,,1766389436606,1,旗舰版,2125,续约旗舰版-门店管理系统,2125.0,CHS202512220314748,PM汽车服务,2025-12-22 00:00:00,2025-12-22 00:00:00,2025-12-22 00:00:00,新签,PM汽车服务,16024929481844097081,2125,,2025-12-01 00:00:00,是,2125.0,2125.0,杨旭,关磊,山东,杨旭,关磊,山东,1.0,1.0,王斌,新增,,[],,0.0,,,0.0,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +65,694b80373f7c91e02f438916,"{'name': '张凯', 'username': '1525590028775887', 'status': 1, 'type': 0, 'departments': [122283546, 127595486, 122514491], 'integrate_id': '1525590028775887'}","{'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}",,2025-12-24 13:55:03,2025-12-25 15:32:02,,1,多年补差价,,新签一个月补1599两年,共计3198三年基础版,1766474568560,2,基础版,1599,续约基础版-门店管理系统2年,799.5,CHS202512070314056,贺州市德宝汽车养护有限公司,2025-12-23 00:00:00,2025-12-07 00:00:00,2025-12-23 00:00:00,新签,贺州市德宝汽车养护有限公司,16019465729355059235,1599,黄宗祥,2025-12-01 00:00:00,是,1599.0,1599.0,黄宗祥,张凯,华南沪,黄宗祥,张凯,华南沪,1.0,1.0,黄宗祥,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +66,694cfd1d990c750feb7392f8,"{'name': '景东强', 'username': '232229053125844557', 'status': 1, 'type': 0, 'departments': [122472424, 122503482], 'integrate_id': '232229053125844557'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-25 17:00:13,2025-12-26 09:45:45,,1,多年补差价,,此客户约定进阶版3年3899元;11月8号付款一年进阶版1999元,12月25日补尾款1900元。胡冰区域,1766652296888,2,进阶版,1900,续约进阶版2年,950.0,CHS202511080312811,宜春晴天汽车服务有限公司,2025-12-25 00:00:00,2025-11-08 00:00:00,2025-12-25 00:00:00,新签,宜春晴天汽车服务有限公司,16009000270293930057,1900,胡冰,2025-12-01 00:00:00,是,1900.0,1900.0,胡冰,景东强,华中,胡冰,景东强,华中,1.0,1.0,金华斌,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +67,69536f0a92ee436c9af5e238,"{'name': '肖军', 'username': '311003461041349', 'status': 1, 'type': 0, 'departments': [122314630, 122323520], 'integrate_id': '311003461041349'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 14:19:54,2025-12-31 10:05:48,,1,多年补差价,,,1767075482440,2,基础版,1101,续约基础版-门店管理系统2年,550.5,CHS202511170313151,成都鑫城南汽车,2025-12-30 00:00:00,2025-11-17 00:00:00,2025-12-30 00:00:00,新签,成都鑫城南汽车,16012170912208031813,1101,陈致欣,2025-12-01 00:00:00,是,1101.0,1101.0,陈致欣,肖军,西南,陈致欣,肖军,西南,1.0,1.0,吴间锐,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +68,6953889d92e8e1b524429a46,"{'name': '景东强', 'username': '232229053125844557', 'status': 1, 'type': 0, 'departments': [122472424, 122503482], 'integrate_id': '232229053125844557'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 16:09:01,2025-12-30 16:15:23,,1,跨区新签,,此客户在孙婷婷区域,但由于客户单店体量较大,于是让陈煜介入洽谈,故业绩陈煜一半9000元。孙婷婷一半9000元。,1765503409986,3,至尊版,17997,至尊版-门店管理系统3年,5999.0,CHS202512120314279,武汉酷卡驰汽车科技有限公司,2025-12-12 00:00:00,2025-12-12 00:00:00,2025-12-12 00:00:00,新签,武汉酷卡驰汽车科技有限公司,16021219284734738438,18000,陈煜,2025-12-01 00:00:00,是,8998.5,8998.5,孙婷婷,景东强,华中,陈煜,景东强,华中,0.5,0.5,刘光春,拆单,,[],,0.135,,,0.135,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 +69,6953bd5a65e053d760df4b80,"{'name': '陈庆伟', 'username': '025366033037741985', 'status': 1, 'type': 0, 'departments': [122311528, 122229573], 'integrate_id': '025366033037741985'}","{'name': '金鹏', 'username': '02545960101197726', 'status': 1, 'type': 0, 'departments': [448339551], 'integrate_id': '02545960101197726'}",,2025-12-30 19:54:02,2025-12-31 09:04:54,,1,多年补差价,,新签3个月内补差价,多年购,1766986879209001,2,进阶版,1600,续约进阶版2年,800.0,CHS202510120310664,鹤岗市南山区鑫宏海中外汽车维修站,2025-12-29 00:00:00,2025-10-12 00:00:00,2025-12-29 00:00:00,新签,鹤岗市南山区鑫宏海中外汽车维修站,15999259585890246734,1600,韩皞,2025-12-01 00:00:00,是,1600.0,1600.0,韩皞,陈庆伟,东北,韩皞,陈庆伟,东北,1.0,1.0,刘立,新增,,[],,0.1,,,0.1,无,新签,当月新签开户,是,66b9678280b37f8a276b1d01,68886b7c0382a7249ae0b5d6 diff --git a/test/非标业绩提报根据拆分做复制.py b/test/非标业绩提报根据拆分做复制.py new file mode 100644 index 0000000..157c1dd --- /dev/null +++ b/test/非标业绩提报根据拆分做复制.py @@ -0,0 +1,326 @@ +# -*- coding: utf-8 -*- +import pandas as pd +import datetime +from config import Config +from api import API +import pymysql # 使用 pymysql 替代 mysql.connector +from back_ground_module import CommonModule +import os +import mysql.connector +import pandas as pd +import json +import numpy as np +import mysql.connector +from mysql.connector import Error +from log_config import configure_task_logger, configure_error_task_logger + +logger = configure_task_logger() +error_task_logger = configure_error_task_logger() +api_instance = API() +common_module = CommonModule() +output_dir = "output" # 设置输出目录 +os.makedirs(output_dir, exist_ok=True) + + +class NonStandardPerformanceToBI: + """ 非标业绩提报转BI""" + def __init__(self): + self.dealer_service_data = None + self.field_mapping = { + "报备类型": "_widget_1753770875899", + "协作内容": "_widget_1753770875915", + "情况说明": "_widget_1753770875944", + "订单编号": "_widget_1753770875887", + "实付金额": "_widget_1753770875889", + "门店编码": "_widget_1753770875890", + "门店名称": "_widget_1753770875888", + "版本": "_widget_1753770875891", + "年限": "_widget_1753948745953", + "支付日期": "_widget_1753770875893", + "开户/处理日期": "_widget_1753770875894", + "小六业绩金额": "_widget_1753770875898", + "区域业绩金额": "_widget_1753770875937", + "报备业绩归属区域经理": "_widget_1753770875903", + "报备业绩归属大区": "_widget_1753866196486", + "原业绩归属人": "_widget_1753856032683", + "原业绩归属区域经理": "_widget_1753866196485", + "小六业绩比例": "_widget_1753770875917", + "区域业绩比例": "_widget_1753770875921", + "运营专家": "_widget_1753770875902", + "提成类型": "_widget_1753778922504", + "SaaS新签提成比例": "_widget_1753770875949", + "服务包提成比例": "_widget_1753778922567", + "提成金额": "_widget_1753770875948", + "新签提成比例-首年": "_widget_1753778922503", + "新签提成比例-非首年": "_widget_1753778922548", + "新签阶段及提成比例": "_widget_1753778656359", + "业绩动作":"_widget_1756708722933", + "提成动作":"_widget_1756708722932", + "新签阶段及提成比例.选择提成阶段": "_widget_1753778656359._widget_1753778656361", + "新签阶段及提成比例.新签阶段": "_widget_1753778656359._widget_1753948745962", + "新签阶段及提成比例.提成比例": "_widget_1753778656359._widget_1753778656362", + "业绩类型":"_widget_1753770875966", + "报备业绩归属小六":"_widget_1753770875901", + "原业绩归属大区":"_widget_1755159216098", + "业绩分类":"_widget_1758706882564", + "流程是否结束":"_widget_1761633418013", + "业绩类型-聚合":"_widget_1758706882564", + "业绩分组":"_widget_1762417447169", + "商品名称":"_widget_1762219744898", + "履约金额":"_widget_1762220516367", + "业绩归属日期":"_widget_1762417447127", + "公司名称":"_widget_1762420723743", + "公司ID":"_widget_1762420723744", + "报备业绩金额-区域提交":"_widget_1766375035236", + "业绩归属小六-区域提交":"_widget_1766461143813", + "业绩归属月":"_widget_1766375035265", + "是否同步衡石":"_widget_1766484337844", + "提交人": "creator", + "提交时间": "createTime", + "更新时间": "updateTime" + } + + # 定义需要特殊处理的列表字段及其内部字段映射 + self.list_fields_config = { + "新签阶段及提成比例": { + "_widget_1753778656361": "选择提成阶段", + "_widget_1753948745962": "新签阶段", + "_widget_1753778656362": "提成比例" + }, + # 可以在这里添加其他列表字段的配置 + # "另一个列表字段": { + # "原始字段名1": "映射后字段名1", + # "原始字段名2": "映射后字段名2" + # } + } + + def load_all_data(self): + # 获取非标业绩提报数据 + payload = {"api_key": "66b9678280b37f8a276b1d01", + "entry_id": "68886b7c0382a7249ae0b5d6", + } + dealer_service = api_instance.entry_data_list(payload) + self.dealer_service_data = dealer_service.get("data") # api请求格式,将数据封装在data字典里 + + def process_list_field(self, field_value, field_config): + """通用方法:处理列表类型的字段""" + if not isinstance(field_value, (list, np.ndarray)): + return field_value + + processed_list = [] + for item in field_value: + if not isinstance(item, dict): + processed_list.append(item) + continue + + processed_item = {} + for original_key, mapped_key in field_config.items(): + if original_key in item: + # 处理包含id的字典字段 + if isinstance(item[original_key], dict) and "id" in item[original_key]: + processed_item[mapped_key] = item[original_key]["id"] + else: + processed_item[mapped_key] = item[original_key] + else: + processed_item[mapped_key] = None + processed_list.append(processed_item) + return processed_list + + def data_process(self): + df = pd.DataFrame(self.dealer_service_data) + # 反转映射字典 + reverse_mapping = {v: k for k, v in self.field_mapping.items()} + # 1.列明替换 + df.columns = [reverse_mapping.get(col, col) for col in df.columns] + + # 只保留流程是否结束为是的内容 + df = df[df["流程是否结束"] == "是"] + + # 2.成员字段取值 + user_columns = ["报备业绩归属小六", "报备业绩归属区域经理", "原业绩归属人", "原业绩归属区域经理", "运营专家","业绩归属小六-区域提交"] + + for col in user_columns: + df[col] = df[col].map(lambda x: x.get("name", "") if isinstance(x, dict) else "") + + # 3.日期字段转为北京时间 + time_columns = ["支付日期", "开户/处理日期", "提交时间", "更新时间", "业绩归属月", "业绩归属日期"] + + for col in time_columns: + # 1. 解析为 datetime,并明确指定为 UTC(即使原始字符串无时区) + dt_utc = pd.to_datetime(df[col], errors='coerce', utc=True) + + # 2. 转换为北京时间 + dt_beijing = dt_utc.dt.tz_convert('Asia/Shanghai') + + # 3. 去掉时区信息(变成 naive datetime),然后格式化为字符串 + df[col] = dt_beijing.dt.tz_localize(None).dt.strftime('%Y-%m-%d %H:%M:%S') + + # 4.业绩动作等于拆单做复制 + + # 4.1. 定义条件 + mask = df['业绩动作'] == '拆单' + + # 4.2. 复制满足条件的行 + new_rows = df[mask].copy() # ⚠️ 一定要用 .copy() 避免 SettingWithCopyWarning + + # 3. 修改新行中的某些列 + new_rows['小六业绩金额'] = -new_rows['小六业绩金额'] + new_rows['区域业绩金额'] = -new_rows['区域业绩金额'] + new_rows['报备业绩归属小六'] = new_rows['原业绩归属人'] + new_rows['报备业绩归属区域经理'] = new_rows['原业绩归属区域经理'] + new_rows['报备业绩归属大区'] = new_rows['原业绩归属大区'] + + # 4. 合并回原 DataFrame + df = pd.concat([df, new_rows], ignore_index=True) + + # 5.处理所有配置的列表字段 + if "新签阶段及提成比例" in df.columns: + # 先处理订单登记表字段 + df["新签阶段及提成比例"] = df["新签阶段及提成比例"].apply( + lambda x: self.process_list_field(x, self.list_fields_config["新签阶段及提成比例"]) + if x is not None and (isinstance(x, (list, dict, np.ndarray)) or not pd.isna(x)) + else None + ) + + # 拆分行 + df_exploded = df.explode("新签阶段及提成比例") + + # 将订单登记表中的字段提取到主表中 + order_fields = self.list_fields_config["新签阶段及提成比例"].values() + for field in order_fields: + df_exploded[field] = df_exploded["新签阶段及提成比例"].apply( + lambda x: x.get(field) if isinstance(x, dict) else None + ) + + # 删除原始的订单登记表列 + df_exploded = df_exploded.drop(columns=["新签阶段及提成比例"]) + + # 重置索引 + df = df_exploded.reset_index(drop=True) + + return df + + def write_to_bi(self, df): + # 数据库连接信息 + HS_DB_Config = Config.HS_DB_Config + table_name = "non_standard_performance_to_BI" # 替换为你的实际表名 + + # 建立数据库连接 + connection = mysql.connector.connect( + host=HS_DB_Config["host"], + user=HS_DB_Config["user"], + password=HS_DB_Config["password"], + database=HS_DB_Config["database"] + ) + cursor = connection.cursor() + + try: + # 查询表列名 + cursor.execute(f"SHOW COLUMNS FROM {table_name}") + columns_info = cursor.fetchall() + db_columns = [col[0] for col in columns_info] # 提取列名 + df = df.replace([None, np.nan, pd.NA, 'nan', 'NaN', 'NAN', ''], None) + # 保留 DataFrame 中与数据库列名匹配的列 + filtered_df = df[df.columns.intersection(db_columns)] + + # 如果没有匹配的列,直接返回 + if filtered_df.empty: + print("DataFrame 中没有与数据库表结构匹配的列。") + return + + # 筛选列之后,插入前处理 dict 类型 + filtered_df = filtered_df.copy() + for col in filtered_df.columns: + if filtered_df[col].apply(lambda x: isinstance(x, (dict, list)) if x is not None else False).any(): + filtered_df.loc[:, col] = filtered_df[col].apply( + lambda x: json.dumps(x, ensure_ascii=False) if x is not None else x + ) + + # 构建插入语句 + placeholders = ', '.join(['%s'] * len(filtered_df.columns)) + # 使用反引号避免特殊列明 + columns = ', '.join([f"`{col}`" for col in filtered_df.columns]) + insert_sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})" + + # 将 DataFrame 写入数据库 + for _, row in filtered_df.iterrows(): + cursor.execute(insert_sql, tuple(row)) + + connection.commit() + logger.info(f"成功写入 {len(filtered_df)} 条记录到 {table_name} 表中。") + + except Exception as e: + error_task_logger.error(f"写入数据库时发生错误: {e}") + connection.rollback() + finally: + cursor.close() + connection.close() + + def clear_table_data(self): + """ + 清空指定 MySQL 表的数据。 + 参数已写死在函数内部,直接调用即可。 + """ + # 数据库连接信息 + HS_DB_Config = { + 'host': "f6-public.rwlb.rds.aliyuncs.com", + 'user': "rw_operation_data_relay", + 'password': "m+q5Z4%IVuF9bf", + 'database': "f6operation_data_relay" + } + table_name = "non_standard_performance_to_BI" # 要清空的表名 + + connection = None + try: + # 建立数据库连接 + connection = mysql.connector.connect( + host=HS_DB_Config["host"], + user=HS_DB_Config["user"], + password=HS_DB_Config["password"], + database=HS_DB_Config["database"] + ) + if connection.is_connected(): + cursor = connection.cursor() + + # 使用TRUNCATE清空表数据 + cursor.execute(f"TRUNCATE TABLE {table_name}") + connection.commit() + + logger.info(f"成功清空表 {table_name} 中的所有数据") + + except Error as e: + error_task_logger.error(f"清空表时发生错误: {e}") + if connection and connection.is_connected(): + connection.rollback() + finally: + if connection and connection.is_connected(): + cursor.close() + connection.close() + logger.info("数据库连接已关闭") + + def main(self): + task_start_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + try: + logger.info("任务开始") + # step1: 获取数据 + self.load_all_data() + logger.info("加载数据完成") + # step2:数据处理 + df = self.data_process() + # df.to_csv(os.path.join(output_dir, "new_dealer_service_order_to_bi.csv")) + logger.info("数据处理完成") + # step3:数据库删除 + self.clear_table_data() + logger.info("目标数据库已清空") + # step4:数据写入BI + self.write_to_bi(df) + logger.info("数据已写入数据库中") + common_module.send_task_status(task_start_time, "非标业绩提报转BI") + except Exception as e: + error_task_logger.error(f"非标业绩提报转BI发生错误{e}") + common_module.send_task_error(task_start_time,"非标业绩提报转BI", str(e)) + + +if __name__ == '__main__': + start = NonStandardPerformanceToBI() + start.main() diff --git a/tools/BI.ipynb b/tools/BI.ipynb index 7591e59..bd036c2 100644 --- a/tools/BI.ipynb +++ b/tools/BI.ipynb @@ -421,8 +421,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T06:03:19.996979900Z", - "start_time": "2026-03-05T06:03:19.335172700Z" + "end_time": "2026-04-03T09:13:22.881255Z", + "start_time": "2026-04-03T09:13:20.171270200Z" } }, "cell_type": "code", @@ -454,7 +454,7 @@ "\n", " # 使用DELETE删除ID大于等于127821的数据\n", " # cursor.execute(f\"DELETE FROM {table_name} WHERE id >= {min_id_to_delete}\")\n", - " cursor.execute(f\"DELETE FROM GP_annual_renewal_rate_new WHERE 月分区(仅用于存储每月最后一天截至数据) = '202602';\")\n", + " cursor.execute(f\"DELETE FROM GP_monthly_renewal_rate_new WHERE 月分区(仅用于存储每月最后一天截至数据) = '202603';\")\n", "\n", " connection.commit()\n", "\n", diff --git a/tools/月初3天删除分子分母表数据.py b/tools/月初3天删除分子分母表数据.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tools/月初3天删除分子分母表数据.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*-