feat(shopro_huifu): 完善汇付进件功能的表单验证和错误处理 - 增加支行搜索功能的城市筛选支持,优化搜索逻辑 - 添加前端表单验证机制,包括身份证、银行卡、区域等必填字段校验 - 改进支行选择验证,确保必须从搜索结果中选择带联行号的支行 - 优化后端保存逻辑,添加强制保存参数控制 - 统一异常处理机制,增加HttpResponseException的正确抛出 - 完善用户界面提示信息,明确操作指引和错误反馈 - 修复表单提交前的数据准备逻辑,确保数据完整性 ```
270 行
11 KiB
PHP
270 行
11 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro_huifu\library\onboard;
|
||
|
||
class HuifuOnboardValidator
|
||
{
|
||
protected static $fieldLabels = [
|
||
'account_type' => '结算账户类型',
|
||
'name' => '真实姓名',
|
||
'cert_no' => '身份证号',
|
||
'reg_name' => '商户名称',
|
||
'license_code' => '统一社会信用代码',
|
||
'legal_name' => '法人姓名',
|
||
'legal_mobile' => '法人手机号',
|
||
'legal_cert_no' => '法人身份证号',
|
||
'reg_detail' => '详细地址',
|
||
'mobile_no' => '手机号',
|
||
'card_no' => '银行卡号',
|
||
'branch_name' => '开户银行',
|
||
'branch_code' => '支行联行号',
|
||
'prov_id' => '所在区域',
|
||
'id_card_front_url' => '身份证人像面',
|
||
'id_card_back_url' => '身份证国徽面',
|
||
'license_url' => '营业执照照片',
|
||
'legal_id_front_url' => '法人身份证人像面',
|
||
'legal_id_back_url' => '法人身份证国徽面',
|
||
];
|
||
|
||
public static function validateSubmit(array $data)
|
||
{
|
||
$type = self::normalizeAccountType($data['account_type'] ?? 'indv');
|
||
$data['account_type'] = $type;
|
||
HuifuOnboardAreaHelper::applyRegionCodes($data);
|
||
self::resolveBranch($data);
|
||
|
||
if ($type === 'ent') {
|
||
self::validateEnterprise($data);
|
||
} else {
|
||
self::validateIndividual($data);
|
||
}
|
||
self::validateBank($data);
|
||
|
||
return self::normalize($data);
|
||
}
|
||
|
||
protected static function validateIndividual(array $data)
|
||
{
|
||
foreach (['name', 'cert_no', 'mobile_no', 'reg_detail', 'id_card_front_url', 'id_card_back_url'] as $field) {
|
||
if (empty($data[$field])) {
|
||
throw new \InvalidArgumentException('请填写' . self::fieldLabel($field));
|
||
}
|
||
}
|
||
if (!preg_match('/^\d{17}[\dXx]$/', $data['cert_no'])) {
|
||
throw new \InvalidArgumentException('身份证号格式不正确');
|
||
}
|
||
if (!preg_match('/^1\d{10}$/', $data['mobile_no'])) {
|
||
throw new \InvalidArgumentException('手机号格式不正确');
|
||
}
|
||
self::assertCertDates($data, 'cert');
|
||
}
|
||
|
||
protected static function validateEnterprise(array $data)
|
||
{
|
||
foreach ([
|
||
'reg_name', 'license_code', 'reg_detail', 'legal_name', 'legal_mobile', 'legal_cert_no',
|
||
'license_url', 'legal_id_front_url', 'legal_id_back_url',
|
||
] as $field) {
|
||
if (empty($data[$field])) {
|
||
throw new \InvalidArgumentException('请填写' . self::fieldLabel($field));
|
||
}
|
||
}
|
||
if (!preg_match('/^[0-9A-Z]{15}$|^[0-9A-Z]{18}$/', strtoupper($data['license_code']))) {
|
||
throw new \InvalidArgumentException('统一社会信用代码格式不正确');
|
||
}
|
||
if (!preg_match('/^\d{17}[\dXx]$/', $data['legal_cert_no'])) {
|
||
throw new \InvalidArgumentException('法人身份证号格式不正确');
|
||
}
|
||
if (!preg_match('/^1\d{10}$/', $data['legal_mobile'])) {
|
||
throw new \InvalidArgumentException('法人手机号格式不正确');
|
||
}
|
||
self::assertCertDates($data, 'license');
|
||
self::assertCertDates($data, 'legal_cert');
|
||
}
|
||
|
||
protected static function validateBank(array $data)
|
||
{
|
||
if (empty($data['card_no'])) {
|
||
throw new \InvalidArgumentException('请填写银行卡号');
|
||
}
|
||
if (!preg_match('/^\d{12,19}$/', preg_replace('/\s+/', '', $data['card_no']))) {
|
||
throw new \InvalidArgumentException('银行卡号格式不正确');
|
||
}
|
||
if (empty($data['branch_code']) && empty($data['branch_name'])) {
|
||
throw new \InvalidArgumentException('请选择开户银行支行');
|
||
}
|
||
if (empty($data['branch_code']) && !empty($data['branch_name'])) {
|
||
throw new \InvalidArgumentException('请从搜索结果中点击选择开户银行支行(需联行号)');
|
||
}
|
||
if (empty($data['prov_id']) || empty($data['area_id'])) {
|
||
throw new \InvalidArgumentException('请选择所在区域');
|
||
}
|
||
$bankAccountType = (string) ($data['bank_account_type'] ?? '0');
|
||
if ($bankAccountType === '0') {
|
||
$mp = trim((string) ($data['mp'] ?? $data['mobile_no'] ?? $data['legal_mobile'] ?? ''));
|
||
if ($mp === '' || !preg_match('/^1\d{10}$/', $mp)) {
|
||
throw new \InvalidArgumentException('请填写银行预留手机号');
|
||
}
|
||
}
|
||
}
|
||
|
||
protected static function assertCertDates(array $data, $prefix)
|
||
{
|
||
$typeKey = $prefix . '_validity_type';
|
||
$beginKey = $prefix . '_begin_date';
|
||
$endKey = $prefix . '_end_date';
|
||
$type = (string) ($data[$typeKey] ?? '');
|
||
if (!in_array($type, ['0', '1'], true)) {
|
||
throw new \InvalidArgumentException('证件有效期类型无效');
|
||
}
|
||
if (empty($data[$beginKey])) {
|
||
throw new \InvalidArgumentException('请选择证件开始日期');
|
||
}
|
||
if ($type === '0' && empty($data[$endKey])) {
|
||
throw new \InvalidArgumentException('请选择证件截止日期');
|
||
}
|
||
if ($type === '0' && !empty($data[$endKey])) {
|
||
self::assertFutureDate($data[$endKey], '证件有效期');
|
||
}
|
||
}
|
||
|
||
protected static function assertFutureDate($ymd, $label)
|
||
{
|
||
$ymd = trim((string) $ymd);
|
||
if (strlen($ymd) !== 8) {
|
||
return;
|
||
}
|
||
$endTs = strtotime(substr($ymd, 0, 4) . '-' . substr($ymd, 4, 2) . '-' . substr($ymd, 6, 2));
|
||
$todayTs = strtotime(date('Y-m-d'));
|
||
if ($endTs !== false && $endTs <= $todayTs) {
|
||
throw new \InvalidArgumentException($label . '须晚于今天,长期有效请选择 2099-12-31');
|
||
}
|
||
}
|
||
|
||
protected static function resolveBranch(array &$data)
|
||
{
|
||
if (!empty($data['branch_code'])) {
|
||
return;
|
||
}
|
||
$name = trim((string) ($data['branch_name'] ?? ''));
|
||
if ($name === '') {
|
||
return;
|
||
}
|
||
$city = $data['area_id'] ?? '';
|
||
$matches = HuifuOnboardBankBranch::search($name, $city, 1);
|
||
if ($matches) {
|
||
$data['branch_code'] = $matches[0]['branch_code'];
|
||
$data['branch_name'] = $matches[0]['branch_name'];
|
||
if (empty($data['prov_id']) && !empty($matches[0]['prov_id'])) {
|
||
$data['prov_id'] = $matches[0]['prov_id'];
|
||
}
|
||
if (empty($data['area_id']) && !empty($matches[0]['area_id'])) {
|
||
$data['area_id'] = $matches[0]['area_id'];
|
||
}
|
||
}
|
||
}
|
||
|
||
public static function normalize(array $data)
|
||
{
|
||
$data['account_type'] = self::normalizeAccountType($data['account_type'] ?? 'indv');
|
||
HuifuOnboardAreaHelper::applyRegionCodes($data);
|
||
self::resolveBranch($data);
|
||
|
||
$data['bank_account_type'] = (string) ($data['bank_account_type'] ?? '0');
|
||
if ($data['account_type'] === 'indv') {
|
||
$data['bank_account_type'] = '0';
|
||
}
|
||
|
||
$data['name'] = trim($data['name'] ?? '');
|
||
$data['cert_no'] = strtoupper(trim($data['cert_no'] ?? ''));
|
||
$data['mobile_no'] = trim($data['mobile_no'] ?? '');
|
||
$data['reg_name'] = trim($data['reg_name'] ?? '');
|
||
$data['license_code'] = strtoupper(trim($data['license_code'] ?? ''));
|
||
$data['legal_name'] = trim($data['legal_name'] ?? '');
|
||
$data['legal_mobile'] = trim($data['legal_mobile'] ?? '');
|
||
$data['legal_cert_no'] = strtoupper(trim($data['legal_cert_no'] ?? ''));
|
||
$data['reg_detail'] = trim($data['reg_detail'] ?? '');
|
||
$data['card_no'] = preg_replace('/\s+/', '', $data['card_no'] ?? '');
|
||
$data['branch_code'] = trim($data['branch_code'] ?? '');
|
||
$data['branch_name'] = trim($data['branch_name'] ?? '');
|
||
$data['prov_id'] = HuifuOnboardAreaHelper::toCode($data['prov_id'] ?? '');
|
||
$data['area_id'] = HuifuOnboardAreaHelper::toCode($data['area_id'] ?? '');
|
||
$data['district_id'] = HuifuOnboardAreaHelper::toCode($data['district_id'] ?? '');
|
||
|
||
foreach (['cert', 'license', 'legal_cert'] as $prefix) {
|
||
$beginKey = $prefix . '_begin_date';
|
||
$endKey = $prefix . '_end_date';
|
||
$typeKey = $prefix . '_validity_type';
|
||
if (!empty($data[$beginKey])) {
|
||
$data[$beginKey] = self::toYmd($data[$beginKey]);
|
||
}
|
||
if (isset($data[$typeKey])) {
|
||
$data[$typeKey] = (string) $data[$typeKey];
|
||
}
|
||
if (($data[$typeKey] ?? '') === '1') {
|
||
unset($data[$endKey]);
|
||
} elseif (!empty($data[$endKey])) {
|
||
$data[$endKey] = self::toYmd($data[$endKey]);
|
||
}
|
||
}
|
||
|
||
if ($data['account_type'] === 'indv') {
|
||
// 汇付对私法人:card_name 必须与个人开户姓名 name 完全一致
|
||
$data['card_name'] = $data['name'];
|
||
$data['mp'] = trim($data['mp'] ?? $data['mobile_no']);
|
||
} else {
|
||
if ($data['bank_account_type'] === '1') {
|
||
$data['card_name'] = trim($data['card_name'] ?: $data['reg_name']);
|
||
} else {
|
||
// 企业/个体户对私结算:card_name 必须与法人姓名一致
|
||
$data['card_name'] = $data['legal_name'];
|
||
$data['cert_no'] = $data['legal_cert_no'];
|
||
$data['cert_validity_type'] = $data['legal_cert_validity_type'];
|
||
$data['cert_begin_date'] = $data['legal_cert_begin_date'];
|
||
$data['cert_end_date'] = $data['legal_cert_end_date'] ?? '';
|
||
}
|
||
$data['mp'] = trim($data['mp'] ?? $data['legal_mobile']);
|
||
$data['mobile_no'] = $data['legal_mobile'];
|
||
$data['name'] = $data['legal_name'];
|
||
}
|
||
|
||
return $data;
|
||
}
|
||
|
||
public static function maskPayload(array $data)
|
||
{
|
||
$copy = $data;
|
||
foreach (['cert_no', 'legal_cert_no'] as $key) {
|
||
if (!empty($copy[$key])) {
|
||
$copy[$key] = substr($copy[$key], 0, 6) . '********' . substr($copy[$key], -4);
|
||
}
|
||
}
|
||
if (!empty($copy['card_no'])) {
|
||
$len = strlen($copy['card_no']);
|
||
$copy['card_no'] = str_repeat('*', max(0, $len - 4)) . substr($copy['card_no'], -4);
|
||
}
|
||
return $copy;
|
||
}
|
||
|
||
protected static function normalizeAccountType($type)
|
||
{
|
||
$type = strtolower(trim((string) $type));
|
||
return in_array($type, ['indv', 'ent'], true) ? $type : 'indv';
|
||
}
|
||
|
||
protected static function toYmd($value)
|
||
{
|
||
$value = trim((string) $value);
|
||
$value = str_replace(['-', '/', '.'], '', $value);
|
||
if (strlen($value) === 8 && ctype_digit($value)) {
|
||
return $value;
|
||
}
|
||
throw new \InvalidArgumentException('日期格式应为 yyyyMMdd 或 yyyy-MM-dd');
|
||
}
|
||
|
||
protected static function fieldLabel($field)
|
||
{
|
||
return self::$fieldLabels[$field] ?? $field;
|
||
}
|
||
}
|