80fbf6c35a
# Conflicts: # config/route.php
290 lines
12 KiB
PHP
290 lines
12 KiB
PHP
<?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);
|
||
}
|
||
} |