微信公众平台现金红包开发源码示例

2017年09月13日 08:35 | 3796次浏览

本文介绍的是基于Java语言开发微信现金红包的例子。由于个人表达能力和编程能力有限,请多多包涵。本文仅介绍拥有微信支付权限的微信公众号开发。

本文分为以下两部分:

1.开发现金红包SDK

2.构造现金红包并发送

首先看一下现金红包接口文档:http://pay.weixin.qq.com/wiki/doc/api/cash_coupon.php?chapter=13_1


首页现金红包

功能介绍

春节期间,微信红包以其独特的魅力,优秀的用户体验和安全的支付环境,一经推出即受到了广大用户的热烈欢迎,现微信支付现金红包向微信支付商户开发,具体能力如下: 

1、商户调用接口时,通过指定发送对象以及发送金额的方式发放红包,这样的方式,允许商户灵活的应用于各种各样丰富的活动场景

2、领取到红包后,用户的资金直接进入微信零钱,避免繁复的领奖流程,带给用户微信支付原生的流畅体验


微信红包发送规则1. 发送频率规则

 ◆ 每分钟发送红包数量不得超过1800个;

 ◆ 北京时间0:00-8:00不触发红包赠送;(如果以上规则不满足您的需求,请发邮件至wxhongbao@tencent.com获取升级指引)

2. 红包规则

 ◆ 单个红包金额介于[1.00元,200.00元]之间;

 ◆ 同一个红包只能发送给一个用户;(如果以上规则不满足您的需求,请发邮件至wxhongbao@tencent.com获取升级指引)


商户侧调用红包接口流程1. 登录微信支付商户平台下载证书以及充值

在调用接口前,请商户使用微信支付商户号登录微信支付商户平台完成下述工作:

备注:

微信支付商户平台地址为pay.weixin.qq.com。微信支付商户号会在商户申请微信支付成功后,通过开户邮件发送给您。请不要使用微信公众平台账号或者appid登录。如果您登录时遇到问题,请联系微信支付小助手weixinpay@tencent.com

 ◆ 下载证书

商户调用微信红包接口时,服务器会进行证书验证,请在商户平台下载证书

◆ 充值

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

2. 微信红包接口调用流程

 ◆ 后台API调用:待进入联调过程时与开发进行详细沟通;

 ◆ 告知服务器:告知服务器接收微信红包的用户openID,告知服务器该用户获得的金额;

 ◆ 从商务号扣款:服务器获取信息后从对应的商务号扣取对应的金额;

 ◆ 调用失败:因不符合发送规则,商务号余额不足等原因造成调用失败,反馈至调用方;

 ◆ 发送成功:以微信红包公众账号发送对应红包至对应用户;

用户交互流程

调用现金红包接口,发放成功后,用户领取红包流程如下:

步骤(一):收到领取红包消息,根据用户微信版本不同,分为:

微信版本在6.1及以上的用户收到企业自身微信号(调用接口时传入appid对应的商户号)下发领取消息;如果用户未关注微信号,那么会收到由“服务通知”下发的消息

微信版本在6.1以下的用户仍按原流程收取消息:由微信红包公众号下发领取消息

步骤(二):点击领取消息,拆红包

src="http://pay.weixin.qq.com/wiki/doc/api/img/chapter13_5.png" "width="261" height="464" style="border: 0px none; vertical-align: top;">

接口列表

业务接口简介
现金红包发放红包

用于企业向微信用户个人发现金红包,目前支持向指定微信用户的openid发放指定金额红包。(获取openid参见微信公众平台开发者文档:  网页授权获取用户基本信息

接口详细说明

1.红包发放说明

用于企业向微信用户个人发现金红包

目前支持向指定微信用户的openid发放指定金额红包。(获取openid参见微信公众平台开发者文档: 网页授权获取用户基本信息)

接口参数与用户领用实际效果对应关系如下:

如需操作请登录https://pay.weixin.qq.com/

 

2.接口调用请求说明

请求Urlhttps://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack
是否需要证书是(证书及使用说明详见商户证书
请求方式POST

3.请求参数,返回参数,错误码,成功返回示例xml,失败返回示例xml请参考https://pay.weixin.qq.com/helper/cashredopenapi_V2.pdf

一、开发现金红包SDK

1.创建一个红包,本文介绍的是创建一个固定金额的红包。

2.接口调用说明

请求Urlhttps://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack
是否需要证书是(证书及使用说明详见商户证书
请求方式POST

证书是必须的,上面的现金红包接口文档里面有介绍。

点击这个链接:http://pay.weixin.qq.com/wiki/doc/api/cash_coupon.php?chapter=13_3

这一节有介绍的。

3.请求参数。注意看下,有的参数是必填的。

二、构造现金红包并发送

构造现金红包实体类

public class SendRedPackPo {
private String nonce_str;
private String sign;
private String mch_billno;
private String mch_id;
private String sub_mch_id;
private String wxappid;
private String nick_name;
private String send_name;
private String re_openid;
private String total_amount;
private String min_value;
private String max_value;
private String total_num;
private String wishing;
private String client_ip;
private String act_name;
private String remark;
private String logo_imgurl;
private String share_content;
private String share_url;
private String share_imgurl;

public String getNonce_str() {
return nonce_str;
}
public void setNonce_str(String nonceStr) {
nonce_str = nonceStr;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public String getMch_billno() {
return mch_billno;
}
public void setMch_billno(String mchBillno) {
mch_billno = mchBillno;
}
public String getMch_id() {
return mch_id;
}
public void setMch_id(String mchId) {
mch_id = mchId;
}
public String getSub_mch_id() {
return sub_mch_id;
}
public void setSub_mch_id(String subMchId) {
sub_mch_id = subMchId;
}
public String getWxappid() {
return wxappid;
}
public void setWxappid(String wxappid) {
this.wxappid = wxappid;
}
public String getNick_name() {
return nick_name;
}
public void setNick_name(String nickName) {
nick_name = nickName;
}
public String getSend_name() {
return send_name;
}
public void setSend_name(String sendName) {
send_name = sendName;
}
public String getRe_openid() {
return re_openid;
}
public void setRe_openid(String reOpenid) {
re_openid = reOpenid;
}
public String getTotal_amount() {
return total_amount;
}
public void setTotal_amount(String totalAmount) {
total_amount = totalAmount;
}
public String getMin_value() {
return min_value;
}
public void setMin_value(String minValue) {
min_value = minValue;
}
public String getMax_value() {
return max_value;
}
public void setMax_value(String maxValue) {
max_value = maxValue;
}
public String getTotal_num() {
return total_num;
}
public void setTotal_num(String totalNum) {
total_num = totalNum;
}
public String getWishing() {
return wishing;
}
public void setWishing(String wishing) {
this.wishing = wishing;
}
public String getClient_ip() {
return client_ip;
}
public void setClient_ip(String clientIp) {
client_ip = clientIp;
}
public String getAct_name() {
return act_name;
}
public void setAct_name(String actName) {
act_name = actName;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getLogo_imgurl() {
return logo_imgurl;
}
public void setLogo_imgurl(String logoImgurl) {
logo_imgurl = logoImgurl;
}
public String getShare_content() {
return share_content;
}
public void setShare_content(String shareContent) {
share_content = shareContent;
}
public String getShare_url() {
return share_url;
}
public void setShare_url(String shareUrl) {
share_url = shareUrl;
}
public String getShare_imgurl() {
return share_imgurl;
}
public void setShare_imgurl(String shareImgurl) {
share_imgurl = shareImgurl;
}

}

生成32位字符串工具类

/**
 * 生成32位字符串
 * */
public class UUIDHexGenerator {


private String sep = "";


private static final int IP;


private static short counter = (short) 0;


private static final int JVM = (int) (System.currentTimeMillis() >>> 8);


private static UUIDHexGenerator uuidgen = new UUIDHexGenerator();


static {
int ipadd;
try {
ipadd = toInt(InetAddress.getLocalHost().getAddress());
} catch (Exception e) {
ipadd = 0;
}
IP = ipadd;
}


public static UUIDHexGenerator getInstance() {
return uuidgen;
}


public static int toInt(byte[] bytes) {
int result = 0;
for (int i = 0; i < 4; i++) {
result = (result << 8) - Byte.MIN_VALUE + (int) bytes[i];
}
return result;
}


protected String format(int intval) {
String formatted = Integer.toHexString(intval);
StringBuffer buf = new StringBuffer("00000000");
buf.replace(8 - formatted.length(), 8, formatted);
return buf.toString();
}


protected String format(short shortval) {
String formatted = Integer.toHexString(shortval);
StringBuffer buf = new StringBuffer("0000");
buf.replace(4 - formatted.length(), 4, formatted);
return buf.toString();
}


protected int getJVM() {
return JVM;
}


protected synchronized short getCount() {
if (counter < 0) {
counter = 0;
}
return counter++;
}


protected int getIP() {
return IP;
}


protected short getHiTime() {
return (short) (System.currentTimeMillis() >>> 32);
}


protected int getLoTime() {
return (int) System.currentTimeMillis();
}


public String generate() {
return new StringBuffer(36)
.append(format(getIP()))
.append(sep)
.append(format(getJVM()))
.append(sep)
.append(format(getHiTime()))
.append(sep)
.append(format(getLoTime()))
.append(sep)
.append(format(getCount()))
.toString();
}
/**
* @param args
*/
public static void main(String[] args) {
String id;
UUIDHexGenerator uuid = UUIDHexGenerator.getInstance();
 
for(int i = 0;i<100;i++)
{
id = uuid.generate();
 
}

}
}

MD5工具类

/** 
* 功能:MD5签名处理核心文件,不需要修改
* 说明:
* 以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要,按照技术文档编写,并非一定要使用该代码。
* */
public class MD5 {

    /**
     * 签名字符串
     * @param text 需要签名的字符串
     * @param key 密钥
     * @param input_charset 编码格式
     * @return 签名结果
     */
    public static String sign(String text, String key, String input_charset) {
    text = text + key;
        return DigestUtils.md5Hex(getContentBytes(text, input_charset));
    }
    
    /**
     * 签名字符串
     * @param text 需要签名的字符串
     * @param sign 签名结果
     * @param key 密钥
     * @param input_charset 编码格式
     * @return 签名结果
     */
    public static boolean verify(String text, String sign, String key, String input_charset) {
    text = text + key;
    String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset));
    if(mysign.equals(sign)) {
    return true;
    }
    else {
    return false;
    }
    }


    /**
     * @param content
     * @param charset
     * @return
     * @throws SignatureException
     * @throws UnsupportedEncodingException 
     */
    private static byte[] getContentBytes(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return content.getBytes();
        }
        try {
            return content.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
        }
    }


}
     
      /**
* 生成6位或10位随机数
* param codeLength(多少位)
* @return
*/private String createCode(int codeLength) {
String code="";
for(int i=0; i<codeLength; i++) {
code += (int)(Math.random() * 9);
}
return code;
}
private static boolean isValidChar(char ch) {  
        if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z')|| (ch >= 'a' && ch <= 'z'))  
            return true;  
        if ((ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f))  
            return true;// 简体中文汉字编码  
        return false;  
    }


/** 
     * 除去数组中的空值和签名参数
     * @param sArray 签名参数组
     * @return 去掉空值与签名参数后的新签名参数组
     */
    public static Map<String, String> paraFilter(Map<String, String> sArray) {


        Map<String, String> result = new HashMap<String, String>();


        if (sArray == null || sArray.size() <= 0) {
            return result;
        }


        for (String key : sArray.keySet()) {
            String value = sArray.get(key);
            if (value == null || value.equals("") || key.equalsIgnoreCase("sign")
                || key.equalsIgnoreCase("sign_type")) {
                continue;
            }
            result.put(key, value);
        }


        return result;
    }


    /** 
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     */
    public static String createLinkString(Map<String, String> params) {


        List<String> keys = new ArrayList<String>(params.keySet());
        Collections.sort(keys);


        String prestr = "";


        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);


            if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }
        }


        return prestr;
    }

/**
* 发送现金红包
* @throws KeyStoreException 
* @throws IOException 
* @throws CertificateException 
* @throws NoSuchAlgorithmException 
* @throws UnrecoverableKeyException 
* @throws KeyManagementException 
* @throws DocumentException 
*/
public void sendRedPack() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, UnrecoverableKeyException, DocumentException {
// 获取uuid作为随机字符串
String nonceStr = uuidGenerator.generate();
String today = new SimpleDateFormat("yyyyMMdd").format(new Date());
String code = createCode(10);
String mch_id = "10000100";//商户号
String appid = "wxd930ea5d5a258f4f";
String opendid = "xxxxxxxxxxxxxxxxx"; //发送给指定微信用户的openid
SendRedPackPo sendRedPackPo = new SendRedPackPo();
String totalAmount = "1";

sendRedPackPo.setNonce_str(nonceStr);
sendRedPackPo.setMch_billno(mch_id + today + code);
sendRedPackPo.setMch_id(mch_id);
sendRedPackPo.setWxappid(appid);
sendRedPackPo.setNick_name("xxx");
sendRedPackPo.setSend_name("xxx");
sendRedPackPo.setRe_openid(opendid);
sendRedPackPo.setTotal_amount(totalAmount);
sendRedPackPo.setMin_value(totalAmount);
sendRedPackPo.setMax_value(totalAmount);
sendRedPackPo.setTotal_num("1");
sendRedPackPo.setWishing("祝您新年快乐!");
sendRedPackPo.setClient_ip("192.168.1.1"); //IP
sendRedPackPo.setAct_name("小游戏");
sendRedPackPo.setRemark("快来抢红包!");


//把请求参数打包成数组
Map<String, String> sParaTemp = new HashMap<String, String>();
sParaTemp.put("nonce_str", sendRedPackPo.getNonce_str());
        sParaTemp.put("mch_billno", sendRedPackPo.getMch_billno());
        sParaTemp.put("mch_id", sendRedPackPo.getMch_id());
sParaTemp.put("wxappid", sendRedPackPo.getWxappid());
sParaTemp.put("nick_name", sendRedPackPo.getNick_name());
sParaTemp.put("send_name", sendRedPackPo.getSend_name());
sParaTemp.put("re_openid", sendRedPackPo.getRe_openid());
sParaTemp.put("total_amount", sendRedPackPo.getTotal_amount());
sParaTemp.put("min_value", sendRedPackPo.getMin_value());
sParaTemp.put("max_value", sendRedPackPo.getMax_value());
sParaTemp.put("total_num", sendRedPackPo.getTotal_num());
sParaTemp.put("wishing", sendRedPackPo.getWishing());
sParaTemp.put("client_ip", sendRedPackPo.getClient_ip());
sParaTemp.put("act_name", sendRedPackPo.getAct_name());
sParaTemp.put("remark", sendRedPackPo.getRemark());


        //除去数组中的空值和签名参数
        Map<String, String> sPara = paraFilter(sParaTemp);
String prestr = createLinkString(sPara); //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
String key = "&key=192006250b4c09247ec02edce69f6a2d"; //商户支付密钥
String mysign = MD5.sign(prestr, key, "utf-8").toUpperCase();
        
sendRedPackPo.setSign(mysign);

        
String respXml = MessageUtil.messageToXml(sendRedPackPo);

//打印respXml发现,得到的xml中有“__”不对,应该替换成“_”
respXml = respXml.replace("__", "_");


        // 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();


KeyStore keyStore  = KeyStore.getInstance("PKCS12");
        FileInputStream instream = new FileInputStream(new File("/home/apiclient_cert.p12")); //此处为证书所放的绝对路径
        
        try {
            keyStore.load(instream, mch_id.toCharArray());
        } finally {
            instream.close();
        }


        // Trust own CA and all self-signed certs
        SSLContext sslcontext = SSLContexts.custom()
                .loadKeyMaterial(keyStore, mch_id.toCharArray())
                .build();
        // Allow TLSv1 protocol only
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[] { "TLSv1" },
                null,
                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        CloseableHttpClient httpclient = HttpClients.custom()
                .setSSLSocketFactory(sslsf)
                .build();
        
        try {


            HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack");
            
            StringEntity  reqEntity  = new StringEntity(respXml, "utf-8");
            
            // 设置类型 
            reqEntity.setContentType("application/x-www-form-urlencoded"); 
            
            httpPost.setEntity(reqEntity);
            
            System.out.println("executing request" + httpPost.getRequestLine());


            CloseableHttpResponse response = httpclient.execute(httpPost);
            try {
                HttpEntity entity = response.getEntity();
                System.out.println(response.getStatusLine());
                if (entity != null) {


            // 从request中取得输入流
            InputStream inputStream = entity.getContent();
            // 读取输入流
            SAXReader reader = new SAXReader();
            Document document = reader.read(inputStream);
            // 得到xml根元素
            Element root = document.getRootElement();
            // 得到根元素的所有子节点
            List<Element> elementList = root.elements();


            // 遍历所有子节点
            for (Element e : elementList)
            map.put(e.getName(), e.getText());


            // 释放资源
            inputStream.close();
            inputStream = null;


                }
                EntityUtils.consume(entity);
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }


// 返回状态码
String return_code = map.get("return_code");
// 返回信息
String return_msg = map.get("return_msg");
// 业务结果
String result_code = map.get("result_code");
// 错误代码
String err_code = map.get("err_code");
// 错误代码描述
String err_code_des = map.get("err_code_des");

/**
* 根据以上返回码进行业务逻辑处理
*/
.
.
.
.

}

MessageUtil工具类代码

package com.test.util;
/***
 * V型知识库www.vxzsk.com
 * 
 */
import java.io.InputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import com.test.message.resp.Article;
import com.test.message.resp.MusicMessage;
import com.test.message.resp.NewsMessage;
import com.test.message.resp.TextMessage;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;


/**
 * 消息工具类
 */
public class MessageUtil {

	/**
	 * 返回消息类型:文本
	 */
	public static final String RESP_MESSAGE_TYPE_TEXT = "text";

	/**
	 * 返回消息类型:音乐
	 */
	public static final String RESP_MESSAGE_TYPE_MUSIC = "music";

	/**
	 * 返回消息类型:图文
	 */
	public static final String RESP_MESSAGE_TYPE_NEWS = "news";

	/**
	 * 请求消息类型:文本
	 */
	public static final String REQ_MESSAGE_TYPE_TEXT = "text";

	/**
	 * 请求消息类型:图片
	 */
	public static final String REQ_MESSAGE_TYPE_IMAGE = "image";

	/**
	 * 请求消息类型:链接
	 */
	public static final String REQ_MESSAGE_TYPE_LINK = "link";

	/**
	 * 请求消息类型:地理位置
	 */
	public static final String REQ_MESSAGE_TYPE_LOCATION = "location";

	/**
	 * 请求消息类型:音频
	 */
	public static final String REQ_MESSAGE_TYPE_VOICE = "voice";

	/**
	 * 请求消息类型:推送
	 */
	public static final String REQ_MESSAGE_TYPE_EVENT = "event";

	/**
	 * 事件类型:subscribe(订阅)
	 */
	public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";

	/**
	 * 事件类型:unsubscribe(取消订阅)
	 */
	public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";

	/**
	 * 事件类型:CLICK(自定义菜单点击事件)
	 */
	public static final String EVENT_TYPE_CLICK = "CLICK";

	/**
	 * 解析微信发来的请求(XML)
	 * 
	 * @param request
	 * @return
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
		// 将解析结果存储在HashMap中
		Map<String, String> map = new HashMap<String, String>();
		// 从request中取得输入流
		InputStream inputStream = request.getInputStream();
		// 读取输入流
		SAXReader reader = new SAXReader();
		Document document = reader.read(inputStream);
		// 得到xml根元素
		Element root = document.getRootElement();
		// 得到根元素的所有子节点
		List<Element> elementList = root.elements();
		// 遍历所有子节点
		for (Element e : elementList) {
			map.put(e.getName(), e.getText());
		}
		// 释放资源
		inputStream.close();
		inputStream = null;
		return map;
	}

	/**
	 * 文本消息对象转换成xml	
	 * @param textMessage 文本消息对象
	 * @return xml
	 */
	public static String textMessageToXml(TextMessage textMessage) {
		xstream.alias("xml", textMessage.getClass());
		return xstream.toXML(textMessage);
	}

	/**
	 * 音乐消息对象转换成xml	
	 * @param musicMessage 音乐消息对象
	 * @return xml
	 */
	public static String musicMessageToXml(MusicMessage musicMessage) {
		xstream.alias("xml", musicMessage.getClass());
		return xstream.toXML(musicMessage);
	}

	/**
	 * 图文消息对象转换成xml	
	 * @param newsMessage 图文消息对象
	 * @return xml
	 */
	public static String newsMessageToXml(NewsMessage newsMessage) {
		xstream.alias("xml", newsMessage.getClass());
		xstream.alias("item", new Article().getClass());
		return xstream.toXML(newsMessage);
	}

	/**
	 * 扩展xstream,使其支持CDATA块	
	 * @date
	 */
	private static XStream xstream = new XStream(new XppDriver() {
		public HierarchicalStreamWriter createWriter(Writer out) {
			return new PrettyPrintWriter(out) {
				// 对所有xml节点的转换都增加CDATA标记
				boolean cdata = true;
				@SuppressWarnings("unchecked")
				public void startNode(String name, Class clazz) {
					super.startNode(name, clazz);
				}
				protected void writeText(QuickWriter writer, String text) {
					if (cdata) {
						writer.write("<![CDATA[");
						writer.write(text);
						writer.write("]]>");
					} else {
						writer.write(text);
					}
				}
			};
		}
	});
}

关于MessageUtil的封装代码请参考https://www.vxzsk.com/63.html 当然,读者也可以修改代码中MessageUtil解析xml部分代码。

 代码中httpclient-4.3.4.jar没有放上去,麻烦小伙伴们自己找下吧。

实现效果

 


小说《我是全球混乱的源头》

感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程