☰
SpringBoot+Vue博物馆展览预约与管理系统全栈实战
2026/10/3 9:05:49 网站建设 项目流程

这两年陆陆续续帮人做过不少Web系统,最常被问到的一个需求就是:能不能做一个能线上预约、线上看展、后台又方便管理的博物馆系统。说实话,博物馆类项目有个很典型的特点——它既不是纯粹的内容管理系统,也不是标准电商,而是展览信息、票务预约、展品管理、用户服务这几块揉在一起。用SpringBoot + Vue来做,恰好能用最熟悉的技术栈把这几块撑起来。这套“博物馆展览与服务一体化系统”,前端用Vue全家桶,后端用SpringBoot + MyBatis-Plus,数据库用MySQL,核心功能覆盖展览展示、票务预约、用户中心、后台展品管理和报表统计。无论是大学生做毕业设计,还是小团队接外包项目,都可以拿它当底座直接改。

这篇内容我会尽量把项目从设计到落地讲透,包括模块怎么切、数据库怎么建、后端接口怎么写、前端页面怎么接,以及实际部署时最容易踩的坑。适合正在准备SpringBoot + Vue全栈项目的同学,也适合想快速搭一套博物馆、美术馆、文化馆业务系统的开发者参考。

1. 项目全貌:这套系统到底解决什么问题

1.1 为什么是SpringBoot + Vue这个组合

在这个项目里,选择SpringBoot + Vue并不是偶然,而是现实约束下的最优解。博物馆业务的访问量通常集中在节假日,日常维护人手又不多,因此系统必须满足三个硬条件:开发效率要快、维护成本要低、前后端要能并行干活。SpringBoot的starter机制和AutoConfiguration大幅削减了配置工作量,Vue的组件化开发又能让页面像搭积木一样拼接,两者结合正好命中这些需求。尤其在国内的中小型项目中,这套组合的人才储备也足够充足,后续无论是维护还是二次开发,找人都容易很多。

如果换作微服务架构,对一个馆内管理系统来说反而是过度设计。博物馆系统真正的复杂度不在并发,而在业务交错:展览信息与展品关联、预约记录与票务统计关联、用户操作与权限关联。单体应用配合模块化分包完全能应对,还省去了服务注册、配置中心、分布式事务等一堆和业务无关的麻烦。这也是我在很多类似项目里都坚持“单体优先”的原因。

1.2 系统整体架构与模块划分

模块划分上,我按用户侧和运营侧把系统切成两大块。用户侧包含首页轮播、展览预告、展品列表、在线预约、意见反馈、个人中心;运营侧包含管理员登录、展览管理、展品管理、预约审核、公告发布、订单统计、用户管理。前后端通过RESTful API交互,统一返回结构为code + message + data,前端根据code判断业务状态,不用关心HTTP状态码的细节。这个设计看似很小,却省掉了无数联调时的扯皮。

后端在SpringBoot框架内按照controller、service、mapper三层标准来分包,另外单拎出config、common、utils三个公共包。config放跨域配置、拦截器配置、文件上传配置;common放统一返回类、全局异常处理、自定义注解;utils放JWT工具、MD5加密工具、日期工具。这样的分包方式很常规,但它有一个实际好处:新成员接手项目时,不用问任何人就知道该往哪里放代码。我自己带过几个实习生,验证过这个结论。

2. 核心功能拆解:从观众到管理员的闭环

2.1 观众端:展览展示、预约与导览

观众端是整个系统的门面,也是用户转化的入口。首页我一般会放一个轮播图区域,展示当前重点展览;下面跟着“近期展览”列表,按开始时间和热度排序。每个展览卡片点击进去,能看到展览详情、展品列表、开放时间、预约入口。展品列表支持按年代、材质、分类筛选,这部分本质上就是一次带条件的列表查询,后端用MyBatis-Plus的LambdaQueryWrapper就能写得很干净。

在线预约是观众端最核心的业务动作。预约流程我设计成:用户选择展览、选择参观日期、填写参观人数、提交预约,系统生成预约单号。预约单号不用数据库自增ID,而是用时间戳加随机数生成,比如20240512104530001,这样做的好处是:方便客服在后台按单号检索投诉,也避免用户通过ID猜测平台单量。预约提交后,状态默认为待审核,管理员在后台确认通过或驳回,这样能防止用户恶意占坑,也方便馆方控制单日接待量。

导览服务这块,我在展品详情页加了一个“语音导览”入口,本质上是一个音频文件的播放功能。音频文件上传时我用OSS语义分目录存储,按banner、exhibition、audio、avatar四类归档,后端在返回展品信息时直接拼出完整的访问URL。前端用原生audio标签播放,不引第三方播放器,因为音频场景简单,不需要复杂解码能力。真正麻烦的是视频导览,后面我会单独讲M3U8播放的坑。

2.2 管理端:展品、票务与内容运营

管理端我按运营人员的日常动线来设计菜单顺序:仪表盘、展览管理、展品管理、预约管理、公告管理、用户管理、系统设置。仪表盘放三块统计卡片:今日预约量、本月展览数、展品总数,下面跟一个近七天的预约趋势拆线图。这个图表不需要前端手写Canvas,直接使用ECharts的折线图组件,后端提供一个聚合查询接口,按日期分组统计即可。

展览管理模块的编辑页会比较复杂,因为一张展览海报、一组展览简介、一组关联展品、一段开放时间说明,全部要在一个表单里完成。我的做法是:展览表和展品表通过exhibition_id关联,保存展览信息时先保存主表拿到ID,再批量更新展品的关联字段。这里最容易出问题的是事务边界:如果先插入展览主表成功,再更新展品关联失败,数据库里就会出现一组没有归属的展品。解决办法是在Service层加@Transactional注解,并指定rollbackFor = Exception.class,因为SpringBoot默认只在RuntimeException时回滚,如果业务方法抛出的是自定义CheckedException,不改这个参数事务是不会回滚的。

预约管理主要做两件事:审核预约和统计到馆人数。审核操作本质上是一个状态机流转:待审核→已通过或已驳回,已通过→已完成或已取消。状态字段我用varchar直接存中文,虽然不够“范式”,但胜在可读性强。运营人员在后台看到的状态和用户看到的完全一致,不需要在脑子里做字典映射。这类内部管理系统,代码可读性和运营效率比所谓的规范重要得多。

2.3 权限设计与多角色控制

权限模型我用的是RBAC,但做了一个轻量简化:角色分管理员和普通用户两种,菜单权限直接写死在前端路由里,后端负责拦截接口。管理员登录后拿到一个带role字段的JWT,前端在路由守卫里根据role判断是否允许进入管理端页面。后端在每个需要管理员权限的接口上加@RequireRole("admin")自定义注解,通过SpringMVC的HandlerInterceptor统一校验。这个方案比引入Spring Security + Sa-Token要轻很多,功能也够用。

这里有个经验值得说明:不要把前端路由当作真正的安全边界。有些开发者只在菜单上隐藏了管理入口,但接口照样能被直接访问,这等于把门锁挂在门上却不锁门闩。后端拦截必须覆盖到Controller层,否则只要有人抓到接口地址,就能绕过页面直接调数据。我在第一次交付这个项目时就犯过这个错,后来加拦截器才发现有几个管理接口完全裸奔。

3. 数据库设计:一张表一张表地抠

3.1 核心表结构说明

数据库我用MySQL 5.7,字符集统一设置utf8mb4,排序规则utf8mb4_general_ci。之所以用utf8mb4而不只是utf8,是因为MySQL的utf8最多只能存3字节字符,遇到生僻字或者emoji全会乱码。博物馆展品名称里经常出现一些生僻字,这一条必须优先保证。全表统一使用InnoDB引擎,主键用自增int,业务上的单号、编号都不当主键用,只在查询频繁的字段上建普通索引。

表名核心字段用途
sys_userid, username, password, real_name, phone, role, avatar用户与管理员
exhibitionid, title, cover, start_date, end_date, open_time, description, status展览信息
exhibitid, exhibition_id, name, era, material, image, audio, description展品信息
reservationid, order_no, user_id, exhibition_id, visit_date, people_count, status, remark预约记录
announcementid, title, content, publish_time, is_top公告

以reservation表为例,实际建表时连续两行容易出现注释遗漏,我的习惯是每个字段都写上COMMENT,这样导出SQL给同事时,不需要额外文档就能看懂字段含义。预约人数people_count我设为INT,而不是用字符串,因为后续统计“总接待人数”时需要SUM求和,字符串字段在聚合时会出现隐式转换的性能问题。

3.2 表关系与冗余设计取舍

表关系其实不复杂:展览是一对多展品,用户是一对多预约,预约是多对一展览。在设计时我故意增加了一些冗余字段,比如reservation表里同时冗余了exhibition_title,这样预约列表页就不用再JOIN展览表去取标题。这种冗余在规范派看来是反范式设计,但在真实项目里,它把一次联表查询变成单表查询,大幅减少了数据库压力。只要冗余字段来源于业务主表且不会频繁变动,是完全可以接受的。

另一个很容易踩坑的设计点是软删除。展品和公告表我都加了deleted字段,默认0,删除操作改为UPDATE而不是DELETE。很多新人会问:数据量这么小,硬删除不就完了?但从运营角度看,管理员误删一个展品后想要恢复,如果没有软删除,就只能靠备份修复,非常痛苦。我在展品管理接口里加了只查询deleted = 0的条件,同时给deleted建了联合索引(exhibition_id, deleted),确保列表查询走索引。

4. 后端SpringBoot实现要点

4.1 环境与项目初始化

我用SpringBoot 2.7.18,配合Java 1.8,这个版本组合最稳定,网上资料最多,遇到问题也最容易搜索到答案。如果选SpringBoot 3.x,Java版本必须升到17,很多老版本的MyBatis-Plus、Druid都要跟着换,和这套项目里的依赖会冲突。所以除非你有特殊需求,否则不建议一上来就用SpringBoot 3。

pom.xml的依赖中,核心就这几个:spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、druid-spring-boot-starter、jjwt、hutool。Hutool是一个工具包,里面封装了日期转换、随机数生成、文件上传等常用方法,能省不少重复代码。配置文件我用YAML格式,关键配置如下:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/museum?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: root druid: initial-size: 5 min-idle: 5 max-active: 20 servlet: multipart: max-file-size: 100MB max-request-size: 100MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

数据库URL里必须带上serverTimezone=Asia/Shanghai,否则MySQL驱动会报时区错误。连接池用Druid而不是HikariCP,是因为Druid自带的监控页面在调试接口时非常方便,能直接看到每条SQL的执行时间和参数,出问题定位很快。上线前我会把mybatis-plus的日志实现改为org.apache.ibatis.logging.nologging.NoLoggingImpl,否则每一条SQL都会打到日志文件里,时间久了文件会膨胀得很夸张。

4.2 登录鉴权与接口拦截

登录接口的逻辑是:接收用户名和密码,密码先MD5加盐再与数据库对比。我为什么没用BCrypt?因为这是一个教学型和交付型项目,很多接手的人要修改密码生成逻辑,MD5加盐足够应付内部系统场景。但如果你要对外正式上线,我建议换成BCrypt。登录成功之后,我用JWT生成一个有效期为24小时的token,把userId和role放进去,通过响应头返回给前端。

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { throw new BusinessException(401, "未登录或登录已过期"); } Claims claims = JwtUtil.parse(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } }

拦截器注册到WebMvcConfigurer时,我配置了排除路径,包括登录接口、首页轮播、展览列表、展品详情等所有游客也能访问的接口。这个排除列表必须维护好,否则前端联调时经常遇到“明明页面能打开,一调接口就401”的情况。有一个特别容易遗漏的点:跨域预检请求OPTIONS也会走拦截器,如果不把OPTIONS方法排除掉,前端就会看到跨域报错,但后端日志里又没有一点异常。

4.3 分页、条件查询与统计接口

分页我直接用MyBatis-Plus的Page对象,配合PaginationInnerInterceptor插件。前端请求时参数名固定为current和pageSize,后端通过Page接收后,返回给前端的结果中包含total、records、pages等字段。展品列表查询是典型的多条件分页:按展览ID、按年代、按关键字。我把这些条件封装成一个QueryParam对象,在Service层通过LambdaQueryWrapper链式拼接,条件为空时自动跳过,避免手写一堆if判断。

@Override public Page<Exhibit> getExhibitPage(ExhibitQueryParam param) { LambdaQueryWrapper<Exhibit> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(param.getExhibitionId() != null, Exhibit::getExhibitionId, param.getExhibitionId()); wrapper.like(StrUtil.isNotBlank(param.getKeyword()), Exhibit::getName, param.getKeyword()); wrapper.eq(StrUtil.isNotBlank(param.getEra()), Exhibit::getEra, param.getEra()); wrapper.orderByAsc(Exhibit::getSortOrder); return exhibitMapper.selectPage(new Page<>(param.getCurrent(), param.getPageSize()), wrapper); }

统计接口我用了几条SQL,比如近七天预约量、本月展览数、展品总数。这里要注意MySQL的日期函数与Java时区的对齐:如果你在配置里把serverTimezone设置为Asia/Shanghai,但Java默认时区是UTC,统计“今天”时会出现8小时偏移。一条排查思路是:在应用入口加TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")),或者在JVM启动参数里加-Duser.timezone=Asia/Shanghai。我两种方法都试过,更推荐启动参数,因为它在任何环境配置下都不会被代码覆盖。

4.4 MinIO文件上传与M3U8播放踩坑

文件上传是整个项目里最容易出问题的环节。我最初用本地磁盘存储,上线后发现服务器重启时临时文件目录可能被清空,后来改成了MinIO对象存储。MinIO与SpringBoot的整合其实不复杂:引入minio依赖,配置endpoint、accessKey、secretKey和bucket名称,然后写一个StorageService封装上传、下载、删除三个方法。上传成功后返回文件的访问URL,这个URL直接存到数据库对应记录的image或audio字段上。

重点说一下M3U8视频播放。很多博物馆展览会提供视频导览,而M3U8是HLS协议的常见格式。前端直接使用原生video标签播放M3U8通常不成功,因为浏览器原生不支持HLS的m3u8播放。我在项目里用vue-video-player这个插件,它内部集成了videojs,配合videojs-contrib-hls插件,可以做到“免安装”播放M3U8流。但有一个前提条件必须满足:MinIO的Bucket必须设置CORS跨域规则,允许前端域名访问,否则视频请求会被浏览器拦截,表现是Audio/Video元素报错,但服务端日志里没有任何记录。

import VueVideoPlayer from 'vue-video-player'; import 'video.js/dist/video-js.css'; import 'vue-video-player/src/custom-theme.css'; Vue.use(VueVideoPlayer);

另一个和MinIO相关的坑是防盗链。如果视频URL是公开可访问的,别人拿到链接后就能直接用,带宽会被白白消耗。MinIO支持预签名URL,可以通过S3SDK的getPresignedObjectUrl方法生成带过期时间的临时访问地址。在博物馆系统的展品详情接口中,我可以动态生成一个有效期10分钟的视频预览地址返给前端,这样既能正常播放,又不会让资源永久暴露。代价是每次接口请求都会多一次SDK调用,性能上有轻微损耗,但对这种低频业务完全可接受。

5. 前端Vue实现要点

5.1 脚手架与目录结构

前端我用Vue 2.6 + Element UI的组合,虽然Vue 3已经成为主流,但对这类管理系统来说,Vue 2的生态更成熟,组件示例多,遇到问题搜到答案的概率大。项目基于vue-cli 4/5创建,目录结构我按“功能分包”来组织:

  • src/api —— 所有接口请求封装,一个模块一个文件
  • src/assets —— 静态资源
  • src/components —— 公共组件(轮播图、上传、表格分页)
  • src/router —— 路由配置与导航守卫
  • src/store —— Vuex状态管理,存用户信息与登录状态
  • src/views —— 页面视图,按用户端、管理端分子目录
  • src/utils —— request.js(axios封装)、auth.js、validate.js

这种结构最大的好处是分工清晰。前端实习同学进来,直接看目录名就知道某个页面代码放在哪里,不会出现把所有组件堆在App.vue里的恐怖情况。在创建项目时我一般会关掉ESLint的严格模式,因为v-for需要key、组件命名规则这些规范提醒,在联调阶段只会拖慢节奏,验收前再统一开起来就来得及。

5.2 路由、状态管理与axios封装

路由设计是整个前端骨架最核心的部分。用户端路由直接配置在router.js里,管理端路由单独配置在admin.js子路由文件里,通过路由守卫判断登录状态和role权限。如果用户未登录,访问需要权限的页面时自动跳转到登录页,并且携带redirect参数,登录成功后回跳。这里有一个体验细节:登录页跳转时要把原始目标路由保存下来,否则用户每次登录后都回到首页,体验会比较差。

axios封装我做了三个核心能力:请求拦截器自动附加Authorization头、响应拦截器统一处理业务码、错误提示统一走Message组件。后端返回的code中,200代表业务成功,401代表未登录或token失效,500代表服务器内部异常。响应拦截器里如果收到401,就直接清除本地用户信息并跳转到登录页,这个逻辑放在前端可以减少很多页面里的重复判断。

service.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res; } if (res.code === 401) { store.commit('logout'); router.push('/login'); return Promise.reject(res.msg); } Message.error(res.msg || '系统异常'); return Promise.reject(res.msg); }, error => { Message.error(error.message || '网络异常'); return Promise.reject(error); } );

Vuex里我保存了token、userInfo、role三种状态。token优先放在localStorage,因为刷新页面后需要恢复登录状态;userInfo可以存内存里,刷新后通过getInfo接口重新拉取。这里不建议把整个用户对象都塞进localStorage,因为用户信息可能在后台被管理员修改,前端拿到的永远是旧数据,刷新页面后展示的还是修改前的值,容易引起运营人员的困惑。

5.3 后端接口对接与多环境配置

前后端并行开发时,接口字段经常调整。我的做法是:所有接口都要在api目录里单独建文件并导出一个函数,不要在组件内直接写axios请求。比如展览接口:

import request from '@/utils/request'; export function getExhibitionList(data) { return request({ url: '/exhibition/list', method: 'post', data }); }

这么做的好处有两个。第一,接口路径统一管理,后端改了路径,前端只用改一个文件;第二,每个接口函数名直接对应后端Controller方法名,前后端沟通时不用查文档,直接说“调getExhibitionList就行”。组件里引调用函数,代码可读性也高很多。

多环境配置我放在.env.development和.env.production两个文件里,分别定义VUE_APP_BASE_URL。开发环境指到本机后端的http://localhost:8080,生产环境指到Nginx反向代理之后的/api前缀。注意不要把生产地址写死在代码里,否则部署到不同域名时要重新打包。Nginx配置里做一层/api的反向代理,把请求转发到SpringBoot应用,这样前端路由走history模式时也不会出现请求路径404。

6. 部署、交付与常见问题实录

6.1 前后端打包与Nginx部署

后端打包用Maven的package命令,打包前先跑一次clean,避免旧的classes残留。生成的jar包用java -jar museum-server.jar启动,生产环境我建议用Systemd服务管理,这样重启、开机自启、日志管理都方便。JVM参数可以设置-Xms512m -Xmx1024m,对这类系统足够。如果你用的是MySQL 8.0以上,注意驱动必须用8.x版本,否则会报Public Key Retrieval is not allowed错误。

前端构建执行npm run build,产物在dist目录。最稳妥的部署方式是:把dist目录放到Nginx的html目录下,然后配置location /api反向代理到后端。还有一个容易被忽略的点:history模式下刷新页面会404,必须在Nginx里配置try_files,把不存在的路径都指到index.html。

server { listen 80; server_name museum.example.com; root /var/www/museum; index 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 / { try_files $uri $uri/ /index.html; } }

注意proxy_pass后面带了个斜杠,这意味着前端请求/api/exhibition/list会被转发到http://127.0.0.1:8080/exhibition/list,后端Controller里不需要写/api前缀。这个细节前后端必须对齐,否则会变成接口请求404但后端日志完全正常的问题。

6.2 让人头疼的8个常见问题

跨域问题堪称全栈项目新手的第一大坑。前后端分离开发时,前端跑在8080端口,后端跑在9527端口,浏览器默认拒绝跨域请求。我推荐在后端加一个全局CorsFilter,允许所有跨域请求;生产环境因为有Nginx反向代理,其实不存在跨域问题,但保留了也不会引发安全问题,因为后端接口有鉴权拦截。

日期格式化是最不起眼但最容易返工的坑。后端传给前端的时间格式默认是2024-01-12T10:20:30.000+00:00,前端展示出来非常丑。解决办法是在实体类的日期字段上加@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")。注意timezone必须写GMT+8,不然即使格式对了,时间也会差8小时。这个坑我帮别人排查过很多次,十次里有八次都是时区问题而不是格式问题。

上传大文件时Nginx默认限制是1MB,前端传一个展览宣传视频就会报413 Request Entity Larger。解决方法是修改Nginx的client_max_body_size为100m,同时在SpringBoot中配置spring.servlet.multipart.max-file-size。两个地方缺一不可,只改一端都不会生效。

数据库乱码是另一个经典问题。MySQL安装时的默认字符集可能是latin1,这时候无论Java还是URL怎么指定utf8,最终存进去的还是乱码。最可靠的验证方法是命令行登录MySQL后执行SHOW VARIABLES LIKE 'character%',看到都是utf8mb4再继续。连接URL要加上characterEncoding=utf8mb4,MySQL驱动和数据库、连接串三者的编码必须完全统一。

管理端接口有时候明明加了拦截器,Swagger联调时却发现某些接口没被拦住。排查思路是:确认拦截器的addPathPatterns和excludePathPatterns是否写错,比如写了/但实际只想拦/api/,那所有非/api路径就漏掉了。我习惯把接口路径设计成统一前缀,比如controllerRequestMapping上加一个/api/xxx,这样拦截器只要配/api/**. 权限逻辑就会清晰很多。

前端在Vue中调用接口后,页面数据不更新。这种情况八成是响应式丢失,比如给对象新增一个属性但没使用Vue.set,或者给数组按下标赋值。我在展品列表里遇到过这个坑:接口返回list后直接this.list[i].status = '已通过',页面上不刷新,改成Vue.set(this.list, i, newObj)后就好了。提升性能是其次,关键是这类问题完全不报错,排查起来很费时间。

前后端联调时接口返回200但业务code是500,这种是最迷惑的。常见原因是后端代码把异常捕获后又返回了统一错误码,但前端只看http状态码直接当成功处理。我在响应拦截器里明确判断了code,如果code不是200就会走错误Message。前端不要看HTTP状态码,因为SpringBoot默认对业务异常一般会返回200,携带code为500,这是国内项目的惯例做法。所有联调问题都要以HTTP状态码 + 业务code双重维度来判断。

最后一个坑是Maven打包时jar包运行后找不到主类。常见情况是SpringBoot Maven插件没配repackage目标,生成的是普通jar而不是可执行jar。检查pom里spring-boot-maven-plugin的版本和配置。另外也要检查主类是否使用了@SpringBootApplication注解并且配了主方法,我一度写过主类没有main方法还理直气壮说编译通过了,运行到一半才发现问题。

6.3 这套项目后续还能怎么扩展

目前这套系统是单体、单库、前后端分离的结构,已经能覆盖博物馆运营的核心需求。如果后续要往生产级方向打磨,有几个方向可以走。

第一个方向是接入第三方支付。现在的预约系统纯靠后台人工审核,没有真正的票务支付环节。如果需要卖付费特展票,建议跳过自己做支付网关,直接接入微信支付和支付宝的路由统一下单接口。关键是要在reservation表中增加支付单号、支付状态、支付时间三个字段,前端在预约成功页唤起支付,支付回调里更新预约状态为已支付,同时短信提醒用户预约成功。

第二个方向是消息通知。现在用户只能自己来网站查审核结果,体验比较被动。可以在预约状态变化时集成阿里云短信服务或者邮件服务,用SpringBoot的@Async注解异步发送通知,避免阻塞主接口的响应。也可以用WebSocket给管理员推送新的预约提醒,这个功能处理起来并不复杂,引入spring-boot-starter-websocket后,在预约接口里向指定客户端发送一条消息即可。

第三个方向是数据分析。目前只有最简单的三个指标,后续可以做客流热力分析、展品受欢迎程度排名、预约取消率统计。这些功能只需要在reservation表和exhibit表上做聚合查询就能出基础报表,加上定时任务把统计数据缓存到Redis里面,运营看板才能做到秒级响应。

在交付源码时,文档和数据库初始化脚本是灵魂。我把SQL脚本分为三类:schema.sql建库建表、data.sql初始化管理员账号、test_data.sql提供演示数据。管理员账号我通常会初始化为用户名admin、密码123456,并且在文档里醒目标注首次登录后必须改密。别再相信在线演示平台的提示了,这类系统凡是能猜到的弱口令都要堵死。

这套项目我已经完整交付过好几次,每次都有人问我能不能再加功能。其实对于一个全栈基础项目来说,现有功能已经足够打通整个开发链路:你从表设计到后端接口,从Vue组件到Nginx部署,每一步都能亲手走一遍,这才是吃透SpringBoot + Vue技术栈的正确方式。拿到代码后不要急着改业务,先把登录流程完整走通,把一条数据从MySQL里查到页面,再把一张图片上传到MinIO并在前端展示出来。这三件事做完,你对这套系统的理解就已经超过大半的“仅下载源码”使用者了。

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

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

立即咨询