SpringBoot+Vue电商商城系统实战:前后端分离与部署全解析

发布时间:2026/9/9 15:14:17

SpringBoot+Vue电商商城系统实战:前后端分离与部署全解析
做电商类项目的朋友应该都有同感SpringBoot Vue 几乎成了国内电商商城系统的默认技术栈。我在开源社区存过一套这样的完整源码——基于SpringBoot与Vue开发的电商商城系统前后端分离、带管理后台、含完整源码。这套东西最大的价值在于你可以把它当作一个可运行的业务蓝本从用户注册登录、商品浏览、购物车、下单支付到后台的商品管理、订单处理、数据统计一整条电商核心链路都完整跑通。它特别适合三类人准备做毕业设计的学生、想快速搭一套自营商城的中小团队、以及想搞懂“前后端分离项目到底怎么串起来”的进阶开发者。但别被“源码”两个字误导。拿到源码能启动和真正理解这套系统中间差着好几个坑。我之前为了把这套项目从本地跑到云端服务器前后折腾了大半个月中间踩过跨域、文件上传、路由刷新404、时间序列化这类非常典型的问题。这篇文章不打算重复README里的启动步骤而是想从架构设计、后端核心实现、前端工程化、前后端联调、环境搭建、部署上线这几个维度把一套SpringBoot Vue电商商城的完整技术链路讲透。你跟着走一遍获得的是一套真正能落地的电商系统而不只是一堆能运行的代码。1. 架构边界与模块划分这套商城系统到底包含什么1.1 为什么选前后端分离而不是SpringBoot模板渲染很多做单体项目的老同学会问SpringBoot自带Thymeleaf为什么还要前后端分离额外搭一套Vue工程这个问题的答案等你把项目部署上线后就彻底清楚了。前后端分离带来的第一个好处是开发并行。前端不用等后端完整接口出来只要前后端先定好接口文档——路径、入参、返回结构这三件事对齐——前端就可以用mock数据先搭页面。我实际体验下来一个商城的前端页面量不少首页、列表页、详情页、购物车、结算、订单中心、后台十几个管理界面如果等后端全部写完再动前端整个周期至少要拉长一倍。第二个好处是部署解耦。前端构建后就是一堆静态文件丢Nginx就行后端是独立Java进程任意一个更新不影响另一个。改个页面样式不用重新打jar包重启服务这在生产环境里是很爽的事情。第三个好处是多端复用。我最初只用它做了Web商城后来想加小程序端时后端接口几乎直接复用只需要新增一套小程序前端。你要是用过Thymeleaf模板渲染就明白那套东西想再输出给小程序基本等于重写。当然前后端分离也有代价跨域问题、鉴权方式变化、部署链路变长。这些问题不是没有解决方案只是需要你在每个环节多留个心眼。后面几章我会逐个讲。1.2 用户端与后台管理的功能清单这套商城系统从用户视角和管理员视角大致可以分为下面这些功能模块端模块核心功能用户端账号体系注册、登录、JWT鉴权、个人信息查看用户端商品浏览分类导航、商品列表分页、关键词搜索、商品详情用户端购物车加入购物车、修改数量、选中/取消选中、删除用户端订单结算提交订单、收货地址填写、在线支付可用模拟支付用户端订单中心订单列表、订单详情、取消订单、确认收货管理端仪表盘商品数量、订单数量、销售额统计管理端商品管理商品新增/编辑/上下架、库存管理、图片上传管理端分类管理商品分类的树形维护管理端订单管理订单列表、发货操作、退款处理入口管理端用户管理用户列表、禁用/启用账号这基本是一个标准的B2C商城最小可用集。你可以在这个基础上加优惠券、积分、秒杀、物流跟踪但核心链路就是上面这些。做毕业设计或者接外包项目时把这张表里的功能稳稳落地交付质量就已经超过大多数同类项目了。1.3 数据库设计核心表与关键字段数据库设计是这个项目最容易被低估的部分。很多初学者上来就建一张商品表、一张订单表越做到后面越别扭。我按这套系统的实际落地情况把核心表结构梳理一下。表名用途关键字段user用户表id, username, password(BCrypt密文), nickname, avatar, phone, statuscategory商品分类表id, name, parent_id, sortproduct商品表id, category_id, title, cover, images, price, stock, sales, status, descriptionproduct_sku商品规格表id, product_id, spec_name, price, stockcart_item购物车表id, user_id, product_id, quantity, selectedorders订单表order_no, user_id, total_amount, status, address, created_time, pay_timeorder_item订单明细表id, order_id, product_id, product_name, price, quantitypayment支付流水表id, order_no, pay_type, pay_status, callback_data先说两个比较重要的设计决策。第一个是product和product_sku为什么分开。商品本身的信息标题、封面、描述和具体规格颜色、尺码、版本是两种粒度。一个“手机壳”商品可能同时有黑色和白色两个SKU它们的价格和库存是独立的。如果不拆表库存控制就没法定到规格级别卖完黑色你没法精确下架。当然了如果你的商城卖的是无规格商品SKU表可以先不做直接在product表上冗余一个spec字段也行看业务需要。第二个是订单为什么要有order_item明细表而且要在里面冗余product_name和price。原因是商品信息会被修改、下架甚至删除但历史订单必须显示用户下单那一刻的商品快照。如果你下单后在订单表里只存product_id等商品改名了订单里的商品名也跟着变了用户投诉起来非常麻烦。还有一点值得注意订单号用单独的order_no不要用自增id。订单号要暴露给支付回调、物流查询这些外部系统自增id一方面会让别人通过订单号推断你的销量另一方面自增id在回调校验时也不安全容易被遍历猜测。实际项目里我习惯用时间戳加随机数拼一个唯一流水号或者用雪花算法生成。2. SpringBoot后端的核心实现从接口设计到安全认证2.1 项目结构、依赖选型与版本搭配的坑后端项目的包结构大致是这样com.example.mall ├── MallApplication.java ├── config // 跨域、MyBatisPlus分页、拦截器注册 ├── controller // 前台接口 后台管理接口 ├── service // 业务逻辑接口 ├── service.impl // 业务实现 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto / vo // 入参对象 / 返回对象 └── utils // JwtUtils, ResultResponse, 全局异常处理核心依赖我用的是下面这组spring-boot-starter-web基础Web能力mybatis-plus-boot-starterORM增强mysql-connector-java数据库驱动lombok消除样板代码jjwtJWT生成与解析spring-boot-starter-validation参数校验hutool工具库避免重复造轮子这里要重点说一个版本问题MyBatis-Plus 3.5.x 和 SpringBoot 2.7.x 是目前最稳定的搭档。如果你手痒用了SpringBoot 3.x那MyBatis-Plus必须上3.5.3而且javax包要换成jakarta包很多老教程的代码会在这里直接编译报错。说实话对一个商城系统来说SpringBoot 2.7 JDK 8足够用了没必要为了追新给自己挖坑。2.2 JWT登录认证从生成Token到拦截器放行规则登录认证我用的方案是JWT。流程不复杂用户提交用户名密码后端校验账号密码密码用BCrypt比对不要明文存储校验通过后生成JWT token返回给前端前端把token存到localStorage或Pinia请求时在请求头加Authorization: Bearer token后端拦截器解析token把用户信息放入ThreadLocal供后续业务直接取JWT工具类核心代码大概长这样Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; // 单位秒 public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire * 1000)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }拦截器里最容易被忽略的就是预检请求放行。前端跨域请求时浏览器会先发一个OPTIONS预检请求如果拦截器把OPTIONS也拦了前端就会看到CORS报错但后端日志里什么都没有。public class TokenInterceptor 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 (StringUtils.isNotBlank(token) token.startsWith(Bearer )) { try { Claims claims jwtUtils.parseToken(token.substring(7)); request.setAttribute(userId, claims.getSubject()); return true; } catch (Exception e) { // token 过期或非法直接走401 } } response.setStatus(401); return false; } }注册拦截器时白名单一定要配好。登录接口、注册接口、商品列表、商品详情、分类查询这些公开接口必须放行否则会出现死循环用户没登录想访问商品页被拦截器踢到登录页但登录接口本身又被拦截器拦住了永远无法登录。生产环境我建议后台管理接口独立一套拦截规则或者在校验token成功后额外校验一下当前用户的role字段是不是admin。这个校验后端必须做死不能依赖前端隐藏按钮。2.3 商品、购物车模块的接口设计实践商品列表接口我设计成GET /api/product/list?categoryId1keyword手机pageNum1pageSize10sortpriceAsc返回分页结果。查询逻辑用MyBatis-Plus的LambdaQueryWrapper写起来非常直观不需要写一堆XML SQL。public PageResultProductVO getProductPage(Integer categoryId, String keyword, Integer pageNum, Integer pageSize, String sort) { PageProduct page new Page(pageNum, pageSize); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Product::getCategoryId, categoryId) .and(StringUtils.isNotBlank(keyword), w - w.like(Product::getTitle, keyword) .or().like(Product::getDescription, keyword)); if (priceAsc.equals(sort)) { wrapper.orderByAsc(Product::getPrice); } else if (saleDesc.equals(sort)) { wrapper.orderByDesc(Product::getSales); } productMapper.selectPage(page, wrapper); return new PageResult(page.getRecords(), page.getTotal(), pageNum, pageSize); }购物车接口这块我的设计思路是以后端存储为准。虽然把购物车放在前端localStorage也能做但用户换个设备购物车就空了体验不好。购物车加到数据库很简单就是一张带user_id的表查询、修改、删除都按user_id过滤。购物车的数量修改接口前端传id和quantity后端更新对应记录。加购接口的核心逻辑是先查同一用户是否已经把该商品加进购物车如果存在就数量累加不存在就插入一条新记录。很多初学者直接无脑insert买一件商品加一次购物车记录购物车里同一个商品出现好几行后面结算逻辑会写得非常痛苦。2.4 订单状态机与库存扣减的原子性订单状态这一块我把它设计成一张明确的状态流转表状态值含义可进入的下一步0待付款141待发货252待收货353已完成-4已取消-5退款/售后中3提交订单的接口是整个后端最需要谨慎的地方因为它涉及多步数据变更校验商品状态和库存、扣减库存、生成订单主表、生成订单明细、清空购物车里已勾选的商品。任何一步失败前面所有操作都必须回滚所以这个接口必须加Transactional注解。扣减库存这一步有一个很关键的经验使用带条件的UPDATE语句而不是先SELECT再UPDATE。Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { // 要扣的库存使用原子性SQL避免并发超卖 for (OrderItemDTO item : dto.getItems()) { int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new BizException(商品库存不足); } } // 生成订单主表 // 生成订单明细 // 清空购物车中选中的商品 return order.getId(); }deductStock对应的SQL是UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这个SQL的关键在于用stock #{quantity}作为条件MySQL的UPDATE语句在行锁层面天然具备原子性只有库存充足时才会更新成功并返回受影响行数。如果你用先查再改的方式两个并发请求同时查到库存还剩1件都认为可以买最后库存就会变成负数——这就是超卖。我见过不少初学者在这里踩坑等到上线后才发现问题。关于支付回调先说明一点很多毕业设计级的商城为了演示方便会做成“一键模拟支付”点支付按钮直接把订单状态改成已支付。这当然可以但如果你要对接支付宝或微信支付回调处理的核心逻辑一定要搞清楚支付平台会异步POST一个回调通知里面带订单号、金额、签名等信息。后端第一件事是验签防止伪造回调第二件事是根据订单号查订单确认当前状态确实是待支付第三件事是更新订单状态为已支付写支付流水最后返回一个固定字符串比如success给支付平台否则支付平台会认为回调失败并持续重试。3. Vue前端的工程化搭建与页面核心逻辑3.1 为什么用Vue 3 Vite Pinia Element Plus前端部分的技术选型我推荐Vue 3 Vite Pinia Element Plus的组合。Vue 3的组合式API配合script setup语法同一个页面的逻辑代码组织起来比Vue 2的Options API清爽很多尤其是商品详情页那种包含图片轮播、SKU选择、数量加减、加入购物车多个交互的页面Composition API的优势会非常明显。Vite相比Webpack最大的感受就是“快”。冷启动秒级热更新几乎无感开发体验完全不是一回事。如果你拿到的这套源码还是Webpack可以考虑迁移到Vite但注意Vite 5要求Node 18装依赖时容易因为Node版本太低或太高出现问题建议直接用nvm管理Node版本。Pinia是Vuex的替代品API比Vuex简单太多。不需要mutations那一套直接在store里定义state、getters、actions就行TypeScript支持也好。版本搭配方面给一张表避免新人卡在环境上依赖建议版本备注Node.js16.20 或 18.xVite 5要求更高建议直接用Node 18Vue^3.3.xVite^4.x 或 ^5.x别盲目上最新依赖生态要跟上Pinia^2.xVue Router^4.xElement Plus^2.xAxios^1.xnpm install时最常遇到的问题是peerDependencies冲突我实测最有效的解决办法是npm install --legacy-peer-deps这个命令表示跳过peer依赖的严格检查。虽然不优雅但确实能解决大多数Vue生态的依赖冲突问题。3.2 路由设计、导航守卫与后端权限的关系前端路由分两套Layout用户端路由挂在根路径/下走一套带顶部导航和底部栏的Layout管理端路由挂在/admin下走一套带侧边栏菜单的AdminLayout。这种结构在Vue Router里的实现就是嵌套路由主Layout里放router-view子路由根据访问路径动态渲染页面。导航守卫是前端权限控制的核心入口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.role admin !isAdmin(token)) { next({ path: /403 }) return } next() })登录后跳回原先想访问的页面这个redirect参数是很多新手的盲区。如果不处理用户会被强制回到首页体验很差。但这里必须强调一句扎心的话前端路由守卫只是用户体验层的门禁不是安全边界。真正拦住非法访问的是后端接口的权限校验。前端隐藏了“商品删除”按钮不代表用户不能直接通过POST请求调用删除接口。所以后端每个管理接口都必须校验当前用户角色前端路由守卫只是让正常用户看不见入口而已。3.3 购物车与订单页的状态管理Pinia怎么用才顺手购物车数据到底放Pinia还是每次进页面都从后端拉我的实践是进购物车页面时调后端接口拿最新的购物车列表拿到后放到Pinia里用户改数量、切换选中、删除时同时更新Pinia里的本地数据和后端。这样页面跳转回来不用反复请求数据也不会丢。一个简单的cartStore示例export const useCartStore defineStore(cart, { state: () ({ items: [], selectedIds: [], }), getters: { selectedItems: (state) state.items.filter(i state.selectedIds.includes(i.id)), totalPrice: (state) state.items .filter(i state.selectedIds.includes(i.id)) .reduce((sum, i) sum i.price * i.quantity, 0), }, actions: { async fetchCart() { const { data } await axios.get(/api/cart/list) this.items data.data this.selectedIds this.items.filter(i i.selected).map(i i.id) }, async updateQuantity(id, quantity) { // 本地立即更新同时异步同步后端 const item this.items.find(i i.id id) if (item) item.quantity quantity await axios.put(/api/cart/update, { id, quantity }) }, }, })订单结算页获取的“总价”一定要以后端计算为准不要直接信任前端提交的金额。前端传的total_amount在请求过程中可以被篡改后端在下单接口里必须根据订单明细重新计算一遍金额。这是电商系统的基本素养很多做管理系统的同学转过来做电商时容易忽略。3.4 axios封装统一处理Token、状态码与401跳转axios封装是我在任何前后端分离项目里都会第一个做的事。统一封装的request实例可以解决三个问题请求头自动带token、响应状态码统一处理、401时自动踢回登录页。const service axios.create({ baseURL: /api, timeout: 10000, }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这里有一个容易被忽略的细节baseURL直接写成/api而不是写死http://localhost:8080。开发环境下交给Vite的proxy代理转发到后端生产环境下交给Nginx反向代理转发到后端。这样前端代码里完全不用区分环境也不会有跨域问题。等你上线之后就会感谢这个设计。4. 前后端联调过程中的高频坑点4.1 跨域问题的完整排查链路跨域可能是联调阶段遇到最多的报错。典型现象是前端启动后访问登录接口浏览器控制台报错Access to XMLHttpRequest at http://localhost:8080/api/user/login from origin http://localhost:5173 has been blocked by CORS policy我建议新手遇到这个报错按下面这条链路排查打开浏览器Network面板看请求是否真的发出、状态码是什么。如果请求在预检阶段就被浏览器拦截后端甚至可能根本没收到请求后端日志也不会有任何记录。确认跨域根源前端地址是5173端口后端是8080端口origin不一致浏览器判定为跨域。检查后端有没有处理OPTIONS预检请求。跨域时浏览器会先发一个OPTIONS请求来探测后端是否允许该跨域请求如果后端没有正确响应OPTIONS后续真正的GET/POST会被浏览器强制拦下。检查拦截器是否放行了OPTIONS。回头看看第2.2节的TokenInterceptor我在里面特意加的OPTIONS放行就是为这个场景准备的。解决方案有两个二选一即可不要叠加使用方案一是后端统一配置CORS过滤器Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }方案二是前端用Vite的proxy代理让浏览器认为所有请求都在同一个源下server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, },生产环境的跨域问题交给Nginx反向代理解决本质上和方案二一样都是在中间层做转发。4.2 文件上传图片回显的路径统一问题商城系统里商品图片上传是标配功能但几乎每个第一次做这个功能的人都会在“图片能上传成功但页面里显示不出来”上耗掉半天。原因很简单后端把文件保存到了本地某个磁盘目录数据库里存的路径前端根本访问不到。正确的做法是后端把文件保存到固定的本地目录比如D:/mall-upload/数据库里存一个相对路径比如/uploads/商品图片.jpg然后通过静态资源映射接口让这个路径可访问。后端需要加这样一段配置Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadDir /); }这里有三个实践要点第一uploadDir不要用硬编码的绝对路径要用配置文件控制。Windows开发机和Linux服务器路径分隔符不一样硬编码绝对路径会处处碰壁。第二建议把上传目录放到项目目录之外比如服务器的/home/ubuntu/mall-upload避免打包时路径失效也方便备份。第三部署时Nginx也要把/uploads/路径代理到后端服务或者直接把静态文件目录映射出来否则就会出现“后端管理页面上传成功但商城前台图片加载失败”这种诡异现象。4.3 JSON时间格式化前后端各显示各的这个坑真的非常隐蔽。Java 8之后的时间类型LocalDateTime在默认Jackson序列化下返回给前端的格式可能是2025-01-01T12:00:00也可能是一串数字时间戳看Jackson版本和配置。前端拿到这种数据直接渲染用户看到的就是“2025-01-01T12:00:00”这种莫名其妙的东西。我建议统一在后端解决时间格式问题前端不用每个页面单独处理Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - builder .serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))) .deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); } }前端展示时用dayjs做格式化双保险。但无论如何后端输出统一格式是第一原则否则十个前端页面就有十种时间处理方式联调时你会疯掉。4.4 分页参数与返回结构不一致MyBatis-Plus的Page对象默认使用current和size作为分页参数分页结果里的字段是records和total。但Element Plus的el-pagination组件默认发的是pageNum和pageSize。如果前后端没有约定好就会出现一种很尴尬的情况第一页数据正常点击第二页没反应或者分页总页数不对。解决思路是前后端约定一套统一的分页参数结构然后固定下来。后端业务代码里再做一次映射把Page对象转成统一PageResult避免前端直接面对ORM框架的数据结构。另外一个必须留意的坑MyBatis-Plus的分页插件如果不注册selectPage方法并不会真正分页而是查出所有数据后内存里截取。所以MyBatis-Plus的配置类一定要记得加Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个问题最典型的症状是接口响应时间很长返回的数据却只有一页。排查时先看SQL日志如果发现SQL没有LIMIT语句基本就是分页插件没生效。5. 环境准备与本地启动拿到源码后复现的最小步骤5.1 本地环境清单和版本搭配建议本地跑起来的步骤不难但版本搭配非常关键。下面这套组合是我实测最稳的组件推荐版本说明JDK8 或 11企业老项目大量在用兼容性最好Maven3.6IDEA自带即可MySQL5.7 或 8.08.0更常见sql脚本基本通用Redis任意如果项目只用做缓存没有也不影响核心链路Node.js16.20 或 18.xVite 5要求更高开发工具IDEA VS Code后端IDEA前端VS Code或WebStorm再次强调SpringBoot版本问题。如果你的源码是SpringBoot 2.7.x直接用JDK 8没问题如果是SpringBoot 3.xJDK必须升到17而且依赖包名要从javax改成jakarta否则项目一启动就报ClassNotFoundException。5.2 数据库初始化拿到源码后先找sql目录下的脚本文件通常叫mall.sql或init.sql。在MySQL里执行mysql -u root -p mall.sql或者用Navicat、DataGrip直接导入脚本。执行完后打开后端的application.yml修改数据库连接信息spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你自己的密码serverTimezoneAsia/Shanghai必须加上否则IDEA启动时会报时区错误。这是国内开发MySQLJava项目最高频的启动报错之一没有例外。5.3 后端启动步骤用IDEA导入后端项目等待Maven下载依赖然后运行MallApplication主类。启动成功后浏览器访问http://localhost:8080/api/product/list如果返回JSON数据或401错误码说明后端已经正常起来。如果项目集成了knife4j或swagger也可以直接访问http://localhost:8080/doc.html查看所有接口文档这个页面在联调阶段非常有用可以快速验证每个接口的入参和返回。5.4 前端启动步骤进入前端项目目录一般是mall-vue或frontend执行npm install npm run dev如果npm install报错用--legacy-peer-deps重试。启动成功后浏览器访问http://localhost:5173用管理账号登录完整走一遍“浏览商品→加购→下单→支付→后台发货→确认收货”的流程。这一步我建议你多走几遍每走一遍注意看网络请求的调用顺序。你会很直观地理解一件商品从点击购买到订单完成的每一步前端调用了哪个接口、后端返回了什么结构、数据库里哪些表发生了变化。这个自动化流程走通本地开发环境就算是彻底搞定了。6. 部署上线从开发机到服务器的完整链路6.1 后端打包与配置外部化本地开发跑通只是开始把项目部署到服务器才是真正的考验。后端打包可以在项目根目录执行mvn clean package -DskipTests执行完target目录下会生成一个可执行的jar包。生产环境不要直接使用application.yml里的默认配置而是通过application-prod.yml覆盖数据库地址、上传目录、JWT密钥等敏感配置。启动时指定profilejava -jar mall-server.jar --spring.profiles.activeprod两个建议数据库密码不要以明文写死在yml里用环境变量注入比如password: ${DB_PASSWORD}JWT的secret也要换掉默认值否则攻击者可以用默认密钥伪造token。这些都是上线前的基本卫生习惯。6.2 前端构建与Nginx配置前端执行npm run build生成dist目录里面是纯静态文件。把dist里的内容上传到服务器的/var/www/mall目录然后配置Nginxserver { listen 80; server_name yourdomain.com; root /var/www/mall; index index.html; # history 路由刷新 404 问题的核心配置 location / { try_files $uri $uri/ /index.html; } # API 反向代理到后端 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 /uploads/ { proxy_pass http://127.0.0.1:8080; } }try_files $uri $uri/ /index.html;这一行是Vue Router history模式部署最关键的配置。它的作用是当用户访问/product/detail/123这样的路径时Nginx在静态目录里找不到这个文件于是兜底返回index.html由前端路由接管渲染。没有这一行刷新页面就会看到404。6.3 Docker Compose一键编排如果服务器环境比较混乱或者你需要频繁搬家、重装建议直接用Docker Compose编排。这样MySQL、后端、前端三个服务可以一键启动非常省事。后端DockerfileFROM openjdk:8-jdk-alpine WORKDIR /app COPY mall-server.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, -Dspring.profiles.activeprod, app.jar]前端Dockerfile用了多阶段构建先构建再拷贝到Nginx镜像FROM node:18-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install --legacy-peer-deps COPY . . RUN npm run build FROM nginx:alpine COPY --frombuild /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80docker-compose.ymlversion: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: mall volumes: - mysql-data:/var/lib/mysql - ./sql:/docker-entrypoint-initdb.d ports: - 3306:3306 backend: build: ./backend environment: DB_HOST: mysql DB_PASSWORD: root123456 depends_on: - mysql ports: - 8080:8080 frontend: build: ./frontend depends_on: - backend ports: - 80:80 volumes: mysql-data:这里有一个Docker部署特有的坑必须提醒容器内部的后端连接数据库不能再写localhost或127.0.0.1要写服务名mysql。因为每个容器有独立的网络命名空间localhost指向的是容器自己。初学Docker的人几乎都会在这里卡一下等看到Connection refused报错时才意识到是网络地址的问题。这套系统最核心的价值不是某个炫技功能而是完整串起了一条“数据怎么流转”的链路。从用户注册时密码的BCrypt加密到商品扣库存时的原子SQL再到订单状态机的每一步流转每一环都需要前后端配合才能跑通。我个人的建议是拿到源码后不要急着改代码先把本地环境跑起来用测试账号完整走几遍主流程再去看代码里对应的实现理解效率会高很多。最后分享一个小习惯给订单表加一个订单日志字段或单独一张订单状态记录表每次状态变更都记录下来。刚开始做商城时可以不加但上线后排查问题时你能直接看到订单什么时候从待支付变成了已支付、谁在什么时候取消了订单。这个小小的设计会在关键时刻帮你省下大把排查问题的时间。

相关新闻

OCRmyPDF 插件开发实战指南:基于 pluggy 的钩子系统、三种加载方式与 v17 OcrOptions 迁移

OCRmyPDF 插件开发实战指南:基于 pluggy 的钩子系统、三种加载方式与 v17 OcrOptions 迁移

2026/9/9 15:14:17

OCRmyPDF 插件开发实战指南:基于 pluggy 的钩子系统、三种加载方式与 v17 OcrOptions 迁移 【免费下载链接】OCRmyPDF OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched 项目地址: https://gitcode.com/GitHub_Trending/oc/O…

UVM object创建时type_id::create的parent参数要不要传this?

UVM object创建时type_id::create的parent参数要不要传this?

2026/9/9 15:14:17

UVM里有个特别容易被忽略、但实际项目里又特别值得较真的细节:在component中创建object时,type_id::create的第二个参数parent到底要不要写this。两种写法编译都能过、仿真也都能跑,甚至绝大部分场景下功能表现完全一致。可一旦环境里例化了多…

three.js WebGLCubeRenderTarget 全面指南:立方体渲染目标与动态环境贴图

three.js WebGLCubeRenderTarget 全面指南:立方体渲染目标与动态环境贴图

2026/9/9 15:14:17

three.js WebGLCubeRenderTarget 全面指南:立方体渲染目标与动态环境贴图 【免费下载链接】three.js JavaScript 3D Library. 项目地址: https://gitcode.com/GitHub_Trending/th/three.js WebGLCubeRenderTarget 是 three.js(当前仓库 GitHub_Tr…

Windows 10/11 跑 Android 子系统:WSABuilds 从零到跑通的手把手安装手册

Windows 10/11 跑 Android 子系统:WSABuilds 从零到跑通的手把手安装手册

2026/9/9 15:54:19

Windows 10/11 跑 Android 子系统:WSABuilds 从零到跑通的手把手安装手册 【免费下载链接】WSABuilds Run Windows Subsystem For Android on your Windows 10 and Windows 11 PC using prebuilt binaries with Google Play Store (MindTheGapps) and/or Magisk or …

WSABuilds 30 分钟上手:在 Windows 上装一个带 Google Play 和 Root 的安卓环境

WSABuilds 30 分钟上手:在 Windows 上装一个带 Google Play 和 Root 的安卓环境

2026/9/9 15:54:19

WSABuilds 30 分钟上手:在 Windows 上装一个带 Google Play 和 Root 的安卓环境 【免费下载链接】WSABuilds Run Windows Subsystem For Android on your Windows 10 and Windows 11 PC using prebuilt binaries with Google Play Store (MindTheGapps) and/or Magi…

OpenCore Legacy Patcher 快速上手指南:3步让旧Mac跑起新版macOS

OpenCore Legacy Patcher 快速上手指南:3步让旧Mac跑起新版macOS

2026/9/9 15:54:19

OpenCore Legacy Patcher 快速上手指南:3步让旧Mac跑起新版macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 2015年的 MacBook Pro 上&#x…

9款开源免费AI论文软件横评:毕业论文全流程工具推荐

9款开源免费AI论文软件横评:毕业论文全流程工具推荐

2026/9/9 15:54:19

又到了毕业论文的季节,后台几乎每天都能收到类似提问:有没有好用又不收费的AI论文软件?开源的工具到底能不能打?说实话,这两年我前后折腾过不下20款AI写作、文献检索和排版工具,踩过的坑能写满一页A4纸。这…

三维工厂设计中PLANT3D结构建模流程与避坑指南

三维工厂设计中PLANT3D结构建模流程与避坑指南

2026/9/9 15:54:19

在三维工厂设计项目里,最容易出现的一种返工是:管道布置都快完成一轮了,设备管嘴标高也对完了,结果结构模型一合进来,发现柱子正好穿过泵入口软管区域,或者管廊梁下净空少了200毫米,保温层根本过…

AI编程工具选型指南:破解‘opencode‘搜索迷雾

AI编程工具选型指南:破解‘opencode‘搜索迷雾

2026/9/9 15:44:19

1. “opencode”不是标准工具,而是开发者对开源编码能力的泛指概念 “opencode”这个词在当前技术社区里没有官方定义,既不是 npm 上注册的知名包,也不是 GitHub 上有明确组织归属的成熟项目。它不像 React、Vue 或 Next.js 那样拥有清晰的官…

中国人民大学杨琳团队《Nature Communications》 | 全球潮汐湿地土壤有机碳时空格局与环境驱动:一项2009-2020年的全球评估

中国人民大学杨琳团队《Nature Communications》 | 全球潮汐湿地土壤有机碳时空格局与环境驱动:一项2009-2020年的全球评估

2026/9/9 1:14:29

本文首发于“生态学者”!从“湿地面积”到“土壤碳密度”:为什么需要重新认识潮汐湿地蓝碳变化?潮汐湿地位于陆地与海洋的交汇地带,包括红树林、盐沼和潮滩,是全球重要的蓝碳生态系统。其土壤能够长期储存大量有机碳&a…

adb抓包

adb抓包

2026/9/8 4:55:53

前言 本文介绍如何通过 tcpdump 在 Android 手机上抓取网络数据包,并在电脑端使用 Wireshark 进行分析。适用于需要排查 App 网络请求、分析接口调用或调试网络问题的开发与测试场景。1. 手机要有 root 权限2. 下载 tcpdump3. adb push C:\Users\zhangkuixun\Downlo…

大模型推理镜像极简瘦身:从 25GB 巨无霸到 3GB 精简镜像实战

大模型推理镜像极简瘦身:从 25GB 巨无霸到 3GB 精简镜像实战

2026/9/8 22:37:26

大模型推理镜像极简瘦身:从 25GB 巨无霸到 3GB 精简镜像实战 在云原生基础设施中,容器镜像体积直接决定了服务的部署速度与弹性扩容敏捷度。对于传统的 Go / Java 微服务,镜像体积通常被严格控制在 50MB 到 200MB 以内,拉取镜像只…

扩散模型图像恢复实战:从DDPM原理到PyQt5可视化系统

扩散模型图像恢复实战:从DDPM原理到PyQt5可视化系统

2026/9/9 0:03:36

简介:面向毕业设计场景的PyQt5扩散模型图像恢复项目,提供完整Python源码与项目说明,适合图像处理、深度学习方向的高年级本科生与研究生参考。项目在模块设计上覆盖图像处理、扩散模型、参数配置、用户界面与结果评估五部分,具体涉…

开关电源环路裕量测试实战:相位裕量与增益裕量详解

开关电源环路裕量测试实战:相位裕量与增益裕量详解

2026/9/9 0:03:36

1. 项目概述:为什么环路裕量测试是电子工程师绕不开的“体检项目”“从零开始的电子工程师生活(6)——环路裕量测试”,这个标题一出来,老电源工程师可能已经下意识摸了摸示波器探头,新同事则大概率在想&…

定时插座芯片怎么选?专用定时IC与单片机MCU选型对比

定时插座芯片怎么选?专用定时IC与单片机MCU选型对比

2026/9/9 0:03:36

拆开市面上不同价位的定时插座,你会发现一个有意思的现象:有的里面躺着一颗黑色的软封装芯片,丝印都看不清;有的则是一块小小的蓝色或绿色PCB,上面赫然印着STM8或者STC的字样。同样叫"定时插座",…

远程协作的工作台整理

远程协作的工作台整理

2026/9/8 4:23:39

远程协作的工作台整理远程协作的核心不是再加一个工具,而是让交接信息足够完整。异步任务要写明目标、输入位置、完成标准和需要决策的人。 工作台的最小配置 将日程、待办、代码和沟通入口收拢到少数固定位置;通知按紧急程度分层。工作台不需要模仿办公…

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

2026/9/8 3:19:39

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

2026/9/8 4:00:23

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…