项目github地址:github.com/pc859107393…html
实时项目同步的地址是国内的码云:git.oschina.net/859107393/m…前端
个人简书首页是:www.jianshu.com/users/86b79…java
上一期是:[手把手教程][第二季]java 后端博客系统文章系统——No10linux
完成微信公众号相关接入git
既然咱们要开发微信相关的功能,那么咱们须要微信相关的资源。首先是打开微信官方的开发者文档。接着咱们应该构建微信相关的代码了。?程序员
事实上并非这样,咱们在开源中国的java项目中能够找到一些跟微信相关的工具,本文中我采用了 fastweixin 来快速进行开发。github
compile 'com.github.sd4324530:fastweixin:1.3.15'复制代码
实现微信互访的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
@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做为咱们新的后端网络请求框架。
下一季,咱们先后端的东西都将要从新规划,保证咱们项目高内聚低耦合,同时展开对微服务的探索。
简要归纳
这两季结束,我相信你必定能够作简单的网站了,毕竟咱们已经拥有:
固然,这些都是没有彻底列举出来。其实还有不少经常使用却不显眼的技巧,毕竟有的东西成了习惯你一时半会却又想不起。这才是咱们要达到的境界,开发的时候行云流水胸有成竹。
若是你承认我所作的事情,而且认为我作的事对你有必定的帮助,但愿你也能打赏我一杯咖啡,谢谢。