微信公众号:bugstack虫洞栈 沉淀、分享、成长,专注于原创专题案例,以最易学习编程的方式分享知识,让自己和他人都能有所收获。目前已完成的专题有;Netty4.x实战专题案例、用Java实现JVM、基于JavaAgent的全链路监控、手写RPC框架、架构设计专题案例[Ing]等。
这个!截至到19年11月我已经工作6年了,从业Java但也折腾过C#、搞PHP也弄过中继器、IO板卡,似乎我就是一个很喜欢在技术上折腾的人!与此同时,我也搞了6年的个人小网站,它们的呈现形式多种多样;有用PHP自己捣鼓的技术站用于分享资料、书籍、软件等、有用PHPWIND和DISCUZ的搭建的个人论坛、有用emlog和wordpress搭建的个人博客、也有借用于github+hexo/jekyll的能力组装出的技术博客。但无一例外它们都战死于征战的路上了,亡于;org域名不能备案、PHP服务器瘫痪被清空、http连接被注入恶意内容、定位不准确经常换模式、缺少核心优质内容等等。但!老衲的心依然如春(cun),因为喜欢干一件事,往往来自于干一了件喜欢的事!
所以!从19年开始我又继续写博客了,注册了新的域名bugstack.cn,备了案、买了服务器、还喊了新的口号;沉淀、分享、成长,让自己和他人都能有所收获!并且将尘封已久的微信公众号找回;结构上调整、内容上布局、粉丝上求关注。在这期间遇到了更大的牛;小灰、王二哥、纯洁的微笑还有松哥等一群伙伴,从他们那学到很多知识,真的非常感谢!
那么!这次的产品功能总结一句话就是;将基于Github+Jekyll搭建的静态博客与我并未开发过的微信公众号功能打通,通过在文章短口令码加锁引导用户到公众号内回复密码可解锁内容,以此来获得粉丝关注,当然如果取消关注了则文章继续锁定。
在多说一句,我理解的产品;其实是使用研发技术力搭建出可以用于承载接收用户在各种设备上所生产的行为数据的一种产品化服务。所以有些产品在做减法,同时也有为丰富的功能做加法,但究其一点我们其实都是在为接收有价值数据服务的。兴衰存亡,皆在核心数据沉淀与运作上!
为了使博客粉丝主动关注微信公众号,我们在用户初次浏览文章时增加权限验证,给每一个用户都生成一个唯一码引导用户在公众号内回复解锁文章,以此来与微信openid对应。当用户取消关注时则进行删除openid或标记状态,使得用户无法继续浏览文章。其实为了更好的体验,我参照了大牛的方式内容60%的区域可见,其余内容渐进遮挡,蒙胧胧的感觉还挺美。整体流程图如下;
为了实现本产品功能,我准备了;
- 微信公众号;bugstack虫洞栈
- 博客;本文使用的是github+jekyll;地址:https://bugstack.cn
- 域名;域名申请比较简单,但可能针对不同的服务商还必须备案后才可以使用
- 友盟;主要为了获取全局的唯一id值,也可以使用其他具备此功能的产品或者自己实现
- 部署环境;Java云服务器、Mysql云数据库、https证书、Jdk1.8、tomcat1.8,云市场比较多按需购买,如果是本地调试可以使用https://natapp.cn,做免费网络穿透映射
前端主要负责针对发布时设置了look: need的文章,在用户浏览文章检查是否有权限查看全部内容,当用户没有权限时隐藏文章60%内容,并通过页面结尾提醒用户在公众号内回复口令解锁文章。
- 找到加锁文章容器ID,文章容器选择器:article.post.container.need,拿到选择器ID针对这类文章进行缩小隐藏处理。
<article class="post container need lock" itemscope="" itemtype="http://schema.org/BlogPosting" style="height: 4400px;">
<div class="row">
<div class="col-md-9 markdown-body">
<h2 id="前言介绍">前言介绍</h2>
<p>为什么会有路由层?因为在微服务架构设计中,往往并不会直接将服务暴漏给调用端,而是通过调用路由层进行业务隔离,以达到不同的业务调用对应的服务模块。</p>
<p><strong>Spring Cloud Zuul</strong></p>
- 使用Jquery把文章所在容器高度缩小,这样会把内容进行压缩
// 文章所在容器的选择器
var articleSelector = 'article.post.container.need';
// 找到文章所在的容器
var $article = $(articleSelector);
// 文章的实际高度
var article = $article[0], height = article.clientHeight;
// 文章隐藏后的高度
var halfHeight = height * 0.4;
// 隐藏缩小
$article.css('height', halfHeight + 'px');
$article.addClass('lock');
- 给文章加一点朦胧感,渐变隐藏
.asb-post-01 {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
display: none;
z-index: 10000;
margin-bottom: 0;
}
.asb-post-01 .mask {
height: 240px;
width: 100%;
background: -webkit-gradient(linear, 0 top, 0 bottom, from(rgba(255, 255, 255, 0)), to(#fff));
}
- 从UM全局唯一ID中获取token 友盟作为网站数据采集服务,会生成一个针对用户的全局唯一值UM_distinctid,而我们需要就使用这个值部分截取后作为唯一token加锁钥匙
UM_distinctid = 16e9cd64925334-0882eb883c9554-7711b3e-144000-16e9cd6492631c
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2)
return parts.pop().split(";").shift();
}
function getToken() {
let value = getCookie('UM_distinctid');
if (!value) {
return getUUID().toUpperCase();
}
return value.substring(value.length - 6).toUpperCase();
}
- 定时轮旋检查是否关注公众号并解锁,可以优化写入全局Jquery值或者记录在cookie,减少轮询检查次数
// 查询后端的结果
var _detect = function() {
console.info(token);
$.ajax({
url : 'https://bugstack.cn/xx/xx/check',
type: "GET",
dataType: "text",
data : {
token : token
},
success : function(data) {
console.log(data);
if (data == 'refuse') {
_lock();
} else {
_unlock();
}
},
error : function(data) {
_unlock();
}
})
}
// 定时任务
_detect();
setInterval(function() {
_detect();
}, 5000);
- 引导用户关注公众号 现在将token值回显到页面,提醒用户关注公众号回复口令解锁全部文章,以此来得到粉丝的关注。效果如下;
- 本地环境开发需要安装(免费)http://natapp.cn/,做网络穿透便于我们进行测试
- 服务度三个主要接口;1同名称的get、post请求,分别用于公众号验验、接收用户行为与回复信息。2给博客提供验证是否解锁接口
- 在微信公众号;开发->基础设置(服务器配置),修改配置内容并开启服务,此时服务端会收到验证请求信息
- Java服务度采用领域驱动设计微服务方式开发,DDD的开发模式会更加清晰以及易于后续功能的拓展
- (提醒)个人微信号,部分接口权限是没有的,同时如果开启开发者功能配置后,自定义菜单和自定义回复都会失效。而自定义菜单又不开放给个人微信号,所以需要再次配置开启,但是不能修改
- jdk 1.8、tomcat8、idea2018、Maven3
- Spring Boot 2.1.2.RELEASE
- mysql 5.6
- natapp 网络穿透
itstack-ark-wx & 领域驱动设计方式设计
itstack-ark-wx
└── src
├── main
│ ├── java
│ │ └── org.itstack.demo
│ │ ├── application
│ │ │ ├── UserLockAuthService.java
│ │ │ ├── WxReceiveService.java
│ │ │ └── WxValidateService.java
│ │ ├── domain
│ │ │ ├── lockauth
│ │ │ │ ├── repository
│ │ │ │ │ └── IUserAuthPatrolRepository.java
│ │ │ │ └── service
│ │ │ │ └── UserLockAuthServiceImpl.java
│ │ │ ├── receive
│ │ │ │ ├── model
│ │ │ │ │ ├── BehaviorMatter.java
│ │ │ │ │ └── MessageTextEntity.java
│ │ │ │ ├── repository
│ │ │ │ │ └── IUserAuthGrantRepository.java
│ │ │ │ └── service
│ │ │ │ ├── engine
│ │ │ │ │ ├── impl
│ │ │ │ │ │ └── MsgEngineHandle.java
│ │ │ │ │ ├── Engine.java
│ │ │ │ │ ├── EngineBase.java
│ │ │ │ │ └── EngineConfig.java
│ │ │ │ ├── logic
│ │ │ │ │ ├── impl
│ │ │ │ │ │ ├── AnswerFilter.java
│ │ │ │ │ │ ├── SubscribeFilter.java
│ │ │ │ │ │ ├── UnlockFilter.java
│ │ │ │ │ │ └── UnsubscribeFilter.java
│ │ │ │ │ └── LogicFilter.java
│ │ │ │ └── WxReceiveServiceImpl.java
│ │ │ └── validate
│ │ │ └── service
│ │ │ └── WxValidateServiceImpl.java
│ │ ├── infrastructure
│ │ │ ├── common
│ │ │ │ └── Constants.java
│ │ │ ├── dao
│ │ │ │ └── UserAuthDao.java
│ │ │ ├── po
│ │ │ │ └── UserAuth.java
│ │ │ ├── repository
│ │ │ │ ├── UserAuthGrantRepository.java
│ │ │ │ └── UserAuthPatrolRepository.java
│ │ │ └── util
│ │ │ ├── sdk
│ │ │ │ └── SignatureUtil.java
│ │ │ └── XmlUtil.java
│ │ ├── interfaces
│ │ │ ├── BlogController.java
│ │ │ └── WxPortalController.java
│ │ └── WxApplication.java
│ └── resources
│ ├── mybatis
│ └── application.yml
└── test
└── java
└── org.itstack.ark.wx.test
└── ApiTest.java
itstack-ark-wx & 建表语句
CREATE TABLE
user_auth
(
id bigint NOT NULL AUTO_INCREMENT,
openId VARCHAR(64),
token VARCHAR(16) NOT NULL,
uuid VARCHAR(128),
createTime DATETIME,
updateTime DATETIME,
PRIMARY KEY (id, token),
CONSTRAINT idx_uuid UNIQUE (uuid)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8
讲解部分重点代码块,完整代码下载关注公众号;bugstack虫洞栈 & 回复:itstack-ark-wx
interfaces接口层
WxPortalController.java & 接收微信公众号验签与行为信息通知
-
微信公众号都是通过服务提供方的一个接口的get/post请求来执行操作的{这样设计真sao但真香}
-
get接口主要是验证签名
-
post接口会收到;关注、取消关注、用户的回复信息
/**
* 微信公众号:bugstack虫洞栈
* 纯洁版博客:https://bugstack.cn
* 沉淀、分享、成长,让自己和他人都能有所收获!
* Create by 付政委 on @2019
*/
@RestController
@RequestMapping("/wx/portal/{appid}")
public class WxPortalController {
private Logger logger = LoggerFactory.getLogger(WxPortalController.class);
@Autowired
private WxValidateService wxValidateService;
@Autowired
private WxReceiveService wxReceiveService;
/**
* 处理微信服务器发来的get请求,进行签名的验证
* <p>
* appid 微信端AppID
* signature 微信端发来的签名
* timestamp 微信端发来的时间戳
* nonce 微信端发来的随机字符串
* echostr 微信端发来的验证字符串
*/
@GetMapping(produces = "text/plain;charset=utf-8")
public String validate(@PathVariable String appid,
@RequestParam(value = "signature", required = false) String signature,
@RequestParam(value = "timestamp", required = false) String timestamp,
@RequestParam(value = "nonce", required = false) String nonce,
@RequestParam(value = "echostr", required = false) String echostr) {
try {
logger.info("微信公众号验签信息{}开始 [{}, {}, {}, {}]", appid, signature, timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
boolean check = wxValidateService.checkSign(signature, timestamp, nonce);
logger.info("微信公众号验签信息{}完成 check:{}", appid, check);
if (!check) return null;
return echostr;
} catch (Exception e) {
logger.error("微信公众号验签信息{}失败 [{}, {}, {}, {}]", appid, signature, timestamp, nonce, echostr, e);
return null;
}
}
/**
* 此处是处理微信服务器的消息转发的
*/
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(@PathVariable String appid,
@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature) {
try {
logger.info("接收微信公众号信息请求{}开始 {}", openid, requestBody);
MessageTextEntity message = XmlUtil.xmlToBean(requestBody, MessageTextEntity.class);
BehaviorMatter behaviorMatter = new BehaviorMatter();
behaviorMatter.setOpenId(openid);
behaviorMatter.setFromUserName(message.getFromUserName());
behaviorMatter.setMsgType(message.getMsgType());
behaviorMatter.setContent(message.getContent());
behaviorMatter.setEvent(message.getEvent());
behaviorMatter.setCreateTime(new Date(Long.parseLong(message.getCreateTime()) * 1000L));
// 处理消息
String result = wxReceiveService.doReceive(behaviorMatter);
logger.info("接收微信公众号信息请求{}完成 {}", openid, result);
return result;
} catch (Exception e) {
logger.error("接收微信公众号信息请求{}失败 {}", openid, requestBody, e);
return "";
}
}
}
BlogController.java & 博客验权接口,用于判断文章是否解锁
- 因为我们这个服务部署在二级域名下,因此需要设置跨域访问
- 由于我们网站是https加密,那么非https是不能被访问,需要下载安装证书到tomcat服务器
- 本接口只返回success和refuse,通过即可解锁
/**
* 微信公众号:bugstack虫洞栈
* 纯洁版博客:https://bugstack.cn
* 沉淀、分享、成长,让自己和他人都能有所收获!
* Create by 付政委 on @2019
*/
@CrossOrigin("https://bugstack.cn")
@RestController
@RequestMapping("/api")
public class BlogController {
private Logger logger = LoggerFactory.getLogger(BlogController.class);
@Autowired
private UserLockAuthService userLockAuthService;
@GetMapping(value = "check", produces = "application/json;charset=utf-8")
public String check(@RequestParam String token) {
try {
logger.info("校验博客浏览用户授权状态{}开始", token);
boolean status = userLockAuthService.checkAuth(token);
logger.info("校验博客浏览用户授权状态{}完成", token, status);
return status ? "success" : "refuse";
} catch (Exception e) {
logger.error("校验博客浏览用户授权状态{}失败", token, e);
return "refuse";
}
}
}
application应用层
- 本层主要定义逻辑分层,属于非常薄的一层,在一些复杂设计中会有一些服务编排
- UserLockAuthService 用户权限验证
- WxReceiveService 接收用户行为消息
- WxValidateService 微信公众号验签
UserLockAuthService.java & 定义后由领域层实现
public interface UserLockAuthService {
boolean checkAuth(String token);
}
domain领域层
- 领域层完成了;权限验证、行为消息处理、公众号验签
- 这里最重要的各种行为消息处理,我们设计为决策树工厂模型,定义了逻辑功能和引擎服务,后续只需要按需扩展即可
logic/LogicFilter.java & 定义逻辑模型,impl中有对应的一组的实现类
public interface LogicFilter {
String filter(BehaviorMatter request);
}
logic/impl/SubscribeFilter.java & 其中一个实现,关注时行为处理
@Service("subscribe")
public class SubscribeFilter implements LogicFilter {
private final String content = "您好!\n" +
"\n" +
"非常感谢您关注,微信公众号:bugstack虫洞栈 | 也期待您分享给更多小伙伴!\n" +
"\n" +
"bugstack虫洞栈,专注于原创技术专题案例,以最易学习编程开发的方式分享技术知识,让萌新、小白、大牛都能有所收获。目前已经完成的专题有;《Netty4.x从入门到实战》、《手写RPC框架》、《用Java实现JVM》、《基于JavaAgent的全链路监控》、《DDD专题案例》,其他更多专题还在排兵布阵中。\n" +
"\n" +
"获取专题案例源码回复;netty案例、rpc案例、用Java实现jvm源码、基于JavaAgent的全链路监控案例、DDD落地。\n" +
"\n" +
"联系作者:付政委 | monkeycode";
@Override
public String filter(BehaviorMatter request) {
return content;
}
}
engine/impl/MsgEngineHandle.java & 引擎路由调用
@Service("msgEngineHandle")
public class MsgEngineHandle extends EngineBase {
@Value("${wx.config.originalid:你的Err默认值}")
private String originalId;
@Override
public String process(BehaviorMatter request) throws Exception {
LogicFilter router = super.router(request);
if (null == router) return null;
String resultStr = router.filter(request);
if (StringUtils.isBlank(resultStr)) return "";
//反馈信息[文本],暂时只有文本后续按需拓展
MessageTextEntity res = new MessageTextEntity();
res.setToUserName(request.getOpenId());
res.setFromUserName(originalId);
res.setCreateTime(String.valueOf(System.currentTimeMillis() / 1000L));
res.setMsgType("text");
res.setContent(resultStr);
return XmlUtil.beanToXml(res);
}
}
infrastructure基础层
- 本层主要提供基础服务能力,包括数据库操作、缓存操作、工具包等
- 如果你的服务中由redis可以在本层来提供
- 在领域驱动设计中数据库不提倡共用,是单独分离,因为此设计中只有用户表还体现的不明显
repository/UserAuthGrantRepository.java & 数据库实现
@Repository("userAuthGrantRepository")
public class UserAuthGrantRepository implements IUserAuthGrantRepository {
@Autowired
private UserAuthDao userAuthDao;
@Override
public void grantAuth(String openId, String token) {
UserAuth userAuthReq = new UserAuth();
userAuthReq.setOpenId(openId);
userAuthReq.setToken(token);
userAuthReq.setUuid(openId + "_" + token);
userAuthDao.insert(userAuthReq);
}
@Override
public void revokeAuth(String openId) {
userAuthDao.delete(openId);
}
}
-
springboot配置打包部署成可运行war包
pom.xml
<packaging>war</packaging>
WxApplication.java
@SpringBootApplication public class WxApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(WxApplication.class); } public static void main(String[] args) { SpringApplication.run(WxApplication.class, args); } }
-
修改配置application.yml,数据库配置、微信配置
server: port: 80 spring: # datasource: # username: root # password: 123456 # url: jdbc:mysql://localhost:3306/itstack-ark-wx?useUnicode=true&characterEncoding=utf8 # driver-class-name: com.mysql.jdbc.Driver mybatis: mapper-locations: classpath:/mybatis/mapper/*.xml config-location: classpath:/mybatis/config/mybatis-config.xml # 微信公众号配置信息 # originalid:原始ID # appid:个人AppID # token:开通接口服务自定义设置 wx: config: originalid: xxxxxxx appid: xxxxxxx token: xxxxxxx
- 申请云服务器,目前提供方比较多,一般可以和域名选择一个服务商
- 准备好Linux服务器jdk1.8、tomcat8
- 看个人域名访问情况,酌情申请Https,配置RSA
- 本地安装XShell、Xftp,方便部署和文件传输
- 准备好个人微信公众号,用于配置服务
- 启动服务
[root@instance-39394m67 bin]# ./startup.sh . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.2.RELEASE) 2019-11-23 18:10:57.131 INFO 22052 --- [ost-startStop-1] org.itstack.ark.wx.WxApplication : Starting WxApplication v1.0.0-SNAPSHOT on instance-39394m67 with PID 22052 (/usr/local/java/apache-tomcat-8.5.37/webapps/itstack-ark-wx/WEB-INF/classes started by root in /usr/local/java/apache-tomcat-8.5.37/bin) 2019-11-23 18:10:57.158 INFO 22052 --- [ost-startStop-1] org.itstack.ark.wx.WxApplication : No active profile set, falling back to default profiles: default 2019-11-23 18:10:59.137 INFO 22052 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1890 ms 2019-11-23 18:11:01.050 INFO 22052 --- [ost-startStop-1] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2019-11-23 18:11:01.336 WARN 22052 --- [ost-startStop-1] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration) 2019-11-23 18:11:01.677 INFO 22052 --- [ost-startStop-1] org.itstack.ark.wx.WxApplication : Started WxApplication in 5.873 seconds (JVM running for 10.311) 23-Nov-2019 18:11:01.718 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/java/apache-tomcat-8.5.37/webapps/itstack-ark-wx.war] has finished in [9,226] ms 23-Nov-2019 18:11:01.720 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/java/apache-tomcat-8.5.37/webapps/ROOT] 23-Nov-2019 18:11:01.746 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/java/apache-tomcat-8.5.37/webapps/ROOT] has finished in [26] ms 23-Nov-2019 18:11:01.753 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-80"] 23-Nov-2019 18:11:01.764 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"] 23-Nov-2019 18:11:01.766 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 9326 ms 2019-11-23 18:11:13.039 INFO 22052 --- [p-nio-80-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2019-11-23 18:11:13.059 INFO 22052 --- [p-nio-80-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 20 ms 2019-11-23 18:11:13.172 INFO 22052 --- [p-nio-80-exec-1] o.i.ark.wx.interfaces.BlogController : 校验博客浏览用户授权状态UDHIUS开始
- 还是很感激那些前途的探路人,为原创研发贡献力量
- 我博客;https://bugstack.cn,可以尝试验证或者克隆对应的代码仓库
- 利用了11月24日周六给自己加个通宵班完成所有代码的开发和服务申请到部署,从最开始拿到想法到最终实现还是经历了很多坎坎坷坷
- 这里面还有很多可以优化的点以及一些拓展的功能,后续会陆续完善,如果由小伙伴有好的点子欢迎随时交流
- 源码贡献给所有研发,微信公众号内回复;itstack-ark-wx,即可获得一份基于领域驱动设计开发的微信公众号与博客打通工程(Star&Frok)
- 夜深人静咣咣敲代码我也会怀疑,我是谁,我在哪,我在干什么!哎,这就是;喜欢干一份工作,往往来自于干了一件喜欢的工作