Build lightweight AI agent admin
This commit is contained in:
@@ -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"]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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, '未知')
|
||||
@@ -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="搜索关键词")
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user