JAVA-实现接入企业微信发送消息(最详细代码讲解!)

uniapp移动端——企业微信H5调用jssdk实现扫一扫,通过weixin-java-cp获取ticket签名,配置config 代码最终部署到服务器上,将服务器的IP配置到白名单里,这样调试就不会提示IP不在白名单范围内,此界面在系统管理员界面有。后端使用的是https://github.com/binarywang/的weixin-java-cp开发工具。(3)新增QywxService,使用weixin-java-cp中的WxCpServiceImpl生成签名。此时获取的url可以是域名重定向后的带端口的,但是此url需要在企业微信上进行可信域名登记。(2)在main.js全局引入weixin-js-sdk。 阅读详情
AI摘要

本文介绍了如何通过企业微信管理员权限配置企业号应用,包括设置 CorpId、agentId和secret,以及使用 SDK 发送文本和文件消息,还涵盖了群机器人创建和消息发送的过程。

1.通过应用发送消息

2.通过群机器人发送群消息

要清楚几个概念

使用企业微信管理员登录到控制台
企业微信后台管理地址

1.CorpId

这个是每个企业独有的企业ID
在这里插入图片描述

2.agentId 和 secret

agentId 相当于是应用ID,secret就理解成key
可以自己创建个应用,然后绑定到相关部门或者具体到人
在这里插入图片描述
在这里插入图片描述
知道这三个概念后,就可以通过SDK发送企业微信消息了

1.引入依赖

        <dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-cp</artifactId>
            <version>3.7.0</version>
        </dependency>

2.添加固定写法的代码

2.1.增加config类,自己新增个config包写到里面

在这里插入图片描述

package com.linkyoyo.weatherpredict.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

/**
 * @author GuoZiXuan
 * @date 2022/06/13 14:45
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Component
public class AppConfig {

    /**
     * 设置微信企业应用的AgentId
     */
    private Integer agentId;

    /**
     * 设置微信企业应用的Secret
     */
    private String secret;

    /**
     * 设置微信企业号的token
     */
    private String token;

    /**
     * 设置微信企业号的EncodingAESKey
     */
    private String aesKey;

}

package com.linkyoyo.weatherpredict.config;

import com.linkyoyo.weatherpredict.handler.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.val;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import me.chanjar.weixin.cp.constant.WxCpConsts;
import me.chanjar.weixin.cp.message.WxCpMessageRouter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
//@Configuration
//@EnableConfigurationProperties(WxCpProperties.class)
@Component
public class WxCpConfiguration {
    private LogHandler logHandler;
    private NullHandler nullHandler;
    private LocationHandler locationHandler;
    private MenuHandler menuHandler;
    private MsgHandler msgHandler;
    private UnsubscribeHandler unsubscribeHandler;
    private SubscribeHandler subscribeHandler;

    private WxCpProperties properties;

    private static Map<Integer, WxCpMessageRouter> routers = Maps.newHashMap();
    private static Map<Integer, WxCpService> cpServices = Maps.newHashMap();

    @Autowired
    public WxCpConfiguration(LogHandler logHandler, NullHandler nullHandler, LocationHandler locationHandler,
                             MenuHandler menuHandler, MsgHandler msgHandler, UnsubscribeHandler unsubscribeHandler,
                             SubscribeHandler subscribeHandler, WxCpProperties properties) {
        this.logHandler = logHandler;
        this.nullHandler = nullHandler;
        this.locationHandler = locationHandler;
        this.menuHandler = menuHandler;
        this.msgHandler = msgHandler;
        this.unsubscribeHandler = unsubscribeHandler;
        this.subscribeHandler = subscribeHandler;
        this.properties = properties;
    }


    public static Map<Integer, WxCpMessageRouter> getRouters() {
        return routers;
    }

    public WxCpService getCpService(Integer agentId, String secret, String corpId) {
        AppConfig appConfig = AppConfig.builder()
                .agentId(agentId)
                .secret(secret)
                .build();

        List<AppConfig> list = Lists.newArrayList();
        list.add(appConfig);
        properties.setAppConfigs(list);
        properties.setCorpId(corpId);


        cpServices = this.properties.getAppConfigs().stream().map(a -> {
            val configStorage = new WxCpDefaultConfigImpl();
            configStorage.setCorpId(this.properties.getCorpId());
            configStorage.setAgentId(a.getAgentId());
            configStorage.setCorpSecret(a.getSecret());
            configStorage.setToken(a.getToken());
            configStorage.setAesKey(a.getAesKey());
            val service = new WxCpServiceImpl();
            service.setWxCpConfigStorage(configStorage);
            routers.put(a.getAgentId(), this.newRouter(service));
            return service;
        }).collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a));
        return cpServices.get(agentId);
    }

    private WxCpMessageRouter newRouter(WxCpService wxCpService) {
        final val newRouter = new WxCpMessageRouter(wxCpService);

        // 记录所有事件的日志 (异步执行)
        newRouter.rule().handler(this.logHandler).next();

        // 自定义菜单事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxConsts.MenuButtonType.CLICK).handler(this.menuHandler).end();

        // 点击菜单链接事件(这里使用了一个空的处理器,可以根据自己需要进行扩展)
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxConsts.MenuButtonType.VIEW).handler(this.nullHandler).end();

        // 关注事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxConsts.EventType.SUBSCRIBE).handler(this.subscribeHandler)
            .end();

        // 取消关注事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxConsts.EventType.UNSUBSCRIBE)
            .handler(this.unsubscribeHandler).end();

        // 上报地理位置事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxConsts.EventType.LOCATION).handler(this.locationHandler)
            .end();

        // 接收地理位置消息
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION)
            .handler(this.locationHandler).end();

        // 扫码事件(这里使用了一个空的处理器,可以根据自己需要进行扩展)
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxConsts.EventType.SCAN).handler(this.nullHandler).end();

        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxCpConsts.EventType.CHANGE_CONTACT).handler(new ContactChangeHandler()).end();

        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxCpConsts.EventType.ENTER_AGENT).handler(new EnterAgentHandler()).end();

        // 默认
        newRouter.rule().async(false).handler(this.msgHandler).end();

        return newRouter;
    }
}

package com.linkyoyo.weatherpredict.config;

import com.linkyoyo.weatherpredict.util.JsonUtils;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Component
public class WxCpProperties {
  /**
   * 设置微信企业号的corpId
   */
  private String corpId;

  private List<AppConfig> appConfigs;

  @Override
  public String toString() {
    return JsonUtils.toJson(this);
  }
}

2.2.增加builder类,自己新增个builder包写到里面

在这里插入图片描述

package com.linkyoyo.weatherpredict.builder;

import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *  @author Binary Wang(https://github.com/binarywang)
 */
public abstract class AbstractBuilder {
  protected final Logger logger = LoggerFactory.getLogger(getClass());

  /**
   * 创建WxCpXmlOutMessage信息
   * @param content
   * @param wxMessage
   * @param service
   * @return
   */
  public abstract WxCpXmlOutMessage build(String content, WxCpXmlMessage wxMessage, WxCpService service);
}

package com.linkyoyo.weatherpredict.builder;

import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessage;

/**
 *  @author Binary Wang(https://github.com/binarywang)
 */
public class TextBuilder extends AbstractBuilder {

  @Override
  public WxCpXmlOutMessage build(String content, WxCpXmlMessage wxMessage,
                                 WxCpService service) {
    WxCpXmlOutTextMessage m = WxCpXmlOutMessage.TEXT().content(content)
        .fromUser(wxMessage.getToUserName()).toUser(wxMessage.getFromUserName())
        .build();
    return m;
  }

}

2.3 增加handle类,放在handler包下

在这里插入图片描述

package com.linkyoyo.weatherpredict.handler;

import me.chanjar.weixin.cp.message.WxCpMessageHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
public abstract class AbstractHandler implements WxCpMessageHandler {
  protected Logger logger = LoggerFactory.getLogger(getClass());
}

package com.linkyoyo.weatherpredict.handler;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
public abstract class AbstractScanHandler extends AbstractHandler {

}

package com.linkyoyo.weatherpredict.handler;


import com.linkyoyo.weatherpredict.builder.TextBuilder;
import com.linkyoyo.weatherpredict.util.JsonUtils;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 通讯录变更事件处理器.
 *
 * @author Binary Wang(https://github.com/binarywang)
 */
@Component
public class ContactChangeHandler extends AbstractHandler {

  @Override
  public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
                                  WxSessionManager sessionManager) {
    String content = "收到通讯录变更事件,内容:" + JsonUtils.toJson(wxMessage);
    this.logger.info(content);

    return new TextBuilder().build(content, wxMessage, cpService);
  }

}

package com.linkyoyo.weatherpredict.handler;

import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;

import java.util.Map;

/**
 * <pre>
 *
 * Created by Binary Wang on 2018/8/27.
 * </pre>
 *
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 */
@Slf4j
public class EnterAgentHandler extends AbstractHandler {
    private static final int TEST_AGENT = 1000002;

    @Override
    public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService, WxSessionManager sessionManager) throws WxErrorException {
        // do something
        return null;
    }
}

package com.linkyoyo.weatherpredict.handler;

import com.linkyoyo.weatherpredict.builder.TextBuilder;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
@Component
public class LocationHandler extends AbstractHandler {

  @Override
  public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
                                  WxSessionManager sessionManager) {
    if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.LOCATION)) {
      //TODO 接收处理用户发送的地理位置消息
      try {
        String content = "感谢反馈,您的的地理位置已收到!";
        return new TextBuilder().build(content, wxMessage, null);
      } catch (Exception e) {
        this.logger.error("位置消息接收处理失败", e);
        return null;
      }
    }

    //上报地理位置事件
    this.logger.info("\n上报地理位置,纬度 : {}\n经度 : {}\n精度 : {}",
        wxMessage.getLatitude(), wxMessage.getLongitude(), String.valueOf(wxMessage.getPrecision()));

    //TODO  可以将用户地理位置信息保存到本地数据库,以便以后使用

    return null;
  }

}

package com.linkyoyo.weatherpredict.handler;

import com.linkyoyo.weatherpredict.util.JsonUtils;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 *  @author Binary Wang(https://github.com/binarywang)
 */
@Component
public class LogHandler extends AbstractHandler {
  @Override
  public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
                                  WxSessionManager sessionManager) {
    this.logger.info("\n接收到请求消息,内容:{}", JsonUtils.toJson(wxMessage));
    return null;
  }

}

package com.linkyoyo.weatherpredict.handler;

import me.chanjar.weixin.common.api.WxConsts.MenuButtonType;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
@Component
public class MenuHandler extends AbstractHandler {

  @Override
  public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
                                  WxSessionManager sessionManager) {

    String msg = String.format("type:%s, event:%s, key:%s",
        wxMessage.getMsgType(), wxMessage.getEvent(),
        wxMessage.getEventKey());
    if (MenuButtonType.VIEW.equals(wxMessage.getEvent())) {
      return null;
    }

    return WxCpXmlOutMessage.TEXT().content(msg)
        .fromUser(wxMessage.getToUserName()).toUser(wxMessage.getFromUserName())
        .build();
  }

}

package com.linkyoyo.weatherpredict.handler;

import com.linkyoyo.weatherpredict.builder.TextBuilder;
import com.linkyoyo.weatherpredict.util.JsonUtils;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
@Component
public class MsgHandler extends AbstractHandler {

  @Override
  public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
                                  WxSessionManager sessionManager) {

    if (!wxMessage.getMsgType().equals(WxConsts.XmlMsgType.EVENT)) {
      //TODO 可以选择将消息保存到本地
    }

    //TODO 组装回复消息
    String content = "收到信息内容:" + JsonUtils.toJson(wxMessage);

    return new TextBuilder().build(content, wxMessage, cpService);

  }

}

package com.linkyoyo.weatherpredict.handler;

import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
@Component
public class NullHandler extends AbstractHandler {

  @Override
  public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
                                  WxSessionManager sessionManager) {
    return null;
  }

}

package com.linkyoyo.weatherpredict.handler;

import com.linkyoyo.weatherpredict.builder.TextBuilder;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpUser;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
@Component
public class SubscribeHandler extends AbstractHandler {

  @Override
  public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
                                  WxSessionManager sessionManager) throws WxErrorException {

    this.logger.info("新关注用户 OPENID: " + wxMessage.getFromUserName());

    // 获取微信用户基本信息
    WxCpUser userWxInfo = cpService.getUserService().getById(wxMessage.getFromUserName());

    if (userWxInfo != null) {
      // TODO 可以添加关注用户到本地
    }

    WxCpXmlOutMessage responseResult = null;
    try {
      responseResult = handleSpecial(wxMessage);
    } catch (Exception e) {
      this.logger.error(e.getMessage(), e);
    }

    if (responseResult != null) {
      return responseResult;
    }

    try {
      return new TextBuilder().build("感谢关注", wxMessage, cpService);
    } catch (Exception e) {
      this.logger.error(e.getMessage(), e);
    }

    return null;
  }

  /**
   * 处理特殊请求,比如如果是扫码进来的,可以做相应处理
   */
  private WxCpXmlOutMessage handleSpecial(WxCpXmlMessage wxMessage) {
    //TODO
    return null;
  }

}

package com.linkyoyo.weatherpredict.handler;

import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @author Binary Wang(https://github.com/binarywang)
 */
@Component
public class UnsubscribeHandler extends AbstractHandler {

  @Override
  public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
                                  WxSessionManager sessionManager) {
    String openId = wxMessage.getFromUserName();
    this.logger.info("取消关注用户 OPENID: " + openId);
    // TODO 可以更新本地数据库为取消关注状态
    return null;
  }

}

2.4 增加表及实体类,用于存应用的信息

这里使用的是postresql,字段一样就行,增加该表的原因是,后面发送消息需要传入应用的agentId和secret还有公司Id,所以需要维护起来

CREATE TABLE "public"."tbl_department" (
  "id" int4 NOT NULL DEFAULT nextval('tbl_department_id_seq'::regclass),
  "agent_id" int4 NOT NULL,
  "secret" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
  "name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
  "corp_id" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
  "create_time" timestamp(6),
  "update_time" timestamp(6),
  CONSTRAINT "tbl_department_pkey" PRIMARY KEY ("id")
)
;

ALTER TABLE "public"."tbl_department" 
  OWNER TO "hzcloud_master";

COMMENT ON COLUMN "public"."tbl_department"."agent_id" IS '微信-应用-agentId';

COMMENT ON COLUMN "public"."tbl_department"."secret" IS '微信-应用-secret';

COMMENT ON COLUMN "public"."tbl_department"."name" IS '微信-应用(部门)-名称';

COMMENT ON COLUMN "public"."tbl_department"."corp_id" IS '微信-企业ID';

COMMENT ON COLUMN "public"."tbl_department"."create_time" IS '创建时间';

COMMENT ON COLUMN "public"."tbl_department"."update_time" IS '修改时间';

COMMENT ON TABLE "public"."tbl_department" IS '微信-部门表';
package com.linkyoyo.weatherpredict.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;

/**
 * @author ZhangWeiXiang
 * @date 2021/11/15 14:19
 * 微信部门
 */
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
@DynamicInsert
@DynamicUpdate
@EntityListeners(AuditingEntityListener.class)
@Entity
@Table(name = "tbl_department")
public class Department {

    /**
     * 主键
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    /**
     * 每个应用都有唯一的agentid
     */
    @Column(name = "agent_id")
    private Integer agentId;

    /**
     * 独立的访问密钥
     */
    @Column(name = "secret")
    private String secret;

    /**
     * 应用名称
     */
    private String name;

    /**
     * 企业ID
     */
    @Column(name = "corp_id")
    private String corpId;

    /**
     * 创建时间
     */
    @CreatedDate
    @Column(name = "create_time")
    private LocalDateTime createTime;

    /**
     * 修改时间
     */
    @LastModifiedDate
    @Column(name = "update_time")
    private LocalDateTime updateTime;
}

2.5 增加工具类
package com.linkyoyo.weatherpredict.util;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

/**
 *  @author Binary Wang(https://github.com/binarywang)
 */
public class JsonUtils {
  private static final ObjectMapper JSON = new ObjectMapper();

  static {
    JSON.setSerializationInclusion(Include.NON_NULL);
    JSON.configure(SerializationFeature.INDENT_OUTPUT, Boolean.TRUE);
  }

  public static String toJson(Object obj) {
    try {
      return JSON.writeValueAsString(obj);
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }

    return null;
  }
}



下面就是我自己封装后的写法了,这里主要有两个,发送普通消息和发送文件消息

1.引入hutool工具类的依赖,我后面有用到

     <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.6.2</version>
        </dependency>

2.编写文件转换方法,就是将上传的文件暂存

2.1新增类FileStoreService
package com.linkyoyo.weatherpredict.service;

import org.springframework.web.multipart.MultipartFile;

import java.io.File;

public interface FileStoreService {
    File upload(MultipartFile multipartFile);

}
// A code block
var foo = 'bar';
2.2编写实现类
package com.linkyoyo.weatherpredict.service.impl;

import cn.hutool.core.io.FileUtil;
import com.linkyoyo.weatherpredict.exception.BizException;
import com.linkyoyo.weatherpredict.properties.LinkyoyoProperties;
import com.linkyoyo.weatherpredict.result.CodeMsg;
import com.linkyoyo.weatherpredict.service.FileStoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

@Service
@Slf4j
@Transactional
@RequiredArgsConstructor
public class FileStoreServiceImpl implements FileStoreService {

    @Autowired
    private LinkyoyoProperties linkyoyoProperties ;

    @Override
    public File upload(MultipartFile multipartFile) {

        String  fileName = multipartFile.getOriginalFilename();

        if(!FileUtil.exist(linkyoyoProperties.getPy().getWordPath()+ File.separator+"temporary"))
            FileUtil.mkdir(linkyoyoProperties.getPy().getWordPath()+File.separator+"temporary") ;
		//linkyoyoProperties.getPy().getWordPath()  这个方法就是从我的配置类获取个路径,这个你可以自己设置成自己的本地某个路径就好
        String path = linkyoyoProperties.getPy().getWordPath()+File.separator+"temporary"+File.separator;

        if(!FileUtil.exist(path))
            FileUtil.mkdir(path) ;
        File file = FileUtil.touch(new File(path, fileName));
        try {
            multipartFile.transferTo(file);
        } catch (IOException e) {
            log.error("文件转存失败 :FileStoreService" + file.getAbsolutePath());
            throw new BizException(CodeMsg.FILE_UPLOAD_ERROR.fillArgs("转存失败"));
        }
        return file;
    }
}

LinkyoyoProperties 这个类的属性在配置文件中


@Data
@ConfigurationProperties(prefix = "linkyoyo")
public class LinkyoyoProperties {

    /**
     * python相关配置
     */
    private PythonProperties py = new PythonProperties();

    /**
     * 企业微信相关配置
     */
    private WxProperties wx = new WxProperties();
}

@Data
public class PythonProperties {

    private String pyExplainerPath;

    private String pythonPath;

    private String wordPath;

    private String filePath;

    private String imagePath;

    private String wordTemplate;

    private String designWordScriptName;

    private String postEvaluationWordScriptName;

    private String doc2PdfScriptName;
}

@Data
public class WxProperties {

    //企业Id
    private String corpId;

    //应用Id
    private String agentId;

    //应用密钥
    private String secret;

    //企业微信群机器人 url
    private String url;

    //企业微信群机器人 key
    private String key;
}



在这里插入图片描述

2.2编写接收类,主要是用于调用企业微信API后,转换为实体类用
package com.linkyoyo.weatherpredict.info;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {

    private String userid;
}

3.!重点来了,编写消息发送工具类

package com.linkyoyo.weatherpredict.util;

import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.linkyoyo.weatherpredict.config.WxCpConfiguration;
import com.linkyoyo.weatherpredict.entity.Department;
import com.linkyoyo.weatherpredict.exception.BizException;
import com.linkyoyo.weatherpredict.info.UserInfo;
import com.linkyoyo.weatherpredict.result.CodeMsg;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpAgent;
import me.chanjar.weixin.cp.bean.WxCpDepart;
import me.chanjar.weixin.cp.bean.WxCpMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
@Slf4j
public class WxSendUtils {

    @Autowired
    private WxCpConfiguration wxCpConfiguration;

    public void sendTextMsg(Department department, String msg)  throws Exception{

        WxCpService cpService = wxCpConfiguration.getCpService(department.getAgentId(), department.getSecret(), department.getCorpId());

        //拿到所有应用的名称和agentId
        List<WxCpAgent> list = cpService.getAgentService().list();

        if(CollectionUtils.isEmpty(list)){
            throw new BizException(CodeMsg.API_CALL_ERROR.fillArgs("该应用不存在!"));
        }

        //获取到该应用下绑定的所有部门
        List<WxCpDepart> wxCpDepartList = cpService.getDepartmentService().list(null);

        //对该应用绑定的部门进行消息的发送
        wxCpDepartList.forEach(wxCpDepart -> {
            WxCpMessage wxCpMessage = WxCpMessage.TEXT()
                    .agentId(department.getAgentId()) // 企业号应用ID
                    .toParty(wxCpDepart.getId()+"")
                    .content(msg)
                    .build();

            try {
                cpService.messageSend(wxCpMessage);
            } catch (WxErrorException e) {
                log.info("微信告警发送报错: {}", e.getError());
            }
        });

        //对该应用绑定的个人进行消息的发送
        List<String> userInfos = getUseInfos(department.getCorpId(), department.getSecret(), department.getAgentId());

        userInfos.forEach(userInfo -> {
            WxCpMessage wxCpMessage = WxCpMessage.TEXT()
                    .agentId(department.getAgentId()) // 企业号应用ID
                    .toUser(userInfo)
                    .content(msg)
                    .build();

            try {
                cpService.messageSend(wxCpMessage);
            } catch (WxErrorException e) {
                log.info("微信告警发送报错: {}", e.getError());
            }
        });
    }


    public void sendFileMsg(Department department, File file) throws WxErrorException {

        //调用资源上传接口并返回资源Id
        String mediaId = getMediaId(department.getCorpId(), department.getSecret(), "file", file);

        WxCpService cpService = wxCpConfiguration.getCpService(department.getAgentId(), department.getSecret(), department.getCorpId());

        //拿到所有应用的名称和agentId
        List<WxCpAgent> list = cpService.getAgentService().list();

        if(CollectionUtils.isEmpty(list)){
            throw new BizException(CodeMsg.API_CALL_ERROR.fillArgs("该应用不存在!"));
        }

        //获取到该应用下绑定的所有部门
        List<WxCpDepart> wxCpDepartList = cpService.getDepartmentService().list(null);

        //对该应用绑定的部门进行消息的发送
        wxCpDepartList.forEach(wxCpDepart -> {
            WxCpMessage wxCpMessage = WxCpMessage.FILE()
                    .agentId(department.getAgentId()) // 企业号应用ID
                    .toParty(wxCpDepart.getId()+"")
                    .mediaId(mediaId)
                    .build();

            try {
                cpService.messageSend(wxCpMessage);
            } catch (WxErrorException e) {
                log.info("微信告警发送报错: {}", e.getError());
            }
        });

        //对该应用绑定的个人进行消息的发送
        List<String> userInfos = getUseInfos(department.getCorpId(), department.getSecret(), department.getAgentId());

        userInfos.forEach(userInfo -> {
            WxCpMessage wxCpMessage = WxCpMessage.FILE()
                    .agentId(department.getAgentId()) // 企业号应用ID
                    .toUser(userInfo)
                    .mediaId(mediaId)
                    .build();

            try {
                cpService.messageSend(wxCpMessage);
            } catch (WxErrorException e) {
                log.info("微信告警发送报错: {}", e.getError());
            }
        });

    }


    /**
     *
     * @param corpid 企业ID
     * @param corpsecret  Secret
     * @param type 媒体文件类型:图片(image)、语音(voice)、视频(video),普通文件(file)
     * @param file
     * @throws Exception
     */
    public String getMediaId(String corpid, String corpsecret, String type,File file){
        String token = null;
        String mediaId = null;
        //获取token
        try {
            String toKenBody = HttpUtil.createGet("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid="+corpid+"&corpsecret="+corpsecret)
                    .execute().body();
            JSONObject toKenObject = JSONObject.parseObject(toKenBody);
            token = toKenObject.get("access_token").toString();
        }catch (Exception e){
            throw new BizException(CodeMsg.API_CALL_ERROR.fillArgs("企业微信 获取token接口 调用失败"));
        }

        try {
            String body = HttpUtil.createPost("https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token="+token+"&type="+type)
                    .form("media",file)
                    .execute().body();
            JSONObject jsonObject = JSONObject.parseObject(body);
            mediaId = jsonObject.get("media_id").toString();
        }catch (Exception e){
            throw new BizException(CodeMsg.API_CALL_ERROR.fillArgs("企业微信 上传临时素材接口 调用失败"));
        }
        return mediaId;
    }


    public List<String> getUseInfos(String corpid, String corpsecret,Integer agentid){
        try {

            String body = HttpUtil.createGet("https://qyapi.weixin.qq.com/cgi-bin/agent/get?access_token="+getToKen(corpid,corpsecret)+"&agentid="+agentid)
                    .execute().body();

            JSONObject toKenObject = JSONObject.parseObject(body);

            Map<String,Object> userinfo = (Map<String, Object>) toKenObject.get("allow_userinfos");

            if(CollectionUtils.isEmpty(userinfo))
                return null;
            JSONArray user = (JSONArray)userinfo.get("user");
            List<UserInfo> userInfos = user.toJavaList(UserInfo.class);
            if(CollectionUtils.isEmpty(user))
                return null;

            return userInfos.stream().map(e->e.getUserid()).collect(Collectors.toList());

        }catch (Exception e){
            throw new BizException(CodeMsg.API_CALL_ERROR.fillArgs("企业微信 上传临时素材接口 调用失败"));
        }
    }


    public String getToKen(String corpid, String corpsecret){
        try {
            String toKenBody = HttpUtil.createGet("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid="+corpid+"&corpsecret="+corpsecret)
                    .execute().body();
            JSONObject toKenObject = JSONObject.parseObject(toKenBody);
            return toKenObject.get("access_token").toString();
        }catch (Exception e){
            throw new BizException(CodeMsg.API_CALL_ERROR.fillArgs("企业微信 获取token接口 调用失败"));
        }
    }
}

代码讲解:普通消息的发送只需要传入四个重要信息,1.公司Id 2.agentId 3.secret 4.消息,而文件消息就有点不一样了,发送的文件必须先调用上传临时文件接口,然后会返回一个mediaId,然后发送文件消息的时候就传这个Id,这样就实现了文件消息的发送。
还有个点就是,应用既可以绑部门,也可以绑个人,所以可以通过方法先获取到该应用下绑定的部门或个人,然后再去往这个部门下的所有人去发消息,或去发个人。

4.编写测试类
package com.linkyoyo.weatherpredict.controller;

import com.linkyoyo.weatherpredict.entity.Department;
import com.linkyoyo.weatherpredict.result.R;
import com.linkyoyo.weatherpredict.service.FileStoreService;
import com.linkyoyo.weatherpredict.util.WxSendUtils;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
@RequestMapping("/sendWx")
public class SendWxController {

    private final WxSendUtils wxSendUtils;

    private final FileStoreService fileStoreService;

    //发送文件消息
    @PostMapping(value = "/FileMsg", headers = "Content-Type=multipart/form-data")
    public R FileMsg(@RequestPart("file") MultipartFile file) throws WxErrorException {
        Department department = new Department();
        department.setName("消息推送");
        department.setCorpId("***");
        department.setAgentId(***);
        department.setSecret("***");
        wxSendUtils.sendFileMsg(department, fileStoreService.upload(file));
        return R.ok();
    }

    //发送普通文本消息
    @PostMapping(value = "/TextMsg")
    public R TextMsg(String msg) throws Exception {
        Department department = new Department();
        department.setName("消息推送");
         department.setCorpId("***");
        department.setAgentId(***);
        department.setSecret("***");
        wxSendUtils.sendTextMsg(department, msg);
        return R.ok();
    }

    @PostMapping(value = "/getUse")
    public R getUse() throws Exception {
        Department department = new Department();
        department.setName("消息推送");
        department.setCorpId("***");
        department.setAgentId(***);
        department.setSecret("***");
        wxSendUtils.getUseInfos(department.getCorpId(),department.getSecret(),department.getAgentId());
        return R.ok();
    }
}

发送群消息

1.创建群机器人

在这里插入图片描述
在这里插入图片描述
主要就是通过这个地址去发送消息的

相关API地址:https://developer.work.weixin.qq.com/document/path/91770

企微API主动发送外部群消息 本文旨在为开发者提供一份关于如何使用Java、Go、Python等主流编程语言,通过企业微信API实现向外部群(客户群)主动发送消息的完整技术指南。文章将从核心原理出发,详细解析API调用链的每个环节,提供多语言的具体实现代码,并分享在生产环境中应用的最佳实践。无论您是希望实现项目群的通知自动化,还是构建客户服务群的智能消息推送,本文都将为您提供清晰的技术路径和实践方案。 阅读详情

相关推荐

Java对接企业微信发送应用消息

Java对接企业微信发送应用消息和回调配置

kaoninian的博客 3056

手把手教你springboot企业微信开发(二)

手把手教你springboot企业微信开发(二)1、企业微信开发第一步2、weixin-java-cp-demo-master1)、引入thymeleaf改造项目2)增加Configuration3)回到企业号4)穿透内外网5)完善 从这一篇开始,开始从实际项目探索企业微信开发。 1、企业微信开发第一步 看一下企业微信的开发者文档: 企业微信开发者文档. 在开发文档的首页中,我们看到:企业&定制化服务商开发流程 1. 获取企业号CorpID&Secret: 企业管理员建立管理组,获取Corp

miaozhicheng1990的博客 1万+

JAVA实现接入企业微信报警

引言 原来系统中报警采用短信和邮件方式,短信采用阿里的短信,近期由于 一些原因,项目中想将报警信息发送到 企业微信中,由于之前小编玩zabbix的时候实现过这个需求,所以认为通过java应该比较简单, 下面分享接入代码,直接上干货。 一、创建应用 1)、登录企业微信后台管理页面,选择应用管理 2)、在最下面 创建应用 这里面可以控制那些人收到报警信息,其中质量的Secret 就是...

Jack 架构师之路 14万+

微信开发笔记

微信初期开发准备以及部分接口开发配置微信配置实体类微信类初始化初始化中各个事件的具体代码门户入口代码微信菜单功能 记录一下 后续会更新 支付和退款 二维码 红包 名片分享 等功能代码 引入一下微信SDK 开发配置 微信配置实体类 litemall.wx 对应yml文件 微信配置 @Data @Configuration @ConfigurationProperties(prefix = "litemall.wx") public class WxProperties { private

weixin_42563880的博客 1322

企业微信发送应用消息

本章实现企业微信向企业用户主动发送应用文本消息。

沉默星河的博客 3470

java通过API给企业微信用户发送微信消息

成果: 描述: 最近工作需要根据需求把跟进记录推送至企业微信用户,提醒对方查看。 参考自企业微信API: https://work.weixin.qq.com/api/doc#90000/90135/90236 准备工作: 写在前面:仔细阅读API文档:在企业微信开发最开始的地方有这样一段话:https://work.weixin.qq.com/api/doc#90000/90135/91039...

一包大豫竹的博客 7558

JAVA实现企业微信的消息推送

JAVA实现企业微信的消息推送(自建小应用) 【需求】将外部事件消息发送至企业微信,所在成员可以根据消息进行事件处理。 【实现流程】管理员在企业微信中自建小程序,将小程序的ID和微信的access_token作为接入连接的凭证,实现外部消息内容与企业微信自建应用聊天框的对接,最终将外部事件消息通过应用发送给企业微信所在成员。 【主要内容】该文档为发送消息的小示范(demo),运行后能将后台信息发送至小程序。

Akazia的博客 1万+

weixin-java-cp-demo:基于Spring Boot和WxJava实现的微信企业号企业微信放置演示

本项目为,基于Spring Boot ,实现企业微信替代开发功能。更多信息请查阅: : 使用步骤:请注意,本演示为简化代码编译时加入了lombok支持,如果不了解lombok的话,请先学习下相关知识,可以比如阅读;另外,新手遇到问题,请首先阅读主页的常见问题部分,可以少走很多弯路,节省大量时间。

王大师企业官方博客 1641

java代码实现简单的向企业微信发送信息

前言

xc9711的博客 9265

weixin-java-cp-2.8.0-API文档-中文版.zip

赠送jar包:weixin-java-cp-2.8.0.jar; 赠送原API文档:weixin-java-cp-2.8.0-javadoc.jar; 赠送源代码:weixin-java-cp-2.8.0-sources.jar; 赠送Maven依赖信息文件:weixin-java-cp-2.8.0.pom; 包含翻译后的API文档:weixin-java-cp-2.8.0-javadoc-API文档-中文(简体)版.zip; Maven坐标:com.github.binarywang:weixin-java-cp:2.8.0; 标签:github、binarywang、weixin、java、cp、中文文档、jar包; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译,请放心使用。

weixin-java-cp-3.7.0.jar

java运行依赖jar包

JAVA-实现接入企业微信发送消息(最详细代码讲解)

使用企业微信管理员登录到控制台。

m0_67394360的博客 3799

实测|龙虾机器人(OpenClaw)Windows系统部署全攻略(含避坑指南)

先简单科普下:龙虾机器人本质是一款开源AI代理框架,核心优势是“能行动、可本地、高灵活”——它不内置大模型,需要对接第三方AI接口(如GPT、Claude、阿里云百炼等),但能将AI的指令转化为实际的系统操作,相当于给AI配了一个“能动手的身体”,这也是它和普通对话大模型的核心区别。今天就结合自己的实测经历,从环境准备、分步部署、初始化配置,到常见问题排查,写一篇保姆级攻略,不管是新手还是有一定技术基础的同学,都能跟着一步步完成部署,少走弯路~解决方案:检查聊天软件插件是否安装成功,权限是否配置完整;

wohaiweifaju的博客 1万+

JAVA使用wecom-sdk企业微信审批功能接入教程

本文分享了使用wecom-sdk对接企业微信审批功能的实践经验。主要包括:1) 引入1.3.4版本wecom-sdk及解决Okhttp依赖冲突;2) 配置企业微信回调接口,设置Token等参数;3) 编写审批提交代码,处理审批流程节点设置;4) 实现审批回调事件处理,针对不同审批状态编写业务逻辑;5) 遇到WecomCallbackSettings需要序列化的问题。通过以上步骤成功实现了企业微信审批功能的对接,为类似项目提供了参考方案。

qq_23320401的博客 1538

VS2022+Qt6保姆级环境配置指南:从插件安装到项目互通全流程

本文提供VS2022与Qt6深度整合的全流程指南,涵盖环境配置、插件安装、项目互通等关键步骤。详细解析编译器版本匹配、跨IDE协作方案及效能优化技巧,帮助开发者高效搭建C++界面设计开发环境,实现软件项目无缝互通。

zero1的博客 730

ADS交叉耦合振荡器VCO相位噪声优化仿真实践

本文详细介绍了在ADS软件中搭建和优化交叉耦合VCO相位噪声的完整仿真实践。通过谐波平衡与瞬态仿真分析电路性能,并重点阐述了通过提升谐振腔Q值、优化晶体管工作点及改善尾电流源来有效降低相位噪声的具体方法,为射频电路设计提供了实用指南。

weixin_29235923的博客 630

Java代码实现向企业微信发送消息

这个方法需要你服务器的ip是公网ip,然后需要在企业微信添加信任ipt=N7T8企业微信https://work.weixin.qq.com/wework_admin/register_wx?

LB_bei的博客 5841

USB2.0信号完整性测试之眼图

名称线缆颜色引脚描述红色绿色白色GND黑色。

qq_53766840的博客 3282

Java代码实现向企业微信用户发送消息

公司内部交流使用的企业微信,最近项目中要实现向员工发送企业微信通知,于是看了下企业微信的api,简单实现了下: 1. 其实就是一个HTTP请求,如下 请求方式:POST(HTTPS)请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN 文本消息请求参数实例如下 { "t...

jay_boolean的博客 1万+

AI 赋能测试全生命周期:多 Agent 协同的自动化提效方案

本文提出了一种基于Coze平台的多Agent协同测试解决方案,旨在解决软件测试中PRD解析不清晰、测试点覆盖不全、用例编写效率低、代码生成耗时等痛点。通过PRD优化、测试点生成、测试用例生成和单元测试框架生成四个Agent的协同工作,结合Cursor工具实现从需求到代码的全链路自动化。以电商平台登录模块为例,展示了从模糊PRD到标准化测试用例和可执行Java单元测试代码的完整转化流程。该方案通过AI赋能显著提升测试效率,减少人工干预,实现测试覆盖率≥95%,为快速迭代的软件研发提供了高效的测试自动化支持。

QINJIEEE的博客 1095

ROS机器人未知环境自主探索功能包explore_lite总结

本系列文章主要针对ROS机器人常使用的未知环境自主探索功能包explore_lite展开全源码的详细解析,并进行概括总结。 本系列文章共包含六篇文章,前五篇文章主要介绍explore_lite功能包中 explore.cpp、costmap_tools.h、frontier_search.cpp、costmap_client.cpp等源码实现文件中全部函数的详细介绍,第六篇文章进行概括总结,包含自主探索功能包explore_lite的简介,实现未知环境自主探索功能的原理及流程总结,效果演示,使用功能包exp

慕羽★的博客 4460

java调用企业微信接口API发送消息(替代短信验证码的一种解决方案)

使用企业微信发送验证码文章采纳源地址:https://www.cnblogs.com/kuzaman/p/6564632.html使用微信发送验证码配置页面:以下步骤获取页面上的参数1、注册企业微信:注册网址:https://work.weixin.qq.com/...

zxl782340680的博客 3万+

流体动力学仿真软件:XFlow_(16).案例研究:热传输与相变

包括密度、比热容、热传导系数等。

kkchenjj的博客 722
上一篇: hibernate之@FilterDef @Filter注解的使用
下一篇: java下载文件接口
Sonk.
博客等级 码龄8年 84粉丝 83原创
评论 13
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonk.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值