由一次年会系统大战所想到的。。。(1)

来源:互联网 发布:3 0和网络教育哪个好 编辑:程序博客网 时间:2024/05/10 11:15

上个月接到了我公司年会系统的需求,觉得做起来有些困难。后来硬着头皮接下来了。年会1月6号顺利举办结束,整体上还算是成功,但是最后的摇一摇比赛出了些问题。在这里记录下用到的技术,遇到的困难和选择,以及做的处理和不足。希望对于大家有些参考。

先上一点结论和感想

1.做一个系统,需要权衡的维度,有如下几个:
这里写图片描述
这就好比经典的CAP理论,鱼和熊掌不可兼得。这里追求了时间(只有两周多的开发时间),成本(实际上不应该过分压缩成本),功能(做全所有功能),放低了安全与严密的要求(例如消息传递没有加密,传递的消息没有盖时间戳验证流程,没有完整的会话保持与权限控制等等),而且把代码放到了GitHub上。
对于一个针对于普通大众的年会,这么做可能是没问题的。但是对于一个纯程序员的年会,这么做就难免出问题(我们现场系统受到了js注入,XSS注入,SQL注入还有指令注入攻击。我们现场改代码热部署)。
现在回想,应该把一些功能做的更严密些,不应该过分压缩成本(其实就是多买两台服务器的事。。。)

2.对于你做的系统,涉及到现场屏幕视觉设计的,一定要提早模拟下视觉匹配

3.之前对于Websocket的理解有误,只在,对于需要单向推送到客户端(手机浏览器)上的消息,应该都用Websocket,而不是采用客户端轮询。轮询对于服务器消耗太大。然后,其实更多情景应该用SSE

4. 对于产品设计上,可能需要改变下自己程序员的思维。程序员都有点because we can的思维,这并不都是缺点。但是把这个思维用在设计产品上就挂了。这里的例子就是摇一摇抽奖。这里我们没用微信摇一摇的功能,而是用js监控陀螺仪移动而做的摇一摇,显示的次数并不是准确的你摇动的次数,可能会有很大偏差。但是我们把这个数字展示出来了,并且没做说明,让很多用户认为这个次数不公正,是我们私下做了手脚。

5.流程太繁琐,走简单流程抢时间难免出问题。目前,内部生产上线流程繁琐而且时间长。我们如果采用的话,全部开发时间都得用来走上线流程。所以,没采用公司资源,自己购买的腾讯云部署的应用,最后安全性出问题。如果走公司内部流程做足检查就不会出这些问题,但是时间上不允许。估计等公司变革完,这个情况会改善很多。

6. 弹幕做了服务降级,其实摇一摇那里也应该做服务降级

1. 需求的确立与任务的分配

刚开始,接到的需求主要有这几个模块:微信签到上墙,CP签到抽奖,弹幕上墙,节目打赏,抽奖,摇一摇比赛还有红包链接展示。时间比较紧,基本上只有两周多的时间去开发。
团队里面算上我一共四人,都是新人(我是最老的员工,刚毕业1.5年。。)。划分了下任务,A同学负责签到前端,抽奖前后端,B同学负责节目管理打赏前端,摇一摇前端,C同学负责节目管理打赏前端,红包链接展示前后端,CP签到抽奖,我负责微信签到后端,微信接口调试和弹幕上前前后端。

2. 微信签到开发

整体逻辑架构设计:
这里写图片描述
微信开发还比较容易,文档全,但是文档有的更新不够新,而且管理界面有时让人第一次使用摸不着头脑。不过尝试出来如何配置后,还比较容易的。
首先,你得先去申请个微信公众号,我们这里要用的微信功能有:网页服务中的网页账号服务,微信JSAPI。摇一摇我们没用微信的摇一摇功能,用的是js的振东事件。对于微信签到,我们只用到网页服务中的网页账号服务,其他的其他功能会用到。
对于公众号,如果需要网页账号服务,则需要你的公众号经过认证。摇一摇需要其他资质认证,比较麻烦所以我没用。
对于测试,可以先申请个微信测试号:http://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
申请好后,我们看到:
这里写图片描述

2.1. 接口配置信息验证

这个是为了测试你的服务器是否认证良好,并且信任这台服务器并把消息转发给这台服务器。在配置时,微信服务器会发一条消息到你配置的服务器,如果返回的结果正确,则配置成功(这里可以填写域名或者IP,正式的公众号必须用域名,而且这个域名是ICP备案过的)。由于我们不做消息处理,而且我们只想简单的启用这个测试号,所以这里,我们只写了一个简单的直接返回结果的认证方法,代码如下:

@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;}

2.2. 网页服务认证

首先先要配置:
这里写图片描述
同样的,这里可以填写域名或者IP,正式的公众号必须用域名,而且这个域名是ICP备案过的
测试号信息中的appID还有appSecret是你的app开放认证信息的证书。
一般的,开放平台都是利用OAuth2.0协议:
这里写图片描述

对于微信,流程如下:
这里写图片描述

第一步:拼接自己的连接:

appId wx0c7b8ab55037d5ca scope 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息),这里我们需要用snsapi_userinfo response_type 只能填写code state 重定向到你的页面时会带上这个state参数,没用的话随便填写就行了 redirect_uri 域名一定要和你配置的一样,否则会报redirect_uri错误,需要url编码

跳转的链接需要接收两个参数,一个是code,一个是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浏览器,这样能调试微信的链接。

第二步,编写微信返回类:

微信的所有返回返回信息都是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;//    用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的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;    }}

由于年会只有一个晚上,我们不用更新用户信息,所以对这里的expires_in并不做处理。
之后通过accessToken拿取用户信息返回的类如下:

public class UserInfoReturn extends BaseReturn {    private String openid;//用户的唯一标识    private String nickname;//用户昵称    private int sex;//用户的性别,值为1时是男性,值为2时是女性,值为0时是未知    private String province;//用户个人资料填写的省份    private String city;//  普通用户个人资料填写的城市    private String country;//国家,如中国为CN    private String headimgurl;//用户头像,最后一个数值代表正方形头像大小(有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;    }}

第三步,根据上面的流程,编写下面代码,拿取用户信息:

@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);        //之后代码略}

2.3. 登陆逻辑

可以参加晚会的人名单是固定的,除了这些人,其他人不能参与晚会。我们先把所有的人名单导入到数据库中。
我们使用工号姓名登陆。工号全是数字,有人有在工号前面加0的习惯,为了都能登录,我们保存在数据库中的类型是数字,前端传输过来的字符串会转换成数字与数据库中的比对。只有工号姓名匹配的用户才能登陆系统。
对于已授权的微信用户,如果登陆过的话,则不用再登陆一次。直接进入年会主界面。
用户输入工号姓名后,它的用户信息会被保存到数据库(包括工号姓名还有微信用户信息)中。由于微信信息中的openid是唯一的,所以根据这个是否在数据库中存在,判断是否是第一次登陆。

完整的代码:

@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-8"), 86400);                return "redirect:/frontend/login.html";            }        } catch (Exception e) {            log.warn(ExceptionUtils.getStackTrace(e));        }        return "redirect:/frontend/404.html";    }

这里我们偷懒了,并没有严格的会话和登录权限控制,只是做了简单的cookie。面对都是程序员的晚会,不应该做这么简单的登陆控制。

2.4. 签到上墙Websocket

在用户第一次成功登陆也就是签到成功时,服务器需要将这个签到消息推送给客户端。这种单项推送的技术,有很多可以选择:

  1. HTTP轮询和长轮询:最容易实现,但是比较繁琐而且耗费服务器资源. 简易轮询由于其本身的缺陷,并不推荐使用。Comet 技术并不是 HTML 5 标准的一部分,从兼容标准的角度出发,也不推荐使用。
  2. Websocket:全双工协议,可以用。市面上所有浏览器都支持,对于Spring有很好的集成,但是是从Spring 4.0开始的,我们用的框架基于Spring 3.X,来不及升级。但是Tomcat 7.X有现成的websocket实现。WebSocket 规范和服务器推送技术都是 HTML 5 标准的组成部分,在主流浏览器上都提供了原生的支持,是推荐使用的。
  3. SSE服务器发送事件:对于简单的服务器数据推送的场景,使用服务器推送(SSE技术)事件就足够了。这个是最适合的,可惜当时我不知道这个技术。可以参考:http://www.cnblogs.com/imstudy/p/5682555.html.

服务器通过Websocket通道,将人员签到的信息推送至签到墙页面,这里我运用的是最简单的tomcat 7的websocket实现。

5 1
原创粉丝点击