"""四阶段升级全面测试""" import uvicorn, time, threading, requests, json, os, sys from pathlib import Path os.environ.setdefault("USE_POSTGRES", "false") backend_dir = str(Path(__file__).parent.parent / "platform" / "backend") sys.path.insert(0, backend_dir) os.chdir(backend_dir) results = {"pass": 0, "fail": 0} def test(name, ok, detail=""): status = chr(10003) if ok else chr(10007) results["pass" if ok else "fail"] += 1 print(f" {status} {name}") if detail: print(f" {detail}") # ===== 0. 编译检查 ===== print("\n====== 阶段 0: 编译检查 ======") root = Path(__file__).parent.parent files = [ "platform/backend/app/models.py", "platform/backend/app/api/auth.py", "platform/backend/app/api/topics.py", "platform/backend/app/api/articles.py", "platform/backend/app/api/calendar.py", "platform/backend/app/api/metrics.py", "platform/backend/app/api/publishing.py", "platform/backend/app/api/system.py", "platform/backend/app/api/admin.py", "platform/backend/app/api/tasks.py", "platform/backend/app/schemas.py", "platform/backend/app/database.py", "platform/backend/app/initial_data.py", "platform/backend/app/main.py", "platform/backend/app/core/nvidia_client.py", "platform/backend/app/core/scheduler.py", "scripts/collector.py", "scripts/sync_metrics.py", "scripts/creator.py", ] for f in files: try: src = (root / f).read_text() compile(src, f, "exec") test(f"编译 {Path(f).name}", True) except SyntaxError as e: test(f"编译 {Path(f).name}", False, str(e)) # ===== 1. 启动服务 ===== print("\n====== 阶段 0: API 服务启动 ======") def start_server(): uvicorn.run("app.main:app", host="127.0.0.1", port=18504, log_level="error") t = threading.Thread(target=start_server, daemon=True) t.start() time.sleep(4) base = "http://127.0.0.1:18504" try: r = requests.get(f"{base}/api/system/status", timeout=5) test("服务可访问", r.status_code == 200) except Exception as e: test("服务可访问", False, str(e)) exit(1) r = requests.post(f"{base}/api/auth/login", json={"username": "admin", "password": "admin123"}, timeout=5) token = r.json().get("token", "") if r.status_code == 200 else "" test("管理员登录", r.status_code == 200 and bool(token)) if not token: exit(1) auth = {"Authorization": f"Bearer {token}"} # Decode JWT to check org_id import jwt payload = jwt.decode(token, options={"verify_signature": False}) test("JWT含org_id", payload.get("org_id") == "default", f"org_id={payload.get('org_id')}") test("JWT含username", payload.get("username") == "admin") test("JWT含role", payload.get("role") == "admin") # ===== Phase 1.1: 配图集成 ===== print("\n====== Phase 1.1: 配图集成 ======") r = requests.get(f"{base}/api/topics", headers=auth, timeout=5) test("选题列表可访问", r.status_code == 200) topics = r.json() test("有选题数据", len(topics) > 0, f"共{len(topics)}个") if topics: tid = topics[0]["id"] r = requests.get(f"{base}/api/articles/{tid}/images", headers=auth, timeout=5) test("配图API可访问", r.status_code == 200) test("配图返回dict", isinstance(r.json(), dict)) r = requests.get(f"{base}/api/articles/{tid}/preview", headers=auth, timeout=5) test("预览API可访问", r.status_code == 200 or r.status_code == 404) # ===== Phase 1.3: 前端三态 ===== print("\n====== Phase 1.3: 前端三态UX ======") pages = ["index.html", "topics.html", "metrics.html", "calendar.html", "articles.html", "tasks.html", "admin.html", "users.html"] for page in pages: p = root / "platform" / "frontend" / page html = p.read_text(encoding="utf-8") has_loading = "loading" in html.lower() has_empty = "empty" in html.lower() or "暂无" in html has_error = "error" in html.lower() or "失败" in html or "catch" in html ok = has_loading and has_empty and has_error test(f"{page}: 三态 ({'✓' if ok else '✗'})", ok, f"loading={has_loading} empty={has_empty} error={has_error}") # ===== Phase 2.1: 多平台发布 ===== print("\n====== Phase 2.1: 多平台发布 ======") r = requests.post(f"{base}/api/topics", headers=auth, json={ "title": f"测试选题_{int(time.time())}", "tags": ["test"] }, timeout=5) test("创建测试选题", r.status_code == 200) new_tid = r.json().get("id", "") if new_tid: r = requests.put(f"{base}/api/topics/{new_tid}", headers=auth, json={"status": "ready"}, timeout=5) test("设置待发布状态", r.status_code == 200) r = requests.post(f"{base}/api/publishing/create", headers=auth, json={ "topic_id": new_tid, "platforms": ["zhihu", "wechat"] }, timeout=5) test("多平台发布API", r.status_code == 200) data = r.json() test("发布返回ok", data.get("ok") == True) test("发布结果含平台列表", len(data.get("results", [])) > 0) if data.get("results"): test("第一平台发布成功", data["results"][0]["status"] == "success") # ===== Phase 2.2: 日历关联 ===== print("\n====== Phase 2.2: 日历选题关联 ======") r = requests.get(f"{base}/api/calendar", headers=auth, params={"year": 2026, "month": 5}, timeout=5) test("日历API可访问", r.status_code == 200) entries = r.json() if entries: e = entries[0] test("含topic_status字段", "topic_status" in e, f"值={e.get('topic_status')}") test("含platform_icon字段", "platform_icon" in e, f"值={e.get('platform_icon')}") # ===== Phase 2.3: SVG图标 + H5 ===== print("\n====== Phase 2.3: SVG图标 + H5响应式 ======") icon_js = (root / "platform" / "frontend" / "icon-components.js").read_text(encoding="utf-8") import re icon_names = set(re.findall(r'Icon[A-Z][a-zA-Z]+', icon_js)) test("icon-components.js注册图标", len(icon_names) >= 20, f"共{len(icon_names)}个图标: {sorted(icon_names)[:10]}...") test("含IconCheck组件", "IconCheck" in icon_js) test("含IconClose组件", "IconClose" in icon_js) test("含IconSetting组件", "IconSetting" in icon_js) test("含IconDashboard组件", "IconDashboard" in icon_js) mobile_pages = ["topics.html", "metrics.html", "admin.html", "users.html", "articles.html"] for page in mobile_pages: html = (root / "platform" / "frontend" / page).read_text() has_card_list = "card-list" in html or "card_list" in html has_media = "@media" in html test(f"{page}: H5卡片+响应式", has_card_list and has_media) # ===== Phase 3.1: 指标同步 ===== print("\n====== Phase 3.1: 指标同步 ======") sync_py = (root / "scripts" / "sync_metrics.py").read_text() test("sync_metrics.py 存在", True) test("含估计算法", "compliance_score" in sync_py or "estimate" in sync_py or "platform_multiplier" in sync_py) test("含upsert逻辑", "upsert" in sync_py.lower() or "on_conflict" in sync_py or "existing" in sync_py) scheduler_py = (root / "platform" / "backend" / "app" / "core" / "scheduler.py").read_text() test("调度含scheduled_metrics_sync", "scheduled_metrics_sync" in scheduler_py) r = requests.get(f"{base}/api/metrics/entries", headers=auth, timeout=5) test("指标API可访问", r.status_code == 200) test("指标列表是list", isinstance(r.json(), list)) # ===== Phase 3.2: Chart.js 看板 ===== print("\n====== Phase 3.2: Chart.js 数据看板 ======") chart_js = root / "platform" / "frontend" / "chart.umd.min.js" test("chart.umd.min.js 存在", chart_js.exists()) if chart_js.exists(): test("chart文件非空", chart_js.stat().st_size > 10000, f"大小={chart_js.stat().st_size}B") r = requests.get(f"{base}/api/metrics/dashboard", headers=auth, timeout=5) test("数据看板API", r.status_code == 200) db_data = r.json() test("含total_topics", "total_topics" in db_data, f"值={db_data.get('total_topics')}") test("含topics_by_status", "topics_by_status" in db_data) test("含total_views", "total_views" in db_data) test("含total_likes", "total_likes" in db_data) test("含avg_engagement_rate", "avg_engagement_rate" in db_data) test("含top_topics", "top_topics" in db_data) test("含recent_metrics", "recent_metrics" in db_data) r = requests.get(f"{base}/api/metrics/trend", headers=auth, timeout=5) test("趋势API", r.status_code == 200, f"返回{len(r.json())}条") r = requests.get(f"{base}/api/metrics/by-platform", headers=auth, timeout=5) test("平台对比API", r.status_code == 200, f"返回{len(r.json())}个平台") metrics_html = (root / "platform" / "frontend" / "metrics.html").read_text() test("metrics.html含Chart.js引用", "chart.umd.min.js" in metrics_html or "chart.js" in metrics_html.lower()) test("含折线图初始化", "new Chart" in metrics_html or "Chart(" in metrics_html) test("含环形图初始化", "doughnut" in metrics_html or "doughnut" in metrics_html.lower()) test("含柱状图初始化", "bar" in metrics_html) # ===== Phase 4: 多租户 ===== print("\n====== Phase 4: 多租户隔离 ======") # 4a. 模型有org_id models_py = (root / "platform" / "backend" / "app" / "models.py").read_text() test("User模型含org_id", "org_id" in models_py) test("Topic模型含org_id", "org_id" in models_py) # 4b. JWT含org_id (已在前面验证) # 4c. 选题创建继承org if new_tid: r = requests.get(f"{base}/api/topics/{new_tid}", headers=auth, timeout=5) test("新选题含org_id", r.json().get("org_id") == "default" or "org_id" in r.json()) # 4d. 组织管理CRUD r = requests.get(f"{base}/api/admin/orgs", headers=auth, timeout=5) test("组织列表API", r.status_code == 200) orgs = r.json() test("默认组织存在", any(o.get("org_id") == "default" for o in orgs)) org_id = f"test_org_{int(time.time())}" r = requests.post(f"{base}/api/admin/orgs", headers=auth, json={ "org_id": org_id, "name": "测试组织", "description": "自动化测试" }, timeout=5) test("创建组织", r.status_code == 200, f"org_id={org_id}") r = requests.put(f"{base}/api/admin/orgs/{org_id}", headers=auth, json={"name": "测试组织(已更新)"}, timeout=5) test("更新组织", r.status_code == 200) r = requests.delete(f"{base}/api/admin/orgs/{org_id}", headers=auth, timeout=5) test("删除组织", r.status_code == 200) # 4e. users.html org_id列 users_html = (root / "platform" / "frontend" / "users.html").read_text() test("users.html含组织列", "org_id" in users_html) test("users.html表含组织头", "组织" in users_html) # 4f. admin.html组织管理标签 admin_html = (root / "platform" / "frontend" / "admin.html").read_text() test("admin.html含组织标签", "组织管理" in admin_html) test("admin.html含组织CRUD函数", "loadOrgs" in admin_html) # 4g. 数据库迁移 db_py = (root / "platform" / "backend" / "app" / "database.py").read_text() test("database.py含org_id迁移", "users.*org_id" in db_py.replace(" ", "").replace("\n", "") or ("org_id" in db_py and "ALTER TABLE" in db_py)) # ===== 统一CSS检查 ===== print("\n====== 统一CSS检查 ======") theme_css = (root / "platform" / "frontend" / "theme-modern.css").read_text() test("theme-modern.css存在", True) test("含响应式@media 768px", "@media (max-width: 768px)" in theme_css) test("含统一卡片类.card", ".card" in theme_css) test("含统一布局.main-content", ".main-content" in theme_css) test("含统一.page-header", ".page-header" in theme_css) test("含空状态.empty-state", ".empty-state" in theme_css) test("含加载状态.loading-state", ".loading-state" in theme_css) mobile_patterns = ["mobile-card", "padding-bottom: 80px", "el-dialog", "el-button"] present = sum(1 for p in mobile_patterns if p in theme_css) test(f"H5触控优化 ({present}/{len(mobile_patterns)})", present >= 2, f"含: mobile-card/padding-bottom/el-dialog/el-button") # ===== 总结 ===== total = results["pass"] + results["fail"] print(f"\n{'='*50}") print(f"阶段测试完成: {results['pass']}/{total} 通过, {results['fail']} 失败") if results["fail"] > 0: exit(1)