公司动态
旅游管理系统完整版:前台+后台全栈开发实例
简介这是一套基于 WebForm 技术栈的旅游管理系统完整源码同时包含后台管理端与配套数据库适合正在学习 ASP.NET WebForms 的开发者、计算机专业学生以及需要快速搭建旅游类站点或后台管理模块的工程人员。资源共910个文件覆盖 .cs 业务逻辑、.aspx 页面、.ascx 公共控件、.ashx 一般处理程序以及 .sql 数据库脚本另有大量 jpg、png、gif 图片与 js、css 文件用于前端展示压缩包大小22.79MB目录结构较为完整便于按模块分析与复用。资源包含 Global.asax、通用页头页脚、缩略图与上传处理、线路管理、线路详情、促销组详情、列表展示等典型功能模块基本构成一套前台展示加后台管理的闭环示例。目前已有3738人学习下载适合用来理解 WebForm 项目组织方式、后台数据交互流程以及旅游业务场景下的常见功能实现也可作为毕业设计或课程项目的参考蓝本。 很多人找我做“旅游管理系统”一上来就是“帮我做个网站展示景点、酒店、路线最好还能订票。”但我一问后台呢对面往往愣住。这套旅游管理系统完整版兼后台是我特意把一个被当成“附加功能”的管理后台拉到了一等公民的位置前台给游客看后台给运营人员用两者之间不是两套割裂的系统而是同一份业务数据的不同操作界面。整个项目从数据库、接口到页面都按这个思路设计。如果你正打算做一个旅游相关的外包/毕设项目或者想理解“前台后台”全栈系统的组织方式这篇笔记应该能给你一个可以直接抄作业的参考。1. 不是所有“带后台”的系统都叫完整版先拆业务边界1.1 一个真实需求方的诉求是什么做这类项目时我习惯先问需求方三个问题游客来网站做什么运营人员登录后台做什么网站上线后谁负责维护内容这三个问题能逼出真正的需求边界。我的客户是一家小型旅游公司日常要发布周边游线路、景点的介绍也要接受游客在线预约、咨询。他们之前用“展示型网站微信群接龙”的方式处理订单经常出现人数记错、日期报满还继续接客的情况。所以这次系统的核心不是“页面好看”而是把“景点信息发布”和“可售余位控制”做准确。拆完之后需求就清晰了。前台要能展示景点、旅行路线、价格游客可以选日期、填人数、提交预订后台必须能维护景点和路线内容处理待确认订单查看每天的报名人数还能发公告。注意这里的前台用户和后台管理员是两类完全不同的身份不能共用一套登录机制。1.2 前台与后台的功能清单如何划分我自己整理过一个很实用的划分表供你参考端功能模块核心操作前台景点/线路展示列表、详情、关键词搜索前台在线预订选日期、填人数、提交订单前台订单查询按手机号/订单号查状态前台公告资讯查看公司公告、旅行须知后台管理员登录账号密码登录、退出后台内容管理景点分类、景点、线路的增删改查后台订单管理查询/筛选订单、确认/取消订单后台会员管理查看游客信息、导出名单后台数据统计每日订单量、热门景点排行这里要特别注意“兼后台”不是简单地在菜单里加一个“管理入口”而是后台改的任何内容前台马上能同步生效前台产生的订单后台能实时看到并处理。数据只有一份展示层分开这才是完整的“前台后台”结构。2. 技术组合与数据设计把“景点-订单-用户”放进同一套模型2.1 为什么前后台共用一套数据但展示层分开技术选型上我最终用了 Spring Boot 2.7 MyBatis-Plus MySQL 8.0前台页面用 Thymeleaf 服务端渲染后台管理端用 Vue 3 Element Plus Vite。很多人会问既然都用了 Vue为什么前台不也做成前后端分离原因很实际旅游系统非常依赖搜索引擎获客景点介绍页面需要被百度收录首屏速度也要快。服务端渲染可以直接输出完整 HTML对 SEO 和首屏体验都更友好。而后台是登录后才能访问的运营系统用 Vue 3 Element Plus 做单页应用交互流畅表格、表单、弹窗这些组件开箱即用开发效率高得多。Spring Boot 在这个项目里同时干两件事用 Thymeleaf 返回前台页面用 REST API 给后台管理端提供数据。这样前后台访问的是同一个 Service 层和同一批 Mapper数据模型天然统一。2.2 核心表结构从分类到订单数据库设计是整个系统的地基。景点、路线、订单、会员这几个核心表必须一开始就理清楚否则后面改表改到怀疑人生。景点表我通常这样建CREATE TABLE scenic_spot ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL COMMENT 分类ID, name VARCHAR(120) NOT NULL COMMENT 景点名称, cover_url VARCHAR(255) COMMENT 封面图, summary VARCHAR(500) COMMENT 简介, content LONGTEXT COMMENT 详细介绍, province VARCHAR(60) COMMENT 省份, city VARCHAR(60) COMMENT 城市, price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 参考价, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, sort INT DEFAULT 0 COMMENT 排序, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_status (status) ) COMMENT景点信息表;订单表是另一个关键表它要记录“谁在什么时间买了哪个景点的多少张票”CREATE TABLE travel_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号, user_id INT NOT NULL COMMENT 前台用户ID, scenic_id INT NOT NULL COMMENT 景点ID, travel_date DATE NOT NULL COMMENT 出行日期, ticket_count INT NOT NULL COMMENT 票数, total_amount DECIMAL(10,2) NOT NULL COMMENT 总金额, contact_name VARCHAR(50) COMMENT 联系人, contact_phone VARCHAR(20) COMMENT 联系电话, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_scenic_date (scenic_id, travel_date) ) COMMENT旅游订单表;后台管理员相关表我建议直接用标准 RBAC 模型管理员表、角色表、权限表再加两张关联表。不要嫌表多一旦后期需要增加“运营”“财务”“超级管理员”等角色这套表会让权限控制非常轻松。2.3 容易被忽略的字段与索引看到这里你可能觉得表结构也没什么特别的但实际踩坑往往在细节上。第一几乎每个业务表都要有status、sort、created_at这三个字段。status用于上下架和软删除不要让用户真的 delete 数据运营误删后想恢复都没办法。sort用于手动控制前台展示顺序运营要排热门景点时就会感激这个字段。created_at不用多说所有列表倒序排序都靠它。第二MyBatis-Plus 的自动填充功能要善用。在实体类的created_at和updated_at上加上TableField(fill FieldFill.INSERT)再写一个MetaObjectHandler实现类插入和更新时就不用每次手动 set 时间。这个细节能省很多重复代码。第三索引不要建太多但查询频繁的组合一定要加。比如travel_order表的(scenic_id, travel_date)联合索引在后台查“某景点某天的订单量”时作用明显。景点表的status加索引因为前台列表只会查上架数据数据量大时这个索引能避免一次全表扫描。3. 前台用户端从景点展示到下单支付的完整动作3.1 服务端渲染的选择与实现前台页面我用 Thymeleaf 来做核心原因就一个景区介绍页需要被搜索引擎收录服务端渲染直接返回 HTML爬虫能够完整抓取页面内容。如果是纯 Vue SPA首屏是空壳SEO 基本靠额外预渲染对一个小团队来说维护成本太高。Controller 层的写法很直接Controller public class WebSpotController { Autowired private ScenicSpotService spotService; GetMapping(/spot/{id}) public String detail(PathVariable Integer id, Model model) { ScenicSpot spot spotService.getById(id); model.addAttribute(spot, spot); return spot/detail; } }对应的 Thymeleaf 模板里用${spot.name}、${spot.content}输出数据即可。注意content字段放的是富文本 HTMLThymeleaf 默认会转义要用th:utext${spot.content}才能正常渲染样式。这个坑我第一次做时就踩过页面一直显示一堆标签排查半天才发现是转义问题。3.2 搜索与分页没有搜索引擎也能做好模糊查询旅游系统常见的搜索场景是“输入关键词找到相关的景点或路线”。数据量在几十万以内时完全没必要上 ElasticsearchMySQL 的LIKE配合分页就够用。用 MyBatis-Plus 的 LambdaQueryWrapper 写起来很清爽public PageScenicSpot searchSpot(String keyword, int page, int size) { PageScenicSpot p new Page(page, size); LambdaQueryWrapperScenicSpot wrapper new LambdaQueryWrapper(); wrapper.eq(ScenicSpot::getStatus, 1) .and(w - w.like(ScenicSpot::getName, keyword) .or().like(ScenicSpot::getSummary, keyword)) .orderByDesc(ScenicSpot::getSort); return spotService.page(p, wrapper); }这里有一个容易被新手忽略的坑like的 keyword 里如果包含%或_会被当成通配符处理造成意外匹配。用户输入的关键词最好先做转义把\、%、_替换掉再用like查询。实际项目中还应该限制 keyword 长度防止有人拼接超大字符串消耗数据库性能。3.3 下单时的余位控制与订单号生成旅行社最怕的就是超卖同一个景点同一天卖了 50 张票实际只放出来 40 个位子。解决思路是引入独立的余位表而不是把余位写在景点表里。我建了一张scenic_stock表主键是(scenic_id, travel_date)字段是total_stock和booked_stock。下单时执行原子扣减UPDATE scenic_stock SET booked_stock booked_stock #{count} WHERE scenic_id #{scenicId} AND travel_date #{travelDate} AND booked_stock #{count} total_stock;这条 SQL 的关键在于WHERE条件里带上booked_stock #{count} total_stock数据库的行级锁会保证并发下单时不会超卖。如果 affected rows 等于 0说明余位不足直接返回“该日期已满”的提示。订单号生成也有讲究不要用自增 ID 直接暴露给用户。我一般用“时间戳 随机数”的方式比如yyyyMMddHHmmss 6位随机数保证唯一性即可。如果并发量特别大可以把随机数换成雪花 ID 算法但在旅游管理这类业务里简单方案完全够用。4. 后台管理端用 Vue3 Element Plus 搭出可维护的管理界面4.1 权限模型与路由守卫后台管理端如果只是自己用做一个简单的登录判断就够了。但为了后续扩展我直接上了 RBAC 模型管理员表、角色表、权限表。管理员登录后返回 JWT token每次请求都带着 token后端用拦截器校验。前端路由则通过路由守卫控制页面能否访问。Vue Router 的守卫写法可以参考router.beforeEach((to, from, next) { const token localStorage.getItem(admin_token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.roles !to.meta.roles.includes(store.state.roleCode)) { next(/403) } else { next() } })这里要注意一个设计细节前端路由守卫控制的是“显示层面”真正权限校验必须落在后端接口上。也就是说管理员没有某个权限时后端接口也要返回 403不能只靠前端隐藏按钮。我见过不少项目只做了前端权限别人手动调 API 就能越权这是很大的安全漏洞。4.2 景点管理、订单列表和统计面板的实现要点后台管理界面的核心就是“表单 表格 弹窗”Element Plus 的el-table、el-form、el-dialog组合起来非常顺手。景点管理页面我通常用一个弹窗表单承载新增和编辑图片上传用el-upload上传成功后把返回的 URL 存到表单里。订单列表是运营每天都要看的页面必须做好三件事分页、筛选、状态修改。分页用el-pagination后端接口接收pageNum和pageSize。状态筛选用下拉框包含“待确认”“已确认”“已完成”“已取消”。状态修改可以直接在表格行内用el-select变更或者点按钮打开确认框。这里建议所有修改操作都加二次确认避免运营误触导致订单状态错乱。统计面板我用 ECharts 展示最近 7 天订单量和热门景点 Top10数据从后端聚合接口取。聚合 SQL 实际上很简单SELECT scenic_id, COUNT(*) AS order_count FROM travel_order WHERE status IN (1, 2) AND create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY scenic_id ORDER BY order_count DESC LIMIT 10;4.3 后台与前台共用接口的设计很多初学者会把前台和后台做成两套完全独立的接口比如前台写一个/api/web/spot/list后台再写一个/api/admin/spot/page两个 Controller 里逻辑一模一样只是返回格式不同。这样代码重复严重后期改一个字段要改两处。正确的做法是让前后台共用 Service 层Controller 只做路径和权限的区分。比如// 前台只返回上架数据 GetMapping(/web/spot/list) public Result webList(SpotQuery query) { query.setStatus(1); return Result.success(spotService.list(query)); } // 后台管理员可查看全部数据 PostMapping(/admin/spot/page) PreAuthorize(hasPermission(spot:list)) public Result adminPage(SpotQuery query) { return Result.success(spotService.page(query)); }两个接口最终都调spotService区别只在于前台强制传入status1后台则完全由管理员自由筛选。这样既保证了前台只能看到上架内容又避免写两套业务逻辑。5. 上线部署与真实踩坑这些细节文档里很少写5.1 用 Docker Compose 一键拉起本地环境部署这一步我最推荐用 Docker Compose 把 MySQL、Redis、Spring Boot、Nginx 四个服务编排在一起。本地开发和线上环境保持一致的镜像版本能避免“本地好好的上线就挂”的经典问题。简化版docker-compose.yml可以这样写version: 3 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: travel ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql app: build: . depends_on: - mysql ports: - 8080:8080 nginx: image: nginx:alpine ports: - 80:80 volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./dist:/usr/share/nginx/html volumes: mysql_data:Nginx 配置里有一个特别关键的try_files后台管理用的是 Vue Router history 模式刷新某个子路由时如果 Nginx 没有配置会直接 404。要这样写location /admin/ { try_files $uri $uri/ /admin/index.html; }5.2 三个耽误我整晚的麻烦这类项目我做完之后踩过最值得记录的三个坑每个都花了大把时间排查。第一个是 Thymeleaf 页面取不到后台传过来的对象值。前台详情页一直显示nullController 里明明已经addAttribute了。最后发现是实体类没有写 getter 方法Thymeleaf 通过反射读取属性时拿不到值。这个错误很低级但 IDE 不会报警告排查起来很烦。第二个是后台订单分页数据错位。前端传过去的pageNum从 0 开始而后端 MyBatis-Plus 的Page对象默认从 1 开始导致第一页数据重复或丢失。后来我统一在前端传页码时1后端分页参数也做了注释说明才真正消停。第三个是 MySQL 8 连接报时区错误。启动时提示The server time zone value Öйú±ê׼ʱ¼ä is unrecognized网上答案五花八门。最终的解决办法是在 JDBC 连接串上显式加上serverTimezoneAsia/Shanghai同时 MySQL 容器启动时也设置TZAsia/Shanghai一个后缀解决。5.3 上线后的性能与安全建议系统能跑起来只是第一步还有几件事建议上线前就做。第一所有查询接口务必统一返回结构。我习惯用Result{ code, message, data }包裹所有接口响应这样前端处理异常、后端记录日志都方便。很多项目接口返回格式乱七八糟联调时到处踩坑。第二订单查询接口必须做数据隔离。游客查订单时后端不仅要有order_no作为查询条件还要强制加上user_id避免用户通过遍历订单号看到别人的订单。管理员也要校验登录状态和权限不能让一个普通管理员随便删景点。第三图片上传要限制文件类型和大小。旅游系统里运营会传景点照片如果不加限制有人传一个 1G 的图片上去一次就把磁盘打满。我用的是 Spring 的MultipartFile加参数校验图片只允许 jpg、png、webp文件大小限制在 5MB 以内同时用thumbnailator做压缩生成缩略图这样列表页加载速度也有保障。最后说一句我个人很深的体会旅游管理系统这种业务核心不是技术有多新而是数据模型是否准确、前后台逻辑是否一致。把“后台”当成和“前台”同等重要的模块来设计一开始就规划好表结构、接口权限和部署方案后面踩的坑会少很多。如果你正在做同类系统照着这个思路先画一张数据流图再动手写代码会稳得多。本文还有配套的精品资源点击获取