411 lines
14 KiB
Python
411 lines
14 KiB
Python
"""科研捐赠管理 — 捐赠单 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: str = Field(..., 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")
|
|
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")
|
|
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")
|
|
|
|
|
|
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"),
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
collector_user_id = _get_current_user_id(user)
|
|
result = rd_svc.list_donations(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
donation_status=donationStatus,
|
|
collector_user_id=collector_user_id,
|
|
)
|
|
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"),
|
|
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,
|
|
)
|
|
return success_response(result)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. 详情
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@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)
|
|
data["collector_user_id"] = 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_none=False, 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,
|
|
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))
|