"""identity 域 service. 架构文档 §4.2: ``identity`` 不得依赖其他业务域 —— 登录链路不能被业务拖累。 import-linter enforces this (contract "identity must not depend on any other business domain"). V1 scope still missing here: 华为账号登录 (FR-ACC-01)、手机号验证码 (FR-ACC-04)、 双令牌与撤销 (FR-ACC-05)、游客合并 (FR-ACC-03)、注销 (FR-ACC-09)、未成年判定 (FR-ACC-11). See packages/identity/README.md. """ from __future__ import annotations from datetime import UTC, datetime from sqlalchemy.ext.asyncio import AsyncSession from packages.domain.ids import next_id from packages.identity import repo from packages.shared.errors import AppError, ErrorCode from packages.shared.models import Consent, User, UserIdentity #: Provider used by the dev-only token endpoint. Never valid in prod. DEV_PROVIDER = "dev" class IdentityService: def __init__(self, session: AsyncSession) -> None: self._session = session async def get_user(self, user_id: int) -> User: user = await repo.get_user(self._session, user_id) if user is None or user.deleted_at is not None or user.status != "active": raise AppError(ErrorCode.UNAUTHENTICATED, message="账号不存在或已注销") return user async def get_or_create_by_external_identity( self, *, provider: str, union_id: str, open_id: str | None = None, register_source: str = "unknown", nickname: str | None = None, ) -> User: """Map an external identity onto an internal ``user_id`` (FR-ACC-01). The internal id is deliberately decoupled from the provider id so a second login channel can be attached later without a data migration. """ identity = await repo.find_identity(self._session, provider=provider, union_id=union_id) if identity is not None: return await self.get_user(identity.user_id) user = repo.add_user( self._session, User( user_id=next_id(), status="active", nickname=nickname, register_source=register_source, last_active_at=datetime.now(UTC), ), ) repo.add_identity( self._session, UserIdentity( identity_id=next_id(), user_id=user.user_id, provider=provider, union_id=union_id, open_id=open_id, ), ) await self._session.flush() return user async def record_consent( self, *, user_id: int, consent_type: str, doc_version: str, client_ip: str | None = None, user_agent: str | None = None, ) -> Consent: """CR-01 留证。人脸的单独同意 (``consent_type='face'``) 必须独立成行。""" consent = repo.add_consent( self._session, Consent( consent_id=next_id(), user_id=user_id, consent_type=consent_type, doc_version=doc_version, client_ip=client_ip, ua=user_agent, ), ) await self._session.flush() return consent