option('company'); if ($companyId <= 0 || ! Company::query()->where('id', $companyId)->exists()) { $this->error('请指定有效的 --company。'); return self::FAILURE; } $payload = json_decode((string) @file_get_contents((string) $this->argument('file')), true); $items = is_array($payload) ? ($payload['versions'] ?? null) : null; if (! is_array($items) || $items === []) { $this->error('文件不是有效的计费规则 JSON(缺少 versions)。'); return self::FAILURE; } $plan = []; $report = []; $seen = []; foreach ($items as $item) { if (! is_array($item)) { $report[] = ['', '', '错误', '版本条目必须是对象']; continue; } $code = (string) ($item['version_id'] ?? ''); if (isset($seen[$code])) { $report[] = [$code, '', '错误', '同一文件中的版本号不能重复']; continue; } $seen[$code] = true; [$status, $note, $data] = $this->prepare($companyId, $item); $report[] = [$code, (string) ($item['company_name'] ?? ''), $status, $note]; if ($data !== null) { $plan[] = $data; } } $this->table(['版本', '快递公司', '结果', '说明'], $report); $counts = array_count_values(array_column($report, 2)); $summary = collect($counts)->map(fn ($n, $k) => "{$k} {$n}")->implode(','); if (isset($counts['错误'])) { $this->error('存在错误,整批未写入:' . $summary); return self::FAILURE; } if ($this->option('dry-run')) { $this->info('校验通过,未写入(dry-run):' . $summary); return self::SUCCESS; } try { DB::transaction(function () use ($plan): void { foreach ($plan as [$version, $structure, $rows]) { $current = DB::table($version->getTable())->where('id', $version->id)->lockForUpdate()->first(); if (! $current || $current->company_name !== $version->company_name || json_decode($current->billing_structure ?? 'null', true) != $version->billing_structure || json_decode($current->billing_rows ?? 'null', true) != $version->billing_rows) { throw new RuntimeException('报价在校验后被其他操作修改,整批未写入,请重新校验。'); } $version->forceFill(['billing_structure' => $structure, 'billing_rows' => $rows])->save(); } }); } catch (RuntimeException $exception) { $this->error($exception->getMessage()); return self::FAILURE; } $this->info('导入完成:' . $summary); return self::SUCCESS; } /** * @param array $item * @return array{0:string,1:string,2:?array} */ private function prepare(int $companyId, array $item): array { $code = (string) ($item['version_id'] ?? ''); $version = CourierQuoteVersion::query()->allCompanies()->where('company_id', $companyId)->where('version_code', $code)->first(); if ($version === null) { return ['错误', '系统中找不到该版本号', null]; } if (trim((string) $version->company_name) !== trim((string) ($item['company_name'] ?? ''))) { return ['错误', '快递名称不一致:系统为「' . $version->company_name . '」', null]; } if (($item['conversion'] ?? '') === 'manual') { return ['跳过', '特殊计费(' . ($item['manual_reason'] ?? '按体积/条码') . '),需人工核算', null]; } if (($error = $this->inputError($item)) !== null) { return ['错误', $error, null]; } $source = (array) ($item['structure'] ?? []); $segments = ($item['conversion'] ?? '') === 'needs_segment_mode'; $mode = $segments ? CourierQuoteStructure::SEGMENTS : (string) ($source['mode'] ?? ''); if (! in_array($mode, self::MODES, true)) { return ['错误', '不支持的计费方式:' . $mode, null]; } $params = (array) ($item['params'] ?? []); $perTicket = 0.0; foreach ((array) ($params['surcharges'] ?? []) as $surcharge) { if (($surcharge['condition'] ?? '') === '每票' && is_numeric($surcharge['amount'] ?? null)) { $perTicket += (float) $surcharge['amount']; } } $structure = [ 'mode' => $mode, 'flat_bands' => $segments ? [] : $this->numbers($source['flat_bands'] ?? []), 'then_mode' => $segments ? '' : (string) ($source['then_mode'] ?? ''), 'first_weight' => ! $segments && is_numeric($source['first_weight'] ?? null) ? (float) $source['first_weight'] : null, 'rate_tiers' => $segments ? [] : $this->numbers($source['rate_tiers'] ?? []), 'rounding' => in_array($source['rounding'] ?? 'none', array_keys(CourierQuoteStructure::ROUNDING), true) ? (string) ($source['rounding'] ?? 'none') : 'none', 'volumetric_divisor' => is_numeric($params['volumetric_divisor'] ?? null) ? (int) $params['volumetric_divisor'] : null, 'weight_limit' => is_numeric($params['weight_limit_kg'] ?? null) ? (float) $params['weight_limit_kg'] : null, 'min_weight' => is_numeric($source['min_weight'] ?? $params['min_weight_kg'] ?? null) ? (float) ($source['min_weight'] ?? $params['min_weight_kg']) : null, 'per_ticket_surcharge' => $perTicket > 0 ? $perTicket : null, 'notes' => array_values(array_map('strval', (array) ($params['notes'] ?? []))), 'imported_from' => 'stock-quote-billing-import', ]; $rows = []; foreach ((array) ($segments ? ($item['segments_rows'] ?? []) : ($item['rows'] ?? [])) as $row) { $region = trim((string) ($row['region'] ?? '')); if ($region === '') { return ['错误', '存在没有地区名的行', null]; } if ($segments) { $list = []; foreach ((array) ($row['segments'] ?? []) as $s) { $list[] = array_filter([ 'lo' => (float) ($s['lo'] ?? 0), 'hi' => is_numeric($s['hi'] ?? null) ? (float) $s['hi'] : null, 'type' => (string) ($s['type'] ?? ''), 'price' => $this->number($s['price'] ?? null), 'base' => $this->number($s['base'] ?? null), 'rate' => $this->number($s['rate'] ?? null), 'fw' => $this->number($s['fw'] ?? null), ], static fn ($v, $k) => $v !== null || $k === 'hi', ARRAY_FILTER_USE_BOTH); } $rows[] = ['region' => $region, 'segments' => $list]; } else { $rows[] = [ 'region' => $region, 'flat' => array_map(fn ($v) => $this->number($v), array_values((array) ($row['flat'] ?? []))), 'base' => $this->number($row['base'] ?? null), 'rates' => array_map(fn ($v) => $this->number($v), array_values((array) ($row['rates'] ?? []))), ]; } } if ($rows === []) { return ['错误', '没有任何地区价格', null]; } $existing = $version->billing_structure; $note = CourierQuoteStructure::describe($structure) . ',' . count($rows) . ' 个地区'; if (is_array($existing) && $existing !== []) { if ($existing == $structure && $version->billing_rows == $rows) { return ['跳过', '已导入且内容相同', null]; } if (! $this->option('force')) { return ['错误', '已有不同的计费规则,如需覆盖请加 --force', null]; } return ['覆盖', $note, [$version, $structure, $rows]]; } return ['写入', $note, [$version, $structure, $rows]]; } private function number(mixed $value): ?float { return is_numeric($value) ? round((float) $value, 4) : null; } /** 金额导入拒绝负数、歧义分段和不支持的附加条件,不静默改写计费含义。 */ private function inputError(array $item): ?string { $structure = $item['structure'] ?? null; $params = $item['params'] ?? []; if (! is_array($structure) || ! is_array($params)) { return 'structure 和 params 必须是对象'; } if (! array_key_exists($structure['rounding'] ?? 'none', CourierQuoteStructure::ROUNDING)) { return '不支持的重量取整方式'; } $valid = static fn ($v): bool => is_numeric($v) && is_finite((float) $v) && (float) $v >= 0; foreach ([$params['volumetric_divisor'] ?? null, $params['weight_limit_kg'] ?? null, $structure['min_weight'] ?? $params['min_weight_kg'] ?? null, $structure['first_weight'] ?? null] as $v) { if ($v !== null && ! $valid($v)) return '重量、体积系数必须是非负有限数字'; } if (isset($params['volumetric_divisor']) && (float) $params['volumetric_divisor'] <= 0) return '体积系数必须大于0'; $minimum = (float) ($structure['min_weight'] ?? $params['min_weight_kg'] ?? 0); if (($params['weight_limit_kg'] ?? 0) > 0 && $minimum > $params['weight_limit_kg']) return '起算重量不能大于限重'; foreach (['flat_bands', 'rate_tiers'] as $field) { $previous = 0; if (! is_array($structure[$field] ?? [])) return '重量分段必须是数组'; foreach ($structure[$field] ?? [] as $edge) { if (! $valid($edge) || (float) $edge <= $previous) return '重量节点必须为严格递增的正数'; $previous = (float) $edge; } } $mode = ($item['conversion'] ?? '') === 'needs_segment_mode' ? CourierQuoteStructure::SEGMENTS : ($structure['mode'] ?? ''); if (in_array($mode, ['flat_bands', 'flat_then'], true) && empty($structure['flat_bands'])) return '缺少一口价重量节点'; if ($mode === 'flat_then' && ! in_array($structure['then_mode'] ?? '', ['waybill_rate', 'first_additional'], true)) return '不支持的超重计费方式'; if (CourierQuoteStructure::usesFirstWeight($mode, $structure['then_mode'] ?? '') && ! (($structure['first_weight'] ?? 0) > 0)) return '首重必须大于0'; if ($mode === 'flat_then' && ! empty($structure['rate_tiers']) && min($structure['rate_tiers']) <= max($structure['flat_bands'])) return '超重费率节点必须大于一口价最后节点'; if (! is_array($params['surcharges'] ?? [])) return '附加费必须是数组'; foreach ($params['surcharges'] ?? [] as $fee) { if (! is_array($fee) || ($fee['condition'] ?? '') !== '每票' || ! $valid($fee['amount'] ?? null)) return '附加费条件不支持自动核算,需人工核对'; } $rows = $item[$mode === CourierQuoteStructure::SEGMENTS ? 'segments_rows' : 'rows'] ?? null; if (! is_array($rows) || $rows === []) return '没有任何地区价格'; $regions = []; foreach ($rows as $row) { if (! is_array($row) || ! is_string($row['region'] ?? null) || trim($row['region']) === '') return '地区行格式不正确'; $region = mb_strtolower(preg_replace('/\s+/u', '', $row['region'])); if (isset($regions[$region])) return '存在重复地区行'; $regions[$region] = true; if ($mode === CourierQuoteStructure::SEGMENTS) { if (! is_array($row['segments'] ?? null) || $row['segments'] === []) return '缺少地区计费分段'; $previous = 0.0; foreach ($row['segments'] as $segment) { if (! is_array($segment) || ! $valid($segment['lo'] ?? null) || (float) $segment['lo'] < $previous || ! in_array($segment['type'] ?? '', ['flat', 'waybill', 'first_add'], true)) return '分段类型不支持或存在重叠'; $hi = $segment['hi'] ?? null; if ($hi !== null && (! $valid($hi) || (float) $hi <= (float) $segment['lo'])) return '分段上限必须大于下限'; $previous = $hi === null ? INF : (float) $hi; foreach (['price', 'base', 'rate', 'fw'] as $key) { if (isset($segment[$key]) && ! $valid($segment[$key])) return '分段金额和重量必须是非负有限数字'; } if ($segment['type'] === 'first_add' && ! $valid($segment['fw'] ?? null)) return '分段首重缺失'; } } else { if (! is_array($row['flat'] ?? []) || ! is_array($row['rates'] ?? [])) return '价格必须是数组'; if (count($row['flat'] ?? []) !== count($structure['flat_bands'] ?? []) || (CourierQuoteStructure::usesRates($mode) && count($row['rates'] ?? []) !== count($structure['rate_tiers'] ?? []) + 1)) return '价格列数与重量节点不匹配'; foreach (array_merge($row['flat'] ?? [], $row['rates'] ?? [], [$row['base'] ?? null]) as $v) { if ($v !== null && ! $valid($v)) return '价格必须是非负有限数字或null(不发)'; } } } return null; } /** @return list */ private function numbers(mixed $values): array { return array_values(array_map('floatval', array_filter((array) $values, 'is_numeric'))); } }