feat: 实现起卦、设置与积分系统

This commit is contained in:
qzl
2026-04-03 16:56:47 +08:00
parent 31594558eb
commit f245eec5f6
170 changed files with 20728 additions and 328 deletions
+1
View File
@@ -0,0 +1 @@
from __future__ import annotations
+58
View File
@@ -0,0 +1,58 @@
from __future__ import annotations
from uuid import UUID
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from models.points_ledger import PointsLedger
from models.user_points import UserPoints
from schemas.shared.points import ApplyPointsChangeCommand
class PointsRepository:
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_or_create_user_points_for_update(
self, *, user_id: UUID
) -> UserPoints:
insert_stmt = (
insert(UserPoints)
.values(user_id=user_id)
.on_conflict_do_nothing(index_elements=[UserPoints.user_id])
)
await self._session.execute(insert_stmt)
stmt = select(UserPoints).where(UserPoints.user_id == user_id).with_for_update()
return (await self._session.execute(stmt)).scalar_one()
async def has_ledger_event(self, *, user_id: UUID, event_id: str) -> bool:
stmt = select(PointsLedger.id).where(
PointsLedger.user_id == user_id,
PointsLedger.event_id == event_id,
)
row = (await self._session.execute(stmt)).scalar_one_or_none()
return row is not None
async def append_ledger(
self,
*,
command: ApplyPointsChangeCommand,
balance_after: int,
) -> None:
entry = PointsLedger(
user_id=command.user_id,
direction=command.direction,
amount=command.amount,
balance_after=balance_after,
change_type=command.change_type.value,
biz_type=command.biz_type.value if command.biz_type is not None else None,
biz_id=command.biz_id,
event_id=command.event_id,
operator_id=command.operator_id,
metadata_json=command.metadata.model_dump(mode="json", exclude_none=True),
)
self._session.add(entry)
await self._session.flush()
+132
View File
@@ -0,0 +1,132 @@
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
import hashlib
from uuid import UUID, uuid4
from core.http.errors import ApiProblemError, problem_payload
from schemas.domain.points import ConsumeLedgerMetadata, PointsChargeSnapshot
from schemas.enums import PointsBizType, PointsChangeType, PointsOperatorType
from schemas.shared.points import ApplyPointsChangeCommand
from v1.points.repository import PointsRepository
RUN_POINTS_COST = 20
@dataclass(frozen=True)
class RunChargeResult:
charged: bool
amount: int
balance_after: int
event_id: str
class PointsService:
def __init__(self, repository: PointsRepository) -> None:
self._repository = repository
async def ensure_run_points_available(
self,
*,
user_id: UUID,
) -> int:
account = await self._repository.get_or_create_user_points_for_update(
user_id=user_id
)
balance = int(account.balance)
frozen_balance = int(account.frozen_balance)
available = balance - frozen_balance
if available < RUN_POINTS_COST:
raise ApiProblemError(
status_code=402,
detail=problem_payload(
code="POINTS_INSUFFICIENT_BALANCE",
detail="Insufficient points for this run",
params={
"required": RUN_POINTS_COST,
"available": max(available, 0),
},
),
)
return available
async def consume_successful_run_points(
self,
*,
user_id: UUID,
session_id: UUID,
run_id: str,
operator_id: UUID | None,
) -> RunChargeResult:
event_source = f"{session_id}:{run_id}".encode("utf-8")
event_hash = hashlib.sha1(event_source).hexdigest()
event_id = f"chat.run.success:{event_hash}"
if await self._repository.has_ledger_event(user_id=user_id, event_id=event_id):
account = await self._repository.get_or_create_user_points_for_update(
user_id=user_id
)
return RunChargeResult(
charged=False,
amount=0,
balance_after=int(account.balance),
event_id=event_id,
)
account = await self._repository.get_or_create_user_points_for_update(
user_id=user_id
)
balance = int(account.balance)
frozen_balance = int(account.frozen_balance)
available = balance - frozen_balance
if available < RUN_POINTS_COST:
raise ApiProblemError(
status_code=402,
detail=problem_payload(
code="POINTS_INSUFFICIENT_BALANCE",
detail="Insufficient points for this run",
params={
"required": RUN_POINTS_COST,
"available": max(available, 0),
},
),
)
account.balance = balance - RUN_POINTS_COST
account.lifetime_spent = int(account.lifetime_spent) + RUN_POINTS_COST
account.version = int(account.version) + 1
metadata = ConsumeLedgerMetadata(
operator_type=PointsOperatorType.USER,
run_id=run_id,
charge=PointsChargeSnapshot(
message_id=uuid4(),
message_seq=1,
model_code="agent_run",
input_tokens=0,
output_tokens=0,
cost=Decimal("0"),
),
ext={"source": "run_success"},
)
command = ApplyPointsChangeCommand(
user_id=user_id,
change_type=PointsChangeType.CONSUME,
biz_type=PointsBizType.CHAT,
biz_id=session_id,
event_id=event_id,
amount=RUN_POINTS_COST,
direction=-1,
operator_id=operator_id,
metadata=metadata,
)
await self._repository.append_ledger(
command=command,
balance_after=int(account.balance),
)
return RunChargeResult(
charged=True,
amount=RUN_POINTS_COST,
balance_after=int(account.balance),
event_id=event_id,
)