Files
server-core/app/api/routers/rbac_compute_pricing.py
T
Pine 4dedb43d7f 算力多级折扣与企业余额体系 - 服务端核心改造
数据模型:
- 新增 tenant_discounts(运营端载体折扣)
- 新增 tenant_user_discounts(载体端账号折扣)
- 新增 compute_recharges(算力充值记录)
- 新增 compute_balance_allocations(余额分配记录)
- 新增 compute_usage_records(算力使用记录)
- 扩展 park_tenants(default_company_discount, default_user_discount)
- 扩展 park_companies(compute_balance, compute_balance_used)
- 扩展 company_members(role, compute_balance, compute_balance_used, left_at, left_reason)

核心服务:
- compute_pricing_service.py:四级折扣价格计算、余额扣费、企业余额管理

API接口(/api/compute):
- 运营端:载体折扣管理、标准价管理
- 载体端:企业折扣设置、账号折扣设置、默认折扣设置
- 企业端:余额充值、成员余额分配/回收、充值/分配记录
- 个人端:我的余额、使用记录、退出企业
- 内部:价格查询、算力扣费

数据库迁移:0064_compute_discount_balance.py
2026-09-06 17:59:56 +08:00

662 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""算力多级折扣与余额管理 API。
包含:
- 运营端:载体折扣管理、标准价管理
- 载体端:企业折扣设置、账号折扣设置
- 企业端:余额充值、成员余额分配
- 个人端:我的余额、退出企业
- 内部:价格查询、算力扣费
"""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy import select, and_
from ..dependencies import get_db, get_current_user
from ..rbac import write_audit, require_role
from ...infrastructure.models import (
ParkTenant, ParkCompany, CompanyMember,
TenantDiscount, TenantUserDiscount,
ComputeRecharge, ComputeBalanceAllocation, ComputeUsageRecord,
)
from ...infrastructure.repositories import Database
from ...services.compute_pricing_service import (
calculate_compute_price, deduct_compute_balance,
get_company_balance, allocate_balance_to_member, reclaim_balance_from_member,
STANDARD_PRICES,
)
router = APIRouter(prefix="/api/compute", tags=["compute-pricing"])
def now_str() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def new_id() -> str:
return uuid.uuid4().hex[:24]
# ═══════════════════════════════════════════════════════════════════
# 运营端:载体折扣管理
# ═══════════════════════════════════════════════════════════════════
class TenantDiscountBody(BaseModel):
tenant_id: str
discount: int = 0
effective_date: str = ""
expire_date: str = ""
reason: str = ""
@router.get("/admin/tenant-discounts", summary="运营端:载体折扣列表")
async def admin_tenant_discounts(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
require_role(user, ["operator"])
result = await db.session.execute(select(TenantDiscount).order_by(TenantDiscount.created_at.desc()))
items = []
for td in result.scalars().all():
tenant = await db.session.get(ParkTenant, td.tenant_id)
items.append({
"id": td.id,
"tenant_id": td.tenant_id,
"tenant_name": tenant.name if tenant else "",
"discount": td.discount,
"effective_date": td.effective_date,
"expire_date": td.expire_date,
"reason": td.reason,
"created_by": td.created_by,
"created_at": td.created_at,
})
return {"items": items}
@router.post("/admin/tenant-discounts", summary="运营端:设置载体折扣")
async def admin_set_tenant_discount(
body: TenantDiscountBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
require_role(user, ["operator"])
body.discount = max(0, min(100, int(body.discount)))
# 检查载体是否存在
tenant = await db.session.get(ParkTenant, body.tenant_id)
if not tenant:
raise HTTPException(status_code=404, detail="载体不存在")
# 检查是否已有折扣,有则更新
result = await db.session.execute(
select(TenantDiscount).where(TenantDiscount.tenant_id == body.tenant_id)
)
td = result.scalars().first()
if td:
td.discount = body.discount
td.effective_date = body.effective_date
td.expire_date = body.expire_date
td.reason = body.reason
td.created_by = user.get("id", "")
td.created_at = now_str()
else:
td = TenantDiscount(
id=new_id(),
tenant_id=body.tenant_id,
discount=body.discount,
effective_date=body.effective_date,
expire_date=body.expire_date,
reason=body.reason,
created_by=user.get("id", ""),
created_at=now_str(),
)
db.session.add(td)
await db.session.commit()
await write_audit(db, action="compute.tenant_discount_set", resource="tenant_discount",
resource_id=td.id, detail=f"tenant={body.tenant_id}, discount={body.discount}",
user=user, request=request)
return {"ok": True, "id": td.id}
# ═══════════════════════════════════════════════════════════════════
# 运营端:标准价管理
# ═══════════════════════════════════════════════════════════════════
class StandardPriceBody(BaseModel):
model: str
price_per_1k: int # 每1000token价格(分)
@router.get("/admin/standard-prices", summary="运营端:标准价列表")
async def admin_standard_prices(
user: dict = Depends(get_current_user),
):
require_role(user, ["operator"])
items = [{"model": k, "price_per_1k": v} for k, v in STANDARD_PRICES.items()]
return {"items": items}
@router.put("/admin/standard-prices/{model}", summary="运营端:修改标准价")
async def admin_update_standard_price(
model: str,
body: StandardPriceBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
require_role(user, ["operator"])
STANDARD_PRICES[model] = max(1, int(body.price_per_1k))
await write_audit(db, action="compute.standard_price_update", resource="standard_price",
resource_id=model, detail=f"price={body.price_per_1k}",
user=user, request=request)
return {"ok": True, "model": model, "price_per_1k": STANDARD_PRICES[model]}
# ═══════════════════════════════════════════════════════════════════
# 载体端:折扣设置
# ═══════════════════════════════════════════════════════════════════
class TenantDiscountSettingsBody(BaseModel):
default_company_discount: int = 0
default_user_discount: int = 0
class CompanyDiscountBody(BaseModel):
discount: int = 0
class UserDiscountBody(BaseModel):
user_id: str
discount: int = 0
reason: str = ""
async def _get_current_tenant(db: Database, user: dict) -> Optional[ParkTenant]:
"""获取当前用户管理的载体。"""
# 通过operator_user_id查找
result = await db.session.execute(
select(ParkTenant).where(ParkTenant.operator_user_id == user.get("id", ""))
)
return result.scalars().first()
@router.get("/tenant/discount-settings", summary="载体端:获取折扣设置")
async def tenant_discount_settings(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
tenant = await _get_current_tenant(db, user)
if not tenant:
raise HTTPException(status_code=403, detail="未绑定载体")
return {
"tenant_id": tenant.id,
"tenant_name": tenant.name,
"default_company_discount": tenant.default_company_discount or 0,
"default_user_discount": tenant.default_user_discount or 0,
}
@router.put("/tenant/discount-settings", summary="载体端:更新默认折扣")
async def tenant_update_discount_settings(
body: TenantDiscountSettingsBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
tenant = await _get_current_tenant(db, user)
if not tenant:
raise HTTPException(status_code=403, detail="未绑定载体")
body.default_company_discount = max(0, min(100, int(body.default_company_discount)))
body.default_user_discount = max(0, min(100, int(body.default_user_discount)))
tenant.default_company_discount = body.default_company_discount
tenant.default_user_discount = body.default_user_discount
await db.session.commit()
await write_audit(db, action="compute.tenant_discount_settings_update", resource="park_tenant",
resource_id=tenant.id, user=user, request=request)
return {"ok": True}
@router.get("/tenant/company-discounts", summary="载体端:下属企业折扣列表")
async def tenant_company_discounts(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
tenant = await _get_current_tenant(db, user)
if not tenant:
raise HTTPException(status_code=403, detail="未绑定载体")
result = await db.session.execute(
select(ParkCompany).where(ParkCompany.tenant_id == tenant.id)
)
items = []
for c in result.scalars().all():
items.append({
"company_id": c.id,
"company_name": c.name,
"discount": c.compute_discount or 0,
"industry": c.industry,
"status": c.status,
})
return {"items": items}
@router.put("/tenant/company-discounts/{company_id}", summary="载体端:设置企业折扣")
async def tenant_set_company_discount(
company_id: str,
body: CompanyDiscountBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
tenant = await _get_current_tenant(db, user)
if not tenant:
raise HTTPException(status_code=403, detail="未绑定载体")
company = await db.session.get(ParkCompany, company_id)
if not company or company.tenant_id != tenant.id:
raise HTTPException(status_code=404, detail="企业不存在或不属于本载体")
body.discount = max(0, min(100, int(body.discount)))
company.compute_discount = body.discount
await db.session.commit()
await write_audit(db, action="compute.company_discount_set", resource="park_company",
resource_id=company_id, detail=f"discount={body.discount}",
user=user, request=request)
return {"ok": True}
@router.get("/tenant/user-discounts", summary="载体端:下属账号折扣列表")
async def tenant_user_discounts(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
tenant = await _get_current_tenant(db, user)
if not tenant:
raise HTTPException(status_code=403, detail="未绑定载体")
result = await db.session.execute(
select(TenantUserDiscount).where(TenantUserDiscount.tenant_id == tenant.id)
)
items = []
for tud in result.scalars().all():
items.append({
"id": tud.id,
"user_id": tud.user_id,
"discount": tud.discount,
"reason": tud.reason,
"created_at": tud.created_at,
})
return {"items": items}
@router.post("/tenant/user-discounts", summary="载体端:设置账号折扣")
async def tenant_set_user_discount(
body: UserDiscountBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
tenant = await _get_current_tenant(db, user)
if not tenant:
raise HTTPException(status_code=403, detail="未绑定载体")
body.discount = max(0, min(100, int(body.discount)))
# 检查是否已有折扣
result = await db.session.execute(
select(TenantUserDiscount).where(
and_(TenantUserDiscount.tenant_id == tenant.id, TenantUserDiscount.user_id == body.user_id)
)
)
tud = result.scalars().first()
if tud:
tud.discount = body.discount
tud.reason = body.reason
tud.created_by = user.get("id", "")
tud.created_at = now_str()
else:
tud = TenantUserDiscount(
id=new_id(),
tenant_id=tenant.id,
user_id=body.user_id,
discount=body.discount,
reason=body.reason,
created_by=user.get("id", ""),
created_at=now_str(),
)
db.session.add(tud)
await db.session.commit()
await write_audit(db, action="compute.user_discount_set", resource="tenant_user_discount",
resource_id=tud.id, detail=f"user={body.user_id}, discount={body.discount}",
user=user, request=request)
return {"ok": True, "id": tud.id}
@router.delete("/tenant/user-discounts/{discount_id}", summary="载体端:删除账号折扣")
async def tenant_delete_user_discount(
discount_id: str,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
tenant = await _get_current_tenant(db, user)
if not tenant:
raise HTTPException(status_code=403, detail="未绑定载体")
tud = await db.session.get(TenantUserDiscount, discount_id)
if not tud or tud.tenant_id != tenant.id:
raise HTTPException(status_code=404, detail="折扣记录不存在")
await db.session.delete(tud)
await db.session.commit()
await write_audit(db, action="compute.user_discount_delete", resource="tenant_user_discount",
resource_id=discount_id, user=user, request=request)
return {"ok": True}
# ═══════════════════════════════════════════════════════════════════
# 企业端:余额管理
# ═══════════════════════════════════════════════════════════════════
class RechargeBody(BaseModel):
amount: int # 充值金额(分)
payment_method: str = "wechat"
class AllocateBody(BaseModel):
user_id: str
amount: int
reason: str = ""
async def _get_admin_company(db: Database, company_id: str, user: dict) -> ParkCompany:
"""校验当前用户是该企业的管理员。"""
result = await db.session.execute(
select(CompanyMember).where(
and_(CompanyMember.company_id == company_id, CompanyMember.user_id == user.get("id", ""))
)
)
member = result.scalars().first()
if not member or not member.is_admin:
raise HTTPException(status_code=403, detail="仅企业管理员可操作")
company = await db.session.get(ParkCompany, company_id)
if not company:
raise HTTPException(status_code=404, detail="企业不存在")
return company
@router.get("/enterprise/{company_id}/balance", summary="企业端:余额总览")
async def enterprise_balance(
company_id: str,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
await _get_admin_company(db, company_id, user)
return await get_company_balance(db, company_id)
@router.post("/enterprise/{company_id}/recharge", summary="企业端:充值")
async def enterprise_recharge(
company_id: str,
body: RechargeBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
company = await _get_admin_company(db, company_id, user)
if body.amount <= 0:
raise HTTPException(status_code=400, detail="充值金额必须大于0")
# 创建充值记录(模拟支付成功)
recharge = ComputeRecharge(
id=new_id(),
company_id=company_id,
amount=body.amount,
compute_amount=body.amount, # 1:1到账
payment_method=body.payment_method,
payment_status="paid",
recharged_by=user.get("id", ""),
created_at=now_str(),
paid_at=now_str(),
)
db.session.add(recharge)
# 增加企业余额
company.compute_balance = (company.compute_balance or 0) + body.amount
await db.session.commit()
await write_audit(db, action="compute.enterprise_recharge", resource="compute_recharge",
resource_id=recharge.id, detail=f"amount={body.amount}",
user=user, request=request)
return {"ok": True, "recharge_id": recharge.id, "balance": company.compute_balance}
@router.get("/enterprise/{company_id}/recharges", summary="企业端:充值记录")
async def enterprise_recharges(
company_id: str,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
await _get_admin_company(db, company_id, user)
result = await db.session.execute(
select(ComputeRecharge).where(ComputeRecharge.company_id == company_id).order_by(ComputeRecharge.created_at.desc())
)
items = [{"id": r.id, "amount": r.amount, "compute_amount": r.compute_amount,
"payment_status": r.payment_status, "created_at": r.created_at}
for r in result.scalars().all()]
return {"items": items}
@router.post("/enterprise/{company_id}/allocate", summary="企业端:给成员分配余额")
async def enterprise_allocate(
company_id: str,
body: AllocateBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
await _get_admin_company(db, company_id, user)
result = await allocate_balance_to_member(db, company_id, user.get("id", ""), body.user_id, body.amount, body.reason)
if not result.get("success"):
raise HTTPException(status_code=400, detail=result.get("reason", "分配失败"))
await write_audit(db, action="compute.balance_allocate", resource="compute_balance_allocation",
resource_id=result["allocation_id"], detail=f"user={body.user_id}, amount={body.amount}",
user=user, request=request)
return result
@router.post("/enterprise/{company_id}/reclaim", summary="企业端:回收成员余额")
async def enterprise_reclaim(
company_id: str,
body: AllocateBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
await _get_admin_company(db, company_id, user)
result = await reclaim_balance_from_member(db, company_id, user.get("id", ""), body.user_id, body.amount, body.reason)
if not result.get("success"):
raise HTTPException(status_code=400, detail=result.get("reason", "回收失败"))
await write_audit(db, action="compute.balance_reclaim", resource="compute_balance_allocation",
resource_id=result["allocation_id"], detail=f"user={body.user_id}, amount={body.amount}",
user=user, request=request)
return result
@router.get("/enterprise/{company_id}/allocations", summary="企业端:分配记录")
async def enterprise_allocations(
company_id: str,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
await _get_admin_company(db, company_id, user)
result = await db.session.execute(
select(ComputeBalanceAllocation).where(ComputeBalanceAllocation.company_id == company_id).order_by(ComputeBalanceAllocation.created_at.desc())
)
items = [{"id": a.id, "from_user_id": a.from_user_id, "to_user_id": a.to_user_id,
"amount": a.amount, "type": a.type, "reason": a.reason, "created_at": a.created_at}
for a in result.scalars().all()]
return {"items": items}
# ═══════════════════════════════════════════════════════════════════
# 个人端:我的余额、退出企业
# ═══════════════════════════════════════════════════════════════════
@router.get("/user/balance", summary="个人端:我的余额")
async def user_balance(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
user_id = user.get("id", "")
# 获取企业分配的余额
result = await db.session.execute(
select(CompanyMember).where(
and_(CompanyMember.user_id == user_id, CompanyMember.status == "active")
)
)
company_balances = []
total_company_balance = 0
for m in result.scalars().all():
company = await db.session.get(ParkCompany, m.company_id)
if company:
balance = m.compute_balance or 0
total_company_balance += balance
company_balances.append({
"company_id": m.company_id,
"company_name": company.name,
"balance": balance,
"used": m.compute_balance_used or 0,
"role": m.role or "staff",
"joined_at": m.created_at,
})
# 个人余额(暂时为0
personal_balance = 0
return {
"personal_balance": personal_balance,
"total_company_balance": total_company_balance,
"total_balance": personal_balance + total_company_balance,
"company_balances": company_balances,
}
@router.get("/user/usage", summary="个人端:使用记录")
async def user_usage(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
user_id = user.get("id", "")
result = await db.session.execute(
select(ComputeUsageRecord).where(ComputeUsageRecord.user_id == user_id).order_by(ComputeUsageRecord.created_at.desc()).limit(100)
)
items = [{"id": r.id, "model": r.model, "token_count": r.token_count,
"standard_price": r.standard_price, "discount": r.discount,
"discount_source": r.discount_source, "actual_amount": r.actual_amount,
"balance_source": r.balance_source, "company_id": r.company_id,
"created_at": r.created_at}
for r in result.scalars().all()]
return {"items": items}
class LeaveCompanyBody(BaseModel):
reason: str = ""
@router.post("/user/companies/{company_id}/leave", summary="个人端:退出企业")
async def user_leave_company(
company_id: str,
body: LeaveCompanyBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
user_id = user.get("id", "")
result = await db.session.execute(
select(CompanyMember).where(
and_(CompanyMember.company_id == company_id, CompanyMember.user_id == user_id)
)
)
member = result.scalars().first()
if not member:
raise HTTPException(status_code=404, detail="您不是该企业成员")
if member.is_admin:
raise HTTPException(status_code=400, detail="企业管理员不可主动退出,请先转让管理员权限")
# 回收剩余企业分配的余额
remaining_balance = member.compute_balance or 0
if remaining_balance > 0:
company = await db.session.get(ParkCompany, company_id)
if company:
company.compute_balance_used = max(0, (company.compute_balance_used or 0) - remaining_balance)
# 设置退出状态
member.status = "left"
member.left_at = now_str()
member.left_reason = body.reason
member.compute_balance = 0
await db.session.commit()
await write_audit(db, action="compute.user_leave_company", resource="company_member",
resource_id=member.id, detail=f"company={company_id}, refund={remaining_balance}",
user=user, request=request)
return {"ok": True, "refunded_balance": remaining_balance}
# ═══════════════════════════════════════════════════════════════════
# 内部接口:价格查询、算力扣费
# ═══════════════════════════════════════════════════════════════════
class PriceQueryBody(BaseModel):
model: str
token_count: int
@router.post("/price", summary="内部:查询实际价格")
async def compute_price(
body: PriceQueryBody,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
return await calculate_compute_price(db, user.get("id", ""), body.model, body.token_count)
class DeductBody(BaseModel):
model: str
token_count: int
@router.post("/deduct", summary="内部:算力扣费")
async def compute_deduct(
body: DeductBody,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
result = await deduct_compute_balance(db, user.get("id", ""), body.model, body.token_count)
if not result.get("success"):
raise HTTPException(status_code=402, detail=result.get("reason", "扣费失败"))
await write_audit(db, action="compute.deduct", resource="compute_usage_record",
resource_id="", detail=f"model={body.model}, tokens={body.token_count}, amount={result['amount']}",
user=user, request=request)
return result