ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot观影网站管理系统开发实战:从数据库设计到JWT鉴权

SpringBoot观影网站管理系统开发实战:从数据库设计到JWT鉴权 如果你在某个资源站或者毕设选题清单上看到“java基于springboot的观影网站管理系统95tuxvwp”这个名字大概率是下面两种情况要么是准备做课程设计的大三学生要么是刚学完SpringBoot想拿个项目练手的新人。我先说结论这个题目属于典型的Java Web全栈入门级项目技术上不复杂但涉及的功能点很全非常适合用来打通“前端页面—后端接口—数据库”这条完整链路。这篇文章我就从项目拆解、数据库设计、后端核心实现、常见坑这几个方向把这类系统从零到一的完整思路给你捋一遍。先解释一下标题里那串“95tuxvwp”这在大多数情况下是代码生成工具或者在线项目管理平台自动生成的唯一标识类似数据库名或者部署包的编号跟业务本身没关系不用管它。包括你在GitHub或者Gitee上搜“观影网站管理系统”会发现大量命名类似的项目前缀都是“java基于springboot”后缀是随机字符串基本可以断定这些是从同一个课程设计模板派生出来的。我的建议是如果你是被分配到这个题目核心精力放在功能实现和文档撰写上如果你是主动找练习项目完全可以用这个题目作为参考加入自己的设计。1. 项目整体设计与技术选型思路1.1 这个系统到底要做什么观影网站管理系统拆开看就是两个端面向普通用户的观影前台和面向管理员的运营后台。前台核心功能是电影展示、搜索、分类筛选、电影详情、评论、收藏、个人中心这些后台核心功能是电影信息管理、上映状态管理、用户管理、评论审核、统计数据。如果再加点延伸可以做座位选座、在线购票、订单管理不过那是影院票务系统的范畴了基础版一般只做到内容管理和用户交互。我刚拿到这类题目时容易犯一个毛病——上来就写代码。但实际上这类系统最容易出彩的地方是需求边界是否清晰。观影网站和电商网站不一样它的核心业务对象是“电影内容”围绕内容产生用户行为收藏、评论、评分围绕用户行为产生管理需求审核、统计、推荐。你只要把这几个层次理清楚功能设计就不会跑偏。1.2 为什么选SpringBoot而不是其他方案你可能会问为什么课程设计和毕业设计清一色都是SpringBoot原因很简单SpringBoot把Spring生态里那些繁琐的XML配置全干掉了启动一个Web项目几乎零配置内置Tomcat打一个Jar包就能跑。对于学生来说这意味着可以把80%的精力放在业务代码上而不是折腾环境。对于公司里的实际项目SpringBoot也是当前Java后端的事实标准学这个方向不会走弯路。具体到技术选型我在做这类管理系统时倾向用这一套组合JDK 8 或 11稳妥兼容性最好SpringBoot 2.7.x不要一上来就追3.x后面我会讲为什么MyBatis-Plus单表CRUD几乎不用写SQL分页也方便MySQL 5.7 或 8.0Redis做缓存和验证码存储非必需但加了是加分项JWT做登录鉴权比Session更适合前后端分离Vue 2 Element UI 或 Thymeleaf看你要不要前后端分离这里我多说一句关于SpringBoot版本的选择。如果你在2024年之后新建项目IDEA里默认会让你选SpringBoot 3.x对应的JDK最低是17。但很多学校机房装的还是JDK8而且网上大量教程、依赖包都是基于SpringBoot 2.x写的。SpringBoot 3.x里javax.servlet包改名成了jakarta.servlet导致很多旧教程代码直接跑不起来。所以我的建议是如果是做课设用SpringBoot 2.7.18 JDK8这是最稳的组合网上资料最多遇到问题随便一搜就有答案。如果你想把技术栈更新一点再考虑SpringBoot 3.x JDK17。1.3 项目目录结构与分层规划SpringBoot项目推荐按经典的分层结构组织不要把所有类都堆在一个包下。我常用的结构是这样src/main/java/com/example/movie ├── controller // 接口层接收请求返回结果 ├── service // 业务层处理核心逻辑 │ └── impl // 业务实现类 ├── mapper // 数据访问层MyBatis-Plus的Mapper接口 ├── entity // 数据库实体类 ├── dto // 数据传输对象用于接口入参/出参 ├── vo // 视图对象用于返回给前端的数据封装 ├── config // 配置类如跨域、拦截器、Redis配置 ├── utils // 工具类如JWT工具、统一返回结果封装 └── common // 公共类如异常处理、常量定义为什么要把dto和vo分开这是很多新手容易忽略的点。简单说DTO负责接收前端传来的参数VO负责给前端返回数据Entity对应数据库表结构。如果三者混用最直接的后果是你查出来的用户密码哈希一不小心就返回给前端了或者前端传了一个多余字段把数据库某条记录覆盖了。分层清晰以后各层之间互不干扰出了问题定位也快。2. 数据库设计一张好的表胜过十次重构2.1 核心表设计与字段规划观影网站管理系统的数据库围绕用户、电影、交互行为这三类核心对象来设计。下面我给出一个常用可落地的表结构方案字段和类型都按主流MySQL写法来。首先是用户表user它承接登录、个人信息、角色权限字段名类型说明idbigint主键自增usernamevarchar(50)用户名唯一passwordvarchar(255)密码BCrypt加密后的哈希值nicknamevarchar(50)昵称avatarvarchar(255)头像地址emailvarchar(100)邮箱可空roletinyint角色0-普通用户1-管理员statustinyint状态0-正常1-禁用create_timedatetime注册时间update_timedatetime更新时间注意password字段我见过太多人直接用明文存密码这是在给自己挖坑。Spring Security自带的BCryptPasswordEncoder可以一步搞定加密和校验后面代码部分我会演示。其次是电影表movie这是整个系统的核心表字段名类型说明idbigint主键titlevarchar(200)电影名称cover_urlvarchar(500)海报封面URLdirectorvarchar(100)导演actorsvarchar(500)主演多个用逗号分隔genrevarchar(100)类型如 剧情/动作/科幻regionvarchar(50)制片地区languagevarchar(50)语言durationint时长分钟release_datedate上映日期ratingdecimal(3,1)综合评分一位小数descriptiontext简介statustinyint状态0-未上映1-上映中2-已下映create_timedatetime创建时间这里有个实际经验genre类型字段我一开始习惯用varchar存“动作,科幻”这种逗号分隔字符串后来发现按类型筛选时要用模糊查询性能和准确性都不好。更好的做法是单独建一个类型表或者用MySQL的JSON类型存储数组。但对于课设级别varchar加like查询完全够了不用过度设计。然后是评论表comment记录用户对电影的评论字段名类型说明idbigint主键movie_idbigint电影IDuser_idbigint评论用户IDcontentvarchar(1000)评论内容scoretinyint评分1-10statustinyint状态0-待审核1-已通过2-已驳回create_timedatetime评论时间最后是收藏表favorite和轮播图表banner这两个表结构比较简单。收藏表字段是id、user_id、movie_id、create_time加一个唯一索引保证同一用户对同一电影只能收藏一次。轮播图表是id、image_url、movie_id、sort、status用于首页展示推荐位。2.2 表关联与索引设计思路这四张表的关系其实很简单评论表和收藏表都通过movie_id和user_id关联电影表和用户表。在数据库层面需要做的优化很少但有两个索引建议加上评论表的 movie_id 加普通索引因为查询某部电影的所有评论是高频操作收藏表的 (user_id, movie_id) 加唯一索引既能保证业务规则一人只能收藏一次又能加速查询很多人设计数据库时不重视索引数据量小的时候感觉不出来一旦某张表有几万条数据不带索引的查询就会明显变慢。SQL层面还有一个容易被忽略的问题count(*)统计评论数时如果评论表很大每次统计都会全表扫描。优化方案是把评论数量冗余到电影表里比如movie表加一个comment_count字段每次有人评论成功就在事务里更新它。类似的思想在真实项目里很常用叫“空间换时间”牺牲一点写入性能换查询速度的大幅提升。对于这个项目数据库里的数据从哪来这是我被问过最多的问题之一。我的建议是找“猫眼电影”或“豆瓣电影”的公开榜单手动整理30-50条电影信息作为种子数据字段齐全一点。不要用爬虫去爬一是反爬机制麻烦二是课设阶段没必要惹这个麻烦。手动录入或写个简单的SQL脚本插入都行。3. 后端核心功能模块实现3.1 统一返回结果与异常处理所有的接口统一返回相同格式的JSON这是一个好习惯。我做了一个Result类泛型设计包含code、message、data三个字段Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }为什么要统一返回格式因为前端拿到数据以后不需要关心每个接口的返回结构是不是一致只需要判断code是不是200然后取data。这个习惯在团队协作和前后端联调时能省很多事。配套的还有全局异常处理。SpringBoot里用RestControllerAdvice注解配合ExceptionHandler可以统一捕获业务异常和系统异常避免一堆堆栈信息直接抛给前端RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public Result? handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } ExceptionHandler(Exception.class) public Result? handleException(Exception e) { return Result.error(500, 系统异常 e.getMessage()); } }这个类里还可以加上参数校验异常的处理比如前端传的参数不合法统一返回“参数错误”而不是把默认的400错误抛给用户。3.2 基于JWT的登录鉴权实现登录功能是这类系统的标配我选择用JWT来实现而不是传统的Session。原因有两个一是前端分离架构下Session跨域处理麻烦二是JWT天然支持无状态扩展后续加移动端接口不用改鉴权逻辑。先说整体流程用户提交用户名和密码后端校验用户名是否存在、密码是否正确校验通过后生成一个JWT令牌返回给前端前端把令牌存在localStorage或请求头里每次请求带上后端通过拦截器解析令牌识别用户身份和角色我用的是jjwt这个库版本用0.9.1配合Jose4j也可以。先封装一个JwtUtil工具类Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expiration}) private Long expiration; public String generateToken(Long userId, String username, Integer role) { Date now new Date(); Date expireDate new Date(now.getTime() expiration * 1000); return Jwts.builder() .setHeaderParam(typ, JWT) .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }这里特别提醒secret不要写死在代码里放到application.yml里并且不要太短。我见过有人用“123456”当密钥这种token基本等于没签随便就能伪造。建议用一个32位以上的随机字符串比如一段字母数字混合的内容。有了JWT工具类之后还需要一个拦截器来统一拦截需要登录的接口。SpringBoot里实现HandlerInterceptor重写preHandle方法Component public class JwtInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims jwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\token无效或已过期\}); return false; } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } }然后把这个拦截器注册到WebMvcConfigurer里设置哪些路径需要拦截、哪些路径放行。放行的路径一般有登录接口、注册接口、电影列表查询接口、电影详情接口因为这些是游客也能看的。需要拦截的路径是用户评论、收藏、个人中心。注意拦截器里对OPTIONS请求要直接放行否则前端跨域请求会被拦掉这是新手经常踩的坑。3.3 电影管理模块的增删改查管理端的电影管理是最基础的功能但在实现时有两个细节值得注意一是图片上传二是分页查询。图片上传如果不想自己写文件存储可以先用本地存储路径就是配置文件里指定一个上传目录然后通过静态资源映射对外暴露访问URL。生产环境再换成OSS或MinIO。本地存储的做法很简单file: upload-path: /Users/xxx/movie-upload/ access-path: /upload/**对应的配置类Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Value(${file.access-path}) private String accessPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(accessPath) .addResourceLocations(file: uploadPath); } }上传接口用MultipartFile接收文件保存时生成UUID作为文件名避免中文名和重复名导致的混乱。下面的代码是一个简化版上传逻辑PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(400, 文件不能为空); } String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; File dest new File(uploadDir, fileName); try { file.transferTo(dest); return Result.success(/upload/ fileName); } catch (IOException e) { return Result.error(500, 上传失败); } }分页查询这里如果你用的是MyBatis-Plus那基本就是一行代码。先配置一个分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后业务代码里直接调用MyBatis-Plus提供的分页方法public PageMovie getMoviePage(int page, int size, String keyword, String genre) { PageMovie p new Page(page, size); LambdaQueryWrapperMovie wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Movie::getTitle, keyword) .eq(StringUtils.hasText(genre), Movie::getGenre, genre) .orderByDesc(Movie::getReleaseDate); return movieMapper.selectPage(p, wrapper); }这里LambdaQueryWrapper的like和eq方法第一个参数是boolean条件当条件为true时才拼接这个查询条件。这样做的好处是前端不传某个参数时后端不用写一堆if判断来拼SQL代码简洁很多。3.4 评论、收藏与个人中心评论模块的逻辑里有一个容易忽略的地方电影的评分是从评论的score字段算出来的。也就是说每次新增一条评论电影表的rating字段都要更新一次。实现方式是在评论service里先插入评论再重新计算该电影的平均分并更新电影表全程放在一个事务里保证数据一致性。Transactional public void addComment(Comment comment) { // 1. 插入评论 commentMapper.insert(comment); // 2. 重新计算平均分 ListComment comments commentMapper.selectList( new LambdaQueryWrapperComment() .eq(Comment::getMovieId, comment.getMovieId()) .eq(Comment::getStatus, 1)); BigDecimal avgScore comments.stream() .map(Comment::getScore) .reduce(BigDecimal.ZERO, BigDecimal::add) .divide(BigDecimal.valueOf(comments.size()), 1, RoundingMode.HALF_UP); // 3. 更新电影评分 Movie movie new Movie(); movie.setId(comment.getMovieId()); movie.setRating(avgScore); movieMapper.updateById(movie); }注意Transactional注解必须加在public方法上而且要通过代理对象调用才会生效。如果你在同一个类里直接调用这个方法事务是不会开启的这是事务失效最常见的场景。另外评论状态有“待审核”和“已通过”之分上面这段逻辑我写的是只统计审核通过的评论如果统计所有评论就可能出现某个用户刚评论完评分就变了但内容还没审核这显然是有问题的。收藏功能的实现就简单多了。收藏和取消收藏用同一个接口传一个movieId后端判断当前用户是否已经收藏过如果收藏了就删除记录反之插入记录。这也是为什么收藏表要加唯一索引它能在数据库层面兜底防止并发请求下重复数据。个人中心接口需要查个人信息、我的收藏列表、我的评论记录。注意返回给前端的时候不要把用户表的password字段带出去。简单做法是在实体类的password字段上加JsonIgnore注解或者查询之后手动把密码字段置空。也可以定义一个UserVO只包含需要返回的字段这是更推荐的做法因为实体类有时候还需要接收前端传来的密码不能一刀切忽略。3.5 统计数据的实现思路管理后台通常需要几个数字撑门面总用户数、总电影数、总评论数、今日新增用户等。这种统计用MyBatis-Plus的selectCount方法就行public MapString, Object getDashboardData() { MapString, Object data new HashMap(); data.put(userCount, userMapper.selectCount(null)); data.put(movieCount, movieMapper.selectCount(null)); data.put(commentCount, commentMapper.selectCount(null)); data.put(todayNewUser, userMapper.selectCount( new LambdaQueryWrapperUser() .ge(User::getCreateTime, LocalDate.now().atStartOfDay()))); return data; }统计类接口查询频率高数据变化不频繁可以加个Redis缓存比如缓存5分钟能明显降低数据库压力。如果项目里还没引入Redis不加也没关系课设阶段数据库这点查询压力完全可以忽略。4. 常见问题与排查技巧实录4.1 SpringBoot版本太高导致javax/jakarta包名错误这个问题在我遇到的咨询里排第一。如果你创建的SpringBoot 3.x项目代码里import javax.servlet.http.HttpServletRequest会直接报红因为Java EE的包名从javax迁移到了jakarta。解决办法有两个一是把项目降到SpringBoot 2.7.x二是把代码里的import从javax.servlet改成jakarta.servlet。拦截器、过滤器、Servlet相关的类都要改一遍。我建议方案一课设实验环境用2.7.18最省心。4.2 分页查询失效返回所有数据MyBatis-Plus的分页插件如果不配置MybatisPlusInterceptor分页就不会生效。很多教程只教你怎么写Page类却没有提醒你要注册分页插件。如果你发现传了page和size参数返回的records还是全量数据先检查一下MybatisPlusConfig类是否存在分页插件是否注册到了容器里。还有一个细节分页插件必须设置数据库类型否则分页SQL可能生成不对。4.3 跨域请求被拦截前后端分离的项目前端跑在8080端口后端跑在8081端口前端发请求时浏览器会拦截报跨域错误。后端需要开启跨域配置。SpringBoot里最简单的方式是直接在启动类上加CrossOrigin或者在WebMvcConfigurer里配置全局跨域Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedHeaders(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); }注意allowCredentials(true)时allowedOrigin不能写*要用allowedOriginPatterns(*)这是高版本SpringBoot的调整。另外跨域配置要和JWT拦截器配合好对于OPTIONS预检请求要直接放行否则前端会一直报跨域错误但后端的日志里根本看不到请求进来。4.4 日期格式化问题接口返回的日期字段默认是时间戳或者带T的格式比如“2024-06-01T10:00:00”前端拿到以后还得做二次处理。解决办法是在application.yml里统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但这样有一个坑LocalDateTime类型不会自动生效需要在实体类的日期字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。所以我统一建议实体类字段用LocalDateTime并在每个需要返回给前端的日期字段上都加JsonFormat保险还直观。4.5 JSON循环引用导致接口报错如果你写了类似Movie实体类里包含List Comment实体类里又引用Movie在查询电影详情时返回JSON就会爆StackOverflowError。解决办法是在实体类的关联字段上加JsonIgnore或者用JsonIdentityInfo。最简单粗暴的是VO类里不引用数据库实体自己封装需要的字段。说到底还是回到前面说的不要直接拿实体类去返回数据用VO隔离能避免一堆坑。5. 项目扩展与个人操作心得如果你做完基础版还有余力我强烈建议加这几个功能对评分和面试都有实实在在的帮助。第一个是Redis做首页热门电影缓存每次查询先把数据存到Redis设置5分钟过期能显著提升接口响应速度第二个是Spring Security整合JWT让权限控制更正规虽然学习成本高一点但面试问到权限设计时你能聊得更深第三个是用WebSocket做观影室或者弹幕功能这个属于加分项里的亮点功能。还有一个思路是整合第三方接口比如用爬虫定时同步最新电影资讯或者接一个公开的影评API让系统内容保持更新。最后分享一个我在做这类项目时最大的体会不要一上来就看网上的完整源码那样你只是在别人的代码里改改注释做完还是一头雾水。正确的方式是先自己把功能列表列出来工具类自己写接口自己调数据库自己设计卡住了再查资料。等你把这个系统从零做完SpringBoot、MyBatis-Plus、JWT、MySQL这些核心技术的使用基本就熟练了。如果时间紧急着交作业那也要把每一步的原理搞清楚至少做到能对着代码讲明白每一行是干什么的。这样不管是答辩还是面试你都能扛得住追问。
返回列表