1、用户冻结解冻
用户冻结/解冻使用管理员在后台系统对用户的惩罚措施。对于发布不当言论或者违法违规内容的用户,可以暂时、永久禁止其登录,评论,发布动态、
后台中解冻/冻结,就是将用户状态写入数据库中
APP端用户在进行登录,评论,发布动态时检测Redis中冻结状态
Mysql数据库
使用Mysql记录冻结数据,记录冻结状态,对于临时性冻结,需要配合定时任务扫描进行解冻
Redis
使用Redis记录冻结数据,借助Redis的失效时间对临时性冻结解冻。
业务分析:
后台中解冻/冻结,就是将用户状态写入数据库中。-
APP在进行登录,评论,发布动态时,检测Redis中冻结状态
用户冻结接口:
冻结用户,将数据存入Redis,并设置key的失效时间。
解冻用户,删除Redis数据
修改用户列表接口,添加字段
探花系统在用户登录,评论,发布动态时判断其冻结状态
如果被冻结抛出异常
1.1 用户冻结 ManageController 1 2 3 4 5 6 @PostMapping("/users/freeze") public ResponseEntity freeze (@RequestBody Map params) { Map map = managerService.userFreeze(params); return ResponseEntity.ok(map); }
ManageService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public Map userFreeze (Map params) { Integer freezingTime = (Integer) params.get("freezingTime" ); String userId = params.get("userId" ).toString(); int days = 0 ; if (freezingTime == 1 ) { days = 3 ; } if (freezingTime == 2 ) { days = 7 ; } if (freezingTime == 3 ) { days = -1 ; } String value = JSON.toJSONString(params); if (days>0 ){ redisTemplate.opsForValue().set(Constants.FREEZE_USER +userId,value,days, TimeUnit.MINUTES); }else { redisTemplate.opsForValue().set(Constants.FREEZE_USER+userId,value); } HashMap map = new HashMap(); map.put("message" ,"冻结成功" ); return map; }
1.2 用户解冻
ManageController 1 2 3 4 5 6 @PostMapping("/users/unfreeze") public ResponseEntity unfreeze (@RequestBody Map params) { Map map = managerService.userUnfreeze(params); return ResponseEntity.ok(map); }
ManageService 1 2 3 4 5 6 7 8 9 public Map userUnfreeze (Map params) { String userId = params.get("userId" ).toString(); String reasonsForThawing = (String) params.get("reasonsForThawing" ); redisTemplate.delete(Constants.FREEZE_USER+userId); Map map = new HashMap(); map.put("message" ,"解冻成功" ); return map; }
1.3 查询数据列表 UserInfo 添加字段
1 2 3 @TableField(exist = false) private String userStatus = "1" ;
ManageService 1 2 3 4 5 6 7 public ResponseEntity findById (Long userId) { UserInfo info = userInfoApi.findById(userId); if (redisTemplate.hasKey(Constants.FREEZE_USER+info.getId())) { info.setUserStatus("2" ); } return ResponseEntity.ok(info); }
1.4 探花系统修改 UserFreezeService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service public class UserFreezeService { @Autowired private RedisTemplate<String,String> redisTemplate; public void checkUserStatus (Integer state,Long userId) { String value = redisTemplate.opsForValue().get(Constants.FREEZE_USER + userId); if (!StringUtils.isEmpty(value)) { Map map = JSON.parseObject(value, Map.class); Integer freezingRange = (Integer) map.get("freezingRange" ); if (freezingRange == state) { throw new BusinessException(ErrorResult.builder().errMessage("您的账号被冻结!" ).build()); } } } }
修改冻结:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void sendMsg (String phone) { User user =userApi.findByMobile(phone); if (user!=null ){ userFreezeService.checkUserStatus(1 ,user.getId()); } String code = "123456" ; redisTemplate.opsForValue().set("CHECK_CODE_" +phone,code, Duration.ofMinutes(5 )); }
2、数据统计
后台系统首页中,显示各种统计数据,比如:累计用户数、新增用户数、登录次数等内容。
1、探花系统将用户操作日志写入RabbitMQ
2、管理后台获取最新消息,构造日志数据存入数据库
3、加入统计表,定时统计(日志表数量大,性能低)
用户日志表(tb_log):
日统计表:
2.1 数据采集 1、探花系统将用户操作日志写入RabbitMQ
2、管理后台获取最新消息,构造日志数据存入数据库
2.1.1 部署RabbitMQ 1 2 3 4 5 6 # 进入目录 cd /root/docker-file/rmq/ # 创建容器并启动 docker-compose up –d # 查看容器 docker ps -a
服务地址:192.168.136.160:5672
管理后台:http://192.168.136.160:15672/
2.1.2 消息类型说明 探花项目间使用RabbitMQ收发消息,这里采用topic类型消息
日志消息key规则:log.xxx
不同的队列负责不同的数据
2.1.3 实体类对象 Log 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.tanhua.model.domain;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;@Data @Builder @NoArgsConstructor @AllArgsConstructor public class Log { private Long id; private Long userId; private String logTime; 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.tanhua.model.domain;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;import java.util.Date;@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; }
2.1.4 发送日志消息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Autowired private AmqpTemplate amqpTemplate;public void login () { ……… Map<String, Object> msg = new HashMap<>(); msg.put(“userId”, UserHolder.getUserId().toString()); msg.put(“date", System.currentTimeMillis()); msg.put(" type", " 0101 ",); String message = JSON.toJSONString(msg); //发送消息 try { amqpTemplate.convertSendAndReceive(" tanhua.log.exchange", " log.user",message); }catch (Exception e) { e.printStackTrace(); } ……… }
2.1.5 监听器处理消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @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 listenCreate (String message) throws Exception { try { Map<String, Object> map = JSON.parseObject(message); Long userId = (Long) map.get("userId" ); String date = (String) map.get("date" ); String objId = (String) map.get("objId" ); String type = (String) map.get("type" ); Log log = new Log(userId,date,type); logMapper.insert(log); } catch (Exception e) { e.printStackTrace(); } } }
2.1.6 消息发送工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.tanhua.server.service;import com.alibaba.fastjson.JSON;import org.springframework.amqp.AmqpException;import org.springframework.amqp.core.AmqpTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.text.SimpleDateFormat;import java.util.Date;import java.util.HashMap;import java.util.Map;@Service public class MqMessageService { @Autowired private AmqpTemplate amqpTemplate; public void sendLogService (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 sendAudiService (String movementId) { try { amqpTemplate.convertAndSend("tanhua.audit.exchange" , "audit.movement" ,movementId); } catch (AmqpException e) { e.printStackTrace(); } } }
2.2 AOP处理日志 项目中大量方法需要改造,加入消息处理。
不易维护且存在耦合
解决方法:使用AOP + 自定义注解
2.2.1 自定义注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LogConfig { String objId () default "" ; String key () ; String type () ; }
2.2.2 切面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Component @Aspect public class LogAspect { @Autowired private AmqpTemplate amqpTemplate; @Before(value="execution(* com.tanhua.server.service.*.*(..)) && @annotation(config)") public void checkUserState (JoinPoint pjp , LogConfig config) throws Throwable { MethodSignature signature = (MethodSignature) pjp.getSignature(); String objId = parse(config.objId(), signature.getParameterNames(), pjp.getArgs()); Map<String, Object> msg = new HashMap<>(); msg.put("userId" , UserHolder.getUserId()); msg.put("date" , new SimpleDateFormat("yyyy-MM-dd" ).format(new Date())); msg.put("objId" , objId); msg.put("type" , config.type()); String message = JSON.toJSONString(msg); try { amqpTemplate.convertSendAndReceive("tanhua.log.exchange" , "log." +config.key(),message); }catch (Exception e) { e.printStackTrace(); } } public String parse (String expression, String[] paraNames,Object [] paras) { if (StringUtils.isEmpty(expression)) return "" ; StandardEvaluationContext context = new StandardEvaluationContext(); for (int i=0 ;i<paraNames.length;i++) { context.setVariable(paraNames[i], paras[i]); } Expression exp = new SpelExpressionParser().parseExpression(expression); Object value = exp.getValue(context); return value == null ? "" : value.toString(); } }
2.2.3 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 @LogConfig(type = "0202",key = "movement",objId = "#movementId") public MovementsVo findById (String movementId) { Movement movement = movementApi.findById(movementId); if (movement != null ) { UserInfo userInfo = userInfoApi.findById(movement.getUserId()); return MovementsVo.init(userInfo,movement); }else { return null ; } }
2.3 定时任务
在实际项目开发中,除了Web应用、SOA服务外,还有一类不可缺少的,那就是定时任务调度。定时任务的场景可以说非常广泛:
某些网站会定时发送优惠邮件;
银行系统还款日信用卡催收款;
某些应用的生日祝福短信等。
那究竟何为定时任务调度,一句话概括就是:基于给定的时间点、给定的时间间隔、自动执行的任务
概述:
java自带的APl java.util.Timer类java.util.TimerTask类
Quartz框架开源功能强大使用起来稍显复杂
Spring 3.0以后自带了task 调度工具,比Quartz更加的简单方便心
2.3.1 入门案例 开启定时任务 1 2 3 4 5 6 7 8 9 @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); } }
定时任务类 1 2 3 4 5 6 7 8 9 10 11 12 @Component public class AnalysisTask { @Scheduled( cron = "0/20 * * * * ? ") public void analysis () throws ParseException { String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ).format(new Date()); System.out.println("当前时间:" +time); } }
2.3.2 CRON表达式 对于定时任务,我们使用的时候主要是注重两个方面,一个是定时任务的业务,另一个就是Cron表达式。
**Cron 表达式支持到六个域 **
名称
是否必须
允许值
特殊字符
秒
是
0-59
, - * /
分
是
0-59
, - * /
时
是
0-23
, - * /
日
是
1-31
, - * ? / L W C
月
是
1-12 或 JAN-DEC
, - * /
周
是
1-7 或 SUN-SAT
, - * ? / L C #
月份和星期的名称是不区分大小写的。FRI 和 fri 是一样的。 域之间有空格分隔
*** 星号**
使用星号(*) 指示着你想在这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发这个 trigger。 表达式样例: 0 * 17 * * ? 意义:每天从下午5点到下午5:59中的每分钟激发一次 trigger。它停在下午 5:59 是因为值 17 在小时域上,在下午 6 点时,小时变为 18 了,也就不再理会这个 trigger,直到下一天的下午5点。 在你希望 trigger 在该域的所有有效值上被激发时使用 * 字符。
? 问号
? 号只能用在日和周域上,但是不能在这两个域上同时使用。你可以认为 ? 字符是 “我并不关心在该域上是什么值。” 这不同于星号,星号是指示着该域上的每一个值。? 是说不为该域指定值。
不能同时这两个域上指定值的理由是难以解释甚至是难以理解的。基本上,假定同时指定值的话,意义就会变得含混不清了:考虑一下,如果一个表达式在日域上有值11,同时在周域上指定了 WED。那么是要 trigger 仅在每个月的11号,且正好又是星期三那天被激发?还是在每个星期三的11号被激发呢?要去除这种不明确性的办法就是不能同时在这两个域上指定值。 只要记住,假如你为这两域的其中一个指定了值,那就必须在另一个字值上放一个 ?。
表达式样例: 0 10,44 14 ? 3 WED 意义:在三月中的每个星期三的下午 2:10 和 下午 2:44 被触发。
, 逗号
逗号 (,) 是用来在给某个域上指定一个值列表的。例如,使用值 0,15,30,45 在秒域上意味着每15秒触发一个 trigger。 表达式样例: 0 0,15,30,45 * * * ? 意义:每刻钟触发一次 trigger。
/ 斜杠
斜杠 (/) 是用于时间表的递增的。我们刚刚用了逗号来表示每15分钟的递增,但是我们也能写成这样 0/15。 表达式样例: 0/15 0/30 * * * ? 意义:在整点和半点时每15秒触发 trigger。
-中划线
中划线 (-) 用于指定一个范围。例如,在小时域上的 3-8 意味着 “3,4,5,6,7 和 8 点。” 域的值不允许回转,所以像 50-10 这样的值是不允许的。 表达式样例: 0 45 3-8 ? * * 意义:在上午的3点至上午的8点的45分时触发 trigger。
L 字母
L 说明了某域上允许的最后一个值。它仅被日和周域支持。当用在日域上,表示的是在月域上指定的月份的最后一天。例如,当月域上指定了 JAN 时,在日域上的 L 会促使 trigger 在1月31号被触发。假如月域上是 SEP,那么 L 会预示着在9月30号触发。换句话说,就是不管指定了哪个月,都是在相应月份的时最后一天触发 trigger。 表达式 0 0 8 L * ? 意义是在每个月最后一天的上午 8:00 触发 trigger。在月域上的 * 说明是 “每个月”。 当 L 字母用于周域上,指示着周的最后一天,就是星期六 (或者数字7)。所以如果你需要在每个月的最后一个星期六下午的 11:59 触发 trigger,你可以用这样的表达式 0 59 23 ? * L。 当使用于周域上,你可以用一个数字与 L 连起来表示月份的最后一个星期 X。例如,表达式 0 0 12 ? * 2L 说的是在每个月的最后一个星期一触发 trigger。 不要让范围和列表值与 L 连用 虽然你能用星期数(1-7)与 L 连用,但是不允许你用一个范围值和列表值与 L 连用。这会产生不可预知的结果。
W 字母
W 字符代表着日 (Mon-Fri),并且仅能用于日域中。它用来指定离指定日的最 的一个日。大部分的商业处理都是基于工作周的,所以 W 字符可能是非常重要的。例如,日域中的 15W 意味着 “离该月15号的最 一个日。” 假如15号是星期六,那么 trigger 会在14号(星期四)触发,因为距15号最 的是星期一,这个例子中也会是17号(译者Unmi注:不会在17号触发的,如果是15W,可能会是在14号(15号是星期六)或者15号(15号是星期天)触发,也就是只能出现在邻的一天,如果15号当天为 日直接就会当日执行)。W 只能用在指定的日域为单天,不能是范围或列表值。
#井号
字符仅能用于周域中。它用于指定月份中的第几周的哪一天。例如,如果你指定周域的值为 6#3,它意思是某月的第三个周五 (6=星期五,#3意味着月份中的第三周)。另一个例子 2#1 意思是某月的第一个星期一 (2=星期一,#1意味着月份中的第一周)。注意,假如你指定 #5,然而月份中没有第 5 周,那么该月不会触发。
2.3.3 定时统计 AnalysisTask 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component public class AnalysisTask { @Autowired private AnalysisService analysisService; @Scheduled( cron = "0/20 * * * * ? ") public void analysis () throws ParseException { String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ).format(new Date()); System.out.println("开始统计:" +time); analysisService.analysis(); System.out.println("结束统计" ); } }
AnalysisService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public void analysis () throws ParseException { String todayStr = new SimpleDateFormat("yyyy-MM-dd" ).format(new Date()); String yestodayStr = DateUtil.yesterday().toString("yyyy-MM-dd" ); Integer numRegistered = logMapper.queryByTypeAndLogTime("0102" ,todayStr); Integer numLogin = logMapper.queryByTypeAndLogTime("0101" ,todayStr); Integer numActive = logMapper.queryByLogTime(todayStr); Integer numRetention1d = logMapper.queryNumRetention1d(todayStr, yestodayStr); QueryWrapper<Analysis> qw = new QueryWrapper<>(); Date todatDate = new SimpleDateFormat("yyyy-MM-dd" ).parse(todayStr); qw.eq("record_date" , todatDate); Analysis analysis = analysisMapper.selectOne(qw); if (analysis == null ) { analysis = new Analysis(); analysis.setRecordDate(todatDate); analysis.setNumRegistered(numRegistered); analysis.setNumLogin(numLogin); analysis.setNumActive(numActive); analysis.setNumRetention1d(numRetention1d); analysis.setCreated(new Date()); analysisMapper.insert(analysis); }else { analysis.setNumRegistered(numRegistered); analysis.setNumLogin(numLogin); analysis.setNumActive(numActive); analysis.setNumRetention1d(numRetention1d); analysisMapper.updateById(analysis); } }
AnalysisMapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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) ; }
测试数据 为了方便操作,可以通过以下单元测试方法。保存若干操作数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.tanhua.manager.test;import com.tanhua.manager.domain.Log;import com.tanhua.manager.mapper.LogMapper;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.util.Random;@RunWith(SpringRunner.class) @SpringBootTest public class LogTest { @Autowired private LogMapper logMapper; private String logTime = "" ; 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(); } }
2.4 首页统计
2.4.1 vo对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Data @AllArgsConstructor @NoArgsConstructor public class AnalysisSummaryVo { private Long cumulativeUsers; private Long activePassMonth; private Long activePassWeek; private Long newUsersToday; private BigDecimal newUsersTodayRate; private Long loginTimesToday; private BigDecimal loginTimesTodayRate; private Long activeUsersToday; private BigDecimal activeUsersTodayRate; }
2.4.2 DashboardController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 @RestController @RequestMapping("/dashboard") public class DashboardController { @Autowired private AnalysisService analysisService; @GetMapping("/dashboard/summary") public AnalysisSummaryVo getSummary () { AnalysisSummaryVo analysisSummaryVo = new AnalysisSummaryVo(); DateTime dateTime = DateUtil.parseDate("2020-09-08" ); analysisSummaryVo.setCumulativeUsers(Long.valueOf(1000 )); analysisSummaryVo.setActivePassMonth(this .analysisService.queryActiveUserCount(dateTime, -30 )); analysisSummaryVo.setActivePassWeek(this .analysisService.queryActiveUserCount(dateTime, -7 )); analysisSummaryVo.setActiveUsersToday(this .analysisService.queryActiveUserCount(dateTime, 0 )); analysisSummaryVo.setNewUsersToday(this .analysisService.queryRegisterUserCount(dateTime, 0 )); analysisSummaryVo.setNewUsersTodayRate(computeRate( analysisSummaryVo.getNewUsersToday(), this .analysisService.queryRegisterUserCount(dateTime, -1 ) )); analysisSummaryVo.setLoginTimesToday(this .analysisService.queryLoginUserCount(dateTime, 0 )); analysisSummaryVo.setLoginTimesTodayRate(computeRate( analysisSummaryVo.getLoginTimesToday(), this .analysisService.queryLoginUserCount(dateTime, -1 ) )); return analysisSummaryVo; } private static BigDecimal computeRate (Long current, Long last) { BigDecimal result; if (last == 0 ) { result = new BigDecimal((current - last) * 100 ); } else { result = BigDecimal.valueOf((current - last) * 100 ).divide(BigDecimal.valueOf(last), 2 , BigDecimal.ROUND_HALF_DOWN); } return result; } private static String offsetDay (Date date,int offSet) { return DateUtil.offsetDay(date,offSet).toDateStr(); } }
2.4.3 AnalysisService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public Long queryActiveUserCount (DateTime today, int offset) { return this .queryUserCount(today, offset, "num_active" ); } public Long queryRegisterUserCount (DateTime today, int offset) { return this .queryUserCount(today, offset, "num_registered" ); } public Long queryLoginUserCount (DateTime today, int offset) { return this .queryUserCount(today, offset, "num_login" ); } private Long queryAnalysisCount (String column,String today,String offset) { return analysisMapper.sumAnalysisData(column,leDate,dtDate); }
2.4.4 AnalysisMapper 1 2 3 4 5 public interface AnalysisMapper extends BaseMapper <Analysis > { @Select("select sum(${column}) from tb_analysis where record_date > #{leDate} and record_date < #{gtDate}") Long sumAnalysisData (@Param("column") String column, @Param("leDate") String leDate, @Param("gtDate") String gtDate) ; }
3、内容审核 内容安全是识别服务,支持对图片、视频、文本、语音等对象进行多样化场景检测,有效降低内容违规风险。
目前很多平台都支持内容检测,如阿里云、腾讯云、百度AI、网易云等国内大型互联网公司都对外提供了API。
按照性能和收费来看,探花交友项目使用的就是阿里云的内容安全接口,使用到了图片和文本的审核。
3.1 阿里云内容审核 3.1.1 准备工作 1,前往阿里云官网 注册账号
2,打开云盾内容安全产品试用页面 ,单击立即开通,正式开通服务
3,在AccessKey管理页面 管理您的AccessKeyID和AccessKeySecret
3.1.2 文本内容垃圾检测 文本垃圾内容检测:点击访问
文本垃圾内容Java SDK: 点击访问
3.1.3 图片审核
图片垃圾内容Java SDK: https://help.aliyun.com/document_detail/53424.html?spm=a2c4g.11186623.6.715.c8f69b12ey35j4
3.1.4 抽取工具 GreenProperties 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data @ConfigurationProperties("tanhua.green") public class GreenProperties { String accessKeyID; String accessKeySecret; String scenes; }
AliyunGreenTemplate 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 package com.tanhua.autoconfig.template;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.aliyuncs.DefaultAcsClient;import com.aliyuncs.IAcsClient;import com.aliyuncs.green.model.v20180509.ImageSyncScanRequest;import com.aliyuncs.green.model.v20180509.TextScanRequest;import com.aliyuncs.http.FormatType;import com.aliyuncs.http.HttpResponse;import com.aliyuncs.http.MethodType;import com.aliyuncs.http.ProtocolType;import com.aliyuncs.profile.DefaultProfile;import com.aliyuncs.profile.IClientProfile;import com.tanhua.autoconfig.properties.GreenProperties;import lombok.extern.slf4j.Slf4j;import java.util.*;@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配置缺失,请补充!" ); } } public Map<String, String> greenTextScan (String content) throws Exception { TextScanRequest textScanRequest = new TextScanRequest(); textScanRequest.setAcceptFormat(FormatType.JSON); 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()); task1.put("content" , content); tasks.add(task1); JSONObject data = new JSONObject(); 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(); imageSyncScanRequest.setAcceptFormat(FormatType.JSON); imageSyncScanRequest.setMethod(MethodType.POST); imageSyncScanRequest.setEncoding("utf-8" ); imageSyncScanRequest.setProtocol(ProtocolType.HTTP); JSONObject httpBody = new JSONObject(); httpBody.put("scenes" , Arrays.asList(greenProperties.getScenes().split("," ))); 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); 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" ); 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 ; } }
TanhuaAutoConfiguration 1 2 3 4 5 @Bean @ConditionalOnProperty(prefix = "tanhua.green",value = "enable", havingValue = "true") public AliyunGreenTemplate aliyunGreenTemplate (GreenProperties properties) { return new AliyunGreenTemplate(properties); }
配置文件 1 2 3 4 5 6 tanhua: green: enable: true accessKeyID: LTAI4GKgob9vZ53k2SZdyAC7 accessKeySecret: LHLBvXmILRoyw0niRSBuXBZewQ30la scenes: porn,terrorism
单元测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Autowired private AliyunGreenTemplate template; @Test public void test () throws Exception { 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); System.out.println("------------" ); map.forEach((k,v)-> System.out.println(k +"--" + v)); }
3.2 动态审核 3.2.1 执行流程 为了解决程序间耦合关系,这里采用RabbitMQ + 阿里云完成内容审核
用户发布动态,保存到数据库
发送RabbitMQ消息
管理后台监听消息,对内容(文本、图片审核)
更新动态的状态
3.2.2 发布消息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void publishMovement (Movement movement, MultipartFile[] imageContent) throws IOException { if (StringUtils.isEmpty(movement.getTextContent())) { throw new BusinessException(ErrorResult.contentError()); } Long userId = UserHolder.getUserId(); List<String> medias = new ArrayList<>(); for (MultipartFile multipartFile : imageContent) { String upload = ossTemplate.upload(multipartFile.getOriginalFilename(), multipartFile.getInputStream()); medias.add(upload); } movement.setUserId(userId); movement.setMedias(medias); String movementId = movementApi.publish(movement); try { amqpTemplate.convertSendAndReceive("tanhua.audit.exchange" ,"audit.movement" ,movementId); }catch (Exception e) { e.printStackTrace(); } }
3.2.3 监听器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Component public class MovementListener { @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 listenCreate (String movementId) throws Exception { try { Movement movement = movementApi.findById(movementId); Integer state = 0 ; if (movement != null && movement.getState() == 0 ) { Map<String, String> textScan = aliyunGreenTemplate.greenTextScan(movement.getTextContent()); Map<String, String> imageScan = aliyunGreenTemplate.imageScan(movement.getMedias()); 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 ; } } } movementApi.update(movementId,state); } catch (Exception e) { e.printStackTrace(); } } }
3.2.4 movementApi 1 2 3 4 5 6 @Override public void update (String movementId, Integer state) { Query query = Query.query(Criteria.where("id" ).in(new ObjectId(movementId))); Update update = Update.update("state" , state); mongoTemplate.updateFirst(query,update,Movement.class); }