This commit is contained in:
Your Name
2026-05-14 15:34:28 +08:00
parent e1df584ed5
commit e35696e153
6 changed files with 266 additions and 63 deletions
@@ -272,7 +272,7 @@
:fetch-fun="prescriptionOrderExport" :fetch-fun="prescriptionOrderExport"
:params="prescriptionOrderExportParams" :params="prescriptionOrderExportParams"
:page-size="pager.size" :page-size="pager.size"
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。" export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:按诊单关联患者取最近一条挂号的渠道字典与细分。"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -1377,7 +1377,7 @@
<!-- 编辑分步向导与创建业务订单弹窗统一 --> <!-- 编辑分步向导与创建业务订单弹窗统一 -->
<el-dialog <el-dialog
v-model="editVisible" v-model="editVisible"
title="编辑业务订单" :title="editGancaoLogisticsOnlyMode ? '修改物流信息(甘草订单)' : '编辑业务订单'"
width="940px" width="940px"
top="4vh" top="4vh"
:close-on-click-modal="false" :close-on-click-modal="false"
@@ -1400,18 +1400,33 @@
</el-tooltip> </el-tooltip>
</div> </div>
<el-steps <el-alert
:active="editOrderStep" v-if="editGancaoLogisticsOnlyMode"
finish-status="success" type="info"
align-center :closable="false"
class="create-order-steps" show-icon
class="mb-4"
title="订单已提交甘草,仅可修改快递单号与承运商;地址、金额等变更请先走取消流程。"
> >
<el-step title="患者与收货" description="诊单与物流地址" /> <template v-if="editGancaoDisplayNo" #default>
<el-step title="服务与支付单" description="套餐与关联收款" /> <div class="text-sm text-gray-700 mt-1">甘草处方单号{{ editGancaoDisplayNo }}</div>
<el-step title="金额与确认" description="费用与备注" /> </template>
</el-steps> </el-alert>
<p class="create-order-step-lead">{{ editOrderStepLead }}</p> <template v-if="!editGancaoLogisticsOnlyMode">
<el-steps
:active="editOrderStep"
finish-status="success"
align-center
class="create-order-steps"
>
<el-step title="患者与收货" description="诊单与物流地址" />
<el-step title="服务与支付单" description="套餐与关联收款" />
<el-step title="金额与确认" description="费用与备注" />
</el-steps>
<p class="create-order-step-lead">{{ editOrderStepLead }}</p>
</template>
<el-form <el-form
ref="editFormRef" ref="editFormRef"
@@ -1420,6 +1435,24 @@
label-width="108px" label-width="108px"
class="create-order-form" class="create-order-form"
> >
<template v-if="editGancaoLogisticsOnlyMode">
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="物流设置">
<div class="flex gap-2 w-full">
<el-select v-model="editForm.express_company" placeholder="承运商" class="w-[140px] shrink-0">
<el-option label="自动识别" value="auto" />
<el-option label="顺丰速运" value="sf" />
<el-option label="京东快递" value="jd" />
<el-option label="极兔速递" value="jt" />
</el-select>
<el-input v-model="editForm.tracking_number" maxlength="80" placeholder="快递单号" class="flex-1 min-w-0" />
</div>
</el-form-item>
</el-col>
</el-row>
</template>
<template v-if="!editGancaoLogisticsOnlyMode">
<div v-show="editOrderStep === 0" class="create-order-step-panel"> <div v-show="editOrderStep === 0" class="create-order-step-panel">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
@@ -1743,12 +1776,13 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</template>
</el-form> </el-form>
</div> </div>
<template #footer> <template #footer>
<div class="create-order-footer"> <div class="create-order-footer">
<div class="create-order-footer__hint"> <div v-if="!editGancaoLogisticsOnlyMode" class="create-order-footer__hint">
<span class="create-order-footer__step"> {{ editOrderStep + 1 }} / 3 </span> <span class="create-order-footer__step"> {{ editOrderStep + 1 }} / 3 </span>
<span v-if="editOrderStep === 2 && editDepositMin > 0" class="create-order-footer__warn"> <span v-if="editOrderStep === 2 && editDepositMin > 0" class="create-order-footer__warn">
订单金额与关联支付单合计须 ¥{{ formatMoney(editDepositMin) }} 订单金额与关联支付单合计须 ¥{{ formatMoney(editDepositMin) }}
@@ -1756,17 +1790,19 @@
</div> </div>
<div class="create-order-footer__actions"> <div class="create-order-footer__actions">
<el-button size="large" @click="editVisible = false">取消</el-button> <el-button size="large" @click="editVisible = false">取消</el-button>
<el-button v-if="editOrderStep > 0" size="large" @click="goEditOrderPrevStep">上一步</el-button> <template v-if="!editGancaoLogisticsOnlyMode">
<el-button v-if="editOrderStep > 0" size="large" @click="goEditOrderPrevStep">上一步</el-button>
<el-button
v-if="editOrderStep < 2"
type="primary"
size="large"
@click="goEditOrderNextStep"
>
下一步
</el-button>
</template>
<el-button <el-button
v-if="editOrderStep < 2" v-if="editGancaoLogisticsOnlyMode || editOrderStep === 2"
type="primary"
size="large"
@click="goEditOrderNextStep"
>
下一步
</el-button>
<el-button
v-if="editOrderStep === 2"
type="primary" type="primary"
size="large" size="large"
:loading="editSaving" :loading="editSaving"
@@ -3234,14 +3270,21 @@ function canEditRow(row: {
gancao_reciperl_order_no?: string | null gancao_reciperl_order_no?: string | null
gancao_submit_time?: number | null gancao_submit_time?: number | null
}) { }) {
// (5)(6)(3)(4)
const fs = Number(row.fulfillment_status) const fs = Number(row.fulfillment_status)
if (fs !== 1 && fs !== 2) return false // (3)(4)
// > 0 if (fs === 3 || fs === 4) return false
const gcNo = String(row.gancao_reciperl_order_no || '').trim() const gcNo = String(row.gancao_reciperl_order_no || '').trim()
const gcTime = Number(row.gancao_submit_time || 0) const gcTime = Number(row.gancao_submit_time || 0)
if (gcNo !== '' || gcTime > 0) return false const gcLocked = gcNo !== '' || gcTime > 0
return true
// /// 6 edit
if (gcLocked) {
return [1, 2, 5, 7].includes(fs)
}
// (1)(2)
return fs === 1 || fs === 2
} }
function canRxAudit(row: { prescription_audit_status?: number; fulfillment_status?: number }) { function canRxAudit(row: { prescription_audit_status?: number; fulfillment_status?: number }) {
@@ -3310,9 +3353,8 @@ function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_s
} }
function canQuickTrackRow(row: { fulfillment_status?: number }) { function canQuickTrackRow(row: { fulfillment_status?: number }) {
// (5) / (6) 使 // (5) (6)
const fs = Number(row.fulfillment_status) return Number(row.fulfillment_status) === 5
return fs === 5 || fs === 6
} }
function canUpdateAmount(row: { id?: number; fulfillment_status?: number } | null | undefined) { function canUpdateAmount(row: { id?: number; fulfillment_status?: number } | null | undefined) {
@@ -3948,6 +3990,10 @@ const editSaving = ref(false)
const editOrderStep = ref(0) const editOrderStep = ref(0)
/** 顶部「关联处方」卡片数据(来自业务订单详情中的 prescription */ /** 顶部「关联处方」卡片数据(来自业务订单详情中的 prescription */
const editOrderPrescription = ref<Record<string, any> | null>(null) const editOrderPrescription = ref<Record<string, any> | null>(null)
/** 甘草 SCM 已提交:弹窗仅展示并提交快递单号与承运商 */
const editGancaoLogisticsOnlyMode = ref(false)
/** 用于提示文案展示甘草处方单号 */
const editGancaoDisplayNo = ref('')
const editOrderStepLead = computed(() => { const editOrderStepLead = computed(() => {
const texts = [ const texts = [
@@ -4126,6 +4172,8 @@ const editRules = computed<FormRules>(() => {
function resetEditOrderDialog() { function resetEditOrderDialog() {
editOrderStep.value = 0 editOrderStep.value = 0
editOrderPrescription.value = null editOrderPrescription.value = null
editGancaoLogisticsOnlyMode.value = false
editGancaoDisplayNo.value = ''
editFormRef.value?.clearValidate() editFormRef.value?.clearValidate()
} }
@@ -4245,7 +4293,12 @@ async function openEdit(row: { id: number }) {
editForm.pay_order_ids = Array.isArray(pids) ? pids.map((x: unknown) => Number(x)).filter((n) => !Number.isNaN(n)) : [] editForm.pay_order_ids = Array.isArray(pids) ? pids.map((x: unknown) => Number(x)).filter((n) => !Number.isNaN(n)) : []
editForm.internal_cost = editForm.internal_cost =
d.internal_cost != null && d.internal_cost !== '' ? Number(d.internal_cost) : undefined d.internal_cost != null && d.internal_cost !== '' ? Number(d.internal_cost) : undefined
const gcNo = String(d.gancao_reciperl_order_no || '').trim()
const gcTime = Number(d.gancao_submit_time || 0)
editGancaoLogisticsOnlyMode.value = gcNo !== '' || gcTime > 0
editGancaoDisplayNo.value = gcNo || ''
await loadEditPaidOrders(editForm.diagnosis_id, editForm.id) await loadEditPaidOrders(editForm.diagnosis_id, editForm.id)
// BUG FIX: NextTick // BUG FIX: NextTick
@@ -4261,8 +4314,14 @@ async function openEdit(row: { id: number }) {
} }
async function submitEdit() { async function submitEdit() {
if (!editFormRef.value || editOrderStep.value !== 2) return if (!editFormRef.value) return
await editFormRef.value.validate() const gancaoLogisticsOnly = editGancaoLogisticsOnlyMode.value
if (!gancaoLogisticsOnly && editOrderStep.value !== 2) return
if (!gancaoLogisticsOnly) {
await editFormRef.value.validate()
} else {
editFormRef.value.clearValidate()
}
editSaving.value = true editSaving.value = true
try { try {
const payload: Record<string, unknown> = { const payload: Record<string, unknown> = {
@@ -4634,8 +4693,8 @@ async function submitQuickTrack() {
if (nd) detailData.value = nd if (nd) detailData.value = nd
} catch { /* 静默 */ } } catch { /* 静默 */ }
} }
// // (2)(5)/(6)
if (canShipRow({ fulfillment_status: 2, tracking_number: quickTrackForm.tracking_number })) { if (canShipRow({ fulfillment_status: Number(d.fulfillment_status) })) {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`快递单号「${quickTrackForm.tracking_number}」已保存,是否立即确认发货?`, `快递单号「${quickTrackForm.tracking_number}」已保存,是否立即确认发货?`,
@@ -3052,14 +3052,18 @@ function canEditRow(row: {
gancao_reciperl_order_no?: string | null gancao_reciperl_order_no?: string | null
gancao_submit_time?: number | null gancao_submit_time?: number | null
}) { }) {
// (5)(6)(3)(4)
const fs = Number(row.fulfillment_status) const fs = Number(row.fulfillment_status)
if (fs !== 1 && fs !== 2) return false if (fs === 3 || fs === 4) return false
// > 0
const gcNo = String(row.gancao_reciperl_order_no || '').trim() const gcNo = String(row.gancao_reciperl_order_no || '').trim()
const gcTime = Number(row.gancao_submit_time || 0) const gcTime = Number(row.gancao_submit_time || 0)
if (gcNo !== '' || gcTime > 0) return false const gcLocked = gcNo !== '' || gcTime > 0
return true
if (gcLocked) {
return [1, 2, 5, 7].includes(fs)
}
return fs === 1 || fs === 2
} }
function canRxAudit(row: { prescription_audit_status?: number; fulfillment_status?: number }) { function canRxAudit(row: { prescription_audit_status?: number; fulfillment_status?: number }) {
@@ -3128,9 +3132,7 @@ function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_s
} }
function canQuickTrackRow(row: { fulfillment_status?: number }) { function canQuickTrackRow(row: { fulfillment_status?: number }) {
// (5) / (6) 使 return Number(row.fulfillment_status) === 5
const fs = Number(row.fulfillment_status)
return fs === 5 || fs === 6
} }
function canUploadGancaoRow(row: { function canUploadGancaoRow(row: {
@@ -4312,8 +4314,8 @@ async function submitQuickTrack() {
if (nd) detailData.value = nd if (nd) detailData.value = nd
} catch { /* 静默 */ } } catch { /* 静默 */ }
} }
// // (2)(5)/(6)
if (canShipRow({ fulfillment_status: 2, tracking_number: quickTrackForm.tracking_number })) { if (canShipRow({ fulfillment_status: Number(d.fulfillment_status) })) {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`快递单号「${quickTrackForm.tracking_number}」已保存,是否立即确认发货?`, `快递单号「${quickTrackForm.tracking_number}」已保存,是否立即确认发货?`,
@@ -109,17 +109,24 @@ class AuthMiddleware
*/ */
private function matchPermissionAlias(string $accessUri, array $adminUris): bool private function matchPermissionAlias(string $accessUri, array $adminUris): bool
{ {
if (!in_array('tcm.diagnosis/dailyrecord', $adminUris, true)) { if (in_array('tcm.diagnosis/dailyrecord', $adminUris, true)
return false; && in_array($accessUri, [
'tcm.diagnosistodo/lists',
'tcm.diagnosistodo/add',
'tcm.diagnosistodo/cancel',
'tcm.diagnosis/trackingnotes',
'tcm.diagnosis/addtrackingnote',
], true)) {
return true;
} }
return in_array($accessUri, [ // 导出与列表共用 PrescriptionOrderLists 数据域;角色漏勾「导出订单」子权限时仍返回 权限不足
'tcm.diagnosistodo/lists', if ($accessUri === 'tcm.prescriptionorder/export'
'tcm.diagnosistodo/add', && in_array('tcm.prescriptionorder/lists', $adminUris, true)) {
'tcm.diagnosistodo/cancel', return true;
'tcm.diagnosis/trackingnotes', }
'tcm.diagnosis/addtrackingnote',
], true); return false;
} }
/** /**
@@ -626,6 +626,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_center_label' => '一中心还是二中心', 'export_center_label' => '一中心还是二中心',
'export_assistant_dept' => '医助部门', 'export_assistant_dept' => '医助部门',
'export_channel' => '渠道', 'export_channel' => '渠道',
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
'export_medication_form' => '药品形态', 'export_medication_form' => '药品形态',
'export_medication_days' => '天数', 'export_medication_days' => '天数',
'export_amount' => '总金额', 'export_amount' => '总金额',
@@ -14,6 +14,7 @@ use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder; use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderLog; use app\common\model\tcm\PrescriptionOrderLog;
use app\common\model\tcm\PrescriptionOrderPayOrder; use app\common\model\tcm\PrescriptionOrderPayOrder;
use app\common\model\dict\DictData;
use app\common\model\doctor\Appointment; use app\common\model\doctor\Appointment;
use app\common\model\auth\Admin; use app\common\model\auth\Admin;
use app\common\service\DataScope\DataScopeService; use app\common\service\DataScope\DataScopeService;
@@ -1441,16 +1442,16 @@ class PrescriptionOrderLogic
return false; return false;
} }
// 已成功提交甘草 SCM(生成了甘草处方单号 或 submit_time>0),再改本地信息会与甘草侧不一致,禁止编辑 if ($fs === 6) {
self::$error = '已签收订单不可修改快递单号';
return false;
}
// 已成功提交甘草 SCM:仅允许更新快递单号与承运商(与甘草侧地址/药方等仍以取消流程为准)
$gcOrderNo = trim((string) $order->gancao_reciperl_order_no); $gcOrderNo = trim((string) $order->gancao_reciperl_order_no);
$gcSubmitTime = (int) $order->gancao_submit_time; $gcSubmitTime = (int) $order->gancao_submit_time;
if ($gcOrderNo !== '' || $gcSubmitTime > 0) { if ($gcOrderNo !== '' || $gcSubmitTime > 0) {
self::$error = sprintf( return self::editGancaoLogisticsOnly($order, $params, $adminId, $adminInfo);
'订单已提交甘草(处方单号:%s),不可再编辑;如需修改请先走取消流程',
$gcOrderNo !== '' ? $gcOrderNo : '—'
);
return false;
} }
$medDays = $params['medication_days'] ?? null; $medDays = $params['medication_days'] ?? null;
@@ -1584,6 +1585,47 @@ class PrescriptionOrderLogic
return $out; return $out;
} }
/**
* 甘草已提交后仅更新物流字段(tracking_number、express_company),忽略金额/地址等其它请求参数。
*
* @param array<string,mixed> $params
* @return array<string,mixed>|false
*/
private static function editGancaoLogisticsOnly(
PrescriptionOrder $order,
array $params,
int $adminId,
array $adminInfo
) {
$order->tracking_number = mb_substr(trim((string) ($params['tracking_number'] ?? '')), 0, 80);
if (array_key_exists('express_company', $params)) {
$order->express_company = self::normalizeExpressCompany($params['express_company']);
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
self::writeLog(
(int) $order->id,
$adminId,
$adminInfo,
'edit',
'甘草订单已提交,仅更新快递信息(单号/承运商)'
);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/** /**
* 确认发货:更新快递信息并将 fulfillment_status 从 2(履约中)推进到 5(已发货) * 确认发货:更新快递信息并将 fulfillment_status 从 2(履约中)推进到 5(已发货)
* *
@@ -2485,6 +2527,60 @@ class PrescriptionOrderLogic
return $type; return $type;
} }
/**
* 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径)
*
* @param array<string, mixed> $apRow doctor_appointment 一行
* @param array<string, string> $channelNameByValue DictData channels value=>name
*/
private static function formatGuahaoChannelSourceForExport(array $apRow, array $channelNameByValue): string
{
$srcKey = trim((string) ($apRow['channel_source'] ?? ''));
if ($srcKey === '' && isset($apRow['channels']) && $apRow['channels'] !== '' && $apRow['channels'] !== null) {
$srcKey = trim((string) $apRow['channels']);
}
$label = '';
if ($srcKey !== '') {
$label = (string) ($channelNameByValue[$srcKey] ?? $channelNameByValue[(string) (int) $srcKey] ?? $srcKey);
}
$detail = trim((string) ($apRow['channel_source_detail'] ?? ''));
if ($label === '' && $detail === '') {
return '';
}
if ($detail !== '') {
return $label !== '' ? "{$label}{$detail}" : $detail;
}
return $label;
}
/**
* 挂号表渠道相关列是否存在(未加 channel_source 的老库仅查 channels 或整列留空)
*
* @return array{channel_source: bool, channel_source_detail: bool, channels: bool}
*/
private static function doctorAppointmentChannelColumnsForExport(): array
{
$out = [
'channel_source' => false,
'channel_source_detail' => false,
'channels' => false,
];
try {
$cols = Db::name('doctor_appointment')->getTableFields();
if (!is_array($cols)) {
return $out;
}
foreach (array_keys($out) as $k) {
$out[$k] = in_array($k, $cols, true);
}
} catch (\Throwable) {
// keep false
}
return $out;
}
/** /**
* 业务订单列表导出:写入与 PrescriptionOrderLists::setExcelFields 对应的字段(export=2 时由列表类调用) * 业务订单列表导出:写入与 PrescriptionOrderLists::setExcelFields 对应的字段(export=2 时由列表类调用)
* *
@@ -2524,6 +2620,39 @@ class PrescriptionOrderLogic
} }
} }
/** 诊单 ID => 该患者最近一条挂号(按预约日、id 倒序),用于导出「挂号渠道来源」 */
$apptChannelByDiagId = [];
$channelNameByValue = [];
$chCols = self::doctorAppointmentChannelColumnsForExport();
$needGuahaoChannel = $chCols['channel_source'] || $chCols['channels'] || $chCols['channel_source_detail'];
if ($diagIdList !== [] && $needGuahaoChannel) {
$selectFields = ['patient_id', 'appointment_date', 'id'];
if ($chCols['channel_source']) {
$selectFields[] = 'channel_source';
}
if ($chCols['channel_source_detail']) {
$selectFields[] = 'channel_source_detail';
}
if ($chCols['channels']) {
$selectFields[] = 'channels';
}
$apptRows = Appointment::whereIn('patient_id', $diagIdList)
->field($selectFields)
->order('appointment_date', 'desc')
->order('id', 'desc')
->select()
->toArray();
foreach ($apptRows as $ar) {
$p = (int) ($ar['patient_id'] ?? 0);
if ($p > 0 && !isset($apptChannelByDiagId[$p])) {
$apptChannelByDiagId[$p] = $ar;
}
}
if ($chCols['channel_source'] || $chCols['channels']) {
$channelNameByValue = DictData::where('type_value', 'channels')->column('name', 'value');
}
}
$rxById = []; $rxById = [];
if ($rxIdList !== []) { if ($rxIdList !== []) {
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time') $rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
@@ -2595,6 +2724,11 @@ class PrescriptionOrderLogic
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []); $item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
$item['export_channel'] = (string) ($item['service_channel'] ?? ''); $item['export_channel'] = (string) ($item['service_channel'] ?? '');
$apCh = $apptChannelByDiagId[$diagId] ?? null;
$item['export_guahao_channel_source'] = \is_array($apCh)
? self::formatGuahaoChannelSourceForExport($apCh, $channelNameByValue)
: '';
$md = $item['medication_days'] ?? null; $md = $item['medication_days'] ?? null;
$item['export_medication_days'] = $md !== null && $md !== '' ? (string) $md : ''; $item['export_medication_days'] = $md !== null && $md !== '' ? (string) $md : '';
@@ -392,7 +392,7 @@ class GancaoCallbackController extends BaseApiController
$nu = (string) ($ext['nu'] ?? ''); $nu = (string) ($ext['nu'] ?? '');
$supplier = (string) ($ext['supplier'] ?? ''); $supplier = (string) ($ext['supplier'] ?? '');
if ($nu !== '') { if ($nu !== '' && trim((string) ($order->tracking_number ?? '')) === '') {
$order->tracking_number = mb_substr($nu, 0, 80); $order->tracking_number = mb_substr($nu, 0, 80);
} }
if ($shippingName !== '') { if ($shippingName !== '') {