ensureWarehouseTable(); $this->ensureWarehouseRuleColumns(); $this->ensureOrderTargetTable(); $this->bootstrapDefaults(); } public function overviewCards(): array { return [ [ 'title' => '仓库总数', 'value' => (int)Db::name('shipping_warehouses')->count(), 'desc' => '当前已维护的检测中心 / 收货仓库数量', ], [ 'title' => '启用仓库', 'value' => (int)Db::name('shipping_warehouses')->where('status', 'enabled')->count(), 'desc' => '当前对前台寄送页可见的仓库数量', ], [ 'title' => '安心验仓库', 'value' => (int)Db::name('shipping_warehouses')->where('service_provider', 'anxinyan')->count(), 'desc' => '归属安心验服务的默认收货中心数量', ], [ 'title' => '中检仓库', 'value' => (int)Db::name('shipping_warehouses')->where('service_provider', 'zhongjian')->count(), 'desc' => '归属中检服务的默认收货中心数量', ], ]; } public function list(): array { $rows = Db::name('shipping_warehouses') ->order('service_provider', 'asc') ->order('is_default', 'desc') ->order('sort_order', 'asc') ->order('id', 'desc') ->select() ->toArray(); return array_map(fn(array $item) => $this->formatWarehouse($item), $rows); } public function save(array $payload, int $id = 0): int { $now = date('Y-m-d H:i:s'); $serviceProvider = trim((string)($payload['service_provider'] ?? 'anxinyan')); $status = trim((string)($payload['status'] ?? 'enabled')); $supportedCategoryIds = $this->normalizeIntArray($payload['supported_category_ids'] ?? []); $serviceAreaProvinces = $this->normalizeStringArray($payload['service_area_provinces'] ?? []); $serviceAreaCities = $this->normalizeStringArray($payload['service_area_cities'] ?? []); $warehouseCode = trim((string)($payload['warehouse_code'] ?? '')); if ($warehouseCode === '') { $warehouseCode = $this->generateWarehouseCode($serviceProvider); } $existsByCode = Db::name('shipping_warehouses') ->where('warehouse_code', $warehouseCode) ->when($id > 0, fn($query) => $query->where('id', '<>', $id)) ->find(); if ($existsByCode) { throw new \RuntimeException('仓库编码已存在,请更换后重试'); } $data = [ 'warehouse_name' => trim((string)($payload['warehouse_name'] ?? '')), 'warehouse_code' => $warehouseCode, 'warehouse_type' => 'detection_center', 'service_provider' => $serviceProvider, 'receiver_name' => trim((string)($payload['receiver_name'] ?? '')), 'receiver_mobile' => trim((string)($payload['receiver_mobile'] ?? '')), 'province' => trim((string)($payload['province'] ?? '')), 'city' => trim((string)($payload['city'] ?? '')), 'district' => trim((string)($payload['district'] ?? '')), 'detail_address' => trim((string)($payload['detail_address'] ?? '')), 'service_time' => trim((string)($payload['service_time'] ?? '')), 'notice' => trim((string)($payload['notice'] ?? '')), 'supported_category_ids_json' => $supportedCategoryIds ? json_encode($supportedCategoryIds, JSON_UNESCAPED_UNICODE) : null, 'service_area_provinces_json' => $serviceAreaProvinces ? json_encode($serviceAreaProvinces, JSON_UNESCAPED_UNICODE) : null, 'service_area_cities_json' => $serviceAreaCities ? json_encode($serviceAreaCities, JSON_UNESCAPED_UNICODE) : null, 'status' => $status !== '' ? $status : 'enabled', 'is_default' => !empty($payload['is_default']) ? 1 : 0, 'sort_order' => (int)($payload['sort_order'] ?? 0), 'remark' => trim((string)($payload['remark'] ?? '')), 'updated_at' => $now, ]; $this->validatePayload($data); Db::startTrans(); try { if ((int)$data['is_default'] === 1) { Db::name('shipping_warehouses') ->where('service_provider', $serviceProvider) ->update([ 'is_default' => 0, 'updated_at' => $now, ]); } if ($id > 0) { Db::name('shipping_warehouses')->where('id', $id)->update($data); $warehouseId = $id; } else { $data['created_at'] = $now; $warehouseId = (int)Db::name('shipping_warehouses')->insertGetId($data); } if ((int)$data['is_default'] !== 1) { $currentDefault = Db::name('shipping_warehouses') ->where('service_provider', $serviceProvider) ->where('status', 'enabled') ->where('is_default', 1) ->find(); if (!$currentDefault) { Db::name('shipping_warehouses')->where('id', $warehouseId)->update([ 'is_default' => 1, 'updated_at' => $now, ]); } } Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } return $warehouseId; } public function resolveForShipping(string $serviceProvider, ?int $categoryId = null, ?array $userAddress = null): array { $options = $this->optionsForOrder($serviceProvider, $categoryId, $userAddress); if (!$options) { return [ 'warehouse_id' => 0, 'warehouse_name' => '', 'warehouse_code' => '', 'receiver_name' => '', 'receiver_mobile' => '', 'province' => '', 'city' => '', 'district' => '', 'detail_address' => '', 'service_time' => '', 'notice' => '', ]; } $matched = $options[0]; return [ 'warehouse_id' => (int)$matched['id'], 'warehouse_name' => $matched['warehouse_name'], 'warehouse_code' => $matched['warehouse_code'], 'receiver_name' => $matched['receiver_name'], 'receiver_mobile' => $matched['receiver_mobile'], 'province' => $matched['province'], 'city' => $matched['city'], 'district' => $matched['district'], 'detail_address' => $matched['detail_address'], 'service_time' => $matched['service_time'], 'notice' => $matched['notice'], ]; } public function bindOrderTarget(int $orderId, string $serviceProvider, ?int $categoryId = null, ?array $userAddress = null): array { $snapshot = $this->resolveForShipping($serviceProvider, $categoryId, $userAddress); $now = date('Y-m-d H:i:s'); $payload = [ 'order_id' => $orderId, 'warehouse_id' => (int)($snapshot['warehouse_id'] ?? 0) ?: null, 'warehouse_name' => $snapshot['warehouse_name'] ?? '', 'warehouse_code' => $snapshot['warehouse_code'] ?? '', 'service_provider' => $serviceProvider, 'receiver_name' => $snapshot['receiver_name'] ?? '', 'receiver_mobile' => $snapshot['receiver_mobile'] ?? '', 'province' => $snapshot['province'] ?? '', 'city' => $snapshot['city'] ?? '', 'district' => $snapshot['district'] ?? '', 'detail_address' => $snapshot['detail_address'] ?? '', 'service_time' => $snapshot['service_time'] ?? '', 'notice' => $snapshot['notice'] ?? '', 'updated_at' => $now, ]; $exists = Db::name('order_shipping_targets')->where('order_id', $orderId)->find(); if ($exists) { Db::name('order_shipping_targets')->where('order_id', $orderId)->update($payload); } else { $payload['created_at'] = $now; Db::name('order_shipping_targets')->insert($payload); } return $payload; } public function getOrderTarget(int $orderId): ?array { $row = Db::name('order_shipping_targets')->where('order_id', $orderId)->find(); if (!$row) { return null; } return [ 'warehouse_id' => (int)($row['warehouse_id'] ?? 0), 'warehouse_name' => $row['warehouse_name'] ?? '', 'warehouse_code' => $row['warehouse_code'] ?? '', 'receiver_name' => $row['receiver_name'] ?? '', 'receiver_mobile' => $row['receiver_mobile'] ?? '', 'province' => $row['province'] ?? '', 'city' => $row['city'] ?? '', 'district' => $row['district'] ?? '', 'detail_address' => $row['detail_address'] ?? '', 'service_time' => $row['service_time'] ?? '', 'notice' => $row['notice'] ?? '', ]; } public function optionsForOrder(string $serviceProvider, ?int $categoryId = null, ?array $userAddress = null): array { $rows = Db::name('shipping_warehouses') ->where('status', 'enabled') ->where('service_provider', $serviceProvider) ->select() ->toArray(); if (!$rows) { $rows = Db::name('shipping_warehouses') ->where('status', 'enabled') ->select() ->toArray(); } $list = array_map(fn(array $item) => $this->formatWarehouse($item), $rows); foreach ($list as &$item) { $item['match_score'] = $this->matchScore($item, $categoryId, $userAddress); $item['is_recommended'] = $item['match_score'] >= 300; $item['recommended_reason'] = $this->recommendedReason($item, $categoryId, $userAddress); } unset($item); usort($list, static function (array $left, array $right) { if ($left['match_score'] === $right['match_score']) { if ((int)$left['is_default'] === (int)$right['is_default']) { if ((int)$left['sort_order'] === (int)$right['sort_order']) { return (int)$left['id'] <=> (int)$right['id']; } return (int)$left['sort_order'] <=> (int)$right['sort_order']; } return (int)$right['is_default'] <=> (int)$left['is_default']; } return (int)$right['match_score'] <=> (int)$left['match_score']; }); return $list; } private function formatWarehouse(array $item): array { $supportedCategoryIds = $this->decodeIntArray($item['supported_category_ids_json'] ?? null); $serviceAreaProvinces = $this->decodeStringArray($item['service_area_provinces_json'] ?? null); $serviceAreaCities = $this->decodeStringArray($item['service_area_cities_json'] ?? null); $categoryNames = []; if ($supportedCategoryIds) { $categoryNames = Db::name('catalog_categories') ->whereIn('id', $supportedCategoryIds) ->column('name'); } return [ 'id' => (int)$item['id'], 'warehouse_name' => $item['warehouse_name'], 'warehouse_code' => $item['warehouse_code'], 'warehouse_type' => $item['warehouse_type'], 'warehouse_type_text' => '检测中心 / 收货仓库', 'service_provider' => $item['service_provider'], 'service_provider_text' => $item['service_provider'] === 'zhongjian' ? '中检鉴定' : '实物鉴定', 'receiver_name' => $item['receiver_name'], 'receiver_mobile' => $item['receiver_mobile'], 'province' => $item['province'], 'city' => $item['city'], 'district' => $item['district'], 'detail_address' => $item['detail_address'], 'full_address' => trim(sprintf('%s%s%s%s', $item['province'], $item['city'], $item['district'], $item['detail_address'])), 'service_time' => $item['service_time'], 'notice' => $item['notice'], 'supported_category_ids' => $supportedCategoryIds, 'supported_category_names' => array_values($categoryNames), 'service_area_provinces' => $serviceAreaProvinces, 'service_area_cities' => $serviceAreaCities, 'status' => $item['status'], 'status_text' => $item['status'] === 'enabled' ? '启用中' : '已停用', 'is_default' => (bool)$item['is_default'], 'sort_order' => (int)$item['sort_order'], 'remark' => $item['remark'] ?? '', 'created_at' => $item['created_at'] ?? '', 'updated_at' => $item['updated_at'] ?? '', ]; } private function validatePayload(array $data): void { foreach (['warehouse_name', 'receiver_name', 'receiver_mobile', 'province', 'city', 'district', 'detail_address', 'service_time'] as $field) { if (trim((string)($data[$field] ?? '')) === '') { throw new \RuntimeException('请完整填写仓库名称、收件信息与地址'); } } } private function normalizeIntArray(mixed $value): array { if (!is_array($value)) { return []; } return array_values(array_unique(array_filter(array_map(static function ($item) { $int = (int)$item; return $int > 0 ? $int : null; }, $value)))); } private function normalizeStringArray(mixed $value): array { if (!is_array($value)) { return []; } $items = []; foreach ($value as $item) { $text = trim((string)$item); if ($text === '') { continue; } $items[] = $text; } return array_values(array_unique($items)); } private function decodeIntArray(mixed $value): array { if (is_array($value)) { return $this->normalizeIntArray($value); } if (is_string($value) && $value !== '') { $decoded = json_decode($value, true); return is_array($decoded) ? $this->normalizeIntArray($decoded) : []; } return []; } private function decodeStringArray(mixed $value): array { if (is_array($value)) { return $this->normalizeStringArray($value); } if (is_string($value) && $value !== '') { $decoded = json_decode($value, true); return is_array($decoded) ? $this->normalizeStringArray($decoded) : []; } return []; } private function generateWarehouseCode(string $serviceProvider): string { $prefix = $serviceProvider === 'zhongjian' ? 'ZJ' : 'AXY'; return sprintf('%s-WH-%s', $prefix, date('YmdHis')); } private function ensureWarehouseTable(): void { Db::execute(<<<'SQL' CREATE TABLE IF NOT EXISTS shipping_warehouses ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, warehouse_name VARCHAR(128) NOT NULL DEFAULT '', warehouse_code VARCHAR(64) NOT NULL DEFAULT '', warehouse_type VARCHAR(32) NOT NULL DEFAULT 'detection_center', service_provider VARCHAR(32) NOT NULL DEFAULT 'anxinyan', receiver_name VARCHAR(64) NOT NULL DEFAULT '', receiver_mobile VARCHAR(32) NOT NULL DEFAULT '', province VARCHAR(64) NOT NULL DEFAULT '', city VARCHAR(64) NOT NULL DEFAULT '', district VARCHAR(64) NOT NULL DEFAULT '', detail_address VARCHAR(255) NOT NULL DEFAULT '', service_time VARCHAR(128) NOT NULL DEFAULT '', notice VARCHAR(500) NOT NULL DEFAULT '', supported_category_ids_json JSON NULL, service_area_provinces_json JSON NULL, service_area_cities_json JSON NULL, status VARCHAR(32) NOT NULL DEFAULT 'enabled', is_default TINYINT(1) NOT NULL DEFAULT 0, sort_order INT NOT NULL DEFAULT 0, remark VARCHAR(255) NOT NULL DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_shipping_warehouses_code (warehouse_code), KEY idx_shipping_warehouses_service_provider (service_provider), KEY idx_shipping_warehouses_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收货仓库 / 检测中心'; SQL); } private function ensureWarehouseRuleColumns(): void { $columns = Db::query("SHOW COLUMNS FROM shipping_warehouses LIKE 'service_area_provinces_json'"); if (!$columns) { Db::execute("ALTER TABLE shipping_warehouses ADD COLUMN service_area_provinces_json JSON NULL AFTER supported_category_ids_json"); } $columns = Db::query("SHOW COLUMNS FROM shipping_warehouses LIKE 'service_area_cities_json'"); if (!$columns) { Db::execute("ALTER TABLE shipping_warehouses ADD COLUMN service_area_cities_json JSON NULL AFTER service_area_provinces_json"); } } private function ensureOrderTargetTable(): void { Db::execute(<<<'SQL' CREATE TABLE IF NOT EXISTS order_shipping_targets ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_id BIGINT UNSIGNED NOT NULL, warehouse_id BIGINT UNSIGNED NULL DEFAULT NULL, warehouse_name VARCHAR(128) NOT NULL DEFAULT '', warehouse_code VARCHAR(64) NOT NULL DEFAULT '', service_provider VARCHAR(32) NOT NULL DEFAULT 'anxinyan', receiver_name VARCHAR(64) NOT NULL DEFAULT '', receiver_mobile VARCHAR(32) NOT NULL DEFAULT '', province VARCHAR(64) NOT NULL DEFAULT '', city VARCHAR(64) NOT NULL DEFAULT '', district VARCHAR(64) NOT NULL DEFAULT '', detail_address VARCHAR(255) NOT NULL DEFAULT '', service_time VARCHAR(128) NOT NULL DEFAULT '', notice VARCHAR(500) NOT NULL DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_shipping_targets_order_id (order_id), KEY idx_order_shipping_targets_warehouse_id (warehouse_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单锁定仓库快照' SQL); } private function bootstrapDefaults(): void { $count = (int)Db::name('shipping_warehouses')->count(); if ($count > 0) { return; } $now = date('Y-m-d H:i:s'); Db::name('shipping_warehouses')->insertAll([ [ 'warehouse_name' => '安心验鉴定中心', 'warehouse_code' => 'AXY-WH-DEFAULT', 'warehouse_type' => 'detection_center', 'service_provider' => 'anxinyan', 'receiver_name' => '安心验鉴定中心', 'receiver_mobile' => '400-800-1314', 'province' => '广东省', 'city' => '深圳市', 'district' => '南山区', 'detail_address' => '科技园鉴定路 88 号 安心验收件中心', 'service_time' => '周一至周日 09:30-18:30', 'notice' => '寄送前请确认订单信息完整,包裹内附上订单号可提升签收后的处理效率。', 'supported_category_ids_json' => null, 'service_area_provinces_json' => null, 'service_area_cities_json' => null, 'status' => 'enabled', 'is_default' => 1, 'sort_order' => 1, 'remark' => '默认仓库', 'created_at' => $now, 'updated_at' => $now, ], [ 'warehouse_name' => '中检合作鉴定中心', 'warehouse_code' => 'ZJ-WH-DEFAULT', 'warehouse_type' => 'detection_center', 'service_provider' => 'zhongjian', 'receiver_name' => '中检合作鉴定中心', 'receiver_mobile' => '400-800-1314', 'province' => '广东省', 'city' => '深圳市', 'district' => '南山区', 'detail_address' => '科技园鉴定路 88 号 安心验中检收件中心', 'service_time' => '周一至周日 09:30-18:30', 'notice' => '中检鉴定订单请优先附上鉴定单号,寄出后尽快填写运单号。', 'supported_category_ids_json' => null, 'service_area_provinces_json' => null, 'service_area_cities_json' => null, 'status' => 'enabled', 'is_default' => 1, 'sort_order' => 1, 'remark' => '默认仓库', 'created_at' => $now, 'updated_at' => $now, ], ]); } private function matchScore(array $warehouse, ?int $categoryId, ?array $userAddress): int { $score = 0; if ($categoryId && (!$warehouse['supported_category_ids'] || in_array($categoryId, $warehouse['supported_category_ids'], true))) { $score += 200; } if ($userAddress) { $province = trim((string)($userAddress['province'] ?? '')); $city = trim((string)($userAddress['city'] ?? '')); if ($province !== '' && (!$warehouse['service_area_provinces'] || in_array($province, $warehouse['service_area_provinces'], true))) { $score += 120; } if ($city !== '' && (!$warehouse['service_area_cities'] || in_array($city, $warehouse['service_area_cities'], true))) { $score += 180; } } if ($warehouse['is_default']) { $score += 40; } return $score; } private function recommendedReason(array $warehouse, ?int $categoryId, ?array $userAddress): string { $reasons = []; if ($categoryId && (!$warehouse['supported_category_ids'] || in_array($categoryId, $warehouse['supported_category_ids'], true))) { $reasons[] = '匹配当前品类'; } if ($userAddress) { $province = trim((string)($userAddress['province'] ?? '')); $city = trim((string)($userAddress['city'] ?? '')); if ($city !== '' && (!$warehouse['service_area_cities'] || in_array($city, $warehouse['service_area_cities'], true))) { $reasons[] = '匹配当前城市'; } elseif ($province !== '' && (!$warehouse['service_area_provinces'] || in_array($province, $warehouse['service_area_provinces'], true))) { $reasons[] = '匹配当前省份'; } } if (!$reasons && $warehouse['is_default']) { $reasons[] = '默认仓库'; } return implode(' / ', $reasons); } }