prepare_frontend_accounts.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import asyncio
  2. import json
  3. import secrets
  4. from datetime import UTC, datetime, timedelta
  5. from uuid import UUID, uuid4
  6. from anyio import Path
  7. from sqlalchemy.ext.asyncio import async_sessionmaker
  8. from zhifu.core.config import Settings
  9. from zhifu.core.database import create_engine
  10. from zhifu.modules.after_sales.models import BusinessEvent, ExchangeRule, OrderProjection
  11. from zhifu.modules.billing.models import BillingGrant
  12. from zhifu.modules.identity.access_models import Principal, StoreGrant
  13. from zhifu.modules.identity.auth import password_hasher
  14. from zhifu.modules.identity.models import Store, Tenant
  15. async def main():
  16. settings = Settings()
  17. if settings.environment == "production":
  18. raise ValueError("开发账户只能在 development 或 test 环境创建")
  19. engine = create_engine(settings)
  20. sessions = async_sessionmaker(engine, expire_on_commit=False)
  21. tenant_id = UUID("ac700000-2026-4000-8000-000000000001")
  22. store_id = UUID("ac700000-2026-4000-8000-000000000002")
  23. target = Path(".work/frontend-accounts.json")
  24. # 数据保留在独立开发商家中;重复运行只更新该商家的演示订单观察时间。
  25. existing = (
  26. json.loads(await target.read_text(encoding="utf-8")) if await target.exists() else None
  27. )
  28. password = existing["password"] if existing else secrets.token_urlsafe(18)
  29. roles = {
  30. "customer01": (
  31. "CUSTOMER",
  32. ["orders:read", "tickets:read", "after_sales:create",
  33. "after_sales:confirm", "after_sales:cancel"],
  34. ),
  35. "staff01": (
  36. "STAFF",
  37. ["orders:read", "tickets:read", "after_sales:create", "after_sales:cancel",
  38. "conversations:read", "conversations:reply", "conversations:manage"],
  39. ),
  40. "supervisor01": ("STAFF", ["orders:read", "tickets:read", "approvals:decide"]),
  41. "admin01": (
  42. "STAFF",
  43. ["orders:read", "tickets:read", "rules:write", "knowledge:write", "billing:read",
  44. "billing:request", "billing:approve", "stores:manage", "staff:manage",
  45. "channels:manage", "agent:publish"],
  46. ),
  47. "finance02": ("STAFF", ["billing:read", "billing:request", "billing:approve"]),
  48. }
  49. actors = {}
  50. now = datetime.now(UTC).replace(tzinfo=None)
  51. try:
  52. async with sessions.begin() as session:
  53. tenant = await session.get(Tenant, tenant_id.bytes)
  54. if tenant is None:
  55. session.add(Tenant(id=tenant_id.bytes, name="前端开发独立商家"))
  56. await session.flush()
  57. session.add(Store(
  58. id=store_id.bytes, tenant_id=tenant_id.bytes, name="前端开发配件店"
  59. ))
  60. await session.flush()
  61. elif existing is None:
  62. raise ValueError("开发商家已存在,但本地账户文件缺失;请使用已有开发账户")
  63. for index, (username, (kind, permissions)) in enumerate(roles.items(), start=10):
  64. actor_id = UUID(f"ac700000-2026-4000-8000-{index:012d}")
  65. actors[username] = str(actor_id)
  66. principal = await session.get(Principal, actor_id.bytes)
  67. if principal is None:
  68. session.add(Principal(
  69. id=actor_id.bytes, tenant_id=tenant_id.bytes, username=username,
  70. password_hash=password_hasher.hash(password), kind=kind,
  71. ))
  72. await session.flush()
  73. for permission in permissions:
  74. session.add(StoreGrant(
  75. tenant_id=tenant_id.bytes, principal_id=actor_id.bytes,
  76. store_id=store_id.bytes, permission=permission,
  77. ))
  78. if permission.startswith("billing:"):
  79. session.add(BillingGrant(
  80. tenant_id=tenant_id.bytes, principal_id=actor_id.bytes,
  81. permission=permission,
  82. ))
  83. elif username == "admin01":
  84. # 开发管理员补充本次管理接口权限,其他账户授权保持原有配置。
  85. for permission in (
  86. "stores:manage", "staff:manage", "channels:manage", "agent:publish"
  87. ):
  88. if await session.get(
  89. StoreGrant,
  90. (tenant_id.bytes, actor_id.bytes, store_id.bytes, permission),
  91. ) is None:
  92. session.add(StoreGrant(
  93. tenant_id=tenant_id.bytes, principal_id=actor_id.bytes,
  94. store_id=store_id.bytes, permission=permission,
  95. ))
  96. await session.flush()
  97. order_id = UUID("ac700000-2026-4000-8000-000000000003")
  98. order = await session.get(OrderProjection, order_id.bytes)
  99. if order is None:
  100. session.add(OrderProjection(
  101. id=order_id.bytes, tenant_id=tenant_id.bytes, store_id=store_id.bytes,
  102. subject_id=UUID(actors["customer01"]).bytes,
  103. source_system="frontend-development", source_version=1,
  104. item_id="keyboard-01", product_type="keyboard", available_quantity=10,
  105. signed_at=now - timedelta(days=1), observed_at=now,
  106. ))
  107. else:
  108. order.observed_at = now
  109. rule = await session.get(ExchangeRule, (tenant_id.bytes, store_id.bytes, 1))
  110. if rule is None:
  111. session.add(ExchangeRule(
  112. tenant_id=tenant_id.bytes, store_id=store_id.bytes, version=1,
  113. window_days=7, product_types=["keyboard"],
  114. citation="签收七天内的键盘故障支持申请换货,以当前审批与履约结果为准。",
  115. ))
  116. session.add(BusinessEvent(
  117. id=uuid4().bytes, tenant_id=tenant_id.bytes, actor_id=UUID(int=0).bytes,
  118. aggregate_id=tenant_id.bytes, aggregate_version=1,
  119. event_type="development.frontend_accounts", request_id=str(uuid4()),
  120. payload={"source": "prepare_frontend_accounts", "accounts": list(roles)},
  121. occurred_at=now,
  122. ))
  123. await target.parent.mkdir(exist_ok=True)
  124. await target.write_text(
  125. json.dumps(
  126. {"tenant_id": str(tenant_id), "store_id": str(store_id),
  127. "order_id": str(order_id), "password": password, "actors": actors},
  128. ensure_ascii=False, indent=2,
  129. ),
  130. encoding="utf-8",
  131. )
  132. print("开发账户信息已保存至 .work/frontend-accounts.json;未输出密码。")
  133. finally:
  134. await engine.dispose()
  135. asyncio.run(main())