springboot学习笔记-尚硅谷微头条项目

根据老师上课代码进行手敲与视屏中的一致

1、环境配置

1.1 数据库脚本

https://www.wolai.com/v5Kuct5ZtPeVBk4NBUGBWF

1.2 boot搭建

1、导入依赖

<dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- mybatis-plus  -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!-- 数据库相关配置启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- druid启动器的依赖  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-3-starter</artifactId>
            <version>1.2.18</version>
        </dependency>

        <!-- 驱动类-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>

    </dependencies>


    <!--    SpringBoot应用打包插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

2、配置类

application.yml

# server配置
server:
  port: 8080
  servlet:
    context-path: /

# 连接池配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      url: jdbc:mysql:///sm_db
      username: root
      password: pj123456
      driver-class-name: com.mysql.cj.jdbc.Driver

# mybatis-plus的配置
mybatis-plus:
  type-aliases-package: com.melon.pojo
  global-config:
    db-config:
      logic-delete-field: isDeleted  #全局逻辑删除
      id-type: auto #主键策略自增长
      table-prefix: news_ # 设置表的前缀

#jwt配置
jwt:
  token:
    tokenExpiration: 120 #有效时间,单位分钟
    tokenSignKey: headline123456  #当前程序签名秘钥 自定义

3、启动类

  • 配置启动类和mybatis-plus配置
package com.melon;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@MapperScan("com.melon.mapper")
public class Main {

    public static void main(String[] args) {
        SpringApplication.run(Main.class,args);
    }

    //配置mybatis-plus插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //分页
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());  //乐观锁
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());  //防全局修改和删除
        return interceptor;
    }

}

4、工具类

  • 结果封装类
package com.melon.utils;

/**
 * 全局统一返回结果类
 */
public class Result<T> {
    // 返回码
    private Integer code;
    // 返回消息
    private String message;
    // 返回数据
    private T data;
    public Result(){}
    // 返回数据
    protected static <T> Result<T> build(T data) {
        Result<T> result = new Result<T>();
        if (data != null)
            result.setData(data);
        return result;
    }
    public static <T> Result<T> build(T body, Integer code, String message) {
        Result<T> result = build(body);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
    public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
        Result<T> result = build(body);
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }
    /**
     * 操作成功
     * @param data  baseCategory1List
     * @param <T>
     * @return
     */
    public static<T> Result<T> ok(T data){
        Result<T> result = build(data);
        return build(data, ResultCodeEnum.SUCCESS);
    }
    public Result<T> message(String msg){
        this.setMessage(msg);
        return this;
    }
    public Result<T> code(Integer code){
        this.setCode(code);
        return this;
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}
package com.melon.utils;

/**
 * 统一返回结果状态信息类
 *
 */
public enum ResultCodeEnum {

    SUCCESS(200,"success"),
    USERNAME_ERROR(501,"usernameError"),
    PASSWORD_ERROR(503,"passwordError"),
    NOTLOGIN(504,"notLogin"),
    USERNAME_USED(505,"userNameUsed");

    private Integer code;
    private String message;
    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }
}

  • MD5加密工具类
package com.melon.utils;

import org.springframework.stereotype.Component;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

@Component
public final class MD5Util {
    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }
}
  • jwt验证类
    • 使用jwt需要先引入依赖
    • yml进行配置
#jwt配置
jwt:
  token:
    tokenExpiration: 120 #有效时间,单位分钟
    tokenSignKey: headline123456  #当前程序签名秘钥 自定义
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
</dependency>
package com.melon.utils;

import com.alibaba.druid.util.StringUtils;
import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
@ConfigurationProperties(prefix = "jwt.token")
public class JwtHelper {

    private  long tokenExpiration; //有效时间,单位毫秒 1000毫秒 == 1秒
    private  String tokenSignKey;  //当前程序签名秘钥

    //生成token字符串
    public  String createToken(Long userId) {
        System.out.println("tokenExpiration = " + tokenExpiration);
        System.out.println("tokenSignKey = " + tokenSignKey);
        String token = Jwts.builder()

        .setSubject("YYGH-USER")
        .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration*1000*60)) //单位分钟
        .claim("userId", userId)
        .signWith(SignatureAlgorithm.HS512, tokenSignKey)
        .compressWith(CompressionCodecs.GZIP)
        .compact();
        return token;
    }

    //从token字符串获取userid
    public  Long getUserId(String token) {
        if(StringUtils.isEmpty(token)) return null;
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        Integer userId = (Integer)claims.get("userId");
        return userId.longValue();
    }



    //判断token是否有效
    public  boolean isExpiration(String token){
        try {
            boolean isExpire = Jwts.parser()
            .setSigningKey(tokenSignKey)
            .parseClaimsJws(token)
            .getBody()
            .getExpiration().before(new Date());
            //没有过期,有效,返回false
            return isExpire;
        }catch(Exception e) {
            //过期出现异常,返回true
            return true;
        }
    }
}

2、后台功能开发

2.1 用户模块开发

1、用户登陆模块

1.controller

    @PostMapping("login")
    public Result login(@RequestBody User user){
        Result result = userService.login(user);
        return result;
    }

2.service

    // 登陆
    Result login(User user);

3.serviceimpl

public Result login(User user) {

        // 根据账号查询
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername,user.getUsername());
        User loginUser = userMapper.selectOne(queryWrapper);

        // 账号判断
        if(loginUser == null){
            // 账号错误
            return Result.build(null, ResultCodeEnum.USERNAME_ERROR);
        }

        // 密码判断
        if(!StringUtils.isEmpty(user.getUserPwd())
                && loginUser.getUserPwd().equals(MD5Util.encrypt(user.getUserPwd())))
        {
            // 账号密码正确
            // 根据用户唯一标识生成token
            String token = jwtHelper.createToken(Long.valueOf(loginUser.getUid()));
            HashMap data = new HashMap();
            data.put("token",token);
            return Result.ok(data);
        }

        // 密码错误
        return Result.build(null,ResultCodeEnum.PASSWORD_ERROR);
    }

2、根据token获取用户信息

大致流程:
1.获取token,解析token对应的userId
2.根据userId,查询用户数据
3.将用户数据的密码置空,并且把用户数据封装到结果中key = loginUser
4.失败返回504 (本次先写到当前业务,后期提取到拦截器和全局异常处理器)

1.controller

    @GetMapping("getUserInfo")
    public Result userInfo(@RequestHeader String token){
        Result result = userService.getUserInfo(token);
        return result;
    }

2.service

    // 查询用户信息
    Result getUserInfo(String token);

3.serviceimpl

public Result getUserInfo(String token) {

        // 判定是否有效期
        if(jwtHelper.isExpiration(token)){
            //true过期,直接返回登陆
            return Result.build(null,ResultCodeEnum.NOTLOGIN);
        }

        // 根据token获取用户对应的uid
        int userId = jwtHelper.getUserId(token).intValue();

        // 查询数据
        User user = userMapper.selectById(userId);

        if(user != null){
            user.setUserPwd(null);
            Map data = new HashMap();
            data.put("loginUser",user);
            return Result.ok(data);
        }
        return Result.build(null,ResultCodeEnum.PASSWORD_ERROR);
    }

问题:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.melon.mapper.UserMapper.selectById

解决:
没有在对应的实体类主键上加@Tableid

@Data
public class User implements Serializable {
    
    @TableId
    private Integer uid;


3、检查账户是否可用

  1. 获取账户数据
  2. 根据账号进行数据库查询
  3. 结果封装

1.controller

    @PostMapping("checkUserName")
    public Result checkUserName(String username){
        Result result = userService.checkUserName(username);
        return result;
    }

2.service

    // 用户注册
    Result regist(User user);

3.serviceimple

    public Result checkUserName(String username) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername,username);
        Long count = userMapper.selectCount(queryWrapper);

        if (count==0) {
            return Result.ok(null);
        }
        return Result.build(null,ResultCodeEnum.USERNAME_USED);
    }

4、用户注册功能

  1. 将密码加密
  2. 将数据插入
  3. 判断结果, 成功 返回200 失败 505
    @PostMapping("regist")
    public Result regist(@RequestBody User user){
        Result result = userService.regist(user);
        return result;
    }
    // 用户注册
    Result regist(User user);
    public Result regist(User user) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername,user.getUsername());
        Long count = userMapper.selectCount(queryWrapper);

        if (count>0) {
            return Result.build(null,ResultCodeEnum.USERNAME_USED);
        }

        user.setUserPwd(MD5Util.encrypt(user.getUserPwd()));
        userMapper.insert(user);
        return Result.ok(null);
    }

2.2 首页模块开发

1、首页类别查询

    @GetMapping("findAllTypes")
    public Result findAllTypes(){
        Result result = typeService.findAllTypes();
        return result;
    }
    public Result findAllTypes() {
        List<Type> types = typeMapper.selectList(null);
        return Result.ok(types);
    }

2、头条新闻分页查询

  • 如果查询的条件并没有对应的实体类,一般创建一个vo类
  • 分页查询:
    • headlineMapper.selectPage(page,portalVo);传入的参数第一个是page,即要查询的分页对象,第二个是查询的条件或对象,可能包括关键字等信息

1.controller

    @PostMapping("findNewsPage")
    public Result findNewsPage(@RequestBody PortalVo portalVo){
        Result result = headlineService.findNewsPage(portalVo);
        return result;
    }

2.service

    // 首页数据查询
    Result findNewsPage(PortalVo portalVo);

3.serviceImpl

    public Result findNewsPage(PortalVo portalVo) {

        // Page -> 当前页面,页容量
        IPage<Map> page = new Page<>(portalVo.getPageNum(),portalVo.getPageSize());
        headlineMapper.selectPage(page,portalVo);

        List<Map> records = page.getRecords();
        Map data = new HashMap();
        // Map<Object, Object> data = new HashMap<>();
        data.put("pageData",records);
        data.put("pageNum",page.getCurrent());
        data.put("pageSize",page.getSize());
        data.put("totalPage",page.getPages());
        data.put("totalSize",page.getTotal());

        Map pageInfo = new HashMap();
        pageInfo.put("pageInfo",data);

        return Result.ok(pageInfo);
    }

4.mapper

  • IPage:这是方法的返回类型。IPage 是 MyBatis-Plus 提供的一个接口,用于分页并包含了分页结果。这里的泛型 指定了结果集的数据类型,即每一条记录将被映射为一个 Map 对象。
  • selectPage:这是方法的名称,它通常用来表示这个方法将被用于执行一个分页查询操作。
  • (IPage iPage, @Param(“portalVo”) PortalVo portalVo):这部分定义了方法的参数列表:
    • IPage iPage:第一个参数是一个 IPage 类型的对象,它包含了分页的配置信息(如当前页号和每页显示的记录数)。这个对象不仅指导 MyBatis-Plus 如何进行分页查询,还用于存储查询结果。
    • @Param(“portalVo”) PortalVo portalVo:第二个参数使用了 @Param 注解,它告诉 MyBatis-Plus 在 SQL 语句中引用这个参数时应该使用注解中提供的名称。这里,“portalVo” 就是传递给 SQL 语句的参数名。PortalVo 类型的 portalVo 参数对象包含了查询所需的其他条件,如筛选关键词、类型等。
    IPage<Map> selectPage(IPage iPage, @Param("portalVo") PortalVo portalVo);

5.mapper.xml

    <select id="selectPage" resultType="map">
        select hid,title,type,page_views pageViews,TIMESTAMPDIFF(HOUR,create_time,NOW()) pastHours,
        publisher from news_headline where is_deleted=0
            <if test="portalVo.keyWords !=null and portalVo.keyWords.length()>0 ">
                and title like concat('%',#{portalVo.keyWords},'%')
            </if>
            <if test="portalVo.type != null and portalVo.type != 0">
                and type = #{portalVo.type}
            </if>
    </select>

3、头条信息详情

  • 连表查询需要自定义查询方法
    @PostMapping("showHeadlineDetail")
    public Result showHeadlineDetail(Integer hid){
        Result result = headlineService.showHeadlineDetail(hid);
        return result;
    }
    // 查询详情信息
    Result showHeadlineDetail(Integer hid);
  • 步骤
    • 获取头条新闻详情,并返回到一个data中
    • 将data放入另一个map集合headlineMap中
    • 创建新闻头条实体并更新阅读量
    • 更新数据库中的新闻头条
public Result showHeadlineDetail(Integer hid) {

        Map data = headlineMapper.queryDetail(hid);
        Map headlineMap = new HashMap();
        headlineMap.put("headline",data);

        // 修改阅读量+1
        Headline headline = new Headline();
        headline.setHid((Integer) data.get("hid"));
        headline.setVersion((Integer) data.get("version"));
        // 阅读量+1
        headline.setPageViews((Integer) data.get("pageViews")+1);

        headlineMapper.updateById(headline);

        return Result.ok(headlineMap);
    }
Map queryDetail(Integer hid);
    <!-- Map queryDetail(Integer hid); -->
    <select id="queryDetail" resultType="map">
        select hid,title,article,type, h.version ,tname typeName ,page_views pageViews
             ,TIMESTAMPDIFF(HOUR,create_time,NOW()) pastHours,publisher
             ,nick_name author from news_headline h
                                        left join news_type t on h.type = t.tid
                                        left join news_user u  on h.publisher = u.uid
        where hid = #{hid}
    </select>

2.3 头条模块开发

1、登陆验证和保护

    @GetMapping("checkLogin")
    public Result checkLogin(@RequestHeader String token){
        boolean expiration = jwtHelper.isExpiration(token);
        if(expiration){
            return Result.build(null, ResultCodeEnum.NOTLOGIN);
        }
        return Result.ok(null);
    }

2、配置拦截器

  • 因为下面所有的/headline的操作都需要先检查用户是否登陆,所以需要配置拦截器
  1. LoginProtectInterceptor

这是一个实现了HandlerInterceptor接口的组件,用于在控制器处理请求之前进行预处理。它主要完成以下任务:

  1. **获取Token:** 从HTTP请求的头部**token**字段中获取JWT token。
  2. **检查Token有效性:** 使用注入的**JwtHelper**组件调用**isExpiration**方法来检查token是否已经过期。注意,如果**isExpiration**方法的意义是“是否过期”,那么方法名可能会让人困惑,更好的命名可能是**isExpired**。
  3. **处理验证结果:**
     - **Token有效:** 如果token未过期(**expiration**为**false**),则方法返回**true**,允许请求继续执行。
     - **Token无效:** 如果token已过期或无效(**expiration**为**true**),则拦截器拒绝进一步处理请求,并向客户端返回一个状态码为504的JSON响应,表示用户未登录或认证过期。使用**ObjectMapper**将错误信息序列化为JSON格式,并通过**response.getWriter().print(json);**发送到客户端。
@Component
public class LoginProtectInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtHelper jwtHelper;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 从请求头中获取token
        String token = request.getHeader("token");
        // 检查是否有效
        boolean expiration = jwtHelper.isExpiration(token);
        // 有效放行
        if(!expiration){
            // 放行
            return true;
        }
        // 状态无效返回504的JSON
        Result result = Result.build(null, ResultCodeEnum.NOTLOGIN);

        // 序列化为JSON格式
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(result);
        // 发送响应给客户端
        response.getWriter().print(json);

        return false;
    }
}
  1. WebMVCConfig
    1. 注入拦截器: 通过**@Autowired注解自动注入LoginProtectInterceptor**。
    2. 配置拦截器:addInterceptors方法中,使用InterceptorRegistry添加loginProtectInterceptor,并通过addPathPatterns("/headline/")指定它只对以/headline/开头的URL路径生效。这意味着所有访问这些路径的请求都会先通过LoginProtectInterceptor**的预处理。
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Autowired
    private LoginProtectInterceptor loginProtectInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginProtectInterceptor).addPathPatterns("/headline/**");
    }
}

3、头条发布实现

  • 需求携带headline,此外还需要将token携带
  • 注意:不要忘了注解@RequestHeader
  1. controller
    @PostMapping("publish")
    public Result publish(@RequestBody Headline headline,@RequestHeader String token){
        Result result = headlineService.publish(headline,token);
        return result;
    }

2.serviceImpl

  • 先根据用户的携带的token查询id
  • 数据装配的时候还需要将publisher等信息填入
    public Result publish(Headline headline, String token) {

        // 根据token查询用户id
        int userId = jwtHelper.getUserId(token).intValue();
        System.out.println(userId);
        // 数据装配
        headline.setPublisher(userId);
        headline.setPageViews(0);
        headline.setCreateTime(new Date());
        headline.setUpdateTime(new Date());

        headlineMapper.insert(headline);

        return Result.ok(null);
    }

4、头条修改数据回显

    @PostMapping("findHeadlineByHid")
    public Result findHeadlineByHid(Integer hid){
        Headline headline = headlineService.getById(hid);
        Map data = new HashMap();
        data.put("headline",headline);
        return Result.ok(data);
    }

5、修改头条信息

    @PostMapping("update")
    public Result update(@RequestBody Headline headline){
        Result result = headlineService.updateData(headline);
        return result;
    }
    public Result updateData(Headline headline) {
        Integer version = headlineMapper.selectById(headline).getVersion();

        headline.setVersion(version); // 乐观锁
        headline.setUpdateTime(new Date());

        headlineMapper.updateById(headline);

        return Result.ok(null);
    }

6、删除头条信息

    @PostMapping("removeByHid")
    public Result removeByHid(Integer hid){
        headlineService.removeById(hid);
        return Result.ok(null);
    }
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值