JhHardwareWRS_BackPoint/app/routers/attendance_points.py
2026-06-24 15:19:14 +08:00

169 lines
6.1 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from app.database import get_db
from app.deps import current_user, require_roles
from app.models import (
AttendancePoint,
DeviceQRCode,
Equipment,
NoticePoint,
PersonAttendancePoint,
Personnel,
Product,
ProductionReport,
ProductionReportItem,
ReconciliationLedgerEntry,
Role,
WorkSession,
WorkSessionDevice,
)
from app.schemas import AttendancePointCreate, AttendancePointOut, PageResponse
from app.services.attendance_points import accessible_points, clean_point_name, ensure_default_attendance_point
from app.services.common import paginate
from app.services.misc_work import ensure_misc_work_product
from app.services.serializers import attendance_point_out
from app.services.work_schedule import normalize_schedule_payload
router = APIRouter(prefix="/api/attendance-points", tags=["attendance-points"])
def _validate_payload(payload: AttendancePointCreate) -> str:
name = clean_point_name(payload.name)
if not name:
raise HTTPException(status_code=400, detail="请输入考勤点名称")
if (payload.latitude is None) != (payload.longitude is None):
raise HTTPException(status_code=400, detail="经纬度必须同时填写")
if payload.latitude is not None and not (-90 <= payload.latitude <= 90):
raise HTTPException(status_code=400, detail="纬度范围为-90到90")
if payload.longitude is not None and not (-180 <= payload.longitude <= 180):
raise HTTPException(status_code=400, detail="经度范围为-180到180")
if int(payload.radius_meters or 0) <= 0:
raise HTTPException(status_code=400, detail="考勤范围半径必须大于0米")
try:
normalize_schedule_payload(payload)
except ValueError as exc:
raise HTTPException(status_code=400, detail=f"{name} {exc}") from exc
return name
def _rename_business_point(db: Session, old_name: str, new_name: str) -> None:
if old_name == new_name:
return
for model in (
PersonAttendancePoint,
Product,
Equipment,
WorkSession,
WorkSessionDevice,
ProductionReport,
ProductionReportItem,
DeviceQRCode,
NoticePoint,
ReconciliationLedgerEntry,
):
db.query(model).filter(model.attendance_point_name == old_name).update(
{model.attendance_point_name: new_name},
synchronize_session=False,
)
@router.get("/accessible", response_model=list[AttendancePointOut])
def list_accessible_attendance_points(
user: Personnel = Depends(current_user),
db: Session = Depends(get_db),
) -> list[AttendancePointOut]:
return [attendance_point_out(point) for point in accessible_points(db, user)]
@router.get("/public", response_model=list[AttendancePointOut])
def list_public_attendance_points(
db: Session = Depends(get_db),
) -> list[AttendancePointOut]:
points = db.scalars(
select(AttendancePoint)
.where(AttendancePoint.is_active.is_(True))
.order_by(AttendancePoint.name.asc())
).all()
rows = []
for point in points:
item = attendance_point_out(point)
item.remark = None
item.created_at = None
item.updated_at = None
rows.append(item)
return rows
@router.get("", response_model=PageResponse)
def list_attendance_points(
keyword: str = "",
page: int = Query(1, ge=1),
page_size: int = Query(100, ge=1, le=500),
_: Personnel = Depends(require_roles(Role.manager)),
db: Session = Depends(get_db),
) -> PageResponse:
default_point = ensure_default_attendance_point(db)
ensure_misc_work_product(db, default_point.name)
db.commit()
query = select(AttendancePoint).order_by(AttendancePoint.updated_at.desc(), AttendancePoint.name.asc())
if keyword:
like = f"%{keyword}%"
query = query.where(or_(AttendancePoint.name.like(like), AttendancePoint.remark.like(like)))
result = paginate(db, query, page, page_size)
return PageResponse(**{**result, "rows": [attendance_point_out(row) for row in result["rows"]]})
@router.post("", response_model=AttendancePointOut)
def save_attendance_point(
payload: AttendancePointCreate,
_: Personnel = Depends(require_roles(Role.manager)),
db: Session = Depends(get_db),
) -> AttendancePointOut:
name = _validate_payload(payload)
original_name = clean_point_name(payload.original_name) or name
point = db.get(AttendancePoint, original_name)
if point is not None and original_name != name:
if db.get(AttendancePoint, name) is not None:
raise HTTPException(status_code=409, detail="该考勤点名称已存在")
next_point = AttendancePoint(name=name)
db.add(next_point)
db.flush()
_rename_business_point(db, original_name, name)
db.delete(point)
point = next_point
elif point is None:
point = db.get(AttendancePoint, name)
if point is None:
point = AttendancePoint(name=name)
db.add(point)
db.flush()
point.latitude = payload.latitude
point.longitude = payload.longitude
point.radius_meters = int(payload.radius_meters or 500)
for field, value in normalize_schedule_payload(payload).items():
setattr(point, field, value)
point.remark = payload.remark
point.is_active = bool(payload.is_active)
ensure_misc_work_product(db, name)
db.commit()
db.refresh(point)
return attendance_point_out(point)
@router.delete("/{name}", status_code=204)
def delete_attendance_point(
name: str,
_: Personnel = Depends(require_roles(Role.manager)),
db: Session = Depends(get_db),
) -> None:
point_name = clean_point_name(name)
point = db.get(AttendancePoint, point_name)
if point is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="考勤点不存在")
if point_name == ensure_default_attendance_point(db).name:
raise HTTPException(status_code=400, detail="默认考勤点不能删除")
point.is_active = False
db.commit()