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