256 lines
9.1 KiB
PHP
256 lines
9.1 KiB
PHP
<?php
|
||
namespace app\common\service;
|
||
|
||
use app\common\model\Order;
|
||
use app\common\model\PaymentTransaction;
|
||
use app\common\model\WechatMerchant;
|
||
use app\common\model\UserWechatIdentity;
|
||
use Illuminate\Database\Capsule\Manager as DB;
|
||
|
||
class PaymentService
|
||
{
|
||
public static function createWechatJsapiPay(Order $order, string $appId, string $openid = ''): array
|
||
{
|
||
if ($order->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');
|
||
}
|
||
}
|