diff --git a/composer.json b/composer.json index 8aa085a..fb76afe 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "tansilu/hfpay-lib", - "version": "0.0.11", + "version": "0.0.12", "type": "library", "require": { "php": ">=7.4", diff --git a/src/Constant.php b/src/Constant.php index 23965f0..738b390 100644 --- a/src/Constant.php +++ b/src/Constant.php @@ -89,4 +89,163 @@ class Constant const RISK_DIV_TYPE_MASTER = 1; const RISK_DIV_TYPE_FEE = 2; const RISK_DIV_TYPE_OTHER = 3; + + /** + * 一级市场->文件上传->业务类型 trans_type 3:企业开户4:个体户开户 7:个人开户 + * */ + + const ONE_UP_TRANS_TYPE_COMPANY = 3; + const ONE_UP_TRANS_TYPE_PEDDLER = 4; + const ONE_UP_TRANS_TYPE_PERSONAL = 7; + + /** + * 一级市场->文件上传->附件类型 attach_type + * + 1:营业执照注册号 + 2:组织结构代码证 + 3:税务登记证号 + 4:法人证件 + 5:开户银行许可证 + 6:统一社会信用代码 + 7:快捷协议 + 8:代扣协议 + 9:开户电子协议 + 10:法人证件反面 + 11:经营照片 + 12:经营照片(地址照片) + 13:经营照片(门头照片) + 14:结算卡正面 + 15:结算卡反面 + 16:经办人证件 + 21:个人身份证正面 + 22:个人身份证反面 + 23:用户平台合作协议 + 99.其他 + * + * */ + const ONE_UP_ATTACH_TYPE_1 = 1; + const ONE_UP_ATTACH_TYPE_2 = 2; + const ONE_UP_ATTACH_TYPE_3 = 3; + const ONE_UP_ATTACH_TYPE_4 = 4; + const ONE_UP_ATTACH_TYPE_5 = 5; + const ONE_UP_ATTACH_TYPE_6 = 6; + const ONE_UP_ATTACH_TYPE_7 = 7; + const ONE_UP_ATTACH_TYPE_8 = 8; + const ONE_UP_ATTACH_TYPE_9 = 9; + const ONE_UP_ATTACH_TYPE_10 = 10; + const ONE_UP_ATTACH_TYPE_11 = 11; + const ONE_UP_ATTACH_TYPE_12 = 12; + const ONE_UP_ATTACH_TYPE_13 = 13; + const ONE_UP_ATTACH_TYPE_14 = 14; + const ONE_UP_ATTACH_TYPE_15 = 15; + const ONE_UP_ATTACH_TYPE_16 = 16; + const ONE_UP_ATTACH_TYPE_21 = 21; + const ONE_UP_ATTACH_TYPE_22 = 22; + const ONE_UP_ATTACH_TYPE_23 = 23; + const ONE_UP_ATTACH_TYPE_99 = 99; + + /** + * 一级市场->文件上传->上传类型 handel_type 1:新增 2:更新 + * */ + + const ONE_UP_HANDEL_TYPE_ADD = 1; + const ONE_UP_HANDEL_TYPE_UPDATE = 2; + + + + /** + * 一级市场-> + * 个人用户开户接口(后台版)/ 企业用户开户接口(后台版) + * ->账户用途 acct_usage_type + * one-level-seller:一级市场卖家户 salesr-promoter:分销员户 + * */ + + const ONE_COM_ACCT_USAGE_TYPE_ONE = 'one-level-seller'; + const ONE_COM_ACCT_USAGE_TYPE_SALESR = 'salesr-promoter'; + + /** + * 一级市场->个人用户开户接口(后台版)->个人职业 occupation + * + * 01:国家机关、党群机关、企事业单位负责人 + 02:金融业从业人员 + 03:房地产业从业人员 + 04:商贸从业人员 + 05:自由职业者 + 06:科教文从业人员 + 07:制造业从业人员 + 08:卫生行业从业人员 + 09:IT业从业人员 + 10:农林牧渔劳动者 + 11:生产工作、运输工作和部分体力劳动者 + 12:退休人员 + 13:不便分类的其他劳动者 + * */ + const ONE_PER_OCCUPATION_1 = '01'; + const ONE_PER_OCCUPATION_2 = '02'; + const ONE_PER_OCCUPATION_3 = '03'; + const ONE_PER_OCCUPATION_4 = '04'; + const ONE_PER_OCCUPATION_5 = '05'; + const ONE_PER_OCCUPATION_6 = '06'; + const ONE_PER_OCCUPATION_7 = '07'; + const ONE_PER_OCCUPATION_8 = '08'; + const ONE_PER_OCCUPATION_9 = '09'; + const ONE_PER_OCCUPATION_10 = '10'; + const ONE_PER_OCCUPATION_11 = '11'; + const ONE_PER_OCCUPATION_12 = '12'; + const ONE_PER_OCCUPATION_13 = '13'; + + /** + * 一级市场-> + * 企业用户开户接口(后台版) /个体户开户接口(后台版) + * ->操作类型 operate_type + * A:新增 M:修改 + * */ + const ONE_COM_OPERATE_TYPE_A = 'A'; + const ONE_COM_OPERATE_TYPE_M = 'M'; + + + /** + * 一级市场-> + * 企业用户开户接口(后台版) + * ->企业证照类型 corp_license_type + * 1.普通证照 2.三证合一 + * */ + const ONE_COM_CORP_LIS_TYPE_1 = 1; + const ONE_COM_CORP_LIS_TYPE_2 = 2; + + /** + * 一级市场-> + * 企业用户开户接口(后台版) + * ->法人证件类型 legal_id_card_type /控股股东(实际控制人) controlling_shareholder -> idCardType + * 10:身份证 + 11:护照 + 12:军官证 + 13:士兵证 + 14:回乡证 + 15:户口本 + 16:警官证 + 17:台胞证 + 18:组织机构代码 + 19:营业执照 + 20:税务登记证 + 21:统一社会信用代码证 + 22:其他 + * */ + const ONE_COM_ID_CARD_TYPE_10 = 10; + const ONE_COM_ID_CARD_TYPE_11 = 11; + const ONE_COM_ID_CARD_TYPE_12 = 12; + const ONE_COM_ID_CARD_TYPE_13 = 13; + const ONE_COM_ID_CARD_TYPE_14 = 14; + const ONE_COM_ID_CARD_TYPE_15 = 15; + const ONE_COM_ID_CARD_TYPE_16 = 16; + const ONE_COM_ID_CARD_TYPE_17 = 17; + const ONE_COM_ID_CARD_TYPE_18 = 18; + const ONE_COM_ID_CARD_TYPE_19 = 19; + const ONE_COM_ID_CARD_TYPE_20 = 20; + const ONE_COM_ID_CARD_TYPE_21 = 21; + const ONE_COM_ID_CARD_TYPE_22 = 22; + + + + } \ No newline at end of file diff --git a/src/HFPayClient.php b/src/HFPayClient.php index d323d6c..460c067 100644 --- a/src/HFPayClient.php +++ b/src/HFPayClient.php @@ -11,6 +11,7 @@ use Tansilu\HfPayLib\factory\impl\ManageFactory; use Tansilu\HfPayLib\factory\impl\MarketFactory; use Tansilu\HfPayLib\factory\impl\QueryFactory; use Tansilu\HfPayLib\factory\impl\TradeFactory; +use Tansilu\HfPayLib\factory\impl\OneLevelFactory; use Tansilu\HfPayLib\http\IHttpClient; use Tansilu\HfPayLib\http\impl\HFPayHttpClient; @@ -102,4 +103,15 @@ class HFPayClient { return $this->container->make(TradeFactory::class); } + /** + * 一级市场 + * + * @return OneLevelFactory + * @throws DependencyException + * @throws NotFoundException + */ + public function one(): OneLevelFactory + { + return $this->container->make(OneLevelFactory::class); + } } \ No newline at end of file diff --git a/src/factory/impl/OneLevelFactory.php b/src/factory/impl/OneLevelFactory.php new file mode 100644 index 0000000..e46c124 --- /dev/null +++ b/src/factory/impl/OneLevelFactory.php @@ -0,0 +1,212 @@ +toParams(); + + $data = $this->uploadPost('/api/alseFile/file01', $request); + if(!$data->isSuccess()) { + throw new BizException('上传失败', $data->getCode()); + } + + return UploadResponse::make($data->getBody()); + } + /** + * 个人用户开户接口(后台版) + * 用于在本平台系统下为个人用户开户。开户时,会有实名验证和身份证OCR验证,请提供真实信息。 + * 本接口只能用于一级市场的卖家户、分销员户的开户。开通的用户只具有收款能力(藏品卖家),不具有付款能力。 + * + * @throws BizException + * @throws ParamsException + * + * @see https://hfpay.cloudpnr.com/customers/nft/#/levelOneOpenAccount/personalOpenAccount + 返回码 + A40001 省份地区信息非法 + A40002 该用户已开户,身份证号已存在 + A40004 黑名单用户,开户失败 + A40005 身份校验失败 + A40006 身份证格式校验失败 + A40007 身份证末位校验失败 + A40008 手续费配置异常 + A40085 该证件用户开户请求正在处理中,请稍后再试 + A40218 附件编号不存在 + A40219 缺少附件信息 + A40220 开户服务费收取失败 + A40211 user_id不能重复开户 + A40212 身份证开户数超过最大限制 + A40203 用户已开户,无需再提交开户 + A40210 年龄不符合要求 + A40223 身份证附件识别不一致 + A40224 身份证OCR失败 + */ + public function personalCreate( PersonalRequest $params) : PersonalResponse + { + $request = $params->toParams(); + + $data = $this->post('/api/acou/user08', $request); + + if(!$data->isSuccess()) { + throw new BizException('一级市场-个人用户开户创建失败', $data->getCode()); + } + return PersonalResponse::make($data->getBody()); + } + + /** + * 企业用户开户接口(后台版) + * 用于在本平台系统下为个人用户开户。开户时,会有实名验证和身份证OCR验证,请提供真实信息。 + * 本接口只能用于一级市场的卖家户、分销员户的开户。开通的用户只具有收款能力(藏品卖家),不具有付款能力。 + * + * @throws BizException + * @throws ParamsException + * + * @see https://hfpay.cloudpnr.com/customers/nft/#/levelOneOpenAccount/enterpriseOpenAccount + 返回码 + A40001 省份地区信息非法 + A40002 该用户已开户,身份证号已存在 + A40004 黑名单用户,开户失败 + A40005 身份校验失败 + A40006 身份证格式校验失败 + A40007 身份证末位校验失败 + A40009 控股股东不能超过二十个 + A40010 控股股东部分字段为空 + A40011 控股股东部分字段长度超长 + A40012 控股股东证件类型不存在 + A40013 控股股东身份证不合法 + A40014 该企业证照类型与证照号不匹配 + A40015 该申请号已开户 + A40016 该企业营业执照注册号或者社会统一信用码已开户 + A40017 该企业开户申请正在审核中,不能修改 + A40018 该企业开户申请已被拒绝,不能修改 + A40019 该企业开户申请已经通过,不能修改 + A40020 未查询到原开户申请号 + A40021 该申请号已申请开户 + A40022 营业执照注册号或者社会统一信用码已申请开户 + A40023 附件信息不存在 + A40024 附件数量超出10个限制 + A40025 附件文件不存在 + A40026 文件已被使用 + A40027 该记录非待审核状态 + A40028 银行信息不存在 + A40031 原企业开户申请状态为关闭,不能修改 + A40045 审核拒绝,【具体拒绝原因】 + A40046 审核待修改,【具体修改理由】 + A40047 开户成功,绑卡失败,【具体绑卡失败原因】 + A40054 审核状态错误 + H10005 银行卡已存在 + A40102 用户不存在 + A40103 该用户已开户 + A40107 收款方必须开通汇付账户 + A40105 开户请求银行处理中,请勿重复提交 + A40106 请上传文件 + */ + public function companyCreate( CompanyRequest $params) : CompanyResponse + { + $request = $params->toParams(); + + $data = $this->post('/api/acou/corp05', $request); + + if(!$data->isSuccess()) { + throw new BizException('一级市场-企业用户开户创建失败', $data->getCode()); + } + return CompanyResponse::make($data->getBody()); + } + + + /** + * 个体商户开户接口(后台版) + * 用于在本平台系统下为个人用户开户。开户时,会有实名验证和身份证OCR验证,请提供真实信息。 + * 本接口只能用于一级市场的卖家户、分销员户的开户。开通的用户只具有收款能力(藏品卖家),不具有付款能力。 + * + * @throws BizException + * @throws ParamsException + * + * @see https://hfpay.cloudpnr.com/customers/nft/#/levelOneOpenAccount/individualOpenAccount + 返回码 + A40024 附件数量超出10个限制 + A40025 附件文件不存在 + A40026 文件已被使用 + A40027 该记录非待审核状态 + A40028 银行信息不存在 + A40032 该个体工商户开户申请审核已通过,不能修改 + A40033 该个体工商户开户申请正在审核中,不能修改 + A40034 该个体工商户开户申请审核拒绝,不能修改 + A40036 原个体户开户请求不存在 + A40037 该申请号已开户 + A40038 个体户开户申请号已申请开户 + A40039 营业执照注册号已被使用 + A40045 审核拒绝,【具体拒绝原因】 + A40046 审核待修改,【具体修改理由】 + A40047 开户成功,绑卡失败,【具体绑卡失败原因】 + A40054 审核状态错误 + H10005 银行卡已存在 + A40102 用户不存在 + A40103 该用户已开户 + A40107 收款方必须开通汇付账户 + A40105 开户请求银行处理中,请勿重复提交 + A40106 请上传文件 + */ + public function peddlarCreate( PeddlarRequest $params) : PeddlarResponse + { + $request = $params->toParams(); + + $data = $this->post('/api/acou/solo02', $request); + + if(!$data->isSuccess()) { + throw new BizException('一级市场-商户用户开户创建失败', $data->getCode()); + } + return PeddlarResponse::make($data->getBody()); + } + +} \ No newline at end of file diff --git a/src/factory/object/RequestObj.php b/src/factory/object/RequestObj.php index 36a9950..32423ff 100644 --- a/src/factory/object/RequestObj.php +++ b/src/factory/object/RequestObj.php @@ -4,7 +4,7 @@ namespace Tansilu\HfPayLib\factory\object; trait RequestObj { - private array $data; + private array $data =[]; public function __set($name, $value) { diff --git a/src/factory/object/ResponseObj.php b/src/factory/object/ResponseObj.php index bf334b1..97c8b03 100644 --- a/src/factory/object/ResponseObj.php +++ b/src/factory/object/ResponseObj.php @@ -11,7 +11,7 @@ trait ResponseObj return $self; } - private array $data; + private array $data = []; public function __get($name) { diff --git a/src/factory/object/onelevel/CompanyRequest.php b/src/factory/object/onelevel/CompanyRequest.php new file mode 100644 index 0000000..22f4434 --- /dev/null +++ b/src/factory/object/onelevel/CompanyRequest.php @@ -0,0 +1,214 @@ +data['order_date'])) { + throw new ParamsException('订单日期不能为空', 'order_date'); + } + if(empty($this->data['order_id'])) { + throw new ParamsException('订单号不能为空', 'order_id'); + } + + if(empty($this->data['acct_usage_type'])) { + throw new ParamsException('账户用途不能为空', 'acct_usage_type'); + } + + if(empty($this->data['user_id'])) { + throw new ParamsException('用户ID不能为空', 'user_id'); + } + if(empty($this->data['apply_id'])) { + throw new ParamsException('开户申请号不能为空', 'apply_id'); + } + + if(empty($this->data['operate_type'])) { + throw new ParamsException('操作类型不能为空', 'operate_type'); + } + if(!in_array($this->data['operate_type'], [Constant::ONE_COM_OPERATE_TYPE_A, Constant::ONE_COM_OPERATE_TYPE_M])) { + throw new ParamsException('操作类型不正确', 'operate_type'); + } + + if(empty($this->data['corp_license_type'])) { + throw new ParamsException('企业证照类型不能为空', 'corp_license_type'); + } + if(!in_array($this->data['corp_license_type'], [Constant::ONE_COM_CORP_LIS_TYPE_1, Constant::ONE_COM_CORP_LIS_TYPE_2])) { + throw new ParamsException('企业证照类型不正确', 'corp_license_type'); + } + //营业执照 + if($this->data['corp_license_type'] == Constant::ONE_COM_CORP_LIS_TYPE_1) { + if(empty($this->data['business_code'])) { + throw new ParamsException('营业执照注册号不能为空', 'business_code'); + } + if(empty($this->data['institution_code'])) { + throw new ParamsException('组织机构代码不能为空', 'institution_code'); + } + if(empty($this->data['tax_code'])) { + throw new ParamsException('税务登记证号不能为空', 'tax_code'); + } + + } + else + { + //统一社会信用代码 + if(empty($this->data['social_credit_code'])) { + throw new ParamsException('统一社会信用代码不能为空', 'social_credit_code'); + } + + } + + if(empty($this->data['corp_name'])) { + throw new ParamsException('企业名称不能为空', 'corp_name'); + } + + /* if(empty($this->data['license_start_date'])) { + throw new ParamsException('证照起始日期不能为空', 'license_start_date'); + } + if(empty($this->data['license_end_date'])) { + throw new ParamsException('证照结束日期不能为空', 'license_end_date'); + } + if(empty($this->data['corp_business_address'])) { + throw new ParamsException('企业经营地址不能为空', 'corp_business_address'); + } + if(empty($this->data['corp_reg_address'])) { + throw new ParamsException('企业注册地址不能为空', 'corp_reg_address'); + } + if(empty($this->data['corp_fixed_telephone'])) { + throw new ParamsException('企业固定电话不能为空', 'corp_fixed_telephone'); + } + if(empty($this->data['business_scope'])) { + throw new ParamsException('经营范围不能为空', 'business_scope'); + }*/ + if(empty($this->data['controlling_shareholder'])) { + throw new ParamsException('控股股东(实际控制人)不能为空', 'controlling_shareholder'); + } + $this->data['controlling_shareholder'] = json_encode($this->data['controlling_shareholder'], JSON_UNESCAPED_UNICODE); + + if(empty($this->data['legal_name'])) { + throw new ParamsException('法人姓名不能为空', 'legal_name'); + } + + if(empty($this->data['legal_id_card_type'])) { + throw new ParamsException('法人证件类型不能为空', 'legal_id_card_type'); + } + + if(empty($this->data['legal_id_card'])) { + throw new ParamsException('法人证件号码不能为空', 'legal_id_card'); + } + + if(empty($this->data['legal_cert_start_date'])) { + throw new ParamsException('法人证件起始日期不能为空', 'legal_cert_start_date'); + } + if(empty($this->data['legal_cert_end_date'])) { + throw new ParamsException('法人证件截止日期不能为空', 'legal_cert_end_date'); + } + if(empty($this->data['legal_mobile'])) { + throw new ParamsException('法人手机号码不能为空', 'legal_mobile'); + } + + if(empty($this->data['contact_name'])) { + throw new ParamsException('企业联系人姓名不能为空', 'contact_name'); + } + if(empty($this->data['contact_mobile'])) { + throw new ParamsException('联系人手机号不能为空', 'contact_mobile'); + } + if(empty($this->data['contact_email'])) { + throw new ParamsException('联系人邮箱不能为空', 'contact_email'); + } + + if(empty($this->data['bank_acct_name'])) { + throw new ParamsException('开户银行账户名不能为空', 'bank_acct_name'); + } + // 银行ID号 5000多个 + //https://cloudpnrcdn.oss-cn-shanghai.aliyuncs.com/hfpay/hfpay-excon-web/customers/nft/resources/%E5%8F%96%E7%8E%B0%E6%94%AF%E6%8C%81%E7%9A%84%E9%93%B6%E8%A1%8C%E4%BF%A1%E6%81%AF.xlsx + if(empty($this->data['bank_id'])) { + throw new ParamsException('开户银行代号不能为空', 'bank_id'); + } + if(empty($this->data['bank_acct_num'])) { + throw new ParamsException('开户银行账号不能为空', 'bank_acct_num'); + } + /*if(empty($this->data['bank_branch'])) { + throw new ParamsException('开户银行支行名称不能为空', 'bank_branch'); + }*/ + if(empty($this->data['bank_prov'])) { + throw new ParamsException('开户银行省份不能为空', 'bank_prov'); + } + if(empty($this->data['bank_area'])) { + throw new ParamsException('开户银行地区不能为空', 'bank_area'); + } + /* if(empty($this->data['industry'])) { + throw new ParamsException('企业行业类别不能为空', 'industry'); + }*/ + if(!empty($this->data['bg_ret_url'])) { + StrFilterHelper::fileUrl($this->data['bg_ret_url']); + } + if(empty($this->data['attach_nos'])) { + throw new ParamsException('附件信息不能为空', 'attach_nos'); + } + /*if(empty($this->data['mer_priv'])) { + throw new ParamsException('商户私有域不能为空', 'mer_priv'); + } + if(empty($this->data['extension'])) { + throw new ParamsException('扩展域不能为空', 'extension'); + } + if(empty($this->data['legal_nationality'])) { + throw new ParamsException('法人国籍不能为空', 'legal_nationality'); + }*/ + + + return $this->data; + } +} \ No newline at end of file diff --git a/src/factory/object/onelevel/CompanyResponse.php b/src/factory/object/onelevel/CompanyResponse.php new file mode 100644 index 0000000..4f7d544 --- /dev/null +++ b/src/factory/object/onelevel/CompanyResponse.php @@ -0,0 +1,14 @@ +data['order_date'])) { + throw new ParamsException('订单日期不能为空', 'order_date'); + } + if (empty($this->data['order_id'])) { + throw new ParamsException('订单号不能为空', 'order_id'); + } + if (empty($this->data['acct_usage_type'])) { + throw new ParamsException('账户用途不能为空', 'acct_usage_type'); + } + if (empty($this->data['user_id'])) { + throw new ParamsException('用户ID不能为空', 'user_id'); + } + if (empty($this->data['apply_id'])) { + throw new ParamsException('开户申请号不能为空', 'apply_id'); + } + if(empty($this->data['operate_type'])) { + throw new ParamsException('操作类型不能为空', 'operate_type'); + } + if(!in_array($this->data['operate_type'], [Constant::ONE_COM_OPERATE_TYPE_A, Constant::ONE_COM_OPERATE_TYPE_M])) { + throw new ParamsException('操作类型不正确', 'operate_type'); + } + if (empty($this->data['solo_name'])) { + throw new ParamsException('个体户名称不能为空', 'solo_name'); + } + if (empty($this->data['business_code'])) { + throw new ParamsException('营业执照注册号不能为空', 'business_code'); + } + if (empty($this->data['license_start_date'])) { + throw new ParamsException('证照起始日期不能为空', 'license_start_date'); + } + if (empty($this->data['license_end_date'])) { + throw new ParamsException('证照结束日期不能为空', 'license_end_date'); + } + if (empty($this->data['solo_business_address'])) { + throw new ParamsException('个体户经营地址不能为空', 'solo_business_address'); + } + /*if (empty($this->data['solo_reg_address'])) { + throw new ParamsException('个体户注册地址不能为空', 'solo_reg_address'); + }*/ + if (empty($this->data['solo_fixed_telephone'])) { + throw new ParamsException('个体户固定电话不能为空', 'solo_fixed_telephone'); + } + if (empty($this->data['business_scope'])) { + throw new ParamsException('经营范围不能为空', 'business_scope'); + } + if (empty($this->data['legal_name'])) { + throw new ParamsException('法人姓名不能为空', 'legal_name'); + } + if (empty($this->data['legal_id_card_type'])) { + throw new ParamsException('法人证件类型不能为空', 'legal_id_card_type'); + } + if (empty($this->data['legal_id_card'])) { + throw new ParamsException('法人证件号码不能为空', 'legal_id_card'); + } + if (empty($this->data['legal_cert_start_date'])) { + throw new ParamsException('法人证件起始日期不能为空', 'legal_cert_start_date'); + } + if (empty($this->data['legal_cert_end_date'])) { + throw new ParamsException('法人证件截止日期不能为空', 'legal_cert_end_date'); + } + if (empty($this->data['legal_mobile'])) { + throw new ParamsException('法人手机号码不能为空', 'legal_mobile'); + } + if (empty($this->data['contact_name'])) { + throw new ParamsException('企业联系人姓名不能为空', 'contact_name'); + } + if (empty($this->data['contact_mobile'])) { + throw new ParamsException('联系人手机号不能为空', 'contact_mobile'); + } + if (empty($this->data['contact_email'])) { + throw new ParamsException('联系人邮箱不能为空', 'contact_email'); + } + if (empty($this->data['occupation'])) { + throw new ParamsException('职业不能为空', 'occupation'); + } + + + if(empty($this->data['attach_nos'])) { + throw new ParamsException('附件信息不能为空', 'attach_nos'); + } + + if(!empty($this->data['bg_ret_url'])) { + StrFilterHelper::fileUrl($this->data['bg_ret_url']); + } + + /*if (empty($this->data['bank_acct_num'])) { + throw new ParamsException('银行卡号不能为空', 'bank_acct_num'); + } + if (empty($this->data['bank_prov'])) { + throw new ParamsException('省份不能为空', 'bank_prov'); + } + if (empty($this->data['bank_area'])) { + throw new ParamsException('地区不能为空', 'bank_area'); + } + + if (empty($this->data['legal_nationality'])) { + throw new ParamsException('法人国籍不能为空', 'legal_nationality'); + } + if(empty($this->data['mer_priv'])) { + throw new ParamsException('商户私有域不能为空', 'mer_priv'); + } + if(empty($this->data['extension'])) { + throw new ParamsException('扩展域不能为空', 'extension'); + }*/ + + return $this->data; + } +} \ No newline at end of file diff --git a/src/factory/object/onelevel/PeddlarResponse.php b/src/factory/object/onelevel/PeddlarResponse.php new file mode 100644 index 0000000..7246e0f --- /dev/null +++ b/src/factory/object/onelevel/PeddlarResponse.php @@ -0,0 +1,14 @@ +data['order_date'])) { + throw new ParamsException('订单日期不能为空', 'order_date'); + } + if(empty($this->data['order_id'])) { + throw new ParamsException('订单号不能为空', 'order_id'); + } + + if(empty($this->data['acct_usage_type'])) { + throw new ParamsException('账户用途不能为空', 'acct_usage_type'); + } + + if(empty($this->data['user_id'])) { + throw new ParamsException('用户ID不能为空', 'user_id'); + } + + if(empty($this->data['user_name'])) { + throw new ParamsException('用户姓名不能为空', 'user_name'); + } + + if(empty($this->data['id_card'])) { + throw new ParamsException('证件号不能为空', 'id_card'); + } + if(empty($this->data['id_card_type'])) { + throw new ParamsException('证件类型不能为空', 'id_card_type'); + } + if(!in_array($this->data['id_card_type'], [Constant::ID_TYPE_CARD])) { + throw new ParamsException('证件类型不支持', 'id_card_type'); + } + if(empty($this->data['user_mobile'])) { + throw new ParamsException('手机号不能为空', 'user_mobile'); + } + if(empty($this->data['card_num'])) { + throw new ParamsException('银行卡号不能为空', 'card_num'); + } + if(empty($this->data['attach_nos'])) { + throw new ParamsException('附件信息不能为空', 'attach_nos'); + } + /*if(empty($this->data['occupation'])) { + throw new ParamsException('职业不能为空', 'occupation'); + } + if(empty($this->data['mer_priv'])) { + throw new ParamsException('商户私有域不能为空', 'mer_priv'); + } + if(empty($this->data['extension'])) { + throw new ParamsException('扩展域不能为空', 'extension'); + }*/ + if(!empty($this->data['bg_ret_url'])) { + StrFilterHelper::fileUrl($this->data['bg_ret_url']); + } + + return $this->data; + } +} \ No newline at end of file diff --git a/src/factory/object/onelevel/PersonalResponse.php b/src/factory/object/onelevel/PersonalResponse.php new file mode 100644 index 0000000..823e8b6 --- /dev/null +++ b/src/factory/object/onelevel/PersonalResponse.php @@ -0,0 +1,13 @@ +data['attach_no'])) { + throw new ParamsException('附件编号不能为空', 'attach_no'); + } + if(empty($this->data['trans_type'])) { + throw new ParamsException('业务类型不能为空', 'trans_type'); + } + if(empty($this->data['attach_type'])) { + throw new ParamsException('附件类型不能为空', 'attach_type'); + } + if(empty($this->data['attach_desc'])) { + throw new ParamsException('文件描述不能为空', 'attach_desc'); + } + + if(empty($this->data['attach_file'])) { + throw new ParamsException('文件不能为空', 'attach_file'); + } + if(empty($this->data['handle_type'])) { + throw new ParamsException('上传类型不能为空', 'handle_type'); + } + return $this->data; + } +} \ No newline at end of file diff --git a/src/factory/object/onelevel/UploadResponse.php b/src/factory/object/onelevel/UploadResponse.php new file mode 100644 index 0000000..090fac6 --- /dev/null +++ b/src/factory/object/onelevel/UploadResponse.php @@ -0,0 +1,13 @@ +client->postJson($url, $data); } + public function uploadPost(string $url, array $data = []): Response + { + return $this->client->uploadPost($url, $data); + } } \ No newline at end of file diff --git a/src/http/IHttpClient.php b/src/http/IHttpClient.php index 5dee64e..5d2a0b2 100644 --- a/src/http/IHttpClient.php +++ b/src/http/IHttpClient.php @@ -18,4 +18,7 @@ interface IHttpClient function postJson(string $uri, ?array $data = []): Response; + function uploadPost(string $uri, ?array $data = []): Response; + + } \ No newline at end of file diff --git a/src/http/Response.php b/src/http/Response.php index fcbad98..a420d86 100644 --- a/src/http/Response.php +++ b/src/http/Response.php @@ -29,7 +29,8 @@ class Response if(!empty($self->source)) { $contentType = $response->getHeader('Content-Type'); if(empty($contentType)) { - throw new ApiException('反馈的消息未指明内容格式Content-Type'); + //throw new ApiException('反馈的消息未指明内容格式Content-Type'); + $contentType[0] = 'application/json; charset=utf-8'; } $bodyFormat = explode(';', $contentType[0]); switch ($bodyFormat[0]) { diff --git a/src/http/impl/HFPayHttpClient.php b/src/http/impl/HFPayHttpClient.php index fc6c3ff..53bebee 100644 --- a/src/http/impl/HFPayHttpClient.php +++ b/src/http/impl/HFPayHttpClient.php @@ -114,7 +114,61 @@ class HFPayHttpClient implements IHttpClient */ private ISigner $signClient; + /** + * 准备 Multipart 数据 + * 核心原则:文件流只进 multipart,不进 signData;文本字段两者都进 + */ + private function _prepareMultipartData(array $body): array + { + $multipart = []; + $signData = []; + + foreach ($body as $key => $value) { + // 1. 资源类型(文件流):仅放入 multipart,绝不参与签名 + if (is_resource($value)) { + $multipart[] = [ + 'name' => $key, + 'contents' => $value, + ]; + continue; + } + + // 2. 已是 Guzzle Multipart 格式的数组 + if (is_array($value) && isset($value['contents'])) { + $multipart[] = $value; + // 仅当 contents 非资源时才参与签名 + if (!is_resource($value['contents'])) { + $signData[$key] = (string)$value['contents']; + } + continue; + } + + // 3. 普通数组:转 JSON 字符串 + if (is_array($value)) { + $jsonStr = json_encode($value, JSON_UNESCAPED_UNICODE); + $multipart[] = ['name' => $key, 'contents' => $jsonStr]; + $signData[$key] = $jsonStr; + continue; + } + + // 4. NULL 值:转为空字符串(汇付通常要求空值也参与签名) + if ($value === null) { + $multipart[] = ['name' => $key, 'contents' => '']; + $signData[$key] = ''; + continue; + } + + // 5. 标量类型:强制转为字符串 + $strVal = (string)$value; + $multipart[] = ['name' => $key, 'contents' => $strVal]; + $signData[$key] = $strVal; + } + return [ + 'multipart' => $multipart, + 'sign_data' => $signData, + ]; + } /** * @param string $method * @param string $uri @@ -131,60 +185,70 @@ class HFPayHttpClient implements IHttpClient { $options = $this->getRequestOptions(); - if(!empty($query)) { + if (!empty($query)) { $options['query'] = array_filter($query, fn($v) => $v !== null); } - //添加固定的参数 - //版本号 - if(empty($body['version'])) { - $body['version'] = $this->config['version'] ?? ''; + + // ✅ 关键修正:固定参数必须在签名之前注入到 $body 中 + // 这样无论是 form/json 还是 multipart,都能保证这些字段参与签名 + if (empty($body['version'])) { + $body['version'] = $this->config['version'] ?? self::API_VERSION; } - if(empty($body['mer_cust_id'])) { + if (empty($body['mer_cust_id'])) { $body['mer_cust_id'] = $this->config['mid'] ?? ''; } - if(empty($body['bg_ret_url']) && !empty($this->config['callback'])) { + if (empty($body['bg_ret_url']) && !empty($this->config['callback'])) { $body['bg_ret_url'] = $this->config['callback']; } - //将内容生成签名 - $sign = $this->signClient->sign($body); - $body['check_value'] = $sign; + if ($format === 'multipart') { + // 1. 分离文件流和文本参数 + $processed = $this->_prepareMultipartData($body); -// echo '请求参数 #####'; -// print_r($body); -// echo '请求参数 end'; - if(empty($format)) { - $format = 'form'; - } - switch ($format) { - case 'json': - $options['json'] = array_filter($body, fn($v) => $v !== null); - break; - case 'form': - $options['form_params'] = array_filter($body, fn($v) => $v !== null); - break; - case 'body': - $options['body'] = array_filter($body, fn($v) => $v !== null); - break; - } + // 2. 使用纯文本参数生成签名(文件流已被排除) + $sign = $this->signClient->sign($processed['sign_data']); + + // 3. 将签名结果作为普通文本字段追加到 multipart + $processed['multipart'][] = [ + 'name' => 'check_value', + 'contents' => $sign, + ]; + $options['multipart'] = $processed['multipart']; + } else { + // 非 multipart 逻辑保持不变 + $sign = $this->signClient->sign($body); + $body['check_value'] = $sign; + + switch ($format) { + case 'json': + $options['json'] = array_filter($body, fn($v) => $v !== null); + break; + case 'form': + $options['form_params'] = array_filter($body, fn($v) => $v !== null); + break; + case 'body': + $options['body'] = json_encode(array_filter($body, fn($v) => $v !== null), JSON_UNESCAPED_UNICODE); + break; + } + } try { $resp = $this->client->request($method, $uri, $options); } catch (GuzzleException $e) { throw new ApiException($e->getMessage()); } - $response = Response::make($resp); - if(!$response->isSuccess()) { + $response = Response::make($resp); + if (!$response->isSuccess()) { throw new ApiException('汇付反馈状态不正确', $resp->getStatusCode()); } - //验证数据 - $body = $response->getBody(); - if(!$this->signClient->verify($body)) { + + $respBody = $response->getBody(); + if (!$this->signClient->verify($respBody)) { throw new ApiException('签名验证未通过'); } - //解码 - $decodeBody = $this->signClient->decode($body['check_value']); + + $decodeBody = $this->signClient->decode($respBody['check_value']); ErrCodeHelper::parseError($decodeBody['resp_code'], $decodeBody['resp_desc']); return Response::tempInit(200, $decodeBody); } @@ -273,4 +337,18 @@ class HFPayHttpClient implements IHttpClient { return $this->request('POST', $uri, [], $data, 'json', $needToken); } + /** + * 上传文件 + * @param string $uri + * @param array|null $data + * @return Response + * @throws ApiException + * @throws BizException + * @throws SignException + */ + function uploadPost(string $uri, ?array $data = []): Response + { + // 调用 request,并指定格式为 'multipart' + return $this->request('POST', $uri, [], $data, 'multipart'); + } } \ No newline at end of file