prepare_storefront_data.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import asyncio
  2. import json
  3. import secrets
  4. from uuid import UUID
  5. import httpx
  6. from anyio import Path as AsyncPath
  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.identity.access_models import Principal, StoreGrant
  11. from zhifu.modules.identity.auth import password_hasher
  12. from zhifu.modules.identity.models import Store, Tenant # noqa: F401
  13. from zhifu.modules.storefront.models import CatalogCategory, CatalogProduct
  14. async def main():
  15. settings = Settings()
  16. if settings.environment == "production":
  17. raise ValueError("开发商品只能在 development 或 test 环境创建")
  18. accounts = json.loads(
  19. await AsyncPath(".work/frontend-accounts.json").read_text(encoding="utf-8")
  20. )
  21. tenant_id, store_id = UUID(accounts["tenant_id"]).bytes, UUID(accounts["store_id"]).bytes
  22. asset_dir = AsyncPath("frontend/public/products")
  23. await asset_dir.mkdir(parents=True, exist_ok=True)
  24. products = [
  25. ("keyboard-01", "keyboard", "办公键盘",
  26. "适合日常文字输入的桌面键盘。使用前连接电脑并检查输入功能。",
  27. {"款式": "标准款", "分类": "键盘"}, 17479949,
  28. "https://www.pexels.com/photo/computer-keyboard-and-a-mouse-on-the-desk-17479949/"),
  29. ("mouse-01", "mouse", "无线鼠标", "日常桌面操作用鼠标。首次使用请依据随附说明完成连接。",
  30. {"款式": "标准款", "分类": "鼠标"}, 28297695,
  31. "https://www.pexels.com/photo/modern-workspace-with-computer-accessories-28297695/"),
  32. ("desk-01", "desk", "桌面配件", "整理电脑周边的桌面配件,适合搭配键盘和鼠标使用。",
  33. {"款式": "标准款", "分类": "桌面配件"}, 15372901,
  34. "https://www.pexels.com/photo/computer-accessories-on-mouse-pad-on-desk-15372901/"),
  35. ]
  36. # 下载官方原图资源,不进行图像修改;来源与授权记录保存在仓库文档中。
  37. async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
  38. for code, _, _, _, _, photo, _ in products:
  39. target = asset_dir / f"{code}.jpg"
  40. if not await target.exists():
  41. response = await client.get(
  42. f"https://images.pexels.com/photos/{photo}/pexels-photo-{photo}.jpeg"
  43. )
  44. response.raise_for_status()
  45. if not response.headers.get("content-type", "").startswith("image/"):
  46. raise ValueError("图片来源返回了非图片内容")
  47. await target.write_bytes(response.content)
  48. credentials_path = AsyncPath(".work/storefront-channel.json")
  49. credentials_exist = await credentials_path.exists()
  50. credentials = json.loads(
  51. await credentials_path.read_text(encoding="utf-8")
  52. ) if credentials_exist else {
  53. "tenant_id": accounts["tenant_id"], "username": "channel-service01",
  54. "password": secrets.token_urlsafe(24), "store_id": accounts["store_id"],
  55. }
  56. engine = create_engine(settings)
  57. sessions = async_sessionmaker(engine, expire_on_commit=False)
  58. try:
  59. async with sessions.begin() as session:
  60. if await session.get(Store, store_id) is None:
  61. raise ValueError("请先准备前端开发账户")
  62. categories = [("keyboard", "键盘"), ("mouse", "鼠标"), ("desk", "桌面配件")]
  63. for position, (code, name) in enumerate(categories):
  64. if await session.get(CatalogCategory, (tenant_id, store_id, code)) is None:
  65. session.add(CatalogCategory(tenant_id=tenant_id, store_id=store_id,
  66. code=code, name=name, position=position))
  67. await session.flush()
  68. for code, category, name, description, specifications, _, source in products:
  69. if await session.get(CatalogProduct, (tenant_id, store_id, code)) is None:
  70. session.add(CatalogProduct(
  71. tenant_id=tenant_id, store_id=store_id, id=code, category_code=category,
  72. name=name, description=description, specifications=specifications,
  73. delivery="配送安排以店铺订单约定为准", image_url=f"/products/{code}.jpg",
  74. image_source=source, status="ACTIVE",
  75. ))
  76. actor_id = UUID("ac700000-2026-4000-8000-000000000020").bytes
  77. actor = await session.get(Principal, actor_id)
  78. if actor is None:
  79. session.add(Principal(
  80. id=actor_id, tenant_id=tenant_id, username=credentials["username"],
  81. password_hash=password_hasher.hash(credentials["password"]), kind="SERVICE",
  82. ))
  83. await session.flush()
  84. for permission in ["identities:verify", "tickets:read"]:
  85. session.add(StoreGrant(tenant_id=tenant_id, store_id=store_id,
  86. principal_id=actor_id, permission=permission))
  87. elif not credentials_exist:
  88. raise ValueError("渠道账户已存在但本地凭据缺失,请恢复原凭据")
  89. customer_id = UUID("ac700000-2026-4000-8000-000000000021").bytes
  90. if await session.get(Principal, customer_id) is None:
  91. session.add(Principal(
  92. id=customer_id, tenant_id=tenant_id, username="storefront-customer02",
  93. password_hash=password_hasher.hash(accounts["password"]), kind="CUSTOMER",
  94. ))
  95. await session.flush()
  96. for permission in ["orders:read", "tickets:read"]:
  97. session.add(StoreGrant(tenant_id=tenant_id, store_id=store_id,
  98. principal_id=customer_id, permission=permission))
  99. await credentials_path.write_text(
  100. json.dumps(credentials, ensure_ascii=False, indent=2), encoding="utf-8"
  101. )
  102. print("3 个分类、3 个商品、渠道服务和隔离验证客户已保存;保留已有经营资料。")
  103. finally:
  104. await engine.dispose()
  105. asyncio.run(main())