寫在前面
最近跟朋友搞了個網(wǎng)游公會,主體運作是《御龍在天》這款游戲,因為公會做了微信公眾號,所以提出這個需求:用戶在公眾號發(fā)送關(guān)鍵字,然后領(lǐng)取CDK,每個用于不可重復(fù)領(lǐng)取。多方資料查閱,發(fā)現(xiàn)這方面的資料很少,所以就獨立的自己研究開發(fā)了一下,不存在任何商業(yè)利益,純粹個人的愛好和貢獻(xiàn)。
原文地址(我的博客):http://zhuxinfeng.com/2019/08/12/wei-xin-gong-zhong-hao-fa-song-guan-jian-zi-ling-qu-cdk/#more
注冊訂閱號
前往微信公眾平臺注冊,這里就不多說了。點擊下面機票,go!
開發(fā)前準(zhǔn)備
服務(wù)器:阿里云或者華為云都可以,配置不需要很高<br />
域名:已備案的域名,或者你的服務(wù)器沒有其它服務(wù),80端口給即將開發(fā)的程序用<br />
Nginx:這個東西沒什么好說的。
MySQL:數(shù)據(jù)庫,用來存儲CDK信息。
架構(gòu)設(shè)計
涉及技術(shù):Spring Boot、Spring Mvc、MyBatis、Druid、MySQL、Lombok、Dom4j、Xstream <br />
Spring Boot、Spring Mvc、MyBatis不需要多說,經(jīng)典的ssm架構(gòu);Druid是阿里爸爸的數(shù)據(jù)庫連接池;Lombok是用來簡化開發(fā)的框架,比如快速省略日志,無需生成Get/Set方法;Dom4j主要用來解析Xml消息;Xstream用來將消息轉(zhuǎn)換為Xml;
新建項目,創(chuàng)建數(shù)據(jù)表
初始化一個Spring Boot項目就不在這提了,不會的點擊下面機票,查看另外一篇文章;
創(chuàng)建數(shù)據(jù)表:
CREATE TABLE `cdk_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
`cdk` varchar(50) NOT NULL DEFAULT '' COMMENT 'CDK',
`type` varchar(50) DEFAULT '' COMMENT 'CDK類型',
`open_id` varchar(50) DEFAULT '' COMMENT '微信的openid',
`create_time` timestamp NULL DEFAULT NULL COMMENT '創(chuàng)建時間',
`modify_time` timestamp NULL DEFAULT NULL COMMENT '修改時間',
`dr` int(10) DEFAULT '1' COMMENT '刪除標(biāo)識: [1 有效; 0 刪除;]',
PRIMARY KEY (`id`,`cdk`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COMMENT='CDK信息表';
Maven依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- alibaba的druid數(shù)據(jù)庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
<!-- 解析xml -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6</version>
</dependency>
<!-- 轉(zhuǎn)xml -->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4</version>
</dependency>
創(chuàng)建微信消息實體類
@Getter
@Setter
@ToString
public class WeChatMessageBean implements Serializable {
private static final long serialVersionUID = 1L;
/** 開發(fā)者微信號 **/
private String ToUserName;
/** 發(fā)送方帳號(一個OpenID) **/
private String FromUserName;
/** 消息創(chuàng)建時間 (整型) **/
private Long CreateTime;
/** 消息類型,文本為text **/
private String MsgType;
/** 文本消息內(nèi)容 **/
private String Content;
/** 消息id,64位整型 **/
private String MsgId;
}
創(chuàng)建XML與JavaBean轉(zhuǎn)換工具類
public class XmlConverterUtil implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 微信公眾號使用:XML轉(zhuǎn)換為Map
* 使用dom4j進(jìn)行xml讀取
*
* @param request HTTP請求
* @return 返回map
* @date 2019/8/12 11:14
*/
public static Map<String, String> xmlToMap(HttpServletRequest request) throws Exception {
Map<String, String> map = new HashMap<>();
SAXReader reader = new SAXReader();
ServletInputStream inputStream = request.getInputStream();
Document document = reader.read(inputStream);
// 獲取跟節(jié)點
Element rootElement = document.getRootElement();
// 獲取根下所有節(jié)點
List<Element> elements = rootElement.elements();
for (Element element : elements) {
map.put(element.getName(), element.getText());
}
// 關(guān)閉流
inputStream.close();
return map;
}
/**
* 微信公眾號使用:對象轉(zhuǎn)換為XML
*
* @param weChatMessage 消息體
* @return 返回字符串
* @date 2019/8/12 11:18
*/
public static String objectToXml(WeChatMessageBean weChatMessage){
XStream xStream = new XStream();
xStream.alias(Constants.MSG_TYPE,weChatMessage.getClass());
return xStream.toXML(weChatMessage);
}
}
開發(fā)服務(wù)器基本配置
登錄微信公眾平臺后,基本配置 -> 服務(wù)器配置 -> 修改配置
URL:配置一個api接口,用來接收服務(wù)器推送的消息,url自定義,一會項目中用的接口
Token:Token不是校驗用的access_token,是單獨校驗服務(wù)器的token,自定義即可,與服務(wù)器配置相同
EncodingAESKey:隨機生成即可,因為是個人開發(fā)使用,所以消息加解密是明文,這個用處不大。
消息加解密方式:選擇明文模式,個人開發(fā)完,就不去解密了。

接收公眾號推送的消息
主要思路:接收公眾號的消息推送 -> 解析
xml消息體,識別消息類型 -> 處理text文本消息,其它消息直接返回success-> 獲取xml消息體中的content內(nèi)容,與設(shè)定的關(guān)鍵詞進(jìn)行對比,符合進(jìn)行下一步,不符合直接返回success-> 獲取FromUserName,其實就是openid,查詢數(shù)據(jù)庫中對應(yīng)類型的cdk是否領(lǐng)取過,領(lǐng)取過返回消息,沒領(lǐng)取過返回cdk消息 -> 封裝xml消息體,返回。
PS:需要注意,在返回
xml消息體的時候,要將接收的消息體FromUserName與ToUserName相反設(shè)置,即將接收方設(shè)置為用戶,發(fā)送方設(shè)置為開發(fā)者。
/**
* 獲取cdk
*
* @param signature 簽名
* @param timestamp 時間戳
* @param nonce 隨機數(shù)
* @param echostr 密文
* @param request 微信公眾平臺的Http請求
* @return 返回xml數(shù)據(jù)或接收成功(success)
* @date 2019/8/12 11:26
*/
@RequestMapping("/msg")
public String msg(@Param("signature") String signature, @Param("timestamp") String timestamp,
@Param("nonce") String nonce, @Param(value = "echostr") String echostr, HttpServletRequest request) {
// 校驗簽名和token
if (StringUtil.isNotEmpty(echostr)) {
logger.info("正在校驗簽名等參數(shù)");
if (WeixinUtil.checkSignature(signature, timestamp, nonce)) {
return echostr;
}
return "";
} else {
try {
// 將request中的xml信息轉(zhuǎn)換為map
Map<String, String> map = XmlConverterUtil.xmlToMap(request);
// 消息類型
String msgType = map.get(Constants.MSG_TYPE_KEY);
// 發(fā)送方賬號openid
String fromUserName = map.get(Constants.FROM_USER_NAME_KEY);
// 開發(fā)者微信號
String toUserName = map.get(Constants.TO_USER_NAME_KEY);
// 用戶發(fā)送的消息
String content = map.get(Constants.CONTENT_KEY);
// 標(biāo)記數(shù)值
String msgId = map.get(Constants.MSGID_KEY);
logger.info("消息類型為:{} ,消息內(nèi)容為:{}", msgType, content);
// 如果是文本消息則進(jìn)行如下處理
if (msgType.equalsIgnoreCase(Constants.MSG_TYPE_VALUE)) {
// 如果關(guān)鍵詞不是“公測CDK”或者“經(jīng)典CDK”,直接返回success
if (!Constants.CDK_TYPE_GC_VALUE.equalsIgnoreCase(content) && !Constants.CDK_TYPE_JD_VALUE.equalsIgnoreCase(content)) {
return Constants.DEFAULT_MSG;
}
// 通過content校驗出cdk類型,GC或者JD
String cdkType = checkCdkType(content);
// 獲取cdk
String cdk = cdkInfoService.getCdk(fromUserName, cdkType);
// 返回信息給用戶
WeChatMessageBean weChatMessage = new WeChatMessageBean();
weChatMessage.setContent(cdk);
weChatMessage.setMsgType(msgType);
weChatMessage.setToUserName(fromUserName);
weChatMessage.setFromUserName(toUserName);
weChatMessage.setCreateTime(System.currentTimeMillis());
weChatMessage.setMsgId(msgId);
// 將消息實體轉(zhuǎn)換為xml
String xml = XmlConverterUtil.objectToXml(weChatMessage);
logger.info("消息發(fā)送成功,時間:{}", getTimeStr());
return xml;
}
} catch (Exception e) {
e.printStackTrace();
}
logger.info("消息發(fā)送成功,時間:{}", getTimeStr());
return Constants.DEFAULT_MSG;
}
}
實現(xiàn)類:
@Override
public String getCdk(String openId,String cdkType){
// 參數(shù)校驗
if(StringUtil.isBlank(openId)){
return Constants.CDK_MSG_OPENID;
}
// 查詢是否領(lǐng)取過
CdkInfoBean params = new CdkInfoBean();
params.setOpenId(openId);
params.setType(cdkType);
CdkInfoBean bean = cdkInfoMapper.queryByOpenId(params);
// 如果是空 則該用戶領(lǐng)取過CDK
if(bean != null){
return Constants.CDK_MSG_RECEIVED;
}
// 獲取一個新的CDK,如果拿不到,說明沒有cdk了。
CdkInfoBean cdk = cdkInfoMapper.queryByAsc(cdkType);
if(cdk == null){
return Constants.CDK_MSG_NULL;
}
cdk.setOpenId(openId);
// 更新為使用狀態(tài)
cdkInfoMapper.update(cdk);
// 返回cdk信息
return Constants.CDK_MSG_RECEIVE + cdk.getCdk();
}
常量類:
public class Constants {
/** 消息類型 **/
public static final String MSG_TYPE = "xml";
/** 微信公眾平臺配置的Token **/
public static final String WECHAT_TOKEN = "xxxxxxxx";
/** 發(fā)送消息的(open_id) **/
public static final String FROM_USER_NAME_KEY ="FromUserName";
/** 我方公眾號 **/
public static final String TO_USER_NAME_KEY="ToUserName";
/** 創(chuàng)建消息時間 **/
public static final String CREATE_TIME_KEY="CreateTime";
/** 創(chuàng)建消息時間 **/
public static final String CONTENT_KEY="Content";
/** 當(dāng)前消息類型 **/
public static final String MSG_TYPE_KEY = "MsgType";
public static final String MSG_TYPE_VALUE = "text";
/** 當(dāng)前語音格式已知的有amr MP3 **/
public static final String FORMAT_KEY = "Format";
/** 消息ID **/
public static final String MSGID_KEY = "MsgId";
/** CDK的類型 **/
public static final String CDK_TYPE_GC_KEY = "GC";
public static final String CDK_TYPE_JD_KEY = "JD";
/** 領(lǐng)取CDK的關(guān)鍵詞 **/
public static final String CDK_TYPE_GC_VALUE = "公測CDK";
public static final String CDK_TYPE_JD_VALUE = "經(jīng)典CDK";
/** 重復(fù)領(lǐng)取 **/
public static final String CDK_MSG_RECEIVED = "您已經(jīng)領(lǐng)取過CDK啦!";
/** CDK前綴 **/
public static final String CDK_MSG_RECEIVE = "感謝您關(guān)注Yy3191明月網(wǎng)游,CKD為:";
/** CDK庫存不足 **/
public static final String CDK_MSG_NULL = "很抱歉,CDK沒有啦,請去Yy3191聯(lián)系管理吧!";
/** 領(lǐng)取出現(xiàn)異常 **/
public static final String CDK_MSG_OPENID = "領(lǐng)取出現(xiàn)異常,請去Yy3191聯(lián)系管理吧!";
/** 默認(rèn)回復(fù)消息,如果不是你想回復(fù)的消息,默認(rèn)回復(fù)success,防止公眾號5s重試 **/
public static final String DEFAULT_MSG = "success";
}
實際效果測試

源碼地址
點擊下方藍(lán)色飛機票,失效聯(lián)系我