task2-phase2: 链接解析与TAPD API

This commit is contained in:
zelong 2026-01-08 15:27:19 +08:00
parent bf4f346096
commit 397c14faee
5 changed files with 1165 additions and 16 deletions

View File

@ -1,4 +1,4 @@
{
"access_token": "1vVdzrLNZahtARUianAypD-WjGnSvHzU9_p0vXE2wmawFj3WigMA-eZZ1W-t0Tki2KWIs_yYrcrTAInDngJcS-_uqupCNlgEYUjL1bMeS2GGnNM1e3spBT9XdUjF8yTvP3XXtwrlY_qS9Se2S09GQEk1VLxnou1okTjmnzdaNQJbTg013-R_uUp3E-CyNFy7t1tQHN87tr9l2GzlzGt6EshjNcJq4COuCgbs5wBA298",
"fetch_time": 1767787715.7755494
"access_token": "GKRSU-6KIm-q1lKGtlImu8cSa1HpyEJLFlYq9FS-Kqfc-T9cNd25A07qTG7BhaCbZS7eii7nselgxqWoczbj3zg8f-t_jCAILpJ2uCwZSXwQBLasB0c5SedemVyA59n4rK7fwqHMOd9j1LFVtxgjfnAcQeIv6xacYE9ZLVppImHBL-ZOl2yb8NCv43j51CzEOGEryTjLH4AhNAREXKYm1mQ64cIYD3vXuLpcAG9f-cU",
"fetch_time": 1767852793.0838747
}

View File

@ -1,15 +1,583 @@
{
"records": [],
{
"api_type": "test",
"operation": "task2/setup_test",
"timestamp": "2026-01-07 20:14:18",
"success": true,
"request": {
"test": "验证脚本测试"
},
"response": {
"status": "success"
}
}
{
"records": [],
{
"api_type": "test",
"operation": "task2/setup_test",
"timestamp": "2026-01-07 20:14:18",
"success": true,
"request": {
"test": "验证脚本测试"
},
"response": {
"status": "success"
}
} ,
{
"api_type": "tapd",
"operation": "stories",
"timestamp": "2026-01-07 20:48:56",
"success": true,
"request": {
"url": "https://tapd-api.bilibili.co/tapd/stories",
"method": "GET",
"params": {
"workspace_id": "58335167",
"id": "1158335167004800796"
},
"auth_user": "g41_tapd"
},
"response": {
"status": 1,
"data": [
{
"Story": {
"id": "1158335167004800796",
"workitem_type_id": "1158335167001002451",
"app_id": "1",
"name": "12月版本性能优化",
"description": "<p><span style=\"color: #3f4a56;\">【设计文档链接】</span><br /></p><p><br /></p><p>【需求文档链接】</p>",
"workspace_id": "58335167",
"creator": "星渊",
"created": "2025-12-19 15:24:53",
"modified": "2025-12-19 18:04:08",
"status": "status_13",
"step": "",
"owner": "星渊;",
"cc": "",
"begin": null,
"due": null,
"size": null,
"priority": "4",
"developer": "星渊;",
"iteration_id": "0",
"test_focus": "",
"type": "",
"source": "",
"module": "",
"version": "",
"completed": null,
"category_id": "1158335167001017101",
"path": "1158335167004800796:",
"parent_id": "0",
"children_id": "||1158335167004800798|1158335167004800799|1158335167004800802|1158335167004800828",
"ancestor_id": "1158335167004800796",
"level": "0",
"business_value": null,
"effort": null,
"effort_completed": "0",
"exceed": "0",
"remain": "0",
"release_id": "0",
"bug_id": "0",
"templated_id": "1158335167001006075",
"created_from": null,
"feature": "",
"label": "",
"progress": "100",
"is_archived": "0",
"tech_risk": null,
"flows": null,
"custom_field_one": "星渊;",
"custom_field_two": "",
"custom_field_three": "",
"custom_field_four": "",
"custom_field_five": "",
"custom_field_six": "",
"custom_field_seven": "",
"custom_field_eight": "",
"secret_root_id": "0",
"progress_manual": "0",
"custom_field_9": "",
"custom_field_10": "",
"custom_field_11": "",
"custom_field_12": "",
"custom_field_13": "",
"custom_field_14": "",
"custom_field_15": "",
"custom_field_16": "",
"custom_field_17": "",
"custom_field_18": "",
"custom_field_19": "",
"custom_field_20": "",
"custom_field_21": "",
"custom_field_22": "",
"custom_field_23": "",
"custom_field_24": "",
"custom_field_25": "",
"custom_field_26": "",
"custom_field_27": "",
"custom_field_28": "",
"custom_field_29": "",
"custom_field_30": "",
"custom_field_31": "",
"custom_field_32": "",
"custom_field_33": "",
"custom_field_34": "",
"custom_field_35": "",
"custom_field_36": "",
"custom_field_37": "",
"custom_field_38": "",
"custom_field_39": "",
"custom_field_40": "",
"custom_field_41": "",
"custom_field_42": "",
"custom_field_43": "",
"custom_field_44": "",
"custom_field_45": "",
"custom_field_46": "",
"custom_field_47": "",
"custom_field_48": "",
"custom_field_49": "",
"custom_field_50": "",
"custom_field_51": "",
"custom_field_52": "",
"custom_field_53": "",
"custom_field_54": "",
"custom_field_55": "",
"custom_field_56": "",
"custom_field_57": "",
"custom_field_58": "",
"custom_field_59": "",
"custom_field_60": "",
"custom_field_61": "",
"custom_field_62": "",
"custom_field_63": "",
"custom_field_64": "",
"custom_field_65": "",
"custom_field_66": "",
"custom_field_67": "",
"custom_field_68": "",
"custom_field_69": "",
"custom_field_70": "",
"custom_field_71": "",
"custom_field_72": "",
"custom_field_73": "",
"custom_field_74": "",
"custom_field_75": "",
"custom_field_76": "",
"custom_field_77": "",
"custom_field_78": "",
"custom_field_79": "",
"custom_field_80": "",
"custom_field_81": "",
"custom_field_82": "",
"custom_field_83": "",
"custom_field_84": "",
"custom_field_85": "",
"custom_field_86": "",
"custom_field_87": "",
"custom_field_88": "",
"custom_field_89": "",
"custom_field_90": "",
"custom_field_91": "",
"custom_field_92": "",
"custom_field_93": "",
"custom_field_94": "",
"custom_field_95": "",
"custom_field_96": "",
"custom_field_97": "",
"custom_field_98": "",
"custom_field_99": "",
"custom_field_100": "",
"custom_field_101": "",
"custom_field_102": "",
"custom_field_103": "",
"custom_field_104": "",
"custom_field_105": "",
"custom_field_106": "",
"custom_field_107": "",
"custom_field_108": "",
"custom_field_109": "",
"custom_field_110": "",
"custom_field_111": "",
"custom_field_112": "",
"custom_field_113": "",
"custom_field_114": "",
"custom_field_115": "",
"custom_field_116": "",
"custom_field_117": "",
"custom_field_118": "",
"custom_field_119": "",
"custom_field_120": "",
"custom_field_121": "",
"custom_field_122": "",
"custom_field_123": "",
"custom_field_124": "",
"custom_field_125": "",
"custom_field_126": "",
"custom_field_127": "",
"custom_field_128": "",
"custom_field_129": "",
"custom_field_130": "",
"custom_field_131": "",
"custom_field_132": "",
"custom_field_133": "",
"custom_field_134": "",
"custom_field_135": "",
"custom_field_136": "",
"custom_field_137": "",
"custom_field_138": "",
"custom_field_139": "",
"custom_field_140": "",
"custom_field_141": "",
"custom_field_142": "",
"custom_field_143": "",
"custom_field_144": "",
"custom_field_145": "",
"custom_field_146": "",
"custom_field_147": "",
"custom_field_148": "",
"custom_field_149": "",
"custom_field_150": "",
"custom_field_151": "",
"custom_field_152": "",
"custom_field_153": "",
"custom_field_154": "",
"custom_field_155": "",
"custom_field_156": "",
"custom_field_157": "",
"custom_field_158": "",
"custom_field_159": "",
"custom_field_160": "",
"custom_field_161": "",
"custom_field_162": "",
"custom_field_163": "",
"custom_field_164": "",
"custom_field_165": "",
"custom_field_166": "",
"custom_field_167": "",
"custom_field_168": "",
"custom_field_169": "",
"custom_field_170": "",
"custom_field_171": "",
"custom_field_172": "",
"custom_field_173": "",
"custom_field_174": "",
"custom_field_175": "",
"custom_field_176": "",
"custom_field_177": "",
"custom_field_178": "",
"custom_field_179": "",
"custom_field_180": "",
"custom_field_181": "",
"custom_field_182": "",
"custom_field_183": "",
"custom_field_184": "",
"custom_field_185": "",
"custom_field_186": "",
"custom_field_187": "",
"custom_field_188": "",
"custom_field_189": "",
"custom_field_190": "",
"custom_field_191": "",
"custom_field_192": "",
"custom_field_193": "",
"custom_field_194": "",
"custom_field_195": "",
"custom_field_196": "",
"custom_field_197": "",
"custom_field_198": "",
"custom_field_199": "",
"custom_field_200": "",
"custom_plan_field_1": "1158335167001033687",
"custom_plan_field_2": "0",
"custom_plan_field_3": "0",
"custom_plan_field_4": "0",
"custom_plan_field_5": "0",
"custom_plan_field_6": "0",
"custom_plan_field_7": "0",
"custom_plan_field_8": "0",
"custom_plan_field_9": "0",
"custom_plan_field_10": "0",
"priority_label": "High"
}
}
],
"info": "success"
}
} ,
{
"api_type": "tapd",
"operation": "stories",
"timestamp": "2026-01-07 20:50:30",
"success": true,
"request": {
"url": "https://tapd-api.bilibili.co/tapd/stories",
"method": "GET",
"params": {
"workspace_id": "58335167",
"id": "1158335167004774712"
},
"auth_user": "g41_tapd"
},
"response": {
"status": 1,
"data": [
{
"Story": {
"id": "1158335167004774712",
"workitem_type_id": "1158335167001002456",
"app_id": "1",
"name": "破绽-通用特效",
"description": "<p><a href=\"https://doc.weixin.qq.com/sheet/e3_AfIAXgaSAKsCNYIUEmQnvQT6pAabt?scode=ANYAEAdoABEiTVm8UVASEAsgY6AKM&amp;tab=4x5x07\" target=\"_blank\" rel=\"noopener\">https://doc.weixin.qq.com/sheet/e3_AfIAXgaSAKsCNYIUEmQnvQT6pAabt?scode=ANYAEAdoABEiTVm8UVASEAsgY6AKM&amp;tab=4x5x07</a></p>",
"workspace_id": "58335167",
"creator": "Haoo",
"created": "2025-11-24 20:46:53",
"modified": "2025-12-08 14:26:37",
"status": "status_7",
"step": "",
"owner": "parkerluo;",
"cc": "",
"begin": null,
"due": "2025-12-31",
"size": null,
"priority": "2",
"developer": "",
"iteration_id": "0",
"test_focus": "",
"type": "",
"source": "",
"module": "",
"version": "",
"completed": null,
"category_id": "1158335167001017699",
"path": "1158335167004782406::1158335167004788137::1158335167004774712:",
"parent_id": "1158335167004788137",
"children_id": "|",
"ancestor_id": "1158335167004782406",
"level": "2",
"business_value": null,
"effort": null,
"effort_completed": "0",
"exceed": "0",
"remain": "0",
"release_id": "0",
"bug_id": "0",
"templated_id": "1158335167001006073",
"created_from": null,
"feature": "",
"label": "",
"progress": "0",
"is_archived": "0",
"tech_risk": null,
"flows": null,
"custom_field_one": "",
"custom_field_two": "",
"custom_field_three": "",
"custom_field_four": "",
"custom_field_five": "",
"custom_field_six": "",
"custom_field_seven": "",
"custom_field_eight": "",
"secret_root_id": "0",
"progress_manual": "0",
"custom_field_9": "",
"custom_field_10": "",
"custom_field_11": "",
"custom_field_12": "",
"custom_field_13": "",
"custom_field_14": "",
"custom_field_15": "",
"custom_field_16": "",
"custom_field_17": "",
"custom_field_18": "",
"custom_field_19": "",
"custom_field_20": "",
"custom_field_21": "",
"custom_field_22": "",
"custom_field_23": "",
"custom_field_24": "",
"custom_field_25": "",
"custom_field_26": "",
"custom_field_27": "",
"custom_field_28": "",
"custom_field_29": "",
"custom_field_30": "",
"custom_field_31": "",
"custom_field_32": "",
"custom_field_33": "",
"custom_field_34": "",
"custom_field_35": "",
"custom_field_36": "",
"custom_field_37": "",
"custom_field_38": "",
"custom_field_39": "",
"custom_field_40": "",
"custom_field_41": "",
"custom_field_42": "",
"custom_field_43": "",
"custom_field_44": "",
"custom_field_45": "",
"custom_field_46": "",
"custom_field_47": "",
"custom_field_48": "",
"custom_field_49": "",
"custom_field_50": "",
"custom_field_51": "",
"custom_field_52": "",
"custom_field_53": "",
"custom_field_54": "",
"custom_field_55": "",
"custom_field_56": "",
"custom_field_57": "",
"custom_field_58": "",
"custom_field_59": "",
"custom_field_60": "",
"custom_field_61": "",
"custom_field_62": "",
"custom_field_63": "",
"custom_field_64": "",
"custom_field_65": "",
"custom_field_66": "",
"custom_field_67": "",
"custom_field_68": "",
"custom_field_69": "",
"custom_field_70": "",
"custom_field_71": "",
"custom_field_72": "",
"custom_field_73": "",
"custom_field_74": "",
"custom_field_75": "",
"custom_field_76": "",
"custom_field_77": "",
"custom_field_78": "",
"custom_field_79": "",
"custom_field_80": "",
"custom_field_81": "",
"custom_field_82": "",
"custom_field_83": "",
"custom_field_84": "",
"custom_field_85": "",
"custom_field_86": "",
"custom_field_87": "",
"custom_field_88": "",
"custom_field_89": "",
"custom_field_90": "",
"custom_field_91": "",
"custom_field_92": "",
"custom_field_93": "",
"custom_field_94": "",
"custom_field_95": "",
"custom_field_96": "",
"custom_field_97": "",
"custom_field_98": "",
"custom_field_99": "",
"custom_field_100": "",
"custom_field_101": "",
"custom_field_102": "",
"custom_field_103": "",
"custom_field_104": "",
"custom_field_105": "",
"custom_field_106": "",
"custom_field_107": "",
"custom_field_108": "",
"custom_field_109": "",
"custom_field_110": "",
"custom_field_111": "",
"custom_field_112": "",
"custom_field_113": "",
"custom_field_114": "",
"custom_field_115": "",
"custom_field_116": "",
"custom_field_117": "",
"custom_field_118": "",
"custom_field_119": "",
"custom_field_120": "",
"custom_field_121": "",
"custom_field_122": "",
"custom_field_123": "",
"custom_field_124": "",
"custom_field_125": "",
"custom_field_126": "",
"custom_field_127": "",
"custom_field_128": "",
"custom_field_129": "",
"custom_field_130": "",
"custom_field_131": "",
"custom_field_132": "",
"custom_field_133": "",
"custom_field_134": "",
"custom_field_135": "",
"custom_field_136": "",
"custom_field_137": "",
"custom_field_138": "",
"custom_field_139": "",
"custom_field_140": "",
"custom_field_141": "",
"custom_field_142": "",
"custom_field_143": "",
"custom_field_144": "",
"custom_field_145": "",
"custom_field_146": "",
"custom_field_147": "",
"custom_field_148": "",
"custom_field_149": "",
"custom_field_150": "",
"custom_field_151": "",
"custom_field_152": "",
"custom_field_153": "",
"custom_field_154": "",
"custom_field_155": "",
"custom_field_156": "",
"custom_field_157": "",
"custom_field_158": "",
"custom_field_159": "",
"custom_field_160": "",
"custom_field_161": "",
"custom_field_162": "",
"custom_field_163": "",
"custom_field_164": "",
"custom_field_165": "",
"custom_field_166": "",
"custom_field_167": "",
"custom_field_168": "",
"custom_field_169": "",
"custom_field_170": "",
"custom_field_171": "",
"custom_field_172": "",
"custom_field_173": "",
"custom_field_174": "",
"custom_field_175": "",
"custom_field_176": "",
"custom_field_177": "",
"custom_field_178": "",
"custom_field_179": "",
"custom_field_180": "",
"custom_field_181": "",
"custom_field_182": "",
"custom_field_183": "",
"custom_field_184": "",
"custom_field_185": "",
"custom_field_186": "",
"custom_field_187": "",
"custom_field_188": "",
"custom_field_189": "",
"custom_field_190": "",
"custom_field_191": "",
"custom_field_192": "",
"custom_field_193": "",
"custom_field_194": "",
"custom_field_195": "",
"custom_field_196": "",
"custom_field_197": "",
"custom_field_198": "",
"custom_field_199": "",
"custom_field_200": "",
"custom_plan_field_1": "1158335167001033687",
"custom_plan_field_2": "0",
"custom_plan_field_3": "0",
"custom_plan_field_4": "0",
"custom_plan_field_5": "0",
"custom_plan_field_6": "0",
"custom_plan_field_7": "0",
"custom_plan_field_8": "0",
"custom_plan_field_9": "0",
"custom_plan_field_10": "0",
"priority_label": "Low"
}
}
],
"info": "success"
}
}
]}

115
src2/link_parser.py Normal file
View File

@ -0,0 +1,115 @@
"""
TAPD链接解析模块
负责从TAPD链接中提取需求单号
支持的链接格式
1. 列表页弹窗链接: https://www.tapd.cn/tapd_fe/{workspace_id}/story/list?...dialog_preview_id=story_{单号}
2. 详情页链接: https://www.tapd.cn/{workspace_id}/prong/stories/view/{单号}
"""
import re
from typing import Tuple, Optional
# 链接类型常量
LINK_TYPE_DIALOG = "dialog" # 列表页弹窗链接
LINK_TYPE_VIEW = "view" # 详情页链接
LINK_TYPE_UNKNOWN = "unknown"
def parse_tapd_link(url: str) -> Tuple[bool, str, str]:
"""
解析TAPD链接提取需求单号
Args:
url: TAPD链接字符串
Returns:
Tuple[bool, str, str]: (是否成功, 单号或错误信息, 链接类型)
- 成功时: (True, "1234567890", "dialog" "view")
- 失败时: (False, "错误信息", "unknown")
"""
if not url:
return (False, "链接为空", LINK_TYPE_UNKNOWN)
# 确保是字符串类型
if not isinstance(url, str):
url = str(url)
url = url.strip()
if not url:
return (False, "链接为空", LINK_TYPE_UNKNOWN)
# 格式一:列表页弹窗链接
# 匹配 dialog_preview_id=story_(\d+)
pattern_dialog = r'dialog_preview_id=story_(\d+)'
match_dialog = re.search(pattern_dialog, url)
if match_dialog:
story_id = match_dialog.group(1)
return (True, story_id, LINK_TYPE_DIALOG)
# 格式二:详情页链接
# 匹配 /stories/view/(\d+)
pattern_view = r'/stories/view/(\d+)'
match_view = re.search(pattern_view, url)
if match_view:
story_id = match_view.group(1)
return (True, story_id, LINK_TYPE_VIEW)
# 未匹配到任何格式
return (False, f"无法识别的链接格式: {url[:100]}", LINK_TYPE_UNKNOWN)
def extract_story_id(url: str) -> Optional[str]:
"""
从TAPD链接中提取需求单号简化接口
Args:
url: TAPD链接字符串
Returns:
Optional[str]: 成功返回单号失败返回None
"""
success, result, _ = parse_tapd_link(url)
return result if success else None
def is_valid_tapd_link(url: str) -> bool:
"""
检查是否为有效的TAPD链接
Args:
url: TAPD链接字符串
Returns:
bool: 是否为有效链接
"""
success, _, _ = parse_tapd_link(url)
return success
if __name__ == "__main__":
print("=== TAPD链接解析器测试 ===\n")
# 测试用例
test_cases = [
# 格式一:列表页弹窗链接
"https://www.tapd.cn/tapd_fe/58335167/story/list?dialog_preview_id=story_1158335167001044388",
# 格式二:详情页链接
"https://www.tapd.cn/58335167/prong/stories/view/1158335167001044388",
# 无效链接
"https://www.tapd.cn/58335167/bugtrace/bugs/view/123456",
"https://www.google.com",
"",
None,
]
for i, url in enumerate(test_cases, 1):
print(f"测试 {i}: {url}")
success, result, link_type = parse_tapd_link(url)
if success:
print(f" ✓ 解析成功: 单号={result}, 类型={link_type}")
else:
print(f" ✗ 解析失败: {result}")
print()

256
src2/tapd_api.py Normal file
View File

@ -0,0 +1,256 @@
"""
TAPD API调用模块任务二专用
负责与TAPD Open API交互查询需求Story信息
与任务一的区别
- 任务一创建和管理Bug单
- 任务二查询需求Story状态信息
"""
import os
import requests
from typing import Dict, Optional, Any
from requests.auth import HTTPBasicAuth
# 导入任务二专用的日志模块
from src2.logger import get_task2_logger
# TAPD状态值映射表
# 将API返回的状态代码转换为中文显示文本
STATUS_MAPPING = {
"status_5": "进行中",
"status_7": "未开始",
"status_8": "已完成",
"status_9": "待验收",
"status_10": "联调",
"status_12": "取消",
"status_13": "待评审",
}
def map_status(status_code: str) -> str:
"""
将TAPD状态代码转换为中文显示文本
Args:
status_code: TAPD API返回的状态代码 "status_5"
Returns:
str: 中文显示文本 "进行中"未知状态返回原始值
"""
if not status_code:
return "未知"
return STATUS_MAPPING.get(status_code, status_code)
class TAPDStoryApi:
"""TAPD需求API封装类任务二专用"""
# TAPD API基础URL与任务一相同
BASE_URL = "https://tapd-api.bilibili.co/tapd"
def __init__(self, workspace_id: str, test_mode: bool = False):
"""
初始化TAPD Story API
Args:
workspace_id: TAPD项目ID
test_mode: 是否启用测试模式显示API请求和响应
Raises:
ValueError: 环境变量未设置时抛出
"""
self.workspace_id = workspace_id
self.test_mode = test_mode
self.session = requests.Session()
# 从环境变量读取认证信息(与任务一共用)
self.api_user = os.environ.get('TAPD_API_USER')
self.api_password = os.environ.get('TAPD_API_PASSWORD')
if not self.api_user or not self.api_password:
raise ValueError(
"TAPD认证信息未设置。请设置环境变量:\n"
" - TAPD_API_USER\n"
" - TAPD_API_PASSWORD"
)
# 设置Basic Auth
self.auth = HTTPBasicAuth(self.api_user, self.api_password)
# 初始化任务二专用的日志记录器
self.logger = get_task2_logger()
print(f" ✓ TAPD Story API初始化完成 (workspace_id: {workspace_id})")
if test_mode:
print(f" ⚠ 测试模式已启用将显示所有API调用的详细信息")
def _make_request(self, endpoint: str, params: Optional[Dict] = None) -> Dict:
"""
发起TAPD API GET请求的通用方法
Args:
endpoint: API端点 "stories"
params: URL查询参数
Returns:
Dict: API响应数据
Raises:
RuntimeError: API调用失败时抛出
"""
url = f"{self.BASE_URL}/{endpoint}"
# 准备日志记录的请求数据
log_request_data = {
"url": url,
"method": "GET",
"params": params,
"auth_user": self.api_user
}
# 测试模式:显示请求信息
if self.test_mode:
print("\n" + "=" * 60)
print(f"【测试模式】TAPD API调用: {endpoint}")
print("=" * 60)
print(f"请求URL: {url}")
if params:
print(f"URL参数:")
for key, value in params.items():
print(f" {key}: {value}")
try:
response = self.session.get(
url,
params=params,
auth=self.auth,
timeout=30
)
# 测试模式:显示响应信息
if self.test_mode:
print(f"\n响应状态码: {response.status_code}")
try:
import json
result = response.json()
print(f"响应数据:")
print(json.dumps(result, ensure_ascii=False, indent=2))
except:
print(f"响应内容: {response.text[:500]}")
print("=" * 60)
response.raise_for_status()
result = response.json()
# 检查TAPD API返回的状态
if result.get('status') != 1:
error_msg = result.get('info', '未知错误')
self.logger.log_api_call(
api_type="tapd",
operation=endpoint,
request_data=log_request_data,
response_data=result,
success=False,
error_message=error_msg
)
raise RuntimeError(f"TAPD API调用失败: {error_msg}")
# 记录成功日志
self.logger.log_api_call(
api_type="tapd",
operation=endpoint,
request_data=log_request_data,
response_data=result,
success=True
)
return result
except requests.exceptions.Timeout:
error_msg = f"TAPD API请求超时: {endpoint}"
self.logger.log_api_call(
api_type="tapd",
operation=endpoint,
request_data=log_request_data,
response_data={},
success=False,
error_message=error_msg
)
raise RuntimeError(error_msg)
except requests.exceptions.RequestException as e:
error_msg = f"TAPD API请求失败: {e}"
self.logger.log_api_call(
api_type="tapd",
operation=endpoint,
request_data=log_request_data,
response_data={},
success=False,
error_message=error_msg
)
raise RuntimeError(error_msg)
def get_story(self, story_id: str) -> Dict:
"""
获取需求详情
Args:
story_id: 需求ID
Returns:
Dict: 需求详细信息
Raises:
RuntimeError: 获取失败时抛出
"""
params = {
'workspace_id': self.workspace_id,
'id': story_id
}
result = self._make_request("stories", params=params)
# TAPD API返回格式: {"status": 1, "data": [{"Story": {...}}]}
data = result.get('data', [])
if not isinstance(data, list) or len(data) == 0:
raise RuntimeError(f"未找到需求: {story_id}")
# 取第一个元素
first_item = data[0]
# 提取Story对象
if isinstance(first_item, dict) and 'Story' in first_item:
story_info = first_item['Story']
else:
raise RuntimeError(f"API返回数据格式异常: {first_item}")
if not story_info:
raise RuntimeError(f"未找到需求: {story_id}")
# 转换状态为中文
raw_status = story_info.get('status', '')
story_info['raw_status'] = raw_status
story_info['status'] = map_status(raw_status)
return story_info
def get_story_url(self, story_id: str) -> str:
"""
生成需求的访问URL
Args:
story_id: 需求ID
Returns:
str: 需求的访问URL
"""
return f"https://www.tapd.cn/{self.workspace_id}/prong/stories/view/{story_id}"
if __name__ == "__main__":
print("=== TAPD Story API 测试 ===\n")
print("请使用 test_phase2.py 进行完整测试")

210
src2/test_phase2.py Normal file
View File

@ -0,0 +1,210 @@
"""
任务二第二阶段验证脚本
测试链接解析和TAPD API功能
"""
import sys
from pathlib import Path
# 将项目根目录添加到 Python 路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src2.link_parser import parse_tapd_link, extract_story_id, is_valid_tapd_link
from src2.tapd_api import TAPDStoryApi, map_status, STATUS_MAPPING
def test_link_parser():
"""测试链接解析功能"""
print("=" * 60)
print("测试1: 链接解析器")
print("=" * 60)
test_cases = [
# 格式一:列表页弹窗链接
(
"https://www.tapd.cn/tapd_fe/58335167/story/list?dialog_preview_id=story_1158335167001044388",
True,
"1158335167001044388",
"dialog"
),
# 格式二:详情页链接
(
"https://www.tapd.cn/58335167/prong/stories/view/1158335167001044388",
True,
"1158335167001044388",
"view"
),
# 无效链接Bug链接
(
"https://www.tapd.cn/58335167/bugtrace/bugs/view/123456",
False,
None,
"unknown"
),
# 无效链接:其他网站
(
"https://www.google.com",
False,
None,
"unknown"
),
# 空链接
(
"",
False,
None,
"unknown"
),
]
passed = 0
failed = 0
for i, (url, expected_success, expected_id, expected_type) in enumerate(test_cases, 1):
success, result, link_type = parse_tapd_link(url)
# 检查结果
if success == expected_success and link_type == expected_type:
if success and result == expected_id:
print(f" [{i}] PASS: {url[:50]}...")
passed += 1
elif not success:
print(f" [{i}] PASS: 正确识别无效链接")
passed += 1
else:
print(f" [{i}] FAIL: 单号不匹配 (期望={expected_id}, 实际={result})")
failed += 1
else:
print(f" [{i}] FAIL: {url[:50]}...")
print(f" 期望: success={expected_success}, type={expected_type}")
print(f" 实际: success={success}, type={link_type}")
failed += 1
print(f"\n链接解析测试结果: {passed} 通过, {failed} 失败")
return failed == 0
def test_status_mapping():
"""测试状态映射功能"""
print("\n" + "=" * 60)
print("测试2: 状态映射")
print("=" * 60)
test_cases = [
("status_5", "进行中"),
("status_7", "未开始"),
("status_8", "已完成"),
("status_9", "待验收"),
("status_10", "联调"),
("status_12", "取消"),
("status_13", "待评审"),
("status_99", "status_99"), # 未知状态返回原值
("", "未知"),
(None, "未知"),
]
passed = 0
failed = 0
for status_code, expected in test_cases:
result = map_status(status_code)
if result == expected:
print(f" PASS: {status_code} -> {result}")
passed += 1
else:
print(f" FAIL: {status_code} -> {result} (期望: {expected})")
failed += 1
print(f"\n状态映射测试结果: {passed} 通过, {failed} 失败")
return failed == 0
def test_tapd_api(story_id: str = None):
"""测试TAPD API功能"""
print("\n" + "=" * 60)
print("测试3: TAPD API")
print("=" * 60)
# 从配置读取workspace_id
from src2.config import Task2ConfigManager
config = Task2ConfigManager()
tapd_config = config.get_tapd_config()
workspace_id = tapd_config['workspace_id']
print(f" workspace_id: {workspace_id}")
try:
# 初始化API
api = TAPDStoryApi(workspace_id, test_mode=True)
print(" ✓ API初始化成功")
except ValueError as e:
print(f" ✗ API初始化失败: {e}")
return False
if not story_id:
print("\n 跳过需求查询测试未提供story_id")
print(" 用法: python test_phase2.py <story_id>")
return True
# 测试获取需求详情
print(f"\n 测试获取需求: {story_id}")
try:
story = api.get_story(story_id)
print(f" ✓ 获取成功")
print(f" - ID: {story.get('id')}")
print(f" - 名称: {story.get('name')}")
print(f" - 状态: {story.get('status')}")
print(f" - 处理人: {story.get('owner')}")
print(f" - 预计开始: {story.get('begin')}")
print(f" - 预计结束: {story.get('due')}")
return True
except RuntimeError as e:
print(f" ✗ 获取失败: {e}")
return False
def main():
"""主函数"""
print("=" * 60)
print("任务二第二阶段验证")
print("=" * 60)
# 获取命令行参数
story_id = None
if len(sys.argv) > 1:
story_id = sys.argv[1]
results = []
# 测试1: 链接解析
results.append(("链接解析", test_link_parser()))
# 测试2: 状态映射
results.append(("状态映射", test_status_mapping()))
# 测试3: TAPD API
results.append(("TAPD API", test_tapd_api(story_id)))
# 汇总结果
print("\n" + "=" * 60)
print("验收结果汇总")
print("=" * 60)
all_passed = True
for name, passed in results:
status = "✓ PASS" if passed else "✗ FAIL"
print(f" {status}: {name}")
if not passed:
all_passed = False
if all_passed:
print("\n所有测试通过!第二阶段验收完成。")
else:
print("\n部分测试失败,请检查。")
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())