feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View 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()