ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot + Vue3 + 微信小程序搭建运动户外商城全栈方案

2026/9/4 5:11:54 拓冰建站 浏览量
Spring Boot + Vue3 + 微信小程序搭建运动户外商城全栈方案 这几年以运动户外、骑行露营、健身装备为代表的生活方式消费增长非常快市面上也出现大量“运动户外交易小程序”类项目。很多同学拿到需求后都会考虑用 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 自带 MavenMySQL持久化业务数据推荐 5.7 或 8.0Redis缓存 Token、商品热点数据需要本机安装并启动没有 Redis 时部分逻辑可以降级Node.js运行 Vue3 项目建议使用 LTS 版本HBuilderX开发/运行 uni-app 小程序或用微信开发者工具直接开发原生小程序微信开发者工具预览和调试小程序需要注册小程序账号并获取 AppIDIDEA / 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version根据你选择的 Spring Boot 版本到 Maven 仓库查询/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency在application.yml中配置数据源、Redis、MyBatis-Plus 和文件上传大小限制spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sports_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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.StdOutImplmap-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 ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT 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 ResultMapString, Object login(RequestBody MapString, 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_typeauthorization_code; MapString, 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); MapString, 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 ResultPageProduct page(RequestParam(defaultValue 1) long page, RequestParam(defaultValue 10) long size, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { LambdaQueryWrapperProduct 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); PageProduct 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 语句。完整流程是校验商品状态和库存 - 计算订单金额 - 生成订单编号 - 插入订单主表和明细表 - 扣减库存 - 调用微信支付下单接口。库存扣减这一步一定要放在数据库中做条件更新不能先查库存再直接减避免高并发下超卖。下面是一个简化的库存扣减 SQLUPDATE 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 vuelatest创建工程也可以直接用npm create vite创建。创建完成后安装依赖npm install npm install element-plus npm install element-plus/icons-vue npm install pinia npm install vue-router4 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 requestresponse拦截器中先判断业务状态码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-modelquery.keyword placeholder请输入商品名称 clearable / /el-form-item el-form-item el-button typeprimary clickloadProduct查询/el-button /el-form-item /el-form el-table :datalist border stripe el-table-column propid labelID width80 / el-table-column propname label商品名称 min-width180 / el-table-column propprice label价格 width120 / el-table-column propstock label库存 width120 template #default{ row } el-tag :typerow.stock 0 ? success : danger {{ row.stock 0 ? 有货 : 缺货 }} /el-tag /template /el-table-column el-table-column propstatus label状态 width100 template #default{ row } el-tag :typerow.status 1 ? primary : info {{ row.status 1 ? 上架 : 下架 }} /el-tag /template /el-table-column el-table-column label操作 width200 template #default{ row } el-button sizesmall clickhandleEdit(row)编辑/el-button el-button sizesmall :typerow.status 1 ? danger : success clickchangeStatus(row) {{ row.status 1 ? 下架 : 上架 }} /el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.page v-model:page-sizequery.size :totaltotal layouttotal, prev, pager, next current-changeloadProduct / /el-card /template script setup langts 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 requestT any(options: { url: string method?: GET | POST | PUT | DELETE data?: any }): PromiseT { 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 classproduct-list view classproduct-card v-foritem in productList :keyitem.id clickgoDetail(item.id) image classproduct-img :srcitem.mainImage modeaspectFill / view classproduct-name{{ item.name }}/view view classproduct-price¥{{ item.price }}/view /view view v-ifproductList.length 0 classempty暂无商品/view /view /template script setup langts import { ref, onLoad } from dcloudio/uni-app import { request } from /utils/request const productList refany[]([]) onLoad(async () { const data await requestany({ 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运动户外商品图片通常比较多modeaspectFill在裁剪显示时效果比较统一。如果在模拟器里看不到图片需要确认商品图片地址后端能否直接访问不要使用带跨域限制的本地图片路径。6.4 商城交易闭环小程序端的交易闭环有几个关键路径用户把户外商品加入购物车后进入购物车页。提交订单时后端重新计算金额并生成订单。选择“微信支付”后端返回支付参数小程序调用uni.requestPayment拉起微信支付。支付成功后刷新订单状态后端通过支付回调完成订单流转。下单页最需要注意的是不要信任前端传上来的商品价格。比如商品单价应以数据库最新价格为准购买数量后端还要再做一次库存校验。商品价格被篡改是电商项目最常见的安全漏洞。调用微信支付的代码例子如下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 从后端启动到小程序联调一个运动户外交易小程序能跑通整个购物流程联调顺序可以按下面的步骤推进启动 MySQL 和 Redis导入数据库 SQL。启动 Spring Boot 后端项目访问http://localhost:8080/api/product/page能返回数据。启动 Vue3 后台管理端确认能登录并查看商品列表。启动 HBuilderX 中的 uni-app 项目或导入微信原生小程序项目。将小程序端请求地址指向后端本机接口。如果在第 5 步遇到网络请求失败优先排查小程序开发工具中的“合法域名校验”设置以及后端服务是否监听在能被本机访问的端口。7.2 高频问题速查表问题现象常见原因解决思路Spring Boot 启动失败JDK 版本过低或数据库连接不上检查 JDK 是否 17检查 MySQL 服务是否启动前端请求后端接口跨域后端未配置跨域或请求地址写错后端增加 CorsConfig或使用代理访问HBuilderX 运行到微信开发者工具提示“不是开发者”微信开发者工具登录的账号不是该小程序项目成员扫码登录并在微信公众平台将该账号添加为项目成员小程序模拟器请求 localhost 失败微信开发者工具未关闭合法域名校验在本地设置中勾选“不校验合法域名”修改了 AppID 后模拟器仍是旧 AppID项目缓存或 manifest.json 未同步重新编译并确认 manifest.json / project.config.json 中的 AppIDVue3 后台页面 Element Plus 图标不显示只安装了组件库没有注册图标全局注册element-plus/icons-vue或按需引入登录成功但访问商品接口显示 401Token 没有放到请求头或 Token 过期检查请求拦截器确认 Redis 中 Token 未过期用户支付成功但订单状态没更新支付回调地址不可达或验签未通过查看后端日志确认回调接口能接收到微信通知7.3 一个真实高频问题的排查举例很多开发者在本地联调时会遇到这样的报错小程序 request 请求后可以看到数据但真机预览时一直转圈或提示“url not in domain list”。原因是微信小程序真机环境强制校验合法域名http://localhost或局域网 IP 不是配置过的合法域名。排查步骤在微信开发者工具中先使用“预览”生成二维码。打开微信公众平台后台在小程序账号的“开发管理 - 开发设置 - 服务器域名”中添加后端正式域名。如果只是临时测试可以点击微信开发者工具右上角“详情 - 本地设置”勾选“不校验合法域名、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 小程序的运动户外交易平台核心价值在于把商品管理、用户登录、购物车、订单、支付回调这一整条电商链路串联起来。开发过程至少会经过数据库设计、后端接口开发、后台管理页面开发、小程序页面开发、本地联调和上线部署几个阶段其中最值得投入精力的不是页面样式而是登录授权、订单状态流转和支付安全这些边界问题。建议下一步按顺序做三件事先把自己项目的商品、订单主流程完整跑通再给后端接口补上管理员鉴权和参数校验最后把支付回调、库存扣减、接口日志这些稳定性细节优化到位。等你把基础商城闭环掌握之后可以继续扩展会员积分、运动课程预约、社区内容分享、优惠券营销等模块一步步让运动户外交易系统更贴近真实商业项目。