```
feat(shopro_huifu): 添加全国联行号导入功能 新增全国联行号导入命令行工具,支持从多种数据源导入银行支行信息: - 支持导入内置种子数据(约120条主要城市数据) - 支持自动下载全国CSV文件(约20MB,来自gweesin/CNAPS) - 支持手动指定文件导入 - 提供多种操作选项:清空重导、批量导入、试运行等 更新README文档,详细说明联行号导入的使用方法和操作步骤, 包括命令示例和参数说明。 扩展bank_branches.json数据文件,添加全国主要城市的银行支行信息, 涵盖工农中建交招等主要银行在各省会城市的分支机构。 添加.gitignore规则,忽略CSV数据文件的版本控制。 ```
这个提交包含在:
@@ -87,6 +87,30 @@ php think shopro_huifu:verify
|
||||
|
||||
执行后退出后台重新登录。
|
||||
|
||||
## 全国联行号导入
|
||||
|
||||
进件表单「开户银行支行」会先查数据库表 `shopro_huifu_bank_branch`,再回退到 `data/bank_branches.json` 种子库。全国约 15 万条联行号需导入数据库后,搜索才能覆盖全国。
|
||||
|
||||
```bash
|
||||
# 1) 导入内置种子(约 120 条,主要城市 + 贵州/广东,无需下载)
|
||||
php think shopro_huifu:import-bank-branch --seed
|
||||
|
||||
# 2) 自动下载全国 CSV 并导入(约 20MB,来源 gweesin/CNAPS)
|
||||
php think shopro_huifu:import-bank-branch --download
|
||||
|
||||
# 3) 手动下载后导入(下载失败时用)
|
||||
# https://hf-mirror.com/datasets/gweesin/CNAPS/resolve/main/cnaps.csv
|
||||
php think shopro_huifu:import-bank-branch --file=addons/shopro_huifu/data/cnaps.csv
|
||||
|
||||
# 可选:清空后全量重导
|
||||
php think shopro_huifu:import-bank-branch --download --truncate
|
||||
|
||||
# 试跑(不写库)
|
||||
php think shopro_huifu:import-bank-branch --file=addons/shopro_huifu/data/cnaps.csv --limit=100 --dry-run
|
||||
```
|
||||
|
||||
导入完成后,小程序/后台搜索支行即可自动匹配联行号;仍搜不到时可手动填写 12 位联行号。
|
||||
|
||||
## 微信发货同步配合
|
||||
|
||||
支付成功通知会优先落库微信 `transaction_id`(`wx_response.transaction_id` / `out_trans_id`),供 `shopro_wechat_trade` 插件调用 `upload_shipping_info`。汇付流水仍保存在 `shopro_huifu_pay_ext.hf_seq_id`。
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro_huifu\command;
|
||||
|
||||
use addons\shopro_huifu\library\onboard\HuifuOnboardBankBranchImporter;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\Db;
|
||||
|
||||
class ImportBankBranch extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('shopro_huifu:import-bank-branch')
|
||||
->setDescription('Import nationwide CNAPS bank branch codes into shopro_huifu_bank_branch')
|
||||
->addOption('file', 'f', Option::VALUE_OPTIONAL, 'CSV/JSON source file path')
|
||||
->addOption('download', 'd', Option::VALUE_NONE, 'Download gweesin/CNAPS cnaps.csv then import')
|
||||
->addOption('seed', null, Option::VALUE_NONE, 'Import bundled data/bank_branches.json seed only')
|
||||
->addOption('truncate', 't', Option::VALUE_NONE, 'Clear table before import')
|
||||
->addOption('limit', 'l', Option::VALUE_OPTIONAL, 'Max rows to import (0=all)', 0)
|
||||
->addOption('batch', 'b', Option::VALUE_OPTIONAL, 'Batch size', 500)
|
||||
->addOption('format', null, Option::VALUE_OPTIONAL, 'Force format: csv|json|auto', 'auto')
|
||||
->addOption('dry-run', null, Option::VALUE_NONE, 'Parse only, do not write DB')
|
||||
->addOption('export-json', null, Option::VALUE_OPTIONAL, 'Export DB rows to JSON file path')
|
||||
->addOption('export-limit', null, Option::VALUE_OPTIONAL, 'Max rows when exporting JSON', 0);
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$exportJson = trim((string) $input->getOption('export-json'));
|
||||
if ($exportJson !== '') {
|
||||
return $this->exportJson($output, $exportJson, (int) $input->getOption('export-limit'));
|
||||
}
|
||||
|
||||
try {
|
||||
Db::execute('SELECT 1 FROM `' . config('database.prefix') . 'shopro_huifu_bank_branch` LIMIT 1');
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('表 shopro_huifu_bank_branch 不存在,请先执行 php think shopro_huifu:repair-menu');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$file = trim((string) $input->getOption('file'));
|
||||
$download = (bool) $input->getOption('download');
|
||||
$seed = (bool) $input->getOption('seed');
|
||||
|
||||
if ($seed) {
|
||||
$file = ADDON_PATH . 'shopro_huifu/data/bank_branches.json';
|
||||
} elseif ($download) {
|
||||
$output->writeln('正在下载全国联行号 CSV(约 20MB,来源 gweesin/CNAPS)...');
|
||||
try {
|
||||
$file = HuifuOnboardBankBranchImporter::downloadCnapsCsv();
|
||||
$output->writeln('已保存: ' . $file);
|
||||
} catch (\Throwable $e) {
|
||||
$output->error($e->getMessage());
|
||||
$output->writeln('手动下载: https://hf-mirror.com/datasets/gweesin/CNAPS/resolve/main/cnaps.csv');
|
||||
$output->writeln('然后执行: php think shopro_huifu:import-bank-branch --file=addons/shopro_huifu/data/cnaps.csv');
|
||||
return 1;
|
||||
}
|
||||
} elseif ($file === '') {
|
||||
$defaultCsv = ADDON_PATH . 'shopro_huifu/data/cnaps.csv';
|
||||
if (is_file($defaultCsv)) {
|
||||
$file = $defaultCsv;
|
||||
} else {
|
||||
$output->error('请指定 --file、--download 或 --seed');
|
||||
$output->writeln('示例:');
|
||||
$output->writeln(' php think shopro_huifu:import-bank-branch --seed');
|
||||
$output->writeln(' php think shopro_huifu:import-bank-branch --download');
|
||||
$output->writeln(' php think shopro_huifu:import-bank-branch --file=addons/shopro_huifu/data/cnaps.csv');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
$options = [
|
||||
'truncate' => (bool) $input->getOption('truncate'),
|
||||
'limit' => (int) $input->getOption('limit'),
|
||||
'batch' => (int) $input->getOption('batch'),
|
||||
'format' => (string) $input->getOption('format'),
|
||||
'dry_run' => (bool) $input->getOption('dry-run'),
|
||||
];
|
||||
|
||||
$output->writeln('导入文件: ' . $file);
|
||||
if ($options['dry_run']) {
|
||||
$output->writeln('[dry-run] 仅解析,不写入数据库');
|
||||
}
|
||||
if ($options['truncate']) {
|
||||
$output->writeln('[truncate] 导入前清空旧数据');
|
||||
}
|
||||
|
||||
try {
|
||||
$stats = HuifuOnboardBankBranchImporter::importFile($file, $options);
|
||||
} catch (\Throwable $e) {
|
||||
$output->error($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln(sprintf(
|
||||
'完成: 新增 %d,更新 %d,跳过 %d,有效 %d',
|
||||
$stats['inserted'],
|
||||
$stats['updated'],
|
||||
$stats['skipped'],
|
||||
$stats['total']
|
||||
));
|
||||
|
||||
$count = Db::name('shopro_huifu_bank_branch')->where('status', 1)->count();
|
||||
$output->writeln('当前库内有效支行: ' . $count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function exportJson(Output $output, $dest, $limit)
|
||||
{
|
||||
try {
|
||||
$count = HuifuOnboardBankBranchImporter::exportJson($dest, $limit);
|
||||
} catch (\Throwable $e) {
|
||||
$output->error($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
$output->writeln('已导出 ' . $count . ' 条到 ' . $dest);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
2
addons/shopro_huifu/data/.gitignore
vendored
普通文件
2
addons/shopro_huifu/data/.gitignore
vendored
普通文件
@@ -0,0 +1,2 @@
|
||||
cnaps.csv
|
||||
cnaps_sample.csv
|
||||
@@ -23,5 +23,99 @@
|
||||
{"branch_code":"105582000010","branch_name":"中国建设银行股份有限公司韶关市分行","bank_name":"建设银行","prov_id":"440000","area_id":"440200"},
|
||||
{"branch_code":"105582000315","branch_name":"中国建设银行股份有限公司韶关曲江支行","bank_name":"建设银行","prov_id":"440000","area_id":"440200"},
|
||||
{"branch_code":"105582000028","branch_name":"中国建设银行股份有限公司韶关武江支行","bank_name":"建设银行","prov_id":"440000","area_id":"440200"},
|
||||
{"branch_code":"105582000132","branch_name":"中国建设银行股份有限公司韶关浈江支行","bank_name":"建设银行","prov_id":"440000","area_id":"440200"}
|
||||
{"branch_code":"105582000132","branch_name":"中国建设银行股份有限公司韶关浈江支行","bank_name":"建设银行","prov_id":"440000","area_id":"440200"},
|
||||
{"branch_code":"102100000030","branch_name":"中国工商银行股份有限公司北京市分行营业部","bank_name":"工商银行","prov_id":"110000","area_id":"110100"},
|
||||
{"branch_code":"102100000056","branch_name":"中国工商银行股份有限公司北京崇文门外大街支行","bank_name":"工商银行","prov_id":"110000","area_id":"110100"},
|
||||
{"branch_code":"102100000072","branch_name":"中国工商银行股份有限公司北京王府井金街支行","bank_name":"工商银行","prov_id":"110000","area_id":"110100"},
|
||||
{"branch_code":"103100000026","branch_name":"中国农业银行股份有限公司北京市分行营业部","bank_name":"农业银行","prov_id":"110000","area_id":"110100"},
|
||||
{"branch_code":"104100000004","branch_name":"中国银行股份有限公司北京市分行","bank_name":"中国银行","prov_id":"110000","area_id":"110100"},
|
||||
{"branch_code":"105100000017","branch_name":"中国建设银行股份有限公司北京市分行","bank_name":"建设银行","prov_id":"110000","area_id":"110100"},
|
||||
{"branch_code":"301100000013","branch_name":"交通银行股份有限公司北京市分行","bank_name":"交通银行","prov_id":"110000","area_id":"110100"},
|
||||
{"branch_code":"308100000013","branch_name":"招商银行股份有限公司北京分行","bank_name":"招商银行","prov_id":"110000","area_id":"110100"},
|
||||
{"branch_code":"102290000207","branch_name":"中国工商银行股份有限公司上海市中山北路支行","bank_name":"工商银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"103290040169","branch_name":"中国农业银行股份有限公司上海市分行营业部","bank_name":"农业银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"103290076178","branch_name":"中国农业银行股份有限公司上海徐汇支行","bank_name":"农业银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"105290071050","branch_name":"中国建设银行股份有限公司上海市分行","bank_name":"建设银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"105290071113","branch_name":"中国建设银行股份有限公司上海市中支行","bank_name":"建设银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"105290075067","branch_name":"中国建设银行股份有限公司上海五角场支行","bank_name":"建设银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"301290000013","branch_name":"交通银行股份有限公司上海市分行","bank_name":"交通银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"308290000013","branch_name":"招商银行股份有限公司上海分行","bank_name":"招商银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"305290002096","branch_name":"中国民生银行股份有限公司上海分行","bank_name":"民生银行","prov_id":"310000","area_id":"310100"},
|
||||
{"branch_code":"102581000018","branch_name":"中国工商银行股份有限公司广州第一支行","bank_name":"工商银行","prov_id":"440000","area_id":"440100"},
|
||||
{"branch_code":"102581000026","branch_name":"中国工商银行股份有限公司广州第二支行","bank_name":"工商银行","prov_id":"440000","area_id":"440100"},
|
||||
{"branch_code":"103581000013","branch_name":"中国农业银行股份有限公司广东省分行营业部","bank_name":"农业银行","prov_id":"440000","area_id":"440100"},
|
||||
{"branch_code":"104581000004","branch_name":"中国银行股份有限公司广东省分行","bank_name":"中国银行","prov_id":"440000","area_id":"440100"},
|
||||
{"branch_code":"105581000013","branch_name":"中国建设银行股份有限公司广东省分行","bank_name":"建设银行","prov_id":"440000","area_id":"440100"},
|
||||
{"branch_code":"301581000013","branch_name":"交通银行股份有限公司广东省分行","bank_name":"交通银行","prov_id":"440000","area_id":"440100"},
|
||||
{"branch_code":"308584000013","branch_name":"招商银行股份有限公司广州分行","bank_name":"招商银行","prov_id":"440000","area_id":"440100"},
|
||||
{"branch_code":"102584000018","branch_name":"中国工商银行股份有限公司深圳分行","bank_name":"工商银行","prov_id":"440000","area_id":"440300"},
|
||||
{"branch_code":"103584000013","branch_name":"中国农业银行股份有限公司深圳市分行","bank_name":"农业银行","prov_id":"440000","area_id":"440300"},
|
||||
{"branch_code":"104584000004","branch_name":"中国银行股份有限公司深圳市分行","bank_name":"中国银行","prov_id":"440000","area_id":"440300"},
|
||||
{"branch_code":"105584000013","branch_name":"中国建设银行股份有限公司深圳市分行","bank_name":"建设银行","prov_id":"440000","area_id":"440300"},
|
||||
{"branch_code":"308584001013","branch_name":"招商银行股份有限公司深圳分行","bank_name":"招商银行","prov_id":"440000","area_id":"440300"},
|
||||
{"branch_code":"102331000018","branch_name":"中国工商银行股份有限公司杭州分行","bank_name":"工商银行","prov_id":"330000","area_id":"330100"},
|
||||
{"branch_code":"103331000013","branch_name":"中国农业银行股份有限公司浙江省分行营业部","bank_name":"农业银行","prov_id":"330000","area_id":"330100"},
|
||||
{"branch_code":"105331000013","branch_name":"中国建设银行股份有限公司浙江省分行","bank_name":"建设银行","prov_id":"330000","area_id":"330100"},
|
||||
{"branch_code":"308331000013","branch_name":"招商银行股份有限公司杭州分行","bank_name":"招商银行","prov_id":"330000","area_id":"330100"},
|
||||
{"branch_code":"102651000018","branch_name":"中国工商银行股份有限公司成都分行","bank_name":"工商银行","prov_id":"510000","area_id":"510100"},
|
||||
{"branch_code":"103651000013","branch_name":"中国农业银行股份有限公司四川省分行营业部","bank_name":"农业银行","prov_id":"510000","area_id":"510100"},
|
||||
{"branch_code":"105651000013","branch_name":"中国建设银行股份有限公司四川省分行","bank_name":"建设银行","prov_id":"510000","area_id":"510100"},
|
||||
{"branch_code":"308651000013","branch_name":"招商银行股份有限公司成都分行","bank_name":"招商银行","prov_id":"510000","area_id":"510100"},
|
||||
{"branch_code":"102521000018","branch_name":"中国工商银行股份有限公司武汉分行","bank_name":"工商银行","prov_id":"420000","area_id":"420100"},
|
||||
{"branch_code":"103521000013","branch_name":"中国农业银行股份有限公司湖北省分行营业部","bank_name":"农业银行","prov_id":"420000","area_id":"420100"},
|
||||
{"branch_code":"105521000013","branch_name":"中国建设银行股份有限公司湖北省分行","bank_name":"建设银行","prov_id":"420000","area_id":"420100"},
|
||||
{"branch_code":"102301000018","branch_name":"中国工商银行股份有限公司南京分行","bank_name":"工商银行","prov_id":"320000","area_id":"320100"},
|
||||
{"branch_code":"103301000013","branch_name":"中国农业银行股份有限公司江苏省分行营业部","bank_name":"农业银行","prov_id":"320000","area_id":"320100"},
|
||||
{"branch_code":"105301000013","branch_name":"中国建设银行股份有限公司江苏省分行","bank_name":"建设银行","prov_id":"320000","area_id":"320100"},
|
||||
{"branch_code":"102791000018","branch_name":"中国工商银行股份有限公司西安分行","bank_name":"工商银行","prov_id":"610000","area_id":"610100"},
|
||||
{"branch_code":"103791000013","branch_name":"中国农业银行股份有限公司陕西省分行营业部","bank_name":"农业银行","prov_id":"610000","area_id":"610100"},
|
||||
{"branch_code":"105791000013","branch_name":"中国建设银行股份有限公司陕西省分行","bank_name":"建设银行","prov_id":"610000","area_id":"610100"},
|
||||
{"branch_code":"102653000018","branch_name":"中国工商银行股份有限公司重庆分行","bank_name":"工商银行","prov_id":"500000","area_id":"500100"},
|
||||
{"branch_code":"103653000013","branch_name":"中国农业银行股份有限公司重庆市分行","bank_name":"农业银行","prov_id":"500000","area_id":"500100"},
|
||||
{"branch_code":"105653000013","branch_name":"中国建设银行股份有限公司重庆市分行","bank_name":"建设银行","prov_id":"500000","area_id":"500100"},
|
||||
{"branch_code":"102121000018","branch_name":"中国工商银行股份有限公司天津分行","bank_name":"工商银行","prov_id":"120000","area_id":"120100"},
|
||||
{"branch_code":"103121000013","branch_name":"中国农业银行股份有限公司天津市分行","bank_name":"农业银行","prov_id":"120000","area_id":"120100"},
|
||||
{"branch_code":"105121000013","branch_name":"中国建设银行股份有限公司天津市分行","bank_name":"建设银行","prov_id":"120000","area_id":"120100"},
|
||||
{"branch_code":"102451000018","branch_name":"中国工商银行股份有限公司济南分行","bank_name":"工商银行","prov_id":"370000","area_id":"370100"},
|
||||
{"branch_code":"103451000013","branch_name":"中国农业银行股份有限公司山东省分行营业部","bank_name":"农业银行","prov_id":"370000","area_id":"370100"},
|
||||
{"branch_code":"105451000013","branch_name":"中国建设银行股份有限公司山东省分行","bank_name":"建设银行","prov_id":"370000","area_id":"370100"},
|
||||
{"branch_code":"102491000018","branch_name":"中国工商银行股份有限公司郑州分行","bank_name":"工商银行","prov_id":"410000","area_id":"410100"},
|
||||
{"branch_code":"103491000013","branch_name":"中国农业银行股份有限公司河南省分行营业部","bank_name":"农业银行","prov_id":"410000","area_id":"410100"},
|
||||
{"branch_code":"105491000013","branch_name":"中国建设银行股份有限公司河南省分行","bank_name":"建设银行","prov_id":"410000","area_id":"410100"},
|
||||
{"branch_code":"102551000018","branch_name":"中国工商银行股份有限公司长沙分行","bank_name":"工商银行","prov_id":"430000","area_id":"430100"},
|
||||
{"branch_code":"103551000013","branch_name":"中国农业银行股份有限公司湖南省分行营业部","bank_name":"农业银行","prov_id":"430000","area_id":"430100"},
|
||||
{"branch_code":"105551000013","branch_name":"中国建设银行股份有限公司湖南省分行","bank_name":"建设银行","prov_id":"430000","area_id":"430100"},
|
||||
{"branch_code":"102611000018","branch_name":"中国工商银行股份有限公司福州分行","bank_name":"工商银行","prov_id":"350000","area_id":"350100"},
|
||||
{"branch_code":"103611000013","branch_name":"中国农业银行股份有限公司福建省分行营业部","bank_name":"农业银行","prov_id":"350000","area_id":"350100"},
|
||||
{"branch_code":"105611000013","branch_name":"中国建设银行股份有限公司福建省分行","bank_name":"建设银行","prov_id":"350000","area_id":"350100"},
|
||||
{"branch_code":"102701000018","branch_name":"中国工商银行股份有限公司昆明分行","bank_name":"工商银行","prov_id":"530000","area_id":"530100"},
|
||||
{"branch_code":"103701000013","branch_name":"中国农业银行股份有限公司云南省分行营业部","bank_name":"农业银行","prov_id":"530000","area_id":"530100"},
|
||||
{"branch_code":"105701000013","branch_name":"中国建设银行股份有限公司云南省分行","bank_name":"建设银行","prov_id":"530000","area_id":"530100"},
|
||||
{"branch_code":"102221000018","branch_name":"中国工商银行股份有限公司沈阳分行","bank_name":"工商银行","prov_id":"210000","area_id":"210100"},
|
||||
{"branch_code":"103221000013","branch_name":"中国农业银行股份有限公司辽宁省分行营业部","bank_name":"农业银行","prov_id":"210000","area_id":"210100"},
|
||||
{"branch_code":"105221000013","branch_name":"中国建设银行股份有限公司辽宁省分行","bank_name":"建设银行","prov_id":"210000","area_id":"210100"},
|
||||
{"branch_code":"102241000018","branch_name":"中国工商银行股份有限公司长春分行","bank_name":"工商银行","prov_id":"220000","area_id":"220100"},
|
||||
{"branch_code":"103241000013","branch_name":"中国农业银行股份有限公司吉林省分行营业部","bank_name":"农业银行","prov_id":"220000","area_id":"220100"},
|
||||
{"branch_code":"105241000013","branch_name":"中国建设银行股份有限公司吉林省分行","bank_name":"建设银行","prov_id":"220000","area_id":"220100"},
|
||||
{"branch_code":"102261000018","branch_name":"中国工商银行股份有限公司哈尔滨分行","bank_name":"工商银行","prov_id":"230000","area_id":"230100"},
|
||||
{"branch_code":"103261000013","branch_name":"中国农业银行股份有限公司黑龙江省分行营业部","bank_name":"农业银行","prov_id":"230000","area_id":"230100"},
|
||||
{"branch_code":"105261000013","branch_name":"中国建设银行股份有限公司黑龙江省分行","bank_name":"建设银行","prov_id":"230000","area_id":"230100"},
|
||||
{"branch_code":"102361000018","branch_name":"中国工商银行股份有限公司合肥分行","bank_name":"工商银行","prov_id":"340000","area_id":"340100"},
|
||||
{"branch_code":"103361000013","branch_name":"中国农业银行股份有限公司安徽省分行营业部","bank_name":"农业银行","prov_id":"340000","area_id":"340100"},
|
||||
{"branch_code":"105361000013","branch_name":"中国建设银行股份有限公司安徽省分行","bank_name":"建设银行","prov_id":"340000","area_id":"340100"},
|
||||
{"branch_code":"102421000018","branch_name":"中国工商银行股份有限公司南昌分行","bank_name":"工商银行","prov_id":"360000","area_id":"360100"},
|
||||
{"branch_code":"103421000013","branch_name":"中国农业银行股份有限公司江西省分行营业部","bank_name":"农业银行","prov_id":"360000","area_id":"360100"},
|
||||
{"branch_code":"105421000013","branch_name":"中国建设银行股份有限公司江西省分行","bank_name":"建设银行","prov_id":"360000","area_id":"360100"},
|
||||
{"branch_code":"102851000018","branch_name":"中国工商银行股份有限公司南宁分行","bank_name":"工商银行","prov_id":"450000","area_id":"450100"},
|
||||
{"branch_code":"103851000013","branch_name":"中国农业银行股份有限公司广西壮族自治区分行营业部","bank_name":"农业银行","prov_id":"450000","area_id":"450100"},
|
||||
{"branch_code":"105851000013","branch_name":"中国建设银行股份有限公司广西壮族自治区分行","bank_name":"建设银行","prov_id":"450000","area_id":"450100"},
|
||||
{"branch_code":"102821000018","branch_name":"中国工商银行股份有限公司海口分行","bank_name":"工商银行","prov_id":"460000","area_id":"460100"},
|
||||
{"branch_code":"103821000013","branch_name":"中国农业银行股份有限公司海南省分行","bank_name":"农业银行","prov_id":"460000","area_id":"460100"},
|
||||
{"branch_code":"105821000013","branch_name":"中国建设银行股份有限公司海南省分行","bank_name":"建设银行","prov_id":"460000","area_id":"460100"},
|
||||
{"branch_code":"102871000018","branch_name":"中国工商银行股份有限公司兰州分行","bank_name":"工商银行","prov_id":"620000","area_id":"620100"},
|
||||
{"branch_code":"103871000013","branch_name":"中国农业银行股份有限公司甘肃省分行营业部","bank_name":"农业银行","prov_id":"620000","area_id":"620100"},
|
||||
{"branch_code":"105871000013","branch_name":"中国建设银行股份有限公司甘肃省分行","bank_name":"建设银行","prov_id":"620000","area_id":"620100"},
|
||||
{"branch_code":"102881000018","branch_name":"中国工商银行股份有限公司乌鲁木齐分行","bank_name":"工商银行","prov_id":"650000","area_id":"650100"},
|
||||
{"branch_code":"103881000013","branch_name":"中国农业银行股份有限公司新疆维吾尔自治区分行营业部","bank_name":"农业银行","prov_id":"650000","area_id":"650100"},
|
||||
{"branch_code":"105881000013","branch_name":"中国建设银行股份有限公司新疆维吾尔自治区分行","bank_name":"建设银行","prov_id":"650000","area_id":"650100"}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro_huifu\library\onboard;
|
||||
|
||||
use think\Db;
|
||||
|
||||
class HuifuOnboardBankBranchImporter
|
||||
{
|
||||
protected static $areaNameMap;
|
||||
|
||||
/**
|
||||
* @return array{inserted:int,updated:int,skipped:int,total:int}
|
||||
*/
|
||||
public static function importFile($file, array $options = [])
|
||||
{
|
||||
$file = (string) $file;
|
||||
if (!is_file($file)) {
|
||||
throw new \InvalidArgumentException('文件不存在: ' . $file);
|
||||
}
|
||||
|
||||
$format = strtolower((string) ($options['format'] ?? 'auto'));
|
||||
if ($format === 'auto') {
|
||||
$format = self::detectFormat($file);
|
||||
}
|
||||
|
||||
$limit = max(0, (int) ($options['limit'] ?? 0));
|
||||
$batch = max(50, min(2000, (int) ($options['batch'] ?? 500)));
|
||||
$dryRun = !empty($options['dry_run']);
|
||||
$truncate = !empty($options['truncate']);
|
||||
|
||||
if ($truncate && !$dryRun) {
|
||||
Db::name('shopro_huifu_bank_branch')->where('id', '>', 0)->delete();
|
||||
}
|
||||
|
||||
$rows = $format === 'json'
|
||||
? self::parseJsonFile($file, $limit)
|
||||
: self::parseCsvFile($file, $limit);
|
||||
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$buffer = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$normalized = self::normalizeRow($row);
|
||||
if (!$normalized) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
$buffer[] = $normalized;
|
||||
if (count($buffer) >= $batch) {
|
||||
$stats = self::flushBuffer($buffer, $dryRun);
|
||||
$inserted += $stats['inserted'];
|
||||
$updated += $stats['updated'];
|
||||
$buffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
if ($buffer) {
|
||||
$stats = self::flushBuffer($buffer, $dryRun);
|
||||
$inserted += $stats['inserted'];
|
||||
$updated += $stats['updated'];
|
||||
}
|
||||
|
||||
return [
|
||||
'inserted' => $inserted,
|
||||
'updated' => $updated,
|
||||
'skipped' => $skipped,
|
||||
'total' => $inserted + $updated,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{inserted:int,updated:int,skipped:int,total:int}
|
||||
*/
|
||||
public static function importSeedJson($jsonFile = null, array $options = [])
|
||||
{
|
||||
$jsonFile = $jsonFile ?: (ADDON_PATH . 'shopro_huifu/data/bank_branches.json');
|
||||
return self::importFile($jsonFile, array_merge(['format' => 'json'], $options));
|
||||
}
|
||||
|
||||
public static function downloadCnapsCsv($dest = null)
|
||||
{
|
||||
$dest = $dest ?: (ADDON_PATH . 'shopro_huifu/data/cnaps.csv');
|
||||
$dir = dirname($dest);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$urls = [
|
||||
'https://hf-mirror.com/datasets/gweesin/CNAPS/resolve/main/cnaps.csv',
|
||||
'https://huggingface.co/datasets/gweesin/CNAPS/resolve/main/cnaps.csv',
|
||||
];
|
||||
|
||||
$lastError = '';
|
||||
foreach ($urls as $url) {
|
||||
try {
|
||||
self::downloadToFile($url, $dest);
|
||||
if (is_file($dest) && filesize($dest) > 1024) {
|
||||
return $dest;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$lastError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException('下载全国联行号 CSV 失败,请手动下载后指定 --file。' . ($lastError ? ' ' . $lastError : ''));
|
||||
}
|
||||
|
||||
public static function exportJson($dest, $limit = 0)
|
||||
{
|
||||
$dest = (string) $dest;
|
||||
$limit = max(0, (int) $limit);
|
||||
$query = Db::name('shopro_huifu_bank_branch')->where('status', 1)->order('id', 'asc');
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
$list = $query->select();
|
||||
$rows = [];
|
||||
foreach ($list ?: [] as $item) {
|
||||
$row = [
|
||||
'branch_code' => (string) $item['branch_code'],
|
||||
'branch_name' => (string) $item['branch_name'],
|
||||
];
|
||||
if (!empty($item['bank_name'])) {
|
||||
$row['bank_name'] = (string) $item['bank_name'];
|
||||
}
|
||||
if (!empty($item['prov_id'])) {
|
||||
$row['prov_id'] = (string) $item['prov_id'];
|
||||
}
|
||||
if (!empty($item['area_id'])) {
|
||||
$row['area_id'] = (string) $item['area_id'];
|
||||
}
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
$json = json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
if ($json === false) {
|
||||
throw new \RuntimeException('导出 JSON 失败');
|
||||
}
|
||||
file_put_contents($dest, $json . "\n");
|
||||
return count($rows);
|
||||
}
|
||||
|
||||
protected static function detectFormat($file)
|
||||
{
|
||||
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
||||
if ($ext === 'json') {
|
||||
return 'json';
|
||||
}
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
protected static function parseJsonFile($file, $limit = 0)
|
||||
{
|
||||
$decoded = json_decode(file_get_contents($file), true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new \InvalidArgumentException('JSON 格式无效: ' . $file);
|
||||
}
|
||||
if ($limit > 0) {
|
||||
return array_slice($decoded, 0, $limit);
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
protected static function parseCsvFile($file, $limit = 0)
|
||||
{
|
||||
$handle = fopen($file, 'rb');
|
||||
if (!$handle) {
|
||||
throw new \RuntimeException('无法读取 CSV: ' . $file);
|
||||
}
|
||||
|
||||
$header = null;
|
||||
$rows = [];
|
||||
while (($line = fgetcsv($handle)) !== false) {
|
||||
if (!$line || self::isEmptyCsvLine($line)) {
|
||||
continue;
|
||||
}
|
||||
if ($header === null) {
|
||||
$header = self::normalizeHeader($line);
|
||||
continue;
|
||||
}
|
||||
$assoc = self::mapCsvLine($header, $line);
|
||||
if ($assoc) {
|
||||
$rows[] = $assoc;
|
||||
}
|
||||
if ($limit > 0 && count($rows) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
protected static function normalizeHeader(array $header)
|
||||
{
|
||||
$map = [];
|
||||
foreach ($header as $idx => $name) {
|
||||
$key = self::normalizeHeaderKey($name);
|
||||
if ($key !== '') {
|
||||
$map[$idx] = $key;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
protected static function normalizeHeaderKey($name)
|
||||
{
|
||||
$name = trim((string) $name);
|
||||
if ($name === '') {
|
||||
return '';
|
||||
}
|
||||
$lower = strtolower($name);
|
||||
$aliases = [
|
||||
'branch_code' => ['branch_code', 'bankid', 'cnaps', 'cnapscode', '联行号', '行号', '开户行联行号', '大额支付行号'],
|
||||
'branch_name' => ['branch_name', 'lname', 'banknamebranch', '支行名称', '开户行', '开户行支行名称', '支行全称', '网点名称'],
|
||||
'bank_name' => ['bank_name', 'bankname', 'banknamemain', '银行', '银行名称', '银行简称', '总行名称'],
|
||||
'province' => ['province', 'provincename', 'areaprovince', '省份', '省'],
|
||||
'city' => ['city', 'cityname', 'areacity', '城市', '市', '地区'],
|
||||
'prov_id' => ['prov_id', 'provincecode', '省码'],
|
||||
'area_id' => ['area_id', 'citycode', '市码'],
|
||||
];
|
||||
foreach ($aliases as $canonical => $list) {
|
||||
if (in_array($lower, $list, true) || in_array($name, $list, true)) {
|
||||
return $canonical;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
protected static function mapCsvLine(array $header, array $line)
|
||||
{
|
||||
$row = [];
|
||||
foreach ($header as $idx => $key) {
|
||||
if (!array_key_exists($idx, $line)) {
|
||||
continue;
|
||||
}
|
||||
$row[$key] = trim((string) $line[$idx]);
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
protected static function isEmptyCsvLine(array $line)
|
||||
{
|
||||
foreach ($line as $cell) {
|
||||
if (trim((string) $cell) !== '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function normalizeRow(array $row)
|
||||
{
|
||||
$branchCode = self::normalizeBranchCode(
|
||||
$row['branch_code'] ?? ($row['BankId'] ?? ($row['cnapsCode'] ?? ''))
|
||||
);
|
||||
$branchName = trim((string) ($row['branch_name'] ?? ($row['LName'] ?? '')));
|
||||
if ($branchCode === '' || $branchName === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bankName = trim((string) ($row['bank_name'] ?? ($row['BankName'] ?? '')));
|
||||
if ($bankName === '') {
|
||||
$bankName = self::guessBankName($branchName);
|
||||
}
|
||||
|
||||
$provId = HuifuOnboardAreaHelper::toCode($row['prov_id'] ?? '');
|
||||
$areaId = HuifuOnboardAreaHelper::toCode($row['area_id'] ?? '');
|
||||
|
||||
$province = trim((string) ($row['province'] ?? ($row['ProvinceName'] ?? '')));
|
||||
$city = trim((string) ($row['city'] ?? ($row['CityName'] ?? '')));
|
||||
|
||||
if ($provId === '' && !empty($row['ProvinceCode'])) {
|
||||
$provId = self::provinceCodeToAdmin((string) $row['ProvinceCode']);
|
||||
}
|
||||
|
||||
if ($provId === '' && $province !== '') {
|
||||
$provId = self::lookupAreaId($province, 1);
|
||||
}
|
||||
if ($areaId === '' && $city !== '') {
|
||||
$areaId = self::lookupAreaId($city, 2, $provId);
|
||||
}
|
||||
if ($provId === '' && $areaId !== '') {
|
||||
$provId = substr($areaId, 0, 2) . '0000';
|
||||
}
|
||||
|
||||
return [
|
||||
'branch_code' => $branchCode,
|
||||
'branch_name' => $branchName,
|
||||
'bank_name' => $bankName,
|
||||
'prov_id' => $provId,
|
||||
'area_id' => $areaId,
|
||||
'status' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
protected static function normalizeBranchCode($code)
|
||||
{
|
||||
$code = preg_replace('/\D+/', '', (string) $code);
|
||||
if ($code === '') {
|
||||
return '';
|
||||
}
|
||||
if (strlen($code) < 8 || strlen($code) > 20) {
|
||||
return '';
|
||||
}
|
||||
if (strlen($code) < 12) {
|
||||
$code = str_pad($code, 12, '0', STR_PAD_LEFT);
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
protected static function provinceCodeToAdmin($code)
|
||||
{
|
||||
$code = preg_replace('/\D+/', '', (string) $code);
|
||||
if ($code === '') {
|
||||
return '';
|
||||
}
|
||||
if (strlen($code) >= 6) {
|
||||
return substr($code, 0, 2) . '0000';
|
||||
}
|
||||
return str_pad($code, 2, '0', STR_PAD_LEFT) . '0000';
|
||||
}
|
||||
|
||||
protected static function guessBankName($branchName)
|
||||
{
|
||||
$rules = [
|
||||
'工商银行' => '工商银行',
|
||||
'农业银行' => '农业银行',
|
||||
'中国银行' => '中国银行',
|
||||
'建设银行' => '建设银行',
|
||||
'交通银行' => '交通银行',
|
||||
'招商银行' => '招商银行',
|
||||
'浦发银行' => '浦发银行',
|
||||
'民生银行' => '民生银行',
|
||||
'光大银行' => '光大银行',
|
||||
'兴业银行' => '兴业银行',
|
||||
'平安银行' => '平安银行',
|
||||
'中信银行' => '中信银行',
|
||||
'华夏银行' => '华夏银行',
|
||||
'广发银行' => '广发银行',
|
||||
'邮储银行' => '邮储银行',
|
||||
'邮政储蓄' => '邮储银行',
|
||||
];
|
||||
foreach ($rules as $needle => $short) {
|
||||
if (mb_strpos($branchName, $needle, 0, 'UTF-8') !== false) {
|
||||
return $short;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
protected static function lookupAreaId($name, $level, $provId = '')
|
||||
{
|
||||
self::loadAreaNameMap();
|
||||
$key = self::areaCacheKey($name, $level, $provId);
|
||||
if (isset(self::$areaNameMap[$key])) {
|
||||
return (string) self::$areaNameMap[$key];
|
||||
}
|
||||
|
||||
$candidates = self::areaNameCandidates($name);
|
||||
foreach ($candidates as $candidate) {
|
||||
$tryKey = self::areaCacheKey($candidate, $level, $provId);
|
||||
if (isset(self::$areaNameMap[$tryKey])) {
|
||||
return (string) self::$areaNameMap[$tryKey];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
protected static function loadAreaNameMap()
|
||||
{
|
||||
if (self::$areaNameMap !== null) {
|
||||
return;
|
||||
}
|
||||
self::$areaNameMap = [];
|
||||
try {
|
||||
$rows = Db::name('shopro_area')->where('level', 'in', [1, 2])->field('id,name,pid,level')->select();
|
||||
} catch (\Throwable $e) {
|
||||
return;
|
||||
}
|
||||
foreach ($rows ?: [] as $row) {
|
||||
$id = (string) $row['id'];
|
||||
$level = (int) $row['level'];
|
||||
$pid = (string) ($row['pid'] ?? '');
|
||||
$provId = $level === 2 ? (substr($id, 0, 2) . '0000') : '';
|
||||
foreach (self::areaNameCandidates($row['name']) as $candidate) {
|
||||
$key = self::areaCacheKey($candidate, $level, $level === 2 ? $provId : '');
|
||||
if (!isset(self::$areaNameMap[$key])) {
|
||||
self::$areaNameMap[$key] = $id;
|
||||
}
|
||||
if ($level === 2 && $pid !== '' && $pid !== '0') {
|
||||
$parentKey = self::areaCacheKey($candidate, $level, (string) $pid);
|
||||
if (!isset(self::$areaNameMap[$parentKey])) {
|
||||
self::$areaNameMap[$parentKey] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static function areaNameCandidates($name)
|
||||
{
|
||||
$name = trim((string) $name);
|
||||
if ($name === '') {
|
||||
return [];
|
||||
}
|
||||
$list = [$name];
|
||||
$stripped = preg_replace('/(省|市|自治区|壮族|回族|维吾尔|特别行政区)$/u', '', $name);
|
||||
if ($stripped && $stripped !== $name) {
|
||||
$list[] = $stripped;
|
||||
}
|
||||
return array_values(array_unique($list));
|
||||
}
|
||||
|
||||
protected static function areaCacheKey($name, $level, $provId = '')
|
||||
{
|
||||
return strtolower($level . '|' . HuifuOnboardAreaHelper::toCode($provId) . '|' . $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array{inserted:int,updated:int}
|
||||
*/
|
||||
protected static function flushBuffer(array $rows, $dryRun = false)
|
||||
{
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
if ($dryRun) {
|
||||
return ['inserted' => count($rows), 'updated' => 0];
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$exists = Db::name('shopro_huifu_bank_branch')->where('branch_code', $row['branch_code'])->find();
|
||||
if ($exists) {
|
||||
Db::name('shopro_huifu_bank_branch')->where('id', $exists['id'])->update([
|
||||
'branch_name' => $row['branch_name'],
|
||||
'bank_name' => $row['bank_name'],
|
||||
'prov_id' => $row['prov_id'],
|
||||
'area_id' => $row['area_id'],
|
||||
'status' => 1,
|
||||
]);
|
||||
$updated++;
|
||||
continue;
|
||||
}
|
||||
Db::name('shopro_huifu_bank_branch')->insert($row);
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
return ['inserted' => $inserted, 'updated' => $updated];
|
||||
}
|
||||
|
||||
protected static function downloadToFile($url, $dest)
|
||||
{
|
||||
if (function_exists('curl_init')) {
|
||||
$fp = fopen($dest, 'wb');
|
||||
if (!$fp) {
|
||||
throw new \RuntimeException('无法写入: ' . $dest);
|
||||
}
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_FILE => $fp,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 600,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_USERAGENT => 'shopro_huifu-importer/1.0',
|
||||
]);
|
||||
$ok = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
fclose($fp);
|
||||
if (!$ok || $code >= 400) {
|
||||
@unlink($dest);
|
||||
throw new \RuntimeException('HTTP ' . $code . ($err ? ': ' . $err : ''));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 600,
|
||||
'header' => "User-Agent: shopro_huifu-importer/1.0\r\n",
|
||||
],
|
||||
]);
|
||||
$content = @file_get_contents($url, false, $ctx);
|
||||
if ($content === false || $content === '') {
|
||||
throw new \RuntimeException('下载失败: ' . $url);
|
||||
}
|
||||
file_put_contents($dest, $content);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
'addons\shopro_huifu\command\TestPayConnection',
|
||||
'addons\shopro_huifu\command\ProbeWechatOnboarding',
|
||||
'addons\shopro_huifu\command\OnboardPoll',
|
||||
'addons\shopro_huifu\command\ImportBankBranch',
|
||||
'addons\shopro_wechat_trade\command\InitOrderPath',
|
||||
'addons\shopro_wechat_trade\command\ProbeOrderPath',
|
||||
'addons\shopro_wechat_trade\command\BackfillShipping',
|
||||
|
||||
在新工单中引用
屏蔽一个用户