更新
This commit is contained in:
@@ -68,6 +68,19 @@ export function yejiStatsLeadLines(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: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
|
||||
@@ -669,6 +669,49 @@
|
||||
</el-table-column>
|
||||
|
||||
</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>
|
||||
</el-form-item>
|
||||
|
||||
@@ -1183,7 +1226,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-form-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注">
|
||||
<el-input
|
||||
v-model="createOrderForm.remark_extra"
|
||||
type="textarea"
|
||||
@@ -1193,6 +1236,16 @@
|
||||
show-word-limit
|
||||
/>
|
||||
</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-row>
|
||||
</div>
|
||||
@@ -1333,6 +1386,7 @@ import {
|
||||
prescriptionDetail,
|
||||
prescriptionOrderCreate,
|
||||
prescriptionOrderPaidPayOrders,
|
||||
prescriptionOrderLists,
|
||||
getDoctors,
|
||||
prescriptionLibraryLists
|
||||
} from '@/api/tcm'
|
||||
@@ -1475,6 +1529,7 @@ const createOrderForm = reactive({
|
||||
amount: 0,
|
||||
internal_cost: undefined as number | undefined,
|
||||
remark_extra: '',
|
||||
remark_assistant: '',
|
||||
pay_order_ids: [] as number[]
|
||||
})
|
||||
|
||||
@@ -1714,6 +1769,7 @@ function resetCreateOrderForm() {
|
||||
createOrderForm.amount = 0
|
||||
createOrderForm.internal_cost = undefined
|
||||
createOrderForm.remark_extra = ''
|
||||
createOrderForm.remark_assistant = ''
|
||||
createOrderForm.pay_order_ids = []
|
||||
createOrderPatientList.value = []
|
||||
createOrderPaidOrders.value = []
|
||||
@@ -1884,7 +1940,8 @@ async function submitCreateOrderFromPrescription() {
|
||||
tracking_number: createOrderForm.tracking_number || '',
|
||||
fee_type: createOrderForm.fee_type,
|
||||
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
|
||||
})
|
||||
|
||||
/** 编辑页:关联业务订单上的服用天数、医助备注(与处方提示一致) */
|
||||
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<{
|
||||
openViewOnly: (id: number) => Promise<void>
|
||||
} | null>(null)
|
||||
@@ -2882,6 +2985,7 @@ const resetForm = () => {
|
||||
editForm.is_system_auto = 0
|
||||
editForm.business_prescription_audit_rejected = 0
|
||||
editForm.business_prescription_audit_remark = ''
|
||||
clearRxLinkedOrderHint()
|
||||
}
|
||||
|
||||
// 新增处方
|
||||
@@ -3009,8 +3113,9 @@ const handleEdit = async (row: any) => {
|
||||
setTimeout(() => {
|
||||
isLoadingData.value = false
|
||||
}, 0)
|
||||
|
||||
|
||||
showEdit.value = true
|
||||
void loadRxLinkedOrderHintForEdit(Number(editForm.id))
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
scheduleEditSignatureInit()
|
||||
|
||||
@@ -994,7 +994,7 @@
|
||||
<template v-if="detailLinkedAppointment">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px]">
|
||||
<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"
|
||||
>时间:{{ detailLinkedAppointment.appointment_date }}
|
||||
@@ -1056,7 +1056,8 @@
|
||||
{{ expressCompanyLabel(detailData.express_company) }}
|
||||
</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">
|
||||
<span class="po-audit-remark">{{ detailData.prescription_audit_remark || '—' }}</span>
|
||||
</el-descriptions-item>
|
||||
@@ -1527,7 +1528,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-form-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注">
|
||||
<el-input
|
||||
v-model="editForm.remark_extra"
|
||||
type="textarea"
|
||||
@@ -1537,6 +1538,16 @@
|
||||
show-word-limit
|
||||
/>
|
||||
</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-row>
|
||||
</div>
|
||||
@@ -2032,11 +2043,11 @@
|
||||
<div class="rx-rp-cols-head">
|
||||
<div class="rx-rp-col-head">
|
||||
<span>用药 (单剂)</span>
|
||||
<span>总量</span>
|
||||
<span>{{ prescriptionTabType === 'user' ? '用量' : '总量' }}</span>
|
||||
</div>
|
||||
<div class="rx-rp-col-head">
|
||||
<span>用药 (单剂)</span>
|
||||
<span>总量</span>
|
||||
<span>{{ prescriptionTabType === 'user' ? '用量' : '总量' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3521,6 +3532,7 @@ const editForm = reactive({
|
||||
fee_type: 3,
|
||||
amount: 0,
|
||||
remark_extra: '',
|
||||
remark_assistant: '',
|
||||
pay_order_ids: [] as number[],
|
||||
internal_cost: undefined as number | string | undefined,
|
||||
/** 关联诊单医助 id(只读,来自订单详情;列表筛选用) */
|
||||
@@ -3771,6 +3783,7 @@ async function openEdit(row: { id: number }) {
|
||||
|
||||
editForm.amount = amt
|
||||
editForm.remark_extra = d.remark_extra || ''
|
||||
editForm.remark_assistant = d.remark_assistant || ''
|
||||
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.internal_cost =
|
||||
@@ -3809,6 +3822,7 @@ async function submitEdit() {
|
||||
fee_type: editForm.fee_type,
|
||||
amount: editForm.amount,
|
||||
remark_extra: editForm.remark_extra || '',
|
||||
remark_assistant: editForm.remark_assistant || '',
|
||||
|
||||
// BUG FIX: 修复之前无法清空部分选填字段的问题
|
||||
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,
|
||||
amount: Number(d.amount) || 0,
|
||||
remark_extra: d.remark_extra || '',
|
||||
remark_assistant: d.remark_assistant || '',
|
||||
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 : [],
|
||||
// 仅覆盖快递字段
|
||||
|
||||
@@ -1091,7 +1091,8 @@
|
||||
{{ expressCompanyLabel(detailData.express_company) }}
|
||||
</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">
|
||||
<span class="po-audit-remark">{{ detailData.prescription_audit_remark || '—' }}</span>
|
||||
</el-descriptions-item>
|
||||
@@ -1563,7 +1564,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-form-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注">
|
||||
<el-input
|
||||
v-model="editForm.remark_extra"
|
||||
type="textarea"
|
||||
@@ -1573,6 +1574,16 @@
|
||||
show-word-limit
|
||||
/>
|
||||
</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-row>
|
||||
</div>
|
||||
@@ -3575,6 +3586,7 @@ const editForm = reactive({
|
||||
fee_type: 3,
|
||||
amount: 0,
|
||||
remark_extra: '',
|
||||
remark_assistant: '',
|
||||
pay_order_ids: [] as number[],
|
||||
internal_cost: undefined as number | string | undefined,
|
||||
/** 关联诊单医助 id(只读,来自订单详情;列表筛选用) */
|
||||
@@ -3825,6 +3837,7 @@ async function openEdit(row: { id: number }) {
|
||||
|
||||
editForm.amount = amt
|
||||
editForm.remark_extra = d.remark_extra || ''
|
||||
editForm.remark_assistant = d.remark_assistant || ''
|
||||
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.internal_cost =
|
||||
@@ -3863,6 +3876,7 @@ async function submitEdit() {
|
||||
fee_type: editForm.fee_type,
|
||||
amount: editForm.amount,
|
||||
remark_extra: editForm.remark_extra || '',
|
||||
remark_assistant: editForm.remark_assistant || '',
|
||||
|
||||
// BUG FIX: 修复之前无法清空部分选填字段的问题
|
||||
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,
|
||||
amount: Number(d.amount) || 0,
|
||||
remark_extra: d.remark_extra || '',
|
||||
remark_assistant: d.remark_assistant || '',
|
||||
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 : [],
|
||||
// 仅覆盖快递字段
|
||||
|
||||
+824
-348
File diff suppressed because it is too large
Load Diff
@@ -1623,6 +1623,7 @@ defineExpose({
|
||||
|
||||
.el-drawer__body {
|
||||
background: #f8fafc;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.el-drawer__footer {
|
||||
@@ -1631,22 +1632,48 @@ defineExpose({
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-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 {
|
||||
display: none;
|
||||
height: 0;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
float: none !important;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.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/leaderboard 医助排行榜(按展示部门分表)
|
||||
* - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条)
|
||||
* - GET stats.yejiStats/revisitBreakdown 二中心复诊下钻:医助 × 业务订单笔数
|
||||
*/
|
||||
class YejiStatsController extends BaseAdminController
|
||||
{
|
||||
@@ -121,4 +122,13 @@ class YejiStatsController extends BaseAdminController
|
||||
|
||||
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) {
|
||||
$query->where('fulfillment_status', '<>', 4);
|
||||
}
|
||||
$this->applyYejiErCenterRevisitOrderFilter($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_time;revisit_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) {
|
||||
PrescriptionOrderLogic::maskInternalCostIfNeeded($item, $this->adminInfo);
|
||||
PrescriptionOrderLogic::maskRemarkExtraIfNeeded($item, $this->adminInfo);
|
||||
$pid = (int) ($item['id'] ?? 0);
|
||||
$item['linked_pay_order_count'] = (int) PrescriptionOrderPayOrder::where(
|
||||
'prescription_order_id',
|
||||
|
||||
@@ -19,6 +19,7 @@ use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminDept;
|
||||
use app\common\model\dept\Dept;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use think\facade\Db;
|
||||
|
||||
|
||||
/**
|
||||
@@ -64,7 +65,9 @@ class DeptLogic extends BaseLogic
|
||||
if (!empty($lists)) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 名称含「二中心」的部门 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 列表树状结构
|
||||
* @param $array
|
||||
|
||||
@@ -14,6 +14,9 @@ use think\facade\Db;
|
||||
*
|
||||
* - 进线数据 = 区间内 add_external_contact 事件,按接待员工(user_id) 归属部门
|
||||
* · 与 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;
|
||||
* 部门归属:优先 COALESCE(挂号.assistant_id, 诊单.assistant_id) 映射到展示「中心」,否则回退接诊医生 doctor_id;仍无法归属则计入「未归属中心」行。
|
||||
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4;NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag/creator 两套口径均按订单计,非挂号条数。
|
||||
@@ -269,6 +272,16 @@ class YejiStatsLogic
|
||||
: null;
|
||||
|
||||
$leads = self::countLeads($startTs, $endTs, $adminToPrimary, $tagFilterId);
|
||||
$assignsByDept = self::countDiagnosisAssignsByDept(
|
||||
$startTs,
|
||||
$endTs,
|
||||
$adminToPrimary,
|
||||
$tagDiagIds,
|
||||
$tagFallback ? $tagAssistantIds : null,
|
||||
$appointmentChannelValues,
|
||||
$channelFilterActive,
|
||||
$dataScopeAdminIds
|
||||
);
|
||||
// 接诊:
|
||||
// - consults(全量) → "接诊诊单" 列;不限渠道
|
||||
// - channelConsults(渠道) → 「{渠道}成交单」列;未选渠道时与 consults 相同
|
||||
@@ -312,6 +325,7 @@ class YejiStatsLogic
|
||||
$channelConsults
|
||||
);
|
||||
self::excludeErCenterFromLeads($primaryDeptIds, $deptNameMap, $leads);
|
||||
self::excludeErCenterFromLeads($primaryDeptIds, $deptNameMap, $assignsByDept);
|
||||
}
|
||||
|
||||
$globalCost = round((float) self::sumGlobalCost($startDate, $endDate, $channelCode), 1);
|
||||
@@ -323,9 +337,18 @@ class YejiStatsLogic
|
||||
);
|
||||
$costs = self::allocateCostByLeads($globalCost, $tableRowDeptIds, $leadsForCost);
|
||||
|
||||
@set_time_limit(120);
|
||||
$revisitPack = DeptLogic::getErCenterRevisitRollupIndexedByDept($startTs, $endTs);
|
||||
$revisitTotals = $revisitPack['totals'];
|
||||
$revisitSlotsByDept = $revisitPack['slots'];
|
||||
|
||||
// ── 5) 组装每行 ──────────────────────────────────────────────
|
||||
$rows = [];
|
||||
$totalLead = 0;
|
||||
$totalAssign = 0;
|
||||
$totalRevisit = 0;
|
||||
/** @var array<int, int> $totalRevisitSlots */
|
||||
$totalRevisitSlots = [];
|
||||
$totalChannelConsult = 0;
|
||||
$totalPerf = 0.0;
|
||||
$totalChannelPerf = 0.0;
|
||||
@@ -348,10 +371,16 @@ class YejiStatsLogic
|
||||
$rowRoi = $rowCost > 0 ? round($cperf / $rowCost, 1) : null;
|
||||
|
||||
$dDeal = (int) ($dealOrderByDept[$deptId] ?? 0);
|
||||
$assignCnt = (int) ($assignsByDept[$deptId] ?? 0);
|
||||
$rv = (int) ($revisitTotals[$deptId] ?? 0);
|
||||
$rvSlots = $revisitSlotsByDept[$deptId] ?? [];
|
||||
$rows[] = [
|
||||
'dept_id' => $deptId,
|
||||
'dept_name' => self::formatYejiDeptRowDisplayName($deptId, $deptById),
|
||||
'lead_count' => $lead,
|
||||
'assign_count' => $assignCnt,
|
||||
'revisit_count' => $rv,
|
||||
'revisit_slots' => $rvSlots,
|
||||
'consult_count' => $cnt,
|
||||
'deal_order_count' => $dDeal,
|
||||
// {渠道}成交单:选定渠道时与 completed_performance 同源;未选渠道时与 consult_count 相同
|
||||
@@ -366,6 +395,12 @@ class YejiStatsLogic
|
||||
];
|
||||
|
||||
$totalLead += $lead;
|
||||
$totalAssign += $assignCnt;
|
||||
$totalRevisit += $rv;
|
||||
foreach ($rvSlots as $slot => $cntRv) {
|
||||
$slot = (int) $slot;
|
||||
$totalRevisitSlots[$slot] = ($totalRevisitSlots[$slot] ?? 0) + (int) $cntRv;
|
||||
}
|
||||
$totalChannelConsult += $cCnt;
|
||||
$totalPerf += $perf;
|
||||
$totalChannelPerf += $cperf;
|
||||
@@ -411,6 +446,9 @@ class YejiStatsLogic
|
||||
'dept_id' => 0,
|
||||
'dept_name' => '未归属中心',
|
||||
'lead_count' => 0,
|
||||
'assign_count' => 0,
|
||||
'revisit_count' => 0,
|
||||
'revisit_slots' => [],
|
||||
'consult_count' => $unmappedConsult,
|
||||
'deal_order_count' => $unmappedDealOrder,
|
||||
'channel_consult_count' => 0,
|
||||
@@ -437,6 +475,10 @@ class YejiStatsLogic
|
||||
// ROI 合计 = 渠道业绩合计(与「completed」列一致,**不含二中心**)÷ 投放成本合计
|
||||
$totalRoiNumerator = round($totalChannelPerf, 1);
|
||||
|
||||
if ($totalRevisitSlots !== []) {
|
||||
ksort($totalRevisitSlots, SORT_NUMERIC);
|
||||
}
|
||||
|
||||
$scopedTableNote = ($explicitDeptFilter
|
||||
? ' 已筛选展示部门时,表格合计仅含所选部门树内数据,不出现「全站未归属」补差行(避免把其他中心业绩/单量算入未归属)。若勾选的是带下级的部门(如某「中心」),表格只列出其下级部门行,不单独占一行父级。'
|
||||
: '')
|
||||
@@ -464,6 +506,9 @@ class YejiStatsLogic
|
||||
'rows' => $rows,
|
||||
'total' => [
|
||||
'lead_count' => $totalLead,
|
||||
'assign_count' => $totalAssign,
|
||||
'revisit_count' => $totalRevisit,
|
||||
'revisit_slots' => $totalRevisitSlots,
|
||||
'consult_count' => $totalConsult,
|
||||
'deal_order_count' => $totalDealOrderCount,
|
||||
'channel_consult_count' => $totalChannelConsult,
|
||||
@@ -503,7 +548,9 @@ class YejiStatsLogic
|
||||
/**
|
||||
* 医助排行榜:按展示部门分表,每表内按诊金降序排名。
|
||||
* 诊金口径:无渠道=诊单 dg.assistant_id 业绩(与列表 assistant_id 筛选一致);有渠道={渠道}业绩/dict 诊单医助;二中心在选渠道下注 0。
|
||||
* 被指派数 = 区间内诊单指派日志(to_assistant_id>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
|
||||
*
|
||||
@@ -532,6 +579,10 @@ class YejiStatsLogic
|
||||
$tagFallback = $c['tagFallback'];
|
||||
$appointmentChannelValues = $c['appointmentChannelValues'];
|
||||
$channelFilterActive = $c['channelFilterActive'];
|
||||
$dataScopeRestricted = !empty($c['dataScopeRestricted']);
|
||||
$dataScopeAdminIds = $dataScopeRestricted && isset($c['dataScopeVisibleAdminIds'])
|
||||
? $c['dataScopeVisibleAdminIds']
|
||||
: null;
|
||||
|
||||
$channelName = $channelInfo ? (string) $channelInfo['channel_name'] : '';
|
||||
|
||||
@@ -569,11 +620,30 @@ class YejiStatsLogic
|
||||
$leadsByAdmin = self::countLeadsByAdmin($startTs, $endTs, $tagFilterId);
|
||||
/** 与部门表接诊同口径;标签/医助穿透时与业绩同源收窄,避免榜上证务与接诊脱节 */
|
||||
$consultsByAdmin = self::aggregateConsultsByAdmin($startTs, $endTs, $tagDiagIds, $scopedAssistants);
|
||||
$assignsByAdmin = self::countDiagnosisAssignsByAdmin(
|
||||
$startTs,
|
||||
$endTs,
|
||||
$tagDiagIds,
|
||||
$tagFallback ? $tagAssistantIds : null,
|
||||
$appointmentChannelValues,
|
||||
$channelFilterActive,
|
||||
$dataScopeAdminIds
|
||||
);
|
||||
|
||||
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');
|
||||
$assistantSet = [];
|
||||
foreach ($assistantIdRows as $rid) {
|
||||
@@ -598,15 +668,16 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
$rangeNote = $channelFilterActive
|
||||
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩口径(与部门表该渠道业绩列一致)。接诊率 = 诊金 ÷ 进线(元/进线)。'
|
||||
. '「二中心」医助在该渠道下诊金与进线计 0。')
|
||||
: '排行诊金 = 处方订单列表 assistant_id 口径(诊单医助业绩,与列表按医助筛选一致)。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。';
|
||||
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩口径(与部门表该渠道业绩列一致)。被指派数与部门表同口径。接诊率 = 诊金 ÷ 进线(元/进线)。'
|
||||
. '「二中心」医助在该渠道下诊金、进线与被指派计 0。')
|
||||
: '排行诊金 = 处方订单列表 assistant_id 口径(诊单医助业绩,与列表按医助筛选一致)。被指派数为指派至该医助的次数。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。';
|
||||
|
||||
$leaderboards = [];
|
||||
foreach ($tableRowDeptIds as $deptId) {
|
||||
$leafName = $deptNameMap[$deptId] ?? ('部门#' . $deptId);
|
||||
$deptName = self::formatYejiDeptRowDisplayName($deptId, $deptById);
|
||||
$deptAbbr = self::deptDisplayAbbr($leafName);
|
||||
$isErCenterLb = isset($erCenterDeptSet[$deptId]);
|
||||
$rows = [];
|
||||
foreach (array_keys($assistantSet) as $aid) {
|
||||
if (($adminToPrimary[$aid] ?? 0) !== $deptId) {
|
||||
@@ -616,21 +687,37 @@ class YejiStatsLogic
|
||||
$lead = (int) ($leadsByAdmin[$aid] ?? 0);
|
||||
$consult = (int) ($consultsByAdmin[$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;
|
||||
}
|
||||
$rawFee = (float) ($feeByAdmin[$aid] ?? 0.0);
|
||||
$rate = $lead > 0 ? round($rawFee / $lead, 1) : 0.0;
|
||||
$rows[] = [
|
||||
$row = [
|
||||
'admin_id' => $aid,
|
||||
'name' => $adminNames[$aid] ?? ('#' . $aid),
|
||||
'fee_amount' => $fee,
|
||||
'lead_count' => $lead,
|
||||
'assign_count' => $assignCnt,
|
||||
'consult_count' => $consult,
|
||||
'deal_order_count' => $dealOrders,
|
||||
'consult_rate' => $rate,
|
||||
'dept_abbr' => $deptAbbr,
|
||||
];
|
||||
if ($isErCenterLb) {
|
||||
$row['revisit_count'] = $rv;
|
||||
$row['revisit_slots'] = $rvSlots;
|
||||
}
|
||||
$rows[] = $row;
|
||||
}
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
if ($a['fee_amount'] != $b['fee_amount']) {
|
||||
@@ -648,6 +735,7 @@ class YejiStatsLogic
|
||||
'dept_id' => $deptId,
|
||||
'dept_name' => $deptName,
|
||||
'dept_abbr' => $deptAbbr,
|
||||
'er_center_subtree' => $isErCenterLb,
|
||||
'rows' => $rows,
|
||||
];
|
||||
}
|
||||
@@ -658,7 +746,7 @@ class YejiStatsLogic
|
||||
'channel_code' => $channelCode,
|
||||
'channel_name' => $channelName,
|
||||
'channel_filter_note' => $channelCode !== ''
|
||||
? '医助榜:名称含「二中心」的部门在选定渠道下诊金、进线计 0,与部门看板一致。'
|
||||
? '医助榜:名称含「二中心」的部门在选定渠道下诊金、进线、被指派数计 0,与部门看板一致。'
|
||||
: '',
|
||||
'range_note' => $rangeNote,
|
||||
'leaderboards' => $leaderboards,
|
||||
@@ -1174,6 +1262,181 @@ class YejiStatsLogic
|
||||
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」等额外限制。
|
||||
@@ -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_slot:0=复诊合计(全部复诊笔),≥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 维度返回全量/渠道业绩金额(不按部门折叠)。
|
||||
*
|
||||
@@ -1931,13 +2288,15 @@ class YejiStatsLogic
|
||||
*
|
||||
* @param array<int, float> $feeByAdmin
|
||||
* @param array<int, int> $leadsByAdmin
|
||||
* @param array<int, int>|null $assignsByAdmin
|
||||
*/
|
||||
private static function excludeErCenterFromAdminLeaderboard(
|
||||
array $adminToPrimary,
|
||||
array $deptNameMap,
|
||||
array &$feeByAdmin,
|
||||
array &$leadsByAdmin,
|
||||
?array &$dealOrderByAdmin = null
|
||||
?array &$dealOrderByAdmin = null,
|
||||
?array &$assignsByAdmin = null
|
||||
): void {
|
||||
foreach ($adminToPrimary as $aid => $pid) {
|
||||
if (!self::isErCenterDisplayPrimaryName($deptNameMap[$pid] ?? '')) {
|
||||
@@ -1948,6 +2307,9 @@ class YejiStatsLogic
|
||||
if ($dealOrderByAdmin !== null) {
|
||||
$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 表示不校验 */
|
||||
public static function depositMinAmount(): float
|
||||
{
|
||||
@@ -613,7 +638,10 @@ class PrescriptionOrderLogic
|
||||
$order->fee_type = (int) $params['fee_type'];
|
||||
$order->amount = round((float) $params['amount'], 2);
|
||||
$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->prescription_audit_status = 0;
|
||||
$order->prescription_audit_remark = '';
|
||||
@@ -637,6 +665,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -658,6 +687,7 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
$arr = $row->toArray();
|
||||
self::maskInternalCostIfNeeded($arr, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($arr, $adminInfo);
|
||||
self::attachLinkedPayOrders($arr);
|
||||
|
||||
// 添加创建人姓名
|
||||
@@ -1050,7 +1080,10 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
$order->fee_type = (int) $params['fee_type'];
|
||||
$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;
|
||||
|
||||
@@ -1141,6 +1174,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1219,6 +1253,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1287,6 +1322,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1365,6 +1401,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1503,6 +1540,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1589,6 +1627,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1659,6 +1698,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1775,6 +1815,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1885,6 +1926,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
@@ -1972,6 +2014,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
|
||||
@@ -29,6 +29,7 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'pay_amount' => 'require|float|gt:0',
|
||||
'pay_remark' => 'max:200',
|
||||
'remark_extra' => 'max:500',
|
||||
'remark_assistant' => 'max:500',
|
||||
'action' => 'require|in:approve,reject',
|
||||
'remark' => 'max:500',
|
||||
'fulfillment_status' => 'require|integer|in:3,7,8,9,10,11,12',
|
||||
@@ -55,13 +56,13 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'create' => [
|
||||
'prescription_id', 'diagnosis_id', 'recipient_name', 'recipient_phone', 'shipping_address',
|
||||
'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'],
|
||||
'edit' => [
|
||||
'id', 'recipient_name', 'recipient_phone', 'shipping_address',
|
||||
'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'],
|
||||
'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'
|
||||
);
|
||||
Reference in New Issue
Block a user