| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- from dataclasses import dataclass
- from datetime import UTC, datetime, timedelta
- from hashlib import sha256
- from app.commands.seed import build_seed_manifest, stable_id
- from app.core.config import Settings
- from app.domains.attribution.models import LeadAttribution, OrderAttribution
- from app.domains.enrollment.models import (
- EnrollmentDraft,
- EnrollmentOrder,
- PaymentTransaction,
- Policy,
- Quote,
- UserConfirmation,
- )
- from app.domains.identity.models import H5User
- from app.infrastructure.mysql.attribution_repositories import (
- SqlAlchemyAttributionRepository,
- )
- from app.infrastructure.mysql.enrollment_repositories import (
- SqlAlchemyEnrollmentRepository,
- )
- from app.infrastructure.mysql.repositories import SqlAlchemyIdentityRepository
- from app.infrastructure.mysql.sessions import (
- DatabaseName,
- create_mysql_engine,
- create_session_factory,
- )
- @dataclass(frozen=True, slots=True)
- class BusinessCase:
- applicant: str
- insured: str
- relationship: str
- product_code: str
- plan_code: str
- status: str
- days_ago: int
- policy_status: str | None = None
- payment_status: str | None = None
- BASE_CASES: tuple[BusinessCase, ...] = (
- BusinessCase(
- "周明远", "周明远", "SELF", "MED-BASIC", "BASIC", "ISSUED", 0, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "林晓雨",
- "林建国",
- "PARENT",
- "MED-SENIOR",
- "SENIOR_STANDARD",
- "ISSUED",
- 1,
- "ACTIVE",
- "SUCCEEDED",
- ),
- BusinessCase(
- "陈子昂", "王思雅", "SPOUSE", "MED-UPGRADE", "STANDARD", "ISSUED", 2, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "赵清和", "赵安安", "CHILD", "ACC-FAMILY", "FAMILY", "ISSUED", 3, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "许文博", "许文博", "SELF", "ACC-FAMILY", "INDIVIDUAL", "ISSUED", 4, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "郑嘉宁",
- "郑国强",
- "PARENT",
- "MED-SENIOR",
- "SENIOR_STANDARD",
- "ISSUED",
- 5,
- "ACTIVE",
- "SUCCEEDED",
- ),
- BusinessCase(
- "孙语桐", "方俊杰", "SPOUSE", "MED-UPGRADE", "ENHANCED", "ISSUED", 7, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "何志诚", "何沐阳", "CHILD", "ACC-FAMILY", "FAMILY", "ISSUED", 9, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "蒋若溪", "蒋若溪", "SELF", "MED-UPGRADE", "STANDARD", "ISSUED", 12, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "唐景行",
- "唐德华",
- "PARENT",
- "MED-SENIOR",
- "SENIOR_STANDARD",
- "ISSUED",
- 15,
- "ACTIVE",
- "SUCCEEDED",
- ),
- BusinessCase(
- "邓舒涵", "宋嘉树", "SPOUSE", "MED-BASIC", "BASIC", "ISSUED", 20, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "梁书航", "梁小满", "CHILD", "ACC-FAMILY", "FAMILY", "ISSUED", 25, "ACTIVE", "SUCCEEDED"
- ),
- BusinessCase(
- "顾念安", "顾念安", "SELF", "MED-BASIC", "BASIC", "ISSUED", 30, "CANCELLED", "SUCCEEDED"
- ),
- BusinessCase(
- "马致远",
- "马国庆",
- "PARENT",
- "MED-SENIOR",
- "SENIOR_STANDARD",
- "ISSUED",
- 430,
- "EXPIRED",
- "SUCCEEDED",
- ),
- BusinessCase(
- "罗婉清", "罗婉清", "SELF", "MED-UPGRADE", "ENHANCED", "PAID", 1, None, "SUCCEEDED"
- ),
- BusinessCase("谢云舟", "沈知夏", "SPOUSE", "MED-BASIC", "BASIC", "PAID", 3, None, "SUCCEEDED"),
- BusinessCase(
- "苏曼青",
- "苏曼青",
- "SELF",
- "ACC-FAMILY",
- "INDIVIDUAL",
- "PENDING_PAYMENT",
- 0,
- None,
- "CREATED",
- ),
- BusinessCase(
- "白敬亭",
- "白国良",
- "PARENT",
- "MED-SENIOR",
- "SENIOR_STANDARD",
- "PENDING_PAYMENT",
- 2,
- None,
- None,
- ),
- BusinessCase(
- "陆星河", "陆小禾", "CHILD", "ACC-FAMILY", "FAMILY", "PENDING_PAYMENT", 5, None, "CREATED"
- ),
- BusinessCase(
- "秦知意", "秦知意", "SELF", "MED-UPGRADE", "STANDARD", "PENDING_PAYMENT", 8, None, None
- ),
- BusinessCase(
- "夏予川", "顾安然", "SPOUSE", "MED-BASIC", "BASIC", "PENDING_PAYMENT", 11, None, "CREATED"
- ),
- BusinessCase("叶听澜", "叶听澜", "SELF", "MED-BASIC", "BASIC", "CANCELLED", 4, None, "FAILED"),
- BusinessCase(
- "程砚秋",
- "程建军",
- "PARENT",
- "MED-SENIOR",
- "SENIOR_STANDARD",
- "CANCELLED",
- 13,
- None,
- "FAILED",
- ),
- BusinessCase("江望舒", "江小鱼", "CHILD", "ACC-FAMILY", "FAMILY", "CANCELLED", 21, None, None),
- )
- GENERATED_NAMES = (
- "沈亦辰",
- "温书瑶",
- "乔景川",
- "孟知微",
- "裴言澈",
- "楚星晚",
- "傅时安",
- "宋予宁",
- "贺云深",
- "姜晚晴",
- "顾承泽",
- "许清欢",
- "陆闻舟",
- "苏念初",
- "程屿森",
- "叶舒然",
- "周砚北",
- "林语棠",
- "陈遇白",
- "赵南乔",
- "唐慕川",
- "梁初夏",
- "郑怀瑾",
- "孙云舒",
- "何景明",
- "蒋依澜",
- "邓星野",
- "罗芷晴",
- "马明谦",
- "谢知夏",
- "秦远洲",
- "白若宁",
- "夏云川",
- "顾安禾",
- "叶庭深",
- "程书妍",
- "江叙白",
- "周晚意",
- "林嘉树",
- "陈思源",
- "赵安澜",
- "唐清越",
- "梁知行",
- "郑初晴",
- "孙景和",
- "何语心",
- "蒋承安",
- "邓若溪",
- )
- def _generated_business_cases() -> tuple[BusinessCase, ...]:
- relationships = ("SELF", "PARENT", "SPOUSE", "CHILD")
- product_plans = (
- ("MED-BASIC", "BASIC"),
- ("MED-BASIC", "BASIC"),
- ("MED-UPGRADE", "STANDARD"),
- ("MED-UPGRADE", "ENHANCED"),
- ("MED-SENIOR", "SENIOR_STANDARD"),
- ("ACC-FAMILY", "INDIVIDUAL"),
- ("ACC-FAMILY", "FAMILY"),
- )
- generated: list[BusinessCase] = []
- for offset, applicant in enumerate(GENERATED_NAMES, start=1):
- index = len(BASE_CASES) + offset
- relationship = relationships[index % len(relationships)]
- product_code, plan_code = product_plans[index % len(product_plans)]
- if product_code == "MED-SENIOR":
- relationship = "PARENT"
- selector = index % 20
- if selector in {0, 7, 14, 18}:
- status = "PENDING_PAYMENT"
- policy_status = None
- payment_status = "CREATED" if index % 2 else None
- elif selector in {9, 19}:
- status = "CANCELLED"
- policy_status = None
- payment_status = "FAILED" if index % 2 else None
- elif selector == 11:
- status = "PAID"
- policy_status = None
- payment_status = "SUCCEEDED"
- else:
- status = "ISSUED"
- policy_status = "ACTIVE"
- payment_status = "SUCCEEDED"
- insured = (
- applicant
- if relationship == "SELF"
- else GENERATED_NAMES[(offset + 11) % len(GENERATED_NAMES)]
- )
- generated.append(
- BusinessCase(
- applicant=applicant,
- insured=insured,
- relationship=relationship,
- product_code=product_code,
- plan_code=plan_code,
- status=status,
- days_ago=(index * 7 + (index // 3) * 5) % 120,
- policy_status=policy_status,
- payment_status=payment_status,
- )
- )
- return tuple(generated)
- CASES = BASE_CASES + _generated_business_cases()
- ATTRIBUTION_PATTERN: tuple[int | None, ...] = (
- 0,
- 0,
- 0,
- 1,
- 0,
- 1,
- 2,
- 0,
- 1,
- 2,
- 3,
- 0,
- 1,
- 2,
- 3,
- 4,
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- None,
- None,
- )
- LEAD_ONLY_COUNT = 42
- PREMIUMS = {
- ("MED-BASIC", "BASIC"): 23900,
- ("MED-UPGRADE", "STANDARD"): 39900,
- ("MED-UPGRADE", "ENHANCED"): 59900,
- ("MED-SENIOR", "SENIOR_STANDARD"): 19900,
- ("ACC-FAMILY", "INDIVIDUAL"): 9900,
- ("ACC-FAMILY", "FAMILY"): 15900,
- }
- def run_seed(settings: Settings | None = None) -> dict[str, int]:
- resolved_settings = settings or Settings()
- engine = create_mysql_engine(resolved_settings, DatabaseName.CORE)
- factory = create_session_factory(engine)
- identities = SqlAlchemyIdentityRepository(factory)
- enrollment = SqlAlchemyEnrollmentRepository(factory)
- attribution = SqlAlchemyAttributionRepository(factory)
- manifest = build_seed_manifest()
- for salesperson in manifest.salespersons:
- attribution.save_salesperson(salesperson)
- for seed_code in manifest.promotion_codes:
- attribution.save_promotion_code(seed_code)
- products = {product.product_code: product for product in manifest.products}
- published_versions = {
- product.product_code: next(
- version
- for version in manifest.versions
- if version.product_id == product.id and version.status == "PUBLISHED"
- )
- for product in manifest.products
- }
- anchor = datetime(2026, 7, 26, 16, 30, tzinfo=UTC)
- customer_ids: dict[str, str] = {}
- for index, case in enumerate(CASES, start=1):
- mobile = f"1886001{index:04d}"
- user_id = stable_id(f"business-user:{mobile}")
- customer_ids[mobile] = user_id
- identities.save_h5_user(
- H5User(
- id=user_id,
- mobile=mobile,
- mobile_masked=f"{mobile[:3]}****{mobile[-4:]}",
- display_name=case.applicant,
- status="ACTIVE",
- created_at=anchor - timedelta(days=case.days_ago + 8),
- )
- )
- promotion_index = ATTRIBUTION_PATTERN[(index - 1) % len(ATTRIBUTION_PATTERN)]
- attributed_code = (
- manifest.promotion_codes[promotion_index] if promotion_index is not None else None
- )
- if attributed_code is not None:
- attribution.save_lead(
- LeadAttribution(
- id=stable_id(f"business-lead:{index}"),
- h5_user_id=user_id,
- salesperson_id=attributed_code.salesperson_id,
- promotion_code_id=attributed_code.id,
- source_code=attributed_code.code,
- visit_count=1 + index % 4,
- first_touch_at=anchor - timedelta(days=case.days_ago + 8),
- last_touch_at=anchor - timedelta(days=case.days_ago + 1),
- )
- )
- product = products[case.product_code]
- version = published_versions[case.product_code]
- plan = next(item for item in version.plans if item.code == case.plan_code)
- created_at = anchor - timedelta(
- days=case.days_ago,
- hours=(index * 3) % 12,
- minutes=(index * 7) % 60,
- )
- premium_cents = PREMIUMS[(case.product_code, case.plan_code)]
- quote_id = stable_id(f"business-quote:{index}")
- draft_id = stable_id(f"business-draft:{index}")
- confirmation_id = stable_id(f"business-confirmation:{index}")
- order_id = stable_id(f"business-order:{index}")
- id_no_suffix = f"{index:04d}"
- applicant_snapshot = {
- "name": case.applicant,
- "id_no": f"51010419880101{id_no_suffix}",
- }
- insured_snapshot = {
- "name": case.insured,
- "id_no": f"51010419920102{id_no_suffix}",
- }
- enrollment.save_quote(
- Quote(
- id=quote_id,
- user_id=user_id,
- product_id=product.id,
- product_version_id=version.id,
- plan_id=plan.id,
- insured_age=65 if case.relationship == "PARENT" else 35,
- insured_region_code="510100",
- occupation_code="GENERAL",
- relationship=case.relationship,
- premium_cents=premium_cents,
- currency="CNY",
- rule_version="eligibility-v1",
- rate_version="rate-v1",
- status="USED",
- expires_at=created_at + timedelta(minutes=30),
- created_at=created_at - timedelta(minutes=12),
- )
- )
- enrollment.save_draft(
- EnrollmentDraft(
- id=draft_id,
- user_id=user_id,
- quote_id=quote_id,
- applicant=applicant_snapshot,
- insured=insured_snapshot,
- contact={"mobile": mobile},
- status="CONFIRMED",
- expires_at=created_at + timedelta(hours=24),
- created_at=created_at - timedelta(minutes=8),
- )
- )
- enrollment.save_confirmation(
- UserConfirmation(
- id=confirmation_id,
- user_id=user_id,
- draft_id=draft_id,
- token_hash=sha256(f"business-confirmation-token:{index}".encode()).hexdigest(),
- status="USED",
- expires_at=created_at + timedelta(minutes=10),
- created_at=created_at - timedelta(minutes=3),
- )
- )
- order = EnrollmentOrder(
- id=order_id,
- order_no=f"ORD-{created_at:%Y%m%d}-B{index:07d}",
- user_id=user_id,
- quote_id=quote_id,
- draft_id=draft_id,
- confirmation_id=confirmation_id,
- idempotency_key=f"business-order-{index:04d}",
- product_id=product.id,
- product_version_id=version.id,
- plan_id=plan.id,
- applicant_snapshot=applicant_snapshot,
- insured_snapshot=insured_snapshot,
- amount_cents=premium_cents,
- currency="CNY",
- status=case.status,
- created_at=created_at,
- )
- enrollment.save_order(order)
- if attributed_code is not None:
- attribution.save_order_attribution(
- OrderAttribution(
- id=stable_id(f"business-order-attribution:{index}"),
- order_id=order.id,
- h5_user_id=user_id,
- salesperson_id=attributed_code.salesperson_id,
- promotion_code_id=attributed_code.id,
- source_code=attributed_code.code,
- amount_cents=premium_cents,
- attributed_at=created_at,
- )
- )
- if case.payment_status is not None:
- payment_id = stable_id(f"business-payment:{index}")
- succeeded_at = (
- created_at + timedelta(minutes=2) if case.payment_status == "SUCCEEDED" else None
- )
- enrollment.save_payment(
- PaymentTransaction(
- id=payment_id,
- payment_no=f"PAY-{created_at:%Y%m%d}-B{index:07d}",
- order_id=order_id,
- user_id=user_id,
- transaction_type="PAYMENT",
- provider="LOCAL_MOCK_PAY",
- idempotency_key=f"business-payment-{index:04d}",
- amount_cents=premium_cents,
- currency="CNY",
- status=case.payment_status,
- provider_transaction_no=(
- f"MP-{created_at:%Y%m%d}-B{index:07d}"
- if case.payment_status == "SUCCEEDED"
- else None
- ),
- succeeded_at=succeeded_at,
- created_at=created_at + timedelta(minutes=1),
- )
- )
- if case.policy_status is not None:
- coverage_start = (created_at + timedelta(days=1)).date()
- coverage_end = coverage_start + timedelta(days=365)
- enrollment.save_policy(
- Policy(
- id=stable_id(f"business-policy:{index}"),
- policy_no=f"POL-{created_at:%Y%m%d}-B{index:07d}",
- order_id=order_id,
- user_id=user_id,
- product_id=product.id,
- product_version_id=version.id,
- plan_id=plan.id,
- premium_cents=premium_cents,
- currency="CNY",
- coverage_start=coverage_start,
- coverage_end=coverage_end,
- status=case.policy_status,
- issued_at=created_at + timedelta(minutes=4),
- )
- )
- for index in range(1, LEAD_ONLY_COUNT + 1):
- mobile = f"1887002{index:04d}"
- user_id = stable_id(f"business-lead-only-user:{mobile}")
- promotion_index = ATTRIBUTION_PATTERN[
- (index * 5 + index // 4) % (len(ATTRIBUTION_PATTERN) - 2)
- ]
- if promotion_index is None:
- continue
- promotion_code = manifest.promotion_codes[promotion_index]
- created_days_ago = (index * 11 + 3) % 150
- identities.save_h5_user(
- H5User(
- id=user_id,
- mobile=mobile,
- mobile_masked=f"{mobile[:3]}****{mobile[-4:]}",
- display_name=f"咨询客户{index:02d}",
- status="ACTIVE",
- created_at=anchor - timedelta(days=created_days_ago),
- )
- )
- attribution.save_lead(
- LeadAttribution(
- id=stable_id(f"business-lead-only:{index}"),
- h5_user_id=user_id,
- salesperson_id=promotion_code.salesperson_id,
- promotion_code_id=promotion_code.id,
- source_code=promotion_code.code,
- visit_count=1 + (index * 3) % 7,
- first_touch_at=anchor - timedelta(days=created_days_ago),
- last_touch_at=anchor - timedelta(days=max(0, created_days_ago - (index % 12))),
- )
- )
- engine.dispose()
- return {
- "customers": len(customer_ids) + LEAD_ONLY_COUNT,
- "orders": len(CASES),
- "policies": sum(case.policy_status is not None for case in CASES),
- "payments": sum(case.payment_status is not None for case in CASES),
- "leads": sum(
- ATTRIBUTION_PATTERN[(index - 1) % len(ATTRIBUTION_PATTERN)] is not None
- for index in range(1, len(CASES) + 1)
- )
- + LEAD_ONLY_COUNT,
- "attributed_orders": sum(
- ATTRIBUTION_PATTERN[(index - 1) % len(ATTRIBUTION_PATTERN)] is not None
- for index in range(1, len(CASES) + 1)
- ),
- }
- if __name__ == "__main__":
- result = run_seed()
- print(
- "业务数据写入完成:"
- f"{result['customers']} 位投保用户,"
- f"{result['orders']} 笔订单,"
- f"{result['payments']} 条支付流水,"
- f"{result['policies']} 张保单,"
- f"{result['leads']} 条推广线索,"
- f"{result['attributed_orders']} 笔推广归因订单。"
- )
|