
做汽车资讯网站这个项目前后花了大概一个月时间。技术栈就是标题里写的Spring Boot前端配合Vue做了个前后端分离的架构数据库用MySQL热点数据用Redis缓存。整体做下来从需求拆解到数据库设计从接口开发到上线部署踩了不少坑也积累了一些实打实的经验。这篇博文我把整个系统设计实现的过程完完整整梳理一遍从为什么这么选型到每个核心模块怎么落地再到实际开发中遇到的坑和解决办法都写出来分享给大家。这套东西比较适合两类人看一类是准备做Java毕设的同学尤其是选题围绕“资讯类网站”或“内容管理系统”的可以直接参考里面的表结构和代码分层另一类是刚接触Spring Boot不久想独立完成一个完整Web项目的开发者跟着走一遍基本能摸清一个真实项目从0到1要经历哪些环节。那些项目结构怎么分、接口怎么设计、缓存怎么加、定时任务怎么写在这里都有明确的答案比零零散散刷教程要高效得多。1. 项目整体设计先拆需求再选技术1.1 核心需求拆解前台展示、后台管理、车型库三块做系统设计第一件事不是写代码而是把需求掰开揉碎。汽车资讯网站表面看是个内容站其实细拆下去核心需求可以分成三条线用户能看的前台、运营能管的后台、车型数据的库。前台这部分用户进网站打开首页看到的是头条推荐、最新资讯、热门车型这些聚合内容。点进资讯详情页要有图文混排的文章展示、标签分类、相关推荐还要支持点赞、收藏、评论。这些交互功能虽然看起来常规但每个都是独立模块牵扯到表结构、接口、缓存策略的设计。车型库则是另一条独立业务线用户要能按品牌筛选、按价格区间筛选、按级别轿车、SUV、MPV筛选还要能查看车型的详细参数。后台管理这块容易被人忽略其实工作量不小。运营人员需要发布资讯、编辑车型参数、审核评论、管理用户、封禁恶意账号。资讯发布还要支持定时排期——编辑提前准备好稿子设定发布时间系统到点自动发布。这背后需要一个独立的定时任务模块来支撑。把这些需求列完之后系统的功能边界就清楚了内容展示模块资讯、车型库、用户模块注册、登录、评论、收藏、后台管理模块资讯管理、车型管理、用户管理、审核、支撑模块搜索、缓存、定时任务、文件上传。有了这个整体地图后面写代码才不会东一榔头西一棒子。1.2 技术选型逻辑为什么是Spring Boot Vue MySQL Redis技术选型不能只看“哪个火选哪个”要看业务特点和团队情况。这个项目选择Spring Boot Vue前后端分离架构有几个非常实际的原因。Spring Boot最大的优势是“开箱即用”。内置Tomcat用SpringBootApplication一个注解就能起服务自动装配机制省掉大量XML配置生态成熟Spring Security、MyBatis、Redis的集成方案都有成熟文档遇到问题搜一下基本都能解决。在这类资讯系统的场景下Spring Boot做后端接口服务能非常高效地支撑RESTful API的开发和迭代。前端选Vue而不是传统的Thymeleaf模板是考虑到资讯站是内容密集型应用页面交互多、异步请求频繁。前后端分离以后前端负责展示和交互后端只出数据两边可以并行开发。而且Vue生态的Element UI、Vue Router、Axios这些工具链都很成熟搭后台管理界面效率极高。数据库这块用了MySQL Redis的组合。MySQL存业务核心数据——用户、文章、评论、车型参数事务和关系查询靠它保证。Redis则用来处理热点数据的缓存——首页头条、热门榜单、车型筛选结果这些高频读操作的数据查询一次后直接放RedisResponse Time从几百毫秒降到几毫秒体验差异明显。文件存储用的是本地磁盘加Nginx静态映射没上云存储服务因为毕设或中小型项目这么做成本最低部署也最简单。这套组合还有一个隐性好处面试或答辩的时候每一层技术栈都能讲出明确的应用场景和设计理由而不是“我用了但是不知道为什么用”。1.3 数据库表设计五张核心表的关联关系数据库表设计是系统的地基这块设计不合理后期改动成本非常高。我最终把核心表拆成五张外加几张辅助表。用户表sys_userID、用户名、密码BCrypt加密存储、昵称、头像URL、手机号、角色编码ROLE_ADMIN / ROLE_USER、状态正常/禁用、创建时间。角色直接冗余在用户表里没有单独做用户角色关联表因为对这个项目来说用户角色只有两种用一张关联表反而过度设计。资讯文章表car_article这是核心表。ID、标题、摘要、封面图URL、正文内容TEXT类型、分类ID、作者ID、浏览量、点赞数、评论数、发布状态0定时发布、1已发布、2下架、创建时间、发布时间。其中有个细节浏览量不能每次请求都UPDATE数据库会带来巨大的写压力后面的优化方案是把点击量先写Redis再定期同步到MySQL。分类表car_categoryID、分类名称、分类编码如NEW_CAR、TEST_DRIVE、INDUSTRY、排序号、状态。分类表用编码做唯一标识不要用中文名称方便前端做路由映射。车型库表car_modelID、品牌、车系、车型名称、级别SUV/轿车/MPV、指导价起、指导价止、发动机参数、变速箱、尺寸参数、上市时间、状态。车型参数是不固定结构的每款车可能参数都不一样所以我把可变参数设计成JSON字段存储查询时如果需要筛选再用MySQL的JSON_EXTRACT函数提取避免为了十几款测试车建几十个字段。评论表car_commentID、文章ID、用户ID、父评论ID支持楼中楼回复、评论内容、点赞数、状态正常/待审核/删除、创建时间。收藏表car_favoriteID、用户ID、目标类型文章/车型、目标ID、创建时间。用目标类型字段统一支持收藏文章和收藏车型一张表搞定。关联关系上文章表和分类表是多对一一篇文章属于一个分类文章表和用户表是多对一一个作者有多篇文章评论表和文章表是多对一收藏表通过目标类型字段实现多态关联。辅助表还有文章标签关联表、后台操作日志表等每个都有自己清晰的使用场景不会冗余。2. 后端分层与核心模块实现细节2.1 项目结构落地controller-service-mapper三层怎么分Spring Boot项目结构直接决定后期维护体验。我采用的是标准的controller-service-mapper三层结构包路径先按业务模块分再按技术层分com.example.autonews ├── controller # 接口层接收请求、参数校验、返回结果 │ ├── ArticleController │ ├── ModelController │ ├── UserController │ └── AdminController ├── service # 业务层核心逻辑都在这里 │ ├── ArticleService │ ├── ModelService │ ├── UserService │ └── CommentService ├── mapper # 数据访问层MyBatis接口 │ ├── ArticleMapper │ ├── ModelMapper │ ├── UserMapper │ └── CommentMapper ├── entity # 数据库实体类 ├── dto # 接口传输对象避免直接暴露实体 ├── config # 配置类跨域、拦截器、Redis配置等 ├── common # 公共类统一返回值、异常处理、工具类 └── task # 定时任务类这里最关键的认知是Controller要瘦Service要胖Mapper要专一。Controller只做三件事——接收参数、做基础校验非空、格式、调用Service返回统一结果对象Service承载真正的业务逻辑比如发布文章时的状态流转、评论时的敏感词过滤、收藏时的幂等校验Mapper只做数据库查询每一行SQL只解决一个数据操作问题。遵循这个原则接口层代码会非常干净后续加功能只改Service不会碰Controller。员工模块和文章模块还有个经验DTO和Entity分离。直接拿Entity返给前端会暴露不想暴露的字段比如用户密码的Hash值而且Entity字段变更会直接影响接口结构。所以我在Controller和Service之间加了一层DTO做隔离比如ArticleVO携带标题、摘要、封面、作者昵称、发布时间但绝不含正文字段——正文只在详情页接口返回列表页不需要省流量也减少解析压力。2.2 资讯内容模型文章、分类、标签如何组织资讯内容建模是这类网站的核心难点。文章、分类、标签三者的关系我的最终设计是“一篇文章属于一个分类但可以拥有多个标签”。分类是垂直维度的归口新车资讯、试驾评测、行业动态标签是水平维度的主题关联新能源、智能驾驶、国产车两者混用会让数据查询混乱所以在设计之初就把它们拆开了。表结构上分类直接存文章表的category_id字段一个字段解决问题查询某分类下文章直接WHERE category_id ?。标签则用中间表实现因为是多对多关系文章标签关联表article_tag_relation里存文章ID和标签ID标签基础表tag里存标签名称和引用次数。每篇文章发布时把前台勾选的标签ID批量插入中间表。为什么不用逗号分隔的字段存标签比如“新能源,智能驾驶,国产车”存在一个字符串字段里。我前面试过这种方案确实简单但查“所有带新能源标签的文章”时只能LIKE匹配无法精准取数。后来数据量到了几千篇这个查询就开始卡只能重构。中间表方案虽然多一张表、多几行代码但支持精准的标签筛选后期做推荐系统、相关文章匹配也能直接复用。文章列表接口和详情接口的查询逻辑是这个模块的核心。列表接口需要返回文章的基本信息作者昵称分类名称用SQL JOIN一次查出来详情接口需要处理浏览量自增、上一条下一条的跳转、相关推荐同分类或同标签这几件事。我把同一篇文章的关联内容查询都收敛到ArticleService的几个方法里前端一次性调用聚合接口避免出现一个页面请求十几次接口的“瀑布请求”现象。2.3 用户体系设计JWT认证与三级权限控制资讯网站的用户权限模型我设计成三级游客只能浏览内容注册用户可以评论、点赞、收藏管理员可以进后台做内容管理和用户管理。三级权限用Spring Security JWT的方案来实现无状态认证。JWT方案的核心流程是用户登录成功后后端用SecretKey签发一个JWT Token返回给前端前端存在localStorage里每次请求在Authorization请求头里带上Bearer token后端通过拦截器解析Token获取用户身份。Token里只存用户ID、用户名、角色编码这三个核心信息不存密码和敏感数据过期时间设置为24小时。实现上继承OncePerRequestFilter写一个JwtAuthenticationFilterComponent public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader request.getHeader(Authorization); if (StringUtils.hasText(authHeader) authHeader.startsWith(Bearer )) { String token authHeader.substring(7); try { Claims claims JwtUtil.parseToken(token); Integer userId claims.get(userId, Integer.class); String role claims.get(role, String.class); // 将用户信息放入SecurityContext后续方法可用 UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userId, null, List.of(new SimpleGrantedAuthority(role))); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { // Token无效则放行让接口层通过注解判断是否需要登录 } } filterChain.doFilter(request, response); } }权限控制上用PreAuthorize(hasRole(ADMIN))注解标注后台管理接口这样只有管理员权限的Token才能访问。评论、点赞、收藏则需要用户登录Controller里从SecurityContext拿当前用户ID。这套方案没有Session、没有Redis存储登录态扩展性好团队协作时也容易理解。这里有个实际教训不要把JWT密钥写死在代码里。我刚开始图省事直接写了个常量后来觉得不安全改成配置文件里用Value注入部署时通过环境变量覆盖。这个习惯在真实项目中是必须的即使现在只是毕设也建议一步到位。2.4 图片上传与访问文件存储方案和踩坑点资讯网站图片是标配文章封面图、车型图片、用户头像。图片处理我踩过的最大一个坑是上传大小限制。Spring Boot默认单文件最大1MB多文件最大10MB第一次传一张手机拍的照片就直接报错。正确做法是在配置文件中调整spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB但这只是后端限制Nginx层还有一个默认限制client_max_body_size默认为1MB如果走Nginx反代这里不调大一样报413错误server { client_max_body_size 50m; location /upload/ { alias /data/autonews/upload/; } }文件存储路径的设计也花了点心思。我统一在服务器建了一个/data/autonews/upload/目录下面按日期分子目录比如20250520/文件名用UUID重命名保留原扩展名路径通过配置项注入不在代码里写死绝对路径。访问时通过Nginx映射为静态文件URL后端接口只返回相对路径如/upload/20250520/abc123.jpg前端拼上域名就能访问。这样做的好处是迁移服务器时不用改代码换静态资源服务比如迁移到OSS也可以直接复用。图片压缩我用了Thumbnailator库上传时自动把大于2MB的图片压缩到合理尺寸封面图单独生成一个宽750px的版本详情页的正文图片不做压缩保留原图清晰度。这个小步骤能显著降低服务器的存储压力和带宽开销尤其是资讯网站每天都有新文章的情况下。3. 四个关键功能从零到一的实操过程3.1 热门资讯排行榜热度算法的设计与实现资讯站首页最显眼的位置就是热门排行榜这个功能看似简单但直接用浏览量排序会有两个问题一是老文章因为发布时间长浏览量天然碾压新文章二是被刷的量影响排序真实热度体现不出来。所以热度计算需要一个综合算法。我的设计是计算热度分Hot Score公式如下HotScore 浏览量 × 0.5 点赞数 × 0.3 评论数 × 0.2然后按时间衰减避免老文章永远霸榜实际热度 HotScore / (1 已发布天数 × 0.05)举个例子一篇文章发布第1天浏览量1000、点赞20、评论10HotScore 1000×0.5 20×0.3 10×0.2 508实际热度也约等于508到了第10天再好的内容也会衰减到约290分给新内容留出上榜空间。查询时先查Redis的ZSet有序集合ZREVRANGE key 0 9 WITHSCORES取出前10名的文章ID再批量查MySQL拿文章详情。Redis的Key设计为car:article:hot_rank每10分钟重新计算一次热度分放进Redis前台访问排行接口时直接读缓存。这个方案实测性能很好1000篇文章的热度计算耗时在几百毫秒内排行榜接口的响应时间稳定在10毫秒左右比直接实时算SQL快了一个数量级。为了保障数据准确性浏览量、点赞数、评论数的变动先更新Redis里的计数再用定时任务每5分钟批量同步到MySQL。这样既保证了排序数据的实时性又避免了频繁UPDATE数据库带来的锁竞争和性能下降。3.2 定时发布功能让编辑可以提前排期定时发布是后台管理里比较有意思的功能。编辑写好了稿子设定“明天早上8点发布”系统到点自动完成发布动作——文章状态从“定时发布”变成“已发布”。实现方案我选了Spring Boot自带的Scheduled注解加一个定时任务类Component public class ArticlePublishTask { Autowired private ArticleMapper articleMapper; Scheduled(fixedDelay 5000) // 每5秒执行一次 public void publishScheduledArticles() { LocalDateTime now LocalDateTime.now(); ListArticle articles articleMapper.findByStatusAndPublishTimeLessThan(0, now); for (Article article : articles) { article.setStatus(1); article.setPublishTime(now); articleMapper.updateById(article); } } }为什么用5秒扫一次而不是用Scheduled(cron 0 0 8 * * ?)因为编辑设置的发布时间是不固定的哪天都有可能有文章要发布固定cron表达式不适合这种业务。定时任务的容错性也要考虑万一任务执行到一半服务重启已经处理完的文章状态已经改了没处理的会在下次扫描时继续处理天然幂等不会重复发布。还要注意多实例部署的问题——如果有多台服务器每台都在跑定时任务就会重复扫描。解决方式是加一个分布式锁比如Redis的SETNX或者用SchedulerLock注解中小项目用Redis锁更直接。这个功能在整个系统里不算复杂但它串联了配置、定时任务、数据库状态流转三个知识点做完以后对Spring Boot的任务调度机制理解会深很多。3.3 资讯搜索从LIKE到全文索引的演进搜索功能按数据量分阶段实现。一开始文章几百篇直接用MySQL的LIKE查询完全够SELECT id, title, summary, cover FROM car_article WHERE (title LIKE CONCAT(%, #{keyword}, %) OR summary LIKE CONCAT(%, #{keyword}, %)) AND status 1 ORDER BY create_time DESC这样写的好处是简单、直接、不引入额外组件几百条数据下性能毫无压力。但数据到几千篇之后LIKE的模式匹配会放弃索引走全表扫描关键词稍微热门一点查询耗时就从几十毫秒涨到几百毫秒体验明显下滑。我当时是直接上了MySQL全文索引FULLTEXT而不是Elasticsearch原因很实际数据量还没到需要分布式搜索引擎的级别全文索引在MySQL 5.7以上的InnoDB引擎里支持中文还行配合ngram分词器可堪一用。建索引的SQL如下ALTER TABLE car_article ADD FULLTEXT INDEX ft_search (title, summary) WITH PARSER ngram;查询时用MATCH ... AGAINST ...SELECT id, title, summary FROM car_article WHERE MATCH(title, summary) AGAINST(新能源 IN NATURAL LANGUAGE MODE) AND status 1做这个设计决策时我自己心里也有评估如果未来文章量到了十万以上、或者需要更精准的中文分词和相关性打分再迁到Elasticsearch不迟。但当前业务场景下MySQL全文索引已经把性能问题解决掉了而且实现成本极低。技术方案的选择不是越重越好而是匹配业务规模就是最好的。3.4 接口防刷与安全签名上线前的必要措施资讯网站面向公网防刷和安全这块不做上线第一天就可能被脚本扫掉几个接口。我做了两道防护IP防刷和接口签名。IP防刷用拦截器实现。同一IP在1分钟内请求超过120次的直接拒绝访问返回“请求过于频繁”。实现上用一个ConcurrentHashMap加滑动窗口记录每次请求算一下当前窗口内的请求次数超过阈值就拦截。Redis也有类似方案但考虑到请求频率高直接Map的性能更好只在多实例部署时才需要迁移到Redis。接口签名主要针对的是API防重放。方式是在请求头携带时间戳、随机数、签名三个参数签名算法是MD5(请求路径 请求时间戳 随机数 Salt)。后端先校验时间戳不能过期比如5分钟内再校验签名是否正确最后用Redis的SETNX校验随机数是否已使用过如果重复则判定为重放请求。这套方案对需要登录态的接口评论、收藏、点赞尤其有用能挡住一大部分脚本自动刷评论和点赞的请求。public class SignatureInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String timestamp request.getHeader(X-Timestamp); String nonce request.getHeader(X-Nonce); String signature request.getHeader(X-Signature); // 校验时间戳在5分钟内 if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) 5 * 60 * 1000) { throw new BusinessException(请求已过期); } // 校验随机数未使用过 Boolean firstUse redisTemplate.opsForValue() .setIfAbsent(nonce: nonce, 1, Duration.ofMinutes(5)); if (Boolean.FALSE.equals(firstUse)) { throw new BusinessException(重复请求); } // 校验签名 String expected Md5Util.encode(request.getRequestURI() timestamp nonce SALT); if (!expected.equals(signature)) { throw new BusinessException(签名校验失败); } return true; } }这套方案做起来不算复杂但它体现了一个完整的”接口安全”思路——防刷、防重放、防盗刷是项目答辩和面试时可以重点讲的加分项。4. 开发实测中的踩坑记录与解决方案4.1 Spring Boot版本选择别一上来就追最新版这是最想分享的一个坑。开发之前我习惯性去看最新版本发现Spring Boot 3.x已经发布了Spring Framework 6.0最低要求Java 17感觉挺先进就打算用。结果一写代码就遇到问题Spring Boot 3把javax.*里的包名换成了jakarta.*比如javax.servlet变成jakarta.servlet很多老教程、老代码里的导入语句直接报错查资料也不方便。更麻烦的是团队的JDK版本还不统一有人是JDK 8有人是JDK 11Spring Boot 3要求最低Java 17有人得先换环境。最后我果断回退到Spring Boot 2.7.x这个版本在Java 8和Java 11下都能跑生态里面绝大多数依赖兼容性都没问题网上各种教程、排错资料也是最多的做项目和毕设完全够用。关于版本的决策我的经验是不用非追最新版稳定且生态兼容性好才是关键。Spring Boot 2.7.x和3.x的核心差异在于底层的Spring Framework版本和Jakarta命名空间对于写业务代码的开发者来说2.7完全够用等真正需要升级的好处时再迁移不迟。4.2 文件上传大小限制前后端两个地方都要改前面提到过文件上传这块有双层限制。后端改了Spring Boot的multipart配置max-file-size结果上传时又报413。查了一圈问题出在Nginx层——它的默认client_max_body_size也是1MB。这个配置在Nginx的http块、server块里都能设置我最后在server块里设置了50m因为这样只影响该站点的上传请求不影响其他站点。还有一个隐藏点如果是前后端分离部署前端页面和后端API在不同的域名下上传请求是跨域的你要确保跨域配置CORS里允许了PUT和POST方法并且正确处理了OPTIONS预检请求。否则前端会报跨域错误看起来像上传失败实际上是请求在真正发出前被浏览器拦截了。这个坑排查了半天印象很深。4.3 跨域问题的真实处理方案说到跨域这是前后端分离开发中每个人都绕不过去的坎。我的处理方案很直接在后端配置一个CorsFilter统一处理所有跨域请求。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:8080); config.addAllowedOrigin(http://localhost:3000); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); config.addExposedHeader(Authorization); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }细节上要注意addAllowedOrigin不用*尤其setAllowCredentials(true)时用通配符会被浏览器拒绝要精确列出允许的前端地址。如果前端地址是多环境本地、开发、测试可以在配置类里统一维护不要散落各处。我在本地开发时用http://localhost:3000部署时换成正式前端域名这个配置类正好集中管理了这些变化。4.4 MyBatis集成时的三个隐藏坑MyBatis集成在Spring Boot项目里是个高频踩坑点我遇到并解决过三个特别隐蔽的问题。第一个是Mapper接口扫描路径问题。MapperScan注解的basePackages没写对或者遗漏了XML文件的路径配置就会报“Invalid bound statement (not found)”错误。解决方式是确认MapperScan(com.example.autonews.mapper)指向了正确的包路径同时在application.yml中显式配置XML路径mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.autonews.entity第二个是返回值为Map时的字段映射。MySQL查询结果默认是下划线命名比如create_time而Java实体的属性是驼峰命名createTime。如果没开启驼峰映射MyBatis封装后的Map里全是下划线key前端拿到的是不统一的字段名。开启方式是在配置里加mybatis: configuration: map-underscore-to-camel-case: true第三个坑比较阴——SELECT查询没查到数据时MyBatis返回null和返回空集合的行为不一致。当Mapper方法的返回值是ListArticle时无论查询结果为空还是nullMyBatis都会返回一个空List不会返回null但返回值是单个Article对象且结果为空时方法返回null。很多刚接触的人在这上面吃过头直接调用article.getTitle()没做null判断结果空指针。所以写Service层时要习惯对所有单品查询的结果做Optional或null判断处理。5. 部署上线从开发环境到服务器运行的完整链路5.1 多环境配置分离开发、测试、生产三个环境互不干扰项目开发到后期环境配置变得复杂。开发环境连的是本地MySQL数据库地址localhost:3306Redis也是本地文件存到项目根目录生产环境连的是服务器的MySQL和Redis路径也完全不同。如果在代码里写死配置每次部署都要改代码这是最低级的错误。我的做法是Spring Boot标准的多环境配置application.yml作为公共配置里面放固定的数据源、Redis配置引用application-dev.yml、application-prod.yml分别放开发和生产环境的差异项。# application-prod.yml server: port: 8080 spring: datasource: url: jdbc:mysql://your-server-ip:3306/autonews?useSSLfalseserverTimezoneAsia/Shanghai username: prod_user password: ${DB_PASSWORD} redis: host: your-server-ip port: 6379数据库密码不写明文用${DB_PASSWORD}环境变量占位启动时从服务器环境变量动态注入。还有一个很容易被忽略的细节MySQL连接串里的serverTimezone参数必须带上否则在服务器时区与本地不同时会报连接超时错误。生产环境我指定的时区是Asia/Shanghai统一用东八区时间避免时间显示偏移的问题。5.2 打包、启动、进程守护上线过程的关键操作Spring Boot项目的打包非常便捷Maven打成jar包服务器上安装好JDK直接java -jar就能运行。但直接跑在前台的问题是不安全SSH一断开服务就停了。所以必须用nohup后台运行把日志输出到文件方便排查问题。nohup java -Xms256m -Xmx512m -jar autonews-server.jar \ --spring.profiles.activeprod \ /data/logs/autonews/app.log 21 -Xms256m -Xmx512m是JVM初始堆大小和最大堆大小对中小型项目来说这个配置合理避免内存占用过大导致服务器崩溃。--spring.profiles.activeprod指定的就是我们刚才说的生产环境配置。日志输出到指定路径后排查问题时直接tail -f看日志就行。如果服务器会重启建议用systemd服务来管理进程写一个autonews.service文件让服务在开机时自动启动、崩溃时自动拉起比手动nohup更可靠。这个上线过程我踩过的坑是打包时忘记排除测试代码的配置文件导致连了测试环境的数据库后来在pom.xml的finalName设计上统一规范了一下jar包命名并确保application-prod.yml在打包产物里正常被读取。部署上线一次之后整条链路就非常顺了。最后的个人体会做这个汽车资讯网站系统设计实现的项目最大的收获不是某一个具体的技术点而是把“完整系统”的认知链条打通了从需求拆解到数据库设计从技术选型到环境配置从编码实现到部署上线是一条连续的路每一环节的错误都可能在后期以意想不到的代价呈现。如果你也在做类似的项目我最想给的建议是——动手写代码之前一定要花时间把表结构和模块边界设计清楚这两个思考做得越扎实写代码的速度越快返工的次数越少。占位符里那些运行时遇到的坑能提前避开的尽量避开网上搜得到的方案多实践几次就成了自己的肌肉记忆。祝大家都能顺利跑通自己的项目有问题欢迎在评论区交流一起把这套方案打磨得更好用。