程序员人生 网站导航

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

栏目:综合技术时间:2017-03-22 09:32:01

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

先上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摇那里也应当做服务降级

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

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

2. 微信签到开发

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

2.1. 接口配置信息验证

这个是为了测试你的服务器是不是认证良好,并且信任这台服务器并把消息转发给这台服务器。在配置时,微佩服务器会发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;
}

2.2. 网页服务认证

首先先要配置:
这里写图片描述
一样的,这里可以填写域名或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);
        //以后代码略
}

2.3. 登陆逻辑

可以参加晚会的人名单是固定的,除这些人,其他人不能参与晚会。我们先把所有的人名单导入到数据库中。
我们使用工号姓名登陆。工号全是数字,有人有在工号前面加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。面对都是程序员的晚会,不应当做这么简单的登陆控制。

2.4. 签到上墙Websocket

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

  1. HTTP轮询和长轮询:最容易实现,但是比较繁琐而且耗费服务器资源. 简易轮询由于其本身的缺点,其实不推荐使用。Comet 技术其实不是 HTML 5 标准的1部份,从兼容标准的角度动身,也不推荐使用。
  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实现。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐