Files
012-kaopeilian/backend/app/api/v1/admin_portal/tenants.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

380 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
租户管理 API
"""
import os
import json
from datetime import datetime
from typing import Optional, List
import pymysql
from fastapi import APIRouter, Depends, HTTPException, status, Query
from .auth import get_current_admin, require_superadmin, get_db_connection, AdminUserInfo
from .schemas import (
TenantCreate,
TenantUpdate,
TenantResponse,
TenantListResponse,
ResponseModel,
)
router = APIRouter(prefix="/tenants", tags=["租户管理"])
def log_operation(cursor, admin: AdminUserInfo, tenant_id: int, tenant_code: str,
operation_type: str, resource_type: str, resource_id: int,
resource_name: str, old_value: dict = None, new_value: dict = None):
"""记录操作日志"""
cursor.execute(
"""
INSERT INTO operation_logs
(admin_user_id, admin_username, tenant_id, tenant_code, operation_type,
resource_type, resource_id, resource_name, old_value, new_value)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(admin.id, admin.username, tenant_id, tenant_code, operation_type,
resource_type, resource_id, resource_name,
json.dumps(old_value, ensure_ascii=False) if old_value else None,
json.dumps(new_value, ensure_ascii=False) if new_value else None)
)
@router.get("", response_model=TenantListResponse, summary="获取租户列表")
async def list_tenants(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
status: Optional[str] = Query(None, description="状态筛选"),
keyword: Optional[str] = Query(None, description="关键词搜索"),
admin: AdminUserInfo = Depends(get_current_admin),
):
"""
获取租户列表
- **page**: 页码
- **page_size**: 每页数量
- **status**: 状态筛选active, inactive, suspended
- **keyword**: 关键词搜索(匹配名称、编码、域名)
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 构建查询条件
conditions = []
params = []
if status:
conditions.append("t.status = %s")
params.append(status)
if keyword:
conditions.append("(t.name LIKE %s OR t.code LIKE %s OR t.domain LIKE %s)")
params.extend([f"%{keyword}%"] * 3)
where_clause = " AND ".join(conditions) if conditions else "1=1"
# 查询总数
cursor.execute(
f"SELECT COUNT(*) as total FROM tenants t WHERE {where_clause}",
params
)
total = cursor.fetchone()["total"]
# 查询列表
offset = (page - 1) * page_size
cursor.execute(
f"""
SELECT t.*,
(SELECT COUNT(*) FROM tenant_configs tc WHERE tc.tenant_id = t.id) as config_count
FROM tenants t
WHERE {where_clause}
ORDER BY t.id DESC
LIMIT %s OFFSET %s
""",
params + [page_size, offset]
)
rows = cursor.fetchall()
items = [TenantResponse(**row) for row in rows]
return TenantListResponse(
items=items,
total=total,
page=page,
page_size=page_size,
)
finally:
conn.close()
@router.get("/{tenant_id}", response_model=TenantResponse, summary="获取租户详情")
async def get_tenant(
tenant_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""获取租户详情"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT t.*,
(SELECT COUNT(*) FROM tenant_configs tc WHERE tc.tenant_id = t.id) as config_count
FROM tenants t
WHERE t.id = %s
""",
(tenant_id,)
)
row = cursor.fetchone()
if not row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在",
)
return TenantResponse(**row)
finally:
conn.close()
@router.post("", response_model=TenantResponse, summary="创建租户")
async def create_tenant(
data: TenantCreate,
admin: AdminUserInfo = Depends(require_superadmin),
):
"""
创建新租户
需要超级管理员权限
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 检查编码是否已存在
cursor.execute("SELECT id FROM tenants WHERE code = %s", (data.code,))
if cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="租户编码已存在",
)
# 检查域名是否已存在
cursor.execute("SELECT id FROM tenants WHERE domain = %s", (data.domain,))
if cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="域名已被使用",
)
# 创建租户
cursor.execute(
"""
INSERT INTO tenants
(code, name, display_name, domain, logo_url, favicon_url,
contact_name, contact_phone, contact_email, industry, remarks, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(data.code, data.name, data.display_name, data.domain,
data.logo_url, data.favicon_url, data.contact_name,
data.contact_phone, data.contact_email, data.industry,
data.remarks, admin.id)
)
tenant_id = cursor.lastrowid
# 记录操作日志
log_operation(
cursor, admin, tenant_id, data.code,
"create", "tenant", tenant_id, data.name,
new_value=data.model_dump()
)
conn.commit()
# 返回创建的租户
return await get_tenant(tenant_id, admin)
finally:
conn.close()
@router.put("/{tenant_id}", response_model=TenantResponse, summary="更新租户")
async def update_tenant(
tenant_id: int,
data: TenantUpdate,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""更新租户信息"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 获取原租户信息
cursor.execute("SELECT * FROM tenants WHERE id = %s", (tenant_id,))
old_tenant = cursor.fetchone()
if not old_tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在",
)
# 如果更新域名,检查是否已被使用
if data.domain and data.domain != old_tenant["domain"]:
cursor.execute(
"SELECT id FROM tenants WHERE domain = %s AND id != %s",
(data.domain, tenant_id)
)
if cursor.fetchone():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="域名已被使用",
)
# 构建更新语句
update_fields = []
update_values = []
for field, value in data.model_dump(exclude_unset=True).items():
if value is not None:
update_fields.append(f"{field} = %s")
update_values.append(value)
if not update_fields:
return await get_tenant(tenant_id, admin)
update_fields.append("updated_by = %s")
update_values.append(admin.id)
update_values.append(tenant_id)
cursor.execute(
f"UPDATE tenants SET {', '.join(update_fields)} WHERE id = %s",
update_values
)
# 记录操作日志
log_operation(
cursor, admin, tenant_id, old_tenant["code"],
"update", "tenant", tenant_id, old_tenant["name"],
old_value=dict(old_tenant),
new_value=data.model_dump(exclude_unset=True)
)
conn.commit()
return await get_tenant(tenant_id, admin)
finally:
conn.close()
@router.delete("/{tenant_id}", response_model=ResponseModel, summary="删除租户")
async def delete_tenant(
tenant_id: int,
admin: AdminUserInfo = Depends(require_superadmin),
):
"""
删除租户
需要超级管理员权限
警告:此操作将删除租户及其所有配置
"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 获取租户信息
cursor.execute("SELECT * FROM tenants WHERE id = %s", (tenant_id,))
tenant = cursor.fetchone()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在",
)
# 记录操作日志
log_operation(
cursor, admin, tenant_id, tenant["code"],
"delete", "tenant", tenant_id, tenant["name"],
old_value=dict(tenant)
)
# 删除租户(级联删除配置)
cursor.execute("DELETE FROM tenants WHERE id = %s", (tenant_id,))
conn.commit()
return ResponseModel(message=f"租户 {tenant['name']} 已删除")
finally:
conn.close()
@router.post("/{tenant_id}/enable", response_model=ResponseModel, summary="启用租户")
async def enable_tenant(
tenant_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""启用租户"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"UPDATE tenants SET status = 'active', updated_by = %s WHERE id = %s",
(admin.id, tenant_id)
)
if cursor.rowcount == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在",
)
# 获取租户信息并记录日志
cursor.execute("SELECT code, name FROM tenants WHERE id = %s", (tenant_id,))
tenant = cursor.fetchone()
log_operation(
cursor, admin, tenant_id, tenant["code"],
"enable", "tenant", tenant_id, tenant["name"]
)
conn.commit()
return ResponseModel(message="租户已启用")
finally:
conn.close()
@router.post("/{tenant_id}/disable", response_model=ResponseModel, summary="禁用租户")
async def disable_tenant(
tenant_id: int,
admin: AdminUserInfo = Depends(get_current_admin),
):
"""禁用租户"""
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"UPDATE tenants SET status = 'inactive', updated_by = %s WHERE id = %s",
(admin.id, tenant_id)
)
if cursor.rowcount == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在",
)
# 获取租户信息并记录日志
cursor.execute("SELECT code, name FROM tenants WHERE id = %s", (tenant_id,))
tenant = cursor.fetchone()
log_operation(
cursor, admin, tenant_id, tenant["code"],
"disable", "tenant", tenant_id, tenant["name"]
)
conn.commit()
return ResponseModel(message="租户已禁用")
finally:
conn.close()