From 0b186fa9f81eac35fdfb8d604fa5fb075cd5df83 Mon Sep 17 00:00:00 2001 From: meijiali <你的邮箱@xxx.com> Date: Fri, 3 Jul 2026 19:14:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=20CSV=20=E5=92=8C=20?= =?UTF-8?q?Markdown=20=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 47 ------------- app/services/export_service.py | 104 ----------------------------- app/static/app.js | 43 ------------ app/templates/hotspots/report.html | 14 ---- app/templates/items/detail.html | 14 ---- docs/UserGuide.md | 15 ++--- tests/integration/test_routes.py | 53 ++++++--------- tests/unit/test_docs.py | 6 +- tests/unit/test_export.py | 24 ------- 9 files changed, 31 insertions(+), 289 deletions(-) delete mode 100644 app/services/export_service.py delete mode 100644 tests/unit/test_export.py diff --git a/app/main.py b/app/main.py index b78d9d1..818663b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,9 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager import logging -from urllib.parse import quote from fastapi import Depends, FastAPI, HTTPException, Request, status from fastapi.responses import HTMLResponse, JSONResponse -from fastapi.responses import Response from fastapi.staticfiles import StaticFiles from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy import select @@ -14,7 +12,6 @@ from sqlalchemy.orm import Session from app.db import SessionLocal, backup_sqlite_files, get_db_session, init_db from app.models import Comment, ContentItem, Hotspot, Report from app.schemas import CreateTaskRequest, CreateTaskResponse, TaskResponse -from app.services.export_service import export_hotspot_comments_csv, export_item_comments_csv, export_report_markdown from app.services.task_service import ( RUNNING_TASK_MESSAGE, create_task, @@ -146,47 +143,3 @@ def item_detail_page(item_id: str, request: Request, session: Session = Depends( "items/detail.html", {"task": task, "hotspot": hotspot, "item": item, "report": report, "comments": comments}, ) - - -@app.get("/api/export/items/{item_id}/comments.csv") -def export_item_comments(item_id: str, session: Session = Depends(get_db_session)) -> Response: - try: - content, filename = export_item_comments_csv(session, item_id) - except ValueError: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="导出数据不存在") - return Response( - content=content, - media_type="text/csv; charset=utf-8", - headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, - ) - - -@app.get("/api/export/hotspots/{hotspot_id}/comments.csv") -def export_hotspot_comments(hotspot_id: str, session: Session = Depends(get_db_session)) -> Response: - try: - content, filename = export_hotspot_comments_csv(session, hotspot_id) - except ValueError: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="导出数据不存在") - return Response( - content=content, - media_type="text/csv; charset=utf-8", - headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}, - ) - - -@app.get("/api/export/hotspots/{hotspot_id}.md") -def export_hotspot_markdown(hotspot_id: str, session: Session = Depends(get_db_session)) -> Response: - try: - content, filename = export_report_markdown(session, report_type="hotspot", hotspot_id=hotspot_id) - except ValueError: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报告不存在") - return Response(content=content, media_type="text/markdown; charset=utf-8", headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}) - - -@app.get("/api/export/items/{item_id}.md") -def export_item_markdown(item_id: str, session: Session = Depends(get_db_session)) -> Response: - try: - content, filename = export_report_markdown(session, report_type="item", item_id=item_id) - except ValueError: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报告不存在") - return Response(content=content, media_type="text/markdown; charset=utf-8", headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"}) diff --git a/app/services/export_service.py b/app/services/export_service.py deleted file mode 100644 index 59db585..0000000 --- a/app/services/export_service.py +++ /dev/null @@ -1,104 +0,0 @@ -import csv -import io -import json -import re - -from sqlalchemy import select -from sqlalchemy.orm import Session - -from app.models import Comment, ContentItem, Hotspot, Report - - -def labels_to_text(labels_json: str | None) -> str: - try: - labels = json.loads(labels_json or "[]") - except json.JSONDecodeError: - return "" - return ",".join(str(label) for label in labels) if isinstance(labels, list) else "" - - -def safe_csv_field(value) -> str: - text = "" if value is None else str(value) - text = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ") - if text.startswith(("=", "+", "-", "@")): - return f"'{text}" - return text - - -def safe_filename(platform: str, task_id: str, hotspot_keyword: str, *, extension: str = "csv") -> str: - keyword = hotspot_keyword[:20] + ("..." if len(hotspot_keyword) > 20 else "") - name = f"{platform}_{task_id}_{keyword}" - name = re.sub(r'[/\\:*?"<>|]+', "_", name) - name = re.sub(r"_+", "_", name).strip("_") - return f"{name}.{extension}" - - -def export_item_comments_csv(session: Session, item_id: str) -> tuple[bytes, str]: - item = session.get(ContentItem, item_id) - if item is None: - raise ValueError("content item not found") - hotspot = session.get(Hotspot, item.hotspot_id) - comments = list(session.scalars(select(Comment).where(Comment.content_item_id == item.id))) - output = io.StringIO() - writer = csv.writer(output) - writer.writerow(["平台", "任务 ID", "热点 ID", "热点标题", "内容条目 ID", "内容条目标题", "评论 ID", "评论内容", "情绪倾向", "方向标签", "点赞数", "评论时间"]) - for comment in comments: - writer.writerow( - [ - item.platform, - item.task_id, - hotspot.id if hotspot else "", - hotspot.title if hotspot else "", - item.id, - item.title or "", - comment.source_comment_id or "", - safe_csv_field(comment.content), - comment.sentiment, - labels_to_text(comment.labels), - comment.like_count or 0, - comment.comment_time or "", - ] - ) - filename = safe_filename(item.platform, item.task_id, hotspot.title if hotspot else item.title or "comments") - return output.getvalue().encode("utf-8-sig"), filename - - -def export_hotspot_comments_csv(session: Session, hotspot_id: str) -> tuple[bytes, str]: - hotspot = session.get(Hotspot, hotspot_id) - if hotspot is None: - raise ValueError("hotspot not found") - comments = list(session.scalars(select(Comment).where(Comment.hotspot_id == hotspot.id))) - output = io.StringIO() - writer = csv.writer(output) - writer.writerow(["平台", "任务 ID", "热点 ID", "热点标题", "内容条目 ID", "内容条目标题", "评论 ID", "评论内容", "情绪倾向", "方向标签", "点赞数", "评论时间"]) - for comment in comments: - item = session.get(ContentItem, comment.content_item_id) - writer.writerow( - [ - comment.platform, - comment.task_id, - hotspot.id, - hotspot.title, - item.id if item else "", - item.title if item else "", - comment.source_comment_id or "", - safe_csv_field(comment.content), - comment.sentiment, - labels_to_text(comment.labels), - comment.like_count or 0, - comment.comment_time or "", - ] - ) - return output.getvalue().encode("utf-8-sig"), safe_filename(hotspot.platform, hotspot.task_id, hotspot.title) - - -def export_report_markdown(session: Session, *, report_type: str, hotspot_id: str | None = None, item_id: str | None = None) -> tuple[str, str]: - query = select(Report).where(Report.report_type == report_type) - if hotspot_id: - query = query.where(Report.hotspot_id == hotspot_id) - if item_id: - query = query.where(Report.content_item_id == item_id) - report = session.scalar(query) - if report is None: - raise ValueError("report not found") - return report.markdown_content or report.markdown, safe_filename("report", report.task_id, report.title, extension="md") diff --git a/app/static/app.js b/app/static/app.js index a7cd2af..e29dcd3 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -111,46 +111,3 @@ function pollTaskListStatus() { }; window.setTimeout(poll, intervalMs); } - -async function downloadExport(url, defaultFilename, event) { - if (event) event.preventDefault(); - const button = event ? event.currentTarget : null; - const originalText = button ? button.textContent : ""; - if (button) { - button.disabled = true; - button.textContent = "下载中..."; - } - try { - const resp = await fetch(url, { cache: "no-store" }); - if (!resp.ok) { - let message = "导出失败,请稍后重试。"; - try { - const data = await resp.json(); - if (data.detail) message = data.detail; - } catch (_error) { - message = resp.status === 503 ? "数据库暂时不可用,请稍后重试。" : message; - } - alert(message); - return; - } - const blob = await resp.blob(); - const disposition = resp.headers.get("Content-Disposition") || ""; - const match = disposition.match(/filename\*=UTF-8''([^;]+)/); - const filename = match ? decodeURIComponent(match[1]) : defaultFilename; - const blobUrl = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = blobUrl; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(blobUrl); - } catch (_error) { - alert("网络异常,导出未完成。"); - } finally { - if (button) { - button.disabled = false; - button.textContent = originalText; - } - } -} diff --git a/app/templates/hotspots/report.html b/app/templates/hotspots/report.html index 9e126eb..876722b 100644 --- a/app/templates/hotspots/report.html +++ b/app/templates/hotspots/report.html @@ -10,19 +10,6 @@ {% block content %}

{{ hotspot.title }} 汇总报告

-
- - -
{% if report %} {% include "partials/report_panel.html" %} @@ -30,7 +17,6 @@

报告生成中,请稍候...

-

Markdown 报告将在分析完成后开放下载;评论 CSV 可先导出已抓取的数据。

{% endif %} {% endblock %} diff --git a/app/templates/items/detail.html b/app/templates/items/detail.html index 6daa8a1..d5b20e9 100644 --- a/app/templates/items/detail.html +++ b/app/templates/items/detail.html @@ -11,19 +11,6 @@ {% block content %}

{{ item.title or item.source_item_id }}

-
- - -
{% if report %} {% include "partials/report_panel.html" %} @@ -31,7 +18,6 @@

报告生成中,请稍候...

-

Markdown 报告将在分析完成后开放下载;评论 CSV 可先导出已抓取的数据。

{% endif %}
评论明细
diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 5830c57..11e7d99 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -93,16 +93,13 @@ http://localhost:8000 如果报告尚未生成,页面会显示“报告生成中,请稍候...”,不会返回 500。 -## 5. 导出文件 +## 5. 文件导出 -支持导出: +当前版本已关闭 CSV / Markdown 文件下载入口和接口。页面仍可直接查看: -- 热点报告 Markdown -- 内容报告 Markdown -- 热点评论 CSV -- 内容评论 CSV - -CSV 使用 UTF-8-BOM,适合 Windows Excel 打开。评论内容中的换行会被替换为空格,`= + - @` 开头的内容会自动加单引号,避免被 Excel 当作公式执行。 +- 热点级汇总报告 +- 内容条目级报告 +- 评论明细 ## 6. 小规模验收 @@ -119,7 +116,7 @@ CSV 使用 UTF-8-BOM,适合 Windows Excel 打开。评论内容中的换行会 - 至少有热点、内容、评论、报告。 - AI 成功率正常,或页面有明确“AI 样本不足”提示。 - 能打开热点报告、内容详情页。 -- 能导出 Markdown 和 CSV。 +- 页面不展示 CSV / Markdown 下载入口。 ## 7. 默认规模验收 diff --git a/tests/integration/test_routes.py b/tests/integration/test_routes.py index 8b12a8f..93b0d1b 100644 --- a/tests/integration/test_routes.py +++ b/tests/integration/test_routes.py @@ -419,8 +419,10 @@ def test_result_pages_render_seeded_data(): assert 'onclick="window.location.reload()"' in task_response.text assert hotspot_response.status_code == 200 assert "热点总结" in hotspot_response.text - assert "downloadExport('/api/export/hotspots/hot-result.md'" in hotspot_response.text - assert "downloadExport('/api/export/hotspots/hot-result/comments.csv'" in hotspot_response.text + assert "downloadExport(" not in hotspot_response.text + assert "/api/export/" not in hotspot_response.text + assert "导出 Markdown" not in hotspot_response.text + assert "导出热点评论 CSV" not in hotspot_response.text assert "评论样本" in hotspot_response.text assert "关联内容" in hotspot_response.text assert "正向" in hotspot_response.text @@ -431,8 +433,10 @@ def test_result_pages_render_seeded_data(): assert "g|很长很长很长很长很长") - - assert "/" not in filename - assert "__" not in filename - assert filename.endswith(".csv")