seed.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. from dataclasses import dataclass
  2. from datetime import UTC, datetime, timedelta
  3. from hashlib import sha256
  4. from app.core.config import Settings
  5. from app.core.passwords import PasswordService
  6. from app.domains.attribution.models import PromotionCode, Salesperson
  7. from app.domains.catalog.models import Plan, Product, ProductChangeLog, ProductVersion
  8. from app.domains.identity.models import AdminUser
  9. from app.infrastructure.mysql.attribution_repositories import (
  10. SqlAlchemyAttributionRepository,
  11. )
  12. from app.infrastructure.mysql.repositories import (
  13. SqlAlchemyCatalogRepository,
  14. SqlAlchemyIdentityRepository,
  15. )
  16. from app.infrastructure.mysql.sessions import (
  17. DatabaseName,
  18. create_mysql_engine,
  19. create_session_factory,
  20. )
  21. INITIAL_ADMIN_PASSWORD = "zaq1XSW@"
  22. @dataclass(frozen=True, slots=True)
  23. class SeedManifest:
  24. admin_users: tuple[AdminUser, ...]
  25. products: tuple[Product, ...]
  26. versions: tuple[ProductVersion, ...]
  27. salespersons: tuple[Salesperson, ...]
  28. promotion_codes: tuple[PromotionCode, ...]
  29. @dataclass(frozen=True, slots=True)
  30. class PlanConfig:
  31. summary: str
  32. premium_cents: int
  33. coverage_amount_cents: int
  34. min_age: int
  35. max_age: int
  36. def stable_id(value: str) -> str:
  37. return sha256(value.encode("utf-8")).hexdigest()[:26].upper()
  38. def build_seed_manifest(password_hash: str = "HASHED_DURING_DATABASE_SEED") -> SeedManifest:
  39. products = (
  40. _product("MED-BASIC", "蓉惠基础医疗险", "MEDICAL"),
  41. _product("MED-UPGRADE", "蓉惠升级医疗险", "MEDICAL"),
  42. _product("MED-SENIOR", "银龄守护医疗险", "MEDICAL"),
  43. _product("ACC-FAMILY", "家庭意外保障险", "ACCIDENT"),
  44. )
  45. product_ids = {product.product_code: product.id for product in products}
  46. versions = (
  47. _version(product_ids, "MED-BASIC", "1.0.0", "EXPIRED", 2023, 2024, ("BASIC",)),
  48. _version(product_ids, "MED-BASIC", "2.0.0", "PUBLISHED", 2025, 2029, ("BASIC",)),
  49. _version(product_ids, "MED-BASIC", "3.0.0", "DRAFT", 2030, None, ("BASIC",)),
  50. _version(
  51. product_ids,
  52. "MED-UPGRADE",
  53. "1.0.0",
  54. "PUBLISHED",
  55. 2025,
  56. 2029,
  57. ("STANDARD", "ENHANCED"),
  58. ),
  59. _version(
  60. product_ids,
  61. "MED-UPGRADE",
  62. "1.1.0",
  63. "DRAFT",
  64. 2030,
  65. None,
  66. ("STANDARD", "ENHANCED"),
  67. ),
  68. _version(
  69. product_ids,
  70. "MED-SENIOR",
  71. "1.0.0",
  72. "PUBLISHED",
  73. 2025,
  74. 2029,
  75. ("SENIOR_STANDARD",),
  76. ),
  77. _version(
  78. product_ids,
  79. "ACC-FAMILY",
  80. "1.0.0",
  81. "PUBLISHED",
  82. 2025,
  83. 2029,
  84. ("INDIVIDUAL", "FAMILY"),
  85. ),
  86. )
  87. admin_users = (
  88. _admin("admin", "超级管理员", "SUPER_ADMIN", "ALL", ("*",), password_hash),
  89. _admin(
  90. "operator01",
  91. "运营人员",
  92. "OPERATOR",
  93. "MASKED_ALL",
  94. ("dashboard:read", "order:read", "policy:read", "user:read"),
  95. password_hash,
  96. ),
  97. _admin(
  98. "reviewer01",
  99. "审核人员",
  100. "REVIEWER",
  101. "READ_ONLY",
  102. ("order:read", "policy:read"),
  103. password_hash,
  104. ),
  105. _admin(
  106. "sales_a",
  107. "张伟",
  108. "SALESPERSON",
  109. "SELF",
  110. (
  111. "dashboard:read",
  112. "order:read",
  113. "policy:read",
  114. "product:read",
  115. "attribution:read",
  116. ),
  117. password_hash,
  118. ),
  119. _admin(
  120. "sales_b",
  121. "李娜",
  122. "SALESPERSON",
  123. "SELF",
  124. (
  125. "dashboard:read",
  126. "order:read",
  127. "policy:read",
  128. "product:read",
  129. "attribution:read",
  130. ),
  131. password_hash,
  132. ),
  133. _admin(
  134. "sales_c",
  135. "王磊",
  136. "SALESPERSON",
  137. "SELF",
  138. (
  139. "dashboard:read",
  140. "order:read",
  141. "policy:read",
  142. "product:read",
  143. "attribution:read",
  144. ),
  145. password_hash,
  146. ),
  147. _admin(
  148. "sales_d",
  149. "陈晨",
  150. "SALESPERSON",
  151. "SELF",
  152. (
  153. "dashboard:read",
  154. "order:read",
  155. "policy:read",
  156. "product:read",
  157. "attribution:read",
  158. ),
  159. password_hash,
  160. ),
  161. _admin(
  162. "sales_e",
  163. "刘洋",
  164. "SALESPERSON",
  165. "SELF",
  166. (
  167. "dashboard:read",
  168. "order:read",
  169. "policy:read",
  170. "product:read",
  171. "attribution:read",
  172. ),
  173. password_hash,
  174. ),
  175. _admin(
  176. "sales_f",
  177. "赵敏",
  178. "SALESPERSON",
  179. "SELF",
  180. (
  181. "dashboard:read",
  182. "order:read",
  183. "policy:read",
  184. "product:read",
  185. "attribution:read",
  186. ),
  187. password_hash,
  188. ),
  189. )
  190. created_at = datetime(2026, 1, 1, tzinfo=UTC)
  191. salespersons = (
  192. Salesperson(
  193. id=stable_id("salesperson:sales_a"),
  194. admin_user_id=stable_id("admin:sales_a"),
  195. code="SALES-A",
  196. name="张伟",
  197. status="ACTIVE",
  198. created_at=created_at,
  199. ),
  200. Salesperson(
  201. id=stable_id("salesperson:sales_b"),
  202. admin_user_id=stable_id("admin:sales_b"),
  203. code="SALES-B",
  204. name="李娜",
  205. status="ACTIVE",
  206. created_at=created_at,
  207. ),
  208. Salesperson(
  209. id=stable_id("salesperson:sales_c"),
  210. admin_user_id=stable_id("admin:sales_c"),
  211. code="SALES-C",
  212. name="王磊",
  213. status="ACTIVE",
  214. created_at=created_at,
  215. ),
  216. Salesperson(
  217. id=stable_id("salesperson:sales_d"),
  218. admin_user_id=stable_id("admin:sales_d"),
  219. code="SALES-D",
  220. name="陈晨",
  221. status="ACTIVE",
  222. created_at=created_at,
  223. ),
  224. Salesperson(
  225. id=stable_id("salesperson:sales_e"),
  226. admin_user_id=stable_id("admin:sales_e"),
  227. code="SALES-E",
  228. name="刘洋",
  229. status="ACTIVE",
  230. created_at=created_at,
  231. ),
  232. Salesperson(
  233. id=stable_id("salesperson:sales_f"),
  234. admin_user_id=stable_id("admin:sales_f"),
  235. code="SALES-F",
  236. name="赵敏",
  237. status="ACTIVE",
  238. created_at=created_at,
  239. ),
  240. )
  241. promotion_codes = (
  242. PromotionCode(
  243. id=stable_id("promotion:ZBT-SALES-A"),
  244. code="ZBT-SALES-A",
  245. salesperson_id=salespersons[0].id,
  246. channel="GROUND_PROMOTION",
  247. status="ACTIVE",
  248. created_at=created_at,
  249. ),
  250. PromotionCode(
  251. id=stable_id("promotion:ZBT-SALES-B"),
  252. code="ZBT-SALES-B",
  253. salesperson_id=salespersons[1].id,
  254. channel="COMMUNITY",
  255. status="ACTIVE",
  256. created_at=created_at,
  257. ),
  258. PromotionCode(
  259. id=stable_id("promotion:ZBT-SALES-C"),
  260. code="ZBT-SALES-C",
  261. salesperson_id=salespersons[2].id,
  262. channel="ENTERPRISE",
  263. status="ACTIVE",
  264. created_at=created_at,
  265. ),
  266. PromotionCode(
  267. id=stable_id("promotion:ZBT-SALES-D"),
  268. code="ZBT-SALES-D",
  269. salesperson_id=salespersons[3].id,
  270. channel="PARTNER",
  271. status="ACTIVE",
  272. created_at=created_at,
  273. ),
  274. PromotionCode(
  275. id=stable_id("promotion:ZBT-SALES-E"),
  276. code="ZBT-SALES-E",
  277. salesperson_id=salespersons[4].id,
  278. channel="ONLINE_REFERRAL",
  279. status="ACTIVE",
  280. created_at=created_at,
  281. ),
  282. PromotionCode(
  283. id=stable_id("promotion:ZBT-SALES-F"),
  284. code="ZBT-SALES-F",
  285. salesperson_id=salespersons[5].id,
  286. channel="STORE",
  287. status="ACTIVE",
  288. created_at=created_at,
  289. ),
  290. )
  291. return SeedManifest(
  292. admin_users=admin_users,
  293. products=products,
  294. versions=versions,
  295. salespersons=salespersons,
  296. promotion_codes=promotion_codes,
  297. )
  298. def run_seed(settings: Settings | None = None) -> SeedManifest:
  299. resolved_settings = settings or Settings()
  300. password_hash = PasswordService().hash(INITIAL_ADMIN_PASSWORD)
  301. manifest = build_seed_manifest(password_hash)
  302. engine = create_mysql_engine(resolved_settings, DatabaseName.CORE)
  303. factory = create_session_factory(engine)
  304. identities = SqlAlchemyIdentityRepository(factory)
  305. catalog = SqlAlchemyCatalogRepository(factory)
  306. attribution = SqlAlchemyAttributionRepository(factory)
  307. for admin_user in manifest.admin_users:
  308. identities.save_admin_user(admin_user)
  309. for product in manifest.products:
  310. catalog.save_product(product)
  311. for version in manifest.versions:
  312. catalog.save_version(version)
  313. audit_anchor = datetime(2026, 7, 1, 9, 0, tzinfo=UTC)
  314. for index, product in enumerate(manifest.products):
  315. catalog.save_change_log(
  316. ProductChangeLog(
  317. id=stable_id(f"product-log:created:{product.id}"),
  318. product_id=product.id,
  319. version_id=None,
  320. action="PRODUCT_CREATED",
  321. actor_id="SYSTEM",
  322. actor_name="系统初始化",
  323. detail={"product_code": product.product_code},
  324. created_at=audit_anchor - timedelta(days=120 - index),
  325. )
  326. )
  327. for index, version in enumerate(manifest.versions):
  328. catalog.save_change_log(
  329. ProductChangeLog(
  330. id=stable_id(f"product-log:version:{version.id}"),
  331. product_id=version.product_id,
  332. version_id=version.id,
  333. action=(
  334. "VERSION_DRAFT_CREATED"
  335. if version.status == "DRAFT"
  336. else "VERSION_IMPORTED"
  337. ),
  338. actor_id="SYSTEM",
  339. actor_name="系统初始化",
  340. detail={"version_no": version.version_no, "status": version.status},
  341. created_at=audit_anchor - timedelta(days=40 - index * 3),
  342. )
  343. )
  344. for salesperson in manifest.salespersons:
  345. attribution.save_salesperson(salesperson)
  346. for promotion_code in manifest.promotion_codes:
  347. attribution.save_promotion_code(promotion_code)
  348. engine.dispose()
  349. return manifest
  350. def _product(code: str, name: str, category: str) -> Product:
  351. summaries = {
  352. "MED-BASIC": "覆盖常见住院医疗费用,为年轻家庭提供简洁实用的基础保障。",
  353. "MED-UPGRADE": "提高医疗费用保障额度,并提供标准与升级两档方案灵活选择。",
  354. "MED-SENIOR": "面向中老年人群的住院医疗保障,覆盖50至75周岁的投保需求。",
  355. "ACC-FAMILY": "覆盖日常意外风险,可按个人或家庭方案为家人统一配置保障。",
  356. }
  357. return Product(
  358. id=stable_id(f"product:{code}"),
  359. product_code=code,
  360. name=name,
  361. category=category,
  362. summary=summaries[code],
  363. status="ACTIVE",
  364. )
  365. def _version(
  366. product_ids: dict[str, str],
  367. product_code: str,
  368. version_no: str,
  369. status: str,
  370. start_year: int,
  371. end_year: int | None,
  372. plan_codes: tuple[str, ...],
  373. ) -> ProductVersion:
  374. version_key = f"{product_code}:{version_no}"
  375. return ProductVersion(
  376. id=stable_id(f"version:{version_key}"),
  377. product_id=product_ids[product_code],
  378. version_no=version_no,
  379. status=status,
  380. effective_from=datetime(start_year, 1, 1, tzinfo=UTC),
  381. effective_to=(
  382. datetime(end_year, 12, 31, 23, 59, 59, tzinfo=UTC) if end_year is not None else None
  383. ),
  384. terms_summary=(
  385. "本产品为一年期保障,等待期、责任免除及赔付比例以电子保险条款为准;"
  386. "投保前请如实完成健康告知并确认被保人信息。"
  387. ),
  388. plans=tuple(
  389. _plan(version_key, code)
  390. for code in plan_codes
  391. ),
  392. )
  393. def _plan_name(code: str) -> str:
  394. return {
  395. "BASIC": "基础计划",
  396. "STANDARD": "标准计划",
  397. "ENHANCED": "升级计划",
  398. "SENIOR_STANDARD": "银龄标准计划",
  399. "INDIVIDUAL": "个人计划",
  400. "FAMILY": "家庭计划",
  401. }[code]
  402. def _plan(version_key: str, code: str) -> Plan:
  403. config = _plan_config(code)
  404. return Plan(
  405. id=stable_id(f"plan:{version_key}:{code}"),
  406. code=code,
  407. name=_plan_name(code),
  408. summary=config.summary,
  409. premium_cents=config.premium_cents,
  410. coverage_amount_cents=config.coverage_amount_cents,
  411. min_age=config.min_age,
  412. max_age=config.max_age,
  413. )
  414. def _plan_config(code: str) -> PlanConfig:
  415. return {
  416. "BASIC": PlanConfig(
  417. "覆盖住院医疗及约定门诊费用,适合首次配置医疗保障的人群。",
  418. 23900,
  419. 200_000_000,
  420. 0,
  421. 65,
  422. ),
  423. "STANDARD": PlanConfig(
  424. "提升医疗保障额度,兼顾常见疾病住院和重大疾病医疗支出。",
  425. 39900,
  426. 300_000_000,
  427. 0,
  428. 65,
  429. ),
  430. "ENHANCED": PlanConfig(
  431. "提供更高医疗额度和扩展责任,适合重视全面保障的家庭。",
  432. 59900,
  433. 600_000_000,
  434. 0,
  435. 65,
  436. ),
  437. "SENIOR_STANDARD": PlanConfig(
  438. "面向中老年人群的住院医疗计划,覆盖高发疾病住院费用。",
  439. 19900,
  440. 100_000_000,
  441. 50,
  442. 75,
  443. ),
  444. "INDIVIDUAL": PlanConfig(
  445. "覆盖个人日常意外身故、伤残及意外医疗责任。",
  446. 9900,
  447. 50_000_000,
  448. 0,
  449. 70,
  450. ),
  451. "FAMILY": PlanConfig(
  452. "一份计划覆盖家庭成员的常见意外风险,适合家庭统一配置。",
  453. 15900,
  454. 100_000_000,
  455. 0,
  456. 70,
  457. ),
  458. }[code]
  459. def _admin(
  460. username: str,
  461. display_name: str,
  462. role: str,
  463. data_scope: str,
  464. permissions: tuple[str, ...],
  465. password_hash: str,
  466. ) -> AdminUser:
  467. return AdminUser(
  468. id=stable_id(f"admin:{username}"),
  469. username=username,
  470. password_hash=password_hash,
  471. display_name=display_name,
  472. status="ACTIVE",
  473. roles=(role,),
  474. permissions=permissions,
  475. data_scope=data_scope,
  476. )
  477. if __name__ == "__main__":
  478. seeded = run_seed()
  479. print(
  480. f"已写入 {len(seeded.admin_users)} 个后台账号、"
  481. f"{len(seeded.products)} 个产品、{len(seeded.versions)} 个产品版本、"
  482. f"{len(seeded.promotion_codes)} 个推广码。"
  483. )