feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
379
backend/app/api/v1/admin_portal/tenants.py
Normal file
379
backend/app/api/v1/admin_portal/tenants.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""
|
||||
租户管理 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()
|
||||
|
||||
Reference in New Issue
Block a user