config = HuifuConfig::getInstance(); $this->client = new HuifuClient(); } public function miniAppPay($order, array $orderData, $platform = 'wxMiniProgram') { SdkLoader::requireRequest('v4/payment/TradePaymentCreateRequest.php'); SdkLoader::requireRequest('v4/WxData.php'); $openid = trim((string) ($orderData['openid'] ?? '')); if ($openid === '') { throw new \RuntimeException('缺少 sub_openid,请确认小程序用户已绑定微信'); } $appId = Bridge::getMiniProgramAppId(); if ($appId === '') { throw new \RuntimeException('商城未配置小程序 AppID'); } $reqDate = date('Ymd'); $reqSeqId = $orderData['out_trade_no']; $transAmt = number_format((float) $orderData['total_fee'], 2, '.', ''); $request = new \BsPaySdk\request\v4\payment\TradePaymentCreateRequest(); $request->setReqDate($reqDate); $request->setReqSeqId($reqSeqId); $request->setHuifuId($this->config->get('huifu_id')); $request->setTradeType('T_MINIAPP'); $request->setTransAmt($transAmt); $request->setGoodsDesc($this->config->formatGoodsDesc($reqSeqId)); $notifyUrl = $this->config->getNotifyUrl('pay', $platform); $request->setNotifyUrl($notifyUrl); if ($this->config->shouldDelayAcct()) { $request->setDelayAcctFlag('Y'); } $wxData = new \BsPaySdk\request\v4\WxData(); $wxData->setSubAppid($appId); $wxData->setSubOpenid($openid); $wxData->setBody($this->config->formatGoodsDesc($reqSeqId)); $request->setMethodExpand(\BsPaySdk\core\Payment::arrayToJsonString($wxData)); $timeExpire = $this->resolveTimeExpire(); if ($timeExpire !== '') { $request->setTimeExpire($timeExpire); } if ($this->config->get('pay_scene')) { $request->setPayScene($this->config->get('pay_scene')); } if ($this->config->get('channel_no')) { $request->setChannelNo($this->config->get('channel_no')); } if ($this->config->get('fee_sign')) { $request->setFeeFlag($this->config->get('fee_sign')); } if ($this->config->get('limit_pay_type')) { $request->setLimitPayType($this->config->get('limit_pay_type')); } Log::info('[shopro_huifu] create-request: ' . json_encode([ 'huifu_id' => $this->config->get('huifu_id'), 'trade_type' => 'T_MINIAPP', 'sub_appid' => $appId, 'sub_openid' => substr($openid, 0, 6) . '***', 'pay_scene' => trim((string) $this->config->get('pay_scene')) ?: null, 'channel_no' => trim((string) $this->config->get('channel_no')) ?: null, 'req_seq_id' => $reqSeqId, 'notify_url' => $notifyUrl, ], JSON_UNESCAPED_UNICODE)); $result = $this->client->payment()->create($request); $data = $this->client->parseResponse($result); $payData = $this->buildPayData($data); $this->persistOrderAfterCreate($order, $reqSeqId, $reqDate, $data); return [ 'pay_data' => $payData, 'pay_action' => 'miniapp', ]; } protected function buildPayData(array $data) { $payInfo = $this->extractPayInfo($data); $decoded = $this->decodePayInfoPayload($payInfo); if (is_array($decoded)) { return $this->normalizeWechatPayData($decoded); } $this->client->logBusinessData($data, 'missing-pay_info'); throw new \RuntimeException($this->formatMissingPayInfoError($data)); } protected function extractPayInfo(array $data) { foreach (['pay_info', 'payInfo'] as $key) { if (!empty($data[$key])) { return $data[$key]; } } foreach (['wx_response', 'wx_resp', 'method_expand'] as $key) { if (empty($data[$key])) { continue; } $nested = $data[$key]; if (is_string($nested)) { $nested = json_decode($nested, true); } if (!is_array($nested)) { continue; } foreach (['pay_info', 'payInfo'] as $innerKey) { if (!empty($nested[$innerKey])) { return $nested[$innerKey]; } } } return ''; } protected function decodePayInfoPayload($payInfo) { if (is_array($payInfo)) { return $payInfo; } if (!is_string($payInfo) || $payInfo === '') { return null; } foreach ([$payInfo, stripslashes($payInfo), urldecode($payInfo)] as $candidate) { $decoded = json_decode($candidate, true); if (is_array($decoded)) { return $decoded; } } return null; } protected function persistOrderAfterCreate($order, $reqSeqId, $reqDate, array $data) { try { $this->savePayExt($order['id'], $reqSeqId, [ 'req_date' => $reqDate, 'req_seq_id' => $reqSeqId, 'hf_seq_id' => $data['hf_seq_id'] ?? '', 'trans_stat' => $data['trans_stat'] ?? 'P', 'delay_acct_flag' => $this->config->shouldDelayAcct() ? 'Y' : 'N', ]); Db::name('shopro_order')->where('id', $order['id'])->update([ 'order_sn' => $reqSeqId, 'pay_type' => 'huifu_wechat', ]); } catch (\Throwable $e) { $msg = trim($e->getMessage()); Log::error('[shopro_huifu] order persist failed: ' . $msg); if ($msg !== '' && (stripos($msg, 'pay_type') !== false || stripos($msg, 'Data truncated') !== false)) { throw new \RuntimeException('订单表 pay_type 未升级,请在服务器执行: php think shopro_huifu:repair-menu'); } if ($msg !== '' && stripos($msg, 'shopro_huifu_pay_ext') !== false) { throw new \RuntimeException('汇付扩展表未安装,请在服务器执行: php think shopro_huifu:repair-menu'); } } } protected function formatMissingPayInfoError(array $data) { $msg = '汇付未返回 pay_info'; if (!empty($data['resp_desc'])) { $msg .= ':' . $data['resp_desc']; } if (!empty($data['bank_message'])) { $msg .= '(' . $data['bank_message'] . ')'; } if (!empty($data['trans_stat'])) { $msg .= ',trans_stat=' . $data['trans_stat']; } $msg .= '。请核对:1) 小程序 AppID 与 sub_openid 是否匹配;2) 汇付商户是否已开通微信小程序通道;3) pay_scene/channel_no 是否配置正确'; return $msg; } protected function normalizeWechatPayData(array $payInfo) { $map = [ 'timeStamp' => ['timeStamp', 'timestamp'], 'nonceStr' => ['nonceStr', 'nonce_str'], 'package' => ['package', 'packageValue', 'prepay_id'], 'signType' => ['signType', 'sign_type'], 'paySign' => ['paySign', 'pay_sign'], ]; $result = []; foreach ($map as $target => $candidates) { foreach ($candidates as $key) { if (!isset($payInfo[$key]) || $payInfo[$key] === '') { continue; } $value = (string) $payInfo[$key]; if ($target === 'package' && strpos($value, 'prepay_id=') !== 0 && $key === 'prepay_id') { $value = 'prepay_id=' . $value; } $result[$target] = $value; break; } } if (count($result) < 5) { $missing = []; foreach (array_keys($map) as $target) { if (!isset($result[$target])) { $missing[] = $target; } } throw new \RuntimeException('pay_info 字段不完整,缺少: ' . implode(', ', $missing)); } return $result; } public static function savePayExt($orderId, $orderSn, array $fields) { $now = time(); $row = null; if ($orderId) { $row = Db::name('shopro_huifu_pay_ext')->where('order_id', $orderId)->find(); } if (!$row && $orderSn) { $row = Db::name('shopro_huifu_pay_ext')->where('order_sn', $orderSn)->find(); if ($row && $orderId) { $fields['order_id'] = $orderId; } } $splitTiming = Db::name('shopro_split_config')->where('name', 'split_timing')->value('value'); $payload = array_merge($fields, [ 'order_sn' => $orderSn, 'split_timing_at_pay' => $splitTiming ?: 'payed', 'updatetime' => $now, ]); if ($row) { Db::name('shopro_huifu_pay_ext')->where('id', $row['id'])->update($payload); } else { $payload['order_id'] = $orderId; $payload['createtime'] = $now; Db::name('shopro_huifu_pay_ext')->insert($payload); } } public static function getPayExt($orderId) { return Db::name('shopro_huifu_pay_ext')->where('order_id', $orderId)->find(); } protected function resolveTimeExpire() { $raw = trim((string) $this->config->get('time_expire')); if ($raw === '') { return ''; } if (preg_match('/^\d{14}$/', $raw)) { return $raw; } if (ctype_digit($raw) && (int) $raw > 0 && (int) $raw <= 1440) { return date('YmdHis', time() + (int) $raw * 60); } return $raw; } }