CaiWuXiTong/app/Support/CourierStockQuoteEntryService.php

198 lines
10 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\Support;
use App\Models\Business\CourierOnboarding;
use App\Models\Business\CourierStockQuoteEntry;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\IOFactory;
final class CourierStockQuoteEntryService
{
public function __construct(
private readonly CourierInformationMirrorRepository $mirror,
private readonly CourierQuoteChangeService $changes,
) {
}
/** 编号是唯一身份;不按品牌或名称合并不同网点、结算主体。 */
public function courierOptions(): array
{
$options = [];
foreach ($this->mirror->overviewSource()['express'] ?? [] as $express) {
$code = trim((string) $express->expcode);
if ($code !== '' && filled($express->expname)) {
$options[$code] = ['code' => $code, 'name' => (string) $express->expname];
}
}
foreach (CourierOnboarding::query()->whereIn('status', [CourierOnboarding::STATUS_APPROVED, CourierOnboarding::STATUS_ACTIVATED])
->whereNotNull('workplatform_expcode')->get() as $courier) {
$code = trim((string) $courier->workplatform_expcode);
if ($code !== '' && filled($courier->courier_name)) {
$options[$code] ??= ['code' => $code, 'name' => $courier->courier_name];
}
}
uasort($options, static fn (array $a, array $b): int => [$a['name'], $a['code']] <=> [$b['name'], $b['code']]);
return array_values($options);
}
/** 保留公式和 Excel 缓存值,不执行上传文件中的公式或外部引用。 */
public function readWorkbook(UploadedFile $file, ?string $sheetName): array
{
$book = null;
try {
$reader = IOFactory::createReaderForFile($file->getRealPath());
$sheets = $reader->listWorksheetNames($file->getRealPath());
if (blank($sheetName) && count($sheets) !== 1) {
throw ValidationException::withMessages(['sheet_name' => '文件有多个工作表,请填写本次录入的准确工作表名称:' . implode('、', $sheets)]);
}
$sheetName = filled($sheetName) ? $sheetName : $sheets[0];
if (! in_array($sheetName, $sheets, true)) {
throw ValidationException::withMessages(['sheet_name' => '工作表不存在,可选:' . implode('、', $sheets)]);
}
foreach ($reader->listWorksheetInfo($file->getRealPath()) as $info) {
if ($info['worksheetName'] === $sheetName && ($info['totalRows'] > 2000 || $info['totalColumns'] > 100 || $info['totalRows'] * $info['totalColumns'] > 50000)) {
throw ValidationException::withMessages(['source_quote' => '单份报价最多 2000 行、100 列、50000 个单元格,请按快递及报价类型拆分。']);
}
}
$reader->setReadDataOnly(false);
$reader->setLoadSheetsOnly([$sheetName]);
$book = $reader->load($file->getRealPath());
$sheet = $book->getSheetByName($sheetName);
$lastRow = $sheet->getHighestDataRow();
$lastColumn = $sheet->getHighestDataColumn();
$columns = Coordinate::columnIndexFromString($lastColumn);
$values = $formulas = [];
$hasContent = false;
for ($row = 1; $row <= $lastRow; $row++) {
$valueRow = $formulaRow = [];
for ($column = 1; $column <= $columns; $column++) {
$cell = $sheet->getCell([$column, $row]);
$formula = $cell->getDataType() === DataType::TYPE_FORMULA ? $cell->getValue() : null;
$value = $formula !== null ? ($cell->getOldCalculatedValue() ?? $formula) : $cell->getValue();
$valueRow[] = is_object($value) ? (string) $value : $value;
$formulaRow[] = $formula;
$hasContent = $hasContent || ($value !== null && $value !== '');
}
$values[] = $valueRow;
$formulas[] = $formulaRow;
}
if (! $hasContent) {
throw ValidationException::withMessages(['source_quote' => '所选工作表为空,请上传实际报价。']);
}
return ['sheet_name' => $sheetName, 'source_range' => "A1:{$lastColumn}{$lastRow}", 'values' => $values, 'formulas' => $formulas];
} catch (ValidationException $exception) {
throw $exception;
} catch (\Throwable $exception) {
throw ValidationException::withMessages(['source_quote' => '无法读取报价 Excel,请确认文件未损坏、未加密,且为 xlsx 或 xls 格式。']);
} finally {
$book?->disconnectWorksheets();
}
}
public function create(array $data, int $userId): CourierStockQuoteEntry
{
$data['quote_type'] = trim($data['quote_type']);
$courier = collect($this->courierOptions())->firstWhere('code', $data['courier_expcode']);
if ($courier === null) {
throw ValidationException::withMessages(['courier_expcode' => '请选择已有快递资料中的唯一编号;没有编号时请先完善快递资料。']);
}
if (CourierStockQuoteEntry::withTrashed()->where('courier_expcode', $courier['code'])->where('quote_type', $data['quote_type'])->exists()) {
$this->duplicateError();
}
$matrix = $this->readWorkbook($data['source_quote'], $data['sheet_name'] ?? null);
$savedPaths = [];
try {
return DB::transaction(function () use ($data, $userId, $courier, $matrix, &$savedPaths): CourierStockQuoteEntry {
$entry = CourierStockQuoteEntry::create([
'company_id' => company_id(), 'courier_expcode' => $courier['code'], 'courier_name' => $courier['name'],
'quote_type' => $data['quote_type'], 'sheet_name' => $matrix['sheet_name'], 'source_range' => $matrix['source_range'],
'values_json' => json_encode($matrix['values'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'formulas_json' => json_encode($matrix['formulas'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'files' => [], 'created_by' => $userId,
]);
$files = $this->storeFiles($entry, $data, $savedPaths);
$entry->update(['files' => $files]);
$change = $this->changes->create([
'source_quote_id' => 'STOCK-' . $entry->id, 'change_type' => '存量报价',
'effective_date' => $data['effective_date'], 'summary' => $data['summary'], 'items' => [],
'quote_payload' => ['stock_quote_entry_id' => $entry->id, 'billing_rules' => $data['billing_rules']],
'proposed_values_json' => $entry->values_json,
], $userId);
$entry->update(['source_change_id' => $change->id]);
return $entry->load('change');
});
} catch (\Throwable $exception) {
if ($savedPaths !== []) {
Storage::disk('uploads')->delete($savedPaths);
}
if ($exception instanceof UniqueConstraintViolationException) {
$this->duplicateError();
}
throw $exception;
}
}
public function update(CourierStockQuoteEntry $entry, array $data, int $userId): void
{
$matrix = isset($data['source_quote']) ? $this->readWorkbook($data['source_quote'], $data['sheet_name'] ?? null) : null;
$savedPaths = [];
try {
DB::transaction(function () use ($entry, $data, $userId, $matrix, &$savedPaths): void {
$files = $entry->files;
if ($matrix !== null) {
foreach ($files as &$file) {
if ($file['kind'] === 'source_quote') {
$file['kind'] = 'previous_source_quote';
}
}
unset($file);
$entry->fill([
'sheet_name' => $matrix['sheet_name'], 'source_range' => $matrix['source_range'],
'values_json' => json_encode($matrix['values'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'formulas_json' => json_encode($matrix['formulas'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
]);
}
$entry->fill(['files' => [...$files, ...$this->storeFiles($entry, $data, $savedPaths)]])->save();
$entry->change->update(['source_sheet_name' => $entry->sheet_name, 'proposed_values_json' => $entry->values_json]);
$this->changes->update($entry->change, [...$data, 'change_type' => '存量报价', 'items' => []], $userId);
});
} catch (\Throwable $exception) {
Storage::disk('uploads')->delete($savedPaths);
throw $exception;
}
}
private function storeFiles(CourierStockQuoteEntry $entry, array $data, array &$savedPaths): array
{
$files = [];
foreach (['source_quote' => isset($data['source_quote']) ? [$data['source_quote']] : [], 'confirmation' => $data['confirmation_files'] ?? [], 'signed' => $data['signed_files'] ?? []] as $kind => $uploads) {
foreach ($uploads as $upload) {
$path = Storage::disk('uploads')->putFile('courier-stock-quotes/' . company_id() . "/{$entry->id}/{$kind}", $upload, 'private');
if ($path === false) {
throw ValidationException::withMessages(['source_quote' => '附件保存失败,请重试;本次录入未保存。']);
}
$savedPaths[] = $path;
$files[] = ['kind' => $kind, 'path' => $path, 'name' => $upload->getClientOriginalName(), 'sha256' => hash_file('sha256', $upload->getRealPath())];
}
}
return $files;
}
private function duplicateError(): never
{
throw ValidationException::withMessages(['courier_expcode' => '该快递编号的此类存量报价已录入,请到报价历史继续处理;后续调价请使用变价。']);
}
}