上个月接到了我公司年会系统的需求,觉得做起来有些困难。后来硬着头皮接下来了。年会1月6号顺利举行结束,整体上还算是成功,但是最后的摇1摇比赛出了些问题。在这里记录下用到的技术,遇到的困难和选择,和做的处理和不足。希望对大家有些参考。
1.做1个系统,需要权衡的维度,有以下几个:
这就好比经典的CAP理论,鱼和熊掌不可兼得。这里寻求了时间(只有两周多的开发时间),本钱(实际上不应当过分紧缩本钱),功能(做全所有功能),放低了安全与周密的要求(例如消息传递没有加密,传递的消息没有盖时间戳验证流程,没有完全的会话保持与权限控制等等),而且把代码放到了GitHub上。
对1个针对普通大众的年会,这么做多是没问题的。但是对1个纯程序员的年会,这么做就难免出问题(我们现场系统遭到了js注入,XSS注入,SQL注入还有指令注入攻击。我们现场改代码热部署)。
现在回想,应当把1些功能做的更周密些,不应当过分紧缩本钱(其实就是多买两台服务器的事。。。)
2.对你做的系统,触及到现场屏幕视觉设计的,1定要提早摹拟下视觉匹配
3.之前对Websocket的理解有误,只在,对需要单向推送到客户端(手机阅读器)上的消息,应当都用Websocket,而不是采取客户端轮询。轮询对服务器消耗太大。然后,其实更多情形应当用SSE
4. 对产品设计上,可能需要改变下自己程序员的思惟。程序员都有点because we can的思惟,这其实不都是缺点。但是把这个思惟用在设计产品上就挂了。这里的例子就是摇1摇抽奖。这里我们没用微信摇1摇的功能,而是用js监控陀螺仪移动而做的摇1摇,显示的次数其实不是准确的你摇动的次数,可能会有很大偏差。但是我们把这个数字展现出来了,并且没做说明,让很多用户认为这个次数不公正,是我们私下做了手脚。
5.流程太繁琐,走简单流程抢时间难免出问题。目前,内部生产上线流程繁琐而且时间长。我们如果采取的话,全部开发时间都得用来走上线流程。所以,没采取公司资源,自己购买的腾讯云部署的利用,最后安全性出问题。如果走公司内部流程做足检查就不会出这些问题,但是时间上不允许。估计等公司变革完,这个情况会改良很多。
6. 弹幕做了服务降级,其实摇1摇那里也应当做服务降级
刚开始,接到的需求主要有这几个模块:微信签到上墙,CP签到抽奖,弹幕上墙,节目打赏,抽奖,摇1摇比赛还有红包链接展现。时间比较紧,基本上只有两周多的时间去开发。
团队里面算上我1共4人,都是新人(我是最老的员工,刚毕业1.5年。。)。划分了下任务,A同学负责签到前端,抽奖前后端,B同学负责节目管理打赏前端,摇1摇前端,C同学负责节目管理打赏前端,红包链接展现前后端,CP签到抽奖,我负责微信签到后端,微信接口调试和弹幕上前前后端。
整体逻辑架构设计:
微信开发回比较容易,文档全,但是文档有的更新不够新,而且管理界面有时让人第1次使用摸不着头脑。不过尝试出来如何配置后,还比较容易的。
首先,你得先去申请个微信公众号,我们这里要用的微信功能有:网页服务中的网页账号服务,微信JSAPI。摇1摇我们没用微信的摇1摇功能,用的是js的振东事件。对微信签到,我们只用到网页服务中的网页账号服务,其他的其他功能会用到。
对公众号,如果需要网页账号服务,则需要你的公众号经过认证。摇1摇需要其他资质认证,比较麻烦所以我没用。
对测试,可以先申请个微信测试号:http://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
申请好后,我们看到:
这个是为了测试你的服务器是不是认证良好,并且信任这台服务器并把消息转发给这台服务器。在配置时,微佩服务器会发1条消息到你配置的服务器,如果返回的结果正确,则配置成功(这里可以填写域名或IP,正式的公众号必须用域名,而且这个域名是ICP备案过的)。由于我们不做消息处理,而且我们只想简单的启用这个测试号,所以这里,我们只写了1个简单的直接返回结果的认证方法,代码以下:
@ResponseBody
@RequestMapping(value = "/weixin/message", method = RequestMethod.GET)
public String getWXUserInfo(@RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr) {
//加解密省略。。。直接返回成功
return echostr;
}
首先先要配置:
一样的,这里可以填写域名或IP,正式的公众号必须用域名,而且这个域名是ICP备案过的
测试号信息中的appID还有appSecret是你的app开放认证信息的证书。
1般的,开放平台都是利用OAuth2.0协议:
对微信,流程以下:
第1步:拼接自己的连接:
appId | wx0c7b8ab55037d5ca |
---|---|
scope | 利用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获得用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即便在未关注的情况下,只要用户授权,也能获得其信息),这里我们需要用snsapi_userinfo |
response_type | 只能填写code |
state | 重定向到你的页面时会带上这个state参数,没用的话随意填写就好了 |
redirect_uri | 域名1定要和你配置的1样,否则会报redirect_uri毛病,需要url编码 |
跳转的链接需要接收两个参数,1个是code,1个是state;假定我们这里跳转的地址为“/weixin/login”,则地址路径为:http://127.0.0.1/weixin/login,经过url编码为:http%3A%2F%2F127.0.0.1%2Fweixin%2Flogin
所以,最后的连接为:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx0c7b8ab55037d5ca&redirect_uri=http%3A%2F%2F127.0.0.1%2Fweixin%2Flogin&response_type=code&scope=snsapi_base&state=123#wechat_redirect
通过这个链接开始调试你的公众号。
建议用QQ阅读器,这样能调试微信的链接。
第2步,编写微信返回类:
微信的所有返回返回信息都是json情势的,如果参数有误,返回的结果都包括errcode和errmsg,所以编写微信返回基类:
public class BaseReturn implements Serializable {
private int errcode;
private String errmsg;
public int getErrcode() {
return errcode;
}
public void setErrcode(int errcode) {
this.errcode = errcode;
}
public String getErrmsg() {
return errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
public boolean isSuccessful() {
return this.errcode == 0;
}
}
客户端根据临时令牌code从服务提供方那里获得访问令牌access token的返回的类以下:
public class UserAuthorizationReturn extends BaseReturn {
private String access_token;// 网页授权接口调用凭证
private int expires_in;//access_token接口调用凭证超时时间,单位(秒)由于access_token具有较短的有效期,当access_token超时后,可使用refresh_token进行刷新,refresh_token具有较长的有效期(7天、30天、60天、90天),当refresh_token失效的后,需要用户重新授权。
private String refresh_token;// 用户刷新access_token
private String openid;// 用户唯1标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生1个用户和公众号唯1的OpenID
private String scope;//用户授权的作用域,使用逗号(,)分隔
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getRefresh_token() {
return refresh_token;
}
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
}
由于年会只有1个晚上,我们不用更新用户信息,所以对这里的expires_in其实不做处理。
以后通过accessToken拿取用户信息返回的类以下:
public class UserInfoReturn extends BaseReturn {
private String openid;//用户的唯1标识
private String nickname;//用户昵称
private int sex;//用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
private String province;//用户个人资料填写的省分
private String city;// 普通用户个人资料填写的城市
private String country;//国家,如中国为CN
private String headimgurl;//用户头像,最后1个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。
private String privilege;//用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
private String unionid;//只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getHeadimgurl() {
return headimgurl;
}
public void setHeadimgurl(String headimgurl) {
this.headimgurl = headimgurl;
}
public String getPrivilege() {
return privilege;
}
public void setPrivilege(String privilege) {
this.privilege = privilege;
}
public String getUnionid() {
return unionid;
}
public void setUnionid(String unionid) {
this.unionid = unionid;
}
}
第3步,根据上面的流程,编写下面代码,拿取用户信息:
@RequestMapping(value = "/weixin/login", method = RequestMethod.GET)
public String getWXUserInfo(@RequestParam("code") String code, HttpServletResponse response) {
try {
String s = httpRequest.sendGet("https://api.weixin.qq.com/sns/oauth2/access_token",
"appid=" + appId + "&secret=" + appSecret + "&code=" + code + "&grant_type=authorization_code");
UserAuthorizationReturn userAuthorizationReturn = JSON.parseObject(s, UserAuthorizationReturn.class);
s = httpRequest.sendGet("https://api.weixin.qq.com/sns/userinfo",
"access_token=" + userAuthorizationReturn.getAccess_token() + "&openid=" + userAuthorizationReturn.getOpenid() + "&lang=zh_CN");
Integer userId = userService.isSignedByWxInfo(userAuthorizationReturn.getOpenid());
log.info("微信返回:" + s);
//以后代码略
}
可以参加晚会的人名单是固定的,除这些人,其他人不能参与晚会。我们先把所有的人名单导入到数据库中。
我们使用工号姓名登陆。工号全是数字,有人有在工号前面加0的习惯,为了都能登录,我们保存在数据库中的类型是数字,前端传输过来的字符串会转换成数字与数据库中的比对。只有工号姓名匹配的用户才能登陆系统。
对已授权的微信誉户,如果登陆过的话,则不用再登陆1次。直接进入年会主界面。
用户输入工号姓名后,它的用户信息会被保存到数据库(包括工号姓名还有微信誉户信息)中。由于微信信息中的openid是唯1的,所以根据这个是不是在数据库中存在,判断是不是是第1次登陆。
完全的代码:
@RequestMapping(value = "/weixin/login", method = RequestMethod.GET)
public String getWXUserInfo(@RequestParam("code") String code, HttpServletResponse response) {
try {
//根据code获得accessToken
String s = httpRequest.sendGet("https://api.weixin.qq.com/sns/oauth2/access_token",
"appid=" + appId + "&secret=" + appSecret + "&code=" + code + "&grant_type=authorization_code");
UserAuthorizationReturn userAuthorizationReturn = JSON.parseObject(s, UserAuthorizationReturn.class);
s = httpRequest.sendGet("https://api.weixin.qq.com/sns/userinfo",
"access_token=" + userAuthorizationReturn.getAccess_token() + "&openid=" + userAuthorizationReturn.getOpenid() + "&lang=zh_CN");
Integer userId = userService.isSignedByWxInfo(userAuthorizationReturn.getOpenid());
log.info("微信返回:" + s);
if (userId != null) { //已签到
CookiesUtil.addCookie(response, "userId", String.valueOf(userId), 86400);
return "redirect:/frontend/main.html";
} else { //未签到
CookiesUtil.addCookie(response, "userJson", URLEncoder.encode(s, "UTF⑻"), 86400);
return "redirect:/frontend/login.html";
}
} catch (Exception e) {
log.warn(ExceptionUtils.getStackTrace(e));
}
return "redirect:/frontend/404.html";
}
这里我们偷懒了,并没有严格的会话和登录权限控制,只是做了简单的cookie。面对都是程序员的晚会,不应当做这么简单的登陆控制。
在用户第1次成功登陆也就是签到成功时,服务器需要将这个签到消息推送给客户端。这类单项推送的技术,有很多可以选择:
服务器通过Websocket通道,将人员签到的信息推送至签到墙页面,这里我应用的是最简单的tomcat 7的websocket实现。