first commit
This commit is contained in:
255
app/common/service/PaymentService.php
Normal file
255
app/common/service/PaymentService.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user