1. 从“三国之家”看企业级网站管理系统的通用骨架
看到这个标题的时候,我第一反应是:这不只是一套“三国主题内容站”的源码,它背后其实藏着一个非常标准的企业级后台管理系统通用骨架。你可能没做过三国题材的项目,但你大概率做过这类系统:前台展示内容、后台管理数据、用户登录、权限分级、分类发布文章、图片上传、配置站点参数。
这类项目在市面上数量极多,但真正把架构分层、权限控制、前后端联调、数据表设计都做到能直接落地复用的,反而不多。这套基于SpringBoot + Vue + MyBatis + MySQL的“三国之家网站管理系统”,刚好覆盖了以上所有核心点。你把它拿过去,换掉“三国武将”“战役事件”等主题内容,配上你自己的业务字段,就变成了一套通用内容管理平台。
所以这篇博文我想做一件比较实际的事:把这套系统的架构逻辑、核心模块设计、数据库建模思路、前后端联调细节、部署过程和常见的坑,逐一拆开来说清楚。无论你是想把这套代码拿来做二次开发,还是想参照它的设计思路去搭一套自己的网站管理系统,这篇文章都能帮你在动手之前先建立一张“地图”。
为了让不同基础的读者都能跟上,我会尽量从“为什么这样做”的角度去讲,而不是只甩代码片段。像是权限表为什么要拆三张、前端路由守卫的作用在哪里、MyBatis的Mapper接口和XML怎么配合,这些基础点我会讲透,但也会直接给出可以拿去用的实操结论。
2. 核心功能模块拆解:前台展示与后台管理如何分工
2.1 前台门户与后台管理的模块边界
“三国之家”这类系统的设计思路非常典型:一个前台给访客浏览,一个后台给管理员维护,两者通过同一套数据库连接起来。刚开始接触前后端分离的人,最常犯的错误就是“前台和后台分成了两个完全独立的世界”,甚至建了两套用户表——这其实是在给自己挖坑。
在这套系统里,前台模块主要承担这些职责:
- 网站首页的聚合展示:推荐位、最新内容、热门内容、轮播图
- 三国阵营页:魏、蜀、吴三个阵营的展示逻辑
- 武将列表与武将详情页
- 战役事件时间线或列表
- 内容分类导航与站内搜索入口
- 用户注册、登录、评论、收藏等交互
而后台管理模块则负责:
- 管理员登录(不同于普通用户登录的权限校验)
- 内容发布与编辑(武将数据、战役文章、新闻公告等)
- 内容分类管理
- 评论审核与管理
- 用户管理与权限分配
- 轮播图、公告等站点参数配置
- 数据统计基础面板
这里有个关键设计值得学习:前台和后台都访问同一套用户表,但在后台里用户被赋予更高角色和权限菜单。这是RBAC(基于角色的访问控制)模型的核心思路。你注册一个普通账号,进后台只能看到“个人中心”;管理员登录同一入口,却能看到“内容管理”“用户管理”等完整菜单。
2.2 典型页面流程:从用户点击到数据回显
我们拿一个最常见的场景——访客点击“武将详情”来走一遍系统全流程。
在传统单体项目或纯模板项目中,用户点击链接之后,后端直接把渲染好的HTML返回来。但在SpringBoot + Vue这套架构里,流程就变成了这样:
- 浏览器访问前端路由
/detail/123,Vue Router拦截到该路径,匹配到武将详情组件 - 组件中通过Axios调用后端接口:
GET /api/general/123 - 后端Controller接收到请求,调用Service层,Service调用Mapper
- MyBatis执行SQL查询武将基础信息以及关联的阵营、战役、评论数据
- 数据以JSON格式返回给前端,Vue使用模板语法渲染到页面上
这个流程你做一两次就会形成肌肉记忆,但初学者经常会忽略一个重要环节:URL上的ID怎么传到后端接口里。在Vue Router里,你需要在路由配置里声明动态参数:
// router/index.js { path: '/detail/:id', name: 'GeneralDetail', component: GeneralDetail }然后在详情页组件里取到这个参数:
// views/GeneralDetail.vue import { useRoute } from 'vue-router' import { getGeneralById } from '@/api/general' const route = useRoute() const generalId = route.params.id getGeneralById(generalId).then(res => { // 渲染数据处理 })很多新手卡在这里的原因是:路由配置里写了:id,但组件里却用route.query.id或者直接硬编码了一个数值,导致页面永远显示的是同一条数据。这是我见过最高频的“前后端联调不成功”的原因之一,后面第5章我会再提到。
3. 权限控制与数据安全:SpringBoot侧的核心逻辑
3.1 RBAC权限模型:为什么是三张表起步
做后台管理系统,权限控制是绕不开的。我见过很多项目在Controller里堆if (user.getRoleId() == 1)这种硬编码判断,小项目初期跑得爽,用户角色一多就完蛋。这套“三国之家”后台用的是标准RBAC模型,你数一下数据库就会发现,权限相关的核心表至少有这些:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| sys_user | 用户表,存储账号密码、昵称、状态 | id, username, password, status, avatar |
| sys_role | 角色表,如超级管理员、编辑、普通用户 | id, role_name, role_code, description |
| sys_menu | 菜单/权限表,后台可见的菜单项和按钮权限 | id, parent_id, menu_name, path, perms, icon |
| sys_user_role | 用户-角色关联表 | user_id, role_id |
| sys_role_menu | 角色-菜单关联表 | role_id, menu_id |
这是我为什么强调“三张表起步,五张表够用”的原因:你要做的是关联关系,而不是把权限字符串塞进用户记录的一个VARCHAR字段里。拆开的好处很多,比如你给“编辑”角色增加一个“评论审核”的菜单权限,只需要在sys_role_menu里插一条记录,把角色和菜单关联起来就行,完全不用动用户表。
在这套系统的实际表结构中,登录后的用户信息会被查出来,然后根据用户ID查出角色集合,再根据角色ID查出菜单权限集合。后续每次访问后端接口,就通过拦截器或过滤器去校验当前用户是否具备该接口要求的权限标识。
3.2 拦截器或切面:接口权限校验的实现思路
SpringBoot里做接口权限校验,常见的做法有两种:Spring MVC的Interceptor(拦截器)和AOP(切面)。两者本质都是在Controller方法执行前一环去“检查并放行或拦截”,但实现上有些差异。我拿拦截器的方案来展开,因为这在你部署这套系统时会直接看到相关配置。
// com.example.config.AuthInterceptor @Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取请求头中的Token String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; } // 从Redis中根据Token获取用户信息 LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get("login_token_" + token); if (loginUser == null) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"登录状态无效\"}"); return false; } // 将用户信息放入ThreadLocal,方便后续业务逻辑获取 UserContextHolder.set(loginUser); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContextHolder.clear(); } }这段代码的核心意思是:每个请求进来先拿Token,用Token去Redis里找对应的登录用户,找不到直接打回401,找到了就放进ThreadLocal供本次请求使用。用Redis存登录状态的好处是:服务重启之后登录态不丢(如果不持久化的话会有隐患)、可以统一控制过期时间、和数据库查询相比更快。
但这里有一个在“企业级”项目里必须注意的点——拦截器拦住了CORS预检请求。前端Vue项目发跨域请求时,浏览器会先发一个OPTIONS请求,如果拦截器把所有请求都拿去校验Token,OPTIONS请求会直接被拦下来,导致前端报“CORS跨域错误”。所以你在配置拦截器时一定记得排除OPTIONS请求:
// WebMvcConfig中不拦截的路径示例 // .excludePathPatterns("/login", "/register", "/api/public/**", "/error", "/**/*.html")或者更稳妥的做法是,在拦截器里直接放行OPTIONS请求。这个问题我见过太多次了,前端的Network里明明是200,但浏览器就是报跨域错误,其实是拦截器把预检请求处理掉了。
3.3 密码存储与登录接口的安全性设计
关于用户的密码,这套系统因为“演示”或“课程设计”属性,有些版本里面接是明文存储的。但如果你要把这套系统用于企业生产或毕业答辩展示,我建议无论如何都要至少做一次哈希加密。SpringBoot生态里最常用的方式是BCrypt:
// PasswordEncoder 配置 @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 注册时加密 String encodedPassword = passwordEncoder.encode(rawPassword); // 登录时校验 boolean matched = passwordEncoder.matches(rawPassword, encodedPassword);BCrypt的细节不用深入,但有一点值得提:它每次生成的哈希值都带随机盐,所以同一个密码两次加密结果不一样。这意味着你不能用UPDATE user SET password = 'xxx'去直接修改数据库密码,而必须通过代码去生成加密串。你自己二次开发时,如果发现修改了数据库密码但登录不了,十有八九是直接写入了明文。
登录接口的设计里还应该包含验证码机制。后台登录尤其需要,否则就会被暴力破解。常见方案是后端生成一个图片验证码,同时把验证码答案存到Redis,设置60-120秒过期;前端把验证码的随机标识和用户输入内容一起提交,后端比对。这套系统的登录流程完全可以把这些缝进去——核心登录逻辑已经有了,你只需要在登录接口里增加一个验证码字段的校验。
4. 数据库表设计:围绕“内容管理”建模的实战思路
4.1 核心数据表之间如何关联
一个以内容展示为主的网站,数据库表设计的好坏直接决定了后续开发的效率。我建议你千万别急着写代码,先把表关系理清楚。拿“三国之家”来举例,它的核心数据对象有:用户、分类、文章、武将、战役、评论、收藏。
它们之间的关系大概是这样的:
- 分类表(category)存储文章的栏目,比如“史实”“演义”“新闻”“攻略”
- 文章表(article)通过
category_id关联分类,通过author_id关联用户 - 武将表(general)存独立于文章的特有属性,比如武力值、统率值、阵营、生平简介
- 战役表(battle)存战役名称、时间、地点、参战方、结果、背景描述
- 评论表(comment)通过
article_id或general_id关联具体内容,通过user_id关联评论者
这里有一个设计经验可以分享:不要把武将信息塞进文章表里。很多新手做这类系统时会把所有信息揉进一张表,字段越加越多,最后变成一张“大杂烩表”。正确的方式是:文章表关注“发布的这篇内容是什么”,武将表关注“这个武将本身有多少属性”,两者通过ID或外键关联。这样后续如果产品经理说要在首页展示“武将战力排行榜”,你只需要查武将表并排序即可,不用把文章表翻个底朝天。
4.2 从建表SQL看常见的字段设计坑
下面这段是仿照这套系统里武将表的建表SQL简化出来的,你可以看一下字段设计:
CREATE TABLE `general` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(64) NOT NULL COMMENT '武将姓名', `camp` varchar(16) DEFAULT NULL COMMENT '所属阵营:wei/shu/wu', `force_value` int(11) DEFAULT NULL COMMENT '武力值', `strategy_value` int(11) DEFAULT NULL COMMENT '统率/谋略值', `title` varchar(128) DEFAULT NULL COMMENT '封号或称号', `profile` text COMMENT '人物简介', `image_url` varchar(255) DEFAULT NULL COMMENT '人物图片地址', `status` tinyint(1) DEFAULT '1' COMMENT '状态:1启用 0禁用', `sort_order` int(11) DEFAULT '0' COMMENT '排序权重', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_camp` (`camp`), KEY `idx_status` (`status`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='三国武将表';这里有几个细节你以后建表时也用得上:
第一,utf8mb4而不是utf8。因为utf8mb4才完整支持中文和各类生僻字、表情符号。你以为“三国人物”都是常见字就不需要?错了,评论表里用户极可能会发表情符号,如果表是utf8,插入表情就会报错或变成问号。
第二,索引不贪多,但查询频率高的字段要建。上面给camp和status建了普通索引,这是合理的,因为列表页通常会“按阵营筛选 + 按状态过滤”。但是不要给每个字段都加索引,索引过多会拖慢写入速度,也会浪费磁盘空间。
第三,时间字段使用datetime而不是varchar。有些从其他语言转过来的开发者习惯用字符串存时间,结果后续排序、比较、分组都非常痛苦。datetime配合DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,可以自动维护创建和更新时间,省掉大量手写代码的功夫。
4.3 MyBatis中多表关联查询的Mapper写法
数据库表设计好了,接下来是查询。这套系统用MyBatis作为持久层框架,它的核心优势是把SQL语句从Java代码里剥离出来,放到XML文件中维护。你可能会问:为什么不用JPA?因为在这个场景下,MyBatis的SQL可控性更强,尤其是多表关联、动态条件查询、分页这些内容型网站的高频操作,自己写SQL反而更直观。
举个典型场景:后台管理页需要展示“文章列表”,同时显示文章所属分类名称和发布人账号。这个列表的SQL就涉及到文章表、分类表、用户表三张表的关联:
<!-- GeneralArticleMapper.xml --> <select id="selectArticleWithCategoryAndAuthor" resultType="java.util.Map"> SELECT a.id, a.title, a.category_id, c.name AS category_name, a.author_id, u.username AS author_name, a.status, DATE_FORMAT(a.create_time, '%Y-%m-%d %H:%i:%s') AS create_time FROM article a LEFT JOIN category c ON a.category_id = c.id LEFT JOIN sys_user u ON a.author_id = u.id <where> <if test="keyword != null and keyword != ''"> AND a.title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="categoryId != null and categoryId != ''"> AND a.category_id = #{categoryId} </if> <if test="status != null"> AND a.status = #{status} </if> </where> ORDER BY a.create_time DESC </select>这里我最想提醒的是<where>标签 +<if>标签的动态SQL组合。你看上面的代码,无论前面的if命中几个,生成的SQL都不会出现“多余的WHERE和AND”问题。比如用户只传了keyword=关羽,SQL就会是WHERE a.title LIKE '%关羽%' ORDER BY ...,开头那个AND被MyBatis的where标签自动处理掉了。如果你不用<where>而是手动拼WHERE 1=1 AND ...,也不是不能用,但会显得比较笨拙,而且维护起来容易出错。
再提一下分页插件。SpringBoot + MyBatis项目里,大家几乎默认使用分页插件来做列表分页。你只需要在Service层设置页号和页大小:
PageHelper.startPage(pageNum, pageSize); List<Map<String, Object>> list = articleMapper.selectArticleWithCategoryAndAuthor(queryMap); PageInfo<Map<String, Object>> pageInfo = new PageInfo<>(list);返回给前端时就可以携带total,list,pageNum,pageSize等字段。前端表格组件直接绑定这些字段就能完成分页渲染。分页插件本身不会泄露数据,但你需要注意一个坑:如果PageHelper.startPage之后执行的SQL不止一条,分页逻辑可能会串,所以它必须紧挨着你要分页的那条SQL调用。
5. 从源码到线上运行:部署配置与踩坑实录
5.1 本地启动的完整步骤梳理
很多拿到源码的人第一步就问“怎么跑起来”。我就直接从上到下捋一遍。前提是你本地已经安装了 JDK(建议8或11)、MySQL(5.7+或8.0)、Maven、Node.js。
第一步,数据库初始化。在MySQL里创建一个数据库,然后把项目提供的SQL脚本导入:
mysql -uroot -p CREATE DATABASE sanguo_house DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; use sanguo_house; source /your/path/to/sanguo_house.sql;如果SQL脚本里有CREATE DATABASE或USE语句,你就不用在命令行手动建库了,直接source整个文件即可。导入过程中如果报“字段太长”“索引超长”之类的错误,多半是字符集设置不对,需要检查表是否用了utf8mb4,以及MySQL版本是否支持(5.7以下对长索引支持不好)。
第二步,修改后端数据库连接配置。在application.yml或application.properties中:
spring: datasource: url: jdbc:mysql://localhost:3306/sanguo_house?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver这里必须提醒你的坑是serverTimezone。如果你不设置或设置为UTC,可能查询出来的时间和本地时间差8小时。建议统一写Asia/Shanghai。另外MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver,老版本是com.mysql.jdbc.Driver,别配错了导致启动报错。
第三步,启动后端。在项目根目录执行:
mvn clean package -DskipTests java -jar target/sanguo-web-1.0.0.jar如果你不想打包,在IDE里直接运行主类也行。看到类似Tomcat started on port(s): 8080的日志就说明后端起来了。
第四步,启动前端。进入前端目录:
cd frontend npm install npm run dev如果npm install很慢或者报错,可以切换镜像源再试:
npm config set registry https://registry.npmmirror.com rm -rf node_modules package-lock.json npm install5.2 跨域配置:为什么后端接口“能被Postman调到却调不通”
这是前后端分离项目里最经典的问题了。后端8080端口,前端5173端口(Vite默认)或8081,这俩端口不同,浏览器在发Ajax请求时会触发同源策略。你在Postman里能调到数据,是因为Postman没有同源策略限制。而浏览器看到前端页面发起的请求要访问另一个“源”的接口,就会先做跨域检查。
解决方法有两种:
第一种,后端允许跨域。在SpringBoot里配置CORS规则,加一个配置类:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }第二种,前端通过Vite或Webpack配置代理。Vite里修改vite.config.js:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这两种方法各有各的适用场景。如果你是拿这套系统做毕业设计或企业内部演示,后端的CORS配置更省事,改完一处全站生效。但如果你将来要部署到Nginx,让Nginx做反向代理来解决跨域,其实更贴近生产环境的做法——因为前端和后端服务通常在同一个域名下提供,由Nginx把/api路径转发到后端的8080端口,浏览器看到的就始终是同源请求,不会产生跨域问题。
5.3 线上环境部署时常见的三个隐蔽问题
当你准备把项目部署到一个云服务器上,会比本地多出几个问题。我把最常见的三个“隐蔽坑”列出来。
隐藏坑一:前端build后的静态资源路径不对。Vue项目打包默认生成的index.html里,引用JS和CSS的路径可能是根路径/assets/...。如果你是把前端文件放在Nginx的某个子路径下面,比如通过http://你的服务器IP/sanguo/来访问,那么这些资源文件就找不到。解决办法是在vite.config.js里设置基础路径:
base: './'或者你直接把前端文件部署在nginx/html根目录下,用一个单独的域名或IP访问,就不会有这个困扰。
隐藏坑二:后端端口被防火墙挡住。云服务器如果购买了安全组,默认只开放22、80、443端口。你的后端跑在8080,外网是访问不到的。你可能会想“那我直接开放8080端口不就行了?”行是行,但这样就把后端接口直接暴露到了公网,更安全的做法是:后端端口只在内网监听或保持防火墙不放行,由Nginx通过proxy_pass转发,外部浏览器始终只和80/443端口通信。Nginx配置示例:
server { listen 80; server_name your_domain_or_ip; location / { root /usr/share/nginx/html/sanguo-front; index index.html; 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; } }前端部署在80端口根路径下,后端通过/api/前缀被Nginx转发到8080端口的SpringBoot服务。浏览器里访问的http://IP/api/general/list最终会被转发到http://127.0.0.1:8080/api/general/list,整个过程对浏览器来说都是同一个域名,完全不会有跨域问题。
隐藏坑三:Vue Router用了history模式后刷新404。前端路由如果用createWebHistory,刷新页面时浏览器会按照路径去请求服务器,而服务器上并不存在这个真实文件,于是返回404。解决办法就是上面Nginx配置里的try_files $uri $uri/ /index.html;这一行——当找不到真实文件时,把所有请求都回退到index.html,让前端路由自己去解析。这是SPA部署的标配操作,遇到刷新404时先检查这条配置。
5.4 线上运维需要关注的几个字段状态
系统上了线,不等于万事大吉。拿这套“三国之家”来说,我建议你上线前先关注这么几个状态:
后台管理员账号。第一次登录后,立刻改掉默认密码,千万别留着初始化密码。
Redis连接状态。如果你用了Redis做登录Session和验证码存储,那Redis挂了之后,登录功能会直接瘫痪。至少配置一个Redis的监控告警,保证它在崩溃时你能第一时间知道。
数据库备份策略。内容型网站最大的资产就是内容本身。建议每天凌晨通过mysqldump备份一次数据库,备份完复制到异机或对象存储,至少保留7天。这个动作成本很低,但一旦数据库误操作或被入侵,能救你一条命。
部署这件事,本质上就是“把代码从开发者的电脑挪到一台永远开机的服务器上”,但细节非常磨人。上面这些坑,每一个我都踩过不止一次。你现在看到的这段文字,就是我用真金白银的服务器流量费换出来的经验。
6. 二次开发建议:如何把这套代码改造成你自己的系统
6.1 改变主题内容:从“三国”到任意文化资讯站
标题里的“三国之家”听起来很具体,但实际上这套系统的结构可以承载很多类似场景。我曾经把一套类似架构的内容管理系统改成过一个地方民俗文化展示平台,改动主要集中在几个方面:
- 数据库里把“武将表”换成“传说人物表”,字段从武力/统率改为“传说地区”“故事类型”
- 文章分类从“史实”“演义”换成“民间故事”“节庆习俗”“特色美食”
- 前台页面的品牌部分从三国Logo换成地方文化Logo
- 接口路径从
/general改为/figure
因为这些内容都遵循“实体 + 分类 + 文章详情 + 评论”的内容模型,所以整体上不需要动架构,属于典型的“换皮不换骨”。这也是这类系统的核心价值——它真正通用的是那套内容管理流程,而不是三国题材本身。
6.2 功能扩展的优先级建议
如果你不满足于“只把主题换了”,想扩展更多功能,我按优先级给你排个序:
第一优先级:新增富文本编辑器。目前很多类似系统用的是普通文本框或简单编辑器,你能发表带格式的文章、插入图片,用户体验才会完整。目前主流的做法是前端集成富文本编辑器(你懂的,市场上常用的那几款),后端把编辑器的HTML内容存到TEXT字段中,读取时原样返回即可。
第二优先级:增加操作日志表。后台管理系统的审计功能非常重要。建议加一张sys_log表,记录谁在什么时间做了什么操作,比如“管理员A删除了文章ID=123”。实现方式可以是AOP切面,拦截Controller方法,把请求路径、参数、操作人的信息记录下来。
第三优先级:数据统计页面。一个后台管理系统如果只有增删改查,总感觉少了点灵魂。你可以把SQL统计出来:今日文章发布量、用户注册趋势、评论数量、最热文章Top10,然后用一套图表组件(你大概率也猜到了,就是开源的图表库)展示在后台首页。
这三个功能做完,这套系统在“企业级”三个字的含金量上会高出一截。
6.3 改造过程中最容易忽略的规范性事项
我见过很多同学拿到源码后第一件事就是打开IDE改名、改包名、改数据库脚本里的表名,一通操作猛如虎,结果项目跑不起来,原因千奇百怪:
- 改包名时忘了改Mapper XML里的
namespace,导致MyBatis报Invalid bound statement - 改了数据库表名前缀,但Mapper XML里的SQL还引着旧表名
- 改了前端页面的路由路径,但调用后端接口时路径根本没变,结果404
我给你的建议是:第一轮改造不要动包名和项目名,先把“换皮”完成,确保系统能跑通。等熟悉了所有配置文件的依赖关系后,再考虑全面重命名。如果一定要重命名,把application.yml、启动类、Mapper XML配置、前端接口请求封装这四个地方首先对照检查一遍,90%的启动问题都出在这几处。
另外,关于代码注释,这套系统可能并不是很完善。你在二次开发时,建议给自己的新代码都写上关键注释,尤其是表结构和业务状态的含义。比如status字段,1代表启用、0代表禁用,这类约定应该至少写在类或字段上的注释里。否则你过三个月再回头看自己的代码,也会一脸懵。
7. 调试与排错的思路,能帮你省下大把时间
这部分算是额外的经验补充。我在带一些同学做类似项目时发现,很多人遇到Bug的第一反应是“把整段代码贴给AI”,第二反应是“把报错日志全截屏”,却很少有人先自己定位“这个Bug到底发生在哪一层”。前后端分离项目里,调试问题的定位思路其实非常固定。
前端报错或数据不对,先按这个顺序排查:
- 打开浏览器开发者工具,看Network面板,请求发出了没有?如果没发出,多半是前端路由或Axios封装有问题
- 如果请求发出了,看响应状态码。如果是404,先检查URL路径是否拼错,尤其注意后端Controller的
@RequestMapping前缀和前端baseURL是否一致 - 如果是200,但数据为空,再看Response里是否真的返回了
data字段,排查是后端返回的结构问题,还是前端取值层级不对 - 如果请求被CORS拦截,浏览器控制台会明确提示
后端报错,按这个顺序排查:
- 看启动日志里有没有异常堆栈,尤其注意
Caused by后面的根因 - MySQL连接失败:看配置文件的IP、端口、账号、密码
- MyBatis绑定异常:看Mapper接口和XML的namespace、id是否完全匹配
- 数据返回NULL:拼接SQL中的字段名是否和数据库列名对应
排错是一个“二分定位”的过程:先判断是前端还是后端,再判断是路由还是接口,再判断是SQL还是业务逻辑。这套方法对任何项目都适用,越早养成,后面开发效率越高。
本文到这里,核心内容就讲完了。如果你正准备把这套“三国之家网站管理系统”跑起来,我的建议是从第二章的模块拆解开始看,按第三、四章的思路理一遍表结构,再用第五章的步骤去启动。你不需要一次理解所有代码,但先把“数据从哪里来、流程到哪里去”这条主线走通,剩下的就是持续填细节的过程了。