[java手把手教程][第二季]java後端博客系統文章系統——No11

項目github地址:github.com/pc859107393…html

實時項目同步的地址是國內的碼雲:git.oschina.net/859107393/m…前端

個人簡書首頁是:www.jianshu.com/users/86b79…java

上一期是:[手把手教程][第二季]java 後端博客系統文章系統——No10linux

行走的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

本期目標

完成微信公衆號相關接入git

資源引入

既然咱們要開發微信相關的功能,那麼咱們須要微信相關的資源。首先是打開微信官方的開發者文檔。接着咱們應該構建微信相關的代碼了。?程序員

事實上並非這樣,咱們在開源中國的java項目中能夠找到一些跟微信相關的工具,本文中我採用了 fastweixin 來快速進行開發。github

compile 'com.github.sd4324530:fastweixin:1.3.15'複製代碼

參照fastweixin說明進行開發

實現微信互訪的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 >。簡要的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項目

固然,這些都是沒有徹底列舉出來。其實還有不少經常使用卻不顯眼的技巧,畢竟有的東西成了習慣你一時半會卻又想不起。這纔是咱們要達到的境界,開發的時候行雲流水胸有成竹。


若是你承認我所作的事情,而且認爲我作的事對你有必定的幫助,但願你也能打賞我一杯咖啡,謝謝。

支付寶捐贈
支付寶捐贈
相關文章
相關標籤/搜索