SpringBoot企业档案管理系统实战:RBAC权限、JWT认证与FastDFS文件存储
2026/9/5 15:11:12 网站建设 项目流程

简介:本资源是一套面向计算机专业本科生的毕业设计级企业档案管理信息系统完整实现方案,基于SpringBoot框架构建,适用于Java Web开发学习、课程设计及毕设参考。系统采用B/S架构,涵盖管理员与普通用户双角色权限体系,功能模块完整,包括档案全生命周期管理(录入、分类、借阅、归还)、人事相关模块(考勤、工资、奖罚)、资料文件管理及意见箱交互等14类核心业务,技术栈清晰明确(Java+MySQL+SpringBoot+Maven),配套开发环境配置说明完备。压缩包共16.21MB,内含可直接运行的源码工程、MySQL数据库脚本、详细设计文档及答辩用PPT,各类文件分工明确:源码支撑功能实现,SQL脚本用于快速建库建表,文档阐述需求分析与系统设计,PPT提炼技术要点与演示逻辑。目前已有44人学习下载,内容结构规范、注释充分,适合作为SpringBoot实战入门与企业级应用开发范例。

1. 项目概述与核心价值

最近在整理过往项目资料时,翻到了一个几年前做的“企业档案管理信息系统”,从设计文档、源码到数据库脚本、部署PPT一应俱全。这个项目虽然技术栈不算最新,但它的设计思路和实现细节,对于想用SpringBoot做企业级后台管理系统的朋友来说,依然有很强的参考价值。企业档案管理,听起来好像就是增删改查,但真做起来,你会发现里面涉及到文档的生命周期管理、权限的精细控制、大量非结构化数据的存储与检索,以及如何保证数据的安全性和可追溯性。这个项目完整地走通了从需求分析、技术选型、编码实现到部署上线的全流程,算是一个麻雀虽小五脏俱全的典型案例。

这个系统主要解决了企业纸质档案电子化过程中的几个核心痛点:一是档案信息杂乱,查询借阅全靠人工,效率低下且易出错;二是权限管理粗放,谁都能看,存在信息泄露风险;三是档案流转过程不透明,借出后去了哪里、何时归还难以追踪;四是缺乏有效的统计和报表功能,管理层无法直观掌握档案资产状况。我们基于SpringBoot快速构建了后端服务,用主流的Vue.js做了前端,数据库选择了MySQL,并针对文件存储引入了FastDFS。接下来,我会把这个项目的核心设计、关键实现以及我踩过的那些坑,毫无保留地分享出来。

2. 系统整体架构与设计思路拆解

2.1 为什么选择SpringBoot作为技术底座

当时技术选型时,微服务概念正热,但考虑到团队规模和项目复杂度(这是一个典型的单体应用),我们最终选择了SpringBoot。这不是保守,而是务实。SpringBoot的“约定大于配置”理念,让我们能快速搭建起一个稳定、可运行的后端服务,避免了在XML配置上耗费大量时间。它内嵌了Tomcat,打包成JAR即可独立运行,部署极其简单。更重要的是,SpringBoot拥有极其丰富的Starter生态,无论是连接数据库(spring-boot-starter-data-jpa 或 mybatis-spring-boot-starter)、做安全控制(spring-boot-starter-security)、还是集成缓存(spring-boot-starter-data-redis),几乎都是开箱即用,极大地提升了开发效率。

在架构设计上,我们采用了经典的分层模式:Controller层处理HTTP请求和响应;Service层实现核心业务逻辑;DAO层(Repository)负责与数据库交互。同时,我们严格遵循了RESTful API设计规范,这使得前后端分离非常彻底,前端可以独立开发,后端API也清晰易懂。为了应对可能未来向微服务演进的需求,我们在模块划分上做了些预留,比如将用户中心、档案管理、文件服务等逻辑在代码层面进行了初步的分离,虽然共用一个数据库,但为后续拆分埋下了伏笔。

2.2 核心功能模块设计

整个系统围绕档案的“全生命周期”进行设计,主要划分为五大模块:

  1. 系统管理模块:这是所有后台系统的基石。包含用户管理、角色管理、部门管理和菜单权限管理。我们实现了基于RBAC(角色-权限访问控制)的权限模型,用户通过角色关联权限,权限精确到按钮级别。例如,“档案管理员”角色有录入、修改、删除档案的权限,而“普通员工”角色可能只有查询和申请借阅的权限。
  2. 档案基础信息管理模块:这是核心业务模块。负责对档案实体进行管理,包括档案的录入、编辑、删除、查询和详情查看。每条档案信息包含元数据,如档案编号、名称、类型(合同、人事、财务等)、密级、保管期限、存放位置、责任人等。
  3. 档案借阅与流转模块:模拟线下借阅流程。员工在线提交借阅申请,审批人(可能是部门领导或档案管理员)在线审批。审批通过后,生成借阅记录,并更新档案状态为“已借出”。系统会记录借出人、借出时间、应还时间。归还时,管理员确认后,状态更新为“在库”。整个流程线上留痕,责任清晰。
  4. 文件存储与管理模块:这是技术难点之一。档案不仅有条目信息,更有对应的电子文件(扫描的PDF、图片等)。我们使用FastDFS作为分布式文件存储系统,将文件实际内容存储在FastDFS集群中,而在数据库里只保存文件的唯一标识(如group1/M00/00/00/xxx.pdf)、原始文件名、大小、MIME类型等元数据。这样实现了文件存储的扩展性和高可用。
  5. 统计报表模块:为管理层提供数据洞察。包括档案总量统计、按类型/密级/部门分布、借阅排行榜、逾期未归还统计等。我们使用ECharts进行前端图表展示,后端提供聚合查询的API。

注意:在设计初期,一定要和业务方反复确认档案的“状态机”。比如,档案从“新建”、“审核中”、“在库”、“借出”、“待归还”、“已归档”到“已销毁”,状态如何流转,每个状态允许哪些操作。这直接关系到核心业务流程的正确性。

3. 关键技术与核心细节实现

3.1 数据库设计与优化实践

数据库设计是系统的灵魂。我们主要设计了以下几张核心表:

  • sys_user(用户表):存储登录账号、密码(加密存储)、姓名、所属部门等。
  • sys_role(角色表)sys_menu(菜单/权限表)sys_user_role(用户角色关联表)sys_role_menu(角色权限关联表):这四张表构成了RBAC权限模型的基础。
  • archives(档案主表):存储档案的核心元数据。
    CREATE TABLE `archives` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `archive_no` varchar(64) NOT NULL COMMENT '档案编号,唯一', `archive_name` varchar(255) NOT NULL COMMENT '档案名称', `archive_type` varchar(50) COMMENT '档案类型', `secret_level` varchar(20) COMMENT '密级', `storage_location` varchar(255) COMMENT '存放位置', `responsible_user_id` bigint(20) COMMENT '责任人ID', `status` varchar(20) DEFAULT 'IN_LIBRARY' COMMENT '状态: IN_LIBRARY, BORROWED, ETC.', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_archive_no` (`archive_no`), KEY `idx_type_status` (`archive_type`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='档案主表';
  • archive_files(档案文件表):与档案主表一对多关联,存储文件在FastDFS中的路径等信息。
  • borrow_apply(借阅申请表)borrow_record(借阅记录表):记录借阅申请和实际的借还操作。

优化点

  1. 字段设计:对于像statustype这类枚举值,使用varchar存储可读性更好,但我们在后端用枚举类严格约束。
  2. 索引策略:除了主键,我们在archive_no上建立了唯一索引防止重复;在archive_typestatus上建立了联合索引,因为这是最常用的查询组合(如“查询所有在库的合同档案”)。
  3. 软删除:我们没有使用物理删除,而是为重要业务表添加了is_deleted字段(默认0),进行逻辑删除。这便于数据追溯和恢复。
  4. 分页优化:对于档案列表这种可能数据量很大的查询,我们坚决避免使用limit m, n这种深度分页,而是在前端传入“最后一条记录的ID”或“时间戳”,后端使用where id > ? limit n的方式,效率要高得多。

3.2 使用Spring Security + JWT实现安全控制

安全是企业的生命线。我们采用Spring Security框架整合JWT(JSON Web Token)来实现无状态的认证与授权。

流程简述

  1. 用户登录:前端提交用户名密码。
  2. 后端验证通过后,使用密钥(如HS256算法)生成一个JWT Token,其中包含用户ID、用户名、权限列表等关键信息(注意不要放密码)。
  3. 将Token返回给前端,前端后续请求都在HTTP Header的Authorization字段中携带此Token(格式:Bearer <token>)。
  4. 后端配置一个JwtAuthenticationFilter,在Spring Security过滤器链中,于登录验证之前拦截请求。它解析Token,验证其有效性(是否过期、签名是否正确),并从中提取用户信息,构建一个Authentication对象放入安全上下文(SecurityContextHolder)。
  5. 后续的权限校验(如@PreAuthorize("hasRole('ADMIN')"))就可以直接使用了。

核心配置与代码片段: 首先,在pom.xml中引入依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>

然后,编写一个JWT工具类,负责生成和解析Token:

@Component public class JwtTokenUtil { private String secret = "your-secret-key"; // 应从配置中心读取 private Long expiration = 7200000L; // 2小时 public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put("sub", userDetails.getUsername()); claims.put("created", new Date()); // 可以将角色等信息也放入claims return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public String getUsernameFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject(); } // ... 其他验证方法 }

最后,配置Spring Security,关键是要放行登录接口,并添加自定义的JWT过滤器:

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() // 前后端分离项目通常禁用CSRF .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers("/api/auth/login").permitAll() // 登录接口放行 .antMatchers("/api/files/**").permitAll() // 文件下载接口有时也需放行或特殊处理 .anyRequest().authenticated() // 其他所有请求都需要认证 .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // 添加JWT过滤器 } }

实操心得:JWT的secret密钥一定要足够复杂且妥善保管,最好放在配置中心或环境变量中,不要硬编码在代码里。Token的过期时间需要根据业务场景权衡,太短影响体验,太长不安全。我们项目中,普通操作Token设为2小时,并提供了刷新Token的机制。

3.3 集成FastDFS管理非结构化文件

档案系统的核心资产是文件。自建文件服务器管理麻烦,扩展性差。我们选择了FastDFS,一个轻量级的分布式文件系统。

集成步骤

  1. 部署FastDFS集群:至少需要一台Tracker Server(调度服务器)和一台Storage Server(存储服务器)。生产环境建议多台做集群。
  2. 引入Java客户端:我们使用了fastdfs-client-java这个库。
  3. 编写配置文件:在application.yml中配置Tracker Server的地址。
    fdfs: so-timeout: 1500 connect-timeout: 600 tracker-list: 192.168.1.100:22122
  4. 编写服务类:封装上传、下载、删除等方法。
    @Service public class FastDfsService { @Autowired private FastFileStorageClient storageClient; public String upload(MultipartFile file) throws IOException { StorePath storePath = storageClient.uploadFile( file.getInputStream(), file.getSize(), FilenameUtils.getExtension(file.getOriginalFilename()), null); return storePath.getFullPath(); // 返回如 group1/M00/00/00/xxx.jpg } public void download(String filePath, HttpServletResponse response) throws IOException { // 根据filePath从FastDFS下载文件流,并写入response byte[] bytes = storageClient.downloadFile(filePath); // ... 设置response的Content-Type, Header等 response.getOutputStream().write(bytes); } }
  5. 业务关联:在档案上传接口中,先调用FastDfsService.upload()获取文件路径,然后将这个路径和其他档案元数据一起保存到archive_files表。

注意事项

  • 文件去重:可以在上传前计算文件的MD5值,在数据库中记录。下次上传相同文件时,直接返回已有的文件路径,实现秒传,节省存储空间。
  • 缩略图:对于图片类档案,可以在上传后,使用Java的图片处理库(如Thumbnailator)生成缩略图,并同样上传到FastDFS,数据库中记录缩略图路径,用于列表页快速展示。
  • 权限控制:文件下载接口不能简单放行。需要在下载前,校验当前登录用户是否有权限访问该文件对应的档案。我们通常是在下载URL中携带一个加密的、有时效性的Token,后端验证Token有效性后才提供文件流。

4. 核心业务逻辑与接口实现详解

4.1 档案借阅审批流程的实现

借阅流程是一个典型的工作流,我们用一个相对轻量的状态机在代码中实现,而没有引入复杂的工作流引擎。

实体与状态设计

  • BorrowApply(借阅申请):包含申请人、档案ID、申请时间、期望借出时间、预计归还时间、申请事由、状态(PENDING待审批、APPROVED已通过、REJECTED已驳回、CANCELLED已取消)等字段。
  • BorrowRecord(借阅记录):在申请通过后创建,包含借阅申请ID、实际借出人(管理员)、实际借出时间、应还时间、实际归还时间、状态(BORROWED借出中、RETURNED已归还、OVERDUE已逾期)等。

核心Service方法逻辑

@Service @Transactional public class BorrowService { @Autowired private BorrowApplyRepository applyRepository; @Autowired private BorrowRecordRepository recordRepository; @Autowired private ArchivesRepository archivesRepository; /** * 提交借阅申请 */ public BorrowApply submitApply(BorrowApplyDTO dto, Long userId) { BorrowApply apply = new BorrowApply(); // ... 属性填充 apply.setApplicantId(userId); apply.setStatus(BorrowApplyStatus.PENDING); // 校验档案是否存在且状态为在库 Archives archive = archivesRepository.findById(dto.getArchiveId()) .orElseThrow(() -> new BusinessException("档案不存在")); if (!ArchiveStatus.IN_LIBRARY.equals(archive.getStatus())) { throw new BusinessException("该档案当前不可借阅"); } return applyRepository.save(apply); } /** * 审批借阅申请 */ public void approveApply(Long applyId, Long approverId, boolean approved, String remark) { BorrowApply apply = applyRepository.findById(applyId) .orElseThrow(() -> new BusinessException("申请不存在")); if (!BorrowApplyStatus.PENDING.equals(apply.getStatus())) { throw new BusinessException("该申请已处理,无法重复操作"); } if (approved) { apply.setStatus(BorrowApplyStatus.APPROVED); apply.setApproverId(approverId); apply.setApproveTime(new Date()); apply.setApproveRemark(remark); // 创建借阅记录 BorrowRecord record = new BorrowRecord(); record.setApplyId(applyId); record.setArchiveId(apply.getArchiveId()); record.setBorrowerId(apply.getApplicantId()); record.setLenderId(approverId); // 审批人即操作借出的管理员 record.setLendTime(new Date()); // 计算应还时间,这里简单处理,实际可能根据档案类型有不同规则 record.setDueReturnTime(calculateDueDate(record.getLendTime(), apply.getExpectedDays())); record.setStatus(BorrowRecordStatus.BORROWED); recordRepository.save(record); // 更新档案状态为“已借出” Archives archive = archivesRepository.findById(apply.getArchiveId()).get(); archive.setStatus(ArchiveStatus.BORROWED); archivesRepository.save(archive); } else { apply.setStatus(BorrowApplyStatus.REJECTED); apply.setApproverId(approverId); apply.setApproveTime(new Date()); apply.setApproveRemark(remark); } applyRepository.save(apply); } /** * 归还档案 */ public void returnArchive(Long recordId, Long operatorId) { BorrowRecord record = recordRepository.findById(recordId) .orElseThrow(() -> new BusinessException("借阅记录不存在")); if (!BorrowRecordStatus.BORROWED.equals(record.getStatus())) { throw new BusinessException("该档案并非借出状态,无法归还"); } record.setStatus(BorrowRecordStatus.RETURNED); record.setActualReturnTime(new Date()); record.setReturnOperatorId(operatorId); recordRepository.save(record); // 更新档案状态为“在库” Archives archive = archivesRepository.findById(record.getArchiveId()).get(); archive.setStatus(ArchiveStatus.IN_LIBRARY); archivesRepository.save(archive); } }

关键点

  • 事务控制approveApplyreturnArchive方法都加了@Transactional注解,确保申请、记录、档案状态三者更新的一致性。
  • 状态校验:每一个状态变更操作前,都必须校验前置状态是否正确,这是保证业务流程不乱的核心。
  • 逾期处理:我们通过一个定时任务(使用Spring的@Scheduled)每天扫描BorrowRecord表中状态为BORROWEDdue_return_time < NOW()的记录,将其状态更新为OVERDUE,并可以发送通知(如站内信、邮件)给借阅人和管理员。

4.2 复杂查询与统计功能的实现

档案系统经常需要按多种条件组合查询,并且需要分页。我们使用Spring Data JPA的Specification动态查询和Pageable分页来实现。

示例:多条件分页查询档案列表

@RestController @RequestMapping("/api/archives") public class ArchivesController { @Autowired private ArchivesService archivesService; @GetMapping public PageResult<ArchivesVO> listArchives( @RequestParam(required = false) String archiveName, @RequestParam(required = false) String archiveType, @RequestParam(required = false) String secretLevel, @RequestParam(required = false) String status, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { ArchivesQueryDTO queryDTO = new ArchivesQueryDTO(archiveName, archiveType, secretLevel, status); Pageable pageable = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Direction.DESC, "createTime")); Page<Archives> page = archivesService.queryByCondition(queryDTO, pageable); // 将Page<Archives>转换为PageResult<ArchivesVO>返回 return PageResult.success(page); } } @Service public class ArchivesServiceImpl implements ArchivesService { @Autowired private ArchivesRepository archivesRepository; @Override public Page<Archives> queryByCondition(ArchivesQueryDTO queryDTO, Pageable pageable) { return archivesRepository.findAll((Specification<Archives>) (root, query, criteriaBuilder) -> { List<Predicate> predicates = new ArrayList<>(); if (StringUtils.hasText(queryDTO.getArchiveName())) { // 模糊查询 predicates.add(criteriaBuilder.like(root.get("archiveName"), "%" + queryDTO.getArchiveName() + "%")); } if (StringUtils.hasText(queryDTO.getArchiveType())) { predicates.add(criteriaBuilder.equal(root.get("archiveType"), queryDTO.getArchiveType())); } if (StringUtils.hasText(queryDTO.getSecretLevel())) { predicates.add(criteriaBuilder.equal(root.get("secretLevel"), queryDTO.getSecretLevel())); } if (StringUtils.hasText(queryDTO.getStatus())) { predicates.add(criteriaBuilder.equal(root.get("status"), queryDTO.getStatus())); } // 可以添加更多条件,如时间范围、责任人等 query.where(predicates.toArray(new Predicate[0])); return query.getRestriction(); }, pageable); } }

统计功能实现: 对于统计报表,我们主要使用JPA的@Query注解写原生SQL或JPQL进行聚合查询,因为这类查询通常不涉及复杂动态条件,但需要GROUP BY和聚合函数。

@Repository public interface ArchivesRepository extends JpaRepository<Archives, Long>, JpaSpecificationExecutor<Archives> { /** * 按档案类型统计数量 */ @Query("SELECT a.archiveType as type, COUNT(a) as count FROM Archives a GROUP BY a.archiveType") List<Map<String, Object>> countByType(); /** * 统计各部门的借阅次数(Top 10) */ @Query(value = "SELECT d.name as deptName, COUNT(br.id) as borrowCount " + "FROM borrow_record br " + "JOIN sys_user u ON br.borrower_id = u.id " + "JOIN sys_dept d ON u.dept_id = d.id " + "WHERE br.lend_time BETWEEN :startDate AND :endDate " + "GROUP BY d.id, d.name " + "ORDER BY borrowCount DESC LIMIT 10", nativeQuery = true) List<Map<String, Object>> countBorrowByDept(@Param("startDate") Date startDate, @Param("endDate") Date endDate); }

踩坑记录:在使用Specification进行多表关联的复杂动态查询时,性能可能会成为问题,特别是关联表数据量大时。如果遇到性能瓶颈,可以考虑以下方案:1) 优化数据库索引;2) 将复杂的多条件查询拆分成多个步骤,或者引入Elasticsearch这类搜索引擎来承担复杂的查询任务;3) 对于固定的报表,可以定时将统计结果计算好存入缓存或单独的统计表,查询时直接读取。

5. 项目部署、运维与问题排查

5.1 从开发到生产:部署实战

项目开发完成后,我们使用Maven进行打包,并采用Docker容器化部署,以保证环境一致性。

1. 打包与配置分离: 使用Spring Boot的spring-boot-maven-plugin打包成可执行的JAR文件。关键一步是配置分离,我们将application.yml中的生产环境配置(数据库密码、FastDFS地址、JWT密钥等)抽离出来,放在JAR包外部的config目录下。Spring Boot会自动加载该目录下的配置文件,优先级高于JAR包内的。

# 项目目录结构 /opt/archive-system/ ├── archive-system.jar # 主程序JAR包 ├── config/ │ └── application-prod.yml # 生产环境配置文件 └── logs/ # 日志目录

2. Docker化部署: 编写Dockerfile

FROM openjdk:8-jdk-alpine VOLUME /tmp # 将Maven打包好的jar包复制到容器中,并重命名 COPY target/archive-system-0.0.1-SNAPSHOT.jar app.jar # 指定容器启动时运行的程序 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

构建镜像并运行:

docker build -t archive-system:latest . docker run -d -p 8080:8080 \ -v /opt/archive-system/config:/config \ -v /opt/archive-system/logs:/logs \ --name archive-system \ archive-system:latest

3. 数据库与中间件

  • MySQL:同样建议用Docker部署,并做好数据卷挂载,保证数据持久化。记得在首次启动后,执行项目中的SQL脚本初始化数据库结构。
  • FastDFS:需要单独部署Tracker和Storage服务。生产环境务必搭建集群,并考虑使用Nginx做Storage的负载均衡和文件访问的反代。
  • Redis(如果用了缓存或Session共享):同样容器化部署。

5.2 常见问题与排查技巧实录

在实际运行中,我们遇到了不少问题,这里总结几个典型的:

问题一:文件上传后,下载链接无法访问或速度慢。

  • 排查
    1. 首先检查FastDFS的Storage Server是否正常运行,Tracker能否正确调度。
    2. 检查返回给前端的文件路径(group1/M00/00/00/xxx.jpg)是否正确。
    3. 如果使用Nginx代理访问文件,检查Nginx配置是否正确,特别是location ~ /group[0-9]/M00的配置,以及root路径是否指向了Storage的data目录。
    4. 检查服务器防火墙是否开放了对应的端口(通常是80/8080用于Nginx,22122用于Tracker)。
  • 解决:我们遇到过一次是因为Storage服务器的磁盘空间满了,导致新文件无法存储。还有一次是Nginx配置中root路径写错了。建议将文件访问域名(如file.yourdomain.com)直接解析到Nginx,并在代码中拼接完整的URL返回给前端,如http://file.yourdomain.com/group1/M00/00/00/xxx.jpg

问题二:系统运行一段时间后,列表查询越来越慢。

  • 排查
    1. 登录数据库,使用SHOW PROCESSLIST;查看当前慢查询。
    2. 对慢查询的SQL语句使用EXPLAIN分析执行计划,看是否走了正确的索引。
    3. 检查代码中是否有深分页查询(limit 100000, 10),这种查询会扫描大量无效数据。
    4. 检查实体类关联关系(如@OneToMany)是否配置了懒加载(FetchType.LAZY),避免N+1查询问题。
  • 解决:我们针对archives表的archive_typestatus字段加了联合索引,解决了大部分列表查询慢的问题。对于深分页,改用了基于“最后ID”的分页方式。对于复杂的统计报表查询,我们将其改为在每天凌晨通过定时任务计算好结果,存入一张statistics_daily表,前端查询时直接查这张表,速度极快。

问题三:用户反馈偶尔登录失败,提示“Token无效”。

  • 排查
    1. 检查服务器时间是否准确,JWT校验依赖服务器时间。
    2. 检查生成Token和验证Token使用的secret是否一致(尤其是在集群部署时,多台机器必须一致)。
    3. 检查Token是否已过期。前端可能在Token即将过期时发起了请求,但收到响应时Token刚好过期。
  • 解决:我们统一使用了NTP服务同步服务器时间。在集群部署时,将JWT的secret放在配置中心(如Nacos)或环境变量中,确保所有实例一致。此外,我们实现了Token刷新机制:当Token即将过期时(比如还剩5分钟),前端调用刷新接口,后端验证旧Token有效后,颁发一个新的Token给前端。

问题四:并发借阅时,同一份档案被借出两次。

  • 排查:这是一个典型的并发问题。在approveApply方法中,虽然我们检查了档案状态,但在“检查”和“更新状态”之间,可能存在另一个线程也通过了检查。
  • 解决:我们需要在更新档案状态时加锁。有两种方式:
    1. 数据库悲观锁:在查询档案时使用SELECT ... FOR UPDATE。但这对性能有影响。
    2. 乐观锁:为archives表增加一个version字段(整数类型)。更新时,UPDATE archives SET status = 'BORROWED', version = version + 1 WHERE id = ? AND version = ?。如果更新行数为0,说明版本不对,抛出异常,让上层业务重试或提示用户“档案状态已变更”。我们最终采用了乐观锁方案,因为它更轻量,适合读多写少的场景。

6. 前端交互与用户体验优化

虽然本文重点在后端,但一个系统的成功离不开良好的前端体验。我们使用Vue.js + Element UI构建了管理后台。

几个关键的前后端协作点

  1. API接口规范:我们定义了统一的响应体格式,包含codemessagedata。例如,成功时返回{“code”: 200, “message”: “成功”, “data”: {...}},失败时返回{“code”: 500, “message”: “服务器内部错误”}。前端根据code进行统一处理。
  2. 文件上传:使用Element UI的el-upload组件,配置为手动上传(:auto-upload="false"),在beforeUpload钩子中校验文件类型和大小,然后通过axiosFile对象传到后端的MultipartFile接口。上传过程中显示进度条。
  3. 大文件下载:对于从FastDFS下载的大文件,后端设置response.setHeader(“Content-Disposition”, “attachment;filename=” + fileName),前端通过window.location.href = url触发浏览器下载。对于需要权限验证的下载,我们通过一个单独的下载接口,先验证权限,再通过后端代理将文件流输出。
  4. 实时通知:对于借阅申请审批、逾期提醒等,我们使用了WebSocket。当管理员登录后,建立WebSocket连接。后端在审批动作完成后,向对应用户的WebSocket会话发送一条通知消息,前端收到后,在页面右上角显示一个通知气泡。这比让用户手动刷新页面体验好得多。

性能优化

  • 图片懒加载:档案列表页的缩略图使用懒加载,减少首次页面加载的请求数。
  • API数据缓存:对于不常变的基础数据,如档案类型、密级枚举等,前端在首次加载后存入localStorage或Vuex中,减少不必要的API调用。
  • 表格虚拟滚动:当档案数据量极大时,使用如vue-virtual-scroller等组件实现表格的虚拟滚动,只渲染可视区域内的行,大幅提升渲染性能。

这个“企业档案管理信息系统”项目,从技术上看,它融合了SpringBoot、Spring Security、JPA、JWT、FastDFS、Docker等一批当时的主流技术,是一个非常好的全栈实践样本。从业务上看,它完整地实现了一个具有核心业务流程的企业应用。我在这个项目里最大的体会是,业务逻辑的严谨性远比重磅技术更重要。把档案的状态流转、权限的边界、数据的完整性这些业务规则想清楚、实现稳,比盲目追求新技术更能让系统可靠地跑起来。源码和文档我都整理好了,如果你正在学习或打算做一个类似的管理系统,希望这份“旧”资料里的“实”战经验,能给你带来一些新的启发。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询