183 lines
7.2 KiB
PHP
183 lines
7.2 KiB
PHP
<?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 换 openid,wx.requestPayment 调起;
|
||
* scene=mp:公众号 H5 网页授权 code 换 openid,WeixinJSBridge 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);
|
||
}
|
||
|
||
/**
|
||
* 通知应答报文(网关约定格式,timestamp:yyyyMMddHHmmssSSS)
|
||
*/
|
||
protected function notifyAck(string $code, string $msg): JsonResponse
|
||
{
|
||
return response()->json([
|
||
'code' => $code,
|
||
'msg' => mb_substr($msg, 0, 100),
|
||
'timestamp' => now()->format('YmdHisv'),
|
||
]);
|
||
}
|
||
}
|