☰
SpringBoot+Vue+MyBatis+MySQL游戏商城实战:前后端分离与订单系统设计
2026/10/8 20:02:14 网站建设 项目流程

1. 项目概览:一个游戏商城拆开来看都有什么

先说下这个项目是怎么来的。我最初只是想找个完整的项目练手,把 SpringBoot 和 Vue 这条链路从头到尾走一遍,后来干脆决定做一个游戏销售平台。原因很简单:游戏商城比普通的增删改查系统多了一些实际业务味道——商品有分类、有库存,订单有状态流转,还要处理购物车、结算、支付模拟这些场景。把这些都跑通,比单纯做一个学生管理系统有价值得多。这套系统用到的技术栈就是标题里写的四个:SpringBoot + Vue + MyBatis + MySQL,前端后台分离,源码完整,部署也能直接照做。

1.1 核心业务模块:从商品浏览到订单完成

整个系统按使用者分成两端:普通用户端和管理员端。用户端做的事情是:浏览轮播图推荐的游戏、按分类筛选商品、点进详情看介绍和价格、把商品加入购物车、购物车结算生成订单、个人中心查看订单状态。管理员端做的事情是:维护游戏商品的上架下架、管理分类和轮播图、查看用户订单。这样划分之后,系统边界很清晰,你做前后端联调的时候也知道每个接口到底服务谁。

商品模块我设计了几个字段:标题、封面图、原价、现价、库存、销量、分类ID、上下架状态。这里有个细节,销量字段不是下单时随便取的,而是在订单支付成功之后才做累加更新,避免用户把商品加入购物车但最后没付款,销量却在涨。库存的扣减时机也值得注意:我在下单时预占库存,支付确认后真正扣减,用户如果超时未付款取消订单,再把库存释放回来。这个逻辑在后面防超卖那一节会展开讲。

订单模块是业务的核心。订单主表记录订单号、用户ID、总金额、收货信息、订单状态、创建时间和支付时间。订单明细表记录每个游戏在订单里的快照信息,包括游戏标题、封面、单价、数量。为什么要快照?因为游戏商品可能之后改价、改封面甚至下架,但订单已经生成了,用户看到的信息必须以成交那一刻为准。这在电商项目里是常识,很多新手教材不做快照,后面查历史订单就会出现“价格对不上”的问题。

1.2 前后端分离的通信约定:统一返回体与API风格

前后端分离之后,两边各自维护一套代码,互相之间只靠 HTTP 接口沟通,所以接口契约一定要稳定。我定义了一个统一返回体Result<T>,包含三个字段:code、msg、data。code=200代表成功,其他值代表各种业务异常,比如401表示未登录或登录过期。前端 axios 拦截器统一处理这个结构,凡是code不是 200 的,直接在页面上弹错误提示,业务代码里就不需要每个请求都写一遍错误分支。

API 路径我按模块做了前缀规划:/api/user管登录注册、/api/game管商品查询、/api/cart管购物车、/api/order管订单、/api/admin管后台操作。所有接口统一用 RESTful 风格,查询用 GET,新增用 POST,更新用 PUT,删除用 DELETE。这样前后端开发可以并行推进,前端拿一份接口文档直接开 mock,不用等后端写好才能动工。

1.3 源码目录导读:拿到代码后先看哪里

如果你拿到的是别人的项目源码,第一件事不是急着点运行,而是先看目录结构。后端 SpringBoot 工程里,重点看controller和service包,它们最能反映业务组织方式。我的目录是:controller放接口层,service放业务逻辑,mapper放 MyBatis 接口,entity放数据库实体,common放统一返回体和 JWT 工具类,config放拦截器注册和 WebMvc 配置。前端 Vue 工程里,重点看src/api目录,里面每一个 js 文件对应一个模块的请求方法,src/router定义路由表,src/store用户登录状态,src/views放页面组件。

这样一个项目,真正写代码的时间大概占一半,另外一半时间全在联调、排错和部署上。接下来我会按选型逻辑、后端实现、前端实现、部署流程四个部分,把最关键的内容拆开讲一遍。

2. 技术选型背后:四件套是怎么凑到一起的

很多初学者喜欢问“什么技术栈最好”,其实脱离业务场景谈技术选型没有意义。我选这套组合,不是因为它们是最新的,而是因为它们在“游戏销售平台”这个规模的项目里,成本和收益最平衡。下面逐个说。

2.1 为什么是 SpringBoot 而不是 SSH 或 Node

SpringBoot 最大的价值是“约定大于配置”。如果回到十多年前用 Spring MVC + Spring + Hibernate 那套 SSH 组合,光 XML 配置文件就能写几十个,新手光是把环境跑通就要花一整天。SpringBoot 把绝大多数配置做成了自动装配,我只需要在pom.xml里引入spring-boot-starter-web,一个main方法就能启动内嵌 Tomcat,这大大降低了上手门槛。

另外 SpringBoot 的生态成熟,社区里能搜到的资料非常多。像用户登录、JWT 鉴权、MyBatis 分页、文件上传、邮件发送这些常见需求,几乎都有现成的 starter 和博客可以参考。对于一个人维护的小项目来说,遇到问题时“一搜就有答案”比“技术更先进但没人踩过坑”重要得多。Node 那边的 Express 和 Nest 也不是不能做,但如果你想同时练一份 Java 后端的求职技能,SpringBoot 的回报率显然更高。

2.2 MyBatis 在中小型项目里的优势与代价

有人会问,为什么不用 JPA 或者 MyBatis Plus?我的看法是:这个项目的 SQL 比较复杂的地方(订单状态流转、联表查询、动态条件筛选)需要我精确控制 SQL,直接用 MyBatis 的 XML 写最稳妥。

MyBatis 的核心优势是 SQL 和代码分离。我把select * from game where status = #{status}这类语句写在 XML 文件里,调整 SQL 时不用重新编译 Java 代码,属于数据库开发者的直觉式操作。对于动态 SQL,比如商品列表页的“分类+关键字+价格区间+排序方式”组合筛选,用 MyBatis 的<where>和<if>标签可以优雅拼接,代码可读性比 JPA 那种层层封装高很多。

代价也很明显:每张表都要写一遍实体类、Mapper 接口和 XML 文件,增删改查的基础代码量偏大。所以我在项目里同时用了一套“通用 BaseMapper”的思路,纯单表无条件的查询直接写在接口默认方法里,有特殊需求的才在 XML 里手写,兼顾效率和可控性。

2.3 Vue 前后端分离到底“分离”了什么

“前后端分离”这个词听起来高级,本质就是讲清楚一件事:前端负责渲染和交互,后端负责数据和安全,双方只通过 JSON 交换信息。

传统 JSP 时代,页面写在服务端,Java 代码和 HTML 混在一起,前端改个按钮样式都得重新部署整个应用。Vue 这种 SPA(单页面应用)框架出现后,HTML、CSS、JavaScript 全在浏览器里运行,打包成静态文件丢到 Nginx 上就能跑,后端只需要提供接口服务。这样做的好处有三个:第一,前端可以使用组件化开发,一个轮播图组件、一个商品卡片组件可以在多个页面复用;第二,后端服务不需要关心页面长什么样,接口可以被小程序、H5、App 多个客户端复用;第三,部署时前端静态文件和后端服务可以分别扩容,前端压力大就加 Nginx 缓存节点,后端压力大就加应用实例。

Vue 本身我也对比过 React。实话说两个都能做,但 Vue 的中文资料更友好,模板语法接近 HTML,对入门者更平滑。Element UI 组件库可以直接提供表格、表单、弹窗、分页这些现成的组件,做后台管理页面很快。

2.4 这套方案在什么情况下需要升级

这套系统是单体架构,数据库是单库单表,不引入 Redis、不引入消息队列、不搞微服务,这是有意为之的。因为一个游戏销售平台的第一版,核心目标是验证业务能不能跑通,用户量、数据量都还不大。

但当出现下面这些情况时,就该考虑升级了:第一,商品库存和购物车频繁读写同一行数据,压测时出现性能瓶颈,需要引入 Redis 做缓存预热,甚至用 Lua 脚本做库存扣减;第二,订单创建和支付回调要求高可用,单体进程挂了会影响所有业务,这时候考虑拆出独立的订单服务、支付服务;第三,商品搜索需要分词、模糊匹配,MySQL 的like '%关键字%'查不动了,就得上 Elasticsearch。技术选型不是一成不变的,关键是知道当前这套方案的边界在哪。

3. 后端核心实现:数据库设计、鉴权与关键接口

后端是整个系统的地基,数据库设计尤其重要。表结构如果设计得不好,后面写 Service 的时候会处处难受,改表代价又大。所以我会花比较多篇幅讲表设计和链路实现。

3.1 七张表搞定整个业务:核心表结构设计

完整项目的数据库共七张表:用户表、分类表、游戏商品表、轮播图表、购物车表、订单主表、订单明细表。这里不去贴全部建表语句,只挑三个关键设计说明。

用户表字段:id、username、password、nickname、avatar、phone、create_time。密码不能明文存,我用的是 BCrypt 加密,这是 Spring Security 自带的加密工具,但我在项目里单独立了一条工具类引进来,避免为了一个加密功能引入整套 Security 导致配置复杂。每次注册时对密码做哈希,登录时用matches方法校验,数据库里即使泄露也不会直接暴露明文。

游戏商品表字段:id、title、cover、category_id、price、original_price、stock、sales_count、status、description、create_time。价格我用的是整数分存储,比如游戏卖 68 元,数据库里存 6800。这是为了避免浮点数精度问题,0.1 + 0.2在计算机里不等于0.3,金额相关的字段绝对不能使用 DOUBLE。返回前端时再除以 100 转成元,展示层再去格式化成人民币符号。

订单表字段里有一个order_no,这个字段是用户下单时生成的一串唯一编号。我生成的规则是:时间戳 + 用户ID + 随机数,再通过数据库唯一索引兜底。不要直接使用数据库自增 id 当订单号发给用户,因为这样会把平台的日订单量暴露出去,而且订单号里带有顺序信息,容易被人推测业务量。

3.2 订单状态机与防超卖逻辑

订单状态我用一个整数字段status表示:0待支付、1已支付、2已发货、3已完成、4已取消。状态之间不是随便跳转的,必须走固定的流转路径。这是个典型的“状态机”场景:待支付可以取消,已支付只能发货,已发货只能完成,已取消不可逆。

在写 Service 层时,我在每个状态变更方法入口都做了前置校验。比如支付时先检查订单状态是不是0,如果不是直接抛异常,防止用户把已取消的订单再支付一遍。前端只能看到按钮,但接口层面必须把状态校验做好,因为攻击者可以绕过前端直接调接口。

防超卖的思路是这样的:下单时不是先查库存再做减法,而是执行一条带条件的更新语句:

update game set stock = stock - 1 where id = #{gameId} and stock > 0

这条 SQL 的妙处在于,如果库存已经不够了,stock > 0条件不成立,影响行数为 0,程序据此判断下单失败。用数据库行锁天然解决并发超卖问题,不需要在 Java 代码里加 synchronized。当然,在高并发量很大的场景下,这条语句会形成行锁竞争,但在学习项目的规模下已经足够稳定。

3.3 SpringBoot 与 MyBatis 的 Mapper 层写法

MyBatis 的常规用法是:Service 调 Mapper 接口,Mapper 接口方法对应 XML 文件里的 SQL 语句。以商品列表查询为例,需要支持按分类筛选、关键字模糊搜索、按价格排序、分页,XML 里这样写:

<select id="selectGameList" parameterType="map" resultType="com.demo.game.entity.Game"> select * from game <where> <if test="categoryId != null"> and category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> and title like concat('%', #{keyword}, '%') </if> and status = 1 </where> order by <choose> <when test="sort == 'sales'">sales_count desc</when> <when test="sort == 'price_asc'">price asc</when> <otherwise>create_time desc</otherwise> </choose> limit #{offset}, #{pageSize} </select>

这里有三个容易踩坑的地方:第一,like拼接不要写在 Java 代码里拼好再传,要使用concat('%', #{keyword}, '%'),防止 SQL 注入;第二,排序字段不要直接拼接前端传入的参数,因为字段名没法用预编译参数占位,所以我用<choose>把可选排序方式限制死了,前端只能传sales、price_asc等枚举值,避免把任意字符串拼进 order by;第三,分页的 offset 是前端传的页码减一乘以 pageSize,后端计算,页面上直接传pageNum=1&pageSize=10。

配置方面,application.yml里设置mybatis.mapper-locations=classpath:/mapper/**/*.xml,并打开驼峰映射map-underscore-to-camel-case: true,这样数据库字段create_time能自动映射到实体类的createTime属性,不用每个字段都手写映射关系。

3.4 JWT 鉴权与拦截器:用户身份的完整链路

用户登录成功后,后端签发一个 JWT 令牌返回给前端,前端存在 localStorage 里,后续每次请求在请求头携带Authorization: Bearer <token>。后端写一个拦截器统一校验 token,并把它解析出用户ID放入 ThreadLocal,业务代码里直接UserContext.getUserId()就能拿到当前登录用户。

Token 里我只放userId和username两个核心信息,过期时间设置为 7 天,密钥写在配置文件中而不是硬编码在代码里。签发和校验用的是 jjwt 库,校验逻辑在拦截器里:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { // 返回 401 } try { Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token.substring(7)).getBody(); UserContext.setUserId(Long.parseLong(claims.get("userId").toString())); return true; } catch (Exception e) { // token 过期或非法,返回 401 } }

这里有个前后端分离项目特有的坑:浏览器发起复杂请求(比如 POST + application/json)之前会先发一个 OPTIONS 预检请求,这个请求不会带 Authorization 头。如果不把 OPTIONS 放过,前端会在控制台看到跨域报错,但后端日志里却没有任何业务异常。排查这种问题很浪费时间,所以拦截器必须对 OPTIONS 请求直接放行。

3.5 商品、购物车、下单三个核心接口的思路

商品详情接口是GET /api/game/detail?id=xxx,返回游戏信息和轮播图列表。这块逻辑不复杂,但要注意访问详情时更新一下点击量,可以做简单的PV + 1,我用的是异步别让更新点击量拖慢响应速度。

购物车接口是 Restful 风格:GET /api/cart/list返回当前用户购物车列表和总价,POST /api/cart/add接收gameId和quantity,PUT /api/cart/update修改数量,DELETE /api/cart/remove?id=xxx删除。添加购物车时先查一下这个商品是否已经在购物车里,如果存在就直接更新数量,避免产生重复记录。列表查询时内连接游戏表,把商品状态和库存带出来,如果商品已下架或库存为 0,前端显示“已失效”,不能勾选和结算。

下单接口是链路过长的一段。流程是:接收用户勾选的购物车记录 ID 列表,后端重新从数据库查出这些记录,校验商品状态和库存,计算总金额(必须后端算,不能信任前端传价格),扣减库存,生成订单主表和明细表,最后批量删除购物车记录。注意购物车的支付状态:必须把“计算金额”和“扣库存”放在一个事务里,任何一个步骤异常都要整体回滚。我在 Service 方法上打了@Transactional,确保不会出现“库存扣了但订单没生成”这种数据不一致。

4. 前端关键细节:Vue 项目里那些不能马虎的点

前端工程是我用 Vue CLI 手动初始化的,用 Vue 2 + Element UI 的组合。这个选择确实不够新,但胜在资料多,组件库稳定,不会有太多版本兼容问题。如果你改成 Vue 3 + Element Plus,思路也一样,只是部分 API 名字变了。

4.1 工程结构、axios 封装与环境变量

前端工程里,我把所有请求统一封装在src/utils/request.js里的一个 axios 实例中。核心配置是:基础路径baseURL: '/api',这样代码里写getGameList时只写相对路径,部署后通过 Nginx 把/api前缀代理到后端服务,开发时则通过 vue.config.js 里的 devServer proxy 代理到本机 8080。这个设计一步到位解决了跨域问题。

请求拦截器里取出 localStorage 的 token 放到请求头上,响应拦截器里统一处理返回体。处理逻辑是:

service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { this.$message.error(res.msg || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.msg)) } return res.data }, error => { // 网络错误统一提示 } )

这里最值得注意的一点是:拦截器返回值直接返回res.data而不是整个返回体。这意味着业务代码里调用const list = await getGameList()拿到的是接口的 data 字段,不需要每个页面都写res.data.data这种重复代码,页面干净很多。

环境变量我也分了:.env.development里VUE_APP_BASE_API = '/api',.env.production里同样走/api前缀。实际上生产环境前端并不需要知道后端的具体地址,统一交给 Nginx 反向代理,这样可以避免后端 IP 暴露到浏览器端。

4.2 路由守卫、登录态持久化与用户体验

系统里的购物车、订单、个人中心页面都需要登录才能访问,我在 vue-router 的全局前置守卫里做判断:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })

这里有一个体验细节:登录成功后,要回跳到用户之前想去的页面,而不是固定跳回首页。所以登录页会读取this.$route.query.redirect,有就跳过去,没有就默认去首页。做购物车结算跳登录的场景时,这个细节很关键,不然用户点半天结算,登录完发现回到了首页,还得重新找商品加购,体验很差。

登录态持久化我直接用了 localStorage,搭配 Vuex 在系统运行时管理用户信息。页面刷新时vuex-persistedstate或者手动从 localStorage 重新拉起用户状态,保证刷新后登录态不丢。需要注意清理逻辑:退出登录或者 401 时,要同时清除 localStorage 和 Vuex 里的数据。

4.3 购物车角标、筛选排序和支付回调的交互

购物车角标在顶部导航栏上实时显示商品总数量,这个数据我先存在 Vuex,每次购物车数量变化时更新。进入系统时如果检测到用户已登录,会调购物车列表接口把数量初始化好。底栏的“全选/反选”和“合计金额”是纯前端的计算属性,勾选状态不提交后端。真正下单时把所有勾选记录的 ID 列表传过去,后端再算真实金额。

商品列表页的筛选排序交互也要强调一点:筛选条件变化后,必须重置当前页码为 1,否则会出现“你搜了关键词,但当前停留在第 5 页,而第 5 页可能没有数据”的尴尬。这是很常见的低级 bug,写代码的时候一定要加。

关于支付,真实项目会对接微信或支付宝,需要后端完成签名和回调验签,这里我用了一个模拟支付的页面:用户点击“去支付”后,页面展示一个确认框,确认后请求支付接口,后端直接把订单状态改成已支付。如果你是照着这套代码改造真实支付,记得把模拟逻辑替换为服务端异步回调,不要在前端写任何“支付成功”的判断,因为回调才是最终凭证。

4.4 联调时最常踩的跨域和日期格式坑

联调阶段常见的问题第一大类就是跨域。开发环境下我通过 vue.config.js 配置 devServer proxy 解决,生产环境下通过 Nginx 同一端口反向代理解决,浏览器始终只访问前端的域名,所以不会出现跨域。如果你直接把前端页面和后端接口部署到不同域名,就必须在后端配置 CORS,否则别怪浏览器拦截。

第二大类是日期显示问题。MySQL 的datetime类型通过 MyBatis 返回给 Jackson 序列化后,默认是一串带时区的时间格式,比如2024-01-01T12:00:00.000+08:00,前端拿到的不是标准yyyy-MM-dd HH:mm:ss。我在后端配置了统一的日期格式化器,让接口返回2024-01-01 12:00:00这样的格式,前端直接用就行,不需要再做一层转换。如果你是从别的项目拿来的代码,先问一下接口日期的格式约定,省得后面为这个来回扯皮。

5. 完整部署流程:从本机跑通到云服务器上线

本地开发环境跑通只是第一步,真正折磨人的是把东西部署到一台干净的云服务器上。很多项目源码给到手里,本地怎么都跑不起来,部署文档又写得模棱两可,最后只能躺平。这里把整个过程按顺序过一遍,你在操作时按部就班就行。

5.1 本地跑通这套系统的三个必要条件

第一个条件是环境版本匹配。Java 用 JDK 1.8(SpringBoot 2.7 在 JDK 8 和 11 下都能跑),Maven 用 3.6 以上,Node 用 14 或 16,MySQL 用 5.7 或 8.0。版本不要追新,追新的代价是你遇到的报错在网上一搜全是外文 issue,解决成本极高。

第二个条件是数据库初始化。先创建数据库game_sales,设置字符集为utf8mb4,再导入项目里的sql文件。MySQL 8.0 默认字符集是utf8mb4,不需要额外指定,但如果你用的是 5.7,一定要确认库表和连接的字符集都是utf8mb4,否则中文数据和 emoji 编码都会出问题。

第三个条件是修改配置文件。在application.yml里把数据库用户名、密码、URL 改成你自己的。URL 里要声明useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8,这三项分别解决 SSL 警告、时区报错、中文乱码问题。很多新手第一次连 8.0 数据库报时区错就是这个原因。

三个条件都满足后,先启动后端,看控制台有没有正常注册接口,再用浏览器直接访问接口文档里的地址验证,之后启动前端npm run serve,访问localhost:8081就能看到页面了。

5.2 Maven 打包与 SpringBoot 外部配置分离

本地一切正常后,开始打包。后端打包命令是mvn clean package -DskipTests,在target目录会生成一个可执行的 jar。我的项目在pom.xml里配置了 finalName,打包出来叫game-sales-server.jar。

一个重要的运维习惯是:不要把数据库密码写死在 jar 里的配置文件里。我的做法是,在 jar 同目录下放一个application-prod.yml,用spring.profiles.active=prod来激活生产配置,生产环境的数据库地址和密码只写在服务器上的这个文件里。启动命令:

nohup java -jar game-sales-server.jar --spring.profiles.active=prod > server.log 2>&1 &

nohup和&让应用在后台运行,日志写入server.log,关掉终端也不影响服务。我见过很多新手用java -jar启动后一关 SSH 窗口服务就没了,就是因为没加nohup。

5.3 Vue 打包与 Nginx 静态资源部署

前端打包命令是npm run build,生成dist目录。把dist里的所有文件上传到服务器 Nginx 的 html 目录下,然后配置 Nginx 的 server 块:

server { listen 80; server_name your.domain.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }

这里有两个关键点。第一,proxy_pass里我保留了/api/前缀,因为后端的接口路径本身就是/api/xxx,如果去掉,代理过去就变成根路径,全 404。第二,try_files $uri $uri/ /index.html是 SPA 路由的关键,没有这一行,用户在商品详情页按 F5 刷新,Nginx 找不到对应的物理文件会返回 404,有这一行后所有前端路由都回退到 index.html,由 Vue Router 自己接管页面渲染。

5.4 服务器端 MySQL 初始化和远程访问配置

如果你把 MySQL 也装在服务器上,安装完成后要做两件事。第一,初始化数据库并导入 SQL 文件,这一步和你本地操作一样。第二,处理远程连接权限:

CREATE USER 'game_user'@'%' IDENTIFIED BY '你的密码'; GRANT ALL PRIVILEGES ON game_sales.* TO 'game_user'@'%'; FLUSH PRIVILEGES;

这里有个很隐蔽的坑:MySQL 8.0 默认认证插件是caching_sha2_password,而 JDBC 驱动如果版本较老,连接时会报Public Key Retrieval is not allowed错误。解决办法有两个:一是升级 MySQL Connector 到 8.0.x,二是在 JDBC URL 加allowPublicKeyRetrieval=true。如果你是 5.7 的库,基本不会有这个问题。

另外检查服务器防火墙和安全组是否放行了 3306 端口。但比起把 3306 端口对公网开放,我更建议把 MySQL 绑定在127.0.0.1,只允许本机的后端服务连接。如果后端和数据库不在同一台机,再用 SSH 隧道或者内网地址。公网直连数据库是很大的安全隐患,生产环境必须避免。

5.5 上线之后如何保证服务稳定

部署完成只是开始,服务和进程的稳定性才是关键。第一件事,给 Java 服务写一个 systemd 服务文件,这样服务器重启后应用可以自动拉起,也能用systemctl restart game-sales优雅重启。如果你图省事用nohup,至少写个 crontab 每分钟检查端口存活的脚本,进程挂掉自动重启。

第二件事,日志处理。SpringBoot 默认日志打到控制台,我在application-prod.yml里配置了 logback,按天滚动生成日志文件,保留最近 30 天。排查问题的时候tail -f server.log是最直接的思路,先看有没有异常堆栈。

第三件事,静态资源缓存。Nginx 里给图片、CSS、JS 配置expires 7d,可以减少后端压力。注意 index.html 本身不要缓存,否则前端发新版后用户拿到还是旧页面。我用的方案是location = /index.html { add_header Cache-Control no-cache; }。

这个项目做完之后,我最直观的感受是:花在依赖配置、环境变量、部署脚本上的时间,其实比写业务代码还多。所以我特别建议你拿到源码后不要只跑通就算了,而是亲手从头搭一遍环境、打一次包、上云部署一次。只有把这些链路走通,才谈得上真正掌握前后端分离项目的交付能力。如果部署过程中卡住,最多的原因无非是端口没开、字符集不对、路径代理写错,沿着日志一层层查,基本都能定位到。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询