Files
fdesk/app/common/service/FinanceReconDubaiFilesService.php
T
mzl92 80fbf6c35a Merge branch 'develop'
# Conflicts:
#	config/route.php
2025-11-11 16:46:22 +08:00

290 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service;
use app\common\validate\FinanceReconDubaiFilesValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\FinanceReconDubaiFiles;
use app\common\utils\excel\ExcelImportTrait;
use support\Request;
/**
* FinanceReconDubaiFilesService
*
* - 提供两类导入:
* 1) M项目对账明细(sheet: M项目对账明细)
* 2) 迪拜两表合并(sheets: Stock in Jebel Ali / Under shipping)
*
* - 公共导入流程复用 ExcelImportTrait
* - OSS 读取通过你的 OssService->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);
}
}