news 2026/9/3 4:44:34

Vue3电商实战:从登录鉴权到扫码支付的完整闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3电商实战:从登录鉴权到扫码支付的完整闭环

很多同学在写 Vue3 电商项目时,经常卡在同一个地方:登录能跳转,商品能加购,但到了“生成订单、拉起支付、确认支付结果”这条链路上就会断掉。后端返回了一个code_url,前端不知道该怎么用;H5 里调不起微信支付;支付成功后订单状态又迟迟不刷新。其实从登录到支付并不是多个孤立功能,而是一条完整的数据流转链路。

本文以一套微型电商前端为主线,从登录鉴权开始讲,经过商品浏览、购物车、结算下单,最后实现支付二维码展示与支付状态轮询闭环。文章会给出完整可运行的 Vue3 代码片段、接口设计约定和常见坑点。无论是准备 Vue3 面试、做毕业设计,还是第一次接触真实电商业务,都可以按这条链路把项目跑通。

1. 需求分析与整体设计

1.1 电商核心闭环拆解

一个标准电商应用的最小闭环,可以拆成以下环节:

  1. 用户登录并获取 token。
  2. 前端保存登录态,并通过请求拦截器自动携带 token。
  3. 用户浏览商品列表。
  4. 用户将商品加入购物车。
  5. 用户选择商品并提交订单。
  6. 后端创建未支付订单。
  7. 前端调用支付预下单接口,获取支付二维码链接。
  8. 用户扫码完成支付。
  9. 前端轮询或等待后端回调,更新订单状态。

很多教程只讲前两步,或者只讲商品管理,导致你跟着做出来后,支付环节依然不知道怎么接。本文会围绕第 5 到第 9 步展开,电商项目中真正容易出错的就是这一段。

1.2 技术选型说明

本文前端选择 Vue3 + Vite + Vue Router + Pinia + Axios + Element Plus。这套组合是目前国内 Vue3 项目的主流组合,也基本是 Vue3 面试中的常客。

模块选型作用
构建工具Vite本地开发与打包
框架Vue3 组合式 API页面与组件开发
路由Vue Router 4页面跳转与路由守卫
状态管理Pinia保存用户 token、购物车数据
HTTP 请求Axios请求接口、处理拦截器
UI 组件Element Plus快速搭建表单、表格、弹窗
二维码qrcode将 code_url 转为二维码图片

这里要特别说明,当前是 Vue3 的技术生态,选项式 API 依然能用,但新项目和企业实际开发中,组合式 API +<script setup>已经成为主流写法。本文统一使用<script setup>

1.3 支付方式如何选择

国内常见的支付方式有:

  • 微信 JSAPI 支付:只能在微信内置浏览器中调起。
  • 微信 Native 支付:适合 PC Web,后端生成code_url,前端展示二维码,用户用手机微信扫码。
  • 支付宝当面付:适合 PC Web 扫码,后端返回二维码字符串。
  • 支付宝手机网站支付:适合 H5 浏览器中跳转支付宝。

本文以“扫码支付”为主线,因为这种模式最容易在本地开发环境中验证,不依赖微信内置浏览器等特殊环境。无论后端对接微信支付还是支付宝沙箱,前端处理逻辑都类似:先通过接口拿到支付二维码内容,再展示二维码并轮询订单状态。

2. 环境准备与项目初始化

2.1 版本环境说明

本文使用的核心版本如下:

  • Node.js:建议使用 18 或 20 LTS 版本。
  • npm:随 Node.js 自带,建议 9 以上。
  • Vue:3.4 或更高版本。
  • Vite:5 或更高版本。
  • Pinia:2.x。
  • Vue Router:4.x。

由于不同版本之间 API 差异不大,你的本地版本不需要完全一致,但尽量使用 Node 18+,避免 Vite 启动时报错。

2.2 创建 Vue3 项目

使用 Vite 创建项目时,可以执行以下命令:

npm create vite@latest vue3-shop-demo -- --template vue

创建完成后进入项目目录:

cd vue3-shop-demo

然后安装项目基础依赖:

npm install

再安装后续需要使用的依赖:

npm install vue-router@4 pinia axios element-plus qrcode

其中qrcode用于将支付链接转换为二维码图片,element-plus用于快速搭建后台风格页面。

2.3 项目目录结构规划

建议把业务代码按模块拆分,不要全部堆在App.vue或某个页面组件中。

src/ ├── api/ │ ├── auth.js │ ├── order.js │ └── product.js ├── assets/ ├── components/ ├── router/ │ └── index.js ├── stores/ │ ├── cart.js │ └── user.js ├── utils/ │ └── request.js ├── views/ │ ├── LoginView.vue │ ├── ProductListView.vue │ ├── CartView.vue │ ├── OrderConfirmView.vue │ ├── PayView.vue │ └── OrderDetailView.vue ├── App.vue └── main.js

这里的api目录负责集中管理接口请求,stores目录管理全局状态,utils/request.js封装 Axios 实例和拦截器。这种分层方式也是中型前端项目比较标准的写法。

3. 后端接口设计约定

本文重点面向 Vue3 前端实战,不展开完整的后端实现,但前后端接口需要约定清楚。真实开发中,你通常是照着后端 Swagger 文档写前端请求。为了方便演示,这里假设后端提供以下接口。

3.1 登录鉴权接口

POST /api/auth/login Content-Type: application/json { "mobile": "13800138000", "password": "123456" }

响应格式:

{ "code": 0, "message": "success", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "userInfo": { "userId": 10001, "nickname": "张三" } } }

后续所有业务接口都需要在请求头中携带:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

3.2 商品、购物车与订单接口

为了便于演示,商品数据可以先使用静态列表或 Mock 数据。

订单提交接口示例:

POST /api/orders Authorization: Bearer <token> Content-Type: application/json { "items": [ { "productId": 1001, "quantity": 2 } ], "receiver": { "name": "张三", "mobile": "13800138000", "address": "北京市海淀区某街道 1 号" } }

响应:

{ "code": 0, "message": "success", "data": { "orderNo": "SN20250101120000123", "payAmount": 19900 } }

这里注意payAmount单位是“分”,不是“元”。金额的单位问题在电商项目中非常关键,前端不要在后端计算金额,更不要自己把元转成分后传回后端,容易造成金额不一致。

3.3 支付预下单与状态查询接口

创建订单成功后,前端需要调用支付预下单接口:

POST /api/payments/native Authorization: Bearer <token> Content-Type: application/json { "orderNo": "SN20250101120000123" }

响应:

{ "code": 0, "message": "success", "data": { "codeUrl": "weixin://wxpay/bizpayurl?pr=xxxxx", "payOrderNo": "PAY20250101120000123" } }

这里的codeUrl是给二维码插件使用的原始字符串。对于支付宝当面付,后端返回的通常是一长串 URL,前端同样把它变成二维码。

支付状态查询接口:

GET /api/orders/SN20250101120000123/pay-status Authorization: Bearer <token>

响应:

{ "code": 0, "message": "success", "data": { "orderNo": "SN20250101120000123", "orderStatus": "PAID" } }

orderStatus常见取值有PENDINGPAIDCANCELLED等。前端轮询时只需要关心是否变为PAID

4. 登录模块与前端鉴权实现

4.1 Axios 封装与请求拦截器

在真实项目中,我们不会在每个组件里直接调用axios.get,而是封装一个统一的request.js,统一处理基础 URL、token、超时、错误码。这样当后端告诉你登录过期时,你只改一处就能全局生效。

// 文件路径:src/utils/request.js import axios from 'axios'; import { ElMessage } from 'element-plus'; import router from '@/router'; import { useUserStore } from '@/stores/user'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); // 请求拦截器:自动携带 token request.interceptors.request.use( (config) => { const userStore = useUserStore(); if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}`; } return config; }, (error) => { return Promise.reject(error); } ); // 响应拦截器:统一处理业务错误 request.interceptors.response.use( (response) => { const res = response.data; if (res.code !== 0) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message || 'Error')); } return res.data; }, (error) => { const status = error.response?.status; if (status === 401) { const userStore = useUserStore(); userStore.clearLoginState(); router.push('/login'); ElMessage.error('登录状态已过期,请重新登录'); } else { ElMessage.error(error.response?.data?.message || '网络异常'); } return Promise.reject(error); } ); export default request;

在响应拦截器中,我们做了两件很重要的事情:

  1. 当后端返回code !== 0时,直接弹出错误提示,业务代码不需要再重复处理错误分支。
  2. 当 HTTP 状态码为 401 时,说明 token 失效,自动清除登录状态并跳回登录页。

这里要注意:ElMessagerouter的导入路径取决于你的项目结构,如果request.jsrouter之间存在循环引用,可以考虑把跳转逻辑放在登录页统一处理,或使用window.location.href做兜底。

4.2 Pinia 管理用户登录态

用户登录后,token 和用户信息属于全局状态,适合放在 Pinia 中。同时我们需要实现 token 的持久化,否则刷新页面后登录态会丢失。

// 文件路径:src/stores/user.js import { defineStore } from 'pinia'; import { loginApi } from '@/api/auth'; export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null') }), getters: { isLoggedIn: (state) => !!state.token }, actions: { async login(loginForm) { const data = await loginApi(loginForm); this.token = data.token; this.userInfo = data.userInfo; localStorage.setItem('token', data.token); localStorage.setItem('userInfo', JSON.stringify(data.userInfo)); }, clearLoginState() { this.token = ''; this.userInfo = null; localStorage.removeItem('token'); localStorage.removeItem('userInfo'); } } });

这里将 token 直接存放在localStorage,是小型项目的常见做法。生产级项目也可以使用 cookie 并设置httpOnly来防 XSS,但鉴权流程思路是类似的。

对应的登录接口api/auth.js可以这样写:

// 文件路径:src/api/auth.js import request from '@/utils/request'; export function loginApi(data) { return request.post('/auth/login', data); } export function getUserInfoApi() { return request.get('/user/info'); }

4.3 登录页面实现

登录页的核心逻辑是调用userStore.login,登录成功后跳转到 redirect 参数指定的页面,否则跳转首页。

<!-- 文件路径:src/views/LoginView.vue --> <template> <div class="login-page"> <el-card class="login-card"> <h2>电商系统登录</h2> <el-form ref="formRef" :model="loginForm" :rules="rules" label-width="80px" > <el-form-item label="手机号" prop="mobile"> <el-input v-model="loginForm.mobile" placeholder="请输入手机号" /> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" show-password /> </el-form-item> <el-form-item> <el-button type="primary" :loading="loading" @click="handleLogin"> 登录 </el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup> import { ref, reactive } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import { ElMessage } from 'element-plus'; import { useUserStore } from '@/stores/user'; const router = useRouter(); const route = useRoute(); const userStore = useUserStore(); const formRef = ref(null); const loading = ref(false); const loginForm = reactive({ mobile: '', password: '' }); const rules = { mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }] }; async function handleLogin() { await formRef.value.validate(); loading.value = true; try { await userStore.login(loginForm); ElMessage.success('登录成功'); const redirect = route.query.redirect; router.replace(redirect || '/'); } finally { loading.value = false; } } </script> <style scoped> .login-page { display: flex; justify-content: center; align-items: center; height: 100vh; background: #f5f7fa; } .login-card { width: 420px; } </style>

4.4 路由守卫控制页面访问

登录页完成后,还需要通过路由守卫控制哪些页面必须登录才能访问。这里使用的是 Vue Router 4 的全局前置守卫。

// 文件路径:src/router/index.js import { createRouter, createWebHistory } from 'vue-router'; import { useUserStore } from '@/stores/user'; const routes = [ { path: '/login', name: 'Login', component: () => import('@/views/LoginView.vue') }, { path: '/', component: () => import('@/views/ProductListView.vue') }, { path: '/cart', name: 'Cart', component: () => import('@/views/CartView.vue'), meta: { requiresAuth: true } }, { path: '/order/confirm', name: 'OrderConfirm', component: () => import('@/views/OrderConfirmView.vue'), meta: { requiresAuth: true } }, { path: '/pay', name: 'Pay', component: () => import('@/views/PayView.vue'), meta: { requiresAuth: true } }, { path: '/order/detail', name: 'OrderDetail', component: () => import('@/views/OrderDetailView.vue'), meta: { requiresAuth: true } } ]; const router = createRouter({ history: createWebHistory(), routes }); router.beforeEach((to) => { const userStore = useUserStore(); if (to.meta.requiresAuth && !userStore.isLoggedIn) { return { path: '/login', query: { redirect: to.fullPath } }; } return true; }); export default router;

这里使用meta.requiresAuth标识需要登录的页面。当用户未登录时,跳转到登录页并携带redirect参数,登录成功后回到原页面。这种实现方式在真实后台管理项目和电商项目中非常常用。

5. 商品浏览、购物车与订单提交

5.1 商品列表页基础实现

商品列表页不是本文核心,但为了让支付链路完整可见,这里给出一个最简实现思路:请求商品列表接口,渲染商品卡片,点击“加入购物车”后存放到 Pinia。

// 文件路径:src/stores/cart.js import { defineStore } from 'pinia'; export const useCartStore = defineStore('cart', { state: () => ({ items: [] }), getters: { totalCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0), totalAmount: (state) => state.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ) }, actions: { addItem(product) { const existing = this.items.find( (item) => item.productId === product.productId ); if (existing) { existing.quantity += 1; } else { this.items.push({ productId: product.productId, title: product.title, price: product.price, image: product.image, quantity: 1 }); } }, removeItem(productId) { this.items = this.items.filter( (item) => item.productId !== productId ); } } });

这里的price字段同样建议以“分”为单位。如果后端返回的是元,前端可以在展示层用过滤器转换为元,但计算时不要使用浮点数累加。

5.2 订单提交流程

购物车页面点击“去结算”后,跳转到订单确认页。订单确认页需要展示商品清单、收货地址和应付金额,然后调用创建订单接口。

以订单确认页中的提交函数为例:

const submitLoading = ref(false); async function submitOrder() { submitLoading.value = true; try { const params = { items: cartStore.items.map((item) => ({ productId: item.productId, quantity: item.quantity })), receiver: { name: receiverForm.name, mobile: receiverForm.mobile, address: receiverForm.address } }; const data = await createOrderApi(params); cartStore.clearCart(); router.push({ path: '/pay', query: { orderNo: data.orderNo } }); } finally { submitLoading.value = false; } }

创建订单后,前端不要本地修改订单状态,应该携带orderNo跳转到支付页。这一步在很多项目中会被忽略,导致用户直接刷新页面找不到订单号,支付流程无法继续。

api/order.js中定义:

// 文件路径:src/api/order.js import request from '@/utils/request'; export function createOrderApi(data) { return request.post('/orders', data); } export function getOrderDetailApi(orderNo) { return request.get(`/orders/${orderNo}`); } export function createNativePaymentApi(orderNo) { return request.post('/payments/native', { orderNo }); } export function getOrderPayStatusApi(orderNo) { return request.get(`/orders/${orderNo}/pay-status`); }

6. 支付模块完整实战:从预下单到状态轮询

6.1 支付页核心逻辑

支付页是整个流程中最关键的一环。页面接收订单号后,需要完成以下操作:

  1. 根据orderNo请求支付预下单接口,拿到codeUrl
  2. 使用qrcode库把codeUrl转为二维码图片。
  3. 展示“订单金额”和“二维码区域”。
  4. 启动定时器,每隔 1 到 3 秒查询一次订单支付状态。
  5. 当订单状态变为PAID后,停止轮询并跳转到订单详情页。

完整代码如下:

<!-- 文件路径:src/views/PayView.vue --> <template> <div class="pay-page"> <el-card class="pay-card"> <template #header> <span>订单支付</span> </template> <div v-if="loading" class="pay-loading"> 正在获取支付信息... </div> <div v-else-if="qrCodeDataUrl" class="pay-content"> <div class="amount-text"> 应付金额: <span class="amount">¥ {{ (payAmount / 100).toFixed(2) }}</span> </div> <img class="qr-img" :src="qrCodeDataUrl" alt="支付二维码" /> <div class="tip-text"> 请使用手机扫码完成支付 </div> <div class="pay-status"> 当前状态:{{ orderStatusText }} </div> </div> </el-card> </div> </template> <script setup> import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; import QRCode from 'qrcode'; import { createNativePaymentApi, getOrderDetailApi, getOrderPayStatusApi } from '@/api/order'; const route = useRoute(); const router = useRouter(); const orderNo = route.query.orderNo; const loading = ref(true); const qrCodeDataUrl = ref(''); const payAmount = ref(0); const orderStatus = ref('PENDING'); let timer = null; const orderStatusText = computed(() => { const map = { PENDING: '待支付', PAID: '已支付', CANCELLED: '已取消' }; return map[orderStatus.value] || orderStatus.value; }); onMounted(async () => { if (!orderNo) { ElMessage.error('缺少订单号'); return; } try { const detail = await getOrderDetailApi(orderNo); payAmount.value = detail.payAmount; const payData = await createNativePaymentApi(orderNo); qrCodeDataUrl.value = await QRCode.toDataURL(payData.codeUrl); timer = setInterval(pollPayStatus, 2000); } catch (error) { console.error('获取支付信息失败', error); } finally { loading.value = false; } }); async function pollPayStatus() { try { const res = await getOrderPayStatusApi(orderNo); orderStatus.value = res.orderStatus; if (res.orderStatus === 'PAID') { clearInterval(timer); timer = null; ElMessage.success('支付成功'); router.replace({ path: '/order/detail', query: { orderNo } }); } } catch (error) { console.error('查询支付状态失败', error); } } onBeforeUnmount(() => { if (timer) { clearInterval(timer); timer = null; } }); </script> <style scoped> .pay-page { display: flex; justify-content: center; padding: 40px 16px; background: #f5f7fa; min-height: 100vh; } .pay-card { width: 480px; text-align: center; } .amount-text { font-size: 16px; margin-bottom: 24px; } .amount { color: #e6a23c; font-size: 28px; font-weight: 600; } .qr-img { width: 240px; height: 240px; border: 1px solid #ebeef5; padding: 8px; background: #fff; } .tip-text { color: #909399; font-size: 14px; margin: 16px 0; } .pay-loading { padding: 40px 0; color: #909399; } </style>

6.2 支付状态轮询的设计要点

支付页中轮询的设计有几个容易被忽略的地方。

第一,轮询必须在组件销毁时关闭。用户直接关闭浏览器标签页会导致组件进入beforeUnmount,所以要把clearInterval放在onBeforeUnmount中,避免后台继续发起无意义的请求。

第二,轮询不能只查询一次就结束。二维码支付的特点是用户扫码时间不可控,所以需要每隔一定时间发起查询。间隔建议设置为 2 到 3 秒,太短会增加服务端压力,太长会让用户感觉延迟明显。

第三,支付成功后要使用router.replace而不是router.push。原因是用户从支付页到达订单详情后,如果点击浏览器返回按钮,不应该再回到“正在支付”的页面,否则可能出现重复支付提示。

第四,真实收款成功必须以服务端收到的支付回调为准,前端轮询结果只能作为展示参考。比如后端服务在自己收到微信或支付宝的异步通知后,才将订单状态更新为已支付。前端不能因为收到“PAID”就直接发货,否则可能造成刷单漏洞。

7. 订单详情页与支付结果的最终展示

当支付状态轮询到PAID后,页面跳转到订单详情页。订单详情页应该展示订单号、商品信息、实付金额、订单状态等信息。

这个页面相对简单,核心是调用订单详情接口,把后端数据回显到页面上。如果支付成功后订单状态仍然没有更新,可以增加一个手动“刷新状态”按钮,让用户主动查询。

<!-- 文件路径:src/views/OrderDetailView.vue --> <template> <div class="order-detail"> <el-card v-if="order" class="detail-card"> <template #header> <span>订单详情</span> </template> <el-descriptions :column="1" border> <el-descriptions-item label="订单号"> {{ order.orderNo }} </el-descriptions-item> <el-descriptions-item label="订单状态"> {{ statusText }} </el-descriptions-item> <el-descriptions-item label="支付金额"> ¥ {{ (order.payAmount / 100).toFixed(2) }} </el-descriptions-item> <el-descriptions-item label="收货信息"> {{ order.receiverName }} {{ order.receiverMobile }} </el-descriptions-item> <el-descriptions-item label="收货地址"> {{ order.receiverAddress }} </el-descriptions-item> </el-descriptions> <el-button v-if="order.orderStatus !== 'PAID'" type="primary" class="refresh-btn" @click="handleRefresh" > 刷新状态 </el-button> </el-card> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { ElMessage } from 'element-plus'; import { getOrderDetailApi } from '@/api/order'; const route = useRoute(); const order = ref(null); const orderNo = route.query.orderNo; const statusText = computed(() => { const map = { PENDING: '待支付', PAID: '已支付', CANCELLED: '已取消' }; return map[order.value?.orderStatus] || '未知'; }); onMounted(async () => { await loadDetail(); }); async function loadDetail() { try { order.value = await getOrderDetailApi(orderNo); } catch (error) { console.error('获取订单详情失败', error); } } async function handleRefresh() { await loadDetail(); if (order.value.orderStatus === 'PAID') { ElMessage.success('订单已支付'); } } </script> <style scoped> .order-detail { padding: 24px; max-width: 800px; margin: 0 auto; } .refresh-btn { margin-top: 16px; } </style>

至此,一条从登录、加购、下单到支付的完整前端闭环就打通了。

8. 常见问题排查

8.1 高频率问题速查表

问题现象常见原因解决思路
登录成功后跳转正常,刷新后登录态丢失token 只存在 Pinia,没有持久化登录后写入 localStorage
请求接口返回 401token 不存在、过期或格式错误检查拦截器 Authorization 头,登录后重新获取 token
后端返回了 code_url,但二维码图片不显示二维码内容未正确传递,或 qrcode 依赖未安装检查 codeUrl 是否为空,安装并引入 qrcode
支付二维码扫了但订单状态一直是待支付轮询间隔太长,或后端状态更新依赖回调检查订单状态查询接口,确认服务端是否收到支付回调
页面提示缺少订单号创建订单后跳转支付页时没有拼接 orderNo使用 route.query.orderNo 接参,跳转时携带 query
支付页面返回上一页仍可重复支付没有在支付成功后清理页面缓存使用 router.replace 跳转
金额计算出现小数误差前端直接使用浮点数计算金额金额统一使用分,展示时再转换为元

8.2 登录失败如何排查

如果你在本地联调时遇到登录失败,可以先确认几个位置:

  1. 请求地址是否正确。如果使用 Vite 开发代理,检查vite.config.js中的 proxy 配置。
  2. 后端返回的字段名是否与前端一致。有的后端返回token,有的返回accessToken,需要统一。
  3. 密码是否在传输前需要加密。真实项目中密码通常不会明文传输,可能需要先做 RSA 加密或使用 HTTPS。

Vite 开发代理参考配置:

// 文件路径:vite.config.js import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } });

8.3 二维码不显示但接口正常

如果后端返回正常,codeUrl也有值,但二维码区域空白,可以检查安装的依赖是qrcode还是qrcode.vue。本文使用的是qrcode,通过QRCode.toDataURL生成 base64 图片。

如果你更习惯组件式写法,也可以使用qrcode.vue,用法如下:

<template> <qrcode-vue :value="codeUrl" :size="240" /> </template> <script setup> import QrcodeVue from 'qrcode.vue'; import { ref } from 'vue'; const codeUrl = ref('https://example.com'); </script>

两种方式都可以,重点是根据项目习惯选择一种并保持一致。

9. 工程化与安全最佳实践

9.1 token 安全与权限控制

在真实电商项目中,token 的存储位置需要认真考虑。localStorage容易受到 XSS 攻击,cookie中的httpOnly可以降低风险,但要同时处理 CSRF 问题。

建议:

  • 不要把用户密码明文保存在前端任何位置。
  • 请求拦截器统一带上Authorization头。
  • 退出登录时清除本地 token 和用户信息。
  • 路由守卫只做页面级控制,真正的权限校验必须在后端接口完成。

9.2 支付安全注意事项

支付功能涉及资金安全,以下原则必须遵守:

  1. 订单金额以后端计算为准,前端绝不能自行修改金额。
  2. 支付预下单接口必须有登录鉴权,不接收前端传入的金额和商品明细。
  3. 订单状态更新以后端支付回调为准,不能只信任前端轮询结果。
  4. 测试环境使用支付宝沙箱或微信支付沙箱商户号,不要使用真实商户信息调试。
  5. 生产环境密钥由服务端保管,前端不直接接触商户私钥和签名密钥。
  6. 页面销毁时清除支付状态轮询定时器。

9.3 代码规范与可维护性

电商项目通常会持续迭代,建议从第一天就保持清晰的代码分层。

接口请求全部放入src/api目录,按模块拆分文件。每次后端接口变更时,只需要修改对应的 api 文件,不影响页面代码。

全局状态放入 Pinia,不要在多个组件中各自保存购物车副本,否则会出现各页面数据不同步的问题。

金额计算封装成通用方法。例如:

// 文件路径:src/utils/money.js export function formatPrice(cents) { return `¥ ${(cents / 100).toFixed(2)}`; }

所有金额展示统一走这个方法,可以避免大量重复的toFixed(2)代码。

10. 总结与下一步建议

本文从零梳理了一套 Vue3 电商应用从登录到支付的完整流程,重点代码集中在登录鉴权、Axios 拦截器、路由守卫、订单创建、支付二维码展示和支付状态轮询这几个阶段。

从登录到支付,最核心的思维不是分别写登录页和支付页,而是理解接口数据如何流动:登录拿到 token,token 通过拦截器自动注入后续请求;下单拿到 orderNo,orderNo 再用于支付预下单和状态轮询。只要这条链路通了,电商前端的主流程就完成了一大半。

如果你打算继续深入,可以从以下方向扩展:

  1. 接入真实后端,把 Mock 换成 Swagger 接口。
  2. 增加商品 SKU 和库存逻辑。
  3. 学习微信小程序中的wx.requestPayment,对比 H5 扫码支付与小程序支付的区别。
  4. 增加订单超时自动取消逻辑。
  5. 完善异常情况,例如支付成功回调延迟、退款流程、订单关闭流程。

第一次实现支付流程时,建议先用支付宝沙箱或微信 Native 支付跑通扫码流程,不要一上来就挑战复杂的 H5 内支付。扫码支付的链路最简单,也能让你更清晰地理解“预下单、展示二维码、轮询结果”这套后端与前端协作模型。等你理解以后,再切换到 JSAPI 或 App 支付,就会发现所有支付渠道的前端逻辑本质上是相同的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 4:40:56

SpringBoot+Vue高校社团管理系统实战架构

简介&#xff1a;这是一套面向计算机专业本科生的高校社团管理实战项目&#xff0c;专为毕业设计、课程设计及Java全栈开发练习打造&#xff0c;解决学生缺乏完整SpringBootVue前后端分离项目经验的痛点。资源包共813个文件&#xff0c;涵盖199个Java后端业务逻辑代码、141个Vu…

作者头像 李华
网站建设 2026/9/3 4:37:47

小天才Z11及以下机型ROOT教程:Magisk修补boot与fastboot刷入

小天才 Z11 及以下机型的 ROOT 教程&#xff0c;最近很多人都在问。与其说是“破解教程”&#xff0c;不如把它理解为一条 Android 定制系统上常见的 Magisk 路线&#xff1a;提取原厂 boot 镜像、用 Magisk 修补、再通过 fastboot 刷回&#xff0c;最后用 Magisk App 管理 Roo…

作者头像 李华
网站建设 2026/9/3 4:36:18

LSM6DS3TR-C与磁力计硬件级姿态解算实战

简介&#xff1a;本资源是一套面向嵌入式开发者与物联网算法工程师的STM32姿态解算实战方案&#xff0c;聚焦于LSM6DS3TR-C与LIS2MDL传感器协同工作下的高精度三维姿态估计问题。通过集成ST官方MotionFX中间件库&#xff0c;完整实现陀螺仪、加速度计与磁力计三轴数据融合&…

作者头像 李华
网站建设 2026/9/3 4:36:05

Luma AI 视频生成工作流:让尘封30年的老电影重新“成真”

一条“Luma AI 让尘封30年电影《Adrenaline Junkies》成真”的消息在影视创作圈和 AI 应用圈里传得很快。大多数讨论都盯着“AI 复活老项目”这个噱头&#xff0c;但作为技术人&#xff0c;我更关心的是它背后那条可以复制的工作流&#xff1a;一部30年前没拍完、没做出来&…

作者头像 李华
网站建设 2026/9/3 4:35:58

ZYNQ软硬协同设计全流程:从HLS加速器到Linux驱动的硬件加速系统实践

简介&#xff1a;本资源是一份面向嵌入式系统开发者与FPGA工程师的ZYNQ软硬协同设计实战文档&#xff0c;聚焦于卷积神经网络&#xff08;CNN&#xff09;硬件加速器的完整实现方案&#xff0c;解决深度学习算法在嵌入式平台部署时面临的计算延迟高、能效比低等核心问题。压缩包…

作者头像 李华