- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
659 lines
21 KiB
Python
659 lines
21 KiB
Python
"""
|
||
岗位管理 API(真实数据库)
|
||
"""
|
||
|
||
from typing import Optional, List
|
||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, and_, func
|
||
from sqlalchemy.orm import selectinload
|
||
import sqlalchemy as sa
|
||
|
||
from app.core.deps import get_current_active_user as get_current_user, get_db, require_admin, require_admin_or_manager
|
||
from app.schemas.base import ResponseModel, PaginationParams, PaginatedResponse
|
||
from app.models.position import Position
|
||
from app.models.position_member import PositionMember
|
||
from app.models.position_course import PositionCourse
|
||
from app.models.user import User
|
||
from app.models.course import Course
|
||
|
||
|
||
router = APIRouter(prefix="/admin/positions")
|
||
|
||
|
||
@router.get("")
|
||
async def list_positions(
|
||
pagination: PaginationParams = Depends(),
|
||
keyword: Optional[str] = Query(None, description="关键词"),
|
||
current_user=Depends(require_admin_or_manager),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""分页获取岗位列表(管理员或经理)。"""
|
||
stmt = select(Position).where(Position.is_deleted == False)
|
||
if keyword:
|
||
like = f"%{keyword}%"
|
||
stmt = stmt.where((Position.name.ilike(like)) | (Position.description.ilike(like)))
|
||
rows = (await db.execute(stmt)).scalars().all()
|
||
total = len(rows)
|
||
sliced = rows[pagination.offset : pagination.offset + pagination.limit]
|
||
|
||
async def to_dict(p: Position) -> dict:
|
||
"""将Position对象转换为字典,并添加统计数据"""
|
||
d = p.__dict__.copy()
|
||
d.pop("_sa_instance_state", None)
|
||
|
||
# 统计岗位成员数量
|
||
member_count_result = await db.execute(
|
||
select(func.count(PositionMember.id)).where(
|
||
and_(
|
||
PositionMember.position_id == p.id,
|
||
PositionMember.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
d["memberCount"] = member_count_result.scalar() or 0
|
||
|
||
# 统计必修课程数量
|
||
required_count_result = await db.execute(
|
||
select(func.count(PositionCourse.id)).where(
|
||
and_(
|
||
PositionCourse.position_id == p.id,
|
||
PositionCourse.course_type == "required",
|
||
PositionCourse.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
d["requiredCourses"] = required_count_result.scalar() or 0
|
||
|
||
# 统计选修课程数量
|
||
optional_count_result = await db.execute(
|
||
select(func.count(PositionCourse.id)).where(
|
||
and_(
|
||
PositionCourse.position_id == p.id,
|
||
PositionCourse.course_type == "optional",
|
||
PositionCourse.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
d["optionalCourses"] = optional_count_result.scalar() or 0
|
||
|
||
return d
|
||
|
||
# 为每个岗位添加统计数据(使用异步)
|
||
items = []
|
||
for p in sliced:
|
||
item = await to_dict(p)
|
||
items.append(item)
|
||
|
||
paged = {
|
||
"items": items,
|
||
"total": total,
|
||
"page": pagination.page,
|
||
"page_size": pagination.page_size,
|
||
"pages": (total + pagination.page_size - 1) // pagination.page_size if pagination.page_size else 1,
|
||
}
|
||
return ResponseModel(message="获取岗位列表成功", data=paged)
|
||
|
||
|
||
@router.get("/tree")
|
||
async def get_position_tree(
|
||
current_user=Depends(require_admin_or_manager), db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
"""获取岗位树(管理员或经理)。"""
|
||
rows = (await db.execute(select(Position).where(Position.is_deleted == False))).scalars().all()
|
||
id_to_node = {p.id: {**p.__dict__, "children": []} for p in rows}
|
||
roots: List[dict] = []
|
||
for p in rows:
|
||
node = id_to_node[p.id]
|
||
parent_id = p.parent_id
|
||
if parent_id and parent_id in id_to_node:
|
||
id_to_node[parent_id]["children"].append(node)
|
||
else:
|
||
roots.append(node)
|
||
# 清理 _sa_instance_state
|
||
def clean(d: dict):
|
||
d.pop("_sa_instance_state", None)
|
||
for c in d.get("children", []):
|
||
clean(c)
|
||
for r in roots:
|
||
clean(r)
|
||
return ResponseModel(message="获取岗位树成功", data=roots)
|
||
|
||
|
||
@router.post("")
|
||
async def create_position(
|
||
payload: dict, current_user=Depends(require_admin), db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
obj = Position(
|
||
name=payload.get("name"),
|
||
code=payload.get("code"),
|
||
description=payload.get("description"),
|
||
parent_id=payload.get("parentId"),
|
||
status=payload.get("status", "active"),
|
||
skills=payload.get("skills"),
|
||
level=payload.get("level"),
|
||
sort_order=payload.get("sort_order", 0),
|
||
created_by=current_user.id,
|
||
)
|
||
db.add(obj)
|
||
await db.commit()
|
||
await db.refresh(obj)
|
||
return ResponseModel(message="创建岗位成功", data={"id": obj.id})
|
||
|
||
|
||
@router.put("/{position_id}")
|
||
async def update_position(
|
||
position_id: int, payload: dict, current_user=Depends(require_admin), db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
obj = await db.get(Position, position_id)
|
||
if not obj or obj.is_deleted:
|
||
return ResponseModel(code=404, message="岗位不存在")
|
||
obj.name = payload.get("name", obj.name)
|
||
obj.code = payload.get("code", obj.code)
|
||
obj.description = payload.get("description", obj.description)
|
||
obj.parent_id = payload.get("parentId", obj.parent_id)
|
||
obj.status = payload.get("status", obj.status)
|
||
obj.skills = payload.get("skills", obj.skills)
|
||
obj.level = payload.get("level", obj.level)
|
||
obj.sort_order = payload.get("sort_order", obj.sort_order)
|
||
obj.updated_by = current_user.id
|
||
await db.commit()
|
||
await db.refresh(obj)
|
||
|
||
# 返回更新后的完整数据
|
||
data = obj.__dict__.copy()
|
||
data.pop("_sa_instance_state", None)
|
||
return ResponseModel(message="更新岗位成功", data=data)
|
||
|
||
|
||
@router.get("/{position_id}")
|
||
async def get_position_detail(
|
||
position_id: int, current_user=Depends(require_admin), db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
obj = await db.get(Position, position_id)
|
||
if not obj or obj.is_deleted:
|
||
return ResponseModel(code=404, message="岗位不存在")
|
||
data = obj.__dict__.copy()
|
||
data.pop("_sa_instance_state", None)
|
||
return ResponseModel(data=data)
|
||
|
||
|
||
@router.get("/{position_id}/check-delete")
|
||
async def check_position_delete(
|
||
position_id: int, current_user=Depends(require_admin), db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
obj = await db.get(Position, position_id)
|
||
if not obj or obj.is_deleted:
|
||
return ResponseModel(code=404, message="岗位不存在")
|
||
|
||
# 检查是否有子岗位
|
||
child_count_result = await db.execute(
|
||
select(func.count(Position.id)).where(
|
||
and_(
|
||
Position.parent_id == position_id,
|
||
Position.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
child_count = child_count_result.scalar() or 0
|
||
|
||
if child_count > 0:
|
||
return ResponseModel(data={
|
||
"deletable": False,
|
||
"reason": f"该岗位下有 {child_count} 个子岗位,请先删除或移动子岗位"
|
||
})
|
||
|
||
# 检查是否有成员(仅作为提醒,不阻止删除)
|
||
member_count_result = await db.execute(
|
||
select(func.count(PositionMember.id)).where(
|
||
and_(
|
||
PositionMember.position_id == position_id,
|
||
PositionMember.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
member_count = member_count_result.scalar() or 0
|
||
|
||
warning = ""
|
||
if member_count > 0:
|
||
warning = f"注意:该岗位当前有 {member_count} 名成员,删除后这些成员将不再属于此岗位"
|
||
|
||
return ResponseModel(data={"deletable": True, "reason": "", "warning": warning, "member_count": member_count})
|
||
|
||
|
||
@router.delete("/{position_id}")
|
||
async def delete_position(
|
||
position_id: int, current_user=Depends(require_admin), db: AsyncSession = Depends(get_db)
|
||
) -> ResponseModel:
|
||
obj = await db.get(Position, position_id)
|
||
if not obj or obj.is_deleted:
|
||
return ResponseModel(code=404, message="岗位不存在")
|
||
|
||
# 检查是否有子岗位
|
||
child_count_result = await db.execute(
|
||
select(func.count(Position.id)).where(
|
||
and_(
|
||
Position.parent_id == position_id,
|
||
Position.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
child_count = child_count_result.scalar() or 0
|
||
|
||
if child_count > 0:
|
||
return ResponseModel(
|
||
code=400,
|
||
message=f"该岗位下有 {child_count} 个子岗位,请先删除或移动子岗位"
|
||
)
|
||
|
||
# 软删除岗位成员关联
|
||
await db.execute(
|
||
sa.update(PositionMember)
|
||
.where(PositionMember.position_id == position_id)
|
||
.values(is_deleted=True)
|
||
)
|
||
|
||
# 软删除岗位课程关联
|
||
await db.execute(
|
||
sa.update(PositionCourse)
|
||
.where(PositionCourse.position_id == position_id)
|
||
.values(is_deleted=True)
|
||
)
|
||
|
||
# 软删除岗位
|
||
obj.is_deleted = True
|
||
await db.commit()
|
||
return ResponseModel(message="岗位已删除")
|
||
|
||
|
||
# ========== 岗位成员管理 API ==========
|
||
|
||
@router.get("/{position_id}/members")
|
||
async def get_position_members(
|
||
position_id: int,
|
||
pagination: PaginationParams = Depends(),
|
||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
||
current_user=Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""获取岗位成员列表"""
|
||
# 验证岗位存在
|
||
position = await db.get(Position, position_id)
|
||
if not position or position.is_deleted:
|
||
return ResponseModel(code=404, message="岗位不存在")
|
||
|
||
# 构建查询
|
||
stmt = (
|
||
select(PositionMember, User)
|
||
.join(User, PositionMember.user_id == User.id)
|
||
.where(
|
||
and_(
|
||
PositionMember.position_id == position_id,
|
||
PositionMember.is_deleted == False,
|
||
User.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
|
||
# 关键词搜索
|
||
if keyword:
|
||
like = f"%{keyword}%"
|
||
stmt = stmt.where(
|
||
(User.username.ilike(like)) |
|
||
(User.full_name.ilike(like)) |
|
||
(User.email.ilike(like))
|
||
)
|
||
|
||
# 执行查询
|
||
result = await db.execute(stmt)
|
||
rows = result.all()
|
||
total = len(rows)
|
||
sliced = rows[pagination.offset : pagination.offset + pagination.limit]
|
||
|
||
# 格式化数据
|
||
items = []
|
||
for pm, user in sliced:
|
||
items.append({
|
||
"id": pm.id,
|
||
"user_id": user.id,
|
||
"username": user.username,
|
||
"full_name": user.full_name,
|
||
"email": user.email,
|
||
"phone": user.phone,
|
||
"role": pm.role,
|
||
"joined_at": pm.joined_at.isoformat() if pm.joined_at else None,
|
||
"user_role": user.role, # 系统角色
|
||
"is_active": user.is_active,
|
||
})
|
||
|
||
return ResponseModel(
|
||
message="获取成员列表成功",
|
||
data={
|
||
"items": items,
|
||
"total": total,
|
||
"page": pagination.page,
|
||
"page_size": pagination.page_size,
|
||
"pages": (total + pagination.page_size - 1) // pagination.page_size if pagination.page_size else 1,
|
||
}
|
||
)
|
||
|
||
|
||
@router.post("/{position_id}/members")
|
||
async def add_position_members(
|
||
position_id: int,
|
||
payload: dict,
|
||
current_user=Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""批量添加岗位成员"""
|
||
# 验证岗位存在
|
||
position = await db.get(Position, position_id)
|
||
if not position or position.is_deleted:
|
||
return ResponseModel(code=404, message="岗位不存在")
|
||
|
||
user_ids = payload.get("user_ids", [])
|
||
if not user_ids:
|
||
return ResponseModel(code=400, message="请选择要添加的用户")
|
||
|
||
# 验证用户存在
|
||
users = await db.execute(
|
||
select(User).where(
|
||
and_(
|
||
User.id.in_(user_ids),
|
||
User.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
valid_users = {u.id: u for u in users.scalars().all()}
|
||
|
||
if len(valid_users) != len(user_ids):
|
||
invalid_ids = set(user_ids) - set(valid_users.keys())
|
||
return ResponseModel(code=400, message=f"部分用户不存在: {invalid_ids}")
|
||
|
||
# 检查是否已存在
|
||
existing = await db.execute(
|
||
select(PositionMember).where(
|
||
and_(
|
||
PositionMember.position_id == position_id,
|
||
PositionMember.user_id.in_(user_ids),
|
||
PositionMember.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
existing_user_ids = {pm.user_id for pm in existing.scalars().all()}
|
||
|
||
# 添加新成员
|
||
added_count = 0
|
||
for user_id in user_ids:
|
||
if user_id not in existing_user_ids:
|
||
member = PositionMember(
|
||
position_id=position_id,
|
||
user_id=user_id,
|
||
role=payload.get("role")
|
||
)
|
||
db.add(member)
|
||
added_count += 1
|
||
|
||
await db.commit()
|
||
|
||
return ResponseModel(
|
||
message=f"成功添加 {added_count} 个成员",
|
||
data={"added_count": added_count}
|
||
)
|
||
|
||
|
||
@router.delete("/{position_id}/members/{user_id}")
|
||
async def remove_position_member(
|
||
position_id: int,
|
||
user_id: int,
|
||
current_user=Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""移除岗位成员"""
|
||
# 查找成员关系
|
||
member = await db.execute(
|
||
select(PositionMember).where(
|
||
and_(
|
||
PositionMember.position_id == position_id,
|
||
PositionMember.user_id == user_id,
|
||
PositionMember.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
member = member.scalar_one_or_none()
|
||
|
||
if not member:
|
||
return ResponseModel(code=404, message="成员关系不存在")
|
||
|
||
# 软删除
|
||
member.is_deleted = True
|
||
await db.commit()
|
||
|
||
return ResponseModel(message="成员已移除")
|
||
|
||
|
||
# ========== 岗位课程管理 API ==========
|
||
|
||
@router.get("/{position_id}/courses")
|
||
async def get_position_courses(
|
||
position_id: int,
|
||
course_type: Optional[str] = Query(None, description="课程类型:required/optional"),
|
||
current_user=Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""获取岗位课程列表"""
|
||
# 验证岗位存在
|
||
position = await db.get(Position, position_id)
|
||
if not position or position.is_deleted:
|
||
return ResponseModel(code=404, message="岗位不存在")
|
||
|
||
# 构建查询
|
||
stmt = (
|
||
select(PositionCourse, Course)
|
||
.join(Course, PositionCourse.course_id == Course.id)
|
||
.where(
|
||
and_(
|
||
PositionCourse.position_id == position_id,
|
||
PositionCourse.is_deleted == False,
|
||
Course.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
|
||
# 课程类型筛选
|
||
if course_type:
|
||
stmt = stmt.where(PositionCourse.course_type == course_type)
|
||
|
||
# 按优先级排序
|
||
stmt = stmt.order_by(PositionCourse.priority, PositionCourse.id)
|
||
|
||
# 执行查询
|
||
result = await db.execute(stmt)
|
||
rows = result.all()
|
||
|
||
# 格式化数据
|
||
items = []
|
||
for pc, course in rows:
|
||
items.append({
|
||
"id": pc.id,
|
||
"course_id": course.id,
|
||
"course_name": course.name,
|
||
"course_description": course.description,
|
||
"course_category": course.category,
|
||
"course_status": course.status,
|
||
"course_duration_hours": course.duration_hours,
|
||
"course_difficulty_level": course.difficulty_level,
|
||
"course_type": pc.course_type,
|
||
"priority": pc.priority,
|
||
"created_at": pc.created_at.isoformat() if pc.created_at else None,
|
||
})
|
||
|
||
# 统计
|
||
stats = {
|
||
"total": len(items),
|
||
"required_count": sum(1 for item in items if item["course_type"] == "required"),
|
||
"optional_count": sum(1 for item in items if item["course_type"] == "optional"),
|
||
}
|
||
|
||
return ResponseModel(
|
||
message="获取课程列表成功",
|
||
data={
|
||
"items": items,
|
||
"stats": stats
|
||
}
|
||
)
|
||
|
||
|
||
@router.post("/{position_id}/courses")
|
||
async def add_position_courses(
|
||
position_id: int,
|
||
payload: dict,
|
||
current_user=Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""批量添加岗位课程"""
|
||
# 验证岗位存在
|
||
position = await db.get(Position, position_id)
|
||
if not position or position.is_deleted:
|
||
return ResponseModel(code=404, message="岗位不存在")
|
||
|
||
course_ids = payload.get("course_ids", [])
|
||
if not course_ids:
|
||
return ResponseModel(code=400, message="请选择要添加的课程")
|
||
|
||
course_type = payload.get("course_type", "required")
|
||
if course_type not in ["required", "optional"]:
|
||
return ResponseModel(code=400, message="课程类型无效")
|
||
|
||
# 验证课程存在
|
||
courses = await db.execute(
|
||
select(Course).where(
|
||
and_(
|
||
Course.id.in_(course_ids),
|
||
Course.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
valid_courses = {c.id: c for c in courses.scalars().all()}
|
||
|
||
if len(valid_courses) != len(course_ids):
|
||
invalid_ids = set(course_ids) - set(valid_courses.keys())
|
||
return ResponseModel(code=400, message=f"部分课程不存在: {invalid_ids}")
|
||
|
||
# 检查是否已存在
|
||
existing = await db.execute(
|
||
select(PositionCourse).where(
|
||
and_(
|
||
PositionCourse.position_id == position_id,
|
||
PositionCourse.course_id.in_(course_ids),
|
||
PositionCourse.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
existing_course_ids = {pc.course_id for pc in existing.scalars().all()}
|
||
|
||
# 获取当前最大优先级
|
||
max_priority_result = await db.execute(
|
||
select(sa.func.max(PositionCourse.priority)).where(
|
||
and_(
|
||
PositionCourse.position_id == position_id,
|
||
PositionCourse.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
max_priority = max_priority_result.scalar() or 0
|
||
|
||
# 添加新课程
|
||
added_count = 0
|
||
for idx, course_id in enumerate(course_ids):
|
||
if course_id not in existing_course_ids:
|
||
pc = PositionCourse(
|
||
position_id=position_id,
|
||
course_id=course_id,
|
||
course_type=course_type,
|
||
priority=max_priority + idx + 1,
|
||
)
|
||
db.add(pc)
|
||
added_count += 1
|
||
|
||
await db.commit()
|
||
|
||
return ResponseModel(
|
||
message=f"成功添加 {added_count} 门课程",
|
||
data={"added_count": added_count}
|
||
)
|
||
|
||
|
||
@router.put("/{position_id}/courses/{pc_id}")
|
||
async def update_position_course(
|
||
position_id: int,
|
||
pc_id: int,
|
||
payload: dict,
|
||
current_user=Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""更新岗位课程设置"""
|
||
# 查找课程关系
|
||
pc = await db.execute(
|
||
select(PositionCourse).where(
|
||
and_(
|
||
PositionCourse.id == pc_id,
|
||
PositionCourse.position_id == position_id,
|
||
PositionCourse.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
pc = pc.scalar_one_or_none()
|
||
|
||
if not pc:
|
||
return ResponseModel(code=404, message="课程关系不存在")
|
||
|
||
# 更新课程类型
|
||
if "course_type" in payload:
|
||
course_type = payload["course_type"]
|
||
if course_type not in ["required", "optional"]:
|
||
return ResponseModel(code=400, message="课程类型无效")
|
||
pc.course_type = course_type
|
||
|
||
# 更新优先级
|
||
if "priority" in payload:
|
||
pc.priority = payload["priority"]
|
||
|
||
# PositionCourse 未继承审计字段,避免写入不存在字段
|
||
await db.commit()
|
||
|
||
return ResponseModel(message="更新成功")
|
||
|
||
|
||
@router.delete("/{position_id}/courses/{course_id}")
|
||
async def remove_position_course(
|
||
position_id: int,
|
||
course_id: int,
|
||
current_user=Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> ResponseModel:
|
||
"""移除岗位课程"""
|
||
# 查找课程关系
|
||
pc = await db.execute(
|
||
select(PositionCourse).where(
|
||
and_(
|
||
PositionCourse.position_id == position_id,
|
||
PositionCourse.course_id == course_id,
|
||
PositionCourse.is_deleted == False
|
||
)
|
||
)
|
||
)
|
||
pc = pc.scalar_one_or_none()
|
||
|
||
if not pc:
|
||
return ResponseModel(code=404, message="课程关系不存在")
|
||
|
||
# 软删除
|
||
pc.is_deleted = True
|
||
# PositionCourse 未继承审计字段,避免写入不存在字段
|
||
await db.commit()
|
||
|
||
return ResponseModel(message="课程已移除")
|
||
|
||
|