前端Vue 后端Laravel 支付问题
1. 环境
1). 前端使用的是Vue 后端使用的是Laravel 、支付是yansongda/pay包、Ngrok映射,使用的是支付宝的沙盒模式,以上都是本地开发。
2. 问题
就是前端点击要确认支付后,弹出的支付宝支付页面是报错502,错误如下:
502 Bad Gateway
The proxy server received an invalid response from an upstream server. Sorry for the inconvenience.
Please report this message and include the following information to us.
Thank you very much!
| URL: | mapi.alipay.net/gateway.do?charset=... |
| Server: | proxy-2-1-1.daily.alipay.net |
| Date: | 2025/09/13 07:57:14 |
Powered by Tengine
代码如下
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Models\Order;
use App\Exceptions\InvalidRequestException;
use Illuminate\Auth\Access\AuthorizationException;
// use Yansongda\Pay\Pay;
use Carbon\Carbon;
use App\Events\OrderPaid;
class PaymentController extends Controller
{
public function payByAlipay(Order $order, Request $request)
{
\Log::info('支付宝支付请求开始', ['order_no' => $order->no]);
try {
$this->authorize('own', $order);
} catch (AuthorizationException $e) {
\Log::warning('无权限操作订单', ['order_no' => $order->no]);
return response()->json([
'success' => false,
'message' => '你无权操作此订单'
], 403);
}
if ($order->paid_at || $order->closed) {
\Log::warning('订单状态不正确', ['order_no' => $order->no, 'paid_at' => $order->paid_at, 'closed' => $order->closed]);
return response()->json([
'success' => false,
'message' => '订单状态不正确'
], 400);
}
try {
\Log::info('调用支付宝接口', ['order_no' => $order->no, 'amount' => $order->total_amount]);
$response = app('alipay')->web([
'out_trade_no' => $order->no,
'total_amount' => (float)$order->total_amount,
'subject' => '支付订单:' . $order->no,
'product_code' => 'FAST_INSTANT_TRADE_PAY'
]);
$paymentHtml = $response->getBody()->getContents();
\Log::info('支付宝接口调用成功', ['order_no' => $order->no, 'html_length' => strlen($paymentHtml)]);
// 检查HTML内容
if (strpos($paymentHtml, 'alipay') === false) {
\Log::error('支付宝返回的HTML内容异常', ['html' => substr($paymentHtml, 0, 200)]);
}
return response()->json([
'success' => true,
'data' => [
'pay_html' => $paymentHtml,
'order_no' => $order->no,
]
]);
} catch (\Exception $e) {
\Log::error('创建支付链接失败', [
'order_no' => $order->no,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json([
'success' => false,
'message' => '创建支付链接失败:' . $e->getMessage()
], 500);
}
}
public function alipayReturn()
{
try {
$data = app('alipay')->verify();
// 验证成功:返回JSON格式的成功响应
return response()->json([
'success' => true,
'message' => '付款成功',
'data' => $data
], 200);
} catch (\Exception $e) {
// 验证失败:返回 JSON 格式的错误响应
return response()->json([
'success' => false,
'message' => '数据验证失败:' . $e->getMessage(), // 可根据需要简化提示
'error' => $e->getMessage() // 可选:仅在开发环境返回详细错误信息
], 400); // 400 表示请求参数错误/数据无效
}
}
public function alipayNotify()
{
\Log::info('支付宝异步通知开始', ['time' => now(), 'request_data' => request()->all()]);
try {
$data = app('alipay')->verify();
\Log::info('支付宝通知验证成功', ['data' => $data->all()]);
} catch (\Exception $e) {
\Log::error('支付宝通知验证失败', [
'error' => $e->getMessage(),
'request_data' => request()->all()
]);
return 'fail';
}
// 如果订单状态不是成功或者结束,则不走后续的逻辑
if(!in_array($data->trade_status, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
\Log::info('交易状态未完成', ['trade_status' => $data->trade_status]);
return app('alipay')->success();
}
// $data->out_trade_no 拿到订单流水号,并在数据库中查询
$order = Order::where('no', $data->out_trade_no)->first();
if (!$order) {
\Log::error('订单不存在', ['out_trade_no' => $data->out_trade_no]);
return 'fail';
}
// 如果这笔订单的状态已经是已支付
if ($order->paid_at) {
\Log::info('订单已支付', ['order_no' => $order->no]);
return app('alipay')->success();
}
// 更新订单
try {
$order->update([
'paid_at' => Carbon::now(),
'payment_method' => 'alipay',
'payment_no' => $data->trade_no,
'status' => 'paid',
]);
\Log::info('订单支付状态更新成功', ['order_no' => $order->no]);
} catch (\Exception $e) {
\Log::error('订单状态更新失败', [
'order_no' => $order->no,
'error' => $e->getMessage()
]);
return 'fail';
}
\Log::info('支付宝异步通知处理完成');
$this->afterPaid($order);
return app('alipay')->success();
}
public function payStatus(Order $order, Request $request) {
$this->authorize('own', $order);
if($order->paid_at) {
return response()->json([
'success' => true,
'data' => [
'is_paid' => true,
'paid_at' => $order->paid_at->toDateTimeString(),
'order_status' => $order->status,
'message' => '订单已支付'
]
]);
} else {
return response()->json([
'success' => false,
'message' => '订单未支付'
]);
}
}
protected function afterPaid(Order $order)
{
event(new OrderPaid($order));
}
}
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\VerificationCodesController;
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\UsersController;
use App\Http\Controllers\Api\CaptchasController;
use App\Http\Controllers\Api\AuthorizationsController;
use App\Http\Controllers\Api\ImagesController;
use App\Http\Controllers\Api\CategoriesController;
use App\Http\Controllers\Api\TopicsController;
use App\Http\Controllers\Api\UserAddressesController;
use App\Http\Controllers\Api\ProductCategoriesController;
use App\Http\Controllers\Api\ProductsController;
use App\Http\Controllers\Api\CartController;
use App\Http\Controllers\Api\OrdersController;
use App\Http\Controllers\Api\PaymentController;
Route::prefix('v1')->name('api.v1.')->group(function () {
# 短信验证码
// Route::post('verificationCodes', [VerificationCodesController::class, 'store'])->name('verificationCodes.store');
# 用户注册
Route::post('users', [UsersController::class, 'store'])->name('users.store');
# 获取第三方登录授权URL
Route::get('socials/{social_type}/authorization-url', [AuthController::class, 'getAuthorizationUrl'])->name('socials');
# 第三方登录
Route::post('socials/{social_type}/authorizations', [AuthorizationsController::class, 'socialStore'])
->where('social_type', 'wechat')
->name('socials.authorizations.store');
# 密码登录
Route::post('authorizations', [AuthorizationsController::class, 'store'])->name('authorizations.store');
# 刷新token
Route::put('authorizations/current', [AuthController::class, 'update'])->name('authorizations.update');
// # 删除token
// Route::delete('authorizations/current', [AuthController::class, 'destroy'])->name('authorizations.destroy');
# 限制 一分钟只能调用60次
Route::middleware('throttle:' . config('api.rate_limits.access'))->group(function () {
# 游客可以访问的接口
// 某个用户的详情
Route::get('users/{user}', [UsersController::class, 'show'])->name('users.show');
// 分类列表
Route::apiResource('categories', CategoriesController::class)->only('index');
// 某个用户发布的话题
Route::get('users/{user}/topics', [TopicsController::class, 'userIndex'])->name('users.topics.index');
// 话题列表、详情
Route::apiResource('topics', TopicsController::class)->only([
'index', 'show'
]);
# 支付后端回调
Route::post('payment/alipay/notify', [PaymentController::class,'alipayNotify'])->name('payment.alipay.notify');
# 登录后才能访问的接口
Route::middleware('auth:api')->group(function() {
// 当前登录用户信息
Route::get('user', [UsersController::class, 'me'])->name('user.show');
// 编辑登录用户信息
Route::patch('user', [UsersController::class, 'update'])->name('user.update');
// 上传图片
Route::post('images', [ImagesController::class, 'store'])->name('images.store');
// 发布、修改、删除话题
Route::apiResource('topics', TopicsController::class)->only([
'store', 'update', 'destroy'
]);
// 用户收货地址
Route::apiResource('addresses', UserAddressesController::class)->only([
'index','store','update','destroy'
]);
Route::post('addresses/{id}/default',[UserAddressesController::class, 'isDefault'])->name('addresses.isDefault');
// 商品分类
Route::apiResource('productcategories', ProductCategoriesController::class)->only([
'index','show','store','update','destroy'
]);
// 商品
Route::apiResource('products', ProductsController::class)->only([
'index','show',
]);
// 购物车
Route::apiResource('carts', CartController::class)->only([
'index','show','store','update',
]);
Route::delete('carts', [CartController::class, 'destroy']);
// 订单
Route::apiResource('orders', OrdersController::class)->only([
'index','show','store','update','destroy',
]);
// 支付
Route::get('payment/{order}/alipay', [PaymentController::class,'payByAlipay']);
//前端回调
Route::get('payment/alipay/return', [PaymentController::class,'alipayReturn'])->name('payment.alipay.return');
// 查询支付结果
Route::get('payment/{order}/pay-status', [PaymentController::class,'payStatus']);
});
});
# 限制 一分钟只能调用10次
Route::middleware('throttle:' . config('api.rate_limits.sign'))->group(function () {
});
// 图片验证码
Route::post('captchas', [CaptchasController::class, 'store'])->name('captchas.store');
# 发送邮箱验证码
Route::post('verification-codes',[VerificationCodesController::class, 'store'])->name('verification-codes.store');
# 使用邮箱验证码登录
Route::post('login/email', [AuthController::class, 'loginWithEmailCode'])->name('login.email');
Route::post('login/password', [AuthController::class, 'loginWithPassword'])->name('login.password');
# 退出登录
Route::delete('logout', [AuthController::class, 'logout'])->name('logout');
});
<?php
use Yansongda\Pay\Pay;
return [
'alipay' => [
'default' => [
// 「必填」支付宝分配的 app_id
'app_id' => env('ALIPAY_APP_ID'),
// 「必填」应用私钥 字符串或路径
'app_secret_cert' => env('ALIPAY_SECRET_CERT'),
// 设置应用私钥后,即可下载得到以
//「必填」应用公钥证书 路径,下3个证书
'app_public_cert_path' => storage_path('cert/appPublicCert.crt'),
// 「必填」支付宝公钥证书 路径
'alipay_public_cert_path' => storage_path('cert/alipayPublicCert.crt'),
// 「必填」支付宝根证书 路径
'alipay_root_cert_path' => storage_path('cert/alipayRootCert.crt'),
// 前端页面的回调,可以不填,在AppServiceProvider.php里面可以直接写
'return_url' => '',
// 服务器端的回调,可以不填,在AppServiceProvider.php里面可以直接写
'notify_url' => '',
'gateway' => 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
// 「选填」默认为正常模式。可选为: 沙盒MODE_SANDBOX 正式MODE_NORMAL, MODE_SERVICE
'mode' => Pay::MODE_NORMAL,
// 「选填」第三方应用授权token
'app_auth_token' => '',
// 「选填」服务商模式下的服务商 id,当 mode 为 Pay::MODE_SERVICE 时使用该参数
'service_provider_id' => '',
'curl_options' => [
CURLOPT_CONNECTTIMEOUT => 30, // 连接超时(秒)
CURLOPT_TIMEOUT => 60, // 请求超时(秒)
],
]
],
'wechat' => [
'default' => [
// 「必填」商户号,服务商模式下为服务商商户号
// 可在 https://pay.weixin.qq.com/ 账户中心->商户信息 查看
'mch_id' => '',
// 「选填」v2商户私钥
'mch_secret_key_v2' => '',
// 「必填」v3 商户秘钥
// 即 API v3 密钥(32字节,形如md5值),可在 账户中心->API安全 中设置
'mch_secret_key' => '',
// 「必填」商户私钥 字符串或路径
// 即 API证书 PRIVATE KEY,可在 账户中心->API安全->申请API证书 里获得
// 文件名形如:apiclient_key.pem
'mch_secret_cert' => '',
// 「必填」商户公钥证书路径
// 即 API证书 CERTIFICATE,可在 账户中心->API安全->申请API证书 里获得
// 文件名形如:apiclient_cert.pem
'mch_public_cert_path' => '',
// 「必填」微信回调url
// 不能有参数,如?号,空格等,否则会无法正确回调
'notify_url' => 'http://shop.test/wechat/notify',
// 「选填」公众号 的 app_id
// 可在 mp.weixin.qq.com 设置与开发->基本配置->开发者ID(AppID) 查看
'mp_app_id' => '2016082000291234',
// 「选填」小程序 的 app_id
'mini_app_id' => '',
// 「选填」app 的 app_id
'app_id' => '',
// 「选填」服务商模式下,子公众号 的 app_id
'sub_mp_app_id' => '',
// 「选填」服务商模式下,子 app 的 app_id
'sub_app_id' => '',
// 「选填」服务商模式下,子小程序 的 app_id
'sub_mini_app_id' => '',
// 「选填」服务商模式下,子商户id
'sub_mch_id' => '',
// 「选填」(适用于 2024-11 及之前开通微信支付的老商户)微信支付平台证书序列号及证书路径,强烈建议 php-fpm 模式下配置此参数
// 「必填」微信支付公钥ID及证书路径,key 填写形如 PUB_KEY_ID_0000000000000024101100397200000006 的公钥id,见 https://pay.weixin.qq.com/doc/v3/merchant/4013053249
'wechat_public_cert_path' => [
'45F59D4DABF31918AFCEC556D5D2C6E376675D57' => __DIR__.'/Cert/wechatPublicKey.crt',
'PUB_KEY_ID_0000000000000024101100397200000006' => __DIR__.'/Cert/publickey.pem',
],
// 「选填」默认为正常模式。可选为: MODE_NORMAL, MODE_SERVICE
'mode' => Pay::MODE_NORMAL,
]
],
'logger' => [
'enable' => true,
'file' => storage_path('/logs/pay.log'),
'level' => 'debug', // 建议生产环境等级调整为 info,开发环境为 debug
'type' => 'daily', // optional, 可选 daily.
'max_file' => 30, // optional, 当 type 为 daily 时有效,默认 30 天
],
'http' => [ // optional
'timeout' => 5.0,
'connect_timeout' => 5.0,
// 更多配置项请参考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
],
];
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Resources\Json\JsonResource;
use MonoLog\Logger;
use Yansongda\Pay\Pay;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
if (app()->isLocal()) {
$this->app->register(\VIACreative\SudoSu\ServiceProvider::class);
}
$config = config('pay');
//判断当前项目运行环境是否为线上环境
if (app()->environment() !== 'production') {
$config['alipay']['default']['mode'] = $config['wechat']['default']['mode'] = Pay::MODE_SANDBOX;
$config['logger']['level'] = 'debug';
} else {
$config['logger']['level'] = 'info';
}
// 往服务容器中注入一个名为 alipay 的单例对象 use ($config) 是继承前面$config设置的配置
$this->app->singleton('alipay', function() use ($config){
\Log::info('支付宝回调URL', [
'return_url' => ngrok_url('api.v1.payment.alipay.return'),
'notify_url' => ngrok_url('api.v1.payment.alipay.notify'),
]); // 查看 storage/logs/laravel.log 确认URL有效
$config['alipay']['default']['return_url'] = ngrok_url('api.v1.payment.alipay.return');
$config['alipay']['default']['notify_url'] = ngrok_url('api.v1.payment.alipay.notify');
// dd($config);
// $ngrokDomain = 'https://52a39f8490d6.ngrok-free.app';
// $config['return_url'] = $ngrokDomain . '/api/v1/payment/alipay/return';
// $config['notify_url'] = $ngrokDomain . '/api/v1/payment/alipay/notify';
//调用Yansongda\Pay来创建一个支付宝支付对象
return Pay::alipay($config);
});
$this->app->singleton('wechat_pay', function() use ($config) {
// $config['wechat']['default']['notify_url'] = ngrok_url('payment.wechat.notify');
// 调用 Yansongda\Pay 来创建一个微信支付对象
return Pay::wechat($config);
});
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
\App\Models\User::observe(\App\Observers\UserObserver::class);
\App\Models\Reply::observe(\App\Observers\ReplyObserver::class);
\App\Models\Topic::observe(\App\Observers\TopicObserver::class);
\App\Models\Link::observe(\App\Observers\LinkObserver::class);
\Illuminate\Pagination\Paginator::useBootstrap();
JsonResource::withoutWrapping();
}
}
前端代码:
<template>
<div class="pay-page">
<!-- 复用结算页公共头部 -->
<Header></Header>
<main class="container">
<!-- 页面标题 + 步骤指示器(与结算页样式一致) -->
<h2 class="page-title">选择支付方式</h2>
<div class="checkout-steps">
<div class="step">
<div class="step-icon">1</div>
<div class="step-text">确认订单</div>
</div>
<div class="step-line active"></div>
<div class="step active">
<div class="step-icon">2</div>
<div class="step-text">支付</div>
</div>
<div class="step-line"></div>
<div class="step">
<div class="step-icon">3</div>
<div class="step-text">完成</div>
</div>
</div>
<!-- 支付内容区(卡片式布局) -->
<div class="pay-content">
<!-- 1. 订单信息卡片 -->
<div class="pay-section order-info-card">
<h3 class="section-title">订单信息</h3>
<div class="order-detail">
<div class="info-item">
<span class="label">订单编号</span>
<span class="value">{{ order.no }}</span>
</div>
<div class="info-item">
<span class="label">创建时间</span>
<span class="value">{{ order.created_at }}</span>
</div>
<div class="info-item">
<span class="label">支付状态</span>
<span class="value status-unpaid">未支付</span>
</div>
<div class="info-item">
<span class="label">收货地址</span>
<span class="value">
{{ order.address.contact_name }} {{ hidePhone(order.address.contact_phone) }}<br>
{{ order.address?.address }}
</span>
</div>
</div>
</div>
<!-- 2. 支付方式选择卡片 -->
<div class="pay-section payment-methods-card">
<h3 class="section-title">选择支付方式</h3>
<div class="methods-list">
<!-- 支付宝支付 -->
<div
class="method-item"
:class="{ active: selectedMethod === 'alipay', disabled: false }"
@click="selectedMethod = 'alipay'"
>
<div class="method-icon alipay"></div>
<div class="method-info">
<div class="method-name">支付宝支付</div>
<div class="method-desc">支持余额、银行卡、花呗等</div>
</div>
<div class="method-check">
<el-radio v-model="selectedMethod" label="alipay" border size="small"></el-radio>
</div>
</div>
<!-- 微信支付 -->
<div
class="method-item"
:class="{ active: selectedMethod === 'wechat', disabled: false }"
@click="selectedMethod = 'wechat'"
>
<div class="method-icon wechat"></div>
<div class="method-info">
<div class="method-name">微信支付</div>
<div class="method-desc">支持微信余额、银行卡、零钱通</div>
</div>
<div class="method-check">
<el-radio v-model="selectedMethod" label="wechat" border size="small"></el-radio>
</div>
</div>
<!-- 银行卡支付(禁用) -->
<div
class="method-item"
:class="{ disabled: true }"
>
<div class="method-icon bankcard"></div>
<div class="method-info">
<div class="method-name">银行卡支付</div>
<div class="method-desc">暂未开通,敬请期待</div>
</div>
<div class="method-check">
<el-radio v-model="selectedMethod" label="bankcard" border size="small" disabled></el-radio>
</div>
</div>
</div>
</div>
<!-- 3. 支付金额与操作卡片(右侧固定) -->
<div class="pay-section amount-operate-card">
<div class="amount-wrapper">
<div class="amount-label">应付金额</div>
<div class="amount-value">¥{{ order.total_amount}}</div>
</div>
<!-- 支付超时提示 -->
<div class="timeout-tip">
<el-icon><Clock /></el-icon>
<span>请在 <span class="countdown" :class="{ warning: countdown < 60 }">{{ countdown }}s</span> 内完成支付</span>
</div>
<!-- 确认支付按钮 -->
<button
class="btn-confirm-pay"
@click="showPayModal"
:disabled="!selectedMethod || countdown <= 0"
>
确认支付
</button>
<!-- 订单操作链接 -->
<div class="order-actions">
<a @click="cancelOrder" class="action-link cancel">取消订单</a>
<a @click="goBack" class="action-link back">返回订单列表</a>
</div>
</div>
</div>
</main>
<!-- 复用结算页公共底部 -->
<Footer></Footer>
<!-- 支付弹窗(扫码界面) -->
<el-dialog
:title="getPayModalTitle"
v-model="payModalVisible"
width="340px"
:close-on-click-modal="false"
:show-close="false"
>
<div class="pay-modal-inner">
<!-- 1. 支付信息展示 -->
<div class="pay-info">
<p class="info-line">订单编号:{{ order.no || '未知' }}</p>
<p class="info-line">支付金额:<span class="amount">¥{{ order.total_amount }}</span></p>
<p class="info-line warning">请在新窗口完成支付,请勿关闭此弹窗</p>
</div>
<!-- 2. 支付宝跳转按钮(核心) -->
<button
class="btn-goto-alipay"
@click="gotoAlipayPage"
:disabled="!alipayPayUrl"
>
立即前往支付宝支付
</button>
<!-- 支付状态查询进度 -->
<div class="pay-status">
<el-progress :percentage="checkProgress" status="success" :stroke-width="5" class="progress-bar"></el-progress>
<p class="status-text">正在查询支付结果...</p>
</div>
</div>
<template #footer>
<el-button @click="payModalVisible = false">关闭弹窗</el-button>
<el-button type="primary" @click="checkPayResult">手动查询结果</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox, ElProgress, ElIcon } from 'element-plus';
import { Clock } from '@element-plus/icons-vue';
// 复用结算页公共组件
import Header from '@/components/Layouts/_header.vue';
import Footer from '@/components/Layouts/_footer.vue';
// 接口服务(实际项目替换为真实接口)
import {
apiRequestAlipay,
apiCheckPayStatus,
apiCancelOrder
} from '@/services/payService';
import { apiGetOrderDetail } from '@/services/orderService'
// 路由与订单数据
const route = useRoute();
const router = useRouter();
const orderId = route.params.id as string; // 从结算页携带订单ID
// 支付相关状态
const selectedMethod = ref('alipay'); // 默认选中支付宝
const payModalVisible = ref(false);
const countdown = ref(180); // 3分钟支付超时
const checkProgress = ref(0); // 支付结果查询进度
let countdownTimer: NodeJS.Timeout | null = null;
let checkTimer: NodeJS.Timeout | null = null;
// 订单数据(模拟,实际从接口获取)
// const order = ref({
// no: '20250828154000789012',
// created_at: '2025-08-28 15:40:30',
// total_amount: 399.0,
// address: {
// contact_name: '张三',
// contact_phone: '13812345678',
// province: '北京市',
// city: '北京市',
// district: '朝阳区',
// address: 'XX街道XX小区1号楼1单元101室'
// }
// });
const order = ref({
no: '',
created_at: '',
total_amount: 0,
address: {
contact_name: '',
contact_phone: '',
address: ''
} // 确保 address 初始是个空对象,而非 undefined
// 之前可能写成了 address: undefined 或未定义 address
});
// 支付二维码(实际项目由接口返回)
const alipayQrcode = ref('https://picsum.photos/id/237/200/200');
const wechatQrcode = ref('https://picsum.photos/id/238/200/200');
// 支付弹窗标题(根据选中方式动态变化)
const getPayModalTitle = computed(() => {
return `${selectedMethod.value === 'alipay' ? '支付宝' : '微信'}扫码支付`;
});
// 工具函数:隐藏手机号中间4位
const hidePhone = (phone: string) => {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
};
// 获取订单详情
const getOrderDetail = async () => {
try {
const { data } = await apiGetOrderDetail(orderId);
console.log(data,'111');
order.value = data;
countdown.value = data.remaining_time || 180;
} catch (error) {
ElMessage.error('获取订单信息失败');
router.push('/orders');
}
};
// 启动支付倒计时
const startCountdown = () => {
countdownTimer = setInterval(() => {
if (countdown.value <= 0) {
clearInterval(countdownTimer!);
ElMessageBox.alert('支付超时,订单已自动关闭', '提示', {
confirmButtonText: '确定',
callback: () => router.push('/orders')
});
return;
}
countdown.value--;
}, 1000000);
};
// 存储支付宝支付链接
const alipayPayUrl = ref('');
// 显示支付弹窗(创建二维码)
const showPayModal = async () => {
if(selectedMethod.value !== 'alipay') {
ElMessage.warning('暂仅支持支付宝支付');
return;
}
try {
const { data } = await apiRequestAlipay(orderId);
if(data.success ) {
if(data.data.pay_url) {
alipayPayUrl.value = data.data.pay_url;
}
else if (data.data.pay_html) {
const tempDiv = document.createElement('div')
tempDiv.innerHTML = data.data.pay_html;
const form = tempDiv.querySelector('form')
if(form) alipayPayUrl.value = form.action
}
}
payModalVisible.value = true;
startCheckPayResult();
} catch (error: any) {
ElMessage.error('获取支付链接失败:' + (error.response?.data?.message || error.message));
}
};
// 新增:跳转支付宝支付页(打开新窗口,避免当前页面刷新)
const gotoAlipayPage = () => {
if (!alipayPayUrl.value) return;
window.open(alipayPayUrl.value, '_blank'); // 新窗口打开支付宝
};
// 轮询检查支付结果
const startCheckPayResult = () => {
checkProgress.value = 0;
// 清除可能存在的旧定时器
if (checkTimer) clearInterval(checkTimer);
checkTimer = setInterval(async () => {
checkProgress.value = (checkProgress.value + 10) %100;
try {
const {data} = await apiCheckPayStatus(orderId);
if (data.success && data.data.is_paid) {
clearInterval(checkTimer)
ElMessage.success('支付成功!')
payModalVisible.value = false;
router.push(`/order-success?orderId=${orderId}`)
}
} catch (error) {
console.error('查询支付状态失败', error);
}
},3000);
};
// 手动查询支付结果
const checkPayResult = async () => {
try {
const { data } = await apiCheckPayStatus(orderId);
if (data.is_paid) {
ElMessage.success('支付成功!');
payModalVisible.value = false;
router.push(`/order-success?orderId=${orderId}`);
} else {
ElMessage.info('暂未检测到支付,请继续完成');
}
} catch (error) {
ElMessage.error('查询失败,请稍后重试');
}
};
// 取消订单
const cancelOrder = async () => {
try {
await ElMessageBox.confirm(
'确定取消订单?取消后库存将返还',
'确认取消',
{ type: 'warning' }
);
await apiCancelOrder(orderId);
ElMessage.success('订单已取消');
router.push('/orders');
} catch (error) {}
};
// 返回订单列表
const goBack = () => router.push('/orders');
// 页面生命周期
onMounted(() => {
if(orderId == '') {
router.push({
name: 'Home'
})
}
getOrderDetail();
startCountdown();
});
onUnmounted(() => {
if (countdownTimer) clearInterval(countdownTimer);
if (checkTimer) clearInterval(checkTimer);
});
watch(payModalVisible, (newVal) => {
if(!newVal && checkTimer) {
clearInterval(checkTimer)
}
})
</script>
<style lang="scss" scoped>
@import '@/assets/css/common/common.scss'; // 复用结算页公共变量(如$primary、$white、$gray等)
// 页面整体样式
.pay-page {
background-color: #f5f5f5;
min-height: 100vh;
}
.container {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 30px 16px;
}
// 页面标题
.page-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 20px;
color: $gray-dark;
}
// 步骤指示器(完全复用结算页样式)
.checkout-steps {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
gap: 20px;
.step {
display: flex;
flex-direction: column;
align-items: center;
.step-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #ddd;
color: $gray;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
font-weight: 600;
}
.step-text {
font-size: 14px;
color: $gray;
}
&.active {
.step-icon {
background-color: $primary;
color: white;
}
.step-text {
color: $primary;
}
}
}
.step-line {
flex: 1;
height: 2px;
background-color: #ddd;
max-width: 100px;
&.active {
background-color: $primary;
}
}
}
// 支付内容区布局
.pay-content {
display: grid;
grid-template-columns: 1fr 360px;
gap: 20px;
// 移动端响应式(与结算页一致)
@media (max-width: 992px) {
grid-template-columns: 1fr;
}
// 支付卡片公共样式(统一卡片风格)
.pay-section {
background-color: $white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
// 卡片标题(复用结算页section-title样式)
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid $gray-light;
color: $gray-dark;
}
}
// 1. 订单信息卡片样式
.order-info-card {
.order-detail {
.info-item {
display: flex;
margin-bottom: 12px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
.label {
width: 100px;
color: $gray;
flex-shrink: 0;
}
.value {
color: $gray-dark;
line-height: 1.5;
&.status-unpaid {
color: #f56c6c; // 未支付红色提示
}
}
}
}
}
// 2. 支付方式选择卡片样式
.payment-methods-card {
.methods-list {
.method-item {
display: flex;
align-items: center;
padding: 12px 16px;
border: 1px solid $gray-light;
border-radius: 6px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.2s;
&:last-child {
margin-bottom: 0;
}
// 选中状态(与结算页选择框风格一致)
&.active {
border-color: $primary;
background-color: rgba($primary, 0.05);
}
// 禁用状态
&.disabled {
cursor: not-allowed;
opacity: 0.6;
}
// 支付方式图标(可替换为实际图标)
.method-icon {
width: 40px;
height: 40px;
border-radius: 4px;
margin-right: 16px;
flex-shrink: 0;
&.alipay {
background-color: #1677ff; // 支付宝蓝
}
&.wechat {
background-color: #07c160; // 微信绿
}
&.bankcard {
background-color: #ff9f43; // 银行卡橙
}
}
// 支付方式信息
.method-info {
flex: 1;
.method-name {
font-size: 14px;
color: $gray-dark;
margin-bottom: 4px;
}
.method-desc {
font-size: 12px;
color: $gray;
}
}
// 单选框容器(右对齐)
.method-check {
flex-shrink: 0;
}
}
}
}
// 3. 支付金额与操作卡片样式(右侧固定,与结算页总结区对齐)
.amount-operate-card {
// 固定定位(移动端取消固定)
@media (min-width: 993px) {
position: sticky;
top: 20px;
align-self: flex-start;
}
// 金额展示区
.amount-wrapper {
margin-bottom: 20px;
.amount-label {
font-size: 14px;
color: $gray;
margin-bottom: 8px;
display: block;
}
.amount-value {
font-size: 24px;
font-weight: 600;
color: #f56c6c; // 金额红色突出
}
}
// 超时提示
.timeout-tip {
display: flex;
align-items: center;
font-size: 12px;
color: $gray;
margin-bottom: 20px;
el-icon {
margin-right: 6px;
color: #f56c6c;
}
.countdown {
color: #f56c6c;
font-weight: 500;
&.warning {
color: #ff4d4f; // 倒计时小于60秒时加深红色
}
}
}
// 确认支付按钮(与结算页按钮风格一致)
.btn-confirm-pay {
width: 100%;
height: 44px;
background-color: $primary;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
margin-bottom: 16px;
&:disabled {
background-color: $gray-light;
cursor: not-allowed;
}
&:hover:not(:disabled) {
background-color: darken($primary, 10%); // hover时加深颜色
}
}
// 订单操作链接
.order-actions {
display: flex;
justify-content: space-between;
font-size: 14px;
.action-link {
color: $primary;
cursor: pointer;
&:hover {
text-decoration: underline;
}
&.cancel {
color: #f56c6c; // 取消订单红色
}
}
}
}
}
// 支付弹窗样式(与页面风格统一)
.pay-modal-inner {
padding: 10px 0;
.pay-info {
margin-bottom: 20px;
}
.info-line {
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.amount {
color: #f56c6c;
font-weight: 600;
}
.warning {
color: #f56c6c;
margin-top: 12px;
}
// 支付提示
.pay-tips {
margin-bottom: 20px;
.tip-line {
font-size: 12px;
color: $gray-dark;
margin-bottom: 8px;
line-height: 1.5;
&:last-child {
margin-bottom: 0;
}
&.text-warning {
color: #f56c6c;
}
}
}
// 支付状态进度
.pay-status {
.progress-bar {
margin-bottom: 8px;
}
.status-text {
font-size: 12px;
color: $gray;
text-align: center;
}
}
}
/* 支付宝跳转按钮 */
.btn-goto-alipay {
width: 100%;
height: 44px;
background-color: #1677ff; /* 支付宝蓝 */
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-goto-alipay:disabled {
background-color: #c9daf7;
cursor: not-allowed;
}
.btn-goto-alipay:hover:not(:disabled) {
background-color: #0f62d9;
}
</style>
// 封装axios
// 请求拦截器
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'; // 从类型声明导入
import { useAuthStore } from '../stores/auth';
import router from '../router'; // 引入路由实例
const api = axios.create({
baseURL: 'http://larabbs.test/api',
// baseURL: 'https://f7c714028c5e.ngrok-free.app/api',
headers: {
'Accept': 'application/json',
},
});
// 请求拦截器
api.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('token');
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器(修改 401 错误处理逻辑)
api.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
(error) => {
// 仅处理 401 错误
if (error.response?.status === 401) {
const authStore = useAuthStore();
const currentPath = router.currentRoute.value.path; // 获取当前路由路径
// 调用 loggou时 跳过 API 请求(避免循环)
if (authStore.isLoggedIn) { // 仅在已登录昨天下处理
localStorage.removeItem('token');
authStore.logout(true);
}
// 2. 仅在非登录页时跳转(避免登录页重复跳转导致刷新)
if (currentPath !== '/login') {
// 使用路由跳转替代 window.location.href(无刷新)
router.push('/login');
}
}
return Promise.reject(error); // 将错误抛回给组件处理
}
);
export default api;
// 引入拦截器
import api from '../api/axios';
// 发起支付宝支付请求
export const apiRequestAlipay = (orderId: number) => {
return api.get(`/v1/payment/${orderId}/alipay`);
}
// 查询订单支付状态
export const apiCheckPayStatus = (orderId: number) => {
return api.get(`/v1/payment/${orderId}/pay-status`);
}
// 支付回调
export const apiVerifyAlipayReturn = () => {
return api.get(`/v1/payment/alipay/return`);
}
// 取消订单
export const apiCancelOrder = (data: Object) => {
return api.post('/v1/carts', data);
}
推荐文章: