SpringBoot+Vue医疗用品商城系统:设计与实现全解析
做医疗用品商城这个项目的时候我一开始以为就是个普通电商系统无非是商品、购物车、订单、支付这几套东西换皮。真的动手之后才发现医疗用品和衣服鞋帽完全是两种玩法光是资质字段、分类校验、库存准实时性就够喝一壶的。这套 SpringBoot Vue 前后端分离的方案是我反复权衡后定下来的组合下面我把完整的设计思路、搭建过程、踩坑经历一次性讲清楚不管是拿来当毕业设计、课程项目还是商用项目的脚手架应该都能少走不少弯路。1. 项目定位与整体方案设计1.1 核心需求拆解医疗用品商城到底要做什么医疗用品销售商城从用户视角看就是一个垂直类电商网站但它的业务特质比普通电商更严谨。这里说的医疗用品通常包括家用医疗器械、医用耗材、消毒防护用品、康复辅助器具等这类商品的共同点是对资质和安全性的要求很高。因此系统除了常规的用户注册登录、商品浏览、加入购物车、提交订单、在线支付、后台管理之外还必须具备两个专属能力一是商品详情要能承载和展示资质信息比如注册证号、生产许可证、产品标准号二是订单流程要支持售后溯源用户下单之后最好能关联物流单号。站在开发的视角我把整个项目分成三条线。用户端前台商城负责浏览和交易首页轮播、商品分类、搜索、商品详情、购物车、结算页、个人中心、订单列表。管理端后台管理系统负责运营商品管理、类目管理、库存管理、订单处理、用户管理、banner管理、基础参数配置。公共基础服务负责支撑登录鉴权、文件上传、数据统计、异常处理。这三条线加在一起构成了一个完整闭环。很多刚入门的同学容易把后台接口和前端页面割裂开来做结果联调的时候发现谁也不知道对方要什么数据结构这是最浪费时间的坑。我的做法是先定义前后端契约再各自开发。1.2 为什么选型 SpringBoot Vue而不是其他组合选型这事没有绝对的最好只有当前阶段最合适。SpringBoot 的生态太成熟了从 MyBatis-Plus 到 Spring Security几乎你能想到的组件都有现成的整合方案招聘市场认可度也高对新手学习路径非常友好。Vue 作为前端框架上手曲线平缓响应式数据和组件化设计让电商页面的开发效率极高而且中文文档和社区资料丰富。这里要特别注意一个版本选择的细节。SpringBoot 3.x 发布之后很多人一上来就选最新版实际上 3.x 要求 JDK 17 和 Jakarta EE 规范原本的 javax.* 包名换成了 jakarta.*很多旧教程和旧代码直接跑不通。做这类综合项目我强烈推荐 SpringBoot 2.7.x JDK 8因为稳定、兼容性好、参考资料最全等把所有核心逻辑跑通了再考虑升级也不迟。Vue 那边我建议直接用 Vue3 Vite Element PlusVite 的冷启动速度比 Webpack 快一个量级开发体验太好了而且 Vue3 的 Composition API 在做复杂状态管理时比 Options API 更清晰。1.3 整体架构设计前后端分离的协作模型整个项目采用前后端完全分离的架构。后端只提供 RESTful API不关心页面渲染默认监听 8080 端口前端是一套纯静态单页应用开发环境跑在 Vite 的 5173 端口通过代理访问后端接口生产环境打包成静态文件后由 Nginx 托管Nginx 再反向代理 API 请求到后端服务。这套模型解决了一个很现实的问题前后端可以让不同技能背景的人并行开发。后端只要定义好接口文档前端就可以用 Mock 数据先把页面画出来两边互不阻塞。数据格式我用 JSON传输标准用 HTTP JWT文件存储用对象存储本地搭建 MinIO热点数据用 Redis 缓存。这种架构的另一个好处是将来扩展方便比如你想再做一个微信小程序端前端技术栈完全可以独立更换后端接口不需要任何改动。2. 数据库设计与后端核心模块实现2.1 数据表设计从用户到订单的完整链路数据库是整个系统的地基表关系如果设计得有缺陷后面所有功能都会受到影响。以我实际落地的方案来说核心表一共 12 张用户表 sys_user、角色表 sys_role、权限表 sys_menu、用户角色关联表、分类表 category、商品表 product、商品资质表 product_qualification、购物车表 cart、订单表 orders、订单明细表 order_item、地址表 address、轮播图表 banner。另外我还加了一张操作日志表记录管理员的关键操作方便排查问题。用户表字段除了常规的 username、password、phone、email 之外我特意加了一个 status 字段做账号封禁防止恶意下单。商品表是信息量最大的表product_name、subtitle、main_image、detail_images、price用 decimal 类型不要用 float精度问题很坑、stock、category_id、status、sales_count外加 sale_type 字段区分一类器械、二类器械、普通耗材这个字段是医疗商城的特色前端要根据它决定是否展示资质模块。订单表包含 order_no唯一订单号、user_id、total_amount、pay_status、delivery_status、receiver_name、receiver_phone、receiver_address。为了保证订单明细快照不被后续商品信息修改影响order_item 表里冗余存储了下单时的商品名称、价格和图片这在电商领域叫快照设计非常重要。建表的一个实操心得所有表都加上 create_time、update_time、deleted 三个公共字段删除一律逻辑删除不要物理删。数据虽然看起来变多了但误操作恢复、历史溯源都方便尤其是订单和商品这种金融级数据物理删除绝对是大忌。2.2 商品模块与资质展示设计医疗商品模块的难度不在增删改查而在于它有很多细碎的展示逻辑。我的做法是在商品表旁边另建一张 product_qualification 表存注册证号、注册证有效期、生产许可证号、生产厂家、产品标准号等字段通过 product_id 和商品一对一关联。为什么单独建表而不是直接塞进 product 表因为资质字段不是所有商品都有比如普通棉签和电子血压计的资质要求就不一样单独建表可以避免大量空字段也让前后端逻辑更清晰。再看商品列表接口的设计。商城首页需要商品瀑布流后台需要商品管理表格这两个场景对字段要求不同。我拆了两个接口listCustomerProduct用户端只返回上架状态、精简字段和 listAdminProduct管理端返回全部字段和上下架状态。这个设计一开始不觉得有什么等做到权限控制的时候就明白好处了。另外价格存储用小数的精度问题必须提一下——数据库用 DECIMAL(10,2)Java 端用 BigDecimal前端展示用 toFixed(2)三端口径一致否则会出现 9.9 变成 9.899999 这种诡异问题。2.3 订单流程与库存扣减方案订单是整个系统并发压力最大、最容易出 Bug 的地方。我的订单状态机是待支付 → 已支付/待发货 → 已发货 → 已完成另外有已取消和售后中两个分支状态。下单接口的核心逻辑是校验商品状态和库存 - 加锁扣库存 - 生成订单主表和明细 - 清空购物车对应商品 - 返回订单号。这里最经典的问题是什么是超卖。我试过三种方案。第一种是最土的前端校验下单前先查一下库存够不够够就扣看似没问题但极端并发下两个请求同时查到库存为 1然后都执行扣减库存就变成 -1 了。第二种是数据库乐观锁在商品表加 version 字段扣库存时用 update product set stock stock - #{count}, version version 1 where id #{id} and stock #{count} and version #{version}受影响行数为 1 才表示扣成功否则重试。第三种是 Redis 分布式锁配合预扣减。对于常见项目我强烈推荐第二种方案简单、可靠、不出幺蛾子。update 语句里带 stock #{count} 条件本身就是防超卖的关键不依赖任何外部组件。如果将来单量真的大到数据库扛不住再演进成 Redis 预扣减 异步回写那是后话。超时未支付的订单怎么处理在订单表建一个 expire_time 字段下单时间 30 分钟用 SpringBoot 自带的 Scheduled 定时任务每分钟扫描一次把超时且未支付的订单置为取消同时恢复库存。这个方案够用不用为了一个小项目上 RabbitMQ 延迟队列增加运维成本。2.4 登录鉴权与权限控制实现SpringBoot 后端常用的鉴权方案有两种Session 和 JWT。前后端分离项目里我建议用 JWT因为后端接口要同时服务于网页、可能的小程序、甚至将来的 App无状态 Token 是最通用的。JWT 的流程是这样的用户登录成功后后端生成 Token 返回给前端前端存在 localStorage 或 Pinia 里每次请求在 HTTP 头加 Authorization: Bearer 后端通过拦截器或者 Spring Security 过滤器解析 Token 并校验合法性。权限控制我用 RBAC 模型用户绑角色角色绑菜单权限后端在需要权限的接口上标注 PreAuthorize(hasAuthority(system:product:edit))。拦截器里先校验 Token 是否有效再通过 Spring Security 的上下文把当前用户信息传给业务层。这里要提醒一个实际坑JWT 一旦签发在过期之前是无法主动失效的。如果用户被封禁他手里的 Token 还能继续用直到过期。解决办法是在用户表加一个 token_version 字段登录时把版本号写进 Token 里每次请求校验版本号是否一致不一致就强制重新登录。这个方法简单有效比黑名单机制干净多了。3. 前端工程化与商城页面实战3.1 前端项目结构与路由设计前端工程我用的 Vue3 Vite Pinia Element Plus Axios目录结构按模块划分src/api 放所有接口请求src/router 放路由配置src/store 放全局状态src/views 放页面组件src/layout 放布局组件用户端公共头部、后台侧边栏src/components 放公共组件。路由设计是整个前端的骨架。用户端路由是公开的首页 /home、商品列表 /list、商品详情 /detail/:id、购物车 /cart、结算 /checkout、订单列表 /orders、登录 /login、注册 /register。后台管理路由需要登录且鉴权我用动态路由的方式实现登录成功后从后端拉取当前用户的菜单权限数据再用 router.addRoute 动态挂载。为什么不能直接写死后台路由因为员工账号和超管账号看到的菜单不一样如果你在路由表里写死了所有路径前端隐藏菜单并没有用用户手动输入 URL 一样能访问。动态路由配合路由守卫 beforeEach才能做到真正的前端权限控制。路由守卫这地方很多同学容易忽略一个细节动态路由 addRoute 是在登录之后异步执行的如果页面刷新Pinia 里的用户状态和动态路由会全部丢失。所以刷新时必须重新调用获取用户信息的接口再重新挂载动态路由否则就会出现登录状态明明在一刷新却提示没有权限的诡异问题。我的解决办法是在全局守卫里判断本地有没有 Token 和用户信息有但没有动态路由表时先去请求后端拉取权限等路由挂载完成再放行。3.2 商城核心页面实现要点首页的轮播图、分类导航、热销商品三个模块数据都来自后端接口。轮播图走 banner 接口返回图片 URL 和跳转链接分类导航走分类接口展示一级分类商品推荐走列表接口按销量降序取前 8 条。这些接口都有一个共性返回的都是商品摘要信息不需要大字段。所以我在商品列表接口里用 JSON 序列化时只保留 id、图片、标题、价格、销量把详情字段排除减少页面加载压力。商品详情页是最复杂的前端页面。上半部分是主图和基本信息区展示价格、标题、销量、库存、资质信息下半部分是商品详情图。交互逻辑集中在立即购买和加入购物车两个按钮上立即购买直接跳到结算页带上当前商品 ID 和数量加入购物车则调用后端接口写入购物车表。这里有个易错点商品的库存单位要统一有的按个有的按盒前端不能拍脑袋直接写数量建议在后端商品表加一个 unit 字段计量单位页面直接展示出来。购物车页相对简单无非是列表展示 数量加减 勾选结算。结算页稍微复杂一点要展示收货地址、商品清单、金额汇总下单时把选中的购物车 ID 列表传给后端后端批量删除。用户的地址管理是另一个独立页面按默认地址排序结算页自动带出默认地址。3.3 Axios 封装与状态管理前端和后端交互最绕不开的就是 Axios。我的做法是封装一个 request.js统一做四件事从 localStorage 取 Token 放到请求头、统一处理后端返回的 code200 表示成功401 表示未登录500 表示业务异常、错误提示统一用 Element Plus 的 ElMessage、响应拦截器里统一处理 401 跳转登录页。这层封装的好处是整个项目里的业务代码只需要关心数据本身不需要在每个页面里重复写错误处理。状态管理用 Pinia。我建了三个 StoreuserStore 存用户信息和登录状态、cartStore 存购物车数量因为头部导航要实时显示角标、appStore 存系统配置比如站点名称、logo。购物车数量这个状态特别容易踩坑——你在购物车页改了数量回到首页头部角标却不更新就是因为两个页面共享的状态没有存在全局 store 里。用 Pinia 把 cartCount 存起来购物车任何变动都同步调用 updateCartCount。3.4 移动端适配与体验优化医疗用品商城的使用场景非常特殊用户很多是拿着手机给家里老人买血压计、血糖试纸所以移动端适配不是可选项是必选项。我的做法分两层第一层是页面响应式布局商品列表用 CSS Grid 自适应列宽大屏显示 4 列小屏显示 2 列第二层是触摸交互优化按钮的触控区域不小于 44px购物车数量加减的 /- 图标用大号点击区域。实测下来Element Plus 的表格组件在手机上体验很差所以后台管理页面我只要求桌面端用户端商城页面才做移动端优化。如果预算充足也可以考虑用 Vant 重做一套移动端商城界面但这里有个更省力的套路直接用同一套 Vue3 代码利用 CSS 媒体查询和组件库的响应式栅格把用户端页面做到能用、好用的程度再配合 Vite 的 viewport 配置调整根字号。能省非常多工作量。4. 前后端联调与常见环境配置4.1 开发环境必备工具箱开始写代码之前先把环境准备好不然中途发现 JDK 版本不对、Maven 仓库依赖拉不下来心态直接爆炸。我的建议清单JDK 1.8对应 SpringBoot 2.7.x、Maven 3.8配置阿里云镜像源、MySQL 8.0、Redis 6.x、Node.js 16/18、VSCode 或 IDEA前端用 VSCode后端用 IDEA 更顺手。后端项目用 IDEA 创建的时候直接在 Spring Initializr 里选 SpringBoot 2.7.x依赖勾选 Spring Web、MyBatis-Plus、MySQL Driver、Lombok、Validation。这里要提醒Spring Initializr 在 IDEA 2023 之后的默认版本可能变成了 3.x创建项目时要注意切换。包结构建议按模块分controller接口层、service业务层、mapper数据访问层、entity实体、dto传输对象、vo视图对象、config配置、common统一响应和异常、utils工具。前端工程的搭建相对简单用 npm create vitelatest 选择 Vue JavaScript 模板然后安装依赖npm i vue-router4 pinia axios element-plus。Element Plus 支持按需引入推荐用 unplugin-vue-components 插件自动按需注册打包体积能小很多。4.2 统一响应结构与全局异常处理前后端联调最怕的就是各写各的格式。我在后端定义一个 Result 类统一包含 code、message、data 三个字段所有接口都返回这个结构。比如成功就是 Result.success(data)参数错误就是 Result.error(400, 参数不能为空)业务异常就是 Result.error(500, 库存不足)。前端 Axios 拦截器里只关心 code 是不是 200不是就弹出 message。全局异常处理用 RestControllerAdvice 注解捕获业务异常、参数校验异常、未知异常三类。这个设计的价值在开发中后期会体现得非常明显之前没有统一异常处理的时候一个空指针异常直接返回 500 和一大坨堆栈信息给前端用户看到的是英文报错很难看处理加了这个全局处理器之后所有异常都变成友好提示日志照常打印排查问题不耽误。4.3 联调阶段最容易踩的坑第一个坑是跨域。前后端分离开发页面在 5173 端口接口在 8080 端口浏览器会拦截跨域请求。解决办法有两个前端用 Vite 的 proxy 代理配置 server.proxy把 /api 开头的请求代理到 http://localhost:8080后端用 CrossOrigin 或者 CORS 配置类。开发阶段用 Vite 代理更省事生产环境交给 Nginx 处理后端不用动。第二个坑是图片路径。开发的时候我直接把图片上传到本地磁盘数据库里存的是 /upload/xxx.jpg 这种相对路径结果前端怎么都显示不出来。原因很简单前端的域名是 localhost:5173它去访问 localhost:5173/upload/xxx.jpg 当然找不到因为文件在 8080 端口。正确做法是数据库存绝对 URL或者约定前端把图片地址前缀统一从后端配置里读取比如系统配置表里存 fileBaseUrl 字段。第三个坑是日期格式。后端返回的 LocalDateTime 默认序列化是一个数组前端看的一头雾水。解决方法是全局配置 Jackson 的日期格式为 yyyy-MM-dd HH:mm:ss或者直接在前端统一用 dayjs 格式化。4.4 文件上传与 MinIO 对象存储商城项目离不开图片上传商品主图、详情图、资质照片、轮播图都要传。本地磁盘存储省事但不合适因为生产环境服务器用 Docker 部署时本地磁盘的文件在容器重构后会丢失而且图片多了之后磁盘管理很混乱。我建议用 MinIO这是一个开源的轻量级对象存储服务社区版免费支持 Docker 一键部署。SpringBoot 整合 MinIO 的核心步骤引入 minio 依赖配置 endpoint、accessKey、secretKey、bucketName写一个存储服务类封装上传和下载方法。上传接口接收 MultipartFile生成新的文件名用 UUID 加上原始文件扩展名然后调用 putObject返回可访问的 URL。这里要记得配置 MinIO 的 bucket 访问权限为 public否则图片链接是私有的需要签名小程序端展示会有额外麻烦。5. 打包部署与生产环境实践5.1 Vite 构建与 Nginx 部署配置前端开发完成后的最后一步是打包npm run buildVite 会生成一个 dist 目录里面是纯静态文件。部署方式我就用 Nginx直接托管 dist 目录简单高效。这里最经典的坑是 history 模式路由刷新 404Vue Router 如果用 createWebHistoryURL 是 /home 这种形式刷新页面时 Nginx 去磁盘找 /home 文件肯定找不到于是返回 404。解决办法在 Nginx 配置里加一条 try_files $uri $uri/ /index.html意思是所有请求都先匹配真实文件匹配不到就统一返回 index.html由前端路由接管。Nginx 还需要反向代理 API 请求。我用 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } 这组配置把前端发的 /api 请求转发到后端服务。生产环境的 Nginx 配置文件我建议再加上 gzip 压缩和静态资源缓存首屏加载速度会明显提升。5.2 SpringBoot 后端打包与进程守护后端打包用 Mavenmvn clean package生成 jar 包。生产环境启动不能用 java -jar 直接前台跑否则 SSH 一断开服务就停了。我用 systemd 创建服务新建 /etc/systemd/system/mall-server.service内容包含 ExecStart 命令、WorkingDirectory、重启策略为 always。配置好之后 systemctl daemon-reload systemctl enable mall-server systemctl start mall-server服务就开机自启、崩溃自动重拉了。还有一个非常影响体验的细节启动参数里要指定 JVM 内存和时区。时区不对会导致订单时间差 8 个小时看起来就像未支付倒计时凭空少了 8 小时这是最容易忽略的。我的启动命令里明确写 java -Xms512m -Xmx1024m -Duser.timezoneAsia/Shanghai -jar mall-server.jar。5.3 数据备份与基础运维MySQL 备份是商城项目的生命线哪天误删了订单数据没有备份就只能哭。我用的是 crontab 定时任务每天凌晨 3 点执行 mysqldump 全量备份备份文件保留最近 7 天。命令大概是 mysqldump -u root -p数据库名 | gzip /backup/db_$(date %Y%m%d).sql.gz。Redis 的话如果只用了缓存和 Token丢失影响不大开启 AOF 持久化就行设置 appendonly yes。安全方面要注意几个点MySQL 不要用 root 直连建一个业务专用账号只授予当前数据库的权限后端服务端口不要直接暴露到公网只让 Nginx 访问管理后台地址不要用简单的 /admin换个不常见的路径能挡住大部分自动化扫描。这些听起来都是小事等真的出事就知道多重要了。6. 常见问题排查与避坑手册6.1 后端高频问题速查现象原因解决办法接口返回一串英文报错前端显示 500Controller 没加全局异常处理用 RestControllerAdvice 统一包装 Result商品列表查询很慢没有走索引给 category_id、status 建联合索引下单时库存变成负数update 语句缺少 stock count 条件用乐观锁防止超卖LocalDateTime 序列化格式怪异Jackson 默认配置问题全局配置日期格式或加 JsonFormat启动时报 javax 包找不到用了 SpringBoot 3.x 但代码是 2.x 的 javax换用 jakarta 包或回退 SpringBoot 2.7Maven 下载依赖失败默认中央仓库太慢配置阿里云镜像6.2 前端高频问题速查现象原因解决办法刷新页面 404history 模式没有对应文件Nginx 加 try_files 规则跨域请求被拦截前后端端口不一致Vite proxy 或后端 CORS登录后刷新路由权限丢失动态路由没重新挂载全局守卫里重新请求权限并 addRoute图片上传成功但页面上不显示相对路径没有域名前缀数据库存完整 URL 或统一读取配置购物车角标不更新状态没放全局 store用 Pinia 管理 cartCountElement Plus 组件不生效没按需引入或样式丢失用 unplugin-vue-componentscreateWebHistory 路由报错部署后访问的不是根路径改用 hash 模式或用 try_files6.3 医疗商城专属的合规细节医疗用品商城和普通商城比还有一个很容易被忽视的模块是合规展示。我实际做的时候在商品详情页专门做了一个资质区展示注册证号、经营许可证号、生产厂家、有效期。后台在发布商品时这些资质信息是必填项如果为空商品不能上架。这种强制校验的初衷是真实的业务需求——购物者买血压计、血糖仪这种器械看到完整的资质信息才安心。另一个细节是售后条款。医疗用品一经拆封原则上不支持七天无理由退换出于卫生安全考虑但这个规则如果不提前告知用户后续纠纷会让人焦头烂额。所以我在商品详情页和结算页都加了商品说明文案标明拆封后退换规则和禁忌事项。这些都是业务层面的设计开发层面只是加字段和展示逻辑但价值很大能让整套系统显得真正懂行。7. 写在最后的一些实操心得整套系统从零开始搭到上线我自己改了不下十遍。第一条心得是数据库设计阶段一定要多想想未来会加什么字段哪怕暂时用不上预留扩展字段的成本远小于以后迁移表的成本。第二条心得是前后端联调最好同一个项目文档里把所有接口参数和返回示例写清楚很多低级 Bug 都是因为前后端理解不一致。第三条心得是别过度设计超卖用乐观锁就够了定时关单用 cron 就够了缓存用 Redis 就够了等真实流量打过来再演进学架构设计的时候那些复杂方案才能体现出价值。技术这块我觉得最值得花时间研究的地方是订单状态机的设计把它做精致了后续的支付回调、异常重试、售后流程全部会变得很丝滑。这个医疗用品商城的骨架可以复用换个皮肤就是生鲜商城、宠物商城、二手商城核心的账号、商品、订单、支付逻辑完全一样这也是做一套完整系统的复利价值。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →