44 lines
2.1 KiB
Python
44 lines
2.1 KiB
Python
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
|