每年到了毕设季,总有一批计算机专业的同学对着选题列表发愁,尤其在"web方向"和"Java方向"之间反复横跳。今天我想认真聊聊一个非常适合练手、又容易出效果的题目:基于web的艺术展览网站设计与实现。这类题目在很多学校的计算机毕设选题清单里都出现过,编号17261这个版本是比较典型的规格——面向B端管理员和C端普通用户的双端系统,核心业务包括展览信息展示、作品管理、用户注册登录、在线预约或购票、后台数据维护等。选它最大的好处是:业务逻辑清楚、技术栈常规、可视化效果好,不管你是准备走前后端分离,还是传统JavaWeb单页应用,都能塞进去。关键是你做完之后,不管用来答辩,还是写进简历里,都能讲出完整故事。
这篇文章不会跟你扯虚的,我直接以这个"艺术展览网站"为题,把整个毕设从需求分析、技术选型、数据库设计、前后端实现,到打包部署、答辩准备,完整撸一遍。文章里所有设计思路、表结构、代码片段都是实践后整理出来的,你可以直接拿过去改改就用,也能帮你避掉大部分新人常踩的坑。
1. 为什么选这个题目:选题价值与定位分析
1.1 艺术展览网站的行业场景
先搞清楚一个根本问题:艺术展览网站到底是个什么东西,它和普通的电商网站、企业官网、博客系统有什么本质区别?
艺术展览网站的核心业务是"展",而不是"卖"。它服务的对象是美术馆、画廊、艺术策展方,主要职能是让观众在线上就能了解到正在展出的艺术展览、欣赏展品图片、查看展品介绍、了解艺术家背景,甚至完成在线预约和票务预订。这和电商那种"加购物车-下单-支付-发货"的强交易流程不一样,它更侧重于信息展示的视觉表现、内容的分类组织、用户浏览体验的沉浸感。
这个场景放在毕设里是非常合适的。第一,需求足够明确,业务边界清楚,答辩时你说得清自己做了什么。第二,它比"图书管理系统"这类老掉牙题目更有视觉表现力,前端能做的东西多,用Vue或原生CSS都能做出漂亮页面,评委看着舒服。第三,它天然带"在线预约"这类交互动作,能引出用户表、订单表、状态流转、时间冲突检测等全套逻辑,技术上不会显得太单薄。
1.2 这个题目对毕设的适配度
很多同学选题目喜欢选那种"看起来很厉害"的:人脸识别、推荐算法、智能客服。但说实话,毕设的评分逻辑不是"技术越新分越高",而是"功能和完整性匹配度"。我见过太多人写推荐系统,结果推荐逻辑就是个简单的按点击量排序,答辩时被老师一追问就露馅了。
艺术展览网站的优势恰恰在于"对味":它难度适中,往上可以做复杂的权限控制、数据可视化大屏、支付流程对接,往下可以做成课程设计级别的CRUD,伸缩空间极大。你完全可以根据自己的能力定目标——拿个中等成绩,做基础版就能过关;想冲优秀,再加几个亮点模块也不难。
从就业角度说也一样,现在Java后端、Web前端、全栈开发仍是市场需求量最大的岗位,这类题做完,你简历上能写的东西是实实在在的,不像某些偏科研的题目,做完都不知道怎么向面试官表达。
1.3 技术难度评估与目标设定
如果你基本功一般,建议不要一上来就搞微服务、分布式,那是给自己挖坑。把目标定在"前后端分离 + 单机部署 + 3个核心业务闭环"就完全够用了。
我给你拆个节奏参考:
- 前端:Vue 2或Vue 3 + Element UI + Axios + ECharts(可选)
- 后端:Spring Boot 2.7.x + MyBatis-Plus + MySQL 8.0
- 部署:本地开发测试 + 云服务器部署(阿里云/腾讯云学生机即可)
- 业务闭环1:用户注册登录 + 个人信息管理
- 业务闭环2:展览列表浏览 + 作品详情 + 在线预约
- 业务闭环3:管理员登录 + 展览/作品/预约/用户管理
这么一套下来,代码量不大,但五脏俱全,你从需求分析到最终答辩,其实是有一条非常清晰的叙事线的。
2. 技术选型与整体架构设计
2.1 各技术栈怎么选才不踩坑
先说你最常见的纠结:用原生Servlet + JSP,还是用Spring Boot + 模板引擎,还是前后端分离?
我的建议非常明确:用Spring Boot做后端API,前端选Vue做单页应用。理由不是追求新潮,而是这套方案在2024年的今天,学习资料最多、出错率最低、就业价值最高。
如果你用Servlet + JSP,短期内写起来可能简单,但后期维护很痛苦,而且答辩时老师大概率会问"为什么不用更现代的框架",你不好答。也用不着担心Spring Boot难,它已经把大量配置都封装好了,说白了就是约定大于配置,你只需要关注业务代码本身。MyBatis-Plus更是帮你把单表CRUD都省了,直接继承BaseMapper就能拥有基础方法,这对赶毕设简直是救命级福利。
前端选Vue的原因也很实在:它对新手友好,模板语法直观,文档中文版本成熟,社区里随便一搜就是成型的例子。Element UI(或新版Element Plus)组件库里的轮播、卡片、分页、表格、表单组件,正好覆盖这个项目的界面需求,不用你自己从零雕CSS细节。
2.2 前后端分离的目录结构规划
这里贴一个我实际用下来非常顺手的项目结构,你可以直接照着建:
art-exhibition-backend (后端Spring Boot项目) ├── src/main/java/com/example/artexhibition │ ├── controller # 控制层:接收前端请求 │ │ ├── AdminController │ │ ├── ExhibitionController │ │ ├── ArtworkController │ │ ├── OrderController │ │ └── UserController │ ├── service # 业务层:核心逻辑 │ ├── mapper # MyBatis-Plus Mapper接口 │ ├── entity # 实体类 │ ├── config # 配置类:跨域、拦截器 │ ├── common # 统一返回结果、异常处理 │ └── utils # JWT、文件上传等工具 ├── src/main/resources │ ├── mapper # XML文件(复杂SQL时可放这里) │ └── application.yml └── pom.xml art-exhibition-frontend (前端Vue项目) ├── public/ ├── src/ │ ├── api/ # Axios接口封装 │ ├── assets/ # 静态资源 │ ├── components/ # 通用组件 │ ├── router/ # 路由配置 │ ├── store/ # Pinia/Vuex状态管理 │ ├── views/ # 页面组件 │ │ ├── admin/ # 后台管理页面 │ │ └── user/ # 前台用户浏览页面 │ ├── App.vue │ └── main.js └── package.json这个结构把前后端完全拆开,开发时可以并行推进,部署时前端Build出来放到Nginx里,后端打成Jar包单独跑,互不干扰。
2.3 为什么建议用JWT而不是Session
老项目里做登录基本都是Session模式,服务端存Session,浏览器带Cookie。但在前后端分离的场景下,CSRF问题、跨域Cookie携带问题都够你喝一壶的。
建议直接用JWT(JSON Web Token)。用户在登录接口输入账号密码,后端验证通过后签发一个Token,前端拿到存到localStorage里,后续每次请求在请求头里带上Authorization: Bearer t。后端用一个拦截器统一校验Token是否有效,有效就放行,无效就返回401。
这样做的好处有三个:无状态,不占服务端内存;天然支持跨域;前后端分离架构下非常自然。网上JWT的封装代码一大把,你花半天时间理解一下核心原理就能用起来。
3. 数据库设计与数据模型
3.1 核心数据表的设计思路
艺术展览网站的数据库设计是整个项目的基石。我见过太多同学上来就建表,结果做到后面发现少字段、关系混乱,返工改SQL改到自闭。我建议你按"用户-内容-交易"三线来梳理:
用户线:sys_user(用户表)、role字段区分管理员和普通用户。
内容线:exhibition(展览表)、artwork(展品表)、artist(艺术家表)。注意这里的关联关系:一个展览可以有多件展品,一件展品通常属于一个展览(也可以做多对多,但毕设建议用一对多简化);展览和艺术家也是多对一。
交易线:appointment/order(预约订单表)、comment(评论表)、collection(收藏表)。预约单是系统的核心动作,要记录预约用户、对应展览、预约日期、状态(待审核/已确认/已取消/已完成)。
3.2 关键字段说明和建表SQL
下面这份SQL是我精简过的核心表结构,你可以直接用,注意官方推荐的命名习惯:表名小写加下划线,字段没有前缀。
-- 用户表 CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密后存储)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `phone` varchar(20) DEFAULT NULL, `email` varchar(50) DEFAULT NULL, `role` varchar(20) DEFAULT 'USER' COMMENT 'USER/ADMIN', `status` tinyint(1) DEFAULT 1 COMMENT '0禁用 1启用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 展览表 CREATE TABLE `exhibition` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '展览标题', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图', `description` text COMMENT '展览简介', `content` longtext COMMENT '展览详情(富文本)', `start_date` date NOT NULL COMMENT '开始日期', `end_date` date NOT NULL COMMENT '结束日期', `location` varchar(100) DEFAULT NULL COMMENT '展馆位置', `ticket_price` decimal(10,2) DEFAULT 0.00 COMMENT '票价', `status` tinyint(1) DEFAULT 1 COMMENT '1上架 0下架', `view_count` int(11) DEFAULT 0 COMMENT '浏览量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 展品表 CREATE TABLE `artwork` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `exhibition_id` bigint(20) NOT NULL COMMENT '所属展览ID', `title` varchar(100) NOT NULL, `artist` varchar(50) DEFAULT NULL COMMENT '作者', `image` varchar(255) DEFAULT NULL COMMENT '作品图片', `description` text COMMENT '作品介绍', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约/订单表 CREATE TABLE `appointment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL COMMENT '预约用户', `exhibition_id` bigint(20) NOT NULL COMMENT '预约展览', `appointment_date` date NOT NULL COMMENT '预约参观日期', `quantity` int(11) DEFAULT 1 COMMENT '预约人数', `total_price` decimal(10,2) DEFAULT 0.00, `status` varchar(20) DEFAULT 'PENDING' COMMENT 'PENDING/CONFIRMED/CANCELLED/COMPLETED', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;另外建议再加两张表:news(展览资讯公告)、banner(首页轮播图配置)。这两块虽然不是核心,但加上它们,前台内容会更丰富,后台管理也更多了一层可操作性,答辩时也有的讲。
3.3 表关系梳理与冗余字段的技巧
在表设计上有一个很实用的经验:不要过度追求范式化,该冗余的字段直接冗余。
举个例子,appointment表里我建议直接存一个exhibition_title快照字段。为什么?因为如果展览下架了或者被管理员改名了,历史订单仍然应该显示出用户当时预约的展览名称。你要是用联表去查,遇到删除场景就会显示空数据。虽然从第三范式角度看这是冗余,但对这个业务来说是合理的设计。
再比如artwork表中有exhibition_id,你在展示展品详情时需要显示展览名称,直接通过联表查询没问题,但如果你做了展品归档、展览删除的软删除功能,也是冗余一个展览名称字段更保险。记住一句话:毕设项目的数据量级根本不需要考虑存储成本,多放语义明确的字段比精打细算少存几个字节要实用得多。
4. 核心功能模块拆解与实现
4.1 用户注册登录:JWT鉴权与密码加密
用户模块是几乎所有系统的入口。我在毕设里见过太多次明文密码,这是一个非常不好的习惯,答辩老师一看密码字段存的是明文,印象分直接砍半。直接用Spring Security的BCryptPasswordEncoder加密,用法非常简单:
// 注册时加密 String encodedPassword = new BCryptPasswordEncoder().encode(rawPassword); user.setPassword(encodedPassword); // 登录时校验 boolean matches = new BCryptPasswordEncoder().matches(rawPassword, user.getPassword());这个加密方式的特性是"同一密码每次加密结果不同",所以你不能用普通的等值比较,必须用matches方法。
登录成功后生成JWT:payload里放userId、username、role,设置过期时间。这里有个细节,Token的过期时间建议24小时,不要设得过长,否则安全性差;也不要太短,否则用户频繁掉线。生成逻辑网上有现成工具类(用io.jsonwebtoken的jjwt依赖),抽出JwtUtil后,在拦截器里统一校验。你还需要处理一个常见问题:跨域时前端要能在响应头里读到Authorization字段,所以需要在CORS配置里显式暴露这个请求头。
前端拿到Token存localStorage,路由守卫里判断有没有Token,没有就跳登录页。这个流程是一个标准得不能再标准的套路,前后端各半小时就能搞定。
4.2 展览与作品展示:列表分页和详情
前台的展览列表是整个网站的门面,承载了用户的第一印象。这里有两个关键点:封面图质量和分页性能。
封面图这块,前端展示时建议统一压缩为适合列表展示的尺寸。你可以在上传图片时后端就做一次压缩处理(用Thumbnailator或者直接让前端上传前用canvas压缩),也可以只做展示层的缩放,但要注意大图太多时加载会很慢。图片渲染上,如果是用Vue,最好配合懒加载指令(Element UI自带的v-lazy),滚动到可视区域再加载,体验会明显好很多。
分页查询用MyBatis-Plus的selectPage就能解决:
Page<Exhibition> page = new Page<>(current, size); LambdaQueryWrapper<Exhibition> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Exhibition::getStatus, 1) .orderByDesc(Exhibition::getCreateTime); exhibitionMapper.selectPage(page, wrapper);在展览详情页,除了基础的标题、时间、地点、票价,一定要展示三个东西:策展前言/简介、展品列表、预约入口。展品列表用横向滚动卡片或者瀑布流都行,前端好看不难,但注意数据量一旦大起来,图片懒加载是必须的。
4.3 在线预约与订单状态流转
预约是整个系统业务闭环的核心动作。用户选择展览、选日期、填人数,提交后生成一条预约订单。这里要处理好三个问题:
- 订单号生成:不能简单的用自增ID,建议用时间戳加随机数,例如
yyyyMMddHHmmss + 6位随机数字,保证唯一。 - 日期校验:预约日期必须落在展览的
start_date和end_date之间,这个校验后端一定要做,前端校验可以绕过。 - 重复预约:一个展览同一用户能不能重复预约?如果你允许,那要放开;如果不允许,需要在数据库加唯一约束或者在查询时校验。建议设计成"可重复预约但状态互不影响",开发起来简单,答辩也好解释。
状态机设计上,PENDING(待审核)-CONFIRMED(已确认)-COMPLETED(已完成),中间可以CANCELLED(已取消)。管理员在后台可以完成"确认"操作,用户在前台可以完成"取消"操作,系统定时任务可以把参观日期已过的订单自动置为COMPLETED。这一步如果愿意做,可以顺手加一个Spring的@Scheduled定时任务,也是加分项。
4.4 后台管理模块:管理员的一站式控制台
后台是给策展方/管理员用的,功能上围绕"内容运营"展开。我建议拆成五个页面:
- 数据概览:展示展览总数、展品总数、用户总数、今日预约数,用卡片陈列,选上ECharts画个一个预约趋势折线图,效果拉满。
- 展览管理:CRUD + 上下架操作 + 图片上传。
- 展品管理:按展览筛选,CRUD。
- 预约管理:列表 + 状态变更(确认/取消)+ 按日期筛选。
- 用户管理:列表 + 启用/禁用。
后台权限控制这块,可以用一个简单方案:管理员登录后多存一个role在JWT里,前端根据role决定路由是否可见;后端接口加拦截器校验,只有ADMIN角色能访问/admin/**路径。不需要上Spring Security的注解权限,否则学习成本会变大。
还有一个细节:文件上传。图片上传是后台模块绕不开的功能。前端用饿了么组件库的Upload组件,后端实现一个通用的文件上传接口,把文件保存到服务器某个目录,返回可访问的URL。要注意三点:限制文件大小(比如单个不超过5MB)、限制图片类型(jpg/png/webp)、给文件重命名(用UUID,避免中文名乱码和文件名冲突)。
5. 前端展示设计与交互体验
5.1 艺术类网站的前端设计原则
艺术展览网站的前端和其他系统的最大不同在于:它强调"氛围感"。你不能把一个美术馆网站做得像后台管理系统一样全是表格和表单。
具体操作上有几个方向,成本低效果好:
第一,主色调走极简路线。大面积留白、黑白灰打底、用一个点缀色(比如赭石色、藏蓝色)。避免用大红大绿,艺术类用户群体偏好克制的高级感。
第二,字体讲究一点。标题用衬线字体(比如宋体、思源宋体),正文用无衬线(思源黑体、系统默认就行)。不用额外引入太多字体文件,用font-family栈指定就行。
第三,图片是主角。让封面图有足够大的展示尺寸,不要用半屏幕的banner,直接把展品图放大居中,配合少量文字说明,这个页面就有美术馆里看展的味道了。
5.2 首页结构:从引导到转化的布局逻辑
首页一般结构我用过很多版本,最优的是这个顺序:
首屏轮播大图(展示近期最重要的展览)→ 正在热展(卡片式展览列表)→ 展品精选(瀑布流)→ 展馆资讯(图文列表)→ 底部关于/联系方式。
首屏轮播是颜值担当,建议放3张宽幅海报,配合大标题和"立即预约"按钮。热展列表展示4到6个展览卡片,每个卡片放封面图加展览时间地点。展品精选要打破网格感,用瀑布流会更有艺术感。整个首页的目标是"引导用户点击进入详情页完成预约",所以每个卡片都可以加一个"查看详情"的悬停效果。
前端布局用Element的el-row和el-col栅格系统,响应式适配PC和移动端。这里有个容易踩得坑:el-col的span值总和必须等于24,否则布局错乱。做移动端适配时注意用xs、sm、md等断点属性来区分不同屏幕尺寸下的列宽。
5.3 图片性能优化:压缩、懒加载与CDN
艺术展览网站是图片密集型站点,不处理图片性能,页面加载速度和用户体验都会翻车。
实操建议三条:
- 上传时压缩。后端用
Thumbnailator库,一行代码就能生成缩略图和原图两种尺寸:Thumbnails.of(inputStream).size(800, 600).toFile(...)。 - 展示时懒加载。Vue项目配合Element的
lazy属性,或者在自定义图片组件里用IntersectionObserver实现滚动懒加载。 - 部署时上CDN(可选)。如果你有云服务器,图片走阿里云OSS + CDN是最好的,但没有的话用Nginx直接托管图片也有不错的效果。关键点是设置缓存头(
Cache-Control),让浏览器对图片做强缓存。
这些优化点随便挑一个写进论文或答辩PPT里,都能体现你真的在考虑系统的实际问题,而不是只写了增删改查。
6. 完整开发流程与实操记录
6.1 从零开始的第一步:先搭后端还是先搭前端
我的经验是:先搭后端,再搭前端,最后联调。因为前端页面需要数据才能跑起来,如果完全没有后端接口,你只能写死Mock数据,浪费时间。
后端搭建流程大概是这样:
- 用Spring Initializr创建项目,勾选Web、MySQL、MyBatis依赖。
- 配置
application.yml,配好数据源和MyBatis-Plus的日志输出。 - 编写统一的返回结果类
Result<T>(code、message、data),定义成功失败方法。 - 实现异常处理器
@RestControllerAdvice,让所有异常都能返回统一格式,避免一堆堆栈抛给前端。 - 集成MyBatis-Plus,写User、Exhibition等实体类,继承
BaseMapper。 - 写Controller和Service,一个接口一个接口测,用Postman验证通过后再继续下一个。
- 最后加JWT拦截器和跨域配置。
这个顺序能保证你每一步都有可验证的结果,而不是咔咔写完一堆代码,一启动报500都不知道错在哪。
6.2 前端核心代码实现:接口封装与登录流程
前端接口封装强烈建议做一层Axios拦截器,统一处理Token携带和错误提示:
// request.js import axios from 'axios'; import { ElMessage } from 'element-ui'; import router from '@/router'; const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api', timeout: 10000 }); // 请求拦截器:自动携带Token service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }); // 响应拦截器:统一处理错误 service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); ElMessage.warning('登录已过期,请重新登录'); } else { ElMessage.error('网络异常,请稍后重试'); } return Promise.reject(error); } ); export default service;登录Page里调用登录接口,成功后把Token和用户信息存到localStorage和Vuex/Pinia里,然后跳转到首页。这个地方有个小细节:调用获取用户信息的接口时,如果表单已经部分填写,直接刷新页面会导致状态丢失,建议用Pinia的持久化插件或手动同步到sessionStorage。
6.3 本地联调的注意事项
前后端分离开发时,最大的敌人是跨域和端口冲突。
后端配置里,前端开发服务器是localhost:5173(Vite默认端口),后端是localhost:8080,两者不同源,所以必须在后端加跨域配置。最省事的办法是用@CrossOrigin注解加在Controller类上,或者在配置类里实现WebMvcConfigurer的addCorsMappings方法,统一配置允许的来源、方法、请求头。
还有一个高频问题:前端请求带了Token,但后端拦截器在preHandle方法里校验Token时报"token为空"。这个多半是跨域预检请求(OPTIONS请求)导致的——浏览器在发起真正的请求前会先发一个OPTIONS请求,这个请求不会带上业务Header,所以拦截器必须要放行OPTIONS请求,否则前端会报CORS错误。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } // ... 校验Token逻辑 }6.4 部署上线:从开发环境到云服务器的完整过程
本地开发差不多了,建议直接把项目部署到云服务器上。很多同学毕设答辩时还打开着localhost演示,一旦现场网络或本机环境出问题,场面非常尴尬。花钱租一台最便宜的云服务器(学生机一年也就几十块),把系统跑在公网上,答辩时用手机热点访问都行,这本身就是加分项。
部署流程我列一下:
- 在服务器上安装JDK 8、MySQL 8.0、Nginx。
- 把本地数据库导出SQL,在服务器上导入。
- 后端项目执行
mvn clean package -DskipTests打包成Jar,用nohup java -jar后台运行。 - 前端项目执行
npm run build,生成dist目录,配置Nginx静态站点指向dist。 - Nginx配置反向代理:把所有
/api开头的请求转发到后端的8080端口。
Nginx配置示例:
server { listen 80; server_name yourdomain.com; root /opt/art-exhibition/dist; index index.html; location / { 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; } }这一步做完整,你的毕设就有"可交付"的完整度了。注意部署后一定要用公网访问一遍所有核心流程,浏览器、手机各测一遍,别等到答辩前夜才发现图片路径写死了localhost。
7. 常见问题与排查技巧实录
7.1 典型报错与解决办法速查表
整个开发过程中,我敢说下面这些报错你至少有三分之一会碰到,提前知道答案能省很多时间。
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 前端请求后端接口报CORS错误 | 后端没有配置跨域,或拦截器拦截了OPTIONS | 全局CORS配置 + 拦截器放行OPTIONS |
| 登录后访问资源返回401 | Token过期或未携带 | 检查请求拦截器Header拼接,检查JWT过期时间 |
| 中文乱码,保存到数据库变成? | 数据库连接URL没加characterEncoding=utf8 | JDBC URL加?useUnicode=true&characterEncoding=utf8 |
| 图片上传成功但访问404 | 上传目录没有配置静态资源映射 | Spring Boot里继承WebMvcConfigurer配置addResourceHandlers |
| Element Plus组件按需加载报样式缺失 | 只引入了组件但没引入样式 | 全量引入或配置unplugin-vue-components |
| 打包后刷新页面404 | Vue Router history模式需要服务端配合 | 修改路由为hash模式,或Nginx配置try_files |
| 后端启动报数据库连不上 | 数据库没创建、账号权限不对、密码错误 | 检查application.yml,确认库名、密码、端口 |
| 睡个觉回来打开项目发现页面空白 | 开发服务器被系统或代理干扰 | 先看浏览器Console报错,Vite需要重启时别犹豫 |
7.2 几个非常值得注意的隐蔽问题
这里分享几个我实操中踩过、但网上很少提到的坑。
第一个是时间格式化问题。后端返回给前端的时间字段默认是UTC格式(比如2024-05-01T04:00:00.000+00:00),前端直接展示会跟本地时间差8小时。解决办法是在application.yml里统一配置时间格式:spring.jackson.date-format: yyyy-MM-dd HH:mm:ss和spring.jackson.time-zone: GMT+8。或者你就用前端dayjs库格式化,也避免时区歧义。
第二个是富文本编辑器的XSS问题。展览详情我建议用富文本编辑器(wangEditor或Tinymce),管理员可以填图文混排内容。但是富文本里可以插入<script>标签,存在XSS跨站脚本注入风险。虽然毕设系统不面向真实生产,但答辩时如果被问到安全问题你答不上来就尴尬了。建议在后端接收入参时做一下过滤,用Jsoup库的Jsoup.clean(html, Safelist.basic())把危险的标签和属性洗掉。一行代码,说出去却有模有样。
第三个是图片上传的目录权限。如果你把上传目录放在项目的src/main/resources下,打包成Jar后,这个目录是只读的,写不进去。正确做法是把图片上传到一个独立的绝对路径下,比如/opt/art-exhibition/upload,再把它通过静态资源映射暴露出去。这个坑几乎每个做Spring Boot上传功能的新手都会踩一次。
7.3 论文与答辩准备的实战建议
最后说点跟代码无关但跟毕业直接相关的。艺术展览网站这类题目,论文里需要你重点写清楚的是:需求分析、数据库设计、系统设计、系统实现、系统测试这几个章节。
答辩时老师最常问的几个问题,提前准备好答案:
- 为什么选这个技术栈?(答:考虑毕设的可维护性 + 就业市场主流技术)
- 数据库为什么这样设计?(答:围绕核心业务闭环,兼顾查询效率和扩展性)
- Token的安全性问题?(答:设置过期时间、BCrypt加密存储、动态刷新)
- 如果用户量大了怎么优化?(答:分库分表、Redis缓存、CDN加速、消息队列——说思路即可,不用真做)
- 线上预约冲突怎么处理?(答:日期范围校验 + 数据库唯一约束 + 定时任务清理过期订单)
这些问题都不难,但你要自己能圆上,别一问就卡壳。
一个很实用的答辩技巧:准备一张系统功能结构图和一张数据库ER图,这是很多评委老师第一眼就会找的东西。做图用ProcessOn或者draw.io就行,别用Visio导出的默认样式,太丑,印象分大打折扣。
最后分享一点个人体会
我每年都会接触不少做毕设的同学,发现一个普遍现象:真正把毕设做完的人,无论题目简单还是复杂,收获都远超预期;而那些总想着"混过去"的人,连代码都不是自己写的,答辩的时候紧张得连项目里有什么模块都说不清。艺术展览网站做下来,你会把从前端到后端、从数据库设计到服务器部署这条路完整走一遍——这比看二十遍教学视频都管用。你可以把这个项目当成一个起点,答辩完再往里面加一个"虚拟展厅"(用Three.js做3D空间),或者接一个真实支付(微信/支付宝沙箱环境),整个项目的技术含量和可讲的故事就完全不一样了。做毕设这件事,没有捷径,但你认真做完一个项目,后面求职面试时你会感谢当时那个熬了几夜也要把Bug调通的自己。