ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Springboot旅游管理系统:从源码部署到毕业设计全流程解析

2026/10/6 17:01:01 拓冰建站 浏览量
Springboot旅游管理系统:从源码部署到毕业设计全流程解析 最近帮好几个朋友看过 Springboot 旅游管理系统源码这个类型的项目在毕业设计和课程设计里真的非常常见。说句实在话压缩包里的程序、数据库脚本、部署文档、论文模板基本都是全的但多数人拿到手之后第一脚就踩坑——要么数据库连不上要么版本对不上要么启动起来之后页面一片空白。旅游管理系统听起来不算大但背后涉及用户、景点、线路、订单、评论、收藏一堆业务再叠加 Spring Boot 后端和前端页面的配合第一次接触的人确实容易卡住。这篇东西不是我凭空在这里讲原理而是把这类项目从业务梳理、技术选型、数据库设计、核心模块实现、调试部署到论文整理这一整条链路重新捋一遍。适合的人群有两类第一类是刚拿到 Springboot 旅游管理系统源码但还没跑起来的人第二类是准备自己从零写一个类似系统、但不知道从哪下手的同学。我会尽量按一个实际开发者的操作顺序来讲每一步为什么这么做、常见怎么翻车、怎么快速解决都展开聊。1. 先把旅游管理系统的业务闭环盘清楚角色、主线和功能清单1.1 系统里有哪两类角色四条核心业务主线是什么很多人犯的第一个错误是拿到代码就急着点运行。我不是反对先跑起来但如果你连这个系统是干什么的都没弄明白后面改需求、加功能、调 bug 的时候会非常痛苦。建议先花半小时把业务逻辑盘清楚。旅游管理系统的本质是把“游客找景点、选线路、订门票酒店、游玩后评价”这件事搬到线上。角色一般就两类管理员维护景点、线路、酒店等基础数据处理订单和评论管理用户。普通用户注册登录、浏览景点和线路、搜索筛选、下单预订、收藏点赞、发表评论。围绕这两个角色业务主线可以整理成四条内容线管理员录入景点、线路、酒店信息用户在前台浏览和搜索。交易线用户对景点门票、线路或酒店下单订单状态从待处理流转到已确认、已完成或已取消。互动线用户对景点收藏、点赞、评论、打分管理员可以回复评论。管理线管理员对全部数据进行增删改查以及基础的统计和上下架控制。这四条线一清楚后面数据库表怎么设计、Controller 怎么拆分、前端页面怎么组织基本上就是顺水推舟的事。我见过很多代码本身没有任何问题但因为开发之前没把业务想清楚导致功能对不上需求答辩时被老师一问就露馅。1.2 功能清单怎么列才不返工我建议拿到项目之后先按模块、角色、功能点、核心表这四个维度列一张功能清单。这张清单既是你看代码的索引也是后面写论文时需求分析章节的底稿。模块角色功能点核心表登录注册用户/管理员注册、登录、退出sys_user景点管理管理员景点增删改查、上下架、分类维护scenic_spot景点展示用户分页列表、按名称/城市筛选、详情查看scenic_spot线路管理管理员线路增删改查、关联景点、价格维护travel_route订单管理用户/管理员创建订单、状态流转、后台确认/取消order_info评论管理用户/管理员发布评论、回复、删除comment收藏管理用户收藏/取消收藏、我的收藏列表favorite统计概览管理员景点数量、订单数量、简单趋势聚合查询表格列完之后再对照源码里的 Controller 和 Service你会发现绝大多数接口其实就是对某一张表的增删改查外面包了一层参数校验和业务判断。有了这张表你就不会在整个项目里漫无目的地搜索“这个功能在哪”了。2. 技术选型为什么是这个组合Spring Boot 与 MyBatis Plus 的取舍2.1 这套技术栈在毕设和课设里的优势Springboot 旅游管理系统这类项目默认组合基本是后端 Spring Boot MyBatis Plus数据库 MySQL前端 Vue Element UI 或者 Thymeleaf 模板鉴权用 JWT 或 Session工具链是 Maven、IDEA、Navicat。为什么这个组合几乎是标准答案因为它每个环节都踩在“够用且省事”的点上。Spring Boot 解决了传统 SSM 项目里大量配置文件的问题内嵌 Tomcat一条java -jar就能把服务跑起来。MyBatis Plus 又比原生 MyBatis 省掉了一大堆 XML 的编写单表操作基本不用手写 SQL分页查询自带 Page 对象开发效率能提升不少。MySQL 免费、资料多绝大多数运行时报错都能在搜索引擎找到现成答案。前端如果选 Vue Element UI后台管理界面基本是靠组件拼出来的对非前端专业的开发者非常友好。当然也有同学问过要不要上 Spring Cloud、要不要加 Redis、要不要换 JPA。我的看法很直接旅游管理系统这个体量单 Spring Boot 完全够。Spring Cloud 那套微服务治理对这个项目来说纯属给自己加戏Redis 做缓存能加分但没到缺了它不行的程度JPA 也能用但 MyBatis Plus 的容错率更高出了问题好定位。2.2 分层结构的约定和工程目录不管你拿到的源码目录长成什么样我都建议按照下面这种分层思路去理解它。src/main/java/com/example/travel ├── config // 配置类拦截器、跨域、WebMvc配置 ├── controller // 接口层接收参数、返回统一结果 ├── service // 业务层核心逻辑都在这里 │ └── impl ├── mapper // MyBatis Plus 的 Mapper 接口 ├── entity // 数据库表对应的实体类 ├── dto // 前端传入的参数对象 ├── vo // 返回给前端的数据对象 ├── common // 统一返回结果、全局异常、常量 └── utils // JWT、日期、文件上传等工具类分层的核心价值在于出了问题能快速定位Controller 层报错就去 Controller 找逻辑不对去 Service 查SQL 有问题再看 Mapper。很多人代码能力和项目理解都不差但最怕遇到目录完全乱铺的源码那才叫真的无从下手。2.3 关键依赖和版本匹配pom.xml 里几个关键依赖我按比较稳的组合列一下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency我这里给的是 Spring Boot 2.7.x 的组合。特别提醒一句Spring Boot 3.x 要求 JDK 17而且包名从 javax 迁移到了 jakarta如果你拿到的源码是 Spring Boot 2.x 写的千万别图新鲜直接升 3.x不然光改 import 就能改到崩溃。MyBatis Plus 也分版本3.5.x 对 Spring Boot 2 支持最好别选错 starter。3. 数据库是系统的地基核心表的字段设计和建表细节3.1 用户表和景点表一切业务围绕它们转数据库表结构决定开发效率这句话不是空话。后端代码再漂亮如果表设计不合理联表查询、字段拼接会搞得你痛不欲生。旅游业系统里用户表和景点表是基础。用户表sys_user我建议至少包含这些字段CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码加密存储, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, email varchar(100) DEFAULT NULL COMMENT 邮箱, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint DEFAULT 1 COMMENT 角色0管理员 1普通用户, status tinyint DEFAULT 1 COMMENT 状态0禁用 1正常, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;有几个细节是很多教程不会单独讲的。密码绝对不要明文存至少 MD5 加盐有条件直接用 BCrypt。username 必须加唯一索引不然注册的时候可能出现重复账号。create_time 和 update_time 让数据库自动维护代码里就不用每次手动 set。景点表scenic_spot是旅游系统的核心资产几乎所有功能都围绕它展开CREATE TABLE scenic_spot ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 景点名称, city varchar(50) DEFAULT NULL COMMENT 所在城市, category varchar(50) DEFAULT NULL COMMENT 分类自然/人文/主题乐园等, description text COMMENT 景点简介, address varchar(200) DEFAULT NULL COMMENT 详细地址, price decimal(10,2) DEFAULT 0.00 COMMENT 门票价格, open_time varchar(100) DEFAULT NULL COMMENT 开放时间, cover_image varchar(255) DEFAULT NULL COMMENT 封面图片地址, images text COMMENT 多图地址JSON数组或逗号分隔, visit_count int DEFAULT 0 COMMENT 浏览量, status tinyint DEFAULT 1 COMMENT 状态0下架 1上架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点表;这里有一个看起来很非主流但其实很省事的做法images 字段直接用 JSON 字符串存多张图片。很多初学者为了“规范化”单独建一张景点图片子表结果查询一次景点要两次连表删除景点还要记得清理子表复杂度翻倍。对于毕设和课设体量的系统JSON 字符串存储完全够用。3.2 订单表和评论表交易链路与用户反馈订单表order_info是交易链路的中心核心字段要能回答这几个问题谁下的单买了什么花了多少钱现在什么状态CREATE TABLE order_info ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint NOT NULL COMMENT 下单用户ID, spot_id bigint DEFAULT NULL COMMENT 景点ID, route_id bigint DEFAULT NULL COMMENT 线路ID与spot_id按类型二选一, order_type tinyint DEFAULT 1 COMMENT 类型1门票 2线路 3酒店, price decimal(10,2) NOT NULL COMMENT 订单金额, quantity int DEFAULT 1 COMMENT 数量, status tinyint DEFAULT 0 COMMENT 状态0待处理 1已确认 2已完成 3已取消, real_name varchar(50) DEFAULT NULL COMMENT 联系人, phone varchar(20) DEFAULT NULL COMMENT 联系电话, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;设计订单表的常见纠结在于一个订单可能买门票也可能订线路或酒店到底建一张表还是拆成三张表务实做法是用 order_type 区分类型spot_id 和 route_id 按类型二选一。拆表看起来很规范但会让查询、统计、后台管理变得很分裂。一张订单表加类型字段是这个体量系统的最优解。评论表comment相对简单CREATE TABLE comment ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, spot_id bigint NOT NULL COMMENT 景点ID, content varchar(500) DEFAULT NULL COMMENT 评论内容, score tinyint DEFAULT 5 COMMENT 评分1-5, reply_content varchar(500) DEFAULT NULL COMMENT 管理员回复, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_spot_id (spot_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;3.3 建表时容易忽略的四个坑第一字符集。MySQL 5.7 以下默认 latin1存中文直接变问号。建表和建库统一用 utf8mb4它比 utf8 多支持 emoji一劳永逸。建库语句CREATE DATABASE travel_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二物理外键。教科书喜欢强调外键但实际项目中外键很多时候是绊脚石。删除一个景点如果订单表有引用外键会直接拦截删除操作报错信息还很含糊。我的习惯是表之间用字段逻辑关联不建物理外键。源码里如果建了外键导致删除失败先检查并去掉它。第三价格用 decimal别用 float。float 做金额计算有精度问题0.1 加 0.2 都能算出 0.30000000000000004这在账单显示上非常尴尬。decimal(10,2) 是标准做法。第四自动维护时间字段。create_time 和 update_time 两条字段建议让 MySQL 自动填充和更新代码里不要手动 set否则数据对不上会很难排查。4. 从登录到下单四个核心模块的代码实现思路4.1 登录鉴权JWT 方案还是 Session 方案先回答最关键的问题如果前后端是分离的推荐 JWT如果项目用了 Thymeleaf 模板渲染Session 完全够用。怎么判断看前端的请求是 fetch/axios 调接口还是直接页面跳转。JWT 的工作流程就是三步登录成功后基于用户信息生成 token 返回前端前端把 token 存起来每次请求放到 Authorization 头里后端写一个拦截器解析 token 并校验通过就放行不通过就返回 401。核心代码// 生成 token public String generateToken(Long userId, String role, String secret, long expire) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setSubject(String.valueOf(userId)) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } // 拦截器解析 token public Long getUserIdFromToken(String token, String secret) { Claims claims Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); return claims.get(userId, Long.class); }有一个容易被忽略但答辩容易问到的点JWT 的 secret 要放进配置文件别硬编码在代码里。硬编码了项目也能跑但这属于一眼看出来的坏习惯。另外过期时间建议设成 24 小时太短用户体验差太长不安全。4.2 景点检索与分页一个接口走天下景点列表是用户最常用、也是前台最核心的功能。多数系统需要支持按关键字搜索名称、按城市筛选、按分类筛选再加分页。用 MyBatis Plus 的 LambdaQueryWrapper 可以写得很简洁public PageScenicSpot searchSpot(String keyword, String city, Long pageNum, Long pageSize) { PageScenicSpot page new Page(pageNum, pageSize); LambdaQueryWrapperScenicSpot wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(keyword), ScenicSpot::getName, keyword) .eq(StringUtils.isNotBlank(city), ScenicSpot::getCity, city) .eq(ScenicSpot::getStatus, 1) .orderByDesc(ScenicSpot::getVisitCount); return spotMapper.selectPage(page, wrapper); }这里最关键的是 like 和 eq 前面那个 boolean 参数前端不传对应条件时这个条件自动失效不需要写一堆 if-else 拼接 wrapper。很多初学者不明白这个设计写出来的代码又长又容易错。分页参数建议固定用 pageNum 和 pageSize不要一个接口用 page、另一个接口用 current前后端对不上非常痛苦。返回结果建议统一格式data 里放 records 和 total前端表格组件直接绑定即可。4.3 订单状态流转简单状态机比谁都改状态强订单是旅游系统里业务约束最多的部分。我建议状态控制在四个以内待处理、已确认、已完成、已取消。流转规则大概是这样用户下单生成待处理订单。管理员在后台确认变成已确认。游玩结束或默认时间到改为已完成。用户取消、管理员拒绝、超时未支付变为已取消。代码里我最推荐的做法是写一个带状态校验的 transform 方法而不是让前端想传什么状态就传什么状态public boolean transformOrderStatus(Long orderId, Integer currentStatus, Integer targetStatus) { OrderInfo order orderMapper.selectById(orderId); if (order null) { throw new BusinessException(订单不存在); } if (!order.getStatus().equals(currentStatus)) { throw new BusinessException(订单状态已变化请刷新后重试); } OrderInfo update new OrderInfo(); update.setId(orderId); update.setStatus(targetStatus); return orderMapper.updateById(update) 0; }currentStatus 不是随便传的而是后端从数据库查出来的真实状态再和目标状态做比对。这样能最大程度避免两个人同时操作同一个订单导致的状态错乱。顺带提一个加分点如果旅游系统涉及票务库存比如景点门票有数量限制下单扣库存时建议用乐观锁UPDATE scenic_spot SET stock stock - 1 WHERE id #{spotId} AND stock 0;影响行数为 0 就说明库存不足。这种写法比先查后改安全得多而且答辩时提“乐观锁解决库存超卖”是很加分的点。5. 调试部署实录从本地跑通到服务器发布的全过程5.1 环境准备版本匹配是第一道坎标题里特意提到“调试部署”这个环节确实是拿到源码后卡住最多人的地方。我按自己实际操作的完整链路走一遍。本地开发环境推荐这样配软件推荐版本理由JDK1.8 或 11Spring Boot 2.x 两者兼容默认 1.8 最稳Maven3.6 以上太老版本存在依赖解析问题MySQL5.7 或 8.0推荐 8.0驱动用对应 8.x 版本IDEA2021.2 以上Spring Boot 支持已经很成熟Navicat任意新版导入 SQL、执行脚本方便第一步在 IDEA 里打开项目等 Maven 把依赖全部下载完。这一步最考验耐心网络不好或镜像源没配会长时间卡在下载界面。建议在 Maven 的 settings.xml 里配置阿里云镜像这算是国内开发的必修课。第二步找到项目里的 SQL 脚本在 Navicat 里新建数据库并执行脚本。执行之前先看脚本开头有没有 CREATE DATABASE 语句如果有数据库名必须和配置文件的连接地址保持一致。第三步修改 application.yml 里的数据库连接。重点检查 url、用户名、密码spring: datasource: url: jdbc:mysql://localhost:3306/travel_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456第四步启动 Application 主类。启动成功后会看到 Tomcat 端口打印比如 Tomcat started on port(s) 8080。5.2 配置文件里最容易踩的三个坑第一个坑数据库时区。MySQL 8.0 默认时区可能和本地不一致连接串里不写 serverTimezoneAsia/Shanghai启动会直接报时区相关的错误。这个参数必须加位置就在 url 末尾。第二个坑端口冲突。Tomcat 默认 8080如果机器上已经有一个服务占了 8080启动会报 Port already in use。解决办法是换端口或杀掉占用进程。换端口server: port: 9090Windows 查端口占用netstat -ano | findstr 8080然后taskkill /PID 进程号 /F。Linux 上对应的是lsof -i:8080和kill -9 进程号。第三个坑上传文件路径。很多旅游系统涉及图片上传代码里如果写了绝对路径比如D:/upload那只是原作者电脑的路径换电脑不改成自己的路径上传功能必定报错。拿到源码之后先在项目里全局搜一遍所有带盘符的路径全部改成自己的本地目录。5.3 打包与部署的完整命令本地跑通之后部署到服务器一般是打 jar 包。项目根目录执行mvn clean package -DskipTeststarget 目录下会生成 jar 包比如 travel-system-0.0.1.jar。上传到服务器之后后台运行nohup java -jar travel-system-0.0.1.jar --server.port8080 app.log 21 把这行命令的参数拆开解释nohup 是让程序在 SSH 断开之后继续跑 app.log 是把日志输出到文件21 是把错误输出也写进同一个文件 表示后台运行。以后想看日志随时tail -f app.log。部署前记得检查服务器上的 MySQL 是否已经建好库并导入数据。服务器密码不要求和本地一样但是改完密码必须同步改 jar 包旁边的配置文件。另外服务器防火墙要放行对应端口云服务器还要在安全组里额外加一条规则。很多人就是卡在“本地能访问服务器访问不了”这个环节。5.4 常见启动报错的排查对照表碰到报错先看控制台异常栈第一行那行基本直接告诉你问题在哪。我总结一张对照表按图索骥能解决大部分问题报错现象大概率原因处理方式Application run failed数据库连接不通检查 MySQL 是否启动、账号密码、库名Port already in use端口被占用换端口或杀进程页面能打开但没有列表数据数据库表为空或表名不匹配导入 SQL检查表名前缀登录提示密码错误加密算法不一致确认注册和登录的加密规则统一接口返回 404上下文路径或路径前缀问题检查 server.servlet.context-path静态资源加载不出来拦截器拦截了静态资源放行 static、css、js 路径6. 1万字技术文档怎么写论文结构和代码的对应关系6.1 论文的每一章都能在代码里找到对应标题里提到的“论文文档 1 万字以上”其实是这类项目的标配。很多同学拿到文档模板不知道该怎么和自己的代码对应起来。其实只要理解了论文结构和代码实现是一一映射的写起来就会顺畅很多。常规的毕业论文结构可以这么对应绪论写研究背景、选题意义、国内外现状。对应你为什么要做旅游管理系统可以落在旅游信息化、线上预订体验这些场景上。需求分析放用例图、功能需求、非功能需求。对着第 1 章的功能清单写每一条需求都要能在代码里找到实际对应。系统设计写总体架构图、模块设计、数据库设计。架构对着技术分层画数据库设计直接放表结构说明和 ER 图。系统实现放每个模块的界面截图和核心代码。对应 Controller 和 Service 层代码贴重点不要把整个类贴进去。系统测试写测试环境、测试用例、测试结果。Excel 表列清楚每个用例的步骤、预期、实际结果。总结与展望总结做了哪些工作后续还可以扩展什么功能。一个核心原则代码里实现了什么论文就写什么代码里没有的论文里别硬吹。答辩老师不一定会逐行看代码但很喜欢问“这个模块在代码里哪个位置”。答不上来比论文写得平庸严重得多。6.2 截图、测试用例和答辩前的资料准备论文里的系统截图质量比大多数人想象的重要。截图之前一定要把界面整理干净测试数据不要乱填。比如演示添加景点的功能时景点名称、城市、描述都要写正式一些一张乱七八糟的截图会让整篇论文的专业度大打折扣。测试用例表建议按这个格式准备用例编号测试项操作步骤预期结果实际结果结论TC-01用户登录输入正确用户名密码点击登录登录成功跳转首页登录成功通过TC-02用户登录输入错误密码点击登录提示密码错误提示密码错误通过TC-03景点分页查询输入关键字点击搜索返回匹配列表且分页正确符合预期通过TC-04下单流程选择门票生成订单生成待处理订单下单成功通过TC-05管理员确认订单后台点击确认状态变为已确认状态更新通过论文格式同样不能输。页边距、字体、目录、页码这些地方提交前认真检查一遍。答辩时还可以把第 5 章提到的技术细节准备成自己的亮点JWT 无状态鉴权解决分布式场景下的扩展问题、MyBatis Plus 提升单表 CRUD 效率、乐观锁避免库存超卖。这些不算重大创新但每一句都能说明你真的动了脑子而不是纯模板搬运。6.3 技术文档里更适合展开写什么如果你拿到的源码包里已经有 1 万字以上的文档别直接原样交上去一定要自己重新捋一遍。最值得展开写的地方恰恰是那些你在调试过程中真正处理过的问题。比如数据库时区问题文档里只写“连接配置见 application.yml”但你在第 5 章的排查记录里可以清晰描述没加 serverTimezone 时启动报了什么错为什么会有时区概念加上 Asia/Shanghai 后问题怎么消失。这种真实场景描述比任何抽象原理都更适合放进“关键技术问题解决”小节。再比如订单状态流转文档可以不止写“订单有四种状态”而是写清楚为什么不用乐观锁之外的做法status 字段的取值范围如何约定管理员确认与用户取消同时发生时系统如何处理。这就是从“能跑”到“懂原理”的区别也是论文写得有深度的关键。我自己实际操作中最大的体会是这类系统的代码量其实不算大真正拉开差距的往往是对业务逻辑的完整理解、对调试部署过程的复盘、以及文档和代码的一致性。把这些事情做扎实比多写一万行花架子代码有意义得多。如果你正在折腾这类 Springboot 旅游管理系统项目建议先把数据库脚本导入跑通再逐个接口跟着看一遍最后再把部署这关完整走一遍你会发现它其实没有想象中那么复杂。