項目github地址:github.com/pc859107393…html
實時項目同步的地址是國內的碼雲:git.oschina.net/859107393/m…前端
個人簡書首頁是:www.jianshu.com/users/86b79…java
上一期是:[手把手教程][第二季]java 後端博客系統文章系統——No10linux
完成微信公衆號相關接入git
既然咱們要開發微信相關的功能,那麼咱們須要微信相關的資源。首先是打開微信官方的開發者文檔。接着咱們應該構建微信相關的代碼了。?程序員
事實上並非這樣,咱們在開源中國的java項目中能夠找到一些跟微信相關的工具,本文中我採用了 fastweixin 來快速進行開發。github
compile 'com.github.sd4324530:fastweixin:1.3.15'複製代碼
實現微信互訪的Controllerweb
爲何說要實現這個?ajax
因此,咱們有一大堆事情要作,可是此時此刻咱們採用的fastweixin已經作好一大步,咱們按照他的說明編寫微信Controller。spring
@RestController
@RequestMapping("/weixin")
public class WeixinController extends WeixinControllerSupport {
private static final Logger log = LoggerFactory.getLogger(WeixinController.class);
private static final String TOKEN = "weixin"; //默認Token爲weixin
@Autowired
private WeichatServiceImpl weichatService;
@Autowired
private PostService postService;
@Override
public void bindServer(HttpServletRequest request, HttpServletResponse response) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
LogPrintUtil.getInstance(WeixinController.class).logOutLittle("bindWeiXin:\fsignature = "
+ signature + "\ntimestamp"
+ timestamp + "\nnonce" + nonce);
super.bindServer(request, response);
}
//設置TOKEN,用於綁定微信服務器
@Override
protected String getToken() {
return weichatService.getWeiConfig().getToken();
}
//使用安全模式時設置:APPID
//再也不強制重寫,有加密須要時自行重寫該方法
@Override
protected String getAppId() {
return weichatService.getWeiConfig().getAppid();
}
//使用安全模式時設置:密鑰
//再也不強制重寫,有加密須要時自行重寫該方法
@Override
protected String getAESKey() {
return null;
}
//重寫父類方法,處理對應的微信消息
@Override
protected BaseMsg handleTextMsg(TextReqMsg msg) {
String content = msg.getContent();
LogPrintUtil.getInstance(WeixinController.class).logOutLittle(String.format("用戶發送到服務器的內容:{%s}", content));
List<Article> articles = new ArrayList<>();
List<PostCustom> byKeyword = null;
try {
byKeyword = postService.findByKeyword(content, null, null);
if (null != byKeyword && byKeyword.size() > 0) {
int count = 0;
for (PostCustom postCustom : byKeyword) {
if (count >= 5) break;
Article article = new Article();
article.setTitle(postCustom.getPostTitle());
article.setDescription(HtmlUtil.getTextFromHtml(postCustom.getPostContent()));
article.setUrl("http://acheng1314.cn/front/post/" + postCustom.getId());
articles.add(article);
count++;
}
return new NewsMsg(articles);
}
} catch (NotFoundException e) {
e.printStackTrace();
}
return new TextMsg("暫未找到該信息!");
}
/*1.1版本新增,重寫父類方法,加入自定義微信消息處理器 *不是必須的,上面的方法是統一處理全部的文本消息,若是業務覺複雜,上面的會顯得比較亂 *這個機制就是爲了應對這種狀況,每一個MessageHandle就是一個業務,只處理指定的那部分消息 */
@Override
protected List<MessageHandle> initMessageHandles() {
List<MessageHandle> handles = new ArrayList<MessageHandle>();
// handles.add(new MyMessageHandle());
return handles;
}
//1.1版本新增,重寫父類方法,加入自定義微信事件處理器,同上
@Override
protected List<EventHandle> initEventHandles() {
List<EventHandle> handles = new ArrayList<EventHandle>();
// handles.add(new MyEventHandle());
return handles;
}
/** * 處理圖片消息,有須要時子類重寫 * * @param msg 請求消息對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleImageMsg(ImageReqMsg msg) {
return super.handleImageMsg(msg);
}
/** * 處理語音消息,有須要時子類重寫 * * @param msg 請求消息對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleVoiceMsg(VoiceReqMsg msg) {
return super.handleVoiceMsg(msg);
}
/** * 處理視頻消息,有須要時子類重寫 * * @param msg 請求消息對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleVideoMsg(VideoReqMsg msg) {
return super.handleVideoMsg(msg);
}
/** * 處理小視頻消息,有須要時子類重寫 * * @param msg 請求消息對象 * @return 響應消息對象 */
@Override
protected BaseMsg hadnleShortVideoMsg(VideoReqMsg msg) {
return super.hadnleShortVideoMsg(msg);
}
/** * 處理地理位置消息,有須要時子類重寫 * * @param msg 請求消息對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleLocationMsg(LocationReqMsg msg) {
return super.handleLocationMsg(msg);
}
/** * 處理連接消息,有須要時子類重寫 * * @param msg 請求消息對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleLinkMsg(LinkReqMsg msg) {
return super.handleLinkMsg(msg);
}
/** * 處理掃描二維碼事件,有須要時子類重寫 * * @param event 掃描二維碼事件對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleQrCodeEvent(QrCodeEvent event) {
return super.handleQrCodeEvent(event);
}
/** * 處理地理位置事件,有須要時子類重寫 * * @param event 地理位置事件對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleLocationEvent(LocationEvent event) {
return super.handleLocationEvent(event);
}
/** * 處理菜單點擊事件,有須要時子類重寫 * * @param event 菜單點擊事件對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleMenuClickEvent(MenuEvent event) {
LogPrintUtil.getInstance(this.getClass()).logOutLittle("點擊" + event.toString());
MyWeChatMenu myWeChatMenu = weichatService.findOneById(StringUtils.toInt(event.getEventKey()));
try {
List<Article> articles = new ArrayList<>();
List<PostCustom> keyword = postService.findByKeyword(myWeChatMenu.getKeyword(), null, null);
if (null != keyword && keyword.size() > 0) {
int i = 0;
for (PostCustom postCustom : keyword) {
if (i >= 5) break;
Article article = new Article();
article.setTitle(postCustom.getPostTitle());
article.setDescription(HtmlUtil.getTextFromHtml(postCustom.getPostContent()));
article.setUrl("http://acheng1314.cn/front/post/" + postCustom.getId());
articles.add(article);
i++;
}
return new NewsMsg(articles);
}
} catch (NotFoundException e) {
e.printStackTrace();
}
return new TextMsg("暫未找到該信息!");
}
/** * 處理菜單跳轉事件,有須要時子類重寫 * * @param event 菜單跳轉事件對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleMenuViewEvent(MenuEvent event) {
LogPrintUtil.getInstance(this.getClass()).logOutLittle("點擊跳轉" + event.toString());
return super.handleMenuViewEvent(event);
}
/** * 處理菜單掃描推事件,有須要時子類重寫 * * @param event 菜單掃描推事件對象 * @return 響應的消息對象 */
@Override
protected BaseMsg handleScanCodeEvent(ScanCodeEvent event) {
return super.handleScanCodeEvent(event);
}
/** * 處理菜單彈出相冊事件,有須要時子類重寫 * * @param event 菜單彈出相冊事件 * @return 響應的消息對象 */
@Override
protected BaseMsg handlePSendPicsInfoEvent(SendPicsInfoEvent event) {
return super.handlePSendPicsInfoEvent(event);
}
/** * 處理模版消息發送事件,有須要時子類重寫 * * @param event 菜單彈出相冊事件 * @return 響應的消息對象 */
@Override
protected BaseMsg handleTemplateMsgEvent(TemplateMsgEvent event) {
return super.handleTemplateMsgEvent(event);
}
/** * 處理添加關注事件,有須要時子類重寫 * * @param event 添加關注事件對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleSubscribe(BaseEvent event) {
return super.handleSubscribe(event);
}
/** * 接收羣發消息的回調方法 * * @param event 羣發回調方法 * @return 響應消息對象 */
@Override
protected BaseMsg callBackAllMessage(SendMessageEvent event) {
return super.callBackAllMessage(event);
}
/** * 處理取消關注事件,有須要時子類重寫 * * @param event 取消關注事件對象 * @return 響應消息對象 */
@Override
protected BaseMsg handleUnsubscribe(BaseEvent event) {
return super.handleUnsubscribe(event);
}
}複製代碼
咱們看上面的衆多方法都已經打上了javadoc,如今咱們須要關注的主要是下面的這三個方法:
//設置TOKEN,用於綁定微信服務器
@Override
protected String getToken() {
return weichatService.getWeiConfig().getToken();
}
//使用安全模式時設置:APPID
//再也不強制重寫,有加密須要時自行重寫該方法
@Override
protected String getAppId() {
return weichatService.getWeiConfig().getAppid();
}
//使用安全模式時設置:密鑰
//再也不強制重寫,有加密須要時自行重寫該方法
@Override
protected String getAESKey() {
return null;
}複製代碼
同時在微信的開發者設置頁面也有對應的設置來控制,測試帳號以下:
按照上圖中,咱們能夠直接獲取appId、APPSecret。固然Token須要本身設置,可是url這個是咱們可以接受微信服務器發送消息的地址。也就是說剛開始要測試可否綁定服務器,咱們能夠直接把appId和Token寫死到上面的方法中。這兩個設置完成後,咱們就能綁定成功微信公衆號到咱們的服務器了。
按照上面的Controller來說,URL已經能夠設置了,就是咱們服務器域名+/weixin。
固然,這不是重點!可是按照前面咱們的開發習慣來說,微信相關的一些設置可以持久化到服務器那就是最好的了。因此咱們仍是寫到數據庫中。(剛開始其實我是寫到properties中,可是因爲properties的特性,因此數據不刷新。乾脆我也就存儲到數據庫中。)
/*建立數據庫表cc_site_option,用來存儲站點基礎信息*/
SET NAMES utf8;
-- ----------------------------
-- Table structure for `cc_site_option`
-- ----------------------------
DROP TABLE IF EXISTS `cc_site_option`;
CREATE TABLE `cc_site_option` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
`option_key` varchar(128) DEFAULT NULL COMMENT '配置KEY',
`option_value` text COMMENT '配置內容',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='配置信息表,用來保存網站的全部配置信息。';複製代碼
其實在上面的表中你們細心點能夠看到我是採用了相似Map的存儲結構,也就是說咱們的數據通俗來說也就是鍵值對的形式,因此讀取數據的時候存儲用的List
@Repository("siteConfigDao")
public interface SiteConfigDao extends Dao {
@Deprecated
@Override
public int add(Serializable serializable);
@Deprecated
@Override
public int del(Serializable serializable);
@Deprecated
@Override
public int update(Serializable serializable);
@Deprecated
@Override
public Serializable findOneById(Serializable Id);
@Override
List<HashMap<String, String>> findAll();
Serializable findOneByKey(@Param("mKey") Serializable key);
void updateOneByKey(@Param("mKey") Serializable key, @Param("mValue") Serializable value);
// @Insert("INSERT INTO `cc_site_option` (`option_key`,`option_value`) VALUES (#{mKey},#{mValue});")
void insertOne(@Param("mKey") Serializable key, @Param("mValue") Serializable value);
}複製代碼
惟一細節一點的就是對應的Service中獲取想要的某一些數據。同時,咱們的微信菜單也是須要存儲的,以下:
CREATE TABLE `cc_wechat_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` text NOT NULL COMMENT '微信菜單的名字',
`parent_id` int(11) DEFAULT '0' COMMENT '父級菜單的id,最外層菜單的parent_id爲0',
`type` varchar(255) DEFAULT NULL COMMENT '微信菜單類型,deleted表示刪除,其餘的都是微信上面的相同類型,click=點擊推事件,view=跳轉URL,scancode_push=掃碼推事件,scancode_waitmsg=掃碼推事件且彈出「消息接收中」提示框,pic_sysphoto=彈出系統拍照發圖,pic_photo_or_album=彈出拍照或者相冊發圖,pic_weixin=彈出微信相冊發圖器,location_select=彈出地理位置選擇器,',
`keyword` text COMMENT '填寫的關鍵字將會觸發「自動回覆」匹配的內容,訪問網頁請填寫URL地址。',
`position` int(11) DEFAULT '0' COMMENT '排序的數字決定了菜單在什麼位置。',
PRIMARY KEY (`id`),
UNIQUE KEY `cc_wechat_menu_id_uindex` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='微信菜單表';複製代碼
固然到這裏後,咱們須要的是微信的Dao(此次在Dao中採用了註解插入sql的方式,這種方式能夠懶得建立mapper文件。)。
@Repository("weChatDao")
public interface WeChatDao extends Dao<MyWeChatMenu> {
@Override
int add(MyWeChatMenu weChatMenu);
@Update("UPDATE `cc_wechat_menu` SET type='deleted' WHERE id=#{id}")
@Override
int del(MyWeChatMenu weChatMenu);
@Update("UPDATE `cc_wechat_menu` SET name=#{name},parent_id=#{parentId},type=#{type},keyword=#{keyword},position=#{position} WHERE id=#{id}")
@Override
int update(MyWeChatMenu weChatMenu);
@Select("SELECT * FROM `cc_wechat_menu` WHERE id=#{id}")
@Override
MyWeChatMenu findOneById(Serializable Id);
@Select("SELECT * FROM `cc_wechat_menu` WHERE type!='deleted'")
@Override
List<MyWeChatMenu> findAll();
@Select("SELECT * FROM `cc_wechat_menu` WHERE type!='deleted' AND parent_id=0")
List<MyWeChatMenu> getParentWeiMenu();
}複製代碼
簡單來講上面的註解插入sql語句這樣執行,注意一點就是這幾個sql的使用。剩下的就是微信的Service,以下:
@Service("weichatService")
public class WeichatServiceImpl {
@Autowired
private SiteConfigDao siteConfigDao;
@Autowired
private WeChatDao weChatDao;
public static String updateMenuUrl = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=";
/** * 同步微信菜單到微信公衆號上面 * * @return */
public String synWeichatMenu() {
try {
WeiChatMenuBean menuBean = creatWeMenuList();
if (null == menuBean) return GsonUtils.toJsonObjStr(null, ResponseCode.FAILED, "菜單內容不能爲空!");
String menuJson = GsonUtils.toJson(menuBean);
LogPrintUtil.getInstance(this.getClass()).logOutLittle(menuJson);
WeiChatResPM pm = null; //微信響應的應答
String responseStr = HttpClientUtil.doJsonPost(String.format("%s%s", updateMenuUrl, getAccessToken()), menuJson);
LogPrintUtil.getInstance(this.getClass()).logOutLittle(responseStr);
pm = GsonUtils.fromJson(responseStr, WeiChatResPM.class);
if (pm.getErrcode() == 0) return GsonUtils.toJsonObjStr(null, ResponseCode.OK, "同步微信菜單成功!");
else throw new Exception(pm.getErrmsg());
} catch (Exception e) {
e.printStackTrace();
return GsonUtils.toJsonObjStr(null, ResponseCode.FAILED, "同步失敗!緣由:" + e.getMessage());
}
}
/** *獲取AccessToken */
public String getAccessToken() throws Exception {
MyWeiConfig weiConfig = getWeiConfig();
return WeiChatUtils.getSingleton(weiConfig.getAppid(), weiConfig.getAppsecret()).getWeAccessToken();
}
/** * 本地組裝微信菜單數據,生成菜單對象<br/> * 微信外層菜單個數必須小於等於3,對應的內部菜單不能超過5個 * @return */
private WeiChatMenuBean creatWeMenuList() throws Exception {
···具體代碼省略···
}
/** * 獲取微信設置,包裝了微信的appid,secret和token * * @return */
public MyWeiConfig getWeiConfig() {
String weiChatAppid = "", weichatAppsecret = "", token = "";
MyWeiConfig apiConfig;
try {
List<HashMap<String, String>> siteInfo = getAllSiteInfo();
LogPrintUtil.getInstance(this.getClass()).logOutLittle(siteInfo.toString());
for (HashMap<String, String> map : siteInfo) {
Set<Map.Entry<String, String>> sets = map.entrySet(); //獲取HashMap鍵值對
for (Map.Entry<String, String> set : sets) { //遍歷HashMap鍵值對
String mKey = set.getValue();
if (mKey.contains(MySiteMap.WECHAT_APPID)) {
weiChatAppid = map.get("option_value");
} else if (mKey.contains(MySiteMap.WECHAT_APPSECRET))
weichatAppsecret = map.get("option_value");
else if (mKey.contains(MySiteMap.WECHAT_TOKEN))
token = map.get("option_value");
}
}
apiConfig = new MyWeiConfig(weiChatAppid, weichatAppsecret, token);
return apiConfig;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String saveOrUpdateMenu(MyWeChatMenu weChatMenu) {
if (null == weChatMenu || StringUtils.isEmpty(weChatMenu.getName()
, weChatMenu.getType()
, weChatMenu.getParentId() + ""))
return GsonUtils.toJsonObjStr(null, ResponseCode.FAILED, "微信菜單信息不能爲空!");
try {
if (weChatMenu.getId() == null || weChatMenu.getId() < 1) {
weChatDao.add(weChatMenu);
return GsonUtils.toJsonObjStr(weChatMenu, ResponseCode.OK, "保存微信菜單信息成功!");
} else if (null != weChatMenu.getId() && weChatMenu.getId() > 0) {
weChatDao.update(weChatMenu);
return GsonUtils.toJsonObjStr(weChatMenu, ResponseCode.OK, "更新微信菜單信息成功!");
}
} catch (Exception e) {
e.printStackTrace();
}
return GsonUtils.toJsonObjStr(null, ResponseCode.FAILED, "保存或更新微信菜單失敗");
}
public List<HashMap<String, String>> getAllSiteInfo() {
List<HashMap<String, String>> allSiteInfo = siteConfigDao.findAll();
if (null != allSiteInfo && !allSiteInfo.isEmpty()) return allSiteInfo;
return null;
}
}複製代碼
在上面的代碼中,有的方法我就直接返回的json語句,同時獲取微信設置的代碼能夠簡要的看一下,仍是很簡單的。可是咱們能夠看到獲取AccessToken的代碼,我能夠說是寫的至關的簡單,可是事實真的如此嗎?看下WeiChatUtils的代碼。
/** * 單例,獲取微信AccessToken */
public class WeiChatUtils {
private static volatile WeiChatUtils singleton = null;
private static ApiConfig apiConfig;
private WeiChatUtils() {
}
public static WeiChatUtils getSingleton(String appId, String appSecret) {
if (singleton == null) {
synchronized (WeiChatUtils.class) {
if (singleton == null) {
singleton = new WeiChatUtils();
apiConfig = new ApiConfig(appId, appSecret);
}
}
}
return singleton;
}
public String getWeAccessToken() {
return apiConfig.getAccessToken();
}
}複製代碼
到這裏,咱們就能夠看明白,在上面的同步數據到微信服務器去得時候須要使用的AccessToken須要用單例保證它的惟一。至於爲何使用這個保證惟一,能夠看下ApiConfig的源碼,這裏就不在贅述。
固然這一期文章到此也差很少結束了。其實微信相關的接入仍是相對簡單。畢竟fastweixin已經幫咱們集成了大部分功能性的東西。我麼剩下只須要考慮業務的組成和數據組裝,畢竟程序員的本質也是這些。
至此,這一季的文章到這裏基本上告一段落了。
這兩天我在家本身把服務器折騰上了IPv6和https,固然不可避免的踩了不少坑,這些都是後話。
下季預告
在下一季中,咱們將採用全新的spring-boot來做爲咱們開發的手腳架,固然前端頁面的手腳架還在尋找中。同時下一季更多注重的是一些快速開發的技巧。 固然下一季的開發中,咱們會用okhttp做爲咱們新的後端網絡請求框架。
下一季,咱們先後端的東西都將要從新規劃,保證咱們項目高內聚低耦合,同時展開對微服務的探索。
簡要歸納
這兩季結束,我相信你必定能夠作簡單的網站了,畢竟咱們已經擁有:
固然,這些都是沒有徹底列舉出來。其實還有不少經常使用卻不顯眼的技巧,畢竟有的東西成了習慣你一時半會卻又想不起。這纔是咱們要達到的境界,開發的時候行雲流水胸有成竹。
若是你承認我所作的事情,而且認爲我作的事對你有必定的幫助,但願你也能打賞我一杯咖啡,謝謝。