138 lines
5.0 KiB
PHP
138 lines
5.0 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 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)
|
||
*
|
||
* 链路:POST /mini/payment/online 下单(返回调起支付参数)
|
||
* → 小程序 wx.requestPayment 完成支付
|
||
* → 网关 POST /mini/payment/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,
|
||
) {}
|
||
|
||
/**
|
||
* 发起在线支付:合并选择本店未支付账单 → 旺铺下单 → 返回调起支付参数
|
||
* @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',
|
||
'remark' => 'nullable|string|max:255',
|
||
], [
|
||
'bill_ids.required' => '请选择要付款的账单',
|
||
'bill_ids.min' => '请选择要付款的账单',
|
||
'code.required' => '微信登录凭证缺失,请重新进入小程序',
|
||
'remark.max' => '备注超过最大长度',
|
||
]);
|
||
|
||
$store = $this->currentStore($request);
|
||
|
||
[$payment, $payParams] = $this->onlinePayment->create(
|
||
$store,
|
||
array_map('intval', $data['bill_ids']),
|
||
(string) $data['code'],
|
||
(string) ($data['remark'] ?? ''),
|
||
);
|
||
|
||
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 ? '支付成功' : '支付结果确认中');
|
||
}
|
||
|
||
/**
|
||
* 旺铺支付结果后台通知(公开路由,解密信封后幂等结账)
|
||
*
|
||
* 通知报文与网关应答同为加密信封(data + signature,商户私钥可解),
|
||
* 解密成功即视为合法通知;应答 {"code":"00"} 视为通知成功,否则网关按 2^n 分钟重试 7 次;
|
||
* 重复通知必须幂等(settle 内部行锁 + 状态判断)。
|
||
*/
|
||
#[PostRoute('/payment/notify', authorize: false)]
|
||
public function notify(Request $request): JsonResponse
|
||
{
|
||
try {
|
||
$params = $this->wangpu->decryptNotify($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, '成功');
|
||
}
|
||
|
||
/**
|
||
* 通知应答报文(网关约定格式,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'),
|
||
]);
|
||
}
|
||
}
|