多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于Java的博物馆管理系统设计与实现:Spring Boot + MyBatis Plus实战解析

基于Java的博物馆管理系统设计与实现:Spring Boot + MyBatis Plus实战解析 简介基于Java语言的博物馆管理系统设计源码面向Java初学者、毕业设计者及中小型博物馆信息化建设人员覆盖藏品管理、展览活动、访问者管理等核心业务场景。整个项目采用JavaHTMLCSSJavaScript技术栈后端144个Java文件处理业务逻辑与数据库交互前端126个HTML页面搭配CSS/JS实现操作界面另有17个XML配置、1个SQL脚本及Javadoc等辅助文件压缩包仅1.32MB结构清晰。已有140人学习阅读适合作为课程设计或工程实训的完整参照。通过源码可学习分层架构、接口实现及数据库设计掌握排班调度、员工管理、藏品管理等模块的调用关系附带的Javadoc配置还能生成API文档便于二次开发与维护适合在此基础上扩展出更完整的博物馆信息平台。1. 博物馆管理系统不只是 CRUD这个选题背后到底要解决什么如果你带过课程设计或者接过多年的外包维护会发现“基于Java语言的博物馆管理系统设计源码”这类项目真正难的地方从来不是登录注册而是藏品、预约、票务、展厅这几类数据在真实业务里互相咬合时的边界问题。预约超卖、藏品分类混乱、展厅容量和导览顺序不一致这些才是系统上线后被用户骂得最多的点。这个标题适合两类人一类是拿它当毕业设计或课程设计想要一套能讲清楚设计思路的源码另一类是刚进公司被分到文旅或场馆类项目想抄一份能落地的业务骨架。后面所有内容都围绕一个目标让你拿到源码后看得懂、跑得起来、敢往生产环境推。2. 技术选型与工程骨架Spring Boot MyBatis Plus 的组合为什么是默认答案2.1 为什么是 Java 而不是低代码平台博物馆管理系统的业务量通常介于“给领导演示”和“真实运营”之间。低代码平台搭个后台演示很快但一旦牵扯到票务库存扣减、藏品批次出入库、与微信小程序对接平台的扩展能力就成了黑匣子出了问题你连日志都拿不到。Java 生态胜在两点第一Spring Boot 把大多数开箱即用的组件都打包好了环境配置的复杂度被大幅压低第二这套技术栈在国内的求职市场、课程设计、外包交付里都有大量现成案例遇到问题搜得到答案不会像冷门框架一样卡住就翻车。所以常见做法是Spring Boot 做应用框架MyBatis Plus 做持久层MySQL 存业务数据Redis 做缓存和临时计数。这个组合在博物馆这类中低并发、强事务、多表单的业务里属于最稳的一条路。2.2 搭建工程Maven 依赖与项目结构新工程我个人习惯从 Maven 骨架开始不用 Spring Initializr 生成的默认结构也可以但依赖版本最好统一管理。下面是一份能直接跑的pom.xml核心依赖注释里标了各自的作用dependencies !-- Web 层提供 REST 接口能力 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 参数校验Valid 注解要靠它生效 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- MyBatis Plus基于 MyBatis 的增强封装 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok减少 getter/setter 样板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies依赖里最值得注意的一条是mybatis-plus-boot-starter的版本选择。3.5.x 之后的内置方法和分页插件 API 都比较稳定网上能搜到的大多数案例也基于这个版本踩坑时更容易对照。启动类长这样package com.museum.admin; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication MapperScan(com.museum.admin.mapper) public class MuseumAdminApplication { public static void main(String[] args) { // 显式指定 profile避免不同环境配置串掉 SpringApplication.run(MuseumAdminApplication.class, args); } }MapperScan是 MyBatis 扫描 Mapper 接口的关键入口。如果漏掉它项目启动时会报找不到 mapper bean 的错。这里的包路径要和你实际放置 Mapper 接口的包完全一致否则即使代码编译通过启动同样会在容器初始化阶段失败。2.3 数据源与 MyBatis Plus 配置环境配置里最容易翻车的三个点写application.yml时数据源这块有三个地方我每次都要核对任何一个不对启动阶段就会卡住。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/museum ?useUnicodetrue characterEncodingutf8 serverTimezoneAsia/Shanghai username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: # 开发环境打印 SQL生产环境务必关掉 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 全局主键策略数据库自增 id-type: auto第一个坑是 MySQL 8 的驱动类必须写成com.mysql.cj.jdbc.Driver如果你是 5.x 老驱动又连的是 MySQL 8启动时直接报 ClassNotFoundException。第二个坑是serverTimezone不写默认按服务器时区走本地测试可能没问题部署到云上就容易出现时间字段整体偏移的问题。第三个坑是log-impl配置的是StdOutImpl它会把每一条 SQL 原样打印到控制台这在排查“为什么查询结果不对”时非常好用但生产环境不关掉日志文件会膨胀得极快。工程骨架搭到这里项目已经可以启动。但此时数据库还是空的下一章要解决的核心问题是表结构从哪来。3. 数据库设计与建表 SQL让实体类当唯一数据字典3.1 博物馆业务有哪些表关系怎么画博物馆管理系统的业务边界通常落在六个核心实体上藏品、藏品分类、展厅、预约单、票务库存、员工账号。藏品表记录名称、编号、分类、朝代、材质、入馆日期、存放展厅编号。分类表是树形的因为“陶瓷”下面还有“青花瓷”“粉彩”这种二级分类。展厅表记录展厅名称、位置、容纳人数、开放状态。预约表和票务库存表是一对多的关系——某个展厅某天有多少可预约名额这是运行时的核心数据。员工表则区分管理员、票务员、库管员三种角色。表之间最微妙的关系是藏品和展厅。一个藏品可能被临时调到别的展厅办特展所以不能简单在藏品表里存一个展厅外键常见做法是在中间加一张collection_exhibition关联表记录“某个藏品在哪个展厅、从哪天到哪天”。设计源码阶段哪怕没用到这个中间表也应该把这个关系画在数据库设计文档里面试或答辩时这属于非常加分的点。3.2 用反射读取 MyBatis Plus 注解生成 CREATE TABLE很多课程设计和外包项目的通病是数据库设计文档和实体类不同步。改实体类不改表改表不改实体类最后查数据全靠猜。为了治这个问题我一般会写一个小的 DDL 生成工具让实体类成为唯一的数据字典。它的原理很简单用反射扫描实体类字段上的TableName、TableId、TableField注解拼出CREATE TABLE语句。package com.museum.admin.common.ddl; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.lang.reflect.Field; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; public class DdlGenerator { /** * 根据实体类生成建表 SQL。 * 只覆盖常用 Java 类型生产环境请把生成结果交给 Flyway 或 DBA 复核。 */ public static String generateDdl(Class? entityClass) { TableName tableName entityClass.getAnnotation(TableName.class); String table tableName null ? camelToUnderscore(entityClass.getSimpleName()) : tableName.value(); StringBuilder sb new StringBuilder(); sb.append(CREATE TABLE IF NOT EXISTS ).append(table).append( (\n); ListString columnLines new ArrayList(); String primaryKeyColumn null; for (Field field : entityClass.getDeclaredFields()) { TableId tableId field.getAnnotation(TableId.class); TableField tableField field.getAnnotation(TableField.class); // TableField.existfalse 表示该字段在数据库中没有对应列 if (tableField ! null !tableField.exist()) { continue; } String columnName tableField ! null !tableField.value().isEmpty() ? tableField.value() : camelToUnderscore(field.getName()); String columnType mapType(field.getType()); StringBuilder line new StringBuilder( columnName columnType); if (tableId ! null) { if (tableId.type() IdType.AUTO) { line.append( AUTO_INCREMENT); } primaryKeyColumn columnName; } else if (tableField ! null !tableField.value().isEmpty()) { // 显式指定了列名的字段默认 NOT NULL 更安全 line.append( NOT NULL); } else { line.append( DEFAULT NULL); } columnLines.add(line.toString()); } if (primaryKeyColumn ! null) { columnLines.add( PRIMARY KEY ( primaryKeyColumn )); } sb.append(String.join(,\n, columnLines)); sb.append(\n) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT).append(table).append(;\n); return sb.toString(); } private static String mapType(Class? javaType) { if (javaType String.class) return VARCHAR(255); if (javaType Integer.class || javaType int.class) return INT; if (javaType Long.class || javaType long.class) return BIGINT; if (javaType BigDecimal.class) return DECIMAL(18,2); if (javaType LocalDate.class) return DATE; if (javaType LocalDateTime.class) return DATETIME; if (javaType Boolean.class || javaType boolean.class) return TINYINT(1); return VARCHAR(255); } private static String camelToUnderscore(String camelName) { return camelName.replaceAll(([A-Z]), _$1).toLowerCase(); } }这段代码的核心逻辑在generateDdl方法里。先读类上的TableName拿到真实表名再遍历字段有TableId并且主键策略是AUTO的字段加上AUTO_INCREMENT有TableField且显式指定了列名的按指定列名输出TableField(existfalse)的字段直接跳过因为它只是内存中的临时属性不需要落库。mapType方法的作用是把 Java 数据类型映射到 MySQL 类型。这里有一个很值得注意的点LocalDate映射成DATELocalDateTime映射成DATETIME。有些老项目习惯用java.util.Date如果新项目还在用这个类型建议尽早改成java.time包下的类型不光是为了代码简洁更是因为 MyBatis Plus 对java.time的支持默认就是完整的类型处理器不需要额外配置。在入口处写一个简单调用把生成的 SQL 打印出来Component public class DdlPrinter implements ApplicationRunner { Override public void run(ApplicationArguments args) { // 开发启动时打印建表语句方便人工核对 System.out.println(DdlGenerator.generateDdl(MuseumCollection.class)); } }执行后会在控制台输出类似下面的 SQLCREATE TABLE IF NOT EXISTS museum_collection ( id BIGINT AUTO_INCREMENT, name VARCHAR(255) DEFAULT NULL, category VARCHAR(255) DEFAULT NULL, era VARCHAR(255) DEFAULT NULL, location_no VARCHAR(255) DEFAULT NULL, create_time DATETIME DEFAULT NULL, update_time DATETIME DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTmuseum_collection;这个方案的另一个好处是数据库迁移文件不再依赖手写文档实体类改动后重新生成一版 SQL丢给 Flyway 或 Liquibase 做版本管理比任何数据库文档都可靠。3.3 字段命名映射与类型映射要盯住哪些点命名映射这块MyBatis Plus 默认开启驼峰转下划线所以 Java 里的locationNo默认对应数据库里的location_no。但如果你在某个实体上用了TableField(location_number)而数据库里真实列名是location_noMyBatis Plus 不会帮你自动纠正查询结果那一列全是 null而且不报错——这是最气人的坑因为问题出在数据层之下根本不抛异常。类型映射则要注意Boolean和TINYINT的对应。MySQL 里TINYINT(1)存 0 和 1Java 里用Boolean接收MyBatis Plus 默认是支持的。但如果你在数据库里用了CHAR(1)存 Y/NJava 端就必须用String接收再手动转换否则会直接报类型转换异常。设计源码阶段就该把这类对应关系确定下来不要在写业务代码时再临时改表结构。4. 核心业务模块源码实现藏品、预约、展厅三条链路4.1 藏品管理 CRUD从 Controller 到 Mapper 的完整闭环藏品管理是博物馆系统的门面模块它比其他普通 CRUD 多一个阶梯藏品分类是树形的、藏品数量多需要分页、藏品名称支持模糊搜索。Controller 层我习惯写得很薄只做参数接收和结果包装业务判断全部下沉到 Service。RestController RequestMapping(/api/collection) RequiredArgsConstructor public class CollectionController { private final CollectionService collectionService; /** * 分页查询藏品列表。 * query 里可以带 name、categoryId、era 三个条件 */ GetMapping(/page) public ResultPageMuseumCollection page(CollectionQuery query) { return Result.ok(collectionService.pageQuery(query)); } PostMapping public ResultLong create(RequestBody Valid MuseumCollection collection) { return Result.ok(collectionService.create(collection)); } PutMapping(/{id}) public ResultVoid update(PathVariable Long id, RequestBody Valid MuseumCollection collection) { collection.setId(id); collectionService.update(collection); return Result.ok(); } }Service 里最值得展开的是分页条件查询。MyBatis Plus 的Page对象直接作为查询参数传入条件构造器LambdaQueryWrapper负责把 Java 方法引用翻译成数据库列名从而避免了硬编码字符串带来的字段重构隐患。Service RequiredArgsConstructor public class CollectionServiceImpl implements CollectionService { private final MuseumCollectionMapper collectionMapper; Override public PageMuseumCollection pageQuery(CollectionQuery query) { PageMuseumCollection page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperMuseumCollection wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getName()), MuseumCollection::getName, query.getName()) .eq(query.getCategoryId() ! null, MuseumCollection::getCategoryId, query.getCategoryId()) .eq(StringUtils.hasText(query.getEra()), MuseumCollection::getEra, query.getEra()) .orderByDesc(MuseumCollection::getCreateTime); return collectionMapper.selectPage(page, wrapper); } }wrapper.like的第一个参数是布尔条件第二个参数是列名第三个参数是值。条件为 false 时整个条件不参与 SQL 拼装。这是处理前端查询参数为空时最优雅的写法避免你写一堆 if-else 去重构 wrapper。orderByDesc(MuseumCollection::getCreateTime)这里用到了 Java 方法引用编译期就能校验列名存在与否重构实体类字段时不会像字符串create_time一样默默失效。4.2 预约购票与库存扣减一条 UPDATE 解决超卖预约购票是博物馆系统里最容易出并发问题的模块。场景是这样的国庆期间的“青铜器特展”展厅每天只有 2000 个名额用户通过小程序同时抢票如果代码先查库存、判断大于 0、再执行扣减三段逻辑之间一定会出现竞态。常见的错误写法是 check-then-act先select stockif 大于 0然后update stock-1。高并发下多个请求同时读到库存还剩 1然后各自执行扣减最终库存变成负数。这不是 Java 代码能解决的必须在数据库层面加约束。正确的做法是把“判断库存是否足够”和“扣减库存”合并成一条 SQLMapper public interface DailyTicketMapper extends BaseMapperDailyTicket { /** * 并发安全地扣减库存。 * 返回受影响的行数为 0 表示库存不足或记录不存在。 */ Update(UPDATE daily_ticket SET stock stock - 1, update_time NOW() WHERE exhibition_id #{exhibitionId} AND visit_date #{visitDate} AND stock 0) int decreaseStock(Param(exhibitionId) Long exhibitionId, Param(visitDate) LocalDate visitDate); }Service 层拿到返回的行数后如果为 0直接抛出业务异常如果为 1说明扣减成功继续创建预约单Transactional(rollbackFor Exception.class) public Reservation createReservation(CreateReservationRequest req) { // 第一步并发安全扣减库存 int rows dailyTicketMapper.decreaseStock(req.getExhibitionId(), req.getVisitDate()); if (rows 0) { throw new BusinessException(该日期已约满请选择其他时间); } // 第二步生成预约单 Reservation reservation new Reservation(); reservation.setVisitorName(req.getVisitorName()); reservation.setVisitorPhone(req.getVisitorPhone()); reservation.setExhibitionId(req.getExhibitionId()); reservation.setVisitDate(req.getVisitDate()); reservation.setStatus(1); // 1预约成功 reservationMapper.insert(reservation); return reservation; }这一步是整个预约模块的关键。Transactional保证异常时库存扣减一并回滚——比如预约单插入失败库存会自动恢复不会出现扣了票但订单没生成的问题。Redis 这种前置计数方案在这个场景只能做流量预判不能作为扣减依据否则一旦 Redis 和 MySQL 数据不一致预约数据就是错的。4.3 展厅与导览推荐排序接口怎么给业务加分展厅模块在很多设计源码里被做成纯静态字典但真实运营时需要在列表页按“当前是否有特展”“展厅容量从大到小”“是否需要排队”做动态排序。这里用到一个很实用的 Java 技巧在内存中对查询结果二次排序。public ListExhibitionHall recommendHalls(Long userId) { // 查询所有开放中的展厅 ListExhibitionHall halls exhibitionHallMapper.selectList( new LambdaQueryWrapperExhibitionHall() .eq(ExhibitionHall::getOpenStatus, 1)); // 内存排序先按容量降序再按特展标记降序 ListExhibitionHall sorted halls.stream() .sorted(Comparator.comparing(ExhibitionHall::getCapacity).reversed() .thenComparing(ExhibitionHall::getHasSpecialExhibition, Comparator.reverseOrder())) .toList(); return sorted; }这里没有使用数据库层的ORDER BY因为排序规则里混合了“是否有特展”这种布尔标记和“容量”这种数值字段。用 Comparator 链式排序代码可读性比一长串 SQL 更好而且测试时可以直接 mock 一批数据验证排序逻辑不用依赖数据库环境。有一点要注意这种内存排序只适合数据量在几千条以内的场景。如果展厅数量将来超过几万条必须把排序逻辑下沉到 SQL 里否则 JVM 的 GC 压力会明显上升。5. 避坑与排查博物馆管理系统上线前要处理的五类问题5.1 预约超卖check-then-act 带来的脏数据现象压测 200 个并发预约同一个展厅结果预约成功的单子超过库存上限。原因Service 里先查库存再判断再更新三步操作不是一个原子动作。两个请求同时读到剩余 1 个名额各自都认为可以预约。解决按我 4.2 节的方式把判断和扣减合并成一条带stock 0条件的 UPDATE。靠数据库行锁保证同一时间只有一个请求能成功扣减。这条 SQL 在 MySQL 的 InnoDB 引擎下命中唯一索引时会锁行不锁表性能足够支撑博物馆这种量级的预约场景。5.2 字段对不上驼峰与下划线之争别靠猜现象查询出来的实体对象里部分字段为 null但数据库里明明有值。原因Java 字段exhibitionId默认映射数据库列exhibition_id但某次手动改表时列名被写成了exhibitionID。MyBatis Plus 的驼峰映射只管下划线风格碰到全大写的缩写词就直接失效而且不报错。解决第一反应不是去猜而是打开log-impl配置看打印出来的 SQL 里查询了哪些列再对比实体类上的TableField注解。碰到历史遗留的不规范列名直接在实体类上显式标注TableField(exhibitionID)。5.3 批量插入慢循环里挨个 save 是性能杀手现象导入 500 件藏品数据程序跑了一分钟还没完成。原因Service 里写了for (item : list) { mapper.insert(item); }。每条 insert 单独走一次网络往返到 MySQL500 条就是 500 次往返事务日志也要刷 500 次。解决用 MyBatis Plus 提供的saveBatch或IService.saveBatch(collection)。底层会把多条 insert 攒成一条批量插入语句500 条数据的导入时间能从几十秒降到一两秒。要注意saveBatch默认分批大小是 1000如果你的单条记录字段很多可以适当调小到 500避免超出 MySQLmax_allowed_packet限制。5.4 分页查询变全表扫描MyBatis Plus 分页插件别看漏现象列表页点第二页时接口响应时间反而比第一页还要长。原因MyBatis Plus 的分页功能不是默认开启的。如果没有配置PaginationInnerInterceptor传入Page对象后SQL 仍然会查出全表数据再在内存里分页等于每查一页都拖全表。解决配置类里必须显式添加分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // DbType.MYSQL 指定数据库方言 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置漏掉分页功能就是假的。验证方法很简单开启 SQL 日志看查询语句里有没有LIMIT字样没有就是没生效。5.5 时间字段差了八小时连接串时区不是小事现象页面展示的预约时间是14:00数据库里存的是14:00但用户在手机端看到的是06:00。原因三处时区不一致。MySQL 连接串没写serverTimezone默认按服务器的UTC读Jackson 序列化又按Asia/Shanghai输出两个时区一换算整整差了八小时。解决数据源 URL 加上serverTimezoneAsia/ShanghaiJackson 配置time-zone: Asia/Shanghai。如果用了 Redis 缓存还要确认 Redis 里存的字符串时间本身带不带时区标记。这是 Java Web 系统里最典型的“改了一个地方另外两个地方一起翻车”的问题。6. 进阶与验证权限、审计日志和启动冒烟检查6.1 用 Spring Security 给管理端加接口权限博物馆管理后台至少要有三种角色管理员、票务员、库管员。管理员能看全部数据票务员只能操作预约审核库管员只能维护藏品入库出库。用 Spring Security 的注解方式最省事——在接口方法上加PreAuthorize业务代码完全不需要感知权限逻辑。RestController RequestMapping(/api/collection) public class CollectionController { PreAuthorize(hasRole(ADMIN) or hasRole(KEEPER)) PostMapping public ResultLong create(RequestBody Valid MuseumCollection collection) { // 只有管理员和库管员能新增藏品 return Result.ok(collectionService.create(collection)); } PreAuthorize(hasRole(ADMIN)) DeleteMapping(/{id}) public ResultVoid delete(PathVariable Long id) { // 删除藏品是高危操作仅限管理员 return Result.ok(); } }配合登录用户自身的数据过滤规则比如票务员只能看本展厅的预约单需要在查询时把当前用户的部门 ID 拼进查询条件。权限到这里才算闭环否则前端隐藏按钮、后端不校验等于没设防。6.2 用 AOP 记录操作日志改动不动业务代码藏品信息被误删、预约状态被改错这些操作如果没有日志排查时就只剩一句“我不知道谁动的”。用 AOP 切在 Controller 上可以无侵入地记录每个写操作的参数、操作人、结果。Aspect Component public class OperationLogAspect { AfterReturning(annotation(operationLog)) public void afterReturning(JoinPoint joinPoint, OperationLog operationLog) { // 从 Spring Security 上下文拿当前登录用户名 String username SecurityContextHolder.getContext().getAuthentication().getName(); // 记录操作人、动作、入参 OperationLogRecord record new OperationLogRecord(); record.setUsername(username); record.setAction(operationLog.value()); record.setParams(JSONUtil.toJsonStr(joinPoint.getArgs())); // 异步写入日志表不影响主流程 operationLogService.saveAsync(record); } }这里刻意用异步方式写日志因为日志写入不能拖慢业务接口。一旦同步写入日志表所在的库出现慢查询藏品管理接口也会跟着变慢。6.3 启动后的最小冒烟验证每次部署完我会上线前执行一次最小的冒烟检查启动应用后确认数据库连接正常、MyBatis Plus 配置生效、预约接口能返回预期数据。具体做法是写一个ApplicationRunner启动时执行一条最简单的查询如果失败就让进程直接退出避免“以为部署成功了实际数据库没连上”的尴尬。Component RequiredArgsConstructor public class StartupSmokeCheck implements ApplicationRunner { private final MuseumCollectionMapper collectionMapper; Override public void run(ApplicationArguments args) { // 查一张表确认数据库连接可用 Long count collectionMapper.selectCount(new LambdaQueryWrapper()); if (count null) { throw new IllegalStateException(数据库连接异常请检查数据源配置); } log.info(启动校验通过当前藏品记录数{}, count); } }这套检查逻辑简单但很有效。把失败启动上抛可以让 CI/CD 流水线在集成阶段就抓到问题而不是把带病的包送上线。我用这招拦住过很多次“数据库密码被运维重置了但没人通知”的线上事故。操作记录关于长期维护这件事我想分享一个个人习惯每年替别人接手的 Java 博物馆管理系统十有八九是挂在字段命名不统一和并发扣减两处。前者靠 DDL 生成工具从源头规范后者靠一条带条件的 UPDATE 语句兜底。先把这两件事做对后续加缓存、拆微服务、上消息队列都只是锦上添花系统本身不会轻易在核心业务上出问题。希望这篇文章里的步骤和踩坑记录能帮你在自己的博物馆管理系统上少走几个弯路把时间花到真正有价值的业务功能上去。本文还有配套的精品资源点击获取
返回列表