From 2840d4871ab64f8cb568938ad7e0b7157a97b598 Mon Sep 17 00:00:00 2001 From: z66 <1415243231@qq.com> Date: Thu, 28 Aug 2025 11:03:46 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=80=E9=81=93=E4=BA=91=E6=88=90=E5=91=98id?= =?UTF-8?q?=E4=B8=8E=E5=AD=97=E6=AE=B5=E7=9B=91=E6=8E=A7=E5=88=86=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back_ground_module/common_module.py | 1 + back_ground_module/data_monitor.py | 444 ++++++++++++++++++-------- back_ground_module/update_ID_form.py | 445 ++++++++++++++++++++++++--- test/BI.ipynb | 26 +- test/logs/task.log | 2 + test/获取外部成员id.ipynb | 202 ++++++++++++ 6 files changed, 951 insertions(+), 169 deletions(-) create mode 100644 test/获取外部成员id.ipynb diff --git a/back_ground_module/common_module.py b/back_ground_module/common_module.py index 84b793b..84d7bbe 100644 --- a/back_ground_module/common_module.py +++ b/back_ground_module/common_module.py @@ -7,6 +7,7 @@ import pymysql from api import API from log_config import configure_task_logger, configure_error_task_logger + api_instance = API() # 获取已经配置好的常规日志记录器 logger = configure_task_logger() diff --git a/back_ground_module/data_monitor.py b/back_ground_module/data_monitor.py index b6ade4b..960350b 100644 --- a/back_ground_module/data_monitor.py +++ b/back_ground_module/data_monitor.py @@ -1,21 +1,40 @@ """字段监控(多平台适配版)""" import sys -import platform -from datetime import datetime, timedelta, timezone from pathlib import Path import pandas as pd import zipfile -import json +from datetime import datetime, timezone, timedelta, date import requests +from typing import Optional, List, Dict, Any +from decimal import Decimal import time -from api import API -from log_config import configure_task_logger, configure_error_task_logger -from back_ground_module import CommonModule +import numpy as np +import json + + +def replace_decimals(obj): + """替换Decimal类型为float""" + if isinstance(obj, dict): + return {k: replace_decimals(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [replace_decimals(item) for item in obj] + elif isinstance(obj, Decimal): + return float(obj) + return obj + + +class NpEncoder(json.JSONEncoder): + """NumPy类型JSON编码器""" + 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) -# 初始化日志记录器 -logger = configure_task_logger() -error_task_logger = configure_error_task_logger() -common_tools = CommonModule() # ---------------------------- 配置项(多平台适配)--------------------------- class Config: @@ -27,12 +46,21 @@ class Config: DATA_DIR = OUTPUT_DIR / "data_snapshots" ARCHIVE_DIR = OUTPUT_DIR / "archives" + # API配置 + JIANDAOYUN_API_TOKEN = "Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN" + # 运行参数 RETAIN_DAYS = 7 COMPRESS_FORMAT = "zip" MAX_RETRIES = 3 RETRY_DELAY = 0.5 # 秒 + # 监控表单配置 + MONITOR_APP_ID = "6694d3c4fcb69ca9a111a6c4" + MONITOR_ENTRY_ID = "6850c044f17c934b3ec01fea" + CHANGES_ENTRY_ID = "6863a402a77925690a470cc5" + STATUS_ENTRY_ID = "67ede908eb9c22261016466e" + @classmethod def get_log_file(cls): """获取跨平台兼容的日志文件路径""" @@ -43,6 +71,7 @@ class Config: """获取跨平台兼容的变更记录文件路径""" return cls.OUTPUT_DIR / "changes_summary.csv" + # ---------------------- 工具函数(多平台兼容)----------------------- class Utils: @staticmethod @@ -51,10 +80,9 @@ class Utils: path = Path(path) if not isinstance(path, Path) else path try: path.mkdir(parents=True, exist_ok=True) - logger.debug(f"Directory ensured: {path}") return True except Exception as e: - error_task_logger.error(f"Failed to create directory {path}: {str(e)}") + print(f"创建目录失败: {e}") return False @staticmethod @@ -83,19 +111,19 @@ class Utils: temp_file.rename(file_path) return True except Exception as e: - error_task_logger.error(f"Failed to write {file_path}: {str(e)}") + print(f"CSV写入失败: {e}") if temp_file.exists(): temp_file.unlink() return False + # ---------------------- API客户端(多平台兼容)----------------------- class APIClient: def __init__(self): self.headers = { - 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN', + 'Authorization': Config.JIANDAOYUN_API_TOKEN, 'Content-Type': 'application/json' } - self.api = API() def request(self, url, payload, method='POST'): """带重试机制的API请求""" @@ -113,7 +141,106 @@ class APIClient: if retry == Config.MAX_RETRIES: raise time.sleep(Config.RETRY_DELAY) - logger.warning(f"Request failed (attempt {retry + 1}/{Config.MAX_RETRIES}): {str(e)}") + + def data_batch_create(self, data: dict, max_retries: int = 20) -> Optional[Dict]: + """新建单条表单数据""" + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/create' + + payload = json.dumps({ + "app_id": data['api_key'], + "entry_id": data['entry_id'], + "data": data['data'], + "is_start_workflow": data.get('is_start_workflow', "false"), + "is_start_trigger": data.get('is_start_trigger', "false"), + "transaction_id": data.get('transaction_id', "") + }) + + for retry in range(max_retries + 1): + try: + response = requests.post(url=url, data=payload, headers=self.headers, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + if retry == max_retries: + print(f"创建数据失败: {e}") + return None + time.sleep(3) + + def entry_data_list(self, data: dict, max_retries: int = 20) -> Dict: + """获取多条表单数据""" + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list' + all_data_batches = [] + last_data_id = None + + while True: + payload = json.dumps({ + "app_id": data['api_key'], + "entry_id": data['entry_id'], + "limit": 100, + "data_id": last_data_id + }) + + for retry in range(max_retries + 1): + try: + response = requests.post(url=url, data=payload, headers=self.headers, timeout=10) + response.raise_for_status() + data_get = response.json() + + if data_get.get("data"): + all_data_batches.extend(data_get['data']) + last_data_id = data_get['data'][-1].get('_id') + break + else: + return {"data": all_data_batches} + + except requests.exceptions.RequestException as e: + if retry == max_retries: + print(f"获取数据列表失败: {e}") + return {"data": all_data_batches} + time.sleep(0.1) + + if not data_get.get("data") or len(data_get['data']) < 100: + break + + return {"data": all_data_batches} + + def entry_data_batch_create(self, data: dict, chunk_size: int = 90, max_retries: int = 20) -> List[Optional[Dict]]: + """新建多条数据""" + data = replace_decimals(data) + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_create' + data_get_list = [] + + total_length = len(data['data_list']) + num_chunks = (total_length + chunk_size - 1) // chunk_size + + for i in range(num_chunks): + start_index = i * chunk_size + end_index = min(start_index + chunk_size, total_length) + + payload = json.dumps({ + "app_id": data['api_key'], + "entry_id": data['entry_id'], + "data_list": data['data_list'][start_index:end_index], + "is_start_workflow": data.get('is_start_workflow', "false"), + "is_start_trigger": data.get('is_start_trigger', "false"), + }, cls=NpEncoder) + + for retry in range(max_retries + 1): + try: + response = requests.post(url=url, data=payload, headers=self.headers, timeout=10) + response.raise_for_status() + data_get = response.json() + if data_get.get("status") == "success": + data_get_list.append(data_get) + break + except requests.exceptions.RequestException as e: + if retry == max_retries: + print(f"批量创建数据失败: {e}") + data_get_list.append(None) + time.sleep(0.1) + + return data_get_list + # ---------------------- 数据处理基类 ----------------------- class DataHandler: @@ -141,7 +268,7 @@ class DataHandler: last_widget = pd.read_csv(self.last_widget_file) if self.last_widget_file.exists() else None return last_data, last_widget except Exception as e: - error_task_logger.error(f"Failed to load last data: {str(e)}") + print(f"加载上次数据失败: {e}") return None, None def save_last_data(self, data, widget_data): @@ -151,9 +278,16 @@ class DataHandler: success &= Utils.safe_csv_write(widget_data, self.last_widget_file) return success except Exception as e: - error_task_logger.error(f"Failed to save current data: {str(e)}") + print(f"保存数据失败: {e}") return False + def field_replacement(self, data, final_data): + """字段替换方法(由id替换为标签名)""" + # 这里实现具体的字段替换逻辑 + # 由于具体替换规则未知,返回原始数据 + return final_data + + # ---------------------- 数据监控主类 ----------------------- class DataMonitor(DataHandler): def __init__(self): @@ -164,8 +298,12 @@ class DataMonitor(DataHandler): """获取所有应用列表""" url = "https://api.jiandaoyun.com/api/v5/app/list" payload = json.dumps({"skip": 0, "limit": 100}) - response = self.api.request(url, payload) - return pd.DataFrame(response.json().get("apps", [])) + try: + response = self.api.request(url, payload) + return pd.DataFrame(response.json().get("apps", [])) + except Exception as e: + print(f"获取应用列表失败: {e}") + return pd.DataFrame() def fetch_entries(self, app_df): """获取所有表单条目""" @@ -174,15 +312,19 @@ class DataMonitor(DataHandler): for _, app in app_df.iterrows(): payload = json.dumps({"app_id": app['app_id']}) - response = self.api.request(url, payload) - entries = response.json().get("forms", []) + try: + response = self.api.request(url, payload) + entries = response.json().get("forms", []) - if entries: - entry_df = pd.DataFrame(entries) - entry_df['app_id'] = app['app_id'] - all_entries.append(entry_df) + if entries: + entry_df = pd.DataFrame(entries) + entry_df['app_id'] = app['app_id'] + all_entries.append(entry_df) + except Exception as e: + print(f"获取表单条目失败: {e}") + continue - return pd.concat(all_entries, ignore_index=True) if all_entries else None + return pd.concat(all_entries, ignore_index=True) if all_entries else pd.DataFrame() def fetch_widgets(self, entry_df): """获取所有字段组件""" @@ -194,37 +336,50 @@ class DataMonitor(DataHandler): "app_id": entry['app_id'], "entry_id": entry['entry_id'] }) - response = self.api.request(url, payload) - widgets = response.json().get('widgets', []) + try: + response = self.api.request(url, payload) + widgets = response.json().get('widgets', []) - if widgets: - widget_df = pd.DataFrame(widgets) - widget_df['app_id'] = entry['app_id'] - widget_df['entry_id'] = entry['entry_id'] - all_widgets.append(widget_df) + if widgets: + widget_df = pd.DataFrame(widgets) + widget_df['app_id'] = entry['app_id'] + widget_df['entry_id'] = entry['entry_id'] + all_widgets.append(widget_df) + except Exception as e: + print(f"获取字段组件失败: {e}") + continue - return pd.concat(all_widgets, ignore_index=True) if all_widgets else None + return pd.concat(all_widgets, ignore_index=True) if all_widgets else pd.DataFrame() def fetch_monitor_data(self): """获取监控数据""" payload = { - "api_key": "6694d3c4fcb69ca9a111a6c4", - "entry_id": "6850c044f17c934b3ec01fea" + "api_key": Config.MONITOR_APP_ID, + "entry_id": Config.MONITOR_ENTRY_ID } - data = self.api.api.entry_data_list(payload).get("data") - data_list = pd.DataFrame(data) + try: + data = self.api.entry_data_list(payload).get("data", []) + data_list = pd.DataFrame(data) - # 处理复杂数据类型 - for col in data_list.columns: - if data_list[col].apply(lambda x: isinstance(x, (dict, list))).any(): - data_list[col] = data_list[col].astype(str) + # 处理复杂数据类型 + for col in data_list.columns: + if data_list[col].apply(lambda x: isinstance(x, (dict, list))).any(): + data_list[col] = data_list[col].astype(str) - return data_list.drop_duplicates() + return data_list.drop_duplicates() + except Exception as e: + print(f"获取监控数据失败: {e}") + return pd.DataFrame() def match_widgets(self, data_list, widget_list): """匹配数据列表和组件列表""" if '_widget_1750122565203' not in data_list.columns: - raise ValueError("Missing required column '_widget_1750122565203'") + print("缺少必需的列 '_widget_1750122565203'") + return pd.DataFrame() + + if widget_list.empty: + return pd.DataFrame() + return widget_list[widget_list['entry_id'].isin(data_list['_widget_1750122565203'])] def archive_old_data(self): @@ -234,12 +389,11 @@ class DataMonitor(DataHandler): for i in range(Config.RETAIN_DAYS) ] - # 查找需要归档的文件 files_to_archive = [ f for f in self.data_dir.iterdir() if f.is_file() and - (f.name.startswith("snapshot_") or f.name.startswith("all_widgets_")) and - f.suffix == '.csv' + (f.name.startswith("snapshot_") or f.name.startswith("all_widgets_")) and + f.suffix == '.csv' ] for file_path in files_to_archive: @@ -253,80 +407,101 @@ class DataMonitor(DataHandler): with zipfile.ZipFile(archive_path, 'a', zipfile.ZIP_DEFLATED) as zipf: zipf.write(file_path, arcname=file_path.name) file_path.unlink() - logger.debug(f"Archived {file_path.name} to {archive_path}") except Exception as e: - error_task_logger.error(f"Failed to archive {file_path}: {str(e)}") + print(f"归档文件失败: {e}") def compare_data(self, current_data): """比较新旧数据差异""" - if not self.last_data_file.exists(): + if not self.last_data_file.exists() or current_data.empty: return None - last_data = pd.read_csv(self.last_data_file) - last_data['unique_id'] = last_data['name'].astype(str) + last_data['app_id'].astype(str) - current_data['unique_id'] = current_data['name'].astype(str) + current_data['app_id'].astype(str) + try: + last_data = pd.read_csv(self.last_data_file) + if last_data.empty: + return None - merged = pd.merge( - last_data, current_data, - on=['unique_id'], - how='outer', - suffixes=('_last', '_current'), - indicator=True - ) + last_data['unique_id'] = last_data['name'].astype(str) + last_data['app_id'].astype(str) + current_data['unique_id'] = current_data['name'].astype(str) + current_data['app_id'].astype(str) - changes = { - 'added': merged[merged['_merge'] == 'right_only'], - 'deleted': merged[merged['_merge'] == 'left_only'], - 'modified': pd.DataFrame() - } + merged = pd.merge( + last_data, current_data, + on=['unique_id'], + how='outer', + suffixes=('_last', '_current'), + indicator=True + ) - for col in ['label', 'type']: - last_col = f"{col}_last" - current_col = f"{col}_current" + changes = { + 'added': merged[merged['_merge'] == 'right_only'], + 'deleted': merged[merged['_merge'] == 'left_only'], + 'modified': pd.DataFrame() + } - if last_col in merged.columns and current_col in merged.columns: - mask = (merged['_merge'] == 'both') & (merged[last_col] != merged[current_col]) - mask = mask & ~merged[last_col].isna() & ~merged[current_col].isna() + for col in ['label', 'type']: + last_col = f"{col}_last" + current_col = f"{col}_current" - if mask.any(): - modified = merged.loc[mask].copy() - modified['changed_field'] = col - modified['old_value'] = modified[last_col] - modified['new_value'] = modified[current_col] - modified['change_status'] = 'update' - changes['modified'] = pd.concat([changes['modified'], modified]) + if last_col in merged.columns and current_col in merged.columns: + mask = (merged['_merge'] == 'both') & (merged[last_col] != merged[current_col]) + mask = mask & ~merged[last_col].isna() & ~merged[current_col].isna() - return changes + if mask.any(): + modified = merged.loc[mask].copy() + modified['changed_field'] = col + modified['old_value'] = modified[last_col] + modified['new_value'] = modified[current_col] + modified['change_status'] = 'update' + changes['modified'] = pd.concat([changes['modified'], modified]) + + return changes + except Exception as e: + print(f"比较数据失败: {e}") + return None def save_changes(self, changes, apps, entries): """保存变更记录""" + if not changes or all(len(v) == 0 for v in changes.values()): + return False + result_rows = [] for change_type in ['added', 'deleted', 'modified']: + df = changes[change_type] + if df.empty: + continue + suffix = 'current' if change_type in ['added', 'modified'] else 'last' - for _, row in changes[change_type].iterrows(): - app_id = row[f'app_id_{suffix}'] - entry_id = row[f'entry_id_{suffix}'] + for _, row in df.iterrows(): + app_id = row.get(f'app_id_{suffix}') + entry_id = row.get(f'entry_id_{suffix}') - app_name = apps.loc[apps['app_id'] == app_id, 'name'].values[0] if not apps[ - apps['app_id'] == app_id].empty else 'Unknown App' - entry_name = entries.loc[ - (entries['app_id'] == app_id) & (entries['entry_id'] == entry_id), 'name' - ].values[0] if not entries[ - (entries['app_id'] == app_id) & (entries['entry_id'] == entry_id) - ].empty else 'Unknown Entry' + if not app_id or not entry_id: + continue + + app_name = 'Unknown App' + entry_name = 'Unknown Entry' + + if not apps.empty: + app_match = apps[apps['app_id'] == app_id] + if not app_match.empty: + app_name = app_match['name'].values[0] + + if not entries.empty: + entry_match = entries[(entries['app_id'] == app_id) & (entries['entry_id'] == entry_id)] + if not entry_match.empty: + entry_name = entry_match['name'].values[0] if change_type == 'added': - content = f"Added field: {row['label_current']}" + content = f"Added field: {row.get('label_current', 'Unknown')}" elif change_type == 'deleted': - content = f"Deleted field: {row['label_last']}" + content = f"Deleted field: {row.get('label_last', 'Unknown')}" else: - content = f"Changed from \"{row['old_value']}\" to \"{row['new_value']}\"" + content = f"Changed from \"{row.get('old_value', 'Unknown')}\" to \"{row.get('new_value', 'Unknown')}\"" result_rows.append({ 'timestamp': self.execution_time, - 'unique_id': row['unique_id'], + 'unique_id': row.get('unique_id', ''), 'app_id': app_id, 'app_name': app_name, 'entry_id': entry_id, @@ -339,7 +514,6 @@ class DataMonitor(DataHandler): result_df = pd.DataFrame(result_rows) changes_file = Config.get_changes_file() try: - # 追加模式写入,保留历史记录 result_df.to_csv( changes_file, mode='a', @@ -350,7 +524,7 @@ class DataMonitor(DataHandler): self.add_to_jiandaoyun(result_df) return True except Exception as e: - error_task_logger.error(f"Failed to save changes: {str(e)}") + print(f"保存变更记录失败: {e}") return False return False @@ -365,24 +539,51 @@ class DataMonitor(DataHandler): } for _, row in result_df.iterrows()] payload = { - "api_key": "6694d3c4fcb69ca9a111a6c4", - "entry_id": "6863a402a77925690a470cc5", + "api_key": Config.MONITOR_APP_ID, + "entry_id": Config.CHANGES_ENTRY_ID, "data_list": all_data } - response = self.api.api.entry_data_batch_create(payload) - - if isinstance(response, list): - logger.info(f"Successfully wrote {len(response)} records to Jiandaoyun") - return True - else: - error_task_logger.error(f"Failed to write to Jiandaoyun: {response.get('message', 'Unknown error')}") + try: + response = self.api.entry_data_batch_create(payload) + return isinstance(response, list) and len(response) > 0 + except Exception as e: + print(f"写入简道云失败: {e}") return False + def send_task_status(self, task_start_time: str, task_name: str) -> None: + """将任务状态发送到简道云""" + try: + end_time_utc = datetime.now(timezone.utc) + task_start_naive = datetime.strptime(task_start_time, "%Y-%m-%d %H:%M:%S") + task_start_utc = task_start_naive - timedelta(hours=8) + task_start_utc = task_start_utc.replace(tzinfo=timezone.utc) + + run_time = end_time_utc - task_start_utc + run_time_sec = int(run_time.total_seconds()) + + today_utc = end_time_utc.strftime("%Y-%m-%d") + task_end_iso = end_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ") + task_start_iso = task_start_utc.strftime("%Y-%m-%dT%H:%M:%SZ") + + payload = { + "api_key": Config.MONITOR_APP_ID, + "entry_id": Config.STATUS_ENTRY_ID, + "data": { + "_widget_1744873387500": {"value": today_utc}, + "_widget_1743644977694": {"value": task_name}, + "_widget_1744873387501": {"value": task_start_iso}, + "_widget_1744873387502": {"value": task_end_iso}, + "_widget_1744873387504": {"value": run_time_sec}, + } + } + + self.api.data_batch_create(payload) + except Exception as e: + print(f"发送任务状态失败: {e}") + def run_daily_snapshot(self): """执行每日快照任务""" - logger.info("=== Starting daily snapshot task ===") - try: apps = self.fetch_apps() entries = self.fetch_entries(apps) @@ -390,28 +591,30 @@ class DataMonitor(DataHandler): monitor_data = self.fetch_monitor_data() matched_data = self.match_widgets(monitor_data, widgets) - # 保存数据 + if matched_data.empty: + print("没有匹配到数据") + return False + today_file = self.data_dir / f"snapshot_{self.today}.csv" widget_file = self.data_dir / f"all_widgets_{self.today}.csv" if not Utils.safe_csv_write(matched_data, today_file): - raise Exception("Failed to save snapshot data") + print("保存快照数据失败") + return False if not Utils.safe_csv_write(widgets, widget_file): - raise Exception("Failed to save widget data") + print("保存组件数据失败") + return False self.archive_old_data() self.save_last_data(matched_data, widgets) - logger.info("=== Daily snapshot task completed successfully ===") return True except Exception as e: - error_task_logger.error(f"Daily snapshot task failed: {str(e)}") + print(f"每日快照任务失败: {e}") return False def run_hourly_check(self): """执行每小时检查任务""" - logger.info("=== Starting hourly check task ===") - try: apps = self.fetch_apps() entries = self.fetch_entries(apps) @@ -425,32 +628,27 @@ class DataMonitor(DataHandler): self.save_last_data(current_data, widgets) - logger.info("=== Hourly check task completed successfully ===") return True except Exception as e: - error_task_logger.error(f"Hourly check task failed: {str(e)}") + print(f"每小时检查任务失败: {e}") return False def main(self): """主运行逻辑""" task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: - logger.info(f"=== Starting data monitoring task ({self.execution_time}) ===") - if Utils.is_first_run_today(): success = self.run_daily_snapshot() else: success = self.run_hourly_check() - common_tools.send_task_status(task_start_time, "字段监控") - - logger.info("=== Data monitoring task completed ===") + self.send_task_status(task_start_time, "字段监控") return success except Exception as e: - error_task_logger.error(f"Data monitoring task failed: {e}") - common_tools.send_task_error(task_start_time, "字段监控", str(e)) + print(f"主运行逻辑失败: {e}") return False + if __name__ == "__main__": # 确保输出目录存在 Utils.ensure_dir(Config.OUTPUT_DIR) diff --git a/back_ground_module/update_ID_form.py b/back_ground_module/update_ID_form.py index 4eb1019..227df24 100644 --- a/back_ground_module/update_ID_form.py +++ b/back_ground_module/update_ID_form.py @@ -1,22 +1,36 @@ +from typing import Optional, List, Dict, Any + import requests import json import pandas as pd -from api import API -from config import Config -from log_config import configure_task_logger, configure_error_task_logger -from back_ground_module import CommonModule -from datetime import datetime +from datetime import datetime, timezone, timedelta, date, UTC +import time +from decimal import Decimal +import numpy as np +import requests +import json -# 初始化API实例 -api_instance = API() -# 获取已经配置好的常规日志记录器 -logger = configure_task_logger() +def replace_decimals(obj): + if isinstance(obj, dict): + return {k: replace_decimals(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [replace_decimals(item) for item in obj] + elif isinstance(obj, Decimal): + return float(obj) # 或者 str(obj) + return obj -# 获取已经配置好的错误任务日志记录器 -error_task_logger = configure_error_task_logger() -common_module = CommonModule() +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 update_ID_form: @@ -24,7 +38,7 @@ class update_ID_form: def __init__(self): self.headers = { - 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 app_key + 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN', # 曹伟应用api测试 app_key 'Content-Type': 'application/json' } self.url = "https://api.jiandaoyun.com/api/v5/corp/department/user/list" @@ -52,27 +66,367 @@ class update_ID_form: search_department_member = response.json() departments_members = search_department_member.get('users') df1 = pd.DataFrame(departments_members) - logger.info("部门成员及ID表已成功获取") return df1 except Exception as e: - error_task_logger.error(f"获取部门成员及ID表失败:{e}") - task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - common_module.send_task_error(task_start_time, "简道云员工ID表更新", str(e)) return None + @staticmethod + def data_batch_create(data: dict, max_retries: int = 20) -> Optional[requests.Response]: # 新建单条数据 + """ + 新建单条表单数据 + :param max_retries: 最大重试次数 + :param data: 应该包含应用id、表单id,以及新建的数据data['data'] + :return: 返回创建后简道云返回的信息 + """ + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/create' + + headers = { + 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN', # 曹伟应用api测试 app_key + 'Content-Type': 'application/json' + } + + """ + data 样式 # 后续优化发送数据样式 目前输入字段,后续优化输入表单名称 + jiandaoyun_data['data'] = {"_widget_1731650067055":{"value":f'{username}{password}'}, + "_widget_1731650067056":{"value": f"{group}"}} + """ + # noinspection DuplicatedCode + payload = json.dumps({ + "app_id": data['api_key'], # 应用ID + "entry_id": data['entry_id'], # 表单ID + "data": data['data'], + "is_start_workflow": data.get('is_start_workflow', "false"), + "is_start_trigger": data.get('is_start_trigger', "false"), + "transaction_id": data.get('transaction_id', "") + } + ) + + retries = 0 + while retries <= max_retries: + try: + res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) + res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 + data_get = res.json() + if res.status_code == 200: + return data_get + else: + retries += 1 + time.sleep(3) # 在重试之间稍作停顿 + except requests.exceptions.RequestException as e: + retries += 1 + time.sleep(3) # 在重试之间稍作停顿 + if retries > max_retries: + return None + + @staticmethod + def entry_widget_list(data: dict) -> Optional[Dict[str, Any]]: # 获取表单字段 + """ + 获取表单字段 + :param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息 + :return: + """ + + url = 'https://api.jiandaoyun.com/api/v5/app/entry/widget/list' + + headers = { + 'Authorization': "Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN", # 曹伟应用api测试 app_key + 'Content-Type': 'application/json' + } + + payload = json.dumps({ + "app_id": data['api_key'], + "entry_id": data['entry_id'], + }) + + res = requests.post(url=url, data=payload, headers=headers, timeout=10) + return res.json() + + @staticmethod + def field_replacement(data: dict, data_get: dict) -> dict: + """ + 字段替换,将id替换为标签名,即唯一值替换为表单中显示字段的名字 + :param data: 简道云插件发送过来的data,包含表单id、数据id、应用id + :param data_get: 简道云请求的数据,一般是根据数据id获取到表单的数据 + :return: 将根据数据id获取到的表单数据,进行替换,返回替换后的数据 + """ + + # 获取表单对应字段标签名称 + widget_list = update_ID_form.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)) # 深拷贝 + + # 替换 data 字段下的所有键 + if 'data' in data_get_copy: + data_get_copy['data'] = replace_keys(data_get_copy['data']) + + return data_get_copy + @staticmethod + def entry_data_list(data: dict, replace: bool = False, max_retries: int = 20) -> Dict: # 获取多条表单数据 + """ + 获取多条表单数据 + :param max_retries: 最大重试次数 + :param replace: 是否替换字段 + :param data: + api_key: 应用id + entry_id: 表单id + :return: + """ + + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list' + + headers = { + 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN', # 曹伟应用api测试 app_key + 'Content-Type': 'application/json' + } + all_data_batches = [] # 用于存储每次请求返回的数据批次 + last_data_id = None + exit_flag = False + while True: + payload = json.dumps({ + "app_id": data['api_key'], # 应用ID + "entry_id": data['entry_id'], # 表单ID + "limit": 100, + "data_id": last_data_id + }) + 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') + break # 成功则跳出循环 + else: + if 'data' not in data_get or len(data_get['data']) == 0: + exit_flag = True + break + retries += 1 + time.sleep(0.1) # 在重试之间稍作停顿 + except requests.exceptions.RequestException as e: + retries += 1 + time.sleep(0.1) # 在重试之间稍作停顿 + if retries > max_retries: + all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理 + + if exit_flag: + break + + # 构建最终返回的字典 + final_data = { + 'data': all_data_batches # 'data' 键对应的值是列表的列表 + } + if replace: + print("进行了替换") + return_data = update_ID_form.field_replacement(data, final_data) # 字段替换,由id替换为标签名 + + return return_data + else: + return final_data + + def send_task_status(self, task_start_time: str, task_name: str) -> None: + """ + 将任务状态发送到简道云(开始时间为北京时间,需转换到 UTC) + :param task_start_time: 任务开始时间(字符串格式:"%Y-%m-%d %H:%M:%S",表示北京时间 UTC+8) + :param task_name: 任务名称 + """ + try: + # 1. 获取当前 UTC 时间(时区感知对象) + end_time_utc = datetime.now(UTC) # ✅ 替代 utcnow() + + # 2. 解析传入的北京时间(UTC+8) + task_start_naive = datetime.strptime(task_start_time, "%Y-%m-%d %H:%M:%S") + + # 3. 转换为 UTC 时间(减去 8 小时,并附加 UTC 时区) + task_start_utc = task_start_naive - timedelta(hours=8) + task_start_utc = task_start_utc.replace(tzinfo=timezone.utc) # 显式标记为 UTC + + # 4. 计算运行时间(时区感知对象可直接相减) + run_time = end_time_utc - task_start_utc + run_time_sec = int(run_time.total_seconds()) + + # 5. 格式化时间为 UTC 的 ISO 8601 格式(带 "Z") + today_utc = end_time_utc.strftime("%Y-%m-%d") + task_end_iso = end_time_utc.strftime("%Y-%m-%dT%H:%M:%SZ") + task_start_iso = task_start_utc.strftime("%Y-%m-%dT%H:%M:%SZ") + + # 6. 构造请求数据(所有时间以 UTC 格式发送) + payload = { + "api_key": "6694d3c4fcb69ca9a111a6c4", + "entry_id": "67ede908eb9c22261016466e", + "data": { + "_widget_1744873387500": {"value": today_utc}, # UTC 日期 + "_widget_1743644977694": {"value": task_name}, + "_widget_1744873387501": {"value": task_start_iso}, # UTC 开始时间 + "_widget_1744873387502": {"value": task_end_iso}, # UTC 结束时间 + "_widget_1744873387504": {"value": run_time_sec}, + } + } + + # 7. 发送请求 + response = update_ID_form.data_batch_create(payload) + except Exception as e: + pass + + @staticmethod + def entry_data_batch_create( + data: dict, + chunk_size: int = 90, + max_retries: int = 20 + ) -> List[Optional[requests.Response]]: # 新建多条数据 注意简道云限制1次最多100条数据 + """ + 新建多条数据 + :param max_retries: 最大重试次数,此处设置20次 + :param data:应包含数据id、表单id、以及需要新建的信息,新建信息应该是一个列表 + :param chunk_size: 简道云限制批量新建一次最多100条,这里默认值设置为90条一次 + :return:返回请求后的结果 + """ + data = replace_decimals(data) + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_create' + + headers = { + 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN', # 曹伟应用api测试 appKey + 'Content-Type': 'application/json' + } + + """ + data_list 样式 # 后续优化发送数据样式 目前输入字段,后续优化输入表单名称 + jiandaoyun_data_list['data_list'] = [{"_widget_1731650067055":{"value":f'{username}{password}'}, + "_widget_1731650067056":{"value": f"{group}"}}, + {"_widget_1731650067055":{"value":f'{username}{password}'}, + "_widget_1731650067056":{"value": f"{group}"}}] + """ + + # 获取data_list长度 + total_length = len(data['data_list']) + + # 计算需要发送的次数 + num_chunks = (total_length + chunk_size - 1) // chunk_size # //整除向下取证,需要加上chunk_size - 1保证不会有缺失数据 + data_get_list = [] + for i in range(num_chunks): + start_index = i * chunk_size + end_index = min(start_index + chunk_size, total_length) + payload = json.dumps({ + "app_id": data['api_key'], # 应用ID + "entry_id": data['entry_id'], # 表单ID + "data_list": data['data_list'][start_index:end_index], + "is_start_workflow": data.get('is_start_workflow', "false"), + "is_start_trigger": data.get('is_start_trigger', "false"), + }, cls=NpEncoder) + retries = 0 + while retries <= max_retries: + try: + res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) + res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 + data_get = res.json() + if data_get["status"] == "success": + data_get_list.append(data_get) + break # 成功则跳出循环 + else: + + retries += 1 + time.sleep(3) # 在重试之间稍作停顿 + except requests.exceptions.RequestException as e: + + retries += 1 + time.sleep(0.1) # 在重试之间稍作停顿 + if retries > max_retries: + data_get_list.append(None) # 或者可以选择记录失败的payload以便后续处理 + + return data_get_list + def get_existing_id_form(self): """读取现有的ID表""" try: - now_ID_form = api_instance.entry_data_list(self.payload1).get('data') + now_ID_form = update_ID_form.entry_data_list(self.payload1).get('data') df = pd.DataFrame(now_ID_form) - logger.info("现有的ID表已成功读取") return df except Exception as e: - error_task_logger.error(f"读取现有的ID表失败:{e}") - task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - common_module.send_task_error(task_start_time, "简道云员工ID表更新", str(e)) return None + @staticmethod + def entry_data_batch_delete( + data: dict, + chunk_size: int = 90, + max_retries: int = 20 + ) -> List[Optional[requests.Response]]: # 新建多条数据 注意简道云限制1次最多100条数据 + """ + 批量删除数据 + :param data: 应包含应用ID、表单ID、数据ID列表 + :param chunk_size:单词删除最大条数,默认90 + :param max_retries:重试次数,默认20 + :return: + """ + + data = replace_decimals(data) + url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_delete' + + headers = { + 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN', # 曹伟应用api测试 appKey + 'Content-Type': 'application/json' + } + + # 获取data_list长度 + total_length = len(data['data_ids']) + + # 计算需要发送的次数 + num_chunks = (total_length + chunk_size - 1) // chunk_size # //整除向下取证,需要加上chunk_size - 1保证不会有缺失数据 + data_get_list = [] + + for i in range(num_chunks): + start_index = i * chunk_size + end_index = min(start_index + chunk_size, total_length) + payload = json.dumps({ + "app_id": data['api_key'], # 应用ID + "entry_id": data['entry_id'], # 表单ID + "data_ids": data['data_ids'][start_index:end_index], + }, cls=NpEncoder) + + retries = 0 + while retries <= max_retries: + try: + res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) + res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 + data_get = res.json() + + if data_get["status"] == "success": + data_get_list.append(data_get) + break # 成功则跳出循环 + else: + + retries += 1 + time.sleep(3) # 在重试之间稍作停顿 + except requests.exceptions.RequestException as e: + + retries += 1 + time.sleep(0.1) # 在重试之间稍作停顿 + if retries > max_retries: + data_get_list.append(None) # 或者可以选择记录失败的payload以便后续处理 + + return data_get_list + def delete_existing_data(self, df): """批量删除现有数据""" try: @@ -80,12 +434,9 @@ class update_ID_form: for index, i in df.iterrows(): all_data.append(i["_id"]) self.delete_payload["data_ids"] = all_data - api_instance.entry_data_batch_delete(self.delete_payload) - logger.info("现有数据已成功删除") + res = update_ID_form.entry_data_batch_delete(self.delete_payload) except Exception as e: - error_task_logger.error(f"批量删除现有数据失败:{e}") - task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - common_module.send_task_error(task_start_time, "简道云员工ID表更新", str(e)) + pass def update_data(self, df1): """批量写入新数据""" @@ -97,29 +448,49 @@ class update_ID_form: "_widget_1734942794145": {"value": i["username"]}, }) self.update_payload["data_list"] = all_data1 - api_instance.entry_data_batch_create(self.update_payload) - logger.info("新数据已成功写入") + update_ID_form.entry_data_batch_create(self.update_payload) except Exception as e: - error_task_logger.error(f"批量写入新数据失败:{e}") - task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - common_module.send_task_error(task_start_time, "简道云员工ID表更新", str(e)) + pass + + def get_out_department_memberships(self): + """获取外部公司成员及ID表""" + try: + + url = "https://api.jiandaoyun.com/api/v5/corp/guest/user/list" + + headers = { + 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN', + 'Content-Type': 'application/json' + } + + response = requests.post(url, headers=headers) + all_data = [] + member_list = response.json().get("member_list", []) + for member in member_list: + name = member.get("name") + username = member.get("username") # 用户id + all_data.append({"name": name, "username": username}) + df2 = pd.DataFrame(all_data) + return df2 + except: + print("获取外部公司成员及ID表失败") def main(self): """主函数""" task_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: - logger.info("每日任务开始执行") df1 = self.get_department_members() + df2 = self.get_out_department_memberships() if df1 is not None: df = self.get_existing_id_form() if df is not None: self.delete_existing_data(df) self.update_data(df1) - logger.info("每日任务执行完成") - common_module.send_task_status(task_start_time, "简道云员工ID表更新") + self.update_data(df2) + + self.send_task_status(task_start_time, "简道云员工ID表更新") except Exception as e: - error_task_logger.error(f"简道云员工ID表更新任务执行失败:{e}") - common_module.send_task_error(task_start_time, "简道云员工ID表更新", str(e)) + print(str(e)) if __name__ == '__main__': diff --git a/test/BI.ipynb b/test/BI.ipynb index 3fd214a..316e5ee 100644 --- a/test/BI.ipynb +++ b/test/BI.ipynb @@ -122,6 +122,7 @@ "} # 衡时数据库链接配置-mysql\n", "table_name = \"thailand_store_data_email\" # 请替换为实际的表名\n", "# table_name = \"yida_process_time_statistics\"\n", + "\n", "# 连接\n", "connection = mysql.connector.connect(\n", " host=HS_DB_Config[\"host\"],\n", @@ -130,12 +131,12 @@ " database=HS_DB_Config[\"database\"]\n", ")\n", "\n", - "print(f\"成功连接 {HS_DB_Config[\"database\"]}\")\n", + "print(f\"成功连接 {HS_DB_Config['database']}\")\n", "cursor = connection.cursor()\n", "\n", "# 读取Excel文件\n", "df = pd.read_excel(\n", - " r\"C:\\Users\\Administrator.DESKTOP-7IC2USJ\\Downloads\\门店日使用数据Workshop’s_Daily_Usage_Data_20250805101517.xlsx\",\n", + " r\"C:\\Users\\Administrator.DESKTOP-7IC2USJ\\Downloads\\门店日使用数据Workshop's_Daily_Usage_Data_20250805101517.xlsx\",\n", " sheet_name=\"Sheet1\")\n", "\n", "# 处理空值 - 将NaN/NaT/空字符串统一转为None\n", @@ -146,16 +147,23 @@ "placeholders = ', '.join(['%s'] * len(df.columns))\n", "insert_query = f\"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})\"\n", "\n", - "# 批量插入数据\n", + "# 批量插入数据,每次1000条\n", "records = [tuple(row) for row in df.values]\n", - "cursor.executemany(insert_query, records)\n", - "connection.commit()\n", + "batch_size = 1000\n", + "total_records = len(records)\n", + "inserted_count = 0\n", "\n", - "print(f\"成功导入 {cursor.rowcount} 条记录到 {table_name} 表\")\n", + "for i in range(0, total_records, batch_size):\n", + " batch = records[i:i+batch_size]\n", + " cursor.executemany(insert_query, batch)\n", + " connection.commit()\n", + " inserted_count += len(batch)\n", + " print(f\"已成功导入 {inserted_count}/{total_records} 条记录\")\n", + "\n", + "print(f\"总共成功导入 {inserted_count} 条记录到 {table_name} 表\")\n", "\n", "cursor.close()\n", - "\n", - "connection.close()\n" + "connection.close()" ], "id": "a98f8dd324b53eeb", "outputs": [ @@ -407,7 +415,7 @@ { "metadata": {}, "cell_type": "markdown", - "source": "## 删除区间数据", + "source": "### 删除区间数据", "id": "8192d432b3f65bc2" }, { diff --git a/test/logs/task.log b/test/logs/task.log index 2ae3442..7936690 100644 --- a/test/logs/task.log +++ b/test/logs/task.log @@ -22,3 +22,5 @@ 2025-08-20 16:48:57,997 - common_module.py - task_logger - INFO - 任务状态发送成功: {'data': {'creator': {'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}, 'updater': {'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}, 'deleter': None, 'createTime': '2025-08-20T08:48:55.784Z', 'updateTime': '2025-08-20T08:48:55.784Z', 'deleteTime': None, '_widget_1744873387500': '2025-08-20T00:00:00.000Z', '_widget_1743644977694': '非标业绩提报转BI', '_widget_1744873387501': '2025-08-20T08:48:57.000Z', '_widget_1744873387502': '2025-08-20T08:48:57.000Z', '_widget_1744873387504': '0', '_id': '68a58bf734f6e13aec32ca2a', 'appId': '6694d3c4fcb69ca9a111a6c4', 'entryId': '67ede908eb9c22261016466e'}} 2025-08-20 16:58:28,211 - api.py - task_logger - INFO - 已获取 8 条数据 2025-08-20 16:58:29,045 - common_module.py - task_logger - INFO - 任务状态发送成功: {'data': {'creator': {'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}, 'updater': {'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}, 'deleter': None, 'createTime': '2025-08-20T08:58:26.821Z', 'updateTime': '2025-08-20T08:58:26.821Z', 'deleteTime': None, '_widget_1744873387500': '2025-08-20T00:00:00.000Z', '_widget_1743644977694': '非标业绩提报转BI', '_widget_1744873387501': '2025-08-20T08:58:28.000Z', '_widget_1744873387502': '2025-08-20T08:58:28.000Z', '_widget_1744873387504': '0', '_id': '68a58e326435007d9a859fa2', 'appId': '6694d3c4fcb69ca9a111a6c4', 'entryId': '67ede908eb9c22261016466e'}} +2025-08-27 11:11:22,356 - api.py - task_logger - INFO - 已获取 1 条数据 +2025-08-27 11:11:49,228 - api.py - task_logger - INFO - 已获取 1 条数据 diff --git a/test/获取外部成员id.ipynb b/test/获取外部成员id.ipynb new file mode 100644 index 0000000..eaec02d --- /dev/null +++ b/test/获取外部成员id.ipynb @@ -0,0 +1,202 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 外部成员结构", + "id": "a58995e7e8657dce" + }, + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-08-27T03:11:49.414114Z", + "start_time": "2025-08-27T03:11:49.016794Z" + } + }, + "source": [ + "from api import API\n", + "\n", + "api_instance = API()\n", + "\n", + "payload = {\n", + " \"api_key\": \"6694d3c4fcb69ca9a111a6c4\",\n", + " \"entry_id\": \"68ae76ddedae9bffae06a911\",\n", + "}\n", + "df = api_instance.entry_data_list(payload, replace=True)\n", + "print(df)" + ], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001B[92m2025-08-27 11:11:49,228 - api.py - task_logger - INFO - 已获取 1 条数据\u001B[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "进行了替换\n", + "{'data': [{'creator': {'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}, 'updater': {'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}, 'deleter': None, 'createTime': '2025-08-27T03:09:49.522Z', 'updateTime': '2025-08-27T03:09:49.522Z', 'deleteTime': None, '成员单选': {'name': '申晨', 'username': 'R-688c8ba43678deccfcb5c386-jdy-jv71cd380jlj', 'status': 1, 'type': 2, 'departments': [-979913651]}, '成员多选': [{'name': '申晨', 'username': 'R-688c8ba43678deccfcb5c386-jdy-jv71cd380jlj', 'status': 1, 'type': 2, 'departments': [-979913651]}, {'name': '张阳', 'username': '4210192048793363', 'status': 1, 'type': 0, 'departments': [449008196], 'integrate_id': '4210192048793363'}], '_id': '68ae76fd74ad62855e55e195', 'appId': '6694d3c4fcb69ca9a111a6c4', 'entryId': '68ae76ddedae9bffae06a911'}]}\n" + ] + } + ], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 向表格内写入外部成员", + "id": "e311761d3eff6179" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-27T03:13:40.574272Z", + "start_time": "2025-08-27T03:13:40.428525Z" + } + }, + "cell_type": "code", + "source": [ + "from api import API\n", + "\n", + "api_instance = API()\n", + "payload = {\n", + " \"api_key\": \"6694d3c4fcb69ca9a111a6c4\",\n", + " \"entry_id\": \"68ae76ddedae9bffae06a911\",\n", + " \"data\":{\"_widget_1756264157512\":{\"value\":\"R-688c8ba43678deccfcb5c386-jdy-jv71cd380jlj\"}}\n", + "}\n", + "dict = api_instance.data_batch_create(payload)\n", + "\n", + "print(dict)" + ], + "id": "80256c669800af2f", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'data': {'creator': {'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}, 'updater': {'name': 'F6汽车科技', 'username': '#admin', 'status': 1, 'type': 0}, 'deleter': None, 'createTime': '2025-08-27T03:13:40.349Z', 'updateTime': '2025-08-27T03:13:40.349Z', 'deleteTime': None, '_widget_1756264157512': {'name': '申晨', 'username': 'R-688c8ba43678deccfcb5c386-jdy-jv71cd380jlj', 'status': 1, 'type': 2, 'departments': [-979913651]}, '_widget_1756264157513': [], '_id': '68ae77e44b356b9bc83e338d', 'appId': '6694d3c4fcb69ca9a111a6c4', 'entryId': '68ae76ddedae9bffae06a911'}}\n" + ] + } + ], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 获取关联企业", + "id": "714a210b956fe262" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-27T03:16:34.842344Z", + "start_time": "2025-08-27T03:16:34.456948Z" + } + }, + "cell_type": "code", + "source": [ + "import requests\n", + "import json\n", + "\n", + "url = \"https://api.jiandaoyun.com/api/v5/corp/guest/department/list\"\n", + "\n", + "headers = {\n", + " 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN',\n", + " 'Content-Type': 'application/json'\n", + "}\n", + "response = requests.post(url, headers=headers)\n", + "print(response.json())\n", + "dept_list = response.json().get(\"dept_list\",[])\n", + "for dept in dept_list:\n", + " print(dept.get(\"name\"))" + ], + "id": "d14ea7a6ab8b00dc", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'dept_list': [{'dept_no': -12, 'name': 'CASTROL LIMITED', 'type': 2, 'status': 1}, {'dept_no': -979913650, 'name': '曹伟手机号注册所处公司', 'type': 2, 'status': 1}, {'dept_no': -979913651, 'name': '申晨', 'type': 2, 'status': 1}]}\n" + ] + } + ], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 获取关联企业对接人", + "id": "28355f7a1f5b7a3a" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-27T03:19:09.922520Z", + "start_time": "2025-08-27T03:19:09.839699Z" + } + }, + "cell_type": "code", + "source": [ + "import requests\n", + "import json\n", + "url = \"https://api.jiandaoyun.com/api/v5/corp/guest/user/list\"\n", + "\n", + "\n", + "headers = {\n", + " 'Authorization': 'Bearer qygHulymo1fekJk4CIZyNKjyQAzG8CFN',\n", + " 'Content-Type': 'application/json'\n", + "}\n", + "\n", + "response = requests.post(url, headers=headers)\n", + "all_data = []\n", + "member_list = response.json().get(\"member_list\",[])\n", + "for member in member_list:\n", + " name = member.get(\"name\")\n", + " username = member.get(\"username\") # 用户id\n", + " all_data.append({\"name\":name,\"username\":username})\n" + ], + "id": "11fd29cc47185320", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sylvia7203@gmail.com\n", + "金鹏测试\n", + "曹伟手机号注册\n", + "葡萄\n", + "申晨\n" + ] + } + ], + "execution_count": 7 + } + ], + "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 +}