Files
group_fqcd_jr/app/service/api_transaction_service.py
T

44 lines
2.1 KiB
Python
Raw Normal View History

2026-09-09 21:55:37 +08:00
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