资讯动态

Java博物馆管理系统实战:Spring Boot+MyBatis+MySQL完整实现与避坑

发布时间:2026/10/5 2:57:16 来源:尧图企业网站定制
简介这套基于Java语言的博物馆管理系统设计源码融合Java、HTML、CSS与JavaScript为博物馆信息化管理提供完整解决方案适合JavaWeb学习者、毕业设计或中小型场馆数字化改造参考。压缩包共296个文件约1.32MB其中144个Java源文件覆盖后端业务逻辑与数据库交互126个HTML构建界面骨架另有XML配置、SQL数据库脚本、CSS样式及JavaScript动态交互文件结构清晰便于按模块研读。系统内置藏品信息管理、展览活动管理、访问者管理等典型功能并配套Javadoc与Markdown文档辅助理解代码脉络。目前已有140人学习下载可作为练手项目或课程设计的实用参考。1. 这个“课程设计”级系统为什么值得你认真做一次如果你搜过“Java 博物馆管理系统”大概率是奔着交课程设计、刷毕业设计选题或者单纯想找一套能跑的 Java Web 源码来练手。博物馆管理系统确实是 Java 后端入门阶段最经典的“缝合怪”项目它有用户登录、权限区分、展品增删改查、预约参观、留言反馈、公告发布——比单纯的学生管理系统多了一层业务语义又比商城类项目少了很多支付和分布式复杂度特别适合拿来把 Spring Boot MyBatis MySQL 这条链路完整走一遍。真正动手你会发现这套系统能不能跑起来和你用的框架版本、表结构设计、日期参数传递甚至编码格式都有直接关系网上下载的源码多数直接跑不起来缺的不是代码而是把环境补齐、把坑绕开的经验。这篇文章就把这套系统的技术选型、数据库设计、核心代码和踩坑点一次说清。2. 技术选型与模块拆解先把边界定死再动手写代码2.1 为什么要用 Spring Boot MyBatis而不是 SSH 或 JSP 裸写博物馆管理系统最常见的源码形态有两种早期的是 JSP Servlet JDBC新一些的是 Spring Boot MyBatis Thymeleaf。我一般建议直接选后者理由很简单SSHStruts Spring Hibernate那套已经退出主流教学很多年了而 JSP Servlet 裸写虽然能让你把请求响应流程看得很清楚但数据源管理、事务控制、参数绑定全要手工处理代码量会膨胀到没法维护。Spring Boot 把 Tomcat 内嵌、自动配置、依赖版本管理都解决了你用spring-boot-starter-web加mybatis-spring-boot-starter就能把项目骨架立起来。常见的做法是前后端不分层直接用 Thymeleaf 做服务端渲染。这样对课程设计来说最稳妥不用单独启动 Vue 前端服务不用处理跨域配置部署时一个 jar 包全搞定。如果你想往工程化方向走也可以拆成 Spring Boot 提供 REST 接口、Vue 单独做页面但这会把工作量放大一倍以上对博物馆管理系统这种以 CRUD 为主的系统来说收益不大。依赖选型上有个细节值得注意MyBatis 的 Spring Boot starter 现在有官方版坐标是mybatis-spring-boot-starter版本号 2.3.x 适配 Spring Boot 2.x如果项目用 Spring Boot 3.x 就得换 3.0 以上的版本。很多下载来的源码跑不起来就是因为项目里的 Spring Boot 版本和本机 JDK 版本对不上——Spring Boot 2.x 用 JDK 8 没问题Spring Boot 3.x 要求 JDK 17 起步。2.2 模块怎么拆用户端和管理员端各管什么博物馆管理系统的业务边界其实挺清晰的。用户端面对的是游客注册登录、浏览展品列表、查看展品详情、在线预约参观、提交留言评价。管理员端面对的是博物馆工作人员登录后台、维护展品信息新增、编辑、上下架、管理展品分类、处理预约订单、发布公告、回复留言。还有一些系统级功能比如用户管理、登录日志、操作日志这些是凑“系统完整性”的关键——答辩时老师问一句“你有没有日志记录”有和没有差别很大。前端页面按角色走两个入口登录成功后根据角色字段跳转到不同首页。用户端一般是展示型页面展品卡片列表加分页详情页放大图加文字介绍。管理员端是表格型页面每一行数据带编辑和删除按钮表单校验放在前端和后端各做一遍。这两套页面共用同一套后端接口只是接口上加了权限控制——管理员接口只允许ROLE_ADMIN访问普通用户调了就返回 403。2.3 项目目录结构怎么组织按包名分别按页面分源码的目录结构直接反映设计水平。拿到一份下载的源码我第一反应是看它java目录下的包结构按 controller/service/mapper/entity 分层的是及格线把业务写在 controller 里的基本可以直接放弃。博物馆管理系统的包划分我习惯这样定com.museum.system ├── controller // 接收请求参数校验返回页面或 JSON ├── service // 业务逻辑预约流程、权限判断、事务控制 ├── mapper // MyBatis 数据访问接口 ├── entity // 数据库实体类和表结构一一对应 ├── dto // 前端传入的参数对象和 entity 分离 ├── config // 拦截器、WebMvc 配置 ├── interceptor // 登录拦截器、管理员权限拦截器 └── utils // MD5/BCrypt 加密、日期处理等工具类为什么要单独拆一个 dto 包因为前端传过来的表单数据不一定和数据库字段完全一致。比如预约表单提交的是展品 ID、日期、人数、联系人姓名和手机号而booking表里还需要记录创建时间、订单状态、用户 ID这些字段是后端自己补上的如果直接用 entity 接收前端参数要么 multipart 对准不上要么得在 controller 里手动 set 一堆字段代码很丑。DTO 单独放一层controller 里用 BeanUtils 复制属性字段对不上时一眼就能看出来。3. 六张核心表怎么建数据库设计是博物馆管理系统的地基3.1 表结构总览从用户到预约一条完整链路博物馆管理系统的数据库设计一般离不开这几张表用户表、展品分类表、展品表、预约表、公告表、留言表。如果还想做得完整一点可以加一张操作日志表。表与表之间的关系非常稳定一个分类下有多个展品一个用户有多条预约记录一条预约记录对应一个展品也可以放宽为一个订单多个展品但课程设计一般不做这么复杂一个展品有多条留言。建表执行要考虑两点第一字符集直接用utf8mb4不要用utf8因为utf8在 MySQL 里最多存 3 字节遇到生僻字或特殊符号会插入失败第二主键用自增int还是bigint我建议用bigint虽然现在数据量不可能爆但int到bigint的迁移成本远高于一开始就用bigint。下面给出一个可以直接复制的建表脚本。CREATE DATABASE IF NOT EXISTS museum_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE museum_db; CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录用户名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密, real_name VARCHAR(50) COMMENT 真实姓名, phone VARCHAR(20) COMMENT 手机号, role TINYINT NOT NULL DEFAULT 1 COMMENT 角色1-普通用户 2-管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-正常 0-禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_category ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 分类ID, name VARCHAR(50) NOT NULL COMMENT 分类名称, description VARCHAR(255) COMMENT 分类描述, sort_order INT DEFAULT 0 COMMENT 排序权重 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT展品分类表; CREATE TABLE t_exhibit ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 展品ID, category_id BIGINT NOT NULL COMMENT 所属分类ID, name VARCHAR(100) NOT NULL COMMENT 展品名称, description TEXT COMMENT 展品详细介绍, image_url VARCHAR(255) COMMENT 图片路径, era VARCHAR(50) COMMENT 年代, status TINYINT DEFAULT 1 COMMENT 状态1-展出中 0-已下架, view_count INT DEFAULT 0 COMMENT 浏览次数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_exhibit_category FOREIGN KEY (category_id) REFERENCES t_category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT展品表; CREATE TABLE t_booking ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 预约ID, user_id BIGINT NOT NULL COMMENT 用户ID, exhibit_id BIGINT NOT NULL COMMENT 展品ID, visit_date DATE NOT NULL COMMENT 参观日期, visitor_count INT NOT NULL DEFAULT 1 COMMENT 参观人数, contact_name VARCHAR(50) COMMENT 联系人, contact_phone VARCHAR(20) COMMENT 联系电话, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0-待确认 1-已确认 2-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_booking_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_booking_exhibit FOREIGN KEY (exhibit_id) REFERENCES t_exhibit(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT参观预约表;这个脚本里有三个细节值得说。一是password字段长度给了 100因为 BCrypt 加密后的字符串有 60 个字符用 50 就会截断或报错二是预约表和展品表之间用了外键这在课程设计里是加分项但也会带来一个问题——删除一个被预约过的展品会触发外键约束报错后面避坑章节会专门说怎么处理三是visit_date用DATE类型而不是DATETIME参观只要精确到天就够了用DATETIME反而会在页面日期选择器的格式转换上多出麻烦。3.2 视图与索引面试官和答辩老师最想看的设计点很多下载的源码只有建表语句没有任何索引和视图答辩时老师一问“这张表查询最多的字段是什么”就卡住。博物馆管理系统里查询频率最高的场景是展品列表按分类筛选、按名称模糊搜索、预约记录按用户查询。这些字段就应该建索引。模糊搜索如果用的是LIKE %关键词%普通索引用不上这是 MySQL 的 B 树特性决定的但user_id、category_id这种等值查询字段建索引后效果非常明显。ALTER TABLE t_exhibit ADD INDEX idx_category (category_id); ALTER TABLE t_booking ADD INDEX idx_user (user_id); ALTER TABLE t_booking ADD INDEX idx_exhibit (exhibit_id); ALTER TABLE t_booking ADD INDEX idx_visit_date (visit_date);这里不用建太多索引每个表 2 到 3 个足够。索引不是越多越好每建一个索引插入和更新时就要多维护一棵 B 树写操作会变慢。课程设计阶段数据量只有几百条索引在性能上体现不出来但设计思路上要能说清楚“为什么这里建索引、那里不建”。再来说视图。我一般会建一个展示预约详情的视图把t_booking关联t_user和t_exhibit这样在管理员后台查看预约列表时不用每次都写三表 JOINSQL 语句会简洁很多。视图在 MySQL 里本质是临时结果集的封装对简单查询来说性能没啥提升但代码可读性会好不少。管理员页面打开预约管理要同时看到用户名、展品名、参观日期、状态这个视图正好对应页面要的数据结构。CREATE VIEW v_booking_detail AS SELECT b.id, u.username, u.real_name, e.name AS exhibit_name, b.visit_date, b.visitor_count, b.contact_name, b.contact_phone, b.status FROM t_booking b JOIN t_user u ON b.user_id u.id JOIN t_exhibit e ON b.exhibit_id e.id;3.3 初始化数据怎么给管理员账号和展示用数据不能省下载的源码如果跑起来一片空白连管理员账号都没有那是相当尴尬的一件事。建表之后必须写一份初始化 SQL插入一个管理员账号、几个分类、一批展品数据。展品数据建议去博物馆官网找真实的馆藏信息比如青铜器、瓷器、书画、玉器各来几件图片可以先用占位图路径。这里的密码不能用明文后面会讲具体加密方式。INSERT INTO t_user (username, password, real_name, role, status) VALUES (admin, $2a$10$yourBCryptHashHere, 系统管理员, 2, 1); INSERT INTO t_user (username, password, real_name, role, status) VALUES (testuser, $2a$10$yourBCryptHashHere, 测试用户, 1, 1); INSERT INTO t_category (name, description) VALUES (青铜器, 中国古代青铜器), (陶瓷, 历代陶瓷精品), (书画, 古代书画作品); INSERT INTO t_exhibit (category_id, name, description, era, status) VALUES (1, 后母戊鼎, 商代后期铸造的大型青铜器形制巨大纹饰精美。, 商代, 1), (2, 青花缠枝莲纹瓶, 明代青花瓷代表作品釉色温润纹饰流畅。, 明代, 1), (3, 洛神赋图, 东晋顾恺之代表作描绘曹植与洛神相会场景。, 东晋, 1);初始化数据的编写思路是“让系统启动后立刻有东西可看”。页面打开是空的很难判断是代码 bug 还是数据没插先塞一批数据进去跑通之后再清掉换自己的展品内容比空库调试效率高得多。4. 核心代码落地登录鉴权、展品 CRUD 与预约流程4.1 登录注册与密码加密明文密码是简历上的污点博物馆管理系统的登录功能看起来简单但密码处理是很多初学者翻车的地方。直接明文存储密码的做法在课程设计里并不少见但答辩时面试官问一句“密码安全怎么保证”答不上来非常扣分。加密方案现在流行 BCryptSpring Security 里自带BCryptPasswordEncoder但整个项目引入 Spring Security 往往会把事情搞复杂——它会接管所有请求的鉴权逻辑跟项目里自己写的拦截器冲突。所以我一般只引入 Spring Security 的 crypto 模块单用它的加密工具。dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId version5.7.11/version /dependency对应的注册逻辑里密码加密和校验的写法如下Service public class UserService { private static final BCryptPasswordEncoder ENCODER new BCryptPasswordEncoder(); public boolean register(UserRegisterDTO dto) { // 检查用户名是否已存在 User existing userMapper.findByUsername(dto.getUsername()); if (existing ! null) { return false; } User user new User(); user.setUsername(dto.getUsername()); // BCrypt 加密而不是 MD5 user.setPassword(ENCODER.encode(dto.getPassword())); user.setRole(1); // 默认普通用户 user.setStatus(1); return userMapper.insert(user) 0; } public User login(String username, String rawPassword) { User user userMapper.findByUsername(username); if (user ! null ENCODER.matches(rawPassword, user.getPassword())) { return user; } return null; } }BCryptPasswordEncoder每次对同一个明文生成的密文都是不同的它内部带随机盐。这意味着你不能用“查数据库密码等于前端传过来的密码”这种 SQL 去校验只能调用matches方法把明文和密文做比对。这个和 MD5 的用法差别很大新手上手容易懵。另外注意前端传递密码时用 POST 请求不要用 GET 拼在 URL 里——浏览器历史记录、代理服务器日志都会留下敏感信息。登录成功后的会话管理我习惯把用户对象放进 Session同时存一个userId和role。拦截器里判断 Session 里有没有userId没有就重定向到登录页再根据role判断能不能访问/admin/**路径。这里有个常见的坑拦截器配置时把静态资源也拦了导致 CSS、JS、图片全部加载不出来后面避坑章会展开说。4.2 展品列表分页与条件搜索MyBatis 动态 SQL 是核心展品列表是博物馆管理系统的门面游客进来第一眼看到的就是它。分页方案我直接用 PageHelper 插件它用 MyBatis 拦截器自动在 SQL 后面拼LIMIT用法非常简洁。PageHelper.startPage(pageNum, pageSize)之后紧跟的查询语句会被自动分页返回的PageInfo里直接带了总记录数和总页数前端做分页按钮非常方便。Controller RequestMapping(/exhibit) public class ExhibitController { Autowired private ExhibitService exhibitService; GetMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 8) Integer pageSize, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword, Model model) { PageInfoExhibit page exhibitService.pageQuery(pageNum, pageSize, categoryId, keyword); model.addAttribute(page, page); model.addAttribute(categoryId, categoryId); model.addAttribute(keyword, keyword); return exhibit/list; } }service 层的关键在动态 SQL 拼接。categoryId和keyword都可能为空直接写两个单独的查询方法会让代码冗余用 MyBatis 的where和if标签解决最合适。select idpageQuery resultTypecom.museum.system.entity.Exhibit SELECT * FROM t_exhibit where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if AND status 1 /where ORDER BY create_time DESC /select这里的LIKE CONCAT(%, #{keyword}, %)写法要注意不要直接在 XML 里写LIKE %#{keyword}%#{}只会在值的位置做占位符处理放在字符串引号内部会直接报错或查不到数据。用${}做拼接虽然能查到但存在 SQL 注入风险。CONCAT函数绕开了这个矛盾是 MyBatis 里做模糊搜索的标准做法。分页参数的边界也要处理。pageNum如果传成负数或者 0PageHelper 会报参数异常所以 controller 里最好做一次防御处理——小于 1 就重置为 1。pageSize不能太大有些人搜索的时候传个 10000数据库直接卡死设置一个上限比如 20 比较合理。4.3 预约功能事务、唯一约束和状态机预约是博物馆管理系统里业务逻辑最重的一个功能。用户选择展品、填写参观日期和人数提交后生成一条待确认的预约记录管理员在后台审核后变为已确认。这里有几个容易出问题的点同一用户同一天不能重复预约同一展品提交预约时要校验展品是否存在、状态是否展出中预约成功和记录插入要放在同一个事务里。防止重复预约最稳妥的方式不是先查再插而是在数据库层面加唯一约束。先查再插存在并发窗口——两个请求同时查不到记录然后同时插入就出现了两条重复预约。唯一约束是数据库层面的强制规则直接解决并发问题。ALTER TABLE t_booking ADD UNIQUE KEY uk_user_exhibit_date (user_id, exhibit_id, visit_date);对应的插入逻辑里重复预约会抛出DuplicateKeyException代码里捕获这个异常并返回友好提示。Transactional public BookingResult createBooking(BookingDTO dto, Long userId) { // 校验展品状态 Exhibit exhibit exhibitMapper.findById(dto.getExhibitId()); if (exhibit null || exhibit.getStatus() ! 1) { return BookingResult.fail(展品不存在或已下架); } // 校验日期不能早于今天 if (dto.getVisitDate().isBefore(LocalDate.now())) { return BookingResult.fail(参观日期不能早于今天); } Booking booking new Booking(); booking.setUserId(userId); booking.setExhibitId(dto.getExhibitId()); booking.setVisitDate(dto.getVisitDate()); booking.setVisitorCount(dto.getVisitorCount()); booking.setContactName(dto.getContactName()); booking.setContactPhone(dto.getContactPhone()); booking.setStatus(0); try { bookingMapper.insert(booking); return BookingResult.success(预约成功等待管理员确认); } catch (DuplicateKeyException e) { return BookingResult.fail(您已预约过该展品请勿重复提交); } }这里用Transactional的意义在于如果插入预约记录之后后面还要做更新展品热度、扣减预约名额之类的操作任何一个环节失败整个预约都不应该生效。虽然现在代码里只有一个 insert看起来事务没用上但后续扩展逻辑时这个注解能防止你踩“部分成功”的坑。事务默认只在抛出 RuntimeException 时回滚如果你在方法里捕获了异常不往外抛事务不会生效——这是 Java 面试高频题也是实际开发中容易埋雷的地方。预约的状态流转也要说清楚。status字段 0 是待确认、1 是已确认、2 是已取消。管理员确认预约的接口要先判断当前状态是不是 0状态是 1 就不需要再确认直接提示“该预约已确认”避免重复操作。取消预约同理已取消的不能再取消。这就是最简单的状态机思想——只允许特定状态向特定状态迁移。5. 避坑指南Java 博物馆管理系统最常见的 5 个翻车点5.1 MySQL 时区报错启动直接挂掉现象项目启动时控制台报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized or represents more than one time zone应用根本起不来。原因MySQL 8.0 以上版本对时区校验更严格而数据库默认时区可能是系统时区和 JDBC 驱动的时区配置对不上。这毛病在中文环境下特别容易触发因为系统时区名在非 UTF-8 环境下可能被读成乱码。解决在数据库连接 URL 后面显式指定时区参数不要依赖数据库默认值。JDBC 连接串改成jdbc:mysql://localhost:3306/museum_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue。其中useSSLfalse是关掉 SSL 校验本地开发不需要characterEncodingutf8保证中文不会乱码allowPublicKeyRetrievaltrue解决 MySQL 8 连接时公钥检索报错的问题。这三个参数是本地开发接到 MySQL 8 的标配一条都别省。5.2 数据库表字段无法映射到 Java 实体类现象SQL 查询返回 null前端页面数据大面积空白但数据库里明明有数据。慢慢排查才发现表里create_time这种下划线命名的字段在 Java 实体类里写成createTimeMyBatis 自动映射对不上所有带下划线的字段全部是 null。原因MyBatis 默认开启的是严格驼峰映射需要配置。很多下载的源码里application.yml没有加map-underscore-to-camel-case: true这个配置项。解决在application.yml的 MyBatis 配置段加上开关mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mapper/*.xml加了之后user_id就会自动映射到userId。这里有个反直觉的点只改实体类的字段名为下划线风格也能解决问题但 Java 变量名用下划线不符合编码规范会在代码审查时被扣分正确做法是开全局驼峰映射。改完之后如果还有字段映射不上检查是不是有别名或嵌套对象——XML 里resultMap中配置了column和property的显式映射优先级高于全局驼峰开关。5.3 前端传日期字符串后端接收直接 400现象用户在前端表单选择参观日期提交预约时报 HTTP 400 错误后台日志显示Failed to convert value of type java.lang.String to required type java.time.LocalDate。原因Spring MVC 默认的日期转换器不认识2025-06-18这种字符串需要手动配置日期格式转换。Java 8 的LocalDate类型和老的java.util.Date处理方式完全不同。解决在项目里加一个全局的日期格式配置类Configuration public class WebMvcConfig implements WebMvcConfigurer { Bean public ConverterString, LocalDate localDateConverter() { return new ConverterString, LocalDate() { Override public LocalDate convert(String source) { return LocalDate.parse(source, DateTimeFormatter.ofPattern(yyyy-MM-dd)); } }; } }这是第一种解法。更简单的做法是在实体类的日期字段上直接加DateTimeFormat(pattern yyyy-MM-dd)注解Spring 会针对这个字段做格式转换。两者选一个就行两条一起加也不会冲突但全局转换器对项目中所有日期字段生效表单里有多种日期格式时容易顾此失彼字段注解更精细能按字段指定格式。我建议字段注解优先因为博物馆管理系统里的日期字段不多逐个标注反而直观。5.4 删除展品时报外键约束错误现象管理员在后台删除一个展品前端弹窗提示删除失败后台日志里有Cannot delete or update a parent row: a foreign key constraint fails。原因t_booking表通过外键引用了t_exhibit如果该展品已经有预约记录直接删除主表记录会被外键拦截。这是数据库在保护数据完整性不是代码 bug。解决按照业务规则展品应该用“逻辑删除”而非“物理删除”。所谓逻辑删除就是给展品表加一个deleted字段删除操作只把deleted改成 1而不是真的执行DELETE语句。这样历史预约记录还能关联到展品名管理员后台也只是不展示已删除的展品而已。Update(UPDATE t_exhibit SET status 0, deleted 1 WHERE id #{id}) int softDelete(Long id);逻辑删除带来的一个连带点是所有查询展品的 SQL 都要加上AND deleted 0条件。如果漏了一个查询接口被“删除”的展品还会出现在某些页面上形成数据不一致。如果你的项目图省事想保留物理删除可以在删除前先删除该展品的所有预约记录再用事务包住两条 SQL但这会丢失预约历史数据对博物馆这种需要留痕的业务场景不是很合适。5.5 拦截器把静态资源拦了页面样式全军覆没现象登录功能正常但登录成功后跳转到首页CSS 样式完全丢失页面变成纯文字排版。原因拦截器配置成拦截所有路径/**当浏览器请求/css/style.css、/js/common.js这些静态资源时因为 Session 中没有用户信息直接被重定向到登录页。浏览器又不会跟随重定向去加载 CSS样式自然就没了。解决在拦截器注册时放行静态资源路径同时放行登录和注册接口Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns( /, /index, /login, /doLogin, /register, /doRegister, /exhibit/list, /exhibit/detail/**, /css/**, /js/**, /images/**, /fonts/**, /error ); registry.addInterceptor(adminInterceptor) .addPathPatterns(/admin/**) .excludePathPatterns(/css/**, /js/**, /images/**, /fonts/**); }这里我把游客接口也放行了浏览展品列表和详情应该允许未登录用户访问只有提交预约时才强制登录。这是一个产品层面的决策——如果所有页面都要登录才能看那这个博物馆管理系统的“展览”功能就没有意义了。设计拦截器之前先把哪些路径是公开的、哪些是登录后可访问的、哪些是管理员专属的列一张清单写代码时照着清单配比事后调试省心。6. 从“能跑”到“能答辩”日志、验证和生产习惯系统跑到这一步功能链路基本通了但距离“拿得出手”还有一段路。我习惯在交付前做三件事一是把操作日志补上二是用真实数据走一遍完整流程三是统一接口返回格式。操作日志是很多课程设计的盲区。简单实现方式是建一张t_operation_log表字段包括用户 ID、操作类型、操作内容、操作时间、IP 地址然后写一个拦截器或 AOP 切面在管理员执行新增、修改、删除操作时自动记录。用 AOP 做一个Log注解配合切面比在每个 controller 方法里手动写日志代码优雅得多。答辩时老师问“你怎么审计管理员操作”这是最直接的答案。Aspect Component public class OperationLogAspect { Around(annotation(log)) public Object record(ProceedingJoinPoint point, Log log) throws Throwable { long start System.currentTimeMillis(); Object result point.proceed(); long duration System.currentTimeMillis() - start; // 此处异步写入日志表 operationLogService.save(log.module(), log.action(), duration); return result; } }验证方面我建议准备一份测试清单按用户注册登录、浏览展品、搜索筛选、提交预约、管理员审核、公告发布、数据统计这几大块逐项过一遍。每项都要写清楚输入的数据、期望的结果、实际的输出。特别是预约流程要测重复预约拦截、日期早于今天拦截、展品下架后预约拦截这三个边界场景这些是最容易被老师考到的点。性能不需要测但 SQL 语句的执行计划可以看一眼——用EXPLAIN关键词确认大表查询有没有走索引这个习惯对后续工作面试很有用。还有一个值得养成的习惯统一的返回结果封装。当前后端分离是行业主流的时候哪怕你的项目是 Thymeleaf 服务端渲染把接口返回封装成统一结构也不是白费功夫。定义一个ResultT类包含code、message、data三个字段前端 Ajax 调用时只需判断code是否等于 200不用每个接口各自为政地返回不同的数据结构。这个习惯会直接迁移到你的第一个企业级项目里。最后说一个我自己的教训源码里一定要留注释但不要留无意义的注释。我第一次交课程设计时代码干干净净一行注释都没有答辩被问得哑口无言。后来学乖了只在业务关键节点写注释比如“这里捕获 DuplicateKeyException 是为了处理并发重复预约”——这种注释是在解释为什么这么做而不是复述代码在做什么。一份设计文档加一套带注释的源码比代码本身更能体现你的工程素养。希望这片踩坑经验能帮你在博物馆管理系统上少走几段弯路。项目做完之后别急着删把它推到自己的代码仓库里后续面试聊 Java 基础、MySQL 设计、事务和索引时这些都是能落地的实战素材。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑