Tổng quan
Trong phần trước, chúng ta đã tìm hiểu cách xây dựng backend cho thanh toán WeChat Mini Program. Phần này sẽ hướng dẫn triển khai backend cho thanh toán WeChat APP. Về cơ bản, thanh toán APP có nhiều điểm tương tự với Mini Program, nhưng có một số khác biệt chính:
- Backend thanh toán APP không yêu cầu thông tin đăng nhập WeChat (login credential)
- Giá trị
trade_typekhi tạo đơn hàng là"APP"thay vì"JSAPI" - Các tham số gửi đến ứng dụng (APP) để khởi tạo thanh toán cũng khác
Do WeChat Mini Program và APP sử dụng các APPID khác nhau, nên chúng ta xây dựng hai bộ xử lý riêng biệt thay vì dùng chung code và phân biệt bằng cấu hình.
Triển khai chi tiết
1. Cấu trúc thư mục
Tạo thư mục AppPay bên trong thư mục WePay (lớp chung cho thanh toán WeChat đã có ở phần trước). Thư mục này sẽ chứa các class phục vụ thanh toán APP.
2. Lớp cấu hình AppPayConfig
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Configuration;
namespace App.Pay.WePay.AppPay
{
public class AppPayConfig : WePayConfig
{
//=======【Cấu hình cơ bản】=====================================
/* Thông tin ứng dụng WeChat
* APPID: APPID của ứng dụng (bắt buộc)
* MCHID: Mã số merchant (bắt buộc)
* KEY: Khóa bảo mật merchant (bắt buộc)
* APPSECRET: Secret của ứng dụng (không bắt buộc cho APP pay)
*/
/// Thanh toán APP
public static string APPID = WebConfigurationManager.AppSettings["AppAppID"].ToString();
public static string MCHID = WebConfigurationManager.AppSettings["AppMchID"].ToString();
public static string KEY = WebConfigurationManager.AppSettings["AppKey"].ToString();
public static string APPSECRET = WebConfigurationManager.AppSettings["AppSecret"].ToString();
//=======【Đường dẫn chứng chỉ】=====================================
/* Đường dẫn chứng chỉ SSL (dùng cho refund, hủy đơn)
*/
public const string SSLCERT_PATH = "cert/apiclient_cert.p12";
public const string SSLCERT_PASSWORD = "1233410002";
//=======【URL callback thanh toán】=====================================
/* URL callback nhận kết quả thanh toán từ WeChat
*/
public static string NOTIFY_URL = WebConfigurationManager.AppSettings["AppNotifyUrl"].ToString();
// Log
public static string LogPath = WebConfigurationManager.AppSettings["AppLog"].ToString();
}
}
3. Lớp AppPayHttpService
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace App.Pay.WePay.AppPay
{
public class AppPayHttpService
{
private static Log Log = new Log(AppPayConfig.LogPath);
public static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
return true;
}
public static string Post(string xml, string url, bool isUseCert, int timeout)
{
System.GC.Collect();
string result = "";
HttpWebRequest request = null;
HttpWebResponse response = null;
Stream reqStream = null;
try
{
ServicePointManager.DefaultConnectionLimit = 200;
if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
{
ServicePointManager.ServerCertificateValidationCallback =
new RemoteCertificateValidationCallback(CheckValidationResult);
}
request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.Timeout = timeout * 1000;
request.ContentType = "text/xml";
byte[] data = System.Text.Encoding.UTF8.GetBytes(xml);
request.ContentLength = data.Length;
if (isUseCert)
{
string path = HttpContext.Current.Request.PhysicalApplicationPath;
X509Certificate2 cert = new X509Certificate2(path + AppPayConfig.SSLCERT_PATH, AppPayConfig.SSLCERT_PASSWORD);
request.ClientCertificates.Add(cert);
}
reqStream = request.GetRequestStream();
reqStream.Write(data, 0, data.Length);
reqStream.Close();
response = (HttpWebResponse)request.GetResponse();
StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
result = sr.ReadToEnd().Trim();
sr.Close();
}
catch (Exception e)
{
Log.Error("HttpService", e.ToString());
throw new WePayException(e.ToString());
}
finally
{
if (response != null) response.Close();
if (request != null) request.Abort();
}
return result;
}
public static string Get(string url)
{
System.GC.Collect();
string result = "";
HttpWebRequest request = null;
HttpWebResponse response = null;
try
{
ServicePointManager.DefaultConnectionLimit = 200;
if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
ServicePointManager.ServerCertificateValidationCallback =
new RemoteCertificateValidationCallback(CheckValidationResult);
request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";
response = (HttpWebResponse)request.GetResponse();
StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
result = sr.ReadToEnd().Trim();
sr.Close();
}
catch (Exception e)
{
Log.Error("HttpService", e.ToString());
throw new WePayException(e.ToString());
}
finally
{
if (response != null) response.Close();
if (request != null) request.Abort();
}
return result;
}
}
}
4. Lớp AppPayData
using LitJson;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace App.Pay.WePay.AppPay
{
public class AppPayData
{
private Log Log = new Log(AppPayConfig.LogPath);
private SortedDictionary dataStore = new SortedDictionary();
public AppPayData() { }
public void SetValue(string key, object value)
{
dataStore[key] = value;
}
public object GetValue(string key)
{
object o = null;
dataStore.TryGetValue(key, out o);
return o;
}
public bool IsSet(string key)
{
object o = null;
dataStore.TryGetValue(key, out o);
return o != null;
}
public string ToXml()
{
if (dataStore.Count == 0)
{
Log.Error(this.GetType().ToString(), "Dữ liệu AppPayData trống!");
throw new WePayException("Dữ liệu AppPayData trống!");
}
string xml = "<xml>";
foreach (var pair in dataStore)
{
if (pair.Value == null)
{
Log.Error(this.GetType().ToString(), "AppPayData chứa trường có giá trị null!");
throw new WePayException("AppPayData chứa trường có giá trị null!");
}
if (pair.Value.GetType() == typeof(int))
xml += "<" + pair.Key + ">" + pair.Value + "</" + pair.Key + ">";
else if (pair.Value.GetType() == typeof(string))
xml += "<" + pair.Key + "><![CDATA[" + pair.Value + "]]></" + pair.Key + ">";
else
{
Log.Error(this.GetType().ToString(), "Kiểu dữ liệu không hợp lệ trong AppPayData!");
throw new WePayException("Kiểu dữ liệu không hợp lệ!");
}
}
xml += "</xml>";
return xml;
}
public SortedDictionary FromXml(string xml)
{
if (string.IsNullOrEmpty(xml))
{
Log.Error(this.GetType().ToString(), "Chuỗi XML trống không hợp lệ!");
throw new WePayException("Chuỗi XML trống!");
}
SafeXmlDocument xmlDoc = new SafeXmlDocument();
xmlDoc.LoadXml(xml);
XmlNode root = xmlDoc.FirstChild;
XmlNodeList nodes = root.ChildNodes;
foreach (XmlNode node in nodes)
{
XmlElement element = (XmlElement)node;
dataStore[element.Name] = element.InnerText;
}
if (dataStore["return_code"].ToString() == "SUCCESS")
CheckSign();
return dataStore;
}
public string ToUrl()
{
string buffer = "";
foreach (var pair in dataStore)
{
if (pair.Value == null)
{
Log.Error(this.GetType().ToString(), "AppPayData chứa trường null!");
throw new WePayException("AppPayData chứa trường null!");
}
if (pair.Key != "sign" && pair.Value.ToString() != "")
buffer += pair.Key + "=" + pair.Value + "&";
}
return buffer.Trim('&');
}
public string ToJson()
{
return JsonMapper.ToJson(dataStore);
}
public string MakeSign()
{
string str = ToUrl();
str += "&key=" + AppPayConfig.KEY;
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"));
return sb.ToString().ToUpper();
}
public bool CheckSign()
{
if (!IsSet("sign") || GetValue("sign") == null || GetValue("sign").ToString() == "")
{
Log.Error(this.GetType().ToString(), "Chữ ký không hợp lệ!");
throw new WePayException("Chữ ký không hợp lệ!");
}
string receivedSign = GetValue("sign").ToString();
string calculatedSign = MakeSign();
if (calculatedSign == receivedSign)
return true;
Log.Error(this.GetType().ToString(), "Xác thực chữ ký thất bại!");
throw new WePayException("Xác thực chữ ký thất bại!");
}
public SortedDictionary GetValues()
{
return dataStore;
}
}
}
5. Lớp AppPayNotify
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace App.Pay.WePay.AppPay
{
public class AppPayNotify
{
public HttpContext Context { get; set; }
public Log Logger = new Log(AppPayConfig.LogPath);
public AppPayNotify(HttpContext context)
{
Context = context;
}
public AppPayData GetNotifyData()
{
System.IO.Stream inputStream = Context.Request.InputStream;
int count = 0;
byte[] buffer = new byte[1024];
StringBuilder builder = new StringBuilder();
while ((count = inputStream.Read(buffer, 0, 1024)) > 0)
{
builder.Append(Encoding.UTF8.GetString(buffer, 0, count));
}
inputStream.Flush();
inputStream.Close();
inputStream.Dispose();
AppPayData data = new AppPayData();
try
{
data.FromXml(builder.ToString());
}
catch (WePayException ex)
{
AppPayData errorResponse = new AppPayData();
errorResponse.SetValue("return_code", "FAIL");
errorResponse.SetValue("return_msg", ex.Message);
Logger.Error(this.GetType().ToString(), "Lỗi chữ ký: " + errorResponse.ToXml());
Context.Response.Write(errorResponse.ToXml());
Context.Response.End();
}
Logger.Info(this.GetType().ToString(), "Xác thực chữ ký thành công");
return data;
}
public virtual void ProcessNotify()
{
}
}
}
6. Lớp AppPayApi
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App.Pay.WePay.AppPay
{
public class AppPayApi
{
public static Log Logger = new Log(AppPayConfig.LogPath);
public static AppPayData Micropay(AppPayData input, int timeout = 10)
{
string url = "https://api.mch.weixin.qq.com/pay/micropay";
if (!input.IsSet("body") || !input.IsSet("out_trade_no") || !input.IsSet("total_fee") || !input.IsSet("auth_code"))
throw new WePayException("Thiếu tham số bắt buộc cho Micropay!");
input.SetValue("spbill_create_ip", WePayConfig.IP);
input.SetValue("appid", AppPayConfig.APPID);
input.SetValue("mch_id", AppPayConfig.MCHID);
input.SetValue("nonce_str", Guid.NewGuid().ToString().Replace("-", ""));
input.SetValue("sign", input.MakeSign());
string xml = input.ToXml();
Logger.Info("AppPayApi", "Micropay request: " + xml);
string response = AppPayHttpService.Post(xml, url, false, timeout);
Logger.Info("AppPayApi", "Micropay response: " + response);
AppPayData result = new AppPayData();
result.FromXml(response);
return result;
}
public static AppPayData OrderQuery(AppPayData input, int timeout = 6)
{
string url = "https://api.mch.weixin.qq.com/pay/orderquery";
if (!input.IsSet("out_trade_no") && !input.IsSet("transaction_id"))
throw new WePayException("Thiếu out_trade_no hoặc transaction_id!");
input.SetValue("appid", AppPayConfig.APPID);
input.SetValue("mch_id", AppPayConfig.MCHID);
input.SetValue("nonce_str", GenerateNonceStr());
input.SetValue("sign", input.MakeSign());
string xml = input.ToXml();
Logger.Info("AppPayApi", "OrderQuery request: " + xml);
string response = AppPayHttpService.Post(xml, url, false, timeout);
Logger.Info("AppPayApi", "OrderQuery response: " + response);
AppPayData result = new AppPayData();
result.FromXml(response);
return result;
}
public static AppPayData UnifiedOrder(AppPayData input, int timeout = 6)
{
string url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
if (!input.IsSet("out_trade_no") || !input.IsSet("body") || !input.IsSet("total_fee") || !input.IsSet("trade_type"))
throw new WePayException("Thiếu tham số bắt buộc cho UnifiedOrder!");
if (input.GetValue("trade_type").ToString() == "JSAPI" && !input.IsSet("openid"))
throw new WePayException("Thiếu openid cho JSAPI!");
if (input.GetValue("trade_type").ToString() == "NATIVE" && !input.IsSet("product_id"))
throw new WePayException("Thiếu product_id cho NATIVE!");
if (!input.IsSet("notify_url"))
input.SetValue("notify_url", AppPayConfig.NOTIFY_URL);
input.SetValue("appid", AppPayConfig.APPID);
input.SetValue("mch_id", AppPayConfig.MCHID);
input.SetValue("spbill_create_ip", WePayConfig.IP);
input.SetValue("nonce_str", GenerateNonceStr());
input.SetValue("sign", input.MakeSign());
string xml = input.ToXml();
Logger.Info("AppPayApi", "UnifiedOrder request: " + xml);
string response = AppPayHttpService.Post(xml, url, false, timeout);
Logger.Info("AppPayApi", "UnifiedOrder response: " + response);
AppPayData result = new AppPayData();
result.FromXml(response);
return result;
}
public static string GenerateNonceStr()
{
return Guid.NewGuid().ToString().Replace("-", "");
}
public static string GenerateTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds).ToString();
}
}
}
7. Ví dụ Controller xử lý thanh toán
using App.Common.Extension;
using App.Pay.WePay.AppPay;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Configuration;
using System.Web.Mvc;
namespace App.WebTest.Controllers
{
public class WeAppPayController : BaseController
{
public ActionResult WxPayApp(string orderIds)
{
int[] orderIdList = Serialize.JsonTo(orderIds).ToArray();
decimal totalAmount = 0;
string productDetail = "";
// Xác thực đơn hàng
// ...
try
{
string notifyUrl = WebConfigurationManager.AppSettings["WxAppNotifyUrl"].ToString();
AppPayData orderData = new AppPayData();
orderData.SetValue("body", "Sản phẩm khóa học");
orderData.SetValue("attach", string.Join(",", orderIdList));
Random random = new Random();
string orderNumber = DateTime.Now.ToString("yyyyMMddHHmmss") + random.Next(0, 1000).ToString().PadLeft(3, '0');
orderData.SetValue("out_trade_no", orderNumber);
orderData.SetValue("total_fee", Convert.ToInt32(totalAmount * 100));
orderData.SetValue("time_start", DateTime.Now.ToString("yyyyMMddHHmmss"));
orderData.SetValue("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));
orderData.SetValue("notify_url", notifyUrl);
orderData.SetValue("trade_type", "APP");
AppPayData orderResult = AppPayApi.UnifiedOrder(orderData);
string appId = orderResult.GetValue("appid").ToString();
string partnerId = orderResult.GetValue("mch_id").ToString();
string prepayId = orderResult.GetValue("prepay_id").ToString();
string nonceStr = orderResult.GetValue("nonce_str").ToString();
string timeStamp = ((DateTime.Now.Ticks - TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)).Ticks) / 10000).ToString();
AppPayData signData = new AppPayData();
signData.SetValue("appid", appId);
signData.SetValue("noncestr", nonceStr);
signData.SetValue("package", "Sign=WXPay");
signData.SetValue("partnerid", partnerId);
signData.SetValue("prepayid", prepayId);
signData.SetValue("timestamp", timeStamp);
string paySign = signData.MakeSign();
return Json(new { appid = appId, partnerid = partnerId, prepayid = prepayId, package = "Sign=WXPay", noncestr = nonceStr, timestamp = timeStamp, sign = paySign });
}
catch (Exception ex)
{
return Json(new { success = false, message = "Lỗi tạo đơn hàng" });
}
}
public string WxAppNotifyUrl()
{
Log logger = new Log(AppPayConfig.LogPath);
logger.Info("WxAppNotifyUrl", "Nhận callback thanh toán");
AppPayNotify notifyHandler = new AppPayNotify(System.Web.HttpContext.Current);
AppPayPayData notifyData = notifyHandler.GetNotifyData();
if (!notifyData.IsSet("transaction_id"))
{
AppPayData errorResponse = new AppPayData();
errorResponse.SetValue("return_code", "FAIL");
errorResponse.SetValue("return_msg", "Thiếu transaction_id");
logger.Error("WxAppNotifyUrl", "Callback không có transaction_id");
Response.Write(errorResponse.ToXml());
Response.End();
}
string transactionId = notifyData.GetValue("transaction_id").ToString();
if (!VerifyOrder(transactionId))
{
AppPayData failResponse = new AppPayData();
failResponse.SetValue("return_code", "FAIL");
failResponse.SetValue("return_msg", "Xác thực đơn hàng thất bại");
Response.Write(failResponse.ToXml());
Response.End();
}
AppPayData successResponse = new AppPayData();
successResponse.SetValue("return_code", "SUCCESS");
successResponse.SetValue("return_msg", "OK");
logger.Info("WxAppNotifyUrl", "Xử lý callback thành công");
string outTradeNo = notifyData.GetValue("out_trade_no").ToString();
string attachData = notifyData.GetValue("attach").ToString();
string payTime = notifyData.GetValue("time_end").ToString();
string totalFee = notifyData.GetValue("total_fee").ToString();
// Cập nhật cơ sở dữ liệu
// ...
Response.Write(successResponse.ToXml());
Response.End();
return "";
}
private bool VerifyOrder(string transactionId)
{
AppPayData queryData = new AppPayData();
queryData.SetValue("transaction_id", transactionId);
AppPayData queryResult = AppPayApi.OrderQuery(queryData);
return queryResult.GetValue("return_code").ToString() == "SUCCESS" &&
queryResult.GetValue("result_code").ToString() == "SUCCESS";
}
}
}
Mã nguồn đầy đủ có thể tham khảo tại: https://github.com/wenha/Utility.git
Lưu ý quan trọng
- Trong controller, không cần truyền thông tin đăng nhập WeChat (như code hoặc openid) vì thanh toán APP sử dụng cơ chế xác thực khác
- Giá trị trade_type phải được đặt là "APP" khi gọi API UnifiedOrder
- Các tham số trả về cho ứng dụng (APP) bao gồm:
appid,partnerid,prepayid,package,noncestr,timestamp,sign - Xử lý callback cần kiểm tra kỹ transaction_id và thực hiện OrderQuery để xác thực đơn hàng trước khi cập nhật dữ liệu