想当个黑客 吼吼吼…
1. 项目介绍及环境配置
2. 短信验证码登录
3. 用户信息
4. MongoDB
5. 推荐好友列表/MongoDB集群/动态发布与查看
6. 圈子动态/圈子互动
7. 即时通讯(基于第三方API)
8. 附近的人(百度地图APi)
9. 小视频
10.网关配置
11.后台管理
探花交友APP建立的后台管理系统,目的是完成探花交友项目的业务闭环,主要功能包括:用户管理、动态管理、审核管理以及系统管理。(实现的功能有:登录、首页、用户管理、动态审核)
网盘资源地址(不需要密码): https://pan.baidu.com/s/1daL566ehyZuQ6s5vzXYNpA?pwd=java
表名称 | 说明 |
---|---|
tb_admin | 管理员用户表 |
tb_analysis | 统计数据表 |
tb_log | 用户操作日志记录表 |
tb_freeze_detail | 冻结用户记录 |
网盘资源地址(不需要密码): https://pan.baidu.com/s/1daL566ehyZuQ6s5vzXYNpA?pwd=java
# 运行方式
# 1. 直接双击 nginx.exe
# 2. 命令
cd nginx根目录
start nginx
浏览器输入 http://localhost:8088/
网盘资源地址(不需要密码): https://pan.baidu.com/s/1daL566ehyZuQ6s5vzXYNpA?pwd=java
粘贴至 tanhua
项目文件根目录 -> 文件 -> 新建 -> 来自现有源代码的模块
新建 tanhua-model/src/main/java/com/tanhua/model/domain/Admin.java
文件:
//后台系统的管理员对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Admin implements Serializable {
/**
* id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 头像
*/
private String avatar;
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/SystemController.java
文件:
@RestController
@RequestMapping("/system/users")
public class SystemController {
@Autowired
private AdminService adminService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
// 获取验证码图片
@GetMapping("/verification")
public void verification(String uuid, HttpServletResponse response) throws IOException {
// 1. 生成验证码对象
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(299, 97);
// 2. 验证码存入redis
String code = captcha.getCode();
redisTemplate.opsForValue().set(Constants.CAP_CODE + uuid, code);
// 3. 输出验证码图片
captcha.write(response.getOutputStream());
}
}
浏览器输入 localhost:18083//system/users/verification?uuid=123
:
改为 网关请求:
浏览器输入 localhost:8888/admin/system/users/verification?uuid=123
页面不变
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/SystemController.java
文件:
/**
* 用户登录:
*/
@PostMapping("/login")
public ResponseEntity login(@RequestBody Map map) {
Map retMap = adminService.login(map);
return ResponseEntity.ok(retMap);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/AdminService.java
文件:
// 用户登录
public Map login(Map map) {
//1、获取请求的参数(username,password,verificationCode(验证码),uuid)
String username = (String) map.get("username");
String password = (String) map.get("password");
String verificationCode = (String) map.get("verificationCode");
String uuid = (String) map.get("uuid");
//2、判断用户名或者密码是否为空
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
//用户名或者密码为空
throw new BusinessException("用户名或者密码为空");
}
//3、判断验证码是否正确
if (StringUtils.isEmpty(username)) {
//验证码为空
throw new BusinessException("验证码为空");
}
//从redis中获取验证码
String key = Constants.CAP_CODE+uuid;
String code = redisTemplate.opsForValue().get(key);
if (StringUtils.isEmpty(code) || !code.equals(verificationCode)) {
//验证码错误
throw new BusinessException("验证码错误");
}
redisTemplate.delete(key);
//4、根据用户名查询用户
QueryWrapper<Admin> qw = new QueryWrapper<Admin>().eq("username", username);
Admin admin = adminMapper.selectOne(qw);
//5、判断用户是否存在,密码是否一致
password = SecureUtil.md5(password); //md5加密
if(admin == null || !admin.getPassword().equals(password)) {
//用户名错误或者密码不一致
throw new BusinessException("用户名或者密码");
}
//6、通过JWT生成token
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("username", username);
claims.put("id", admin.getId());
String token = JwtUtils.getToken(claims);
//8、构造返回值
Map resMap = new HashMap();
resMap.put("token", token);
return resMap;
}
(暂时还不能实现跳转页面)
博主在编写开发项目时,遇到无法处理的mysql版本问题,使用了跨过校验规则,如果遇到了同样的问题,可以查看下方gitee仓库地址(关键词:后台登录_跨过校验)
Gitee仓库: https://gitee.com/yuan0_0/tanhua_20220918.git
新建 tanhua-model/src/main/java/com/tanhua/model/vo/AdminVo.java
文件:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdminVo {
// id
private String id;
// 用户名
private String username;
// 头像
private String avatar;
public static AdminVo init(Admin admin) {
AdminVo vo = new AdminVo();
vo.setAvatar(admin.getAvatar());
vo.setUsername(admin.getUsername());
vo.setId(admin.getId().toString());
return vo;
}
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/SystemController.java
文件:
/**
* 获取用户的信息
*/
@PostMapping("/profile")
public ResponseEntity profile() {
AdminVo vo = adminService.profile();
return ResponseEntity.ok(vo);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/AdminService.java
文件:
// 获取用户信息
public AdminVo profile() {
Long userId = AdminHolder.getUserId();
Admin admin = adminMapper.selectById(userId);
return AdminVo.init(admin);
}
后台管理系统可以对所有注册用户进行统一管理。如查看用户列表,用户详情,用户发布的视频/动态等
新建 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java
文件:
@RestController
@RequestMapping("/manage")
public class ManageController {
@Autowired
private ManagerService managerService;
/**
* 查询用户列表
*/
@GetMapping("/users")
public ResponseEntity users(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pagesize) {
PageResult result = managerService.findAllUsers(page,pagesize);
return ResponseEntity.ok(result);
}
}
新建 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java
文件:
@Service
public class ManagerService {
@DubboReference
private UserInfoApi userInfoApi;
// 查询用户列表
public PageResult findAllUsers(Integer page, Integer pagesize) {
IPage iPage = userInfoApi.findALL(page, pagesize);
return new PageResult(page, pagesize, iPage.getTotal(), iPage.getRecords());
}
}
编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/UserInfoApi.java
文件:
// 分页查询
IPage findALL(Integer page, Integer pagesize);
编辑 tanhua-dubbo/tanhua-dubbo-db/src/main/java/com/tanhua/dubbo/api/UserInfoApiImpl.java
文件:
// 分页查询
@Override
public IPage findALL(Integer page, Integer pagesize) {
return userInfoMapper.selectPage(new Page<UserInfo>(page, pagesize), null);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java
文件:
/**
* 查询用户详情
*/
@GetMapping("/users/{userId}")
public ResponseEntity findUserById(@PathVariable("userId") Long userId) {
UserInfo userInfo = managerService.findUserById(userId);
return ResponseEntity.ok(userInfo);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java
文件:
// 查询用户详情
public UserInfo findUserById(Long userId) {
return userInfoApi.findById(userId);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java
文件:
/**
* 查询视频列表
*/
@GetMapping("/videos")
public ResponseEntity videos(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pagesize,
Long uid ) {
PageResult result = managerService.findAllVideos(page,pagesize,uid);
return ResponseEntity.ok(result);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java
文件:
// 查询视频列表
public PageResult findAllVideos(Integer page, Integer pagesize, Long uid) {
return videoApi.findByUserId(page, pagesize, uid);
}
编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/VideoApi.java
文件:
// 根据用户id查询
PageResult findByUserId(Integer page, Integer pagesize, Long userId);
编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/VideoApiImpl.java
文件:
// 根据用户id查询
@Override
public PageResult findByUserId(Integer page, Integer pagesize, Long userId) {
Query query = Query.query(Criteria.where("userId").in(userId));
long count = mongoTemplate.count(query, Video.class);
query.skip((page - 1) * pagesize).limit(pagesize)
.with(Sort.by(Sort.Order.desc("created")));
List<Video> list = mongoTemplate.find(query, Video.class);
return new PageResult(page, pagesize, count, list);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java
文件:
/**
* 查询动态列表
*/
@GetMapping("/messages")
public ResponseEntity messages(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pagesize,
Long uid,Integer state ) {
PageResult result = managerService.findAllMovements(page,pagesize,uid,state);
return ResponseEntity.ok(result);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java
文件:
// 查询动态列表
public PageResult findAllMovements(Integer page, Integer pagesize, Long uid, Integer state) {
//1、调用API查询数据 :Movment对象
PageResult result = movementApi.findByUserId(uid, state, page, pagesize);
//2、解析PageResult,获取Movment对象列表
List<Movement> items = (List<Movement>) result.getItems();
//3、一个Movment对象转化为一个Vo
if(CollUtil.isEmpty(items)) {
return new PageResult();
}
List<Long> userIds = CollUtil.getFieldValues(items, "userId", Long.class);
Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null);
List<MovementsVo> vos = new ArrayList<>();
for (Movement movement : items) {
UserInfo userInfo = map.get(movement.getUserId());
if(userInfo != null) {
MovementsVo vo = MovementsVo.init(userInfo, movement);
vos.add(vo);
}
}
//4、构造返回值
result.setItems(vos);
return result;
}
编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/MovementApi.java
文件:
// 根据用户id,查询
PageResult findByUserId(Long uid, Integer state, Integer page, Integer pagesize);
编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/MovementApiImpl.java
文件:
// 根据用户id,查询
@Override
public PageResult findByUserId(Long uid, Integer state, Integer page, Integer pagesize) {
Query query = new Query();
if(uid != null) {
query.addCriteria(Criteria.where("userId").is(uid));
}
if(state != null) {
query.addCriteria(Criteria.where("state").is(state));
}
long count = mongoTemplate.count(query, Movement.class);
query.with(Sort.by(Sort.Order.desc("created"))).limit(pagesize).skip((page -1) * pagesize);
List<Movement> list = mongoTemplate.find(query, Movement.class);
return new PageResult(page,pagesize,count,list);
}
用户冻结/解冻是管理员在后台系统对用户的惩罚措施。对于发布不当言论或者违法违规内容的用户,可以暂时、永久禁止其登录,评论,发布动态、
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java
文件:
/**
* 用户冻结
*/
@PostMapping("/users/freeze")
public ResponseEntity freeze(@RequestBody Map params) {
Map map = managerService.userFreeze(params);
return ResponseEntity.ok(map);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java
文件:
// 用户冻结
public Map userFreeze(Map params) {
// 1. 构造key
String userId = params.get("userId").toString();
String key = Constants.USER_FREEZE + userId;
// 2. 构造失效时间
Integer freezingTime = Integer.valueOf(params.get("freezingTime").toString()); // 冻结时间: 1为冻3天,2为冻7天,3为冻永久
int days = 0;
if (freezingTime == 1) {
days = 3;
} else if (freezingTime == 2) {
days = 7;
}
// 3. 将数据存入redis
String value = JSON.toJSONString(params);
if(days > 0) {
redisTemplate.opsForValue().set(key, value, days, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(key, value);
}
// 4. 构造返回值
Map retMap = new HashMap<>();
retMap.put("message", "冻结成功");
return retMap;
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/controller/ManageController.java
文件:
/**
* 用户解冻
*/
@PostMapping("/users/unfreeze")
public ResponseEntity unfreeze(@RequestBody Map params) {
Map map = managerService.userUnfreeze(params);
return ResponseEntity.ok(map);
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/service/ManagerService.java
文件:
// 用户解冻
public Map userUnfreeze(Map params) {
// 1. 构造key
String userId = params.get("userId").toString();
String key = Constants.USER_FREEZE + userId;
// 2. 删除redis数据
redisTemplate.delete(key);
// 3. 构造返回值
Map retMap = new HashMap<>();
retMap.put("message", "解冻成功");
return retMap;
}
新建 tanhua-app-server/src/main/java/com/tanhua/server/service/UserFreezeService.java
文件:
@Service
public class UserFreezeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void checkUserStatus(String state, Long userId) {
// 1. 拼接key, 查询redis数据
String key = Constants.USER_FREEZE + userId;
String value = redisTemplate.opsForValue().get(key);
// 2. 如果数据存在,且冻结范围一致,抛出异常
if(!StringUtils.isEmpty(value)) {
Map map = JSON.parseObject(value, Map.class);
String freezingRange = (String) map.get("freezingRange");
if(state.equals(freezingRange)) {
throw new BusinessException(ErrorResult.builder().errMessage("您的账号被冻结!").build());
}
}
}
}
编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/UserService.java
文件:
/**
* 发送短信验证码
* @param phone
*/
public void sendMsg(String phone) {
// 校验用户是否被冻结
User user = userApi.findByMobile(phone);
if(user != null) {
userFreezeService.checkUserStatus("1", user.getId());
}
// 1. 随机生成6位数数字
// String code = RandomStringUtils.randomNumeric(6);
// !!! 项目开发不用真正实现短信发送
String code = "123456";
// 2. 调用template对象, 发送验证码
// !!! 项目开发不用真正实现短信发送
// template.sendSms(phone, code);
// 3. 将验证码存入redis
redisTemplate.opsForValue().set("CHECK_CODE_"+phone,code, Duration.ofMinutes(5));
}
后台系统首页中,显示各种统计数据,比如:累计用户数、新增用户数、登录次数等内容
0101-登录,0102-注册,0201-发动态,0202-查看,0203-点赞,0204-喜欢,0205-评论,0206-取消点赞,0207-取消喜欢0301-发视频,0302-视频点赞,0303-视频取消点赞,0304-视频评论
探花交友项目所需的第三方服务组件,已经以Docker-Compose准备好了。仅仅需要进入相关目录,以命令形式启动运行即可
# 进入目录
cd /root/docker-file/rmq/
# 创建容器并启动
docker-compose up –d
# 查看容器
docker ps -a
探花项目间使用RabbitMQ收发消息,这里采用topic类型消息
日志消息key规则:log.xxx
编辑 tanhua-app-server/src/main/resources/application.yml
文件:
server:
port: 18080
spring:
profiles:
active: prod
application:
name: tanhua-app-server
cloud:
nacos:
discovery:
server-addr: 192.168.136.160:8848
config:
server-addr: 192.168.136.160:8848
file-extension: yml
新建 tanhua-app-server/src/main/java/com/tanhua/server/service/MqMessageService.java
文件:
@Service
public class MqMessageService {
@Autowired
private AmqpTemplate amqpTemplate;
//发送日志消息
public void sendLogMessage(Long userId,String type,String key,String busId) {
try {
Map map = new HashMap();
map.put("userId",userId.toString());
map.put("type",type);
map.put("logTime",new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
map.put("busId",busId);
String message = JSON.toJSONString(map);
amqpTemplate.convertAndSend("tanhua.log.exchange",
"log."+key,message);
} catch (AmqpException e) {
e.printStackTrace();
}
}
//发送动态审核消息
public void sendAudiMessage(String movementId) {
try {
amqpTemplate.convertAndSend("tanhua.audit.exchange",
"audit.movement",movementId);
} catch (AmqpException e) {
e.printStackTrace();
}
}
}
编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/UserService.java
文件:
/**
* 验证登录
* @param phone
* @param code
*/
public Map loginVerification(String phone, String code) {
// 1. 从redis中获取验证码
String redisCode = redisTemplate.opsForValue().get("CHECK_CODE_" + phone);
// 2. 对验证码进行校验
if(StringUtils.isEmpty(redisCode) || !redisCode.equals(code)) {
// throw new RuntimeException("验证码错误");
throw new BusinessException(ErrorResult.loginError());
}
// 3. 删除redis中的验证码
redisTemplate.delete("CHECK_CODE_" + phone);
// 4. 通过手机号查询用户
User user = userApi.findByMobile(phone);
boolean isNew = false;
// 5. 如果用户不存在,创建用户保存到数据库
String type = "0101"; // 登录
if(user == null) {
type = "0102"; // 注册
user = new User();
user.setMobile(phone);
// user.setCreated(new Date());
// user.setUpdated(new Date());
user.setPassword(DigestUtils.md5Hex("123456"));
Long userId = userApi.save(user);
user.setId(userId);
isNew = true;
// 注册环信用户
String hxUser = "hx" + user.getId();
Boolean create = huanXinTemplate.createUser(hxUser, Constants.INIT_PASSWORD);
if(create) {
user.setHxUser(hxUser);
user.setHxPassword(Constants.INIT_PASSWORD);
userApi.update(user);
}
}
// 发送日志消息
// try {
// Map map = new HashMap<>();
// map.put("userId", user.getId().toString());
// map.put("type", type);
// map.put("logTime", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
// String message = JSON.toJSONString(map);
// amqpTemplate.convertAndSend("tanhua.log.exchange", "log.user", message);
// } catch (Exception e) {
// e.printStackTrace();
// }
mqMessageService.sendLogMessage(user.getId(), type, "user", null);
// 6. 通过JWT生成token(存入手机号和用户ID)
Map tokenMap = new HashMap();
tokenMap.put("id", user.getId());
tokenMap.put("mobile", phone);
String token = JwtUtils.getToken(tokenMap);
// 7. 构造返回值
Map retMap = new HashMap();
retMap.put("token", token);
retMap.put("isNew", isNew);
return retMap;
}
编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/MovementService.java
文件:
// 查询单条动态
public MovementsVo findById(String movementId) {
// 发送日志
mqMessageService.sendLogMessage(UserHolder.getUserId(), "0202", "movement", movementId);
// 1. 调用API查询动态详情
Movement movement = movementApi.findById(movementId);
// 2. 转换vo对象
if(movement != null) {
UserInfo userInfo = userInfoApi.findById(movement.getUserId());
return MovementsVo.init(userInfo, movement);
} else {
return null;
}
}
新建 tanhua-model/src/main/java/com/tanhua/model/domain/Log.java
文件:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Log {
/**
* id
*/
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 操作时间
*/
private String logTime;
/**
* 操作类型,
* 0101为登录,0102为注册,
* 0201为发动态,0202为浏览动态,0203为动态点赞,0204为动态喜欢,0205为评论,0206为动态取消点赞,0207为动态取消喜欢,
* 0301为发小视频,0302为小视频点赞,0303为小视频取消点赞,0304为小视频评论
*/
private String type;
/**
* 登陆地点
*/
private String place;
/**
* 登陆设备
*/
private String equipment;
public Log(Long userId, String logTime, String type) {
this.userId = userId;
this.logTime = logTime;
this.type = type;
}
}
新建 tanhua-model/src/main/java/com/tanhua/model/domain/Analysis.java
文件:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Analysis{
private Long id;
/**
* 日期
*/
private Date recordDate;
/**
* 新注册用户数
*/
private Integer numRegistered = 0;
/**
* 活跃用户数
*/
private Integer numActive = 0;
/**
* 登陆次数
*/
private Integer numLogin = 0;
/**
* 次日留存用户数
*/
private Integer numRetention1d = 0;
private Date created;
}
新建 tanhua-admin/src/main/java/com/tanhua/admin/mapper/LogMapper.java
文件:
public interface LogMapper extends BaseMapper<Log> {
}
新建 tanhua-admin/src/main/java/com/tanhua/admin/mapper/AnalysisMapper.java
文件:
public interface AnalysisMapper extends BaseMapper<Analysis> {
}
新建 tanhua-admin/src/main/java/com/tanhua/admin/listener/LogListener.java
文件:
@Component
public class LogListener {
@Autowired
private LogMapper logMapper;
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(
value = "tanhua.log.queue",
durable = "true"
),
exchange = @Exchange(
value = "tanhua.log.exchange",
type = ExchangeTypes.TOPIC),
key = {"log.*"}
)
)
public void log(String message) {
try {
Map map = JSON.parseObject(message, Map.class);
map.forEach((k, v) -> System.out.println(k + "--" + v));
// 1. 解析map获取数据
Long userId = Long.valueOf(map.get("userId").toString());
String type = (String) map.get("type");
String logTime = (String) map.get("logTime");
// 2. 构造log对象,保存到数据库
Log log = new Log(userId, logTime, type);
logMapper.insert(log);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
定时任务调度,一句话概括就是:基于给定的时间点、给定的时间间隔、自动执行的任务
编辑 tanhua-admin/src/main/java/com/tanhua/admin/AdminServerApplication.java
文件:
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
@MapperScan("com.tanhua.admin.mapper")
@EnableScheduling // 开启定时任务
public class AdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServerApplication.class,args);
}
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/task/AnalysisTask.java
文件:
@Component
public class AnalysisTask {
/**
* 配置时间规则: 每5秒执行一次
*/
@Scheduled( cron = "0/5 * * * * ? ")
public void analysis() {
//业务逻辑
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
System.out.println("当前时间:"+time);
}
}
Cron 表达式支持到六个域
名称 | 是否必须 | 允许值 | 特殊字符 |
---|---|---|---|
秒 | 是 | 0-59 | , - * / |
分 | 是 | 0-59 | , - * / |
时 | 是 | 0-23 | , - * / |
日 | 是 | 1-31 | , - * ? / L W C |
月 | 是 | 1-12 或 JAN-DEC | , - * / |
周 | 是 | 1-7 或 SUN-SAT | , - * ? / L C # |
*
:匹配该域的任意值?
:忽略该域,只能用在周和日两个域。因为二者会相互影响-
:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次/
:表示起始时间开始触发,然后每隔固定时间触发一次,
:表示列出枚举值示例:
0 15 10 ? * *
- 每天上午10:15触发0 15 10 * * ?
- 每天上午10:15触发0 * 14 * * ?
- 在每天下午2点到下午2:59期间的每1分钟触发0 0/5 14 * * ?
- 在每天下午2点到下午2:55期间的每5分钟触发0 0/5 14,18 * * ?
- 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发编辑 tanhua-admin/src/main/java/com/tanhua/admin/task/AnalysisTask.java
文件:
@Component
public class AnalysisTask {
@Autowired
private AnalysisService analysisService;
/**
* 配置时间规则: 每5秒执行一次
*/
@Scheduled( cron = "0/10 * * * * ? ")
public void analysis() {
//业务逻辑
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
System.out.println("当前时间:"+time);
try {
analysisService.analysis();
} catch (Exception e) {
e.printStackTrace();
}
}
}
编辑 tanhua-admin/src/main/java/com/tanhua/admin/mapper/LogMapper.java
文件:
public interface LogMapper extends BaseMapper<Log> {
@Select("SELECT COUNT(DISTINCT user_id) FROM tb_log WHERE TYPE=#{type} AND log_time=#{logTime}")
Integer queryByTypeAndLogTime(@Param("type") String type, @Param("logTime") String logTime); //根据操作时间和类型
@Select("SELECT COUNT(DISTINCT user_id) FROM tb_log WHERE log_time=#{logTime}")
Integer queryByLogTime(String logTime); //展示记录时间查询
@Select("SELECT COUNT(DISTINCT user_id) FROM tb_log WHERE log_time=#{today} AND user_id IN (\n " +
" SELECT user_id FROM tb_log WHERE TYPE=\"0102\" AND log_time=#{yestoday} \n " +
")")
Integer queryNumRetention1d(@Param("today") String today,@Param("yestoday") String yestoday); //查询次日留存
}
新建 tanhua-admin/src/main/java/com/tanhua/admin/service/AnalysisService.java
文件:
@Service
public class AnalysisService {
@Autowired
private LogMapper logMapper;
@Autowired
private AnalysisMapper analysisMapper;
/**
* 定时统计日志数据到统计表中
*/
public void analysis() throws ParseException {
// 1、查询tb_log表中的数 (每日注册用户数,每日登陆用户,活跃的用户数据,次日留存的用户)
// 1.1 定义查询的日期
String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String yesTodayStr = DateUtil.yesterday().toString("yyyy-MM-dd");
// 1.2 统计数据 - 注册用户数
Integer regCount = logMapper.queryByTypeAndLogTime("0102",todayStr);
// 1.3 统计数据 - 登陆用户
Integer loginCount = logMapper.queryByTypeAndLogTime("0101",todayStr);
// 1.4 统计数据 - 活跃用户
Integer activeCount = logMapper.queryByLogTime(todayStr);
// 1.5 统计数据 - 次日留存
Integer numRetention1d = logMapper.queryNumRetention1d(todayStr, yesTodayStr);
// 2. 构造Analysis对象
// 2.1 根据日期查询书库
QueryWrapper<Analysis> qw = new QueryWrapper<Analysis>();
qw.eq("record_date", new SimpleDateFormat("yyyy-MM-dd").parse(todayStr));
Analysis analysis = analysisMapper.selectOne(qw);
// 3、完成统计数据的更新或者保存
if(analysis != null) {
// 3.1 如果存在,就更新
analysis.setNumRegistered(regCount);
analysis.setNumLogin(loginCount);
analysis.setNumActive(activeCount);
analysis.setNumRetention1d(numRetention1d);
analysisMapper.updateById(analysis);
} else {
// 3.2 如果不存在,就保存
analysis = new Analysis();
analysis.setNumRegistered(regCount);
analysis.setNumLogin(loginCount);
analysis.setNumActive(activeCount);
analysis.setNumRetention1d(numRetention1d);
analysis.setRecordDate(new SimpleDateFormat("yyyy-MM-dd").parse(todayStr));
analysis.setCreated(new Date());
analysisMapper.insert(analysis);
}
}
}
新建 tanhua-admin/src/test/java/com/tanhua/admin/LogTest.java
文件:
@RunWith(SpringRunner.class)
@SpringBootTest
public class LogTest {
@Autowired
private LogMapper logMapper;
// 输入想创建日志的日期时间
private String logTime = "2022-11-30";
//模拟登录数据
public void testInsertLoginLog() {
for (int i = 0; i < 5; i++) {
Log log = new Log();
log.setUserId((long)(i+1));
log.setLogTime(logTime);
log.setType("0101");
logMapper.insert(log);
}
}
//模拟注册数据
public void testInsertRegistLog() {
for (int i = 0; i < 10; i++) {
Log log = new Log();
log.setUserId((long)(i+1));
log.setLogTime(logTime);
log.setType("0102");
logMapper.insert(log);
}
}
//模拟其他操作
public void testInsertOtherLog() {
String[] types = new String[]{"0201","0202","0203","0204","0205","0206","0207","0301","0302","0303","0304"};
for (int i = 0; i < 10; i++) {
Log log = new Log();
log.setUserId((long)(i+1));
log.setLogTime(logTime);
int index = new Random().nextInt(10);
log.setType(types[index]);
logMapper.insert(log);
}
}
@Test
public void generData() {
testInsertLoginLog();
testInsertRegistLog();
testInsertOtherLog();
}
}
阿里云云盾官网: https://www.aliyun.com/product/lvwang
文本垃圾内容检测: https://help.aliyun.com/document_detail/53427.html
图片垃圾内容检测:https://help.aliyun.com/document_detail/53424.html
编辑 tanhua-admin/src/main/resources/bootstrap.yml
文件:
server:
port: 18083
spring:
profiles:
active: prod
application:
name: tanhua-admin
cloud:
nacos:
discovery:
server-addr: 192.168.136.160:8848
config:
server-addr: 192.168.136.160:8848
file-extension: yml
tanhua:
green:
enable: true
accessKeyID: !!!申请的key
accessKeySecret: !!!申请的keyS
scenes: porn,terrorism #色情,暴力
新建 tanhua-autoconfig/src/main/java/com/tanhua/autoconfig/properties/GreenProperties.java
文件:
@Data
@ConfigurationProperties("tanhua.green")
public class GreenProperties {
/**
* 账号
*/
String accessKeyID;
/**
* 密钥
*/
String accessKeySecret;
/**
* 场景
*/
String scenes;
}
新建 tanhua-autoconfig/src/main/java/com/tanhua/autoconfig/template/AliyunGreenTemplate.java
文件:
@Slf4j
public class AliyunGreenTemplate {
private IAcsClient client;
private GreenProperties greenProperties;
public AliyunGreenTemplate(GreenProperties greenProperties) {
this.greenProperties = greenProperties;
try {
IClientProfile profile = DefaultProfile
.getProfile("cn-shanghai", greenProperties.getAccessKeyID(), greenProperties.getAccessKeySecret());
DefaultProfile
.addEndpoint("cn-shanghai", "cn-shanghai", "Green", "green.cn-shanghai.aliyuncs.com");
client = new DefaultAcsClient(profile);
} catch (Exception e) {
e.printStackTrace();
log.error("Green配置缺失,请补充!");
}
}
/**
* 阿里云文本内容检查
*
* @param content
* @return map key - suggestion内容
* pass:文本正常,可以直接放行,
* review:文本需要进一步人工审核,
* block:文本违规,可以直接删除或者限制公开
* value - 通过,或 出错原因
* @throws Exception
*/
public Map<String, String> greenTextScan(String content) throws Exception {
TextScanRequest textScanRequest = new TextScanRequest();
textScanRequest.setAcceptFormat(FormatType.JSON); // 指定api返回格式
textScanRequest.setHttpContentType(FormatType.JSON);
textScanRequest.setMethod(MethodType.POST); // 指定请求方法
textScanRequest.setEncoding("UTF-8");
textScanRequest.setRegionId("cn-shanghai");
List<Map<String, Object>> tasks = new ArrayList<>();
Map<String, Object> task1 = new LinkedHashMap<>();
task1.put("dataId", UUID.randomUUID().toString());
/**
* 待检测的文本,长度不超过10000个字符
*/
task1.put("content", content);
tasks.add(task1);
JSONObject data = new JSONObject();
/**
* 检测场景,文本垃圾检测传递:antispam
**/
data.put("scenes", Arrays.asList("antispam"));
data.put("tasks", tasks);
log.info("检测任务内容:{}", JSON.toJSONString(data, true));
textScanRequest.setHttpContent(data.toJSONString().getBytes("UTF-8"), "UTF-8", FormatType.JSON);
// 请务必设置超时时间
textScanRequest.setConnectTimeout(3000);
textScanRequest.setReadTimeout(6000);
// 返回结果内容
Map<String, String> resultMap = new HashMap<>();
try {
HttpResponse httpResponse = client.doAction(textScanRequest);
if (!httpResponse.isSuccess()) {
new RuntimeException("阿里云文本内容检查出现异常!");
}
JSONObject scrResponse = JSON.parseObject(new String(httpResponse.getHttpContent(), "UTF-8"));
log.info("检测结果内容:{}", JSON.toJSONString(scrResponse, true));
if (200 != scrResponse.getInteger("code")) {
new RuntimeException("阿里云文本内容检查出现异常!");
}
JSONArray taskResults = scrResponse.getJSONArray("data");
for (Object taskResult : taskResults) {
if (200 != ((JSONObject) taskResult).getInteger("code")) {
new RuntimeException("阿里云文本内容检查出现异常!");
}
JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
for (Object sceneResult : sceneResults) {
String scene = ((JSONObject) sceneResult).getString("scene");
String label = ((JSONObject) sceneResult).getString("label");
String suggestion = ((JSONObject) sceneResult).getString("suggestion");
log.info("最终内容检测结果,suggestion = {},label={}", suggestion, label);
// 设置默认错误返回内容
resultMap.put("suggestion", suggestion);
if (suggestion.equals("review")) {
resultMap.put("reson", "文章内容中有不确定词汇");
log.info("返回结果,resultMap={}", resultMap);
return resultMap;
} else if (suggestion.equals("block")) {
String reson = "文章内容中有敏感词汇";
if (label.equals("spam")) {
reson = "文章内容中含垃圾信息";
} else if (label.equals("ad")) {
reson = "文章内容中含有广告";
} else if (label.equals("politics")) {
reson = "文章内容中含有涉政";
} else if (label.equals("terrorism")) {
reson = "文章内容中含有暴恐";
} else if (label.equals("abuse")) {
reson = "文章内容中含有辱骂";
} else if (label.equals("porn")) {
reson = "文章内容中含有色情";
} else if (label.equals("flood")) {
reson = "文章内容灌水";
} else if (label.equals("contraband")) {
reson = "文章内容违禁";
} else if (label.equals("meaningless")) {
reson = "文章内容无意义";
}
resultMap.put("reson", reson);
log.info("返回结果,resultMap={}", resultMap);
return resultMap;
}
}
}
resultMap.put("suggestion", "pass");
resultMap.put("reson", "检测通过");
} catch (Exception e) {
log.error("阿里云文本内容检查出错!");
e.printStackTrace();
new RuntimeException("阿里云文本内容检查出错!");
}
log.info("返回结果,resultMap={}", resultMap);
return resultMap;
}
/**
* 阿里云图片内容安全
*/
public Map imageScan(List<String> imageList) throws Exception {
IClientProfile profile = DefaultProfile
.getProfile("cn-shanghai", greenProperties.getAccessKeyID(), greenProperties.getAccessKeySecret());
ImageSyncScanRequest imageSyncScanRequest = new ImageSyncScanRequest();
// 指定api返回格式
imageSyncScanRequest.setAcceptFormat(FormatType.JSON);
// 指定请求方法
imageSyncScanRequest.setMethod(MethodType.POST);
imageSyncScanRequest.setEncoding("utf-8");
//支持http和https
imageSyncScanRequest.setProtocol(ProtocolType.HTTP);
JSONObject httpBody = new JSONObject();
/**
* 设置要检测的场景, 计费是按照该处传递的场景进行
* 一次请求中可以同时检测多张图片,每张图片可以同时检测多个风险场景,计费按照场景计算
* 例如:检测2张图片,场景传递porn、terrorism,计费会按照2张图片鉴黄,2张图片暴恐检测计算
* porn: porn表示色情场景检测
*/
httpBody.put("scenes", Arrays.asList(greenProperties.getScenes().split(",")));
/**
* 如果您要检测的文件存于本地服务器上,可以通过下述代码片生成url
* 再将返回的url作为图片地址传递到服务端进行检测
*/
/**
* 设置待检测图片, 一张图片一个task
* 多张图片同时检测时,处理的时间由最后一个处理完的图片决定
* 通常情况下批量检测的平均rt比单张检测的要长, 一次批量提交的图片数越多,rt被拉长的概率越高
* 这里以单张图片检测作为示例, 如果是批量图片检测,请自行构建多个task
*/
List list = new ArrayList();
for (String imageUrl : imageList) {
JSONObject task = new JSONObject();
task.put("dataId", UUID.randomUUID().toString());
// 设置图片链接。
task.put("url", imageUrl);
task.put("time", new Date());
list.add(task);
}
httpBody.put("tasks",list);
imageSyncScanRequest.setHttpContent(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(httpBody.toJSONString()),
"UTF-8", FormatType.JSON);
/**
* 请设置超时时间, 服务端全链路处理超时时间为10秒,请做相应设置
* 如果您设置的ReadTimeout小于服务端处理的时间,程序中会获得一个read timeout异常
*/
imageSyncScanRequest.setConnectTimeout(3000);
imageSyncScanRequest.setReadTimeout(10000);
HttpResponse httpResponse = null;
try {
httpResponse = client.doAction(imageSyncScanRequest);
} catch (Exception e) {
e.printStackTrace();
}
Map<String, String> resultMap = new HashMap<>();
//服务端接收到请求,并完成处理返回的结果
if (httpResponse != null && httpResponse.isSuccess()) {
JSONObject scrResponse = JSON.parseObject(org.apache.commons.codec.binary.StringUtils.newStringUtf8(httpResponse.getHttpContent()));
System.out.println(JSON.toJSONString(scrResponse, true));
int requestCode = scrResponse.getIntValue("code");
//每一张图片的检测结果
JSONArray taskResults = scrResponse.getJSONArray("data");
if (200 == requestCode) {
for (Object taskResult : taskResults) {
//单张图片的处理结果
int taskCode = ((JSONObject) taskResult).getIntValue("code");
//图片要检测的场景的处理结果, 如果是多个场景,则会有每个场景的结果
JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
if (200 == taskCode) {
for (Object sceneResult : sceneResults) {
String scene = ((JSONObject) sceneResult).getString("scene");
String label = ((JSONObject) sceneResult).getString("label");
String suggestion = ((JSONObject) sceneResult).getString("suggestion");
//根据scene和suggetion做相关处理
//do something
System.out.println("scene = [" + scene + "]");
System.out.println("suggestion = [" + suggestion + "]");
System.out.println("suggestion = [" + label + "]");
if (!suggestion.equals("pass")) {
resultMap.put("suggestion", suggestion);
resultMap.put("label", label);
return resultMap;
}
}
} else {
//单张图片处理失败, 原因视具体的情况详细分析
log.error("task process fail. task response:" + JSON.toJSONString(taskResult));
return null;
}
}
resultMap.put("suggestion", "pass");
return resultMap;
} else {
/**
* 表明请求整体处理失败,原因视具体的情况详细分析
*/
log.error("the whole image scan request failed. response:" + JSON.toJSONString(scrResponse));
return null;
}
}
return null;
}
}
编辑 tanhua-autoconfig/src/main/java/com/tanhua/autoconfig/TanhuaAutoConfiguration.java
文件:
@EnableConfigurationProperties({
SmsProperties.class,
OssProperties.class,
AipFaceProperties.class,
HuanXinProperties.class,
GreenProperties.class
})
public class TanhuaAutoConfiguration {
@Bean
public SmsTemplate smsTemplate(SmsProperties properties) {
return new SmsTemplate(properties);
}
@Bean
public OssTemplate ossTemplate(OssProperties properties) {return new OssTemplate(properties);}
@Bean
public AipFaceTemplate aipFaceTemplate() {return new AipFaceTemplate();}
@Bean
public HuanXinTemplate huanXinTemplate(HuanXinProperties properties) {return new HuanXinTemplate(properties);}
@Bean
// 配置文件中是否具有 tanhua.green 开头的配置, 并且enable = true
@ConditionalOnProperty(prefix = "tanhua.green",value = "enable", havingValue = "true")
public AliyunGreenTemplate aliyunGreenTemplate(GreenProperties properties) {
return new AliyunGreenTemplate(properties);
}
}
新建 tanhua-admin/src/test/java/com/tanhua/admin/GreenTemplateTest.java
文件:
@RunWith(SpringRunner.class)
@SpringBootTest
public class GreenTemplateTest {
@Autowired
private AliyunGreenTemplate template;
@Test
public void test() throws Exception {
// Map map = template.greenTextScan("巧笑倩兮,美目盼兮");
// Map map = template.greenTextScan("本校小额贷款,安全、快捷、方便、无抵押,随机随贷,当天放款,上门服务");
List<String> list = new ArrayList<>();
list.add("http://images.china.cn/site1000/2018-03/17/dfd4002e-f965-4e7c-9e04-6b72c601d952.jpg");
Map<String, String> map = template.imageScan(list);
map.forEach((k, v) -> System.out.println(k + "--" + v));
}
}
编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/MovementApi.java
文件:
// 发布动态
// void publish(Movement movement);
String publish(Movement movement);
编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/MovementApiImpl.java
文件:
// 发布动态
// public void publish(Movement movement) {
public String publish(Movement movement) {
try {
// 1. 保存动态详情
// 1.1 设置PID(同名时序列自增)
movement.setPid(idWorker.getNextId("movement"));
// 1.2 设置时间
movement.setCreated(System.currentTimeMillis());
// 1.3 保存动态
mongoTemplate.save(movement);
// // 2. 查询当前用户的好友数据
// Criteria criteria = Criteria.where("userId").is(movement.getUserId());
// Query query = Query.query(criteria);
// List friends = mongoTemplate.find(query, Friend.class);
//
// // 3. 循环好友数据, 构建时间线数据存入数据库
// for (Friend friend : friends) {
// MovementTimeLine timeLine = new MovementTimeLine();
// timeLine.setMovementId(movement.getId());
// timeLine.setUserId(friend.getUserId());
// timeLine.setFriendId(friend.getFriendId());
// timeLine.setCreated(System.currentTimeMillis());
// mongoTemplate.save(timeLine);
// }
// 异步多线程调用
timeLineService.saveTimeLine(movement.getUserId(), movement.getId());
} catch (Exception e) {
// 忽略事物处理
e.printStackTrace();
}
// 返回动态id
return movement.getId().toHexString();
}
编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/MovementService.java
文件:
/**
* 发布动态
*/
public void publishMovement(Movement movement, MultipartFile[] imageContent) throws IOException {
// 1. 判断发布动态的内容是否存在
if(StringUtils.isEmpty(movement.getTextContent())) {
throw new BusinessException(ErrorResult.contentError());
}
// 2. 获取当前登录的用户id
Long userId = UserHolder.getUserId();
// 3. 将文件内容上传到阿里云OSS, 获取请求地址
List<String> medias = new ArrayList<>();
for (MultipartFile multipartFile : imageContent) {
// String upload = ossTemplate.upload(multipartFile.getOriginalFilename(), multipartFile.getInputStream());
// !!! 阿里云OSS收费, 这里暂时跳过
String upload = "https://img0.baidu.com/it/u=1501084209,93021381&fm=253&fmt=auto&app=138&f=JPEG";
medias.add(upload);
}
// 4. 将数据封装到movement对象
movement.setUserId(userId);
movement.setMedias(medias);
//5. 调用API完成动态发布
// movementApi.publish(movement);
// 发送动态审核消息
String movementId = movementApi.publish(movement);
mqMessageService.sendAudiMessage(movementId);
}
新建 tanhua-admin/src/main/java/com/tanhua/admin/listener/AuditListener.java
文件:
@Component
public class AuditListener {
@DubboReference
private MovementApi movementApi;
@Autowired
private AliyunGreenTemplate aliyunGreenTemplate;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(
value = "tanhua.audit.queue",
durable = "true"
),
exchange = @Exchange(
value = "tanhua.audit.exchange",
type = ExchangeTypes.TOPIC),
key = {"audit.movement"})
)
public void audit(String movementId) throws Exception {
System.out.println("内容审核id: id=" + movementId);
try {
// 1. 根据动态id查询动态
Movement movement = movementApi.findById(movementId);
if(movement != null && movement.getState() == 0) {
// 2. 审核文本/审核图片
Map<String, String> textScan = aliyunGreenTemplate.greenTextScan(movement.getTextContent());
Map<String, String> imageScan = aliyunGreenTemplate.imageScan(movement.getMedias());
// 3. 判断审核结果
int state = 0;
if(textScan != null && imageScan != null) {
String textSuggestion = textScan.get("suggestion");
String imageSuggestion = imageScan.get("suggestion");
if ("block".equals(textSuggestion) || "block".equals(textSuggestion)){
state = 2; // 驳回
}else if("pass".equals(textSuggestion) || "pass".equals(textSuggestion)) {
state = 1; // 通过
}
}
// 4. 更新动态状态
movementApi.update(movementId, state);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
编辑 tanhua-dubbo/tanhua-dubbo-interface/src/main/java/com/tanhua/dubbo/api/MovementApi.java
文件:
// 更新动态状态
void update(String movementId, int state);
编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/MovementApiImpl.java
文件:
// 更新动态状态
@Override
public void update(String movementId, int state) {
Query query = Query.query(Criteria.where("id").is(new ObjectId(movementId)));
Update update = Update.update("state", state);
mongoTemplate.updateFirst(query, update, Movement.class);
}
编辑 tanhua-dubbo/tanhua-dubbo-mongo/src/main/java/com/tanhua/dubbo/api/MovementApiImpl.java
文件:
// 根据用户id,查询当前用户发布的动态数据列表
@Override
public PageResult findByUserId(Long userId, Integer page, Integer pagesize) {
// Criteria criteria = Criteria.where("userId").is(userId);
// 根据审核状态筛选动态
Criteria criteria = Criteria.where("userId").is(userId).and("state").is(1);
Query query = Query.query(criteria).skip((page - 1) * pagesize).limit(pagesize)
.with(Sort.by(Sort.Order.desc("created")));
List<Movement> movements = mongoTemplate.find(query, Movement.class);
return new PageResult(page, pagesize, 0l, movements);
}
/**
* 查询用户好友发布的动态数据列表
* @param friendId 当前操作用户id
* @return
*/
public List<Movement> findFriendMovements(Integer page, Integer pagesize, Long friendId) {
// 1. 根据friendId查询时间线表
Query query = Query.query(Criteria.where("friendId").is(friendId))
.skip((page - 1) * pagesize).limit(pagesize)
.with(Sort.by(Sort.Order.desc("created")));
List<MovementTimeLine> lineList = mongoTemplate.find(query, MovementTimeLine.class);
// 2. 提取动态id列表
List<ObjectId> list = CollUtil.getFieldValues(lineList, "movementId", ObjectId.class);
// 3. 根据动态id查询动态详情
// Query movementQuery = Query.query(Criteria.where("id").is(list));
// 根据审核状态筛选动态
Query movementQuery = Query.query(Criteria.where("id").is(list).and("state").is(1));
List<Movement> movementList = mongoTemplate.find(movementQuery, Movement.class);
// 4. 返回
return movementList;
}
为了解决信息过载和用户无明确需求的问题,找到用户感兴趣的物品,才有了个性化推荐系统
推荐系统广泛存在于各类网站中,作为一个应用为用户提供个性化的推荐。它需要一些用户的历史数据,一般由三个部分组成:基础数据、推荐算法系统、前台展示
迄今为止,在个性化推荐系统中,协同过滤技术是应用最成功的技术。目前国内外有许多大型网站应用这项技术为用户更加智能(个性化、千人千面)的推荐内容。
核心思想: 协同过滤一般是在海量的用户中发掘出一小部分和你品位比较类似的,在协同过滤中,这些用户成为邻居,然后根据他们喜欢的其他东西组织成一个排序的目彔作为推荐给你。
对于用户A,根据用户的历史偏好,这里只计算得到一个邻居–用户C,然后将用户C 喜欢的物品D 推荐给用户A。
行为 | 作用 |
---|---|
评分 | 通过评分,可以精准获取用户偏好 |
投票 | 投票可以较精准的得到用户偏好 |
评论 | 通过评论可以分析用户对物品的喜好 |
购买 | 用户的购买很明确的说明对物品感兴趣 |
页面停留 | 停留时间一定程度说明用户喜欢,但噪音较大 |
基于用户的协同过滤算法先计算的是用户与用户的相似度(兴趣相投,物以类聚人以群分),然后将相似度比较接近的用户A购买的物品推荐给用户B,专业的说法是该算法用最近邻居(nearest-neighbor)算法找出一个用户的邻居集合,该集合的用户和该用户有相似的喜好,算法根据邻居的偏好对该用户进行预测。
ALS算法矩阵:
评分规则:
字段 | 权重分 | |||
---|---|---|---|---|
年龄差 | 0-2岁 30分 | 3-5 20分 | 5-10岁 10分 | 10岁以上 0分 |
性别 | 异性 30分 | 同性 0分 | ||
位置 | 同城 20分 | 不同 0分 | ||
学历 | 相同 20分 | 不同 0分 |
在探花项目中已经以 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数据,2分钟后可以发现重新生成了新的推荐数据
在圈子功能中,针对于用户发布的动态信息,系统可以根据用户的发布、浏览、点赞等操作,对动态信息做计算,然后对每个用户进行不同的推荐
行为 | 评分 | 说明 |
---|---|---|
浏览 | 1分 | 查看动态详情 |
点赞 | 5分 | 动态点赞 |
喜欢 | 8分 | 动态喜欢 |
评论 | 10分 | 发布动态评论 |
发布动态 | 基础5分 | 文字长度:50以内1分,50~100之间2分,100以上3分 |
编辑 tanhua-recommend/pom.xml
文件:
<dependencies>
<dependency>
<groupId>com.itheimagroupId>
<artifactId>tanhua-modelartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.4.3version>
dependency>
dependencies>
新建 tanhua-recommend/src/main/resources/application.yml
文件:
spring:
rabbitmq:
host: 192.168.136.160
port: 5672
username: guest
password: guest
data:
mongodb:
uri: mongodb://192.168.136.160:27017/tanhua
新建 tanhua-recommend/src/main/java/com/tanhua/recommend/RecommendApplication.java
文件:
@SpringBootApplication
public class RecommendApplication {
public static void main(String[] args) {
SpringApplication.run(RecommendApplication.class,args);
}
}
新建 tanhua-model/src/main/java/com/tanhua/model/mongo/MovementScore.java
文件:
//大数据动态评分实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document("recomment_movement_score")
public class MovementScore {
private ObjectId id;
private Long userId;// 用户id
private Long movementId; //动态id,需要转化为Long类型
private Double score; //得分
private Long date; //时间戳
}
编辑 tanhua-recommend/src/main/java/com/tanhua/recommend/listener/RecommendMovementListener.java
文件:
@Component
public class RecommendMovementListener {
@Autowired
private MongoTemplate mongoTemplate;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(
value = "tanhua.movement.queue",
durable = "true"
),
exchange = @Exchange(
value = "tanhua.log.exchange",
type = ExchangeTypes.TOPIC),
key = {"log.movement"})
)
public void recommend(String message) {
Map map = JSON.parseObject(message, Map.class);
// 1. 解析数据
Long userId = Long.valueOf(map.get("userId").toString());
String type = (String) map.get("type");
String logTime = (String) map.get("logTime");
String movementId = (String) map.get("busId");
// 2. 构造MovementScore,设置评分
Movement movement = mongoTemplate.findById(movementId, Movement.class);
if(movement != null) {
MovementScore ms = new MovementScore();
ms.setUserId(userId);
ms.setMovementId(movement.getPid());
ms.setDate(System.currentTimeMillis());
ms.setScore(getScore(type,movement));
// 3. 保存到数据库
mongoTemplate.save(ms);
}
}
private static Double getScore(String type,Movement movement) {
//0201为发动态 基础5分 50以内1分,50~100之间2分,100以上3分
//0202为浏览动态, 1
//0203为动态点赞, 5
//0204为动态喜欢, 8
//0205为评论, 10
//0206为动态取消点赞, -5
//0207为动态取消喜欢 -8
Double score = 0d;
switch (type) {
case "0201":
score = 5d;
score += movement.getMedias().size();
int length = StrUtil.length(movement.getTextContent());
if (length >= 0 && length < 50) {
score += 1;
} else if (length < 100) {
score += 2;
} else {
score += 3;
}
break;
case "0202":
score = 1d;
break;
case "0203":
score = 5d;
break;
case "0204":
score = 8d;
break;
case "0205":
score = 10d;
break;
case "0206":
score = -5d;
break;
case "0207":
score = -8d;
break;
default:
break;
}
return score;
}
}
启动后,MongoDB会记录用户操作日志,根据用户操作日志推荐动态数据
行为 | 评分 | 说明 |
---|---|---|
点赞 | 5分 | 视频点赞 |
评论 | 10分 | 发布视频评论 |
发布视频 | 2分 | 发布视频 |
新建 tanhua-model/src/main/java/com/tanhua/model/mongo/VideoScore.java
文件:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document("recomment_video_score")
public class VideoScore {
private ObjectId id;
private Long userId;// 用户id
private Long videoId; //视频id,需要转化为Long类型 video中的vid字段
private Double score; //得分
private Long date; //时间戳
}
新建 tanhua-recommend/src/main/java/com/tanhua/recommend/listener/RecommendVideoListener.java
文件:
@Component
public class RecommendVideoListener {
@Autowired
private MongoTemplate mongoTemplate;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(
value = "tanhua.video.queue",
durable = "true"
),
exchange = @Exchange(
value = "tanhua.log.exchange",
type = ExchangeTypes.TOPIC),
key = {"log.video"})
)
public void recommend(String message) {
Map map = JSON.parseObject(message, Map.class);
// 1. 解析数据
Long userId = Long.valueOf(map.get("userId").toString());
String type = (String) map.get("type");
String logTime = (String) map.get("logTime");
String videoId = (String) map.get("busId");
// 2. 构造MovementScore,设置评分
Video video = mongoTemplate.findById(videoId, Video.class);
if(video != null) {
VideoScore vs = new VideoScore();
vs.setUserId(userId);
vs.setVideoId(video.getVid());
vs.setDate(System.currentTimeMillis());
vs.setScore(getScore(type));
// 3. 保存到数据库
mongoTemplate.save(vs);
}
}
private static Double getScore(String type) {
//0301为发小视频,0302为小视频点赞,0303为小视频取消点赞,0304为小视频评论
Double score = 0d;
switch (type) {
case "0301":
score=2d;
break;
case "0302":
score=5d;
break;
case "0303":
score = -5d;
break;
case "0304":
score = 10d;
break;
default:
break;
}
return score;
}
}
编辑 tanhua-app-server/src/main/java/com/tanhua/server/service/SmallVideosService.java
文件:
/**
* 上传视频
* @param videoThumbnail 视频封面图片
* @param videoFile 视频文件
*/
@CacheEvict(value="videoList",allEntries = true) // 清空缓存
public void saveVideos(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException {
if(videoThumbnail.isEmpty() || videoFile.isEmpty()) {
throw new BusinessException(ErrorResult.error());
}
// 1. 将视频上传到FastDFS,获取访问url
String filename = videoFile.getOriginalFilename();
filename = filename.substring(filename.lastIndexOf(".") + 1);
StorePath storePath = client.uploadFile(videoFile.getInputStream(), videoFile.getSize(), filename, null);
String videoUrl = webServer.getWebServerUrl() + storePath.getFullPath();
// 2. 将封面图片上传到阿里云OSS,获取访问的url
// String upload = ossTemplate.upload(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream());
// // !!! 阿里云OSS收费, 这里暂时跳过
String upload = "https://img0.baidu.com/it/u=8672387,2873147723&fm=253&fmt=auto&app=138&f=JPEG";
// 3. 构建Videos对象
Video video = new Video();
video.setUserId(UserHolder.getUserId());
video.setVideoUrl(videoUrl);
video.setPicUrl(upload);
video.setText("我就是我, 是颜色不一样的烟火");
// 4. 调用API保存数据
String videoId = videoApi.save(video);
if(StringUtils.isEmpty(videoId)) {
throw new BusinessException(ErrorResult.error());
}
// 发送日志消息
mqMessageService.sendLogMessage(UserHolder.getUserId(), "0301", "video", videoId);
}
启动后,MongoDB会记录用户操作日志,根据用户操作日志推荐视频数据