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

1,586 阅读13分钟

项目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项目

当然,这些都是没有完全列举出来。其实还有很多常用却不显眼的技巧,毕竟有的东西成了习惯你一时半会却又想不起。这才是我们要达到的境界,开发的时候行云流水成竹在胸。


如果你认可我所做的事情,并且认为我做的事对你有一定的帮助,希望你也能打赏我一杯咖啡,谢谢。

支付宝捐赠
支付宝捐赠