diff --git a/admin/src/views/fans/yeji.vue b/admin/src/views/fans/yeji.vue
index a190d5dc..7f6b8bab 100644
--- a/admin/src/views/fans/yeji.vue
+++ b/admin/src/views/fans/yeji.vue
@@ -411,7 +411,7 @@
add_external_contact 事件数,按接待企微成员归属部门;定渠道时须客户带对应企微标签。
- 名称含「二中心」的部门在选定渠道下进线计 0。
+ 二中心及其组织下级(与部门 ErCenter 子树一致)在选定渠道下进线计 0。
@@ -423,9 +423,9 @@
- 诊单指派日志条数(成功指派:to_assistant_id > 0),按被指派医助归属展示部门。
+ tcm_diagnosis_assign_log 行数(成功指派:to_assistant_id > 0),**仅按 related_po_create_time** 落在所选区间内(与日志表快照一致;诊单手动指派已写入该字段)。按被指派医助归属展示部门。
选定渠道时收窄与「{{ tb.channel_name }}」业绩同口径(标签诊单或 channels EXISTS)。
- 名称含「二中心」的部门在选定渠道下本列计 0。
+ 二中心及其组织下级在选定渠道下本列计 0。
未选渠道时不限客户来源,与「合计业绩」列渠道无关。
@@ -547,11 +547,12 @@
- 有渠道时:合计行 ROI = {{ tb.channel_name }}业绩合计(与上表该渠道业绩列合计一致,不含名称含「二中心」的部门)÷ 投放成本合计。
- 部门行 ROI = 该部门 {{ tb.channel_name }}业绩 ÷ 该部门 投放成本;二中心部门该渠道业绩与进线为 0。与「合计业绩」列无关。
+ 有渠道时:合计行 ROI = {{ tb.channel_name }}业绩合计(与上表该渠道业绩列合计一致,不含二中心及其组织下级)÷ 投放成本合计。
+ 部门行 ROI = 该部门 {{ tb.channel_name }}业绩 ÷ 该部门 投放成本;二中心子树部门该渠道业绩与进线为 0、投放成本分摊进线计 0,故无 ROI。与「合计业绩」列无关。
- 合计业绩 ÷ 投放成本。投放成本按各部门进线条数占比从总成本中分摊。
+ 合计行 ROI = 全表业绩合计(不含二中心子树各部门行业绩)÷ 投放成本合计;
+ 部门行 = 该部门「合计业绩」口径业绩 ÷ 该部门均摊投放成本;二中心子树部门不参与成本分摊(进线占比计 0),故无 ROI。
@@ -564,7 +565,7 @@
区间与渠道与账务表一致后得到总投放成本;单进线成本 = 总成本 ÷ 各展示部门进线合计;
- 各部门行投放成本 = 单进线成本 × 该部门进线(四舍五入尾差并入进线最多的部门)。
+ 各部门行投放成本 = 单进线成本 × 该部门进线(四舍五入尾差并入进线最多的部门)。二中心及其组织下级进线在分摊中计 0(与 ROI 分子扣除范围一致)。
diff --git a/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue b/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue
index 87638f45..46a56711 100644
--- a/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue
+++ b/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue
@@ -12,6 +12,25 @@
{{ assistantLogCellText(row, 'to') }}
+
+
+ {{ relatedPoCreatorCell(row) }}
+
+
+
+
+ 快照·业务单创建时间
+
+
+
+
+
+ {{ relatedPoCreateTimeCell(row) }}
+
+
@@ -20,6 +39,7 @@
+
+
diff --git a/admin/src/views/tcm/diagnosis/index.vue b/admin/src/views/tcm/diagnosis/index.vue
index f797cdd1..3c6bfb27 100644
--- a/admin/src/views/tcm/diagnosis/index.vue
+++ b/admin/src/views/tcm/diagnosis/index.vue
@@ -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 = []
}
diff --git a/server/app/adminapi/logic/stats/YejiStatsLogic.php b/server/app/adminapi/logic/stats/YejiStatsLogic.php
index f1a24942..14185758 100644
--- a/server/app/adminapi/logic/stats/YejiStatsLogic.php
+++ b/server/app/adminapi/logic/stats/YejiStatsLogic.php
@@ -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 $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 $feeByAdmin
- * @param array $leadsByAdmin
+ * @param array $adminToPrimary
+ * @param array $erCenterDeptSet
+ * @param array $feeByAdmin
+ * @param array $leadsByAdmin
+ * @param array|null $dealOrderByAdmin
* @param array|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 $erCenterDeptSet
* @param array $channelPerformance
- * @param array $channelConsults
- * @return float 扣减金额(与 sumGlobalChannelScopedPerformance 对齐)
+ * @param array $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 $leads
+ * @param array $map
+ * @param array $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;
}
}
diff --git a/server/app/adminapi/logic/tcm/DiagnosisLogic.php b/server/app/adminapi/logic/tcm/DiagnosisLogic.php
index d8f69b26..fccdd594 100644
--- a/server/app/adminapi/logic/tcm/DiagnosisLogic.php
+++ b/server/app/adminapi/logic/tcm/DiagnosisLogic.php
@@ -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;