项目github地址:https://github.com/pc859107393/SpringMvcMybatis
实时项目同步的地址是国内的码云:https://git.oschina.net/859107393/mmBlog-ser
我的简书首页是:http://www.jianshu.com/users/86b79c50cfb3/latest_articles
上一期是:[手把手教程][第二季]java 后端博客系统文章系统——No10
行走的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<HashMap<String,String>>。简要的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项目
当然,这些都是没有完全列举出来。其实还有很多常用却不显眼的技巧,毕竟有的东西成了习惯你一时半会却又想不起。这才是我们要达到的境界,开发的时候行云流水成竹在胸。
如果你认可我所做的事情,并且认为我做的事对你有一定的帮助,希望你也能打赏我一杯咖啡,谢谢。
支付宝捐赠
网友评论