会员积分改版V1
This commit is contained in:
@@ -7,6 +7,8 @@ from app.api.admin.team import router as team_router
|
||||
from app.api.admin.home_material import router as home_material_router
|
||||
from app.api.admin.private_portrait import router as private_portrait_router
|
||||
from app.api.admin.recharge_package import router as recharge_package_router
|
||||
from app.api.admin.credit_management import router as credit_management_router
|
||||
from app.api.admin.llm_billing import router as llm_billing_router
|
||||
from app.api.admin.menu_config import router as menu_config_router
|
||||
from app.api.admin.upload import router as admin_upload_router
|
||||
from app.api.admin.contact import router as admin_contact_router
|
||||
@@ -19,6 +21,8 @@ router.include_router(team_router)
|
||||
router.include_router(home_material_router)
|
||||
router.include_router(private_portrait_router)
|
||||
router.include_router(recharge_package_router)
|
||||
router.include_router(credit_management_router)
|
||||
router.include_router(llm_billing_router)
|
||||
router.include_router(menu_config_router)
|
||||
router.include_router(admin_upload_router)
|
||||
router.include_router(admin_contact_router)
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_admin_user, get_db
|
||||
from app.enums.credit_balance import CreditBalanceSourceType
|
||||
from app.models.credit.balance import UserCreditBalance
|
||||
from app.models.credit.product import CreditProduct
|
||||
from app.models.user import User
|
||||
from app.schemas.credit_balance import AdminCreditDeductRequest, AdminCreditGrantRequest
|
||||
from app.schemas.credit_product import CreditProductCreate, CreditProductRenewalUpdate, CreditProductUpdate
|
||||
from app.services.credit.ledger_service import deduct_credits, grant_credits
|
||||
from app.services.credit.product_service import product_to_dict
|
||||
from app.services.credit.query_service import (
|
||||
apply_balance_status_filter,
|
||||
effective_balance_status,
|
||||
get_balance_summary,
|
||||
)
|
||||
from app.services.credit.time_policy import add_natural_months, last_usable_at
|
||||
from app.services.credit.utils import utc_now
|
||||
from app.services.notification import create_notification
|
||||
from app.services.operation_log import log_operation
|
||||
from app.services.operation_log_service import log_operation_event
|
||||
from app.utils.id_gen import generate_id
|
||||
|
||||
router = APIRouter(prefix="/admin/credit-management", tags=["admin-credit-management"])
|
||||
|
||||
|
||||
def _apply_product_payload(product: CreditProduct, payload: dict) -> None:
|
||||
mapping = {"features": "features_json"}
|
||||
for key, value in payload.items():
|
||||
setattr(product, mapping.get(key, key), value)
|
||||
if product.product_type == "credit_addon":
|
||||
product.validity_months = 1
|
||||
if product.product_type == "subscription":
|
||||
product.price = product.regular_price or 0
|
||||
product.grant_credits = None
|
||||
product.validity_months = None
|
||||
else:
|
||||
product.renewal_enabled = False
|
||||
product.tier_code = None
|
||||
product.tier_rank = None
|
||||
product.billing_cycle = None
|
||||
product.monthly_grant_credits = None
|
||||
product.first_purchase_price = None
|
||||
product.regular_price = None
|
||||
product.activity_price = None
|
||||
product.activity_start_at = None
|
||||
product.activity_end_at = None
|
||||
|
||||
|
||||
def _validate_product_entity(product: CreditProduct) -> None:
|
||||
if product.product_type == "subscription":
|
||||
required = {
|
||||
"套餐等级编码": product.tier_code,
|
||||
"套餐等级顺序": product.tier_rank,
|
||||
"订阅周期": product.billing_cycle,
|
||||
"每月积分": product.monthly_grant_credits,
|
||||
"首充价格": product.first_purchase_price,
|
||||
"原价": product.regular_price,
|
||||
}
|
||||
missing = [label for label, value in required.items() if value is None]
|
||||
if missing:
|
||||
raise HTTPException(status_code=400, detail=f"订阅套餐缺少字段:{'、'.join(missing)}")
|
||||
if product.activity_price is None:
|
||||
if product.activity_start_at is not None or product.activity_end_at is not None:
|
||||
raise HTTPException(status_code=400, detail="未配置活动价时不能单独配置活动周期")
|
||||
elif product.activity_start_at is None or product.activity_end_at is None:
|
||||
raise HTTPException(status_code=400, detail="配置活动价时必须同时配置活动开始和结束时间")
|
||||
elif product.activity_end_at <= product.activity_start_at:
|
||||
raise HTTPException(status_code=400, detail="活动结束时间必须晚于开始时间")
|
||||
elif product.product_type == "credit_addon":
|
||||
if product.grant_credits is None or product.price is None:
|
||||
raise HTTPException(status_code=400, detail="积分增值包必须配置价格和积分数量")
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="不支持的积分商品类型")
|
||||
|
||||
|
||||
@router.get("/products")
|
||||
async def list_products(
|
||||
product_type: str | None = Query(default=None),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
stmt = select(CreditProduct)
|
||||
if product_type:
|
||||
stmt = stmt.where(CreditProduct.product_type == product_type)
|
||||
result = await db.execute(stmt.order_by(CreditProduct.product_type, CreditProduct.sort_order, CreditProduct.id))
|
||||
return [product_to_dict(item) for item in result.scalars().all()]
|
||||
|
||||
|
||||
@router.post("/products")
|
||||
async def create_product(
|
||||
data: CreditProductCreate,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
exists = await db.execute(select(CreditProduct.id).where(CreditProduct.product_code == data.product_code).limit(1))
|
||||
if exists.scalar_one_or_none():
|
||||
raise HTTPException(status_code=409, detail="商品编码已存在")
|
||||
product = CreditProduct(id=generate_id())
|
||||
_apply_product_payload(product, data.model_dump())
|
||||
_validate_product_entity(product)
|
||||
db.add(product)
|
||||
await db.flush()
|
||||
snapshot = product_to_dict(product)
|
||||
await log_operation(db, admin.id, admin.username, f"创建积分商品 {product.name}", "POST", "/admin/credit-management/products", detail=json.dumps(snapshot, ensure_ascii=False, default=str))
|
||||
log_operation_event(domain="credit_product", module="credit", event_type="CREDIT_PRODUCT_CREATED", user_id=admin.id, detail=snapshot)
|
||||
# 商品保存后前端会立即使用返回值刷新列表。这里显式提交,避免依赖
|
||||
# yield 依赖退出阶段提交时出现紧随其后的 GET 读到旧状态。
|
||||
await db.commit()
|
||||
return snapshot
|
||||
|
||||
|
||||
@router.put("/products/{product_id}")
|
||||
async def update_product(
|
||||
product_id: str,
|
||||
data: CreditProductUpdate,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(CreditProduct).where(CreditProduct.id == product_id).limit(1).with_for_update())
|
||||
product = result.scalar_one_or_none()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="商品不存在")
|
||||
before = product_to_dict(product)
|
||||
payload = data.model_dump(exclude_unset=True)
|
||||
new_code = payload.get("product_code")
|
||||
if new_code and new_code != product.product_code:
|
||||
duplicate = await db.execute(
|
||||
select(CreditProduct.id).where(
|
||||
CreditProduct.product_code == new_code, CreditProduct.id != product.id
|
||||
).limit(1)
|
||||
)
|
||||
if duplicate.scalar_one_or_none():
|
||||
raise HTTPException(status_code=409, detail="商品编码已存在")
|
||||
_apply_product_payload(product, payload)
|
||||
_validate_product_entity(product)
|
||||
await db.flush()
|
||||
after = product_to_dict(product)
|
||||
await log_operation(db, admin.id, admin.username, f"更新积分商品 {product.name}", "PUT", f"/admin/credit-management/products/{product_id}", detail=json.dumps({"before": before, "after": after}, ensure_ascii=False, default=str))
|
||||
log_operation_event(domain="credit_product", module="credit", event_type="CREDIT_PRODUCT_UPDATED", user_id=admin.id, detail={"product_id": product_id})
|
||||
await db.commit()
|
||||
refreshed = await db.execute(select(CreditProduct).where(CreditProduct.id == product_id).limit(1))
|
||||
persisted = refreshed.scalar_one_or_none()
|
||||
if persisted is None:
|
||||
raise HTTPException(status_code=404, detail="商品不存在")
|
||||
return product_to_dict(persisted)
|
||||
|
||||
|
||||
@router.put("/products/{product_id}/renewal")
|
||||
async def update_product_renewal(
|
||||
product_id: str,
|
||||
data: CreditProductRenewalUpdate,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(CreditProduct)
|
||||
.where(CreditProduct.id == product_id)
|
||||
.limit(1)
|
||||
.with_for_update()
|
||||
)
|
||||
product = result.scalar_one_or_none()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="商品不存在")
|
||||
if product.product_type != "subscription":
|
||||
raise HTTPException(status_code=400, detail="积分增值包不支持续费开关")
|
||||
|
||||
before = bool(product.renewal_enabled)
|
||||
product.renewal_enabled = bool(data.renewal_enabled)
|
||||
await db.flush()
|
||||
await log_operation(
|
||||
db,
|
||||
admin.id,
|
||||
admin.username,
|
||||
f"{'开启' if product.renewal_enabled else '关闭'}积分商品续费 {product.name}",
|
||||
"PUT",
|
||||
f"/admin/credit-management/products/{product_id}/renewal",
|
||||
detail=json.dumps(
|
||||
{"before": before, "after": bool(product.renewal_enabled)},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# 提交后重新查询,返回数据库真实持久化结果,避免前端误用事务内快照。
|
||||
refreshed = await db.execute(
|
||||
select(CreditProduct).where(CreditProduct.id == product_id).limit(1)
|
||||
)
|
||||
persisted = refreshed.scalar_one_or_none()
|
||||
if persisted is None:
|
||||
raise HTTPException(status_code=404, detail="商品不存在")
|
||||
if bool(persisted.renewal_enabled) != bool(data.renewal_enabled):
|
||||
raise HTTPException(status_code=500, detail="续费状态保存后校验失败")
|
||||
return product_to_dict(persisted)
|
||||
|
||||
|
||||
@router.delete("/products/{product_id}")
|
||||
async def disable_product(
|
||||
product_id: str,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(CreditProduct).where(CreditProduct.id == product_id).limit(1).with_for_update())
|
||||
product = result.scalar_one_or_none()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="商品不存在")
|
||||
product.is_active = False
|
||||
await db.flush()
|
||||
await log_operation(db, admin.id, admin.username, f"下架积分商品 {product.name}", "DELETE", f"/admin/credit-management/products/{product_id}")
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/users/{user_id}/summary")
|
||||
async def get_user_credit_summary(
|
||||
user_id: str,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return (await get_balance_summary(db, user_id)).to_dict()
|
||||
|
||||
|
||||
@router.get("/users/{user_id}/balances")
|
||||
async def list_user_credit_balances(
|
||||
user_id: str,
|
||||
status: str | None = Query(default=None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
checked_at = utc_now()
|
||||
stmt = select(UserCreditBalance).where(UserCreditBalance.user_id == user_id)
|
||||
stmt = apply_balance_status_filter(stmt, status, request_time=checked_at)
|
||||
result = await db.execute(stmt.order_by(UserCreditBalance.expires_at.asc(), UserCreditBalance.id.asc()).offset((page - 1) * page_size).limit(page_size))
|
||||
return [
|
||||
{
|
||||
"id": item.id,
|
||||
"credit_level": item.credit_level,
|
||||
"source_type": item.source_type,
|
||||
"source_id": item.source_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),
|
||||
}
|
||||
for item in result.scalars().all()
|
||||
]
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/grant")
|
||||
async def admin_grant_credit(
|
||||
user_id: str,
|
||||
data: AdminCreditGrantRequest,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
starts_at = data.valid_from or utc_now()
|
||||
ends_at = starts_at + timedelta(days=data.validity_value) if data.validity_unit == "day" else add_natural_months(starts_at, data.validity_value)
|
||||
result = await grant_credits(
|
||||
db,
|
||||
user_id=user_id,
|
||||
amount=data.amount,
|
||||
description=data.description,
|
||||
source_type=CreditBalanceSourceType.ADMIN_GRANT.value,
|
||||
source_id=admin.id,
|
||||
valid_from=starts_at,
|
||||
expires_at=ends_at,
|
||||
credit_level=data.credit_level,
|
||||
related_id=admin.id,
|
||||
biz_key=f"admin-grant:{admin.id}:{generate_id()}",
|
||||
)
|
||||
await create_notification(
|
||||
db, user_id, "积分变动通知",
|
||||
f"您的积分已增加{data.amount}积分。原因:{data.description}", "credit",
|
||||
)
|
||||
await log_operation(db, admin.id, admin.username, f"给用户 {user_id} 增加积分 {data.amount}", "POST", f"/admin/credit-management/users/{user_id}/grant")
|
||||
return {"ok": True, "credits": result.balance_after}
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/deduct")
|
||||
async def admin_deduct_credit(
|
||||
user_id: str,
|
||||
data: AdminCreditDeductRequest,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
result = await deduct_credits(
|
||||
db,
|
||||
user_id=user_id,
|
||||
amount=data.amount,
|
||||
description=data.description,
|
||||
related_id=admin.id,
|
||||
biz_key=f"admin-deduct:{admin.id}:{generate_id()}",
|
||||
)
|
||||
except Exception as exc:
|
||||
if exc.__class__.__name__ == "InsufficientCreditsError":
|
||||
raise HTTPException(status_code=400, detail="用户有效积分不足") from exc
|
||||
raise
|
||||
await create_notification(
|
||||
db, user_id, "积分变动通知",
|
||||
f"您的积分已扣除{data.amount}积分。原因:{data.description}", "credit",
|
||||
)
|
||||
await log_operation(db, admin.id, admin.username, f"扣除用户 {user_id} 积分 {data.amount}", "POST", f"/admin/credit-management/users/{user_id}/deduct")
|
||||
return {"ok": True, "credits": result.balance_after}
|
||||
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_admin_user, get_db
|
||||
from app.enums.llm_billing import LLM_BILLING_SCENE_LABELS
|
||||
from app.models.llm_billing.policy import LlmBillingPolicyModel
|
||||
from app.models.user import User
|
||||
from app.schemas.llm_billing import LlmBillingPolicyCreate, LlmBillingPolicyUpdate
|
||||
from app.services.llm_billing.query_service import list_executions_with_calls
|
||||
from app.services.operation_log_service import log_operation_event
|
||||
from app.utils.id_gen import generate_id
|
||||
|
||||
router = APIRouter(prefix="/admin/llm-billing", tags=["admin-llm-billing"])
|
||||
|
||||
|
||||
@router.get("/policies")
|
||||
async def list_policies(
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(LlmBillingPolicyModel).order_by(LlmBillingPolicyModel.scene_code))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.post("/policies")
|
||||
async def create_policy(
|
||||
data: LlmBillingPolicyCreate,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if data.scene_code not in LLM_BILLING_SCENE_LABELS:
|
||||
raise HTTPException(status_code=400, detail="不支持的LLM业务场景")
|
||||
existing = await db.execute(select(LlmBillingPolicyModel.id).where(LlmBillingPolicyModel.scene_code == data.scene_code).limit(1))
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(status_code=409, detail="该LLM业务场景已存在")
|
||||
policy = LlmBillingPolicyModel(
|
||||
id=generate_id(),
|
||||
scene_code=data.scene_code,
|
||||
scene_name=LLM_BILLING_SCENE_LABELS[data.scene_code],
|
||||
pre_deduct_credits=data.pre_deduct_credits,
|
||||
is_active=data.is_active,
|
||||
version=1,
|
||||
created_by=admin.id,
|
||||
updated_by=admin.id,
|
||||
)
|
||||
db.add(policy)
|
||||
await db.flush()
|
||||
log_operation_event(domain="llm_billing", module="admin", event_type="LLM_BILLING_POLICY_CREATED", user_id=admin.id, detail={"scene_code": policy.scene_code, "credits": float(policy.pre_deduct_credits)})
|
||||
return policy
|
||||
|
||||
|
||||
@router.put("/policies/{policy_id}")
|
||||
async def update_policy(
|
||||
policy_id: str,
|
||||
data: LlmBillingPolicyUpdate,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(LlmBillingPolicyModel).where(LlmBillingPolicyModel.id == policy_id).limit(1).with_for_update())
|
||||
policy = result.scalar_one_or_none()
|
||||
if not policy:
|
||||
raise HTTPException(status_code=404, detail="LLM计费场景不存在")
|
||||
payload = data.model_dump(exclude_unset=True)
|
||||
payload.pop("scene_name", None)
|
||||
for key, value in payload.items():
|
||||
setattr(policy, key, value)
|
||||
policy.scene_name = LLM_BILLING_SCENE_LABELS.get(policy.scene_code, policy.scene_name)
|
||||
policy.version += 1
|
||||
policy.updated_by = admin.id
|
||||
await db.flush()
|
||||
log_operation_event(domain="llm_billing", module="admin", event_type="LLM_BILLING_POLICY_UPDATED", user_id=admin.id, detail={"scene_code": policy.scene_code, "version": policy.version})
|
||||
return policy
|
||||
|
||||
|
||||
@router.get("/executions")
|
||||
async def list_executions(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
scene_code: str | None = Query(default=None),
|
||||
status: str | None = Query(default=None),
|
||||
user_id: str | None = Query(default=None),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
items, total = await list_executions_with_calls(db, page=page, page_size=page_size, scene_code=scene_code, status=status, user_id=user_id)
|
||||
return {"items": items, "total": total}
|
||||
@@ -1,148 +1,24 @@
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_admin_user
|
||||
from app.dependencies import get_admin_user, get_db
|
||||
from app.enums.credit_product import CreditProductType
|
||||
from app.models.credit.product import CreditProduct
|
||||
from app.models.user import User
|
||||
from app.models.recharge_package import RechargePackage
|
||||
from app.schemas.recharge_package import (
|
||||
RechargePackageCreate,
|
||||
RechargePackageUpdate,
|
||||
RechargePackageOut,
|
||||
)
|
||||
from app.services.operation_log import log_operation
|
||||
from app.utils.id_gen import generate_id
|
||||
from app.services.credit.product_service import product_to_dict
|
||||
|
||||
router = APIRouter(prefix="/admin/recharge-packages", tags=["admin-recharge-packages"])
|
||||
|
||||
|
||||
def _to_out(pkg: RechargePackage) -> dict:
|
||||
return {
|
||||
"id": pkg.id,
|
||||
"name": pkg.name,
|
||||
"credits": round(pkg.credits, 2),
|
||||
"price": round(pkg.price, 2),
|
||||
"bonus_credits": round(pkg.bonus_credits, 2),
|
||||
"total_credits": round(pkg.credits + pkg.bonus_credits, 2),
|
||||
"description": pkg.description,
|
||||
"package_type": pkg.package_type,
|
||||
"is_gift": pkg.is_gift,
|
||||
"is_active": pkg.is_active,
|
||||
"sort_order": pkg.sort_order,
|
||||
}
|
||||
|
||||
|
||||
@router.get("", response_model=list[RechargePackageOut])
|
||||
@router.get("")
|
||||
async def admin_list_packages(
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(RechargePackage).order_by(RechargePackage.sort_order)
|
||||
select(CreditProduct)
|
||||
.where(CreditProduct.product_type == CreditProductType.CREDIT_ADDON.value)
|
||||
.order_by(CreditProduct.sort_order, CreditProduct.id)
|
||||
)
|
||||
return [_to_out(p) for p in result.scalars().all()]
|
||||
|
||||
|
||||
@router.post("", response_model=RechargePackageOut)
|
||||
async def create_package(
|
||||
data: RechargePackageCreate,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
pkg = RechargePackage(id=generate_id(), **data.model_dump())
|
||||
db.add(pkg)
|
||||
await db.flush()
|
||||
await log_operation(
|
||||
db,
|
||||
admin.id,
|
||||
admin.username,
|
||||
f"创建充值套餐 {pkg.name}",
|
||||
"POST",
|
||||
"/admin/recharge-packages",
|
||||
detail=json.dumps(
|
||||
{
|
||||
"id": pkg.id,
|
||||
"name": pkg.name,
|
||||
"price": round(pkg.price, 2),
|
||||
"credits": round(pkg.credits, 2),
|
||||
"bonus_credits": round(pkg.bonus_credits, 2),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
return _to_out(pkg)
|
||||
|
||||
|
||||
@router.put("/{pkg_id}", response_model=RechargePackageOut)
|
||||
async def update_package(
|
||||
pkg_id: str,
|
||||
data: RechargePackageUpdate,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(RechargePackage).where(RechargePackage.id == pkg_id).limit(1)
|
||||
)
|
||||
pkg = result.scalar_one_or_none()
|
||||
if not pkg:
|
||||
raise HTTPException(status_code=404, detail="套餐不存在")
|
||||
before = _to_out(pkg)
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(pkg, k, v)
|
||||
await db.flush()
|
||||
after = _to_out(pkg)
|
||||
await log_operation(
|
||||
db,
|
||||
admin.id,
|
||||
admin.username,
|
||||
f"更新充值套餐 {pkg.name}",
|
||||
"PUT",
|
||||
f"/admin/recharge-packages/{pkg_id}",
|
||||
detail=json.dumps(
|
||||
{
|
||||
"id": pkg_id,
|
||||
"before": before,
|
||||
"after": after,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
return _to_out(pkg)
|
||||
|
||||
|
||||
@router.delete("/{pkg_id}")
|
||||
async def delete_package(
|
||||
pkg_id: str,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(RechargePackage).where(RechargePackage.id == pkg_id).limit(1)
|
||||
)
|
||||
pkg = result.scalar_one_or_none()
|
||||
if not pkg:
|
||||
raise HTTPException(status_code=404, detail="套餐不存在")
|
||||
pkg_name = pkg.name
|
||||
await db.delete(pkg)
|
||||
await db.flush()
|
||||
await log_operation(
|
||||
db,
|
||||
admin.id,
|
||||
admin.username,
|
||||
f"删除充值套餐 {pkg_name}",
|
||||
"DELETE",
|
||||
f"/admin/recharge-packages/{pkg_id}",
|
||||
detail=json.dumps(
|
||||
{
|
||||
"id": pkg_id,
|
||||
"name": pkg_name,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
return [product_to_dict(item) for item in result.scalars().all()]
|
||||
|
||||
Reference in New Issue
Block a user