Bootstrap

并不想吹牛皮,但!为了把Github博客粉丝转移到公众号,我干了!

微信公众号:bugstack虫洞栈

沉淀、分享、成长,专注于原创专题案例,以最易学习编程的方式分享知识,让自己和他人都能有所收获。目前已完成的专题有;Netty4.x实战专题案例、用Java实现JVM、基于JavaAgent的全链路监控、手写RPC框架、架构设计专题案例[Ing]等。

1. 前言介绍

这个!截至到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搭建的静态博客与我并未开发过的微信公众号功能打通,通过在文章短口令码加锁引导用户到公众号内回复密码可解锁内容,以此来获得粉丝关注,当然如果取消关注了则文章继续锁定。

在多说一句,我理解的产品;其实是使用研发技术力搭建出可以用于承载接收用户在各种设备上所生产的行为数据的一种产品化服务。所以有些产品在做减法,同时也有为丰富的功能做加法,但究其一点我们其实都是在为接收有价值数据服务的。兴衰存亡,皆在核心数据沉淀与运作上!

2. 流程设计

为了使博客粉丝主动关注微信公众号,我们在用户初次浏览文章时增加权限验证,给每一个用户都生成一个唯一码引导用户在公众号内回复解锁文章,以此来与微信openid对应。当用户取消关注时则进行删除openid或标记状态,使得用户无法继续浏览文章。其实为了更好的体验,我参照了大牛的方式内容60%的区域可见,其余内容渐进遮挡,蒙胧胧的感觉还挺美。整体流程图如下;

3. 功能实现

为了实现本产品功能,我准备了;

3.1 前端

前端主要负责针对发布时设置了look: need的文章,在用户浏览文章检查是否有权限查看全部内容,当用户没有权限时隐藏文章60%内容,并通过页面结尾提醒用户在公众号内回复口令解锁文章。

前言介绍

为什么会有路由层?因为在微服务架构设计中,往往并不会直接将服务暴漏给调用端,而是通过调用路由层进行业务隔离,以达到不同的业务调用对应的服务模块。

Spring Cloud Zuul

// 文章所在容器的选择器
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_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();
}

// 查询后端的结果
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值回显到页面,提醒用户关注公众号回复口令解锁全部文章,以此来得到粉丝的关注。效果如下;

3.2 后端

开发环境

工程代码

>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请求,进行签名的验证
     * 

* 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);
    }

}

3.3 部署

1. 工程打包

>pom.xml

```xml

war

```

>WxApplication.java

```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);

}

}

```

```java

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

```

2. 服务上线

```java

[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开始

```

3. 功能验证

4. 综上总结