from __future__ import annotations from flask import Blueprint, flash, redirect, render_template, request, url_for from flask.typing import ResponseReturnValue from app.extensions import db from app.services import ( check_share_token, get_household_or_none, household_has_active_gift_records, is_head_name_taken, list_enabled_options, list_households, normalize_search_term, parse_admin_form, recalculate_household_gift_summary, serialize_admin_edit_snapshot, write_audit_log, ) share_bp = Blueprint("share", __name__, url_prefix="/s") def _is_partial_request(expected_partial: str) -> bool: requested_partial = request.args.get("partial", "").strip() header_partial = request.headers.get("X-HW-Partial", "").strip() return requested_partial == expected_partial or header_partial == expected_partial @share_bp.get("/") def search_page(token: str) -> ResponseReturnValue: check_result = check_share_token(token) if not check_result.ok: return render_template("share/invalid.html", reason=check_result.reason), 404 search_term = request.args.get("q", "").strip() households = list_households(search_term=search_term, limit=20) if normalize_search_term(search_term) else [] template_name = "share/_results.html" if _is_partial_request("results") else "share/search.html" return render_template( template_name, token=token, search_term=search_term, households=households, ) @share_bp.get("//households//edit") def edit_page(token: str, household_id: int) -> ResponseReturnValue: check_result = check_share_token(token) if not check_result.ok: return render_template("share/invalid.html", reason=check_result.reason), 404 household = get_household_or_none(household_id) if household is None: flash("未找到要修改的户信息。", "error") return redirect(url_for("share.search_page", token=token)) return render_template( "share/edit.html", token=token, household=household, search_term=request.args.get("q", "").strip(), relation_category_options=list_enabled_options("relation_category"), tag_options=list_enabled_options("tag"), gift_method_options=list_enabled_options("gift_method"), gift_scene_options=list_enabled_options("gift_scene"), ) @share_bp.post("//households//edit") def update_household(token: str, household_id: int) -> ResponseReturnValue: check_result = check_share_token(token) if not check_result.ok: return render_template("share/invalid.html", reason=check_result.reason), 404 household = get_household_or_none(household_id) if household is None: flash("未找到要修改的户信息。", "error") return redirect(url_for("share.search_page", token=token)) search_term = request.form.get("q", "").strip() redirect_to_search = url_for("share.search_page", token=token, q=search_term) if search_term else url_for("share.search_page", token=token) redirect_to_self = ( url_for("share.edit_page", token=token, household_id=household_id, q=search_term) if search_term else url_for("share.edit_page", token=token, household_id=household_id) ) submitted_version = request.form.get("version", "").strip() try: version = int(submitted_version) except ValueError: flash("表单版本无效,请重新打开该户信息后再试。", "error") return redirect(redirect_to_self) if version != household.version: flash("该户信息已被其他人更新,请刷新后重新确认再保存。", "warning") return redirect(redirect_to_self) relation_category_options = list_enabled_options("relation_category") tag_options = list_enabled_options("tag") gift_method_options = list_enabled_options("gift_method") gift_scene_options = list_enabled_options("gift_scene") payload, errors = parse_admin_form( dict(request.form), raw_tag_option_ids=request.form.getlist("tag_option_ids_json"), valid_relation_category_ids={option.id for option in relation_category_options}, relation_detail_parent_map={}, valid_tag_ids={option.id for option in tag_options}, valid_method_ids={option.id for option in gift_method_options}, valid_scene_ids={option.id for option in gift_scene_options}, ) if errors: for error in errors: flash(error, "error") return redirect(redirect_to_self) head_name = str(payload["head_name"]) if is_head_name_taken(head_name, excluding_household_id=household_id): flash("户主姓名已存在,请修改后重试。", "error") return redirect(redirect_to_self) if household_has_active_gift_records(household_id): payload.pop("total_gift_amount", None) payload.pop("gift_method_option_id", None) payload.pop("gift_scene_option_id", None) before_snapshot = serialize_admin_edit_snapshot(household) for field_name, value in payload.items(): setattr(household, field_name, value) if household_has_active_gift_records(household_id): recalculate_household_gift_summary(household) if serialize_admin_edit_snapshot(household) == before_snapshot: flash("没有检测到需要保存的变化。", "warning") db.session.rollback() return redirect(redirect_to_search) household.version += 1 share_link = check_result.link write_audit_log( action_type="update_household_share_link", target_type="household", actor_username=f"share:{share_link.token}" if share_link is not None else "share", target_id=household.id, target_display_name=household.head_name, before_data=before_snapshot, after_data=serialize_admin_edit_snapshot(household) | { "share_link_id": share_link.id if share_link is not None else None, "share_link_label": share_link.label if share_link is not None else None, }, commit=False, ) db.session.commit() flash(f"已保存 {household.head_name} 的信息。", "success") return redirect(redirect_to_search)