新闻头条项目

一、项目介绍

新闻头条项目_java

二、功能架构

新闻头条项目_java_02

1.平台管理端功能大纲

新闻头条项目_自媒体_03

2.自媒体端功能大纲

新闻头条项目_spring_04

3.APP主要功能大纲

新闻头条项目_spring_05

三、数据库

新闻头条项目_spring_06
新闻头条项目_分布式事务_07

四、通用接口说明

1.通用响应对象PageResponseResult

新闻头条项目_java_08

2.通用的请求dtos

新闻头条项目_java_09

3.通用的异常枚举

新闻头条项目_json_10

五、平台管理端功能

1.频道管理

新闻头条项目_分布式事务_11
新闻头条项目_自媒体_12

curd的说明

先开发对应实体类
package com.heima.model.admin.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 * 频道信息表
 * </p>
 *
 * @author itheima
 */
@Data
@TableName("ad_channel")
public class AdChannel implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 频道名称
     */
    @TableField("name")
    private String name;

    /**
     * 频道描述
     */
    @TableField("description")
    private String description;

    /**
     * 是否默认频道
     */
    @TableField("is_default")
    private Boolean isDefault;

    @TableField("status")
    private Boolean status;

    /**
     * 默认排序
     */
    @TableField("ord")
    private Integer ord;

    /**
     * 创建时间
     */
    @TableField("created_time")
    private Date createdTime;

}
分页

采用mybatis-plus分页插件PaginationInterceptor,写在springboot引导类中

定义微服务接口

新闻头条项目_spring_13
新闻头条项目_json_14

持久层

新闻头条项目_java_15

业务层

新闻头条项目_自媒体_16
业务层实现类

package com.heima.admin.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.admin.mapper.AdChannelMapper;
import com.heima.admin.service.AdChannelService;
import com.heima.model.admin.dtos.ChannelDto;
import com.heima.model.admin.pojos.AdChannel;
import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

@Service
public class AdChannelServiceImpl extends ServiceImpl<AdChannelMapper, AdChannel> implements AdChannelService {



    @Override
    public ResponseResult findByNameAndPage(ChannelDto dto) {

        //1.参数检测
        if(dto==null){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        //分页参数检查
        dto.checkParam();

        //2.安装名称模糊分页查询
        Page page = new Page(dto.getPage(),dto.getSize());
        LambdaQueryWrapper<AdChannel> lambdaQueryWrapper = new LambdaQueryWrapper();
        if(StringUtils.isNotBlank(dto.getName())){
            lambdaQueryWrapper.like(AdChannel::getName,dto.getName());
        }
        IPage result = page(page, lambdaQueryWrapper);

        //3.结果封装
        ResponseResult responseResult = new PageResponseResult(dto.getPage(),dto.getSize(),(int)result.getTotal());
        responseResult.setData(result.getRecords());
        return responseResult;
    }
}
控制层

新闻头条项目_spring_17

2.敏感词管理

新闻头条项目_自媒体_18
新闻头条项目_自媒体_19

3.登录功能

加密前置知识

MD5密码加密

md5相同的密码每次加密都一样,不太安全

手动加密(md5+随机字符串)

新闻头条项目_java_20

BCrypt密码加密
String gensalt = BCrypt.gensalt();//这个是盐  29个字符,随机生成
System.out.println(gensalt);
String password = BCrypt.hashpw("123456", gensalt);  //根据盐对密码进行加密
System.out.println(password);//加密后的字符串前29位就是盐

jwt介绍

token认证

新闻头条项目_java_21

什么是JWT?

新闻头条项目_java_22
新闻头条项目_分布式事务_23
新闻头条项目_json_24

登录功能加入网关gateway

全局过滤器实现jwt校验
新闻头条项目_json_25
新闻头条项目_json_26

4.用户认证(审核是核心功能)

新闻头条项目_java_27
新闻头条项目_分布式事务_28

核心功能:用户认证后审核

新闻头条项目_自媒体_29
新闻头条项目_分布式事务_30
新闻头条项目_分布式事务_31
@EnableFeignClients使用feign进行远程调用,平台管理admin端远程调用自媒体用户和文章作者模块。

分布式事务解决认证过程中数据不一致问题
CAP定理

新闻头条项目_自媒体_32
新闻头条项目_json_33
新闻头条项目_java_34
新闻头条项目_java_35

BASE理论

新闻头条项目_json_36

六、自媒体端功能

1.自媒体用户保存

新闻头条项目_自媒体_37
在新建自媒体账户时需要把apuser信息赋值给自媒体用户

2.查询作者和保存作者

新闻头条项目_自媒体_38

3.素材管理

上传图片到fastdfs,同时要保存一份数据到表中,方便后期管理
新闻头条项目_分布式事务_39

图片上传

新闻头条项目_分布式事务_40
新闻头条项目_java_41

图片删除

新闻头条项目_分布式事务_42

4.自媒体用户登录

自媒体登录操作与admin端登录思路是一样的

5.自媒体端文章列表

新闻头条项目_spring_43
新闻头条项目_java_44

6.自媒体文章发布

新闻头条项目_java_45
新闻头条项目_自媒体_46

文章删除

新闻头条项目_spring_47
当文章状态为9(已发布)且已上架则不能删除文章,下架状态可以删除,如果是其他状态可以删除
删除文章之前需要先把素材与文章的关系删除掉

文章上下架

当前已经发布(状态为9)的文章可以上架(enable = 1),也可以下架(enable = 0)
在上架和下架操作的同时,需要同步app端的文章配置信息,暂时不做,后期讲到审核文章的时候再优化

7. 自媒体文章审核(核心功能)

新闻头条项目_分布式事务_48
新闻头条项目_自媒体_49

表结构

新闻头条项目_java_50
新闻头条项目_分布式事务_51
新闻头条项目_spring_52
新闻头条项目_spring_53
新闻头条项目_spring_54

分布式id

后期由于文章数量较多,需要采用分库分表,id为自增策略则可能产生重复id。
新闻头条项目_java_55
新闻头条项目_自媒体_56

定时任务扫描待发布文章

新闻头条项目_java_57

七、全局异常处理

@ControllerAdvice和@ExceptionHandler注解配合使用
新闻头条项目_spring_58

八、项目的问题

1.项目中用到了nacos,它与eureka有什么区别

新闻头条项目_自媒体_59

2.分布式事务解决方案

基于XA协议的两阶段提交

新闻头条项目_自媒体_60
新闻头条项目_java_61

TCC补偿机制

新闻头条项目_json_62
新闻头条项目_分布式事务_63

消息最终一致性

新闻头条项目_spring_64
新闻头条项目_分布式事务_65

3.项目中使用Seata实现分布式事务

Seata事务模式-AT模式

新闻头条项目_json_66

4.分布式文件系统FastDFS

新闻头条项目_自媒体_67
新闻头条项目_分布式事务_68
上传流程
新闻头条项目_自媒体_69

5.kafka面试题

相关概念

新闻头条项目_分布式事务_70
新闻头条项目_json_71
新闻头条项目_分布式事务_72
新闻头条项目_spring_73

发送消息的工作原理

新闻头条项目_json_74

消费者工作原理

新闻头条项目_自媒体_75

九、项目核心功能

1.登录

前置知识

常见的加密方式
可逆加密算法

解释: 加密后, 密文可以反向解密得到密码原文.

对称加密
新闻头条项目_json_76
非对称加密
新闻头条项目_自媒体_77
新闻头条项目_java_78

不可逆加密算法

新闻头条项目_spring_79

Base64编码

新闻头条项目_java_80

密码加密的方式选型

MD5密码加密

新闻头条项目_json_81

手动加密(md5+随机字符串)

新闻头条项目_json_82

BCrypt密码加密

新闻头条项目_json_83

boolean checkpw = BCrypt.checkpw("123456",     "$2a$10$61ogZY7EXsMDWeVGQpDq3OBF1.phaUu7.xrwLyWFTOu8woE08zMIW");
System.out.println(checkpw);
项目中最终采用的方式

项目中采用第二种手动加盐的方式进行加密
新闻头条项目_java_84

jwt介绍

token认证

新闻头条项目_json_85

什么是JWT?

新闻头条项目_spring_86
新闻头条项目_分布式事务_87
新闻头条项目_自媒体_88

生成token

需要引入jwt相关依赖

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

工具类

package com.heima.utils.common;

import io.jsonwebtoken.*;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;

public class AppJwtUtil {

    // TOKEN的有效期一天(S)
    private static final int TOKEN_TIME_OUT = 3_600;
    // 加密KEY
    private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
    // 最小刷新间隔(S)
    private static final int REFRESH_TIME = 300;

    // 生产ID
    public static String getToken(Long id){
        Map<String, Object> claimMaps = new HashMap<>();
        claimMaps.put("id",id);
        long currentTime = System.currentTimeMillis();
        return Jwts.builder()
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(new Date(currentTime))  //签发时间
                .setSubject("system")  //说明
                .setIssuer("heima") //签发者信息
                .setAudience("app")  //接收用户
                .compressWith(CompressionCodecs.GZIP)  //数据压缩方式
                .signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
                .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000))  //过期时间戳
                .addClaims(claimMaps) //cla信息
                .compact();
    }

    /**
     * 获取token中的claims信息
     *
     * @param token
     * @return
     */
    private static Jws<Claims> getJws(String token) {
            return Jwts.parser()
                    .setSigningKey(generalKey())
                    .parseClaimsJws(token);
    }

    /**
     * 获取payload body信息
     *
     * @param token
     * @return
     */
    public static Claims getClaimsBody(String token) {
        try {
            return getJws(token).getBody();
        }catch (ExpiredJwtException e){
            return null;
        }
    }

    /**
     * 获取hearder body信息
     *
     * @param token
     * @return
     */
    public static JwsHeader getHeaderBody(String token) {
        return getJws(token).getHeader();
    }

    /**
     * 是否过期
     *
     * @param claims
     * @return -1:有效,0:有效,1:过期,2:过期
     */
    public static int verifyToken(Claims claims) {
        if(claims==null){
            return 1;
        }
        try {
            claims.getExpiration()
                    .before(new Date());
            // 需要自动刷新TOKEN
            if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
                return -1;
            }else {
                return 0;
            }
        } catch (ExpiredJwtException ex) {
            return 1;
        }catch (Exception e){
            return 2;
        }
    }

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    public static void main(String[] args) {
       /* Map map = new HashMap();
        map.put("id","11");*/
        System.out.println(AppJwtUtil.getToken(1102L));
        Jws<Claims> jws = AppJwtUtil.getJws("eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQqEMAwA_5KzhURNt_qb1KZYQSi0wi6Lf9942NsMw3zh6AVW2DYmDGl2WabkZgreCaM6VXzhFBfJMcMARTqsxIG9Z888QLui3e3Tup5Pb81013KKmVzJTGo11nf9n8v4nMUaEY73DzTabjmDAAAA.4SuqQ42IGqCgBai6qd4RaVpVxTlZIWC826QA9kLvt9d-yVUw82gU47HDaSfOzgAcloZedYNNpUcd18Ne8vvjQA");
        Claims claims = jws.getBody();
        System.out.println(claims.get("id"));

    }
}
token相对于session和cookie的优势

优势

项目中登录功能的实现方式

流程图

新闻头条项目_spring_89
新闻头条项目_java_90

网关中全局过滤器的实现
package com.heima.admin.gateway.filter;

import com.heima.admin.gateway.utils.AppJwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@Log4j2
public class AuthorizeFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取请求对象和响应对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        //2.判断当前的请求是否为登录,如果是,直接放行
        if(request.getURI().getPath().contains("/login/in")){
            //放行
            return chain.filter(exchange);
        }

        //3.获取当前用户的请求头jwt信息
        HttpHeaders headers = request.getHeaders();
        String jwtToken = headers.getFirst("token");

        //4.判断当前令牌是否存在
        if(StringUtils.isEmpty(jwtToken)){
            //如果不存在,向客户端返回错误提示信息
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        try {
            //5.如果令牌存在,解析jwt令牌,判断该令牌是否合法,如果不合法,则向客户端返回错误信息
            Claims claims = AppJwtUtil.getClaimsBody(jwtToken);
            int result = AppJwtUtil.verifyToken(claims);
            if(result == 0 || result == -1){
                //5.1 合法,则向header中重新设置userId
                Integer id = (Integer) claims.get("id");
                log.info("find userid:{} from uri:{}",id,request.getURI());
                //重新设置token到header中
                ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
                    httpHeaders.add("userId", id + "");
                }).build();
                exchange.mutate().request(serverHttpRequest).build();
            }
        }catch (Exception e){
            e.printStackTrace();
            //想客户端返回错误提示信息
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }


        //6.放行
        return chain.filter(exchange);
    }

    /**
     * 优先级设置
     * 值越小,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}