Compare commits

...
Author SHA1 Message Date
Your Name 99eca8b2e5 更新 2026-07-07 09:24:40 +08:00
Your Name 55e3b5d9e0 更新 2026-07-06 16:34:42 +08:00
12 changed files with 990 additions and 155 deletions
+12
View File
@@ -523,6 +523,18 @@ export function prescriptionOrderLogs(params: { id: number }) {
return request.get({ url: '/tcm.prescriptionOrder/logs', params }) return request.get({ url: '/tcm.prescriptionOrder/logs', params })
} }
/** 手工新增操作日志(可选调整处方/支付单审核状态) */
export function prescriptionOrderAddLog(params: {
id: number
summary: string
prescription_audit_status?: number | ''
payment_slip_audit_status?: number | ''
prescription_audit_remark?: string
payment_slip_audit_remark?: string
}) {
return request.post({ url: '/tcm.prescriptionOrder/addLog', params })
}
/** 修改订单金额 */ /** 修改订单金额 */
export function prescriptionOrderUpdateAmount(params: { id: number; amount: number }) { export function prescriptionOrderUpdateAmount(params: { id: number; amount: number }) {
return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params }) return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params })
@@ -631,7 +631,7 @@
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item> <el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item> <el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item> <el-descriptions-item label="服务套餐">{{ detailServicePackageText }}</el-descriptions-item>
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item> <el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
<el-descriptions-item v-if="showInternalCost" label="内部成本"> <el-descriptions-item v-if="showInternalCost" label="内部成本">
@@ -780,7 +780,18 @@
class="po-panel border-gray-100 mt-4" class="po-panel border-gray-100 mt-4"
> >
<template #header> <template #header>
<span class="font-medium text-[15px]">操作日志</span> <div class="flex items-center justify-between gap-2">
<span class="font-medium text-[15px]">操作日志</span>
<el-button
v-if="canAddPrescriptionOrderLog()"
type="primary"
link
size="small"
@click="openAddLogDialog"
>
新增日志
</el-button>
</div>
</template> </template>
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2"> <el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
<el-timeline-item <el-timeline-item
@@ -800,16 +811,92 @@
</el-timeline> </el-timeline>
<el-empty v-else description="暂无操作日志" :image-size="64" /> <el-empty v-else description="暂无操作日志" :image-size="64" />
</el-card> </el-card>
<!-- 新增操作日志 -->
<el-dialog
v-model="addLogVisible"
title="新增操作日志"
width="520px"
:close-on-click-modal="false"
destroy-on-close
append-to-body
@closed="resetAddLogForm"
>
<el-form ref="addLogFormRef" :model="addLogForm" :rules="addLogRules" label-width="108px">
<el-form-item label="日志内容" prop="summary">
<el-input
v-model="addLogForm.summary"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="记录本次操作说明、沟通结果等"
/>
</el-form-item>
<el-form-item v-if="canSetRxAuditOnAddLog" label="处方审核">
<el-select v-model="addLogForm.prescription_audit_status" class="w-full" clearable placeholder="不修改">
<el-option label="待审核" :value="0" />
<el-option label="已通过" :value="1" />
<el-option label="已驳回" :value="2" />
</el-select>
<div v-if="detailData" class="text-xs text-gray-400 mt-1">
当前:{{ auditStatusText(detailData.prescription_audit_status) }}
</div>
</el-form-item>
<el-form-item
v-if="canSetRxAuditOnAddLog && addLogForm.prescription_audit_status !== '' && addLogForm.prescription_audit_status !== null && addLogForm.prescription_audit_status !== undefined"
label="处方审核意见"
>
<el-input
v-model="addLogForm.prescription_audit_remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="选填"
/>
</el-form-item>
<el-form-item v-if="canSetPayAuditOnAddLog" label="支付单审核">
<el-select v-model="addLogForm.payment_slip_audit_status" class="w-full" clearable placeholder="不修改">
<el-option label="待审核" :value="0" />
<el-option label="已通过" :value="1" />
<el-option label="已驳回" :value="2" />
</el-select>
<div v-if="detailData" class="text-xs text-gray-400 mt-1">
当前:{{ auditStatusText(detailData.payment_slip_audit_status) }}
</div>
</el-form-item>
<el-form-item
v-if="canSetPayAuditOnAddLog && addLogForm.payment_slip_audit_status !== '' && addLogForm.payment_slip_audit_status !== null && addLogForm.payment_slip_audit_status !== undefined"
label="支付审核意见"
>
<el-input
v-model="addLogForm.payment_slip_audit_remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="选填"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addLogVisible = false">取消</el-button>
<el-button type="primary" :loading="addLogSaving" @click="submitAddLog">保存</el-button>
</template>
</el-dialog>
</div> </div>
</el-drawer> </el-drawer>
</template> </template>
<script lang="ts" setup name="PrescriptionOrderDetailDrawer"> <script lang="ts" setup name="PrescriptionOrderDetailDrawer">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue' import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue'
import { import {
prescriptionOrderDetail, prescriptionOrderDetail,
prescriptionOrderLogs, prescriptionOrderLogs,
prescriptionOrderAddLog,
prescriptionOrderLogisticsTrace, prescriptionOrderLogisticsTrace,
prescriptionOrderLogisticsJdUpdate, prescriptionOrderLogisticsJdUpdate,
prescriptionOrderPaidPayOrders prescriptionOrderPaidPayOrders
@@ -831,6 +918,7 @@ import {
consumerRxAuditTag, consumerRxAuditTag,
expressCompanyLabel, expressCompanyLabel,
logActionText, logActionText,
auditStatusText,
formatPayOrderSource, formatPayOrderSource,
normalizeBizPhone, normalizeBizPhone,
recipientVsPrescriptionPhoneMismatch, recipientVsPrescriptionPhoneMismatch,
@@ -841,7 +929,10 @@ import {
analyzeLogisticsPayloadUrgent, analyzeLogisticsPayloadUrgent,
parseLogisticsTracePayload, parseLogisticsTracePayload,
canUpdateAmount, canUpdateAmount,
formatDietaryTaboo formatDietaryTaboo,
type ServicePackageOption,
normalizeServicePackageOptions,
formatServicePackageLabels
} from './prescription-order-utils' } from './prescription-order-utils'
const props = withDefaults( const props = withDefaults(
@@ -873,6 +964,7 @@ const emit = defineEmits<{
(e: 'view-prescription'): void (e: 'view-prescription'): void
(e: 'test-gancao-preview'): void (e: 'test-gancao-preview'): void
(e: 'view-patient'): void (e: 'view-patient'): void
(e: 'detail-changed'): void
}>() }>()
const userStore = useUserStore() const userStore = useUserStore()
@@ -1088,36 +1180,20 @@ const detailFullAddress = computed(() => {
}) })
// ─── 服务套餐字典 ─── // ─── 服务套餐字典 ───
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([]) const servicePackageOptions = ref<ServicePackageOption[]>([])
async function loadServicePackageOptions() { async function loadServicePackageOptions() {
try { try {
const data: any = await getDictData({ type: 'server_order' }) const data: any = await getDictData({ type: 'server_order' })
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0) servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
} catch { } catch {
servicePackageOptions.value = [] servicePackageOptions.value = []
} }
} }
function formatServicePackage(value: any): string { const detailServicePackageText = computed(() =>
if (!value) return '—' formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
)
let packages: string[] = []
if (Array.isArray(value)) {
packages = value
} else if (typeof value === 'string') {
packages = value.split(',').filter((v) => v.trim() !== '')
}
if (packages.length === 0) return '—'
const names = packages.map((val) => {
const option = servicePackageOptions.value.find((opt) => opt.value === val)
return option ? option.name : val
})
return names.join('、')
}
onMounted(() => { onMounted(() => {
loadServicePackageOptions() loadServicePackageOptions()
@@ -1142,6 +1218,102 @@ async function fetchLogs(id: number) {
} }
} }
function hasPerm(perm: string) {
const p = userStore.perms || []
return p.includes('*') || p.includes(perm)
}
function canAddPrescriptionOrderLog() {
return hasPerm('tcm.prescriptionOrder/addLog')
}
/** 与后端 canAuditPrescriptionOrder 同档:超管或 prescription_audit_roles */
const canSetRxAuditOnAddLog = computed(() => {
const u = userStore.userInfo
if (!u || Number(u.root) === 1) return true
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
return ids.some((id) => PRESCRIPTION_AUDIT_ROLE_IDS.includes(id))
})
/** 与后端 canAuditPaymentSlipOrder 同档:超管或 prescription_order_payment_audit_roles 默认 0,3 */
const canSetPayAuditOnAddLog = computed(() => {
const u = userStore.userInfo
if (!u || Number(u.root) === 1) return true
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
return ids.some((id) => [0, 3].includes(id))
})
const addLogVisible = ref(false)
const addLogSaving = ref(false)
const addLogFormRef = ref<FormInstance>()
const addLogForm = reactive({
summary: '',
prescription_audit_status: '' as number | '',
payment_slip_audit_status: '' as number | '',
prescription_audit_remark: '',
payment_slip_audit_remark: ''
})
const addLogRules: FormRules = {
summary: [{ required: true, message: '请填写日志内容', trigger: 'blur' }]
}
function resetAddLogForm() {
addLogForm.summary = ''
addLogForm.prescription_audit_status = ''
addLogForm.payment_slip_audit_status = ''
addLogForm.prescription_audit_remark = ''
addLogForm.payment_slip_audit_remark = ''
addLogFormRef.value?.clearValidate()
}
function openAddLogDialog() {
if (!detailData.value?.id) return
resetAddLogForm()
const d = detailData.value
addLogForm.prescription_audit_remark = String(d.prescription_audit_remark || '')
addLogForm.payment_slip_audit_remark = String(d.payment_slip_audit_remark || '')
addLogVisible.value = true
}
async function submitAddLog() {
if (!addLogFormRef.value || !detailData.value?.id) return
await addLogFormRef.value.validate()
addLogSaving.value = true
try {
const payload: Record<string, unknown> = {
id: detailData.value.id,
summary: addLogForm.summary.trim()
}
if (
canSetRxAuditOnAddLog.value &&
addLogForm.prescription_audit_status !== '' &&
addLogForm.prescription_audit_status !== null &&
addLogForm.prescription_audit_status !== undefined
) {
payload.prescription_audit_status = addLogForm.prescription_audit_status
payload.prescription_audit_remark = addLogForm.prescription_audit_remark
}
if (
canSetPayAuditOnAddLog.value &&
addLogForm.payment_slip_audit_status !== '' &&
addLogForm.payment_slip_audit_status !== null &&
addLogForm.payment_slip_audit_status !== undefined
) {
payload.payment_slip_audit_status = addLogForm.payment_slip_audit_status
payload.payment_slip_audit_remark = addLogForm.payment_slip_audit_remark
}
await prescriptionOrderAddLog(payload as any)
feedback.msgSuccess('日志已添加')
addLogVisible.value = false
await refresh()
emit('detail-changed')
} catch {
/* 拦截器已提示 */
} finally {
addLogSaving.value = false
}
}
// ─── 未关联支付单 ─── // ─── 未关联支付单 ───
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) { async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
if (!diagnosisId) { if (!diagnosisId) {
@@ -137,11 +137,22 @@ export function logActionText(act: string) {
patch_rx_patient: '处方患者信息', patch_rx_patient: '处方患者信息',
update_amount: '修改订单金额', update_amount: '修改订单金额',
complete: '完成订单', complete: '完成订单',
refund: '退款' refund: '退款',
manual_log: '手工备注',
assign_assistant: '改派医助',
add_pay_order: '补齐支付单',
set_ship_mode: '发货类型'
} }
return m[act] || act return m[act] || act
} }
/** 处方/支付单审核状态文案(0 待审核 / 1 已通过 / 2 已驳回) */
export function auditStatusText(s: number | undefined) {
if (s === 1) return '已通过'
if (s === 2) return '已驳回'
return '待审核'
}
/** 支付单来源/方式:企微对外收款、付呗、快递代收等创建链路 + 支付方式回退 */ /** 支付单来源/方式:企微对外收款、付呗、快递代收等创建链路 + 支付方式回退 */
export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) { export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) {
const createType = String(row?.create_type || '') const createType = String(row?.create_type || '')
@@ -357,3 +368,85 @@ export function formatDietaryTaboo(raw: unknown): string {
} }
return '' return ''
} }
/** 服务套餐 dictserver_order */
export type ServicePackageOption = { name: string; value: string; status?: number }
export function normalizeServicePackageValue(v: unknown): string {
return String(v ?? '').trim()
}
/** 解析订单 service_package(逗号串 / 数组 / 单值数字) */
export function parseServicePackageValues(raw: unknown): string[] {
if (raw == null || raw === '') return []
if (Array.isArray(raw)) {
return raw.map(normalizeServicePackageValue).filter(Boolean)
}
if (typeof raw === 'string') {
return raw.split(',').map((v) => v.trim()).filter(Boolean)
}
const one = normalizeServicePackageValue(raw)
return one ? [one] : []
}
export function servicePackageValueEquals(a: unknown, b: unknown): boolean {
const sa = normalizeServicePackageValue(a)
const sb = normalizeServicePackageValue(b)
if (!sa || !sb) return false
if (sa === sb) return true
const na = Number(sa)
const nb = Number(sb)
return Number.isFinite(na) && Number.isFinite(nb) && na === nb
}
export function normalizeServicePackageOptions(raw: unknown): ServicePackageOption[] {
if (!Array.isArray(raw)) return []
return raw
.map((item: any) => ({
name: String(item?.name ?? '').trim() || normalizeServicePackageValue(item?.value),
value: normalizeServicePackageValue(item?.value),
status: Number(item?.status ?? 1)
}))
.filter((item) => item.value !== '')
}
export function findServicePackageOption(
options: ServicePackageOption[],
value: unknown
): ServicePackageOption | undefined {
const key = normalizeServicePackageValue(value)
if (!key) return undefined
return options.find((opt) => servicePackageValueEquals(opt.value, key))
}
/** 展示用:value → 字典 name,多选用「、」连接 */
export function formatServicePackageLabels(
value: unknown,
options: ServicePackageOption[],
emptyText = '—'
): string {
const packages = parseServicePackageValues(value)
if (packages.length === 0) return emptyText
const names = packages.map((val) => {
const option = findServicePackageOption(options, val)
return option?.name || val
})
return names.join('、')
}
/** 编辑下拉:字典项 + 当前已选但字典缺失的兜底项 */
export function mergeServicePackageSelectOptions(
options: ServicePackageOption[],
selected: unknown[]
): ServicePackageOption[] {
const known = new Set(options.map((o) => o.value))
const extras: ServicePackageOption[] = []
for (const raw of selected) {
const val = normalizeServicePackageValue(raw)
if (!val || known.has(val)) continue
const matched = findServicePackageOption(options, val)
extras.push(matched ?? { name: val, value: val, status: 0 })
known.add(val)
}
return extras.length ? [...options, ...extras] : options
}
@@ -298,7 +298,7 @@
:fetch-fun="prescriptionOrderExport" :fetch-fun="prescriptionOrderExport"
:params="prescriptionOrderExportParams" :params="prescriptionOrderExportParams"
:page-size="pager.size" :page-size="pager.size"
export-hint="导出范围与上方筛选一致履约状态创建时间及其他条件均会生效自媒体渠道挂号渠道来源优先取该单关联处方登记的挂号无则诊单下同患者挂号取 id 最大的一条与前台挂号选择的记录一致业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源服务套餐按字典 server_order 解析展示关联收款记录与详情侧栏同源已支付/已退款/待审核每笔两行展示摘要行+明细行多笔空行分隔单元格自动换行签收日期与详情/业绩看板同源仅读物流库轨迹/签收时间导出不再实时查快递100速度只取决于数据库签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库刚发货尚未同步的单子会暂时为空待下次回填/定时任务刷新后显示" export-hint="导出范围与上方筛选一致履约状态创建时间及其他条件均会生效自媒体渠道挂号渠道来源优先取该单关联处方登记的挂号无则诊单下同患者挂号取 id 最大的一条与前台挂号选择的记录一致业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源服务套餐按字典 server_order 解析展示处方导出主方/辅方药材明细主方/辅方服用方式天数与详情侧栏处方笺同口径天数优先取订单 medication_days缺省回退处方 usage_days / 辅方 aux_usage关联收款记录与详情侧栏同源已支付/已退款/待审核每笔两行展示摘要行+明细行多笔空行分隔单元格自动换行签收日期与详情/业绩看板同源仅读物流库轨迹/签收时间导出不再实时查快递100速度只取决于数据库签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库刚发货尚未同步的单子会暂时为空待下次回填/定时任务刷新后显示"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -623,6 +623,7 @@
@view-prescription="detailData && openPrescriptionView(detailData)" @view-prescription="detailData && openPrescriptionView(detailData)"
@test-gancao-preview="testGancaoPreviewFromDetail" @test-gancao-preview="testGancaoPreviewFromDetail"
@view-patient="openDiagnosisPatientDetailFromOrder" @view-patient="openDiagnosisPatientDetailFromOrder"
@detail-changed="getLists"
> >
<template #header-extra="{ detail }"> <template #header-extra="{ detail }">
<div class="flex items-center gap-2 ml-4 shrink-0"> <div class="flex items-center gap-2 ml-4 shrink-0">
@@ -1024,10 +1025,11 @@
class="w-full" class="w-full"
> >
<el-option <el-option
v-for="item in servicePackageOptions" v-for="item in editServicePackageSelectOptions"
:key="item.value" :key="item.value"
:label="item.name" :label="item.name"
:value="item.value" :value="item.value"
:disabled="item.status === 0"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -2225,7 +2227,11 @@ import {
canUpdateAmount, canUpdateAmount,
formatDietaryTaboo, formatDietaryTaboo,
type SlipFormulaType, type SlipFormulaType,
type SlipAuxUsageForm type SlipAuxUsageForm,
type ServicePackageOption,
normalizeServicePackageOptions,
parseServicePackageValues,
mergeServicePackageSelectOptions
} from './components/prescription-order-utils' } from './components/prescription-order-utils'
import { useListTimeFilter } from '@/hooks/useListTimeFilter' import { useListTimeFilter } from '@/hooks/useListTimeFilter'
import { import {
@@ -2414,8 +2420,8 @@ async function submitReassign() {
// 省市区数据 // 省市区数据
const regionOptions = ref([]) const regionOptions = ref([])
// 服务套餐选项 // 服务套餐选项(含已停用项,便于编辑时回显历史值)
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([]) const servicePackageOptions = ref<ServicePackageOption[]>([])
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */ /** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
const doctorOptions = ref<Array<{ id: number; name: string }>>([]) const doctorOptions = ref<Array<{ id: number; name: string }>>([])
@@ -2536,8 +2542,7 @@ const loadRegionData = async () => {
const loadServicePackageOptions = async () => { const loadServicePackageOptions = async () => {
try { try {
const data = await getDictData({ type: 'server_order' }) const data = await getDictData({ type: 'server_order' })
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0) servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
console.log('服务套餐选项已加载:', servicePackageOptions.value.length)
} catch (error) { } catch (error) {
console.error('加载服务套餐选项失败:', error) console.error('加载服务套餐选项失败:', error)
servicePackageOptions.value = [] servicePackageOptions.value = []
@@ -3195,10 +3200,17 @@ async function onDetailShipModeChange(mode: string | number | boolean | undefine
} }
} }
function canAddPayOrderRow(row: { fulfillment_status?: number }) { function canAddPayOrderRow(row: {
// 已发货(5) / 已签收(6) 状态可补齐支付单 fulfillment_status?: number
amount?: number | string
linked_pay_paid_total?: number | string
}) {
// 已发货(5) / 已签收(6) 状态可补齐支付单;总金额已付清则不允许
const fs = Number(row.fulfillment_status) const fs = Number(row.fulfillment_status)
return fs === 5 || fs === 6 if (fs !== 5 && fs !== 6) return false
const orderAmount = Math.round((Number(row.amount) || 0) * 100) / 100
const paidTotal = Math.round((Number(row.linked_pay_paid_total) || 0) * 100) / 100
return paidTotal < orderAmount
} }
function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) { function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) {
@@ -3449,6 +3461,11 @@ const editForm = reactive({
diagnosis_creator_dept_path: '' diagnosis_creator_dept_path: ''
}) })
/** 编辑弹窗下拉:字典项 + 当前已选但字典中缺失的兜底项 */
const editServicePackageSelectOptions = computed(() =>
mergeServicePackageSelectOptions(servicePackageOptions.value, editForm.service_package)
)
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */ /** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => { const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
const raw = String(editForm.diagnosis_creator_dept_path || '').trim() const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
@@ -3681,18 +3698,7 @@ async function openEdit(row: {
editForm.dose_unit = d.dose_unit || '剂' editForm.dose_unit = d.dose_unit || '剂'
editForm.prev_staff = d.prev_staff || '' editForm.prev_staff = d.prev_staff || ''
editForm.service_channel = d.service_channel || '' editForm.service_channel = d.service_channel || ''
// 处理服务套餐:如果是字符串,转换为数组 editForm.service_package = parseServicePackageValues(d.service_package)
if (d.service_package) {
if (Array.isArray(d.service_package)) {
editForm.service_package = d.service_package
} else if (typeof d.service_package === 'string') {
editForm.service_package = d.service_package.split(',').filter(v => v.trim() !== '')
} else {
editForm.service_package = []
}
} else {
editForm.service_package = []
}
editForm.express_company = String(d.express_company || 'auto') || 'auto' editForm.express_company = String(d.express_company || 'auto') || 'auto'
editForm.tracking_number = d.tracking_number || '' editForm.tracking_number = d.tracking_number || ''
editForm.fee_type = Number(d.fee_type) || 3 editForm.fee_type = Number(d.fee_type) || 3
@@ -4509,7 +4515,18 @@ async function loadAddPayOrderAvailable(diagnosisId: number, currentLinkedIds: n
} }
} }
function openAddPayOrder(row: { id: number; diagnosis_id?: number; pay_order_ids?: number[] }) { function openAddPayOrder(row: {
id: number
diagnosis_id?: number
pay_order_ids?: number[]
fulfillment_status?: number
amount?: number | string
linked_pay_paid_total?: number | string
}) {
if (!canAddPayOrderRow(row)) {
feedback.msgWarning('订单总金额与已付金额一致,无需补齐支付单')
return
}
addPayOrderRowId.value = row.id addPayOrderRowId.value = row.id
addPayOrderForm.add_mode = 'create' addPayOrderForm.add_mode = 'create'
addPayOrderForm.order_type = 3 addPayOrderForm.order_type = 3
@@ -1139,7 +1139,7 @@
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item> <el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item> <el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item> <el-descriptions-item label="服务套餐">{{ detailServicePackageText }}</el-descriptions-item>
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item> <el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
<el-descriptions-item v-if="detailData.internal_cost != null && detailData.internal_cost !== ''" label="内部成本"> <el-descriptions-item v-if="detailData.internal_cost != null && detailData.internal_cost !== ''" label="内部成本">
@@ -1525,10 +1525,11 @@
class="w-full" class="w-full"
> >
<el-option <el-option
v-for="item in servicePackageOptions" v-for="item in editServicePackageSelectOptions"
:key="item.value" :key="item.value"
:label="item.name" :label="item.name"
:value="item.value" :value="item.value"
:disabled="item.status === 0"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -2573,7 +2574,14 @@ import {
getDoctors, getDoctors,
getAssistants getAssistants
} from '@/api/tcm' } from '@/api/tcm'
import { formatDietaryTaboo } from './components/prescription-order-utils' import {
formatDietaryTaboo,
type ServicePackageOption,
normalizeServicePackageOptions,
parseServicePackageValues,
mergeServicePackageSelectOptions,
formatServicePackageLabels
} from './components/prescription-order-utils'
import html2canvas from 'html2canvas' import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf' import { jsPDF } from 'jspdf'
import { getDictData } from '@/api/app' import { getDictData } from '@/api/app'
@@ -2669,7 +2677,7 @@ const canViewFinanceFields = () => {
const regionOptions = ref([]) const regionOptions = ref([])
// //
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([]) const servicePackageOptions = ref<ServicePackageOption[]>([])
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */ /** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
const doctorOptions = ref<Array<{ id: number; name: string }>>([]) const doctorOptions = ref<Array<{ id: number; name: string }>>([])
@@ -2790,8 +2798,7 @@ const loadRegionData = async () => {
const loadServicePackageOptions = async () => { const loadServicePackageOptions = async () => {
try { try {
const data = await getDictData({ type: 'server_order' }) const data = await getDictData({ type: 'server_order' })
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0) servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
console.log('服务套餐选项已加载:', servicePackageOptions.value.length)
} catch (error) { } catch (error) {
console.error('加载服务套餐选项失败:', error) console.error('加载服务套餐选项失败:', error)
servicePackageOptions.value = [] servicePackageOptions.value = []
@@ -3199,28 +3206,6 @@ function feeTypeText(t: number | undefined) {
return m[Number(t)] ?? '—' return m[Number(t)] ?? '—'
} }
//
function formatServicePackage(value: any): string {
if (!value) return '—'
let packages: string[] = []
if (Array.isArray(value)) {
packages = value
} else if (typeof value === 'string') {
packages = value.split(',').filter(v => v.trim() !== '')
}
if (packages.length === 0) return '—'
//
const names = packages.map(val => {
const option = servicePackageOptions.value.find(opt => opt.value === val)
return option ? option.name : val
})
return names.join('、')
}
function auditStatusText(s: number | undefined) { function auditStatusText(s: number | undefined) {
if (s === 1) return '已通过' if (s === 1) return '已通过'
if (s === 2) return '已驳回' if (s === 2) return '已驳回'
@@ -3473,6 +3458,10 @@ const detailVisible = ref(false)
const detailLoading = ref(false) const detailLoading = ref(false)
const detailData = ref<Record<string, any> | null>(null) const detailData = ref<Record<string, any> | null>(null)
const detailServicePackageText = computed(() =>
formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
)
// --- --- // --- ---
const workflowActiveStep = computed(() => { const workflowActiveStep = computed(() => {
if (!detailData.value) return 0 if (!detailData.value) return 0
@@ -4060,6 +4049,10 @@ const editForm = reactive({
diagnosis_creator_dept_path: '' diagnosis_creator_dept_path: ''
}) })
const editServicePackageSelectOptions = computed(() =>
mergeServicePackageSelectOptions(servicePackageOptions.value, editForm.service_package)
)
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */ /** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => { const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
const raw = String(editForm.diagnosis_creator_dept_path || '').trim() const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
@@ -4310,18 +4303,7 @@ async function openEdit(row: {
editForm.dose_unit = d.dose_unit || '剂' editForm.dose_unit = d.dose_unit || '剂'
editForm.prev_staff = d.prev_staff || '' editForm.prev_staff = d.prev_staff || ''
editForm.service_channel = d.service_channel || '' editForm.service_channel = d.service_channel || ''
// editForm.service_package = parseServicePackageValues(d.service_package)
if (d.service_package) {
if (Array.isArray(d.service_package)) {
editForm.service_package = d.service_package
} else if (typeof d.service_package === 'string') {
editForm.service_package = d.service_package.split(',').filter(v => v.trim() !== '')
} else {
editForm.service_package = []
}
} else {
editForm.service_package = []
}
editForm.express_company = String(d.express_company || 'auto') || 'auto' editForm.express_company = String(d.express_company || 'auto') || 'auto'
editForm.tracking_number = d.tracking_number || '' editForm.tracking_number = d.tracking_number || ''
editForm.fee_type = Number(d.fee_type) || 3 editForm.fee_type = Number(d.fee_type) || 3
+122 -20
View File
@@ -121,6 +121,16 @@
end-placeholder="最近挂号结束" end-placeholder="最近挂号结束"
@change="handleLatestAppointmentFilterChange" @change="handleLatestAppointmentFilterChange"
/> />
<daterange-picker
class="latest-assign-range"
v-model:startTime="formData.latest_assign_start_date"
v-model:endTime="formData.latest_assign_end_date"
picker-type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="最近指派开始"
end-placeholder="最近指派结束"
@change="handleLatestAssignFilterChange"
/>
<el-select <el-select
v-model="formData.latest_appointment_channel_source" v-model="formData.latest_appointment_channel_source"
placeholder="最近挂号渠道" placeholder="最近挂号渠道"
@@ -173,6 +183,7 @@
v-loading="pager.loading" v-loading="pager.loading"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@row-dblclick="goReadonly" @row-dblclick="goReadonly"
@sort-change="handleTableSortChange"
:row-class-name="getRowClassName" :row-class-name="getRowClassName"
class="diagnosis-table" class="diagnosis-table"
stripe stripe
@@ -283,7 +294,14 @@
<span v-else class="status-unprescribed">未开方</span> <span v-else class="status-unprescribed">未开方</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="未服务天数" width="110" align="center"> <el-table-column
label="未服务天数"
prop="unserved_days"
width="110"
align="center"
sortable="custom"
:sort-orders="['descending', 'ascending']"
>
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip <el-tooltip
v-if="row.last_blood_record_at" v-if="row.last_blood_record_at"
@@ -762,6 +780,10 @@ const formData = reactive({
latest_appointment_start_date: '' as string, latest_appointment_start_date: '' as string,
latest_appointment_end_date: '' as string, latest_appointment_end_date: '' as string,
latest_appointment_channel_source: '' as string, latest_appointment_channel_source: '' as string,
latest_assign_start_date: '' as string,
latest_assign_end_date: '' as string,
/** 未服务天数排序:desc=天数多到少 asc=少到多 */
sort_unserved_days: '' as '' | 'asc' | 'desc',
diagnosis_confirmed: '' as '' | '0' | '1', diagnosis_confirmed: '' as '' | '0' | '1',
appointment_date: '' as string, appointment_date: '' as string,
has_appointment: '' as '' | '0' | '1', has_appointment: '' as '' | '0' | '1',
@@ -849,22 +871,50 @@ function resolvePendingAssignOrderMonthForRequest(): string {
return dayjs().format('YYYY-MM') return dayjs().format('YYYY-MM')
} }
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」 */ /** 除顶部 Tab 专属条件外,与主列表共用的「更多筛选」参数(角标 count 需同步 */
function buildPendingAssignCountPayload(): Record<string, unknown> { function buildSharedDiagnosisFilterPayload(): Record<string, unknown> {
return buildTcmDiagnosisListRequestPayload({ return {
...formData, keyword: formData.keyword,
diagnosis_type: formData.diagnosis_type,
syndrome_type: formData.syndrome_type,
assistant_id: formData.assistant_id,
diagnosis_confirmed: formData.diagnosis_confirmed,
has_appointment: formData.has_appointment,
latest_appointment_start_date: formData.latest_appointment_start_date,
latest_appointment_end_date: formData.latest_appointment_end_date,
latest_appointment_channel_source: formData.latest_appointment_channel_source,
latest_assign_start_date: formData.latest_assign_start_date,
latest_assign_end_date: formData.latest_assign_end_date
}
}
/** 顶部 Tab 角标 count 请求:带上共用筛选,再叠加各 Tab 专属条件 */
function buildDateCountRequestPayload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
page_no: 1, page_no: 1,
page_size: 1, page_size: 1,
pending_assign: 1, ...buildSharedDiagnosisFilterPayload(),
appointment_date: '', appointment_date: '',
has_appointment: '',
pending_booking: '', pending_booking: '',
completed_appointment: '', completed_appointment: '',
latest_appointment_start_date: '', pending_assign: '',
latest_appointment_end_date: '', pending_assign_order_month: '',
latest_appointment_channel_source: '', pending_assign_keyword: '',
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest() sort_unserved_days: '',
} as Record<string, unknown>) as Record<string, unknown> ...overrides
}
}
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
function buildPendingAssignCountPayload(): Record<string, unknown> {
return buildTcmDiagnosisListRequestPayload(
buildDateCountRequestPayload({
pending_assign: 1,
has_appointment: '',
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest(),
pending_assign_keyword: formData.pending_assign_keyword
}) as Record<string, unknown>
) as Record<string, unknown>
} }
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) => const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
@@ -893,6 +943,9 @@ function clearSecondaryFiltersWhenPendingAssignWideSearch() {
formData.latest_appointment_start_date = '' formData.latest_appointment_start_date = ''
formData.latest_appointment_end_date = '' formData.latest_appointment_end_date = ''
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
formData.sort_unserved_days = ''
formData.pending_assign_order_month = '' formData.pending_assign_order_month = ''
activeTab.value = 'all' activeTab.value = 'all'
if (kw1 !== '') { if (kw1 !== '') {
@@ -994,14 +1047,26 @@ const onPendingAssignOrderMonthChange = async (val: string | null) => {
const fetchDateCounts = async () => { const fetchDateCounts = async () => {
try { try {
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([ const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
tcmDiagnosisLists({ appointment_date: yesterdayStr.value, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ appointment_date: dayBeforeStr.value, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: yesterdayStr.value, has_appointment: '' }) as any
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }), ),
tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: dayBeforeStr.value, has_appointment: '' }) as any
tcmDiagnosisLists({ page_no: 1, page_size: 1 }), ),
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: todayStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(
buildDateCountRequestPayload({ appointment_date: tomorrowStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(
buildDateCountRequestPayload({ appointment_date: dayAfterStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(buildDateCountRequestPayload() as any),
tcmDiagnosisLists(buildDateCountRequestPayload({ has_appointment: 0 }) as any),
tcmDiagnosisLists(
buildDateCountRequestPayload({ completed_appointment: 1, has_appointment: '' }) as any
),
tcmDiagnosisLists(buildPendingAssignCountPayload() as any) tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
]) ])
dateCounts.value = { dateCounts.value = {
@@ -1135,6 +1200,11 @@ const clearLatestAppointmentFilters = () => {
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
} }
const clearLatestAssignFilters = () => {
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
}
const hasLatestAppointmentFilter = () => const hasLatestAppointmentFilter = () =>
!!( !!(
formData.latest_appointment_start_date || formData.latest_appointment_start_date ||
@@ -1142,6 +1212,9 @@ const hasLatestAppointmentFilter = () =>
formData.latest_appointment_channel_source formData.latest_appointment_channel_source
) )
const hasLatestAssignFilter = () =>
!!(formData.latest_assign_start_date || formData.latest_assign_end_date)
const handleLatestAppointmentFilterChange = () => { const handleLatestAppointmentFilterChange = () => {
if (hasLatestAppointmentFilter()) { if (hasLatestAppointmentFilter()) {
formData.appointment_date = '' formData.appointment_date = ''
@@ -1154,6 +1227,27 @@ const handleLatestAppointmentFilterChange = () => {
doSearch() doSearch()
} }
const handleLatestAssignFilterChange = () => {
doSearch()
}
const handleTableSortChange = ({
prop,
order
}: {
prop: string
order: 'ascending' | 'descending' | null
}) => {
if (prop === 'unserved_days') {
formData.sort_unserved_days =
order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : ''
} else {
formData.sort_unserved_days = ''
}
pager.page = 1
getLists()
}
const latestAppointmentChannelText = (row: any) => { const latestAppointmentChannelText = (row: any) => {
const desc = String(row?.latest_appointment_channel_source_desc || '').trim() const desc = String(row?.latest_appointment_channel_source_desc || '').trim()
const raw = String(row?.latest_appointment_channel_source || '').trim() const raw = String(row?.latest_appointment_channel_source || '').trim()
@@ -1200,6 +1294,9 @@ const handleReset = () => {
formData.latest_appointment_start_date = '' formData.latest_appointment_start_date = ''
formData.latest_appointment_end_date = '' formData.latest_appointment_end_date = ''
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
formData.sort_unserved_days = ''
formData.diagnosis_confirmed = '' formData.diagnosis_confirmed = ''
formData.appointment_date = '' formData.appointment_date = ''
formData.has_appointment = '' formData.has_appointment = ''
@@ -2298,6 +2395,11 @@ onUnmounted(() => {
max-width: 100%; max-width: 100%;
} }
.latest-assign-range {
width: 260px;
max-width: 100%;
}
.latest-appointment-channel { .latest-appointment-channel {
width: 170px; width: 170px;
} }
@@ -328,6 +328,20 @@ class PrescriptionOrderController extends BaseAdminController
return $this->success('', $result); return $this->success('', $result);
} }
/**
* 手工新增操作日志(可选同步调整处方/支付单审核状态)
*/
public function addLog()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('addLog');
$result = PrescriptionOrderLogic::addLog($params, $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(PrescriptionOrderLogic::getError());
}
return $this->success('日志已添加', $result);
}
/** /**
* 为「已发货」订单新增一条关联支付单,并重置支付单审核状态为待审核 * 为「已发货」订单新增一条关联支付单,并重置支付单审核状态为待审核
*/ */
@@ -140,6 +140,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
$this->applyLatestAppointmentFilters($query, $pendingWideSearch); $this->applyLatestAppointmentFilters($query, $pendingWideSearch);
$this->applyLatestAssignFilters($query, $pendingWideSearch);
$this->applyPendingAssignBusinessOrderMonthFilter($query); $this->applyPendingAssignBusinessOrderMonthFilter($query);
@@ -167,30 +168,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
} }
// 按挂号状态优先级排序:已过号(4) > 已预约(1) > 已完成(3),然后按挂号日期+时间升序 $orderRaw = $this->resolveListOrderRaw($pendingWideSearch);
// 若传了 appointment_date(当天/明天等筛选),只按「该日」的挂号排序与展示
$diagTbl = (new Diagnosis())->getTable();
$aptTbl = (new Appointment())->getTable();
$minAptDateCond = '';
if (!$pendingWideSearch && !empty($this->params['appointment_date'])) {
$sortAptDate = addslashes((string) $this->params['appointment_date']);
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
}
// 获取最早的挂号状态(用于排序优先级)
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
// 获取最早的挂号时间(用于同状态内排序)
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
// 「已完成」Tab:按最近一条「已完成」(status=3) 挂号日期+时间降序…
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
if ($isCompletedTab) {
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
} else {
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
}
$lists = $query $lists = $query
->with(['DiagnosisViewRecord']) ->with(['DiagnosisViewRecord'])
@@ -571,6 +549,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
$this->applyLatestAppointmentFilters($query, $pendingWideSearch); $this->applyLatestAppointmentFilters($query, $pendingWideSearch);
$this->applyLatestAssignFilters($query, $pendingWideSearch);
// 仅已开方(待分配+关键词检索时不限制) // 仅已开方(待分配+关键词检索时不限制)
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') { if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
@@ -644,6 +623,99 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
$query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions)); $query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions));
} }
/**
* 最近一次成功指派过滤:按 create_time DESC, id DESC 取 to_assistant_id>0 的一条。
*
* @param mixed $query
*/
private function applyLatestAssignFilters($query, bool $pendingWideSearch): void
{
if ($pendingWideSearch) {
return;
}
$startDate = $this->normalizeYmd($this->params['latest_assign_start_date'] ?? '');
$endDate = $this->normalizeYmd($this->params['latest_assign_end_date'] ?? '');
if ($startDate === '' && $endDate === '') {
return;
}
$logTbl = Db::name('tcm_diagnosis_assign_log')->getTable();
$diagTbl = (new Diagnosis())->getTable();
$latestIdSql = $this->latestAssignLogIdSubSql($logTbl, $diagTbl);
$conditions = ["latest_lg.id = ({$latestIdSql})"];
if ($startDate !== '') {
$startTs = (int) strtotime($startDate . ' 00:00:00');
$conditions[] = "latest_lg.create_time >= {$startTs}";
}
if ($endDate !== '') {
$endTs = (int) strtotime($endDate . ' 23:59:59');
$conditions[] = "latest_lg.create_time <= {$endTs}";
}
$query->whereExists("SELECT 1 FROM {$logTbl} latest_lg WHERE " . implode(' AND ', $conditions));
}
private function latestAssignLogIdSubSql(string $logTbl, string $diagTbl): string
{
return "SELECT lg_latest.id FROM {$logTbl} lg_latest "
. "WHERE lg_latest.diagnosis_id = {$diagTbl}.id "
. 'AND lg_latest.to_assistant_id > 0 '
. 'ORDER BY lg_latest.create_time DESC, lg_latest.id DESC LIMIT 1';
}
/**
* 列表默认排序;支持 sort_unserved_days=asc|desc 按未服务天数排序。
*/
private function resolveListOrderRaw(bool $pendingWideSearch): string
{
$diagTbl = (new Diagnosis())->getTable();
$sortUnserved = strtolower(trim((string) ($this->params['sort_unserved_days'] ?? '')));
if (in_array($sortUnserved, ['asc', 'desc'], true)) {
$anchorExpr = $this->unservedAnchorExpr($diagTbl);
$nullLast = "CASE WHEN IFNULL({$anchorExpr}, 0) = 0 THEN 1 ELSE 0 END ASC";
if ($sortUnserved === 'desc') {
return "{$nullLast}, IFNULL({$anchorExpr}, 0) ASC, {$diagTbl}.id DESC";
}
return "{$nullLast}, IFNULL({$anchorExpr}, 0) DESC, {$diagTbl}.id DESC";
}
$aptTbl = (new Appointment())->getTable();
$minAptDateCond = '';
if (!$pendingWideSearch && !empty($this->params['appointment_date'])) {
$sortAptDate = addslashes((string) $this->params['appointment_date']);
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
}
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
if ($isCompletedTab) {
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
return $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
}
return $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
}
/** 未服务天数锚点:血糖/饮食/运动记录最近 record_date 的最大值 */
private function unservedAnchorExpr(string $diagTbl): string
{
$bloodTbl = (new BloodRecord())->getTable();
$dietTbl = (new DietRecord())->getTable();
$exerciseTbl = (new ExerciseRecord())->getTable();
return 'GREATEST('
. "COALESCE((SELECT MAX(br.record_date) FROM {$bloodTbl} br WHERE br.diagnosis_id = {$diagTbl}.id AND br.delete_time IS NULL), 0), "
. "COALESCE((SELECT MAX(dr.record_date) FROM {$dietTbl} dr WHERE dr.diagnosis_id = {$diagTbl}.id AND dr.delete_time IS NULL), 0), "
. "COALESCE((SELECT MAX(er.record_date) FROM {$exerciseTbl} er WHERE er.diagnosis_id = {$diagTbl}.id AND er.delete_time IS NULL), 0)"
. ')';
}
private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string
{ {
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES); $statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);
@@ -820,6 +820,11 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)', 'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
'export_medication_form' => '药品形态', 'export_medication_form' => '药品形态',
'export_prescription_name' => '药方名称', 'export_prescription_name' => '药方名称',
'export_prescription_herbs' => '处方',
'export_main_usage' => '主方服用方式',
'export_main_usage_days' => '主方天数',
'export_aux_usage' => '辅方服用方式',
'export_aux_usage_days' => '辅方天数',
'export_service_package' => '服务套餐', 'export_service_package' => '服务套餐',
'export_medication_days' => '天数', 'export_medication_days' => '天数',
'export_amount' => '总金额', 'export_amount' => '总金额',
@@ -909,6 +914,9 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$wrapWideKeys = [ $wrapWideKeys = [
'export_linked_pay_records', 'export_linked_pay_records',
'export_prescription_name', 'export_prescription_name',
'export_prescription_herbs',
'export_main_usage',
'export_aux_usage',
'export_guahao_channel_source', 'export_guahao_channel_source',
'export_assistant_dept', 'export_assistant_dept',
'export_service_package', 'export_service_package',
@@ -919,6 +927,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_patient_gender' => 6, 'export_patient_gender' => 6,
'export_patient_age' => 6, 'export_patient_age' => 6,
'export_medication_days' => 6, 'export_medication_days' => 6,
'export_main_usage_days' => 8,
'export_aux_usage_days' => 8,
'export_amount' => 10, 'export_amount' => 10,
'export_paid_amount' => 10, 'export_paid_amount' => 10,
'export_refund_amount' => 10, 'export_refund_amount' => 10,
@@ -927,6 +937,9 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_supply_mode' => 10, 'export_supply_mode' => 10,
'export_linked_pay_records' => 52, 'export_linked_pay_records' => 52,
'export_prescription_name' => 34, 'export_prescription_name' => 34,
'export_prescription_herbs' => 36,
'export_main_usage' => 28,
'export_aux_usage' => 28,
'export_guahao_channel_source' => 22, 'export_guahao_channel_source' => 22,
'export_assistant_dept' => 24, 'export_assistant_dept' => 24,
'export_service_package' => 18, 'export_service_package' => 18,
@@ -2375,6 +2375,123 @@ class PrescriptionOrderLogic
->toArray(); ->toArray();
} }
/**
* 手工新增操作日志;可选单独调整处方审核 / 支付单审核状态(不触发常规审核流程副作用)
*
* @param array<string,mixed> $params id, summary, prescription_audit_status?, payment_slip_audit_status?, prescription_audit_remark?, payment_slip_audit_remark?
* @return array<string,mixed>|false
*/
public static function addLog(array $params, int $adminId, array $adminInfo)
{
self::$error = '';
$id = (int) ($params['id'] ?? 0);
$summary = mb_substr(trim((string) ($params['summary'] ?? '')), 0, 500);
if ($summary === '') {
self::$error = '请填写日志内容';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
$changeParts = [];
$hasRxChange = array_key_exists('prescription_audit_status', $params)
&& $params['prescription_audit_status'] !== ''
&& $params['prescription_audit_status'] !== null;
$hasPayChange = array_key_exists('payment_slip_audit_status', $params)
&& $params['payment_slip_audit_status'] !== ''
&& $params['payment_slip_audit_status'] !== null;
if ($hasRxChange) {
if (!self::canAuditPrescriptionOrder($adminInfo)) {
self::$error = '无处方审核权限,不能调整处方审核状态';
return false;
}
$newRx = (int) $params['prescription_audit_status'];
if (!in_array($newRx, [0, 1, 2], true)) {
self::$error = '处方审核状态无效';
return false;
}
$oldRx = (int) $order->prescription_audit_status;
if ($newRx !== $oldRx) {
$order->prescription_audit_status = $newRx;
$changeParts[] = '处方审核:' . self::auditStatusLabelForLog($oldRx)
. ' → ' . self::auditStatusLabelForLog($newRx);
}
if (array_key_exists('prescription_audit_remark', $params)) {
$order->prescription_audit_remark = mb_substr(trim((string) $params['prescription_audit_remark']), 0, 500);
}
}
if ($hasPayChange) {
if (!self::canAuditPaymentSlipOrder($adminInfo)) {
self::$error = '无支付单审核权限,不能调整支付单审核状态';
return false;
}
$newPay = (int) $params['payment_slip_audit_status'];
if (!in_array($newPay, [0, 1, 2], true)) {
self::$error = '支付单审核状态无效';
return false;
}
$oldPay = (int) $order->payment_slip_audit_status;
if ($newPay !== $oldPay) {
$order->payment_slip_audit_status = $newPay;
$changeParts[] = '支付单审核:' . self::auditStatusLabelForLog($oldPay)
. ' → ' . self::auditStatusLabelForLog($newPay);
}
if (array_key_exists('payment_slip_audit_remark', $params)) {
$order->payment_slip_audit_remark = mb_substr(trim((string) $params['payment_slip_audit_remark']), 0, 500);
}
}
if ($hasRxChange || $hasPayChange) {
self::syncFulfillmentStatus($order);
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
}
$logSummary = $summary;
if ($changeParts !== []) {
$logSummary .= '' . implode('', $changeParts) . '';
}
self::writeLog($id, $adminId, $adminInfo, 'manual_log', $logSummary);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
private static function auditStatusLabelForLog(int $status): string
{
return match ($status) {
1 => '已通过',
2 => '已驳回',
default => '待审核',
};
}
/** /**
* 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单新增一条关联支付单(zyt_order), * 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单新增一条关联支付单(zyt_order),
* 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。 * 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。
@@ -3107,6 +3224,210 @@ class PrescriptionOrderLogic
return $type; return $type;
} }
/**
* 导出用:处方药材明细(主方/辅方分行,与处方笺一致)
*
* @param array<string, mixed> $rx
*/
public static function formatPrescriptionHerbsForExport(array $rx): string
{
[$mainHerbs, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rx);
$formatList = static function (array $herbs): string {
$parts = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
$name = trim((string) ($h['name'] ?? ''));
if ($name === '') {
continue;
}
$parts[] = $name . ' ' . self::formatExportDosageNumber((float) ($h['dosage'] ?? 0)) . 'g';
}
return implode('、', $parts);
};
$sections = [];
$mainText = $formatList($mainHerbs);
if ($mainText !== '') {
$sections[] = '主方:' . $mainText;
}
$auxText = $formatList($auxHerbs);
if ($auxText !== '') {
$sections[] = '辅方:' . $auxText;
}
return implode("\n", $sections);
}
/**
* 导出用:主方/辅方服用方式(与前端 buildUsageSegmentText / 处方笺同口径)
*
* @param array<string, mixed> $usage
*/
public static function formatUsageSegmentForExport(
array $usage,
string $prescriptionType = '浓缩水丸',
string $fallbackWay = '',
string $fallbackTime = ''
): string {
$pt = trim($prescriptionType) !== '' ? trim($prescriptionType) : '浓缩水丸';
$times = (int) ($usage['times_per_day'] ?? 0);
if ($times <= 0) {
$times = 3;
}
$amount = isset($usage['dosage_amount']) && $usage['dosage_amount'] !== '' && $usage['dosage_amount'] !== null
? (float) $usage['dosage_amount']
: 10.0;
$unit = trim((string) ($usage['usage_dosage_unit'] ?? ($usage['dosage_unit'] ?? '')));
if ($unit === '') {
$unit = $pt === '饮片' ? 'ml' : 'g';
}
$usageWay = trim((string) ($usage['usage_way'] ?? ''));
if ($usageWay === '') {
$usageWay = $fallbackWay !== '' ? $fallbackWay : '温水送服';
}
$usageTime = trim((string) ($usage['usage_time'] ?? ''));
if ($usageTime === '') {
$usageTime = $fallbackTime;
}
$seg = ['每天' . $times . '次'];
if ($pt === '浓缩水丸') {
$bags = (int) ($usage['dosage_bag_count'] ?? 0);
if ($bags <= 0) {
$bags = 1;
}
$seg[] = '一次' . $bags . '袋';
$seg[] = '每袋' . self::formatExportDosageNumber($amount) . $unit;
} else {
$seg[] = '一次' . self::formatExportDosageNumber($amount) . $unit;
}
$seg[] = $usageWay;
if ($usageTime !== '') {
$seg[] = $usageTime;
}
return implode(', ', $seg);
}
/**
* 导出用:辅方用法 JSON 规范化(与前端 normalizeSlipAuxUsageForm 默认值一致)
*
* @return array<string, mixed>
*/
private static function normalizeAuxUsageForExport($raw, string $prescriptionType): array
{
$pt = trim($prescriptionType) !== '' ? trim($prescriptionType) : '浓缩水丸';
if ($pt === '饮片') {
$base = [
'dosage_amount' => 50.0,
'dosage_bag_count' => 1,
'times_per_day' => 3,
'usage_days' => 7,
];
} elseif ($pt === '浓缩水丸') {
$base = [
'dosage_amount' => 5.0,
'dosage_bag_count' => 1,
'times_per_day' => 3,
'usage_days' => 7,
];
} else {
$base = [
'dosage_amount' => 1.0,
'dosage_bag_count' => 1,
'times_per_day' => 3,
'usage_days' => 7,
];
}
if (\is_string($raw) && $raw !== '') {
$decoded = json_decode($raw, true);
$raw = \is_array($decoded) ? $decoded : null;
}
if (!\is_array($raw)) {
return $base;
}
return [
'dosage_amount' => isset($raw['dosage_amount']) && $raw['dosage_amount'] !== '' && $raw['dosage_amount'] !== null
? (float) $raw['dosage_amount']
: $base['dosage_amount'],
'dosage_bag_count' => (int) ($raw['dosage_bag_count'] ?? 0) > 0
? (int) $raw['dosage_bag_count']
: $base['dosage_bag_count'],
'times_per_day' => (int) ($raw['times_per_day'] ?? 0) > 0
? (int) $raw['times_per_day']
: $base['times_per_day'],
'usage_days' => (int) ($raw['usage_days'] ?? 0) > 0
? (int) $raw['usage_days']
: $base['usage_days'],
];
}
/**
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>}
*/
private static function splitPrescriptionHerbsFromRx(array $rx): array
{
$herbs = $rx['herbs'] ?? null;
if (\is_string($herbs) && $herbs !== '') {
$decoded = json_decode($herbs, true);
$herbs = \is_array($decoded) ? $decoded : [];
}
if (!\is_array($herbs)) {
$herbs = [];
}
$mainHerbs = [];
$auxHerbs = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
if (((string) ($h['formula_type'] ?? '')) === '辅方') {
$auxHerbs[] = $h;
} else {
$mainHerbs[] = $h;
}
}
return [$mainHerbs, $auxHerbs];
}
private static function formatExportDosageNumber(float $dosage): string
{
if (floor($dosage) === $dosage) {
return (string) (int) $dosage;
}
return rtrim(rtrim(number_format($dosage, 4, '.', ''), '0'), '.');
}
/**
* 导出用:主方/辅方服用天数(优先业务订单 medication_days,缺省回退处方 usage_days / 辅方 aux_usage
*
* @param array<string, mixed> $rx
* @param array<string, mixed>|null $auxUsage
*/
private static function resolveExportUsageDays(array $rx, ?array $auxUsage, $orderMedicationDays, bool $isAux): string
{
$medDays = $orderMedicationDays;
if ($medDays !== null && $medDays !== '' && (int) $medDays > 0) {
return (string) (int) $medDays;
}
if ($isAux) {
$days = (int) ($auxUsage['usage_days'] ?? 0);
return $days > 0 ? (string) $days : '';
}
$days = (int) ($rx['usage_days'] ?? 0);
return $days > 0 ? (string) $days : '';
}
/** /**
* 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径) * 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径)
* *
@@ -3403,7 +3724,12 @@ class PrescriptionOrderLogic
$rxById = []; $rxById = [];
if ($rxIdList !== []) { if ($rxIdList !== []) {
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time') $rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id', 'prescription_name', 'aux_usage', 'herbs', 'creator_id']) ->field([
'id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id',
'prescription_name', 'aux_usage', 'herbs', 'creator_id',
'dosage_amount', 'dosage_unit', 'dosage_bag_count', 'times_per_day', 'usage_days',
'usage_way', 'usage_time',
])
->select() ->select()
->toArray(); ->toArray();
foreach ($rxRows as $xr) { foreach ($rxRows as $xr) {
@@ -3637,6 +3963,39 @@ class PrescriptionOrderLogic
} }
$item['export_prescription_name'] = implode(' ', $rxNameParts); $item['export_prescription_name'] = implode(' ', $rxNameParts);
$rxArr = \is_array($rx) ? $rx : [];
$rxType = trim((string) ($rxArr['prescription_type'] ?? '')) ?: '浓缩水丸';
$item['export_prescription_herbs'] = self::formatPrescriptionHerbsForExport($rxArr);
$item['export_main_usage'] = $rxArr !== []
? self::formatUsageSegmentForExport($rxArr, $rxType)
: '';
[, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rxArr);
$auxUsageNorm = $auxHerbs !== []
? self::normalizeAuxUsageForExport($rxArr['aux_usage'] ?? null, $rxType)
: null;
if ($auxHerbs !== [] && $auxUsageNorm !== null) {
$item['export_aux_usage'] = self::formatUsageSegmentForExport(
[
'dosage_amount' => $auxUsageNorm['dosage_amount'],
'dosage_bag_count' => $auxUsageNorm['dosage_bag_count'],
'times_per_day' => $auxUsageNorm['times_per_day'],
'usage_dosage_unit' => $rxArr['dosage_unit'] ?? '',
'usage_way' => $rxArr['usage_way'] ?? '',
'usage_time' => $rxArr['usage_time'] ?? '',
],
$rxType,
(string) ($rxArr['usage_way'] ?? ''),
(string) ($rxArr['usage_time'] ?? '')
);
} else {
$item['export_aux_usage'] = '';
}
$orderMedDays = $item['medication_days'] ?? null;
$item['export_main_usage_days'] = self::resolveExportUsageDays($rxArr, $auxUsageNorm, $orderMedDays, false);
$item['export_aux_usage_days'] = $auxHerbs !== []
? self::resolveExportUsageDays($rxArr, $auxUsageNorm, $orderMedDays, true)
: '';
$item['export_service_package'] = self::formatServicePackageForExport( $item['export_service_package'] = self::formatServicePackageForExport(
$item['service_package'] ?? '', $item['service_package'] ?? '',
$packageNameByValue $packageNameByValue
@@ -3807,27 +4166,7 @@ class PrescriptionOrderLogic
$doctorId = (int) ($rx['creator_id'] ?? 0); $doctorId = (int) ($rx['creator_id'] ?? 0);
$herbs = $rx['herbs'] ?? null; [$mainHerbs, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rx);
if (\is_string($herbs) && $herbs !== '') {
$decoded = json_decode($herbs, true);
$herbs = \is_array($decoded) ? $decoded : [];
}
if (!\is_array($herbs)) {
$herbs = [];
}
$mainHerbs = [];
$auxHerbs = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
if (((string) ($h['formula_type'] ?? '')) === '辅方') {
$auxHerbs[] = $h;
} else {
$mainHerbs[] = $h;
}
}
$lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string { $lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string {
if ($hs === []) { if ($hs === []) {
@@ -32,6 +32,11 @@ class PrescriptionOrderValidate extends BaseValidate
'remark_assistant' => 'max:500', 'remark_assistant' => 'max:500',
'action' => 'require|in:approve,reject', 'action' => 'require|in:approve,reject',
'remark' => 'max:500', 'remark' => 'max:500',
'summary' => 'require|max:500',
'prescription_audit_status' => 'in:0,1,2',
'payment_slip_audit_status' => 'in:0,1,2',
'prescription_audit_remark' => 'max:500',
'payment_slip_audit_remark' => 'max:500',
'fulfillment_status' => 'require|integer|in:3,7,8,9,11,12', 'fulfillment_status' => 'require|integer|in:3,7,8,9,11,12',
'reason' => 'require|max:500', 'reason' => 'require|max:500',
'refund_amount' => 'float|egt:0', 'refund_amount' => 'float|egt:0',
@@ -74,6 +79,7 @@ class PrescriptionOrderValidate extends BaseValidate
'withdraw' => ['id'], 'withdraw' => ['id'],
'ship' => ['id', 'tracking_number', 'express_company', 'ship_mode'], 'ship' => ['id', 'tracking_number', 'express_company', 'ship_mode'],
'logs' => ['id'], 'logs' => ['id'],
'addLog' => ['id', 'summary'],
'paidPayOrders' => ['diagnosis_id'], 'paidPayOrders' => ['diagnosis_id'],
'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'], 'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'],
'linkPayOrder' => ['id', 'pay_order_id'], 'linkPayOrder' => ['id', 'pay_order_id'],
@@ -0,0 +1,13 @@
-- 处方业务订单:手工新增操作日志(可选调整处方/支付单审核状态)
SET @po_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/lists' LIMIT 1);
INSERT INTO zyt_system_menu (
pid, type, name, icon, sort, perms, paths, component,
selected, params, is_cache, is_show, is_disable, create_time, update_time
)
SELECT
@po_menu_id, 'A', '新增操作日志', '', 10, 'tcm.prescriptionOrder/addLog', '', '',
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE @po_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/addLog');