diff --git a/addons/shopro_huifu/application/admin/view/shopro_huifu/onboard/add.html b/addons/shopro_huifu/application/admin/view/shopro_huifu/onboard/add.html index b8b246b..fa360c8 100644 --- a/addons/shopro_huifu/application/admin/view/shopro_huifu/onboard/add.html +++ b/addons/shopro_huifu/application/admin/view/shopro_huifu/onboard/add.html @@ -389,17 +389,23 @@ - - 须从搜索结果中点击选择支行,仅输入文字无法保存 + + 搜索支行名称,系统将自动匹配联行号(与小程序一致);也可从下方结果点击选择 + 本地库未搜到,保存时服务端会再次自动匹配;仍失败请手动填写联行号 已选:{{ form.branch_name }}(联行号 {{ form.branch_code }}) {{ item.branch_name }} - {{ item.branch_code }} + {{ item.branch_code }} · {{ item.area_id }} + + + + + diff --git a/addons/shopro_huifu/data/bank_branches.json b/addons/shopro_huifu/data/bank_branches.json index eff4cab..8115aa3 100644 --- a/addons/shopro_huifu/data/bank_branches.json +++ b/addons/shopro_huifu/data/bank_branches.json @@ -18,5 +18,10 @@ {"branch_code":"103124075623","branch_name":"中国农业银行股份有限公司铜仁分行","bank_name":"农业银行","prov_id":"520000","area_id":"520600"}, {"branch_code":"103124075624","branch_name":"中国农业银行股份有限公司黔西南分行","bank_name":"农业银行","prov_id":"520000","area_id":"522300"}, {"branch_code":"103124075625","branch_name":"中国农业银行股份有限公司黔东南分行","bank_name":"农业银行","prov_id":"520000","area_id":"522600"}, - {"branch_code":"103124075626","branch_name":"中国农业银行股份有限公司黔南分行","bank_name":"农业银行","prov_id":"520000","area_id":"522700"} + {"branch_code":"103124075626","branch_name":"中国农业银行股份有限公司黔南分行","bank_name":"农业银行","prov_id":"520000","area_id":"522700"}, + {"branch_code":"105603000311","branch_name":"中国建设银行股份有限公司中山高科技支行","bank_name":"建设银行","prov_id":"440000","area_id":"442000"}, + {"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"} ] diff --git a/addons/shopro_huifu/library/onboard/HuifuOnboardBankBranch.php b/addons/shopro_huifu/library/onboard/HuifuOnboardBankBranch.php index dc396b5..e76c8da 100644 --- a/addons/shopro_huifu/library/onboard/HuifuOnboardBankBranch.php +++ b/addons/shopro_huifu/library/onboard/HuifuOnboardBankBranch.php @@ -19,13 +19,29 @@ class HuifuOnboardBankBranch } $limit = max(1, min(50, (int) $limit)); $cityCode = HuifuOnboardAreaHelper::toCode($cityCode); + $provCode = self::provinceFromCity($cityCode); - $rows = self::searchDatabase($keyword, $cityCode, $limit); + $rows = self::collectSearch($keyword, $cityCode, $provCode, $limit); + if ($rows) { + return $rows; + } + if ($cityCode !== '') { + $rows = self::collectSearch($keyword, '', $provCode, $limit); + if ($rows) { + return $rows; + } + } + return self::collectSearch($keyword, '', '', $limit); + } + + protected static function collectSearch($keyword, $cityCode, $provCode, $limit) + { + $rows = self::searchDatabase($keyword, $cityCode, $provCode, $limit); if (count($rows) >= $limit) { return $rows; } - $jsonRows = self::searchJson($keyword, $cityCode, $limit - count($rows)); + $jsonRows = self::searchJson($keyword, $cityCode, $provCode, $limit - count($rows)); $existing = array_column($rows, 'branch_code'); foreach ($jsonRows as $row) { if (in_array($row['branch_code'], $existing, true)) { @@ -37,22 +53,18 @@ class HuifuOnboardBankBranch } } - return $rows; + return self::rankMatches($rows, $keyword); } - protected static function searchDatabase($keyword, $cityCode, $limit) + protected static function searchDatabase($keyword, $cityCode, $provCode, $limit) { try { $query = Db::name('shopro_huifu_bank_branch')->where('status', 1); - if ($cityCode !== '') { - $query->where('area_id', $cityCode); - } + self::applyAreaScope($query, $cityCode, $provCode); $query->where(function ($q) use ($keyword) { - $q->whereLike('branch_name', '%' . $keyword . '%') - ->whereOr('branch_code', 'like', '%' . $keyword . '%') - ->whereOr('bank_name', 'like', '%' . $keyword . '%'); + self::applyKeywordScope($q, $keyword); }); - $list = $query->limit($limit)->select(); + $list = $query->limit($limit * 3)->select(); if (!$list) { return []; } @@ -66,41 +78,192 @@ class HuifuOnboardBankBranch 'area_id' => (string) ($item['area_id'] ?? ''), ]; } - return $rows; + return array_slice(self::rankMatches($rows, $keyword), 0, $limit); } catch (\Throwable $e) { return []; } } - protected static function searchJson($keyword, $cityCode = '', $limit = 20) + protected static function searchJson($keyword, $cityCode = '', $provCode = '', $limit = 20) { $limit = max(0, (int) $limit); if ($limit <= 0) { return []; } $cityCode = HuifuOnboardAreaHelper::toCode($cityCode); + $provCode = HuifuOnboardAreaHelper::toCode($provCode); + $tokens = self::keywordTokens($keyword); $all = self::loadJson(); - $keywordLower = mb_strtolower($keyword, 'UTF-8'); $rows = []; foreach ($all as $item) { - if ($cityCode !== '' && HuifuOnboardAreaHelper::toCode($item['area_id'] ?? '') !== $cityCode) { + if (!self::matchAreaScope($item, $cityCode, $provCode)) { continue; } - $hay = mb_strtolower( - ($item['branch_name'] ?? '') . ' ' . ($item['bank_name'] ?? '') . ' ' . ($item['branch_code'] ?? ''), - 'UTF-8' - ); - if (mb_strpos($hay, $keywordLower, 0, 'UTF-8') === false) { + if (!self::matchKeywordTokens($item, $tokens, $keyword)) { continue; } $rows[] = $item; - if (count($rows) >= $limit) { - break; + } + return array_slice(self::rankMatches($rows, $keyword), 0, $limit); + } + + protected static function applyAreaScope($query, $cityCode, $provCode) + { + $cityCode = HuifuOnboardAreaHelper::toCode($cityCode); + $provCode = HuifuOnboardAreaHelper::toCode($provCode); + if ($cityCode !== '') { + $query->where('area_id', $cityCode); + return; + } + if ($provCode !== '') { + $query->where('prov_id', $provCode); + } + } + + protected static function matchAreaScope(array $item, $cityCode, $provCode) + { + $cityCode = HuifuOnboardAreaHelper::toCode($cityCode); + $provCode = HuifuOnboardAreaHelper::toCode($provCode); + if ($cityCode !== '') { + return HuifuOnboardAreaHelper::toCode($item['area_id'] ?? '') === $cityCode; + } + if ($provCode !== '') { + return HuifuOnboardAreaHelper::toCode($item['prov_id'] ?? '') === $provCode; + } + return true; + } + + protected static function applyKeywordScope($query, $keyword) + { + $tokens = self::keywordTokens($keyword); + if (!$tokens) { + $query->whereLike('branch_name', '%' . $keyword . '%'); + return; + } + foreach ($tokens as $token) { + $query->where(function ($q) use ($token) { + $q->whereLike('branch_name', '%' . $token . '%') + ->whereOr('branch_code', 'like', '%' . $token . '%') + ->whereOr('bank_name', 'like', '%' . $token . '%'); + }); + } + } + + protected static function normalizeKeyword($keyword) + { + $keyword = trim((string) $keyword); + $keyword = str_replace(['理行', '支理'], '支行', $keyword); + $keyword = preg_replace('/([\x{4e00}-\x{9fff}])支行/u', '$1 支行', $keyword); + $keyword = preg_replace('/([\x{4e00}-\x{9fff}])分行/u', '$1 分行', $keyword); + return trim(preg_replace('/\s+/u', ' ', $keyword)); + } + + protected static function keywordTokens($keyword) + { + $keyword = self::normalizeKeyword($keyword); + if ($keyword === '') { + return []; + } + if (preg_match('/^\d{8,20}$/', $keyword)) { + return [$keyword]; + } + $parts = preg_split('/[\s,,、\/]+/u', $keyword, -1, PREG_SPLIT_NO_EMPTY); + $tokens = []; + foreach ($parts ?: [$keyword] as $part) { + $part = trim($part); + if ($part === '') { + continue; + } + $tokens[] = $part; + $expanded = self::expandKeywordAliases($part); + foreach ($expanded as $alias) { + if (!in_array($alias, $tokens, true)) { + $tokens[] = $alias; + } } } + return array_values(array_unique($tokens)); + } + + protected static function expandKeywordAliases($token) + { + $map = [ + '建行' => ['建设银行', '建设'], + '农行' => ['农业银行', '农业'], + '工行' => ['工商银行', '工商'], + '中行' => ['中国银行', '中国'], + '交行' => ['交通银行', '交通'], + '理行' => ['支行'], + ]; + $aliases = []; + foreach ($map as $short => $fullList) { + if (mb_strpos($token, $short, 0, 'UTF-8') !== false) { + $aliases = array_merge($aliases, $fullList); + } + } + return $aliases; + } + + protected static function matchKeywordTokens(array $item, array $tokens, $keyword) + { + if (preg_match('/^\d{8,20}$/', $keyword)) { + return strpos((string) ($item['branch_code'] ?? ''), $keyword) !== false; + } + $hay = mb_strtolower( + ($item['branch_name'] ?? '') . ' ' . ($item['bank_name'] ?? '') . ' ' . ($item['branch_code'] ?? ''), + 'UTF-8' + ); + if (!$tokens) { + return mb_strpos($hay, mb_strtolower($keyword, 'UTF-8'), 0, 'UTF-8') !== false; + } + foreach ($tokens as $token) { + if (mb_strpos($hay, mb_strtolower($token, 'UTF-8'), 0, 'UTF-8') === false) { + return false; + } + } + return true; + } + + protected static function rankMatches(array $rows, $keyword) + { + if (!$rows) { + return []; + } + $keywordLower = mb_strtolower(trim((string) $keyword), 'UTF-8'); + usort($rows, function ($a, $b) use ($keywordLower) { + return self::matchScore($b, $keywordLower) <=> self::matchScore($a, $keywordLower); + }); return $rows; } + protected static function matchScore(array $item, $keywordLower) + { + $name = mb_strtolower((string) ($item['branch_name'] ?? ''), 'UTF-8'); + $code = (string) ($item['branch_code'] ?? ''); + if ($keywordLower !== '' && $name === $keywordLower) { + return 1000; + } + if ($keywordLower !== '' && mb_strpos($name, $keywordLower, 0, 'UTF-8') === 0) { + return 900; + } + if ($keywordLower !== '' && mb_strpos($name, $keywordLower, 0, 'UTF-8') !== false) { + return 800; + } + if ($keywordLower !== '' && strpos($code, $keywordLower) !== false) { + return 700; + } + return 100; + } + + protected static function provinceFromCity($cityCode) + { + $cityCode = HuifuOnboardAreaHelper::toCode($cityCode); + if (strlen($cityCode) < 2) { + return ''; + } + return substr($cityCode, 0, 2) . '0000'; + } + protected static function loadJson() { if (self::$jsonCache !== null) { diff --git a/addons/shopro_huifu/library/onboard/HuifuOnboardValidator.php b/addons/shopro_huifu/library/onboard/HuifuOnboardValidator.php index 89994fc..0f463f4 100644 --- a/addons/shopro_huifu/library/onboard/HuifuOnboardValidator.php +++ b/addons/shopro_huifu/library/onboard/HuifuOnboardValidator.php @@ -91,10 +91,10 @@ class HuifuOnboardValidator throw new \InvalidArgumentException('银行卡号格式不正确'); } if (empty($data['branch_code']) && empty($data['branch_name'])) { - throw new \InvalidArgumentException('请选择开户银行支行'); + throw new \InvalidArgumentException('请填写开户银行支行'); } - if (empty($data['branch_code']) && !empty($data['branch_name'])) { - throw new \InvalidArgumentException('请从搜索结果中点击选择开户银行支行(需联行号)'); + if (empty($data['branch_code'])) { + throw new \InvalidArgumentException('未能自动匹配支行联行号,请检查支行名称或手动填写联行号'); } if (empty($data['prov_id']) || empty($data['area_id'])) { throw new \InvalidArgumentException('请选择所在区域'); @@ -150,9 +150,13 @@ class HuifuOnboardValidator if ($name === '') { return; } - $city = $data['area_id'] ?? ''; - $matches = HuifuOnboardBankBranch::search($name, $city, 1); + $city = $data['area_id'] ?? ($data['shopro_city_id'] ?? ''); + $matches = HuifuOnboardBankBranch::search($name, $city, 5); + if (!$matches) { + $matches = HuifuOnboardBankBranch::search($name, '', 5); + } if ($matches) { + $matches = [$matches[0]]; $data['branch_code'] = $matches[0]['branch_code']; $data['branch_name'] = $matches[0]['branch_name']; if (empty($data['prov_id']) && !empty($matches[0]['prov_id'])) { diff --git a/addons/shopro_huifu/public/assets/js/backend/shopro_huifu/onboard.js b/addons/shopro_huifu/public/assets/js/backend/shopro_huifu/onboard.js index 6e6100e..17e3525 100644 --- a/addons/shopro_huifu/public/assets/js/backend/shopro_huifu/onboard.js +++ b/addons/shopro_huifu/public/assets/js/backend/shopro_huifu/onboard.js @@ -90,6 +90,7 @@ define(['jquery', 'bootstrap', 'backend'], function ($, undefined, Backend) { areaProps: { value: 'id', label: 'name', children: 'children' }, branchKeyword: '', branchResults: [], + branchSearched: false, uploadUrl: Fast.api.fixurl('ajax/upload'), saving: false }; @@ -242,6 +243,7 @@ define(['jquery', 'bootstrap', 'backend'], function ($, undefined, Backend) { branchTimer = setTimeout(function () { if (!keyword) { vm.branchResults = []; + vm.branchSearched = false; vm.form.branch_code = ''; vm.form.branch_name = ''; return; @@ -255,10 +257,21 @@ define(['jquery', 'bootstrap', 'backend'], function ($, undefined, Backend) { } }, function (ret, res) { vm.branchResults = res.data || []; + vm.branchSearched = true; + if (vm.branchResults.length === 1 && !vm.form.branch_code) { + vm.selectBranch(vm.branchResults[0]); + } return false; }); }, 300); }, + onBranchCodeInput: function () { + var code = (this.form.branch_code || '').replace(/\D/g, ''); + this.form.branch_code = code; + if (code && !this.form.branch_name && this.branchKeyword) { + this.form.branch_name = this.branchKeyword.trim(); + } + }, prepareFormBeforeSubmit: function () { var keyword = (this.branchKeyword || '').trim(); if (keyword) { @@ -286,12 +299,15 @@ define(['jquery', 'bootstrap', 'backend'], function ($, undefined, Backend) { Toastr.error('请填写银行预留手机号'); return false; } - if (!form.branch_code) { - if ((this.branchKeyword || '').trim() || form.branch_name) { - Toastr.error('请从支行搜索结果中点击选择开户银行(需联行号)'); - } else { - Toastr.error('请搜索并选择开户银行支行'); - } + if (!form.branch_code && !(this.branchKeyword || '').trim() && !form.branch_name) { + Toastr.error('请填写开户银行支行'); + return false; + } + if (!form.branch_name && this.branchKeyword) { + form.branch_name = this.branchKeyword.trim(); + } + if (form.branch_code && !/^\d{8,20}$/.test(form.branch_code)) { + Toastr.error('联行号格式不正确,应为8-20位数字'); return false; } if (!form.shopro_province_id && !form.prov_id) { diff --git a/application/admin/view/shopro_huifu/onboard/add.html b/application/admin/view/shopro_huifu/onboard/add.html index b8b246b..fa360c8 100644 --- a/application/admin/view/shopro_huifu/onboard/add.html +++ b/application/admin/view/shopro_huifu/onboard/add.html @@ -389,17 +389,23 @@ - - 须从搜索结果中点击选择支行,仅输入文字无法保存 + + 搜索支行名称,系统将自动匹配联行号(与小程序一致);也可从下方结果点击选择 + 本地库未搜到,保存时服务端会再次自动匹配;仍失败请手动填写联行号 已选:{{ form.branch_name }}(联行号 {{ form.branch_code }}) {{ item.branch_name }} - {{ item.branch_code }} + {{ item.branch_code }} · {{ item.area_id }} + + + + + diff --git a/public/assets/js/backend/shopro_huifu/onboard.js b/public/assets/js/backend/shopro_huifu/onboard.js index 6e6100e..17e3525 100644 --- a/public/assets/js/backend/shopro_huifu/onboard.js +++ b/public/assets/js/backend/shopro_huifu/onboard.js @@ -90,6 +90,7 @@ define(['jquery', 'bootstrap', 'backend'], function ($, undefined, Backend) { areaProps: { value: 'id', label: 'name', children: 'children' }, branchKeyword: '', branchResults: [], + branchSearched: false, uploadUrl: Fast.api.fixurl('ajax/upload'), saving: false }; @@ -242,6 +243,7 @@ define(['jquery', 'bootstrap', 'backend'], function ($, undefined, Backend) { branchTimer = setTimeout(function () { if (!keyword) { vm.branchResults = []; + vm.branchSearched = false; vm.form.branch_code = ''; vm.form.branch_name = ''; return; @@ -255,10 +257,21 @@ define(['jquery', 'bootstrap', 'backend'], function ($, undefined, Backend) { } }, function (ret, res) { vm.branchResults = res.data || []; + vm.branchSearched = true; + if (vm.branchResults.length === 1 && !vm.form.branch_code) { + vm.selectBranch(vm.branchResults[0]); + } return false; }); }, 300); }, + onBranchCodeInput: function () { + var code = (this.form.branch_code || '').replace(/\D/g, ''); + this.form.branch_code = code; + if (code && !this.form.branch_name && this.branchKeyword) { + this.form.branch_name = this.branchKeyword.trim(); + } + }, prepareFormBeforeSubmit: function () { var keyword = (this.branchKeyword || '').trim(); if (keyword) { @@ -286,12 +299,15 @@ define(['jquery', 'bootstrap', 'backend'], function ($, undefined, Backend) { Toastr.error('请填写银行预留手机号'); return false; } - if (!form.branch_code) { - if ((this.branchKeyword || '').trim() || form.branch_name) { - Toastr.error('请从支行搜索结果中点击选择开户银行(需联行号)'); - } else { - Toastr.error('请搜索并选择开户银行支行'); - } + if (!form.branch_code && !(this.branchKeyword || '').trim() && !form.branch_name) { + Toastr.error('请填写开户银行支行'); + return false; + } + if (!form.branch_name && this.branchKeyword) { + form.branch_name = this.branchKeyword.trim(); + } + if (form.branch_code && !/^\d{8,20}$/.test(form.branch_code)) { + Toastr.error('联行号格式不正确,应为8-20位数字'); return false; } if (!form.shopro_province_id && !form.prov_id) {