From 7aec1a971395509b555d7828b1f9f728f37fa957 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 16:30:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/tcm-prescription/index.vue | 12 + .../consumer/prescription/order_list.vue | 3 +- .../consumer/prescription/order_list_h5.vue | 3 +- .../src/views/fans/commission-settlement.vue | 45 +- .../adminapi/logic/stats/YejiStatsLogic.php | 1006 ++++++++++++----- .../common/service/ExpressTrackingService.php | 117 ++ 6 files changed, 925 insertions(+), 261 deletions(-) diff --git a/admin/src/components/tcm-prescription/index.vue b/admin/src/components/tcm-prescription/index.vue index 0b677816..3601d883 100644 --- a/admin/src/components/tcm-prescription/index.vue +++ b/admin/src/components/tcm-prescription/index.vue @@ -550,6 +550,7 @@
+
{{ prescriptionTabType === 'internal' ? '药房联' : SLIP_TITLE }}
服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点 @@ -1622,6 +1623,8 @@ async function handlePasteRecipeImport() { } } +const SLIP_TITLE = '成都双流甄养堂互联网医院 处方笺' + const templateConfig = reactive({ stationName: '成都双流甄养堂互联网医院', showDisclaimer: true, @@ -2693,6 +2696,15 @@ defineExpose({ overflow: visible; } +.rx-title { + text-align: center; + font-size: 28px; + font-weight: 700; + line-height: 1.2; + margin: 10px 0 35px; + letter-spacing: 0.08em; +} + .rx-notice { display: flex; align-items: center; diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index f8f56b94..c8c044b7 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -2363,7 +2363,7 @@
-
{{ prescriptionTabType === 'internal' ? '药房联' : '处方单' }}
+
{{ prescriptionTabType === 'internal' ? '药房联' : SLIP_TITLE }}
服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点 @@ -5495,6 +5495,7 @@ async function confirmRevokePayAudit(row: { id: number }) { // ─── 查看处方详情(处方单样式)──────────────────────────────────────────── /** 处方笺抬头/页脚(与诊间打印一致) */ const SLIP_HOSPITAL_NAME = '成都双流甄养堂互联网医院' +const SLIP_TITLE = `${SLIP_HOSPITAL_NAME} 处方笺` const SLIP_COMPANY_LINE = '成都双流甄养堂互联网医院有限公司 联系方式:4001667339' const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄龙大道二段280号' diff --git a/admin/src/views/consumer/prescription/order_list_h5.vue b/admin/src/views/consumer/prescription/order_list_h5.vue index 403e43f9..75d797db 100644 --- a/admin/src/views/consumer/prescription/order_list_h5.vue +++ b/admin/src/views/consumer/prescription/order_list_h5.vue @@ -2091,7 +2091,7 @@
-
处方单
+
{{ SLIP_TITLE }}
服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点 @@ -4784,6 +4784,7 @@ async function confirmRevokePayAudit(row: { id: number }) { // ─── 查看处方详情(处方单样式)──────────────────────────────────────────── /** 处方笺抬头/页脚(与诊间打印一致) */ const SLIP_HOSPITAL_NAME = '成都双流甄养堂互联网医院' +const SLIP_TITLE = `${SLIP_HOSPITAL_NAME} 处方笺` const SLIP_COMPANY_LINE = '成都双流甄养堂互联网医院有限公司 联系方式:4001667339' const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄龙大道二段280号' diff --git a/admin/src/views/fans/commission-settlement.vue b/admin/src/views/fans/commission-settlement.vue index 99a44059..56b3451b 100644 --- a/admin/src/views/fans/commission-settlement.vue +++ b/admin/src/views/fans/commission-settlement.vue @@ -405,6 +405,18 @@
+ + + @@ -492,6 +504,18 @@
+ + + @@ -948,10 +972,8 @@ async function loadLeafOrders(page: number) { page_size: leafOrdersPageSize.value, } if (d.mode === 'dept') { - p.dept_ids = String(d.dept_id) - if (selectedChannel.value) { - p.channel_code = selectedChannel.value - } + Object.assign(p, buildQueryParams()) + p.table_row_dept_id = d.dept_id } else { Object.assign(p, buildQueryParams()) p.appt_channel_value = d.appt_channel_value @@ -1981,6 +2003,21 @@ onMounted(async () => { .cs-muted { color: var(--cs-muted); } +.cs-patient-cell { + display: flex; + flex-direction: column; + gap: 2px; + line-height: 1.35; +} + +.cs-patient-cell__name { + color: var(--cs-ink, #0f172a); +} + +.cs-patient-cell__meta { + font-size: 12px; +} + /* ============ Element Plus overrides (scoped via deep) ============ */ .cs-page { :deep(.el-input__wrapper), diff --git a/server/app/adminapi/logic/stats/YejiStatsLogic.php b/server/app/adminapi/logic/stats/YejiStatsLogic.php index df9f3672..11063431 100755 --- a/server/app/adminapi/logic/stats/YejiStatsLogic.php +++ b/server/app/adminapi/logic/stats/YejiStatsLogic.php @@ -5391,81 +5391,155 @@ class YejiStatsLogic } /** - * 挂号渠道分组:根据诊单 id 列表,反查每个诊单对应的「挂号渠道值」(doctor_appointment.channels, - * 与 dict_data(type_value=channels).value 对齐)。一个诊单理论上 1:1 关联一条挂号,多挂号场景取 id 最大者。 - * - * @param int[] $diagnosisIds - * - * @return array diagnosis_id => channels (>0) + * 业务订单关联挂号渠道 SQL:与 PrescriptionOrderLogic 列表/详情一致(处方 appointment_id 优先,否则诊单下 MAX(挂号.id))。 */ - private static function commissionSettlementDiagnosisLatestApptChannel(array $diagnosisIds): array + private static function commissionSettlementSqlOrderLinkedApptChannelExpr(): string { - $ids = array_values(array_unique(array_filter(array_map('intval', $diagnosisIds), static fn (int $x): bool => $x > 0))); - if ($ids === []) { + $rxTable = self::tableWithPrefix('tcm_prescription'); + $daTable = self::tableWithPrefix('doctor_appointment'); + + return 'COALESCE(' + . '(SELECT `da_rx`.`channels` FROM `' . $rxTable . '` `rx_lnk` ' + . 'INNER JOIN `' . $daTable . '` `da_rx` ON `da_rx`.`id` = `rx_lnk`.`appointment_id` ' + . 'WHERE `rx_lnk`.`id` = `o`.`prescription_id` AND `rx_lnk`.`delete_time` IS NULL ' + . 'AND IFNULL(`rx_lnk`.`appointment_id`, 0) > 0 AND IFNULL(`da_rx`.`channels`, 0) > 0), ' + . '(SELECT `da_fb`.`channels` FROM `' . $daTable . '` `da_fb` ' + . 'WHERE `da_fb`.`id` = (SELECT MAX(`da_m`.`id`) FROM `' . $daTable . '` `da_m` WHERE `da_m`.`patient_id` = `o`.`diagnosis_id`) ' + . 'AND IFNULL(`da_fb`.`channels`, 0) > 0)' + . ')'; + } + + /** + * 与 PrescriptionOrderLogic 列表/详情一致:按业务订单解析关联挂号(处方 appointment_id 优先,否则诊单下 MAX(挂号.id))。 + * + * @param list $rows + * + * @return array + */ + private static function commissionSettlementBatchResolveLinkedAppointmentsByOrders(array $rows): array + { + if ($rows === []) { return []; } - $rows = Db::name('doctor_appointment') - ->whereIn('patient_id', $ids) - ->where('status', 3) - ->where('channels', '>', 0) - ->order('patient_id asc, id desc') - ->field(['patient_id', 'channels']) - ->select() - ->toArray(); - - $out = []; - foreach ($rows as $r) { - $dg = (int) ($r['patient_id'] ?? 0); - if ($dg <= 0 || isset($out[$dg])) { + $rxIds = []; + $diagNeedFallback = []; + foreach ($rows as $row) { + $oid = (int) ($row['id'] ?? 0); + if ($oid <= 0) { continue; } - $ch = (int) ($r['channels'] ?? 0); - if ($ch > 0) { - $out[$dg] = $ch; + $rxId = (int) ($row['prescription_id'] ?? 0); + $dgId = (int) ($row['diagnosis_id'] ?? 0); + if ($rxId > 0) { + $rxIds[$rxId] = true; } + if ($rxId <= 0 && $dgId > 0) { + $diagNeedFallback[$dgId] = true; + } + } + + $rxApptByRx = []; + if ($rxIds !== []) { + $rxApptByRx = Db::name('tcm_prescription') + ->whereIn('id', array_keys($rxIds)) + ->whereNull('delete_time') + ->column('appointment_id', 'id'); + foreach ($rxApptByRx as $rxId => $apptId) { + $rxApptByRx[(int) $rxId] = (int) $apptId; + } + } + + $latestApptByDiag = []; + $fallbackDiagIds = array_keys($diagNeedFallback); + if ($fallbackDiagIds !== []) { + foreach (array_chunk($fallbackDiagIds, 800) as $chunk) { + $agg = Db::name('doctor_appointment') + ->whereIn('patient_id', $chunk) + ->fieldRaw('patient_id, MAX(id) AS max_id') + ->group('patient_id') + ->select() + ->toArray(); + foreach ($agg as $r) { + $pid = (int) ($r['patient_id'] ?? 0); + $mid = (int) ($r['max_id'] ?? 0); + if ($pid > 0 && $mid > 0) { + $latestApptByDiag[$pid] = $mid; + } + } + } + } + + $resolvedApptByOrder = []; + foreach ($rows as $row) { + $oid = (int) ($row['id'] ?? 0); + if ($oid <= 0) { + continue; + } + $rxId = (int) ($row['prescription_id'] ?? 0); + $dgId = (int) ($row['diagnosis_id'] ?? 0); + $rxApptId = $rxId > 0 ? (int) ($rxApptByRx[$rxId] ?? 0) : 0; + $resolved = $rxApptId; + if ($resolved <= 0 && $dgId > 0) { + $resolved = (int) ($latestApptByDiag[$dgId] ?? 0); + } + if ($resolved > 0) { + $resolvedApptByOrder[$oid] = $resolved; + } + } + + if ($resolvedApptByOrder === []) { + return []; + } + + $apptIds = array_values(array_unique(array_map('intval', $resolvedApptByOrder))); + $apptRows = []; + foreach (array_chunk($apptIds, 800) as $chunk) { + $part = Db::name('doctor_appointment') + ->whereIn('id', $chunk) + ->field(['id', 'channels', 'appointment_date', 'status']) + ->select() + ->toArray(); + foreach ($part as $r) { + $aid = (int) ($r['id'] ?? 0); + if ($aid > 0) { + $apptRows[$aid] = $r; + } + } + } + + $out = []; + foreach ($resolvedApptByOrder as $oid => $apptId) { + $apptId = (int) $apptId; + $r = $apptRows[$apptId] ?? null; + if (!is_array($r)) { + continue; + } + $out[(int) $oid] = [ + 'appointment_id' => $apptId, + 'channels' => (int) ($r['channels'] ?? 0), + 'appointment_date' => trim((string) ($r['appointment_date'] ?? '')), + 'appointment_status' => (int) ($r['status'] ?? 0), + ]; } return $out; } /** - * 诊单关联挂号摘要展示:优先 status=3(已完成),否则取该诊单维度下最近一条挂号(patient_id=诊单 id)。 - * doctor_appointment.patient_id 与诊单主键对齐,见 DiagnosisLists。 + * @param list> $rows * - * @param int[] $diagnosisIds - * - * @return array diagnosis_id => 摘要 + * @return array{0: array>, 1: array} */ - private static function commissionSettlementDiagnosisLatestCompletedAppointment(array $diagnosisIds): array + private static function commissionSettlementLoadApptContextForOrders(array $rows): array { - $ids = array_values(array_unique(array_filter(array_map('intval', $diagnosisIds), static fn (int $x): bool => $x > 0))); - if ($ids === []) { - return []; + $linkedApptByOrderId = self::commissionSettlementBatchResolveLinkedAppointmentsByOrders($rows); + $chVals = []; + foreach ($linkedApptByOrderId as $ap) { + $chVals[] = (int) ($ap['channels'] ?? 0); } - $rows = Db::name('doctor_appointment') - ->whereIn('patient_id', $ids) - ->orderRaw('`patient_id` asc, (case when `status` = 3 then 1 else 0 end) desc, `id` desc') - ->field(['id', 'patient_id', 'channels', 'appointment_date', 'status']) - ->select() - ->toArray(); - - $out = []; - foreach ($rows as $r) { - $dg = (int) ($r['patient_id'] ?? 0); - if ($dg <= 0 || isset($out[$dg])) { - continue; - } - $out[$dg] = [ - 'appointment_id' => (int) ($r['id'] ?? 0), - 'channels' => (int) ($r['channels'] ?? 0), - 'appointment_date' => trim((string) ($r['appointment_date'] ?? '')), - 'appointment_status' => (int) ($r['status'] ?? 0), - ]; - } - - return $out; + return [$linkedApptByOrderId, self::commissionSettlementApptChannelsDictMap($chVals)]; } /** @@ -5560,22 +5634,15 @@ class YejiStatsLogic } /** - * 签收 / 尾款时间:对物流与支付关联做 GROUP BY 后 LEFT JOIN。 - * 签收时间不仅用 express_tracking.sign_time:历史数据或部分同步路径下 sign_time 仍为 0, - * 但 express_trace 已有「签收」语义轨迹(与 logisticsTrace 展示同源),此处用轨迹时间戳回填,避免误算为「无签收」而全部顺延。 + * 单条 express_tracking 的有效签收时间 SQL 表达式(与 ExpressTrackingService::effectiveSignUnixFromTrackingDetail 对齐)。 */ - private static function commissionSettlementAttachExpressPayAggregates(Query $query): void + private static function commissionSettlementExpressRowSignExpr(string $etTable, string $traceTable): string { - $etTable = self::tableWithPrefix('express_tracking'); - $traceTable = self::tableWithPrefix('express_trace'); - $linkTbl = self::tableWithPrefix('tcm_prescription_order_pay_order'); - $payTbl = self::tableWithPrefix('order'); - - // 单条物流记录上的「有效签收时间戳」:sign_time > 0 优先;否则签收类轨迹;再否则主表已签收/已标记签收时用最新轨迹时间兜底 $sigTraceCond = "`tr_sig`.`status_code` IN ('3','301','302','304') OR `tr_sig`.`status` LIKE '%签收%' " . "OR `tr_sig`.`trace_context` LIKE '%签收%' OR `tr_sig`.`trace_context` LIKE '%妥投%' " . "OR `tr_sig`.`trace_context` LIKE '%已送达%' OR `tr_sig`.`trace_context` LIKE '%本人签收%'"; - $rowSignExpr = 'GREATEST(' + + return 'GREATEST(' . 'IF(IFNULL(`et`.`sign_time`, 0) > 0, `et`.`sign_time`, 0), ' . 'IFNULL((SELECT MAX(`tr_sig`.`trace_time_stamp`) FROM `' . $traceTable . '` `tr_sig` ' . 'WHERE `tr_sig`.`tracking_id` = `et`.`id` AND IFNULL(`tr_sig`.`trace_time_stamp`, 0) > 0 AND (' @@ -5586,6 +5653,20 @@ class YejiStatsLogic . 'WHERE `tr_any`.`tracking_id` = `et`.`id` AND IFNULL(`tr_any`.`trace_time_stamp`, 0) > 0), 0), ' . '0)' . ')'; + } + + /** + * 签收 / 尾款时间:对物流与支付关联做 GROUP BY 后 LEFT JOIN。 + * 签收时间不仅用 express_tracking.sign_time:历史数据或部分同步路径下 sign_time 仍为 0, + * 但 express_trace 已有「签收」语义轨迹(与 logisticsTrace 展示同源),此处用轨迹时间戳回填,避免误算为「无签收」而全部顺延。 + */ + private static function commissionSettlementAttachExpressPayAggregates(Query $query): void + { + $etTable = self::tableWithPrefix('express_tracking'); + $traceTable = self::tableWithPrefix('express_trace'); + $linkTbl = self::tableWithPrefix('tcm_prescription_order_pay_order'); + $payTbl = self::tableWithPrefix('order'); + $rowSignExpr = self::commissionSettlementExpressRowSignExpr($etTable, $traceTable); $poTable = self::tableWithPrefix('tcm_prescription_order'); // tracking_number:TRIM 避免空格导致匹配失败;collation 统一避免 HY000 1267 @@ -5628,10 +5709,155 @@ class YejiStatsLogic ]; } + /** + * @param int[] $orderIds + * + * @return array order_id => pay unix + */ + private static function commissionSettlementBatchPayTsMap(array $orderIds): array + { + $orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds), static fn (int $x): bool => $x > 0))); + if ($orderIds === []) { + return []; + } + + $linkTbl = self::tableWithPrefix('tcm_prescription_order_pay_order'); + $payTbl = self::tableWithPrefix('order'); + $out = []; + + foreach (array_chunk($orderIds, 800) as $chunk) { + $in = implode(',', $chunk); + $sql = "SELECT l.prescription_order_id AS cs_oid, " + . 'MAX(CASE WHEN pay.order_type IN (5, 7) THEN UNIX_TIMESTAMP(pay.payment_time) END) AS cs_pay_tail_ts, ' + . 'MAX(UNIX_TIMESTAMP(pay.payment_time)) AS cs_pay_any_ts ' + . "FROM {$linkTbl} l INNER JOIN {$payTbl} pay ON pay.id = l.pay_order_id AND pay.delete_time IS NULL " + . 'WHERE pay.status = 2 AND l.prescription_order_id IN (' . $in . ') ' + . 'GROUP BY l.prescription_order_id'; + + $rows = Db::query($sql); + if (!is_array($rows)) { + continue; + } + foreach ($rows as $r) { + $oid = (int) ($r['cs_oid'] ?? 0); + if ($oid <= 0) { + continue; + } + $tail = (int) ($r['cs_pay_tail_ts'] ?? 0); + $any = (int) ($r['cs_pay_any_ts'] ?? 0); + $out[$oid] = $tail > 0 ? $tail : $any; + } + } + + return $out; + } + + /** + * 按候选业务单 id 批量解析签收时间(限定 IN 范围,供 orderLines 分页/bucket 分块使用)。 + * + * @param int[] $orderIds + * + * @return array order_id => sign unix + */ + private static function commissionSettlementBatchSignTsMapForOrderIds(array $orderIds): array + { + $orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds), static fn (int $x): bool => $x > 0))); + if ($orderIds === []) { + return []; + } + + $etTable = self::tableWithPrefix('express_tracking'); + $traceTable = self::tableWithPrefix('express_trace'); + $poTable = self::tableWithPrefix('tcm_prescription_order'); + $rowSignExpr = self::commissionSettlementExpressRowSignExpr($etTable, $traceTable); + $trackingJoinOn = + 'TRIM(`po`.`tracking_number`) COLLATE utf8mb4_unicode_ci = TRIM(`et`.`tracking_number`) COLLATE utf8mb4_unicode_ci'; + + $out = []; + foreach (array_chunk($orderIds, 400) as $chunk) { + $in = implode(',', $chunk); + $sql = 'SELECT `u`.`order_id`, MAX(`u`.`cs_row_sign`) AS `cs_sign_ts` FROM (' + . 'SELECT `et`.`order_id` AS `order_id`, ' . $rowSignExpr . ' AS `cs_row_sign` FROM `' . $etTable . '` `et` ' + . 'INNER JOIN `' . $poTable . '` `po_by_id` ON `po_by_id`.`id` = `et`.`order_id` AND `po_by_id`.`delete_time` IS NULL ' + . 'WHERE `et`.`delete_time` IS NULL AND IFNULL(`et`.`order_id`, 0) > 0 AND `et`.`order_id` IN (' . $in . ') ' + . 'UNION ALL ' + . 'SELECT `po`.`id` AS `order_id`, ' . $rowSignExpr . ' AS `cs_row_sign` FROM `' . $etTable . '` `et` ' + . 'INNER JOIN `' . $poTable . '` `po` ON ' . $trackingJoinOn + . ' AND `po`.`id` IN (' . $in . ') AND `po`.`delete_time` IS NULL ' + . " AND LENGTH(TRIM(IFNULL(`po`.`tracking_number`, ''))) > 0 " + . 'WHERE `et`.`delete_time` IS NULL' + . ') AS `u` GROUP BY `u`.`order_id`'; + + $rows = Db::query($sql); + if (!is_array($rows)) { + continue; + } + foreach ($rows as $r) { + $oid = (int) ($r['order_id'] ?? 0); + $ts = (int) ($r['cs_sign_ts'] ?? 0); + if ($oid > 0 && $ts > 0) { + $out[$oid] = $ts; + } + } + } + + return $out; + } + + /** + * 为订单行批量补齐 sign_ts / pay_ts。默认走「候选单批量查库」,避免全表物流 LEFT JOIN 与 overview 逐单 HTTP。 + * + * @param list> $rows + * + * @return list> + */ + private static function commissionSettlementHydrateSignPay(array $rows, bool $allowKuaidiFallback = false): array + { + if ($rows === []) { + return $rows; + } + + $orderIds = []; + foreach ($rows as $r) { + $oid = (int) ($r['id'] ?? 0); + if ($oid <= 0) { + continue; + } + $orderIds[] = $oid; + } + + $payMap = self::commissionSettlementBatchPayTsMap($orderIds); + $signMap = self::commissionSettlementBatchSignTsMapForOrderIds($orderIds); + + foreach ($rows as &$r) { + $oid = (int) ($r['id'] ?? 0); + if ($oid <= 0) { + continue; + } + $signTs = (int) ($signMap[$oid] ?? 0); + if ($signTs <= 0) { + $tn = trim((string) ($r['tracking_number'] ?? '')); + if ($tn !== '') { + $signTs = ExpressTrackingService::resolveSignUnixTimeForCommission( + $tn, + (string) ($r['express_company'] ?? 'auto'), + (string) ($r['recipient_phone'] ?? ''), + $allowKuaidiFallback + ); + } + } + $r['sign_ts'] = $signTs; + $r['pay_ts'] = (int) ($payMap[$oid] ?? 0); + } + unset($r); + + return $rows; + } + /** * @param array $pack commissionSettlementResolveBasePack 返回值 */ - private static function commissionSettlementBuildFilteredOrderQuery(array $pack): Query + private static function commissionSettlementBuildFilteredOrderQuery(array $pack, bool $attachSignPayJoin = false): Query { $c = $pack['c']; $diagTable = $pack['diagTable']; @@ -5654,7 +5880,9 @@ class YejiStatsLogic } else { $query->join("{$rxTable} rx", $rxJoinCond, 'LEFT'); } - self::commissionSettlementAttachExpressPayAggregates($query); + if ($attachSignPayJoin) { + self::commissionSettlementAttachExpressPayAggregates($query); + } $query->whereNull('o.delete_time') ->where('o.fulfillment_status', $fulfillmentStatus) @@ -5761,13 +5989,13 @@ class YejiStatsLogic ]; } - $query = self::commissionSettlementBuildFilteredOrderQuery($pack); + $query = self::commissionSettlementBuildFilteredOrderQuery($pack, false); $att = $pack['att']; - $signPayFields = self::commissionSettlementSignPayFieldRawList(); - $rowsRaw = $query->field(array_merge([ + $rowsRaw = $query->field([ 'o.id', 'o.order_no', + 'o.prescription_id', 'o.diagnosis_id', 'o.create_time', 'o.amount', @@ -5776,17 +6004,15 @@ class YejiStatsLogic 'o.recipient_phone', Db::raw("({$att}) AS assistant_id"), Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'), - ], $signPayFields))->select()->toArray(); + ])->select()->toArray(); + $rowsRaw = self::commissionSettlementHydrateSignPay($rowsRaw, false); - $diagIdSet = []; - foreach ($rowsRaw as $r) { - $dg = (int) ($r['diagnosis_id'] ?? 0); - if ($dg > 0) { - $diagIdSet[$dg] = true; - } + $linkedApptByOrderId = self::commissionSettlementBatchResolveLinkedAppointmentsByOrders($rowsRaw); + $chValsForDict = []; + foreach ($linkedApptByOrderId as $ap) { + $chValsForDict[] = (int) ($ap['channels'] ?? 0); } - $diagToApptChannel = self::commissionSettlementDiagnosisLatestApptChannel(array_keys($diagIdSet)); - $apptChannelsDictMap = self::commissionSettlementApptChannelsDictMap(array_values(array_unique(array_map('intval', $diagToApptChannel)))); + $apptChannelsDictMap = self::commissionSettlementApptChannelsDictMap($chValsForDict); $curAgg = []; $defAgg = []; @@ -5804,7 +6030,7 @@ class YejiStatsLogic $cntDoctorCur = []; $cntDoctorDef = []; - /** 按 (业绩归属医助 × 挂号渠道值) / (开方医生 × 挂号渠道值) 拆分聚合,渠道值取该订单对应诊单的最新已完成挂号 channels */ + /** 按 (业绩归属医助 × 挂号渠道值) / (开方医生 × 挂号渠道值) 拆分聚合,渠道取各业务订单绑定挂号(与处方订单列表同源) */ $assistChTot = []; $assistChCur = []; $assistChDef = []; @@ -5852,20 +6078,8 @@ class YejiStatsLogic $doctorId = (int) ($r['doctor_id'] ?? 0); $signTs = (int) ($r['sign_ts'] ?? 0); $payTs = (int) ($r['pay_ts'] ?? 0); - if ($signTs <= 0) { - $tn0 = trim((string) ($r['tracking_number'] ?? '')); - if ($tn0 !== '') { - $signTs = ExpressTrackingService::resolveSignUnixTimeForCommission( - $tn0, - (string) ($r['express_company'] ?? 'auto'), - (string) ($r['recipient_phone'] ?? ''), - true - ); - } - } $amt = round((float) ($r['amount'] ?? 0), 2); - $dgId = (int) ($r['diagnosis_id'] ?? 0); - $apCh = (int) ($diagToApptChannel[$dgId] ?? 0); + $apCh = (int) (($linkedApptByOrderId[$oid] ?? [])['channels'] ?? 0); if ($aid <= 0 || $amt < 0) { continue; @@ -6158,7 +6372,7 @@ class YejiStatsLogic : '未限制仅系统代开处方(含手动),与列表在同一时段、同一状态下条数应一致(另受本页展示部门/渠道筛选影响)。') . ' 签收与尾款时间不晚于结算月 7 日 24 点的计入「本期提成」,其余计入「顺延下期」。' . '上期同渠道同部门范围内「确定业绩」产生的顺延订单并入本期。' - . '「按医助」「按医生」拆分按 (人 × 挂号渠道):挂号渠道取最新已完成挂号 channels;未关联展示为「未匹配挂号渠道」。'; + . '「按医助」「按医生」拆分按 (人 × 挂号渠道):挂号渠道取各业务订单绑定挂号(处方 appointment_id 优先,否则诊单最近一条);未关联展示为「未匹配挂号渠道」。'; } else { $ruleNoteFull = ($requireSysAutoPo ? '订单创建月为结算月的上一自然月;仅统计系统代开处方(is_system_auto=1)对应的履约已完成(默认 fulfillment_status=3)业务订单。 ' @@ -6198,10 +6412,334 @@ class YejiStatsLogic ]; } + /** + * orderLines 明细查询:在订单池 query 上叠加医助/医生/部门/挂号渠道等 SQL 筛选。 + * + * @param array|null $linesAssistScopeFlip + */ + private static function commissionSettlementApplyOrderLinesSqlFilters( + Query $query, + array $pack, + array $params, + ?array $linesAssistScopeFlip, + bool $linesSkipPhpDs + ): void { + $att = $pack['att']; + $cPack = $pack['c']; + $adminToPrimary = $pack['adminToPrimary']; + + $filterAssistantId = max(0, (int) ($params['assistant_id'] ?? 0)); + $filterDoctorId = max(0, (int) ($params['doctor_id'] ?? 0)); + $filterApptChannelActive = array_key_exists('appt_channel_value', $params) + && $params['appt_channel_value'] !== null + && $params['appt_channel_value'] !== ''; + $tableRowDeptId = max(0, (int) ($params['table_row_dept_id'] ?? 0)); + + if ($filterAssistantId > 0) { + $query->where('o.creator_id', $filterAssistantId); + } + + if ($filterDoctorId > 0) { + $query->where('rx.creator_id', $filterDoctorId); + } + + if (!$linesSkipPhpDs && $linesAssistScopeFlip !== null) { + if ($filterAssistantId > 0) { + if (!isset($linesAssistScopeFlip[$filterAssistantId])) { + $query->whereRaw('0 = 1'); + } + } else { + $ids = array_values(array_unique(array_map('intval', array_keys($linesAssistScopeFlip)))); + $ids = array_values(array_filter($ids, static fn (int $x): bool => $x > 0)); + if ($ids === []) { + $query->whereRaw('0 = 1'); + } elseif (\count($ids) === 1) { + $query->whereRaw("({$att}) = ?", [$ids[0]]); + } else { + $query->whereRaw("({$att}) IN (" . implode(',', $ids) . ')'); + } + } + } + + if ($tableRowDeptId > 0) { + $creatorIds = self::collectAdminIdsAttributedToYejiTableRow( + $tableRowDeptId, + $params['dept_ids'] ?? null + ); + if ($creatorIds === []) { + $query->whereRaw('0 = 1'); + } elseif (\count($creatorIds) === 1) { + $query->where('o.creator_id', (int) $creatorIds[0]); + } else { + $query->whereIn('o.creator_id', $creatorIds); + } + } elseif (!empty($cPack['explicitDeptFilter'])) { + if ($filterAssistantId > 0) { + if (!isset($adminToPrimary[$filterAssistantId])) { + $query->whereRaw('0 = 1'); + } + } else { + $deptAdminIds = array_values(array_unique(array_map('intval', array_keys($adminToPrimary)))); + $deptAdminIds = array_values(array_filter($deptAdminIds, static fn (int $x): bool => $x > 0)); + if ($deptAdminIds === []) { + $query->whereRaw('0 = 1'); + } elseif (\count($deptAdminIds) === 1) { + $query->whereRaw("({$att}) = ?", [$deptAdminIds[0]]); + } else { + $query->whereRaw("({$att}) IN (" . implode(',', $deptAdminIds) . ')'); + } + } + } + + if ($filterApptChannelActive) { + self::commissionSettlementApplyApptChannelSqlFilter( + $query, + (int) $params['appt_channel_value'] + ); + } + } + + /** + * 挂号渠道筛选:与 PrescriptionOrderLogic 列表/详情一致,按业务订单绑定挂号解析 channels。 + */ + private static function commissionSettlementApplyApptChannelSqlFilter(Query $query, int $channelValue): void + { + $expr = self::commissionSettlementSqlOrderLinkedApptChannelExpr(); + if ($channelValue > 0) { + $query->whereRaw('IFNULL(' . $expr . ', 0) = ?', [$channelValue]); + + return; + } + + $query->whereRaw('IFNULL(' . $expr . ', 0) = 0'); + } + + /** + * @param array|null $linesAssistScopeFlip + */ + private static function commissionSettlementBuildOrderLinesQuery( + array $pack, + array $params, + ?array $linesAssistScopeFlip, + bool $linesSkipPhpDs + ): Query { + $query = self::commissionSettlementBuildFilteredOrderQuery($pack, false); + self::commissionSettlementApplyOrderLinesSqlFilters( + $query, + $pack, + $params, + $linesAssistScopeFlip, + $linesSkipPhpDs + ); + + return $query; + } + + /** + * @return array + */ + private static function commissionSettlementOrderLinesSelectFields(string $att): array + { + return [ + 'o.id', + 'o.order_no', + 'o.prescription_id', + 'o.tracking_number', + 'o.express_company', + 'o.recipient_phone', + 'o.diagnosis_id', + 'o.create_time', + 'o.amount', + 'dg.patient_name', + 'dg.phone AS patient_phone', + 'dg.gender AS patient_gender', + Db::raw("({$att}) AS assistant_id"), + Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'), + ]; + } + + /** + * @param list> $rows + * + * @return list> + */ + private static function commissionSettlementFormatOrderLineRows( + array $rows, + array $pack, + array $carryFlip, + int $cutoffTs, + array $linkedApptByOrderId, + array $apptChannelsDictMap, + array $adminNames + ): array { + $tableRowDeptIds = $pack['tableRowDeptIds']; + $deptById = $pack['deptById']; + $adminToPrimary = $pack['adminToPrimary']; + + $aids = []; + foreach ($rows as $r) { + $aid = (int) ($r['assistant_id'] ?? 0); + if ($aid > 0) { + $aids[] = $aid; + } + } + $adminToTable = ($tableRowDeptIds !== [] && $deptById !== [] && $aids !== []) + ? self::batchMapPerformanceAdminsToTableDept( + array_values(array_unique($aids)), + $tableRowDeptIds, + $deptById + ) + : []; + + $out = []; + foreach ($rows as $r) { + $oid = (int) ($r['id'] ?? 0); + $aid = (int) ($r['assistant_id'] ?? 0); + $doctorId = (int) ($r['doctor_id'] ?? 0); + $dgId = (int) ($r['diagnosis_id'] ?? 0); + $signTs = (int) ($r['sign_ts'] ?? 0); + $payTs = (int) ($r['pay_ts'] ?? 0); + $bucketMeta = self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs); + $eligible = $bucketMeta['eligible']; + $b = $eligible ? 'current' : 'deferred'; + + $apDet = $linkedApptByOrderId[$oid] ?? null; + $chForLabel = \is_array($apDet) ? (int) ($apDet['channels'] ?? 0) : 0; + $apptDateRaw = \is_array($apDet) ? trim((string) ($apDet['appointment_date'] ?? '')) : ''; + $apptStatus = \is_array($apDet) ? (int) ($apDet['appointment_status'] ?? 0) : 0; + $trackingNo = trim((string) ($r['tracking_number'] ?? '')); + + $primaryDeptId = (int) ($adminToTable[$aid] ?? 0); + if ($primaryDeptId <= 0) { + $primaryDeptId = (int) ($adminToPrimary[$aid] ?? 0); + } + + $patientName = trim((string) ($r['patient_name'] ?? '')); + $patientPhone = trim((string) ($r['patient_phone'] ?? '')); + $patientGender = (int) ($r['patient_gender'] ?? 0); + $patientGenderText = $patientGender === 1 ? '男' : ($patientGender === 2 ? '女' : ''); + + $out[] = [ + 'id' => $oid, + 'order_no' => (string) ($r['order_no'] ?? ''), + 'tracking_number' => $trackingNo, + 'express_company' => trim((string) ($r['express_company'] ?? '')), + 'diagnosis_id' => $dgId, + 'patient_name' => $patientName, + 'patient_phone' => $patientPhone, + 'patient_gender' => $patientGender, + 'patient_gender_text' => $patientGenderText, + 'amount' => round((float) ($r['amount'] ?? 0), 2), + 'create_time' => (int) ($r['create_time'] ?? 0), + 'create_time_text' => !empty($r['create_time']) ? date('Y-m-d H:i:s', (int) $r['create_time']) : '', + 'sign_ts' => $signTs, + 'sign_time_text' => $signTs > 0 ? date('Y-m-d H:i:s', $signTs) : '', + 'pay_ts' => $payTs, + 'pay_time_text' => $payTs > 0 ? date('Y-m-d H:i:s', $payTs) : '', + 'assistant_id' => $aid, + 'doctor_id' => $doctorId, + 'assistant_name' => $aid > 0 ? (string) ($adminNames[$aid] ?? ('#' . $aid)) : '—', + 'doctor_name' => $doctorId > 0 ? (string) ($adminNames[$doctorId] ?? ('#' . $doctorId)) : '未关联开方人', + 'bucket' => $b, + 'bucket_defer_code' => $eligible ? '' : (string) ($bucketMeta['defer_code'] ?? ''), + 'bucket_defer_text' => $eligible ? '' : (string) ($bucketMeta['defer_text'] ?? ''), + 'is_carry_in' => $oid > 0 && isset($carryFlip[$oid]) ? 1 : 0, + 'primary_dept_id' => $primaryDeptId, + 'primary_dept_name' => $primaryDeptId > 0 ? self::formatYejiDeptRowDisplayName($primaryDeptId, $deptById) : '', + 'appointment_id' => \is_array($apDet) ? (int) ($apDet['appointment_id'] ?? 0) : 0, + 'appointment_date' => $apptDateRaw, + 'appointment_date_text' => $apptDateRaw !== '' ? $apptDateRaw : '', + 'appointment_status' => $apptStatus, + 'appt_channel_value' => $chForLabel, + 'appt_channel_name' => self::commissionSettlementApptChannelLabel($chForLabel, $apptChannelsDictMap), + ]; + } + + return $out; + } + + /** + * bucket=current|deferred:分块拉取候选单 id 并批量补签收/尾款(仅库表),避免一次性加载全量导致 504。 + * + * @return array{0: int[], 1: array} + */ + private static function commissionSettlementCollectBucketMatchingOrders( + array $pack, + array $params, + ?array $linesAssistScopeFlip, + bool $linesSkipPhpDs, + int $cutoffTs, + string $bucket, + int $chunkSize = 400 + ): array { + $matchingIds = []; + $signPayById = []; + $cursor = PHP_INT_MAX; + + while (true) { + $chunkQuery = self::commissionSettlementBuildOrderLinesQuery( + $pack, + $params, + $linesAssistScopeFlip, + $linesSkipPhpDs + ); + $chunkRows = $chunkQuery + ->where('o.id', '<', $cursor) + ->field([ + 'o.id', + 'o.tracking_number', + 'o.express_company', + 'o.recipient_phone', + ]) + ->order('o.id', 'desc') + ->limit($chunkSize) + ->select() + ->toArray(); + + if ($chunkRows === []) { + break; + } + + $minId = PHP_INT_MAX; + foreach ($chunkRows as $r) { + $oid = (int) ($r['id'] ?? 0); + if ($oid > 0 && $oid < $minId) { + $minId = $oid; + } + } + if ($minId === PHP_INT_MAX) { + break; + } + $cursor = $minId; + + $chunkRows = self::commissionSettlementHydrateSignPay($chunkRows, false); + foreach ($chunkRows as $r) { + $oid = (int) ($r['id'] ?? 0); + if ($oid <= 0) { + continue; + } + $signTs = (int) ($r['sign_ts'] ?? 0); + $payTs = (int) ($r['pay_ts'] ?? 0); + $eligible = self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs)['eligible']; + $b = $eligible ? 'current' : 'deferred'; + if ($bucket === 'current' && $b !== 'current') { + continue; + } + if ($bucket === 'deferred' && $b !== 'deferred') { + continue; + } + $matchingIds[] = $oid; + $signPayById[$oid] = ['sign_ts' => $signTs, 'pay_ts' => $payTs]; + } + } + + return [$matchingIds, $signPayById]; + } + /** * 提成核对:订单明细(分页)。 * - * @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, page?: int|string, page_size?: int|string, bucket?: string, assistant_id?: int|string, doctor_id?: int|string, appt_channel_value?: int|string|null} $params bucket: 空|current|deferred;appt_channel_value 与其它筛选同时传递时一并生效(含 0=未匹配挂号渠道) + * @param array{settlement_month?: string, dept_ids?: int[]|string, table_row_dept_id?: int|string, channel_code?: string, page?: int|string, page_size?: int|string, bucket?: string, assistant_id?: int|string, doctor_id?: int|string, appt_channel_value?: int|string|null} $params table_row_dept_id:点击部门行下钻时传入,与 overview foldPerformanceRowsToDeptByTable 同源;dept_ids 仍表示当前页部门筛选上下文(父级钻取),勿仅用叶子部门 id 替代 * * @return array */ @@ -6210,19 +6748,13 @@ class YejiStatsLogic $pack = self::commissionSettlementResolveBasePack($params, $viewerAdminId, $viewerAdminInfo); $cutoffTs = $pack['cutoffTs']; $tableRowDeptIds = $pack['tableRowDeptIds']; - $deptById = $pack['deptById']; $adminToPrimary = $pack['adminToPrimary']; $carryFlip = array_flip($pack['carryOrderIds']); + $att = $pack['att']; $bucket = trim((string) ($params['bucket'] ?? '')); $page = max(1, (int) ($params['page'] ?? 1)); $pageSize = min(100, max(10, (int) ($params['page_size'] ?? 20))); - $filterAssistantId = max(0, (int) ($params['assistant_id'] ?? 0)); - $filterDoctorId = max(0, (int) ($params['doctor_id'] ?? 0)); - $filterApptChannelActive = array_key_exists('appt_channel_value', $params) - && $params['appt_channel_value'] !== null - && $params['appt_channel_value'] !== ''; - $filterApptChannelVal = $filterApptChannelActive ? (int) $params['appt_channel_value'] : null; if ($tableRowDeptIds === [] || $adminToPrimary === []) { return ['list' => [], 'count' => 0, 'page' => $page, 'page_size' => $pageSize]; @@ -6241,160 +6773,135 @@ class YejiStatsLogic } $linesSkipPhpDs = !empty($pack['skipPhpDataScopeAttributionFilter']); + $bucketNeedsPhp = ($bucket === 'current' || $bucket === 'deferred'); + $selectFields = self::commissionSettlementOrderLinesSelectFields($att); - $query = self::commissionSettlementBuildFilteredOrderQuery($pack); - $att = $pack['att']; - $signPayFields = self::commissionSettlementSignPayFieldRawList(); - - $baseRows = $query->field(array_merge([ - 'o.id', - 'o.order_no', - 'o.tracking_number', - 'o.express_company', - 'o.recipient_phone', - 'o.diagnosis_id', - 'o.create_time', - 'o.amount', - Db::raw("({$att}) AS assistant_id"), - Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'), - ], $signPayFields))->order('o.id', 'desc')->select()->toArray(); - - $diagIdSet = []; - foreach ($baseRows as $r) { - $dg = (int) ($r['diagnosis_id'] ?? 0); - if ($dg > 0) { - $diagIdSet[$dg] = true; - } - } - $diagKeys = array_keys($diagIdSet); - $diagToApptChannel = self::commissionSettlementDiagnosisLatestApptChannel($diagKeys); - $diagToApptDetail = self::commissionSettlementDiagnosisLatestCompletedAppointment($diagKeys); - $chValsForDict = []; - foreach ($diagToApptChannel as $v) { - $chValsForDict[] = (int) $v; - } - foreach ($diagToApptDetail as $drow) { - $chValsForDict[] = (int) ($drow['channels'] ?? 0); - } - $apptChannelsDictMap = self::commissionSettlementApptChannelsDictMap($chValsForDict); - - $mapped = []; - foreach ($baseRows as $r) { - $dgId = (int) ($r['diagnosis_id'] ?? 0); - $apCh = (int) ($diagToApptChannel[$dgId] ?? 0); - if ($filterApptChannelActive && $apCh !== (int) $filterApptChannelVal) { - continue; - } - $preAid = (int) ($r['assistant_id'] ?? 0); - if (!$linesSkipPhpDs && $linesAssistScopeFlip !== null && ($preAid <= 0 || !isset($linesAssistScopeFlip[$preAid]))) { - continue; - } - if (!empty($cPack['explicitDeptFilter']) && ($preAid <= 0 || !isset($adminToPrimary[$preAid]))) { - continue; - } - $preDoctorId = (int) ($r['doctor_id'] ?? 0); - if ($filterAssistantId > 0 && $preAid !== $filterAssistantId) { - continue; - } - if ($filterDoctorId > 0 && $preDoctorId !== $filterDoctorId) { - continue; - } - $signTs = (int) ($r['sign_ts'] ?? 0); - $payTs = (int) ($r['pay_ts'] ?? 0); - $tnForSig = trim((string) ($r['tracking_number'] ?? '')); - if ($signTs <= 0 && $tnForSig !== '') { - $signTs = ExpressTrackingService::resolveSignUnixTimeForCommission( - $tnForSig, - (string) ($r['express_company'] ?? 'auto'), - (string) ($r['recipient_phone'] ?? ''), - true - ); - } - $bucketMeta = self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs); - $eligible = $bucketMeta['eligible']; - $b = $eligible ? 'current' : 'deferred'; - if ($bucket === 'current' && $b !== 'current') { - continue; - } - if ($bucket === 'deferred' && $b !== 'deferred') { - continue; - } - $oid = (int) ($r['id'] ?? 0); - $aid = $preAid; - $doctorId = $preDoctorId; - $apDet = $diagToApptDetail[$dgId] ?? null; - $chForLabel = \is_array($apDet) ? (int) ($apDet['channels'] ?? 0) : 0; - if ($chForLabel <= 0) { - $chForLabel = (int) ($diagToApptChannel[$dgId] ?? 0); - } - $apptDateRaw = \is_array($apDet) ? trim((string) ($apDet['appointment_date'] ?? '')) : ''; - $apptStatus = \is_array($apDet) ? (int) ($apDet['appointment_status'] ?? 0) : 0; - $trackingNo = trim((string) ($r['tracking_number'] ?? '')); - $foldRows = [['assistant_id' => $aid, 'cnt' => 1]]; - $byDept = self::foldPerformanceRowsToDeptByTable($foldRows, $adminToPrimary, $tableRowDeptIds, $deptById, 'cnt'); - $primaryDeptId = 0; - foreach ($byDept as $did => $_c) { - $primaryDeptId = (int) $did; - break; + if (!$bucketNeedsPhp) { + $total = (int) self::commissionSettlementBuildOrderLinesQuery( + $pack, + $params, + $linesAssistScopeFlip, + $linesSkipPhpDs + )->count(); + if ($total <= 0) { + return ['list' => [], 'count' => 0, 'page' => $page, 'page_size' => $pageSize]; } - $mapped[] = [ - 'id' => $oid, - 'order_no' => (string) ($r['order_no'] ?? ''), - 'tracking_number' => $trackingNo, - 'express_company' => trim((string) ($r['express_company'] ?? '')), - 'diagnosis_id' => (int) ($r['diagnosis_id'] ?? 0), - 'amount' => round((float) ($r['amount'] ?? 0), 2), - 'create_time' => (int) ($r['create_time'] ?? 0), - 'create_time_text' => !empty($r['create_time']) ? date('Y-m-d H:i:s', (int) $r['create_time']) : '', - 'sign_ts' => $signTs, - 'sign_time_text' => $signTs > 0 ? date('Y-m-d H:i:s', $signTs) : '', - 'pay_ts' => $payTs, - 'pay_time_text' => $payTs > 0 ? date('Y-m-d H:i:s', $payTs) : '', - 'assistant_id' => $aid, - 'doctor_id' => $doctorId, - 'assistant_name' => '', - 'doctor_name' => '', - 'bucket' => $b, - 'bucket_defer_code' => $eligible ? '' : (string) ($bucketMeta['defer_code'] ?? ''), - 'bucket_defer_text' => $eligible ? '' : (string) ($bucketMeta['defer_text'] ?? ''), - 'is_carry_in' => $oid > 0 && isset($carryFlip[$oid]) ? 1 : 0, - 'primary_dept_id' => $primaryDeptId, - 'primary_dept_name' => $primaryDeptId > 0 ? self::formatYejiDeptRowDisplayName($primaryDeptId, $deptById) : '', - 'appointment_id' => \is_array($apDet) ? (int) ($apDet['appointment_id'] ?? 0) : 0, - 'appointment_date' => $apptDateRaw, - 'appointment_date_text' => $apptDateRaw !== '' ? $apptDateRaw : '', - 'appointment_status' => $apptStatus, - 'appt_channel_value' => $chForLabel, - 'appt_channel_name' => self::commissionSettlementApptChannelLabel($chForLabel, $apptChannelsDictMap), + $pageRows = self::commissionSettlementBuildOrderLinesQuery( + $pack, + $params, + $linesAssistScopeFlip, + $linesSkipPhpDs + ) + ->field($selectFields) + ->order('o.id', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + $pageRows = self::commissionSettlementHydrateSignPay($pageRows, false); + + [$linkedApptByOrderId, $apptChannelsDictMap] = + self::commissionSettlementLoadApptContextForOrders($pageRows); + + $nameIds = []; + foreach ($pageRows as $r) { + $na = (int) ($r['assistant_id'] ?? 0); + if ($na > 0) { + $nameIds[$na] = true; + } + $nd = (int) ($r['doctor_id'] ?? 0); + if ($nd > 0) { + $nameIds[$nd] = true; + } + } + $adminNames = self::commissionSettlementBatchAdminNames(array_map('intval', array_keys($nameIds))); + + $list = self::commissionSettlementFormatOrderLineRows( + $pageRows, + $pack, + $carryFlip, + $cutoffTs, + $linkedApptByOrderId, + $apptChannelsDictMap, + $adminNames + ); + + return [ + 'list' => $list, + 'count' => $total, + 'page' => $page, + 'page_size' => $pageSize, ]; } + // bucket=current|deferred:分块判定签收/尾款后再 PHP 分页 + [$matchingIds, $signPayById] = self::commissionSettlementCollectBucketMatchingOrders( + $pack, + $params, + $linesAssistScopeFlip, + $linesSkipPhpDs, + $cutoffTs, + $bucket + ); + if ($matchingIds === []) { + return ['list' => [], 'count' => 0, 'page' => $page, 'page_size' => $pageSize]; + } + + $total = \count($matchingIds); + $pageIds = array_slice($matchingIds, ($page - 1) * $pageSize, $pageSize); + if ($pageIds === []) { + return ['list' => [], 'count' => $total, 'page' => $page, 'page_size' => $pageSize]; + } + + $pageQuery = self::commissionSettlementBuildOrderLinesQuery( + $pack, + $params, + $linesAssistScopeFlip, + $linesSkipPhpDs + ); + $pageRows = $pageQuery + ->whereIn('o.id', $pageIds) + ->field($selectFields) + ->order('o.id', 'desc') + ->select() + ->toArray(); + foreach ($pageRows as &$r) { + $oid = (int) ($r['id'] ?? 0); + if ($oid > 0 && isset($signPayById[$oid])) { + $r['sign_ts'] = $signPayById[$oid]['sign_ts']; + $r['pay_ts'] = $signPayById[$oid]['pay_ts']; + } + } + unset($r); + + [$linkedApptByOrderId, $apptChannelsDictMap] = + self::commissionSettlementLoadApptContextForOrders($pageRows); + $nameIds = []; - foreach ($mapped as $row) { - $na = (int) ($row['assistant_id'] ?? 0); + foreach ($pageRows as $r) { + $na = (int) ($r['assistant_id'] ?? 0); if ($na > 0) { $nameIds[$na] = true; } - $nd = (int) ($row['doctor_id'] ?? 0); + $nd = (int) ($r['doctor_id'] ?? 0); if ($nd > 0) { $nameIds[$nd] = true; } } $adminNames = self::commissionSettlementBatchAdminNames(array_map('intval', array_keys($nameIds))); - foreach ($mapped as &$row) { - $na = (int) ($row['assistant_id'] ?? 0); - $nd = (int) ($row['doctor_id'] ?? 0); - $row['assistant_name'] = $na > 0 ? (string) ($adminNames[$na] ?? ('#' . $na)) : '—'; - $row['doctor_name'] = $nd > 0 ? (string) ($adminNames[$nd] ?? ('#' . $nd)) : '未关联开方人'; - } - unset($row); - $total = count($mapped); - $slice = array_slice($mapped, ($page - 1) * $pageSize, $pageSize); + $list = self::commissionSettlementFormatOrderLineRows( + $pageRows, + $pack, + $carryFlip, + $cutoffTs, + $linkedApptByOrderId, + $apptChannelsDictMap, + $adminNames + ); return [ - 'list' => $slice, + 'list' => $list, 'count' => $total, 'page' => $page, 'page_size' => $pageSize, @@ -6502,35 +7009,24 @@ class YejiStatsLogic $overview = self::commissionSettlementOverview($params, $viewerAdminId, $viewerAdminInfo); $total = $overview['total'] ?? []; - $query = self::commissionSettlementBuildFilteredOrderQuery($pack); + $query = self::commissionSettlementBuildFilteredOrderQuery($pack, false); $att = $pack['att']; - $signPayFields = self::commissionSettlementSignPayFieldRawList(); $cutoffTs = $pack['cutoffTs']; - $rowsRaw = $query->field(array_merge([ + $rowsRaw = $query->field([ 'o.id', 'o.create_time', 'o.tracking_number', 'o.express_company', 'o.recipient_phone', Db::raw("({$att}) AS assistant_id"), - ], $signPayFields))->select()->toArray(); + ])->select()->toArray(); + $rowsRaw = self::commissionSettlementHydrateSignPay($rowsRaw, false); $deferredIds = []; foreach ($rowsRaw as $r) { $signTs = (int) ($r['sign_ts'] ?? 0); $payTs = (int) ($r['pay_ts'] ?? 0); - if ($signTs <= 0) { - $tn0 = trim((string) ($r['tracking_number'] ?? '')); - if ($tn0 !== '') { - $signTs = ExpressTrackingService::resolveSignUnixTimeForCommission( - $tn0, - (string) ($r['express_company'] ?? 'auto'), - (string) ($r['recipient_phone'] ?? ''), - true - ); - } - } if (!self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs)['eligible']) { $deferredIds[] = (int) ($r['id'] ?? 0); } diff --git a/server/app/common/service/ExpressTrackingService.php b/server/app/common/service/ExpressTrackingService.php index a4838063..6fddfc3c 100755 --- a/server/app/common/service/ExpressTrackingService.php +++ b/server/app/common/service/ExpressTrackingService.php @@ -1291,4 +1291,121 @@ class ExpressTrackingService return false; } + + /** + * 提成结算 overview 等大批量场景:按订单批量从库表推导签收时间(不调用快递100)。 + * 语义与 YejiStatsLogic 物流子查询 / effectiveSignUnixFromTrackingDetail 对齐。 + * + * @param list $items + * + * @return array order_id => sign unix + */ + public static function batchResolveSignUnixFromDbForOrders(array $items): array + { + if ($items === []) { + return []; + } + + $orderIds = []; + $tnByOrder = []; + $trackingNums = []; + foreach ($items as $it) { + if (!is_array($it)) { + continue; + } + $oid = (int) ($it['order_id'] ?? 0); + if ($oid <= 0) { + continue; + } + $orderIds[$oid] = true; + $tn = trim((string) ($it['tracking_number'] ?? '')); + $tnByOrder[$oid] = $tn; + if ($tn !== '') { + $trackingNums[$tn] = true; + } + } + $orderIds = array_keys($orderIds); + if ($orderIds === []) { + return []; + } + + $trackingById = []; + $orderToTrackingIds = []; + $tnToTrackingIds = []; + + $attachTrackingRow = static function (array $etRow) use (&$trackingById, &$orderToTrackingIds, &$tnToTrackingIds): void { + $tid = (int) ($etRow['id'] ?? 0); + if ($tid <= 0) { + return; + } + $trackingById[$tid] = $etRow; + $oid = (int) ($etRow['order_id'] ?? 0); + if ($oid > 0) { + $orderToTrackingIds[$oid][$tid] = true; + } + $tn = trim((string) ($etRow['tracking_number'] ?? '')); + if ($tn !== '') { + $tnToTrackingIds[$tn][$tid] = true; + } + }; + + foreach (array_chunk($orderIds, 800) as $chunk) { + $byOrderId = ExpressTracking::with(['traces' => static function ($query): void { + $query->order('trace_time_stamp', 'desc'); + }]) + ->whereIn('order_id', $chunk) + ->whereNull('delete_time') + ->select(); + foreach ($byOrderId as $tracking) { + $row = $tracking->toArray(); + $row['traces'] = $tracking->traces ? $tracking->traces->toArray() : []; + $attachTrackingRow($row); + } + } + + $trackingNumList = array_keys($trackingNums); + if ($trackingNumList !== []) { + foreach (array_chunk($trackingNumList, 400) as $chunk) { + $byTn = ExpressTracking::with(['traces' => static function ($query): void { + $query->order('trace_time_stamp', 'desc'); + }]) + ->whereRaw('TRIM(`tracking_number`) IN (' . implode(',', array_fill(0, count($chunk), '?')) . ')', $chunk) + ->whereNull('delete_time') + ->select(); + foreach ($byTn as $tracking) { + $row = $tracking->toArray(); + $row['traces'] = $tracking->traces ? $tracking->traces->toArray() : []; + $attachTrackingRow($row); + } + } + } + + $out = []; + foreach ($orderIds as $oid) { + $oid = (int) $oid; + $candidateIds = []; + foreach (array_keys($orderToTrackingIds[$oid] ?? []) as $tid) { + $candidateIds[(int) $tid] = true; + } + $tn = $tnByOrder[$oid] ?? ''; + if ($tn !== '') { + foreach (array_keys($tnToTrackingIds[$tn] ?? []) as $tid) { + $candidateIds[(int) $tid] = true; + } + } + $best = 0; + foreach (array_keys($candidateIds) as $tid) { + $detail = $trackingById[(int) $tid] ?? null; + if (!is_array($detail)) { + continue; + } + $best = max($best, self::effectiveSignUnixFromTrackingDetail($detail)); + } + if ($best > 0) { + $out[$oid] = $best; + } + } + + return $out; + } }