feat: add kuaidi100 logistics sync

This commit is contained in:
wushumin
2026-05-26 17:08:33 +08:00
parent 09d9fcbe69
commit a5f00d7e31
31 changed files with 2596 additions and 67 deletions

View File

@@ -2,6 +2,7 @@
namespace app\support;
use support\Log;
use support\think\Db;
class ExpressCompanyService
@@ -9,6 +10,7 @@ class ExpressCompanyService
public function __construct()
{
$this->ensureTable();
$this->ensureCatalogTable();
$this->bootstrapDefaults();
}
@@ -118,6 +120,414 @@ class ExpressCompanyService
return trim((string)($row['company_name'] ?? ''));
}
public function catalogList(string $keyword = '', int $limit = 30): array
{
$keyword = trim($keyword);
$limit = max(1, min(100, $limit));
$this->ensureCatalogTable();
$query = Db::name('kuaidi100_express_company_catalog')
->order('sort_order', 'asc')
->order('company_name', 'asc')
->order('id', 'asc');
if ($keyword !== '') {
$like = '%' . $keyword . '%';
$query->whereRaw('(company_name LIKE :keyword OR company_code LIKE :keyword OR company_type LIKE :keyword)', [
'keyword' => $like,
]);
}
$rows = $query->limit($limit)->select()->toArray();
if (!$rows) {
$rows = $this->localCompanyFallbackList($keyword, $limit);
}
return array_map(fn (array $item) => $this->formatCatalog($item), $rows);
}
public function catalogTotal(): int
{
$this->ensureCatalogTable();
return (int)Db::name('kuaidi100_express_company_catalog')->count();
}
public function catalogSyncedAt(): string
{
$this->ensureCatalogTable();
$value = Db::name('kuaidi100_express_company_catalog')
->order('synced_at', 'desc')
->value('synced_at');
$value = trim((string)($value ?? ''));
return $value === '0' ? '' : $value;
}
public function syncCatalog(): array
{
$client = new Kuaidi100Client();
$binary = $client->downloadCompanyCatalogWorkbook();
$tmpFile = tempnam(sys_get_temp_dir(), 'kdbm_');
if ($tmpFile === false) {
throw new \RuntimeException('快递100公司码表临时文件创建失败');
}
file_put_contents($tmpFile, $binary);
$now = date('Y-m-d H:i:s');
Db::startTrans();
try {
$rows = $this->parseCatalogWorkbook($tmpFile);
if (!$rows) {
throw new \RuntimeException('快递100公司码表解析结果为空');
}
$existingCodes = Db::name('kuaidi100_express_company_catalog')
->column('company_code');
$existingCodeMap = array_fill_keys(array_map('strval', $existingCodes), true);
$inserted = 0;
$updated = 0;
$payloadRows = [];
foreach ($rows as $index => $item) {
$payload = [
'company_name' => $item['company_name'],
'company_code' => $item['company_code'],
'company_type' => $item['company_type'],
'sort_order' => (int)($item['sort_order'] ?? $index + 1),
'synced_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
if (isset($existingCodeMap[$payload['company_code']])) {
$updated++;
} else {
$inserted++;
}
$payloadRows[$payload['company_code']] = $payload;
}
foreach (array_chunk(array_values($payloadRows), 300) as $chunkIndex => $chunk) {
$valuesSql = [];
$bindings = [];
foreach ($chunk as $rowIndex => $payload) {
$prefix = 'r' . $chunkIndex . '_' . $rowIndex;
$valuesSql[] = sprintf(
'(:%s_company_name, :%s_company_code, :%s_company_type, :%s_sort_order, :%s_synced_at, :%s_created_at, :%s_updated_at)',
$prefix,
$prefix,
$prefix,
$prefix,
$prefix,
$prefix,
$prefix
);
foreach ($payload as $field => $value) {
$bindings[$prefix . '_' . $field] = $value;
}
}
Db::execute(
'INSERT INTO kuaidi100_express_company_catalog '
. '(company_name, company_code, company_type, sort_order, synced_at, created_at, updated_at) VALUES '
. implode(',', $valuesSql)
. ' ON DUPLICATE KEY UPDATE '
. 'company_name = VALUES(company_name), '
. 'company_type = VALUES(company_type), '
. 'sort_order = VALUES(sort_order), '
. 'synced_at = VALUES(synced_at), '
. 'updated_at = VALUES(updated_at)',
$bindings
);
}
$backfilled = $this->backfillLocalCompanyCodesFromCatalog($now);
Db::commit();
} catch (\Throwable $e) {
Db::rollback();
throw $e;
} finally {
@unlink($tmpFile);
}
return [
'total' => count($rows),
'inserted' => $inserted ?? 0,
'updated' => $updated ?? 0,
'backfilled' => $backfilled ?? 0,
'synced_at' => $now,
];
}
public function resolveCompanyCode(string $companyNameOrCode, string $trackingNo = ''): string
{
$result = $this->recognizeCompany($companyNameOrCode, $trackingNo);
return (string)($result['company_code'] ?? '');
}
public function recognizeCompany(string $companyNameOrCode, string $trackingNo = ''): array
{
$companyNameOrCode = trim($companyNameOrCode);
$trackingNo = trim($trackingNo);
$recognitionError = '';
$resolved = $this->resolveLocalCompanyMatch($companyNameOrCode);
if (!$resolved && $trackingNo !== '') {
try {
$candidates = $this->recognizeKuaidi100Candidates($trackingNo);
} catch (\Throwable $e) {
$candidates = [];
$recognitionError = $e->getMessage();
}
$resolved = $this->pickRecognizedCandidate($candidates, $companyNameOrCode);
return [
'input' => $companyNameOrCode,
'tracking_no' => $trackingNo,
'company_code' => (string)($resolved['company_code'] ?? ''),
'company_name' => (string)($resolved['company_name'] ?? $companyNameOrCode),
'status' => $resolved ? 'resolved' : ($candidates ? 'multiple' : 'none'),
'status_text' => $resolved ? '已识别' : ($candidates ? '识别到多个候选' : '未识别'),
'error_message' => $recognitionError,
'resolved' => $resolved,
'candidates' => $candidates,
];
}
if ($resolved) {
return [
'input' => $companyNameOrCode,
'tracking_no' => $trackingNo,
'company_code' => (string)($resolved['company_code'] ?? ''),
'company_name' => (string)($resolved['company_name'] ?? $companyNameOrCode),
'status' => 'resolved',
'status_text' => '已识别',
'error_message' => '',
'resolved' => $resolved,
'candidates' => [$resolved],
];
}
return [
'input' => $companyNameOrCode,
'tracking_no' => $trackingNo,
'company_code' => '',
'company_name' => $companyNameOrCode,
'status' => 'none',
'status_text' => '未识别',
'error_message' => $recognitionError,
'resolved' => null,
'candidates' => [],
];
}
public function resolveCompanyNameByCode(string $companyCode): string
{
$companyCode = trim($companyCode);
if ($companyCode === '') {
return '';
}
$row = Db::name('express_companies')
->where('company_code', $companyCode)
->find();
$name = trim((string)($row['company_name'] ?? ''));
if ($name !== '') {
return $name;
}
$row = Db::name('kuaidi100_express_company_catalog')
->where('company_code', $companyCode)
->find();
$name = trim((string)($row['company_name'] ?? ''));
if ($name !== '') {
return $name;
}
return $this->companyAliasName($companyCode);
}
private function resolveLocalCompanyMatch(string $companyNameOrCode): ?array
{
$companyNameOrCode = trim($companyNameOrCode);
if ($companyNameOrCode === '') {
return null;
}
$row = Db::name('express_companies')
->where('company_name', $companyNameOrCode)
->find();
$code = trim((string)($row['company_code'] ?? ''));
if ($this->isValidCompanyCode($code)) {
return [
'company_name' => (string)($row['company_name'] ?? $companyNameOrCode),
'company_code' => $code,
'source' => 'local',
];
}
$row = Db::name('express_companies')
->where('company_code', $companyNameOrCode)
->find();
$code = trim((string)($row['company_code'] ?? ''));
if ($this->isValidCompanyCode($code)) {
$name = trim((string)($row['company_name'] ?? ''));
if ($name === '') {
$name = $this->resolveCompanyNameByCode($code);
}
if ($name === '') {
$name = $companyNameOrCode;
}
return [
'company_name' => $name,
'company_code' => $code,
'source' => 'local',
];
}
$catalogRow = Db::name('kuaidi100_express_company_catalog')
->where('company_name', $companyNameOrCode)
->find();
$code = trim((string)($catalogRow['company_code'] ?? ''));
if ($this->isValidCompanyCode($code)) {
return [
'company_name' => (string)($catalogRow['company_name'] ?? $companyNameOrCode),
'company_code' => $code,
'source' => 'catalog',
];
}
$catalogRow = Db::name('kuaidi100_express_company_catalog')
->where('company_code', $companyNameOrCode)
->find();
$code = trim((string)($catalogRow['company_code'] ?? ''));
if ($this->isValidCompanyCode($code)) {
$name = trim((string)($catalogRow['company_name'] ?? ''));
if ($name === '') {
$name = $this->resolveCompanyNameByCode($code);
}
if ($name === '') {
$name = $companyNameOrCode;
}
return [
'company_name' => $name,
'company_code' => $code,
'source' => 'catalog',
];
}
$aliasCode = $this->companyAliasCode($companyNameOrCode);
if ($aliasCode !== '') {
return [
'company_name' => $this->resolveCompanyNameByCode($aliasCode) ?: $companyNameOrCode,
'company_code' => $aliasCode,
'source' => 'alias',
];
}
return $this->isValidCompanyCode($companyNameOrCode)
? [
'company_name' => $this->resolveCompanyNameByCode($companyNameOrCode) ?: $companyNameOrCode,
'company_code' => $companyNameOrCode,
'source' => 'code',
]
: null;
}
private function recognizeKuaidi100Candidates(string $trackingNo): array
{
$client = new Kuaidi100Client();
$response = $client->recognize($trackingNo);
$items = $this->extractRecognizeItems($response);
if (!$items) {
return [];
}
$candidates = [];
foreach ($items as $item) {
$candidate = $this->normalizeRecognizeCandidate($item);
if ($candidate) {
$candidates[] = $candidate;
}
}
return $candidates;
}
private function extractRecognizeItems(array $response): array
{
if (isset($response['data']) && is_array($response['data'])) {
$items = $response['data'];
} else {
$items = $response;
}
if ($this->isAssocArray($items) && (isset($items['comCode']) || isset($items['company_code']))) {
return [$items];
}
return array_values(array_filter($items, static fn ($item) => is_array($item)));
}
private function normalizeRecognizeCandidate(array $item): ?array
{
$companyCode = trim((string)($item['comCode'] ?? $item['company_code'] ?? $item['code'] ?? ''));
$officialName = trim((string)($item['name'] ?? $item['company_name'] ?? $item['comName'] ?? ''));
if ($companyCode === '' && $officialName === '') {
return null;
}
$localName = $companyCode !== '' ? $this->resolveCompanyNameByCode($companyCode) : '';
$companyName = $localName !== '' ? $localName : ($officialName !== '' ? $officialName : $companyCode);
return [
'company_name' => $companyName,
'company_code' => $companyCode,
'official_name' => $officialName,
'display_text' => $companyName !== '' && $companyCode !== ''
? sprintf('%s / %s', $companyName, $companyCode)
: ($companyName !== '' ? $companyName : $companyCode),
'length_pre' => (int)($item['lengthPre'] ?? $item['length_pre'] ?? 0),
'source' => $localName !== '' ? 'catalog' : 'kuaidi100',
];
}
private function pickRecognizedCandidate(array $candidates, string $companyNameOrCode): ?array
{
if (!$candidates) {
return null;
}
if (count($candidates) === 1) {
return $candidates[0];
}
$companyNameOrCode = trim($companyNameOrCode);
if ($companyNameOrCode === '') {
return null;
}
foreach ($candidates as $candidate) {
if (!is_array($candidate)) {
continue;
}
$candidateName = trim((string)($candidate['company_name'] ?? ''));
$candidateCode = trim((string)($candidate['company_code'] ?? ''));
if ($candidateName === $companyNameOrCode || $candidateCode === $companyNameOrCode) {
return $candidate;
}
}
return null;
}
private function isAssocArray(array $items): bool
{
return array_keys($items) !== range(0, count($items) - 1);
}
private function format(array $item): array
{
$status = (string)($item['status'] ?? 'enabled');
@@ -136,21 +546,63 @@ class ExpressCompanyService
];
}
private function formatCatalog(array $item): array
{
$source = (string)($item['source'] ?? 'kuaidi100');
$companyName = (string)($item['company_name'] ?? '');
$companyCode = (string)($item['company_code'] ?? '');
$companyType = (string)($item['company_type'] ?? '');
return [
'id' => (int)($item['id'] ?? 0),
'company_name' => $companyName,
'company_code' => $companyCode,
'company_type' => $companyType,
'display_text' => $companyType !== ''
? sprintf('%s / %s / %s', $companyName, $companyCode, $companyType)
: sprintf('%s / %s', $companyName, $companyCode),
'source' => $source,
'synced_at' => (string)($item['synced_at'] ?? ''),
];
}
private function bootstrapDefaults(): void
{
$now = date('Y-m-d H:i:s');
$exists = Db::name('express_companies')->where('company_name', '顺丰速运')->find();
if (!$exists) {
Db::name('express_companies')->insert([
'company_name' => '顺丰速运',
'company_code' => 'sf_express',
'status' => 'enabled',
'is_default' => 1,
'sort_order' => 1,
'remark' => '系统默认快递公司',
'created_at' => $now,
'updated_at' => $now,
]);
$defaults = [
['company_name' => '顺丰速运', 'company_code' => 'shunfeng', 'sort_order' => 1, 'is_default' => 1],
['company_name' => '京东快递', 'company_code' => 'jd', 'sort_order' => 2, 'is_default' => 0],
['company_name' => 'EMS', 'company_code' => 'ems', 'sort_order' => 3, 'is_default' => 0],
['company_name' => '中通快递', 'company_code' => 'zhongtong', 'sort_order' => 4, 'is_default' => 0],
['company_name' => '圆通速递', 'company_code' => 'yuantong', 'sort_order' => 5, 'is_default' => 0],
['company_name' => '申通快递', 'company_code' => 'shentong', 'sort_order' => 6, 'is_default' => 0],
['company_name' => '韵达快递', 'company_code' => 'yunda', 'sort_order' => 7, 'is_default' => 0],
['company_name' => '极兔速递', 'company_code' => 'jtexpress', 'sort_order' => 8, 'is_default' => 0],
];
foreach ($defaults as $default) {
$exists = Db::name('express_companies')->where('company_name', $default['company_name'])->find();
if (!$exists) {
Db::name('express_companies')->insert([
'company_name' => $default['company_name'],
'company_code' => $default['company_code'],
'status' => 'enabled',
'is_default' => $default['is_default'],
'sort_order' => $default['sort_order'],
'remark' => '系统默认快递公司',
'created_at' => $now,
'updated_at' => $now,
]);
continue;
}
$currentCode = (string)($exists['company_code'] ?? '');
if ($currentCode === '' || $currentCode === 'sf_express' || str_starts_with($currentCode, 'express_')) {
Db::name('express_companies')->where('id', $exists['id'])->update([
'company_code' => $default['company_code'],
'updated_at' => $now,
]);
}
}
$this->ensureEnabledDefault($now);
@@ -179,9 +631,289 @@ class ExpressCompanyService
}
}
private function generateCompanyCode(string $companyName): string
private function backfillLocalCompanyCodesFromCatalog(string $now): int
{
return 'express_' . substr(hash('sha256', $companyName), 0, 12);
$catalogRows = Db::name('kuaidi100_express_company_catalog')
->select()
->toArray();
if (!$catalogRows) {
return 0;
}
$catalogMap = [];
foreach ($catalogRows as $row) {
$name = trim((string)($row['company_name'] ?? ''));
$code = trim((string)($row['company_code'] ?? ''));
if ($name === '' || !$this->isValidCompanyCode($code)) {
continue;
}
$catalogMap[$name] = $code;
}
if (!$catalogMap) {
return 0;
}
$updated = 0;
$rows = Db::name('express_companies')->select()->toArray();
foreach ($rows as $row) {
$companyName = trim((string)($row['company_name'] ?? ''));
$currentCode = trim((string)($row['company_code'] ?? ''));
if ($companyName === '' || !isset($catalogMap[$companyName])) {
continue;
}
if (!$this->shouldBackfillCompanyCode($currentCode)) {
continue;
}
Db::name('express_companies')->where('id', (int)$row['id'])->update([
'company_code' => $catalogMap[$companyName],
'updated_at' => $now,
]);
$updated++;
}
return $updated;
}
private function shouldBackfillCompanyCode(string $currentCode): bool
{
return $currentCode === '' || $currentCode === 'sf_express' || str_starts_with($currentCode, 'express_');
}
private function localCompanyFallbackList(string $keyword, int $limit): array
{
$query = Db::name('express_companies')
->order('is_default', 'desc')
->order('sort_order', 'asc')
->order('id', 'asc');
if ($keyword !== '') {
$like = '%' . $keyword . '%';
$query->whereRaw('(company_name LIKE :keyword OR company_code LIKE :keyword)', [
'keyword' => $like,
]);
}
$rows = $query->limit($limit)->select()->toArray();
return array_map(static fn (array $item) => [
'id' => (int)$item['id'],
'company_name' => (string)$item['company_name'],
'company_code' => (string)$item['company_code'],
'company_type' => '本地快递公司',
'source' => 'local',
'synced_at' => '',
], $rows);
}
private function parseCatalogWorkbook(string $filePath): array
{
$zip = new \ZipArchive();
if ($zip->open($filePath) !== true) {
throw new \RuntimeException('快递100公司码表压缩包打开失败');
}
$sheetXml = $zip->getFromName('xl/worksheets/sheet1.xml');
if (!is_string($sheetXml) || $sheetXml === '') {
$zip->close();
throw new \RuntimeException('快递100公司码表缺少工作表数据');
}
$sharedStrings = [];
$sharedStringsXml = $zip->getFromName('xl/sharedStrings.xml');
if (is_string($sharedStringsXml) && $sharedStringsXml !== '') {
$sharedStrings = $this->readSharedStrings($sharedStringsXml);
}
$zip->close();
$xml = simplexml_load_string($sheetXml, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($xml === false) {
throw new \RuntimeException('快递100公司码表工作表解析失败');
}
$xml->registerXPathNamespace('x', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$rows = $xml->xpath('//x:sheetData/x:row') ?: [];
$headerMap = [];
$parsedRows = [];
foreach ($rows as $row) {
$values = [];
foreach ($row->c as $cell) {
$columnIndex = $this->columnIndexFromCellRef((string)$cell['r']);
if ($columnIndex <= 0) {
continue;
}
$values[$columnIndex] = $this->readXlsxCellValue($cell, $sharedStrings);
}
if (!$values) {
continue;
}
if (!$headerMap) {
$headerMap = $this->buildCatalogHeaderMap($values);
if ($headerMap) {
continue;
}
}
$companyName = trim((string)($values[$headerMap['company_name'] ?? 1] ?? ''));
$companyCode = trim((string)($values[$headerMap['company_code'] ?? 2] ?? ''));
$companyType = trim((string)($values[$headerMap['company_type'] ?? 3] ?? ''));
if ($companyName === '' || !$this->isValidCompanyCode($companyCode)) {
continue;
}
$parsedRows[] = [
'company_name' => $companyName,
'company_code' => $companyCode,
'company_type' => $companyType,
'sort_order' => count($parsedRows) + 1,
];
}
return $parsedRows;
}
private function buildCatalogHeaderMap(array $values): array
{
$map = [];
foreach ($values as $columnIndex => $value) {
$text = trim((string)$value);
if ($text === '') {
continue;
}
if (in_array($text, ['公司名称', '快递公司名称', '名称'], true)) {
$map['company_name'] = $columnIndex;
} elseif (in_array($text, ['公司编码', '快递公司编码', '编码', 'code'], true)) {
$map['company_code'] = $columnIndex;
} elseif (in_array($text, ['公司类型', '类型'], true)) {
$map['company_type'] = $columnIndex;
}
}
if (!$map) {
return [];
}
$map['company_name'] ??= 1;
$map['company_code'] ??= 2;
$map['company_type'] ??= 3;
return $map;
}
private function readSharedStrings(string $xml): array
{
$document = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($document === false) {
throw new \RuntimeException('快递100公司码表共享字符串解析失败');
}
$document->registerXPathNamespace('x', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$items = $document->xpath('//x:si') ?: [];
$values = [];
foreach ($items as $item) {
$text = '';
foreach ($item->xpath('.//x:t') ?: [] as $textNode) {
$text .= (string)$textNode;
}
$values[] = $text;
}
return $values;
}
private function readXlsxCellValue(\SimpleXMLElement $cell, array $sharedStrings): string
{
$type = (string)$cell['t'];
if ($type === 'inlineStr') {
$text = '';
foreach ($cell->xpath('.//*[local-name()="t"]') ?: [] as $textNode) {
$text .= (string)$textNode;
}
return trim($text);
}
if ($type === 's') {
$index = (int)($cell->v ?? 0);
return trim((string)($sharedStrings[$index] ?? ''));
}
if (isset($cell->v)) {
return trim((string)$cell->v);
}
return trim((string)$cell);
}
private function columnIndexFromCellRef(string $cellRef): int
{
if (!preg_match('/^([A-Z]+)\d+$/i', $cellRef, $matches)) {
return 0;
}
$letters = strtoupper($matches[1]);
$index = 0;
$length = strlen($letters);
for ($i = 0; $i < $length; $i++) {
$index = $index * 26 + (ord($letters[$i]) - 64);
}
return $index;
}
private function isValidCompanyCode(string $code): bool
{
return $code !== '' && preg_match('/^[a-z0-9]+$/', $code) === 1 && !str_starts_with($code, 'express_');
}
private function companyAliasCode(string $companyName): string
{
$aliases = [
'顺丰速运' => 'shunfeng',
'顺丰' => 'shunfeng',
'sf_express' => 'shunfeng',
'京东快递' => 'jd',
'京东物流' => 'jd',
'EMS' => 'ems',
'ems' => 'ems',
'中通快递' => 'zhongtong',
'圆通速递' => 'yuantong',
'圆通快递' => 'yuantong',
'申通快递' => 'shentong',
'韵达快递' => 'yunda',
'极兔速递' => 'jtexpress',
'极兔快递' => 'jtexpress',
'德邦快递' => 'debangwuliu',
'邮政快递包裹' => 'youzhengguonei',
];
return $aliases[$companyName] ?? '';
}
private function companyAliasName(string $companyCode): string
{
$aliases = [
'shunfeng' => '顺丰速运',
'sf_express' => '顺丰速运',
'jd' => '京东快递',
'ems' => 'EMS',
'zhongtong' => '中通快递',
'yuantong' => '圆通速递',
'shentong' => '申通快递',
'yunda' => '韵达快递',
'jtexpress' => '极兔速递',
'debangwuliu' => '德邦快递',
'youzhengguonei' => '邮政快递包裹',
];
return $aliases[$companyCode] ?? '';
}
private function ensureTable(): void
@@ -203,6 +935,27 @@ CREATE TABLE IF NOT EXISTS express_companies (
KEY idx_express_companies_status (status),
KEY idx_express_companies_default (is_default)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='快递公司字典'
SQL);
}
private function ensureCatalogTable(): void
{
Db::execute(<<<'SQL'
CREATE TABLE IF NOT EXISTS kuaidi100_express_company_catalog (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
company_name VARCHAR(64) NOT NULL DEFAULT '',
company_code VARCHAR(64) NOT NULL DEFAULT '',
company_type VARCHAR(64) NOT NULL DEFAULT '',
sort_order INT NOT NULL DEFAULT 0,
synced_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
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_kuaidi100_express_company_catalog_code (company_code),
KEY idx_kuaidi100_express_company_catalog_name (company_name),
KEY idx_kuaidi100_express_company_catalog_type (company_type),
KEY idx_kuaidi100_express_company_catalog_synced_at (synced_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='快递100官方公司码表'
SQL);
}
}