173 lines
9.1 KiB
PHP
173 lines
9.1 KiB
PHP
<?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' => '导入预览保存失败,请重试。']);
|
||
}
|
||
}
|
||
}
|