简介面向Java Web入门与课程实验的「Web原理及应用个人微博应用」项目源码包围绕会员管理、个人中心与消息发布管理等核心模块帮助读者理解HTTP请求响应、Servlet/JSP动态页面、数据库访问及前端样式交互在社交类应用中的落地方式。压缩包共71个文件约3.33MB包含22个Java源文件、24个class编译文件、5个JSP页面、3个XML配置与3个jar依赖另有CSS样式、图片资源、数据库文件及工程元数据覆盖后端逻辑、页面展示、静态资源与运行配置等主要环节。已有224人学习适合需要对照完整工程结构、梳理模块分工与查漏补缺的初中级开发者可从中观察项目目录组织、Servlet与JSP协作、会员与消息数据表设计思路并借助现有源码、页面和配置快速复盘课程实验理解个人微博类应用从前端展示到后端处理的开发脉络。1. 从“能发一条微博”倒推个人微博应用要做成什么样很多人拿到“个人微博应用”这个题目第一反应是做一个输入框加一个发布按钮页面能把文字显示出来就算完两小时跑通然后卡了整整一周。课程设计要交的不是一张表单而是一套闭环。前端 java web 这套组合落到这个题目上本质是服务端渲染为主的轻量系统Java 侧负责会话、权限、业务规则和持久化前端负责页面结构、表单校验和局部异步刷新。真正耗时间的是四件事——登录态怎么在多个页面之间保持一致、关注关系怎么存、时间线怎么按关注的人查出来、图片上传之后怎么访问到。这套方案就围绕这四件事展开适合要交课程设计或毕设、希望结构经得起老师追问的人。2. 技术选型与总体架构Servlet 容器、MVC 分层与前后端职责划分选型这件事在答辩现场会被问得比想象中细。老师不会只问“你用了什么框架”而是问“为什么不直接用 JSP”“Controller 里为什么不能写 SQL”“事务加在哪一层”。所以选型的标准不是哪个新而是每一层边界能不能用一句话讲清楚。我一般的做法是先把职责划分画明白再回头决定用哪套技术而不是先抄一个脚手架再往里塞功能。2.1 三条常见技术路线的取舍同一个题目实现路线差别很大先看下面这张表把代价摆清楚再决定往哪走。路线开发量调试成本答辩可解释性适用情况纯 Servlet JSP JDBC大每个请求一个 ServletSQL 手写拼接低所见即所得高Web 原理全暴露课程明确要求手写原理SSMSpring SpringMVC MyBatis中XML 配置多容器要单独装中启动慢、配置出错难定位中分层清楚但配置文件多需要体现分层、又不想用 BootSpring Boot MyBatis Thymeleaf小起步依赖一站式低内嵌容器一条命令启动中高注解多需要能讲清自动配置时间紧、要功能完整、要能演示我倾向于第三种理由很实在内嵌 Tomcat 省掉装容器和配web.xml的一堆事把时间留给业务MyBatis 的 XML 里 SQL 是明文答辩时可以直接把时间线那条 SQL 投到屏幕上讲索引和 JOIN比 ORM 自动生成的语句好解释得多。但有一条硬约束——如果课程大纲明确要求手写 Servlet 生命周期、filter 链那就老老实实按大纲走用框架反而会被扣“没有体现 Web 原理”。选型的第一原则是匹配考核要求不是匹配流行度。前端这一侧不要上构建工具链。Thymeleaf 做服务端渲染加少量原生 JS 处理点赞、关注、加载更多这类不需要整页刷新的动作static目录直接放 css 和 js浏览器刷新就能看到效果。这个组合的调试链路最短改一行 HTML 刷新即见效比配一整套打包工具省下来的时间够多写两个模块。2.2 包结构与分层边界一条规则管住乱写目录结构定下来之后后面加功能就是往里填不会越写越乱。weibo-app ├── src/main/java/com/example/weibo │ ├── controller # 只接收参数、调 service、返回视图或 JSON │ ├── service # 业务规则与事务边界impl 放实现 │ ├── mapper # MyBatis 接口一个方法对应一条 SQL │ ├── entity # 与数据库表一一对应的对象 │ ├── dto # 页面/接口传输对象不直接把 entity 丢给前端 │ ├── config # 拦截器注册、静态资源映射、上传目录配置 │ └── common # Result、业务异常、常量、工具类 ├── src/main/resources │ ├── mapper # XML 映射文件与 mapper 接口同名 │ ├── templates # Thymeleaf 页面 │ ├── static # css / js / 默认头像 │ └── application.yml # 数据源、上传路径、服务端口 └── pom.xml分层边界用一句话记住Controller 不出现 SQLService 不出现HttpServletRequestMapper 只管单表。举个具体例子“发布微博时内容为空要报错”这条规则放 Service因为换一个入口比如以后加个接口也要生效“当前登录用户是谁”从 Session 取这个动作放 Controller取出来当参数传给 ServiceService 就变成了纯粹的业务方法单元测试也好写。事务注解Transactional一律加在 Service 的公开方法上不要在 Controller 上加一是注解在非 public 方法或同类内部调用时容易失效二是事务边界本来就该跟业务边界对齐。DTO 这一层别省。微博列表页需要作者昵称、头像、当前用户是否点过赞这些字段分散在weibo、user、like_record三张表里直接返回 entity 就得在实体上挂一堆Transient字段越挂越脏。定义一个WeiboCardVO专门装列表要显示的东西SQL 里一次查出来页面模板直接取值改动范围可控。2.3 统一响应体与分页参数约定前后端之间只要有一个统一的壳JS 那边就不用为每个接口写一套判断。public class ResultT { private int code; // 0 表示成功非 0 为业务错误码 private String msg; // 给用户看的提示语 private T data; // 业务数据失败时为 null public static T ResultT ok(T data) { ResultT r new Result(); r.code 0; r.msg ok; r.data data; return r; } public static T ResultT fail(int code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } // getter / setter 省略 }分页参数固定成三个pageNum从 1 开始pageSize默认 10 且上限 50lastId用于游标分页后面时间线那节会讲为什么两种并存。pageSize一定要在服务端卡上限前端传pageSize10000直接把全表拉出来演示时数据库小看不出来答辩老师翻一页就问出来了。返回体用PageResultT字段是list、total、hasMore其中hasMore由服务端算好前端“加载更多”按钮只认这个布尔值不用自己拿 total 和当前条数去比。2.4 前端页面组织模板片段加原生 JS页面用 Thymeleaf 的 fragment 拆成head、navbar、weibo-card三个片段微博卡片在首页、个人主页、搜索结果页三处复用改一次样式三处同时生效。这是服务端渲染相对组件化框架最实用的一个点不需要额外配置。异步动作统一走一个小的 fetch 封装重点在于告诉后端“这个请求是 AJAX”否则未登录时后端返回 302 重定向fetch 会安安静静地把整个登录页 HTML 当成响应体交给回调前端拿resp.json()解析直接抛错控制台一片红人还以为是接口挂了。// 统一请求封装带上 AJAX 标识401 时跳登录 async function request(url, options {}) { const resp await fetch(url, { ...options, headers: { X-Requested-With: XMLHttpRequest, ...(options.headers || {}) } }); if (resp.status 401) { // 未登录交给页面跳转 location.href /login?redirect encodeURIComponent(location.pathname); return null; } const body await resp.json(); if (body.code ! 0) { // 业务错误统一提示 alert(body.msg); return null; } return body.data; } // 点赞按钮先请求再按服务端返回的计数刷新避免前端自己加减算错 async function toggleLike(weiboId, btn) { const data await request(/api/like/ weiboId, { method: POST }); if (!data) return; btn.classList.toggle(active, data.liked); btn.querySelector(.count).textContent data.likeCount; }这段代码里有两个容易忽略的点。X-Requested-With这个请求头是后端拦截器判断“返回 JSON 还是重定向”的依据少了它整个登录态处理链路就断。点赞后的数字以服务端返回为准不在前端做1因为同一个人重复点击、多个标签页同时打开的情况都会让前端自己算的数字和数据库对不上最后就是那个经典场面页面显示 5 个赞刷新变成 4 个。提示location.href跳登录时把当前路径塞进redirect参数登录成功后跳回来演示时这个细节很加分。3. 数据模型设计五张表撑起用户、微博与社交关系表设计是这个题目里最不该省事的部分。功能就那么几个但表结构一旦定歪后面时间线查不出来、点赞数对不上、取关之后微博还在首页全是结构问题而不是代码问题。我的习惯是先把手写纸上的实体关系画一遍确认每条查询都能落到索引上再写建表语句。3.1 用户表与微博表字段该留的留该删的删CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT 登录名唯一, password CHAR(64) NOT NULL COMMENT 密码摘要不存明文, nickname VARCHAR(32) NOT NULL COMMENT 展示昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像相对路径, bio VARCHAR(140) DEFAULT NULL COMMENT 个人简介, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 正常 1 禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE weibo ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 作者, content VARCHAR(2000) NOT NULL DEFAULT COMMENT 正文可为空但需配图, image_url VARCHAR(255) DEFAULT NULL, like_count INT NOT NULL DEFAULT 0 COMMENT 冗余计数, comment_count INT NOT NULL DEFAULT 0 COMMENT 冗余计数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 正常 1 已删除, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_created (user_id, created_at), KEY idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段的取舍说明一下。content用VARCHAR(2000)而不是TEXT是因为这个长度足够覆盖“一条微博”的语义而且能把长度上限写进表结构业务层校验和数据库约束对得上。删除一律用status逻辑删除不要物理删除否则评论、点赞记录会变成孤儿数据列表页还得额外过滤。created_at单独建索引首页的“全部最新”列表走这个索引(user_id, created_at)联合索引服务个人主页那种“某个人的微博按时间倒序”的查询。密码字段存的是摘要注册时用不可逆算法加盐处理登录时比对摘要而不是把明文读出来比。答辩时这一条经常被问能说清“为什么数据库里看不到明文密码”就已经赢了。3.2 关注关系表单向关注与两个方向的索引CREATE TABLE follow ( id BIGINT NOT NULL AUTO_INCREMENT, follower_id BIGINT NOT NULL COMMENT 关注发起方, followee_id BIGINT NOT NULL COMMENT 被关注方, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_pair (follower_id, followee_id), KEY idx_followee (followee_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;联合唯一索引uk_pair是防止重复关注的关键接口层面再判断一次“是否已关注”两层保障。idx_followee这个索引容易被漏掉它的用途是查粉丝列表“谁关注了我”首页时间线查的是“我关注了谁”走的是联合索引的最左前缀follower_id两个方向需求对称索引也要对称建。关注关系我没有做成双向记录。A 关注 B 只写一条(A, B)互相关注就是两条记录互指。这样存储量减半取关就是删一条逻辑比维护双向状态简单得多。3.3 时间线为什么选拉模式时间线有两种做法。推模式是发微博时给每个粉丝的收件箱插一条记录读的时候直接按收件箱查读快写慢拉模式是读的时候现查“我关注的人发了什么”走 JOIN写快读慢。个人微博应用直接选拉模式原因是粉丝规模小——课程设计场景下几十上百个用户一条带子查询的 SQL 在毫秒级返回而推模式要为每个粉丝写扩散发布一次要做 N 次插入事务变长还得维护收件箱表复杂度翻上去但收益为零。判断标准很简单关注数超过几千、读请求远多于写请求时再考虑推模式或加缓存层否则拉模式是对的。3.4 评论表与点赞表CREATE TABLE comment ( id BIGINT NOT NULL AUTO_INCREMENT, weibo_id BIGINT NOT NULL, user_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_weibo (weibo_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE like_record ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, weibo_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_weibo (user_id, weibo_id), KEY idx_weibo (weibo_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;点赞表用like_record这个名字不叫like因为它是 SQL 关键字建表时不加反引号就报语法错误取别名、写查询时也处处要转义属于纯粹的自我麻烦。uk_user_weibo唯一索引是防止重复点赞的第一道闸业务代码里再配合INSERT IGNORE和影响行数判断双重保险。3.5 计数冗余不是偷懒是避免每次翻页扫全表like_count和comment_count两个冗余字段本质是用一点一致性维护成本换列表页的查询效率。如果每次渲染列表都去COUNT(*)统计一页 10 条微博就是 20 次额外查询这就是典型的 N1用户越活跃越慢。维护方式点赞成功后在同一事务里UPDATE weibo SET like_count like_count 1取消点赞减一并保证减完不小于 0。数据库层面加一条约束或者用GREATEST(like_count - 1, 0)兜底防止异常路径下出现负数——页面上显示“-1 个赞”这种场面演示时真的会发生。注意冗余字段一定要在同一个事务里更新先写明细表再改计数中间失败要回滚否则明细和计数会长期漂移。4. 核心功能实现登录态、发布链路与时间线查询功能实现部分按用户的真实操作顺序走一遍登录 → 发微博 → 看到自己的和关注的人的内容 → 点赞评论。每一段都给关键代码和它背后的判断抄结构可以参数和路径要按自己的项目改。4.1 登录态与拦截器一条请求头决定返回 JSON 还是重定向登录成功后把用户 id 和昵称写进 Session后续请求靠拦截器校验。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { Object uid req.getSession().getAttribute(uid); if (uid ! null) { return true; // 已登录放行 } // 关键分支AJAX 请求不能重定向否则前端拿到一整页登录 HTML if (XMLHttpRequest.equals(req.getHeader(X-Requested-With))) { resp.setStatus(401); resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\code\:401,\msg\:\请先登录\}); return false; } resp.sendRedirect(/login?redirect req.getRequestURI()); return false; } }注册拦截器时要配白名单/login、/register、/static/**、/css/**、/js/**全部排除否则登录页自己也被拦截形成死循环。Session 超时时间在application.yml里设成 30 分钟比默认值短一点演示中途被踢出去能顺便讲一句“会话过期处理”反而是加分项。4.2 发布链路校验顺序、文件名重命名与路径分离发布方法要做四件事校验、存图、落库、返回。顺序不能乱先校验再存文件避免内容不合法却已经往磁盘写了一堆垃圾。Transactional public Long publish(Long userId, String content, MultipartFile image) { String text content null ? : content.trim(); boolean hasImage image ! null !image.isEmpty(); if (text.isEmpty() !hasImage) { throw new BizException(内容和图片至少填一项); } if (text.length() 2000) { throw new BizException(内容不能超过 2000 字); } String imageUrl null; if (hasImage) { String original image.getOriginalFilename(); String ext original null ? : original.substring(original.lastIndexOf(.)).toLowerCase(); // 白名单校验后缀防止上传可执行文件 if (!Arrays.asList(.jpg, .jpeg, .png, .gif).contains(ext)) { throw new BizException(仅支持 jpg / png / gif); } // UUID 重命名避免中文名、重名覆盖和路径穿越 String fileName UUID.randomUUID().toString().replace(-, ) ext; Path dir Paths.get(uploadDir); // 磁盘真实目录 Files.createDirectories(dir); image.transferTo(dir.resolve(fileName).toFile()); imageUrl /upload/ fileName; // 对外访问路径 } Weibo w new Weibo(); w.setUserId(userId); w.setContent(text); w.setImageUrl(imageUrl); weiboMapper.insert(w); return w.getId(); }这里最值得讲的是“磁盘目录”和“访问路径”分开两套配置。uploadDir指向服务器上的真实位置比如项目外的data/uploadimageUrl存的是相对路径由静态资源映射把/upload/**映射到那个目录。这么做的好处是重新打包部署不会把用户上传的图片冲掉把图片放src/main/resources/static下面是最常见的翻车点一执行打包就全没了。transferTo之后建议顺手限制单个文件大小Spring Boot 里配spring.servlet.multipart.max-file-size和max-request-size不配的话默认值偏小手机拍的图直接报“文件超过限制”演示时一脸懵。4.3 时间线查询一条 SQL 解决关注流游标分页替代深翻页SELECT w.id, w.content, w.image_url, w.created_at, w.like_count, w.comment_count, u.id AS authorId, u.nickname, u.avatar, EXISTS (SELECT 1 FROM like_record l WHERE l.weibo_id w.id AND l.user_id #{uid}) AS liked FROM weibo w JOIN user u ON u.id w.user_id WHERE w.status 0 AND w.id #{lastId} -- 游标从上一页最后一条的 id 往前取 AND (w.user_id #{uid} -- 自己发的 OR w.user_id IN (SELECT followee_id FROM follow WHERE follower_id #{uid})) ORDER BY w.id DESC LIMIT #{pageSize}三个设计点在答辩时可以直接讲。EXISTS子查询只对当前页的 10 条执行代价可控比在 Java 里循环查十次点赞状态干净。分页用lastId游标而不是LIMIT offset, size因为深分页时数据库要跳过前 N 条再取页数越深越慢游标分页每页代价恒定这是列表类接口的常规做法。w.id lastId依赖自增主键近似时间序如果业务上有定时发布之类会打乱顺序的需求就得改用created_at id联合游标。首页还要一个“全站最新”的 tab去掉关注条件即可走idx_created索引。个人主页则是按user_id过滤走联合索引。三条查询各有各的索引别指望一条 SQL 走所有场景。4.4 点赞唯一索引加原子更新不做“先查再插”Transactional public boolean toggleLike(Long userId, Long weiboId) { int inserted likeMapper.insertIgnore(userId, weiboId); // INSERT IGNORE重复则影响 0 行 if (inserted 1) { weiboMapper.incrLikeCount(weiboId, 1); // 原子自增 return true; } likeMapper.delete(userId, weiboId); // 已点赞则取消 weiboMapper.incrLikeCount(weiboId, -1); return false; }对应的 SQLINSERT IGNORE INTO like_record (user_id, weibo_id) VALUES (#{uid}, #{wid}); UPDATE weibo SET like_count GREATEST(like_count #{delta}, 0) WHERE id #{wid};先查询再决定插入还是删除的写法在并发下会出现两次点击都查到“未点赞”然后两次插入靠唯一索引报错INSERT IGNORE把并发问题交给数据库的唯一约束处理影响行数是 1 才说明真的新增成功逻辑更短也更稳。GREATEST是防止计数被减成负数的后悔药虽然正常情况下不该触发但线上数据出问题往往就是这么来的。4.5 关注按钮与个人主页的联动关注接口只需要三个动作不能关注自己、已关注则删除、未关注则插入。判断“是否已关注”用一条SELECT COUNT(1)走联合唯一索引很快。个人主页展示三块数据用户信息、微博数、关注数/粉丝数。微博数直接COUNT一次可以接受因为只查一个人的如果在意性能就在用户表加weibo_count冗余跟点赞计数一个思路。前端关注按钮点击后只切换文字和样式不改页面其他部分评论和个人主页的入口都不动。整页刷新会导致滚动位置丢失体验很差也是一个容易被忽略但有体感差异的细节。5. 避坑与常见问题排查五类让演示当场翻车的细节下面这几条都是这个题目里高频出现的问题共同特点是本地跑得好好的换台机器、换个浏览器、或者数据量稍微大一点就暴露。每一条按“现象 → 原因 → 解决”写遇到时按顺序对一遍比漫无目的翻代码快。5.1 中文乱码三处编码必须同时对齐少一处就白搭现象发布的中文微博在列表里显示成问号或方块控制台打印的 SQL 参数却是正常的。原因编码链路上有三个环节各自独立——数据库和表的字符集、JDBC 连接串的编码参数、响应体的Content-Type。任何一处是utf8MySQL 里这个别名实际是三字节存不下 emoji或者缺省中文就会在某一环被截断。很多人只改了连接串表和响应头没动问题依旧。解决建库建表统一utf8mb4连接串写成jdbc:mysql://localhost:3306/weibo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiSpring Boot 里配server.servlet.encoding.charsetUTF-8和forcetrue。三处改完重启再发一条带 emoji 的微博验证——emoji 能正常显示说明四字节字符链路通了中文自然没问题。5.2 图片上传成功但访问 404路径映射和真实目录不是一回事现象上传接口返回成功数据库里也存了/upload/xxx.jpg浏览器打开是 404。原因文件被写到了项目临时目录或resources下面而静态资源映射指向的是另一个位置或者写进了src/main/resources/static/upload运行时读的却是编译后的target/classes重启就丢。解决上传目录配成绝对路径的独立目录再用配置类把访问路径映射过去。Configuration public class WebConfig implements WebMvcConfigurer { Value(${app.upload-dir}) // 例如 /data/weibo/upload private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 的请求映射到磁盘真实目录与打包产物解耦 registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); } }file:前缀不能少不加会被当成 classpath 路径去找。改完清一次浏览器缓存再验图片这类静态资源缓存很顽固经常是代码已经对了但看到的是旧结果。5.3 时间线越翻越慢N1 查询把一页拖成十几秒现象首页第一页打开挺快翻到第五页开始明显卡顿控制台里 SQL 日志刷了几十条。原因列表查出 10 条微博后在 Java 循环里为每条再查一次作者信息、一次点赞状态、一次评论数10 条微博就是 30 次查询。数据量小的时候看不出来一旦每页条数变大或者并发上来数据库连接池直接被占满。解决把所有列表需要的字段一次性 JOIN 出来点赞状态用EXISTS子查询在当前页范围内判断作者信息 JOIN 用户表。判断标准很直接打开 MyBatis 的 SQL 日志渲染一页微博打印的 SQL 条数应该是个位数如果跟条数成正比就是 N1回去改 SQL。5.4 点赞数对不上重复提交和前端自增各占一半原因现象同一个人连续点两次点赞数变成 2或者页面显示 5 个赞刷新变 4 个。原因一是前端在请求返回前就把数字1请求失败或重复点击时本地数字已经错了二是后端用“先查再插”的写法并发下两次都判定为未点赞插了两条记录。解决前端只负责触发请求数字以服务端返回为准后端靠唯一索引加INSERT IGNORE用影响行数判断是否真正新增计数用like_count like_count 1这种数据库原子操作不要读到内存里加完再写回去。最后加一个校准手段写一条按like_record重算like_count的 SQL出现争议时跑一遍就知道谁错了。5.5 会话过期后页面白屏AJAX 拿到了一整页 HTML现象在页面上停留半小时点一下点赞按钮没有任何提示控制台报Unexpected token 。原因Session 过期后拦截器走的是sendRedirect浏览器跟着 302 拿到了登录页的 HTML前端按 JSON 解析自然失败。这个错误信息完全不指向真实原因属于典型的黑匣子式报错。解决就是 4.1 里那个分支——拦截器根据X-Requested-With判断AJAX 请求返回 401 加 JSON页面请求才重定向。同时前端封装里统一处理 401 跳登录。两个改动配合会话过期就从一个莫名其妙的报错变成一个正常的跳转。6. 验收与答辩一份能当场跑通的演示脚本与评分对照代码写完只是及格线能不能拿到分取决于演示那十分钟。我见过功能全做完但现场翻车的也见过功能不多但演示流畅拿了高分的。差别不在代码量在于有没有提前把演示路径固化下来。做法是准备一份“演示脚本”按操作顺序列清楚每一步点什么、期望看到什么、这一步对应哪个考核点。答辩前至少完整走三遍每遍用不同的账号并且提前造好数据两个账号、互相有部分关注、二十条左右的微博、几条评论和点赞。数据太干净演示不出时间线的效果数据太乱又容易点错二十条是刚好的量。演示环节具体操作预期结果对应考核点注册登录新账号注册后自动跳首页右上角显示昵称未登录访问首页跳登录页会话与权限控制发布发一条带图的微博列表顶部出现图片正常显示表单处理、文件上传删除删除刚发的微博列表消失他人主页也不再出现逻辑删除与数据归属校验关注A 关注 BA 首页出现 B 的内容B 粉丝数 1多表关联与关系维护取关A 取消关注 BB 的内容从 A 首页消失关系删除的一致性点赞同一账号连点两次数字只加一按钮状态正确切换并发与唯一约束时间线翻到第三页内容不重复、顺序稳定分页与索引异常未登录调点赞接口返回 401 JSON 并跳登录边界与异常处理答辩提问基本围绕三个方向为什么这么设计、数据怎么保证一致、出问题怎么查。第一个用分层和选型理由回答第二个用事务边界、唯一索引、冗余计数配合说明第三个最容易被问倒所以提前准备好把 SQL 日志打开演示一遍让老师看到一页列表只打了几条 SQL比嘴上解释一百句都有说服力。还有两个细节值得提前处理。演示用的数据库提前备份一份 SQL现场如果误删数据一分钟就能恢复不至于当场重新注册账号造数据。项目启动命令写进文档并自己念一遍确认端口没被占用、数据库密码没写错这种低级问题每年都有人栽。从那以后我每次做这类课设或毕设都会在交之前强制走一遍“换一台干净机器从零跑起来”的流程——拉代码、建库、导数据、启动、走完整演示脚本。本地环境里藏着的默认配置、残留的临时文件、忘了提交的图片目录只有换环境才会暴露。这个习惯救过我至少两次答辩。希望帮到你。本文还有配套的精品资源点击获取