first commit

This commit is contained in:
wushumin
2026-04-16 11:17:18 +08:00
commit 5b9c398e68
98 changed files with 8701 additions and 0 deletions

View 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');
}
}