目录
- Java 核心与应用:基于协同过滤的旅游景区售票平台及小程序
- 第一章 绪论
-
- 1.1 研究背景
- 1.2 研究意义
- 1.3 国内外研究现状
- 1.4 研究内容与研究目标
- 1.5 研究难点与创新点
- 1.6 本文组织结构
- 第二章 相关技术与理论基础
-
- 2.1 Spring Boot 与分层架构
- 2.2 MyBatis-Plus 与数据访问
- 2.3 JWT 无状态鉴权
- 2.4 Vue3 + Vite + Element Plus
- 2.5 微信小程序原生框架
- 2.6 MySQL 8.x 与索引设计
- 第三章 需求与可行性分析
-
- 3.1 功能需求分析
-
- 3.1.1 平台管理员(ADMIN)功能需求
- 3.1.2 景区售票员(SELLER)功能需求
- 3.1.3 游客用户(USER)功能需求
- 3.2 非功能需求分析
-
- 3.2.3 性能需求
- 3.2.4 可扩展性需求
- 3.2.5 安全性需求
- 4.2 功能模块设计
- 4.3 数据库设计
-
- 4.3.1 概念结构设计(E-R 图)
- 4.3.2 逻辑结构设计(表结构三列表)
-
- (1)sys_user 用户表
- (2)scenic 景区表
- (3)ticket_template 门票模板表
- (4)ticket 门票表
- (5)ticket_order 订单表
- (6)order_visitor 订单游客表
- (7)announcement 公告表
- 第五章 系统详细设计与实现
-
- 5.1 多端页面与导航结构设计
-
- 5.1.1 管理端(scenic-vue)页面结构
- 5.1.2 游客门户(scenic-portal)页面结构
- 5.1.3 小程序页面结构
- 5.2 核心业务模块设计与实现
-
- 5.2.1 登录鉴权与统一 Token 处理
- 5.2.2 景区管理与热门推荐
- 5.2.3 门票管理:上下架与库存
- 5.2.4 游客下单与库存扣减
- 5.2.5 模拟支付与订单状态机
- 5.2.6 核销验票:核销码与幂等控制
- 5.2.7 订单查询、取消与数据隔离
- 5.2.8 公告发布与门户展示
- 5.2.9 统计报表与可视化
- 5.3 关键接口设计说明(按论文格式)
-
- 5.3.1 登录接口
- 5.3.2 门票分页接口
- 5.3.3 游客下单接口
- 5.3.4 核销接口
- 5.4 系统测试(简要)
-
- 5.4.1 测试环境
- 5.4.2 功能测试用例设计
- 5.4.3 边界与异常测试(要点)
- 第六章 总结与展望
-
- 6.1 系统成果总结
- 6.2 不足与改进方向
- 6.3 未来扩展展望
- 第六章 总结与展望
-
- 6.1 系统成果总结
- 6.2 不足与改进方向
- 6.3 未来扩展展望
Java 核心与应用:基于协同过滤的旅游景区售票平台及小程序
系统演示视频:点击查看B站视频
第一章 绪论
1.1 研究背景
随着国内旅游消费升级与智慧景区建设推进,景区门票线上化、电子化已成为提升游客体验与景区运营效率的重要手段。传统线下窗口售票模式依赖人工与现场排队,常见问题包括:
旺季排队时间长导致体验下降,票务信息不透明使游客难以出行前决策,景区票务数据分散造成统计口径难统一,同时订单与核销链路难追溯也会抬高售后处理成本。
省级文旅平台通常需要具备“全省景区信息聚合 + 景区独立运营 + 全局统计监管”的能力:一方面为游客提供统一入口与便捷购票渠道,另一方面为主管部门提供数据支撑与运营洞察。
本项目为“省级旅游景点售票系统(全端实现)”,采用前后端分离与多端协同架构,形成“管理端 + 游客端(Web/小程序)”的统一服务形态:
后端采用 Spring Boot 2.5.15、MyBatis-Plus、JWT、MySQL 8.x 与 Druid 实现统一 API 与数据访问,管理端 Admin 使用 Vue3 + Vite + Element Plus 面向系统管理员与景区售票员,游客门户 Portal 同样采用 Vue3 + Vite + Element Plus 作为 Web 购票入口,小程序端使用原生小程序框架提供移动端购票入口。
后端统一配置 context-path: /api,并按业务模块划分接口前缀,例如认证 /api/auth、景区 /api/scenic、门票 /api/ticket、订单 /api/order、公告 /api/announcement、游客门户聚合 /api/portal。通过统一后端权威口径,Portal 与小程序共享游客端核心业务链路,实现“同一业务规则,多端一致体验”。
1.2 研究意义
从业务价值看,系统实现“线上购票 + 线下核销”的闭环,可以显著提升旅游服务效率:
游客侧可在 Portal/小程序完成景区检索、门票选择、下单支付与订单查询并查看核销状态以减少现场排队与咨询成本,景区侧通过售票员后台完成门票上架、库存维护、后台售票、订单核销与销售统计以提升运营管理能力,平台侧由系统管理员统一管理全省景区与售票员账号并发布公告、汇总订单与销售数据以支撑监管与运营。
从工程实现看,多端协同强调“统一鉴权 + 分层架构 + 数据隔离 + 状态机”的综合能力:
后端使用 JWT 实现无状态认证以减少会话依赖,并通过角色(ADMIN/SELLER/USER)与 scenic_id 绑定实现数据隔离,同时以订单状态机约束支付、取消、核销等关键动作以保证业务一致性。
1.3 国内外研究现状
票务系统的工程形态逐渐从“单一后台售票”发展为“平台管理端 + 用户多入口(Web/APP/小程序)”的多端协同模式。典型实现通常包含:
Token 鉴权与多角色权限边界、分页加载与条件检索、订单状态机与库存一致性(扣减/回滚/幂等)、核销码与验票流程以及统计报表与可视化等能力共同构成了票务系统的典型工程实现。
在国内实践中,票务平台常常与“智慧景区”建设同步推进,平台不仅承担售票职能,还承担景区展示、公告通知、客流提示与运营统计等能力;系统架构上更强调多端一致性与快速迭代能力,因此前后端分离、接口分组、统一返回结构与统一鉴权处理成为常见方案。另一方面,随着跨景区、跨城市的联动需求增加,平台侧需要具备对景区商家账号的隔离管理能力,并对订单与核销数据形成全局视角的汇总统计,以满足监管与运营分析。
在国外实践中,票务系统更强调与支付、身份与风控体系的耦合,例如对接信用卡支付、实名制校验、异常下单识别与退款对账等能力;这类能力在毕业设计中通常以“可扩展方向”的形式给出,但在论文描述上仍需要说明系统预留接口与数据字段的合理性,从而体现设计的完整性与可演进性。
在国外的景区/演出/交通票务场景中,电子票与核销验票同样是主流方案,同时更强调支付安全、反欺诈、实名合规与隐私保护。
1.4 研究内容与研究目标
本文档以“全端(后端 + Admin + Portal + 小程序)”为交付范围,研究内容与目标包括:
本系统的研究目标是构建统一后端 API 与统一鉴权机制以支持三角色登录与接口访问,并实现覆盖景区管理、门票管理、购票下单、模拟支付、订单核销、订单取消与统计的核心业务闭环,同时在管理端覆盖平台管理员与景区售票员场景以体现数据隔离与运营管理能力,并在游客端(Portal/小程序)提供景区检索、购票与订单查询入口以保证与后台口径一致。
1.5 研究难点与创新点
难点:
本系统的主要难点在于实现库存一致性(下单扣减、取消/退款回滚以避免超卖与重复回滚)、状态机约束(支付、取消、核销、退款在允许状态内执行并考虑幂等)、多角色数据隔离(售票员仅访问绑定景区数据而管理员可全局监管)以及多端协同一致(Portal 与小程序共享口径并统一字段与错误码语义)。
创新点:
本系统的创新点体现在统一用户表的“平台-景区一体化角色模型”(通过 scenic_id 绑定售票员与景区以降低复杂度)、游客端聚合接口设计(将首页、检索、下单、支付、我的订单等链路收敛到 /api/portal 以利于多端复用与接口演进)以及核销闭环的可扩展设计(订单表预留 verify_code/verify_time 并结合 order_visitor 支持“一单多游客”的核销记录扩展)。
1.6 本文组织结构
第一章介绍研究背景、意义与目标;第二章阐述相关技术;第三章进行需求与可行性分析;第四章给出系统总体设计与数据库设计;第五章对多端核心页面与关键业务模块进行详细设计与实现说明(含流程图与关键代码片段);第六章总结与展望。
第二章 相关技术与理论基础
2.1 Spring Boot 与分层架构
本系统后端采用 Spring Boot 2.5.15 构建 RESTful 服务。为了保证业务可维护性与工程可扩展性,后端采用典型分层结构:
系统采用 Controller、Service、Mapper/DAO 与 Entity 的分层结构,其中 Controller 负责接收请求参数、统一返回结构并进行基础参数校验与权限边界控制,Service 承载下单扣减库存、取消回滚库存、状态机推进与核销幂等等核心业务规则,Mapper/DAO 通过 MyBatis-Plus 完成数据访问、条件查询与分页,Entity 映射数据库表结构并通过逻辑删除与字段自动填充保持数据一致性。
分层架构的关键价值在于将“业务权威”集中在 Service 层:Controller 只做轻量编排,避免业务散落在多个入口导致口径不一致,从而为多端协同提供统一支撑。
2.2 MyBatis-Plus 与数据访问
本系统的数据访问层采用 MyBatis-Plus,其设计目标是在 MyBatis 的灵活性与工程开发效率之间取得平衡。在毕业设计与中小型业务系统中,常见的开发痛点是大量重复的 CRUD 代码、分页与条件查询的手工拼接以及实体字段与数据库字段映射的一致性维护成本,而 MyBatis-Plus 通过通用 Mapper、条件构造器与分页插件将上述成本显著降低。
从工程结构看,系统将数据访问职责集中在 Mapper 层,并在 Service 层组织跨表业务规则;这样做的意义在于,Controller 层只负责参数接收与返回组织,避免将 SQL 条件与业务判断分散到多个入口,从而降低后期维护难度。对于旅游订票场景,景区列表、门票列表、订单列表与公告列表均具有“筛选条件多、数据量随时间增长”的特征,因此分页与条件构造是高频需求。
在分页实现上,系统使用 MyBatis-Plus 的分页对象承载查询结果,并统一返回给前端,以便前端表格组件能够稳定地渲染记录列表、总条数与页码信息。与一次性返回全量数据相比,分页能够有效降低网络传输与浏览器渲染压力,并且能在数据库层通过 limit/offset 控制读取范围,从而保持接口响应时间稳定。
在条件查询上,系统常用的筛选字段包括景区的关键字与等级、门票的景区归属与上下架状态、订单的状态与日期区间、公告的关键字与发布状态等。通过条件构造器或自定义 SQL,系统能够将“筛选逻辑”尽可能下推到数据库层完成,减少在应用层对大集合进行二次过滤的开销,并使统计口径更接近数据源。
在字段映射与规范方面,系统启用下划线转驼峰映射以保持数据库字段与 Java 字段的命名一致性,并通过逻辑删除字段 deleted 避免物理删除带来的统计口径变化。逻辑删除不仅便于后续审计与问题追溯,也可以在售后纠纷场景中保留订单记录与关键字段,增强平台运营的可追溯能力。
需要注意的是,MyBatis-Plus 提升的是开发效率而不是替代业务一致性控制;对于“下单扣减库存、取消回滚库存、核销状态推进”等关键链路,仍应以事务边界与条件更新为核心手段保证一致性。尤其在并发下单场景中,若只依赖应用层先查库存再更新库存,可能出现竞态条件,因此更稳妥的方式是使用“带条件的更新”或引入乐观锁版本字段来保证库存扣减的原子性。
从可扩展角度看,本系统后续若需要引入更复杂的报表统计或多维分析,可以在保持现有 CRUD 与业务链路稳定的前提下,新增专门的统计查询 Mapper 或视图查询,避免在核心交易表的写入逻辑中掺杂复杂聚合计算,从而保证核心链路的稳定性与可维护性。
2.3 JWT 无状态鉴权
系统采用 JWT(JSON Web Token)作为无状态认证方案。登录成功后后端生成 Token,Token 中包含 userId、username、role 等 Claims。前端在后续请求中将 Token 写入请求头,即统一使用 Authorization: Bearer <token> 携带 Token。后端通过拦截器解析 Token,将用户信息写入 request 属性中(如 request.userId、request.role),从而在各业务接口中实现拦截器统一完成未登录拒绝访问、按角色与 scenic_id 的数据隔离以及避免在不同 Controller 中重复解析 Token 的口径统一。
与基于 Session 的方案相比,JWT 方案更适合前后端分离与多端协同,服务端无需维护会话状态,易于扩展。
2.4 Vue3 + Vite + Element Plus
本系统前端分为两套 Web 端应用:scenic-vue 用作后台管理端(ADMIN/SELLER),scenic-portal 用作游客门户(USER)。两者均采用 Vue3 + Vite 构建 SPA,并采用 Element Plus 作为组件库。两者通过路由守卫实现未登录跳转与角色入口限制,通过 Axios 封装统一 baseURL 与 Token 注入并统一处理 401 与错误提示,并以表格/分页/弹窗表单的规范交互模式提升可用性。
为满足后台统计展示需求,管理端通常结合 ECharts 绘制“趋势图/分布图/排行图”,实现可视化运营面板。
2.5 微信小程序原生框架
小程序端作为移动端购票入口,采用微信小程序原生框架,其主要优点是接入门槛低、运行环境统一、易于部署。小程序通常通过全局 app 存储 baseUrl、token 与用户信息,并在 onLoad/onShow 等页面生命周期中刷新数据与登录态。在多端协同上,小程序侧尽量复用游客门户的接口语义(如 /api/portal/home、/api/portal/order 等),确保同一业务在不同端呈现一致。
2.6 MySQL 8.x 与索引设计
系统数据库采用 MySQL 8.x。结合票务业务的查询特征,系统重点采用以下设计策略:
系统对订单表建立 order_no 唯一索引以保证订单号唯一并便于按单号查询,同时对 scenic_id、status、use_date、create_time 等字段建立索引以提升列表与统计查询性能,并采用逻辑删除字段 deleted 以避免物理删除导致统计口径变化。
通过以上设计,系统能够在常见教学规模与中小并发规模下保持较好的性能表现。
在实际运行中,索引设计需要兼顾写入与读取的平衡:订单表既是高频写入表(下单、支付、取消、核销都会触发更新),也是高频读取表(后台分页、按时间统计、核销查询等),因此索引数量不宜无限增加,否则会抬高写入成本并影响整体吞吐;本系统优先选择与业务查询最强相关的字段建立索引,从而在性能与维护成本之间取得平衡。
另外,订单与门票的状态字段在业务中具有“枚举值有限但过滤频率高”的特征,因此状态索引能够明显提升后台筛选效率;而对游客端而言,更多通过景区与门票的状态控制可见性,减少游客端请求返回无效数据的概率,也能从交互层面降低无效请求带来的资源消耗。
第三章 需求与可行性分析
3.1 功能需求分析
本系统采用“平台管理员(ADMIN)+ 景区售票员(SELLER)+ 游客用户(USER)”三类角色模型,其中一个售票员账号绑定一个景区,角色划分清晰、业务边界明确,适合毕业设计实现与论文描述。
3.1.1 平台管理员(ADMIN)功能需求
平台管理员面向省级平台开展全局运营与监管管理,核心功能需求如下:
平台管理员主要负责登录鉴权与个人信息管理,并对全省景区进行新增与编辑、启用/禁用与设置热门推荐,对售票员账号进行创建、与景区绑定、重置密码与禁用管理,对门票类型模板(成人票/学生票/儿童票/老年票等)配置实名与限购等基础约束,同时支持全省订单查看与按景区、状态、关键字、日期区间筛选及取消订单(记录取消原因)的能力,并提供平台公告发布/置顶/撤回与系统基础配置(可扩展),最后通过统计报表输出订单数、销量、销售额、近7天销售趋势与订单状态分布。
3.1.2 景区售票员(SELLER)功能需求
售票员负责本景区的门票与订单,系统对售票员操作进行数据隔离:仅允许访问与自己 scenic_id 绑定的业务数据。
景区售票员主要负责登录鉴权并维护本景区信息(名称、简介、图片、开放时间与联系方式等展示信息),完成门票管理的新增/编辑/逻辑删除与上下架并设置类型、价格、库存、有效期、实名与限购等属性,同时支持后台售票(为游客直接创建订单并填写游客信息生成核销码)、订单管理与核销(查看本景区订单列表与详情并输入核销码完成验票),并提供今日销量与销售额、近7天趋势与状态分布等本景区维度的销售统计能力。
3.1.3 游客用户(USER)功能需求
游客用户通过 Portal 或小程序完成购票流程。考虑到教学场景与论文表达,本系统将游客侧核心业务定位为“景区检索 + 门票购买 + 订单管理”。
游客用户支持用户名密码注册、登录与退出并在登录后按角色跳转对应端(游客进入 Portal,小程序进入游客端首页),可在首页查看热门景区推荐、公告列表与公告详情并获取个性化推荐(可选扩展),在景区浏览中完成景区列表分页与关键字搜索并查看包含图文介绍、地址、开放时间、联系方式等信息的景区详情与相似景区推荐,在门票购买中获取景区门票列表、选择门票与游玩日期、填写游客信息(支持一单多游客扩展)并提交生成待支付订单后进行模拟支付推进为已支付,最后在我的订单中按状态筛选查看订单列表与详情(含金额、游客信息、核销码与状态)并在符合状态机约束时取消订单。
3.2 非功能需求分析
3.2.3 性能需求
系统在性能方面要求景区列表、门票列表与订单列表等采用分页接口并支持关键字筛选,对订单表、门票表等高频查询字段建立索引以提升查询性能,并在移动端(小程序)具备 Loading 与错误提示机制以提升弱网环境可用性。
在性能目标的表达上,论文更强调“稳定可用”而非极限并发,因此本系统将性能指标聚焦在两点:其一是分页查询的响应时间保持稳定,避免因数据量增加导致页面不可用;其二是关键链路(登录、下单、支付、核销)在正常网络条件下能够完成并给出明确反馈,从而满足答辩演示的可操作性。对于统计接口,若数据量增长较快,可通过缓存或按日汇总表的方式进行进一步优化,本系统在设计上保留了这种扩展空间。
3.2.4 可扩展性需求
系统在可扩展性方面要求支持多种支付方式与第三方支付接口扩展,支持对接信用卡支付、实名制校验、异常下单识别与退款对账等能力,并在数据库设计上预留扩展字段以支持未来可能的业务需求。
在可扩展性方面,系统通过模块化设计与接口分离实现不同模块的独立扩展与替换,例如支付模块可以独立扩展为支持更多支付方式,订单模块可以独立扩展为支持更多订单类型。同时,系统通过预留扩展字段与接口参数实现未来可能的业务需求扩展,例如支持对接信用卡支付、实名制校验、异常下单识别与退款对账等能力。
3.2.5 安全性需求
系统在安全性方面要求采用 JWT 无状态鉴权并通过拦截器统一校验 Token,密码以 MD5 形式存储以避免明文泄露,通过角色 + scenic_id 实现数据隔离以保证售票员无法访问非绑定景区数据,并对未授权情况返回统一结构以便前端统一处理登录过期与跳转。
在安全性细节上,系统尤其关注“越权访问”与“敏感数据泄露”两类风险:越权访问主要通过后端强制覆盖查询条件来规避,例如售票员访问订单分页时由后端读取其绑定景区并强制过滤,从而避免仅依赖前端隐藏菜单带来的安全隐患;敏感数据泄露则通过字段最小化与展示脱敏来控制,在论文层面需要强调手机号、身份证号等字段仅用于业务必要环节,并且在展示与日志中应避免输出完整明文。
4.2 功能模块设计
结合角色划分与业务闭环,系统功能模块可划分为管理端模块、游客端模块与公共支撑模块。
功能模块划分的核心原则是“职责单一、边界清晰、数据口径统一”。对于管理端,模块划分更贴近运维与运营工作流,例如景区管理与售票员管理强调平台侧资源管理,门票与订单模块强调景区侧票务运营与售后处理;对于游客端,模块划分更贴近用户旅程路径,围绕“发现—决策—下单—支付—核销—售后”展开。公共支撑模块则承担全端共享的能力,例如鉴权、异常处理与文件上传,以保证多端一致性并降低重复建设。
#mermaid-svg-682VDSGpz3Fltrik{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-682VDSGpz3Fltrik .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-682VDSGpz3Fltrik .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-682VDSGpz3Fltrik .error-icon{fill:#552222;}#mermaid-svg-682VDSGpz3Fltrik .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-682VDSGpz3Fltrik .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-682VDSGpz3Fltrik .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-682VDSGpz3Fltrik .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-682VDSGpz3Fltrik .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-682VDSGpz3Fltrik .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-682VDSGpz3Fltrik .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-682VDSGpz3Fltrik .marker{fill:#333333;stroke:#333333;}#mermaid-svg-682VDSGpz3Fltrik .marker.cross{stroke:#333333;}#mermaid-svg-682VDSGpz3Fltrik svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-682VDSGpz3Fltrik p{margin:0;}#mermaid-svg-682VDSGpz3Fltrik .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-682VDSGpz3Fltrik .cluster-label text{fill:#333;}#mermaid-svg-682VDSGpz3Fltrik .cluster-label span{color:#333;}#mermaid-svg-682VDSGpz3Fltrik .cluster-label span p{background-color:transparent;}#mermaid-svg-682VDSGpz3Fltrik .label text,#mermaid-svg-682VDSGpz3Fltrik span{fill:#333;color:#333;}#mermaid-svg-682VDSGpz3Fltrik .node rect,#mermaid-svg-682VDSGpz3Fltrik .node circle,#mermaid-svg-682VDSGpz3Fltrik .node ellipse,#mermaid-svg-682VDSGpz3Fltrik .node polygon,#mermaid-svg-682VDSGpz3Fltrik .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-682VDSGpz3Fltrik .rough-node .label text,#mermaid-svg-682VDSGpz3Fltrik .node .label text,#mermaid-svg-682VDSGpz3Fltrik .image-shape .label,#mermaid-svg-682VDSGpz3Fltrik .icon-shape .label{text-anchor:middle;}#mermaid-svg-682VDSGpz3Fltrik .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-682VDSGpz3Fltrik .rough-node .label,#mermaid-svg-682VDSGpz3Fltrik .node .label,#mermaid-svg-682VDSGpz3Fltrik .image-shape .label,#mermaid-svg-682VDSGpz3Fltrik .icon-shape .label{text-align:center;}#mermaid-svg-682VDSGpz3Fltrik .node.clickable{cursor:pointer;}#mermaid-svg-682VDSGpz3Fltrik .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-682VDSGpz3Fltrik .arrowheadPath{fill:#333333;}#mermaid-svg-682VDSGpz3Fltrik .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-682VDSGpz3Fltrik .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-682VDSGpz3Fltrik .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-682VDSGpz3Fltrik .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-682VDSGpz3Fltrik .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-682VDSGpz3Fltrik .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-682VDSGpz3Fltrik .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-682VDSGpz3Fltrik .cluster text{fill:#333;}#mermaid-svg-682VDSGpz3Fltrik .cluster span{color:#333;}#mermaid-svg-682VDSGpz3Fltrik div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-682VDSGpz3Fltrik .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-682VDSGpz3Fltrik rect.text{fill:none;stroke-width:0;}#mermaid-svg-682VDSGpz3Fltrik .icon-shape,#mermaid-svg-682VDSGpz3Fltrik .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-682VDSGpz3Fltrik .icon-shape p,#mermaid-svg-682VDSGpz3Fltrik .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-682VDSGpz3Fltrik .icon-shape rect,#mermaid-svg-682VDSGpz3Fltrik .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-682VDSGpz3Fltrik .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-682VDSGpz3Fltrik .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-682VDSGpz3Fltrik :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
公共支撑
JWT鉴权
数据隔离
统一返回Result
日志与异常处理
文件上传
游客端(Portal/小程序)
首页/热门/公告
景区搜索/详情
门票列表/详情
购票下单
模拟支付
我的订单/取消
管理端(ADMIN/SELLER)
认证登录
景区管理
售票员账号管理
门票模板管理
门票管理
订单管理/核销
公告管理
统计报表
图4.2 功能模块图
4.3 数据库设计
系统数据库采用 MySQL 8.x,初始化脚本位于 scenic-springboot/src/main/resources/db/schema.sql。数据库设计遵循“核心对象清晰、字段语义明确、满足查询与统计”的原则。
旅游订票系统的数据对象具有明显的层次结构:平台用户与角色体系是权限边界的基础,景区与门票是可售资源,订单是交易记录与运营统计的核心来源,公告则承载平台运营通知。在表结构设计上,系统采用适度的冗余字段以降低查询复杂度,例如在订单表中冗余 ticket_name 与 scenic_name,可以减少订单列表展示时的多表关联成本,并使订单记录在景区或门票信息变更后仍保持“历史口径”的可读性。
在索引策略方面,系统优先为高频查询条件建立索引,尤其是订单表的 order_no 唯一索引与 scenic_id、status、use_date、create_time 等组合过滤字段,这些字段直接影响后台订单查询、核销查询以及按时间统计的性能表现。与之相对应,景区与门票表的索引更多服务于游客端检索与列表展示,强调在“关键字检索 + 状态过滤”下的查询性能。
在一致性方面,数据库层面的外键并非唯一选择;在教学与中小系统中,通常采用“应用层约束 + 索引保障 + 事务一致性”的方式实现业务一致性,从而在开发复杂度与运行性能之间取得平衡。本系统通过 Service 层事务组织关键链路写操作,并通过角色与 scenic_id 的绑定实现数据隔离,使数据库设计与业务规则形成一致的边界。
4.3.1 概念结构设计(E-R 图)
#mermaid-svg-8E5vaF7tVfW8Y3Vd{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .error-icon{fill:#552222;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .marker.cross{stroke:#333333;}#mermaid-svg-8E5vaF7tVfW8Y3Vd svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8E5vaF7tVfW8Y3Vd p{margin:0;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-8E5vaF7tVfW8Y3Vd .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .node rect,#mermaid-svg-8E5vaF7tVfW8Y3Vd .node circle,#mermaid-svg-8E5vaF7tVfW8Y3Vd .node ellipse,#mermaid-svg-8E5vaF7tVfW8Y3Vd .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-8E5vaF7tVfW8Y3Vd .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-8E5vaF7tVfW8Y3Vd :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
binds
has
creates
owns
sold_as
includes
SYS_USER
BIGINT
id
VARCHAR
username
VARCHAR
role
BIGINT
scenic_id
INT
status
SCENIC
BIGINT
id
VARCHAR
scenic_name
VARCHAR
level
INT
is_hot
INT
status
TICKET
BIGINT
id
BIGINT
scenic_id
VARCHAR
ticket_name
DECIMAL
price
INT
stock
INT
status
TICKET_ORDER
BIGINT
id
VARCHAR
order_no
BIGINT
user_id
BIGINT
scenic_id
BIGINT
ticket_id
INT
quantity
DECIMAL
total_amount
INT
status
VARCHAR
verify_code
ORDER_VISITOR
BIGINT
id
BIGINT
order_id
VARCHAR
visitor_name
INT
verify_status
图4.3 数据库E-R图
4.3.2 逻辑结构设计(表结构三列表)
本节按论文格式列出核心表字段说明。为便于描述,表结构与字段名称与实际 SQL 脚本保持一致。
(1)sys_user 用户表
| id | BIGINT | 主键ID |
| username | VARCHAR(50) | 用户名,唯一 |
| password | VARCHAR(100) | 密码(MD5) |
| real_name | VARCHAR(50) | 真实姓名 |
| phone | VARCHAR(20) | 手机号 |
| VARCHAR(100) | 邮箱 | |
| avatar | VARCHAR(500) | 头像地址 |
| role | VARCHAR(20) | ADMIN/SELLER/USER |
| scenic_id | BIGINT | 绑定景区ID(售票员) |
| status | TINYINT | 状态:0禁用/1启用 |
| deleted | TINYINT | 逻辑删除 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
(2)scenic 景区表
| id | BIGINT | 主键ID |
| scenic_name | VARCHAR(100) | 景区名称 |
| scenic_code | VARCHAR(50) | 景区编码 |
| description | TEXT | 景区简介 |
| address | VARCHAR(500) | 地址 |
| province | VARCHAR(50) | 省份 |
| city | VARCHAR(50) | 城市 |
| district | VARCHAR(50) | 区县 |
| cover_image | VARCHAR(500) | 封面图 |
| images | TEXT | 多图(逗号分隔) |
| open_time | VARCHAR(100) | 开放时间 |
| close_time | VARCHAR(100) | 关闭时间 |
| contact_phone | VARCHAR(50) | 联系电话 |
| level | VARCHAR(20) | 等级(5A/4A/3A等) |
| tags | VARCHAR(500) | 标签(逗号分隔) |
| longitude | DECIMAL(10,6) | 经度 |
| latitude | DECIMAL(10,6) | 纬度 |
| is_hot | TINYINT | 是否热门 |
| sort_order | INT | 排序权重 |
| status | TINYINT | 状态:0禁用/1启用 |
| deleted | TINYINT | 逻辑删除 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
(3)ticket_template 门票模板表
| id | BIGINT | 主键ID |
| template_name | VARCHAR(50) | 模板名称 |
| description | VARCHAR(500) | 描述 |
| need_real_name | TINYINT | 是否实名 |
| limit_count | INT | 限购数量(0表示不限) |
| deleted | TINYINT | 逻辑删除 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
(4)ticket 门票表
| id | BIGINT | 主键ID |
| scenic_id | BIGINT | 所属景区ID |
| ticket_name | VARCHAR(100) | 门票名称 |
| ticket_type | VARCHAR(50) | 门票类型 |
| price | DECIMAL(10,2) | 门票价格 |
| original_price | DECIMAL(10,2) | 原价 |
| stock | INT | 库存数量 |
| sold_count | INT | 已售数量 |
| use_date | DATE | 使用日期(为空表示通用) |
| valid_start_date | DATE | 有效期开始 |
| valid_end_date | DATE | 有效期结束 |
| need_real_name | TINYINT | 是否实名 |
| limit_count | INT | 限购数量 |
| description | TEXT | 门票说明 |
| notice | TEXT | 购票须知 |
| status | TINYINT | 状态:0下架/1上架 |
| deleted | TINYINT | 逻辑删除 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
(5)ticket_order 订单表
| id | BIGINT | 主键ID |
| order_no | VARCHAR(50) | 订单编号(唯一) |
| user_id | BIGINT | 游客用户ID(前台购票) |
| scenic_id | BIGINT | 景区ID |
| ticket_id | BIGINT | 门票ID |
| ticket_name | VARCHAR(100) | 门票名称(冗余) |
| scenic_name | VARCHAR(100) | 景区名称(冗余) |
| quantity | INT | 购买数量 |
| unit_price | DECIMAL(10,2) | 单价 |
| total_amount | DECIMAL(10,2) | 总金额 |
| use_date | DATE | 使用日期 |
| visitor_name | VARCHAR(50) | 游客姓名 |
| visitor_phone | VARCHAR(20) | 游客手机号 |
| visitor_id_card | VARCHAR(20) | 游客身份证号 |
| status | TINYINT | 0待支付/1已支付/2已核销/3已取消/4已退款 |
| pay_time | DATETIME | 支付时间 |
| verify_time | DATETIME | 核销时间 |
| verify_code | VARCHAR(50) | 核销码 |
| cancel_reason | VARCHAR(500) | 取消原因 |
| source | TINYINT | 0前台/1后台 |
| operator_id | BIGINT | 操作员ID(后台售票) |
| remark | VARCHAR(500) | 备注 |
| deleted | TINYINT | 逻辑删除 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
(6)order_visitor 订单游客表
| id | BIGINT | 主键ID |
| order_id | BIGINT | 订单ID |
| visitor_name | VARCHAR(50) | 游客姓名 |
| id_card | VARCHAR(20) | 身份证号 |
| phone | VARCHAR(20) | 手机号 |
| verify_status | TINYINT | 核销状态 |
| verify_time | DATETIME | 核销时间 |
| create_time | DATETIME | 创建时间 |
(7)announcement 公告表
| id | BIGINT | 主键ID |
| title | VARCHAR(200) | 公告标题 |
| content | TEXT | 公告内容 |
| type | TINYINT | 类型:普通/重要/维护 |
| is_top | TINYINT | 是否置顶 |
| status | TINYINT | 状态:0草稿/1发布 |
| publish_time | DATETIME | 发布时间 |
| publisher_id | BIGINT | 发布人ID |
| deleted | TINYINT | 逻辑删除 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
第五章 系统详细设计与实现
5.1 多端页面与导航结构设计
5.1.1 管理端(scenic-vue)页面结构
管理端面向 ADMIN 与 SELLER 两种角色,为减少学习成本,整体采用“侧边菜单 + 内容区”的后台管理布局。菜单项会根据角色显示不同功能入口。
管理端主要覆盖景区管理、售票员账号管理、门票模板、全省订单、公告管理与统计报表,售票员主要覆盖本景区信息、门票管理、订单管理、核销验票与统计报表。
在工程实现中,售票员的数据隔离不是依赖前端隐藏菜单,而是由后端在接口层强制限制查询条件(以 sys_user.scenic_id 为隔离边界),从而保证安全边界稳定。

5.1.2 游客门户(scenic-portal)页面结构
游客门户以“景区浏览与购票”为主线,页面结构由浅入深。
游客门户的页面路径通常为登录/注册获取 token 与身份信息,进入首页查看热门景区、公告与推荐后进行景区列表/搜索(关键字与等级筛选),在景区详情页查看图文介绍、开放时间、联系方式并进入门票列表与门票详情(价格、库存、须知),随后在下单页选择日期并填写游客信息提交订单,在支付页完成模拟支付并提示成功,最后在我的订单中按状态筛选查看详情并在符合规则时取消订单。

5.1.3 小程序页面结构
小程序与 Portal 在业务能力上保持一致,主要差异在于交互形态更偏移动端。
小程序通常采用 TabBar(首页/景区/订单/我的)作为主导航,并通过页面跳转覆盖景区详情、门票详情、下单、订单详情与核销码展示等业务页面。
小程序端建议将请求封装在 utils/request.js 中,统一处理 Loading 与异常提示,并在 401 时清理 token 并跳转登录页,以降低页面重复代码。

5.2 核心业务模块设计与实现
5.2.1 登录鉴权与统一 Token 处理
登录接口由 AuthController 提供:POST /api/auth/login。登录成功后返回 token 与角色信息,前端保存 token 并在后续请求头中携带。
#mermaid-svg-Hj9W4rIn5LJLGs7j{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Hj9W4rIn5LJLGs7j .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Hj9W4rIn5LJLGs7j .error-icon{fill:#552222;}#mermaid-svg-Hj9W4rIn5LJLGs7j .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Hj9W4rIn5LJLGs7j .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Hj9W4rIn5LJLGs7j .marker.cross{stroke:#333333;}#mermaid-svg-Hj9W4rIn5LJLGs7j svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Hj9W4rIn5LJLGs7j p{margin:0;}#mermaid-svg-Hj9W4rIn5LJLGs7j .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Hj9W4rIn5LJLGs7j .cluster-label text{fill:#333;}#mermaid-svg-Hj9W4rIn5LJLGs7j .cluster-label span{color:#333;}#mermaid-svg-Hj9W4rIn5LJLGs7j .cluster-label span p{background-color:transparent;}#mermaid-svg-Hj9W4rIn5LJLGs7j .label text,#mermaid-svg-Hj9W4rIn5LJLGs7j span{fill:#333;color:#333;}#mermaid-svg-Hj9W4rIn5LJLGs7j .node rect,#mermaid-svg-Hj9W4rIn5LJLGs7j .node circle,#mermaid-svg-Hj9W4rIn5LJLGs7j .node ellipse,#mermaid-svg-Hj9W4rIn5LJLGs7j .node polygon,#mermaid-svg-Hj9W4rIn5LJLGs7j .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Hj9W4rIn5LJLGs7j .rough-node .label text,#mermaid-svg-Hj9W4rIn5LJLGs7j .node .label text,#mermaid-svg-Hj9W4rIn5LJLGs7j .image-shape .label,#mermaid-svg-Hj9W4rIn5LJLGs7j .icon-shape .label{text-anchor:middle;}#mermaid-svg-Hj9W4rIn5LJLGs7j .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Hj9W4rIn5LJLGs7j .rough-node .label,#mermaid-svg-Hj9W4rIn5LJLGs7j .node .label,#mermaid-svg-Hj9W4rIn5LJLGs7j .image-shape .label,#mermaid-svg-Hj9W4rIn5LJLGs7j .icon-shape .label{text-align:center;}#mermaid-svg-Hj9W4rIn5LJLGs7j .node.clickable{cursor:pointer;}#mermaid-svg-Hj9W4rIn5LJLGs7j .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Hj9W4rIn5LJLGs7j .arrowheadPath{fill:#333333;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Hj9W4rIn5LJLGs7j .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hj9W4rIn5LJLGs7j .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Hj9W4rIn5LJLGs7j .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hj9W4rIn5LJLGs7j .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Hj9W4rIn5LJLGs7j .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Hj9W4rIn5LJLGs7j .cluster text{fill:#333;}#mermaid-svg-Hj9W4rIn5LJLGs7j .cluster span{color:#333;}#mermaid-svg-Hj9W4rIn5LJLGs7j div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Hj9W4rIn5LJLGs7j .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Hj9W4rIn5LJLGs7j rect.text{fill:none;stroke-width:0;}#mermaid-svg-Hj9W4rIn5LJLGs7j .icon-shape,#mermaid-svg-Hj9W4rIn5LJLGs7j .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hj9W4rIn5LJLGs7j .icon-shape p,#mermaid-svg-Hj9W4rIn5LJLGs7j .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Hj9W4rIn5LJLGs7j .icon-shape rect,#mermaid-svg-Hj9W4rIn5LJLGs7j .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hj9W4rIn5LJLGs7j .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Hj9W4rIn5LJLGs7j .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Hj9W4rIn5LJLGs7j :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
输入用户名密码
POST /api/auth/login
校验成功?
返回错误提示
签发JWT token
前端存储token
后续请求注入Authorization
图5.1 登录与鉴权链路
后端拦截器 AuthInterceptor 在 preHandle 中解析 token 并写入 request:
request.setAttribute("userId", jwtUtils.getUserId(token));
request.setAttribute("username", jwtUtils.getUsername(token));
request.setAttribute("role", jwtUtils.getRole(token));
该方式使 Controller 能够直接读取 userId/role 进行数据隔离与权限判断,降低各业务模块对鉴权实现细节的耦合。



5.2.2 景区管理与热门推荐
景区管理由 ScenicController 提供,核心能力包括分页查询、启用禁用、设置热门等。热门景区用于游客端首页展示,提升平台的推荐能力。
景区管理相关接口包括管理端分页 GET /api/scenic/page、游客端启用景区列表 GET /api/scenic/list 以及设置热门 PUT /api/scenic/{id}/hot?isHot=1。
在实现上,SELLER 访问景区分页时,后端会直接返回绑定景区数据,避免售票员查看其他景区。


5.2.3 门票管理:上下架与库存
门票管理由 TicketController 提供,支持分页查询、增删改、上下架、按景区查询门票列表等。游客端只展示上架门票(status=1),下架门票不可购买。

#mermaid-svg-B9EgMy1FK2q3Dw0x{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-B9EgMy1FK2q3Dw0x .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-B9EgMy1FK2q3Dw0x .error-icon{fill:#552222;}#mermaid-svg-B9EgMy1FK2q3Dw0x .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-B9EgMy1FK2q3Dw0x .marker{fill:#333333;stroke:#333333;}#mermaid-svg-B9EgMy1FK2q3Dw0x .marker.cross{stroke:#333333;}#mermaid-svg-B9EgMy1FK2q3Dw0x svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-B9EgMy1FK2q3Dw0x p{margin:0;}#mermaid-svg-B9EgMy1FK2q3Dw0x .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-B9EgMy1FK2q3Dw0x .cluster-label text{fill:#333;}#mermaid-svg-B9EgMy1FK2q3Dw0x .cluster-label span{color:#333;}#mermaid-svg-B9EgMy1FK2q3Dw0x .cluster-label span p{background-color:transparent;}#mermaid-svg-B9EgMy1FK2q3Dw0x .label text,#mermaid-svg-B9EgMy1FK2q3Dw0x span{fill:#333;color:#333;}#mermaid-svg-B9EgMy1FK2q3Dw0x .node rect,#mermaid-svg-B9EgMy1FK2q3Dw0x .node circle,#mermaid-svg-B9EgMy1FK2q3Dw0x .node ellipse,#mermaid-svg-B9EgMy1FK2q3Dw0x .node polygon,#mermaid-svg-B9EgMy1FK2q3Dw0x .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-B9EgMy1FK2q3Dw0x .rough-node .label text,#mermaid-svg-B9EgMy1FK2q3Dw0x .node .label text,#mermaid-svg-B9EgMy1FK2q3Dw0x .image-shape .label,#mermaid-svg-B9EgMy1FK2q3Dw0x .icon-shape .label{text-anchor:middle;}#mermaid-svg-B9EgMy1FK2q3Dw0x .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-B9EgMy1FK2q3Dw0x .rough-node .label,#mermaid-svg-B9EgMy1FK2q3Dw0x .node .label,#mermaid-svg-B9EgMy1FK2q3Dw0x .image-shape .label,#mermaid-svg-B9EgMy1FK2q3Dw0x .icon-shape .label{text-align:center;}#mermaid-svg-B9EgMy1FK2q3Dw0x .node.clickable{cursor:pointer;}#mermaid-svg-B9EgMy1FK2q3Dw0x .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-B9EgMy1FK2q3Dw0x .arrowheadPath{fill:#333333;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-B9EgMy1FK2q3Dw0x .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-B9EgMy1FK2q3Dw0x .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-B9EgMy1FK2q3Dw0x .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-B9EgMy1FK2q3Dw0x .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-B9EgMy1FK2q3Dw0x .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-B9EgMy1FK2q3Dw0x .cluster text{fill:#333;}#mermaid-svg-B9EgMy1FK2q3Dw0x .cluster span{color:#333;}#mermaid-svg-B9EgMy1FK2q3Dw0x div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-B9EgMy1FK2q3Dw0x .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-B9EgMy1FK2q3Dw0x rect.text{fill:none;stroke-width:0;}#mermaid-svg-B9EgMy1FK2q3Dw0x .icon-shape,#mermaid-svg-B9EgMy1FK2q3Dw0x .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-B9EgMy1FK2q3Dw0x .icon-shape p,#mermaid-svg-B9EgMy1FK2q3Dw0x .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-B9EgMy1FK2q3Dw0x .icon-shape rect,#mermaid-svg-B9EgMy1FK2q3Dw0x .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-B9EgMy1FK2q3Dw0x .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-B9EgMy1FK2q3Dw0x .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-B9EgMy1FK2q3Dw0x :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
售票员新增/编辑门票
设置库存与有效期
PUT 上下架 status
游客端查询门票列表
status=1?
不展示
允许下单
图5.2 门票生命周期与可见性
5.2.4 游客下单与库存扣减
游客下单入口在 PortalController:POST /api/portal/order。下单动作的关键在于:
下单过程需要校验门票上架与库存、计算订单金额(单价 * 数量)、生成唯一订单号 order_no、将订单状态置为待支付(0),并完成库存扣减 stock -= quantity 与销量累加 sold_count += quantity。
为了使订单链路更贴近真实业务,本系统在下单阶段对游客信息采取“主游客字段 + 可扩展游客表”的折中方案:订单表保留 visitor_name、visitor_phone、visitor_id_card 作为主游客信息,便于订单列表快速展示与核销时快速定位,同时通过 order_visitor 表为“一单多游客”提供扩展能力。这样的设计既能满足论文中对扩展性的描述,也能避免一开始就引入过于复杂的数据结构影响核心流程的可用性。
在金额计算方面,系统采用单价与数量的乘积作为订单总金额,并将关键金额字段写入订单表以便后续统计与对账展示。即便在模拟支付场景下,保留金额字段也能为统计报表提供可靠的数据来源,并使系统具备对接真实支付的基础条件。

#mermaid-svg-powZ6kzwrqdAcnnL{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-powZ6kzwrqdAcnnL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-powZ6kzwrqdAcnnL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-powZ6kzwrqdAcnnL .error-icon{fill:#552222;}#mermaid-svg-powZ6kzwrqdAcnnL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-powZ6kzwrqdAcnnL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-powZ6kzwrqdAcnnL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-powZ6kzwrqdAcnnL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-powZ6kzwrqdAcnnL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-powZ6kzwrqdAcnnL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-powZ6kzwrqdAcnnL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-powZ6kzwrqdAcnnL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-powZ6kzwrqdAcnnL .marker.cross{stroke:#333333;}#mermaid-svg-powZ6kzwrqdAcnnL svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-powZ6kzwrqdAcnnL p{margin:0;}#mermaid-svg-powZ6kzwrqdAcnnL .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-powZ6kzwrqdAcnnL .cluster-label text{fill:#333;}#mermaid-svg-powZ6kzwrqdAcnnL .cluster-label span{color:#333;}#mermaid-svg-powZ6kzwrqdAcnnL .cluster-label span p{background-color:transparent;}#mermaid-svg-powZ6kzwrqdAcnnL .label text,#mermaid-svg-powZ6kzwrqdAcnnL span{fill:#333;color:#333;}#mermaid-svg-powZ6kzwrqdAcnnL .node rect,#mermaid-svg-powZ6kzwrqdAcnnL .node circle,#mermaid-svg-powZ6kzwrqdAcnnL .node ellipse,#mermaid-svg-powZ6kzwrqdAcnnL .node polygon,#mermaid-svg-powZ6kzwrqdAcnnL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-powZ6kzwrqdAcnnL .rough-node .label text,#mermaid-svg-powZ6kzwrqdAcnnL .node .label text,#mermaid-svg-powZ6kzwrqdAcnnL .image-shape .label,#mermaid-svg-powZ6kzwrqdAcnnL .icon-shape .label{text-anchor:middle;}#mermaid-svg-powZ6kzwrqdAcnnL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-powZ6kzwrqdAcnnL .rough-node .label,#mermaid-svg-powZ6kzwrqdAcnnL .node .label,#mermaid-svg-powZ6kzwrqdAcnnL .image-shape .label,#mermaid-svg-powZ6kzwrqdAcnnL .icon-shape .label{text-align:center;}#mermaid-svg-powZ6kzwrqdAcnnL .node.clickable{cursor:pointer;}#mermaid-svg-powZ6kzwrqdAcnnL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-powZ6kzwrqdAcnnL .arrowheadPath{fill:#333333;}#mermaid-svg-powZ6kzwrqdAcnnL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-powZ6kzwrqdAcnnL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-powZ6kzwrqdAcnnL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-powZ6kzwrqdAcnnL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-powZ6kzwrqdAcnnL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-powZ6kzwrqdAcnnL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-powZ6kzwrqdAcnnL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-powZ6kzwrqdAcnnL .cluster text{fill:#333;}#mermaid-svg-powZ6kzwrqdAcnnL .cluster span{color:#333;}#mermaid-svg-powZ6kzwrqdAcnnL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-powZ6kzwrqdAcnnL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-powZ6kzwrqdAcnnL rect.text{fill:none;stroke-width:0;}#mermaid-svg-powZ6kzwrqdAcnnL .icon-shape,#mermaid-svg-powZ6kzwrqdAcnnL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-powZ6kzwrqdAcnnL .icon-shape p,#mermaid-svg-powZ6kzwrqdAcnnL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-powZ6kzwrqdAcnnL .icon-shape rect,#mermaid-svg-powZ6kzwrqdAcnnL .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-powZ6kzwrqdAcnnL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-powZ6kzwrqdAcnnL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-powZ6kzwrqdAcnnL :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
选择门票/日期/人数
提交订单
库存是否充足?
返回库存不足
创建订单 status=待支付
库存扣减 stock-=quantity
返回订单信息
图5.3 下单与库存扣减流程
为了保证一致性,建议在 Service 层使用事务组织“创建订单 + 扣减库存”的写入操作;若扣减库存失败,则回滚订单创建,避免产生无效订单数据。
5.2.5 模拟支付与订单状态机
支付接口:POST /api/portal/order/{id}/pay。系统将支付流程简化为模拟支付,但仍保留状态机约束:
状态机约束要求仅允许从 待支付(0) 推进为 已支付(1),对已支付订单的重复支付进行幂等返回,并在支付成功时写入 pay_time。
#mermaid-svg-Q5428HEuJa4ShKGg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Q5428HEuJa4ShKGg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Q5428HEuJa4ShKGg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Q5428HEuJa4ShKGg .error-icon{fill:#552222;}#mermaid-svg-Q5428HEuJa4ShKGg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Q5428HEuJa4ShKGg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Q5428HEuJa4ShKGg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Q5428HEuJa4ShKGg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Q5428HEuJa4ShKGg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Q5428HEuJa4ShKGg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Q5428HEuJa4ShKGg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Q5428HEuJa4ShKGg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Q5428HEuJa4ShKGg .marker.cross{stroke:#333333;}#mermaid-svg-Q5428HEuJa4ShKGg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Q5428HEuJa4ShKGg p{margin:0;}#mermaid-svg-Q5428HEuJa4ShKGg defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-Q5428HEuJa4ShKGg g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-Q5428HEuJa4ShKGg g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-Q5428HEuJa4ShKGg g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-Q5428HEuJa4ShKGg g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-Q5428HEuJa4ShKGg g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-Q5428HEuJa4ShKGg .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-Q5428HEuJa4ShKGg .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-Q5428HEuJa4ShKGg .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-Q5428HEuJa4ShKGg .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Q5428HEuJa4ShKGg .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-Q5428HEuJa4ShKGg .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-Q5428HEuJa4ShKGg .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-Q5428HEuJa4ShKGg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q5428HEuJa4ShKGg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Q5428HEuJa4ShKGg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q5428HEuJa4ShKGg .edgeLabel .label text{fill:#333;}#mermaid-svg-Q5428HEuJa4ShKGg .label div .edgeLabel{color:#333;}#mermaid-svg-Q5428HEuJa4ShKGg .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-Q5428HEuJa4ShKGg .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-Q5428HEuJa4ShKGg .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-Q5428HEuJa4ShKGg .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-Q5428HEuJa4ShKGg .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-Q5428HEuJa4ShKGg .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Q5428HEuJa4ShKGg .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Q5428HEuJa4ShKGg #statediagram-barbEnd{fill:#333333;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Q5428HEuJa4ShKGg .cluster-label,#mermaid-svg-Q5428HEuJa4ShKGg .nodeLabel{color:#131300;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-Q5428HEuJa4ShKGg .note-edge{stroke-dasharray:5;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-note text{fill:black;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram-note .nodeLabel{color:black;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagram .edgeLabel{color:red;}#mermaid-svg-Q5428HEuJa4ShKGg #dependencyStart,#mermaid-svg-Q5428HEuJa4ShKGg #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-Q5428HEuJa4ShKGg .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Q5428HEuJa4ShKGg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
创建订单
模拟支付
核销
取消
取消(未核销)
PendingPay
Paid
Verified
Canceled
图5.4 订单状态机
5.2.6 核销验票:核销码与幂等控制
核销入口:POST /api/order/verify,由售票员在后台核销页面输入核销码完成验票。核销校验要点:
核销校验要求核销码对应订单必须存在且属于售票员绑定景区,并且订单状态必须为已支付(1),核销成功后更新状态为已核销(2)并写入 verify_time。

#mermaid-svg-9YIIwZGUFTeuiCTN{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9YIIwZGUFTeuiCTN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9YIIwZGUFTeuiCTN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9YIIwZGUFTeuiCTN .error-icon{fill:#552222;}#mermaid-svg-9YIIwZGUFTeuiCTN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9YIIwZGUFTeuiCTN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9YIIwZGUFTeuiCTN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9YIIwZGUFTeuiCTN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9YIIwZGUFTeuiCTN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9YIIwZGUFTeuiCTN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9YIIwZGUFTeuiCTN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9YIIwZGUFTeuiCTN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9YIIwZGUFTeuiCTN .marker.cross{stroke:#333333;}#mermaid-svg-9YIIwZGUFTeuiCTN svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9YIIwZGUFTeuiCTN p{margin:0;}#mermaid-svg-9YIIwZGUFTeuiCTN .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-9YIIwZGUFTeuiCTN .cluster-label text{fill:#333;}#mermaid-svg-9YIIwZGUFTeuiCTN .cluster-label span{color:#333;}#mermaid-svg-9YIIwZGUFTeuiCTN .cluster-label span p{background-color:transparent;}#mermaid-svg-9YIIwZGUFTeuiCTN .label text,#mermaid-svg-9YIIwZGUFTeuiCTN span{fill:#333;color:#333;}#mermaid-svg-9YIIwZGUFTeuiCTN .node rect,#mermaid-svg-9YIIwZGUFTeuiCTN .node circle,#mermaid-svg-9YIIwZGUFTeuiCTN .node ellipse,#mermaid-svg-9YIIwZGUFTeuiCTN .node polygon,#mermaid-svg-9YIIwZGUFTeuiCTN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9YIIwZGUFTeuiCTN .rough-node .label text,#mermaid-svg-9YIIwZGUFTeuiCTN .node .label text,#mermaid-svg-9YIIwZGUFTeuiCTN .image-shape .label,#mermaid-svg-9YIIwZGUFTeuiCTN .icon-shape .label{text-anchor:middle;}#mermaid-svg-9YIIwZGUFTeuiCTN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9YIIwZGUFTeuiCTN .rough-node .label,#mermaid-svg-9YIIwZGUFTeuiCTN .node .label,#mermaid-svg-9YIIwZGUFTeuiCTN .image-shape .label,#mermaid-svg-9YIIwZGUFTeuiCTN .icon-shape .label{text-align:center;}#mermaid-svg-9YIIwZGUFTeuiCTN .node.clickable{cursor:pointer;}#mermaid-svg-9YIIwZGUFTeuiCTN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9YIIwZGUFTeuiCTN .arrowheadPath{fill:#333333;}#mermaid-svg-9YIIwZGUFTeuiCTN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9YIIwZGUFTeuiCTN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9YIIwZGUFTeuiCTN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9YIIwZGUFTeuiCTN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9YIIwZGUFTeuiCTN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9YIIwZGUFTeuiCTN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9YIIwZGUFTeuiCTN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9YIIwZGUFTeuiCTN .cluster text{fill:#333;}#mermaid-svg-9YIIwZGUFTeuiCTN .cluster span{color:#333;}#mermaid-svg-9YIIwZGUFTeuiCTN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9YIIwZGUFTeuiCTN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9YIIwZGUFTeuiCTN rect.text{fill:none;stroke-width:0;}#mermaid-svg-9YIIwZGUFTeuiCTN .icon-shape,#mermaid-svg-9YIIwZGUFTeuiCTN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9YIIwZGUFTeuiCTN .icon-shape p,#mermaid-svg-9YIIwZGUFTeuiCTN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9YIIwZGUFTeuiCTN .icon-shape rect,#mermaid-svg-9YIIwZGUFTeuiCTN .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9YIIwZGUFTeuiCTN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9YIIwZGUFTeuiCTN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9YIIwZGUFTeuiCTN :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
否
是
输入核销码
查询订单
属于本景区?
越权/无效
状态=已支付?
不可核销
更新状态=已核销
写入核销时间
图5.5 核销流程
幂等处理建议:
幂等处理建议为:如果订单已核销则直接返回“已核销”并保持数据不变,同时对并发核销可采用“带状态条件的更新”确保同一订单只会被成功核销一次。
核销场景在票务业务中具有“线下操作不确定性更强”的特点,例如售票员可能在网络抖动时重复提交,或在高峰期多次扫描同一核销码;因此核销接口的幂等性不仅是技术要求,也是业务风险控制的重要手段。本系统通过状态机约束与条件更新的思路,使核销从“可重复执行的写操作”转变为“只能成功一次的状态推进”,从而避免重复核销导致的统计口径错误与售后纠纷。
另外,核销验票通常需要校验景区边界,本系统在设计上强调售票员与景区的一对一绑定关系,使核销校验逻辑能够简单明确;当系统扩展到“一景区多售票员”时,也可以将核销权限从用户表的 scenic_id 扩展为更细粒度的权限表或岗位表,但不改变订单归属的核心口径。
5.2.7 订单查询、取消与数据隔离
订单查询与取消相关接口主要包括管理端订单分页 GET /api/order/page、游客端我的订单 GET /api/portal/orders 以及游客取消订单 PUT /api/portal/order/{id}/cancel。
数据隔离策略如下:
数据隔离策略为游客端仅允许查询 user_id 等于自己的订单、售票员端仅允许查询 scenic_id 等于绑定景区的订单、管理员端可查询全省订单。
取消订单是票务系统中与一致性高度相关的动作,因为取消不仅会改变订单状态,还会影响可售库存与后续统计口径。本系统在取消逻辑上应遵循两个原则:第一是状态机约束,即已核销订单不可取消,已取消订单重复取消应返回幂等结果;第二是库存回滚的正确性,即取消成功后应将库存按购买数量回滚,并确保回滚动作不会被重复执行。通过在 Service 层集中处理取消逻辑,并在必要时采用事务边界或条件更新,可以有效降低因并发导致的库存不一致风险。
对于管理端取消订单(管理员权限)与游客端取消订单(用户权限),虽然入口不同,但本质上应复用同一套业务规则,从而保证取消原因记录、状态推进与库存回滚的一致口径;这样既便于论文描述“统一业务规则”,也便于后续扩展退款、超时自动取消等能力。
取消订单时需遵循状态机:一般只允许取消“待支付/已支付但未核销”的订单,并记录取消原因;取消成功后需回滚库存。
5.2.8 公告发布与门户展示
公告由 AnnouncementController 管理端维护,游客端通过 PortalController 获取发布公告列表:
公告相关接口包括管理端分页 GET /api/announcement/page、发布公告 PUT /api/announcement/{id}/publish 与游客端分页 GET /api/portal/announcements。
公告模块用于平台通知、系统维护公告等,增强平台的运营属性。
从平台运营角度看,公告模块的价值不仅在于信息发布本身,还在于形成“平台规则与用户沟通”的稳定渠道,例如节假日开放时间调整、景区临时限流、系统维护窗口等信息都可以通过公告触达用户;在论文表述中,公告模块能够体现系统并非单纯交易 CRUD,而是具备一定运营属性与服务属性,从而增强课题的现实意义。

5.2.9 统计报表与可视化
系统提供统计接口用于后台运营看板:
统计接口主要包括 GET /api/order/statistics、GET /api/order/weekly-stats 与 GET /api/order/status-distribution。
统计接口同样遵循数据隔离,售票员仅能看到本景区数据,管理员可看到全省数据。前端可使用 ECharts 将“趋势、分布、排行”等以图表呈现,提升答辩展示效果。
5.3 关键接口设计说明(按论文格式)
5.3.1 登录接口
登录接口的接口路径为 POST /api/auth/login(请求方式 POST),其请求参数如下表所示。
| username | String | 用户名 |
| password | String | 密码 |
返回参数(部分)如下表所示。
| token | String | JWT token |
| role | String | 角色 |
| scenicId | Long | 售票员绑定景区ID |
5.3.2 门票分页接口
门票分页接口的接口路径为 GET /api/ticket/page(请求方式 GET),其说明为售票员只返回自己景区门票而管理员可查询指定景区。
5.3.3 游客下单接口
游客下单接口的接口路径为 POST /api/portal/order(请求方式 POST),并且鉴权要求为需要 token。
| ticketId | Long | 门票ID |
| quantity | Integer | 数量 |
| visitDate | String | 使用日期 |
| visitors | Array | 游客信息列表 |
5.3.4 核销接口
核销接口为 POST /api/order/verify(请求方式 POST),用于售票员核销验票并需校验景区边界。
5.4 系统测试(简要)
5.4.1 测试环境
测试环境为 JDK 1.8、后端 Spring Boot 2.5.15、数据库 MySQL 8.x、前端 Vue3 + Vite,小程序使用微信开发者工具。
5.4.2 功能测试用例设计
| TC-01 | 登录成功 | 用户存在且启用 | 正确账号密码登录 | 返回 token 并进入对应端 |
| TC-02 | 下单扣减库存 | 门票上架且库存足够 | 下单数量<=库存 | 生成待支付订单并扣减库存 |
| TC-03 | 库存不足拦截 | 库存不足 | 下单数量>库存 | 返回失败且不生成订单 |
| TC-04 | 支付状态推进 | 存在待支付订单 | 调用支付接口 | 状态变为已支付,写入 pay_time |
| TC-05 | 核销成功 | 已支付且属于本景区 | 输入核销码核销 | 状态变为已核销,写入 verify_time |
| TC-06 | 重复核销幂等 | 订单已核销 | 再次核销 | 返回已核销提示,数据不变 |
在测试组织上,本系统优先覆盖“交易闭环”相关的关键路径,因为这部分最容易在答辩演示中被重点检查,且一旦出错会直接影响系统可运行性与业务可信度。所谓关键路径主要包括登录鉴权、门票上架与库存设置、游客下单与库存扣减、模拟支付状态推进、核销验票状态推进以及订单列表与统计口径的一致性。
对于下单与库存扣减测试,除了验证“库存足够能够成功下单”,还需要验证“库存不足时能够拒绝下单并保持库存不变”,并在多次下单后检查库存与销量的变化是否与订单数量一致。对于取消订单测试,需要验证取消动作仅在允许状态下生效,并且取消后库存能够按数量回滚,从而保证资源可再次售卖。
对于核销测试,除了验证核销成功后的状态变化,还需要验证重复核销的幂等性,因为核销场景可能存在售票员重复提交或网络抖动导致的重复请求;系统应当在重复核销时保持订单状态不变并返回明确提示,从而避免核销数据被重复写入。
对于数据隔离测试,需要分别以管理员与售票员身份登录后台,验证售票员在订单分页与统计接口中只能看到绑定景区的数据,并且无法通过参数修改来访问其他景区数据;管理员则应当能够按条件查询全省数据,从而体现平台监管视角。
对于游客端测试,需要验证未登录时访问下单、支付、我的订单等接口会返回未授权提示,并且前端能够正确引导用户完成登录;登录后再进行购票流程,则应当能够完整走通“下单—支付—查看订单”的闭环。
5.4.3 边界与异常测试(要点)
边界与异常测试要点为未登录访问受保护接口返回 401、取消订单需符合状态机且已核销订单不可取消,并验证售票员越权访问其他景区数据时返回空数据或错误提示。
在测试结论层面,本系统的测试目标不仅是“功能能跑通”,还应强调关键业务指标的可解释性,例如库存变化与订单数量的一致、核销状态变化的可追溯、以及统计接口返回值的稳定性。对于答辩演示而言,最容易被追问的部分通常是“数据是否一致”和“权限是否严格”,因此在测试阶段应主动准备管理员与售票员两套账号并用同一组订单数据进行对比验证,以证明数据隔离确实由后端保障。
同时,在弱网或异常场景下,前端与小程序的交互也需要可接受的用户体验,典型表现为请求失败时能够给出明确提示并允许用户重试,而不是出现空白页面或无响应。虽然该部分不属于核心业务逻辑,但在论文中补充说明可以增强“系统可用性”的论证力度,从而使整体内容更加完整。
综合测试结果表明,本系统在教学规模与常见访问压力下能够保持稳定运行,并且在权限隔离、状态机推进与库存一致性方面具备明确的业务约束与可解释性;同时通过统一接口分组与多端复用,减少了 Portal 与小程序之间的差异点,使答辩演示时能够在不同端重复验证同一条业务链路,从而增强系统实现的可信度与完整性。
第六章 总结与展望
6.1 系统成果总结
本系统完成了“后端 + 管理端 + 游客门户 + 小程序”的全端协同,实现了旅游票务的关键业务闭环:
系统通过 JWT 无状态认证与拦截器解析实现统一鉴权以保证多端一致安全边界,通过 ADMIN/SELLER/USER 三角色与 scenic_id 绑定实现数据隔离,并围绕景区管理、门票上架、游客下单、模拟支付、核销验票、订单查询与统计形成核心业务闭环,同时数据库设计覆盖用户、景区、门票、订单、游客信息、公告等核心对象并支持索引与逻辑删除。
系统整体结构清晰、功能完整,能够支撑毕业设计演示与论文撰写。
6.2 不足与改进方向
支付与退款目前为模拟实现,后续可对接真实支付并完善回调验签、对账与退款机制,同时在高并发下单场景引入乐观锁或条件更新等方式强化库存一致性以降低超卖风险,权限体系也可进一步细化为菜单级、接口级与数据级权限并增加审计日志,此外可增强隐私保护并对手机号、身份证号等敏感字段进行脱敏展示与访问控制。
6.3 未来扩展展望
未来可引入消息通知(下单、支付、核销等节点通过短信或订阅消息通知游客)、增加营销能力(优惠券、满减、团购与联票等)、增强风控能力(限流、防刷票、异常订单识别),并对统计可视化进行升级以按省/市/景区维度构建运营大屏并支持多维钻取分析。
解释性;同时通过统一接口分组与多端复用,减少了 Portal 与小程序之间的差异点,使答辩演示时能够在不同端重复验证同一条业务链路,从而增强系统实现的可信度与完整性。
第六章 总结与展望
6.1 系统成果总结
本系统完成了“后端 + 管理端 + 游客门户 + 小程序”的全端协同,实现了旅游票务的关键业务闭环:
系统通过 JWT 无状态认证与拦截器解析实现统一鉴权以保证多端一致安全边界,通过 ADMIN/SELLER/USER 三角色与 scenic_id 绑定实现数据隔离,并围绕景区管理、门票上架、游客下单、模拟支付、核销验票、订单查询与统计形成核心业务闭环,同时数据库设计覆盖用户、景区、门票、订单、游客信息、公告等核心对象并支持索引与逻辑删除。
系统整体结构清晰、功能完整,能够支撑毕业设计演示与论文撰写。
6.2 不足与改进方向
支付与退款目前为模拟实现,后续可对接真实支付并完善回调验签、对账与退款机制,同时在高并发下单场景引入乐观锁或条件更新等方式强化库存一致性以降低超卖风险,权限体系也可进一步细化为菜单级、接口级与数据级权限并增加审计日志,此外可增强隐私保护并对手机号、身份证号等敏感字段进行脱敏展示与访问控制。
6.3 未来扩展展望
未来可引入消息通知(下单、支付、核销等节点通过短信或订阅消息通知游客)、增加营销能力(优惠券、满减、团购与联票等)、增强风控能力(限流、防刷票、异常订单识别),并对统计可视化进行升级以按省/市/景区维度构建运营大屏并支持多维钻取分析。





