应用场景:
网站某一类型注册类型会员通过微信公众账号进入其账号中心时,可以获取一次领取红包资格,红包资格在某一个特定范围内。
微信红包调用流程:
后台API调用:待进入联调过程时与开发进行详细沟通;
告知服务器:告知服务器接收微信红包的用户openID,告知服务器该用户获得的金额;
从商务号扣款:服务器获取信息后从对应的商务号扣取对应的金额;
调用失败:因不符合发送规则,商务号余额不足等原因造成调用失败,反馈至调用方;
发送成功:以微信红包公众账号发送对应红包至对应用户;
实现过程:
1、用户打开抢红包页面,通过微信提供的 网页授权获取用户基本信息 不弹出授权页面方式获取用户的openid;
2、根据获取到的openid判断用户是否已经领取过红包
① openid已经领取过红包,跳转到已经领取页面。
② openid没有领取过红包,到3。
3、根据用户输入的手机判断该手机是否已经领取过红包
① 手机号已经领取过红包,跳转到已经领取页面。
② 手机号没有领取过红包,到4。
4、通过微信提供的接口与微信服务器交互,根据微信服务器的返回结果释放锁定的金额。
5、用户到微信公众号领取红包。
为了防止用户篡改openid,建议将openid放在session里面,而不是在用户提交手机号码的时候作为参数传递给后台。
数据库方面涉及两个表,一个表(hongbao)用来记录每个用户领取过的红包详情,一个表(c_hongbao)用来保存本次活动发放的红包总金额,已经发放的红包金额和剩余的红包金额。当对hongbao表操作时,通过触发器修改c_hongbao的值。
hongbao表
- public class HongBao implements Serializable{
- private static final long serialVersionUID = 1L;
- private Long hongBaoId;
- private Date addTime; //红包发送时间
- private Integer amount; //金额 单位分
- private String billNo; //订单号
- private String openid; //用户
- private String remark; //微信返回的内容
- private String mobile; //用户输入的手机号
- private Integer result; //发送结果
- }
- public class Balance implements Serializable {
- private Long cHongbaoId;
- private Integer total; //红包总金额 单位分
- private Integer balance; //红包余额 单位分
- private Integer send; //已经发送红包金额 单位分
- }
- DELIMITER $$
- CREATE
- TRIGGER insert_hongbao AFTER INSERT
- ON hongbao
- FOR EACH ROW BEGIN
- update c_hongbao set balance = balance - new.amount , send = send + new.amount ;
- END$$
- DELIMITER ;
- DELIMITER $$
- CREATE
- TRIGGER update_hongbao AFTER UPDATE
- ON hongbao
- FOR EACH ROW BEGIN
- if new.result = 0 then
- update c_hongbao set balance = balance + old.amount , send = send - old.amount ;
- end if;
- END$$
- DELIMITER ;
Dao层接口
- public interface LuckyMoneyMapper {
- /**
- *
- * <p>Description:是否存在成功发送红包的记录 </p>
- * @author userwyh
- * @param params
- * @return
- */
- Integer existSendRecord(Map<String,Object> params);
- /**
- *
- * <p>Description: 查询商户红包余额</p>
- * @author userwyh
- * @return
- */
- Balance selectCHongbao();
- }
- public interface HongBaoMapper {
- int insertSelective(HongBao record);
- int updateByPrimaryKeySelective(HongBao record);
- }
Dao层实现
- <?xml version="1.0" encoding="UTF-8" ?>
- <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
- <mapper namespace="****.mapper.LuckyMoneyMapper" >
- <!-- 是否已经领取过红包 -->
- <select id="existSendRecord" parameterType="java.util.Map" resultType="Integer">
- <if test="openid != null" >
- select count(*) from cm_hongbao where openid = #{openid} and result = 1
- </if>
- <if test="mobile != null" >
- select count(*) from cm_hongbao where mobile = #{mobile} and result = 1
- </if>
- </select>
- <!-- 查询商户红包余额 -->
- <select id="selectCHongbao" resultType="com.caimei.hehe.po.Balance">
- select * from c_hongbao
- </select>
- </mapper>
Service层接口
- public interface LuckyMoneyService {
- /**
- *
- * <p>Description: 根据用户填写的手机号发送红包</p>
- * @author userwyh
- * @param mobile
- * @param openid
- * @throws Exception
- */
- boolean sendLuckyMoney(String openid,String mobile) throws Exception;
- /**
- *
- * <p>Description: 是否已经领取过红包</p>
- * @author userwyh
- * @param params
- * @return
- */
- boolean existSendRecord(Map<String,Object> params);
- }
- @Transactional
- @Service(value="luckyMoneyService")
- public class LuckyMoneyServiceImpl implements LuckyMoneyService {
- @Autowired
- private LuckyMoneyMapper luckyMoneyMapper;
- @Autowired
- private HongBaoMapper hongBaoMapper;
- @Override
- public boolean sendLuckyMoney(String openid,String mobile) throws Exception {
- //处理一些发送红包前的业务...
- // TODO
- //发送微信红包
- return sendWechatLuckyMoney(openid,mobile);
- }
- private boolean sendWechatLuckyMoney(String openid,String mobile) throws Exception {
- String billNo = HongBaoUtil.createBillNo(mobile.substring(1,11));
- HongBao hongbao = getAmount(openid,billNo,mobile);
- //默认发送结果失败
- hongbao.setResult(HongBaoUtil.FAIL);
- int amount = hongbao.getAmount();
- SortedMap<String, String> sortMap = HongBaoUtil.createMap(billNo, openid ,amount);
- HongBaoUtil.sign(sortMap);
- String requestXML = HongBaoUtil.getRequestXml(sortMap);
- SystemLogger.info("["+mobile+"--"+mobile+"]报名领取红包,领红包提交微信的请求:"+requestXML);
- try {
- HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
- InputStream instream = request.getSession().getServletContext().getResourceAsStream("/WEB-INF/weixinCert/apiclient_cert.p12");
- String responseXML = HongBaoUtil.post(requestXML,instream);
- SystemLogger.info("["+mobile+"--"+mobile+"]报名</span><span style="font-size:12px;">领取红包</span><span style="font-size:12px;">,微信发送红包处理结果:"+responseXML);
- Map<String,String> resultMap = XmlUtil.parseXml(responseXML);
- String return_code = resultMap.get("return_code").toString();
- if("SUCCESS".equals(return_code)){
- hongbao.setResult(HongBaoUtil.SUCCESS);
- }
- hongbao.setRemark(responseXML);
- } catch (KeyManagementException e) {
- e.printStackTrace();
- } catch (UnrecoverableKeyException e) {
- e.printStackTrace();
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- } catch (CertificateException e) {
- e.printStackTrace();
- } catch (KeyStoreException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- } finally{
- //不管发送成功不成功,都要更新红包发送记录,失败的商户余额回滚,成功的标志记录为成功
- hongBaoMapper.updateByPrimaryKeySelective(hongbao);
- }
- //如果微信发送失败抛出异常的话,会导致收集到的用户信息回滚,此处为了保留用户信息
- return hongbao.getResult().equals(HongBaoUtil.SUCCESS) ? true :false;
- }
- /**
- *
- * <p>Description: 获取红包金额</p>
- * @author userwyh
- * @param openid
- * @param billNo
- * @param mobile
- * @return
- */
- private synchronized HongBao getAmount(String openid,String billNo,String mobile){
- //该用户获取的随机红包金额
- int amount = (int) Math.round(Math.random()*(1000-500)+500);
- //商户的红包总余额
- Balance balance = luckyMoneyMapper.selectCHongbao();
- //如果此次随机金额比商户红包余额还要大,则返回商户红包余额
- if(amount > balance.getBalance()){
- amount = balance.getBalance();
- }
- HongBao hongBao = new HongBao();
- hongBao.setAddTime(new Date());
- hongBao.setAmount(amount);
- hongBao.setOpenid(openid);
- hongBao.setResult(HongBaoUtil.LOCK);
- hongBao.setBillNo(billNo);
- hongBao.setMobile(mobile);
- //先锁定用户领取的金额,防止领取金额超过预算金额
- hongBaoMapper.insertSelective(hongBao);
- return hongBao;
- }
- @Override
- public boolean existSendRecord(Map<String,Object> params) {
- return luckyMoneyMapper.existSendRecord(params) > 0;
- }
- }
HongBaoUtil类
- public class HongBaoUtil {
- public static final String MCH_ID = "*****"; //商户号
- public static final String WXAPPID = ************; //公众账号appid
- public static final String NICK_NAME = "*********"; //提供方名称
- public static final String SEND_NAME = "********"; //商户名称
- public static final int HONGBAO_MIN_VALUE = 500; //红包最小金额 单位:分
- public static final int HONGBAO_MAX_VALUE = 1000; //红包最大金额 单位:分
- public static final int TOTAL_NUM = 1; //红包发放人数
- public static final String WISHING = "********************"; //红包祝福语
- public static final String CLIENT_IP = "127.0.0.1"; //调用接口的机器IP
- public static final String ACT_NAME = "****"; //活动名称
- public static final String REMARK = "****"; //备注
- public static final String KEY = "**************************"; //秘钥
- public static final int FAIL = 0; //领取失败
- public static final int SUCCESS = 1; //领取成功
- public static final int LOCK = 2; //已在余额表中锁定该用户的余额,防止领取的红包金额大于预算
- /**
- * 对请求参数名ASCII码从小到大排序后签名
- * @param openid
- * @param userId
- * @return
- */
- public static void sign(SortedMap<String, String> params){
- Set<Entry<String,String>> entrys=params.entrySet();
- Iterator<Entry<String,String>> it=entrys.iterator();
- String result = "";
- while(it.hasNext())
- {
- Entry<String,String> entry=it.next();
- result +=entry.getKey()+"="+entry.getValue()+"&";
- }
- result +="key="+KEY;
- String sign = null;
- try {
- sign = MD5Util.MD5(result);
- } catch (Exception e) {
- e.printStackTrace();
- }
- params.put("sign", sign);
- }
- public static String getRequestXml(SortedMap<String,String> params){
- StringBuffer sb = new StringBuffer();
- sb.append("<xml>");
- Set es = params.entrySet();
- Iterator it = es.iterator();
- while(it.hasNext()) {
- Map.Entry entry = (Map.Entry)it.next();
- String k = (String)entry.getKey();
- String v = (String)entry.getValue();
- if ("nick_name".equalsIgnoreCase(k)||"send_name".equalsIgnoreCase(k)||"wishing".equalsIgnoreCase(k)||"act_name".equalsIgnoreCase(k)||"remark".equalsIgnoreCase(k)||"sign".equalsIgnoreCase(k)) {
- sb.append("<"+k+">"+"<![CDATA["+v+"]]></"+k+">");
- }else {
- sb.append("<"+k+">"+v+"</"+k+">");
- }
- }
- sb.append("</xml>");
- return sb.toString();
- }
- public static SortedMap<String, String> createMap(String billNo,String openid,int amount){
- SortedMap<String, String> params = new TreeMap<String, String>();
- params.put("wxappid",WXAPPID);
- params.put("nonce_str",createNonceStr());
- params.put("mch_billno",billNo);
- params.put("mch_id", MCH_ID);
- params.put("nick_name", NICK_NAME);
- params.put("send_name", SEND_NAME);
- params.put("re_openid", openid);
- params.put("total_amount", amount+"");
- params.put("min_value", amount+"");
- params.put("max_value", amount+"");
- params.put("total_num", TOTAL_NUM+"");
- params.put("wishing", WISHING);
- params.put("client_ip", CLIENT_IP);
- params.put("act_name", ACT_NAME);
- params.put("remark", REMARK);
- return params;
- }
- /**
- * 生成随机字符串
- * @return
- */
- public static String createNonceStr() {
- return UUID.randomUUID().toString().toUpperCase().replace("-", "");
- }
- /**
- * 生成商户订单号
- * @param mch_id 商户号
- * @param userId 该用户的userID
- * @return
- */
- public static String createBillNo(String userId){
- //组成: mch_id+yyyymmdd+10位一天内不能重复的数字
- //10位一天内不能重复的数字实现方法如下:
- //因为每个用户绑定了userId,他们的userId不同,加上随机生成的(10-length(userId))可保证这10位数字不一样
- Date dt=new Date();
- SimpleDateFormat df = new SimpleDateFormat("yyyymmdd");
- String nowTime= df.format(dt);
- int length = 10 - userId.length();
- return MCH_ID + nowTime + userId + getRandomNum(length);
- }
- /**
- * 生成特定位数的随机数字
- * @param length
- * @return
- */
- public static String getRandomNum(int length) {
- String val = "";
- Random random = new Random();
- for (int i = 0; i < length; i++) {
- val += String.valueOf(random.nextInt(10));
- }
- return val;
- }
- public static String post(String requestXML,InputStream instream) throws NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, UnrecoverableKeyException, KeyStoreException{
- KeyStore keyStore = KeyStore.getInstance("PKCS12");
- try {
- keyStore.load(instream, MCH_ID.toCharArray());
- } finally {
- instream.close();
- }
- SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, MCH_ID.toCharArray()).build();
- SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
- sslcontext,
- new String[] { "TLSv1" },
- null,
- SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
- CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
- String result = "";
- try {
- HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack");
- StringEntity reqEntity = new StringEntity(requestXML,"UTF-8");
- // 设置类型
- reqEntity.setContentType("application/x-www-form-urlencoded");
- httpPost.setEntity(reqEntity);
- CloseableHttpResponse response = httpclient.execute(httpPost);
- try {
- HttpEntity entity = response.getEntity();
- if (entity != null) {
- BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(),"UTF-8"));
- String text;
- while ((text = bufferedReader.readLine()) != null) {
- result +=text;
- System.out.println(text);
- }
- }
- EntityUtils.consume(entity);
- } finally {
- response.close();
- }
- } finally {
- httpclient.close();
- }
- return result;
- }
- }
授权回调的Controller
- @Controller
- @RequestMapping("/wechat/")
- public class OauthController extends BaseController {
- @RequestMapping("oauth")
- public String oauth(@RequestParam("code") String code,@RequestParam("state") String state) throws Exception{
- try {
- //没有code,非法链接进来的
- if(StringUtils.isBlank(code)){
- return "error";
- }
- String url = WeiXinUtil.getUrl().replace("CODE", code);
- Map<String, Object> map = JsonUtil.getMapByUrl(url);
- if(map.get("openid") != null){
- if(ShareState.LUCKYMONEY.getDesc().equals(state)){
- SessionHelper.setAttribute(WeiXinUtil.OPENID_KEY, map.get("openid").toString());
- return "redirect:/luckymoney/signup.shtml";
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return "error";
- }
- }
接受请求的Controller
- @Controller
- @RequestMapping("/luckymoney/")
- public class LuckyMoneyController extends BaseController{
- @Resource
- private LuckyMoneyService luckyMoneyService;
- /**
- *
- * <p>Description: 去报名领取红包</p>
- * @author userwyh
- * @date 2015年12月28日 下午3:54:23
- * @return
- * @throws Exception
- */
- @RequestMapping("signup")
- public String signup() throws Exception{
- Object openid = SessionHelper.getAttribute(WeiXinUtil.OPENID_KEY);
- if(openid == null){
- //没有获取到openid 重新授权进入
- String reOauth = WeiXinUtil.getOauthLinkURL(ShareState.LUCKYMONEY.getDesc());
- return "redirect:"+reOauth;
- }
- Map<String,Object> params = new HashMap<String,Object>();
- params.put("openid", openid);
- //是否已经发送过红包记录
- boolean existRecord = luckyMoneyService.existSendRecord(params);
- getRequest().setAttribute("existRecord", existRecord);
- getRequest().setAttribute("openid", openid);
- return "activity/signup";
- }
- /**
- * <p>Description: 发送红包</p>
- * <p>code = 0 本次领取成功</p>
- * <p>code = 1 输入内容有误</p>
- * <p>code = 2 系统错误</p>
- * <p>code = 3 已经领取过红包</p>
- * @author userwyh
- * @date 2015年12月28日 下午3:53:24
- * @param mobile 注册手机号
- * @return
- * @throws Exception
- */
- @ResponseBody
- @RequestMapping("init")
- public Map<String, Object> initHeHe(String mobile) throws Exception{
- Map<String, Object> result = new HashMap<>();
- int code = 0;
- if(StringUtils.isBlank(mobile)){
- result.put("mobileerr", "请填写您的手机号");
- code = 1;
- }else if(!Pattern.compile("^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$").matcher(mobile).find()){
- result.put("mobileerr", "手机号格式不正确");
- code = 1;
- }
- if(code == 0){
- Object openid = SessionHelper.getAttribute(WeiXinUtil.OPENID_KEY);
- if(openid == null){
- result.put("syserr", "用户信息丢失,请重新进入");
- code = 2;
- }else{
- Map<String,Object> params = new HashMap<String,Object>();
- params.put("mobile", mobile);
- boolean existRecord = luckyMoneyService.existSendRecord(params);
- if(!existRecord){
- try {
- boolean sendResult = luckyMoneyService.sendLuckyMoney((String)openid, mobile);
- if(!sendResult){
- result.put("wechaterr", "通过微信发送红包失败");
- code = 2;
- }
- } catch (Exception e) {
- result.put("syserr", e.getMessage());
- code = 2;
- }
- }else{
- code = 3;
- }
- }
- }
- result.put("code", code);
- return result;
- }
- }
测试结果:
随机在100-200分之间生成的一个随机数,写了几百行代码换来的红包