CaiWuXiTong/app/Support/CourierStockQuoteBatchService.php

173 lines
9.1 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);
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'] ?? '';
$metadata = [
'quote_type' => trim($data['quote_type']), 'effective_date' => ($row['effective_date'] ?? '') ?: ($data['effective_date'] ?? ''),
'billing_rules' => trim($rules), 'summary' => $data['summary'],
];
$validation = Validator::make($metadata, [
'effective_date' => ['required', 'date_format:Y-m-d'],
'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) ? '此快递的同类报价已录入,请取消选择;后续调价使用变价。' : null;
$reason ??= $this->entries->isDuplicate($courier, $metadata['quote_type'], $identities) ? '与本批其他工作表填写了同一家快递,请检查名称或取消选择。' : null;
$identities[] = [...$courier, 'quote_type' => $metadata['quote_type']];
}
if ($reason !== null) {
$errors['entries.' . $index] = '「' . ($sheet['sheet_name'] ?? $index) . '」' . $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, $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];
}
$result = ['created' => $created, 'skipped' => count($manifest['sheets']) - count($created), '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' => '导入预览保存失败,请重试。']);
}
}
}