import hashlib import json from collections.abc import Awaitable, Callable from typing import Any from sqlalchemy import select from sqlalchemy.dialects.mysql import insert from sqlalchemy.ext.asyncio import AsyncSession from app.core.contracts import RequestContext from app.core.errors import IdempotencyConflictError, ValidationAgentError from app.infrastructure.db import SessionFactory from app.repository.platform_repository import PlatformRepository def digest(value: Any) -> str: return hashlib.sha256(json.dumps(value, sort_keys=True, default=str, ensure_ascii=False).encode()).hexdigest() class ApiTransactionService: async def execute( self, context: RequestContext, scope: str, key: str | None, body: Any, action: Callable[[AsyncSession], Awaitable[dict[str, Any]]], ) -> dict[str, Any]: if key is None or not 16 <= len(key) <= 128 or not key.isascii(): raise ValidationAgentError("必须提供 16-128 位 ASCII Idempotency-Key") async with SessionFactory() as session, session.begin(): table = await PlatformRepository(session).table("api_request_receipt") statement = insert(table).values(user_id=int(context.user_id), scope_hash=digest(scope), idempotency_key=key, request_hash=digest(body)) await session.execute(statement.on_duplicate_key_update(id=table.c.id)) row = (await session.execute(select(table).where( table.c.user_id == int(context.user_id), table.c.scope_hash == digest(scope), table.c.idempotency_key == key).with_for_update())).mappings().one() if row["request_hash"] != digest(body): raise IdempotencyConflictError("同一幂等键对应不同请求") if row["response_json"] is not None: return dict(row["response_json"]) response = await action(session) await session.execute(table.update().where(table.c.id == row["id"]) .values(response_json=response)) return response