diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 4dad5244..572b18ce 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -216,6 +216,22 @@ @keyup.enter="resetPage" /> + + + + + { + const u = userStore.userInfo + if (!u || Number(u.root) === 1) return false + const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] + if (!ids.includes(ORDER_PLACER_ROLE_ID)) return false + return !ORDER_PLACER_EXEMPT_ROLE_IDS.some((rid) => ids.includes(rid)) +}) + +/** 超管 / 经理 / 管理员:可按任意下单人筛选 */ +const canSearchAnyAuditAdmin = computed(() => { + const u = userStore.userInfo + if (!u) return false + if (Number(u.root) === 1) return true + const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] + return ORDER_PLACER_EXEMPT_ROLE_IDS.some((rid) => ids.includes(rid)) +}) + +/** 是否展示「下单人」筛选(下单角色 + 经理/管理员/超管) */ +const showAuditAdminFilter = computed( + () => isOrderPlacerAuditFilterRestricted.value || canSearchAnyAuditAdmin.value +) + +/** 下单人下拉(lists extend.audit_admin_options,下单角色用户) */ +const auditAdminOptions = ref>([]) + /** * 是否展示「处方审核」筛选:纯医助(含角色 2 且不具备处方审核角色)仅能用「支付单审核」筛选 */ @@ -2928,7 +2974,9 @@ const queryParams = reactive({ /** 服务渠道:'' 不限;'0' 未指派(库内 '' 或 '0') */ service_channel: '' as '' | '0', /** 列表排除履约已取消(4):重点看板「待处方审核」等用 */ - exclude_fulfillment_cancelled: 0 as number + exclude_fulfillment_cancelled: 0 as number, + /** 下单人(关联操作日志 audit_rx_* / audit_pay_*) */ + audit_admin_id: '' as number | '' }) const rxAuditTabs = [ @@ -3041,6 +3089,11 @@ function normalizePrescriptionOrderListQuery(params: Record): R } if (!p.order_no) delete p.order_no if (!String(p.patient_keyword || '').trim()) delete p.patient_keyword + if (p.audit_admin_id === '' || p.audit_admin_id === undefined || p.audit_admin_id === null) { + delete p.audit_admin_id + } else { + p.audit_admin_id = Number(p.audit_admin_id) + } if (!String(p.express_keyword || '').trim()) delete p.express_keyword if (!p.express_company || p.express_company === '') delete p.express_company if (p.service_channel === '' || p.service_channel === undefined || p.service_channel === null) { @@ -3076,6 +3129,24 @@ const { pager, getLists, resetPage, resetParams } = usePaging({ params: queryParams }) +watch( + () => (pager.extend as Record | undefined)?.audit_admin_options, + (raw) => { + if (!Array.isArray(raw)) return + auditAdminOptions.value = raw + .map((r: unknown) => { + if (r == null || typeof r !== 'object') return null + const row = r as Record + const id = Number(row.id) + const name = String(row.name ?? '').trim() + if (!Number.isFinite(id) || id <= 0 || name === '') return null + return { id, name } + }) + .filter((x): x is { id: number; name: string } => x != null) + }, + { immediate: true } +) + /** 业绩看板等入口:URL 携带创建区间、诊单医助部门 / 医助 id */ function applyRouteQueryToPrescriptionOrderList() { const q = route.query @@ -3267,6 +3338,7 @@ function handleReset() { queryParams.payment_slip_audit_status = '' queryParams.supply_mode = '' queryParams.service_channel = '' + queryParams.audit_admin_id = '' queryParams.exclude_fulfillment_cancelled = 0 resetParams() } diff --git a/admin/src/views/consumer/prescription/order_list_h5.vue b/admin/src/views/consumer/prescription/order_list_h5.vue index b8ccbc0e..403e43f9 100644 --- a/admin/src/views/consumer/prescription/order_list_h5.vue +++ b/admin/src/views/consumer/prescription/order_list_h5.vue @@ -234,6 +234,22 @@ @keyup.enter="resetPage" /> + + + + + { + const u = userStore.userInfo + if (!u || Number(u.root) === 1) return false + const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] + if (!ids.includes(ORDER_PLACER_ROLE_ID)) return false + return !ORDER_PLACER_EXEMPT_ROLE_IDS.some((rid) => ids.includes(rid)) +}) + +/** 超管 / 经理 / 管理员:可按任意下单人筛选 */ +const canSearchAnyAuditAdmin = computed(() => { + const u = userStore.userInfo + if (!u) return false + if (Number(u.root) === 1) return true + const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] + return ORDER_PLACER_EXEMPT_ROLE_IDS.some((rid) => ids.includes(rid)) +}) + +/** 是否展示「下单人」筛选(下单角色 + 经理/管理员/超管) */ +const showAuditAdminFilter = computed( + () => isOrderPlacerAuditFilterRestricted.value || canSearchAnyAuditAdmin.value +) + +/** 下单人下拉(lists extend.audit_admin_options,下单角色用户) */ +const auditAdminOptions = ref>([]) + /** * 是否展示「处方审核」筛选:纯医助(含角色 2 且不具备处方审核角色)仅能用「支付单审核」筛选 */ @@ -2673,7 +2719,9 @@ const queryParams = reactive({ prescription_audit_status: '' as number | '', payment_slip_audit_status: '' as number | '', /** 供货方式:甘草(已传甘草药方单号)/ 自营(无甘草单号) */ - supply_mode: '' as '' | 'gancao' | 'self' + supply_mode: '' as '' | 'gancao' | 'self', + /** 下单人(关联操作日志 audit_rx_* / audit_pay_*) */ + audit_admin_id: '' as number | '' }) const rxAuditTabs = [ @@ -2767,6 +2815,11 @@ async function fetchLists(params: Record) { } if (!p.order_no) delete p.order_no if (!String(p.patient_keyword || '').trim()) delete p.patient_keyword + if (p.audit_admin_id === '' || p.audit_admin_id === undefined || p.audit_admin_id === null) { + delete p.audit_admin_id + } else { + p.audit_admin_id = Number(p.audit_admin_id) + } if (!String(p.express_keyword || '').trim()) delete p.express_keyword if (!p.express_company || p.express_company === '') delete p.express_company if (!String(p.start_time || '').trim() || !String(p.end_time || '').trim()) { @@ -2789,6 +2842,24 @@ const { pager, getLists, resetPage, resetParams } = usePaging({ params: queryParams }) +watch( + () => (pager.extend as Record | undefined)?.audit_admin_options, + (raw) => { + if (!Array.isArray(raw)) return + auditAdminOptions.value = raw + .map((r: unknown) => { + if (r == null || typeof r !== 'object') return null + const row = r as Record + const id = Number(row.id) + const name = String(row.name ?? '').trim() + if (!Number.isFinite(id) || id <= 0 || name === '') return null + return { id, name } + }) + .filter((x): x is { id: number; name: string } => x != null) + }, + { immediate: true } +) + /** 选医助部门后,医助下拉仅显示该部门及子部门成员(与后台含子级一致) */ const filteredAssistantOptions = computed(() => { const deptRaw = queryParams.assistant_dept_id @@ -2950,6 +3021,7 @@ function handleReset() { queryParams.prescription_audit_status = '' queryParams.payment_slip_audit_status = '' queryParams.supply_mode = '' + queryParams.audit_admin_id = '' resetParams() } diff --git a/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php b/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php index 23b6a983..a4ba7490 100755 --- a/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php +++ b/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php @@ -14,10 +14,12 @@ use app\common\lists\ListsExtendInterface; use app\common\lists\ListsSearchInterface; use app\common\lists\Traits\HasDataScopeFilter; use app\common\model\Order; -use app\common\model\tcm\Diagnosis; +use app\common\model\auth\Admin; use app\common\model\auth\AdminDept; +use app\common\model\tcm\Diagnosis; use app\common\model\tcm\Prescription; use app\common\model\tcm\PrescriptionOrder; +use app\common\model\tcm\PrescriptionOrderLog; use app\common\model\tcm\PrescriptionOrderPayOrder; use app\common\model\ExpressTracking; use app\common\service\gancao\GancaoScmRecipelService; @@ -106,6 +108,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn $this->applyExpressKeywordFilter($query); $this->applyServiceChannelFilter($query); $this->applySupplyModeFilter($query); + $this->applyAuditAdminFilter($query); if (!$this->shouldBypassListVisibilityForDiagnosisEdit()) { // 业绩看板按部门点「合计业绩」/复诊下钻:部门或数据域内医助筛选已收口,勿再叠「仅本人订单」 if (!$this->shouldSkipCreatorOnlyForYejiDrawer()) { @@ -121,6 +124,105 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn YejiStatsLogic::applyYejiDrawerChannelFilterToPrescriptionOrderQuery($query, $this->params); } + /** + * 按下单人筛选:关联操作日志中该用户执行的处方/支付单审核记录 + */ + private function applyAuditAdminFilter($query): void + { + $auditAdminId = (int) ($this->params['audit_admin_id'] ?? 0); + $keyword = trim((string) ($this->params['audit_admin_keyword'] ?? '')); + + if ($auditAdminId <= 0 && $keyword === '') { + return; + } + + $restricted = PrescriptionOrderLogic::isOrderPlacerAuditFilterRestricted($this->adminInfo); + if ($restricted) { + $selfId = (int) $this->adminId; + if ($auditAdminId > 0 && $auditAdminId !== $selfId) { + $query->whereRaw('0 = 1'); + + return; + } + if ($keyword !== '') { + $selfName = trim((string) ($this->adminInfo['name'] ?? '')); + if ($selfName === '' + || (mb_stripos($selfName, $keyword) === false && mb_stripos($keyword, $selfName) === false)) { + $query->whereRaw('0 = 1'); + + return; + } + } + $auditAdminId = $selfId; + } elseif ($auditAdminId <= 0 && $keyword !== '') { + $roleIds = Config::get('project.prescription_order_placer_role_ids', [6]); + $roleIds = array_values(array_unique(array_filter(array_map( + 'intval', + is_array($roleIds) ? $roleIds : [] + ), static fn(int $id): bool => $id > 0))); + if ($roleIds === []) { + $roleIds = [6]; + } + $adminIds = Admin::alias('a') + ->join('admin_role ar', 'a.id = ar.admin_id') + ->whereLike('a.name', '%' . $keyword . '%') + ->whereIn('ar.role_id', $roleIds) + ->where('a.disable', 0) + ->whereNull('a.delete_time') + ->column('a.id'); + $adminIds = array_values(array_filter(array_map('intval', $adminIds), static function (int $id): bool { + return $id > 0; + })); + if ($adminIds === []) { + $query->whereRaw('0 = 1'); + + return; + } + $this->applyAuditedByAdminIdsFilter($query, $adminIds); + + return; + } + + if ($auditAdminId <= 0) { + return; + } + if (!PrescriptionOrderLogic::adminHasPlacerRole($auditAdminId)) { + $query->whereRaw('0 = 1'); + + return; + } + + $this->applyAuditedByAdminIdsFilter($query, [$auditAdminId]); + } + + /** + * @param int[] $adminIds + */ + private function applyAuditedByAdminIdsFilter($query, array $adminIds): void + { + $adminIds = array_values(array_unique(array_filter(array_map('intval', $adminIds), static function (int $id): bool { + return $id > 0; + }))); + if ($adminIds === []) { + $query->whereRaw('0 = 1'); + + return; + } + + $logTbl = (new PrescriptionOrderLog())->getTable(); + $poTbl = (new PrescriptionOrder())->getTable(); + $actions = PrescriptionOrderLogic::prescriptionOrderAuditLogActions(); + $actionIn = implode(',', array_map(static function (string $a): string { + return "'" . addslashes($a) . "'"; + }, $actions)); + $idIn = implode(',', $adminIds); + + $query->whereExists( + "SELECT 1 FROM `{$logTbl}` l WHERE l.`prescription_order_id` = `{$poTbl}`.`id`" + . " AND l.`admin_id` IN ({$idIn}) AND l.`action` IN ({$actionIn})" + ); + } + /** * 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_time;revisit_slot=0 为全部复诊分项合计)。 * admin 维度按订单创建人 o.creator_id(与部门表复诊列、排行榜复诊列同口径)。 @@ -388,6 +490,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn $query = PrescriptionOrder::where($this->searchWhereWithoutCreateTimeRange())->whereNull('delete_time'); $this->applyDoctorAssistantFilters($query); $this->applyPatientKeywordFilter($query); + $this->applyAuditAdminFilter($query); if ($this->shouldSkipCreatorOnlyForYejiDrawer()) { $this->applyDataScopeForPrescriptionOrder($query); } elseif (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) { @@ -634,6 +737,10 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn 'focus_pending_pay_count' => $focus['pending_pay'], 'focus_pending_ship_count' => $focus['pending_ship'], 'focus_risk_count' => $focus['risk'], + /** 1=「下单」角色审核人筛选仅本人 */ + 'list_placer_audit_filter_restricted' => PrescriptionOrderLogic::isOrderPlacerAuditFilterRestricted($this->adminInfo) ? 1 : 0, + /** 下单人下拉(下单角色用户) */ + 'audit_admin_options' => PrescriptionOrderLogic::listAuditAdminOptions($this->adminId, $this->adminInfo), ]; $yejiConsult = $this->computeYejiDrawerConsultCountIfRequested(); if ($yejiConsult !== null) { diff --git a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php index 2ff76319..f77663b4 100755 --- a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php +++ b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php @@ -115,6 +115,123 @@ class PrescriptionOrderLogic return self::roleIntersect($adminInfo, is_array($roles) ? $roles : []); } + /** + * 「下单」角色:列表「审核人」筛选仅允许查本人审核过的订单(经理/管理员等豁免) + */ + public static function isOrderPlacerAuditFilterRestricted(array $adminInfo): bool + { + if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { + return false; + } + $placer = Config::get('project.prescription_order_placer_role_ids', [6]); + if (!self::roleIntersect($adminInfo, is_array($placer) ? $placer : [])) { + return false; + } + $exempt = Config::get('project.prescription_order_placer_exempt_role_ids', [3, 8]); + + return !self::roleIntersect($adminInfo, is_array($exempt) ? $exempt : []); + } + + /** @return string[] */ + public static function prescriptionOrderAuditLogActions(): array + { + return ['audit_rx_approve', 'audit_rx_reject', 'audit_pay_approve', 'audit_pay_reject']; + } + + /** 是否展示「下单人」筛选(下单 / 经理 / 管理员 / 超管) */ + public static function shouldShowAuditAdminFilter(array $adminInfo): bool + { + if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { + return true; + } + if (self::isOrderPlacerAuditFilterRestricted($adminInfo)) { + return true; + } + $exempt = Config::get('project.prescription_order_placer_exempt_role_ids', [3, 8]); + + return self::roleIntersect($adminInfo, is_array($exempt) ? $exempt : []); + } + + /** + * 下单人下拉:直接取「下单」角色下的后台用户(project.prescription_order_placer_role_ids) + * + * @return array + */ + public static function listAuditAdminOptions(int $adminId, array $adminInfo): array + { + if ($adminId <= 0 || !self::shouldShowAuditAdminFilter($adminInfo)) { + return []; + } + + $roleIds = Config::get('project.prescription_order_placer_role_ids', [6]); + $roleIds = array_values(array_unique(array_filter(array_map( + 'intval', + is_array($roleIds) ? $roleIds : [] + ), static fn(int $id): bool => $id > 0))); + if ($roleIds === []) { + $roleIds = [6]; + } + + if (self::isOrderPlacerAuditFilterRestricted($adminInfo)) { + $name = trim((string) ($adminInfo['name'] ?? '')); + + return [ + [ + 'id' => $adminId, + 'name' => $name !== '' ? $name : ('管理员#' . $adminId), + ], + ]; + } + + $rows = Db::name('admin') + ->alias('a') + ->join('admin_role ar', 'a.id = ar.admin_id') + ->whereIn('ar.role_id', $roleIds) + ->where('a.disable', 0) + ->whereNull('a.delete_time') + ->field(['a.id', 'a.name']) + ->distinct(true) + ->order('a.name', 'asc') + ->select() + ->toArray(); + + $out = []; + foreach ($rows as $r) { + $id = (int) ($r['id'] ?? 0); + if ($id <= 0) { + continue; + } + $name = trim((string) ($r['name'] ?? '')); + if ($name === '') { + $name = '管理员#' . $id; + } + $out[] = ['id' => $id, 'name' => $name]; + } + + return $out; + } + + /** 指定后台账号是否拥有「下单」角色 */ + public static function adminHasPlacerRole(int $adminId): bool + { + if ($adminId <= 0) { + return false; + } + $roleIds = Config::get('project.prescription_order_placer_role_ids', [6]); + $roleIds = array_values(array_unique(array_filter(array_map( + 'intval', + is_array($roleIds) ? $roleIds : [] + ), static fn(int $id): bool => $id > 0))); + if ($roleIds === []) { + $roleIds = [6]; + } + + return Db::name('admin_role') + ->where('admin_id', $adminId) + ->whereIn('role_id', $roleIds) + ->count() > 0; + } + /** * 菜单权限:非全量角色可额外查看「关联处方开方人为本账号」的业务订单(医助代建单) * diff --git a/server/config/project.php b/server/config/project.php index d082a4db..88b8505a 100755 --- a/server/config/project.php +++ b/server/config/project.php @@ -131,6 +131,11 @@ return [ // 业务订单列表金额统计:医助角色 ID;非支付审核白名单的医助仅统计其诊单 assistant_id=本人 的业绩 'prescription_order_stats_assistant_role_id' => 2, + // 业务订单列表:「下单」角色(id=6)可按审核人姓名筛选本人审核过的订单 + 'prescription_order_placer_role_ids' => [6], + // 同时拥有以下角色时可按任意审核人筛选(如经理、管理员) + 'prescription_order_placer_exempt_role_ids' => [3, 8], + /* * 数据隔离(按部门):全局可用,角色上的 data_scope 控制范围 * 1=全部 2=本部门及下级 3=仅本部门 4=仅本人