Files
012-kaopeilian/backend/app/api/v1/positions.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

659 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
岗位管理 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="课程已移除")