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.
602 lines
20 KiB
602 lines
20 KiB
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from decimal import Decimal
|
|
|
|
from app.extensions import db
|
|
from app.models import Account, Household, HouseholdMember, OptionItem
|
|
|
|
|
|
DEMO_ADMIN_USERNAME = "admin"
|
|
|
|
DEFAULT_STRESS_HOUSEHOLD_COUNT = 1000
|
|
DEFAULT_STRESS_MEMBERS_PER_HOUSEHOLD = 4
|
|
DEFAULT_STRESS_HOUSEHOLD_CODE_PREFIX = "STRESS-"
|
|
|
|
_STRESS_SIDE_OPTIONS: tuple[str, ...] = ("groom_side", "bride_side", "both_side")
|
|
_STRESS_RELATION_CATEGORY_CODES: tuple[str, ...] = ("relative", "friend", "colleague")
|
|
_STRESS_TAG_CODE_SETS: tuple[tuple[str, ...], ...] = ((),)
|
|
_STRESS_INVITE_STATUS_OPTIONS: tuple[str, ...] = ("not_invited", "invited", "confirmed")
|
|
_STRESS_ATTENDANCE_STATUS_OPTIONS: tuple[str, ...] = ("pending", "attending", "partial", "absent")
|
|
_STRESS_GIVEN_STATUS_OPTIONS: tuple[str, ...] = ("not_given", "given")
|
|
_STRESS_CHILD_RED_PACKET_STATUS_OPTIONS: tuple[str, ...] = ("not_given", "partial", "given")
|
|
_STRESS_GIFT_METHOD_CODES: tuple[str, ...] = ("cash", "transfer")
|
|
_STRESS_GIFT_SCENE_CODES: tuple[str, ...] = ("pre_wedding", "wedding_day", "post_wedding")
|
|
_STRESS_RELATION_TO_HEAD_OPTIONS: tuple[str, ...] = (
|
|
"本人",
|
|
"配偶",
|
|
"子女",
|
|
"亲属",
|
|
"同事",
|
|
"同学",
|
|
)
|
|
_STRESS_GENDER_OPTIONS: tuple[str, ...] = ("male", "female")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DemoAccountSpec:
|
|
username: str
|
|
display_name: str
|
|
password: str
|
|
role: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DemoMemberSpec:
|
|
name: str
|
|
relation_to_head: str | None
|
|
gender: str | None
|
|
age_group: str | None
|
|
is_child: bool
|
|
needs_red_packet: bool
|
|
expected_to_attend: bool
|
|
actually_attended: bool
|
|
sort_order: int
|
|
note: str | None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DemoHouseholdSpec:
|
|
household_code: str
|
|
head_name: str
|
|
phone: str | None
|
|
side: str
|
|
relation_category_code: str | None
|
|
relation_detail_code: str | None
|
|
tag_codes: tuple[str, ...]
|
|
invite_status: str
|
|
attendance_status: str
|
|
expected_attendee_count: int
|
|
actual_attendee_count: int
|
|
child_count: int
|
|
red_packet_child_count: int
|
|
total_gift_amount: Decimal
|
|
gift_method_code: str | None
|
|
gift_scene_code: str | None
|
|
favor_status: str
|
|
candy_status: str
|
|
child_red_packet_status: str
|
|
note: str | None
|
|
members: tuple[DemoMemberSpec, ...]
|
|
|
|
|
|
DEMO_ACCOUNT_SPECS: tuple[DemoAccountSpec, ...] = (
|
|
DemoAccountSpec(
|
|
username="editor-demo",
|
|
display_name="演示录入员",
|
|
password="EditorDemo123!",
|
|
role="editor",
|
|
),
|
|
DemoAccountSpec(
|
|
username="entry-demo",
|
|
display_name="长辈演示账号",
|
|
password="EntryDemo123!",
|
|
role="entry_only",
|
|
),
|
|
DemoAccountSpec(
|
|
username="quick-editor-demo",
|
|
display_name="快速录入账号",
|
|
password="QuickEditor123!",
|
|
role="quick_editor",
|
|
),
|
|
)
|
|
|
|
|
|
DEMO_HOUSEHOLD_SPECS: tuple[DemoHouseholdSpec, ...] = (
|
|
DemoHouseholdSpec(
|
|
household_code="D001",
|
|
head_name="王大爷",
|
|
phone="13800001234",
|
|
side="groom_side",
|
|
relation_category_code="relative",
|
|
relation_detail_code=None,
|
|
tag_codes=(),
|
|
invite_status="confirmed",
|
|
attendance_status="pending",
|
|
expected_attendee_count=4,
|
|
actual_attendee_count=0,
|
|
child_count=1,
|
|
red_packet_child_count=1,
|
|
total_gift_amount=Decimal("666.00"),
|
|
gift_method_code="cash",
|
|
gift_scene_code="wedding_day",
|
|
favor_status="not_given",
|
|
candy_status="given",
|
|
child_red_packet_status="not_given",
|
|
note="演示数据:待电话确认当天到场时间。",
|
|
members=(
|
|
DemoMemberSpec(
|
|
name="王大爷",
|
|
relation_to_head="本人",
|
|
gender="male",
|
|
age_group="elder",
|
|
is_child=False,
|
|
needs_red_packet=False,
|
|
expected_to_attend=True,
|
|
actually_attended=False,
|
|
sort_order=10,
|
|
note="户主",
|
|
),
|
|
DemoMemberSpec(
|
|
name="王阿姨",
|
|
relation_to_head="配偶",
|
|
gender="female",
|
|
age_group="elder",
|
|
is_child=False,
|
|
needs_red_packet=False,
|
|
expected_to_attend=True,
|
|
actually_attended=False,
|
|
sort_order=20,
|
|
note=None,
|
|
),
|
|
DemoMemberSpec(
|
|
name="王小宝",
|
|
relation_to_head="孙辈",
|
|
gender="male",
|
|
age_group="child",
|
|
is_child=True,
|
|
needs_red_packet=True,
|
|
expected_to_attend=True,
|
|
actually_attended=False,
|
|
sort_order=30,
|
|
note="需准备儿童红包",
|
|
),
|
|
),
|
|
),
|
|
DemoHouseholdSpec(
|
|
household_code="D002",
|
|
head_name="李阿姨",
|
|
phone="13900004567",
|
|
side="bride_side",
|
|
relation_category_code="friend",
|
|
relation_detail_code=None,
|
|
tag_codes=(),
|
|
invite_status="invited",
|
|
attendance_status="attending",
|
|
expected_attendee_count=2,
|
|
actual_attendee_count=2,
|
|
child_count=0,
|
|
red_packet_child_count=0,
|
|
total_gift_amount=Decimal("888.00"),
|
|
gift_method_code="transfer",
|
|
gift_scene_code="pre_wedding",
|
|
favor_status="given",
|
|
candy_status="given",
|
|
child_red_packet_status="not_given",
|
|
note="演示数据:已确认提前到场帮忙。",
|
|
members=(
|
|
DemoMemberSpec(
|
|
name="李阿姨",
|
|
relation_to_head="本人",
|
|
gender="female",
|
|
age_group="adult",
|
|
is_child=False,
|
|
needs_red_packet=False,
|
|
expected_to_attend=True,
|
|
actually_attended=True,
|
|
sort_order=10,
|
|
note=None,
|
|
),
|
|
DemoMemberSpec(
|
|
name="李叔叔",
|
|
relation_to_head="配偶",
|
|
gender="male",
|
|
age_group="adult",
|
|
is_child=False,
|
|
needs_red_packet=False,
|
|
expected_to_attend=True,
|
|
actually_attended=True,
|
|
sort_order=20,
|
|
note=None,
|
|
),
|
|
),
|
|
),
|
|
DemoHouseholdSpec(
|
|
household_code="D003",
|
|
head_name="陈老师",
|
|
phone="13700007890",
|
|
side="both_side",
|
|
relation_category_code="colleague",
|
|
relation_detail_code=None,
|
|
tag_codes=(),
|
|
invite_status="confirmed",
|
|
attendance_status="partial",
|
|
expected_attendee_count=3,
|
|
actual_attendee_count=2,
|
|
child_count=1,
|
|
red_packet_child_count=0,
|
|
total_gift_amount=Decimal("520.00"),
|
|
gift_method_code="cash",
|
|
gift_scene_code="wedding_day",
|
|
favor_status="given",
|
|
candy_status="not_given",
|
|
child_red_packet_status="partial",
|
|
note="演示数据:一位孩子不单独领红包。",
|
|
members=(
|
|
DemoMemberSpec(
|
|
name="陈老师",
|
|
relation_to_head="本人",
|
|
gender="female",
|
|
age_group="adult",
|
|
is_child=False,
|
|
needs_red_packet=False,
|
|
expected_to_attend=True,
|
|
actually_attended=True,
|
|
sort_order=10,
|
|
note=None,
|
|
),
|
|
DemoMemberSpec(
|
|
name="陈先生",
|
|
relation_to_head="配偶",
|
|
gender="male",
|
|
age_group="adult",
|
|
is_child=False,
|
|
needs_red_packet=False,
|
|
expected_to_attend=True,
|
|
actually_attended=True,
|
|
sort_order=20,
|
|
note=None,
|
|
),
|
|
DemoMemberSpec(
|
|
name="陈小朋友",
|
|
relation_to_head="子女",
|
|
gender="female",
|
|
age_group="child",
|
|
is_child=True,
|
|
needs_red_packet=False,
|
|
expected_to_attend=True,
|
|
actually_attended=False,
|
|
sort_order=30,
|
|
note="部分到场示例",
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def seed_demo_accounts() -> int:
|
|
created_count = 0
|
|
for spec in DEMO_ACCOUNT_SPECS:
|
|
existing_account = db.session.execute(
|
|
db.select(Account).where(Account.username == spec.username),
|
|
).scalar_one_or_none()
|
|
if existing_account is not None:
|
|
continue
|
|
|
|
account = Account(
|
|
username=spec.username,
|
|
display_name=spec.display_name,
|
|
role=spec.role,
|
|
status="active",
|
|
)
|
|
account.set_password(spec.password)
|
|
db.session.add(account)
|
|
created_count += 1
|
|
|
|
return created_count
|
|
|
|
|
|
def seed_demo_households() -> int:
|
|
created_count = 0
|
|
option_index = _build_option_index()
|
|
admin_account = _get_admin_account()
|
|
admin_id = admin_account.id if admin_account is not None else None
|
|
|
|
for spec in DEMO_HOUSEHOLD_SPECS:
|
|
existing_household = db.session.execute(
|
|
db.select(Household).where(Household.household_code == spec.household_code),
|
|
).scalar_one_or_none()
|
|
if existing_household is not None:
|
|
continue
|
|
|
|
household = Household(
|
|
household_code=spec.household_code,
|
|
head_name=spec.head_name,
|
|
phone=spec.phone,
|
|
side=spec.side,
|
|
relation_category_option_id=_resolve_option_id(option_index, "relation_category", spec.relation_category_code),
|
|
relation_detail_option_id=_resolve_option_id(option_index, "relation_detail", spec.relation_detail_code),
|
|
tag_option_ids_json=[
|
|
option_id
|
|
for option_id in (
|
|
_resolve_option_id(option_index, "tag", tag_code)
|
|
for tag_code in spec.tag_codes
|
|
)
|
|
if option_id is not None
|
|
],
|
|
invite_status=spec.invite_status,
|
|
attendance_status=spec.attendance_status,
|
|
expected_attendee_count=spec.expected_attendee_count,
|
|
actual_attendee_count=spec.actual_attendee_count,
|
|
child_count=spec.child_count,
|
|
red_packet_child_count=spec.red_packet_child_count,
|
|
total_gift_amount=spec.total_gift_amount,
|
|
gift_method_option_id=_resolve_option_id(option_index, "gift_method", spec.gift_method_code),
|
|
gift_scene_option_id=_resolve_option_id(option_index, "gift_scene", spec.gift_scene_code),
|
|
favor_status=spec.favor_status,
|
|
candy_status=spec.candy_status,
|
|
child_red_packet_status=spec.child_red_packet_status,
|
|
note=spec.note,
|
|
version=1,
|
|
created_by=admin_id,
|
|
updated_by=admin_id,
|
|
)
|
|
db.session.add(household)
|
|
db.session.flush()
|
|
|
|
for member_spec in spec.members:
|
|
member = HouseholdMember(
|
|
household_id=household.id,
|
|
name=member_spec.name,
|
|
relation_to_head=member_spec.relation_to_head,
|
|
gender=member_spec.gender,
|
|
age_group=member_spec.age_group,
|
|
is_child=member_spec.is_child,
|
|
needs_red_packet=member_spec.needs_red_packet,
|
|
expected_to_attend=member_spec.expected_to_attend,
|
|
actually_attended=member_spec.actually_attended,
|
|
sort_order=member_spec.sort_order,
|
|
note=member_spec.note,
|
|
created_by=admin_id,
|
|
updated_by=admin_id,
|
|
)
|
|
db.session.add(member)
|
|
|
|
created_count += 1
|
|
|
|
return created_count
|
|
|
|
|
|
def seed_stress_households(
|
|
*,
|
|
household_count: int = DEFAULT_STRESS_HOUSEHOLD_COUNT,
|
|
members_per_household: int = DEFAULT_STRESS_MEMBERS_PER_HOUSEHOLD,
|
|
household_code_prefix: str = DEFAULT_STRESS_HOUSEHOLD_CODE_PREFIX,
|
|
) -> tuple[int, int]:
|
|
normalized_prefix = household_code_prefix.strip().upper()
|
|
if household_count <= 0:
|
|
raise ValueError("household_count must be greater than 0")
|
|
if members_per_household <= 0:
|
|
raise ValueError("members_per_household must be greater than 0")
|
|
if not normalized_prefix:
|
|
raise ValueError("household_code_prefix cannot be empty")
|
|
|
|
created_household_count = 0
|
|
created_member_count = 0
|
|
option_index = _build_option_index()
|
|
admin_account = _get_admin_account()
|
|
admin_id = admin_account.id if admin_account is not None else None
|
|
|
|
for index in range(1, household_count + 1):
|
|
created = _create_stress_household(
|
|
index=index,
|
|
household_code=f"{normalized_prefix}{index:04d}",
|
|
household_code_prefix=normalized_prefix,
|
|
members_per_household=members_per_household,
|
|
option_index=option_index,
|
|
admin_id=admin_id,
|
|
)
|
|
if created is None:
|
|
continue
|
|
|
|
created_household_count += 1
|
|
created_member_count += created
|
|
|
|
return (created_household_count, created_member_count)
|
|
|
|
|
|
def _create_stress_household(
|
|
*,
|
|
index: int,
|
|
household_code: str,
|
|
household_code_prefix: str,
|
|
members_per_household: int,
|
|
option_index: dict[tuple[str, str], int],
|
|
admin_id: int | None,
|
|
) -> int | None:
|
|
existing_household = db.session.execute(
|
|
db.select(Household).where(Household.household_code == household_code),
|
|
).scalar_one_or_none()
|
|
if existing_household is not None:
|
|
return None
|
|
|
|
household = _build_stress_household(
|
|
index=index,
|
|
household_code=household_code,
|
|
household_code_prefix=household_code_prefix,
|
|
members_per_household=members_per_household,
|
|
option_index=option_index,
|
|
admin_id=admin_id,
|
|
)
|
|
db.session.add(household)
|
|
db.session.flush()
|
|
|
|
for member_index in range(1, members_per_household + 1):
|
|
member = _build_stress_member(
|
|
household_id=household.id,
|
|
household_index=index,
|
|
member_index=member_index,
|
|
members_per_household=members_per_household,
|
|
admin_id=admin_id,
|
|
)
|
|
db.session.add(member)
|
|
|
|
return members_per_household
|
|
|
|
|
|
def _build_stress_household(
|
|
*,
|
|
index: int,
|
|
household_code: str,
|
|
household_code_prefix: str,
|
|
members_per_household: int,
|
|
option_index: dict[tuple[str, str], int],
|
|
admin_id: int | None,
|
|
) -> Household:
|
|
relation_category_code = _STRESS_RELATION_CATEGORY_CODES[(index - 1) % len(_STRESS_RELATION_CATEGORY_CODES)]
|
|
side = _STRESS_SIDE_OPTIONS[(index - 1) % len(_STRESS_SIDE_OPTIONS)]
|
|
invite_status = _STRESS_INVITE_STATUS_OPTIONS[(index - 1) % len(_STRESS_INVITE_STATUS_OPTIONS)]
|
|
attendance_status = _STRESS_ATTENDANCE_STATUS_OPTIONS[(index - 1) % len(_STRESS_ATTENDANCE_STATUS_OPTIONS)]
|
|
tag_codes = _STRESS_TAG_CODE_SETS[(index - 1) % len(_STRESS_TAG_CODE_SETS)]
|
|
child_count = 1 if members_per_household >= 3 and index % 3 == 0 else 0
|
|
red_packet_child_count = child_count if index % 2 == 0 else 0
|
|
actual_attendee_count = _stress_actual_attendee_count(
|
|
expected_attendee_count=members_per_household,
|
|
attendance_status=attendance_status,
|
|
)
|
|
total_gift_amount = Decimal(index * 50).quantize(Decimal("0.01"))
|
|
phone_number = f"139{index:08d}"[-11:]
|
|
normalized_phone = f"1{phone_number[-10:]}"
|
|
head_name_prefix = "" if household_code_prefix == DEFAULT_STRESS_HOUSEHOLD_CODE_PREFIX else household_code_prefix
|
|
return Household(
|
|
household_code=household_code,
|
|
head_name=f"{head_name_prefix}压力户主{index:04d}",
|
|
phone=normalized_phone,
|
|
side=side,
|
|
relation_category_option_id=_resolve_option_id(option_index, "relation_category", relation_category_code),
|
|
relation_detail_option_id=None,
|
|
tag_option_ids_json=[
|
|
option_id
|
|
for option_id in (
|
|
_resolve_option_id(option_index, "tag", tag_code)
|
|
for tag_code in tag_codes
|
|
)
|
|
if option_id is not None
|
|
],
|
|
invite_status=invite_status,
|
|
attendance_status=attendance_status,
|
|
expected_attendee_count=members_per_household,
|
|
actual_attendee_count=actual_attendee_count,
|
|
child_count=child_count,
|
|
red_packet_child_count=red_packet_child_count,
|
|
total_gift_amount=total_gift_amount,
|
|
gift_method_option_id=_resolve_option_id(
|
|
option_index,
|
|
"gift_method",
|
|
_STRESS_GIFT_METHOD_CODES[(index - 1) % len(_STRESS_GIFT_METHOD_CODES)],
|
|
),
|
|
gift_scene_option_id=_resolve_option_id(
|
|
option_index,
|
|
"gift_scene",
|
|
_STRESS_GIFT_SCENE_CODES[(index - 1) % len(_STRESS_GIFT_SCENE_CODES)],
|
|
),
|
|
favor_status=_STRESS_GIVEN_STATUS_OPTIONS[index % len(_STRESS_GIVEN_STATUS_OPTIONS)],
|
|
candy_status=_STRESS_GIVEN_STATUS_OPTIONS[(index + 1) % len(_STRESS_GIVEN_STATUS_OPTIONS)],
|
|
child_red_packet_status=_STRESS_CHILD_RED_PACKET_STATUS_OPTIONS[
|
|
(index - 1) % len(_STRESS_CHILD_RED_PACKET_STATUS_OPTIONS)
|
|
],
|
|
note=f"压力测试数据 #{index:04d}",
|
|
version=1,
|
|
created_by=admin_id,
|
|
updated_by=admin_id,
|
|
)
|
|
|
|
|
|
def _build_stress_member(
|
|
*,
|
|
household_id: int,
|
|
household_index: int,
|
|
member_index: int,
|
|
members_per_household: int,
|
|
admin_id: int | None,
|
|
) -> HouseholdMember:
|
|
is_head = member_index == 1
|
|
is_child = members_per_household >= 3 and member_index == members_per_household and household_index % 3 == 0
|
|
attended = household_index % 4 in {1, 2} and not is_child
|
|
return HouseholdMember(
|
|
household_id=household_id,
|
|
name=f"压力成员{household_index:04d}-{member_index}",
|
|
relation_to_head=_stress_relation_to_head(member_index=member_index),
|
|
gender=_STRESS_GENDER_OPTIONS[(household_index + member_index - 2) % len(_STRESS_GENDER_OPTIONS)],
|
|
age_group="child" if is_child else _stress_age_group(member_index=member_index, is_head=is_head),
|
|
is_child=is_child,
|
|
needs_red_packet=is_child and household_index % 2 == 0,
|
|
expected_to_attend=True,
|
|
actually_attended=attended,
|
|
sort_order=member_index * 10,
|
|
note="压力测试儿童成员" if is_child else ("户主" if is_head else None),
|
|
created_by=admin_id,
|
|
updated_by=admin_id,
|
|
)
|
|
|
|
|
|
def _stress_actual_attendee_count(*, expected_attendee_count: int, attendance_status: str) -> int:
|
|
if attendance_status == "attending":
|
|
return expected_attendee_count
|
|
if attendance_status == "partial":
|
|
return max(expected_attendee_count - 1, 1)
|
|
if attendance_status == "absent":
|
|
return 0
|
|
return 0
|
|
|
|
|
|
def _stress_relation_to_head(*, member_index: int) -> str:
|
|
if member_index == 1:
|
|
return "本人"
|
|
if member_index == 2:
|
|
return "配偶"
|
|
if member_index == 3:
|
|
return "子女"
|
|
return _STRESS_RELATION_TO_HEAD_OPTIONS[(member_index - 1) % len(_STRESS_RELATION_TO_HEAD_OPTIONS)]
|
|
|
|
|
|
def _stress_age_group(*, member_index: int, is_head: bool) -> str:
|
|
if is_head:
|
|
return "adult"
|
|
if member_index == 2:
|
|
return "adult"
|
|
return "elder" if member_index % 2 == 0 else "adult"
|
|
|
|
|
|
def demo_login_lines() -> tuple[str, ...]:
|
|
return (
|
|
"admin / ChangeMe123!(管理员)",
|
|
"editor-demo / EditorDemo123!(演示录入员)",
|
|
"entry-demo / EntryDemo123!(长辈录入演示)",
|
|
"quick-editor-demo / QuickEditor123!(快速录入账号)",
|
|
)
|
|
|
|
|
|
def _build_option_index() -> dict[tuple[str, str], int]:
|
|
option_items = db.session.execute(db.select(OptionItem)).scalars().all()
|
|
return {
|
|
(option_item.option_group, option_item.option_code): option_item.id
|
|
for option_item in option_items
|
|
}
|
|
|
|
|
|
def _resolve_option_id(
|
|
option_index: dict[tuple[str, str], int],
|
|
option_group: str,
|
|
option_code: str | None,
|
|
) -> int | None:
|
|
if option_code is None:
|
|
return None
|
|
|
|
return option_index.get((option_group, option_code))
|
|
|
|
|
|
def _get_admin_account() -> Account | None:
|
|
return db.session.execute(
|
|
db.select(Account).where(Account.username == DEMO_ADMIN_USERNAME),
|
|
).scalar_one_or_none()
|
|
|