
做小区购物系统这个事听起来不复杂但真动手你会知道把商品展示、购物车、下单、订单状态、后台管理全部串起来再加上“尽快跑起来”的时间约束完全没有想象中那么轻松。我这次要聊的这套前后端分离小区购物系统技术栈很经典SpringBoot Vue MyBatis MySQL。它解决的问题很具体——小区居民在线浏览商品、加购物车、下单物业或运营方在后台维护商品和订单。说白了这就是一个垂直场景的商城系统很适合两类人正在找完整项目练手的人以及给社区物业或小范围集采场景搭建线上购物工具的人。这套系统如果只是做个 Demo一天也能搭出来。但要做成能演示、能部署、能应付答辩或者真实试用的项目核心工作量其实都在细节里表结构怎么设计才不返工接口返回格式怎么统一前端怎么和后端对齐打包部署有哪些必踩的坑。这篇内容我会按照从零到部署上线的顺序把整套链路拆开讲清楚技术方案不炫技但每一步都能落得了地。1. 为什么要做一套前后端分离的小区购物系统1.1 场景痛点集中采购最怕人工统计和订单混乱先还原一下使用场景。一个小区在特殊时期或者日常便民服务中经常会出现集中采购的需求物业提前上架一批商品居民在线看清单、选购、下单然后统一配送到自提点或者按楼栋分发。如果这套流程靠微信群接龙或者 Excel 统计三个问题马上会暴露出来。第一是订单信息天然分散群里几百条接龙消息根本没法快速汇总。第二是库存容易超卖前面的人还没统计完后面的人又在接龙等真正核算的时候才发现货不够分。第三是用户改单、取消单之后人工台账很容易对不上账最后很难向居民交代。这个系统的核心价值就在这里把选品、下单、订单管理都收敛到一套线上流程里居民自主操作后台自动统计订单和库存。特殊时期是应急手段放到日常它就是社区团购和物业小商城的雏形需求长期存在不是做完就扔的作业。1.2 前后端分离协作效率更高迭代路径更清楚选择前后端分离而不是传统的服务端渲染页面不是单纯因为“流行”而是基于实际维护成本的考量。用 JSP 或者 Thymeleaf 做传统单体页面早期开发确实快但到后期会很难受前端页面逻辑和后端业务代码混在一个工程里改一个按钮样式可能要重新打包整个后端联调的时候前后端互相阻塞。前后端分离之后后端只提供 JSON 接口前端负责页面渲染和交互两者通过 HTTP 通信。这样有几个直接好处。开发阶段可以用 Vue 的代理服务器转发请求到 SpringBoot 接口前端不用关心后端部署在哪里。部署阶段可以让 Nginx 托管前端静态文件后端独立运行在 Java 进程里彼此互不影响。项目给到别人手里看源码时结构也清楚得多前端一个目录后端一个目录谁都不用去一堆混合代码里找文件。1.3 技术选型SpringBootVueMyBatis 的组合为什么够用这套技术栈在国内项目里的地位相当于家常菜里的番茄炒蛋不稀奇但稳定覆盖面广。后端选 SpringBoot主要看中的是自动配置和起步依赖一个内嵌 Tomcat 就能把服务跑起来省掉了传统 SSM 整合时一堆 XML 配置的麻烦。MyBatis 负责持久层相比 JPA 那种把 SQL 藏起来的方案MyBatis 把 SQL 明晃晃写在 Mapper 文件里对习惯了写 SQL 的开发者来说更可控尤其是涉及到订单统计、多表联查的时候SQL 自己写反而心里有底。数据库用 MySQL没得说主流、资料多、团队认知度也高。前端用 Vue 2 Element UI虽然现在 Vue 3 已经很成熟但 Vue 2 的生态积累和教学资料更厚Element UI 组件库覆盖表格、表单、弹窗、分页这些后台管理场景非常顺手上手成本也低。如果你更想用 Vue 3 Element Plus逻辑是一样的只是 API 细节有差异这套项目的代码结构同样能参考。这一整套选型的核心逻辑就一条用最稳妥、最容易招到人接手、也最容易找到资料的技术把业务跑通。对于小区购物系统这种业务复杂度中等的项目来说这个组合完全够用。2. 数据库设计先把自己的地基搭稳2.1 六大核心表的最小可用设计数据库表设计决定了后面写接口是顺畅还是痛苦。以这套系统为例我拿到需求后的第一件事不是写代码而是先把表结构画出来。最小可用的方案需要六张表用户表、商品分类表、商品表、购物车表、订单表、订单明细表。用户表负责两类角色普通居民和管理员通过一个 role 字段区分。手机号是登录账号再加上密码、昵称、楼栋门牌号这些基本信息。商品分类表解决商品归类的问题比如蔬菜、水果、粮油、日用品。商品表是核心包含商品名称、图片、价格、库存、销量、上下架状态外键关联分类。购物车表比较简单一个用户对应多个商品条目记录商品 ID 和数量。订单表是业务重点一笔订单要记录订单编号、下单用户、收货人信息、总金额、订单状态、下单时间。订单明细表把订单和商品的快照关系拆开每一行记录一个商品的下单名称、下单价格和数量。为什么订单明细里要冗余商品的名称和价格而不是只存 product_id原因是商品价格和名称随时可能变如果只存 ID订单历史就会被后续商品修改污染这在严谨的订单系统里是不允许的。快照设计是这里最容易忽略但最值得坚持的一条原则。2.2 字段类型和索引订单表尤其要上心字段类型看着是小事但选错会很耽误事。价格字段我建议用 DECIMAL(10, 2)不要用 FLOAT 或 DOUBLE二进制浮点数在计算金额时会出现精度误差这是做电商类系统的基本底线。库存和销量用 INT 就行大数量也够用。商品图片存的是 URL 路径而不是二进制图片本身图片文件落盘或者走对象存储这样数据库的压力会小很多。订单金额和单价统一用 DECIMAL状态字段用 TINYINT 或者 VARCHAR 都行我习惯用 TINYINT 配一个状态枚举前端展示时再映射成文字这样写条件查询时数字比较效率更高。索引方面主键是默认的聚簇索引不需要额外操作。需要额外关注的是业务查询字段订单表的 user_id 会经常用来查“我的订单”order_no 会用来精确查询订单详情商品表的 category_id 用来做分类商品列表status 字段如果查询频繁也可以加索引。加索引的核心原则是优先给 where 条件里的字段和经常 order by 的字段加但不是越多越好索引写多了会拖慢插入和更新。2.3 建库建表时的三个注意事项第一字符集直接设置成 utf8mb4不要用 utf8。原因很简单utf8 在 MySQL 里最多支持三个字节遇到生僻字或者 emoji 会出现乱码或者报错而现在的用户昵称、收货地址里什么字符都可能出现。第二订单编号不要用数据库自增主键暴露给用户。自增主键可以自己用但对外展示的订单号建议单独生成格式可以定为时间戳用户 ID 后几位随机数这样既不容易被遍历猜测也显得专业。代码里生成好再插入订单表不要把这件事丢给数据库。第三下单操作要意识到库存扣减不是单条 SQL 就能解决的。虽然我们不是高并发抢购系统但在设计表结构时要把商品的库存字段和订单明细表之间的关系想明白。库存扣减一定要在事务里执行并且用UPDATE product SET stock stock - #{num} WHERE id #{id} AND stock #{num}这种条件更新避免超卖。3. 后端编码SpringBootMyBatis 如何撑起全部业务3.1 工程分包与三层结构后端工程我会按标准的 controller、service、mapper、entity、config、common 分包。common 里放统一返回体、异常处理、工具类。controller 只做参数接收和结果返回不写业务逻辑service 层处理具体业务比如下单要校验库存、计算金额、减库存、生成订单明细这个编排动作都在 service 里完成mapper 层通过 MyBatis 操作数据库。这样做最大的好处是出了问题知道去哪找。接口报错先看 controller 参数传对没有业务逻辑不对去 service 层看流程SQL 跑得慢或者数据不对去 mapper 和 XML 里排查。分层清晰之后这段代码就算过三个月再看也不需要从头捋一遍。MyBatis 的 Mapper 我建议用 XML 文件而不是全写在注解里。注解适合非常简单的 SQL一旦 SQL 超过三行Java 代码里拼字符串看着就难受。XML 可以把 SQL 独立出来格式化起来舒服写复杂的动态 SQL 时也比注解灵活得多。项目里的 Mapper 接口只定义方法SQL 写在 classpath 下的 mapper 目录里在 application.yml 中配置 mapper-locations 指过去就行。3.2 统一返回体与登录鉴权前后端对接最容易出现的问题就是每个接口返回格式都不一样。前端处理数据时需要挨个判断测试时也很难受。我的做法是定义一个 Result 类统一结构为 code、message、data 三个字段。code 为 200 表示成功500 表示服务端异常401 表示未登录或登录过期。service 层返回数据controller 统一包一层 Result 返回。登录鉴权这块我选择的是 JWT 方案。用户登录成功后后端生成一个 token 返回给前端前端在后续请求的请求头里带上这个 token。后端写一个拦截器拦截需要登录的接口从 token 里解析用户信息并放入 ThreadLocal。这样做的好处是后端服务可以保持无状态多个实例部署也不依赖 session 同步对这个体量的项目来说足够清晰。需要注意拦截器要放行登录注册接口和商品列表接口不能全部拦截。我在实际开发中碰到的很多新手翻车现场就是拦截器配置过猛结果前端调登录接口都被拦住了排查半天才发现是放行路径没配齐。3.3 核心接口一览从浏览商品到后台管理把核心接口列出来之后整个系统的雏形就清楚了。用户端接口用户注册、用户登录、获取商品分类列表、分页查询商品、查询商品详情、加入购物车、查看购物车、修改购物车商品数量、删除购物车条目、创建订单、查询我的订单列表、查询订单详情、取消订单。管理端接口商品分类的新增和修改、商品的新增和修改、商品上下架、订单列表查询、订单状态更新、用户列表查询。这里有一个接口设计上的小建议能用查询参数就别把查询条件拼在 URL 路径上太深。分页查询商品我习惯用GET /api/product/list?page1size10keyword大米categoryId3这种形式前端传参方便后端接收也规整。以创建订单为例service 里的核心逻辑是这样的根据购物车里的条目查商品当前价格重新计算总金额而不是直接信任前端传来的金额。原因很容易理解前端传来的金额完全可以被篡改后端必须以数据库里的商品价格为准。然后依次校验库存、扣减库存、插入订单主表和订单明细表、清空购物车整个过程加上事务注解任何一步失败都会整体回滚。3.4 实际编码中遇到的 MyBatis 细节MyBatis 写起来容易但细节决定成败。第一个细节是 XML 里的参数符号。#{}是预编译参数占位可以防止 SQL 注入而${}是字符串拼接只在需要动态传入表名或排序字段这种场景才使用。我会在代码规范里直接定死平时一律用#{}禁止把前端参数拼进${}。第二个细节是分页。分页查询我使用 PageHelper 插件它用起来确实方便但有一点必须注意分页插件必须落在紧接着查询语句的第一条 SQL 上。如果业务里先执行了其他查询再执行真正要分页的查询PageHelper 会把分页参数作用到错误的 SQL 上导致数据异常。正确写法是线程内第一条执行的查询就是目标分页查询语句。第三个细节是 Mapper 方法参数多于一个的时候要用Param注解给参数命名。不写Param的话XML 里只能靠 arg0、param1 这种索引取参写过一次的人基本都会回头马上改掉因为完全不知道 arg0 到底代表哪个字段。4. 前端落地Vue 页面如何和真实接口对齐4.1 路由规划用户端和管理端怎么拆前端工程用 Vue CLI 创建项目结构分为视图组件、路由配置、状态管理、API 请求、公共组件。路由规划是这个阶段最值得动脑的部分因为用户端和管理端的页面特性差异很大。用户端页面包括登录注册、首页商品列表、商品详情、购物车、订单确认、我的订单、订单详情。管理端页面包括后台首页、商品管理、分类管理、订单管理、用户管理。管理端页面不能直接暴露给普通用户所以路由要单独拆分。我的做法是管理端的一组页面放在/admin前缀下通过路由守卫检查用户角色。用户登录后把角色信息存在本地存储里前端路由跳转时判断 target 路径是否以 /admin 开头如果不是管理员就重定向回首页。这种前端路由守卫只能控制页面显示后端接口也必须有同样的权限校验前端判断只是体验层面的优化真正的安全边界必须由后端兜底这个原则要记牢。4.2 Axios 封装、拦截器与跨域处理前端请求接口我不会在每个页面里直接写 axios而是先在 src 下的 api 目录统一封装一个 request 实例。封装时要定好三件事请求超时时间、请求头格式、拦截器逻辑。请求拦截器里统一把 localStorage 里的 token 拿出来放到 Authorization 头里。响应拦截器里统一处理返回结构当 code 不等于 200 时弹出对应的错误提示并直接 reject 不让业务代码继续往下走。这样页面里只需要处理正常数据异常分支交给全局拦截器处理代码会干净很多。开发环境的跨域问题用 Vue CLI 自带的 devServer 代理解决。在 vue.config.js 里配置 proxy把/api开头的请求代理到http://localhost:8080后端服务。这样浏览器里请求的都是前端开发服务器的地址不存在跨域问题。生产环境跨域则通过 Nginx 反向代理解决后面的部署部分会详细说。4.3 购物车和下单流程的实现要点购物车页面看起来只是一个表格实际上有几个状态要管理好商品选中状态、总价计算、数量改变后的价格联动。我会在购物车数据里维护一个 checked 字段勾选或者取消勾选时重新计算总价。总价计算建议依赖 Vue 的 computed 属性它会自动追踪依赖的数据变化比手动触发计算要可靠得多。商品数量加减要调用后端更新接口保持数据同步不能只在本地改数字否则刷新页面或者换设备登录就全乱了。下单流程的用户体验顺序是从购物车勾选商品进入订单确认页确认页展示商品清单、收货人信息和应付总金额点击提交订单后调创建订单接口。前端在提交按钮上要加一个防重复提交逻辑比如提交后按钮置为 loading 状态防止用户手快连点两次导致同一笔订单被创建两次。虽然后端事务能兜底但前端体验上的问题最好还是在前端拦掉。订单列表页的订单状态展示建议用 Element UI 的 Tag 标签组件不同状态映射不同颜色。待付款用橙色、待发货用蓝色、已发货用绿色、已完成用灰色一眼扫过去就知道整体情况体验会好很多。4.4 前后端联调参数命名的统一规范前后端联调是很多项目延期的主要原因而大部分联调问题都出在参数命名不一致上。前端传的是 createTime后端接收的是 create_time这种小问题排查起来极其消耗耐心。我的做法是在项目启动阶段就把参数规范定死JSON 字段统一使用小驼峰命名法也就是createTime、userName、totalAmount。MySQL 表字段虽然叫create_time但在实体类里用TableField或直接让 MyBatis 开启驼峰映射map-underscore-to-camel-case: true数据库下划线字段自动映射到实体类驼峰属性。这样前端拿到的 JSON 天然是驼峰格式不用做二次转换前后端认知一致。联调时我会先打开浏览器的开发者工具把每个接口的请求地址、请求参数、响应数据录一遍对比后端接口文档逐项核对。响应数据里如果某个字段是 null不一定说明后端数据有问题也可能是实体类里没配置对应属性直接在 Mapper 的查询结果里就不存在这个字段排查思路不能跑偏。5. 部署上线从本地跑通到云服务器可用5.1 本地开发环境准备本地把项目跑起来需要先准备好几样东西JDK 1.8、Maven 3.6 及以上、MySQL 5.7 或 8.0、Node.js 14 及以上。版本建议尽量贴近这些我试过用 JDK 17 跑 SpringBoot 2.x 项目虽然大部分功能正常但部分版本组合还是会出现依赖兼容问题。为了避免复现过程中无谓的折腾用经典版本组合最省心。后端工程导入 IDE 后等待 Maven 把依赖拉取完然后修改 application.yml 里的数据库地址、账号、密码改成自己本地 MySQL 的连接信息。用 IDE 直接启动 SpringBoot 主类看到内嵌 Tomcat 启动日志后后端就跑起来了。前端工程在终端里执行npm install安装依赖然后npm run serve启动开发服务。这里有个小点要提醒npm install 如果卡住或者报错大概率是网络原因可以设置 npm 镜像源再试。5.2 后端打包配置文件先改这三处本地跑通只是开始部署到服务器前要检查配置文件。第一处是数据库连接地址要改成服务器上 MySQL 的实际地址和库名。第二处是文件上传保存路径如果项目里有图片上传功能默认的本地路径要改成服务器上的固定目录并且要给目录配置可写权限。第三处是端口号默认 8080 可以不改但服务器防火墙里要放行。后端打包用 Maven 命令mvn clean package执行完 target 目录下会生成一个 jar 包。这个 jar 包就是整个后端服务上传到服务器后用java -jar shopping.jar命令启动。如果怕操作窗口关闭后服务跟着停要借助 nohup 或者进程守护工具来保持后台运行。我第一次在服务器上直接用java -jar启动然后关闭终端就发现服务断了所以这个细节一定要提前处理好。5.3 前端打包与 Nginx 静态部署前端打包命令是npm run build执行完后项目根目录下会生成 dist 文件夹里面是纯静态资源。把 dist 目录上传到服务器用 Nginx 作为 Web 服务器托管这些静态文件。Nginx 配置的核心有两块。第一块是静态文件路由让浏览器访问服务器 IP 或域名时直接加载 dist 里的 index.html 和静态资源。第二块是接口反向代理前端请求/api开头的地址Nginx 会把请求转发到本机的 8080 端口交给 SpringBoot 处理这样就解决了生产环境的跨域问题。还有一个容易被忽略的关键点Vue 开启 history 路由模式后用户直接在浏览器里刷新/admin/order这类子页面会出现 404因为 Nginx 找不到对应路径的文件解决方案是加上try_files $uri $uri/ /index.html;把所有找不到的路径都回退到 index.html让前端路由接管。5.4 部署之后一定做一次全链路验证项目部署完成后别急着宣布完工。我会习惯性地从用户视角把全链路走一遍打开首页能看到商品列表点商品详情正常展示图片和价格加入购物车成功提交订单后能在我的订单里看到新记录后台能看到这笔订单并更新状态。这几部都能跑通才算真正上线。部署之后还有两个需要验证的细节一是 Nginx 日志和后端日志有没有出现请求异常二是服务器的时间时区是否正常。如果服务器时区不是东八区数据库里的下单时间会差 8 个小时排查起来很让人困惑。这个坑我第一次遇到时完全没想到后来看数据库里的创建时间比实际时间慢了 8 小时才反应过来。6. 踩坑记录与问题排查速查表6.1 高频问题跨域、404、中文乱码跨域问题是最常见的第一道坎。开发环境里如果控制台报错出现Access-Control-Allow-Origin相关字样先检查前端 vue.config.js 的 proxy 是否生效再看后端是否写了跨域配置类。一般情况下开发环境用前端 proxy 就够了不需要后端再开 CORS两边同时开反而可能出现奇怪的重复头信息问题。前端页面刷新出现 404优先检查 Nginx 的 try_files 配置。中文乱码问题则需要三处排查MySQL 连接 URL 是否设置了characterEncodingutf8数据库连接的字符集是否是 utf8mb4前端 HTML 页面是否设置了 utf8 编码。想省事的话这三处在初始配置时就全部按 utf8mb4 设置基本能规避掉大多数乱码场景。6.2 数据库层面的隐蔽异常有一类异常特别容易误导人报错不会直接指出是数据库的问题但业务逻辑就是不对。比如创建订单失败日志显示主键冲突通常是订单号生成逻辑里随机数碰撞了或者时间戳精度不够。我经历过的方案是把订单号生成规则改成时间戳用户ID补位随机数碰撞概率基本可以忽略。还有一类是数据库连接池耗尽的报错页面会间歇性卡顿。排查时会发现写代码时某次查询没有关闭连接而在传统 JDBC 时代这个问题非常普遍。用 SpringBoot 连接池后这类问题很少但要注意如果项目里有自定义拦截器或者异步线程使用数据库连接一定要确认连接能正确释放。MySQL 8 和 MySQL 5.7 还有一个隐蔽差异MySQL 8 默认的密码认证插件是 caching_sha2_password而某些老版本的驱动或连接工具不认识这个认证方式。如果本地用 5.7 好好的换到云服务器的 MySQL 8.0 后一直报认证失败要先检查驱动版本是不是太老或者把用户认证方式切回 mysql_native_password。6.3 部署环境的几个经典坑服务器端口不通是最容易忽略的问题。程序明明启动了日志也正常但外网就是访问不了最后发现是云服务商的安全组规则限定了端口即使服务器本机防火墙放行了也不生效。改完安全组规则之后通常还会遇到另一个问题服务是用 TCP 端口监听的本地 curl 测试一下curl http://127.0.0.1:8080/api/product/list能通就说明程序本身没问题。磁盘空间不足也是部署阶段的一个隐形雷。前端不清理的话node_modules 动辄几百兆后端 Maven 本地仓库更是能积累到十几个 G。尤其是云服务器默认系统盘才几十 G 的场景部署到一半因为磁盘写满导致数据库挂掉是很尴尬的现场。我每次部署前都会习惯性看一眼df -h先把空间隐患排除掉。最后分享一个我做这类项目的心得技术栈可以朴素但流程必须完整。很多功能看起来简单真正实现起来才会碰到各种各样的问题而解决问题的过程才是这个项目最大的收获。这套前后端分离的小区购物系统从需求梳理到部署上线最大的价值不是用了多新的技术而是让你把 SpringBoot、Vue、MyBatis、MySQL 这一整套组合真正串了起来知道每个环节之间是怎么衔接的。代码能跑起来只是第一步能把整个项目讲清楚、部署出来、应对未来可能的功能扩展才算真正把这类系统吃透。