
选商城类项目练手最怕的不是代码报错而是项目“看起来完整、实际上没打通”。很多同学拿到的健身器材电商项目要么只有一堆管理后台页面要么是小程序静态界面数据库表和订单状态完全对不上结果毕设答辩只能反复演示同一个截图。这篇写一个能跑通的思路围绕基于 Spring Boot Vue3 微信小程序的健身器材交易系统把用户端、管理后台、后端接口这条主线拆到底。你要重点关注的不是“健身器材”这四个字而是它背后清晰的业务主流程注册登录、商品管理、购物车、订单状态、支付回调、后台发货、确认收货。这类垂直品类商城项目比通用百货电商更适合拿来当学习样板。健身器材有品牌、分类、规格、库存、重量等属性数据模型比简单博客系统复杂又不像秒杀系统那样需要一堆中间件。用它理解前后端分离和交易闭环难度刚好。读完这篇你会搞清楚三件事一套商城系统前后端到底该分成哪些模块订单状态和库存扣减怎么设计才不容易出事故小程序端登录、请求封装和管理后台之间是如何协作的。1. 这是怎样一个项目定位、角色与适用人群从标题看这个项目的定位是“健身器材交易小程序”本质是一个面向 C 端用户的垂直电商平台。它不是一个单机 Demo也不是只有商品展示的静态页面而是包含用户交易和后台管理的真实业务系统。项目通常包含三个端用户端微信小程序面向普通健身爱好者。功能包括小程序登录、浏览分类、搜索器材、查看商品详情、加入购物车、提交订单、查看订单、确认收货、发表评价等。管理端Vue3 后台面向运营人员。功能包括经营数据看板、商品分类维护、商品上架管理、库存管理、订单处理、发货、用户管理等。服务端Spring Boot 项目为小程序和管理后台提供统一 RESTful API负责业务逻辑、权限校验、数据持久化、微信登录凭证校验、支付回调处理等。对学习者的价值在于这是典型的“小程序 管理后台 服务端”三段式架构。你在简历里写“独立开发前后端分离商城项目”如果没有这种完整结构很容易在面试中被问倒。这个项目比较适合以下几类人准备毕业设计或课程设计的学生。业务模块典型功能量适中论文和演示都有素材。想从前端转全栈的开发者。可以先不纠结高并发把一个交易流程做成闭环理解后端接口设计。准备给线下健身房或器材经销商做线上商城的技术人员。业务范围小可以按需裁剪。不适合的场景也很明显如果目标是做一个支持百万用户、复杂营销玩法的大型电商平台这个项目形态需要做大量架构升级。它不是为高并发设计的商城核心价值在于业务完整度和学习效率。2. 技术栈与架构设计先统一认识这是一套前后端分离系统。这里解释一下前后端分离的价值。传统单体项目通常让后端渲染页面Java 代码既要写接口又要写 HTML。健身器材商城如果需要支持微信小程序这种模式很别扭因为小程序根本不需要服务端渲染 HTML它只需要 JSON 数据。所以更合理的做法是Spring Boot 只负责提供 API返回 JSON。Vue3 管理后台通过 HTTP 调用后端接口渲染成运营人员看到的页面。微信小程序直接请求后端接口把数据渲染到手机端。这三者之间不直接共享代码只通过约定好的接口文档或类型定义通信。开发时经常需要并行推进这就是前后端分离的核心原因。常见的技术选型大致如下端技术栈主要用途服务端Spring Boot、MyBatis-Plus、MySQL、Redis鉴权、商品、订单、支付回调管理后台Vue3、Vite、Element Plus、Pinia、Axios商品管理、订单处理、数据看板小程序端原生微信小程序或 uni-app浏览商品、下单、支付、个人中心数据存储MySQL Redis持久化数据、验证码/热点数据缓存需要注意标题里出现了 SpringBoot4 这种写法。实际开发时不必过度纠结这个名字它更像资料站对新一代 Spring Boot 版本的计数习惯。真正写代码时以你本地 Maven 仓库和官方稳定版为准。如果项目使用 Spring Boot 3.x则注意包名是jakarta.*如果使用 2.7则是javax.*。底层核心思路是一样的即使版本不同也不会影响你对本章内容的理解。后端分层建议遵循标准结构controller接收前端请求做参数校验。service写核心业务逻辑例如库存扣减和订单生成。mapper负责数据库持久化操作。entity与数据库表对应的实体类。dto/vo接收前端传入的数据、封装返回给前端的数据。管理后台技术选型中Vue3 带来两个关键改变组合式 API 让逻辑更集中响应式系统更容易追踪数据变化。Element Plus 能快速搭出表格、表单、弹窗这类后台管理界面。如果之前的项目停留在 Vue2 Element UI换成 Vue3 Element Plus 时要注意组件注册方式和部分属性名差异。3. 核心功能模块拆解健身器材交易系统从功能上可以拆成三大块小程序用户端、管理后台业务端、后端支撑模块。先从用户视角看小程序端。用户打开小程序后看到首页轮播图和分类入口通常包括哑铃、跑步机、瑜伽垫、运动护具等分类。点击分类进入商品列表可以按销量、价格、新品排序。搜索框支持按商品名称模糊查询。进入商品详情页后用户可以查看参数、库存和商品介绍选择数量后加入购物车或立即购买。购物车和用户体系绑定。如果用户没有登录点击加入购物车会先跳转登录页。小程序端登录依赖微信生态用户授权后小程序通过wx.login拿到临时 code后端再用 code 换取 openid。这个 openid 就是用户在系统里的唯一标识。提交订单是整个系统的关键也是很多初学项目容易忽略的地方。创建订单时必须多表操作查询商品、计算价格、扣减库存、生成订单主表、生成订单明细表。这些操作要么全部成功要么全部失败后端必须用事务保证。从运营人员视角看管理后台。管理员先登录后台看到数据看板今日订单数、支付金额、商品总数、待发货数量。商品管理模块可以新增商品、修改价格和库存、上下架。分类管理维护健身器材分类树。订单管理支持按订单号搜索查看订单详情执行发货操作。用户管理可以查看注册用户列表必要时禁用异常账号。后端支撑模块则负责把这些能力串起来包括统一返回结构、全局异常处理、JWT 用户身份识别、接口权限拦截、文件上传处理、微信支付回调验签。每个模块都可以单独再拆但对单体商城系统来说重点是把这些层分清楚不要把业务逻辑堆在 Controller 中。4. 数据库设计先定数据模型再写后端商城项目最容易返工的地方是数据库表设计。很多新手上来就建一张很大的商品表把所有字段塞进去后面商品要增加参数才发现表结构不好扩展。健身器材商城采用垂直电商结构核心表可以拆为用户表、商品分类表、商品表、购物车表、订单表、订单项表、收货地址表、评价表、收藏表。商品表的核心字段包括分类 ID、商品名称、主图、价格、库存、单位、销量、是否上架、排序权重。价格用DECIMAL不要用FLOAT和DOUBLE因为二进制浮点数在金额运算中会出现精度丢失。库存字段必须是非负数扣库存时要加条件判断防止超卖。订单表建议将状态字段设计为TINYINT或INT用数字表示状态注释里写明含义0 待付款、1 待发货、2 待收货、3 已完成、4 已取消。这样后端处理方便索引也快。订单金额字段记录下单时的价格快照为什么不用实时查询商品价格因为商品价格可能在下单后被修改但用户这笔订单成交金额不应该被影响。下面给出一份简化的核心建表 SQL方便你理解字段设计思路CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 用户ID, openid varchar(64) NOT NULL COMMENT 微信openid, nickname varchar(64) DEFAULT COMMENT 昵称, avatar varchar(255) DEFAULT COMMENT 头像地址, phone varchar(20) DEFAULT COMMENT 手机号, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT COMMENT 商品ID, category_id bigint NOT NULL COMMENT 分类ID, name varchar(100) NOT NULL COMMENT 商品名称, main_image varchar(255) DEFAULT COMMENT 商品主图, price decimal(10,2) NOT NULL COMMENT 销售价格, original_price decimal(10,2) DEFAULT NULL COMMENT 原价, stock int NOT NULL COMMENT 库存, sales int DEFAULT 0 COMMENT 销量, status tinyint DEFAULT 1 COMMENT 状态1上架 0下架, detail text COMMENT 商品详情, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健身器材商品表; CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint NOT NULL COMMENT 用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, pay_amount decimal(10,2) NOT NULL COMMENT 实付金额, status tinyint NOT NULL DEFAULT 0 COMMENT 订单状态0待付款 1待发货 2待收货 3已完成 4已取消, address_id bigint NOT NULL COMMENT 收货地址ID, pay_time datetime DEFAULT NULL COMMENT 支付时间, delivery_time datetime DEFAULT NULL COMMENT 发货时间, finish_time datetime DEFAULT NULL COMMENT 完成时间, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE order_item ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, order_id bigint NOT NULL COMMENT 订单ID, product_id bigint NOT NULL COMMENT 商品ID, product_name varchar(100) NOT NULL COMMENT 商品名称快照, product_image varchar(255) DEFAULT COMMENT 商品图片快照, price decimal(10,2) NOT NULL COMMENT 成交单价, quantity int NOT NULL COMMENT 购买数量, subtotal decimal(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表; CREATE TABLE cart ( id bigint NOT NULL AUTO_INCREMENT COMMENT 购物车ID, user_id bigint NOT NULL COMMENT 用户ID, product_id bigint NOT NULL COMMENT 商品ID, quantity int NOT NULL COMMENT 数量, selected tinyint DEFAULT 1 COMMENT 是否勾选, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表;这里做了两个关键设计订单表和订单明细表分离是为了让一张订单可以包含多个商品订单明细冗余商品名称和图片快照是为了防止后续商品改名或删除后历史订单无法追溯。购物车表对user_id和product_id建唯一索引同一个用户添加同一个商品只更新数量不产生重复记录。如果是毕业设计建议把地址表、评价表、收藏表和轮播图表也补上业务会显得更完整。收货地址表至少要有user_id、收货人姓名、手机号、省市区、详细地址。评价表关联订单项或商品记录评分和文字内容。收藏表用user_idproduct_id唯一索引。5. 后端工程实现与核心代码解析后端工程的代码组织决定了项目后期维护成本。下面是一个比较清晰的包结构com.example.fitness ├── FitnessApplication.java ├── common │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config │ ├── CorsConfig.java │ └── WebMvcConfig.java ├── controller │ ├── AuthController.java │ ├── ProductController.java │ ├── CartController.java │ └── OrderController.java ├── service │ ├── ProductService.java │ ├── CartService.java │ └── OrderService.java ├── mapper │ ├── ProductMapper.java │ ├── CartMapper.java │ └── OrderMapper.java ├── entity │ ├── User.java │ ├── Product.java │ └── Orders.java └── dto ├── LoginDTO.java └── OrderCreateDTO.java关键依赖中Spring Boot Web 负责提供 HTTP 接口MySQL 驱动和 MyBatis-Plus 负责数据持久化Redis 可选用于缓存验证码、用户会话和热点数据JWT 库用于生成和解析登录令牌。下面是一段 Maven 依赖示例版本号需要结合你的 Spring Boot 父工程确定不必照抄dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency在application.yml里配置数据源、Redis 和项目启动端口。本地开发时数据库地址、账号密码要和你的 MySQL 实际环境保持一致。server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/fitness_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true商品列表接口是第一个高频接口小程序端首页和管理后台的商品管理都会复用。下面用一个 MyBatis-Plus 的示例来演示后端如何对商品分页查询// ProductController.java RestController RequestMapping(/api/product) public class ProductController { Resource private ProductService productService; GetMapping(/list) public ResultIPageProductVO list( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { PageProduct page new Page(pageNum, pageSize); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1); if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } if (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword); } wrapper.orderByDesc(Product::getCreateTime); IPageProduct resultPage productService.page(page, wrapper); // 实际项目中建议将 Entity 转换为 VO 后返回避免把 detail 等重量级字段全部暴露 return Result.success(resultPage); } }需要注意Controller 层不应该直接返回数据库实体类。如果实体类里包含大字段detail列表接口就不需要查询它否则接口响应体积会明显变大用户滑动小程序时会觉得页面卡顿。更合理的做法是定义ProductVO列表里只放 id、名称、主图、价格、销量这些轻量字段详情接口再返回完整介绍。接口统一返回结构也需要提前设计。每个接口返回固定格式例如{ code: 200, message: success, data: {} }这个结构乍看多此一举真正联调时会发现它很有用。小程序端 Axios 或wx.request只需要判断code业务异常也能统一弹出提示而不是把后端堆栈直接暴露给用户。6. 下单核心购物车、订单状态与库存扣减下单是电商系统中最能体现后端功力的模块。购物车里可能同时有多件商品用户点击提交订单后后端需要做至少五件事从购物车读取用户勾选的商品和数量。逐一检查商品状态是否上架。统计总金额。扣减库存。生成订单主表和订单明细表。这五步不能分多次请求完成否则可能用户提交到一半网络闪断库存却已扣减或者订单生成了但购物车没有清空。Spring Boot 中简单可靠的方式是使用Transactional注解让方法内的数据库操作处于同一个事务中中途出现异常则全部回滚。下面给出一个简化的下单 Service 写法// OrderService.java Service public class OrderService { Resource private ProductMapper productMapper; Resource private OrderMapper orderMapper; Resource private OrderItemMapper orderItemMapper; Resource private CartMapper cartMapper; Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, OrderCreateDTO dto) { // 1. 生成订单编号实际项目可以用雪花算法或日期随机数 String orderNo FK System.currentTimeMillis(); Orders order new Orders(); order.setOrderNo(orderNo); order.setUserId(userId); order.setAddressId(dto.getAddressId()); order.setStatus(0); order.setTotalAmount(BigDecimal.ZERO); BigDecimal totalAmount BigDecimal.ZERO; // 2. 遍历提交的商品项 for (OrderItemDTO itemDTO : dto.getItems()) { // 加锁或使用乐观扣减防止库存错误 Product product productMapper.selectById(itemDTO.getProductId()); if (product null || product.getStatus() ! 1) { throw new BusinessException(商品不存在或已下架); } // 关键带库存条件的 update 语句 int rows productMapper.deductStock(itemDTO.getProductId(), itemDTO.getQuantity()); if (rows 0) { throw new BusinessException(商品库存不足 product.getName()); } OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setProductImage(product.getMainImage()); item.setPrice(product.getPrice()); item.setQuantity(itemDTO.getQuantity()); BigDecimal subtotal product.getPrice().multiply(BigDecimal.valueOf(itemDTO.getQuantity())); item.setSubtotal(subtotal); orderItemMapper.insert(item); totalAmount totalAmount.add(subtotal); } order.setTotalAmount(totalAmount); order.setPayAmount(totalAmount); orderMapper.insert(order); // 3. 删除购物车中已下单商品 cartMapper.deleteByUserIdAndProductIds(userId, dto.getItems()); return order.getId(); } }这段代码里扣减库存不是先读出来再更新而是直接在 SQL 里做条件更新UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}stock quantity这个条件非常关键它是防止库存扣成负数的最后一道防线。在高并发场景下即使多个请求同时进入也只可能有一个请求成功扣减因为行锁会让后来的更新失败返回受影响行数为 0。订单状态如果只在数据库里存一个数字逻辑容易混乱。建议在项目里建一个状态常量类或枚举。以本项目为例状态流转方向大致是状态码状态触发动作下一步0待付款用户提交订单用户支付或取消1待发货用户支付成功回调商家后台发货2待收货商家发货用户确认收货3已完成用户确认收货无4已取消用户取消或超时未付无后端在接收状态变更请求时必须校验当前状态是否允许跳转到目标状态不能直接把用户传来的 status 写进数据库。比如已发货订单不允许用户直接改成已完成这里需要按规则判断再由后端统一更新。7. Vue3 管理后台实现与代码改造要点管理后台是运营和商家操作的入口。用 Vue3 Vite Element Plus 构建时项目的目录结构通常长这样src ├── api │ ├── request.js │ └── product.js ├── router │ └── index.js ├── store │ └── user.js ├── views │ ├── dashboard │ ├── product │ │ ├── index.vue │ │ └── edit.vue │ └── order │ └── index.vue └── layout └── index.vuerequest.js负责创建 Axios 实例、设置基础 URL、注入 token、拦截统一错误码。管理后台的 token 通常在用户登录后保存在 Pinia 和本地存储中每次请求在请求拦截器里放到请求头Authorization。后端过滤器或拦截器每次从请求头解析 token判断用户是否合法。下面是一段常见的request.js封装// src/api/request.js import axios from axios import { ElMessage } from element-plus import { useUserStore } from /store/user import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization userStore.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.data }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) router.push(/login) } else { ElMessage.error(网络异常) } return Promise.reject(error) } ) export default request商品管理的 API 模块可以这样拆// src/api/product.js import request from /api/request export function getProductPage(params) { return request({ url: /product/list, method: get, params }) } export function addProduct(data) { return request({ url: /product/add, method: post, data }) } export function updateProduct(data) { return request({ url: /product/update, method: put, data }) } export function deleteProduct(id) { return request({ url: /product/delete/${id}, method: delete }) }商品管理页面是后台最常用的界面。页面结构通常分成三块顶部的搜索表单、中间的表格、底部的分页组件。Element Plus 的el-table直接绑定数据操作列放“编辑”“上下架”“删除”按钮。下面给一个简化版模板!-- src/views/product/index.vue -- template div el-card el-form inline el-form-item label商品名称 el-input v-modelqueryParams.keyword placeholder请输入商品名称 clearable / /el-form-item el-form-item el-button typeprimary clickloadData查询/el-button el-button clickhandleAdd新增商品/el-button /el-form-item /el-form /el-card el-card el-table :datatableData border stripe el-table-column propid labelID width80 / el-table-column propname label商品名称 / el-table-column propprice label价格 / el-table-column propstock label库存 / el-table-column label状态 template #default{ row } el-tag :typerow.status 1 ? success : info {{ row.status 1 ? 上架 : 下架 }} /el-tag /template /el-table-column el-table-column label操作 width220 template #default{ row } el-button sizesmall clickhandleEdit(row)编辑/el-button el-button sizesmall typeprimary clickhandleToggle(row) {{ row.status 1 ? 下架 : 上架 }} /el-button /template /el-table-column /el-table el-pagination v-model:current-pagequeryParams.pageNum v-model:page-sizequeryParams.pageSize :totaltotal layouttotal, prev, pager, next current-changeloadData / /el-card /div /template script setup import { onMounted, reactive, ref } from vue import { getProductPage } from /api/product const tableData ref([]) const total ref(0) const queryParams reactive({ pageNum: 1, pageSize: 10, keyword: }) async function loadData() { const data await getProductPage(queryParams) tableData.value data.records total.value data.total } function handleAdd() {} function handleEdit(row) {} async function handleToggle(row) {} onMounted(loadData) /script这个页面已经把搜索、分页和列表数据绑定的基本逻辑展示出来了。和 Vue2 写法的区别在于这里用script setup组合式 API变量和函数可以直接在模板中使用不再需要return一堆方法。项目规模变大以后这种组织方式对代码复用和按功能拆分更友好。管理后台还需要在路由里配置登录守卫。简单理解就是用户没有登录时无论手动输入哪个后台地址都要被强制重定向到登录页。这个工作由router.beforeEach完成它是后台系统安全的第一个入口。8. 微信小程序端实现与登录安全小程序端重点解决两件事请求封装和登录状态。原生微信小程序请求无法直接用 Axios通常使用wx.request。可以把它封装成 Promise这样页面里写起来更接近前端习惯。下面是一个基础请求工具// utils/request.js const BASE_URL https://你的域名.com/api function request({ url, method GET, data {}, header {} }) { return new Promise((resolve, reject) { const token wx.getStorageSync(token) if (token) { header.Authorization token } wx.request({ url: BASE_URL url, method, data, header, success(res) { if (res.statusCode 200) { const body res.data if (body.code 200) { resolve(body.data) } else { wx.showToast({ title: body.message || 请求失败, icon: none }) reject(body) } } else if (res.statusCode 401) { wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/index }) reject(res) } else { wx.showToast({ title: 网络异常, icon: none }) reject(res) } }, fail(err) { reject(err) } }) }) } module.exports { get: (url, data) request({ url, data }), post: (url, data) request({ url, method: POST, data }) }登录流程要重点理解。小程序登录不能让后端存储微信密码也不可能直接拿到用户手机号标准流程是小程序端调用wx.login()拿到一个临时 code有效期较短。小程序端把 code 发给自己的后端。后端拿着 code 请求微信接口换取 openid 和 session_key。后端把 openid 作为用户唯一标识存入数据库。后端生成自定义 token 返回给小程序端后续请求都带上这个 token。为什么不能把 code 交给别人处理因为 code 是一次性凭证换取成功后就失效。真正敏感的是 openid 和 session_key它们不应该被返回给小程序端存储。项目里如果看到有接口把 openid 直接返回给前端要注意这个设计存在安全隐患攻击者可以用别人的 openid 伪造用户身份。首页商品列表是一个典型的小程序页面。用户进入首页后请求后端商品列表接口把返回数据渲染成卡片列表// pages/index/index.js const { get } require(../../utils/request) Page({ data: { productList: [], pageNum: 1, pageSize: 10, loading: false }, onLoad() { this.fetchProductList() }, onPullDownRefresh() { this.setData({ productList: [] }) this.fetchProductList().finally(() wx.stopPullDownRefresh()) }, fetchProductList() { this.setData({ loading: true }) return get(/product/list, { pageNum: this.data.pageNum, pageSize: this.data.pageSize }).then(res { this.setData({ productList: this.data.productList.concat(res.records) }) }).catch(() { wx.showToast({ title: 加载失败, icon: none }) }).finally(() { this.setData({ loading: false }) }) }, goDetail(e) { const id e.currentTarget.dataset.id wx.navigateTo({ url: /pages/product/detail?id${id} }) } })这个页面的逻辑并不复杂更有价值的是理解“用户触发的分页加载”和“下拉刷新”怎么配合。很多初学小程序的人会忽略onPullDownRefresh拉取最新列表前要先清空原有数组导致刷新后列表重复。只有把这一类细节处理好项目演示时才不会露怯。商品详情页还需要调用收藏、加入购物车、立即购买等功能。用户每次点击加入购物车之前最好检查本地有没有 token没有登录时引导用户登录可以使用wx.getUserProfile和wx.login配合完成新用户自动注册。页面跳转时商品 ID 可以通过wx.navigateTo的 URL 参数传递。一个小坑小程序页面 URL 参数是字符串跳到详情页后如果直接拿这个字符串去数据库比较部分后端框架类型转换能处理但如果做严格校验可能报错。建议在onLoad(options)中把options.id转成数字再使用避免出现“类型对不上”的接口 500 问题。9. 项目运行、验证方法与常见问题排查拿到一个完整项目后建议按下面顺序启动不要一上来就双击小程序开发者工具。第一步初始化数据库。执行项目里的fitness_mall.sql确认 MySQL 中出现所有业务表。如果 SQL 执行报错多半是字符集或外键顺序问题可以把SET NAMES utf8mb4放在文件开头。第二步配置后端。修改application.yml里的数据库账号密码。如果项目里配置了 Redis先启动本地 Redis没有安装 Redis 的可以直接先把依赖注释掉让项目能跑通再说。第三步启动 Spring Boot 工程。看到Started FitnessApplication日志再用浏览器访问http://localhost:8080/api/product/list确认返回 JSON。第四步启动 Vue3 管理后台。在项目根目录执行npm install npm run dev如果管理后台需要登录先用项目自带的默认管理员账号登录。需要确认前端接口代理是否配好。Vite 项目通常会在vite.config.js里配置 proxy把/api代理到http://localhost:8080这样开发时不会因为跨域报错。第五步用微信开发者工具导入小程序端目录。本地开发时在“详情 - 本地设置”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”否则wx.request请求http://localhost会被拦截。但要注意这个选项只是开发辅助上线前必须配置合法 HTTPS 域名。启动后可以按以下路径冒烟测试管理后台新增分类“跑步机”新增商品“商用跑步机”设置价格和库存。小程序首页刷新确认能看到新商品。小程序未登录加购验证是否弹出登录。登录后加入购物车、提交订单。后端数据库中确认订单状态为待付款。如果已配置支付执行支付回调订单状态变待发货。后台执行发货小程序端看到待收货。小程序确认收货订单状态变为已完成。常见问题集中在几个点上。请求跨域报错先检查后端是否配置了 CORS或者前端是否配置了代理。小程序白屏且控制台报request:fail大概率是开发者工具校验问题或者 BASE_URL 配置不对。启动时提示数据库连接失败优先检查账号密码和 MySQL 服务是否开启。后端启动成功后接口直接 401不要慌先看请求头里有没有正确携带 Authorization。登录后 session 失效可能是 token 过期时间太短也可能小程序端没有正确把 token 存入 storage。10. 生产环境安全与工程化建议项目跑通以后如果你想把它提升到可演示、可部署的水平下面这几个点值得投入。第一统一参数校验。前端传进来的参数必须在后端校验不能依赖前端的必填提示。Spring Boot 可以使用Validated 注解校验比如NotBlank、NotNull、DecimalMin。购物车数量不能为负数订单金额不能为负数地址 ID 必须是当前用户的地址。这些校验都是后端应该做、而且必须做的事情。第二小程序端身份认证要遵循最小权限原则。后端每个接口在查询数据时不仅要判断用户是否登录还要判断这个数据是否属于当前用户。例如用户查询订单列表时SQL 里必须带user_id条件不能简单按订单号查询否则会出现水平越权漏洞。水平越权指的是攻击者通过遍历 ID 看到其他用户的订单这在电商项目里是严重问题。第三微信支付回调要做验签和幂等处理。线上支付回调可能因为网络原因被微信服务器重试多次后端需要保证同一个支付通知只处理一次。处理逻辑通常是收到回调后先验证签名再查订单当前状态如果已经支付则直接返回成功不再重复修改状态。第四可以引入 Redis 缓存热点商品。首页和商品详情是访问量最高的接口把商品列表或商品详情缓存到 Redis能明显减少数据库压力。但要注意缓存和数据库的一致性问题后台修改商品后需要主动删除缓存或更新缓存。对于健身器材这类低频变更数据读多写少缓存效果会比较明显。第五图片上传不能只存在项目本地。如果后台把图片传进 Spring Boot 项目的static目录重启后文件可能丢失也不方便小程序端访问。小型项目可以把图片上传到对象存储或图床就算只是本地部署演示也建议把图片上传路径配置成独立目录通过 Nginx 或静态资源映射访问。第六订单支付时间不能长期占着库存。如果用户提交订单后一直不付款库存会被无效订单占用。常见的解决方式是定期关单订单创建后设置过期时间比如 30 分钟未支付自动取消并释放库存。可以使用定时任务扫描待付款订单也可以借助延迟队列。对课程设计而言定时任务已经足够说明你的工程意识。第七日志要记录关键节点。订单创建、支付回调、库存扣减、后台发货这些操作都应该打日志。日志中至少要包含订单号、用户 ID、操作时间和操作结果。否则线上订单出问题时你会完全没有排查线索。11. 结语把这条主流程跑通项目就成功了一半健身器材交易小程序的骨架并不复杂难点在于把整个交易闭环打通。我建议你在本地动手时先不要急着加秒杀、优惠券、消息推送这些花哨功能而是先把“商品 - 购物车 - 下单 - 支付回调 - 后台发货 - 确认收货”这条链路完整跑一遍。只要这条链路通了项目的基础能力就立住了后面的功能都只是在这个主流程上做加法。真正的学习收获并不是记住某个标签而是理解为什么订单要设计状态机、为什么扣库存要用条件更新、为什么小程序登录不能把 openid 直接暴露、为什么后端列表不能直接返回大字段。这些经验会直接迁移到未来的工作中。如果这篇文章对你理解这个项目有帮助建议先收藏然后打开源码对照数据库表手动跑一遍主流程。遇到具体报错欢迎在评论区把日志和现象发出来一起排查。