This commit is contained in:
Your Name
2026-05-08 15:20:02 +08:00
parent dfbb6d82b3
commit 623bda2ecb
5 changed files with 271 additions and 98 deletions
+8 -7
View File
@@ -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 &gt; 0</b><b>被指派医助</b>归属展示部门<template v-if="tb.channel_name">
<b>tcm_diagnosis_assign_log</b> 成功指派<b>to_assistant_id &gt; 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_timeUnix)格式化为可读时间 */
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>
+4 -5
View File
@@ -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;
}
}
+116 -24
View File
@@ -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;