Files
xin-procurement/app/Http/Controllers/Mini/OnlinePaymentController.php
T
2026-09-04 23:30:13 +08:00

183 lines
7.2 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\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Models\PaymentModel;
use App\Services\OnlinePaymentService;
use App\Services\WangpuPayService;
use App\Services\WechatPayService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Throwable;
/**
* 小程序在线支付(渠道二选一:旺铺网关 JSAPI / 微信官方支付 APIv3;场景二选一:小程序 / 公众号 H5)
*
* 链路:POST /mini/payment/online 下单(scene=mini 小程序 / scene=mp 公众号,返回调起支付参数)
* → 小程序 wx.requestPayment / 公众号 H5 WeixinJSBridge 完成支付
* → 渠道后台通知(旺铺 /mini/payment/notify;微信 /mini/payment/wechat-notify)验签 + 幂等结账
* → 小程序 GET /mini/payment/online/{paymentNo}/query 主动同步支付结果(回调兜底)
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class OnlinePaymentController extends BaseMiniController
{
public function __construct(
protected OnlinePaymentService $onlinePayment,
protected WangpuPayService $wangpu,
protected WechatPayService $wxpay,
) {}
/**
* 发起在线支付:合并选择本店未支付账单 → 渠道下单 → 返回调起支付参数
*
* scene=mini(默认):小程序 wx.login code 换 openidwx.requestPayment 调起;
* scene=mp:公众号 H5 网页授权 code 换 openidWeixinJSBridge getBrandWCPayRequest 调起。
* @throws Throwable
*/
#[PostRoute('/payment/online', authorize: true)]
public function create(Request $request): JsonResponse
{
$data = $request->validate([
'bill_ids' => 'required|array|min:1',
'bill_ids.*' => 'integer|distinct',
'code' => 'required|string|max:64',
'scene' => 'nullable|string|in:mini,mp',
'remark' => 'nullable|string|max:255',
], [
'bill_ids.required' => '请选择要付款的账单',
'bill_ids.min' => '请选择要付款的账单',
'code.required' => '微信登录/授权凭证缺失,请重新进入',
'scene.in' => '支付场景不正确',
'remark.max' => '备注超过最大长度',
]);
$store = $this->currentStore($request);
[$payment, $payParams] = $this->onlinePayment->create(
$store,
array_map('intval', $data['bill_ids']),
(string) $data['code'],
(string) ($data['remark'] ?? ''),
(string) ($data['scene'] ?? OnlinePaymentService::SCENE_MINI),
);
return $this->success([
'id' => $payment->id,
'payment_no' => $payment->payment_no,
'amount' => $payment->amount,
'pay_params' => $payParams,
], '下单成功,请调起支付');
}
/**
* 查询支付结果:网关回调可能延迟/丢失,小程序完成支付后主动调用同步结账
* @throws Throwable
*/
#[GetRoute('/payment/online/{paymentNo}/query', authorize: true)]
public function query(string $paymentNo, Request $request): JsonResponse
{
$store = $this->currentStore($request);
$payment = PaymentModel::query()
->where('store_id', $store->id)
->where('payment_no', $paymentNo)
->where('pay_type', PaymentModel::TYPE_ONLINE)
->first();
if ($payment === null) {
throw new RepositoryException('支付记录不存在');
}
$result = $this->onlinePayment->queryAndSettle($payment);
$payment = $result['payment'];
return $this->success([
'payment_no' => $payment->payment_no,
'status' => $payment->status,
'status_name' => PaymentModel::ONLINE_STATUS_NAMES[$payment->status] ?? '待支付',
'paid_at' => $payment->paid_at,
'trade_no' => $payment->trade_no,
], $payment->status === PaymentModel::STATUS_APPROVED ? '支付成功' : '支付结果确认中');
}
/**
* 旺铺支付结果后台通知(公开路由,MD5 验签后幂等结账)
*
* 通知报文为明文表单(不涉及加解密),sign 之外非空数据元 ASCII 升序拼接 + 专用加签Key 做 MD5,
* 验签通过即视为核心平台合法通知;应答 {"code":"00"} 视为通知成功,否则网关按 2^n 分钟重试 7 次;
* 重复通知必须幂等(settle 内部行锁 + 状态判断)。
*/
#[PostRoute('/payment/notify', authorize: false)]
public function notify(Request $request): JsonResponse
{
try {
$params = $this->wangpu->verifyNotify($request->all());
} catch (Throwable $e) {
Log::warning('旺铺支付通知验签失败', ['error' => $e->getMessage()]);
return $this->notifyAck('01', '报文验签失败');
}
try {
$this->onlinePayment->settleByNotify($params);
} catch (Throwable $e) {
Log::error('旺铺支付通知处理失败', ['params' => $params, 'error' => $e->getMessage()]);
return $this->notifyAck('01', $e->getMessage());
}
return $this->notifyAck(WangpuPayService::NOTIFY_ACK_OK, '成功');
}
/**
* 微信支付结果后台通知(公开路由,验签 + AES-GCM 解密后幂等结账)
*
* 应答 {"code":"SUCCESS","message":"成功"} 视为通知成功,否则微信按阶梯间隔重推;
* 重复通知必须幂等(settle 内部行锁 + 状态判断)。
*/
#[PostRoute('/payment/wechat-notify', authorize: false)]
public function wechatNotify(Request $request): JsonResponse
{
try {
$params = $this->wxpay->decryptNotify($request);
} catch (Throwable $e) {
Log::warning('微信支付通知验签/解密失败', ['error' => $e->getMessage()]);
return $this->wechatNotifyAck(WechatPayService::NOTIFY_ACK_FAIL, '报文验签解密失败');
}
try {
$this->onlinePayment->settleByWechatNotify($params);
} catch (Throwable $e) {
Log::error('微信支付通知处理失败', ['params' => $params, 'error' => $e->getMessage()]);
return $this->wechatNotifyAck(WechatPayService::NOTIFY_ACK_FAIL, $e->getMessage());
}
return $this->wechatNotifyAck(WechatPayService::NOTIFY_ACK_OK, '成功');
}
/**
* 微信通知应答报文(微信约定格式)
*/
protected function wechatNotifyAck(string $code, string $msg): JsonResponse
{
return response()->json([
'code' => $code,
'message' => mb_substr($msg, 0, 64),
], $code === WechatPayService::NOTIFY_ACK_OK ? 200 : 500);
}
/**
* 通知应答报文(网关约定格式,timestampyyyyMMddHHmmssSSS
*/
protected function notifyAck(string $code, string $msg): JsonResponse
{
return response()->json([
'code' => $code,
'msg' => mb_substr($msg, 0, 100),
'timestamp' => now()->format('YmdHisv'),
]);
}
}