推荐一个好用的动画库Animate.css

看到一些电商应用,在添加商品到购物车时,都会有一个动画。

知道是用css实现,但没有自己写过帧,偶尔发现一个好用的库,正如它的名称所展示的,它就是专门做动画的,传递门https://daneden.github.io/animate.css/

特效特别种类的多,就像是幻灯片切换效果

cart-animation.gif

我从里面找出自己所需要的购物车跳动bounceIn效果,移植到自己的灵犀微商城里,就有了以下的效果

animate.gif

@keyframes bounceIn {
  from, 20%, 40%, 60%, 80%, to {
    animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
  }

  0% {
    opacity: 0;
    transform: scale3d(.3, .3, .3);
  }

  20% {
    transform: scale3d(1.1, 1.1, 1.1);
  }

  40% {
    transform: scale3d(.9, .9, .9);
  }

  60% {
    opacity: 1;
    transform: scale3d(1.03, 1.03, 1.03);
  }

  80% {
    transform: scale3d(.97, .97, .97);
  }

  to {
    opacity: 1;
    transform: scale3d(1, 1, 1);
  }
}

.bounceIn {
  animation-name: bounceIn;
  animation-duration: 1.25s;
}

相应的wxml文件加个一个变量{{animate}}关联它的生效与否就可以了。

<image src="../../images/icon_cart.png" class="button-image {{animate}}" />

而js代码中就是先添加移除样式再添加样式,好比jQuery中的removeClass().addClass()

     addCart: function() {
+       this.setData({
+           animate: ''
+       });
        api.addCart(this.data.goods);
+       this.setData({
+           animate: 'bounceIn'
+       })
    },

除Animate.css,还有一个动画库也常提起,http://vivify.mkcreative.cz/,不妨也试一试。

mp

借道公众号实现新订单推送

使用场景

小程序里,只能是用户发起form请求,才能被动收到1次或3次通知,比如支付,发货的通知,因此有客户下单,作为管理员收不到消息推送的,必须要时不时地看一下web后台订单管理很不方便,于是借道服务号的模板消息,向指定用户(即管理员)推送来单了的消息。本文的初衷就是为了解决这个无奈的。

解题思路

1.在小程序后台生成一个二维码,带着appid的参数,生成链接地址,例如https://mp.it577.net/bind?appid=wx15pq
2.扫一扫拿到上述的参数进入服务号体系,取得操作人即web后台管理员的openid
3.将openid与appid相关联
4.小程序中有用户在就向公众号暴露的发起推送指令,例如https://mp.it577.net/message/push?appid=wx15
pq
5.公众号接收到指令向管理员的openid推送订单模板信息

步骤

一、创建了数据库,使用express-generator脚手架生成应用

1.服务号api体系
1.1 bind (appid),其中appid由二维码参数得到,其中openid进入服务号时自然获得
1.2 pushMessage (appid,templateid,data)
2.小程序api体系
1.1 payment回调成功,即用户付款成功之后,向服务号api调用pushMessage接口

小程序web后台代码

1.创建二维码

这里使用一个第三方module,qr-image

详情用法如下

var {bindUrl} = require('../utils/config');
    var qr = require('qr-image');
    try {
        var img = qr.image(bindUrl, {size :5});
        res.writeHead(200, {'Content-Type': 'image/png'});
        img.pipe(res);
    } catch (e) {
        res.writeHead(414, {'Content-Type': 'text/html'});
        res.end('<h1>414 Request-URI Too Large</h1>');
    }

服务号代码

1.公众号管理后台绑定可信域名

2.请求服务号网页授权登录接口

文档地址:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN

3.代码描述

router.get('/auth', function(req, res) {
    var REDIRECT_URI = encodeURI(req.protocol + '://' + req.headers.host + req.originalUrl.replace(req.url, '') + '/code');
    console.log(REDIRECT_URI);
    var obj = {
        appid: mp.APPID,
        redirect_uri: REDIRECT_URI,
        response_type: 'code',
        scope: 'snsapi_userinfo',
        state: req.query.appid
    };
    var param = querystring.stringify(obj);
    var url = `https://open.weixin.qq.com/connect/oauth2/authorize?${param}`;
    console.log(url);
    res.redirect(url);
});

router.get('/code', function(req, res) {
    console.log("req.query");
    console.log(req.query.code);
    var obj = {
        appid: mp.APPID,
        secret: mp.SECRET,
        code: req.query.code,
        grant_type: 'authorization_code'
    };
    var appid = req.query.state;
    var param = querystring.stringify(obj);
    var url = `https://api.weixin.qq.com/sns/oauth2/access_token?${param}`;
    request(url, function(error, response, body) {
        console.log('error:', error); // Print the error if one occurred
        console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
        console.log('body:', body); // Print the HTML for the Google homepage.
        if (!body.errcode) {
            var data = JSON.parse(body);
            console.log(data);
            data = _.extend(data, {appid: appid});
            // 获取粉丝信息
            var infoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${data.access_token}&openid=${data.openid}&lang=zh_CN`;
            request(infoUrl, function (error, response, body) {
                console.log(infoUrl)
                console.log(body)
                data = _.extend(data, JSON.parse(body));
                // 判断是否已经存在
                Customer.findOneAndUpdate(_.pick(data, 'openid appid'), data, {upsert: true}, (err, result) => {
                    if (err) {
                        console.log(err);
                    }
                    res.render('success');
                });
            });
        }
    });
});

以上代码共完成了3部分操作:1.请求code,2.取出用户openId,3.绑定openId与小程序appid

3.小程序有用户下单支付向服务器请求推送模块消息

首先,创建服务号的模板消息,注意,这里并不是小程序的模板消息。

依然获得了data格式如下

{{first.DATA}}

订单编号:{{keyword1.DATA}}

客户昵称:{{keyword2.DATA}}

订单价格:{{keyword3.DATA}}

订单标题:{{keyword4.DATA}}

订单截止时间:{{keyword5.DATA}}

{{remark.DATA}}

接下来,在服务号创建被暴露一个message/push接口,供用户支付成功被调用

// 推送消息
router.post('/push', function(req, res) {
    // 使用querystring来格式化query字符串
    var url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${mp.APPID}&secret=${mp.SECRET}`;
    console.log(url);
    request(url, function(error, response, body) {
        var access_token = JSON.parse(body).access_token;
        // 请求模板消息接口
        var messageUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`;

        // 通过appid反查openid
        Customer.findOne({
            appid: req.body.appid
        }, (err, customer) => {
            if (err) {
                console.log(err);
            }
            // 用户id
            var openid = customer.openid;
            //
            var data = {
                "touser": openid,
                "template_id": "8_Vlmaf3EpW4fu-u94yTwHUurQWsymvdBxM-lsHZBTM",
                "url": req.body.url,
                "data": {
                    "first": {
                        "value": "来新订单了",
                        "color": "#173177"
                    },
                    "keyword1": {
                        "value": req.body.title,
                        "color": "#173177"
                    },
                    "keyword2": {
                        "value": moment.full(req.body.createdAt),
                        "color": "#173177"
                    },
                    "keyword3": {
                        "value": req.body.address,
                        "color": "#173177"
                    },
                    "keyword4": {
                        "value": req.body.nickname,
                        "color": "#173177"
                    },
                    "keyword5": {
                        "value": "已支付",
                        "color": "#173177"
                    },
                    "remark": {
                        "value": "请登录后台查看",
                        "color": "#173177"
                    }
                }
            };
            request({
                    url: messageUrl,
                    method: "POST",
                    json: true,
                    body: data
                },
                function(error, response, body) {
                    console.log('error:', error); // Print the error if one occurred
                    console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
                    console.log('body:', body); // Print the HTML for the Google homepage.
                }
            );
        });
    });
    res.send('push message success');
});

顺带提一句,querystring这个第三方module很好用,将对象转成?name=huangxj&age=30这样的字符串

结尾

经过这样的操作,就完成了借道服务号完成向小程序管理员推送消息的任务。以上思路来自于Bmob的消息推送机制,还有粉丝【是我没跑】的提点。

另辟蹊径

以上是通过软方式解决,还有一种是通过市面上云打印机的硬方案,用户下单,直接给打印推单子,机子直接吐小票,更要干脆利落。

mp

node.js实现微信支付退款

缘起

有付款就会有退款

注意,退款支持部分退款

左口袋的钱退到右口袋罗

退款0.01元

这次发起的退款请求0.01元是实时到账的,因此,用户在小程序端发起的退款只是一个请求到后台,后台审核人员审核无误后才后微信发起退款操作。

引入第三方module

在package.json 中加入"weixin-pay": "^1.1.7"这一条

代码目录结构

目录结构如下

入参

{ transaction_id: '4200000005201712165508745023', // 交易
  out_trade_no: '5b97cba0ae164bd58dfe9e77891d3aaf', // 自己这头的交易号
  out_refund_no: '6f3240c353934105be34eb9f2d364cec', // 退款订单,自己生成
  total_fee: 1, // 退款总额
  nonce_str: '1xSZW0op0KcdKoMYxnyxhEuF1fAQefhU', // 随机串
  appid: 'wxff154ce14ad59a55', // 小程序 appid
  mch_id: '1447716902', // 微信支付商户id
  sign: '416FCB62F9B8F03C82E83052CC77524B' // 签名,weixin-pay这个module帮助生成 }

然后由wxpay为我们生成其余字段,比如nonce_str,sign,当然还少不了p12证书,
这个早选在wxpay初始代码里已经配置了,pfx: fs.readFileSync(__dirname + '/../../../cert/apiclient_cert.p12'), //微信商户平台证书

lib/wechat/utils/wxpay.js的源码

const WXPay = require('weixin-pay'); // 引入weixin-pay这个第三方模块
const {weapp} = require('../../../utils/config'); // 我自己的全局配置文件,包括了appid key 等
const fs = require('fs');

const wxpay = WXPay({
    appid: weapp.APPID,
    mch_id: weapp.MCHID,
    partner_key: weapp.KEY, //微信商户平台 API secret,非小程序 secret
    pfx: fs.readFileSync(__dirname + '/../../../cert/apiclient_cert.p12'), 
});

module.exports = wxpay;

另外还有一个util.js工具类

用于验证与错误回调

const wxpay = require('./wxpay');

const validateSign = results => {
  const sign = wxpay.sign(results);
  if (sign !== results.sign) {
    const error = new Error('微信返回参数签名结果不正确');
    error.code = 'INVALID_RESULT_SIGN';
    throw error;
  };
  return results;
};

const handleError = results => {
  if (results.return_code === 'FAIL') {
    throw new Error(results.return_msg);
  }
  if (results.result_code !== 'SUCCESS') {
    const error = new Error(results.err_code_des);
    error.code = results.err_code;
    throw error;
  }
  return results;
};

module.exports = {
  validateSign,
  handleError,
};

发起退款请求

退款逻辑是这样的,先从自己这边的Order数据表中查出transaction_id/out_trade_no/total_fee,再拼上自己生成的out_refund_no退款单号,本次退款部分金额refund_fee,最后由weixin-pay这个模块下的wxpay.refund调起就可以了,成功就把订单状态改成"退款成功"

// 退款
router.post('/refund', function(req, res) {
    Order.findById(req.body._id, (err, order) => {
        if (err) {
            console.log(err);
        }
        console.log(order);
        // 生成微信设定的订单格式
        var data = {
            transaction_id: order.transactionId,
            out_trade_no: order.tradeId,
            out_refund_no: uuid().replace(/-/g, ''),
            total_fee: order.amount,
            refund_fee: order.amount
        };
        console.log(data);
        // 先查询订单,再退订单
        wxpay.refund(data, (err, result) => {
            if (err) {
                console.log(err);
                res.send(
                    utils.json({
                        code: 500,
                        msg: '退款失败'
                    })
                );
            }
            // 返回退款请求成功后,要将订单状态改成REFUNDED
            if (result.result_code === 'SUCCESS') {
                console.log(result);
                order.status = 'REFUNDED';
                order.save((err, response) => {
                    res.send(
                        utils.json({
                            msg: '退款成功'
                        })
                    );
                });
            } else {
                res.send(
                    utils.json({
                        code: 500,
                        msg: result.err_code_des
                    })
                );
            }

        });
    });
});

入参的坑

1.这次遇到的坑是refund_fee忘记传值,也就是说微信退款是支持部分退款的,如果是全额退款,那么将它赋值为total_fee相同

2.网上说的op_user_id: weapp.MCHID这个参数是非必选的

3.transaction_id 与 out_trade_no 二选一即可,这样在没有记录transaction_id的情况(比如没有写支付成功的callback)下,也能发起退款;其中优先级前者大于后者,在我在分别前其一故意给错的过程中得到了验证。

4.报了一个appid与商户号不匹配的报错,return_code: 'FAIL', return_msg: '商户号mch_id与appid不匹配’原来是小程序还没绑定公众号微信支付,这真是一个乌龙。

成功退款微信返回的数据

1. appid:"wxff154ce14ad59a55"
2. cash_fee:"1"
3. cash_refund_fee:"1"
4. coupon_refund_count:"0"
5. coupon_refund_fee:"0"
6. mch_id:"1447716902"
7. nonce_str:"c44wOvB6a4bQJfRk"
8. out_refund_no:"9ace1466432a4d548065dc8df95d904a"
9. out_trade_no:"5b97cba0ae164bd58dfe9e77891d3aaf"
10. refund_channel:""
11. refund_fee:"1"
12. refund_id:"50000705182017121702756172970"
13. result_code:"SUCCESS"
14. return_code:"SUCCESS"
15. return_msg:"OK"
16. sign:"5C2E67B3250054E8A665BF1AE2E9BDA3"
17. total_fee:"1"
18. transaction_id:”4200000005201712165508745023”

重复退款将返回如下

1. appid:"wxff154ce14ad59a55"
2. err_code:"ERROR"
3. err_code_des:"订单已全额退款"
4. mch_id:"1447716902"
5. nonce_str:"KP1YWlU7a5viZEgK"
6. result_code:"FAIL"
7. return_code:"SUCCESS"
8. return_msg:"OK"
9. sign:”C2A7DED787BEA644C325E37D96E9F41C”

人工退款

最后

如果没有写退款功能或者不想写退款功能怎么办,其实可以从微信支付的后台pay.weixin.qq.com,也是能退款出去的,只是不想忘记了要人工将订单状态置为退款状态。

鸣谢

感谢weixin-pay作者提供这个module,上述源码移植于LeanCloud小程序支付的demo,git地址是https://github.com/leancloud/weapp-pay-getting-started.git,一并感谢。

praise

mp

nginx+node在阿里云部署https

缘起

最近在写node+mongodb版本的灵犀微商城,所以免不了要自己去部署自己的https证书到阿里服务器

申请

等了2天才拿到免费的https证书,在阿里一直是显示售罄,难道最近https普遍起来导致供不应求了。

0元购买

绑定域名

填写资料——其实并不会打手机过来,注意要验证类型这里选的是文件

上传认证文件供访问

部署

部署步骤

上传了fileauth.txt到指定的目录下,几分钟后,就审核通过了,期间并没有审核电话打进来。操作部署阿里给出很详尽,复制过来就可以,不过要注意要将cert文件夹放在conf下,而不是说明上所说的nginx根目录。当你按阿里原原本本地操作,将看到这个报错,fopen:No such file or directory:fopen('/usr/local/nginx/conf/cert/214389510580391.pem','r') error:2006D080:BIO routines:BIO_new_file:no such file),也就明白是cert存在目录的问题。

node反向代理

location / {
            proxy_pass http://127.0.0.1:4001;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

node还是照旧跑,不关心https还是http,它由nginx完成。第1个proxy_pass是反向代理,后面3个proxy_set_header是将请求完全由mall.it577.net接管,不加上虽然能被前端请求,但是req.originUrl仍然还是会解析为http://127.0.0.1:4001,这就影响到了微信支付回调notify_url的解耦了,于是加上这3行。

结果

演示

从地址栏看到已经支持https了。

以上的过程是很简单,基本都能配置上,说下我遇到的坑。因为我阿里预装nginx并不带https模块,所以要自己手动下载安装包再重新编译,最后复制新编译出来的nginx执行文件替换并重启服务。

报错,提示找不到ssl模块

nginx: [emerg] unknown directive "ssl" in /usr/local/nginx/conf/nginx.conf:123

下载相应版本的ngix压缩包

wget http://nginx.org/download/nginx-1.12.1.tar.gz

解压

tar zxvf nginx-1.12.1.tar.gz

带参编译得到ssl模块

./configure --with-http_ssl_module

make

停止nginx

/usr/local/nginx/sbin/nginx -s stop

当然可以kill进程,网上搜到的是让kill的3种方法

替换旧nginx

cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
cp ./objs/nginx /usr/local/nginx/sbin/

重启nginx并重新加载conf配置文件

/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
/usr/local/nginx/sbin/nginx -s reload

上述踩坑参考了文章http://blog.51cto.com/1439337369/1863395,感谢前人栽树。

这时再在浏览器访问就看到上面给出的截图效果了,大功告成。

ps. 今天有位朋友说部署不了,需要协助安装的,发了红包忘记留言备注你的联系方式了,看到的话,留言给出联系方式吧。

mp

mongoose更新对象的两种方法对比

演示

cart.gif

描述场景

更新购物车数量与勾选状态

业务逻辑

查询到当前用户的购物车对象Cart,更新前端传递过来的quantity与selected字段

方法一

var _ = require('underscore');

Cart.findOneAndUpdate({
        _id: req.body.cart._id,
        user: user
    }, _.pick(req.body.cart, 'quantity', 'selected'), {
        new: true
    },
    function(err, updatedCart) {
        res.send(
            utils.json({
                data: updatedCart
            })
        );
    }
);

注:_.pick相当于

    {
        quantity: req.body.cart.quantity,
        selected: req.body.cart.selected
    }

方法二

var _ = require('underscore');

Cart.findOne({
        _id: req.body.cart._id,
        user: user
    }, function(err, cart) {
        if (err) {
            console.log(err);
        }
        // 复制对象
        _.extend(cart, req.body.cart);
        cart.save(function(err, updatedCart) {
            res.send(
                utils.json({
                    data: updatedCart
                })
            );
        });
    }
);

对比

第一种代码使用findOneAndUpdate只用了一步,更加简洁,适用于更新的字段少且非常明确的场景

第二种先findOne再对entity进行save操作,利用了underscore对象复制,面向整个对象操作更加灵活,适用于字段多且不确定的场景

结论

需求总是在变的,所以我一般采用第二种。

praise

mp