项目文件夹

文件
wehub-resource-sync 6ede33ccdb
Build and Push Docker Images / create_manifest (web, surfsense-web, , cpu) (push) Has been cancelled
Build and Push Docker Images / finalize_release (push) Has been cancelled
Obsidian Plugin Lint / lint (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / compute_version (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / verify_digests (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, , cpu) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda, cuda) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda126, cuda126) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:33:44 +08:00

100 行
3.5 KiB
Python

"""Debit-triggered credit auto-reload.
``maybe_trigger_auto_reload`` is a cheap, best-effort pre-filter invoked after
every credit debit (ETL ``charge_credits`` and premium ``credit_finalize``).
When the wallet drops below the user's configured threshold it enqueues the
Celery task that performs the authoritative re-check and the off-session Stripe
charge. All real safety (row lock, cooldown, Stripe idempotency) lives in the
task — this function only avoids enqueuing work that obviously isn't needed.
Everything here is gated behind ``config.AUTO_RELOAD_ENABLED``; when the flag is
off this module is inert.
"""
from __future__ import annotations
import logging
from datetime import UTC, datetime, timedelta
from sqlalchemy import select
from app.config import config
logger = logging.getLogger(__name__)
async def maybe_trigger_auto_reload(user_id: str) -> None:
"""Enqueue an auto-reload charge if the user's balance fell below threshold.
Best-effort: any failure is swallowed by the caller. Opens its own
short-lived session so it never interferes with the caller's transaction
(it always runs after the caller has already committed the debit).
"""
if not config.AUTO_RELOAD_ENABLED:
return
from app.db import CreditPurchase, CreditPurchaseStatus, User, async_session_maker
async with async_session_maker() as session:
user = (
(await session.execute(select(User).where(User.id == user_id)))
.unique()
.scalar_one_or_none()
)
if user is None or not user.auto_reload_enabled:
return
if not (user.stripe_customer_id and user.auto_reload_payment_method_id):
return
threshold = user.auto_reload_threshold_micros
amount = user.auto_reload_amount_micros
if not threshold or not amount:
return
available = user.credit_micros_balance - user.credit_micros_reserved
if available >= threshold:
return
# Cheap cooldown pre-check: skip if a recent auto-reload purchase exists
# or a recent attempt failed (avoids hammering a declined card).
cutoff = datetime.now(UTC) - timedelta(
minutes=max(config.AUTO_RELOAD_COOLDOWN_MINUTES, 0)
)
if user.auto_reload_failed_at and user.auto_reload_failed_at >= cutoff:
return
recent = (
await session.execute(
select(CreditPurchase.id)
.where(
CreditPurchase.user_id == user.id,
CreditPurchase.source == "auto_reload",
CreditPurchase.created_at >= cutoff,
CreditPurchase.status.in_(
[
CreditPurchaseStatus.PENDING,
CreditPurchaseStatus.COMPLETED,
]
),
)
.limit(1)
)
).first()
if recent is not None:
return
# Enqueue outside the session. The task re-checks everything with a row
# lock before charging, so a benign race here only costs a no-op task run.
try:
from app.tasks.celery_tasks.auto_reload_task import (
auto_reload_credits_task,
)
auto_reload_credits_task.delay(str(user_id))
except Exception:
logger.warning(
"Failed to enqueue auto_reload_credits task for user %s",
user_id,
exc_info=True,
)