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=仅本人