From e1fe818dcec373e3e858121ff2202bdd4ea92b21 Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 22 Jul 2026 18:00:17 +0800 Subject: [PATCH] feat: integrate Luoyang pharmacy workflow --- admin/src/api/tcm.ts | 15 + .../components/medicine-name-select/index.vue | 63 +- .../src/components/tcm-prescription/index.vue | 35 +- .../GancaoSubmissionReconcileButton.vue | 129 ++++ .../PrescriptionOrderDetailDrawer.vue | 7 + .../components/prescription-order-utils.ts | 41 ++ .../src/views/consumer/prescription/index.vue | 45 +- .../src/views/consumer/prescription/list.vue | 8 +- .../consumer/prescription/order_list.vue | 117 ++-- .../consumer/prescription/order_list_h5.vue | 107 ++-- .../components/prescription-drawer.vue | 4 +- .../tcm/PrescriptionOrderController.php | 38 +- .../http/middleware/AuthMiddleware.php | 8 +- .../lists/tcm/PrescriptionOrderLists.php | 49 +- .../logic/tcm/PrescriptionLibraryLogic.php | 24 +- .../adminapi/logic/tcm/PrescriptionLogic.php | 115 +++- .../logic/tcm/PrescriptionOrderLogic.php | 570 +++++++++++++++++- .../tcm/PrescriptionOrderValidate.php | 7 +- .../EjPharmacyCallbackController.php | 455 ++++++++++++++ server/app/api/route/app.php | 3 +- .../common/service/ExpressTrackingService.php | 1 + .../pharmacy/EjPharmacyShipmentPolicy.php | 36 +- .../PharmacySubmissionClaimService.php | 58 +- .../format_luoyang_pharmacy_workflow_logs.sql | 9 + .../luoyang_pharmacy_erp_status_rollback.sql | 15 + ...anslate_luoyang_pharmacy_callback_logs.sql | 72 +++ .../pharmacy/callback_auth_integration.php | 57 +- 27 files changed, 1925 insertions(+), 163 deletions(-) create mode 100644 admin/src/views/consumer/prescription/components/GancaoSubmissionReconcileButton.vue create mode 100644 server/app/api/controller/EjPharmacyCallbackController.php create mode 100644 server/sql/1.9.20260722/format_luoyang_pharmacy_workflow_logs.sql create mode 100644 server/sql/1.9.20260722/luoyang_pharmacy_erp_status_rollback.sql create mode 100644 server/sql/1.9.20260722/translate_luoyang_pharmacy_callback_logs.sql diff --git a/admin/src/api/tcm.ts b/admin/src/api/tcm.ts index d1215643..534ac61c 100644 --- a/admin/src/api/tcm.ts +++ b/admin/src/api/tcm.ts @@ -528,6 +528,21 @@ export function prescriptionOrderSubmitGancaoRecipel(params: { id: number }) { return request.post({ url: '/tcm.prescriptionOrder/submitGancaoRecipel', params }) } +/** 按业务订单 ship_mode 上传:gancao 走甘草,direct 走洛阳药房 ERP */ +export function prescriptionOrderUploadToPharmacy(params: { id: number }) { + return request.post({ url: '/tcm.prescriptionOrder/uploadToPharmacy', params }) +} + +/** 人工核对甘草不确定提交结果。 */ +export function prescriptionOrderConfirmGancaoSubmission(params: { + id: number + resolution: 'CONFIRM_SUCCESS' | 'CONFIRM_NOT_CREATED' + remote_order_no?: string + note: string +}) { + return request.post({ url: '/tcm.prescriptionOrder/confirmGancaoSubmission', params }) +} + /** 甘草药管家:预下单测试(仅 CTM_PREVIEW,不提交订单) */ export function prescriptionOrderPreviewGancaoRecipel(params: { id: number diff --git a/admin/src/components/medicine-name-select/index.vue b/admin/src/components/medicine-name-select/index.vue index 2b981767..bd9cd72c 100644 --- a/admin/src/components/medicine-name-select/index.vue +++ b/admin/src/components/medicine-name-select/index.vue @@ -2,7 +2,7 @@ {{ modelValue || '—' }} - + +
+ {{ item.name }} + + {{ item.id > 0 ? [item.supplier, item.unit, `ID ${item.id}`].filter(Boolean).join(' · ') : '历史名称,请重新选择' }} + +
+
@@ -24,6 +31,7 @@ import { medicineLists } from '@/api/medicine' const props = withDefaults( defineProps<{ modelValue: string + medicineId?: number | null disabled?: boolean }>(), { disabled: false } @@ -31,18 +39,30 @@ const props = withDefaults( const emit = defineEmits<{ 'update:modelValue': [value: string] + 'update:medicineId': [value: number | undefined] }>() const loading = ref(false) -const options = ref<{ id: number; name: string }[]>([]) +type MedicineOption = { id: number; name: string; supplier?: string; unit?: string } +const options = ref([]) + +const selectedMedicineId = computed(() => { + const id = Number(props.medicineId) + if (Number.isInteger(id) && id > 0) { + return id + } + return (props.modelValue || '').trim() ? 0 : '' +}) function ensureCurrentInOptions() { const v = (props.modelValue || '').trim() if (!v) { return } - if (!options.value.some((o) => o.name === v)) { - options.value = [{ id: 0, name: v }, ...options.value] + const id = Number(props.medicineId) + const currentId = Number.isInteger(id) && id > 0 ? id : 0 + if (!options.value.some((o) => o.id === currentId)) { + options.value = [{ id: currentId, name: v }, ...options.value] } } @@ -56,7 +76,7 @@ const remoteMethod = async (query: string) => { page_size: 100, status: 1 }) - options.value = res.lists || [] + options.value = (res.lists || []) as MedicineOption[] ensureCurrentInOptions() } catch (e) { console.error(e) @@ -72,13 +92,19 @@ const onVisibleChange = (open: boolean) => { } } -const onUpdate = (val: string) => { - emit('update:modelValue', val || '') +const onUpdate = (val: number | string) => { + const id = Number(val) + const selected = id > 0 ? options.value.find((item) => item.id === id) : undefined + emit('update:modelValue', selected?.name || '') + emit('update:medicineId', selected?.id) } watch( - () => props.modelValue, + () => [props.modelValue, props.medicineId], () => { + if (!(props.modelValue || '').trim() && Number(props.medicineId) > 0) { + emit('update:medicineId', undefined) + } ensureCurrentInOptions() }, { immediate: true } @@ -93,4 +119,23 @@ watch( .medicine-name-select.w-full { width: 100%; } + +.medicine-name-option { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 60%); + align-items: center; + gap: 16px; + + > span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.medicine-name-option__meta { + color: var(--el-text-color-secondary); + font-size: 12px; + text-align: right; +} diff --git a/admin/src/components/tcm-prescription/index.vue b/admin/src/components/tcm-prescription/index.vue index 018d9f52..d65e3e99 100644 --- a/admin/src/components/tcm-prescription/index.vue +++ b/admin/src/components/tcm-prescription/index.vue @@ -139,7 +139,12 @@ :class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }" >
主方 {{ row.index + 1 }}
- +
辅方
- +
0) { + row.medicine_id = medicineId + } if (raw?.locked === true || raw?.locked === 1) { row.locked = true } @@ -1578,8 +1593,8 @@ function parseRecipeToHerbs(text: string): { name: string; dosage: number }[] { return herbs } -/** 仅在药品库中存在「完全同名」药材时返回规范药名,否则返回 null(不模糊猜测) */ -async function resolveHerbNameFromLibrary(rawName: string): Promise { +/** 仅有一条完全同名记录时返回稳定药材身份,否则不猜测。 */ +async function resolveHerbFromLibrary(rawName: string): Promise<{ medicine_id: number; name: string } | null> { const q = rawName.trim() if (!q) return null try { @@ -1590,8 +1605,10 @@ async function resolveHerbNameFromLibrary(rawName: string): Promise (item.name ?? '').trim() === q) - return exact ? exact.name.trim() : null + const exact = lists.filter((item) => (item.name ?? '').trim() === q) + return exact.length === 1 + ? { medicine_id: Number(exact[0].id), name: exact[0].name.trim() } + : null } catch { return null } @@ -1612,12 +1629,12 @@ async function handlePasteRecipeImport() { const resolved: Herb[] = [] const skippedNames: string[] = [] for (const row of parsed) { - const name = await resolveHerbNameFromLibrary(row.name) - if (!name) { + const medicine = await resolveHerbFromLibrary(row.name) + if (!medicine) { skippedNames.push(row.name.trim()) continue } - resolved.push({ name, dosage: row.dosage, formula_type: '主方' }) + resolved.push({ ...medicine, dosage: row.dosage, formula_type: '主方' }) } const skippedUnique = [...new Set(skippedNames.filter(Boolean))] if (resolved.length === 0) { diff --git a/admin/src/views/consumer/prescription/components/GancaoSubmissionReconcileButton.vue b/admin/src/views/consumer/prescription/components/GancaoSubmissionReconcileButton.vue new file mode 100644 index 00000000..06b9025b --- /dev/null +++ b/admin/src/views/consumer/prescription/components/GancaoSubmissionReconcileButton.vue @@ -0,0 +1,129 @@ + + + diff --git a/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue b/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue index 053b7c2c..d2e25fc0 100644 --- a/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue +++ b/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue @@ -30,6 +30,13 @@ round class="ml-2" >甘草 {{ detailData.gancao_reciperl_order_no }} + 洛阳 {{ detailData.ej_pharmacy_order_no }}
diff --git a/admin/src/views/consumer/prescription/components/prescription-order-utils.ts b/admin/src/views/consumer/prescription/components/prescription-order-utils.ts index cce70c16..8113b916 100644 --- a/admin/src/views/consumer/prescription/components/prescription-order-utils.ts +++ b/admin/src/views/consumer/prescription/components/prescription-order-utils.ts @@ -11,6 +11,45 @@ export const TCM_ASSISTANT_ROLE_ID = 2 /** 与 server/config/project.php prescription_audit_roles 默认一致,可处方审核的角色 */ export const PRESCRIPTION_AUDIT_ROLE_IDS = [0, 3, 6] +export function isRemoteSnapshotLocked(row: Record | null | undefined): boolean { + if (!row) return false + if (String(row.gancao_reciperl_order_no || '').trim()) return true + if (String(row.ej_pharmacy_order_no || '').trim()) return true + if (Number(row.gancao_submit_time || 0) > 0 || Number(row.ej_pharmacy_submit_time || 0) > 0) { + return true + } + return ['PENDING', 'UNKNOWN', 'PENDING_RECONCILE', 'SUCCESS'].includes( + String(row.pharmacy_claim_status || '').toUpperCase() + ) +} + +export type SupplyMode = 'gancao' | 'direct' | 'self' + +export function supplyModeKey(row: Record | null | undefined): SupplyMode { + if ( + String(row?.ship_mode || '') + .trim() + .toLowerCase() === 'direct' + ) + return 'direct' + if (String(row?.gancao_reciperl_order_no || '').trim()) return 'gancao' + return 'self' +} + +export function supplyModeLabel(row: Record | null | undefined): string { + const mode = supplyModeKey(row) + if (mode === 'direct') return '洛阳直发' + return mode === 'gancao' ? '甘草' : '自营' +} + +export function supplyModeTagType( + row: Record | null | undefined +): 'success' | 'warning' | 'info' { + const mode = supplyModeKey(row) + if (mode === 'direct') return 'warning' + return mode === 'gancao' ? 'success' : 'info' +} + export function formatTime(v: unknown) { if (v === null || v === undefined || v === '') return '—' if (typeof v === 'number' && v > 1e9 && v < 1e11) { @@ -134,6 +173,8 @@ export function logActionText(act: string) { revoke_rx_audit: '撤回处方审核', revoke_pay_audit: '撤回支付审核', gancao_submit: '甘草下单', + ej_pharmacy_submit: '洛阳药房下单', + ej_pharmacy_callback: '洛阳药房状态', patch_rx_patient: '处方患者信息', patch_rx_usage: '服用参数', update_amount: '修改订单金额', diff --git a/admin/src/views/consumer/prescription/index.vue b/admin/src/views/consumer/prescription/index.vue index c3efe1f3..3bb92825 100644 --- a/admin/src/views/consumer/prescription/index.vue +++ b/admin/src/views/consumer/prescription/index.vue @@ -696,7 +696,12 @@ @@ -732,7 +737,12 @@ @@ -1294,6 +1304,14 @@
+ + + + 甘草药房 + 洛阳药房 + + + @@ -1709,7 +1727,7 @@ import jsPDF from 'jspdf' const TcmDiagnosisEditView = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue')) type FormulaType = '主方' | '辅方' -type HerbRow = { name: string; dosage: number; formula_type: FormulaType; locked?: boolean } +type HerbRow = { medicine_id?: number; name: string; dosage: number; formula_type: FormulaType; locked?: boolean } type AuxUsageForm = { dosage_amount?: number @@ -1814,6 +1832,10 @@ function normalizeHerbRow(raw: any): HerbRow { dosage: Number(raw?.dosage) || 0, formula_type: normalizeFormulaType(raw?.formula_type) } + const medicineId = Number(raw?.medicine_id ?? raw?.id ?? 0) + if (Number.isInteger(medicineId) && medicineId > 0) { + row.medicine_id = medicineId + } if (raw?.locked === true || raw?.locked === 1) { row.locked = true } @@ -2006,6 +2028,7 @@ const createOrderForm = reactive({ service_package: [] as string[], express_company: 'auto', tracking_number: '', + ship_mode: 'gancao' as 'gancao' | 'direct', fee_type: 3, amount: 0, internal_cost: undefined as number | undefined, @@ -2252,6 +2275,7 @@ function resetCreateOrderForm() { createOrderForm.service_package = [] createOrderForm.express_company = 'auto' createOrderForm.tracking_number = '' + createOrderForm.ship_mode = 'gancao' createOrderForm.fee_type = 3 createOrderForm.amount = 0 createOrderForm.internal_cost = undefined @@ -2425,6 +2449,7 @@ async function submitCreateOrderFromPrescription() { : '', express_company: createOrderForm.express_company || 'auto', tracking_number: createOrderForm.tracking_number || '', + ship_mode: createOrderForm.ship_mode, fee_type: createOrderForm.fee_type, amount: createOrderForm.amount, remark_extra: createOrderForm.remark_extra || '', @@ -3601,7 +3626,7 @@ function parseRecipePasteToHerbs(text: string): { name: string; dosage: number } return herbs } -async function resolvePasteHerbNameFromLibrary(rawName: string): Promise { +async function resolvePasteHerbFromLibrary(rawName: string): Promise<{ medicine_id: number; name: string } | null> { const q = rawName.trim() if (!q) return null try { @@ -3612,8 +3637,10 @@ async function resolvePasteHerbNameFromLibrary(rawName: string): Promise (item.name ?? '').trim() === q) - return exact ? exact.name.trim() : null + const exact = lists.filter((item) => (item.name ?? '').trim() === q) + return exact.length === 1 + ? { medicine_id: Number(exact[0].id), name: exact[0].name.trim() } + : null } catch { return null } @@ -3634,12 +3661,12 @@ async function handlePasteRecipeImport() { const resolved: HerbRow[] = [] const skippedNames: string[] = [] for (const row of parsed) { - const name = await resolvePasteHerbNameFromLibrary(row.name) - if (!name) { + const medicine = await resolvePasteHerbFromLibrary(row.name) + if (!medicine) { skippedNames.push(row.name.trim()) continue } - resolved.push({ name, dosage: row.dosage, formula_type: '主方' }) + resolved.push({ ...medicine, dosage: row.dosage, formula_type: '主方' }) } const skippedUnique = [...new Set(skippedNames.filter(Boolean))] if (resolved.length === 0) { diff --git a/admin/src/views/consumer/prescription/list.vue b/admin/src/views/consumer/prescription/list.vue index b182d2a9..9992724e 100644 --- a/admin/src/views/consumer/prescription/list.vue +++ b/admin/src/views/consumer/prescription/list.vue @@ -157,7 +157,11 @@ @@ -257,7 +261,7 @@ const editForm = reactive({ id: 0, prescription_name: '', formula_type: '主方', - herbs: [] as Array<{ name: string; dosage: number }>, + herbs: [] as Array<{ medicine_id?: number; name: string; dosage: number }>, is_public: 0, disable_edit: 0 }) diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index b8408da2..7dba0f51 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -373,9 +373,9 @@ - {{ String(row.gancao_reciperl_order_no || '').trim() ? '甘草' : '自营' }} + {{ supplyModeLabel(row) }} #{{ row.id }}
@@ -594,13 +594,13 @@ @click="confirmWithdraw(row)" >撤回 上传药方 + :loading="pharmacySubmitId === row.id" + @click="confirmUploadToPharmacy(row)" + >上传药房
@@ -638,7 +638,7 @@ 甘草药房 洛阳药房