1 项目简介

1.1 功能结构图

1.2 功能架构图

1.3 课程安排

1.4 项目结构

1.5 项目运行

01.前端
    虚拟机      root   / itcast
    虚拟机      itcast / itcast
    ---------------------------------------------------------------------------------------------------------
    app端       http://127.0.0.1:8801                         13511223456 / admin
    自媒体      http://127.0.0.1:8802                         admin       / admin
    平台管理    http://127.0.0.1:8803                         guest       / guest
    ---------------------------------------------------------------------------------------------------------
    xxl-job    http://127.0.0.1:8888/xxl-job-admin/           admin       / 123456

02.后端
    AppGatewayApplication           51601
    WemediaGatewayAplication        51602
    AdminGatewayApplication         51603
    -------------------------------------------------------------------------------------------------------------
    UserApplication                 51801                    http://localhost:51801/swagger-ui.html          http://localhost:51801/doc.html
    ArticleApplication              51802
    WemediaApplication              51803
    SearchApplication               51804
    BehaviorApplication             51805
    CommentApplication              51806
    AdminApplication                51809
    ScheduleApplication             51701
    ---------------------------------------------------------------------------------------------------------
    FreemarkerApplication           8881                     http://127.0.0.1:8881/basic                     http://127.0.0.1:8881/list

03.测试
    http://localhost:51801/api/v1/login/login_auth

    被gateway路由到
    http://localhost:51601/user/api/v1/login/login_auth

2 数据库

垂直分表:将一个表的字段分散到多个表中,每个表存储其中一部分字段。
优势:
1.减少IO争抢,减少锁表的几率,查看文章概述与文章详情互不影响
2.充分发挥高频数据的操作效率,对文章概述数据操作的高效率不会被操作文章详情数据的低效率所拖累。

拆分规则:
1.把不常用的字段单独放在一张表
2.把text,blob等大字段拆分出来单独放在一张表
3.经常组合查询的字段单独放在一张表中

2.1 leadnews_user

2.1.1 ap_user:APP用户信息表

2.1.2 ap_user_fan:APP用户粉丝信息表

2.1.3 ap_user_follow:APP用户关注信息表

2.1.4 ap_user_realname:APP实名认证信息表

2.2 leadnews_article

2.2.1 ap_article:文章信息表

2.2.2 ap_article_config:APP已发布文章配置表

2.2.3 ap_article_content:APP已发布文章内容表

2.2.4 ap_author:APP文章作者信息表

2.2.5 ap_collection:APP收藏信息表

2.3 leadnews_wemedia

2.3.1 wm_channel:频道信息表

2.3.2 wm_fans_statistics:自媒体粉丝数据统计表

2.3.3 wm_material:自媒体图文素材信息表

2.3.4 wm_news:自媒体图文内容信息表

2.3.5 wm_news_material:自媒体图文引用素材信息表

2.3.6 wm_news_statistics:自媒体图文数据统计表

2.3.7 wm_user:自媒体用户信息表

3 图片上传:MinIO

3.1 依赖

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>7.1.0</version>
</dependency>

3.2 测试类

public static void main(String[] args) {

    FileInputStream fileInputStream = null;
    try {

        fileInputStream =  new FileInputStream("D:\\list.html");;

        //1.创建minio链接客户端
        MinioClient minioClient = MinioClient.builder().credentials("minioadmin", "minioadmin").endpoint("http://127.0.0.1:9000").build();
        //2.上传
        PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                .object("list.html")//文件名
                .contentType("text/html")//文件类型
                .bucket("leadnews")//桶名词  与minio创建的名词一致
                .stream(fileInputStream, fileInputStream.available(), -1) //文件流
                .build();
        minioClient.putObject(putObjectArgs);

        System.out.println("http://127.0.0.1:9000/leadnews/list.html");

    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

3.3 测试:Test

D:\software_ware\Typora\02.Directory02\Chapter06\00.Source.assets\minio-file

-------------------------------------------------------------------------------------------------------------

01.配置文件
    minio:
      accessKey: minioadmin
      secretKey: minioadmin
      bucket: leadnews
      endpoint: http://127.0.0.1:9000
      readPath: http://127.0.0.1:9000

02.测试类
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = MinIOApplication.class)
    public class MinIOTest {
        @Autowired
        private FileStorageService fileStorageService;

        @Test
        public void testUpdateImgFile() {
            try {
                FileInputStream fileInputStream = new FileInputStream("C:\\document\\Background\\01.bio\\09.jpg");
                String filePath = fileStorageService.uploadImgFile("", "09.jpg", fileInputStream);
                System.out.println(filePath);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

3.4 测试:Controller层

@RestController
@RequestMapping("/api/v1/material")
public class WmMaterialController {

    @Autowired
    private FileStorageService fileStorageService;
    
    @PostMapping("/upload_picture")
    public ResponseResult uploadPicture(MultipartFile multipartFile) {

        //1.检查参数
        if(multipartFile == null || multipartFile.getSize() == 0){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        //2.上传图片到minIO中
        String fileName = UUID.randomUUID().toString().replace("-", "");
        //aa.jpg
        String originalFilename = multipartFile.getOriginalFilename();
        String postfix = originalFilename.substring(originalFilename.lastIndexOf("."));
        String fileId = null;
        try {
            fileId = fileStorageService.uploadImgFile("", fileName + postfix, multipartFile.getInputStream());
            log.info("上传图片到MinIO中,fileId:{}",fileId);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("WmMaterialServiceImpl-上传文件失败");
        }

        //3.保存到数据库中
        WmMaterial wmMaterial = new WmMaterial();
        wmMaterial.setUserId(WmThreadLocalUtil.getUser().getId());
        wmMaterial.setUrl(fileId);
        wmMaterial.setIsCollection((short)0);
        wmMaterial.setType((short)0);
        wmMaterial.setCreatedTime(new Date());
        save(wmMaterial);

        //4.返回结果

        return ResponseResult.okResult(wmMaterial);
    }
}

4 后端实现

4.1 过滤器:header的userId生成

4.1.1 AppJwtUtil

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

}

4.1.2 AuthorizeFilter

@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取request和response对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //2.判断是否是登录
        if(request.getURI().getPath().contains("/login")){
            //放行
            return chain.filter(exchange);
        }


        //3.获取token
        String token = request.getHeaders().getFirst("token");

        //4.判断token是否存在
        if(StringUtils.isBlank(token)){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //5.判断token是否有效
        try {
            Claims claimsBody = AppJwtUtil.getClaimsBody(token);
            //是否是过期
            int result = AppJwtUtil.verifyToken(claimsBody);
            if(result == 1 || result  == 2){
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }
        }catch (Exception e){
            e.printStackTrace();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

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

    /**
     * 优先级设置  值越小  优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取request和response对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //2.判断是否是登录
        if(request.getURI().getPath().contains("/login")){
            //放行
            return chain.filter(exchange);
        }

        //3.获取token
        String token = request.getHeaders().getFirst("token");

        //4.判断token是否存在
        if(StringUtils.isBlank(token)){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //5.判断token是否有效
        try {
            Claims claimsBody = AppJwtUtil.getClaimsBody(token);
            //是否是过期
            int result = AppJwtUtil.verifyToken(claimsBody);
            if(result == 1 || result  == 2){
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }
            //获取用户信息
            Object userId = claimsBody.get("id");

            //存储header中
            ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
                httpHeaders.add("userId", userId + "");
            }).build();
            //重置请求
            exchange.mutate().request(serverHttpRequest);

        } catch (Exception e) {
            e.printStackTrace();
        }

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

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

4.2 拦截器:header的setId

4.2.1 WmThreadLocalUtil

public class WmThreadLocalUtil {

    private final static ThreadLocal<WmUser> WM_USER_THREAD_LOCAL = new ThreadLocal<>();

    //存入线程中
    public static void setUser(WmUser wmUser){
        WM_USER_THREAD_LOCAL.set(wmUser);
    }

    //从线程中获取
    public static WmUser getUser(){
        return WM_USER_THREAD_LOCAL.get();
    }

    //清理
    public static void clear(){
        WM_USER_THREAD_LOCAL.remove();
    }

}

4.2.2 HandlerInterceptor

public class WmTokenInterceptor implements HandlerInterceptor {

    /**
     * 得到header中的用户信息,并且存入到当前线程中
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String userId = request.getHeader("userId");
        if(userId != null){
            //存入到当前线程中
            WmUser wmUser = new WmUser();
            wmUser.setId(Integer.valueOf(userId));
            WmThreadLocalUtil.setUser(wmUser);

        }
        return true;
    }

    /**
     * 清理线程中的数据
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        WmThreadLocalUtil.clear();
    }
}

4.2.3 注册自定义拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new WmTokenInterceptor()).addPathPatterns("/**");
    }
}

4.3 自媒体自动审核

4.3.1 审核流程

4.3.2 文本审核、图片审核

https://help.aliyun.com/document_detail/70439.html

https://help.aliyun.com/document_detail/70292.html