業務流程圖解
- 用戶發起充值 → Controller 生成訂單號、金額、類型
- 前端 JSP 渲染自動提交 Form → 支付網關頁面
- 用戶完成支付 → 支付網關異步通知(Notify URL)+ 同步跳轉(Return URL)
- 系統驗證簽名、更新訂單狀態、發放餘額/權益
核心代碼模組
1. Controller 入口
@PostMapping("/paySubmit.htm")
public ModelAndView paySubmit(@RequestParam Map<String,Object> params) {
String orderNo = IdUtil.fastUUID(); // 雪花/UUID
String amount = params.get("amt").toString();
String type = params.get("type").toString(); // 1=民生, 2=通聯等
ModelAndView mv = new ModelAndView("pay/submit");
mv.addObject("orderNo", orderNo);
mv.addObject("amt", amount);
mv.addObject("type", type);
return mv;
}
2. 統一提交頁面 submit.jsp
<%@ page import="com.util.PayUtil" %>
<% request.setCharacterEncoding("UTF-8");
String type = (String) request.getAttribute("type");
String html = "1".equals(type) ?
PayUtil.buildForm((String)request.getAttribute("orderNo"),
(String)request.getAttribute("amt"), type) :
PayUtil.allInPayBuildForm(...);
out.println(html);
%>
3. PayUtil 核心工具類(以民生銀行為例)
public class PayUtil {
// 讀取配置文件
private static final Properties PROP = loadProps("pay/cmcb.properties");
public static String buildForm(String orderNo, String amount, String type) {
Map<String,String> params = new LinkedHashMap<>();
params.put("merchantid", PROP.getProperty("merchantid"));
params.put("merorderid", orderNo);
params.put("amountsum", amount);
params.put("subject", PROP.getProperty("subject"));
params.put("currencytype", "01");
params.put("autojump", "1");
params.put("waittime", "0");
params.put("merurl", PROP.getProperty("returnUrl"));
params.put("informmer", "1");
params.put("informurl", PROP.getProperty("notifyUrl"));
params.put("confirm", "1");
params.put("merbank", "empty");
params.put("tradetype", "0");
params.put("bankInput", "0");
params.put("interface", "5.00");
params.put("remark", "充值");
params.put("bankcardtype", "00");
params.put("pdtdnm", "虛擬商品");
params.put("pdtdetailurl", PROP.getProperty("detailUrl"));
// 簽名:按鍵排序拼接 + 商戶私鑰 → MD5
String signSrc = buildSignSource(params) + "&merkey=" + PROP.getProperty("md5Key");
String mac = Crypto.md5(signSrc);
params.put("mac", mac);
return buildAutoSubmitForm(PROP.getProperty("gatewayUrl"), params);
}
private static String buildAutoSubmitForm(String url, Map<String,String> params) {
StringBuilder sb = new StringBuilder();
sb.append("<form id='payForm' action='").append(url).append("' method='post'>");
params.forEach((k,v) -> sb.append("<input type='hidden' name='").append(k)
.append("' value='").append(escapeHtml(v)).append("'/>"));
sb.append("<input type='submit' style='display:none'/></form>");
sb.append("<script>document.getElementById('payForm').submit();</script>");
return sb.toString();
}
}
4. MD5 加密工具
public class Crypto {
public static String md5(String src) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] d = md.digest(src.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : d) sb.append(String.format("%02x", b));
return sb.toString();
} catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
}
}
5. 異步通知入口(關鍵:驗簽 + 冪等 + 事務)
@PostMapping("/allPayReturn.htm")
public void notify(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Map<String,String[]> pm = req.getParameterMap();
Map<String,String> params = pm.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()[0]));
String sign = params.remove("mac"); // 移除簽名欄位
String localSign = Crypto.md5(buildSignSource(params) + "&merkey=" + PROP.getProperty("md5Key"));
if (!localSign.equals(sign)) { resp.getWriter().write("FAIL"); return; }
String orderNo = params.get("merorderid");
String result = params.get("retcode"); // "00" 成功
// 冪等處理:檢查訂單狀態,僅處理首次成功通知
if ("00".equals(result) && orderService.tryConfirmPay(orderNo)) {
resp.getWriter().write("SUCCESS"); // 告訴銀行已處理
} else {
resp.getWriter().write("FAIL");
}
}
安全與健壯性清單
- 簽名驗證:防篡改、防偽造通知
- 冪等設計:同一訂單多次通知只生效一次
- 配置外置:商戶號、密鑰、網關地址全配置化,環境隔離
- 日誌審計:請求/響應全鏈路記錄,含簽名源串,便於對賬
- 異常重試:通知失敗銀行會重發,系統需保證處理邏輯可重入