From 397c14faeec2d26cd1dad5a9240ff077ffd69669 Mon Sep 17 00:00:00 2001 From: zelong <2895587166@qq.com> Date: Thu, 8 Jan 2026 15:27:19 +0800 Subject: [PATCH] =?UTF-8?q?task2-phase2:=20=E9=93=BE=E6=8E=A5=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E4=B8=8ETAPD=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/token_cache.json | 4 +- logs2/api_log_2026-01-07.json | 596 +++++++++++++++++++++++++++++++++- src2/link_parser.py | 115 +++++++ src2/tapd_api.py | 256 +++++++++++++++ src2/test_phase2.py | 210 ++++++++++++ 5 files changed, 1165 insertions(+), 16 deletions(-) create mode 100644 src2/link_parser.py create mode 100644 src2/tapd_api.py create mode 100644 src2/test_phase2.py diff --git a/config/token_cache.json b/config/token_cache.json index 0e70d50..b7e7878 100644 --- a/config/token_cache.json +++ b/config/token_cache.json @@ -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 } \ No newline at end of file diff --git a/logs2/api_log_2026-01-07.json b/logs2/api_log_2026-01-07.json index 95fed23..edac0c0 100644 --- a/logs2/api_log_2026-01-07.json +++ b/logs2/api_log_2026-01-07.json @@ -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": "

【设计文档链接】


【需求文档链接】

", + "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": "

https://doc.weixin.qq.com/sheet/e3_AfIAXgaSAKsCNYIUEmQnvQT6pAabt?scode=ANYAEAdoABEiTVm8UVASEAsgY6AKM&tab=4x5x07

", + "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" + } +} ]} \ No newline at end of file diff --git a/src2/link_parser.py b/src2/link_parser.py new file mode 100644 index 0000000..e220298 --- /dev/null +++ b/src2/link_parser.py @@ -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() diff --git a/src2/tapd_api.py b/src2/tapd_api.py new file mode 100644 index 0000000..eb3de49 --- /dev/null +++ b/src2/tapd_api.py @@ -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 进行完整测试") + diff --git a/src2/test_phase2.py b/src2/test_phase2.py new file mode 100644 index 0000000..707e953 --- /dev/null +++ b/src2/test_phase2.py @@ -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 ") + 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())