A classmate spent several days struggling with the payment process of QQ Mini Program, but couldn't complete the unified order. Since I have experience with WeChat and Alipay payments, he asked for my help. After spending a good two or three hours on it without success, I finally thought the issue might be with the merchant key. Although I asked the first party multiple times and was told that the key was not the problem, I still felt that the issue likely lay with the key. So, I went ahead and directly reset the key for him, and then, the payment was successfully completed... In summary, never trust the first party.
QQ Mini Program payment is similar to WeChat Mini Program payment, with completely identical signature methods, but with some differences in the submitted XML.
First, there is the configuration class, set to have package-private access. In reality, it should be placed in a properties file or directly configured in XML. However, I took a shortcut and wrote it directly in the code.
public class PayConfigs {
final static String appid="";
final static String mchid="";
final static String key="";
final static String reqAd="https://qpay.qq.com/cgi-bin/pay/qpay_unified_order.cgi";
}
For the Mini Program payment, it is necessary to first send a request to the backend with some product information and submit XML, then return a prepay_id to the front end. The Mini Program provides an API call to trigger payment.
qq.request({url:"Request Address",data:{/* Data */},success:function(result){if(result.data){ qq.requestPayment({package:"prepay_id="+ result.data.prepay_id,bargainor_id:"",//Merchant IDsuccess(res){},fail(res){}})}}})
The Java method for initiating payment requires the use of a utility class, as mentioned at the end of the document.
public Map<String,String> qqPay() throws Exception{
String mchid = PayConfigs.mchid;
String nonce_str = PayUtil.getRandomStringByLength(16);
String body = "Test";
String out_trade_no = "OTS"+ PayUtil.getRandomStringByLength(12); //Merchant Order Number
String fee_type = "CNY";
String total_fee = "100"; //Custom currency total, unit in cents
String spbill_create_ip = ""; // User client IP
String trade_type = "JSAPI"; //Default to JSAPI for Mini Program
String notify_url = "http://www.baidu.com"; //Callback address
Map<String, String> packageParams = new HashMap<>();
packageParams.put("mch_id", mchid);
packageParams.put("nonce_str", nonce_str);
packageParams.put("body", body);
packageParams.put("out_trade_no", out_trade_no + ""); //Merchant Order Number
packageParams.put("total_fee", total_fee + ""); //Payment amount, needs to be converted to a string
packageParams.put("spbill_create_ip", spbill_create_ip);
packageParams.put("notify_url", notify_url); //Callback address after successful payment
packageParams.put("trade_type", trade_type); //Payment method
String result = PayUtil.exec(packageParams,PayConfigs.key,PayConfigs.reqAd);
System.out.println(result);
// Business logic
return PayUtil.xmlToMap(result);
}
Upon successful user payment, Tencent's servers will access the submitted notify_url (the callback address) and provide the order number and signature verification in XML.
public String acceptPay(HttpServletRequest request) throws Exception{
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream()));
String line;
StringBuilder stringBuilder = new StringBuilder();
while ((line = br.readLine()) != null) {
stringBuilder.append(line);
}
br.close();
String notityXml = stringBuilder.toString();
Map<String,String> acceptParam = PayUtil.xmlToMap(notityXml);
if (acceptParam.get("trade_state").equals("SUCCESS") && PayUtil.verifySign(acceptParam,PayConfigs.key)){
// Please note that before the QQ server receives the "Accept", there may be multiple callbacks. There needs to be code to handle multiple callbacks
// Business logic
System.out.println(PayUtil.acceptXML());
}
return PayUtil.acceptXML();
}
private static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
StringBuilder stringBuilder = new StringBuilder();
try {
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(requestMethod);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.connect();
if (null != outputStr) {
OutputStream os = conn.getOutputStream();
os.write(outputStr.getBytes(StandardCharsets.UTF_8));
os.close();
}
InputStream is = conn.getInputStream();
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
stringBuilder.append(line);
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
return stringBuilder.toString();
}
private static Map<String, String> sortMapByKey(Map<String, String> map) {
List<String> keys = new ArrayList<>(map.keySet());
Collections.sort(keys);
// The underlying implementation of HashMap is an array plus a linked list. It will put the value of the key on the array coordinates of the object hashed by the hash algorithm,
// So the retrieved value is based on the hash table, so it is not related to the order of putting in. To maintain order, a LinkedHashMap is needed.
Map<String, String> m = new LinkedHashMap<>();
for (String key : keys) {
m.put(key, map.get(key));
}
map.clear();
return m;
}
private static String mapToXml(Map<String, String> map, String sign) {
StringBuilder stringBuilder = new StringBuilder().append("<xml>");
for (Map.Entry<String, String> m : map.entrySet()) {
stringBuilder.append("<").append(m.getKey()).append(">")
.append(m.getValue()).append("</").append(m.getKey()).append(">");
}
stringBuilder.append("<sign>").append(sign).append("</sign>").append("</xml>");
return stringBuilder.toString();
}
}