fix: 恢复 rsync 误删的近期开发(agents/skills/mcp/knowledge/workflow/tenant/billing/storage/privacy/promotion/achievement/automation/code-exec/developer/distribution/im-bot/portal/casdoor 等后端模块 + 对应前端页面路由, 补回 main.py 挂载与 App.tsx 路由

This commit is contained in:
qyyfw committed 2026-07-11 11:40:39 +08:00
1 parent d95b847fee
commit e8e5ffa1a9
65 files changed
+35624 -339

No files matched your search

@@ -0,0 +1,462 @@
"""
计费与支付管理后端 API
- 计费规则、充值套餐、交易记录、账单管理、支付配置
- 路由前缀: /app/api/admin/billing
"""
import uuid
import json
from datetime import datetime, timedelta
from fastapi import APIRouter, Query, HTTPException, Depends
from ...database import SessionLocal, get_db
from ..auth import get_current_user
from sqlalchemy import text
from sqlalchemy.orm import Session
router = APIRouter()
CST_TZ = None
def _format_cst_time(dt_val):
global CST_TZ
if CST_TZ is None:
from datetime import timezone
CST_TZ = timezone(timedelta(hours=8))
if dt_val is None:
return None
if isinstance(dt_val, str):
return dt_val
if dt_val.tzinfo is None:
dt_val = dt_val.replace(tzinfo=CST_TZ)
return dt_val.strftime("%Y-%m-%d %H:%M:%S")
def _row_to_dict(row, columns):
return {col: _format_cst_time(val) if isinstance(val, datetime) else val for col, val in zip(columns, row)}
# ============================================================
# 计费规则
# ============================================================
@router.get("/rules")
async def list_billing_rules(
status: str = Query("", description="状态筛选"),
billing_type: str = Query("", description="计费类型筛选"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""获取计费规则列表"""
where = "1=1"
params = {}
if status:
where += " AND status=:status"
params["status"] = status
if billing_type:
where += " AND billing_type=:bt"
params["bt"] = billing_type
total = db.execute(text(f"SELECT COUNT(*) FROM billing_rules WHERE {where}"), params).scalar()
offset = (page - 1) * page_size
rows = db.execute(text(f"""
SELECT * FROM billing_rules WHERE {where}
ORDER BY created_at DESC LIMIT :limit OFFSET :offset
"""), {**params, "limit": page_size, "offset": offset}).fetchall()
cols = ['id','name','description','billing_type','model_id','model_name','rate','currency',
'min_charge','free_quota','tiered_pricing','status','created_at','updated_at']
items = [_row_to_dict(r, cols) for r in rows]
return {"code": 200, "data": {"items": items, "total": total, "page": page, "page_size": page_size}}
@router.post("/rules/create")
async def create_billing_rule(
name: str = Query(...),
description: str = Query(""),
billing_type: str = Query("per_token"),
model_id: str = Query(""),
model_name: str = Query(""),
rate: float = Query(0.0),
min_charge: float = Query(0.01),
free_quota: int = Query(0),
tiered_pricing: str = Query("", description="阶梯定价JSON"),
status: str = Query("active"),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""创建计费规则"""
rid = str(uuid.uuid4())
tp = None
if tiered_pricing:
try:
tp = json.loads(tiered_pricing)
except:
tp = None
db.execute(text("""
INSERT INTO billing_rules (id, name, description, billing_type, model_id, model_name, rate,
min_charge, free_quota, tiered_pricing, status)
VALUES (:id, :name, :desc, :bt, :mid, :mname, :rate, :mc, :fq, :tp, :status)
"""), {
"id": rid, "name": name, "desc": description, "bt": billing_type,
"mid": model_id or None, "mname": model_name or None, "rate": rate,
"mc": min_charge, "fq": free_quota, "tp": json.dumps(tp) if tp else None,
"status": status
})
db.commit()
return {"code": 200, "data": {"id": rid, "message": "计费规则创建成功"}}
@router.put("/rules/{rule_id}")
async def update_billing_rule(
rule_id: str,
name: str = Query(None),
description: str = Query(None),
rate: float = Query(None),
min_charge: float = Query(None),
free_quota: int = Query(None),
status: str = Query(None),
tiered_pricing: str = Query(None),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""更新计费规则"""
sets = []
params = {"id": rule_id}
for field, val in [("name", name), ("description", description), ("rate", rate),
("min_charge", min_charge), ("free_quota", free_quota), ("status", status)]:
if val is not None:
sets.append(f"{field}=:{field}")
params[field] = val
if tiered_pricing is not None:
sets.append("tiered_pricing=:tp")
try:
params["tp"] = json.dumps(json.loads(tiered_pricing))
except:
params["tp"] = None
if not sets:
return {"code": 200, "data": {"message": "无更新"}}
db.execute(text(f"UPDATE billing_rules SET {','.join(sets)} WHERE id=:id"), params)
db.commit()
return {"code": 200, "data": {"message": "更新成功"}}
@router.delete("/rules/{rule_id}")
async def delete_billing_rule(rule_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
"""删除计费规则"""
db.execute(text("UPDATE billing_rules SET status='inactive' WHERE id=:id"), {"id": rule_id})
db.commit()
return {"code": 200, "data": {"message": "规则已停用"}}
# ============================================================
# 充值套餐
# ============================================================
@router.get("/packages")
async def list_packages(
status: str = Query(""),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""获取充值套餐列表"""
where = "1=1"
params = {}
if status:
where += " AND status=:status"
params["status"] = status
rows = db.execute(text(f"""
SELECT * FROM recharge_packages WHERE {where}
ORDER BY sort_order ASC, price ASC
"""), params).fetchall()
cols = ['id','name','description','tokens','bonus_tokens','price','original_price',
'currency','valid_days','is_popular','sort_order','status','created_at','updated_at']
items = [_row_to_dict(r, cols) for r in rows]
return {"code": 200, "data": items}
@router.post("/packages/create")
async def create_package(
name: str = Query(...),
description: str = Query(""),
tokens: int = Query(...),
bonus_tokens: int = Query(0),
price: float = Query(...),
original_price: float = Query(0),
valid_days: int = Query(365),
is_popular: int = Query(0),
sort_order: int = Query(0),
status: str = Query("active"),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""创建充值套餐"""
pid = str(uuid.uuid4())
db.execute(text("""
INSERT INTO recharge_packages (id, name, description, tokens, bonus_tokens, price, original_price,
valid_days, is_popular, sort_order, status)
VALUES (:id, :name, :desc, :tokens, :bonus, :price, :oprice, :vd, :pop, :sort, :status)
"""), {
"id": pid, "name": name, "desc": description, "tokens": tokens, "bonus": bonus_tokens,
"price": price, "oprice": original_price or None, "vd": valid_days,
"pop": is_popular, "sort": sort_order, "status": status
})
db.commit()
return {"code": 200, "data": {"id": pid, "message": "套餐创建成功"}}
@router.put("/packages/{package_id}")
async def update_package(
package_id: str,
name: str = Query(None),
price: float = Query(None),
tokens: int = Query(None),
bonus_tokens: int = Query(None),
status: str = Query(None),
is_popular: int = Query(None),
sort_order: int = Query(None),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""更新充值套餐"""
sets = []
params = {"id": package_id}
for field, val in [("name", name), ("price", price), ("tokens", tokens),
("bonus_tokens", bonus_tokens), ("status", status),
("is_popular", is_popular), ("sort_order", sort_order)]:
if val is not None:
sets.append(f"{field}=:{field}")
params[field] = val
if not sets:
return {"code": 200, "data": {"message": "无更新"}}
db.execute(text(f"UPDATE recharge_packages SET {','.join(sets)} WHERE id=:id"), params)
db.commit()
return {"code": 200, "data": {"message": "更新成功"}}
@router.delete("/packages/{package_id}")
async def delete_package(package_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
"""删除充值套餐"""
db.execute(text("UPDATE recharge_packages SET status='inactive' WHERE id=:id"), {"id": package_id})
db.commit()
return {"code": 200, "data": {"message": "套餐已停用"}}
# ============================================================
# 交易记录
# ============================================================
@router.get("/transactions")
async def list_transactions(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user_id: str = Query("", description="用户筛选"),
transaction_type: str = Query("", description="类型筛选"),
start_date: str = Query("", description="开始日期"),
end_date: str = Query("", description="结束日期"),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""获取交易记录"""
where = "1=1"
params = {}
if user_id:
where += " AND bt.user_id=:uid"
params["uid"] = user_id
if transaction_type:
where += " AND bt.transaction_type=:tt"
params["tt"] = transaction_type
if start_date:
where += " AND bt.created_at>=:sd"
params["sd"] = start_date
if end_date:
where += " AND bt.created_at<=:ed"
params["ed"] = end_date + " 23:59:59"
total = db.execute(text(f"SELECT COUNT(*) FROM billing_transactions bt WHERE {where}"), params).scalar()
offset = (page - 1) * page_size
rows = db.execute(text(f"""
SELECT bt.*, u.username
FROM billing_transactions bt
LEFT JOIN users u ON bt.user_id = u.id
WHERE {where}
ORDER BY bt.created_at DESC LIMIT :limit OFFSET :offset
"""), {**params, "limit": page_size, "offset": offset}).fetchall()
cols = ['id','tenant_id','user_id','order_id','transaction_type','amount','tokens',
'balance_after','model_id','model_name','description','metadata','status',
'created_at','username']
items = [_row_to_dict(r, cols) for r in rows]
return {"code": 200, "data": {"items": items, "total": total, "page": page, "page_size": page_size}}
# ============================================================
# 账单管理
# ============================================================
@router.get("/invoices")
async def list_invoices(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
status: str = Query(""),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""获取账单列表"""
where = "1=1"
params = {}
if status:
where += " AND status=:status"
params["status"] = status
total = db.execute(text(f"SELECT COUNT(*) FROM invoices WHERE {where}"), params).scalar()
offset = (page - 1) * page_size
rows = db.execute(text(f"""
SELECT i.*, u.username
FROM invoices i
LEFT JOIN users u ON i.user_id = u.id
WHERE {where}
ORDER BY i.created_at DESC LIMIT :limit OFFSET :offset
"""), {**params, "limit": page_size, "offset": offset}).fetchall()
cols = ['id','invoice_number','tenant_id','user_id','period_start','period_end',
'subtotal','discount','tax','total','items','status','issued_at','paid_at',
'due_at','payment_method','created_at','updated_at','username']
items = [_row_to_dict(r, cols) for r in rows]
return {"code": 200, "data": {"items": items, "total": total, "page": page, "page_size": page_size}}
@router.post("/invoices/generate")
async def generate_invoice(
user_id: str = Query(..., description="用户ID"),
period_start: str = Query(..., description="周期开始"),
period_end: str = Query(..., description="周期结束"),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""生成账单"""
# Calculate usage from token_usage_logs
usage = db.execute(text("""
SELECT model_name, SUM(total_tokens) as tokens, SUM(cost) as total_cost
FROM token_usage_logs
WHERE user_id=:uid AND created_at>=:ps AND created_at<=:pe
GROUP BY model_name
"""), {"uid": user_id, "ps": period_start, "pe": period_end}).fetchall()
if not usage:
return {"code": 200, "data": {"message": "该周期内无使用记录"}}
items = []
subtotal = 0
for row in usage:
model_name = row[0] or "未知模型"
tokens = row[1] or 0
cost = float(row[2] or 0)
items.append({"name": model_name, "quantity": tokens, "unit_price": cost / tokens if tokens > 0 else 0, "amount": cost})
subtotal += cost
invoice_number = f"INV-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
iid = str(uuid.uuid4())
db.execute(text("""
INSERT INTO invoices (id, invoice_number, user_id, period_start, period_end, subtotal, total, items, status)
VALUES (:id, :num, :uid, :ps, :pe, :sub, :total, :items, 'issued')
"""), {
"id": iid, "num": invoice_number, "uid": user_id,
"ps": period_start, "pe": period_end, "sub": subtotal,
"total": subtotal, "items": json.dumps(items)
})
db.commit()
return {"code": 200, "data": {"id": iid, "invoice_number": invoice_number, "total": subtotal, "items": items}}
# ============================================================
# 支付配置 (扩展现有 payment_configs)
# ============================================================
@router.get("/payment-configs")
async def list_payment_configs(db: Session = Depends(get_db), user=Depends(get_current_user)):
"""获取支付配置列表"""
rows = db.execute(text("SELECT * FROM payment_configs ORDER BY created_at DESC")).fetchall()
cols = ['id','type','app_id','merchant_id','api_key','cert_path','gateway_url','status',
'created_at','updated_at','api_v3_key','cert_serial_no','alipay_public_key',
'app_private_key','notify_url','return_url']
items = []
for r in rows:
d = _row_to_dict(r, cols)
# Mask sensitive fields
d['api_key'] = '***' if d.get('api_key') else None
d['api_v3_key'] = '***' if d.get('api_v3_key') else None
d['app_private_key'] = '***' if d.get('app_private_key') else None
items.append(d)
return {"code": 200, "data": items}
@router.put("/payment-configs/{config_id}")
async def update_payment_config(
config_id: str,
status: str = Query(None),
app_id: str = Query(None),
merchant_id: str = Query(None),
gateway_url: str = Query(None),
notify_url: str = Query(None),
return_url: str = Query(None),
db: Session = Depends(get_db),
user=Depends(get_current_user)
):
"""更新支付配置"""
sets = []
params = {"id": config_id}
for field, val in [("status", status), ("app_id", app_id), ("merchant_id", merchant_id),
("gateway_url", gateway_url), ("notify_url", notify_url), ("return_url", return_url)]:
if val is not None:
sets.append(f"{field}=:{field}")
params[field] = val
if not sets:
return {"code": 200, "data": {"message": "无更新"}}
db.execute(text(f"UPDATE payment_configs SET {','.join(sets)} WHERE id=:id"), params)
db.commit()
return {"code": 200, "data": {"message": "支付配置已更新"}}
# ============================================================
# 统计
# ============================================================
@router.get("/stats/overview")
async def billing_stats(db: Session = Depends(get_db), user=Depends(get_current_user)):
"""计费统计概览"""
total_revenue = db.execute(text("""
SELECT COALESCE(SUM(amount), 0) FROM billing_transactions
WHERE transaction_type='recharge' AND status='completed'
""")).scalar() or 0
total_consumption = db.execute(text("""
SELECT COALESCE(SUM(amount), 0) FROM billing_transactions
WHERE transaction_type='consumption' AND status='completed'
""")).scalar() or 0
total_transactions = db.execute(text("SELECT COUNT(*) FROM billing_transactions")).scalar()
pending_invoices = db.execute(text("SELECT COUNT(*) FROM invoices WHERE status IN ('issued','overdue')")).scalar()
paid_invoices = db.execute(text("SELECT COUNT(*) FROM invoices WHERE status='paid'")).scalar()
active_rules = db.execute(text("SELECT COUNT(*) FROM billing_rules WHERE status='active'")).scalar()
active_packages = db.execute(text("SELECT COUNT(*) FROM recharge_packages WHERE status='active'")).scalar()
# Revenue by day (last 7 days)
revenue_7d = db.execute(text("""
SELECT DATE(created_at) as d, COALESCE(SUM(amount), 0) as rev
FROM billing_transactions
WHERE transaction_type='recharge' AND status='completed'
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(created_at) ORDER BY d
""")).fetchall()
daily_revenue = [{"date": str(r[0]), "revenue": float(r[1])} for r in revenue_7d]
return {"code": 200, "data": {
"total_revenue": float(total_revenue),
"total_consumption": float(total_consumption),
"total_transactions": total_transactions,
"pending_invoices": pending_invoices,
"paid_invoices": paid_invoices,
"active_rules": active_rules,
"active_packages": active_packages,
"daily_revenue_7d": daily_revenue,
}}