点击这里加入微信应用开发群
http://shang.qq.com/wpa/qunwpa?idkey=7322d7b39a8564d24b48b5e7d83fcde645f4c0e9ad84f1f7ddc7baff47574d7d

跟大家分享微信H5摇红包应用开发相关的技术。本案例来自实际项目,某企业80万现金红包。如果出现图片丢失的情况,请访问原文
http://www.imwinlion.com/archives/168

1、应用场景描述
年初,我们收到一个需求,线下40多个城市2000多家实体店需要推广一个新产品,用户每次购买新产品,将获得一次摇红包的机会。
线下还有有针对经销商的会议营销活动,一个会议室内可能有数百人至数千人参加会议,会议中场有摇红包活动。红包通过微信发放。
需求就这些了,看起来简单,但实际生产环境中,发现了N多坑,在后面我们将一一论述。
2、物料准备
物料名称 如何获得
1 微信认证且开头支付的服务号 mp.weixin.qq.com 申请获得
2 微信appid、appsecret,等基本信息 微信公众账号平台提供
3 用户openid 通过auth2 接口获得
4 微信支付apikey pay.weixin.qq.com平台提供
5 微信支付cerfile,及rsa key 文件 pay.weixin.qq.com平台提供
3、 相关流程设计及技能点说明
3.1 活动流程设计

上图展示了用户侧的相关操作,此次活动用户最多只有一次获奖机会,另一方面,服务器开启定时轮询服务,用于发放红包,至于为什么.,后面再阐述
3.2 数据库表结构设计及红包队列机制

repacket 表是红包基础信息表,用户摇红包时,会在该表中创建一条记录,此时stat状态是0。
另一方面,服务器上有轮询程序,5秒轮询一次,每次从表redpacket 中取出stat=0的10条记录,进行红包发放操作,每发放成功一个,对于的stat设置为1。
为了确保红包送到,我们设计了ntrytimes 字段,每次发送如果未成功,则 该字段增1,值到该字段数值高于某值,一般为5
wxlog 表用来记录红包发放反馈细节,用于红包发放会碰到很多突发问题,该表记录每次发放的细节,有利于提高我们的服务质量
3.3、如何防止被微信支付拒绝
微信红包被拒有很多情况,如下是其中最常见的三种,
3.3.1 发放失败,此请求可能存在风险,已被微信拦截 该情况一般是因为用户帐号存在异常,比如微信监控机制发现用户利用红包在赌博,则会屏蔽。此类问题没有解决方法。
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[发放失败,此请求可能存在风险,已被微信拦截]]></return_msg>
<result_code><![CDATA[FAIL]]></result_code>
<err_code><![CDATA[NO_AUTH]]></err_code>
<err_code_des><![CDATA[发放失败,此请求可能存在风险,已被微信拦截]]></err_code_des>
<mch_billno><![CDATA[re2016031816491917528]]></mch_billno>
<mch_id>1237758902</mch_id>
<wxappid><![CDATA[公众号APPID]]></wxappid>
<re_openid><![CDATA[okoQctzL9-fRrRlr-gIj4cEazNFM]]></re_openid>
<total_amount>100</total_amount>
</xml>
3.3.2 超过频率限制,请稍后再试,公众帐号对某一个用户发放红包频次不能太多,否则会出现这种情况。另外系统发放红包对微信API请求不能过于频繁。此类问题解决方法就是控制用户红包数目,并且控制API请求频率
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[超过频率限制,请稍后再试]]></return_msg>
<result_code><![CDATA[FAIL]]></result_code>
<err_code><![CDATA[FREQ_LIMIT]]></err_code>
<err_code_des><![CDATA[超过频率限制,请稍后再试]]></err_code_des>
<mch_billno><![CDATA[re2016032110454813571]]></mch_billno>
<mch_id>1237758902</mch_id>
<wxappid><![CDATA[公众号APPID]]></wxappid>
<re_openid><![CDATA[okoQctyDbaPvyPKSXq9QberLnPUM]]></re_openid>
<total_amount>200</total_amount>
</xml>
3.2 帐号余额不足,请到商户平台充值后再重试. 一种可能是真没钱了,另外一种可能是并发太大,微信会返回此种情况,解决方案是将请求变成顺序的。后面章节会单独说明
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[帐号余额不足,请到商户平台充值后再重试]]></return_msg>
<result_code><![CDATA[FAIL]]></result_code>
<err_code><![CDATA[NOTENOUGH]]></err_code>
<err_code_des><![CDATA[帐号余额不足,请到商户平台充值后再重试]]></err_code_des>
<mch_billno><![CDATA[re2016032110460657709]]></mch_billno>
<mch_id>1237758902</mch_id>
<wxappid><![CDATA[公众号APPID]]></wxappid>
<re_openid><![CDATA[okoQct14A0OYz_bx0Spe0P-6HNTw]]></re_openid>
<total_amount>100</total_amount>
</xml>
3.3 如果发放成功,则返回如下信息
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[发放成功]]></return_msg>
<result_code><![CDATA[SUCCESS]]></result_code>
<mch_billno><![CDATA[re2016032417512098479]]></mch_billno>
<mch_id>1237758902</mch_id>
<wxappid><![CDATA[公众号APPID]]></wxappid>
<re_openid><![CDATA[okoQct7fPhd1JqtDz0LqTUxf1kNU]]></re_openid>
<total_amount>200</total_amount>
<send_listid><![CDATA[0010173893201603240631676524]]></send_listid>
<send_time><![CDATA[20160324175121]]></send_time>
</xml>
3.3.4 下面附录微信官方给出的错误提示及解决方案
错误码 错误描述 原因 解决方式
NO_AUTH 发放失败,此请求可能存在风险,已被微信拦截 用户账号异常,被拦截 请提醒用户检查自身帐号是否异常。使用常用的活跃的微信号可避免这种情况。
SENDNUM_LIMIT 该用户今日领取红包个数超过限制 该用户今日领取红包个数超过你在微信支付商户平台配置的上限 如有需要、请在微信支付商户平台【api安全】中重新配置 【每日同一用户领取本商户红包不允许超过的个数】。
ILLEGAL_APPID 非法appid,请确认是否为公众号的appid,不能为APP的appid 错误传入了app的appid 接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。
MONEY_LIMIT 红包金额发放限制 发送红包金额不再限制范围内 每个红包金额必须大于1元,小于200元(可联系微信支付wxhongbao@tencent.com申请调高额度)
SEND_FAILED 红包发放失败,请更换单号再重试 该红包已经发放失败 如果需要重新发放,请更换单号再发放
FATAL_ERROR openid和原始单参数不一致 更换了openid,但商户单号未更新 请商户检查代码实现逻辑
金额和原始单参数不一致 更换了金额,但商户单号未更新 请商户检查代码实现逻辑 请检查金额、商户订单号是否正确
CA_ERROR CA证书出错,请登录微信支付商户平台下载证书 请求携带的证书出错 到商户平台下载证书,请求带上证书后重试
SIGN_ERROR 签名错误 1、没有使用商户平台设置的商户API密钥进行加密(有可能之前设置过密钥,后来被修改了,没有使用新的密钥进行加密)。 1. 到商户平台重新设置新的密钥后重试
2、加密前没有按照文档进行参数排序(可参考文档) 2. 检查请求参数把空格去掉重试
3、把值为空的参数也进行了签名。可到(http://mch.weixin.qq.com/wiki/tools/signverify/ )验证。 3. 中文不需要进行encode,使用CDATA
4、如果以上3步都没有问题,把请求串中(post的数据)里面中文都去掉,换成英文,试下,看看是否是编码问题。(post的数据要求是utf8) 4. 按文档要求生成签名后再重试
在线签名验证工具:http://mch.weixin.qq.com/wiki/tools/signverify/
SYSTEMERROR 请求已受理,请稍后使用原单号查询发放结果 系统无返回明确发放结果 使用原单号调用接口,查询发放结果,如果使用新单号调用接口,视为新发放请求
xml_ERROR 输入xml参数格式错误 请求的xml格式错误,或者post的数据为空 检查请求串,确认无误后重试
FREQ_LIMIT 超过频率限制,请稍后再试 受频率限制 请对请求做频率控制(可联系微信支付wxhongbao@tencent.com申请调高)
NOTENOUGH 帐号余额不足,请到商户平台充值后再重试 账户余额不足 充值后重试
OPENID_ERROR openid和appid不匹配 openid和appid不匹配 发红包的openid必须是本appid下的openid
PARAM_ERROR act_name字段必填,并且少于32个字符 请求的act_name字段填写错误 填写正确的act_name后重试
发放金额、最小金额、最大金额必须相等 请求的金额相关字段填写错误 按文档要求填写正确的金额后重试
红包金额参数错误 红包金额过大 修改金额重试
appid字段必填,最长为32个字符 请求的appid字段填写错误 填写正确的appid后重试
订单号字段必填,最长为28个字符 请求的mch_billno字段填写错误 填写正确的billno后重试
client_ip必须是合法的IP字符串 请求的client_ip填写不正确 填写正确的IP后重试
输入的商户号有误 请求的mchid字段非法(或者没填) 填写对应的商户号再重试
找不到对应的商户号 请求的mchid字段填写错误 填写正确的mchid字段后重试
nick_name字段必填,并且少于16字符 请求的nick_name字段错误 按文档填写正确的nick_name后重试
nonce_str字段必填,并且少于32字符 请求的nonce_str字段填写不正确 按文档要求填写正确的nonce_str值后重试
re_openid字段为必填并且少于32个字符 请求的re_openid字段非法 填写对re_openid后重试
remark字段为必填,并且少于256字符 请求的remark字段填写错误 填写正确的remark后重试
send_name字段为必填并且少于32字符 请求的send_name字段填写不正确 按文档填写正确的send_name字段后重试
total_num必须为1 total_num字段值不为1 修改total_num值为1后重试
wishing字段为必填,并且少于128个字符 缺少wishing字段 填写wishing字段再重试
商户号和wxappid不匹配 商户号和wxappid不匹配 请修改Mchid或wxappid参数
3.4 操作系统下的定时器
我们之所以要启用定时器操作,是为了保证我们能及时地响应红包发放操作。同时也为了保证任意时间我们向微信支付平台调用支付请求最多只有一个http请求。在win系统下,我们需要添加定时任务,如下是相应的俩张图,详情自己可以百度
Win7怎么设置定时自动执行任务?Win7怎么设置定时自动执行任务?
在创建定时任务的时候,我们最好先创建一个bat(hongbao.bat)文件,内容如下
C:\phpStudy\php53\php.exe C:\WWW\www.xxx.com\task.php Index/hongbao
task.php 是一个php文件, Index/hongbao 是Index模块下的 hongbao发放方法
该示例采用THINKPHP框架,CGI模式
linux下定时器使用crontab,考虑到时间限制,我们一般一次拉取10个红包,并10秒循环一次
crontab -e 编辑crontab服务文件

*/2 * * * * /bin/sh /home/admin/jiaoben/hongbao.sh

保存文件并并退出

关于crontab相应的资料很多,可以自行百度。

hongbao.sh 里的内容是thinkphp cgi模式的php 接口

php /var/WWW/www.xxx.com\task.php Index/hongbao

3.5 h5实现摇动代码解析
var u = navigator.userAgent;
var isAndroid = u.indexOf(‘Android’) > -1 || u.indexOf(‘Adr’) > -1; //android终端
var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
var C_W= document.body.clientWidth;
var S_W = window.screen.availWidth;
var S_H = window.screen.availHeight;
var scare = C_W/S_W;
var scaretu = C_W/640;
var C_H =S_H * scare -(isAndroid?60:20)+20;
var SHAKE_THRESHOLD =-1;
var last_update = 0;
var x = y = z = lastX = lastY = lastZ = 0;
var canShake = 1;
var ntimes = 0;
var notdoing = 1;
var idex = Math.ceil(Math.random() * 4) +2;
var speed = 0;
var dmt = [];
var sum = 0;
var ntest = 3;
var deviceinfo = {
}
deviceinfo.device =isiOS?”ios”:(isAndroid?”android”:u);
/*监听核心函数*/
function deviceMotionHandler(eventData){
var acceleration = eventData.accelerationIncludingGravity;
var curTime = new Date().getTime();
//100ms监听一次,拒绝重复监听
if ((curTime – last_update) > 200) {
var diffTime = curTime – last_update;
last_update = curTime;
x = acceleration.x;
y = acceleration.y;
z = acceleration.z;
speed = Math.abs(x-lastX)>Math.abs(y-lastY)? Math.abs(x-lastX): Math.abs(y-lastY);
speed = Math.abs(z-lastZ)>speed? Math.abs(z-lastZ): speed;
//校验阈值,有些手机可能灵敏 有些人可能摇动值比较大,那么我们取23次平均值就好了。
if(dmt.length<ntest){
speed > 10 && dmt.push(Math.ceil(speed)) && Audio.play();
return;
}else{
if(SHAKE_THRESHOLD==-1){
sum = 0;
for(var i=0;i<dmt.length;i++){
sum = sum+parseInt(dmt[i]);
}
SHAKE_THRESHOLD = Math.ceil(sum/dmt.length);
deviceinfo.threhole = SHAKE_THRESHOLD;
return ;
}
}
//如下是动画操作和声音播放以及reqChance 等请求操作,reqChance 是向后端申请红包,值得一提的是红包并不是马上到位,这里只是申请红包机会,真正的红包发放 那个是上面章节提到的 定时器操作。
if(speed > SHAKE_THRESHOLD && canShake==1){
$(“#scene3-tipshake”).attr(“src”,”__APPS__shinimanred/2-shake-shijin.png”);
Audio.play()
$(“#scene3-headerRotate”).attr(“class”,”xlelement headerRotate”);
$(“#scene3-headercircle”).attr(“class”,”xlelement scene3-headercircle”);
ntimes+=1;
notdoing = 0;
$(“#scene3-tipshake”).attr(“class”,”xlelement”);
if(ntimes>idex && !recorder.giftid){
canShake = 0;
if(lock==true){
return ;
}
lock = true;
deviceinfo.address = localStorage.getItem(“mobile”)+”,”+localStorage.getItem(“name”);
if(GAME.curentscent==”shake”) {
$.post(“{:U(‘reqChance’,array(‘id’=>$appcfg[id],’stageid’=>$stageinfo[id]))}”, deviceinfo, function (json) {
if (json.status == 1) {
recorder = json.data;
curentgift = giftsmapinfo[recorder.giftid];
canShake = 0;
if(parseInt(recorder.amount)==0 && recorder.gifttype==”HONGBAO”){
GAME.dispatcher((“youhavewinno”));
}else{
GAME.dispatcher((“youhavewin”+curentgift.type).toLowerCase());
}
} else {
alert(json.info);
}
lock = false;
}).error(function (e) {
//alert(JSON.stringify(e));
lock = false;
});
}
}
}else{
if(notdoing!=0 && notdoing>2){
$(“#scene3-tipshake”).attr(“src”,”__APPS__shinimanred/2-shake-oncemore.png”);
$(“#scene3-headerRotate”).attr(“class”,”xlelement”);
$(“#scene3-headercircle”).attr(“class”,”xlelement”);
$(“#scene3-tipshake”).addClass(“animated”).addClass(“bounce”);
}
canShake = 1;
notdoing += 1;
};
}
lastX = x;
lastY = y;
lastZ = z;
}
3.6 如何处理音频
该应用有俩个音乐,一个是摇中的音乐,一个是摇动中的,何时播放,如何流程地播放,这是个大问题,本章节采用如下js 很好地解觉了该问题
/*如下是音频处理*/
var Audio={
res:{},
isios:false,
isfree:1
};
Audio.init=function(o){
this.res = o;
this.isios = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
return this;
}
Audio.play=function(){
var obj = this;
if(this.isios){
if(this.isfree==1){
this.isfree=0;
this.res.play();
setTimeout(function(){
obj.isfree = 1;
},1000);
}
}else{
this.res.play();
}
return this;
}
使用方法
比如dom中有
<audio src=”shakeget.mp3″ preload=”preload” id=”videoShakeOk”></audio>
则可以使用如下代码直接调用
var btnShakeOk = document.getElementById(‘videoShakeOk’);
Audio.init(btnShakeOk).play();
3.7 php 实现红包发放核心函数,以thinkphp为例
/**
*publish:红包发放统一接口
* @param $mchid 商户号码
* @param $wxappid 微信的appid
* @param $apikey ,api密钥
* @param $cerpath //cert 文件 路径
* @param $keypath //key 文件 路径
* @param $openid //发放给谁
* @param $amount //发放数目,以分为单位
* @param $sendname //发放这的名称
* @param $wishing //发放者寄语
* @param $actname //此次活动名称
* @param $reamak //此次活动简单说明
* @return array status: 0 标识失败 1 标识成功
* @version 1.0
*/
public function publish($mchid,$wxappid,$apikey,$cerpath,$keypath,$openid,$amount,$sendname,$wishing,$actname,$reamak){
$config = array();
$config[“nonce_str”] = md5(microtime());
$config[“mch_billno”] = “re” . date(“YmdHis”) . mt_rand(10000,99999);
$config[“mch_id”] =$mchid;
$config[“wxappid”] =$wxappid;
$config[“send_name”] =$sendname;
$config[“re_openid”] = $openid;
$config[“total_amount”] = $amount;
$config[“total_num”] = 1;
$config[“wishing”] =$wishing;
$config[“client_ip”] = get_client_ip();
$config[“act_name”] = $actname;
$config[“remark”] =$reamak;
ksort($config);
$str = urldecode(http_build_query($config));
$str .= “&key=” . $apikey;
$sig = strtoupper(md5($str));
$config[“sign”] = $sig;
$xml = “<xml>”;
foreach($config as $k => $v){
$xml .= “<$k>$v</$k>”;
}
$xml .= “</xml>”;
$ret = self::postxmlCurl(“https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack”,$xml,$cerpath,$keypath);
if($ret[“status”]){
//这里可以记录相应的信息到日记
return array(“status”=>1,”errmsg”=>””,”retmsg”=>$ret[“retxml”]);
}else{
//这里可以记录相应的信息到日记
if($ret[“err_code”]==”NOTENOUGH”){
$str = “不好意思,本次红包已经领完了敬请期待下次活动”;
return array(“status”=>0,”info”=>$str,”retxml”=>$ret[“retxml”],”errorcode”=>$ret[“err_code”]);
}else{
return array(“status”=>0,”info”=>$ret[“info”],”retxml”=>$ret[“retxml”],”errorcode”=>$ret[“err_code”]);
}
}
}
/**
* 以post方式提交xml到对应的接口url
*
* @param string $xml 需要post的xml数据
* @param string $url url
* @param bool $useCert 是否需要证书,默认不需要
* @param int $second url执行超时时间,默认30s
* @throws WxPayException
*/
private static function postxmlCurl($url,$xml,$cerpath,$keypath, $second = 30)
{
$ch = curl_init();
//设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);//严格校验2
//设置header
curl_setopt($ch, CURLOPT_HEADER, FALSE);
//要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch,CURLOPT_SSLCERTTYPE,’PEM’);
curl_setopt($ch,CURLOPT_SSLCERT, $cerpath);
curl_setopt($ch,CURLOPT_SSLKEYTYPE,’PEM’);
curl_setopt($ch,CURLOPT_SSLKEY, $keypath);
//post提交方式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
//运行curl
$data = curl_exec($ch);
//返回结果
/*
<xml>
<return_code><![CDATA[FAIL]]></return_code>
<return_msg><![CDATA[openid和appid不匹配]]></return_msg>
<result_code><![CDATA[FAIL]]></result_code>
<err_code><![CDATA[OPENID_ERROR]]></err_code>
<err_code_des><![CDATA[openid和appid不匹配]]></err_code_des>
<mch_billno><![CDATA[re2015120319085419755]]></mch_billno>
<mch_id>1290684001</mch_id>
<wxappid><![CDATA[wx378a45531255fc75]]></wxappid>
<re_openid><![CDATA[omyG8jqGAbLCTxEz-GaCWNOpdgDM]]></re_openid>
<total_amount>2000</total_amount>
</xml>
*/
if($data){
curl_close($ch);
$xmldata = json_decode(json_encode(simplexml_load_string($data, ‘SimplexmlElement’, LIBxml_NOCDATA)), true);
if($xmldata[“return_code”]==”SUCCESS” && $xmldata[“result_code”]==”SUCCESS”){
return array(“status”=>1,”err_code”=>$xmldata[“err_code”],”info”=>”红包发放成功”,”retxml”=>$data);
}else{
return array(“status”=>0,”err_code”=>$xmldata[“err_code”],”info”=>$xmldata[“return_msg”],”retxml”=>$data);
}
} else {
$error = curl_errno($ch);
curl_close($ch);
return array(“status”=>0,”err_code”=>”CURL_ERROR”,”info”=>”网络通信错误”,”data”=>”错误码$error”,”retxml”=>$data);
}
}
3.8 相关代码,正在整理,请添加微信号码itchuangyebuluo ,随时获得最新动态。
转载请注明:创事记 » 微信摇红包H5应用开发技术分享及源代码(80万微信红包实战)