SpringBoot+Vue前后端分离艺术展示平台实战:从表设计到部署上线 这个平台项目我做完有一阵子了最近整理技术笔记的时候翻出来觉得里面有不少可以复用的思路。当时是帮某艺术机构搭一套基于 SpringBoot Vue 的艺术作品展示平台需求很典型线上展厅、作品集浏览、艺术家入驻、作品上传与审核、用户点赞评论。前后端分离架构三周完成主体开发最后部署到一台云服务器。如果你也在做作品展示、文创、艺术教育类的Web平台这篇文章从需求拆解、表设计、接口规范到部署踩坑都有涉及可以直接对照着做。1. 项目定位与整体设计思路1.1 这个平台到底解决了什么问题这类展示平台的核心场景不是简单的放一堆图片而是要把艺术作品的展示流程线上化。线下办展需要场地、布展、导览成本高且辐射范围有限线上平台则要让作品能被更多人看到、能被检索、能被互动同时还要解决版权和审核问题。所以我给这个项目拆出了三个核心闭环作品生产端艺术家注册入驻、上传作品、维护个人主页。作品运营端管理员审核作品、分类管理、数据统计、推荐位配置。作品消费端访客浏览、搜索筛选、点赞收藏、评论交流。这三个闭环覆盖了内容从哪来、怎么管理、如何被消费的完整链路。很多人在做类似项目时容易犯的错是只做展示不做管理导致后台上传全靠手工改数据库这在实际运营中完全不可接受。1.2 需求分析阶段的关键取舍需求评审时和机构方来回磨了两轮最后定了几个关键决定。第一个是前台以浏览体验为优先不搞强制注册登录游客可以看作品、看详情但点赞、评论、收藏必须登录。第二个是后台上传的作品必须经过审核才能出现在前台避免出现不合规内容。第三个是移动端优先因为实际看展的人大部分用手机。这三个决定直接影响后续的表设计和接口设计。比如游客可浏览意味着列表接口不能依赖用户身份审核状态必须单独用一个字段控制展示逻辑移动端优先则意味着图片压缩和懒加载要提前规划不能等上线了再优化。1.3 为什么选 SpringBoot Vue 这对组合选型时对比过几个方案纯模板渲染比如 Thymeleaf JQuery、Node全栈、SpringBoot Vue。最后定前者是因为这个项目需要同时承担PC和移动端的访问前后端分离后一套接口可以喂两套前端后期如果做小程序也能直接复用。而且机构方后续可能要接入排期系统、库存系统SpringBoot的生态对接这些东西省事。Vue这边选了Vue 3 Vite Element Plus组件库成熟出页面速度快团队成员熟悉度高。说到底选型不是追求技术新颖而是看团队熟不熟、生态全不全、后期好不好维护。SpringBoot的约定优于配置极大减少了样板代码Vue的响应式机制做图片列表这类交互密集的页面比传统的字符串拼接舒服太多了。2. 数据库设计核心表结构与字段规划2.1 五张核心表的建模思路数据库是这个项目最值得提前花时间的部分。当时设计时围绕用户、作品、交互、审核四条线建了五张核心表。用户表做了角色区分用 role 字段控制 USER、ARTIST、ADMIN 三类身份这样避免建多张用户表导致关联复杂。艺术家信息简介、代表作链接、个人标签当时选择直接冗余在用户表的扩展字段里没有单独拆表。拆表的劣势是每次查艺术家详情都要join而且很多艺术家其实只发三五件作品拆表的收益不高。当然了如果你的平台要做复杂的艺术家认证审核流程那单独建模是必要的。作品表是核心中的核心字段设计上踩过一个坑最初把分类直接做成字符串字段后来发现统计每个分类下有多少作品这种需求时特别难受最后还是拆了分类表用外键关联。交互这块点赞和收藏当时没有分开建两张表用了同一张 user_artwork_interaction 表通过 type 字段区分。后来验证这个设计遇到了性能瓶颈但前期数据量小完全够用而且用户取消点赞、重新点赞这类操作逻辑也统一了。评论表单独建因为评论有分页加载和嵌套回复的扩展需求。2.2 作品表字段设计与状态机作品表部分字段如下CREATE TABLE artwork ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 上传者ID, title VARCHAR(100) NOT NULL COMMENT 作品标题, category_id BIGINT NOT NULL COMMENT 分类ID, style VARCHAR(50) COMMENT 艺术风格, description TEXT COMMENT 作品描述, image_url VARCHAR(255) NOT NULL COMMENT 原图地址, thumbnail_url VARCHAR(255) COMMENT 缩略图地址, width INT COMMENT 作品宽度(cm), height INT COMMENT 作品高度(cm), create_year VARCHAR(20) COMMENT 创作年份, status TINYINT DEFAULT 0 COMMENT 0待审核 1已展示 2已下架 3审核驳回, view_count INT DEFAULT 0, like_count INT DEFAULT 0, created_at DATETIME, updated_at DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status 字段是整个平台流转的核心。从上传到展示的完整链路是艺术家创建作品记录status0待审核→ 管理员在后台审核通过则 status1驳回则 status3 并填驳回原因→ 运营可手动下架status2。前台列表只查询 status1 的数据这个规则在后端接口和前端页面都要严格执行不能只在页面上过滤否则接口被直接调用时就露馅了。2.3 图片存储方案的选择图片处理是作品展示平台最重要的技术点之一。当时机构方没有对象存储的预算所以我选了本地存储 Nginx 静态映射的方案。图片上传路径按日期分目录比如/data/artwork/2025/06/每天一个文件夹避免单目录文件过多。缩略图处理用了 Java 的 Thumbnator 库上传原图后自动生成宽度800的等比缩略图。这里有几个参数值得注意缩略图质量参数设置为0.85实测体积能压缩70%以上画质肉眼几乎无差别原图保留用于详情页的放大查看。如果直接用原图做列表缩略图首页一次加载20张图每张3MB移动端直接卡死这也是很多类似项目翻车的地方。3. 后端接口设计与核心逻辑实现3.1 RESTful 接口设计与统一返回结构后端接口遵循 RESTful 规范统一以/api开头按模块划分资源路径。用户模块/api/user/**作品模块/api/artwork/**后台管理模块/api/admin/**上传模块/api/upload/**。所有接口的返回结构统一封装成 Result 对象包含 code、message、data 三个字段Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }这个统一返回结构很重要前端 Axios 拦截器只需要判断 code 是否为 200 即可异常处理都在全局异常处理器里统一转换成 Result避免前端到处 try-catch。业务异常用自定义 BusinessException 抛出参数校验异常用 Spring 的 BindException 捕获这样接口层非常干净。3.2 登录认证JWT 方案与权限拦截登录认证选了 JWTJSON Web Token方案没有引入 Spring Security 全家桶而是基于拦截器实现了一个轻量级认证框架原因在于项目权限模型比较简单用 Security 反而增加配置复杂度。具体实现分四步用户登录成功后后端生成 JWT 返回给前端Token 中只存放 userId 和 role不存敏感信息。前端将 Token 存储在 localStorage每次 Axios 请求时从拦截器里取出并放到 Authorization 请求头。后端定义一个 AuthInterceptor拦截除白名单外的所有/api/**请求解析并校验 Token将用户信息放入 ThreadLocal。需要管理员权限的接口在业务层判断 ThreadLocal 中用户角色的值不是 ADMIN 直接抛权限异常。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); // 这里实际项目应从缓存中校验 Token 是否有效 // 避免用户注销后 Token 仍能使用 if (!StringUtils.hasText(token)) { throw new BusinessException(401, 未登录); } // 解析 token将 userId 存入 UserContext UserContext.set(userId); return true; } }这里必须提醒一个坑JWT 无状态特性导致注销和封禁无法即时生效。当时做用户封禁功能时发现被封禁用户的 Token 依然能通过校验因为后台只验证了签名有效性。后来加了一层 Token 黑名单机制保存 Token 的 jti 标识注销或封禁时写入 Redis 黑名单。如果你用 JWT 且涉及用户封禁功能这个一定要提前考虑不能只靠过期时间。3.3 分页查询与筛选条件的组合处理作品列表是访问最频繁的接口我用了 MyBatis-Plus 的 LambdaQueryWrapper 做动态条件拼接支持分类、风格、创作年份和标题关键字四个筛选维度。分页参数 page 和 pageSize 做了上限控制pageSize 最大 40防止别人传个 10000 把数据库打爆。public PageResultArtworkVO listArtworks(ArtworkQuery query) { LambdaQueryWrapperArtwork wrapper Wrappers.lambdaQuery(); wrapper.eq(Artwork::getStatus, 1) // 只查已展示状态 .eq(query.getCategoryId() ! null, Artwork::getCategoryId, query.getCategoryId()) .like(StringUtils.hasText(query.getKeyword()), Artwork::getTitle, query.getKeyword()); wrapper.orderByDesc(Artwork::getCreatedAt); PageArtwork page artworkMapper.selectPage( new Page(query.getPage(), query.getPageSize()), wrapper); // 转换 VO补充缩略图完整路径、作者信息等 return convertToPageResult(page); }这里有个性能细节列表接口不要 select *只查列表所需的字段。因为作品表里 description 可能是几百上千字的大字段列表页用不到全查出来会显著增加传输体积和数据库 IO。用wrapper.select()指定查询列或者干脆建 VO 用自定义 SQL 只取需要的列。3.4 图片上传接口与文件校验上传接口是后端最容易出安全问题的地方简单说三点实践。第一文件类型校验不能只判断扩展名要读取文件头Magic Number判断真实类型防止改后缀上传脚本文件。第二文件大小限制要通过 Spring 的多文件配置和业务层双重校验单张图片我限制为5MB。第三上传后的文件名绝对不能用用户原始文件名用 UUID 时间戳重新生成防止路径穿越。spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB上传成功后将访问路径返回给前端前端拿这个路径拼到作品表单数据里一起提交这样作品创建和图片上传是分离的。如果先传图再建记录万一用户中途放弃会留下大量孤儿图片文件后续要定期清理临时目录。4. 前端核心页面与交互实现要点4.1 Vue 项目结构与路由设计前端用的 Vue 3 Composition API 写法。项目结构按模块划分不是按页面类型划分src/ ├── api/ # 接口请求封装 │ ├── artwork.js │ ├── user.js │ └── admin.js ├── router/ # 路由配置 ├── stores/ # Pinia 状态 │ ├── user.js │ └── app.js ├── views/ │ ├── home/ # 前台首页 │ ├── artwork/ # 作品列表、详情 │ ├── user/ # 个人中心、登录注册 │ ├── artist/ # 艺术家主页 │ └── admin/ # 后台管理 └── components/ # 通用组件路由加了一个全局前置守卫判断访问后台管理页面时用户是否已登录且角色为管理员否则重定向到登录页。这里有个交互细节守卫只做前端路由级拦截真正安全还是要靠后端接口权限控制前端拦截只是用户体验层面的东西不能让用户看到闪现的空白页。4.2 画廊列表页的滚动加载与性能优化作品列表页没有用传统分页组件而是用了滚动到底自动加载的模式更符合画廊浏览场景。用 VueUse 的useIntersectionObserver监听底部占位元素的可见性触发加载下一页script setup const page ref(1) const list ref([]) const loading ref(false) const finished ref(false) const loadMore async () { if (loading.value || finished.value) return loading.value true const res await getArtworkList({ page: page.value, pageSize: 20 }) list.value.push(...res.data.records) page.value if (page.value res.data.pages) finished.value true loading.value false } /script图片懒加载用了原生loadinglazy属性这是性价比最高的方案不需要额外引库。列表图片外层包裹了固定宽高比的容器用aspect-ratioCSS 属性预留空间避免滚动时页面布局反复跳动。另外一个经验桌面端网格列数用 CSS Grid 自适应但移动端强制两列展示这两列模式下缩略图尺寸控制在 400px 内移动网络环境下加载速度会快很多。4.3 作品详情页的图片放大与交互反馈详情页的核心是查看大图。原图文件往往有几MB直接铺在页面上会卡所以我用了图片分档加载策略。详情页默认展示缩略图提供一个查看原图按钮点击后再加载原图地址加载期间显示骨架屏。原图打开后支持双击或者点击左右区域放大查看细节实现方式是简单的 overlay 弹层没有引入复杂的查看器库。这里的小技巧是弹层中图片不能用width: 100%直接铺满应该按图片原始宽高等比缩放超出屏幕时允许滚动或缩放否则竖构图的画被横屏撑开后完全没法看。点赞和收藏按钮做了防重复提交处理。用户点击点赞后立即更新 UI 状态并置灰同时向接口发送请求如果请求失败再回滚 UI。这个乐观更新模式在移动端网络抖动时体验远好于等接口返回再更新。4.4 后台管理页面的表格与统计数据后台管理用的是 Element Plus 的 Table 组件配合标签页做审核流程。这里值得一提的不是表格本身而是管理员的快速审阅设计。审核列表每行支持通过/驳回两个操作驳回时弹窗填写原因前端会把原因带上前端创作者中心会展示审驳原因方便修改后重新提交。另外用 ECharts 做了三个统计图作品分类分布饼图、近30天作品上传趋势折线图、用户增长柱状图。统计接口用 SQL group by 聚合注意聚合查询一定要在数据库层完成不要在 Java 内存里循环算数据量上来之后性能差别很大。5. 测试部署与高频问题排查5.1 前后端分离部署的具体方案部署环境用的是单台云服务器4核8G配置跑了 Nginx、MySQL 8.0、后端 jar 包三个进程常规访问量完全够用。后端直接mvn package打成 jar 包用 systemd 管理进程systemctl start artwork-server就能启动。重点说 Nginx 配置两个核心点静态资源映射和前端路由 history 模式处理。server { listen 80; server_name your-domain.com; # 前端页面 location / { root /data/artwork-web/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 作品图片静态资源 location /files/ { alias /data/artwork-upload/; expires 7d; add_header Cache-Control public, max-age604800; } }try_files那行是关键没有它 Vue Router 的 history 模式在刷新非首页路径时会直接 404。部署完最好手动刷新几个深层路径测试一遍这是最常见的部署后翻车点。5.2 跨域、时间与上传问题排查实录项目开发期和上线初期遇到的高频问题整理成了一份速查表问题现象根因解决方案前端请求接口报 CORS 错误前后端分离后源不同Nginx 代理同源部署后端无需额外开启 CORS数据库时间比实际晚8小时MySQL 时区与服务器不一致JDBC URL 加serverTimezoneAsia/Shanghai上传大图后列表加载慢缩略图未生成或质量过高Thumbnator 压缩到800px宽、质量0.85刷新页面404history 路由未配置 fallbackNginxtry_files指向 index.html审核后前台看不到作品Redis/本地缓存未失效审核操作后主动清理对应缓存 keyToken 过期后接口报401白屏前端未统一处理401跳转Axios 响应拦截器捕获401自动跳登录页跨域问题这里多说一句开发环境我用了 Vite 的 proxy 配置代理/api到后端服务生产环境用 Nginx 同源转发整个项目完全没有在 Spring 里配置过 CORS 跨域。很多初学者喜欢在后端加CrossOrigin注解或者 CORS 过滤器这在生产环境配合 Nginx 部署时反而容易出问题建议从方案上消除跨域而不是解决跨域。图片上传排查遇到过一个有意思的坑某次上传的图片在浏览器里能看到但缩略图生成失败日志报NullPointerException。排查后发现是用户上传的图片文件名带中文Thumbnator 读取时编码出问题。后面统一在生成缩略图前用 UUID 重命名原文件这个隐患消除了。你如果也做文件处理强烈建议任何涉及文件名的逻辑统一用英文/数字命名。5.3 上线后的数据统计与性能观察平台上线后跑了两个月核心观察数据可以给后续做类似项目的人参考。作品列表接口单次查询耗时在 30ms-80ms 之间20条数据列表页的完整请求响应约 120ms符合预期。访问量高峰期在晚上 8 点到 10 点QPS 峰值约 404核8G的服务器 CPU 占用峰值 30%MySQL 慢查询日志中没有出现超过 500ms 的查询。这个量级下其实还没有到需要上 Redis 缓存的阶段但我当时给列表接口和首页接口加了基于 Spring Cache 的本地缓存TTL 设置 5 分钟效果显著列表接口的响应直接降到 10ms 以内。如果你的平台预期访问量更大建议把缓存替换成 Redis 并处理缓存穿透问题否则高并发下数据库压力会很明显。写在最后这个项目做下来我最大的感受是艺术品展示平台的技术难点不在高深而在细致。图片处理、状态流转、权限控制、部署路由每一个点单独看都不复杂但串起来就能看出一个项目是否真的经过实战打磨。如果你正在做或者计划做类似的项目建议先花两天时间把表结构和状态流转画清楚再动手写代码这比上来就敲 CRUD 省的时间多得多。另外分享一个个人习惯每次给这种内容展示类平台做上线前的最后检查我都会把首页、详情页、上传页面全部模拟一遍弱网环境测试Chrome 开发者工具可以限速因为艺术作品类网站的体验瓶颈几乎都出在图片加载上。这个测试动作帮我提前发现过好几次大图加载导致的页面卡死问题你也值得试一试。