ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot+Vue+微信小程序的多店铺商城系统DTS-SHOP实战解析

基于SpringBoot+Vue+微信小程序的多店铺商城系统DTS-SHOP实战解析

简介:聚惠星商城 DTS-SHOP 是一套基于微信小程序、Spring Boot 与 Vue 构建的 Java 商城系统,适合需要快速搭建或二次开发电商平台的 Java 工程师、毕业设计学生及多店铺运营团队,也可作为高校相关课程的实战案例。项目已完成功能闭环并达到商用标准,支持单店铺与多店铺入驻,包含用户端微信小程序与后台管理端,核心业务链路完整,代码分层明确,可直接作为项目蓝本或商用底座。资源以 zip 压缩包形式整理,包大小约 3.16MB,核心内容为项目源码与相关工程配置,可导入开发环境查看整体结构。目前已有 833 人学习下载。借助该资源可深入理解前后端分离商城项目的整体架构,梳理小程序端、Vue 管理后台与 Spring Boot 后端之间的协作方式,也可将项目导入开发环境进一步扩展,对商城系统的学习、毕设或商用落地均有较高参考价值。项目自身为开源体系,官方说明允许用于毕业设计与商家商用,使用限制较少。

1. 先看清 DTS-SHOP 是什么:一个能直接上线的多店铺商城底座

接到一个连锁零售商城项目,第一反应是排期要三个月。但如果你手里有一套基于微信小程序 + SpringBoot + Vue 构建、已经功能闭环且达到商用标准的多店铺商城源码,情况就完全不一样——先花一个下午把它跑通,再谈改造。DTS-SHOP(聚惠星商城)就是这样的项目:后端是 Java 语言的 SpringBoot 服务,C 端是微信小程序,运营端是 Vue 管理后台,支持单店铺和多家店铺入驻两种模式。这套代码最值钱的地方不是某个页面花哨,而是「闭环」两个字——从开店、上商品、下单、支付走到对账,一条链路是完整的。适合拿来当电商毕设底座、外包起步工程,或者公司内部快速搭商城演示系统。

2. 技术选型与工程结构:三端协作的边界在哪里

2.1 技术栈选型:为什么是 Java + 小程序 + Vue 的组合

先聊选型。商城这类项目,C 端最需要的是微信生态的支付能力,小程序内支付体验最顺,登录也省掉了一套账号体系,直接用微信授权。后端选 SpringBoot 是 Java 生态里做电商最稳的选择,微信支付 SDK、对账、财务相关的中间件都很成熟,团队招人也容易。管理后台用 Vue,是因为这类系统 90% 的页面是表格、表单、弹窗这些 CRUD,Vue 的组件生态能把这些活压到很低的成本。

我拆的版本管理后台用的是 Vue 3 + Element Plus,接口请求走 axios,路由用 vue-router。如果你拿到的是 Vue 2 版本,改造思路完全一样,无非是把组件写法从 options API 换成 composition API。后端持久层常见做法是 MyBatis 加 MySQL,Redis 干缓存和登录态这类的活。整套选型不追新,但每一环都有成熟的替代方案,出问题找得到人、搜得到案例,这正是商用项目最看重的。

2.2 工程目录结构:三端如何组织

把源码解压后,顶层目录分三块,结构非常清晰:

dts-shop ├── dts-shop-backend # SpringBoot 后端服务 │ ├── src/main/java │ │ └── com/dts/shop │ │ ├── controller # 接口层,接收前端请求 │ │ ├── service # 业务层,核心逻辑都在这里 │ │ ├── mapper # MyBatis 数据访问层 │ │ └── entity # 数据库实体对象 │ └── src/main/resources │ ├── mapper # MyBatis XML 映射文件 │ └── application.yml # 环境配置 ├── dts-shop-admin # Vue 管理后台 │ ├── src/views # 页面组件 │ ├── src/api # 接口封装 │ └── package.json └── dts-shop-miniapp # 微信小程序端 ├── pages # 小程序页面 ├── utils/request.js # 请求封装 └── app.js

后端是标准的 Controller → Service → Mapper 三层,一眼能看到数据是怎么流下去的。管理后台按页面维度组织,商品、订单、店铺、会员各一个目录。小程序端按 tabBar 页面拆,首页、分类、购物车、我的这几个主入口在pages下平铺。三端共用同一套 REST API,接口返回统一包装成{ code, msg, data }结构,前端拿到 code 为 0 才取 data,这套约定在三个工程里是一致的。

2.3 多店铺的数据模型:单库多表加 shop_id 隔离

多店铺这个需求,最直接的设计是「单库多表 + shop_id 字段隔离」,中小规模的商城这么做完全够用,不用每个店铺开一个数据库实例。店铺本身是一张独立的表:

@Entity @Table(name = "dts_shop") public class Shop { /** 店铺ID,主键自增 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** 店铺编号,业务上唯一,订单号会用到 */ @Column(name = "shop_no", unique = true) private String shopNo; /** 店铺名称 */ @Column(name = "shop_name") private String shopName; /** 状态:0 待审核,1 正常,2 冻结 */ @Column(name = "status") private Integer status; // 省略 getter / setter }

核心约束是:商品表、订单表、购物车表、售后表,每张业务表都必须带shop_id字段。查询时,平台管理员不带 shop_id 查全部,店铺管理员只查自己店铺的数据。常见的做法是在 MyBatis 层写一个拦截器,根据当前登录用户的角色自动拼接shop_id条件,这样业务代码里不用反复手写「where shop_id = ?」,既省事又不容易漏条件导致越权。

3. 核心模块拆解:一条请求在商城里的完整旅程

3.1 登录链路:code 换 openid,再签发 Token

小程序端没有任何传统意义上的账号密码登录,用户打开小程序后,前端调用wx.login拿到一个临时 code,把这个 code 发给后端,后端拿它去微信接口换 openid,再签发 token。整个链路的第一步是登录接口:

@RestController @RequestMapping("/api/wx/auth") public class WxAuthController { @Resource private WxAuthService wxAuthService; /** 小程序登录入口,接收前端传过来的 code */ @PostMapping("/login") public Result<LoginVO> login(@RequestBody LoginDTO dto) { // 1. 拿 code 去微信接口换 openid String openid = wxAuthService.code2Openid(dto.getCode()); // 2. 用 openid 签发登录态 token String token = wxAuthService.createToken(openid); return Result.ok(new LoginVO(token, openid)); } }

登录成功的标志是拿到 token,后面所有需要身份的接口都在 Header 里带Authorization: token。这里有个细节:code 是一次性的,有效期只有五分钟,换完 openid 就作废,所以「刷新登录」需要重新走一遍wx.login,不能缓存 code 重复调用。token 这边常用 JWT 风格,把 openid 和 userId 塞进 payload,服务端不存 session,Redis 里只存黑名单,登录态校验是无状态的,集群部署时不用额外做会话同步。

3.2 商品模块:SPU/SKU 的存储与查询设计

商城的商品模型必须拆 SPU 和 SKU 两层。SPU 是商品主体,描述「这是什么」;SKU 是具体规格,描述「哪个颜色哪个尺码、卖多少钱、还剩多少库存」。添加商品这个动作在事务里同时落主表和规格表:

@Service public class GoodsServiceImpl implements GoodsService { @Resource private GoodsMapper goodsMapper; @Resource private SkuMapper skuMapper; /** 添加商品,必须在事务里同时保存 SPU 和全部 SKU */ @Transactional(rollbackFor = Exception.class) public void addGoods(GoodsDTO dto, Long shopId) { Goods goods = new Goods(); BeanUtils.copyProperties(dto, goods); // 关键点:商品归属当前操作的店铺 goods.setShopId(shopId); goodsMapper.insert(goods); for (SaveSkuDTO sku : dto.getSkus()) { Sku entity = new Sku(); entity.setGoodsId(goods.getId()); entity.setSpec(sku.getSpec()); // 如 "黑色;M码" entity.setPrice(sku.getPrice()); // 单位是分,Integer 类型 entity.setStock(sku.getStock()); // 库存数量 skuMapper.insert(entity); } } }

两个值得注意的参数约定。第一,价格在数据库里统一用「分」存整数,不用小数,这是电商场景里的血泪经验——用 float 存价格,金额计算迟早出精度问题,对账的时候翻车到怀疑人生。第二,库存字段是普通的 int,秒杀这类场景需要加乐观锁或 Redis 预扣库存,这套基础代码里先保证普通下单不错,高并发方案留到改造阶段再说。商品查询的时候 SPU 只查一次,SKU 按 goods_id 批量查出来组装好再返回,避免循环查询的 N+1 问题。

3.3 订单状态机:从待支付到完成的流转校验

订单模块最容易出 bug 的地方不是写 SQL,而是状态流转不受控。用户取消了订单,结果状态从「已发货」跳到了「已关闭」,这在业务上是灾难。DTS-SHOP 的做法是把状态定义成枚举,用一张流转表约束哪些状态能跳到哪些状态:

public enum OrderStatus { WAIT_PAY(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CLOSED(4, "已关闭"), REFUNDING(5, "退款中"); private final int code; private final String desc; /** 状态流转表:当前状态 -> 允许跳转到的状态集合 */ private static final Map<Integer, Set<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(0, Set.of(1, 4)); // 待支付可以支付或关闭 TRANSITIONS.put(1, Set.of(2, 5)); // 已支付可以发货或申请退款 TRANSITIONS.put(2, Set.of(3, 5)); // 已发货可以完成或退款 } /** 判断 from 状态能否合法流转到 to 状态 */ public static boolean canTransit(int from, int to) { return TRANSITIONS.getOrDefault(from, Set.of()).contains(to); } }

每次执行更新订单状态的 SQL 之前,先调一遍canTransit,非法流转直接抛业务异常回滚。比如「待支付」的订单不可能直接变「已完成」,因为中间少了两步。这个状态机定义清楚之后,订单的所有判断逻辑都变得非常简单,后续写售后、写对账都基于这张表做校验,不用东一个 if 西一个 else 地散落判断。

3.4 管理后台:商品上架与销售看板

管理后台的页面长什么样,直接决定运营愿不愿意用。DTS-SHOP 管理端是 Vue 3 + Element Plus,商品列表页是一个典型的表格页:

<template> <el-table :data="list" v-loading="loading" @row-click="openDetail"> <el-table-column prop="name" label="商品名称" min-width="160" /> <el-table-column prop="price" label="售价(分)" width="100" /> <el-table-column prop="stock" label="库存" width="90" /> <el-table-column label="操作" width="140"> <template #default="{ row }"> <el-button type="primary" link @click.stop="onEdit(row)">编辑</el-button> </template> </el-table-column> </el-table> </template> <script setup> import { ref, onMounted } from 'vue' import { fetchGoodsList } from '@/api/goods' const loading = ref(false) const list = ref([]) onMounted(async () => { loading.value = true try { // currentShopId 来自登录用户的店铺上下文 // 平台管理员传空,店铺管理员传自己的 shopId const res = await fetchGoodsList({ shopId: currentShopId, page: 1 }) list.value = res.data } finally { loading.value = false } }) </script>

这里管理端和 C 端小程序共用同一套商品 API,区别只在请求参数:平台管理员不传 shopId,看到的是全平台商品;店铺管理员传 shopId,只能看到自己店铺。销售看板部分用 ECharts 拉取近七天的订单金额和商品销量,数据源是订单表和订单明细表的聚合查询。整套后台的交互密度控制在「能用就行」的标准,没有太多花哨动效,但每个核心动作都有反馈。

4. 项目运行手册:从空环境到下单成功的落地步骤

4.1 环境版本清单

跑这套项目,环境版本卡得不算死,但下面的组合是我验证过最顺的:

组件版本建议用途
MySQL8.0+业务数据主存储
Redis6.x+缓存、登录态、验证码
JDK17后端编译运行环境
Maven3.8+后端依赖管理
Node.js16+管理后台构建运行
微信开发者工具稳定版小程序端调试

JDK 17 对应 SpringBoot 3 的常见搭配,如果你本地是 JDK 8,优先检查项目里pom.xml的 spring-boot 版本,2.x 用 JDK 8 没问题,3.x 必须上 17。这个顺序别搞反,我第一次跑类似项目时没看版本,后端直接起不来,报的是 UnsupportedClassVersionError。

4.2 数据库初始化与后端启动

项目里有 SQL 初始化脚本,通常放在dts-shop-backend/docs/sql目录下。先建库再导入:

mysql -uroot -p -e "CREATE DATABASE dts_shop DEFAULT CHARACTER SET utf8mb4;" mysql -uroot -p dts_shop < dts_shop.sql

注意用 utf8mb4 而不是 utf8,否则商品名称里带 Emoji 字符会存不进去。导入完成后看一遍表清单,核心是dts_shop、dts_goods、dts_sku、dts_order、dts_order_item这几张,心里有个数。

后端配置在application.yml,本地跑只需要改三处:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/dts_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 # 改成你自己的密码 redis: host: localhost port: 6379 # password: # 如果 Redis 设置了密码,填在这里 wx: appid: 你的小程序appid secret: 你的小程序secret

连接串末尾的serverTimezone=Asia/Shanghai必须保留,去掉之后订单时间会差八个钟头。Redis 密码留空说明本地没设密码,生产环境必须配置。启动命令在dts-shop-backend目录下执行:

mvn spring-boot:run

看到Started Application日志出来就算成功。如果 Redis 没启动,登录接口会报连接失败,所以本地最好先把redis-server起好再启动后端。

4.3 管理后台启动

管理后台是标准的 Vue 工程。进入dts-shop-admin目录:

npm install npm run dev

依赖安装慢的话,可以把 registry 切到国内镜像。启动后默认端口一般是 9528,页面里用到的接口通过 vue.config.js 的 devServer 代理转发到后端 8080:

proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } }

这个 proxy 配置是关键,它解决了开发环境跨域的问题。如果后端换了端口,同步改这里。管理后台登录用的账号密码在项目文档里有说明,首次登录后先去「店铺管理」建一个测试店铺,后续商品和订单数据都挂在这个店铺下。

4.4 小程序端联调

用微信开发者工具导入dts-shop-miniapp目录。导入后第一件事是检查project.config.json里的 appid,可以填你自己的小程序 appid,也可以先用测试号。改完后打开utils/request.js,把 baseURL 指向本地后端:

const BASE_URL = 'http://localhost:8080/api' export function request(path, options = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, header: { Authorization: wx.getStorageSync('token') || '' }, ...options, success: (res) => resolve(res.data), fail: reject }) }) }

本地联调必须在微信开发者工具的「详情」设置里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」,否则所有请求都会被拦截。这一步是新手最容易卡住的地方,界面一片空白,控制台里全是url not in domain list的报错。

4.5 走一遍功能闭环

前后端都起来之后,按这条路径完整走一遍,才能确认项目真的通了:

  1. 管理后台登录,新建店铺并录入一个商品,价格填 1000 分,库存 10。
  2. 小程序端用微信授权登录,进首页能看到管理后台录入的商品。
  3. 加购物车、提交订单,状态变为待支付。
  4. 支付这块本地联调通常走模拟回调,把订单状态推到已支付。
  5. 回到管理后台,在订单列表看到这笔订单,点发货。
  6. 小程序端确认收货,订单状态变成已完成。

每走一步,去数据库里看一眼对应订单的status字段变化,能直接体会状态机是怎么工作的。这一步走完,项目的核心环就真正闭环了。

5. 部署与排查:商用环境下的踩坑记录与注意点

5.1 五个高频问题:现象、原因、解决

第一坑:管理后台登录接口报跨域。现象是登录按钮转圈,控制台红字Access-Control-Allow-Origin。原因是后端没配 CORS 或者前端没用代理。解决:开发环境走 vue.config.js 的 proxy,生产环境用 Nginx 反代/api到后端,不要依赖后端全开跨域。

第二坑:小程序请求报url not in domain list。现象是页面空白,请求全挂。原因是本地没勾选不校验域名的开关。解决:开发者工具详情里勾上「不校验合法域名」,真机预览则必须在小程序管理后台配置 HTTPS 的 request 合法域名,这个是上线的硬门槛。

第三坑:订单时间差了 8 小时。现象是后台看到的下单时间比实际晚。原因是 MySQL 连接串没设置时区。解决:JDBC URL 里加serverTimezone=Asia/Shanghai,MySQL 服务端的默认时区也建议改成东八区。

第四坑:登录接口报Could not connect to Redis。现象是验证码拿不到,登录失败。原因是本地 Redis 没启动,或者配置的端口密码不对。解决:先redis-server起服务,再核对 application.yml 里的端口和密码,本地最常见的死法是密码留空但 Redis 配了密码。

第五坑:微信支付回调验签失败。现象是支付成功但订单状态没变。原因是证书路径用了相对路径,回调 URL 没走 HTTPS。解决:证书配绝对路径,回调地址必须是公网 HTTPS 域名,且在小程序后台的支付目录里和商户平台都配好。

5.2 上生产前的配置核对清单

本地跑通不等于能上线。生产部署之前,最少过一遍这几项:第一,MySQL 和 Redis 都要设密码,且不可能是 123456 这种;第二,后端跑在 Nginx 后面,HTTPS 证书必须配好,微信小程序的 request 合法域名只认 HTTPS;第三,微信支付要用真实的商户号,本地模拟支付的回调接口上线时换成微信回调地址;第四,生产环境不要用mvn spring-boot:run起服务,打成 jar 包用nohup java -jar跑,配 systemd 更稳;第五,日志要按天切割,否则几个月下来磁盘被日志写满,后台接口全部变慢。

5.3 本地与生产的差异意识

还有一个容易被忽略的点:本地环境和生产环境的配置要彻底分开。application.yml里放公共配置,本地覆盖用application-dev.yml,生产用application-prod.yml,启动时通过spring.profiles.active指定。千万别把本地数据库密码推上 Git,这是个事故高发点。另外本地可以关闭支付真实回调,用一个 mock 接口直接改状态,生产必须走微信回调验签。这两套逻辑不同,代码里通常用配置项做开关,改造时把开关的默认值搞清楚就行。

6. 二次开发路径:从能跑到能改的最小改造演练

拿到一套能跑的商城源码,最大的价值在于改造。给你演示两个最小改造,体会一下这套代码的扩展手感。

第一个改造:给商品加一个「产地」字段。步骤拆三步,先执行 SQL:

ALTER TABLE dts_goods ADD COLUMN origin VARCHAR(64) DEFAULT '' COMMENT '产地';

然后给Goods实体加private String origin字段,最后在管理后台的商品表单里加一个输入框,提交时带上这个字段。商品详情接口返回的数据里自动就有 origin 了,因为序列化是反射做的,前端直接读就行。这个改造十分钟搞定。

第二个改造:换订单号生成策略。原项目如果用的是简单时间戳,并发下单容易重复,改成「店铺号 + 时间 + 四位随机数」:

public String generateOrderNo(Long shopId) { return String.format("%s%s%s", shopId, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")), RandomStringUtils.randomNumeric(4)); }

在创建订单的 Service 里调用这个方法,把生成结果填到订单号的字段。注意这里有个关联点:订单表如果给 order_no 建了唯一索引,重复生成会直接抛异常,正好兜底;如果没建索引,建议顺手补上。

验证改造是否成功,路径就一条:本地把后端和管理后台跑起来,走一遍「创建订单 → 查数据库订单表 → 看订单号格式 → 看新字段有没有值」。我拆过的项目里,DTS-SHOP 属于那种结构不花哨、但该有的环节一个不少的类型。从那以后,我每次拿到一套二手商城源码,都强制走一遍先看数据库脚本、再看配置文件、最后跑主流程的顺序,这个习惯帮我少浪费了很多时间,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进