feat: 重构质量管理和设备巡检模块

质量管理: 由平铺记录改为任务制工作流(qc_task/qc_task_item/qc_defect三表)
设备巡检: 由点位+记录改为巡检模板制(eqp_checklist/item/record/detail四表)
前端: Quality.vue 支持任务列表+检验项详情+缺陷记录双Tab
前端: Inspection.vue 支持模板管理+项目维护+巡检记录+明细查看

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 16:29:02 +08:00
parent 6ae24cb14d
commit b461f0d2f8
11 changed files with 1645 additions and 749 deletions

View File

@@ -1,13 +1,16 @@
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from typing import Optional
from datetime import datetime
from app.database import get_db
from app.models.inspection import InspectionLocation, InspectionRecord
from app.models.inspection import EqpChecklist, EqpChecklistItem, EqpInspectionRecord, EqpInspectionDetail
from app.schemas.inspection import (
InspectionLocationCreate, InspectionLocationOut,
InspectionRecordCreate, InspectionRecordOut,
EqpChecklistCreate, EqpChecklistUpdate, EqpChecklistOut,
EqpChecklistItemCreate, EqpChecklistItemOut,
EqpInspectionRecordCreate, EqpInspectionRecordOut,
EqpInspectionDetailOut,
)
from app.schemas.common import Response, PageResponse
from app.services.auth_service import get_current_user
@@ -15,61 +18,117 @@ from app.services.auth_service import get_current_user
router = APIRouter()
@router.get("/locations", response_model=Response[list[InspectionLocationOut]])
async def list_locations(
# ─── 巡检模板 ────────────────────────────────────────────────────────────────
@router.get("/checklists", response_model=Response[list[EqpChecklistOut]])
async def list_checklists(
equipment_code: Optional[str] = None,
db: AsyncSession = Depends(get_db),
_ = Depends(get_current_user),
_=Depends(get_current_user),
):
result = await db.execute(
select(InspectionLocation).order_by(InspectionLocation.sort_order, InspectionLocation.id)
)
items = [InspectionLocationOut.model_validate(r) for r in result.scalars()]
return Response.ok(items)
query = select(EqpChecklist).order_by(EqpChecklist.id)
if equipment_code:
query = query.where(EqpChecklist.equipment_code == equipment_code)
r = await db.execute(query)
return Response.ok([EqpChecklistOut.model_validate(c) for c in r.scalars()])
@router.post("/locations", response_model=Response[InspectionLocationOut])
async def create_location(
body: InspectionLocationCreate,
db: AsyncSession = Depends(get_db),
_ = Depends(get_current_user),
):
loc = InspectionLocation(**body.model_dump())
db.add(loc)
@router.post("/checklists", response_model=Response[EqpChecklistOut])
async def create_checklist(body: EqpChecklistCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
items_data = body.items or []
data = body.model_dump(exclude={"items"})
checklist = EqpChecklist(**data)
db.add(checklist)
await db.flush()
return Response.ok(InspectionLocationOut.model_validate(loc))
for item in items_data:
ci = EqpChecklistItem(checklist_id=checklist.id, **item.model_dump())
db.add(ci)
await db.flush()
return Response.ok(EqpChecklistOut.model_validate(checklist))
@router.get("/records", response_model=Response[PageResponse[InspectionRecordOut]])
@router.put("/checklists/{checklist_id}", response_model=Response[EqpChecklistOut])
async def update_checklist(checklist_id: int, body: EqpChecklistUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(EqpChecklist).where(EqpChecklist.id == checklist_id))
checklist = r.scalar_one_or_none()
if not checklist:
raise HTTPException(status_code=404, detail="模板不存在")
for k, v in body.model_dump(exclude_none=True).items():
setattr(checklist, k, v)
await db.flush()
return Response.ok(EqpChecklistOut.model_validate(checklist))
@router.get("/checklists/{checklist_id}/items", response_model=Response[list[EqpChecklistItemOut]])
async def list_checklist_items(checklist_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(
select(EqpChecklistItem).where(EqpChecklistItem.checklist_id == checklist_id)
.order_by(EqpChecklistItem.sort_order, EqpChecklistItem.id)
)
return Response.ok([EqpChecklistItemOut.model_validate(i) for i in r.scalars()])
@router.post("/checklist-items", response_model=Response[EqpChecklistItemOut])
async def create_checklist_item(
body: EqpChecklistItemCreate,
checklist_id: int,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
):
item = EqpChecklistItem(checklist_id=checklist_id, **body.model_dump())
db.add(item)
await db.flush()
return Response.ok(EqpChecklistItemOut.model_validate(item))
# ─── 巡检记录 ────────────────────────────────────────────────────────────────
@router.get("/records", response_model=Response[PageResponse[EqpInspectionRecordOut]])
async def list_records(
page: int = 1,
page_size: int = 30,
location_id: Optional[int] = None,
page_size: int = 20,
checklist_id: Optional[int] = None,
status: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
db: AsyncSession = Depends(get_db),
_ = Depends(get_current_user),
_=Depends(get_current_user),
):
query = select(InspectionRecord).order_by(desc(InspectionRecord.created_at))
if location_id:
query = query.where(InspectionRecord.location_id == location_id)
query = select(EqpInspectionRecord).order_by(desc(EqpInspectionRecord.created_at))
if checklist_id:
query = query.where(EqpInspectionRecord.checklist_id == checklist_id)
if status:
query = query.where(EqpInspectionRecord.status == status)
if start_date:
query = query.where(EqpInspectionRecord.inspect_time >= datetime.fromisoformat(start_date))
if end_date:
query = query.where(EqpInspectionRecord.inspect_time <= datetime.fromisoformat(end_date + "T23:59:59"))
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
items = [InspectionRecordOut.model_validate(r) for r in result.scalars()]
rows = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
items = [EqpInspectionRecordOut.model_validate(r) for r in rows.scalars()]
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
@router.post("/records", response_model=Response[InspectionRecordOut])
async def create_record(
body: InspectionRecordCreate,
db: AsyncSession = Depends(get_db),
_ = Depends(get_current_user),
):
loc_result = await db.execute(
select(InspectionLocation).where(InspectionLocation.id == body.location_id)
)
loc = loc_result.scalar_one_or_none()
record = InspectionRecord(
**body.model_dump(),
location_name=loc.name if loc else None,
)
@router.post("/records", response_model=Response[EqpInspectionRecordOut])
async def create_record(body: EqpInspectionRecordCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
details_data = body.details or []
# get checklist name
cl_r = await db.execute(select(EqpChecklist).where(EqpChecklist.id == body.checklist_id))
cl = cl_r.scalar_one_or_none()
data = body.model_dump(exclude={"details"})
record = EqpInspectionRecord(**data, checklist_name=cl.name if cl else None)
db.add(record)
await db.flush()
return Response.ok(InspectionRecordOut.model_validate(record))
for d in details_data:
detail = EqpInspectionDetail(record_id=record.id, **d.model_dump())
db.add(detail)
await db.flush()
return Response.ok(EqpInspectionRecordOut.model_validate(record))
@router.get("/records/{record_id}/details", response_model=Response[list[EqpInspectionDetailOut]])
async def get_record_details(record_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(
select(EqpInspectionDetail).where(EqpInspectionDetail.record_id == record_id).order_by(EqpInspectionDetail.id)
)
return Response.ok([EqpInspectionDetailOut.model_validate(d) for d in r.scalars()])

View File

@@ -5,128 +5,177 @@ from typing import Optional
from datetime import datetime
from app.database import get_db
from app.models.quality import QualityRecord
from app.schemas.quality import QualityRecordCreate, QualityRecordUpdate, QualityRecordOut
from app.models.quality import QcTask, QcTaskItem, QcDefect
from app.schemas.quality import (
QcTaskCreate, QcTaskUpdate, QcTaskOut,
QcTaskItemCreate, QcTaskItemUpdate, QcTaskItemOut,
QcDefectCreate, QcDefectUpdate, QcDefectOut,
)
from app.schemas.common import Response, PageResponse
from app.services.auth_service import get_current_user
from app.services.prediction import QualityPredictionModel
router = APIRouter()
@router.get("/summary", response_model=Response[dict])
async def quality_summary(
# ─── 检验任务 ───────────────────────────────────────────────────────────────
@router.get("/tasks", response_model=Response[PageResponse[QcTaskOut]])
async def list_tasks(
page: int = 1,
page_size: int = 20,
task_code: Optional[str] = None,
coil_no: Optional[str] = None,
status: Optional[int] = None,
result: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
):
"""合格率、平均评分、班次分组统计"""
total_q = await db.execute(select(func.count()).select_from(QualityRecord))
passed_q = await db.execute(
select(func.count()).select_from(QualityRecord).where(QualityRecord.is_passed == True)
)
avg_pi_q = await db.execute(select(func.avg(QualityRecord.pi_score)).select_from(QualityRecord))
avg_suf_q = await db.execute(select(func.avg(QualityRecord.surface_score)).select_from(QualityRecord))
# Grade distribution
grade_counts: dict = {}
for grade in ["A1", "A2", "B1", "B2", "C"]:
cnt_q = await db.execute(
select(func.count()).select_from(QualityRecord).where(QualityRecord.overall_grade == grade)
)
grade_counts[grade] = cnt_q.scalar() or 0
total = total_q.scalar() or 0
passed = passed_q.scalar() or 0
pass_rate = round(passed / total * 100, 1) if total > 0 else 0.0
return Response.ok({
"total": total,
"passed": passed,
"pass_rate": pass_rate,
"avg_pi_score": round(avg_pi_q.scalar() or 0, 1),
"avg_surface_score": round(avg_suf_q.scalar() or 0, 1),
"grade_distribution": grade_counts,
})
@router.get("/", response_model=Response[PageResponse[QualityRecordOut]])
async def list_quality(
page: int = 1,
page_size: int = 20,
coil_no: Optional[str] = None,
overall_grade: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
):
query = select(QualityRecord).order_by(desc(QualityRecord.created_at))
query = select(QcTask).where(QcTask.del_flag == 0).order_by(desc(QcTask.created_at))
if task_code:
query = query.where(QcTask.task_code.ilike(f"%{task_code}%"))
if coil_no:
query = query.where(QualityRecord.coil_no.ilike(f"%{coil_no}%"))
if overall_grade:
query = query.where(QualityRecord.overall_grade == overall_grade)
query = query.where(QcTask.coil_no.ilike(f"%{coil_no}%"))
if status is not None:
query = query.where(QcTask.status == status)
if result:
query = query.where(QcTask.result == result)
if start_date:
query = query.where(QualityRecord.created_at >= start_date)
query = query.where(QcTask.created_at >= datetime.fromisoformat(start_date))
if end_date:
query = query.where(QualityRecord.created_at <= end_date)
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
items = [QualityRecordOut.model_validate(r) for r in result.scalars()]
query = query.where(QcTask.created_at <= datetime.fromisoformat(end_date + "T23:59:59"))
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
result_rows = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
items = [QcTaskOut.model_validate(r) for r in result_rows.scalars()]
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
@router.post("/", response_model=Response[QualityRecordOut])
async def create_quality(
body: QualityRecordCreate,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
):
"""
创建质量记录。若未提供 pi_score / surface_score / overall_grade
则尝试用 QualityPredictionModel 自动填充需要thickness_actual和相关参数
"""
data = body.model_dump()
if (
data.get("pi_score") is None
and data.get("thickness_actual") is not None
):
# Use default avg values for auto-prediction when detailed params are absent
avg_speed = 100.0 # m/min default
acid_conc_avg = 160.0 # g/L default
acid_temp_avg = 75.0 # °C default
try:
pred = QualityPredictionModel(
thickness=data["thickness_actual"],
avg_speed=avg_speed,
acid_conc_avg=acid_conc_avg,
acid_temp_avg=acid_temp_avg,
).calculate()
data["pi_score"] = pred["pi_score"]
data["surface_score"] = pred["surface_score"]
data["overall_grade"] = pred["overall_grade"]
except Exception:
pass # best-effort, do not block creation
record = QualityRecord(**data)
db.add(record)
@router.post("/tasks", response_model=Response[QcTaskOut])
async def create_task(body: QcTaskCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
task = QcTask(**body.model_dump())
db.add(task)
await db.flush()
return Response.ok(QualityRecordOut.model_validate(record))
return Response.ok(QcTaskOut.model_validate(task))
@router.put("/{quality_id}", response_model=Response[QualityRecordOut])
async def update_quality(
quality_id: int,
body: QualityRecordUpdate,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
):
result = await db.execute(select(QualityRecord).where(QualityRecord.id == quality_id))
record = result.scalar_one_or_none()
if not record:
raise HTTPException(status_code=404, detail="质量记录不存在")
@router.get("/tasks/{task_id}", response_model=Response[QcTaskOut])
async def get_task(task_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(QcTask).where(QcTask.id == task_id, QcTask.del_flag == 0))
task = r.scalar_one_or_none()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
return Response.ok(QcTaskOut.model_validate(task))
@router.put("/tasks/{task_id}", response_model=Response[QcTaskOut])
async def update_task(task_id: int, body: QcTaskUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(QcTask).where(QcTask.id == task_id, QcTask.del_flag == 0))
task = r.scalar_one_or_none()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
for k, v in body.model_dump(exclude_none=True).items():
setattr(record, k, v)
setattr(task, k, v)
await db.flush()
return Response.ok(QualityRecordOut.model_validate(record))
return Response.ok(QcTaskOut.model_validate(task))
@router.delete("/tasks/{task_id}", response_model=Response[dict])
async def delete_task(task_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(QcTask).where(QcTask.id == task_id))
task = r.scalar_one_or_none()
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
task.del_flag = 1
await db.flush()
return Response.ok({})
# ─── 检验项目 ────────────────────────────────────────────────────────────────
@router.get("/tasks/{task_id}/items", response_model=Response[list[QcTaskItemOut]])
async def list_task_items(task_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(QcTaskItem).where(QcTaskItem.task_id == task_id).order_by(QcTaskItem.id))
return Response.ok([QcTaskItemOut.model_validate(i) for i in r.scalars()])
@router.post("/task-items", response_model=Response[QcTaskItemOut])
async def create_task_item(body: QcTaskItemCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
item = QcTaskItem(**body.model_dump())
db.add(item)
await db.flush()
return Response.ok(QcTaskItemOut.model_validate(item))
@router.put("/task-items/{item_id}", response_model=Response[QcTaskItemOut])
async def update_task_item(item_id: int, body: QcTaskItemUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(QcTaskItem).where(QcTaskItem.id == item_id))
item = r.scalar_one_or_none()
if not item:
raise HTTPException(status_code=404, detail="检验项不存在")
for k, v in body.model_dump(exclude_none=True).items():
setattr(item, k, v)
await db.flush()
return Response.ok(QcTaskItemOut.model_validate(item))
# ─── 缺陷记录 ────────────────────────────────────────────────────────────────
@router.get("/defects", response_model=Response[PageResponse[QcDefectOut]])
async def list_defects(
page: int = 1,
page_size: int = 20,
coil_no: Optional[str] = None,
defect_type: Optional[str] = None,
degree: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
):
query = select(QcDefect).where(QcDefect.del_flag == 0).order_by(desc(QcDefect.created_at))
if coil_no:
query = query.where(QcDefect.coil_no.ilike(f"%{coil_no}%"))
if defect_type:
query = query.where(QcDefect.defect_type.ilike(f"%{defect_type}%"))
if degree:
query = query.where(QcDefect.degree == degree)
if start_date:
query = query.where(QcDefect.created_at >= datetime.fromisoformat(start_date))
if end_date:
query = query.where(QcDefect.created_at <= datetime.fromisoformat(end_date + "T23:59:59"))
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar()
rows = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
items = [QcDefectOut.model_validate(r) for r in rows.scalars()]
return Response.ok(PageResponse(total=total, page=page, page_size=page_size, items=items))
@router.post("/defects", response_model=Response[QcDefectOut])
async def create_defect(body: QcDefectCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
defect = QcDefect(**body.model_dump())
db.add(defect)
await db.flush()
return Response.ok(QcDefectOut.model_validate(defect))
@router.put("/defects/{defect_id}", response_model=Response[QcDefectOut])
async def update_defect(defect_id: int, body: QcDefectUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(QcDefect).where(QcDefect.id == defect_id, QcDefect.del_flag == 0))
defect = r.scalar_one_or_none()
if not defect:
raise HTTPException(status_code=404, detail="缺陷记录不存在")
for k, v in body.model_dump(exclude_none=True).items():
setattr(defect, k, v)
await db.flush()
return Response.ok(QcDefectOut.model_validate(defect))
@router.delete("/defects/{defect_id}", response_model=Response[dict])
async def delete_defect(defect_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)):
r = await db.execute(select(QcDefect).where(QcDefect.id == defect_id))
defect = r.scalar_one_or_none()
if not defect:
raise HTTPException(status_code=404, detail="缺陷记录不存在")
defect.del_flag = 1
await db.flush()
return Response.ok({})