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.
201 lines
6.1 KiB
201 lines
6.1 KiB
"""家庭成员管理服务函数。"""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
|
|
from sqlalchemy import Select, func
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.extensions import db
|
|
from app.models import HouseholdMember
|
|
|
|
# 成员性别选项
|
|
GENDER_OPTIONS = ("male", "female", "other")
|
|
GENDER_LABELS = {
|
|
"male": "男",
|
|
"female": "女",
|
|
"other": "其他",
|
|
}
|
|
|
|
# 年龄段选项
|
|
AGE_GROUP_OPTIONS = ("child", "teen", "adult", "senior")
|
|
AGE_GROUP_LABELS = {
|
|
"child": "儿童(0-12岁)",
|
|
"teen": "青少年(13-17岁)",
|
|
"adult": "成人(18-59岁)",
|
|
"senior": "长者(60岁以上)",
|
|
}
|
|
|
|
# 与户主关系选项
|
|
RELATION_TO_HEAD_OPTIONS = (
|
|
"户主",
|
|
"配偶",
|
|
"子女",
|
|
"儿媳/女婿",
|
|
"孙辈",
|
|
"父母",
|
|
"岳父母/公婆",
|
|
"兄弟姐妹",
|
|
"其他亲属",
|
|
"朋友",
|
|
"其他",
|
|
)
|
|
|
|
MEMBER_VALUE_LABELS = {
|
|
"gender": GENDER_LABELS,
|
|
"age_group": AGE_GROUP_LABELS,
|
|
}
|
|
|
|
|
|
def member_query(household_id: int | None = None) -> Select[tuple[HouseholdMember]]:
|
|
"""构建家庭成员查询。"""
|
|
query = db.select(HouseholdMember).options(
|
|
selectinload(HouseholdMember.household),
|
|
)
|
|
if household_id is not None:
|
|
query = query.where(HouseholdMember.household_id == household_id)
|
|
return query.order_by(HouseholdMember.sort_order.asc(), HouseholdMember.id.asc())
|
|
|
|
|
|
def list_members(household_id: int) -> Sequence[HouseholdMember]:
|
|
"""列出指定户的所有成员。"""
|
|
result = db.session.execute(
|
|
member_query(household_id=household_id),
|
|
)
|
|
return result.scalars().all()
|
|
|
|
|
|
def get_member_or_none(member_id: int) -> HouseholdMember | None:
|
|
"""根据 ID 获取成员,不存在返回 None。"""
|
|
result = db.session.execute(
|
|
member_query().where(HouseholdMember.id == member_id),
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
def get_member_count(household_id: int) -> int:
|
|
"""获取指定户的成员数量。"""
|
|
result = db.session.execute(
|
|
db.select(func.count(HouseholdMember.id)).where(
|
|
HouseholdMember.household_id == household_id,
|
|
),
|
|
)
|
|
return result.scalar_one() or 0
|
|
|
|
|
|
def suggest_next_sort_order(household_id: int) -> int:
|
|
"""建议下一个排序序号。"""
|
|
result = db.session.execute(
|
|
db.select(func.max(HouseholdMember.sort_order)).where(
|
|
HouseholdMember.household_id == household_id,
|
|
),
|
|
)
|
|
max_order = result.scalar_one()
|
|
return (max_order or 0) + 10
|
|
|
|
|
|
def build_new_member_draft(household_id: int) -> HouseholdMember:
|
|
"""构建新成员草稿对象。"""
|
|
member = HouseholdMember()
|
|
member.household_id = household_id
|
|
member.name = ""
|
|
member.relation_to_head = None
|
|
member.gender = None
|
|
member.age_group = None
|
|
member.is_child = False
|
|
member.needs_red_packet = False
|
|
member.expected_to_attend = True
|
|
member.actually_attended = False
|
|
member.sort_order = suggest_next_sort_order(household_id)
|
|
member.note = None
|
|
return member
|
|
|
|
|
|
def parse_member_form(
|
|
form: dict[str, str],
|
|
*,
|
|
household_id: int,
|
|
) -> tuple[dict[str, object], list[str]]:
|
|
"""解析成员表单数据,返回有效载荷和错误列表。"""
|
|
errors: list[str] = []
|
|
|
|
# 必填字段:姓名
|
|
name = form.get("name", "").strip()
|
|
if not name:
|
|
errors.append("成员姓名不能为空。")
|
|
|
|
# 与户主关系
|
|
relation_to_head = form.get("relation_to_head", "").strip() or None
|
|
if relation_to_head and relation_to_head not in RELATION_TO_HEAD_OPTIONS:
|
|
errors.append("与户主关系选项不合法。")
|
|
|
|
# 性别
|
|
gender = form.get("gender", "").strip() or None
|
|
if gender and gender not in GENDER_OPTIONS:
|
|
errors.append("性别选项不合法。")
|
|
|
|
# 年龄段
|
|
age_group = form.get("age_group", "").strip() or None
|
|
if age_group and age_group not in AGE_GROUP_OPTIONS:
|
|
errors.append("年龄段选项不合法。")
|
|
|
|
# 是否儿童
|
|
is_child_raw = form.get("is_child", "false").strip().lower()
|
|
is_child = is_child_raw in ("true", "1", "yes", "on")
|
|
|
|
# 是否需要红包
|
|
needs_red_packet_raw = form.get("needs_red_packet", "false").strip().lower()
|
|
needs_red_packet = needs_red_packet_raw in ("true", "1", "yes", "on")
|
|
|
|
# 预期是否到场
|
|
expected_to_attend_raw = form.get("expected_to_attend", "true").strip().lower()
|
|
expected_to_attend = expected_to_attend_raw in ("true", "1", "yes", "on")
|
|
|
|
# 实际是否到场
|
|
actually_attended_raw = form.get("actually_attended", "false").strip().lower()
|
|
actually_attended = actually_attended_raw in ("true", "1", "yes", "on")
|
|
|
|
if errors:
|
|
return {}, errors
|
|
|
|
return {
|
|
"household_id": household_id,
|
|
"name": name,
|
|
"relation_to_head": relation_to_head,
|
|
"gender": gender,
|
|
"age_group": age_group,
|
|
"is_child": is_child,
|
|
"needs_red_packet": needs_red_packet,
|
|
"expected_to_attend": expected_to_attend,
|
|
"actually_attended": actually_attended,
|
|
"sort_order": suggest_next_sort_order(household_id),
|
|
"note": form.get("note", "").strip() or None,
|
|
}, []
|
|
|
|
|
|
def serialize_member_snapshot(member: HouseholdMember) -> dict[str, object]:
|
|
"""序列化成员快照,用于审计日志。"""
|
|
return {
|
|
"id": member.id,
|
|
"household_id": member.household_id,
|
|
"name": member.name,
|
|
"relation_to_head": member.relation_to_head,
|
|
"gender": member.gender,
|
|
"age_group": member.age_group,
|
|
"is_child": member.is_child,
|
|
"needs_red_packet": member.needs_red_packet,
|
|
"expected_to_attend": member.expected_to_attend,
|
|
"actually_attended": member.actually_attended,
|
|
"sort_order": member.sort_order,
|
|
"note": member.note,
|
|
}
|
|
|
|
|
|
def member_value_label(field_name: str, value: str | None) -> str:
|
|
"""获取成员字段的中文标签。"""
|
|
if value is None:
|
|
return "-"
|
|
normalized_value = value.strip()
|
|
if not normalized_value:
|
|
return "-"
|
|
return MEMBER_VALUE_LABELS.get(field_name, {}).get(normalized_value, normalized_value)
|
|
|