ARTICLE DETAIL

资讯详情

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

Spring Boot旅游社交分享系统毕设开发实战:从数据库设计到部署上线

Spring Boot旅游社交分享系统毕设开发实战:从数据库设计到部署上线 1. 写在前面这个题到底在做什么每年毕业季Java Web方向的毕设题目里“旅游社交分享系统”“宠物社交系统”“短视频社区平台”这类名字永远占半壁江山。你搜一下就能发现各种“基于Web的XX系统设计与实现”满天飞源码、论文、部署视频一条龙。但我得先说一句大实话标题看着花哨本质上都是在做同一套事——用户注册登录、内容发布、互动点赞、关注私信再加一个后台管理。你把这个核心吃透了换什么“旅游”“宠物”“美食”前缀都只是换皮。这个题目真正的价值在于它覆盖了Java后端开发的完整链路Spring Boot/SSM框架整合、MySQL表结构设计、文件上传与展示、分页查询、用户权限控制、部署上线。做完这一个项目你简历上能写的东西至少有三个模块。所以别被“旅游社交”这几个字迷惑它只是一个承载场景背后那套CRUD加业务关系的设计才是你要掌握的硬功夫。这篇文章我不讲虚的直接按我自己的开发流程走一遍需求分析怎么做、表结构怎么设计、核心代码怎么写、部署时踩过哪些坑、答辩老师一般盯哪里。你在网上买的全套源码大概率不会教你这些但这些东西才是你真正毕业和面试时用得上的。适合谁来读如果你选了这个题或者正在纠结类似的社交类Web毕设并且不想只做一个会敲启动按钮的工具人那你把这篇看完再对照自己手头的代码走一遍绝对比你自己闷头看三天源码有效。2. 项目整体设计与技术选型思路2.1 从题目反推需求旅游社交到底要哪些功能很多人拿到题目第一件事是搜源码这不对。第一步应该做的是角色代入这个系统里有哪些人他们分别要干什么旅游社交分享系统核心角色就两类普通用户和管理员。用户端的需求顺着日常习惯想就能列出来注册登录不然我怎么发游记、点赞别人这是所有社交产品的入场券。浏览内容首页要有推荐游记、热门景点不然新用户来了看到空页面直接走了。发布游记这是“分享”二字的落地得支持标题、正文、图片、定位。互动操作点赞、评论、收藏——没有互动就不叫社交。关注机制我关注了一个旅游达人以后他发的内容我能优先看到。个人主页展示我发过的游记、我的粉丝和关注列表。管理端要什么内容审核、用户管理、分类管理。因为这是Web公开平台不是朋友圈所以管理员必须能删除违规内容、禁用恶意账号、维护景点分类。把这些功能列完再看系统名字里那个“旅游”体现在哪无非是多了景点分类、游记关联目的地、按城市或景点检索。说白了就是在通用社交模型上加了一层旅游数据维度。想明白这一点你就不会被题目唬住——你不是在做一个旅游产品你是在做一个带旅游标签的社区。2.2 技术栈选择的底层逻辑技术方案这部分学校往往没硬性要求但老师心里是有偏好。我见过最高频的组合是这几种方案后端前端特点适合人群方案ASpring Boot MyBatis-PlusVue Element UI前后端分离目前企业主流有一定基础愿意多花时间方案BSpring Boot JPA/MyBatisThymeleaf模板前后端一体部署简单想省事专注Java逻辑方案CSSMSpringSpringMVCMyBatisJSP页面老派经典教程多学校强制SSM时选我个人的建议是方案A理由有三个。第一Spring Boot本身就是你毕业后工作的主流技能现在写简历全是Spring Boot没几个人写SSM了。第二前后端分离的模式更接近真实项目答辩时老师问你“为什么用Vue”你可以理直气壮回答因为前后端并行开发、后期维护方便这是一个加分项。第三MyBatis-Plus帮你去掉大量重复的CRUD代码让你把时间花在业务逻辑上。但如果你Java基础一般或者剩下的时间不到一个月我建议你选方案B。Thymeleaf和Boot整合后Java代码和页面在一个工程里部署就是一个jar包的事少操心跨域、Nginx这些额外的东西。毕设的目标是顺利毕业不是炫技。Java版本、构建工具这些细节我用的是Java 8 Maven。别追新用Java 17或Gradle没必要。Boot版本用2.7.x稳定MyBatis-Plus用3.5.x网上资料最多遇到问题一搜就有答案。2.3 为什么最终方案要锁定“小而全”有些同学喜欢把方案设计得很庞大Redis缓存、RabbitMQ消息队列、ElasticSearch搜索全塞进去。我劝你别这么干。毕设答辩的老师学历和水平都不低你写个Redis缓存引出来的一堆问题缓存一致性、缓存穿透、序列化方式就能把你问趴下。这个题目本身是个单体应用所有功能放在一个Spring Boot工程里就完全够用。旅游社交系统的体量决定了它的瓶颈不在并发而在功能完整性所以正确的策略是“小而全”技术栈不追求新但功能边界要完整覆盖用户从注册到发布再到互动的全流程。一句话总结我的设计原则能用一张表解决的绝不用两张表能用简单循环解决的绝不引入中间件凡是引入额外组件都要能解释清楚“没有它会怎样”。这个原则保了我写论文时少挨很多骂。3. 数据库设计与核心表结构解析3.1 从业务名词到数据库表的映射数据库设计是毕设的地基地基建歪了后面所有代码都跟着别扭。我的做法是先写实体类再反推表而不是一上来就画E-R图。你先把用户、游记、评论、点赞、关注、收藏、分类这些名词写出来然后逐个问这个实体和别的实体是什么关系用户和游记一对多。一个用户能发多篇游记一篇游记只属于一个用户。 游记和点赞/评论/收藏一对多。这三者都依赖于游记存在。 用户和用户关注关系多对多。自己设计一张关注表存follower_id和followed_id简单直接。 游记和分类/景点多对一。一篇游记归属一个目的地分类。理完关系建表就顺了。我这里直接给出我建的一套核心表结构网上买的源码也基本是这个套路你对比着看。user用户表核心字段id主键自增username用户名唯一索引password密码存加密后的值nickname昵称用于页面展示avatar头像URLgender、signature个人资料扩展create_time注册时间后面做统计有用travel_note游记表核心字段id主键user_id外键关联user表发布者title标题不能太长限制60字以内content正文内容用TEXT类型cover_image封面图URL首页列表展示用destination目的地比如“云南丽江”category_id关联分类表view_count浏览量status状态字段0待审核、1已发布、2已驳回create_time、update_timecategory分类表就更简单id、name、description三件套。注意分类不要做得太深两级就够比如“国内游—云南”再深页面没法展示。comment评论表id、note_id、user_id、content、create_time。 like_record点赞表id、note_id、user_id、create_time。这里一定要给note_id和user_id加联合唯一索引保证一个人对同一篇游记只能点一次赞这个细节我后文还会讲到。 follow关注表id、follower_id关注者、followed_id被关注者、create_time同样加联合唯一索引。 message私信表id、from_user_id、to_user_id、content、is_read、create_time。3.2 建表SQL怎么写得漂亮我把最重要的几张表建表SQL贴出来你拿去改改就能用。注意字符集和排序规则这一步很多人忽略导致后面中文乱码和排序异常。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, gender tinyint(1) DEFAULT 0 COMMENT 性别0未知 1男 2女, signature varchar(200) DEFAULT NULL COMMENT 个性签名, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;CREATE TABLE travel_note ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, title varchar(60) NOT NULL, content text COMMENT 游记正文, cover_image varchar(255) DEFAULT NULL COMMENT 封面图, destination varchar(50) DEFAULT NULL COMMENT 目的地, category_id bigint(20) DEFAULT NULL, view_count int(11) DEFAULT 0, status tinyint(1) DEFAULT 1 COMMENT 0待审核 1已发布 2已驳回, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT游记表;这里有个容易被忽略但很关键的设计status字段。很多初写者不设计状态直接上架结果发现程序无法下架违规内容只能物理删除而删除会导致评论、点赞、收藏这些关联数据变成孤儿数据。加一个状态字段下架就是update一条SQL的事这才是正经做法。3.3 关联表中联合唯一索引的重要性点赞表是网上很多源码做得最敷衍的地方。你随便搜一份源码可能它的点赞表就没有唯一约束导致用户可以无限点赞刷热度而你自己测试的时候因为点得慢根本发现不了。CREATE TABLE like_record ( id bigint(20) NOT NULL AUTO_INCREMENT, note_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_note_user (note_id,user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT点赞记录表;联合唯一索引uk_note_user就是兜底防线。你在代码里先查一遍有没有点过赞再允许点赞这属于业务层校验但万一并发请求下两次查询同时通过呢有了这个唯一索引数据库会直接拒绝第二条插入报DuplicateKeyException你再捕获这个异常返回“你已点赞过”这套组合拳写出来答辩时老师问“你怎么防止重复点赞”你把这个流程说出来绝对是好印象。4. 核心模块设计与业务逻辑实现4.1 登录注册JWT还是Session登录模块是每个答辩老师都会看的。方案无非两种Session方案和Token方案JWT。Session方案是传统做法登录成功后在服务端存一份用户状态给浏览器发一个带sessionId的Cookie用户后续请求带这个Cookie来识别身份。优点是实现简单只要Spring Security或者拦截器里getSession就行不需要额外依赖。缺点是前后端分离时跨域处理比较麻烦你后端是8080端口前端Vue跑在8081端口Cookie默认不跨域你得配置一堆allowedOrigins、allowCredentials参数搞不好还会踩Cookie丢失的坑。JWT方案是现在的主流。用户登录后服务端生成一个签名字符串返回给前端前端把它存在localStorage里每次请求在Authorization请求头里带上后端拦截器解析这个Token就能知道是谁。不依赖Cookie完美适配前后端分离。我用的就是JWT具体实现依赖jjwt库// 生成Token public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) // 7天过期 .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }Token过期时间设为7天比较合理。太短了用户玩一会儿就被踢下线体验很烂太长又不安全。7天是社区类产品的常见选择答辩时被问到还能扯一句“参考了主流社交产品的会话时长设计”显得你有产品思维。拦截器里解析Token就不用说了无非是try-catch解析解析失败就返回401让前端跳登录页。注意JWT的secret不要写死在代码里放到application.yml配置文件中这算一个容易被忽略但答辩会挑的小毛病。4.2 发布游记事务、图片上传与内容安全发游记是整个系统最核心的交互操作这个接口涉及的东西最多图片上传、正文保存、浏览量初始化。先看流程用户在前端填标题、写正文、传图片、选分类和目的地点提交后后端依次执行校验参数标题不能为空、内容不能太短这是后端必须做的前端校验只是用户体验。处理图片接收MultipartFile保存到本地服务器指定目录返回文件访问URL再存库。插入游记主记录拿到主键ID。如果一张游记有多张图片可能需要单独一张note_image表存每张图记录note_id。图片上传是毕设中踩坑重灾区。有些人直接把图片BASE64编码塞进数据库数据库瞬间膨胀页面加载卡成PPT。正确做法是图片按二进制写入服务器磁盘或云存储数据库只存访问路径。本地存储方案// 保存图片 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName UUID.randomUUID().toString().replace(-, ) ext; String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); File uploadDir new File(uploadPath / datePath); if (!uploadDir.exists()) uploadDir.mkdirs(); file.transferTo(new File(uploadDir, newFileName)); // 返回前可访问的URL路径/upload/yyyyMMdd/文件名这里有个很隐蔽的坑文件名为什么不用原名字因为用户上传的照片很可能叫“IMG_20210501_123456.jpg”这种或者干脆俩用户传了重名文件会互相覆盖。用UUID重命名就永远不会冲突。插入游记和图片这两个操作必须放在同一个事务里。如果游记插入成功但图片插入失败你不回滚就会出现一篇没有图片的空游记页面是真的难看。方法上加Transactional注解是最省事的做法。另外值得一提的是内容安全问题。既然是公开社区答辩老师一定会问如果有人发违规内容怎么办除了管理员后台能下架你还可以在发布接口里做简单的敏感词过滤或者至少把审核流程走通。哪怕你只是把status初始值设为0待审核然后在接口查询时只返回status1的数据也是一套完整的内容安全闭环。这个小设计写在论文里非常加分。4.3 首页热门游记的智能推荐与排序“推荐”这个词听起来很高端但要是真去搞协同过滤、用户画像那不是毕设该干的事。这里的推荐指的是简单的热度排序。你完全可以设计一个热度分公式hotScore viewCount * 1 likeCount * 3 commentCount * 5浏览量权重最低评论权重最高因为评论是深度互动的体现比点赞还要“铁”。这个公式简单、能解释、效果好。你甚至可以用SQL直接算出来排序SELECT n.*, (n.view_count * 1 (SELECT COUNT(*) FROM like_record l WHERE l.note_id n.id) * 3 (SELECT COUNT(*) FROM comment c WHERE c.note_id n.id) * 5) AS hot_score FROM travel_note n WHERE n.status 1 ORDER BY hot_score DESC LIMIT 10;这个SQL看着简单但它有一个性能隐患每行游记都要执行子查询统计点赞数和评论数。数据量小无所谓但如果你测试时造了几万条假数据首页接口会明显变慢。更优的做法是给travel_note表冗余两个字段like_count、comment_count每次有人点赞或评论时同步给这两字段1。这就是典型的空间换时间策略也是互联网大厂最常用的做法。我做的时候两个方案都实现了查询用count字段排序写操作时维护count。论文里把“为什么要冗余计数”写成一个小节老师看了会认为你懂性能优化。此外加一个时间衰减因子也可以新发的游记权重稍微高一点避免老游记霸榜这个属于锦上添花不做不影响毕业。4.4 关注与Feed流怎么让首页出现“我关注的人”社交系统和普通内容网站最大的不同是有一个“社交关系”在牵动内容分发。关注功能本身很简单点关注时insert一条关注记录取消就delete。但“登录后首页展示我关注的人近期发布的游记”这个需求才是关注的灵魂。实现方式也很直接写一条带子查询的SQLSELECT * FROM travel_note WHERE user_id IN ( SELECT followed_id FROM follow WHERE follower_id #{currentUserId} ) AND status 1 ORDER BY create_time DESC;这条SQL就是一个简化版Feed流。注意IN子查询的写法数据库会对子查询结果做匹配逻辑没问题。数据量大了以后这个写法性能不佳需要用JOIN改写但毕设数据量根本到不了那个级别能用、能讲清楚就行。我在评论区还收到过一个同学的问题为什么关注的人发的游记排序不是按时间而是混了很多旧内容原因就是他没有在SQL里加ORDER BY create_time DESC。别笑这种错误太常见了写代码之前先想清楚用户想看到什么想看到最新的动态就别偷懒省略排序条件。4.5 管理后台状态机与批量操作后台管理页面List页是每个Java毕设的标配但很多人写成了只读列表这不够。管理端应该有的核心操作是内容审核把status从0改为1或2、用户禁用把user表的status改为禁用状态、分类维护增删改查。我的建议是管理端一定要实现批量操作虽然批量操作只是前端勾选循环调用删除但它在展示上会很好看。注意管理员的接口权限所有后台接口都要走一次拦截器校验当前登录用户的角色是ADMIN这一步是安全底线也是答辩老师最关心的“权限控制”功能。后台页面不用搞得太花哨一个表格几个条件筛选加上弹窗编辑就够了。我用的前端组件库是Element UI的Table和Dialog组件大概半天能做完这一整块后台页面。这个效率问题不是技术问题而是乙方人多。5. 前后端接口设计与关键代码实现5.1 统一返回格式从R类开始前后端分离模式下最忌讳的就是一个接口返回一种格式。今天这个接口返回{code:0,data:{}}明天那个接口返回{success:true,result:{}}前端同事能骂死你。正确做法是写一个统一的R类也叫Result类public class RT { private Integer code; // 200成功500失败401未登录 private String message; // 提示信息 private T data; // 泛型数据 public static T RT ok(T data) { ... } public static T RT fail(String msg) { ... } }以后所有Controller的返回值都是R类型的R.ok(data)、R.fail(“参数不能为空”)。前端拿到响应后先看codecode为200再处理data。这个R类看着简单但它是整个项目代码风格的基石。很多人焦头烂额就是因为接口返回格式不统一前端到处写if判断不同结构。花十分钟写这个类后面省下十小时。5.2 分页查询接口的标准写法列表页全都要分页MyBatis-Plus自带的Page对象是最省事的GetMapping(/note/list) public RIPageTravelNoteVO list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId) { PageTravelNote page new Page(pageNum, pageSize); LambdaQueryWrapperTravelNote wrapper new LambdaQueryWrapper(); wrapper.eq(TravelNote::getStatus, 1); if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(TravelNote::getTitle, keyword) .or().like(TravelNote::getContent, keyword)); } if (categoryId ! null) { wrapper.eq(TravelNote::getCategoryId, categoryId); } wrapper.orderByDesc(TravelNote::getCreateTime); IPageTravelNote result travelNoteMapper.selectPage(page, wrapper); return R.ok(result); }注意几个细节。第一公开列表页一定默认过滤status1否则待审核的内容泄露出去了前面做的内容审核就白做了。第二关键字搜索的like条件要注意加括号就是上面wrapper.and那段不然or条件会把前面的eq条件“吃掉”导致查出审核中的数据。这个Bug我曾经在测试时翻过车写出来给大家避个雷。第三返回的数据不要直接把数据库实体丢给前端尤其是user表里的password绝对不能返给前端。所以封装一个VO类只返回必要的字段游记信息加发布者的昵称、头像。关联查询用VO类承接两个表的数据这是分层的正确姿势。5.3 点赞与取消点赞的接口设计点赞是高频操作设计时要注意接口的幂等性同一个操作执行多次结果一致。我的接口设计是POST /note/like参数noteId执行点赞逻辑DELETE /note/like参数noteId执行取消逻辑一个操作一个专用动词比那种传一个type1/2区分点赞取消的接口更清晰。点赞逻辑内部先判断游记是否存在不存在直接抛业务异常。尝试insert点赞记录捕获DuplicateKeyException异常如果捕获到了说明已经点过赞直接返回“重复操作”提示。执行游记表的like_count加一操作。这里有个细节先insert点赞记录再更新count而不是反过来。因为insert是“安全”的有唯一索引兜底而update是无脑加一。如果先加count再insert失败count就永远多了一个空涨的数字不好回滚。先insert再加count即使两个操作间出了异常导致count没加到最多只是统计数字少了1不会虚高。做互联网项目数据宁愿少不能多这个原则写代码时可以记一下。5.4 评论模块递归处理还是用嵌套查询评论功能有两种形态一级评论只支持直接回复和嵌套评论支持楼中楼。毕设做成一级评论就够了嵌套评论光前端渲染就有递归、缩进、展开收起一堆事情性价比很低。我做的是两级结构主评论parent_id为null和回复。所有直接挂在主评论下的回复查询时统一按parent_id关联主评论。前端展示时主评论下面缩进显示所有回复这样既有了“楼中楼”的效果后端又不需要递归。-- 查询某篇游记的所有主评论 SELECT * FROM comment WHERE note_id ? AND parent_id IS NULL ORDER BY create_time DESC; -- 查询某个主评论下的所有回复 SELECT * FROM comment WHERE parent_id ? ORDER BY create_time ASC;第二个查询一次只查一个主评论的回复如果一篇游记有100个主评论就要执行100次查询这就是经典的N1问题。优化方案是一次性把所有评论查出来包括主评论和回复然后按parent_id在内存里分组组装。具体实现不展开但性能优化的思路讲出来面试官会点头的。5.5 文件上传接口与前端页面联调文件上传接口本质上是接收MultipartFile并保存。但要注意三件事一是限制上传大小application.yml里配置spring.servlet.multipart.max-file-size: 10MB防止有人传一个电影文件上来把服务器磁盘塞满二是按日期分目录存储方便以后清理三是上传成功后返回的URL必须能被前端直接访问——如果你后端是8080端口前端是8081端口那么图片URL要写完整路径比如http://localhost:8080/upload/20240520/xxx.jpg不能只写/upload/xxx.jpg否则前端页面用相对路径去访问会请求到前端服务器上直接404。前端联调时的跨域问题也要处理。Spring Boot侧配置CORSConfiguration 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); } }注意allowedOriginPatterns和allowCredentials必须配合使用如果你用了allowedOrigins(*)又开了allowCredentials(true)浏览器会直接报错这两个配置放一起是冲突的。这个坑我见很多人踩过。6. 从编码到上线本地运行与服务器部署实录6.1 本地先把环境收拾干净开发之前先把环境准备好。我推荐的组合是JDK 1.8装完必须配JAVA_HOME命令行里java -version能打出来版本号。Maven 3.6配置阿里云镜像源不然下载依赖能卡到你怀疑人生。MySQL 8.0密码不要设太复杂本地开发用一个好记的就行root/root在很多毕设里是默认值。IDEA 2023版本以上自带Spring Boot插件。Node.js 16前端工程如果需要npm install的话记得把registry切到淘宝源否则又是等待两小时。不上手踩一遍永远不知道这些“准备工作”有多能消耗时间。我亲眼见过一个同学卡在Maven下载依赖三天就因为他没配镜像源。配置文件在家目录下的.m2/settings.xml里加一段mirror配置mirror idaliyun/id mirrorOfcentral/mirrorOf namealiyun maven/name urlhttps://maven.aliyun.com/repository/public/url /mirror6.2 Maven打包与jar包部署开发完成后打包最简单的做法是在项目的根目录下执行mvn clean package -DskipTests打包完成后target目录下会生成一个xxx.jar文件这就是整个后端应用的成品。测试环境运行java -jar target/travel-platform-0.0.1-SNAPSHOT.jar这里注意一个常被忽略的配置问题如果你的平台需要通过外部浏览器访问而不是本机自己访问那么server.address不能写localhostserver.port要设定好。我建议直接在application.yml里把端口固定为8080server: port: 8080打包前还需要改数据库连接配置把localhost改成你的线上MySQL地址用户名密码换成线上环境的。强烈建议把数据库连接信息放到application-prod.yml这种独立配置文件里用spring.profiles.active切换避免每次部署都要改主配置。部署到云服务器我买的是2核4G的轻量级服务器对毕设项目来说绰绰有余。使用nohup把jar跑在后台nohup java -jar travel-platform.jar --spring.profiles.activeprod app.log 21 这行命令的意思是后台运行日志输出到app.log错误输出也一并重定向。不写这个你一关SSH窗口Java进程就跟着没了哭都来不及。6.3 Nginx反代与前端静态资源部署如果你用方案A前后端分离前端代码构建后是纯静态文件。Vue项目执行npm run build后生成dist目录里面就是一堆html/js/css。部署方式很简单把dist目录整个上传到服务器然后用Nginx指过去。关键的Nginx配置server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; # 前端路由history模式必需 } # 后端API location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传文件访问 location /upload/ { alias /data/upload/; } }try_files这行是前端history模式路由的核心意思是如果请求的文件不存在就一律返回index.html让前端路由接管。没有这一行你一刷新页面就404这是最常见的Vue部署问题。API用/api/前缀转发到后端8080端口这样一个Nginx就同时管了前端和后端也顺便解决了跨域问题因为浏览器看到的是一个域下的同源请求。这也是为什么有了Nginx之后CORS配置不一定要开——但这个要结合你自己的部署方案来确定如果你没有Nginx只靠跨域配置硬顶也能用。图片上传路径和Nginx的alias路径必须对应。我上传的代码里配的是uploadPath/data/upload那么Nginx里alias就要指到同一个目录否则页面上的图片全部加载不出来。6.4 云服务器内存不够怎么办2G内存的服务器跑Java应用会出现一个问题只要是个人项目JVM默认拿它最大内存的一半左右当堆内存Spring Boot启动大概要占用300-500MB加上MySQL、Nginx物理内存就快见底了。系统会开始用到Swap整体变慢。处理方式是在启动时限制JVM内存java -Xms256m -Xmx512m -jar travel-platform.jar这个参数把堆内存最大值限制在512MB对毕设这种低并发应用完全够用但能显著降低服务器压力。你还可以排查一下是不是因为MySQL的配置太激进innodb_buffer_pool_size默认是128MB对于小服务器可以改成64MB。把这些参数写进启动脚本是部署经验中的加分项。6.5 打包注意的几个坑打包启动时最常遇到的坑我列成一张排查表你可以对着查现象原因解决方案启动报数据库连接失败配置里的URL、账户密码不对检查mysql的host、port、dbname、用户名、密码端口被占用8080端口被其他程序占用杀掉占用进程或改server.port前端图片加载不出来Nginx alias路径和上传路径不匹配核对两边绝对路径页面刷新404前端history模式没有try_files加try_files $uri $uri/ /index.html接口502 Bad GatewayNginx反代的后端没启动检查Java进程是否活着nohup —— 启动后有没有退出中文乱码文件编码不是UTF-8IDEA中统一File Encoding为UTF-8MySQL连接URL加characterEncodingutf87. 典型问题实录与排查思路7.1 MyBatis-Plus自动填充时间不生效你用了TableField(fill FieldFill.INSERT)注解createTime还是null。原因是你没有配MetaObjectHandler。Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这是MyBatis-Plus的一项约定光注解不配处理器自动填充功能是不生效的。网上很多精简教程省略了这步导致一堆人踩坑。还有一个细节strictInsertFill方法对null才会填充如果你手动set了值它不会覆盖。这个设计挺好的允许特殊场景下手动指定时间。7.2 删除用户时外键关联的数据全报错你删user表数据时报外键约束失败因为他的id被travel_note、comment这些表引用了。这个问题的正确处理方式有两个方向一是逻辑删除给user表加一个deleted字段0正常1删除所谓“删除”只是把deleted置1。这样历史上他发过的游记还保留着只是账号不能登录。毕设系统推荐用逻辑删除因为你想保留他发过的内容游记不能随账号消失否则别人的评论就没意义了。二是物理删除时先删关联那你就要写一串SQL从评论表、点赞表、关注表、游记表按顺序依次删除顺序还不能错因为外键关系是层层依赖的。操作繁琐且容易遗漏。对于毕设场景我提议用逻辑删除虽然空间多占了一点但代码里一个update搞定所有事且不会误删数据。你甚至可以在user表逻辑删除的同时把所有游记也批量下架这是可以通过一条update完成的要在Service层做。7.3 数据库时区报错Server time zoneMySQL 8.0连接时经常报 java.sql.SQLException: The server time zone value Öйú±ê׼ʱ¼ä is unrecognized or represents more than one time zone这个乱码是中文“中国标准时间”的编码问题。解决方案是在数据库连接URL上加一个时区参数jdbc:mysql://localhost:3306/travel_db?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai是必须的这也是MySQL 8.0和5.7的一个配置差异。你网上下的旧源码里如果连接串没这个参数启动时大概率报这个错先改这里再想别的。7.4 前端明明穿着Token后端还是401典型的场景前端请求头带了Authorization但后端在拦截器里解析出一个null或者报错。排查方向有这几个第一看前端的请求拦截器是否真的把Token拼进去了。axios默认没有这个行为必须自己配置axios.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; } return config; });第二看后端拦截器求的是Authorization还是access-token两边名字要一致。第三排除CORS预检请求OPTIONS拦截。浏览器发跨域请求时会先发一个OPTIONS预请求你的拦截器如果不放行OPTIONS就会拦掉这个预检返回401浏览器直接放弃正式请求。所以拦截器里必须对OPTIONS请求直接放行。7.5 数据查出来了但一直显示“暂无数据”页面表格渲染不出来大概率是字段名对不上。后端返回的字段是createTime前端表格里写的是create_time自然显示不了。解决方案一是后端VO里统一用驼峰命名前端就用驼峰二是前端再加一层字段映射。比较省事的是后端统一用驼峰这也是Java和MySQL交互的默认设计——MyBatis-Plus里有map-underscore-to-camel-case配置默认为true会把数据库的snake_case自动映射成实体的驼峰字段你只需要保证前端拿到的JSON也是驼峰就行。这种问题调试方式是用浏览器的Network面板看响应体确认字段名后端可以开Full Log日志看到每条SQL这个调试方式一定要学会。8. 关于毕业论文和答辩几个过来人经验8.1 文档不要等到最后再写我知道99%的人都是先写代码再写文档现在说这个已经晚了但如果你还在早期记住这个教训每完成一个模块立刻把它的代码写进论文。发游记接口写完就把需求分析、接口设计、核心代码、流程图这部分先写好。不然最后几天熬夜补论文你会边写边怀疑自己当时为什么会写出这么绕的代码。8.2 论文里的核心图要怎么画三张图是必有的系统功能结构图把前台用户功能和后台管理功能分两个大块列出来。系统架构图画出浏览器、Controller层、Service层、DAO层、MySQL数据库的分层调用关系。E-R图把user、travel_note、comment、like_record这些表画出来连线标上一对多、多对多关系。画图工具用ProcessOn或者draw.io都行注意统一画风别一会儿彩色一会儿黑白。E-R图最容易出错的是表关系画错。多对多关系要在E-R图中表示成一张独立的关联表比如用户和用户之间的关注直接画成follow表两边都是外键这是老师爱看的规范画法。8.3 答辩时怎么回答“项目难点”老师问难点你别说“这个项目没有难点”那等于自爆。你必须准备至少三个能讲的“难点”既不是吹牛也有技术含量第一个可以说“Feed流的性能优化”就是本文4.4节讲的那个子查询方案在大数据量下的性能问题你是如何通过冗余count字段或JOIN改写的。第二个可以说“重复点赞的并发防护”从业务校验到数据库唯一索引再到异常捕获形成了一套完整的三层防护这个是真实存在的场景老师会听出你不是背的。第三个可以说“图片上传文件名冲突和处理”这个虽然简单但讲出“UUID日期目录”这个方案为什么能解决这两个问题也算一个言之有物的点。回答难点的逻辑要遵循“场景描述——技术方案——落地效果”三段式。比如“用户在快速点击点赞按钮时后端如果并发处理可能短时间内插入多条点赞记录导致一个用户对同一篇游记点出多个赞。我采用数据库联合唯一索引实现兜底加上代码里在插入前查询一次当DuplicateKeyException抛出时统一提示‘你已点过赞了’。这套方案测试时用JMeter并发发50个请求数据库只会成功插入一条记录。”只要你能把这个流程像这样讲出来答辩老师大概率就给过了。8.4 千万不要真去“一条龙”市面上那些全bao、一条龙我可以直白告诉你他们卖给你的源码最大的问题不是能不能跑而是你根本讲不出东西。上面那些拦截器、JWT、事务注解别人代码里可能有但你从头到尾没自己写过答辩时一个追问你就露馅。我已不止一次遇到被一条龙坑惨的学生代码部署起来没问题但让他讲需求分析他只会复述论文里的目录让他改一个字段名他翻了几十个文件找不到地方。这种情况老师一眼就看出来不是自己做的轻则答辩不通过重则在整个学院挂了名。所以我的态度再明确一点你可以参考网上任何现成源码但一定要自己把核心代码重新敲一遍。尤其登录鉴权、发布游记、点赞、这三大块用自己的逻辑重新实现哪怕整体风格和原来相似你也已经掌握了。这篇文章全部内容其实就是你自己实现它的完整地图。9. 部署上线后的最后几点真心话我在做这个系统的过程中最有价值的收获不是答辩拿了什么等级而是我终于把“一个能用的Web应用是怎么从0到1走到线上的”这条链路完整走了一遍。这套能力是纯看教程学不来的只能在一次次启动报错、环境变量不对、Nginx配置写错、前端联调失败的反复折磨中建立起来。如果你正在做类似题目我个人建议你把上线操作完整做一遍不要只停留在IDEA里能跑。申请一台最便宜的云服务器把jar包打出来丢上去把Nginx反代配好然后用手机浏览器访问一次你的系统那个时刻你会觉得之前所有的暴躁都是值得的。最后再送一个实用小技巧上线前务必在系统的每个页面走一遍“极端操作”比如不登录直接访问发布页、反复快速点击点赞按钮、上传一个0字节的空图片。这些是你测试时最容易忽略但在答辩演示时最容易翻车的地方。毕竟现场投影那么多双眼睛看着一个空指针异常的红色报错能在瞬间毁掉前面所有的精心准备。把这些极端输入都拦住了你的系统才算真正稳了。
返回列表