status !== 'wait_pay') { throw new \RuntimeException('订单状态不正确'); } $merchant = self::selectWechatMerchantForJsapi($order, $appId); if (!$merchant) { throw new \RuntimeException('未配置可用的微信商户号'); } $notifyUrl = (string)($merchant->notify_url ?? ''); if ($notifyUrl === '') { $notifyUrl = (string)(getenv('WECHATPAY_NOTIFY_URL') ?: ''); } if ($notifyUrl === '') { throw new \RuntimeException('回调地址未配置'); } $outTradeNo = self::genOutTradeNo($order); $amountFen = (int)round(((float)$order->total_price) * 100); $description = '安心验-鉴定订单 ' . $order->order_no; $openid = trim($openid); if ($openid === '') { $identity = UserWechatIdentity::where('user_id', $order->user_id)->where('app_id', $appId)->first(); if ($identity) { $openid = (string)$identity->openid; } } if ($openid === '') { throw new \RuntimeException('缺少 openid,无法发起 JSAPI 支付'); } DB::beginTransaction(); try { $tx = PaymentTransaction::create([ 'order_id' => $order->id, 'channel' => 'wechat', 'merchant_id' => $merchant->id, 'out_trade_no' => $outTradeNo, 'amount' => $order->total_price, 'status' => 'created', ]); $order->pay_channel = 'wechat'; $order->pay_status = 'paying'; $order->pay_merchant_id = $merchant->id; $order->pay_out_trade_no = $outTradeNo; $order->save(); $client = new WechatPayV3Client($merchant); $resp = $client->createJsapiTransaction($outTradeNo, $description, $amountFen, $notifyUrl, $openid); $tx->prepay_id = $resp['prepay_id'] ?? null; $tx->raw_json = $resp; $tx->save(); if (!$tx->prepay_id) { throw new \RuntimeException('微信支付下单失败:缺少 prepay_id'); } $payParams = $client->buildJsapiPayParams($appId, $tx->prepay_id); DB::commit(); return [ 'channel' => 'wechat', 'pay_type' => 'jsapi', 'out_trade_no' => $outTradeNo, 'merchant' => [ 'id' => $merchant->id, 'name' => $merchant->name, 'mode' => $merchant->mode, 'mch_id' => $merchant->mch_id, ], 'pay_params' => $payParams, ]; } catch (\Throwable $e) { DB::rollBack(); throw $e; } } public static function createWechatNativePay(Order $order): array { if ($order->status !== 'wait_pay') { throw new \RuntimeException('订单状态不正确'); } $merchant = self::selectWechatMerchant($order); if (!$merchant) { throw new \RuntimeException('未配置可用的微信商户号'); } $notifyUrl = (string)($merchant->notify_url ?? ''); if ($notifyUrl === '') { $notifyUrl = (string)(getenv('WECHATPAY_NOTIFY_URL') ?: ''); } if ($notifyUrl === '') { throw new \RuntimeException('回调地址未配置'); } $outTradeNo = self::genOutTradeNo($order); $amountFen = (int)round(((float)$order->total_price) * 100); $description = '安心验-鉴定订单 ' . $order->order_no; DB::beginTransaction(); try { $tx = PaymentTransaction::create([ 'order_id' => $order->id, 'channel' => 'wechat', 'merchant_id' => $merchant->id, 'out_trade_no' => $outTradeNo, 'amount' => $order->total_price, 'status' => 'created', ]); $order->pay_channel = 'wechat'; $order->pay_status = 'paying'; $order->pay_merchant_id = $merchant->id; $order->pay_out_trade_no = $outTradeNo; $order->save(); $client = new WechatPayV3Client($merchant); $resp = $client->createNativeTransaction($outTradeNo, $description, $amountFen, $notifyUrl); $tx->prepay_id = $resp['prepay_id'] ?? null; $tx->code_url = $resp['code_url'] ?? null; $tx->raw_json = $resp; $tx->save(); DB::commit(); return [ 'channel' => 'wechat', 'pay_type' => 'native', 'out_trade_no' => $outTradeNo, 'code_url' => $tx->code_url, 'merchant' => [ 'id' => $merchant->id, 'name' => $merchant->name, 'mode' => $merchant->mode, 'mch_id' => $merchant->mch_id, ], ]; } catch (\Throwable $e) { DB::rollBack(); throw $e; } } public static function selectWechatMerchant(Order $order): ?WechatMerchant { $list = WechatMerchant::where('status', 1) ->whereIn('mode', ['direct', 'service_provider']) ->whereNotNull('serial_no')->where('serial_no', '<>', '') ->whereNotNull('api_v3_key')->where('api_v3_key', '<>', '') ->where(function ($q) { $q->where(function ($q2) { $q2->whereNotNull('private_key_pem')->where('private_key_pem', '<>', ''); })->orWhere(function ($q2) { $q2->whereNotNull('apiclient_key_path')->where('apiclient_key_path', '<>', ''); }); }) ->orderByDesc('is_default') ->orderBy('id') ->get(); if ($list->count() === 0) return null; if ($list->count() === 1) return $list->first(); $default = $list->firstWhere('is_default', 1); if ($default) return $default; $idx = abs(crc32((string)$order->order_no)) % $list->count(); return $list->values()->get($idx); } public static function selectWechatMerchantForJsapi(Order $order, string $appId): ?WechatMerchant { $appId = trim($appId); if ($appId === '') { throw new \RuntimeException('缺少 app_id'); } $list = WechatMerchant::where('status', 1) ->whereIn('mode', ['direct', 'service_provider']) ->whereNotNull('serial_no')->where('serial_no', '<>', '') ->whereNotNull('api_v3_key')->where('api_v3_key', '<>', '') ->where(function ($q) { $q->where(function ($q2) { $q2->whereNotNull('private_key_pem')->where('private_key_pem', '<>', ''); })->orWhere(function ($q2) { $q2->whereNotNull('apiclient_key_path')->where('apiclient_key_path', '<>', ''); }); }) ->get() ->filter(function ($m) use ($appId) { $mode = (string)($m->mode ?? ''); if ($mode === 'direct') { return (string)($m->app_id ?? '') === $appId; } if ($mode === 'service_provider') { $subAppId = (string)($m->sub_app_id ?? ''); if ($subAppId !== '') return $subAppId === $appId; return (string)($m->app_id ?? '') === $appId; } return false; }) ->values(); if ($list->count() === 0) return null; if ($list->count() === 1) return $list->first(); $default = $list->firstWhere('is_default', 1); if ($default) return $default; $idx = abs(crc32((string)$order->order_no)) % $list->count(); return $list->get($idx); } private static function genOutTradeNo(Order $order): string { return 'AXY' . date('YmdHis') . $order->id . random_int(1000, 9999); } private static function resolveJsapiAppId(WechatMerchant $merchant): string { $mode = (string)($merchant->mode ?? 'direct'); $appId = (string)($merchant->app_id ?? ''); $subAppId = (string)($merchant->sub_app_id ?? ''); if ($mode === 'direct') { if ($appId === '') throw new \RuntimeException('商户 AppID 未配置'); return $appId; } if ($mode === 'service_provider') { if ($subAppId !== '') return $subAppId; if ($appId === '') throw new \RuntimeException('服务商 AppID 未配置'); return $appId; } throw new \RuntimeException('该商户类型暂不支持 JSAPI'); } }