(string) $order['trade_no'], 'description' => (string) $order['subject'], // 微信金额单位是【分】,且必须是整数 'amount' => [ 'total' => (int) round((float) $order['amount'] * 100), 'currency' => 'CNY', ], ]; if (!empty($urls['notify'])) { $params['notify_url'] = (string) $urls['notify']; } $pay = PayFactory::wechat(); switch ($scene) { case 'h5': $params['scene_info'] = [ 'payer_client_ip' => (string) ($order['client_ip'] ?? (request()->ip() ?: '127.0.0.1')), 'h5_info' => ['type' => 'Wap'], ]; $result = $pay->h5($params); break; case 'mp': case 'mini': // JSAPI / 小程序必须带 openid,由业务侧传入(order['openid']) if (empty($order['openid'])) { throw new PayException('微信 JSAPI/小程序下单缺少 openid', 400); } $params['payer'] = ['openid' => (string) $order['openid']]; $result = $scene === 'mini' ? $pay->mini($params) : $pay->mp($params); break; case 'app': $result = $pay->app($params); break; default: $result = $pay->scan($params); } return ['trade_no' => (string) $order['trade_no']] + self::normalize($result); } public function query(string $tradeNo): array { $result = PayFactory::wechat()->query(['out_trade_no' => $tradeNo]); return [ 'paid' => (string) $result->get('trade_state', '') === 'SUCCESS', 'amount' => self::amount($result->get('amount')), 'gateway_no' => (string) $result->get('transaction_id', ''), ]; } public function notify(): array { $data = PayFactory::wechat()->callback(); return [ 'paid' => in_array((string) $data->get('trade_state', ''), ['SUCCESS', 'REFUND'], true), 'trade_no' => (string) $data->get('out_trade_no', ''), 'amount' => self::amount($data->get('amount')), 'gateway_no' => (string) $data->get('transaction_id', ''), ]; } public function ack(bool $ok = true): Response { $body = $ok ? json_encode(['code' => 'SUCCESS', 'message' => '成功'], JSON_UNESCAPED_UNICODE) : json_encode(['code' => 'FAIL', 'message' => '失败'], JSON_UNESCAPED_UNICODE); return response($body, $ok ? 200 : 500, ['Content-Type' => 'application/json']); } public function refund(string $tradeNo, float $amount, float $total): array { try { PayFactory::wechat()->refund([ 'out_trade_no' => $tradeNo, 'out_refund_no' => 'R' . $tradeNo . date('His'), 'amount' => [ 'refund' => (int) round($amount * 100), 'total' => (int) round($total * 100), 'currency' => 'CNY', ], ]); return ['ok' => true, 'message' => '退款申请已提交']; } catch (\Throwable $e) { return ['ok' => false, 'message' => $e->getMessage()]; } } /** * 微信金额字段有两种形态:数组 ['total'=>100,'currency'=>'CNY'](分)或标量。 */ private static function amount($amount): float { if (is_array($amount)) { return round(((int) ($amount['total'] ?? 0)) / 100, 2); } return round((float) $amount, 2); } /** * @return array{type: string, content: mixed} */ private static function normalize($result): array { if ($result instanceof Rocket) { $result = $result->getDestination() ?? $result->getPayload(); } if ($result instanceof MessageInterface) { return ['type' => 'form', 'content' => $result->getBody()->getContents()]; } if ($result instanceof Collection) { $qr = (string) ($result->get('code_url') ?: $result->get('qr_code') ?: ''); if ($qr !== '') { return ['type' => 'qrcode', 'content' => $qr]; } $url = (string) ($result->get('h5_url') ?: $result->get('mweb_url') ?: ''); if ($url !== '') { return ['type' => 'url', 'content' => $url]; } return ['type' => 'params', 'content' => $result->toArray()]; } if (is_string($result)) { return ['type' => 'form', 'content' => $result]; } throw new PayException('支付发起失败:网关返回了无法识别的响应', 500); } }