Files
shanxia2.0/backend/app/api/pilot_application.py
T
2026-05-28 13:05:46 +08:00

805 lines
28 KiB
Python

"""中试服务申请单管理 API(申请单、患者信息、输注方案、输注计划、出库联动)"""
from typing import Optional
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.core.response import error_response, success_response
from app.crud import pilot_application
from app.models.models import SysUser
router = APIRouter(prefix="/web/admin", tags=["中试服务申请单管理"])
def _get_tenant_no(db: Session, user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失,无法确定租户")
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
if not sys_user or not sys_user.tenant_no:
raise PermissionError("用户租户信息缺失")
return sys_user.tenant_no
# ---------------------------------------------------------------------------
# Pydantic Schemas
# ---------------------------------------------------------------------------
class IdBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str
class PlanIdBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
plan_id: str = Field(..., alias="infusionPlanId")
class ApplicationCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
channel_service_order_id: Optional[str] = Field(None, alias="channelServiceOrderId")
remark: Optional[str] = None
biz_type: Optional[str] = Field(None, alias="bizType")
class ApplicationUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str
remark: Optional[str] = None
class PatientSaveBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
application_id: str = Field(..., alias="pilotApplicationId")
customer_id: Optional[str] = Field(None, alias="customerId")
patient_name: str = Field(..., alias="patientName")
sex: Optional[str] = None
age: Optional[int] = None
id_card_no: Optional[str] = Field(None, alias="idCardNo")
phone_no: Optional[str] = Field(None, alias="phoneNo")
emergency_contact: Optional[str] = Field(None, alias="emergencyContact")
emergency_phone: Optional[str] = Field(None, alias="emergencyPhone")
clinical_diagnosis: Optional[str] = Field(None, alias="clinicalDiagnosis")
diagnosis_attachments: Optional[str] = Field(None, alias="diagnosisAttachments")
need_checkup: int = Field(1, alias="needCheckup")
class InfusionPlanCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
pilot_application_id: str = Field(..., alias="pilotApplicationId")
cell_type: str = Field(..., alias="cellType")
spec_text: Optional[str] = Field(None, alias="specText")
total_course_count: int = Field(..., alias="totalCourseCount")
course_interval: Optional[str] = Field(None, alias="courseInterval")
infusion_route: Optional[str] = Field(None, alias="infusionRoute")
infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
class InfusionPlanUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str
cell_type: Optional[str] = Field(None, alias="cellType")
spec_text: Optional[str] = Field(None, alias="specText")
total_course_count: Optional[int] = Field(None, alias="totalCourseCount")
course_interval: Optional[str] = Field(None, alias="courseInterval")
infusion_route: Optional[str] = Field(None, alias="infusionRoute")
infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
class InfusionScheduleUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str
infusion_time: Optional[str] = Field(None, alias="infusionTime")
remark: Optional[str] = None
attachments: Optional[str] = None
class ScheduleCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
plan_id: str = Field(..., alias="infusionPlanId")
infusion_time: Optional[str] = Field(None, alias="infusionTime")
remark: Optional[str] = None
amount: Optional[float] = None
# --- 复合保存 Schema(参考采集协议单一次性保存模式)---
class PatientNestedBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
customer_id: Optional[str] = Field(None, alias="customerId")
patient_name: Optional[str] = Field(None, alias="patientName")
sex: Optional[str] = None
age: Optional[int] = None
id_card_no: Optional[str] = Field(None, alias="idCardNo")
phone_no: Optional[str] = Field(None, alias="phoneNo")
emergency_contact: Optional[str] = Field(None, alias="emergencyContact")
emergency_phone: Optional[str] = Field(None, alias="emergencyPhone")
clinical_diagnosis: Optional[str] = Field(None, alias="clinicalDiagnosis")
diagnosis_attachments: Optional[str] = Field(None, alias="diagnosisAttachments")
need_checkup: int = Field(1, alias="needCheckup")
class ScheduleProductNestedBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: Optional[str] = None
price_id: Optional[str] = Field(None, alias="priceId")
project_code: Optional[str] = Field(None, alias="projectCode")
project_name: Optional[str] = Field(None, alias="projectName")
cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
spec_text: Optional[str] = Field(None, alias="specText")
agreement_price: Optional[float] = Field(None, alias="agreementPrice")
actual_price: Optional[float] = Field(None, alias="actualPrice")
quantity: Optional[int] = 1
status: Optional[str] = None
remark: Optional[str] = None
class InfusionScheduleNestedBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: Optional[str] = None
sort_no: Optional[int] = Field(None, alias="sortNo")
infusion_time: Optional[str] = Field(None, alias="infusionTime")
infusion_location: Optional[str] = Field(None, alias="infusionLocation")
infusion_method: Optional[str] = Field(None, alias="infusionMethod")
amount: Optional[float] = None
cell_products: Optional[list[ScheduleProductNestedBody]] = Field(None, alias="cellProducts")
class InfusionPlanNestedBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: Optional[str] = None
cell_type: Optional[str] = Field(None, alias="cellType")
spec_text: Optional[str] = Field(None, alias="specText")
total_course_count: Optional[int] = Field(None, alias="totalCourseCount")
course_interval: Optional[str] = Field(None, alias="courseInterval")
infusion_route: Optional[str] = Field(None, alias="infusionRoute")
infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
channel_agreement_id: Optional[str] = Field(None, alias="channelAgreementId")
class ApplicationSaveBody(BaseModel):
"""一揽子保存:申请单 + 患者信息 + 输注方案 + 输注计划(含细胞产品)"""
model_config = ConfigDict(populate_by_name=True)
id: Optional[str] = None
application_name: Optional[str] = Field(None, alias="applicationName")
channel_service_order_id: Optional[str] = Field(None, alias="channelServiceOrderId")
channel_id: Optional[str] = Field(None, alias="channelId")
biz_type: Optional[str] = Field(None, alias="bizType")
remark: Optional[str] = None
patient: Optional[PatientNestedBody] = None
infusion_plan: Optional[InfusionPlanNestedBody] = Field(None, alias="infusionPlan")
infusion_schedules: Optional[list[InfusionScheduleNestedBody]] = Field(
None, alias="infusionSchedules"
)
infusion_plans: Optional[list[InfusionPlanNestedBody]] = Field(None, alias="infusionPlans")
# ---------------------------------------------------------------------------
# 1. 申请单 CRUD + 状态变更
# ---------------------------------------------------------------------------
@router.get("/pilot-application/list")
async def api_list_applications(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = Query(None, alias="keyword"),
channelId: Optional[str] = Query(None, alias="channelId"),
applicationStatus: Optional[str] = Query(None, alias="applicationStatus"),
channelServiceOrderId: Optional[str] = Query(None, alias="channelServiceOrderId"),
bizType: Optional[str] = Query(None, alias="bizType"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.list_applications(
db,
tenant_no=tenant_no,
page=page,
page_size=pageSize,
channel_id=channelId,
application_status=applicationStatus,
channel_service_order_id=channelServiceOrderId,
keyword=keyword,
biz_type=bizType,
)
return success_response(result)
@router.get("/pilot-application/detail")
async def api_get_application(
id: str = Query(...),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.get_application(db, app_id=id, tenant_no=tenant_no)
if result is None:
return error_response(404, "申请单不存在")
return success_response(result)
@router.post("/pilot-application/create")
async def api_create_application(
body: ApplicationCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {
"channelServiceOrderId": body.channel_service_order_id or "",
"remark": body.remark,
"bizType": body.biz_type or "pilot_service",
}
result = pilot_application.create_application(
db,
data=data,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/pilot-application/update")
async def api_update_application(
body: ApplicationUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {}
if body.remark is not None:
data["remark"] = body.remark
result = pilot_application.update_application(
db, app_id=body.id, data=data, tenant_no=tenant_no
)
return success_response(result, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-application/save")
async def api_save_application(
body: ApplicationSaveBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = user.get("user_id")
dept_id = user.get("region_id", "")
is_edit = bool(body.id)
biz_type = body.biz_type or "pilot_service"
app_data: dict = {
"channelServiceOrderId": body.channel_service_order_id or "",
"channelId": body.channel_id or "",
"bizType": biz_type,
"remark": body.remark,
}
if body.application_name is not None:
app_data["applicationName"] = body.application_name
if not is_edit and biz_type != "clinical_application" and not app_data["channelServiceOrderId"]:
return error_response(400, "请选择渠道服务订单")
if not is_edit and biz_type == "clinical_application" and not app_data["channelId"]:
return error_response(400, "请选择渠道")
if is_edit:
app_result = pilot_application.update_application(
db,
app_id=body.id,
data=app_data,
tenant_no=tenant_no,
)
app_id = body.id
else:
app_result = pilot_application.create_application(
db,
data=app_data,
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
app_id = app_result.get("id")
if not app_id:
return error_response(500, "创建申请单失败,未获取到ID")
if body.patient:
pilot_application.save_patient(
db,
app_id=app_id,
data=body.patient.model_dump(by_alias=True),
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
if body.infusion_plan is not None or body.infusion_schedules:
plan_dict = (
body.infusion_plan.model_dump(by_alias=True) if body.infusion_plan else {}
)
schedules = [
s.model_dump(by_alias=True) for s in (body.infusion_schedules or [])
]
if is_edit:
pilot_application.upsert_application_infusion(
db,
app_id=app_id,
plan_data=plan_dict,
schedules_data=schedules,
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
else:
pilot_application.save_application_infusion(
db,
app_id=app_id,
plan_data=plan_dict,
schedules_data=schedules,
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
replace_existing=False,
)
elif body.infusion_plans:
for plan_data in body.infusion_plans:
plan_result = pilot_application.create_infusion_plan(
db,
app_id=app_id,
data=plan_data.model_dump(by_alias=True),
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
plan_id = plan_result.get("id")
if plan_id and (plan_data.total_course_count or 0) > 0:
pilot_application.auto_generate_schedules(
db,
plan_id=plan_id,
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
app_result = pilot_application.get_application(db, app_id=app_id, tenant_no=tenant_no)
return success_response(app_result, message="保存成功")
except ValueError as e:
return error_response(400, str(e))
except Exception as e:
return error_response(500, str(e))
@router.delete("/pilot-application/delete")
async def api_delete_application(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
pilot_application.soft_delete_application(db, app_id=body.id, tenant_no=tenant_no)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-application/start")
async def api_start_application(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.start_application(
db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
)
return success_response(result, message="操作成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-application/complete")
async def api_complete_application(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.complete_application(
db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
)
return success_response(result, message="操作成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-application/void")
async def api_void_application(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.void_application(
db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
)
return success_response(result, message="操作成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 2. 患者信息
# ---------------------------------------------------------------------------
@router.get("/pilot-patient/detail")
async def api_get_patient(
application_id: str = Query(..., alias="pilotApplicationId"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.get_patient_by_application(
db, app_id=application_id, tenant_no=tenant_no
)
if result is None:
return success_response(None)
return success_response(result)
@router.post("/pilot-patient/save")
async def api_save_patient(
body: PatientSaveBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {
"customerId": body.customer_id,
"patientName": body.patient_name,
"sex": body.sex,
"age": body.age,
"idCardNo": body.id_card_no,
"phoneNo": body.phone_no,
"emergencyContact": body.emergency_contact,
"emergencyPhone": body.emergency_phone,
"clinicalDiagnosis": body.clinical_diagnosis,
"diagnosisAttachments": body.diagnosis_attachments,
"needCheckup": body.need_checkup,
}
result = pilot_application.save_patient(
db,
app_id=body.application_id,
data=data,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="保存成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 3. 输注方案
# ---------------------------------------------------------------------------
@router.get("/pilot-infusion-plan/list")
async def api_list_plans(
application_id: str = Query(..., alias="pilotApplicationId"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.list_infusion_plans(
db, app_id=application_id, tenant_no=tenant_no
)
return success_response(result)
@router.post("/pilot-infusion-plan/create")
async def api_create_plan(
body: InfusionPlanCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {
"cellType": body.cell_type,
"specText": body.spec_text,
"totalCourseCount": body.total_course_count,
"courseInterval": body.course_interval,
"infusionRoute": body.infusion_route,
"infusionStartDate": body.infusion_start_date,
}
result = pilot_application.create_infusion_plan(
db,
app_id=body.pilot_application_id,
data=data,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/pilot-infusion-plan/update")
async def api_update_plan(
body: InfusionPlanUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {}
if body.cell_type is not None:
data["cellType"] = body.cell_type
if body.spec_text is not None:
data["specText"] = body.spec_text
if body.total_course_count is not None:
data["totalCourseCount"] = body.total_course_count
if body.course_interval is not None:
data["courseInterval"] = body.course_interval
if body.infusion_route is not None:
data["infusionRoute"] = body.infusion_route
if body.infusion_start_date is not None:
data["infusionStartDate"] = body.infusion_start_date
result = pilot_application.update_infusion_plan(
db, plan_id=body.id, data=data, tenant_no=tenant_no
)
return success_response(result, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.delete("/pilot-infusion-plan/delete")
async def api_delete_plan(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
pilot_application.soft_delete_infusion_plan(
db, plan_id=body.id, tenant_no=tenant_no
)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 4. 输注计划
# ---------------------------------------------------------------------------
@router.get("/pilot-infusion-schedule/list")
async def api_list_schedules(
plan_id: str = Query(..., alias="infusionPlanId"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.list_schedules_by_plan(
db, plan_id=plan_id, tenant_no=tenant_no
)
return success_response(result)
@router.post("/pilot-infusion-schedule/auto-generate")
async def api_auto_generate(
body: PlanIdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.auto_generate_schedules(
db,
plan_id=body.plan_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="生成成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-infusion-schedule/create")
async def api_create_schedule(
body: ScheduleCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {
"infusionTime": body.infusion_time,
"remark": body.remark,
"amount": body.amount,
}
result = pilot_application.create_schedule(
db,
plan_id=body.plan_id,
data=data,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/pilot-infusion-schedule/update")
async def api_update_schedule(
body: InfusionScheduleUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {}
if body.infusion_time is not None:
data["infusionTime"] = body.infusion_time
if body.remark is not None:
data["remark"] = body.remark
if body.attachments is not None:
data["attachments"] = body.attachments
result = pilot_application.update_schedule(
db, schedule_id=body.id, data=data, tenant_no=tenant_no
)
return success_response(result, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.delete("/pilot-infusion-schedule/delete")
async def api_delete_schedule(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
pilot_application.soft_delete_schedule(
db, schedule_id=body.id, tenant_no=tenant_no
)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-infusion-schedule/mark-complete")
async def api_mark_complete(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.mark_schedule_complete(
db, schedule_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
)
return success_response(result, message="操作成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 出库生产工单 / 出库质检工单 联动
# ---------------------------------------------------------------------------
class ScheduleIdBody(BaseModel):
scheduleId: str
@router.post("/pilot-infusion-schedule/create-production-work-order")
async def api_create_outbound_production(
body: ScheduleIdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
from app.crud import production_work_order as pwo_svc
from app.crud.qc_work_order import resolve_outbound_biz_type_for_schedule
outbound_biz_type = resolve_outbound_biz_type_for_schedule(
db, body.scheduleId, tenant_no
)
wo = pwo_svc.create_outbound_production_work_order(
db,
schedule_id=body.scheduleId,
tenant_no=tenant_no,
user_id=user.get("user_id"),
create_dept_id=user.get("dept_id", ""),
biz_type=outbound_biz_type,
)
db.commit()
db.refresh(wo)
return success_response(pwo_svc._wo_to_dict(wo), message="创建出库生产工单成功")
except ValueError as e:
return error_response(400, str(e))
except Exception as e:
db.rollback()
return error_response(400, str(e))
class OutboundQcBody(BaseModel):
scheduleId: str
cellProductId: str = ""
@router.post("/pilot-infusion-schedule/create-qc-work-order")
async def api_create_outbound_qc(
body: OutboundQcBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
from app.crud import qc_work_order as qc_svc
from app.crud.qc_work_order import resolve_outbound_biz_type_for_schedule
outbound_biz_type = resolve_outbound_biz_type_for_schedule(
db, body.scheduleId, tenant_no
)
wo = qc_svc.create_outbound_qc_work_order(
db,
schedule_id=body.scheduleId,
cell_product_id=body.cellProductId,
tenant_no=tenant_no,
user_id=user.get("user_id"),
create_dept_id=user.get("dept_id", ""),
biz_type=outbound_biz_type,
)
return success_response(qc_svc._wo_to_dict(wo), message="创建出库质检工单成功")
except ValueError as e:
return error_response(400, str(e))
except Exception as e:
db.rollback()
return error_response(400, str(e))