diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 9e743a70..2c0c625c 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -292,7 +292,7 @@ :fetch-fun="prescriptionOrderExport" :params="prescriptionOrderExportParams" :page-size="pager.size" - export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示。「签收日期」与详情/业绩看板同源,仅读物流库(轨迹/签收时间):导出不再实时查快递100,速度只取决于数据库;签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库,刚发货尚未同步的单子会暂时为空,待下次回填/定时任务刷新后显示。" + export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示。「关联收款记录」与详情侧栏同源(已支付/已退款/待审核),每笔两行展示(摘要行+明细行),多笔空行分隔,单元格自动换行。「签收日期」与详情/业绩看板同源,仅读物流库(轨迹/签收时间):导出不再实时查快递100,速度只取决于数据库;签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库,刚发货尚未同步的单子会暂时为空,待下次回填/定时任务刷新后显示。" /> diff --git a/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php b/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php index 749f6c3a..2475872a 100755 --- a/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php +++ b/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php @@ -9,6 +9,7 @@ use app\adminapi\logic\dept\DeptLogic; use app\adminapi\logic\stats\YejiStatsLogic; use app\adminapi\logic\tcm\PrescriptionOrderLogic; use app\common\enum\ExportEnum; +use app\common\cache\ExportCache; use app\common\lists\ListsExcelInterface; use app\common\lists\ListsExtendInterface; use app\common\lists\ListsSearchInterface; @@ -26,6 +27,12 @@ use app\common\service\gancao\GancaoScmRecipelService; use think\facade\Config; use think\facade\Db; use think\db\Query; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Alignment; +use PhpOffice\PhpSpreadsheet\Style\Border; +use PhpOffice\PhpSpreadsheet\Style\Fill; class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface, ListsExcelInterface { @@ -783,6 +790,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn 'export_medication_days' => '天数', 'export_amount' => '总金额', 'export_paid_amount' => '已付金额', + 'export_linked_pay_records' => '关联收款记录', 'export_refund_amount' => '退款金额', 'export_agency_collect' => '代收金额', 'export_tracking_number' => '快递单号', @@ -795,6 +803,133 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn ]; } + /** + * 处方业务订单导出:深色表头、斑马纹、长文本自动换行(覆盖默认亮蓝表头) + * + * @param array $excelFields + * @param array> $lists + */ + public function createExcel($excelFields, $lists): string + { + $title = array_values($excelFields); + $fieldKeys = array_keys($excelFields); + + $data = []; + foreach ($lists as $row) { + $temp = []; + foreach ($excelFields as $key => $excelField) { + $fieldData = $row[$key] ?? ''; + if (is_numeric($fieldData) && strlen((string) $fieldData) >= 12) { + $fieldData .= "\t"; + } + $temp[$key] = $fieldData; + } + $data[] = $temp; + } + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('处方业务订单'); + + foreach ($title as $key => $value) { + $sheet->setCellValueByColumnAndRow($key + 1, 1, $value); + } + + $rowNum = 2; + foreach ($data as $item) { + $column = 1; + foreach ($item as $value) { + $sheet->setCellValueByColumnAndRow($column, $rowNum, $value); + ++$column; + } + ++$rowNum; + } + + $highest = $sheet->getHighestRowAndColumn(); + $highestRow = (int) $highest['row']; + $lastColumn = (string) $highest['column']; + $titleScope = 'A1:' . $lastColumn . '1'; + $allScope = 'A1:' . $lastColumn . $highestRow; + $bodyScope = 'A2:' . $lastColumn . $highestRow; + + $sheet->freezePane('A2'); + $sheet->getRowDimension(1)->setRowHeight(26); + + $sheet->getStyle($titleScope)->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB('FF334155'); + $sheet->getStyle($titleScope)->getFont()->getColor()->setARGB('FFFFFFFF'); + $sheet->getStyle($titleScope)->getFont()->setBold(true); + $sheet->getStyle($titleScope)->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER) + ->setVertical(Alignment::VERTICAL_CENTER); + + $sheet->getStyle($allScope)->getBorders()->getAllBorders() + ->setBorderStyle(Border::BORDER_THIN) + ->getColor()->setARGB('FFE2E8F0'); + + $sheet->getStyle($bodyScope)->getAlignment() + ->setVertical(Alignment::VERTICAL_TOP) + ->setWrapText(true); + + $wrapWideKeys = [ + 'export_linked_pay_records', + 'export_prescription_name', + 'export_guahao_channel_source', + 'export_assistant_dept', + 'export_service_package', + ]; + $fixedWidths = [ + 'export_fulfillment_status_text' => 12, + 'export_order_time' => 18, + 'export_patient_gender' => 6, + 'export_patient_age' => 6, + 'export_medication_days' => 6, + 'export_amount' => 10, + 'export_paid_amount' => 10, + 'export_refund_amount' => 10, + 'export_agency_collect' => 10, + 'export_sign_time' => 12, + 'export_supply_mode' => 10, + 'export_linked_pay_records' => 52, + 'export_prescription_name' => 34, + 'export_guahao_channel_source' => 22, + 'export_assistant_dept' => 24, + 'export_service_package' => 18, + 'export_tracking_number' => 18, + ]; + + foreach ($fieldKeys as $idx => $fieldKey) { + $colLetter = Coordinate::stringFromColumnIndex($idx + 1); + if (isset($fixedWidths[$fieldKey])) { + $sheet->getColumnDimension($colLetter)->setWidth($fixedWidths[$fieldKey]); + } elseif (!in_array($fieldKey, $wrapWideKeys, true)) { + $sheet->getColumnDimension($colLetter)->setAutoSize(true); + } else { + $sheet->getColumnDimension($colLetter)->setWidth(28); + } + } + + for ($r = 2; $r <= $highestRow; ++$r) { + if ($r % 2 === 0) { + $sheet->getStyle('A' . $r . ':' . $lastColumn . $r)->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB('FFF8FAFC'); + } + } + + $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); + $exportCache = new ExportCache(); + $src = $exportCache->getSrc(); + if (!file_exists($src)) { + mkdir($src, 0775, true); + } + $writer->save($src . $this->fileName); + $vars = ['file' => $exportCache->setFile($this->fileName)]; + + return (string) url('adminapi/download/export', $vars, true, true); + } + /** * 业绩看板侧栏专用:约诊/接诊条数,与 YejiStatsLogic::applyConsultFiltersAlignedWithAppointmentLists 同口径 * (doctor_appointment.status=3,appointment_date 落入区间,有效医助 COALESCE(挂号.assistant_id,诊单.assistant_id))。 diff --git a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php index 4bbf6602..83bab5a4 100755 --- a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php +++ b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php @@ -3161,6 +3161,193 @@ class PrescriptionOrderLogic return $out; } + /** + * 导出用:统一解析时间(支持 Unix 秒/毫秒与 Y-m-d H:i:s 字符串,与前端 formatOrderTime 同口径) + * + * @param mixed $value + */ + private static function formatDateTimeForExport($value, string $format = 'Y-m-d H:i'): string + { + if ($value === null || $value === '' || $value === false) { + return ''; + } + if (\is_int($value) || \is_float($value)) { + $ts = (int) $value; + if ($ts > 9999999999) { + $ts = (int) floor($ts / 1000); + } + + return $ts > 0 ? date($format, $ts) : ''; + } + $str = trim((string) $value); + if ($str === '') { + return ''; + } + if (preg_match('/^\d+$/', $str)) { + $ts = (int) $str; + if ($ts > 9999999999) { + $ts = (int) floor($ts / 1000); + } + + return $ts > 0 ? date($format, $ts) : ''; + } + if (str_contains($str, '-') || str_contains($str, '/')) { + $ts = strtotime($str); + + return $ts !== false ? date($format, $ts) : $str; + } + + return $str; + } + + /** + * 导出用:支付单来源/方式(与前端 formatPayOrderSource 同口径) + * + * @param array $row + */ + private static function formatPayOrderSourceForExport(array $row): string + { + $createType = trim((string) ($row['create_type'] ?? '')); + if ($createType === 'wechat_work') { + return '企业微信对外收款'; + } + if ($createType === 'fubei') { + return '付呗'; + } + $paymentMethod = trim((string) ($row['payment_method'] ?? '')); + $methodMap = [ + 'alipay' => '支付宝', + 'wechat' => '微信', + 'wechat_work' => '企业微信', + 'fubei' => '付呗', + 'manual' => '手动确认到账', + ]; + if ($paymentMethod !== '' && isset($methodMap[$paymentMethod])) { + return $methodMap[$paymentMethod]; + } + + return '普通订单'; + } + + /** + * 导出用:单条关联收款可读文本(两行结构,便于 Excel 自动换行阅读) + * + * @param array $pay + * @param array $creatorNames + */ + private static function formatLinkedPayRecordLineForExport(array $pay, array $creatorNames, int $index = 1): string + { + $typeMap = [ + 1 => '挂号费', + 2 => '问诊费', + 3 => '药品费用', + 4 => '首付费用', + 5 => '尾款费用', + 6 => '其他费用', + 7 => '全部费用', + 8 => '驼奶费用', + ]; + $statusMap = [ + 1 => '待支付', + 2 => '已支付', + 3 => '已取消', + 4 => '已退款', + 5 => '待审核', + ]; + $orderNo = trim((string) ($pay['order_no'] ?? '')); + $amount = number_format(round((float) ($pay['amount'] ?? 0), 2), 2, '.', ''); + $typeDesc = $typeMap[(int) ($pay['order_type'] ?? 0)] ?? '—'; + $statusDesc = $statusMap[(int) ($pay['status'] ?? 0)] ?? '—'; + $source = self::formatPayOrderSourceForExport($pay); + $cid = (int) ($pay['creator_id'] ?? 0); + $creator = $cid > 0 ? (string) ($creatorNames[$cid] ?? '') : '—'; + $timeStr = self::formatDateTimeForExport($pay['create_time'] ?? '') ?: '—'; + + $line1 = sprintf('[%d] %s ¥%s %s %s', $index, $orderNo, $amount, $typeDesc, $statusDesc); + $line2 = sprintf(' %s %s %s', $source, $creator, $timeStr); + + return $line1 . "\n" . $line2; + } + + /** + * 导出用:批量解析业务订单关联收款记录(与详情 linked_pay_orders 同口径:status ∈ {2,4,5}) + * + * @param int[] $poIds + * + * @return array prescription_order_id => 多笔以空行分隔的可读文本 + */ + private static function batchLinkedPayRecordsExportTextByPoIds(array $poIds): array + { + $poIds = array_values(array_filter(array_unique(array_map('intval', $poIds)), static fn (int $id): bool => $id > 0)); + if ($poIds === []) { + return []; + } + + $linksByPo = []; + $allPayIds = []; + $linkRows = PrescriptionOrderPayOrder::whereIn('prescription_order_id', $poIds) + ->field(['prescription_order_id', 'pay_order_id']) + ->order('id', 'asc') + ->select() + ->toArray(); + foreach ($linkRows as $l) { + $poid = (int) ($l['prescription_order_id'] ?? 0); + $payId = (int) ($l['pay_order_id'] ?? 0); + if ($poid <= 0 || $payId <= 0) { + continue; + } + $linksByPo[$poid][] = $payId; + $allPayIds[$payId] = true; + } + + $out = []; + foreach ($poIds as $poid) { + $out[$poid] = ''; + } + if ($allPayIds === []) { + return $out; + } + + $payRows = Order::whereIn('id', array_keys($allPayIds)) + ->whereNull('delete_time') + ->whereIn('status', [2, 4, 5]) + ->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'payment_method', 'create_type']) + ->select() + ->toArray(); + $payById = []; + $creatorIds = []; + foreach ($payRows as $r) { + $id = (int) ($r['id'] ?? 0); + if ($id <= 0) { + continue; + } + $payById[$id] = $r; + $cid = (int) ($r['creator_id'] ?? 0); + if ($cid > 0) { + $creatorIds[$cid] = true; + } + } + $creatorNames = $creatorIds !== [] + ? Admin::whereIn('id', array_keys($creatorIds))->column('name', 'id') + : []; + + foreach ($poIds as $poid) { + $lines = []; + $seq = 0; + foreach ($linksByPo[$poid] ?? [] as $payId) { + $pay = $payById[$payId] ?? null; + if (!\is_array($pay)) { + continue; + } + ++$seq; + $lines[] = self::formatLinkedPayRecordLineForExport($pay, $creatorNames, $seq); + } + $out[$poid] = implode("\n\n", $lines); + } + + return $out; + } + /** * 业务订单列表导出:写入与 PrescriptionOrderLists::setExcelFields 对应的字段(export=2 时由列表类调用) * 「挂号渠道来源」取值与详情/列表解析一致:处方 appointment_id → 否则诊单下 MAX(挂号.id);业绩抽屉带渠道时与列表同源高亮。 @@ -3392,6 +3579,7 @@ class PrescriptionOrderLogic $firstVisitAssistantByDiag = self::batchFirstVisitAssistantNameByDiagnosis($diagIdList, $diagById); $assistantDeptCache = []; + $linkedPayExportByPo = self::batchLinkedPayRecordsExportTextByPoIds($poIds); foreach ($lists as &$item) { $poId = (int) ($item['id'] ?? 0); @@ -3464,6 +3652,7 @@ class PrescriptionOrderLogic $paid = round((float) ($item['linked_pay_paid_total'] ?? 0), 2); $item['export_amount'] = number_format($amt, 2, '.', ''); $item['export_paid_amount'] = number_format($paid, 2, '.', ''); + $item['export_linked_pay_records'] = $linkedPayExportByPo[$poId] ?? ''; $refundStored = round((float) ($item['refund_amount'] ?? 0), 2); $item['export_refund_amount'] = $refundStored > 0 ? number_format($refundStored, 2, '.', '')