1. 项目背景与核心需求
这个线上历史馆藏管理系统源于某省级档案馆的数字化改造需求。传统纸质档案管理存在查阅不便、保存成本高、易损毁等问题,我们团队接到的核心任务是构建一个支持百万级档案条目在线管理的全栈系统。
系统需要实现三大核心能力:
- 多维度档案检索(支持全文、分类、时间范围等组合查询)
- 高精度图像展示(支持古籍文献的放大、旋转、对比度调节)
- 分级权限管理体系(区分游客、研究员、管理员等6种角色)
技术选型上,后端采用SpringBoot 2.7 + MyBatis-Plus 3.5的组合,主要考虑:
- SpringBoot的自动配置特性简化了档案扫描服务的异步任务配置
- MyBatis-Plus的Lambda查询完美适配动态检索条件构建
- 内置的Actuator端点便于监控PDF转换服务的健康状态
前端选择Vue 3 + Element Plus的组合,关键考量是:
- 虚拟滚动技术应对万级列表渲染
- 自定义指令实现档案图片的懒加载
- Composition API更好地组织复杂的检索逻辑
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心架构设计
2.1 系统分层架构
采用经典的三层架构,但针对档案业务做了特殊调整:
code复制表现层
├─ Web前端(Vue 3 + Axios)
└─ 移动端H5(适配微信浏览器)
业务层
├─ 档案服务(核心业务逻辑)
├─ 检索服务(Elasticsearch集成)
└─ 文件服务(MinIO存储)
数据层
├─ 主库(MySQL 8.0事务型数据)
├─ 从库(历史数据归档)
└─ 缓存(Redis 6.2)
特别设计了"冷热数据分离"策略:
- 热数据:近5年档案存放主库
- 冷数据:5年前数据自动迁移到从库
通过Spring的@Transactional注解保证数据迁移时的事务一致性
2.2 数据库关键设计
MySQL表结构设计要点:
sql复制-- 档案主表
CREATE TABLE `archive` (
`id` BIGINT UNSIGNED PRIMARY KEY,
`title` VARCHAR(100) COLLATE utf8mb4_bin NOT NULL,
`archive_no` VARCHAR(30) UNIQUE COMMENT '档案编号',
`category_id` INT NOT NULL,
`storage_path` VARCHAR(255) NOT NULL COMMENT 'MinIO路径',
`digest` TEXT COMMENT '档案摘要',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
FULLTEXT INDEX `ft_idx` (`title`, `digest`) WITH PARSER ngram
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 档案分类表
CREATE TABLE `category` (
`id` INT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL,
`parent_id` INT DEFAULT 0,
`level` TINYINT DEFAULT 1 COMMENT '分类层级'
);
特别注意:
- 使用utf8mb4_bin校对规则确保档案编号精确匹配
- 采用自增分片ID避免主键冲突
- 添加ngram全文索引支持中文检索
3. 核心功能实现
3.1 多条件检索接口
后端采用MyBatis-Plus的动态SQL构建:
java复制public Page<ArchiveVO> queryArchives(ArchiveQueryDTO dto) {
return lambdaQuery()
.eq(dto.getCategoryId() != null, Archive::getCategoryId, dto.getCategoryId())
.ge(dto.getStartTime() != null, Archive::getCreateTime, dto.getStartTime())
.le(dto.getEndTime() != null, Archive::getCreateTime, dto.getEndTime())
.and(StringUtils.isNotBlank(dto.getKeyword()),
wrapper -> wrapper.like(Archive::getTitle, dto.getKeyword())
.or()
.like(Archive::getDigest, dto.getKeyword()))
.page(dto.toPage())
.convert(this::toVO);
}
前端配合使用Vue的Composition API:
javascript复制const search = reactive({
keyword: '',
categoryId: null,
dateRange: []
})
const { data, pending, execute } = useAsyncData(
'archives',
() => $fetch('/api/archives', {
params: {
keyword: search.keyword,
categoryId: search.categoryId,
startTime: search.dateRange[0],
endTime: search.dateRange[1]
}
}),
{
watch: [search]
}
)
3.2 高精度图像查看器
基于Viewer.js封装Vue组件:
vue复制<template>
<div class="viewer-container">
<img
v-for="img in images"
:key="img.id"
:src="getThumbnail(img.path)"
:data-original="getOriginal(img.path)"
class="archive-image"
/>
</div>
</template>
<script setup>
import Viewer from 'viewerjs'
import 'viewerjs/dist/viewer.css'
const props = defineProps({
images: Array
})
let viewer = null
onMounted(() => {
viewer = new Viewer(document.querySelector('.viewer-container'), {
toolbar: {
zoomIn: 1,
zoomOut: 1,
rotateLeft: 1,
rotateRight: 1,
flipHorizontal: 1,
flipVertical: 1,
},
viewed() {
viewer.zoomTo(1)
}
})
})
</script>
4. 安全与性能优化
4.1 安全防护措施
- 文件上传安全:
java复制// 校验文件类型
String ext = FilenameUtils.getExtension(file.getOriginalFilename());
if (!ALLOWED_EXTENSIONS.contains(ext.toLowerCase())) {
throw new IllegalFileTypeException();
}
// 校验文件内容
byte[] magic = new byte[4];
file.getInputStream().read(magic);
if (!isValidFileHeader(magic)) {
throw new FileContentException();
}
- SQL注入防护:
- 强制使用MyBatis-Plus的LambdaQueryWrapper
- 禁用${}拼接SQL
- 定期执行SQL注入测试
4.2 性能优化实践
- 缓存策略:
java复制@Cacheable(value = "archive", key = "#id", unless = "#result == null")
public ArchiveVO getById(Long id) {
return baseMapper.selectById(id);
}
@CacheEvict(value = "archive", key = "#id")
public void updateArchive(ArchiveUpdateDTO dto) {
// 更新逻辑
}
- 前端性能优化:
- 使用Vue的keep-alive缓存路由组件
- 图片懒加载指令
- 虚拟滚动列表(vue-virtual-scroller)
5. 部署与监控
5.1 Docker Compose部署
yaml复制version: '3.8'
services:
app:
image: archive-system:1.0
ports:
- "8080:8080"
depends_on:
- redis
- mysql
environment:
- SPRING_PROFILES_ACTIVE=prod
mysql:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=arch123
- MYSQL_DATABASE=archive_db
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
5.2 Prometheus监控配置
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: archive-system
6. 开发经验与踩坑记录
- MyBatis-Plus分页插件冲突:
- 现象:自定义分页与MP分页同时生效导致页码错误
- 解决:在配置类中明确声明分页插件bean
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
- Vue路由懒加载白屏:
- 现象:生产环境路由懒加载偶现白屏
- 根因:Webpack分包策略问题
- 解决:调整chunk分割配置
javascript复制configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 10000,
maxSize: 250000
}
}
}
- 大文件上传中断:
- 方案:采用分片上传+断点续传
- 前端使用spark-md5计算文件指纹
- 后端通过Redis记录上传进度
这个项目让我深刻体会到,历史档案数字化不仅是技术实现,更需要考虑人文关怀。比如我们在设计检索系统时,特意为老年研究者保留了放大镜图标的手动检索入口,而不是完全依赖智能搜索。技术应该服务于人,而不是让人适应技术。
