小程序支付

This commit is contained in:
liu
2026-09-03 20:36:09 +08:00
parent d190bdaade
commit 1370926780
7 changed files with 874 additions and 21 deletions
+127 -18
View File
@@ -13,34 +13,57 @@ use Illuminate\Support\Facades\Log;
use Throwable;
/**
* 在线支付编排服务(旺铺网关
* 在线支付编排服务(渠道二选一:旺铺网关 / 微信官方支付
*
* 链路:门店小程序选择账单合并付款
* → code2session 换 openid → 校验并锁定账单 → 创建支付单(pay_type=2
* → 旺铺统一下单 → 返回调起支付参数 → 小程序 wx.requestPayment
* → 按当前渠道(site_config pay.online_channel)走旺铺统一下单或微信 JSAPI 下单
* → 返回调起支付参数 → 小程序 wx.requestPayment
* → 网关后台通知 / 小程序主动查询 → settle() 幂等结账:
* 支付单置成功 + 关联账单批量置已支付 + 累加门店总采购金额(只统计商品金额)+ 通知门店
*
* 已创建的支付单按自身 pay_method 固定在原渠道查询/结账(渠道切换不影响进行中的支付单)。
* 结账口径与后台「支付审核通过 / 线下收款登记」保持一致。
*/
class OnlinePaymentService
{
/** 在线支付渠道:旺铺支付网关 */
public const string CHANNEL_WANGPU = 'wangpu';
/** 在线支付渠道:微信官方支付 */
public const string CHANNEL_WECHAT = 'wechat';
public function __construct(
protected BillNumberService $billNumber,
protected WangpuPayService $wangpu,
protected WechatPayService $wxpay,
protected WechatMiniService $wechat,
) {}
/**
* 当前在线支付渠道(site_config pay.online_channel 优先,回退 env PAY_ONLINE_CHANNEL,默认旺铺)
*/
public function channel(): string
{
$channel = (string) site_config('pay.online_channel', '');
if ($channel === '') {
$channel = (string) config('services.pay.online_channel', self::CHANNEL_WANGPU);
}
return $channel === self::CHANNEL_WECHAT ? self::CHANNEL_WECHAT : self::CHANNEL_WANGPU;
}
/**
* 发起在线支付
*
* @param array<int, int> $billIds 合并付款的账单ID
* @param string $code 小程序 wx.login() 返回的登录凭证
* @return array{0: PaymentModel, 1: array<string, mixed>} 支付单与旺铺返回的调起支付参数
* @return array{0: PaymentModel, 1: array<string, mixed>} 支付单与渠道返回的调起支付参数
* @throws Throwable
*/
public function create(StoreModel $store, array $billIds, string $code, string $remark = ''): array
{
$channel = $this->channel();
$payMethod = $channel === self::CHANNEL_WECHAT ? PaymentModel::METHOD_WECHAT : PaymentModel::METHOD_WANGPU;
// 换取付款人 openid 并绑定到门店(下次可直接复用)
$openid = $this->wechat->code2session($code);
if ((string) $store->openid !== $openid) {
@@ -48,7 +71,7 @@ class OnlinePaymentService
$store->save();
}
[$payment, $bills] = DB::transaction(function () use ($store, $billIds, $openid, $remark) {
[$payment, $bills] = DB::transaction(function () use ($store, $billIds, $openid, $remark, $payMethod) {
$bills = BillModel::query()
->where('store_id', $store->id)
->whereIn('id', $billIds)
@@ -79,7 +102,7 @@ class OnlinePaymentService
'store_id' => $store->id,
'amount' => $amount,
'pay_type' => PaymentModel::TYPE_ONLINE,
'pay_method' => PaymentModel::METHOD_WANGPU,
'pay_method' => $payMethod,
'voucher_ids' => '',
'status' => PaymentModel::STATUS_PENDING,
'openid' => $openid,
@@ -93,21 +116,30 @@ class OnlinePaymentService
});
try {
$gatewayData = $this->wangpu->createOrder([
'mer_order_id' => $payment->payment_no,
'order_amt' => (string) $payment->amount,
'open_id' => $openid,
'sub_appid' => $this->subAppid(),
'order_title' => '账单合并付款-' . $payment->payment_no,
'notifyurl' => $this->wangpu->notifyUrl(),
]);
$gatewayData = $channel === self::CHANNEL_WECHAT
? $this->wxpay->createOrder(
$payment->payment_no,
(string) $payment->amount,
$openid,
'账单合并付款-' . $payment->payment_no,
)
: $this->wangpu->createOrder([
'mer_order_id' => $payment->payment_no,
'order_amt' => (string) $payment->amount,
'open_id' => $openid,
'sub_appid' => $this->subAppid(),
'order_title' => '账单合并付款-' . $payment->payment_no,
'notifyurl' => $this->wangpu->notifyUrl(),
]);
} catch (Throwable $e) {
// 网关下单失败:整笔作废并释放账单,门店可重新发起
$this->discard($payment, $e->getMessage());
throw $e;
}
$payment->order_id = (string) ($gatewayData['order_id'] ?? '');
if ($channel === self::CHANNEL_WANGPU) {
$payment->order_id = (string) ($gatewayData['order_id'] ?? '');
}
$payment->pay_params = $gatewayData;
$payment->save();
@@ -115,7 +147,7 @@ class OnlinePaymentService
}
/**
* 支付成功后台通知处理:验签由控制器完成,此处按 mer_order_id 定位支付单并幂等结账
* 旺铺支付成功后台通知处理:验签由控制器完成,此处按 mer_order_id 定位支付单并幂等结账
*
* @param array<string, mixed> $params 通知报文(已验签)
* @return bool 本次是否执行了结账(false = 重复通知)
@@ -145,9 +177,46 @@ class OnlinePaymentService
}
/**
* 主动查询网关订单状态:已支付则结账(回调延迟/丢失时的兜底,小程序支付完成后调用)
* 微信支付成功后台通知处理:验签/解密由 WechatPayService 完成,此处按 out_trade_no 定位支付单并幂等结账
*
* @return array{payment: PaymentModel, order_status: int} 最新支付单与网关订单状态
* @param array<string, mixed> $params 解密后的交易报文(out_trade_no/trade_state/transaction_id/success_time/amount
* @return bool 本次是否执行了结账(false = 重复通知)
* @throws Throwable
*/
public function settleByWechatNotify(array $params): bool
{
$outTradeNo = (string) ($params['out_trade_no'] ?? '');
$payment = PaymentModel::query()
->where('payment_no', $outTradeNo)
->where('pay_type', PaymentModel::TYPE_ONLINE)
->first();
if ($payment === null) {
throw new RepositoryException('支付记录不存在:' . $outTradeNo);
}
if (($params['trade_state'] ?? '') !== WechatPayService::TRADE_STATE_SUCCESS) {
throw new RepositoryException('订单未支付成功(trade_state=' . ($params['trade_state'] ?? '空') . '');
}
// 商户号一致性校验,防串号
$mchId = (string) ($params['mchid'] ?? '');
if ($mchId !== '' && $mchId !== $this->wxpayMchId()) {
throw new RepositoryException('通知商户号与配置不一致');
}
return $this->settle(
$payment,
(string) ($params['transaction_id'] ?? ''),
'',
(string) ($params['success_time'] ?? ''),
bcdiv((string) (int) ($params['amount']['total'] ?? 0), '100', 2),
);
}
/**
* 主动查询渠道订单状态:已支付则结账(回调延迟/丢失时的兜底,小程序支付完成后调用)
*
* 按支付单的 pay_method 固定在原渠道查询,渠道切换不影响进行中的支付单
*
* @return array{payment: PaymentModel, order_status: int} 最新支付单与渠道订单状态(1=已支付)
* @throws Throwable
*/
public function queryAndSettle(PaymentModel $payment): array
@@ -156,6 +225,10 @@ class OnlinePaymentService
return ['payment' => $payment, 'order_status' => WangpuPayService::ORDER_STATUS_PAID];
}
if ($payment->pay_method === PaymentModel::METHOD_WECHAT) {
return $this->queryWechatAndSettle($payment);
}
$data = $this->wangpu->queryOrder($payment->payment_no, (string) $payment->order_id);
$orderStatus = (int) ($data['order_status'] ?? -1);
@@ -172,6 +245,30 @@ class OnlinePaymentService
return ['payment' => $payment->fresh(), 'order_status' => $orderStatus];
}
/**
* 微信渠道主动查询:trade_state=SUCCESS 则结账
*
* @return array{payment: PaymentModel, order_status: int}
* @throws Throwable
*/
protected function queryWechatAndSettle(PaymentModel $payment): array
{
$data = $this->wxpay->queryOrder($payment->payment_no);
$paid = ($data['trade_state'] ?? '') === WechatPayService::TRADE_STATE_SUCCESS;
if ($paid) {
$this->settle(
$payment,
(string) ($data['transaction_id'] ?? ''),
'',
(string) ($data['success_time'] ?? ''),
bcdiv((string) (int) ($data['amount']['total'] ?? 0), '100', 2),
);
}
return ['payment' => $payment->fresh(), 'order_status' => $paid ? 1 : 0];
}
/**
* 幂等结账(网关通知与主动查询共用入口,内部事务 + 行锁防重)
*
@@ -208,7 +305,7 @@ class OnlinePaymentService
'status' => BillModel::STATUS_PAID,
'paid_at' => $paidAt,
'paid_operator_id' => 0,
'pay_remark' => PaymentModel::METHOD_NAMES[PaymentModel::METHOD_WANGPU] . '(支付单号 ' . $payment->payment_no . '',
'pay_remark' => (PaymentModel::METHOD_NAMES[$payment->pay_method] ?? '在线支付') . '(支付单号 ' . $payment->payment_no . '',
]);
// 按门店累加总采购金额(只统计商品金额,不含配送费/附加金额)
@@ -307,4 +404,16 @@ class OnlinePaymentService
}
return trim($subAppid);
}
/**
* 微信渠道商户号(用于通知商户号一致性校验)
*/
protected function wxpayMchId(): string
{
$mchId = (string) site_config('pay.wxpay_mch_id', '');
if ($mchId === '') {
$mchId = (string) config('services.wxpay.mch_id', '');
}
return trim($mchId);
}
}