Files
zkxw/backend/app/api/collection_protocol.py
T
2026-05-17 12:31:11 +08:00

659 lines
24 KiB
Python

"""细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API"""
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from sqlalchemy import and_ as sa_and
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.core.utils import parse_datetime
from app.crud import collection_protocol as cp_svc
from app.crud import customer as customer_crud
from app.models.models import BizCollectionProtocol, BizCustomer, 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
def _get_current_user_id(user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失")
return str(user_id)
_CAMEL_TO_SNAKE_DATETIME = {
"newbornDeliveryTime": "newborn_delivery_time",
"sampleCollectTime": "sample_collect_time",
"maternalBloodCollectTime": "maternal_blood_collect_time",
}
def _parse_datetime_fields(d: dict, camel_keys: list) -> None:
for camel in camel_keys:
val = d.get(camel)
if val is not None and isinstance(val, str):
snake = _CAMEL_TO_SNAKE_DATETIME.get(camel, camel)
parsed = parse_datetime(val)
if parsed:
d.pop(camel, None)
d[snake] = parsed
else:
d.pop(camel, None)
d[snake] = None
def _ensure_customer_for_mother(db: Session, party_customer: dict, tenant_no: str, user_id: str) -> dict:
"""If mother info exists but no customerId, create a customer record and link it."""
if not party_customer:
return party_customer
customer_id = party_customer.get("customer_id") or party_customer.get("customerId")
mother_name = (party_customer.get("mother_name") or party_customer.get("motherName") or "").strip()
if customer_id or not mother_name:
return party_customer
# Build customer fields from mother info
mother_phone = (party_customer.get("mother_phone") or party_customer.get("motherPhone") or "").strip() or None
mother_id_card = (party_customer.get("mother_id_card") or party_customer.get("motherIdCard") or "").strip() or None
# Check if customer with this id_card already exists
if mother_id_card and customer_crud.id_card_taken(db, mother_id_card, tenant_no):
existing = db.query(BizCustomer).filter(
sa_and(
BizCustomer.id_card_no == mother_id_card,
BizCustomer.tenant_no == tenant_no,
BizCustomer.del_flag == 0,
)
).first()
if existing:
party_customer["customer_id"] = existing.id
return party_customer
# Create new customer
try:
obj = customer_crud.create_customer(
db,
customer_name=mother_name,
sex="1", # female (mother)
id_card_no=mother_id_card,
phone_no=mother_phone,
tenant_no=tenant_no,
user_id=user_id,
)
party_customer["customer_id"] = obj.id
except ValueError:
# If creation fails (e.g. duplicate), try to find existing
pass
return party_customer
# ---------------------------------------------------------------------------
# 采集协议单
# ---------------------------------------------------------------------------
@router.get("/collection-protocols")
async def list_collection_protocols(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
protocolStatus: Optional[str] = Query(None, alias="protocolStatus"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
current_user_id = _get_current_user_id(user)
items, total = cp_svc.list_collection_protocols(
db,
page=page,
page_size=pageSize,
keyword=keyword,
protocol_status=protocolStatus,
tenant_no=tenant_no,
scope_collector_user_id=current_user_id,
)
tp = cp_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@router.get("/collection-protocols/{protocol_id}")
async def get_collection_protocol_detail(
protocol_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
current_user_id = _get_current_user_id(user)
bundle = cp_svc.get_protocol_detail_bundle(
db, protocol_id, tenant_no, scope_collector_user_id=current_user_id
)
if not bundle:
return error_response(404, "协议单不存在或无权查看")
return success_response(bundle)
class ProtocolCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
protocol_no: str = Field(..., alias="protocolNo")
protocol_no_id: Optional[str] = Field(None, alias="protocolNoId")
protocol_status: str = Field("dsx", alias="protocolStatus")
protocol_attachments: Optional[str] = Field(None, alias="protocolAttachments")
register_date: Optional[str] = Field(None, alias="registerDate")
collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
remark: Optional[str] = None
party_customer: Optional[dict] = Field(None, alias="partyCustomer")
collection_info: Optional[dict] = Field(None, alias="collectionInfo")
samples: Optional[list] = Field(None, alias="samples")
payment: Optional[dict] = Field(None, alias="payment")
payment_details: Optional[list] = Field(None, alias="paymentDetails")
attachments: Optional[list] = Field(None, alias="attachments")
storage_cert: Optional[dict] = Field(None, alias="storageCert")
class ProtocolUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
protocol_no: Optional[str] = Field(None, alias="protocolNo")
protocol_status: Optional[str] = Field(None, alias="protocolStatus")
protocol_attachments: Optional[str] = Field(None, alias="protocolAttachments")
register_date: Optional[str] = Field(None, alias="registerDate")
effective_time: Optional[str] = Field(None, alias="effectiveTime")
effective_desc: Optional[str] = Field(None, alias="effectiveDesc")
expire_time: Optional[str] = Field(None, alias="expireTime")
expire_desc: Optional[str] = Field(None, alias="expireDesc")
void_time: Optional[str] = Field(None, alias="voidTime")
void_desc: Optional[str] = Field(None, alias="voidDesc")
to_research_time: Optional[str] = Field(None, alias="toResearchTime")
to_research_desc: Optional[str] = Field(None, alias="toResearchDesc")
collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
remark: Optional[str] = None
party_customer: Optional[dict] = Field(None, alias="partyCustomer")
collection_info: Optional[dict] = Field(None, alias="collectionInfo")
samples: Optional[list] = Field(None, alias="samples")
payment: Optional[dict] = Field(None, alias="payment")
payment_details: Optional[list] = Field(None, alias="paymentDetails")
attachments: Optional[list] = Field(None, alias="attachments")
storage_cert: Optional[dict] = Field(None, alias="storageCert")
def _resolve_protocol_attachments(
protocol_attachments: Optional[str],
attachments: Optional[list],
) -> Optional[str]:
from_urls = cp_svc.attachments_to_urls(attachments)
if from_urls:
return from_urls
return protocol_attachments
def _persist_protocol_sub_resources(
db: Session,
protocol_id: str,
tenant_no: str,
user_id: str,
*,
party_customer: Optional[dict],
collection_info: Optional[dict],
samples: Optional[list],
payment: Optional[dict],
payment_details: Optional[list],
storage_cert: Optional[dict],
) -> None:
if party_customer:
_parse_datetime_fields(party_customer, ["newbornDeliveryTime"])
party_customer = _ensure_customer_for_mother(db, party_customer, tenant_no, user_id)
cp_svc.upsert_party_customer(db, protocol_id, tenant_no, user_id, **party_customer)
if collection_info:
_parse_datetime_fields(collection_info, ["sampleCollectTime", "maternalBloodCollectTime"])
cp_svc.upsert_collection_info(db, protocol_id, tenant_no, user_id, **collection_info)
if samples is not None:
cp_svc.save_samples(db, protocol_id, tenant_no, user_id, samples)
if payment:
cp_svc.upsert_payment(db, protocol_id, tenant_no, user_id, **payment)
if payment_details is not None:
pay = cp_svc.get_payment_by_protocol_id(db, protocol_id, tenant_no)
if not pay and payment:
pay = cp_svc.upsert_payment(db, protocol_id, tenant_no, user_id, **payment)
if pay:
cp_svc.save_payment_details(db, pay.id, tenant_no, user_id, payment_details)
if storage_cert:
cp_svc.upsert_storage_cert(db, protocol_id, tenant_no, user_id, **storage_cert)
@router.post("/collection-protocols")
async def create_collection_protocol(
body: ProtocolCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
try:
protocol_attachments = _resolve_protocol_attachments(
body.protocol_attachments,
body.attachments,
)
obj = cp_svc.create_collection_protocol(
db,
protocol_no=body.protocol_no,
protocol_no_id=body.protocol_no_id,
protocol_status=body.protocol_status,
protocol_attachments=protocol_attachments,
register_date=body.register_date,
collector_user_id=user_id,
remark=body.remark,
tenant_no=tenant_no,
user_id=user_id,
)
protocol_id = obj.id
_persist_protocol_sub_resources(
db,
protocol_id,
tenant_no,
user_id,
party_customer=body.party_customer,
collection_info=body.collection_info,
samples=body.samples,
payment=body.payment,
payment_details=body.payment_details,
storage_cert=body.storage_cert,
)
return success_response(cp_svc._protocol_to_dict(obj), message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/collection-protocols/{protocol_id}")
async def update_collection_protocol(
protocol_id: str,
body: ProtocolUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
try:
payload = body.model_dump(exclude_unset=True, by_alias=False)
sub_keys = {
"party_customer", "collection_info", "samples",
"payment", "payment_details", "attachments", "storage_cert",
}
main_payload = {k: v for k, v in payload.items() if k not in sub_keys}
main_payload.pop("collector_user_id", None)
if body.attachments is not None:
resolved = _resolve_protocol_attachments(
main_payload.get("protocol_attachments"),
body.attachments,
)
if resolved is not None:
main_payload["protocol_attachments"] = resolved
obj = cp_svc.update_collection_protocol(
db,
protocol_id,
tenant_no=tenant_no,
user_id=user_id,
scope_collector_user_id=user_id,
**{k: v for k, v in main_payload.items() if v is not None},
)
_persist_protocol_sub_resources(
db,
protocol_id,
tenant_no,
user_id,
party_customer=body.party_customer,
collection_info=body.collection_info,
samples=body.samples,
payment=body.payment,
payment_details=body.payment_details,
storage_cert=body.storage_cert,
)
return success_response(cp_svc._protocol_to_dict(obj), message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.delete("/collection-protocols/{protocol_id}")
async def delete_collection_protocol(
protocol_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
try:
cp_svc.soft_delete_collection_protocol(
db,
protocol_id,
tenant_no=tenant_no,
scope_collector_user_id=user_id,
user_id=user_id,
)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 审核
# ---------------------------------------------------------------------------
@router.get("/collection-protocol-audits")
async def list_collection_protocol_audits(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
auditStatus: Optional[str] = Query(None, alias="auditStatus"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items, total = cp_svc.list_audits(
db,
page=page,
page_size=pageSize,
keyword=keyword,
audit_status=auditStatus,
tenant_no=tenant_no,
)
tp = cp_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
class AuditCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
audit_status: str = Field(..., alias="auditStatus")
audit_remark: Optional[str] = Field(None, alias="auditRemark")
@router.post("/collection-protocols/{protocol_id}/audit")
async def submit_collection_protocol_audit(
protocol_id: str,
body: AuditCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
try:
cp_svc.create_audit(
db,
collection_protocol_id=protocol_id,
audit_status=body.audit_status,
audit_remark=body.audit_remark,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
p = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.id == protocol_id).first()
pn = (p.protocol_no if p else "") or ""
return success_response({"protocolNo": pn}, message="审核已提交")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 收款确认
# ---------------------------------------------------------------------------
@router.get("/protocol-payment-details")
async def list_payment_details(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
payStatus: str = Query("dfj", alias="payStatus"),
keyword: str = Query("", alias="keyword"),
transferType: str = Query("", alias="transferType"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items, total = cp_svc.list_payment_details_page(
db,
page=page,
page_size=pageSize,
pay_status=payStatus,
keyword=keyword or None,
transfer_type=transferType or None,
tenant_no=tenant_no,
)
tp = cp_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
class BatchDetailIdsBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
detail_ids: List[str] = Field(..., alias="detailIds", min_length=1)
class RefundFinanceSubmitBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
refund_amount: float = Field(..., alias="refundAmount", gt=0)
refund_remark: Optional[str] = Field(None, alias="refundRemark")
refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
class BatchRefundFinanceSubmitBody(RefundFinanceSubmitBody):
model_config = ConfigDict(populate_by_name=True)
detail_ids: List[str] = Field(..., alias="detailIds", min_length=1)
@router.post("/protocol-payment-details/{detail_id}/submit-receipt-finance")
async def post_submit_receipt_finance(
detail_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
try:
cp_svc.submit_receipt_finance(
db,
detail_id=detail_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(None, message="提交成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/protocol-payment-details/submit-receipt-finance-batch")
async def post_submit_receipt_finance_batch(
body: BatchDetailIdsBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
try:
count = cp_svc.batch_submit_receipt_finance(
db,
detail_ids=body.detail_ids,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response({"count": count}, message=f"已成功提交 {count} 条")
except ValueError as e:
return error_response(400, str(e))
@router.post("/protocol-payment-details/{detail_id}/submit-refund-finance")
async def post_submit_refund_finance(
detail_id: str,
body: RefundFinanceSubmitBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
try:
cp_svc.submit_refund_finance(
db,
detail_id=detail_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
refund_amount=body.refund_amount,
refund_remark=body.refund_remark,
refund_attachments=body.refund_attachments,
)
return success_response(None, message="提交成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/protocol-payment-details/submit-refund-finance-batch")
async def post_submit_refund_finance_batch(
body: BatchRefundFinanceSubmitBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
try:
count = cp_svc.batch_submit_refund_finance(
db,
detail_ids=body.detail_ids,
tenant_no=tenant_no,
user_id=user.get("user_id"),
refund_amount=body.refund_amount,
refund_remark=body.refund_remark,
refund_attachments=body.refund_attachments,
)
return success_response({"count": count}, message=f"已成功提交 {count} 条")
except ValueError as e:
return error_response(400, str(e))
@router.get("/protocol-payment-details/receipt-confirm")
async def list_receipt_confirm(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items, total = cp_svc.list_receipt_confirm_details(db, page=page, page_size=pageSize, tenant_no=tenant_no)
tp = cp_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@router.get("/protocol-payment-details/receipt-confirmed")
async def list_receipt_confirmed(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items, total = cp_svc.list_receipt_confirmed_details(
db, page=page, page_size=pageSize, tenant_no=tenant_no
)
tp = cp_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
class FinanceConfirmBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
finance_confirm_attachments: Optional[str] = Field(None, alias="financeConfirmAttachments")
finance_remark: Optional[str] = Field(None, alias="financeRemark")
class RefundFinanceConfirmBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
refund_finance_confirm_attachments: Optional[str] = Field(
None, alias="refundFinanceConfirmAttachments"
)
refund_finance_remark: Optional[str] = Field(None, alias="refundFinanceRemark")
@router.post("/protocol-payment-details/{detail_id}/receipt-confirm")
async def post_receipt_confirm(
detail_id: str,
body: FinanceConfirmBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
try:
cp_svc.confirm_receipt(
db,
detail_id=detail_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
finance_confirm_attachments=body.finance_confirm_attachments,
finance_remark=body.finance_remark,
)
return success_response(None, message="确认成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 退费确认
# ---------------------------------------------------------------------------
@router.get("/protocol-payment-details/refund-confirm")
async def list_refund_confirm(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items, total = cp_svc.list_refund_confirm_details(db, page=page, page_size=pageSize, tenant_no=tenant_no)
tp = cp_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@router.get("/protocol-payment-details/refund-confirmed")
async def list_refund_confirmed(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items, total = cp_svc.list_refund_confirmed_details(
db, page=page, page_size=pageSize, tenant_no=tenant_no
)
tp = cp_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@router.post("/protocol-payment-details/{detail_id}/refund-confirm")
async def post_refund_confirm(
detail_id: str,
body: RefundFinanceConfirmBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
try:
cp_svc.confirm_refund(
db,
detail_id=detail_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
refund_finance_confirm_attachments=body.refund_finance_confirm_attachments,
refund_finance_remark=body.refund_finance_remark,
)
return success_response(None, message="确认成功")
except ValueError as e:
return error_response(400, str(e))