This commit is contained in:
Your Name
2026-05-06 18:17:37 +08:00
parent c389fea73c
commit a7fd55aa1c
16 changed files with 2069 additions and 375 deletions
+13
View File
@@ -68,6 +68,19 @@ export function yejiStatsLeadLines(params: {
return request.get({ url: '/stats.yejiStats/leadLines', params }) return request.get({ url: '/stats.yejiStats/leadLines', params })
} }
/** 二中心复诊:部门行下钻医助 × 业务订单笔数 */
export function yejiStatsRevisitBreakdown(params: {
start_date: string
end_date: string
dept_id: number
revisit_slot: number
dept_ids?: string
channel_code?: string
tag_id?: string
}) {
return request.get({ url: '/stats.yejiStats/revisitBreakdown', params })
}
export function doctorDailyStatsOverview(params: { export function doctorDailyStatsOverview(params: {
start_date?: string start_date?: string
end_date?: string end_date?: string
+108 -3
View File
@@ -669,6 +669,49 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-alert
v-if="editMode === 'edit'"
type="warning"
:closable="false"
show-icon
class="mt-3"
v-loading="rxLinkedOrderHint.loading"
>
<template #title>业务订单要求(请与本处方保持一致)</template>
<div class="text-sm mt-1 leading-relaxed">
<template
v-if="
rxLinkedOrderHint.orderNo ||
rxLinkedOrderHint.medicationDays != null ||
rxLinkedOrderHint.remarkAssistant
"
>
<p v-if="rxLinkedOrderHint.orderNo" class="mb-1.5">
最近关联订单(按创建倒序):
<span class="font-mono text-primary">{{ rxLinkedOrderHint.orderNo }}</span>
</p>
<p class="mb-1.5">
<span class="font-semibold">服用天数(订单)</span>
{{
rxLinkedOrderHint.medicationDays != null
? `${rxLinkedOrderHint.medicationDays} 天`
: '— 订单未填写'
}}
</p>
<p>
<span class="font-semibold">医助备注(订单)</span>
<span class="whitespace-pre-wrap">{{
rxLinkedOrderHint.remarkAssistant || '— 无'
}}</span>
</p>
</template>
<p v-else-if="!rxLinkedOrderHint.loading" class="text-gray-600">
当前处方暂无关联业务订单,或您无订单列表权限;若有订单,将在此处显示订单内
<span class="font-semibold">服用天数</span>与
<span class="font-semibold">医助备注</span>。
</p>
</div>
</el-alert>
</div> </div>
</el-form-item> </el-form-item>
@@ -1183,7 +1226,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="备注"> <el-form-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注">
<el-input <el-input
v-model="createOrderForm.remark_extra" v-model="createOrderForm.remark_extra"
type="textarea" type="textarea"
@@ -1193,6 +1236,16 @@
show-word-limit show-word-limit
/> />
</el-form-item> </el-form-item>
<el-form-item label="医助备注">
<el-input
v-model="createOrderForm.remark_assistant"
type="textarea"
:rows="3"
placeholder="选填,医助对内说明"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -1333,6 +1386,7 @@ import {
prescriptionDetail, prescriptionDetail,
prescriptionOrderCreate, prescriptionOrderCreate,
prescriptionOrderPaidPayOrders, prescriptionOrderPaidPayOrders,
prescriptionOrderLists,
getDoctors, getDoctors,
prescriptionLibraryLists prescriptionLibraryLists
} from '@/api/tcm' } from '@/api/tcm'
@@ -1475,6 +1529,7 @@ const createOrderForm = reactive({
amount: 0, amount: 0,
internal_cost: undefined as number | undefined, internal_cost: undefined as number | undefined,
remark_extra: '', remark_extra: '',
remark_assistant: '',
pay_order_ids: [] as number[] pay_order_ids: [] as number[]
}) })
@@ -1714,6 +1769,7 @@ function resetCreateOrderForm() {
createOrderForm.amount = 0 createOrderForm.amount = 0
createOrderForm.internal_cost = undefined createOrderForm.internal_cost = undefined
createOrderForm.remark_extra = '' createOrderForm.remark_extra = ''
createOrderForm.remark_assistant = ''
createOrderForm.pay_order_ids = [] createOrderForm.pay_order_ids = []
createOrderPatientList.value = [] createOrderPatientList.value = []
createOrderPaidOrders.value = [] createOrderPaidOrders.value = []
@@ -1884,7 +1940,8 @@ async function submitCreateOrderFromPrescription() {
tracking_number: createOrderForm.tracking_number || '', tracking_number: createOrderForm.tracking_number || '',
fee_type: createOrderForm.fee_type, fee_type: createOrderForm.fee_type,
amount: createOrderForm.amount, amount: createOrderForm.amount,
remark_extra: createOrderForm.remark_extra || '' remark_extra: createOrderForm.remark_extra || '',
remark_assistant: createOrderForm.remark_assistant || ''
} }
// 添加省市区字段 // 添加省市区字段
@@ -2238,6 +2295,52 @@ const editForm = reactive({
times_per_day: 2 times_per_day: 2
}) })
/** 编辑页:关联业务订单上的服用天数、医助备注(与处方提示一致) */
const rxLinkedOrderHint = reactive({
loading: false,
orderNo: '',
medicationDays: null as number | null,
remarkAssistant: ''
})
function clearRxLinkedOrderHint() {
rxLinkedOrderHint.loading = false
rxLinkedOrderHint.orderNo = ''
rxLinkedOrderHint.medicationDays = null
rxLinkedOrderHint.remarkAssistant = ''
}
async function loadRxLinkedOrderHintForEdit(prescriptionId: number) {
rxLinkedOrderHint.orderNo = ''
rxLinkedOrderHint.medicationDays = null
rxLinkedOrderHint.remarkAssistant = ''
if (!prescriptionId) {
rxLinkedOrderHint.loading = false
return
}
rxLinkedOrderHint.loading = true
try {
const data: any = await prescriptionOrderLists({
page_no: 1,
page_size: 5,
prescription_id: prescriptionId
})
const lists = data?.lists
const row = Array.isArray(lists) && lists.length ? lists[0] : null
if (row) {
rxLinkedOrderHint.orderNo = String(row.order_no || '').trim()
const md = row.medication_days
const mdn = md !== null && md !== undefined && String(md).trim() !== '' ? Number(md) : NaN
rxLinkedOrderHint.medicationDays = Number.isFinite(mdn) && mdn > 0 ? mdn : null
rxLinkedOrderHint.remarkAssistant = String(row.remark_assistant || '').trim()
}
} catch {
/* 无列表权限或网络错误:保持空 */
} finally {
rxLinkedOrderHint.loading = false
}
}
const diagnosisViewRef = ref<{ const diagnosisViewRef = ref<{
openViewOnly: (id: number) => Promise<void> openViewOnly: (id: number) => Promise<void>
} | null>(null) } | null>(null)
@@ -2882,6 +2985,7 @@ const resetForm = () => {
editForm.is_system_auto = 0 editForm.is_system_auto = 0
editForm.business_prescription_audit_rejected = 0 editForm.business_prescription_audit_rejected = 0
editForm.business_prescription_audit_remark = '' editForm.business_prescription_audit_remark = ''
clearRxLinkedOrderHint()
} }
// 新增处方 // 新增处方
@@ -3009,8 +3113,9 @@ const handleEdit = async (row: any) => {
setTimeout(() => { setTimeout(() => {
isLoadingData.value = false isLoadingData.value = false
}, 0) }, 0)
showEdit.value = true showEdit.value = true
void loadRxLinkedOrderHintForEdit(Number(editForm.id))
nextTick(() => { nextTick(() => {
formRef.value?.clearValidate() formRef.value?.clearValidate()
scheduleEditSignatureInit() scheduleEditSignatureInit()
@@ -994,7 +994,7 @@
<template v-if="detailLinkedAppointment"> <template v-if="detailLinkedAppointment">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px]"> <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px]">
<span <span
>预约 <span class="font-mono text-primary">#{{ detailLinkedAppointment.id }}</span></span >流转编号(挂号) <span class="font-mono text-primary">#{{ detailLinkedAppointment.id }}</span></span
> >
<span class="text-gray-500" <span class="text-gray-500"
>时间:{{ detailLinkedAppointment.appointment_date }} >时间:{{ detailLinkedAppointment.appointment_date }}
@@ -1056,7 +1056,8 @@
{{ expressCompanyLabel(detailData.express_company) }} {{ expressCompanyLabel(detailData.express_company) }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{ detailData.remark_extra || '—' }}</el-descriptions-item> <el-descriptions-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注" :span="3">{{ detailData.remark_extra || '—' }}</el-descriptions-item>
<el-descriptions-item label="医助备注" :span="3">{{ detailData.remark_assistant || '—' }}</el-descriptions-item>
<el-descriptions-item label="处方审核意见" :span="3"> <el-descriptions-item label="处方审核意见" :span="3">
<span class="po-audit-remark">{{ detailData.prescription_audit_remark || '—' }}</span> <span class="po-audit-remark">{{ detailData.prescription_audit_remark || '—' }}</span>
</el-descriptions-item> </el-descriptions-item>
@@ -1527,7 +1528,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="备注"> <el-form-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注">
<el-input <el-input
v-model="editForm.remark_extra" v-model="editForm.remark_extra"
type="textarea" type="textarea"
@@ -1537,6 +1538,16 @@
show-word-limit show-word-limit
/> />
</el-form-item> </el-form-item>
<el-form-item label="医助备注">
<el-input
v-model="editForm.remark_assistant"
type="textarea"
:rows="3"
placeholder="选填,医助对内说明"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -2032,11 +2043,11 @@
<div class="rx-rp-cols-head"> <div class="rx-rp-cols-head">
<div class="rx-rp-col-head"> <div class="rx-rp-col-head">
<span>用药 (单剂)</span> <span>用药 (单剂)</span>
<span>总量</span> <span>{{ prescriptionTabType === 'user' ? '用量' : '总量' }}</span>
</div> </div>
<div class="rx-rp-col-head"> <div class="rx-rp-col-head">
<span>用药 (单剂)</span> <span>用药 (单剂)</span>
<span>总量</span> <span>{{ prescriptionTabType === 'user' ? '用量' : '总量' }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -3521,6 +3532,7 @@ const editForm = reactive({
fee_type: 3, fee_type: 3,
amount: 0, amount: 0,
remark_extra: '', remark_extra: '',
remark_assistant: '',
pay_order_ids: [] as number[], pay_order_ids: [] as number[],
internal_cost: undefined as number | string | undefined, internal_cost: undefined as number | string | undefined,
/** 关联诊单医助 id(只读,来自订单详情;列表筛选用) */ /** 关联诊单医助 id(只读,来自订单详情;列表筛选用) */
@@ -3771,6 +3783,7 @@ async function openEdit(row: { id: number }) {
editForm.amount = amt editForm.amount = amt
editForm.remark_extra = d.remark_extra || '' editForm.remark_extra = d.remark_extra || ''
editForm.remark_assistant = d.remark_assistant || ''
const pids = d.pay_order_ids const pids = d.pay_order_ids
editForm.pay_order_ids = Array.isArray(pids) ? pids.map((x: unknown) => Number(x)).filter((n) => !Number.isNaN(n)) : [] editForm.pay_order_ids = Array.isArray(pids) ? pids.map((x: unknown) => Number(x)).filter((n) => !Number.isNaN(n)) : []
editForm.internal_cost = editForm.internal_cost =
@@ -3809,6 +3822,7 @@ async function submitEdit() {
fee_type: editForm.fee_type, fee_type: editForm.fee_type,
amount: editForm.amount, amount: editForm.amount,
remark_extra: editForm.remark_extra || '', remark_extra: editForm.remark_extra || '',
remark_assistant: editForm.remark_assistant || '',
// BUG FIX: 修复之前无法清空部分选填字段的问题 // BUG FIX: 修复之前无法清空部分选填字段的问题
medication_days: (editForm.medication_days != null && String(editForm.medication_days).trim() !== '') ? editForm.medication_days : '', medication_days: (editForm.medication_days != null && String(editForm.medication_days).trim() !== '') ? editForm.medication_days : '',
@@ -4090,6 +4104,7 @@ async function submitQuickTrack() {
fee_type: Number(d.fee_type) || 3, fee_type: Number(d.fee_type) || 3,
amount: Number(d.amount) || 0, amount: Number(d.amount) || 0,
remark_extra: d.remark_extra || '', remark_extra: d.remark_extra || '',
remark_assistant: d.remark_assistant || '',
internal_cost: (d.internal_cost != null && d.internal_cost !== '') ? d.internal_cost : '', internal_cost: (d.internal_cost != null && d.internal_cost !== '') ? d.internal_cost : '',
pay_order_ids: Array.isArray(d.pay_order_ids) ? d.pay_order_ids : [], pay_order_ids: Array.isArray(d.pay_order_ids) ? d.pay_order_ids : [],
// 仅覆盖快递字段 // 仅覆盖快递字段
@@ -1091,7 +1091,8 @@
{{ expressCompanyLabel(detailData.express_company) }} {{ expressCompanyLabel(detailData.express_company) }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{ detailData.remark_extra || '—' }}</el-descriptions-item> <el-descriptions-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="备注" :span="3">{{ detailData.remark_extra || '' }}</el-descriptions-item>
<el-descriptions-item label="医助备注" :span="3">{{ detailData.remark_assistant || '—' }}</el-descriptions-item>
<el-descriptions-item label="处方审核意见" :span="3"> <el-descriptions-item label="处方审核意见" :span="3">
<span class="po-audit-remark">{{ detailData.prescription_audit_remark || '—' }}</span> <span class="po-audit-remark">{{ detailData.prescription_audit_remark || '—' }}</span>
</el-descriptions-item> </el-descriptions-item>
@@ -1563,7 +1564,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="备注"> <el-form-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注">
<el-input <el-input
v-model="editForm.remark_extra" v-model="editForm.remark_extra"
type="textarea" type="textarea"
@@ -1573,6 +1574,16 @@
show-word-limit show-word-limit
/> />
</el-form-item> </el-form-item>
<el-form-item label="医助备注">
<el-input
v-model="editForm.remark_assistant"
type="textarea"
:rows="3"
placeholder="选填,医助对内说明"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -3575,6 +3586,7 @@ const editForm = reactive({
fee_type: 3, fee_type: 3,
amount: 0, amount: 0,
remark_extra: '', remark_extra: '',
remark_assistant: '',
pay_order_ids: [] as number[], pay_order_ids: [] as number[],
internal_cost: undefined as number | string | undefined, internal_cost: undefined as number | string | undefined,
/** 关联诊单医助 id(只读,来自订单详情;列表筛选用) */ /** 关联诊单医助 id(只读,来自订单详情;列表筛选用) */
@@ -3825,6 +3837,7 @@ async function openEdit(row: { id: number }) {
editForm.amount = amt editForm.amount = amt
editForm.remark_extra = d.remark_extra || '' editForm.remark_extra = d.remark_extra || ''
editForm.remark_assistant = d.remark_assistant || ''
const pids = d.pay_order_ids const pids = d.pay_order_ids
editForm.pay_order_ids = Array.isArray(pids) ? pids.map((x: unknown) => Number(x)).filter((n) => !Number.isNaN(n)) : [] editForm.pay_order_ids = Array.isArray(pids) ? pids.map((x: unknown) => Number(x)).filter((n) => !Number.isNaN(n)) : []
editForm.internal_cost = editForm.internal_cost =
@@ -3863,6 +3876,7 @@ async function submitEdit() {
fee_type: editForm.fee_type, fee_type: editForm.fee_type,
amount: editForm.amount, amount: editForm.amount,
remark_extra: editForm.remark_extra || '', remark_extra: editForm.remark_extra || '',
remark_assistant: editForm.remark_assistant || '',
// BUG FIX: // BUG FIX:
medication_days: (editForm.medication_days != null && String(editForm.medication_days).trim() !== '') ? editForm.medication_days : '', medication_days: (editForm.medication_days != null && String(editForm.medication_days).trim() !== '') ? editForm.medication_days : '',
@@ -4142,6 +4156,7 @@ async function submitQuickTrack() {
fee_type: Number(d.fee_type) || 3, fee_type: Number(d.fee_type) || 3,
amount: Number(d.amount) || 0, amount: Number(d.amount) || 0,
remark_extra: d.remark_extra || '', remark_extra: d.remark_extra || '',
remark_assistant: d.remark_assistant || '',
internal_cost: (d.internal_cost != null && d.internal_cost !== '') ? d.internal_cost : '', internal_cost: (d.internal_cost != null && d.internal_cost !== '') ? d.internal_cost : '',
pay_order_ids: Array.isArray(d.pay_order_ids) ? d.pay_order_ids : [], pay_order_ids: Array.isArray(d.pay_order_ids) ? d.pay_order_ids : [],
// //
File diff suppressed because it is too large Load Diff
+30 -3
View File
@@ -1623,6 +1623,7 @@ defineExpose({
.el-drawer__body { .el-drawer__body {
background: #f8fafc; background: #f8fafc;
min-width: 0;
} }
.el-drawer__footer { .el-drawer__footer {
@@ -1631,22 +1632,48 @@ defineExpose({
box-shadow: 0 -4px 24px rgb(15 23 42 / 7%); box-shadow: 0 -4px 24px rgb(15 23 42 / 7%);
} }
/* Tab 标签保证在窄抽屉内可横向滚动 flex 子项 min-width EP 内层 overflow */
.tcm-diagnosis-tabs.el-tabs {
width: 100%;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
}
.tcm-diagnosis-tabs .el-tabs__header {
min-width: 0;
width: 100%;
}
.tcm-diagnosis-tabs .el-tabs__nav-wrap { .tcm-diagnosis-tabs .el-tabs__nav-wrap {
flex: 1;
min-width: 0;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
scrollbar-width: none; scrollbar-width: thin;
scrollbar-color: rgb(148 163 184 / 55%) transparent;
} }
.tcm-diagnosis-tabs .el-tabs__nav-wrap::-webkit-scrollbar { .tcm-diagnosis-tabs .el-tabs__nav-wrap::-webkit-scrollbar {
display: none; height: 4px;
height: 0; }
.tcm-diagnosis-tabs .el-tabs__nav-wrap::-webkit-scrollbar-thumb {
background-color: rgb(148 163 184 / 55%);
border-radius: 4px;
}
.tcm-diagnosis-tabs .el-tabs__nav-scroll {
overflow: visible !important;
} }
.tcm-diagnosis-tabs .el-tabs__nav { .tcm-diagnosis-tabs .el-tabs__nav {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
float: none !important; float: none !important;
width: max-content;
min-width: 100%;
} }
.tcm-diagnosis-tabs .el-tabs__item { .tcm-diagnosis-tabs .el-tabs__item {
File diff suppressed because one or more lines are too long
@@ -15,6 +15,7 @@ use app\adminapi\logic\stats\YejiStatsLogic;
* - GET stats.yejiStats/deptOptions 部门下拉 * - GET stats.yejiStats/deptOptions 部门下拉
* - GET stats.yejiStats/leaderboard 医助排行榜(按展示部门分表) * - GET stats.yejiStats/leaderboard 医助排行榜(按展示部门分表)
* - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条) * - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条)
* - GET stats.yejiStats/revisitBreakdown 二中心复诊下钻:医助 × 业务订单笔数
*/ */
class YejiStatsController extends BaseAdminController class YejiStatsController extends BaseAdminController
{ {
@@ -121,4 +122,13 @@ class YejiStatsController extends BaseAdminController
return $this->data(YejiStatsLogic::leadLineList($params, $this->adminId, $this->adminInfo)); return $this->data(YejiStatsLogic::leadLineList($params, $this->adminId, $this->adminInfo));
} }
/** 二中心复诊:按部门行 + 复诊分项拆解医助与业务订单笔数 */
public function revisitBreakdown()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::revisitDeptAssistantBreakdown($params, $this->adminId, $this->adminInfo));
}
} }
@@ -105,6 +105,44 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) { if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
$query->where('fulfillment_status', '<>', 4); $query->where('fulfillment_status', '<>', 4);
} }
$this->applyYejiErCenterRevisitOrderFilter($query);
}
/**
* 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_timerevisit_slot=0 为全部复诊分项合计)。
*/
private function applyYejiErCenterRevisitOrderFilter($query): void
{
if ((int) ($this->params['yeji_er_center_revisit_only'] ?? 0) !== 1) {
return;
}
$assistantId = (int) ($this->params['assistant_id'] ?? 0);
$slot = (int) ($this->params['yeji_er_center_revisit_slot'] ?? 0);
if ($assistantId <= 0) {
return;
}
$st = trim((string) ($this->params['start_time'] ?? ''));
$et = trim((string) ($this->params['end_time'] ?? ''));
if ($st === '' || $et === '') {
return;
}
$t0 = strtotime($st);
$t1 = strtotime($et);
if ($t0 === false || $t1 === false) {
return;
}
$ids = DeptLogic::listErCenterRevisitOrderIdsForAssistant(
$assistantId,
$slot,
(int) $t0,
(int) $t1
);
if ($ids === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereIn('id', $ids);
} }
/** /**
@@ -397,6 +435,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
} }
foreach ($lists as &$item) { foreach ($lists as &$item) {
PrescriptionOrderLogic::maskInternalCostIfNeeded($item, $this->adminInfo); PrescriptionOrderLogic::maskInternalCostIfNeeded($item, $this->adminInfo);
PrescriptionOrderLogic::maskRemarkExtraIfNeeded($item, $this->adminInfo);
$pid = (int) ($item['id'] ?? 0); $pid = (int) ($item['id'] ?? 0);
$item['linked_pay_order_count'] = (int) PrescriptionOrderPayOrder::where( $item['linked_pay_order_count'] = (int) PrescriptionOrderPayOrder::where(
'prescription_order_id', 'prescription_order_id',
+555 -1
View File
@@ -19,6 +19,7 @@ use app\common\model\auth\Admin;
use app\common\model\auth\AdminDept; use app\common\model\auth\AdminDept;
use app\common\model\dept\Dept; use app\common\model\dept\Dept;
use app\common\service\DataScope\DataScopeService; use app\common\service\DataScope\DataScopeService;
use think\facade\Db;
/** /**
@@ -64,7 +65,9 @@ class DeptLogic extends BaseLogic
if (!empty($lists)) { if (!empty($lists)) {
$pid = min(array_column($lists, 'pid')); $pid = min(array_column($lists, 'pid'));
} }
return self::getTree($lists, $pid); $tree = self::getTree($lists, $pid);
return $tree;
} }
/** /**
@@ -141,6 +144,557 @@ class DeptLogic extends BaseLogic
return $map; return $map;
} }
/**
* 名称含「二中心」的部门 id(与业绩看板二中心规则一致)。
*
* @return list<int>
*/
private static function findErCenterRootDeptIds(): array
{
$rows = Dept::whereNull('delete_time')
->field(['id', 'name'])
->select()
->toArray();
$out = [];
foreach ($rows as $r) {
$name = (string) ($r['name'] ?? '');
if ($name !== '' && mb_strpos($name, '二中心') !== false) {
$out[] = (int) $r['id'];
}
}
return array_values(array_unique($out));
}
/**
* @param list<int> $rootIds
*
* @return list<int>
*/
private static function unionErCenterSubtreeDeptIds(array $rootIds): array
{
$set = [];
foreach ($rootIds as $rid) {
$rid = (int) $rid;
if ($rid <= 0) {
continue;
}
foreach (self::getSelfAndDescendantIds($rid) as $id) {
$set[(int) $id] = true;
}
}
return array_keys($set);
}
/**
* 名称含「二中心」的部门及其全部下级 id(map),与业绩看板判定一致。
*
* @return array<int, true>
*/
public static function getErCenterSubtreeDeptIdSet(): array
{
$erRoots = self::findErCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
return array_fill_keys($subtreeIds, true);
}
/**
* 二中心复诊统计用的业务订单行(与 rollup 同源 SQL)。
*
* @return list<array{diagnosis_id: int, create_time: int, id: int, assistant_id: int}>
*/
private static function fetchErCenterRevisitCandidateOrders(?int $orderStartTs = null, ?int $orderEndTs = null): array
{
$q = Db::name('tcm_prescription_order')
->alias('o')
->join('tcm_diagnosis dg', 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->whereNull('o.delete_time')
->where('o.diagnosis_id', '>', 0)
->where('dg.assistant_id', '>', 0)
->whereRaw('NOT (o.fulfillment_status <=> ?)', [4]);
if ($orderStartTs !== null && $orderEndTs !== null) {
$q->where('o.create_time', 'between', [$orderStartTs, $orderEndTs]);
}
return $q
->field(['o.diagnosis_id', 'o.create_time', 'o.id', 'dg.assistant_id'])
->order(['o.diagnosis_id' => 'asc', 'o.create_time' => 'asc', 'o.id' => 'asc'])
->select()
->toArray();
}
/**
* 二中心子树:各部门复诊 rollup;同时返回按诊单医助聚合的分项(供医助排行榜)。
*
* @param list<int> $subtreeDeptIds
*
* @return array{
* totals: array<int, int>,
* slots: array<int, array<int, int>>,
* by_assistant_slots: array<int, array<int, int>>
* }
*/
private static function computeErCenterRevisitFullPack(
array $subtreeDeptIds,
?int $orderStartTs = null,
?int $orderEndTs = null
): array {
$emptySlots = [];
$emptyTotals = [];
foreach ($subtreeDeptIds as $did) {
$did = (int) $did;
$emptySlots[$did] = [];
$emptyTotals[$did] = 0;
}
if ($subtreeDeptIds === []) {
return ['totals' => [], 'slots' => [], 'by_assistant_slots' => []];
}
$subtreeSet = array_fill_keys($subtreeDeptIds, true);
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
if ($orderRows === []) {
return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_assistant_slots' => []];
}
$byPatient = [];
foreach ($orderRows as $r) {
$pid = (int) ($r['diagnosis_id'] ?? 0);
if ($pid <= 0) {
continue;
}
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
}
}
}
$assistantIds = array_keys($assistantNeed);
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree($assistantIds, $subtreeSet);
/** @var array<int, array<int, int>> $leafByDeptSlot dept_id => [ slot => cnt ]slot≥2 */
$leafByDeptSlot = [];
/** @var array<int, array<int, int>> $byAssistantSlot */
$byAssistantSlot = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$slot = $i + 1;
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
$deptId = $canonicalDept[$aid] ?? null;
if ($deptId === null || !isset($subtreeSet[$deptId])) {
continue;
}
if (!isset($leafByDeptSlot[$deptId])) {
$leafByDeptSlot[$deptId] = [];
}
$leafByDeptSlot[$deptId][$slot] = ($leafByDeptSlot[$deptId][$slot] ?? 0) + 1;
if ($aid > 0) {
if (!isset($byAssistantSlot[$aid])) {
$byAssistantSlot[$aid] = [];
}
$byAssistantSlot[$aid][$slot] = ($byAssistantSlot[$aid][$slot] ?? 0) + 1;
}
}
}
$childrenByPid = self::buildAllDeptChildrenByPid();
$rollupSlots = self::rollupRevisitSlotsBySubtree($subtreeDeptIds, $subtreeSet, $childrenByPid, $leafByDeptSlot);
$rollupTotals = [];
foreach ($subtreeDeptIds as $id) {
$id = (int) $id;
$rollupTotals[$id] = array_sum($rollupSlots[$id] ?? []);
}
foreach ($byAssistantSlot as $aid => $sm) {
if ($sm !== []) {
ksort($byAssistantSlot[$aid], SORT_NUMERIC);
}
}
return [
'totals' => $rollupTotals,
'slots' => $rollupSlots,
'by_assistant_slots' => $byAssistantSlot,
];
}
/**
* 二中心子树:各部门复诊合计与分项(第 2 笔订单=复诊2…),供业绩看板等复用。
* $orderStartTs/$orderEndTs 均非 null 时仅统计该 create_time 窗口内业务单(与看板区间一致);均为 null 时不限时间(慎用,数据量大)。
*
* @return array{totals: array<int, int>, slots: array<int, array<int, int>>}
*/
public static function getErCenterRevisitRollupIndexedByDept(?int $orderStartTs = null, ?int $orderEndTs = null): array
{
$erRoots = self::findErCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
if ($subtreeIds === []) {
return ['totals' => [], 'slots' => []];
}
$pack = self::computeErCenterRevisitFullPack($subtreeIds, $orderStartTs, $orderEndTs);
return ['totals' => $pack['totals'], 'slots' => $pack['slots']];
}
/**
* 二中心子树内:各医助成交的复诊业务单笔数(合计 + 复诊2/3/…分项),口径与部门 rollup 同源。
*
* @return array{totals: array<int, int>, slots: array<int, array<int, int>>}
*/
public static function getErCenterRevisitCountsByAssistant(?int $orderStartTs = null, ?int $orderEndTs = null): array
{
$erRoots = self::findErCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
if ($subtreeIds === []) {
return ['totals' => [], 'slots' => []];
}
$pack = self::computeErCenterRevisitFullPack($subtreeIds, $orderStartTs, $orderEndTs);
$by = $pack['by_assistant_slots'];
$totals = [];
$slots = [];
foreach ($by as $aid => $slotMap) {
$aid = (int) $aid;
$totals[$aid] = (int) array_sum($slotMap);
$slots[$aid] = $slotMap;
}
return ['totals' => $totals, 'slots' => $slots];
}
/**
* 某医助在区间内、计入二中心复诊的业务订单 id(用于看板侧栏按复诊笔数下钻)。
* $revisitSlot = 0 表示累计全部复诊(第 2 笔起);≥2 时仅该分项。
*
* @return list<int>
*/
public static function listErCenterRevisitOrderIdsForAssistant(
int $assistantId,
int $revisitSlot,
int $orderStartTs,
int $orderEndTs
): array {
if ($assistantId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) {
return [];
}
if ($revisitSlot < 0 || $revisitSlot === 1) {
return [];
}
$erRoots = self::findErCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
if ($subtreeIds === []) {
return [];
}
$subtreeSet = array_fill_keys($subtreeIds, true);
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
if ($orderRows === []) {
return [];
}
$byPatient = [];
foreach ($orderRows as $r) {
$pid = (int) ($r['diagnosis_id'] ?? 0);
if ($pid <= 0) {
continue;
}
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
}
}
}
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $subtreeSet);
$ids = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$slot = $i + 1;
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid !== $assistantId) {
continue;
}
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
continue;
}
$dcan = $canonicalDept[$aid] ?? null;
if ($dcan === null || !isset($subtreeSet[$dcan])) {
continue;
}
$oid = (int) ($orders[$i]['id'] ?? 0);
if ($oid > 0) {
$ids[$oid] = true;
}
}
}
return array_keys($ids);
}
/**
* 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按诊单医助拆解笔数。
* 与看板行内「复诊」列口径一致:诊单下订单序列第 2 笔起;医助 canonical 部门须落在二中心子树且在该部门行的子树内。
*
* @return array{rows: list<array{admin_id: int, name: string, order_count: int}>}
*/
public static function getErCenterRevisitAssistantBreakdownForDept(
int $rootDeptId,
int $revisitSlot,
int $orderStartTs,
int $orderEndTs
): array {
if ($rootDeptId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) {
return ['rows' => []];
}
if ($revisitSlot < 0 || $revisitSlot === 1) {
return ['rows' => []];
}
$erSubtreeSet = self::getErCenterSubtreeDeptIdSet();
if ($erSubtreeSet === []) {
return ['rows' => []];
}
$descIds = self::getSelfAndDescendantIds($rootDeptId);
$descFlip = [];
foreach ($descIds as $did) {
$did = (int) $did;
if ($did > 0) {
$descFlip[$did] = true;
}
}
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
if ($orderRows === []) {
return ['rows' => []];
}
$byPatient = [];
foreach ($orderRows as $r) {
$pid = (int) ($r['diagnosis_id'] ?? 0);
if ($pid <= 0) {
continue;
}
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
}
}
}
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $erSubtreeSet);
/** @var array<int, int> $counts */
$counts = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$slot = $i + 1;
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
continue;
}
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid <= 0) {
continue;
}
$canon = $canonicalDept[$aid] ?? null;
if ($canon === null || !isset($erSubtreeSet[$canon]) || !isset($descFlip[$canon])) {
continue;
}
$counts[$aid] = ($counts[$aid] ?? 0) + 1;
}
}
if ($counts === []) {
return ['rows' => []];
}
$ids = array_keys($counts);
$nameRows = [];
if ($ids !== []) {
$nameRows = Db::name('admin')
->whereIn('id', $ids)
->whereNull('delete_time')
->column('name', 'id');
}
$rows = [];
foreach ($counts as $aid => $cnt) {
$rows[] = [
'admin_id' => (int) $aid,
'name' => (string) ($nameRows[$aid] ?? ('#' . $aid)),
'order_count' => (int) $cnt,
];
}
usort($rows, static function (array $a, array $b): int {
if ($a['order_count'] !== $b['order_count']) {
return $b['order_count'] <=> $a['order_count'];
}
return strcmp((string) $a['name'], (string) $b['name']);
});
return ['rows' => $rows];
}
/**
* @return array<int, list<int>>
*/
private static function buildAllDeptChildrenByPid(): array
{
$rows = Dept::field(['id', 'pid'])->select()->toArray();
$out = [];
foreach ($rows as $r) {
$pid = (int) ($r['pid'] ?? 0);
$id = (int) ($r['id'] ?? 0);
if ($id <= 0) {
continue;
}
if (!isset($out[$pid])) {
$out[$pid] = [];
}
$out[$pid][] = $id;
}
return $out;
}
/**
* @param array<int, array<int, int>> $leafByDeptSlot
*
* @return array<int, array<int, int>> dept_id => [ slot => count ],已含下级汇总
*/
private static function rollupRevisitSlotsBySubtree(
array $subtreeDeptIds,
array $subtreeSet,
array $childrenByPid,
array $leafByDeptSlot
): array {
$memo = [];
$dfs = static function (int $id) use (&$dfs, $childrenByPid, $leafByDeptSlot, $subtreeSet, &$memo): array {
if (isset($memo[$id])) {
return $memo[$id];
}
$acc = [];
if (isset($leafByDeptSlot[$id])) {
foreach ($leafByDeptSlot[$id] as $slot => $cnt) {
$acc[(int) $slot] = (int) $cnt;
}
}
foreach ($childrenByPid[$id] ?? [] as $cid) {
$cid = (int) $cid;
if (!isset($subtreeSet[$cid])) {
continue;
}
$childAcc = $dfs($cid);
foreach ($childAcc as $slot => $cnt) {
$acc[$slot] = ($acc[$slot] ?? 0) + $cnt;
}
}
if ($acc !== []) {
ksort($acc, SORT_NUMERIC);
}
$memo[$id] = $acc;
return $acc;
};
$out = [];
foreach ($subtreeDeptIds as $id) {
$id = (int) $id;
$out[$id] = $dfs($id);
}
return $out;
}
/**
* @param list<int> $assistantIds
* @param array<int, true> $subtreeSet
*
* @return array<int, int> admin_id => dept_id
*/
private static function buildAssistantCanonicalDeptInSubtree(array $assistantIds, array $subtreeSet): array
{
$ids = array_values(array_unique(array_filter(array_map('intval', $assistantIds), static function (int $v): bool {
return $v > 0;
})));
if ($ids === []) {
return [];
}
$subtreeIdList = array_keys($subtreeSet);
$rows = AdminDept::whereIn('admin_id', $ids)
->whereIn('dept_id', $subtreeIdList)
->field(['admin_id', 'dept_id'])
->select()
->toArray();
$candidates = [];
foreach ($rows as $r) {
$a = (int) ($r['admin_id'] ?? 0);
$d = (int) ($r['dept_id'] ?? 0);
if ($a > 0 && $d > 0) {
$candidates[$a][] = $d;
}
}
$depthMap = self::buildDeptIdDepthMap();
$out = [];
foreach ($candidates as $aid => $depts) {
$best = null;
$bestDepth = -1;
foreach ($depts as $d) {
$depth = (int) ($depthMap[$d] ?? 0);
if (
$best === null
|| $depth > $bestDepth
|| ($depth === $bestDepth && $d < (int) $best)
) {
$bestDepth = $depth;
$best = $d;
}
}
if ($best !== null) {
$out[$aid] = (int) $best;
}
}
return $out;
}
/**
* @return array<int, int> dept_id => 从根层深度 0 起的层级
*/
private static function buildDeptIdDepthMap(): array
{
$rows = Dept::field(['id', 'pid'])->select()->toArray();
$pidById = [];
foreach ($rows as $r) {
$pidById[(int) $r['id']] = (int) $r['pid'];
}
$depth = [];
$getDepth = static function (int $id) use (&$getDepth, &$depth, $pidById): int {
if (isset($depth[$id])) {
return $depth[$id];
}
$p = (int) ($pidById[$id] ?? 0);
$depth[$id] = $p > 0 ? $getDepth($p) + 1 : 0;
return $depth[$id];
};
foreach (array_keys($pidById) as $id) {
$getDepth((int) $id);
}
return $depth;
}
/** /**
* @notes 列表树状结构 * @notes 列表树状结构
* @param $array * @param $array
@@ -14,6 +14,9 @@ use think\facade\Db;
* *
* - 进线数据 = 区间内 add_external_contact 事件,按接待员工(user_id) 归属部门 * - 进线数据 = 区间内 add_external_contact 事件,按接待员工(user_id) 归属部门
* · 与 qywx.customer/todayArrival、客户列表同源事件表;选定渠道时客户须带对应企微标签(contact_tag);**二中心展示部门在选定渠道下计 0** * · 与 qywx.customer/todayArrival、客户列表同源事件表;选定渠道时客户须带对应企微标签(contact_tag);**二中心展示部门在选定渠道下计 0**
* - 被指派数 = 区间内 tcm_diagnosis_assign_log 成功指派次数(to_assistant_id>0),按**被指派医助**归属展示部门;选定渠道时收窄与「{渠道}业绩」同源(channels EXISTS 或 tag 诊单/医助)
* · **二中心展示部门在选定渠道下计 0**(与进线一致)
* - 复诊统计 = 仅**名称含「二中心」的部门及其组织架构下级**:区间内业务单按患者 create_time 排序,第 1 笔不计,第 2 笔起分列「复诊2」「复诊3」…;口径同业绩单条件;按诊单医助在组织树内归属 rollup(含下级),与 dept 模块 ErCenter 复诊算法一致
* - 接诊诊单 = 已完成挂号 status=3,**与 admin 挂号列表条数同口径**appointment_date、诊单未软删),不限医助角色、不要求 assistant_id 非 0 * - 接诊诊单 = 已完成挂号 status=3,**与 admin 挂号列表条数同口径**appointment_date、诊单未软删),不限医助角色、不要求 assistant_id 非 0
* 部门归属:优先 COALESCE(挂号.assistant_id, 诊单.assistant_id) 映射到展示「中心」,否则回退接诊医生 doctor_id;仍无法归属则计入「未归属中心」行。 * 部门归属:优先 COALESCE(挂号.assistant_id, 诊单.assistant_id) 映射到展示「中心」,否则回退接诊医生 doctor_id;仍无法归属则计入「未归属中心」行。
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag/creator 两套口径均按订单计,非挂号条数。 * - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag/creator 两套口径均按订单计,非挂号条数。
@@ -269,6 +272,16 @@ class YejiStatsLogic
: null; : null;
$leads = self::countLeads($startTs, $endTs, $adminToPrimary, $tagFilterId); $leads = self::countLeads($startTs, $endTs, $adminToPrimary, $tagFilterId);
$assignsByDept = self::countDiagnosisAssignsByDept(
$startTs,
$endTs,
$adminToPrimary,
$tagDiagIds,
$tagFallback ? $tagAssistantIds : null,
$appointmentChannelValues,
$channelFilterActive,
$dataScopeAdminIds
);
// 接诊: // 接诊:
// - consults(全量) → "接诊诊单" 列;不限渠道 // - consults(全量) → "接诊诊单" 列;不限渠道
// - channelConsults(渠道) → 「{渠道}成交单」列;未选渠道时与 consults 相同 // - channelConsults(渠道) → 「{渠道}成交单」列;未选渠道时与 consults 相同
@@ -312,6 +325,7 @@ class YejiStatsLogic
$channelConsults $channelConsults
); );
self::excludeErCenterFromLeads($primaryDeptIds, $deptNameMap, $leads); self::excludeErCenterFromLeads($primaryDeptIds, $deptNameMap, $leads);
self::excludeErCenterFromLeads($primaryDeptIds, $deptNameMap, $assignsByDept);
} }
$globalCost = round((float) self::sumGlobalCost($startDate, $endDate, $channelCode), 1); $globalCost = round((float) self::sumGlobalCost($startDate, $endDate, $channelCode), 1);
@@ -323,9 +337,18 @@ class YejiStatsLogic
); );
$costs = self::allocateCostByLeads($globalCost, $tableRowDeptIds, $leadsForCost); $costs = self::allocateCostByLeads($globalCost, $tableRowDeptIds, $leadsForCost);
@set_time_limit(120);
$revisitPack = DeptLogic::getErCenterRevisitRollupIndexedByDept($startTs, $endTs);
$revisitTotals = $revisitPack['totals'];
$revisitSlotsByDept = $revisitPack['slots'];
// ── 5) 组装每行 ────────────────────────────────────────────── // ── 5) 组装每行 ──────────────────────────────────────────────
$rows = []; $rows = [];
$totalLead = 0; $totalLead = 0;
$totalAssign = 0;
$totalRevisit = 0;
/** @var array<int, int> $totalRevisitSlots */
$totalRevisitSlots = [];
$totalChannelConsult = 0; $totalChannelConsult = 0;
$totalPerf = 0.0; $totalPerf = 0.0;
$totalChannelPerf = 0.0; $totalChannelPerf = 0.0;
@@ -348,10 +371,16 @@ class YejiStatsLogic
$rowRoi = $rowCost > 0 ? round($cperf / $rowCost, 1) : null; $rowRoi = $rowCost > 0 ? round($cperf / $rowCost, 1) : null;
$dDeal = (int) ($dealOrderByDept[$deptId] ?? 0); $dDeal = (int) ($dealOrderByDept[$deptId] ?? 0);
$assignCnt = (int) ($assignsByDept[$deptId] ?? 0);
$rv = (int) ($revisitTotals[$deptId] ?? 0);
$rvSlots = $revisitSlotsByDept[$deptId] ?? [];
$rows[] = [ $rows[] = [
'dept_id' => $deptId, 'dept_id' => $deptId,
'dept_name' => self::formatYejiDeptRowDisplayName($deptId, $deptById), 'dept_name' => self::formatYejiDeptRowDisplayName($deptId, $deptById),
'lead_count' => $lead, 'lead_count' => $lead,
'assign_count' => $assignCnt,
'revisit_count' => $rv,
'revisit_slots' => $rvSlots,
'consult_count' => $cnt, 'consult_count' => $cnt,
'deal_order_count' => $dDeal, 'deal_order_count' => $dDeal,
// {渠道}成交单:选定渠道时与 completed_performance 同源;未选渠道时与 consult_count 相同 // {渠道}成交单:选定渠道时与 completed_performance 同源;未选渠道时与 consult_count 相同
@@ -366,6 +395,12 @@ class YejiStatsLogic
]; ];
$totalLead += $lead; $totalLead += $lead;
$totalAssign += $assignCnt;
$totalRevisit += $rv;
foreach ($rvSlots as $slot => $cntRv) {
$slot = (int) $slot;
$totalRevisitSlots[$slot] = ($totalRevisitSlots[$slot] ?? 0) + (int) $cntRv;
}
$totalChannelConsult += $cCnt; $totalChannelConsult += $cCnt;
$totalPerf += $perf; $totalPerf += $perf;
$totalChannelPerf += $cperf; $totalChannelPerf += $cperf;
@@ -411,6 +446,9 @@ class YejiStatsLogic
'dept_id' => 0, 'dept_id' => 0,
'dept_name' => '未归属中心', 'dept_name' => '未归属中心',
'lead_count' => 0, 'lead_count' => 0,
'assign_count' => 0,
'revisit_count' => 0,
'revisit_slots' => [],
'consult_count' => $unmappedConsult, 'consult_count' => $unmappedConsult,
'deal_order_count' => $unmappedDealOrder, 'deal_order_count' => $unmappedDealOrder,
'channel_consult_count' => 0, 'channel_consult_count' => 0,
@@ -437,6 +475,10 @@ class YejiStatsLogic
// ROI 合计 = 渠道业绩合计(与「completed」列一致,**不含二中心**)÷ 投放成本合计 // ROI 合计 = 渠道业绩合计(与「completed」列一致,**不含二中心**)÷ 投放成本合计
$totalRoiNumerator = round($totalChannelPerf, 1); $totalRoiNumerator = round($totalChannelPerf, 1);
if ($totalRevisitSlots !== []) {
ksort($totalRevisitSlots, SORT_NUMERIC);
}
$scopedTableNote = ($explicitDeptFilter $scopedTableNote = ($explicitDeptFilter
? ' 已筛选展示部门时,表格合计仅含所选部门树内数据,不出现「全站未归属」补差行(避免把其他中心业绩/单量算入未归属)。若勾选的是带下级的部门(如某「中心」),表格只列出其下级部门行,不单独占一行父级。' ? ' 已筛选展示部门时,表格合计仅含所选部门树内数据,不出现「全站未归属」补差行(避免把其他中心业绩/单量算入未归属)。若勾选的是带下级的部门(如某「中心」),表格只列出其下级部门行,不单独占一行父级。'
: '') : '')
@@ -464,6 +506,9 @@ class YejiStatsLogic
'rows' => $rows, 'rows' => $rows,
'total' => [ 'total' => [
'lead_count' => $totalLead, 'lead_count' => $totalLead,
'assign_count' => $totalAssign,
'revisit_count' => $totalRevisit,
'revisit_slots' => $totalRevisitSlots,
'consult_count' => $totalConsult, 'consult_count' => $totalConsult,
'deal_order_count' => $totalDealOrderCount, 'deal_order_count' => $totalDealOrderCount,
'channel_consult_count' => $totalChannelConsult, 'channel_consult_count' => $totalChannelConsult,
@@ -503,7 +548,9 @@ class YejiStatsLogic
/** /**
* 医助排行榜:按展示部门分表,每表内按诊金降序排名。 * 医助排行榜:按展示部门分表,每表内按诊金降序排名。
* 诊金口径:无渠道=诊单 dg.assistant_id 业绩(与列表 assistant_id 筛选一致);有渠道={渠道}业绩/dict 诊单医助;二中心在选渠道下注 0。 * 诊金口径:无渠道=诊单 dg.assistant_id 业绩(与列表 assistant_id 筛选一致);有渠道={渠道}业绩/dict 诊单医助;二中心在选渠道下注 0。
* 被指派数 = 区间内诊单指派日志(to_assistant_id>0)按**被指派医助**计数,与部门表「被指派数」同收窄/二中心规则。
* 接诊率 = 排行用诊金 ÷ 进线条数(元/进线,1 位小数;进线为 0 时为 0)。 * 接诊率 = 排行用诊金 ÷ 进线条数(元/进线,1 位小数;进线为 0 时为 0)。
* 复诊(属二中心组织架构的展示部门):同一诊单下业务单按 create_time 排序第 2 笔起分项,与部门业绩表「二中心复诊」同源;前端点击数字可下钻对应业务订单。
* *
* @param array{start_date?:string,end_date?:string,dept_ids?:int[]|string,channel_code?:string,tag_id?:string} $params * @param array{start_date?:string,end_date?:string,dept_ids?:int[]|string,channel_code?:string,tag_id?:string} $params
* *
@@ -532,6 +579,10 @@ class YejiStatsLogic
$tagFallback = $c['tagFallback']; $tagFallback = $c['tagFallback'];
$appointmentChannelValues = $c['appointmentChannelValues']; $appointmentChannelValues = $c['appointmentChannelValues'];
$channelFilterActive = $c['channelFilterActive']; $channelFilterActive = $c['channelFilterActive'];
$dataScopeRestricted = !empty($c['dataScopeRestricted']);
$dataScopeAdminIds = $dataScopeRestricted && isset($c['dataScopeVisibleAdminIds'])
? $c['dataScopeVisibleAdminIds']
: null;
$channelName = $channelInfo ? (string) $channelInfo['channel_name'] : ''; $channelName = $channelInfo ? (string) $channelInfo['channel_name'] : '';
@@ -569,11 +620,30 @@ class YejiStatsLogic
$leadsByAdmin = self::countLeadsByAdmin($startTs, $endTs, $tagFilterId); $leadsByAdmin = self::countLeadsByAdmin($startTs, $endTs, $tagFilterId);
/** 与部门表接诊同口径;标签/医助穿透时与业绩同源收窄,避免榜上证务与接诊脱节 */ /** 与部门表接诊同口径;标签/医助穿透时与业绩同源收窄,避免榜上证务与接诊脱节 */
$consultsByAdmin = self::aggregateConsultsByAdmin($startTs, $endTs, $tagDiagIds, $scopedAssistants); $consultsByAdmin = self::aggregateConsultsByAdmin($startTs, $endTs, $tagDiagIds, $scopedAssistants);
$assignsByAdmin = self::countDiagnosisAssignsByAdmin(
$startTs,
$endTs,
$tagDiagIds,
$tagFallback ? $tagAssistantIds : null,
$appointmentChannelValues,
$channelFilterActive,
$dataScopeAdminIds
);
if ($channelFilterActive) { if ($channelFilterActive) {
self::excludeErCenterFromAdminLeaderboard($adminToPrimary, $deptNameMap, $feeByAdmin, $leadsByAdmin, $dealOrderByAdmin); self::excludeErCenterFromAdminLeaderboard(
$adminToPrimary,
$deptNameMap,
$feeByAdmin,
$leadsByAdmin,
$dealOrderByAdmin,
$assignsByAdmin
);
} }
$erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
$revisitByAssistant = DeptLogic::getErCenterRevisitCountsByAssistant($startTs, $endTs);
$assistantIdRows = Db::name('admin_role')->where('role_id', 2)->column('admin_id'); $assistantIdRows = Db::name('admin_role')->where('role_id', 2)->column('admin_id');
$assistantSet = []; $assistantSet = [];
foreach ($assistantIdRows as $rid) { foreach ($assistantIdRows as $rid) {
@@ -598,15 +668,16 @@ class YejiStatsLogic
} }
$rangeNote = $channelFilterActive $rangeNote = $channelFilterActive
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩口径(与部门表该渠道业绩列一致)。接诊率 = 诊金 ÷ 进线(元/进线)。' ? ('排行诊金 = 选定渠道「' . $channelName . '」业绩口径(与部门表该渠道业绩列一致)。被指派数与部门表同口径。接诊率 = 诊金 ÷ 进线(元/进线)。'
. '「二中心」医助在该渠道下诊金进线计 0。') . '「二中心」医助在该渠道下诊金进线与被指派计 0。')
: '排行诊金 = 处方订单列表 assistant_id 口径(诊单医助业绩,与列表按医助筛选一致)。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。'; : '排行诊金 = 处方订单列表 assistant_id 口径(诊单医助业绩,与列表按医助筛选一致)。被指派数为指派至该医助的次数。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。';
$leaderboards = []; $leaderboards = [];
foreach ($tableRowDeptIds as $deptId) { foreach ($tableRowDeptIds as $deptId) {
$leafName = $deptNameMap[$deptId] ?? ('部门#' . $deptId); $leafName = $deptNameMap[$deptId] ?? ('部门#' . $deptId);
$deptName = self::formatYejiDeptRowDisplayName($deptId, $deptById); $deptName = self::formatYejiDeptRowDisplayName($deptId, $deptById);
$deptAbbr = self::deptDisplayAbbr($leafName); $deptAbbr = self::deptDisplayAbbr($leafName);
$isErCenterLb = isset($erCenterDeptSet[$deptId]);
$rows = []; $rows = [];
foreach (array_keys($assistantSet) as $aid) { foreach (array_keys($assistantSet) as $aid) {
if (($adminToPrimary[$aid] ?? 0) !== $deptId) { if (($adminToPrimary[$aid] ?? 0) !== $deptId) {
@@ -616,21 +687,37 @@ class YejiStatsLogic
$lead = (int) ($leadsByAdmin[$aid] ?? 0); $lead = (int) ($leadsByAdmin[$aid] ?? 0);
$consult = (int) ($consultsByAdmin[$aid] ?? 0); $consult = (int) ($consultsByAdmin[$aid] ?? 0);
$dealOrders = (int) ($dealOrderByAdmin[$aid] ?? 0); $dealOrders = (int) ($dealOrderByAdmin[$aid] ?? 0);
if ($fee < 0.005 && $lead === 0 && $consult === 0 && $dealOrders === 0) { $assignCnt = (int) ($assignsByAdmin[$aid] ?? 0);
$rv = $isErCenterLb ? (int) ($revisitByAssistant['totals'][$aid] ?? 0) : 0;
$rvSlots = $isErCenterLb ? ($revisitByAssistant['slots'][$aid] ?? []) : [];
if (
$fee < 0.005
&& $lead === 0
&& $consult === 0
&& $dealOrders === 0
&& $assignCnt === 0
&& $rv === 0
) {
continue; continue;
} }
$rawFee = (float) ($feeByAdmin[$aid] ?? 0.0); $rawFee = (float) ($feeByAdmin[$aid] ?? 0.0);
$rate = $lead > 0 ? round($rawFee / $lead, 1) : 0.0; $rate = $lead > 0 ? round($rawFee / $lead, 1) : 0.0;
$rows[] = [ $row = [
'admin_id' => $aid, 'admin_id' => $aid,
'name' => $adminNames[$aid] ?? ('#' . $aid), 'name' => $adminNames[$aid] ?? ('#' . $aid),
'fee_amount' => $fee, 'fee_amount' => $fee,
'lead_count' => $lead, 'lead_count' => $lead,
'assign_count' => $assignCnt,
'consult_count' => $consult, 'consult_count' => $consult,
'deal_order_count' => $dealOrders, 'deal_order_count' => $dealOrders,
'consult_rate' => $rate, 'consult_rate' => $rate,
'dept_abbr' => $deptAbbr, 'dept_abbr' => $deptAbbr,
]; ];
if ($isErCenterLb) {
$row['revisit_count'] = $rv;
$row['revisit_slots'] = $rvSlots;
}
$rows[] = $row;
} }
usort($rows, static function (array $a, array $b): int { usort($rows, static function (array $a, array $b): int {
if ($a['fee_amount'] != $b['fee_amount']) { if ($a['fee_amount'] != $b['fee_amount']) {
@@ -648,6 +735,7 @@ class YejiStatsLogic
'dept_id' => $deptId, 'dept_id' => $deptId,
'dept_name' => $deptName, 'dept_name' => $deptName,
'dept_abbr' => $deptAbbr, 'dept_abbr' => $deptAbbr,
'er_center_subtree' => $isErCenterLb,
'rows' => $rows, 'rows' => $rows,
]; ];
} }
@@ -658,7 +746,7 @@ class YejiStatsLogic
'channel_code' => $channelCode, 'channel_code' => $channelCode,
'channel_name' => $channelName, 'channel_name' => $channelName,
'channel_filter_note' => $channelCode !== '' 'channel_filter_note' => $channelCode !== ''
? '医助榜:名称含「二中心」的部门在选定渠道下诊金、进线计 0,与部门看板一致。' ? '医助榜:名称含「二中心」的部门在选定渠道下诊金、进线、被指派数计 0,与部门看板一致。'
: '', : '',
'range_note' => $rangeNote, 'range_note' => $rangeNote,
'leaderboards' => $leaderboards, 'leaderboards' => $leaderboards,
@@ -1174,6 +1262,181 @@ class YejiStatsLogic
return $byDept; return $byDept;
} }
/**
* 被指派数:诊单指派日志(to_assistant_id>0)按被指派医助归属展示部门;选定渠道时收窄与「{渠道}业绩」同源。
*
* @param array<int, int> $adminToPrimary
* @param int[]|null $tagDiagIds
* @param array<int, int>|null $tagAssistantIds tag 退化口径下按 to_assistant_id 过滤(与 aggregatePerformance 一致)
*
* @return array<int, int>
*/
private static function countDiagnosisAssignsByDept(
int $startTs,
int $endTs,
array $adminToPrimary,
?array $tagDiagIds,
?array $tagAssistantIds,
array $appointmentChannelValues,
bool $channelFilterActive,
?array $dataScopeAdminIds = null
): array {
if ($adminToPrimary === []) {
return [];
}
$diagTable = self::tableWithPrefix('tcm_diagnosis');
$query = Db::name('tcm_diagnosis_assign_log')
->alias('lg')
->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->where('lg.create_time', 'between', [$startTs, $endTs])
->where('lg.to_assistant_id', '>', 0);
if ($channelFilterActive) {
if ($appointmentChannelValues !== []) {
$norm = self::normalizeAppointmentChannelValues($appointmentChannelValues);
if ($norm === []) {
return [];
}
$apTable = self::tableWithPrefix('doctor_appointment');
$adminRoleTable = self::tableWithPrefix('admin_role');
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $norm)));
$ph = implode(',', array_fill(0, count($strVals), '?'));
$channelCond = "ap.channels IN ({$ph})";
$existsSql = "EXISTS (SELECT 1 FROM {$apTable} ap INNER JOIN {$adminRoleTable} ar "
. "ON ar.admin_id = ap.assistant_id AND ar.role_id = 2 WHERE ap.patient_id = dg.id "
. "AND ap.status = 3 AND {$channelCond})";
$query->whereRaw($existsSql, $strVals);
} elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
if ($tagDiagIds !== null && $tagDiagIds === []) {
return [];
}
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
return [];
}
if ($tagDiagIds !== null) {
$query->whereIn('lg.diagnosis_id', $tagDiagIds);
}
if ($tagAssistantIds !== null) {
$query->whereIn('lg.to_assistant_id', array_map('intval', array_keys($tagAssistantIds)));
}
} else {
return [];
}
}
$query->field(['lg.to_assistant_id', Db::raw('COUNT(*) AS cnt')])
->group('lg.to_assistant_id');
$rows = $query->select()->toArray();
if ($rows === []) {
return [];
}
$visFlip = $dataScopeAdminIds !== null && $dataScopeAdminIds !== []
? array_flip($dataScopeAdminIds)
: null;
$byDept = [];
foreach ($rows as $r) {
$aid = (int) ($r['to_assistant_id'] ?? 0);
if ($aid <= 0) {
continue;
}
if ($visFlip !== null && !isset($visFlip[$aid])) {
continue;
}
$primary = (int) ($adminToPrimary[$aid] ?? 0);
if ($primary <= 0) {
continue;
}
$byDept[$primary] = ($byDept[$primary] ?? 0) + (int) ($r['cnt'] ?? 0);
}
return $byDept;
}
/**
* 医助维度被指派次数:与 countDiagnosisAssignsByDept 同过滤与区间,按 to_assistant_id 汇总(供排行榜)。
*
* @return array<int, int>
*/
private static function countDiagnosisAssignsByAdmin(
int $startTs,
int $endTs,
?array $tagDiagIds,
?array $tagAssistantIds,
array $appointmentChannelValues,
bool $channelFilterActive,
?array $dataScopeAdminIds = null
): array {
$diagTable = self::tableWithPrefix('tcm_diagnosis');
$query = Db::name('tcm_diagnosis_assign_log')
->alias('lg')
->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->where('lg.create_time', 'between', [$startTs, $endTs])
->where('lg.to_assistant_id', '>', 0);
if ($channelFilterActive) {
if ($appointmentChannelValues !== []) {
$norm = self::normalizeAppointmentChannelValues($appointmentChannelValues);
if ($norm === []) {
return [];
}
$apTable = self::tableWithPrefix('doctor_appointment');
$adminRoleTable = self::tableWithPrefix('admin_role');
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $norm)));
$ph = implode(',', array_fill(0, count($strVals), '?'));
$channelCond = "ap.channels IN ({$ph})";
$existsSql = "EXISTS (SELECT 1 FROM {$apTable} ap INNER JOIN {$adminRoleTable} ar "
. "ON ar.admin_id = ap.assistant_id AND ar.role_id = 2 WHERE ap.patient_id = dg.id "
. "AND ap.status = 3 AND {$channelCond})";
$query->whereRaw($existsSql, $strVals);
} elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
if ($tagDiagIds !== null && $tagDiagIds === []) {
return [];
}
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
return [];
}
if ($tagDiagIds !== null) {
$query->whereIn('lg.diagnosis_id', $tagDiagIds);
}
if ($tagAssistantIds !== null) {
$query->whereIn('lg.to_assistant_id', array_map('intval', array_keys($tagAssistantIds)));
}
} else {
return [];
}
}
$query->field(['lg.to_assistant_id', Db::raw('COUNT(*) AS cnt')])
->group('lg.to_assistant_id');
$rows = $query->select()->toArray();
if ($rows === []) {
return [];
}
$visFlip = $dataScopeAdminIds !== null && $dataScopeAdminIds !== []
? array_flip($dataScopeAdminIds)
: null;
$byAdmin = [];
foreach ($rows as $r) {
$aid = (int) ($r['to_assistant_id'] ?? 0);
if ($aid <= 0) {
continue;
}
if ($visFlip !== null && !isset($visFlip[$aid])) {
continue;
}
$byAdmin[$aid] = ($byAdmin[$aid] ?? 0) + (int) ($r['cnt'] ?? 0);
}
return $byAdmin;
}
/** /**
* 接诊诊单聚合:返回两组按部门归属的接诊计数。 * 接诊诊单聚合:返回两组按部门归属的接诊计数。
* - all: 区间内 status=3 挂号条数按部门归属(与列表计数一致),不含「仅 role_id=2」等额外限制。 * - all: 区间内 status=3 挂号条数按部门归属(与列表计数一致),不含「仅 role_id=2」等额外限制。
@@ -1642,6 +1905,100 @@ class YejiStatsLogic
]; ];
} }
/**
* 业绩看板:二中心复诊按部门行下钻 —— 医助姓名与业务订单笔数(与表格「复诊」列同口径)。
*
* @param array{
* start_date?:string,end_date?:string,dept_ids?:int[]|string,channel_code?:string,tag_id?:string,
* dept_id?:int|string,revisit_slot?:int|string
* } $params
* revisit_slot0=复诊合计(全部复诊笔),≥2=对应分项
*
* @return array{
* start_date:string,end_date:string,dept_id:int,dept_name:string,revisit_slot:int,revisit_slot_label:string,
* rows:list<array{admin_id:int,name:string,order_count:int}>,note:string
* }
*/
public static function revisitDeptAssistantBreakdown(
array $params,
int $viewerAdminId = 0,
array $viewerAdminInfo = []
): array {
$c = self::resolveYejiContext($params);
if ($viewerAdminId > 0) {
self::applyYejiDataScope($c, $viewerAdminId, $viewerAdminInfo);
}
$deptId = (int) ($params['dept_id'] ?? 0);
$revisitSlot = (int) ($params['revisit_slot'] ?? 0);
$deptName = $deptId > 0 ? self::formatYejiDeptRowDisplayName($deptId, $c['deptById']) : '';
$slotLabel = '复诊合计';
if ($revisitSlot >= 2) {
if ($revisitSlot === 2) {
$slotLabel = '复诊2';
} elseif ($revisitSlot >= 3 && $revisitSlot <= 10) {
$cn = ['三', '四', '五', '六', '七', '八', '九', '十'];
$slotLabel = '复诊' . ($cn[$revisitSlot - 3] ?? (string) $revisitSlot);
} else {
$slotLabel = '复诊' . (string) $revisitSlot;
}
}
$out = [
'start_date' => (string) $c['startDate'],
'end_date' => (string) $c['endDate'],
'dept_id' => $deptId,
'dept_name' => $deptName,
'revisit_slot' => $revisitSlot,
'revisit_slot_label' => $slotLabel,
'rows' => [],
'note' => '',
];
if ($deptId <= 0) {
$out['note'] = '请选择有效部门行。';
return $out;
}
if (!in_array($deptId, $c['tableRowDeptIds'], true)) {
$out['note'] = '该部门不在当前展示部门或数据权限范围内。';
return $out;
}
if ($revisitSlot < 0 || $revisitSlot === 1) {
$out['note'] = '无效的复诊分项。';
return $out;
}
$pack = DeptLogic::getErCenterRevisitAssistantBreakdownForDept(
$deptId,
$revisitSlot,
(int) $c['startTs'],
(int) $c['endTs']
);
$rows = $pack['rows'];
if (!empty($c['dataScopeRestricted']) && isset($c['dataScopeVisibleAdminIds'])) {
$flip = array_flip($c['dataScopeVisibleAdminIds']);
$filtered = [];
foreach ($rows as $rw) {
if (isset($flip[(int) ($rw['admin_id'] ?? 0)])) {
$filtered[] = $rw;
}
}
$rows = $filtered;
}
$out['rows'] = $rows;
if ($rows === []) {
$out['note'] = '当前部门与区间下无符合条件的复诊业务订单(口径与看板「二中心复诊」列一致)。';
}
return $out;
}
/** /**
* 与 sumPerformance 对应,按 admin 维度返回全量/渠道业绩金额(不按部门折叠)。 * 与 sumPerformance 对应,按 admin 维度返回全量/渠道业绩金额(不按部门折叠)。
* *
@@ -1931,13 +2288,15 @@ class YejiStatsLogic
* *
* @param array<int, float> $feeByAdmin * @param array<int, float> $feeByAdmin
* @param array<int, int> $leadsByAdmin * @param array<int, int> $leadsByAdmin
* @param array<int, int>|null $assignsByAdmin
*/ */
private static function excludeErCenterFromAdminLeaderboard( private static function excludeErCenterFromAdminLeaderboard(
array $adminToPrimary, array $adminToPrimary,
array $deptNameMap, array $deptNameMap,
array &$feeByAdmin, array &$feeByAdmin,
array &$leadsByAdmin, array &$leadsByAdmin,
?array &$dealOrderByAdmin = null ?array &$dealOrderByAdmin = null,
?array &$assignsByAdmin = null
): void { ): void {
foreach ($adminToPrimary as $aid => $pid) { foreach ($adminToPrimary as $aid => $pid) {
if (!self::isErCenterDisplayPrimaryName($deptNameMap[$pid] ?? '')) { if (!self::isErCenterDisplayPrimaryName($deptNameMap[$pid] ?? '')) {
@@ -1948,6 +2307,9 @@ class YejiStatsLogic
if ($dealOrderByAdmin !== null) { if ($dealOrderByAdmin !== null) {
$dealOrderByAdmin[$aid] = 0; $dealOrderByAdmin[$aid] = 0;
} }
if ($assignsByAdmin !== null) {
$assignsByAdmin[$aid] = 0;
}
} }
} }
@@ -240,6 +240,31 @@ class PrescriptionOrderLogic
} }
} }
/**
* 菜单权限:查看/编辑业务订单「备注」(对内 remark_extra
*
* perms: tcm.prescriptionOrder/editRemarkExtra
*/
public static function canEditOrderRemarkExtra(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
return in_array('tcm.prescriptionOrder/editRemarkExtra', $perms, true);
}
/**
* @param array<string,mixed> $row
*/
public static function maskRemarkExtraIfNeeded(array &$row, array $adminInfo): void
{
if (!self::canEditOrderRemarkExtra($adminInfo)) {
unset($row['remark_extra']);
}
}
/** 定金门槛(元),0 表示不校验 */ /** 定金门槛(元),0 表示不校验 */
public static function depositMinAmount(): float public static function depositMinAmount(): float
{ {
@@ -613,7 +638,10 @@ class PrescriptionOrderLogic
$order->fee_type = (int) $params['fee_type']; $order->fee_type = (int) $params['fee_type'];
$order->amount = round((float) $params['amount'], 2); $order->amount = round((float) $params['amount'], 2);
$order->internal_cost = $internalCost; $order->internal_cost = $internalCost;
$order->remark_extra = (string) ($params['remark_extra'] ?? ''); $order->remark_extra = self::canEditOrderRemarkExtra($adminInfo)
? (string) ($params['remark_extra'] ?? '')
: '';
$order->remark_assistant = (string) ($params['remark_assistant'] ?? '');
$order->linked_pay_order_id = $payOrderIds !== [] ? $payOrderIds[0] : null; $order->linked_pay_order_id = $payOrderIds !== [] ? $payOrderIds[0] : null;
$order->prescription_audit_status = 0; $order->prescription_audit_status = 0;
$order->prescription_audit_remark = ''; $order->prescription_audit_remark = '';
@@ -637,6 +665,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -658,6 +687,7 @@ class PrescriptionOrderLogic
} }
$arr = $row->toArray(); $arr = $row->toArray();
self::maskInternalCostIfNeeded($arr, $adminInfo); self::maskInternalCostIfNeeded($arr, $adminInfo);
self::maskRemarkExtraIfNeeded($arr, $adminInfo);
self::attachLinkedPayOrders($arr); self::attachLinkedPayOrders($arr);
// 添加创建人姓名 // 添加创建人姓名
@@ -1050,7 +1080,10 @@ class PrescriptionOrderLogic
} }
$order->fee_type = (int) $params['fee_type']; $order->fee_type = (int) $params['fee_type'];
$order->amount = round((float) $params['amount'], 2); $order->amount = round((float) $params['amount'], 2);
$order->remark_extra = (string) ($params['remark_extra'] ?? ''); if (self::canEditOrderRemarkExtra($adminInfo)) {
$order->remark_extra = (string) ($params['remark_extra'] ?? '');
}
$order->remark_assistant = (string) ($params['remark_assistant'] ?? '');
$diagId = (int) $order->diagnosis_id; $diagId = (int) $order->diagnosis_id;
@@ -1141,6 +1174,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1219,6 +1253,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1287,6 +1322,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1365,6 +1401,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1503,6 +1540,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1589,6 +1627,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1659,6 +1698,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1775,6 +1815,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1885,6 +1926,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -1972,6 +2014,7 @@ class PrescriptionOrderLogic
$out = $order->toArray(); $out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo); self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out); self::attachLinkedPayOrders($out);
return $out; return $out;
@@ -29,6 +29,7 @@ class PrescriptionOrderValidate extends BaseValidate
'pay_amount' => 'require|float|gt:0', 'pay_amount' => 'require|float|gt:0',
'pay_remark' => 'max:200', 'pay_remark' => 'max:200',
'remark_extra' => 'max:500', 'remark_extra' => 'max:500',
'remark_assistant' => 'max:500',
'action' => 'require|in:approve,reject', 'action' => 'require|in:approve,reject',
'remark' => 'max:500', 'remark' => 'max:500',
'fulfillment_status' => 'require|integer|in:3,7,8,9,10,11,12', 'fulfillment_status' => 'require|integer|in:3,7,8,9,10,11,12',
@@ -55,13 +56,13 @@ class PrescriptionOrderValidate extends BaseValidate
'create' => [ 'create' => [
'prescription_id', 'diagnosis_id', 'recipient_name', 'recipient_phone', 'shipping_address', 'prescription_id', 'diagnosis_id', 'recipient_name', 'recipient_phone', 'shipping_address',
'is_follow_up', 'prev_staff', 'service_channel', 'service_package', 'is_follow_up', 'prev_staff', 'service_channel', 'service_package',
'tracking_number', 'express_company', 'fee_type', 'amount', 'remark_extra', 'pay_order_ids', 'tracking_number', 'express_company', 'fee_type', 'amount', 'remark_extra', 'remark_assistant', 'pay_order_ids',
], ],
'detail' => ['id'], 'detail' => ['id'],
'edit' => [ 'edit' => [
'id', 'recipient_name', 'recipient_phone', 'shipping_address', 'id', 'recipient_name', 'recipient_phone', 'shipping_address',
'is_follow_up', 'prev_staff', 'service_channel', 'service_package', 'is_follow_up', 'prev_staff', 'service_channel', 'service_package',
'tracking_number', 'express_company', 'fee_type', 'amount', 'remark_extra', 'pay_order_ids', 'tracking_number', 'express_company', 'fee_type', 'amount', 'remark_extra', 'remark_assistant', 'pay_order_ids',
], ],
'logisticsTrace' => ['id', 'phone_tail'], 'logisticsTrace' => ['id', 'phone_tail'],
'auditPrescription' => ['id', 'action', 'remark'], 'auditPrescription' => ['id', 'action', 'remark'],
@@ -0,0 +1,16 @@
-- 处方业务订单:查看/编辑「备注」(对内 remark_extra),与前端 v-perms、接口脱敏一致
-- 执行前确认表前缀为 zyt_;若已有相同 perms 则跳过。
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', '订单对内备注', '', 53,
'tcm.prescriptionOrder/editRemarkExtra', '', '',
'', '', 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/editRemarkExtra');
@@ -0,0 +1,3 @@
-- 消费者处方业务订单:医助备注(对内)
ALTER TABLE `tcm_prescription_order`
ADD COLUMN `remark_assistant` varchar(500) NOT NULL DEFAULT '' COMMENT '医助备注' AFTER `remark_extra`;
@@ -0,0 +1,15 @@
-- 业绩看板「二中心复诊」医助拆解接口(fans/yeji · pid=293
-- 幂等:按 perms 判重
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
293, 'A', '业绩-复诊拆解', '', 11,
'stats.yejiStats/revisitBreakdown', '', '', '', '',
1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE EXISTS (SELECT 1 FROM `zyt_system_menu` p WHERE p.id = 293)
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.perms = 'stats.yejiStats/revisitBreakdown'
);