feat: switch protocol view cert/insurance handle panels to sample level.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-13 11:04:25 +08:00
1 parent 8cebb8e0b8
commit efbf8f6ac9
6 files changed
+353 -14

No files matched your search

+2
View File
@@ -43,6 +43,7 @@ from app.api.mini_menu import router as mini_menu_router
from app.api.market_mobile import router as market_mobile_router from app.api.market_mobile import router as market_mobile_router
from app.api.sample_storage_cert_qc import router as sample_storage_cert_qc_router from app.api.sample_storage_cert_qc import router as sample_storage_cert_qc_router
from app.api.customer_service_dict import router as customer_service_dict_router from app.api.customer_service_dict import router as customer_service_dict_router
from app.api.sample_cert_insurance import router as sample_cert_insurance_router
__all__ = [ __all__ = [
"public_router", "public_router",
@@ -89,4 +90,5 @@ __all__ = [
"market_mobile_router", "market_mobile_router",
"sample_storage_cert_qc_router", "sample_storage_cert_qc_router",
"customer_service_dict_router", "customer_service_dict_router",
"sample_cert_insurance_router",
] ]
+210
View File
@@ -0,0 +1,210 @@
"""样本级储存证 / 保险办理 API"""
from typing import Optional
from fastapi import APIRouter, Depends
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_any_menu_code
from app.core.response import error_response, success_response
from app.core.utils import parse_datetime
from app.crud import sample_insurance as insurance_crud
from app.crud import sample_storage_cert as cert_crud
from app.models.models import BizProtocolSample, SysUser
router = APIRouter(prefix="/web/admin", tags=["样本储存证保险"])
_MENU = require_any_menu_code("cell-storage:sample-info:list")
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)
def _resolve_protocol_id(db: Session, tenant_no: str, sample_code: str) -> Optional[str]:
row = (
db.query(BizProtocolSample)
.filter(
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.sample_code == sample_code,
BizProtocolSample.del_flag == 0,
)
.first()
)
return (row.collection_protocol_id or "").strip() or None if row else None
class SampleHandleBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
handle_time: Optional[str] = Field(None, alias="handleTime")
handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
cert_image_urls: Optional[str] = Field(None, alias="certImageUrls")
insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
remark: Optional[str] = None
handle_remark: Optional[str] = Field(None, alias="handleRemark")
mail_cert_time: Optional[str] = Field(None, alias="mailCertTime")
policy_send_time: Optional[str] = Field(None, alias="policySendTime")
collection_protocol_id: Optional[str] = Field(None, alias="collectionProtocolId")
@router.get("/sample-storage-certs/{sample_code}")
async def get_sample_storage_cert(
sample_code: str,
db: Session = Depends(get_db),
user: dict = Depends(_MENU),
):
try:
tenant_no = _get_tenant_no(db, user)
code = (sample_code or "").strip()
row = cert_crud.get_by_sample_code(db, tenant_no, code)
if row is None:
return success_response(
{
"sampleCode": code,
"storageCertHandled": False,
"handleTime": "",
"handlerUserId": "",
"certImageUrls": "",
"mailCertTime": "",
"remark": "",
}
)
return success_response(cert_crud.row_to_dict(row))
except PermissionError as e:
return error_response(403, str(e))
except Exception as e:
return error_response(400, str(e))
@router.put("/sample-storage-certs/{sample_code}")
async def put_sample_storage_cert(
sample_code: str,
body: SampleHandleBody,
db: Session = Depends(get_db),
user: dict = Depends(_MENU),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
code = (sample_code or "").strip()
pid = (body.collection_protocol_id or "").strip() or _resolve_protocol_id(
db, tenant_no, code
)
ht = parse_datetime(body.handle_time) if body.handle_time else None
mail_raw = body.mail_cert_time
update_mail = mail_raw is not None
mail_t = parse_datetime(mail_raw) if mail_raw and str(mail_raw).strip() else None
row = cert_crud.upsert_handle(
db,
tenant_no=tenant_no,
sample_code=code,
collection_protocol_id=pid,
user_id=user_id,
handle_time=ht,
handler_user_id=body.handler_user_id,
cert_image_urls=body.cert_image_urls or "",
remark=body.remark or "",
mail_cert_time=mail_t,
update_mail_cert_time=update_mail,
)
db.commit()
return success_response(cert_crud.row_to_dict(row), message="保存成功")
except PermissionError as e:
db.rollback()
return error_response(403, str(e))
except ValueError as e:
db.rollback()
return error_response(400, str(e))
except Exception as e:
db.rollback()
return error_response(400, str(e))
@router.get("/sample-insurances/{sample_code}")
async def get_sample_insurance(
sample_code: str,
db: Session = Depends(get_db),
user: dict = Depends(_MENU),
):
try:
tenant_no = _get_tenant_no(db, user)
code = (sample_code or "").strip()
row = insurance_crud.get_by_sample_code(db, tenant_no, code)
if row is None:
return success_response(
{
"sampleCode": code,
"insuranceHandled": False,
"handleTime": "",
"handlerUserId": "",
"insuranceAttachments": "",
"policySendTime": "",
"handleRemark": "",
}
)
return success_response(insurance_crud.row_to_dict(row))
except PermissionError as e:
return error_response(403, str(e))
except Exception as e:
return error_response(400, str(e))
@router.put("/sample-insurances/{sample_code}")
async def put_sample_insurance(
sample_code: str,
body: SampleHandleBody,
db: Session = Depends(get_db),
user: dict = Depends(_MENU),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
code = (sample_code or "").strip()
pid = (body.collection_protocol_id or "").strip() or _resolve_protocol_id(
db, tenant_no, code
)
ht = parse_datetime(body.handle_time) if body.handle_time else None
policy_raw = body.policy_send_time
update_policy = policy_raw is not None
policy_t = (
parse_datetime(policy_raw) if policy_raw and str(policy_raw).strip() else None
)
row = insurance_crud.upsert_handle(
db,
tenant_no=tenant_no,
sample_code=code,
collection_protocol_id=pid,
user_id=user_id,
handle_time=ht,
handler_user_id=body.handler_user_id,
insurance_attachments=body.insurance_attachments or "",
handle_remark=body.handle_remark or body.remark or "",
policy_send_time=policy_t,
update_policy_send_time=update_policy,
)
db.commit()
return success_response(insurance_crud.row_to_dict(row), message="保存成功")
except PermissionError as e:
db.rollback()
return error_response(403, str(e))
except ValueError as e:
db.rollback()
return error_response(400, str(e))
except Exception as e:
db.rollback()
return error_response(400, str(e))
+2 -1
View File
@@ -13,7 +13,7 @@ from app.core.database import SessionLocal
from app.core.api_request_log_middleware import ApiRequestLogMiddleware from app.core.api_request_log_middleware import ApiRequestLogMiddleware
from app.core.api_crypto_middleware import ApiCryptoMiddleware from app.core.api_crypto_middleware import ApiCryptoMiddleware
from app.models import models # noqa: F401 from app.models import models # noqa: F401
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, sample_freight_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router, market_mobile_router, sample_storage_cert_qc_router, customer_service_dict_router from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, sample_freight_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router, market_mobile_router, sample_storage_cert_qc_router, customer_service_dict_router, sample_cert_insurance_router
@asynccontextmanager @asynccontextmanager
@@ -155,6 +155,7 @@ app.include_router(mini_menu_router, prefix=settings.API_V1_PREFIX)
app.include_router(market_mobile_router, prefix=settings.API_V1_PREFIX) app.include_router(market_mobile_router, prefix=settings.API_V1_PREFIX)
app.include_router(sample_storage_cert_qc_router, prefix=settings.API_V1_PREFIX) app.include_router(sample_storage_cert_qc_router, prefix=settings.API_V1_PREFIX)
app.include_router(customer_service_dict_router, prefix=settings.API_V1_PREFIX) app.include_router(customer_service_dict_router, prefix=settings.API_V1_PREFIX)
app.include_router(sample_cert_insurance_router, prefix=settings.API_V1_PREFIX)
def _mask_secret(value: str) -> str: def _mask_secret(value: str) -> str:
+42
View File
@@ -55,3 +55,45 @@ export function fetchUpdateFullmoonHandle(
) { ) {
return request.put({ url: `/web/admin/fullmoon-todos/${protocolId}/handle`, data }) return request.put({ url: `/web/admin/fullmoon-todos/${protocolId}/handle`, data })
} }
export function fetchSampleStorageCert(sampleCode: string) {
return request.get({ url: `/web/admin/sample-storage-certs/${encodeURIComponent(sampleCode)}` })
}
export function fetchUpdateSampleStorageCert(
sampleCode: string,
data: {
handleTime?: string | null
handlerUserId?: string | null
certImageUrls?: string
remark?: string
mailCertTime?: string | null
collectionProtocolId?: string
}
) {
return request.put({
url: `/web/admin/sample-storage-certs/${encodeURIComponent(sampleCode)}`,
data
})
}
export function fetchSampleInsurance(sampleCode: string) {
return request.get({ url: `/web/admin/sample-insurances/${encodeURIComponent(sampleCode)}` })
}
export function fetchUpdateSampleInsurance(
sampleCode: string,
data: {
handleTime?: string | null
handlerUserId?: string | null
insuranceAttachments?: string
handleRemark?: string
policySendTime?: string | null
collectionProtocolId?: string
}
) {
return request.put({
url: `/web/admin/sample-insurances/${encodeURIComponent(sampleCode)}`,
data
})
}
@@ -1,5 +1,6 @@
<template> <template>
<div class="protocol-handle-panel"> <div class="protocol-handle-panel">
<p v-if="sampleCode" class="handle-sample-hint">当前样本:{{ sampleCode }}</p>
<el-form label-position="top" class="biz-dialog-form" @submit.prevent> <el-form label-position="top" class="biz-dialog-form" @submit.prevent>
<div class="form-grid-2"> <div class="form-grid-2">
<el-form-item label="办理时间" required> <el-form-item label="办理时间" required>
@@ -31,6 +32,26 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="mode === 'storage-cert'" label="邮寄储存证时间">
<el-date-picker
v-model="form.mailCertTime"
type="datetime"
placeholder="可选"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
clearable
/>
</el-form-item>
<el-form-item v-if="mode === 'insurance'" label="保单发送时间">
<el-date-picker
v-model="form.policySendTime"
type="datetime"
placeholder="可选"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
clearable
/>
</el-form-item>
<el-form-item :label="attachmentLabel" class="form-col-span-2"> <el-form-item :label="attachmentLabel" class="form-col-span-2">
<div class="attachment-upload"> <div class="attachment-upload">
<div class="attachment-list"> <div class="attachment-list">
@@ -78,7 +99,11 @@
import { fetchUserList } from '@/api/admin' import { fetchUserList } from '@/api/admin'
import { fetchUploadAttachment } from '@/api/business' import { fetchUploadAttachment } from '@/api/business'
import { import {
fetchSampleInsurance,
fetchSampleStorageCert,
fetchUpdateFullmoonHandle, fetchUpdateFullmoonHandle,
fetchUpdateSampleInsurance,
fetchUpdateSampleStorageCert,
fetchUpdateStorageCertHandle fetchUpdateStorageCertHandle
} from '@/api/sample-info-handle' } from '@/api/sample-info-handle'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
@@ -88,6 +113,8 @@
const props = defineProps<{ const props = defineProps<{
mode: 'storage-cert' | 'insurance' mode: 'storage-cert' | 'insurance'
protocolId: string protocolId: string
/** 有值时按样本级读写(客户服务管理入口) */
sampleCode?: string
initial?: Record<string, unknown> | null initial?: Record<string, unknown> | null
}>() }>()
@@ -101,13 +128,17 @@
const form = reactive({ const form = reactive({
handleTime: '' as string, handleTime: '' as string,
handlerUserId: '' as string, handlerUserId: '' as string,
remark: '' remark: '',
mailCertTime: '',
policySendTime: ''
}) })
const attachmentLabel = computed(() => const attachmentLabel = computed(() =>
props.mode === 'storage-cert' ? '储存证附件' : '保险附件' props.mode === 'storage-cert' ? '储存证附件' : '保险附件'
) )
const useSampleLevel = computed(() => Boolean((props.sampleCode || '').trim()))
function urlsToFiles(raw: string) { function urlsToFiles(raw: string) {
return String(raw || '') return String(raw || '')
.split(',') .split(',')
@@ -120,32 +151,55 @@
})) }))
} }
function applyInitial() { function applyInitial(init: Record<string, unknown>) {
const init = props.initial || {}
form.handleTime = String(init.handleTime || '') form.handleTime = String(init.handleTime || '')
form.handlerUserId = String(init.handlerUserId || '') form.handlerUserId = String(init.handlerUserId || '')
form.remark = String( form.remark = String(
props.mode === 'storage-cert' ? init.remark || '' : init.handleRemark || '' props.mode === 'storage-cert' ? init.remark || '' : init.handleRemark || init.remark || ''
) )
form.mailCertTime = String(init.mailCertTime || '')
form.policySendTime = String(init.policySendTime || '')
const urlField = props.mode === 'storage-cert' ? 'certImageUrls' : 'insuranceAttachments' const urlField = props.mode === 'storage-cert' ? 'certImageUrls' : 'insuranceAttachments'
files.value = urlsToFiles(String(init[urlField] || '')) files.value = urlsToFiles(String(init[urlField] || ''))
const uid = form.handlerUserId || String(userStore.info?.userId || '') const uid = form.handlerUserId || String((userStore.getUserInfo as any)?.id || '')
if (!form.handlerUserId && !form.handleTime) { if (!form.handlerUserId && !form.handleTime) {
form.handlerUserId = uid form.handlerUserId = uid
} }
if (form.handlerUserId) { if (form.handlerUserId) {
const name = String(init.handlerUserName || userStore.info?.userName || '') const name = String(
init.handlerUserName ||
(userStore.getUserInfo as any)?.realName ||
(userStore.getUserInfo as any)?.userName ||
''
)
if (name) { if (name) {
userOptions.value = [ userOptions.value = [{ id: form.handlerUserId, nickName: name, userName: name }]
{ id: form.handlerUserId, nickName: name, userName: name }
]
} }
} }
} }
async function loadSampleInitial() {
const code = (props.sampleCode || '').trim()
if (!code) {
applyInitial(props.initial || {})
return
}
try {
const res =
props.mode === 'storage-cert'
? ((await fetchSampleStorageCert(code)) as Record<string, unknown>)
: ((await fetchSampleInsurance(code)) as Record<string, unknown>)
applyInitial(res || {})
} catch {
applyInitial(props.initial || {})
}
}
watch( watch(
() => [props.protocolId, props.initial], () => [props.protocolId, props.sampleCode, props.initial, props.mode],
() => applyInitial(), () => {
void loadSampleInitial()
},
{ immediate: true, deep: true } { immediate: true, deep: true }
) )
@@ -182,11 +236,33 @@
ElMessage.warning('填写办理时间时必须指定办理人员') ElMessage.warning('填写办理时间时必须指定办理人员')
return return
} }
if (!props.protocolId) return const code = (props.sampleCode || '').trim()
if (useSampleLevel.value && !code) return
if (!useSampleLevel.value && !props.protocolId) return
saving.value = true saving.value = true
try { try {
const urls = files.value.map((f) => f.fileUrl).filter(Boolean).join(',') const urls = files.value.map((f) => f.fileUrl).filter(Boolean).join(',')
if (props.mode === 'storage-cert') { if (useSampleLevel.value) {
if (props.mode === 'storage-cert') {
await fetchUpdateSampleStorageCert(code, {
handleTime: form.handleTime || null,
handlerUserId: form.handleTime ? form.handlerUserId : null,
certImageUrls: urls,
remark: form.remark,
mailCertTime: form.mailCertTime || null,
collectionProtocolId: props.protocolId
})
} else {
await fetchUpdateSampleInsurance(code, {
handleTime: form.handleTime || null,
handlerUserId: form.handleTime ? form.handlerUserId : null,
insuranceAttachments: urls,
handleRemark: form.remark,
policySendTime: form.policySendTime || null,
collectionProtocolId: props.protocolId
})
}
} else if (props.mode === 'storage-cert') {
await fetchUpdateStorageCertHandle(props.protocolId, { await fetchUpdateStorageCertHandle(props.protocolId, {
handleTime: form.handleTime || null, handleTime: form.handleTime || null,
handlerUserId: form.handleTime ? form.handlerUserId : null, handlerUserId: form.handleTime ? form.handlerUserId : null,
@@ -210,6 +286,12 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.handle-sample-hint {
margin: 0 0 10px;
font-size: 13px;
color: var(--mint-text-muted, #64748b);
}
.handle-actions { .handle-actions {
margin-top: 8px; margin-top: 8px;
} }
@@ -256,6 +256,7 @@
v-if="protocolId" v-if="protocolId"
mode="storage-cert" mode="storage-cert"
:protocol-id="protocolId" :protocol-id="protocolId"
:sample-code="highlightSampleCode || undefined"
:initial="storageCertInfo" :initial="storageCertInfo"
@saved="loadDetail" @saved="loadDetail"
/> />
@@ -270,6 +271,7 @@
v-if="protocolId" v-if="protocolId"
mode="insurance" mode="insurance"
:protocol-id="protocolId" :protocol-id="protocolId"
:sample-code="highlightSampleCode || undefined"
:initial="fullmoonInsuranceInfo" :initial="fullmoonInsuranceInfo"
@saved="loadDetail" @saved="loadDetail"
/> />