简介:在Java Web开发领域,SSM与SpringBoot的整合是构建企业级应用的经典方案。SSM作为Spring、SpringMVC与MyBatis的组合,提供了灵活的分层架构与SQL控制能力;而SpringBoot通过自动配置与内嵌容器,让项目搭建和部署变得极简。二者并非替代关系,而是底座与组件的协作。本文以旅游网站为例,从技术选型、表结构与订单状态机设计,到搜索、下单、模拟支付等核心业务闭环,再到JWT+Redis登录鉴权、文件上传安全、PDF与XSS防护等加固细节,最后讲解Jar包与Docker部署方案。内容覆盖毕业设计及简历项目的完整链路,帮助开发者快速构建一个功能齐全、安全可靠的旅游平台,并深刻理解SpringBoot整合MyBatis的工程实践。 最近接了一个挺典型的咨询:一个准备做毕设的同学,把“SSM+Springboot 一个功能齐全的旅游网站”这个题目发给我,第一句话就是“这俩框架是不是二选一,我到底学哪个”。这个问题我太熟悉了,因为“SSM”和“SpringBoot”这两个词放在一起,确实容易让人懵:SSM 不是已经被 SpringBoot 取代了吗?怎么还能加在一起?
实际上,这两个东西压根不在一个维度上。SSM 指的是 Spring + SpringMVC + MyBatis 这套经典 Java Web 组合,而 SpringBoot 是一个自动配置、开箱即用的开发框架底座。在现代项目里,完全可以用 SpringBoot 作为底座,上面照样跑 SpringMVC 和 MyBatis,这就是标题里“SSM + SpringBoot”的真实含义,也是很多毕业设计、课程设计、简历项目里最常见的写法。
这篇我就以自己完整做过的一个旅游网站项目为例,从技术选型、数据库设计、核心业务实现、安全加固,一直讲到打包部署,把“功能齐全”这四个字落地的过程完整拆一遍。适合准备做毕设或简历项目的同学,也适合刚入门 SpringBoot 想跑通一条完整业务链路的初级开发。整个过程不搞花活,重点说清楚每一步为什么这么设计,以及我实际踩过的坑。
1. SSM和SpringBoot不是二选一:旅游网站的技术底座怎么搭
1.1 标题里的SSM到底指什么
很多教程会把“SSM”解释成 Spring + SpringMVC + MyBatis,这个说法没有错,但在 SpringBoot 时代,它很容易被误解成一个“老掉牙”的框架组合。真实的开发情况是:SpringBoot 本身包含了 Spring 的核心能力,也内置了 SpringMVC,只需要引入对应 Starter 就能用,MyBatis 则通过 mybatis-spring-boot-starter 完成整合。
所以当你看到“SSM + SpringBoot”这个标题时,可以理解成下面这个结构:
- SpringBoot:项目底座,负责自动装配、配置管理、内嵌 Tomcat、健康检查;
- SpringMVC:负责前端请求的路由和响应,就是平时写的 Controller;
- MyBatis:负责数据库访问,所有 SQL 都集中在 Mapper 层管理。
这种组合的好处是,你既有 SSM 时代“SQL 写起来灵活、分层清晰”的优点,又能享受 SpringBoot 带来的极简配置和快速部署。对旅游网站这种业务场景来说,恰恰是性价比最高的搭配。
1.2 旅游网站为什么适合这套组合
功能齐全的旅游网站,本质上是一个“内容展示 + 交易”的系统。它包含景点介绍、旅游线路、酒店信息这类内容型数据,又包含注册登录、下单、支付、订单管理这类交易型数据。这种项目最大的特点就是:增删改查多、表关联复杂、业务流程长。
MyBatis 在这类场景下非常合适,因为线路列表、景点详情、订单详情这些页面往往需要多表联查,SQL 直接写在 Mapper XML 里,调整条件或优化查询都很方便。SpringMVC 的 Controller 足够简单,能撑起从用户端到管理后台的所有接口。SpringBoot 则把所有繁琐的 XML 配置、包扫描、数据源初始化都自动化了,省下的时间可以全部花在业务逻辑上。
另外还得考虑到一个现实因素:这类项目经常用来做毕业设计或者简历项目。SpringBoot 是目前企业使用的主流框架,掌握它意味着你能直接适应公司项目;而 MyBatis 在国内企业里占有率也一直不低。学一套组合,等于同时覆盖了旧项目维护和新项目开发的常见场景。
1.3 版本选择的建议:别盲目上3.x
SpringBoot 3.x 已经发布很久了,但如果你是为了快速做完旅游网站这类项目,我不建议直接从 3.x 开始。原因很简单:SpringBoot 3 底层从 javax 包迁移到了 jakarta 包,MyBatis 和相关组件的版本要求都变了,网上大量旧教程里的配置和方法会直接报错。你本来是想快速搭项目,结果光折腾包名和依赖就耗掉一两天。
我实际的推荐组合是 SpringBoot 2.7.18。这个版本属于 2.x 的末期维护版,稳定、资料多,兼容性也足够好。下面的依赖版本我在项目里实测过,可以直接抄:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>这里有个细节很多人会踩:MySQL 官方驱动从 8.0.31 之后,某些版本在连接时要求显式开启 allowPublicKeyRetrieval,否则会报 “Public Key Retrieval is not allowed”。所以在 JDBC URL 里最好加上这个参数,后面配置章节我会再提。
2. 功能齐全先从数据库开始:核心表与订单状态设计
2.1 先盘点“功能齐全”包含哪些模块
做项目最容易犯的错,是一上来就写代码,写到一半发现表结构撑不起业务。所以动手之前,我习惯先用一页纸把功能模块盘点清楚。旅游网站的“功能齐全”,至少应该覆盖以下几条链路:
用户端:
- 注册、登录、修改资料、头像上传;
- 景点列表和详情、旅游线路的搜索与筛选;
- 线路/景点门票的加入订单、结算下单;
- 模拟支付、订单状态查询、取消订单;
- 订单完成后评价、收藏景点/线路。
管理端:
- 管理员登录、用户管理;
- 景点、线路、酒店等内容管理;
- 订单查询、发货/核销、退款处理;
- 基础数据统计(比如各线路销量、用户增长)。
把你做的模块列表写清楚,数据库设计也就有依据了。模块之间不是孤立的,用户下单会关联线路、支付流水、订单状态,评价会关联订单和线路,收藏会关联用户和产品。这张关系网,就是核心表结构的来源。
2.2 核心表结构和字段设计
我这里把实际用到的核心表简化展示一下,字段没有列全,但关键字段都保留着。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, username, password, nickname, phone, avatar, status, create_time | 用户表,password存BCrypt密文 |
| scenic | id, name, city, address, price, open_time, cover, intro, click_count, status | 景点表 |
| line | id, title, destination, days, line_type, price, origin_price, cover, detail, stock, status | 旅游线路表 |
| orders | id, order_no, user_id, product_type, product_id, product_name, product_cover, total_price, status, pay_time, cancel_time, create_time | 订单表,product_type区分线路/门票/酒店 |
| payment | id, payment_no, order_no, channel, amount, status, callback_time | 支付流水表 |
| comment | id, user_id, product_type, product_id, order_no, rating, content, create_time | 评价表 |
| favorite | id, user_id, product_type, product_id, create_time | 收藏表,user_id+product_id唯一 |
订单表这里我要多说一句。很多人会设计成 order 主表 + order_item 子表,这当然规范。但如果你做的是个人项目,且一次订单主要买一个线路或一张门票,完全可以把商品名称、商品封面直接冗余在主表里。这样查询订单列表时不需要再去关联线路表,既减少一次 JOIN,也能避免线路信息被修改后,历史订单显示错乱。
2.3 表设计时容易被忽略的细节
第一,金额字段不要用 float 或 double。二进制浮点数在金额累计、比较时会出现精度问题,轻则显示不对,重则订单金额对不上。正确做法是用 decimal(10,2),或者干脆用 int,以“分”为单位存储,展示时再转换为元。
第二,订单号不要用自增 id。用户端会看到订单号,自增 id 不仅暴露业务量,还容易被枚举攻击。我习惯用“业务前缀 + 时间戳 + 随机数”的方式生成,比如TC202406121030001234。如果后面要做并发压测,建议再对订单号加唯一索引,防止极端情况下生成重复。
第三,状态字段建议用 tinyint 存数字,配合代码里的枚举类使用。比如订单状态:0待支付、1已支付、2已完成、3已取消、4已退款。直接用数字存有一个好处,就是索引效率高、占空间小;但前提是代码里必须有明确的常量映射,不能到处写魔法数字。
第四,软删除字段 delete_flag 尽量加上。用户删除了收藏记录、管理员下架了线路,这些操作做成逻辑删除比物理删除安全得多,后面想恢复数据也方便。
2.4 订单状态机是可讨论的重点
订单状态是整个交易系统的核心。面试官或者答辩老师问得最多的也是这里:用户下单之后,状态是怎么一步步变化的,各种异常情况下你如何处理。
我的订单状态流转是这样设计的:
- 用户提交订单,生成一条 status=0(待支付)的订单;
- 用户支付成功,支付回调更新 status=1(已支付);
- 用户确认出游/核销完成,更新 status=2(已完成);
- 用户在待支付状态下主动取消,更新 status=3(已取消);
- 支付后申请退款,管理端审核通过,更新 status=4(已退款)。
这里有一个容易忽略的问题:待支付订单如果一直不支付怎么办?不能让它占着库存不释放,所以必须有一个“超时未支付自动取消”的兜底机制。最简单的做法是定时任务每几分钟扫一次订单表,把创建时间超过30分钟且 status=0 的订单改掉,并回补库存。如果你想让方案更完善,可以在下单时用 Redis 的setnx记录订单号,设置过期时间;但定时任务仍然是个人项目里最稳、最好解释的方案。
3. 骨架搭建:依赖、配置文件和第一道坑
3.1 目录结构与包名规划
项目结构我习惯按“模块分包”而不是“技术分层”来组织。也就是说,不是controller/ service/ mapper/三层平铺到底,而是先分出功能模块,再在每个模块内分层。比如:
com.travel ├── common // 通用返回结果、异常处理、常量 ├── config // 配置类:WebMvc、Redis、Druid等 ├── controller // 控制层 ├── service // 业务层 ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体 ├── dto // 请求参数对象 └── vo // 响应视图对象当然,如果项目规模不大,按传统三层分包也没有问题。关键是包里职责要清晰,不要让 Controller 里写一堆 SQL 逻辑,也不要让 Service 层变成又长又臭的“上帝类”。
3.2 配置文件:这些参数踩坑率最高
application.yml 是 SpringBoot 整合 SSM 时最容易出问题的地方。下面是一份我实际项目里能直接跑起来的配置,关键地方我加了注释:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 druid: initial-size: 5 max-active: 20 min-idle: 5 validation-query: SELECT 1 redis: host: localhost port: 6379 timeout: 3000ms servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.travel.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true这里几个连接参数值得单独说:
serverTimezone=Asia/Shanghai 必须加。不加的话,MySQL 8 默认时区是 UTC,你存入的时间会比北京时间早 8 小时,查出来会一脸懵。
allowPublicKeyRetrieval=true 是 MySQL 8 和某些驱动版本组合下的常见报错点。开发环境开着没事,生产环境如果用 SSL 或更安全的认证方式,再按实际情况收紧。
map-underscore-to-camel-case=true 这一行一定要开。数据库字段是 create_time,实体类是 createTime,没有这个配置,MyBatis 映射回来全是 null。
分页插件 reasonable=true 的作用是:当你请求第 100 页但总共只有 10 页时,自动获取最后一页数据,而不是报错。这个参数在前后端联调时非常友好。
3.3 统一返回、全局异常和事务控制
我建议写业务代码之前,先把三件事做掉:统一返回对象、全局异常处理器、事务配置。否则后面每写一个接口都要重复处理错误码和异常,代码会变得非常难看。
统一返回对象我一般是这样定义的:
public class Result<T> { private Integer code; private String message; private T data; // 构造方法、getter/setter 省略 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.code = code; result.message = message; return result; } }全局异常用 SpringBoot 提供的@RestControllerAdvice实现,业务异常、参数校验异常、兜底异常分别处理。核心思路是:异常信息不能直接甩给前端,尤其是 SQL 异常里可能包含表名、字段名等敏感信息。
事务方面,SpringBoot 已经自动配置了事务管理器,我只需要在 Service 方法上加@Transactional即可。但这里有一个非常重要的经验:不要把远程调用、消息发送、文件上传这类耗时操作放在事务方法里。事务的本质是锁住数据库资源,如果事务长时间不提交,连接池很容易被占满,系统整体性能会直线下降。
3.4 我踩过的第一个坑:拦截器把静态资源也拦了
SpringBoot 整合 SSM 项目后,最常见的报错不是依赖冲突,而是“静态资源被拦截器拦掉了”。因为你用拦截器校验用户登录时,很容易写成“所有请求都要通过登录校验”,结果网站前台的 CSS、JS、图片全部被当成未登录请求打回去,页面一片乱。
正确做法是在拦截器注册时,添加排除列表。比如用户登录接口、注册接口、验证码接口、公开的景点列表接口、静态资源路径/static/**、上传文件访问路径/upload/**都要放行。我自己的经验是,登录拦截范围只覆盖/user/**和/admin/**这类明显需要身份的路径,前后台的公开接口另做排除,不要图省事拦截全部请求再慢慢加白名单。
4. 搜索、下单、支付、评价:功能齐全的项目要把链路做闭环
4.1 线路搜索与动态SQL
旅游网站的搜索功能通常是多条件组合筛选:按目的地、游玩天数、价格区间、线路类型、关键词。这些条件并不是每次都会传,所以不能每换一个条件就写一条 SQL,而要用 MyBatis 的动态 SQL 来处理。
<select id="searchLines" resultType="com.travel.entity.Line"> SELECT * FROM line <where> <if test="destination != null and destination != ''"> AND destination LIKE CONCAT('%', #{destination}, '%') </if> <if test="days != null"> AND days = #{days} </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR destination LIKE CONCAT('%', #{keyword}, '%')) </if> AND status = 1 </where> ORDER BY create_time DESC </select><where>标签会自动去掉第一个条件前面的 AND,这个语法是 MyBatis 最常用的功能,写动态查询一定要熟练。
如果你想把搜索做得更像样一点,可以考虑引入分词。比如中文关键词“三亚五日游”,如果直接用 LIKE '%三亚五日游%',搜索效果很差;如果先分词成“三亚”“五日游”再用 OR 拼接,命中率会高很多。做 Java 的项目里,HanLP 是一个不错的中文分词工具,在 SpringBoot 里引入对应依赖,业务层先调用分词接口,再构造动态条件。不过这个属于锦上添花,如果你的项目时段紧张,先做好普通模糊搜索即可。
4.2 下单防重复与库存扣减
下单接口是整个系统里并发压力最大、也最容易出 Bug 的地方。核心问题有两个:重复下单和超卖。
重复下单的常见原因不是用户手抖,而是前端请求超时后自动重试,或者用户连续点击“提交订单”。前端可以防止手抖,但后端必须有能力兜底。我的做法是:
第一,同一个用户、同一个产品、在待支付状态下不允许再生成新订单。查询时把user_id、product_id、status=0作为条件,如果存在,直接返回“您有未支付的订单”。
第二,给订单表加唯一索引,防止极端情况下两条记录同时插入。订单号本身可以用唯一索引,因为它是程序生成的,不会重复。
库存扣减更值得注意。新手最容易写出的代码是:
- 先查库存;
- 判断库存是否大于0;
- 执行减库存 update。
这段逻辑在单线程下没问题,并发场景下就会超卖。正确的做法是把判断和扣减放在一条 SQL 里:
UPDATE line SET stock = stock - 1 WHERE id = #{id} AND stock > 0这条 SQL 利用数据库行锁保证原子性:只有库存大于0时才会更新成功,受影响行数为1,表示扣减成功;为0则表示库存不足。然后把“减库存”和“生成订单”放在同一个事务里,任一步失败就整体回滚。
4.3 支付流程:先做模拟,再理解真实回调
个人项目接真实微信/支付宝支付,需要营业执照和商户号,很多同学不具备这个条件。所以最务实的方案是做“模拟支付”,但模拟支付的设计逻辑要尽量贴近真实支付,不然面试时讲不明白。
我的模拟支付流程是这样的:
用户在前端点击“去支付”后,后端生成一条支付流水记录 payment,包含支付单号、订单号、渠道、金额、状态=0(待支付)。前端跳转到一个模拟收银台页面,用户点击“确认支付”,前端请求“模拟支付确认接口”,后端执行以下逻辑:
- 根据支付单号查出流水;
- 校验流水金额和订单金额一致;
- 把流水状态改为支付成功;
- 把订单状态从待支付改为已支付;
- 记录支付时间,返回成功。
真实支付的回调通知,本质上就是这个“模拟支付确认接口”的替代品:支付平台异步通知你的后端接口,携带支付结果和签名,后端验签后更新订单状态。
这里关键的一点是幂等处理。支付平台的通知可能会重复发送好几次,所以回调接口里必须先查订单状态——如果已经是已支付,直接返回成功,不再重复更新。否则就可能出现订单状态被覆盖、消费者重复收到消息的问题。
支付成功后,可以继续做两件事:一是给用户生成消费凭证(类似一个二维码或核销码),二是发通知。通知可以不放在事务里同步做,利用 ActiveMQ 或 Spring 事件机制,支付成功后发布事件,消费者异步发邮件/短信。这样主链路响应快,系统也不容易被第三方服务拖垮。
4.4 评价与收藏的约束细节
评价功能看起来就是往 comment 表插一条记录,但业务约束必须做全。用户只能评价自己订单里的产品,订单状态必须是已完成,且同一个订单不能重复评价。如果订单表里有 comment_status 字段,也可以用这个字段来判断是否已经评价过。
收藏功能的核心是唯一约束。建议在 favorite 表上建立(user_id, product_type, product_id)的唯一索引,数据库层面直接拦截重复收藏。代码里再根据受影响行数返回不同提示,体验会更好。
5. 登录鉴权只算入门:上传安全与PDF/XSS这类隐蔽问题
5.1 用户登录与接口鉴权
旅游网站有用户端和管理端,最简单有效的鉴权方案是 JWT + Redis。用户登录成功后,后端签发一个 JWT token,同时把 token 存到 Redis 里,设置过期时间。前端请求时在 header 里带上 token,拦截器解析 token,再查一下 Redis 确认 token 是否有效。
为什么加了 JWT 还要存 Redis?因为 JWT 本身是无状态、不可撤销的。如果用户被管理员封禁,或者用户自己修改了密码,旧的 JWT 在过期之前仍然有效,这是安全隐患。把 token 存在 Redis 里,后端可以做主动失效;同时还能实现续签、单点踢出等逻辑。
拦截器的注册和前面说的静态资源放行是一套逻辑。我通常这样设计:
/api/**下的部分接口需要登录,排除登录、注册、公开查询;/admin/**下的全部接口需要管理员权限,可以自定义一个@RequireAdmin注解,用拦截器判断当前用户的角色。
5.2 给第三方接口加签名校验
如果项目里有对外提供数据的接口,比如给合作商查询线路信息,就不能只靠 token。常见的做法是 AppKey + AppSecret + 签名。AppKey 标识身份,AppSecret 用于签名,请求参数加上时间戳,服务端用相同规则计算签名并比对。这样可以防篡改;时间戳还可以用来防重放,比如超过5分钟的请求直接拒绝。
这个点在简历项目里非常亮眼,因为大部分毕业设计不会考虑到接口对接方的安全需求。实现也不复杂:一个拦截器,从 header 里取 AppKey、Sign、Timestamp,查库拿到 AppSecret,按约定规则拼接字符串、MD5 或 HMAC 签名,比对一致才放行。
5.3 文件上传:扩展名校验是最弱的防线
旅游后台要上传景点图片、线路封面图,用户要上传头像,这些都涉及文件上传安全。很多人只做了前端扩展名校验,比如.jpg .png就放行,这是非常危险的。攻击者完全可以把一个包含恶意代码的文件改成 .jpg 后缀上传。
我实际项目里做的校验包括四层:
第一,文件大小校验。SpringBoot 里用 MultipartFile 的 getSize() 判断,超过限制直接拒绝,同时配合 yml 里的 max-file-size 做统一限制。
第二,扩展名白名单校验。判断文件名的后缀必须在.jpg .jpeg .png .gif .pdf等白名单里。
第三,文件头魔数校验。这是最有效的一层。图片文件有自己的二进制头部标记,比如 JPEG 文件前三个字节是FF D8 FF,PNG 文件前八个字节是89 50 4E 47 0D 0A 1A 0A,PDF 文件通常是25 50 44 46(即 %PDF)。读取文件前几个字节判断是否匹配,能拦截大多数“改后缀”的攻击。
第四,重新命名存储。不要用用户上传的原始文件名保存,而是用 UUID 或时间戳生成新文件名,扩展名从白名单里取。这样即使文件内容有问题,也不会因为文件名中的特殊字符引发 XSS 或路径穿越问题。
5.4 PDF/XSS:一个容易被忽略的安全坑
“SpringBoot 解决 PDF XSS 攻击”这个问题在搜索热度里出现过很多次,说明有不少人在文件上传和预览环节遇到过麻烦。PDF 文件本身可能内嵌 JavaScript,如果网站直接在浏览器中预览用户上传的 PDF,脚本可能会在 PDF 上下文里执行,带来跨站脚本攻击风险。另外,文件名的处理不当,也可能把<script>标签以 HTML 形式反射到页面上,形成反射型 XSS。
对策的思路是把“文件存储”和“文件展示”分开隔离:
- 存储时只允许特定扩展名,并校验文件头;
- 文件名统一重命名为随机字符串,原始文件名如果一定要展示,输出时对文件名做编码处理,使用 URLEncoder 或 Content-Disposition 的标准格式;
- PDF 预览不要直接用浏览器原生打开,而是使用 pdf.js 这类可控的渲染库,关闭不必要的特性;
- 后台富文本编辑器上传内容时,用 Jsoup 做白名单过滤,把
<script>、事件属性等标签直接清理掉。
5.5 密码存储和其他敏感信息
用户密码不要用 MD5 或 SHA 直接存储,更不能用明文。推荐使用 BCrypt 加密。Spring Security 里自带的 BCryptPasswordEncoder 可以单独拿出来用,注册时加密,登录时 matches 校验。BCrypt 的特点是慢哈希,自带盐值,同样的密码每次加密结果都不同,暴力破解成本高很多。
管理后台展示用户手机号、身份证等敏感信息时,记得脱敏。比如手机号中间四位用*代替。后端返回给前端的字段,不要让敏感字段出现在接口响应里,该用 VO 的地方必须用 VO,不要直接把 Entity 转成 JSON 返回。
6. 从本地跑通到上线:Jar包、Docker、资源映射和中间件适配
6.1 打包是小事,但很多人第一遍都会踩坑
本地开发运行没问题,部署到服务器就用mvn打成可执行 jar:
mvn clean package -Dmaven.test.skip=true打包完成后,在target目录下会生成一个可执行的 jar。上传到服务器,然后用下面这条命令启动:
nohup java -jar travel.jar --spring.profiles.active=prod > logs/app.log 2>&1 &这里面的几个点要注意:
第一,--spring.profiles.active=prod指定使用生产环境配置,application-prod.yml 里应该配置生产数据库、Redis 地址和密码,而不是开发环境配置。
第二,nohup和&组合让应用在后台运行,日志输出到 logs/app.log,避免关掉终端后进程就没了。
第三,服务器时区问题。建议在启动命令或者 Dockerfile 里显式加上-Duser.timezone=Asia/Shanghai,否则 JVM 默认取服务器系统时区,时间对不上日志都排查不清楚。
6.2 Docker 部署:写一份干净的Dockerfile
现在很多服务器的部署环境都要求容器化,SpringBoot 项目打成镜像也很简单。下面是我项目里用的 Dockerfile,很小很干净:
FROM openjdk:8-jdk-alpine ENV TZ=Asia/Shanghai COPY travel.jar /app.jar ENTRYPOINT ["java", "-Duser.timezone=Asia/Shanghai", "-jar", "/app.jar", "--spring.profiles.active=prod"]构建镜像:
docker build -t travel-service:1.0 .启动容器,同时做端口映射和目录挂载:
docker run -d --name travel-service \ -p 8080:8080 \ -v /data/upload:/data/upload \ -v /data/logs:/data/logs \ travel-service:1.0挂载 upload 目录非常关键。旅游网站有大量上传的图片、PDF 文件,如果这些文件存在容器内部,容器删掉重建后数据就全没了。挂载到宿主机目录后,无论容器怎么重建,文件都还在。
6.3 上传文件的访问:SpringBoot资源映射
上线后前端页面要能访问上传的图片,需要一个 HTTP 路径映射到服务器上的文件目录。SpringBoot 里通过 WebMvcConfigurer 实现:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:/data/upload/"); } }这样用户在浏览器访问http://你的域名/upload/xxx.jpg,就会映射到服务器/data/upload/xxx.jpg路径下的文件。生产环境下这个路径不要放在应用目录内部,数据和应用分离,后续升级、迁移都方便。
6.4 非内嵌Servlet容器的适配
有些项目现场会指定应用必须部署到某个中间件上,而不是用 SpringBoot 内嵌的 Tomcat。比如有人问过“改成信创的话,是否需要东方通的 TongWeb”。这个问题从技术上回答很直接:SpringBoot 项目默认打成 jar 用内嵌 Tomcat 运行,但 SpringBoot 完全支持打成 war 包,部署到外部 Servlet 容器。
如果你遇到这样的要求,改动点主要有三个:
第一,pom.xml 的打包方式改为 war:
<packaging>war</packaging>第二,排除内嵌 Tomcat 依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>第三,启动类继承 SpringBootServletInitializer:
@SpringBootApplication public class TravelApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(TravelApplication.class); } public static void main(String[] args) { SpringApplication.run(TravelApplication.class, args); } }然后在外部容器里部署 war 包即可。这类中间件通常实现了标准的 Servlet 规范,SpringBoot 项目迁移过去的主要兼容性差异,集中在 Servlet API 版本、WebSocket 支持、部分 Tomcat 私有 API 的依赖上。如果项目里用到了这些能力,开工前先和现场确认中间件的版本,再针对性调整。
把整个项目从头到尾做一遍之后,我最大的感受是:所谓“功能齐全”,不是把功能列表堆得越长越好,而是每一个用户能触达的操作链路都得是闭环的。注册之后能登录,下单之后能支付,支付之后订单有状态,评价之后后台能看见。这些看不见的“链路完整性”,才是项目真正值钱的地方。
如果你也是拿这个题目做毕设或简历项目,我的建议是先按照这篇里的思路,把数据库表和订单状态机画清楚,然后按“用户端主链路优先、管理端补充、安全加固最后”的顺序推进。等第一版完整跑通,再考虑加秒杀、加推荐、加更复杂的技术组件。先把地基打牢,后面加什么东西都不慌。
本文还有配套的精品资源,点击获取