更新
This commit is contained in:
@@ -11,8 +11,10 @@ use app\adminapi\lists\firstvisit\MyPatientProgressLists;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\doctor\AppointmentLogic;
|
||||
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
|
||||
use app\adminapi\validate\doctor\AppointmentValidate;
|
||||
use app\adminapi\validate\tcm\DiagnosisValidate;
|
||||
use app\adminapi\validate\tcm\PrescriptionOrderValidate;
|
||||
use app\common\model\doctor\Appointment;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
@@ -52,6 +54,74 @@ class MyPatientController extends BaseAdminController
|
||||
return $this->dataLists(new MyPatientProgressLists());
|
||||
}
|
||||
|
||||
/** 当前账号数据范围内可被指派的医助。 */
|
||||
public function assistants()
|
||||
{
|
||||
if (!$this->hasPagePermission() || !$this->hasOriginalPermission('tcm.diagnosis/assign')) {
|
||||
return $this->fail('权限不足,无法获取医助列表');
|
||||
}
|
||||
|
||||
return $this->data(DiagnosisLogic::getAssistants($this->adminId, $this->adminInfo));
|
||||
}
|
||||
|
||||
/** 从“我的患者”指派医助,先校验患者行级数据范围和目标医助范围。 */
|
||||
public function assign()
|
||||
{
|
||||
if (!$this->hasPagePermission() || !$this->hasOriginalPermission('tcm.diagnosis/assign')) {
|
||||
return $this->fail('权限不足,无法指派患者');
|
||||
}
|
||||
|
||||
$params = $this->request->post();
|
||||
$diagnosisId = (int) ($params['id'] ?? 0);
|
||||
$assistantId = (int) ($params['assistant_id'] ?? 0);
|
||||
if (!MyPatientLogic::canAccessDiagnosis($diagnosisId, $this->adminId, $this->adminInfo)) {
|
||||
return $this->fail('患者不存在或无权操作');
|
||||
}
|
||||
if ($assistantId <= 0 || !$this->canAssignToAssistant($assistantId)) {
|
||||
return $this->fail('所选医助不在当前可指派范围内');
|
||||
}
|
||||
|
||||
$result = DiagnosisLogic::assign([
|
||||
'id' => $diagnosisId,
|
||||
'assistant_id' => $assistantId,
|
||||
'is_inherit' => (int) ($params['is_inherit'] ?? 0) === 1 ? 1 : 0,
|
||||
]);
|
||||
if ($result === false) {
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('指派成功');
|
||||
}
|
||||
|
||||
/** 从“我的患者”补全身份证,复用诊单身份证校验和年龄计算。 */
|
||||
public function fillIdCard()
|
||||
{
|
||||
if (!$this->hasPagePermission() || !$this->hasOriginalPermission('tcm.diagnosis/edit')) {
|
||||
return $this->fail('权限不足,无法补全身份证');
|
||||
}
|
||||
|
||||
$params = (new DiagnosisValidate())->post()->goCheck('fillIdCard');
|
||||
$diagnosisId = (int) ($params['id'] ?? 0);
|
||||
if (!MyPatientLogic::canAccessDiagnosis($diagnosisId, $this->adminId, $this->adminInfo)) {
|
||||
return $this->fail('患者不存在或无权操作');
|
||||
}
|
||||
|
||||
$duplicate = DiagnosisLogic::checkIdCard([
|
||||
'id' => $diagnosisId,
|
||||
'id_card' => trim((string) ($params['id_card'] ?? '')),
|
||||
]);
|
||||
if (!empty($duplicate['exists'])) {
|
||||
return $this->fail((string) ($duplicate['message'] ?? '该身份证号已存在'));
|
||||
}
|
||||
|
||||
$result = DiagnosisLogic::fillIdCard($params);
|
||||
if ($result === false) {
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('补全成功,年龄已自动更新');
|
||||
}
|
||||
|
||||
/** 当前患者范围内的订单详情;仍要求原订单详情权限。 */
|
||||
public function orderDetail()
|
||||
{
|
||||
@@ -384,6 +454,17 @@ class MyPatientController extends BaseAdminController
|
||||
return $order;
|
||||
}
|
||||
|
||||
private function canAssignToAssistant(int $assistantId): bool
|
||||
{
|
||||
foreach (DiagnosisLogic::getAssistants($this->adminId, $this->adminInfo) as $assistant) {
|
||||
if ((int) ($assistant['id'] ?? 0) === $assistantId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $params @param array<int,string> $keys */
|
||||
private function onlyParams(array $params, array $keys): array
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface,
|
||||
|
||||
$rows = $query
|
||||
->field([
|
||||
'd.id', 'd.patient_id', 'd.patient_name', 'd.phone', 'd.gender', 'd.age',
|
||||
'd.id', 'd.patient_id', 'd.patient_name', 'd.phone', 'd.id_card', 'd.gender', 'd.age',
|
||||
'd.diagnosis_date', 'd.diagnosis_type', 'd.syndrome_type', 'd.assistant_id',
|
||||
'd.assign_read_at', 'd.create_time',
|
||||
])
|
||||
@@ -269,6 +269,8 @@ class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface,
|
||||
$row['source_patient_id'] = (int) ($row['patient_id'] ?? 0);
|
||||
$row['phone_masked'] = $this->maskPhone((string) ($row['phone'] ?? ''));
|
||||
unset($row['phone']);
|
||||
$row['has_id_card'] = trim((string) ($row['id_card'] ?? '')) !== '' ? 1 : 0;
|
||||
unset($row['id_card']);
|
||||
$row['gender_desc'] = (int) ($row['gender'] ?? 0) === 1 ? '男' : '女';
|
||||
$row['diagnosis_date_text'] = $this->formatDiagnosisDate($row['diagnosis_date'] ?? '');
|
||||
$row['assistant_name'] = (string) ($adminNames[$assistantId] ?? '未分配');
|
||||
|
||||
@@ -412,11 +412,32 @@ class FirstVisitConversionLogic
|
||||
/** @param array<int,array<string,mixed>> $rows @return array<int,array<string,mixed>> */
|
||||
private static function rankingRows(array $rows): array
|
||||
{
|
||||
if (count($rows) === 1 && is_array($rows[0]['children'] ?? null) && $rows[0]['children'] !== []) {
|
||||
return $rows[0]['children'];
|
||||
// lists 里可能同时存在“未绑定/未分配部门”等虚拟根节点。它们会让顶层节点数量
|
||||
// 大于 1,导致原逻辑无法展开唯一的真实组织根节点,图表最终只显示医院汇总行。
|
||||
$visibleRows = array_values(array_filter($rows, static function (array $row): bool {
|
||||
return (int) ($row['id'] ?? 0) > 0 && !((bool) ($row['_virtual_bucket'] ?? false));
|
||||
}));
|
||||
|
||||
// 每个可见顶层分支只展示同一层级:有权限看到下级时展示直属子部门;没有可见
|
||||
// 下级时保留当前部门。这样既能按角色/DataScope 展示子部门,也不会把父子汇总
|
||||
// 同时放进占比图造成重复计算。
|
||||
$chartRows = [];
|
||||
foreach ($visibleRows as $row) {
|
||||
$children = array_values(array_filter(
|
||||
is_array($row['children'] ?? null) ? $row['children'] : [],
|
||||
static fn (array $child): bool => (int) ($child['id'] ?? 0) > 0
|
||||
&& !((bool) ($child['_virtual_bucket'] ?? false))
|
||||
));
|
||||
if ($children !== []) {
|
||||
foreach ($children as $child) {
|
||||
$chartRows[] = $child;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$chartRows[] = $row;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
return $chartRows;
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $rows @return array<int,array<string,mixed>> */
|
||||
|
||||
@@ -25,6 +25,7 @@ use think\facade\Db;
|
||||
class PerformanceDashboardLogic
|
||||
{
|
||||
private const TREND_DAYS = 7;
|
||||
private const PAID_APPOINTMENT_DEPT_NAME = '一中心';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
@@ -68,6 +69,7 @@ class PerformanceDashboardLogic
|
||||
'time_type' => 'today',
|
||||
'include_members' => 0,
|
||||
'include_filters' => 0,
|
||||
'exclude_cancelled_appointments' => 1,
|
||||
'page_no' => 1,
|
||||
'page_size' => 100,
|
||||
], $adminId, $adminInfo);
|
||||
@@ -77,6 +79,7 @@ class PerformanceDashboardLogic
|
||||
'time_type' => 'yesterday',
|
||||
'include_members' => 0,
|
||||
'include_filters' => 0,
|
||||
'exclude_cancelled_appointments' => 1,
|
||||
'page_no' => 1,
|
||||
'page_size' => 100,
|
||||
], $adminId, $adminInfo);
|
||||
@@ -111,10 +114,13 @@ class PerformanceDashboardLogic
|
||||
$yesterdayOrderCount = (int) self::dailyMetric($orderDaily, $yesterday, 'count');
|
||||
$todayOrderAmount = self::dailyMetric($orderDaily, $today, 'amount');
|
||||
$yesterdayOrderAmount = self::dailyMetric($orderDaily, $yesterday, 'amount');
|
||||
$todayPaidAppointmentRate = round((float) ($todaySummary['paid_appointment_rate'] ?? 0), 2);
|
||||
$yesterdayPaidAppointmentRate = round((float) ($yesterdaySummary['paid_appointment_rate'] ?? 0), 2);
|
||||
$todayInterviewReceiveRate = round((float) ($todaySummary['interview_receive_rate'] ?? 0), 2);
|
||||
$yesterdayInterviewReceiveRate = round((float) ($yesterdaySummary['interview_receive_rate'] ?? 0), 2);
|
||||
$todayPaidAppointmentCount = self::paidAppointmentCountWithCenterRule($todayOverview);
|
||||
$yesterdayPaidAppointmentCount = self::paidAppointmentCountWithCenterRule($yesterdayOverview);
|
||||
$todayPaidAppointmentRate = self::percent($todayPaidAppointmentCount, $todayAddFansCount);
|
||||
$yesterdayPaidAppointmentRate = self::percent($yesterdayPaidAppointmentCount, $yesterdayAddFansCount);
|
||||
// 接诊卡片使用的是有效业务订单,接诊率必须使用同一订单口径,不能继续读取旧的双审完成单。
|
||||
$todayInterviewReceiveRate = self::percent($todayOrderCount, $todayInterviewCount);
|
||||
$yesterdayInterviewReceiveRate = self::percent($yesterdayOrderCount, $yesterdayInterviewCount);
|
||||
|
||||
$target = self::buildTargetProgress(
|
||||
$adminId,
|
||||
@@ -130,6 +136,9 @@ class PerformanceDashboardLogic
|
||||
'month_amount' => round($monthAmount, 2),
|
||||
'month_compare_rate' => self::relativeChange($monthAmount, $previousMonthAmount),
|
||||
'month_compare_label' => '较上月同期',
|
||||
'today_amount' => round($todayOrderAmount, 2),
|
||||
'today_compare_rate' => self::relativeChange($todayOrderAmount, $yesterdayOrderAmount),
|
||||
'today_compare_label' => '较昨日',
|
||||
'yesterday_amount' => round($yesterdayAmount, 2),
|
||||
'yesterday_compare_rate' => self::relativeChange($yesterdayAmount, $dayBeforeAmount),
|
||||
'yesterday_compare_label' => '较前一日',
|
||||
@@ -142,6 +151,7 @@ class PerformanceDashboardLogic
|
||||
// 保留原响应字段名以兼容已发布前端,数值含义已统一为“计入业绩的业务订单”。
|
||||
'completed_order_count' => $todayOrderCount,
|
||||
'completed_order_amount' => $todayOrderAmount,
|
||||
'paid_appointment_count' => $todayPaidAppointmentCount,
|
||||
'paid_appointment_rate' => $todayPaidAppointmentRate,
|
||||
'interview_receive_rate' => $todayInterviewReceiveRate,
|
||||
'comparisons' => [
|
||||
@@ -177,11 +187,60 @@ class PerformanceDashboardLogic
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'commission_note' => '本人业绩按当前账号创建的业务订单统计,排除已取消、拒收和退款订单。',
|
||||
'rate_note' => '近 7 天趋势与业绩统计一致:挂号排除已取消记录,进线仅统计当前范围内可归属业绩中心的新增客户事件,诊单按订单创建时间统计并排除履约 4/9/10。',
|
||||
'rate_note' => '付费挂号率:一中心员工的有效挂号全部按付费挂号,其他部门按 5 元实付挂号;面诊接诊率:有效业务诊单数 / 已完成面诊数。近 7 天挂号排除已取消记录,诊单按订单创建时间统计并排除履约 4/9/10。',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 一中心员工的有效挂号全部视为付费;其它部门仍沿用 5 元实付挂号口径。
|
||||
* 部门树的父节点已汇总子节点,因此命中“一中心”后不再向下递归,避免重复累计。
|
||||
*
|
||||
* @param array<string, mixed> $overview
|
||||
*/
|
||||
private static function paidAppointmentCountWithCenterRule(array $overview): int
|
||||
{
|
||||
$summary = is_array($overview['summary'] ?? null) ? $overview['summary'] : [];
|
||||
$paidTotal = (int) ($summary['paid_appointment_count'] ?? 0);
|
||||
$rows = is_array($overview['lists'] ?? null) ? $overview['lists'] : [];
|
||||
[$centerAppointments, $centerPaidAppointments] = self::namedDepartmentAppointmentMetrics(
|
||||
$rows,
|
||||
self::PAID_APPOINTMENT_DEPT_NAME
|
||||
);
|
||||
|
||||
return max(0, $paidTotal - $centerPaidAppointments + $centerAppointments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array{0:int,1:int}
|
||||
*/
|
||||
private static function namedDepartmentAppointmentMetrics(array $rows, string $departmentName): array
|
||||
{
|
||||
$appointmentCount = 0;
|
||||
$paidAppointmentCount = 0;
|
||||
foreach ($rows as $row) {
|
||||
if (trim((string) ($row['name'] ?? '')) === $departmentName) {
|
||||
$appointmentCount += (int) ($row['appointment_total_count'] ?? 0);
|
||||
$paidAppointmentCount += (int) ($row['paid_appointment_count'] ?? 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
$children = is_array($row['children'] ?? null) ? $row['children'] : [];
|
||||
if ($children === []) {
|
||||
continue;
|
||||
}
|
||||
[$childAppointments, $childPaidAppointments] = self::namedDepartmentAppointmentMetrics(
|
||||
$children,
|
||||
$departmentName
|
||||
);
|
||||
$appointmentCount += $childAppointments;
|
||||
$paidAppointmentCount += $childPaidAppointments;
|
||||
}
|
||||
|
||||
return [$appointmentCount, $paidAppointmentCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -310,6 +369,15 @@ class PerformanceDashboardLogic
|
||||
return round((float) ($daily[$date][$metric] ?? 0), 2);
|
||||
}
|
||||
|
||||
private static function percent(float $numerator, int $denominator): float
|
||||
{
|
||||
if ($denominator <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return round(($numerator / $denominator) * 100, 2);
|
||||
}
|
||||
|
||||
private static function relativeChange(float $current, float $previous): ?float
|
||||
{
|
||||
if (abs($previous) < 0.00001) {
|
||||
|
||||
Reference in New Issue
Block a user