| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- """科研捐赠管理 — 捐赠单 CRUD / 审核 / 状态变更 / 转换 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 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 research_donation as rd_svc
- from app.models.models import SysUser
- from app.models.research_donation import BizResearchDonation
- 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)
- # ---------------------------------------------------------------------------
- # Pydantic bodies
- # ---------------------------------------------------------------------------
- class DonationCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- donation_no: Optional[str] = Field(None, alias="donationNo")
- donation_status: Optional[str] = Field("dsx", alias="donationStatus")
- donation_date: Optional[str] = Field(None, alias="donationDate")
- source_type: Optional[str] = Field("direct", alias="sourceType")
- source_protocol_id: Optional[str] = Field(None, alias="sourceProtocolId")
- consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
- consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
- attachment_urls: Optional[str] = Field(None, alias="attachmentUrls")
- other_attachments: Optional[list] = Field(None, alias="otherAttachments")
- infectious_disease_report_urls: Optional[str] = Field(
- None, alias="infectiousDiseaseReportUrls"
- )
- infectious_disease_report_attachments: Optional[list] = Field(
- None, alias="infectiousDiseaseReportAttachments"
- )
- remark: Optional[str] = None
- party: Optional[dict] = None
- party_customer: Optional[dict] = Field(None, alias="partyCustomer")
- collection: Optional[dict] = None
- collection_info: Optional[dict] = Field(None, alias="collectionInfo")
- samples: Optional[list] = None
- collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
- class DonationUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- donation_no: Optional[str] = Field(None, alias="donationNo")
- donation_status: Optional[str] = Field(None, alias="donationStatus")
- donation_date: Optional[str] = Field(None, alias="donationDate")
- source_type: Optional[str] = Field(None, alias="sourceType")
- source_protocol_id: Optional[str] = Field(None, alias="sourceProtocolId")
- consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
- consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
- attachment_urls: Optional[str] = Field(None, alias="attachmentUrls")
- other_attachments: Optional[list] = Field(None, alias="otherAttachments")
- infectious_disease_report_urls: Optional[str] = Field(
- None, alias="infectiousDiseaseReportUrls"
- )
- infectious_disease_report_attachments: Optional[list] = Field(
- None, alias="infectiousDiseaseReportAttachments"
- )
- remark: Optional[str] = None
- party: Optional[dict] = None
- party_customer: Optional[dict] = Field(None, alias="partyCustomer")
- collection: Optional[dict] = None
- collection_info: Optional[dict] = Field(None, alias="collectionInfo")
- samples: Optional[list] = None
- collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
- class BatchDeleteBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- ids: List[str] = Field(..., alias="ids", min_length=1)
- class AuditSubmitBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- audit_status: str = Field(..., alias="auditStatus")
- audit_remark: Optional[str] = Field(None, alias="auditRemark")
- class StatusChangeBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- new_status: str = Field(..., alias="newStatus")
- desc: Optional[str] = Field(None, alias="desc")
- time_val: Optional[str] = Field(None, alias="timeVal")
- void_type: Optional[str] = Field(None, alias="voidType")
- class ConvertFromStorageBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- protocol_id: str = Field(..., alias="protocolId")
- # ---------------------------------------------------------------------------
- # 1. 列表
- # ---------------------------------------------------------------------------
- @router.get("/research-donations")
- async def list_research_donations(
- page: int = Query(1, ge=1),
- pageSize: int = Query(20, ge=1, le=200, alias="pageSize"),
- keyword: Optional[str] = Query(None, alias="keyword"),
- donationStatus: Optional[str] = Query(None, alias="donationStatus"),
- createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
- createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
- sampleCollectTimeStart: Optional[str] = Query(None, alias="sampleCollectTimeStart"),
- sampleCollectTimeEnd: Optional[str] = Query(None, alias="sampleCollectTimeEnd"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- result = rd_svc.list_donations(
- db,
- tenant_no=tenant_no,
- page=page,
- page_size=pageSize,
- keyword=keyword,
- donation_status=donationStatus,
- create_time_start=createTimeStart,
- create_time_end=createTimeEnd,
- sample_collect_time_start=sampleCollectTimeStart,
- sample_collect_time_end=sampleCollectTimeEnd,
- user_info=user,
- )
- return success_response(result)
- # ---------------------------------------------------------------------------
- # 2. 审核列表(须在 {donation_id} 详情路由之前注册,避免 audits 被当作 id)
- # ---------------------------------------------------------------------------
- @router.get("/research-donations/audits")
- async def list_research_donation_audits(
- page: int = Query(1, ge=1),
- pageSize: int = Query(20, ge=1, le=200, alias="pageSize"),
- keyword: Optional[str] = Query(None, alias="keyword"),
- auditStatus: Optional[str] = Query(None, alias="auditStatus"),
- researchDonationId: Optional[str] = Query(None, alias="researchDonationId"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- result = rd_svc.list_audits(
- db,
- tenant_no=tenant_no,
- page=page,
- page_size=pageSize,
- keyword=keyword,
- audit_status=auditStatus,
- research_donation_id=researchDonationId,
- user_info=user,
- )
- return success_response(result)
- # ---------------------------------------------------------------------------
- # 3. 下一捐赠样本编码(须在 {donation_id} 详情路由之前注册)
- # ---------------------------------------------------------------------------
- @router.get("/research-donations/next-sample-code")
- async def next_research_donation_sample_code(
- pendingCodes: Optional[str] = Query(None, alias="pendingCodes"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- extra = [c.strip() for c in (pendingCodes or "").split(",") if c.strip()]
- code = rd_svc.next_donation_sample_code(db, tenant_no=tenant_no, extra_codes=extra)
- return success_response({"sampleCode": code})
- # ---------------------------------------------------------------------------
- # 4. 详情
- # ---------------------------------------------------------------------------
- @router.get("/research-donations/{donation_id}")
- async def get_research_donation_detail(
- donation_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- result = rd_svc.get_donation_detail(db, donation_id, tenant_no)
- if not result:
- return error_response(404, "捐赠单不存在或无权查看")
- return success_response(result)
- # ---------------------------------------------------------------------------
- # 4. 创建
- # ---------------------------------------------------------------------------
- @router.post("/research-donations")
- async def create_research_donation(
- body: DonationCreateBody,
- 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:
- data = body.model_dump(exclude_none=False, by_alias=True)
- collector = (data.get("collectorUserId") or data.get("collector_user_id") or "").strip()
- if not collector:
- data["collectorUserId"] = user_id
- obj = rd_svc.create_donation(
- db,
- data=data,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- return success_response(
- rd_svc._donation_to_dict(obj, db, tenant_no),
- message="创建成功",
- )
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 5. 更新
- # ---------------------------------------------------------------------------
- @router.put("/research-donations/{donation_id}")
- async def update_research_donation(
- donation_id: str,
- body: DonationUpdateBody,
- 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:
- data = body.model_dump(exclude_unset=True, by_alias=True)
- obj = rd_svc.update_donation(
- db,
- donation_id,
- data=data,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- return success_response(
- rd_svc._donation_to_dict(obj, db, tenant_no),
- message="更新成功",
- )
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 6. 删除
- # ---------------------------------------------------------------------------
- @router.delete("/research-donations/{donation_id}")
- async def delete_research_donation(
- donation_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:
- rd_svc.delete_donation(
- db,
- donation_id,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 7. 批量删除
- # ---------------------------------------------------------------------------
- @router.post("/research-donations/batch-delete")
- async def batch_delete_research_donations(
- body: BatchDeleteBody,
- 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:
- count = rd_svc.batch_delete_donations(
- db,
- ids=body.ids,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- return success_response({"count": count}, message=f"已成功删除 {count} 条")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 8. 提交审核
- # ---------------------------------------------------------------------------
- @router.post("/research-donations/{donation_id}/audit")
- async def submit_donation_audit(
- donation_id: str,
- body: AuditSubmitBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- rd_svc.submit_audit(
- db,
- donation_id,
- audit_status=body.audit_status,
- audit_remark=body.audit_remark,
- user_id=user.get("user_id"),
- tenant_no=tenant_no,
- )
- d = (
- db.query(BizResearchDonation)
- .filter(BizResearchDonation.id == donation_id)
- .first()
- )
- dn = (d.donation_no if d else "") or ""
- return success_response({"donationNo": dn}, message="审核已提交")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 9. 状态变更
- # ---------------------------------------------------------------------------
- @router.post("/research-donations/{donation_id}/status-change")
- async def change_donation_status(
- donation_id: str,
- body: StatusChangeBody,
- 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:
- obj = rd_svc.change_status(
- db,
- donation_id,
- new_status=body.new_status,
- desc=body.desc,
- time_val=body.time_val,
- void_type=body.void_type,
- user_id=user_id,
- tenant_no=tenant_no,
- )
- return success_response(
- rd_svc._donation_to_dict(obj, db, tenant_no),
- message="状态变更成功",
- )
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 10. 细胞存储 → 科研捐赠
- # ---------------------------------------------------------------------------
- @router.post("/research-donations/convert-from-storage")
- async def convert_from_storage(
- body: ConvertFromStorageBody,
- 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:
- obj = rd_svc.convert_from_storage(
- db,
- body.protocol_id,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- return success_response(
- rd_svc._donation_to_dict(obj, db, tenant_no),
- message="转换成功",
- )
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 11. 科研捐赠 → 细胞存储
- # ---------------------------------------------------------------------------
- @router.post("/research-donations/{donation_id}/convert-to-storage")
- async def convert_to_storage(
- donation_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:
- obj = rd_svc.convert_to_storage(
- db,
- donation_id,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- return success_response(
- rd_svc._donation_to_dict(obj, db, tenant_no),
- message="转换成功",
- )
- except ValueError as e:
- return error_response(400, str(e))
|