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 %}
报告生成中,请稍候...
-Markdown 报告将在分析完成后开放下载;评论 CSV 可先导出已抓取的数据。
报告生成中,请稍候...
-Markdown 报告将在分析完成后开放下载;评论 CSV 可先导出已抓取的数据。
g|很长很长很长很长很长")
-
- assert "/" not in filename
- assert "__" not in filename
- assert filename.endswith(".csv")