diff --git a/app/schemas.py b/app/schemas.py index 523a239..cb7fd87 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -34,6 +34,11 @@ class TaskResponse(BaseModel): current_stage: str | None = None current_stage_label: str | None = None last_progress_at: datetime | None = None + display_number: int = 0 + display_id: str = "" + created_at_label: str = "" + scale_label: str = "" + error_summary: str | None = None running_seconds: int = 0 seconds_since_last_progress: int | None = None running_duration_label: str = "刚刚" diff --git a/app/services/task_service.py b/app/services/task_service.py index add9092..dcc51b1 100644 --- a/app/services/task_service.py +++ b/app/services/task_service.py @@ -34,6 +34,10 @@ STAGE_LABELS = { "success": "已完成", "failed": "失败", } +PLATFORM_LABELS = { + "xiaohongshu": "小红书", + "douyin": "抖音", +} def has_running_task(session: Session) -> bool: @@ -150,6 +154,15 @@ def get_task(session: Session, task_id: str) -> Task | None: def hydrate_task_progress(session: Session, task: Task) -> Task: + display_number = calculate_task_display_number(session, task) + task.display_number = display_number + task.display_id = f"#{display_number}" + task.created_at_label = format_datetime_minute(task.created_at) + task.scale_label = ( + f"{PLATFORM_LABELS.get(task.platform, task.platform)} · " + f"{task.hotspot_limit}热点 × {task.item_limit_per_hotspot}内容 × {task.comment_limit_per_item}评论" + ) + task.error_summary = build_task_error_summary(task) task.hotspots_count = session.scalar(select(func.count(Hotspot.id)).where(Hotspot.task_id == task.id)) or 0 task.comments_count = session.scalar(select(func.count(Comment.id)).where(Comment.task_id == task.id)) or 0 task.reports_count = session.scalar(select(func.count(Report.id)).where(Report.task_id == task.id)) or 0 @@ -182,6 +195,29 @@ def hydrate_task_progress(session: Session, task: Task) -> Task: return task +def calculate_task_display_number(session: Session, task: Task) -> int: + task_ids = list(session.scalars(select(Task.id).order_by(Task.created_at, Task.id))) + try: + return task_ids.index(task.id) + 1 + except ValueError: + return len(task_ids) + 1 + + +def format_datetime_minute(value: datetime | None) -> str: + value = ensure_utc_datetime(value) + return value.strftime("%Y-%m-%d %H:%M") if value else "" + + +def build_task_error_summary(task: Task) -> str | None: + if task.error_message: + return task.error_message + if task.error_stage == "system" and task.error_type == "unexpected_restart": + return RESTART_ERROR_MESSAGE + if task.error_stage or task.error_type: + return "任务失败,请查看后端日志" + return None + + def format_duration_zh(total_seconds: int | None) -> str: if total_seconds is None: return "暂无记录" diff --git a/app/templates/base.html b/app/templates/base.html index 27e481a..cbb8425 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -28,8 +28,5 @@ {% endblock %} {% block content %}{% endblock %} - diff --git a/app/templates/hotspots/report.html b/app/templates/hotspots/report.html index 781e9d9..9e126eb 100644 --- a/app/templates/hotspots/report.html +++ b/app/templates/hotspots/report.html @@ -3,7 +3,7 @@ {% block breadcrumbs %} {% endblock %} diff --git a/app/templates/items/detail.html b/app/templates/items/detail.html index 91b82a7..6daa8a1 100644 --- a/app/templates/items/detail.html +++ b/app/templates/items/detail.html @@ -3,7 +3,7 @@ {% block breadcrumbs %} diff --git a/app/templates/partials/task_rows.html b/app/templates/partials/task_rows.html index 37f7804..4c28b71 100644 --- a/app/templates/partials/task_rows.html +++ b/app/templates/partials/task_rows.html @@ -4,14 +4,14 @@ {% set ai_percent = rate_percent(task.analysis_success_rate) %} - {{ task.id }} + {{ task.display_id or ("#" ~ loop.index) }} {% if task.is_demo %}
Demo 数据 {% endif %} {{ task.platform | platform_label }} - {{ task.created_at }} - 热点 {{ task.hotspot_limit }} / 内容 {{ task.item_limit_per_hotspot }} / 评论 {{ task.comment_limit_per_item }} + {{ task.created_at_label }} + {{ task.scale_label }}
已处理 {{ task.processed_items_count }} / 共 {{ task.total_items_count }} 条内容 @@ -32,11 +32,8 @@ {{ status_label }} - {% if task.error_stage or task.error_type %} -
{{ task.error_stage }} / {{ task.error_type }} - {% endif %} - {% if task.error_message %} -
{{ task.error_message }} + {% if task.error_summary %} +
{{ task.error_summary }} {% endif %} 查看 diff --git a/app/templates/tasks/detail.html b/app/templates/tasks/detail.html index 30ace04..f6ca8ce 100644 --- a/app/templates/tasks/detail.html +++ b/app/templates/tasks/detail.html @@ -1,14 +1,14 @@ {% extends "base.html" %} -{% block title %}任务 #{{ task.id }} - 热榜评论分析工具{% endblock %} +{% block title %}任务 {{ task.display_id }} - 热榜评论分析工具{% endblock %} {% block breadcrumbs %} {% endblock %} {% block content %}
-

任务 #{{ task.id }}

+

任务 {{ task.display_id }}

@@ -20,6 +20,7 @@

平台:{{ task.platform | platform_label }} {{ label }}{% if task.is_demo %}Demo 数据{% endif %}

阶段:{{ task.current_stage_label or "等待启动" }}

+
创建时间 {{ task.created_at_label }} · 完整 ID {{ task.id }}
已处理 {{ task.processed_items_count }} / 共 {{ task.total_items_count }} 条内容 @@ -72,7 +73,7 @@ AI 样本不足 {% endif %}
- {% if task.error_message %}
{{ task.error_stage }} / {{ task.error_type }}:{{ task.error_message }}
{% endif %} + {% if task.error_summary %}
{{ task.error_summary }}
{% endif %}
{% if task.status == "running" and not hotspots %}

正在抓取热点数据,请稍候...

diff --git a/docs/MVP-WorkOrders.md b/docs/MVP-WorkOrders.md index 0deda46..a313025 100644 --- a/docs/MVP-WorkOrders.md +++ b/docs/MVP-WorkOrders.md @@ -1325,6 +1325,29 @@ feat: 优化 MVP 页面产品化体验 - UI 风格方向:更偏数据仪表盘、内部工具,还是偏演示型产品页面。 - 是否需要提供简单品牌名 / Logo / 说明文案。 +完成记录: + +- 完成日期:2026-07-03 +- 相关改动: + - 任务列表和任务详情默认展示顺序号 `#1/#2/#3`,不再把完整 UUID 铺在列表和标题中。 + - 任务详情页保留“完整 ID”排查信息,方便复制给开发定位。 + - 创建时间压缩为 `YYYY-MM-DD HH:mm`,配置规模压缩为 `小红书 · 5热点 × 5内容 × 50评论`。 + - 失败状态只展示中文错误说明,不再直接展示 `system / unexpected_restart`、`recover / interrupted` 等内部枚举。 + - 移除页脚 `内部演示工具 | 仅供学习参考`。 +- 验证命令: + - `docker cp app/. hot-comments-tool-app-1:/app/app/` + - `docker cp tests/. hot-comments-tool-app-1:/app/tests/` + - `docker exec hot-comments-tool-app-1 sh -lc 'python -m pytest /app/tests/integration/test_routes.py::test_index_page_uses_short_task_numbers_compact_fields_and_friendly_error_copy /app/tests/integration/test_routes.py::test_task_detail_uses_short_number_title_and_keeps_full_uuid_for_diagnostics -q'` + - `docker exec hot-comments-tool-app-1 sh -lc 'python -m pytest /app/tests/integration/test_routes.py -q'` +- 验证结果: + - WO-16 聚焦测试:`2 passed, 1 warning` + - 路由集成测试:`23 passed, 1 warning` +- 验收结论: + - 任务列表、任务详情、报告页和内容详情页的任务引用已改为短编号。 + - 用户界面不再展示内部英文错误枚举和演示页脚文案。 +- 遗留问题: + - 本轮不做大规模视觉重设计,品牌名 / Logo 仍需用户确认后再纳入正式工单。 + ### WO-17 默认规模数据量解释与展示优化 优先级:P1 diff --git a/tests/integration/test_routes.py b/tests/integration/test_routes.py index b9c1a29..8b12a8f 100644 --- a/tests/integration/test_routes.py +++ b/tests/integration/test_routes.py @@ -1,5 +1,7 @@ from datetime import UTC, datetime, timedelta +from bs4 import BeautifulSoup + from app.models import Comment, ContentItem, Hotspot, Report, Task from tests.helpers import make_test_client from sqlalchemy.exc import OperationalError @@ -59,6 +61,58 @@ def test_index_page_lists_existing_tasks(): assert "DOMContentLoaded" in response.text +def test_index_page_uses_short_task_numbers_compact_fields_and_friendly_error_copy(): + first_id = "11111111-1111-4111-8111-111111111111" + second_id = "22222222-2222-4222-8222-222222222222" + with make_test_client() as (client, engine): + from sqlalchemy.orm import Session + + with Session(engine) as session: + session.add( + Task( + id=first_id, + platform="xiaohongshu", + status="failed", + created_at=datetime(2026, 7, 3, 10, 30, tzinfo=UTC), + hotspot_limit=5, + item_limit_per_hotspot=5, + comment_limit_per_item=50, + error_stage="system", + error_type="unexpected_restart", + error_message="系统重启,任务被中断", + ) + ) + session.add( + Task( + id=second_id, + platform="douyin", + status="success", + created_at=datetime(2026, 7, 3, 10, 35, tzinfo=UTC), + hotspot_limit=1, + item_limit_per_hotspot=1, + comment_limit_per_item=10, + ) + ) + session.commit() + + response = client.get("/") + + assert response.status_code == 200 + visible_text = BeautifulSoup(response.text, "html.parser").get_text(" ") + assert "#1" in visible_text + assert "#2" in visible_text + assert first_id not in visible_text + assert second_id not in visible_text + assert "2026-07-03 10:30" in visible_text + assert "2026-07-03 10:35" in visible_text + assert "小红书 · 5热点 × 5内容 × 50评论" in visible_text + assert "抖音 · 1热点 × 1内容 × 10评论" in visible_text + assert "系统重启,任务被中断" in visible_text + assert "system / unexpected_restart" not in visible_text + assert "unexpected_restart" not in visible_text + assert "内部演示工具 | 仅供学习参考" not in visible_text + + def test_index_page_does_not_auto_poll_without_running_tasks(): with make_test_client() as (client, engine): from sqlalchemy.orm import Session @@ -117,6 +171,37 @@ def test_running_task_detail_page_auto_polls_current_task(): assert "AI 成功率 100%" in response.text +def test_task_detail_uses_short_number_title_and_keeps_full_uuid_for_diagnostics(): + task_id = "33333333-3333-4333-8333-333333333333" + with make_test_client() as (client, engine): + from sqlalchemy.orm import Session + + with Session(engine) as session: + session.add( + Task( + id=task_id, + platform="xiaohongshu", + status="running", + created_at=datetime(2026, 7, 3, 10, 30, tzinfo=UTC), + hotspot_limit=5, + item_limit_per_hotspot=5, + comment_limit_per_item=50, + ) + ) + session.commit() + + response = client.get(f"/tasks/{task_id}") + + assert response.status_code == 200 + visible_text = BeautifulSoup(response.text, "html.parser").get_text(" ") + assert "任务 #1 - 热榜评论分析工具" in response.text + assert "任务 #1" in visible_text + assert "完整 ID" in visible_text + assert task_id in visible_text + assert "任务 #33333333-3333-4333-8333-333333333333" not in visible_text + assert "创建时间 2026-07-03 10:30" in visible_text + + def test_running_task_detail_page_keeps_polling_after_hotspots_exist(): with make_test_client() as (client, engine): from sqlalchemy.orm import Session @@ -241,7 +326,8 @@ def test_failed_task_detail_page_shows_failure_reason_without_loading_copy(): assert response.status_code == 200 assert "失败" in response.text - assert "recover / interrupted:系统重启,任务被中断" in response.text + assert "系统重启,任务被中断" in response.text + assert "recover / interrupted" not in response.text assert "正在抓取热点数据" not in response.text assert "pollTaskDetailStatus" not in response.text