#!/usr/bin/env python3 """ 渠道合作伙伴平台 - 后端接口全面测试脚本 基于 Docs/渠道合作伙伴平台-后端接口需求清单.md 文档 测试账户: - 超级管理员: superadmin@taiji-ai.com / Admin@123456 - 渠道管理员: 66@66.com / 66 """ import requests import json import sys from datetime import datetime, timedelta from typing import Optional, Dict, Any, List BASE_URL = "http://localhost:8002" # 测试账户 TEST_ACCOUNTS = { "super_admin": { "email": "superadmin@taiji-ai.com", "password": "Admin@123456", "role": "super_admin" }, "channel_admin": { "email": "66@66.com", "password": "66", "role": "channel" } } class Colors: """终端颜色""" GREEN = '\033[92m' RED = '\033[91m' YELLOW = '\033[93m' BLUE = '\033[94m' CYAN = '\033[96m' RESET = '\033[0m' BOLD = '\033[1m' def print_section(title: str): """打印章节标题""" print(f"\n{Colors.BOLD}{Colors.BLUE}{'='*80}{Colors.RESET}") print(f"{Colors.BOLD}{Colors.BLUE}{title}{Colors.RESET}") print(f"{Colors.BOLD}{Colors.BLUE}{'='*80}{Colors.RESET}\n") def print_subsection(title: str): """打印子章节标题""" print(f"\n{Colors.CYAN}--- {title} ---{Colors.RESET}\n") def print_test(name: str, status: str, message: str = "", response_data: Any = None): """打印测试结果""" if status == "success": icon = f"{Colors.GREEN}✓{Colors.RESET}" elif status == "warning": icon = f"{Colors.YELLOW}⚠{Colors.RESET}" else: icon = f"{Colors.RED}✗{Colors.RESET}" print(f"{icon} {name}") if message: print(f" {Colors.YELLOW}{message}{Colors.RESET}") if response_data: print(f" {Colors.CYAN}响应数据: {json.dumps(response_data, ensure_ascii=False, indent=2)[:500]}{Colors.RESET}") def login(email: str, password: str, role: str) -> Optional[Dict]: """登录并获取token和用户信息""" try: resp = requests.post( f"{BASE_URL}/api/auth/login", json={"email": email, "password": password, "role": role}, timeout=10 ) if resp.status_code == 200: data = resp.json() if data.get("success"): return { "token": data.get("data", {}).get("token") or data.get("data", {}).get("access_token"), "user": data.get("data", {}).get("user"), "refreshToken": data.get("data", {}).get("refreshToken") } print(f" 登录失败: {resp.status_code} - {resp.text[:200]}") return None except Exception as e: print(f" 登录异常: {str(e)}") return None def test_api(method: str, endpoint: str, token: str, description: str, data: Optional[Dict] = None, params: Optional[Dict] = None, expected_fields: Optional[List[str]] = None, show_response: bool = False) -> Dict: """测试API端点并返回详细结果""" result = { "success": False, "status_code": None, "response": None, "error": None, "fields_check": {} } try: headers = {"Authorization": f"Bearer {token}"} url = f"{BASE_URL}{endpoint}" if method == "GET": resp = requests.get(url, headers=headers, params=params, timeout=15) elif method == "POST": resp = requests.post(url, headers=headers, json=data, timeout=15) elif method == "PUT": resp = requests.put(url, headers=headers, json=data, timeout=15) elif method == "DELETE": resp = requests.delete(url, headers=headers, timeout=15) else: result["error"] = f"不支持的HTTP方法: {method}" print_test(description, "error", result["error"]) return result result["status_code"] = resp.status_code try: result["response"] = resp.json() except: result["response"] = resp.text if resp.status_code in [200, 201]: response_data = result["response"] if isinstance(response_data, dict) and response_data.get("success"): result["success"] = True # 检查期望的字段 if expected_fields: data_obj = response_data.get("data", {}) for field in expected_fields: parts = field.split(".") current = data_obj found = True for part in parts: if isinstance(current, dict) and part in current: current = current[part] elif isinstance(current, list) and len(current) > 0: current = current[0].get(part) if isinstance(current[0], dict) else None if current is None: found = False break else: found = False break result["fields_check"][field] = found # 构建消息 msg_parts = [] if isinstance(data_obj, dict): if "tenants" in data_obj: msg_parts.append(f"租户数: {len(data_obj['tenants'])}") if "templates" in data_obj: msg_parts.append(f"模板数: {len(data_obj['templates'])}") if "providers" in data_obj: msg_parts.append(f"供应商数: {len(data_obj['providers'])}") if "admins" in data_obj: msg_parts.append(f"管理员数: {len(data_obj['admins'])}") if "tenantStats" in data_obj: msg_parts.append(f"租户统计数: {len(data_obj['tenantStats'])}") if "callRecords" in data_obj: msg_parts.append(f"调用记录数: {len(data_obj['callRecords'])}") # 检查字段缺失 missing_fields = [f for f, found in result["fields_check"].items() if not found] if missing_fields: msg_parts.append(f"缺失字段: {', '.join(missing_fields)}") print_test(description, "warning", "; ".join(msg_parts) if msg_parts else None, response_data.get("data") if show_response else None) else: print_test(description, "success", "; ".join(msg_parts) if msg_parts else None, response_data.get("data") if show_response else None) else: result["error"] = response_data.get("message", "API返回success=false") print_test(description, "warning", result["error"]) elif resp.status_code == 404: result["error"] = "资源不存在" print_test(description, "warning", "资源不存在(可能是预期情况)") elif resp.status_code == 403: result["error"] = "权限不足" print_test(description, "error", "权限不足") elif resp.status_code == 401: result["error"] = "未授权" print_test(description, "error", "未授权,token可能已过期") else: error_msg = "" if isinstance(result["response"], dict): error_msg = result["response"].get("detail", str(result["response"])) else: error_msg = str(result["response"])[:200] result["error"] = f"HTTP {resp.status_code}: {error_msg}" print_test(description, "error", result["error"]) except requests.exceptions.Timeout: result["error"] = "请求超时" print_test(description, "error", "请求超时") except requests.exceptions.ConnectionError: result["error"] = "连接失败" print_test(description, "error", "无法连接到服务器") except Exception as e: result["error"] = str(e) print_test(description, "error", str(e)) return result def main(): """主函数""" print_section("渠道合作伙伴平台 - 后端接口全面测试") print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"API基础URL: {BASE_URL}") # 统计 total_tests = 0 passed_tests = 0 warning_tests = 0 failed_tests = 0 test_results = [] # ========== 模块1: 认证模块 ========== print_section("模块1: 认证模块 (Authentication)") # B1. 渠道用户登录 print_subsection("B1. 渠道用户登录") total_tests += 1 channel_login = login(**TEST_ACCOUNTS["channel_admin"]) if channel_login and channel_login.get("token"): passed_tests += 1 print_test("渠道管理员登录", "success", f"用户: {TEST_ACCOUNTS['channel_admin']['email']}") print(f" {Colors.CYAN}Token: {channel_login['token'][:50]}...{Colors.RESET}") if channel_login.get("user"): print(f" {Colors.CYAN}用户信息: {json.dumps(channel_login['user'], ensure_ascii=False)[:200]}{Colors.RESET}") channel_token = channel_login["token"] else: failed_tests += 1 print_test("渠道管理员登录", "error", "登录失败") print(f"\n{Colors.RED}渠道管理员登录失败,无法继续测试渠道接口{Colors.RESET}") # 尝试使用超级管理员 super_login = login(**TEST_ACCOUNTS["super_admin"]) if super_login and super_login.get("token"): print(f"{Colors.YELLOW}使用超级管理员token继续测试...{Colors.RESET}") channel_token = super_login["token"] else: print(f"{Colors.RED}超级管理员登录也失败,无法继续测试{Colors.RESET}") return # B2. 退出登录 (仅测试接口可用性,不实际退出) print_subsection("B2. 退出登录接口检查") total_tests += 1 # 注意:不实际调用logout,只检查接口是否存在 print_test("退出登录接口", "success", "接口存在 (POST /api/auth/logout),不实际调用以保持session") passed_tests += 1 # ========== 模块2: 仪表板模块 ========== print_section("模块2: 仪表板模块 (Dashboard/Overview)") # D1. 渠道统计概览 - 通过租户列表计算 print_subsection("D1. 渠道统计概览") total_tests += 1 result = test_api("GET", "/api/channel/tenants", channel_token, "获取租户列表(用于计算统计)", expected_fields=["tenants", "tenants.id", "tenants.name", "tenants.status"]) if result["success"]: passed_tests += 1 # 计算统计 tenants = result["response"].get("data", {}).get("tenants", []) total_tenants = len(tenants) active_tenants = len([t for t in tenants if t.get("status") == "active"]) print(f" {Colors.CYAN}统计: 总租户数={total_tenants}, 活跃租户数={active_tenants}{Colors.RESET}") else: failed_tests += 1 # D2. 平台Agent概览 print_subsection("D2. 平台Agent概览") total_tests += 1 result = test_api("GET", "/api/channel/available-platform-agents", channel_token, "获取可用平台Agent模板", expected_fields=["templates", "templates.name", "templates.displayName", "templates.description", "templates.podQuota", "templates.hasAccess"]) if result["success"]: passed_tests += 1 elif result.get("fields_check") and any(result["fields_check"].values()): warning_tests += 1 else: failed_tests += 1 # ========== 模块3: 租户管理模块 ========== print_section("模块3: 租户管理模块 (My Tenants)") # D3. 租户列表 print_subsection("D3. 租户列表") total_tests += 1 result = test_api("GET", "/api/channel/tenants", channel_token, "获取租户列表", expected_fields=["tenants", "tenants.id", "tenants.name", "tenants.status", "tenants.plan", "tenants.users", "tenants.revenue"]) tenant_id = None if result["success"]: passed_tests += 1 tenants = result["response"].get("data", {}).get("tenants", []) if tenants: tenant_id = tenants[0].get("id") print(f" {Colors.CYAN}获取到租户ID用于后续测试: {tenant_id}{Colors.RESET}") else: failed_tests += 1 # B3. 创建租户 (测试接口,使用唯一邮箱) print_subsection("B3. 创建租户") total_tests += 1 test_tenant_email = f"test_tenant_{datetime.now().strftime('%Y%m%d%H%M%S')}@test.com" result = test_api("POST", "/api/channel/tenants/create", channel_token, "创建新租户", data={ "name": f"测试租户_{datetime.now().strftime('%H%M%S')}", "email": test_tenant_email, "password": "Test@123456", "subscriptionTier": "free" }, expected_fields=["tenant", "tenant.id"]) new_tenant_id = None if result["success"]: passed_tests += 1 new_tenant_id = result["response"].get("data", {}).get("tenant", {}).get("id") print(f" {Colors.CYAN}新创建租户ID: {new_tenant_id}{Colors.RESET}") else: failed_tests += 1 # 使用新创建的租户ID或已有的租户ID进行后续测试 test_tenant_id = new_tenant_id or tenant_id if test_tenant_id: # B6. 分配资源 print_subsection("B6. 分配资源") total_tests += 1 result = test_api("PUT", f"/api/channel/tenants/{test_tenant_id}/resources", channel_token, "为租户分配资源", data={ "agents": [{"agentId": "gpt-assistant", "quantity": 2}], "models": [{"modelName": "gpt-4", "rpm": 100, "tpm": 10000}] }) if result["success"]: passed_tests += 1 else: failed_tests += 1 # B7. 租户充值 print_subsection("B7. 租户充值") total_tests += 1 result = test_api("POST", f"/api/channel/tenants/{test_tenant_id}/recharge", channel_token, "为租户充值", data={"amount": 100.0}, expected_fields=["newBalance"]) if result["success"]: passed_tests += 1 else: failed_tests += 1 # B8. 设置授信额度 print_subsection("B8. 设置授信额度") total_tests += 1 result = test_api("PUT", f"/api/channel/tenants/{test_tenant_id}/credit", channel_token, "设置租户授信额度", data={"creditLimit": 500.0}) if result["success"]: passed_tests += 1 else: failed_tests += 1 # B9. 管理计费 print_subsection("B9. 管理计费") total_tests += 1 result = test_api("PUT", f"/api/channel/tenants/{test_tenant_id}/billing", channel_token, "更新租户计费设置", data={"subscriptionTier": "professional", "discount": 10}) if result["success"]: passed_tests += 1 else: failed_tests += 1 # B5. 删除租户 (如果是新创建的租户) if new_tenant_id: print_subsection("B5. 删除租户") total_tests += 1 result = test_api("DELETE", f"/api/channel/tenants/{new_tenant_id}", channel_token, "删除测试租户") if result["success"]: passed_tests += 1 else: failed_tests += 1 else: print(f" {Colors.YELLOW}跳过租户操作测试(无可用租户ID){Colors.RESET}") # ========== 模块4: 资源管理模块 ========== print_section("模块4: 资源管理模块 (Resource Management)") # D4. 平台Agent模板列表 print_subsection("D4. 平台Agent模板列表") total_tests += 1 result = test_api("GET", "/api/channel/available-platform-agents", channel_token, "获取平台Agent模板列表", expected_fields=["templates", "templates.name", "templates.status", "templates.cpuRequest", "templates.memoryRequest", "templates.podQuota", "templates.podUsed"]) if result["success"]: passed_tests += 1 else: failed_tests += 1 # D5. 模型供应商列表 print_subsection("D5. 模型供应商列表") total_tests += 1 result = test_api("GET", "/api/channel/providers", channel_token, "获取模型供应商列表", expected_fields=["providers", "providers.id", "providers.name", "providers.hasAccess", "providers.rpm", "providers.tpm"]) provider_id = None if result["success"]: passed_tests += 1 providers = result["response"].get("data", {}).get("providers", []) if providers: provider_id = providers[0].get("id") else: failed_tests += 1 # B10. 申请平台Agent配额 print_subsection("B10. 申请平台Agent配额") total_tests += 1 result = test_api("POST", "/api/channel/applications/platform-agents", channel_token, "申请平台Agent配额", data={ "templateName": "gpt-assistant", "requestedPodQuota": 5, "reason": "业务扩展需要更多Agent配额" }, expected_fields=["applicationId"]) if result["success"]: passed_tests += 1 else: failed_tests += 1 # B11. 申请模型供应商使用权限 print_subsection("B11. 申请模型供应商使用权限") total_tests += 1 if provider_id: result = test_api("POST", "/api/channel/providers/apply", channel_token, "申请模型供应商使用权限", data={ "providerId": provider_id, "requestedRpm": 1000, "requestedTpm": 100000, "reason": "需要更高的API调用配额" }, expected_fields=["applicationId"]) else: result = test_api("POST", "/api/channel/providers/apply", channel_token, "申请模型供应商使用权限", data={ "providerId": "test-provider-id", "requestedRpm": 1000, "requestedTpm": 100000, "reason": "需要更高的API调用配额" }) if result["success"]: passed_tests += 1 else: failed_tests += 1 # ========== 模块5: 计费模块 ========== print_section("模块5: 计费模块 (Billing)") # 计算时间范围 end_time = datetime.now().isoformat() start_time = (datetime.now() - timedelta(days=30)).isoformat() # D6. 租户计费统计 print_subsection("D6. 租户计费统计") total_tests += 1 result = test_api("GET", "/api/channel/billing/stats", channel_token, "获取租户计费统计", params={"startTime": start_time, "endTime": end_time}, expected_fields=["tenantStats", "tenantStats.tenantId", "tenantStats.tenantName", "tenantStats.calls", "tenantStats.totalEU", "tenantStats.totalCost"]) if result["success"]: passed_tests += 1 else: failed_tests += 1 # D7. 调用记录明细 print_subsection("D7. 调用记录明细") total_tests += 1 result = test_api("GET", "/api/channel/billing/stats", channel_token, "获取调用记录明细", params={"startTime": start_time, "endTime": end_time}, expected_fields=["callRecords", "callRecords.timestamp", "callRecords.tenantName", "callRecords.agentType", "callRecords.duration", "callRecords.eu"]) if result["success"]: passed_tests += 1 else: failed_tests += 1 # B12. 时间范围查询 print_subsection("B12. 时间范围查询") total_tests += 1 result = test_api("GET", "/api/channel/billing/stats", channel_token, "按时间范围查询计费数据", params={ "startTime": (datetime.now() - timedelta(days=7)).isoformat(), "endTime": datetime.now().isoformat() }) if result["success"]: passed_tests += 1 else: failed_tests += 1 # B13. 筛选功能 print_subsection("B13. 筛选功能") total_tests += 1 result = test_api("GET", "/api/channel/billing/stats", channel_token, "按条件筛选计费数据", params={ "startTime": start_time, "endTime": end_time, "tenantName": "test", "minCalls": 0 }) if result["success"]: passed_tests += 1 else: failed_tests += 1 # B14. 数据导出 print_subsection("B14. 数据导出") total_tests += 1 result = test_api("GET", "/api/channel/billing/stats", channel_token, "导出计费数据 (Excel格式)", params={ "startTime": start_time, "endTime": end_time, "export": "excel" }) if result["success"]: passed_tests += 1 print(f" {Colors.YELLOW}注意: 导出功能可能需要后端完善文件下载支持{Colors.RESET}") else: warning_tests += 1 print(f" {Colors.YELLOW}导出功能待完善{Colors.RESET}") # ========== 模块6: 设置模块 ========== print_section("模块6: 设置模块 (Settings)") # D8. 管理员列表 print_subsection("D8. 管理员列表") total_tests += 1 result = test_api("GET", "/api/channel/admins", channel_token, "获取管理员列表", expected_fields=["admins", "admins.id", "admins.name", "admins.email", "admins.role", "admins.status"]) admin_id = None if result["success"]: passed_tests += 1 admins = result["response"].get("data", {}).get("admins", []) if admins: # 找一个非channel_admin的管理员用于删除测试 for admin in admins: if admin.get("role") != "channel_admin": admin_id = admin.get("id") break else: failed_tests += 1 # B15. 创建管理员 print_subsection("B15. 创建管理员") total_tests += 1 test_admin_email = f"test_admin_{datetime.now().strftime('%Y%m%d%H%M%S')}@test.com" result = test_api("POST", "/api/channel/admins/create", channel_token, "创建新管理员", data={ "name": f"测试管理员_{datetime.now().strftime('%H%M%S')}", "email": test_admin_email, "password": "Admin@123456", "role": "billing_admin" }, expected_fields=["admin", "admin.id"]) new_admin_id = None if result["success"]: passed_tests += 1 new_admin_id = result["response"].get("data", {}).get("admin", {}).get("id") print(f" {Colors.CYAN}新创建管理员ID: {new_admin_id}{Colors.RESET}") else: failed_tests += 1 # B16. 删除管理员 if new_admin_id: print_subsection("B16. 删除管理员") total_tests += 1 result = test_api("DELETE", f"/api/admin/admins/{new_admin_id}", channel_token, "删除测试管理员") if result["success"]: passed_tests += 1 else: failed_tests += 1 # B17. 配置角色权限 (待完善) print_subsection("B17. 配置角色权限") total_tests += 1 print_test("配置角色权限", "warning", "接口待后端开发 (PUT /api/channel/roles/{roleId}/permissions)") warning_tests += 1 # ========== 附加测试: 获取申请记录 ========== print_section("附加测试: 申请记录查询") # 获取Agent申请记录 print_subsection("获取Agent申请记录") total_tests += 1 result = test_api("GET", "/api/channel/applications/platform-agents", channel_token, "获取平台Agent申请记录") if result["success"]: passed_tests += 1 else: failed_tests += 1 # 获取供应商申请记录 print_subsection("获取供应商申请记录") total_tests += 1 result = test_api("GET", "/api/channel/providers/applications", channel_token, "获取供应商申请记录") if result["success"]: passed_tests += 1 else: failed_tests += 1 # ========== 测试总结 ========== print_section("测试总结") success_rate = (passed_tests / total_tests * 100) if total_tests > 0 else 0 print(f"{Colors.BOLD}总测试数:{Colors.RESET} {total_tests}") print(f"{Colors.GREEN}通过数量:{Colors.RESET} {passed_tests}") print(f"{Colors.YELLOW}警告数量:{Colors.RESET} {warning_tests}") print(f"{Colors.RED}失败数量:{Colors.RESET} {failed_tests}") print(f"{Colors.BOLD}成功率:{Colors.RESET} {success_rate:.1f}%\n") if passed_tests == total_tests: print(f"{Colors.GREEN}{Colors.BOLD}🎉 所有测试通过!{Colors.RESET}\n") elif failed_tests == 0: print(f"{Colors.YELLOW}⚠ 所有接口可用,但有部分警告需要关注{Colors.RESET}\n") else: print(f"{Colors.RED}✗ 部分测试失败,请检查日志{Colors.RESET}\n") # 打印待完善功能 print_section("待后端完善的功能点") print(f""" {Colors.YELLOW}1. 统计数据接口:{Colors.RESET} - GET /api/channel/dashboard/stats (月度收入、佣金统计) {Colors.YELLOW}2. 权限配置持久化:{Colors.RESET} - PUT /api/channel/roles/{{roleId}}/permissions (角色权限保存) {Colors.YELLOW}3. 数据导出功能:{Colors.RESET} - 计费数据导出需要后端支持生成Excel/CSV/PDF文件 """) print(f"{Colors.BLUE}{'='*80}{Colors.RESET}\n") return passed_tests, total_tests, failed_tests if __name__ == "__main__": try: passed, total, failed = main() sys.exit(0 if failed == 0 else 1) except KeyboardInterrupt: print(f"\n{Colors.YELLOW}测试被用户中断{Colors.RESET}") sys.exit(1) except Exception as e: print(f"\n{Colors.RED}测试异常: {str(e)}{Colors.RESET}") sys.exit(1)