Spring Boot美食分享网站源码解析:从分层架构到部署避坑实战指南 简介面向地方美食分享类网站的设计与实现需求这份基于SpringBoot和Vue的Java Web源码适用于高校毕业设计、课程实训及初级开发者学习完整的前后端项目搭建。系统包含用户信息、图片素材、视频素材等核心模块覆盖用户管理、美食内容展示与后端接口开发技术栈涉及MySQL数据库、MyBatisPlus持久层、Maven构建与AJAX交互能够帮助理解B/S架构下的业务实现流程。项目文档同步包含选题动因、背景与意义以及MySQL、Vue、B/S架构等章节便于对照代码理解整体设计思路。压缩包共812个文件以Java源码、Vue组件、JavaScript脚本为主辅以CSS样式、HTML页面、SQL脚本、XML配置及项目说明文档整体大小约21.23MB目录结构清晰便于按模块定位代码。前端组件、控制器与Mapper接口均可直接复用能够显著减少同类型网站从零搭建环境的时间成本。目前已有195人学习下载适合需要快速上手SpringBoot整合Vue实际项目的读者参考。1. 拿到地方美食分享网站源码先做什么Spring Boot 项目两百字导读这份地方美食分享网站源码是用 Spring Boot 搭起来的一套 Java Web 单体项目。和常见的图书借阅、商品管理课设相比它更接近真实内容社区的最小闭环用户注册登录、发布美食帖子、上传图片、分类浏览、关键词搜索、收藏评论外加帖子管理后台。我拿到后先做的不是翻代码而是导入 SQL、改数据库配置、把项目跑起来再注册两个账号互发带图帖子走完整链路。走完才确认它不是演示壳子分页、热度排序、拦截器、图片映射都有落地。适合要写毕业设计的学生、从 SSM 过渡到 Spring Boot 的学习者以及想快速搭地方内容社区的开发者。2. 项目骨架与核心链路从目录结构到一条美食帖子的完整请求流2.1 分层结构与启动入口controller / service / mapper 三条线怎么切拿到源码第一步先把项目结构扫一遍。这套项目采用的是最标准的 controller / service / mapper 三层分包实体类单独放 entity公共返回包装放在 common。这样切的好处是请求只能往下一层走controller 不碰 SQLmapper 不写业务判断出问题时能顺着调用链直接定位到具体层而不是在一堆互相调用的代码里找头绪。src/main/java/com/foodshare ├── FoodShareApplication.java # Spring Boot 启动入口 ├── controller/ # 接收请求、做轻量校验、包装返回 │ ├── UserController.java │ ├── FoodController.java │ ├── CategoryController.java │ ├── CollectController.java │ └── AdminController.java ├── service/ # 业务逻辑事务边界在这一层 ├── mapper/ # MyBatis 数据访问接口 ├── entity/ # 数据库表对应的实体类 ├── config/ # 拦截器、静态资源映射等配置 └── common/ ├── Result.java # 统一返回体 └── PageResult.java # 分页结果包装controller 按业务对象分文件User 管登录注册Food 管帖子和列表Collect 管收藏Admin 管后台操作。service 层是事务边界比如收藏操作要同时更新收藏表和热度计数事务注解就加在 service 方法上一个方法内的所有数据库操作要么全成功、要么全回滚。mapper 层只留接口和 XMLSQL 全部集中在 resources/mapper 目录方便在不改动 Java 代码的前提下调 SQL。启动入口就是标准的 Spring Boot 主类代码量很少但两个注解决定了整个项目的装配方式SpringBootApplication MapperScan(com.foodshare.mapper) public class FoodShareApplication { public static void main(String[] args) { SpringApplication.run(FoodShareApplication.class, args); } }SpringBootApplication 包含组件扫描和自动配置MapperScan 把 mapper 包下的接口批量注册成 MyBatis 代理省去在每个接口上写 Mapper 注解的重复工作。改包名时必须同步改这个扫描路径否则启动会直接报找不到 mapper bean这是 Spring Boot 打包后最容易踩的第一类环境问题。2.2 核心表设计user / food / category / collect / comment 五张表怎么关联一个美食分享网站的数据库不复杂关键是表之间关系要顺。user 表存用户food 表存帖子category 表存分类collect 表是用户和帖子的多对多关联comment 表挂帖子 ID。看源码里 food 表的建表语句能直接看出设计者的取舍CREATE TABLE food ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, title VARCHAR(100) NOT NULL COMMENT 美食标题, description TEXT COMMENT 描述, cover_image VARCHAR(255) COMMENT 封面图路径, images VARCHAR(500) COMMENT 多图路径逗号分隔, category_id BIGINT COMMENT 分类ID, user_id BIGINT COMMENT 发布用户ID, view_count INT DEFAULT 0 COMMENT 浏览量, collect_count INT DEFAULT 0 COMMENT 收藏数, status TINYINT DEFAULT 1 COMMENT 1正常 0下架, create_time DATETIME COMMENT 创建时间, update_time DATETIME COMMENT 更新时间 );view_count 和 collect_count 是冗余字段值由浏览和收藏操作实时更新。选冗余而不是实时 COUNT 两张表是为了列表页排序时直接 ORDER BY collect_count DESC 就能拿到热度排名不用在多表关联里做聚合。代价是每次收藏操作都必须同时改 collect 表和这个计数少一步就会出现收藏数和实际记录对不上的问题这一点在后文翻车记录里会细说。collect 表的建表语句是理解收藏功能的关键CREATE TABLE collect ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, food_id BIGINT NOT NULL, create_time DATETIME COMMENT 收藏时间, UNIQUE KEY uk_user_food (user_id, food_id) );user_id 和 food_id 上的联合唯一约束是整套收藏逻辑的地基。它从数据库层面挡住了重复收藏后面判断是否已收藏就是查这条唯一索引存不存在。comment 表结构同理只是约束条件从唯一变成普通索引按 food_id 查评论列表。category 表字段最简单id 加 namefood 表通过 category_id 关联它做分类筛选时直接当筛选条件用。2.3 发布美食帖子的请求链路Controller 到 SQL 的完整落库流程以发布帖子为例看一条请求怎么走完三层。前端把表单数据用 JSON 提交到 /food/publishFoodController 先从 Session 里取登录用户把 user_id 补进去再交给 service 层校验和入库PostMapping(/publish) public Result publish(RequestBody Food food, HttpSession session) { User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { return Result.error(请先登录); } food.setUserId(loginUser.getId()); food.setStatus(1); return foodService.publish(food); }RequestBody 要求前端传 JSON参数名和 Food 实体字段对应。这里刻意不信任前端传的 userId而是从 Session 里取这是防止用户伪造他人发帖的常见手段。controller 只做两件事取登录态、补基础字段真正校验和落库往下放。status 默认置 1意思是发布即上架后台管理可以通过改这个值实现下架。service 层的方法长这样Transactional public Result publish(Food food) { if (food.getTitle() null || food.getTitle().trim().length() 2) { return Result.error(标题至少2个字符); } food.setCreateTime(new Date()); food.setUpdateTime(new Date()); foodMapper.insert(food); return Result.success(food.getId()); }Transactional 保证 insert 失败时不会留下半截数据。校验只拦标题长度description 和 images 为空也允许发布这是刻意留的宽容度避免造测试数据时被参数校验卡住。insert 使用 MyBatis 的自动回填主键 id 从数据库生成后回填到 Food 对象返回给前端方便跳转详情页。对应 Mapper XML 里的插入语句insert idinsert parameterTypecom.foodshare.entity.Food useGeneratedKeystrue keyPropertyid INSERT INTO food (title, description, cover_image, images, category_id, user_id, view_count, collect_count, status, create_time, update_time) VALUES (#{title}, #{description}, #{coverImage}, #{images}, #{categoryId}, #{userId}, 0, 0, #{status}, #{createTime}, #{updateTime}) /insertview_count 和 collect_count 不从前端接收插入时直接写死 0这是对代码审核比较友好的处理。useGeneratedKeys 让 MySQL 生成的主键回填到 Food 对象的 id 属性keyProperty 明确告诉 MyBatis 回填到哪个字段。整条链路走完一次发帖从 JSON 请求变成 food 表里的一行数据从浏览器 Network 面板到数据库客户端都能观察到完整过程。3. 登录、权限与图片上传Java Web 用户模块的三个翻车点3.1 Session 登录态与拦截器白名单里多一个路径就是多一个洞登录模块决定了整个项目对当前用户是谁的判断方式。这份源码用的是 Session 方案登录成功后把 user 对象放进程内 Session后续请求从 Session 取取不到就拦截。相比 JWTSession 方案代码量小、天然支持服务端主动失效单体项目用起来最省心。拦截器是实现登录校验的标准件preHandle 返回 false 时请求直接中断不会进入 controllerpublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } return true; } }手动设置 401 状态码并写 JSON 响应体这一步很关键。如果只 return false 不写响应体普通表单请求会看到一个空白页Ajax 请求会收到空响应前端判断不出发生了什么。把响应码和提示信息写清楚前端才能统一跳转登录页。注册拦截器在 WebConfig 里完成这是访问控制的全部入口Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/food/publish, /collect/**, /user/info) .excludePathPatterns(/user/login, /user/register, /food/list, /food/detail/**, /category/list); } }addPathPatterns 是要保护的路径excludePathPatterns 是放行路径。源码里放行的思路很明确读操作全部放行写操作拦截。游客能看列表、看详情、看分类只有发帖、收藏、改资料这些动作才要求登录。这里最容易翻车的是少写一个放行路径漏掉 /food/detail 的话分享出去的帖子链接在未登录时直接 401反过来多写了 /food/publish 的话没登录也能发帖权限等于白做。改这一节时建议顺手写一个路径对照表逐条核对。3.2 密码加盐与 MD5为什么盐必须跟着用户走用户表里专门留了 salt 字段这是处理密码明文的关键。注册时生成随机盐把密码和盐拼接后再做 MD5数据库里只存摘要结果和盐本身。这样即使数据库泄露攻击者拿到的也不是明文密码想批量反推还得对每个用户单独跑彩虹表成本高得多public User register(String username, String password) { User user new User(); user.setUsername(username); String salt UUID.randomUUID().toString().substring(0, 6); user.setSalt(salt); user.setPassword(md5WithSalt(password, salt)); userMapper.insert(user); return user; } private String md5WithSalt(String password, String salt) { return DigestUtils.md5DigestAsHex( (password salt).getBytes(StandardCharsets.UTF_8) ); }UUID 的随机性保证了每个用户的盐不同同一个密码在不同用户账号下生成的摘要完全不一样。salt 取前 6 位对课设和内部项目够用生产环境我更建议直接换 BCrypt密码长度和安全性都更可控。登录校验时从库里查出用户用用户自己的盐重新计算摘要再比对public User login(String username, String password) { User user userMapper.selectByUsername(username); if (user ! null user.getPassword().equals(md5WithSalt(password, user.getSalt()))) { return user; } return null; }这里有个高频坑改造时新增字段后忘了把 salt 写入数据库登录时拿到的盐是 null拼接出来的摘要永远和库里不一致表现为注册成功但登录永远失败。我一般会在注册方法后面加一段日志打印 user 的完整字段确认盐真的落库再继续测登录能省掉至少半个小时的排查时间。3.3 图片上传与本地存储访问 404 大概率是虚拟路径映射没配美食帖子最重要的内容就是图片图片能不能稳定显示决定整个项目的真实感。源码采用本地磁盘存储方案上传时把 MultipartFile 写到服务器目录再通过静态资源映射把 /upload/** 这个 URL 指向那个目录请求由 Spring MVC 直接从文件系统读文件不经过 controllerPostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName System.currentTimeMillis() _ new Random().nextInt(1000) ext; String basePath D:/foodshare/upload/; File dir new File(basePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(basePath fileName)); return Result.success(/upload/ fileName); }文件名用时间戳加随机数重命名避免中文文件名乱码和重名文件互相覆盖。ext 从原始文件名取后缀这里源码没有做白名单校验实际部署建议补上只允许 jpg、png、webp防止有人传 jsp 或 exe。basePath 直接写死在代码里是课设常见做法缺点是换机器就得改代码。我习惯把它挪到 application.yml通过 Value 注入部署时只改配置不动代码。目录能写文件不代表 URL 能访问还要在 WebConfig 里注册虚拟路径映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:D:/foodshare/upload/); }addResourceHandler 是浏览器访问的 URL 前缀addResourceLocations 是磁盘物理路径两个都必须以 / 结尾。Windows 下写 file:D:/foodshare/upload/Linux 下必须改成 file:/home/app/upload/斜杠方向和多出来的斜杠是图片在服务器上 404 的头号原因。上传接口返回成功但浏览器打不开图片时先检查这一行的路径格式。4. 检索与排序美食列表的分类筛选、关键词搜索和热度排序4.1 动态 SQL 组合查询where 和 if 如何拼出干净的条件列表页是项目流量最大的接口既要支持分类筛选又要支持关键词搜索还要能按热度或时间排序。如果每个组合都写一条静态 SQL条件一多代码就爆炸。源码用的是 MyBatis 动态 SQL在 XML 里用 where 加 if 组合条件有值才拼条件没值直接跳过select idselectByCondition parameterTypemap resultTypecom.foodshare.entity.Food SELECT * FROM food where if testcategoryId ! null category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND status #{status} /if /where ORDER BY choose when testsort hot collect_count DESC, view_count DESC /when otherwise create_time DESC /otherwise /choose LIMIT #{offset}, #{pageSize} /selectwhere 标签会自动去掉第一个条件前多余的 AND所以第一个 if 不用写 AND后面的都写。keyword 同时匹配标题和描述LIKE 用 CONCAT 拼接而不是直接写 %#{keyword}%因为后一种写法在预编译时会出问题% 被当成参数的一部分导致匹配失效。choose 是互斥分支sort 不是 hot 就默认按创建时间降序。这套组合写完之后新增筛选条件只要在 where 里加一个 if不需要动 Java 代码。4.2 分页参数与边界offset 换算不能省page 越界要有兜底分页接口最容易出错的点在页码计算。前端传的 page 从 1 开始SQL 里的 LIMIT 第一个参数是偏移量两者之间差一个 pageSize 的倍数关系。Service 层负责换算mapper 只管接收结果public PageResult pageList(int page, int pageSize, Long categoryId, String keyword, String sort) { int offset (page - 1) * pageSize; ListFood list foodMapper.selectByCondition( categoryId, keyword, sort, offset, pageSize); int total foodMapper.countByCondition(categoryId, keyword); return PageResult.build(list, total, page); }配套的 count 查询和列表查询共用同一套动态条件只是把 SELECT 列换成 COUNT(*)这样分页组件才能算出总页数。这里能看到的接口参数可以整理成一张表改前端对接时直接用参数类型说明pageint当前页码从 1 开始pageSizeint每页条数一般限制在 20 以内categoryIdLong分类 ID为空时查全部分类keywordString搜索关键词为空时跳过 LIKE 条件sortStringhot 按收藏量和浏览量降序其他值按时间降序page 越界时比如总共 5 页请求第 6 页list 返回空列表但 total 不变前端按 total 除以 pageSize 算总页数并禁用翻页按钮。这种设计的好处是后端不参与修正页码这种业务判断超过范围就展示空页面用户自然会退回上一页。常见误用是有人直接在 LIMIT 里写 page 而不做换算结果第一页跳过了第一条数据第二页跳过前 20 条列表永远错位。看到列表第一条对不上时先查 Service 层 offset 换算有没有写。4.3 热度排序与收藏统计冗余字段和事务缺一不可热度排序的数据来源是 food 表的 collect_count 和 view_count。收藏接口在做收藏和取消收藏时同时维护 collect 表和 food 表的计数。这两个操作必须放在同一个事务里否则可能出现收藏记录插进去了、计数没更新或者取消收藏后计数还挂着的情况Transactional public Result toggleCollect(Long foodId, Long userId) { if (collectMapper.checkExists(foodId, userId) 0) { collectMapper.deleteByFoodIdAndUserId(foodId, userId); foodMapper.decreaseCollectCount(foodId); return Result.success(已取消收藏); } collectMapper.insert(foodId, userId); foodMapper.increaseCollectCount(foodId); return Result.success(收藏成功); }checkExists 依赖 collect 表上 user_id 和 food_id 的联合唯一索引先查再插。没有唯一索引时并发请求可能插入两条重复收藏记录之后再取消收藏只会删一条数据就对不上了。increase 和 decrease 在 SQL 里写成 collect_count collect_count 1 和 collect_count collect_count - 1是数据库层面的原子操作比先 SELECT 再在 Java 里计算后 UPDATE 安全因为行锁会串行化并发更新。view_count 的更新更简单详情页接口被访问一次就自增一次不需要事务。这个方案有一个必须接受的代价冗余字段和真实数据之间存在短暂不一致窗口比如收藏事务还没提交时列表页看到的计数就是旧的。对内容社区来说排行榜差一个数不影响体验但如果是电商库存这类强一致场景就不能用这种同步冗余方案改成消息队列异步更新更稳妥。5. 部署与避坑从本地跑通到上线的四类问题排查记录5.1 本地启动三步走建库、改配置、跑 main 方法拿到源码后最快的验证路径是先让项目在本机跑通再谈改代码。第一步是建库导表SQL 脚本在 resources/db 目录下用命令行导入后确认表结构完整mysql -uroot -p foodshare.sql第二步改数据源配置把用户名密码换成自己本机的。MySQL 8 以上版本时区必须显式指定否则启动会报 time zone 相关的 SQLException。还要注意 URL 里的 符号在 YAML 里会被解析成特殊字符所以整个 URL 要用引号包起来spring: datasource: url: jdbc:mysql://localhost:3306/foodshare?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的数据库密码characterEncodingutf8 解决中文乱码serverTimezoneAsia/Shanghai 解决数据库时间差 8 小时的问题。这两个参数是本地启动最常见的环境坑少任何一个轻则列表页中文乱码重则启动直接失败。第三步运行 FoodShareApplication 的 main 方法控制台出现 Tomcat started 后访问 http://localhost:8080 验证首页。这里建议注册一个账号、发一条带图的帖子再关掉把登录、上传、发布三个最核心的接口都确认到位项目才算真正跑通。提示YAML 里多行拼接 URL 在 Spring Boot 里很容易踩坑一行写完整条 URL 最省事。本地调试时如果出现数据源解析错误先检查有没有引号。5.2 避坑记录四个在我手上真实发生过的部署问题以下四类问题在跑 Spring Boot 课设项目时反复出现按现象、原因、解决三步排列对照排查即可。中文乱码页面和数据库里显示的内容全是问号。 现象通过表单提交中文标题后列表页展示的标题、描述变成一串问号数据库里存的也是问号。 原因建库时没指定 utf8mb4或者 JDBC URL 缺少 characterEncoding 参数个别情况下是源码文件被编辑器存成了 GBK 编码。 解决建库语句写成 CREATE DATABASE foodshare DEFAULT CHARACTER SET utf8mb4URL 带上 characterEncodingutf8IDEA 右下角把项目文件编码统一改为 UTF-8 后再重新编译。图片上传返回成功但访问 404上传接口返回了 /upload/xxx.jpg浏览器直接打不开。 原因Spring Boot 默认不映射本地磁盘目录少了 addResourceHandlers 虚拟路径配置或者本地用 Windows 路径部署到 Linux 后没改成 file:/app/upload/ 格式。 解决确认 WebConfig 里 addResourceHandler(/upload/**) 和 addResourceLocations(file:D:/foodshare/upload/) 成对存在服务器上改成 file:/app/foodshare-upload/并在上传代码里加目录不存在则自动创建的判断。登录后请求接口还是返回未登录登录接口调用成功紧接着发帖却被 401 拦下。 现象浏览器里能正常访问登录接口拿到成功响应但再调 /food/publish 时返回未登录。 原因拦截器把 /user/login 也拦了或者登录时存入 Session 的 key 和拦截器取出的 key 不一致。 解决把 /user/login 和 /user/register 加进 excludePathPatterns全局搜索 loginUser 这个字符串确保 login 和拦截器两处写的完全一致。启动报 8080 端口被占用Tomcat 起不来控制台提示 Port 8080 was already in use。 现象本地有别的 Java 进程或开发工具占用了 8080比如之前没关掉的旧项目实例。 原因端口冲突Spring Boot 默认端口就是 8080多个项目同时跑必然撞车。 解决用 netstat -ano | findstr 8080 找到占用进程的 PID在任务管理器里结束或者在 application.yml 里把 server.port 改成 8081开发期我习惯直接换端口省得每次都要杀进程。5.3 打包部署profile 切换与 jar 包运行时的注意点本地跑通后部署到服务器推荐 Maven 打包成 jar 直接运行比 war 包省去外部 Tomcat 的版本匹配问题。打包时跳过测试能省时间但前提是你已经本地验证过核心流程mvn clean package -DskipTests打包产物在 target 目录下文件名是 artifactId 加版本号。启动时用 --spring.profiles.active 指定环境配置比如新建 application-prod.yml 放生产环境数据源然后这样启动java -jar target/foodshare-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod把环境差异收敛到配置文件是本阶段最值得做的事。本地配置连本机 MySQLprod 配置连云数据库上传目录从 D:/foodshare 换成 /app/foodshare-uploadjar 包里只留一套业务代码。部署时数据库连不上第一件事看 prod 配置里的 IP 和端口是不是数据库服务器的真实地址很多人把 localhost 原样带到了服务器上库当然连不通。6. 把项目改造成自己的收藏推荐与定时导出的两个低成本扩展6.1 用收藏记录做简化版偏好推荐收藏功能最容易被当成存进去就完事其实它是最便宜的推荐数据源。不需要引入算法框架统计用户收藏过的帖子在哪些分类然后用分类做过滤就能做出一个你可能还喜欢的推荐位public ListFood recommendForUser(Long userId) { ListLong categoryIds collectMapper.selectCategoryIdsByUserId(userId); if (categoryIds null || categoryIds.isEmpty()) { return foodMapper.selectHotList(10); } return foodMapper.selectByCategoryIdsAndExcludeCollected(categoryIds, userId, 10); }逻辑分红两条路新用户没有任何收藏记录直接返回热度榜前 10这是冷启动的回退方案老用户有收藏就按收藏过的分类取同类帖子同时排除已经收藏过的。selectCategoryIdsByUserId 从 collect 表关联 food 表拿到分类 ID 列表再用 IN 查询捞食品。这个改动只涉及一个 service 方法和两句 SQL却能让详情页侧边栏从死数据变成动态内容。验证方法很直接新注册账号看推荐位显示的是热度榜收藏几条川菜帖子后再看推荐位里川菜比例明显上升。6.2 定时导出帖子数据做后端备份另一个值得加的小功能是每天凌晨把 food 表全量导出成文件。这只需要一个定时任务类用 Spring 自带的 Scheduled 就能跑不需要引入 QuartzComponent EnableScheduling public class BackupTask { Scheduled(cron 0 0 2 * * ?) public void exportFood() { ListFood list foodMapper.selectAll(); String date LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String fileName /backup/food_ date .json; // 用 Jackson 把 list 序列化后写入 fileName } }cron 表达式0 0 2 * * ?表示每天凌晨 2 点执行。EnableScheduling 要加在启动类或任意配置类上否则 Scheduled 不会生效。顺带提醒定时任务默认单线程阻塞执行如果以后加了导出评论、导出用户的多个任务记得单独配置线程池避免一个任务卡住把后面的全部阻塞。说到这我忍不住提一句自己的习惯拿到任何 Spring Boot 源码我不追求先理解每一行而是强制走一遍建库、启动、注册、发帖、传图、打包这条完整链路。每次走都能暴露出别人代码里至少一个隐含假设比如上传目录写死、数据库密码留在配置文件、时区没指定。把这些假设全部记下来项目才算真正属于你。这套地方美食分享源码在课设项目里完整度算中上推荐和备份这两个点补上之后代码会从能跑变成像认真设计过希望帮到你。本文还有配套的精品资源点击获取