1、了解推荐系统1.1、什么是推荐系统？为了解决信息过载和用户无明确需求的问题，找到用户感兴趣的物品，才有了个性化推荐系统。其实，解决信息过载的问题，代表性的解决方案是分类目录和搜索引擎，如hao123，电商首页的分类目录以及百度，360搜索等。不过分类目录和搜索引擎只能解决用户主动查找信息的需求，即用户知道自己想要什么，并不能解决用户没用明确需求很随便的问题。经典语录是：你想吃什么，随便！面对这种很随便又得罪不起的用户（女友和上帝），只能通过分析用户的历史行为给用户的兴趣建模，从而主动给用户推荐能够满足他们兴趣和需求的信息。比如问问女友的闺蜜，她一般什么时候喜欢吃什么。1.2、电商是推荐系统的先行者[ ] 电子商务网站是个性化推荐系统重要地应用的领域之一，亚马逊就是个性化推荐系统的积极应用者和推广者，亚马逊的推荐系统深入到网站的各类商品，为亚马逊带来了至少30%的销售额。[ ] 不光是电商类，推荐系统无处不在。QQ，微信的好友推荐；新浪微博的你可能感兴趣的人；优酷，土豆的电影推荐；豆瓣的图书推荐；大从点评的餐饮推荐；脉脉的同事推荐等。[ ] 推荐引擎的鼻祖思想源泉：http://portal.acm.org/citation.cfm?id=1070751[ ] 亚马逊最早提出基于物品的协同过滤推荐算法：http://portal.acm.org/citation.cfm?id=372071京东的推荐： 1.3、推荐系统业务流程推荐系统广泛存在于各类网站中，作为一个应用为用户提供个性化的推荐。它需要一些用户的历史数据，一般由三个部分组成：基础数据、推荐算法系统、前台展示。基础数据包括很多维度，包括用户的访问、浏览、下单、收藏，用户的历史订单信息，评价信息等很多信息；推荐算法系统主要是根据不同的推荐诉求由多个算法组成的推荐模型；前台展示主要是对客户端系统进行响应，返回相关的推荐信息以供展示。1.4、协同过滤推荐算法迄今为止，在个性化推荐系统中，协同过滤技术是应用最成功的技术。目前国内外有许多大型网站应用这项技术为用户更加智能（个性化、千人千面）的推荐内容。核心思想：协同过滤一般是在海量的用户中发掘出一小部分和你品位比较类似的，在协同过滤中，这些用户成为邻居，然后根据他们喜欢的其他东西组织成一个排序的目彔作为推荐给你。1.4.1、基于用户的推荐 UserCF对于用户A，根据用户的历史偏好，这里只计算得到一个邻居–用户C，然后将用户C 喜欢的物品D 推荐给用户A。基于用户的协同过滤算法先计算的是用户与用户的相似度（兴趣相投，物以类聚人以群分），然后将相似度比较接近的用户A购买的物品推荐给用户B，专业的说法是该算法用最近邻居（nearest-neighbor）算法找出一个用户的邻居集合，该集合的用户和该用户有相似的喜好，算法根据邻居的偏好对该用户进行预测。1.4.2、基于商品的推荐 ItemCF基于ItemCF的原理和基于UserCF类似，只是在计算邻居时采用物品本身，而不是从用户的角度，即基于用户对物品的偏好找到相似的物品，然后根据用户的历史偏好，推荐相似的物品给他。从计算的角度看，就是将所有用户对某个物品的偏好作为一个向量来计算物品之间的相似度，得到物品的相似物品后，根据用户历史的偏好预测当前用户还没有表示偏好的物品，计算得到一个排序的物品列表作为推荐。解释：对于物品A，根据所有用户的历史偏好，喜欢物品A 的用户都喜欢物品C，得出物品A 和物品C 比较相似，而用户C 喜欢物品A，那么可以推断出用户C 可能也喜欢物品C。1.5、ALS算法ALS 是交替最小二乘 （alternating least squares）的简称。在机器学习的上下文中，ALS 特指使用交替最小二乘求解的一个协同推荐算法。它通过观察到的所有用户给产品的打分，来推断每个用户的喜好并向用户推荐适合的产品。从协同过滤的分类来说，ALS算法属于User-Item CF，也叫做混合CF。它同时考虑了User和Item两个方面。用户和商品的关系，可以抽象为如下的三元组：<User,Item,Rating>。其中，Rating是用户对商品的评分，表征用户对该商品的喜好程度。如下：196 242 3 881250949 186 302 3 891717742 22 377 1 878887116 244 51 2 880606923 166 346 1 886397596 298 474 4 884182806 115 265 2 881171488 253 465 5 891628467 305 451 3 886324817 6 86 3 883603013 62 257 2 879372434 286 1014 5 879781125 200 222 5 876042340 210 40 3 891035994 ................2、好友推荐对于好友的推荐，需要找出每个用户之间的相似性，具体规则如下：字段权重分 年龄差0-2岁 30分3-5 20分5-10岁 10分10岁以上 0分性别异性 30分同性 0分 位置同城 20分不同 0分 学历相同 20分不同 0分 2.1、流程2.2、部署好友推荐服务推荐系统是由专业的大数据人员开发编写的，我们只需要部署运行。在课程中已经以docker-compose提供了相应系统#进入目录 cd /root/docker-file/recommend/ #创建容器并启动 docker-compose up –d #查看容器 docker ps -a推荐系统内部包含了好友推荐，动态推荐以及视频推荐。推荐系统会访问本地数据库，所以需要打开本地数据库的远程访问权限如果想要用户root可以远程登录，则可通过修改user表中root用户对应的host字段值为“%”即可。我们用以下语句进行修改：update user set host = '%' where user = 'root'; #或者 GRANT ALL PRIVILEGES ON *.* TO '登录id'@'%' IDENTIFIED BY '登录密码' WITH GRANT OPTION;执行完成后，可以看到MongoDB中的recommend_user表中数据已经重新生成了。3、圈子推荐3.1、功能说明在圈子功能中，针对于用户发布的动态信息，系统可以根据用户的发布、浏览、点赞等操作，对动态信息做计算，然后对每个用户进行不同的推荐。3.2、流程说明流程说明：用户对圈子的动态操作，如：发布、浏览、点赞、喜欢等，就会给RocketMQ进行发送消息；推荐系统接收消息，并且处理消息数据，处理之后将结果数据写入到MongoDB中；Spark系统拉取数据，然后进行推荐计算；计算之后的结果数据写入到Redis中，为每个用户都进行个性化推荐；3.3、动态计分规则浏览 +1点赞 +5喜欢 +8评论 + 10发布动态文字长度：50以内1分，50~100之间2分，100以上3分图片个数：每个图片一分核心推荐逻辑：推荐模型：用户 | 动态 | 评分其中，评分是用户对动态操作的得分合计为什么自己发布动态还要计分？ 是因为，自己发布就相当于自己对此动态也感兴趣，这样就可以在相似的人之间进行推荐了。3.4、发送消息在之前的系统中，已经完成的日志消息的发送探花系统发布的日志消息统一发送到MQ3.5、接收消息接收消息的工作需要新创建tanhua-recommend工程，在此工程中完成相关的操作。3.5.1、创建tanhua-recommend工程<dependencies> <dependency> <groupId>com.itheima</groupId> <artifactId>tanhua-model</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.4.3</version> </dependency> </dependencies>3.5.2、配置文件application.ymlspring: rabbitmq: host: 192.168.136.160 port: 5672 username: guest password: guest data: mongodb: uri: mongodb://192.168.136.160:27017/tanhua3.5.3、启动类package com.tanhua.recommend; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RecommendApplication { public static void main(String[] args) { SpringApplication.run(RecommendApplication.class,args); } }3.5.4、实体类存储到MongoDB的中的实体结构。package com.tanhua.model.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; //大数据动态评分实体类 @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; //时间戳 }3.5.5、RecommendMovementListenerpackage com.tanhua.recommend.listener; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSON; import com.tanhua.model.mongo.Movement; import com.tanhua.model.mongo.MovementScore; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.stereotype.Component; import java.util.Map; @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) throws Exception { System.out.println("处理动态消息："+message); try { Map<String, Object> map = JSON.parseObject(message); //1、获取数据 Long userId = Long.valueOf(map.get("userId").toString()); String date = (String) map.get("date"); String objId = (String) map.get("objId"); String type = (String) map.get("type"); Movement movement = mongoTemplate.findById(objId, Movement.class); if(movement != null) { MovementScore ms = new MovementScore(); ms.setUserId(userId); ms.setDate(System.currentTimeMillis()); ms.setMovementId(movement.getPid()); ms.setScore(getScore(type,movement)); mongoTemplate.save(ms); } } catch (Exception e) { e.printStackTrace(); } } 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; } }3.6、测试测试方法：使用APP进行操作，可以看到在MongoDB中已经有数据写入。等待片刻，进入redis查看是否已经有数据4、小视频推荐小视频的推荐和动态推荐的实现逻辑非常的类似。4.1、动态计分规则发布+2点赞 +5评论 + 104.2、发送消息4.3、接收消息5.3.1、VideoScore视频评分实体类package com.tanhua.model.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; @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; //时间戳 }5.3.2、RecommendVideoListenerpackage com.tanhua.recommend.listener; import com.alibaba.fastjson.JSON; import com.tanhua.model.mongo.Video; import com.tanhua.model.mongo.VideoScore; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.stereotype.Component; import java.util.Map; @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) throws Exception { System.out.println("处理视频消息："+message); try { Map<String, Object> map = JSON.parseObject(message); //1、获取数据 Long userId = (Long) map.get("userId"); String date = (String) map.get("date"); String objId = (String) map.get("objId"); String type = (String) map.get("type"); Video video = mongoTemplate.findById(objId, Video.class); if(video != null) { VideoScore vs = new VideoScore(); vs.setUserId(userId); vs.setDate(System.currentTimeMillis()); vs.setVideoId(video.getVid()); vs.setScore(getScore(type)); mongoTemplate.save(vs); } } catch (Exception e) { e.printStackTrace(); } } 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; } }5.3.3、测试可以看到，用户1对于视频有点赞、取消点赞、评论等操作。测试Reids，可以发现已经存入了推荐数据
1、用户冻结解冻用户冻结/解冻使用管理员在后台系统对用户的惩罚措施。对于发布不当言论或者违法违规内容的用户，可以暂时、永久禁止其登录，评论，发布动态、后台中解冻/冻结，就是将用户状态写入数据库中APP端用户在进行登录，评论，发布动态时检测Redis中冻结状态1.1 用户冻结ManageController//用户冻结 @PostMapping("/users/freeze") public ResponseEntity freeze(@RequestBody Map params) { Map map = managerService.userFreeze(params); return ResponseEntity.ok(map); }ManageService//用户冻结 public Map userFreeze(Map params) { Integer freezingTime = (Integer) params.get("freezingTime"); Long userId = (Long) params.get("userId"); int days = 0; if (freezingTime == 1) { days = 3; } if (freezingTime == 2) { days = 7; } if (freezingTime == 3) { days = -1; } String value = JSON.toJSONString(params); redisTemplate.opsForValue().set(Constants.FREEZE_USER+userId,value,days, TimeUnit.MINUTES); Map map = new HashMap(); map.put("message","冻结成功"); return map; }1.2 用户解冻ManageController//用户解冻 @PostMapping("/users/unfreeze") public ResponseEntity unfreeze(@RequestBody Map params) { Map map = managerService.userUnfreeze(params); return ResponseEntity.ok(map); }ManageService//用户解冻 public Map userUnfreeze(Map params) { Long userId = (Long) params.get("userId"); 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为冻结 @TableField(exist = false) private String userStatus = "1";ManageServicepublic 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@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()); } } } }2、数据统计后台系统首页中，显示各种统计数据，比如：累计用户数、新增用户数、登录次数等内容。1、探花系统将用户操作日志写入RabbitMQ2、管理后台获取最新消息，构造日志数据存入数据库3、加入统计表，定时统计2.1 数据采集1、探花系统将用户操作日志写入RabbitMQ2、管理后台获取最新消息，构造日志数据存入数据库2.1.1 部署RabbitMQ探花交友所需的第三方服务组件，已经以Docker-Compose准备好了。仅仅需要进入相关目录，以命令形式启动运行即可#进入目录 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.xxx2.1.3 实体类对象Logpackage com.tanhua.model.domain; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @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; } }Analysispackage 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 发送日志消息@Autowired private AmqpTemplate amqpTemplate; public void login() { ……… //构造Map集合 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 监听器处理消息@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); //1、获取数据 Long userId = (Long) map.get("userId"); String date = (String) map.get("date"); String objId = (String) map.get("objId"); String type = (String) map.get("type"); //2、保存到数据库 Log log = new Log(userId,date,type); logMapper.insert(log); } catch (Exception e) { e.printStackTrace(); } } }2.1.6 消息发送工具类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 自定义注解@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LogConfig { //动态获取方法参数，支持SpringEL String objId() default ""; //路由的key String key(); //日志类型 String type(); }2.2.2 切面@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 { //解析SpringEL获取动态参数 MethodSignature signature = (MethodSignature) pjp.getSignature(); String objId = parse(config.objId(), signature.getParameterNames(), pjp.getArgs()); //构造Map集合 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 配置//根据id查询 @LogConfig(type = "0202",key = "movement",objId = "#movementId") public MovementsVo findById(String movementId) { //1、调用api根据id查询动态详情 Movement movement = movementApi.findById(movementId); //2、转化vo对象 if(movement != null) { UserInfo userInfo = userInfoApi.findById(movement.getUserId()); return MovementsVo.init(userInfo,movement); }else { return null; } }2.3 定时任务在实际项目开发中，除了Web应用、SOA服务外，还有一类不可缺少的，那就是定时任务调度。定时任务的场景可以说非常广泛：某些网站会定时发送优惠邮件；银行系统还款日信用卡催收款；某些应用的生日祝福短信等。那究竟何为定时任务调度，一句话概括就是：基于给定的时间点、给定的时间间隔、自动执行的任务2.3.1 入门案例开启定时任务@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); } }定时任务类@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 是一样的。 域之间有空格分隔*：表示所有可能的值/：表示增量，例如“0/5”在秒字段中表示“每5秒触发一次”,：表示附加多个值，例如“MON,WED,FRI”在星期字段中表示“周一、周三和周五”-：表示区间，例如“9-17”在小时字段中表示“从上午9点到下午5点”?：仅用于日和星期两个字段，表示不指定任何值L：仅用于日和星期两个字段，表示“最后一天”，例如“L”在日字段中表示“当月的最后一天”W：仅用于日字段，表示最接近指定日期的工作日（周一至周五）C：仅用于日和星期两个字段，表示计划执行的最近工作日或周末#：仅用于星期字段，表示该月的第几个星期几，例如“2#3”表示该月的第三个星期二2.3.3 定时统计AnalysisTask@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); //调logService完成日志统计 analysisService.analysis(); System.out.println("结束统计"); } }AnalysisService/** * 定时统计日志数据到统计表中 * 1、查询tb_log表中的数 （每日注册用户数，每日登陆用户，活跃的用户数据，次日留存的用户） * 2、构造AnalysisByDay对象 * 3、完成统计数据的更新或者保存 */ public void analysis() throws ParseException { String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); String yestodayStr = DateUtil.yesterday().toString("yyyy-MM-dd"); //工具类 //1、统计每日注册用户数 Integer numRegistered = logMapper.queryByTypeAndLogTime("0102",todayStr); //2、统计每日登陆用户 Integer numLogin = logMapper.queryByTypeAndLogTime("0101",todayStr); //3、统计活跃的用户数 Integer numActive = logMapper.queryByLogTime(todayStr); //4、统计次日留存的用户数 Integer numRetention1d = logMapper.queryNumRetention1d(todayStr, yestodayStr); //5、根据当前时间查询AnalysisByDay数据 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) { //7、如果不存在，保存 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{ //8、如果存在，更新 analysis.setNumRegistered(numRegistered); analysis.setNumLogin(numLogin); analysis.setNumActive(numActive); analysis.setNumRetention1d(numRetention1d); analysisMapper.updateById(analysis); } }LogMapperpublic 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); //查询次日留存 }测试数据为了方便操作，可以通过以下单元测试方法。保存若干操作数据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对象@Data @AllArgsConstructor @NoArgsConstructor public class AnalysisSummaryVo { /** * 累计用户数 */ private Long cumulativeUsers; /** * 过去30天活跃用户数 */ private Long activePassMonth; /** * 过去7天活跃用户 */ 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@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)); //过去30天活跃用户 analysisSummaryVo.setActivePassMonth(this.analysisService.queryActiveUserCount(dateTime, -30)); //过去7天活跃用户 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/** * 查询活跃用户的数量 */ 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 AnalysisMapperpublic 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和AccessKeySecret3.1.2 文本内容垃圾检测文本垃圾内容检测：点击访问文本垃圾内容Java SDK: 点击访问3.1.3 图片审核图片垃圾内容Java SDK: https://help.aliyun.com/document_detail/53424.html?spm=a2c4g.11186623.6.715.c8f69b12ey35j43.1.4 抽取工具GreenProperties@Data @ConfigurationProperties("tanhua.green") public class GreenProperties { /** * 账号 */ String accessKeyID; /** * 密钥 */ String accessKeySecret; /** * 场景 */ String scenes; }AliyunGreenTemplatepackage 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.*; /** * @author: itheima * @create: 2021-05-31 00:46 */ @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; } }TanhuaAutoConfiguration@Bean @ConditionalOnProperty(prefix = "tanhua.green",value = "enable", havingValue = "true") public AliyunGreenTemplate aliyunGreenTemplate(GreenProperties properties) { return new AliyunGreenTemplate(properties); }配置文件tanhua: green: enable: true accessKeyID: LTAI4GKgob9vZ53k2SZdyAC7 accessKeySecret: LHLBvXmILRoyw0niRSBuXBZewQ30la scenes: porn,terrorism #色情，暴力单元测试@Autowired private AliyunGreenTemplate template; @Test public void test() throws Exception { // Long data = analysisMapper.sumAnalysisData("num_registered", "2020-09-14", "2020-09-18"); // System.out.println(data); // Map<String, String> map = template.greenTextScan("本校小额贷款，安全、快捷、方便、无抵押，随机随贷，当天放款，上门服务"); // map.forEach((k,v)-> System.out.println(k +"--" + v)); 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 发布消息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()); medias.add(upload); } //4、将数据封装到Movement对象 movement.setUserId(userId); movement.setMedias(medias); //5、调用API完成发布动态 String movementId = movementApi.publish(movement); try { amqpTemplate.convertSendAndReceive("tanhua.audit.exchange","audit.movement",movementId); }catch (Exception e) { e.printStackTrace(); } }3.2.3 监听器@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 { //1、根据动态id查询动态 Movement movement = movementApi.findById(movementId); //对于RocketMQ消息有可能出现重复，解决方法判断 (幂等性) 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@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); }
1、项目架构1.1 概述API网关有很多实现方式，我们通过SpringCloud Gateway实现使用Nacos作为配置中心1.2 API网关1.2.1 搭建网关依赖<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- nacos配置中心依赖支持 <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> --> <dependency> <groupId>com.itheima</groupId> <artifactId>tanhua-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>引导类@SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }跨域问题配置类package com.itheima.gateway.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.util.pattern.PathPatternParser; /** * 跨域支持 */ @Configuration public class CorsConfig { @Bean public CorsWebFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }配置文件server: port: 8888 spring: profiles: active: prod application: name: tanhua-gateway cloud: nacos: discovery: server-addr: 192.168.136.160:8848 gateway: globalcors: add-to-simple-url-handler-mapping: true corsConfigurations: '[/**]': allowedHeaders: "*" allowedOrigins: "*" allowedMethods: - GET - POST - DELETE - PUT - OPTIONserver: port: 8888 spring: profiles: active: prod application: name: tanhua-gateway cloud: nacos: discovery: server-addr: 192.168.136.160:8848 gateway: globalcors: add-to-simple-url-handler-mapping: true corsConfigurations: '[/**]': allowedHeaders: "*" allowedOrigins: "*" allowedMethods: - GET - POST - DELETE - PUT - OPTION routes: # 探花系统 - id: tanhua-app-server uri: lb://tanhua-app-server predicates: - Path=/app/** filters: - StripPrefix= 1 # 后台系统 - id: tanhua-admin uri: lb://tanhua-admin predicates: - Path=/admin/** filters: - StripPrefix= 1 gateway: excludedUrls: /user/login,/user/loginVerification,/system/users/verification,/system/users/login1.2.2 配置鉴权管理器@Component public class AuthFilter implements GlobalFilter, Ordered { //排除的链接 @Value("#{'${gateway.excludedUrls}'.split(',')}") private List<String> excludedUrls; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String url = exchange.getRequest().getURI().getPath(); System.out.println( "url:"+ url); //排除特殊接口 不校验 if(excludedUrls.contains(url)){ return chain.filter(exchange); } String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if(!StringUtils.isEmpty(token)){ token = token.replace("Bearer ", ""); } ServerHttpResponse response = exchange.getResponse(); //2、使用工具类，判断token是否有效 boolean verifyToken = JwtUtils.verifyToken(token); //3、如果token失效，返回状态码401，拦截 if(!verifyToken) { Map<String, Object> responseData = new HashMap<>(); responseData.put("errCode", 401); responseData.put("errMessage", "用户未登录"); return responseError(response,responseData); } return chain.filter(exchange); } //响应错误数据 private Mono<Void> responseError(ServerHttpResponse response,Map<String, Object> responseData){ // 将信息转换为 JSON ObjectMapper objectMapper = new ObjectMapper(); byte[] data = new byte[0]; try { data = objectMapper.writeValueAsBytes(responseData); } catch (JsonProcessingException e) { e.printStackTrace(); } // 输出错误信息到页面 DataBuffer buffer = response.bufferFactory().wrap(data); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } /** * 设置过滤器的执行顺序 */ @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }1.3 Nacos配置中心Nacos提供了注册中心和配置管理的能力，使用Nacos可以快速实现服务发现、服务配置管理等需求1.3.1 添加依赖<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>1.3.2 添加bootstrap.yml配置server: port: 8888 spring: profiles: active: prod application: name: tanhua-gateway cloud: nacos: discovery: server-addr: 192.168.136.160:8848 config: server-addr: 192.168.136.160:8848 file-extension: yml1.3.3 nacos添加配置server: port: 9997 spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true corsConfigurations: '[/**]': allowedHeaders: "*" allowedOrigins: "*" allowedMethods: - GET - POST - DELETE - PUT - OPTION routes: # 用户微服务 - id: tanhua-app-server uri: lb://tanhua-app-server predicates: - Path=/app/** filters: - StripPrefix= 1 # 文章微服务 - id: tanhua-admin uri: lb://tanhua-admin predicates: - Path=/admin/** filters: - StripPrefix= 1 gateway: excludedUrls: /user/login,/user/loginVerification,/system/users/verification,/system/users/login2、后台系统2.1 概述探花交友APP建立的后台管理系统，目的是完成探花交友项目的业务闭环，主要功能包括：用户管理、动态管理、审核管理以及系统管理。课程中实现的功能有：登录、首页、用户管理、动态审核。2.2 环境前端搭建2.2.1 导入数据库将资料中的tanhua-admin.sql引入到mysql数据库中2.2.2 导入静态页面后台系统也是采用前后端分离的方式，前端采用Vue.js实现，关于前端系统我们不进行实现，拿来直接使用。nginx安装将资料中提供的nginx解压到合适的位置其中html目录中为，vue编译后的所有页面。修改Nginx的/conf/nginx.conf配置文件：server { listen 8088; #请求端口 server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; } location /management { proxy_pass http://127.0.0.1:18083/; #转发后台地址 } #....略 }访问vue页面的路径：http://localhost:8088/其中内部调用java服务器的路径 ：http://127.0.0.1:18083/测试双击nginx.exe,待启动完成后访问：http://127.0.0.1:8088即可访问后台项目2.3 搭建后端环境2.3.1 Admin实体类//后台系统的管理员对象 @Data @NoArgsConstructor @AllArgsConstructor public class Admin implements Serializable { /** * id */ private Long id; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 头像 */ private String avatar; }2.3.2 导入项目将今日资料中的tanhua-admin模块导入到探花项目中，完成开发。2.3.3 nacos中加入配置DataID：tanhua-admin-prod.ymlspring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/tanhua-admin?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false username: root password: root rabbitmq: host: 192.168.136.160 port: 5672 username: guest password: guest redis: host: 192.168.136.160 port: 6379 cloud: #nacos配置 nacos: discovery: server-addr: 192.168.136.160:8848 dubbo: #dubbo配置 registry: address: spring-cloud://localhost consumer: check: false retries: 0 protocols: dubbo: port: -1 #配置短信平台信息 tanhua: #手机验证码，咱们自己注册（不要白嫖） sms: signName: 物流云商 templateCode: SMS_106590012 accessKey: LTAI4GKgob9vZ53k2SZdyAC7 secret: LHLBvXmILRoyw0niRSBuXBZewQ30la oss: accessKey: LTAI4GKgob9vZ53k2SZdyAC7 secret: LHLBvXmILRoyw0niRSBuXBZewQ30la endpoint: oss-cn-beijing.aliyuncs.com bucketName: tanhua143 url: https://tanhua143.oss-cn-beijing.aliyuncs.com aip: appId: 22837663 apiKey: nA43galrxfUZTGtYRVK8F8tb secretKey: MQp567q4nGnIKfniURa2XAw8bT1SlPE3 huanxin: appkey: 1110201018107234#tanhua clientId: YXA6nxJJ_pdEQ_eYUlqcRicS4w clientSecret: YXA6GMUxVEZhAvxlMn4OvHSXbWuEUTE #mybaits-plus mybatis-plus: global-config: db-config: table-prefix: tb_ #数据库表前缀 id-type: auto #数据库表主键的策略2.4 管理员登录后台系统的登录模块独立于APP端的登录。2.4.1 生成验证码验证码：页面端发送请求到服务端。服务端生成一个验证码的图片，已流的形式返回SystemController/** * 生成图片验证码 */ @GetMapping("/verification") public void verification(String uuid, HttpServletResponse response) throws IOException { //设置响应参数 response.setDateHeader("Expires", 0); // Set standard HTTP/1.1 no-cache headers. response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); // Set IE extended HTTP/1.1 no-cache headers (use addHeader). response.addHeader("Cache-Control", "post-check=0, pre-check=0"); // Set standard HTTP/1.0 no-cache header. response.setHeader("Pragma", "no-cache"); response.setContentType("image/jpeg"); //1、通过工具类生成验证码对象（图片数据和验证码信息） LineCaptcha captcha = CaptchaUtil.createLineCaptcha(299, 97); String code = captcha.getCode(); //1234 //2、调用service，将验证码存入到redis redisTemplate.opsForValue().set(Constants.CAP_CODE+uuid,code); //3、通过输出流输出验证码 captcha.write(response.getOutputStream()); }2.4.2 用户登录SystemController/** * 用户登录： */ @PostMapping("/login") public ResponseEntity login(@RequestBody Map map) { Map retMap = adminService.login(map); return ResponseEntity.ok(retMap); }SystemService//用户登录 public ResponseEntity 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("用户名或者密码为空"); Map map1 = new HashMap(); map.put("message","用户名或者密码为空"); return ResponseEntity.status(500).body(map1); } //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<>(); qw.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 res = new HashMap(); res.put("token", token); return ResponseEntity.ok(res); }2.4.3 获取用户资料AdminVO@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; } }SystemController/** * 获取用户的信息 */ @PostMapping("/profile") public ResponseEntity profile() { AdminVo vo = adminService.profile(); return ResponseEntity.ok(vo); }SystemService//获取当前用户的用户资料 public AdminVo profile() { Long id = AdminHolder.getId(); Admin admin = adminMapper.selectById(id); return AdminVo.init(admin); }3、用户管理3.1 需求分析用户管理：对探花交友客户端注册用户的管理（查询业务数据），使用Dubbo的形式调用tanhua-dubbo-service获取响应的数据结果3.2 查询用户列表ManageController@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); }ManagerServicepublic ResponseEntity findAllUsers(Integer page, Integer pagesize) { //1、调用API分页查询数据列表 Ipage<UserInfo> IPage<UserInfo> iPage = userInfoApi.findAll(page,pagesize); //2、需要将Ipage转化为PageResult PageResult result = new PageResult(page, pagesize, iPage.getTotal(), iPage.getRecords()); //3、构造返回值 return ResponseEntity.ok(result); }UserInfoAPI@Override public IPage<UserInfo> findAll(Integer page, Integer pagesize) { return userInfoMapper.selectPage(new Page<UserInfo>(page,pagesize),null); }3.3 查看用户详情ManageController/** * 根据id查询用户详情 */ @GetMapping("/users/{userId}") public ResponseEntity findById(@PathVariable("userId") Long userId) { return managerService.findById(userId); }ManagerService//根据id查询用户详情 public ResponseEntity findById(Long userId) { UserInfo info = userInfoApi.findById(userId); return ResponseEntity.ok(info); }3.4 查看视频列表ManageController/** * 查询指定用户发布的所有视频列表 */ @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) }ManagerService//根据用户id分页查询此用户发布的所有视频列表 public PageResult findAllVideos(Integer page, Integer pagesize, Long userId) { //1、调用API查询视频列表（PageResult ） List items = videoApi.findByUserId(page,pagesize,userId); //2、获取到分页对象中的List List UserInfo info = userInfoApi.findById(userId); //3、一个Video转化成一个VideoVo List<VideoVo> list = new ArrayList<>(); for (Video item : items) { VideoVo vo = VideoVo.init(info, item); list.add(vo); } //4、构造返回 return ResponseEntity.ok(new PageResult(page,pagesize,0l,list)); }3.5 查看动态列表ManageController@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) }ManagerService//查询指定用户发布的所有动态 public ResponseEntity findAllMovements(Integer page, Integer pagesize, Long userId, Integer state) { //1、调用API查询 ：（PageResult<Publish>） PageResult result = movementApi.findByUserId(userId,page,pagesize); //2、一个Publsh构造一个Movements List<Movement> items = ( List<Movement>)result.getItems(); List<MovementsVo> list = new ArrayList<>(); for (Movement item : items) { UserInfo userInfo = userInfoApi.findById(item.getUserId()); MovementsVo vo = MovementsVo.init(userInfo, item); list.add(vo); } //3、构造返回值 result.setItems(list); return ResponseEntity.ok(result); }
我的动态（查询思路）圈子的互动（点赞，喜欢，评论）数据表：quanzi_comment(commentType记录操作类型)点赞，喜欢，评论（保存记录）取消点赞，取消喜欢（删除记录）为了保证效率（结合redis缓存，互动数量记录到动态表）课程说明小视频功能说明FastDFS环境搭建小视频的功能实现1、我的访客查询别人来访了我的主页的信息，其他用户在浏览我的主页时，需要记录访客数据。访客在一天内每个用户只记录一次。查询数据时，如果用户查询过列表，就需要记录这次查询数据的时间，下次查询时查询大于等于该时间的数据。如果，用户没有记录查询时间，就查询最近的5个来访用户。1.1、dubbo服务1.1.1、实体对象@Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "visitors") public class Visitors implements java.io.Serializable{ private static final long serialVersionUID = 2811682148052386573L; private ObjectId id; private Long userId; //我的id private Long visitorUserId; //来访用户id private String from; //来源，如首页、圈子等 private Long date; //来访时间 private String visitDate;//来访日期 private Double score; //得分 }1.1.2、定义接口package com.tanhua.dubbo.server.api; import com.tanhua.dubbo.server.pojo.Visitors; import java.util.List; public interface VisitorsApi { /** * 保存访客数据 * * @param userId 我的id * @param visitorUserId 访客id * @param from 来源 * @return */ String save(Long userId, Long visitorUserId, String from); /** * 查询我的访客数据，存在2种情况： * 1. 我没有看过我的访客数据，返回前5个访客信息 * 2. 之前看过我的访客，从上一次查看的时间点往后查询5个访客数据 */ List<Visitors> queryMyVisitor(Long userId); }1.1.3、编写实现package com.tanhua.dubbo.server.api; import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; import com.alibaba.dubbo.config.annotation.Service; import com.tanhua.dubbo.server.pojo.RecommendUser; import com.tanhua.dubbo.server.pojo.Visitors; import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.redis.core.RedisTemplate; import java.util.List; @Service(version = "1.0.0") public class VisitorsApiImpl implements VisitorsApi { @Autowired private MongoTemplate mongoTemplate; private static final String VISITOR_REDIS_KEY = "VISITOR_USER"; @Autowired private RedisTemplate<String, String> redisTemplate; @Override public String saveVisitor(Long userId, Long visitorUserId, String from) { //校验 if (!ObjectUtil.isAllNotEmpty(userId, visitorUserId, from)) { return null; } //查询访客用户在今天是否已经记录过，如果已经记录过，不再记录 String today = DateUtil.today(); Long minDate = DateUtil.parseDateTime(today + " 00:00:00").getTime(); Long maxDate = DateUtil.parseDateTime(today + " 23:59:59").getTime(); Query query = Query.query(Criteria.where("userId").is(userId) .and("visitorUserId").is(visitorUserId) .andOperator(Criteria.where("date").gte(minDate), Criteria.where("date").lte(maxDate) ) ); long count = this.mongoTemplate.count(query, Visitors.class); if (count > 0) { //今天已经记录过的 return null; } Visitors visitors = new Visitors(); visitors.setFrom(from); visitors.setVisitorUserId(visitorUserId); visitors.setUserId(userId); visitors.setDate(System.currentTimeMillis()); visitors.setId(ObjectId.get()); //存储数据 this.mongoTemplate.save(visitors); return visitors.getId().toHexString(); } @Override public List<Visitors> queryMyVisitor(Long userId) { // 查询前5个访客数据，按照访问时间倒序排序 // 如果用户已经查询过列表，记录查询时间，后续查询需要按照这个时间往后查询 // 上一次查询列表的时间 Long date = Convert.toLong(this.redisTemplate.opsForHash().get(VISITOR_REDIS_KEY, String.valueOf(userId))); PageRequest pageRequest = PageRequest.of(0, 5, Sort.by(Sort.Order.desc("date"))); Query query = Query.query(Criteria.where("userId").is(userId)) .with(pageRequest); if (ObjectUtil.isNotEmpty(date)) { query.addCriteria(Criteria.where("date").gte(date)); } List<Visitors> visitorsList = this.mongoTemplate.find(query, Visitors.class); //查询每个来访用户的得分 for (Visitors visitors : visitorsList) { Query queryScore = Query.query(Criteria.where("toUserId") .is(userId).and("userId").is(visitors.getVisitorUserId()) ); RecommendUser recommendUser = this.mongoTemplate.findOne(queryScore, RecommendUser.class); if(ObjectUtil.isNotEmpty(recommendUser)){ visitors.setScore(recommendUser.getScore()); }else { //默认得分 visitors.setScore(90d); } } return visitorsList; } }1.2、记录访客数据//com.tanhua.server.service.TanHuaService public TodayBest queryUserInfo(Long userId) { UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(userId); if(ObjectUtil.isEmpty(userInfo)){ return null; } TodayBest todayBest = new TodayBest(); todayBest.setId(userId); todayBest.setAge(userInfo.getAge()); todayBest.setGender(userInfo.getSex().name().toLowerCase()); todayBest.setNickname(userInfo.getNickName()); todayBest.setTags(Convert.toStrArray(StrUtil.split(userInfo.getTags(),','))); todayBest.setAvatar(userInfo.getLogo()); //缘分值 User user = UserThreadLocal.get(); todayBest.setFateValue(this.recommendUserService.queryScore(userId, user.getId()).longValue()); //记录来访用户 this.visitorsApi.saveVisitor(userId, user.getId(), "个人主页"); return todayBest; }1.3、首页谁看过我1.3.1、VO对象@Data @NoArgsConstructor @AllArgsConstructor public class VisitorsVo { private Long id; //用户id private String avatar; private String nickname; private String gender; //性别 man woman private Integer age; private String[] tags; private Long fateValue; //缘分值 /** * 在vo对象中，补充一个工具方法，封装转化过程 */ public static VisitorsVo init(UserInfo userInfo, Visitors visitors) { VisitorsVo vo = new VisitorsVo(); BeanUtils.copyProperties(userInfo,vo); if(userInfo.getTags() != null) { vo.setTags(userInfo.getTags().split(",")); } vo.setFateValue(visitors.getScore().longValue()); return vo; } }1.3.2、MovementController/** * 谁看过我 */ @GetMapping("visitors") public ResponseEntity queryVisitorsList(){ List<VisitorsVo> list = movementService.queryVisitorsList(); return ResponseEntity.ok(list); }1.3.3、MovementServicepublic List<VisitorsVo> queryVisitorsList() { User user = UserThreadLocal.get(); List<Visitors> visitorsList = this.visitorsApi.queryMyVisitor(user.getId()); if (CollUtil.isEmpty(visitorsList)) { return Collections.emptyList(); } List<Object> userIds = CollUtil.getFieldValues(visitorsList, "visitorUserId"); List<UserInfo> userInfoList = this.userinfoService.queryByUserIdList(userIds); List<VisitorsVo> visitorsVoList = new ArrayList<>(); for (Visitors visitor : visitorsList) { for (UserInfo userInfo : userInfoList) { if (ObjectUtil.equals(visitor.getVisitorUserId(), userInfo.getUserId())) { VisitorsVo visitorsVo = BeanUtil.toBeanIgnoreError(userInfo, VisitorsVo.class); visitorsVo.setGender(userInfo.getSex().name().toLowerCase()); visitorsVo.setFateValue(visitor.getScore().intValue()); visitorsVoList.add(visitorsVo); break; } } } return visitorsVoList; }2、小视频功能说明小视频功能类似于抖音、快手小视频的应用，用户可以上传小视频进行分享，也可以浏览查看别人分享的视频，并且可以对视频评论和点赞操作。1、视频发布（视频：容量大，视频存储到什么位置？）2、查询视频列表（问题：数据库表）3、关注视频作者4、视频播放（客户端获取视频的URL地址，自动的播放）效果：查看详情：评论：点赞：3、FastDFS视频存储阿里云OSS（视频简单，贵！！！）自建存储系统对于小视频的功能的开发，核心点就是：存储 + 推荐 + 加载速度 。对于存储而言，小视频的存储量以及容量都是非常巨大的所以我们选择自己搭建分布式存储系统 FastDFS进行存储对于推荐算法，我们将采用多种权重的计算方式进行计算对于加载速度，除了提升服务器带宽外可以通过CDN的方式进行加速，当然了这需要额外购买CDN服务2.1、FastDFS是什么？FastDFS是分布式文件系统。使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。2.2、工作原理FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载，通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。Tracker server 作用是负载均衡和调度，通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。Storage server 作用是文件存储，客户端上传的文件最终存储在 Storage 服务器上，Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。每个 tracker 节点地位平等。收集 Storage 集群的状态。Storage 分为多个组，每个组之间保存的文件是不同的。每个组内部可以有多个成员，组成员内部保存的内容是一样的，组成员的地位是一致的，没有主从的概念。2.1.3、文件的上传客户端上传文件后存储服务器将文件 ID 返回给客户端，此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括：组名，虚拟磁盘路径，数据两级目录，文件名。2.1.4、文件的下载客户端下载请求到Tracker服务，Tracker返回给客户端storage的信息，客户端根据这些信息进行请求storage获取到文件。2.3、FastDFS环境搭建企业中搭建FastDFS是一个比较繁琐和复杂的过程（多个服务器之间的配合和配置等，专业的人员搭建），但是在学习阶段。由于所有的组件全部配置到linux虚拟机，已docker运行。所以linux的内存有要求（运行的过程中，可能会出现fastdfs的容器，启动之后自动关闭，表示虚拟机内存不足，适当的扩大内存），学习环境中使用一台调度服务器，一台存储服务器2.3.1、搭建服务我们使用docker进行搭建。目前所有的组件全部以docker的形式配置#进入目录 cd /root/docker-file/fastdfs/ #启动 docker-compose up -d #查看容器 docker ps -aFastDFS调度服务器地址：192.168.136.160:22122FastDFS存储服务器地址：http://192.168.136.160:8888/2.3.2、java client导入依赖（已经存在，被注释）在application.yml中配置Fastdfs注入FastFileStorageClient对象，完成文件上传导入依赖：找到 tanhua-server的pom文件，打开fastdfs的依赖如下<dependency> <groupId>com.github.tobato</groupId> <artifactId>fastdfs-client</artifactId> <version>1.26.7</version> <exclusions> <exclusion> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </exclusion> </exclusions> </dependency>application.yml# =================================================================== # 分布式文件系统FDFS配置 # =================================================================== fdfs: so-timeout: 1500 connect-timeout: 600 #缩略图生成参数 thumb-image: width: 150 height: 150 #TrackerList参数,支持多个 tracker-list: 192.168.136.160:22122 web-server-url: http://192.168.136.160:8888/测试package com.tanhua.server.test; import com.github.tobato.fastdfs.domain.conn.FdfsWebServer; import com.github.tobato.fastdfs.domain.fdfs.StorePath; import com.github.tobato.fastdfs.service.FastFileStorageClient; import com.tanhua.server.TanhuaServerApplication; 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.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @RunWith(SpringRunner.class) @SpringBootTest(classes = TanhuaServerApplication.class) public class TestFastDFS { //测试将文件上传到FastDFS文件系统中 //从调度服务器获取，一个目标存储服务器，上传 @Autowired private FastFileStorageClient client; @Autowired private FdfsWebServer webServer;// 获取存储服务器的请求URL @Test public void testFileUpdate() throws FileNotFoundException { //1、指定文件 File file = new File("D:\\1.jpg"); //2、文件上传 StorePath path = client.uploadFile(new FileInputStream(file), file.length(), "jpg", null); //3、拼接访问路径 String url = webServer.getWebServerUrl() + path.getFullPath(); } }存储服务器：在线的存储服务器：阿里云OSS自己搭建分布式的存储服务器：fastdfs4、发布小视频4.0、分析过程表结构{ "_id" : ObjectId("5fa60707ed0ad13fa89925cc"), "vid" : NumberLong(1), "userId" : NumberLong(1), "text" : "我就是我不一样的烟火~", "picUrl" : "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/video/video_1.png", "videoUrl" : "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/video/1576134125940400.mp4", "created" : NumberLong(1604716296066), "likeCount" : 0, "commentCount" : 0, "loveCount" : 0, "_class" : "com.tanhua.domain.mongo.Video" }操作步骤4.1、搭建环境4.1.1 实体类tanhua-domain中配置实体类Videopackage com.tanhua.domain.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "video") public class Video implements java.io.Serializable { private static final long serialVersionUID = -3136732836884933873L; private ObjectId id; //主键id private Long vid; //自动增长 private Long created; //创建时间 private Long userId; private String text; //文字 private String picUrl; //视频封面文件，URL private String videoUrl; //视频文件，URL private Integer likeCount=0; //点赞数 private Integer commentCount=0; //评论数 private Integer loveCount=0; //喜欢数 }4.1.2 API接口与实现定义接口tanhua-dubbo-interface工程定义API接口VideoApipackage com.tanhua.dubbo.api.mongo; import com.tanhua.domain.mongo.FollowUser; import com.tanhua.domain.mongo.Video; import com.tanhua.domain.vo.PageResult; public interface VideoApi { }接口实现类tanhua-dubbo-service工程定义API接口实现类VideoApiImplpackage com.tanhua.dubbo.api.mongo; import com.tanhua.domain.mongo.FollowUser; import com.tanhua.domain.mongo.Video; import com.tanhua.domain.vo.PageResult; import com.tanhua.dubbo.utils.IdService; import org.apache.dubbo.config.annotation.Service; import org.bson.types.ObjectId; import org.checkerframework.checker.units.qual.C; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import java.util.List; @Service public class VideoApiImpl implements VideoApi { }4.1.3、controller控制器tanhua-server定义 SmallVideoControllerpackage com.tanhua.server.controller; import com.tanhua.server.service.VideoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @RestController @RequestMapping("/smallVideos") public class SmallVideoController { @Autowired private SmallVideosService videosService; }4.1.4、service业务层tanhua-server定义 SmallVideosServicepackage com.tanhua.server.service; import com.github.tobato.fastdfs.domain.conn.FdfsWebServer; import com.github.tobato.fastdfs.domain.fdfs.StorePath; import com.github.tobato.fastdfs.service.FastFileStorageClient; import com.tanhua.autoconfig.templates.OssTemplate; import com.tanhua.domain.db.UserInfo; import com.tanhua.domain.mongo.FollowUser; import com.tanhua.domain.mongo.Video; import com.tanhua.domain.vo.PageResult; import com.tanhua.domain.vo.VideoVo; import com.tanhua.dubbo.api.UserInfoApi; import com.tanhua.dubbo.api.mongo.VideoApi; import com.tanhua.server.interceptor.UserHolder; import org.apache.dubbo.config.annotation.Reference; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.ArrayList; import java.util.List; @Service public class SmallVideosService { @Autowired private OssTemplate ossTemplate; @Autowired private FastFileStorageClient client; @Autowired private FdfsWebServer webServer; @Reference private VideoApi videoApi; @Reference private UserInfoApi userInfoApi; @Autowired private RedisTemplate<String,String> redisTemplate; }4.2、发布视频4.2.1、接口文档http://192.168.136.160:3000/project/19/interface/api/2144.2.2、SmallVideosController@RestController @RequestMapping("/smallVideos") public class SmallVideosController { @Autowired private SmallVideosService videosService; /** * 发布视频 * 接口路径：POST * 请求参数： * videoThumbnail：封面图 * videoFile：视频文件 */ @PostMapping public ResponseEntity saveVideos(MultipartFile videoThumbnail,MultipartFile videoFile) throws IOException { videosService.saveVideos(videoThumbnail,videoFile); return ResponseEntity.ok(null); } }4.2.3、SmallVideosService@Service public class SmallVideosService { @Reference private VideoApi videoApi; @Reference private UserInfoApi userInfoApi; @Autowired private OssTemplate ossTemplate; @Autowired private FastFileStorageClient storageClient; @Autowired private FdfsWebServer webServer; @Autowired private RedisTemplate<String,String> redisTemplate; /** * 发布视频 */ //@CacheEvict(value = "videoList",allEntries = true) public ResponseEntity saveVideos(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException { //1、封面图上传到阿里云OSS，获取地址 String picUrl = ossTemplate.upload(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream()); //2、视频上传到fdfs上，获取请求地址 //获取文件的后缀名 String filename = videoFile.getOriginalFilename(); //ssss.avi String sufix = filename.substring(filename.lastIndexOf(".")+1); StorePath storePath = storageClient.uploadFile(videoFile.getInputStream(), videoFile.getSize(), sufix, null);//文件输入流，文件长度，文件后缀，元数据 String videoUrl = webServer.getWebServerUrl() + storePath.getFullPath(); //3、创建Video对象，并设置属性 Video video = new Video(); video.setUserId(UserHolder.getUserId()); video.setPicUrl(picUrl); video.setVideoUrl(videoUrl); video.setText("我就是我，不一样的烟火"); //4、调用API完成保存 videoApi.save(video); //5、构造返回值 return ResponseEntity.ok(null); } }4.2.4、API接口VideoApi与VideoApiImpl中编写保存video的方法//视频服务实现类 @Service public class VideoApiImpl implements VideoApi { @Autowired private MongoTemplate mongoTemplate; @Autowired private IdService idService; //保存 public void save(Video video) { video.setId(ObjectId.get()); video.setCreated(System.currentTimeMillis()); video.setVid(idService.getNextId("video")); mongoTemplate.save(video); } }4.2.5、测试问题对于SpringBoot工程进行文件上传，默认支持最大的文件是1M。为了解决这个问题，需要在application.yml中配置文件限制如果上传视频，会导致异常，是因为请求太大的缘故：4.2.6、配置文件解析在tanhua-server工程的application.yml中添加解析器，配置请求文件和请求体Spring: servlet: multipart: max-file-size: 30MB max-request-size: 30MB5、小视频列表5.0、分析数据库表：video创建VO对象创建controller对象，并配置分页查询接口方法创建service对象，其中调用API，构造vo对象返回值调用API：PageResult 将Video转化成VO对象在API服务层，创建方法，分页查询小视频列表，返回 PageResult 5.1、Controller/** * 视频列表 */ @GetMapping public ResponseEntity queryVideoList(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize) { PageResult result = videosService.queryVideoList(page, pagesize); return ResponseEntity.ok(result); }5.2、Servicepublic PageResult queryVideoList(Integer page, Integer pagesize) { //1、调用API查询分页数据 PageResult PageResult result = videoApi.findAll(page,pagesize); //2、获取分页对象中list集合 List List items = (List )result.getItems(); //3、一个Video转化成一个VideoVo对象 List<VideoVo> list = new ArrayList<>(); for (Video item : items) { UserInfo info = userInfoApi.findById(item.getUserId()); VideoVo vo = VideoVo.init(info, item); //加入了作者关注功能，从redis判断是否存在关注的key，如果存在设置hasFocus=1 if(redisTemplate.hasKey("followUser_"+UserHolder.getUserId()+"_"+item.getUserId())) { vo.setHasFocus(1); } list.add(vo); } //4、替换PageResult中的list列表 result.setItems(list); //5、构造返回值 return result; }5.3、API接口和实现VideoApi与VideoApiImpl中编写分页查询方法//分页查询视频列表 public PageResult findAll(Integer page, Integer pagesize) { //1、查询总数 long count = mongoTemplate.count(new Query(), Video.class); //2、分页查询数据列表 Query query = new Query().limit(pagesize).skip((page-1) * pagesize) .with(Sort.by(Sort.Order.desc("created"))); List list = mongoTemplate.find(query, Video.class); //3、构建返回 return new PageResult(page,pagesize,(int) count,list); }5.4、定义Vo对象tanhua-domain定义返回VO对象VideoVopackage com.tanhua.domain.vo; import com.tanhua.domain.db.UserInfo; import com.tanhua.domain.mongo.Video; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.beans.BeanUtils; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor public class VideoVo implements Serializable { private Long userId; private String avatar; //头像 private String nickname; //昵称 private String id; private String cover; //封面 private String videoUrl; //视频URL private String signature; //发布视频时，传入的文字内容 private Integer likeCount; //点赞数量 private Integer hasLiked; //是否已赞（1是，0否） private Integer hasFocus; //是否关注 （1是，0否） private Integer commentCount; //评论数量 public static VideoVo init(UserInfo userInfo, Video item) { VideoVo vo = new VideoVo(); //copy用户属性 BeanUtils.copyProperties(userInfo,vo); //source,target //copy视频属性 BeanUtils.copyProperties(item,vo); vo.setCover(item.getPicUrl()); vo.setId(item.getId().toHexString()); vo.setSignature(item.getText()); vo.setHasFocus(0); vo.setHasLiked(0); return vo; } }5.5、测试测试问题在运行测试时，及其容易出现空指针等异常。解决方法之所以出现这类问题“”或者空指针异常，是由于MongoDB中非关系数据库，不能自动约束检测表关系。我们检查Video数据库表得知。其中有几条数据的发布人是虚拟构造，在用户表中并不存在解决思路很简单，删除错误数据即可6、关注用户关注用户是关注小视频发布的作者，这样我们后面计算推荐时，关注的用户将权重更重一些。关注用户controller：接受参数service：调用API，操作redisapi接口（VideoAPI）：关注的保存和删除修改之前的查询service，从redis获取数据（如果存在返回1:，不存在返回0）6.1、实体类对象package com.tanhua.domain.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; //用户关注表（关注小视频的发布作者） @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "focus_user") public class FocusUser implements java.io.Serializable{ private static final long serialVersionUID = 3148619072405056052L; private ObjectId id; //主键id private Long userId; //用户id 106 private Long followUserId; //关注的用户id 1 private Long created; //关注时间 }6.2、Controller/** * 关注视频作者 */ @PostMapping("/{id}/userFocus") public ResponseEntity userFocus(@PathVariable("id") Long followUserId) { videosService.userFocus(followUserId); return ResponseEntity.ok(null); } /** * 取消关注视频作者 */ @PostMapping("/{id}/userUnFocus") public ResponseEntity userUnFocus(@PathVariable("id") Long followUserId) { videosService.userUnFocus(followUserId); return ResponseEntity.ok(null); }6.3、Service//关注视频作者 public void userFocus(Long followUserId) { //1、创建FollowUser对象，并设置属性 FollowUser followUser = new FollowUser(); followUser.setUserId(UserHolder.getUserId()); followUser.setFollowUserId(followUserId); //2、调用API保存 videoApi.saveFollowUser(followUser); //3、将关注记录存入redis中 String key = Constants.FOCUS_USER_KEY + UserHolder.getUserId(); String hashKey = String.valueOf(followUserId); redisTemplate.opsForHash().put(key,hashKey,"1"); } //取消关注视频作者 public void userUnFocus(Long followUserId) { //1、调用API删除关注数据 videoApi.deleteFollowUser(UserHolder.getUserId(),followUserId); //2、删除redis中关注记录 String key = Constants.FOCUS_USER_KEY + UserHolder.getUserId(); String hashKey = String.valueOf(followUserId); redisTemplate.opsForHash().delete(key,hashKey); }6.4、API和实现类VideoApi与VideoApiImpl中编写关注方法解决重复关注的问题：在保存关注数据时，可以根据userId和followUserId查询数据库，如果存在则不再保存数据6.5、修改查询视频列表查询视频列表是，从redis中获取关注数据7、通用缓存SpringCache实现缓存逻辑有2种方式：每个接口单独控制缓存逻辑统一控制缓存逻辑Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术；并支持使用JCache（JSR-107）注解简化我们开发；Cache接口为缓存的组件规范定义，包含缓存的各种操作集合；Cache接口下Spring提供了各种xxxCache的实现；如RedisCache，EhCacheCache ,ConcurrentMapCache等；每次调用需要缓存功能的方法时，Spring会检查检查指定参数的指定的目标方法是否已经被调用过；如果有就直接从缓存中获取方法调用后的结果，如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。使用Spring缓存抽象时我们需要关注以下两点；1、确定方法需要被缓存以及他们的缓存策略2、从缓存中读取之前缓存存储的数据内部使用AOP的形式，对redis操作进行简化7.1、重要概念名称解释@Cacheable主要针对方法配置，能够根据方法的请求参数对其进行缓存@CacheEvict清空缓存更多注解看下面文章[post cid="1835" cover="" size=""/]7.2、入门案例7.2.1 导入依赖导入SpringDataRedis的依赖，并在application.yml中配置 （略）7.2.2 开启缓存支持然后在启动类注解@EnableCaching开启缓存@SpringBootApplication @EnableCaching //开启缓存 public class DemoApplication{ public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }7.2.3 编写UserInfoServicepackage com.tanhua.server.test; import com.tanhua.domain.db.UserInfo; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service public class UserInfoService { //根据id查询 public UserInfo queryById(Long userId) { //从数据库查询 UserInfo user = new UserInfo(); user.setId(userId); user.setNickname("ceshi"); return user; } //根据id修改 public void update(Long userId) { UserInfo user = new UserInfo(); user.setId(userId); user.setNickname("itcast"); } }7.2.3 缓存@Cacheable@Cacheable注解会先查询是否已经有缓存，有会使用缓存，没有则会执行方法并缓存。@Cacheable(value = "user",key = "#userId") public UserInfo queryById(Long userId) { //从数据库查询 UserInfo user = new UserInfo(); user.setId(userId); user.setNickname("ceshi"); return user; }此处的 value是必需的，它指定了你的缓存存放在哪块命名空间。此处的 key是使用的spEL表达式，参考上章。这里有一个小坑，如果你把 methodName换成 method运行会报错，观察它们的返回类型，原因在于 methodName是 String而 methoh是 Method。此处的 User实体类一定要实现序列化 public class User implements Serializable，否则会报 java.io.NotSerializableException异常。到这里，你已经可以运行程序检验缓存功能是否实现。7.2.4 清除@CacheEvict@CachEvict 的作用 主要针对方法配置，能够根据一定的条件对缓存进行清空 。//根据id修改 @CacheEvict(value = "user",key = "#userId") public void update(Long userId) { //修改用户 UserInfo user = new UserInfo(); user.setId(userId); user.setNickname("itcast"); }7.3、视频列表缓存处理修改VideoService，分页列表存入缓存，发布视频删除缓存由于使用Reids缓存处理数据时，不能缓存ResponseEntity对象，所以需要修改方法返回值为PageResult@Cacheable(value="videoList",key="#page + '_' + #pagesize") public PageResult queryVideoList(Integer page, Integer pagesize) { //1、调用API查询 ： PageReulst PageResult result = videoApi.findAll(page,pagesize); //2、获取分页中的list集合 List List items = (List )result.getItems(); //3、循环视频列表，一个Video构造一个Vo List<VideoVo> list = new ArrayList<>(); for (Video item : items) { UserInfo userInfo = userInfoApi.findById(item.getUserId()); VideoVo vo = VideoVo.init(userInfo, item); //从redis中获取，当前用户是否已经关注了视频发布作者 String key = "followUser_"+UserHolder.getUserId()+"_"+item.getUserId(); if (redisTemplate.hasKey(key)) { vo.setHasFocus(1); } list.add(vo); } //4、替换result中的item数据 result.setItems(list); //5、构造返回值 result; }7.4、发布视频清空缓存//发布视频 @CacheEvict(value="videoList",allEntries = true) public ResponseEntity saveVideo(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException { //1、图片上传到阿里云oss，获取请求地址 String picUrl = ossTemplate.upload(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream()); //2、视频上传到fdfs上，获取请求地址 String filename = videoFile.getOriginalFilename(); //xxxx.avi //获取文件后缀 String sufix = filename.substring(filename.lastIndexOf(".")+1); StorePath storePath = client.uploadFile(videoFile.getInputStream(), videoFile.getSize(), sufix, null); //文件输入流，文件长度（大小），文件的后缀名，元数据（null） String videoUrl = webServer.getWebServerUrl() + storePath.getFullPath(); //3、构建Video对象，并设置属性 Video video = new Video(); video.setPicUrl(picUrl); video.setVideoUrl(videoUrl); video.setText("传智播客是一个负责任的教育机构~"); //客户端未传递，手动模拟 video.setUserId(UserHolder.getUserId()); //4、调用api保存 videoApi.save(video); //5、构建返回值 return ResponseEntity.ok(null); }
安装如果你的项目使用Maven构建并且spring boot是2.4.3以上版本，可以在pom.xml中添加下面代码：<dependency> <groupId>com.easemob.im</groupId> <artifactId>im-sdk-core</artifactId> <version>0.5.9</version> </dependency>如果你使用的spring-boot是2.4.3以下版本的，还需要在pom.xml中添加:<dependencyManagement> <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-bom</artifactId> <version>4.1.59.Final</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-bom</artifactId> <version>2020.0.4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>准备在使用Server SDK之前，需要准备环信appkey、Client ID、ClientSecre。如果你有环信管理后台账号并创建过应用，请先登录环信管理后台，点击 这里，然后到“应用列表” → 点击“查看”即可获取到appkey、Client ID、ClientSecret。使用EMService是所有API的入口，可以这样初始化：EMProperties properties = EMProperties.builder() .setAppkey(cliProperties.getAppkey()) .setClientId(cliProperties.getClientId()) .setClientSecret(cliProperties.getClientSecret()) .build(); EMService service = new EMService(properties);根据业务资源，API分为：Attachment 用于上传下载附件Block 用于限制访问Contact 用于管理联系人Group 用于管理群组Message 用于发送消息User 用于管理用户Room 用于管理聊天室后续用到的API所有的API上面链接可查// 创建环信用户 service.user().create("user01", "123456").block(); // 添加好友 service.contact().add("user01, "user02").block(); // 删除好友 service.contact().remove("user01", "uer02").block(); // 服务端发送消息 Set<String> set = CollUtil.newHashSet("123"); service.message().send("user01", "users", set, new EMTextMessage().text("java"), null) .block();
1、即时通信1.1、什么是即时通信？1.2、功能说明在探花交友项目中也提供了类似微信的聊天功能，用户可以和好友或陌生人聊天。如果是陌生人，通过《聊一下》功能进行打招呼，如果对方同意后，就成为了好友，可以进行聊天了。陌生人之间如果相互喜欢，那么就会成为好友，也就可以聊天了。在消息界面中也可以查看：点赞、评论、喜欢、公告等消息信息。1.3、技术方案对于高并发的即时通讯实现，还是很有挑战的，所需要考虑的点非常多，除了要实现功能，还要考虑并发、流量、负载、服务器、容灾等等。虽然有难度也并不是高不可攀。对于现实即时通讯往往有两种方案：方案一：自主实现，从设计到架构，再到实现。技术方面可以采用：Netty + WebSocket + RocketMQ + MongoDB + Redis + ZooKeeper + MySQL方案二：对接第三方服务完成。这种方式简单，只需要按照第三方的api进行对接就可以了。如：环信、网易、容联云通讯等。如何选择呢？如果是中大型企业做项目可以选择自主研发，如果是中小型企业研发中小型的项目，选择第二种方案即可。方案一需要有大量的人力、物力的支持，开发周期长，成本高，但可控性强。方案二，成本低，开发周期短，能够快速的集成起来进行功能的开发，只是在可控性方面来说就差了一些。探花交友项目选择方案二进行实现。2、环信官网：https://www.easemob.com/ 稳定健壮，消息必达，亿级并发的即时通讯云2.1、开发简介平台架构：集成：环信和用户体系的集成主要发生在2个地方，服务器端集成和客户端集成。探花集成：探花前端使用AndroidSDK进行集成文档：http://docs-im.easemob.com/im/android/sdk/import后端集成用户体系文档：http://docs-im.easemob.com/im/server/ready/user2.2、环信Console需要使用环信平台，那么必须要进行注册，登录之后即可创建应用。环信100以内的用户免费使用，100以上就要注册企业版了。企业版价格：创建应用：创建完成：2.3、接口说明2.3.1、Appkey 数据结构当您申请了 AppKey 后，会得到一个 xxxx#xxxx 格式的字符串，字符串只能由小写字母数字组成，AppKey是环信应用的唯一标识。前半部分 org_name 是在多租户体系下的唯一租户标识，后半部分 app_name 是租户下的app唯一标识（在环信后台创建一个app时填写的应用 id 即是 app_name ）。下述的 REST API 中，/{org_name}/{app_name}的请求，均是针对一个唯一的appkey进行的。目前环信注册的appkey暂不能由用户自己完成删除操作，如果对 APP 删除需要联系环信操作完成。Appkeyxxxx分隔符xxxx环信应用的唯一标识org_name#app_name2.3.2、环信 ID 数据结构环信作为一个聊天通道，只需要提供环信 ID （也就是 IM 用户名）和密码就够了。名称字段名数据类型描述环信 IDusernameString在 AppKey 的范围内唯一用户名。用户密码passwordString用户登录环信使用的密码。2.3.4、获取管理员权限环信提供的 REST API 需要权限才能访问，权限通过发送 HTTP 请求时携带 token 来体现，下面描述获取 token 的方式。说明：API 描述的时候使用到的 {APP 的 client_id} 之类的这种参数需要替换成具体的值。重要提醒：获取 token 时服务器会返回 token 有效期，具体值参考接口返回的 expires_in 字段值。由于网络延迟等原因，系统不保证 token 在此值表示的有效期内绝对有效，如果发现 token 使用异常请重新获取新的 token，比如“http response code”返回 401。另外，请不要频繁向服务器发送获取 token 的请求，同一账号发送此请求超过一定频率会被服务器封号，切记，切记！！client_id 和 client_secret 可以在环信管理后台的 APP 详情页面看到。HTTP Request/{org_name}/{app_name}/token Request Headers参数说明Content-Typeapplication/jsonRequest Body参数说明grant_typeclient_credentialsclient_idApp的client_id，可在app详情页找到client_secretApp的client_secret，可在app详情页找到Response Body参数说明access_token有效的token字符串expires_intoken 有效时间，以秒为单位，在有效期内不需要重复获取application当前 App 的 UUID 值环信API介绍[post cid="1698" cover="" size=""/]3、抽取环信组件抽取环信组件到 tanhua-autoconfig工程中3.1、编写HuanXinTemplate@Slf4j public class HuanXinTemplate { private EMService service; public HuanXinTemplate(HuanXinProperties properties) { EMProperties emProperties = EMProperties.builder() .setAppkey(properties.getAppkey()) .setClientId(properties.getClientId()) .setClientSecret(properties.getClientSecret()) .build(); service = new EMService(emProperties); } //创建环信用户 public Boolean createUser(String username,String password) { try { //创建环信用户 service.user().create(username.toLowerCase(), password) .block(); return true; }catch (Exception e) { e.printStackTrace(); log.error("创建环信用户失败~"); } return false; } //添加联系人 public Boolean addContact(String username1,String username2) { try { //创建环信用户 service.contact().add(username1,username2) .block(); return true; }catch (Exception e) { log.error("添加联系人失败~"); } return false; } //删除联系人 public Boolean deleteContact(String username1,String username2) { try { //创建环信用户 service.contact().remove(username1,username2) .block(); return true; }catch (Exception e) { log.error("删除联系人失败~"); } return false; } //发送消息 public Boolean sendMsg(String username,String content) { try { //接收人用户列表 Set<String> set = CollUtil.newHashSet(username); //文本消息 EMTextMessage message = new EMTextMessage().text(content); //发送消息 from：admin是管理员发送 service.message().send("admin","users", set,message,null).block(); return true; }catch (Exception e) { log.error("删除联系人失败~"); } return false; } }3.2、编写Properties对象@Configuration @ConfigurationProperties(prefix = "tanhua.huanxin") @Data public class HuanXinProperties { private String appkey; private String clientId; private String clientSecret; }3.3、配置tanhua-app-server工程的 application.yml文件加入配置如下tanhua: huanxin: appkey: 1110201018107234#tanhua clientId: YXA6nxJJ_pdEQ_eYUlqcRicS4w clientSecret: YXA6GMUxVEZhAvxlMn4OvHSXbWuEUTE3.4、测试@RunWith(SpringRunner.class) @SpringBootTest public class HuanXinTest { @Autowired private HuanXinTemplate template; @Test public void testRegister() { template.createUser("user01","123456"); } }4、用户体系集成将用户体系集成的逻辑写入到 tanhua-server系统中。探花用户注册时需要将用户信息注册到环信系统中对于老数据：编写单元测试方法批量的注册到环信对于新用户：改造代码（用户注册的时候，自动注册到环信）APP从服务端获取当前用户的环信用户密码，自动登入环信系统编写一个接口，获取当前用户在环信的用户名密码APP自动获取环信服务器发送的信息数据4.1、注册环信用户在用户登录逻辑中，当第一次注册时，将用户信息注册到环信/** * 验证登录 * @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 BusinessException(ErrorResult.loginError()); } //3、删除redis中的验证码 redisTemplate.delete("CHECK_CODE_" + phone); //4、通过手机号码查询用户 User user = userApi.findByMobile(phone); boolean isNew = false; //5、如果用户不存在，创建用户保存到数据库中 if(user == null) { user = new User(); user.setMobile(phone); 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); } } //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; }4.2、查询环信用户信息在app中，用户登录后需要根据用户名密码登录环信，由于用户名密码保存在后台，所以需要提供接口进行返回。4.2.1 API接口api地址：http://192.168.136.160:3000/project/19/interface/api/854.2.2 vo对象import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class HuanXinUserVo { private String username; private String password; }4.2.3 代码实现编写HuanXinController实现：4.3、环信用户ID查询用户信息在好友聊天时，完全基于环信服务器实现。为了更好的页面效果，需要展示出用户的基本信息，这是需要通过环信用户id查询用户。MessagesController@RestController @RequestMapping("/messages") public class MessagesController { @Autowired private MessagesService messagesService; @GetMapping("/userinfo") public ResponseEntity userinfo(String huanxinId) { UserInfoVo vo = messagesService.findUserInfoByHuanxin(huanxinId); return ResponseEntity.ok(vo); } }MessagesService@Service public class MessagesService { @DubboReference private UserApi userApi; @DubboReference private UserInfoApi userInfoApi; @DubboReference private FriendApi friendApi; @Autowired private HuanXinTemplate huanXinTemplate; /** * 根据环信id查询用户详情 */ public UserInfoVo findUserInfoByHuanxin(String huanxinId) { //1、根据环信id查询用户 User user = userApi.findByHuanxin(huanxinId); //2、根据用户id查询用户详情 UserInfo userInfo = userInfoApi.findById(user.getId()); UserInfoVo vo = new UserInfoVo(); BeanUtils.copyProperties(userInfo,vo); //copy同名同类型的属性 if(userInfo.getAge() != null) { vo.setAge(userInfo.getAge().toString()); } return vo; } }4.4、发送消息给客户端目前已经完成了用户体系的对接，下面我们进行测试发送消息，场景是这样的：点击“聊一下”，就会给对方发送一条陌生人信息，这个消息由系统发送完成。我们暂时通过环信的控制台进行发送： 消息内容：{"userId":106,"huanXinId":"hx106","nickname":"黑马小妹","strangerQuestion":"你喜欢去看蔚蓝的大海还是去爬巍峨的高山？","reply":"我喜欢秋天的落叶，夏天的泉水，冬天的雪地，只要有你一切皆可~"}可以看到已经接收到了消息。4.5、数据处理针对新注册用户已经可以同步到环信服务器，而老数据需要程序员手动处理。注意：使用测试账号最多支持100个用户@Test public void register() { for (int i = 1; i < 106; i++) { User user = userApi.findById(Long.valueOf(i)); if(user != null) { Boolean create = template.createUser("hx" + user.getId(), Constants.INIT_PASSWORD); if (create){ user.setHxUser("hx" + user.getId()); user.setHxPassword(Constants.INIT_PASSWORD); userApi.update(user); } } } }5、联系人管理涵盖了系统业务：查看感兴趣的用户，点击“聊一下”，查看陌生人问题回答陌生人问题，会给感兴趣的用户发送一条消息（发送的添加好友的请求）对方获取一条消息（服务端发送）对方查看消息：如果两个投缘（点击聊一下，双方加为好友）将好友关系记录到探花的MongoDB数据库中将好友关系记录到环信成为好友后，可以查看好友列表对目标好友发送消息（和服务端无关）5.1、查看用户详情在首页可以查看感兴趣人的详细资料。点击“聊一下”，可以查看对方的问题5.1.1、mock接口地址：http://192.168.136.160:3000/project/19/interface/api/1035.1.2、TanhuaController/** * 查看佳人详情 */ @GetMapping("/{id}/personalInfo") public ResponseEntity personalInfo(@PathVariable("id") Long userId) { TodayBest best = tanhuaService.personalInfo(userId); return ResponseEntity.ok(best); }5.1.3、TanhuaService//查看佳人详情 public TodayBest personalInfo(Long userId) { //1、根据用户id查询，用户详情 UserInfo userInfo = userInfoApi.findById(userId); //2、根据操作人id和查看的用户id，查询两者的推荐数据 RecommendUser user = recommendUserApi.queryByUserId(userId,UserHolder.getUserId()); //3、构造返回值 return TodayBest.init(userInfo,user); }5.1.4、API接口和实现类RecommendUserApi和 RecommendUserApiImpl编写查询用户缘分值方法@Override public RecommendUser queryByUserId(Long userId, Long toUserId) { Criteria criteria = Criteria.where("toUserId").is(toUserId).and("userId").is(userId); Query query = Query.query(criteria); RecommendUser user = mongoTemplate.findOne(query, RecommendUser.class); if(user == null) { user = new RecommendUser(); user.setUserId(userId); user.setToUserId(toUserId); //构建缘分值 user.setScore(95d); } return user; }5.2、查看陌生人消息点击“聊一下”，可以查看对方的问题5.2.1、mock接口地址：http://192.168.136.160:3000/project/19/interface/api/1245.2.2、TanhuaController/** * 查看陌生人问题 */ @GetMapping("/strangerQuestions") public ResponseEntity strangerQuestions(Long userId) { String questions = tanhuaService.strangerQuestions(userId); return ResponseEntity.ok(questions); }5.2.3、TanhuaService//查看陌生人问题 public String strangerQuestions(Long userId) { Question question = questionApi.findByUserId(userId); return question == null ? "你喜欢java编程吗？" : question.getTxt(); }5.3、回复陌生人消息需求：通过服务器端，给目标用户发送一条陌生人消息5.3.1、mock接口地址：http://192.168.136.160:3000/project/19/interface/api/1065.3.2、TanhuaController/** * 回复陌生人问题 */ @PostMapping("/strangerQuestions") public ResponseEntity replyQuestions(@RequestBody Map map) { //前端传递的userId:是Integer类型的 String obj = map.get("userId").toString(); Long userId = Long.valueOf(obj); String reply = map.get("reply").toString(); tanhuaService.replyQuestions(userId,reply); return ResponseEntity.ok(null); }5.3.3、TanhuaService创建 TanhuaService并编写方法，完成回复陌生人消息功能{ "userId": 106, "huanXinId": "hx106", "nickname": "黑马小妹", "strangerQuestion": "你喜欢去看蔚蓝的大海还是去爬巍峨的高山？", "reply": "我喜欢秋天的落叶，夏天的泉水，冬天的雪地，只要有你一切皆可~" }//回复陌生人问题 public void replyQuestions(Long userId, String reply) { //1、构造消息数据 Long currentUserId = UserHolder.getUserId(); UserInfo userInfo = userInfoApi.findById(currentUserId); Map map = new HashMap(); map.put("userId",currentUserId); map.put("huanXinId", Constants.HX_USER_PREFIX+currentUserId); map.put("nickname",userInfo.getNickname()); map.put("strangerQuestion",strangerQuestions(userId)); map.put("reply",reply); String message = JSON.toJSONString(map); //2、调用template对象，发送消息 Boolean aBoolean = template.sendMsg(Constants.HX_USER_PREFIX + userId, message);//1、接受方的环信id，2、消息 if(!aBoolean) { throw new BusinessException(ErrorResult.error()); } }5.4、添加联系人用户获取陌生人消息后，点击“聊一下”，就会成为联系人（好友）。实现：将好友写入到MongoDB中将好友关系注册到环信5.4.1、mock接口地址： http://192.168.136.160:3000/project/19/interface/api/2055.4.2、定义MessagesController/** * 添加好友 */ @PostMapping("/contacts") public ResponseEntity contacts(@RequestBody Map map) { Long friendId = Long.valueOf(map.get("userId").toString()); messagesService.contacts(friendId); return ResponseEntity.ok(null); }5.4.3、编写Service方法MessageService补充添加联系人方法//添加好友关系 public void contacts(Long friendId) { //1、将好友关系注册到环信 Boolean aBoolean = huanXinTemplate.addContact(Constants.HX_USER_PREFIX + UserHolder.getUserId(), Constants.HX_USER_PREFIX + friendId); if(!aBoolean) { throw new BusinessException(ErrorResult.error()); } //2、如果注册成功，记录好友关系到mongodb friendApi.save(UserHolder.getUserId(),friendId); }5.4.4、dubbo服务创建 FriendApi和 FriendApiImpl并编写添加好友的方法FriendApiImpl实现类@Override public void save(Long userId, Long friendId) { //1、保存自己的好友数据 Query query1 = Query.query(Criteria.where("userId").is(userId).and("frinedId").is(friendId)); //1.1 判断好友关系是否存在 if(!mongoTemplate.exists(query1, Friend.class)) { //1.2 如果不存在，保存 Friend friend1 = new Friend(); friend1.setUserId(userId); friend1.setFriendId(friendId); friend1.setCreated(System.currentTimeMillis()); mongoTemplate.save(friend1); } //2、保存好友的数据 Query query2 = Query.query(Criteria.where("userId").is(friendId).and("frinedId").is(userId)); //2.1 判断好友关系是否存在 if(!mongoTemplate.exists(query2, Friend.class)) { //2.2 如果不存在，保存 Friend friend1 = new Friend(); friend1.setUserId(friendId); friend1.setFriendId(userId); friend1.setCreated(System.currentTimeMillis()); mongoTemplate.save(friend1); } }5.4.5、测试可以看到好友已经添加成功。5.5、联系人列表联系人列表：分页查询好友列表数据 （tanhua-users：好友关系表）5.5.1、mock接口地址：http://192.168.136.160:3000/project/19/interface/api/202响应数据结构：5.5.2、定义ContactVopackage com.tanhua.domain.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor public class ContactVo implements Serializable { private Long id; private String userId; private String avatar; private String nickname; private String gender; private Integer age; private String city; public static ContactVo init(UserInfo userInfo) { ContactVo vo = new ContactVo(); if(userInfo != null) { BeanUtils.copyProperties(userInfo,vo); vo.setUserId("hx"+userInfo.getId().toString()); } return vo; } }5.5.3、编写MessagesController在 MessagesController中添加查询联系人列表方法/** * 分页查询联系人列表 */ @GetMapping("/contacts") public ResponseEntity contacts(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize, String keyword) { PageResult pr = messagesService.findFriends(page,pagesize,keyword); return ResponseEntity.ok(pr); }5.5.4、编写Service在 MessageService中添加查询联系人方法//分页查询联系人列表 public PageResult findFriends(Integer page, Integer pagesize, String keyword) { //1、调用API查询当前用户的好友数据 -- List<Friend> List<Friend> list = friendApi.findByUserId(UserHolder.getUserId(),page,pagesize); if(CollUtil.isEmpty(list)) { return new PageResult(); } //2、提取数据列表中的好友id List<Long> userIds = CollUtil.getFieldValues(list, "friendId", Long.class); //3、调用UserInfoAPI查询好友的用户详情 UserInfo info = new UserInfo(); info.setNickname(keyword); Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, info); //4、构造VO对象 List<ContactVo> vos = new ArrayList<>(); for (Friend friend : list) { UserInfo userInfo = map.get(friend.getFriendId()); if(userInfo != null) { ContactVo vo = ContactVo.init(userInfo); vos.add(vo); } } return new PageResult(page,pagesize,0l,vos); }5.5.5、dubbo服务在 FriendApi和 FriendApiImpl中添加分页查询方法@Override public List<Friend> findByUserId(Long userId, Integer page, Integer pagesize) { Criteria criteria = Criteria.where("userId").is(userId); Query query = Query.query(criteria).skip((page - 1) * pagesize).limit(pagesize) .with(Sort.by(Sort.Order.desc("created"))); return mongoTemplate.find(query,Friend.class); }
课程说明圈子动态查询圈子实现评论圈子实现点赞、喜欢功能圈子实现评论1、动态查询我的动态：查询个人发布的动态列表（分页查询），和之前实现的好友动态，推荐动态实现逻辑是一致。1.1、查询好友动态查询好友动态与查询推荐动态显示的结构是一样的，只是其查询数据源不同1.1.1、接口文档API接口文档：http://192.168.136.160:3000/project/19/interface/api/1421.1.2、代码步骤Controller层接受请求参数Service数据封装调用API查询好友动态详情数据调用API查询动态发布人详情构造VO对象API层根据用户ID查询好友发布动态详情查询好友时间线表查询动态详情1.1.3、代码实现MovementController /** * 查询好友动态 */ @GetMapping public ResponseEntity movements(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize) { PageResult pr = movementService.findFriendMovements(page,pagesize); return ResponseEntity.ok(pr); }MovementService//好友动态 public PageResult findFriendMovements(Integer page, Integer pagesize) { //1、获取当前用户id Long userId = UserHolder.getUserId(); //2、查询数据列表 List<Movement> items = movementApi.findFriendMovements(userId,page,pagesize); //3、非空判断 if(CollUtil.isEmpty(items)) { return new PageResult(); } //4、获取好友用户id List<Long> userIds = CollUtil.getFieldValues(items, "userId", Long.class); //5、循环数据列表 Map<Long, UserInfo> userMaps = userInfoApi.findByIds(userIds, null); List<MovementsVo> vos = new ArrayList<>(); for (Movement item : items) { //5、一个Movement构建一个Vo对象 UserInfo userInfo = userMaps.get(item.getUserId()); MovementsVo vo = MovementsVo.init(userInfo, item); vos.add(vo); } //6、构建返回值 return new PageResult(page,pagesize,0L,vos); }movementApi@Override public List<Movement> findFriendMovements(Long friendId, Integer page, Integer pagesize) { //1、查询好友时间线表 Query query = Query.query(Criteria.where("friendId").in(friendId)) .skip((page - 1)*pagesize).limit(pagesize) .with(Sort.by(Sort.Order.desc("created"))); List<MovementTimeLine> lines = mongoTemplate.find(query, MovementTimeLine.class); //2、提取动态id集合 List<ObjectId> movementIds = CollUtil.getFieldValues(lines, "movementId", ObjectId.class); //3、根据动态id查询动态详情 Query movementQuery = Query.query(Criteria.where("id").in(movementIds)); return mongoTemplate.find(movementQuery, Movement.class); }1.2、查询推荐动态推荐动态是通过推荐系统计算出的结果，现在我们只需要实现查询即可，推荐系统在后面的课程中完成。推荐系统计算完成后，会将结果数据写入到Redis中，数据如下：192.168.31.81:6379> get MOVEMENTS_RECOMMEND_1 "2562,3639,2063,3448,2128,2597,2893,2333,3330,2642,2541,3002,3561,3649,2384,2504,3397,2843,2341,2249"可以看到，在Redis中的数据是有多个发布id组成（pid）由逗号分隔。所以实现中需要自己对这些数据做分页处理。1.2.1、接口文档API接口文档：http://192.168.136.160:3000/project/19/interface/api/1451.2.2、代码步骤Controller层接受请求参数Service数据封装从redis获取当前用户的推荐PID列表如果不存在，调用API随机获取10条动态数据如果存在，调用API根据PID列表查询动态数据构造VO对象API层编写方法随机获取根据pid列表查询1.2.3、代码实现Constantspackage com.tanhua.commons.utils; //常量定义 public class Constants { //手机APP短信验证码CHECK_CODE_ public static final String SMS_CODE = "CHECK_CODE_"; //推荐动态 public static final String MOVEMENTS_RECOMMEND = "MOVEMENTS_RECOMMEND_"; //推荐视频 public static final String VIDEOS_RECOMMEND = "VIDEOS_RECOMMEND_"; //圈子互动KEY public static final String MOVEMENTS_INTERACT_KEY = "MOVEMENTS_INTERACT_"; //动态点赞用户HashKey public static final String MOVEMENT_LIKE_HASHKEY = "MOVEMENT_LIKE_"; //动态喜欢用户HashKey public static final String MOVEMENT_LOVE_HASHKEY = "MOVEMENT_LOVE_"; //视频点赞用户HashKey public static final String VIDEO_LIKE_HASHKEY = "VIDEO_LIKE"; //访问用户 public static final String VISITORS = "VISITORS"; //关注用户 public static final String FOCUS_USER = "FOCUS_USER_{}_{}"; //初始化密码 public static final String INIT_PASSWORD = "123456"; //环信用户前缀 public static final String HX_USER_PREFIX = "hx"; //jwt加密盐 public static final String JWT_SECRET = "itcast"; //jwt超时时间 public static final int JWT_TIME_OUT = 3_600; } MovementController/** * 推荐动态 */ @GetMapping("/recommend") public ResponseEntity recommend(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize) { PageResult pr = movementService.findRecommendMovements(page,pagesize); return ResponseEntity.ok(pr); }MovementService//推荐动态 public PageResult findRecommendMovements(Integer page, Integer pagesize) { String redisKey = "MOVEMENTS_RECOMMEND_" + UserHolder.getUserId(); String redisData = this.redisTemplate.opsForValue().get(redisKey); List<Movement> list = Collections.EMPTY_LIST; if(StringUtils.isEmpty(redisData)){ list = movementApi.randomMovements(pagesize); }else { String[] split = redisData.split(","); if ((page-1) * pagesize > split.length) { return new PageResult(); } List<Long> pids = Arrays.stream(split) .skip((page - 1) * pagesize) .limit(pagesize) .map(e -> Convert.toLong(e)) .collect(Collectors.toList()); list = movementApi.findByPids(pids); } List<Long> userIds = CollUtil.getFieldValues(list, "userId", Long.class); //5、循环数据列表 Map<Long, UserInfo> userMaps = userInfoApi.findByIds(userIds, null); List<MovementsVo> vos = new ArrayList<>(); for (Movement item : list) { //5、一个Movement构建一个Vo对象 UserInfo userInfo = userMaps.get(item.getUserId()); MovementsVo vo = MovementsVo.init(userInfo, item); vos.add(vo); } //6、构建返回值 return new PageResult(page,pagesize,0L,vos); }movementApi//随机获取 public List<Movement> randomMovements(Integer counts) { TypedAggregation aggregation = Aggregation.newAggregation(Movement.class, Aggregation.sample(counts)); AggregationResults<Movement> movements = mongoTemplate.aggregate(aggregation,Movement.class); return movements.getMappedResults(); } //根据PID查询 public List<Movement> findByPids(List<Long> pids) { Query query = Query.query(Criteria.where("pId").in(pids)); return mongoTemplate.find(query, Movement.class); }1.3、根据id查询动态根据id查询动态：当手机端查看评论内容时（需要根据动态id，查询动态详情），后续再去查询评论列表1.3.1、接口文档API接口文档：http://192.168.136.160:3000/project/19/interface/api/1511.3.2、代码实现MovementController /** * 根据id查询动态 */ @GetMapping("/{id}") public ResponseEntity findById(@PathVariable("id") String movementId) { MovementsVo vo = movementService.findMovementById(movementId); return ResponseEntity.ok(vo); } MovementService public MovementsVo findMovementById(String movementId) { Movement movements = movementApi.findById(movementId); if(movements == null) { return null; }else { UserInfo userInfo = userInfoApi.findById(movements.getUserId()); return MovementsVo.init(userInfo,movements); } }movementApi@Override public Movement findById(String movementId) { return mongoTemplate.findById(movementId,Movement.class); }2、圈子互动点赞、喜欢、评论等均可理解为用户对动态的互动。数据库表：quanzi_comment将数据记录到表中：保存到MongoDB中 互动表需要几张：需要一张表即可（quanzi_comment） 里面的数据需要分类：通过字段commentType 1-点赞，2-评论，3-喜欢 { "_id" : ObjectId("5fe7f9263c851428107cd4e8"), "publishId" : ObjectId("5fae53947e52992e78a3afa5"), "commentType" : 1, "userId" : NumberLong(1), "publishUserId" : NumberLong(1), "created" : NumberLong(1609038118275), "likeCount" : 0, "_class" : "com.tanhua.domain.mongo.Comment" }数据存储位置：redis，mongodbmongodb中的数据在动态详情Movement表中，加入喜欢，点赞，评论数量：检查数据库访问压力互动操作的时候，不要忘记对上面的字段进行维护圈子互动的表 comment互动完成（点赞，喜欢）：不仅要将数据保存到mongo中，需要记录到redis中页面查询圈子列表时，可以从redis中判断是否有点赞，和喜欢历史2.1、环境搭建2.1.1 创建API接口public interface CommentApi { }2.1.2 创建API实现类@DubboService public class CommentApiImpl implements CommentApi { @Autowired private MongoTemplate mongoTemplate; }2.1.3 Movement对象@Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "movement") public class Movement implements java.io.Serializable { private ObjectId id; //主键id //redis实现，使用Mongodb实现 private Long pid; //Long类型，用于推荐系统的模型(自动增长) private Long created; //发布时间 private Long userId; private String textContent; //文字 private List<String> medias; //媒体数据，图片或小视频 url private String longitude; //经度 private String latitude; //纬度 private String locationName; //位置名称 private Integer state = 0;//状态 0：未审（默认），1：通过，2：驳回 //补充字段 private Integer likeCount = 0; //点赞数 private Integer commentCount = 0; //评论数 private Integer loveCount = 0; //喜欢数 //根据评论类型，获取对应的互动数量 public Integer statisCount(Integer commentType) { if (commentType == CommentType.LIKE.getType()) { return this.likeCount; } else if (commentType == CommentType.COMMENT.getType()) { return this.commentCount; } else { return loveCount; } } }2.1.4 实体类对象package com.tanhua.domain.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; /** * 圈子互动表（点赞，评论，喜欢） */ @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "comment") public class Comment implements java.io.Serializable{ private ObjectId id; private ObjectId publishId; //发布id private Integer commentType; //评论类型，1-点赞，2-评论，3-喜欢 private String content; //评论内容 private Long userId; //评论人 private Long publishUserId; //被评论人ID private Long created; //发表时间 private Integer likeCount = 0; //当前评论的点赞数 }2.1.5 VO对象@Data @NoArgsConstructor @AllArgsConstructor public class CommentVo implements Serializable { private String id; //评论id private String avatar; //头像 private String nickname; //昵称 private String content; //评论 private String createDate; //评论时间 private Integer likeCount; //点赞数 private Integer hasLiked; //是否点赞（1是，0否） public static CommentVo init(UserInfo userInfo, Comment item) { CommentVo vo = new CommentVo(); BeanUtils.copyProperties(userInfo, vo); BeanUtils.copyProperties(item, vo); vo.setHasLiked(0); Date date = new Date(item.getCreated()); vo.setCreateDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)); vo.setId(item.getId().toHexString()); return vo; } }2.1.6 CommentType枚举/** * 评论类型：1-点赞，2-评论，3-喜欢 */ public enum CommentType { LIKE(1), COMMENT(2), LOVE(3); int type; CommentType(int type) { this.type = type; } public int getType() { return type; } }2.2、动态评论功能包括：查询评论列表，发布评论，对评论点赞和取消点赞。2.2.1 分页列表查询CommentController//分页查询评理列表 @GetMapping public ResponseEntity findComments(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize, String movementId) { PageResult pr = commentsService.findComments(movementId,page,pagesize); return ResponseEntity.ok(pr); }CommentService//分页查询评理列表 public PageResult findComments(String movementId, Integer page, Integer pagesize) { //1、调用API查询评论列表 List<Comment> list = commentApi.findComments(movementId,CommentType.COMMENT,page,pagesize); //2、判断list集合是否存在 if(CollUtil.isEmpty(list)) { return new PageResult(); } //3、提取所有的用户id,调用UserInfoAPI查询用户详情 List<Long> userIds = CollUtil.getFieldValues(list, "userId", Long.class); Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null); //4、构造vo对象 List<CommentVo> vos = new ArrayList<>(); for (Comment comment : list) { UserInfo userInfo = map.get(comment.getUserId()); if(userInfo != null) { CommentVo vo = CommentVo.init(userInfo, comment); vos.add(vo); } } //5、构造返回值 return new PageResult(page,pagesize,0l,vos); }CommentAPI//分页查询 public List<Comment> findComments(String movementId, CommentType commentType, Integer page, Integer pagesize) { //1、构造查询条件 Query query = Query.query(Criteria.where("publishId").is(new ObjectId(movementId)).and("commentType") .is(commentType.getType())) .skip((page -1) * pagesize) .limit(pagesize) .with(Sort.by(Sort.Order.desc("created"))); //2、查询并返回 return mongoTemplate.find(query,Comment.class); }2.2.2 发布评论CommentController@RestController @RequestMapping("/comments") public class CommentsController { @Autowired private CommentsService commentsService; /** * 发布评论 */ @PostMapping public ResponseEntity saveComments(@RequestBody Map map) { String movementId = (String )map.get("movementId"); String comment = (String)map.get("comment"); commentsService.saveComments(movementId,comment); return ResponseEntity.ok(null); } }CommentService@Service @Slf4j public class CommentsService { @DubboReference private CommentApi commentApi; //发布评论 public void saveComments(String movementId, String comment) { //1、获取操作用户id Long userId = UserHolder.getUserId(); //2、构造Comment Comment comment1 = new Comment(); comment1.setPublishId(new ObjectId(movementId)); comment1.setCommentType(CommentType.COMMENT.getType()); comment1.setContent(comment); comment1.setUserId(userId); comment1.setCreated(System.currentTimeMillis()); //3、调用API保存评论 Integer commentCount = commentApi.save(comment1); log.info("commentCount = " + commentCount); } }CommentAPI//发布评论，并获取评论数量 public Integer save(Comment comment) { //1、查询动态 Movement movement = mongoTemplate.findById(comment.getPublishId(), Movement.class); //2、向comment对象设置被评论人属性 if(movement != null) { comment.setPublishUserId(movement.getUserId()); } //3、保存到数据库 mongoTemplate.save(comment); //4、更新动态表中的对应字段 Query query = Query.query(Criteria.where("id").is(comment.getPublishId())); Update update = new Update(); if(comment.getCommentType() == CommentType.LIKE.getType()) { update.inc("likeCount",1); }else if (comment.getCommentType() == CommentType.COMMENT.getType()){ update.inc("commentCount",1); }else { update.inc("loveCount",1); } //设置更新参数 FindAndModifyOptions options = new FindAndModifyOptions(); options.returnNew(true) ;//获取更新后的最新数据 Movement modify = mongoTemplate.findAndModify(query, update, options, Movement.class); //5、获取最新的评论数量，并返回 return modify.statisCount(comment.getCommentType() ); }测试API代码@RunWith(SpringRunner.class) @SpringBootTest(classes = AppServerApplication.class) public class CommentApiTest { @DubboReference private CommentApi commentApi; @Test public void testSave() { Comment comment = new Comment(); comment.setCommentType(CommentType.COMMENT.getType()); comment.setUserId(106l); comment.setCreated(System.currentTimeMillis()); comment.setContent("测试评论"); comment.setPublishId(); commentApi.save(comment); } }2.3、点赞2.3.1、编写Controller修改MovementsController代码，添加点赞与取消点赞方法/** * 点赞 */ @GetMapping("/{id}/like") public ResponseEntity like(@PathVariable("id") String movementId) { Integer likeCount = commentsService.likeComment(movementId); return ResponseEntity.ok(likeCount); } /** * 取消点赞 */ @GetMapping("/{id}/dislike") public ResponseEntity dislike(@PathVariable("id") String movementId) { Integer likeCount = commentsService.dislikeComment(movementId); return ResponseEntity.ok(likeCount); }2.3.2、编写Service创建CommentService，添加点赞与取消点赞方法//动态点赞 public Integer likeComment(String movementId) { //1、调用API查询用户是否已点赞 Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LIKE); //2、如果已经点赞，抛出异常 if(hasComment) { throw new BusinessException(ErrorResult.likeError()); } //3、调用API保存数据到Mongodb Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LIKE.getType()); comment.setUserId(UserHolder.getUserId()); comment.setCreated(System.currentTimeMillis()); Integer count = commentApi.save(comment); //4、拼接redis的key，将用户的点赞状态存入redis String key = Constants.MOVEMENTS_INTERACT_KEY + movementId; String hashKey = Constants.MOVEMENT_LIKE_HASHKEY + UserHolder.getUserId(); redisTemplate.opsForHash().put(key,hashKey,"1"); return count; } //取消点赞 public Integer dislikeComment(String movementId) { //1、调用API查询用户是否已点赞 Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LIKE); //2、如果未点赞，抛出异常 if(!hasComment) { throw new BusinessException(ErrorResult.disLikeError()); } //3、调用API，删除数据，返回点赞数量 Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LIKE.getType()); comment.setUserId(UserHolder.getUserId()); Integer count = commentApi.delete(comment); //4、拼接redis的key，删除点赞状态 String key = Constants.MOVEMENTS_INTERACT_KEY + movementId; String hashKey = Constants.MOVEMENT_LIKE_HASHKEY + UserHolder.getUserId(); redisTemplate.opsForHash().delete(key,hashKey); return count; }2.3.3、修改API服务判断Comment数据是否存在//判断comment数据是否存在 public Boolean hasComment(String movementId, Long userId, CommentType commentType) { Criteria criteria = Criteria.where("userId").is(userId) .and("publishId").is(new ObjectId(movementId)) .and("commentType").is(commentType.getType()); Query query = Query.query(criteria); return mongoTemplate.exists(query,Comment.class); //判断数据是否存在 }删除互动数据//删除 public Integer delete(Comment comment) { //1、删除Comment表数据 Criteria criteria = Criteria.where("userId").is(comment.getUserId()) .and("publishId").is(comment.getPublishId()) .and("commentType").is(comment.getCommentType()); Query query = Query.query(criteria); mongoTemplate.remove(query,Comment.class); //2、修改动态表中的总数量 Query movementQuery = Query.query(Criteria.where("id").is(comment.getPublishId())); Update update = new Update(); if(comment.getCommentType() == CommentType.LIKE.getType()) { update.inc("likeCount",-1); }else if (comment.getCommentType() == CommentType.COMMENT.getType()){ update.inc("commentCount",-1); }else { update.inc("loveCount",-1); } //设置更新参数 FindAndModifyOptions options = new FindAndModifyOptions(); options.returnNew(true) ;//获取更新后的最新数据 Movement modify = mongoTemplate.findAndModify(movementQuery, update, options, Movement.class); //5、获取最新的评论数量，并返回 return modify.statisCount(comment.getCommentType() ); }2.3.4、修改查询动态点赞数修改之前的查询圈子列表代码，从redis查询是否具有操作记录2.4、喜欢喜欢和取消喜欢：和刚才的点赞与取消点赞基本上市一模一样的！操作的类型comment_type=3,操作的字段loveCountMovementsController修改MovementsController代码，添加喜欢与取消喜欢方法/** * 喜欢 */ @GetMapping("/{id}/love") public ResponseEntity love(@PathVariable("id") String movementId) { Integer likeCount = commentsService.loveComment(movementId); return ResponseEntity.ok(likeCount); } /** * 取消喜欢 */ @GetMapping("/{id}/unlove") public ResponseEntity unlove(@PathVariable("id") String movementId) { Integer likeCount = commentsService.disloveComment(movementId); return ResponseEntity.ok(likeCount); }CommentService修改CommentService，添加点赞与取消点赞方法//喜欢 public Integer loveComment(String movementId) { //1、调用API查询用户是否已点赞 Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LOVE); //2、如果已经喜欢，抛出异常 if(hasComment) { throw new BusinessException(ErrorResult.loveError()); } //3、调用API保存数据到Mongodb Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LOVE.getType()); comment.setUserId(UserHolder.getUserId()); comment.setCreated(System.currentTimeMillis()); Integer count = commentApi.save(comment); //4、拼接redis的key，将用户的点赞状态存入redis String key = Constants.MOVEMENTS_INTERACT_KEY + movementId; String hashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId(); redisTemplate.opsForHash().put(key,hashKey,"1"); return count; } //取消喜欢 public Integer disloveComment(String movementId) { //1、调用API查询用户是否已点赞 Boolean hasComment = commentApi.hasComment(movementId,UserHolder.getUserId(),CommentType.LOVE); //2、如果未点赞，抛出异常 if(!hasComment) { throw new BusinessException(ErrorResult.disloveError()); } //3、调用API，删除数据，返回点赞数量 Comment comment = new Comment(); comment.setPublishId(new ObjectId(movementId)); comment.setCommentType(CommentType.LOVE.getType()); comment.setUserId(UserHolder.getUserId()); Integer count = commentApi.delete(comment); //4、拼接redis的key，删除点赞状态 String key = Constants.MOVEMENTS_INTERACT_KEY + movementId; String hashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId(); redisTemplate.opsForHash().delete(key,hashKey); return count; }2.5 修复前端点赞和喜欢显示效果Bug显示效果和MovementsVo中的hasLiked与hasLoved绑定了，所以我们在分页查询动态数据构造vo对象时，需要判断是否点赞和喜欢了，对应修改hasLiked与hasLoved即可。private PageResult getPageResult(Integer page, Integer pagesize, List<Movement> movementList) { if (movementList == null || movementList.isEmpty()) { return new PageResult(); } // 4. 提取动态发布人的id列表 List<Long> userIds = CollUtil.getFieldValues(movementList, "userId", Long.class); // 5. 根据用户的id列表获取用户信息 Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null); // 6. 一个Movement构造一个vo对象 List<MovementsVo> vos = new ArrayList<>(); for (Movement movement : movementList) { UserInfo userInfo = map.get(movement.getUserId()); if (userInfo != null) { MovementsVo vo = MovementsVo.init(userInfo, movement); // 修复点赞和喜欢的bug String key = Constants.MOVEMENTS_INTERACT_KEY + movement.getId().toHexString(); String likeHashKey = Constants.MOVEMENT_LIKE_HASHKEY + UserHolder.getUserId(); if (redisTemplate.opsForHash().hasKey(key, likeHashKey)) { vo.setHasLiked(1); } String loveHashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId(); if (redisTemplate.opsForHash().hasKey(key, loveHashKey)) { vo.setHasLoved(1); } vos.add(vo); } } // 7. 构造PageResult并返回 return new PageResult(page, pagesize, 0, vos); }
1、首页推荐1.1、接口分析地址：http://192.168.136.160:3000/project/19/interface/api/118响应：{ "counts": 4698, "pagesize": 20, "pages": 58, "page": 16, "items": [ { "id": 1011, "avatar": "assets/images/avatar_2.png", "nickname": "黑马小妹", "gender": "woman", "age": 23, "tags": [ "本科", "年龄相仿", "单身" ], "fateValue": 96 }, { "id": 2495, "avatar": "assets/images/avatar_1.png", "nickname": "米朵妹妹", "gender": "man", "age": 28, "tags": [ "年龄相仿", "本科", "单身" ], "fateValue": 87 }, { "id": 5708, "avatar": "assets/images/avatar_4.png", "nickname": "黑马小妹", "gender": "man", "age": 24, "tags": [ "单身", "本科", "年龄相仿" ], "fateValue": 94 }, { "id": 4768, "avatar": "assets/images/avatar_3.png", "nickname": "黑马小妹", "gender": "man", "age": 24, "tags": [ "年龄相仿", "单身", "本科" ], "fateValue": 80 } ] }1.2、功能实现1.2.1 controllerTanhuaController编写推荐列表方法/** * 查询分页推荐好友列表 */ @GetMapping("/recommendation") public ResponseEntity recommendation(RecommendUserDto dto) { PageResult pr = tanhuaService.recommendation(dto); return ResponseEntity.ok(pr); }1.2.2 serviceTanhuaService编写推荐列表方法//查询分页推荐好友列表 public PageResult recommendation(RecommendUserDto dto) { //1、获取用户id Long userId = UserHolder.getUserId(); //2、调用recommendUserApi分页查询数据列表（PageResult -- RecommendUser） PageResult pr = recommendUserApi.queryRecommendUserList(dto.getPage(),dto.getPagesize(),userId); //3、获取分页中的RecommendUser数据列表 List<RecommendUser> items = (List<RecommendUser>) pr.getItems(); //4、判断列表是否为空 if(items == null || items.size() <=0) { return pr; } //5、提取所有推荐的用户id列表 List<Long> ids = CollUtil.getFieldValues(items, "userId", Long.class); UserInfo userInfo = new UserInfo(); userInfo.setAge(dto.getAge()); userInfo.setGender(dto.getGender()); //6、构建查询条件，批量查询所有的用户详情 Map<Long, UserInfo> map = userInfoApi.findByIds(ids, userInfo); //7、循环推荐的数据列表，构建vo对象 List<TodayBest> list = new ArrayList<>(); for (RecommendUser item : items) { UserInfo info = map.get(item.getUserId()); if(info!=null) { TodayBest vo = TodayBest.init(info, item); list.add(vo); } } //8、构造返回值 pr.setItems(list); return pr; }1.2.3 API接口在RecommendUserApi 接口和RecommendUserApiImpl实现类中添加方法查询//分页查询 public PageResult queryRecommendUserList(Integer page, Integer pagesize, Long toUserId) { //1、构建Criteria对象 Criteria criteria = Criteria.where("toUserId").is(toUserId); //2、创建Query对象 Query query = Query.query(criteria).with(Sort.by(Sort.Order.desc("score"))).limit(pagesize) .skip((page - 1) * pagesize); //3、调用mongoTemplate查询 List<RecommendUser> list = mongoTemplate.find(query, RecommendUser.class); long count = mongoTemplate.count(query, RecommendUser.class); //4、构建返回值PageResult return new PageResult(page,pagesize,count,list); }在UserInfoApi接口和UserInfoApiImpl实现类中添加优化查询方法在tanhuaServer中优化SQL查询public Map<Long, UserInfo> findByIds(List<Long> userIds, UserInfo info) { LambdaQueryWrapper<UserInfo> lqw = new LambdaQueryWrapper<>(); // 1、添加用户列表 lqw.in(UserInfo::getId, userIds); // 2、添加筛选条件 if (info != null) { lqw.lt(info.getAge() != null, UserInfo::getAge, info.getAge()); lqw.eq(!StringUtils.isEmpty(info.getGender()), UserInfo::getGender, info.getGender()); } List<UserInfo> userInfos = userInfoMapper.selectList(lqw); // 将userInfos转化为map Map<Long, UserInfo> map = CollUtil.fieldValueMap(userInfos, "id"); return map; }1.2.4 请求dto对象import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class RecommendUserDto { private Integer page = 1; //当前页数 private Integer pagesize = 10; //页尺寸 private String gender; //性别 man woman private String lastLogin; //近期登陆时间 private Integer age; //年龄 private String city; //居住地 private String education; //学历 }2、MongoDB集群2.1 问题分析单机Mongodb并不适用于企业场景，存在两个问题亟需解决单点故障：单一MongoDB提供服务，在服务器宕机时造成整体应用崩溃海量数据存储：单一MongoDB，并不能支持海量数据存储2.2 集群概述为了解决单点故障和海量数据存储问题，MongoDB提供了三种集群形式来支持Master-Slaver（主从集群）： 是一种主从副本的模式，目前已经不推荐使用Replica Set （副本集群）：模式取代了 Master-Slaver 模式，是一种互为主从的关系。可以解决单点故障问题Sharding （分片集群）：可以解决单点故障和海量数据存储问题[scode type="red" size=""]企业中一般由专业人员部署构建集群，故MongoDB集群不需要自行搭建，学习集群需要理解各个集群的特点以及适用场景即可[/scode][post cid="1683" cover="" size=""/][post cid="1685" cover="" size=""/]3、圈子功能3.1、功能说明探花交友项目中的圈子功能，类似微信的朋友圈，基本的功能为：发布动态、浏览好友动态、浏览推荐动态、点赞、评论、喜欢等功能。发布：3.2、实现方案分析对于圈子功能的实现，我们需要对它的功能特点做分析：数据量会随着用户数增大而增大读多写少非好友看不到其动态内容……针对以上特点，我们来分析一下：对于数据量大而言，显然不能够使用关系型数据库进行存储，我们需要通过MongoDB进行存储对于读多写少的应用，需要减少读取的成本比如说，一条SQL语句，单张表查询一定比多张表查询要快对于每个人数据在存储层面最好做到相互隔离，这样的话就不会有影响所以对于存储而言，主要是核心的4张表：发布表：记录了所有用户的发布的东西信息，如图片、视频等。自己时间线：相册是每个用户独立的，记录了该用户所发布的所有内容。好友时间线：所谓“刷朋友圈”，就是刷时间线，就是一个用户所有的朋友的发布内容。好友表：记录好友关系3.3、技术方案(重点)根据之前我们的分析，对于技术方案而言，将采用MongoDB+Redis来实现，其中MongoDB负责存储，Redis负责缓存数据。3.4、表结构设计发布表：动态总记录表（记录每个人发送的动态详情）#表名：movement { "_id": ObjectId("5e82dc416401952928c211d8"), "pid": NumberLong("10064"), "userId": NumberLong("6"), "textContent": "最悲伤却又是最痛苦的谎言，就是我还好，没有关系。", "medias": [ "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/7/1.jpg", "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/7/1564567349498.jpg", "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/7/1564567352977.jpg", "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/7/1564567360406.jpg" ], "longitude": "121.588627", "latitude": "30.935781", "state": NumberInt("0"), "locationName": "中国上海市奉贤区人民路445弄", "created": NumberLong("1585634369493"), "_class": "com.tanhua.dubbo.server.pojo.Publish" }好友时间线表：记录当前好友发布的动态数据#表名：movement_timeline { "_id": ObjectId("609cf6538743d448c02c61f0"), "movementId": ObjectId("609cf6538743d448c02c61ef"), "userId": NumberLong("106"), "friendId": NumberLong("1"), "created": NumberLong("1620899411043"), "_class": "com.tanhua.model.mongo.MovementTimeLine" }好友关系表：记录好友的双向关系（双向）#表名：friend { "_id": ObjectId("6018bc055098b2230031e2da"), "created": NumberLong("1612233733056"), "userId": NumberLong("1"), "friendId": NumberLong("106"), "_class": "com.itheima.domain.mongo.Friend" }4、圈子实现4.1、环境搭建Mongodb中实现字段的自增：两种解决方法（1、使用redis保证自动增长，2、使用mongodb自定义表）4.1.1、mongo主键自增第一步：创建实体类package com.tanhua.domain.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; @Document(collection = "sequence") @Data @AllArgsConstructor @NoArgsConstructor public class Sequence { private ObjectId id; private long seqId; //自增序列 private String collName; //集合名称 }第二步：编写servicepackage com.tanhua.dubbo.utils; import com.tanhua.domain.mongo.Sequence; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.FindAndModifyOptions; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Component; @Component public class IdWorker { @Autowired private MongoTemplate mongoTemplate; public Long getNextId(String collName) { Query query = new Query(Criteria.where("collName").is(collName)); Update update = new Update(); update.inc("seqId", 1); FindAndModifyOptions options = new FindAndModifyOptions(); options.upsert(true); options.returnNew(true); Sequence sequence = mongoTemplate.findAndModify(query, update, options, Sequence.class); return sequence.getSeqId(); } }4.1.2、实体类写到tanhua-domain工程中：MovementMovement：发布信息表（总记录表数据）package com.tanhua.domain.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; import java.util.List; //动态详情表 @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "movement") public class Movement implements java.io.Serializable { private ObjectId id; //主键id private Long pid; //Long类型，用于推荐系统的模型(自动增长) private Long created; //发布时间 private Long userId; private String textContent; //文字 private List<String> medias; //媒体数据，图片或小视频 url private String longitude; //经度 private String latitude; //纬度 private String locationName; //位置名称 private Integer state = 0;//状态 0：未审（默认），1：通过，2：驳回 }MovementTimeLinemovement_timeline：好友时间线表，用于存储好友发布（或推荐）的数据，每一个用户一张表进行存储package com.tanhua.domain.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; /** * 好友时间线表，用于存储好友发布的数据 */ @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "movement_timeline") public class MovementTimeLine implements java.io.Serializable { private static final long serialVersionUID = 9096178416317502524L; private ObjectId id; private ObjectId movementId;//动态id private Long userId; //发布动态用户id private Long friendId; // 可见好友id private Long created; //发布的时间 }FriendFriend 好友关系表package com.tanhua.domain.mongo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; /** * 好友表:好友关系表 */ @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "friend") public class Friend implements java.io.Serializable{ private static final long serialVersionUID = 6003135946820874230L; private ObjectId id; private Long userId; //用户id private Long friendId; //好友id private Long created; //时间 }4.1.3、API接口package com.tanhua.dubbo.api.mongo; import com.tanhua.domain.mongo.Publish; import com.tanhua.domain.vo.PageResult; public interface MovementApi { }4.1.4、API实现类package com.tanhua.dubbo.api.mongo; import com.tanhua.domain.mongo.*; import com.tanhua.domain.vo.PageResult; import com.tanhua.dubbo.utils.IdService; import org.apache.dubbo.config.annotation.Service; import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import java.util.ArrayList; import java.util.List; @Service public class MovementApiImpl implements PublishApi { }4.1.5、MovementsControllerpackage com.tanhua.server.controller; import com.tanhua.domain.mongo.Publish; import com.tanhua.server.service.MovementsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @RestController @RequestMapping("/movements") public class MovementsController { @Autowired private MovementsService movementsService; }4.1.6、MovementsServicepackage com.tanhua.server.service; import com.tanhua.autoconfig.templates.OssTemplate; import com.tanhua.domain.db.UserInfo; import com.tanhua.domain.mongo.Publish; import com.tanhua.domain.vo.Movements; import com.tanhua.domain.vo.PageResult; import com.tanhua.dubbo.api.UserInfoApi; import com.tanhua.dubbo.api.mongo.PublishApi; import com.tanhua.server.interceptor.UserHolder; import io.jsonwebtoken.lang.Collections; import org.apache.dubbo.config.annotation.Reference; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.*; @Service public class MovementsService { @Autowired private OssTemplate ossTemplate; @DubboReference private MovementApi movementApi; @DubboReference private UserInfoApi userInfoApi; @Autowired private RedisTemplate<String,String> redisTemplate; }4.2、发布动态后续的测试：使用106（13800138000）和1号用户（13500000000）4.2.0、思路步骤4.2.1、MovementsControllertanhua-server工程编写MovementsController，完成发布动态功能@RestController @RequestMapping("/movements") public class MovementController { @Autowired private MovementService movementService; @Autowired private CommentsService commentsService; /** * 发布动态 */ @PostMapping public ResponseEntity movements(Movement movement, MultipartFile imageContent[]) throws IOException { movementService.publishMovement(movement,imageContent); return ResponseEntity.ok(null); } }4.2.2、编写servicetanhua-server工程编写MovementsService，完成发布动态功能/** * 发布动态 */ 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()); medias.add(upload); } //4、将数据封装到Movement对象 movement.setUserId(userId); movement.setMedias(medias); //5、调用API完成发布动态 movementApi.publish(movement); }4.2.3、API层package com.tanhua.dubbo.api; import com.tanhua.dubbo.utils.IdWorker; import com.tanhua.dubbo.utils.TimeLineService; import com.tanhua.model.mongo.Movement; import com.tanhua.model.vo.PageResult; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import java.util.List; @DubboService public class MovementApiImpl implements MovementApi{ @Autowired private MongoTemplate mongoTemplate; @Autowired private IdWorker idWorker; @Autowired private TimeLineService timeLineService; //发布动态 public void publish(Movement movement) { //1、保存动态详情 try { //设置PID movement.setPid(idWorker.getNextId("movement")); //设置时间 movement.setCreated(System.currentTimeMillis()); //movement.setId(ObjectId.get()); mongoTemplate.save(movement); // //2、查询当前用户的好友数据 // Criteria criteria = Criteria.where("userId").is(movement.getUserId()); // Query query = Query.query(criteria); // List<Friend> friends = mongoTemplate.find(query, Friend.class); // Thread.sleep(10000); // //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(); } } }4.2.4、异步处理工具类@Component public class TimeLineService { @Autowired private MongoTemplate mongoTemplate; @Async public void saveTimeLine(Long userId, ObjectId movementId) { //2、查询当前用户的好友数据 Criteria criteria = Criteria.where("userId").is(userId); Query query = Query.query(criteria); List<Friend> friends = mongoTemplate.find(query, Friend.class); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } //3、循环好友数据，构建时间线数据存入数据库 for (Friend friend : friends) { MovementTimeLine timeLine = new MovementTimeLine(); timeLine.setMovementId(movementId); timeLine.setUserId(friend.getUserId()); timeLine.setFriendId(friend.getFriendId()); timeLine.setCreated(System.currentTimeMillis()); mongoTemplate.save(timeLine); } } }4.2.5、整合测试@RunWith(SpringRunner.class) @SpringBootTest(classes = AppServerApplication.class) public class MovementApiTest { @DubboReference private MovementApi movementApi; @Test public void testPublish() { Movement movement = new Movement(); movement.setUserId(106l); movement.setTextContent("你的酒窝没有酒，我却醉的像条狗"); List<String> list = new ArrayList<>(); list.add("https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/tanhua/avatar_1.png"); list.add("https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/tanhua/avatar_2.png"); movement.setMedias(list); movement.setLatitude("40.066355"); movement.setLongitude("116.350426"); movement.setLocationName("中国北京市昌平区建材城西路16号"); movementApi.publish(movement); } }4.3、查询个人动态查询好友动态其实就是查询自己的时间线表，好友在发动态时已经将动态信息写入到了自己的时间线表中。4.3.0、思路分析4.3.1、vo对象package com.tanhua.model.vo; import com.tanhua.model.domain.UserInfo; import com.tanhua.model.mongo.Movement; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.util.StringUtils; import java.io.Serializable; import java.text.SimpleDateFormat; import java.util.Date; @Data @NoArgsConstructor @AllArgsConstructor public class MovementsVo implements Serializable { private String id; //动态id private Long userId; //用户id private String avatar; //头像 private String nickname; //昵称 private String gender; //性别 man woman private Integer age; //年龄 private String[] tags; //标签 private String textContent; //文字动态 private String[] imageContent; //图片动态 private String distance; //距离 private String createDate; //发布时间 如: 10分钟前 private Integer likeCount; //点赞数 private Integer commentCount; //评论数 private Integer loveCount; //喜欢数 private Integer hasLiked; //是否点赞（1是，0否） private Integer hasLoved; //是否喜欢（1是，0否） public static MovementsVo init(UserInfo userInfo, Movement item) { MovementsVo vo = new MovementsVo(); //设置动态数据 BeanUtils.copyProperties(item, vo); vo.setId(item.getId().toHexString()); //设置用户数据 BeanUtils.copyProperties(userInfo, vo); if(!StringUtils.isEmpty(userInfo.getTags())) { vo.setTags(userInfo.getTags().split(",")); } //图片列表 vo.setImageContent(item.getMedias().toArray(new String[]{})); //距离 vo.setDistance("500米"); Date date = new Date(item.getCreated()); vo.setCreateDate(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date)); //设置是否点赞(后续处理) vo.setHasLoved(0); vo.setHasLiked(0); return vo; } }4.3.2、controller修改MovementsController完成查询好友动态功能/** * 查询我的动态 */ @GetMapping("/all") public ResponseEntity findByUserId(Long userId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pagesize) { PageResult pr = movementService.findByUserId(userId,page,pagesize); return ResponseEntity.ok(pr); }4.3.3、service修改MovementsService完成查询好友动态功能//查询个人动态 public PageResult findByUserId(Long userId, Integer page, Integer pagesize) { //1、根据用户id，调用API查询个人动态内容（PageResult -- Movement） PageResult pr = movementApi.findByUserId(userId,page,pagesize); //2、获取PageResult中的item列表对象 List<Movement> items = (List<Movement>) pr.getItems(); //3、非空判断 if(items == null) { return pr; } //4、循环数据列表 UserInfo userInfo = userInfoApi.findById(userId); List<MovementsVo> vos = new ArrayList<>(); for (Movement item : items) { //5、一个Movement构建一个Vo对象 MovementsVo vo = MovementsVo.init(userInfo, item); vos.add(vo); } //6、构建返回值 pr.setItems(vos); return pr; }4.3.4、API层修改PublishApi和PublishApiImpl完成查询好友动态功能@Override public PageResult findByUserId(Long userId, Integer page, Integer pagesize) { Criteria criteria = Criteria.where("userId").is(userId); 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); }4.3.5、测试
课程介绍MongoDB环境搭建MongoDB基本CRUD操作通过JavaApi操作MongoDBSpringBoot整合MongoDB1、通用设置1.1 需求分析1.1.1 需求分析通用设置，包含探花交友APP基本的软件设置功能。包含：设置陌生人问题：当平台其他用户想进行在线交流时需要回答陌生人问题。通用设置：包含一些APP通知设置黑名单：对于不感兴趣的用户设置黑名单屏蔽骚扰1.1.2 数据库表通用设置CREATE TABLE `tb_settings` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) DEFAULT NULL, `like_notification` tinyint(4) DEFAULT '1' COMMENT '推送喜欢通知', `pinglun_notification` tinyint(4) DEFAULT '1' COMMENT '推送评论通知', `gonggao_notification` tinyint(4) DEFAULT '1' COMMENT '推送公告通知', `created` datetime DEFAULT NULL, `updated` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='设置表';问题表CREATE TABLE `tb_question` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) DEFAULT NULL COMMENT '用户id', `txt` varchar(200) DEFAULT NULL COMMENT '问题内容', `created` datetime DEFAULT NULL, `updated` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `user_id` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8黑名单CREATE TABLE `tb_black_list` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) DEFAULT NULL, `black_user_id` bigint(20) DEFAULT NULL, `created` datetime DEFAULT NULL, `updated` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='黑名单';1.1.3 搭建提供者环境实体类（1） Settings@Data @NoArgsConstructor @AllArgsConstructor public class Settings extends BasePojo { private Long id; private Long userId; private Boolean likeNotification; private Boolean pinglunNotification; private Boolean gonggaoNotification; }（2）Question@Data @NoArgsConstructor @AllArgsConstructor public class Question extends BasePojo { private Long id; private Long userId; //问题内容 private String txt; }（3）BlackList@Data @NoArgsConstructor @AllArgsConstructor public class BlackList extends BasePojo { private Long id; private Long userId; private Long blackUserId; }mapper接口（1）SettingsMapperpublic interface SettingsMapper extends BaseMapper<Settings> { }（2）QuestionMapperpublic interface QuestionMapper extends BaseMapper<Question> { }（3）BlackListMapperpublic interface BlackListMapper extends BaseMapper<BlackList> { }api接口（1） SettingApipackage com.tanhua.dubbo.api; import com.tanhua.domain.db.Settings; public interface SettingsApi { }（2）QuestionApipackage com.tanhua.dubbo.api; import com.tanhua.domain.db.Question; public interface QuestionApi { }（3）BlackListApipackage com.tanhua.dubbo.api; import com.baomidou.mybatisplus.core.metadata.IPage; import com.tanhua.domain.db.UserInfo; public interface BlackListApi { }api服务实现类（1）SettingServiceImplpackage com.tanhua.dubbo.api; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.tanhua.domain.db.Settings; import com.tanhua.dubbo.mapper.SettingsMapper; import org.apache.dubbo.config.annotation.Service; import org.springframework.beans.factory.annotation.Autowired; @Service public class SettingsApiImpl implements SettingsApi { @Autowired private SettingsMapper settingsMapper; }（2）QuestionServiceImplpackage com.tanhua.dubbo.api; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.tanhua.domain.db.Question; import com.tanhua.dubbo.mapper.QuestionMapper; import org.apache.dubbo.config.annotation.Service; import org.springframework.beans.factory.annotation.Autowired; @Service public class QuestionApiImpl implements QuestionApi { @Autowired private QuestionMapper questionMapper; }（3）BlackListServiceImplpackage com.tanhua.dubbo.api; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tanhua.domain.db.BlackList; import com.tanhua.domain.db.UserInfo; import com.tanhua.dubbo.mapper.BlackListMapper; import com.tanhua.dubbo.mapper.UserInfoMapper; import org.apache.dubbo.config.annotation.Service; import org.springframework.beans.factory.annotation.Autowired; @Service public class BlackListApiImpl implements BlackListApi { @Autowired private BlackListMapper blackListMapper; }1.2 查询通用设置1.2.1 接口文档1.2.2 代码实现vo对象@Data @NoArgsConstructor @AllArgsConstructor public class SettingsVo implements Serializable { private Long id; private String strangerQuestion = ""; private String phone; private Boolean likeNotification = true; private Boolean pinglunNotification = true; private Boolean gonggaoNotification = true; }SettingsController在tanhua-server工程创建SettingsController完成代码编写@RestController @RequestMapping("/users") public class SettingsController { @Autowired private SettingsService settingsService; /** * 查询通用设置 */ @GetMapping("/settings") public ResponseEntity settings() { SettingsVo vo = settingsService.settings(); return ResponseEntity.ok(vo); } }SettingService在tanhua-server工程创建SettingService完成代码编写@Service public class SettingsService { @DubboReference private QuestionApi questionApi; @DubboReference private SettingsApi settingsApi; @DubboReference private BlackListApi blackListApi; //查询通用设置 public SettingsVo settings() { SettingsVo vo = new SettingsVo(); //1、获取用户id Long userId = UserHolder.getUserId(); vo.setId(userId); //2、获取用户的手机号码 vo.setPhone(UserHolder.getMobile()); //3、获取用户的陌生人问题 Question question = questionApi.findByUserId(userId); String txt = question == null ? "你喜欢java吗？" : question.getTxt(); vo.setStrangerQuestion(txt); //4、获取用户的APP通知开关数据 Settings settings = settingsApi.findByUserId(userId); if(settings != null) { vo.setGonggaoNotification(settings.getGonggaoNotification()); vo.setPinglunNotification(settings.getPinglunNotification()); vo.setLikeNotification(settings.getLikeNotification()); } return vo; } }QuestionApi在tanhua-dubbo中的QuestionApi和QuestionApiImpl补充方法@Override public Question findByUserId(Long userId) { QueryWrapper<Question> qw = new QueryWrapper<>(); qw.eq("user_id",userId); return questionMapper.selectOne(qw); }SettingApi在tanhua-dubbo中的SettingApi和SettingApiImpl补充方法//根据用户id查询 public Settings findByUserId(Long userId) { QueryWrapper<Settings> qw = new QueryWrapper<>(); qw.eq("user_id",userId); return settingsMapper.selectOne(qw); }1.2 陌生人问题对数据库表进行操作：如果存在数据，更新数据库。如果不存在数据，保存数据库表数据1.2.1 接口文档1.2.2 代码实现SettingsController/** * 设置陌生人问题 */ @PostMapping("/questions") public ResponseEntity questions(@RequestBody Map map) { //获取参数 String content = (String) map.get("content"); settingsService.saveQuestion(content); return ResponseEntity.ok(null); }SettingsService//设置陌生人问题 public void saveQuestion(String content) { //1、获取当前用户id Long userId = UserHolder.getUserId(); //2、调用api查询当前用户的陌生人问题 Question question = questionApi.findByUserId(userId); //3、判断问题是否存在 if(question == null) { //3.1 如果不存在，保存 question = new Question(); question.setUserId(userId); question.setTxt(content); questionApi.save(question); }else { //3.2 如果存在，更新 question.setTxt(content); questionApi.update(question); } }QuestionApitanhua-dubbo工程中的QuestionApi和QuestionApiImpl中添加保存和更新方法@Override public void save(Question question) { questionMapper.insert(question); } @Override public void update(Question question) { questionMapper.updateById(question); }1.3 通知设置1.3.1 接口文档通知管理：对通知进行保存或者更新的操作http://192.168.136.160:3000/project/19/interface/api/2801.3.2 代码实现SettingsController/** * 通知设置 */ @PostMapping("/notifications/setting") public ResponseEntity notifications(@RequestBody Map map) { //获取参数 settingsService.saveSettings(map); return ResponseEntity.ok(null); }SettingsService//通知设置 public void saveSettings(Map map) { boolean likeNotification = (Boolean) map.get("likeNotification"); boolean pinglunNotification = (Boolean) map.get("pinglunNotification"); boolean gonggaoNotification = (Boolean) map.get("gonggaoNotification"); //1、获取当前用户id Long userId = UserHolder.getUserId(); //2、根据用户id，查询用户的通知设置 Settings settings = settingsApi.findByUserId(userId); //3、判断 if(settings == null) { //保存 settings = new Settings(); settings.setUserId(userId); settings.setPinglunNotification(pinglunNotification); settings.setLikeNotification(likeNotification); settings.setGonggaoNotification(gonggaoNotification); settingsApi.save(settings); }else { settings.setPinglunNotification(pinglunNotification); settings.setLikeNotification(likeNotification); settings.setGonggaoNotification(gonggaoNotification); settingsApi.update(settings); } }SettingsApitanhua-dubbo工程中的SettingsApi和SettingsApiImpl中添加保存和更新方法@Override public void save(Settings settings) { settingsMapper.insert(settings); } @Override public void update(Settings settings) { settingsMapper.updateById(settings); }1.4 黑名单管理1.3.1 接口文档查询黑名单列表移除黑名单1.3.2 分页查询vo对象tanhua-domain工程的配置分页vo对象package com.tanhua.domain.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Collections; import java.util.List; @Data @AllArgsConstructor @NoArgsConstructor public class PageResult implements Serializable { private Integer counts = 0;//总记录数 private Integer pagesize;//页大小 private Integer pages = 0;//总页数 private Integer page;//当前页码 private List<?> items = Collections.emptyList(); //列表 public PageResult(Integer page,Integer pagesize, int counts,List list) { this.page = page; this.pagesize = pagesize; this.items = list; this.counts = counts; this.pages = counts % pagesize == 0 ? counts / pagesize : counts / pagesize + 1; } }SettingsController/** * 分页查询黑名单列表 */ @GetMapping("/blacklist") public ResponseEntity blacklist( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { //1、调用service查询 PageResult pr = settingsService.blacklist(page,size); //2、构造返回 return ResponseEntity.ok(pr); } /** * 取消黑名单 */ @DeleteMapping("/blacklist/{uid}") public ResponseEntity deleteBlackList(@PathVariable("uid") Long blackUserId) { settingsService.deleteBlackList(blackUserId); return ResponseEntity.ok(null); }SettingService//分页查询黑名单列表 public PageResult blacklist(int page, int size) { //1、获取当前用户的id Long userId = UserHolder.getUserId(); //2、调用API查询用户的黑名单分页列表 Ipage对象 IPage<UserInfo> iPage = blackListApi.findByUserId(userId,page,size); //3、对象转化，将查询的Ipage对象的内容封装到PageResult中 PageResult pr = new PageResult(page,size,iPage.getTotal(),iPage.getRecords()); //4、返回 return pr; } //取消黑名单 public void deleteBlackList(Long blackUserId) { //1、获取当前用户id Long userId = UserHolder.getUserId(); //2、调用api删除 blackListApi.delete(userId,blackUserId); }BlackListApi@Override public IPage<UserInfo> findByUserId(Long userId, int page, int size) { //1、构建分页参数对象Page Page pages = new Page(page,size); //2、调用方法分页（自定义编写 分页参数Page，sql条件参数） return userInfoMapper.findBlackList(pages,userId); } @Override public void delete(Long userId, Long blackUserId) { QueryWrapper<BlackList> qw = new QueryWrapper<>(); qw.eq("user_id",userId); qw.eq("black_user_id",blackUserId); blackListMapper.delete(qw); }UserInfoMapperpublic interface UserInfoMapper extends BaseMapper<UserInfo> { @Select("select * from tb_user_info where id in (\n" + " SELECT black_user_id FROM tb_black_list where user_id=#{userId}\n" + ")") IPage<UserInfo> findBlackList(@Param("pages") Page pages, @Param("userId") Long userId); }MybatisPlusConfigtanhua-dubbo-db引导类开启mybatis-plus分页插件支持@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }使用mybatis-plus的分页：创建分页对象：Page，指定当前页和每页查询条数基础查询：mapper.selectPage(page,查询条件)自定义查询：Ipage 方法名称（Page对象，xxx查询条件）2、MongoDB简介对于社交类软件的功能，我们需要对它的功能特点做分析：数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态内容地理位置的查询……针对以上特点，我们来分析一下：mysql：关系型数据库（效率低）redis：redis缓存（微博，效率高，数据格式不丰富）对于数据量大而言，显然不能够使用关系型数据库进行存储，我们需要通过MongoDB进行存储对于读多写少的应用，需要减少读取的成本比如说，一条SQL语句，单张表查询一定比多张表查询要快探花交友mongodb：存储业务数据（圈子，推荐的数据，小视频数据，点赞，评论等）redis：承担的角色是缓存层（提升查询效率）mysql：存储和核心业务数据，账户1.1、MongoDB简介MongoDB：是一个高效的非关系型数据库（不支持表关系：只能操作单表）MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之间的产品，是非关系数据库当中功能最丰富，最像关系数据库的，它支持的数据结构非常松散，是类似json的bson格式，因此可以存储比较复杂的数据类型。MongoDB最大的特点是它支持的查询语言非常强大，其语法有点类似于面向对象的查询语言，几乎可以实现类似关系数据库单表查询的绝大部分功能，而且还支持对数据建立索引。官网：https://www.mongodb.com1.2、MongoDB的特点MongoDB 最大的特点是他支持的查询语言非常强大，其语法有点类似于面向对象的查询语言，几乎可以实现类似关系数据库单表查询的绝大部分功能，而且还支持对数据建立索引。它是一个面向集合的,模式自由的文档型数据库。具体特点总结如下：面向集合存储，易于存储对象类型的数据模式自由支持动态查询支持完全索引，包含内部对象支持复制和故障恢复使用高效的二进制数据存储，包括大型对象（如视频等）自动处理碎片，以支持云计算层次的扩展性支持 Python，PHP，Ruby，Java，C，C#，Javascript，Perl及C++语言的驱动程 序， 社区中也提供了对Erlang及.NET 等平台的驱动程序文件存储格式为 BSON（一种 JSON 的扩展）1.2.1、通过docker安装MongoDB在课程资料的虚拟机中已经提供了MongoDB的镜像和容器，我们只需要使用简单的命令即可启动#进入base目录 cd /root/docker-file/base/ #批量创建启动容器，其中已经包含了redis，zookeeper，mongodb容器 docker-compose up -d #查看容器 docker ps -a可以看到mongoDB已经启动，对外暴露了27017的操作端口1.2.2、MongoDB体系结构MongoDB 的逻辑结构是一种层次结构。主要由： 文档(document)、集合(collection)、数据库(database)这三部分组成的。逻辑结构是面 向用户的，用户使用 MongoDB 开发应用程序使用的就是逻辑结构。MongoDB 的文档（document），相当于关系数据库中的一行记录。多个文档组成一个集合（collection），相当于关系数据库的表。多个集合（collection），逻辑上组织在一起，就是数据库（database）。一个 MongoDB 实例支持多个数据库（database）。 文档(document)、集合(collection)、数据库(database)的层次结构如下图:为了更好的理解，下面与SQL中的概念进行对比：SQL术语/概念MongoDB术语/概念解释/说明databasedatabase数据库tablecollection数据库表/集合rowdocument表中的一条数据columnfield数据字段/域indexindex索引table joins 表连接,MongoDB不支持primary keyprimary key主键,MongoDB自动将_id字段设置为主键1.3 数据类型数据格式：BSON {aa:bb}null：用于表示空值或者不存在的字段，{“x”:null}布尔型：布尔类型有两个值true和false，{“x”:true}数值：shell默认使用64为浮点型数值。{“x”：3.14}或{“x”：3}。对于整型值，可以使用 NumberInt（4字节符号整数）或NumberLong（8字节符号整数）， {“x”:NumberInt(“3”)}{“x”:NumberLong(“3”)}字符串：UTF-8字符串都可以表示为字符串类型的数据，{“x”：“呵呵”}日期：日期被存储为自新纪元依赖经过的毫秒数，不存储时区，{“x”:new Date()}正则表达式：查询时，使用正则表达式作为限定条件，语法与JavaScript的正则表达式相 同，{“x”:/[abc]/}数组：数据列表或数据集可以表示为数组，{“x”： [“a“，“b”,”c”]}内嵌文档：文档可以嵌套其他文档，被嵌套的文档作为值来处理，{“x”:{“y”:3 }}对象Id：对象id是一个12字节的字符串，是文档的唯一标识，{“x”: objectId() }二进制数据：二进制数据是一个任意字节的字符串。它不能直接在shell中使用。如果要 将非utf-字符保存到数据库中，二进制数据是唯一的方式。3、MongoDB入门2.1、数据库以及表的操作#查看所有的数据库 > show dbs #通过use关键字切换数据库 > use admin #创建数据库 #说明：在MongoDB中，数据库是自动创建的，通过use切换到新数据库中，进行插入数据即可自动创建数据库 > use testdb > show dbs #并没有创建数据库 > db.user.insert({id:1,name:'zhangsan'}) #插入数据 > show dbs #查看表 > show tables > show collections #删除集合（表） > db.user.drop() true #如果成功删除选定集合，则 drop() 方法返回 true，否则返回 false。 #删除数据库 > use testdb #先切换到要删除的数据中 > db.dropDatabase() #删除数据库2.2、新增数据在MongoDB中，存储的文档结构是一种类似于json的结构，称之为bson（全称为：Binary JSON）。#插入数据 #语法：db.表名.insert(json字符串) > db.user.insert({id:1,username:'zhangsan',age:20}) > db.user.find() #查询数据2.3、更新数据update() 方法用于更新已存在的文档。语法格式如下：db.collection.update( <query>, <update>, [ upsert: <boolean>, multi: <boolean>, writeConcern: <document> ] )参数说明：query : update的查询条件，类似sql update查询内where后面的。update : update的对象和一些更新的操作符（如$,$inc.$set）等，也可以理解为sql update查询内set后面的upsert : 可选，这个参数的意思是，如果不存在update的记录，是否插入objNew,true为插入，默认是false，不插入。multi : 可选，mongodb 默认是false,只更新找到的第一条记录，如果这个参数为true,就把按条件查出来多条记录全部更新。writeConcern :可选，抛出异常的级别。#查询全部 > db.user.find() #更新数据 > db.user.update({id:1},{$set:{age:22}}) #注意：如果这样写，会删除掉其他的字段 > db.user.update({id:1},{age:25}) #更新不存在的字段，会新增字段 > db.user.update({id:2},{$set:{sex:1}}) #更新数据 #更新不存在的数据，默认不会新增数据 > db.user.update({id:3},{$set:{sex:1}}) #如果设置第一个参数为true，就是新增数据 > db.user.update({id:3},{$set:{sex:1}},true)2.4、删除数据通过remove()方法进行删除数据，语法如下：db.collection.remove( <query>, { justOne: <boolean>, writeConcern: <document> } )参数说明：query :（可选）删除的文档的条件。justOne : （可选）如果设为 true 或 1，则只删除一个文档，如果不设置该参数，或使用默认值 false，则删除所有匹配条件的文档。writeConcern :（可选）抛出异常的级别。实例：#删除数据 > db.user.remove({}) #插入4条测试数据 db.user.insert({id:1,username:'zhangsan',age:20}) db.user.insert({id:2,username:'lisi',age:21}) db.user.insert({id:3,username:'wangwu',age:22}) db.user.insert({id:4,username:'zhaoliu',age:22}) > db.user.remove({age:22},true) #删除所有数据 > db.user.remove({})2.5、查询数据MongoDB 查询数据的语法格式如下：db.user.find([query],[fields])query ：可选，使用查询操作符指定查询条件fields ：可选，使用投影操作符指定返回的键。查询时返回文档中所有键值， 只需省略该参数即可（默认省略）。条件查询：操作格式范例RDBMS中的类似语句等于{:}db.col.find({"by":"黑马程序员"}).pretty()where by = '黑马程序员'小于{:{$lt:}}db.col.find({"likes":{$lt:50}}).pretty()where likes < 50小于或等于{:{$lte:}}db.col.find({"likes":{$lte:50}}).pretty()where likes <= 50大于{:{$gt:}}db.col.find({"likes":{$gt:50}}).pretty()where likes > 50大于或等于{:{$gte:}}db.col.find({"likes":{$gte:50}}).pretty()where likes >= 50不等于{:{$ne:}}db.col.find({"likes":{$ne:50}}).pretty()where likes != 50实例：#插入测试数据 db.user.insert({id:1,username:'zhangsan',age:20}) db.user.insert({id:2,username:'lisi',age:21}) db.user.insert({id:3,username:'wangwu',age:22}) db.user.insert({id:4,username:'zhaoliu',age:22}) db.user.find() #查询全部数据 db.user.find({},{id:1,username:1}) #只查询id与username字段 db.user.find().count() #查询数据条数 db.user.find({id:1}) #查询id为1的数据 db.user.find({age:{$lte:21}}) #查询小于等于21的数据 db.user.find({$or:[{id:1},{id:2}]}) #查询id=1 or id=2 #分页查询：Skip()跳过几条，limit()查询条数 db.user.find().limit(2).skip(1) #跳过1条数据，查询2条数据 db.user.find().sort({id:-1}) #按照id倒序排序，-1为倒序，1为正序2.6、索引索引通常能够极大的提高查询的效率，如果没有索引，MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。这种扫描全集合的查询效率是非常低的，特别在处理大量的数据时，查询可以要花费几十秒甚至几分钟，这对网站的性能是非常致命的。索引是特殊的数据结构，索引存储在一个易于遍历读取的数据集合中，索引是对数据库表中一列或多列的值进行排序的一种结构#创建索引 > db.user.createIndex({'age':1}) #查看索引 > db.user.getIndexes() [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "testdb.user" } ] #说明：1表示升序创建索引，-1表示降序创建索引。2.7、执行计划MongoDB 查询分析可以确保我们建议的索引是否有效，是查询语句性能分析的重要工具。#插入1000条数据 for(var i=1;i<1000;i++)db.user.insert({id:100+i,username:'name_'+i,age:10+i}) #查看执行计划 > db.user.find({age:{$gt:100},id:{$lt:200}}).explain() #测试没有使用索引 > db.user.find({username:'zhangsan'}).explain() #winningPlan：最佳执行计划 #"stage" : "FETCH", #查询方式，常见的有COLLSCAN/全表扫描、IXSCAN/索引扫描、FETCH/根据索引去检索文档、SHARD_MERGE/合并分片结果、IDHACK/针对_id进行查询4、SpringData-MongoSpring-data对MongoDB做了支持，使用spring-data-mongodb可以简化MongoDB的操作，封装了底层的mongodb-driver。地址：https://spring.io/projects/spring-data-mongodb使用Spring-Data-MongoDB很简单，只需要如下几步即可：导入起步依赖编写配置信息编写实体类（配置注解 @Document，@Id）操作mongodb注入MongoTemplate对象，完成CRUD操作编写Repository接口，注入接口完成基本Crud操作4.1、环境搭建第一步，导入依赖：<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.9.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>第二步，编写application.yml配置文件spring: data: mongodb: uri: mongodb://192.168.136.160:27017/test第三步，编写启动类package com.tanhua.mongo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MongoApplication { public static void main(String[] args) { SpringApplication.run(MongoApplication.class, args); } }4.2、完成基本操作第一步，编写实体类package com.tanhua.mongo.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Document; @Data @AllArgsConstructor @NoArgsConstructor @Document(value="person") public class Person { private ObjectId id; private String name; private int age; private String address; }第二步，通过MongoTemplate完成CRUD操作package com.itheima.mongo.test; import com.itheima.mongo.domain.Person; 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.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; @RunWith(SpringRunner.class) @SpringBootTest public class MongoTest { /** * 1. 注入MongoTemplate对象 * 2. 调用其他方法完成数据的CRUD */ @Autowired private MongoTemplate mongoTemplate; // 保存 @Test public void testSave() { Person person = new Person(); person.setName("李四"); person.setAge(18); person.setAddress("北京东城"); mongoTemplate.save(person); } // 查询 @Test public void testFindAll() { List<Person> persons = mongoTemplate.findAll(Person.class); for (Person person : persons) { System.out.println(person); } } // 条件查询 @Test public void testFind() { // 1. 创建Criteria对象，并设置查询条件 Criteria criteria = Criteria.where("myname").is("李四") .and("age").is(18); // is相当于sql中的= // 2. 根据Criteria对象创建Query对象 Query query = Query.query(criteria); // 3. 使用MongoTemplate进行查询 List<Person> persons = mongoTemplate.find(query, Person.class); // Query实体类对象 for (Person person : persons) { System.out.println(person); } } // 分页查询 @Test public void testFindPage() { int page = 1; int size = 2; // 1. 创建Criteria对象，并设置查询条件 Criteria criteria = Criteria.where("age").lt(50); // 2. 根据Criteria对象创建Query对象,在Query中设置分页参数 Query query = Query.query(criteria) .skip((page - 1) * size) // 从第几条开始查询 .limit(size) // 每页查询条数 .with(Sort.by(Sort.Order.desc("age"))); // 降序排序 // 3. 使用MongoTemplate进行查询 List<Person> persons = mongoTemplate.find(query, Person.class); // Query实体类对象 for (Person person : persons) { System.out.println(person); } } // 更新 @Test public void testUpdate() { // 1. 创建Criteria对象，并设置查询条件 Criteria criteria = Criteria.where("id").is("633ac0a9a4140a30e6836053"); // 2. 构建Query对象 Query query = Query.query(criteria); // 3. 设置需要更新的内容 Update update = new Update(); update.set("age", 10); update.set("myname", "赵六"); // 4. 执行操作 mongoTemplate.updateFirst(query, update, Person.class); } // 删除 @Test public void testRemove() { // 1. 创建Criteria对象，并设置查询条件 Criteria criteria = Criteria.where("id").is("633ac0a9a4140a30e6836053"); // 2. 构建Query对象 Query query = Query.query(criteria); // 2. 删除 mongoTemplate.remove(query, Person.class); } }5、今日佳人在用户登录成功后，就会进入首页，首页中有今日佳人、推荐好友、探花、搜附近等功能。今日佳人，会推荐缘分值最大的用户，进行展现出来。缘分值的计算是由用户的行为进行打分，如：点击、点赞、评论、学历、婚姻状态等信息组合而成的。实现：我们先不考虑推荐的逻辑，假设现在已经有推荐的结果，我们只需要从结果中查询到缘分值最高的用户就可以了。至于推荐的逻辑以及实现，我们将后面的课程中讲解。流程：5.1、表结构设计#表结构 recommend_user { "userId":1001, #推荐的用户id "toUserId":1002, #用户id "score":90, #推荐得分 "date":"2019/1/1" #日期 }在MongoDB中只存储用户的id数据，其他的数据需要通过接口查询。5.2、服务提供者5.2.0、导入依赖找到tanhua-domain模块的pom.xml打开mongo的依赖5.2.1、实体类@AllArgsConstructor @NoArgsConstructor @Data @Document(collection = "recommend_user") public class RecommendUser implements java.io.Serializable { private ObjectId id; //主键id private Long userId; //推荐的用户id private Long toUserId; //用户id private Double score =0d; //推荐得分 private String date; //日期 }5.2.2、RecommendUserApi接口public interface RecommendUserApi { RecommendUser queryWithMaxScore(Long toUserId); }5.2.3、RecommendUserApiImpl@DubboService public class RecommendUserApiImpl implements RecommendUserApi { @Autowired private MongoTemplate mongoTemplate; //查询今日佳人 public RecommendUser queryWithMaxScore(Long toUserId) { //根据toUserId查询，根据评分score排序，获取第一条 //构建Criteria Criteria criteria = Criteria.where("toUserId").is(toUserId); //构建Query对象 Query query = Query.query(criteria).with(Sort.by(Sort.Order.desc("score"))) .limit(1); //调用mongoTemplate查询 return mongoTemplate.findOne(query,RecommendUser.class); } }5.2.4、application配置server: port: 18082 spring: application: name: tanhua-dubbo-mongo cloud: nacos: discovery: server-addr: 192.168.136.160:8848 data: mongodb: uri: mongodb://192.168.136.160:27017/tanhua dubbo: protocol: name: dubbo port: 20881 registry: address: spring-cloud://localhost scan: base-packages: com.tanhua.dubbo.api #dubbo中包扫描5.2.5 启动类package com.tanhua.dubbo; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class DubboMongoApplication { public static void main(String[] args) { SpringApplication.run(DubboMongoApplication.class,args); } }5.3、代码实现5.3.1、接口说明地址：http://192.168.136.160:3000/project/19/interface/api/1005.3.2、TanhuaController@RestController @RequestMapping("/tanhua") public class TanhuaController { @Autowired private TanhuaService tanhuaService; //今日佳人 @GetMapping("/todayBest") public ResponseEntity todayBest() { TodayBest vo = tanhuaService.todayBest(); return ResponseEntity.ok(vo); } }5.3.3、TanhuaService@Service public class TanhuaService { @DubboReference private RecommendUserApi recommendUserApi; @DubboReference private UserInfoApi userInfoApi; @DubboReference private QuestionApi questionApi; @Autowired private HuanXinTemplate template; //查询今日佳人数据 public TodayBest todayBest() { //1、获取用户id Long userId = UserHolder.getUserId(); //2、调用API查询 RecommendUser recommendUser = recommendUserApi.queryWithMaxScore(userId); if(recommendUser == null) { recommendUser = new RecommendUser(); recommendUser.setUserId(1l); recommendUser.setScore(99d); } //3、将RecommendUser转化为TodayBest对象 UserInfo userInfo = userInfoApi.findById(recommendUser.getUserId()); TodayBest vo = TodayBest.init(userInfo, recommendUser); //4、返回 return vo; } }5.3.4、vo对象package com.tanhua.domain.vo; import com.tanhua.domain.db.UserInfo; import com.tanhua.domain.mongo.RecommendUser; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.beans.BeanUtils; /** * 今日佳人 */ @Data @NoArgsConstructor @AllArgsConstructor public class TodayBest { private Long id; //用户id private String avatar; private String nickname; private String gender; //性别 man woman private Integer age; private String[] tags; private Long fateValue; //缘分值 /** * 在vo对象中，补充一个工具方法，封装转化过程 */ public static TodayBest init(UserInfo userInfo, RecommendUser recommendUser) { TodayBest vo = new TodayBest(); BeanUtils.copyProperties(userInfo,vo); if(userInfo.getTags() != null) { vo.setTags(userInfo.getTags().split(",")); } vo.setFateValue(recommendUser.getScore().longValue()); return vo; } }5.3.5、解决MongoDB启动bug在项目中，添加了mongo的依赖的话，springboot就会自动去连接本地的mongo，由于他连接不上会导致出错。解决有2种方案：排除掉mongo的依赖springboot中添加排除自动配置的注解package com.tanhua.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @SpringBootApplication(exclude = { MongoAutoConfiguration.class, MongoDataAutoConfiguration.class }) //排除mongo的自动配置 public class TanhuaServerApplication { public static void main(String[] args) { SpringApplication.run(TanhuaServerApplication.class,args); } }
1、完善用户信息用户在首次登录时需要完善个人信息，包括性别、昵称、生日、城市、头像等。其中，头像数据需要做图片上传，这里采用阿里云的OSS服务作为我们的图片服务器，并且对头像要做人脸识别，非人脸照片不得上传。首次登录时（手机号码不存在），需要创建用户存入数据库中客户端检测首次登录需要完善用户信息填写用户基本信息上传用户头像（需要人脸认证）1.1、阿里云OSS实现图片上传服务，需要有存储的支持，那么我们的解决方案将以下几种：直接将图片保存到服务的硬盘（springmvc将的文件上传）优点：开发便捷，成本低缺点：扩容困难使用分布式文件系统进行存储优点：容易实现扩容缺点：开发复杂度稍大（有成熟的产品可以使用，比如：FastDFS）使用第三方的存储服务优点：开发简单，拥有强大功能，免维护缺点：付费在本套课程中选用阿里云的OSS服务进行图片存储。1.1.1、概述对象存储服务（Object Storage Service，OSS）是一种海量、安全、低成本、高可靠的云存储服务，适合存放任意类型的文件。容量和处理能力弹性扩展，多种存储类型供选择，全面优化存储成本。地址：https://www.aliyun.com/product/oss1.1.2、账号申请购买服务使用第三方服务最大的缺点就是需要付费，下面，我们看下如何购买开通服务。购买下行流量包： （不购买也可以使用，按照流量付费）说明：OSS的上行流量是免费的，但是下行流量是需要购买的。创建Bucket使用OSS，首先需要创建Bucket，Bucket翻译成中文是水桶的意思，把存储的图片资源看做是水，想要盛水必须得有桶，就是这个意思了。进入控制台，https://oss.console.aliyun.com/overview选择Bucket后，即可看到对应的信息，如：url、消耗流量等 :文件管理：查看文件：1.1.3、抽取模板工具和发送短信类似，阿里云OSS也是采用自定义工具的形式进行封装OssPropertiestanhua-autoconfig创建配置类@Data @ConfigurationProperties(prefix = "tanhua.oss") public class OssProperties { private String accessKey; private String secret; private String bucketName; private String url; //域名 private String endpoint; }OssTemplatetanhua-autoconfig创建模板对象package com.tanhua.autoconfig.template; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.tanhua.autoconfig.properties.OssProperties; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID; public class OssTemplate { private OssProperties properties; public OssTemplate(OssProperties properties) { this.properties = properties; } /** * 文件上传 * 1：文件名称 * 2：输入流 */ public String upload(String filename, InputStream is) { //3、拼写图片路径 filename = new SimpleDateFormat("yyyy/MM/dd").format(new Date()) +"/"+ UUID.randomUUID().toString() + filename.substring(filename.lastIndexOf(".")); // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1（杭州）为例，Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。 String endpoint = properties.getEndpoint(); // 阿里云主账号AccessKey拥有所有API的访问权限，风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维，请登录 https://ram.console.aliyun.com 创建RAM账号。 String accessKeyId = properties.getAccessKey(); String accessKeySecret = properties.getSecret(); // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId,accessKeySecret); // 填写Byte数组。 // 填写Bucket名称和Object完整路径。Object完整路径中不能包含Bucket名称。 ossClient.putObject(properties.getBucketName(), filename, is); // 关闭OSSClient。 ossClient.shutdown(); String url = properties.getUrl() +"/" + filename; return url; } }TanhuaAutoConfigurationTanhuaAutoConfiguration加入配置@EnableConfigurationProperties({ SmsProperties.class, OssProperties.class }) public class TanhuaAutoConfiguration { @Bean public SmsTemplate smsTemplate(SmsProperties properties) { return new SmsTemplate(properties); } @Bean public OssTemplate ossTemplate(OssProperties properties) { return new OssTemplate(properties); } }1.1.4、测试tanhua-app-server加入配置内容，并测试tanhua: oss: accessKey: LTAI4GKgob9vZ53k2SZdyAC7 secret: LHLBvXmILRoyw0niRSBuXBZewQ30la endpoint: oss-cn-beijing.aliyuncs.com bucketName: tanhua001 url: https://tanhua001.oss-cn-beijing.aliyuncs.com/编写测试类@RunWith(SpringRunner.class) @SpringBootTest(classes = AppServerApplication.class) public class OssTest { @Autowired private OssTemplate template; @Test public void testTemplateUpload() throws FileNotFoundException { String path = "C:\\Users\\lemon\\Desktop\\课程资源\\02-完善用户信息\\03-资料\\2.jpg"; FileInputStream inputStream = new FileInputStream(new File(path)); String imageUrl = template.upload(path, inputStream); System.out.println(imageUrl); } }1.2、百度人脸识别人脸识别（Face Recognition）基于图像或视频中的人脸检测、分析和比对技术，提供对您已获授权前提下的私有数据的人脸检测与属性分析、人脸对比、人脸搜索、活体检测等能力。灵活应用于金融、泛安防、零售等行业场景，满足身份核验、人脸考勤、闸机通行等业务需求1.2.1、概述地址：https://ai.baidu.com/tech/face1.2.2、账号申请账号登录注册百度云AI支持百度账号登录，也可以支持云账号。按需注册即可创建应用按需创建应用1.2.3、抽取模板工具AipFaceProperties@Data @ConfigurationProperties("tanhua.aip") public class AipFaceProperties { private String appId; private String apiKey; private String secretKey; @Bean public AipFace aipFace() { AipFace client = new AipFace(appId, apiKey, secretKey); // 可选：设置网络连接参数 client.setConnectionTimeoutInMillis(2000); client.setSocketTimeoutInMillis(60000); return client; } }AipFaceTemplatepackage com.tanhua.autoconfig.template; import com.baidu.aip.face.AipFace; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import java.util.HashMap; public class AipFaceTemplate { @Autowired private AipFace client; /** * 检测图片中是否包含人脸 * true：包含 * false：不包含 */ public boolean detect(String imageUrl) { // 调用接口 String imageType = "URL"; HashMap<String, String> options = new HashMap<String, String>(); options.put("face_field", "age"); options.put("max_face_num", "2"); options.put("face_type", "LIVE"); options.put("liveness_control", "LOW"); // 人脸检测 JSONObject res = client.detect(imageUrl, imageType, options); System.out.println(res.toString(2)); Integer error_code = (Integer) res.get("error_code"); return error_code == 0; } }1.2.4、测试tanhua-app-server加入百度AI的配置信息tanhua: aip: appId: 24021388 apiKey: ZnMTwoETXnu4OPIGwGAO2H4G secretKey: D4jXShyinv5q26bUS78xRKgNLnB9IfZh编写单元测试类@RunWith(SpringRunner.class) @SpringBootTest(classes = AppServerApplication.class) public class FaceTest { @Autowired private AipFaceTemplate template; @Test public void detectFace() { String image = "https://tanhua001.oss-cn-beijing.aliyuncs.com/2021/04/19/a3824a45-70e3-4655-8106-a1e1be009a5e.jpg"; boolean detect = template.detect(image); } }1.3、保存用户信息1.3.1、接口文档YAPI接口地址：http://192.168.136.160:3000/project/19/interface/api/881.3.2、需求分析数据库表tb_user_info（用户基本资料表）用户表和用户信息表是一对一的关系，两者采用主键关联的形式配置主键关联：用户表主键和用户资料表主键要保持一致（如：用户表id=1，此用户的资料表id=1）执行流程手机端发送请求到消费者消费者构造数据，调用提供者消费者完成业务处理，操作数据库1.3.3、代码实现UserInfo实体类@Data @NoArgsConstructor @AllArgsConstructor public class UserInfo implements Serializable { /** * 由于userinfo表和user表之间是一对一关系 * userInfo的id来源于user表的id */ @TableId(type= IdType.INPUT) private Long id; //用户id private String nickname; //昵称 private String avatar; //用户头像 private String birthday; //生日 private String gender; //性别 private Integer age; //年龄 private String city; //城市 private String income; //收入 private String education; //学历 private String profession; //行业 private Integer marriage; //婚姻状态 private String tags; //用户标签：多个用逗号分隔 private String coverPic; // 封面图片 private Date created; private Date updated; //用户状态,1为正常，2为冻结 @TableField(exist = false) private String userStatus = "1"; }UserController@RestController @RequestMapping("/user") public class UserController { @Autowired private UserInfoService userInfoService; /** * 保存用户信息 * UserInfo * 请求头中携带token */ @PostMapping("/loginReginfo") public ResponseEntity loginReginfo(@RequestBody UserInfo userInfo, @RequestHeader("Authorization") String token) { //1、解析token Claims claims = JwtUtils.getClaims(token); Integer id = (Integer) claims.get("id"); //2、向userinfo中设置用户id userInfo.setId(Long.valueOf(id)); //3、调用service userInfoService.save(userInfo); return ResponseEntity.ok(null); } }UserInfoService@Service public class UserInfoService { @DubboReference private UserInfoApi userInfoApi; public void save(UserInfo userInfo) { userInfoApi.save(userInfo); } }UserInfoApipublic interface UserInfoApi { public void save(UserInfo userInfo); }UserInfoApiImpl@DubboService public class UserInfoApiImpl implements UserInfoApi { @Autowired private UserInfoMapper userInfoMapper; @Override public void save(UserInfo userInfo) { userInfoMapper.insert(userInfo); } }UserInfoMapperpublic interface UserInfoMapper extends BaseMapper<UserInfo> { }1.4、上传用户头像1.4.1、需求分析接口文档YAPI地址：http://192.168.136.160:3000/project/19/interface/api/91文件上传采用POST传递，mvc中经过文件解析器转化为MultipartFile对象处理后续请求中，请求headr中都有Authorization参数。执行流程1.4.2、代码实现UsersControler/** * 更新用户资料 */ @PutMapping public ResponseEntity updateUserInfo(@RequestBody UserInfo userInfo,@RequestHeader("Authorization") String token) { Claims claims = JwtUtils.getClaims(token); Integer id = (Integer) claims.get("id"); userInfo.setId(Long.valueOf(id)); userInfoService.update(userInfo); return ResponseEntity.ok(null); }UserInfoService//更新用户头像 public void updateHead(MultipartFile headPhoto, Long id) throws IOException { //1、将图片上传到阿里云oss String imageUrl = ossTemplate.upload(headPhoto.getOriginalFilename(), headPhoto.getInputStream()); //2、调用百度云判断是否包含人脸 boolean detect = aipFaceTemplate.detect(imageUrl); //2.1 如果不包含人脸，抛出异常 if(!detect) { throw new RuntimeException(); }else{ //2.2 包含人脸，调用API更新 UserInfo userInfo = new UserInfo(); userInfo.setId(id); userInfo.setAvatar(imageUrl); userInfoApi.update(userInfo); } }UserInfoApipublic void update(UserInfo userInfo);UserInfoApiImpl@Override public void update(UserInfo userInfo) { userInfoMapper.updateById(userInfo); }2、用户信息管理2.1、查询用户资料2.1.1、接口文档YAPI地址：http://192.168.136.160:3000/project/19/interface/api/265参数userID： 用户id，当不传递时，查询当前用户的资料信息2.1.2、代码实现UserInfoVo@Data @NoArgsConstructor @AllArgsConstructor public class UserInfoVo implements Serializable { private Long id; //用户id private String nickname; //昵称 private String avatar; //用户头像 private String birthday; //生日 private String gender; //性别 private String age; //年龄 private String city; //城市 private String income; //收入 private String education; //学历 private String profession; //行业 private Integer marriage; //婚姻状态 }UsersController@RestController @RequestMapping("/users") public class UsersControler { @Autowired private UserInfoService userInfoService; /** * 查询用户资料 */ @GetMapping public ResponseEntity users(Long userID,@RequestHeader("Authorization") String token) { if(userID == null) { Claims claims = JwtUtils.getClaims(token); Integer id = (Integer) claims.get("id"); userID = Long.valueOf(id); } UserInfoVo userInfo = userInfoService.findById(userID); return ResponseEntity.ok(userInfo); } }UserInfoService//根据id查寻 public UserInfoVo findById(Long id) { UserInfo userInfo = userInfoApi.findById(id); UserInfoVo vo = new UserInfoVo(); BeanUtils.copyProperties(userInfo,vo); //copy同名同类型的属性 if(userInfo.getAge() != null) { vo.setAge(userInfo.getAge().toString()); } return vo; }UserInfoApiImpl@Override public UserInfo findById(Long id) { return userInfoMapper.selectById(id); }2.2、更新用户资料2.2.1、接口文档YAPI地址：http://192.168.136.160:3000/project/19/interface/api/2592.2.2、代码实现UsersController/** * 更新用户资料 */ @PutMapping public ResponseEntity updateUserInfo(@RequestBody UserInfo userInfo,@RequestHeader("Authorization") String token) { Claims claims = JwtUtils.getClaims(token); Integer id = (Integer) claims.get("id"); userID = Long.valueOf(id); userInfo.setId(UserHolder.getUserId()); userInfoService.update(userInfo); return ResponseEntity.ok(null); }UserInfoService//更新 public void update(UserInfo userInfo) { userInfoApi.update(userInfo); }UserInfoApiImpl@Override public void update(UserInfo userInfo) { userInfoMapper.updateById(userInfo); }3、统一token处理3.1、代码存在的问题3.2、解决方案基于ThreadLocal + 拦截器的形式统一处理拦截器（Interceptor）是一种动态拦截方法调用的机制；类似于Servlet 开发中的过滤器Filter，用于对处理器进行前置处理和后置处理。ThreadLocal线程内部的存储类，赋予了线程存储数据的能力。线程内调用的方法都可以从ThreadLocal中获取同一个对象。多个线程中ThreadLocal数据相互隔离Threadlocal使用方法很简单ThreadLocal<T> threadLocal = new ThreadLocal<T>(); threadLocal.set() //将数据绑定到当前线程 threadLocal.get() //从当前线程中获取数据3.3、代码实现3.3.1、ThreadLocal工具类定义ThreadLocal工具类，仅需要调用set方法即可将数据存入ThreadLocal中/** * 工具类：实现向threadlocal存储数据的方法 */ public class UserHolder { private static ThreadLocal<User> tl = new ThreadLocal<>(); //将用户对象，存入Threadlocal public static void set(User user) { tl.set(user); } //从当前线程，获取用户对象 public static User get() { return tl.get(); } //从当前线程，获取用户对象的id public static Long getUserId() { return tl.get().getId(); } //从当前线程，获取用户对象的手机号码 public static String getMobile() { return tl.get().getMobile(); } }3.3.2、定义拦截器定义拦截器，在前置拦截方法preHandle中解析token并验证有效性，如果失效返回状态码401。如果有效，解析User对象，存入ThreadLocal中public class TokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1、获取请求头 String token = request.getHeader("Authorization"); //2、使用工具类，判断token是否有效 boolean verifyToken = JwtUtils.verifyToken(token); //3、如果token失效，返回状态码401，拦截 if(!verifyToken) { response.setStatus(401); return false; } //4、如果token正常可用，放行 //解析token，获取id和手机号码，构造User对象，存入Threadlocal Claims claims = JwtUtils.getClaims(token); String mobile = (String) claims.get("mobile"); Integer id = (Integer) claims.get("id"); User user = new User(); user.setId(Long.valueOf(id)); user.setMobile(mobile); UserHolder.set(user); return true; } }3.3.3、注册拦截器拦截器需要注册到MVC容器中@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new TokenInterceptor()) .addPathPatterns("/**") .excludePathPatterns(new String[]{"/user/login","/user/loginVerification"}); } }3.3.4、测试修改UsersController方法，从ThreadLoca中获取用户数据/** * 查询用户资料 */ @GetMapping public ResponseEntity users(Long userID) { if(userID == null) { userID = UserHolder.getUserId(); } UserInfoVo userInfo = userInfoService.findById(userID); return ResponseEntity.ok(userInfo); }4、统一异常处理软件开发过程中，不可避免的是需要处理各种异常，常见的形式就是逐层向上抛出，web层进行处理。使用try {...} catch {...}很方便就能对异常做到业务处理冗余代码多，影响代码可读性异常处理和业务代码耦合4.1、解决方案SpringMVC提供了一套解决全局异常的处理方案，可以在代码无侵入的前提下完成异常处理。遵循逐层抛出，异常处理器统一处理的思路4.2、代码实现项目中可能存在不可预知的各种异常，如：空指针，数组越界等。针对这类异常，可以直接在异常处理器中统一处理；还有一类是可预知的错误，如图片不合法，验证码错误等等。这类错误也可以理解为业务异常，可以通过自定义异常类来处理；4.2.1、业务异常对象为了方便操作，将一些常见的业务错误封装到ErrorResult对象中@Data @NoArgsConstructor @AllArgsConstructor @Builder public class ErrorResult { private String errCode = "999999"; private String errMessage; public static ErrorResult error() { return ErrorResult.builder().errCode("999999").errMessage("系统异常稍后再试").build(); } public static ErrorResult fail() { return ErrorResult.builder().errCode("000001").errMessage("发送验证码失败").build(); } public static ErrorResult loginError() { return ErrorResult.builder().errCode("000002").errMessage("验证码失效").build(); } public static ErrorResult faceError() { return ErrorResult.builder().errCode("000003").errMessage("图片非人像，请重新上传!").build(); } public static ErrorResult mobileError() { return ErrorResult.builder().errCode("000004").errMessage("手机号码已注册").build(); } public static ErrorResult contentError() { return ErrorResult.builder().errCode("000005").errMessage("动态内容为空").build(); } public static ErrorResult likeError() { return ErrorResult.builder().errCode("000006").errMessage("用户已点赞").build(); } public static ErrorResult disLikeError() { return ErrorResult.builder().errCode("000007").errMessage("用户未点赞").build(); } public static ErrorResult loveError() { return ErrorResult.builder().errCode("000008").errMessage("用户已喜欢").build(); } public static ErrorResult disloveError() { return ErrorResult.builder().errCode("000009").errMessage("用户未喜欢").build(); } }4.2.2 业务异常类自定义业务异常类，针对业务错误之间抛出业务异常即可/** * 自定义异常类 */ @Data public class BusinessException extends RuntimeException { private ErrorResult errorResult; public BusinessException(ErrorResult errorResult) { super(errorResult.getErrMessage()); this.errorResult = errorResult; } }4.2.3、异常处理器@ControllerAdvice方法和@Controller方法是类似的，也可以指定ResponseEntity的返回值，在参数上可以获取当前的异常类型。/** * 自定义统一异常处理 * 1、通过注解，声明异常处理类 * 2、编写方法，在方法内部处理异常，构造响应数据 * 3、方法上编写注解，指定此方法可以处理的异常类型 */ @ControllerAdvice public class ExceptionAdvice { //处理业务异常 @ExceptionHandler(BusinessException.class) public ResponseEntity handlerException(BusinessException be) { be.printStackTrace(); ErrorResult errorResult = be.getErrorResult(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult); } //处理不可预知的异常 @ExceptionHandler(Exception.class) public ResponseEntity handlerException1(Exception be) { be.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResult.error()); } }