config = HuifuConfig::getInstance(); SdkLoader::boot(); } public function payment() { return new \BsPaySdk\core\Payment(); } public function postRequest($request, $tag = false) { $client = new \BsPaySdk\core\BsPayClient(); try { $result = $client->postRequest($request, $tag); } catch (\Throwable $e) { Log::error('[shopro_huifu] sdk postRequest exception: ' . $e->getMessage()); throw new \RuntimeException('汇付 SDK 请求异常:' . $e->getMessage(), 0, $e); } if (!$result) { Log::error('[shopro_huifu] sdk postRequest returned null'); } return $result; } public function verifyNotify($respData, $sign) { if (!$respData || !$sign) { return false; } $sign = HuifuNotifyParser::normalizeSign($sign); return BsPayTools::verifySign($sign, $respData, $this->config->getPublicKey()) === 1; } public function parseResponse($result) { return $this->extractResponseData($result, true); } public function extractResponseData($result, $assertSuccess = false) { if (!$result || $result->isError()) { $message = self::formatSdkError($result); Log::error('[shopro_huifu] sdk-error: ' . $message); throw new \RuntimeException($message); } $datas = $result->getRspDatas(); $data = $this->normalizeBusinessData($datas['data'] ?? $datas); if ($assertSuccess) { $this->assertBusinessSuccess($data); } return $data; } public function safeApiResult($result) { if (!$result || $result->isError()) { return [ 'ok' => false, 'message' => self::formatSdkError($result), 'data' => null, ]; } $data = $this->extractResponseData($result, false); return [ 'ok' => true, 'message' => (string) ($data['resp_desc'] ?? 'OK'), 'data' => $data, ]; } /** * 将 SDK 错误转为可读诊断信息($huifu-pay-integration / copilot-troubleshooting-playbooks) */ public static function formatSdkError($result) { if (!$result) { return '汇付 SDK 无响应(请检查 huifu_id、skill_source 及图片上传参数)'; } $error = $result->getErrorInfo(); $code = is_array($error) ? (string) ($error['code'] ?? '') : ''; $msg = is_array($error) ? (string) ($error['msg'] ?? '汇付 SDK 错误') : (string) $error; $http = method_exists($result, 'getHttpStateCode') ? (int) $result->getHttpStateCode() : 0; $parts = [$msg]; if ($code !== '') { $parts[] = 'code=' . $code; } if ($http > 0) { $parts[] = 'http=' . $http; } $rsp = method_exists($result, 'getRspDatas') ? $result->getRspDatas() : null; if (is_array($rsp)) { if (empty($rsp['sign'])) { $parts[] = '响应缺少 sign 字段'; } $inner = $rsp['data'] ?? null; if (is_string($inner) && $inner !== '') { $decoded = json_decode($inner, true); if (is_array($decoded)) { if (!empty($decoded['resp_desc'])) { $parts[] = '汇付提示=' . $decoded['resp_desc']; } if (!empty($decoded['resp_code'])) { $parts[] = 'resp_code=' . $decoded['resp_code']; } } else { $parts[] = 'data片段=' . self::previewText($inner); } } elseif (is_array($inner) && !empty($inner['resp_desc'])) { $parts[] = '汇付提示=' . $inner['resp_desc']; } } elseif (is_string($rsp) && $rsp !== '') { $parts[] = 'body片段=' . self::previewText($rsp); } if ($code === 'RESP_SIGN_VERIFY_UNDO' || $http === 401) { $parts[] = '排查: 1) 后台 env 与 sys_id/product_id/商户私钥必须同一环境(测试/生产不能混用)' . ' 2) 确认 SdkLoader 已按 env 设置 PROD_MODE' . ' 3) 商户私钥须为汇付控台商户RSA私钥' . ' 4) 汇付RSA公钥须为汇付平台公钥(不是商户公钥)'; } if ($code === 'RESP_SIGN_VERIFY_FAILED') { $parts[] = '排查: 汇付RSA公钥配置错误,请粘贴汇付控台「汇付公钥」而非商户公钥'; } return implode(';', $parts); } protected static function previewText($text) { $text = preg_replace('/\s+/', ' ', (string) $text); if (function_exists('mb_substr')) { return mb_substr($text, 0, 120); } return substr($text, 0, 120); } /** * 汇付同步应答的 data 可能是 array,也可能是 JSON 字符串。 */ public function normalizeBusinessData($data) { if (is_string($data)) { $decoded = json_decode($data, true); if (is_array($decoded)) { $data = $decoded; } } if (!is_array($data)) { throw new \RuntimeException('汇付返回 data 格式异常'); } return $data; } public function assertBusinessSuccess(array $data) { $respCode = (string) ($data['resp_code'] ?? ''); $transStat = (string) ($data['trans_stat'] ?? ''); // $huifu-pay-integration: 00000100=下单受理/处理中,T_MINIAPP 同步常见,不等于失败 $acceptedCodes = ['00000000', '00000100']; if ($respCode !== '' && !in_array($respCode, $acceptedCodes, true)) { throw new \RuntimeException($this->formatBusinessError($data)); } if ($transStat === 'F') { throw new \RuntimeException($this->formatBusinessError($data, '交易失败')); } } protected function formatBusinessError(array $data, $prefix = '汇付业务失败') { $parts = [$prefix]; if (!empty($data['resp_desc'])) { $parts[] = (string) $data['resp_desc']; } if (!empty($data['bank_message'])) { $parts[] = (string) $data['bank_message']; } if (!empty($data['resp_code'])) { $parts[] = 'resp_code=' . $data['resp_code']; } if (!empty($data['trans_stat'])) { $parts[] = 'trans_stat=' . $data['trans_stat']; } $respCode = (string) ($data['resp_code'] ?? ''); if ($respCode === '22000005' && strpos((string) ($data['resp_desc'] ?? ''), '入驻') !== false) { $parts[] = '排查: 1) 插件 huifu_id 须与控台该微信入驻商户一致 2) 渠道模式 sys_id=渠道商号、huifu_id=子商户号、私钥用渠道商 3) pay_scene:01=线上/02=线下或留空 4) 跑 php think shopro_huifu:probe-wechat 查汇付侧 AppID 绑定'; } return implode(',', $parts); } public function logBusinessData(array $data, $tag = 'huifu-response') { $safe = [ 'resp_code' => $data['resp_code'] ?? '', 'resp_desc' => $data['resp_desc'] ?? '', 'trans_stat' => $data['trans_stat'] ?? '', 'bank_message' => $data['bank_message'] ?? '', 'hf_seq_id' => $data['hf_seq_id'] ?? '', 'has_pay_info' => !empty($data['pay_info']), 'keys' => array_keys($data), ]; Log::error('[shopro_huifu] ' . $tag . ': ' . json_encode($safe, JSON_UNESCAPED_UNICODE)); } /** * 用一笔不存在的订单做查询,验证「请求签名 + 应答验签 + 网关环境」是否打通。 */ public function probeSignedApi() { SdkLoader::requireRequest('v4/payment/TradePaymentScanpayQueryRequest.php'); $request = new \BsPaySdk\request\v4\payment\TradePaymentScanpayQueryRequest(); $request->setHuifuId($this->config->get('huifu_id')); $request->setReqDate(date('Ymd')); $request->setReqSeqId('PROBE' . date('His') . mt_rand(1000, 9999)); $result = $this->payment()->query($request); if (!$result || $result->isError()) { return [ 'ok' => false, 'message' => self::formatSdkError($result), ]; } $data = $this->extractResponseData($result, false); $respCode = (string) ($data['resp_code'] ?? ''); $respDesc = (string) ($data['resp_desc'] ?? ''); // 探针查单:订单不存在 = 签名/验签/网关已打通 if ($respCode === '23000001' || strpos($respDesc, '交易不存在') !== false || strpos($respDesc, '订单不存在') !== false) { return [ 'ok' => true, 'message' => '应答验签通过(探针订单不存在属正常),resp_code=' . $respCode, 'data' => $data, ]; } return [ 'ok' => true, 'message' => '应答验签通过,resp_code=' . $respCode . ',' . $respDesc, 'data' => $data, ]; } public function queryWechatConfig($bankChannelNo = '') { SdkLoader::requireRequest('V2MerchantBusiConfigQueryRequest.php'); $request = new \BsPaySdk\request\V2MerchantBusiConfigQueryRequest(); $request->setHuifuId($this->config->get('huifu_id')); $request->setReqDate(date('Ymd')); $request->setReqSeqId('WXCFG' . date('YmdHis') . mt_rand(1000, 9999)); if ($bankChannelNo !== '') { $request->setExtendInfo(['bank_channel_no' => $bankChannelNo]); } return $this->safeApiResult($this->postRequest($request)); } public function queryWechatMercode($feeType = '01') { SdkLoader::requireRequest('V2MerchantBusiMercodeQueryRequest.php'); $request = new \BsPaySdk\request\V2MerchantBusiMercodeQueryRequest(); $request->setHuifuId($this->config->get('huifu_id')); $request->setReqDate(date('Ymd')); $request->setReqSeqId('WXMID' . date('YmdHis') . mt_rand(1000, 9999)); $request->setPayWay('W'); $request->setExtendInfo(['fee_type' => $feeType]); return $this->safeApiResult($this->postRequest($request)); } public function probeMiniAppCreate($subOpenid, $subAppid = '', $amount = '0.01') { SdkLoader::requireRequest('v4/payment/TradePaymentCreateRequest.php'); SdkLoader::requireRequest('v4/WxData.php'); $subOpenid = trim((string) $subOpenid); $subAppid = trim((string) $subAppid); if ($subAppid === '') { $subAppid = Bridge::getMiniProgramAppId(); } if ($subOpenid === '' || $subAppid === '') { return ['ok' => false, 'message' => '缺少 sub_openid 或 sub_appid', 'data' => null]; } $request = new \BsPaySdk\request\v4\payment\TradePaymentCreateRequest(); $request->setReqDate(date('Ymd')); $request->setReqSeqId('PROBE' . date('YmdHis') . mt_rand(1000, 9999)); $request->setHuifuId($this->config->get('huifu_id')); $request->setTradeType('T_MINIAPP'); $request->setTransAmt(number_format((float) $amount, 2, '.', '')); $request->setGoodsDesc('汇付探针订单'); $wxData = new \BsPaySdk\request\v4\WxData(); $wxData->setSubAppid($subAppid); $wxData->setSubOpenid($subOpenid); $wxData->setBody('汇付探针订单'); $request->setMethodExpand(\BsPaySdk\core\Payment::arrayToJsonString($wxData)); $payScene = trim((string) $this->config->get('pay_scene')); $channelNo = trim((string) $this->config->get('channel_no')); if ($payScene !== '') { $request->setPayScene($payScene); } if ($channelNo !== '') { $request->setChannelNo($channelNo); } $feeSign = trim((string) $this->config->get('fee_sign')); if ($feeSign !== '') { $request->setFeeFlag($feeSign); } try { $result = $this->payment()->create($request); $data = $this->extractResponseData($result, true); return ['ok' => true, 'message' => '下单成功', 'data' => $data]; } catch (\Exception $e) { return ['ok' => false, 'message' => $e->getMessage(), 'data' => null]; } } public static function analyzeCredentials(HuifuConfig $config) { $sysId = trim((string) $config->get('sys_id')); $huifuId = trim((string) $config->get('huifu_id')); $upper = trim((string) $config->get('upper_huifu_id')); $lines = []; if ($sysId === '' || $huifuId === '') { $lines[] = '[FAIL] sys_id 或 huifu_id 为空'; return $lines; } if ($sysId === $huifuId) { $lines[] = '[OK] 直连模式:sys_id 与 huifu_id 相同,私钥应为该商户密钥'; } else { $lines[] = '[INFO] 渠道模式:sys_id(' . $sysId . ') ≠ huifu_id(' . $huifuId . ')'; $lines[] = ' 私钥通常填渠道商(sys_id)密钥;huifu_id 填实际收款子商户'; if ($upper === '') { $lines[] = '[WARN] upper_huifu_id 为空,若报入驻/权限错误请补渠道商号'; } } return $lines; } }