專案github地址:github.com/pc859107393…
實時專案同步的地址是國內的碼雲:git.oschina.net/859107393/m…
我的簡書首頁是:www.jianshu.com/users/86b79…
上一期是:[手把手教程][第二季]java 後端部落格系統文章系統——No10
工具
- IDE為idea2017.1.5
- JDK環境為1.8
- gradle構建,版本:2.14.1
- Mysql版本為5.5.27
- Tomcat版本為7.0.52
- 流程圖繪製(xmind)
- 建模分析軟體PowerDesigner16.5
- 資料庫工具MySQLWorkBench,版本:6.3.7build
本期目標
完成微信公眾號相關接入
資源引入
既然我們要開發微信相關的功能,那麼我們需要微信相關的資源。首先是開啟微信官方的開發者文件。接著我們應該構建微信相關的程式碼了。?
事實上並不是這樣,我們在開源中國的java專案中可以找到一些跟微信相關的工具,本文中我採用了 fastweixin 來快速進行開發。
compile 'com.github.sd4324530:fastweixin:1.3.15'複製程式碼
參照fastweixin說明進行開發
實現微信互訪的Controller
為什麼說要實現這個?
- 配置微信相關設定
- 根據生成的設定和微信伺服器互聯
- 跟微信伺服器互動,繫結微信賬號
- 獲取和微信互動資料的令牌
所以,我們有一大堆事情要做,但是此時此刻我們採用的fastweixin已經做好一大步,我們按照他的說明編寫微信Controller。
@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作為我們新的後端網路請求框架。
下一季,我們前後端的東西都將要重新規劃,保證我們專案高內聚低耦合,同時展開對微服務的探索。
簡要概括
這兩季結束,我相信你一定可以做簡單的網站了,畢竟我們已經擁有:
- web前端技巧
- ajax的使用
- js的常用寫法
- js對html的dom操作
- 前端框架的引入和使用
- jstl載入網頁資料
- 後端開發技巧
- 程式業務流程分析(流程圖)
- 後端開發流程實現(三層開發)
- 複雜sql的編寫
- 常用註解的使用(三層註解、快取註解、sql註解)
- apiDocs文件的整合(spring-fox|swagger)
- spring框架的搭建(spring+springMvc+mybatis+Druid,資源掃描分配)
- 事務處理(異常和回滾)
- 檔案上傳處理
- Ueditor的接入
- 二級快取的接入(Ehcache)
- 使用者許可權認證(Shiro)
- 後端微信開發(採用fastweixin框架)
- httpclient的使用和簡易封裝(支援ssl連結)
- Gson快速序列化
- 加密策略
- restFul風格api的編寫
- 伺服器技巧
- linux環境搭建
- linux軟體配置
- linux常用命令
- mac、win系統連線控制linux伺服器
- 快速構建gradle專案
當然,這些都是沒有完全列舉出來。其實還有很多常用卻不顯眼的技巧,畢竟有的東西成了習慣你一時半會卻又想不起。這才是我們要達到的境界,開發的時候行雲流水成竹在胸。
如果你認可我所做的事情,並且認為我做的事對你有一定的幫助,希望你也能打賞我一杯咖啡,謝謝。