ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3明星周边电商系统:前后端分离架构与订单设计实践

SpringBoot+Vue3明星周边电商系统:前后端分离架构与订单设计实践 1. 星之语这个项目到底在做什么明星周边电商的真实业务拆解先聊点实际的。很多人看到明星周边产品销售网站第一反应是这不就是个普通商城吗商品管理、购物车、下单、支付四大件找个开源商城改改不就行了我当初也这么想真动手做星之语之后才发现明星周边这个垂直品类跟卖3C数码、卖服装完全是两套玩法。最核心的差异有三个第一商品形态极度非标准化。专辑、海报、手办、应援棒、小卡、舞台服装复刻每种周边的属性维度完全不同。手办有材质、高度、限定编号小卡有版本、拍摄场次、成员单人还是团体光一张专辑就能拆出普通版、特别版、签售版、平台特典版。你用名称价格库存这种通用字段去建表后台录入员能崩溃。第二预售和限时抢购是常态。明星周边不像普通商品那样稳定有货大部分是粉丝团购、预售、按销量分批出货。订单系统必须支持先付款后发货、尾款补款、批次发货这种电商里相对边缘的流程。第三围绕人的营销属性极强。普通商城按分类逛周边商城按明星逛。首页要有人气明星榜、新周边速递、粉丝热推专区商品详情页必须展示关联明星的全部周边。这直接决定了前端信息架构和数据库表结构的设计方向。回到星之语这个项目本身它是一个标准的前后端分离单体应用技术栈是SpringBoot Vue3 MyBatis MySQL。整个系统的业务边界可以拆成两条线前台用户端用户注册登录手机号验证码JWT鉴权明星专区浏览、商品按分类/关键词检索商品详情多图展示、规格选择、库存展示购物车管理、下单结算、订单状态跟踪个人中心收货地址、我的订单、我的收藏后台管理端明星管理绑定商品、维护热度排序商品管理多规格SKU、库存、上下架订单管理发货、退款、修改收货信息用户管理角色权限、账户状态轮播图/运营位管理、销售统计这套功能清单定下来整个项目的工作量其实已经超过80%的开源电商模板了。它不是应付毕业设计那种能跑就行的东西而是按真实上线标准来设计的。2. 为什么选SpringBoot Vue3 MyBatis MySQL这套组合技术选型这部分网上争论很多我直接说结论这套组合不是最潮的但它是当前中小型前后端分离项目里试错成本最低、最不容易把自己绕进去的组合。2.1 后端用SpringBoot而不是Spring MVC省掉的是配置心智SpringBoot的价值不在于功能上比SpringMVC多什么而在于它把让项目跑起来这件事的成本降到了最低。内置Tomcat、自动配置、starter机制让开发人员能把精力放到业务代码上而不是折腾web.xml、配置文件里那些繁琐的bean声明。我在一些用SSM框架的老项目里被虐过最崩溃的是环境搭建阶段就花了一整天Spring版本和Jackson版本冲突、MyBatis的mapper扫描路径配错、事务管理器忘了注入。SpringBoot通过统一版本管理BOM和自动配置把这些问题基本消灭了。比如引入spring-boot-starter-web后Jackson、Tomcat、参数校验这些基础组件全都自动装配好你只管写接口就行。星之语项目使用的是SpringBoot 2.7.x版本。这里有一个很实际的经验别一上来就追最新的SpringBoot 3.x。3.x底层是Jakarta EE规范很多老牌第三方库包括部分MyBatis增强插件、代码生成器还在适配期遇到问题网上的解决方案也多基于2.x。等3.x生态彻底成熟了再迁移也不迟。2.2 Vue3的价值是组合式API带来的组织效率Vue2时代一个复杂的商品详情页组件会变得很长data、methods、computed、watch混在一起不同功能的代码逻辑被拆散到各自的选项中。Vue3的组合式APIComposition API把同一个业务功能的所有逻辑收拢到一块儿可读性和复用性都好了很多。我在星之语项目里用setup语法糖编写组件最典型的例子是购物车逻辑把购物车的数据加载、增删改数量、选中状态、结算勾选抽成一个useCart()组合函数。页面组件里只需要一行const { cartList, updateCount, checkedItems } useCart()代码量肉眼可见地减少而且这个逻辑可以被购物车页、商品详情页、结算页共用。当然Vue3搭配Vite开发服务器的热更新体验比Vue2配Webpack快得不是一点半点秒级刷新在开发体验上优势很明显。2.3 MyBatis的半自动ORM特性写SQL不被框架绑架项目里没用MyBatis-Plus刻意保留了原生MyBatis。原因很简单这个系统的查询场景足够复杂明星周边网站的多条件动态查询非常多——按明星筛选、按品类筛选、按价格区间筛选、按销量排序、组合条件叠加这些场景下原生MyBatis的if动态SQL极其灵活你能精确控制每一条SQL的生成逻辑。MyBatis-Plus那种QueryWrapper链式查询确实省代码但在多表关联、子查询、复杂统计的场景里你会发现最终还是要手写SQL而且模板方法查出来的字段映射容易出坑。反过来原生MyBatis配合resultMap做结果映射任何查询都能精准控制。有一个体会很关键写业务查询前先用SQL直接在Navicat里验证确认无误后再搬进Mapper.xml。这样排错成本最低也容易发现SQL本身的性能问题。2.4 MySQL 5.7/8.0的选择没有悬念这个项目用的MySQL 5.7有一批人建议直接上8.0。我的看法是独立开发项目直接用8.0就好如果部署环境是老服务器担心兼容性5.7也完全够用。重点是创建连接串时必须显式指定serverTimezoneAsia/Shanghai否则Java连接MySQL时会出现时区报错。另外8.0默认的caching_sha2_password认证插件老版的mysql-connector-java驱动连不上记得使用com.mysql.cj.jdbc.Driver并且驱动版本要大于5.1.47。3. 后端架构与核心模块设计从分层到鉴权的落地细节3.1 包结构与分层逻辑星之语后端是单模块Maven项目包结构这样划分com.xingzhiyu ├── common │ ├── result统一返回结果Result、ResultCode枚举 │ ├── exception业务异常类、全局异常处理器 │ ├── utilsJwtUtil、MD5加密、日期工具 ├── config跨域配置、WebMvcConfig、MyBatis配置 ├── controller ├── service接口impl ├── mapper ├── entity ├── dto接收前端参数的专用对象 ├── vo返回前端数据的专用对象这里的核心思想是实体分层entity对应数据库表结构dto接收前端入参vo定义接口返回结构。一定不要混用。很多新手图省事前端传什么全用一个Map接收接口返回也直接丢实体类短期速度快后期维护简直是灾难——字段暴露安全隐患、文档无法自动生成、前端对接全靠猜。我在项目里连地址修改这种小接口都单独建了AddressDTO时间长了你会发现前期这点麻烦是值得的。3.2 统一返回结果与全局异常处理前后端分离项目接口返回格式必须统一否则前端Axios响应拦截器没法做统一处理。星之语项目的统一返回结构是这样设计的Data public class ResultT { private Integer code; // 200成功其他为失败 private String message; // 提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }与之配套的是全局异常处理器。我在ControllerAdvice里捕获三类异常业务异常手动抛出的提示给用户的友好信息、参数校验异常Validated触发的MethodArgumentNotValidException、兜底Exception记录日志并返回系统繁忙。这样做最大的好处是Controller层代码极其清爽业务逻辑里发现了问题直接throw new BizException(库存不足)不用每个方法写try-catch前端拿到的永远是结构化错误信息。3.3 JWT登录鉴权的完整链路星之语项目的用户鉴权用的是JWTJSON Web Token流程是登录成功 → 后端生成Token并返回 → 前端存入localStorage → 每次请求在请求头带Authorization: Bearer xxx→ 拦截器解析校验 → 放行或拒绝。关键实现点有三个第一个Token里存什么。我设计的是用户ID、用户名、角色user/admin、过期时间。一定不要存敏感信息手机号、密码因为JWT的Payload只是Base64编码任何人拿到Token都能解码看到内容。但这个Token本身是签名防篡改的。第二个拦截器怎么排除白名单。登录注册接口、首页轮播图、商品列表、商品详情这些不需要登录就能访问的接口需要放行。我的做法是在配置类里维护一个白名单list配合HandlerInterceptor使用。这里有个经验静态资源也要放行否则前端通过后端代理图片时会被拦截器拦下来。第三个解析Token后怎么获取用户信息。我在拦截器中解析出用户ID后用ThreadLocal存储当前登录用户对象在Controller的任意位置通过UserContext.getUserId()获取。请求结束后在afterCompletion中务必执行UserContext.clear()否则高并发下用户数据会串。3.4 MyBatis的配置与手写SQL的核心场景星之语项目在application.yml里MyBatis配置如下mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.xingzhiyu.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImplmap-underscore-to-camel-case: true这个配置强烈建议开启数据库字段create_time可以自动映射到实体类的createTime省掉一堆手动resultMap。最复杂的SQL集中在商品多条件检索上。明星周边的筛选条件经常是动态组合的——用户选了王一博、手办、价格300-500、按销量排序对应的Mapper就是多条件动态查询select idselectProductByCondition resultTypecom.xingzhiyu.vo.ProductVO SELECT p.id, p.name, p.main_image, p.price, p.sales_count, s.name AS star_name, c.name AS category_name FROM product p LEFT JOIN star s ON p.star_id s.id LEFT JOIN category c ON p.category_id c.id where if teststarId ! null AND p.star_id #{starId} /if if testcategoryId ! null AND p.category_id #{categoryId} /if if testminPrice ! null AND p.price gt; #{minPrice} /if if testmaxPrice ! null AND p.price lt; #{maxPrice} /if if testkeyword ! null and keyword ! AND (p.name LIKE CONCAT(%, #{keyword}, %) OR s.name LIKE CONCAT(%, #{keyword}, %)) /if /where if testsortType sales ORDER BY p.sales_count DESC /if if testsortType price_asc ORDER BY p.price ASC /if if testsortType newest ORDER BY p.create_time DESC /if /select这段SQL有两个细节要注意一是where标签会自动处理第一个条件前缀的AND不用手动where 11这种丑写法二是排序字段不能用#{}因为预编译占位符只适用于值所以排序走if判断防止SQL注入。分页方面项目使用的是PageHelper插件配置了reasonable: true这样页码越界时插件会自动修正不用自己写边界逻辑。4. 前端Vue3工程化实践从Vite构建到Axios封装的完整链路4.1 项目初始化的版本坑创建Vue3项目我建议用Vite而不是Vue CLI。Vite 2.x要求Node.js版本12.2.0但如果用Vite 4.x或5.xNode版本最好在16。我在开发星之语时用的是Vite 4 Vue 3.3 Element Plus 2.3整体生态稳定。一个实际的坑如果服务器上Node版本较老比如12.x执行npm install会出现大量依赖报错不要硬扛直接装个nvm进行多版本管理项目目录里维护好.nvmrc文件指明版本号。4.2 前端目录结构怎么组织星之语前端结构如下src ├── api按业务模块拆分的接口请求auth.js、product.js、cart.js、order.js ├── assets静态资源 ├── components通用组件GoodsCard、Pagination、StarAvatar等 ├── router路由配置路由守卫 ├── storesPinia状态userStore、cartStore ├── utilsrequest.js、auth.js ├── views页面级组件 │ ├── home首页 │ ├── product商品列表/详情 │ ├── cart │ ├── order │ ├── user个人中心 │ └── admin后台管理 ├── App.vue ├── main.js4.3 Axios封装与请求拦截器前后端分离项目里Axios封装是第一道基础设施。我在utils/request.js里做了这三件事第一统一baseURL和环境区分。开发环境用Vite代理转发到后端8080端口生产环境通过环境变量VITE_API_BASE_URL配置避免把接口地址写死在代码里。第二请求拦截器注入Token。从localStorage读取Token如果存在就设置请求头Authorization: Bearer ${token}。第三响应拦截器统一处理返回。约定后端返回结构为{code, message, data}code200直接返回data给业务代码code401表示Token过期清空本地用户信息并跳转登录页其他code统一ElMessage.error(message)提示。登录接口的调用因此变得非常清爽const login async (loginForm) { const res await api.post(/auth/login, loginForm) userStore.setToken(res.token) userStore.setUserInfo(res.userInfo) router.push(/) }4.4 路由守卫控制页面访问权限星之语项目有普通用户和管理员两种角色路由守卫需要做双重判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) return } if (to.meta.requiresAdmin) { const role localStorage.getItem(role) if (role ! admin) { next(/) return } } next() })meta.requiresMeta标记需要登录的路由meta.requiresAdmin标记管理端。这里我踩过的一个坑是刷新页面时Pinia的store会被清空而localStorage还留着用户信息所以刷新后需要重新从localStorage恢复用户状态否则路由守卫会误判登录态。4.5 购物车与订单的状态流转设计购物车模块是前端里最容易写乱的部分。我设计的状态流转是加入购物车时只把商品ID、规格、数量、选中状态存在前端用Pinia localStorage持久化真正下单时把购物车选中项一次性提交到后端生成订单。这种设计的好处是购物车本身不需要后端表减轻了服务端压力而且用户未登录状态下也能加购物车登录后依然保留。等用户提交订单的时候后端再校验库存、计算金额、生成订单记录。5. 数据库设计明星周边商城的数据模型到底该怎么建模5.1 核心表结构拆解星之语项目的数据库一共12张表我把最核心的几张讲清楚。用户表userCREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名/手机号, password varchar(255) NOT NULL COMMENT MD5加密密码, nickname varchar(50) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role tinyint(1) NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1正常 0封禁, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码加密用的是MD5加盐方案即MD5(password 固定盐值)。我知道有人会说BCrypt更安全但中小型项目MD5加盐已经够用重点是绝不能明文存密码。商品表productCREATE TABLE product ( id int(11) NOT NULL AUTO_INCREMENT, star_id int(11) NOT NULL COMMENT 关联明星ID, category_id int(11) NOT NULL COMMENT 关联分类ID, name varchar(100) NOT NULL, subtitle varchar(200) DEFAULT NULL COMMENT 副标题, main_image varchar(255) NOT NULL COMMENT 主图, detail text COMMENT 富文本详情, price decimal(10,2) NOT NULL COMMENT 售价, original_price decimal(10,2) DEFAULT NULL COMMENT 原价, stock int(11) NOT NULL DEFAULT 0, sales_count int(11) NOT NULL DEFAULT 0, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_star_id (star_id), KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意两个设计一是金额用decimal(10,2)不用double或float浮点数在计算时会产生精度误差这在订单金额计算里是致命的二是对star_id和category_id建了索引因为首页和列表页的查询都是按明星和分类过滤的。商品规格表product_sku明星周边商品的规格很杂我在星之语里设计了独立的SKU表CREATE TABLE product_sku ( id int(11) NOT NULL AUTO_INCREMENT, product_id int(11) NOT NULL, spec_info varchar(255) NOT NULL COMMENT 规格描述如普通版/单张海报, price decimal(10,2) NOT NULL, stock int(11) NOT NULL DEFAULT 0, sku_image varchar(255) DEFAULT NULL COMMENT 规格专属图片, PRIMARY KEY (id), KEY idx_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;商品详情页选择不同规格时实时切换价格和库存前端数据都由这个SKU表提供。订单主表order与订单明细表order_item订单采用主表明细表的经典设计。主表只存订单编号、用户ID、总金额、状态、收货信息快照明细表存商品ID、SKU、单价、数量、小计金额。这里有一个很多人忽略的点收货地址要在订单表里做快照。因为用户下单后可能修改默认地址如果订单查询时去读最新的用户地址历史订单信息会被篡改。我把收货人姓名、电话、地址直接冗余进订单表订单就变成了不可变记录。订单状态我用整数类型存储0待付款 1待发货 2已发货 3已完成 4已取消后端用状态机约束流转比如只有0待付款状态才能取消只有1待发货才能发货。明星表star与分类表category明星表和分类表是周边商城区别于普通商城的设计核心。明星表有name、avatar、fans_count热度用于首页排序、description。分类表做了parent_id自关联支持专辑-普通版-特别版这种多级分类。这种以明星为第一入口以分类为第二入口的信息架构体现在首页设计上就是明星排行分类导航两个模块平级展示。5.2 购物车为什么不建表前面提到购物车数据存在前端数据库里没有购物车表。很多人会质疑这一点我的理由很实在周边电商的购物车只是临时存储用户未必会结算存数据库纯属浪费空间前端用Pinia localStorage持久化体验更流畅加购不经过网络请求真正需要落库的信息下单商品、数量在订单明细表里完整保留但是前端localStorage存购物车有个边界条件用户分页刷新时Token过期会导致强退购物车数据不会丢localStorage但会失去登录状态下单时需要重新登录购物车数据不会丢localStorage但会失去登录状态下单时需要重新登录。这个体验可以在购物车结算按钮上提示用户登录后结算再做一次引导即可。6. 前后端联调与部署中的真实踩坑记录6.1 跨域问题不是配一次就能一劳永逸前后端分离项目最经典的坑就是跨域。我在星之语项目里开发环境采用Vite代理解决// vite.config.js server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }开发环境建议用代理而不是在后端开启全放行的CORS配置。原因有两点一是代理对前端代码零侵入接口路径保持/api/xxx二是生产环境一旦忘了关闭后端的CORS全放行安全风险很大。但生产环境还是要考虑后端的CORS配置。我的做法是在后端配置类里addCorsMappings只放行指定的前端域名来源并限定allowedMethods为GET,POST,PUT,DELETE,OPTIONS。6.2 图片上传与静态资源映射明星周边网站图片量大商品主图、规格图、轮播图、用户头像。开发阶段我把图片存在本地服务器目录通过一个上传接口接收MultipartFile落到D:/upload/目录然后配置了静态资源映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourcePaths(file:D:/upload/); }这样图片访问路径就是http://localhost:8080/upload/xxx.jpg数据库里只存相对路径即可。这个方案有一个隐患服务器重启后如果上传目录配置错误图片会404。所以建议把上传路径抽到配置项里部署时改配置文件就行。真要上线建议把图片迁到云OSS/COS配合CDN加速本地存储方案只适合开发和内网环境。6.3 Nginx部署配置与history路由404问题Vue3项目打包后是纯静态文件部署到Nginx时最大的坑是前端路由用history模式刷新页面会404。因为前端路由是JS虚拟的刷新时浏览器直接请求了/product/detail/123但服务器上根本没这个物理路径。Nginx配置需要加上try_filesserver { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; } location /upload/ { proxy_pass http://localhost:8080/upload/; } }这套配置同时处理了三件事前端history路由回退到index.html、后端接口转发、图片资源转发。其中location /api/的proxy_pass末尾要带/表示把请求原样转发给后端否则会丢掉/api前缀导致404。6.4 MySQL连接串里的两个经典参数第一次把后端部署到服务器时如果Java连不上MySQL九成是这两个问题第一时区问题。报错信息类似The server time zone value Öйú±ê׼ʱ¼ä is unrecognized解决办法是在连接串上加serverTimezoneAsia/Shanghai。第二SSL警告。MySQL 8默认开启SSL但本地测试环境没有配置证书连接时会报SSL connection error或大量警告日志。解决办法是连接串加useSSLfalse。注意这里如果数据库配置了SSL证书强制关掉会影响安全所以更推荐的写法是useSSLfalseallowPublicKeyRetrievaltrue后者解决8.0版本的公钥检索认证问题。下面是我项目里最终使用的连接串可以直接抄jdbc:mysql://localhost:3306/xingzhiyu?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue6.5 金额精度与库存超卖的隐性坑订单金额计算如果全用Java的double或float总金额会出现类似199.99999999的值。我在后端计算金额时统一用BigDecimal加法用add乘法用multiply并且divide时指定精度和舍入模式。库存扣减是高并发的另一个雷区。我的做法是更新时加条件判断UPDATE product SET stock stock - 1, sales_count sales_count 1 WHERE id #{productId} AND stock 0;stock 0这个条件保证不会超卖。同时检测受影响行数如果为0说明库存不足抛出手速太快啦库存不足的业务异常。这就是典型的乐观锁思想不用分布式锁也能扛住常规的并发压力。7. 从能跑到好用这个系统的进级优化方向如果只是复现星之语当前的功能和架构已经完整但要真正运营起来有几个明显的优化方向值得投入。第一个方向是热搜榜和推荐逻辑。当前首页明星热度是人工维护的fans_count字段实际上可以基于搜索关键词频率、商品收藏量、订单销量做一个热度分值用定时任务每天凌晨计算一次让榜单更真实。第二个方向是Redis缓存热点数据。商品详情页是访问最密集的接口每次都查MySQL虽然能扛住但浪费资源。可以把商品详情、明星列表、首页轮播这些读多写少的数据缓存到Redis设置合理的过期时间接口响应能从200ms降到20ms级别。注意缓存更新策略要处理商品修改后删除对应缓存即可。第三个方向是订单超时自动取消。当前待付款订单需要用户手动取消真实运营场景下加一个定时任务扫描超过30分钟未付款的订单自动置为取消状态并回滚库存。这个功能用Spring的Scheduled就能实现不需要引入消息队列那么重的组件。第四个方向是前后端分离之后的文档管理。接口多了以后前后端对接全靠口头沟通效率极低。可以引入SpringDocOpenAPI 3生成接口文档配合Knife4j的界面后端写完接口自动生成文档前端直接照着对接省掉反复询问字段含义的沟通成本。我个人在实际操作中体会最深的一点是这个系统的代码量并不算大真正花了最多时间的是字段设计和状态流转。明星周边这种强垂直领域的电商业务规则比技术实现更值得花心思。如果你拿这套源码去学习建议不要急着跑通功能而是先把数据库表结构、订单状态机、前后端数据流这三条主线理清楚再动手去改需求收获会大很多。最后再分享一个小技巧本地开发时后端接口地址和前端代理配置尽量做成环境变量驱动不要在每个文件里硬编码。把VITE_API_BASE_URL、server.port、upload.path这类配置全部集中管理部署到不同环境时只改一份配置能少掉不少头发。
返回列表