简道云成员id与字段监控分离
This commit is contained in:
@@ -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()
|
||||
|
||||
+321
-123
@@ -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)
|
||||
|
||||
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user