更新
This commit is contained in:
@@ -20,6 +20,16 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
|
||||
{
|
||||
use HasDataScopeFilter;
|
||||
|
||||
/**
|
||||
* 诊单编辑/详情「挂号记录」Tab:按诊单 ID 拉全量挂号,不做医生/医助角色收窄与数据范围过滤。
|
||||
* 须同时传 patient_id(挂号表存的是诊单 id)与本开关,避免列表页被滥用拓宽可见范围。
|
||||
*/
|
||||
private function appointmentListsScopeRelaxedForDiagnosis(): bool
|
||||
{
|
||||
return (int) ($this->params['diag_scope_relax'] ?? 0) === 1
|
||||
&& (int) ($this->params['patient_id'] ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据隔离:医生或医助(u.assistant_id = diag.assistant_id)命中可见集合;progress_board 场景放开(看板跨医生查看)
|
||||
*/
|
||||
@@ -28,6 +38,9 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
|
||||
if ($progressBoard) {
|
||||
return;
|
||||
}
|
||||
if ($this->appointmentListsScopeRelaxedForDiagnosis()) {
|
||||
return;
|
||||
}
|
||||
if (!$this->dataScopeShouldApply()) {
|
||||
return;
|
||||
}
|
||||
@@ -128,7 +141,8 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
|
||||
|
||||
// 面诊进度看板:可切换查看各医生挂号,不按当前账号角色收窄
|
||||
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
|
||||
if (!$progressBoard) {
|
||||
$diagScopeRelax = $this->appointmentListsScopeRelaxedForDiagnosis();
|
||||
if (!$progressBoard && !$diagScopeRelax) {
|
||||
// 如果是医生角色(role_id=1),只显示挂自己号的预约
|
||||
if (in_array(1, $roleIds)) {
|
||||
$query->where('a.doctor_id', $this->adminId);
|
||||
@@ -288,7 +302,8 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
|
||||
}
|
||||
|
||||
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
|
||||
if (!$progressBoard) {
|
||||
$diagScopeRelax = $this->appointmentListsScopeRelaxedForDiagnosis();
|
||||
if (!$progressBoard && !$diagScopeRelax) {
|
||||
if (in_array(1, $roleIds)) {
|
||||
$query->where('a.doctor_id', $this->adminId);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
|
||||
/**
|
||||
* 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_time;revisit_slot=0 为全部复诊分项合计)。
|
||||
* admin 维度按订单创建人 o.creator_id(与部门表复诊列、排行榜复诊列同口径)。
|
||||
*/
|
||||
private function applyYejiErCenterRevisitOrderFilter($query): void
|
||||
{
|
||||
@@ -138,7 +139,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
if ($t0 === false || $t1 === false) {
|
||||
return;
|
||||
}
|
||||
$ids = DeptLogic::listErCenterRevisitOrderIdsForAssistant(
|
||||
$ids = DeptLogic::listErCenterRevisitOrderIdsForCreator(
|
||||
$assistantId,
|
||||
$slot,
|
||||
(int) $t0,
|
||||
@@ -870,16 +871,23 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
$query->whereExists("SELECT 1 FROM {$rxTbl} rx WHERE rx.id = {$poTbl}.prescription_id AND rx.delete_time IS NULL AND rx.creator_id = {$doctorId}");
|
||||
}
|
||||
|
||||
/** 业绩看板入口(yeji_order_drawer=1)所有筛选统一按订单创建人;其它入口(处方订单列表 / 患者跟进等)保留旧口径(创建人 ∪ 诊单医助) */
|
||||
$yejiPanel = (int) ($this->params['yeji_order_drawer'] ?? 0) === 1;
|
||||
|
||||
if (isset($this->params['assistant_id']) && (int) $this->params['assistant_id'] > 0) {
|
||||
$assistantId = (int) $this->params['assistant_id'];
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$query->where(function ($q) use ($poTbl, $diagTbl, $assistantId) {
|
||||
$q->where('creator_id', $assistantId);
|
||||
$q->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`"
|
||||
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
|
||||
);
|
||||
});
|
||||
if ($yejiPanel) {
|
||||
$query->where('creator_id', $assistantId);
|
||||
} else {
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$query->where(function ($q) use ($poTbl, $diagTbl, $assistantId) {
|
||||
$q->where('creator_id', $assistantId);
|
||||
$q->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`"
|
||||
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->params['assistant_dept_id']) && $this->params['assistant_dept_id'] !== '' && (int) $this->params['assistant_dept_id'] > 0) {
|
||||
@@ -909,23 +917,31 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
return;
|
||||
}
|
||||
$inList = implode(',', $deptIds);
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$adTbl = (new AdminDept())->getTable();
|
||||
$query->where(function ($q) use ($poTbl, $diagTbl, $adTbl, $inList) {
|
||||
$q->whereExists(
|
||||
"SELECT 1 FROM `{$adTbl}` ad INNER JOIN `{$diagTbl}` dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL "
|
||||
. "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND ad.dept_id IN ({$inList})"
|
||||
if ($yejiPanel) {
|
||||
/** 业绩看板部门侧栏:仅订单创建人人事部门 ∈ 子树(与「合计业绩」/「接诊诊单」/「复诊」聚合同口径) */
|
||||
$query->whereExists(
|
||||
"SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})"
|
||||
);
|
||||
$q->whereExists(
|
||||
"SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})",
|
||||
'OR'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$query->where(function ($q) use ($poTbl, $diagTbl, $adTbl, $inList) {
|
||||
$q->whereExists(
|
||||
"SELECT 1 FROM `{$adTbl}` ad INNER JOIN `{$diagTbl}` dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL "
|
||||
. "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND ad.dept_id IN ({$inList})"
|
||||
);
|
||||
$q->whereExists(
|
||||
"SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})",
|
||||
'OR'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩看板侧栏:只保留「业绩归属医助」落在给定 admin_id 集合的订单(与 YejiStatsLogic::sqlPerformanceAttributionAdminExpr 一致)。
|
||||
* 业绩看板侧栏:只保留「业绩归属医助」落在给定 admin_id 集合的订单
|
||||
* (与 YejiStatsLogic::sqlPerformanceAttributionAdminExpr 一致——按订单创建人)。
|
||||
*
|
||||
* @param int[] $adminIds
|
||||
*/
|
||||
@@ -939,18 +955,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
|
||||
return;
|
||||
}
|
||||
$poTbl = (new PrescriptionOrder())->getTable();
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$rxTbl = (new Prescription())->getTable();
|
||||
$inList = implode(',', $adminIds);
|
||||
$query->whereRaw(
|
||||
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg "
|
||||
. "LEFT JOIN `{$rxTbl}` rx ON rx.`id` = `{$poTbl}`.`prescription_id` AND rx.`delete_time` IS NULL "
|
||||
. "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL AND `{$poTbl}`.`diagnosis_id` > 0 AND "
|
||||
. '(CASE WHEN dg.`assistant_id` > 0 THEN dg.`assistant_id` '
|
||||
. "WHEN `{$poTbl}`.`creator_id` > 0 AND (rx.`id` IS NULL OR NOT (`{$poTbl}`.`creator_id` <=> rx.`creator_id`)) THEN `{$poTbl}`.`creator_id` "
|
||||
. 'ELSE 0 END) IN (' . $inList . '))'
|
||||
);
|
||||
$query->whereIn('creator_id', $adminIds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -219,21 +219,24 @@ class DeptLogic extends BaseLogic
|
||||
}
|
||||
|
||||
return $q
|
||||
->field(['o.diagnosis_id', 'o.create_time', 'o.id', 'dg.assistant_id'])
|
||||
->field(['o.diagnosis_id', 'o.create_time', 'o.id', 'dg.assistant_id', 'o.creator_id'])
|
||||
->order(['o.diagnosis_id' => 'asc', 'o.create_time' => 'asc', 'o.id' => 'asc'])
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 二中心子树:各部门复诊 rollup;同时返回按诊单医助聚合的分项(供医助排行榜)。
|
||||
* 二中心子树:各部门复诊 rollup(按订单创建人 creator_id 归属)。
|
||||
* 候选订单仍要求 dg.assistant_id > 0(保持「er-center 案例 = 有医助的诊单」的定义),
|
||||
* 但**复诊归属**改为订单创建人——leaf 部门归属取 creator_id 在二中心子树内的 canonical 部门,
|
||||
* 并产出 by_creator_slots 供医助排行榜复诊列复用,与诊金 / 接诊诊单口径完全一致。
|
||||
*
|
||||
* @param list<int> $subtreeDeptIds
|
||||
*
|
||||
* @return array{
|
||||
* totals: array<int, int>,
|
||||
* slots: array<int, array<int, int>>,
|
||||
* by_assistant_slots: array<int, array<int, int>>
|
||||
* by_creator_slots: array<int, array<int, int>>
|
||||
* }
|
||||
*/
|
||||
private static function computeErCenterRevisitFullPack(
|
||||
@@ -249,12 +252,12 @@ class DeptLogic extends BaseLogic
|
||||
$emptyTotals[$did] = 0;
|
||||
}
|
||||
if ($subtreeDeptIds === []) {
|
||||
return ['totals' => [], 'slots' => [], 'by_assistant_slots' => []];
|
||||
return ['totals' => [], 'slots' => [], 'by_creator_slots' => []];
|
||||
}
|
||||
$subtreeSet = array_fill_keys($subtreeDeptIds, true);
|
||||
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
|
||||
if ($orderRows === []) {
|
||||
return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_assistant_slots' => []];
|
||||
return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_creator_slots' => []];
|
||||
}
|
||||
|
||||
$byPatient = [];
|
||||
@@ -266,29 +269,30 @@ class DeptLogic extends BaseLogic
|
||||
$byPatient[$pid][] = $r;
|
||||
}
|
||||
|
||||
$assistantNeed = [];
|
||||
$creatorNeed = [];
|
||||
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;
|
||||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||||
if ($cid > 0) {
|
||||
$creatorNeed[$cid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$assistantIds = array_keys($assistantNeed);
|
||||
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree($assistantIds, $subtreeSet);
|
||||
$creatorIds = array_keys($creatorNeed);
|
||||
/** 复用 buildAssistantCanonicalDeptInSubtree —— 该方法实际是 admin → 子树内 canonical 部门,与 admin 角色无关 */
|
||||
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree($creatorIds, $subtreeSet);
|
||||
|
||||
/** @var array<int, array<int, int>> $leafByDeptSlot dept_id => [ slot => cnt ],slot≥2 */
|
||||
$leafByDeptSlot = [];
|
||||
/** @var array<int, array<int, int>> $byAssistantSlot */
|
||||
$byAssistantSlot = [];
|
||||
/** @var array<int, array<int, int>> $byCreatorSlot 复诊按订单创建人聚合(与诊金/接诊诊单口径一致) */
|
||||
$byCreatorSlot = [];
|
||||
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;
|
||||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||||
$deptId = $canonicalDept[$cid] ?? null;
|
||||
if ($deptId === null || !isset($subtreeSet[$deptId])) {
|
||||
continue;
|
||||
}
|
||||
@@ -296,11 +300,11 @@ class DeptLogic extends BaseLogic
|
||||
$leafByDeptSlot[$deptId] = [];
|
||||
}
|
||||
$leafByDeptSlot[$deptId][$slot] = ($leafByDeptSlot[$deptId][$slot] ?? 0) + 1;
|
||||
if ($aid > 0) {
|
||||
if (!isset($byAssistantSlot[$aid])) {
|
||||
$byAssistantSlot[$aid] = [];
|
||||
if ($cid > 0) {
|
||||
if (!isset($byCreatorSlot[$cid])) {
|
||||
$byCreatorSlot[$cid] = [];
|
||||
}
|
||||
$byAssistantSlot[$aid][$slot] = ($byAssistantSlot[$aid][$slot] ?? 0) + 1;
|
||||
$byCreatorSlot[$cid][$slot] = ($byCreatorSlot[$cid][$slot] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,16 +317,16 @@ class DeptLogic extends BaseLogic
|
||||
$rollupTotals[$id] = array_sum($rollupSlots[$id] ?? []);
|
||||
}
|
||||
|
||||
foreach ($byAssistantSlot as $aid => $sm) {
|
||||
foreach ($byCreatorSlot as $cid => $sm) {
|
||||
if ($sm !== []) {
|
||||
ksort($byAssistantSlot[$aid], SORT_NUMERIC);
|
||||
ksort($byCreatorSlot[$cid], SORT_NUMERIC);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'totals' => $rollupTotals,
|
||||
'slots' => $rollupSlots,
|
||||
'by_assistant_slots' => $byAssistantSlot,
|
||||
'by_creator_slots' => $byCreatorSlot,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -345,11 +349,11 @@ class DeptLogic extends BaseLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 二中心子树内:各医助成交的复诊业务单笔数(合计 + 复诊2/3/…分项),口径与部门 rollup 同源。
|
||||
* 二中心子树复诊:按订单创建人 creator_id 聚合(与部门 rollup 同源,与诊金/接诊诊单口径一致)。
|
||||
*
|
||||
* @return array{totals: array<int, int>, slots: array<int, array<int, int>>}
|
||||
*/
|
||||
public static function getErCenterRevisitCountsByAssistant(?int $orderStartTs = null, ?int $orderEndTs = null): array
|
||||
public static function getErCenterRevisitCountsByCreator(?int $orderStartTs = null, ?int $orderEndTs = null): array
|
||||
{
|
||||
$erRoots = self::findErCenterRootDeptIds();
|
||||
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
|
||||
@@ -357,42 +361,36 @@ class DeptLogic extends BaseLogic
|
||||
return ['totals' => [], 'slots' => []];
|
||||
}
|
||||
$pack = self::computeErCenterRevisitFullPack($subtreeIds, $orderStartTs, $orderEndTs);
|
||||
$by = $pack['by_assistant_slots'];
|
||||
$by = $pack['by_creator_slots'];
|
||||
$totals = [];
|
||||
$slots = [];
|
||||
foreach ($by as $aid => $slotMap) {
|
||||
$aid = (int) $aid;
|
||||
$totals[$aid] = (int) array_sum($slotMap);
|
||||
$slots[$aid] = $slotMap;
|
||||
foreach ($by as $cid => $slotMap) {
|
||||
$cid = (int) $cid;
|
||||
$totals[$cid] = (int) array_sum($slotMap);
|
||||
$slots[$cid] = $slotMap;
|
||||
}
|
||||
|
||||
return ['totals' => $totals, 'slots' => $slots];
|
||||
}
|
||||
|
||||
/**
|
||||
* 某医助在区间内、计入二中心复诊的业务订单 id(用于看板侧栏按复诊笔数下钻)。
|
||||
* 二中心子树复诊:列出某「订单创建人」在区间内、复诊序列(slot≥2)命中的业务订单 id(用于看板侧栏按复诊笔数下钻)。
|
||||
* $revisitSlot = 0 表示累计全部复诊(第 2 笔起);≥2 时仅该分项。
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function listErCenterRevisitOrderIdsForAssistant(
|
||||
int $assistantId,
|
||||
public static function listErCenterRevisitOrderIdsForCreator(
|
||||
int $creatorId,
|
||||
int $revisitSlot,
|
||||
int $orderStartTs,
|
||||
int $orderEndTs
|
||||
): array {
|
||||
if ($assistantId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) {
|
||||
if ($creatorId <= 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 [];
|
||||
@@ -405,31 +403,16 @@ class DeptLogic extends BaseLogic
|
||||
}
|
||||
$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])) {
|
||||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||||
if ($cid !== $creatorId) {
|
||||
continue;
|
||||
}
|
||||
$oid = (int) ($orders[$i]['id'] ?? 0);
|
||||
@@ -443,8 +426,8 @@ class DeptLogic extends BaseLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按诊单医助拆解笔数。
|
||||
* 与看板行内「复诊」列口径一致:诊单下订单序列第 2 笔起;医助 canonical 部门须落在二中心子树且在该部门行的子树内。
|
||||
* 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按**订单创建人**拆解笔数。
|
||||
* 与看板行内「复诊」列口径一致:订单序列第 2 笔起;创建人 canonical 部门须落在二中心子树且在该部门行的子树内。
|
||||
*
|
||||
* @return array{rows: list<array{admin_id: int, name: string, order_count: int}>}
|
||||
*/
|
||||
@@ -484,17 +467,17 @@ class DeptLogic extends BaseLogic
|
||||
}
|
||||
$byPatient[$pid][] = $r;
|
||||
}
|
||||
$assistantNeed = [];
|
||||
$creatorNeed = [];
|
||||
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;
|
||||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||||
if ($cid > 0) {
|
||||
$creatorNeed[$cid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $erSubtreeSet);
|
||||
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($creatorNeed), $erSubtreeSet);
|
||||
/** @var array<int, int> $counts */
|
||||
$counts = [];
|
||||
foreach ($byPatient as $orders) {
|
||||
@@ -504,15 +487,15 @@ class DeptLogic extends BaseLogic
|
||||
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
|
||||
continue;
|
||||
}
|
||||
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
|
||||
if ($aid <= 0) {
|
||||
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
|
||||
if ($cid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$canon = $canonicalDept[$aid] ?? null;
|
||||
$canon = $canonicalDept[$cid] ?? null;
|
||||
if ($canon === null || !isset($erSubtreeSet[$canon]) || !isset($descFlip[$canon])) {
|
||||
continue;
|
||||
}
|
||||
$counts[$aid] = ($counts[$aid] ?? 0) + 1;
|
||||
$counts[$cid] = ($counts[$cid] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
if ($counts === []) {
|
||||
@@ -527,10 +510,10 @@ class DeptLogic extends BaseLogic
|
||||
->column('name', 'id');
|
||||
}
|
||||
$rows = [];
|
||||
foreach ($counts as $aid => $cnt) {
|
||||
foreach ($counts as $cid => $cnt) {
|
||||
$rows[] = [
|
||||
'admin_id' => (int) $aid,
|
||||
'name' => (string) ($nameRows[$aid] ?? ('#' . $aid)),
|
||||
'admin_id' => (int) $cid,
|
||||
'name' => (string) ($nameRows[$cid] ?? ('#' . $cid)),
|
||||
'order_count' => (int) $cnt,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -18,17 +18,17 @@ use think\facade\Db;
|
||||
* · 与 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 复诊算法一致
|
||||
* - 复诊统计 = 仅**名称含「二中心」的部门及其组织架构下级**:区间内业务单按患者 create_time 排序,第 1 笔不计,第 2 笔起分列「复诊2」「复诊3」…;口径同业绩单条件;按**订单创建人**所在部门在组织树内 rollup(含下级),与排行榜复诊一致
|
||||
* - 已完成挂号 = status=3 挂号条数(appointment_date),列「已完成挂号」;部门归属:有效医助→接诊医生→展示行映射。
|
||||
* - 接诊诊单 = 列名对应**业务订单条数**(非取消、create_time 区间),与处方列表按 **assistant_dept_id** 筛选时一致:诊单 **assistant_id** 或订单 **creator_id** 任一人事部门落在该行子树即计入;多行命中时取**最深的展示部门行**,与「业绩归属医助」分组可能不同。
|
||||
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4;NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag 口径均按订单计(tag 时按诊单医助收窄),非挂号条数。
|
||||
* - 合计业绩 = 区间内业务订单 amount 求和(排除已取消(4) 与软删),**部门列**与「接诊诊单」一致:诊单医助或订单创建人的人事部门落在该展示行子树则计入,多行命中取最深展示行;与处方列表按 assistant_dept_id 筛选后的金额合计一致。
|
||||
* 无法归入任一展示部门的金额单列「未归属中心」行;**全表合计行**仍为同时间/业绩条件的全表 SUM(不限部门)。
|
||||
* - 接诊诊单 = 列名对应**业务订单条数**(非取消、create_time 区间),按**订单创建人**人事部门落在该行子树即计入;多行命中时取**最深的展示部门行**。与处方列表按 assistant_dept_id 筛选不同(列表保留旧口径:诊单医助 ∪ 创建人)。
|
||||
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4;NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag 口径均按订单计(按订单创建人归属),非挂号条数。
|
||||
* - 合计业绩 = 区间内业务订单 amount 求和(排除已取消(4) 与软删),**部门列**与「接诊诊单」一致:**订单创建人**人事部门落在该展示行子树则计入,多行命中取最深展示行;与排行榜诊金同口径。
|
||||
* 无法归入任一展示部门的金额单列「未归属中心」行;**底栏「合计业绩」「接诊诊单」= 当前表内各行业绩列相加**(含未归属行),与弹窗摊分口径一致。
|
||||
* · 时间窗与 stats_order_amount_performance 一致;列表若另有患者筛选或账号数据域收窄,金额可能仍不同
|
||||
* · 选定渠道时「{渠道}业绩」列仍按比例展示;合计列全量与渠道列含未归属补差;**二中心及其组织下级展示部门不参与该渠道进线/业绩/成交单/ROI 分子**(与同模块 DeptLogic::getErCenterSubtreeDeptIdSet 一致)
|
||||
* - {渠道}业绩 = 与「处方订单列表」业绩同口径(≠4):按 **o.create_time** 落入区间,按**业绩归属医助**归部门(与处方列表医助筛选一致);
|
||||
* - {渠道}业绩 = 与「处方订单列表」业绩同口径(≠4):按 **o.create_time** 落入区间,按**订单创建人**归部门;
|
||||
* 渠道判定仅用 EXISTS:"患者有任一已完成挂号 (status=3) 且 channels 命中字典",**不限挂号时点**;无字典映射时回退 tag/creator 口径
|
||||
* - 成交单数 = 区间内非取消业务订单 **条数**;按**业绩归属医助**映射到「中心」(与处方订单列表医助条件一致);无法归属计入「未归属中心」;合计=全表 COUNT。
|
||||
* - 成交单数 = 区间内非取消业务订单 **条数**;按**订单创建人**映射到「中心」;无法归属计入「未归属中心」;合计=全表 COUNT。
|
||||
* - 投放成本 = zyt_account_cost 在日期区间内 **amount** 合计(选定渠道时 `media_channel_code = channel_code` 过滤)。
|
||||
* · **各部门行投放成本** = 总投放成本 × (该部门进线 ÷ 各展示部门进线合计)。相当于先算 **单进线成本** = 总成本 ÷ 总进线,再乘部门进线。
|
||||
* · **二中心及其组织下级**不参与均摊投放成本(进线分摊分母对其计 0,与选定渠道下进线清零规则独立);其余部门按进线占比分摊。合计行成本 = 账务汇总总额。
|
||||
@@ -482,27 +482,6 @@ class YejiStatsLogic
|
||||
}
|
||||
$totalBooked = $narrowTotalsToTable ? $bookedSumInTable : array_sum($bookedByDept);
|
||||
|
||||
$listAlignedTotalPerf = null;
|
||||
$listAlignedDealCnt = null;
|
||||
if ($narrowTotalsToTable) {
|
||||
$deptForListAlign = $explicitDeptFilter && $primaryDeptIds !== [] ? $primaryDeptIds : null;
|
||||
$visForListAlign = $dataScopeRestricted && $dataScopeAdminIds !== null && $dataScopeAdminIds !== [] ? $dataScopeAdminIds : null;
|
||||
if ($deptForListAlign !== null || $visForListAlign !== null) {
|
||||
$listAlignedTotalPerf = self::sumOrderListAlignedPerformanceAmount(
|
||||
$startTs,
|
||||
$endTs,
|
||||
$deptForListAlign,
|
||||
$visForListAlign
|
||||
);
|
||||
$listAlignedDealCnt = self::countOrderListAlignedPerformanceOrders(
|
||||
$startTs,
|
||||
$endTs,
|
||||
$deptForListAlign,
|
||||
$visForListAlign
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($narrowTotalsToTable) {
|
||||
$totalConsult = $consultSumInTable;
|
||||
$unmappedDealOrder = 0;
|
||||
@@ -539,15 +518,20 @@ class YejiStatsLogic
|
||||
];
|
||||
}
|
||||
|
||||
if ($narrowTotalsToTable) {
|
||||
$totalPerf = $listAlignedTotalPerf !== null ? $listAlignedTotalPerf : $allocatedPerf;
|
||||
$totalChannelPerf = $allocatedChannelPerf;
|
||||
$totalDealOrderCount = $listAlignedDealCnt !== null ? $listAlignedDealCnt : $allocatedDealOrderSum;
|
||||
} else {
|
||||
$totalPerf = $globalPerfAll;
|
||||
$totalChannelPerf = $globalPerfChannel;
|
||||
$totalDealOrderCount = $globalDealOrderCount;
|
||||
// 底栏「合计业绩」「{渠道}业绩」「接诊诊单」= 当前表格全部数据行相加(含「未归属中心」),
|
||||
// 与各行展示口径一致;不再使用全表 SUM(会与仅含展示部门的摊分行脱钩)。
|
||||
$sumPerfRows = 0.0;
|
||||
$sumChannelPerfRows = 0.0;
|
||||
$sumDealRows = 0;
|
||||
foreach ($rows as $rw) {
|
||||
$sumPerfRows += (float) ($rw['performance_amount'] ?? 0);
|
||||
$sumChannelPerfRows += (float) ($rw['completed_performance_amount'] ?? 0);
|
||||
$sumDealRows += (int) ($rw['deal_order_count'] ?? 0);
|
||||
}
|
||||
$totalPerf = round($sumPerfRows, 1);
|
||||
$totalChannelPerf = round($sumChannelPerfRows, 1);
|
||||
$totalDealOrderCount = $sumDealRows;
|
||||
|
||||
$totalCost = round($globalCost, 1);
|
||||
|
||||
// ROI 合计:选渠道时底栏「{渠道}业绩合计」已扣除二中心子树($erCenterChannelStrip);未选渠道时用全表业绩并减去各二中心子树展示行之和
|
||||
@@ -566,12 +550,12 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
$scopedTableNote = ($explicitDeptFilter
|
||||
? ' 已筛选展示部门时,表格「合计业绩」「接诊诊单」各行为同一套列表部门口径(诊单医助或创建人∪子树、最深展示行);底栏合计业绩/成交单与处方订单列表 extend(所选部门树+数据域)一致,可能与各行相加不同。'
|
||||
? ' 已筛选展示部门时,表格仅展示所选树中的部门行;底栏「合计业绩」「接诊诊单」与当前表内各行业绩列相加一致(含「未归属中心」行若展示)。'
|
||||
: '')
|
||||
. ($dataScopeRestricted
|
||||
? ' 当前账号受数据范围限制时,底栏合计业绩与列表同域。'
|
||||
: '');
|
||||
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入。部门行「合计业绩」与「接诊诊单」均为列表 assistant_dept_id 口径(诊单医助或订单创建人人事部门∪子树,多行命中取最深展示行)。多表「本月」区间为当月1日至服务器当日。**投放成本**按进线占比分摊,**二中心子树展示部门进线在分摊中计 0**(与部门 ErCenter 子树一致)。';
|
||||
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入。部门行「合计业绩」「接诊诊单」「复诊」均按**订单创建人人事部门 ∈ 该展示行子树**归属(多行命中取最深展示行),与医助排行榜诊金一致;与处方订单列表 assistant_dept_id 默认筛选(诊单医助 ∪ 创建人)不同——侧栏点击会自动收窄为创建人口径。多表「本月」区间为当月1日至服务器当日。**投放成本**按进线占比分摊,**二中心子树展示部门进线在分摊中计 0**(与部门 ErCenter 子树一致)。';
|
||||
|
||||
return [
|
||||
'start_date' => $startDate,
|
||||
@@ -581,9 +565,9 @@ class YejiStatsLogic
|
||||
'channel_name' => $channelInfo ? (string) $channelInfo['channel_name'] : '',
|
||||
'channel_group' => $channelInfo ? (string) $channelInfo['source_group_name'] : '',
|
||||
'channel_filter_note' => $channelCode !== ''
|
||||
? ('{渠道}业绩:与「处方订单列表」业绩同口径,按订单 create_time 归日、仅剔除履约已取消(4)(NULL 与其它状态均计入);渠道判定用"该患者是否有挂号 channels 命中 dict + status=3"(不限挂号时点)。'
|
||||
. ' {渠道}成交单数:与{渠道}业绩同源,仅剔除已取消(4)的订单条数(非挂号条数)。'
|
||||
. ' 「已完成挂号」列同 admin 挂号已完成列表;「接诊诊单」列为业务订单条数(非取消),与列表按 assistant_dept_id 筛选时一致(诊单医助或创建人∪子树)。**二中心及其组织下级**(与部门模块 ErCenter 子树一致)**不计入**该渠道进线、业绩与成交单。**投放成本**按各部门进线条数占展示部门进线合计的比例分摊,**二中心子树进线在分摊中计 0**(未选渠道时仍显示进线数,仅不参与均摊)。ROI = 渠道业绩(不含二中心子树)÷ 总投放成本。进线按所选标签客户精确过滤。'
|
||||
? ('{渠道}业绩:按订单 create_time 归日、仅剔除履约已取消(4)(NULL 与其它状态均计入);渠道判定用"该患者是否有挂号 channels 命中 dict + status=3"(不限挂号时点)。部门归属按**订单创建人**人事部门 ∈ 子树。'
|
||||
. ' {渠道}成交单数:与{渠道}业绩同源,仅剔除已取消(4)的订单条数(非挂号条数),按订单创建人归部门。'
|
||||
. ' 「已完成挂号」列同 admin 挂号已完成列表;「接诊诊单」列为业务订单条数(非取消),同样按订单创建人归部门。**二中心及其组织下级**(与部门模块 ErCenter 子树一致)**不计入**该渠道进线、业绩与成交单。**投放成本**按各部门进线条数占展示部门进线合计的比例分摊,**二中心子树进线在分摊中计 0**(未选渠道时仍显示进线数,仅不参与均摊)。ROI = 渠道业绩(不含二中心子树)÷ 总投放成本。进线按所选标签客户精确过滤。'
|
||||
. ($tagFallback
|
||||
? ' ⚠ 当前业务尚未回填 tcm_diagnosis.external_userid 与 user_auth.unionid,已自动退化为「该标签客户的跟进医助 → 区间内接诊/业绩」口径,仅用于趋势观察。'
|
||||
: '')
|
||||
@@ -740,7 +724,8 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
$erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
|
||||
$revisitByAssistant = DeptLogic::getErCenterRevisitCountsByAssistant($startTs, $endTs);
|
||||
/** 医助排行榜复诊:按订单创建人归属(与诊金 / 接诊诊单同口径),不再用 dg.assistant_id */
|
||||
$revisitByAssistant = DeptLogic::getErCenterRevisitCountsByCreator($startTs, $endTs);
|
||||
|
||||
$assistantIdRows = Db::name('admin_role')->where('role_id', 2)->column('admin_id');
|
||||
$assistantSet = [];
|
||||
@@ -766,10 +751,10 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
$rangeNote = $channelFilterActive
|
||||
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩口径(与部门表该渠道业绩列一致)。被指派数与部门表同口径。接诊率 = 诊金 ÷ 进线(元/进线)。'
|
||||
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩中「订单创建人 = 该医助」的部分;与部门表该渠道业绩列同口径(按订单创建人归属)。被指派数与部门表同口径。接诊率 = 诊金 ÷ 进线(元/进线)。'
|
||||
. '「二中心」及其组织下级医助在该渠道下诊金、进线与被指派计 0。'
|
||||
. ' 预约诊单:预约表按「预约日期」落在区间内、有效医助口径同接诊,状态含已预约/已完成/已过号(不含已取消);科室下医助全量展示。')
|
||||
: '排行诊金 = 处方订单列表医助口径(诊单医助或代建创建人与列表一致)。被指派数为指派至该医助的次数。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。'
|
||||
: '排行诊金 = 业务订单中「订单创建人 = 该医助」的金额合计;与部门表「合计业绩」同口径(按订单创建人归属)。被指派数为指派至该医助的次数。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。'
|
||||
. ' 预约诊单:预约表按「预约日期」落在区间内、有效医助口径同接诊,状态含已预约/已完成/已过号(不含已取消);科室下医助全量展示。';
|
||||
|
||||
$leaderboards = [];
|
||||
@@ -2966,6 +2951,7 @@ class YejiStatsLogic
|
||||
|
||||
/**
|
||||
* 与 sumPerformance 对应,按 admin 维度返回全量/渠道业绩金额(不按部门折叠)。
|
||||
* 归属固定为 sqlPerformanceAttributionAdminExpr()——按订单创建人。
|
||||
*
|
||||
* @param int[] $appointmentChannelValues
|
||||
*
|
||||
@@ -3001,7 +2987,7 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 sumPerformanceByAdmin 对应:按诊单医助统计成交单数(非取消业务订单条数),全量/渠道分支一致。
|
||||
* 与 sumPerformanceByAdmin 对应:按订单创建人统计成交单数(非取消业务订单条数),全量/渠道分支一致。
|
||||
*
|
||||
* @param int[] $appointmentChannelValues
|
||||
*
|
||||
@@ -3091,7 +3077,7 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* dict(channels) 渠道成交单数按诊单医助聚合(与 aggregatePerformanceAmountsByAssistantChannel 同源)。
|
||||
* dict(channels) 渠道成交单数按业绩归属医助聚合(与 aggregatePerformanceAmountsByAssistantChannel 同源)。
|
||||
*
|
||||
* @param int[] $appointmentChannelValues
|
||||
*
|
||||
@@ -3146,7 +3132,7 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩按归属医助聚合到 admin(与处方订单列表医助筛选口径一致:诊单医助或代建创建人)。
|
||||
* 业绩按归属医助聚合到 admin(统一按订单创建人 o.creator_id;与 sqlPerformanceAttributionAdminExpr 一致)。
|
||||
*
|
||||
* @return array<int, float>
|
||||
*/
|
||||
@@ -3335,13 +3321,14 @@ class YejiStatsLogic
|
||||
|
||||
/**
|
||||
* 业绩归属医助 admin_id SQL 表达式(表别名固定为 o / dg / rx)。
|
||||
* 与 PrescriptionOrderLists 医助筛选一致:诊单 assistant_id 优先;否则订单创建人且非关联处方开方人。
|
||||
* 业绩看板(部门表 / 医助排行榜 / 渠道列 / ROI / 复诊 / 未归属中心)统一按订单创建人 o.creator_id 归属。
|
||||
* 即:诊金、接诊诊单、复诊、渠道业绩等所有金额/条数仅记给实际下单的医助;不再回退到诊单 assistant_id。
|
||||
* 处方订单列表侧栏(yeji_order_drawer=1)的「医助」/「部门」筛选与本归属对齐;其它管理端的 assistant_id /
|
||||
* assistant_dept_id 列表过滤保留旧口径(创建人 ∪ 诊单医助),不受本表达式影响。
|
||||
*/
|
||||
private static function sqlPerformanceAttributionAdminExpr(): string
|
||||
{
|
||||
return 'CASE WHEN dg.assistant_id > 0 THEN dg.assistant_id '
|
||||
. 'WHEN o.creator_id > 0 AND (rx.id IS NULL OR NOT (o.creator_id <=> rx.creator_id)) THEN o.creator_id '
|
||||
. 'ELSE 0 END';
|
||||
return 'o.creator_id';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3411,106 +3398,6 @@ class YejiStatsLogic
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
/**
|
||||
* 与处方订单列表 buildBaseQueryForStats(仅人事筛选维度)对齐的订单查询 WHERE。
|
||||
*
|
||||
* @param int[]|null $expandedDeptIds
|
||||
* @param int[]|null $visibleAdminIds
|
||||
*/
|
||||
private static function applyOrderListStatsVisibilityToPrescriptionQuery(
|
||||
$query,
|
||||
?array $expandedDeptIds,
|
||||
?array $visibleAdminIds
|
||||
): void {
|
||||
$hasDept = $expandedDeptIds !== null && $expandedDeptIds !== [];
|
||||
$hasVis = $visibleAdminIds !== null && $visibleAdminIds !== [];
|
||||
if (!$hasDept && !$hasVis) {
|
||||
return;
|
||||
}
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$adTable = self::tableWithPrefix('admin_dept');
|
||||
if ($hasDept) {
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $expandedDeptIds), static function (int $x): bool {
|
||||
return $x > 0;
|
||||
})));
|
||||
if ($ids === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
$inList = implode(',', $ids);
|
||||
$query->where(function ($q) use ($diagTable, $adTable, $inList) {
|
||||
$q->whereExists(
|
||||
"SELECT 1 FROM {$adTable} ad INNER JOIN {$diagTable} dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL "
|
||||
. "WHERE dg.id = o.diagnosis_id AND ad.dept_id IN ({$inList})"
|
||||
);
|
||||
$q->whereExists(
|
||||
"SELECT 1 FROM {$adTable} adc WHERE adc.admin_id = o.creator_id AND adc.dept_id IN ({$inList})",
|
||||
'OR'
|
||||
);
|
||||
});
|
||||
}
|
||||
if ($hasVis) {
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $visibleAdminIds), static function (int $x): bool {
|
||||
return $x > 0;
|
||||
})));
|
||||
if ($ids === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
$inList = implode(',', $ids);
|
||||
$query->where(function ($q) use ($diagTable, $inList, $ids) {
|
||||
$q->whereIn('o.creator_id', $ids);
|
||||
$q->whereOrRaw(
|
||||
"EXISTS (SELECT 1 FROM {$diagTable} dg WHERE dg.id = o.diagnosis_id "
|
||||
. "AND dg.delete_time IS NULL AND dg.assistant_id IN ({$inList}))"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收窄合计时与订单列表 extend「业绩金额」一致(create_time、NOT(fulfillment<=>4)、人事筛选)。
|
||||
*
|
||||
* @param int[]|null $expandedDeptIds
|
||||
* @param int[]|null $visibleAdminIds
|
||||
*/
|
||||
private static function sumOrderListAlignedPerformanceAmount(
|
||||
int $startTs,
|
||||
int $endTs,
|
||||
?array $expandedDeptIds,
|
||||
?array $visibleAdminIds
|
||||
): float {
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->whereNull('o.delete_time')
|
||||
->where('o.create_time', 'between', [$startTs, $endTs]);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||
self::applyOrderListStatsVisibilityToPrescriptionQuery($query, $expandedDeptIds, $visibleAdminIds);
|
||||
|
||||
return round((float) $query->sum('o.amount'), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 sumOrderListAlignedPerformanceAmount 同源,计订单条数。
|
||||
*/
|
||||
private static function countOrderListAlignedPerformanceOrders(
|
||||
int $startTs,
|
||||
int $endTs,
|
||||
?array $expandedDeptIds,
|
||||
?array $visibleAdminIds
|
||||
): int {
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->whereNull('o.delete_time')
|
||||
->where('o.create_time', 'between', [$startTs, $endTs]);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||
self::applyOrderListStatsVisibilityToPrescriptionQuery($query, $expandedDeptIds, $visibleAdminIds);
|
||||
|
||||
return (int) $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签/医助兜底口径下的渠道向业绩全量 SUM(与 sumPerformance 的 scoped 分支同一组 WHERE,无部门归属过滤)。
|
||||
*/
|
||||
@@ -3657,7 +3544,7 @@ class YejiStatsLogic
|
||||
if ($adminToPrimary === []) {
|
||||
return ['all' => [], 'scoped' => []];
|
||||
}
|
||||
// 1) 部门「合计业绩」:有展示行时与「接诊诊单」同列表部门口径(可由 overview 预计算避免重复查单)
|
||||
// 1) 部门「合计业绩」:有展示行时与「接诊诊单」同属 {@see aggregateListAlignedOrderMetricsByDept}(仅订单创建人摊行)
|
||||
if ($precomputedListAlignedAmountByDept !== null) {
|
||||
$allByDept = [];
|
||||
foreach ($precomputedListAlignedAmountByDept as $deptId => $v) {
|
||||
@@ -3749,7 +3636,7 @@ class YejiStatsLogic
|
||||
/**
|
||||
* 渠道业绩:与「处方订单列表」业绩完全同口径 —— 按 o.create_time 归日、仅剔除已取消(4)(含 NULL);
|
||||
* EXISTS 仅用于"该患者属于此渠道"的判定(有任一挂号 channels 命中 + status=3,**不限挂号时点**);
|
||||
* 部门归属按业绩归属医助(与处方列表医助条件一致:诊单医助或代建创建人)。
|
||||
* 部门归属按业绩归属表达式(订单创建人 o.creator_id)。
|
||||
*/
|
||||
private static function aggregatePerformanceByAppointmentChannels(
|
||||
int $startTs,
|
||||
@@ -3937,8 +3824,8 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 {@see PrescriptionOrderLists::applyDoctorAssistantFilters} 中 assistant_dept_id(未开抽屉对齐开关)一致:
|
||||
* 诊单医助或订单创建人的人事部门落在该行子树即命中;命中多行时取组织深度最大的展示行。
|
||||
* 与业绩看板部门列一致:仅按订单创建人候选,将其人事部门落在展示行子树即命中;命中多行时取组织深度最大的展示行。
|
||||
* (处方订单列表默认 assistant_dept_id 仍为「诊单医助 ∪ 创建人」并集,与看板有别。)
|
||||
*
|
||||
* @param array<int, true> $candidates admin_id => true
|
||||
* @param array<int, int[]> $adminDeptIdList
|
||||
@@ -4012,7 +3899,8 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表部门口径(诊单医助 ∪ 创建人 ⇄ 展示行子树,最深行):同时聚单笔数与金额,供「接诊诊单」与「合计业绩」列共用。
|
||||
* 看板「合计业绩 / 接诊诊单」列用的部门摊行:仅按订单创建人 o.creator_id 的人事部门映射到展示行子树(命中多行取最深),
|
||||
* 与侧栏 yeji_order_drawer=1、{@see sqlPerformanceAttributionAdminExpr} 一致。不再把诊单医助纳入归属候选。
|
||||
*
|
||||
* @return array{count: array<int, int>, amount: array<int, float>}
|
||||
*/
|
||||
@@ -4038,7 +3926,6 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$att = self::sqlPerformanceAttributionAdminExpr();
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->join("{$diagTable} dg", 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
||||
@@ -4047,21 +3934,20 @@ class YejiStatsLogic
|
||||
->where('o.diagnosis_id', '>', 0);
|
||||
self::joinPrescriptionForPerformanceAttribution($query);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||
$query->whereRaw('(dg.assistant_id > 0 OR o.creator_id > 0)');
|
||||
$query->where('o.creator_id', '>', 0);
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('o.diagnosis_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$ids = array_map('intval', array_keys($tagAssistantIds));
|
||||
$in = implode(',', $ids);
|
||||
$query->whereRaw("(({$att}) IN ({$in}) OR dg.assistant_id IN ({$in}) OR o.creator_id IN ({$in}))");
|
||||
$query->whereRaw("o.creator_id IN ({$in})");
|
||||
}
|
||||
if ($channelScopedExistsSql !== null && $channelScopedExistsSql !== '') {
|
||||
$query->whereRaw($channelScopedExistsSql, $channelScopedExistsBindings);
|
||||
}
|
||||
|
||||
$orderRows = $query->field([
|
||||
Db::raw('dg.assistant_id AS diag_assistant_id'),
|
||||
'o.creator_id',
|
||||
'o.amount',
|
||||
])->select()->toArray();
|
||||
@@ -4081,11 +3967,7 @@ class YejiStatsLogic
|
||||
|
||||
$adminIds = [];
|
||||
foreach ($orderRows as $r) {
|
||||
$a = (int) ($r['diag_assistant_id'] ?? 0);
|
||||
$c = (int) ($r['creator_id'] ?? 0);
|
||||
if ($a > 0) {
|
||||
$adminIds[] = $a;
|
||||
}
|
||||
if ($c > 0) {
|
||||
$adminIds[] = $c;
|
||||
}
|
||||
@@ -4107,15 +3989,11 @@ class YejiStatsLogic
|
||||
$byCnt = [];
|
||||
$byAmt = [];
|
||||
foreach ($orderRows as $r) {
|
||||
$asst = (int) ($r['diag_assistant_id'] ?? 0);
|
||||
$cr = (int) ($r['creator_id'] ?? 0);
|
||||
$cand = [];
|
||||
if ($asst > 0) {
|
||||
$cand[$asst] = true;
|
||||
}
|
||||
if ($cr > 0) {
|
||||
$cand[$cr] = true;
|
||||
if ($cr <= 0) {
|
||||
continue;
|
||||
}
|
||||
$cand = [$cr => true];
|
||||
$deptKey = self::resolveDeepestYejiTableRowForListDeptOr(
|
||||
$cand,
|
||||
$adminDeptIdList,
|
||||
@@ -4136,9 +4014,9 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 成交单数按部门:**处方订单列表**按 assistant_dept_id 筛选时的业务单条数(诊单医助或创建人 ∪ 部门子树),
|
||||
* 与 {@see PrescriptionOrderLists::applyDoctorAssistantFilters} 在 <b>未</b> 传 yeji_drawer_match_table_performance 时一致。
|
||||
* 选定医助 tag 时收窄为(业绩归属医助 ∈ 集 **或** 诊单医助 ∈ 集 **或** 创建人 ∈ 集)。
|
||||
* 成交单数按部门:有展示行时与 {@see aggregateListAlignedOrderMetricsByDept} 同口径(仅订单创建人归属)。
|
||||
* 无展示行时按 sqlPerformanceAttributionAdminExpr 分组再 fold(同样为创建人)。
|
||||
* 选定医助 tag 时收窄为 o.creator_id ∈ 集。
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
@@ -4179,14 +4057,14 @@ class YejiStatsLogic
|
||||
->where('o.diagnosis_id', '>', 0);
|
||||
self::joinPrescriptionForPerformanceAttribution($query);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||
$query->whereRaw('(dg.assistant_id > 0 OR o.creator_id > 0)');
|
||||
$query->where('o.creator_id', '>', 0);
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('o.diagnosis_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$ids = array_map('intval', array_keys($tagAssistantIds));
|
||||
$in = implode(',', $ids);
|
||||
$query->whereRaw("(({$att}) IN ({$in}) OR dg.assistant_id IN ({$in}) OR o.creator_id IN ({$in}))");
|
||||
$query->whereRaw("o.creator_id IN ({$in})");
|
||||
}
|
||||
|
||||
$query->whereRaw("({$att}) > 0", []);
|
||||
@@ -4446,7 +4324,7 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 「未归属中心」业绩下钻:按诊单医助汇总无法映射到展示「中心」的金额(与 aggregatePerformance 部门行互补)。
|
||||
* 「未归属中心」业绩下钻:按订单创建人汇总无法映射到展示「中心」的金额(与 aggregatePerformance 部门行互补)。
|
||||
*
|
||||
* @return array{
|
||||
* start_date: string,
|
||||
@@ -4533,7 +4411,7 @@ class YejiStatsLogic
|
||||
foreach ($rows as &$row) {
|
||||
$aid = (int) $row['admin_id'];
|
||||
if ($aid <= 0) {
|
||||
$row['name'] = '(无诊单或医助未填)';
|
||||
$row['name'] = '(无订单或创建人未填)';
|
||||
} else {
|
||||
$row['name'] = (string) ($names[$aid] ?? ('#' . $aid));
|
||||
}
|
||||
@@ -4545,7 +4423,7 @@ class YejiStatsLogic
|
||||
'end_date' => (string) $c['endDate'],
|
||||
'channel_code' => (string) $c['channelCode'],
|
||||
'rows' => $rows,
|
||||
'note' => '与看板「未归属中心」补差一致:按订单 create_time、剔除履约已取消(4);诊单医助无法映射到任一展示「中心」计入各行;其余(无诊单/医助为空)汇总在首行。账号数据权限下仅列出可见医助。',
|
||||
'note' => '与看板「未归属中心」补差一致:按订单 create_time、剔除履约已取消(4);**订单创建人**人事部门无法映射到任一展示「中心」计入各行;其余(无诊单/创建人为空)汇总在首行。账号数据权限下仅列出可见医助。',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5164,6 +5042,96 @@ class YejiStatsLogic
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂号渠道分组:根据诊单 id 列表,反查每个诊单对应的「挂号渠道值」(doctor_appointment.channels,
|
||||
* 与 dict_data(type_value=channels).value 对齐)。一个诊单理论上 1:1 关联一条挂号,多挂号场景取 id 最大者。
|
||||
*
|
||||
* @param int[] $diagnosisIds
|
||||
*
|
||||
* @return array<int, int> diagnosis_id => channels (>0)
|
||||
*/
|
||||
private static function commissionSettlementDiagnosisLatestApptChannel(array $diagnosisIds): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $diagnosisIds), static fn (int $x): bool => $x > 0)));
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Db::name('doctor_appointment')
|
||||
->whereIn('patient_id', $ids)
|
||||
->where('status', 3)
|
||||
->where('channels', '>', 0)
|
||||
->order('patient_id asc, id desc')
|
||||
->field(['patient_id', 'channels'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$dg = (int) ($r['patient_id'] ?? 0);
|
||||
if ($dg <= 0 || isset($out[$dg])) {
|
||||
continue;
|
||||
}
|
||||
$ch = (int) ($r['channels'] ?? 0);
|
||||
if ($ch > 0) {
|
||||
$out[$dg] = $ch;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂号字典 channels:value(int) => name(string)。仅取本次涉及到的 value 集合,减少 dict 全量读取。
|
||||
*
|
||||
* @param int[] $values
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private static function commissionSettlementApptChannelsDictMap(array $values): array
|
||||
{
|
||||
$values = array_values(array_unique(array_filter(array_map('intval', $values), static fn (int $x): bool => $x > 0)));
|
||||
if ($values === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Db::name('dict_data')
|
||||
->where('type_value', 'channels')
|
||||
->whereIn('value', array_map('strval', $values))
|
||||
->field(['value', 'name'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$v = (int) ($r['value'] ?? 0);
|
||||
$n = trim((string) ($r['name'] ?? ''));
|
||||
if ($v > 0 && $n !== '') {
|
||||
$out[$v] = $n;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道值 → 展示标签。未关联 / 字典缺失时统一标签为「未匹配挂号渠道」。
|
||||
*
|
||||
* @param array<int, string> $dictMap
|
||||
*/
|
||||
private static function commissionSettlementApptChannelLabel(int $channelValue, array $dictMap): string
|
||||
{
|
||||
if ($channelValue <= 0) {
|
||||
return '未匹配挂号渠道';
|
||||
}
|
||||
$name = trim((string) ($dictMap[$channelValue] ?? ''));
|
||||
if ($name === '') {
|
||||
return '未匹配挂号渠道·#' . $channelValue;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 签收 / 尾款时间:对物流与支付关联做 GROUP BY 后 LEFT JOIN,避免 SELECT 中逐行相关子查询导致 overview 超时。
|
||||
*/
|
||||
@@ -5273,19 +5241,21 @@ class YejiStatsLogic
|
||||
|
||||
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||
return [
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => '订单创建月为结算月的上一自然月;统计系统代开处方对应的已完成业务订单;签收与尾款均在结算月 7 日 24 点前完成的金额计入本期提成,否则计入顺延下期。上期「确定业绩」写入的顺延订单会在本期并入统计。',
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => 0,
|
||||
'confirm' => $confirm,
|
||||
'rows' => [],
|
||||
'assistant_rows' => [],
|
||||
'doctor_rows' => [],
|
||||
'total' => $emptyTotal,
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => '订单创建月为结算月的上一自然月;统计系统代开处方对应的已完成业务订单;签收与尾款均在结算月 7 日 24 点前完成的金额计入本期提成,否则计入顺延下期。上期「确定业绩」写入的顺延订单会在本期并入统计。',
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => 0,
|
||||
'confirm' => $confirm,
|
||||
'rows' => [],
|
||||
'assistant_rows' => [],
|
||||
'doctor_rows' => [],
|
||||
'assistant_channel_rows' => [],
|
||||
'doctor_channel_rows' => [],
|
||||
'total' => $emptyTotal,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5303,6 +5273,16 @@ class YejiStatsLogic
|
||||
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
||||
], $signPayFields))->select()->toArray();
|
||||
|
||||
$diagIdSet = [];
|
||||
foreach ($rowsRaw as $r) {
|
||||
$dg = (int) ($r['diagnosis_id'] ?? 0);
|
||||
if ($dg > 0) {
|
||||
$diagIdSet[$dg] = true;
|
||||
}
|
||||
}
|
||||
$diagToApptChannel = self::commissionSettlementDiagnosisLatestApptChannel(array_keys($diagIdSet));
|
||||
$apptChannelsDictMap = self::commissionSettlementApptChannelsDictMap(array_values(array_unique(array_map('intval', $diagToApptChannel))));
|
||||
|
||||
$curAgg = [];
|
||||
$defAgg = [];
|
||||
$totAgg = [];
|
||||
@@ -5319,6 +5299,22 @@ class YejiStatsLogic
|
||||
$cntDoctorCur = [];
|
||||
$cntDoctorDef = [];
|
||||
|
||||
/** 按 (业绩归属医助 × 挂号渠道值) / (开方医生 × 挂号渠道值) 拆分聚合,渠道值取该订单对应诊单的最新已完成挂号 channels */
|
||||
$assistChTot = [];
|
||||
$assistChCur = [];
|
||||
$assistChDef = [];
|
||||
$assistChCnt = [];
|
||||
$assistChCurCnt = [];
|
||||
$assistChDefCnt = [];
|
||||
$assistChCarry = [];
|
||||
$doctorChTot = [];
|
||||
$doctorChCur = [];
|
||||
$doctorChDef = [];
|
||||
$doctorChCnt = [];
|
||||
$doctorChCurCnt = [];
|
||||
$doctorChDefCnt = [];
|
||||
$doctorChCarry = [];
|
||||
|
||||
foreach ($rowsRaw as $r) {
|
||||
$oid = (int) ($r['id'] ?? 0);
|
||||
$isCarry = $oid > 0 && isset($carryFlip[$oid]);
|
||||
@@ -5331,6 +5327,8 @@ class YejiStatsLogic
|
||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||
$amt = round((float) ($r['amount'] ?? 0), 2);
|
||||
$dgId = (int) ($r['diagnosis_id'] ?? 0);
|
||||
$apCh = (int) ($diagToApptChannel[$dgId] ?? 0);
|
||||
|
||||
if ($aid <= 0 || $amt < 0) {
|
||||
continue;
|
||||
@@ -5362,6 +5360,32 @@ class YejiStatsLogic
|
||||
$defByDoctor[$doctorId] = ($defByDoctor[$doctorId] ?? 0.0) + $amt;
|
||||
$cntDoctorDef[$doctorId] = ($cntDoctorDef[$doctorId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
$assistChTot[$aid][$apCh] = ($assistChTot[$aid][$apCh] ?? 0.0) + $amt;
|
||||
$assistChCnt[$aid][$apCh] = ($assistChCnt[$aid][$apCh] ?? 0) + 1;
|
||||
if ($eligible) {
|
||||
$assistChCur[$aid][$apCh] = ($assistChCur[$aid][$apCh] ?? 0.0) + $amt;
|
||||
$assistChCurCnt[$aid][$apCh] = ($assistChCurCnt[$aid][$apCh] ?? 0) + 1;
|
||||
} else {
|
||||
$assistChDef[$aid][$apCh] = ($assistChDef[$aid][$apCh] ?? 0.0) + $amt;
|
||||
$assistChDefCnt[$aid][$apCh] = ($assistChDefCnt[$aid][$apCh] ?? 0) + 1;
|
||||
}
|
||||
if ($isCarry) {
|
||||
$assistChCarry[$aid][$apCh] = ($assistChCarry[$aid][$apCh] ?? 0) + 1;
|
||||
}
|
||||
|
||||
$doctorChTot[$doctorId][$apCh] = ($doctorChTot[$doctorId][$apCh] ?? 0.0) + $amt;
|
||||
$doctorChCnt[$doctorId][$apCh] = ($doctorChCnt[$doctorId][$apCh] ?? 0) + 1;
|
||||
if ($eligible) {
|
||||
$doctorChCur[$doctorId][$apCh] = ($doctorChCur[$doctorId][$apCh] ?? 0.0) + $amt;
|
||||
$doctorChCurCnt[$doctorId][$apCh] = ($doctorChCurCnt[$doctorId][$apCh] ?? 0) + 1;
|
||||
} else {
|
||||
$doctorChDef[$doctorId][$apCh] = ($doctorChDef[$doctorId][$apCh] ?? 0.0) + $amt;
|
||||
$doctorChDefCnt[$doctorId][$apCh] = ($doctorChDefCnt[$doctorId][$apCh] ?? 0) + 1;
|
||||
}
|
||||
if ($isCarry) {
|
||||
$doctorChCarry[$doctorId][$apCh] = ($doctorChCarry[$doctorId][$apCh] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
$foldAmt = static function (array $byAssistant): array {
|
||||
@@ -5503,20 +5527,108 @@ class YejiStatsLogic
|
||||
return $cmp !== 0 ? $cmp : (($b['doctor_id'] ?? 0) <=> ($a['doctor_id'] ?? 0));
|
||||
});
|
||||
|
||||
/** 按 (业绩归属医助 × 挂号渠道) 拆分行:复诊2 / 复诊3 / 自媒体1 等每个 channels 值各自独立一行 */
|
||||
$assistantChannelRows = [];
|
||||
foreach ($assistChTot as $aid => $byCh) {
|
||||
$aid = (int) $aid;
|
||||
if ($aid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$pid = (int) ($adminToPrimary[$aid] ?? 0);
|
||||
foreach ($byCh as $ch => $amt) {
|
||||
$ch = (int) $ch;
|
||||
$assistantChannelRows[] = [
|
||||
'assistant_id' => $aid,
|
||||
'assistant_name' => (string) ($assistNameMap[$aid] ?? ('#' . $aid)),
|
||||
'primary_dept_id' => $pid,
|
||||
'primary_dept_name' => $pid > 0 ? self::formatYejiDeptRowDisplayName($pid, $deptById) : '',
|
||||
'appt_channel_value' => $ch,
|
||||
'appt_channel_name' => self::commissionSettlementApptChannelLabel($ch, $apptChannelsDictMap),
|
||||
'carry_in_order_count' => (int) ($assistChCarry[$aid][$ch] ?? 0),
|
||||
'completed_order_count' => (int) ($assistChCnt[$aid][$ch] ?? 0),
|
||||
'completed_order_amount' => round((float) $amt, 2),
|
||||
'current_period_count' => (int) ($assistChCurCnt[$aid][$ch] ?? 0),
|
||||
'current_period_amount' => round((float) ($assistChCur[$aid][$ch] ?? 0), 2),
|
||||
'deferred_period_count' => (int) ($assistChDefCnt[$aid][$ch] ?? 0),
|
||||
'deferred_period_amount' => round((float) ($assistChDef[$aid][$ch] ?? 0), 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
/** 同医助按总业绩 desc 排序,组内按当前行金额 desc;保证同医助行连续,方便前端合并单元格 */
|
||||
usort($assistantChannelRows, static function (array $a, array $b) use ($totAgg): int {
|
||||
$aa = (int) ($a['assistant_id'] ?? 0);
|
||||
$bb = (int) ($b['assistant_id'] ?? 0);
|
||||
if ($aa !== $bb) {
|
||||
$aTot = (float) ($totAgg[$aa] ?? 0);
|
||||
$bTot = (float) ($totAgg[$bb] ?? 0);
|
||||
if ($aTot !== $bTot) {
|
||||
return $bTot <=> $aTot;
|
||||
}
|
||||
return $bb <=> $aa;
|
||||
}
|
||||
return ($b['completed_order_amount'] ?? 0) <=> ($a['completed_order_amount'] ?? 0);
|
||||
});
|
||||
|
||||
/** 按 (开方医生 × 挂号渠道) 拆分行;未关联开方人(0) 行也保留,沉到末尾 */
|
||||
$doctorChannelRows = [];
|
||||
foreach ($doctorChTot as $did => $byCh) {
|
||||
$did = (int) $did;
|
||||
$pid = $did > 0 ? (int) ($adminToPrimary[$did] ?? 0) : 0;
|
||||
foreach ($byCh as $ch => $amt) {
|
||||
$ch = (int) $ch;
|
||||
$doctorChannelRows[] = [
|
||||
'doctor_id' => $did,
|
||||
'doctor_name' => $did > 0 ? (string) ($doctorNameMap[$did] ?? ('#' . $did)) : '未关联开方人',
|
||||
'primary_dept_id' => $pid,
|
||||
'primary_dept_name' => $pid > 0 ? self::formatYejiDeptRowDisplayName($pid, $deptById) : '',
|
||||
'appt_channel_value' => $ch,
|
||||
'appt_channel_name' => self::commissionSettlementApptChannelLabel($ch, $apptChannelsDictMap),
|
||||
'carry_in_order_count' => (int) ($doctorChCarry[$did][$ch] ?? 0),
|
||||
'completed_order_count' => (int) ($doctorChCnt[$did][$ch] ?? 0),
|
||||
'completed_order_amount' => round((float) $amt, 2),
|
||||
'current_period_count' => (int) ($doctorChCurCnt[$did][$ch] ?? 0),
|
||||
'current_period_amount' => round((float) ($doctorChCur[$did][$ch] ?? 0), 2),
|
||||
'deferred_period_count' => (int) ($doctorChDefCnt[$did][$ch] ?? 0),
|
||||
'deferred_period_amount' => round((float) ($doctorChDef[$did][$ch] ?? 0), 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
usort($doctorChannelRows, static function (array $a, array $b) use ($totByDoctor): int {
|
||||
$aa = (int) ($a['doctor_id'] ?? 0);
|
||||
$bb = (int) ($b['doctor_id'] ?? 0);
|
||||
/** 「未关联开方人」(doctor_id=0) 一律置末尾 */
|
||||
$za = $aa === 0 ? 1 : 0;
|
||||
$zb = $bb === 0 ? 1 : 0;
|
||||
if ($za !== $zb) {
|
||||
return $za <=> $zb;
|
||||
}
|
||||
if ($aa !== $bb) {
|
||||
$aTot = (float) ($totByDoctor[$aa] ?? 0);
|
||||
$bTot = (float) ($totByDoctor[$bb] ?? 0);
|
||||
if ($aTot !== $bTot) {
|
||||
return $bTot <=> $aTot;
|
||||
}
|
||||
return $bb <=> $aa;
|
||||
}
|
||||
return ($b['completed_order_amount'] ?? 0) <=> ($a['completed_order_amount'] ?? 0);
|
||||
});
|
||||
|
||||
return [
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => '订单创建月为结算月的上一自然月;仅统计系统代开处方(is_system_auto=1)对应的履约已完成业务订单。签收时间取物流表签收时间最大值;尾款时间取关联支付单已支付记录(优先类型为尾款/全部)。签收与尾款均不晚于结算月 7 日 24 点的计入「本期提成」,其余已完成订单计入「顺延下期」。上期同渠道同部门范围内「确定业绩」产生的顺延订单,会计入本期订单池。「按医助下发」按业绩归属医助聚合;「按医生下发」按处方开方人(creator_id)聚合。',
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => $carryInCnt,
|
||||
'confirm' => $confirm,
|
||||
'rows' => $rows,
|
||||
'assistant_rows' => $assistantRows,
|
||||
'doctor_rows' => $doctorRows,
|
||||
'total' => [
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => '订单创建月为结算月的上一自然月;仅统计系统代开处方(is_system_auto=1)对应的履约已完成业务订单。签收时间取物流表签收时间最大值;尾款时间取关联支付单已支付记录(优先类型为尾款/全部)。签收与尾款均不晚于结算月 7 日 24 点的计入「本期提成」,其余已完成订单计入「顺延下期」。上期同渠道同部门范围内「确定业绩」产生的顺延订单,会计入本期订单池。「按医助下发」「按医生下发」按 (人 × 挂号渠道) 拆分;挂号渠道取该订单对应诊单的最新已完成挂号 doctor_appointment.channels(dict_data type_value=channels),如「复诊2」「自媒体1」等保持各自独立一行,未关联挂号统一展示为「未匹配挂号渠道」。',
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => $carryInCnt,
|
||||
'confirm' => $confirm,
|
||||
'rows' => $rows,
|
||||
'assistant_rows' => $assistantRows,
|
||||
'doctor_rows' => $doctorRows,
|
||||
'assistant_channel_rows' => $assistantChannelRows,
|
||||
'doctor_channel_rows' => $doctorChannelRows,
|
||||
'total' => [
|
||||
'completed_order_count' => $sumCompletedCnt,
|
||||
'completed_order_amount' => round($sumCompletedAmt, 2),
|
||||
'current_period_count' => $sumCurCnt,
|
||||
|
||||
Reference in New Issue
Block a user