☰
Spring Boot + Vue3 + 微信小程序搭建运动户外商城全栈方案
2026/9/30 1:49:39 网站建设 项目流程

这几年以运动户外、骑行露营、健身装备为代表的生活方式消费增长非常快,市面上也出现大量“运动户外交易小程序”类项目。很多同学拿到需求后都会考虑用 Spring Boot 提供后端接口、Vue3 搭建后台管理系统、再用微信小程序做用户端商城,形成一套完整的电商闭环。本文将从项目全局出发,拆解运动户外交易平台的功能定位、技术选型、数据库设计、后端接口、Vue3 管理后台、小程序端商品交易流程,并且把开发和联调阶段最容易踩到的坑一起整理出来。

这篇内容更适合下面几类人阅读:准备做毕业设计或课程项目的同学、想了解“小程序 + 管理后台 + 后端接口”三类端如何协同的开发者、正在规划运动户外电商产品但没有完整技术方案的工程师。通读并动手实践后,你能理解电商项目的目录结构、接口设计思路、登录态处理方式、商品与订单模块的大致实现,以及正式上线前必须考虑的安全和交易风险。

1. 项目背景与核心功能拆解

1.1 项目解决什么问题

运动户外交易系统本质上是一个垂直电商平台。它不像京东、淘宝那样覆盖全品类,而是聚焦在“运动器械、户外装备、健身穿搭、露营用品”等场景,比如用户可以浏览帐篷、登山鞋、瑜伽垫、跑步服等商品,加入购物车后下单支付,管理员则在后台上架商品、管理分类、处理订单。

在线运动户外销售系统的核心不是页面好看,而是“商品信息可维护、商品可售卖、订单可跟踪、库存和状态可控制”。对于刚接触全栈项目的开发者来说,用一个真实业务把小程序的用户入口、管理后台和 Spring Boot 服务串起来,是理解企业级开发流程的最好方式。

1.2 核心角色与功能模块

整个系统可以拆成三个端:用户使用的小程序端、管理员使用的后台管理系统、统一提供接口的后端服务。三端对应的功能各有侧重。

端面向对象主要功能
微信小程序端普通用户商品浏览、分类检索、商品详情、购物车、创建订单、微信支付、订单查询、个人中心、收货地址管理
Vue3 后台管理端运营/管理员仪表盘统计、商品管理、分类管理、订单管理、用户管理、轮播图管理、系统配置
Spring Boot 后端服务三类端共用用户登录与 Token 签发、商品/分类 CRUD、购物车接口、订单接口、微信支付回调、文件上传、权限校验

1.3 为什么选择 Spring Boot + Vue3 + 小程序这套组合

从工程角度分析,运动户外电商平台的前后端分离模式非常合适:

  • Spring Boot 生态成熟,整合 MyBatis、Redis、微信支付 SDK、对象存储都很方便,适合快速开发中小型电商后端。
  • Vue3 + Vite 开发后台管理系统效率高,配合 Element Plus 可以快速搭建表格、表单、弹窗等中后台页面。
  • 微信小程序天然覆盖大量移动用户,不需要单独做 App,并且微信登录、微信支付能力可以直接为电商业务服务。

这也是为什么这类项目在毕设和企业内部系统中都很常见。开发者只需要维护一套后端接口,小程序端和管理端都能复用。

2. 环境准备与版本说明

2.1 基础开发环境

在开始搭建前,建议准备下面的开发环境。不同操作系统的安装包搜索官网即可,这里不推荐某个具体下载链接。

软件用途建议
JDK运行 Spring Boot 后端如果使用 Spring Boot 3.x,建议 JDK 17 及以上
Maven管理后端依赖也可以使用 IDE 自带 Maven
MySQL持久化业务数据推荐 5.7+ 或 8.0+
Redis缓存 Token、商品热点数据需要本机安装并启动,没有 Redis 时部分逻辑可以降级
Node.js运行 Vue3 项目建议使用 LTS 版本
HBuilderX开发/运行 uni-app 小程序或用微信开发者工具直接开发原生小程序
微信开发者工具预览和调试小程序需要注册小程序账号并获取 AppID
IDEA / VS Code编写代码IDEA 适合后端,VS Code 适合前端

2.2 Spring Boot 版本说明:不要被“SpringBoot4”误导

在项目标题中看到“SpringBoot4 + Vue3”这样的组合时,需要先说明一个容易让新手困惑的点:这类标题里的“SpringBoot4”更像资料命名习惯,不代表某个已发布并广泛使用的稳定大版本。在实际创建后端工程时,请不要为了标题硬选预览版或未知版本,应该使用 Spring Initializr 或 IDE 中提供的稳定版本。

本文代码以 Spring Boot 3.x 风格为主。Spring Boot 3 启用jakarta.*命名空间,如果大家使用的是 Spring Boot 2.7.x,则包名是javax.*,部分代码需要对应调整。还有一点,使用 Spring Boot 3 时 JDK 版本必须 17 以上,否则项目无法正常启动。

技术选型上,后端通常使用 Spring Boot + MyBatis-Plus + MySQL + Redis。如果同学的毕业设计不需要太复杂,也可以去掉 Redis,用数据库表存储 Token,但会牺牲一部分性能和安全性,建议直接引入 Redis。

2.3 项目规划

建议按下面结构拆分工程,方便维护:

sports-outdoor-mall ├── backend # Spring Boot 后端工程 │ ├── src/main/java/com/example/mall │ │ ├── common # 通用响应、异常、工具类 │ │ ├── config # 跨域、Redis、MyBatis-Plus 配置 │ │ ├── controller # 控制器 │ │ ├── entity # 实体类 │ │ ├── mapper # 数据访问层 │ │ ├── service # 业务层 │ │ └── SecurityUtil # 登录用户信息工具等 │ └── pom.xml ├── admin-ui # Vue3 后台管理前端 └── mp-app # 用户端小程序或者 uni-app 项目

后端项目尽量不要把所有类都写在启动类同一个包下面,也不要把商品表、分类表相关的代码全部堆到一个 Java 类中,否则功能扩展后会非常难维护。

3. 数据库设计与核心表结构

3.1 需求到数据库表

电商类项目无论是什么垂直品类,数据库表的核心都绕不开“用户 - 商品 - 订单”三个主链路。运动户外交易系统可以在此基础上增加分类、轮播图、购物车记录,使前端页面能够完整跑通。

下面是一张简化后的表关系:

  • 用户表:保存会员基础信息、微信 openid、昵称、头像。
  • 分类表:用于后台维护“露营、跑步、健身”等一级或二级分类。
  • 商品表:商品名称、图片、价格、库存、分类外键、状态。
  • 购物车表:用户 id、商品 id、数量。
  • 订单表:订单编号、用户 id、总金额、状态、收货地址快照。
  • 订单明细表:订单 id、商品 id、商品快照、购买价格、数量。

订单表与订单明细表拆分的原因是要还原下单那一刻的商品名称和价格。如果商品后续改名或涨价,用户历史订单仍然应该显示原来购买时的信息,所以订单明细中需要保存商品快照字段。

3.2 商品表与订单表 SQL 示例

下面给出该运动户外交易项目的核心建表 SQL,实际开发可以按团队规范增加公共字段和索引。

-- 商品表 CREATE TABLE `product` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `name` varchar(128) NOT NULL COMMENT '商品名称', `subtitle` varchar(255) DEFAULT NULL COMMENT '商品副标题', `main_image` varchar(500) DEFAULT NULL COMMENT '主图', `detail` text COMMENT '商品详情', `price` decimal(10,2) NOT NULL COMMENT '商品价格', `stock` int DEFAULT '0' COMMENT '库存', `sales` int DEFAULT '0' COMMENT '销量', `status` tinyint DEFAULT '1' COMMENT '状态:1上架 0下架', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运动户外商品表'; -- 订单主表 CREATE TABLE `order_info` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` varchar(64) NOT NULL COMMENT '订单编号', `user_id` bigint NOT NULL COMMENT '用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总额', `status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0待支付 1已支付 2已发货 3已完成 4已取消', `receiver_name` varchar(50) DEFAULT NULL COMMENT '收货人', `receiver_phone` varchar(20) DEFAULT NULL COMMENT '收货电话', `receiver_address` varchar(255) DEFAULT NULL COMMENT '收货地址', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';

商品表加idx_category索引是因为小程序首页和分类页经常按分类查询;订单表的order_no设置唯一键,是为了避免用户重复下单也能快速定位问题。

4. 后端 Spring Boot 接口服务搭建

4.1 创建工程与基础配置

创建 Spring Boot 工程后,在pom.xml中引入 Web、Redis、MyBatis-Plus、MySQL、Lombok 等依赖。具体版本应根据你新建工程时选择的 Spring Boot 版本而定,比如 Spring Boot 3.x 环境下,MyBatis-Plus 需要引入mybatis-plus-spring-boot3-starter。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>根据你选择的 Spring Boot 版本到 Maven 仓库查询</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

在application.yml中配置数据源、Redis、MyBatis-Plus 和文件上传大小限制:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sports_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 data: redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

map-underscore-to-camel-case开启后,数据库字段create_time可以自动映射到实体类的createTime属性,减少手写 ResultMap 的麻烦。

4.2 统一响应体和全局异常处理

为了让小程序端和管理后台在使用接口时保持一致的返回结构,后端需要定义一个统一响应对象。常见结构是code、message、data。

// 文件路径:backend/src/main/java/com/example/mall/common/Result.java package com.example.mall.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

如果不做统一异常处理,当库存不足或参数错误时,系统默认返回的异常堆栈对小程序的渲染很不友好。实际项目可以增加@RestControllerAdvice全局异常处理器,将BusinessException、参数校验异常等统一包装为Result.error(...)。这样做的好处是前端 axios 拦截器可以统一识别code,出现错误时弹出统一提示,不需要每个接口单独判断异常类型。

4.3 微信小程序登录接口实现

小程序端登录遵循“微信登录 code 换 openid,后端签发 Token”的流程。前端调用uni.login拿到临时code,后端使用code请求微信接口jscode2session,拿到用户唯一标识openid。

下面这是核心逻辑,需要把代码写在WxAuthController和对应 Service 中。

// 文件路径:backend/src/main/java/com/example/mall/controller/WxAuthController.java package com.example.mall.controller; import cn.hutool.core.util.StrUtil; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; @RestController @RequestMapping("/api/wx") public class WxAuthController { private final RestTemplate restTemplate; private final StringRedisTemplate redisTemplate; public WxAuthController(RestTemplate restTemplate, StringRedisTemplate redisTemplate) { this.restTemplate = restTemplate; this.redisTemplate = redisTemplate; } @PostMapping("/login") public Result<Map<String, Object>> login(@RequestBody Map<String, String> params) { String code = params.get("code"); if (StrUtil.isBlank(code)) { return Result.error(400, "code不能为空"); } // 实际开发时 appid 和 secret 应放入配置中心或环境变量,不要硬编码在代码中 String appid = "你的小程序AppId"; String secret = "你的小程序AppSecret"; String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; Map<String, Object> wxResult = restTemplate.getForObject(url, Map.class); if (wxResult == null || wxResult.get("openid") == null) { return Result.error(500, "微信登录失败"); } String openid = wxResult.get("openid").toString(); // 根据 openid 查询用户表,不存在则创建 // 这里省略 mapper 查询逻辑,假设 userId = 1L String token = UUID.randomUUID().toString().replace("-", ""); // 将 token 与用户关联保存到 Redis,并设置过期时间 redisTemplate.opsForValue().set("wx:login:token:" + token, openid, 7, TimeUnit.DAYS); Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("openid", openid); return Result.success(data); } }

这里把 Token 放入 Redis 而不是简单返回一个 userId,是因为小程序端后续所有接口都需要知道当前用户是谁。只靠前端传来的 userId 很容易被伪造,使用服务端保管的 Token 更安全。

需要额外提醒的是,AppSecret绝对不能出现在小程序端代码中,也尽量不要硬编码在后端代码里,生产环境应放到环境变量、配置中心或密钥管理服务中。

4.4 商品分页查询接口

商品查询是商城最核心的基础接口。小程序端首页、分类页、搜索页都会调用分页接口,因此商品 Controller 中需要提供按分类查、按关键字查的能力。

// 文件路径:backend/src/main/java/com/example/mall/controller/ProductController.java package com.example.mall.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @RestController @RequestMapping("/api/product") public class ProductController { @Resource private ProductMapper productMapper; @GetMapping("/page") public Result<Page<Product>> page(@RequestParam(defaultValue = "1") long page, @RequestParam(defaultValue = "10") long size, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Product::getStatus, 1); if (categoryId != null) { wrapper.eq(Product::getCategoryId, categoryId); } if (keyword != null && !keyword.trim().isEmpty()) { wrapper.like(Product::getName, keyword.trim()); } wrapper.orderByDesc(Product::getId); Page<Product> result = productMapper.selectPage(new Page<>(page, size), wrapper); return Result.success(result); } }

如果项目里没有引入javax.annotation.Resource而是使用 Spring Boot 3,需要改为jakarta.annotation.Resource。这段代码只是接口层示例,实际项目中建议在 Service 层做参数校验和 VO 转换,不要在 Controller 中直接操作 Mapper。

4.5 下单、支付回调与库存扣减思路

订单提交不能只是一条 INSERT 语句。完整流程是:校验商品状态和库存 -> 计算订单金额 -> 生成订单编号 -> 插入订单主表和明细表 -> 扣减库存 -> 调用微信支付下单接口。库存扣减这一步一定要放在数据库中做条件更新,不能先查库存再直接减,避免高并发下超卖。

下面是一个简化的库存扣减 SQL:

UPDATE product SET stock = stock - #{count} WHERE id = #{productId} AND stock >= #{count}

通过stock >= #{count}条件来保证并发请求下库存不会被扣成负数。如果受影响的记录数为 0,说明库存不足,业务端应该抛出异常并终止剩余流程。

支付回调环节要注意验签。微信支付成功后回调后端接口,后端必须根据微信支付平台证书验证签名,并核对订单金额是否一致,防止恶意伪造回调。验签成功后更新订单状态为已支付,同时更新支付时间。

5. Vue3 后台管理端模块开发

5.1 初始化 Vue3 工程

后台管理端面向运营人员,常用技术栈是 Vite + Vue3 + Pinia + Vue Router + Element Plus。可以用npm create vue@latest创建工程,也可以直接用npm create vite创建。创建完成后安装依赖:

npm install npm install element-plus npm install @element-plus/icons-vue npm install pinia npm install vue-router@4 npm install axios

创建后的项目目录中,src/views存放页面,src/api存接口封装,src/router存放路由,src/store存放 Pinia 状态。项目里建议使用import.meta.env.VITE_API_BASE_URL区分开发与生产环境接口地址。

5.2 axios 封装与登录拦截

后台管理端所有请求都需要携带管理员令牌。封装 axios 实例后,统一处理请求头中的Authorization,遇到 401 时跳回登录页。

// 文件路径:admin-ui/src/utils/request.ts import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 15000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('admin-token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求异常') return Promise.reject(new Error(res.message || '请求异常')) } return res }, error => { if (error.response?.status === 401) { localStorage.removeItem('admin-token') router.push('/login') } ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } ) export default request

response拦截器中先判断业务状态码code,再返回res。这样在具体页面中不需要每次写if (res.code !== 200)。

axios 拦截器是后台管理端很重要的一个工程化手段,几乎所有 Vue3 后台管理项目都使用同样思路。如果登录后一段时间内没有操作,后端返回 401,前端自动清除本地 Token 并跳转登录页,这是保证管理后台安全性的常见方法。

5.3 商品管理页面核心代码

商品管理页要完成的功能是:分页展示商品、搜索商品、上架/下架、编辑价格库存。下面用最简化的方式演示script setup语法:

<!-- 文件路径:admin-ui/src/views/product/ProductList.vue --> <template> <el-card> <el-form inline> <el-form-item label="商品名称"> <el-input v-model="query.keyword" placeholder="请输入商品名称" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="loadProduct">查询</el-button> </el-form-item> </el-form> <el-table :data="list" border stripe> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="商品名称" min-width="180" /> <el-table-column prop="price" label="价格" width="120" /> <el-table-column prop="stock" label="库存" width="120"> <template #default="{ row }"> <el-tag :type="row.stock > 0 ? 'success' : 'danger'"> {{ row.stock > 0 ? '有货' : '缺货' }} </el-tag> </template> </el-table-column> <el-table-column prop="status" label="状态" width="100"> <template #default="{ row }"> <el-tag :type="row.status === 1 ? 'primary' : 'info'"> {{ row.status === 1 ? '上架' : '下架' }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="200"> <template #default="{ row }"> <el-button size="small" @click="handleEdit(row)">编辑</el-button> <el-button size="small" :type="row.status === 1 ? 'danger' : 'success'" @click="changeStatus(row)" > {{ row.status === 1 ? '下架' : '上架' }} </el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" layout="total, prev, pager, next" @current-change="loadProduct" /> </el-card> </template> <script setup lang="ts"> import { reactive, ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import request from '@/utils/request' const list = ref([]) const total = ref(0) const query = reactive({ page: 1, size: 10, keyword: '' }) async function loadProduct() { const res: any = await request.get('/product/page', { params: query }) list.value = res.data.records total.value = res.data.total } function handleEdit(row: any) { // 实际项目一般会打开对话框,将 row 数据传入子组件 ElMessage.info(`编辑商品:${row.name}`) } async function changeStatus(row: any) { await request.put('/product/status', { id: row.id, status: row.status === 1 ? 0 : 1 }) ElMessage.success('操作成功') loadProduct() } onMounted(() => { loadProduct() }) </script>

这一段代码演示了 Vue3 组合式 API 的核心写法:reactive定义响应式查询对象,ref定义列表和总数,onMounted中加载数据,el-table通过插槽自定义状态列。如果你对 Vue2 比较熟悉,会发现 Vue3 中使用setup语法后,业务逻辑聚合度更高,不需要再把数据写在data()里。

5.4 路由守卫控制页面权限

后台管理系统一般会有多个角色,不同角色能访问的菜单不同。最简单的权限方案是在登录后把路由权限列表或角色标识保存到 Pinia 中,然后在全局前置守卫中判断目标路由是否需要登录和权限。

// 文件路径:admin-ui/src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/', component: () => import('@/layout/AdminLayout.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/Dashboard.vue') }, { path: 'product', component: () => import('@/views/product/ProductList.vue') }, { path: 'order', component: () => import('@/views/order/OrderList.vue') } ] } ] }) router.beforeEach((to) => { const token = localStorage.getItem('admin-token') if (to.path !== '/login' && !token) { return '/login' } return true }) export default router

路由守卫生效后,即使有人在地址栏手动输入商品管理 URL,未登录状态也会被重定向到登录页。但要注意,这只是一种前端交互层面的控制,后端接口仍然需要校验管理员身份,真正的安全边界永远在后端。

6. 微信小程序商城端开发

6.1 原生小程序还是 uni-app

运动户外交易小程序开发有两种主流方式:使用微信原生小程序,或者使用 uni-app 跨端框架。

如果用微信原生小程序,语法是 WXML、WXSS、JS/TS,优点是性能好、调试方便,但代码只能在微信小程序平台运行。如果用 uni-app 开发,可以使用 Vue3 语法,代码编译后可以生成小程序、H5、App 等多端产物,一套代码能覆盖更多发布渠道。

很多运动户外商城项目会选 uni-app,因为后端接口不变的情况下,以后做抖音小程序或 H5 端都能快速复用。如果你刚开始接触,建议先把 uni-app 的 Vue3 模板跑起来,再对照微信开发者工具的页面来学习。

6.2 请求封装与登录态初始化

小程序端同样不能每次请求都手写uni.request,需要封装一个统一请求方法。封装时要注意:请求头带 Token、响应体判断业务 code、网络异常统一 toast、登录过期后跳转登录页。

下面是一个适合 uni-app Vue3 项目的简单封装:

// 文件路径:mp-app/utils/request.ts const BASE_URL = 'http://localhost:8080/api' export function request<T = any>(options: { url: string method?: 'GET' | 'POST' | 'PUT' | 'DELETE' data?: any }): Promise<T> { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', Authorization: token ? `Bearer ${token}` : '' }, success: (res) => { const body = res.data as any if (body.code === 200) { resolve(body.data) } else if (body.code === 401) { uni.removeStorageSync('token') uni.navigateTo({ url: '/pages/login/login' }) reject(new Error(body.message)) } else { uni.showToast({ title: body.message || '请求失败', icon: 'none' }) reject(new Error(body.message)) } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }

小程序在生产环境中请求地址必须是https且在微信公众平台配置合法域名,本地开发调试时可以在微信开发者工具的“详情 -> 本地设置”中勾选“不校验合法域名”。仅本地开发可用,正式版本必须配置合法域名。

6.3 小程序首页商品列表渲染

小程序首页通常是搜索框、轮播图、分类导航、商品瀑布流。下面用最简化的方式演示商品列表渲染,重点在生命周期和循环渲染。

<!-- 文件路径:mp-app/pages/index/index.vue --> <template> <view class="product-list"> <view class="product-card" v-for="item in productList" :key="item.id" @click="goDetail(item.id)" > <image class="product-img" :src="item.mainImage" mode="aspectFill" /> <view class="product-name">{{ item.name }}</view> <view class="product-price">¥{{ item.price }}</view> </view> <view v-if="productList.length === 0" class="empty">暂无商品</view> </view> </template> <script setup lang="ts"> import { ref, onLoad } from '@dcloudio/uni-app' import { request } from '@/utils/request' const productList = ref<any[]>([]) onLoad(async () => { const data = await request<any>({ url: '/product/page', method: 'GET', data: { page: 1, size: 10 } }) productList.value = data.records }) function goDetail(id: number) { uni.navigateTo({ url: `/pages/product/detail?id=${id}` }) } </script>

运动户外商品图片通常比较多,mode="aspectFill"在裁剪显示时效果比较统一。如果在模拟器里看不到图片,需要确认商品图片地址后端能否直接访问,不要使用带跨域限制的本地图片路径。

6.4 商城交易闭环

小程序端的交易闭环有几个关键路径:

  1. 用户把户外商品加入购物车后进入购物车页。
  2. 提交订单时后端重新计算金额并生成订单。
  3. 选择“微信支付”,后端返回支付参数,小程序调用uni.requestPayment拉起微信支付。
  4. 支付成功后刷新订单状态,后端通过支付回调完成订单流转。

下单页最需要注意的是:不要信任前端传上来的商品价格。比如商品单价应以数据库最新价格为准,购买数量后端还要再做一次库存校验。商品价格被篡改是电商项目最常见的安全漏洞。

调用微信支付的代码例子如下:

const result = await request({ url: '/order/pay', method: 'POST', data: { orderNo } }) uni.requestPayment({ provider: 'wxpay', timeStamp: result.timeStamp, nonceStr: result.nonceStr, package: result.package, signType: result.signType, paySign: result.paySign, success: () => { uni.showToast({ title: '支付成功' }) }, fail: () => { uni.showToast({ title: '支付取消', icon: 'none' }) } })

requestPayment中传入的timeStamp、paySign等参数是由后端调用微信支付统一下单接口后返回的。小程序端只负责拉取支付面板,不能自己生成签名。

7. 运行联调与常见问题排查

7.1 从后端启动到小程序联调

一个运动户外交易小程序能跑通整个购物流程,联调顺序可以按下面的步骤推进:

  1. 启动 MySQL 和 Redis,导入数据库 SQL。
  2. 启动 Spring Boot 后端项目,访问http://localhost:8080/api/product/page能返回数据。
  3. 启动 Vue3 后台管理端,确认能登录并查看商品列表。
  4. 启动 HBuilderX 中的 uni-app 项目或导入微信原生小程序项目。
  5. 将小程序端请求地址指向后端本机接口。

如果在第 5 步遇到网络请求失败,优先排查小程序开发工具中的“合法域名校验”设置,以及后端服务是否监听在能被本机访问的端口。

7.2 高频问题速查表

问题现象常见原因解决思路
Spring Boot 启动失败JDK 版本过低或数据库连接不上检查 JDK 是否 17+,检查 MySQL 服务是否启动
前端请求后端接口跨域后端未配置跨域,或请求地址写错后端增加 CorsConfig,或使用代理访问
HBuilderX 运行到微信开发者工具提示“不是开发者”微信开发者工具登录的账号不是该小程序项目成员扫码登录,并在微信公众平台将该账号添加为项目成员
小程序模拟器请求 localhost 失败微信开发者工具未关闭合法域名校验在本地设置中勾选“不校验合法域名”
修改了 AppID 后模拟器仍是旧 AppID项目缓存或 manifest.json 未同步重新编译并确认 manifest.json / project.config.json 中的 AppID
Vue3 后台页面 Element Plus 图标不显示只安装了组件库没有注册图标全局注册@element-plus/icons-vue或按需引入
登录成功但访问商品接口显示 401Token 没有放到请求头,或 Token 过期检查请求拦截器,确认 Redis 中 Token 未过期
用户支付成功但订单状态没更新支付回调地址不可达或验签未通过查看后端日志,确认回调接口能接收到微信通知

7.3 一个真实高频问题的排查举例

很多开发者在本地联调时会遇到这样的报错:小程序 request 请求后可以看到数据,但真机预览时一直转圈或提示“url not in domain list”。

原因是微信小程序真机环境强制校验合法域名,http://localhost或局域网 IP 不是配置过的合法域名。排查步骤:

  1. 在微信开发者工具中先使用“预览”生成二维码。
  2. 打开微信公众平台后台,在小程序账号的“开发管理 -> 开发设置 -> 服务器域名”中添加后端正式域名。
  3. 如果只是临时测试,可以点击微信开发者工具右上角“详情 -> 本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。

最常见的误区是只在本地开发中改了前端地址,却没有确认手机和电脑是否在同一局域网,或者后端服务没有监听0.0.0.0。可以用同一局域网内的另一台设备访问后端接口地址来确认服务可达性。

8. 工程化最佳实践

8.1 后端分层与编码规范

业务较复杂的运动户外商城,后端建议严格分 Controller、Service、Mapper 层,Controller 只负责参数接收与结果包装,Service 负责业务规则,Mapper 负责数据操作。实体类不要直接从数据库返回给前端,而是定义 VO 或 DTO,隐藏库存等内部信息。

接口路径按照资源命名,比如/api/product/page、/api/order/pay。管理端接口与小程序端接口如果权限完全不同,可以把路径前缀分开成/admin/**和/app/**,方便做不同的认证策略。

8.2 安全设计

安全是所有电商项目不能省略的部分。小程序端与后台管理端应使用两套 Token 体系,至少也要在 Token 中标记角色,避免普通用户访问管理员接口。

  • 用户密码加密使用 BCrypt,不使用 MD5。
  • 小程序 AppSecret、微信支付商户密钥只能保存在后端。
  • 管理端接口必须校验管理员角色,不能只靠前端隐藏路由。
  • 商品库存扣减要使用数据库条件更新,防止超卖。
  • 微信支付回调必须验签并校验订单金额。

如果项目上线后需要接入更多营销活动,例如秒杀、优惠券,建议引入 Redis 分布式锁或消息队列处理高并发,而不是直接改数据库表。

8.3 配置与日志

Spring Boot 项目中数据库密码、Redis 密码、微信支付密钥都属于敏感配置,原则是“不提交到 Git 仓库”。开发环境可以放在本地application-dev.yml,生产环境使用环境变量或配置中心。

日志方面至少打印这几类信息:用户登录成功/失败的 openid、订单提交金额与状态、支付回调结果、库存扣减失败记录。每次改动订单状态或商品状态前,最好都打一条包含操作人、时间和旧状态/新状态的日志,方便出问题时回溯。

实际开发中,不要在日志中打印完整的paySign、AppSecret、数据库密码等敏感数据,否则日志文件泄露也会造成风险。

9. 总结与继续优化建议

一个基于 Spring Boot + Vue3 + 小程序的运动户外交易平台,核心价值在于把商品管理、用户登录、购物车、订单、支付回调这一整条电商链路串联起来。开发过程至少会经过数据库设计、后端接口开发、后台管理页面开发、小程序页面开发、本地联调和上线部署几个阶段,其中最值得投入精力的不是页面样式,而是登录授权、订单状态流转和支付安全这些边界问题。

建议下一步按顺序做三件事:先把自己项目的商品、订单主流程完整跑通;再给后端接口补上管理员鉴权和参数校验;最后把支付回调、库存扣减、接口日志这些稳定性细节优化到位。等你把基础商城闭环掌握之后,可以继续扩展会员积分、运动课程预约、社区内容分享、优惠券营销等模块,一步步让运动户外交易系统更贴近真实商业项目。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询