ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue3+MyBatis+MySQL前后端分离旅游系统实战拆解

SpringBoot+Vue3+MyBatis+MySQL前后端分离旅游系统实战拆解 做旅游出行指南这类系统我见过太多人一上来就开写 CRUD结果做到一半才发现表结构不合理、分页插件配了没生效、前端联调跨域又卡两天。这套 SpringBoot Vue3 MyBatis MySQL 的前后端分离项目核心价值不是“能跑”而是把一条完整的旅游业务链路——线路展示、攻略分享、收藏下单、后台管理——用一套主流技术栈串起来。源码本身是一个很好的学习样板但如果只看不拆你就错过了里面最值钱的东西技术选型的逻辑、分页和缓存这类高频坑的处理方式、以及前后端联调时那些说不出口的细节。这篇文章会从项目整体设计开始逐个拆后端的核心配置、MyBatis 的进阶用法、Vue3 前端的页面落地最后再把部署上线和常见报错一次性清干净。不管你拿这套源码是准备做毕业设计、应付领导需求还是纯粹想练全栈按这个思路读下来你的收获会比单纯 star 一个仓库大得多。1. 项目定位与技术选型为什么是这套组合1.1 旅游出行指南系统到底解决什么问题先说业务。一个旅游出行指南系统的核心使用者有两类一类是普通游客他们要查路线、看攻略、了解景点、甚至在线提交出行意向另一类是平台运营人员需要维护线路信息、审核攻略、处理订单状态。所以从功能模块上看它应该是“前台展示 后台管理”的双入口结构。前台的关键页面包括旅游线路列表带分类筛选和分页、线路详情行程安排、景点列表、价格天数、攻略文章列表与详情、个人中心收藏记录、出行订单。后台则要覆盖分类管理、线路维护、攻略审核、订单查看这几个高频操作。这种业务模型非常典型CRUD 覆盖率高但又不像纯管理系统那么枯燥用来学习 SpringBoot Vue3 全栈开发刚刚好。1.2 前后端分离架构的数据流转这套源码采用的是前后端完全分离的架构前端跑在 Nginx 或者 Vite 开发服务器上后端是一个独立的 SpringBoot 服务二者通过 HTTP 接口通信前端用 axios 发请求后端返回统一结构的数据。这跟传统的服务端渲染最大的区别在于页面渲染逻辑完全交给浏览器后端只负责输出 JSON。这样做的好处很实际前后端可以并行开发前端 mock 数据后端测接口互不阻塞。接口可以复用将来做小程序、App 时可以直接调同一套 API。部署时可以独立扩容哪边压力大就扩哪边。数据流转的链路大概是Vue3 组件触发事件 → Pinia/组件 data 中调用封装好的 request 函数 → axios 携带 token通常放 Header发到后端 → SpringBoot 的 Controller 接收参数 → Service 层处理业务 → MyBatis 拦截器介入执行 SQL → MySQL 返回结果 → 逐层封装成统一 Result 对象 → 前端拿到数据渲染页面。理解这条链路比记住任何一行代码都重要。后端能拿到什么数据、前端该怎么传参、错误怎么抛全靠这条链路的每个环节约定的格式是否一致。1.3 技术栈选型背后的真实考量很多人选技术栈是“哪个火爆选哪个”但这套源码的选型是有明确理由的我来逐一拆SpringBootJava 系做接口服务的默认选项。内置 Tomcat、自动配置机制、生态成熟。相比 SSM 时代的繁琐 XML 配置SpringBoot 的 starter 机制让你十分钟就能起一个能跑的服务。这里没有用 Spring Cloud因为单机服务不需要服务注册与发现引入微服务一套反而加重学习负担。Vue3 ViteVue3 的 Composition API 让逻辑复用更干净setup语法糖写起来比 Options API 舒服很多。Vite 开发服务器基于 ESBuild冷启动快到没朋友。如果你之前用 vue-cli 配 webpack 启动要等半分钟换 Vite 就是秒开。这套源码用 Vue3 而不是 Vue2还有一个很实际的原因生态已经全面转向 Vue3Element Plus、Vant 等组件库的新版本都只对 Vue3 做完整支持。MyBatis为什么不用 MyBatis-Plus 或者 JPAMyBatis 的 SQL 掌控力最强XML 里写多少 SQL 完全由你决定复杂多表连接时不容易“被框架限制”。虽然 MyBatis-Plus 用起来省事但源码级学习项目里手写 SQL 更能让你理解数据库操作的本质。而且 MyBatis 的动态 SQL 机制正是这套系统里做条件筛选、批量插入的核心武器。MySQL开源、易得、生态成熟。旅游系统的数据量级远没到需要上 PostgreSQL 特殊特性的程度MySQL InnoDB 引擎支持事务正好满足订单和支付这类需要一致性的场景。这套组合放在当下依然是 Java 全栈的“标准答案”拿它学习学到的东西不会过时。2. 后端 SpringBoot MyBatis 核心实操2.1 领域模型与数据库表设计对旅游系统来说表设计是整个项目的命脉。表没设计好后面每个接口都像在螺蛳壳里做道场。这套源码的表结构大致包含七张核心表我先给你列出来再逐个解释表名用途关键字段user用户表id, username, password, nickname, avatar, phone, create_timecategory线路分类表id, name, sort_orderroute旅游线路表id, category_id, title, summary, cover_img, days, price, difficulty, view_count, status, create_timescenic_spot景点表id, route_id, name, lat, lng, description, sort_orderguide_note攻略文章表id, user_id, title, content, images, status, publish_timefavorite收藏表id, user_id, route_id, create_timetravel_order出行订单表id, order_no, user_id, route_id, contact_name, contact_phone, travel_date, people_count, remark, status, create_time这里有几个设计细节值得你注意线路和景点是一对多关系。一条线路比如“云南丽江五日游”会包含多个景点景点表通过route_id关联到线路并通过sort_order字段控制展示顺序。为什么不用 JSON 字段存景点列表因为景点可能需要单独维护比如抓取地图坐标独立成表更灵活也方便将来做“景点库”。收藏表是典型的用户-线路多对多关系表。它不直接存在 user 表里存一个favorite_route_ids字段因为那样做会导致查询某用户收藏列表要解析字符串、取消收藏要改整个字段、无法统计线路的收藏数。单独建表加唯一索引(user_id, route_id)查询和统计都干净。travel_order 里的 order_no 必须独立生成。不要用自增 ID 当订单号暴露给用户因为自增 ID 可猜测容易被人遍历抓数据。一般做法是拼上时间戳和随机数或者用雪花 ID。所有时间字段统一用 datetime 类型排序查询方便。不要为了省空间用 timestamp因为 timestamp 有 2038 年问题而且时区处理更麻烦后面部署章节我会详细说时区这个坑。2.2 SpringBoot 集成 MyBatis 的关键配置集成本身不难但绝大部分人第一次跑这套源码失败都栽在配置文件上。先说 pom.xml核心依赖就这几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency这里有一个我踩过的坑mybatis-spring-boot-starter的版本要跟 SpringBoot 版本兼容。如果你是 SpringBoot 2.7用 2.3.x 没问题如果你是 SpringBoot 3.x那就要用mybatis-spring-boot-starter:3.0.x因为 SpringBoot 3 基于 Jakarta EE包名都从javax改成jakarta了直接照抄旧配置启动时会报ClassNotFoundException。然后是application.yml里的核心配置spring: datasource: url: jdbc:mysql://localhost:3306/travel_guide?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.travelguide.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逐个解释配置项mapper-locations指定 XML 映射文件的位置不配这个MyBatis 找不到你的 SQL启动不报错但一查询就报Invalid bound statement。这个错在上线排查里出现频率极高十个人有八个是因为漏配或路径写错。map-underscore-to-camel-case开启驼峰映射。数据库字段是create_timeJava 属性是createTime开这个配置后 MyBatis 自动帮忙映射不用你写一堆resultMap。这是一个偷懒但极其好用的开关。log-impl配成 StdOutImpl在控制台直接打印 SQL。开发联调阶段非常有用肉眼可见 SQL 长什么样、参数传了什么。生产环境建议关掉或改成 logback 输出到文件别让 SQL 刷屏。pagehelper的三个配置helper-dialect告诉插件数据库方言reasonable为 true 时如果页码小于 1 自动返回第一页大于总页数自动返回最后一页防止接口被异常参数打到很深的分页support-methods-arguments支持从方法参数里取分页字段。最后别忘了启动类上加上MapperScan注解否则 Mapper 接口不会被扫描进 Spring 容器SpringBootApplication MapperScan(com.travelguide.mapper) public class TravelGuideApplication { public static void main(String[] args) { SpringApplication.run(TravelGuideApplication.class, args); } }2.3 分页插件用对才不翻车旅游线路列表页必须有分页不然数据一多页面直接卡死。这套源码接入了 PageHelper用法看起来简单但很多人用着用着就“不生效”了。我先把正确的用法写出来public PageResultRouteVO pageRoutes(RouteQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListRouteVO list routeMapper.selectRoutePage(query); PageInfoRouteVO pageInfo new PageInfo(list); return PageResult.of(pageInfo); }然后是你必须记住的三条铁律第一PageHelper.startPage后面必须紧跟第一条 Mapper 查询。它背后原理是ThreadLocal存了分页参数MyBatis 拦截器在下一句 SQL 执行时把参数捞出来拼上LIMIT和COUNT然后清掉。如果你在中间插了别的查询语句分页参数会被那条查询“吃掉”导致目标查询没分页、多余的查询被分页数据全乱。这一点真的值得反复强调我见过太多凌晨三点还在查这个问题的兄弟。第二多表联查时 COUNT 语句可能不准。PageHelper 的自动 count 对单表没问题但如果是多表 join 且做了 distinct 去重生成的 count 可能跟 list 的条数对不上。遇到这种情况可以自己手写 count 方法或者干脆用PageInfo里的总数做兜底别太依赖插件的自动计算。第三前端传的页码必须做校验。虽然配了reasonable: true但接口层面还是建议手动校验pageNum 1时直接赋值为 1。这是防御式编程别把安全完全交给框架。2.4 MyBatis 缓存默认的坑与正确的开法热词里出现了“mybatis缓存 面试题”这块确实是高频考点也是这套系统里值得研究的东西。MyBatis 有一级缓存和二级缓存默认行为是一级缓存SqlSession 级别默认开启。同一个 SqlSession 中执行两次相同的查询第二次直接走缓存。但注意Spring 整合 MyBatis 后每次 Mapper 方法调用都会创建新的 SqlSession默认情况下方法结束就关闭所以一级缓存实际上只在同一个方法内有效。跨方法调用别指望缓存命中。二级缓存Mapper 级别默认关闭。开启方式是在对应的 XML 映射文件里加一个cache/标签多个 SqlSession 就能共享缓存。那这个系统里应该怎么用呢我的建议是别无脑全开。二级缓存有一个非常经典的脏读场景——多表联查。比如查询“线路详情”时要 join 景点表如果给这个查询开启了二级缓存结果它以selectRouteDetail这个 namespace 为 key 缓存了整条数据。但与此同时后台管理员修改了景点表的某个字段触发了updateScenicSpot这个 statement 的缓存清空问题在于它清空的只是自己 namespace 下的缓存根本不会管selectRouteDetail的缓存。于是第二次查询线路详情时拿到的是之前缓存的旧景点数据。这种问题排查起来极其痛苦因为它是“偶发性数据不一致”不是每次都崩。所以我的建议是只在单表查询且数据变更频率很低的接口上开启二级缓存比如分类列表。多表联查一律不开。如果非要在多表场景用就通过CacheNamespaceRef把相关 namespace 绑定起来但那个复杂度完全没必要。另外开发环境强烈建议把log-impl打开这样你能直观看到哪些 SQL 走了 Cache Hit/Miss不用瞎猜。2.5 动态 SQL 与 XML 映射文件写法这套系统的核心查询大多写在 XML 映射文件里而不是注解里。为什么因为 XML 支持动态 SQL逻辑复杂时一眼能看全不用把 Java 代码拼得跟包寿司一样。一个典型的分页条件查询长这样select idselectRoutePage resultTypecom.travelguide.entity.Route SELECT r.*, c.name AS category_name FROM route r LEFT JOIN category c ON r.category_id c.id where if testkeyword ! null and keyword ! AND r.title LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND r.category_id #{categoryId} /if if teststatus ! null AND r.status #{status} /if /where ORDER BY r.create_time DESC /select这里最值得讲的是where标签。它有一个隐藏能力自动去掉第一个AND或OR前缀。如果没有where你写WHERE 11 AND ...虽然也能跑但看着难受。动态 SQL 的四个核心标签——if、where、set、foreach——在这套源码里都用得到if条件判断动态拼 SQL 的基石。where解决多条件组合时AND前缀问题。set用在 update 语句里自动去掉最后一个逗号避免 update 时误把别的字段置空。foreach批量插入或者IN查询场景必备。批量插入时注意 MySQL 连接串要加allowMultiQueriestrue否则批量 SQL 会被拦截。#{}和${}的区别在这里也必须强调。#{}是预编译参数占位符最终以?传给 JDBC能防 SQL 注入${}是直接字符串拼接把变量内容拼进 SQL。这个系统里凡是用户输入的内容一律用#{}${}只用在排序列名这种不可变的场景。你可以拿接口用 sqlmap 扫一下凡是出现${}的地方基本都是风险点。3. 前端 Vue3 实现与联调细节3.1 Vite 搭建 Vue3 工程的结构这套系统的前端工程用 Vite 创建标准命令是npm create vitelatest travel-web -- --template vue-ts创建完以后目录结构里你要重点关注这几个位置src/router路由配置首页、线路详情、攻略列表、个人中心、后台管理都在这里。src/storePinia 状态管理token、用户信息放这里。src/api按模块拆分的接口调用文件比如route.js、guide.js、order.js。src/utils/request.jsaxios 实例封装所有请求都从这出去。路由配置建议用懒加载const routes [ { path: /, component: () import(/views/Home.vue) }, { path: /route/:id, component: () import(/views/RouteDetail.vue) } ];懒加载的好处是首屏只加载需要的 JS访问到对应路由时才异步加载组件。旅游网站首页元素多如果首屏全量加载组件白屏时间会明显变长。3.2 核心页面模块与组件拆分思路线路列表页是前台流量最大的页面。它通常包含三个区域顶部分类 Tab从 category 表动态拉取、中间的搜索区关键字 筛选条件、下方的卡片列表。列表组件我建议拆成RouteCard.vue每个卡片展示封面图、标题、天数、价格、难度标签。列表轮询滚动加载或者点击分页按钮调用getRoutePage接口传pageNum和pageSize。线路详情页要展示的信息最重轮播封面图、基本信息卡片、行程安排按天展示景点列表、攻略列表、收藏按钮和下单入口。这里有个细节封面图建议用 OSS 或对象存储的 CDN 地址不要在数据库里存 base64 图片否则数据库表会迅速膨胀。个人中心包含收藏列表和订单列表。收藏列表本质上就是把 favorite 表 join 上 route 表前端只需要展示线路标题、图片和一个“取消收藏”按钮。订单列表需要区分状态待出行、已完成、已取消每个状态对应不同的操作按钮。后台管理页面不用复杂表格 表单弹窗就够了。Element Plus 的el-table和el-dialog组合基本能覆盖所有后台需求。3.3 axios 封装与权限处理axios 封装是所有接口调用的入口这套源码里的request.js建议包含以下能力基础 URL 配置、请求拦截器自动携带 token、响应拦截器统一处理 code 和错误提示、超时设置。核心结构如下import axios from axios; import { ElMessage } from element-plus; import { useUserStore } from /store/user; import router from /router; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use((config) { const userStore useUserStore(); if (userStore.token) { config.headers.Authorization Bearer ${userStore.token}; } return config; }); request.interceptors.response.use( (response) { const res response.data; if (res.code 200) { return res.data; } if (res.code 401) { userStore.clearToken(); router.push(/login); } ElMessage.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg || 请求失败)); }, (error) { ElMessage.error(error.message || 网络异常); return Promise.reject(error); } ); export default request;一个非常重要的细节401 拦截跳登录。如果后端返回未授权前端统一清理本地 token 再跳转登录页比每个业务组件里自己判断要干净得多。baseURL: /api是配合 Vite 代理用的。开发环境前端跑在 5173 端口后端跑在 8080 端口直接请求后端会有跨域问题。在vite.config.ts里配代理server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } }这里我踩过一个坑rewrite不写的话后端接口路径会变成/api/route/list但后端 Controller 映射的是/route/list结果所有接口全 404。改了vite.config.ts之后必须重启 dev server它不会热更新这个配置文件。3.4 前后端联调的踩坑清单联调阶段的问题往往比编码阶段更折磨人我把这套源码联调中最高频的问题总结成一张表现象原因解决方案接口 404Vite 代理 rewrite 没配好检查 vite.config.ts 里 rewrite重启 dev server接口 405前端 GET 后端 POST反之检查 axios method 和后端 GetMapping/PostMapping 是否一致数据返回但字段为 null后端开启了驼峰映射但前端字段名不对检查 Java 属性名、JSON 序列化后的字段名、前端接收字段名三者一致401 循环跳转请求拦截器对登录请求也加了 token对/login接口放行不加 Authorization 头文件上传跨域后端没允许 Content-Type: multipart/form-data后端 CORS 配置里allowedHeaders写*日期显示为时间戳Jackson 默认序列化 Date 为 long全局配置spring.jackson.date-format或使用JsonFormat热词里提到“vue3 组件”和“vue3 面试题”我再补充一点实战建议组件拆分不要过分。列表卡片、搜索表单这种复用性强的可以拆详情页里只出现一次的模块没必要硬拆成组件拆太细反而导致 props 传参满天飞维护成本剧增。4. 安全、部署与常见问题排查4.1 全局过滤器与 XSS 防护热词里有“springboot项目全局过滤器处理上传pdf文件时xss攻击”这说明安全问题确实是实际需求。XSS跨站脚本攻击的本质是用户提交的内容里带了一段恶意脚本后端原样存下来前端再原样渲染脚本就执行了。解决思路有两个方向转义存储或转义输出。后端入手最可靠的做法是写一个全局过滤器把请求参数里的特殊字符转义掉。实现上就是实现OncePerRequestFilter包装HttpServletRequestWrapperpublic class XssFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { chain.doFilter(new XssHttpServletRequestWrapper(request), response); } }核心逻辑在包装类里重写getParameter和getInputStream把,,,,替换成 HTML 实体。这里有个注意点富文本编辑器提交的攻略正文本身就有合法的p、img标签无脑转义会把正常内容也破坏了。所以通常要区分字段富文本字段走白名单过滤只允许少量安全标签普通字段走严格转义。那 PDF 上传和 XSS 有什么关系实际上 PDF 文件本身不是 XSS 的主战场真正的风险在文件名和 PDF 自带的 URL 参数。上传文件时后端必须校验扩展名、限制文件大小并且把文件名重新生成比如 UUID不要让用户原始文件名直接落库。否则用户传一个scriptalert(1)/script.pdf前端展示文件名时脚本就执行了。4.2 MySQL 编码与时区两个隐藏的地雷MySQL 的编码问题我在这套源码里也吃过亏。建库时如果没有显式指定字符集默认可能是 latin1中文存进去直接乱码。正确姿势CREATE DATABASE travel_guide DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这里强调一下utf8mb4 不是 utf8 的升级版那么简单MySQL 的 utf8 实际上只能存 3 字节像 emoji 表情以及生僻字都是 4 字节用 utf8 会报Incorrect string value错误。现在做的项目一律用 utf8mb4没有例外。时区问题的典型表现是数据库时间正常Java 拿到后差了 8 小时或者 14 小时。根本原因是 JDBC 连接串里的serverTimezone没设或者 MySQL 的time_zone设置跟 JVM 不一致。统一的方法是在数据库连接串上加上serverTimezoneAsia/Shanghai并且 MySQL 执行SET GLOBAL time_zone 08:00两边保持一致就不会出幺蛾子。4.3 打包部署上线流程前后端分离项目的部署流程对新手来说第一次跑会比较懵我按步骤讲后端打包在项目根目录执行mvn clean package -DskipTests生成的 jar 在target/目录下用java -jar travel-guide.jar启动。注意生产环境可以用--spring.profiles.activeprod切换配置java -jar travel-guide.jar --spring.profiles.activeprodapplication-prod.yml里配生产数据库连接密码不要写明文用环境变量注入spring: datasource: password: ${DB_PASSWORD}前端构建npm run build构建产物在dist/目录。用 Nginx 托管server { listen 80; server_name travel.example.com; root /var/www/travel/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }关键点是try_files $uri $uri/ /index.html这是 SPA 的路由回退否则你访问/route/1刷新页面时 Nginx 会去找物理路径直接 404。location /api/反向代理到后端这样前端请求/api/route/listNginx 转发到后端 8080 端口的/route/list。数据库导入在部署前完成mysql -u root -p travel_guide.sql4.4 常见问题速查表把我这些年跑 Java 全栈项目遇到的高频问题整理成一个速查表这套源码的启动和运行大概率逃不出这几个问题症状可能原因解决办法启动报Failed to configure a DataSource数据源配置缺失或连接串错误检查 application.yml 中 url/username/password启动报ClassNotFoundException: jakarta.servlet.*SpringBoot 3 用了旧版 mybatis starter换成 mybatis-spring-boot-starter 3.x查询报Invalid bound statementmapper XML 路径没配或 namespace 写错检查 mapper-locations 和 XML namespace分页不生效startPage 后不是紧邻查询调整代码确保 startPage 后直接调用 Mapper 方法前端接口跨域CORS 未配置或代理错误开发用 Vite proxy生产用 Nginx 代理中文乱码数据库/连接字符集不对统一 utf8mb4连接串加 characterEncoding上传文件大小超限报错默认 1MB 限制在配置里调大spring.servlet.multipart.max-file-sizeSQL 被截断执行报错连接串缺 allowMultiQueries需要批量执行时添加该参数JVM 内存溢出频繁默认堆太小启动加-Xms256m -Xmx512m还有一点关于“怎么将springboot jar反编译成项目”这种热词我的态度很明确拿到一套源码别急着用反编译工具去“逆向”什么黑科技。你把源码跑起来从pom.xml读起捋清楚 Controller → Service → Mapper 这条调用链再对着数据库表结构看字段映射收获比反编译大十倍。反编译工具比如 jadx 或 procyon适合看第三方库的逻辑不适合看这种业务型项目。有人会问MySQL 的安装配置是不是个大坑。说实话在 Windows 上装 MySQL 8.x解压版比安装版更适合开发者下载 ZIP 包、解压、写my.ini端口、字符集、basedir、datadir、mysqld --initialize-insecure、mysqld --install、net start mysql六步搞定。装完之后第一件事就是改 root 密码并创建业务账号别用 root 直连业务库。这套源码我在部署时也踩过Access denied for user rootlocalhost的坑多半是 8.0 的认证插件问题用ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY password;可以兜底。最后再给一个实在的扩展方向这套系统拿到手之后别急着加功能先把日志体系做好。引入logback-spring.xml区分 info 和 error 级别输出把/logs目录挂到外部。这样系统上线之后你才能知道用户实际在点哪里、哪个接口慢、哪个 SQL 跑了全表扫描。没有日志的系统出了问题就像在黑屋子里抓猫你连猫在不在屋里都不知道。我实际跑这套源码时有个很大的体会真正的技术提升不是看它写得有多完美而是你动手改了一个功能之后再回来看它原本的实现才会明白设计者当时为什么这么写。比如你试着给线路表加一个“推荐指数”字段从前端表单到数据库变更再到列表展示全部走一遍你对这套全栈链路的理解会比读十遍源码都深刻。源码是地图路得自己走。
返回列表