新闻头条项目
一、项目介绍
二、功能架构
1.平台管理端功能大纲
2.自媒体端功能大纲
3.APP主要功能大纲
三、数据库
四、通用接口说明
1.通用响应对象PageResponseResult
2.通用的请求dtos
3.通用的异常枚举
五、平台管理端功能
1.频道管理
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引导类中
定义微服务接口
持久层
业务层
业务层实现类
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;
}
}
控制层
2.敏感词管理
3.登录功能
加密前置知识
MD5密码加密
md5相同的密码每次加密都一样,不太安全
手动加密(md5+随机字符串)
BCrypt密码加密
String gensalt = BCrypt.gensalt();//这个是盐 29个字符,随机生成
System.out.println(gensalt);
String password = BCrypt.hashpw("123456", gensalt); //根据盐对密码进行加密
System.out.println(password);//加密后的字符串前29位就是盐
jwt介绍
token认证
什么是JWT?
登录功能加入网关gateway
全局过滤器实现jwt校验
4.用户认证(审核是核心功能)
核心功能:用户认证后审核
@EnableFeignClients使用feign进行远程调用,平台管理admin端远程调用自媒体用户和文章作者模块。
分布式事务解决认证过程中数据不一致问题
CAP定理
BASE理论
六、自媒体端功能
1.自媒体用户保存
在新建自媒体账户时需要把apuser信息赋值给自媒体用户
2.查询作者和保存作者
3.素材管理
上传图片到fastdfs,同时要保存一份数据到表中,方便后期管理
图片上传
图片删除
4.自媒体用户登录
自媒体登录操作与admin端登录思路是一样的
5.自媒体端文章列表
6.自媒体文章发布
文章删除
当文章状态为9(已发布)且已上架则不能删除文章,下架状态可以删除,如果是其他状态可以删除
删除文章之前需要先把素材与文章的关系删除掉
文章上下架
当前已经发布(状态为9)的文章可以上架(enable = 1),也可以下架(enable = 0)
在上架和下架操作的同时,需要同步app端的文章配置信息,暂时不做,后期讲到审核文章的时候再优化
7. 自媒体文章审核(核心功能)
表结构
分布式id
后期由于文章数量较多,需要采用分库分表,id为自增策略则可能产生重复id。
定时任务扫描待发布文章
七、全局异常处理
@ControllerAdvice和@ExceptionHandler注解配合使用
八、项目的问题
1.项目中用到了nacos,它与eureka有什么区别
2.分布式事务解决方案
基于XA协议的两阶段提交
TCC补偿机制
消息最终一致性
3.项目中使用Seata实现分布式事务
Seata事务模式-AT模式
4.分布式文件系统FastDFS
上传流程
5.kafka面试题
相关概念
发送消息的工作原理
消费者工作原理
九、项目核心功能
1.登录
前置知识
常见的加密方式
可逆加密算法
解释: 加密后, 密文可以反向解密得到密码原文.
对称加密
非对称加密
不可逆加密算法
Base64编码
密码加密的方式选型
MD5密码加密
手动加密(md5+随机字符串)
BCrypt密码加密
boolean checkpw = BCrypt.checkpw("123456", "$2a$10$61ogZY7EXsMDWeVGQpDq3OBF1.phaUu7.xrwLyWFTOu8woE08zMIW");
System.out.println(checkpw);
项目中最终采用的方式
项目中采用第二种手动加盐的方式进行加密
jwt介绍
token认证
什么是JWT?
生成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的优势
项目中登录功能的实现方式
流程图
网关中全局过滤器的实现
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;
}
}