尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Spring Boot的文物管理系统:表结构设计到定时任务实战

基于Spring Boot的文物管理系统:表结构设计到定时任务实战 简介这是一份基于SpringBoot与Vue前后端分离架构的文物管理系统项目源码面向JavaWeb学习者、毕业设计及课程设计人群。资源内含可运行源码、SQL脚本和LW文档配套Maven3.3.9、JDK1.8、MySQL5.7等环境说明并附有安装、运行、构建的bat脚本便于快速启动调试。压缩包共666个文件主要包括147个Java后端类、112个Vue前端组件、159个SVG图标资源以及SQL、XML、配置文件等整体大小约31.94MB目录按前后端分离结构组织层次清晰。目前已有1813人学习浏览项目覆盖文物信息的增删改查、分类管理等典型业务场景既能作为毕设题目参考也适合用于课程作业二次开发对理解SpringBoot接口编写、Vue页面交互及前后端联调有较好的借鉴价值。1. 文物管理系统不只是 CRUD先看业务闭环再谈 Spring Boot拿到「基于 Spring Boot 的文物管理系统」这类标题时大多数人第一反应是做一个文物信息的增删改查。实际上真正难的不是 Spring Boot 本身而是文物这个对象的管理规则一件文物从入馆建档、日常在库到外借展出、修复养护中间每一次位置和状态的变更都必须留痕且不能出现「出库了但没记录谁经手」这种数据断层。这正是「藏品管理」和「普通商品管理」的本质区别。本文按一个单体 Spring Boot 项目的常见落地路径来拆解先设计领域表结构再实现认证、检索和审批状态流转然后处理图片上传与联调排错最后补一个定时提醒的进阶技巧。适合正在做毕设、想接小项目或者第一次用 Spring Boot 搭管理系统的开发者。2. 表结构先行文物主表、出入库流水与审批表的设计2.1 设计逻辑文物管理的四个核心问题任何文物管理系统最终都要回答四个问题这件文物是什么、现在在哪、经历过哪些流转、谁批准的流转。围绕这四个问题表设计就清晰了不需要刻意追求复杂的业务建模。「是什么」由文物主表承担包含名称、编号、年代、类别、材质、级别、馆藏位置等静态属性「现在在哪」和「经历过哪些流转」由主表的状态字段加流水表共同承担主表只存当前状态流水表存每一次变更的历史「谁批准的」由审批表承担外借、出库这类动作先走审批审批通过后流水才落库。这里有一个容易犯的错把审批信息和流水信息混在一张表里。我一般建议拆开因为审批关注的是「申请理由、审批意见、审批人」流水关注的是「何时出、何时回、经手人」。两者关联字段用biz_no或relic_id都可以但生命周期不同拆开以后统计和回溯都更顺手。2.2 文物主表与出入库流水表的 DDL先建主表。注意两个点一是业务编号relic_no必须是逻辑唯一比如馆藏编号能唯一标识一件文物不能依赖自增主键做业务标识二是状态字段用 TINYINT 存数字枚举不要直接用字符串避免写错大小写和中文。CREATE TABLE cultural_relic ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, relic_no VARCHAR(32) NOT NULL COMMENT 文物编号业务唯一, name VARCHAR(128) NOT NULL COMMENT 文物名称, category VARCHAR(32) DEFAULT COMMENT 类别陶瓷/书画/青铜/杂项, dynasty VARCHAR(32) DEFAULT COMMENT 年代/朝代, level VARCHAR(16) DEFAULT 一般 COMMENT 级别一级/二级/三级/一般, material VARCHAR(64) DEFAULT COMMENT 材质, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0在库 1出库 2外借 3修复中 4盘点中 5注销, location VARCHAR(64) DEFAULT COMMENT 馆藏位置, image_url VARCHAR(255) DEFAULT COMMENT 图片相对路径, description TEXT COMMENT 描述, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0未删 1已删, PRIMARY KEY (id), UNIQUE KEY uk_relic_no (relic_no), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文物主表;UNIQUE KEY uk_relic_no是硬约束用来兜底业务编号重复这个我在项目里必加数据层面防止并发下插入相同编号。deleted配合 MyBatis-Plus 的TableLogic做逻辑删除文物记录不能物理删除这是行业习惯删除一条文物档案等同于销毁历史绝对不能允许。再来是流水表。流水表要冗余relic_no这样查询某件文物的全量历史时不需要 JOIN 主表单表查询直接出结果性能足够也简单。CREATE TABLE relic_stock_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, relic_id BIGINT NOT NULL COMMENT 文物主键, relic_no VARCHAR(32) NOT NULL COMMENT 文物编号, type TINYINT NOT NULL COMMENT 1出库 2归还入库 3外借出 4外借归还 5修复出 6修复回, apply_reason VARCHAR(255) DEFAULT COMMENT 事由, operator VARCHAR(32) NOT NULL COMMENT 经办人, expected_return_time DATETIME DEFAULT NULL COMMENT 预计归还时间外借时必填, actual_return_time DATETIME DEFAULT NULL COMMENT 实际归还时间NULL表示未归还, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_relic_id (relic_id), KEY idx_type_time (type, expected_return_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文物出入库流水表;这里的idx_type_time组合索引是给后续定时提醒用的查「外借出且超过预计归还时间未还」的文物走这个索引可以快速过滤。索引不是为了查询快而建而是为了特定业务语句而建想清楚再建。2.3 外借审批表与状态机的对照关系审批表负责记录申请和审批过程。一张表同时存申请信息和审批结果先插入待审记录审批通过后更新状态和审批人这是单体管理系统里最常规的做法无需拆成申请头和审批链两张表。CREATE TABLE relic_approval ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, relic_id BIGINT NOT NULL COMMENT 文物主键, apply_user VARCHAR(32) NOT NULL COMMENT 申请人, apply_reason VARCHAR(255) NOT NULL COMMENT 申请事由, borrow_start DATE NOT NULL COMMENT 预计借出日期, borrow_end DATE NOT NULL COMMENT 预计归还日期, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1通过 2驳回, approver VARCHAR(32) DEFAULT NULL COMMENT 审批人, approve_comment VARCHAR(255) DEFAULT NULL COMMENT 审批意见, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT外借审批表;文物状态流转的约束关系用一个对照表能说清当前状态允许的操作流转后状态在库(0)出库、外借申请出库(1)、外借(2)出库(1)归还入库在库(0)外借(2)外借归还在库(0)修复中(3)修复完成回库在库(0)注意状态机如果有复杂的审批链可以把审批状态单独建表但这个小系统的核心是状态闭环不要引入工作流引擎这是过度设计。3. 用 Spring Boot MyBatis-Plus 实现检索与审批状态流转3.1 依赖选型与版本陷阱搭建这类系统持久层选 MyBatis-Plus 是主流做法单表 CRUD 不需要写 SQL查询条件构造器也能省掉大量 XML。先看依赖这里有一个高频踩坑点Spring Boot 3.x 和 2.x 对应的 starter 坐标不同。!-- Spring Boot 2.7.x 使用这个 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency !-- Spring Boot 3.x 必须改成 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency如果是在 IDEA 里从 start.spring.io 创建项目默认拉到 Spring Boot 3.x再用mybatis-plus-boot-starter就会在启动时报MapperScan相关错误或 Bean 加载异常。这是「Spring Boot 版本太高」最常见的一类现场。我的建议是如果只是做管理系统这种常规 Web 项目直接锁定 Spring Boot 2.7.18稳定且网上能查到的资料最多如果坚持用 3.x确认 JDK 17 和mybatis-plus-spring-boot3-starter这两个前提。3.2 登录认证JWT HandlerInterceptor 的轻量路径管理系统的后台接口不能裸奔但引入 Spring Security 对这个体量的项目来说太繁琐。常见的做法是 JWT 加一个拦截器登录接口发放 token其余接口校验 token。轻量、够用、可控。public class JwtUtil { private static final SecretKey KEY Keys.hmacShaKeyFor( relic-system-secret-key-please-change-123456.getBytes(StandardCharsets.UTF_8)); public static String generate(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(uid, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 12 * 60 * 60 * 1000)) .signWith(KEY, SignatureAlgorithm.HS256) .compact(); } }提示hmacShaKeyFor要求密钥长度至少 32 字节短了会直接抛WeakKeyException这是 jjwt 0.11 的硬性安全要求。拦截器校验逻辑就是取 Header 里的Authorization去掉Bearer前缀后解析解析失败返回 401。注册拦截器时放行登录接口、静态资源和上传目录其余路径全部拦。3.3 出库流转事务、状态校验与流水落库出入库是这个系统的核心操作要保证「修改文物状态」和「插入流水」要么都成功要么都失败。这里必须加Transactional否则状态改了流水没写进去后续盘库就对不上账。Service RequiredArgsConstructor public class RelicStockService { private final RelicMapper relicMapper; private final StockLogMapper stockLogMapper; Transactional(rollbackFor Exception.class) public void stockOut(StockOutCommand cmd) { Relic relic relicMapper.selectById(cmd.getRelicId()); if (relic null) { throw new BizException(文物不存在); } if (relic.getStatus() ! RelicStatus.IN_STOCK) { throw new BizException(当前状态不可出库); } relic.setStatus(RelicStatus.OUT_STOCK); relicMapper.updateById(relic); StockLog log new StockLog(); log.setRelicId(relic.getId()); log.setRelicNo(relic.getRelicNo()); log.setType(StockLogType.OUT); log.setApplyReason(cmd.getReason()); log.setOperator(cmd.getOperator()); stockLogMapper.insert(log); } }这里有两个细节值得说明。第一状态校验用!而不是枚举判断因为status来自数据库可能包含脏数据直接比较反而能兜住未知状态。第二Transactional(rollbackFor Exception.class)必须显式指定受检查异常Spring 默认只对运行时异常回滚如果自定义了受检查异常不加这个参数会导致事务不生效。3.4 多条件检索LambdaQueryWrapper 的条件参数文物档案的检索页面通常是编号模糊、名称模糊、状态精确、年代区间。这不是固定 SQL 能覆盖的需要动态拼接条件。MyBatis-Plus 的LambdaQueryWrapper第一个布尔参数是「是否启用该条件」比手写 if-else 干净得多。public PageResultRelicVO pageQuery(RelicQuery query) { LambdaQueryWrapperRelic wrapper Wrappers.lambdaQuery(Relic.class) .eq(StringUtils.hasText(query.getRelicNo()), Relic::getRelicNo, query.getRelicNo()) .like(StringUtils.hasText(query.getName()), Relic::getName, query.getName()) .eq(query.getStatus() ! null, Relic::getStatus, query.getStatus()) .orderByDesc(Relic::getUpdatedAt); PageRelic page relicMapper.selectPage( new Page(query.getPageNum(), query.getPageSize()), wrapper); return PageResult.of(page.getRecords(), page.getTotal()); }like的模糊匹配默认是%name%这个语义是「包含」不是「以什么开头」。如果想用左前缀匹配就要手写.apply(name like concat({0}, %), query.getName())。年代区间查询同理如果前端传的是「唐代」「宋代」这种枚举值直接用eq精确匹配只有传年限范围时才需要between。4. 文物图片上传路径映射、安全放行与联调排错4.1 上传目录与静态资源映射配置文物档案必须配图否则管理毫无意义。图片文件的正确做法是存磁盘数据库里存相对路径而不是把图片二进制塞进数据库或者用 Base64 字符串存字段。先在application.yml里定义自定义路径spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB custom: upload-dir: /data/relic-uploadmax-file-size限制单文件大小max-request-size限制单次请求总大小。这两个参数要同时设只设前者会导致多文件上传时被后者拦截。Spring Boot 默认只映射classpath:/static/外部磁盘路径要手动加映射。写一个WebMvcConfigurer的实现类Configuration public class WebConfig implements WebMvcConfigurer { Value(${custom.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadDir File.separator); } }addResourceHandler(/uploads/**)表示 URL 以/uploads/开头的请求去本地磁盘uploadDir目录找对应的文件。file:协议前缀不能漏后面目录分隔符也要带Windows 下File.separator是反斜杠Linux 下是正斜杠用这个写法可以避免跨平台路径拼接问题。4.2 上传接口UUID 重命名与扩展名白名单上传接口的代码套路很固定但有两个安全习惯必须养成文件名不能用前端原始名扩展名要做白名单校验。PostMapping(/api/relic/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { String originalFilename file.getOriginalFilename(); String ext StringUtils.getFilenameExtension(originalFilename); if (ext null || !Set.of(jpg, jpeg, png, webp).contains(ext.toLowerCase())) { throw new BizException(仅支持 jpg/jpeg/png/webp 图片格式); } String filename UUID.randomUUID().toString().replace(-, ) . ext; Path target Path.of(uploadDir, filename); Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING); return Result.ok(/uploads/ filename); }UUID.randomUUID()生成 32 位随机字符串作为文件名避免中文文件名、路径穿越和重名覆盖三类问题。StringUtils.getFilenameExtension取扩展名时是安全的它只截取最后一个点后面的内容。注意前端如果传非法路径如../../evil.jspgetFilenameExtension依然能拿到扩展名但文件名已经被 UUID 替换跳出了目录所以上传接口的核心防御就是「永远不使用原始文件名」。4.3 与前端联调时的三个典型问题联调阶段最常踩的坑按出现频率排序是跨域、静态资源 404、MultipartFile 拿不到值。跨域问题Spring Boot 2.4 之后allowedOrigins(*)不允许和allowCredentials(true)共存必须改用allowedOriginPatterns(*)。Vue 项目如果开了withCredentials后端 CORS 配置要写成registry.allowedOriginPatterns(*).allowCredentials(true)。图片 404先确认网络面板里的状态码。如果是 404用浏览器直接访问图片 URL看返回的是 HTML 还是纯文字错误如果是 Spring Boot 错误页说明映射没生效检查addResourceLocations的路径末尾有没有写文件分隔符如果是 403查一下磁盘目录自己的写权限。记住一个原则开发环境图片能访问生产环境 404先查 Nginx 有没有配置/uploads的 location而不是改 Java 代码。MultipartFile 为空这几乎都是前端的问题。用 axios 上传时Content-Type要交给浏览器自动生成手动设置成application/json会让 Spring 无法解析 multipart 数据。前端正确写法是let formData new FormData(); formData.append(file, file); axios.post(url, formData)不要再手动指定头。提示生产环境不要暴露/actuator/heapdump和/actuator/env端点这类敏感信息泄露漏洞在 Spring Boot 项目里很常见。如果不需要监控直接不引入 actuator 依赖比配置放行更省心。5. 进阶技巧Spring Task 定时盘点外借文物并输出提醒5.1 需求场景外借文物到了预计归还日期没还靠人工盯不现实。利用 Spring Task 写一个定时任务每天凌晨扫一遍流水表找出「外借出且实际归还时间为空且超过预计归还日期」的记录输出提醒。这是流水表actual_return_time字段和组合索引idx_type_time发挥作用的地方也是这个系统从「能增删改查」到「能用起来」的一个分水岭。5.2 实现定时任务在启动类或配置类上开启EnableScheduling再写一个任务类Component RequiredArgsConstructor public class BorrowRemindTask { private static final Logger log LoggerFactory.getLogger(BorrowRemindTask.class); private final StockLogMapper stockLogMapper; Scheduled(cron 0 0 1 * * ?) public void checkOverdueBorrow() { ListStockLog overdueList stockLogMapper.selectList( Wrappers.lambdaQuery(StockLog.class) .eq(StockLog::getType, StockLogType.BORROW_OUT) .isNull(StockLog::getActualReturnTime) .lt(StockLog::getExpectedReturnTime, LocalDate.now()) ); overdueList.forEach(item - log.warn( 文物[{}]外借已超过预计归还日期[{}]经办人[{}], item.getRelicNo(), item.getExpectedReturnTime(), item.getOperator())); } }Scheduled(cron 0 0 1 * * ?)是每天凌晨 1 点执行一次。cron 表达式有 6 位秒、分、时、日、月、星期这里0 0 1表示 1 点 0 分 0 秒*表示每天?表示不指定星期几。查询条件里isNull(actualReturnTime)和lt(expectedReturnTime, LocalDate.now())两个条件联合起来就精确命中「应该还但没还」的记录。5.3 验证定时任务是否正常触发定时任务最怕写了不触发这里给一套快速的验证方法第一步临时把 cron 表达式改成Scheduled(cron */10 * * * * ?)每 10 秒执行一次避免等一天。第二步手工往relic_stock_log插入一条测试数据type3外借出、actual_return_timeNULL、expected_return_time昨天。第三步观察控制台日志是否出现文物[xxx]外借已超过预计归还日期的 warn 输出。第四步验证完成后删除测试数据把 cron 表达式改回0 0 1 * * ?同时确认EnableScheduling只开启了配置。这个方法同样适用于任何Scheduled任务核心套路是「缩短周期、造一条触发数据、看日志、还原现场」。本文还有配套的精品资源点击获取
返回列表