Files
zyt/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php
T
2026-05-07 18:21:00 +08:00

896 lines
37 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\adminapi\lists\tcm;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\dept\DeptLogic;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
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\AdminDept;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
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
{
use HasDataScopeFilter;
public function setSearch(): array
{
return [
'=' => ['prescription_id', 'diagnosis_id', 'fulfillment_status', 'prescription_audit_status', 'payment_slip_audit_status'],
'%like%' => ['order_no'],
'between_time' => 'create_time',
];
}
/**
* 列表/统计条数:含全部搜索条件(含创建时间区间)
*/
private function buildFilteredQuery(): Query
{
$query = PrescriptionOrder::where($this->searchWhere)->whereNull('delete_time');
$this->applyPermissionAndExtraFilters($query);
return $query;
}
/**
* 今日汇总:与列表相同的「非时间」条件(医生/医助/患者/状态/单号等),不含创建时间区间,避免与「今日」语义重复
*
* @return array<int, array{0: string, 1: string, 2: mixed}>
*/
private function searchWhereWithoutCreateTimeRange(): array
{
return array_values(array_filter(
$this->searchWhere,
static function ($w): bool {
if (!\is_array($w) || \count($w) < 2) {
return true;
}
if (($w[0] ?? '') === 'create_time' && ($w[1] ?? '') === 'between') {
return false;
}
return true;
}
));
}
/**
* 重点看板统计:去掉审核/履约状态筛选,保留其它筛选(时间、医生、医助、患者、供货方式等)
*
* @return array<int, array{0: string, 1: string, 2: mixed}>
*/
private function searchWhereWithoutStatusFilters(): array
{
return array_values(array_filter(
$this->searchWhere,
static function ($w): bool {
if (!\is_array($w) || \count($w) < 2) {
return true;
}
$field = (string) ($w[0] ?? '');
if (!in_array($field, ['prescription_audit_status', 'payment_slip_audit_status', 'fulfillment_status'], true)) {
return true;
}
$op = strtolower((string) ($w[1] ?? ''));
return $op !== '=';
}
));
}
private function applyPermissionAndExtraFilters($query): void
{
$this->applyDoctorAssistantFilters($query);
$this->applyPatientKeywordFilter($query);
$this->applyPatientIdFilter($query);
$this->applyExpressCompanyFilter($query);
$this->applyExpressKeywordFilter($query);
$this->applySupplyModeFilter($query);
$this->applyCreatorOrOwnPrescriptionVisibility($query);
$this->applyDataScopeForPrescriptionOrder($query);
// 业绩看板侧栏等:不展示履约已取消(4),与看板合计业绩 NULL 安全口径一致
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, '');
}
$this->applyYejiErCenterRevisitOrderFilter($query);
YejiStatsLogic::applyYejiDrawerChannelFilterToPrescriptionOrderQuery($query, $this->params);
}
/**
* 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_timerevisit_slot=0 为全部复诊分项合计)。
*/
private function applyYejiErCenterRevisitOrderFilter($query): void
{
if ((int) ($this->params['yeji_er_center_revisit_only'] ?? 0) !== 1) {
return;
}
$assistantId = (int) ($this->params['assistant_id'] ?? 0);
$slot = (int) ($this->params['yeji_er_center_revisit_slot'] ?? 0);
if ($assistantId <= 0) {
return;
}
$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;
}
$ids = DeptLogic::listErCenterRevisitOrderIdsForAssistant(
$assistantId,
$slot,
(int) $t0,
(int) $t1
);
if ($ids === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereIn('id', $ids);
}
/**
* 按快递公司筛选(点击快捷标签:顺丰 / 京东)
*
* 说明:主表 express_company 常为 auto/空;快递100 回写承运商在 zyt_express_tracking,需一并匹配。
*/
private function applyExpressCompanyFilter($query): void
{
$company = strtolower(trim((string) ($this->params['express_company'] ?? '')));
if (!in_array($company, ['sf', 'jd'], true)) {
return;
}
$poTbl = (new PrescriptionOrder())->getTable();
$etTbl = (new ExpressTracking())->getTable();
if ($company === 'sf') {
$po = "(LOWER(TRIM(IFNULL(`{$poTbl}`.`express_company`,''))) IN ('sf','shunfeng')"
. " OR `{$poTbl}`.`express_company` LIKE '%顺丰%'"
. " OR (TRIM(IFNULL(`{$poTbl}`.`tracking_number`,'')) <> ''"
. " AND (`{$poTbl}`.`tracking_number` LIKE 'SF%' OR `{$poTbl}`.`tracking_number` LIKE 'sf%')))";
$et = "EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL"
. " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'"
. " AND (LOWER(TRIM(IFNULL(et.`express_company`,''))) IN ('sf','shunfeng')"
. " OR et.`express_company_name` LIKE '%顺丰%'"
. " OR (TRIM(IFNULL(et.`tracking_number`,'')) <> ''"
. " AND (et.`tracking_number` LIKE 'SF%' OR et.`tracking_number` LIKE 'sf%'))))";
$query->whereRaw("(($po) OR ($et))");
return;
}
$po = "(LOWER(TRIM(IFNULL(`{$poTbl}`.`express_company`,''))) IN ('jd','jingdong')"
. " OR `{$poTbl}`.`express_company` LIKE '%京东%'"
. " OR (TRIM(IFNULL(`{$poTbl}`.`tracking_number`,'')) <> ''"
. " AND (`{$poTbl}`.`tracking_number` LIKE 'JD%' OR `{$poTbl}`.`tracking_number` LIKE 'jd%'"
. " OR `{$poTbl}`.`tracking_number` LIKE 'JDVB%' OR `{$poTbl}`.`tracking_number` LIKE 'jdvb%')))";
$et = "EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL"
. " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'"
. " AND (LOWER(TRIM(IFNULL(et.`express_company`,''))) IN ('jd','jingdong')"
. " OR et.`express_company_name` LIKE '%京东%'"
. " OR (TRIM(IFNULL(et.`tracking_number`,'')) <> ''"
. " AND (et.`tracking_number` LIKE 'JD%' OR et.`tracking_number` LIKE 'jd%'"
. " OR et.`tracking_number` LIKE 'JDVB%' OR et.`tracking_number` LIKE 'jdvb%'))))";
$query->whereRaw("(($po) OR ($et))");
}
/**
* 供货方式:甘草(已上传甘草药方单号)/ 自营(无甘草单号,走药房直发等)
*
* 入参 supply_modegancao | self,空表示不限
*/
private function applySupplyModeFilter($query): void
{
$mode = strtolower(trim((string) ($this->params['supply_mode'] ?? '')));
if ($mode === '') {
return;
}
if ($mode === 'gancao') {
$query->whereRaw("TRIM(IFNULL(`gancao_reciperl_order_no`,'')) <> ''");
return;
}
if ($mode === 'self') {
$query->whereRaw("TRIM(IFNULL(`gancao_reciperl_order_no`,'')) = ''");
return;
}
}
/**
* 非「业务订单全量」角色:默认仅本人创建;若持有 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
*/
private function applyDataScopeForPrescriptionOrder($query): void
{
if (!$this->dataScopeShouldApply()) {
return;
}
$ids = $this->getDataScopeVisibleAdminIds();
if ($ids === null) {
return;
}
if ($ids === []) {
$query->whereRaw('0 = 1');
return;
}
$poTbl = (new PrescriptionOrder())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$inList = implode(',', $ids);
$query->where(function ($q) use ($poTbl, $diagTbl, $inList) {
$q->whereIn('creator_id', explode(',', $inList));
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` "
. "AND dg.`delete_time` IS NULL AND dg.`assistant_id` IN ({$inList}))"
);
});
}
/**
* 医助:仅「诊单医助=本人」的订单计业绩
*/
private function applyAssistantDiagnosisOnlyFilter($query): void
{
$poTbl = (new PrescriptionOrder())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$aid = (int) $this->adminId;
$query->whereExists(
"SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$aid}"
);
}
/**
* 统计用查询骨架:不含 create_time 区间;其它筛选项 + 统计专用数据域(不重复 where)
*
* @param bool $applyYejiDrawerChannel 是否应用业绩侧栏渠道收窄(合计业绩统计须传 false)
*/
private function buildBaseQueryForStats(bool $applyYejiDrawerChannel = true): Query
{
$query = PrescriptionOrder::where($this->searchWhereWithoutCreateTimeRange())->whereNull('delete_time');
$this->applyDoctorAssistantFilters($query);
$this->applyPatientKeywordFilter($query);
if (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) {
$this->applyDataScopeForPrescriptionOrder($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);
}
}
// 与看板「合计业绩」一致:NULL 安全剔除履约已取消(4),勿用 `<>` 以免误丢 NULL 状态行
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, '');
}
if ($applyYejiDrawerChannel) {
YejiStatsLogic::applyYejiDrawerChannelFilterToPrescriptionOrderQuery($query, $this->params);
}
return $query;
}
/**
* 统计所对应的时间:与入参 start_time / end_time 一致;未传则按自然日「今天」
*
* @return array{0: int, 1: int, 2: string, 3: string}
*/
private function resolveStatsTimeWindow(): array
{
$st = trim((string) ($this->params['start_time'] ?? ''));
$et = trim((string) ($this->params['end_time'] ?? ''));
if ($st !== '' && $et !== '') {
$t0 = strtotime($st);
$t1 = strtotime($et);
if ($t0 !== false && $t1 !== false) {
return [(int) $t0, (int) $t1, $st, $et];
}
}
$t0 = strtotime(date('Y-m-d 00:00:00'));
$t1 = strtotime(date('Y-m-d 23:59:59'));
if ($t0 === false || $t1 === false) {
return [0, 0, '', ''];
}
return [
(int) $t0,
(int) $t1,
date('Y-m-d 00:00:00'),
date('Y-m-d 23:59:59'),
];
}
public function lists(): array
{
$query = $this->buildFilteredQuery();
$lists = $query
->order('id', 'desc')
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
$poIds = array_values(array_filter(array_map('intval', array_column($lists, 'id'))));
$paidSumByPo = [];
if ($poIds !== []) {
$links = PrescriptionOrderPayOrder::whereIn('prescription_order_id', $poIds)
->field(['prescription_order_id', 'pay_order_id'])
->select()
->toArray();
$byPo = [];
foreach ($links as $l) {
$poid = (int) ($l['prescription_order_id'] ?? 0);
$oid = (int) ($l['pay_order_id'] ?? 0);
if ($poid > 0 && $oid > 0) {
$byPo[$poid][] = $oid;
}
}
$allOids = [];
foreach ($byPo as $oids) {
$allOids = array_merge($allOids, $oids);
}
$allOids = array_values(array_unique($allOids));
$amountByOid = [];
if ($allOids !== []) {
$amountByOid = Order::whereIn('id', $allOids)->whereNull('delete_time')->column('amount', 'id');
}
foreach ($poIds as $pid) {
$s = 0.0;
foreach ($byPo[$pid] ?? [] as $oid) {
$s += (float) ($amountByOid[$oid] ?? 0);
}
$paidSumByPo[$pid] = round($s, 2);
}
}
// 获取创建人姓名
$creatorIds = array_values(array_unique(array_filter(array_map('intval', array_column($lists, 'creator_id')))));
$creatorNames = [];
if ($creatorIds !== []) {
$creatorNames = \app\common\model\auth\Admin::whereIn('id', $creatorIds)->column('name', 'id');
}
// 获取处方ID对应的开方人姓名
$prescriptionIds = array_values(array_unique(array_filter(array_map('intval', array_column($lists, 'prescription_id')))));
$prescriptionCreators = [];
if ($prescriptionIds !== []) {
$prescriptions = \app\common\model\tcm\Prescription::whereIn('id', $prescriptionIds)
->whereNull('delete_time')
->field(['id', 'creator_id', 'doctor_name', 'phone'])
->select()
->toArray();
$doctorIds = [];
foreach ($prescriptions as $rx) {
$rxId = (int) ($rx['id'] ?? 0);
$doctorId = (int) ($rx['creator_id'] ?? 0);
$doctorName = (string) ($rx['doctor_name'] ?? '');
if ($rxId > 0) {
$prescriptionCreators[$rxId] = [
'doctor_id' => $doctorId,
'doctor_name' => $doctorName,
'phone' => (string) ($rx['phone'] ?? ''),
];
if ($doctorId > 0) {
$doctorIds[] = $doctorId;
}
}
}
// 如果 doctor_name 为空,从 Admin 表获取
$doctorIds = array_values(array_unique($doctorIds));
if ($doctorIds !== []) {
$doctorNamesFromAdmin = \app\common\model\auth\Admin::whereIn('id', $doctorIds)->column('name', 'id');
foreach ($prescriptionCreators as &$pc) {
if (empty($pc['doctor_name']) && $pc['doctor_id'] > 0) {
$pc['doctor_name'] = $doctorNamesFromAdmin[$pc['doctor_id']] ?? '';
}
}
unset($pc);
}
}
$depMin = PrescriptionOrderLogic::depositMinAmount();
$diagIdsForAssist = array_values(array_unique(array_filter(array_map('intval', array_column($lists, 'diagnosis_id')))));
$assistantByDiag = [];
if ($diagIdsForAssist !== []) {
$assistantByDiag = Diagnosis::whereIn('id', $diagIdsForAssist)->whereNull('delete_time')->column('assistant_id', 'id');
}
$assistantIds = array_values(array_unique(array_filter(array_map('intval', array_values($assistantByDiag)))));
$assistantNames = $assistantIds !== []
? \app\common\model\auth\Admin::whereIn('id', $assistantIds)->column('name', 'id')
: [];
foreach ($lists as &$item) {
PrescriptionOrderLogic::maskInternalCostIfNeeded($item, $this->adminInfo);
PrescriptionOrderLogic::maskRemarkExtraIfNeeded($item, $this->adminInfo);
$pid = (int) ($item['id'] ?? 0);
$item['linked_pay_order_count'] = (int) PrescriptionOrderPayOrder::where(
'prescription_order_id',
$pid
)->count();
$item['linked_pay_paid_total'] = $paidSumByPo[$pid] ?? 0.0;
$item['deposit_min_amount'] = $depMin;
$item['can_upload_gancao_reciperl'] = PrescriptionOrderLogic::canUploadGancaoRecipel(
$item,
$this->adminId,
$this->adminInfo,
$assistantByDiag
);
// 添加创建人姓名
$creatorId = (int) ($item['creator_id'] ?? 0);
$item['creator_name'] = $creatorId > 0 ? ($creatorNames[$creatorId] ?? '') : '';
// 添加医助(来自诊单 assistant_id
$diagIdForAssist = (int) ($item['diagnosis_id'] ?? 0);
$assistantIdForItem = (int) ($assistantByDiag[$diagIdForAssist] ?? 0);
$item['assistant_id'] = $assistantIdForItem;
$item['assistant_name'] = $assistantIdForItem > 0
? ($assistantNames[$assistantIdForItem] ?? '')
: '';
// 添加开方人姓名、处方登记手机(与业务订单收货手机比对用)
$rxId = (int) ($item['prescription_id'] ?? 0);
$item['doctor_name'] = $rxId > 0 ? ($prescriptionCreators[$rxId]['doctor_name'] ?? '') : '';
$item['prescription_phone'] = $rxId > 0 ? ($prescriptionCreators[$rxId]['phone'] ?? '') : '';
}
unset($item);
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists);
}
return $lists;
}
public function extend(): array
{
$s = $this->computeListStatsForExtend();
$focus = $this->computeFocusCountsForExtend();
$base = [
'deposit_min_amount' => PrescriptionOrderLogic::depositMinAmount(),
'gancao_scm_enabled' => GancaoScmRecipelService::isConfigured(),
'stats_order_amount' => $s['order_amount'],
'stats_order_amount_not_cancelled' => $s['order_amount_not_cancelled'],
'stats_order_amount_cancelled' => $s['order_amount_cancelled'],
/** 业绩:业务订单金额,剔除履约已取消(fulfillment_status=4),其余状态全部计入 */
'stats_order_amount_performance' => $s['order_amount_not_cancelled'],
'stats_linked_pay_amount' => $s['linked_pay_amount'],
'stats_linked_pay_amount_not_cancelled' => $s['linked_pay_amount_not_cancelled'],
'stats_linked_pay_amount_cancelled' => $s['linked_pay_amount_cancelled'],
/** 业绩-关联实付:剔除履约已取消订单的关联支付金额 */
'stats_linked_pay_amount_performance' => $s['linked_pay_amount_not_cancelled'],
'stats_time_start' => $s['label_start'],
'stats_time_end' => $s['label_end'],
/** 统计口径:all=支付审核白名单全量;assistant=医助仅诊单协助业绩;restricted=与列表同域 */
'stats_scope' => $s['scope'],
'focus_pending_rx_count' => $focus['pending_rx'],
'focus_pending_pay_count' => $focus['pending_pay'],
'focus_pending_ship_count' => $focus['pending_ship'],
'focus_risk_count' => $focus['risk'],
];
$yejiConsult = $this->computeYejiDrawerConsultCountIfRequested();
if ($yejiConsult !== null) {
$base['stats_yeji_consult_count'] = $yejiConsult;
}
$yejiDrawerTotal = $this->computeYejiDrawerPerformanceTotalForExtend();
if ($yejiDrawerTotal !== null) {
$base['stats_yeji_drawer_total_performance_amount'] = $yejiDrawerTotal['amount'];
$base['stats_yeji_drawer_total_performance_order_count'] = $yejiDrawerTotal['order_count'];
}
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)];
}
/**
* 侧栏已选渠道时:「合计业绩」金额与订单数(与看板列同口径,不限渠道收窄)
*
* @return array{amount: float, order_count: int}|null
*/
private function computeYejiDrawerPerformanceTotalForExtend(): ?array
{
if ((int) ($this->params['yeji_order_drawer'] ?? 0) !== 1) {
return null;
}
if (trim((string) ($this->params['channel_code'] ?? '')) === '') {
return null;
}
[$t0, $t1] = $this->resolveStatsTimeWindow();
if ($t0 <= 0 || $t1 <= 0) {
return null;
}
if ($t1 < $t0) {
$tmp = $t0;
$t0 = $t1;
$t1 = $tmp;
}
$q = $this->buildBaseQueryForStats(false)->whereBetween('create_time', [$t0, $t1]);
// 全量「合计业绩」与看板列同口径:始终剔除履约已取消(4),不依赖 exclude 参数
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, '');
$amount = round((float) (clone $q)->sum('amount'), 2);
$count = (int) (clone $q)->count();
return ['amount' => $amount, 'order_count' => $count];
}
/**
* 重点看板计数(基于“当前筛选(去除审核/履约状态)”后的全量命中)
*
* @return array{pending_rx:int,pending_pay:int,pending_ship:int,risk:int}
*/
private function computeFocusCountsForExtend(): array
{
$query = PrescriptionOrder::where($this->searchWhereWithoutStatusFilters())->whereNull('delete_time');
$this->applyPermissionAndExtraFilters($query);
$pendingRxQuery = clone $query;
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($pendingRxQuery, '');
return [
// 处方审核待处理(不含履约已取消)
'pending_rx' => (int) $pendingRxQuery->where('prescription_audit_status', 0)->count(),
// 支付审核待处理(前提:处方已通过)
'pending_pay' => (int) (clone $query)->where('prescription_audit_status', 1)->where('payment_slip_audit_status', 0)->count(),
// 履约待发货
'pending_ship' => (int) (clone $query)->where('fulfillment_status', 2)->count(),
// 重点异常:支付审核驳回(与前端「重点异常」快捷筛选口径一致)
'risk' => (int) (clone $query)->where('payment_slip_audit_status', 2)->count(),
];
}
/**
* 关联实付按业务订单 id 列表汇总
*
* @param array<int> $poIds
*/
private function sumLinkedPayForPrescriptionOrderIds(array $poIds): float
{
if ($poIds === []) {
return 0.0;
}
$sum = (float) PrescriptionOrderPayOrder::alias('l')
->join('order o', 'l.pay_order_id = o.id')
->whereIn('l.prescription_order_id', $poIds)
->whereNull('o.delete_time')
->sum('o.amount');
return round($sum, 2);
}
/**
* @return array{order_amount: float, order_amount_not_cancelled: float, order_amount_cancelled: float, linked_pay_amount: float, linked_pay_amount_not_cancelled: float, linked_pay_amount_cancelled: float, label_start: string, label_end: string, scope: string}
*/
private function computeListStatsForExtend(): array
{
[$t0, $t1, $l0, $l1] = $this->resolveStatsTimeWindow();
if ($t0 <= 0 || $t1 <= 0) {
return $this->statsZeroPayload();
}
if ($t1 < $t0) {
$tmp = $t0;
$t0 = $t1;
$t1 = $tmp;
}
$q = $this->buildBaseQueryForStats()->whereBetween('create_time', [$t0, $t1]);
$orderTotal = round((float) (clone $q)->sum('amount'), 2);
$orderCancelled = round((float) (clone $q)->where('fulfillment_status', 4)->sum('amount'), 2);
$orderNotCancelled = round($orderTotal - $orderCancelled, 2);
$idsAll = (clone $q)->column('id');
$idsAll = array_values(array_filter(array_map('intval', $idsAll), static function (int $id): bool {
return $id > 0;
}));
if ($idsAll === []) {
return $this->statsExtendPayload(
$orderTotal,
$orderNotCancelled,
$orderCancelled,
0.0,
0.0,
0.0,
$l0,
$l1
);
}
$idsCancel = (clone $q)->where('fulfillment_status', 4)->column('id');
$idsCancel = array_values(array_filter(array_map('intval', $idsCancel), static function (int $id): bool {
return $id > 0;
}));
$idsNotCancelled = array_values(array_diff($idsAll, $idsCancel));
$linkedTotal = $this->sumLinkedPayForPrescriptionOrderIds($idsAll);
$linkedNotCancelled = $this->sumLinkedPayForPrescriptionOrderIds($idsNotCancelled);
$linkedCancelled = $this->sumLinkedPayForPrescriptionOrderIds($idsCancel);
return $this->statsExtendPayload(
$orderTotal,
$orderNotCancelled,
$orderCancelled,
$linkedTotal,
$linkedNotCancelled,
$linkedCancelled,
$l0,
$l1
);
}
/**
* @return array{order_amount: float, order_amount_not_cancelled: float, order_amount_cancelled: float, linked_pay_amount: float, linked_pay_amount_not_cancelled: float, linked_pay_amount_cancelled: float, label_start: string, label_end: string, scope: string}
*/
private function statsZeroPayload(): array
{
return $this->statsExtendPayload(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, '', '');
}
/**
* @return array{order_amount: float, order_amount_not_cancelled: float, order_amount_cancelled: float, linked_pay_amount: float, linked_pay_amount_not_cancelled: float, linked_pay_amount_cancelled: float, label_start: string, label_end: string, scope: string}
*/
private function statsExtendPayload(
float $orderTotal,
float $orderNotCancelled,
float $orderCancelled,
float $linkedTotal,
float $linkedNotCancelled,
float $linkedCancelled,
string $l0,
string $l1
): array {
$scope = 'restricted';
if (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) {
$scope = 'all';
} else {
$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';
}
}
return [
'order_amount' => $orderTotal,
'order_amount_not_cancelled' => $orderNotCancelled,
'order_amount_cancelled' => $orderCancelled,
'linked_pay_amount' => $linkedTotal,
'linked_pay_amount_not_cancelled' => $linkedNotCancelled,
'linked_pay_amount_cancelled' => $linkedCancelled,
'label_start' => $l0,
'label_end' => $l1,
'scope' => $scope,
];
}
public function count(): int
{
$query = $this->buildFilteredQuery();
return (int) $query->count();
}
/**
* 按开方医生(关联处方 creator_id)/ 诊单医助(关联诊单 assistant_id)筛选
*/
private function applyDoctorAssistantFilters($query): void
{
$poTbl = (new PrescriptionOrder())->getTable();
if (isset($this->params['doctor_id']) && (int) $this->params['doctor_id'] > 0) {
$doctorId = (int) $this->params['doctor_id'];
$rxTbl = (new Prescription())->getTable();
$query->whereExists("SELECT 1 FROM {$rxTbl} rx WHERE rx.id = {$poTbl}.prescription_id AND rx.delete_time IS NULL AND rx.creator_id = {$doctorId}");
}
if (isset($this->params['assistant_id']) && (int) $this->params['assistant_id'] > 0) {
$assistantId = (int) $this->params['assistant_id'];
$diagTbl = (new Diagnosis())->getTable();
$query->whereExists("SELECT 1 FROM {$diagTbl} dg WHERE dg.id = {$poTbl}.diagnosis_id AND dg.delete_time IS NULL AND dg.assistant_id = {$assistantId}");
}
if (isset($this->params['assistant_dept_id']) && $this->params['assistant_dept_id'] !== '' && (int) $this->params['assistant_dept_id'] > 0) {
$rootDeptId = (int) $this->params['assistant_dept_id'];
$deptIds = DeptLogic::getSelfAndDescendantIds($rootDeptId);
$deptIds = array_values(array_filter(array_map('intval', $deptIds), static function (int $id): bool {
return $id > 0;
}));
if ($deptIds === []) {
$query->whereRaw('0 = 1');
return;
}
$inList = implode(',', $deptIds);
$diagTbl = (new Diagnosis())->getTable();
$adTbl = (new AdminDept())->getTable();
$query->whereExists(
"SELECT 1 FROM `{$adTbl}` ad INNER JOIN `{$diagTbl}` dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL "
. "WHERE dg.id = `{$poTbl}`.`diagnosis_id` AND ad.dept_id IN ({$inList})"
);
}
}
/**
* 按诊单患者姓名 / 手机号模糊检索(关联 diagnosis_id
*/
private function applyPatientKeywordFilter($query): void
{
$kw = trim((string) ($this->params['patient_keyword'] ?? ''));
if ($kw === '') {
return;
}
$poTbl = (new PrescriptionOrder())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$likeInner = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $kw);
$like = '%' . $likeInner . '%';
$likeSql = str_replace("'", "''", $like);
$query->whereExists(
"SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL "
. "AND (dg.`patient_name` LIKE '{$likeSql}' OR dg.`phone` LIKE '{$likeSql}')"
);
}
/**
* 按真实患者ID精确过滤(tcm_diagnosis.patient_id 是跨诊单的患者唯一ID
* 用于诊单编辑抽屉「业务订单」tab 列出该患者所有诊单产生的处方业务订单。
*/
private function applyPatientIdFilter($query): void
{
$patientId = (int) ($this->params['patient_id'] ?? 0);
if ($patientId <= 0) {
return;
}
$poTbl = (new PrescriptionOrder())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$query->whereExists(
"SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` "
. "AND dg.`delete_time` IS NULL AND dg.`patient_id` = {$patientId}"
);
}
/**
* 按快递单号 / 快递公司 模糊检索
*/
private function applyExpressKeywordFilter($query): void
{
$kw = trim((string) ($this->params['express_keyword'] ?? ''));
if ($kw === '') {
return;
}
$likeInner = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $kw);
$like = '%' . $likeInner . '%';
$likeSql = str_replace("'", "''", $like);
$poTbl = (new PrescriptionOrder())->getTable();
$etTbl = (new ExpressTracking())->getTable();
$query->whereRaw(
"(IFNULL(`{$poTbl}`.`tracking_number`,'') LIKE '{$likeSql}'"
. " OR IFNULL(`{$poTbl}`.`express_company`,'') LIKE '{$likeSql}'"
. " OR EXISTS (SELECT 1 FROM `{$etTbl}` et WHERE et.`delete_time` IS NULL"
. " AND et.`order_id` = `{$poTbl}`.`id` AND et.`order_type` = 'prescription'"
. " AND (IFNULL(et.`tracking_number`,'') LIKE '{$likeSql}'"
. " OR IFNULL(et.`express_company`,'') LIKE '{$likeSql}'"
. " OR IFNULL(et.`express_company_name`,'') LIKE '{$likeSql}'))"
. ")"
);
}
}