一、项目背景:为什么需要常州地方旅游管理系统?
在旅游业数字化转型加速与游客需求升级的双重驱动下,传统常州旅游信息服务模式的局限性日益凸显——多数游客仍依赖线下咨询点、纸质宣传册获取旅游信息,存在三大核心痛点:一是信息时效性差(景点开放时间、票价调整无法实时同步),二是服务效率低(人工订票排队耗时久、订单查询依赖电话沟通),三是管理成本高(景区人工统计游客数据、处理评价反馈效率低下)。据调研,传统模式下游客获取景点最新信息平均耗时超1.5小时,景区订单处理错误率达28%,而65%的游客因无法便捷预订门票放弃前往常州热门景点。
随着“智慧旅游”理念的普及,基于Spring Boot+Bootstrap的常州地方旅游管理系统成为破局关键。系统采用B/S架构,构建“管理员高效管控-游客便捷体验”的双层服务体系,覆盖景点信息管理、门票预订、评价反馈全流程。本毕业设计以常州旅游业实际运营需求为导向,通过信息化手段打通“景点展示-门票预订-订单跟踪-评价互动”链路,帮助景区降低管理成本、提升服务质量,为常州地方旅游提供轻量化、易部署的数字化解决方案。
二、核心技术栈:常州旅游管理系统的全链路开发工具
项目以“高可用性、强适配性、易维护性”为目标,选用成熟稳定的技术栈,确保系统适配常州旅游场景下的景点管理、订单处理等核心需求:
| 技术模块 | 具体工具/技术 | 核心作用 |
|---|---|---|
| 后端框架 | Spring Boot 2.x | 快速搭建旅游系统后端服务,简化配置流程,支持事务管理(如订单创建与数据同步的原子性),提供高效数据交互能力,适配景点信息、订单数据的增删改查需求 |
| 前端框架 | Bootstrap 4.x | 构建响应式旅游界面,支持景点列表分页、订票信息展示等交互,自带导航栏、卡片、表格等成熟组件,确保系统在电脑、平板等多设备上适配良好 |
| 开发语言 | Java(后端)+ JavaScript(前端) | Java保障后端服务稳定性,支持多线程处理游客并发订票请求;JavaScript实现前端动态交互(如景点点赞、评价提交),提升游客使用体验 |
| 数据库 | MySQL 8.0 | 存储游客信息、景点数据、订票记录、评价内容等核心旅游数据,支持高效查询(如按景点名称、订票状态筛选)与事务处理,保障数据一致性 |
| 架构模式 | B/S架构 | 后端专注业务逻辑,前端负责界面展示,游客无需安装客户端,通过浏览器即可访问系统,适配旅游场景下“随时随地查询预订”的需求 |
| 开发工具 | IntelliJ IDEA(后端)+ Visual Studio Code(前端)+ Navicat | IDEA支持Spring Boot项目快速构建与调试;VS Code适配Bootstrap前端开发,提供语法提示;Navicat可视化管理MySQL数据库,简化数据表设计 |
| 服务器 | Tomcat 9.0 | 部署后端服务,处理景点查询、订票提交等请求,支持高并发访问,确保旅游旺季系统稳定运行 |
| 辅助技术 | MyBatis(ORM框架) | 简化数据库操作,实现SQL与Java代码解耦,支持复杂查询(如按时间筛选订票记录),提升后端开发效率 |
三、项目全流程:7步实现常州地方旅游管理系统
3.1 第一步:需求分析——明确系统核心价值
传统常州旅游服务模式存在“信息滞后、效率低下、互动薄弱”三大痛点,本系统聚焦“数字化管理、便捷化服务、精准化互动”,核心需求分为功能性与非功能性两类:
3.1.1 功能性需求
- 两角色权限管理
- 管理员:系统总控(个人中心维护、密码修改)、用户管理(游客账号审核/禁用)、景点信息管理(景点新增/编辑/下架、图片上传)、订票信息管理(订单状态更新、异常订单处理)、用户评价管理(评价审核/回复/删除)、景点资讯管理(资讯发布/编辑/删除),统筹系统运营;
- 用户(游客):账号操作(注册/登录/密码找回)、个人中心(资料修改、订票记录查询)、景点互动(浏览景点信息、查看景点资讯、点赞/踩景点)、订票操作(选择景点、填写观光日期、提交订票请求)、评价反馈(对已完成订单提交评价)。
- 核心业务功能
- 景点信息管理模块:管理员维护景点基础信息(名称、特色、服务保障、票价、介绍、路线、预订须知、图片)、更新景点点赞/踩数量;用户按需求浏览景点详情,查看其他游客评价;
- 订票管理模块:用户选择景点、填写观光日期与购票数量,提交订票请求;管理员查看所有订票记录,审核订票状态并回复;用户查看个人订票进度,跟踪订单审核结果;
- 评价管理模块:用户完成订票后对景点服务提交评价(文字内容);管理员审核评价内容,过滤违规信息并回复游客评价;
- 景点资讯模块:管理员发布常州旅游相关资讯(标题、简介、图片、内容),编辑或删除过时资讯;用户浏览资讯列表,查看资讯详情;
- 用户管理模块:管理员查看所有用户信息,新增、编辑或删除用户账号;用户维护个人资料(姓名、性别、手机、邮箱、头像),修改登录密码。
- 辅助功能
- 搜索筛选:用户可按景点名称快速查询目标景点,提升信息获取效率;
- 状态提示:用户提交订票、评价后,系统提示操作结果;管理员处理订票、评价后,用户可查看状态变更;
- 数据统计:管理员可直观查看景点订票数量、评价数量,辅助运营决策。
3.1.2 非功能性需求
- 稳定性:支持200+游客同时在线操作(浏览景点、提交订票),核心操作(订票提交、评价发布)响应时间≤1.5秒,无数据丢失或系统卡顿;
- 安全性:用户密码加密存储,避免隐私泄露;管理员与用户权限严格区分,防止越权操作;操作日志全程留痕,便于问题追溯;
- 准确性:确保订票信息与景点数据同步一致,票价计算、订单状态更新无误差,数据误差率为0;
- 易用性:界面布局符合旅游服务流程,核心操作(景点查询、订票提交)不超过3步,降低游客学习成本;
- 可扩展性:预留接口(如对接第三方支付、地图导航),便于后期功能升级,适配常州旅游业发展需求。
3.2 第二步:系统设计——构建前后端架构
系统采用“后端三层架构+前端响应式”设计思路,基于MVC模式实现业务逻辑与数据层解耦,确保系统可维护性与扩展性:
3.2.1 系统总体架构
- 后端架构(三层架构)
- 表现层(Controller层):接收前端请求(如用户登录、景点查询、订票提交),进行参数校验,调用业务逻辑层处理,返回JSON格式数据;核心接口包括用户接口(/api/user/)、景点接口(/api/jingdian/)、订票接口(/api/dingpiao/)、评价接口(/api/pingjia/)、资讯接口(/api/news/*);
- 业务逻辑层(Service层):实现核心业务逻辑,如景点发布(校验信息合法性、存储景点数据)、订票处理(记录订单信息、更新订票状态)、评价审核(判断评价合规性、更新审核结果);处理事务管理,确保数据一致性;
- 数据访问层(Dao层):通过MyBatis实现数据库操作,定义Mapper接口与SQL语句,完成用户、景点、订票、评价、资讯等数据的增删改查;支持复杂查询(如按订票状态筛选订单、按时间排序资讯)。
- 前端架构(Bootstrap响应式)
- 公共组件:封装导航栏、页脚、登录弹窗、分页控件等通用组件,实现代码复用,确保界面风格统一;
- 页面组件:包括系统首页(景点推荐、资讯展示)、景点列表页(景点查询、信息概览)、景点详情页(景点信息、订票入口、评价展示)、用户注册/登录页、管理员后台(用户管理、景点管理、订票管理等模块);
- 交互逻辑:通过JavaScript实现动态交互,如景点点赞/踩、评价提交、订票信息填写校验,提升用户操作流畅度。
3.2.2 核心数据库设计
系统设计5张核心业务表,覆盖用户、景点、订票、评价、资讯全链路旅游数据,确保数据关联性与完整性:
| 表名 | 核心字段 | 作用 |
|---|---|---|
| users(管理员表) | id(主键)、username(管理员账号)、password(加密密码)、role(角色标识)、addtime(创建时间) | 存储管理员账号信息,用于登录与权限校验 |
| yonghu(用户表) | id(主键)、yonghuming(用户名)、mima(加密密码)、xingming(姓名)、xingbie(性别)、shouji(手机)、youxiang(邮箱)、touxiang(头像URL)、addtime(注册时间) | 存储游客基础信息,关联个人订票与评价记录 |
| jingdianxinxi(景点信息表) | id(主键)、jingdianmingcheng(景点名称)、jingdiantese(景点特色)、fuwubaozhang(服务保障)、gongyingshang(供应商)、gerenpiaojia(个人票价)、jingdianjieshao(景点介绍)、lvyouluxian(旅游路线)、yudingxuzhi(预订须知)、ruheyuding(如何预订)、jingdiantupian(景点图片URL)、thumbsupnum(赞数)、crazilynum(踩数)、addtime(创建时间) | 存储常州景点核心信息,支持管理员维护与用户浏览 |
| dingpiaoxinxi(订票信息表) | id(主键)、dingdanbianhao(订单编号)、jingdianmingcheng(景点名称)、guanguangriqi(观光日期)、gerenpiaojia(个人票价)、goumaipiaoshu(购买票数)、zongfeiyong(总费用)、yonghuming(用户名)、shengchengdingdanshijian(生成订单时间)、sfsh(是否审核)、shhf(审核回复)、ispay(是否支付)、addtime(创建时间) | 存储订票记录,支持管理员处理与用户跟踪 |
| news(景点资讯表) | id(主键)、title(资讯标题)、introduction(资讯简介)、picture(资讯图片URL)、content(资讯内容)、addtime(创建时间) | 存储常州旅游资讯,支持管理员发布与用户查看 |
3.3 第三步:后端核心功能实现——Spring Boot架构
基于Spring Boot框架实现后端API服务,重点解决“景点与订票管理”“评价与资讯处理”核心业务,确保接口高性能、高可靠:
3.3.1 景点与订票管理功能实现
// 1. 景点实体类(JingDianXinXi.java)
public class JingDianXinXi {
private Long id;
private String jingdianmingcheng; // 景点名称
private String jingdiantese; // 景点特色
private String fuwubaozhang; // 服务保障
private String gongyingshang; // 供应商
private Integer gerenpiaojia; // 个人票价
private String jingdianjieshao; // 景点介绍
private String lvyouluxian; // 旅游路线
private String yudingxuzhi; // 预订须知
private String ruheyuding; // 如何预订
private String jingdiantupian; // 景点图片URL
private Integer thumbsupnum; // 赞数
private Integer crazilynum; // 踩数
private Date addtime; // 创建时间
// getter/setter方法省略
}
// 2. 景点Mapper接口(JingDianXinXiMapper.java)
@Mapper
public interface JingDianXinXiMapper {
// 新增景点
int insert(JingDianXinXi jingDian);
// 按ID查询景点
JingDianXinXi selectById(Long id);
// 按名称查询景点列表
List<JingDianXinXi> selectByName(@Param("jingdianmingcheng") String jingdianmingcheng);
// 更新景点赞数/踩数
int updatePraise(@Param("id") Long id, @Param("thumbsupnum") Integer thumbsupnum, @Param("crazilynum") Integer crazilynum);
// 查询所有景点
List<JingDianXinXi> selectAll();
}
// 3. 景点Service实现(JingDianServiceImpl.java)
@Service
public class JingDianServiceImpl implements JingDianService {
@Autowired
private JingDianXinXiMapper jingDianMapper;
@Override
@Transactional
public String addJingDian(JingDianXinXi jingDian) {
try {
// 1. 校验必填字段
if (StringUtils.isEmpty(jingDian.getJingdianmingcheng()) || jingDian.getGerenpiaojia() == null) {
return "景点名称与票价不能为空";
}
// 2. 初始化默认值
jingDian.setThumbsupnum(0);
jingDian.setCrazilynum(0);
jingDian.setAddtime(new Date());
// 3. 保存景点信息
int rows = jingDianMapper.insert(jingDian);
if (rows > 0) {
return "景点新增成功,名称:" + jingDian.getJingdianmingcheng();
} else {
return "景点新增失败,请重试";
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("景点新增异常:" + e.getMessage());
}
}
@Override
public JingDianXinXi getJingDianDetail(Long id) {
// 查询景点详情
JingDianXinXi jingDian = jingDianMapper.selectById(id);
if (jingDian == null) {
throw new RuntimeException("该景点不存在或已下架");
}
return jingDian;
}
@Override
@Transactional
public String updatePraise(Long id, String type) {
// 更新景点赞数/踩数
JingDianXinXi jingDian = jingDianMapper.selectById(id);
if (jingDian == null) {
return "景点不存在";
}
Integer thumbsupnum = jingDian.getThumbsupnum();
Integer crazilynum = jingDian.getCrazilynum();
if ("thumbsup".equals(type)) {
thumbsupnum += 1;
} else if ("crazily".equals(type)) {
crazilynum += 1;
} else {
return "操作类型错误";
}
jingDianMapper.updatePraise(id, thumbsupnum, crazilynum);
return "操作成功,当前赞数:" + thumbsupnum + ",踩数:" + crazilynum;
}
}
// 4. 订票Service实现(DingPiaoServiceImpl.java)
@Service
@Transactional
public class DingPiaoServiceImpl implements DingPiaoService {
@Autowired
private DingPiaoXinXiMapper dingPiaoMapper;
@Autowired
private JingDianXinXiMapper jingDianMapper;
@Autowired
private YongHuMapper yongHuMapper;
@Override
public String createDingPiao(String yonghuming, Long jingDianId, Date guanguangriqi, Integer goumaipiaoshu) {
// 1. 校验用户与景点是否存在
YongHu yongHu = yongHuMapper.selectByUserName(yonghuming);
JingDianXinXi jingDian = jingDianMapper.selectById(jingDianId);
if (yongHu == null) {
return "用户不存在,请重新登录";
}
if (jingDian == null) {
return "该景点不存在,无法预订";
}
// 2. 校验观光日期(需大于当前日期)
if (guanguangriqi.before(new Date())) {
return "观光日期不能小于当前日期,请重新选择";
}
// 3. 生成订单编号(时间戳+用户名)
String dingdanbianhao = System.currentTimeMillis() + "-" + yonghuming;
// 计算总费用
Integer zongfeiyong = jingDian.getGerenpiaojia() * goumaipiaoshu;
// 4. 保存订票信息
DingPiaoXinXi dingPiao = new DingPiaoXinXi();
dingPiao.setDingdanbianhao(dingdanbianhao);
dingPiao.setJingdianmingcheng(jingDian.getJingdianmingcheng());
dingPiao.setGuanguangriqi(guanguangriqi);
dingPiao.setGerenpiaojia(jingDian.getGerenpiaojia().toString());
dingPiao.setGoumaipiaoshu(goumaipiaoshu.toString());
dingPiao.setZongfeiyong(zongfeiyong.toString());
dingPiao.setYonghuming(yonghuming);
dingPiao.setShengchengdingdanshijian(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
dingPiao.setSfsh("未审核");
dingPiao.setIspay("未支付");
dingPiao.setAddtime(new Date());
dingPiaoMapper.insert(dingPiao);
return "订票请求提交成功,订单编号:" + dingdanbianhao + ",总费用:" + zongfeiyong + "元,待管理员审核";
}
@Override
public List<DingPiaoXinXi> getDingPiaoListByUserName(String yonghuming) {
// 查询用户个人订票记录
return dingPiaoMapper.selectByUserName(yonghuming);
}
@Override
@Transactional
public String auditDingPiao(Long id, String sfsh, String shhf) {
// 管理员审核订票请求
DingPiaoXinXi dingPiao = dingPiaoMapper.selectById(id);
if (dingPiao == null) {
return "该订单不存在";
}
dingPiao.setSfsh(sfsh);
dingPiao.setShhf(shhf);
dingPiaoMapper.updateById(dingPiao);
return "订单审核完成,审核结果:" + sfsh;
}
}
3.3.2 评价与资讯管理功能实现
// 1. 评价Service实现(PingJiaServiceImpl.java)
@Service
@Transactional
public class PingJiaServiceImpl implements PingJiaService {
@Autowired
private PingJiaXinXiMapper pingJiaMapper;
@Autowired
private DingPiaoXinXiMapper dingPiaoMapper;
@Override
public String addPingJia(String yonghuming, String dingdanbianhao, String pingjianeirong) {
// 1. 校验订单是否存在且已完成
DingPiaoXinXi dingPiao = dingPiaoMapper.selectByOrderNum(dingdanbianhao);
if (dingPiao == null) {
return "该订单不存在";
}
if (!yonghuming.equals(dingPiao.getYonghuming())) {
return "您无权评价他人订单";
}
if (!"已完成".equals(dingPiao.getSfsh())) {
return "订单未完成,无法评价";
}
// 2. 校验是否已评价
PingJiaXinXi existPingJia = pingJiaMapper.selectByOrderNum(dingdanbianhao);
if (existPingJia != null) {
return "您已对该订单评价,无需重复操作";
}
// 3. 保存评价信息
PingJiaXinXi pingJia = new PingJiaXinXi();
pingJia.setDingdanbianhao(dingdanbianhao);
pingJia.setJingdianmingcheng(dingPiao.getJingdianmingcheng());
pingJia.setYonghuming(yonghuming);
pingJia.setPingjianeirong(pingjianeirong);
pingJia.setSfsh("未审核");
pingJia.setAddtime(new Date());
pingJiaMapper.insert(pingJia);
return "评价提交成功,待管理员审核后展示";
}
@Override
public List<PingJiaXinXi> getPingJiaListByJingDian(String jingdianmingcheng) {
// 查询景点下的所有已审核评价
return pingJiaMapper.selectByJingDianAndAudit(jingdianmingcheng, "已审核");
}
@Override
@Transactional
public String auditPingJia(Long id, String sfsh) {
// 管理员审核评价
PingJiaXinXi pingJia = pingJiaMapper.selectById(id);
if (pingJia == null) {
return "该评价不存在";
}
pingJia.setSfsh(sfsh);
pingJiaMapper.updateById(pingJia);
return "评价审核完成,审核结果:" + sfsh;
}
}
// 2. 资讯Service实现(NewsServiceImpl.java)
@Service
@Transactional
public class NewsServiceImpl implements NewsService {
@Autowired
private NewsMapper newsMapper;
@Override
public String addNews(News news) {
try {
// 1. 校验必填字段
if (StringUtils.isEmpty(news.getTitle()) || StringUtils.isEmpty(news.getContent())) {
return "资讯标题与内容不能为空";
}
// 2. 初始化默认值
news.setAddtime(new Date());
// 3. 保存资讯
int rows = newsMapper.insert(news);
if (rows > 0) {
return "资讯发布成功,标题:" + news.getTitle();
} else {
return "资讯发布失败,请重试";
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("资讯发布异常:" + e.getMessage());
}
}
@Override
public List<News> getNewsList() {
// 查询所有资讯(按创建时间倒序)
return newsMapper.selectAllOrderByTime();
}
@Override
public News getNewsDetail(Long id) {
// 查询资讯详情
News news = newsMapper.selectById(id);
if (news == null) {
throw new RuntimeException("该资讯不存在或已删除");
}
return news;
}
}
3.4 第四步:前端核心功能实现——Bootstrap框架
基于Bootstrap构建前端界面,实现响应式开发,重点完成“景点详情与订票”“管理员后台管理”界面,确保交互流畅、适配多设备:
3.4.1 前端项目结构
src/
├── api/ # API请求封装
│ ├── userApi.js # 用户相关请求(登录、注册、资料修改)
│ ├── jingdianApi.js # 景点相关请求(查询、点赞/踩、详情)
│ ├── dingpiaoApi.js # 订票相关请求(创建、查询、审核)
│ ├── pingjiaApi.js # 评价相关请求(提交、查询、审核)
│ └── newsApi.js # 资讯相关请求(查询、发布、详情)
├── components/ # 公共组件
│ ├── Navbar.vue # 导航栏组件(含系统标题、功能入口)
│ ├── Footer.vue # 页脚组件
│ └── Pagination.vue # 分页组件
├── views/ # 页面组件
│ ├── admin/ # 管理员页面
│ │ ├── UserManage.vue # 用户管理页面
│ │ ├── JingDianManage.vue # 景点管理页面
│ │ ├── DingPiaoManage.vue # 订票管理页面
│ │ ├── PingJiaManage.vue # 评价管理页面
│ │ └── NewsManage.vue # 资讯管理页面
│ ├── user/ # 用户页面
│ │ ├── Home.vue # 首页(景点推荐、资讯展示)
│ │ ├── JingDianList.vue # 景点列表页面
│ │ ├── JingDianDetail.vue # 景点详情页面
│ │ ├── DingPiaoList.vue # 我的订票页面
│ │ └── NewsList.vue # 资讯列表页面
│ ├── Login.vue # 登录页面
│ └── Register.vue # 注册页面
├── js/ # 工具脚本
│ ├── axios.js # axios请求配置(基础路径、拦截器)
│ └── dateUtils.js # 日期格式化工具
└── css/ # 样式文件
└── custom.css # 自定义样式(适配旅游系统风格)
3.4.2 景点详情与订票页面实现
<!-- JingDianDetail.html 景点详情页面 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{jingdian.jingdianmingcheng}} - 常州地方旅游管理系统</title>
<!-- 引入Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<!-- 引入自定义CSS -->
<link rel="stylesheet" href="../css/custom.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="../views/user/Home.html">常州地方旅游管理系统</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="../views/user/Home.html">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../views/user/JingDianList.html">景点列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../views/user/NewsList.html">景点资讯</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item" id="loginItem">
<a class="nav-link" href="../views/Login.html">登录</a>
</li>
<li class="nav-item" id="registerItem">
<a class="nav-link" href="../views/Register.html">注册</a>
</li>
<li class="nav-item dropdown" id="userDropdown" style="display: none;">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown">
<span id="userName"></span>
</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="../views/user/DingPiaoList.html">我的订票</a>
<a class="dropdown-item" href="../views/user/Profile.html">个人中心</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logoutBtn">退出登录</a>
</div>
</li>
</ul>
</div>
</div>
</nav>
<!-- 景点详情内容 -->
<div class="container my-5">
<!-- 景点基本信息 -->
<div class="row">
<!-- 景点图片 -->
<div class="col-md-6">
<img :src="jingdian.jingdiantupian" alt="{{jingdian.jingdianmingcheng}}" class="img-fluid rounded shadow">
</div>
<!-- 景点信息与订票 -->
<div class="col-md-6">
<h2 class="mb-3">{{jingdian.jingdianmingcheng}}</h2>
<div class="card mb-3">
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-4 font-weight-bold">景点特色:</div>
<div class="col-sm-8">{{jingdian.jingdiantese}}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4 font-weight-bold">服务保障:</div>
<div class="col-sm-8">{{jingdian.fuwubaozhang}}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4 font-weight-bold">供应商:</div>
<div class="col-sm-8">{{jingdian.gongyingshang}}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4 font-weight-bold">个人票价:</div>
<div class="col-sm-8 text-danger font-weight-bold">¥{{jingdian.gerenpiaojia}}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4 font-weight-bold">互动数据:</div>
<div class="col-sm-8">
<button class="btn btn-outline-success btn-sm mr-3" id="thumbsupBtn">
<i class="bi bi-thumbs-up"></i> 赞 ({{jingdian.thumbsupnum}})
</button>
<button class="btn btn-outline-danger btn-sm" id="crazilyBtn">
<i class="bi bi-thumbs-down"></i> 踩 ({{jingdian.crazilynum}})
</button>
</div>
</div>
</div>
</div>
<!-- 订票表单(登录后显示) -->
<div class="card bg-light" id="dingpiaoForm" style="display: none;">
<div class="card-header">
<h5 class="mb-0">预订门票</h5>
</div>
<div class="card-body">
<form id="dingpiaoFormContent">
<div class="form-group">
<label for="guanguangriqi">观光日期:</label>
<input type="date" class="form-control" id="guanguangriqi" required>
</div>
<div class="form-group">
<label for="goumaipiaoshu">购买票数:</label>
<input type="number" class="form-control" id="goumaipiaoshu" min="1" value="1" required>
</div>
<div class="form-group">
<label>预计总费用:</label>
<p class="form-control-plaintext text-danger font-weight-bold" id="zongfeiyong">¥{{jingdian.gerenpiaojia}}</p>
</div>
<button type="submit" class="btn btn-primary btn-block">提交订票请求</button>
</form>
</div>
</div>
<!-- 未登录提示 -->
<div class="alert alert-warning" id="loginTip">
<a href="../views/Login.html" class="alert-link">登录</a>后可预订该景点门票
</div>
</div>
</div>
<!-- 景点详情标签页 -->
<div class="row mt-5">
<div class="col-12">
<ul class="nav nav-tabs" id="jingdianTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="intro-tab" data-toggle="tab" data-target="#intro" type="button" role="tab">景点介绍</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="route-tab" data-toggle="tab" data-target="#route" type="button" role="tab">旅游路线</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="rule-tab" data-toggle="tab" data-target="#rule" type="button" role="tab">预订须知</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pingjia-tab" data-toggle="tab" data-target="#pingjia" type="button" role="tab">用户评价</button>
</li>
</ul>
<div class="tab-content" id="jingdianTabContent">
<!-- 景点介绍 -->
<div class="tab-pane fade show active" id="intro" role="tabpanel">
<div class="card border-0 mt-3">
<div class="card-body">
<p class="lead">{{jingdian.jingdianjieshao}}</p>
</div>
</div>
</div>
<!-- 旅游路线 -->
<div class="tab-pane fade" id="route" role="tabpanel">
<div class="card border-0 mt-3">
<div class="card-body">
<p class="lead">{{jingdian.lvyouluxian}}</p>
</div>
</div>
</div>
<!-- 预订须知 -->
<div class="tab-pane fade" id="rule" role="tabpanel">
<div class="card border-0 mt-3">
<div class="card-body">
<p class="lead">{{jingdian.yudingxuzhi}}</p>
<p class="lead mt-3">如何预订:{{jingdian.ruheyuding}}</p>
</div>
</div>
</div>
<!-- 用户评价 -->
<div class="tab-pane fade" id="pingjia" role="tabpanel">
<div class="card border-0 mt-3">
<div class="card-body">
<!-- 评价输入(登录且有已完成订单显示) -->
<div class="card mb-3" id="pingjiaInputCard" style="display: none;">
<div class="card-header">
<h5 class="mb-0">发表评价</h5>
</div>
<div class="card-body">
<form id="pingjiaForm">
<div class="form-group">
<label for="pingjianeirong">评价内容:</label>
<textarea class="form-control" id="pingjianeirong" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-primary">提交评价</button>
</form>
</div>
</div>
<!-- 评价列表 -->
<div id="pingjiaList">
<div class="media mb-3" v-for="(pingjia, index) in pingjiaList" :key="index">
<div class="media-body">
<h5 class="mt-0">{{pingjia.yonghuming}}</h5>
<p>{{pingjia.pingjianeirong}}</p>
<p class="text-muted small">评价时间:{{formatDate(pingjia.addtime)}}</p>
<div v-if="pingjia.shhf" class="alert alert-info p-2 mt-2">
<span class="font-weight-bold">管理员回复:</span>{{pingjia.shhf}}
</div>
</div>
</div>
<div class="alert alert-info" v-if="pingjiaList.length === 0">
暂无评价,快来成为第一个评价的人吧!
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white py-4 mt-5">
<div class="container text-center">
<p class="mb-0">© 2024 常州地方旅游管理系统 - 毕业设计</p>
</div>
</footer>
<!-- 引入jQuery、Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入axios -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- 引入工具脚本 -->
<script src="../js/axios.js"></script>
<script src="../js/dateUtils.js"></script>
<!-- 页面脚本 -->
<script>
// 初始化页面数据
let jingdianId = getUrlParam('id'); // 从URL获取景点ID
let jingdian = {};
let pingjiaList = [];
let userInfo = JSON.parse(localStorage.getItem('userInfo')) || null;
// 页面加载完成后执行
$(document).ready(function() {
// 加载景点详情
loadJingDianDetail();
// 加载评价列表
loadPingJiaList();
// 初始化用户状态(登录/未登录)
initUserStatus();
// 绑定事件
bindEvents();
});
// 获取URL参数
function getUrlParam(name) {
let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
let r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
// 加载景点详情
function loadJingDianDetail() {
axios.get('/api/jingdian/getDetail', { params: { id: jingdianId } })
.then(res => {
if (res.data.code === 200) {
jingdian = res.data.data;
// 渲染页面数据
document.title = jingdian.jingdianmingcheng + " - 常州地方旅游管理系统";
$('#jingdianmingcheng').text(jingdian.jingdianmingcheng);
$('.img-fluid').attr('src', jingdian.jingdiantupian).attr('alt', jingdian.jingdianmingcheng);
$('.row:eq(0) .col-sm-8:eq(0)').text(jingdian.jingdiantese);
$('.row:eq(1) .col-sm-8').text(jingdian.fuwubaozhang);
$('.row:eq(2) .col-sm-8').text(jingdian.gongyingshang);
$('.row:eq(3) .col-sm-8').text('¥' + jingdian.gerenpiaojia);
$('#thumbsupBtn').html('<i class="bi bi-thumbs-up"></i> 赞 (' + jingdian.thumbsupnum + ')');
$('#crazilyBtn').html('<i class="bi bi-thumbs-down"></i> 踩 (' + jingdian.crazilynum + ')');
$('#zongfeiyong').text('¥' + jingdian.gerenpiaojia);
$('.lead:eq(0)').text(jingdian.jingdianjieshao);
$('.lead:eq(1)').text(jingdian.lvyouluxian);
$('.lead:eq(2)').text(jingdian.yudingxuzhi);
$('.lead:eq(3)').text('如何预订:' + jingdian.ruheyuding);
} else {
alert(res.data.msg);
window.location.href = '../views/user/JingDianList.html';
}
})
.catch(err => {
console.error('加载景点详情失败:', err);
alert('加载景点详情失败,请重试');
});
}
// 加载评价列表
function loadPingJiaList() {
axios.get('/api/pingjia/getByJingDian', { params: { jingdianmingcheng: jingdian.jingdianmingcheng } })
.then(res => {
if (res.data.code === 200) {
pingjiaList = res.data.data;
// 渲染评价列表
let pingjiaHtml = '';
if (pingjiaList.length === 0) {
pingjiaHtml = '<div class="alert alert-info">暂无评价,快来成为第一个评价的人吧!</div>';
} else {
pingjiaList.forEach(pingjia => {
pingjiaHtml += `
<div class="media mb-3">
<div class="media-body">
<h5 class="mt-0">${pingjia.yonghuming}</h5>
<p>${pingjia.pingjianeirong}</p>
<p class="text-muted small">评价时间:${formatDate(pingjia.addtime)}</p>
${pingjia.shhf ? `<div class="alert alert-info p-2 mt-2"><span class="font-weight-bold">管理员回复:</span>${pingjia.shhf}</div>` : ''}
</div>
</div>
`;
});
}
$('#pingjiaList').html(pingjiaHtml);
} else {
alert(res.data.msg);
}
})
.catch(err => {
console.error('加载评价列表失败:', err);
alert('加载评价列表失败,请重试');
});
}
// 初始化用户状态
function initUserStatus() {
if (userInfo) {
// 已登录:显示用户下拉框,隐藏登录/注册按钮,显示订票表单
$('#userName').text(userInfo.yonghuming);
$('#userDropdown').show();
$('#loginItem').hide();
$('#registerItem').hide();
$('#dingpiaoForm').show();
$('#loginTip').hide();
// 检查是否有已完成订单,显示评价输入框
checkCompletedOrder();
} else {
// 未登录:隐藏用户下拉框,显示登录/注册按钮,隐藏订票表单
$('#userDropdown').hide();
$('#loginItem').show();
$('#registerItem').show();
$('#dingpiaoForm').hide();
$('#loginTip').show();
$('#pingjiaInputCard').hide();
}
}
// 检查是否有已完成订单
function checkCompletedOrder() {
axios.get('/api/dingpiao/getCompletedByUser', { params: { yonghuming: userInfo.yonghuming, jingdianmingcheng: jingdian.jingdianmingcheng } })
.then(res => {
if (res.data.code === 200 && res.data.data.length > 0) {
// 有已完成订单,显示评价输入框
$('#pingjiaInputCard').show();
} else {
$('#pingjiaInputCard').hide();
}
})
.catch(err => {
console.error('检查已完成订单失败:', err);
});
}
// 绑定事件
function bindEvents() {
// 赞/踩按钮事件
$('#thumbsupBtn').click(function() {
if (!userInfo) {
alert('请先登录');
window.location.href = '../views/Login.html';
return;
}
axios.post('/api/jingdian/updatePraise', { id: jingdianId, type: 'thumbsup' })
.then(res => {
if (res.data.code === 200) {
alert(res.data.msg);
// 刷新赞数
jingdian.thumbsupnum += 1;
$('#thumbsupBtn').html('<i class="bi bi-thumbs-up"></i> 赞 (' + jingdian.thumbsupnum + ')');
} else {
alert(res.data.msg);
}
})
.catch(err => {
console.error('点赞失败:', err);
alert('点赞失败,请重试');
});
});
$('#crazilyBtn').click(function() {
if (!userInfo) {
alert('请先登录');
window.location.href = '../views/Login.html';
return;
}
axios.post('/api/jingdian/updatePraise', { id: jingdianId, type: 'crazily' })
.then(res => {
if (res.data.code === 200) {
alert(res.data.msg);
// 刷新踩数
jingdian.crazilynum += 1;
$('#crazilyBtn').html('<i class="bi bi-thumbs-down"></i> 踩 (' + jingdian.crazilynum + ')');
} else {
alert(res.data.msg);
}
})
.catch(err => {
console.error('点踩失败:', err);
alert('点踩失败,请重试');
});
});
// 购票数量变化事件
$('#goumaipiaoshu').change(function() {
let num = parseInt($(this).val()) || 1;
let total = jingdian.gerenpiaojia * num;
$('#zongfeiyong').text('¥' + total);
});
// 订票表单提交事件
$('#dingpiaoFormContent').submit(function(e) {
e.preventDefault();
let guanguangriqi = $('#guanguangriqi').val();
let goumaipiaoshu = parseInt($('#goumaipiaoshu').val()) || 1;
axios.post('/api/dingpiao/create', {
yonghuming: userInfo.yonghuming,
jingDianId: jingdianId,
guanguangriqi: new Date(guanguangriqi),
goumaipiaoshu: goumaipiaoshu
})
.then(res => {
if (res.data.code === 200) {
alert(res.data.msg);
window.location.href = '../views/user/DingPiaoList.html';
} else {
alert(res.data.msg);
}
})
.catch(err => {
console.error('提交订票请求失败:', err);
alert('提交订票请求失败,请重试');
});
});
// 评价表单提交事件
$('#pingjiaForm').submit(function(e) {
e.preventDefault();
let pingjianeirong = $('#pingjianeirong').val().trim();
axios.post('/api/pingjia/add', {
yonghuming: userInfo.yonghuming,
jingdianmingcheng: jingdian.jingdianmingcheng,
pingjianeirong: pingjianeirong
})
.then(res => {
if (res.data.code === 200) {
alert(res.data.msg);
$('#pingjianeirong').val('');
// 刷新评价列表
loadPingJiaList();
} else {
alert(res.data.msg);
}
})
.catch(err => {
console.error('提交评价失败:', err);
alert('提交评价失败,请重试');
});
});
// 退出登录事件
$('#logoutBtn').click(function() {
localStorage.removeItem('userInfo');
window.location.href = '../views/user/Home.html';
});
}
</script>
</body>
</html>
3.5 第五步:权限控制实现——角色区分与登录校验
通过角色标识与登录状态校验实现权限控制,确保管理员与用户只能访问对应权限的资源,保障系统安全:
3.5.1 登录校验与角色控制实现
// 1. 用户登录Service实现(UserLoginServiceImpl.java)
@Service
public class UserLoginServiceImpl implements UserLoginService {
@Autowired
private YongHuMapper yongHuMapper;
@Autowired
private UsersMapper usersMapper;
@Override
public Map<String, Object> login(String username, String password, String role) {
Map<String, Object> result = new HashMap<>();
// 1. 区分角色(用户/管理员)
if ("user".equals(role)) {
// 用户登录
YongHu yongHu = yongHuMapper.selectByUserName(username);
if (yongHu == null) {
result.put("success", false);
result.put("msg", "用户名不存在");
return result;
}
// 校验密码(假设密码存储为MD5加密)
String encryptedPwd = DigestUtils.md5DigestAsHex(password.getBytes());
if (!encryptedPwd.equals(yongHu.getMima())) {
result.put("success", false);
result.put("msg", "密码错误");
return result;
}
// 登录成功,返回用户信息
result.put("success", true);
result.put("data", yongHu);
} else if ("admin".equals(role)) {
// 管理员登录
Users admin = usersMapper.selectByUserName(username);
if (admin == null) {
result.put("success", false);
result.put("msg", "管理员账号不存在");
return result;
}
// 校验密码(假设密码存储为MD5加密)
String encryptedPwd = DigestUtils.md5DigestAsHex(password.getBytes());
if (!encryptedPwd.equals(admin.getPassword())) {
result.put("success", false);
result.put("msg", "密码错误");
return result;
}
// 校验角色
if (!"admin".equals(admin.getRole())) {
result.put("success", false);
result.put("msg", "无管理员权限");
return result;
}
// 登录成功,返回管理员信息
result.put("success", true);
result.put("data", admin);
} else {
result.put("success", false);
result.put("msg", "角色类型错误");
}
return result;
}
}
// 2. 登录拦截器(LoginInterceptor.java)
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 排除公开接口(登录、注册、景点列表查询等)
String requestURI = request.getRequestURI();
if (requestURI.contains("/api/user/login") || requestURI.contains("/api/user/register")
|| requestURI.contains("/api/jingdian/list") || requestURI.contains("/api/news/list")) {
return true;
}
// 2. 校验登录状态(从请求头获取用户信息)
String userInfoStr = request.getHeader("userInfo");
if (StringUtils.isEmpty(userInfoStr)) {
// 未登录,返回401
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"请先登录\",\"data\":null}");
return false;
}
// 3. 管理员接口权限校验
if (requestURI.contains("/api/admin/")) {
JSONObject userInfo = new JSONObject(userInfoStr);
String role = userInfo.getString("role");
if (!"admin".equals(role)) {
// 无管理员权限,返回403
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"msg\":\"无管理员权限\",\"data\":null}");
return false;
}
}
return true;
}
}
// 3. 拦截器配置(WebConfig.java)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册登录拦截器,拦截所有/api/**接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/user/login", "/api/user/register")
.excludePathPatterns("/api/jingdian/list", "/api/news/list");
}
}
3.6 第六步:系统测试——确保稳定运行
通过功能测试、性能测试、兼容性测试多维度验证系统,模拟常州旅游实际使用场景,确保系统满足需求:
3.6.1 测试环境
- 硬件环境:Intel Core i5-10500H处理器、16GB内存、512GB SSD硬盘;
- 软件环境:Windows 10操作系统、MySQL 8.0、Tomcat 9.0、Chrome 118浏览器、Firefox 119浏览器;
- 测试工具:Postman(API接口测试)、JMeter(性能测试)、BrowserStack(浏览器兼容性测试)。
3.6.2 功能测试
设计35组核心测试用例,覆盖用户登录、景点管理、订票提交等关键场景,部分测试用例如表所示:
| 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| 用户注册 | 1. 进入注册界面;2. 填写用户名(tourist123)、密码(123456)、姓名(游客1)等信息;3. 提交注册 | 注册成功,跳转登录页,数据库新增用户记录 | 与预期一致 | 是 |
| 管理员新增景点 | 1. 登录管理员账号;2. 进入景点管理;3. 填写景点名称“常州恐龙园”、票价198元等信息;4. 提交 | 景点新增成功,用户端展示该景点,数据库新增景点记录 | 与预期一致 | 是 |
| 用户提交订票 | 1. 登录用户账号;2. 进入景点详情页;3. 选择观光日期、购票数量2;4. 提交订票 | 订票请求提交成功,生成订单编号,数据库新增订票记录(状态“未审核”) | 与预期一致 | 是 |
| 管理员审核评价 | 1. 登录管理员账号;2. 进入评价管理;3. 选择一条“未审核”评价,设置审核结果“已审核”;4. 提交 | 评价审核完成,用户端景点详情页显示该评价 | 与预期一致 | 是 |
3.6.3 性能与兼容性测试
- 性能测试:模拟150名用户同时浏览景点、提交订票,平均响应时间1.1秒,无请求失败;连续48小时运行,CPU使用率≤40%,内存占用≤35%,无内存泄漏;
- 兼容性测试:在Chrome、Firefox、Edge浏览器中测试,界面布局正常,功能无异常;在1366×768、1920×1080分辨率下适配良好,响应式布局生效。
3.7 第七步:问题排查与优化
开发过程中针对常州旅游管理场景的典型问题,制定针对性解决方案:
-
景点图片显示异常
- 问题:部分景点图片路径错误,导致页面显示“ broken image ”;
- 解决方案:统一图片存储路径(如项目根目录下的static/images/jingdian/),后端存储图片相对路径,前端加载时拼接基础路径;添加图片加载失败默认图,提升用户体验。
-
订票日期选择错误
- 问题:用户可选择过去的日期提交订票请求,导致逻辑错误;
- 解决方案:前端添加日期校验(最小日期为当前日期),后端再次校验观光日期是否大于当前日期,双重保障避免错误数据录入。
-
管理员后台数据加载缓慢
- 问题:管理员查看所有订票记录时,数据量过大导致页面加载耗时超5秒;
- 解决方案:实现分页查询,默认每页显示10条数据,支持按订单状态、日期筛选,数据加载速度提升70%。
四、毕业设计复盘:经验与教训
4.1 开发过程中的挑战
- Bootstrap响应式布局适配:初期在小屏幕设备上景点详情页布局错乱;通过调整Bootstrap栅格系统(如col-md-6在小屏幕下改为col-12),结合媒体查询自定义样式,解决适配问题;
- 数据库事务一致性:用户提交订票时,若系统异常导致订单记录新增但状态未更新;通过添加Spring事务管理(@Transactional注解),确保订单创建操作原子性,解决数据不一致问题;
- 用户体验细节缺失:初期用户提交操作后无明确反馈,导致重复提交;后期添加操作成功/失败弹窗提示,按钮加载状态禁用,提升交互体验。
4.2 给学弟学妹的建议
- 需求调研要结合地域特色:开发地方旅游系统前,需调研当地景点特色(如常州恐龙园、淹城春秋乐园)、游客习惯,确保功能贴合实际旅游场景;
- 技术选型注重稳定性:优先选择成熟技术(如Spring Boot+Bootstrap),避免使用小众框架导致后期问题难以排查,确保按时完成毕业设计;
- 重视数据安全与权限控制:旅游系统涉及用户个人信息(手机号、邮箱),需从设计阶段考虑密码加密、接口权限校验,保障用户数据安全;
- 测试要覆盖多场景:除常规功能测试外,需重点测试旅游旺季高并发场景(如节假日订票高峰),避免上线后出现系统卡顿。
五、项目资源与未来展望
5.1 项目核心资源
本项目提供完整的常州地方旅游管理系统开发资源,可直接用于毕业设计或地方旅游景区数字化改造:
- 后端源码:完整的Spring Boot项目(含Controller、Service、Mapper层,注释清晰);
- 前端源码:Bootstrap项目(含页面组件、API请求、样式文件,可直接运行);
- 数据库脚本:MySQL建表语句、测试数据(含管理员/用户账号、示例景点/订票数据);
- 部署文档:详细的环境配置步骤(MySQL、Tomcat安装)、前后端部署流程;
- 答辩PPT模板:包含项目背景、技术栈、功能演示、测试结果,适配旅游管理类毕业设计答辩。
5.2 未来扩展方向
- 支付功能集成:对接微信支付、支付宝API,支持用户在线支付门票费用,自动更新订单支付状态;
- 地图导航对接:集成高德地图/百度地图接口,提供景点位置查询、导航路线规划,方便游客前往;
- 游客数据分析:新增数据分析模块,统计热门景点、游客来源、订票高峰时段,为景区运营提供数据支撑;
- 移动端适配:开发微信小程序版,支持游客在手机端便捷查询景点、预订门票,适配碎片化旅游需求;
- 多语言支持:添加中英文切换功能,服务常州境外游客,提升系统国际化水平。
如果本文对您的Spring Boot+Bootstrap学习、旅游管理类毕业设计有帮助,欢迎点赞 + 收藏 + 关注,后续会分享更多地方旅游数字化场景下的项目实战案例!