rpush:多平台统一消息推送系统

本贴最后更新于 1185 天前,其中的信息可能已经事过境迁

gitee 代码传送

一个接口触达多平台(包括微信公众号、企业微信、钉钉、邮箱等任何想的到平台,都能一个接口一次推送;极简的代码调用,极大减少业务方消息推送的代码量);
同时提供基于 netty 和 websocket 的即时通讯实现,实现单聊、群聊等功能。开箱即用,采用 SpringCloud 微服务架构,扩展简单且没有单点问题。致力于包揽所有和消息推送有关的技术开发工作,节省开发资源。

  • 一个接口触达多平台,支持一个接口多平台同时发送
  • 消息平台逻辑与业务逻辑的解耦,业务方不需要关心各个平台的对接实现,只需要关心:要用哪些平台发、要发给对应平台的哪些人、要发什么内容
  • 极强的扩展性,要新增一个消息平台的支持,理论只需要新增几个类就能完成,且不需要写任何前端代码即可获得该平台对应的 ui 交互(包括:配置交互、接收人维护、web 手动消息发送交互等)。
  • 当然支持 web 端手动发送消息
  • 当然也支持定时任务
  • 消息方案预设置
  • 提供即时通讯实现,且支持服务器横向扩展
  • 接收人导入
  • 接收人按分组划分
  • 消息日志
  • ...

在线体验

http://159.75.121.163/
admin admin

目前支持的消息类型

  • 邮箱
  • 企业微信-应用消息
    • 文本消息
    • 图片消息
    • 视频消息
    • 文本消息
    • 文本卡片消息
    • 图文消息
    • Markdown 消息
  • 企业微信-群机器人
    • 文本消息
    • 图片消息
    • 图文消息
    • Markdown 消息
  • 微信公众号
    • 文本消息
    • 图文消息
    • 模板消息
  • 钉钉-工作通知
    • 文本
    • Markdown
    • 链接消息
    • 卡片消息
    • OA 消息
  • 钉钉-群机器人
    • 文本
    • Markdown
    • 链接消息
    • 卡片消息
    • FeedCard

Rpush 的架构决定了扩展一个消息平台的消息类型会非常简单,所以如果要扩展一个消息平台,大部分时间都会花在查找该平台的对接文档上。后续会在工作之余加上其它的平台或消息类型。当然,欢迎参与扩展(扩展一个消息平台的消息类型,只需要几个 java 类即可,不需要写任何前端代码,即可获得包括 ui 交互内的所有功能)。

效果展示

单个消息类型发送示例

1.gif

web 端多平台发送示例

2.gif

postman 多平台发送示例

3.gif

用代码发消息

秉持”业务服务只负责发消息“的解耦原则,业务服务在需要发消息的时候,代码应该越简单越好。所以,Rpush 的发消息的 sdk,一种消息只需要一行代码,有几种消息就有几行代码。比如这样:

/**
 * @author shuangmulin
 * @since 2021/6/8/008 11:37
 **/
public class RpushSenderTest {
    /**
     * 要发送的内容
     */
    public static final String content = "您的会议室已经预定 \n" +
            ">**事项详情** \n" +
            ">事 项:开会\n" +
            ">组织者:@miglioguan \n" +
            ">参与者:@miglioguan、@kunliu、@jamdeezhou、@kanexiong、@kisonwang \n" +
            "> \n" +
            ">会议室:广州TIT 1楼 301\n" +
            ">日 期:2021年5月18日\n" +
            ">时 间:上午9:00-11:00\n" +
            "> \n" +
            ">请准时参加会议。 \n" +
            "> \n" +
            ">如需修改会议信息,请点击:[修改会议信息](https://work.weixin.qq.com)";

    public static void main(String[] args) {
        // 企业微信-markdown消息
        MarkdownMessageDTO markdown = RpushMessage.WECHAT_WORK_AGENT_MARKDOWN().content(content).receiverIds(Collections.singletonList("ZhongBaoLin")).build();
        // 企业微信-群机器人消息
        TextMessageDTO text = RpushMessage.WECHAT_WORK_ROBOT_TEXT().content(content).receiverIds(Collections.singletonList("ZhongBaoLin")).build();
        // 邮箱
        EmailMessageDTO email = RpushMessage.EMAIL().title("会议通知").content(content).build();
        RpushService.instance("baolin", "666666").sendMessage(markdown, text, email); // 填上账号密码,运行即可
    }
}

以上代码,一次发送了三种不同平台的不同消息类型,全部代码加起来也只需要四五行代码而已。要获得以上效果,只需要 maven 引用 rpush 的 sdk 模块即可:


<project>
    <!-- 设置 jitpack.io 仓库 -->
    <repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>

    <dependencies>
        <!-- 添加rpush-sdk依赖 -->
        <dependency>
            <groupId>com.github.shuangmulin.rpush</groupId>
            <artifactId>rpush-sdk</artifactId>
            <version>v1.0.2</version>
        </dependency>
    </dependencies>
</project>

即时通讯

Rpush 对即时通讯的实现方式比较 包容
,即对具体的连接实现做了解耦,不局限于某一种连接方式,可以 netty,可以 websocket,可以 comet,当然也可以用原始的 bio 来做。这里展示 websocke 的网页端和 netty 实现的命令行客户端之间互相单聊和群聊的效果(该示例的相关代码:客户端示例代码地址):
4.gif

一些比较核心的扩展点

1. 可自由扩展的消息平台和消息类型

在 Rpush 的设计里,消息被归类为“消息平台”和“消息类型”,分别对应如下两个枚举:

/**
 * 消息平台枚举
 **/
public enum MessagePlatformEnum {

    EMAIL(EmailConfig.class, "邮箱", "", "^[_a-z0-9-]+(\\.[_a-z0-9-]+)*@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,})$", true),
    WECHAT_WORK_AGENT(WechatWorkAgentConfig.class, "企业微信-应用消息", "", "", true),
    WECHAT_WORK_ROBOT(WechatWorkRobotConfig.class, "企业微信-群机器人", "", "", true),
    WECHAT_OFFICIAL_ACCOUNT(WechatOfficialAccountConfig.class, "微信公众号", "", "", true),
    DING_TALK_CORP(DingTalkCorpConfig.class, "钉钉-工作通知", "", "", true),
    RPUSH_SERVER(EmptyConfig.class, "rpush服务", "", "", true);
}

/**
 * 消息类型枚举
 **/
public enum MessageType {

    EMAIL("普通邮件 ", MessagePlatformEnum.EMAIL),
    RPUSH_SERVER("文本", MessagePlatformEnum.RPUSH_SERVER),

    // ================================企业微信-应用====================================
    WECHAT_WORK_AGENT_TEXT("文本", MessagePlatformEnum.WECHAT_WORK_AGENT),
    WECHAT_WORK_AGENT_IMAGE("图片", MessagePlatformEnum.WECHAT_WORK_AGENT),
    WECHAT_WORK_AGENT_VIDEO("视频", MessagePlatformEnum.WECHAT_WORK_AGENT),
    WECHAT_WORK_AGENT_FILE("文件", MessagePlatformEnum.WECHAT_WORK_AGENT),
    WECHAT_WORK_AGENT_TEXTCARD("文本卡片", MessagePlatformEnum.WECHAT_WORK_AGENT),
    WECHAT_WORK_AGENT_NEWS("图文消息", MessagePlatformEnum.WECHAT_WORK_AGENT),
    WECHAT_WORK_AGENT_MARKDOWN("Markdown", MessagePlatformEnum.WECHAT_WORK_AGENT),

    // ================================企业微信-群机器人====================================
    WECHAT_WORK_ROBOT_TEXT("文本", MessagePlatformEnum.WECHAT_WORK_ROBOT),
    WECHAT_WORK_ROBOT_IMAGE("图片", MessagePlatformEnum.WECHAT_WORK_ROBOT),
    WECHAT_WORK_ROBOT_NEWS("图文消息", MessagePlatformEnum.WECHAT_WORK_ROBOT),
    WECHAT_WORK_ROBOT_MARKDOWN("Markdown", MessagePlatformEnum.WECHAT_WORK_ROBOT),

    // ================================微信公众号====================================
    WECHAT_OFFICIAL_ACCOUNT_TEXT("文本", MessagePlatformEnum.WECHAT_OFFICIAL_ACCOUNT),
    WECHAT_OFFICIAL_ACCOUNT_NEWS("图文消息", MessagePlatformEnum.WECHAT_OFFICIAL_ACCOUNT),
    WECHAT_OFFICIAL_ACCOUNT_TEMPLATE("模板消息", MessagePlatformEnum.WECHAT_OFFICIAL_ACCOUNT),

    // ================================钉钉-工作通知====================================
    DING_TALK_COPR_TEXT("文本", MessagePlatformEnum.DING_TALK_CORP),
    DING_TALK_COPR_MARKDOWN("Markdown", MessagePlatformEnum.DING_TALK_CORP),
    DING_TALK_COPR_LINK("链接消息", MessagePlatformEnum.DING_TALK_CORP),
    DING_TALK_COPR_ACTION_CARD_SINGLE("卡片-单按钮", MessagePlatformEnum.DING_TALK_CORP),
    DING_TALK_COPR_ACTION_CARD_MULTI("卡片-多按钮", MessagePlatformEnum.DING_TALK_CORP),
    DING_TALK_COPR_OA("OA消息", MessagePlatformEnum.DING_TALK_CORP),

    ;
}

这里拿“企业微信-应用的文本类型”的消息举例。假设现在要在 Rpush 实现这个类型的消息,步骤如下:

  1. 定义企业微信的配置类,如下:
/**
 * 企业微信配置
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class WechatWorkAgentConfig extends Config {
    private static final long serialVersionUID = -9206902816158196669L;

    @ConfigValue(value = "企业ID", description = "在此页面查看:https://work.weixin.qq.com/wework_admin/frame#profile")
    private String corpId;
    @ConfigValue(value = "应用Secret")
    private String secret;
    @ConfigValue(value = "应用agentId")
    private Integer agentId;

}

里面的字段就按对应平台需要的字段去定义就行,比如这里的企业微信就只有三个字段需要配置。而每个字段上的 @ConfigValue
注解,是用来自动生成页面的,也就是说,只需要打上这个注解,就可以自动在页面上生成对应的增删改查的界面和交互(无需写一行前端代码)。

  1. MessagePlatformEnumMessageType
    里加上对应的枚举,即 WECHAT_WORK_AGENT(WechatWorkAgentConfig.class, "企业微信-应用消息", "", "", true)
    WECHAT_WORK_AGENT_TEXT("文本", MessagePlatformEnum.WECHAT_WORK_AGENT),。这里要注意下平台枚举的第一个参数就是第一步定义的配置类的 Class。
  2. 定义企业微信-应用-文本消息的参数,如下:
/**
 * 企业微信消息发送DTO
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class TextMessageDTO extends BaseMessage {
    private static final long serialVersionUID = -3289428483627765265L;

    /**
     * 接收人分组列表
     */
    @SchemeValue(type = SchemeValueType.RECEIVER_GROUP)
    private List<Long> receiverGroupIds;

    /**
     * 接收人列表
     */
    @SchemeValue(type = SchemeValueType.RECEIVER)
    private List<String> receiverIds;

    @SchemeValue(description = "PartyID列表,非必填,多个接受者用‘|’分隔。当touser为@all时忽略本参数")
    private String toParty;

    @SchemeValue(description = "TagID列表,非必填,多个接受者用‘|’分隔。当touser为@all时忽略本参数")
    private String toTag;

    @SchemeValue(type = SchemeValueType.TEXTAREA, description = "请输入内容...")
    private String content;

}

同样的,里面的字段根据该消息类型需要的字段去定义就行。比如企业微信-应用-文本消息就只需要一个 content 内容字段以及接收人相关的字段。这里涉及到的 @SchemeValue
注解,同样也是用来自动生成页面交互的,即只需要打上这个注解,就能自动在发消息页面生成对应的 ui 和交互。同时可以使用 com.regent.rpush.route.utils.sdk.SdkGenerator 类自动生成 sdk 代码。

  1. 实现 com.regent.rpush.route.handler.MessageHandler 接口,正式写发消息的代码。
/**
 * 企业微信文本消息handler
 **/
@Component
public class AgentTextMessageHandler extends MessageHandler<TextMessageDTO> {

    @Override
    public MessageType messageType() {
        return MessageType.WECHAT_WORK_AGENT_TEXT;
    }

    @Override
    public void handle(TextMessageDTO param) {
        // 具体的发消息代码
    }
}

这里有以下需要关心的点:

  • 接口上的泛型填第 3 步定义的类
  • 实现 messageType 方法,返回当前类要处理的消息类型
  • 实现 handle 方法,写发消息的代码,里面的参数是自动解析到这个方法的,直接使用即可

到这里,就不需要多做任何其它的事了。也就是说,做完以上四个步骤,就已经完成了一个消息类型的扩展。事做的少,获得的功能并不少:

  1. 自动获得对应平台配置的增删改交互和 ui
    a.png
  2. 自动获得对应平台接收人和接收人分组的增删改交互和 ui(包括导入功能)
    b.png
  3. 自动获得该消息类型手动发送消息的交互和 ui
    c.png
  4. 执行 com.regent.rpush.route.utils.sdk.SdkGenerator,自动完成该消息类型的 sdk 代码
  5. 自动获得该消息类型的定时任务的增删改交互和 uid.png而且增加一个消息类型,不会对业务服务之前正在使用的消息类型有任何影响,是纯粹的叠加”能力“。

2. 可自由扩展的即时通讯实现

在 rpush 的架构里,投递一个消息的流程大致可以概括为:调用统一的接口向路由服务投递消息-》路由服务查出消息目标所在的服务器地址-》路由服务向对应的服务器传递消息-》对应的服务找到对应的会话发送消息。
这里的扩展点在最后一步,即用户和服务器的会话维护。要实现服务端向客户端推送消息,会有比较多的解决方案,比如用 netty 起一个 nio 服务器,客户端去连 netty 服务器或者服务端用 socketio 提供 websocket 实现,客户端按 websocket 的方式连服务器或者用 comet 实现长连接让客户端连等等。

这里做成扩展点的一个比较重要的考虑点就是要实现不同端之间的消息通信,比如上面例子里的命令行和网页之间的聊天,或者实现移动端和网页之间的聊天。

RpushClient

不管是什么技术实现的服务端推送,都会有一个“客户端”性质的类,比如 netty 会有 Channel,socketio 提供的 websocket 会有 SocketIOClient
。而对于 rpush 来说,只关心它们的一个共有的能力:消息投递。即 RpushClient 接口:

/**
 * 客户端
 **/
public interface RpushClient {

    /**
     * 推送消息
     */
    void pushMessage(NormalMessageDTO message);

    void close();
}

只要实现了这个接口的类,不管是什么技术的实现,都被认为是 rpush 的客户端。也就是说,netty 也好,websocket 也好,只要提供给 rpush 这个接口的能力即可,从而达到解耦具体实现的目的。
目前 rpush 已经做了 netty 和 socketio 两个实现,分别对应 com.regent.rpush.server.socket.nio.NioSocketChannelClient
com.regent.rpush.server.socket.websocket.WebSocketClient 两个类。

netty 客户端 sdk

rpush 提供了 netty 对应的客户端的 sdk,项目依赖 rpush-client 即可,使用也非常简单,只需要几行代码即可。

public class Main {
    public static void main(String[] args) {
        RpushClient rpushClient = new RpushClient(servicePath, registrationId); // 填上rpush服务地址和id
        rpushClient.addMsgProcessor(new PingIgnoreMsgProcessor()); // 忽略心跳消息
        rpushClient.start(); // 向服务端发起连接
        rpushClient.addMsgProcessor(msg -> {
            // 处理接收到的消息
            return false;
        });
    }
}

关于架构

rpush 目前主要提供两大功能,一个是消息分发,另一个是即时通讯功能。消息分发由路由服务 rpush-route 提供,即时通讯的长连接维护由 socket 服务 rpush-server 服务提供。

1. 可自由集群的路由服务

为了保证消息投递的统一性以及解耦消息分发和即时通讯之间的关系,路由服务只做一件事,即 负责将消息分发到各个平台,也就是说 rpush 提供的即时通讯功能,对路由服务来说和其他第三方平台没什么区别,都被视为一个 平台
所以在架构层面,如果只需要用到消息分发的功能,就不需要部署 rpush-server
服务,只需要部署 eureka、zuul 和路由服务即可。zuul 作为系统对外的入口,隔离掉了路由服务器和用户端,同时路由服务又是无状态的,这样就使得路由服务可以根据实际业务情况自由集群,即想加一台路由服务就加,想减一台就减。

2. 可自由集群的 socket 服务

rpush-server 作为 socket 服务,主要功能就是维护客户端的长连接。这个服务的承载能力直接决定了即时通讯功能一次可以在线多少用户,所以这个服务毫无疑问必须要是可集群部署的。
假设部署 5 台 socket 服务,都正常配置 eureka 为注册中心。 为了实现 socket 服务的集群,一个客户端连接 rpush 服务的流程为:

  1. 客户端问路由服务要一个可用的 socket 服务器 ip 和端口
  2. 路由服务通过合适的负载均衡算法得到一个可用的 socket 服务器 ip 和端口并返回给客户端
  3. 客户端向拿到的 socket 服务发起长连接
  4. 连接成功后,对应的 socket 服务器维护服务级别的 session 信息,然后向路由服务汇报该客户端,路由服务保存该客户端和 socket 服务的对应关系
  5. 客户端与对应的 socket 服务保持一定频率的心跳,并在心跳失败判定连接断开后重新发起以上流程,直到再次连接成功

客户端基于以上步骤上线之后,其他客户端向该客户端投递消息的流程为:

  1. 客户端请求路由服务提供的消息投递接口(这个就是前面说到的消息投递接口,因为 socket 服务维护的长连接对路由服务来说和其他第三方平台没什么区别,所以消息投递的方式也是一样的)
  2. 路由服务实现的 socket 服务消息处理器(com.regent.rpush.route.handler.RpushMessageHandler),根据消息上的目标客户端 id 找到对应的 socket 服务,并向该服务投递消息
  3. socket 服务从自己维护的 session 里找到目标客户端,最终完成消息投递

实现以上流程之后,socket 服务就可以做到自由集群了。

上面说的流程偏理论化,有几个技术实现点这里做一下详细说明:

  1. 路由服务如何通过合适的负载均衡算法得到一个可用的 socket 服务器 ip 和端口?

实现的手段其实非常的简单暴力。首先由 socket 服务提供一个查询本机 ip 和端口的接口,路由服务直接通过 ribbon 去请求这个接口,然后自定义一个负载均衡规则类,来实现 socket 服务的选择:

/**
 * 路由->Socket服务端请求的实例选择
 */
public class ServerBalancer extends ZoneAvoidanceRule {

    @Override
    public Server choose(Object o) {
        // ...
        // 用默认的负载均衡算法选出一个可用的socket服务(这里的算法可以根据实际业务更改)
        return super.choose(o);
        // ...
    }

}

在配置文件里配置这个“规则类”:

rpush-server:
  ribbon:
    NFLoadBalancerRuleClassName: com.regent.rpush.route.loadbalancer.ServerBalancer

路由服务在向 socket 服务请求的时候会”经过“这个”规则类“,然后由这个“规则类”来选出一个可用的 socket 服务。最终 socket 服务的端口和 ip 信息,也是由选中的 socket 服务通过这次请求返回给路由服务的。
当然这个规则类不是只做这一件事,还有一个问题也需要这个类来完成。

  1. 消息投递的时候,路由服务如何根据消息上的目标客户端 id 找到对应的 socket 服务?

首先,在客户端与某一个 socket 服务连接成之后,客户端与 socket 服务之间的关系需要保存起来(mysql 或 redis)。然后新增一个 feign 的请求拦截器(com.regent.rpush.route.loadbalancer.MessageRequestInterceptor
):


@Component
public class MessageRequestInterceptor implements RequestInterceptor {

    /**
     * 存放本次消息投递的目标socket服务id
     */
    static final ThreadLocal<String> SERVER_ID = new ThreadLocal<>();

    @Autowired
    private IRpushServerOnlineService rpushServerOnlineService;

    @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
    @Override
    public void apply(RequestTemplate requestTemplate) {
        String url = requestTemplate.url();
        String method = requestTemplate.method();
        if (!"/push".equals(url) || !"POST".equals(method)) {
            // 只处理消息投递接口
            return;
        }

        // 如果是消息推送,需要给接收端连接的服务端投放消息,在服务端集群的情况下,要找到对应的服务端
        String body = new String(requestTemplate.body());
        JSONObject jsonObject = new JSONObject(body);
        String sendTo = jsonObject.getStr("sendTo"); // 拿到目标客户端的id
        String serverId = ""; // 从redis或mysql查到该客户端对应的socket服务id
        SERVER_ID.set(serverId); // 添加到当前线程里
    }

}

这个“拦截类”配合上面的”规则类“,就能在路由服务向 socket 服务传递消息时准确的找到对应的 socket 服务。 完整的”规则类“:

/**
 * 路由->Socket服务端请求的实例选择
 */
public class ServerBalancer extends ZoneAvoidanceRule {

    @Override
    public Server choose(Object o) {
        try {
            // 从拦截类里看有没有指定服务端实例
            String serverId = MessageRequestInterceptor.SERVER_ID.get();
            if (StringUtils.isEmpty(serverId)) {
                // 如果没有指定服务端实例,用默认的负载均衡算法
                return super.choose(o);
            }
            // 如果指定了服务端实例,说明是消息传递,用指定好的实例向socket服务发请求
            List<Server> servers = getLoadBalancer().getAllServers();
            for (Server server : servers) {
                if (StringUtils.equals(server.getId(), serverId)) {
                    return server;
                }
            }
            throw new IllegalArgumentException("没有可用的RPUSH_SERVER实例");
        } finally {
            MessageRequestInterceptor.SERVER_ID.remove();
        }
    }
}

而且有了这两个类,路由服务向 socket 服务传递消息的代码也会非常的”干净“:


@Component
public class RpushMessageHandler extends MessageHandler<RpushMessageDTO> {

    // ...
    @Override
    public void handle(RpushMessageDTO param) {
        List<String> sendTos = param.getReceiverIds();

        for (String sendTo : sendTos) {
            // ...
            messagePushService.push(build); // 路由服务直接调用接口请求即可,”规则类“和”拦截类“屏蔽掉了其它逻辑,所以这里不需要关心会不会发给错误socket服务
        }
    }
}
3. 其它
  1. 队列。路由服务内部用 Disruptor 环形队列做了异步处理,尽可能地让消息推送接口更快地返回。如果是并发量较高的情况,可以加入 kafka,路由服务直接监听 kafka 的消息,以此来提升服务整体性能。
  2. 缓存。客户端的上线信息可根据情况做多级缓存。即路由服务内部缓存 +redis 缓存,当然加的缓存越多,缓存一致性的问题就越复杂,需要考虑的情况也会更多。redis 也是需要根据实际情况来决定是否要集群部署。
  3. 监控。可使用 Spring Boot Admin 做服务状态监控。

用 docker-compose 快速部署一个 Rpush 服务

version: '2'
services:
  nginx:
    image: nginx
    container_name: nginx
    ports:
      - 80:80
    volumes:
      - /data/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
      - /data/nginx/log:/var/log/nginx
      - /data/nginx/html:/usr/share/nginx/html
  rpush-eureka:
    image: shuangmulin/rpush-eureka
    container_name: rpush-eureka
    ports:
      - 8761:8761
  rpush-zuul:
    image: shuangmulin/rpush-zuul
    environment:
      - eureka-service-ip=172.16.0.11
      - eureka-service-port=8761
    container_name: rpush-zuul
    ports:
      - 8124:8124
  rpush-route:
    image: shuangmulin/rpush-route
    environment:
      - eureka-service-ip=localhost
      - eureka-service-port=8761
      - jdbc.url=jdbc:mysql://localhost:3306/rpush?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
      - jdbc.username=root
      - jdbc.password=123456
      - super-admin.username=superadmin
      - super-admin.password=superadmin
      - jwtSigningKey=fjksadjfklds
    container_name: rpush-route
    ports:
      - 8121:8121
  rpush-server:
    image: shuangmulin/rpush-server
    environment:
      - eureka-service-ip=localhost
      - eureka-service-port=8761
    container_name: rpush-server
    ports:
      - 8122:8122
  rpush-scheduler:
    image: shuangmulin/rpush-scheduler
    environment:
      - eureka-service-ip=localhost
      - eureka-service-port=8761
      - jdbc.url=jdbc:mysql://localhost:3306/rpush?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
      - jdbc.username=root
      - jdbc.password=123456
      - super-admin.username=superadmin
      - super-admin.password=superadmin
      - jwtSigningKey=fasdferear
    container_name: rpush-scheduler
    ports:
      - 8123:8123

运行 docker-compose up -d 之后,直接访问 8124 端口即可

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • 大疆创新

    深圳市大疆创新科技有限公司(DJI-Innovations,简称 DJI),成立于 2006 年,是全球领先的无人飞行器控制系统及无人机解决方案的研发和生产商,客户遍布全球 100 多个国家。通过持续的创新,大疆致力于为无人机工业、行业用户以及专业航拍应用提供性能最强、体验最佳的革命性智能飞控产品和解决方案。

    2 引用 • 14 回帖
  • Netty

    Netty 是一个基于 NIO 的客户端-服务器编程框架,使用 Netty 可以让你快速、简单地开发出一个可维护、高性能的网络应用,例如实现了某种协议的客户、服务端应用。

    49 引用 • 33 回帖 • 22 关注
  • BookxNote

    BookxNote 是一款全新的电子书学习工具,助力您的学习与思考,让您的大脑更高效的记忆。

    笔记整理交给我,一心只读圣贤书。

    1 引用 • 1 回帖
  • 大数据

    大数据(big data)是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合,是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高增长率和多样化的信息资产。

    93 引用 • 113 回帖
  • 小薇

    小薇是一个用 Java 写的 QQ 聊天机器人 Web 服务,可以用于社群互动。

    由于 Smart QQ 从 2019 年 1 月 1 日起停止服务,所以该项目也已经停止维护了!

    34 引用 • 467 回帖 • 742 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • 外包

    有空闲时间是接外包好呢还是学习好呢?

    26 引用 • 232 回帖 • 2 关注
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    286 引用 • 248 回帖 • 62 关注
  • 招聘

    哪里都缺人,哪里都不缺人。

    190 引用 • 1057 回帖
  • 安全

    安全永远都不是一个小问题。

    199 引用 • 816 回帖 • 1 关注
  • RESTful

    一种软件架构设计风格而不是标准,提供了一组设计原则和约束条件,主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

    30 引用 • 114 回帖 • 1 关注
  • 电影

    这是一个不能说的秘密。

    120 引用 • 599 回帖
  • Love2D

    Love2D 是一个开源的, 跨平台的 2D 游戏引擎。使用纯 Lua 脚本来进行游戏开发。目前支持的平台有 Windows, Mac OS X, Linux, Android 和 iOS。

    14 引用 • 53 回帖 • 531 关注
  • DevOps

    DevOps(Development 和 Operations 的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。

    47 引用 • 25 回帖 • 1 关注
  • 开源中国

    开源中国是目前中国最大的开源技术社区。传播开源的理念,推广开源项目,为 IT 开发者提供了一个发现、使用、并交流开源技术的平台。目前开源中国社区已收录超过两万款开源软件。

    7 引用 • 86 回帖
  • SpaceVim

    SpaceVim 是一个社区驱动的模块化 vim/neovim 配置集合,以模块的方式组织管理插件以
    及相关配置,为不同的语言开发量身定制了相关的开发模块,该模块提供代码自动补全,
    语法检查、格式化、调试、REPL 等特性。用户仅需载入相关语言的模块即可得到一个开箱
    即用的 Vim-IDE。

    3 引用 • 31 回帖 • 99 关注
  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 362 关注
  • HHKB

    HHKB 是富士通的 Happy Hacking 系列电容键盘。电容键盘即无接点静电电容式键盘(Capacitive Keyboard)。

    5 引用 • 74 回帖 • 471 关注
  • SOHO

    为成为自由职业者在家办公而努力吧!

    7 引用 • 55 回帖 • 18 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖 • 1 关注
  • V2EX

    V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。

    17 引用 • 236 回帖 • 328 关注
  • 以太坊

    以太坊(Ethereum)并不是一个机构,而是一款能够在区块链上实现智能合约、开源的底层系统。以太坊是一个平台和一种编程语言 Solidity,使开发人员能够建立和发布下一代去中心化应用。 以太坊可以用来编程、分散、担保和交易任何事物:投票、域名、金融交易所、众筹、公司管理、合同和知识产权等等。

    34 引用 • 367 回帖
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    107 引用 • 295 回帖
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 384 关注
  • Quicker

    Quicker 您的指尖工具箱!操作更少,收获更多!

    32 引用 • 130 回帖 • 2 关注
  • LaTeX

    LaTeX(音译“拉泰赫”)是一种基于 ΤΕΧ 的排版系统,由美国计算机学家莱斯利·兰伯特(Leslie Lamport)在 20 世纪 80 年代初期开发,利用这种格式,即使使用者没有排版和程序设计的知识也可以充分发挥由 TeX 所提供的强大功能,能在几天,甚至几小时内生成很多具有书籍质量的印刷品。对于生成复杂表格和数学公式,这一点表现得尤为突出。因此它非常适用于生成高印刷质量的科技和数学类文档。

    12 引用 • 54 回帖 • 63 关注
  • 一些有用的避坑指南。

    69 引用 • 93 回帖