退件费快照的表头跟另外两个不一样:单号列叫「交易订单」、金额列叫「结算金额」,所以 CourierPayableSummaryMap 的列定位改成按候选关键字找(先全等、再包含),三个汇总页共用一套逻辑。
140 lines
4.9 KiB
PHP
140 lines
4.9 KiB
PHP
<?php
|
||
|
||
namespace App\Business;
|
||
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Storage;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 快递应付的两个汇总页 → 「交易单号 ⇒ 金额类取值」映射。
|
||
*
|
||
* 这些数据都是财务在「应付款项 › 快递应付 › 运费差 / 其他附加费汇总 / 退件费」页面上传的汇总快照
|
||
* (JSON 文件,不是数据库表),快递运费页面按交易单号把对应金额带出来展示。
|
||
* 快照重新导入后文件时间会变,缓存键跟着变,自动失效。
|
||
*/
|
||
class CourierPayableSummaryMap
|
||
{
|
||
/** 运费差:差异(结算运费-提供成本运费)。 */
|
||
public const PAGE_FREIGHT_DIFFERENCE = 'freight-difference';
|
||
|
||
/** 其他附加费汇总:金额。 */
|
||
public const PAGE_OTHER_SURCHARGE = 'other-surcharge-summary';
|
||
|
||
/** 退件费:结算金额(单号列叫「交易订单」)。 */
|
||
public const PAGE_RETURN_FEE = 'return-fee';
|
||
|
||
/**
|
||
* 汇总页 → [单号列的关键字候选, 取值列的关键字候选]。
|
||
*
|
||
* 各页表头写法不统一(「交易单号」/「交易订单」、「金额」/「结算金额」),
|
||
* 所以按候选顺序找:先全等匹配,再退化成包含匹配。
|
||
*/
|
||
private const PAGES = [
|
||
self::PAGE_FREIGHT_DIFFERENCE => [['交易单号', '交易订单'], ['差异', '金额']],
|
||
self::PAGE_OTHER_SURCHARGE => [['交易单号', '交易订单'], ['金额']],
|
||
self::PAGE_RETURN_FEE => [['交易订单', '交易单号'], ['结算金额', '金额']],
|
||
];
|
||
|
||
private const CACHE_MINUTES = 10;
|
||
|
||
public function __construct(private readonly CourierPayableSummaryStore $store) {}
|
||
|
||
/**
|
||
* @param string $page self::PAGE_* 之一
|
||
* @return array<string,string> 交易单号 => 金额(保留源文件里的原值字符串)
|
||
*/
|
||
public function map(int $companyId, string $page): array
|
||
{
|
||
if (! isset(self::PAGES[$page])) {
|
||
return [];
|
||
}
|
||
|
||
$disk = Storage::disk('uploads');
|
||
$path = $this->store->path($companyId, $page);
|
||
if (! $disk->exists($path)) {
|
||
return [];
|
||
}
|
||
|
||
// 文件时间戳进缓存键:财务重新导入运费差后不用等缓存过期。
|
||
$key = sprintf('courier-payable-summary-map:v1:%s:%d:%d', $page, $companyId, (int) $disk->lastModified($path));
|
||
|
||
return Cache::remember($key, now()->addMinutes(self::CACHE_MINUTES), function () use ($companyId, $page): array {
|
||
try {
|
||
return $this->build($companyId, $page);
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return [];
|
||
}
|
||
});
|
||
}
|
||
|
||
/** @return array<string,string> */
|
||
private function build(int $companyId, string $page): array
|
||
{
|
||
$snapshot = $this->store->snapshot($companyId, $page);
|
||
if ($snapshot === null) {
|
||
return [];
|
||
}
|
||
|
||
[$orderHeaders, $valueHeaders] = self::PAGES[$page];
|
||
$headers = array_map(static fn ($value): string => trim((string) $value), (array) $snapshot['headers']);
|
||
$orderColumn = $this->columnIndex($headers, $orderHeaders);
|
||
$valueColumn = $this->columnIndex($headers, $valueHeaders);
|
||
if ($orderColumn === null || $valueColumn === null) {
|
||
return [];
|
||
}
|
||
|
||
$map = [];
|
||
foreach ((array) $snapshot['rows'] as $row) {
|
||
$orderNos = trim((string) ($row[$orderColumn] ?? ''));
|
||
if ($orderNos === '') {
|
||
continue;
|
||
}
|
||
|
||
$value = trim((string) ($row[$valueColumn] ?? ''));
|
||
if ($value === '') {
|
||
continue;
|
||
}
|
||
|
||
// 源表一个格子可能放多个交易单号(逗号分隔),逐个挂到同一个金额上。
|
||
foreach (explode(',', $orderNos) as $orderNo) {
|
||
$orderNo = trim($orderNo);
|
||
if ($orderNo !== '' && ! isset($map[$orderNo])) {
|
||
$map[$orderNo] = $value;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
/**
|
||
* 按表头名定位:候选顺序优先,每个候选先找完全相等的表头,再退化成包含匹配
|
||
* (例如「差异(结算运费-提供成本运费)」只能靠包含匹配)。
|
||
*
|
||
* @param list<string> $needles
|
||
*/
|
||
private function columnIndex(array $headers, array $needles): ?int
|
||
{
|
||
foreach ($needles as $needle) {
|
||
foreach ($headers as $index => $header) {
|
||
if ($header === $needle) {
|
||
return (int) $index;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($needles as $needle) {
|
||
foreach ($headers as $index => $header) {
|
||
if ($header !== '' && mb_strpos($header, $needle) !== false) {
|
||
return (int) $index;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|