前言

公司积分抽奖活动有红包奖项(虽然基本上都是这个奖),但是用户兑奖的过程十分麻烦: 需要先联系公司客服,然后加客服微信,加完之后客服发一个微信红包作为兑换。所以决定简化这个兑奖过程,能不能将这个兑奖的过程改为由用户自己兑换,不用联系公司客服也能兑奖。

初步想法

公司有微信公众号,可以通过微信公众号进行发红包操作,一方面可以实现发送红包的功能,另一方面也可以推广公司的微信公众号。初步想法是用户在微信公众号里输入一个兑换码,然后微信自动发送一个红包给用户,用户只要点一下红包,红包就进用户自己口袋了

业务流程

如果用户抽奖中了红包奖励,系统弹出一个提示框,里面有公司的微信公众号二维码图片(微信公众平台可以获取),及兑奖的兑换码,提示用户关注微信公众号,在公众号里面输入兑换码就可以获取红包

准备

1、先介绍几个平台

I.微信公众平台:是微信公众账号申请入口和管理后台。商户可以在公众平台提交基本资料、业务资料、财务资料申请开通微信支付功能。

平台入口:http://mp.weixin.qq.com。

II.微信商户平台:微信商户平台是微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能。

平台入口:http://pay.weixin.qq.com

III. 红包接口地址: https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3

2、 在微信公众平台获取公众号二维码图片(提供了多种尺寸大小),该二维码是提供给用户扫描, 及公众账号appid

3、 在微信商户平台上下载证书(账户中心 - 账户设置 - API安全 - API证书(下载zip压缩包格式即可,无须解压),获取 商户号(账户中心 - 账户设置 - 商户信息 - 基本账户信息 - 微信支付商户号),API密钥(账户中心 - 账户设置 - API安全 - API密钥 - 设置密钥(密钥为32位,需要自行重新设置,记录并保存好,实在没记住也可以更改) 这些参数

4、 开通红包功能,以及充值红包金额,红包金额与充值交易金额是区分开来的,所以需要单独充值,发放现金红包将扣除商户的可用余额,请注意,可用余额并不是微信支付交易额,需要预先充值,确保可用余额充足。查看可用余额、充值、提现请登录微信支付商户平台,进入“资金管理”菜单,进行操作

5、 介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
现金红包简介
微信红包,2014年春节一经推出即受到广大用户好评,引发全民抢红包热潮。现将微信红包打造成“现金红包”,成为一款定向资金发放的营销工具,供商户使用。

申请红包条件
1、T+0 结算商户需满足两个条件:1、入驻满90天,2、截止今日往回推30天内连续正常交易。
2、其余结算周期的商户无限制,可立即前往【商户平台】->【产品中心】申请开通。

发放方式介绍
商户发放现金红包有3种发放方式:
1)接口发放
商户根据文档”【商户平台】现金红包API文档V2“进行开发,一次调用可以给一个指定用户发送一个指定金额的红包,满足多元化的运营需求;
2)通过上传openid文件发放
收集要发送红包对象的openid,将openid编辑成txt文件,登录微信支付商户平台,使用上传文件功能发放。一份文件对应一个红包模板,便于管理;
为了防止商户手误重复操作发送红包,创建的同一个文件只能上传一次。若需要重复发放则需要修改文件名称或重新创建。
3)配置营销规则“满额送”发放
配置的规则不可使用红包模版进行发放,商户须在【产品中心】-【现金红包】-【前往功能】中创建红包后配置自助规则:用户使用微信支付发生交易满足一定条件,立送现金红包。

税务和发票问题
商户给用户发红包,微信支付按照商户指定红包金额扣除完全对等的充值资金,资金最终进入用户零钱。微信支付并未从中收取资金作为营收,所以不予开具发票。
发放现金红包请商户遵照国家法律依法纳税,在商户充值之前,我们默认商户已经合法上税,商户使用本功能的行为若涉及纳税或代扣代缴税款的义务,由商户自行承担该义务,我们不会替商户缴纳税款 。

程序实现

1、用户抽奖中了红包奖励, 生成一笔抽奖记录,同时生成一笔红包记录,所以需要新建一个红包记录表(表结构如下图),一开始生成的记录中红包状态是 0-未发放的状态,同时生成兑换码。

兑换码规则:

10位大写字母:3位固定字母开头 + 7位随机字母(大写字母是为了防止【数字0 与字母o O】【 1与字母l】混淆导致用户兑换不了奖, 3位固定字母是为了防止恶意用户无限次输入兑换码导致老是触发红包处理程序,如果不是以这个三个字母开头的文字,统一回复欢迎关注本微信公众号)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DROP TABLE IF EXISTS `ge_lottery_redpack_record`;
CREATE TABLE `ge_lottery_redpack_record` (
`id` varchar(32) NOT NULL,
`created_by` varchar(32) NOT NULL,
`created_date` datetime NOT NULL,
`last_modified_by` varchar(32) NOT NULL,
`last_modified_date` datetime NOT NULL,
`remarks` varchar(255) DEFAULT NULL,
`version` int(11) DEFAULT NULL,
`locked` bit(1) DEFAULT b'0',
`enable` bit(1) DEFAULT b'0',
`fd_lottery_record_id` varchar(32) DEFAULT NULL COMMENT '中奖纪录id 作为外键',
`fd_status` int(11) DEFAULT NULL COMMENT '红包状态 0:未发放 1:已发放待领取 2:发放失败 3:已领取 4:未领取已退款',
`fd_redpack_send_date` datetime DEFAULT NULL COMMENT '红包发送时间(非微信)',
`fd_redpack_order_id` varchar(32) DEFAULT NULL COMMENT '微信红包订单单号',
`fd_redpack_openid` varchar(32) DEFAULT NULL COMMENT '微信红包订单用户在wxappid下的openid',
`fd_redeem_code` varchar(32) DEFAULT NULL COMMENT '兑换码',
`fd_redpack_price` decimal(19,4) DEFAULT NULL COMMENT '红包金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖红包纪录表';

2、用户得到兑换码之后,就是在微信公众号里输入兑换码,当用户输入兑换码之后,用户信息会发到我们自己服务器后台中, 这里用户触发的是文本事件,我们需要的是三个参数

(1) 用户微信OpenID(用户在本微信公众号的唯一标识)

(2) 用户发的兑换码

(3) HttpServletRequest(用于获取用户ip,发红包接口入参需要)

1
2
3
4
5
6
7
8
9
10
11
// 文本事件
if (WechatBindUtil.MESSAGE_TEXT.equals(msgType)) {
String content = map.get("Content").trim();
// 如果输入的文本是是以RED开头的, 执行发红包操作
if (content.startsWith("RED")) {
String resultContent = lotteryRedpackRecordService.sendRedpackByRedeemCode(fromUserName,content,req);
message = WechatBindUtil.initText(toUserName, fromUserName, resultContent);
} else {
message = WechatBindUtil.initText(toUserName, fromUserName, "欢迎关注本微信公众号");
}
}

3、如果输入的文本是是以RED开头的, 执行发红包操作, 首先是查询红包记录表有没有该兑换码且红包状态为未发放,如果有的话调用微信发红包接口,同时更改红包状态,记录
红包发送时间,微信红包订单单号,微信红包订单用户在wxappid下的openid,没有的话给出提示,实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 根据兑换码发送微信红包
* @param openid
* @param redeemCode
* @return
*/
public String sendRedpackByRedeemCode (String openid, String redeemCode, HttpServletRequest request) {
Page<LotteryRedpackRecord> page = new Page<>(0);
page.setParams("fdRedeemCode",redeemCode);
List<LotteryRedpackRecord> redpackRecords = findAutoByPage(page);
if (redpackRecords.size() != 0) {
LotteryRedpackRecord oldRecord = redpackRecords.get(0);
// 调用微信红包查询接口, 先更新红包纪录状态
updateRedpackState(oldRecord.getId());

// 更新完成之后获取新的红包纪录
LotteryRedpackRecord record = findById(oldRecord.getId());
Integer fdStatus = record.getFdStatus();
if (fdStatus.equals(R.LotteryRedpackRecord.FdStatus.未发放.getIndex())) {
RedpackDTO redpackDTO = new RedpackDTO();
// 以红包纪录id作为 微信红包接口请求的商品订单号 截取28位
String mchBillNo = record.getId();
mchBillNo= mchBillNo.substring(0,28);
redpackDTO.setMchBillNo(mchBillNo);
// 指定哪一个微信用户
redpackDTO.setReOpenid(openid);
// 红包价格
redpackDTO.setTotalAmount(record.getFdRedpackPrice());
// 调用接口的机器Ip地址
String clientIp = WxUtils.getRemoteIp(request);
redpackDTO.setClientIp(clientIp);
Map<String,String> sendredpackResult = weixinPayService.sendredpack(redpackDTO);

// 更新红包纪录表
LotteryRedpackRecord lotteryRedpackRecord = findById(record.getId());
lotteryRedpackRecord.setFdRedpackOpenid(openid);
if (sendredpackResult.get("status").equals("success")) {
lotteryRedpackRecord.setFdStatus(R.LotteryRedpackRecord.FdStatus.已发放待领取.getIndex());
// 红包订单的微信单号-微信服务器上红包纪录的唯一标识
String redpackOrderId = sendredpackResult.get("sendListid");
lotteryRedpackRecord.setFdRedpackOrderId(redpackOrderId);
lotteryRedpackRecord.setFdRedpackSendDate(new Date());

// 更新抽奖纪录表, 变为已兑换
LotteryRecord lotteryRecord = lotteryRecordService.findById(record.getFdLotteryRecordId());
lotteryRecord.setFdStatus(R.LotteryRecordItem.FdStatus.已兑换.getIndex());
lotteryRecordService.saveSelective(lotteryRecord);
saveSelective(lotteryRedpackRecord);
} else {
lotteryRedpackRecord.setFdStatus(R.LotteryRedpackRecord.FdStatus.发放失败.getIndex());
lotteryRedpackRecord.setRemarks(sendredpackResult.get("message"));
saveSelective(lotteryRedpackRecord);
return "红包发送失败, 请及时联系对应的客服!";
}
} else if (fdStatus.equals(R.LotteryRedpackRecord.FdStatus.已发放待领取.getIndex())) {
return "该兑换码对应的红包已发送! 请注意查收";
} else if (fdStatus.equals(R.LotteryRedpackRecord.FdStatus.发放失败.getIndex())) {
return "红包发送失败, 请及时联系对应的客服!";
} else if (fdStatus.equals(R.LotteryRedpackRecord.FdStatus.已领取.getIndex())) {
return "该兑换码对应的红包已领取";
} else {
return "该兑换码对应的红包已过时, 请及时联系对应的客服";
}
} else {
return "该兑换码无效, 请输入正确的兑换码!";
}
return "红包已发送请注意查收! 注: 24小时后未领取该红包失效";
}

4、调用微信红包接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* 微信公众号发红包接口
* @param redpackDTO
* @return
*/
public Map<String,String> sendredpack(RedpackDTO redpackDTO) {
Map<String,String> resultMap = Maps.newHashMap();
Map<String,String> wxResultMap = Maps.newHashMap();
// 请求参数
Map<String, String> reqData = Maps.newHashMap();
logger.info("--------------------->>> 开始发送红包start", redpackDTO);
try {
// 随机字符串
reqData.put("nonce_str", WXPayUtil.generateNonceStr());
// 商户订单号
reqData.put("mch_billno",redpackDTO.getMchBillNo());
// 商户号
reqData.put("mch_id", WxPcPayConfigImpl.MCH_ID);
// 公众账号appid
reqData.put("wxappid", WxPcPayConfigImpl.SENDREDPACK_WX_APPID);
// 商户名称
reqData.put("send_name", WxPcPayConfigImpl.SENDREDPACK_SEND_NAME);
// 用户openid
reqData.put("re_openid",redpackDTO.getReOpenid());
// 付款金额
reqData.put("total_amount", AmountUtils.transAmountToCent(redpackDTO.getTotalAmount()));
// 红包发放总人数
reqData.put("total_num", WxPcPayConfigImpl.SENDREDPACK_TOTAL_NUM);
// 红包祝福语
reqData.put("wishing", WxPcPayConfigImpl.SENDREDPACK_WISHING);
// Ip地址
reqData.put("client_ip", redpackDTO.getClientIp());
// 活动名称
reqData.put("act_name", WxPcPayConfigImpl.SENDREDPACK_ACT_NAME);
//备注
reqData.put("remark", WxPcPayConfigImpl.SENDREDPACK_REMARK);
// 生成签名
reqData.put("sign", WXPayUtil.generateSignature(reqData, WxPcPayConfigImpl.API_KEY, WXPayConstants.SignType.MD5 ));
String respXml = wxpay.requestWithCert(WxPcPayConfigImpl.SENDREDPACK_URL,reqData , 10000, 10000);
wxResultMap = WXPayUtil.xmlToMap(respXml);
} catch (Exception e) {
e.printStackTrace();
logger.info("--------------------->>> 微信公众号发送红包异常");
resultMap.put("message", e.getMessage());
resultMap.put("status", "failed");
}
// 通信标识结果
String returnCode = wxResultMap.get("return_code");
if ("SUCCESS".equals(returnCode)) {
String resultCode = wxResultMap.get("result_code");
// 业务处理结果
if ("SUCCESS".equals(resultCode)) {
logger.info("--------------------->>> 微信公众号发送红包成功");
// 红包订单的微信单号
String sendListid = wxResultMap.get("send_listid");
resultMap.put("sendListid",sendListid);
resultMap.put("message","操作成功");
resultMap.put("status", "success");
} else {
logger.info("--------------------->>> 微信公众号发送红包失败, 原因: " + wxResultMap.get("err_code_des"));
String errCodeDes = wxResultMap.get("err_code_des");
resultMap.put("message",errCodeDes);
resultMap.put("status", "failed");
return resultMap;
}
} else {
String returnMsg = wxResultMap.get("return_msg");
logger.info("--------------------->>> 微信公众号发送红包失败, 原因: " + returnMsg);
resultMap.put("message", returnMsg);
resultMap.put("status", "failed");
return resultMap;
}
return resultMap;
}

总结及注意事项

1.红包是以分为单位,必须大于100分,小于20000分之间,这个很重要,不要一不小心把公司的钱都转出去了

2.需要对请求参数进行加签操作,wxpay里面封装了方法

3.现金红包接口请求是需要证书的,因为是出账,不像充值是属于进账不用证书, 需要调用requestWithCert 这个请求接口

1
2
3
4
5
6
7
8
9
10
11
/**
* 需要证书的请求
* @param strUrl String
* @param reqData 向wxpay post的请求数据 Map
* @param connectTimeoutMs 超时时间,单位是毫秒
* @param readTimeoutMs 超时时间,单位是毫秒
* @return API返回数据
* @throws Exception
*/
public String requestWithCert(String strUrl, Map<String, String> reqData,
int connectTimeoutMs, int readTimeoutMs) throws Exception {

4.由于红包发出去了不知道用户有没有领取,所以可以用一个定时任务或者一个按钮调用微信红包状态查询接口,以更新红包的最新状态

5.可以借助第三方平台,如摇摇啦应用平台,借助这些平台可以不用开发接口,只要配置好参数就可以使用了,但唯一的缺点是要钱