This commit is contained in:
Your Name
2026-05-05 13:28:45 +08:00
parent 81b2808eaf
commit d6e105f912
19 changed files with 3730 additions and 267 deletions
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\DoctorDailyStatsLogic;
/**
* 医生日统计(系统/手动开方、成交、挂号状态)
*
* - GET stats.doctorDailyStats/overview
* ?start_date=&end_date=&channel_code=
* 不传/忽略 dept_ids(医生统计不按展示部门检索);未传日期时由后端默认当日。
*/
class DoctorDailyStatsController extends BaseAdminController
{
public function overview()
{
$params = $this->request->get();
return $this->data(DoctorDailyStatsLogic::overview($params, $this->adminId, $this->adminInfo));
}
}
@@ -23,7 +23,7 @@ class YejiStatsController extends BaseAdminController
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::overview($params));
return $this->data(YejiStatsLogic::overview($params, $this->adminId, $this->adminInfo));
}
/**
@@ -79,13 +79,13 @@ class YejiStatsController extends BaseAdminController
return $this->data([
'ranges' => $ranges,
'tables' => YejiStatsLogic::overviewBatch($ranges, $base),
'tables' => YejiStatsLogic::overviewBatch($ranges, $base, $this->adminId, $this->adminInfo),
]);
}
public function deptOptions()
{
return $this->data(YejiStatsLogic::deptOptions());
return $this->data(YejiStatsLogic::deptOptions($this->adminId, $this->adminInfo));
}
/** 渠道(标签)下拉,按 source_group_name 分组 */
@@ -100,6 +100,6 @@ class YejiStatsController extends BaseAdminController
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::assistantLeaderboards($params));
return $this->data(YejiStatsLogic::assistantLeaderboards($params, $this->adminId, $this->adminInfo));
}
}
@@ -19,6 +19,7 @@ use app\common\model\tcm\PrescriptionOrderPayOrder;
use app\common\model\ExpressTracking;
use app\common\service\gancao\GancaoScmRecipelService;
use think\facade\Config;
use think\facade\Db;
use think\db\Query;
class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
@@ -98,10 +99,12 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$this->applyExpressCompanyFilter($query);
$this->applyExpressKeywordFilter($query);
$this->applySupplyModeFilter($query);
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
$query->where('creator_id', $this->adminId);
}
$this->applyCreatorOrOwnPrescriptionVisibility($query);
$this->applyDataScopeForPrescriptionOrder($query);
// 业绩看板侧栏等:不展示履约已取消(4),与 stats 业绩统计口径一致
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
$query->where('fulfillment_status', '<>', 4);
}
}
/**
@@ -172,6 +175,30 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
}
}
/**
* 非「业务订单全量」角色:默认仅本人创建;若持有 viewOrdersForOwnPrescription 则包含关联处方开方人为本人的订单
*/
private function applyCreatorOrOwnPrescriptionVisibility($query): void
{
if (PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
return;
}
$adminId = (int) $this->adminId;
if (PrescriptionOrderLogic::canViewOrdersForOwnPrescription($this->adminInfo)) {
$poTbl = (new PrescriptionOrder())->getTable();
$rxTbl = (new Prescription())->getTable();
$query->where(function ($q) use ($poTbl, $rxTbl, $adminId) {
$q->where('creator_id', $adminId);
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$poTbl}`.`prescription_id`"
. " AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$adminId})"
);
});
} else {
$query->where('creator_id', $adminId);
}
}
/**
* 数据隔离:创建者 ∈ 可见 admin,或 关联诊单医助 ∈ 可见 admin
*/
@@ -224,20 +251,23 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$this->applyPatientKeywordFilter($query);
if (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) {
$this->applyDataScopeForPrescriptionOrder($query);
return $query;
} else {
$assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
$myRoles = array_map('intval', $this->adminInfo['role_id'] ?? []);
if ($assistantRid > 0 && in_array($assistantRid, $myRoles, true)) {
$this->applyAssistantDiagnosisOnlyFilter($query);
$this->applyDataScopeForPrescriptionOrder($query);
} else {
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
$this->applyCreatorOrOwnPrescriptionVisibility($query);
}
$this->applyDataScopeForPrescriptionOrder($query);
}
}
$assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
$myRoles = array_map('intval', $this->adminInfo['role_id'] ?? []);
if ($assistantRid > 0 && in_array($assistantRid, $myRoles, true)) {
$this->applyAssistantDiagnosisOnlyFilter($query);
$this->applyDataScopeForPrescriptionOrder($query);
return $query;
// 与列表 applyPermissionAndExtraFilters 一致:业绩侧栏传 exclude 时不统计已取消行
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
$query->where('fulfillment_status', '<>', 4);
}
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
$query->where('creator_id', $this->adminId);
}
$this->applyDataScopeForPrescriptionOrder($query);
return $query;
}
@@ -400,7 +430,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$s = $this->computeListStatsForExtend();
$focus = $this->computeFocusCountsForExtend();
return [
$base = [
'deposit_min_amount' => PrescriptionOrderLogic::depositMinAmount(),
'gancao_scm_enabled' => GancaoScmRecipelService::isConfigured(),
'stats_order_amount' => $s['order_amount'],
@@ -422,6 +452,83 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'focus_pending_ship_count' => $focus['pending_ship'],
'focus_risk_count' => $focus['risk'],
];
$yejiConsult = $this->computeYejiDrawerConsultCountIfRequested();
if ($yejiConsult !== null) {
$base['stats_yeji_consult_count'] = $yejiConsult;
}
return $base;
}
/**
* 业绩看板侧栏专用:约诊/接诊条数,与 YejiStatsLogic::applyConsultFiltersAlignedWithAppointmentLists 同口径
* doctor_appointment.status=3appointment_date 落入区间,有效医助 COALESCE(挂号.assistant_id,诊单.assistant_id))。
* 请求须带 yeji_order_drawer=1,且 assistant_id 或 assistant_dept_id 与起止时间有效。
*/
private function computeYejiDrawerConsultCountIfRequested(): ?int
{
if ((int) ($this->params['yeji_order_drawer'] ?? 0) !== 1) {
return null;
}
$assistantId = (int) ($this->params['assistant_id'] ?? 0);
$deptRootId = (int) ($this->params['assistant_dept_id'] ?? 0);
if ($assistantId <= 0 && $deptRootId <= 0) {
return null;
}
[$startDate, $endDate] = $this->resolveYejiConsultAppointmentDateRange();
if ($startDate === '' || $endDate === '') {
return null;
}
$eff = 'COALESCE(NULLIF(a.assistant_id, 0), NULLIF(u.assistant_id, 0))';
$query = Db::name('doctor_appointment')->alias('a')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->where('a.status', 3)
->whereBetween('a.appointment_date', [$startDate, $endDate])
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)');
if ($assistantId > 0) {
$query->whereRaw("({$eff}) = " . $assistantId);
} else {
$deptIds = DeptLogic::getSelfAndDescendantIds($deptRootId);
$deptIds = array_values(array_filter(array_map('intval', $deptIds), static function (int $id): bool {
return $id > 0;
}));
if ($deptIds === []) {
return 0;
}
$adTbl = (new AdminDept())->getTable();
$inList = implode(',', $deptIds);
$query->whereRaw(
"EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.admin_id = {$eff} AND ad.dept_id IN ({$inList}))"
);
}
return (int) $query->count();
}
/**
* @return array{0: string, 1: string} Y-m-d
*/
private function resolveYejiConsultAppointmentDateRange(): array
{
$st = trim((string) ($this->params['start_time'] ?? ''));
$et = trim((string) ($this->params['end_time'] ?? ''));
if ($st === '' || $et === '') {
return ['', ''];
}
$t0 = strtotime($st);
$t1 = strtotime($et);
if ($t0 === false || $t1 === false) {
return ['', ''];
}
if ($t1 < $t0) {
$tmp = $t0;
$t0 = $t1;
$t1 = $tmp;
}
return [date('Y-m-d', $t0), date('Y-m-d', $t1)];
}
/**
@@ -551,6 +658,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$ar = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
if ($ar > 0 && in_array($ar, array_map('intval', $this->adminInfo['role_id'] ?? []), true)) {
$scope = 'assistant';
} elseif (PrescriptionOrderLogic::canViewOrdersForOwnPrescription($this->adminInfo)) {
$scope = 'own_rx';
}
}
+7 -3
View File
@@ -86,13 +86,17 @@ class RoleLogic extends BaseLogic
try {
$menuId = !empty($params['menu_id']) ? $params['menu_id'] : [];
SystemRole::update([
$roleRow = [
'id' => $params['id'],
'name' => $params['name'],
'desc' => $params['desc'] ?? '',
'sort' => $params['sort'] ?? 0,
'data_scope' => self::normalizeDataScope($params['data_scope'] ?? null),
]);
];
// 分配权限等非编辑页提交的 edit 若不传 data_scope,不得把库里的范围覆盖成默认全部
if (array_key_exists('data_scope', $params)) {
$roleRow['data_scope'] = self::normalizeDataScope($params['data_scope'] ?? null);
}
SystemRole::update($roleRow);
if (!empty($menuId)) {
SystemRoleMenu::where(['role_id' => $params['id']])->delete();
+7 -3
View File
@@ -14,7 +14,6 @@
namespace app\adminapi\logic\dept;
use app\common\enum\YesNoEnum;
use app\common\logic\BaseLogic;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminDept;
@@ -271,11 +270,16 @@ class DeptLogic extends BaseLogic
*/
public static function getAllData()
{
$data = Dept::where(['status' => YesNoEnum::YES])
->order(['sort' => 'desc', 'id' => 'desc'])
// 与业绩看板等统计口径一致:含全部未软删部门(不再仅限 status=启用),
// 避免外链 assistant_dept_id 在树下拉中不存在导致 TreeSelect 初始化异常。
$data = Dept::order(['sort' => 'desc', 'id' => 'desc'])
->select()
->toArray();
if ($data === []) {
return [];
}
$pid = min(array_column($data, 'pid'));
return self::getTree($data, $pid);
}
@@ -0,0 +1,497 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\common\model\auth\Admin;
use app\common\service\DataScope\DataScopeService;
use think\facade\Db;
/**
* 医生统计:日期区间、渠道/标签、数据范围与业绩看板一致;**不按展示部门收窄**(始终按全部「中心」展示树,再套数据权限)。
*
* 医生范围:<b>admin_role.role_id = 1</b> 且管理员 <b>未软删</b>delete_time 为空);再按账号「数据范围」收窄。
*
* - 系统/手动开方:tcm_prescription.prescription_date ∈ [start,end];渠道/标签与业绩渠道列同源 EXISTS / 诊单标签
* - 成交:订单 create_time、排除履约(4),按处方 creator_id;渠道/标签同 sumPerformance 口径
* - 挂号:appointment_date ∈ [start,end];选渠道时挂号 channels 命中字典值;标签渠道时 patient_id ∈ 标签诊单集
*/
class DoctorDailyStatsLogic
{
/**
* @param array{
* start_date?:string,
* end_date?:string,
* channel_code?:string,
* tag_id?:string,
* doctor_id?:int|string
* } $params
*
* @return array{start_date:string,end_date:string,rows:array,total:array<string,mixed>}
*/
public static function overview(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
{
// 医生统计不参与「展示部门」检索:忽略 dept_ids,避免与业绩表部门筛选联动
$paramsForCtx = $params;
unset($paramsForCtx['dept_ids']);
$ctx = YejiStatsLogic::resolveSharedYejiFilterContext($paramsForCtx, $viewerAdminId, $viewerAdminInfo);
$startDate = $ctx['startDate'];
$endDate = $ctx['endDate'];
$startTs = $ctx['startTs'];
$endTs = $ctx['endTs'];
$appointmentChannelValues = $ctx['appointmentChannelValues'];
$channelFilterActive = $ctx['channelFilterActive'];
$tagDiagIds = $ctx['tagDiagIds'];
$tagAssistantIds = $ctx['tagAssistantIds'];
$tagFallback = $ctx['tagFallback'];
$filterDoctorId = (int) ($params['doctor_id'] ?? 0);
$doctorIds = self::resolveDoctorAdminIdsForStats($viewerAdminId, $viewerAdminInfo);
if ($filterDoctorId > 0) {
$doctorIds = in_array($filterDoctorId, $doctorIds, true) ? [$filterDoctorId] : [];
}
if ($doctorIds === []) {
return [
'start_date' => $startDate,
'end_date' => $endDate,
'rows' => [],
'total' => self::emptyTotals(),
];
}
$rxMap = self::loadPrescriptionCounts(
$startDate,
$endDate,
$doctorIds,
$appointmentChannelValues,
$channelFilterActive,
$tagDiagIds,
$tagAssistantIds,
$tagFallback
);
$orderMap = self::loadOrderAggregates(
$startTs,
$endTs,
$doctorIds,
$appointmentChannelValues,
$channelFilterActive,
$tagDiagIds,
$tagAssistantIds,
$tagFallback
);
$apptMap = self::loadAppointmentAggregates(
$startDate,
$endDate,
$doctorIds,
$appointmentChannelValues,
$channelFilterActive,
$tagDiagIds
);
$adminRows = Admin::whereIn('id', $doctorIds)
->whereNull('delete_time')
->field(['id', 'name'])
->order('id', 'asc')
->select()
->toArray();
$nameById = [];
foreach ($adminRows as $r) {
$nameById[(int) $r['id']] = (string) ($r['name'] ?? '');
}
$rows = [];
foreach ($doctorIds as $aid) {
$rx = $rxMap[$aid] ?? ['system' => 0, 'manual' => 0];
$ord = $orderMap[$aid] ?? ['amount' => 0.0, 'count' => 0];
$ap = $apptMap[$aid] ?? ['completed' => 0, 'missed' => 0, 'cancelled' => 0];
$cnt = (int) $ord['count'];
$amt = round((float) $ord['amount'], 2);
$rows[] = [
'admin_id' => $aid,
'doctor_name' => $nameById[$aid] ?? ('#' . $aid),
'system_prescription_count' => (int) $rx['system'],
'manual_prescription_count' => (int) $rx['manual'],
'deal_amount' => $amt,
'deal_order_count' => $cnt,
'avg_deal_amount' => $cnt > 0 ? round($amt / $cnt, 2) : null,
'appointment_completed' => (int) $ap['completed'],
'appointment_missed' => (int) $ap['missed'],
'appointment_cancelled' => (int) $ap['cancelled'],
];
}
usort($rows, static function (array $a, array $b): int {
if (($a['deal_amount'] ?? 0) != ($b['deal_amount'] ?? 0)) {
return ($b['deal_amount'] ?? 0) <=> ($a['deal_amount'] ?? 0);
}
return strcmp((string) ($a['doctor_name'] ?? ''), (string) ($b['doctor_name'] ?? ''));
});
return [
'start_date' => $startDate,
'end_date' => $endDate,
'rows' => $rows,
'total' => self::sumTotals($rows),
];
}
/**
* 医生角色(role_id=1)、管理员未删除、且在数据范围内的 admin_id。
*
* @return int[]
*/
private static function resolveDoctorAdminIdsForStats(int $viewerAdminId, array $viewerAdminInfo): array
{
$roleDoctors = Db::name('admin_role')->alias('ar')
->join('admin a', 'a.id = ar.admin_id')
->where('ar.role_id', 1)
->whereNull('a.delete_time')
->column('ar.admin_id');
$doctorIds = array_values(array_unique(array_filter(array_map('intval', $roleDoctors), static function (int $v): bool {
return $v > 0;
})));
sort($doctorIds);
if ($viewerAdminId > 0 && DataScopeService::isEnabled()) {
$visibleIds = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
if ($visibleIds !== null) {
if ($visibleIds === []) {
return [];
}
$flip = array_flip($visibleIds);
$doctorIds = array_values(array_filter($doctorIds, static function (int $id) use ($flip): bool {
return isset($flip[$id]);
}));
}
}
return $doctorIds;
}
/**
* @return array<string, float|int|null>
*/
private static function emptyTotals(): array
{
return [
'system_prescription_count' => 0,
'manual_prescription_count' => 0,
'deal_amount' => 0.0,
'deal_order_count' => 0,
'avg_deal_amount' => null,
'appointment_completed' => 0,
'appointment_missed' => 0,
'appointment_cancelled' => 0,
];
}
/**
* @param array<int, array<string, mixed>> $rows
*
* @return array<string, float|int|null>
*/
private static function sumTotals(array $rows): array
{
$t = self::emptyTotals();
foreach ($rows as $r) {
$t['system_prescription_count'] += (int) ($r['system_prescription_count'] ?? 0);
$t['manual_prescription_count'] += (int) ($r['manual_prescription_count'] ?? 0);
$t['deal_amount'] += (float) ($r['deal_amount'] ?? 0);
$t['deal_order_count'] += (int) ($r['deal_order_count'] ?? 0);
$t['appointment_completed'] += (int) ($r['appointment_completed'] ?? 0);
$t['appointment_missed'] += (int) ($r['appointment_missed'] ?? 0);
$t['appointment_cancelled'] += (int) ($r['appointment_cancelled'] ?? 0);
}
$t['deal_amount'] = round((float) $t['deal_amount'], 2);
$dc = (int) $t['deal_order_count'];
$t['avg_deal_amount'] = $dc > 0 ? round((float) $t['deal_amount'] / $dc, 2) : null;
return $t;
}
/**
* @param int[] $appointmentChannelValues
* @param int[]|null $tagDiagIds
* @param array<int,int>|null $tagAssistantIds
*
* @return array<int, array{system:int, manual:int}>
*/
private static function loadPrescriptionCounts(
string $startDate,
string $endDate,
array $doctorIds,
array $appointmentChannelValues,
bool $channelFilterActive,
?array $tagDiagIds,
?array $tagAssistantIds,
bool $tagFallback
): array {
if (!self::tagScopeNonEmpty($tagDiagIds, $tagAssistantIds)) {
return [];
}
$query = Db::name('tcm_prescription')
->alias('rx')
->whereNull('rx.delete_time')
->whereRaw('IFNULL(rx.void_status, 0) <> 1')
->whereBetween('rx.prescription_date', [$startDate, $endDate])
->whereIn('rx.creator_id', $doctorIds)
->where('rx.diagnosis_id', '>', 0);
self::applyPrescriptionChannelTagFilter(
$query,
'rx',
$appointmentChannelValues,
$channelFilterActive,
$tagDiagIds,
$tagAssistantIds,
$tagFallback
);
$query->field([
'rx.creator_id',
Db::raw('SUM(CASE WHEN IFNULL(rx.is_system_auto, 0) = 1 THEN 1 ELSE 0 END) AS system_cnt'),
Db::raw('SUM(CASE WHEN IFNULL(rx.is_system_auto, 0) <> 1 THEN 1 ELSE 0 END) AS manual_cnt'),
])->group('rx.creator_id');
$out = [];
foreach ($query->select()->toArray() as $r) {
$id = (int) ($r['creator_id'] ?? 0);
if ($id <= 0) {
continue;
}
$out[$id] = [
'system' => (int) ($r['system_cnt'] ?? 0),
'manual' => (int) ($r['manual_cnt'] ?? 0),
];
}
return $out;
}
/**
* @param int[] $appointmentChannelValues
* @param int[]|null $tagDiagIds
* @param array<int,int>|null $tagAssistantIds
*
* @return array<int, array{amount: float, count: int}>
*/
private static function loadOrderAggregates(
int $startTs,
int $endTs,
array $doctorIds,
array $appointmentChannelValues,
bool $channelFilterActive,
?array $tagDiagIds,
?array $tagAssistantIds,
bool $tagFallback
): array {
if (!self::tagScopeNonEmpty($tagDiagIds, $tagAssistantIds)) {
return [];
}
$q = Db::name('tcm_prescription_order')
->alias('o')
->join('tcm_prescription rx', 'rx.id = o.prescription_id AND rx.delete_time IS NULL', 'INNER')
->whereNull('o.delete_time')
->whereBetween('o.create_time', [$startTs, $endTs])
->whereRaw('NOT (o.fulfillment_status <=> 4)')
->whereIn('rx.creator_id', $doctorIds)
->where('o.diagnosis_id', '>', 0);
$normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues);
if ($normCh !== []) {
$apTable = self::tableWithPrefix('doctor_appointment');
$adminRoleTable = self::tableWithPrefix('admin_role');
$ph = implode(',', array_fill(0, count($normCh), '?'));
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $normCh)));
$channelCond = "ap.channels IN ({$ph})";
$existsSql = "EXISTS (SELECT 1 FROM {$apTable} ap INNER JOIN {$adminRoleTable} ar "
. "ON ar.admin_id = ap.assistant_id AND ar.role_id = 2 WHERE ap.patient_id = o.diagnosis_id "
. "AND ap.status = 3 AND {$channelCond})";
$q->whereRaw($existsSql, $strVals);
} elseif ($channelFilterActive) {
self::applyOrderTagFilter($q, 'o', 'rx', $tagDiagIds, $tagAssistantIds, $tagFallback);
}
$q->field([
'rx.creator_id',
Db::raw('SUM(o.amount) AS amount_sum'),
Db::raw('COUNT(*) AS order_cnt'),
])->group('rx.creator_id');
$out = [];
foreach ($q->select()->toArray() as $r) {
$id = (int) ($r['creator_id'] ?? 0);
if ($id <= 0) {
continue;
}
$out[$id] = [
'amount' => (float) ($r['amount_sum'] ?? 0),
'count' => (int) ($r['order_cnt'] ?? 0),
];
}
return $out;
}
/**
* 订单标签条件(无字典渠道映射时,与业绩 tag 分支一致;开方人维度用 rx.creator_id 兜底)。
*
* @param \think\db\Query $q
*/
private static function applyOrderTagFilter(
$q,
string $orderAlias,
string $rxAlias,
?array $tagDiagIds,
?array $tagAssistantIds,
bool $tagFallback
): void {
if ($tagDiagIds !== null) {
$q->whereIn("{$orderAlias}.diagnosis_id", $tagDiagIds);
}
if ($tagAssistantIds !== null) {
$ids = array_keys($tagAssistantIds);
if ($tagFallback) {
$q->whereIn("{$rxAlias}.creator_id", array_map('intval', $ids));
} else {
$q->whereIn("{$orderAlias}.creator_id", array_map('intval', $ids));
}
}
}
/**
* @param \think\db\Query $query rx 别名查询
* @param string $rxAlias
* @param int[] $appointmentChannelValues
* @param int[]|null $tagDiagIds
* @param array<int,int>|null $tagAssistantIds
*/
private static function applyPrescriptionChannelTagFilter(
$query,
string $rxAlias,
array $appointmentChannelValues,
bool $channelFilterActive,
?array $tagDiagIds,
?array $tagAssistantIds,
bool $tagFallback
): void {
$normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues);
if ($normCh !== []) {
$apTable = self::tableWithPrefix('doctor_appointment');
$adminRoleTable = self::tableWithPrefix('admin_role');
$ph = implode(',', array_fill(0, count($normCh), '?'));
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $normCh)));
$channelCond = "ap.channels IN ({$ph})";
$existsSql = "EXISTS (SELECT 1 FROM {$apTable} ap INNER JOIN {$adminRoleTable} ar "
. "ON ar.admin_id = ap.assistant_id AND ar.role_id = 2 WHERE ap.patient_id = {$rxAlias}.diagnosis_id "
. "AND ap.status = 3 AND {$channelCond})";
$query->whereRaw($existsSql, $strVals);
} elseif ($channelFilterActive) {
if ($tagDiagIds !== null) {
$query->whereIn("{$rxAlias}.diagnosis_id", $tagDiagIds);
}
if ($tagAssistantIds !== null && $tagFallback) {
$query->whereIn("{$rxAlias}.creator_id", array_map('intval', array_keys($tagAssistantIds)));
}
}
}
/**
* 标签范围显式为空(0 条诊单/医助)时整段统计不再查询。
*/
private static function tagScopeNonEmpty(?array $tagDiagIds, ?array $tagAssistantIds): bool
{
if ($tagDiagIds !== null && $tagDiagIds === []) {
return false;
}
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
return false;
}
return true;
}
/**
* @param int[] $appointmentChannelValues
* @param int[]|null $tagDiagIds
*
* @return array<int, array{completed:int, missed:int, cancelled:int}>
*/
private static function loadAppointmentAggregates(
string $startDate,
string $endDate,
array $doctorIds,
array $appointmentChannelValues,
bool $channelFilterActive,
?array $tagDiagIds
): array {
$q = Db::name('doctor_appointment')
->whereBetween('appointment_date', [$startDate, $endDate])
->whereIn('doctor_id', $doctorIds);
$normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues);
if ($normCh !== []) {
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $normCh)));
$q->whereIn('channels', $strVals);
} elseif ($channelFilterActive && $tagDiagIds !== null) {
if ($tagDiagIds === []) {
return [];
}
$q->whereIn('patient_id', $tagDiagIds);
}
$q->field([
'doctor_id',
Db::raw('SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS completed'),
Db::raw('SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) AS missed'),
Db::raw('SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS cancelled'),
])->group('doctor_id');
$out = [];
foreach ($q->select()->toArray() as $r) {
$id = (int) ($r['doctor_id'] ?? 0);
if ($id <= 0) {
continue;
}
$out[$id] = [
'completed' => (int) ($r['completed'] ?? 0),
'missed' => (int) ($r['missed'] ?? 0),
'cancelled' => (int) ($r['cancelled'] ?? 0),
];
}
return $out;
}
/**
* @return int[]
*/
private static function normalizeAppointmentChannelInts(array $raw): array
{
$out = [];
foreach ($raw as $v) {
$i = (int) $v;
if ($i > 0) {
$out[] = $i;
}
}
return array_values(array_unique($out));
}
private static function tableWithPrefix(string $table): string
{
$prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_');
return $prefix . $table;
}
}
File diff suppressed because it is too large Load Diff
@@ -111,6 +111,31 @@ class PrescriptionOrderLogic
return self::roleIntersect($adminInfo, is_array($roles) ? $roles : []);
}
/**
* 菜单权限:非全量角色可额外查看「关联处方开方人为本账号」的业务订单(医助代建单)
*
* perms: tcm.prescriptionOrder/viewOrdersForOwnPrescription
*/
public static function canViewOrdersForOwnPrescription(array $adminInfo): bool
{
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
return in_array('tcm.prescriptionOrder/viewOrdersForOwnPrescription', $perms, true);
}
/**
* 业务订单关联处方的开方人是否为指定管理员(处方 creator_id
*/
public static function isPrescriptionOrderPrescriber(int $prescriptionId, int $adminId): bool
{
if ($prescriptionId <= 0 || $adminId <= 0) {
return false;
}
$cid = (int) Prescription::where('id', $prescriptionId)->whereNull('delete_time')->value('creator_id');
return $cid === $adminId;
}
/**
* @param PrescriptionOrder $row
*/
@@ -119,16 +144,28 @@ class PrescriptionOrderLogic
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
return true;
}
if ((int) $row->creator_id === $adminId) {
return true;
}
if (self::canViewOrdersForOwnPrescription($adminInfo)) {
return self::isPrescriptionOrderPrescriber((int) $row->prescription_id, $adminId);
}
return (int) $row->creator_id === $adminId;
return false;
}
/**
* 撤回:仅创建人或全量角色(避免开方医生仅「查看医助代建单」权限误撤他人单子)
*
* @param PrescriptionOrder $row
*/
public static function canWithdrawOrder($row, int $adminId, array $adminInfo): bool
{
return self::canAccessOrder($row, $adminId, $adminInfo);
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
return true;
}
return (int) $row->creator_id === $adminId;
}
public static function canViewInternalCost(array $adminInfo): bool
@@ -2040,6 +2077,12 @@ class PrescriptionOrderLogic
if ((int) ($item['creator_id'] ?? 0) === $adminId) {
return true;
}
if (
self::canViewOrdersForOwnPrescription($adminInfo)
&& self::isPrescriptionOrderPrescriber((int) ($item['prescription_id'] ?? 0), $adminId)
) {
return true;
}
$did = (int) ($item['diagnosis_id'] ?? 0);
$aid = (int) ($assistantByDiag[$did] ?? 0);