Release Enterprise Red Bag (C#) for Enterprise WeChat Development
1. Enterprise WeChat API
Address: http://work.weixin.qq.com/api/doc#11543
2. Explanation of parameters
1. Send enterprise red envelopes
Request method: POST (HTTPS)
Request address: https://api.mch.weixin.qq.com/mmpaymkttransfers/sendworkwxredpack
Is Certificate Required: Yes
Data format: xml
See API interface documentation for detailed parameter descriptions
2. Specific implementation code
WxPayData data = new WxPayData();
data.SetValue("nonce_str", WxPayApi.GenerateNonceStr());//Random String
data.SetValue("mch_billno", WxPayApi.GenerateOutTradeNo()); //Merchant Order Number
data.SetValue("mch_id", WxPayConfig.MCHID);//Business Number
data.SetValue("wxappid", WxPayConfig.APPID);//Public Account ID
data.SetValue("sender_name", "ly"); //Sender Name
data.SetValue("sender_header_media_id", "1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0"); //Sender's avatar, which is the default avatar for WeChat (see Part 3 if you want to customize your avatar)
string openid = ConvertToOpenidByUserId(_accessToken,"13212345678");
var openInfo = JsonConvert.DeserializeObject<U_OpenInfo>(openid);
data.SetValue("re_openid", openInfo.openid); //User openid
data.SetValue("total_amount", 100); //Amount of payment, minimum one dollar per unit
data.SetValue("wishing", "Happy Chinese Valentine's Day!"); //Red envelope greeting
data.SetValue("act_name", "XX activity"); //Activity Name
data.SetValue("remark", "Come and grab"); //Remarks
data.SetValue("scene_id", "PRODUCT_4"); //Scene (required if amount is greater than 200 yuan)
data.SetValue("workwx_sign", data.MakeWorkWxSign("redPacket")); //Enterprise WeChat Signature
data.SetValue("sign", data.MakeSign()); //WeChat Payment Signature
string xml = data.ToXml();
const string postUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/sendworkwxredpack ";//Send Enterprise Red Packet Interface Address
string response = PostWebRequest(postUrl, xml, Encoding.UTF8, true);//Call HTTP communication interface to submit data to API
WxPayData result = new WxPayData();
result.FromXml(response);
public class WxPayData
{
//The advantage of using sorted Dictionary is that it is easy to sign packets without having to sort them again before signing them again.
private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>();
/// <summary>
///Set the value of a field
/// </summary>
/// <param name="key">field name</param>
/// <param name="value">field value</param>
public void SetValue(string key, object value)
{
m_values[key] = value;
}
/// <summary>
///Get the value of a field based on its name
/// </summary>
/// <param name="key">field name</param>
/// <returns>corresponding field value </returns>
public object GetValue(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
return o;
}
/// <summary>
///Determine if a field is set
/// </summary>
/// <param name="key">field name</param>
/// <returns>Returns true if the field key is set, false</returns if not
public bool IsSet(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
if (null != o)
return true;
else
return false;
}
/// <summary>
///Convert Dictionary to xml
/// </summary>
/// <returns>Converted xml string </returns>
public string ToXml()
{
//Cannot convert to xml format when data is empty
if (0 == m_values.Count)
{
LogManager.WriteLog(LogFile.Error, "WxPayData Data is empty!");
throw new WxPayException("WxPayData Data is empty!");
}
string xml = "<xml>";
foreach (KeyValuePair<string, object> pair in m_values)
{
//Field value cannot be null, affecting subsequent processes
if (pair.Value == null)
{
throw new WxPayException("WxPayData Internal Containment Value null Fields of!");
}
if (pair.Value is int)
{
xml += "<" + pair.Key + ">" + pair.Value + "</" + pair.Key + ">";
}
else if (pair.Value is string)
{
xml += "<" + pair.Key + ">" + "<![CDATA[" + pair.Value + "]]></" + pair.Key + ">";
}
else//No other data types except string and int
{
throw new WxPayException("WxPayData Field data type error!");
}
}
xml += "</xml>";
return xml;
}
/// <summary>
///Convert xml to a WxPayData object and return data inside the object
/// </summary>
/// <param name="xml">xml string to be converted </param>
/// <returns>Converted Dictionary</returns>
public SortedDictionary<string, object> FromXml(string xml)
{
if (string.IsNullOrEmpty(xml))
{
throw new WxPayException("Will be empty xml Convert string to WxPayData Wrongful!");
}
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
XmlNode xmlNode = xmlDoc.FirstChild;//Get root node <xml>
XmlNodeList nodes = xmlNode.ChildNodes;
foreach (XmlNode xn in nodes)
{
XmlElement xe = (XmlElement)xn;
m_values[xe.Name] = xe.InnerText;//Get xml key-value pairs to data inside WxPayData
}
try
{
if (m_values["return_code"].ToString() != "SUCCESS")
{
return m_values;
}
// CheckSign(); //Verify signature, failing throws an exception
}
catch (WxPayException ex)
{
throw new WxPayException(ex.Message);
}
return m_values;
}
/// <summary>
/// Dictionary format to url parameter format
/// </summary>
/// <returns>url format string, which does not contain the sign field value </returns>
public string ToUrl()
{
string buff = "";
foreach (KeyValuePair<string, object> pair in m_values)
{
if (pair.Value == null)
{
LogManager.WriteLog(LogFile.Error, "WxPayData Internal Containment Value null Fields of!");
throw new WxPayException("WxPayData Internal Containment Value null Fields of!");
}
if (pair.Key != "sign" && pair.Value.ToString() != "")
{
buff += pair.Key + "=" + pair.Value + "&";
}
}
buff = buff.Trim('&');
return buff;
}
/// <summary>
/// Dictionary format to url parameter format
/// </summary>
/// <returns>url format string, which does not contain the sign field value </returns>
public string ToWorkWxUrl(string type)
{
string buff = "";
foreach (KeyValuePair<string, object> pair in m_values)
{
if (pair.Value == null)
{
throw new WxPayException("WxPayData Internal Containment Value null Fields of!");
}
var paras = new List<string>();
switch (type)
{
case "redPacket":
paras = new[] { "act_name", "mch_billno", "mch_id", "nonce_str", "re_openid", "total_amount", "wxappid" }.ToList();
break;
case "payment":
paras = new[] { "amount", "desc", "mch_id", "nonce_str", "openid", "partner_trade_no", "appid", "ww_msg_type" }.ToList();
break;
}
if (paras.Contains(pair.Key))
{
buff += pair.Key + "=" + pair.Value + "&";
}
}
buff = buff.Trim('&');
return buff;
}
/// <summary>
///Generate signatures as detailed in the signature generation algorithm
/// </summary>
/// <returns>Signature, sign field does not participate in signature </returns>
public string MakeSign()
{
//To url format
string str = ToUrl();
//Add API KEY after string
str += "&key=" + WxPayConfig.KEY;
//MD5 Encryption
var md5 = MD5.Create();
var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
var sb = new StringBuilder();
foreach (byte b in bs)
{
sb.Append(b.ToString("x2"));
}
//All characters uppercase
return sb.ToString().ToUpper();
}
/// <summary>
///Generate Enterprise WeChat Signature
/// </summary>
/// <returns></returns>
public string MakeWorkWxSign(string type)
{
//To url format
string str = ToWorkWxUrl(type);
//Add secret after string
str += "&secret=" + WxPayConfig.PAYMENTSECRET;
//MD5 Encryption
var md5 = MD5.Create();
var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
var sb = new StringBuilder();
foreach (byte b in bs)
{
sb.Append(b.ToString("x2"));
}
//All characters uppercase
return sb.ToString().ToUpper();
}
/// <summary>
///Check if the signature is correct
/// </summary>
/// <returns>Returns true correctly, throws an exception by error </returns>
public bool CheckSign()
{
//Skip detection if no signature is set
if (!IsSet("sign"))
{
LogManager.WriteLog(LogFile.Error, "WxPayData Signature exists but is not legal!");
throw new WxPayException("WxPayData Signature exists but is not legal!");
}
//If a signature is set but the signature is empty, an exception is thrown
else if (GetValue("sign") == null || GetValue("sign").ToString() == "")
{
LogManager.WriteLog(LogFile.Error, "WxPayData Signature exists but is not legal!");
throw new WxPayException("WxPayData Signature exists but is not legal!");
}
//Get the received signature
string return_sign = GetValue("sign").ToString();
//Calculate new signatures locally
string cal_sign = MakeSign();
if (cal_sign == return_sign)
{
return true;
}
LogManager.WriteLog(LogFile.Error, "WxPayData Signature verification error!");
throw new WxPayException("WxPayData Signature verification error!");
}
/// <summary>
///Get Dictionary
/// </summary>
/// <returns></returns>
public SortedDictionary<string, object> GetValues()
{
return m_values;
}
}
public class WxPayException : Exception
{
public WxPayException(string msg)
: base(msg)
{
}
}
public class WxPayApi
{
protected Hashtable Parameters = new Hashtable();
/// <summary>
///Generate order number based on current system time plus random sequence
/// </summary>
/// <returns>@return order number </returns>
public static string GenerateOutTradeNo()
{
var ran = new Random();
return string.Format("{0}{1:yyyyMMddHHmmss}{2}", WxPayConfig.MCHID, DateTime.Now, ran.Next(999));
}
/// <summary>
///Generate timestamp, Standard Beijing Time, Time Zone East Eight, seconds since 10:0 minutes and 0 seconds on January 1, 1970
/// </summary>
/// <returns>@return timestamp</returns>
public static string GenerateTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds).ToString();
}
/// <summary>
///Generate a random string containing letters or numbers
/// </summary>
/// <returns> @return random string </returns>
public static string GenerateNonceStr()
{
//Random random = new Random();
//return GetMD5(random.Next(1000).ToString(), "GBK");
return Guid.NewGuid().ToString().Replace("-", "");
}
/// <summary>
///Get md5 encrypted string
/// </summary>
/// <param name="encypStr"></param>
/// <param name="charset"></param>
/// <returns></returns>
protected static string GetMD5(string encypStr, string charset)
{
byte[] bytes;
//Create md5 object
MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider();
//Converts a string to a byte array using GB2312 encoding.
try
{
bytes = Encoding.GetEncoding(charset).GetBytes(encypStr);
}
catch (Exception)
{
bytes = Encoding.GetEncoding("GB2312").GetBytes(encypStr);
}
return BitConverter.ToString(provider.ComputeHash(bytes)).Replace("-", "").ToUpper();
}
protected void SetParameter(string parameter, string parameterValue)
{
if (!string.IsNullOrEmpty(parameter))
{
if (this.Parameters.Contains(parameter))
{
this.Parameters.Remove(parameter);
}
this.Parameters.Add(parameter, parameterValue);
}
}
}
public class WxPayConfig
{
//=======[Basic Information Settings]============================================
/* WeChat Public Number Information Configuration
* APPID: APPID for Binding Payment (must be configured)
* MCHID: Business Number (must be configured)
* KEY: Merchant Payment Key, refer to Opening Mail Settings (must be configured)
* APPSECRET: Public account secert (configure only when JSAPI pays)
*/
public static readonly string APPID = "111111111111"; //Write all about yourself
public static readonly string APPSECRET = "111111";
public static readonly string PAYMENTSECRET ="111111";
public static readonly string MCHID = "111111"; //Merchant id Number
public static readonly string KEY = "111111111111";
//=======[Certificate Path Settings]=========================================
/* Certificate path, note that absolute path should be filled in (only for refunds, cancellations of orders)
*/
public static readonly string SSLCERT_PATH = "cert/apiclient_cert.p12";
public static readonly string SSLCERT_PASSWORD =MCHID ;
}
3. Notes
(1) Calculate Enterprise WeChat Signature
The secret at the end of the string is the secret of the payment application page on the management side of Enterprise WeChat (see below)
Instead of Enterprise WeChat's secret.(Picture below) Remember!!!
(2) Or calculate the signature of Enterprise WeChat
The RedPack ap has and only has the following fields participating in the signature (as reflected in the code):
act_name
mch_billno
mch_id
nonce_str
re_openid
total_amount
wxappid
Do not involve all parameters in calculating signatures, otherwise the WeChat signature error will be returned!
3. Uploading Temporary Materials
1. There is a parameter in the API interface of RedPack that is "sender_header_media_id", which is the sender's avatar, which can be obtained through the open upload material interface of Enterprise WeChat.
Request method: POST (HTTPS)
Request address: https://qyapi.weixin.qq.com/cgi-bin/media/upload?Access_token=ACCESS_TOKEN&type=TYPE
Upload the file using http multipart/form-data with the file ID "media".
Parameter description:
parameter |
Must |
Explain |
access_token |
yes |
Call interface credentials |
type |
yes |
Media file types, such as picture, voice, video and file. |
media |
yes |
Identification of media file in form-data with information such as filename, filelength, content-type, etc. |
Permission Instructions:
Fully public, media_id s can be shared between applications within the same enterprise.
Return data:
{
"errcode": 0,
"errmsg": "",
"type": "image",
"media_id": "1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0",
"created_at": "1380000000"
}
2. Specific implementation
/// <summary>
///Upload temporary material
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public string UploadTempResource(string filePath)
{
const string url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={0}&type=image";
var uploadUrl = string.Format(url, _accessToken);
var mediaId = "1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0";
using (var client = new WebClient())
{
byte[] resource = client.UploadFile(new Uri(uploadUrl), filePath);
string retdata = Encoding.UTF8.GetString(resource);
var data = JsonConvert.DeserializeObject(retdata) as JObject;
if (data != null)
{
mediaId = data["media_id"].ToString();
}
}
return mediaId;
}
}
4. Achieving Effect