SpringBoot微版头条

SpringBoot版微头条实战

一、微头条案例介绍

微头条业务简介

  • 用户功能
    • 注册功能
    • 登录功能
    • jwt实现
  • 头条新闻
    • 新闻的分页浏览
    • 通过标题关键字搜索新闻
    • 查看新闻详情
    • 新闻的修改和删除

技术栈介绍

前端技术栈

  • ES6作为基础JS语法
  • nodejs用于运行环境
  • npm用于项目依赖管理工具
  • vite用于项目的构建架工具
  • Vue3用于项目数据的渲染框架
  • Axios用于前后端数据的交互
  • Router用于页面的跳转
  • Pinia用于存储用户的数据
  • LocalStorage作为用户校验token的存储手段
  • Element-Plus提供组件

后端技术栈

  • JAVA作为开发语言,版本为JDK17
  • Tomcat作为服务容器,版本为10.1.7
  • Mysql8用于项目存储数据
  • SpringMVC用于控制层实现前后端数据交互
  • MyBatis-Plus用于实现数据的CURD
  • Druid用于提供数据源的连接池
  • SpringBoot作为项目基础架构
  • MD5用于用户密码的加密
  • Jwt用于token的生成和校验
  • Jackson用于转换JSON

二、微头条前端搭建

确保本地node,npm,vscode安装完毕!

下载前端代码my-vue3-vite-project

使用VsCode打开上面的工程

通过 npm run dev启动前端项目

npm install 
npm run dev

三、基于SpringBoot搭建项目基础结构

3.1 数据库脚本执行

执行数据库脚本:top_news

3.2 搭建SpringBoot工程

  1. 导入依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.5</version>
    </parent>
    
    <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.22</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.yaml配置文件

    # server配置
    server:
      port: 8080
      servlet:
        context-path: /
    
    # 连接池配置
    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          url: jdbc:mysql://localhost:3306/sm_db?characterEncoding=utf-8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
    
    # mybatis-plus的配置
    mybatis-plus:
      type-aliases-package: com.yue.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配置

    包:com.yue

    @SpringBootApplication
    @MapperScan("com.yue.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. 工具类准备

    一、工具类准备

    /**
     * 全局统一返回结果类
     */
    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;
        }
    }
    

    二、解决枚举类

    /**
     * 统一返回结果状态信息类
     *
     */
    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加密工具类

    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);
            }
        }
    }
    

3.3 MyBatisX逆向工程

IDEA连接数据库

完善实体类注解

删除@TableName注解,全局统一设置

添加主键,乐观锁,逻辑删除注解!!!

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

    private String username;

    private String userPwd;

    private String nickName;

    @Version
    private Integer version;

    @TableLogic
    private Integer isDeleted;

    private static final long serialVersionUID = 1L;
}

四、后台功能开发

4.1 用户模块开发

4.1.1 jwt和token介绍

token介绍

令牌(Token):在计算机领域,令牌是一种代表某种访问权限或身份认证信息的令牌。它可以是一串随机生成的字符或数字,用于验证用户的身份或授权用户对特定资源的访问。普通的令牌可能以各种形式出现,如访问令牌、身份令牌、刷新令牌等。

简单理解 : 每个用户生成的唯一字符串标识,可以进行用户识别和校验

优势: token验证标识无法直接识别用户的信息,盗取token后也无法登录程序! 相对安全!

jwt介绍

Token是一项规范和标准(接口)

JWT(JSON Web Token)是具体可以生成,校验,解析等动作Token的技术(实现类)

  1. jwt工作流程

    • 用户提供其凭据(通常是用户名和密码)进行身份验证。
    • 服务器对这些凭据进行验证,并在验证成功后创建一个JWT。
    • 服务器将JWT发送给客户端,并客户端在后续的请求中将JWT附加在请求头或参数中。
    • 服务器接收到请求后,验证JWT的签名和有效性,并根据JWT中的声明进行身份验证和授权操作
  2. jwt数据组成和包含信息

    JWT由三部分组成: header(头部).payload(载荷).signature(签名)

    我们需要理解的是, jwt可以携带很多信息! 一般情况,需要加入:有效时间,签名秘钥,其他用户标识信息!

    有效时间为了保证token的时效性,过期可以重新登录获取!

    签名秘钥为了防止其他人随意解析和校验token数据!

    用户信息为了我们自己解析的时候,知道Token对应的具体用户!

  3. jwt使用和测试

    1. 导入依赖
<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>
  1. 编写配置

    application.yaml

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

    封装jwt技术工具类

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;
        }
    }
}

4. 使用和测试
@org.springframework.boot.test.context.SpringBootTest
public class SpringBootTest {

    @Autowired
    private JwtHelper jwtHelper;

    @Test
    public void test(){
        //生成 传入用户标识
        String token = jwtHelper.createToken(1L);
        System.out.println("token = " + token);

        //解析用户标识
        int userId = jwtHelper.getUserId(token).intValue();
        System.out.println("userId = " + userId);

        //校验是否到期! false 未到期 true到期
        boolean expiration = jwtHelper.isExpiration(token);
        System.out.println("expiration = " + expiration);
    }

}

4.1.2 登录功能实现

一、需求描述

用户在客户端输入用户名密码并向后端提交,后端根据用户名和密码判断登录是否成功,用户有误或者密码有误响应不同的提示信息!

二、接口描述

  • url地址: user/login
  • 请求方式:POST
  • 请求参数:
{
    "username":"zhangsan", //用户名
    "userPwd":"123456"     //明文密码
}
  • 响应数据:
  • 成功
{
   "code":"200",         // 成功状态码 
   "message":"success"   // 成功状态描述
   "data":{
    "token":"... ..." // 用户id的token
  }
}
  • 失败
{
   "code":"501",
   "message":"用户名有误"
   "data":{}
}
{
   "code":"503",
   "message":"密码有误"
   "data":{}
}

三、实现代码

  1. controller

    @RestController
    @RequestMapping("user")
    @CrossOrigin
    public class UserController {
    
        @Autowired
        private UserService userService;
    
         /** 大概流程:
         *    1. 账号进行数据库查询 返回用户对象
         *    2. 对比用户密码(md5加密)
         *    3. 成功,根据userId生成token -> map key=token value=token值 - result封装
         *    4. 失败,判断账号还是密码错误,封装对应的枚举错误即可
         */
        @PostMapping("login")
        public Result login(@RequestBody User user){
            Result result = userService.login(user);
            return result;
        }
    }
    
  2. service

    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService{
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private JwtHelper jwtHelper;
    
        /**
         * 登录业务实现
         *    1.根据账号查询
         *    2.账号判断
         *    3.密码判断
         * @param user
         * @return result封装
         */
        @Override
        public Result login(User user) {
    
            // 1.根据账号查询
            LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(User::getUsername,user.getUsername());
            User loginUser = userMapper.selectOne(lambdaQueryWrapper);
    
            // 2.账号判断
            if (loginUser == null){
                // 账号错误
                return Result.build(null, ResultCodeEnum.USERNAME_ERROR);
            }
    
            // 3.密码判断
            if (!StringUtils.isEmpty(user.getUserPwd())
                    && loginUser.getUserPwd().equals(MD5Util.encrypt(user.getUserPwd())))
            {
                // 密码正确
                // 根据用户唯一标识生成token
                String token = jwtHelper.createToken(Long.valueOf(loginUser.getUid()));
    
                // 根据获取格式,需要放到map集合里
                Map data = new HashMap();
                data.put("token",token);
    
                return Result.ok(data);
            }
            // 密码错误
            return Result.build(null,ResultCodeEnum.PASSWORD_ERROR);
        }
    }
    

4.1.3 根据token获取用户数据

一、需求描述

客户端发送请求,提交token请求头,后端根据token请求头获取登录用户的详细信息并响应给客户端进行存储

二、接口描述

  • url地址:user/getUserInfo
  • 请求方式:GET
  • 请求头:
token: token内容
  • 响应数据:
  • 成功
{
    "code": 200,
    "message": "success",
    "data": {
        "loginUser": {
            "uid": 1,
            "username": "zhangsan",
            "userPwd": "",
            "nickName": "张三"
        }
    }
}
  • 失败
{
    "code": 504,
    "message": "notLogin",
    "data": null
}

三、代码实现

  1. controller

    /**
         * 获取token对应用户
         * 大概流程:
         *   1.获取token,解析token对应的userId
         *   2.根据userId,查询用户数据
         *   3.将用户数据的密码置空,并且把用户数据封装到结果中key = loginUser
         *   4.失败返回504 (本次先写到当前业务,后期提取到拦截器和全局异常处理器)
         */
    @GetMapping("getUserInfo")
    public Result getUserInfo(@RequestHeader String token){
        Result result = userService.getUserInfo(token);
        return result;
    }
    
  2. service

    /**
         * 获取token对应用户数据
         *      1.判断token是否过期
         *      2.token未过期,获取token对应的用户
         *      3.查询数据
         * @param token
         * @return result封装
         */
    @Override
    public Result getUserInfo(String token) {
        // 1.判断token是否过期
        if (jwtHelper.isExpiration(token)){
            // true过期,直接返回未登陆
            return Result.build(null,ResultCodeEnum.NOTLOGIN);
        }
    
        // 2.token未过期,获取token对应的用户
        int userId = jwtHelper.getUserId(token).intValue();
    
        // 3.查询数据
        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.NOTLOGIN);
    }
    

4.1.4 注册用户名检查是否存在

一、需求描述

用户在注册时输入用户名时,立刻将用户名发送给后端,后端根据用户名查询用户名是否可用并做出响应

二、接口描述

  • url地址:user/checkUserName
  • 请求方式:POST
  • 请求参数:param形式
username=zhangsan
  • 响应数据:

  • 成功

{
   "code":"200",
   "message":"success"
   "data":{}
}
  • 失败
{
    "code":"505",
   "message":"用户名占用"
   "data":{}
}

三、代码实现

  1. controller

    /**
         * 检查注册用户名是否可用
         * 实现步骤:
         *   1. 获取账号数据
         *   2. 根据账号进行数据库查询
         *   3. 结果封装
         */
    @PostMapping("checkUserName")
    public Result checkUserName(String username){
        Result result = userService.checkUserName(username);
        return result;
    }
    
  2. service

    /**
         * 检查账号是否可以注册
         *    1.获取账号数据
         *    2.根据账号进行数据库查询
         *    3.结果封装
         * @param username 账号信息
         * @return
         */
    @Override
    public Result checkUserName(String username) {
        // 1.获取账号数据
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getUsername,username);
    
        // 2.根据账号进行数据库查询
        User user = userMapper.selectOne(lambdaQueryWrapper);
    
        // 3.结果封装
        if (user != null){
            // 如果用户名存在,则不能注册
            return Result.build(null,ResultCodeEnum.USERNAME_USED);
        }
    
        // 用户名不存在,可以注册
        return Result.ok(null);
    }
    

4.1.5 用户注册功能

一、需求描述

客户端将新用户信息发送给服务端,服务端将新用户存入数据库,存入之前做用户名是否被占用校验,校验通过响应成功提示,否则响应失败提示

二、接口描述

  • url地址:user/regist
  • 请求方式:POST
  • 请求参数:
{
    "username":"zhangsan",
    "userPwd":"123456", 
    "nickName":"张三"
}
  • 响应数据:

  • 成功

{
   "code":"200",
   "message":"success"
   "data":{}
}
  • 失败
{
   "code":"505",
   "message":"用户名占用"
   "data":{}
}

三、代码实现

  1. controller

    /**
         *  用户注册功能
         *  实现步骤:
         *   1. 将密码加密
         *   2. 将数据插入
         *   3. 判断结果,成 返回200 失败 505
         */
    @PostMapping("regist")
    public Result regist(@RequestBody User user){
        Result result = userService.regist(user);
        return result;
    }
    
  2. service

    /**
         * 注册业务
         *  1.检查账号是否已经注册
         *  2.密码加密处理
         *  3.账号数据保存
         *  4.返回结果
         * @param user
         * @return
         */
    @Override
    public Result regist(User user) {
    
        // 1.检查账号是否已经注册
        LambdaQueryWrapper<User> lambdaQueryWrapper =
            new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getUsername,user.getUsername());
        User loginUser = userMapper.selectOne(lambdaQueryWrapper);
    
        if (loginUser != null){
            return Result.build(null,ResultCodeEnum.USERNAME_USED);
        }
    
        // 2.密码加密处理
        user.setUserPwd(MD5Util.encrypt(user.getUserPwd()));
    
        // 3.账号数据保存
        userMapper.insert(user);
    
        return Result.ok(null);
    }
    

4.2 首页模块开发

4.2.1 查询首页分类

一、需求描述

进入新闻首页,查询所有分类并动态展示新闻类别栏位

二、接口描述

  • url地址:portal/findAllTypes

  • 请求方式:Get

  • 请求参数:无

  • 响应数据:

  • 成功

{
   "code":"200",
   "message":"OK"
   "data":{
            [
                {
                    "tid":"1",
                    "tname":"新闻"
                },
                {
                    "tid":"2",
                    "tname":"体育"
                },
                {
                    "tid":"3",
                    "tname":"娱乐"
                },
                {
                    "tid":"4",
                    "tname":"科技"
                },
                {
                    "tid":"5",
                    "tname":"其他"
                }
            ]
    }
}

三、代码实现

  1. controller

    @RestController
    @RequestMapping("portal")
    @CrossOrigin
    public class PortalController {
    
        @Autowired
        private TypeService typeService;
    
        /**
         * 查询全部类别信息
         * @return
         */
        @GetMapping("findAllTypes")
        public Result findAllTypes(){
            // 直接调用业务层,查询全部数据
            List<Type> list = typeService.list();
            return  Result.ok(list);
        }
    }
    

4.2.2 分页查询

一、需求描述

  • 客户端向服务端发送查询关键字,新闻类别,页码数,页大小
  • 服务端根据条件搜索分页信息,返回含页码数,页大小,总页数,总记录数,当前页数据等信息,并根据时间降序,浏览量降序排序

二、接口描述

  • url地址:portal/findNewsPage
  • 请求方式:Post
  • 请求参数:
{
    "keyWords":"马斯克", // 搜索标题关键字
    "type":0,           // 新闻类型
    "pageNum":1,        // 页码数
    "pageSize":10     // 页大小
}
  • 响应数据:
  • 成功
{
   "code":"200",
   "message":"success"
   "data":{
      "pageInfo":{
        "pageData":[
          {
            "hid":"1",                     // 新闻id 
            "title":"尚硅谷宣布 ... ...",   // 新闻标题
            "type":"1",                    // 新闻所属类别编号
            "pageViews":"40",              // 新闻浏览量
            "pastHours":"3" ,              // 发布时间已过小时数
            "publisher":"1"                // 发布用户ID
        },
        {
            "hid":"1",                     // 新闻id 
            "title":"尚硅谷宣布 ... ...",   // 新闻标题
            "type":"1",                    // 新闻所属类别编号
            "pageViews":"40",              // 新闻浏览量
            "pastHours":"3",              // 发布时间已过小时数
            "publisher":"1"                // 发布用户ID
        },
        {
            "hid":"1",                     // 新闻id 
            "title":"尚硅谷宣布 ... ...",   // 新闻标题
            "type":"1",                    // 新闻所属类别编号
            "pageViews":"40",              // 新闻浏览量
            "pastHours":"3",               // 发布时间已过小时数
            "publisher":"1"                // 发布用户ID
        }
        ],
      "pageNum":1,    //页码数
      "pageSize":10,  // 页大小
      "totalPage":20, // 总页数
      "totalSize":200 // 总记录数
    }
  }
}

三、代码实现

  1. 准备条件实体类

    @Data
    public class PortalVo {
        
        private String keyWords;
        private Integer type;
        private Integer pageNum = 1;
        private Integer pageSize =10;
    }
    
  2. 自定义分页查询mapper方法

    public interface HeadlineMapper extends BaseMapper<Headline> {
        // 自定义分页查询方法
        IPage<Map> selectPageMap(IPage<Headline> page, 
                                 @Param("portalVo") PortalVo portalVo);
    }
    
  3. HeadlineMapper.xml编写sql语句

    <select id="selectPageMap" resultType="map">
        select hid,title,type,page_views, 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 != 0">
            and type = #{portalVo.type}
        </if>
    </select>
    
  4. controller

    /**
     * 首页分页查询
     */
    @PostMapping("findNewPage")
    public Result findNewsPage(@RequestBody PortalVo portalVo){
        Result result = headlineService.findNewPage(portalVo);
        return result;
    }
    
  5. service

    /**
    * @description 针对表【news_headline】的数据库操作Service实现
    */
    @Service
    public class HeadlineServiceImpl extends ServiceImpl<HeadlineMapper, Headline>
        implements HeadlineService{
    	
        // 自动装备HeadlineMapper
        @Autowired
        private HeadlineMapper headlineMapper;
    
        /**
         * 首页数据查询
         *  1.条件拼接 需要非空判断
         *  2.分页参数
         *  3.分页查询
         *  4.分页数据封装
         *  注意:1.查询需要自定义语句,自定义mapper方法,携带分页
         *      2.返回结果为List<map>,即数组里嵌套map集合
         * @param portalVo
         * @return
         */
        @Override
        public Result findNewsPage(PortalVo portalVo) {
    
            // 1.条件拼接 需要非空判断
            LambdaQueryWrapper<Headline> lambdaQueryWrapper =
                    new LambdaQueryWrapper<>();
            lambdaQueryWrapper.like(!StringUtils.isEmpty(portalVo.getKeyWords()),Headline::getTitle,portalVo.getKeyWords())
                    .eq(portalVo.getType()!=null,Headline::getType,portalVo.getType());
    
            // 2.分页参数
            IPage<Headline> page = new Page<>(portalVo.getPageNum(),portalVo.getPageSize());
    
            // 3.分页查询
            // 查询的结果 "pastHours":"3"   // 发布时间已过小时数 我们查询返回一个map
            // 使用我们自定义分页方法
            headlineMapper.selectPageMap(page,portalVo);
    
            // 4.分页数据封装
            Map<String,Object> pageInfo = new HashMap<>();
            pageInfo.put("pageData",page.getRecords()); // 获得新闻数据
            pageInfo.put("pageNum",page.getCurrent());  // 页码
            pageInfo.put("pageSize",page.getSize());    // 页容量
            pageInfo.put("totalPage",page.getPages());  // 总页数
            pageInfo.put("totalSize",page.getTotal());  // 总条数
    
            Map<String,Object> pageInfoMap = new HashMap<>();
            pageInfoMap.put("pageInfo",pageInfo);
    
            // 相应JSON
            return Result.ok(pageInfoMap);
        }
    }
    

4.2.3 查询头条详情

一、需求描述

  • 用户点击"查看全文"时,向服务端发送新闻id
  • 后端根据新闻id查询完整新闻文章信息并返回
  • 后端要同时让新闻的浏览量+1

二、接口描述

  • url地址:portal/showHeadlineDetail
  • 请求方式:Post
  • 请求参数:
hid=1 param形成参数
  • 响应数据:

  • 成功

{
    "code":"200",
    "message":"success",
    "data":{
        "headline":{
            "hid":"1",                     // 新闻id 
            "title":"马斯克宣布 ... ...",   // 新闻标题
            "article":"... ..."            // 新闻正文
            "type":"1",                    // 新闻所属类别编号
            "typeName":"科技",             // 新闻所属类别
            "pageViews":"40",              // 新闻浏览量
            "pastHours":"3" ,              // 发布时间已过小时数
            "publisher":"1" ,              // 发布用户ID
            "author":"张三"                 // 新闻作者
        }
    }
}

三、代码实现

  1. controller

     /**
     * 首页详情接口
     * @param hid
     * @return
     */
    @PostMapping("showHeadlineDetail")
    public Result showHeadlineDetail(Integer hid){
        Result result = headlineService.showHeadlineDetail(hid);
        return result;
    }
    
  2. service

    /**
         * 详情数据查询
         *   1.实现根据id的查询(多表查询)
         *   2.拼接头条对象(阅读量和version)进行数据更新
         * 注意: 是多表查询 , 需要更新浏览量+1
         */
    @Override
    public Result showHeadlineDetail(Integer hid) {
    
        // 1.实现根据id的查询(多表查询)
        Map headLineDetail = headlineMapper.selectDelailMap(hid);
    
        // 2.拼接头条对象(阅读量和version)进行数据更新
        Headline headline = new Headline();
        headline.setHid(hid);
    
        // 设置阅读量 + 1
        headline.setPageViews((Integer) headLineDetail.get("pageViews") + 1);
        // 更新版本
        headline.setVersion((Integer) headLineDetail.get("version"));
    	// 保存上面修改
        headlineMapper.updateById(headline);
    
        // 结果封装
        Map<String,Object> pageInfoMap = new HashMap<>();
        pageInfoMap.put("headline",headLineDetail);
    
        return Result.ok(pageInfoMap);
    }
    
  3. mapper接口

    /**
     * 分页查询头条详情
     * @param hid
     * @return
     */
    Map selectDetailMap(Integer hid);
    
  4. HeadlineMapper.xml添加sql语句

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

4.3 头条模块开发

4.3.1 登陆认证与授权

一、需求描述

  • 客户端在进入发布页前、发布新闻前、进入修改页前、修改前、删除新闻前先向服务端发送请求携带token请求头
  • 后端接收token请求头后,校验用户登录是否过期并做响应
  • 前端根据响应信息提示用户进入登录页还是进入正常业务页面

二、接口描述

  • url地址:user/checkLogin
  • 请求方式:get
  • 请求参数: 无
  • 请求头: token:用户的token
  • 响应数据:
  • 未过期:
{
    "code":"200",
    "message":"success",
    "data":{}
}
  • 过期:
{
    "code":"504",
    "message":"loginExpired",
    "data":{}
}

三、代码实现

  1. controller 【登录检查】防止token过期还能还能使用功能

    /**
      * 登陆检查,防止token过期
      */
    @GetMapping("checkLogin")
    public Result checkLogin(@RequestHeader String token){
    
        //没有传或者过期 未登录
        if (StringUtils.isEmpty(token) || jwtHelper.isExpiration(token)){
            return Result.build(null, ResultCodeEnum.NOTLOGIN);
        }
        return Result.ok(null);
    }
    
  2. 拦截器 【所有/headline开头都需要检查登陆】

    /**
     * Description:登录拦截器
     *      检查请求头是否包含有token
     *      有并且有效,放行
     *      无,拦截,返回504
     */
    @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");
            // 检查token是否有效
            if(StringUtils.isEmpty(token) || jwtHelper.isExpiration(token)){
                // 没有或过期拦截
                Result result = Result.build(null, ResultCodeEnum.NOTLOGIN);
                // 将结果使用ObjectMapper转化为json字符串
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(result);
                // 响应返回json数据
                response.getWriter().print(json);
    
                return false;
            }
            return true;
        }
    }
    
  3. 拦截器配置

    /**
     * Description:拦截器配置类
     */
    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    
        @Autowired
        private LoginProtectInterceptor loginProtectInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(loginProtectInterceptor)
                .addPathPatterns("/headline/**");
        }
    }
    

4.3.2 头条发布实现

一、需求描述

  • 用户在客户端输入发布的新闻信息完毕后
  • 发布前先请求后端的登录校验接口验证登录
  • 登录通过则提交新闻信息
  • 后端将新闻信息存入数据库

二、接口描述

  • url地址:headline/publish

  • 请求方式:post

  • 请求头:token:... ...

  • 请求参数:

{
    "title":"尚硅谷宣布 ... ...",   // 文章标题
    "article":"... ...",          // 文章内容
    "type":"1"                    // 文章类别
}
  • 响应数据:
  • 未登录
{
    "code":"504",
    "message":"loginExpired",
    "data":{}
}
  • 成功
{
    "code":"200",
    "message":"success",
    "data":{}
}

三、代码实现

  1. controller

    /**
         * 头条发布
         * 实现步骤:
         *   1. 根据token获取userId [无需校验,拦截器会校验]
         *   2. 封装headline数据
         *   3. 插入数据即可
         */
    @PostMapping("publish")
    public Result publish(@RequestBody Headline headline,
                          @RequestHeader String token){
    
        Result result = headlineService.publish(headline,token);
        return result;
    }
    
  2. service

    /**
         * 头条发布
         * 实现步骤:
         *   1. 根据token获取userId [无需校验,拦截器会校验]
         *   2. 补全headlin数据
         *   3. 封装headline数据
         *   4. 插入数据即可
         */
    @Override
    public Result publish(Headline headline, String token) {
    
        // 1.根据token获取userId [无需校验,拦截器会校验]
        int userId = jwtHelper.getUserId(token).intValue();
        headline.setPublisher(userId);
    
        // 2.补全headlin数据
        headline.setPageViews(0);
        headline.setCreateTime(new Date());
        headline.setUpdateTime(new Date());
    
        // 3.封装headline数据
        headlineMapper.insert(headline);
    
        return Result.ok(null);
    }
    

4.3.3 修改头条回显

一、需求描述

  • 前端先调用登录校验接口,校验登录是否过期
  • 登录校验通过后 ,则根据新闻id查询新闻的完整信息并响应给前端

二、接口描述

  • url地址:headline/findHeadlineByHid
  • 请求方式:Post
  • 请求参数:
hid=1 param形成参数
  • 响应数据:
  • 成功
{
    "code":"200",
    "message":"success",
    "data":{
        "headline":{
            "hid":"1",
            "title":"马斯克宣布",
            "article":"... ... ",
            "type":"2"
        }
    }
}

三、代码实现

  1. controller

    /**
         * 修改头条回显
         */
    @PostMapping ("findHeadlineByHid")
    public Result findHeadlineByHid(Integer hid){
        Result result = headlineService.findHeadlineByHid(hid);
        return result;
    }
    
  2. service

    /**
      * 修改头条回显
      *  1.根据id查询回显
      */
    @Override
    public Result findHeadlineByHid(Integer hid) {
        // 1.根据id查询回显
        Headline headline = headlineMapper.selectById(hid);
    
        Map<String,Object> pageInfoMap = new HashMap<>();
        pageInfoMap.put("headline",headline);
    
        return Result.ok(pageInfoMap);
    }
    

4.3.4 头条修改实现

一、需求描述

  • 客户端将新闻信息修改后,提交前先请求登录校验接口校验登录状态
  • 登录校验通过则提交修改后的新闻信息,后端接收并更新进入数据库

二、接口描述

  • url地址:headline/update
  • 请求方式:post
  • 请求参数:
{
    "hid":"1",
    "title":"尚硅谷宣布 ... ...",
    "article":"... ...",
    "type":"2"
}
  • 响应数据:
  • 成功
{
    "code":"200",
    "message":"success",
    "data":{}
}

三、代码实现

  1. controller

    /**
         * 修改头条数据
         */
    @PostMapping("update")
    public Result updateHeadlineData(@RequestBody Headline headline){
        Result result = headlineService.updateHeadlineData(headline);
        return result;
    }
    
  2. service

    /**
         * 修改头条数据
         *  1.查询version版本
         *  2.补全属性,修改时间 , 版本!
         */
    @Override
    public Result updateHeadlineData(Headline headline) {
    
        // 1.查询version版本
        Integer version = headlineMapper.selectById(headline.getHid()).getVersion();
        // 2.补全属性,修改时 ,版本!
        headline.setVersion(version);
        headline.setUpdateTime(new Date());
        // 3.结果封装
        headlineMapper.updateById(headline);
    
        return Result.ok(null);
    }
    

4.3.5 删除头条功能

一、需求描述

  • 将要删除的新闻id发送给服务端
  • 服务端校验登录是否过期,未过期则直接删除,过期则响应登录过期信息

二、接口描述

  • url地址:headline/removeByHid

  • 请求方式:post

  • 请求参数:

hid=1 param形成参数
  • 响应数据:

  • 成功

{
    "code":"200",
    "message":"success",
    "data":{}
}

三、代码实现

直接在controller层实现即可

/**
* 删除头条数据
*/
@PostMapping("removeByHid")
public Result removeByHid(Integer hid){
    headlineService.removeById(hid);
    return Result.ok(null);
}
posted @ 2024-06-09 13:12  粤先生  阅读(124)  评论(0)    收藏  举报