Files
video-gen/video-gen-api/app/services/credit/upgrade_service.py
T
2026-08-11 09:24:18 +08:00

164 lines
5.9 KiB
Python

from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.enums.credit_product import SubscriptionBillingCycle
from app.enums.credit_subscription import CreditSubscriptionPeriodStatus
from app.models.credit.product import CreditProduct
from app.models.credit.subscription_period import UserCreditSubscriptionPeriod
from app.models.payment_order import PaymentOrder
from app.models.user import User
from app.services.credit.locking import acquire_user_credit_lock
from app.services.credit.product_service import (
ProductPriceQuote,
current_product_price,
get_active_subscription,
)
from app.services.credit.utils import to_credit_decimal
async def quote_and_reserve_product_purchase(
db: AsyncSession,
*,
user: User,
product: CreditProduct,
order_id: str,
request_time: datetime,
) -> ProductPriceQuote:
if product.is_credit_addon:
price = to_credit_decimal(product.price)
return ProductPriceQuote(
product=product,
purchase_scene="credit_addon",
price_type="regular",
base_price=price,
activity_price=None,
target_price=price,
deduction_amount=Decimal("0.00"),
payable_amount=price,
)
# 所有订阅购买/升级先取得统一用户积分事务锁,再锁订阅和周期,
# 与月度发放、支付履约保持同一锁顺序,避免升级边界死锁。
await acquire_user_credit_lock(db, user.id)
pending_result = await db.execute(
select(PaymentOrder.id).where(
PaymentOrder.user_id == user.id,
PaymentOrder.id != order_id,
PaymentOrder.status == "pending",
PaymentOrder.product_type == "subscription",
).limit(1)
)
if pending_result.scalar_one_or_none() is not None:
raise ValueError("已有待支付的订阅或升级订单,请先完成或等待订单过期")
current = await get_active_subscription(
db,
user.id,
request_time=request_time,
for_update=True,
)
first_purchase = user.first_membership_paid_at is None
if not first_purchase and not bool(product.renewal_enabled):
raise ValueError("该订阅套餐暂未开放续费或升级")
if current is None:
base, activity, target, price_type = current_product_price(
product,
first_purchase=first_purchase,
request_time=request_time,
upgrade=False,
)
return ProductPriceQuote(
product=product,
purchase_scene="first_purchase" if first_purchase else "renewal",
price_type=price_type,
base_price=base,
activity_price=activity,
target_price=target,
deduction_amount=Decimal("0.00"),
payable_amount=target,
)
if product.billing_cycle != current.billing_cycle:
raise ValueError("当前订阅有效,只能升级同周期更高等级套餐")
if int(product.tier_rank or 0) <= int(current.tier_rank or 0):
raise ValueError("当前订阅有效,不能提前续费或降级")
base, activity, target, price_type = current_product_price(
product,
first_purchase=False,
request_time=request_time,
upgrade=True,
)
period_ids: list[str] = []
periods: list[UserCreditSubscriptionPeriod] = []
deduction = Decimal("0.00")
if current.billing_cycle in {
SubscriptionBillingCycle.QUARTERLY.value,
SubscriptionBillingCycle.YEARLY.value,
}:
result = await db.execute(
select(UserCreditSubscriptionPeriod)
.where(
UserCreditSubscriptionPeriod.subscription_id == current.id,
UserCreditSubscriptionPeriod.scheduled_at > request_time,
UserCreditSubscriptionPeriod.status == CreditSubscriptionPeriodStatus.SCHEDULED.value,
)
.order_by(UserCreditSubscriptionPeriod.sequence.asc())
.with_for_update()
)
periods = list(result.scalars().all())
for period in periods:
deduction += to_credit_decimal(period.allocated_paid_amount)
period_ids.append(period.id)
payable = target - deduction
if payable <= Decimal("0.00"):
raise ValueError("当前升级抵扣金额已达到或超过目标套餐价格,暂不支持0元升级,请联系客服处理")
for period in periods:
period.status = CreditSubscriptionPeriodStatus.UPGRADE_RESERVED.value
period.upgrade_order_id = order_id
period.reserved_at = request_time
return ProductPriceQuote(
product=product,
purchase_scene="upgrade",
price_type=price_type,
base_price=base,
activity_price=activity,
target_price=target,
deduction_amount=deduction,
payable_amount=payable,
source_subscription_id=current.id,
upgrade_period_ids=tuple(period_ids),
)
async def release_upgrade_reservation(
db: AsyncSession,
*,
order: PaymentOrder,
released_at: datetime,
) -> int:
period_ids = list(order.upgrade_period_ids_json or [])
if not period_ids:
return 0
await acquire_user_credit_lock(db, order.user_id)
result = await db.execute(
select(UserCreditSubscriptionPeriod)
.where(
UserCreditSubscriptionPeriod.id.in_(period_ids),
UserCreditSubscriptionPeriod.upgrade_order_id == order.id,
UserCreditSubscriptionPeriod.status == CreditSubscriptionPeriodStatus.UPGRADE_RESERVED.value,
)
.with_for_update()
)
periods = list(result.scalars().all())
for period in periods:
period.status = CreditSubscriptionPeriodStatus.SCHEDULED.value
period.upgrade_order_id = None
period.reserved_at = None
await db.flush()
return len(periods)