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 @@
+
+
+
+ {{ row.patient_name || '—' }}
+
+ {{ row.patient_gender_text }}
+ ·
+ {{ row.patient_phone }}
+
+
+
+
{{ row.tracking_number || '—' }}
@@ -492,6 +504,18 @@
+
+
+
+ {{ row.patient_name || '—' }}
+
+ {{ row.patient_gender_text }}
+ ·
+ {{ row.patient_phone }}
+
+
+
+
{{ row.tracking_number || '—' }}
@@ -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;
+ }
}