'phase4
This commit is contained in:
parent
6e51a505bd
commit
9e959f58e0
1162
TAPD接口文档.md
1162
TAPD接口文档.md
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
||||
|
||||
[TAPD]
|
||||
# TAPD项目ID
|
||||
workspace_id = 58335167
|
||||
workspace_id = 5833516 7
|
||||
# Bug报告人
|
||||
reporter = G41小助手
|
||||
|
||||
|
||||
4
config/token_cache.json
Normal file
4
config/token_cache.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"access_token": "NWdoeLm7usm42yi-6fXJ3KjEcY_JzON22cG5tH0Ezio1ytQyRbst9D52xjK01Ga0tXEI6q9U9zeCS1fjM51IfSPGRqo1By4yAgbbNQwIOJEsQr0fMXIRaMN3yi_DauifuRgqyv56h2sfInkuFB_3bhUNTMpe-xp0Vn1iSmg-D9Lo4gMAzvuqxXR1_WPyUZi9H9ZDg_KQtx4YsNMHxvxdl7jA6QvZckwveA96CRAb9fU",
|
||||
"fetch_time": 1766040126.4893737
|
||||
}
|
||||
74
src/main.py
74
src/main.py
@ -17,6 +17,7 @@ from src.smartsheet import SmartSheetAPI
|
||||
from src.validator import RecordValidator
|
||||
from src.tapd_api import TAPDApi
|
||||
from src.mapper import FieldMapper, BugCreationResult
|
||||
from src.token_manager import TokenManager
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
@ -31,16 +32,23 @@ def parse_arguments():
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例用法:
|
||||
# 手动提供access_token
|
||||
python main.py --access-token YOUR_ACCESS_TOKEN
|
||||
python main.py -t YOUR_ACCESS_TOKEN --config /path/to/config.ini
|
||||
|
||||
# 自动从环境变量获取access_token(需要设置CORPID和CORPSECRET)
|
||||
python main.py
|
||||
|
||||
# 指定配置文件路径
|
||||
python main.py --config /path/to/config.ini
|
||||
"""
|
||||
)
|
||||
|
||||
# 必需参数
|
||||
# 可选参数:access_token(如果不提供,将自动从环境变量获取)
|
||||
parser.add_argument(
|
||||
'-t', '--access-token',
|
||||
required=True,
|
||||
help='企业微信access_token(必填)'
|
||||
required=False,
|
||||
default=None,
|
||||
help='企业微信access_token(可选,如不提供则自动从环境变量获取)'
|
||||
)
|
||||
|
||||
# 可选参数
|
||||
@ -65,22 +73,47 @@ def parse_arguments():
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def validate_access_token(access_token):
|
||||
def get_or_validate_access_token(access_token_arg):
|
||||
"""
|
||||
验证access_token是否有效
|
||||
获取或验证access_token
|
||||
|
||||
如果命令行提供了token,则验证并使用;
|
||||
如果未提供,则通过TokenManager自动获取
|
||||
|
||||
Args:
|
||||
access_token: 待验证的token
|
||||
access_token_arg: 命令行传入的token(可能为None)
|
||||
|
||||
Returns:
|
||||
str: 有效的access_token
|
||||
|
||||
Raises:
|
||||
ValueError: token无效时抛出
|
||||
ValueError: token无效或环境变量未设置时抛出
|
||||
RuntimeError: 自动获取token失败时抛出
|
||||
"""
|
||||
if not access_token or not access_token.strip():
|
||||
if access_token_arg:
|
||||
# 命令行提供了token,进行验证
|
||||
print("使用命令行提供的access_token")
|
||||
|
||||
if not access_token_arg.strip():
|
||||
raise ValueError("access_token不能为空")
|
||||
|
||||
# 基本格式检查(企业微信的access_token通常较长)
|
||||
if len(access_token.strip()) < 20:
|
||||
raise ValueError("access_token格式可能不正确(长度过短)")
|
||||
|
||||
return access_token_arg.strip()
|
||||
else:
|
||||
# 命令行未提供token,使用TokenManager自动获取
|
||||
print("命令行未提供access_token,将自动从环境变量获取")
|
||||
print()
|
||||
|
||||
try:
|
||||
token_manager = TokenManager()
|
||||
access_token = token_manager.get_token()
|
||||
return access_token
|
||||
except ValueError as e:
|
||||
# 环境变量未设置
|
||||
raise ValueError(f"自动获取access_token失败: {e}")
|
||||
except RuntimeError as e:
|
||||
# API调用失败
|
||||
raise RuntimeError(f"自动获取access_token失败: {e}")
|
||||
|
||||
|
||||
def scan_and_validate_records(access_token: str, docid: str, verbose: bool = False, test_mode: bool = False):
|
||||
@ -437,7 +470,7 @@ def main():
|
||||
"""主函数"""
|
||||
print("=" * 60)
|
||||
print("autoTAPD - Debug阶段自动开单工具")
|
||||
print("版本: 0.2.0 (第二阶段)")
|
||||
print("版本: 0.4.0 (第四阶段 - 支持自动获取access_token)")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
@ -452,10 +485,11 @@ def main():
|
||||
print(f" - verbose: {args.verbose}")
|
||||
print(f" - test: {args.test}")
|
||||
|
||||
# 2. 验证access_token
|
||||
print("[2/3] 验证access_token...")
|
||||
validate_access_token(args.access_token)
|
||||
print(" ✓ access_token格式验证通过")
|
||||
# 2. 获取或验证access_token
|
||||
print("[2/3] 获取access_token...")
|
||||
access_token = get_or_validate_access_token(args.access_token)
|
||||
print(" ✓ access_token准备就绪")
|
||||
print()
|
||||
|
||||
# 3. 加载配置文件
|
||||
print("[3/3] 加载配置文件...")
|
||||
@ -472,14 +506,14 @@ def main():
|
||||
print("-" * 60)
|
||||
print(f"TAPD workspace_id: {all_config['tapd']['workspace_id']}")
|
||||
print(f"SmartSheet docid: {all_config['smartsheet']['docid'][:20]}...")
|
||||
print(f"Access Token: {args.access_token[:10]}...(已隐藏)")
|
||||
print(f"Access Token: {access_token[:10]}...(已隐藏)")
|
||||
if args.test:
|
||||
print(f"测试模式: 已启用")
|
||||
print("=" * 60)
|
||||
|
||||
# 4. 扫描智能表格并校验记录
|
||||
result = scan_and_validate_records(
|
||||
args.access_token,
|
||||
access_token,
|
||||
all_config['smartsheet']['docid'],
|
||||
args.verbose,
|
||||
args.test
|
||||
@ -509,7 +543,7 @@ def main():
|
||||
sheet_id = result.get('sheet_id')
|
||||
if sheet_id:
|
||||
writeback_result = write_back_results(
|
||||
args.access_token,
|
||||
access_token,
|
||||
all_config['smartsheet']['docid'],
|
||||
sheet_id,
|
||||
creation_result['success_results'],
|
||||
|
||||
@ -70,15 +70,7 @@ class SmartSheetAPI:
|
||||
import json
|
||||
print(f"\n响应状态码: {response.status_code}")
|
||||
print(f"响应数据:")
|
||||
# 对于records数据,只显示摘要信息,避免输出过多
|
||||
if endpoint == "smartsheet/get_records" and 'records' in result:
|
||||
display_result = result.copy()
|
||||
records_count = len(result.get('records', []))
|
||||
if records_count > 0:
|
||||
display_result['records'] = f"[{records_count}条记录,已省略详情]"
|
||||
display_result['_records_sample'] = result['records'][0] if records_count > 0 else None
|
||||
print(json.dumps(display_result, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
# 测试模式下显示完整的响应数据
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
print("=" * 80)
|
||||
|
||||
@ -219,11 +211,11 @@ class SmartSheetAPI:
|
||||
|
||||
# 构造过滤条件:开单状态字段为空
|
||||
filter_spec = {
|
||||
"conjunction": "and",
|
||||
"conjunction": "CONJUNCTION_AND",
|
||||
"conditions": [
|
||||
{
|
||||
"field_id": status_field_id,
|
||||
"operator": "is_empty"
|
||||
"operator": "OPERATOR_IS_EMPTY"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
328
src/token_manager.py
Normal file
328
src/token_manager.py
Normal file
@ -0,0 +1,328 @@
|
||||
"""
|
||||
企业微信 Access Token 管理模块
|
||||
负责自动获取、缓存和刷新 access_token
|
||||
|
||||
该模块完全独立,不依赖其他业务模块
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
|
||||
|
||||
class TokenManager:
|
||||
"""企业微信 Access Token 管理器"""
|
||||
|
||||
# 企业微信获取token的API地址
|
||||
TOKEN_API_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||
|
||||
# Token有效期(秒),企业微信返回的是7200秒
|
||||
TOKEN_EXPIRES_IN = 7200
|
||||
|
||||
# 提前刷新时间(秒),在token过期前5分钟就刷新
|
||||
REFRESH_BEFORE_EXPIRE = 300
|
||||
|
||||
def __init__(self, cache_file_path: Optional[str] = None):
|
||||
"""
|
||||
初始化Token管理器
|
||||
|
||||
Args:
|
||||
cache_file_path: token缓存文件路径,如果为None则使用默认路径
|
||||
"""
|
||||
# 确定缓存文件路径
|
||||
if cache_file_path is None:
|
||||
# 默认路径:项目根目录/config/token_cache.json
|
||||
project_root = Path(__file__).parent.parent
|
||||
cache_file_path = project_root / "config" / "token_cache.json"
|
||||
|
||||
self.cache_file_path = Path(cache_file_path)
|
||||
|
||||
# 从环境变量读取企业微信配置
|
||||
self.corpid = os.environ.get('CORPID')
|
||||
self.corpsecret = os.environ.get('CORPSECRET')
|
||||
|
||||
def _validate_env_config(self):
|
||||
"""
|
||||
验证环境变量配置是否完整
|
||||
|
||||
Raises:
|
||||
ValueError: 环境变量未设置时抛出
|
||||
"""
|
||||
if not self.corpid:
|
||||
raise ValueError(
|
||||
"环境变量 CORPID 未设置\n"
|
||||
"请设置环境变量: export CORPID=your_corpid"
|
||||
)
|
||||
|
||||
if not self.corpsecret:
|
||||
raise ValueError(
|
||||
"环境变量 CORPSECRET 未设置\n"
|
||||
"请设置环境变量: export CORPSECRET=your_corpsecret"
|
||||
)
|
||||
|
||||
def _fetch_token_from_api(self) -> Dict[str, any]:
|
||||
"""
|
||||
从企业微信API获取新的access_token
|
||||
|
||||
Returns:
|
||||
Dict: 包含access_token和expires_in的字典
|
||||
|
||||
Raises:
|
||||
RuntimeError: API调用失败时抛出
|
||||
"""
|
||||
print("正在从企业微信API获取新的access_token...")
|
||||
|
||||
try:
|
||||
params = {
|
||||
'corpid': self.corpid,
|
||||
'corpsecret': self.corpsecret
|
||||
}
|
||||
|
||||
response = requests.get(self.TOKEN_API_URL, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
# 检查返回的错误码
|
||||
errcode = result.get('errcode', 0)
|
||||
if errcode != 0:
|
||||
errmsg = result.get('errmsg', '未知错误')
|
||||
raise RuntimeError(f"获取access_token失败: errcode={errcode}, errmsg={errmsg}")
|
||||
|
||||
access_token = result.get('access_token')
|
||||
expires_in = result.get('expires_in', self.TOKEN_EXPIRES_IN)
|
||||
|
||||
if not access_token:
|
||||
raise RuntimeError("API返回的数据中未找到access_token")
|
||||
|
||||
print(f" ✓ 成功获取access_token (有效期: {expires_in}秒)")
|
||||
|
||||
return {
|
||||
'access_token': access_token,
|
||||
'expires_in': expires_in
|
||||
}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
raise RuntimeError("获取access_token超时,请检查网络连接")
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise RuntimeError(f"获取access_token失败: {e}")
|
||||
|
||||
def _load_cache(self) -> Optional[Dict]:
|
||||
"""
|
||||
从缓存文件加载token
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: 缓存的token数据,如果文件不存在或格式错误则返回None
|
||||
"""
|
||||
if not self.cache_file_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(self.cache_file_path, 'r', encoding='utf-8') as f:
|
||||
cache_data = json.load(f)
|
||||
|
||||
# 验证缓存数据格式
|
||||
if 'access_token' not in cache_data or 'fetch_time' not in cache_data:
|
||||
print(" ⚠ 缓存文件格式不正确,将重新获取token")
|
||||
return None
|
||||
|
||||
return cache_data
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(" ⚠ 缓存文件JSON格式错误,将重新获取token")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f" ⚠ 读取缓存文件失败: {e},将重新获取token")
|
||||
return None
|
||||
|
||||
def _save_cache(self, access_token: str, fetch_time: float):
|
||||
"""
|
||||
保存token到缓存文件
|
||||
|
||||
Args:
|
||||
access_token: access_token值
|
||||
fetch_time: 获取时间(时间戳)
|
||||
|
||||
Raises:
|
||||
RuntimeError: 保存失败时抛出
|
||||
"""
|
||||
try:
|
||||
# 确保缓存目录存在
|
||||
self.cache_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cache_data = {
|
||||
'access_token': access_token,
|
||||
'fetch_time': fetch_time
|
||||
}
|
||||
|
||||
with open(self.cache_file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(cache_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f" ✓ Token已缓存到: {self.cache_file_path}")
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"保存token缓存失败: {e}")
|
||||
|
||||
def _is_token_expired(self, fetch_time: float) -> bool:
|
||||
"""
|
||||
检查token是否已过期或即将过期
|
||||
|
||||
Args:
|
||||
fetch_time: token获取时间(时间戳)
|
||||
|
||||
Returns:
|
||||
bool: 如果已过期或即将过期返回True,否则返回False
|
||||
"""
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - fetch_time
|
||||
|
||||
# 如果已使用时间超过 (有效期 - 提前刷新时间),则认为需要刷新
|
||||
return elapsed_time >= (self.TOKEN_EXPIRES_IN - self.REFRESH_BEFORE_EXPIRE)
|
||||
|
||||
def get_token(self) -> str:
|
||||
"""
|
||||
获取有效的access_token
|
||||
|
||||
优先从缓存读取,如果缓存不存在或已过期,则从API获取新token
|
||||
|
||||
Returns:
|
||||
str: 有效的access_token
|
||||
|
||||
Raises:
|
||||
ValueError: 环境变量未设置时抛出
|
||||
RuntimeError: 获取token失败时抛出
|
||||
"""
|
||||
# 1. 验证环境变量配置
|
||||
self._validate_env_config()
|
||||
|
||||
# 2. 尝试从缓存加载
|
||||
print("检查token缓存...")
|
||||
cache_data = self._load_cache()
|
||||
|
||||
if cache_data:
|
||||
access_token = cache_data['access_token']
|
||||
fetch_time = cache_data['fetch_time']
|
||||
|
||||
# 检查是否过期
|
||||
if not self._is_token_expired(fetch_time):
|
||||
elapsed_time = time.time() - fetch_time
|
||||
remaining_time = self.TOKEN_EXPIRES_IN - elapsed_time
|
||||
print(f" ✓ 使用缓存的token (剩余有效期: {int(remaining_time)}秒)")
|
||||
return access_token
|
||||
else:
|
||||
print(" ⚠ 缓存的token已过期或即将过期,将重新获取")
|
||||
else:
|
||||
print(" ⚠ 未找到有效的token缓存")
|
||||
|
||||
# 3. 从API获取新token
|
||||
token_data = self._fetch_token_from_api()
|
||||
access_token = token_data['access_token']
|
||||
fetch_time = time.time()
|
||||
|
||||
# 4. 保存到缓存
|
||||
self._save_cache(access_token, fetch_time)
|
||||
|
||||
return access_token
|
||||
|
||||
def refresh_token(self) -> str:
|
||||
"""
|
||||
强制刷新token(无论是否过期)
|
||||
|
||||
Returns:
|
||||
str: 新的access_token
|
||||
|
||||
Raises:
|
||||
ValueError: 环境变量未设置时抛出
|
||||
RuntimeError: 获取token失败时抛出
|
||||
"""
|
||||
print("强制刷新access_token...")
|
||||
|
||||
# 验证环境变量配置
|
||||
self._validate_env_config()
|
||||
|
||||
# 从API获取新token
|
||||
token_data = self._fetch_token_from_api()
|
||||
access_token = token_data['access_token']
|
||||
fetch_time = time.time()
|
||||
|
||||
# 保存到缓存
|
||||
self._save_cache(access_token, fetch_time)
|
||||
|
||||
return access_token
|
||||
|
||||
def clear_cache(self):
|
||||
"""
|
||||
清除token缓存文件
|
||||
"""
|
||||
if self.cache_file_path.exists():
|
||||
try:
|
||||
self.cache_file_path.unlink()
|
||||
print(f" ✓ 已清除token缓存: {self.cache_file_path}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 清除缓存失败: {e}")
|
||||
else:
|
||||
print(" ℹ 缓存文件不存在,无需清除")
|
||||
|
||||
|
||||
def get_access_token(cache_file_path: Optional[str] = None) -> str:
|
||||
"""
|
||||
便捷函数:获取有效的access_token
|
||||
|
||||
这是一个简化的接口,用于快速获取token而不需要创建TokenManager实例
|
||||
|
||||
Args:
|
||||
cache_file_path: token缓存文件路径,如果为None则使用默认路径
|
||||
|
||||
Returns:
|
||||
str: 有效的access_token
|
||||
|
||||
Raises:
|
||||
ValueError: 环境变量未设置时抛出
|
||||
RuntimeError: 获取token失败时抛出
|
||||
"""
|
||||
manager = TokenManager(cache_file_path)
|
||||
return manager.get_token()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""测试代码"""
|
||||
print("=" * 60)
|
||||
print("Token Manager 测试")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
try:
|
||||
# 测试获取token
|
||||
manager = TokenManager()
|
||||
token = manager.get_token()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试结果")
|
||||
print("=" * 60)
|
||||
print(f"✓ 成功获取access_token")
|
||||
print(f" Token (前20字符): {token[:20]}...")
|
||||
print(f" Token长度: {len(token)}")
|
||||
print("=" * 60)
|
||||
|
||||
# 测试再次获取(应该使用缓存)
|
||||
print("\n再次获取token(测试缓存)...")
|
||||
token2 = manager.get_token()
|
||||
|
||||
if token == token2:
|
||||
print(" ✓ 成功使用缓存的token")
|
||||
else:
|
||||
print(" ⚠ 获取了新的token(可能缓存失效)")
|
||||
|
||||
except ValueError as e:
|
||||
print(f"\n✗ 配置错误: {e}")
|
||||
print("\n请设置以下环境变量:")
|
||||
print(" export CORPID=your_corpid")
|
||||
print(" export CORPSECRET=your_corpsecret")
|
||||
except RuntimeError as e:
|
||||
print(f"\n✗ 运行错误: {e}")
|
||||
except Exception as e:
|
||||
print(f"\n✗ 未预期的错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
274
开发路线.md
274
开发路线.md
@ -7,14 +7,14 @@
|
||||
## 二、开发说明
|
||||
|
||||
### 2.1 access_token管理策略
|
||||
- **第五阶段前:** 所有需要access_token的操作,由开发者手动获取token并传入程序
|
||||
- **第五阶段:** 实现access_token的自动获取与缓存机制
|
||||
- **第五阶段后:** 程序自动管理access_token的获取、缓存和刷新
|
||||
- **第四阶段前:** 所有需要access_token的操作,由开发者手动获取token并传入程序
|
||||
- **第四阶段:** 实现access_token的自动获取与缓存机制
|
||||
- **第四阶段后:** 程序自动管理access_token的获取、缓存和刷新
|
||||
|
||||
### 2.2 日志记录策略
|
||||
- **所有api调用记录都要写入log文件夹下的api_log.json**
|
||||
- **第八阶段前:** 使用简单的print输出关键信息
|
||||
- **第八阶段:** 实现完整的日志记录系统
|
||||
- **第九阶段前:** 使用简单的print输出关键信息
|
||||
- **第九阶段:** 实现完整的日志记录系统
|
||||
|
||||
### 2.3 API频率限制
|
||||
|
||||
@ -269,9 +269,9 @@
|
||||
- 重试机制
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 开单成功后能正确回写✅到"开单状态"字段
|
||||
- [ ] 能正确回写TAPD单号,并生成可点击的链接
|
||||
- [ ] 能正确回写bug状态
|
||||
- [x] 开单成功后能正确回写✅到"开单状态"字段
|
||||
- [x] 能正确回写TAPD单号,并生成可点击的链接
|
||||
- [x] 能正确回写bug状态
|
||||
- [ ] 开单失败后能正确回写❌到"开单状态"字段
|
||||
- [ ] 回写失败时有重试机制
|
||||
- [ ] 所有操作都有详细的日志记录
|
||||
@ -288,102 +288,152 @@
|
||||
|
||||
---
|
||||
|
||||
### 第四阶段:定时任务与服务化
|
||||
### 第四阶段:access_token自动获取与缓存
|
||||
|
||||
**目标:** 将工具部署为服务,实现定时扫描和状态同步
|
||||
**目标:** 实现access_token的自动获取与缓存机制,不再需要手动传入token,该模块应该与其他功能独立
|
||||
|
||||
**任务清单:**
|
||||
|
||||
1. 实现企业微信认证模块
|
||||
- 从环境变量读取CORPID和CORPSECRET
|
||||
- 调用企业微信API获取access_token
|
||||
- 处理认证失败的情况
|
||||
2. 设计token缓存文件结构
|
||||
- 在项目根目录创建token缓存文件(token_cache.json)
|
||||
- 存储token值
|
||||
- 存储获取时间(时间戳)
|
||||
3. 实现token缓存逻辑
|
||||
- 首次获取时写入缓存文件
|
||||
- 每次使用前从文件读取并检查是否过期(当前时间 - 获取时间 >= 7200秒)
|
||||
- 过期则重新获取并更新缓存文件
|
||||
4. 实现token失效处理
|
||||
- 检测token失效(API返回42001错误码)
|
||||
- 自动重新获取并更新缓存
|
||||
5. 重构现有代码
|
||||
- 保留命令行传入access_token的逻辑,如无传入则自动从缓存获取
|
||||
- 所有API调用自动使用缓存的token
|
||||
- 修改wework_api.py,添加token管理功能
|
||||
|
||||
**验收标准:**
|
||||
- [x] 能从环境变量读取corpid和corpsecret
|
||||
- [x] 能自动获取access_token并写入缓存文件
|
||||
- [x] token能正确从文件读取和复用
|
||||
- [x] token过期时能自动重新获取
|
||||
- [ ] token失效时(API返回42001)能自动重新获取
|
||||
- [x] 不再需要手动传入access_token(命令行参数可选)
|
||||
- [ ] 减少了获取token的API调用次数
|
||||
- [ ] 环境变量未设置时有清晰的错误提示
|
||||
- [x] 缓存文件格式清晰易读
|
||||
|
||||
*修复了开单状态不为空的记录也被读取的问题:filter_spec的conjunction应该为CONJUNCTION_AND而不是and
|
||||
|
||||
**技术要点:**
|
||||
|
||||
- token有效期为7200秒(2小时)
|
||||
- 使用时间戳判断过期(time.time())
|
||||
- 使用os.environ读取环境变量
|
||||
- JSON文件的读写操作
|
||||
- 异常处理:网络错误、认证失败、文件读写错误等
|
||||
|
||||
**缓存文件格式示例:**
|
||||
```json
|
||||
{
|
||||
"access_token": "xxx",
|
||||
"fetch_time": 1702800000
|
||||
}
|
||||
```
|
||||
|
||||
**潜在问题记录:**
|
||||
- 多实例部署时的token文件竞争问题(本阶段暂不处理)
|
||||
- 时钟不同步导致的过期判断错误
|
||||
- 环境变量的安全性问题
|
||||
- 缓存文件的权限问题
|
||||
|
||||
---
|
||||
|
||||
### 第五阶段:定时任务与服务化
|
||||
|
||||
**目标:** 实现定时任务调度,让工具能够自动定期执行开单扫描
|
||||
|
||||
**任务清单:**
|
||||
1. 实现定时任务调度
|
||||
- 使用schedule库或APScheduler
|
||||
- 配置开单扫描频率(默认5分钟)
|
||||
- 配置状态同步频率(默认15分钟)
|
||||
2. 实现bug状态同步功能
|
||||
- 查询已开单的记录
|
||||
- 调用TAPD API获取bug最新状态
|
||||
- 更新智能表格的"bug状态"字段
|
||||
3. 实现服务启动和停止
|
||||
- 命令行参数解析
|
||||
- 优雅退出机制
|
||||
4. 实现配置文件扩展
|
||||
- 添加轮询频率配置
|
||||
- 添加状态同步频率配置
|
||||
5. 完善日志和监控
|
||||
- 添加运行统计信息
|
||||
- 添加性能监控
|
||||
- 创建调度脚本(scheduler.py)
|
||||
2. 实现服务启动和停止
|
||||
- 命令行参数解析(启动、停止)
|
||||
- 优雅退出机制(捕获SIGINT、SIGTERM信号)
|
||||
3. 实现配置文件扩展
|
||||
- 在config.ini中添加轮询频率配置
|
||||
4. 完善执行统计
|
||||
- 每次执行后输出统计信息(扫描X条,成功Y条,失败Z条)
|
||||
- 记录执行时间
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 服务能正常启动和停止
|
||||
- [ ] 能按配置的频率执行开单扫描
|
||||
- [ ] 能按配置的频率执行状态同步
|
||||
- [ ] 状态同步功能正常工作
|
||||
- [ ] 服务异常时能自动恢复
|
||||
- [ ] 有完善的运行日志和统计信息
|
||||
- [ ] 每次执行都调用main.py的核心逻辑
|
||||
- [ ] 服务异常时能自动恢复(捕获异常后继续下次执行)
|
||||
- [ ] 有清晰的执行统计信息
|
||||
- [ ] Ctrl+C能优雅退出
|
||||
|
||||
**技术要点:**
|
||||
- 定时任务的线程安全
|
||||
- 信号处理(SIGINT、SIGTERM)
|
||||
- schedule库的基本使用
|
||||
- 信号处理(signal模块)
|
||||
- 异常捕获和恢复
|
||||
- 配置热加载(可选)
|
||||
- 循环执行的设计
|
||||
|
||||
**潜在问题记录:**
|
||||
- 长时间运行的内存泄漏问题
|
||||
- access_token过期的自动刷新
|
||||
- 并发执行的冲突处理
|
||||
- 并发执行的冲突处理(如果上次执行未完成)
|
||||
|
||||
---
|
||||
|
||||
### 第五阶段:access_token自动获取与缓存
|
||||
### 第六阶段:bug状态同步功能
|
||||
|
||||
**目标:** 实现access_token的自动获取与缓存机制,不再需要手动传入token
|
||||
**目标:** 实现定期同步TAPD的bug状态到智能表格
|
||||
|
||||
**任务清单:**
|
||||
1. 实现企业微信认证模块
|
||||
- 从环境变量读取corpid和corpsecret
|
||||
- 调用企业微信API获取access_token
|
||||
- 处理认证失败的情况
|
||||
2. 设计token缓存结构
|
||||
- 存储token值
|
||||
- 存储过期时间
|
||||
- 存储获取时间
|
||||
3. 实现token缓存逻辑
|
||||
- 首次获取时缓存
|
||||
- 使用前检查是否过期
|
||||
- 过期前自动刷新(建议提前5分钟)
|
||||
4. 实现持久化存储(可选)
|
||||
- 使用文件存储token
|
||||
- 服务重启后能恢复token
|
||||
5. 实现token失效处理
|
||||
- 检测token失效(API返回42001错误码)
|
||||
- 自动重新获取
|
||||
6. 重构现有代码
|
||||
- 保留命令行传入access_token的逻辑,如无传入则自动调用缓存的token
|
||||
- 所有API调用自动使用缓存的token
|
||||
1. 实现状态同步功能模块
|
||||
- 查询智能表格中已开单的记录("开单状态"为✅且"TAPD单号"不为空)
|
||||
- 过滤掉状态为"完成"或"取消"的记录
|
||||
- 调用TAPD API获取bug最新状态
|
||||
- 对比状态是否变化
|
||||
2. 实现状态回写逻辑
|
||||
- 只有状态发生变化时才更新智能表格
|
||||
- 批量更新智能表格的"bug状态"字段
|
||||
3. 集成到定时任务
|
||||
- 在scheduler.py中添加状态同步任务
|
||||
- 配置状态同步频率(默认15分钟)
|
||||
4. 实现配置文件扩展
|
||||
- 在config.ini中添加状态同步频率配置
|
||||
5. 完善错误处理
|
||||
- TAPD API调用失败的处理
|
||||
- 智能表格更新失败的处理
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 能从环境变量读取corpid和corpsecret
|
||||
- [ ] 能自动获取access_token
|
||||
- [ ] token能正确缓存和复用
|
||||
- [ ] token过期前能自动刷新
|
||||
- [ ] token失效时能自动重新获取
|
||||
- [ ] 不再需要手动传入access_token
|
||||
- [ ] 减少了获取token的API调用次数
|
||||
- [ ] 环境变量未设置时有清晰的错误提示
|
||||
- [ ] 能正确查询已开单的记录
|
||||
- [ ] 能正确过滤"完成"和"取消"状态的记录
|
||||
- [ ] 能成功调用TAPD API获取bug状态
|
||||
- [ ] 状态变化时能正确回写到智能表格
|
||||
- [ ] 状态未变化时不进行更新(减少API调用)
|
||||
- [ ] 能按配置的频率执行状态同步
|
||||
- [ ] 有清晰的同步统计信息(检查X条,更新Y条)
|
||||
|
||||
**技术要点:**
|
||||
- token有效期为7200秒(2小时)
|
||||
- 建议在过期前5分钟刷新
|
||||
- 使用os.environ读取环境变量
|
||||
- 线程安全的缓存实现
|
||||
- 异常处理:网络错误、认证失败等
|
||||
- TAPD获取bug详情API
|
||||
- 状态对比逻辑
|
||||
- 批量更新优化
|
||||
- 定时任务的多任务调度
|
||||
|
||||
**潜在问题记录:**
|
||||
- 多实例部署时的token共享问题
|
||||
- 时钟不同步导致的过期判断错误
|
||||
- 环境变量的安全性问题
|
||||
- 大量bug的状态查询性能问题
|
||||
- TAPD API频率限制
|
||||
- 状态映射的准确性
|
||||
|
||||
---
|
||||
|
||||
### 第六阶段:企业微信推送功能
|
||||
### 第七阶段:企业微信推送功能
|
||||
|
||||
**目标:** 实现开单失败时的企业微信推送通知
|
||||
|
||||
@ -425,48 +475,27 @@
|
||||
|
||||
---
|
||||
|
||||
### 第七阶段:字段合法性校验与优化
|
||||
### 第八阶段:测试
|
||||
|
||||
**目标:** 完善字段值的合法性校验,提高数据质量
|
||||
**目标:** 进行边界测试等,防止出现意外情况导致服务出错
|
||||
|
||||
**任务清单:**
|
||||
1. 实现优先级字段校验
|
||||
- 获取TAPD项目的优先级配置
|
||||
- 校验智能表格中的值是否合法
|
||||
2. 实现严重程度字段校验
|
||||
- 校验可选值:fatal、serious、normal、prompt、advice
|
||||
3. 实现人员字段校验
|
||||
- 校验userid格式
|
||||
- 校验用户是否存在
|
||||
4. 实现模块字段校验
|
||||
- 获取TAPD项目的模块配置
|
||||
- 校验模块是否存在
|
||||
5. 实现版本字段校验
|
||||
- 获取TAPD项目的版本配置
|
||||
- 校验版本是否存在
|
||||
6. 完善错误提示
|
||||
- 明确指出不合法的字段和原因
|
||||
- 提供修正建议
|
||||
1.
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 能正确校验所有字段的合法性
|
||||
- [ ] 不合法的值有明确的错误提示
|
||||
- [ ] 校验失败时不创建TAPD单
|
||||
- [ ] 校验结果能回写到智能表格(可选)
|
||||
- [ ]
|
||||
- [ ]
|
||||
|
||||
**技术要点:**
|
||||
- TAPD API:获取字段配置接口
|
||||
- 智能表格:单选字段的option_id
|
||||
- 数据缓存:避免频繁查询配置
|
||||
- 错误信息的友好展示
|
||||
|
||||
-
|
||||
|
||||
**潜在问题记录:**
|
||||
- TAPD配置变更时的同步问题
|
||||
- 自定义字段的校验规则
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
### 第八阶段:日志系统与性能优化
|
||||
### 第九阶段:日志系统与性能优化
|
||||
|
||||
**目标:** 实现完整的日志记录系统,优化系统性能,添加监控和告警
|
||||
|
||||
@ -534,8 +563,8 @@
|
||||
- argparse:命令行参数解析
|
||||
- json:JSON文件处理
|
||||
- datetime:时间处理
|
||||
- logging:日志记录(第八阶段)
|
||||
- schedule/APScheduler:定时任务(第四阶段)
|
||||
- logging:日志记录(第九阶段)
|
||||
- schedule/APScheduler:定时任务(第五阶段)
|
||||
- **API:**
|
||||
- 企业微信文档API
|
||||
- TAPD Open API
|
||||
@ -551,14 +580,15 @@ workspace_id = 10158231
|
||||
docid = your_doc_id
|
||||
|
||||
[Schedule]
|
||||
# 第四阶段添加
|
||||
# 第五阶段添加
|
||||
scan_interval = 5
|
||||
# 第六阶段添加
|
||||
sync_interval = 15
|
||||
```
|
||||
|
||||
### 环境变量(第五阶段开始使用)
|
||||
### 环境变量(第四阶段开始使用)
|
||||
```bash
|
||||
# 企业微信(第五阶段)
|
||||
# 企业微信(第四阶段)
|
||||
WEWORK_CORPID=your_corpid
|
||||
WEWORK_CORPSECRET=your_corpsecret
|
||||
|
||||
@ -577,18 +607,20 @@ autoTAPD/
|
||||
│ ├── __init__.py
|
||||
│ ├── api_test.py # 前期准备:API测试工具
|
||||
│ ├── config.py # 配置管理
|
||||
│ ├── wework_api.py # 企业微信API(第五阶段完善)
|
||||
│ ├── wework_api.py # 企业微信API(第四阶段完善)
|
||||
│ ├── smartsheet.py # 智能表格操作
|
||||
│ ├── tapd_api.py # TAPD API
|
||||
│ ├── validator.py # 数据校验
|
||||
│ ├── mapper.py # 字段映射
|
||||
│ ├── token_cache.py # token缓存(第五阶段)
|
||||
│ ├── scheduler.py # 定时任务(第四阶段)
|
||||
│ ├── logger.py # 日志模块(第八阶段)
|
||||
│ ├── scheduler.py # 定时任务(第五阶段)
|
||||
│ ├── sync_status.py # bug状态同步(第六阶段)
|
||||
│ ├── logger.py # 日志模块(第九阶段)
|
||||
│ └── main.py # 主程序
|
||||
├── logs/ # 日志目录(第八阶段)
|
||||
├── logs/ # 日志目录(第九阶段)
|
||||
│ ├── api_log.json # API调用记录
|
||||
│ └── api_test_log.json # API测试记录
|
||||
├── tests/ # 测试代码
|
||||
├── token_cache.json # token缓存文件(第四阶段)
|
||||
├── requirements.txt # 依赖包
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
@ -609,8 +641,8 @@ autoTAPD/
|
||||
3. **错误恢复:** 服务异常时要能自动恢复,不影响后续执行
|
||||
4. **向后兼容:** 配置文件和数据结构的变更要考虑向后兼容
|
||||
5. **文档更新:** 每个阶段完成后更新相关文档
|
||||
6. **access_token管理:** 第五阶段前手动传入,第五阶段后自动管理
|
||||
7. **日志策略:** 第八阶段前使用print,第八阶段后使用logging模块
|
||||
6. **access_token管理:** 第四阶段前手动传入,第四阶段后自动管理
|
||||
7. **日志策略:** 第九阶段前使用print,第九阶段后使用logging模块
|
||||
|
||||
## 十、阶段依赖关系
|
||||
|
||||
@ -623,15 +655,17 @@ autoTAPD/
|
||||
↓
|
||||
第三阶段(回写结果)
|
||||
↓
|
||||
第四阶段(定时任务)
|
||||
第四阶段(access_token自动化)
|
||||
↓
|
||||
第五阶段(access_token自动化)
|
||||
第五阶段(定时任务与服务化)
|
||||
↓
|
||||
第六阶段(企业微信推送)
|
||||
第六阶段(bug状态同步)
|
||||
↓
|
||||
第七阶段(字段合法性校验)
|
||||
第七阶段(企业微信推送)
|
||||
↓
|
||||
第八阶段(日志系统 + 性能优化)
|
||||
第八阶段(测试)
|
||||
↓
|
||||
第九阶段(日志系统 + 性能优化)
|
||||
```
|
||||
|
||||
## 十一、后续优化方向
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user