generatePrivateUrl() 完成 */ class FinanceReconDubaiFilesService { use CrudServiceTrait; use ExcelImportTrait; // 复用通用导入方法 /** 指定当前要操作的模型(用于 CrudServiceTrait) */ protected string $modelClass = FinanceReconDubaiFiles::class; /** 验证器 */ protected $validator = FinanceReconDubaiFilesValidate::class; /** 获取验证器实例 */ public function getValidator() { return new $this->validator; } /** * 写入前置:将上传组件的结构转为 *_url / *_name,并记录创建人 */ public function beforeServiceStore($data, Request $request) { $data['created_by'] = $request->user->uid ?? 0; $data['caiwu_file_url'] = $data['caiwu_file']['url'] ?? ''; $data['caiwu_file_name'] = $data['caiwu_file']['name'] ?? ''; $data['dubai_file_url'] = $data['dubai_file']['url'] ?? ''; $data['dubai_file_name'] = $data['dubai_file']['name'] ?? ''; return $data; } /* ============================================================ * 一、M项目对账明细 导入配置(sheet: M项目对账明细) * ============================================================ */ /** 中文表头 => 目标字段名 */ protected array $headerMapM = [ '车架号' => 'vin', '开票价' => 'invoice_price', '实际采购价' => 'actual_purchase_price', '开票价和实际采购价差额' => 'invoice_vs_purchase_diff', '车源中介费' => 'source_broker_fee', '配件' => 'parts_fee', '停车费' => 'parking_fee', '保险费' => 'insurance_fee', '车辆手续费' => 'vehicle_service_fee', '检测费' => 'inspection_fee', '购置税' => 'purchase_tax', '汽车其他费用' => 'other_vehicle_fee', '转移待出口' => 'transfer_to_export', '国际运费' => 'freight_international', '国内运费1' => 'freight_domestic_1', '国内运费2' => 'freight_domestic_2', '报关费及港杂费' => 'customs_port_fee', '注销费' => 'cancellation_fee', '车船税' => 'vehicle_vessel_tax', '其他财务费用' => 'other_finance_fee', '汽车小额杂费' => 'minor_vehicle_misc', '上牌费' => 'registration_fee', '其他保险费' => 'other_insurance_fee', '油补' => 'fuel_subsidy', '快递费' => 'express_fee', '国内成本合计' => 'domestic_cost_total', '退税额' => 'tax_refund_amount', 'cif杰贝阿里成本价' => 'cif_jebel_ali_cost', '范经理发车看板CIF' => 'fan_manager_cif', '财务-范经理 差额' => 'finance_vs_fan_diff', ]; /** 需要转为 decimal 的目标字段 */ protected array $decimalFieldsM = [ 'invoice_price','actual_purchase_price','invoice_vs_purchase_diff', 'source_broker_fee','parts_fee','parking_fee','insurance_fee','vehicle_service_fee','inspection_fee', 'purchase_tax','other_vehicle_fee','transfer_to_export', 'freight_international','freight_domestic_1','freight_domestic_2','customs_port_fee', 'cancellation_fee','vehicle_vessel_tax','other_finance_fee','minor_vehicle_misc', 'registration_fee','other_insurance_fee','fuel_subsidy','express_fee', 'domestic_cost_total','tax_refund_amount','cif_jebel_ali_cost','fan_manager_cif','finance_vs_fan_diff', ]; /** * M项目对账明细导入 * * @param string $file 可为“签名 URL”或“OSS 对象键” * @param bool $isUrl 若传入的是已签名 URL,请设 true;若是对象键,设 false * @param bool $insertToDb 是否立即入库 * * 返回:['sheet','total_rows','skipped','errors','to_import','inserted','preview_3',('error'?)] */ public function mImport(string $file, bool $isUrl = true, bool $insertToDb = true): array { $sheet = 'M项目对账明细'; // 允许传对象键:当 isUrl=false 且非 http(s) 时,自动用 OssService 签名 if ($isUrl === false && !preg_match('#^https?://#i', $file)) { $file = $this->ossSignedUrl($file); $isUrl = true; } $rows = $this->readSheetRows($file, $sheet, $isUrl, headerRow: 1); $mapped = []; $skipped = 0; $errors = 0; foreach ($rows as $row) { try { if ($this->shouldSkipByVinAliases($row, ['车架号'])) { $skipped++; continue; } $rec = $this->mapAndNormalizeRow($row, $this->headerMapM, $this->decimalFieldsM); $this->stamp($rec); $mapped[] = $rec; } catch (\Throwable $e) { // 单行异常不阻断全局导入 $errors++; continue; } } $result = [ 'sheet' => $sheet, 'total_rows' => count($rows), 'skipped' => $skipped, 'errors' => $errors, 'to_import' => count($mapped), ]; if ($insertToDb) { // 注意表名:按你的业务这里写入 finance_recon_m_detail $ret = $this->batchInsert('finance_recon_m_detail', $mapped); $result['inserted'] = $ret['inserted']; if ($ret['error']) $result['error'] = 'Database insert error: ' . $ret['error']; } else { $result['inserted'] = 0; } $result['preview_3'] = array_slice($mapped, 0, 3); return $result; } /** M项目对账明细:从 OSS 对象键导入(语义化便捷入口) */ public function mImportFromOssKey(string $objectKey, bool $insertToDb = true): array { $signedUrl = $this->ossSignedUrl($objectKey); return $this->mImport($signedUrl, true, $insertToDb); } /** M项目对账明细:从已签名 URL 导入(语义化便捷入口) */ public function mImportFromSignedUrl(string $signedUrl, bool $insertToDb = true): array { return $this->mImport($signedUrl, true, $insertToDb); } /* ============================================================ * 二、迪拜两表合并 导入配置(sheets: Stock in Jebel Ali / Under shipping) * ============================================================ */ /** 表头映射(已兼容不一致列名:VIN NO/车架号、国际运费和杂费/SHIPPING COST) */ protected array $headerMapDubai = [ 'VIN NO' => 'vin', '车架号' => 'vin', '开票价' => 'invoice_price', '中介费' => 'broker_fee', '购置税' => 'purchase_tax', '退税' => 'tax_refund', '不含税车价' => 'price_ex_vat', '费用' => 'fee_total', '国内运费' => 'domestic_freight', 'FCA成本价' => 'fca_cost', 'FCA成本价汇率' => 'fca_fx', '国际运费和杂费' => 'shipping_cost', 'SHIPPING COST' => 'shipping_cost', '运费汇率' => 'shipping_fx', 'CIF 杰贝阿里成本价' => 'cif_jebel_ali_cost', 'CIF RMB' => 'cif_rmb', // Under shipping 没有该列,将在逻辑中推导 ]; /** 需要转为 decimal 的目标字段(汇率字段使用 DECIMAL(18,6) 建表) */ protected array $decimalFieldsDubai = [ 'invoice_price','broker_fee','purchase_tax','tax_refund','price_ex_vat', 'fee_total','domestic_freight','fca_cost','fca_fx', 'shipping_cost','shipping_fx','cif_jebel_ali_cost','cif_rmb', ]; /** * 迪拜两表合并导入(Stock in Jebel Ali / Under shipping) * * 缺失值规则: * - Under shipping 无 “CIF RMB”,尝试:cif_rmb = cif_jebel_ali_cost × shipping_fx * 若运费汇率缺失则用 fca_fx 兜底,都没有则置 NULL。 * * 返回:['sheets','total_rows','skipped','to_import','inserted','preview_5',('error'?)] */ public function dImport(string $file, bool $isUrl = false, bool $insertToDb = true): array { // 允许传对象键:当 isUrl=false 且非 http(s) 时,自动用 OssService 签名 if ($isUrl === false && !preg_match('#^https?://#i', $file)) { $file = $this->ossSignedUrl($file); $isUrl = true; } $sheets = ['Stock in Jebel Ali', 'Under shipping']; $parsed = []; // 逐 Sheet 读取 foreach ($sheets as $sheet) { $parsed[$sheet] = $this->readSheetRows($file, $sheet, $isUrl, headerRow: 1); } // 行处理与合并 $rowsAll = []; $skipped = 0; foreach ($sheets as $sheet) { foreach ($parsed[$sheet] as $row) { if ($this->shouldSkipByVinAliases($row, ['VIN NO','车架号'])) { $skipped++; continue; } // 映射 + 数值规范化 $rec = $this->mapAndNormalizeRow($row, $this->headerMapDubai, $this->decimalFieldsDubai); // Under shipping 无 CIF RMB:尝试用汇率推导 if (empty($rec['cif_rmb'])) { $cif = $rec['cif_jebel_ali_cost'] ?? null; $fx = $rec['shipping_fx'] ?? null; if ($cif !== null && $fx !== null) { $rec['cif_rmb'] = round($cif * $fx, 2); } elseif ($cif !== null && !empty($rec['fca_fx'])) { $rec['cif_rmb'] = round($cif * (float)$rec['fca_fx'], 2); } else { $rec['cif_rmb'] = null; } } // 统一 VIN 与来源 $rec['vin'] = $this->pickFirstNonEmpty($row, ['VIN NO','车架号']); $rec['source_sheet'] = $sheet; $this->stamp($rec); $rowsAll[] = $rec; } } // 结果与入库 $result = [ 'sheets' => $sheets, 'total_rows' => array_sum(array_map('count', $parsed)), 'skipped' => $skipped, 'to_import' => count($rowsAll), ]; if ($insertToDb) { // 注意表名:按你的业务这里写入 finance_recon_d_detail $ret = $this->batchInsert('finance_recon_d_detail', $rowsAll); $result['inserted'] = $ret['inserted']; if ($ret['error']) $result['error'] = 'Database insert error: ' . $ret['error']; } else { $result['inserted'] = 0; } $result['preview_5'] = array_slice($rowsAll, 0, 5); return $result; } /** 迪拜两表:从 OSS 对象键导入(语义化便捷入口) */ public function dImportFromOssKey(string $objectKey, bool $insertToDb = true): array { $signedUrl = $this->ossSignedUrl($objectKey); return $this->dImport($signedUrl, true, $insertToDb); } /** 迪拜两表:从已签名 URL 导入(语义化便捷入口) */ public function dImportFromSignedUrl(string $signedUrl, bool $insertToDb = true): array { return $this->dImport($signedUrl, true, $insertToDb); } }