feat: add entitlement plans and topic sessions
This commit is contained in:
112
ai_knowledge_base_v2/apps/backend/app/api/admin_entitlements.py
Normal file
112
ai_knowledge_base_v2/apps/backend/app/api/admin_entitlements.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.entitlement import EntitlementPlan
|
||||
from app.models.user import User
|
||||
from app.schemas.admin import EntitlementPlanSaveRequest, UserEntitlementAssignRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
from app.services.entitlement_service import EntitlementService, entitlement_dict, plan_dict
|
||||
from app.services.topic_session_service import TopicSessionService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/entitlement/plan/list")
|
||||
def list_entitlement_plans(
|
||||
includeDisabled: bool = Query(default=True),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
plans = EntitlementService.list_plans(db, include_disabled=includeDisabled)
|
||||
return api_success([plan_dict(plan) for plan in plans])
|
||||
|
||||
|
||||
@router.post("/entitlement/plan")
|
||||
def create_entitlement_plan(
|
||||
payload: EntitlementPlanSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
plan = EntitlementPlan()
|
||||
_apply_plan_payload(plan, payload)
|
||||
db.add(plan)
|
||||
db.flush()
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="entitlement", action="create_plan", target_id=plan.id)
|
||||
db.commit()
|
||||
db.refresh(plan)
|
||||
return api_success(plan_dict(plan))
|
||||
|
||||
|
||||
@router.put("/entitlement/plan/{plan_id}")
|
||||
def update_entitlement_plan(
|
||||
plan_id: int,
|
||||
payload: EntitlementPlanSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
plan = db.get(EntitlementPlan, plan_id)
|
||||
if plan is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="权益版本不存在")
|
||||
_apply_plan_payload(plan, payload)
|
||||
db.add(plan)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="entitlement", action="update_plan", target_id=plan.id)
|
||||
db.commit()
|
||||
db.refresh(plan)
|
||||
return api_success(plan_dict(plan))
|
||||
|
||||
|
||||
@router.post("/user/{user_id}/entitlement")
|
||||
def assign_user_entitlement(
|
||||
user_id: int,
|
||||
payload: UserEntitlementAssignRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
user = db.get(User, user_id)
|
||||
if user is None or user.is_deleted:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||
entitlement = EntitlementService.assign_user_plan(
|
||||
db,
|
||||
user=user,
|
||||
plan_id=payload.planId,
|
||||
operated_by=current_admin.id,
|
||||
effective_at=payload.effectiveAt,
|
||||
expired_at=payload.expiredAt,
|
||||
remark=payload.remark,
|
||||
)
|
||||
OperationLogService.write(
|
||||
db,
|
||||
admin_id=current_admin.id,
|
||||
module="entitlement",
|
||||
action="assign_user_plan",
|
||||
target_id=user.id,
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(entitlement)
|
||||
view = EntitlementService.active_entitlement(
|
||||
db,
|
||||
user,
|
||||
monthly_topic_used=TopicSessionService.monthly_used_count(db, user.id),
|
||||
)
|
||||
return api_success(entitlement_dict(view))
|
||||
|
||||
|
||||
def _apply_plan_payload(plan: EntitlementPlan, payload: EntitlementPlanSaveRequest) -> None:
|
||||
plan.name = payload.name.strip()
|
||||
plan.plan_type = payload.planType
|
||||
plan.description = payload.description.strip() if payload.description else None
|
||||
plan.validity_days = payload.validityDays
|
||||
plan.monthly_topic_limit = payload.monthlyTopicLimit
|
||||
plan.enable_growth_profile = payload.enableGrowthProfile
|
||||
plan.enable_periodic_reports = payload.enablePeriodicReports
|
||||
plan.allow_help_card = payload.allowHelpCard
|
||||
plan.allow_share_draft = payload.allowShareDraft
|
||||
plan.deduct_quota = payload.deductQuota
|
||||
plan.status = payload.status
|
||||
plan.sort_order = payload.sortOrder
|
||||
@@ -304,6 +304,7 @@ def _message_dict(message: ChatMessage) -> dict:
|
||||
return {
|
||||
"id": message.id,
|
||||
"sessionId": message.session_id,
|
||||
"topicSessionId": message.topic_session_id,
|
||||
"userId": message.user_id,
|
||||
"role": message.role,
|
||||
"content": message.content,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
from datetime import UTC, date, datetime
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import extract, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -16,9 +16,12 @@ from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.ai_config import SystemConfig
|
||||
from app.models.entitlement import EntitlementPlan, UserEntitlement
|
||||
from app.models.chat import TopicSession
|
||||
from app.models.user import User
|
||||
from app.schemas.admin import AdminUserCreateRequest, AdminUserImportItem, AdminUserImportRequest, AdminUserUpdateRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
from app.services.entitlement_service import EntitlementService, entitlement_dict, view_from_plan
|
||||
from app.api.pagination import page_result
|
||||
|
||||
router = APIRouter()
|
||||
@@ -40,7 +43,8 @@ def list_users(
|
||||
query = query.where((User.phone.like(like)) | (User.name.like(like)))
|
||||
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
|
||||
users = db.scalars(query.offset((page - 1) * pageSize).limit(pageSize)).all()
|
||||
return api_success(page_result([_user_dict(user) for user in users], total=total, page=page, page_size=pageSize))
|
||||
entitlements = _entitlement_views(db, users)
|
||||
return api_success(page_result([_user_dict(user, entitlements.get(user.id)) for user in users], total=total, page=page, page_size=pageSize))
|
||||
|
||||
|
||||
@router.post("/user")
|
||||
@@ -68,7 +72,12 @@ def create_user(
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="user", action="create", target_id=user.id)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return api_success(_user_dict(user))
|
||||
entitlement = EntitlementService.active_entitlement(
|
||||
db,
|
||||
user,
|
||||
monthly_topic_used=_monthly_topic_counts(db, [user.id]).get(user.id, 0),
|
||||
)
|
||||
return api_success(_user_dict(user, entitlement_dict(entitlement)))
|
||||
|
||||
|
||||
@router.post("/user/import")
|
||||
@@ -240,10 +249,24 @@ def update_user(
|
||||
if payload.expiredAt is not None:
|
||||
user.expired_at = payload.expiredAt.replace(tzinfo=None)
|
||||
db.add(user)
|
||||
if payload.entitlementPlanId is not None:
|
||||
EntitlementService.assign_user_plan(
|
||||
db,
|
||||
user=user,
|
||||
plan_id=payload.entitlementPlanId,
|
||||
operated_by=current_admin.id,
|
||||
expired_at=payload.entitlementExpiredAt,
|
||||
remark=payload.entitlementRemark,
|
||||
)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="user", action="update", target_id=user.id)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return api_success(_user_dict(user))
|
||||
entitlement = EntitlementService.active_entitlement(
|
||||
db,
|
||||
user,
|
||||
monthly_topic_used=_monthly_topic_counts(db, [user.id]).get(user.id, 0),
|
||||
)
|
||||
return api_success(_user_dict(user, entitlement_dict(entitlement)))
|
||||
|
||||
|
||||
@router.delete("/user/{user_id}")
|
||||
@@ -269,7 +292,7 @@ def _get_user(db: Session, user_id: int) -> User:
|
||||
return user
|
||||
|
||||
|
||||
def _user_dict(user: User) -> dict:
|
||||
def _user_dict(user: User, entitlement: dict | None = None) -> dict:
|
||||
return {
|
||||
"id": user.id,
|
||||
"phone": user.phone,
|
||||
@@ -281,9 +304,64 @@ def _user_dict(user: User) -> dict:
|
||||
"expiredAt": user.expired_at,
|
||||
"lastLoginAt": user.last_login_at,
|
||||
"createdAt": user.created_at,
|
||||
"entitlement": entitlement,
|
||||
}
|
||||
|
||||
|
||||
def _entitlement_views(db: Session, users: list[User]) -> dict[int, dict]:
|
||||
user_ids = [user.id for user in users]
|
||||
if not user_ids:
|
||||
return {}
|
||||
counts = _monthly_topic_counts(db, user_ids)
|
||||
explicit = _active_entitlement_rows(db, user_ids)
|
||||
result: dict[int, dict] = {}
|
||||
for user in users:
|
||||
if user.id in explicit:
|
||||
entitlement, plan = explicit[user.id]
|
||||
view = view_from_plan(plan, monthly_topic_used=counts.get(user.id, 0), entitlement=entitlement, source="assigned")
|
||||
else:
|
||||
view = EntitlementService.active_entitlement(db, user, monthly_topic_used=counts.get(user.id, 0))
|
||||
result[user.id] = entitlement_dict(view)
|
||||
return result
|
||||
|
||||
|
||||
def _active_entitlement_rows(db: Session, user_ids: list[int]) -> dict[int, tuple[UserEntitlement, EntitlementPlan]]:
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
rows = db.execute(
|
||||
select(UserEntitlement, EntitlementPlan)
|
||||
.join(EntitlementPlan, EntitlementPlan.id == UserEntitlement.plan_id)
|
||||
.where(
|
||||
UserEntitlement.user_id.in_(user_ids),
|
||||
UserEntitlement.status == "active",
|
||||
EntitlementPlan.status == 1,
|
||||
)
|
||||
.where((UserEntitlement.effective_at.is_(None)) | (UserEntitlement.effective_at <= now))
|
||||
.where((UserEntitlement.expired_at.is_(None)) | (UserEntitlement.expired_at >= now))
|
||||
.order_by(UserEntitlement.created_at.desc(), UserEntitlement.id.desc())
|
||||
).all()
|
||||
result: dict[int, tuple[UserEntitlement, EntitlementPlan]] = {}
|
||||
for entitlement, plan in rows:
|
||||
result.setdefault(entitlement.user_id, (entitlement, plan))
|
||||
return result
|
||||
|
||||
|
||||
def _monthly_topic_counts(db: Session, user_ids: list[int]) -> dict[int, int]:
|
||||
if not user_ids:
|
||||
return {}
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
rows = db.execute(
|
||||
select(TopicSession.user_id, func.count(TopicSession.id))
|
||||
.where(
|
||||
TopicSession.user_id.in_(user_ids),
|
||||
TopicSession.quota_deducted == 1,
|
||||
extract("year", TopicSession.started_at) == now.year,
|
||||
extract("month", TopicSession.started_at) == now.month,
|
||||
)
|
||||
.group_by(TopicSession.user_id)
|
||||
).all()
|
||||
return {int(user_id): int(count) for user_id, count in rows}
|
||||
|
||||
|
||||
def _apply_user_payload(
|
||||
user: User,
|
||||
payload: AdminUserCreateRequest | AdminUserImportItem,
|
||||
|
||||
@@ -6,6 +6,7 @@ from app.api import (
|
||||
admin_auth,
|
||||
admin_agent_records,
|
||||
admin_dashboard,
|
||||
admin_entitlements,
|
||||
admin_knowledge,
|
||||
admin_knowledge_lifecycle,
|
||||
admin_records,
|
||||
@@ -25,6 +26,7 @@ api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||
api_router.include_router(admin_auth.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_agent_records.router, prefix="/admin", tags=["admin-agent-records"])
|
||||
api_router.include_router(admin_dashboard.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_entitlements.router, prefix="/admin", tags=["admin-entitlements"])
|
||||
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_knowledge.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_knowledge_lifecycle.router, prefix="/admin", tags=["admin-knowledge-lifecycle"])
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.core.responses import api_success
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserProfile
|
||||
from app.services.entitlement_service import EntitlementService, entitlement_dict
|
||||
from app.services.topic_session_service import TopicSessionService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
def profile(current_user: User = Depends(get_current_user)) -> dict:
|
||||
return api_success(UserProfile.model_validate(current_user).model_dump(mode="json"))
|
||||
def profile(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
data = UserProfile.model_validate(current_user).model_dump(mode="json")
|
||||
view = EntitlementService.active_entitlement(
|
||||
db,
|
||||
current_user,
|
||||
monthly_topic_used=TopicSessionService.monthly_used_count(db, current_user.id),
|
||||
)
|
||||
data["entitlement"] = entitlement_dict(view)
|
||||
return api_success(data)
|
||||
|
||||
Reference in New Issue
Block a user