Files
appraisal_center_api/app/common/service/PaymentService.php
2026-04-16 11:17:18 +08:00

256 lines
9.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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');
}
}