SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)

想当个黑客 吼吼吼…

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第1张图片


系列文章目录

1. 项目介绍及环境配置
2. 短信验证码登录
3. 用户信息
4. MongoDB
5. 推荐好友列表/MongoDB集群/动态发布与查看
6. 圈子动态/圈子互动
7. 即时通讯(基于第三方API)
8. 附近的人(百度地图APi)
9. 小视频
10.网关配置
11.后台管理


文章目录

  • 系列文章目录
  • 一、 功能介绍
  • 二、 环境搭建
    • 1. MYSQL数据库
    • 2. Nginx
    • 3. 引入 admin 模块
    • 4. 创建实体类
  • 三、 管理员登录
    • 1. 需求分析
    • 2. 获取图片验证码
      • ⑴. 接口文档
      • ⑵. 编码实现
      • ⑶. 测试
      • ⑷. 页面展示
    • 3. 登录
      • ⑴. 接口文档
      • ⑵. 编码实现
        • ①. Controller
        • ②. Service
      • ⑶. 页面效果
    • 4. 查询用户资料
      • ⑴. 接口文档
      • ⑵. 编码实现
        • ①. vo对象
        • ②. Controller
        • ③. Service
      • ⑶. 页面效果
  • 四、 用户信息管理
    • 1. 查询用户列表
      • ⑴. 接口文档
      • ⑵. 编码实现
        • ①. Controller
        • ②. Service
        • ③. Api
        • ④. ApiImpl
      • ⑶. 页面效果
    • 2. 查询用户详情
      • ⑴. 接口文档
      • ⑵. 编码实现
        • ①. Controller
        • ②. Service
      • ⑶. 页面效果
    • 3. 查询视频列表
      • ⑴. 接口文档
      • ⑵. 编码实现
        • ①. Controller
        • ②. Service
        • ③. Api
        • ④. ApiImpl
      • ⑶. 页面效果
    • 4. 查询动态列表
      • ⑴. 接口文档
      • ⑵. 编码实现
        • ①. Controller
        • ②. Service
        • ③. Api
        • ④. ApiImpl
      • ⑶. 页面效果
    • 5. 用户冻结
      • ⑴. 需求分析
      • ⑵. 冻结
        • ①. 接口文档
        • ②. Controller
        • ③. Service
        • ④. 页面效果
      • ⑶. 解冻
        • ①. 接口文档
        • ②. Controller
        • ③. Service
        • ④. 页面效果
      • ⑷. 登录校验
        • ①. 公共类
        • ②. Service
  • 五、 数据统计
    • 1. 数据采集
      • ⑴. 需求分析
      • ⑵. 发送日志消息
        • ①. RabbitMQ依赖
        • ②. 消息类型
        • ③. 编码实现
          • Ⅰ. yml文件
          • Ⅱ. 工具类 - 发送消息
          • Ⅲ. 登录 - 发送日志消息
          • Ⅳ. 动态 - 发送日志消息
      • ⑶. 获取消息内容
        • ①. 实体类
          • Ⅰ. Log
          • Ⅱ. Analysis
        • ②. Mapper
          • Ⅰ. LogMapper
          • Ⅱ. AnalysisMapper
        • ③. 拦截器
        • ④. 测试
    • 2. 定时任务
      • ⑴. 概述
      • ⑵. 入门案例
        • ①. 启动类
        • ②. 测试类
        • ③. 打印结果
      • ⑶. CRON表达式
      • ⑷. 数据统计
        • ①. 定时器
        • ②. Mapper
        • ③. Service
        • ④. 添加测试数据
  • 六、 内容审核
    • 1. 阿里云内容审核
      • ⑴. 阿里云安全
        • ①. 技术选型
        • ②. 阿里云 - 内容安全
        • ③. AccessKey管理
        • ④. SDK文档
      • ⑵. 入门案例
        • ①. yml配置
        • ②. Properties
        • ③. template
        • ④. 启动类
        • ⑤. 测试类
        • ⑥. 测试结果
      • ⑶. 发送动态审核消息
        • ①. Api
        • ②. ApiImpl
        • ③. Service
      • ⑷. 后台系统获取消息
        • ①. 动态拦截器
        • ②. Api
        • ③. ApiImpl
      • ⑸. 完善查询动态接口
  • 七、 推荐系统
    • 1. 理论知识
      • ⑴. 电商是推荐系统的先行者
      • ⑵. 推荐系统业务流程
      • ⑶. 协同过滤推荐算法
        • ①. 基于用户的推荐 UserCF
        • ②. 基于商品的推荐 ItemCF
      • ⑷. ALS算法
    • 2. 用户推荐
      • ⑴. 流程
      • ⑵. 评分规则
      • ⑶. 推荐系统部署
        • ①. docker
        • ②. SQL数据库
      • ⑷. 测试
    • 3. 动态推荐
      • ⑴. 需求分析
        • ①. 流程说明
        • ②. 流程说明
        • ③. 评分规则
      • ⑵. 创建数据采集系统
        • ①. 新模块
        • ②. pom依赖
        • ③. yml配置文件
        • ④. 启动类
        • ⑤. 实体类
      • ⑶. 动态监听器
    • 4. 视频推荐
      • ⑴. 需求分析
        • ①. 流程说明
        • ②. 评分规则
      • ⑵. 视频监听器
        • ①. 实体类
        • ②. 监听器
        • ③. 发送日志消息
      • ⑶. 测试


一、 功能介绍

探花交友APP建立的后台管理系统,目的是完成探花交友项目的业务闭环,主要功能包括:用户管理、动态管理、审核管理以及系统管理。(实现的功能有:登录、首页、用户管理、动态审核)

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第2张图片



二、 环境搭建

1. MYSQL数据库

网盘资源地址(不需要密码): https://pan.baidu.com/s/1daL566ehyZuQ6s5vzXYNpA?pwd=java

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第3张图片

  • 新建连接 -> MySQL
    SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第4张图片

  • 运行SQL文件
    在这里插入图片描述

表名称 说明
tb_admin 管理员用户表
tb_analysis 统计数据表
tb_log 用户操作日志记录表
tb_freeze_detail 冻结用户记录

2. Nginx

网盘资源地址(不需要密码): https://pan.baidu.com/s/1daL566ehyZuQ6s5vzXYNpA?pwd=java

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第5张图片

解压后:
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第6张图片

# 运行方式

# 1. 直接双击 nginx.exe 

# 2. 命令
cd nginx根目录
start nginx

浏览器输入 http://localhost:8088/


3. 引入 admin 模块

网盘资源地址(不需要密码): https://pan.baidu.com/s/1daL566ehyZuQ6s5vzXYNpA?pwd=java

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第7张图片

粘贴至 tanhua 项目文件根目录 -> 文件 -> 新建 -> 来自现有源代码的模块
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第8张图片

4. 创建实体类

新建 tanhua-model/src/main/java/com/tanhua/model/domain/Admin.java 文件:

//后台系统的管理员对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Admin implements Serializable {
    /**
     * id
     */
    private Long id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 头像
     */
    private String avatar;
}


三、 管理员登录

1. 需求分析

  • 页面刷新时,自动发送获取验证码请求到服务端
  • 输入验证码登录,登录成功返回token
  • 通过token获取用户详情,跳转主页

2. 获取图片验证码

⑴. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第9张图片

⑵. 编码实现

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第10张图片
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/SystemController.java 文件:

@RestController
@RequestMapping("/system/users")
public class SystemController {

    @Autowired
    private AdminService adminService;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    // 获取验证码图片
    @GetMapping("/verification")
    public void verification(String uuid, HttpServletResponse response) throws IOException {
        // 1. 生成验证码对象
        LineCaptcha captcha = CaptchaUtil.createLineCaptcha(299, 97);
        // 2. 验证码存入redis
        String code = captcha.getCode();
        redisTemplate.opsForValue().set(Constants.CAP_CODE + uuid, code);
        // 3. 输出验证码图片
        captcha.write(response.getOutputStream());
    }
}

⑶. 测试

浏览器输入 localhost:18083//system/users/verification?uuid=123
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第11张图片

改为 网关请求:
浏览器输入 localhost:8888/admin/system/users/verification?uuid=123 页面不变

⑷. 页面展示


3. 登录

⑴. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第12张图片

⑵. 编码实现

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第13张图片

①. Controller

编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/SystemController.java 文件:

    /**
     * 用户登录:
     */
    @PostMapping("/login")
    public ResponseEntity login(@RequestBody Map map) {
        Map retMap = adminService.login(map);
        return ResponseEntity.ok(retMap);
    }

②. Service

编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/AdminService.java 文件:

    // 用户登录
    public Map login(Map map) {
        //1、获取请求的参数(username,password,verificationCode(验证码),uuid)
        String username = (String) map.get("username");
        String password = (String) map.get("password");
        String verificationCode = (String) map.get("verificationCode");
        String uuid = (String) map.get("uuid");
        //2、判断用户名或者密码是否为空
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            //用户名或者密码为空
            throw new BusinessException("用户名或者密码为空");
        }
        //3、判断验证码是否正确
        if (StringUtils.isEmpty(username)) {
            //验证码为空
            throw new BusinessException("验证码为空");
        }
        //从redis中获取验证码
        String key = Constants.CAP_CODE+uuid;
        String code = redisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(code) || !code.equals(verificationCode)) {
            //验证码错误
            throw new BusinessException("验证码错误");
        }
        redisTemplate.delete(key);
        //4、根据用户名查询用户
        QueryWrapper<Admin> qw = new QueryWrapper<Admin>().eq("username", username);
        Admin admin = adminMapper.selectOne(qw);
        //5、判断用户是否存在,密码是否一致
        password = SecureUtil.md5(password); //md5加密
        if(admin == null || !admin.getPassword().equals(password)) {
            //用户名错误或者密码不一致
            throw new BusinessException("用户名或者密码");
        }
        //6、通过JWT生成token
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("username", username);
        claims.put("id", admin.getId());
        String token = JwtUtils.getToken(claims);
        //8、构造返回值
        Map resMap = new HashMap();
        resMap.put("token", token);
        return resMap;
    }

⑶. 页面效果

(暂时还不能实现跳转页面)


4. 查询用户资料

⑴. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第14张图片

⑵. 编码实现

博主在编写开发项目时,遇到无法处理的mysql版本问题,使用了跨过校验规则,如果遇到了同样的问题,可以查看下方gitee仓库地址(关键词:后台登录_跨过校验)
Gitee仓库: https://gitee.com/yuan0_0/tanhua_20220918.git

①. vo对象

新建 tanhua-model/src/main/java/com/tanhua/model/vo/AdminVo.java 文件:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdminVo {

    // id
    private String id;

    // 用户名
    private String username;

    // 头像
    private String avatar;

    public static AdminVo init(Admin admin) {
        AdminVo vo = new AdminVo();
        vo.setAvatar(admin.getAvatar());
        vo.setUsername(admin.getUsername());
        vo.setId(admin.getId().toString());
        return vo;
    }
}

②. Controller

编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/SystemController.java 文件:

    /**
     * 获取用户的信息
     */
    @PostMapping("/profile")
    public ResponseEntity profile() {
        AdminVo vo = adminService.profile();
        return ResponseEntity.ok(vo);
    }

③. Service

编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/AdminService.java 文件:

    // 获取用户信息
    public AdminVo profile() {
        Long userId = AdminHolder.getUserId();
        Admin admin = adminMapper.selectById(userId);
        return AdminVo.init(admin);
    }

⑶. 页面效果


SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第15张图片



四、 用户信息管理

后台管理系统可以对所有注册用户进行统一管理。如查看用户列表,用户详情,用户发布的视频/动态等

1. 查询用户列表

⑴. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第16张图片

⑵. 编码实现

①. Controller

新建 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java 文件:

@RestController
@RequestMapping("/manage")
public class ManageController {

    @Autowired
    private ManagerService managerService;

    /**
     * 查询用户列表
     */
    @GetMapping("/users")
    public ResponseEntity users(@RequestParam(defaultValue = "1") Integer page,
                                @RequestParam(defaultValue = "10") Integer pagesize) {
        PageResult result = managerService.findAllUsers(page,pagesize);
        return ResponseEntity.ok(result);
    }
}

②. Service

新建 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java 文件:

@Service
public class ManagerService {

    @DubboReference
    private UserInfoApi userInfoApi;

    // 查询用户列表
    public PageResult findAllUsers(Integer page, Integer pagesize) {
        IPage iPage = userInfoApi.findALL(page, pagesize);
        return new PageResult(page, pagesize, iPage.getTotal(), iPage.getRecords());
    }
}

③. Api

编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/UserInfoApi.java 文件:

    // 分页查询
    IPage findALL(Integer page, Integer pagesize);

④. ApiImpl

编辑 tanhua-dubbo/tanhua-dubbo-db/src/main/java/com/tanhua/dubbo/api/UserInfoApiImpl.java 文件:

    // 分页查询
    @Override
    public IPage findALL(Integer page, Integer pagesize) {
        return userInfoMapper.selectPage(new Page<UserInfo>(page, pagesize), null);
    }

⑶. 页面效果

浏览器输入 http://127.0.0.1:8088
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第17张图片


2. 查询用户详情

⑴. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第18张图片

⑵. 编码实现

①. Controller

编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java 文件:

    /**
     * 查询用户详情
     */
    @GetMapping("/users/{userId}")
    public ResponseEntity findUserById(@PathVariable("userId") Long userId) {
        UserInfo userInfo = managerService.findUserById(userId);
        return ResponseEntity.ok(userInfo);
    }

②. Service

编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java 文件:

    // 查询用户详情
    public UserInfo findUserById(Long userId) {
        return userInfoApi.findById(userId);
    }

⑶. 页面效果

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第19张图片


3. 查询视频列表

⑴. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第20张图片

⑵. 编码实现

①. Controller

编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java 文件:

    /**
     * 查询视频列表
     */
    @GetMapping("/videos")
    public ResponseEntity videos(@RequestParam(defaultValue = "1") Integer page,
                                 @RequestParam(defaultValue = "10") Integer pagesize,
                                 Long uid ) {
        PageResult result = managerService.findAllVideos(page,pagesize,uid);
        return ResponseEntity.ok(result);
    }

②. Service

编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java 文件:

    // 查询视频列表
    public PageResult findAllVideos(Integer page, Integer pagesize, Long uid) {
        return videoApi.findByUserId(page, pagesize, uid);
    }

③. Api

编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/VideoApi.java 文件:

    // 根据用户id查询
    PageResult findByUserId(Integer page, Integer pagesize, Long userId);

④. ApiImpl

编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/VideoApiImpl.java 文件:

    // 根据用户id查询
    @Override
    public PageResult findByUserId(Integer page, Integer pagesize, Long userId) {
        Query query = Query.query(Criteria.where("userId").in(userId));
        long count = mongoTemplate.count(query, Video.class);
        query.skip((page - 1) * pagesize).limit(pagesize)
                .with(Sort.by(Sort.Order.desc("created")));
        List<Video> list = mongoTemplate.find(query, Video.class);
        return new PageResult(page, pagesize, count, list);
    }

⑶. 页面效果

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第21张图片
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第22张图片


4. 查询动态列表

⑴. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第23张图片

⑵. 编码实现

①. Controller

编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java 文件:

    /**
     * 查询动态列表
     */
    @GetMapping("/messages")
    public ResponseEntity messages(@RequestParam(defaultValue = "1") Integer page,
                                   @RequestParam(defaultValue = "10") Integer pagesize,
                                   Long uid,Integer state ) {
        PageResult result = managerService.findAllMovements(page,pagesize,uid,state);
        return ResponseEntity.ok(result);
    }

②. Service

编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java 文件:

    // 查询动态列表
    public PageResult findAllMovements(Integer page, Integer pagesize, Long uid, Integer state) {
        //1、调用API查询数据 :Movment对象
        PageResult result = movementApi.findByUserId(uid, state, page, pagesize);
        //2、解析PageResult,获取Movment对象列表
        List<Movement> items = (List<Movement>) result.getItems();
        //3、一个Movment对象转化为一个Vo
        if(CollUtil.isEmpty(items)) {
            return new PageResult();
        }
        List<Long> userIds = CollUtil.getFieldValues(items, "userId", Long.class);
        Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null);
        List<MovementsVo> vos = new ArrayList<>();
        for (Movement movement : items) {
            UserInfo userInfo = map.get(movement.getUserId());
            if(userInfo != null) {
                MovementsVo vo = MovementsVo.init(userInfo, movement);
                vos.add(vo);
            }
        }
        //4、构造返回值
        result.setItems(vos);
        return result;
    }

③. Api

编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/MovementApi.java 文件:

    // 根据用户id,查询
    PageResult findByUserId(Long uid, Integer state, Integer page, Integer pagesize);

④. ApiImpl

编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/MovementApiImpl.java 文件:

    // 根据用户id,查询
    @Override
    public PageResult findByUserId(Long uid, Integer state, Integer page, Integer pagesize) {
        Query query = new Query();
        if(uid != null) {
            query.addCriteria(Criteria.where("userId").is(uid));
        }
        if(state != null) {
            query.addCriteria(Criteria.where("state").is(state));
        }
        long count = mongoTemplate.count(query, Movement.class);
        query.with(Sort.by(Sort.Order.desc("created"))).limit(pagesize).skip((page -1) * pagesize);
        List<Movement> list = mongoTemplate.find(query, Movement.class);
        return new PageResult(page,pagesize,count,list);
    }

⑶. 页面效果

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第24张图片


5. 用户冻结

⑴. 需求分析

用户冻结/解冻是管理员在后台系统对用户的惩罚措施。对于发布不当言论或者违法违规内容的用户,可以暂时、永久禁止其登录,评论,发布动态、

  • Mysql数据库
    • 使用Mysql记录冻结数据,记录冻结状态。对于临时性冻结,需要配合定时任务扫描进行解冻
  • Redis
    • 使用Redis记录冻结数据,借助Redis的失效时间对临时性冻结解冻

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第25张图片

⑵. 冻结

①. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第26张图片

②. Controller

编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java 文件:

    /**
     * 用户冻结
     */
    @PostMapping("/users/freeze")
    public ResponseEntity freeze(@RequestBody Map params) {
        Map map =  managerService.userFreeze(params);
        return ResponseEntity.ok(map);
    }

③. Service

编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java 文件:

    // 用户冻结
    public Map userFreeze(Map params) {
        // 1. 构造key
        String userId = params.get("userId").toString();
        String key = Constants.USER_FREEZE + userId;
        // 2. 构造失效时间
        Integer freezingTime = Integer.valueOf(params.get("freezingTime").toString()); // 冻结时间: 1为冻3天,2为冻7天,3为冻永久
        int days = 0;
        if (freezingTime == 1) {
            days = 3;
        } else if (freezingTime == 2) {
            days = 7;
        }
        // 3. 将数据存入redis
        String value = JSON.toJSONString(params);
        if(days > 0) {
            redisTemplate.opsForValue().set(key, value, days, TimeUnit.MINUTES);
        } else {
            redisTemplate.opsForValue().set(key, value);
        }
        // 4. 构造返回值
        Map retMap = new HashMap<>();
        retMap.put("message", "冻结成功");
        return retMap;
    }

④. 页面效果

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第27张图片
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第28张图片


⑶. 解冻

①. 接口文档

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第29张图片

②. Controller

编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java 文件:

    /**
     * 用户解冻
     */
    @PostMapping("/users/unfreeze")
    public ResponseEntity unfreeze(@RequestBody  Map params) {
        Map map =  managerService.userUnfreeze(params);
        return ResponseEntity.ok(map);
    }

③. Service

编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java 文件:

    // 用户解冻
    public Map userUnfreeze(Map params) {
        // 1. 构造key
        String userId = params.get("userId").toString();
        String key = Constants.USER_FREEZE + userId;
        // 2. 删除redis数据
        redisTemplate.delete(key);
        // 3. 构造返回值
        Map retMap = new HashMap<>();
        retMap.put("message", "解冻成功");
        return retMap;
    }

④. 页面效果

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第30张图片

⑷. 登录校验

①. 公共类

新建 tanhua-app-server/src/main/java/com/tanhua/server/service/UserFreezeService.java 文件:

@Service
public class UserFreezeService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public void checkUserStatus(String state, Long userId) {
        // 1. 拼接key, 查询redis数据
        String key = Constants.USER_FREEZE + userId;
        String value = redisTemplate.opsForValue().get(key);
        // 2. 如果数据存在,且冻结范围一致,抛出异常
        if(!StringUtils.isEmpty(value)) {
            Map map = JSON.parseObject(value, Map.class);
            String freezingRange = (String) map.get("freezingRange");
            if(state.equals(freezingRange)) {
                throw new BusinessException(ErrorResult.builder().errMessage("您的账号被冻结!").build());
            }
        }
    }
}

②. Service

编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/UserService.java 文件:

    /**
     * 发送短信验证码
     * @param phone
     */
    public void sendMsg(String phone) {
        // 校验用户是否被冻结
        User user = userApi.findByMobile(phone);
        if(user != null) {
            userFreezeService.checkUserStatus("1", user.getId());
        }

        // 1. 随机生成6位数数字
        // String code = RandomStringUtils.randomNumeric(6);
        // !!! 项目开发不用真正实现短信发送
         String code = "123456";

        // 2. 调用template对象, 发送验证码
        // !!! 项目开发不用真正实现短信发送
        // template.sendSms(phone, code);

        // 3. 将验证码存入redis
        redisTemplate.opsForValue().set("CHECK_CODE_"+phone,code, Duration.ofMinutes(5));
    }


五、 数据统计

1. 数据采集

⑴. 需求分析

后台系统首页中,显示各种统计数据,比如:累计用户数、新增用户数、登录次数等内容

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第31张图片
用户日志表(tb_log):
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第32张图片

0101-登录,0102-注册,0201-发动态,0202-查看,0203-点赞,0204-喜欢,0205-评论,0206-取消点赞,0207-取消喜欢0301-发视频,0302-视频点赞,0303-视频取消点赞,0304-视频评论

日统计表(tb_analysis):
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第33张图片

⑵. 发送日志消息

①. RabbitMQ依赖

探花交友项目所需的第三方服务组件,已经以Docker-Compose准备好了。仅仅需要进入相关目录,以命令形式启动运行即可

# 进入目录
cd /root/docker-file/rmq/
# 创建容器并启动
docker-compose up –d
# 查看容器
docker ps -a

在这里插入图片描述

②. 消息类型

探花项目间使用RabbitMQ收发消息,这里采用topic类型消息
日志消息key规则:log.xxx
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第34张图片

③. 编码实现

Ⅰ. yml文件

编辑 tanhua-app-server/src/main/resources/application.yml 文件:

server:
  port: 18080
spring:
  profiles:
    active: prod
  application:
    name: tanhua-app-server
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.136.160:8848
      config:
        server-addr: 192.168.136.160:8848
        file-extension: yml

Ⅱ. 工具类 - 发送消息

新建 tanhua-app-server/src/main/java/com/tanhua/server/service/MqMessageService.java 文件:

@Service
public class MqMessageService {

    @Autowired
    private AmqpTemplate amqpTemplate;

    //发送日志消息
    public void sendLogMessage(Long userId,String type,String key,String busId) {
        try {
            Map map = new HashMap();
            map.put("userId",userId.toString());
            map.put("type",type);
            map.put("logTime",new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
            map.put("busId",busId);
            String message = JSON.toJSONString(map);
            amqpTemplate.convertAndSend("tanhua.log.exchange",
                    "log."+key,message);
        } catch (AmqpException e) {
            e.printStackTrace();
        }
    }

    //发送动态审核消息
    public void sendAudiMessage(String movementId) {
        try {
            amqpTemplate.convertAndSend("tanhua.audit.exchange",
                    "audit.movement",movementId);
        } catch (AmqpException e) {
            e.printStackTrace();
        }
    }
}

Ⅲ. 登录 - 发送日志消息

编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/UserService.java 文件:

    /**
     * 验证登录
     * @param phone
     * @param code
     */
    public Map loginVerification(String phone, String code) {
        // 1. 从redis中获取验证码
        String redisCode = redisTemplate.opsForValue().get("CHECK_CODE_" + phone);

        // 2. 对验证码进行校验
        if(StringUtils.isEmpty(redisCode) || !redisCode.equals(code)) {
            // throw new RuntimeException("验证码错误");
            throw new BusinessException(ErrorResult.loginError());
        }

        // 3. 删除redis中的验证码
        redisTemplate.delete("CHECK_CODE_" + phone);

        // 4. 通过手机号查询用户
        User user = userApi.findByMobile(phone);
        boolean isNew = false;

        // 5. 如果用户不存在,创建用户保存到数据库
        String type = "0101"; // 登录
        if(user == null) {
            type = "0102"; // 注册
            user = new User();
            user.setMobile(phone);
            // user.setCreated(new Date());
            // user.setUpdated(new Date());
            user.setPassword(DigestUtils.md5Hex("123456"));
            Long userId = userApi.save(user);
            user.setId(userId);
            isNew = true;

            // 注册环信用户
            String hxUser = "hx" + user.getId();
            Boolean create = huanXinTemplate.createUser(hxUser, Constants.INIT_PASSWORD);
            if(create) {
                user.setHxUser(hxUser);
                user.setHxPassword(Constants.INIT_PASSWORD);
                userApi.update(user);
            }
        }

        // 发送日志消息
        // try {
        //    Map map = new HashMap<>();
        //     map.put("userId", user.getId().toString());
        //     map.put("type", type);
        //     map.put("logTime", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
        //     String message = JSON.toJSONString(map);
        //     amqpTemplate.convertAndSend("tanhua.log.exchange", "log.user", message);
        // } catch (Exception e) {
        //      e.printStackTrace();
        // }
        mqMessageService.sendLogMessage(user.getId(), type, "user", null);

        // 6. 通过JWT生成token(存入手机号和用户ID)
        Map tokenMap = new HashMap();
        tokenMap.put("id", user.getId());
        tokenMap.put("mobile", phone);
        String token = JwtUtils.getToken(tokenMap);

        // 7. 构造返回值
        Map retMap = new HashMap();
        retMap.put("token", token);
        retMap.put("isNew", isNew);

        return retMap;
    }

Ⅳ. 动态 - 发送日志消息

编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/MovementService.java 文件:

    // 查询单条动态
    public MovementsVo findById(String movementId) {
        // 发送日志
        mqMessageService.sendLogMessage(UserHolder.getUserId(), "0202", "movement", movementId);

        // 1. 调用API查询动态详情
        Movement movement = movementApi.findById(movementId);

        // 2. 转换vo对象
        if(movement != null) {
            UserInfo userInfo = userInfoApi.findById(movement.getUserId());
            return MovementsVo.init(userInfo, movement);
        } else {
            return null;
        }
    }

⑶. 获取消息内容

①. 实体类

Ⅰ. Log

新建 tanhua-model/src/main/java/com/tanhua/model/domain/Log.java 文件:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Log {
    /**
     * id
     */
    private Long id;
    /**
     * 用户id
     */
    private Long userId;
    /**
     * 操作时间
     */
    private String logTime;

    /**
     * 操作类型,
     * 0101为登录,0102为注册,
     * 0201为发动态,0202为浏览动态,0203为动态点赞,0204为动态喜欢,0205为评论,0206为动态取消点赞,0207为动态取消喜欢,
     * 0301为发小视频,0302为小视频点赞,0303为小视频取消点赞,0304为小视频评论
     */
    private String type;

    /**
     * 登陆地点
     */
    private String place;
    /**
     * 登陆设备
     */
    private String equipment;

    public Log(Long userId, String logTime, String type) {
        this.userId = userId;
        this.logTime = logTime;
        this.type = type;
    }
}

Ⅱ. Analysis

新建 tanhua-model/src/main/java/com/tanhua/model/domain/Analysis.java 文件:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Analysis{

    private Long id;
    /**
     * 日期
     */
    private Date recordDate;
    /**
     * 新注册用户数
     */
    private Integer numRegistered = 0;
    /**
     * 活跃用户数
     */
    private Integer numActive = 0;
    /**
     * 登陆次数
     */
    private Integer numLogin = 0;
    /**
     * 次日留存用户数
     */
    private Integer numRetention1d = 0;

    private Date created;
}

②. Mapper

Ⅰ. LogMapper

新建 tanhua-admin/src/main/java/com/tanhua/admin/mapper/LogMapper.java 文件:

public interface LogMapper extends BaseMapper<Log> {
}

Ⅱ. AnalysisMapper

新建 tanhua-admin/src/main/java/com/tanhua/admin/mapper/AnalysisMapper.java 文件:

public interface AnalysisMapper extends BaseMapper<Analysis> {
}

③. 拦截器

新建 tanhua-admin/src/main/java/com/tanhua/admin/listener/LogListener.java 文件:

@Component
public class LogListener {

    @Autowired
    private LogMapper logMapper;

    @RabbitListener(
            bindings = @QueueBinding(
                value = @Queue(
                        value = "tanhua.log.queue",
                        durable = "true"
                ),
                exchange = @Exchange(
                        value = "tanhua.log.exchange",
                        type = ExchangeTypes.TOPIC),
                key = {"log.*"}
            )
    )
    public void log(String message) {
        try {
            Map map = JSON.parseObject(message, Map.class);
            map.forEach((k, v) -> System.out.println(k + "--" + v));
            // 1. 解析map获取数据
            Long userId = Long.valueOf(map.get("userId").toString());
            String type = (String) map.get("type");
            String logTime = (String) map.get("logTime");
            // 2. 构造log对象,保存到数据库
            Log log = new Log(userId, logTime, type);
            logMapper.insert(log);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
    }
}

④. 测试


2. 定时任务

⑴. 概述

定时任务调度,一句话概括就是:基于给定的时间点、给定的时间间隔、自动执行的任务

  • java自带的API、java.util.Timer类、java.util.TimerTask类
  • Quartz框架、开源、功能强大、使用起来稍显复杂
  • Spring 3.0以后自带了task 调度工具,比Quartz更加的简单方便

⑵. 入门案例

①. 启动类

编辑 tanhua-admin/src/main/java/com/tanhua/admin/AdminServerApplication.java 文件:

@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
@MapperScan("com.tanhua.admin.mapper")
@EnableScheduling // 开启定时任务
public class AdminServerApplication {

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

②. 测试类

编辑 tanhua-admin/src/main/java/com/tanhua/admin/task/AnalysisTask.java 文件:

@Component
public class AnalysisTask {
    /**
     * 配置时间规则: 每5秒执行一次
     */
    @Scheduled( cron = "0/5 * * * * ? ")
    public void analysis() {
        //业务逻辑
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        System.out.println("当前时间:"+time);
    }
}


③. 打印结果

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第35张图片


⑶. CRON表达式

  • Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义
  • corn从左到右(用空格隔开):秒 分 时 期 月 周 年

Cron 表达式支持到六个域

名称 是否必须 允许值 特殊字符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W C
1-12 或 JAN-DEC , - * /
1-7 或 SUN-SAT , - * ? / L C #
  • *:匹配该域的任意值
  • ?:忽略该域,只能用在周和日两个域。因为二者会相互影响
  • -:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
  • /:表示起始时间开始触发,然后每隔固定时间触发一次
  • ,:表示列出枚举值

示例:

  • 0 15 10 ? * * - 每天上午10:15触发
  • 0 15 10 * * ? - 每天上午10:15触发
  • 0 * 14 * * ? - 在每天下午2点到下午2:59期间的每1分钟触发
  • 0 0/5 14 * * ? - 在每天下午2点到下午2:55期间的每5分钟触发
  • 0 0/5 14,18 * * ? - 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

⑷. 数据统计

①. 定时器

编辑 tanhua-admin/src/main/java/com/tanhua/admin/task/AnalysisTask.java 文件:

@Component
public class AnalysisTask {

    @Autowired
    private AnalysisService analysisService;

    /**
     * 配置时间规则: 每5秒执行一次
     */
    @Scheduled( cron = "0/10 * * * * ? ")
    public void analysis() {
        //业务逻辑
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        System.out.println("当前时间:"+time);

        try {
            analysisService.analysis();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

②. Mapper

编辑 tanhua-admin/src/main/java/com/tanhua/admin/mapper/LogMapper.java 文件:

public interface LogMapper extends BaseMapper<Log> {
    @Select("SELECT COUNT(DISTINCT user_id) FROM tb_log WHERE TYPE=#{type} AND log_time=#{logTime}")
    Integer queryByTypeAndLogTime(@Param("type") String type, @Param("logTime") String logTime); //根据操作时间和类型

    @Select("SELECT COUNT(DISTINCT user_id) FROM tb_log WHERE log_time=#{logTime}")
    Integer queryByLogTime(String logTime); //展示记录时间查询

    @Select("SELECT COUNT(DISTINCT user_id)  FROM tb_log WHERE log_time=#{today} AND user_id IN (\n " +
            " SELECT user_id FROM tb_log WHERE TYPE=\"0102\" AND log_time=#{yestoday} \n " +
            ")")
    Integer queryNumRetention1d(@Param("today")  String today,@Param("yestoday") String yestoday); //查询次日留存
}

③. Service

新建 tanhua-admin/src/main/java/com/tanhua/admin/service/AnalysisService.java 文件:

@Service
public class AnalysisService {

    @Autowired
    private LogMapper logMapper;

    @Autowired
    private AnalysisMapper analysisMapper;

    /**
     * 定时统计日志数据到统计表中
     */
    public void analysis() throws ParseException {
        // 1、查询tb_log表中的数 (每日注册用户数,每日登陆用户,活跃的用户数据,次日留存的用户)
        // 1.1 定义查询的日期
        String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        String yesTodayStr =  DateUtil.yesterday().toString("yyyy-MM-dd");
        // 1.2 统计数据 - 注册用户数
        Integer regCount = logMapper.queryByTypeAndLogTime("0102",todayStr);
        // 1.3 统计数据 - 登陆用户
        Integer loginCount = logMapper.queryByTypeAndLogTime("0101",todayStr);
        // 1.4 统计数据 - 活跃用户
        Integer activeCount = logMapper.queryByLogTime(todayStr);
        // 1.5 统计数据 - 次日留存
        Integer numRetention1d = logMapper.queryNumRetention1d(todayStr, yesTodayStr);
        // 2. 构造Analysis对象
        // 2.1 根据日期查询书库
        QueryWrapper<Analysis> qw = new QueryWrapper<Analysis>();
        qw.eq("record_date", new SimpleDateFormat("yyyy-MM-dd").parse(todayStr));
        Analysis analysis = analysisMapper.selectOne(qw);
        // 3、完成统计数据的更新或者保存
        if(analysis != null) {
            // 3.1 如果存在,就更新
            analysis.setNumRegistered(regCount);
            analysis.setNumLogin(loginCount);
            analysis.setNumActive(activeCount);
            analysis.setNumRetention1d(numRetention1d);
            analysisMapper.updateById(analysis);
        } else {
            // 3.2 如果不存在,就保存
            analysis = new Analysis();
            analysis.setNumRegistered(regCount);
            analysis.setNumLogin(loginCount);
            analysis.setNumActive(activeCount);
            analysis.setNumRetention1d(numRetention1d);
            analysis.setRecordDate(new SimpleDateFormat("yyyy-MM-dd").parse(todayStr));
            analysis.setCreated(new Date());
            analysisMapper.insert(analysis);
        }
    }
}

④. 添加测试数据

新建 tanhua-admin/src/test/java/com/tanhua/admin/LogTest.java 文件:

@RunWith(SpringRunner.class)
@SpringBootTest
public class LogTest {

    @Autowired
    private LogMapper logMapper;

    // 输入想创建日志的日期时间
    private String logTime = "2022-11-30";

    //模拟登录数据
    public void testInsertLoginLog() {
        for (int i = 0; i < 5; i++) {
            Log log = new Log();
            log.setUserId((long)(i+1));
            log.setLogTime(logTime);
            log.setType("0101");
            logMapper.insert(log);
        }
    }

    //模拟注册数据
    public void testInsertRegistLog() {
        for (int i = 0; i < 10; i++) {
            Log log = new Log();
            log.setUserId((long)(i+1));
            log.setLogTime(logTime);
            log.setType("0102");
            logMapper.insert(log);
        }
    }
    //模拟其他操作
    public void testInsertOtherLog() {
        String[] types = new String[]{"0201","0202","0203","0204","0205","0206","0207","0301","0302","0303","0304"};
        for (int i = 0; i < 10; i++) {
            Log log = new Log();
            log.setUserId((long)(i+1));
            log.setLogTime(logTime);
            int index = new Random().nextInt(10);
            log.setType(types[index]);
            logMapper.insert(log);
        }
    }

	@Test
    public void generData() {
        testInsertLoginLog();
        testInsertRegistLog();
        testInsertOtherLog();
    }
}


六、 内容审核

1. 阿里云内容审核

⑴. 阿里云安全

①. 技术选型

  • 内容安全是识别服务,支持对图片、视频、文本、语音等对象进行多样化场景检测,有效降低内容违规风险
  • 目前很多平台都支持内容检测,如阿里云、腾讯云、百度AI、网易云等国内大型互联网公司都对外提供了API
  • 按照性能和收费来看,探花交友项目使用的就是阿里云的内容安全接口,使用到了图片和文本的审核
  • 阿里云收费标准:https://www.aliyun.com/price/product/?spm=a2c4g.11186623.2.10.4146401eg5oeu8#/lvwang/detail

②. 阿里云 - 内容安全

阿里云云盾官网: https://www.aliyun.com/product/lvwang

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第36张图片


③. AccessKey管理

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第37张图片

④. SDK文档

文本垃圾内容检测: https://help.aliyun.com/document_detail/53427.html
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第38张图片

图片垃圾内容检测:https://help.aliyun.com/document_detail/53424.html
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第39张图片

⑵. 入门案例

①. yml配置

编辑 tanhua-admin/src/main/resources/bootstrap.yml 文件:

server:
  port: 18083
spring:
  profiles:
    active: prod
  application:
    name: tanhua-admin
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.136.160:8848
      config:
        server-addr: 192.168.136.160:8848
        file-extension: yml
tanhua:
  green:
    enable: true
    accessKeyID: !!!申请的key
    accessKeySecret: !!!申请的keyS
    scenes: porn,terrorism #色情,暴力


②. Properties

新建 tanhua-autoconfig/src/main/java/com/tanhua/autoconfig/properties/GreenProperties.java 文件:

@Data
@ConfigurationProperties("tanhua.green")
public class GreenProperties {
    /**
     * 账号
     */
    String accessKeyID;
    /**
     * 密钥
     */
    String accessKeySecret;

    /**
     * 场景
     */
    String scenes;
}

③. template

新建 tanhua-autoconfig/src/main/java/com/tanhua/autoconfig/template/AliyunGreenTemplate.java 文件:

@Slf4j
public class AliyunGreenTemplate {

    private IAcsClient client;

    private GreenProperties greenProperties;

    public AliyunGreenTemplate(GreenProperties greenProperties) {
        this.greenProperties = greenProperties;
        try {
            IClientProfile profile = DefaultProfile
                    .getProfile("cn-shanghai", greenProperties.getAccessKeyID(), greenProperties.getAccessKeySecret());
            DefaultProfile
                    .addEndpoint("cn-shanghai", "cn-shanghai", "Green", "green.cn-shanghai.aliyuncs.com");
            client = new DefaultAcsClient(profile);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("Green配置缺失,请补充!");
        }
    }


    /**
     * 阿里云文本内容检查
     *
     * @param content
     * @return map  key - suggestion内容
     * pass:文本正常,可以直接放行,
     * review:文本需要进一步人工审核,
     * block:文本违规,可以直接删除或者限制公开
     * value -   通过,或 出错原因
     * @throws Exception
     */
    public Map<String, String> greenTextScan(String content) throws Exception {
        TextScanRequest textScanRequest = new TextScanRequest();
        textScanRequest.setAcceptFormat(FormatType.JSON); // 指定api返回格式
        textScanRequest.setHttpContentType(FormatType.JSON);
        textScanRequest.setMethod(MethodType.POST); // 指定请求方法
        textScanRequest.setEncoding("UTF-8");
        textScanRequest.setRegionId("cn-shanghai");
        List<Map<String, Object>> tasks = new ArrayList<>();
        Map<String, Object> task1 = new LinkedHashMap<>();
        task1.put("dataId", UUID.randomUUID().toString());
        /**
         * 待检测的文本,长度不超过10000个字符
         */
        task1.put("content", content);
        tasks.add(task1);
        JSONObject data = new JSONObject();

        /**
         * 检测场景,文本垃圾检测传递:antispam
         **/
        data.put("scenes", Arrays.asList("antispam"));
        data.put("tasks", tasks);
        log.info("检测任务内容:{}", JSON.toJSONString(data, true));
        textScanRequest.setHttpContent(data.toJSONString().getBytes("UTF-8"), "UTF-8", FormatType.JSON);
        // 请务必设置超时时间
        textScanRequest.setConnectTimeout(3000);
        textScanRequest.setReadTimeout(6000);

//        返回结果内容
        Map<String, String> resultMap = new HashMap<>();
        try {
            HttpResponse httpResponse = client.doAction(textScanRequest);
            if (!httpResponse.isSuccess()) {
                new RuntimeException("阿里云文本内容检查出现异常!");
            }
            JSONObject scrResponse = JSON.parseObject(new String(httpResponse.getHttpContent(), "UTF-8"));
            log.info("检测结果内容:{}", JSON.toJSONString(scrResponse, true));
            if (200 != scrResponse.getInteger("code")) {
                new RuntimeException("阿里云文本内容检查出现异常!");
            }
            JSONArray taskResults = scrResponse.getJSONArray("data");
            for (Object taskResult : taskResults) {
                if (200 != ((JSONObject) taskResult).getInteger("code")) {
                    new RuntimeException("阿里云文本内容检查出现异常!");
                }
                JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
                for (Object sceneResult : sceneResults) {
                    String scene = ((JSONObject) sceneResult).getString("scene");
                    String label = ((JSONObject) sceneResult).getString("label");
                    String suggestion = ((JSONObject) sceneResult).getString("suggestion");
                    log.info("最终内容检测结果,suggestion = {},label={}", suggestion, label);
//                    设置默认错误返回内容
                    resultMap.put("suggestion", suggestion);
                    if (suggestion.equals("review")) {
                        resultMap.put("reson", "文章内容中有不确定词汇");
                        log.info("返回结果,resultMap={}", resultMap);
                        return resultMap;
                    } else if (suggestion.equals("block")) {
                        String reson = "文章内容中有敏感词汇";
                        if (label.equals("spam")) {
                            reson = "文章内容中含垃圾信息";
                        } else if (label.equals("ad")) {
                            reson = "文章内容中含有广告";
                        } else if (label.equals("politics")) {
                            reson = "文章内容中含有涉政";
                        } else if (label.equals("terrorism")) {
                            reson = "文章内容中含有暴恐";
                        } else if (label.equals("abuse")) {
                            reson = "文章内容中含有辱骂";
                        } else if (label.equals("porn")) {
                            reson = "文章内容中含有色情";
                        } else if (label.equals("flood")) {
                            reson = "文章内容灌水";
                        } else if (label.equals("contraband")) {
                            reson = "文章内容违禁";
                        } else if (label.equals("meaningless")) {
                            reson = "文章内容无意义";
                        }
                        resultMap.put("reson", reson);
                        log.info("返回结果,resultMap={}", resultMap);
                        return resultMap;
                    }

                }
            }
            resultMap.put("suggestion", "pass");
            resultMap.put("reson", "检测通过");

        } catch (Exception e) {
            log.error("阿里云文本内容检查出错!");
            e.printStackTrace();
            new RuntimeException("阿里云文本内容检查出错!");
        }
        log.info("返回结果,resultMap={}", resultMap);
        return resultMap;
    }

    /**
     * 阿里云图片内容安全
     */
    public Map imageScan(List<String> imageList) throws Exception {
        IClientProfile profile = DefaultProfile
                .getProfile("cn-shanghai", greenProperties.getAccessKeyID(), greenProperties.getAccessKeySecret());
        ImageSyncScanRequest imageSyncScanRequest = new ImageSyncScanRequest();
        // 指定api返回格式
        imageSyncScanRequest.setAcceptFormat(FormatType.JSON);
        // 指定请求方法
        imageSyncScanRequest.setMethod(MethodType.POST);
        imageSyncScanRequest.setEncoding("utf-8");
        //支持http和https
        imageSyncScanRequest.setProtocol(ProtocolType.HTTP);
        JSONObject httpBody = new JSONObject();
        /**
         * 设置要检测的场景, 计费是按照该处传递的场景进行
         * 一次请求中可以同时检测多张图片,每张图片可以同时检测多个风险场景,计费按照场景计算
         * 例如:检测2张图片,场景传递porn、terrorism,计费会按照2张图片鉴黄,2张图片暴恐检测计算
         * porn: porn表示色情场景检测
         */

        httpBody.put("scenes", Arrays.asList(greenProperties.getScenes().split(",")));

        /**
         * 如果您要检测的文件存于本地服务器上,可以通过下述代码片生成url
         * 再将返回的url作为图片地址传递到服务端进行检测
         */
        /**
         * 设置待检测图片, 一张图片一个task
         * 多张图片同时检测时,处理的时间由最后一个处理完的图片决定
         * 通常情况下批量检测的平均rt比单张检测的要长, 一次批量提交的图片数越多,rt被拉长的概率越高
         * 这里以单张图片检测作为示例, 如果是批量图片检测,请自行构建多个task
         */
        List list = new ArrayList();
        for (String imageUrl : imageList) {
            JSONObject task = new JSONObject();
            task.put("dataId", UUID.randomUUID().toString());
            // 设置图片链接。
            task.put("url", imageUrl);
            task.put("time", new Date());
            list.add(task);
        }

        httpBody.put("tasks",list);

        imageSyncScanRequest.setHttpContent(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(httpBody.toJSONString()),
                "UTF-8", FormatType.JSON);
        /**
         * 请设置超时时间, 服务端全链路处理超时时间为10秒,请做相应设置
         * 如果您设置的ReadTimeout小于服务端处理的时间,程序中会获得一个read timeout异常
         */
        imageSyncScanRequest.setConnectTimeout(3000);
        imageSyncScanRequest.setReadTimeout(10000);
        HttpResponse httpResponse = null;
        try {
            httpResponse = client.doAction(imageSyncScanRequest);
        } catch (Exception e) {
            e.printStackTrace();
        }

        Map<String, String> resultMap = new HashMap<>();

        //服务端接收到请求,并完成处理返回的结果
        if (httpResponse != null && httpResponse.isSuccess()) {
            JSONObject scrResponse = JSON.parseObject(org.apache.commons.codec.binary.StringUtils.newStringUtf8(httpResponse.getHttpContent()));
            System.out.println(JSON.toJSONString(scrResponse, true));
            int requestCode = scrResponse.getIntValue("code");
            //每一张图片的检测结果
            JSONArray taskResults = scrResponse.getJSONArray("data");
            if (200 == requestCode) {
                for (Object taskResult : taskResults) {
                    //单张图片的处理结果
                    int taskCode = ((JSONObject) taskResult).getIntValue("code");
                    //图片要检测的场景的处理结果, 如果是多个场景,则会有每个场景的结果
                    JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
                    if (200 == taskCode) {
                        for (Object sceneResult : sceneResults) {
                            String scene = ((JSONObject) sceneResult).getString("scene");
                            String label = ((JSONObject) sceneResult).getString("label");
                            String suggestion = ((JSONObject) sceneResult).getString("suggestion");
                            //根据scene和suggetion做相关处理
                            //do something
                            System.out.println("scene = [" + scene + "]");
                            System.out.println("suggestion = [" + suggestion + "]");
                            System.out.println("suggestion = [" + label + "]");
                            if (!suggestion.equals("pass")) {
                                resultMap.put("suggestion", suggestion);
                                resultMap.put("label", label);
                                return resultMap;
                            }
                        }

                    } else {
                        //单张图片处理失败, 原因视具体的情况详细分析
                        log.error("task process fail. task response:" + JSON.toJSONString(taskResult));
                        return null;
                    }
                }
                resultMap.put("suggestion", "pass");
                return resultMap;
            } else {
                /**
                 * 表明请求整体处理失败,原因视具体的情况详细分析
                 */
                log.error("the whole image scan request failed. response:" + JSON.toJSONString(scrResponse));
                return null;
            }
        }
        return null;
    }
}

④. 启动类

编辑 tanhua-autoconfig/src/main/java/com/tanhua/autoconfig/TanhuaAutoConfiguration.java 文件:

@EnableConfigurationProperties({
        SmsProperties.class,
        OssProperties.class,
        AipFaceProperties.class,
        HuanXinProperties.class,
        GreenProperties.class
})
public class TanhuaAutoConfiguration {

    @Bean
    public SmsTemplate smsTemplate(SmsProperties properties) {
        return new SmsTemplate(properties);
    }

    @Bean
    public OssTemplate ossTemplate(OssProperties properties) {return new OssTemplate(properties);}

    @Bean
    public AipFaceTemplate aipFaceTemplate() {return new AipFaceTemplate();}

    @Bean
    public HuanXinTemplate huanXinTemplate(HuanXinProperties properties) {return new HuanXinTemplate(properties);}

    @Bean
    // 配置文件中是否具有 tanhua.green 开头的配置, 并且enable = true
    @ConditionalOnProperty(prefix = "tanhua.green",value = "enable", havingValue = "true")
    public AliyunGreenTemplate aliyunGreenTemplate(GreenProperties properties) {
        return new AliyunGreenTemplate(properties);
    }
}

⑤. 测试类

新建 tanhua-admin/src/test/java/com/tanhua/admin/GreenTemplateTest.java 文件:

@RunWith(SpringRunner.class)
@SpringBootTest
public class GreenTemplateTest {

    @Autowired
    private AliyunGreenTemplate template;

    @Test
    public void test() throws Exception {
        // Map map = template.greenTextScan("巧笑倩兮,美目盼兮");
        // Map map = template.greenTextScan("本校小额贷款,安全、快捷、方便、无抵押,随机随贷,当天放款,上门服务");

        List<String> list = new ArrayList<>();
        list.add("http://images.china.cn/site1000/2018-03/17/dfd4002e-f965-4e7c-9e04-6b72c601d952.jpg");
        Map<String, String> map = template.imageScan(list);
        map.forEach((k, v) -> System.out.println(k + "--" + v));
    }
}

⑥. 测试结果

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第40张图片

⑶. 发送动态审核消息

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第41张图片

①. Api

编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/MovementApi.java 文件:

    // 发布动态
    // void publish(Movement movement);
    String publish(Movement movement);

②. ApiImpl

编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/MovementApiImpl.java 文件:

    // 发布动态
    // public void publish(Movement movement) {
    public String publish(Movement movement) {
        try {
            // 1. 保存动态详情
            // 1.1 设置PID(同名时序列自增)
            movement.setPid(idWorker.getNextId("movement"));
            // 1.2 设置时间
            movement.setCreated(System.currentTimeMillis());
            // 1.3 保存动态
            mongoTemplate.save(movement);

//            // 2. 查询当前用户的好友数据
//            Criteria criteria = Criteria.where("userId").is(movement.getUserId());
//            Query query = Query.query(criteria);
//            List friends = mongoTemplate.find(query, Friend.class);
//
//            // 3. 循环好友数据, 构建时间线数据存入数据库
//            for (Friend friend : friends) {
//                MovementTimeLine timeLine = new MovementTimeLine();
//                timeLine.setMovementId(movement.getId());
//                timeLine.setUserId(friend.getUserId());
//                timeLine.setFriendId(friend.getFriendId());
//                timeLine.setCreated(System.currentTimeMillis());
//                mongoTemplate.save(timeLine);
//            }

            // 异步多线程调用
            timeLineService.saveTimeLine(movement.getUserId(), movement.getId());
        } catch (Exception e) {
            // 忽略事物处理
            e.printStackTrace();
        }

        // 返回动态id
        return movement.getId().toHexString();
    }

③. Service

编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/MovementService.java 文件:

    /**
     * 发布动态
     */
    public void publishMovement(Movement movement, MultipartFile[] imageContent) throws IOException {
        // 1. 判断发布动态的内容是否存在
        if(StringUtils.isEmpty(movement.getTextContent())) {
            throw new BusinessException(ErrorResult.contentError());
        }

        // 2. 获取当前登录的用户id
        Long userId = UserHolder.getUserId();

        // 3. 将文件内容上传到阿里云OSS, 获取请求地址
        List<String> medias = new ArrayList<>();
        for (MultipartFile multipartFile : imageContent) {
            // String upload = ossTemplate.upload(multipartFile.getOriginalFilename(), multipartFile.getInputStream());
            // !!! 阿里云OSS收费, 这里暂时跳过
            String upload = "https://img0.baidu.com/it/u=1501084209,93021381&fm=253&fmt=auto&app=138&f=JPEG";
            medias.add(upload);
        }

        // 4. 将数据封装到movement对象
        movement.setUserId(userId);
        movement.setMedias(medias);

        //5. 调用API完成动态发布
        // movementApi.publish(movement);

        // 发送动态审核消息
        String movementId = movementApi.publish(movement);
        mqMessageService.sendAudiMessage(movementId);
    }

⑷. 后台系统获取消息

①. 动态拦截器

新建 tanhua-admin/src/main/java/com/tanhua/admin/listener/AuditListener.java 文件:

@Component
public class AuditListener {

    @DubboReference
    private MovementApi movementApi;

    @Autowired
    private AliyunGreenTemplate aliyunGreenTemplate;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(
                    value = "tanhua.audit.queue",
                    durable = "true"
            ),
            exchange = @Exchange(
                    value = "tanhua.audit.exchange",
                    type = ExchangeTypes.TOPIC),
            key = {"audit.movement"})
    )
    public void audit(String movementId) throws Exception {
        System.out.println("内容审核id: id=" + movementId);
        try {
            // 1. 根据动态id查询动态
            Movement movement = movementApi.findById(movementId);
            if(movement != null && movement.getState() == 0) {
                // 2. 审核文本/审核图片
                Map<String, String> textScan = aliyunGreenTemplate.greenTextScan(movement.getTextContent());
                Map<String, String> imageScan = aliyunGreenTemplate.imageScan(movement.getMedias());
                // 3. 判断审核结果
                int state = 0;
                if(textScan != null && imageScan != null) {
                    String textSuggestion = textScan.get("suggestion");
                    String imageSuggestion = imageScan.get("suggestion");
                    if ("block".equals(textSuggestion) || "block".equals(textSuggestion)){
                        state = 2; // 驳回
                    }else if("pass".equals(textSuggestion) || "pass".equals(textSuggestion)) {
                        state = 1; // 通过
                    }
                }
                // 4. 更新动态状态
                movementApi.update(movementId, state);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

②. Api

编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/MovementApi.java 文件:

    // 更新动态状态
    void update(String movementId, int state);

③. ApiImpl

编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/MovementApiImpl.java 文件:

    // 更新动态状态
    @Override
    public void update(String movementId, int state) {
        Query query = Query.query(Criteria.where("id").is(new ObjectId(movementId)));
        Update update = Update.update("state", state);
        mongoTemplate.updateFirst(query, update, Movement.class);
    }

⑸. 完善查询动态接口

编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/MovementApiImpl.java 文件:

    // 根据用户id,查询当前用户发布的动态数据列表
    @Override
    public PageResult findByUserId(Long userId, Integer page, Integer pagesize) {
        // Criteria criteria = Criteria.where("userId").is(userId);

        // 根据审核状态筛选动态
        Criteria criteria = Criteria.where("userId").is(userId).and("state").is(1);

        Query query = Query.query(criteria).skip((page  - 1) * pagesize).limit(pagesize)
                .with(Sort.by(Sort.Order.desc("created")));
        List<Movement> movements = mongoTemplate.find(query, Movement.class);
        return new PageResult(page, pagesize, 0l, movements);
    }

    /**
     * 查询用户好友发布的动态数据列表
     * @param friendId 当前操作用户id
     * @return
     */
    public List<Movement> findFriendMovements(Integer page, Integer pagesize, Long friendId) {
        // 1. 根据friendId查询时间线表
        Query query = Query.query(Criteria.where("friendId").is(friendId))
                .skip((page - 1) * pagesize).limit(pagesize)
                .with(Sort.by(Sort.Order.desc("created")));
        List<MovementTimeLine> lineList = mongoTemplate.find(query, MovementTimeLine.class);

        // 2. 提取动态id列表
        List<ObjectId> list = CollUtil.getFieldValues(lineList, "movementId", ObjectId.class);

        // 3. 根据动态id查询动态详情
        // Query movementQuery = Query.query(Criteria.where("id").is(list));

        // 根据审核状态筛选动态
        Query movementQuery = Query.query(Criteria.where("id").is(list).and("state").is(1));

        List<Movement> movementList = mongoTemplate.find(movementQuery, Movement.class);

        // 4. 返回
        return movementList;
    }


七、 推荐系统

1. 理论知识

为了解决信息过载和用户无明确需求的问题,找到用户感兴趣的物品,才有了个性化推荐系统

⑴. 电商是推荐系统的先行者

  • 电子商务网站是个性化推荐系统重要地应用的领域之一,亚马逊就是个性化推荐系统的积极应用者和推广者,亚马逊的推荐系统深入到网站的各类商品,为亚马逊带来了至少30%的销售额。
  • 不光是电商类,推荐系统无处不在。QQ,微信的好友推荐;新浪微博的你可能感兴趣的人;优酷,土豆的电影推荐;豆瓣的图书推荐;大从点评的餐饮推荐;脉脉的同事推荐等。
  • 推荐引擎的鼻祖思想源泉:http://portal.acm.org/citation.cfm?id=1070751
  • 亚马逊最早提出基于物品的协同过滤推荐算法:http://portal.acm.org/citation.cfm?id=372071

⑵. 推荐系统业务流程

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第42张图片

推荐系统广泛存在于各类网站中,作为一个应用为用户提供个性化的推荐。它需要一些用户的历史数据,一般由三个部分组成:基础数据、推荐算法系统、前台展示

  • 基础数据: 包括很多维度,包括用户的访问、浏览、下单、收藏,用户的历史订单信息,评价信息等很多信息;
  • 推荐算法系统: 主要是根据不同的推荐诉求由多个算法组成的推荐模型;
  • 前台展示: 主要是对客户端系统进行响应,返回相关的推荐信息以供展示。

⑶. 协同过滤推荐算法

迄今为止,在个性化推荐系统中,协同过滤技术是应用最成功的技术。目前国内外有许多大型网站应用这项技术为用户更加智能(个性化、千人千面)的推荐内容。

核心思想: 协同过滤一般是在海量的用户中发掘出一小部分和你品位比较类似的,在协同过滤中,这些用户成为邻居,然后根据他们喜欢的其他东西组织成一个排序的目彔作为推荐给你。

①. 基于用户的推荐 UserCF

对于用户A,根据用户的历史偏好,这里只计算得到一个邻居–用户C,然后将用户C 喜欢的物品D 推荐给用户A。

行为 作用
评分 通过评分,可以精准获取用户偏好
投票 投票可以较精准的得到用户偏好
评论 通过评论可以分析用户对物品的喜好
购买 用户的购买很明确的说明对物品感兴趣
页面停留 停留时间一定程度说明用户喜欢,但噪音较大

基于用户的协同过滤算法先计算的是用户与用户的相似度(兴趣相投,物以类聚人以群分),然后将相似度比较接近的用户A购买的物品推荐给用户B,专业的说法是该算法用最近邻居(nearest-neighbor)算法找出一个用户的邻居集合,该集合的用户和该用户有相似的喜好,算法根据邻居的偏好对该用户进行预测。
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第43张图片

②. 基于商品的推荐 ItemCF

  • 基于ItemCF的原理和基于UserCF类似,只是在计算邻居时采用物品本身,而不是从用户的角度,即基于用户对物品的偏好找到相似的物品,然后根据用户的历史偏好,推荐相似的物品给他。
  • 从计算的角度看,就是将所有用户对某个物品的偏好作为一个向量来计算物品之间的相似度,得到物品的相似物品后,根据用户历史的偏好预测当前用户还没有表示偏好的物品,计算得到一个排序的物品列表作为推荐。
  • 解释:对于物品A,根据所有用户的历史偏好,喜欢物品A 的用户都喜欢物品C,得出物品A 和物品C 比较相似,而用户C 喜欢物品A,那么可以推断出用户C 可能也喜欢物品C。
    SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第44张图片

⑷. ALS算法

  • ALS 是交替最小二乘 (alternatng least squares)的简称,是基于模型的推荐算法。
  • 通过观察到的所有用户给产品的打分,来推断每个用户的喜好并向用户推荐适合的产品。
  • 从协同过滤的分类来说,ALS算法属于User-Item CF,也叫做混合CF,它同时考虑了User和Item两个方面。

ALS算法矩阵:

  • 首先拿到的原始数据是每个听众对每首歌的评分矩阵A
  • ALS矩阵分解会把矩阵A分解成两个矩阵的相乘,分别是X矩阵和Y矩阵
    SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第45张图片
    评分矩阵A = 矩阵X 和 矩阵Y 的转秩的 乘积
    SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第46张图片
    在这里插入图片描述

2. 用户推荐

⑴. 流程

  • 推荐系统读取数据库中的用户数据
  • 根据用户资料,进行推荐运算
  • 将推荐数据存入到MongoDB
    SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第47张图片

⑵. 评分规则

  • 通过用户的个人资料推荐与其具有相似兴趣爱好的其他用户
  • 根据‘社交链’进行推荐(熟人社交)

评分规则:

字段 权重分
年龄差 0-2岁 30分 3-5 20分 5-10岁 10分 10岁以上 0分
性别 异性 30分 同性 0分
位置 同城 20分 不同 0分
学历 相同 20分 不同 0分

⑶. 推荐系统部署

①. docker

在探花项目中已经以 docker-compose 提供了推荐系统相应系统

#进入目录
cd /root/docker-file/recommend/
#创建容器并启动
docker-compose up –d
#查看容器
docker ps -a

在这里插入图片描述

②. SQL数据库

推荐系统会访问本地数据库,所以需要打开本地数据库的远程访问权限

如果想要用户root可以远程登录,则可通过修改user表中root用户对应的host字段值为“%”即可。我们用以下语句进行修改:

update user set host = '%' where user = 'root';

#或者
GRANT ALL PRIVILEGES ON *.* TO '登录id'@'%' IDENTIFIED BY '登录密码' WITH GRANT OPTION;

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第48张图片

⑷. 测试

删除MongoDB中recommend_user数据,2分钟后可以发现重新生成了新的推荐数据
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第49张图片


3. 动态推荐

在圈子功能中,针对于用户发布的动态信息,系统可以根据用户的发布、浏览、点赞等操作,对动态信息做计算,然后对每个用户进行不同的推荐

⑴. 需求分析

①. 流程说明

  • 用户对圈子的动态操作,如:发布、浏览、点赞、喜欢等,就会给RocketMQ进行发送消息;
  • 推荐系统接收消息,并且处理消息数据,处理之后将结果数据写入到MongoDB中;
  • Spark系统拉取数据,然后进行推荐计算;
  • 计算之后的结果数据写入到Redis中,为每个用户都进行个性化推荐;

②. 流程说明

  • 探花项目间使用RabbitMQ收发消息,这里采用topic类型消息
  • 日志消息key规则:log.xxx
    SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第50张图片

③. 评分规则

行为 评分 说明
浏览 1分 查看动态详情
点赞 5分 动态点赞
喜欢 8分 动态喜欢
评论 10分 发布动态评论
发布动态 基础5分 文字长度:50以内1分,50~100之间2分,100以上3分

⑵. 创建数据采集系统

①. 新模块

SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第51张图片


②. pom依赖

编辑 tanhua-recommend/pom.xml 文件:

    <dependencies>
        <dependency>
            <groupId>com.itheimagroupId>
            <artifactId>tanhua-modelartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>5.4.3version>
        dependency>
    dependencies>

③. yml配置文件

新建 tanhua-recommend/src/main/resources/application.yml 文件:

spring:
  rabbitmq:
    host: 192.168.136.160
    port: 5672
    username: guest
    password: guest
  data:
    mongodb:
      uri: mongodb://192.168.136.160:27017/tanhua

④. 启动类

新建 tanhua-recommend/src/main/java/com/tanhua/recommend/RecommendApplication.java 文件:

@SpringBootApplication
public class RecommendApplication {

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

⑤. 实体类

新建 tanhua-model/src/main/java/com/tanhua/model/mongo/MovementScore.java 文件:

//大数据动态评分实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document("recomment_movement_score")
public class MovementScore {

    private ObjectId id;
    private Long userId;// 用户id
    private Long movementId; //动态id,需要转化为Long类型
    private Double score; //得分
    private Long date; //时间戳
}

⑶. 动态监听器

编辑 tanhua-recommend/src/main/java/com/tanhua/recommend/listener/RecommendMovementListener.java 文件:

@Component
public class RecommendMovementListener {

    @Autowired
    private MongoTemplate mongoTemplate;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(
                    value = "tanhua.movement.queue",
                    durable = "true"
            ),
            exchange = @Exchange(
                    value = "tanhua.log.exchange",
                    type = ExchangeTypes.TOPIC),
            key = {"log.movement"})
    )
    public void recommend(String message) {
        Map map = JSON.parseObject(message, Map.class);
        // 1. 解析数据
        Long userId = Long.valueOf(map.get("userId").toString());
        String type = (String) map.get("type");
        String logTime = (String) map.get("logTime");
        String movementId = (String) map.get("busId");
        // 2. 构造MovementScore,设置评分
        Movement movement = mongoTemplate.findById(movementId, Movement.class);
        if(movement != null) {
            MovementScore ms = new MovementScore();
            ms.setUserId(userId);
            ms.setMovementId(movement.getPid());
            ms.setDate(System.currentTimeMillis());
            ms.setScore(getScore(type,movement));
            // 3. 保存到数据库
            mongoTemplate.save(ms);
        }
    }

    private static Double getScore(String type,Movement movement) {
        //0201为发动态  基础5分 50以内1分,50~100之间2分,100以上3分
        //0202为浏览动态, 1
        //0203为动态点赞, 5
        //0204为动态喜欢, 8
        //0205为评论,     10
        //0206为动态取消点赞, -5
        //0207为动态取消喜欢   -8
        Double score = 0d;
        switch (type) {
            case "0201":
                score = 5d;
                score += movement.getMedias().size();
                int length = StrUtil.length(movement.getTextContent());
                if (length >= 0 && length < 50) {
                    score += 1;
                } else if (length < 100) {
                    score += 2;
                } else {
                    score += 3;
                }
                break;
            case "0202":
                score = 1d;
                break;
            case "0203":
                score = 5d;
                break;
            case "0204":
                score = 8d;
                break;
            case "0205":
                score = 10d;
                break;
            case "0206":
                score = -5d;
                break;
            case "0207":
                score = -8d;
                break;
            default:
                break;
        }
        return score;
    }
}

启动后,MongoDB会记录用户操作日志,根据用户操作日志推荐动态数据
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第52张图片


4. 视频推荐

⑴. 需求分析

①. 流程说明

  • 用户的业务操作,发送日志消息到RabbitMQ
  • 数据采集系统获取消息,将数据存入MongoDB
  • 推荐系统(Spark)拉取数据,进行推荐计算并存入Redis
  • 探花系统从Redis获取推荐数据结果
    SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第53张图片

②. 评分规则

行为 评分 说明
点赞 5分 视频点赞
评论 10分 发布视频评论
发布视频 2分 发布视频

⑵. 视频监听器

①. 实体类

新建 tanhua-model/src/main/java/com/tanhua/model/mongo/VideoScore.java 文件:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document("recomment_video_score")
public class VideoScore {

    private ObjectId id;
    private Long userId;// 用户id
    private Long videoId; //视频id,需要转化为Long类型 video中的vid字段
    private Double score; //得分
    private Long date; //时间戳
}

②. 监听器

新建 tanhua-recommend/src/main/java/com/tanhua/recommend/listener/RecommendVideoListener.java 文件:

@Component
public class RecommendVideoListener {

    @Autowired
    private MongoTemplate mongoTemplate;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(
                    value = "tanhua.video.queue",
                    durable = "true"
            ),
            exchange = @Exchange(
                    value = "tanhua.log.exchange",
                    type = ExchangeTypes.TOPIC),
            key = {"log.video"})
    )
    public void recommend(String message) {
        Map map = JSON.parseObject(message, Map.class);
        // 1. 解析数据
        Long userId = Long.valueOf(map.get("userId").toString());
        String type = (String) map.get("type");
        String logTime = (String) map.get("logTime");
        String videoId = (String) map.get("busId");
        // 2. 构造MovementScore,设置评分
        Video video = mongoTemplate.findById(videoId, Video.class);
        if(video != null) {
            VideoScore vs = new VideoScore();
            vs.setUserId(userId);
            vs.setVideoId(video.getVid());
            vs.setDate(System.currentTimeMillis());
            vs.setScore(getScore(type));
            // 3. 保存到数据库
            mongoTemplate.save(vs);
        }
    }

    private static Double getScore(String type) {
        //0301为发小视频,0302为小视频点赞,0303为小视频取消点赞,0304为小视频评论
        Double score = 0d;
        switch (type) {
            case "0301":
                score=2d;
                break;
            case "0302":
                score=5d;
                break;
            case "0303":
                score = -5d;
                break;
            case "0304":
                score = 10d;
                break;
            default:
                break;
        }
        return score;
    }
}

③. 发送日志消息

编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/SmallVideosService.java 文件:

    /**
     * 上传视频
     * @param videoThumbnail 视频封面图片
     * @param videoFile 视频文件
     */
    @CacheEvict(value="videoList",allEntries = true) // 清空缓存
    public void saveVideos(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException {
        if(videoThumbnail.isEmpty() || videoFile.isEmpty()) {
            throw new BusinessException(ErrorResult.error());
        }
        // 1. 将视频上传到FastDFS,获取访问url
        String filename = videoFile.getOriginalFilename();
        filename = filename.substring(filename.lastIndexOf(".") + 1);
        StorePath storePath = client.uploadFile(videoFile.getInputStream(), videoFile.getSize(), filename, null);
        String videoUrl = webServer.getWebServerUrl() + storePath.getFullPath();

        // 2. 将封面图片上传到阿里云OSS,获取访问的url
        // String upload = ossTemplate.upload(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream());
        // // !!! 阿里云OSS收费, 这里暂时跳过
        String upload = "https://img0.baidu.com/it/u=8672387,2873147723&fm=253&fmt=auto&app=138&f=JPEG";

        // 3. 构建Videos对象
        Video video = new Video();
        video.setUserId(UserHolder.getUserId());
        video.setVideoUrl(videoUrl);
        video.setPicUrl(upload);
        video.setText("我就是我, 是颜色不一样的烟火");

        // 4. 调用API保存数据
        String videoId = videoApi.save(video);
        if(StringUtils.isEmpty(videoId)) {
            throw new BusinessException(ErrorResult.error());
        }

        // 发送日志消息
        mqMessageService.sendLogMessage(UserHolder.getUserId(), "0301", "video", videoId);
    }

⑶. 测试

启动后,MongoDB会记录用户操作日志,根据用户操作日志推荐视频数据
SpringBoot交友APP项目实战(详细介绍+案例源码) - 11.后台管理(完结啦)_第54张图片


你可能感兴趣的:(Java,spring,boot,交友,java,后端,微服务)