This commit is contained in:
wushumin
2026-05-11 15:28:27 +08:00
commit edd1a02157
302 changed files with 67193 additions and 0 deletions

View File

@@ -0,0 +1,574 @@
<?php
namespace app\support;
use support\think\Db;
class WarehouseService
{
public function __construct()
{
$this->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);
}
}