You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

135 lines
4.9 KiB

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",
},
)