SpringBoot+Vue打造线上历史馆藏系统:从数据库设计到部署全解析
不知道你有没有发现博物馆、校史馆、民俗馆这几年都在集中做一件事——把展厅里的实物搬到线上。青铜器、书画、老照片、口述史影像光靠线下展柜只能覆盖一小部分观众真正要传播和沉淀还得靠一套能检索、能浏览、能管理的线上数字馆藏系统。我最近正好完整做了一套“线上历史馆藏系统”技术栈是 SpringBoot Vue MyBatis MySQL前后端分离架构。整套源码、表结构、部署脚本都已整理完毕这篇文章我想把从设计到上线的全过程拆开来讲清楚包括关键表设计、接口权限、文件存储、视频播放、SprintBoot 和 Vue 打包后的部署细节以及我踩过的坑。不管你是要交毕业设计还是想给单位做一套数字馆藏平台这套完整源码 部署教程的思路都可以直接参考复现。1. 项目定位与技术选型为什么是这套组合1.1 线上历史馆藏系统的核心诉求先想清楚一个问题线上馆藏系统到底要解决什么拿我做这套系统时梳理的需求来说核心就三条藏品可见性把文物、史料、影像以数字化的形式展示给公众游客不用到现场也能浏览。管理效率馆方人员要能对藏品信息、分类、展陈状态、借调记录进行统一维护替代之前的 Excel 表格。可控的开放度不是所有藏品都要对游客开放部分可能涉及版权或处于修复中所以要有角色权限控制。围绕这三条需求系统很自然地划分成了两个端管理后台馆方用和门户端游客用。这也是我选择前后端分离的根本原因——两个端的界面形态、交互逻辑、更新节奏都不同强行揉在一个单体页面里后期修改成本很高。1.2 技术栈选型的取舍逻辑技术选型这块我需要给出我的真实想法而不是无脑堆新技术。SpringBoot目前做 Java 后端项目效率最高的起步框架不需要繁琐的 XML 配置内嵌 Tomcat直接打成 jar 包就能跑。我选的是 2.7.x 版本后面会专门讲为什么不要一上来就追最新版。Vue前端这块Vue 在国内的生态成熟度、中文文档质量、上手曲线都比其他框架更适合中小型团队。用 Vue Router 做路由管理Vuex 做全局登录态管理再配合 Element UI 做后台界面搭建速度非常快。MyBatis它是半自动 ORM 框架SQL 由自己控制在做复杂的多表关联查询比如“藏品 分类 借调记录 多媒体资源”这种多对多关系时比 JPA 那种全自动框架要更可控SQL 也能针对大表做手工优化。MySQL稳定、社区资料丰富、部署简单对于馆藏这种以结构化数据为主、并发量不会特别夸张的系统来说完全够用而且后续如果要接全文检索或数据分析生态也不会受限。尽量别选那种网上教程少、团队没人用过的新技术。项目的第一目标是稳定交付不是为了炫技。1.3 整体架构与目录规划项目整体结构见下。我先说目录规划这是很多初学者最容易忽略的——结构清晰后面维护和写论文/说明文档都会轻松很多。historical-collection-backend (SpringBoot) ├── src/main/java/com/museum │ ├── controller # 接口层 │ ├── service # 业务层 │ ├── mapper # MyBatis数据访问层 │ ├── entity # 实体类 │ ├── config # 配置类 │ ├── common # 通用工具/返回结果封装 │ └── security # 登录认证与权限 ├── src/main/resources │ ├── mapper # MyBatis XML文件 │ └── application.yml historical-collection-frontend (Vue) ├── src │ ├── api # axios接口封装 │ ├── views # 页面组件 │ ├── router # 路由配置 │ ├── store # vuex状态 │ └── components # 公共组件前后端分离的含义在目录上就能体现前端只关心页面渲染和用户操作后端只提供 JSON 数据和权限校验。两者通过 RESTful 接口通信互不干扰。你要改首页排版不需要动后端你要调整藏品字段校验也不需要碰前端。2. 数据库模型设计藏品、展柜与用户权限如何落表2.1 核心表结构很多项目做到一半推倒重来问题多半出在表结构没设计好。馆藏系统的核心是“藏品”但藏品的属性并不是一个简单的单表能装下的。我做表设计的时候至少运营了五个核心表外加三张中间表。藏品主表 collection_item字段类型说明idbigint 主键藏品IDcollection_novarchar(50)藏品编号唯一索引namevarchar(100)藏品名称dynastyvarchar(50)朝代/年代materialvarchar(50)材质size_descvarchar(200)尺寸描述statustinyint1-展览中 2-库房 3-修复中is_publictinyint是否对外展示create_timedatetime入馆时间分类表 category因为历史藏品必然涉及多级分类一级如“书画”“青铜”“陶瓷”二级如“书法”“绘画”我用了 parent_id 自关联这样前端可以做成树形结构。用户表 sys_user账号、密码BCrypt加密存储、手机号、角色标识。角色表 sys_role 和用户角色关联表 sys_user_role多对多关系这也是我后面做动态路由的权限依据。多媒体资源表 collection_media一条藏品可以对应多张图片、多个视频所以单独成表用 collection_id 关联。这里要特别提醒不要再把图片字段设计成 varchar 存一个路径那么简单因为实际运营中你会发现一个藏品可能有实物照片、细节图、历史照片、3D模型、语音讲解等多种格式必须一对多设计。对了一开始我把“借调记录”也塞进了主表后来发现完全不对。借调是一个频繁变更的业务动作和藏品的基本属性生命周期完全不同必须独立成表 collection_loan_record记录借入方、借出日期、预计归还日期、经办人。2.2 MyBatis 映射与关联查询的关键处理表设计好后真正的难点在 MyBatis 的关联查询上。因为你要在页面列表里显示“藏品名称 封面图 分类 状态”而数据分散在四张表。我的做法是在 collection_item 实体里加了一个冗余的categoryName字段和coverUrl字段但不建表字段只用来接收联表查询结果。在 XML 里这样写resultMap idCollectionItemVO typecom.museum.entity.CollectionItem id columnid propertyid/ result columnname propertyname/ result columncategoryName propertycategoryName/ result columncoverUrl propertycoverUrl/ /resultMap select idselectPageWithInfo resultMapCollectionItemVO SELECT ci.id, ci.name, c.name AS categoryName, (SELECT m.file_url FROM collection_media m WHERE m.collection_id ci.id AND m.is_cover 1 LIMIT 1) AS coverUrl FROM collection_item ci LEFT JOIN category c ON ci.category_id c.id where if testkeyword ! null and keyword ! AND (ci.name LIKE CONCAT(%, #{keyword}, %) OR ci.collection_no LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND ci.status #{status} /if /where ORDER BY ci.create_time DESC LIMIT #{offset}, #{pageSize} /select为什么封面图要用子查询而不是 LEFT JOIN media 表这是经验教训。一条藏品的媒体文件可能是 5 张、10 张如果直接 LEFT JOIN会查出重复行分页总数也会出错。用子查询取第一条正好避免这个问题而且 MySQL 对这种带 LIMIT 1 的关联子查询优化得还不错。MyBatis 里where标签非常值得养成习惯它能自动处理第一个条件前的 AND省去拼接 SQL 时的各种 if 判断烦恼。2.3 索引设计别偷懒我见过太多人做完表结构一行索引都不建等数据到几万条才来叫慢。收藏品系统需要考虑的索引至少包括collection_no唯一索引category_id普通索引collection_media表的collection_id索引collection_loan_record表的collection_id和loan_status组合索引MySQL 联合查询的性能瓶颈基本都在关联字段的索引上。这里不用追求过度设计覆盖最频繁的查询路径就可以。3. 后端核心实现SpringBoot 接口层与业务层的拆解3.1 Maven 工程搭建与配置细节后端我用 Maven 做依赖管理。这里有一个非常容易踩的坑——SpringBoot 版本高到离谱导致依赖冲突查半天查不出来。我最终用的是 SpringBoot 2.7.18这个版本是 2.x 的最后一个维护版本稳定性和第三方兼容性都验证得差不多了。选版本的时候可以遵循一个经验不要选刚发布的大版本如 3.x 刚出的时候要选那个大版本里的小版本号最大的。pom.xml核心依赖很简单parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies这里要多说一句mybatis-spring-boot-starter的版本不受 SpringBoot 父工程管理必须自己指定。很多人直接不写 version然后启动报一堆奇怪的 Bean 注入错误就是因为它默认拉了一个和你 SpringBoot 完全不兼容的版本。application.yml里的关键配置是这样的spring: datasource: url: jdbc:mysql://localhost:3306/museum_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.museum.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl server: port: 8080配置里三个非常容易忽略的参数useSSLfalseMySQL 8 默认走 SSL而本地测试环境根本没配 SSL 证书不关掉会连不上。serverTimezoneAsia/Shanghai不设时区查询时间字段会和本地差 8 小时。allowPublicKeyRetrievaltrueMySQL 8 使用 caching_sha2_password 认证时连接工具需要先拿到 RSA 公钥不配置这个会报Public Key Retrieval is not allowed。3.2 登录认证与权限拦截基于 JWT 的一套简洁方案这个系统有管理员、内容编辑、游客三种角色Or 准确说游客不需要登录也能看公开藏品管理端才需要登录。所以权限必须区隔开。我的方案是 JWT 拦截器。用户登录成功后后端签发一个带用户ID和角色标识的 token前端把 token 存在 localStorage 里每次 axios 请求都带上Authorization: Bearer token。后端拦截器做两件事校验 token 是否有效无效直接返回 401。读取用户角色判断接口是否允许访问。管理端接口统一放在/api/admin/**路径下游客能访问的/api/portal/**不校验登录。拦截器核心逻辑public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri request.getRequestURI(); if (uri.startsWith(/api/portal) || uri.equals(/api/auth/login)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } // 解析token校验通过则放行 try { Claims claims Jwts.parser().setSigningKey(secretKey) .parseClaimsJws(token.replace(Bearer , )).getBody(); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }要注意拦截器只是第一道防线管理端接口的 Service 层里还要再做一次角色判断防止有人绕过网关直接调用内网接口。真正的安全不是一个 token 就完事而是分层防御。3.3 文件上传用 MinIO 保存藏品多媒体文件馆藏系统最核心的资源除了结构化数据就是图片和视频。一开始我想直接把文件存在服务器的某个目录后来发现不行——前后端分离部署时后端可能会被放到一台独立服务器Nginx 静态代理未必能方便地指到同一个目录而且文件多了以后单个目录文件数量过大会影响访问速度。所以我加入了 MinIO对象存储服务。它是开源的兼容 S3 协议自己一台服务器就能跑不用依赖云厂商。SpringBoot 集成 MinIO 的关键代码// 在 pom.xml 引入 minio 依赖 dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency Configuration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } } // 上传接口 public String upload(MultipartFile file, String bucketName) throws Exception { boolean bucketExists minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if (!bucketExists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } String objectName System.currentTimeMillis() _ file.getOriginalFilename(); minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return endpoint / bucketName / objectName; }我把图片资源放到collection-imagesbucket视频放到collection-videosbucket规则清晰后续要迁移到云存储也方便因为接口就是 S3 兼容的。文件上传一定要做类型和白名单校验不能只靠前端拦截。后端要检查文件的 content-type 和大小否则有人直接拿脚本往你 MinIO 里传恶意文件图片目录就会变成文件托管站这是非常实际的安全隐患。3.4 视频播放的实现m3u8 流媒体方案线上历史馆藏系统不只是图片展示很多馆藏会有视频资料比如口述历史采访、文物修复过程、展厅全景漫游。但是直接上传 mp4 放在浏览器里播有几个问题大 mp4 文件首屏加载慢。拖进度条时需要整文件缓冲用户体验差。移动端兼容性不统一。所以视频模块我采用了 m3u8 切片方案。核心思路是把视频切成一个个很小的 ts 分片再生成一个 m3u8 索引文件前端用 HLS 协议播放。这个方案的优势是加载快、拖动进度条秒开、断点续播天然支持。我当时选了 ffmpeg 来做切片命令大概是ffmpeg -i input.mp4 -c:v h264 -c:a aac -hls_time 10 -hls_list_size 0 -f hls output.m3u8这里几个参数的意思是-hls_time 10每个分片 10 秒。-hls_list_size 0生成的 m3u8 列表保留所有分片不删除旧文件这个必须加否则播放到一半请求不到旧分片。-c:v h264用 H.264 编码兼容性最好浏览器不需要额外装解码器。前端 Vue 播放 m3u8我一开始试过video.js配置略重后来改用hls.js这个轻量方案几行代码就能跑import Hls from hls.js; if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(videoUrl); hls.attachMedia(videoElement); } else { // 兜底方案用原生 video 播放但兼容性会差一些 videoElement.src videoUrl; }如果你想让编码杂一些的视频也能播放建议用 H.264 AAC 这个老少通吃的组合。4. 前端实现Vue 动态路由与组件化开发4.1 环境搭建与工程初始化前端这块我使用 Vue CLI 建的工程。之所以用 Vue CLI 而不是 Vite理由可能有点“土”但很实际——我当时用 Vite 搭过一次构建确实快但某些旧依赖在 Vite 下会报兼容性警告折腾成本高于收益。稳定的项目工具链成熟比速度快更重要。Node 版本建议 16.x 以上npm 装依赖容易出各种问题惯用的解决办法是先清理缓存再装npm cache clean --force npm install如果遇到 node-sass 装不上的情况稳妥的办法是用 sass 替换或者干脆换dart-sass。这个项目我直接用了 sass 的现代编译版本避免一路踩原生模块编译的坑。4.2 动态路由与权限控制管理端不同角色看到的菜单和页面不一样。比如内容编辑只能进入藏品管理界面而系统管理员还能看到用户管理界面。这种需求如果只靠前端硬编码路由加一个角色就要改一次代码太笨了。我采用“后端返回路由表前端动态添加”的方案。用户登录后后端根据其角色返回允许访问的路由配置[ { path: /admin/collections, component: collection/index, meta: { title: 藏品管理, roles: [admin, editor] } }, { path: /admin/users, component: system/user, meta: { title: 用户管理, roles: [admin] } } ]前端拿到这份配置后用router.addRoute动态注册const buildRoutes (menus) { const compMap { collection/index: () import(/views/admin/collection/index.vue), system/user: () import(/views/admin/system/user.vue) }; return menus.filter(item item.meta.roles.includes(userStore.role)) .map(item ({ path: item.path, component: compMap[item.component], meta: item.meta })); }; const routes buildRoutes(menuData); routes.forEach(route router.addRoute(admin, route));有个细节值得注意component字段在前端匹配组件时我使用的是import()懒加载而不是直接写死组件引用。这样可以保证“该用户访问不到的路由在打包产物里也不包含对应代码”从代码层面也隔离了权限而不是仅仅隐藏菜单。另外每次刷新页面后状态会丢失所以刷新时要做一次 token 校验和路由重新加载否则刷新后就 404 或者跳回登录页。4.3 藏品展示页面的实现门户端的藏品展示页是用户最直接接触的部分我在设计上走了“卡片瀑布 分类筛选 搜索”的路线。列表接口的分页查询我用的是自定义的分页参数没有引入 PageHelper。原因有两个一是这个系统分页查询逻辑并不复杂手写 LIMIT 更可控二是 PageHelper 的拦截器偶尔会碰到多数据源 SQL 解析出错的问题引入它徒增复杂度。前端核心交互代码// 列表请求参数 const queryParams { pageNum: 1, pageSize: 12, categoryId: null, keyword: }; const fetchList async () { const res await getCollectionList(queryParams); collectionList.value res.data.records; total.value res.data.total; }; // 搜索框防抖处理 const debouncedSearch debounce(() { queryParams.pageNum 1; fetchList(); }, 300); // 简单防抖函数 const debounce (fn, delay) { let timer null; return function(...args) { if (timer) clearTimeout(timer); timer setTimeout(() fn.apply(this, args), delay); }; };搜索输入做防抖的目的很简单——用户每敲一个字就请求一次接口用户体验反而差。300 毫秒的防抖既能保证实时性又不会把后端打崩。藏品详情页我做了“左右结构”左边是图片轮播和视频播放区右边是藏品的详细信息列表编号、朝代、材质、尺寸、描述。这种布局很常规但非常贴合博物馆类网站的浏览习惯。4.4 关于 Vue 显示 PDF 的一个处理收藏品系统中经常有“藏品档案 PDF”需要在线预览比如文物修复报告、鉴定证书。Vue 里直接展示 PDF我的经验是浏览器原生支持 PDF 预览的直接把 PDF 地址塞进iframe就行Chrome、Edge 都能显示。如果想要完整的播放控制、缩放、下载限制可以用pdf.js。注意跨域问题。如果 PDF 放在 MinIO 里而 MinIO 和前端域名不一致需要给 MinIO 配置跨域规则。我最终的做法是给 MinIO 配置了 CORS 规则让/documents/**路径允许跨域访问前端用iframe嵌入 PDF 地址效果很稳定。5. 部署全流程从本地到云服务器5.1 服务器环境准备部署这块我的建议是不要在一开始就追求 K8s、Docker Compose 那套先把单机部署跑通后面再容器化不迟。服务器环境需要准备的东西JDK 1.8 或 11Maven 3.6Node.js 16NginxMySQL 8.0MinIO数据库部署我用的是 rpm 方式安装的 MySQL 8.0。这里有一个易错点MySQL 8 安装完默认的 root 密码不在日志里而是在/var/log/mysqld.log里需要grep temporary password /var/log/mysqld.log然后登录修改密码ALTER USER rootlocalhost IDENTIFIED BY YourNewPassword;注意 MySQL 8 默认密码策略要求至少 8 位且包含大小写字母和特殊字符如果只是个人项目想设简单密码需要先降低策略SET GLOBAL validate_password_policy LOW;5.2 后端打包与启动后端打包之前先把application.yml里的数据库地址、MinIO 地址从本地 localhost 改成服务器实际 IP。然后使用 Maven 打包mvn clean package -DskipTests打包完成后jar 文件在target/目录下。启动命令nohup java -jar historical-collection-1.0.0.jar \ --spring.profiles.activeprod \ app.log 21 echo $!关键参数解释--spring.profiles.activeprod加载application-prod.yml的配置这样可以做到本地环境和生产环境配置隔离。nohup ... 让进程在后台运行关掉终端也不会被杀掉。 app.log 21把标准输出和错误输出都写进日志文件排查问题用。启动后验证接口curl http://localhost:8080/api/portal/collection/list如果返回 JSON 数据且没有报错后端就起来了。5.3 前端构建与静态资源托管前端的构建很简单npm run build构建产物在dist/目录。有两种部署方式我最初用方式一后来切到了方式二方式一前端产物复制到 SpringBoot 的 static 目录把dist/的内容复制到后端src/main/resources/static/下重新打包。这样做的好处是不需要单独配 Nginx一个 Java 服务跑所有。但缺点也很明显——前端改动后需要重新打包后端耦合了。方式二Nginx 托管前端反向代理后端这是标准的前后端分离部署方式。前端构建产物放在/usr/share/nginx/html/historical/Nginx 配置server { listen 80; server_name museum.example.com; root /usr/share/nginx/html/historical; index index.html; # 前端路由 history 模式必须配置 location / { try_files $uri $uri/ /index.html; } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 视频流媒体请求超时时间调长避免播放中断 location /collection/video/ { proxy_pass http://127.0.0.1:8080/collection/video/; proxy_buffering off; proxy_read_timeout 300s; } }这里try_files $uri $uri/ /index.html;这句非常关键。因为 Vue Router 用了 history 模式如果用户直接刷新/collection/detail/123这个地址Nginx 会去找这个物理路径肯定是 404。这句配置的作用就是兜底到index.html再让 Vue Router 接管路由。proxy_buffering off是我针对视频流加的一个优化关闭代理缓冲后播放器可以边下边播不会等 Nginx 缓冲完整个响应对 m3u8 分片播放很友好。5.4 把 Vue 打包文件放进 SpringBoot 的取舍热搜词里有“vue打包放进springboot中”我再补充说下这个方案。如果你不想部署 Nginx或者你的服务器只有一台且内存紧张完全可以采用方式一。实现很简单npm run build cp -r dist/* ../backend/src/main/resources/static/ cd ../backend mvn clean package -DskipTests java -jar target/app.jar这样访问http://localhost:8080/index.html就能看到前端页面。但要接受一个现实前端和后端从此绑定在一个进程里前端发版必须重新打包而且静态资源无法利用 Nginx 做缓存和压缩。所以这个方案适合个人项目、毕业设计演示或者临时环境正式运营我还是建议用方式二。6. 常见问题与排查实录6.1 MySQL 连接错误留给新手的几个经典报错这个项目里我前后遇到过几个 MySQL 连接问题都很有代表性。报错一Public Key Retrieval is not allowed解决方式在配置里已经写了allowPublicKeyRetrievaltrue必须加上。遇到这个错先不用怀疑服务器防火墙先看你的 JDBC URL 是不是少了参数。报错二The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这是中文系统下 MySQL 的时区设置问题。解决方案是在 JDBC URL 加serverTimezoneAsia/Shanghai。报错三Navicat 连接 MySQL 8 报caching_sha2_password错误MySQL 8 默认认证插件是caching_sha2_password老版本 Navicat 不支持。有两种解法ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY password;或者升级 Navicat 到新版本。但要注意这两种方案在 MySQL 8.0.34 之后mysql_native_password插件默认被禁用了需要先启用再修改不然会报Plugin mysql_native_password is disabled。6.2 SpringBoot 版本太高引发的兼容问题说句实在话一个项目里最耗时间的往往是“版本冲突”。我遇到过把 SpringBoot 升到 3.0 后MyBatis starter 直接跑不起来的情况。原因很简单SpringBoot 3 基于 Jakarta EE 9包名从javax.*改成了jakarta.*很多旧版 starter 根本没有适配。所以我的建议非常明确如果项目要稳用 SpringBoot 2.7.x。如果要上 3.x必须确认所有依赖都支持 Jakarta。另外还有一个常见的坑是spring-boot-maven-plugin的版本和 SpringBoot 版本不匹配启动时提示找不到主类。解决办法是显式指定插件版本。6.3 Vue 打包后资源路径 404npm run build后默认的资源引用路径是绝对路径/js/app.js。如果你部署在域名的子路径下比如http://ip:80/historical/那这个绝对路径会直接 404。解决方式很简单在vue.config.js里设置module.exports { publicPath: process.env.NODE_ENV production ? ./ : / };把 publicPath 改成相对路径./这样打包出来的资源引用会变成相对路径不管你把前端文件放在哪个子目录都能正常加载。如果在 Nginx 下做代理还可以配置server_name和alias的方式统一处理。6.4 MyBatis 缓存问题与排查MyBatis 的缓存一开始我差点掉进坑里。MyBatis 有一级缓存SqlSession 级别和二级缓存namespace 级别。一级缓存默认开启在一个 SqlSession 中两次相同的查询不会请求数据库。这在事务环境下没问题但如果同一个 SqlSession 跨多个请求复用就可能查到旧数据。实际上 Spring 管理的 SqlSession 每次请求都是新的所以一级缓存导致的问题很少见。二级缓存默认是关闭的。网上很多教程说“开启二级缓存提高性能”但实际上在这个馆藏系统里我建议别开。因为二级缓存生效的前提是对该表的查询和修改都走同一个 namespace一旦出现多表联查缓存的一致性就非常难保证。举个例子collection_item改了数据但查询走的是关联了collection_media的联表 SQL结果这个联表 SQL 命中的缓存没被清掉展示端就出现了脏数据。所以如果要做缓存我的建议是在 Service 层用 Redis 做显式缓存自己控制失效策略而不是依赖 MyBatis 的二级缓存。控制权在自己手里才睡得着觉。6.5 视频播放不了m3u8 常见问题速查视频这块我也踩了不少坑集中说一下表现原因解决黑屏但能拖动进度条视频编码不是 H.264用 ffmpeg 重编码为 h264播放到一半卡住Nginx 代理缓冲导致分片请求超时设置proxy_buffering off和proxy_read_timeout 300s手机浏览器无法播放部分手机浏览器不支持 hls.js 的 MSE可以引入mpegts.js或者在后端加一层转码为 mp4 的兜底接口跨域导致无法加载 m3u8MinIO 或视频服务器未配置 CORS给存储桶配置允许跨域访问的规则最让我头疼的是第二种“播放到一半卡住”。排查时用浏览器的网络面板看了半天发现是 Nginx 默认的 60 秒超时导致加载 ts 分片失败。把超时时间调大后问题立即消失。经验就是视频响应的超时时间必须单独设置不能套用普通 API 的超时配置。7. 部署上线后的下一步扩展方向这套系统做完并稳定运行后我发现要让它真正服务于“线上历史馆藏”的定位后续还可以考虑几个扩展方向也分享给大家做参考。全文检索。当藏品数据量达到几千条以后MySQL 的 LIKE 查询会越来越吃力尤其是“名称 描述 背景故事”全文检索场景。可以引入 Elasticsearch或者先用 MySQL 的全文索引过渡。藏品系统的检索体验直接决定用户留存这块值得投入。数据统计与可视化。馆方很关心“哪些藏品最受欢迎”“哪个时间段访问量最高”。可以对接一个轻量级的数据采集 eg. 访问日志写入 MySQL然后在管理后台用 ECharts 做访问趋势、分类热度、藏品浏览 Top10 的可视化看板。这个功能做出来后系统从“管理工具”变成了“决策工具”价值会再上一个台阶。多媒体文件的转码服务。目前视频切片是离线用 ffmpeg 手动处理的。如果要支持运营人员自助上传视频后自动切片需要加一个异步任务队列比如用 RabbitMQ ffmpeg 做转码任务上传完成后自动生成 m3u8。这个架构本身也不复杂值得做。权限和审计日志。博物馆行业的馆藏数据有一定敏感性管理端的每次操作修改、借调、删除都应该留痕。加一张操作日志表在 Service 层用 AOP 切面统一记录操作人、操作时间、操作内容对后续追责和合规也有帮助。我个人在实际操作中的体会是一个馆藏系统真正难的不是技术而是对业务的理解。技术痛点其实就那么几个——表设计、权限控制、文件存储、部署解决了就顺畅了。而业务上的持续优化——如何让一件文物背后的故事被更多观众看到如何让馆藏管理员使用起来更顺手——才是这个系统活下来的根本。希望大家在自己的项目里也能把这两点结合起来不要只做一个“能跑”的 demo而是做成一个“有人用”的平台。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →