feat: 新增告警、成本、配额、微信模块及缓存服务
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- 新增告警模块 (alerts): 告警规则配置与触发 - 新增成本管理模块 (cost): 成本统计与分析 - 新增配额模块 (quota): 配额管理与限制 - 新增微信模块 (wechat): 微信相关功能接口 - 新增缓存服务 (cache): Redis 缓存封装 - 新增请求日志中间件 (request_logger) - 新增异常处理和链路追踪中间件 - 更新 dashboard 前端展示 - 更新 SDK stats_client 功能
This commit is contained in:
430
backend/app/routers/alerts.py
Normal file
430
backend/app/routers/alerts.py
Normal file
@@ -0,0 +1,430 @@
|
||||
"""告警管理路由"""
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, func
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.alert import AlertRule, AlertRecord, NotificationChannel
|
||||
from ..services.alert import AlertService
|
||||
from .auth import get_current_user, require_operator
|
||||
from ..models.user import User
|
||||
|
||||
router = APIRouter(prefix="/alerts", tags=["告警管理"])
|
||||
|
||||
|
||||
# ============= Schemas =============
|
||||
|
||||
class AlertRuleCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
rule_type: str
|
||||
scope_type: str = "global"
|
||||
scope_value: Optional[str] = None
|
||||
condition: dict
|
||||
notification_channels: Optional[List[dict]] = None
|
||||
cooldown_minutes: int = 30
|
||||
max_alerts_per_day: int = 10
|
||||
priority: str = "medium"
|
||||
|
||||
|
||||
class AlertRuleUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
condition: Optional[dict] = None
|
||||
notification_channels: Optional[List[dict]] = None
|
||||
cooldown_minutes: Optional[int] = None
|
||||
max_alerts_per_day: Optional[int] = None
|
||||
priority: Optional[str] = None
|
||||
status: Optional[int] = None
|
||||
|
||||
|
||||
class NotificationChannelCreate(BaseModel):
|
||||
name: str
|
||||
channel_type: str
|
||||
config: dict
|
||||
|
||||
|
||||
class NotificationChannelUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
config: Optional[dict] = None
|
||||
status: Optional[int] = None
|
||||
|
||||
|
||||
# ============= Alert Rules API =============
|
||||
|
||||
@router.get("/rules")
|
||||
async def list_alert_rules(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
rule_type: Optional[str] = None,
|
||||
status: Optional[int] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取告警规则列表"""
|
||||
query = db.query(AlertRule)
|
||||
|
||||
if rule_type:
|
||||
query = query.filter(AlertRule.rule_type == rule_type)
|
||||
if status is not None:
|
||||
query = query.filter(AlertRule.status == status)
|
||||
|
||||
total = query.count()
|
||||
rules = query.order_by(desc(AlertRule.created_at)).offset((page - 1) * size).limit(size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": [format_rule(r) for r in rules]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/rules/{rule_id}")
|
||||
async def get_alert_rule(
|
||||
rule_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取告警规则详情"""
|
||||
rule = db.query(AlertRule).filter(AlertRule.id == rule_id).first()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="告警规则不存在")
|
||||
return format_rule(rule)
|
||||
|
||||
|
||||
@router.post("/rules")
|
||||
async def create_alert_rule(
|
||||
data: AlertRuleCreate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建告警规则"""
|
||||
rule = AlertRule(
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
rule_type=data.rule_type,
|
||||
scope_type=data.scope_type,
|
||||
scope_value=data.scope_value,
|
||||
condition=data.condition,
|
||||
notification_channels=data.notification_channels,
|
||||
cooldown_minutes=data.cooldown_minutes,
|
||||
max_alerts_per_day=data.max_alerts_per_day,
|
||||
priority=data.priority,
|
||||
status=1
|
||||
)
|
||||
db.add(rule)
|
||||
db.commit()
|
||||
db.refresh(rule)
|
||||
|
||||
return {"success": True, "id": rule.id}
|
||||
|
||||
|
||||
@router.put("/rules/{rule_id}")
|
||||
async def update_alert_rule(
|
||||
rule_id: int,
|
||||
data: AlertRuleUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新告警规则"""
|
||||
rule = db.query(AlertRule).filter(AlertRule.id == rule_id).first()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="告警规则不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(rule, key, value)
|
||||
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/rules/{rule_id}")
|
||||
async def delete_alert_rule(
|
||||
rule_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除告警规则"""
|
||||
rule = db.query(AlertRule).filter(AlertRule.id == rule_id).first()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="告警规则不存在")
|
||||
|
||||
db.delete(rule)
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ============= Alert Records API =============
|
||||
|
||||
@router.get("/records")
|
||||
async def list_alert_records(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
status: Optional[str] = None,
|
||||
severity: Optional[str] = None,
|
||||
alert_type: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取告警记录列表"""
|
||||
query = db.query(AlertRecord)
|
||||
|
||||
if status:
|
||||
query = query.filter(AlertRecord.status == status)
|
||||
if severity:
|
||||
query = query.filter(AlertRecord.severity == severity)
|
||||
if alert_type:
|
||||
query = query.filter(AlertRecord.alert_type == alert_type)
|
||||
if tenant_id:
|
||||
query = query.filter(AlertRecord.tenant_id == tenant_id)
|
||||
if start_date:
|
||||
query = query.filter(AlertRecord.created_at >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(AlertRecord.created_at <= end_date + " 23:59:59")
|
||||
|
||||
total = query.count()
|
||||
records = query.order_by(desc(AlertRecord.created_at)).offset((page - 1) * size).limit(size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": [format_record(r) for r in records]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/records/summary")
|
||||
async def get_alert_summary(
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取告警摘要统计"""
|
||||
today = datetime.now().date()
|
||||
week_start = today - timedelta(days=7)
|
||||
|
||||
# 今日告警数
|
||||
today_count = db.query(func.count(AlertRecord.id)).filter(
|
||||
func.date(AlertRecord.created_at) == today
|
||||
).scalar()
|
||||
|
||||
# 本周告警数
|
||||
week_count = db.query(func.count(AlertRecord.id)).filter(
|
||||
func.date(AlertRecord.created_at) >= week_start
|
||||
).scalar()
|
||||
|
||||
# 活跃告警数
|
||||
active_count = db.query(func.count(AlertRecord.id)).filter(
|
||||
AlertRecord.status == 'active'
|
||||
).scalar()
|
||||
|
||||
# 按严重程度统计
|
||||
severity_stats = db.query(
|
||||
AlertRecord.severity,
|
||||
func.count(AlertRecord.id)
|
||||
).filter(
|
||||
func.date(AlertRecord.created_at) >= week_start
|
||||
).group_by(AlertRecord.severity).all()
|
||||
|
||||
return {
|
||||
"today_count": today_count,
|
||||
"week_count": week_count,
|
||||
"active_count": active_count,
|
||||
"by_severity": {s: c for s, c in severity_stats}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/records/{record_id}")
|
||||
async def get_alert_record(
|
||||
record_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取告警记录详情"""
|
||||
record = db.query(AlertRecord).filter(AlertRecord.id == record_id).first()
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="告警记录不存在")
|
||||
return format_record(record)
|
||||
|
||||
|
||||
@router.post("/records/{record_id}/acknowledge")
|
||||
async def acknowledge_alert(
|
||||
record_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""确认告警"""
|
||||
service = AlertService(db)
|
||||
record = service.acknowledge_alert(record_id, user.username)
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="告警记录不存在")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/records/{record_id}/resolve")
|
||||
async def resolve_alert(
|
||||
record_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""解决告警"""
|
||||
service = AlertService(db)
|
||||
record = service.resolve_alert(record_id)
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="告警记录不存在")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ============= Check Alerts API =============
|
||||
|
||||
@router.post("/check")
|
||||
async def trigger_alert_check(
|
||||
background_tasks: BackgroundTasks,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""手动触发告警检查"""
|
||||
service = AlertService(db)
|
||||
alerts = await service.check_all_rules()
|
||||
|
||||
# 异步发送通知
|
||||
for alert in alerts:
|
||||
rule = db.query(AlertRule).filter(AlertRule.id == alert.rule_id).first()
|
||||
if rule:
|
||||
background_tasks.add_task(service.send_notification, alert, rule)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"triggered_count": len(alerts),
|
||||
"alerts": [format_record(a) for a in alerts]
|
||||
}
|
||||
|
||||
|
||||
# ============= Notification Channels API =============
|
||||
|
||||
@router.get("/channels")
|
||||
async def list_notification_channels(
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取通知渠道列表"""
|
||||
channels = db.query(NotificationChannel).order_by(desc(NotificationChannel.created_at)).all()
|
||||
return [format_channel(c) for c in channels]
|
||||
|
||||
|
||||
@router.post("/channels")
|
||||
async def create_notification_channel(
|
||||
data: NotificationChannelCreate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建通知渠道"""
|
||||
channel = NotificationChannel(
|
||||
name=data.name,
|
||||
channel_type=data.channel_type,
|
||||
config=data.config,
|
||||
status=1
|
||||
)
|
||||
db.add(channel)
|
||||
db.commit()
|
||||
db.refresh(channel)
|
||||
|
||||
return {"success": True, "id": channel.id}
|
||||
|
||||
|
||||
@router.put("/channels/{channel_id}")
|
||||
async def update_notification_channel(
|
||||
channel_id: int,
|
||||
data: NotificationChannelUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新通知渠道"""
|
||||
channel = db.query(NotificationChannel).filter(NotificationChannel.id == channel_id).first()
|
||||
if not channel:
|
||||
raise HTTPException(status_code=404, detail="通知渠道不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(channel, key, value)
|
||||
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/channels/{channel_id}")
|
||||
async def delete_notification_channel(
|
||||
channel_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除通知渠道"""
|
||||
channel = db.query(NotificationChannel).filter(NotificationChannel.id == channel_id).first()
|
||||
if not channel:
|
||||
raise HTTPException(status_code=404, detail="通知渠道不存在")
|
||||
|
||||
db.delete(channel)
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ============= Helper Functions =============
|
||||
|
||||
def format_rule(rule: AlertRule) -> dict:
|
||||
return {
|
||||
"id": rule.id,
|
||||
"name": rule.name,
|
||||
"description": rule.description,
|
||||
"rule_type": rule.rule_type,
|
||||
"scope_type": rule.scope_type,
|
||||
"scope_value": rule.scope_value,
|
||||
"condition": rule.condition,
|
||||
"notification_channels": rule.notification_channels,
|
||||
"cooldown_minutes": rule.cooldown_minutes,
|
||||
"max_alerts_per_day": rule.max_alerts_per_day,
|
||||
"priority": rule.priority,
|
||||
"status": rule.status,
|
||||
"created_at": rule.created_at,
|
||||
"updated_at": rule.updated_at
|
||||
}
|
||||
|
||||
|
||||
def format_record(record: AlertRecord) -> dict:
|
||||
return {
|
||||
"id": record.id,
|
||||
"rule_id": record.rule_id,
|
||||
"rule_name": record.rule_name,
|
||||
"alert_type": record.alert_type,
|
||||
"severity": record.severity,
|
||||
"title": record.title,
|
||||
"message": record.message,
|
||||
"tenant_id": record.tenant_id,
|
||||
"app_code": record.app_code,
|
||||
"metric_value": record.metric_value,
|
||||
"threshold_value": record.threshold_value,
|
||||
"notification_status": record.notification_status,
|
||||
"status": record.status,
|
||||
"acknowledged_by": record.acknowledged_by,
|
||||
"acknowledged_at": record.acknowledged_at,
|
||||
"resolved_at": record.resolved_at,
|
||||
"created_at": record.created_at
|
||||
}
|
||||
|
||||
|
||||
def format_channel(channel: NotificationChannel) -> dict:
|
||||
return {
|
||||
"id": channel.id,
|
||||
"name": channel.name,
|
||||
"channel_type": channel.channel_type,
|
||||
"config": channel.config,
|
||||
"status": channel.status,
|
||||
"created_at": channel.created_at,
|
||||
"updated_at": channel.updated_at
|
||||
}
|
||||
333
backend/app/routers/cost.py
Normal file
333
backend/app/routers/cost.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""费用管理路由"""
|
||||
from typing import Optional, List
|
||||
from decimal import Decimal
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.pricing import ModelPricing, TenantBilling
|
||||
from ..services.cost import CostCalculator
|
||||
from .auth import get_current_user, require_operator
|
||||
from ..models.user import User
|
||||
|
||||
router = APIRouter(prefix="/cost", tags=["费用管理"])
|
||||
|
||||
|
||||
# ============= Schemas =============
|
||||
|
||||
class ModelPricingCreate(BaseModel):
|
||||
model_name: str
|
||||
provider: Optional[str] = None
|
||||
display_name: Optional[str] = None
|
||||
input_price_per_1k: float = 0
|
||||
output_price_per_1k: float = 0
|
||||
fixed_price_per_call: float = 0
|
||||
pricing_type: str = "token"
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ModelPricingUpdate(BaseModel):
|
||||
provider: Optional[str] = None
|
||||
display_name: Optional[str] = None
|
||||
input_price_per_1k: Optional[float] = None
|
||||
output_price_per_1k: Optional[float] = None
|
||||
fixed_price_per_call: Optional[float] = None
|
||||
pricing_type: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
status: Optional[int] = None
|
||||
|
||||
|
||||
class CostCalculateRequest(BaseModel):
|
||||
model_name: str
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
|
||||
|
||||
# ============= Model Pricing API =============
|
||||
|
||||
@router.get("/pricing")
|
||||
async def list_model_pricing(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(50, ge=1, le=100),
|
||||
provider: Optional[str] = None,
|
||||
status: Optional[int] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取模型价格配置列表"""
|
||||
query = db.query(ModelPricing)
|
||||
|
||||
if provider:
|
||||
query = query.filter(ModelPricing.provider == provider)
|
||||
if status is not None:
|
||||
query = query.filter(ModelPricing.status == status)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(ModelPricing.model_name).offset((page - 1) * size).limit(size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": [format_pricing(p) for p in items]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/pricing/{pricing_id}")
|
||||
async def get_model_pricing(
|
||||
pricing_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取模型价格详情"""
|
||||
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
|
||||
if not pricing:
|
||||
raise HTTPException(status_code=404, detail="模型价格配置不存在")
|
||||
return format_pricing(pricing)
|
||||
|
||||
|
||||
@router.post("/pricing")
|
||||
async def create_model_pricing(
|
||||
data: ModelPricingCreate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建模型价格配置"""
|
||||
# 检查是否已存在
|
||||
existing = db.query(ModelPricing).filter(ModelPricing.model_name == data.model_name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="该模型价格配置已存在")
|
||||
|
||||
pricing = ModelPricing(
|
||||
model_name=data.model_name,
|
||||
provider=data.provider,
|
||||
display_name=data.display_name,
|
||||
input_price_per_1k=Decimal(str(data.input_price_per_1k)),
|
||||
output_price_per_1k=Decimal(str(data.output_price_per_1k)),
|
||||
fixed_price_per_call=Decimal(str(data.fixed_price_per_call)),
|
||||
pricing_type=data.pricing_type,
|
||||
description=data.description,
|
||||
status=1
|
||||
)
|
||||
db.add(pricing)
|
||||
db.commit()
|
||||
db.refresh(pricing)
|
||||
|
||||
return {"success": True, "id": pricing.id}
|
||||
|
||||
|
||||
@router.put("/pricing/{pricing_id}")
|
||||
async def update_model_pricing(
|
||||
pricing_id: int,
|
||||
data: ModelPricingUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新模型价格配置"""
|
||||
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
|
||||
if not pricing:
|
||||
raise HTTPException(status_code=404, detail="模型价格配置不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
# 转换价格字段
|
||||
for field in ['input_price_per_1k', 'output_price_per_1k', 'fixed_price_per_call']:
|
||||
if field in update_data and update_data[field] is not None:
|
||||
update_data[field] = Decimal(str(update_data[field]))
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(pricing, key, value)
|
||||
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/pricing/{pricing_id}")
|
||||
async def delete_model_pricing(
|
||||
pricing_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除模型价格配置"""
|
||||
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
|
||||
if not pricing:
|
||||
raise HTTPException(status_code=404, detail="模型价格配置不存在")
|
||||
|
||||
db.delete(pricing)
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ============= Cost Calculation API =============
|
||||
|
||||
@router.post("/calculate")
|
||||
async def calculate_cost(
|
||||
request: CostCalculateRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""计算调用费用"""
|
||||
calculator = CostCalculator(db)
|
||||
cost = calculator.calculate_cost(
|
||||
model_name=request.model_name,
|
||||
input_tokens=request.input_tokens,
|
||||
output_tokens=request.output_tokens
|
||||
)
|
||||
|
||||
return {
|
||||
"model": request.model_name,
|
||||
"input_tokens": request.input_tokens,
|
||||
"output_tokens": request.output_tokens,
|
||||
"cost": float(cost)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_cost_summary(
|
||||
tenant_id: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取费用汇总"""
|
||||
calculator = CostCalculator(db)
|
||||
return calculator.get_cost_summary(
|
||||
tenant_id=tenant_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
|
||||
@router.get("/by-tenant")
|
||||
async def get_cost_by_tenant(
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""按租户统计费用"""
|
||||
calculator = CostCalculator(db)
|
||||
return calculator.get_cost_by_tenant(
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
|
||||
@router.get("/by-model")
|
||||
async def get_cost_by_model(
|
||||
tenant_id: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""按模型统计费用"""
|
||||
calculator = CostCalculator(db)
|
||||
return calculator.get_cost_by_model(
|
||||
tenant_id=tenant_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
|
||||
# ============= Billing API =============
|
||||
|
||||
@router.get("/billing")
|
||||
async def list_billing(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
tenant_id: Optional[str] = None,
|
||||
billing_month: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取账单列表"""
|
||||
query = db.query(TenantBilling)
|
||||
|
||||
if tenant_id:
|
||||
query = query.filter(TenantBilling.tenant_id == tenant_id)
|
||||
if billing_month:
|
||||
query = query.filter(TenantBilling.billing_month == billing_month)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(desc(TenantBilling.billing_month)).offset((page - 1) * size).limit(size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": [format_billing(b) for b in items]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/billing/generate")
|
||||
async def generate_billing(
|
||||
tenant_id: str = Query(...),
|
||||
billing_month: str = Query(..., description="格式: YYYY-MM"),
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""生成月度账单"""
|
||||
calculator = CostCalculator(db)
|
||||
billing = calculator.generate_monthly_billing(tenant_id, billing_month)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"billing": format_billing(billing)
|
||||
}
|
||||
|
||||
|
||||
@router.post("/recalculate")
|
||||
async def recalculate_costs(
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""重新计算事件费用"""
|
||||
calculator = CostCalculator(db)
|
||||
updated = calculator.update_event_costs(start_date, end_date)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"updated_count": updated
|
||||
}
|
||||
|
||||
|
||||
# ============= Helper Functions =============
|
||||
|
||||
def format_pricing(pricing: ModelPricing) -> dict:
|
||||
return {
|
||||
"id": pricing.id,
|
||||
"model_name": pricing.model_name,
|
||||
"provider": pricing.provider,
|
||||
"display_name": pricing.display_name,
|
||||
"input_price_per_1k": float(pricing.input_price_per_1k or 0),
|
||||
"output_price_per_1k": float(pricing.output_price_per_1k or 0),
|
||||
"fixed_price_per_call": float(pricing.fixed_price_per_call or 0),
|
||||
"pricing_type": pricing.pricing_type,
|
||||
"description": pricing.description,
|
||||
"status": pricing.status,
|
||||
"created_at": pricing.created_at,
|
||||
"updated_at": pricing.updated_at
|
||||
}
|
||||
|
||||
|
||||
def format_billing(billing: TenantBilling) -> dict:
|
||||
return {
|
||||
"id": billing.id,
|
||||
"tenant_id": billing.tenant_id,
|
||||
"billing_month": billing.billing_month,
|
||||
"total_calls": billing.total_calls,
|
||||
"total_input_tokens": billing.total_input_tokens,
|
||||
"total_output_tokens": billing.total_output_tokens,
|
||||
"total_cost": float(billing.total_cost or 0),
|
||||
"cost_by_model": billing.cost_by_model,
|
||||
"cost_by_app": billing.cost_by_app,
|
||||
"status": billing.status,
|
||||
"created_at": billing.created_at,
|
||||
"updated_at": billing.updated_at
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
"""日志路由"""
|
||||
import csv
|
||||
import io
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
|
||||
@@ -13,6 +17,14 @@ from ..services.auth import decode_token
|
||||
router = APIRouter(prefix="/logs", tags=["logs"])
|
||||
settings = get_settings()
|
||||
|
||||
# 尝试导入openpyxl
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill
|
||||
OPENPYXL_AVAILABLE = True
|
||||
except ImportError:
|
||||
OPENPYXL_AVAILABLE = False
|
||||
|
||||
|
||||
def get_current_user_optional(authorization: Optional[str] = Header(None)):
|
||||
"""可选的用户认证"""
|
||||
@@ -113,3 +125,154 @@ async def query_logs(
|
||||
for item in items
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/export")
|
||||
async def export_logs(
|
||||
format: str = Query("csv", description="导出格式: csv 或 excel"),
|
||||
log_type: Optional[str] = None,
|
||||
level: Optional[str] = None,
|
||||
app_code: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
limit: int = Query(10000, ge=1, le=100000, description="最大导出记录数"),
|
||||
db: Session = Depends(get_db),
|
||||
user = Depends(get_current_user_optional)
|
||||
):
|
||||
"""导出日志
|
||||
|
||||
支持CSV和Excel格式,最多导出10万条记录
|
||||
"""
|
||||
query = db.query(PlatformLog)
|
||||
|
||||
if log_type:
|
||||
query = query.filter(PlatformLog.log_type == log_type)
|
||||
if level:
|
||||
query = query.filter(PlatformLog.level == level)
|
||||
if app_code:
|
||||
query = query.filter(PlatformLog.app_code == app_code)
|
||||
if tenant_id:
|
||||
query = query.filter(PlatformLog.tenant_id == tenant_id)
|
||||
if start_date:
|
||||
query = query.filter(PlatformLog.log_time >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(PlatformLog.log_time <= end_date + " 23:59:59")
|
||||
|
||||
items = query.order_by(desc(PlatformLog.log_time)).limit(limit).all()
|
||||
|
||||
if format.lower() == "excel":
|
||||
return export_excel(items)
|
||||
else:
|
||||
return export_csv(items)
|
||||
|
||||
|
||||
def export_csv(logs: list) -> StreamingResponse:
|
||||
"""导出为CSV格式"""
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
# 写入表头
|
||||
headers = [
|
||||
"ID", "类型", "级别", "应用", "租户", "Trace ID",
|
||||
"消息", "路径", "方法", "状态码", "耗时(ms)",
|
||||
"IP地址", "时间"
|
||||
]
|
||||
writer.writerow(headers)
|
||||
|
||||
# 写入数据
|
||||
for log in logs:
|
||||
writer.writerow([
|
||||
log.id,
|
||||
log.log_type,
|
||||
log.level,
|
||||
log.app_code or "",
|
||||
log.tenant_id or "",
|
||||
log.trace_id or "",
|
||||
log.message or "",
|
||||
log.path or "",
|
||||
log.method or "",
|
||||
log.status_code or "",
|
||||
log.duration_ms or "",
|
||||
log.ip_address or "",
|
||||
str(log.log_time) if log.log_time else ""
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
|
||||
# 生成文件名
|
||||
filename = f"logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
||||
"Content-Type": "text/csv; charset=utf-8-sig"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def export_excel(logs: list) -> StreamingResponse:
|
||||
"""导出为Excel格式"""
|
||||
if not OPENPYXL_AVAILABLE:
|
||||
raise HTTPException(status_code=400, detail="Excel导出功能不可用,请安装openpyxl")
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "日志导出"
|
||||
|
||||
# 表头样式
|
||||
header_font = Font(bold=True, color="FFFFFF")
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
# 写入表头
|
||||
headers = [
|
||||
"ID", "类型", "级别", "应用", "租户", "Trace ID",
|
||||
"消息", "路径", "方法", "状态码", "耗时(ms)",
|
||||
"IP地址", "时间"
|
||||
]
|
||||
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.font = header_font
|
||||
cell.fill = header_fill
|
||||
cell.alignment = header_alignment
|
||||
|
||||
# 写入数据
|
||||
for row, log in enumerate(logs, 2):
|
||||
ws.cell(row=row, column=1, value=log.id)
|
||||
ws.cell(row=row, column=2, value=log.log_type)
|
||||
ws.cell(row=row, column=3, value=log.level)
|
||||
ws.cell(row=row, column=4, value=log.app_code or "")
|
||||
ws.cell(row=row, column=5, value=log.tenant_id or "")
|
||||
ws.cell(row=row, column=6, value=log.trace_id or "")
|
||||
ws.cell(row=row, column=7, value=log.message or "")
|
||||
ws.cell(row=row, column=8, value=log.path or "")
|
||||
ws.cell(row=row, column=9, value=log.method or "")
|
||||
ws.cell(row=row, column=10, value=log.status_code or "")
|
||||
ws.cell(row=row, column=11, value=log.duration_ms or "")
|
||||
ws.cell(row=row, column=12, value=log.ip_address or "")
|
||||
ws.cell(row=row, column=13, value=str(log.log_time) if log.log_time else "")
|
||||
|
||||
# 调整列宽
|
||||
column_widths = [8, 10, 10, 12, 12, 36, 50, 30, 8, 10, 10, 15, 20]
|
||||
for col, width in enumerate(column_widths, 1):
|
||||
ws.column_dimensions[chr(64 + col)].width = width
|
||||
|
||||
# 保存到内存
|
||||
output = io.BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
|
||||
# 生成文件名
|
||||
filename = f"logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
}
|
||||
)
|
||||
|
||||
264
backend/app/routers/quota.py
Normal file
264
backend/app/routers/quota.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""配额管理路由"""
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import date
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.tenant import Subscription
|
||||
from ..services.quota import QuotaService
|
||||
from .auth import get_current_user, require_operator
|
||||
from ..models.user import User
|
||||
|
||||
router = APIRouter(prefix="/quota", tags=["配额管理"])
|
||||
|
||||
|
||||
# ============= Schemas =============
|
||||
|
||||
class QuotaConfigUpdate(BaseModel):
|
||||
daily_calls: int = 0
|
||||
daily_tokens: int = 0
|
||||
monthly_calls: int = 0
|
||||
monthly_tokens: int = 0
|
||||
monthly_cost: float = 0
|
||||
concurrent_calls: int = 0
|
||||
|
||||
|
||||
class SubscriptionCreate(BaseModel):
|
||||
tenant_id: str
|
||||
app_code: str
|
||||
start_date: Optional[str] = None
|
||||
end_date: Optional[str] = None
|
||||
quota: QuotaConfigUpdate
|
||||
|
||||
|
||||
class SubscriptionUpdate(BaseModel):
|
||||
start_date: Optional[str] = None
|
||||
end_date: Optional[str] = None
|
||||
quota: Optional[QuotaConfigUpdate] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# ============= Quota Check API =============
|
||||
|
||||
@router.get("/check")
|
||||
async def check_quota(
|
||||
tenant_id: str = Query(..., alias="tid"),
|
||||
app_code: str = Query(..., alias="aid"),
|
||||
estimated_tokens: int = Query(0),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""检查配额是否足够
|
||||
|
||||
用于调用前检查,返回是否允许继续调用
|
||||
"""
|
||||
service = QuotaService(db)
|
||||
result = service.check_quota(tenant_id, app_code, estimated_tokens)
|
||||
|
||||
return {
|
||||
"allowed": result.allowed,
|
||||
"reason": result.reason,
|
||||
"quota_type": result.quota_type,
|
||||
"limit": result.limit,
|
||||
"used": result.used,
|
||||
"remaining": result.remaining
|
||||
}
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_quota_summary(
|
||||
tenant_id: str = Query(...),
|
||||
app_code: str = Query(...),
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取配额使用汇总"""
|
||||
service = QuotaService(db)
|
||||
return service.get_quota_summary(tenant_id, app_code)
|
||||
|
||||
|
||||
@router.get("/usage")
|
||||
async def get_quota_usage(
|
||||
tenant_id: str = Query(...),
|
||||
app_code: str = Query(...),
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取配额使用情况"""
|
||||
service = QuotaService(db)
|
||||
usage = service.get_usage(tenant_id, app_code)
|
||||
|
||||
return {
|
||||
"daily_calls": usage.daily_calls,
|
||||
"daily_tokens": usage.daily_tokens,
|
||||
"monthly_calls": usage.monthly_calls,
|
||||
"monthly_tokens": usage.monthly_tokens,
|
||||
"monthly_cost": round(usage.monthly_cost, 2)
|
||||
}
|
||||
|
||||
|
||||
# ============= Subscription API =============
|
||||
|
||||
@router.get("/subscriptions")
|
||||
async def list_subscriptions(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
tenant_id: Optional[str] = None,
|
||||
app_code: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取订阅列表"""
|
||||
query = db.query(Subscription)
|
||||
|
||||
if tenant_id:
|
||||
query = query.filter(Subscription.tenant_id == tenant_id)
|
||||
if app_code:
|
||||
query = query.filter(Subscription.app_code == app_code)
|
||||
if status:
|
||||
query = query.filter(Subscription.status == status)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(desc(Subscription.created_at)).offset((page - 1) * size).limit(size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": [format_subscription(s) for s in items]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/subscriptions/{subscription_id}")
|
||||
async def get_subscription(
|
||||
subscription_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取订阅详情"""
|
||||
subscription = db.query(Subscription).filter(Subscription.id == subscription_id).first()
|
||||
if not subscription:
|
||||
raise HTTPException(status_code=404, detail="订阅不存在")
|
||||
return format_subscription(subscription)
|
||||
|
||||
|
||||
@router.post("/subscriptions")
|
||||
async def create_subscription(
|
||||
data: SubscriptionCreate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建订阅"""
|
||||
# 检查是否已存在
|
||||
existing = db.query(Subscription).filter(
|
||||
Subscription.tenant_id == data.tenant_id,
|
||||
Subscription.app_code == data.app_code,
|
||||
Subscription.status == 'active'
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="该租户应用已有活跃订阅")
|
||||
|
||||
subscription = Subscription(
|
||||
tenant_id=data.tenant_id,
|
||||
app_code=data.app_code,
|
||||
start_date=data.start_date or date.today(),
|
||||
end_date=data.end_date,
|
||||
quota=data.quota.model_dump() if data.quota else {},
|
||||
status='active'
|
||||
)
|
||||
db.add(subscription)
|
||||
db.commit()
|
||||
db.refresh(subscription)
|
||||
|
||||
return {"success": True, "id": subscription.id}
|
||||
|
||||
|
||||
@router.put("/subscriptions/{subscription_id}")
|
||||
async def update_subscription(
|
||||
subscription_id: int,
|
||||
data: SubscriptionUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新订阅"""
|
||||
subscription = db.query(Subscription).filter(Subscription.id == subscription_id).first()
|
||||
if not subscription:
|
||||
raise HTTPException(status_code=404, detail="订阅不存在")
|
||||
|
||||
if data.start_date:
|
||||
subscription.start_date = data.start_date
|
||||
if data.end_date:
|
||||
subscription.end_date = data.end_date
|
||||
if data.quota:
|
||||
subscription.quota = data.quota.model_dump()
|
||||
if data.status:
|
||||
subscription.status = data.status
|
||||
|
||||
db.commit()
|
||||
|
||||
# 清除缓存
|
||||
service = QuotaService(db)
|
||||
cache_key = f"quota:config:{subscription.tenant_id}:{subscription.app_code}"
|
||||
service._cache.delete(cache_key)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/subscriptions/{subscription_id}")
|
||||
async def delete_subscription(
|
||||
subscription_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除订阅"""
|
||||
subscription = db.query(Subscription).filter(Subscription.id == subscription_id).first()
|
||||
if not subscription:
|
||||
raise HTTPException(status_code=404, detail="订阅不存在")
|
||||
|
||||
db.delete(subscription)
|
||||
db.commit()
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.put("/subscriptions/{subscription_id}/quota")
|
||||
async def update_quota(
|
||||
subscription_id: int,
|
||||
data: QuotaConfigUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新订阅配额"""
|
||||
subscription = db.query(Subscription).filter(Subscription.id == subscription_id).first()
|
||||
if not subscription:
|
||||
raise HTTPException(status_code=404, detail="订阅不存在")
|
||||
|
||||
subscription.quota = data.model_dump()
|
||||
db.commit()
|
||||
|
||||
# 清除缓存
|
||||
service = QuotaService(db)
|
||||
cache_key = f"quota:config:{subscription.tenant_id}:{subscription.app_code}"
|
||||
service._cache.delete(cache_key)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ============= Helper Functions =============
|
||||
|
||||
def format_subscription(subscription: Subscription) -> dict:
|
||||
return {
|
||||
"id": subscription.id,
|
||||
"tenant_id": subscription.tenant_id,
|
||||
"app_code": subscription.app_code,
|
||||
"start_date": str(subscription.start_date) if subscription.start_date else None,
|
||||
"end_date": str(subscription.end_date) if subscription.end_date else None,
|
||||
"quota": subscription.quota or {},
|
||||
"status": subscription.status,
|
||||
"created_at": subscription.created_at,
|
||||
"updated_at": subscription.updated_at
|
||||
}
|
||||
264
backend/app/routers/wechat.py
Normal file
264
backend/app/routers/wechat.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""企业微信JS-SDK路由"""
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.tenant_app import TenantApp
|
||||
from ..models.tenant_wechat_app import TenantWechatApp
|
||||
from ..services.wechat import WechatService, get_wechat_service_by_id
|
||||
|
||||
router = APIRouter(prefix="/wechat", tags=["企业微信"])
|
||||
|
||||
|
||||
class JssdkSignatureRequest(BaseModel):
|
||||
"""JS-SDK签名请求"""
|
||||
url: str # 当前页面URL(不含#及其后面部分)
|
||||
|
||||
|
||||
class JssdkSignatureResponse(BaseModel):
|
||||
"""JS-SDK签名响应"""
|
||||
appId: str
|
||||
agentId: str
|
||||
timestamp: int
|
||||
nonceStr: str
|
||||
signature: str
|
||||
|
||||
|
||||
class OAuth2UrlRequest(BaseModel):
|
||||
"""OAuth2授权URL请求"""
|
||||
redirect_uri: str
|
||||
scope: str = "snsapi_base"
|
||||
state: str = ""
|
||||
|
||||
|
||||
class UserInfoRequest(BaseModel):
|
||||
"""用户信息请求"""
|
||||
code: str
|
||||
|
||||
|
||||
@router.post("/jssdk/signature")
|
||||
async def get_jssdk_signature(
|
||||
request: JssdkSignatureRequest,
|
||||
tenant_id: str = Query(..., alias="tid"),
|
||||
app_code: str = Query(..., alias="aid"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取JS-SDK签名
|
||||
|
||||
用于前端初始化企业微信JS-SDK
|
||||
|
||||
Args:
|
||||
request: 包含当前页面URL
|
||||
tenant_id: 租户ID
|
||||
app_code: 应用代码
|
||||
|
||||
Returns:
|
||||
JS-SDK签名信息
|
||||
"""
|
||||
# 查找租户应用配置
|
||||
tenant_app = db.query(TenantApp).filter(
|
||||
TenantApp.tenant_id == tenant_id,
|
||||
TenantApp.app_code == app_code,
|
||||
TenantApp.status == 1
|
||||
).first()
|
||||
|
||||
if not tenant_app:
|
||||
raise HTTPException(status_code=404, detail="租户应用配置不存在")
|
||||
|
||||
if not tenant_app.wechat_app_id:
|
||||
raise HTTPException(status_code=400, detail="该应用未配置企业微信")
|
||||
|
||||
# 获取企微服务
|
||||
wechat_service = await get_wechat_service_by_id(tenant_app.wechat_app_id, db)
|
||||
if not wechat_service:
|
||||
raise HTTPException(status_code=404, detail="企业微信应用不存在或已禁用")
|
||||
|
||||
# 生成签名
|
||||
signature_data = await wechat_service.get_jssdk_signature(request.url)
|
||||
if not signature_data:
|
||||
raise HTTPException(status_code=500, detail="获取JS-SDK签名失败")
|
||||
|
||||
return signature_data
|
||||
|
||||
|
||||
@router.get("/jssdk/signature")
|
||||
async def get_jssdk_signature_get(
|
||||
url: str = Query(..., description="当前页面URL"),
|
||||
tenant_id: str = Query(..., alias="tid"),
|
||||
app_code: str = Query(..., alias="aid"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取JS-SDK签名(GET方式)
|
||||
|
||||
方便前端JSONP调用
|
||||
"""
|
||||
# 查找租户应用配置
|
||||
tenant_app = db.query(TenantApp).filter(
|
||||
TenantApp.tenant_id == tenant_id,
|
||||
TenantApp.app_code == app_code,
|
||||
TenantApp.status == 1
|
||||
).first()
|
||||
|
||||
if not tenant_app:
|
||||
raise HTTPException(status_code=404, detail="租户应用配置不存在")
|
||||
|
||||
if not tenant_app.wechat_app_id:
|
||||
raise HTTPException(status_code=400, detail="该应用未配置企业微信")
|
||||
|
||||
# 获取企微服务
|
||||
wechat_service = await get_wechat_service_by_id(tenant_app.wechat_app_id, db)
|
||||
if not wechat_service:
|
||||
raise HTTPException(status_code=404, detail="企业微信应用不存在或已禁用")
|
||||
|
||||
# 生成签名
|
||||
signature_data = await wechat_service.get_jssdk_signature(url)
|
||||
if not signature_data:
|
||||
raise HTTPException(status_code=500, detail="获取JS-SDK签名失败")
|
||||
|
||||
return signature_data
|
||||
|
||||
|
||||
@router.post("/oauth2/url")
|
||||
async def get_oauth2_url(
|
||||
request: OAuth2UrlRequest,
|
||||
tenant_id: str = Query(..., alias="tid"),
|
||||
app_code: str = Query(..., alias="aid"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取OAuth2授权URL
|
||||
|
||||
用于企业微信内网页获取用户身份
|
||||
"""
|
||||
# 查找租户应用配置
|
||||
tenant_app = db.query(TenantApp).filter(
|
||||
TenantApp.tenant_id == tenant_id,
|
||||
TenantApp.app_code == app_code,
|
||||
TenantApp.status == 1
|
||||
).first()
|
||||
|
||||
if not tenant_app:
|
||||
raise HTTPException(status_code=404, detail="租户应用配置不存在")
|
||||
|
||||
if not tenant_app.wechat_app_id:
|
||||
raise HTTPException(status_code=400, detail="该应用未配置企业微信")
|
||||
|
||||
# 获取企微服务
|
||||
wechat_service = await get_wechat_service_by_id(tenant_app.wechat_app_id, db)
|
||||
if not wechat_service:
|
||||
raise HTTPException(status_code=404, detail="企业微信应用不存在或已禁用")
|
||||
|
||||
# 生成OAuth2 URL
|
||||
oauth_url = wechat_service.get_oauth2_url(
|
||||
redirect_uri=request.redirect_uri,
|
||||
scope=request.scope,
|
||||
state=request.state
|
||||
)
|
||||
|
||||
return {"url": oauth_url}
|
||||
|
||||
|
||||
@router.post("/oauth2/userinfo")
|
||||
async def get_user_info(
|
||||
request: UserInfoRequest,
|
||||
tenant_id: str = Query(..., alias="tid"),
|
||||
app_code: str = Query(..., alias="aid"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""通过OAuth2 code获取用户信息
|
||||
|
||||
在OAuth2回调后,用code换取用户信息
|
||||
"""
|
||||
# 查找租户应用配置
|
||||
tenant_app = db.query(TenantApp).filter(
|
||||
TenantApp.tenant_id == tenant_id,
|
||||
TenantApp.app_code == app_code,
|
||||
TenantApp.status == 1
|
||||
).first()
|
||||
|
||||
if not tenant_app:
|
||||
raise HTTPException(status_code=404, detail="租户应用配置不存在")
|
||||
|
||||
if not tenant_app.wechat_app_id:
|
||||
raise HTTPException(status_code=400, detail="该应用未配置企业微信")
|
||||
|
||||
# 获取企微服务
|
||||
wechat_service = await get_wechat_service_by_id(tenant_app.wechat_app_id, db)
|
||||
if not wechat_service:
|
||||
raise HTTPException(status_code=404, detail="企业微信应用不存在或已禁用")
|
||||
|
||||
# 获取用户信息
|
||||
user_info = await wechat_service.get_user_info_by_code(request.code)
|
||||
if not user_info:
|
||||
raise HTTPException(status_code=400, detail="获取用户信息失败,code可能已过期")
|
||||
|
||||
return user_info
|
||||
|
||||
|
||||
@router.get("/oauth2/userinfo")
|
||||
async def get_user_info_get(
|
||||
code: str = Query(..., description="OAuth2回调的code"),
|
||||
tenant_id: str = Query(..., alias="tid"),
|
||||
app_code: str = Query(..., alias="aid"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""通过OAuth2 code获取用户信息(GET方式)"""
|
||||
# 查找租户应用配置
|
||||
tenant_app = db.query(TenantApp).filter(
|
||||
TenantApp.tenant_id == tenant_id,
|
||||
TenantApp.app_code == app_code,
|
||||
TenantApp.status == 1
|
||||
).first()
|
||||
|
||||
if not tenant_app:
|
||||
raise HTTPException(status_code=404, detail="租户应用配置不存在")
|
||||
|
||||
if not tenant_app.wechat_app_id:
|
||||
raise HTTPException(status_code=400, detail="该应用未配置企业微信")
|
||||
|
||||
# 获取企微服务
|
||||
wechat_service = await get_wechat_service_by_id(tenant_app.wechat_app_id, db)
|
||||
if not wechat_service:
|
||||
raise HTTPException(status_code=404, detail="企业微信应用不存在或已禁用")
|
||||
|
||||
# 获取用户信息
|
||||
user_info = await wechat_service.get_user_info_by_code(code)
|
||||
if not user_info:
|
||||
raise HTTPException(status_code=400, detail="获取用户信息失败,code可能已过期")
|
||||
|
||||
return user_info
|
||||
|
||||
|
||||
@router.get("/user/{user_id}")
|
||||
async def get_user_detail(
|
||||
user_id: str,
|
||||
tenant_id: str = Query(..., alias="tid"),
|
||||
app_code: str = Query(..., alias="aid"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取企业微信成员详细信息"""
|
||||
# 查找租户应用配置
|
||||
tenant_app = db.query(TenantApp).filter(
|
||||
TenantApp.tenant_id == tenant_id,
|
||||
TenantApp.app_code == app_code,
|
||||
TenantApp.status == 1
|
||||
).first()
|
||||
|
||||
if not tenant_app:
|
||||
raise HTTPException(status_code=404, detail="租户应用配置不存在")
|
||||
|
||||
if not tenant_app.wechat_app_id:
|
||||
raise HTTPException(status_code=400, detail="该应用未配置企业微信")
|
||||
|
||||
# 获取企微服务
|
||||
wechat_service = await get_wechat_service_by_id(tenant_app.wechat_app_id, db)
|
||||
if not wechat_service:
|
||||
raise HTTPException(status_code=404, detail="企业微信应用不存在或已禁用")
|
||||
|
||||
# 获取用户详情
|
||||
user_detail = await wechat_service.get_user_detail(user_id)
|
||||
if not user_detail:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
return user_detail
|
||||
Reference in New Issue
Block a user