210 lines
9.0 KiB
Python
210 lines
9.0 KiB
Python
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import case, func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.dependencies import get_current_user, get_db
|
|
from app.models.credit.balance import UserCreditBalance
|
|
from app.models.credit.allocation import CreditRecordAllocation
|
|
from app.models.credit_record import CreditRecord
|
|
from app.enums.credit_balance import (
|
|
CREDIT_BALANCE_SOURCE_TYPE_LABELS,
|
|
CREDIT_BALANCE_STATUS_LABELS,
|
|
CREDIT_LEVEL_LABELS,
|
|
CREDIT_SCOPE_LABELS,
|
|
CreditScope,
|
|
)
|
|
from app.enums.credit_record import CREDIT_RECORD_BILLING_SCENE_LABELS, CREDIT_RECORD_TYPE_LABELS
|
|
from app.models.credit_ratio import CreditRatio
|
|
from app.models.image_engine import ImageEngine
|
|
from app.models.user import User
|
|
from app.models.video_engine import VideoEngine
|
|
from app.schemas.credit import CreditRecordOut
|
|
from app.schemas.credit_balance import CreditBalanceItemOut
|
|
from app.schemas.credit_ratio import CreditRatioOut
|
|
from app.services.credit.query_service import (
|
|
apply_balance_status_filter,
|
|
effective_balance_status,
|
|
get_balance_summary,
|
|
)
|
|
from app.services.credit.utils import utc_now
|
|
from app.services.credit.time_policy import last_usable_at
|
|
from app.services.credit_ratio_service import list_all_credit_ratios
|
|
from app.services.credits import get_records
|
|
|
|
router = APIRouter(prefix="/credits", tags=["credits"])
|
|
|
|
|
|
def _balance_to_out(item: UserCreditBalance, *, checked_at) -> CreditBalanceItemOut:
|
|
return CreditBalanceItemOut(
|
|
id=item.id,
|
|
credit_scope=item.credit_scope,
|
|
credit_scope_label=CREDIT_SCOPE_LABELS.get(item.credit_scope, "其他积分"),
|
|
team_id=item.team_id,
|
|
credit_level=item.credit_level,
|
|
credit_level_label=CREDIT_LEVEL_LABELS.get(item.credit_level, "其他积分等级"),
|
|
source_type=item.source_type,
|
|
source_type_label=CREDIT_BALANCE_SOURCE_TYPE_LABELS.get(item.source_type, "其他来源"),
|
|
source_id=item.source_id,
|
|
product_id=item.product_id,
|
|
payment_order_id=item.payment_order_id,
|
|
subscription_id=item.subscription_id,
|
|
subscription_period_id=item.subscription_period_id,
|
|
grant_amount=float(item.grant_amount),
|
|
unspent_amount=float(item.unspent_amount),
|
|
consumed_amount=float(item.consumed_amount),
|
|
expired_amount=float(item.expired_amount),
|
|
revoked_amount=float(item.revoked_amount),
|
|
valid_from=item.valid_from,
|
|
expires_at=item.expires_at,
|
|
last_usable_at=last_usable_at(item.expires_at),
|
|
status=effective_balance_status(item, request_time=checked_at),
|
|
status_label=CREDIT_BALANCE_STATUS_LABELS.get(
|
|
effective_balance_status(item, request_time=checked_at), "其他状态"
|
|
),
|
|
created_at=item.created_at,
|
|
)
|
|
|
|
|
|
async def _records_with_scope_amounts(db: AsyncSession, records: list[CreditRecord]) -> list[dict]:
|
|
if not records:
|
|
return []
|
|
ids = [item.id for item in records]
|
|
result = await db.execute(
|
|
select(
|
|
CreditRecordAllocation.credit_record_id,
|
|
CreditRecordAllocation.credit_scope_snapshot,
|
|
func.coalesce(func.sum(CreditRecordAllocation.amount), 0).label("amount"),
|
|
)
|
|
.where(CreditRecordAllocation.credit_record_id.in_(ids))
|
|
.group_by(CreditRecordAllocation.credit_record_id, CreditRecordAllocation.credit_scope_snapshot)
|
|
)
|
|
scope_map: dict[str, dict[str, float]] = {}
|
|
for row in result.all():
|
|
scope_map.setdefault(str(row.credit_record_id), {})[str(row.credit_scope_snapshot)] = float(row.amount or 0)
|
|
output = []
|
|
for record in records:
|
|
parts = scope_map.get(record.id, {})
|
|
sign = -1.0 if float(record.amount or 0) < 0 else 1.0
|
|
output.append({
|
|
"id": record.id,
|
|
"type": record.type,
|
|
"type_label": CREDIT_RECORD_TYPE_LABELS.get(record.type, "其他"),
|
|
"amount": float(record.amount),
|
|
"personal_amount": sign * float(parts.get("personal", 0)),
|
|
"team_amount": sign * float(parts.get("team", 0)),
|
|
"balance_delta": float(record.balance_delta or 0),
|
|
"expired_amount": float(record.expired_amount or 0),
|
|
"balance_after": float(record.balance_after or 0),
|
|
"description": record.description,
|
|
"billing_scene": record.billing_scene,
|
|
"billing_scene_label": CREDIT_RECORD_BILLING_SCENE_LABELS.get(record.billing_scene, "其他场景") if record.billing_scene else None,
|
|
"scene_name_snapshot": record.scene_name_snapshot,
|
|
"input_tokens": record.input_tokens,
|
|
"output_tokens": record.output_tokens,
|
|
"total_tokens": record.total_tokens,
|
|
"llm_call_count": record.llm_call_count,
|
|
"llm_success_call_count": record.llm_success_call_count,
|
|
"llm_failed_call_count": record.llm_failed_call_count,
|
|
"created_at": record.created_at,
|
|
})
|
|
return output
|
|
|
|
|
|
@router.get("")
|
|
async def get_credits(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
records, total = await get_records(db, current_user.id, page, page_size)
|
|
summary = await get_balance_summary(db, current_user.id)
|
|
totals_result = await db.execute(
|
|
select(
|
|
func.coalesce(func.sum(case((CreditRecord.type == "recharge", CreditRecord.amount), else_=0)), 0),
|
|
func.coalesce(func.sum(case((CreditRecord.type == "consume", func.abs(CreditRecord.amount)), else_=0)), 0),
|
|
func.coalesce(func.sum(case((CreditRecord.type == "refund", CreditRecord.amount), else_=0)), 0),
|
|
func.coalesce(func.sum(case((CreditRecord.type == "expire", CreditRecord.expired_amount), else_=0)), 0),
|
|
).where(CreditRecord.user_id == current_user.id)
|
|
)
|
|
total_granted, total_consumed, total_refunded, total_expired = totals_result.one()
|
|
return {
|
|
**summary.to_dict(),
|
|
"records": await _records_with_scope_amounts(db, records),
|
|
"total": total,
|
|
"total_granted": float(total_granted or 0),
|
|
"total_consumed": float(total_consumed or 0),
|
|
"total_refunded": float(total_refunded or 0),
|
|
"total_expired": float(total_expired or 0),
|
|
}
|
|
|
|
|
|
@router.get("/balances")
|
|
async def list_credit_balances(
|
|
status: str | None = Query(default=None),
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
checked_at = utc_now()
|
|
# 通用积分页只展示用户自己的个人积分批次。团队资金池归属成交时队长,
|
|
# 不能因为 Balance.owner 是队长就在这里展示整个团队资金池;团队席位与资金池明细统一在团队管理页查看。
|
|
stmt = select(UserCreditBalance).where(
|
|
UserCreditBalance.user_id == current_user.id,
|
|
UserCreditBalance.credit_scope == CreditScope.PERSONAL.value,
|
|
)
|
|
stmt = apply_balance_status_filter(stmt, status, request_time=checked_at)
|
|
stmt = stmt.order_by(UserCreditBalance.expires_at.asc(), UserCreditBalance.id.asc())
|
|
result = await db.execute(stmt.offset((page - 1) * page_size).limit(page_size))
|
|
return [_balance_to_out(item, checked_at=checked_at) for item in result.scalars().all()]
|
|
|
|
|
|
@router.get(
|
|
"/credit-ratios",
|
|
response_model=list[CreditRatioOut],
|
|
summary="获取积分比例列表",
|
|
)
|
|
async def list_client_credit_ratios(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await list_all_credit_ratios(db)
|
|
|
|
|
|
@router.get("/ratios", response_model=dict)
|
|
async def get_credit_ratios(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
async def get_ratios_for_engine_type(gen_type: str, engine_ids: list):
|
|
for engine_id in engine_ids:
|
|
result = await db.execute(
|
|
select(CreditRatio)
|
|
.where(CreditRatio.gen_type == gen_type)
|
|
.where(CreditRatio.model_config_id == engine_id)
|
|
)
|
|
ratios = result.scalars().all()
|
|
if ratios:
|
|
return [CreditRatioOut.model_validate(r) for r in ratios]
|
|
return []
|
|
|
|
video_result = await db.execute(
|
|
select(VideoEngine.id)
|
|
.where(VideoEngine.is_active.is_(True), VideoEngine.deleted_at.is_(None))
|
|
.order_by(VideoEngine.priority.desc())
|
|
)
|
|
image_result = await db.execute(
|
|
select(ImageEngine.id)
|
|
.where(ImageEngine.is_active.is_(True), ImageEngine.deleted_at.is_(None))
|
|
.order_by(ImageEngine.priority.desc())
|
|
)
|
|
grouped = {}
|
|
video_ratios = await get_ratios_for_engine_type("video", list(video_result.scalars().all()))
|
|
image_ratios = await get_ratios_for_engine_type("image", list(image_result.scalars().all()))
|
|
if video_ratios:
|
|
grouped["video"] = video_ratios
|
|
if image_ratios:
|
|
grouped["image"] = image_ratios
|
|
return grouped
|