今年被问得最多的一个项目,就是标题里这套“基于SpringBoot+Vue的三七原产地销售平台”。说实话,很多同学拿到压缩包的第一反应是:配环境、跑起来、截图、准备答辩。但等真把这套系统看明白,你会发现它并不是一个套了商城模板的demo,而是围绕“原产地”这个业务标签做了不少专门的建模和设计。
这篇我打算换一种讲法,不按“前端有什么页面、后端有什么接口”的清单来罗列,而是从业务需求、技术选型、数据库设计、后端链路、前端工程、部署上线这六条线完整拆一遍。无论你是拿它做毕设、做课程项目,还是想通过这个项目入门前后端分离开发,这篇都能帮你省下不少自己摸索的时间。
1. 产地直供平台的特殊之处:为什么不能直接套商城模板
1.1 农产品分级规格,比“颜色 + 尺码”复杂得多
普通电商教程里讲SKU,默认都是“红色、白色,S、M、L”这种服装思维。但三七这类药材电商,规格体系完全是另一套逻辑。
懂行的人知道,三七是按“头数”分级的,一斤多少个就是多少头,比如20头、30头、40头,头数越少价格越高。而且同一头数下还能继续按加工方式拆:带不带剪口、是整棵还是切片。这意味着商品规格不是简单的两个枚举值组合,而是一组有业务含义的维度。如果直接把“20头”“30头”写进商品标题,用一个商品对应一个SKU,后面一定会被SKU数量搞崩——同一款三七稍微扩展几个维度,SKU数量就会爆炸式膨胀。
这个项目在处理上采用的是标准SPU + SKU模型:一款三七作为一个SPU,管理基本信息;具体头数和加工方式作为SKU的规格属性,价格和库存挂在SKU上。这才是一个能支撑真实运营的规格设计。
1.2 溯源信息是平台信任的载体
普通商城详情页展示的是参数、图片、用户评价。但产地直供平台的核心卖点是“让用户相信这是源头货”。
这套系统在商品详情页专门做了溯源信息展示:产地批次号、种植年份、采收说明。这个设计在产品层面看是一个信任背书,落到数据库层面就是一张独立的批次表(batch),与商品表做关联。更关键的是,这个批次号不是只在商品详情页出现一次就完了——下单成功后,它会跟着订单明细一起保存下来。这样用户随时能在订单里回看自己买到的货是哪一批的,平台也具备售后服务追溯的基础。
很多套模板的项目在最容易被忽略的地方就是这里:商品表做得很全,订单表也做了,但商品和订单之间缺少这种业务上的双向勾稽关系。
1.3 管理端不是摆设,是一整套运营后台
另一个容易低估的是管理端。用户端页面谁都能做得好看,但管理端的分类维护、商品上架、库存调整、订单发货、数据统计,才是真正决定平台能不能持续运营下去的部分。
管理端设计要关注的几个点:
- 商品状态:上架、下架、售罄三个状态之间的流转,直接影响前端是否展示;
- 订单状态:从待支付到已支付、发货、完成,每一步对应的管理员操作边界要清晰;
- 数据统计:管理端首页一般需要订单量、销售额、用户数、热销商品等汇总数据,这些要用聚合SQL或者额外统计表来支撑。
把这三块想明白,再写代码其实就是往既定模型里填实现而已。想不明白的话,项目做到一半再返工,改表结构的成本是很高的。
2. SpringBoot + Vue这套组合的选型逻辑
2.1 单体架构在这个体量下就是最优解
现在微服务已经是简历上很常见的词了,但一个销售平台项目,用户量、业务复杂度都远没到需要分布式架构的程度。用SpringBoot做单体应用,主要原因有三个:
第一,SpringBoot本身约定了绝大部分配置,内置Tomcat,Maven管理依赖,一个jar包就能跑起来,开发调试的成本极低。第二,单体架构下事务边界清晰,一个下单接口从查库存到扣库存到生成订单,整个过程都在同一个事务里,不需要考虑分布式事务这种高难度话题。第三,部署运维简单,单人或者小团队可以完全掌控。
微服务不是不好,而是这个阶段把精力花在服务注册发现、配置中心、分布式事务上,就是典型的用大炮打蚊子,最后还要为这些基础设施的调试买单。
2.2 持久层与认证方案的取舍
持久层用MyBatis-Plus是这类项目里很常见的选型。它最大的价值就是单表CRUD完全不用写XML,内置分页插件,几十个管理端接口可以非常高效地写出来。复杂查询再手写SQL兜底,学习成本也比JPA低——至少写SQL的人心里始终清楚数据库在执行什么。
登录认证方面,这个项目的做法是JWT + 拦截器,没有引入Spring Security这种重量级框架。原因很实际:角色就两种(普通用户、管理员),接口权限只需要区分登录和未登录、管理员和非管理员,一套JWT工具类加一个拦截器就能实现。把token校验逻辑放进拦截器里,需要保护的路由注册进去就行。
JWT这套机制值得吃透:用户登录成功后,服务端签发一个包含用户ID和角色信息的token,后续每次请求在Header里带上,拦截器校验签名和有效期,再把用户信息放进请求上下文。整个过程无状态,对单体应用和后续可能的前后端分离场景都很友好。
2.3 前端为什么选Vue + Element
前端选Vue的原因太直白了:双向绑定和组件化让列表、表单、弹窗这类后台页面开发效率比手写jQuery高一个量级。配合Element UI(Vue3对应Element Plus),后台管理90%的界面需求都能直接用现成组件堆出来。
实际操作中,管理端的页面模式高度统一:el-table展示列表、搜索表单筛选数据、el-dialog嵌套el-form做新增编辑、分页组件切换页码。把这一套组合练熟之后,无论加多少管理页面,都是一个套路往里套。用户端虽然有更多视觉设计要求,但首页、列表、详情、购物车、结算这些流程,Vue Router + Vuex(或Pinia)也足够应付。
2.4 环境版本锁定,是这类项目最容易翻车的地方
这里给一个我自己验证过踩坑最少的组合参考:
| 组件 | 建议版本 | 原因 |
|---|---|---|
| JDK | 1.8 | SpringBoot 2.x最稳定的运行环境 |
| SpringBoot | 2.7.x | 资料最多,兼容性最好 |
| MyBatis-Plus | 3.5.x | 分页、字段填充等功能完善 |
| MySQL | 5.7 或 8.0 | 两个版本都很常用 |
| Node.js | 14+ | 配合Vue2和Vue3工程均没问题 |
| Vue | 2.6 + Element UI 2.15 | 经典组合,教程资源极其丰富 |
提示:拿到项目先看pom.xml里锁定的SpringBoot版本,再决定用哪个JDK。SpringBoot 3.0以上强制JDK17起步,很多老教程里的配置会直接失效,没必要在版本适配这件事上强行追新。
3. 数据库设计:十几张表怎么支撑“产地 + 销售”闭环
3.1 核心表清单
整个项目表数量大约在13到15张之间,我把核心表和核心字段整理一下:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, username, password, role, phone | 用户表,角色区分用户/管理员 |
| category | id, name, parent_id | 商品分类,支持二级 |
| product | id, category_id, name, main_image, detail, status | 商品SPU表,status控制上架/下架 |
| product_sku | id, product_id, spec_name, spec_value, price, stock | SKU表,头数+加工方式作为规格 |
| product_batch | id, product_id, batch_no, origin, year, description | 溯源批次表 |
| cart | id, user_id, sku_id, quantity | 购物车表 |
| address | id, user_id, name, phone, region, detail | 收货地址表 |
| orders | id, order_no, user_id, total, status, pay_time, ship_time | 订单主表 |
| order_item | id, order_id, sku_id, product_name, price, quantity, batch_no | 订单明细表,冗余商品快照 |
| banner | id, image, title, sort | 首页轮播 |
| notice | id, title, content, create_time | 公告 |
| collect | id, user_id, product_id | 收藏 |
角色只有两种的情况下,不需要独立的权限表、菜单表、角色表,user表的role字段就可以区分登录后的权限边界。引入过多表结构只会让项目看起来臃肿,对业务没有任何实际增益。
3.2 SPU/SKU和溯源信息的建模细节
三七这个业务场景给SPU/SKU模型提供了一个很好的落地案例。
举个例子,商品表里是一条“精选原产地三七”,SKU表里则是具体的可售规格:20头不带剪口、20头带剪口、30头切片等。每个SKU有自己的价格和库存。前端详情页要做的就是:展示SPU信息,然后让用户选择具体SKU,选择后把SKU的价格、图片、库存带入当前视图。
这里有一个很重要的设计习惯:订单明细表不是简单地存一个SKU外键,而是把商品名称、价格、规格、批次号等字段冗余存储在order_item里。原因在于,商品表和SKU表是可变的——商品可能改价、改名、甚至被删除,但历史订单必须保持当时购买的快照。不冗余,订单数据会随商品表变化而失真;冗余了,即使商品后来下架,用户和平台依然能查看到订单当时的确切状态。
product_batch表的批次号可以用“产地编号 + 年份 + 序号”的规则生成,例如P001-2024-001。这个编号看起来不复杂,但它在下单时复制到order_item里以后,就形成了从商品展示到用户购买再到售后的完整溯源链路。
3.3 订单状态机和金额字段
订单状态机是必须提前画清楚的,否则写接口时一定会出现逻辑漏洞。这个项目的核心状态是这样流转的:
0 待支付 → 1 已支付待发货 → 2 已发货 → 3 已完成
0 待支付 → 4 已取消
状态字段我建议用整型数字存,在代码里定义常量或枚举类。状态机明确之后,接口判断条件就非常清晰:
- 待支付状态下,用户可支付、可取消;
- 已支付状态下,管理员可发货;
- 已发货状态下,用户可确认收货;
- 已完成和已取消是终态,不再允许任何操作。
金额字段必须用decimal(10,2),不要用float或double。浮点类型在累加时会出现0.1+0.2这类精度问题,跟钱相关的字段没有任何理由冒这个险。另一个容易被忽略的点是:下单金额不要信任前端传上来的total数值,后端应该根据订单明细里的SKU单价和数量重新计算。这个原则在课程项目里可能看不出区别,但真实电商系统里这是基本功。
4. SpringBoot后端实现思路:三条主链路
4.1 登录认证与权限拦截
后端接口的入口是Controller,但真正决定“谁能访问”的是拦截器。这个项目的认证链路是:
- 用户提交账号密码,后端查询user表,用BCrypt算法校验密码;
- 校验通过后生成JWT,token里包含userId和role,返回给前端;
- 前端把token存到localStorage,后续请求在Header里带Authorization;
- 后端拦截器统一校验token,解析出来的用户信息放入请求上下文。
密码存储这一点多说几句:BCrypt和MD5有本质区别,同一个密码用BCrypt每次生成的hash都不同,但每次校验结果一致,因为hash里包含了随机盐。MD5加盐虽然也能做,但BCrypt是专门为密码存储设计的算法,综合安全性更好,这也是Spring Security生态里默认采用它的原因。
拦截器核心代码逻辑如下:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { // 未携带token,返回401 throw new BusinessException(401, "未登录或登录已过期"); } // 校验并解析token,获得userId和role LoginUser loginUser = JwtUtil.parseToken(token); // 把用户信息放入request,后续Controller直接取用 request.setAttribute("loginUser", loginUser); return true; } }管理端的接口以/api/admin/**开头,在拦截器里追加一层角色判断,不是管理员角色直接返回403。这种基于URL前缀的权限控制虽然简单,但在角色只有两种的项目里已经足够清晰。
4.2 商品发布与图片上传
管理端新增商品时,除了文本字段外,最常见的就是图片上传。这个项目的图片存储采用本地磁盘方案,没有接对象存储。本地方案下的关键配置是:上传路径必须在application.yml里配置成绝对路径,不要写相对路径。
如果上传后图片在页面加载不出来,90%的情况是上传路径和访问路径不一致。上传时用的是磁盘绝对路径/home/app/upload,访问时如果后端的静态资源映射没配到这个目录,或者Nginx没有把/upload前缀映射到对应目录,前端拿到图片URL就会404。
一个合理的最小实现是:Controller接收MultipartFile,保存到配置的upload.path目录,文件名用时间戳或UUID重命名防止重名,返回图片访问URL。需要访问图片时,要么在SpringBoot里配置WebMvcConfigurer的addResourceHandlers,要么在Nginx层做映射。
4.3 下单与库存扣减的并发问题
下单是电商系统的核心链路,最经典的问题是“超卖”——多个用户同时买最后一个库存,结果都下单成功了。
很多新手写的逻辑是:
- 查询SKU库存;
- 判断库存大于购买数;
- 执行库存更新。
这套“先查后改”的逻辑在单线程下没问题,但在并发环境下,多个请求同时读到库存为1,同时判定“大于0”,然后同时执行扣减,库存就会变成负数。
正确且简单的做法是使用SQL原子更新:
UPDATE product_sku SET stock = stock - #{num} WHERE id = #{skuId} AND stock >= #{num}这条SQL把“判断库存是否充足”和“扣减库存”合并成了一个原子操作,数据库行锁保证不会出现超卖。执行后判断受影响行数,等于1才继续创建订单,等于0就说明库存不足,直接返回“库存不足”提示。
对单体应用来说,这个方案足够可靠。如果非要追求极致的性能,可以上Redis预扣减加Lua脚本,但对教学级项目来说,理解“原子更新比先查后改可靠”的思想,比堆一堆中间件更有价值。
4.4 模拟支付与幂等处理
真实支付需要对接第三方网关,这类平台项目通常做的是模拟支付:用户在支付页面点击“确认支付”,后端直接把订单状态从待支付改成已支付,并记录支付时间。
这个接口最关键的一点是幂等处理。同一笔订单如果用户连续点击两次支付按钮,或者前端重试请求,后端不能扣两次款、生成两条流水。实现方式很简单,更新订单状态时带着状态条件来更新:
UPDATE orders SET status = 1, pay_time = NOW() WHERE id = #{orderId} AND status = 0受影响行数为0,说明订单已经支付过了,直接返回成功即可。这种带条件更新的思路,本质上和防超卖是同一个套路,理解了其中一个,另一个就非常容易举一反三。
5. Vue前端工程落地:路由、请求封装和页面骨架
5.1 工程目录与路由规划
前端工程结构直接决定多人协作和后续维护的体验,一个清晰的目录大概是这样的:
src/ api/ // 按模块拆分的请求接口定义 assets/ // 图片、样式等静态资源 components/ // 公共组件,如上传组件、分页组件 router/ // 路由配置 store/ // 用户信息、购物车状态等 views/ admin/ // 管理端页面 user/ // 用户端页面前端路由按前缀区分用户端和管理端:用户端是/下的首页、商品列表、详情、购物车、订单等;管理端统一挂在/admin下。路由懒加载是按需加载页面的基本方式,尤其像管理端这种页面数量较多的工程,懒加载能明显提升首次访问速度。
5.2 登录守卫与状态管理
路由守卫的核心逻辑写在router.beforeEach里:
- 访问需要登录的页面时,检查本地有没有token,没有就跳到登录页;
- 访问管理端路由时,额外检查当前用户的角色,不是管理员就跳回首页并提示无权限;
- 登录页和注册页在已登录状态下再次访问,直接跳回首页。
状态管理方面,最常用的是Vuex(Vue2)或Pinia(Vue3),主要存放用户信息和购物车状态。购物车用本地状态管理带来的体验差异很明显:加入购物车操作不用等接口返回,界面立即响应,刷新后再从后端同步数据。
5.3 axios请求封装的正确方式
前端最容易写乱的地方就是请求。如果一个页面里到处是axios.get(...),那维护成本会直线上升。标准做法是在api目录里统一封装:
// api/request.js import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动注入token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) // 响应拦截器:统一处理业务码和错误 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { // 统一错误提示 if (res.code === 401) { // token失效,清除本地并跳转登录页 } return Promise.reject(new Error(res.msg || '请求失败')) } return res }, error => { return Promise.reject(error) } ) export default service页面里不再直接接触axios,每个接口函数只负责拼接参数和接收数据。这样的好处是,之后哪怕后端改了接口前缀或者要统一加加密逻辑,也只需要改一个文件。
5.4 用户端核心页面流程
用户端的核心链路是:首页 → 商品列表 → 商品详情 → 加入购物车 → 购物车结算 → 提交订单 → 模拟支付 → 订单列表。
商品详情页需要特别处理的是SKU选择逻辑:页面从后端拿到商品下所有SKU列表,用户点击某个规格项后,通过规格项匹配出唯一SKU,再展示对应价格和库存。如果SKU没有匹配上,要禁用对应选项。这个交互逻辑,其实比很多教程里写得要复杂得多,也是前端容易出错的地方。
购物车页面重点在“勾选合计”的计算:已勾选的SKU数量、总价,都应该通过计算属性实时计算,而不是每次手动操作按钮去累加。
5.5 管理端页面的模式化开发
管理端页面基本都能拆成“一个表格 + 一个搜索区 + 一个弹窗表单”的组合。以商品管理为例:
- 页面加载时请求分页数据,渲染到el-table;
- 搜索区字段绑定搜索条件,点击搜索重新请求第一页;
- 新增按钮打开弹窗,填完表单提交到后端,成功后刷新表格;
- 编辑按钮打开弹窗并回填当前行数据;
- 下架/上架按钮直接调状态切换接口。
这套模式熟练之后,你会发现分类管理、订单管理、公告管理、用户管理都是在复制这套模板,只是字段不同。这恰恰是前后端分离开发的高效之处——页面模式统一,重点精力放在业务字段和校验规则上。
5.6 联调中最常见的三个坑
第一个是字段命名不一致。后端用驼峰风格返回userId,前端写user_id,结果页面上显示undefined。这个靠肉眼排查效率很低,最好的办法是定好约定:后端JSON统一使用小驼峰,前后端各写各的,联调前先对照一次接口返回示例。
第二个是日期格式。后端LocalDateTime默认序列化出来是一串带T的ISO字符串或者数组格式,前端显示时很难看。需要后端额外配置Jackson的日期格式,或者后端统一返回时间戳,前端用格式化工具处理。
第三个是开发环境的跨域。前后端分离开发时,前端地址是localhost:8080,后端接口是localhost:9090,直接请求会跨域。最省事的方案不是在后端逐个接口加@CrossOrigin,而是在vue.config.js里配置proxy代理,把/api前缀的请求转发到后端地址。生产环境则完全交给Nginx处理,后端代码里不需要为跨域做任何事。
6. 部署上线:从本地跑通到服务器发布
6.1 后端打包与启动
后端部署的基本操作是把SpringBoot项目打成可执行jar包:
mvn clean package -Dmaven.test.skip=true java -jar target/xxx.jar --spring.profiles.active=prod生产环境跑起来之前,重点检查三处配置:
- 数据库连接地址是否改成生产库地址;
- 上传文件路径是否改成服务器上的绝对路径;
- 服务端口是否有冲突。
项目里的配置文件一般会区分application-dev.yml和application-prod.yml,通过启动参数激活对应配置。这个做法要养成习惯,不要在改配置时直接改默认的application.yml,不然本地开发和线上部署配置来回切换,迟早会忘改某一行。
6.2 前端构建与Nginx配置
前端部署的产出是构建后的静态文件目录dist:
npm install npm run build然后把dist里的文件上传到服务器,用Nginx托管,同时把/api开头的请求反向代理到后端服务。Nginx核心配置如下:
server { listen 80; server_name localhost; root /var/www/dist; index index.html; location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }最后一行try_files $uri $uri/ /index.html;是Vue Router history模式必须的重定向规则,否则前端路由在刷新页面时会404。如果后端上传图片的访问路径也通过Nginx映射,再增加一个/upload的location指向服务器上的上传目录。
6.3 数据库初始化与常见报错排查
上线导入SQL脚本时,注意确认MySQL的字符集是utf8mb4,不然遇到中文表情符号或特殊字符会报编码错误。导入后启动应用,第一个要验证的就是数据库连接是否正常,常见的报错集中在:
Unknown database:数据库名不匹配;Access denied:用户名或密码错误;Communications link failure:数据库服务没启动,或者连接地址写错。
部署完成后,按用户路径完整走一遍:注册、登录、浏览商品、加入购物车、提交订单、模拟支付、管理员发货、用户确认收货。这条链路里任何一个环节出问题,都能通过浏览器开发者工具和后端日志快速定位。
6.4 部署后常见问题清单
| 现象 | 大概率原因 |
|---|---|
| 页面能打开,接口全部404 | Nginx的/api代理没生效,或后端没启动 |
| 接口返回500 | 后端日志里有异常,多半是数据库连接或SQL问题 |
| 前端页面刷新后404 | Nginx没配置try_files回退index.html |
| 上传图片无法显示 | 上传路径和静态资源映射不一致 |
| 时间显示差8小时 | 数据库时区和应用时区配置不一致 |
7. 项目复盘:这套系统真正锻炼了什么能力
7.1 从“会写接口”到“会建模”
写完这个项目最大的收获,是彻底理解了数据建模不是画几张表那么简单。业务里的每个规则都要映射到表和字段上:头数分级怎么落到SKU、溯源批次怎么跟着订单走、订单状态如何驱动接口行为。这些能力不是看教程能学会的,必须跟着一个完整项目从零捋一遍才体会得到。
7.2 前后端联调与自测能力
很多单独学前端或单独学后端的人,第一次做前后端分离项目都会栽在联调上。字段对不上、类型不一样、状态码没约定好,这些问题在单人开发时容易暴露,在团队协作里会被无限放大。通过这个项目把请求封装、错误处理、联调规范跑通一遍,后面再做任何项目都会顺手得多。
7.3 部署不再是黑盒
自己完成一次从开发到部署的全流程之后,再看到服务器、域名、端口这些词就不会发怵了。Nginx反向代理、前端静态资源托管、后端jar包运行、数据库初始化,这一整套链路跑通,工程化能力会有一个明显的提升。以后哪怕换一个更复杂的系统,核心思路也是相通的。
7.4 这类项目的通病与提升方向
如果用苛刻的眼光看,这类项目还有不少可以提升的地方:支付是模拟的,没有对接真实网关;图片存储是本地磁盘方案,生产环境更适合对象存储;权限控制只做了简单的角色区分,没有细粒度的功能权限;没有引入缓存层,商品详情页的高频查询都直接打到数据库。
如果要在这个方向继续深入,我的建议是优先补两块:一是给商品查询加Redis缓存,二是把订单模块抽出更完善的状态机。这两块做下去,项目深度会比堆十个普通功能页面更有说服力。
写在最后
这套系统本身不算“高大上”,它最舒服的地方在于完整:完整的业务场景、完整的技术链路、完整的部署方案。对我而言,比代码本身更有价值的,是它把“从需求到上线”这条路完整走了一遍——从分析三七这个品类的产地属性开始,到设计表结构,再到前后端联动,最后看到页面在服务器上跑起来,那种把一条业务链路真正握在手里的感觉,和单纯刷完几个框架教程是完全不一样的。
如果你正在做同类项目,或者准备拿它作为前后端分离入门的第一块跳板,我建议不要急着删掉某个模块“减负”。把这个项目的核心链路捋清楚,搞清楚每个表为什么存在、每个状态为什么这么流转,再谈改造和扩展,收获会大得多。