task2-phase1: 基础框架搭建

This commit is contained in:
zelong 2026-01-07 20:15:02 +08:00
parent 8b9a96118a
commit bf4f346096
8 changed files with 754 additions and 9 deletions

13
config/config_task2.ini Normal file
View File

@ -0,0 +1,13 @@
# 任务二配置文件TAPD状态实时同步至腾讯智能表格
[TAPD]
# TAPD项目ID
workspace_id = 58335167
[SmartSheet]
# 智能表格文档ID任务二专用
docid = your_task2_doc_id
[Schedule]
# 同步频率(分钟)
sync_interval = 15

View File

@ -1,4 +1,4 @@
{
"access_token": "SPFfB9jMxleGiJn76FQH6v5pploseAJXTCVkTVVxl1_PEmHZJiqXsNGpEyGAK4qmYe3WRxYJ57xgCQLRCHopVfeoDfP87IgxVCytCqQABGES5ndG05SVkrI-9evg8Z4kbstlsiRMmfPGGGoNUgL1kUoZc2No0FYytm8FTfulnAXfiTExzoF8OCTdEPc9mA0g8JKFhlkiS2F0agBESS_2_ewbcZvA0i44-ChTKRBdRa0",
"fetch_time": 1767599732.4166858
"access_token": "1vVdzrLNZahtARUianAypD-WjGnSvHzU9_p0vXE2wmawFj3WigMA-eZZ1W-t0Tki2KWIs_yYrcrTAInDngJcS-_uqupCNlgEYUjL1bMeS2GGnNM1e3spBT9XdUjF8yTvP3XXtwrlY_qS9Se2S09GQEk1VLxnou1okTjmnzdaNQJbTg013-R_uUp3E-CyNFy7t1tQHN87tr9l2GzlzGt6EshjNcJq4COuCgbs5wBA298",
"fetch_time": 1767787715.7755494
}

View File

@ -0,0 +1,15 @@
{
"records": [],
{
"api_type": "test",
"operation": "task2/setup_test",
"timestamp": "2026-01-07 20:14:18",
"success": true,
"request": {
"test": "验证脚本测试"
},
"response": {
"status": "success"
}
}
]}

View File

@ -22,7 +22,13 @@ from src.config import ConfigManager
class WeWorkAPITester:
"""企业微信API测试类"""
def __init__(self):
def __init__(self, auto_load_token=True):
"""
初始化企业微信API测试类
Args:
auto_load_token: 是否自动加载token默认为True
"""
self.access_token = None
self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs', 'api_test_log.json')
self.base_url = "https://qyapi.weixin.qq.com/cgi-bin"
@ -30,12 +36,92 @@ class WeWorkAPITester:
# 确保日志文件存在
self._init_log_file()
# 自动加载token
if auto_load_token:
self._auto_load_token()
def _init_log_file(self):
"""初始化日志文件"""
if not os.path.exists(self.log_file):
with open(self.log_file, 'w', encoding='utf-8') as f:
json.dump({"records": []}, f, ensure_ascii=False, indent=2)
def _auto_load_token(self):
"""
自动加载access_token
优先从缓存读取如果缓存不存在或已过期则尝试从API获取新token
"""
print("\n=== 自动加载access_token ===")
# 先尝试从缓存读取
if self._load_token_from_cache_silent():
return
# 缓存无效尝试从API获取
print(" 尝试从API获取新token...")
try:
from src.token_manager import TokenManager
token_manager = TokenManager()
self.access_token = token_manager.get_token()
print(f" ✓ 成功获取access_token")
print(f" Token: {self.access_token[:20]}...")
except ValueError as e:
print(f" ⚠ 环境变量未配置: {e}")
print(" 请手动选择菜单选项1获取token")
except Exception as e:
print(f" ⚠ 自动获取token失败: {e}")
print(" 请手动选择菜单选项1获取token")
def _load_token_from_cache_silent(self):
"""
静默从缓存读取token不打印标题
Returns:
bool: 是否成功读取有效token
"""
cache_file = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
'config',
'token_cache.json'
)
try:
if not os.path.exists(cache_file):
print(" 缓存文件不存在")
return False
with open(cache_file, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
access_token = cache_data.get('access_token')
fetch_time = cache_data.get('fetch_time')
if not access_token:
print(" 缓存文件中没有access_token")
return False
# 检查token是否过期7200秒 = 2小时提前5分钟刷新
import time
current_time = time.time()
elapsed_time = current_time - fetch_time
remaining_time = 7200 - elapsed_time
if remaining_time <= 300: # 剩余不足5分钟视为过期
print(f" 缓存的token已过期或即将过期")
return False
# token有效
self.access_token = access_token
print(f" ✓ 从缓存读取access_token成功")
print(f" Token: {self.access_token[:20]}...")
print(f" 剩余有效期: {int(remaining_time)}秒 ({int(remaining_time//60)}分钟)")
return True
except Exception as e:
print(f" 读取缓存失败: {e}")
return False
def _log_api_call(self, operation, request_data, response_data):
"""记录API调用到JSON文件"""
try:
@ -481,6 +567,110 @@ class TAPDAPITester:
except Exception as e:
print(f"✗ 记录日志失败: {str(e)}")
def get_story_fields_info(self):
"""
获取TAPD需求的所有字段配置及候选值
Returns:
dict: 字段配置信息失败返回None
"""
print("\n=== 获取TAPD需求字段配置 ===")
# 初始化认证信息
if not self._init_auth():
return None
# 初始化workspace_id
if not self._init_workspace_id():
return None
url = f"{self.base_url}/stories/get_fields_info"
params = {
"workspace_id": self.workspace_id
}
try:
from requests.auth import HTTPBasicAuth
auth = HTTPBasicAuth(self.api_user, self.api_password)
print(f"\n正在请求TAPD API...")
print(f" URL: {url}")
print(f" workspace_id: {self.workspace_id}")
response = requests.get(url, params=params, auth=auth, timeout=30)
response_data = response.json()
# 记录API调用
request_data = {
"url": url,
"method": "GET",
"params": params,
"auth_user": self.api_user
}
self._log_api_call("get_story_fields_info", request_data, response_data)
# 检查返回结果
if response_data.get("status") == 1:
data = response_data.get("data", {})
print(f"\n✓ 成功获取需求字段配置")
# 显示字段统计
if isinstance(data, dict):
field_count = len(data)
print(f"{field_count} 个字段配置")
# 保存到单独的文件
output_file = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
'logs',
'tapd_story_fields.json'
)
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f" ✓ 字段配置已保存到: {output_file}")
# 显示部分字段信息
print(f"\n需求字段列表预览:")
print("-" * 80)
for idx, (field_name, field_info) in enumerate(list(data.items())[:10], 1):
field_label = field_info.get('label', '(无标签)')
html_type = field_info.get('html_type', '(未知类型)')
print(f" {idx}. {field_name} ({field_label}) - 类型: {html_type}")
# 如果有候选值,显示
options = field_info.get('options', [])
if options:
if isinstance(options, dict):
option_list = list(options.values())[:5]
total_count = len(options)
elif isinstance(options, list):
option_list = options[:5]
total_count = len(options)
else:
option_list = []
total_count = 0
if option_list:
print(f" 候选值: {', '.join(str(o) for o in option_list)}" +
(f" ...等{total_count}" if total_count > 5 else ""))
if field_count > 10:
print(f" ... 还有 {field_count - 10} 个字段,详见输出文件")
print("-" * 80)
return data
else:
print(f"\n✗ 获取失败")
print(f" 状态码: {response_data.get('status')}")
print(f" 错误信息: {response_data.get('info', '未知错误')}")
return None
except Exception as e:
print(f"\n✗ 请求异常: {str(e)}")
import traceback
traceback.print_exc()
return None
def get_bug_custom_fields(self):
"""
获取TAPD缺陷的所有字段配置及候选值
@ -586,6 +776,121 @@ class TAPDAPITester:
traceback.print_exc()
return None
def get_story(self, story_id):
"""
根据需求ID获取需求详情
Args:
story_id: 需求ID
Returns:
dict: 需求信息失败返回None
"""
print("\n=== 获取TAPD需求 ===")
# 初始化认证信息
if not self._init_auth():
return None
# 初始化workspace_id
if not self._init_workspace_id():
return None
# 验证story_id
if not story_id:
print("✗ 需求ID不能为空")
return None
url = f"{self.base_url}/stories"
params = {
"workspace_id": self.workspace_id,
"id": story_id
}
try:
from requests.auth import HTTPBasicAuth
auth = HTTPBasicAuth(self.api_user, self.api_password)
print(f"\n正在请求TAPD API...")
print(f" URL: {url}")
print(f" workspace_id: {self.workspace_id}")
print(f" story_id: {story_id}")
response = requests.get(url, params=params, auth=auth, timeout=30)
response_data = response.json()
# 记录API调用
request_data = {
"url": url,
"method": "GET",
"params": params,
"auth_user": self.api_user
}
self._log_api_call("get_story", request_data, response_data)
# 检查返回结果
if response_data.get("status") == 1:
data = response_data.get("data", [])
if not data:
print(f"\n✗ 未找到需求ID为 {story_id} 的需求")
return None
# TAPD返回的是列表取第一个
story_data = data[0] if isinstance(data, list) else data
story_info = story_data.get("Story", {})
print(f"\n✓ 成功获取需求信息")
print(f"\n需求详情:")
print("=" * 80)
# 显示关键字段
key_fields = [
("id", "ID"),
("name", "标题"),
("status", "状态"),
("priority", "优先级"),
("owner", "处理人"),
("creator", "创建人"),
("created", "创建时间"),
("modified", "最后修改时间"),
("iteration_id", "迭代ID"),
("description", "详细描述")
]
for field_name, field_label in key_fields:
value = story_info.get(field_name, '')
if field_name == "description" and value:
# 描述字段可能很长只显示前100个字符
if len(str(value)) > 100:
value = str(value)[:100] + "..."
print(f" {field_label}: {value}")
print("=" * 80)
# 保存完整数据到文件
output_file = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
'logs',
f'tapd_story_{story_id}.json'
)
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(story_info, f, ensure_ascii=False, indent=2)
print(f"\n✓ 完整需求信息已保存到: {output_file}")
return story_info
else:
print(f"\n✗ 获取失败")
print(f" 状态码: {response_data.get('status')}")
print(f" 错误信息: {response_data.get('info', '未知错误')}")
return None
except Exception as e:
print(f"\n✗ 请求异常: {str(e)}")
import traceback
traceback.print_exc()
return None
def upload_attachment(self, file_path, entry_type, entry_id, owner=None, overwrite=False):
"""
上传附件到TAPD
@ -927,10 +1232,12 @@ def print_menu():
print("5. 发送应用消息")
print("\n【TAPD API】")
print("6. 获取缺陷字段配置")
print("7. 获取附件列表")
print("8. 上传附件")
print("7. 获取需求字段配置")
print("8. 获取需求")
print("9. 获取附件列表")
print("10. 上传附件")
print("\n【其他】")
print("9. 查看日志文件")
print("11. 查看日志文件")
print("0. 退出")
print("="*50)
@ -942,7 +1249,7 @@ def main():
while True:
print_menu()
choice = input("\n请选择操作 (0-9): ").strip()
choice = input("\n请选择操作 (0-11): ").strip()
if choice == "0":
print("\n感谢使用,再见!")
@ -1018,6 +1325,18 @@ def main():
tapd_tester.get_bug_custom_fields()
elif choice == "7":
# 获取TAPD需求字段配置
tapd_tester.get_story_fields_info()
elif choice == "8":
# 获取TAPD需求
story_id = input("\n请输入需求ID: ").strip()
if not story_id:
print("✗ 需求ID不能为空")
continue
tapd_tester.get_story(story_id)
elif choice == "9":
# 获取TAPD附件列表
print("\n=== 获取附件列表 ===")
print("是否需要添加筛选条件?")
@ -1055,7 +1374,7 @@ def main():
limit=limit
)
elif choice == "8":
elif choice == "10":
# 上传附件到TAPD
print("\n=== 上传附件 ===")
file_path = input("请输入文件路径: ").strip()
@ -1099,7 +1418,7 @@ def main():
overwrite=overwrite
)
elif choice == "9":
elif choice == "11":
print("\n=== 查看日志文件 ===")
try:
with open(wework_tester.log_file, 'r', encoding='utf-8') as f:

3
src2/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""
任务二TAPD状态实时同步至腾讯智能表格
"""

119
src2/config.py Normal file
View File

@ -0,0 +1,119 @@
"""
任务二配置管理模块
复用任务一的ConfigManager读取任务二专用配置文件
"""
import sys
from pathlib import Path
# 将项目根目录添加到 Python 路径,以便导入 src 模块
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.config import ConfigManager as BaseConfigManager
class Task2ConfigManager(BaseConfigManager):
"""任务二配置管理器继承自任务一的ConfigManager"""
def __init__(self, config_path=None):
"""
初始化任务二配置管理器
Args:
config_path: 配置文件路径如果为None则使用任务二默认路径
"""
if config_path is None:
# 默认路径:项目根目录/config/config_task2.ini
config_path = project_root / "config" / "config_task2.ini"
super().__init__(config_path)
def get_tapd_config(self):
"""
获取TAPD配置任务二版本不需要reporter
Returns:
dict: 包含workspace_id的字典
"""
if not self.config.has_section('TAPD'):
raise ValueError("配置文件缺少[TAPD]节")
if not self.config.has_option('TAPD', 'workspace_id'):
raise ValueError("配置文件[TAPD]节缺少workspace_id配置项")
workspace_id = self.config.get('TAPD', 'workspace_id').strip()
if not workspace_id:
raise ValueError("workspace_id配置项不能为空")
return {
'workspace_id': workspace_id
}
def get_schedule_config(self):
"""
获取调度配置任务二版本只需要sync_interval
Returns:
dict: 包含sync_interval的字典
"""
default_sync_interval = 15
if not self.config.has_section('Schedule'):
return {'sync_interval': default_sync_interval}
if not self.config.has_option('Schedule', 'sync_interval'):
return {'sync_interval': default_sync_interval}
sync_interval_str = self.config.get('Schedule', 'sync_interval').strip()
try:
sync_interval = int(sync_interval_str)
except ValueError:
raise ValueError(f"sync_interval必须为整数当前值: {sync_interval_str}")
if sync_interval <= 0:
raise ValueError(f"sync_interval必须为正整数当前值: {sync_interval}")
return {'sync_interval': sync_interval}
def get_all_config(self):
"""获取所有配置"""
return {
'tapd': self.get_tapd_config(),
'smartsheet': self.get_smartsheet_config(),
'schedule': self.get_schedule_config()
}
def print_config(self):
"""打印当前配置信息"""
print("\n=== 任务二配置信息 ===")
try:
tapd_config = self.get_tapd_config()
print(f"[TAPD]")
print(f" workspace_id: {tapd_config['workspace_id']}")
except ValueError as e:
print(f"[TAPD] 配置错误: {e}")
try:
smartsheet_config = self.get_smartsheet_config()
print(f"[SmartSheet]")
print(f" docid: {smartsheet_config['docid']}")
except ValueError as e:
print(f"[SmartSheet] 配置错误: {e}")
try:
schedule_config = self.get_schedule_config()
print(f"[Schedule]")
print(f" sync_interval: {schedule_config['sync_interval']} 分钟")
except ValueError as e:
print(f"[Schedule] 配置错误: {e}")
print("======================\n")
if __name__ == "__main__":
try:
config = Task2ConfigManager()
config.print_config()
except Exception as e:
print(f"错误: {e}")

56
src2/logger.py Normal file
View File

@ -0,0 +1,56 @@
"""
任务二日志模块
创建独立的 APILogger 实例日志写入 logs2/ 目录
设计说明
- 不修改 src/api_logger.py get_logger() 全局单例
- 任务二使用独立的 logger 实例避免与任务一冲突
- 两个任务可以同时运行日志互不干扰
"""
import sys
from pathlib import Path
# 将项目根目录添加到 Python 路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.api_logger import APILogger
# 任务二日志目录
TASK2_LOG_DIR = project_root / "logs2"
# 任务二专用的 logger 实例(模块级单例)
_task2_logger = None
def get_task2_logger() -> APILogger:
"""
获取任务二专用的日志记录器
Returns:
APILogger: 任务二专用的日志记录器实例
"""
global _task2_logger
if _task2_logger is None:
_task2_logger = APILogger(log_dir=str(TASK2_LOG_DIR))
return _task2_logger
if __name__ == "__main__":
print("=== 任务二日志模块测试 ===\n")
logger = get_task2_logger()
# 测试记录一条日志
logger.log_api_call(
api_type="test",
operation="task2/test_log",
request_data={"test": "任务二日志测试"},
response_data={"status": "ok"},
success=True
)
print(f"日志目录: {TASK2_LOG_DIR}")
print(f"日志文件: {logger._get_today_log_file()}")
print("\n测试完成,请检查 logs2/ 目录")

220
src2/test_setup.py Normal file
View File

@ -0,0 +1,220 @@
"""
任务二第一阶段验证脚本
验证基础框架搭建是否正确
"""
import sys
from pathlib import Path
# 将项目根目录添加到 Python 路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
def test_directory_structure():
"""测试1: 验证目录结构"""
print("=" * 50)
print("测试1: 验证目录结构")
print("=" * 50)
src2_dir = project_root / "src2"
logs2_dir = project_root / "logs2"
config_file = project_root / "config" / "config_task2.ini"
results = []
# 检查 src2 目录
if src2_dir.exists() and src2_dir.is_dir():
print(f" [OK] src2/ 目录存在")
results.append(True)
else:
print(f" [FAIL] src2/ 目录不存在")
results.append(False)
# 检查 logs2 目录
if logs2_dir.exists() and logs2_dir.is_dir():
print(f" [OK] logs2/ 目录存在")
results.append(True)
else:
print(f" [FAIL] logs2/ 目录不存在")
results.append(False)
# 检查配置文件
if config_file.exists():
print(f" [OK] config/config_task2.ini 存在")
results.append(True)
else:
print(f" [FAIL] config/config_task2.ini 不存在")
results.append(False)
return all(results)
def test_config_read():
"""测试2: 验证配置文件读取"""
print("\n" + "=" * 50)
print("测试2: 验证配置文件读取")
print("=" * 50)
try:
from src2.config import Task2ConfigManager
config = Task2ConfigManager()
# 读取TAPD配置
tapd_config = config.get_tapd_config()
print(f" [OK] TAPD配置读取成功")
print(f" workspace_id: {tapd_config['workspace_id']}")
# 读取SmartSheet配置
smartsheet_config = config.get_smartsheet_config()
print(f" [OK] SmartSheet配置读取成功")
print(f" docid: {smartsheet_config['docid']}")
# 读取Schedule配置
schedule_config = config.get_schedule_config()
print(f" [OK] Schedule配置读取成功")
print(f" sync_interval: {schedule_config['sync_interval']} 分钟")
return True
except Exception as e:
print(f" [FAIL] 配置读取失败: {e}")
return False
def test_logger():
"""测试3: 验证日志写入"""
print("\n" + "=" * 50)
print("测试3: 验证日志写入到 logs2/")
print("=" * 50)
try:
from src2.logger import get_task2_logger, TASK2_LOG_DIR
logger = get_task2_logger()
# 写入测试日志
logger.log_api_call(
api_type="test",
operation="task2/setup_test",
request_data={"test": "验证脚本测试"},
response_data={"status": "success"},
success=True
)
# 检查日志文件是否创建
log_file = logger._get_today_log_file()
if log_file.exists():
print(f" [OK] 日志写入成功")
print(f" 日志目录: {TASK2_LOG_DIR}")
print(f" 日志文件: {log_file.name}")
return True
else:
print(f" [FAIL] 日志文件未创建")
return False
except Exception as e:
print(f" [FAIL] 日志测试失败: {e}")
return False
def test_token_manager():
"""测试4: 验证Token管理器复用"""
print("\n" + "=" * 50)
print("测试4: 验证Token管理器复用")
print("=" * 50)
try:
from src.token_manager import TokenManager
# 创建TokenManager实例使用默认缓存路径
token_manager = TokenManager()
print(f" [OK] TokenManager导入成功")
print(f" 缓存文件: {token_manager.cache_file_path}")
# 尝试获取token
token = token_manager.get_token()
print(f" [OK] Token获取成功")
print(f" Token前20字符: {token[:20]}...")
return True
except ValueError as e:
print(f" [WARN] 环境变量未设置: {e}")
print(f" 这不影响框架搭建,后续运行时需要设置")
return True # 环境变量未设置不算失败
except Exception as e:
print(f" [FAIL] Token测试失败: {e}")
return False
def test_smartsheet_api():
"""测试5: 验证SmartSheetAPI复用"""
print("\n" + "=" * 50)
print("测试5: 验证SmartSheetAPI复用")
print("=" * 50)
try:
from src.smartsheet import SmartSheetAPI
from src.token_manager import TokenManager
from src2.config import Task2ConfigManager
print(f" [OK] SmartSheetAPI导入成功")
# 获取配置
config = Task2ConfigManager()
docid = config.get_smartsheet_config()['docid']
# 获取token
token_manager = TokenManager()
token = token_manager.get_token()
# 创建SmartSheetAPI实例
api = SmartSheetAPI(token, docid)
print(f" [OK] SmartSheetAPI实例创建成功")
print(f" docid: {docid}")
return True
except Exception as e:
print(f" [FAIL] SmartSheetAPI测试失败: {e}")
return False
def main():
"""运行所有测试"""
print("\n" + "=" * 50)
print("任务二第一阶段验证")
print("=" * 50)
results = {
"目录结构": test_directory_structure(),
"配置读取": test_config_read(),
"日志写入": test_logger(),
"Token管理": test_token_manager(),
"SmartSheetAPI": test_smartsheet_api()
}
# 汇总结果
print("\n" + "=" * 50)
print("验证结果汇总")
print("=" * 50)
passed = 0
failed = 0
for name, result in results.items():
status = "[OK]" if result else "[FAIL]"
print(f" {status} {name}")
if result:
passed += 1
else:
failed += 1
print(f"\n总计: {passed} 通过, {failed} 失败")
if failed == 0:
print("\n第一阶段验收通过!")
else:
print("\n请检查失败项并修复")
return failed == 0
if __name__ == "__main__":
main()