- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
380 lines
12 KiB
Python
380 lines
12 KiB
Python
"""
|
||
租户管理 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()
|
||
|