更新
This commit is contained in:
@@ -42,3 +42,12 @@ export function yejiStatsLeaderboard(params: {
|
||||
}) {
|
||||
return request.get({ url: '/stats.yejiStats/leaderboard', params })
|
||||
}
|
||||
|
||||
/** 医生统计(start/end、channel_code;含数据范围;不按展示部门 dept_ids 筛选) */
|
||||
export function doctorDailyStatsOverview(params: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
channel_code?: string
|
||||
}) {
|
||||
return request.get({ url: '/stats.doctorDailyStats/overview', params })
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//引入柱状图图表,图表后缀都为 Chart
|
||||
import {
|
||||
BarChart,
|
||||
FunnelChart,
|
||||
LineChart,
|
||||
MapChart,
|
||||
PictorialBarChart,
|
||||
@@ -43,6 +44,7 @@ echarts.use([
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
BarChart,
|
||||
FunnelChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
|
||||
@@ -2258,6 +2258,7 @@
|
||||
|
||||
<script lang="ts" setup name="prescriptionOrderList">
|
||||
import { computed, onMounted, reactive, ref, nextTick, watch, defineAsyncComponent } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Refresh, Loading, ArrowDown, InfoFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import {
|
||||
@@ -2296,6 +2297,7 @@ import useUserStore from '@/stores/modules/user'
|
||||
const TcmDiagnosisEditView = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
|
||||
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
|
||||
const diagnosisViewRef = ref<{
|
||||
openViewOnly: (id: number) => Promise<void>
|
||||
@@ -2662,6 +2664,34 @@ const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
/** 业绩看板等入口:URL 携带创建区间、诊单医助部门 / 医助 id */
|
||||
function applyRouteQueryToPrescriptionOrderList() {
|
||||
const q = route.query
|
||||
const normDate = (v: unknown) => {
|
||||
const s = v != null ? String(v).trim() : ''
|
||||
if (!s) return ''
|
||||
return s.length >= 10 ? s.slice(0, 10) : s
|
||||
}
|
||||
const st = normDate(q.start_time)
|
||||
const et = normDate(q.end_time)
|
||||
if (st) queryParams.start_time = st
|
||||
if (et) queryParams.end_time = et
|
||||
const ad = q.assistant_dept_id
|
||||
if (ad !== undefined && ad !== null && String(ad) !== '') {
|
||||
const n = Number(ad)
|
||||
if (Number.isFinite(n) && n > 0) {
|
||||
queryParams.assistant_dept_id = n
|
||||
}
|
||||
}
|
||||
const ai = q.assistant_id
|
||||
if (ai !== undefined && ai !== null && String(ai) !== '') {
|
||||
const n = Number(ai)
|
||||
if (Number.isFinite(n) && n > 0) {
|
||||
queryParams.assistant_id = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 选医助部门后,医助下拉仅显示该部门及子部门成员(与后台含子级一致) */
|
||||
const filteredAssistantOptions = computed(() => {
|
||||
const deptRaw = queryParams.assistant_dept_id
|
||||
@@ -2732,6 +2762,9 @@ const listStats = computed(() => {
|
||||
'统计口径:支付单审核白名单角色,按全量订单;与下方筛选项(医生/医助/医助部门/患者等,不含「创建时间」)一致'
|
||||
} else if (scope === 'assistant') {
|
||||
scopeHint = '统计口径:仅计诊单医助为本人且满足下方其它筛选项的订单'
|
||||
} else if (scope === 'own_rx') {
|
||||
scopeHint =
|
||||
'统计口径:本人创建的订单 + 关联处方开方人为本人的订单(含医助代建);与下方筛选项(不含「创建时间」)一致'
|
||||
}
|
||||
const hasCustomTime =
|
||||
String(queryParams.start_time || '').trim() !== '' && String(queryParams.end_time || '').trim() !== ''
|
||||
@@ -3998,6 +4031,8 @@ onMounted(async () => {
|
||||
await loadRegionData()
|
||||
await loadServicePackageOptions()
|
||||
await loadDoctorAssistantOptions()
|
||||
applyRouteQueryToPrescriptionOrderList()
|
||||
await nextTick()
|
||||
getLists()
|
||||
})
|
||||
|
||||
|
||||
@@ -2789,6 +2789,9 @@ const listStats = computed(() => {
|
||||
'统计口径:支付单审核白名单角色,按全量订单;与下方筛选项(医生/医助/医助部门/患者等,不含「创建时间」)一致'
|
||||
} else if (scope === 'assistant') {
|
||||
scopeHint = '统计口径:仅计诊单医助为本人且满足下方其它筛选项的订单'
|
||||
} else if (scope === 'own_rx') {
|
||||
scopeHint =
|
||||
'统计口径:本人创建的订单 + 关联处方开方人为本人的订单(含医助代建);与下方筛选项(不含「创建时间」)一致'
|
||||
}
|
||||
const hasCustomTime =
|
||||
String(queryParams.start_time || '').trim() !== '' && String(queryParams.end_time || '').trim() !== ''
|
||||
|
||||
+2122
-134
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,7 @@ const formData = reactive({
|
||||
name: '',
|
||||
desc: '',
|
||||
sort: 0,
|
||||
data_scope: 1,
|
||||
menu_id: [] as any[]
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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=3,appointment_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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ use think\facade\Config;
|
||||
* - DEPT (3) = 仅本部门(取 admin 全部部门的并集,不含子孙)
|
||||
* - SELF (4) = 仅本人
|
||||
*
|
||||
* 多角色时取「范围最大」= data_scope 最小值。
|
||||
* 多角色时取「最严格」可见范围 = data_scope 最大值(1=全部 … 4=仅本人),
|
||||
* 与常见「数据权限取交集」一致,避免挂了一个「全部」角色就把其它角色的部门范围冲掉。
|
||||
* root 管理员固定为 ALL。未挂任何部门时,范围退化为 SELF(可由 config 关闭)。
|
||||
*
|
||||
* 关键返回:`getVisibleAdminIds` 返回 int[](可见 admin_id 集合)或 null(ALL = 不过滤)。
|
||||
@@ -42,10 +43,7 @@ class DataScopeService
|
||||
if ((int) ($adminInfo['root'] ?? 0) === 1) {
|
||||
return self::SCOPE_ALL;
|
||||
}
|
||||
$roleIds = array_values(array_filter(array_map(
|
||||
'intval',
|
||||
is_array($adminInfo['role_id'] ?? null) ? $adminInfo['role_id'] : []
|
||||
)));
|
||||
$roleIds = self::normalizeRoleIds($adminInfo['role_id'] ?? null);
|
||||
$exempt = array_map('intval', Config::get('project.data_scope.exempt_roles', []) ?: []);
|
||||
if ($roleIds !== [] && array_intersect($roleIds, $exempt) !== []) {
|
||||
return self::SCOPE_ALL;
|
||||
@@ -59,11 +57,50 @@ class DataScopeService
|
||||
$scopes = array_values(array_filter(array_map('intval', $scopes), static function (int $v): bool {
|
||||
return $v >= self::SCOPE_ALL && $v <= self::SCOPE_SELF;
|
||||
}));
|
||||
// 角色存在但库中无有效 data_scope(缺失/脏数据/已删角色):宁可收窄到「仅本人」,避免误放开到全站
|
||||
if ($scopes === []) {
|
||||
return self::SCOPE_ALL;
|
||||
return self::SCOPE_SELF;
|
||||
}
|
||||
|
||||
return (int) min($scopes);
|
||||
return (int) max($scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一解析 token/cache 中的 role_id(数组 | 单整数 | JSON 字符串)。
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private static function normalizeRoleIds(mixed $raw): array
|
||||
{
|
||||
if ($raw === null || $raw === '') {
|
||||
return [];
|
||||
}
|
||||
if (\is_int($raw) || \is_float($raw)) {
|
||||
$v = (int) $raw;
|
||||
|
||||
return $v > 0 ? [$v] : [];
|
||||
}
|
||||
if (\is_string($raw) && is_numeric($raw)) {
|
||||
$v = (int) $raw;
|
||||
|
||||
return $v > 0 ? [$v] : [];
|
||||
}
|
||||
if (\is_string($raw)) {
|
||||
$decoded = json_decode($raw, true);
|
||||
if (\is_array($decoded)) {
|
||||
$raw = $decoded;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
if (!\is_array($raw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
static fn ($v): int => (int) $v,
|
||||
$raw
|
||||
), static fn (int $v): bool => $v > 0));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -126,7 +126,7 @@ return [
|
||||
/*
|
||||
* 数据隔离(按部门):全局可用,角色上的 data_scope 控制范围
|
||||
* 1=全部 2=本部门及下级 3=仅本部门 4=仅本人
|
||||
* root 管理员永远视为「全部」;多角色取最大范围(数值最小)
|
||||
* root 管理员永远视为「全部」;多角色取最严格范围(数值最大,与 DataScopeService 一致)
|
||||
*/
|
||||
'data_scope' => [
|
||||
// 顶层总开关:false 时所有列表不应用按部门的数据隔离(仍保留老的白名单规则)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 业绩看板页内「医生日统计」接口权限:挂在 fans/yeji 同级菜单 id=293 下(仅 API,不新增独立页面)
|
||||
-- 幂等:按 perms 判重
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
293, 'A', '业绩-医生日统计', '', 7,
|
||||
'stats.doctorDailyStats/overview', '', '', '', '',
|
||||
1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE EXISTS (SELECT 1 FROM `zyt_system_menu` p WHERE p.id = 293)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.perms = 'stats.doctorDailyStats/overview'
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 处方业务订单:医生可查看医助代建的、关联处方开方人为本人的业务单
|
||||
-- 执行前确认表前缀为 zyt_;若已有相同 perms 则跳过。
|
||||
-- 需在「角色权限」中为医生等角色勾选本按钮权限。
|
||||
|
||||
SET @po_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/lists' LIMIT 1);
|
||||
|
||||
INSERT INTO zyt_system_menu (
|
||||
pid, type, name, icon, sort, perms, paths, component,
|
||||
selected, params, is_cache, is_show, is_disable, create_time, update_time
|
||||
)
|
||||
SELECT
|
||||
@po_menu_id, 'A', '查看本人处方的业务订单(医助代建)', '', 53,
|
||||
'tcm.prescriptionOrder/viewOrdersForOwnPrescription', '', '',
|
||||
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE @po_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/viewOrdersForOwnPrescription');
|
||||
Reference in New Issue
Block a user