更新
This commit is contained in:
@@ -100,6 +100,7 @@ export function doctorDailyStatsOverview(params: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
channel_code?: string
|
||||
dept_ids?: number[] | string
|
||||
}) {
|
||||
return request.get({ url: '/stats.doctorDailyStats/overview', params })
|
||||
}
|
||||
|
||||
@@ -1707,6 +1707,8 @@ type AuxUsageForm = {
|
||||
bags_per_dose: number
|
||||
times_per_day: number
|
||||
usage_days: number
|
||||
/** 从处方库导入辅方时写入的模板名称,供药房联展示 */
|
||||
prescription_name?: string
|
||||
}
|
||||
|
||||
function defaultAuxUsage(prescriptionType = '浓缩水丸'): AuxUsageForm {
|
||||
@@ -1753,7 +1755,8 @@ function normalizeAuxUsageForm(raw: unknown, prescriptionType: string): AuxUsage
|
||||
need_decoction: o.need_decoction === 1 || o.need_decoction === true,
|
||||
bags_per_dose: o.bags_per_dose != null ? Number(o.bags_per_dose) || 1 : base.bags_per_dose,
|
||||
times_per_day: o.times_per_day != null ? Number(o.times_per_day) || 3 : base.times_per_day,
|
||||
usage_days: o.usage_days != null ? Number(o.usage_days) || 7 : base.usage_days
|
||||
usage_days: o.usage_days != null ? Number(o.usage_days) || 7 : base.usage_days,
|
||||
prescription_name: String(o.prescription_name ?? '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3448,6 +3451,10 @@ const handleImportLibrary = (row: any) => {
|
||||
} else {
|
||||
editForm.herbs.push(...imported)
|
||||
}
|
||||
if (formulaType === '辅方') {
|
||||
editForm.aux_usage.prescription_name = String(row.prescription_name ?? '').trim()
|
||||
}
|
||||
|
||||
const modeHint = libraryImportMode.value === 'append' ? '(已追加)' : ''
|
||||
if (findDuplicateHerbNamesLocal().length) {
|
||||
feedback.msgWarning(
|
||||
|
||||
@@ -293,7 +293,7 @@
|
||||
:fetch-fun="prescriptionOrderExport"
|
||||
:params="prescriptionOrderExportParams"
|
||||
:page-size="pager.size"
|
||||
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。"
|
||||
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示;「签收日期」与详情/业绩看板提成口径一致:以快递主表 sign_time 为主,无效时回退至签收类轨迹时间或已签收状态下的最新轨迹时间。"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -2449,7 +2449,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="slipAuxHerbs.length">
|
||||
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
|
||||
<div class="rx-herb-section-label rx-herb-section-label--aux">
|
||||
辅方<span v-if="slipAuxLibraryName" class="rx-herb-library-name">({{ slipAuxLibraryName }})</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(h, i) in slipAuxHerbs"
|
||||
:key="'aux-' + i"
|
||||
@@ -2461,10 +2463,28 @@
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="(h, i) in slipHerbsList" :key="i" class="rx-herb-cell">
|
||||
<span class="rx-herb-name">{{ h.name }}</span>
|
||||
<span class="rx-herb-total">{{ h.dosage }}克</span>
|
||||
</div>
|
||||
<template v-if="slipMainHerbs.length">
|
||||
<div class="rx-herb-section-label">主方</div>
|
||||
<div
|
||||
v-for="(h, i) in slipMainHerbs"
|
||||
:key="'user-main-' + i"
|
||||
class="rx-herb-cell"
|
||||
>
|
||||
<span class="rx-herb-name">{{ h.name }}</span>
|
||||
<span class="rx-herb-total">{{ h.dosage }}克</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="slipAuxHerbs.length">
|
||||
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
|
||||
<div
|
||||
v-for="(h, i) in slipAuxHerbs"
|
||||
:key="'user-aux-' + i"
|
||||
class="rx-herb-cell"
|
||||
>
|
||||
<span class="rx-herb-name">{{ h.name }}</span>
|
||||
<span class="rx-herb-total">{{ h.dosage }}克</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2714,6 +2734,7 @@ import {
|
||||
prescriptionOrderSubmitGancaoRecipel,
|
||||
prescriptionOrderPreviewGancaoRecipel,
|
||||
prescriptionDetail,
|
||||
prescriptionLibraryLists,
|
||||
getDoctors,
|
||||
getAssistants
|
||||
} from '@/api/tcm'
|
||||
@@ -5480,6 +5501,8 @@ const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄
|
||||
const prescriptionViewVisible = ref(false)
|
||||
const prescriptionViewLoading = ref(false)
|
||||
const prescriptionViewData = ref<any>(null)
|
||||
/** 药房联辅方标题:处方库中该辅方模板的 prescription_name(导入或药材匹配解析) */
|
||||
const slipAuxLibraryName = ref('')
|
||||
const prescriptionTabType = ref('internal')
|
||||
const prescriptionSlipPrintRef = ref<HTMLElement | null>(null)
|
||||
const prescriptionSlipExporting = ref(false)
|
||||
@@ -5883,6 +5906,60 @@ function openPrescriptionViewFromEdit() {
|
||||
openPrescriptionView(editForm)
|
||||
}
|
||||
|
||||
function herbSetMatchKey(herbs: Array<{ name?: unknown; dosage?: unknown }>): string {
|
||||
return herbs
|
||||
.map((h) => `${String(h.name ?? '').trim()}:${Number(h.dosage) || 0}`)
|
||||
.sort()
|
||||
.join('|')
|
||||
}
|
||||
|
||||
function herbsMatchPrescriptionLibrary(
|
||||
prescriptionHerbs: Array<{ name?: unknown; dosage?: unknown }>,
|
||||
libraryHerbs: Array<{ name?: unknown; dosage?: unknown }>
|
||||
): boolean {
|
||||
if (!prescriptionHerbs.length || !libraryHerbs.length) return false
|
||||
if (prescriptionHerbs.length !== libraryHerbs.length) return false
|
||||
return herbSetMatchKey(prescriptionHerbs) === herbSetMatchKey(libraryHerbs)
|
||||
}
|
||||
|
||||
function readAuxLibraryNameFromUsage(raw: unknown): string {
|
||||
if (!raw || typeof raw !== 'object') return ''
|
||||
const o = raw as Record<string, unknown>
|
||||
return String(o.prescription_name ?? o.library_name ?? '').trim()
|
||||
}
|
||||
|
||||
/** 解析药房联辅方对应的处方库名称:优先 aux_usage 内持久化字段,否则按开方医师处方库辅方模板药材精确匹配 */
|
||||
async function resolveSlipAuxLibraryName(data: Record<string, unknown>): Promise<string> {
|
||||
const fromUsage = readAuxLibraryNameFromUsage(data.aux_usage)
|
||||
if (fromUsage) return fromUsage
|
||||
|
||||
const herbs = Array.isArray(data.herbs) ? data.herbs : []
|
||||
const auxHerbs = herbs.filter((h) => normalizeSlipFormulaType((h as any)?.formula_type) === '辅方')
|
||||
if (!auxHerbs.length) return ''
|
||||
|
||||
const doctorId = Number(data.creator_id) > 0 ? Number(data.creator_id) : 0
|
||||
if (!doctorId) return ''
|
||||
|
||||
try {
|
||||
const res: any = await prescriptionLibraryLists({
|
||||
page_no: 1,
|
||||
page_size: 200,
|
||||
formula_type: '辅方',
|
||||
prescribing_creator_id: doctorId
|
||||
})
|
||||
const lists = Array.isArray(res?.lists) ? res.lists : []
|
||||
for (const row of lists) {
|
||||
const libHerbs = Array.isArray(row?.herbs) ? row.herbs : []
|
||||
if (herbsMatchPrescriptionLibrary(auxHerbs, libHerbs)) {
|
||||
return String(row?.prescription_name ?? '').trim()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* 静默:无匹配时仅展示「辅方」 */
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function openPrescriptionView(row: any) {
|
||||
if (!row.prescription_id) {
|
||||
feedback.msgWarning('该订单未关联处方')
|
||||
@@ -5892,6 +5969,7 @@ async function openPrescriptionView(row: any) {
|
||||
prescriptionViewVisible.value = true
|
||||
prescriptionViewLoading.value = true
|
||||
prescriptionViewData.value = null
|
||||
slipAuxLibraryName.value = ''
|
||||
prescriptionTabType.value = 'internal'
|
||||
|
||||
try {
|
||||
@@ -5926,12 +6004,15 @@ async function openPrescriptionView(row: any) {
|
||||
merged.appointment_id = Number(linkedAp.id)
|
||||
}
|
||||
prescriptionViewData.value = merged
|
||||
slipAuxLibraryName.value = await resolveSlipAuxLibraryName(merged)
|
||||
} else {
|
||||
prescriptionViewData.value = null
|
||||
slipAuxLibraryName.value = ''
|
||||
}
|
||||
} catch (e: any) {
|
||||
feedback.msgError(e?.message || '加载处方详情失败')
|
||||
prescriptionViewVisible.value = false
|
||||
slipAuxLibraryName.value = ''
|
||||
} finally {
|
||||
prescriptionViewLoading.value = false
|
||||
}
|
||||
@@ -6585,6 +6666,11 @@ async function downloadPrescriptionSlipPdf() {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.rx-herb-library-name {
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.rx-herb-cell {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 64px;
|
||||
|
||||
@@ -181,9 +181,44 @@
|
||||
<template #default="{ row }">¥ {{ formatDoctorDailyMoney(row.deal_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deal_order_count" label="接诊诊单" min-width="88" align="right" />
|
||||
<el-table-column
|
||||
prop="appointment_total"
|
||||
label="总挂号"
|
||||
min-width="88"
|
||||
align="right"
|
||||
>
|
||||
<template #header>
|
||||
<span>总挂号</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
placement="top"
|
||||
content="区间内该医生 doctor_appointment 全部状态合计(已预约/已完成/已过号/已取消);选部门时仅计经手医助归属在所选部门子树的挂号(与业绩看板「预约诊单」医助口径一致)"
|
||||
>
|
||||
<el-icon class="doctor-daily-th__hint"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="appointment_completed" label="挂号完成" min-width="88" align="right" />
|
||||
<el-table-column prop="appointment_missed" label="过号" min-width="72" align="right" />
|
||||
<el-table-column prop="appointment_cancelled" label="取消" min-width="72" align="right" />
|
||||
<el-table-column
|
||||
prop="appointment_conversion_rate"
|
||||
label="挂号率"
|
||||
min-width="92"
|
||||
align="right"
|
||||
>
|
||||
<template #header>
|
||||
<span>挂号率</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
placement="top"
|
||||
content="挂号率 = 成交单数 / 总挂号数 × 100;总挂号为 0 时显示「—」"
|
||||
>
|
||||
<el-icon class="doctor-daily-th__hint"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="{ row }">{{ formatDoctorDailyRate(row.appointment_conversion_rate) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<el-empty v-else description="当前账号无医生统计权限" />
|
||||
@@ -2078,6 +2113,10 @@ function buildDoctorDailyRequestParams(): Record<string, any> {
|
||||
if (selectedChannel.value) {
|
||||
p.channel_code = selectedChannel.value
|
||||
}
|
||||
// 选中父级时由后端 resolvePrimaryDepts 自动展开所有子级,再用 admin_dept 收窄医生集合。
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
p.dept_ids = selectedDeptIds.value.join(',')
|
||||
}
|
||||
if (customRange.value?.[0] && customRange.value?.[1]) {
|
||||
p.start_date = customRange.value[0]
|
||||
p.end_date = customRange.value[1]
|
||||
@@ -2096,6 +2135,15 @@ function formatDoctorDailyMoney(n: number | string | undefined) {
|
||||
return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
/** 挂号率展示:null 或非有限数显示「—」;保留 2 位小数 + % 后缀 */
|
||||
function formatDoctorDailyRate(v: number | string | null | undefined): string {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n)) return '—'
|
||||
|
||||
return `${n.toFixed(2)}%`
|
||||
}
|
||||
|
||||
function getDoctorDailySummaries(param: { columns: any[] }) {
|
||||
const t = doctorDailyTotal.value
|
||||
const sums: string[] = []
|
||||
@@ -2118,6 +2166,9 @@ function getDoctorDailySummaries(param: { columns: any[] }) {
|
||||
case 'deal_order_count':
|
||||
sums[index] = String(t.deal_order_count ?? 0)
|
||||
break
|
||||
case 'appointment_total':
|
||||
sums[index] = String(t.appointment_total ?? 0)
|
||||
break
|
||||
case 'appointment_completed':
|
||||
sums[index] = String(t.appointment_completed ?? 0)
|
||||
break
|
||||
@@ -2127,6 +2178,9 @@ function getDoctorDailySummaries(param: { columns: any[] }) {
|
||||
case 'appointment_cancelled':
|
||||
sums[index] = String(t.appointment_cancelled ?? 0)
|
||||
break
|
||||
case 'appointment_conversion_rate':
|
||||
sums[index] = formatDoctorDailyRate(t.appointment_conversion_rate)
|
||||
break
|
||||
default:
|
||||
sums[index] = ''
|
||||
}
|
||||
@@ -4100,6 +4154,19 @@ onMounted(async () => {
|
||||
background: var(--yj-accent-soft) !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.doctor-daily-th__hint {
|
||||
margin-left: 4px;
|
||||
color: var(--yj-muted);
|
||||
cursor: help;
|
||||
vertical-align: -2px;
|
||||
font-size: 13px;
|
||||
transition: color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: var(--yj-brand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-surface {
|
||||
|
||||
@@ -8,11 +8,14 @@ use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\logic\stats\DoctorDailyStatsLogic;
|
||||
|
||||
/**
|
||||
* 医生日统计(系统/手动开方、成交、挂号状态)
|
||||
* 医生日统计(系统/手动开方、成交、挂号状态、总挂号、挂号率)
|
||||
*
|
||||
* - GET stats.doctorDailyStats/overview
|
||||
* ?start_date=&end_date=&channel_code=
|
||||
* 不传/忽略 dept_ids(医生统计不按展示部门检索);未传日期时由后端默认当日。
|
||||
* ?start_date=&end_date=&channel_code=&dept_ids=
|
||||
* dept_ids 可传逗号串或数组;选中父级会自动展开全部子级(与业绩看板部门树同口径)。
|
||||
* 筛选口径:部门 → admin_dept 命中的医助集合 → 该医助经手的挂号(COALESCE(a.assistant_id,u.assistant_id))/
|
||||
* 订单(o.creator_id)/ 处方(tcm_diagnosis.assistant_id),再按医生(rx.creator_id / a.doctor_id)汇总;
|
||||
* 显式选部门时隐藏全 0 医生行。未传日期时由后端默认当日。
|
||||
*/
|
||||
class DoctorDailyStatsController extends BaseAdminController
|
||||
{
|
||||
|
||||
@@ -776,11 +776,13 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
'export_channel' => '渠道',
|
||||
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
|
||||
'export_medication_form' => '药品形态',
|
||||
'export_service_package' => '服务套餐',
|
||||
'export_medication_days' => '天数',
|
||||
'export_amount' => '总金额',
|
||||
'export_paid_amount' => '已付金额',
|
||||
'export_agency_collect' => '代收金额',
|
||||
'export_tracking_number' => '快递单号',
|
||||
'export_sign_time' => '签收日期',
|
||||
'export_supply_mode' => '甘草还是自营',
|
||||
'export_gancao_prescription_cost' => '处方成本',
|
||||
'export_first_visit_assistant' => '初诊医助',
|
||||
|
||||
@@ -10,13 +10,20 @@ use app\common\service\DataScope\DataScopeService;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 医生统计:日期区间、渠道/标签、数据范围与业绩看板一致;**不按展示部门收窄**(始终按全部「中心」展示树,再套数据权限)。
|
||||
* 医生统计:日期区间、渠道/标签、数据范围与业绩看板一致;当显式传入 dept_ids 时,
|
||||
* **以部门下的医助为入口**收窄聚合(部门 → admin_dept 命中的医助集合 → 该医助经手的挂号 / 订单 / 处方 → 医生)。
|
||||
*
|
||||
* 医生范围:<b>admin_role.role_id = 1</b> 且管理员 <b>未软删</b>(delete_time 为空);再按账号「数据范围」收窄。
|
||||
*
|
||||
* - 系统/手动开方:tcm_prescription.prescription_date ∈ [start,end];渠道/标签与业绩渠道列同源 EXISTS / 诊单标签
|
||||
* - 成交:订单 create_time、排除履约 4/9/10,按处方 creator_id;渠道/标签同 sumPerformance 口径
|
||||
* - 挂号:appointment_date ∈ [start,end];选渠道时挂号 channels 命中字典值;标签渠道时 patient_id ∈ 标签诊单集
|
||||
* - 部门:dept_ids 由 YejiStatsLogic::resolveSharedYejiFilterContext 解析后透出 adminToPrimary(含全部子级展开后的 admin 集合),
|
||||
* 下方聚合在挂号 / 订单 / 处方表上用以下口径筛选医助:
|
||||
* · 挂号:COALESCE(NULLIF(a.assistant_id,0), NULLIF(u.assistant_id,0)) ∈ 医助集合
|
||||
* · 订单:o.creator_id ∈ 医助集合(与业绩归属同源)
|
||||
* · 处方:tcm_diagnosis.assistant_id ∈ 医助集合(rx.diagnosis_id JOIN tcm_diagnosis)
|
||||
* 医生集合不被部门收窄(医生通常不挂在「中心」部门);展示行最终在前端按所有医生输出,仅在 dept_filter 激活时隐藏「全 0」医生。
|
||||
*/
|
||||
class DoctorDailyStatsLogic
|
||||
{
|
||||
@@ -24,6 +31,7 @@ class DoctorDailyStatsLogic
|
||||
* @param array{
|
||||
* start_date?:string,
|
||||
* end_date?:string,
|
||||
* dept_ids?:int[]|string,
|
||||
* channel_code?:string,
|
||||
* tag_id?:string,
|
||||
* doctor_id?:int|string
|
||||
@@ -33,11 +41,9 @@ class DoctorDailyStatsLogic
|
||||
*/
|
||||
public static function overview(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
|
||||
{
|
||||
// 医生统计不参与「展示部门」检索:忽略 dept_ids,避免与业绩表部门筛选联动
|
||||
$paramsForCtx = $params;
|
||||
unset($paramsForCtx['dept_ids']);
|
||||
|
||||
$ctx = YejiStatsLogic::resolveSharedYejiFilterContext($paramsForCtx, $viewerAdminId, $viewerAdminInfo);
|
||||
// dept_ids 透传至共享上下文:未传时仍按默认「中心」树解析(仅用于挂号率默认 0 等兜底);
|
||||
// 显式传入时由下方 $deptScopedAdminIds 分支用 adminToPrimary 取出医助集合,并下推到三类聚合作为「经手医助」筛选。
|
||||
$ctx = YejiStatsLogic::resolveSharedYejiFilterContext($params, $viewerAdminId, $viewerAdminInfo);
|
||||
$startDate = $ctx['startDate'];
|
||||
$endDate = $ctx['endDate'];
|
||||
$startTs = $ctx['startTs'];
|
||||
@@ -49,6 +55,7 @@ class DoctorDailyStatsLogic
|
||||
$tagFallback = $ctx['tagFallback'];
|
||||
|
||||
$filterDoctorId = (int) ($params['doctor_id'] ?? 0);
|
||||
$deptFilterActive = self::hasExplicitDeptIds($params['dept_ids'] ?? null);
|
||||
|
||||
$doctorIds = self::resolveDoctorAdminIdsForStats($viewerAdminId, $viewerAdminInfo);
|
||||
|
||||
@@ -56,6 +63,24 @@ class DoctorDailyStatsLogic
|
||||
$doctorIds = in_array($filterDoctorId, $doctorIds, true) ? [$filterDoctorId] : [];
|
||||
}
|
||||
|
||||
// 部门下医助集合(含全部子级展开后的 admin_dept 命中者);未显式选部门时不参与筛选 → null。
|
||||
// 显式选部门但集合为空 ⇒ 该部门下无可见医助,直接返回空结果。
|
||||
$deptScopedAdminIds = null;
|
||||
if ($deptFilterActive) {
|
||||
$deptScopedAdminIds = array_values(array_unique(array_map(
|
||||
'intval',
|
||||
array_keys($ctx['adminToPrimary'] ?? [])
|
||||
)));
|
||||
if ($deptScopedAdminIds === []) {
|
||||
return [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'rows' => [],
|
||||
'total' => self::emptyTotals(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($doctorIds === []) {
|
||||
return [
|
||||
'start_date' => $startDate,
|
||||
@@ -73,7 +98,8 @@ class DoctorDailyStatsLogic
|
||||
$channelFilterActive,
|
||||
$tagDiagIds,
|
||||
$tagAssistantIds,
|
||||
$tagFallback
|
||||
$tagFallback,
|
||||
$deptScopedAdminIds
|
||||
);
|
||||
$orderMap = self::loadOrderAggregates(
|
||||
$startTs,
|
||||
@@ -83,7 +109,8 @@ class DoctorDailyStatsLogic
|
||||
$channelFilterActive,
|
||||
$tagDiagIds,
|
||||
$tagAssistantIds,
|
||||
$tagFallback
|
||||
$tagFallback,
|
||||
$deptScopedAdminIds
|
||||
);
|
||||
$apptMap = self::loadAppointmentAggregates(
|
||||
$startDate,
|
||||
@@ -91,7 +118,8 @@ class DoctorDailyStatsLogic
|
||||
$doctorIds,
|
||||
$appointmentChannelValues,
|
||||
$channelFilterActive,
|
||||
$tagDiagIds
|
||||
$tagDiagIds,
|
||||
$deptScopedAdminIds
|
||||
);
|
||||
|
||||
$adminRows = Admin::whereIn('id', $doctorIds)
|
||||
@@ -109,9 +137,10 @@ class DoctorDailyStatsLogic
|
||||
foreach ($doctorIds as $aid) {
|
||||
$rx = $rxMap[$aid] ?? ['system' => 0, 'manual' => 0];
|
||||
$ord = $orderMap[$aid] ?? ['amount' => 0.0, 'count' => 0];
|
||||
$ap = $apptMap[$aid] ?? ['completed' => 0, 'missed' => 0, 'cancelled' => 0];
|
||||
$ap = $apptMap[$aid] ?? ['completed' => 0, 'missed' => 0, 'cancelled' => 0, 'total' => 0];
|
||||
$cnt = (int) $ord['count'];
|
||||
$amt = round((float) $ord['amount'], 2);
|
||||
$apTotal = (int) ($ap['total'] ?? 0);
|
||||
$rows[] = [
|
||||
'admin_id' => $aid,
|
||||
'doctor_name' => $nameById[$aid] ?? ('#' . $aid),
|
||||
@@ -120,12 +149,26 @@ class DoctorDailyStatsLogic
|
||||
'deal_amount' => $amt,
|
||||
'deal_order_count' => $cnt,
|
||||
'avg_deal_amount' => $cnt > 0 ? round($amt / $cnt, 2) : null,
|
||||
'appointment_total' => $apTotal,
|
||||
'appointment_completed' => (int) $ap['completed'],
|
||||
'appointment_missed' => (int) $ap['missed'],
|
||||
'appointment_cancelled' => (int) $ap['cancelled'],
|
||||
// 挂号率 = 成交单数 / 总挂号数 × 100;总挂号为 0 时返回 null(前端展示「—」)。
|
||||
'appointment_conversion_rate' => $apTotal > 0 ? round($cnt / $apTotal * 100, 2) : null,
|
||||
];
|
||||
}
|
||||
|
||||
// 显式部门筛选时隐藏「该部门无任何关联」的医生,避免列出大量全 0 行。
|
||||
if ($deptFilterActive) {
|
||||
$rows = array_values(array_filter($rows, static function (array $r): bool {
|
||||
return (int) ($r['system_prescription_count'] ?? 0) > 0
|
||||
|| (int) ($r['manual_prescription_count'] ?? 0) > 0
|
||||
|| (float) ($r['deal_amount'] ?? 0) > 0
|
||||
|| (int) ($r['deal_order_count'] ?? 0) > 0
|
||||
|| (int) ($r['appointment_total'] ?? 0) > 0;
|
||||
}));
|
||||
}
|
||||
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
if (($a['deal_amount'] ?? 0) != ($b['deal_amount'] ?? 0)) {
|
||||
return ($b['deal_amount'] ?? 0) <=> ($a['deal_amount'] ?? 0);
|
||||
@@ -142,6 +185,31 @@ class DoctorDailyStatsLogic
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显式传入 dept_ids(含 1 个以上正整数即视为显式)。与 YejiStatsLogic::hasExplicitDeptIdsParam 同口径。
|
||||
*
|
||||
* @param mixed $raw
|
||||
*/
|
||||
private static function hasExplicitDeptIds($raw): bool
|
||||
{
|
||||
if (\is_string($raw) && trim($raw) !== '') {
|
||||
foreach (explode(',', $raw) as $p) {
|
||||
if ((int) trim($p) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (\is_array($raw)) {
|
||||
foreach ($raw as $v) {
|
||||
if ((int) $v > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 医生角色(role_id=1)、管理员未删除、且在数据范围内的 admin_id。
|
||||
*
|
||||
@@ -186,9 +254,11 @@ class DoctorDailyStatsLogic
|
||||
'deal_amount' => 0.0,
|
||||
'deal_order_count' => 0,
|
||||
'avg_deal_amount' => null,
|
||||
'appointment_total' => 0,
|
||||
'appointment_completed' => 0,
|
||||
'appointment_missed' => 0,
|
||||
'appointment_cancelled' => 0,
|
||||
'appointment_conversion_rate' => null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -205,6 +275,7 @@ class DoctorDailyStatsLogic
|
||||
$t['manual_prescription_count'] += (int) ($r['manual_prescription_count'] ?? 0);
|
||||
$t['deal_amount'] += (float) ($r['deal_amount'] ?? 0);
|
||||
$t['deal_order_count'] += (int) ($r['deal_order_count'] ?? 0);
|
||||
$t['appointment_total'] += (int) ($r['appointment_total'] ?? 0);
|
||||
$t['appointment_completed'] += (int) ($r['appointment_completed'] ?? 0);
|
||||
$t['appointment_missed'] += (int) ($r['appointment_missed'] ?? 0);
|
||||
$t['appointment_cancelled'] += (int) ($r['appointment_cancelled'] ?? 0);
|
||||
@@ -212,6 +283,9 @@ class DoctorDailyStatsLogic
|
||||
$t['deal_amount'] = round((float) $t['deal_amount'], 2);
|
||||
$dc = (int) $t['deal_order_count'];
|
||||
$t['avg_deal_amount'] = $dc > 0 ? round((float) $t['deal_amount'] / $dc, 2) : null;
|
||||
$apTotal = (int) $t['appointment_total'];
|
||||
// 合计行挂号率:行行加总后再统一计算,与汇总后的 成交单数 / 总挂号数 对齐。
|
||||
$t['appointment_conversion_rate'] = $apTotal > 0 ? round($dc / $apTotal * 100, 2) : null;
|
||||
|
||||
return $t;
|
||||
}
|
||||
@@ -220,6 +294,7 @@ class DoctorDailyStatsLogic
|
||||
* @param int[] $appointmentChannelValues
|
||||
* @param int[]|null $tagDiagIds
|
||||
* @param array<int,int>|null $tagAssistantIds
|
||||
* @param int[]|null $deptScopedAdminIds 部门下医助集合:非 null 时加 tcm_diagnosis.assistant_id IN (...) 约束
|
||||
*
|
||||
* @return array<int, array{system:int, manual:int}>
|
||||
*/
|
||||
@@ -231,11 +306,15 @@ class DoctorDailyStatsLogic
|
||||
bool $channelFilterActive,
|
||||
?array $tagDiagIds,
|
||||
?array $tagAssistantIds,
|
||||
bool $tagFallback
|
||||
bool $tagFallback,
|
||||
?array $deptScopedAdminIds = null
|
||||
): array {
|
||||
if (!self::tagScopeNonEmpty($tagDiagIds, $tagAssistantIds)) {
|
||||
return [];
|
||||
}
|
||||
if ($deptScopedAdminIds !== null && $deptScopedAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = Db::name('tcm_prescription')
|
||||
->alias('rx')
|
||||
@@ -245,6 +324,12 @@ class DoctorDailyStatsLogic
|
||||
->whereIn('rx.creator_id', $doctorIds)
|
||||
->where('rx.diagnosis_id', '>', 0);
|
||||
|
||||
// 部门 → 医助:仅保留经手医助归属在所选部门子树的处方(通过诊单 assistant_id 关联)
|
||||
if ($deptScopedAdminIds !== null) {
|
||||
$query->join('tcm_diagnosis dg', 'dg.id = rx.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
||||
->whereIn('dg.assistant_id', $deptScopedAdminIds);
|
||||
}
|
||||
|
||||
self::applyPrescriptionChannelTagFilter(
|
||||
$query,
|
||||
'rx',
|
||||
@@ -280,6 +365,7 @@ class DoctorDailyStatsLogic
|
||||
* @param int[] $appointmentChannelValues
|
||||
* @param int[]|null $tagDiagIds
|
||||
* @param array<int,int>|null $tagAssistantIds
|
||||
* @param int[]|null $deptScopedAdminIds 部门下医助集合:非 null 时加 o.creator_id IN (...) 约束(与业绩归属同源)
|
||||
*
|
||||
* @return array<int, array{amount: float, count: int}>
|
||||
*/
|
||||
@@ -291,11 +377,15 @@ class DoctorDailyStatsLogic
|
||||
bool $channelFilterActive,
|
||||
?array $tagDiagIds,
|
||||
?array $tagAssistantIds,
|
||||
bool $tagFallback
|
||||
bool $tagFallback,
|
||||
?array $deptScopedAdminIds = null
|
||||
): array {
|
||||
if (!self::tagScopeNonEmpty($tagDiagIds, $tagAssistantIds)) {
|
||||
return [];
|
||||
}
|
||||
if ($deptScopedAdminIds !== null && $deptScopedAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
@@ -306,6 +396,11 @@ class DoctorDailyStatsLogic
|
||||
$q->whereIn('rx.creator_id', $doctorIds)
|
||||
->where('o.diagnosis_id', '>', 0);
|
||||
|
||||
// 部门 → 医助:业绩归属同源(o.creator_id 即订单创建医助)
|
||||
if ($deptScopedAdminIds !== null) {
|
||||
$q->whereIn('o.creator_id', $deptScopedAdminIds);
|
||||
}
|
||||
|
||||
$normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues);
|
||||
if ($normCh !== []) {
|
||||
$apTable = self::tableWithPrefix('doctor_appointment');
|
||||
@@ -423,8 +518,10 @@ class DoctorDailyStatsLogic
|
||||
/**
|
||||
* @param int[] $appointmentChannelValues
|
||||
* @param int[]|null $tagDiagIds
|
||||
* @param int[]|null $deptScopedAdminIds 部门下医助集合:非 null 时加
|
||||
* COALESCE(NULLIF(a.assistant_id,0), NULLIF(u.assistant_id,0)) IN (...) 约束
|
||||
*
|
||||
* @return array<int, array{completed:int, missed:int, cancelled:int}>
|
||||
* @return array<int, array{completed:int, missed:int, cancelled:int, total:int}>
|
||||
*/
|
||||
private static function loadAppointmentAggregates(
|
||||
string $startDate,
|
||||
@@ -432,29 +529,54 @@ class DoctorDailyStatsLogic
|
||||
array $doctorIds,
|
||||
array $appointmentChannelValues,
|
||||
bool $channelFilterActive,
|
||||
?array $tagDiagIds
|
||||
?array $tagDiagIds,
|
||||
?array $deptScopedAdminIds = null
|
||||
): array {
|
||||
$q = Db::name('doctor_appointment')
|
||||
->whereBetween('appointment_date', [$startDate, $endDate])
|
||||
->whereIn('doctor_id', $doctorIds);
|
||||
if ($deptScopedAdminIds !== null && $deptScopedAdminIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$needsDiagJoin = $deptScopedAdminIds !== null;
|
||||
if ($needsDiagJoin) {
|
||||
$q = Db::name('doctor_appointment')->alias('a')
|
||||
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
|
||||
->whereBetween('a.appointment_date', [$startDate, $endDate])
|
||||
->whereIn('a.doctor_id', $doctorIds)
|
||||
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)');
|
||||
$aliasPrefix = 'a.';
|
||||
} else {
|
||||
$q = Db::name('doctor_appointment')
|
||||
->whereBetween('appointment_date', [$startDate, $endDate])
|
||||
->whereIn('doctor_id', $doctorIds);
|
||||
$aliasPrefix = '';
|
||||
}
|
||||
|
||||
$normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues);
|
||||
if ($normCh !== []) {
|
||||
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $normCh)));
|
||||
$q->whereIn('channels', $strVals);
|
||||
$q->whereIn($aliasPrefix . 'channels', $strVals);
|
||||
} elseif ($channelFilterActive && $tagDiagIds !== null) {
|
||||
if ($tagDiagIds === []) {
|
||||
return [];
|
||||
}
|
||||
$q->whereIn('patient_id', $tagDiagIds);
|
||||
$q->whereIn($aliasPrefix . 'patient_id', $tagDiagIds);
|
||||
}
|
||||
|
||||
// 部门 → 医助:与业绩看板 consultEffectiveAssistantSql 同口径 —— 优先挂号创建人,回退诊单医助
|
||||
if ($needsDiagJoin) {
|
||||
$effIds = array_map('intval', $deptScopedAdminIds);
|
||||
$inList = implode(',', $effIds);
|
||||
$q->whereRaw('COALESCE(NULLIF(a.assistant_id, 0), NULLIF(u.assistant_id, 0)) IN (' . $inList . ')');
|
||||
}
|
||||
|
||||
// total = 当前筛选下挂号总数(含 status=1 已预约/2 已取消/3 已完成/4 已过号),用于计算「挂号率」。
|
||||
$q->field([
|
||||
'doctor_id',
|
||||
Db::raw('SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS completed'),
|
||||
Db::raw('SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) AS missed'),
|
||||
Db::raw('SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS cancelled'),
|
||||
])->group('doctor_id');
|
||||
$aliasPrefix . 'doctor_id',
|
||||
Db::raw('COUNT(*) AS total'),
|
||||
Db::raw('SUM(CASE WHEN ' . $aliasPrefix . 'status = 3 THEN 1 ELSE 0 END) AS completed'),
|
||||
Db::raw('SUM(CASE WHEN ' . $aliasPrefix . 'status = 4 THEN 1 ELSE 0 END) AS missed'),
|
||||
Db::raw('SUM(CASE WHEN ' . $aliasPrefix . 'status = 2 THEN 1 ELSE 0 END) AS cancelled'),
|
||||
])->group($aliasPrefix . 'doctor_id');
|
||||
|
||||
$out = [];
|
||||
foreach ($q->select()->toArray() as $r) {
|
||||
@@ -463,6 +585,7 @@ class DoctorDailyStatsLogic
|
||||
continue;
|
||||
}
|
||||
$out[$id] = [
|
||||
'total' => (int) ($r['total'] ?? 0),
|
||||
'completed' => (int) ($r['completed'] ?? 0),
|
||||
'missed' => (int) ($r['missed'] ?? 0),
|
||||
'cancelled' => (int) ($r['cancelled'] ?? 0),
|
||||
|
||||
@@ -185,6 +185,7 @@ class PrescriptionLogic
|
||||
'bags_per_dose' => isset($raw['bags_per_dose']) ? (int) $raw['bags_per_dose'] : 1,
|
||||
'times_per_day' => (int) ($raw['times_per_day'] ?? 3),
|
||||
'usage_days' => (int) ($raw['usage_days'] ?? 7),
|
||||
'prescription_name' => trim((string) ($raw['prescription_name'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2693,6 +2693,36 @@ class PrescriptionOrderLogic
|
||||
return $pathSegments === [] ? '' : implode(';', $pathSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出用:服务套餐(与列表/详情 formatServicePackage 同口径,按 dict_data type_value=server_order 解析)
|
||||
* 入参可能是逗号分隔字符串或数组;空串返回空,避免 Excel 显示「—」占位。
|
||||
*
|
||||
* @param mixed $value zyt_tcm_prescription_order.service_package
|
||||
* @param array<string, string> $packageNameByValue dict_data server_order value=>name 字典缓存
|
||||
*/
|
||||
public static function formatServicePackageForExport($value, array $packageNameByValue): string
|
||||
{
|
||||
if ($value === null || $value === '' || $value === false) {
|
||||
return '';
|
||||
}
|
||||
$items = [];
|
||||
if (\is_array($value)) {
|
||||
$items = $value;
|
||||
} else {
|
||||
$items = explode(',', (string) $value);
|
||||
}
|
||||
$names = [];
|
||||
foreach ($items as $raw) {
|
||||
$key = trim((string) $raw);
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$names[] = (string) ($packageNameByValue[$key] ?? $packageNameByValue[(string) (int) $key] ?? $key);
|
||||
}
|
||||
|
||||
return $names === [] ? '' : implode('、', $names);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出用:药品形态(丸剂 / 饮片+代煎|自煎等)
|
||||
*
|
||||
@@ -2944,6 +2974,117 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
}
|
||||
|
||||
// 服务套餐字典:与前端 formatServicePackage 同口径(dict_data type_value=server_order;只取启用项做兜底取原值)
|
||||
$packageNameByValue = [];
|
||||
$hasAnyPackage = false;
|
||||
foreach ($lists as $row) {
|
||||
$pkRaw = $row['service_package'] ?? '';
|
||||
if (\is_array($pkRaw)) {
|
||||
if ($pkRaw !== []) {
|
||||
$hasAnyPackage = true;
|
||||
break;
|
||||
}
|
||||
} elseif (trim((string) $pkRaw) !== '') {
|
||||
$hasAnyPackage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($hasAnyPackage) {
|
||||
try {
|
||||
$packageNameByValue = DictData::where('type_value', 'server_order')->column('name', 'value');
|
||||
} catch (\Throwable) {
|
||||
$packageNameByValue = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 签收时间批量解析:与 ExpressTrackingService::effectiveSignUnixFromTrackingDetail 同口径,
|
||||
// 取 GREATEST(sign_time, max 签收类轨迹 trace_time_stamp, 已签收主表时的最新轨迹 trace_time_stamp);
|
||||
// 与详情/业绩看板提成「签收时间」语义一致,避免历史数据 sign_time=0 但实际已签收时被当作「未签收」。
|
||||
/** @var array<string, int> $signTsByTracking trim 后的 tracking_number => Unix 秒 */
|
||||
$signTsByTracking = [];
|
||||
$trackingNumbers = [];
|
||||
foreach ($lists as $row) {
|
||||
$tn = trim((string) ($row['tracking_number'] ?? ''));
|
||||
if ($tn !== '') {
|
||||
$trackingNumbers[$tn] = true;
|
||||
}
|
||||
}
|
||||
$trackingNumbers = array_keys($trackingNumbers);
|
||||
if ($trackingNumbers !== []) {
|
||||
try {
|
||||
$etRows = Db::name('express_tracking')
|
||||
->whereIn('tracking_number', $trackingNumbers)
|
||||
->whereNull('delete_time')
|
||||
->field(['id', 'tracking_number', 'sign_time', 'current_state', 'is_signed'])
|
||||
->select()
|
||||
->toArray();
|
||||
$etIdToNum = [];
|
||||
$signedEtIds = [];
|
||||
$allEtIds = [];
|
||||
foreach ($etRows as $r) {
|
||||
$tn = trim((string) ($r['tracking_number'] ?? ''));
|
||||
$eid = (int) ($r['id'] ?? 0);
|
||||
if ($tn === '' || $eid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$etIdToNum[$eid] = $tn;
|
||||
$allEtIds[] = $eid;
|
||||
$st = (int) ($r['sign_time'] ?? 0);
|
||||
if ($st > 0) {
|
||||
$signTsByTracking[$tn] = max($signTsByTracking[$tn] ?? 0, $st);
|
||||
}
|
||||
if ((string) ($r['current_state'] ?? '') === '3' || (int) ($r['is_signed'] ?? 0) === 1) {
|
||||
$signedEtIds[$eid] = true;
|
||||
}
|
||||
}
|
||||
if ($allEtIds !== []) {
|
||||
$sigCond = "(`status_code` IN ('3','301','302','304') "
|
||||
. "OR `status` LIKE '%签收%' "
|
||||
. "OR `trace_context` LIKE '%签收%' "
|
||||
. "OR `trace_context` LIKE '%妥投%' "
|
||||
. "OR `trace_context` LIKE '%已送达%' "
|
||||
. "OR `trace_context` LIKE '%本人签收%')";
|
||||
$sigTraces = Db::name('express_trace')
|
||||
->whereIn('tracking_id', $allEtIds)
|
||||
->where('trace_time_stamp', '>', 0)
|
||||
->whereRaw($sigCond)
|
||||
->fieldRaw('tracking_id, MAX(trace_time_stamp) AS max_ts')
|
||||
->group('tracking_id')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($sigTraces as $t) {
|
||||
$eid = (int) ($t['tracking_id'] ?? 0);
|
||||
$tn = $etIdToNum[$eid] ?? '';
|
||||
$ts = (int) ($t['max_ts'] ?? 0);
|
||||
if ($tn !== '' && $ts > 0) {
|
||||
$signTsByTracking[$tn] = max($signTsByTracking[$tn] ?? 0, $ts);
|
||||
}
|
||||
}
|
||||
$signedIdList = array_keys($signedEtIds);
|
||||
if ($signedIdList !== []) {
|
||||
$anyTraces = Db::name('express_trace')
|
||||
->whereIn('tracking_id', $signedIdList)
|
||||
->where('trace_time_stamp', '>', 0)
|
||||
->fieldRaw('tracking_id, MAX(trace_time_stamp) AS max_ts')
|
||||
->group('tracking_id')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($anyTraces as $t) {
|
||||
$eid = (int) ($t['tracking_id'] ?? 0);
|
||||
$tn = $etIdToNum[$eid] ?? '';
|
||||
$ts = (int) ($t['max_ts'] ?? 0);
|
||||
if ($tn !== '' && $ts > 0) {
|
||||
$signTsByTracking[$tn] = max($signTsByTracking[$tn] ?? 0, $ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('appendPrescriptionOrderListExportRows resolve sign_time failed: ' . $e->getMessage());
|
||||
$signTsByTracking = [];
|
||||
}
|
||||
}
|
||||
|
||||
$assistantDeptCache = [];
|
||||
|
||||
foreach ($lists as &$item) {
|
||||
@@ -2982,6 +3123,10 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
|
||||
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
|
||||
$item['export_service_package'] = self::formatServicePackageForExport(
|
||||
$item['service_package'] ?? '',
|
||||
$packageNameByValue
|
||||
);
|
||||
|
||||
$item['export_channel'] = (string) ($item['service_channel'] ?? '');
|
||||
$apptIdResolved = $resolvedApptIdByPoId[$poId] ?? 0;
|
||||
@@ -3000,6 +3145,9 @@ class PrescriptionOrderLogic
|
||||
$item['export_agency_collect'] = number_format(round($amt - $paid, 2), 2, '.', '');
|
||||
|
||||
$item['export_tracking_number'] = (string) ($item['tracking_number'] ?? '');
|
||||
$tnTrim = trim((string) ($item['tracking_number'] ?? ''));
|
||||
$signTs = $tnTrim !== '' ? (int) ($signTsByTracking[$tnTrim] ?? 0) : 0;
|
||||
$item['export_sign_time'] = $signTs > 0 ? date('Y-m-d', $signTs) : '';
|
||||
$isGc = trim((string) ($item['gancao_reciperl_order_no'] ?? '')) !== '';
|
||||
$item['export_supply_mode'] = $isGc ? '甘草' : '自营';
|
||||
// 「处方成本」列:与列表/详情一致,取订单 internal_cost(含「测试价格」预报价写入;甘草/自营均导出)
|
||||
|
||||
Reference in New Issue
Block a user