欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

IM 系统,学习这个开源项目就足够了(后端章节)

最编程 2024-05-31 21:44:25
...

引言

最近有朋友委托笔者帮忙开发一个简易的聊天系统,简单支持单聊和群聊等功能即可。限于能力也为了节省时间,笔者只好去 gitee 和 github 搜基于Java技术栈的IM通讯系统项目,最后搜到了一个后端基于SpringBoot和Netty技术实现的cim开源项目和另一个后端基于SpringBoot和WebSocket技术,前端基于uniApp和Vue3实现的考拉IM通讯系统项目都做的比较好。虽然笔者本人更喜欢后端采用Netty框架实现的通讯系统,但是奈何cim开源项目大部分功能代码并没有开源出来,很多核心功能都是要收费的,而考拉IM项目则无论后端还是前端大部分功能都开源了出来,于是笔者重点研究了下考拉IM通讯系统的前后端项目源码,并尝试在本地开发环境启动前后端项目。

项目简介

  • 考拉IM系统是一个仿照腾讯微信中的通讯功能开发的多端项目,目前已实现支持Androi端iOS端H5端,后期会继续适配小程序端桌面端(windows、mac)和web端, 支持单聊、群聊、音视频通话、高德地图定位、搜索附近的人和摇一摇等功能。
  • 考拉IM系统采用前后端分离架构模式,前端项目使用uniApp+Vue3开发, 后端项目主要使用SpringBoot+Websocket+Redis+第三方服务等技术栈实现。
  • 前端项目源码地址:gitee.com/lakaola/im-…
  • 后端项目源码地址:gitee.com/lakaola/im-…

技术栈

  • 推送:uniPush + websocket
  • 资源:阿里OSS(图片、声音、视频、文件等)
  • 音视频:TRTC
  • 地图:高德地图
  • 短信:阿里云短信
  • 后端:Hutool,Mybatis-Plus, Shiro, undertow, sharding-jdbc, 接口版本控制等
  • 前端:uniApp + Vue3

后端项目im-platform

1)获取项目源码

使用git将后端项目im-platform从gitee克隆到本地磁盘,克隆完成后使用 IntelliJ IDEA 打开项目并添加Maven依赖。

2)建表与初始化sql脚本

使用root账户登录Mysql数据库客户端Navicat, 然后在命令行控制台中执行建表与添加数据的sql脚本,脚本位置:gitee.com/heshengfu12…

3)修改应用配置文件

修改application-dev.yml应用配置文件

spring:
# ShardingSphere 配置项
  shardingsphere:
    # 数据源配置
    datasource:
      # 所有数据源的名字
      names: master
      # 主库的数据源配置
      master:
        type: com.zaxxer.hikari.HikariDataSource # 使用 Hikari 数据库连接池
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/boot_im?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&verifyServerCertificate=false&serverTimezone=GMT%2B8&allowMultiQueries=true
        username: boot_im
        password: bootim2023admin
    # 拓展属性配置
    props:
      sql:
        show: true # 打印 SQL
    sharding:
      default-data-source-name: master
# 项目相关配置
platform:
  # 富文本路径 示例( Windows配置D:/platform/uploadPath,Linux配置 /home/platform/uploadPath)
  rootPath: E:/platform/uploadPath
  # 系统版本
  version: 1.0.0
  # 日志地址
  logPath: ./logs
  # token超时时间(分钟)默认7天
  timeout: 10080
  # 短信开关(N关Y开)
  sms: Y
# oss配置
oss:
  serverUrl:
  accessKey:
  secretKey:
  bucketName:
  region:

# 实时语音/视频 可接入腾讯或阿里的语音视频服务后获得appId和secret
trtc:
  # appId
  appId:
  # 签名过期时间,单位秒
  expire:
  # 签名秘钥
  secret:

# 推送配置 新建应用并获取配置信息参考文档:https://docs.getui.com/getui/start/devcenter/
push:
  appId: 
  appKey:
  appSecret:
  masterSecret: 
  
# 腾讯nlp配置
tencent:
  appId: 1301260368
  appKey: AKID******2nja
  appSecret: rb******eB     

上面数据库连接池配置只改了jdbc-url, username 与password等变量。

除此之外,我们需要完成一些第三方服务器的访问凭证配置,包括对象云存储OSS配置信息、高德地图密钥配置、腾讯自然语言(NLP)服务访问凭证配置、实时音视频服务(trtc)访问凭证配置等。这些配置信息都需要开通对应的第三方服务拿到对应的访问凭据后才能在项目的配置文件中加以补充,但是它并不影响后台项目的启动。相关的配置项如下,待笔者开通了这些第三方服务并找到了对应的访问凭据信息再来补充这些配置项的value值。

几个重要的配置类

ApplicationConfig

/**
 * 程序注解配置
 */
@Configuration
// 表示通过aop框架暴露该代理对象,AopContext能够访问
@EnableAspectJAutoProxy(exposeProxy = true)
// 指定要扫描的Mapper类的包的路径
@MapperScan({"com.platform.modules.**.dao"})
// 扫描spring工具类
@ComponentScan(basePackages = {"cn.hutool.extra.spring"})
public class ApplicationConfig {

    /**
     * 时区配置
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() {
        return builder -> builder.timeZone(TimeZone.getDefault());//去系统默认时区
    }

    /**
     * 序列化枚举值为数据库存储值
     *
     * @return
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
    }

    @Bean
    public static MappingJackson2HttpMessageConverter objectMapper() {
        final ObjectMapper objectMapper = new ObjectMapper();
        // 忽略未知的枚举字段
        objectMapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
        // 忽略多余的字段不参与序列化
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        // 忽略null属性字段
//        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // null属性字段转""
        objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
            @Override
            public void serialize(Object arg0, JsonGenerator arg1, SerializerProvider arg2) throws IOException {
                arg1.writeString("");
            }
        });
        SimpleModule simpleModule = new SimpleModule();
        // 格式化Long
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        // 格式化时间
        simpleModule.addSerializer(Date.class, new JsonSerializer<Date>() {
            @Override
            public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                jsonGenerator.writeString(DateUtil.format(date, DatePattern.NORM_DATETIME_FORMAT));
            }
        });
        // 格式化金额
        simpleModule.addSerializer(BigDecimal.class, new JsonSerializer<BigDecimal>() {
            @Override
            public void serialize(BigDecimal decimal, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                jsonGenerator.writeString(decimal.setScale(2, BigDecimal.ROUND_HALF_DOWN).toString());
            }
        });
        // 注册 module
        objectMapper.registerModule(simpleModule);
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setObjectMapper(objectMapper);
        return converter;
    }

}

WebsocketConfig

/*
1、如果使用默认的嵌入式容器 比如Tomcat 则必须手工在上下文提供ServerEndpointExporter。
2、如果使用外部容器部署war包,则不需要提供提供ServerEndpointExporter,因为此时SpringBoot默认将扫描 服务端的行为交给外部容器处理,所以线上部署的时候要把WebSocketConfig中这段注入bean的代码注掉
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Resource
    private BootWebSocketHandler bootWebSocketHandler;

    @Resource
    private BootWebSocketInterceptor bootWebSocketInterceptor;

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        webSocketHandlerRegistry
                .addHandler(bootWebSocketHandler, "/ws")
                .addInterceptors(bootWebSocketInterceptor)
                .setAllowedOrigins("*");
    }

}

PlatformConfig

@Component
@Configuration
@ConfigurationProperties(prefix = "platform")
public class PlatformConfig {

    /**
     * 上传路径
     */
    public static String ROOT_PATH;

    /**
     * 文件预览
     */
    public static String PREVIEW = "/preview/";

    /**
     * token超时时间(分钟)
     */
    public static Integer TIMEOUT;

    /**
     * 是否开启短信
     */
    public static YesOrNoEnum SMS;

    @Value("${platform.timeout}")
    public void setTokenTimeout(Integer timeout) {
        PlatformConfig.TIMEOUT = timeout;
    }

    @Value("${platform.sms:N}")  // 该值为Y表示开启短信服务
    public void setSms(String sms) {
        PlatformConfig.SMS = EnumUtils.toEnum(YesOrNoEnum.class, sms, YesOrNoEnum.NO);
    }

    @Value("${platform.rootPath}")
    public void setRootPath(String rootPath) {
        PlatformConfig.ROOT_PATH = rootPath;
    }
}

MybatisPlusConfig

@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        // 防全表更新与删除插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }

    /**
     * 批量操作增强
     */
    @Bean
    public DefaultSqlInjector mybatisSqlInjector() {
        return new DefaultSqlInjector();
    }
    // 源码中使用的mybatis-plus是3.4.3版本,而我们这里它改为了4.4.3版本,可省去一部分代码

}

CorsConfig跨域配置类

@Configuration
public class CorsConfig {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

该配置类是用于前端向后端访问是由于端口号不同需要支持跨域。

WebMvcConfig

/**
 * 通用配置
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Resource
    private VersionInterceptor versionInterceptor;

    @Resource
    private DeviceInterceptor deviceInterceptor;

    @Override
    public RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new VersionHandlerMapping();
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(ApplicationConfig.objectMapper());
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        /** 本地文件上传路径 */
//        registry.addResourceHandler(PlatformConfig.PREVIEW + "/**").addResourceLocations("file:" + PlatformConfig.ROOT_PATH + "/");
    }

    /**
     * 自定义拦截规则
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(versionInterceptor).addPathPatterns("/**");
        registry.addInterceptor(deviceInterceptor).addPathPatterns("/**");
    }
}

ShiroConfiguration

/**
 * ShiroConfiguration
 */
@Configuration
public class ShiroConfiguration {

    /**
     * 下面两个方法对 注解权限起作用有很大的关系,请把这两个方法,放在配置的最上面
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 身份认证Realm,此处的注入不可以缺少。否则会在UserRealm中注入对象会报空指针.
     * 将自己的验证方式加入容器
     */
    @Bean
    public ShiroRealm shiroRealm() {
        ShiroRealm realm = new ShiroRealm();
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

    /**
     * 配置shiro session 的一个管理器
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionListeners(new ArrayList<>());
        return sessionManager;
    }

    /**
     * 核心的安全事务管理器
     * 设置realm、cacheManager等
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置realm
        securityManager.setRealm(shiroRealm());
        // 设置sessionManager
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;否则@RequiresRoles等注解无法生效
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 哈希密码比较器。在myShiroRealm中作用参数使用
     * 登陆时会比较用户输入的密码,跟数据库密码配合盐值salt解密后是否一致。
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用md5算法;
        hashedCredentialsMatcher.setHashIterations(1);//散列的次数,比如散列两次,相当于 md5( md5(""));
        return hashedCredentialsMatcher;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //oauth过滤
        Map<String, Filter> filters = new HashMap<>(16);
        filters.put("oauth2", new ShiroTokenFilter());
        shiroFilterFactoryBean.setFilters(filters);
        //权限控制map
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 自定义拦截全部写到下面↓↓↓
        // 免登录接口,增加@IgnoreAuth注解
        // 自定义拦截全部写到上面↑↑↑
        filterChainDefinitionMap.put("/**", "oauth2");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

}

该配置类主要用于控制系统安全访问与授权,这个类需要关注ShiroRealm类。ShiroRealm#doGetAuthenticationInfo方法为获取当前登录用户认证信息, ShiroRealm#isPermitted方法用于校验当前登录用户是否有某项操作权限。

几个重要的属性配置类

OssConfig类源码

@Component
@Data
public class OssConfig {
    
    @Value("${oss.serverUrl}")
    private String serverUrl;
    @Value("${oss.accessKey}")
    private String accessKey;
    @Value("${oss.secretKey}")
    private String secretKey;
    @Value("${oss.bucketName}")
    private String bucketName;
    @Value("${oss.region}")
    private String region;
}

PushConfig类源码

/**
 * 推送配置
 */
@Component
@Data
public class PushConfig {

    @Value("${push.appId}")
    private String appId;

    @Value("${push.appKey}")
    private String appKey;

    @Value("${push.appSecret}")
    private String appSecret;

    @Value("${push.masterSecret}")
    private String masterSecret;
}

TrtcConfig类源码

/**
 * 读取trtc相关配置
 */
@Component
@Data
public class TrtcConfig {

    @Value("${trtc.appId}")
    private String appId;
    @Value("${trtc.expire}")
    private String expire;
    @Value("${trtc.secret}")
    private String secret;
}

TencentConfig类源码

/**
 * 腾讯nlp配置
 */
@Component
@Data
public class TencentConfig {

    @Value("${tencent.appId}")
    private String appId;
    
    @Value("${tencent.appKey}")
    private String appKey;

    @Value("${tencent.appSecret}")
    private String appSecret;
}

SmsConfig配置类源码

@Configuration
public class SmsConfig {

    @Bean
    public SmsClient smsClient(TencentConfig tencentConfig){
        Credential cred = new Credential(tencentConfig.getAppKey(), tencentConfig.getAppSecret());
        HttpProfile httpProfile = new HttpProfile();
        httpProfile.setReqMethod("POST");
        httpProfile.setConnTimeout(60);
        httpProfile.setEndpoint("sms.tencentcloudapi.com");
        ClientProfile clientProfile = new ClientProfile();
        clientProfile.setSignMethod("HmacSHA256");
        clientProfile.setHttpProfile(httpProfile);
        SmsClient smsClient = new SmsClient(cred, "ap-guangzhou",clientProfile);
        return smsClient;
    }
}

AmapConfig类源码

/**
 * 读取高德地图相关配置
 */
@Component
@Data
public class AmapConfig {

    @Value("${amap.key}")
    private String key;
}

这几个类在application-dev.yml文件中都有其对应的配置变量,在IntelliJ IDEA中打开对应的属性配置类,然后按住Ctrl键+鼠标单击对应的属性配置类就可以看到该类在那些服务类或工具类中被用到。

几个重要的服务类

文件服务类

文件服务接口类FileService源码

/**
 * 文件服务
 */
public interface FileService {

    /**
     * 普通文件上传
     *
     * @param file
     * @return
     */
    UploadFileVo uploadFile(MultipartFile file);

    /**
     * 视频文件上传
     *
     * @param file
     * @return
     */
    UploadVideoVo uploadVideo(MultipartFile file);

    /**
     * 音频文件上传
     *
     * @param file
     * @return
     */
    UploadAudioVo uploadAudio(MultipartFile file);

}

文件服务实现类FileServiceImpl源码

@Service("fileService")
public class FileServiceImpl implements FileService {

    @Resource
    private OssConfig ossConfig;

    @Resource
    private TencentConfig tencentConfig;

    @Override
    public UploadFileVo uploadFile(MultipartFile file) {
        String fileType = FileNameUtil.extName(file.getOriginalFilename());
        if ("webp".equalsIgnoreCase(fileType)) {
            throw new BaseException(StrUtil.format("暂不支持{}格式上传", fileType));
        }
        // 初始化
        BaseUtils.init(BeanUtil.toBean(ossConfig, UploadConfig.class));
        // 上传
        return OssUtils.uploadFile(file);
    }

    @Override
    public UploadVideoVo uploadVideo(MultipartFile videoFile) {
        // 初始化
        BaseUtils.init(BeanUtil.toBean(ossConfig, UploadConfig.class));
        // 上传视频文件
        UploadFileVo videoFileVo = OssUtils.uploadFile(videoFile);
        return BeanUtil.toBean(videoFileVo, UploadVideoVo.class)
                .setScreenShot(videoFileVo.getFullPath() + AppConstants.VIDEO_PARAM);
    }

    @Override
    public UploadAudioVo uploadAudio(MultipartFile audioFile) {
        // 初始化
        BaseUtils.init(BeanUtil.toBean(ossConfig, UploadConfig.class));
        // 上传音频文件
        UploadFileVo audioFileVo = OssUtils.uploadFile(audioFile);
        String data;
        try {
            data = Base64.encode(audioFile.getInputStream());// 字节流编码
        } catch (IOException e) {
            throw new BaseException("语音识别接口调用异常,请稍后再试");
        }
        return BeanUtil.toBean(audioFileVo, UploadAudioVo.class).setSourceText(TencentUtils.audio2Text(tencentConfig, data)); // 音频转文本
    }
}

实时音视频服务

实时音视频接口类TrtcService

/**
 * 实时语音/视频
 */
public interface TrtcService {

    /**
     * 实时语音/视频
     */
    TrtcVo getSign();

}

实时音视频接口实现类TrtcServiceImpl

@Service("trtcService")
public class TrtcServiceImpl implements TrtcService {

    @Resource
    private TrtcConfig trtcConfig;

    @Resource
    private RedisUtils redisUtils;

    @Override
    public TrtcVo getSign() {
        String key = AppConstants.REDIS_TRTC_SIGN + ShiroUtils.getUserId();
        if (redisUtils.hasKey(key)) {
            return JSONUtil.toBean(redisUtils.get(key), TrtcVo.class);
        }
        String userId = AppConstants.REDIS_TRTC_USER + ShiroUtils.getUserId();
        long currTime = DateUtil.currentSeconds();
        Dict doc = Dict.create()
                .set("TLS.ver", "2.0")
                .set("TLS.identifier", userId)
                .set("TLS.sdkappid", trtcConfig.getAppId())
                .set("TLS.expire", trtcConfig.getExpire())
                .set("TLS.time", currTime)
                .set("TLS.sig", hmacsha256(userId, currTime));
        Deflater compressor = new Deflater();
        compressor.setInput(JSONUtil.toJsonStr(doc).getBytes(StandardCharsets.UTF_8));
        compressor.finish();
        byte[] bytes = new byte[2048];
        int length = compressor.deflate(bytes);
        compressor.end();
        TrtcVo trtcVo = new TrtcVo().setUserId(userId)
                .setAppId(trtcConfig.getAppId())
                .setExpire(trtcConfig.getExpire())
                .setSign(base64EncodeUrl(ArrayUtil.resize(bytes, length)));
        redisUtils.set(key, JSONUtil.toJsonStr(trtcVo), 5, TimeUnit.DAYS);
        return trtcVo;
    }

    private String hmacsha256(String userId, long currTime) {
        String contentToBeSigned = "TLS.identifier:" + userId + "\n"
                + "TLS.sdkappid:" + trtcConfig.getAppId() + "\n"
                + "TLS.time:" + currTime + "\n"
                + "TLS.expire:" + trtcConfig.getExpire() + "\n";
        HMac mac = new HMac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(trtcConfig.getSecret(), StandardCharsets.UTF_8));
        byte[] signed = mac.digest(contentToBeSigned);
        return cn.hutool.core.codec.Base64.encode(signed);
    }

    private String base64EncodeUrl(byte[] input) {
        byte[] base64 = Base64.encode(input).getBytes(StandardCharsets.UTF_8);
        for (int i = 0; i < base64.length; ++i)
            switch (base64[i]) {
                case '+':
                    base64[i] = '*';
                    break;
                case '/':
                    base64[i] = '-';
                    break;
                case '=':
                    base64[i] = '_';
                    break;
                default:
                    break;
            }
        return new String(base64);
    }
}

短信服务接口类SmsService

/**
 * 短信 服务层
 */
public interface SmsService {

    /**
     * 发送短信
     * @return
     */
    Dict sendSms(SmsVo smsVo);

    /**
     * 验证短信
     * @return
     */
    void verifySms(String phone, String code, SmsTypeEnum type);

}

短信服务接口实现类SmsServiceImpl

/**
 * 短信服务类
 */
@Service("smsService")
@Slf4j
public class SmsServiceImpl implements SmsService {

    @Resource
    private RedisUtils redisUtils;
    
    @Resource
    private SmsClient smsClient;
    @Value("${tencent.appId}")
    private String sdkAppId;

    @Override
    public Dict sendSms(SmsVo smsVo) {
        // 验证手机号
        if (!Validator.isMobile(smsVo.getPhone())) {
            throw new BaseException("请输入正确的手机号");
        }
        SmsTypeEnum smsType = smsVo.getType();
        String key = smsType.getPrefix().concat(smsVo.getPhone());
        // 生成验证码
        String code = String.valueOf(RandomUtil.randomInt(1000, 9999));
        // 发送短信
        if (YesOrNoEnum.YES.equals(PlatformConfig.SMS)) {
            Dict dict = Dict.create()
                    .set("code", code);
            SmsTemplateEnum templateEnum;
            if(SmsTypeEnum.LOGIN.getCode().equals(smsVo.getType().getCode())){
                templateEnum = SmsTemplateEnum.LOGIN_VERIFY_CODE;
            }else if(SmsTypeEnum.REGISTERED.getCode().equals(smsVo.getType().getCode())){
                templateEnum = SmsTemplateEnum.REGISTER_VERIFY_CODE;
            } else {
                templateEnum = SmsTemplateEnum.RESET_PASS_VERIFY_CODE;
            }
            doSendSms(smsVo.getPhone(), templateEnum, dict);
        }
        // 存入缓存
        redisUtils.set(key, code, smsType.getTimeout(), TimeUnit.MINUTES);
        return Dict.create().set("expiration", smsType.getTimeout());
    }

    @Override
    public void verifySms(String phone, String code, SmsTypeEnum type) {
        // 验证手机号
        if (!Validator.isMobile(phone)) {
            throw new BaseException("请输入正确的手机号");
        }
        String key = type.getPrefix().concat(phone);
        if (!redisUtils.hasKey(key)) {
            throw new BaseException("验证码已过期,请重新获取");
        }
        String value = redisUtils.get(key);
        if (value.equalsIgnoreCase(code)) {
            redisUtils.delete(key);
        } else {
            throw new BaseException("验证码不正确,请重新获取");
        }
    }

    /**
     * 执行发送短信
     *
     * @param phone
     * @param templateCode
     * @param dict
     */
    private void doSendSms(String phone, SmsTemplateEnum templateCode, Dict dict) {
        SendSmsRequest req = new SendSmsRequest();
        req.setSmsSdkAppId(sdkAppId);
        String signName = "你申请的短信签名";
        req.setSignName(signName);
        req.setTemplateId(templateCode.getCode());
        String[] templateParamSet = {dict.getStr("code"), dict.getStr("timeout")};
        req.setTemplateParamSet(templateParamSet);
        if(!phone.startsWith("+86")){
            phone = "+86" + phone;
        }
        String[] phoneNumberSet = {phone};
        req.setPhoneNumberSet(phoneNumberSet);
        try {
            SendSmsResponse res = smsClient.SendSms(req);
            log.info("doSendSms_res:"+ JSONUtil.toJsonStr(res));
        } catch (TencentCloudSDKException e) {
            log.error("doSendSms_error", e);
        }
    }

}

这个类笔者作了一些修改,并接入了腾讯云的短信服务,实现了发送短信功能

聊天消息推送服务接口类ChatPushService

/**
 * 用户推送 服务层
 * q3z3
 */
public interface ChatPushService {

    /**
     * 注册别名
     */
    void setAlias(Long userId, String cid);

    /**
     * 解除别名
     */
    void delAlias(Long userId, String cid);

    /**
     * 发送消息
     */
    void pushMsg(PushParamVo from, PushMsgTypeEnum msgType);

    /**
     * 发送消息
     */
    void pushMsg(List<PushParamVo> userList, PushMsgTypeEnum msgType);

    /**
     * 发送消息
     */
    void pushMsg(List<PushParamVo> userList, PushParamVo group, PushMsgTypeEnum msgType);

    /**
     * 拉取离线消息
     */
    void pullOffLine(Long userId);

    /**
     * 发送通知
     */
    void pushNotice(PushParamVo paramVo, PushNoticeTypeEnum pushNoticeType);

    /**
     * 发送通知
     */
    void pushNotice(List<PushParamVo> userList, PushNoticeTypeEnum pushNoticeType);

}

聊天消息推送服务接口实现类ChatPushServiceImpl

@Service("chatPushService")
@Slf4j
public class ChatPushServiceImpl implements ChatPushService {

    @Resource
    private RedisUtils redisUtils;

    @Resource
    private PushConfig pushConfig;

    @Resource
    private BootWebSocketHandler bootWebSocketHandler;

    /**
     * 消息长度
     */
    private static final Integer MSG_LENGTH = 2048;

    @Override
    public void setAlias(Long userId, String cid) {
        // 异步注册
        PushTokenDto pushTokenDto = initPushToken();
        ThreadUtil.execAsync(() -> {
            PushAliasVo aliasVo = new PushAliasVo()
                    .setCid(cid)
                    .setAlias(NumberUtil.toStr(userId));
            PushUtils.setAlias(pushTokenDto, aliasVo);
        });
    }

    @Override
    public void delAlias(Long userId, String cid) {
        // 异步注册
        PushTokenDto pushTokenDto = initPushToken();
        ThreadUtil.execAsync(() -> {
            PushAliasVo aliasVo = new PushAliasVo()
                    .setCid(cid)
                    .setAlias(NumberUtil.toStr(userId));
            PushUtils.delAlias(pushTokenDto, aliasVo);
        });
    }

    @Override
    public void pushMsg(PushParamVo from, PushMsgTypeEnum msgType) {
        PushTokenDto pushTokenDto = initPushToken();
        // 异步发送
        ThreadUtil.execAsync(() -> {
            doMsg(from, null, msgType, pushTokenDto);
        });
    }

    @Override
    public void pushMsg(List<PushParamVo> userList, PushMsgTypeEnum msgType) {
        PushTokenDto pushTokenDto = initPushToken();
        // 异步发送
        ThreadUtil.execAsync(() -> {
            userList.forEach(e -> {
                doMsg(e, e, msgType, pushTokenDto);
            });
        });
    }

    @Override
    public void pushMsg(List<PushParamVo> userList, PushParamVo group, PushMsgTypeEnum msgType) {
        PushTokenDto pushTokenDto = initPushToken();
        // 异步发送
        ThreadUtil.execAsync(() -> {
            userList.forEach(e -> {
                doMsg(e, group, msgType, pushTokenDto);
            });
        });
    }

    /**
     * 发送消息
     */
    private void doMsg(PushParamVo from, PushParamVo to, PushMsgTypeEnum msgType, PushTokenDto pushTokenDto) {
        Long userId = from.getToId();
        // 组装消息体