Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Post Module - 岗位管理模块
"""
from core.post.model import Post
from core.post.service import PostService
from core.post.api import router
__all__ = ["Post", "PostService", "router"]
+390
View File
@@ -0,0 +1,390 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Post API - 岗位管理接口
提供岗位的 CRUD 操作和用户管理
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Request
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.post.schema import (
PostCreate, PostUpdate, PostResponse, PostSimple,
PostBatchDeleteIn, PostBatchDeleteOut, PostBatchUpdateStatusIn, PostBatchUpdateStatusOut,
PostUserSchema, PostUserIn, PostStatsResponse, PostSearchRequest
)
from core.post.service import PostService
router = APIRouter(prefix="/post", tags=["岗位管理"])
@router.post("", response_model=PostResponse, summary="创建岗位")
async def create_post(
data: PostCreate,
db: AsyncSession = Depends(get_db)
):
"""创建岗位(自动记录创建人)"""
# 编码唯一性校验
if not await PostService.check_unique(db, field="code", value=data.code):
raise HTTPException(status_code=400, detail=f"岗位编码已存在: {data.code}")
post = await PostService.create(db=db, data=data)
return await _build_post_response(db, post)
@router.get("/all", response_model=List[PostSimple], summary="获取所有岗位(简化版)")
async def get_all_posts(db: AsyncSession = Depends(get_db)):
"""获取所有启用的岗位(不分页,简化版,用于选择器)"""
posts = await PostService.get_all_simple(db)
return [PostSimple.model_validate(post) for post in posts]
@router.get("", response_model=PaginatedResponse[PostResponse], summary="获取岗位列表")
async def get_post_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="岗位名称"),
code: Optional[str] = Query(None, description="岗位编码"),
post_type: Optional[int] = Query(None, alias="post_type", description="岗位类型"),
post_level: Optional[int] = Query(None, alias="post_level", description="岗位级别"),
status: Optional[bool] = Query(None, description="岗位状态"),
dept_id: Optional[str] = Query(None, alias="dept_id", description="部门ID"),
db: AsyncSession = Depends(get_db)
):
"""获取岗位列表(分页)"""
from core.post.model import Post
filters = []
if name:
filters.append(Post.name.ilike(f"%{name}%"))
if code:
filters.append(Post.code.ilike(f"%{code}%"))
if post_type is not None:
filters.append(Post.post_type == post_type)
if post_level is not None:
filters.append(Post.post_level == post_level)
if status is not None:
filters.append(Post.status == status)
if dept_id:
filters.append(Post.dept_id == dept_id)
items, total = await PostService.get_list(
db=db,
page=page,
page_size=page_size,
filters=filters
)
response_items = [await _build_post_response(db, item) for item in items]
return PaginatedResponse(items=response_items, total=total)
@router.get("/export/excel", summary="导出岗位Excel")
async def export_post_excel(
db: AsyncSession = Depends(get_db)
):
"""导出岗位到Excel"""
output = await PostService.export_to_excel(
db=db,
data_converter=PostService._export_converter
)
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=posts.xlsx"}
)
@router.get("/import/template", summary="下载岗位导入模板")
async def download_post_template():
"""下载岗位导入模板"""
output = PostService.get_import_template()
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=post_template.xlsx"}
)
@router.post("/import/excel", response_model=ResponseModel, summary="导入岗位Excel")
async def import_post_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 PostService.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_post_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 = ["code", "name"]
if field not in allowed_fields:
raise HTTPException(status_code=400, detail=f"不支持检查字段: {field}")
is_unique = await PostService.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=PostBatchDeleteOut, summary="批量删除岗位")
async def batch_delete_posts(
data: PostBatchDeleteIn,
hard: bool = Query(default=False, description="是否物理删除"),
db: AsyncSession = Depends(get_db)
):
"""批量删除岗位"""
count, failed_ids = await PostService.batch_delete(db, data.ids, hard=hard)
return PostBatchDeleteOut(count=count, failed_ids=failed_ids)
@router.post("/batch/status", response_model=PostBatchUpdateStatusOut, summary="批量更新岗位状态")
async def batch_update_post_status(
data: PostBatchUpdateStatusIn,
db: AsyncSession = Depends(get_db)
):
"""批量更新岗位状态"""
count = await PostService.batch_update_status(db, data.ids, data.status)
return PostBatchUpdateStatusOut(count=count)
@router.post("/search", response_model=PaginatedResponse[PostResponse], summary="搜索岗位")
async def search_post(
data: PostSearchRequest,
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="每页数量"),
db: AsyncSession = Depends(get_db)
):
"""搜索岗位(模糊匹配名称、编码、描述,自动应用数据权限过滤)"""
items, total = await PostService.search_with_data_scope(
db=db,
keyword=data.keyword,
page=page,
page_size=page_size
)
response_items = [await _build_post_response(db, item) for item in items]
return PaginatedResponse(items=response_items, total=total)
@router.get("/stats", response_model=PostStatsResponse, summary="获取岗位统计信息")
async def get_post_stats(db: AsyncSession = Depends(get_db)):
"""获取岗位统计信息"""
stats = await PostService.get_stats(db)
return PostStatsResponse(**stats)
@router.get("/by/ids", response_model=List[PostResponse], summary="根据ID列表获取岗位")
async def get_posts_by_ids(
ids: str = Query(..., description="岗位ID列表,逗号分隔"),
db: AsyncSession = Depends(get_db)
):
"""根据岗位ID列表批量获取岗位信息"""
post_ids = [id.strip() for id in ids.split(',') if id.strip()]
posts = await PostService.get_by_ids(db, post_ids)
return [await _build_post_response(db, post) for post in posts]
@router.get("/by/dept/{dept_id}", response_model=List[PostSimple], summary="根据部门ID获取岗位")
async def get_posts_by_dept(
dept_id: str,
db: AsyncSession = Depends(get_db)
):
"""根据部门ID获取该部门的所有岗位"""
posts = await PostService.get_by_dept(db, dept_id)
return [PostSimple.model_validate(post) for post in posts]
@router.get("/by/type/{post_type}", response_model=List[PostSimple], summary="根据类型获取岗位")
async def get_posts_by_type(
post_type: int,
db: AsyncSession = Depends(get_db)
):
"""根据岗位类型获取岗位列表"""
if post_type not in [0, 1, 2, 3, 4]:
raise HTTPException(status_code=400, detail="岗位类型必须在 0-4 之间")
posts = await PostService.get_by_type(db, post_type)
return [PostSimple.model_validate(post) for post in posts]
@router.get("/by/level/{post_level}", response_model=List[PostSimple], summary="根据级别获取岗位")
async def get_posts_by_level(
post_level: int,
db: AsyncSession = Depends(get_db)
):
"""根据岗位级别获取岗位列表"""
if post_level not in [0, 1, 2, 3]:
raise HTTPException(status_code=400, detail="岗位级别必须在 0-3 之间")
posts = await PostService.get_by_level(db, post_level)
return [PostSimple.model_validate(post) for post in posts]
@router.get("/users/by/post_id", response_model=PaginatedResponse[PostUserSchema], summary="获取岗位用户列表")
async def get_post_users(
post_id: str = Query(..., alias="post_id", description="岗位ID"),
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="每页数量"),
db: AsyncSession = Depends(get_db)
):
"""获取岗位下的用户列表"""
post = await PostService.get_by_id(db, post_id)
if not post:
raise HTTPException(status_code=404, detail="岗位不存在")
users = await PostService.get_post_users(db, post_id)
# 分页处理
total = len(users)
start = (page - 1) * page_size
end = start + page_size
paged_users = users[start:end]
result = []
for user in paged_users:
dept_name = None
if user.dept_id:
from core.dept.service import DeptService
dept = await DeptService.get_by_id(db, user.dept_id)
dept_name = dept.name if dept else None
result.append(PostUserSchema(
id=user.id,
name=user.name,
username=user.username,
avatar=user.avatar,
email=user.email,
dept_name=dept_name
))
return PaginatedResponse(items=result, total=total)
@router.post("/users/by/post_id", response_model=ResponseModel, summary="为岗位添加用户")
async def add_user_to_post(
data: PostUserIn,
db: AsyncSession = Depends(get_db)
):
"""将用户添加到岗位"""
post = await PostService.get_by_id(db, data.post_id)
if not post:
raise HTTPException(status_code=404, detail="岗位不存在")
if not data.user_ids:
raise HTTPException(status_code=400, detail="用户ID列表不能为空")
added_count = await PostService.add_users_to_post(db, data.post_id, data.user_ids)
return ResponseModel(message=f"成功添加 {added_count} 个用户")
@router.delete("/users/{post_id}", response_model=ResponseModel, summary="从岗位中移除用户")
async def remove_user_from_post(
post_id: str,
data: PostUserIn,
db: AsyncSession = Depends(get_db)
):
"""从岗位中移除用户(支持批量删除)"""
post = await PostService.get_by_id(db, post_id)
if not post:
raise HTTPException(status_code=404, detail="岗位不存在")
# 优先使用 user_ids(批量),如果没有则使用 user_id(单个)
user_ids_to_remove = data.user_ids if data.user_ids else ([data.user_id] if data.user_id else [])
if not user_ids_to_remove:
raise HTTPException(status_code=400, detail="用户ID不能为空")
removed_count = await PostService.remove_users_from_post(db, post_id, user_ids_to_remove)
return ResponseModel(message=f"成功移除 {removed_count} 个用户")
@router.get("/{post_id}", response_model=PostResponse, summary="获取岗位详情")
async def get_post_by_id(post_id: str, db: AsyncSession = Depends(get_db)):
"""获取岗位详情"""
post = await PostService.get_by_id(db, post_id)
if post is None:
raise HTTPException(status_code=404, detail="岗位不存在")
return await _build_post_response(db, post)
@router.put("/{post_id}", response_model=PostResponse, summary="更新岗位")
async def update_post(
post_id: str,
data: PostUpdate,
db: AsyncSession = Depends(get_db)
):
"""更新岗位(自动记录修改人)"""
# 编码唯一性校验
if data.code:
if not await PostService.check_unique(db, field="code", value=data.code, exclude_id=post_id):
raise HTTPException(status_code=400, detail=f"岗位编码已存在: {data.code}")
# 自动从上下文获取当前用户ID并填充 sys_modifier_id
post = await PostService.update(db, record_id=post_id, data=data)
if post is None:
raise HTTPException(status_code=404, detail="岗位不存在")
return await _build_post_response(db, post)
@router.delete("/{post_id}", response_model=ResponseModel, summary="删除岗位")
async def delete_post(
post_id: str,
hard: bool = Query(default=False, description="是否物理删除"),
db: AsyncSession = Depends(get_db)
):
"""删除岗位"""
can_del, reason = await PostService.can_delete(db, post_id)
if not can_del:
raise HTTPException(status_code=400, detail=reason)
success = await PostService.delete(db, record_id=post_id, hard=hard)
if not success:
raise HTTPException(status_code=404, detail="岗位不存在")
return ResponseModel(message="删除成功")
async def _build_post_response(db: AsyncSession, post) -> PostResponse:
"""构建岗位响应"""
# 获取部门名称
dept_name = None
if post.dept_id:
from core.dept.service import DeptService
dept = await DeptService.get_by_id(db, post.dept_id)
dept_name = dept.name if dept else None
# 获取用户数量
user_count = await PostService.get_user_count(db, post.id)
return PostResponse(
id=post.id,
name=post.name,
code=post.code,
post_type=post.post_type,
post_type_display=post.get_post_type_display(),
post_level=post.post_level,
post_level_display=post.get_post_level_display(),
status=post.status,
description=post.description,
dept_id=post.dept_id,
dept_name=dept_name,
user_count=user_count,
sort=post.sort,
is_deleted=post.is_deleted,
sys_create_datetime=post.sys_create_datetime,
sys_update_datetime=post.sys_update_datetime,
)
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Post Model - 岗位模型
用于管理组织中的岗位信息
"""
from sqlalchemy import Column, String, Integer, Boolean, Text
from app.base_model import BaseModel
class Post(BaseModel):
"""
岗位模型 - 用于职位管理
字段说明:
- name: 岗位名称
- code: 岗位编码(唯一)
- post_type: 岗位类型(0-管理岗, 1-技术岗, 2-业务岗, 3-职能岗, 4-其他)
- post_level: 岗位级别(0-高层, 1-中层, 2-基层, 3-一般员工)
- status: 岗位状态(启用/禁用)
- description: 岗位描述
- dept_id: 所属部门ID(逻辑外键)
"""
__tablename__ = "core_post"
# 岗位类型选择
POST_TYPE_CHOICES = {
0: '管理岗',
1: '技术岗',
2: '业务岗',
3: '职能岗',
4: '其他',
}
# 岗位级别选择
POST_LEVEL_CHOICES = {
0: '高层',
1: '中层',
2: '基层',
3: '一般员工',
}
# 岗位名称
name = Column(String(64), nullable=False, index=True, comment="岗位名称")
# 岗位编码
code = Column(String(32), unique=True, nullable=False, index=True, comment="岗位编码")
# 岗位类型
post_type = Column(Integer, default=4, index=True, comment="岗位类型(0-管理岗, 1-技术岗, 2-业务岗, 3-职能岗, 4-其他)")
# 岗位级别
post_level = Column(Integer, default=3, index=True, comment="岗位级别(0-高层, 1-中层, 2-基层, 3-一般员工)")
# 岗位状态
status = Column(Boolean, default=True, index=True, comment="岗位状态(启用/禁用)")
# 岗位描述
description = Column(Text, nullable=True, comment="岗位描述/职责")
# 所属部门(逻辑外键)
dept_id = Column(String(21), nullable=True, index=True, comment="所属部门ID(逻辑外键关联core_dept")
def __str__(self):
return f"{self.name} ({self.code})"
def get_post_type_display(self) -> str:
"""获取岗位类型的显示名称"""
return self.POST_TYPE_CHOICES.get(self.post_type, '未知')
def get_post_level_display(self) -> str:
"""获取岗位级别的显示名称"""
return self.POST_LEVEL_CHOICES.get(self.post_level, '未知')
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Post Schema - 岗位数据验证模式
"""
from datetime import datetime
from typing import Optional, List, Dict
from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.base_schema import CSTDatetime
class PostBase(BaseModel):
"""岗位基础Schema"""
name: str = Field(..., min_length=1, max_length=64, description="岗位名称")
code: str = Field(..., min_length=1, max_length=32, description="岗位编码")
post_type: int = Field(default=4, description="岗位类型(0-管理岗, 1-技术岗, 2-业务岗, 3-职能岗, 4-其他)")
post_level: int = Field(default=3, description="岗位级别(0-高层, 1-中层, 2-基层, 3-一般员工)")
status: bool = Field(default=True, description="岗位状态")
description: Optional[str] = Field(None, description="岗位描述")
dept_id: Optional[str] = Field(None, description="所属部门ID")
sort: int = Field(default=0, description="排序")
@field_validator("code")
@classmethod
def validate_code(cls, v):
"""验证岗位编码格式"""
if not v:
raise ValueError("岗位编码不能为空")
if not all(c.isalnum() or c in '_-' for c in v):
raise ValueError("岗位编码只能包含字母、数字、下划线和横线")
return v
@field_validator("post_type")
@classmethod
def validate_post_type(cls, v):
"""验证岗位类型"""
if v is not None and v not in [0, 1, 2, 3, 4]:
raise ValueError("岗位类型必须在 0-4 之间")
return v
@field_validator("post_level")
@classmethod
def validate_post_level(cls, v):
"""验证岗位级别"""
if v is not None and v not in [0, 1, 2, 3]:
raise ValueError("岗位级别必须在 0-3 之间")
return v
class PostCreate(PostBase):
"""岗位创建Schema"""
pass
class PostUpdate(BaseModel):
"""岗位更新Schema - 所有字段可选"""
name: Optional[str] = Field(None, min_length=1, max_length=64, description="岗位名称")
code: Optional[str] = Field(None, min_length=1, max_length=32, description="岗位编码")
post_type: Optional[int] = Field(None, description="岗位类型")
post_level: Optional[int] = Field(None, description="岗位级别")
status: Optional[bool] = Field(None, description="岗位状态")
description: Optional[str] = Field(None, description="岗位描述")
dept_id: Optional[str] = Field(None, description="所属部门ID")
sort: Optional[int] = Field(None, description="排序")
@field_validator("code")
@classmethod
def validate_code(cls, v):
"""验证岗位编码格式"""
if v is not None:
if not v:
raise ValueError("岗位编码不能为空")
if not all(c.isalnum() or c in '_-' for c in v):
raise ValueError("岗位编码只能包含字母、数字、下划线和横线")
return v
@field_validator("post_type")
@classmethod
def validate_post_type(cls, v):
"""验证岗位类型"""
if v is not None and v not in [0, 1, 2, 3, 4]:
raise ValueError("岗位类型必须在 0-4 之间")
return v
@field_validator("post_level")
@classmethod
def validate_post_level(cls, v):
"""验证岗位级别"""
if v is not None and v not in [0, 1, 2, 3]:
raise ValueError("岗位级别必须在 0-3 之间")
return v
class PostResponse(BaseModel):
"""岗位响应Schema"""
id: str
name: str
code: str
post_type: int
post_type_display: Optional[str] = None
post_level: int
post_level_display: Optional[str] = None
status: bool
description: Optional[str] = None
dept_id: Optional[str] = None
dept_name: Optional[str] = None
user_count: int = 0
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 PostSimple(BaseModel):
"""岗位简单输出(用于选择器)"""
id: str
name: str
code: str
post_type: int
post_level: int
status: bool
model_config = ConfigDict(from_attributes=True)
class PostBatchDeleteIn(BaseModel):
"""批量删除岗位输入"""
ids: List[str] = Field(..., description="要删除的岗位ID列表")
class PostBatchDeleteOut(BaseModel):
"""批量删除岗位输出"""
count: int = Field(..., description="删除的记录数")
failed_ids: List[str] = Field(default=[], description="删除失败的ID列表")
class PostBatchUpdateStatusIn(BaseModel):
"""批量更新岗位状态输入"""
ids: List[str] = Field(..., description="岗位ID列表")
status: bool = Field(..., description="岗位状态")
class PostBatchUpdateStatusOut(BaseModel):
"""批量更新岗位状态输出"""
count: int = Field(..., description="更新的记录数")
class PostUserSchema(BaseModel):
"""岗位用户信息"""
id: str
name: Optional[str] = None
username: str
avatar: Optional[str] = None
email: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class PostUserIn(BaseModel):
"""岗位用户操作输入"""
post_id: str = Field(..., description="岗位ID")
user_ids: List[str] = Field(default=[], description="用户ID列表")
user_id: Optional[str] = Field(None, description="单个用户ID")
class PostStatsResponse(BaseModel):
"""岗位统计输出"""
total_count: int = Field(..., description="总岗位数")
active_count: int = Field(..., description="启用岗位数")
inactive_count: int = Field(..., description="禁用岗位数")
type_stats: Dict[str, int] = Field(..., description="按类型统计")
level_stats: Dict[str, int] = Field(..., description="按级别统计")
class PostSearchRequest(BaseModel):
"""搜索岗位请求"""
keyword: str = Field(..., description="搜索关键词")
+461
View File
@@ -0,0 +1,461 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Post Service - 岗位服务层
"""
from io import BytesIO
from typing import Tuple, Dict, Any, Optional, List
from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession
from app.base_service import BaseService
from core.post.model import Post
from core.post.schema import PostCreate, PostUpdate
class PostService(BaseService[Post, PostCreate, PostUpdate]):
"""
岗位服务层
继承BaseService,自动获得增删改查功能
"""
model = Post
# Excel导入导出配置
excel_columns = {
"name": "岗位名称",
"code": "岗位编码",
"post_type": "岗位类型",
"post_level": "岗位级别",
"status": "状态",
"description": "描述",
}
excel_sheet_name = "岗位列表"
@classmethod
def _export_converter(cls, item: Any) -> Dict[str, Any]:
"""导出数据转换器"""
return {
"name": item.name,
"code": item.code,
"post_type": item.get_post_type_display(),
"post_level": item.get_post_level_display(),
"status": "启用" if item.status else "禁用",
"description": item.description or "",
}
@classmethod
def _import_processor(cls, row: Dict[str, Any]) -> Optional[Post]:
"""导入数据处理器"""
name = row.get("name")
code = row.get("code")
if not name or not code:
return None
# 岗位类型映射
type_map = {"管理岗": 0, "技术岗": 1, "业务岗": 2, "职能岗": 3, "其他": 4}
post_type_str = row.get("post_type", "其他")
post_type = type_map.get(post_type_str, 4)
# 岗位级别映射
level_map = {"高层": 0, "中层": 1, "基层": 2, "一般员工": 3}
post_level_str = row.get("post_level", "一般员工")
post_level = level_map.get(post_level_str, 3)
status_str = row.get("status", "启用")
status = status_str in ("启用", "true", "True", "1", True)
return Post(
name=str(name),
code=str(code),
post_type=post_type,
post_level=post_level,
status=status,
description=str(row.get("description") or "") or None,
)
@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 get_user_count(cls, db: AsyncSession, post_id: str) -> int:
"""获取岗位下的用户数量"""
from core.user.model import User
result = await db.execute(
select(func.count(User.id)).where(
User.post_id == post_id,
User.is_deleted == False # noqa: E712
)
)
return result.scalar() or 0
@classmethod
async def can_delete(cls, db: AsyncSession, post_id: str) -> Tuple[bool, str]:
"""
检查岗位是否可以删除
:return: (是否可删除, 原因)
"""
user_count = await cls.get_user_count(db, post_id)
if user_count > 0:
return False, f"该岗位下还有 {user_count} 个用户,无法删除"
return True, ""
@classmethod
async def batch_update_status(
cls,
db: AsyncSession,
ids: List[str],
status: bool
) -> int:
"""
批量更新岗位状态
:return: 更新的记录数
"""
count = 0
for post_id in ids:
post = await cls.get_by_id(db, post_id)
if post:
post.status = 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 post_id in ids:
can_del, reason = await cls.can_delete(db, post_id)
if can_del:
if await cls.delete(db, post_id, hard=hard):
success_count += 1
else:
failed_ids.append(post_id)
else:
failed_ids.append(post_id)
return success_count, failed_ids
@classmethod
async def search(
cls,
db: AsyncSession,
keyword: str,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Post], int]:
"""
搜索岗位(模糊匹配名称、编码、描述)
"""
if not keyword:
return [], 0
# 构建搜索条件
search_filter = or_(
Post.name.ilike(f"%{keyword}%"),
Post.code.ilike(f"%{keyword}%"),
Post.description.ilike(f"%{keyword}%")
)
# 查询总数
count_result = await db.execute(
select(func.count(Post.id)).where(
search_filter,
Post.is_deleted == False # noqa: E712
)
)
total = count_result.scalar() or 0
# 查询数据
result = await db.execute(
select(Post).where(
search_filter,
Post.is_deleted == False # noqa: E712
)
.order_by(Post.sys_create_datetime.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
items = list(result.scalars().all())
return items, total
@classmethod
async def search_with_data_scope(
cls,
db: AsyncSession,
keyword: str,
page: int,
page_size: int
) -> Tuple[List[Post], int]:
"""
搜索岗位(模糊匹配名称、编码、描述,自动应用数据权限过滤)
自动从上下文获取当前用户信息和请求信息,无需手动传递任何参数
"""
if not keyword:
return [], 0
from utils.permission import apply_data_scope_filter
from utils.context import get_current_user_info_from_context
# 从上下文获取用户信息和请求信息
user_info = get_current_user_info_from_context()
if not user_info:
return [], 0
# 获取数据权限过滤条件
data_scope_filter = await apply_data_scope_filter(
db=db,
role_id=user_info.get("role_id"),
is_superuser=user_info.get("is_superuser", False),
user_id=user_info.get("user_id"),
user_dept_id=user_info.get("dept_id"),
request_path=user_info.get("request_path"),
http_method=user_info.get("http_method")
)
# 构建搜索条件
search_filter = or_(
Post.name.ilike(f"%{keyword}%"),
Post.code.ilike(f"%{keyword}%"),
Post.description.ilike(f"%{keyword}%")
)
# 构建基础查询
base_query = select(Post).where(
search_filter,
Post.is_deleted == False # noqa: E712
)
# 应用数据权限过滤
base_query = cls._apply_data_scope_to_query(
query=base_query,
data_scope_filter=data_scope_filter
)
# 查询总数
count_result = await db.execute(
select(func.count()).select_from(base_query.subquery())
)
total = count_result.scalar() or 0
# 查询数据
result = await db.execute(
base_query.order_by(Post.sys_create_datetime.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
items = list(result.scalars().all())
return items, total
@classmethod
async def get_stats(cls, db: AsyncSession) -> Dict[str, Any]:
"""获取岗位统计信息"""
# 总数
total_result = await db.execute(
select(func.count(Post.id)).where(Post.is_deleted == False) # noqa: E712
)
total_count = total_result.scalar() or 0
# 启用数
active_result = await db.execute(
select(func.count(Post.id)).where(
Post.status == True, # noqa: E712
Post.is_deleted == False # noqa: E712
)
)
active_count = active_result.scalar() or 0
# 按类型统计
type_stats = {}
for type_code, type_name in Post.POST_TYPE_CHOICES.items():
count_result = await db.execute(
select(func.count(Post.id)).where(
Post.post_type == type_code,
Post.is_deleted == False # noqa: E712
)
)
type_stats[type_name] = count_result.scalar() or 0
# 按级别统计
level_stats = {}
for level_code, level_name in Post.POST_LEVEL_CHOICES.items():
count_result = await db.execute(
select(func.count(Post.id)).where(
Post.post_level == level_code,
Post.is_deleted == False # noqa: E712
)
)
level_stats[level_name] = count_result.scalar() or 0
return {
'total_count': total_count,
'active_count': active_count,
'inactive_count': total_count - active_count,
'type_stats': type_stats,
'level_stats': level_stats,
}
@classmethod
async def get_by_dept(cls, db: AsyncSession, dept_id: str) -> List[Post]:
"""根据部门ID获取岗位列表"""
result = await db.execute(
select(Post).where(
Post.dept_id == dept_id,
Post.status == True, # noqa: E712
Post.is_deleted == False # noqa: E712
).order_by(Post.post_level, Post.name)
)
return list(result.scalars().all())
@classmethod
async def get_by_type(cls, db: AsyncSession, post_type: int) -> List[Post]:
"""根据岗位类型获取岗位列表"""
result = await db.execute(
select(Post).where(
Post.post_type == post_type,
Post.status == True, # noqa: E712
Post.is_deleted == False # noqa: E712
).order_by(Post.post_level, Post.name)
)
return list(result.scalars().all())
@classmethod
async def get_by_level(cls, db: AsyncSession, post_level: int) -> List[Post]:
"""根据岗位级别获取岗位列表"""
result = await db.execute(
select(Post).where(
Post.post_level == post_level,
Post.status == True, # noqa: E712
Post.is_deleted == False # noqa: E712
).order_by(Post.name)
)
return list(result.scalars().all())
@classmethod
async def get_by_ids(cls, db: AsyncSession, ids: List[str]) -> List[Post]:
"""根据ID列表批量获取岗位"""
if not ids:
return []
result = await db.execute(
select(Post).where(
Post.id.in_(ids),
Post.is_deleted == False # noqa: E712
)
)
return list(result.scalars().all())
@classmethod
async def get_post_users(cls, db: AsyncSession, post_id: str) -> List[Any]:
"""获取岗位下的用户列表"""
from core.user.model import User
result = await db.execute(
select(User).where(
User.post_id == post_id,
User.user_status == 1,
User.is_deleted == False # noqa: E712
)
)
return list(result.scalars().all())
@classmethod
async def add_users_to_post(
cls,
db: AsyncSession,
post_id: str,
user_ids: List[str]
) -> int:
"""将用户添加到岗位"""
from core.user.model import User
post = await cls.get_by_id(db, post_id)
if not post:
return 0
added_count = 0
for user_id in user_ids:
result = await db.execute(
select(User).where(User.id == user_id)
)
user = result.scalar_one_or_none()
if user and user.post_id != post_id:
user.post_id = post_id
added_count += 1
if added_count > 0:
await db.commit()
return added_count
@classmethod
async def remove_users_from_post(
cls,
db: AsyncSession,
post_id: str,
user_ids: List[str]
) -> int:
"""从岗位中移除用户"""
from core.user.model import User
removed_count = 0
for user_id in user_ids:
result = await db.execute(
select(User).where(User.id == user_id)
)
user = result.scalar_one_or_none()
if user and user.post_id == post_id:
user.post_id = None
removed_count += 1
if removed_count > 0:
await db.commit()
return removed_count
@classmethod
async def get_all_simple(cls, db: AsyncSession) -> List[Post]:
"""获取所有启用的岗位(简化版,用于选择器)"""
result = await db.execute(
select(Post).where(
Post.status == True, # noqa: E712
Post.is_deleted == False # noqa: E712
).order_by(Post.post_level, Post.name)
)
return list(result.scalars().all())