This commit is contained in:
gr
2026-09-24 11:48:20 +08:00
parent 68fc86a167
commit b7e91f5fe3
5 changed files with 240 additions and 67 deletions
+134 -53
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\mcp\service;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
use app\common\service\DataScope\DataScopeService;
use think\facade\Db;
@@ -26,36 +27,39 @@ class PerfTools
public const DEPTS = 'stats.yejiStats/overview';
/** 指标 => [后台列名, 单位];名称与后台“业绩看板”页面列名一致,方便对账 */
/**
* 指标 => [名称, 单位, 后台列名, 口径]。名称写清楚数的是什么;后台列名留作对账用——后台“接诊单数”其实是
* 面诊完成的挂号人次、“接诊诊单”才是订单数,直接沿用会让人把 96 人次面诊当成 96 单(线上真实出现过)。
*/
private const ASSISTANT_METRICS = [
'fee_amount' => ['诊金', '元'],
'deal_order_count' => ['接诊诊单', '单'],
'consult_count' => ['接诊单数', '单'],
'appointment_count' => ['预约诊单', '个'],
'assign_count' => ['被指派数', '个'],
'lead_count' => ['进线', '个'],
'consult_rate' => ['接诊率', '元/进线'],
'fee_amount' => ['诊金', '元', '诊金', '本人创建的业务订单金额,按订单创建时间,不含已取消/拒收/退款;与处方业务订单列表按医助筛选时顶部“医助业绩(业务订单额)”一致'],
'deal_order_count' => ['成交订单数', '单', '接诊诊单', '上述订单的笔数;问“多少单/订单数”用这个。处方业务订单列表按医助筛选时显示全部状态,比这里多出已取消/拒收/退款的订单'],
'consult_count' => ['面诊完成数', '人次', '接诊单数', '预约日期在区间内、状态为已完成的挂号人次(医助取挂号创建人,没有时取诊单医助);是面诊人次,不是订单数'],
'appointment_count' => ['预约数', '个', '预约诊单', '预约日期在区间内的挂号数(已预约/已完成/已过号,不含已取消)'],
'assign_count' => ['被指派数', '次', '被指派数', '区间内指派给该医助的次数(不含勾选“继承”的指派)'],
'lead_count' => ['进线数', '个', '进线', '企业微信添加客户事件数(接待人为该医助)'],
'consult_rate' => ['每进线诊金', '元/进线', '接诊率', '诊金 ÷ 进线数'],
];
private const DOCTOR_METRICS = [
'deal_amount' => ['成交金额', '元'],
'deal_order_count' => ['接诊诊单', '单'],
'avg_deal_amount' => ['客单价', '元'],
'appointment_total' => ['总挂号', '个'],
'appointment_completed' => ['挂号完成', '个'],
'appointment_conversion_rate' => ['挂号率', '%'],
'system_prescription_count' => ['系统开方', '张'],
'manual_prescription_count' => ['手动开方', '张'],
'deal_amount' => ['成交金额', '元', '成交金额', '开方医生为本人的业务订单金额,按订单创建时间,不含已取消/拒收/退款及发生过退款的订单'],
'deal_order_count' => ['成交订单数', '单', '接诊诊单', '上述订单的笔数;问“多少单/订单数”用这个'],
'avg_deal_amount' => ['客单价', '元', '客单价', '成交金额 ÷ 成交订单数'],
'appointment_total' => ['挂号总数', '个', '总挂号', '预约日期在区间内的全部挂号(含已预约/已取消/已完成/已过号)'],
'appointment_completed' => ['面诊完成数', '人次', '挂号完成', '预约日期在区间内、已完成的挂号人次'],
'appointment_conversion_rate' => ['挂号成交率', '%', '挂号率', '成交订单数 ÷ 挂号总数 × 100'],
'system_prescription_count' => ['系统开方数', '张', '系统开方', '处方日期在区间内的系统代开处方数'],
'manual_prescription_count' => ['手动开方数', '张', '手动开方', '处方日期在区间内的手动开方数'],
];
private const DEPT_METRICS = [
'performance_amount' => ['合计业绩', '元'],
'deal_order_count' => ['接诊诊单', '单'],
'consult_count' => ['接诊单数', '单'],
'appointment_booked_count' => ['预约诊单', '个'],
'lead_count' => ['进线数据', '个'],
'assign_count' => ['被指派数', '个'],
'cost_amount' => ['投放成本', '元'],
'performance_amount' => ['合计业绩', '元', '合计业绩', '订单创建人属于该部门(含下级)的业务订单金额,按订单创建时间,不含已取消/拒收/退款'],
'deal_order_count' => ['成交订单数', '单', '接诊诊单', '上述订单的笔数'],
'consult_count' => ['面诊完成数', '人次', '接诊单数', '预约日期在区间内、已完成的挂号人次;是面诊人次,不是订单数'],
'appointment_booked_count' => ['预约数', '个', '预约诊单', '预约日期在区间内的挂号数(不含已取消)'],
'lead_count' => ['进线数', '个', '进线数据', '企业微信添加客户事件数(按接待人所在部门)'],
'assign_count' => ['被指派数', '次', '被指派数', '区间内指派给部门内医助的次数(不含继承指派)'],
'cost_amount' => ['投放成本', '元', '投放成本', '按进线占比分摊的投放成本'],
];
private const PERIODS = [
@@ -82,11 +86,12 @@ class PerfTools
$channel = ['type' => 'string', 'description' => '渠道编码(可选,取值见 stats.yejiStats/channelOptions)'];
$top = ['type' => 'integer', 'minimum' => 1, 'maximum' => 50, 'description' => '返回前几名,默认 15'];
$chartNote = '结果里的 ```chart 代码块请原样放进回答,行知会显示为统计图。';
$countNote = '注意:订单数看“成交订单数”(后台列名“接诊诊单”);“面诊完成数”(后台列名“接诊单数”)是完成的挂号人次,不是订单数,回答时不要写成“X 单”。';
$tools = [];
if ($canAssistants) {
$tools[] = Tools::tool('zyt_perf_assistants',
'医助业绩排行(与后台“业绩看板·医助排行榜”同口径):一次返回全部可见医助的诊金、接诊诊单、接诊单数、预约诊单、被指派数、进线、接诊率,含排名、合计和部门小计。'
. '问“医助业绩/排行/谁业绩最好/某位医助本月业绩”时直接用它,不要逐人、逐天调用明细接口。' . $chartNote, [
'医助业绩排行(与后台“业绩看板·医助排行榜”同口径):一次返回全部可见医助的诊金、成交订单数、面诊完成数、预约数、被指派数、进线数、每进线诊金,含排名、合计、部门小计和每个指标的口径。'
. '问“医助业绩/排行/谁业绩最好/某位医助本月业绩或订单数”时直接用它,不要逐人、逐天调用明细接口;只看一个人时还会给出处方业务订单列表的对账数字。' . $countNote . $chartNote, [
'period' => $period, 'start_date' => $date, 'end_date' => $date,
'sort_by' => ['type' => 'string', 'enum' => array_keys(self::ASSISTANT_METRICS), 'description' => '排名依据:' . self::metricText(self::ASSISTANT_METRICS) . ';默认 fee_amount'],
'top' => $top,
@@ -97,8 +102,8 @@ class PerfTools
}
if ($canDoctors) {
$tools[] = Tools::tool('zyt_perf_doctors',
'医生业绩排行(与后台“业绩看板·医生统计”同口径):一次返回可见医生的成交金额、接诊诊单、客单价、总挂号、挂号完成/过号/取消、挂号率、系统/手动开方数,含排名和合计。'
. '问“医生业绩/哪个医生成交最多/某医生本月业绩”时直接用它。' . $chartNote, [
'医生业绩排行(与后台“业绩看板·医生统计”同口径):一次返回可见医生的成交金额、成交订单数、客单价、挂号总数、面诊完成/过号/取消、挂号成交率、系统/手动开方数,含排名、合计和口径。'
. '问“医生业绩/哪个医生成交最多/某医生本月业绩”时直接用它。订单数看“成交订单数”(后台列名“接诊诊单”)。' . $chartNote, [
'period' => $period, 'start_date' => $date, 'end_date' => $date,
'sort_by' => ['type' => 'string', 'enum' => array_keys(self::DOCTOR_METRICS), 'description' => '排名依据:' . self::metricText(self::DOCTOR_METRICS) . ';默认 deal_amount'],
'top' => $top,
@@ -110,7 +115,7 @@ class PerfTools
}
if (self::usable($identity, self::DEPTS)) {
$tools[] = Tools::tool('zyt_stats_performance',
'部门业绩看板(与后台“业绩看板·甄养堂诊金”同口径):一段时间内各部门的合计业绩、接诊诊单、接诊单数、预约诊单、进线、被指派、投放成本、ROI 及合计。问“各部门/各中心业绩”时用它。' . $chartNote, [
'部门业绩看板(与后台“业绩看板·甄养堂诊金”同口径):一段时间内各部门的合计业绩、成交订单数、面诊完成数、预约数、进线数、被指派数、投放成本、ROI 及合计。问“各部门/各中心业绩”时用它。' . $countNote . $chartNote, [
'period' => $period, 'start_date' => $date, 'end_date' => $date,
'sort_by' => ['type' => 'string', 'enum' => array_keys(self::DEPT_METRICS), 'description' => '图表按哪个指标画:' . self::metricText(self::DEPT_METRICS) . ';默认 performance_amount'],
'dept' => $dept,
@@ -121,14 +126,14 @@ class PerfTools
if ($canAssistants || $canDoctors) {
$by = array_keys(array_filter(['assistant' => $canAssistants, 'doctor' => $canDoctors]));
$tools[] = Tools::tool('zyt_perf_trend',
'业绩走势:按日/周/月给出业绩金额和接诊诊单数的变化,可对比最多 4 位医助或医生。问“本月每天业绩走势”“张三和李四这个月业绩对比”时用它,一次调用即可,不要逐天查询。'
'业绩走势:按日/周/月给出业绩金额和成交订单数的变化,可对比最多 4 位医助或医生。问“本月每天业绩走势”“张三和李四这个月业绩对比”时用它,一次调用即可,不要逐天查询。'
. '医助口径同医助排行榜诊金(订单创建人),医生口径同医生统计成交金额(开方医生)。' . $chartNote, [
'period' => $period, 'start_date' => $date, 'end_date' => $date,
'by' => ['type' => 'string', 'enum' => $by, 'description' => 'assistant 按医助(默认)/ doctor 按医生'],
'names' => ['type' => 'string', 'description' => '要对比的人的姓名,多个用逗号分隔,最多 4 个(可选;不传=可见范围内全部合计)'],
'ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'maxItems' => 4, 'description' => '要对比的人的后台账号ID(可选,与 names 二选一)'],
'granularity' => ['type' => 'string', 'enum' => ['auto', 'day', 'week', 'month'], 'description' => '时间粒度,默认 auto(45 天内按日、190 天内按周、更长按月)'],
'metric' => ['type' => 'string', 'enum' => ['amount', 'orders'], 'description' => '图表画哪个:amount 金额(默认)/ orders 接诊诊单数'],
'metric' => ['type' => 'string', 'enum' => ['amount', 'orders'], 'description' => '图表画哪个:amount 金额(默认)/ orders 成交订单数'],
]);
}
return $tools;
@@ -204,10 +209,10 @@ class PerfTools
$matched = self::filterPeople($ranked, $args, 'assistant_id');
$shown = array_slice($matched, 0, $top);
$filtered = $matched !== $ranked;
[$metricName, $unit] = self::ASSISTANT_METRICS[$sortBy];
[$metricName] = self::ASSISTANT_METRICS[$sortBy];
$scope = self::scopeText($start, $end, $periodName, $deptIds !== null ? $depts : null, (string) ($data['channel_name'] ?? ''));
$summary = sprintf('医助业绩(%s):可见医助 %d 人,其中 %d 人有业绩;合计诊金 %s 元、接诊诊单 %s、接诊单数 %s、预约诊单 %s、被指派 %s、进线 %s,接诊率 %s 元/进线。',
$summary = sprintf('医助业绩(%s):可见医助 %d 人,其中 %d 人有业绩;合计诊金 %s 元、成交订单 %s 单;面诊完成 %s 人次、预约 %s 个、被指派 %s 次、进线 %s 个,每进线诊金 %s 元。',
$scope, count($rows), $active, self::num($totals['fee_amount']), self::num($totals['deal_order_count']), self::num($totals['consult_count']),
self::num($totals['appointment_count']), self::num($totals['assign_count']), self::num($totals['lead_count']), self::num($totals['consult_rate'], 1));
if ($rows === []) {
@@ -217,10 +222,8 @@ class PerfTools
} else {
$summary .= sprintf('按%s排名,下面是前 %d 名。', $metricName, count($shown));
}
$chart = $filtered && count($shown) < 2 ? null : self::rankingChart($shown, $sortBy, self::ASSISTANT_METRICS[$sortBy],
'医助' . $metricName . '排行', $scope . ($filtered ? '' : ' · 前 ' . count($shown) . ' 名'), '口径同后台业绩看板·医助排行榜');
return self::respond($identity, $audit, $summary, [
$summary .= self::notOrdersNote($sortBy, self::ASSISTANT_METRICS);
$out = [
'period' => ['start_date' => $start, 'end_date' => $end],
'sort_by' => $sortBy,
'assistants' => count($rows),
@@ -229,8 +232,25 @@ class PerfTools
'departments' => $depts,
'rows' => $shown,
'columns' => self::columns(self::ASSISTANT_METRICS),
'definitions' => self::metricDefinitions(self::ASSISTANT_METRICS),
'note' => (string) ($data['range_note'] ?? ''),
], $chart);
];
// 只看一个人时附上处方业务订单列表(按医助筛选)的数字,用户拿后台列表对账时能直接看出差在哪里
if ($filtered && count($matched) === 1) {
$person = $matched[0];
$check = self::orderListCheck($identity, 'assistant_id', $person['admin_id'], $start, $end);
if ($check !== null) {
$check['not_counted'] = max(0, $check['total'] - $person['deal_order_count']);
$out['order_list'] = $check;
$summary .= sprintf('对账:处方业务订单列表按该医助、同一时间段筛选共 %d 条(含全部状态),金额 %s 元,其中计入业绩 %s 元;业绩只算未取消、未拒收、未退款的订单,所以成交订单是 %d 单%s。',
$check['total'], self::num($check['amount']), self::num($check['performance_amount']), $person['deal_order_count'],
$check['not_counted'] > 0 ? ',列表里另有 ' . $check['not_counted'] . ' 条不计入业绩' : '');
}
}
$chart = $filtered && count($shown) < 2 ? null : self::rankingChart($shown, $sortBy, self::ASSISTANT_METRICS[$sortBy],
'医助' . $metricName . '排行', $scope . ($filtered ? '' : ' · 前 ' . count($shown) . ' 名'), '口径同后台业绩看板·医助排行榜');
return self::respond($identity, $audit, $summary, $out, $chart);
}
// ───────────────────────────── 医生排行 ─────────────────────────────
@@ -275,7 +295,7 @@ class PerfTools
[$metricName] = self::DOCTOR_METRICS[$sortBy];
$scope = self::scopeText($start, $end, $periodName, $deptIds !== null ? [['dept' => self::deptNames($identity, $deptIds)]] : null, '');
$summary = sprintf('医生业绩(%s):可见医生 %d 位,其中 %d 位有成交;合计成交金额 %s 元、接诊诊单 %s、客单价 %s 元,总挂号 %s、挂号完成 %s、过号 %s、取消 %s,挂号率 %s%%,系统开方 %s、手动开方 %s。',
$summary = sprintf('医生业绩(%s):可见医生 %d 位,其中 %d 位有成交;合计成交金额 %s 元、成交订单 %s 单、客单价 %s 元;挂号 %s 个(面诊完成 %s 人次、过号 %s、取消 %s),挂号成交率 %s%%;系统开方 %s 张、手动开方 %s 张。',
$scope, count($rows), $active, self::num($total['deal_amount'] ?? 0), self::num($total['deal_order_count'] ?? 0), self::num($total['avg_deal_amount'] ?? null),
self::num($total['appointment_total'] ?? 0), self::num($total['appointment_completed'] ?? 0), self::num($total['appointment_missed'] ?? 0),
self::num($total['appointment_cancelled'] ?? 0), self::num($total['appointment_conversion_rate'] ?? null), self::num($total['system_prescription_count'] ?? 0),
@@ -287,10 +307,8 @@ class PerfTools
} else {
$summary .= sprintf('按%s排名,下面是前 %d 名。', $metricName, count($shown));
}
$chart = $filtered && count($shown) < 2 ? null : self::rankingChart($shown, $sortBy, self::DOCTOR_METRICS[$sortBy],
'医生' . $metricName . '排行', $scope . ($filtered ? '' : ' · 前 ' . count($shown) . ' 名'), '口径同后台业绩看板·医生统计');
return self::respond($identity, $audit, $summary, [
$summary .= self::notOrdersNote($sortBy, self::DOCTOR_METRICS);
$out = [
'period' => ['start_date' => $start, 'end_date' => $end],
'sort_by' => $sortBy,
'doctors' => count($rows),
@@ -298,9 +316,25 @@ class PerfTools
'totals' => $total,
'rows' => $shown,
'columns' => self::columns(self::DOCTOR_METRICS) + ['appointment_missed' => '过号', 'appointment_cancelled' => '取消'],
'note' => '成交金额=业务订单金额,按订单创建时间,剔除已取消/拒收/退款及发生过退款的订单,归属开方医生;接诊诊单=这些订单条数;挂号按预约日期;挂号率=接诊诊单÷总挂号×100。'
. '只统计当前账号数据范围内的医生;按部门筛选时只统计该部门医助经手的挂号、订单和处方,并隐藏全为 0 的医生。',
], $chart);
'definitions' => self::metricDefinitions(self::DOCTOR_METRICS),
'note' => '只统计当前账号数据范围内的医生;按部门筛选时只统计该部门医助经手的挂号、订单和处方,并隐藏全为 0 的医生。',
];
// 按开方医生筛选列表时,非“业务订单全量”角色只能看到自己创建的订单,数字会偏小,只给能看全量订单的账号对账
if ($filtered && count($matched) === 1 && PrescriptionOrderLogic::canSeeAllPrescriptionOrders($identity->adminInfo)) {
$person = $matched[0];
$check = self::orderListCheck($identity, 'doctor_id', $person['admin_id'], $start, $end);
if ($check !== null) {
$check['not_counted'] = max(0, $check['total'] - $person['deal_order_count']);
$out['order_list'] = $check;
$summary .= sprintf('对账:处方业务订单列表按该开方医生、同一时间段筛选共 %d 条(含全部状态),金额 %s 元;成交订单只算未取消、未拒收、未退款且没有发生过退款的订单,所以是 %d 单%s。',
$check['total'], self::num($check['amount']), $person['deal_order_count'],
$check['not_counted'] > 0 ? ',列表里另有 ' . $check['not_counted'] . ' 条不计入' : '');
}
}
$chart = $filtered && count($shown) < 2 ? null : self::rankingChart($shown, $sortBy, self::DOCTOR_METRICS[$sortBy],
'医生' . $metricName . '排行', $scope . ($filtered ? '' : ' · 前 ' . count($shown) . ' 名'), '口径同后台业绩看板·医生统计');
return self::respond($identity, $audit, $summary, $out, $chart);
}
// ───────────────────────────── 部门业绩看板 ─────────────────────────────
@@ -342,20 +376,22 @@ class PerfTools
[$metricName] = self::DEPT_METRICS[$sortBy];
$scope = self::scopeText($start, $end, $periodName, null, $channelName);
$summary = sprintf('部门业绩看板(%s):%d 个部门行,合计业绩 %s 元、接诊诊单 %s、接诊单数 %s、预约诊单 %s、进线 %s、被指派 %s,投放成本 %s 元,ROI %s。',
$summary = sprintf('部门业绩看板(%s):%d 个部门行,合计业绩 %s 元、成交订单 %s 单;面诊完成 %s 人次、预约 %s 个、进线 %s 个、被指派 %s 次;投放成本 %s 元,ROI %s。',
$scope, count($rows), self::num($total['performance_amount'] ?? 0), self::num($total['deal_order_count'] ?? 0), self::num($total['consult_count'] ?? 0),
self::num($total['appointment_booked_count'] ?? 0), self::num($total['lead_count'] ?? 0), self::num($total['assign_count'] ?? 0),
self::num($total['cost_amount'] ?? 0), self::num($total['roi'] ?? null, 1));
if ($channelName !== '') {
$summary .= '渠道业绩合计 ' . self::num($total['completed_performance_amount'] ?? 0) . ' 元。';
}
$summary .= self::notOrdersNote($sortBy, self::DEPT_METRICS);
$chart = self::rankingChart(self::rank($rows, $sortBy, 'performance_amount'), $sortBy, self::DEPT_METRICS[$sortBy], '各部门' . $metricName, $scope, '口径同后台业绩看板·甄养堂诊金');
return self::respond($identity, $audit, $summary, [
'period' => ['start_date' => $start, 'end_date' => $end],
'channel' => $channelName,
'totals' => $total,
'rows' => $rows,
'columns' => self::columns(self::DEPT_METRICS) + ['revisit_count' => '复诊合计', 'avg_price' => '客单价', 'roi' => 'ROI', 'channel_performance_amount' => '渠道业绩'],
'columns' => self::columns(self::DEPT_METRICS) + ['revisit_count' => '复诊合计', 'avg_price' => '客单价(元,合计业绩÷面诊完成数)', 'roi' => 'ROI', 'channel_performance_amount' => '渠道业绩'],
'definitions' => self::metricDefinitions(self::DEPT_METRICS),
'note' => (string) ($data['channel_filter_note'] ?? ''),
], $chart);
}
@@ -376,7 +412,7 @@ class PerfTools
[$start, $end, $periodName] = self::range($args);
$metric = (string) ($args['metric'] ?? 'amount');
if (!in_array($metric, ['amount', 'orders'], true)) {
throw new McpException('metric 只支持 amount(金额)或 orders(接诊诊单数)', 'invalid');
throw new McpException('metric 只支持 amount(金额)或 orders(成交订单数)', 'invalid');
}
$startTs = (int) strtotime($start . ' 00:00:00');
$endTs = (int) strtotime($end . ' 23:59:59');
@@ -443,13 +479,13 @@ class PerfTools
$scope = self::scopeText($start, $end, $periodName, null, '') . ',' . $gText;
$parts = [];
foreach ($seriesOut as $s) {
$parts[] = sprintf('%s 合计%s %s 元、接诊诊单 %s%s', $s['name'], $amountName, self::num($s['total_amount']), self::num($s['total_orders']),
$parts[] = sprintf('%s 合计%s %s 元、成交订单 %s 单%s', $s['name'], $amountName, self::num($s['total_amount']), self::num($s['total_orders']),
$s['peak'] ? ',最高在 ' . $s['peak']['period'] . '(' . self::num($s['peak']['amount']) . ' 元)' : '');
}
$summary = ($by === 'doctor' ? '医生' : '医助') . '业绩走势(' . $scope . '):' . implode(';', $parts) . '。';
$unit = $metric === 'amount' ? '元' : '单';
$chart = self::chart(count($keys) >= 3 ? 'line' : 'column',
(count($seriesOut) > 1 ? '业绩走势对比 · ' : $seriesOut[0]['name'] . ' · ') . ($metric === 'amount' ? $amountName : '接诊诊单'),
(count($seriesOut) > 1 ? '业绩走势对比 · ' : $seriesOut[0]['name'] . ' · ') . ($metric === 'amount' ? $amountName : '成交订单数'),
$scope, $unit, $labels,
array_map(static fn ($s) => ['name' => $s['name'], 'data' => $s[$metric === 'amount' ? 'amount' : 'orders']], $seriesOut),
$by === 'doctor' ? '口径同医生统计成交金额(开方医生)' : '口径同医助排行榜诊金(订单创建人)');
@@ -676,12 +712,55 @@ class PerfTools
private static function metricText(array $metrics): string
{
return implode('、', array_map(static fn ($k, $v) => $k . ' ' . $v[0], array_keys($metrics), $metrics));
return implode('、', array_map(static fn ($k, $v) => $k . ' ' . $v[0] . ($v[2] !== $v[0] ? '(后台“' . $v[2] . '”)' : ''), array_keys($metrics), $metrics));
}
/** 列名:名称(单位)· 后台列名 */
private static function columns(array $metrics): array
{
return array_map(static fn ($v) => $v[0] . ($v[1] !== '' ? '(' . $v[1] . ')' : ''), $metrics);
return array_map(static fn ($v) => $v[0] . '(' . $v[1] . ')' . ($v[2] !== $v[0] ? ' · 后台“' . $v[2] . '”' : ''), $metrics);
}
/** 每个指标数的是什么,让模型按口径解释数字 */
private static function metricDefinitions(array $metrics): array
{
return array_map(static fn ($v) => $v[0] . ':' . $v[3] . ($v[2] !== $v[0] ? '(后台列名“' . $v[2] . '”)' : ''), $metrics);
}
/** 按不是订单的指标排名时提醒一句,避免把“面诊完成 96 人次”说成“96 单” */
private static function notOrdersNote(string $metric, array $metrics): string
{
[$name, $unit, $alias] = $metrics[$metric];
if (!in_array($metric, ['consult_count', 'appointment_count', 'appointment_booked_count', 'appointment_total', 'appointment_completed', 'assign_count', 'lead_count'], true)) {
return '';
}
return sprintf('注意:%s(%s%s)不是订单数,订单数看成交订单数(后台“接诊诊单”)。', $name, $unit, $alias !== $name ? ',后台列名“' . $alias . '”' : '');
}
/**
* 处方业务订单列表按医助(订单创建人)或开方医生筛选、同一时间段的条数和金额——就是用户在后台列表页看到的数字。
* 账号没有列表权限或查询失败时返回 null(不影响业绩结果)。
*/
private static function orderListCheck(Identity $identity, string $filter, int $adminId, string $start, string $end): ?array
{
$resource = Catalog::get('tcm.prescriptionOrder/lists');
if ($resource === null || Catalog::denialFor($identity, $resource) !== null) {
return null;
}
$params = Tools::params($resource, [$filter => $adminId, 'start_time' => $start . ' 00:00:00', 'end_time' => $end . ' 23:59:59'], true);
$envelope = Dispatcher::call($identity, $resource, Tools::listParams($resource, $params, 1, 1));
if ($envelope['code'] !== 1) {
return null;
}
$extend = (array) ($envelope['data']['extend'] ?? []);
return [
'filter' => $filter === 'doctor_id' ? '开方医生' : '医助(订单创建人)',
'total' => (int) ($envelope['data']['count'] ?? 0),
'amount' => round((float) ($extend['stats_order_amount'] ?? 0), 2),
'performance_amount' => round((float) ($extend['stats_order_amount_performance'] ?? $extend['stats_order_amount_not_cancelled'] ?? 0), 2),
'cancelled_amount' => round((float) ($extend['stats_order_amount_cancelled'] ?? 0), 2),
'note' => '处方业务订单列表按该条件、同一时间段筛选的结果:total 含全部状态;performance_amount 是其中计入业绩的金额(不含已取消/拒收/退款)。',
];
}
private static function top(array $args): int
@@ -801,7 +880,9 @@ class PerfTools
}
$counts = array_count_values(array_map('strval', array_column($rows, 'name')));
$labels = array_map(static fn ($r) => ($counts[(string) $r['name']] ?? 0) > 1 && !empty($r['dept']) ? $r['name'] . '(' . $r['dept'] . ')' : (string) $r['name'], $rows);
return self::chart('bar', $title, $subtitle, $meta[1], $labels, [['name' => $meta[0], 'data' => array_column($rows, $metric)]], $note);
// 图下说明写清楚这个数是什么(含后台列名),图离开上下文被转发时也不会被误读
$meaning = $meta[0] . ($meta[2] !== $meta[0] ? '(后台“' . $meta[2] . '”)' : '') . ':' . $meta[3];
return self::chart('bar', $title, $subtitle, $meta[1], $labels, [['name' => $meta[0], 'data' => array_column($rows, $metric)]], $meaning . '。' . $note);
}
private static function chart(string $type, string $title, string $subtitle, string $unit, array $labels, array $series, string $note = ''): array