""" 租户管理 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()