Spring Boot旅游管理系统实战:从审核流程到广告位管理的关键设计 前阵子接手了一个师弟的课程设计项目标题起得很直白——基于Spring Boot的新疆旅游管理系统。乍一看这就是个典型的CRUD管理系统但真正把需求理清楚之后发现里面值得抠的细节比想象中多得多。尤其是帖子审核、话题聚合、广告位展示这些业务逻辑远比单纯写几个增删改查接口要复杂。这篇文章就把我做这个项目的完整思路和踩坑记录整理出来包括数据库设计、审核流程的实现方式、常见问题的排查思路希望能给正在做类似旅游类管理系统的朋友一些参考。说明文中所有代码和配置都基于实际项目中验证过的方案但涉及具体业务参数的场景会使用通用示例方便直接迁移到自己的项目里。1. 系统需求分析与整体设计思路1.1 这个系统到底要解决什么问题拿到“旅游管理系统”这个需求时第一步不是急着建表而是要想清楚谁在用这个系统他们要完成什么任务。和师弟一起梳理需求时我们确定了两个核心受众管理员和普通游客浏览者。管理员的痛点是旅游信息分散在各个渠道没有统一的后台来发布、审核和管理内容游客的痛点是希望在一个页面上看到分类清晰的旅游攻略、景点介绍和实用信息而不是在各个网站翻来找去。基于这个定位系统的核心功能闭环就清晰了管理员登录后台 → 发布或导入旅游帖子攻略、景点介绍等 → 设置话题分类 → 内容审核 → 前台官网页展示 → 游客浏览。其中“审核”这个动作是关键它保证了前台展示的内容是经过确认的这也符合真实运营场景中“后台发布、审核通过、前台可见”的通用规则。1.2 功能模块拆分与角色权限设计系统拆成两大部分后台管理端和前台展示端。后台管理端围绕内容生产和管理来设计包含帖子管理、话题分类管理、官网框架配置、广告位管理、审核记录管理和用户管理。前台展示端则是数据消费侧游客可以查看话题分类下的帖子列表、帖子详情、广告位展示内容等。权限控制上普通游客只能访问前台页面管理员才能访问后台接口。这里我选择了基于Spring Security JWT的方案因为前后端分离的项目用Token比用Session更适合——管理后台是单独的Vue页面通过Axios请求携带Token后端通过拦截器校验权限。这里有个经验点JWT有效期不能设置太长我一般设置2小时然后配合一个小的刷新机制这样既保证用户体验又降低了Token泄露带来的风险。2. 技术选型解析为什么是Spring Boot这一套组合2.1 后端框架选型的思考过程既然是课程设计既要体现技术深度又不能太超前导致做不出来。Spring Boot 2.7.x搭配MyBatis-Plus是最稳妥的方案。Spring Boot简化了配置流程内嵌Tomcat让部署变得容易MyBatis-Plus提供了分页插件、代码生成器、条件构造器能大幅减少SQL编写量尤其是像帖子列表这种带动态条件的查询用LambdaQueryWrapper写起来非常直观。其他关键组件选型如下权限Spring Security JWTjjwt 0.9.1用于登录认证和接口权限控制。缓存RedisSpring Data Redis用于缓存话题列表、首页推荐帖子等读多写少的数据。数据库MySQL 8.0存储业务数据。工具类Hutool简化日期处理、文件上传等操作。接口文档Knife4j基于Swagger方便前端联调和后期自测。2.2 前后端分离架构与关键依赖版本后端是一个标准的Spring Boot Maven项目按功能分包controller、service、mapper、entity、config、common等。前端我用的Vue 2 Element UI因为这套组合对课程设计来说最顺手——Element UI的表格组件和表单组件能快速搭建管理界面Vue 2的生态也足够成熟网上的解决方案一抓一大把。依赖版本这里是一个容易忽略的坑。Spring Boot 2.7.x对应Spring Security 5.7.x和MyBatis-Plus 3.5.x这几个版本配合是没问题的。但如果你把Spring Boot升到3.x那么javax.包要全部改成jakarta.MyBatis-Plus也要换适配版本很多网上找的旧代码就没法直接用了。所以说定版本这件事在一开始就要统一项目做一半再升级框架是大忌。3. 数据库设计旅游管理系统的地基3.1 核心业务表设计帖子、话题与关联关系这个系统的核心业务对象是“帖子”tourism_post它承载了景点介绍、游记攻略、美食推荐等内容。帖子的表结构要考虑到审核状态、发布时间、浏览量、封面图等字段。我最开始设计表时差点犯一个错误把话题和帖子做成多对多关系一个帖子可以属于多个话题后来在梳理需求时发现真实的业务场景中一个帖子就应该属于一个最核心的话题类别否则前台列表展示时会出现同一个帖子出现在多个分类下的困惑。最终的设计是一个帖子属于一个话题一个话题下可以包含多个帖子也就是一对多关系。表结构如下-- 话题类别表 CREATE TABLE tourism_topic ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 话题名称, description VARCHAR(255) COMMENT 话题描述, cover_image VARCHAR(255) COMMENT 话题封面图, sort_order INT DEFAULT 0 COMMENT 排序权重, status TINYINT DEFAULT 1 COMMENT 状态0-禁用1-启用, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 帖子表 CREATE TABLE tourism_post ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL COMMENT 帖子标题, summary VARCHAR(255) COMMENT 摘要, content LONGTEXT COMMENT 正文内容, cover_image VARCHAR(255) COMMENT 封面图, topic_id BIGINT NOT NULL COMMENT 所属话题ID, author VARCHAR(50) COMMENT 作者, view_count INT DEFAULT 0 COMMENT 浏览量, like_count INT DEFAULT 0 COMMENT 点赞数, audit_status TINYINT DEFAULT 0 COMMENT 审核状态0-待审核1-通过2-不通过, audit_remark VARCHAR(255) COMMENT 审核备注, status TINYINT DEFAULT 1 COMMENT 删除标记0-删除1-正常, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );这里audit_status虽然可以单独建审核记录表来跟踪每次审核的历史但考虑到课程设计的管理系统一般只需要记录最终状态所以在帖子表里直接维护一个审核状态字段就够了。如果你想要更完整的审核历史追溯那就需要一个帖子审核记录表这个我在后面会讲。3.2 审核记录表的设计为什么需要闭环帖子表里的audit_status记录了“当前处于什么状态”但如果你想知道“这个帖子是谁在什么时间审核的、审核备注是什么”就需要单独的审核记录表。这个表属于典型的审计日志表它存在的价值是让内容管理操作有迹可循这在真实运营中是必须的。CREATE TABLE tourism_post_audit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, post_id BIGINT NOT NULL COMMENT 帖子ID, auditor_id BIGINT NOT NULL COMMENT 审核人ID, action_type TINYINT DEFAULT 1 COMMENT 操作类型1-通过2-不通过3-撤回, audit_comment VARCHAR(255) COMMENT 审核意见, created_time DATETIME DEFAULT CURRENT_TIMESTAMP );在业务实现上当管理员点击“通过”按钮时系统同时执行两个操作更新帖子的audit_status1插入一条审核记录。这样一个帖子被多次驳回、多次修改再提交的历史就能被完整保留。这个设计在答辩或汇报时是一个不错的亮点——说明你考虑了数据的可追溯性。3.3 官网框架表与广告位表的实践细节“官网框架表”这个名词听起来有点抽象实际上管的是前台页面的布局配置和内容展示。我把它拆成两块导航栏配置tourism_nav和轮播图配置tourism_banner它们本质上都是展示位的管理。广告位表的设计服务于一个更实际的需求前台页面的固定位置展示运营推广内容。广告位表的核心字段如下CREATE TABLE tourism_ad_position ( id BIGINT PRIMARY KEY AUTO_INCREMENT, position_code VARCHAR(50) NOT NULL COMMENT 广告位编码HOME_TOP-首页顶部SIDE_BAR-侧边栏, ad_name VARCHAR(100) COMMENT 广告名称, ad_image VARCHAR(255) COMMENT 广告图片, ad_url VARCHAR(255) COMMENT 跳转链接, sort_order INT DEFAULT 0, status TINYINT DEFAULT 1, start_time DATETIME COMMENT 投放开始时间, end_time DATETIME COMMENT 投放结束时间, created_time DATETIME DEFAULT CURRENT_TIMESTAMP );设计这个表时我特别加了position_code字段因为前台不同位置的广告位尺寸要求不同在后台配置时给广告位一个编码前端页面就能根据编码去加载对应广告。投放时间字段让广告位在过期后自动不显示不需要人工手动下架这个细节在实际运营中能省不少事。4. 核心模块实现与踩坑实录4.1 登录认证与权限拦截的实现要点管理后台的登录认证用Spring Security JWT实现。核心逻辑是用户提交用户名密码后端校验通过后生成Token返回前端前端后续请求在Header中携带Token后端通过过滤器校验Token的合法性和有效期。具体实现分三步第一步自定义UserDetailsService从用户表加载用户信息。Service public class UserDetailsServiceImpl implements UserDetailsService { Autowired private UserMapper userMapper; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user userMapper.selectOne(new LambdaQueryWrapperUser() .eq(User::getUsername, username)); if (user null) { throw new UsernameNotFoundException(用户不存在); } return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .authorities(ROLE_ADMIN) .build(); } }这里有一个很关键的点密码不能明文存储在数据库必须用BCrypt加密。Spring Security自带的BCryptPasswordEncoder是很成熟的选择。有些同学为了省事直接在数据库里存明文密码一旦项目被公开或者泄露后果非常严重这个习惯一定不能有。第二步写Token工具类负责生成、解析Token。Component public class JwtTokenUtil { // 密钥实际项目中应该放到配置文件中用环境变量注入 private String secret your-secret-key; // 生成Token public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // 从Token中获取用户名 public String getUsernameFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject(); } }第三步登录成功后返回Token给前端同时需要配置一个JWT过滤器在每次请求时从Header中取出Token进行校验。这个过滤器需要继承OncePerRequestFilter保证每个请求只经过一次。我实际操作中遇到的一个大坑是Spring Security默认的过滤链顺序问题。如果你自定义了JWT过滤器必须通过addFilterBefore方式把它放在UsernamePasswordAuthenticationFilter之前同时要在SecurityConfig中对放行路径做好配置——登录接口、前台接口不需要Token后台管理接口全部需要。这个配置顺序写错就会导致接口一直401排查半天都不知道是哪里出了问题。4.2 帖子发布与审核的业务流实现帖子发布流程是这个系统的核心业务我把它拆成两个场景管理员的直接发布和管理员对已提交内容的审核。在后台管理员新建一个帖子时可以设置审核状态直接通过因为管理员自己就是内容的生产者不需要再走一遍审核也可以选择先存草稿、后续再审核。而在真实的多人协作场景中可能还有“编辑提交-管理员审核-通过上线”这样一个多人协作流程我在系统里实现了这个闭环。审核的核心逻辑是一段事务操作更新帖子状态 插入审核记录。为什么要用事务因为这两个操作必须同时成功或同时失败否则会出现帖子状态变成了“已通过”但审核记录没写进去的情况导致数据不一致。Transactional(rollbackFor Exception.class) public void auditPost(Long postId, Long auditorId, Integer actionType, String auditComment) { // 1. 更新帖子审核状态 Integer targetStatus actionType 1 ? 1 : (actionType 2 ? 2 : 0); Post post new Post(); post.setId(postId); post.setAuditStatus(targetStatus); post.setAuditRemark(auditComment); postMapper.updateById(post); // 2. 插入审核记录 PostAudit auditRecord new PostAudit(); auditRecord.setPostId(postId); auditRecord.setAuditorId(auditorId); auditRecord.setActionType(actionType); auditRecord.setAuditComment(auditComment); auditRecordMapper.insert(auditRecord); }前台展示帖子列表时只需要过滤audit_status1的记录。这里又是一个容易忽略的点列表查询接口的条件必须要明确写出eq(Post::getAuditStatus, 1)不能只靠前端传状态过来。因为前端传过来的参数可以被篡改以游客身份访问时接口必须强制做状态过滤这是安全底线。同理如果帖子表本身有逻辑删除字段status那么查询时也要加上eq(Post::getStatus, 1)确保被删除的记录不会出现在前台。4.3 话题聚合与官网展示的数据组装话题聚合这个功能不难但很能体现代码组织能力。前台的“话题列表”页面需要展示话题名称、封面图、帖子数量、下面挂载的最新帖子列表这是一个典型的主表子表统计的需求。实现方式有两种一种是在Service层循环查询先查话题列表再逐个查每个话题下的帖子数量和最新帖子代码简单但会产生N1次查询帖子量大的时候性能会很差另一种是写一个SQL通过LEFT JOIN GROUP BY一次性查出所有话题的统计数据再用一条查询查最新帖子组装后返回。我选择了第二种。设计一个前台展示的VO对象包含话题本身的信息再加上它的子集合public class TopicWithPostsVO { private Long id; private String name; private String coverImage; private Long postCount; private ListPostVO latestPosts; }查询第一个阶段用分组统计查每个话题下的帖子数量第二个阶段查每个话题的最新帖子。这里需要注意最新的定义是按创建时间倒序还是按更新时间倒序。我建议按创建时间倒序因为对于一个运营了一段时间的系统来说用户看到“新内容”比看到“被编辑过的旧内容”更符合信息流习惯。当然你如果想要按热度排列那就ORDER BY view_count DESC这个完全取决于产品定位没有绝对的对错。官网框架的数据组装逻辑也类似。前台首页需要展示导航栏、轮播图、推荐话题、最新帖子、热门帖子、广告位等内容这些数据属于典型的“读多写少”非常适合做Redis缓存。我用Spring Cache的Cacheable注解给首页数据接口加缓存缓存过期时间设置为10分钟。这样后台管理员更新了广告位或者新增了帖子后最多10分钟前台就能更新既保证了数据的一致性又避免每次请求都打数据库。5. 管理后台功能落地的关键细节5.1 审核面板与菜单配置操作效率的秘密管理后台的审核面板是管理员使用频率最高的页面它的体验直接决定了系统的可用性。我实现的是一个“待审核列表”页面默认只展示audit_status0的帖子列表里显示帖子标题、所属话题、提交时间操作按钮是“通过”和“驳回”两个。点驳回时会弹出一个对话框要求填写驳回理由这个理由会被写入帖子的audit_remark字段并展示给内容提交方。这里有一个体验细节驳回操作虽然是“快速操作”但你不能让管理员只点一个按钮就完事。必须强制填写意见哪怕是一句话。从产品角度说这让审核操作有据可依避免管理员随意驳回从系统角度说这些意见会被记录到审核记录表形成完整的操作证据链。菜单配置方面管理后台的菜单应该只包含后台相关的功能模块比如内容管理帖子管理、话题管理、审核管理、运营管理广告位管理、系统管理用户管理。前台页面是单独的Vue项目不需要在后台菜单里考虑。我用的是Element UI的侧边栏菜单组件配合路由表菜单和路由表一一对应关系在项目里清晰可维护即可。5.2 数据统计与报表显示几个实用的SQL统计管理后台首页一般要展示几个关键数字帖子总数、已通过帖子数、待审核帖子数、话题总数、今日新增帖子数。这些数字不需要用复杂的大数据方案直接用SQL统计就行。我写了一个统计Service用Mapper层写对应的统计SQL比如select idcountPostByDate resultTypejava.util.Map SELECT COUNT(*) AS total, SUM(CASE WHEN audit_status 1 THEN 1 ELSE 0 END) AS approved, SUM(CASE WHEN audit_status 0 THEN 1 ELSE 0 END) AS pending FROM tourism_post WHERE DATE(created_time) CURDATE() /select每个话题的帖子数量分布图可以用一个GROUP BY查询实现。在管理后台用一个简单的柱状图或者文字列表展示出来这类看似不起眼的小功能在实际呈现项目成果时很有说服力——它说明你不只做了CRUD还把运营侧的诉求考虑进去了。6. 上线前必须知道的常见问题与排查技巧6.1 五个高频问题速查表开发调试过程中最容易遇到的就是以下几类问题。我把它们整理成了表格方便你对照排查问题现象可能原因排查方法登录接口报401Security配置中放行路径未生效检查SecurityConfig中permitAll()的路径是否与请求路径完全一致注意context-path前缀帖子列表接口返回403当前用户角色权限不足检查是否有PreAuthorize注解确认登录用户确实拥有对应角色前台看不到新发布的帖子帖子审核状态不是1或发布时未走审核流程检查SQL中是否强制过滤audit_status1以及发布时是否设置了默认状态点击“通过”后数据库没有更新事务未生效或Mapper方法未提交确认Service方法上标注了Transactional(rollbackFor Exception.class)调用方是否在同一类中导致代理失效Redis连接失败Redis未启动或地址配置错误检查application.yml中Redis配置确认前缀是否设置了密码先本机telnet测端口通断6.2 关于Spring Session与跨域问题的实战笔记如果你在一个项目里同时用了Spring Security和Session但又是前后端分离架构那么跨域问题会表现得特别诡异——因为浏览器对跨域请求会先发一个OPTIONS预检请求如果你的后端接口对OPTIONS请求没有正确处理那么大概率会出现“明明接口能通但就是不返回数据”的怪象。解决办法是在SecurityConfig中显式放行OPTIONS请求http.cors().and().csrf().disable() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS, /**).permitAll() // 其他路径规则...同时配合全局跨域配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有一个细节allowCredentials(true)时allowedOrigins不能是必须用allowedOriginPatterns()。这是我踩得最深的一个坑光这个问题就排查了整整一个晚上最后是看了源码注释才搞明白的。另外把帖子管理、话题管理这些管理后台接口全部纳入一个统一的API前缀例如/admin/**这样配置Security的权限规则会非常优雅一个规则匹配整个后台模块。同理前台接口统一用/api/**前缀这样放行时也只要一个规则。6.3 性能和安全的三个经验经验一避免N1查询。这个话题我在前面已经提过但值得再强调一次——真的有很多人的系统是这么写的查话题列表时在循环里逐个查帖子数。帖子量到几百条之后性能就开始肉眼可见地下滑。循环查库不是不能用但你要知道它的代价是多少。我一般给自己定的规矩是列表接口最多允许一层循环查库一旦超过就必须重构成批量查询或SQL聚合。经验二密码必须加密存储。这一条无论说多少遍都不嫌多。哪怕只是课程设计存储明文密码也是不可接受的坏习惯。用BCryptPasswordEncoder加密后数据库里密码字段至少是60位字符串密码相同的情况下密文都不同安全性有质的提升。经验三文件上传要做类型校验。帖子封面图和广告位图片都涉及文件上传如果不对上传文件的类型做限制可能会被恶意上传脚本文件配合服务器配置失误就可能造成安全问题。至少要做扩展名校验最好再用Hutool的ImgUtil判断文件头是否为真实图片格式。这种细节在答辩时被问到也是加分项。7. 结语这个系统后续还能怎么扩展这个项目做完之后我自己在复盘时想如果后续要继续演进有三个方向值得考虑。第一个方向是加一个用户维度的数据支持。现在的系统主要是管理员运营内容、游客浏览内容没有用户注册和收藏互动功能。如果加入普通用户注册、登录、收藏景点、评论帖子那就从“信息展示系统”进化成了“社区互动平台”业务价值和复杂度都会上一个台阶。第二个方向是引入地理位置能力。新疆地域辽阔旅游景点之间距离远如果能在系统里接入地图API给每个景点标注地理位置信息前台可以通过地图浏览周边的景点这在旅游场景中是非常实用的功能。第三个方向是关于数据导入导出的完善。目前内容都是管理员手动录入或者后台直接新增如果能把Excel批量导入导出功能加上对运营人员来说效率提升是巨大的尤其要处理大量历史数据迁移时这个功能几乎是必备的。最后说点实在的。做这种管理系统功夫花在“业务的合理性”上会比“代码的花哨程度”更值钱。你在用户登录权限、内容审核闭环、数据关联关系上思考得越深入做出来的系统就越像一个真正可以上线的产品而不只是一个应付检查的演示项目。遇到问题时多想想“真实的运营者会怎么操作”很多设计难题就迎刃而解了。