更新
This commit is contained in:
@@ -411,7 +411,7 @@
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
add_external_contact 事件数,按接待企微成员归属部门;定渠道时须客户带对应企微标签。<br />
|
||||
<template v-if="tb.channel_name">
|
||||
<b>名称含「二中心」的部门</b>在选定渠道下进线计 <b>0</b>。
|
||||
<b>二中心及其组织下级</b>(与部门 ErCenter 子树一致)在选定渠道下进线计 <b>0</b>。
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -423,9 +423,9 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
诊单指派日志条数(成功指派:<b>to_assistant_id > 0</b>),按<b>被指派医助</b>归属展示部门。<template v-if="tb.channel_name">
|
||||
<b>tcm_diagnosis_assign_log</b> 行数(成功指派:<b>to_assistant_id > 0</b>),**仅按 <b>related_po_create_time</b>** 落在所选区间内(与日志表快照一致;诊单手动指派已写入该字段)。按<b>被指派医助</b>归属展示部门。<template v-if="tb.channel_name">
|
||||
选定渠道时收窄与「{{ tb.channel_name }}」业绩同口径(标签诊单或 channels EXISTS)。<br />
|
||||
<b>名称含「二中心」的部门</b>在选定渠道下本列计 <b>0</b>。
|
||||
<b>二中心及其组织下级</b>在选定渠道下本列计 <b>0</b>。
|
||||
</template>
|
||||
<template v-else><br />未选渠道时不限客户来源,与「合计业绩」列渠道无关。</template>
|
||||
</div>
|
||||
@@ -547,11 +547,12 @@
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
<template v-if="tb.channel_name">
|
||||
有渠道时:<b>合计行 ROI</b> = <b>{{ tb.channel_name }}业绩合计</b>(与上表该渠道业绩列合计一致,<b>不含名称含「二中心」的部门</b>)÷ <b>投放成本合计</b>。<br />
|
||||
部门行 ROI = 该部门 <b>{{ tb.channel_name }}业绩</b> ÷ 该部门 <b>投放成本</b>;二中心部门该渠道业绩与进线为 0。与「合计业绩」列无关。
|
||||
有渠道时:<b>合计行 ROI</b> = <b>{{ tb.channel_name }}业绩合计</b>(与上表该渠道业绩列合计一致,<b>不含二中心及其组织下级</b>)÷ <b>投放成本合计</b>。<br />
|
||||
部门行 ROI = 该部门 <b>{{ tb.channel_name }}业绩</b> ÷ 该部门 <b>投放成本</b>;<b>二中心子树</b>部门该渠道业绩与进线为 0、投放成本分摊进线计 0,故无 ROI。与「合计业绩」列无关。
|
||||
</template>
|
||||
<template v-else>
|
||||
<b>合计业绩</b> ÷ <b>投放成本</b>。投放成本按各部门<b>进线条数占比</b>从总成本中分摊。
|
||||
<b>合计行 ROI</b> = 全表业绩合计(<b>不含二中心子树</b>各部门行业绩)÷ <b>投放成本合计</b>;<br />
|
||||
部门行 = 该部门「合计业绩」口径业绩 ÷ 该部门均摊投放成本;<b>二中心子树</b>部门不参与成本分摊(进线占比计 0),故无 ROI。
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -564,7 +565,7 @@
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
区间与渠道与账务表一致后得到<b>总投放成本</b>;<b>单进线成本</b> = 总成本 ÷ 各展示部门<b>进线合计</b>;<br />
|
||||
各部门行投放成本 = 单进线成本 × 该部门进线(四舍五入尾差并入进线最多的部门)。
|
||||
各部门行投放成本 = 单进线成本 × 该部门进线(四舍五入尾差并入进线最多的部门)。<b>二中心及其组织下级</b>进线在分摊中计 <b>0</b>(与 ROI 分子扣除范围一致)。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
|
||||
@@ -12,6 +12,25 @@
|
||||
{{ assistantLogCellText(row, 'to') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="快照·业务单创建人" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ relatedPoCreatorCell(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="快照·业务单创建时间" width="190">
|
||||
<template #header>
|
||||
<span>快照·业务单创建时间</span>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
content="related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"
|
||||
>
|
||||
<el-icon class="assign-log-col-hint"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
{{ relatedPoCreateTimeCell(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" width="110" prop="operator_name" />
|
||||
<el-table-column label="操作账号" width="120" prop="operator_account" show-overflow-tooltip />
|
||||
<el-table-column label="IP" width="130" prop="ip" show-overflow-tooltip />
|
||||
@@ -20,6 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { tcmDiagnosisAssignLogList } from '@/api/tcm'
|
||||
|
||||
@@ -30,6 +50,35 @@ const props = defineProps<{
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
|
||||
/** 对应日志表 related_po_creator_id:触发本次释放/指派的业务订单 creator */
|
||||
function relatedPoCreatorCell(row: Record<string, unknown>): string {
|
||||
const name = row.related_po_creator_name
|
||||
if (name != null && String(name).trim() !== '') {
|
||||
return String(name)
|
||||
}
|
||||
const id = Number(row.related_po_creator_id)
|
||||
if (Number.isFinite(id) && id > 0) {
|
||||
return `ID:${id}`
|
||||
}
|
||||
return '—'
|
||||
}
|
||||
|
||||
/** 对应日志表 related_po_create_time(Unix)格式化为可读时间 */
|
||||
function relatedPoCreateTimeCell(row: Record<string, unknown>): string {
|
||||
const t = row.related_po_create_time_text
|
||||
if (t != null && String(t).trim() !== '') {
|
||||
return String(t)
|
||||
}
|
||||
const ts = Number(row.related_po_create_time)
|
||||
if (!Number.isFinite(ts) || ts <= 0) {
|
||||
return '—'
|
||||
}
|
||||
const d = new Date(ts * 1000)
|
||||
if (Number.isNaN(d.getTime())) return '—'
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
function assistantLogCellText(row: Record<string, unknown>, side: 'from' | 'to'): string {
|
||||
const nameKey = side === 'from' ? 'from_assistant_name' : 'to_assistant_name'
|
||||
const idKey = side === 'from' ? 'from_assistant_id' : 'to_assistant_id'
|
||||
@@ -69,3 +118,12 @@ watch(
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.assign-log-col-hint {
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: help;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1390,13 +1390,12 @@ const handleConfirmAssign = async () => {
|
||||
feedback.msgWarning('没有可指派的诊单,请关闭后重新勾选')
|
||||
return
|
||||
}
|
||||
const promises = ids.map((id) =>
|
||||
tcmDiagnosisAssign({
|
||||
for (const id of ids) {
|
||||
await tcmDiagnosisAssign({
|
||||
id,
|
||||
assistant_id: assignForm.value.assistant_id
|
||||
assistant_id: assignForm.value.assistant_id!,
|
||||
})
|
||||
)
|
||||
await Promise.all(promises)
|
||||
}
|
||||
selectedIds.value = []
|
||||
batchAssignIds.value = []
|
||||
}
|
||||
|
||||
@@ -15,9 +15,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**(与进线一致)
|
||||
* · 与 qywx.customer/todayArrival、客户列表同源事件表;选定渠道时客户须带对应企微标签(contact_tag);**二中心及其组织下级在选定渠道下进线计 0**
|
||||
* - 被指派数 = 区间内 `tcm_diagnosis_assign_log` 且 **to_assistant_id>0**,且 **related_po_create_time>0** 并落在区间(**一律按快照字段统计**,与履约释放写入一致;诊单后台指派时会回填该字段,见 DiagnosisLogic::assign)
|
||||
* · **二中心及其组织下级在选定渠道下计 0**(与进线一致)
|
||||
* - 复诊统计 = 仅**名称含「二中心」的部门及其组织架构下级**:区间内业务单按患者 create_time 排序,第 1 笔不计,第 2 笔起分列「复诊2」「复诊3」…;口径同业绩单条件;按诊单医助在组织树内归属 rollup(含下级),与 dept 模块 ErCenter 复诊算法一致
|
||||
* - 已完成挂号 = status=3 挂号条数(appointment_date),列「已完成挂号」;部门归属:有效医助→接诊医生→展示行映射。
|
||||
* - 接诊诊单 = 列名对应**业务订单条数**(非取消、create_time 区间),与处方列表按 **assistant_dept_id** 筛选时一致:诊单 **assistant_id** 或订单 **creator_id** 任一人事部门落在该行子树即计入;多行命中时取**最深的展示部门行**,与「业绩归属医助」分组可能不同。
|
||||
@@ -25,15 +25,15 @@ use think\facade\Db;
|
||||
* - 合计业绩 = 区间内业务订单 amount 求和(排除已取消(4) 与软删),**部门列**与「接诊诊单」一致:诊单医助或订单创建人的人事部门落在该展示行子树则计入,多行命中取最深展示行;与处方列表按 assistant_dept_id 筛选后的金额合计一致。
|
||||
* 无法归入任一展示部门的金额单列「未归属中心」行;**全表合计行**仍为同时间/业绩条件的全表 SUM(不限部门)。
|
||||
* · 时间窗与 stats_order_amount_performance 一致;列表若另有患者筛选或账号数据域收窄,金额可能仍不同
|
||||
* · 选定渠道时「{渠道}业绩」列仍按比例展示;合计列全量与渠道列含未归属补差;**名称含「二中心」的展示部门不参与该渠道进线/业绩/成交单/ROI 分子**
|
||||
* · 选定渠道时「{渠道}业绩」列仍按比例展示;合计列全量与渠道列含未归属补差;**二中心及其组织下级展示部门不参与该渠道进线/业绩/成交单/ROI 分子**(与同模块 DeptLogic::getErCenterSubtreeDeptIdSet 一致)
|
||||
* - {渠道}业绩 = 与「处方订单列表」业绩同口径(≠4):按 **o.create_time** 落入区间,按**业绩归属医助**归部门(与处方列表医助筛选一致);
|
||||
* 渠道判定仅用 EXISTS:"患者有任一已完成挂号 (status=3) 且 channels 命中字典",**不限挂号时点**;无字典映射时回退 tag/creator 口径
|
||||
* - 成交单数 = 区间内非取消业务订单 **条数**;按**业绩归属医助**映射到「中心」(与处方订单列表医助条件一致);无法归属计入「未归属中心」;合计=全表 COUNT。
|
||||
* - 投放成本 = zyt_account_cost 在日期区间内 **amount** 合计(选定渠道时 `media_channel_code = channel_code` 过滤)。
|
||||
* · **各部门行投放成本** = 总投放成本 × (该部门进线 ÷ 各展示部门进线合计)。相当于先算 **单进线成本** = 总成本 ÷ 总进线,再乘部门进线。
|
||||
* · 选定渠道下二中心部门进线为 0,故该行分摊成本为 0;合计行成本 = 账务汇总总额(部门行分摊之和与之对齐,尾差归入进线最多的部门)。
|
||||
* · ROI = 渠道业绩合计 ÷ 总投放成本(选定渠道时分子不含二中心渠道业绩)。
|
||||
* - ROI = 选定渠道时为 **{渠道}业绩合计** ÷ 投放成本(与表格「渠道业绩」列合计一致;未选渠道时分子为全表业绩合计;成本为 0 时返回 null)
|
||||
* · **二中心及其组织下级**不参与均摊投放成本(进线分摊分母对其计 0,与选定渠道下进线清零规则独立);其余部门按进线占比分摊。合计行成本 = 账务汇总总额。
|
||||
* · ROI = (渠道业绩合计或全量业绩合计,**均不含二中心子树各部门的对应业绩**)÷ 总投放成本(成本为 0 时返回 null)
|
||||
* - ROI = **不含二中心子树**后的 **{渠道}业绩合计**(或未选渠道时为全量业绩口径下同一套「部门行之和」)÷ 投放成本;部门行在四舍五入前若分摊成本为 0 则无 ROI
|
||||
*
|
||||
* 「业绩/接诊标签匹配」:
|
||||
* 优先 tcm_diagnosis.external_userid = 客户 external_userid;
|
||||
@@ -274,6 +274,7 @@ class YejiStatsLogic
|
||||
: null;
|
||||
|
||||
$leads = self::countLeads($startTs, $endTs, $adminToPrimary, $tagFilterId);
|
||||
$erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
|
||||
$assignsByDept = self::countDiagnosisAssignsByDept(
|
||||
$startTs,
|
||||
$endTs,
|
||||
@@ -338,6 +339,14 @@ class YejiStatsLogic
|
||||
);
|
||||
$performance = $perfBoth['all'];
|
||||
$channelPerformance = $perfBoth['scoped'];
|
||||
/** 二中心子树展示行「计入 ROI 分子的业绩」合计({渠道}业绩或与合计业绩相同),供未选渠道时从全表分子里扣除 */
|
||||
$sumCompletedPerfErCenter = 0.0;
|
||||
foreach ($tableRowDeptIds as $erDeptId) {
|
||||
if (!isset($erCenterDeptSet[(int) $erDeptId])) {
|
||||
continue;
|
||||
}
|
||||
$sumCompletedPerfErCenter += round((float) ($channelPerformance[$erDeptId] ?? 0), 1);
|
||||
}
|
||||
// 成交单数(全量):与合计业绩列同列表部门口径
|
||||
$dealOrderByDept = $listAlignedPack !== null
|
||||
? $listAlignedPack['count']
|
||||
@@ -352,17 +361,15 @@ class YejiStatsLogic
|
||||
);
|
||||
$globalDealOrderCount = self::countGlobalDealOrders($startTs, $endTs);
|
||||
|
||||
/** 选定渠道时从全量渠道 SUM 与部门行中同步扣减「二中心」展示部门上的渠道业绩 */
|
||||
/** 选定渠道时从全量渠道 SUM 与部门行中同步扣减二中心子树部门的渠道业绩/成交单;进线、被指派同清零 */
|
||||
$erCenterChannelStrip = 0.0;
|
||||
if ($channelCode !== '') {
|
||||
$erCenterChannelStrip = self::excludeErCenterFromChannelMetrics(
|
||||
$primaryDeptIds,
|
||||
$deptNameMap,
|
||||
$erCenterChannelStrip = self::excludeErCenterSubtreeFromChannelMetrics(
|
||||
$tableRowDeptIds,
|
||||
$erCenterDeptSet,
|
||||
$channelPerformance,
|
||||
$channelConsults
|
||||
);
|
||||
self::excludeErCenterFromLeads($primaryDeptIds, $deptNameMap, $leads);
|
||||
self::excludeErCenterFromLeads($primaryDeptIds, $deptNameMap, $assignsByDept);
|
||||
}
|
||||
|
||||
$globalCost = round((float) self::sumGlobalCost($startDate, $endDate, $channelCode), 1);
|
||||
@@ -372,8 +379,14 @@ class YejiStatsLogic
|
||||
$leads,
|
||||
$deptToDescendants
|
||||
);
|
||||
self::zeroIntKeyedDeptMetricsInErCenterSubtree($leadsForCost, $erCenterDeptSet);
|
||||
$costs = self::allocateCostByLeads($globalCost, $tableRowDeptIds, $leadsForCost);
|
||||
|
||||
if ($channelCode !== '') {
|
||||
self::zeroIntKeyedDeptMetricsInErCenterSubtree($leads, $erCenterDeptSet);
|
||||
self::zeroIntKeyedDeptMetricsInErCenterSubtree($assignsByDept, $erCenterDeptSet);
|
||||
}
|
||||
|
||||
@set_time_limit(120);
|
||||
$revisitPack = DeptLogic::getErCenterRevisitRollupIndexedByDept($startTs, $endTs);
|
||||
$revisitTotals = $revisitPack['totals'];
|
||||
@@ -537,8 +550,16 @@ class YejiStatsLogic
|
||||
}
|
||||
$totalCost = round($globalCost, 1);
|
||||
|
||||
// ROI 合计 = 渠道业绩合计(与「completed」列一致,**不含二中心**)÷ 投放成本合计
|
||||
$totalRoiNumerator = round($totalChannelPerf, 1);
|
||||
// ROI 合计:选渠道时底栏「{渠道}业绩合计」已扣除二中心子树($erCenterChannelStrip);未选渠道时用全表业绩并减去各二中心子树展示行之和
|
||||
$totalRoiNumerator = round(
|
||||
max(
|
||||
0.0,
|
||||
$channelFilterActive
|
||||
? $totalChannelPerf
|
||||
: ($totalChannelPerf - $sumCompletedPerfErCenter)
|
||||
),
|
||||
1
|
||||
);
|
||||
|
||||
if ($totalRevisitSlots !== []) {
|
||||
ksort($totalRevisitSlots, SORT_NUMERIC);
|
||||
@@ -550,7 +571,7 @@ class YejiStatsLogic
|
||||
. ($dataScopeRestricted
|
||||
? ' 当前账号受数据范围限制时,底栏合计业绩与列表同域。'
|
||||
: '');
|
||||
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入。部门行「合计业绩」与「接诊诊单」均为列表 assistant_dept_id 口径(诊单医助或订单创建人人事部门∪子树,多行命中取最深展示行)。多表「本月」区间为当月1日至服务器当日。**投放成本**按进线占比分摊。';
|
||||
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入。部门行「合计业绩」与「接诊诊单」均为列表 assistant_dept_id 口径(诊单医助或订单创建人人事部门∪子树,多行命中取最深展示行)。多表「本月」区间为当月1日至服务器当日。**投放成本**按进线占比分摊,**二中心子树展示部门进线在分摊中计 0**(与部门 ErCenter 子树一致)。';
|
||||
|
||||
return [
|
||||
'start_date' => $startDate,
|
||||
@@ -562,7 +583,7 @@ class YejiStatsLogic
|
||||
'channel_filter_note' => $channelCode !== ''
|
||||
? ('{渠道}业绩:与「处方订单列表」业绩同口径,按订单 create_time 归日、仅剔除履约已取消(4)(NULL 与其它状态均计入);渠道判定用"该患者是否有挂号 channels 命中 dict + status=3"(不限挂号时点)。'
|
||||
. ' {渠道}成交单数:与{渠道}业绩同源,仅剔除已取消(4)的订单条数(非挂号条数)。'
|
||||
. ' 「已完成挂号」列同 admin 挂号已完成列表;「接诊诊单」列为业务订单条数(非取消),与列表按 assistant_dept_id 筛选时一致(诊单医助或创建人∪子树)。名称含「二中心」的部门**不计入**该渠道进线、业绩与成交单。**投放成本**按各部门进线条数占展示部门进线合计的比例,从账务总投放金额中分摊(总成本÷总进线=单进线成本)。ROI = 渠道业绩(不含二中心)÷ 总投放成本。进线按所选标签客户精确过滤。'
|
||||
. ' 「已完成挂号」列同 admin 挂号已完成列表;「接诊诊单」列为业务订单条数(非取消),与列表按 assistant_dept_id 筛选时一致(诊单医助或创建人∪子树)。**二中心及其组织下级**(与部门模块 ErCenter 子树一致)**不计入**该渠道进线、业绩与成交单。**投放成本**按各部门进线条数占展示部门进线合计的比例分摊,**二中心子树进线在分摊中计 0**(未选渠道时仍显示进线数,仅不参与均摊)。ROI = 渠道业绩(不含二中心子树)÷ 总投放成本。进线按所选标签客户精确过滤。'
|
||||
. ($tagFallback
|
||||
? ' ⚠ 当前业务尚未回填 tcm_diagnosis.external_userid 与 user_auth.unionid,已自动退化为「该标签客户的跟进医助 → 区间内接诊/业绩」口径,仅用于趋势观察。'
|
||||
: '')
|
||||
@@ -707,9 +728,10 @@ class YejiStatsLogic
|
||||
);
|
||||
|
||||
if ($channelFilterActive) {
|
||||
$erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
|
||||
self::excludeErCenterFromAdminLeaderboard(
|
||||
$adminToPrimary,
|
||||
$deptNameMap,
|
||||
$erCenterDeptSet,
|
||||
$feeByAdmin,
|
||||
$leadsByAdmin,
|
||||
$dealOrderByAdmin,
|
||||
@@ -745,7 +767,7 @@ class YejiStatsLogic
|
||||
|
||||
$rangeNote = $channelFilterActive
|
||||
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩口径(与部门表该渠道业绩列一致)。被指派数与部门表同口径。接诊率 = 诊金 ÷ 进线(元/进线)。'
|
||||
. '「二中心」医助在该渠道下诊金、进线与被指派计 0。'
|
||||
. '「二中心」及其组织下级医助在该渠道下诊金、进线与被指派计 0。'
|
||||
. ' 预约诊单:预约表按「预约日期」落在区间内、有效医助口径同接诊,状态含已预约/已完成/已过号(不含已取消);科室下医助全量展示。')
|
||||
: '排行诊金 = 处方订单列表医助口径(诊单医助或代建创建人与列表一致)。被指派数为指派至该医助的次数。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。'
|
||||
. ' 预约诊单:预约表按「预约日期」落在区间内、有效医助口径同接诊,状态含已预约/已完成/已过号(不含已取消);科室下医助全量展示。';
|
||||
@@ -815,7 +837,7 @@ class YejiStatsLogic
|
||||
'channel_code' => $channelCode,
|
||||
'channel_name' => $channelName,
|
||||
'channel_filter_note' => $channelCode !== ''
|
||||
? '医助榜:名称含「二中心」的部门在选定渠道下诊金、进线、被指派数计 0,与部门看板一致。'
|
||||
? '医助榜:**二中心及其组织下级**在选定渠道下诊金、进线、被指派数计 0,与部门看板一致。'
|
||||
: '',
|
||||
'range_note' => $rangeNote,
|
||||
'leaderboards' => $leaderboards,
|
||||
@@ -1613,7 +1635,8 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 被指派数:诊单指派日志(to_assistant_id>0)按被指派医助归属展示部门;选定渠道时收窄与「{渠道}业绩」同源。
|
||||
* 被指派数:统计 `tcm_diagnosis_assign_log`(to_assistant_id>0),**仅**按 **related_po_create_time** 落入区间(须 >0)。
|
||||
* 选定字典渠道时 INNER JOIN 诊单以满足挂号 channels EXISTS;tag 退化口径只过滤日志字段,不强制挂诊单。
|
||||
*
|
||||
* @param array<int, int> $adminToPrimary
|
||||
* @param int[]|null $tagDiagIds
|
||||
@@ -1636,13 +1659,11 @@ class YejiStatsLogic
|
||||
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.related_po_create_time', '>', 0)
|
||||
->where('lg.related_po_create_time', 'between', [$startTs, $endTs])
|
||||
->where('lg.to_assistant_id', '>', 0);
|
||||
|
||||
if ($channelFilterActive) {
|
||||
if ($appointmentChannelValues !== []) {
|
||||
$norm = self::normalizeAppointmentChannelValues($appointmentChannelValues);
|
||||
@@ -1653,6 +1674,8 @@ class YejiStatsLogic
|
||||
if ($pack === null) {
|
||||
return [];
|
||||
}
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$query->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER');
|
||||
$query->whereRaw($pack[0], $pack[1]);
|
||||
} elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
@@ -1718,11 +1741,10 @@ class YejiStatsLogic
|
||||
?array $dataScopeAdminIds = null,
|
||||
?array $channelInfo = 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.related_po_create_time', '>', 0)
|
||||
->where('lg.related_po_create_time', 'between', [$startTs, $endTs])
|
||||
->where('lg.to_assistant_id', '>', 0);
|
||||
|
||||
if ($channelFilterActive) {
|
||||
@@ -1735,6 +1757,8 @@ class YejiStatsLogic
|
||||
if ($pack === null) {
|
||||
return [];
|
||||
}
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$query->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER');
|
||||
$query->whereRaw($pack[0], $pack[1]);
|
||||
} elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
@@ -2361,14 +2385,14 @@ class YejiStatsLogic
|
||||
);
|
||||
}
|
||||
|
||||
$deptNameMap = $c['deptNameMap'];
|
||||
$channelCode = (string) $c['channelCode'];
|
||||
if ($channelCode !== '' && self::isErCenterDisplayPrimaryName((string) ($deptNameMap[$deptId] ?? ''))) {
|
||||
$erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
|
||||
if ($channelCode !== '' && isset($erCenterDeptSet[$deptId])) {
|
||||
return $empty(
|
||||
$c,
|
||||
$deptId,
|
||||
self::formatYejiDeptRowDisplayName($deptId, $c['deptById']),
|
||||
'与看板一致:名称含「二中心」的展示部门在选定渠道下进线计 0,无明细。'
|
||||
'与看板一致:二中心及其组织下级展示部门在选定渠道下进线计 0,无明细。'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3227,22 +3251,26 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 选定渠道时:二中心展示部门下属医助的渠道诊金、进线清零(与部门行一致)。
|
||||
* 选定渠道时:二中心子树(DeptLogic::getErCenterSubtreeDeptIdSet)下属医助的渠道诊金、进线清零(与部门行一致)。
|
||||
*
|
||||
* @param array<int, float> $feeByAdmin
|
||||
* @param array<int, int> $leadsByAdmin
|
||||
* @param array<int, int> $adminToPrimary
|
||||
* @param array<int, true> $erCenterDeptSet
|
||||
* @param array<int, float> $feeByAdmin
|
||||
* @param array<int, int> $leadsByAdmin
|
||||
* @param array<int, int>|null $dealOrderByAdmin
|
||||
* @param array<int, int>|null $assignsByAdmin
|
||||
*/
|
||||
private static function excludeErCenterFromAdminLeaderboard(
|
||||
array $adminToPrimary,
|
||||
array $deptNameMap,
|
||||
array $erCenterDeptSet,
|
||||
array &$feeByAdmin,
|
||||
array &$leadsByAdmin,
|
||||
?array &$dealOrderByAdmin = null,
|
||||
?array &$assignsByAdmin = null
|
||||
): void {
|
||||
foreach ($adminToPrimary as $aid => $pid) {
|
||||
if (!self::isErCenterDisplayPrimaryName($deptNameMap[$pid] ?? '')) {
|
||||
$pid = (int) $pid;
|
||||
if ($pid <= 0 || !isset($erCenterDeptSet[$pid])) {
|
||||
continue;
|
||||
}
|
||||
$feeByAdmin[$aid] = 0.0;
|
||||
@@ -4270,51 +4298,46 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示部门名称是否属「二中心」系:选定渠道时该部门不参与该渠道进线/业绩/成交单/ROI 分子。
|
||||
*/
|
||||
private static function isErCenterDisplayPrimaryName(string $name): bool
|
||||
{
|
||||
return mb_strpos($name, '二中心') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选定渠道时:名称含「二中心」的展示部门渠道业绩与渠道成交单置 0,并从后续全量渠道 SUM 中扣减同等金额。
|
||||
* 选定渠道时:二中心子树展示行渠道业绩与渠道成交单置 0,并返回扣减金额(与 sumGlobalChannelScopedPerformance 对齐)。
|
||||
*
|
||||
* @param int[] $tableRowDeptIds
|
||||
* @param array<int, true> $erCenterDeptSet
|
||||
* @param array<int, float|string|int> $channelPerformance
|
||||
* @param array<int, int> $channelConsults
|
||||
* @return float 扣减金额(与 sumGlobalChannelScopedPerformance 对齐)
|
||||
* @param array<int, int> $channelConsults
|
||||
*/
|
||||
private static function excludeErCenterFromChannelMetrics(
|
||||
array $primaryDeptIds,
|
||||
array $deptNameMap,
|
||||
private static function excludeErCenterSubtreeFromChannelMetrics(
|
||||
array $tableRowDeptIds,
|
||||
array $erCenterDeptSet,
|
||||
array &$channelPerformance,
|
||||
array &$channelConsults
|
||||
): float {
|
||||
$stripped = 0.0;
|
||||
foreach ($primaryDeptIds as $pid) {
|
||||
if (!self::isErCenterDisplayPrimaryName($deptNameMap[$pid] ?? '')) {
|
||||
foreach ($tableRowDeptIds as $tid) {
|
||||
$tid = (int) $tid;
|
||||
if (!isset($erCenterDeptSet[$tid])) {
|
||||
continue;
|
||||
}
|
||||
$stripped += (float) ($channelPerformance[$pid] ?? 0);
|
||||
$channelPerformance[$pid] = 0.0;
|
||||
$channelConsults[$pid] = 0;
|
||||
$stripped += (float) ($channelPerformance[$tid] ?? 0);
|
||||
$channelPerformance[$tid] = 0.0;
|
||||
$channelConsults[$tid] = 0;
|
||||
}
|
||||
|
||||
return round($stripped, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选定渠道时:名称含「二中心」的展示部门进线数置 0(与同渠道向业绩/成交单一致)。
|
||||
* 将 map 中落在二中心子树(DeptLogic)的部门 key 清零,用于进线分摊或渠道清零后的展示列。
|
||||
*
|
||||
* @param array<int, int> $leads
|
||||
* @param array<int|string, int> $map
|
||||
* @param array<int, true> $erCenterDeptSet
|
||||
*/
|
||||
private static function excludeErCenterFromLeads(array $primaryDeptIds, array $deptNameMap, array &$leads): void
|
||||
private static function zeroIntKeyedDeptMetricsInErCenterSubtree(array &$map, array $erCenterDeptSet): void
|
||||
{
|
||||
foreach ($primaryDeptIds as $pid) {
|
||||
if (!self::isErCenterDisplayPrimaryName($deptNameMap[$pid] ?? '')) {
|
||||
continue;
|
||||
foreach (array_keys($map) as $k) {
|
||||
$id = (int) $k;
|
||||
if ($id > 0 && isset($erCenterDeptSet[$id])) {
|
||||
$map[$k] = 0;
|
||||
}
|
||||
$leads[$pid] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -478,7 +478,66 @@ class DiagnosisLogic extends BaseLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 指派医助(每次成功更新写入一条操作记录,批量指派为多次接口调用各记一条)
|
||||
* 诊单下用于指派日志快照的「最新」业务单:按 create_time 最新,其次 id(避免误用更早下单但 id 更大的记录)。
|
||||
*
|
||||
* @return array{creator_id: int, create_time: int}|null
|
||||
*/
|
||||
private static function getLatestPrescriptionOrderSnapshotForDiagnosis(int $diagnosisId): ?array
|
||||
{
|
||||
if ($diagnosisId <= 0) {
|
||||
return null;
|
||||
}
|
||||
$row = Db::name('tcm_prescription_order')
|
||||
->where('diagnosis_id', $diagnosisId)
|
||||
->whereNull('delete_time')
|
||||
->order('create_time', 'desc')
|
||||
->order('id', 'desc')
|
||||
->field(['creator_id', 'create_time'])
|
||||
->find();
|
||||
|
||||
if ($row === null || $row === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'creator_id' => (int) ($row['creator_id'] ?? 0),
|
||||
'create_time' => (int) ($row['create_time'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 诊单 current assistant_id 为 0 时,从最近一条指派日志推断「原医助」(如发货释放后:上一条多为 from=X、to=0)。
|
||||
* - 最近一条 to_assistant_id > 0:视为上一任持有人(库未同步时的兜底)
|
||||
* - 否则若 from_assistant_id > 0:视为释放/取消前的持有人
|
||||
*/
|
||||
private static function inferPreviousAssistantIdFromLatestAssignLog(int $diagnosisId): int
|
||||
{
|
||||
if ($diagnosisId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$prev = Db::name('tcm_diagnosis_assign_log')
|
||||
->where('diagnosis_id', $diagnosisId)
|
||||
->order('id', 'desc')
|
||||
->field(['from_assistant_id', 'to_assistant_id'])
|
||||
->find();
|
||||
if ($prev === null || $prev === []) {
|
||||
return 0;
|
||||
}
|
||||
$pTo = (int) ($prev['to_assistant_id'] ?? 0);
|
||||
$pFrom = (int) ($prev['from_assistant_id'] ?? 0);
|
||||
if ($pTo > 0) {
|
||||
return $pTo;
|
||||
}
|
||||
if ($pFrom > 0) {
|
||||
return $pFrom;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 指派医助(每次成功更新写入一条操作记录,批量指派为多次接口调用各记一条)。
|
||||
* **原医助**:优先诊单 `assistant_id`;若为 0 且本次为指派到某人,则从**最近一条**指派日志推断(覆盖发货释放后库为 0、业务上仍有「前任」医助的情况)。
|
||||
* @param array $params
|
||||
* @return bool
|
||||
*/
|
||||
@@ -493,33 +552,66 @@ class DiagnosisLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
$diagnosis = Diagnosis::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$diagnosis) {
|
||||
self::setError('诊单不存在');
|
||||
$now = time();
|
||||
$poSnap = self::getLatestPrescriptionOrderSnapshotForDiagnosis($id);
|
||||
$relatedPoCreatorId = 0;
|
||||
$relatedPoCreateTime = 0;
|
||||
if ($poSnap !== null) {
|
||||
$relatedPoCreatorId = $poSnap['creator_id'];
|
||||
$relatedPoCreateTime = $poSnap['create_time'];
|
||||
}
|
||||
if ($relatedPoCreateTime <= 0) {
|
||||
$relatedPoCreateTime = $now;
|
||||
$relatedPoCreatorId = 0;
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 事务内 FOR UPDATE 直读库:确保「原医助」与 tcm_diagnosis.assistant_id 一致,避免模型缓存或并发覆盖
|
||||
$diagLock = Db::name('tcm_diagnosis')
|
||||
->where('id', $id)
|
||||
->whereNull('delete_time')
|
||||
->lock(true)
|
||||
->field(['id', 'assistant_id'])
|
||||
->find();
|
||||
if ($diagLock === null || $diagLock === []) {
|
||||
Db::rollback();
|
||||
self::setError('诊单不存在');
|
||||
|
||||
return false;
|
||||
}
|
||||
$fromAssistantId = (int) ($diagLock['assistant_id'] ?? 0);
|
||||
if ($fromAssistantId <= 0 && $toAssistantId > 0) {
|
||||
$fromAssistantId = self::inferPreviousAssistantIdFromLatestAssignLog($id);
|
||||
}
|
||||
|
||||
Db::name('tcm_diagnosis')
|
||||
->where('id', $id)
|
||||
->whereNull('delete_time')
|
||||
->update(['assistant_id' => $toAssistantId]);
|
||||
|
||||
$req = request();
|
||||
$admin = $req->adminInfo ?? [];
|
||||
// Db::insert 直写:与 assignLogList 一致,避免模型 allowField/schema 丢列
|
||||
Db::name('tcm_diagnosis_assign_log')->insert([
|
||||
'diagnosis_id' => $id,
|
||||
'from_assistant_id' => $fromAssistantId,
|
||||
'to_assistant_id' => $toAssistantId,
|
||||
'operator_admin_id' => (int) ($admin['admin_id'] ?? 0),
|
||||
'operator_name' => (string) ($admin['name'] ?? ''),
|
||||
'operator_account' => (string) ($admin['account'] ?? ''),
|
||||
'ip' => (string) ($req->ip() ?? ''),
|
||||
'related_po_creator_id' => $relatedPoCreatorId,
|
||||
'related_po_create_time' => $relatedPoCreateTime,
|
||||
'create_time' => $now,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
self::setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$fromAssistantId = (int) ($diagnosis->getAttr('assistant_id') ?? 0);
|
||||
|
||||
Db::startTrans();
|
||||
Diagnosis::where('id', $id)->whereNull('delete_time')->update([
|
||||
'assistant_id' => $toAssistantId,
|
||||
]);
|
||||
|
||||
$req = request();
|
||||
$admin = $req->adminInfo ?? [];
|
||||
DiagnosisAssignLog::create([
|
||||
'diagnosis_id' => $id,
|
||||
'from_assistant_id' => $fromAssistantId,
|
||||
'to_assistant_id' => $toAssistantId,
|
||||
'operator_admin_id' => (int) ($admin['admin_id'] ?? 0),
|
||||
'operator_name' => (string) ($admin['name'] ?? ''),
|
||||
'operator_account' => (string) ($admin['account'] ?? ''),
|
||||
'ip' => (string) ($req->ip() ?? ''),
|
||||
'create_time' => time(),
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user