'修复任务2单次同步记录过多触发429限流,会暂停1分钟'

This commit is contained in:
zelong 2026-02-28 17:16:30 +08:00
parent 2d7fa8ea7a
commit 0769cf4755
8 changed files with 670 additions and 63 deletions

View File

@ -9,8 +9,8 @@ workspace_id = 58335167
# 支持配置多个docid用逗号分隔例如 # 支持配置多个docid用逗号分隔例如
# docid = doc1,doc2,doc3 # docid = doc1,doc2,doc3
# 同步时会依次对所有表格进行同步 # 同步时会依次对所有表格进行同步
#docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ,dcHWzWyaHpZNQwUkZzgH5Kfyx9cMvQzVjZIapajGDuXqjS4nEe0LQqOojBL8s3rlwghw4deOgVnbOqHLoxcKzaHg docid = dcOsT3czWy0YEDg38vlDqwVCTjv0kzwC_GU2XmT9wSZctQ0ZJQUAV7vMQ3ljZx-n_NqxzEEYG2DiLAvNdNsHJwgQ,dcHWzWyaHpZNQwUkZzgH5Kfyx9cMvQzVjZIapajGDuXqjS4nEe0LQqOojBL8s3rlwghw4deOgVnbOqHLoxcKzaHg
docid =dc2Q5Kb0T4zerbo4_ag0MMcXHCusIaFJX5fO6_8n-l_yV-bn5brZSi1kNw3kjme-qIs0LvPKbC5GDEEPaZ1BGlvA #docid =dc2Q5Kb0T4zerbo4_ag0MMcXHCusIaFJX5fO6_8n-l_yV-bn5brZSi1kNw3kjme-qIs0LvPKbC5GDEEPaZ1BGlvA
[Schedule] [Schedule]
# 同步频率(分钟) # 同步频率(分钟)
@ -20,4 +20,5 @@ sync_interval = 30
# 企业微信应用ID # 企业微信应用ID
agentid = 1000615 agentid = 1000615
# 接收人列表用户ID多个用|分隔,@all表示全部成员 # 接收人列表用户ID多个用|分隔,@all表示全部成员
receivers = 046364 # receivers = 046364
receivers = 040005

View File

@ -1,4 +1,4 @@
{ {
"access_token": "2LMUjFN9nR32QOSk-RYWTfpdYRL4CLtdgk10JWam9o4mvbfTnsLt8a0ODt-S_eu7MFJ4CFnC6fjLMbAUmfOufPXZrwa2sndigq7xoTWnUqeGqsu_2YcRW3VwilGMJuMG1_6_SYJgNwizMoS8BapKpGW1b37i1ITlQERhRR-iCyO-4DOezbQzb_07y_G1XNM1T3uyo09BNvfIIRx1yGAbmyv_63koAjW4LG3x_AbZnPE", "access_token": "GQKX61Nh9c5A4Mp0FDOGy4nZgnFok2gefTA0_X4Y5PEL7NkpD9UHWwji3lWkZLsHMJf3dpbJ_l-NdichZ5qSZuPhF7kJNU47Blf2yLQRqFctmXMU6m1cWU80iLiY0vrX2EPzvldaHMR-al3HgKK6PUSU9T2a5Xp-lCjh9StPzEnQJUnwickV4PiPegLLGcH5F6jcM-9pHztkJ6pSV6bfP5QFFATAldmj-71Occib9V0",
"fetch_time": 1770177536.5388677 "fetch_time": 1772269143.6670134
} }

14
core/__init__.py Normal file
View File

@ -0,0 +1,14 @@
"""全局核心模块导出"""
from core.global_log_system import (
GlobalLogSystem,
create_task1_log_system,
create_task2_log_system,
)
__all__ = [
"GlobalLogSystem",
"create_task1_log_system",
"create_task2_log_system",
]

210
core/global_log_system.py Normal file
View File

@ -0,0 +1,210 @@
"""
全局日志系统内核
职责
1. 按天写入 jsonl 日志
2. 统一记录 API 调用事件
3. 记录同步开始/结束事件与统计
4. token 等敏感字段做脱敏
"""
from __future__ import annotations
import json
import re
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
class GlobalLogSystem:
"""全局日志记录器"""
ALLOWED_MODULES = {"smartsheet", "tapd", "wework"}
SENSITIVE_KEYS = {
"access_token",
"token",
"authorization",
"corpsecret",
"api_password",
"password",
"secret",
"auth",
}
TOKEN_PATTERNS = [
re.compile(r"(access_token=)([^&\s]+)", re.IGNORECASE),
re.compile(r"(corpsecret=)([^&\s]+)", re.IGNORECASE),
re.compile(r"(token=)([^&\s]+)", re.IGNORECASE),
]
def __init__(self, task_name: str, log_dir: str | Path):
self.task_name = task_name
self.log_dir = Path(log_dir)
self.log_dir.mkdir(parents=True, exist_ok=True)
self._active_sync_id: Optional[str] = None
def start_sync(self,
trigger: str,
metadata: Optional[Dict[str, Any]] = None,
sync_id: Optional[str] = None) -> str:
"""记录一次同步开始事件并返回 sync_id"""
if sync_id is None:
sync_id = self._generate_sync_id()
event = {
"event_type": "start_sync",
"timestamp": self._now_string(),
"task": self.task_name,
"sync_id": sync_id,
"trigger": trigger,
"metadata": self._sanitize(metadata or {}),
}
self._active_sync_id = sync_id
self._write_event_safely(event)
return sync_id
def log_api(self,
module: str,
operation: str,
request_data: Optional[Dict[str, Any]],
response_data: Optional[Dict[str, Any]],
success: bool,
error_message: Optional[str] = None,
duration_ms: Optional[int] = None,
sync_id: Optional[str] = None,
extra: Optional[Dict[str, Any]] = None) -> None:
"""记录单次 API 调用事件"""
if module not in self.ALLOWED_MODULES:
raise ValueError(
f"module 不合法: {module},允许值: {sorted(self.ALLOWED_MODULES)}"
)
resolved_sync_id = sync_id or self._active_sync_id
event = {
"event_type": "api_call",
"timestamp": self._now_string(),
"task": self.task_name,
"sync_id": resolved_sync_id,
"module": module,
"operation": operation,
"success": success,
"request": self._sanitize(request_data or {}),
"response": self._sanitize(response_data or {}),
"error_message": self._sanitize_text(error_message),
"duration_ms": duration_ms,
"extra": self._sanitize(extra or {}),
}
self._write_event_safely(event)
def end_sync_with_stats(self,
stats: Dict[str, Any],
success: bool,
error_message: Optional[str] = None,
sync_id: Optional[str] = None,
extra: Optional[Dict[str, Any]] = None) -> None:
"""记录一次同步结束事件"""
resolved_sync_id = sync_id or self._active_sync_id
event = {
"event_type": "end_sync",
"timestamp": self._now_string(),
"task": self.task_name,
"sync_id": resolved_sync_id,
"success": success,
"stats": self._sanitize(stats),
"error_message": self._sanitize_text(error_message),
"extra": self._sanitize(extra or {}),
}
self._write_event_safely(event)
if resolved_sync_id == self._active_sync_id:
self._active_sync_id = None
def _write_event_safely(self, event: Dict[str, Any]) -> None:
"""安全写入:日志失败不影响主流程"""
try:
self._write_event(event)
except Exception as exc:
print(f"⚠️ 全局日志写入失败: {exc}")
def _write_event(self, event: Dict[str, Any]) -> None:
log_file = self._get_today_log_file()
with open(log_file, "a", encoding="utf-8") as handle:
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
def _get_today_log_file(self) -> Path:
today = datetime.now().strftime("%Y-%m-%d")
return self.log_dir / f"api_log_{today}.jsonl"
def _generate_sync_id(self) -> str:
time_part = datetime.now().strftime("%Y%m%d_%H%M%S")
random_part = uuid.uuid4().hex[:8]
return f"{self.task_name}_{time_part}_{random_part}"
def _now_string(self) -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def _sanitize(self, value: Any, parent_key: str = "") -> Any:
if isinstance(value, dict):
sanitized: Dict[str, Any] = {}
for key, item in value.items():
key_lower = str(key).lower()
if key_lower in self.SENSITIVE_KEYS:
sanitized[key] = self._mask_sensitive_value(item)
else:
sanitized[key] = self._sanitize(item, parent_key=key_lower)
return sanitized
if isinstance(value, list):
return [self._sanitize(item, parent_key=parent_key) for item in value]
if isinstance(value, tuple):
return tuple(self._sanitize(item, parent_key=parent_key) for item in value)
if isinstance(value, str):
if parent_key in self.SENSITIVE_KEYS:
return self._mask_sensitive_value(value)
return self._sanitize_text(value)
return value
def _sanitize_text(self, text: Optional[str]) -> Optional[str]:
if text is None:
return None
masked = text
for pattern in self.TOKEN_PATTERNS:
masked = pattern.sub(r"\1***", masked)
return masked
def _mask_sensitive_value(self, value: Any) -> str:
text = "" if value is None else str(value)
if not text:
return "***"
if text.lower().startswith("bearer "):
return "Bearer ***"
if len(text) <= 6:
return "***"
return f"{text[:3]}***{text[-2:]}"
def create_task1_log_system(project_root: Optional[Path] = None) -> GlobalLogSystem:
"""创建任务一日志系统(固定 logs/"""
root = project_root or Path(__file__).parent.parent
return GlobalLogSystem(task_name="task1", log_dir=root / "logs")
def create_task2_log_system(project_root: Optional[Path] = None) -> GlobalLogSystem:
"""创建任务二日志系统(固定 logs2/"""
root = project_root or Path(__file__).parent.parent
return GlobalLogSystem(task_name="task2", log_dir=root / "logs2")

111
docs/全局框架文档.md Normal file
View File

@ -0,0 +1,111 @@
# 全局框架文档
> 用途:统一维护项目模块、脚本职责与接口定义,避免调用不存在接口与跨任务耦合失控。
> 范围:`src/``src2/`,以及新增的全局模块(如后续 `core/`)。
---
## 1. 日志系统总览(重构目标态)
### 1.1 目标架构
- **全局日志内核**:位于任务目录外,提供统一记录能力。
- **任务一适配层**:写入 `logs/`
- **任务二适配层**:写入 `logs2/`
- **同步边界管理**:每次同步使用唯一 `sync_id` 分隔,结束时写统计。
### 1.2 统一日志字段(约定)
- `event_type``start_sync` / `api_call` / `end_sync`
- `timestamp`:事件时间
- `task``task1` / `task2`
- `sync_id`:单次同步唯一标识
- `module``smartsheet` / `tapd` / `wework`
- `operation`:接口操作名
- `request`:请求快照(含脱敏)
- `response`:响应快照(含脱敏)
- `success`:调用是否成功
- `error_message`:失败原因
- `duration_ms`:调用耗时(毫秒)
- `stats`:同步完成统计(仅 `end_sync`
---
## 2. 模块清单与职责(当前状态)
## 2.0 全局核心(`core/`
- `core/global_log_system.py`
- **职责**提供统一日志内核jsonl 写入、同步分隔、API 事件、统计事件、脱敏)。
- **接口(对内)**`start_sync``log_api``end_sync_with_stats`
- **创建器**`create_task1_log_system`(固定 `logs/`)、`create_task2_log_system`(固定 `logs2/`)。
- `core/__init__.py`
- **职责**:导出全局日志内核与创建器。
## 2.1 任务一(`src/`
- `src/scheduler.py`
- **职责**:任务一调度入口,定时触发单次同步。
- **关键依赖**`src/main.py`
- `src/main.py`
- **职责**:任务一主流程编排(扫描、校验、开单、回写、通知)。
- **接口(对内)**`run_once(...)`
- `src/smartsheet.py`
- **职责**:智能表格 API 封装。
- **接口(对内)**`get_sheet_list``get_fields``get_records``update_records` 等。
- `src/tapd_api.py`
- **职责**TAPD Bug API 封装(创建、查询、附件上传)。
- **接口(对内)**`create_bug``get_bug``upload_attachment` 等。
- `src/token_manager.py`
- **职责**:企业微信 `access_token` 获取与缓存。
- **接口(对内)**`get_token()`
- `src/wework_notifier.py`
- **职责**:企业微信消息通知。
- **接口(对内)**`send_validation_failure_notification(...)`
- `src/api_logger.py`
- **职责**:现有日志记录器(后续将作为兼容层)。
## 2.2 任务二(`src2/`
- `src2/scheduler.py`
- **职责**:任务二调度入口,定时触发同步。
- **关键依赖**`src2/sync_service.py`
- `src2/sync_service.py`
- **职责**:任务二同步编排(读取、解析链接、查询 TAPD、回写、通知
- **接口(对内)**`sync_once()`
- `src2/smartsheet.py`
- **职责**:任务二智能表格 API 适配层。
- **关键点**:应固定写入 `logs2/`
- `src2/smartsheet_sync.py`
- **职责**:任务二表格字段检查、记录提取与回写构造。
- `src2/tapd_api.py`
- **职责**:任务二 TAPD Story 查询与状态映射。
- `src2/notifier.py`
- **职责**:任务二失败通知封装(当前复用任务一通知器,存在串目录风险)。
- `src2/logger.py`
- **职责**:任务二日志实例入口(后续接入统一内核)。
---
## 3. 现存问题与待改造点
- **串目录问题**:任务二复用 `TokenManager``WeWorkNotifier` 可能写入 `logs/`
- **双记录矛盾**:同一次请求在部分分支可能出现先成功后失败两条记录。
- **写入稳定性问题**:现有按 JSON 数组拼接的策略会造成结构损坏风险。
- **同步边界缺失**:缺少标准化 `start_sync` / `end_sync` 分隔与统计记录。
---
## 4. 模块接口演进计划(摘要)
- 阶段1新增全局日志内核模块定义统一接口。已完成
- 阶段2`src/api_logger.py` 改造成兼容层,保证旧调用可用。
- 阶段3`src2/logger.py` 与任务二编排层切换到统一内核,修复串目录。
- 阶段4更新查看工具与文档支持 `jsonl + sync_id`
---
## 5. 变更记录
### 2026-02-28
- 新建本框架文档。
- 写入日志系统重构目标态、模块职责清单、问题清单与演进路线。
### 2026-02-28更新
- 新增 `core/global_log_system.py``core/__init__.py` 模块说明。
- 标记阶段1完成统一日志内核已落地待阶段2/3接线。

105
docs/全局迭代日志.md Normal file
View File

@ -0,0 +1,105 @@
# 全局迭代日志
> 用途:跨任务(`src` / `src2`)记录每个阶段的增改内容、验收结果与回滚点。
> 约束:每次阶段验收通过后,必须追加一条日志记录。
---
## 记录模板
### 阶段:
- **阶段名称**
- **日期**
- **目标**
### 变更清单
- **新增文件**
- **修改文件**
- **删除文件**
### 关键改动说明
- **日志结构变更**
- **接口/调用链变更**
- **兼容性说明**
### 验收结果
- **通过项**
- **未通过项**
- **遗留风险**
### 回滚与追踪
- **可回滚点**
- **关联文档**
- **备注**
---
## 阶段日志
## 阶段0文档与约定先行
- **阶段名称**:日志系统重构 - 阶段0
- **日期**2026-02-28
- **目标**:建立重构方案与全局文档骨架,为后续代码改造提供统一约束。
### 变更清单
- **新增文件**
- `docs/日志系统重构实施方案.md`
- `docs/全局迭代日志.md`
- `docs/全局框架文档.md`
- **修改文件**:无
- **删除文件**:无
### 关键改动说明
- **日志结构变更**:确定后续采用 `jsonl`,并以 `sync_id` 分隔每次同步。
- **接口/调用链变更**:明确生产链路以 `src/scheduler.py``src2/scheduler.py` 为准。
- **兼容性说明**阶段0仅文档不影响现网行为。
### 验收结果
- **通过项**
- 分阶段路线、边界条件、验收清单已落文档。
- 已建立全局迭代日志与框架文档容器。
- **未通过项**:无
- **遗留风险**
- 任务二存在复用模块导致串目录风险待阶段3修复
- 现有日志写入策略存在双记录与结构损坏风险待阶段1/2修复
### 回滚与追踪
- **可回滚点**:当前为纯文档提交,可直接整提交回滚。
- **关联文档**`docs/日志系统重构实施方案.md`
- **备注**阶段1开始前需再次确认日志字段最终版。
## 阶段1实现全局日志内核
- **阶段名称**:日志系统重构 - 阶段1
- **日期**2026-02-28
- **负责人**Codex
- **目标**:在任务目录外提供可复用的统一日志内核,支持 jsonl、sync_id、token 脱敏。
### 变更清单
- **新增文件**
- `core/global_log_system.py`
- `core/__init__.py`
- **修改文件**
- `docs/全局迭代日志.md`
- `docs/全局框架文档.md`
- **删除文件**:无
### 关键改动说明
- **日志结构变更**:新增 `start_sync` / `api_call` / `end_sync` 三类事件模型。
- **接口/调用链变更**:提供 `start_sync``log_api``end_sync_with_stats` 三个核心接口。
- **兼容性说明**阶段1仅新增内核未接入任务一/任务二业务调用,不影响现网逻辑。
### 验收结果
- **通过项**
- 已支持按天写入 `api_log_YYYY-MM-DD.jsonl`
- 已支持 `sync_id` 生命周期记录。
- 已支持 token/secret 脱敏。
- 已提供任务一、任务二创建器(固定目录)。
- **未通过项**
- 尚未接入 `src` / `src2`,串目录与双记录矛盾仍待后续阶段修复。
- **遗留风险**
- 阶段2/3接线时若沿用旧 logger 分支逻辑,可能再次引入双记录。
### 回滚与追踪
- **可回滚点**`core/` 新增为独立改动,可单独回滚。
- **关联文档**`docs/日志系统重构实施方案.md`
- **备注**:下一阶段优先完成任务一接入并验证“单次调用单条记录”。

View File

@ -0,0 +1,119 @@
# 日志系统重构实施方案(任务一 + 任务二)
## 1. 目标与边界
### 1.1 重构目标
- 在任务一和任务二之外,提供统一的全局日志记录系统。
- 覆盖生产链路:`src/scheduler.py``src2/scheduler.py` 触发的同步流程。
- 在每一个发起 API 调用的地方记录请求与结果(成功/失败都记录)。
- 任务一日志落在 `logs/`,任务二日志落在 `logs2/`
- 每条日志必须包含 `module` 字段,允许值:`smartsheet` / `tapd` / `wework`
- 通过 `sync_id` 分隔每次同步,并在同步完成后写入当次统计信息。
### 1.2 明确要解决的问题
- 解决“串目录”问题:任务二不得写入 `logs/`,任务一不得写入 `logs2/`
- 解决“双记录矛盾”问题:同一次 API 调用只能有一条最终记录(不能先 success 再 failure
- 日志格式改为 `jsonl`(每行一条完整 JSON 记录)。
- 对 token 做脱敏,不做响应内容截断。
### 1.3 非目标
- 本次不改业务流程语义(只改日志系统与接线方式)。
- 本次不对历史 JSON 文件做离线迁移,仅保证新写入生效。
---
## 2. 总体设计
### 2.1 新增全局日志内核(任务外)
- 新增独立模块(建议路径:`core/global_log_system.py`)。
- 提供统一接口:
- `start_sync(...)`:开始一次同步,生成 `sync_id`
- `log_api(...)`:记录单次 API 调用结果(成功/失败统一出口)。
- `end_sync_with_stats(...)`:写入同步统计并结束。
- 通过构造参数确定任务上下文:`task_name``log_dir`、默认 `module`
### 2.2 日志存储格式
- 文件命名:`api_log_YYYY-MM-DD.jsonl`
- 存储目录:任务一 `logs/`,任务二 `logs2/`
- 记录模型:
- 通用字段:`event_type``timestamp``task``sync_id``module`
- API 事件字段:`operation``request``response``success``error_message``duration_ms`
- 同步边界字段:`start_sync` / `end_sync` 事件及统计 `stats`
### 2.3 脱敏规则
- `request/response` 中出现 token 字段统一脱敏(如 `access_token``Authorization``corpsecret``api_password`)。
- 脱敏策略:保留前后少量字符,中间替换为 `***`
---
## 3. 分阶段实施路线(小步走)
## 阶段0文档与约定先行
### 工作内容
- 新建并维护两份全局文档:
- `docs/全局迭代日志.md`
- `docs/全局框架文档.md`
- 在文档中建立日志重构专属章节、字段定义、阶段验收标准。
### 验收标准
- 文档结构可用于后续持续更新。
- 明确字段与职责边界(尤其是 `sync_id``task``module`)。
## 阶段1实现全局日志内核
### 工作内容
- 新增 `core/global_log_system.py`,实现 `jsonl` 写入、脱敏、同步分隔和统计写入。
- 新增兼容层,避免一次性改动过大。
### 验收标准
- 能独立写入 `logs/` / `logs2/`
- `start_sync -> 多条 log_api -> end_sync_with_stats` 链路完整。
## 阶段2接入任务一src
### 工作内容
- 改造 `src/api_logger.py` 为新内核兼容封装。
- 在任务一同步主流程增加 `sync_id` 生命周期。
- 覆盖所有 API 调用记录点,统一单次调用单条记录。
### 验收标准
- 任务一生产链路日志全部位于 `logs/`
- 无“双记录矛盾”。
## 阶段3接入任务二src2
### 工作内容
- 改造 `src2/logger.py` 接入同一内核并固定 `logs2/`
- 在任务二同步主流程增加 `sync_id` 生命周期。
- 修复复用模块导致的串目录问题(含 token 获取、wework 通知链路)。
### 验收标准
- 任务二生产链路日志全部位于 `logs2/`
- 与任务一日志完全隔离。
## 阶段4查看工具与收尾
### 工作内容
- 更新 `src/log_viewer.py` 支持读取 `jsonl`
- 更新 `logs/README.md`(新增 jsonl 规范与排障说明)。
### 验收标准
- 可按日期与 `sync_id` 追踪一次完整同步。
- 文档、代码、日志格式三者一致。
---
## 4. 核心风险与规避
- 风险1兼容层改造影响原调用。
- 规避:先保留 `log_api_call(...)` 旧接口,再逐步替换调用。
- 风险2任务二通过复用模块写错目录。
- 规避:统一使用可注入 logger/context禁止隐式全局默认目录。
- 风险3单次请求出现多状态记录。
- 规避:采用“单出口记录”,业务错误也只写一次最终状态。
---
## 5. 验收清单(最终)
- [ ] 生产链路(两个 scheduler所有 API 调用均有日志。
- [ ] 每次同步都有 `start_sync``end_sync`,并可通过 `sync_id` 聚合。
- [ ] 任务一只写 `logs/`,任务二只写 `logs2/`
- [ ] 无同一次 API 调用 success/failure 双写冲突。
- [ ] token 已脱敏。
- [ ] `jsonl` 可被查看工具读取。

View File

@ -9,6 +9,7 @@ TAPD API调用模块任务二专用
import os import os
import requests import requests
import time
from typing import Dict, Optional, Any from typing import Dict, Optional, Any
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
@ -105,7 +106,7 @@ class TAPDStoryApi:
def _make_request(self, endpoint: str, params: Optional[Dict] = None) -> Dict: def _make_request(self, endpoint: str, params: Optional[Dict] = None) -> Dict:
""" """
发起TAPD API GET请求的通用方法 发起TAPD API GET请求的通用方法支持429错误重试
Args: Args:
endpoint: API端点 "stories" endpoint: API端点 "stories"
@ -138,6 +139,11 @@ class TAPDStoryApi:
for key, value in params.items(): for key, value in params.items():
print(f" {key}: {value}") print(f" {key}: {value}")
# 429错误重试逻辑最多重试1次
max_retries = 1
retry_count = 0
while retry_count <= max_retries:
try: try:
response = self.session.get( response = self.session.get(
url, url,
@ -158,6 +164,42 @@ class TAPDStoryApi:
print(f"响应内容: {response.text[:500]}") print(f"响应内容: {response.text[:500]}")
print("=" * 60) print("=" * 60)
# 检查是否是429错误在raise_for_status之前检查
if response.status_code == 429:
if retry_count < max_retries:
retry_count += 1
wait_seconds = 60
print(f"\n⚠️ 触发TAPD API限流 (429 Too Many Requests)")
print(f" [开始等待] 等待 {wait_seconds} 秒后重试... (第 {retry_count}/{max_retries} 次重试)")
print(f" [重要] 在等待期间,代码会阻塞在这里,不会继续处理其他记录")
# 记录429错误日志
self.logger.log_api_call(
api_type="tapd",
operation=endpoint,
request_data=log_request_data,
response_data={"status_code": 429, "retry_count": retry_count},
success=False,
error_message=f"429 Too Many Requests, 等待{wait_seconds}秒后重试"
)
time.sleep(wait_seconds)
print(f" [等待结束] 开始重试请求...")
continue # 重试
else:
# 已达到最大重试次数,抛出异常
error_msg = "TAPD API限流 (429 Too Many Requests),重试后仍然失败"
self.logger.log_api_call(
api_type="tapd",
operation=endpoint,
request_data=log_request_data,
response_data={"status_code": 429, "retry_count": retry_count},
success=False,
error_message=error_msg
)
raise RuntimeError(error_msg)
# 对于非429错误调用raise_for_status检查HTTP状态
response.raise_for_status() response.raise_for_status()
result = response.json() result = response.json()
@ -198,6 +240,8 @@ class TAPDStoryApi:
raise RuntimeError(error_msg) raise RuntimeError(error_msg)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
# 如果是HTTPError且状态码是429由上面的status_code检查处理
# 这里不应该到达因为429在response.status_code检查时已处理
error_msg = f"TAPD API请求失败: {e}" error_msg = f"TAPD API请求失败: {e}"
self.logger.log_api_call( self.logger.log_api_call(
api_type="tapd", api_type="tapd",
@ -209,6 +253,9 @@ class TAPDStoryApi:
) )
raise RuntimeError(error_msg) raise RuntimeError(error_msg)
# 理论上不会到达这里
raise RuntimeError("TAPD API请求失败未知错误")
def get_story(self, story_id: str) -> Dict: def get_story(self, story_id: str) -> Dict:
""" """
获取需求详情 获取需求详情