
很多同学在准备电商类项目或毕业设计时都绕不开“商城小程序”这类题目。如果商品类目再聚焦到汽车用品难度会明显上升汽车用品规格多、品牌杂脚垫、雨刷、座套这类商品还和车型强相关买错了就会引发售后纠纷。本文围绕“基于 Spring Boot Vue3 微信小程序的汽车用品交易小程序”完整拆解从技术选型、数据库设计到后端接口、Vue3 管理后台、微信小程序端的落地过程。无论你是在做课程设计、毕设还是想在公司内部从零搭建一套商城系统这篇内容都值得收藏参照。1. 汽车用品电商小程序的业务背景1.1 汽车用品商城的特殊性汽车用品交易系统本质上仍是电商系统但与普通服装、数码类商城不同它有几个非常明显的业务特征。第一商品结构复杂。汽车用品不仅包括机油、玻璃水等消耗品还包括脚垫、座套、行车记录仪、车载支架等硬件商品。硬件商品往往需要区分品牌、型号、颜色、适配车型等多维度信息这就对商品表和 SKU库存量单位设计提出了更高要求。第二车型适配是关键。买脚垫或雨刮器时用户必须先确认自己的车型和年份系统才能给出可购买的商品。如果系统不做任何车型适配只靠用户自己猜退货率会居高不下。这也是汽车用品商城区别于普通商城的重要设计点。第三订单和售后的链路更复杂。汽车用品的安装服务、退换货、维修质保都会产生额外状态。所以在订单状态设计时除了常见的待付款、待发货、待收货、已完成之外最好预留退换货或售后状态。1.2 汽车用品交易系统需要包含哪些角色一套完整的汽车用品交易小程序并不只有“用户端小程序”这一个入口它通常包含三类使用对象。C 端买家通过微信小程序浏览商品、搜索商品、查看车型适配、加入购物车、下单支付、查看订单和售后记录。运营/商家通过 Vue3 后台管理系统维护商品、分类、库存、订单发货、处理用户退款等。系统管理员在后台管理用户状态、角色权限、运营数据等。因此项目从形式上通常被拆成三个工程Spring Boot 后端 API、Vue3 管理后台、微信小程序用户端。三端共用同一套后端接口既满足前后端分离的开发模式也方便后续扩展 App 或 H5。1.3 本项目适合哪些读者学习如果你属于下面几种情况之一这篇文章非常适合你正在准备 Java 后端相关的毕设或实训项目需要一套有业务深度的系统作为主线。已经会写 Spring Boot 的单表 CRUD想了解多表事务、登录鉴权、库存扣减这些真实电商问题的处理思路。想学习 Vue3 Vite Element Plus 如何配合后端完成管理后台开发。想了解原生微信小程序的登录流程、请求封装和商城页面开发方式。接下来我们直接进入技术方案与架构设计。2. 技术选型与整体架构设计2.1 前后端分离的三端架构本项目的整体访问链路非常清晰微信小程序用户端 ----\ --- Spring Boot API --- MySQL Vue3 运营管理后台 ----/ | Redis从职责上看后端只负责提供标准 JSON 格式的 RESTful API不关心页面长什么样。Vue3 管理后台通过 axios 调用后端接口完成商品、订单、用户管理。微信小程序用户端也通过 wx.request 调用同一套接口完成业务操作。这种结构的优点是后端接口可以被多个端复用后续要增加 H5 商城或 App只需要新增前端工程。小程序端和管理后台可以并行开发后端只需要维护一份接口文档。项目部署时可以分别部署静态文件和后端服务便于做流量分担。如果团队规模较小也可以把 Vue3 管理后台和小程序放在同一个仓库中后端单独放在另一个仓库。本文默认采用“后端独立工程 两个前端工程”的方式。2.2 标题中的 SpringBoot4 如何理解你可能会注意到项目标题里写的是 SpringBoot4。这里需要说明一下技术社区中这种写法通常代表项目是基于较新的 Spring Boot 大版本开发的。但由于 Spring Boot 的版本迭代比较快从 2.x 升级到 3.x 时代码层面最大的变化是javax.*包名全部替换成了jakarta.*同时最低要求 JDK 17。将来如果升级到 4.x大概率还会涉及自动配置类、依赖管理等细节调整。本文中的示例代码采用当前主流且稳定的 Spring Boot 3.x 写法。当你使用 IDEA 的 Spring Initializr 初始化项目时可以直接选择官方已经发布的稳定版本。如果后续需要迁移到 Spring Boot 4应该重点查看官方升级文档中关于依赖管理和自动配置的变化不要盲目把版本号改成 4.0否则很多第三方 starter 可能还没完成适配。2.3 核心后端工程结构后端工程建议按业务模块分包而不是把所有 Controller 写在一个包下。虽然项目初期包结构看不出差异但业务一旦增加混乱的包结构会严重拖慢开发速度。参考结构如下car-supply-api ├── pom.xml └── src/main/java/com/carsupply ├── CarSupplyApplication.java ├── common // 通用返回、异常、常量 ├── config // 跨域、MyBatis-Plus、拦截器配置 ├── controller // 接收前端请求 ├── dto // 入参和出参对象 ├── entity // 数据库实体 ├── mapper // MyBatis-Plus Mapper 接口 ├── service // 业务逻辑层 └── utils // 工具类例如 JwtUtil代码中所有返回给前端的数据建议统一包装成通用返回对象 Result不要直接返回 Map 或裸对象否则前端统一拦截异常和提示会非常困难。3. 环境准备与项目初始化3.1 开发环境和版本建议由于不同公司的项目模板不同版本不完全一致。本文使用的关键环境如下你在实际开发时应以自己的稳定环境为准JDK 17 及以上。Maven 3.8 或 Ggradle。Node.js 18用于 Vue3 管理后台。MySQL 8.0用于数据存储。Redis 5.0如果项目引入缓存和登录令牌管理。微信开发者工具最新的稳定版本即可。IDEA 或 VS Code按你的习惯选择。Spring Boot 3.x 与 JDK 8 不兼容这是一个容易踩坑的细节。如果电脑上安装的还是 JDK 8请先重新安装 JDK 17否则项目会直接启动失败。3.2 创建后端项目和引入依赖打开 IDEA使用 Spring Initializr 创建工程Group 填写 com.carsupplyArtifact 填写 car-supply-apiJava 版本选择 17。然后在 pom.xml 中引入 Web、MySQL、Lombok、MyBatis-Plus 等依赖。如果现有 pom.xml 还没有这些依赖可以参考下面的片段parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId !-- 改为你实际使用的 Spring Boot 3.x 稳定版本 -- version3.3.4/version relativePath/ /parent properties java.version17/java.version mybatis-plus.version3.5.7/mybatis-plus.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies需要特别说明的是MyBatis-Plus 针对 Spring Boot 2 和 Spring Boot 3 提供了不同的 starter 坐标。如果你使用的是 Spring Boot 3.x必须引入mybatis-plus-spring-boot3-starter而不是老的mybatis-plus-boot-starter。3.3 编写 application.yml 配置在src/main/resources目录下新建application.yml配置数据源和 MyBatis-Plus。server: port: 8080 spring: application: name: car-supply-api datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/car_supply?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的数据库密码 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有两个建议。第一map-underscore-to-camel-case设置为 true 后数据库字段create_time可以自动映射到 Java 实体中的createTime省去大量手动映射。第二MyBatis-Plus 的逻辑删除配置可以帮你在执行删除时自动变成UPDATE ... SET deleted 1对商城类系统比较实用。我们在表设计中会为关键表增加deleted字段因此这里提前配置逻辑删除。注意在实际生产环境中不要把数据库密码直接写在application.yml里建议通过环境变量或配置中心注入比如写成${DB_PASSWORD}。4. 数据库设计汽车用品商城的基础4.1 核心表的设计思路在设计数据库之前先想清楚商城核心流程。用户打开小程序浏览商品看到汽车用品分类选择适配自己车型的商品加入购物车提交订单并支付。运营人员则在后台维护商品上下架处理订单。围绕这条链路数据库至少需要下面这些表用户表car_user分类表car_category商品表car_product商品 SKU 表car_product_sku购物车表car_cart订单表car_order订单明细表car_order_item收货地址表car_address车型库表car_vehicle_model商品车型适配表car_product_vehicle商品表保存通用信息如商品名称、主图、价格区间、销量、上下架状态。SKU 表保存具体的规格信息例如同一款行车记录仪有“单镜头版”和“双镜头版”这两个 SKU 的库存和价格可能不同。汽车用品的价格波动较大数据库金额字段建议统一用“分”为单位存放避免浮点数计算误差。订单表除了记录订单号、用户 ID、总金额、状态外还要考虑用户可能修改收货地址所以订单明细中需要保存下单时的商品快照信息比如商品名称和图片避免商品后来改名或下架导致历史订单不可追溯。4.2 建表 SQL 示例为了控制篇幅这里给出最核心的几张建表语句你需要先在 MySQL 中创建数据库car_supply然后依次执行。CREATE DATABASE IF NOT EXISTS car_supply DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE car_supply; CREATE TABLE car_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, openid VARCHAR(64) NOT NULL COMMENT 微信 openid, nickname VARCHAR(50) DEFAULT NULL COMMENT 用户昵称, avatar_url VARCHAR(255) DEFAULT NULL COMMENT 用户头像, mobile VARCHAR(20) DEFAULT NULL COMMENT 手机号, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除0未删除1已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE car_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, parent_id BIGINT DEFAULT 0 COMMENT 父分类ID0表示一级分类, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort INT DEFAULT 0 COMMENT 排序值, icon_url VARCHAR(255) DEFAULT NULL COMMENT 分类图标, status TINYINT DEFAULT 1 COMMENT 状态1启用0禁用, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分类表; CREATE TABLE car_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, category_id BIGINT NOT NULL COMMENT 分类ID, name VARCHAR(100) NOT NULL COMMENT 商品名称, subtitle VARCHAR(255) DEFAULT NULL COMMENT 商品卖点/副标题, main_image VARCHAR(500) DEFAULT NULL COMMENT 主图URL, detail_html MEDIUMTEXT COMMENT 商品详情HTML, price_fen BIGINT NOT NULL DEFAULT 0 COMMENT 展示价格单位分, sales_count INT DEFAULT 0 COMMENT 销量, status TINYINT DEFAULT 1 COMMENT 状态1上架0下架, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT汽车用品商品表; CREATE TABLE car_product_sku ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT SKU ID, product_id BIGINT NOT NULL COMMENT 商品ID, specs VARCHAR(255) NOT NULL COMMENT 规格描述例如黑色/单镜头, stock INT NOT NULL DEFAULT 0 COMMENT 库存, price_fen BIGINT NOT NULL COMMENT 价格单位分, image_url VARCHAR(500) DEFAULT NULL COMMENT 规格图片, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, KEY idx_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品SKU表; CREATE TABLE car_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL COMMENT 用户ID, address_snapshot VARCHAR(500) NOT NULL COMMENT 收货地址快照, total_fee BIGINT NOT NULL COMMENT 订单总金额单位分, pay_fee BIGINT DEFAULT 0 COMMENT 实付金额单位分, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态0待付款1待发货2待收货3已完成4已关闭5退货中, pay_time DATETIME DEFAULT NULL COMMENT 支付时间, finish_time DATETIME DEFAULT NULL COMMENT 完成时间, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE car_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, order_id BIGINT NOT NULL COMMENT 订单ID, product_id BIGINT NOT NULL COMMENT 商品ID, sku_id BIGINT NOT NULL COMMENT SKU ID, product_name VARCHAR(100) NOT NULL COMMENT 商品名称快照, sku_specs VARCHAR(255) DEFAULT NULL COMMENT 规格快照, product_image VARCHAR(500) DEFAULT NULL COMMENT 商品图片快照, price_fen BIGINT NOT NULL COMMENT 成交单价单位分, quantity INT NOT NULL COMMENT 购买数量, KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;这几张表已经可以支撑最基础的商城流程。商品与车型的适配关系可以单独设计car_product_vehicle(product_id, vehicle_model_id)等基础版本跑通后再扩展。5. Spring Boot 后端核心业务实现5.1 通用返回结果处理在common包下新建ResultT类统一返回结构。// 文件路径src/main/java/com/carsupply/common/Result.java package com.carsupply.common; import lombok.Data; Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); 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; } }这样的好处是前端可以通过code 200判断请求是否成功不用每次在业务代码里处理异常堆栈。5.2 商品分页查询接口汽车用品商城首页和分类页都会频繁请求商品列表所以后端需要提供一个分页接口。参数包括当前页、每页数量、分类 ID 和商品名称关键字。先编写 Product 实体类。// 文件路径src/main/java/com/carsupply/entity/Product.java package com.carsupply.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data TableName(car_product) public class Product { TableId(type IdType.AUTO) private Long id; private Long categoryId; private String name; private String subtitle; private String mainImage; private Long priceFen; private Integer salesCount; private Integer status; TableLogic private Integer deleted; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }实体类中的deleted字段加上了TableLogic注解之后使用 MyBatis-Plus 内置方法时查询会自动追加deleted 0条件。接着编写 Mapper 接口。// 文件路径src/main/java/com/carsupply/mapper/ProductMapper.java package com.carsupply.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.carsupply.entity.Product; import org.apache.ibatis.annotations.Mapper; Mapper public interface ProductMapper extends BaseMapperProduct { }然后编写 Controller 提供分页接口。// 文件路径src/main/java/com/carsupply/controller/ProductController.java package com.carsupply.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.carsupply.common.Result; import com.carsupply.entity.Product; import com.carsupply.mapper.ProductMapper; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/product) public class ProductController { private final ProductMapper productMapper; public ProductController(ProductMapper productMapper) { this.productMapper productMapper; } GetMapping(/list) public ResultPageProduct list( RequestParam(defaultValue 1) long current, 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 (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword); } wrapper.orderByDesc(Product::getCreateTime); return Result.ok(productMapper.selectPage(new Page(current, size), wrapper)); } GetMapping(/{id}) public ResultProduct detail(PathVariable Long id) { Product product productMapper.selectById(id); return Result.ok(product); } }这里使用了构造器注入这是 Spring 官方推荐的注入方式比直接Autowired字段注入更容易测试。不过要注意MyBatis-Plus 的分页插件需要额外配置否则selectPage只能查出全部记录而不会真正分页。在config包下创建 MybatisPlusConfig。// 文件路径src/main/java/com/carsupply/config/MybatisPlusConfig.java package com.carsupply.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration MapperScan(com.carsupply.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }5.3 小程序登录接口小程序登录与普通账号密码登录不同。前端通过wx.login()获取临时 code后端拿到 code 后调用微信接口换取 openid 和 session_key。由于小程序端不需要传统密码所以后端只需要保存 openid 作为唯一标识然后返回一个自定义登录令牌。下面是一段简化版的登录接口代码。// 文件路径src/main/java/com/carsupply/controller/AuthController.java package com.carsupply.controller; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.carsupply.common.Result; import com.carsupply.entity.User; import com.carsupply.mapper.UserMapper; import com.carsupply.utils.JwtUtil; import org.springframework.web.bind.annotation.*; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; RestController RequestMapping(/api/auth) public class AuthController { private final UserMapper userMapper; public AuthController(UserMapper userMapper) { this.userMapper userMapper; } PostMapping(/wx-login) public ResultString wxLogin(RequestBody WxLoginDTO dto) throws Exception { String url https://api.weixin.qq.com/sns/jscode2session?appid dto.getAppid() secret dto.getSecret() js_code dto.getCode() grant_typeauthorization_code; HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); JSONObject json JSONUtil.parseObj(response.body()); String openid json.getStr(openid); if (openid null) { return Result.error(400, 微信登录失败 json.toString()); } User user userMapper.selectOne(new LambdaQueryWrapperUser().eq(User::getOpenid, openid)); if (user null) { user new User(); user.setOpenid(openid); userMapper.insert(user); } String token JwtUtil.generateToken(user.getId()); return Result.ok(token); } public static class WxLoginDTO { private String code; private String appid; private String secret; // getter/setter实际接入时应删除 appid/secret 入参避免泄露 } }这里把 appid 和 secret 放在 DTO 中是方便演示实际项目中 secret 必须存放在后端环境变量绝不能下发到小程序端。更合理的做法是前端只传 code后端从自己的配置中心读取 appid 和 secret。如果你不希望代码被 hutool 工具包影响可以移除 hutool 依赖改用 Fastjson2 或 Jackson 解析微信返回结果。5.4 订单创建时库存扣减创建订单是高并发场景下最容易出问题的地方。不能先查询库存再在 Java 代码里判断库存是否足够因为并发请求同时读到库存 10就可能出现超卖。推荐使用数据库原子更新扣减库存。在 Service 中实现下单逻辑时可以先创建订单记录然后执行带条件的更新 SQL。// 文件路径src/main/java/com/carsupply/service/OrderService.java package com.carsupply.service; import com.carsupply.entity.Order; import com.carsupply.entity.OrderItem; import com.carsupply.entity.ProductSku; import com.carsupply.mapper.OrderItemMapper; import com.carsupply.mapper.OrderMapper; import com.carsupply.mapper.ProductSkuMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; Service public class OrderService { private final OrderMapper orderMapper; private final OrderItemMapper orderItemMapper; private final ProductSkuMapper productSkuMapper; public OrderService(OrderMapper orderMapper, OrderItemMapper orderItemMapper, ProductSkuMapper productSkuMapper) { this.orderMapper orderMapper; this.orderItemMapper orderItemMapper; this.productSkuMapper productSkuMapper; } Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, Long addressId, ListOrderItemParam items) { // 1. 生成订单号保存订单主表状态为待付款 Order order new Order(); order.setUserId(userId); order.setStatus(0); // 生成订单号逻辑省略建议使用时间戳 随机数 用户ID order.setOrderNo(buildOrderNo()); orderMapper.insert(order); // 2. 循环明细并扣减库存 long totalFee 0; for (OrderItemParam item : items) { int result productSkuMapper.deductStock(item.getSkuId(), item.getQuantity()); if (result 0) { throw new RuntimeException(库存不足或商品已下架); } ProductSku sku productSkuMapper.selectById(item.getSkuId()); OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(sku.getProductId()); orderItem.setSkuId(sku.getId()); orderItem.setPriceFen(sku.getPriceFen()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); totalFee sku.getPriceFen() * item.getQuantity(); } order.setTotalFee(totalFee); orderMapper.updateById(order); return order.getId(); } }对应 ProductSkuMapper 中需要执行如下 SQL。// 文件路径src/main/java/com/carsupply/mapper/ProductSkuMapper.java public interface ProductSkuMapper extends BaseMapperProductSku { Update(UPDATE car_product_sku SET stock stock - #{quantity} WHERE id #{skuId} AND stock #{quantity} AND deleted 0) int deductStock(Param(skuId) Long skuId, Param(quantity) Integer quantity); }当更新影响行数为 0 时说明库存不足。通过这种方式数据库在更新行时会自动加行锁保证并发场景下不会超卖。6. Vue3 运营管理后台6.1 使用 Vite 创建 Vue3 项目管理后台采用 Vue3 Vite Element Plus。先打开命令行执行下面的命令创建项目npm create vitelatest car-admin -- --template vue cd car-admin npm install npm install axios element-plus安装完成后在src/main.js中引入 Element Plusimport { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue const app createApp(App) app.use(ElementPlus) app.mount(#app)6.2 封装 axios 请求后台管理中的商品、订单操作都会调用后端接口建议对 axios 做统一封装。// 文件路径src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) 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) { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request为了让接口跨域友好可以在 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 } } } })这样在开发环境的浏览器地址栏中访问http://localhost:5173/api/product/list实际会由 Vite 服务器转发到http://localhost:8080/api/product/list。6.3 商品管理列表页商品管理列表页的表格是后台系统的核心页面。创建src/views/product/ProductList.vue输入下面内容。template div el-card shadownever el-form :inlinetrue el-form-item label商品名称 el-input v-modelquery.keyword placeholder请输入商品名称 clearable / /el-form-item el-form-item el-button typeprimary clickfetchList查询/el-button el-button typesuccess clickopenEditor新增商品/el-button /el-form-item /el-form el-table :dataproductList border stripe v-loadingloading el-table-column propid labelID width70 / el-table-column label商品图片 width100 template #default{ row } el-image :srcrow.mainImage stylewidth: 60px; height: 60px fitcover / /template /el-table-column el-table-column propname label商品名称 min-width200 / el-table-column label价格 width120 template #default{ row } ¥{{ (row.priceFen / 100).toFixed(2) }} /template /el-table-column el-table-column propsalesCount label销量 width90 / el-table-column label状态 width90 template #default{ row } el-tag :typerow.status 1 ? success : info {{ row.status 1 ? 上架 : 下架 }} /el-tag /template /el-table-column el-table-column label操作 width180 template #default{ row } el-button link typeprimary clickopenEditor(row)编辑/el-button el-button link typedanger clickchangeStatus(row)上下架/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.current v-model:page-sizequery.size :totaltotal layouttotal, prev, pager, next current-changefetchList stylemargin-top: 15px; justify-content: flex-end / /el-card /div /template script setup import { onMounted, reactive, ref } from vue import request from ../../utils/request import { ElMessage } from element-plus const productList ref([]) const total ref(0) const loading ref(false) const query reactive({ current: 1, size: 10, keyword: }) const fetchList async () { loading.value true try { const res await request.get(/product/list, { params: query }) productList.value res.data.records total.value Number(res.data.total) } finally { loading.value false } } const openEditor (row) { // 实际项目中可以跳转到商品编辑页或打开抽屉表单 console.log(编辑商品, row) } const changeStatus async (row) { // 调用后端上下架接口省略实现 await request.put(/product/status/${row.id}) ElMessage.success(操作成功) fetchList() } onMounted(() { fetchList() }) /script页面完成后执行npm run dev打开浏览器即可看到后台商品列表。开发阶段如果报跨域错误优先检查 Vite 代理配置是否生效而不是直接在后端开启全局跨域。小程序端不走 Vite 代理因此小程序真机测试时需要使用完整域名或局域网 IP。7. 微信小程序用户端7.1 小程序项目的基础结构小程序端选择原生微信小程序开发没有引入额外框架。原因很简单原生小程序启动更快不受第三方框架版本升级影响。对汽车用品商城来说页面复杂度远低于后台管理系统原生开发足够。在微信开发者工具中新建一个小程序项目AppID 可以先使用测试号目录结构参考如下miniprogram ├── app.js // 全局逻辑 ├── app.json // 全局配置 ├── app.wxss // 全局样式 └── pages ├── index // 首页商品列表 ├── category // 分类页 ├── cart // 购物车 ├── order // 订单列表 └── user // 个人中心7.2 小程序中的登录态处理小程序登录与普通网页不同。小程序没有 Cookie 机制通常做法是通过wx.login获取 code把 code 提交给后端后端换取 openid 后返回业务自定义的 Token。小程序把 Token 存储到本地 Storage并在后续请求中通过 header 携带。在实际项目中不能把 AppSecret 放到任何前端代码中因为小程序代码可以被反编译或抓包。后端应使用环境变量保存 AppSecret。创建utils/request.js封装请求方法。// 文件路径pages/utils/request.js const request (url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token) wx.request({ url: getApp().globalData.baseUrl url, method, data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.data.code 200) { resolve(res.data.data) } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }) reject(res.data) } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) } module.exports { request }在app.js中定义全局变量App({ globalData: { baseUrl: http://localhost:8080 } })开发者工具调试时可以在本地服务器使用http://localhost:8080。真机预览时需要把 baseUrl 改成电脑局域网 IP例如http://192.168.1.100:8080同时必须保证手机和电脑处于同一个局域网并且后端服务监听地址不是仅本机回环。7.3 首页商品列表页面首页需要请求后端/api/product/list接口展示商品卡片。先看pages/index/index.js。const { request } require(../utils/request) Page({ data: { products: [], page: 1, hasMore: true }, onLoad() { this.fetchProducts() }, async fetchProducts() { if (!this.data.hasMore) return const data await request(/product/list?current${this.data.page}size10) const newList this.data.page 1 ? data.records : this.data.products.concat(data.records) this.setData({ products: newList, page: this.data.page 1, hasMore: data.records.length 10 }) }, goDetail(e) { const id e.currentTarget.dataset.id wx.navigateTo({ url: /pages/product/detail?productId${id} }) } })对应页面结构pages/index/index.wxmlview classproduct-list view classproduct-card wx:for{{products}} wx:keyid >UPDATE car_product_sku SET stock stock - #{quantity} WHERE id #{skuId} AND stock #{quantity}当数据库返回影响行数为 1说明扣减成功影响行数为 0说明库存不足。这个方法比使用 synchronized 锁更可靠因为 synchronized 只能保护单个 Java 实例进程无法解决多实例部署时的并发问题。9. 项目工程化与上线最佳实践9.1 金额与时间字段的处理金额是一个非常容易出错的点。数据库中的DECIMAL虽然能表示小数但在 Java 中转换为BigDecimal后序列化和计算都比较繁琐。如果项目要求不高最常见的电商做法是直接把金额单位定义为“分”使用BIGINT或INT存储。前端展示时除以 100计算时也全部基于整数。订单创建时间、支付时间建议统一使用LocalDateTime并在数据库中使用DATETIME类型。不要用VARCHAR存时间字符串否则后续做统计分析或按时间区间查询时无法有效利用索引。9.2 商品车型适配的思路汽车用品商城最容易被低估的功能就是车型适配。建议在商品表旁边增加车型库表和适配关系表示例表结构如下。CREATE TABLE car_vehicle_model ( id BIGINT PRIMARY KEY AUTO_INCREMENT, brand VARCHAR(50) NOT NULL COMMENT 品牌, series VARCHAR(50) NOT NULL COMMENT 车系, year_value VARCHAR(20) DEFAULT NULL COMMENT 年款, model_name VARCHAR(100)