项目基础搭建

This commit is contained in:
zhongzb
2023-04-22 21:13:22 +08:00
parent 783ad1ee26
commit ceb28ace8b
149 changed files with 8026 additions and 0 deletions

114
mallchat-common/pom.xml Normal file
View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mallchat</artifactId>
<groupId>com.abin.mallchat</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mallchat-common</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<!-- MyBatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<exclusions>
<exclusion>
<artifactId>mybatis-plus-extension</artifactId>
<groupId>com.baomidou</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--JWT(Json Web Token)登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!-- 阿里云OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<!--使用Swagger2-->
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.0</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,60 @@
package com.abin.mallchat.common.chat.dao;
import cn.hutool.core.util.StrUtil;
import com.abin.mallchat.common.chat.domain.entity.Message;
import com.abin.mallchat.common.chat.domain.enums.MessageStatusEnum;
import com.abin.mallchat.common.chat.mapper.MessageMapper;
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
import com.abin.mallchat.common.common.utils.CursorUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 消息表 服务实现类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-25
*/
@Service
public class MessageDao extends ServiceImpl<MessageMapper, Message> {
@Autowired
private CursorUtils cursorUtils;
public CursorPageBaseResp<Message> getCursorPage(Long roomId, CursorPageBaseReq request) {
return cursorUtils.getCursorPageByMysql(this, request, wrapper -> {
wrapper.eq(Message::getRoomId, roomId);
wrapper.eq(Message::getStatus, MessageStatusEnum.NORMAL.getStatus());
}, Message::getId);
}
/**
* 乐观更新消息类型
*/
public boolean riseOptimistic(Long id, Integer oldType, Integer newType) {
return lambdaUpdate()
.eq(Message::getId, id)
.eq(Message::getType, oldType)
.set(Message::getType, newType)
.update();
}
public Integer getGapCount(Long roomId, Long fromId, Long toId) {
return lambdaQuery()
.eq(Message::getRoomId, roomId)
.gt(Message::getId, fromId)
.le(Message::getId, toId)
.count();
}
public void updateGapCount(Long id, Integer gapCount) {
lambdaUpdate()
.eq(Message::getId, id)
.set(Message::getGapCount, gapCount);
}
}

View File

@@ -0,0 +1,43 @@
package com.abin.mallchat.common.chat.dao;
import com.abin.mallchat.common.chat.domain.entity.MessageMark;
import com.abin.mallchat.common.chat.mapper.MessageMarkMapper;
import com.abin.mallchat.common.chat.service.IMessageMarkService;
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 消息标记表 服务实现类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-04-08
*/
@Service
public class MessageMarkDao extends ServiceImpl<MessageMarkMapper, MessageMark> {
public MessageMark get(Long uid, Long msgId, Integer markType) {
return lambdaQuery().eq(MessageMark::getUid, uid)
.eq(MessageMark::getMsgId, msgId)
.eq(MessageMark::getType, markType)
.one();
}
public Integer getMarkCount(Long msgId, Integer markType) {
return lambdaQuery().eq(MessageMark::getMsgId, msgId)
.eq(MessageMark::getType, markType)
.eq(MessageMark::getStatus, YesOrNoEnum.NO.getStatus())
.count();
}
public List<MessageMark> getValidMarkByMsgIdBatch(List<Long> msgIds) {
return lambdaQuery()
.in(MessageMark::getMsgId, msgIds)
.eq(MessageMark::getStatus, YesOrNoEnum.NO.getStatus())
.list();
}
}

View File

@@ -0,0 +1,33 @@
package com.abin.mallchat.common.chat.dao;
import com.abin.mallchat.common.chat.domain.entity.Message;
import com.abin.mallchat.common.chat.domain.entity.Room;
import com.abin.mallchat.common.chat.domain.enums.MessageStatusEnum;
import com.abin.mallchat.common.chat.domain.enums.RoomTypeEnum;
import com.abin.mallchat.common.chat.mapper.RoomMapper;
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
import com.abin.mallchat.common.common.utils.CursorUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 会话表 服务实现类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-25
*/
@Service
public class RoomDao extends ServiceImpl<RoomMapper, Room> {
@Autowired
private CursorUtils cursorUtils;
public CursorPageBaseResp<Room> getCursorPage(CursorPageBaseReq request) {
return cursorUtils.getCursorPageByMysql(this, request, wrapper -> {
wrapper.ne(Room::getType, RoomTypeEnum.GROUP.getStatus());
}, Room::getActiveTime);
}
}

View File

@@ -0,0 +1,96 @@
package com.abin.mallchat.common.chat.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.util.Date;
import lombok.*;
import lombok.experimental.Accessors;
/**
* <p>
* 消息表
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-25
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("message")
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Message implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 会话表id
*/
@TableField("room_id")
private Long roomId;
/**
* 消息发送者uid
*/
@TableField("from_uid")
private Long fromUid;
/**
* 消息内容
*/
@TableField("content")
private String content;
/**
* 回复的消息内容
*/
@TableField("reply_msg_id")
private Long replyMsgId;
/**
* 消息状态 0正常 1删除
* @see com.abin.mallchat.common.chat.domain.enums.MessageStatusEnum
*/
@TableField("status")
private Integer status;
/**
* 与回复消息的间隔条数
*/
@TableField("gap_count")
private Integer gapCount;
/**
* 消息类型 1正常文本 2.爆赞 点赞超过103.危险发言举报超5
* @see com.abin.mallchat.common.chat.domain.enums.MessageTypeEnum
*/
@TableField("type")
private Integer type;
/**
* 创建时间
*/
@TableField("create_time")
private Date createTime;
/**
* 修改时间
*/
@TableField("update_time")
private Date updateTime;
}

View File

@@ -0,0 +1,75 @@
package com.abin.mallchat.common.chat.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.Accessors;
/**
* <p>
* 消息标记表
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-04-08
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("message_mark")
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MessageMark implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 消息表id
*/
@TableField("msg_id")
private Long msgId;
/**
* 标记人uid
*/
@TableField("uid")
private Long uid;
/**
* 标记类型 1点赞 2举报
* @see com.abin.mallchat.common.chat.domain.enums.MessageMarkTypeEnum
*/
@TableField("type")
private Integer type;
/**
* 消息状态 0正常 1取消
*/
@TableField("status")
private Integer status;
/**
* 创建时间
*/
@TableField("create_time")
private LocalDateTime createTime;
/**
* 修改时间
*/
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,67 @@
package com.abin.mallchat.common.chat.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
* 会话表
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-25
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("room")
public class Room implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 会话名
*/
@TableField("name")
private String name;
/**
* 会话类型 1大群聊 2沸点
* @see com.abin.mallchat.common.chat.domain.enums.RoomTypeEnum
*/
@TableField("type")
private Integer type;
/**
* 最后活跃时间-排序
*/
@TableField("active_time")
private Date activeTime;
/**
* 创建时间
*/
@TableField("create_time")
private Date createTime;
/**
* 修改时间
*/
@TableField("update_time")
private Date updateTime;
}

View File

@@ -0,0 +1,37 @@
package com.abin.mallchat.common.chat.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: 消息标记类型
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum MessageMarkTypeEnum {
LIKE(1, "点赞",10,MessageTypeEnum.LIKE),
DISLIKE(2, "点踩",5,MessageTypeEnum.DISLIKE),
;
private final Integer type;
private final String desc;
private final Integer riseNum;//需要多少个标记升级
private final MessageTypeEnum riseEnum;//消息升级成什么类型的消息
private static Map<Integer, MessageMarkTypeEnum> cache;
static {
cache = Arrays.stream(MessageMarkTypeEnum.values()).collect(Collectors.toMap(MessageMarkTypeEnum::getType, Function.identity()));
}
public static MessageMarkTypeEnum of(Integer type) {
return cache.get(type);
}
}

View File

@@ -0,0 +1,35 @@
package com.abin.mallchat.common.chat.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: 消息状态
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum MessageStatusEnum {
NORMAL(0, "正常"),
DELETE(1, "删除"),
;
private final Integer status;
private final String desc;
private static Map<Integer, MessageStatusEnum> cache;
static {
cache = Arrays.stream(MessageStatusEnum.values()).collect(Collectors.toMap(MessageStatusEnum::getStatus, Function.identity()));
}
public static MessageStatusEnum of(Integer type) {
return cache.get(type);
}
}

View File

@@ -0,0 +1,36 @@
package com.abin.mallchat.common.chat.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: 消息状态
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum MessageTypeEnum {
NORMAL(1, "正常"),
LIKE(2, "爆赞"),
DISLIKE(3, "危险发言"),
;
private final Integer type;
private final String desc;
private static Map<Integer, MessageTypeEnum> cache;
static {
cache = Arrays.stream(MessageTypeEnum.values()).collect(Collectors.toMap(MessageTypeEnum::getType, Function.identity()));
}
public static MessageTypeEnum of(Integer type) {
return cache.get(type);
}
}

View File

@@ -0,0 +1,35 @@
package com.abin.mallchat.common.chat.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: 消息状态
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum RoomTypeEnum {
GROUP(1, "大群聊"),
BOILING(2, "沸点"),
;
private final Integer status;
private final String desc;
private static Map<Integer, RoomTypeEnum> cache;
static {
cache = Arrays.stream(RoomTypeEnum.values()).collect(Collectors.toMap(RoomTypeEnum::getStatus, Function.identity()));
}
public static RoomTypeEnum of(Integer type) {
return cache.get(type);
}
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.chat.mapper;
import com.abin.mallchat.common.chat.domain.entity.Message;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 消息表 Mapper 接口
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-25
*/
public interface MessageMapper extends BaseMapper<Message> {
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.chat.mapper;
import com.abin.mallchat.common.chat.domain.entity.MessageMark;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 消息标记表 Mapper 接口
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-04-08
*/
public interface MessageMarkMapper extends BaseMapper<MessageMark> {
}

View File

@@ -0,0 +1,17 @@
package com.abin.mallchat.common.chat.mapper;
import com.abin.mallchat.common.chat.domain.entity.Room;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 会话表 Mapper 接口
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-25
*/
public interface RoomMapper extends BaseMapper<Room> {
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.chat.service;
import com.abin.mallchat.common.chat.domain.entity.MessageMark;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 消息标记表 服务类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-04-08
*/
public interface IMessageMarkService extends IService<MessageMark> {
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.chat.service;
import com.abin.mallchat.common.chat.domain.entity.Message;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 消息表 服务类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-25
*/
public interface IMessageService extends IService<Message> {
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.chat.service;
import com.abin.mallchat.common.chat.domain.entity.Room;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 会话表 服务类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-25
*/
public interface IRoomService extends IService<Room> {
}

View File

@@ -0,0 +1,59 @@
package com.abin.mallchat.common.common.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 频控注解
*/
@Repeatable(FrequencyControlContainer.class)//可重复
@Retention(RetentionPolicy.RUNTIME)//运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface FrequencyControl {
/**
* key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做频控,就自己指定
*
* @return key的前缀
*/
String prefixKey() default "";
/**
* 频控对象默认el表达指定具体的频控对象
* 对于ip 和uid模式需要是http入口的对象保证RequestHolder里有值
*
* @return 对象
*/
Target target() default Target.EL;
/**
* springEl 表达式target=EL必填
*
* @return 表达式
*/
String spEl() default "";
/**
* 频控时间范围,默认单位秒
*
* @return 时间范围
*/
int time();
/**
* 频控时间单位,默认秒
*
* @return 单位
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 单位时间内最大访问次数
*
* @return 次数
*/
int count();
enum Target {
UID, IP, EL
}
}

View File

@@ -0,0 +1,10 @@
package com.abin.mallchat.common.common.annotation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)//运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface FrequencyControlContainer {
FrequencyControl[] value();
}

View File

@@ -0,0 +1,91 @@
package com.abin.mallchat.common.common.aspect;
import cn.hutool.core.util.StrUtil;
import com.abin.mallchat.common.common.annotation.FrequencyControl;
import com.abin.mallchat.common.common.exception.BusinessException;
import com.abin.mallchat.common.common.exception.CommonErrorEnum;
import com.abin.mallchat.common.common.utils.RedisUtils;
import com.abin.mallchat.common.common.utils.RequestHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.*;
/**
* Description: 频控实现
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-20
*/
@Slf4j
@Aspect
@Component
public class FrequencyControlAspect {
@Autowired
private RedisUtils redisUtils;
private final ExpressionParser parser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(com.abin.mallchat.common.common.annotation.FrequencyControl)||@annotation(com.abin.mallchat.common.common.annotation.FrequencyControlContainer)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
FrequencyControl[] annotationsByType = method.getAnnotationsByType(FrequencyControl.class);
Map<String, FrequencyControl> keyMap = new HashMap<>();
for (int i = 0; i < annotationsByType.length; i++) {
FrequencyControl frequencyControl = annotationsByType[i];
String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? method.toGenericString() + ":index:" + i : frequencyControl.prefixKey();//默认方法限定名+注解排名(可能多个)
String key = "";
switch (frequencyControl.target()) {
case EL:
key = parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl());
break;
case IP:
key = RequestHolder.get().getIp();
break;
case UID:
key = RequestHolder.get().getUid().toString();
}
keyMap.put(prefix + ":" + key, frequencyControl);
}
//批量获取redis统计的值
ArrayList<String> keyList = new ArrayList<>(keyMap.keySet());
List<Integer> countList = redisUtils.mget(keyList, Integer.class);
for (int i = 0; i < keyList.size(); i++) {
String key = keyList.get(i);
Integer count = countList.get(i);
FrequencyControl frequencyControl = keyMap.get(key);
if (Objects.nonNull(count) && count >= frequencyControl.count()) {//频率超过了
log.warn("frequencyControl limit key:{},count:{}", key, count);
throw new BusinessException(CommonErrorEnum.FREQUENCY_LIMIT);
}
}
try {
return joinPoint.proceed();
} finally {
//不管成功还是失败,都增加次数
keyMap.forEach((k, v) -> {
redisUtils.inc(k,v.time(),v.unit());
});
}
}
private String parseSpEl(Method method, Object[] args, String spEl) {
String[] params = parameterNameDiscoverer.getParameterNames(method);//解析参数名
EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去
}
Expression expression = parser.parseExpression(spEl);
return expression.getValue(context, String.class);
}
}

View File

@@ -0,0 +1,30 @@
package com.abin.mallchat.common.common.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.concurrent.TimeUnit;
@EnableCaching
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
@Bean("caffeineCacheManager")
@Primary
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 方案一(常用)定制化缓存Cache
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.initialCapacity(100)
.maximumSize(200));
return cacheManager;
}
}

View File

@@ -0,0 +1,20 @@
package com.abin.mallchat.common.common.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* 新增分页拦截器并设置数据库类型为mysql
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@@ -0,0 +1,58 @@
package com.abin.mallchat.common.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.SerializationUtils;
import org.springframework.util.Assert;
import java.net.UnknownHostException;
import java.util.Objects;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 创建模板
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置序列化工具
MyRedisSerializerCustomized jsonRedisSerializer =
new MyRedisSerializerCustomized();
// key和 hashKey采用 string序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// value和 hashValue采用 JSON序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
public class MyRedisSerializerCustomized extends GenericJackson2JsonRedisSerializer {
@Override
public byte[] serialize(Object source) throws SerializationException {
if (Objects.nonNull(source)) {
if (source instanceof String || source instanceof Character) {
return source.toString().getBytes();
}
}
return super.serialize(source);
}
@Override
public <T> T deserialize(byte[] source, Class<T> type) throws SerializationException {
Assert.notNull(type,
"Deserialization type must not be null! Please provide Object.class to make use of Jackson2 default typing.");
if (source == null || source.length == 0) {
return null;
}
if (type.isInstance(String.class) || type.isInstance(Character.class)) {
return (T) new String(source);
}
return super.deserialize(source, type);
}
}
}

View File

@@ -0,0 +1,59 @@
package com.abin.mallchat.common.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* Description: 线程池配置
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-09
*/
@Configuration
@EnableAsync
public class ThreadPoolConfig implements AsyncConfigurer {
/**
* 项目共用线程池
*/
public static final String MALLCHAT_EXECUTOR = "mallchatExecutor";
/**
* websocket通信线程池
*/
public static final String WS_EXECUTOR = "websocketExecutor";
@Override
public Executor getAsyncExecutor() {
return mallchatExecutor();
}
@Bean(MALLCHAT_EXECUTOR)
@Primary
public ThreadPoolTaskExecutor mallchatExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("mallchat-executor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//满了调用线程执行,认为重要任务
executor.initialize();
return executor;
}
@Bean(WS_EXECUTOR)
public ThreadPoolTaskExecutor websocketExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(1000);//支持同时推送1000人
executor.setThreadNamePrefix("websocket-executor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());//满了直接丢弃,默认为不重要消息推送
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,6 @@
package com.abin.mallchat.common.common.constant;
public interface MDCKey {
String TID = "tid";
String UID = "uid";
}

View File

@@ -0,0 +1,33 @@
package com.abin.mallchat.common.common.constant;
/**
* @author zhongzb create on 2021/06/10
*/
public class RedisKey {
private static final String BASE_KEY = "mallchat:";
/**
* 在线用户列表
*/
public static final String ONLINE_UID_ZET = "online";
/**
* 离线用户列表
*/
public static final String OFFLINE_UID_ZET = "offline";
/**
* 用户信息
*/
public static final String USER_INFO_STRING = "userInfo:uid_%d";
/**
* 用户token存放
*/
public static final String USER_TOKEN_STRING = "userToken:uid_%d";
public static String getKey(String key, Object... objects) {
return BASE_KEY + String.format(key, objects);
}
}

View File

@@ -0,0 +1,14 @@
package com.abin.mallchat.common.common.domain.dto;
import lombok.Data;
/**
* Description: web请求信息收集类
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-05
*/
@Data
public class RequestInfo {
private Long uid;
private String ip;
}

View File

@@ -0,0 +1,35 @@
package com.abin.mallchat.common.common.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: 幂等类型
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum IdempotentEnum {
UID(1, "uid"),
MSG_ID(2, "消息id"),
;
private final Integer type;
private final String desc;
private static Map<Integer, IdempotentEnum> cache;
static {
cache = Arrays.stream(IdempotentEnum.values()).collect(Collectors.toMap(IdempotentEnum::getType, Function.identity()));
}
public static IdempotentEnum of(Integer type) {
return cache.get(type);
}
}

View File

@@ -0,0 +1,39 @@
package com.abin.mallchat.common.common.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: ws前端请求类型枚举
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum YesOrNoEnum {
NO(0, ""),
YES(1, ""),
;
private final Integer status;
private final String desc;
private static Map<Integer, YesOrNoEnum> cache;
static {
cache = Arrays.stream(YesOrNoEnum.values()).collect(Collectors.toMap(YesOrNoEnum::getStatus, Function.identity()));
}
public static YesOrNoEnum of(Integer type) {
return cache.get(type);
}
public static Integer toStatus(Boolean bool){
return bool?YES.getStatus():NO.getStatus();
}
}

View File

@@ -0,0 +1,38 @@
package com.abin.mallchat.common.common.domain.vo.request;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.generator.config.querys.XuguQuery;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Objects;
/**
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@ApiModel("游标翻页请求")
@AllArgsConstructor
@NoArgsConstructor
public class CursorPageBaseReq {
@ApiModelProperty("页面大小")
private Integer pageSize = 10;
@ApiModelProperty("游标初始为null后续请求附带上次翻页的游标")
private String cursor;
public Page plusPage() {
return new Page(1, this.pageSize);
}
@JsonIgnore
public Boolean isFirstPage() {
return Objects.isNull(cursor);
}
}

View File

@@ -0,0 +1,30 @@
package com.abin.mallchat.common.common.domain.vo.request;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@ApiModel("基础翻页请求")
public class PageBaseReq{
@ApiModelProperty("页面大小")
private Integer pageSize = 10;
@ApiModelProperty("页面索引从1开始")
private Integer pageNo = 1;
/**
* 获取mybatisPlus的page
*
* @return
*/
public Page plusPage() {
return new Page(pageNo, pageSize);
}
}

View File

@@ -0,0 +1,58 @@
package com.abin.mallchat.common.common.domain.vo.response;
import com.abin.mallchat.common.common.exception.ErrorEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* Description: 通用返回体
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-23
*/
@Data
@ApiModel("基础返回体")
public class ApiResult<T> {
@ApiModelProperty("成功标识true or false")
private Boolean success;
@ApiModelProperty("错误码")
private Integer errCode;
@ApiModelProperty("错误消息")
private String errMsg;
@ApiModelProperty("返回对象")
private T data;
public static <T> ApiResult<T> success() {
ApiResult<T> result = new ApiResult<T>();
result.setData(null);
result.setSuccess(Boolean.TRUE);
return result;
}
public static <T> ApiResult<T> success(T data) {
ApiResult<T> result = new ApiResult<T>();
result.setData(data);
result.setSuccess(Boolean.TRUE);
return result;
}
public static <T> ApiResult<T> fail(Integer code, String msg) {
ApiResult<T> result = new ApiResult<T>();
result.setSuccess(Boolean.FALSE);
result.setErrCode(code);
result.setErrMsg(msg);
return result;
}
public static <T> ApiResult<T> fail(ErrorEnum errorEnum) {
ApiResult<T> result = new ApiResult<T>();
result.setSuccess(Boolean.FALSE);
result.setErrCode(errorEnum.getErrorCode());
result.setErrMsg(errorEnum.getErrorMsg());
return result;
}
public boolean isSuccess() {
return this.success;
}
}

View File

@@ -0,0 +1,54 @@
package com.abin.mallchat.common.common.domain.vo.response;
import cn.hutool.core.collection.CollectionUtil;
import com.abin.mallchat.common.chat.domain.entity.Message;
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@ApiModel("游标翻页返回")
@AllArgsConstructor
@NoArgsConstructor
public class CursorPageBaseResp<T> {
@ApiModelProperty("游标(下次翻页带上这参数)")
private String cursor;
@ApiModelProperty("是否最后一页")
private Boolean isLast = Boolean.FALSE;
@ApiModelProperty("数据列表")
private List<T> list;
public static CursorPageBaseResp init(CursorPageBaseResp cursorPage, List list) {
CursorPageBaseResp cursorPageBaseResp = new CursorPageBaseResp();
cursorPageBaseResp.setIsLast(cursorPage.getIsLast());
cursorPageBaseResp.setList(list);
cursorPageBaseResp.setCursor(cursorPage.getCursor());
return cursorPageBaseResp;
}
@JsonIgnore
public Boolean isEmpty() {
return CollectionUtil.isEmpty(list);
}
public static CursorPageBaseResp empty() {
CursorPageBaseResp cursorPageBaseResp = new CursorPageBaseResp();
cursorPageBaseResp.setIsLast(true);
cursorPageBaseResp.setList(new ArrayList());
return cursorPageBaseResp;
}
}

View File

@@ -0,0 +1,23 @@
package com.abin.mallchat.common.common.domain.vo.response;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author zhongzb create on 2021/05/31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IdRespVO {
@ApiModelProperty("id")
private long id;
public static IdRespVO id(Long id) {
IdRespVO idRespVO = new IdRespVO();
idRespVO.setId(id);
return idRespVO;
}
}

View File

@@ -0,0 +1,79 @@
package com.abin.mallchat.common.common.domain.vo.response;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("基础翻页返回")
public class PageBaseResp<T> {
@ApiModelProperty("当前页数")
private Integer pageNo;
@ApiModelProperty("每页查询数量")
private Integer pageSize;
@ApiModelProperty("总记录数")
private Long totalRecords;
@ApiModelProperty("是否最后一页")
private Boolean isLast = Boolean.FALSE;
@ApiModelProperty("数据列表")
private List<T> list;
public static PageBaseResp empty() {
PageBaseResp r = new PageBaseResp();
r.setPageNo(1);
r.setPageSize(0);
r.setIsLast(true);
r.setTotalRecords(0L);
r.setList(new ArrayList());
return r;
}
public static <T> PageBaseResp<T> init(Integer pageNo, Integer pageSize, Long totalRecords, Boolean isLast, List<T> list) {
return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLast, list);
}
public static <T> PageBaseResp<T> init(Integer pageNo, Integer pageSize, Long totalRecords, List<T> list) {
return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLastPage(totalRecords, pageNo, pageSize), list);
}
public static <T> PageBaseResp<T> init(IPage<T> page) {
return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), page.getRecords());
}
public static <T> PageBaseResp<T> init(IPage page, List<T> list) {
return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), list);
}
public static <T> PageBaseResp<T> init(PageBaseResp resp, List<T> list) {
return init(resp.getPageNo(), resp.getPageSize(), resp.getTotalRecords(), resp.getIsLast(), list);
}
/**
* 是否是最后一页
*/
public static Boolean isLastPage(long totalRecords, int pageNo, int pageSize) {
if (pageSize == 0) {
return false;
}
long pageTotal = totalRecords / pageSize + (totalRecords % pageSize == 0 ? 0 : 1);
return pageNo >= pageTotal ? true : false;
}
}

View File

@@ -0,0 +1,32 @@
package com.abin.mallchat.common.common.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Description: 业务校验异常码
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-26
*/
@AllArgsConstructor
@Getter
public enum BusinessErrorEnum implements ErrorEnum {
//==================================common==================================
BUSINESS_ERROR(1001, "{}"),
//==================================user==================================
//==================================chat==================================
SYSTEM_ERROR(1001, "系统出小差了,请稍后再试哦~~"),
;
private Integer code;
private String msg;
@Override
public Integer getErrorCode() {
return code;
}
@Override
public String getErrorMsg() {
return msg;
}
}

View File

@@ -0,0 +1,55 @@
package com.abin.mallchat.common.common.exception;
import lombok.Data;
@Data
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
*  错误码
*/
protected Integer errorCode;
/**
*  错误信息
*/
protected String errorMsg;
public BusinessException() {
super();
}
public BusinessException(String errorMsg) {
super(errorMsg);
this.errorMsg = errorMsg;
}
public BusinessException(Integer errorCode, String errorMsg) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BusinessException(Integer errorCode, String errorMsg, Throwable cause) {
super(errorMsg, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BusinessException(ErrorEnum error) {
super(error.getErrorMsg());
this.errorCode = error.getErrorCode();
this.errorMsg = error.getErrorMsg();
}
@Override
public String getMessage() {
return errorMsg;
}
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}

View File

@@ -0,0 +1,31 @@
package com.abin.mallchat.common.common.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Description: 通用异常码
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-26
*/
@AllArgsConstructor
@Getter
public enum CommonErrorEnum implements ErrorEnum {
SYSTEM_ERROR(-1, "系统出小差了,请稍后再试哦~~"),
PARAM_VALID(-2, "参数校验失败"),
FREQUENCY_LIMIT(-3, "请求太频繁了,请稍后再试哦~~"),
;
private Integer code;
private String msg;
@Override
public Integer getErrorCode() {
return this.code;
}
@Override
public String getErrorMsg() {
return this.msg;
}
}

View File

@@ -0,0 +1,8 @@
package com.abin.mallchat.common.common.exception;
public interface ErrorEnum {
Integer getErrorCode();
String getErrorMsg();
}

View File

@@ -0,0 +1,50 @@
package com.abin.mallchat.common.common.exception;
import com.abin.mallchat.common.common.domain.vo.response.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* validation参数校验异常
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ApiResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException e) {
StringBuilder errorMsg = new StringBuilder();
e.getBindingResult().getFieldErrors().forEach(x -> errorMsg.append(x.getField()).append(x.getDefaultMessage()).append(","));
String message = errorMsg.toString();
log.info("validation parameters errorThe reason is:{}", message);
return ApiResult.fail(-1, message.substring(0, message.length() - 1));
}
/**
* 处理空指针的异常
*/
@ExceptionHandler(value = NullPointerException.class)
public ApiResult exceptionHandler( NullPointerException e) {
log.error("null point exceptionThe reason is:{}", e.getMessage(), e);
return ApiResult.fail(CommonErrorEnum.SYSTEM_ERROR);
}
/**
* 未知异常
*/
@ExceptionHandler(value = Exception.class)
public ApiResult systemExceptionHandler( Exception e) {
log.error("system exceptionThe reason is{}", e.getMessage(), e);
return ApiResult.fail(CommonErrorEnum.SYSTEM_ERROR);
}
/**
* 自定义校验异常(如参数校验等)
*/
@ExceptionHandler(value = BusinessException.class)
public ApiResult businessExceptionHandler(BusinessException e) {
log.info("business exceptionThe reason is{}", e.getMessage(), e);
return ApiResult.fail(e.getErrorCode(), e.getMessage());
}
}

View File

@@ -0,0 +1,51 @@
package com.abin.mallchat.common.common.exception;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.abin.mallchat.common.common.domain.vo.response.ApiResult;
import com.google.common.base.Charsets;
import io.netty.handler.codec.http.HttpContent;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.lang3.CharSet;
import org.apache.commons.lang3.CharSetUtils;
import org.springframework.http.HttpStatus;
import sun.awt.CharsetString;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
/**
* Description: 业务校验异常码
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-26
*/
@AllArgsConstructor
@Getter
public enum HttpErrorEnum implements ErrorEnum {
ACCESS_DENIED(401, "登录失效,请重新登录"),
;
private Integer httpCode;
private String msg;
@Override
public Integer getErrorCode() {
return httpCode;
}
@Override
public String getErrorMsg() {
return msg;
}
public void sendHttpError(HttpServletResponse response) throws IOException {
response.setStatus(this.getErrorCode());
ApiResult responseData = ApiResult.fail(this);
response.setContentType(ContentType.JSON.toString(Charsets.UTF_8));
response.getWriter().write(JSONUtil.toJsonStr(responseData));
}
}

View File

@@ -0,0 +1,78 @@
package com.abin.mallchat.common.common.utils;
import cn.hutool.core.util.ObjectUtil;
import com.abin.mallchat.common.common.exception.BusinessErrorEnum;
import com.abin.mallchat.common.common.exception.BusinessException;
import com.abin.mallchat.common.common.exception.ErrorEnum;
import com.abin.mallchat.common.user.domain.entity.UserBackpack;
import org.apache.commons.lang3.StringUtils;
import java.util.Objects;
/**
* 校验工具类
*/
public class AssertUtil {
//如果不是true则抛异常
public static void isTrue(boolean expression, String msg) {
if (!expression) {
throwException(msg);
}
}
public static void isTrue(boolean expression, ErrorEnum errorEnum, Object... args) {
if (!expression) {
throwException(errorEnum, args);
}
}
//如果是true则抛异常
public static void isFalse(boolean expression, String msg) {
if (expression) {
throwException(msg);
}
}
//如果是true则抛异常
public static void isFalse(boolean expression, ErrorEnum errorEnum, Object... args) {
if (expression) {
throwException(errorEnum, args);
}
}
//如果不是非空对象,则抛异常
public static void isNotEmpty(Object obj, String msg) {
if (isEmpty(obj)) {
throwException(msg);
}
}
//如果不是非空对象,则抛异常
public static void isEmpty(Object obj, String msg) {
if (!isEmpty(obj)) {
throwException(msg);
}
}
public static void equal(Object o1,Object o2, String msg) {
if (!ObjectUtil.equal(o1,o2)) {
throwException(msg);
}
}
private static boolean isEmpty(Object obj) {
return ObjectUtil.isEmpty(obj);
}
private static void throwException(String msg) {
throwException(null, msg);
}
private static void throwException(ErrorEnum errorEnum, Object... arg) {
if (Objects.isNull(errorEnum)) {
errorEnum = BusinessErrorEnum.BUSINESS_ERROR;
}
throw new BusinessException(errorEnum.getErrorCode(), String.format(errorEnum.getErrorMsg(), arg));
}
}

View File

@@ -0,0 +1,77 @@
package com.abin.mallchat.common.common.utils;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.lang.Tuple;
import cn.hutool.core.util.StrUtil;
import com.abin.mallchat.common.chat.dao.MessageDao;
import com.abin.mallchat.common.chat.domain.entity.Message;
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
import com.abin.mallchat.common.common.domain.vo.request.PageBaseReq;
import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
import com.abin.mallchat.common.common.domain.vo.response.PageBaseResp;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.errorprone.annotations.Var;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import reactor.util.function.Tuple2;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Description: 游标分页工具类
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-28
*/
@Component
public class CursorUtils {
@Autowired
private RedisUtils redisUtils;
public <T> CursorPageBaseResp<Pair<T, Double>> getCursorPageByRedis(CursorPageBaseReq cursorPageBaseReq, String redisKey, Function<String, T> typeConvert) {
Set<ZSetOperations.TypedTuple<String>> typedTuples;
if (StrUtil.isBlank(cursorPageBaseReq.getCursor())) {//第一次
typedTuples = redisUtils.zReverseRangeWithScores(redisKey, cursorPageBaseReq.getPageSize());
} else {
typedTuples = redisUtils.zReverseRangeByScoreWithScores(redisKey, Double.parseDouble(cursorPageBaseReq.getCursor()), cursorPageBaseReq.getPageSize());
}
List<Pair<T, Double>> result = typedTuples
.stream()
.map(t -> Pair.of(typeConvert.apply(t.getValue()), t.getScore()))
.sorted((o1, o2) -> o2.getValue().compareTo(o1.getValue()))
.collect(Collectors.toList());
String cursor = Optional.ofNullable(CollectionUtil.getLast(result))
.map(Pair::getValue)
.map(String::valueOf)
.orElse(null);
Boolean isLast = result.size() != cursorPageBaseReq.getPageSize();
return new CursorPageBaseResp(cursor, isLast, result);
}
public <T> CursorPageBaseResp<T> getCursorPageByMysql(IService<T> mapper, CursorPageBaseReq request, Consumer<LambdaQueryWrapper<T>> initWrapper, SFunction<T, ?> cursorColumn) {
LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
initWrapper.accept(wrapper);
if (StrUtil.isNotBlank(request.getCursor())) {
wrapper.lt(cursorColumn, request.getCursor());
}
wrapper.orderByDesc(cursorColumn);
Page<T> page = mapper.page(request.plusPage(), wrapper);
String cursor = Optional.ofNullable(CollectionUtil.getLast(page.getRecords()))
.map(cursorColumn)
.map(String::valueOf)
.orElse(null);
Boolean isLast = page.getRecords().size() != request.getPageSize();
return new CursorPageBaseResp(cursor, isLast, page.getRecords());
}
}

View File

@@ -0,0 +1,87 @@
package com.abin.mallchat.common.common.utils;
import cn.hutool.json.JSONUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* Description: jwt的token生成与解析
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-03
*/
@Slf4j
@Component
public class JwtUtils {
/**
* token秘钥请勿泄露请勿随便修改
*/
@Value("jwt.secret")
private String secret;
private static final String UID_CLAIM = "uid";
/**
* JWT生成Token.<br/>
* <p>
* JWT构成: header, payload, signature
*/
public String createToken(Long uid) {
// build token
String token = JWT.create()
.withClaim(UID_CLAIM, uid) // 只存一个uid信息其他的自己去redis查
.sign(Algorithm.HMAC256(secret)); // signature
return token;
}
public static void main(String[] args) {
JwtUtils jwtUtils = new JwtUtils();
String token = jwtUtils.createToken(123L);
System.out.println(token);
}
/**
* 解密Token
*
* @param token
* @return
*/
public Map<String, Claim> verifyToken(String token) {
if (Objects.isNull(token)) {
return null;
}
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaims();
} catch (Exception e) {
log.info("decode error,token:{}", token, e);
}
return null;
}
/**
* 根据Token获取uid
*
* @param token
* @return uid
*/
public Long getUidOrNull(String token) {
return Optional.ofNullable(verifyToken(token))
.map(map -> map.get(UID_CLAIM))
.map(Claim::asLong)
.orElse(null);
}
}

View File

@@ -0,0 +1,25 @@
package com.abin.mallchat.common.common.utils;
import com.abin.mallchat.common.common.domain.dto.RequestInfo;
/**
* Description: 请求上下文
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-05
*/
public class RequestHolder {
private static final ThreadLocal<RequestInfo> threadLocal = new ThreadLocal<>();
public static void set(RequestInfo requestInfo) {
threadLocal.set(requestInfo);
}
public static RequestInfo get() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
}

View File

@@ -0,0 +1,26 @@
package com.abin.mallchat.common.user.dao;
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
import com.abin.mallchat.common.user.mapper.ItemConfigMapper;
import com.abin.mallchat.common.user.service.IItemConfigService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 功能物品配置表 服务实现类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Service
public class ItemConfigDao extends ServiceImpl<ItemConfigMapper, ItemConfig> {
public List<ItemConfig> getByType(Integer type) {
return lambdaQuery().eq(ItemConfig::getType, type).list();
}
}

View File

@@ -0,0 +1,60 @@
package com.abin.mallchat.common.user.dao;
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
import com.abin.mallchat.common.user.domain.entity.User;
import com.abin.mallchat.common.user.domain.entity.UserBackpack;
import com.abin.mallchat.common.user.mapper.UserBackpackMapper;
import com.abin.mallchat.common.user.service.IUserBackpackService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 用户背包表 服务实现类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Service
public class UserBackpackDao extends ServiceImpl<UserBackpackMapper, UserBackpack> {
public Integer getCountByValidItemId(Long uid, Long itemId) {
LambdaQueryWrapper<UserBackpack> wrapper = new QueryWrapper<UserBackpack>().lambda()
.eq(UserBackpack::getUid, uid)
.eq(UserBackpack::getItemId, itemId)
.eq(UserBackpack::getStatus, YesOrNoEnum.NO.getStatus());
return count(wrapper);
}
public UserBackpack getFirstValidItem(Long uid, Long itemId) {
LambdaQueryWrapper<UserBackpack> wrapper = new QueryWrapper<UserBackpack>().lambda()
.eq(UserBackpack::getUid, uid)
.eq(UserBackpack::getItemId, itemId)
.eq(UserBackpack::getStatus, YesOrNoEnum.NO.getStatus())
.last("limit 1");
return getOne(wrapper);
}
public boolean invalidItem(Long itemId) {
UserBackpack update = new UserBackpack();
update.setItemId(itemId);
update.setStatus(YesOrNoEnum.YES.getStatus());
return updateById(update);
}
public List<UserBackpack> getByItemIds(Long uid, List<Long> itemIds) {
return lambdaQuery().eq(UserBackpack::getUid, uid)
.in(UserBackpack::getItemId, itemIds)
.eq(UserBackpack::getStatus, YesOrNoEnum.NO.getStatus())
.list();
}
public UserBackpack getByIdp(String idempotent) {
return lambdaQuery().eq(UserBackpack::getIdempotent, idempotent).one();
}
}

View File

@@ -0,0 +1,40 @@
package com.abin.mallchat.common.user.dao;
import com.abin.mallchat.common.user.domain.entity.User;
import com.abin.mallchat.common.user.mapper.UserMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 用户表 服务实现类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Service
public class UserDao extends ServiceImpl<UserMapper, User> {
public User getByOpenId(String openId) {
LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda().eq(User::getOpenId, openId);
return getOne(wrapper);
}
public void modifyName(Long uid, String name) {
User update = new User();
update.setId(uid);
update.setName(name);
updateById(update);
}
public void wearingBadge(Long uid, Long badgeId) {
User update = new User();
update.setId(uid);
update.setItemId(badgeId);
updateById(update);
}
}

View File

@@ -0,0 +1,26 @@
package com.abin.mallchat.common.user.domain.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Objects;
/**
* Description:
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-18
*/
@Data
public class IpResult<T> implements Serializable {
@ApiModelProperty("错误码")
private Integer code;
@ApiModelProperty("错误消息")
private String msg;
@ApiModelProperty("返回对象")
private T data;
public boolean isSuccess() {
return Objects.nonNull(this.code) && this.code == 0;
}
}

View File

@@ -0,0 +1,33 @@
package com.abin.mallchat.common.user.domain.entity;
import lombok.*;
import java.io.Serializable;
/**
* <p>
* 用户ip信息
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class IpDetail implements Serializable {
private static final long serialVersionUID = 1L;
//注册时的ip
private String ip;
//最新登录的ip
private String isp;
private String isp_id;
private String city;
private String city_id;
private String country;
private String country_id;
private String region;
private String region_id;
}

View File

@@ -0,0 +1,63 @@
package com.abin.mallchat.common.user.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.io.Serializable;
import java.util.Objects;
import java.util.Optional;
/**
* <p>
* 用户ip信息
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class IpInfo implements Serializable {
private static final long serialVersionUID = 1L;
//注册时的ip
private String createIp;
//注册时的ip详情
private IpDetail createIpDetail;
//最新登录的ip
private String updateIp;
//最新登录的ip详情
private IpDetail updateIpDetail;
public void refreshIp(String ip) {
if (Objects.isNull(ip)) {
return;
}
updateIp = ip;
if (createIp == null) {
createIp = ip;
}
}
/**
* 需要刷新的ip这里判断更新ip就够初始化的时候ip也是相同的只需要设置的时候多设置进去就行
*
* @return
*/
public String needRefreshIp() {
boolean notNeedRefresh = Optional.ofNullable(updateIpDetail)
.map(IpDetail::getIp)
.filter(ip -> ip.equals(updateIp))
.isPresent();
return notNeedRefresh ? null : updateIp;
}
public void refreshIpDetail(IpDetail ipDetail) {
if (Objects.equals(createIp, ipDetail)) {
createIpDetail = ipDetail;
}
if (Objects.equals(updateIp, ipDetail)) {
updateIpDetail = ipDetail;
}
}
}

View File

@@ -0,0 +1,63 @@
package com.abin.mallchat.common.user.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
* 功能物品配置表
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("item_config")
public class ItemConfig implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId("id")
private Long id;
/**
* 物品类型 1改名卡 2徽章
*/
@TableField("type")
private Integer type;
/**
* 物品图片
*/
@TableField("img")
private String img;
/**
* 物品功能描述
*/
@TableField("`describe`")
private String describe;
/**
* 创建时间
*/
@TableField("create_time")
private LocalDateTime createTime;
/**
* 修改时间
*/
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,103 @@
package com.abin.mallchat.common.user.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.util.Date;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import lombok.experimental.Accessors;
/**
* <p>
* 用户表
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户昵称
*/
@TableField("name")
private String name;
/**
* 用户头像
*/
@TableField("avatar")
private String avatar;
/**
* 性别 1为男性2为女性
*/
@TableField("sex")
private Integer sex;
/**
* 微信openid用户标识
*/
@TableField("open_id")
private String openId;
/**
* 最后上下线时间
*/
@TableField("last_opt_time")
private Date lastOptTime;
/**
* 最后上下线时间
*/
@TableField(value = "ip_info", typeHandler = JacksonTypeHandler.class)
private IpInfo ipInfo;
/**
* 佩戴的徽章id
*/
@TableField("item_id")
private Long itemId;
/**
* 创建时间
*/
@TableField("create_time")
private Date createTime;
/**
* 修改时间
*/
@TableField("update_time")
private Date updateTime;
public IpInfo getIpInfo() {
if (ipInfo == null) {
ipInfo = new IpInfo();
}
return ipInfo;
}
}

View File

@@ -0,0 +1,74 @@
package com.abin.mallchat.common.user.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.Accessors;
/**
* <p>
* 用户背包表
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("user_backpack")
public class UserBackpack implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* uid
*/
@TableField("uid")
private Long uid;
/**
* 物品id
*/
@TableField("item_id")
private Long itemId;
/**
* 使用状态 0.未失效 1失效
*/
@TableField("status")
private Integer status;
/**
* 幂等号
*/
@TableField("idempotent")
private String idempotent;
/**
* 创建时间
*/
@TableField("create_time")
private LocalDateTime createTime;
/**
* 修改时间
*/
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,35 @@
package com.abin.mallchat.common.user.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: ws前端请求类型枚举
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum ChatActiveStatusEnum {
ONLINE(1, "在线"),
OFFLINE(2, "离线"),
;
private final Integer status;
private final String desc;
private static Map<Integer, ChatActiveStatusEnum> cache;
static {
cache = Arrays.stream(ChatActiveStatusEnum.values()).collect(Collectors.toMap(ChatActiveStatusEnum::getStatus, Function.identity()));
}
public static ChatActiveStatusEnum of(Integer type) {
return cache.get(type);
}
}

View File

@@ -0,0 +1,38 @@
package com.abin.mallchat.common.user.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: 物品枚举
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum ItemEnum {
MODIFY_NAME_CARD(1L, ItemTypeEnum.MODIFY_NAME_CARD, "改名卡"),
LIKE_BADGE(2L, ItemTypeEnum.BADGE, "爆赞徽章"),
REG_TOP10_BADGE(2L, ItemTypeEnum.BADGE, "爆赞徽章"),
REG_TOP100_BADGE(2L, ItemTypeEnum.BADGE, "爆赞徽章"),
;
private final Long id;
private final ItemTypeEnum typeEnum;
private final String desc;
private static Map<Long, ItemEnum> cache;
static {
cache = Arrays.stream(ItemEnum.values()).collect(Collectors.toMap(ItemEnum::getId, Function.identity()));
}
public static ItemEnum of(Integer type) {
return cache.get(type);
}
}

View File

@@ -0,0 +1,35 @@
package com.abin.mallchat.common.user.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: 物品枚举
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-19
*/
@AllArgsConstructor
@Getter
public enum ItemTypeEnum {
MODIFY_NAME_CARD(1, "改名卡"),
BADGE(2, "徽章"),
;
private final Integer type;
private final String desc;
private static Map<Integer, ItemTypeEnum> cache;
static {
cache = Arrays.stream(ItemTypeEnum.values()).collect(Collectors.toMap(ItemTypeEnum::getType, Function.identity()));
}
public static ItemTypeEnum of(Integer type) {
return cache.get(type);
}
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.user.mapper;
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 功能物品配置表 Mapper 接口
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
public interface ItemConfigMapper extends BaseMapper<ItemConfig> {
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.user.mapper;
import com.abin.mallchat.common.user.domain.entity.UserBackpack;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 用户背包表 Mapper 接口
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
public interface UserBackpackMapper extends BaseMapper<UserBackpack> {
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.user.mapper;
import com.abin.mallchat.common.user.domain.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 用户表 Mapper 接口
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,16 @@
package com.abin.mallchat.common.user.service;
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 功能物品配置表 服务类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
public interface IItemConfigService extends IService<ItemConfig> {
}

View File

@@ -0,0 +1,27 @@
package com.abin.mallchat.common.user.service;
import com.abin.mallchat.common.common.domain.enums.IdempotentEnum;
import com.abin.mallchat.common.user.domain.entity.UserBackpack;
import com.abin.mallchat.common.user.domain.enums.ItemEnum;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 用户背包表 服务类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
public interface IUserBackpackService {
/**
* 用户获取一个物品
* @param uid 用户id
* @param itemId 物品id
* @param idempotentEnum 幂等类型
* @param businessId 上层业务发送的唯一标识
*/
void acquireItem(Long uid, Long itemId, IdempotentEnum idempotentEnum, String businessId);
}

View File

@@ -0,0 +1,10 @@
package com.abin.mallchat.common.user.service;
public interface IpService {
/**
* 异步更新用户ip详情
*
* @param id
*/
void refreshIpDetailAsync(Long id);
}

View File

@@ -0,0 +1,42 @@
package com.abin.mallchat.common.user.service.cache;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Pair;
import com.abin.mallchat.common.common.constant.RedisKey;
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
import com.abin.mallchat.common.common.utils.CursorUtils;
import com.abin.mallchat.common.common.utils.RedisUtils;
import com.abin.mallchat.common.user.dao.ItemConfigDao;
import com.abin.mallchat.common.user.dao.UserDao;
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
import com.abin.mallchat.common.user.domain.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Description: 用户相关缓存
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-27
*/
@Component
public class ItemCache {//todo 多级缓存
@Autowired
private ItemConfigDao itemConfigDao;
@Cacheable(cacheNames = "item", key = "'itemsByType:'+#type")
public List<ItemConfig> getByType(Integer type) {
return itemConfigDao.getByType(type);
}
@Cacheable(cacheNames = "item", key = "'item:'+#itemId")
public ItemConfig getById(Long itemId) {
return itemConfigDao.getById(itemId);
}
}

View File

@@ -0,0 +1,112 @@
package com.abin.mallchat.common.user.service.cache;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Pair;
import com.abin.mallchat.common.common.constant.RedisKey;
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
import com.abin.mallchat.common.common.utils.CursorUtils;
import com.abin.mallchat.common.common.utils.RedisUtils;
import com.abin.mallchat.common.user.dao.UserDao;
import com.abin.mallchat.common.user.domain.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.security.PublicKey;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Description: 用户相关缓存
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-03-27
*/
@Component
public class UserCache {
@Autowired
private RedisUtils redisUtils;
@Autowired
private CursorUtils cursorUtils;
@Autowired
private UserDao userDao;
public Long getOnlineNum() {
String onlineKey = RedisKey.getKey(RedisKey.ONLINE_UID_ZET);
return redisUtils.zCard(onlineKey);
}
public Long getOfflineNum() {
String offlineKey = RedisKey.getKey(RedisKey.OFFLINE_UID_ZET);
return redisUtils.zCard(offlineKey);
}
//用户上线
public void online(Long uid, Date optTime) {
String onlineKey = RedisKey.getKey(RedisKey.ONLINE_UID_ZET);
String offlineKey = RedisKey.getKey(RedisKey.OFFLINE_UID_ZET);
//移除离线表
redisUtils.zRemove(offlineKey, uid);
//更新上线表
redisUtils.zAdd(onlineKey, uid, optTime.getTime());
}
//用户下线
public void offline(Long uid, Date optTime) {
String onlineKey = RedisKey.getKey(RedisKey.ONLINE_UID_ZET);
String offlineKey = RedisKey.getKey(RedisKey.OFFLINE_UID_ZET);
//移除上线线表
redisUtils.zRemove(onlineKey, uid);
//更新上线表
redisUtils.zAdd(offlineKey, uid, optTime.getTime());
}
public CursorPageBaseResp<Pair<Long, Double>> getOnlineCursorPage(CursorPageBaseReq pageBaseReq) {
return cursorUtils.getCursorPageByRedis(pageBaseReq, RedisKey.getKey(RedisKey.ONLINE_UID_ZET), Long::parseLong);
}
public CursorPageBaseResp<Pair<Long, Double>> getOfflineCursorPage(CursorPageBaseReq pageBaseReq) {
return cursorUtils.getCursorPageByRedis(pageBaseReq, RedisKey.getKey(RedisKey.OFFLINE_UID_ZET), Long::parseLong);
}
/**
* 获取用户信息,盘路缓存模式
*
* @param uid
* @return
*/
public User getUserInfo(Long uid) {//todo 后期做二级缓存
return getUserInfoBatch(Collections.singleton(uid)).get(uid);
}
/**
* 获取用户信息,盘路缓存模式
*
* @param uids
* @return
*/
public Map<Long, User> getUserInfoBatch(Set<Long> uids) {
List<String> keys = uids.stream().map(a -> RedisKey.getKey(RedisKey.USER_INFO_STRING, a)).collect(Collectors.toList());
List<User> mget = redisUtils.mget(keys, User.class);
Map<Long, User> map = mget.stream().collect(Collectors.toMap(User::getId, Function.identity()));
//还需要load更新的uid
List<Long> needLoadUidList = uids.stream().filter(a -> !map.containsKey(a)).collect(Collectors.toList());
if (CollUtil.isNotEmpty(needLoadUidList)) {
List<User> needLoadUserList = userDao.listByIds(needLoadUidList);
Map<String, User> redisMap = needLoadUserList.stream().collect(Collectors.toMap(a -> RedisKey.getKey(RedisKey.USER_INFO_STRING, a.getId()), Function.identity()));
redisUtils.mset(redisMap, 5 * 60);
map.putAll(needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity())));
}
return map;
}
public void delUserInfo(Long uid) {
String key = RedisKey.getKey(RedisKey.USER_INFO_STRING, uid);
redisUtils.del(key);
}
}

View File

@@ -0,0 +1,119 @@
package com.abin.mallchat.common.user.service.impl;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.thread.NamedThreadFactory;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.abin.mallchat.common.common.domain.vo.response.ApiResult;
import com.abin.mallchat.common.user.dao.UserDao;
import com.abin.mallchat.common.user.domain.dto.IpResult;
import com.abin.mallchat.common.user.domain.entity.IpDetail;
import com.abin.mallchat.common.user.domain.entity.IpInfo;
import com.abin.mallchat.common.user.domain.entity.User;
import com.abin.mallchat.common.user.service.IpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.jar.Manifest;
/**
* Description: ip
* Author: <a href="https://github.com/zongzibinbin">abin</a>
* Date: 2023-04-18
*/
@Service
@Slf4j
public class IpServiceImpl implements IpService, DisposableBean {
private static ExecutorService executor = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(500), new NamedThreadFactory("refresh-ipDetail", false));
@Autowired
private UserDao userDao;
@Override
public void refreshIpDetailAsync(Long uid) {
executor.execute(() -> {
User user = userDao.getById(uid);
IpInfo ipInfo = user.getIpInfo();
String ip = ipInfo.needRefreshIp();
if (StrUtil.isBlank(ip)) {
return;
}
IpDetail ipDetail = TryGetIpDetailOrNullTreeTimes(ip);
if (Objects.nonNull(ipDetail)) {
ipInfo.refreshIpDetail(ipDetail);
User update = new User();
update.setId(uid);
update.setIpInfo(ipInfo);
userDao.updateById(update);
} else {
log.error("get ip detail fail ip:{},uid:{}", ip, uid);
}
});
}
private static IpDetail TryGetIpDetailOrNullTreeTimes(String ip) {
for (int i = 0; i < 3; i++) {
IpDetail ipDetail = getIpDetailOrNull(ip);
if (Objects.nonNull(ipDetail)) {
return ipDetail;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
public static IpDetail getIpDetailOrNull(String ip) {
String body = HttpUtil.get("https://ip.taobao.com/outGetIpInfo?ip=" + ip + "&accessKey=alibaba-inc");
try {
IpResult<IpDetail> result = JSONUtil.toBean(body, new TypeReference<IpResult<IpDetail>>() {
}, false);
if (result.isSuccess()) {
return result.getData();
}
} catch (Exception ignored) {
}
return null;
}
//测试耗时结果 100次查询总耗时约100s平均一次成功查询需要1s,可以接受
//第99次成功,目前耗时99545ms
public static void main(String[] args) {
Date begin = new Date();
for (int i = 0; i < 100; i++) {
int finalI = i;
executor.execute(() -> {
IpDetail ipDetail = TryGetIpDetailOrNullTreeTimes("113.90.36.126");
if (Objects.nonNull(ipDetail)) {
Date date = new Date();
System.out.println(String.format("第%d次成功,目前耗时:%dms", finalI, (date.getTime() - begin.getTime())));
}
});
}
}
@Override
public void destroy() throws InterruptedException {
executor.shutdown();
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {//最多等30秒处理不完就拉倒
if (log.isErrorEnabled()) {
log.error("Timed out while waiting for executor [{}] to terminate", executor);
}
}
}
}

View File

@@ -0,0 +1,62 @@
package com.abin.mallchat.common.user.service.impl;
import com.abin.mallchat.common.common.domain.enums.IdempotentEnum;
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
import com.abin.mallchat.common.user.dao.ItemConfigDao;
import com.abin.mallchat.common.user.dao.UserBackpackDao;
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
import com.abin.mallchat.common.user.domain.entity.UserBackpack;
import com.abin.mallchat.common.user.domain.enums.ItemTypeEnum;
import com.abin.mallchat.common.user.service.IUserBackpackService;
import com.abin.mallchat.common.user.service.cache.ItemCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* <p>
* 用户背包表 服务类
* </p>
*
* @author <a href="https://github.com/zongzibinbin">abin</a>
* @since 2023-03-19
*/
@Service
public class UserBackpackServiceImpl implements IUserBackpackService {
@Autowired
private UserBackpackDao userBackpackDao;
@Autowired
private ItemConfigDao itemConfigDao;
@Autowired
private ItemCache itemCache;
@Override
public void acquireItem(Long uid, Long itemId, IdempotentEnum idempotentEnum, String businessId) {//todo 分布式锁
String idempotent = getIdempotent(itemId, idempotentEnum, businessId);
UserBackpack userBackpack = userBackpackDao.getByIdp(idempotent);
//幂等检查
if (Objects.nonNull(userBackpack)) {
return;
}
//业务检查
ItemConfig itemConfig = itemCache.getById(itemId);
if (ItemTypeEnum.BADGE.getType().equals(itemConfig.getType())) {//徽章类型做唯一性检查
Integer countByValidItemId = userBackpackDao.getCountByValidItemId(uid, itemId);
if (countByValidItemId > 0) {//已经有徽章了不发
return;
}
}
//发物品
UserBackpack insert = UserBackpack.builder()
.itemId(itemId)
.status(YesOrNoEnum.NO.getStatus())
.idempotent(idempotent)
.build();
userBackpackDao.save(insert);
}
private String getIdempotent(Long itemId, IdempotentEnum idempotentEnum, String businessId) {
return String.format("%d_%d_%s", itemId, idempotentEnum.getType(), businessId);
}
}

View File

@@ -0,0 +1,21 @@
#todo 记得把这些配置信息补充了
##################mysql配置##################
mallchat.mysql.ip=127.0.0.1
mallchat.mysql.port=3306
mallchat.mysql.db=mallchat
mallchat.mysql.username=root
mallchat.mysql.password=123456
##################redis配置##################
mallchat.redis.host=127.0.0.1
mallchat.redis.port=6379
mallchat.redis.password=123456
##################jwt##################
mallchat.jwt.secret=dsfsdfsdfsdfsd
##################微信公众号信息##################
mallchat.wx.callback=http://127.0.0.1:8080
mallchat.wx.appId=appid
mallchat.wx.secret=380bfc1c9147fdsf4sf07
# 接口配置里的Token值
mallchat.wx.token=sdfsf
# 接口配置里的EncodingAESKey值
mallchat.wx.aesKey=sha1

View File

@@ -0,0 +1,60 @@
logging:
level:
org.springframework.web: INFO
com.github.binarywang.demo.wx.mp: DEBUG
me.chanjar.weixin: DEBUG
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
spring:
profiles:
#运行的环境
active: test
application:
name: mallchat
datasource:
url: jdbc:mysql://${mallchat.mysql.ip}:${mallchat.mysql.port}/${mallchat.mysql.db}?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: ${mallchat.mysql.username}
password: ${mallchat.mysql.password}
driver-class-name: com.mysql.cj.jdbc.Driver
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
redis:
# Redis服务器地址
host: ${mallchat.redis.host}
# Redis服务器端口号
port: ${mallchat.redis.port}
# 使用的数据库索引默认是0
database: 0
# 连接超时时间
timeout: 1800000
# 设置密码
password: ${mallchat.redis.password}
lettuce:
pool:
# 最大阻塞等待时间,负数表示没有限制
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 5
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中最大连接数,负数表示没有限制
max-active: 20
jackson:
serialization:
write-dates-as-timestamps: true
jwt:
secret: ${mallchat.jwt.secret}
wx:
mp:
# callback: http://f4cd-113-92-129-127.ngrok.io
callback: ${mallchat.wx.callback}
configs:
- appId: ${mallchat.wx.appId} # 第一个公众号的appid
secret: ${mallchat.wx.secret} # 公众号的appsecret
token: ${mallchat.wx.token} # 接口配置里的Token值
aesKey: ${mallchat.wx.aesKey} # 接口配置里的EncodingAESKey值

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.abin.mallchat.common.chat.mapper.MessageMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.abin.mallchat.common.chat.mapper.MessageMarkMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.abin.mallchat.common.chat.mapper.RoomMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.abin.mallchat.common.user.mapper.ItemConfigMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.abin.mallchat.common.user.mapper.UserBackpackMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.abin.mallchat.common.user.mapper.UserMapper">
</mapper>