[java手把手教程][第二季]java後端部落格系統文章系統——No11

pc859107393發表於2017-08-04

專案github地址:github.com/pc859107393…

實時專案同步的地址是國內的碼雲:git.oschina.net/859107393/m…

我的簡書首頁是:www.jianshu.com/users/86b79…

上一期是:[手把手教程][第二季]java 後端部落格系統文章系統——No10

行走的java全棧
行走的java全棧

工具

  • 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>。簡要的Dao如下:

@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專案

當然,這些都是沒有完全列舉出來。其實還有很多常用卻不顯眼的技巧,畢竟有的東西成了習慣你一時半會卻又想不起。這才是我們要達到的境界,開發的時候行雲流水成竹在胸。


如果你認可我所做的事情,並且認為我做的事對你有一定的幫助,希望你也能打賞我一杯咖啡,謝謝。

支付寶捐贈
支付寶捐贈

相關文章