713 lines
24 KiB
PHP
713 lines
24 KiB
PHP
<?php
|
||
|
||
namespace App\Support;
|
||
|
||
use JsonException;
|
||
|
||
/**
|
||
* 逆向物流报价快照:从本机私有 JSON(由逆向物流报价.xlsx 生成)读取报价版本,
|
||
* 并把三种原表版式(矩阵、单一到达省、重量段)拆分为可用于运费计算的运价明细。
|
||
*
|
||
* 与 QuoteManagementSnapshot 同构:只读展示,不重新计算金额。
|
||
*/
|
||
final class ReverseLogisticsQuoteSnapshot
|
||
{
|
||
public const RELATIVE_PATH = 'app/private/quote-management/reverse-logistics-quotes.json';
|
||
|
||
/** 出发省份 × 到达省份矩阵,单元格为面单价格 + 续重价格(如申通退货)。 */
|
||
private const LAYOUT_MATRIX = 'matrix';
|
||
|
||
/** 单一到达省份,每行一个出发省份(如常州德邦逆向)。 */
|
||
private const LAYOUT_SINGLE_DESTINATION = 'single-destination';
|
||
|
||
/** 单一到达仓,每行一个出发省份,按重量段给出费率(如中通快运逆向)。 */
|
||
private const LAYOUT_WEIGHT_BAND = 'weight-band';
|
||
|
||
public function __construct(private readonly ?string $path = null)
|
||
{
|
||
}
|
||
|
||
public function load(): array
|
||
{
|
||
$path = $this->path ?? storage_path(self::RELATIVE_PATH);
|
||
|
||
if (! is_file($path)) {
|
||
return $this->emptySnapshot('missing', '尚未导入本机逆向物流报价文件。');
|
||
}
|
||
|
||
try {
|
||
$payload = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
|
||
} catch (JsonException) {
|
||
return $this->emptySnapshot('invalid', '本机逆向物流报价快照无法读取,请重新导入源文件。');
|
||
}
|
||
|
||
if (! is_array($payload) || ! is_array($payload['sheets'] ?? null)) {
|
||
return $this->emptySnapshot('invalid', '本机逆向物流报价快照结构不完整,请重新导入源文件。');
|
||
}
|
||
|
||
$quotes = [];
|
||
|
||
foreach (array_values($payload['sheets']) as $position => $sheet) {
|
||
if (! is_array($sheet) || trim((string) ($sheet['name'] ?? '')) === '') {
|
||
continue;
|
||
}
|
||
|
||
$quotes[] = $this->normalizeSheet($sheet, $position);
|
||
}
|
||
|
||
$rates = [];
|
||
$rateGroups = [];
|
||
$unparsedSheets = [];
|
||
$originKeys = [];
|
||
$destinationKeys = [];
|
||
|
||
foreach ($quotes as $quote) {
|
||
$extracted = $this->extractRates($quote);
|
||
|
||
if ($extracted === null) {
|
||
$unparsedSheets[] = $quote['sheet_name'];
|
||
|
||
continue;
|
||
}
|
||
|
||
$rateGroups[] = $extracted['rule'];
|
||
|
||
foreach ($extracted['rates'] as $rate) {
|
||
$rates[] = $rate;
|
||
|
||
if (! in_array($rate['origin_key'], $originKeys, true)) {
|
||
$originKeys[] = $rate['origin_key'];
|
||
}
|
||
|
||
if (! in_array($rate['destination_key'], $destinationKeys, true)) {
|
||
$destinationKeys[] = $rate['destination_key'];
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
'available' => true,
|
||
'state' => 'ready',
|
||
'message' => null,
|
||
'source_file' => basename(str_replace('\\', '/', (string) ($payload['sourcePath'] ?? '逆向物流报价.xlsx'))),
|
||
'generated_at' => (string) ($payload['generatedAt'] ?? ''),
|
||
'quotes' => $quotes,
|
||
'rates' => $rates,
|
||
'rate_groups' => $rateGroups,
|
||
'unparsed_sheets' => $unparsedSheets,
|
||
'provinces' => [
|
||
'origins' => $originKeys,
|
||
'destinations' => $destinationKeys,
|
||
],
|
||
'summary' => [
|
||
'quote_count' => count($quotes),
|
||
'carrier_count' => count(array_unique(array_column($quotes, 'carrier'))),
|
||
'dated_count' => count(array_filter($quotes, fn (array $quote) => $quote['effective_date'] !== null)),
|
||
'formula_count' => array_sum(array_column($quotes, 'formula_count')),
|
||
],
|
||
'rate_summary' => [
|
||
'rate_count' => count($rates),
|
||
'carrier_count' => count($rateGroups),
|
||
'origin_count' => count(array_unique(array_column($rates, 'origin'))),
|
||
'destination_count' => count(array_unique(array_column($rates, 'destination'))),
|
||
'weight_band_count' => count(array_unique(array_filter(
|
||
array_column($rates, 'weight_band'),
|
||
static fn ($band) => $band !== null && $band !== ''
|
||
))),
|
||
'available_count' => count(array_filter($rates, static fn (array $rate) => $rate['available'])),
|
||
'missing_count' => count(array_filter($rates, static fn (array $rate) => ! $rate['available'])),
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 把一个报价工作表拆分为运价明细;无法识别版式时返回 null。
|
||
*
|
||
* @return array{rule:array<string,mixed>,rates:list<array<string,mixed>>}|null
|
||
*/
|
||
private function extractRates(array $quote): ?array
|
||
{
|
||
$values = $quote['values'];
|
||
$layout = $this->detectLayout($values);
|
||
|
||
if ($layout === null) {
|
||
return null;
|
||
}
|
||
|
||
$notes = $this->noteLines($values, $layout);
|
||
$context = [
|
||
'carrier' => $quote['carrier'],
|
||
'sheet_name' => $quote['sheet_name'],
|
||
'quote_type' => $quote['quote_type'],
|
||
'effective_date' => $quote['effective_date'],
|
||
'layout' => $layout,
|
||
'notes' => $notes,
|
||
];
|
||
|
||
return match ($layout) {
|
||
self::LAYOUT_MATRIX => $this->extractMatrixRates($values, $context),
|
||
self::LAYOUT_SINGLE_DESTINATION => $this->extractSingleDestinationRates($values, $context),
|
||
self::LAYOUT_WEIGHT_BAND => $this->extractWeightBandRates($values, $context),
|
||
default => null,
|
||
};
|
||
}
|
||
|
||
/** @param list<array<int,mixed>> $values */
|
||
private function detectLayout(array $values): ?string
|
||
{
|
||
$header = $values[1] ?? [];
|
||
|
||
foreach ($header as $index => $label) {
|
||
if (! is_string($label)) {
|
||
continue;
|
||
}
|
||
|
||
if ($index >= 3 && preg_match('/^\d+\s*-\s*\d+\s*kg$/u', trim($label)) === 1) {
|
||
return self::LAYOUT_WEIGHT_BAND;
|
||
}
|
||
}
|
||
|
||
if (isset($header[0], $header[1])
|
||
&& trim((string) $header[0]) === '出发省份'
|
||
&& trim((string) $header[1]) === '到达省份') {
|
||
return self::LAYOUT_MATRIX;
|
||
}
|
||
|
||
if (isset($header[1]) && is_string($header[1]) && str_starts_with(trim($header[1]), '到达省份')) {
|
||
return self::LAYOUT_SINGLE_DESTINATION;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 矩阵版式:列方向按“到达省份”成组,每组两列(面单价格、续重价格),行方向为出发省份。
|
||
*
|
||
* @param list<array<int,mixed>> $values
|
||
* @param array<string,mixed> $context
|
||
* @return array{rule:array<string,mixed>,rates:list<array<string,mixed>>}
|
||
*/
|
||
private function extractMatrixRates(array $values, array $context): array
|
||
{
|
||
$destinations = [];
|
||
$subHeader = $values[2] ?? [];
|
||
|
||
foreach ($subHeader as $index => $label) {
|
||
if (! is_string($label) || trim($label) !== '面单价格') {
|
||
continue;
|
||
}
|
||
|
||
$name = trim((string) ($values[1][$index] ?? ''));
|
||
|
||
if ($name === '' || $name === '到达省份') {
|
||
continue;
|
||
}
|
||
|
||
$destinations[$index] = ['name' => $name, 'increment' => $index + 1];
|
||
}
|
||
|
||
$rates = [];
|
||
$position = 0;
|
||
|
||
foreach ($values as $rowIndex => $row) {
|
||
if ($rowIndex < 3) {
|
||
continue;
|
||
}
|
||
|
||
$origin = trim((string) ($row[1] ?? ''));
|
||
|
||
if ($origin === '') {
|
||
continue;
|
||
}
|
||
|
||
foreach ($destinations as $baseIndex => $destination) {
|
||
$position++;
|
||
$baseAmount = $this->amount($row[$baseIndex] ?? null);
|
||
$incrementAmount = $this->amount($row[$destination['increment']] ?? null);
|
||
|
||
$rates[] = $this->rateRow($context, $position, [
|
||
'origin' => $origin,
|
||
'destination' => $destination['name'],
|
||
'weight_band' => null,
|
||
'base_amount' => $baseAmount,
|
||
'increment_amount' => $incrementAmount,
|
||
'source' => $this->cellRef($baseIndex, $rowIndex) . ':' . $this->cellRef($destination['increment'], $rowIndex),
|
||
]);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'rule' => $this->rateRule($context, [
|
||
'pricing_mode' => 'first_weight_increment',
|
||
'pricing_label' => '首重 + 续重 × (结算重量 - 1)',
|
||
'base_label' => '面单价格(首重 1kg)',
|
||
'increment_label' => '续重价格',
|
||
'route_label' => sprintf('行=出发省份,列=到达省份(%d 个到达省)', count($destinations)),
|
||
'weight_bands' => [],
|
||
'rates' => $rates,
|
||
]),
|
||
'rates' => $rates,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 单一到达省版式:A 列出发省份,随后两列为面单价格、续重价格。
|
||
*
|
||
* @param list<array<int,mixed>> $values
|
||
* @param array<string,mixed> $context
|
||
* @return array{rule:array<string,mixed>,rates:list<array<string,mixed>>}
|
||
*/
|
||
private function extractSingleDestinationRates(array $values, array $context): array
|
||
{
|
||
$destination = trim((string) ($values[1][1] ?? ''));
|
||
$destination = trim((string) preg_replace('/^到达省份/u', '', $destination));
|
||
|
||
if ($destination === '') {
|
||
$destination = '江苏';
|
||
}
|
||
|
||
$rates = [];
|
||
$position = 0;
|
||
|
||
foreach ($values as $rowIndex => $row) {
|
||
if ($rowIndex < 3) {
|
||
continue;
|
||
}
|
||
|
||
$origin = trim((string) ($row[0] ?? ''));
|
||
|
||
if ($origin === '') {
|
||
continue;
|
||
}
|
||
|
||
$baseAmount = $this->amount($row[1] ?? null);
|
||
$incrementAmount = $this->amount($row[2] ?? null);
|
||
|
||
if ($baseAmount === null && $incrementAmount === null) {
|
||
continue;
|
||
}
|
||
|
||
$position++;
|
||
$rates[] = $this->rateRow($context, $position, [
|
||
'origin' => $origin,
|
||
'destination' => $destination,
|
||
'weight_band' => null,
|
||
'base_amount' => $baseAmount,
|
||
'increment_amount' => $incrementAmount,
|
||
'source' => $this->cellRef(1, $rowIndex) . ':' . $this->cellRef(2, $rowIndex),
|
||
]);
|
||
}
|
||
|
||
return [
|
||
'rule' => $this->rateRule($context, [
|
||
'pricing_mode' => 'base_plus_weight_rate',
|
||
'pricing_label' => '面单 + 重量 × 费率',
|
||
'base_label' => '面单价格',
|
||
'increment_label' => '续重价格',
|
||
'route_label' => '行=出发省份,到达固定为 ' . $destination,
|
||
'weight_bands' => [],
|
||
'rates' => $rates,
|
||
]),
|
||
'rates' => $rates,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 重量段版式:A 列出发省份、B 列面单费,其后每列一个重量段费率。
|
||
*
|
||
* @param list<array<int,mixed>> $values
|
||
* @param array<string,mixed> $context
|
||
* @return array{rule:array<string,mixed>,rates:list<array<string,mixed>>}
|
||
*/
|
||
private function extractWeightBandRates(array $values, array $context): array
|
||
{
|
||
$bands = [];
|
||
$header = $values[1] ?? [];
|
||
|
||
foreach ($header as $index => $label) {
|
||
if ($index < 3 || ! is_string($label)) {
|
||
continue;
|
||
}
|
||
|
||
$label = trim($label);
|
||
|
||
if ($label === '' || $label === '备注') {
|
||
continue;
|
||
}
|
||
|
||
$bands[$index] = $label;
|
||
}
|
||
|
||
$destination = $this->weightBandDestination($context['notes']);
|
||
|
||
$rates = [];
|
||
$position = 0;
|
||
|
||
foreach ($values as $rowIndex => $row) {
|
||
if ($rowIndex < 2) {
|
||
continue;
|
||
}
|
||
|
||
$origin = trim((string) ($row[0] ?? ''));
|
||
|
||
if ($origin === '') {
|
||
continue;
|
||
}
|
||
|
||
$baseAmount = $this->amount($row[1] ?? null);
|
||
|
||
foreach ($bands as $index => $band) {
|
||
$position++;
|
||
$rates[] = $this->rateRow($context, $position, [
|
||
'origin' => $origin,
|
||
'destination' => $destination,
|
||
'weight_band' => $band,
|
||
'base_amount' => $baseAmount,
|
||
'increment_amount' => $this->amount($row[$index] ?? null),
|
||
'source' => $this->cellRef($index, $rowIndex),
|
||
]);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'rule' => $this->rateRule($context, [
|
||
'pricing_mode' => 'base_plus_weight_rate',
|
||
'pricing_label' => '面单 + 重量段费率 × 结算重量(不足 15kg 按 15kg 计费,向上取整)',
|
||
'base_label' => '面单费',
|
||
'increment_label' => '重量段费率',
|
||
'route_label' => '行=出发省份,到达固定为 ' . $destination,
|
||
'weight_bands' => array_values($bands),
|
||
'rates' => $rates,
|
||
]),
|
||
'rates' => $rates,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $context
|
||
* @param array<string,mixed> $values
|
||
* @return array<string,mixed>
|
||
*/
|
||
private function rateRow(array $context, int $position, array $values): array
|
||
{
|
||
return [
|
||
'id' => sprintf('%s-%03d', $context['sheet_name'], $position),
|
||
'carrier' => $context['carrier'],
|
||
'sheet_name' => $context['sheet_name'],
|
||
'quote_type' => $context['quote_type'],
|
||
'origin' => $values['origin'],
|
||
'destination' => $values['destination'],
|
||
'origin_key' => self::provinceKey($values['origin']),
|
||
'destination_key' => self::provinceKey($values['destination']),
|
||
'weight_band' => $values['weight_band'],
|
||
'base_amount' => $values['base_amount'],
|
||
'increment_amount' => $values['increment_amount'],
|
||
'available' => $values['base_amount'] !== null || $values['increment_amount'] !== null,
|
||
'source' => $values['source'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param array<string,mixed> $context
|
||
* @param array<string,mixed> $values
|
||
* @return array<string,mixed>
|
||
*/
|
||
private function rateRule(array $context, array $values): array
|
||
{
|
||
$rates = $values['rates'];
|
||
|
||
return [
|
||
'carrier' => $context['carrier'],
|
||
'sheet_name' => $context['sheet_name'],
|
||
'quote_type' => $context['quote_type'],
|
||
'layout' => $context['layout'],
|
||
'pricing_mode' => $values['pricing_mode'],
|
||
'pricing_label' => $values['pricing_label'],
|
||
'base_label' => $values['base_label'],
|
||
'increment_label' => $values['increment_label'],
|
||
'route_label' => $values['route_label'],
|
||
'base_unit' => '元/票',
|
||
'increment_unit' => '元/kg',
|
||
'weight_bands' => $values['weight_bands'],
|
||
'effective_date' => $context['effective_date'],
|
||
'constraints' => $this->constraintLines($context['notes']),
|
||
'notes' => $context['notes'],
|
||
'rate_count' => count($rates),
|
||
'available_count' => count(array_filter($rates, static fn (array $rate) => $rate['available'])),
|
||
'origin_count' => count(array_unique(array_column($rates, 'origin'))),
|
||
'destination_count' => count(array_unique(array_column($rates, 'destination'))),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 备注列:矩阵版式取 A 列尾部说明行,其余版式取最后一列说明。
|
||
*
|
||
* @param list<array<int,mixed>> $values
|
||
* @return list<string>
|
||
*/
|
||
private function noteLines(array $values, string $layout): array
|
||
{
|
||
[$startRow, $column] = match ($layout) {
|
||
self::LAYOUT_MATRIX => [3, 0],
|
||
self::LAYOUT_SINGLE_DESTINATION => [3, 3],
|
||
self::LAYOUT_WEIGHT_BAND => [2, 5],
|
||
default => [null, null],
|
||
};
|
||
|
||
if ($column === null) {
|
||
return [];
|
||
}
|
||
|
||
$lines = [];
|
||
|
||
foreach ($values as $rowIndex => $row) {
|
||
if ($rowIndex < $startRow) {
|
||
continue;
|
||
}
|
||
|
||
$cell = $row[$column] ?? null;
|
||
|
||
if (! is_string($cell) || trim($cell) === '') {
|
||
continue;
|
||
}
|
||
|
||
foreach (preg_split('/\r\n|\r|\n/u', $cell) ?: [] as $line) {
|
||
$line = trim($line);
|
||
|
||
if ($line !== '') {
|
||
$lines[] = $line;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $lines;
|
||
}
|
||
|
||
/** @param list<string> $notes @return list<string> */
|
||
private function constraintLines(array $notes): array
|
||
{
|
||
$keywords = ['限重', '限长', '超标', '超长超重', '保丢', '计泡', '记泡', '抛比'];
|
||
|
||
return array_values(array_filter(
|
||
$notes,
|
||
static function (string $line) use ($keywords): bool {
|
||
foreach ($keywords as $keyword) {
|
||
if (str_contains($line, $keyword)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
));
|
||
}
|
||
|
||
/** @param list<string> $notes */
|
||
private function weightBandDestination(array $notes): string
|
||
{
|
||
foreach ($notes as $line) {
|
||
if (str_contains($line, '常州仓')) {
|
||
return '常州仓(江苏)';
|
||
}
|
||
}
|
||
|
||
return '江苏';
|
||
}
|
||
|
||
private function amount(mixed $value): ?float
|
||
{
|
||
if (is_int($value) || is_float($value)) {
|
||
return round((float) $value, 4);
|
||
}
|
||
|
||
if (! is_string($value)) {
|
||
return null;
|
||
}
|
||
|
||
$value = trim($value);
|
||
|
||
if ($value === '' || in_array($value, ['/', '-', '—', '无'], true)) {
|
||
return null;
|
||
}
|
||
|
||
return is_numeric($value) ? round((float) $value, 4) : null;
|
||
}
|
||
|
||
private function cellRef(int $columnIndex, int $rowIndex): string
|
||
{
|
||
$label = '';
|
||
$value = $columnIndex + 1;
|
||
|
||
while ($value > 0) {
|
||
$value--;
|
||
$label = chr(65 + ($value % 26)) . $label;
|
||
$value = intdiv($value, 26);
|
||
}
|
||
|
||
return $label . ($rowIndex + 1);
|
||
}
|
||
|
||
/** 省份归一口径:去掉“省/市/自治区”后缀,仓名取括号内省份(如“常州仓(江苏)”)。 */
|
||
public static function provinceKey(string $name): string
|
||
{
|
||
$text = trim($name);
|
||
|
||
if (preg_match('/^[^((]*[((]([^))]+)[))]$/u', $text, $matches) === 1) {
|
||
$text = trim($matches[1]);
|
||
}
|
||
|
||
$text = (string) preg_replace('/(维吾尔自治区|壮族自治区|回族自治区|自治区|特别行政区|省|市)$/u', '', $text);
|
||
|
||
return $text === '内蒙' ? '内蒙古' : $text;
|
||
}
|
||
private function normalizeSheet(array $sheet, int $position): array
|
||
{
|
||
$name = trim((string) $sheet['name']);
|
||
$values = $this->normalizeMatrix($sheet['values'] ?? []);
|
||
$formulas = $this->normalizeMatrix($sheet['formulas'] ?? []);
|
||
$columnCount = max(array_map('count', $values ?: [[]]));
|
||
|
||
return [
|
||
'id' => sprintf('RL-%03d', $position + 1),
|
||
'sheet_index' => (int) ($sheet['index'] ?? $position),
|
||
'sheet_name' => $name,
|
||
'company_name' => $this->inferCompanyName($name),
|
||
'carrier' => $this->inferCarrier($name),
|
||
'quote_type' => $this->inferQuoteType($name),
|
||
'effective_date' => $this->extractEffectiveDate($values),
|
||
'source_range' => trim((string) ($sheet['address'] ?? '')),
|
||
'row_count' => count($values),
|
||
'column_count' => $columnCount,
|
||
'formula_count' => $this->formulaCount($formulas),
|
||
'status' => $this->extractEffectiveDate($values) === null ? '未标执行日期' : '已标执行日期',
|
||
'signature_status' => 'Excel 未提供',
|
||
'values' => $values,
|
||
'formulas' => $formulas,
|
||
];
|
||
}
|
||
|
||
private function inferCompanyName(string $name): string
|
||
{
|
||
$companyName = trim((string) preg_replace('/(?:逆向|退货|报价表|报价)$/u', '', $name));
|
||
|
||
return $companyName !== '' ? $companyName : $name;
|
||
}
|
||
|
||
private function inferCarrier(string $name): string
|
||
{
|
||
foreach ([
|
||
'中通快运', '顺心捷达', '安能快运', '京广', 'EMS', '中通', '圆通', '德邦',
|
||
'申通', '极兔', '安能', '邮政', '韵达', '顺丰',
|
||
] as $carrier) {
|
||
if (str_contains($name, $carrier)) {
|
||
return $carrier;
|
||
}
|
||
}
|
||
|
||
return '其他快递';
|
||
}
|
||
|
||
private function inferQuoteType(string $name): string
|
||
{
|
||
if (str_contains($name, '退货')) {
|
||
return '退货报价';
|
||
}
|
||
|
||
if (str_contains($name, '快运')) {
|
||
return '逆向快运报价';
|
||
}
|
||
|
||
return '逆向报价';
|
||
}
|
||
|
||
private function extractEffectiveDate(array $values): ?string
|
||
{
|
||
foreach ($values as $row) {
|
||
foreach ($row as $cell) {
|
||
if (! is_string($cell)) {
|
||
continue;
|
||
}
|
||
|
||
if (preg_match('/(20\d{2})[.\/\-年]\s*(\d{1,2})[.\/\-月]\s*(\d{1,2})日?/u', $cell, $match) === 1) {
|
||
return sprintf('%04d-%02d-%02d', (int) $match[1], (int) $match[2], (int) $match[3]);
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private function normalizeMatrix(mixed $matrix): array
|
||
{
|
||
if (! is_array($matrix)) {
|
||
return [];
|
||
}
|
||
|
||
return array_map(function ($row) {
|
||
if (! is_array($row)) {
|
||
return [$this->normalizeCell($row)];
|
||
}
|
||
|
||
return array_map(fn ($cell) => $this->normalizeCell($cell), array_values($row));
|
||
}, array_values($matrix));
|
||
}
|
||
|
||
private function normalizeCell(mixed $cell): string|int|float|bool|null
|
||
{
|
||
if ($cell === null || is_string($cell) || is_int($cell) || is_float($cell) || is_bool($cell)) {
|
||
return $cell;
|
||
}
|
||
|
||
return (string) $cell;
|
||
}
|
||
|
||
private function formulaCount(array $formulas): int
|
||
{
|
||
$count = 0;
|
||
|
||
foreach ($formulas as $row) {
|
||
foreach ($row as $formula) {
|
||
if (is_string($formula) && trim($formula) !== '') {
|
||
$count++;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $count;
|
||
}
|
||
|
||
private function emptySnapshot(string $state, string $message): array
|
||
{
|
||
return [
|
||
'available' => false,
|
||
'state' => $state,
|
||
'message' => $message,
|
||
'source_file' => '逆向物流报价.xlsx',
|
||
'generated_at' => '',
|
||
'quotes' => [],
|
||
'rates' => [],
|
||
'rate_groups' => [],
|
||
'unparsed_sheets' => [],
|
||
'provinces' => [
|
||
'origins' => [],
|
||
'destinations' => [],
|
||
],
|
||
'summary' => [
|
||
'quote_count' => 0,
|
||
'carrier_count' => 0,
|
||
'dated_count' => 0,
|
||
'formula_count' => 0,
|
||
],
|
||
'rate_summary' => [
|
||
'rate_count' => 0,
|
||
'carrier_count' => 0,
|
||
'origin_count' => 0,
|
||
'destination_count' => 0,
|
||
'weight_band_count' => 0,
|
||
'available_count' => 0,
|
||
'missing_count' => 0,
|
||
],
|
||
];
|
||
}
|
||
}
|