Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
用户模块
|
||||
"""
|
||||
from core.user.model import User
|
||||
from core.user.service import UserService
|
||||
|
||||
__all__ = ["User", "UserService"]
|
||||
@@ -0,0 +1,506 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
User API - 用户接口
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import settings
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from core.user.schema import (
|
||||
UserCreate, UserUpdate, UserResponse, UserSimple, OrgChartNode, OrgChartChainNode,
|
||||
UserPasswordResetIn, UserPasswordSetIn,
|
||||
UserBatchDeleteIn, UserBatchDeleteOut,
|
||||
UserBatchUpdateStatusIn, UserBatchUpdateStatusOut,
|
||||
UserProfileUpdateIn
|
||||
)
|
||||
from core.user.service import UserService
|
||||
from utils.security import get_current_user, get_current_user_id
|
||||
|
||||
router = APIRouter(prefix="/user", tags=["用户管理"])
|
||||
|
||||
|
||||
@router.post("", response_model=UserResponse, summary="创建用户")
|
||||
async def create_user(data: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""创建用户"""
|
||||
# 用户名唯一性校验
|
||||
if not await UserService.check_unique(db, field="username", value=data.username):
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
# 邮箱唯一性校验
|
||||
if data.email:
|
||||
if not await UserService.check_unique(db, field="email", value=data.email):
|
||||
raise HTTPException(status_code=400, detail="邮箱已存在")
|
||||
|
||||
# 手机号唯一性校验
|
||||
if data.mobile:
|
||||
if not await UserService.check_unique(db, field="mobile", value=data.mobile):
|
||||
raise HTTPException(status_code=400, detail="手机号已存在")
|
||||
|
||||
user = await UserService.create(db=db, data=data)
|
||||
|
||||
# 构建响应并添加 role_ids
|
||||
user_dict = _build_user_response(user).model_dump()
|
||||
role_ids = await UserService.get_user_role_ids(db, user.id)
|
||||
user_dict['role_ids'] = role_ids
|
||||
return user_dict
|
||||
|
||||
|
||||
@router.get("", response_model=PaginatedResponse[UserResponse], summary="获取用户列表")
|
||||
async def get_user_list(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize",
|
||||
description="每页数量"),
|
||||
name: Optional[str] = Query(None, description="姓名"),
|
||||
username: Optional[str] = Query(None, description="用户名"),
|
||||
mobile: Optional[str] = Query(None, description="手机号"),
|
||||
email: Optional[str] = Query(None, description="邮箱"),
|
||||
user_status: Optional[int] = Query(None, alias="user_status", description="用户状态"),
|
||||
user_type: Optional[int] = Query(None, alias="user_type", description="用户类型"),
|
||||
dept_id: Optional[str] = Query(None, alias="dept_ids", description="部门ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取用户列表(分页)"""
|
||||
from core.user.model import User
|
||||
|
||||
filters = []
|
||||
if name:
|
||||
filters.append(User.name.ilike(f"%{name}%"))
|
||||
if username:
|
||||
filters.append(User.username.ilike(f"%{username}%"))
|
||||
if mobile:
|
||||
filters.append(User.mobile.ilike(f"%{mobile}%"))
|
||||
if email:
|
||||
filters.append(User.email.ilike(f"%{email}%"))
|
||||
if user_status is not None:
|
||||
filters.append(User.user_status == user_status)
|
||||
if user_type is not None:
|
||||
filters.append(User.user_type == user_type)
|
||||
if dept_id:
|
||||
filters.append(User.dept_id == dept_id)
|
||||
|
||||
items, total = await UserService.get_list(db, page=page, page_size=page_size, filters=filters)
|
||||
|
||||
# 为每个用户添加 role_ids
|
||||
response_items = []
|
||||
for item in items:
|
||||
user_dict = _build_user_response(item).model_dump()
|
||||
# 获取用户的角色ID列表
|
||||
role_ids = await UserService.get_user_role_ids(db, item.id)
|
||||
user_dict['role_ids'] = role_ids
|
||||
response_items.append(user_dict)
|
||||
|
||||
return PaginatedResponse(
|
||||
items=response_items,
|
||||
total=total
|
||||
)
|
||||
|
||||
|
||||
@router.get("/simple", response_model=List[UserSimple], summary="获取用户简单列表")
|
||||
async def get_user_simple_list(
|
||||
user_status: Optional[int] = Query(None, alias="userStatus", description="用户状态"),
|
||||
dept_id: Optional[str] = Query(None, alias="deptId", description="部门ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取用户简单列表(用于选择器)"""
|
||||
from core.user.model import User
|
||||
|
||||
filters = []
|
||||
if user_status is not None:
|
||||
filters.append(User.user_status == user_status)
|
||||
if dept_id:
|
||||
filters.append(User.dept_id == dept_id)
|
||||
|
||||
items, _ = await UserService.get_list(db, page=1, page_size=1000, filters=filters)
|
||||
return [_build_user_simple(item) for item in items]
|
||||
|
||||
|
||||
@router.get("/export/excel", summary="导出用户Excel")
|
||||
async def export_user_excel(db: AsyncSession = Depends(get_db)):
|
||||
"""导出用户到Excel"""
|
||||
output = await UserService.export_to_excel(db)
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=users.xlsx"}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/import/template", summary="下载用户导入模板")
|
||||
async def download_user_template():
|
||||
"""下载用户导入模板"""
|
||||
output = UserService.get_import_template()
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=user_template.xlsx"}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/import/excel", response_model=ResponseModel, summary="导入用户Excel")
|
||||
async def import_user_excel(
|
||||
file: UploadFile = File(..., description="Excel文件(.xlsx)"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""从Excel导入用户"""
|
||||
if not file.filename.endswith(".xlsx"):
|
||||
raise HTTPException(status_code=400, detail="只支持.xlsx格式")
|
||||
|
||||
content = await file.read()
|
||||
success, fail = await UserService.import_from_excel(db, content)
|
||||
return ResponseModel(message=f"成功{success}条,失败{fail}条", data={"success": success, "fail": fail})
|
||||
|
||||
|
||||
@router.get("/check/unique", response_model=ResponseModel, summary="检查用户唯一性")
|
||||
async def check_user_unique(
|
||||
field: str = Query(..., description="字段名"),
|
||||
value: str = Query(..., description="字段值"),
|
||||
exclude_id: Optional[str] = Query(None, alias="excludeId", description="排除ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""检查用户字段唯一性"""
|
||||
allowed_fields = ["username", "email", "mobile"]
|
||||
if field not in allowed_fields:
|
||||
raise HTTPException(status_code=400, detail=f"不支持检查字段: {field}")
|
||||
|
||||
is_unique = await UserService.check_unique(db, field=field, value=value, exclude_id=exclude_id)
|
||||
return ResponseModel(message="可用" if is_unique else "已存在", data={"unique": is_unique})
|
||||
|
||||
|
||||
@router.post("/batch_delete", response_model=UserBatchDeleteOut, summary="批量删除用户")
|
||||
async def batch_delete_users(
|
||||
data: UserBatchDeleteIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量删除用户"""
|
||||
success_count, fail_count = await UserService.batch_delete(db, data.ids)
|
||||
return UserBatchDeleteOut(success_count=success_count, fail_count=fail_count)
|
||||
|
||||
|
||||
@router.post("/batch/status", response_model=UserBatchUpdateStatusOut, summary="批量更新用户状态")
|
||||
async def batch_update_user_status(
|
||||
data: UserBatchUpdateStatusIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量更新用户状态"""
|
||||
count = await UserService.batch_update_status(db, data.ids, data.user_status)
|
||||
return UserBatchUpdateStatusOut(count=count)
|
||||
|
||||
|
||||
@router.get("/org-chart/top", response_model=List[OrgChartNode], summary="获取组织架构顶层节点")
|
||||
async def get_org_chart_top(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取组织架构顶层用户(无上级)"""
|
||||
users = await UserService.get_top_users(db)
|
||||
result = []
|
||||
for user in users:
|
||||
count = await UserService.get_subordinate_count(db, user.id)
|
||||
result.append(_build_org_chart_node(user, count))
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/org-chart/{user_id}", response_model=OrgChartNode, summary="获取指定用户的组织架构节点")
|
||||
async def get_org_chart_node(
|
||||
user_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取指定用户的组织架构节点信息"""
|
||||
user = await UserService.get_by_id(db, record_id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
count = await UserService.get_subordinate_count(db, user.id)
|
||||
return _build_org_chart_node(user, count)
|
||||
|
||||
|
||||
@router.get("/org-chart/{user_id}/chain", response_model=OrgChartChainNode, summary="获取用户汇报链")
|
||||
async def get_org_chart_chain(
|
||||
user_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取从指定用户到顶层的完整汇报链(嵌套树结构,顶层为根)"""
|
||||
chain = await UserService.get_report_chain(db, user_id)
|
||||
if not chain:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# chain: [当前用户, 上级, 上上级, ..., 顶层]
|
||||
# 从当前用户开始往上构建嵌套,每个上级把下级作为 children
|
||||
result = None
|
||||
for user in chain:
|
||||
count = await UserService.get_subordinate_count(db, user.id)
|
||||
node = _build_org_chart_node(user, count)
|
||||
chain_node = OrgChartChainNode(
|
||||
id=node.id,
|
||||
name=node.name,
|
||||
username=node.username,
|
||||
avatar=node.avatar,
|
||||
dept_name=node.dept_name,
|
||||
post_name=node.post_name,
|
||||
subordinate_count=node.subordinate_count,
|
||||
children=[result] if result else [],
|
||||
)
|
||||
result = chain_node
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/org-chart/{user_id}/children", response_model=List[OrgChartNode], summary="获取组织架构子节点")
|
||||
async def get_org_chart_children(
|
||||
user_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取指定用户的下属(组织架构子节点)"""
|
||||
subordinates = await UserService.get_subordinates(db, user_id)
|
||||
result = []
|
||||
for user in subordinates:
|
||||
count = await UserService.get_subordinate_count(db, user.id)
|
||||
result.append(_build_org_chart_node(user, count))
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{user_id}/subordinates", response_model=List[UserSimple], summary="获取下属用户")
|
||||
async def get_user_subordinates(
|
||||
user_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取下属用户列表"""
|
||||
subordinates = await UserService.get_subordinates(db, user_id)
|
||||
return [_build_user_simple(item) for item in subordinates]
|
||||
|
||||
|
||||
@router.post("/{user_id}/reset-password", response_model=ResponseModel, summary="重置用户密码")
|
||||
async def reset_user_password(
|
||||
user_id: str,
|
||||
data: UserPasswordSetIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""管理员重置用户密码"""
|
||||
success = await UserService.reset_password(db, user_id, data.new_password)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return ResponseModel(message="密码重置成功")
|
||||
|
||||
|
||||
@router.post("/change-password", response_model=ResponseModel, summary="当前用户修改密码")
|
||||
async def change_my_password(
|
||||
data: UserPasswordResetIn,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""当前登录用户修改自己的密码"""
|
||||
success, message = await UserService.change_password(
|
||||
db, user_id, data.old_password, data.new_password
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return ResponseModel(message=message)
|
||||
|
||||
|
||||
@router.post("/{user_id}/change-password", response_model=ResponseModel, summary="管理员修改用户密码")
|
||||
async def change_user_password(
|
||||
user_id: str,
|
||||
data: UserPasswordResetIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""管理员修改指定用户的密码"""
|
||||
success, message = await UserService.change_password(
|
||||
db, user_id, data.old_password, data.new_password
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return ResponseModel(message=message)
|
||||
|
||||
|
||||
@router.get("/profile", response_model=UserResponse, summary="获取当前用户详细信息")
|
||||
async def get_my_profile(
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
"""获取当前登录用户的详细信息"""
|
||||
return _build_user_response(current_user)
|
||||
|
||||
|
||||
@router.put("/profile", response_model=UserResponse, summary="更新当前用户个人信息")
|
||||
async def update_my_profile(
|
||||
data: UserProfileUpdateIn,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""更新当前登录用户的个人信息"""
|
||||
# 邮箱唯一性校验
|
||||
if data.email:
|
||||
if not await UserService.check_unique(db, field="email", value=data.email, exclude_id=user_id):
|
||||
raise HTTPException(status_code=400, detail="邮箱已存在")
|
||||
|
||||
# 手机号唯一性校验
|
||||
if data.mobile:
|
||||
if not await UserService.check_unique(db, field="mobile", value=data.mobile, exclude_id=user_id):
|
||||
raise HTTPException(status_code=400, detail="手机号已存在")
|
||||
|
||||
# 转换为UserUpdate
|
||||
update_data = UserUpdate(**data.model_dump(exclude_unset=True))
|
||||
user = await UserService.update(db, record_id=user_id, data=update_data)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return _build_user_response(user)
|
||||
|
||||
|
||||
@router.put("/{user_id}/profile", response_model=UserResponse, summary="更新指定用户个人信息(管理员)")
|
||||
async def update_user_profile(
|
||||
user_id: str,
|
||||
data: UserProfileUpdateIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""更新指定用户的个人信息(管理员功能)"""
|
||||
# 邮箱唯一性校验
|
||||
if data.email:
|
||||
if not await UserService.check_unique(db, field="email", value=data.email, exclude_id=user_id):
|
||||
raise HTTPException(status_code=400, detail="邮箱已存在")
|
||||
|
||||
# 手机号唯一性校验
|
||||
if data.mobile:
|
||||
if not await UserService.check_unique(db, field="mobile", value=data.mobile, exclude_id=user_id):
|
||||
raise HTTPException(status_code=400, detail="手机号已存在")
|
||||
|
||||
# 转换为UserUpdate
|
||||
update_data = UserUpdate(**data.model_dump(exclude_unset=True))
|
||||
user = await UserService.update(db, record_id=user_id, data=update_data)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return _build_user_response(user)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse, summary="获取用户详情")
|
||||
async def get_user_by_id(user_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""获取用户详情"""
|
||||
user = await UserService.get_by_id(db, user_id)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 构建响应并添加 role_ids
|
||||
user_dict = _build_user_response(user).model_dump()
|
||||
role_ids = await UserService.get_user_role_ids(db, user_id)
|
||||
user_dict['role_ids'] = role_ids
|
||||
return user_dict
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=UserResponse, summary="更新用户")
|
||||
async def update_user(user_id: str, data: UserUpdate, db: AsyncSession = Depends(get_db)):
|
||||
"""更新用户"""
|
||||
# 用户名唯一性校验
|
||||
if data.username:
|
||||
if not await UserService.check_unique(db, field="username", value=data.username, exclude_id=user_id):
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
# 邮箱唯一性校验
|
||||
if data.email:
|
||||
if not await UserService.check_unique(db, field="email", value=data.email, exclude_id=user_id):
|
||||
raise HTTPException(status_code=400, detail="邮箱已存在")
|
||||
|
||||
# 手机号唯一性校验
|
||||
if data.mobile:
|
||||
if not await UserService.check_unique(db, field="mobile", value=data.mobile, exclude_id=user_id):
|
||||
raise HTTPException(status_code=400, detail="手机号已存在")
|
||||
|
||||
user = await UserService.update(db, record_id=user_id, data=data)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 构建响应并添加 role_ids
|
||||
user_dict = _build_user_response(user).model_dump()
|
||||
role_ids = await UserService.get_user_role_ids(db, user_id)
|
||||
user_dict['role_ids'] = role_ids
|
||||
return user_dict
|
||||
|
||||
|
||||
@router.delete("/{user_id}", response_model=ResponseModel, summary="删除用户")
|
||||
async def delete_user(
|
||||
user_id: str,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除用户"""
|
||||
user = await UserService.get_by_id(db, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
if not user.can_delete():
|
||||
raise HTTPException(status_code=400, detail="系统用户或超级管理员不能删除")
|
||||
|
||||
success = await UserService.delete(db, record_id=user_id, hard=hard)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
def _build_user_response(user) -> UserResponse:
|
||||
"""构建用户响应"""
|
||||
# 获取岗位名称
|
||||
post_name = None
|
||||
if hasattr(user, 'post') and user.post:
|
||||
post_name = user.post.name
|
||||
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
mobile=user.mobile,
|
||||
avatar=user.avatar,
|
||||
name=user.name,
|
||||
gender=user.gender if user.gender is not None else 0,
|
||||
gender_display=user.get_gender_display(),
|
||||
user_type=user.user_type if user.user_type is not None else 1,
|
||||
user_type_display=user.get_user_type_display(),
|
||||
user_status=user.user_status if user.user_status is not None else 1,
|
||||
user_status_display=user.get_user_status_display(),
|
||||
birthday=user.birthday,
|
||||
city=user.city,
|
||||
address=user.address,
|
||||
bio=user.bio,
|
||||
is_superuser=user.is_superuser,
|
||||
is_active=user.is_active,
|
||||
dept_id=user.dept_id,
|
||||
post_id=user.post_id,
|
||||
post_name=post_name,
|
||||
manager_id=user.manager_id,
|
||||
last_login=user.last_login,
|
||||
last_login_ip=user.last_login_ip,
|
||||
last_login_type=user.last_login_type,
|
||||
sort=user.sort,
|
||||
is_deleted=user.is_deleted,
|
||||
sys_create_datetime=user.sys_create_datetime,
|
||||
sys_update_datetime=user.sys_update_datetime,
|
||||
)
|
||||
|
||||
|
||||
def _build_org_chart_node(user, subordinate_count: int = 0) -> OrgChartNode:
|
||||
"""构建组织架构节点"""
|
||||
post_name = None
|
||||
if hasattr(user, 'post') and user.post:
|
||||
post_name = user.post.name
|
||||
return OrgChartNode(
|
||||
id=user.id,
|
||||
name=user.name,
|
||||
username=user.username,
|
||||
avatar=user.avatar,
|
||||
dept_name=user.dept.name if user.dept else None,
|
||||
post_name=post_name,
|
||||
subordinate_count=subordinate_count,
|
||||
)
|
||||
|
||||
|
||||
def _build_user_simple(user) -> UserSimple:
|
||||
"""构建用户简单响应"""
|
||||
return UserSimple(
|
||||
id=user.id,
|
||||
name=user.name,
|
||||
username=user.username,
|
||||
avatar=user.avatar,
|
||||
email=user.email,
|
||||
mobile=user.mobile,
|
||||
dept_name=user.dept.name if user.dept else None,
|
||||
)
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
User Model - 用户模型
|
||||
用于管理系统用户
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import Column, String, Text, Boolean, Integer, Date, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
"""
|
||||
用户模型 - 系统用户管理
|
||||
|
||||
字段说明:
|
||||
- username: 用户名(唯一)
|
||||
- password: 密码(加密存储)
|
||||
- is_superuser: 是否为超级管理员
|
||||
- email: 邮箱
|
||||
- mobile: 手机号
|
||||
- avatar: 头像
|
||||
- name: 真实姓名
|
||||
- gender: 性别(0-未知, 1-男, 2-女)
|
||||
- user_type: 用户类型(0-系统用户, 1-普通用户, 2-外部用户)
|
||||
- user_status: 用户状态(0-禁用, 1-正常, 2-锁定)
|
||||
- dept_id: 所属部门ID
|
||||
- manager_id: 直属上级ID
|
||||
- is_active: 是否激活
|
||||
"""
|
||||
__tablename__ = "core_user"
|
||||
|
||||
# 用户名
|
||||
username = Column(String(150), unique=True, nullable=False, index=True, comment="用户名")
|
||||
|
||||
# 密码(加密存储,OAuth用户可为空)
|
||||
password = Column(String(128), nullable=True, comment="密码")
|
||||
|
||||
# 是否为超级管理员
|
||||
is_superuser = Column(Boolean, default=False, index=True, comment="是否超级管理员")
|
||||
|
||||
# 最后登录时间
|
||||
last_login = Column(DateTime, nullable=True, comment="最后登录时间")
|
||||
|
||||
# 邮箱
|
||||
email = Column(String(255), nullable=True, index=True, comment="邮箱")
|
||||
|
||||
# 手机号
|
||||
mobile = Column(String(11), nullable=True, index=True, comment="手机号")
|
||||
|
||||
# 头像
|
||||
avatar = Column(String(36), nullable=True, comment="头像UUID")
|
||||
|
||||
# 真实姓名
|
||||
name = Column(String(64), nullable=True, index=True, comment="真实姓名")
|
||||
|
||||
# 性别:0-未知, 1-男, 2-女
|
||||
gender = Column(Integer, default=0, nullable=True, comment="性别")
|
||||
|
||||
# 用户类型:0-系统用户, 1-普通用户, 2-外部用户
|
||||
user_type = Column(Integer, default=1, index=True, comment="用户类型")
|
||||
|
||||
# 用户状态:0-禁用, 1-正常, 2-锁定
|
||||
user_status = Column(Integer, default=1, index=True, comment="用户状态")
|
||||
|
||||
# 生日
|
||||
birthday = Column(Date, nullable=True, comment="生日")
|
||||
|
||||
# 所在城市
|
||||
city = Column(String(100), nullable=True, comment="所在城市")
|
||||
|
||||
# 地址
|
||||
address = Column(String(200), nullable=True, comment="详细地址")
|
||||
|
||||
# 个人简介
|
||||
bio = Column(Text, nullable=True, comment="个人简介")
|
||||
|
||||
# 最后登录IP
|
||||
last_login_ip = Column(String(45), nullable=True, comment="最后登录IP")
|
||||
|
||||
# 最后登录方式
|
||||
last_login_type = Column(String(20), nullable=True, index=True, comment="最后登录方式")
|
||||
|
||||
# 是否激活
|
||||
is_active = Column(Boolean, default=True, index=True, comment="是否激活")
|
||||
|
||||
# 所属部门ID(逻辑外键,不创建数据库约束)
|
||||
dept_id = Column(String(21), nullable=True, index=True, comment="所属部门ID")
|
||||
|
||||
# 所属岗位ID(逻辑外键,不创建数据库约束)
|
||||
post_id = Column(String(21), nullable=True, index=True, comment="所属岗位ID")
|
||||
|
||||
# 所属角色ID(逻辑外键,不创建数据库约束)
|
||||
# 注意:已改为多对多关系,通过 core_user_role 表关联
|
||||
# 保留此字段用于数据迁移,后续版本可删除
|
||||
role_id = Column(String(21), nullable=True, index=True, comment="所属角色ID(已废弃)")
|
||||
|
||||
# 直属上级ID(逻辑外键,不创建数据库约束)
|
||||
manager_id = Column(String(21), nullable=True, comment="直属上级ID")
|
||||
|
||||
# OAuth 相关字段
|
||||
oauth_provider = Column(String(50), nullable=True, index=True, comment="OAuth提供商")
|
||||
gitee_id = Column(String(200), unique=True, nullable=True, index=True, comment="Gitee用户ID")
|
||||
github_id = Column(String(200), unique=True, nullable=True, index=True, comment="GitHub用户ID")
|
||||
qq_id = Column(String(200), unique=True, nullable=True, index=True, comment="QQ用户openid")
|
||||
google_id = Column(String(200), unique=True, nullable=True, index=True, comment="Google用户ID")
|
||||
wechat_unionid = Column(String(200), unique=True, nullable=True, index=True, comment="微信UnionID")
|
||||
wechat_openid = Column(String(200), nullable=True, index=True, comment="微信OpenID")
|
||||
microsoft_id = Column(String(200), unique=True, nullable=True, index=True, comment="Microsoft用户ID")
|
||||
dingtalk_unionid = Column(String(200), unique=True, nullable=True, index=True, comment="钉钉UnionID")
|
||||
dingtalk_userid = Column(String(200), unique=True, nullable=True, index=True, comment="钉钉用户UserID")
|
||||
feishu_union_id = Column(String(200), unique=True, nullable=True, index=True, comment="飞书UnionID")
|
||||
feishu_userid = Column(String(200), unique=True, nullable=True, index=True, comment="飞书用户OpenID")
|
||||
wecom_userid = Column(String(200), unique=True, nullable=True, index=True, comment="企业微信UserID")
|
||||
|
||||
# 关系定义(使用primaryjoin指定逻辑关联,lazy='selectin'支持异步加载)
|
||||
dept = relationship("Dept", foreign_keys="User.dept_id", primaryjoin="User.dept_id == Dept.id", backref="users", lazy="selectin")
|
||||
post = relationship("Post", foreign_keys="User.post_id", primaryjoin="User.post_id == Post.id", lazy="selectin")
|
||||
manager = relationship("User", remote_side="User.id", foreign_keys="User.manager_id", primaryjoin="User.manager_id == User.id", backref="subordinates", lazy="selectin")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User {self.name or self.username} ({self.username})>"
|
||||
|
||||
def get_user_type_display(self) -> str:
|
||||
"""获取用户类型的显示名称"""
|
||||
type_map = {0: "系统用户", 1: "普通用户", 2: "外部用户"}
|
||||
return type_map.get(self.user_type, "未知")
|
||||
|
||||
def get_user_status_display(self) -> str:
|
||||
"""获取用户状态的显示名称"""
|
||||
status_map = {0: "禁用", 1: "正常", 2: "锁定"}
|
||||
return status_map.get(self.user_status, "未知")
|
||||
|
||||
def get_gender_display(self) -> str:
|
||||
"""获取性别的显示名称"""
|
||||
gender_map = {0: "未知", 1: "男", 2: "女"}
|
||||
return gender_map.get(self.gender, "未知")
|
||||
|
||||
def is_active_user(self) -> bool:
|
||||
"""判断用户是否为正常状态"""
|
||||
return self.user_status == 1
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
"""判断用户是否被锁定"""
|
||||
return self.user_status == 2
|
||||
|
||||
def is_disabled(self) -> bool:
|
||||
"""判断用户是否被禁用"""
|
||||
return self.user_status == 0
|
||||
|
||||
def can_delete(self) -> bool:
|
||||
"""判断用户是否可以删除(系统用户和超级管理员不能删除)"""
|
||||
return self.user_type != 0 and not self.is_superuser
|
||||
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
User Schema - 用户数据验证模式
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, EmailStr
|
||||
|
||||
from app.base_schema import CSTDatetime
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
"""用户基础Schema"""
|
||||
username: str = Field(..., min_length=3, max_length=150, description="用户名")
|
||||
email: Optional[str] = Field(None, max_length=255, description="邮箱")
|
||||
mobile: Optional[str] = Field(None, max_length=11, description="手机号")
|
||||
avatar: Optional[str] = Field(None, max_length=36, description="头像UUID")
|
||||
name: Optional[str] = Field(None, max_length=64, description="真实姓名")
|
||||
gender: int = Field(default=0, ge=0, le=2, description="性别:0-未知, 1-男, 2-女")
|
||||
user_type: int = Field(default=1, ge=0, le=2, description="用户类型:0-系统用户, 1-普通用户, 2-外部用户")
|
||||
user_status: int = Field(default=1, ge=0, le=2, description="用户状态:0-禁用, 1-正常, 2-锁定")
|
||||
birthday: Optional[date] = Field(None, description="生日")
|
||||
city: Optional[str] = Field(None, max_length=100, description="所在城市")
|
||||
address: Optional[str] = Field(None, max_length=200, description="详细地址")
|
||||
bio: Optional[str] = Field(None, description="个人简介")
|
||||
is_active: bool = Field(default=True, description="是否激活")
|
||||
dept_id: Optional[str] = Field(None, description="所属部门ID")
|
||||
manager_id: Optional[str] = Field(None, description="直属上级ID")
|
||||
role_ids: Optional[List[str]] = Field(default=None, description="角色ID列表")
|
||||
sort: int = Field(default=0, description="排序")
|
||||
|
||||
@field_validator("mobile")
|
||||
@classmethod
|
||||
def validate_mobile(cls, v):
|
||||
"""验证手机号"""
|
||||
if v:
|
||||
if not v.isdigit():
|
||||
raise ValueError("手机号只能包含数字")
|
||||
if len(v) != 11:
|
||||
raise ValueError("手机号必须为11位")
|
||||
return v
|
||||
|
||||
@field_validator("user_status")
|
||||
@classmethod
|
||||
def validate_user_status(cls, v):
|
||||
"""验证用户状态"""
|
||||
if v not in [0, 1, 2]:
|
||||
raise ValueError("用户状态必须为 0(禁用)、1(正常) 或 2(锁定)")
|
||||
return v
|
||||
|
||||
@field_validator("user_type")
|
||||
@classmethod
|
||||
def validate_user_type(cls, v):
|
||||
"""验证用户类型"""
|
||||
if v not in [0, 1, 2]:
|
||||
raise ValueError("用户类型必须为 0(系统用户)、1(普通用户) 或 2(外部用户)")
|
||||
return v
|
||||
|
||||
@field_validator("gender")
|
||||
@classmethod
|
||||
def validate_gender(cls, v):
|
||||
"""验证性别"""
|
||||
if v not in [0, 1, 2]:
|
||||
raise ValueError("性别必须为 0(未知)、1(男) 或 2(女)")
|
||||
return v
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""用户创建Schema"""
|
||||
# password: str = Field(..., min_length=6, max_length=20, description="密码")
|
||||
pass
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""用户更新Schema - 所有字段可选"""
|
||||
username: Optional[str] = Field(None, min_length=3, max_length=150, description="用户名")
|
||||
email: Optional[str] = Field(None, max_length=255, description="邮箱")
|
||||
mobile: Optional[str] = Field(None, max_length=11, description="手机号")
|
||||
avatar: Optional[str] = Field(None, max_length=36, description="头像UUID")
|
||||
name: Optional[str] = Field(None, max_length=64, description="真实姓名")
|
||||
gender: Optional[int] = Field(None, ge=0, le=2, description="性别")
|
||||
user_type: Optional[int] = Field(None, ge=0, le=2, description="用户类型")
|
||||
user_status: Optional[int] = Field(None, ge=0, le=2, description="用户状态")
|
||||
birthday: Optional[date] = Field(None, description="生日")
|
||||
city: Optional[str] = Field(None, max_length=100, description="所在城市")
|
||||
address: Optional[str] = Field(None, max_length=200, description="详细地址")
|
||||
bio: Optional[str] = Field(None, description="个人简介")
|
||||
is_active: Optional[bool] = Field(None, description="是否激活")
|
||||
dept_id: Optional[str] = Field(None, description="所属部门ID")
|
||||
manager_id: Optional[str] = Field(None, description="直属上级ID")
|
||||
role_ids: Optional[List[str]] = Field(None, description="角色ID列表")
|
||||
sort: Optional[int] = Field(None, description="排序")
|
||||
|
||||
@field_validator("mobile")
|
||||
@classmethod
|
||||
def validate_mobile(cls, v):
|
||||
"""验证手机号"""
|
||||
if v is not None and v:
|
||||
if not v.isdigit():
|
||||
raise ValueError("手机号只能包含数字")
|
||||
if len(v) != 11:
|
||||
raise ValueError("手机号必须为11位")
|
||||
return v
|
||||
|
||||
@field_validator("user_status")
|
||||
@classmethod
|
||||
def validate_user_status(cls, v):
|
||||
"""验证用户状态"""
|
||||
if v is not None and v not in [0, 1, 2]:
|
||||
raise ValueError("用户状态必须为 0(禁用)、1(正常) 或 2(锁定)")
|
||||
return v
|
||||
|
||||
@field_validator("user_type")
|
||||
@classmethod
|
||||
def validate_user_type(cls, v):
|
||||
"""验证用户类型"""
|
||||
if v is not None and v not in [0, 1, 2]:
|
||||
raise ValueError("用户类型必须为 0(系统用户)、1(普通用户) 或 2(外部用户)")
|
||||
return v
|
||||
|
||||
@field_validator("gender")
|
||||
@classmethod
|
||||
def validate_gender(cls, v):
|
||||
"""验证性别"""
|
||||
if v is not None and v not in [0, 1, 2]:
|
||||
raise ValueError("性别必须为 0(未知)、1(男) 或 2(女)")
|
||||
return v
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""用户响应Schema"""
|
||||
id: str
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
gender: int = 0
|
||||
gender_display: Optional[str] = None
|
||||
user_type: int = 1
|
||||
user_type_display: Optional[str] = None
|
||||
user_status: int = 1
|
||||
user_status_display: Optional[str] = None
|
||||
birthday: Optional[date] = None
|
||||
city: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
bio: Optional[str] = None
|
||||
is_superuser: bool = False
|
||||
is_active: bool = True
|
||||
dept_id: Optional[str] = None
|
||||
post_id: Optional[str] = None
|
||||
post_name: Optional[str] = None
|
||||
manager_id: Optional[str] = None
|
||||
role_ids: Optional[List[str]] = None
|
||||
last_login: Optional[CSTDatetime] = None
|
||||
last_login_ip: Optional[str] = None
|
||||
last_login_type: Optional[str] = None
|
||||
sort: int = 0
|
||||
is_deleted: bool = False
|
||||
sys_create_datetime: Optional[CSTDatetime] = None
|
||||
sys_update_datetime: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UserSimple(BaseModel):
|
||||
"""用户简单输出(用于选择器)"""
|
||||
id: str
|
||||
name: Optional[str] = None
|
||||
username: str
|
||||
avatar: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
dept_name: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class OrgChartNode(BaseModel):
|
||||
"""组织架构节点"""
|
||||
id: str
|
||||
name: Optional[str] = None
|
||||
username: str
|
||||
avatar: Optional[str] = None
|
||||
dept_name: Optional[str] = None
|
||||
post_name: Optional[str] = None
|
||||
subordinate_count: int = 0
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class OrgChartChainNode(BaseModel):
|
||||
"""组织架构汇报链节点(嵌套结构)"""
|
||||
id: str
|
||||
name: Optional[str] = None
|
||||
username: str
|
||||
avatar: Optional[str] = None
|
||||
dept_name: Optional[str] = None
|
||||
post_name: Optional[str] = None
|
||||
subordinate_count: int = 0
|
||||
children: List["OrgChartChainNode"] = []
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UserPasswordResetIn(BaseModel):
|
||||
"""重置密码输入"""
|
||||
old_password: str = Field(..., description="旧密码")
|
||||
new_password: str = Field(..., min_length=6, max_length=20, description="新密码")
|
||||
confirm_password: str = Field(..., description="确认新密码")
|
||||
|
||||
@field_validator("confirm_password")
|
||||
@classmethod
|
||||
def validate_confirm_password(cls, v, info):
|
||||
"""验证确认密码"""
|
||||
if info.data.get("new_password") and v != info.data.get("new_password"):
|
||||
raise ValueError("两次输入的密码不一致")
|
||||
return v
|
||||
|
||||
|
||||
class UserPasswordSetIn(BaseModel):
|
||||
"""管理员设置密码输入"""
|
||||
new_password: str = Field(..., min_length=6, max_length=20, description="新密码")
|
||||
|
||||
|
||||
class UserBatchDeleteIn(BaseModel):
|
||||
"""批量删除用户输入"""
|
||||
ids: List[str] = Field(..., description="要删除的用户ID列表")
|
||||
|
||||
|
||||
class UserBatchDeleteOut(BaseModel):
|
||||
"""批量删除用户输出"""
|
||||
success_count: int = Field(..., description="删除的记录数")
|
||||
fail_count: int = Field(..., description="删除失败的ID列表")
|
||||
|
||||
|
||||
class UserBatchUpdateStatusIn(BaseModel):
|
||||
"""批量更新用户状态输入"""
|
||||
ids: List[str] = Field(..., description="用户ID列表")
|
||||
user_status: int = Field(..., ge=0, le=2, description="用户状态:0-禁用,1-正常,2-锁定")
|
||||
|
||||
@field_validator("user_status")
|
||||
@classmethod
|
||||
def validate_user_status(cls, v):
|
||||
"""验证用户状态"""
|
||||
if v not in [0, 1, 2]:
|
||||
raise ValueError("用户状态必须为 0(禁用)、1(正常) 或 2(锁定)")
|
||||
return v
|
||||
|
||||
|
||||
class UserBatchUpdateStatusOut(BaseModel):
|
||||
"""批量更新用户状态输出"""
|
||||
count: int = Field(..., description="更新的记录数")
|
||||
|
||||
|
||||
class UserProfileUpdateIn(BaseModel):
|
||||
"""用户个人信息更新输入"""
|
||||
name: Optional[str] = Field(None, max_length=64, description="真实姓名")
|
||||
email: Optional[str] = Field(None, max_length=255, description="邮箱")
|
||||
mobile: Optional[str] = Field(None, max_length=11, description="手机号")
|
||||
avatar: Optional[str] = Field(None, max_length=36, description="头像UUID")
|
||||
gender: Optional[int] = Field(None, ge=0, le=2, description="性别")
|
||||
birthday: Optional[date] = Field(None, description="生日")
|
||||
city: Optional[str] = Field(None, max_length=100, description="所在城市")
|
||||
address: Optional[str] = Field(None, max_length=200, description="详细地址")
|
||||
bio: Optional[str] = Field(None, description="个人简介")
|
||||
|
||||
@field_validator("birthday", mode="before")
|
||||
@classmethod
|
||||
def validate_birthday(cls, v):
|
||||
"""验证生日,将空字符串转换为None"""
|
||||
if v == "" or v is None:
|
||||
return None
|
||||
return v
|
||||
|
||||
@field_validator("mobile")
|
||||
@classmethod
|
||||
def validate_mobile(cls, v):
|
||||
"""验证手机号"""
|
||||
if v is not None and v:
|
||||
if not v.isdigit():
|
||||
raise ValueError("手机号只能包含数字")
|
||||
if len(v) != 11:
|
||||
raise ValueError("手机号必须为11位")
|
||||
return v
|
||||
|
||||
@field_validator("gender")
|
||||
@classmethod
|
||||
def validate_gender(cls, v):
|
||||
"""验证性别"""
|
||||
if v is not None and v not in [0, 1, 2]:
|
||||
raise ValueError("性别必须为 0(未知)、1(男) 或 2(女)")
|
||||
return v
|
||||
@@ -0,0 +1,507 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
User Service - 用户服务层
|
||||
"""
|
||||
from io import BytesIO
|
||||
from typing import Tuple, Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.base_service import BaseService
|
||||
from core.user.model import User
|
||||
from core.user.schema import UserCreate, UserUpdate
|
||||
from utils.security import hash_password as _hash_password, verify_password as _verify_password
|
||||
|
||||
|
||||
class UserService(BaseService[User, UserCreate, UserUpdate]):
|
||||
"""
|
||||
用户服务层
|
||||
继承BaseService,自动获得增删改查功能
|
||||
"""
|
||||
|
||||
model = User
|
||||
|
||||
# Excel导入导出配置
|
||||
excel_columns = {
|
||||
"username": "用户名",
|
||||
"name": "姓名",
|
||||
"email": "邮箱",
|
||||
"mobile": "手机号",
|
||||
"gender": "性别",
|
||||
"user_type": "用户类型",
|
||||
"user_status": "用户状态",
|
||||
}
|
||||
excel_sheet_name = "用户列表"
|
||||
|
||||
# 使用 generate_field_metadata 自定义字段元数据
|
||||
# 只需指定敏感字段、可脱敏字段和隐藏字段即可
|
||||
from app.field_metadata_generator import generate_field_metadata
|
||||
FIELD_METADATA = generate_field_metadata(
|
||||
User,
|
||||
sensitive_fields=['name', 'email', 'mobile', 'password'],
|
||||
maskable_fields=['name', 'email', 'mobile'],
|
||||
hidden_fields=['password'],
|
||||
field_labels={
|
||||
'username': '用户名',
|
||||
'name': '姓名',
|
||||
'email': '邮箱',
|
||||
'mobile': '手机号',
|
||||
'password': '密码',
|
||||
'gender': '性别',
|
||||
'avatar': '头像',
|
||||
'user_type': '用户类型',
|
||||
'user_status': '用户状态',
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def hash_password(cls, password: str) -> str:
|
||||
"""加密密码"""
|
||||
return _hash_password(password)
|
||||
|
||||
@classmethod
|
||||
def verify_password(cls, plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return _verify_password(plain_password, hashed_password)
|
||||
|
||||
@classmethod
|
||||
async def get_user_role_ids(cls, db: AsyncSession, user_id: str) -> List[str]:
|
||||
"""
|
||||
获取用户的所有角色ID
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户ID
|
||||
:return: 角色ID列表
|
||||
"""
|
||||
from core.user.user_role_model import UserRole
|
||||
|
||||
stmt = select(UserRole.role_id).where(
|
||||
UserRole.user_id == user_id,
|
||||
UserRole.is_deleted == False
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
role_ids = [row[0] for row in result.all()]
|
||||
|
||||
# 如果用户没有通过关联表分配角色,尝试从旧的 role_id 字段获取
|
||||
if not role_ids:
|
||||
user = await cls.get_by_id(db, user_id)
|
||||
if user and user.role_id:
|
||||
role_ids = [user.role_id]
|
||||
|
||||
return role_ids
|
||||
|
||||
@classmethod
|
||||
def _export_converter(cls, item: Any) -> Dict[str, Any]:
|
||||
"""导出数据转换器"""
|
||||
return {
|
||||
"username": item.username,
|
||||
"name": item.name or "",
|
||||
"email": item.email or "",
|
||||
"mobile": item.mobile or "",
|
||||
"gender": item.get_gender_display(),
|
||||
"user_type": item.get_user_type_display(),
|
||||
"user_status": item.get_user_status_display(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _import_processor(cls, row: Dict[str, Any]) -> Optional[User]:
|
||||
"""导入数据处理器"""
|
||||
username = row.get("username")
|
||||
if not username:
|
||||
return None
|
||||
|
||||
# 性别映射
|
||||
gender_map = {"未知": 0, "男": 1, "女": 2}
|
||||
gender_str = row.get("gender", "未知")
|
||||
gender = gender_map.get(gender_str, 0)
|
||||
|
||||
# 用户类型映射
|
||||
type_map = {"系统用户": 0, "普通用户": 1, "外部用户": 2}
|
||||
type_str = row.get("user_type", "普通用户")
|
||||
user_type = type_map.get(type_str, 1)
|
||||
|
||||
# 用户状态映射
|
||||
status_map = {"禁用": 0, "正常": 1, "锁定": 2}
|
||||
status_str = row.get("user_status", "正常")
|
||||
user_status = status_map.get(status_str, 1)
|
||||
|
||||
return User(
|
||||
username=str(username),
|
||||
password=cls.hash_password("123456"), # 默认密码
|
||||
name=str(row.get("name") or "") or None,
|
||||
email=str(row.get("email") or "") or None,
|
||||
mobile=str(row.get("mobile") or "") or None,
|
||||
gender=gender,
|
||||
user_type=user_type,
|
||||
user_status=user_status,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def export_to_excel(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
data_converter: Any = None
|
||||
) -> BytesIO:
|
||||
"""导出到Excel"""
|
||||
return await super().export_to_excel(db, cls._export_converter)
|
||||
|
||||
@classmethod
|
||||
async def import_from_excel(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
file_content: bytes,
|
||||
row_processor: Any = None
|
||||
) -> Tuple[int, int]:
|
||||
"""从Excel导入"""
|
||||
return await super().import_from_excel(db, file_content, cls._import_processor)
|
||||
|
||||
@classmethod
|
||||
async def create(cls, db: AsyncSession, data: UserCreate) -> User:
|
||||
"""
|
||||
创建用户,自动加密密码,并处理角色关联
|
||||
"""
|
||||
user_data = data.model_dump()
|
||||
# 提取 role_ids
|
||||
role_ids = user_data.pop('role_ids', None)
|
||||
|
||||
# 加密密码
|
||||
user_data["password"] = cls.hash_password('123456')
|
||||
|
||||
db_obj = User(**user_data)
|
||||
db.add(db_obj)
|
||||
await db.flush() # 先 flush 获取 user id
|
||||
|
||||
# 创建用户角色关联
|
||||
if role_ids:
|
||||
from core.user.user_role_model import UserRole
|
||||
for role_id in role_ids:
|
||||
user_role = UserRole(user_id=db_obj.id, role_id=role_id)
|
||||
db.add(user_role)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
@classmethod
|
||||
async def get_by_username(cls, db: AsyncSession, username: str) -> Optional[User]:
|
||||
"""
|
||||
根据用户名获取用户
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.username == username,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def get_by_email(cls, db: AsyncSession, email: str) -> Optional[User]:
|
||||
"""
|
||||
根据邮箱获取用户
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.email == email,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def get_by_mobile(cls, db: AsyncSession, mobile: str) -> Optional[User]:
|
||||
"""
|
||||
根据手机号获取用户
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.mobile == mobile,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def authenticate(cls, db: AsyncSession, username: str, password: str) -> Optional[User]:
|
||||
"""
|
||||
用户认证
|
||||
|
||||
:return: 认证成功返回用户,失败返回None
|
||||
"""
|
||||
user = await cls.get_by_username(db, username)
|
||||
if not user:
|
||||
return None
|
||||
if not cls.verify_password(password, user.password):
|
||||
return None
|
||||
if not user.is_active_user():
|
||||
return None
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
async def change_password(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
old_password: str,
|
||||
new_password: str
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
修改密码
|
||||
|
||||
:return: (是否成功, 消息)
|
||||
"""
|
||||
user = await cls.get_by_id(db, user_id)
|
||||
if not user:
|
||||
return False, "用户不存在"
|
||||
|
||||
if not cls.verify_password(old_password, user.password):
|
||||
return False, "原密码错误"
|
||||
|
||||
user.password = cls.hash_password(new_password)
|
||||
await db.commit()
|
||||
return True, "密码修改成功"
|
||||
|
||||
@classmethod
|
||||
async def reset_password(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
new_password: str
|
||||
) -> bool:
|
||||
"""
|
||||
重置密码(管理员操作)
|
||||
"""
|
||||
user = await cls.get_by_id(db, user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
user.password = cls.hash_password(new_password)
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def update(cls, db: AsyncSession, record_id: str, data: UserUpdate) -> Optional[User]:
|
||||
"""
|
||||
更新用户,处理角色关联
|
||||
"""
|
||||
user_data = data.model_dump(exclude_unset=True)
|
||||
# 提取 role_ids
|
||||
role_ids = user_data.pop('role_ids', None)
|
||||
|
||||
# 更新用户基本信息
|
||||
user = await cls.get_by_id(db, record_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
for key, value in user_data.items():
|
||||
setattr(user, key, value)
|
||||
|
||||
# 更新用户角色关联
|
||||
if role_ids is not None: # 只有当传递了 role_ids 时才更新
|
||||
from core.user.user_role_model import UserRole
|
||||
from sqlalchemy import delete
|
||||
|
||||
# 删除现有角色关联
|
||||
await db.execute(
|
||||
delete(UserRole).where(UserRole.user_id == record_id)
|
||||
)
|
||||
|
||||
# 创建新的角色关联
|
||||
for role_id in role_ids:
|
||||
user_role = UserRole(user_id=record_id, role_id=role_id)
|
||||
db.add(user_role)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
# 角色变更后清除缓存,使权限立即生效
|
||||
if role_ids is not None:
|
||||
await cls._invalidate_user_permission_cache(record_id)
|
||||
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
async def _invalidate_user_permission_cache(cls, user_id: str):
|
||||
"""
|
||||
清除用户相关的权限缓存,使角色变更立即生效
|
||||
|
||||
1. 清除 Redis 用户信息缓存(中间件下次请求会从数据库重新加载)
|
||||
2. 清除菜单路由 Redis 缓存
|
||||
3. 清除 API 权限内存缓存
|
||||
"""
|
||||
# 1. 清除 Redis 用户信息缓存(角色等动态信息)
|
||||
from utils.user_info_cache import delete_cached_user_info
|
||||
await delete_cached_user_info(user_id)
|
||||
|
||||
# 2. 清除该用户的菜单路由缓存
|
||||
from core.menu.service import menu_cache, USER_ROUTE_CACHE_PREFIX
|
||||
await menu_cache.delete_pattern(f"{USER_ROUTE_CACHE_PREFIX}{user_id}*")
|
||||
|
||||
# 3. 清除所有角色的 API 权限 Redis 缓存(因为不知道旧角色是哪些)
|
||||
from utils.permission import clear_all_role_permission_cache
|
||||
await clear_all_role_permission_cache()
|
||||
|
||||
@classmethod
|
||||
async def update_last_login(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
ip: Optional[str] = None,
|
||||
login_type: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
更新最后登录信息
|
||||
"""
|
||||
user = await cls.get_by_id(db, user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
user.last_login = datetime.now()
|
||||
if ip:
|
||||
user.last_login_ip = ip
|
||||
if login_type:
|
||||
user.last_login_type = login_type
|
||||
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def update_login_info(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
ip: Optional[str] = None,
|
||||
login_type: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
更新登录信息(update_last_login的别名)
|
||||
"""
|
||||
return await cls.update_last_login(db, user_id, ip, login_type)
|
||||
|
||||
@classmethod
|
||||
async def batch_update_status(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
ids: List[str],
|
||||
user_status: int
|
||||
) -> int:
|
||||
"""
|
||||
批量更新用户状态
|
||||
|
||||
:return: 更新的记录数
|
||||
"""
|
||||
count = 0
|
||||
for user_id in ids:
|
||||
user = await cls.get_by_id(db, user_id)
|
||||
if user and not user.is_superuser: # 超级管理员不能被修改状态
|
||||
user.user_status = user_status
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
await db.commit()
|
||||
|
||||
return count
|
||||
|
||||
# @classmethod
|
||||
# async def batch_delete(
|
||||
# cls,
|
||||
# db: AsyncSession,
|
||||
# ids: List[str],
|
||||
# hard: bool = False
|
||||
# ) -> Tuple[int, List[str]]:
|
||||
# """
|
||||
# 批量删除用户
|
||||
#
|
||||
# :return: (删除成功数, 删除失败的ID列表)
|
||||
# """
|
||||
# success_count = 0
|
||||
# failed_ids = []
|
||||
#
|
||||
# for user_id in ids:
|
||||
# user = await cls.get_by_id(db, user_id)
|
||||
# if user:
|
||||
# if user.can_delete():
|
||||
# if await cls.delete(db, user_id, hard=hard):
|
||||
# success_count += 1
|
||||
# else:
|
||||
# failed_ids.append(user_id)
|
||||
# else:
|
||||
# failed_ids.append(user_id)
|
||||
# else:
|
||||
# failed_ids.append(user_id)
|
||||
#
|
||||
# return success_count, failed_ids
|
||||
|
||||
@classmethod
|
||||
async def get_subordinates(cls, db: AsyncSession, user_id: str) -> List[User]:
|
||||
"""
|
||||
获取下属用户列表
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.manager_id == user_id,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def get_top_users(cls, db: AsyncSession) -> List[User]:
|
||||
"""
|
||||
获取顶层用户(无上级的用户,用于组织架构图根节点)
|
||||
"""
|
||||
from sqlalchemy import or_
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
or_(User.manager_id == None, User.manager_id == ''), # noqa: E711
|
||||
User.is_deleted == False # noqa: E712
|
||||
).order_by(User.sort.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def get_report_chain(cls, db: AsyncSession, user_id: str) -> List[User]:
|
||||
"""
|
||||
获取用户的汇报链(从当前用户一直到顶层),返回列表 [当前用户, 上级, 上上级, ..., 顶层]
|
||||
"""
|
||||
chain = []
|
||||
current_id = user_id
|
||||
visited = set()
|
||||
while current_id and current_id not in visited:
|
||||
visited.add(current_id)
|
||||
user = await cls.get_by_id(db, record_id=current_id)
|
||||
if not user:
|
||||
break
|
||||
chain.append(user)
|
||||
current_id = user.manager_id
|
||||
return chain
|
||||
|
||||
@classmethod
|
||||
async def get_subordinate_count(cls, db: AsyncSession, user_id: str) -> int:
|
||||
"""
|
||||
获取下属数量
|
||||
"""
|
||||
from sqlalchemy import func as sa_func
|
||||
result = await db.execute(
|
||||
select(sa_func.count(User.id)).where(
|
||||
User.manager_id == user_id,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@classmethod
|
||||
async def get_by_dept(cls, db: AsyncSession, dept_id: str) -> List[User]:
|
||||
"""
|
||||
获取部门下的用户列表
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.dept_id == dept_id,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UserRole Model - 用户角色关联表
|
||||
用于实现用户和角色的多对多关系
|
||||
"""
|
||||
from sqlalchemy import Column, String, Index
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class UserRole(BaseModel):
|
||||
"""
|
||||
用户角色关联表
|
||||
|
||||
实现用户和角色的多对多关系
|
||||
一个用户可以拥有多个角色
|
||||
一个角色可以分配给多个用户
|
||||
"""
|
||||
__tablename__ = "core_user_role"
|
||||
|
||||
# 用户ID(逻辑外键)
|
||||
user_id = Column(String(21), nullable=False, index=True, comment="用户ID")
|
||||
|
||||
# 角色ID(逻辑外键)
|
||||
role_id = Column(String(21), nullable=False, index=True, comment="角色ID")
|
||||
|
||||
# 创建联合唯一索引,确保同一用户不会重复分配同一角色
|
||||
__table_args__ = (
|
||||
Index('idx_user_role_unique', 'user_id', 'role_id', unique=True),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserRole(user_id={self.user_id}, role_id={self.role_id})>"
|
||||
Reference in New Issue
Block a user