CaiWuXiTong/app/Support/CourierStockQuoteBatchService.php

182 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\CourierQuoteVersion;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
final class CourierStockQuoteBatchService
{
public function __construct(private readonly CourierStockQuoteEntryService $entries, private readonly CourierQuoteChangeService $changes)
{
}
public function ruleTemplates(): array
{
return CourierQuoteVersion::query()->whereNotNull('billing_rules')->latest('id')->limit(100)->get()
->filter(fn ($version) => filled($version->billing_rules))
->unique('billing_rules')->map(fn ($version) => [
'name' => $version->carrier_name . ' · ' . $version->quote_type,
'rules' => $version->billing_rules,
])->values()->all();
}
public function preview(UploadedFile $file, array $defaults, int $userId): string
{
$batch = (string) Str::uuid();
$disk = Storage::disk('uploads');
$savedPaths = [];
try {
$originalPath = $disk->putFileAs($this->directory($batch, $userId), $file, 'original.' . $file->getClientOriginalExtension(), 'private');
if ($originalPath === false) {
throw ValidationException::withMessages(['source_quote' => '报价原文件保存失败,请重试。']);
}
$savedPaths[] = $originalPath;
$sheets = $this->entries->readBatchWorkbook($file, $this->directory($batch, $userId) . '/images', $savedPaths);
$manifest = [
'company_id' => (int) company_id(), 'user_id' => $userId, 'created_at' => now()->toIso8601String(),
'defaults' => $defaults, 'sheets' => $sheets, 'result' => null,
'source_file' => ['kind' => 'source_quote', 'path' => $originalPath, 'name' => $file->getClientOriginalName(), 'sha256' => hash_file('sha256', $file->getRealPath())],
];
$savedPaths[] = $this->directory($batch, $userId) . '/manifest.json';
$this->write($batch, $userId, $manifest);
} catch (\Throwable $exception) {
$disk->delete($savedPaths);
throw $exception;
}
return $batch;
}
public function load(string $batch, int $userId): array
{
abort_unless(Str::isUuid($batch), 404);
$disk = Storage::disk('uploads');
$path = $this->directory($batch, $userId) . '/manifest.json';
abort_unless($disk->exists($path), 404, '导入预览不存在,请重新上传报价。');
$manifest = json_decode($disk->get($path), true, 512, JSON_THROW_ON_ERROR);
abort_unless($manifest['company_id'] === (int) company_id() && $manifest['user_id'] === $userId, 404);
return $manifest;
}
public function rows(array $manifest, array $couriers): array
{
$existing = $this->entries->existingIdentities();
return array_map(function (array $sheet) use ($manifest, $couriers, $existing): array {
$key = mb_strtolower(trim($sheet['sheet_name']));
$matches = array_values(array_filter($couriers, static fn ($courier) => in_array($key, [
mb_strtolower($courier['code']), mb_strtolower(trim($courier['name'])),
mb_strtolower($courier['name'] . ' · ' . $courier['code']),
], true)));
$courier = count($matches) === 1 ? $matches[0] : ['code' => '', 'name' => trim($sheet['sheet_name'])];
$code = $courier['code'] ?? '';
$duplicate = $this->entries->isDuplicate($courier, $manifest['defaults']['quote_type'], $existing, ($manifest['defaults']['effective_date'] ?? null) ?: null, ($manifest['defaults']['end_date'] ?? null) ?: null);
return [...$sheet, 'courier_expcode' => $code, 'courier_name' => $courier['name'], 'duplicate' => $duplicate,
'match_status' => $sheet['error'] ?: ($duplicate ? '已录入,默认跳过' : ($code !== '' ? '已匹配' : (count($matches) > 1 ? '名称对应多条资料,请确认发货名称;按名称保存' : '可手填名称,请与发货数据一致'))),
];
}, $manifest['sheets']);
}
public function save(string $batch, int $userId, array $data): array
{
$manifest = $this->load($batch, $userId);
if ($manifest['result'] !== null) {
return $manifest['result'];
}
$couriers = $this->entries->courierOptions();
$selected = array_filter($data['entries'], static fn ($row) => (bool) ($row['selected'] ?? false));
if ($selected === []) {
throw ValidationException::withMessages(['entries' => '请至少选择一家需要录入的快递。']);
}
$errors = $prepared = $identities = [];
$existing = $this->entries->existingIdentities();
foreach ($selected as $index => $row) {
$sheet = $manifest['sheets'][$index] ?? null;
$courier = null;
$courierError = null;
try {
$courier = $this->entries->resolveCourier($row, $couriers);
} catch (ValidationException $exception) {
$courierError = $exception->validator->errors()->first();
}
$rules = $data['rule_groups'][$row['rule_group'] ?? -1]['rules'] ?? '';
$periods = $row['periods'] ?? [[
'effective_date' => ($row['effective_date'] ?? '') ?: ($data['effective_date'] ?? ''),
'end_date' => ($row['end_date'] ?? null) ?: (($data['end_date'] ?? null) ?: null),
]];
foreach ($periods as $periodIndex => $period) {
$metadata = [
'quote_type' => trim($data['quote_type']), 'effective_date' => $period['effective_date'],
'end_date' => ($period['end_date'] ?? null) ?: null,
'billing_rules' => trim($rules), 'summary' => $data['summary'],
];
$validation = Validator::make($metadata, [
'effective_date' => ['required', 'date_format:Y-m-d'],
'end_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:effective_date'],
'billing_rules' => ['required', 'string', 'min:2', 'max:4000'],
], ['effective_date.required' => '请填写实际执行日期或设置共用日期。', 'billing_rules.required' => '请选择并填写计费规则。']);
$reason = $sheet === null ? '工作表不存在。' : ($sheet['error'] ?? null);
$reason ??= $courierError;
$reason ??= $validation->fails() ? $validation->errors()->first() : null;
foreach ($sheet['signed_images'] ?? [] as $image) {
if (! Storage::disk('uploads')->exists($image['path'])) {
$reason ??= '原件图片存档缺失,请重新上传原 Excel,不能带缺失附件录入。';
}
}
if ($courier !== null) {
$reason ??= $this->entries->isDuplicate($courier, $metadata['quote_type'], $existing, $metadata['effective_date'], $metadata['end_date']) ? '与已录入的同类报价执行期间重叠,请核对起止日期。' : null;
$reason ??= $this->entries->isDuplicate($courier, $metadata['quote_type'], $identities, $metadata['effective_date'], $metadata['end_date']) ? '与本批同一家快递的执行期间重叠,请核对起止日期。' : null;
$identities[] = [...$courier, 'quote_type' => $metadata['quote_type'], 'effective_date' => $metadata['effective_date'], 'end_date' => $metadata['end_date']];
}
if ($reason !== null) {
$errors['entries.' . $index . (isset($row['periods']) ? '.periods.' . $periodIndex : '')] = '「' . ($sheet['sheet_name'] ?? $index) . '」第 ' . (array_search($periodIndex, array_keys($periods), true) + 1) . ' 段:' . $reason;
} else {
$prepared[] = compact('metadata', 'courier', 'sheet');
}
}
}
if ($errors !== []) {
throw ValidationException::withMessages($errors);
}
abort_unless(Storage::disk('uploads')->exists($manifest['source_file']['path']), 404, '报价原文件不存在,请重新上传。');
// 一批同进同退;校验失败时保留上传原件和所有已选配置,便于修正重试。
return DB::transaction(function () use ($prepared, $selected, $manifest, $userId, $data, $batch): array {
$created = [];
foreach ($prepared as $item) {
$entry = $this->entries->createPrepared($item['metadata'], $userId, $item['courier'], $item['sheet'], [$manifest['source_file'], ...($item['sheet']['signed_images'] ?? [])]);
if ($data['submit_for_review'] ?? false) {
$this->changes->submit($entry->change, $userId);
}
$created[] = ['id' => $entry->id, 'change_id' => $entry->source_change_id, 'sheet_name' => $entry->sheet_name, 'courier_name' => $entry->courier_name, 'courier_expcode' => $entry->courier_expcode,
'effective_date' => $entry->effective_date->toDateString(), 'end_date' => $entry->end_date?->toDateString()];
}
$result = ['created' => $created, 'selected_sheets' => count($selected), 'skipped' => count($manifest['sheets']) - count($selected), 'submitted' => (bool) ($data['submit_for_review'] ?? false)];
$manifest['result'] = $result;
$this->write($batch, $userId, $manifest);
return $result;
});
}
private function directory(string $batch, int $userId): string
{
return 'courier-stock-quote-batches/' . company_id() . '/' . $userId . '/' . $batch;
}
private function write(string $batch, int $userId, array $manifest): void
{
if (! Storage::disk('uploads')->put($this->directory($batch, $userId) . '/manifest.json', json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'private')) {
throw ValidationException::withMessages(['source_quote' => '导入预览保存失败,请重试。']);
}
}
}