from __future__ import annotations from flask import Blueprint, Response, flash, g, redirect, render_template, request, stream_with_context, url_for from app.auth import role_required from app.extensions import db from app.models import Account from app.services import ( build_household_csv_template, build_household_export_filename, delete_household_import_preview, get_household_import_conflict_modes, list_households, load_household_import_preview, preview_household_csv, stream_household_csv, write_audit_log, apply_household_import_preview, ) from app.services.csv_households import CsvImportError csv_bp = Blueprint("csv", __name__, url_prefix="/csv") @csv_bp.get("/households/import") @role_required("admin", "editor") def import_households_page() -> str: preview_token = request.args.get("preview_token", "").strip() preview = load_household_import_preview(preview_token) if preview_token else None return render_template( "csv/import_preview.html", preview=preview, conflict_modes=get_household_import_conflict_modes(), ) @csv_bp.post("/households/import/preview") @role_required("admin", "editor") def preview_households_import(): file_storage = request.files.get("file") if file_storage is None or not file_storage.filename: flash("请先选择一个 CSV 文件。", "error") return redirect(url_for("csv.import_households_page")) try: preview = preview_household_csv( file_name=file_storage.filename, file_bytes=file_storage.read(), ) except CsvImportError as exc: flash(str(exc), "error") return redirect(url_for("csv.import_households_page")) flash("CSV 预览已生成,请先检查无效行和冲突行。", "success") return redirect(url_for("csv.import_households_page", preview_token=preview["token"])) @csv_bp.post("/households/import/confirm") @role_required("admin", "editor") def confirm_households_import(): preview_token = request.form.get("preview_token", "").strip() conflict_mode = request.form.get("conflict_mode", "").strip() preview = load_household_import_preview(preview_token) if preview is None: flash("导入预览已失效,请重新上传 CSV。", "error") return redirect(url_for("csv.import_households_page")) current_account = g.current_account if isinstance(getattr(g, "current_account", None), Account) else None try: summary = apply_household_import_preview( preview=preview, actor_id=current_account.id if current_account is not None else None, conflict_mode=conflict_mode, ) except CsvImportError as exc: flash(str(exc), "error") return redirect(url_for("csv.import_households_page", preview_token=preview_token)) write_audit_log( action_type="import_households_csv", target_type="household", actor=current_account, target_display_name=str(summary["file_name"]), after_data=summary, commit=False, ) db.session.commit() delete_household_import_preview(preview_token) flash( f"CSV 导入完成:新增 {summary['rows_created']} 户,更新 {summary['rows_updated']} 户,跳过 {summary['rows_skipped']} 行,无效 {summary['rows_invalid']} 行。", "success", ) return redirect(url_for("main.index")) @csv_bp.get("/households/template") @role_required("admin", "editor") def download_household_template() -> Response: return Response( build_household_csv_template(), mimetype="text/csv; charset=utf-8", headers={ "Content-Disposition": f"attachment; filename={build_household_export_filename(scope='template')}", "Cache-Control": "no-store", }, ) @csv_bp.get("/households/export") @role_required("admin", "editor") def export_households() -> Response: scope = request.args.get("scope", "filtered").strip() if scope == "all": households = list_households(limit=None) filename = build_household_export_filename(scope="all") else: households = list_households( search_term=request.args.get("q", "").strip(), side=request.args.get("side", "").strip(), invite_status=request.args.get("invite_status", "").strip(), attendance_status=request.args.get("attendance_status", "").strip(), sort_by=request.args.get("sort_by", "updated_at").strip(), sort_order=request.args.get("sort_order", "desc").strip(), limit=None, ) filename = build_household_export_filename(scope="filtered") return Response( stream_with_context(stream_household_csv(households)), mimetype="text/csv; charset=utf-8", headers={ "Content-Disposition": f"attachment; filename={filename}", "Cache-Control": "no-store", }, )