042512a527
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 28s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m17s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m56s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m39s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m1s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 5m16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m44s
CI/CD Pipeline / Validate - Style (push) Successful in 5m53s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m18s
CI/CD Pipeline / Integration Tests (push) Successful in 6m23s
AI Code Review / AI Code Review (pull_request) Successful in 6m54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m20s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m41s
CI/CD Pipeline / Validate - Security (push) Successful in 15m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m43s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 6m3s
CI/CD Pipeline / Unit Tests (push) Successful in 16m38s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
"""读取管理后台配置的会员套餐 / 积分充值包(共享库真实数据)。
|
|
|
|
替代旧的硬编码 MEMBERSHIP_PRICES / POINTS_PACKAGES。
|
|
短 TTL 缓存(30 秒),后台改价/启停后用户端最多 30 秒可见。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from typing import Any
|
|
|
|
_CACHE_TTL = 30.0
|
|
_lock = threading.Lock()
|
|
_cache: dict[str, tuple[float, Any]] = {}
|
|
|
|
_QUOTA_LABELS = {
|
|
"4k": "4K 超清分辨率",
|
|
"batch_render": "批量渲染",
|
|
"priority_queue": "优先处理队列",
|
|
"ai_matting": "AI 智能抠像",
|
|
"remove_watermark": "去水印",
|
|
}
|
|
|
|
|
|
def _cached(key: str, loader):
|
|
now = time.time()
|
|
hit = _cache.get(key)
|
|
if hit and now - hit[0] < _CACHE_TTL:
|
|
return hit[1]
|
|
with _lock:
|
|
hit = _cache.get(key)
|
|
if hit and time.time() - hit[0] < _CACHE_TTL:
|
|
return hit[1]
|
|
value = loader()
|
|
_cache[key] = (time.time(), value)
|
|
return value
|
|
|
|
|
|
def _quota_features(quotas: dict[str, Any] | None) -> dict[str, Any]:
|
|
quotas = quotas or {}
|
|
features: dict[str, Any] = {}
|
|
for k, v in quotas.items():
|
|
if k == "credits_per_month":
|
|
features["credits_per_month"] = v
|
|
elif k in _QUOTA_LABELS:
|
|
features[_QUOTA_LABELS[k]] = v
|
|
else:
|
|
features[k] = v
|
|
return features
|
|
|
|
|
|
def get_membership_plans() -> list[dict[str, Any]]:
|
|
"""读取 is_enabled=true 的套餐,按年/月周期展开为用户端档位。"""
|
|
|
|
def _load() -> list[dict[str, Any]]:
|
|
from sqlalchemy import text
|
|
|
|
from packages.adapters.sqlalchemy_impl.session import SessionLocal
|
|
|
|
if SessionLocal is None:
|
|
return []
|
|
|
|
session = SessionLocal()
|
|
try:
|
|
rows = session.execute(text("""
|
|
SELECT plan_key, name, description, monthly_price, yearly_price,
|
|
quotas, display_order
|
|
FROM plans
|
|
WHERE is_enabled = TRUE
|
|
ORDER BY display_order NULLS LAST, created_at
|
|
""")).fetchall()
|
|
finally:
|
|
session.close()
|
|
|
|
plans: list[dict[str, Any]] = []
|
|
for r in rows:
|
|
base_features = _quota_features(r.quotas if isinstance(r.quotas, dict) else None)
|
|
if r.yearly_price and float(r.yearly_price) > 0:
|
|
plans.append(
|
|
{
|
|
"plan_id": r.plan_key,
|
|
"billing_cycle": "yearly",
|
|
"name": r.name,
|
|
"description": r.description,
|
|
"price_cents": int(round(float(r.yearly_price) * 100)),
|
|
"monthly_price_cents": int(round(float(r.yearly_price) * 100 / 12)),
|
|
"duration_days": 365,
|
|
"features": dict(base_features),
|
|
}
|
|
)
|
|
if r.monthly_price and float(r.monthly_price) > 0:
|
|
plans.append(
|
|
{
|
|
"plan_id": r.plan_key,
|
|
"billing_cycle": "monthly",
|
|
"name": r.name,
|
|
"description": r.description,
|
|
"price_cents": int(round(float(r.monthly_price) * 100)),
|
|
"monthly_price_cents": int(round(float(r.monthly_price) * 100)),
|
|
"duration_days": 30,
|
|
"features": dict(base_features),
|
|
}
|
|
)
|
|
return plans
|
|
|
|
return _cached("membership_plans", _load)
|
|
|
|
|
|
def get_points_packages() -> list[dict[str, Any]]:
|
|
"""读取 is_active=true 的积分充值包。"""
|
|
|
|
def _load() -> list[dict[str, Any]]:
|
|
from sqlalchemy import text
|
|
|
|
from packages.adapters.sqlalchemy_impl.session import SessionLocal
|
|
|
|
if SessionLocal is None:
|
|
return []
|
|
|
|
session = SessionLocal()
|
|
try:
|
|
rows = session.execute(text("""
|
|
SELECT package_key, name, price, credits, bonus_credits,
|
|
is_recommended, description, sort_order
|
|
FROM credit_packages
|
|
WHERE is_active = TRUE
|
|
ORDER BY sort_order NULLS LAST, price
|
|
""")).fetchall()
|
|
finally:
|
|
session.close()
|
|
|
|
packages: list[dict[str, Any]] = []
|
|
for r in rows:
|
|
total_points = int(r.credits or 0) + int(r.bonus_credits or 0)
|
|
price_cents = int(round(float(r.price) * 100))
|
|
unit = (price_cents / 100 / total_points) if total_points else 0
|
|
packages.append(
|
|
{
|
|
"code": r.package_key,
|
|
"name": r.name,
|
|
"points": total_points,
|
|
"bonus_credits": int(r.bonus_credits or 0),
|
|
"price_cents": price_cents,
|
|
"unit_price": f"¥{unit:.3f}/积分",
|
|
"is_recommended": bool(r.is_recommended),
|
|
"description": r.description,
|
|
}
|
|
)
|
|
return packages
|
|
|
|
return _cached("points_packages", _load)
|