Add V2 admin console

This commit is contained in:
2026-07-06 18:13:08 +08:00
parent 301661e7b4
commit 62eeb3186c
34 changed files with 3132 additions and 1 deletions

View File

@@ -0,0 +1,24 @@
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_admin
from app.core.responses import api_success
from app.models.admin import Admin
from app.schemas.admin import AdminLoginRequest, AdminLoginResponse, AdminRead
from app.services.admin_service import AdminAuthService
router = APIRouter()
@router.post("/login")
def login(payload: AdminLoginRequest, db: Session = Depends(get_db)) -> dict:
result = AdminAuthService.login(db, payload.username, payload.password)
return api_success(AdminLoginResponse.model_validate(result).model_dump(mode="json"))
@router.get("/profile")
def profile(current_admin: Admin = Depends(get_current_admin)) -> dict:
return api_success(AdminRead.model_validate(current_admin).model_dump())

View File

@@ -0,0 +1,22 @@
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_admin
from app.core.responses import api_success
from app.models.admin import Admin
from app.schemas.admin import DashboardStats
from app.services.admin_service import AdminDashboardService
router = APIRouter()
@router.get("/dashboard")
def dashboard(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
stats = AdminDashboardService.stats(db)
return api_success(DashboardStats.model_validate(stats).model_dump())

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
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.knowledge import Knowledge
from app.schemas.admin import KnowledgeSaveRequest
from app.services.admin_service import OperationLogService
router = APIRouter()
@router.get("/knowledge/list")
def list_knowledge(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
items = db.scalars(select(Knowledge).order_by(Knowledge.id.desc())).all()
return api_success([_knowledge_dict(item) for item in items])
@router.post("/knowledge")
def create_knowledge(
payload: KnowledgeSaveRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
knowledge = Knowledge(
name=payload.name,
feishu_space_id=payload.feishuSpaceId,
feishu_node_id=payload.feishuNodeId,
status=payload.status,
remark=payload.remark,
)
db.add(knowledge)
db.flush()
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="create", target_id=knowledge.id)
db.commit()
db.refresh(knowledge)
return api_success(_knowledge_dict(knowledge))
@router.put("/knowledge/{knowledge_id}")
def update_knowledge(
knowledge_id: int,
payload: KnowledgeSaveRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
knowledge = _get_knowledge(db, knowledge_id)
knowledge.name = payload.name
knowledge.feishu_space_id = payload.feishuSpaceId
knowledge.feishu_node_id = payload.feishuNodeId
knowledge.status = payload.status
knowledge.remark = payload.remark
db.add(knowledge)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="update", target_id=knowledge.id)
db.commit()
db.refresh(knowledge)
return api_success(_knowledge_dict(knowledge))
@router.delete("/knowledge/{knowledge_id}")
def delete_knowledge(
knowledge_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
knowledge = _get_knowledge(db, knowledge_id)
db.delete(knowledge)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="delete", target_id=knowledge.id)
db.commit()
return api_success()
def _get_knowledge(db: Session, knowledge_id: int) -> Knowledge:
knowledge = db.get(Knowledge, knowledge_id)
if knowledge is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库不存在")
return knowledge
def _knowledge_dict(knowledge: Knowledge) -> dict:
return {
"id": knowledge.id,
"name": knowledge.name,
"feishuSpaceId": knowledge.feishu_space_id,
"feishuNodeId": knowledge.feishu_node_id,
"status": knowledge.status,
"remark": knowledge.remark,
"createdAt": knowledge.created_at,
"updatedAt": knowledge.updated_at,
}

View File

@@ -0,0 +1,118 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
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.chat import ChatMessage, ChatSession
from app.models.logs import AiRequestLog, OperationLog
router = APIRouter()
@router.get("/chat/list")
def chat_list(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
sessions = db.scalars(
select(ChatSession)
.where(ChatSession.is_deleted == 0)
.order_by(ChatSession.updated_at.desc())
.limit(200)
).all()
return api_success(
[
{
"id": item.id,
"userId": item.user_id,
"title": item.title,
"messageCount": item.message_count,
"lastMessageAt": item.last_message_at,
"updatedAt": item.updated_at,
}
for item in sessions
]
)
@router.get("/chat/{session_id}")
def chat_detail(
session_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
messages = db.scalars(
select(ChatMessage)
.where(ChatMessage.session_id == session_id)
.order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc())
).all()
return api_success(
[
{
"id": item.id,
"role": item.role,
"content": item.content,
"messageStatus": item.message_status,
"tokenInput": item.token_input,
"tokenOutput": item.token_output,
"responseTimeMs": item.response_time_ms,
"createdAt": item.created_at,
}
for item in messages
]
)
@router.get("/log/list")
def operation_logs(
module: str = Query(default=""),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = select(OperationLog).order_by(OperationLog.created_at.desc()).limit(200)
if module:
query = query.where(OperationLog.module == module)
logs = db.scalars(query).all()
return api_success(
[
{
"id": item.id,
"adminId": item.admin_id,
"module": item.module,
"action": item.action,
"targetId": item.target_id,
"result": item.result,
"createdAt": item.created_at,
}
for item in logs
]
)
@router.get("/ai-log/list")
def ai_logs(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
logs = db.scalars(select(AiRequestLog).order_by(AiRequestLog.created_at.desc()).limit(200)).all()
return api_success(
[
{
"id": item.id,
"sessionId": item.session_id,
"messageId": item.message_id,
"userId": item.user_id,
"modelName": item.model_name,
"knowledgeIds": item.knowledge_ids,
"retrieveCount": item.retrieve_count,
"totalToken": item.total_token,
"costMs": item.cost_ms,
"status": item.status,
"errorMessage": item.error_message,
"createdAt": item.created_at,
}
for item in logs
]
)

View File

@@ -0,0 +1,159 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
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.ai_config import ModelConfig, Prompt, SystemConfig
from app.schemas.admin import EnableModelRequest, ModelSaveRequest, PromptSaveRequest, SystemConfigSaveRequest
from app.services.admin_service import OperationLogService
router = APIRouter()
DEFAULT_PROMPT = "你是企业飞书知识库 AI 助手。你只能基于提供的知识片段回答,不能编造。"
@router.get("/prompt")
def get_prompt(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
prompt = db.scalar(select(Prompt).order_by(Prompt.updated_at.desc(), Prompt.id.desc()).limit(1))
return api_success({"promptContent": prompt.prompt_content if prompt else DEFAULT_PROMPT})
@router.put("/prompt")
def save_prompt(
payload: PromptSaveRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
prompt = Prompt(prompt_content=payload.promptContent, updated_by=current_admin.id)
db.add(prompt)
db.flush()
OperationLogService.write(db, admin_id=current_admin.id, module="prompt", action="save", target_id=prompt.id)
db.commit()
return api_success({"promptContent": prompt.prompt_content})
@router.post("/prompt/reset")
def reset_prompt(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
prompt = Prompt(prompt_content=DEFAULT_PROMPT, updated_by=current_admin.id)
db.add(prompt)
db.flush()
OperationLogService.write(db, admin_id=current_admin.id, module="prompt", action="reset", target_id=prompt.id)
db.commit()
return api_success({"promptContent": prompt.prompt_content})
@router.get("/model/list")
def list_models(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
models = db.scalars(select(ModelConfig).order_by(ModelConfig.id.desc())).all()
return api_success([_model_dict(model) for model in models])
@router.post("/model")
def create_model(
payload: ModelSaveRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
model = ModelConfig(
provider=payload.provider,
model_name=payload.modelName,
api_url=payload.apiUrl,
api_key=payload.apiKey,
temperature=payload.temperature,
max_token=payload.maxToken,
timeout_second=payload.timeoutSecond,
enabled=0,
)
db.add(model)
db.flush()
OperationLogService.write(db, admin_id=current_admin.id, module="model", action="create", target_id=model.id)
db.commit()
db.refresh(model)
return api_success(_model_dict(model))
@router.post("/model/enable")
def enable_model(
payload: EnableModelRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
target = db.get(ModelConfig, payload.modelId)
if target is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
for model in db.scalars(select(ModelConfig)).all():
model.enabled = 1 if model.id == target.id else 0
db.add(model)
OperationLogService.write(db, admin_id=current_admin.id, module="model", action="enable", target_id=target.id)
db.commit()
return api_success()
@router.delete("/model/{model_id}")
def delete_model(
model_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
model = db.get(ModelConfig, model_id)
if model is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
db.delete(model)
OperationLogService.write(db, admin_id=current_admin.id, module="model", action="delete", target_id=model.id)
db.commit()
return api_success()
@router.get("/config")
def list_config(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
configs = db.scalars(select(SystemConfig).order_by(SystemConfig.config_key.asc())).all()
return api_success([_config_dict(config) for config in configs])
@router.put("/config")
def save_config(
payload: SystemConfigSaveRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == payload.configKey))
if config is None:
config = SystemConfig(config_key=payload.configKey, config_value=payload.configValue)
config.config_value = payload.configValue
config.description = payload.description
config.updated_by = current_admin.id
db.add(config)
db.flush()
OperationLogService.write(db, admin_id=current_admin.id, module="config", action="save", target_id=config.id)
db.commit()
db.refresh(config)
return api_success(_config_dict(config))
def _model_dict(model: ModelConfig) -> dict:
return {
"id": model.id,
"provider": model.provider,
"modelName": model.model_name,
"apiUrl": model.api_url,
"apiKeyMasked": "******" if model.api_key else "",
"temperature": float(model.temperature) if model.temperature is not None else None,
"maxToken": model.max_token,
"timeoutSecond": model.timeout_second,
"enabled": model.enabled,
}
def _config_dict(config: SystemConfig) -> dict:
return {
"id": config.id,
"configKey": config.config_key,
"configValue": config.config_value,
"description": config.description,
"updatedAt": config.updated_at,
}

View File

@@ -0,0 +1,98 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
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.user import User
from app.schemas.admin import AdminUserUpdateRequest
from app.services.admin_service import OperationLogService
router = APIRouter()
@router.get("/user/list")
def list_users(
keyword: str = Query(default=""),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = select(User).where(User.is_deleted == 0).order_by(User.id.desc())
if keyword:
like = f"%{keyword}%"
query = query.where((User.phone.like(like)) | (User.name.like(like)))
users = db.scalars(query.limit(200)).all()
return api_success([_user_dict(user) for user in users])
@router.get("/user/{user_id}")
def user_detail(
user_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
user = _get_user(db, user_id)
return api_success(_user_dict(user))
@router.put("/user/{user_id}")
def update_user(
user_id: int,
payload: AdminUserUpdateRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
user = _get_user(db, user_id)
if payload.status is not None:
user.status = payload.status
if payload.dailyChatLimit is not None:
user.daily_chat_limit = payload.dailyChatLimit
if payload.expiredAt is not None:
user.expired_at = payload.expiredAt.replace(tzinfo=None)
db.add(user)
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))
@router.delete("/user/{user_id}")
def delete_user(
user_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
user = _get_user(db, user_id)
user.is_deleted = 1
db.add(user)
OperationLogService.write(db, admin_id=current_admin.id, module="user", action="delete", target_id=user.id)
db.commit()
return api_success()
def _get_user(db: Session, user_id: int) -> User:
user = db.get(User, user_id)
if user is None or user.is_deleted:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
return user
def _user_dict(user: User) -> dict:
return {
"id": user.id,
"phone": user.phone,
"name": user.name,
"nickname": user.nickname,
"status": user.status,
"dailyChatLimit": user.daily_chat_limit,
"dailyChatUsed": user.daily_chat_used,
"expiredAt": user.expired_at,
"lastLoginAt": user.last_login_at,
"createdAt": user.created_at,
}

View File

@@ -2,10 +2,27 @@ from __future__ import annotations
from fastapi import APIRouter
from app.api import auth, chat, health, user
from app.api import (
admin_auth,
admin_dashboard,
admin_knowledge,
admin_records,
admin_settings,
admin_users,
auth,
chat,
health,
user,
)
api_router = APIRouter()
api_router.include_router(health.router, tags=["health"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(user.router, prefix="/user", tags=["user"])
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_dashboard.router, prefix="/admin", tags=["admin"])
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_settings.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_records.router, prefix="/admin", tags=["admin"])