add backend source code

This commit is contained in:
2026-02-28 15:06:52 +08:00
parent 1bc330e20c
commit 2c37aa9064
67 changed files with 11654 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
from fastapi import APIRouter
from app.api.v1 import departments, staff, indicators, assessments, salary, stats, auth, finance, performance_plans, menus, templates, surveys
api_router = APIRouter()
api_router.include_router(auth.router)
api_router.include_router(departments.router)
api_router.include_router(staff.router)
api_router.include_router(indicators.router)
api_router.include_router(assessments.router)
api_router.include_router(salary.router)
api_router.include_router(stats.router)
api_router.include_router(finance.router)
api_router.include_router(performance_plans.router)
api_router.include_router(menus.router)
api_router.include_router(templates.router)
api_router.include_router(surveys.router)

View File

@@ -0,0 +1,165 @@
"""
API路由 - 绩效考核管理
"""
from typing import Annotated, Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
AssessmentCreate, AssessmentUpdate, AssessmentResponse,
AssessmentListResponse, ResponseBase
)
from app.services.assessment_service import AssessmentService
from app.models.models import User
router = APIRouter(prefix="/assessments", tags=["绩效考核"])
@router.get("", summary="获取考核列表")
async def get_assessments(
staff_id: Optional[int] = Query(None, description="员工ID"),
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
status: Optional[str] = Query(None, description="状态"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取考核列表"""
assessments, total = await AssessmentService.get_list(
db, staff_id, department_id, period_year, period_month, status, page, page_size
)
# 转换响应
result = []
for assessment in assessments:
item = AssessmentListResponse.model_validate(assessment).model_dump()
item["staff_name"] = assessment.staff.name if assessment.staff else None
item["department_name"] = assessment.staff.department.name if assessment.staff and assessment.staff.department else None
result.append(item)
return {
"code": 200,
"message": "success",
"data": result,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/{assessment_id}", summary="获取考核详情")
async def get_assessment(
assessment_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取考核详情"""
assessment = await AssessmentService.get_by_id(db, assessment_id)
if not assessment:
raise HTTPException(status_code=404, detail="考核记录不存在")
result = AssessmentResponse.model_validate(assessment).model_dump()
result["staff_name"] = assessment.staff.name if assessment.staff else None
result["department_name"] = assessment.staff.department.name if assessment.staff and assessment.staff.department else None
# 添加明细指标名称
for detail in result.get("details", []):
for d in assessment.details:
if d.id == detail["id"] and d.indicator:
detail["indicator_name"] = d.indicator.name
break
return {"code": 200, "message": "success", "data": result}
@router.post("", summary="创建考核记录")
async def create_assessment(
assessment_data: AssessmentCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""创建考核记录"""
assessment = await AssessmentService.create(db, assessment_data)
return {"code": 200, "message": "创建成功", "data": {"id": assessment.id}}
@router.put("/{assessment_id}", summary="更新考核记录")
async def update_assessment(
assessment_id: int,
assessment_data: AssessmentUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""更新考核记录"""
assessment = await AssessmentService.update(db, assessment_id, assessment_data)
if not assessment:
raise HTTPException(status_code=400, detail="无法更新,考核记录不存在或状态不允许")
return {"code": 200, "message": "更新成功", "data": {"id": assessment.id}}
@router.post("/{assessment_id}/submit", summary="提交考核")
async def submit_assessment(
assessment_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""提交考核"""
assessment = await AssessmentService.submit(db, assessment_id)
if not assessment:
raise HTTPException(status_code=400, detail="无法提交,考核记录不存在或状态不允许")
return {"code": 200, "message": "提交成功"}
@router.post("/{assessment_id}/review", summary="审核考核")
async def review_assessment(
assessment_id: int,
approved: bool = Query(..., description="是否通过"),
remark: Optional[str] = Query(None, description="审核意见"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""审核考核(需要管理员或经理权限)"""
# 从当前用户获取审核人ID
reviewer_id = current_user.id
assessment = await AssessmentService.review(db, assessment_id, reviewer_id, approved, remark)
if not assessment:
raise HTTPException(status_code=400, detail="无法审核,考核记录不存在或状态不允许")
return {"code": 200, "message": "审核通过" if approved else "已驳回"}
@router.post("/{assessment_id}/finalize", summary="确认考核")
async def finalize_assessment(
assessment_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""确认考核(需要管理员或经理权限)"""
assessment = await AssessmentService.finalize(db, assessment_id)
if not assessment:
raise HTTPException(status_code=400, detail="无法确认,考核记录不存在或状态不允许")
return {"code": 200, "message": "确认成功"}
@router.post("/batch-create", summary="批量创建考核")
async def batch_create_assessments(
department_id: int = Query(..., description="科室ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
indicators: List[int] = Query(..., description="指标ID列表"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""为科室批量创建考核(需要管理员或经理权限)"""
assessments = await AssessmentService.batch_create_for_department(
db, department_id, period_year, period_month, indicators
)
return {
"code": 200,
"message": f"成功创建 {len(assessments)} 条考核记录",
"data": {"count": len(assessments)}
}

135
backend/app/api/v1/auth.py Normal file
View File

@@ -0,0 +1,135 @@
"""
API路由 - 用户认证
"""
from typing import Annotated, List
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.security import verify_password, create_access_token, get_current_active_user
from app.schemas.schemas import UserLogin, UserCreate, UserResponse, Token
from app.models.models import User
router = APIRouter(prefix="/auth", tags=["用户认证"])
@router.post("/login", response_model=Token, summary="用户登录")
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db)
):
"""用户登录"""
from sqlalchemy import select
result = await db.execute(
select(User).where(User.username == form_data.username)
)
user = result.scalar_one_or_none()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(status_code=401, detail="用户名或密码错误")
if not user.is_active:
raise HTTPException(status_code=403, detail="账户已禁用")
access_token = create_access_token(user.id)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/login-json", summary="用户登录(JSON格式)")
async def login_json(
login_data: UserLogin,
db: AsyncSession = Depends(get_db)
):
"""用户登录(JSON格式)"""
result = await db.execute(
select(User).where(User.username == login_data.username)
)
user = result.scalar_one_or_none()
if not user or not verify_password(login_data.password, user.password_hash):
raise HTTPException(status_code=401, detail="用户名或密码错误")
if not user.is_active:
raise HTTPException(status_code=403, detail="账户已禁用")
access_token = create_access_token(user.id)
return {"code": 200, "message": "登录成功", "data": {"access_token": access_token, "token_type": "bearer"}}
@router.post("/register", summary="用户注册")
async def register(
user_data: UserCreate,
db: AsyncSession = Depends(get_db)
):
"""用户注册"""
from sqlalchemy import select
from app.core.security import get_password_hash
# 检查用户名是否已存在
result = await db.execute(
select(User).where(User.username == user_data.username)
)
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="用户名已存在")
user = User(
username=user_data.username,
password_hash=get_password_hash(user_data.password),
staff_id=user_data.staff_id,
role=user_data.role
)
db.add(user)
await db.flush()
return {"code": 200, "message": "注册成功", "data": {"id": user.id}}
@router.get("/me", response_model=UserResponse, summary="获取当前用户")
async def get_current_user_info(
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""获取当前用户信息"""
return current_user
@router.get("/users", summary="获取用户列表")
async def get_users(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取用户列表(需要登录)"""
# 查询用户总数
count_result = await db.execute(select(User))
all_users = count_result.scalars().all()
total = len(all_users)
# 分页查询
offset = (page - 1) * page_size
result = await db.execute(
select(User).order_by(User.id.desc()).offset(offset).limit(page_size)
)
users = result.scalars().all()
return {
"code": 200,
"message": "success",
"data": [
{
"id": u.id,
"username": u.username,
"role": u.role,
"is_active": u.is_active,
"last_login": u.last_login,
"created_at": u.created_at
}
for u in users
],
"total": total,
"page": page,
"page_size": page_size
}

View File

@@ -0,0 +1,107 @@
"""
API路由 - 科室管理
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
DepartmentCreate, DepartmentUpdate, DepartmentResponse,
ResponseBase, PaginatedResponse
)
from app.services.department_service import DepartmentService
from app.models.models import User
router = APIRouter(prefix="/departments", tags=["科室管理"])
@router.get("", summary="获取科室列表")
async def get_departments(
dept_type: Optional[str] = Query(None, description="科室类型"),
is_active: Optional[bool] = Query(None, description="是否启用"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室列表"""
departments, total = await DepartmentService.get_list(
db, dept_type, is_active, page, page_size
)
return {
"code": 200,
"message": "success",
"data": [DepartmentResponse.model_validate(d) for d in departments],
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/tree", summary="获取科室树形结构")
async def get_department_tree(
dept_type: Optional[str] = Query(None, description="科室类型"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室树形结构"""
tree = await DepartmentService.get_tree(db, dept_type)
return {"code": 200, "message": "success", "data": tree}
@router.get("/{dept_id}", summary="获取科室详情")
async def get_department(
dept_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室详情"""
department = await DepartmentService.get_by_id(db, dept_id)
if not department:
raise HTTPException(status_code=404, detail="科室不存在")
return {"code": 200, "message": "success", "data": DepartmentResponse.model_validate(department)}
@router.post("", summary="创建科室")
async def create_department(
dept_data: DepartmentCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建科室(需要管理员或经理权限)"""
# 检查编码是否已存在
existing = await DepartmentService.get_by_code(db, dept_data.code)
if existing:
raise HTTPException(status_code=400, detail="科室编码已存在")
department = await DepartmentService.create(db, dept_data)
return {"code": 200, "message": "创建成功", "data": DepartmentResponse.model_validate(department)}
@router.put("/{dept_id}", summary="更新科室")
async def update_department(
dept_id: int,
dept_data: DepartmentUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新科室(需要管理员或经理权限)"""
department = await DepartmentService.update(db, dept_id, dept_data)
if not department:
raise HTTPException(status_code=404, detail="科室不存在")
return {"code": 200, "message": "更新成功", "data": DepartmentResponse.model_validate(department)}
@router.delete("/{dept_id}", summary="删除科室")
async def delete_department(
dept_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除科室(需要管理员或经理权限)"""
success = await DepartmentService.delete(db, dept_id)
if not success:
raise HTTPException(status_code=400, detail="无法删除,科室下存在子科室")
return {"code": 200, "message": "删除成功"}

View File

@@ -0,0 +1,216 @@
"""
API路由 - 财务核算
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
FinanceRecordCreate, FinanceRecordUpdate, FinanceRecordResponse,
DepartmentBalance, CategorySummary, ResponseBase
)
from app.services.finance_service import FinanceService
from app.models.finance import RevenueCategory, ExpenseCategory, FinanceType
from app.models.models import User
router = APIRouter(prefix="/finance", tags=["财务核算"])
@router.get("/revenue", summary="获取科室收入")
async def get_revenue(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室收入列表"""
data = await FinanceService.get_department_revenue(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/expense", summary="获取科室支出")
async def get_expense(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室支出列表"""
data = await FinanceService.get_department_expense(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/balance", summary="获取收支结余")
async def get_balance(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室收支结余"""
data = await FinanceService.get_department_balance(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/revenue/by-category", summary="按类别统计收入")
async def get_revenue_by_category(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""按类别统计收入"""
data = await FinanceService.get_revenue_by_category(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/expense/by-category", summary="按类别统计支出")
async def get_expense_by_category(
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""按类别统计支出"""
data = await FinanceService.get_expense_by_category(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/summary", summary="获取科室财务汇总")
async def get_department_summary(
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., ge=1, le=12, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取所有科室的财务汇总"""
data = await FinanceService.get_department_summary(
db, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/categories", summary="获取财务类别")
async def get_categories(
current_user: User = Depends(get_current_active_user)
):
"""获取收入和支出类别"""
revenue_categories = [
{"value": cat.value, "label": label}
for cat, label in FinanceService.REVENUE_LABELS.items()
]
expense_categories = [
{"value": cat.value, "label": label}
for cat, label in FinanceService.EXPENSE_LABELS.items()
]
return {
"code": 200,
"message": "success",
"data": {
"revenue": revenue_categories,
"expense": expense_categories
}
}
@router.post("", summary="创建财务记录")
async def create_finance_record(
record_data: FinanceRecordCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建财务记录(需要管理员或经理权限)"""
# 验证类别
if record_data.finance_type == FinanceType.REVENUE:
valid_categories = [cat.value for cat in RevenueCategory]
else:
valid_categories = [cat.value for cat in ExpenseCategory]
if record_data.category not in valid_categories:
raise HTTPException(
status_code=400,
detail=f"无效的类别: {record_data.category}"
)
record = await FinanceService.create_finance_record(
db,
department_id=record_data.department_id,
finance_type=record_data.finance_type,
category=record_data.category,
amount=record_data.amount,
period_year=record_data.period_year,
period_month=record_data.period_month,
source=record_data.source,
remark=record_data.remark
)
return {"code": 200, "message": "创建成功", "data": {"id": record.id}}
@router.put("/{record_id}", summary="更新财务记录")
async def update_finance_record(
record_id: int,
record_data: FinanceRecordUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新财务记录(需要管理员或经理权限)"""
record = await FinanceService.update_finance_record(
db, record_id, **record_data.model_dump(exclude_unset=True)
)
if not record:
raise HTTPException(status_code=404, detail="财务记录不存在")
return {"code": 200, "message": "更新成功"}
@router.delete("/{record_id}", summary="删除财务记录")
async def delete_finance_record(
record_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除财务记录(需要管理员或经理权限)"""
success = await FinanceService.delete_finance_record(db, record_id)
if not success:
raise HTTPException(status_code=404, detail="财务记录不存在")
return {"code": 200, "message": "删除成功"}

View File

@@ -0,0 +1,141 @@
"""
API路由 - 考核指标管理
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
IndicatorCreate, IndicatorUpdate, IndicatorResponse,
ResponseBase
)
from app.services.indicator_service import IndicatorService
from app.models.models import User
router = APIRouter(prefix="/indicators", tags=["考核指标"])
@router.get("", summary="获取指标列表")
async def get_indicators(
indicator_type: Optional[str] = Query(None, description="指标类型"),
bs_dimension: Optional[str] = Query(None, description="BSC 维度"),
is_active: Optional[bool] = Query(None, description="是否启用"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取指标列表"""
indicators, total = await IndicatorService.get_list(
db, indicator_type, bs_dimension, is_active, page, page_size
)
return {
"code": 200,
"message": "success",
"data": [IndicatorResponse.model_validate(i, from_attributes=True) for i in indicators],
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/active", summary="获取所有启用的指标")
async def get_active_indicators(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取所有启用的指标"""
indicators = await IndicatorService.get_active_indicators(db)
return {
"code": 200,
"message": "success",
"data": [IndicatorResponse.model_validate(i, from_attributes=True) for i in indicators]
}
@router.get("/{indicator_id}", summary="获取指标详情")
async def get_indicator(
indicator_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取指标详情"""
indicator = await IndicatorService.get_by_id(db, indicator_id)
if not indicator:
raise HTTPException(status_code=404, detail="指标不存在")
return {"code": 200, "message": "success", "data": IndicatorResponse.model_validate(indicator, from_attributes=True)}
@router.post("", summary="创建指标")
async def create_indicator(
indicator_data: IndicatorCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建指标(需要管理员或经理权限)"""
# 检查编码是否已存在
existing = await IndicatorService.get_by_code(db, indicator_data.code)
if existing:
raise HTTPException(status_code=400, detail="指标编码已存在")
indicator = await IndicatorService.create(db, indicator_data)
return {"code": 200, "message": "创建成功", "data": IndicatorResponse.model_validate(indicator, from_attributes=True)}
@router.put("/{indicator_id}", summary="更新指标")
async def update_indicator(
indicator_id: int,
indicator_data: IndicatorUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新指标(需要管理员或经理权限)"""
indicator = await IndicatorService.update(db, indicator_id, indicator_data)
if not indicator:
raise HTTPException(status_code=404, detail="指标不存在")
return {"code": 200, "message": "更新成功", "data": IndicatorResponse.model_validate(indicator, from_attributes=True)}
@router.delete("/{indicator_id}", summary="删除指标")
async def delete_indicator(
indicator_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除指标(需要管理员或经理权限)"""
success = await IndicatorService.delete(db, indicator_id)
if not success:
raise HTTPException(status_code=404, detail="指标不存在")
return {"code": 200, "message": "删除成功"}
@router.get("/templates/list", summary="获取指标模板列表")
async def get_indicator_templates(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取指标模板列表"""
templates = await IndicatorService.get_templates()
return {
"code": 200,
"message": "success",
"data": templates
}
@router.post("/templates/import", summary="导入指标模板")
async def import_indicator_template(
template_data: dict,
overwrite: bool = Query(False, description="是否覆盖已存在的指标"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""导入指标模板"""
count = await IndicatorService.import_template(db, template_data, overwrite)
return {
"code": 200,
"message": f"成功导入 {count} 个指标",
"data": {"created_count": count}
}

163
backend/app/api/v1/menus.py Normal file
View File

@@ -0,0 +1,163 @@
"""
菜单管理 API
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import MenuCreate, MenuUpdate, MenuResponse, ResponseBase
from app.services.menu_service import MenuService
from app.models.models import User, Menu
router = APIRouter(prefix="/menus", tags=["菜单管理"])
@router.get("/tree", summary="获取菜单树")
async def get_menu_tree(
visible_only: bool = Query(True, description="是否只返回可见菜单"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取菜单树形结构"""
tree = await MenuService.get_tree(db, visible_only)
return {
"code": 200,
"message": "success",
"data": tree
}
@router.get("", summary="获取菜单列表")
async def get_menus(
menu_type: Optional[str] = Query(None, description="菜单类型"),
is_visible: Optional[bool] = Query(None, description="是否可见"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取菜单列表"""
menus = await MenuService.get_list(db, menu_type, is_visible)
menu_list = []
for menu in menus:
menu_dict = {
"id": menu.id,
"parent_id": menu.parent_id,
"menu_type": menu.menu_type,
"menu_name": menu.menu_name,
"menu_icon": menu.menu_icon,
"path": menu.path,
"component": menu.component,
"permission": menu.permission,
"sort_order": menu.sort_order,
"is_visible": menu.is_visible,
"is_active": menu.is_active,
"created_at": menu.created_at,
"updated_at": menu.updated_at
}
menu_list.append(menu_dict)
return {
"code": 200,
"message": "success",
"data": menu_list
}
@router.get("/{menu_id}", summary="获取菜单详情")
async def get_menu(
menu_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取菜单详情"""
menu = await MenuService.get_by_id(db, menu_id)
if not menu:
raise HTTPException(status_code=404, detail="菜单不存在")
return {
"code": 200,
"message": "success",
"data": {
"id": menu.id,
"parent_id": menu.parent_id,
"menu_type": menu.menu_type,
"menu_name": menu.menu_name,
"menu_icon": menu.menu_icon,
"path": menu.path,
"component": menu.component,
"permission": menu.permission,
"sort_order": menu.sort_order,
"is_visible": menu.is_visible,
"is_active": menu.is_active
}
}
@router.post("", summary="创建菜单")
async def create_menu(
menu_data: MenuCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建菜单(需要管理员或经理权限)"""
# 如果指定了父菜单,检查父菜单是否存在
if menu_data.parent_id:
parent = await MenuService.get_by_id(db, menu_data.parent_id)
if not parent:
raise HTTPException(status_code=400, detail="父菜单不存在")
menu = await MenuService.create(db, menu_data.model_dump())
return {
"code": 200,
"message": "创建成功",
"data": {"id": menu.id}
}
@router.put("/{menu_id}", summary="更新菜单")
async def update_menu(
menu_id: int,
menu_data: MenuUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新菜单(需要管理员或经理权限)"""
menu = await MenuService.update(db, menu_id, menu_data.model_dump(exclude_unset=True))
if not menu:
raise HTTPException(status_code=404, detail="菜单不存在")
return {
"code": 200,
"message": "更新成功",
"data": {"id": menu.id}
}
@router.delete("/{menu_id}", summary="删除菜单")
async def delete_menu(
menu_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除菜单(需要管理员或经理权限)"""
success = await MenuService.delete(db, menu_id)
if not success:
raise HTTPException(status_code=400, detail="无法删除,菜单不存在或存在子菜单")
return {
"code": 200,
"message": "删除成功"
}
@router.post("/init", summary="初始化默认菜单")
async def init_default_menus(
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""初始化默认菜单(需要管理员权限)"""
await MenuService.init_default_menus(db)
return {
"code": 200,
"message": "初始化成功"
}

View File

@@ -0,0 +1,309 @@
"""
绩效计划管理 API
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
PerformancePlanCreate, PerformancePlanUpdate, PerformancePlanResponse,
PerformancePlanStats, PlanKpiRelationCreate, PlanKpiRelationUpdate,
ResponseBase
)
from app.services.performance_plan_service import PerformancePlanService
from app.models.models import User, PlanStatus
router = APIRouter(prefix="/plans", tags=["绩效计划管理"])
@router.get("", summary="获取绩效计划列表")
async def get_performance_plans(
plan_level: Optional[str] = Query(None, description="计划层级"),
plan_year: Optional[int] = Query(None, description="计划年度"),
department_id: Optional[int] = Query(None, description="科室 ID"),
status: Optional[str] = Query(None, description="状态"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效计划列表"""
plans, total = await PerformancePlanService.get_list(
db, plan_level, plan_year, department_id, status, page, page_size
)
# 构建响应数据
plan_list = []
for plan in plans:
plan_dict = {
"id": plan.id,
"plan_name": plan.plan_name,
"plan_code": plan.plan_code,
"plan_level": plan.plan_level,
"plan_year": plan.plan_year,
"plan_month": plan.plan_month,
"status": plan.status,
"department_id": plan.department_id,
"department_name": plan.department.name if plan.department else None,
"staff_id": plan.staff_id,
"staff_name": plan.staff.name if plan.staff else None,
"description": plan.description,
"created_at": plan.created_at,
"updated_at": plan.updated_at
}
plan_list.append(plan_dict)
return {
"code": 200,
"message": "success",
"data": plan_list,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/tree", summary="获取绩效计划树")
async def get_performance_plan_tree(
plan_year: Optional[int] = Query(None, description="计划年度"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效计划树形结构"""
tree = await PerformancePlanService.get_tree(db, plan_year)
return {
"code": 200,
"message": "success",
"data": tree
}
@router.get("/stats", summary="获取绩效计划统计")
async def get_performance_plan_stats(
plan_year: Optional[int] = Query(None, description="计划年度"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效计划统计信息"""
stats = await PerformancePlanService.get_stats(db, plan_year)
return {
"code": 200,
"message": "success",
"data": stats
}
@router.get("/{plan_id}", summary="获取绩效计划详情")
async def get_performance_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效计划详情"""
plan = await PerformancePlanService.get_by_id(db, plan_id)
if not plan:
raise HTTPException(status_code=404, detail="绩效计划不存在")
# 构建响应数据
kpi_relations = []
for relation in plan.kpi_relations:
kpi_relations.append({
"id": relation.id,
"indicator_id": relation.indicator_id,
"indicator_name": relation.indicator.name if relation.indicator else None,
"indicator_code": relation.indicator.code if relation.indicator else None,
"target_value": relation.target_value,
"target_unit": relation.target_unit,
"weight": relation.weight,
"scoring_method": relation.scoring_method,
"scoring_params": relation.scoring_params,
"remark": relation.remark
})
plan_data = {
"id": plan.id,
"plan_name": plan.plan_name,
"plan_code": plan.plan_code,
"plan_level": plan.plan_level,
"plan_year": plan.plan_year,
"plan_month": plan.plan_month,
"plan_type": plan.plan_type,
"department_id": plan.department_id,
"department_name": plan.department.name if plan.department else None,
"staff_id": plan.staff_id,
"staff_name": plan.staff.name if plan.staff else None,
"parent_plan_id": plan.parent_plan_id,
"description": plan.description,
"strategic_goals": plan.strategic_goals,
"key_initiatives": plan.key_initiatives,
"status": plan.status,
"submitter_id": plan.submitter_id,
"submit_time": plan.submit_time,
"approver_id": plan.approver_id,
"approve_time": plan.approve_time,
"approve_remark": plan.approve_remark,
"version": plan.version,
"is_active": plan.is_active,
"created_at": plan.created_at,
"updated_at": plan.updated_at,
"kpi_relations": kpi_relations
}
return {
"code": 200,
"message": "success",
"data": plan_data
}
@router.post("", summary="创建绩效计划")
async def create_performance_plan(
plan_data: PerformancePlanCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""创建绩效计划"""
plan = await PerformancePlanService.create(db, plan_data, current_user.id)
return {
"code": 200,
"message": "创建成功",
"data": {"id": plan.id}
}
@router.put("/{plan_id}", summary="更新绩效计划")
async def update_performance_plan(
plan_id: int,
plan_data: PerformancePlanUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""更新绩效计划"""
plan = await PerformancePlanService.update(db, plan_id, plan_data)
if not plan:
raise HTTPException(status_code=404, detail="绩效计划不存在")
return {
"code": 200,
"message": "更新成功",
"data": {"id": plan.id}
}
@router.post("/{plan_id}/submit", summary="提交绩效计划")
async def submit_performance_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""提交绩效计划"""
plan = await PerformancePlanService.submit(db, plan_id)
if not plan:
raise HTTPException(status_code=400, detail="无法提交,计划不存在或状态不允许")
return {
"code": 200,
"message": "提交成功"
}
@router.post("/{plan_id}/approve", summary="审批绩效计划")
async def approve_performance_plan(
plan_id: int,
approved: bool = Query(..., description="是否通过"),
remark: Optional[str] = Query(None, description="审批意见"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""审批绩效计划(需要管理员或经理权限)"""
plan = await PerformancePlanService.approve(db, plan_id, current_user.id, approved, remark)
if not plan:
raise HTTPException(status_code=400, detail="无法审批,计划不存在或状态不允许")
return {
"code": 200,
"message": "审核通过" if approved else "已驳回"
}
@router.post("/{plan_id}/activate", summary="激活绩效计划")
async def activate_performance_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""激活绩效计划(需要管理员或经理权限)"""
plan = await PerformancePlanService.activate(db, plan_id)
if not plan:
raise HTTPException(status_code=400, detail="无法激活,计划不存在或状态不允许")
return {
"code": 200,
"message": "激活成功"
}
@router.delete("/{plan_id}", summary="删除绩效计划")
async def delete_performance_plan(
plan_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除绩效计划(需要管理员或经理权限)"""
success = await PerformancePlanService.delete(db, plan_id)
if not success:
raise HTTPException(status_code=404, detail="绩效计划不存在")
return {
"code": 200,
"message": "删除成功"
}
@router.post("/{plan_id}/kpi-relations", summary="添加计划指标关联")
async def add_kpi_relation(
plan_id: int,
kpi_data: PlanKpiRelationCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""添加计划指标关联(需要管理员或经理权限)"""
relation = await PerformancePlanService.add_kpi_relation(db, plan_id, kpi_data)
if not relation:
raise HTTPException(status_code=404, detail="绩效计划不存在")
return {
"code": 200,
"message": "添加成功",
"data": {"id": relation.id}
}
@router.put("/kpi-relations/{relation_id}", summary="更新计划指标关联")
async def update_kpi_relation(
relation_id: int,
kpi_data: PlanKpiRelationUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新计划指标关联(需要管理员或经理权限)"""
relation = await PerformancePlanService.update_kpi_relation(db, relation_id, kpi_data.model_dump())
if not relation:
raise HTTPException(status_code=404, detail="指标关联不存在")
return {
"code": 200,
"message": "更新成功",
"data": {"id": relation.id}
}
@router.delete("/kpi-relations/{relation_id}", summary="删除计划指标关联")
async def delete_kpi_relation(
relation_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除计划指标关联(需要管理员或经理权限)"""
success = await PerformancePlanService.delete_kpi_relation(db, relation_id)
if not success:
raise HTTPException(status_code=404, detail="指标关联不存在")
return {
"code": 200,
"message": "删除成功"
}

View File

@@ -0,0 +1,155 @@
"""
API路由 - 工资核算管理
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
SalaryRecordCreate, SalaryRecordUpdate, SalaryRecordResponse,
ResponseBase
)
from app.services.salary_service import SalaryService
from app.models.models import User
router = APIRouter(prefix="/salary", tags=["工资核算"])
@router.get("", summary="获取工资记录列表")
async def get_salary_records(
staff_id: Optional[int] = Query(None, description="员工ID"),
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
status: Optional[str] = Query(None, description="状态"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取工资记录列表"""
records, total = await SalaryService.get_list(
db, staff_id, department_id, period_year, period_month, status, page, page_size
)
result = []
for record in records:
item = SalaryRecordResponse.model_validate(record).model_dump()
item["staff_name"] = record.staff.name if record.staff else None
item["department_name"] = record.staff.department.name if record.staff and record.staff.department else None
result.append(item)
return {
"code": 200,
"message": "success",
"data": result,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/{record_id}", summary="获取工资记录详情")
async def get_salary_record(
record_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取工资记录详情"""
record = await SalaryService.get_by_id(db, record_id)
if not record:
raise HTTPException(status_code=404, detail="工资记录不存在")
result = SalaryRecordResponse.model_validate(record).model_dump()
result["staff_name"] = record.staff.name if record.staff else None
result["department_name"] = record.staff.department.name if record.staff and record.staff.department else None
return {"code": 200, "message": "success", "data": result}
@router.post("", summary="创建工资记录")
async def create_salary_record(
record_data: SalaryRecordCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建工资记录(需要管理员或经理权限)"""
record = await SalaryService.create(db, record_data)
return {"code": 200, "message": "创建成功", "data": {"id": record.id}}
@router.put("/{record_id}", summary="更新工资记录")
async def update_salary_record(
record_id: int,
record_data: SalaryRecordUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新工资记录(需要管理员或经理权限)"""
record = await SalaryService.update(db, record_id, record_data)
if not record:
raise HTTPException(status_code=400, detail="无法更新,记录不存在或状态不允许")
return {"code": 200, "message": "更新成功", "data": {"id": record.id}}
@router.post("/generate", summary="根据考核生成工资")
async def generate_salary(
staff_id: int = Query(..., description="员工ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""根据考核记录生成工资记录(需要管理员或经理权限)"""
record = await SalaryService.generate_from_assessment(
db, staff_id, period_year, period_month
)
if not record:
raise HTTPException(status_code=400, detail="无法生成,未找到已确认的考核记录或已存在工资记录")
return {"code": 200, "message": "生成成功", "data": {"id": record.id}}
@router.post("/batch-generate", summary="批量生成工资")
async def batch_generate_salary(
department_id: int = Query(..., description="科室ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""为科室批量生成工资记录(需要管理员或经理权限)"""
records = await SalaryService.batch_generate_for_department(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": f"成功生成 {len(records)} 条工资记录",
"data": {"count": len(records)}
}
@router.post("/{record_id}/confirm", summary="确认工资")
async def confirm_salary(
record_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""确认工资(需要管理员或经理权限)"""
record = await SalaryService.confirm(db, record_id)
if not record:
raise HTTPException(status_code=400, detail="无法确认,记录不存在或状态不允许")
return {"code": 200, "message": "确认成功"}
@router.post("/batch-confirm", summary="批量确认工资")
async def batch_confirm_salary(
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
department_id: Optional[int] = Query(None, description="科室ID"),
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""批量确认工资(需要管理员或经理权限)"""
count = await SalaryService.batch_confirm(db, period_year, period_month, department_id)
return {"code": 200, "message": f"成功确认 {count} 条工资记录", "data": {"count": count}}

123
backend/app/api/v1/staff.py Normal file
View File

@@ -0,0 +1,123 @@
"""
API路由 - 员工管理
"""
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
StaffCreate, StaffUpdate, StaffResponse,
ResponseBase
)
from app.services.staff_service import StaffService
from app.models.models import User
router = APIRouter(prefix="/staff", tags=["员工管理"])
@router.get("", summary="获取员工列表")
async def get_staff_list(
department_id: Optional[int] = Query(None, description="科室ID"),
status: Optional[str] = Query(None, description="状态"),
keyword: Optional[str] = Query(None, description="搜索关键词"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取员工列表"""
staff_list, total = await StaffService.get_list(
db, department_id, status, keyword, page, page_size
)
# 添加科室名称
result = []
for staff in staff_list:
staff_dict = StaffResponse.model_validate(staff).model_dump()
staff_dict["department_name"] = staff.department.name if staff.department else None
result.append(staff_dict)
return {
"code": 200,
"message": "success",
"data": result,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/{staff_id}", summary="获取员工详情")
async def get_staff(
staff_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取员工详情"""
staff = await StaffService.get_by_id(db, staff_id)
if not staff:
raise HTTPException(status_code=404, detail="员工不存在")
staff_dict = StaffResponse.model_validate(staff).model_dump()
staff_dict["department_name"] = staff.department.name if staff.department else None
return {"code": 200, "message": "success", "data": staff_dict}
@router.post("", summary="创建员工")
async def create_staff(
staff_data: StaffCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建员工(需要管理员或经理权限)"""
# 检查工号是否已存在
existing = await StaffService.get_by_employee_id(db, staff_data.employee_id)
if existing:
raise HTTPException(status_code=400, detail="工号已存在")
staff = await StaffService.create(db, staff_data)
return {"code": 200, "message": "创建成功", "data": StaffResponse.model_validate(staff)}
@router.put("/{staff_id}", summary="更新员工")
async def update_staff(
staff_id: int,
staff_data: StaffUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新员工(需要管理员或经理权限)"""
staff = await StaffService.update(db, staff_id, staff_data)
if not staff:
raise HTTPException(status_code=404, detail="员工不存在")
return {"code": 200, "message": "更新成功", "data": StaffResponse.model_validate(staff)}
@router.delete("/{staff_id}", summary="删除员工")
async def delete_staff(
staff_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除员工(需要管理员或经理权限)"""
success = await StaffService.delete(db, staff_id)
if not success:
raise HTTPException(status_code=404, detail="员工不存在")
return {"code": 200, "message": "删除成功"}
@router.get("/department/{department_id}", summary="获取科室员工")
async def get_department_staff(
department_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室下所有员工"""
staff_list = await StaffService.get_by_department(db, department_id)
return {
"code": 200,
"message": "success",
"data": [StaffResponse.model_validate(s) for s in staff_list]
}

241
backend/app/api/v1/stats.py Normal file
View File

@@ -0,0 +1,241 @@
"""
统计报表 API
"""
from typing import Optional
from datetime import datetime
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user
from app.models.models import User
from app.services.stats_service import StatsService
router = APIRouter(prefix="/stats", tags=["统计报表"])
@router.get("/bsc-dimension", summary="BSC 维度分析")
async def get_bsc_dimension_stats(
department_id: Optional[int] = Query(None, description="科室 ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取 BSC 四个维度的统计分析"""
result = await StatsService.get_bsc_dimension_stats(
db, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/department", summary="科室绩效统计")
async def get_department_stats(
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取各科室绩效统计"""
result = await StatsService.get_department_stats(db, period_year, period_month)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/trend", summary="趋势分析")
async def get_trend_stats(
department_id: Optional[int] = Query(None, description="科室 ID"),
period_year: Optional[int] = Query(None, description="年度"),
months: Optional[int] = Query(6, ge=1, le=24, description="最近几个月"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效趋势分析(月度)"""
# 如果没有指定年份,使用当前年份
if not period_year:
period_year = datetime.now().year
result = await StatsService.get_trend_stats(db, department_id, period_year, months)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/alerts", summary="预警数据")
async def get_alerts(
limit: Optional[int] = Query(10, ge=1, le=100, description="返回数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取预警数据(考核到期、工资未发等)"""
# TODO: 从数据库实际查询预警数据
# 目前返回模拟数据用于演示
return {
"code": 200,
"message": "success",
"data": {
"lowScoreStaff": [], # 低分员工
"incompleteDepartments": [], # 未完成考核科室
"anomalyData": [] # 异常数据
}
}
@router.get("/period", summary="周期统计")
async def get_period_stats(
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取周期统计数据"""
# 如果没有指定年月,使用当前年月
if not period_year:
period_year = datetime.now().year
if not period_month:
period_month = datetime.now().month
# 获取该周期的考核统计
result = await StatsService.get_department_stats(db, period_year, period_month)
# 计算汇总数据
total_departments = len(result)
total_staff = sum(dept.get('staff_count', 0) for dept in result)
avg_score = sum(dept.get('avg_score', 0) for dept in result) / total_departments if total_departments > 0 else 0
return {
"code": 200,
"message": "success",
"data": {
"period": f"{period_year}{period_month}",
"total_departments": total_departments,
"total_staff": total_staff,
"avg_score": round(avg_score, 2),
"departments": result
}
}
@router.get("/kpi-gauges", summary="关键指标仪表盘")
async def get_kpi_gauges(
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取关键指标仪表盘数据"""
# 如果没有指定年月,使用当前年月
if not period_year:
period_year = datetime.now().year
if not period_month:
period_month = datetime.now().month
# TODO: 从数据库实际计算这些指标
# 目前返回模拟数据用于演示
return {
"code": 200,
"message": "success",
"data": {
"bed_usage_rate": 85.5, # 床位使用率 (%)
"drug_ratio": 32.8, # 药占比 (%)
"material_ratio": 18.5, # 材料占比 (%)
"satisfaction_rate": 92.3 # 患者满意度 (%)
}
}
@router.get("/finance-trend", summary="收支趋势")
async def get_finance_trend(
months: Optional[int] = Query(6, ge=1, le=24, description="最近几个月"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取收支趋势数据"""
# TODO: 从数据库实际查询收支数据
# 目前返回模拟数据用于演示
from datetime import datetime
current_month = datetime.now().month
data = []
for i in range(months, 0, -1):
month = current_month - i + 1
if month < 1:
month += 12
data.append({
"period": f"{month}",
"income": 1000000 + (months - i) * 50000,
"expense": 800000 + (months - i) * 30000,
"profit": 200000 + (months - i) * 20000
})
return {
"code": 200,
"message": "success",
"data": data
}
@router.get("/department-ranking", summary="科室绩效排名")
async def get_department_ranking(
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
limit: Optional[int] = Query(10, ge=1, le=100, description="返回数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取科室绩效排名"""
# 如果没有指定年月,使用当前年月
if not period_year:
period_year = datetime.now().year
if not period_month:
period_month = datetime.now().month
result = await StatsService.get_department_stats(db, period_year, period_month)
# 返回前 limit 个
return {
"code": 200,
"message": "success",
"data": result[:limit] if limit else result
}
@router.get("/ranking", summary="绩效排名")
async def get_ranking_stats(
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
limit: int = Query(10, ge=1, le=100, description="返回数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取绩效排名前 N 名"""
result = await StatsService.get_ranking_stats(db, period_year, period_month, limit)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/completion", summary="指标完成度")
async def get_completion_stats(
indicator_id: Optional[int] = Query(None, description="指标 ID"),
period_year: int = Query(..., description="年度"),
period_month: int = Query(..., description="月份"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取指标完成度统计"""
result = await StatsService.get_completion_stats(db, indicator_id, period_year, period_month)
return {
"code": 200,
"message": "success",
"data": result
}

View File

@@ -0,0 +1,401 @@
"""
满意度调查 API 接口
"""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, Field, ConfigDict
from datetime import datetime
import json
from app.core.database import get_db
from app.models.models import SurveyStatus, SurveyType, QuestionType
from app.services.survey_service import SurveyService
router = APIRouter(prefix="/surveys", tags=["满意度调查"])
# ==================== Pydantic Schemas ====================
class QuestionCreate(BaseModel):
"""题目创建"""
question_text: str = Field(..., description="题目内容")
question_type: str = Field(..., description="题目类型")
options: Optional[List[dict]] = Field(None, description="选项列表")
score_max: int = Field(5, description="最高分值")
is_required: bool = Field(True, description="是否必答")
class QuestionResponse(BaseModel):
"""题目响应"""
model_config = ConfigDict(from_attributes=True)
id: int
question_text: str
question_type: str
options: Optional[str] = None
score_max: int
is_required: bool
sort_order: int
class SurveyCreate(BaseModel):
"""问卷创建"""
survey_name: str = Field(..., description="问卷名称")
survey_code: str = Field(..., description="问卷编码")
survey_type: str = Field(..., description="问卷类型")
description: Optional[str] = Field(None, description="描述")
target_departments: Optional[List[int]] = Field(None, description="目标科室")
is_anonymous: bool = Field(True, description="是否匿名")
questions: Optional[List[QuestionCreate]] = Field(None, description="题目列表")
class SurveyUpdate(BaseModel):
"""问卷更新"""
survey_name: Optional[str] = None
description: Optional[str] = None
target_departments: Optional[List[int]] = None
is_anonymous: Optional[bool] = None
class SurveyResponse(BaseModel):
"""问卷响应"""
model_config = ConfigDict(from_attributes=True)
id: int
survey_name: str
survey_code: str
survey_type: str
description: Optional[str] = None
target_departments: Optional[str] = None
total_questions: int
status: str
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
is_anonymous: bool
created_at: datetime
class SurveyDetailResponse(SurveyResponse):
"""问卷详情响应"""
questions: Optional[List[QuestionResponse]] = None
class AnswerSubmit(BaseModel):
"""回答提交"""
question_id: int
answer_value: str
class ResponseSubmit(BaseModel):
"""问卷回答提交"""
department_id: Optional[int] = None
respondent_type: str = "patient"
respondent_id: Optional[int] = None
respondent_phone: Optional[str] = None
answers: List[AnswerSubmit]
class ResponseResult(BaseModel):
"""回答结果"""
model_config = ConfigDict(from_attributes=True)
id: int
survey_id: int
department_id: Optional[int] = None
total_score: float
max_score: float
satisfaction_rate: float
submitted_at: datetime
# ==================== API Endpoints ====================
@router.get("", summary="获取问卷列表")
async def get_survey_list(
survey_type: Optional[str] = Query(None, description="问卷类型"),
status: Optional[str] = Query(None, description="状态"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db)
):
"""获取问卷列表"""
type_filter = SurveyType(survey_type) if survey_type else None
status_filter = SurveyStatus(status) if status else None
surveys, total = await SurveyService.get_survey_list(
db, type_filter, status_filter, page, page_size
)
return {
"code": 200,
"message": "success",
"data": [
SurveyResponse.model_validate(s) for s in surveys
],
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/{survey_id}", summary="获取问卷详情")
async def get_survey_detail(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""获取问卷详情"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
detail = SurveyDetailResponse.model_validate(survey)
if survey.questions:
detail.questions = [QuestionResponse.model_validate(q) for q in survey.questions]
return {
"code": 200,
"message": "success",
"data": detail
}
@router.post("", summary="创建问卷")
async def create_survey(
survey_data: SurveyCreate,
db: AsyncSession = Depends(get_db)
):
"""创建问卷"""
try:
survey_type = SurveyType(survey_data.survey_type)
except ValueError:
raise HTTPException(status_code=400, detail="无效的问卷类型")
survey = await SurveyService.create_survey(
db,
survey_name=survey_data.survey_name,
survey_code=survey_data.survey_code,
survey_type=survey_type,
description=survey_data.description,
target_departments=survey_data.target_departments,
is_anonymous=survey_data.is_anonymous
)
# 添加题目
if survey_data.questions:
for question in survey_data.questions:
try:
question_type = QuestionType(question.question_type)
except ValueError:
continue
await SurveyService.add_question(
db,
survey_id=survey.id,
question_text=question.question_text,
question_type=question_type,
options=question.options,
score_max=question.score_max,
is_required=question.is_required
)
await db.commit()
return {
"code": 200,
"message": "创建成功",
"data": SurveyResponse.model_validate(survey)
}
@router.put("/{survey_id}", summary="更新问卷")
async def update_survey(
survey_id: int,
survey_data: SurveyUpdate,
db: AsyncSession = Depends(get_db)
):
"""更新问卷"""
try:
update_dict = survey_data.model_dump(exclude_unset=True)
survey = await SurveyService.update_survey(db, survey_id, **update_dict)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
await db.commit()
return {
"code": 200,
"message": "更新成功",
"data": SurveyResponse.model_validate(survey)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{survey_id}/publish", summary="发布问卷")
async def publish_survey(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""发布问卷"""
try:
survey = await SurveyService.publish_survey(db, survey_id)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
await db.commit()
return {
"code": 200,
"message": "发布成功",
"data": SurveyResponse.model_validate(survey)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{survey_id}/close", summary="结束问卷")
async def close_survey(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""结束问卷"""
try:
survey = await SurveyService.close_survey(db, survey_id)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
await db.commit()
return {
"code": 200,
"message": "问卷已结束",
"data": SurveyResponse.model_validate(survey)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/{survey_id}", summary="删除问卷")
async def delete_survey(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""删除问卷"""
try:
success = await SurveyService.delete_survey(db, survey_id)
if not success:
raise HTTPException(status_code=404, detail="问卷不存在")
await db.commit()
return {
"code": 200,
"message": "删除成功"
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{survey_id}/submit", summary="提交问卷回答")
async def submit_response(
survey_id: int,
response_data: ResponseSubmit,
db: AsyncSession = Depends(get_db)
):
"""提交问卷回答"""
try:
answers = [a.model_dump() for a in response_data.answers]
response = await SurveyService.submit_response(
db,
survey_id=survey_id,
department_id=response_data.department_id,
answers=answers,
respondent_type=response_data.respondent_type,
respondent_id=response_data.respondent_id,
respondent_phone=response_data.respondent_phone
)
await db.commit()
return {
"code": 200,
"message": "提交成功",
"data": ResponseResult.model_validate(response)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{survey_id}/stats", summary="获取问卷统计")
async def get_survey_stats(
survey_id: int,
db: AsyncSession = Depends(get_db)
):
"""获取问卷各题目统计"""
survey = await SurveyService.get_survey_by_id(db, survey_id)
if not survey:
raise HTTPException(status_code=404, detail="问卷不存在")
stats = await SurveyService.get_question_stats(db, survey_id)
return {
"code": 200,
"message": "success",
"data": {
"survey_id": survey_id,
"survey_name": survey.survey_name,
"total_responses": await _get_response_count(db, survey_id),
"question_stats": stats
}
}
@router.get("/stats/department", summary="获取科室满意度统计")
async def get_department_satisfaction(
survey_id: Optional[int] = Query(None, description="问卷ID"),
department_id: Optional[int] = Query(None, description="科室ID"),
period_year: Optional[int] = Query(None, description="年度"),
period_month: Optional[int] = Query(None, description="月份"),
db: AsyncSession = Depends(get_db)
):
"""获取科室满意度统计"""
stats = await SurveyService.get_department_satisfaction(
db, survey_id, department_id, period_year, period_month
)
return {
"code": 200,
"message": "success",
"data": stats
}
@router.get("/stats/trend/{department_id}", summary="获取满意度趋势")
async def get_satisfaction_trend(
department_id: int,
months: int = Query(6, ge=1, le=12, description="查询月数"),
db: AsyncSession = Depends(get_db)
):
"""获取满意度趋势"""
trend = await SurveyService.get_satisfaction_trend(db, department_id, months)
return {
"code": 200,
"message": "success",
"data": trend
}
async def _get_response_count(db: AsyncSession, survey_id: int) -> int:
"""获取问卷回答数"""
from sqlalchemy import select, func
from app.models.models import SurveyResponse
result = await db.execute(
select(func.count(SurveyResponse.id))
.where(SurveyResponse.survey_id == survey_id)
)
return result.scalar() or 0

View File

@@ -0,0 +1,271 @@
"""
API路由 - 指标模板管理
"""
from typing import Annotated, Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_active_user, get_current_manager_user
from app.schemas.schemas import (
IndicatorTemplateCreate, IndicatorTemplateUpdate,
IndicatorTemplateResponse, IndicatorTemplateListResponse,
TemplateIndicatorCreate, TemplateIndicatorUpdate, TemplateIndicatorResponse,
ResponseBase
)
from app.services.template_service import TemplateService
from app.models.models import User
router = APIRouter(prefix="/templates", tags=["指标模板"])
@router.get("", summary="获取模板列表")
async def get_templates(
template_type: Optional[str] = Query(None, description="模板类型"),
is_active: Optional[bool] = Query(None, description="是否启用"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取模板列表"""
templates, total = await TemplateService.get_list(
db, template_type, is_active, page, page_size
)
return {
"code": 200,
"message": "success",
"data": templates,
"total": total,
"page": page,
"page_size": page_size
}
@router.get("/types", summary="获取模板类型列表")
async def get_template_types(
current_user: User = Depends(get_current_active_user)
):
"""获取模板类型列表"""
types = [
{"value": "general", "label": "通用模板"},
{"value": "surgical", "label": "手术临床科室"},
{"value": "nonsurgical_ward", "label": "非手术有病房科室"},
{"value": "nonsurgical_noward", "label": "非手术无病房科室"},
{"value": "medical_tech", "label": "医技科室"},
{"value": "nursing", "label": "护理单元"},
{"value": "admin", "label": "行政科室"},
{"value": "logistics", "label": "后勤科室"}
]
return {"code": 200, "message": "success", "data": types}
@router.get("/dimensions", summary="获取BSC维度列表")
async def get_dimensions(
current_user: User = Depends(get_current_active_user)
):
"""获取BSC维度列表"""
dimensions = [
{"value": "financial", "label": "财务管理", "weight_range": "30%-40%"},
{"value": "customer", "label": "顾客服务", "weight_range": "25%-35%"},
{"value": "internal_process", "label": "内部流程", "weight_range": "20%-30%"},
{"value": "learning_growth", "label": "学习与成长", "weight_range": "5%-15%"}
]
return {"code": 200, "message": "success", "data": dimensions}
@router.get("/{template_id}", summary="获取模板详情")
async def get_template(
template_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取模板详情"""
template = await TemplateService.get_by_id(db, template_id)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
# 构建响应数据
indicators = []
for ti in template.indicators:
ind_dict = {
"id": ti.id,
"template_id": ti.template_id,
"indicator_id": ti.indicator_id,
"indicator_name": ti.indicator.name if ti.indicator else None,
"indicator_code": ti.indicator.code if ti.indicator else None,
"indicator_type": ti.indicator.indicator_type.value if ti.indicator else None,
"bs_dimension": ti.indicator.bs_dimension.value if ti.indicator else None,
"category": ti.category,
"target_value": float(ti.target_value) if ti.target_value else None,
"target_unit": ti.target_unit,
"weight": float(ti.weight),
"scoring_method": ti.scoring_method,
"scoring_params": ti.scoring_params,
"sort_order": ti.sort_order,
"remark": ti.remark,
"created_at": ti.created_at,
"updated_at": ti.updated_at
}
indicators.append(ind_dict)
response_data = {
"id": template.id,
"template_name": template.template_name,
"template_code": template.template_code,
"template_type": template.template_type.value,
"description": template.description,
"dimension_weights": template.dimension_weights,
"assessment_cycle": template.assessment_cycle,
"is_active": template.is_active,
"created_at": template.created_at,
"updated_at": template.updated_at,
"indicators": indicators
}
return {"code": 200, "message": "success", "data": response_data}
@router.post("", summary="创建模板")
async def create_template(
template_data: IndicatorTemplateCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""创建模板(需要管理员或经理权限)"""
# 检查编码是否已存在
existing = await TemplateService.get_by_code(db, template_data.template_code)
if existing:
raise HTTPException(status_code=400, detail="模板编码已存在")
template = await TemplateService.create(db, template_data)
return {"code": 200, "message": "创建成功", "data": {"id": template.id}}
@router.put("/{template_id}", summary="更新模板")
async def update_template(
template_id: int,
template_data: IndicatorTemplateUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新模板(需要管理员或经理权限)"""
template = await TemplateService.update(db, template_id, template_data)
if not template:
raise HTTPException(status_code=404, detail="模板不存在")
return {"code": 200, "message": "更新成功"}
@router.delete("/{template_id}", summary="删除模板")
async def delete_template(
template_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""删除模板(需要管理员或经理权限)"""
success = await TemplateService.delete(db, template_id)
if not success:
raise HTTPException(status_code=404, detail="模板不存在")
return {"code": 200, "message": "删除成功"}
# ==================== 模板指标管理 ====================
@router.get("/{template_id}/indicators", summary="获取模板指标列表")
async def get_template_indicators(
template_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""获取模板指标列表"""
indicators = await TemplateService.get_template_indicators(db, template_id)
result = []
for ti in indicators:
ind_dict = {
"id": ti.id,
"template_id": ti.template_id,
"indicator_id": ti.indicator_id,
"indicator_name": ti.indicator.name if ti.indicator else None,
"indicator_code": ti.indicator.code if ti.indicator else None,
"indicator_type": ti.indicator.indicator_type.value if ti.indicator else None,
"bs_dimension": ti.indicator.bs_dimension.value if ti.indicator else None,
"category": ti.category,
"target_value": float(ti.target_value) if ti.target_value else None,
"target_unit": ti.target_unit,
"weight": float(ti.weight),
"scoring_method": ti.scoring_method,
"scoring_params": ti.scoring_params,
"sort_order": ti.sort_order,
"remark": ti.remark,
"created_at": ti.created_at,
"updated_at": ti.updated_at
}
result.append(ind_dict)
return {"code": 200, "message": "success", "data": result}
@router.post("/{template_id}/indicators", summary="添加模板指标")
async def add_template_indicator(
template_id: int,
indicator_data: TemplateIndicatorCreate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""添加模板指标"""
ti = await TemplateService.add_indicator(db, template_id, indicator_data)
if not ti:
raise HTTPException(status_code=400, detail="添加失败,模板不存在或指标已存在")
return {"code": 200, "message": "添加成功", "data": {"id": ti.id}}
@router.put("/{template_id}/indicators/{indicator_id}", summary="更新模板指标")
async def update_template_indicator(
template_id: int,
indicator_id: int,
indicator_data: TemplateIndicatorUpdate,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""更新模板指标"""
ti = await TemplateService.update_indicator(db, template_id, indicator_id, indicator_data)
if not ti:
raise HTTPException(status_code=404, detail="模板指标不存在")
return {"code": 200, "message": "更新成功"}
@router.delete("/{template_id}/indicators/{indicator_id}", summary="移除模板指标")
async def remove_template_indicator(
template_id: int,
indicator_id: int,
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""移除模板指标"""
success = await TemplateService.remove_indicator(db, template_id, indicator_id)
if not success:
raise HTTPException(status_code=404, detail="模板指标不存在")
return {"code": 200, "message": "移除成功"}
@router.post("/{template_id}/indicators/batch", summary="批量添加模板指标")
async def batch_add_template_indicators(
template_id: int,
indicators_data: List[TemplateIndicatorCreate],
db: AsyncSession = Depends(get_db),
current_user: Annotated[User, Depends(get_current_manager_user)] = None
):
"""批量添加模板指标"""
added_count = 0
for idx, ind_data in enumerate(indicators_data):
ind_data.sort_order = ind_data.sort_order or idx
ti = await TemplateService.add_indicator(db, template_id, ind_data)
if ti:
added_count += 1
return {
"code": 200,
"message": f"成功添加 {added_count} 个指标",
"data": {"added_count": added_count}
}