diff --git a/addons/shopro_huifu/README.md b/addons/shopro_huifu/README.md index a03bdd9..cea0a51 100644 --- a/addons/shopro_huifu/README.md +++ b/addons/shopro_huifu/README.md @@ -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`。 diff --git a/addons/shopro_huifu/command/ImportBankBranch.php b/addons/shopro_huifu/command/ImportBankBranch.php new file mode 100644 index 0000000..ec9b3e0 --- /dev/null +++ b/addons/shopro_huifu/command/ImportBankBranch.php @@ -0,0 +1,123 @@ +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; + } +} diff --git a/addons/shopro_huifu/data/.gitignore b/addons/shopro_huifu/data/.gitignore new file mode 100644 index 0000000..3135359 --- /dev/null +++ b/addons/shopro_huifu/data/.gitignore @@ -0,0 +1,2 @@ +cnaps.csv +cnaps_sample.csv diff --git a/addons/shopro_huifu/data/bank_branches.json b/addons/shopro_huifu/data/bank_branches.json index 8115aa3..a4cdc77 100644 --- a/addons/shopro_huifu/data/bank_branches.json +++ b/addons/shopro_huifu/data/bank_branches.json @@ -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"} ] diff --git a/addons/shopro_huifu/library/onboard/HuifuOnboardBankBranchImporter.php b/addons/shopro_huifu/library/onboard/HuifuOnboardBankBranchImporter.php new file mode 100644 index 0000000..4ddd77d --- /dev/null +++ b/addons/shopro_huifu/library/onboard/HuifuOnboardBankBranchImporter.php @@ -0,0 +1,492 @@ +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> $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); + } +} diff --git a/application/command.php b/application/command.php index 9d6fccd..1c20bb2 100644 --- a/application/command.php +++ b/application/command.php @@ -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',