Java生鮮電商平臺-支付模塊的設計與架構html
開源生鮮電商平臺支付目前支持支付寶與微信。針對的是APP端(android or IOS)java
1。 數據庫表設計。android
說明:不管是支付寶仍是微信支付,都會有一個服務端的回調,業務根據回調的結果處理相應的業務邏輯。git
pay_logs這個表主要是記錄相關的用戶支付信息。是一個日誌記錄。github
好比:誰付款的,何時付款的,訂單號多少,是支付寶仍是微信,支付狀態是支付成功仍是支付失敗,仍是未支付。web
特別注意:訂單主表也有相似的回調信息。這樣用多張表記錄相應的信息,能夠統計相應的業務指標,包括用戶的行爲分析等。spring
關於表的設計,個人經驗分享是:若是能夠,核心業務表必定要有一個日誌記錄表,若是能夠,能夠用時間軸等方式進行數據的插入,與時間軸的顯示。數據庫
時間軸能夠清楚的知道用戶的行爲點,幫助更加清晰的設計業務流程與系統架構。 apache
相應的支付寶回調代碼以下:(注意,這個業務模塊屬於買家。)json
APP調用後端的業務代碼
/*** * APP端調用請求支付寶 */ @RestController @RequestMapping("/buyer") public class AlipayController extends BaseController { private static final Logger logger = LoggerFactory.getLogger(AlipayController.class); /** * 服務器通知地址 */ private static final String NOTIFY_URL=""; /**待支付*/ private static final int PAY_LOGS_READY=0; @Autowired private OrderInfoService orderInfoService; @Autowired private BuyerService buyerService; @Autowired private PayLogsService payLogsService;/** * APP端請求調用支付寶 * @param request * @param response * @return */ @RequestMapping(value="/alipay/invoke",method={RequestMethod.GET,RequestMethod.POST}) public JsonResult alipayInvoke(HttpServletRequest req, HttpServletResponse resp) { String result=""; try { /**訂單號*/ String orderNumber=this.getNotNull("orderNumber", req); /**金額*/ String money=this.getNotNull("money", req); /**優惠券id*/ String couponReceiveId = req.getParameter("couponReceiveId"); if(StringUtils.isBlank(orderNumber) || StringUtils.isBlank(money)) { return new JsonResult(JsonResultCode.FAILURE,"請求參數有誤,請稍後重試",""); } //對比金額 OrderInfo orderInfo=this.orderInfoService.getOrderInfoByOrderNumber(orderNumber); if(orderInfo==null) { return new JsonResult(JsonResultCode.FAILURE,"訂單號不存在,請稍後重試",""); } //獲取訂單的金額 BigDecimal orderAmount=orderInfo.getOrderAmount(); //減餘額 Long buyerId=orderInfo.getBuyerId(); Buyer buyer=this.buyerService.getBuyerById(buyerId); //用戶餘額 BigDecimal balanceMoney=buyer.getBalanceMoney(); //計算最終須要給支付寶的金額 BigDecimal payAmount=orderAmount.subtract(balanceMoney); //買家支付時抵扣優惠券 if(StringUtils.isNotBlank(couponReceiveId)){ Long id = Long.parseLong(couponReceiveId); payAmount = couponReceiveService.deductionCouponMoney(id, orderNumber, payAmount); } logger.info("[AlipayController][alipayInvoke] orderNumber:" +orderNumber +" money:" +money+" orderAmount:"+orderAmount+" balanceMoney:"+balanceMoney+" payAmount:" +payAmount); //考慮重複訂單的問題,會產生重複日誌 PayLogs payLogs=this.payLogsService.getPayLogsByOrderNumber(orderNumber); if(payLogs==null) { //建立訂單日誌 PayLogs logs=new PayLogs(); logs.setUserId(buyerId); logs.setOrderId(orderInfo.getOrderId()); logs.setOrderNumber(orderNumber); logs.setOrderAmount(payAmount); logs.setStatus(PAY_LOGS_READY); logs.setCreateTime(new Date()); int payLogsResult=payLogsService.addPayLogs(logs); logger.info("[AlipayController][alipayInvoke] 建立訂單日誌結果:" + (payLogsResult>0)); }else { logger.info("[AlipayController][alipayInvoke] 建立重複訂單"); } AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do","","", "json","UTF-8","","RSA2"); AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest(); AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); model.setBody(""); model.setSubject("xxx"); model.setOutTradeNo(orderNumber); model.setTimeoutExpress("15m"); model.setTotalAmount(payAmount.toString()); model.setProductCode("QUICK_MSECURITY_PAY"); request.setBizModel(model); request.setNotifyUrl(NOTIFY_URL); AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request); result=response.getBody(); logger.info("[AlipayController][alipayNotify] result: " +result); } catch (AlipayApiException ex) { logger.error("[AlipayController][alipayNotify] exception:",ex); return new JsonResult(JsonResultCode.FAILURE,"系統錯誤,請稍後重試",""); } return new JsonResult(JsonResultCode.SUCCESS,"操做成功",result); } }
支付寶服務端回調代碼
import java.io.IOException; import java.math.BigDecimal; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.alipay.api.AlipayApiException; import com.alipay.api.internal.util.AlipaySignature; /** * alipay 支付寶服務端回調 * 參考文檔:https://docs.open.alipay.com/204/105301/ * 對於App支付產生的交易,支付寶會根據原始支付API中傳入的異步通知地址notify_url,經過POST請求的形式將支付結果做爲參數通知到商戶系統。 */ @Controller @RequestMapping("/buyer") public class AlipayNotifyController extends BaseController { private static final Logger logger = LoggerFactory.getLogger(AlipayNotifyController.class); private static final String ALIPAYPUBLICKEY = ""; private static final String CHARSET = "UTF-8"; /**支付成功*/ private static final int PAY_LOGS_SUCCESS=1; @Autowired private OrderInfoService orderInfoService; @RequestMapping(value = "/alipay/notify", method = { RequestMethod.GET, RequestMethod.POST }) public void alipayNotify(HttpServletRequest request, HttpServletResponse response) throws IOException { String url=request.getRequestURL().toString(); logger.info("AlipayNotifyController.alipayNotify.s+tart"); logger.info("[AlipayNotifyController][alipayNotify] url:" +url); ServletOutputStream out = response.getOutputStream(); //支付寶交易號 String alipayTradeNo=this.getNotNull("trade_no", request); //商戶訂單號 String outerTradeNo=this.getNotNull("out_trade_no", request); //買家支付寶用戶號 String buyerId=this.getNotNull("buyer_id", request); //買家支付寶帳號 String buyerLogonId=this.getNotNull("buyer_logon_id", request); //交易狀態 String tradeStatus=this.getNotNull("trade_status", request); //訂單金額,精確到小數點後2位 String money=getNotNull("total_amount", request); logger.info("[AlipayNotifyController][alipayNotify] tradeStatus:" +tradeStatus+" money:"+money); StringBuffer buf = new StringBuffer(); if (request.getMethod().equalsIgnoreCase("POST")) { Enumeration<String> em = request.getParameterNames(); for (; em.hasMoreElements();) { Object o = em.nextElement(); buf.append(o).append("=").append(request.getParameter(o.toString())).append(","); } logger.info("回調 method:post]http://" + request.getServerName() + request.getServletPath() + " [<prams:" + buf + ">]"); } else { buf.append(request.getQueryString()); logger.info("回調 method:get]http://" + request.getServerName() + request.getServletPath() + "?" + request.getQueryString()); } //檢驗支付寶參數 if(!verifyAlipay(request)) { out.print("fail"); return; } //交易成功 if("TRADE_SUCCESS".equalsIgnoreCase(tradeStatus)) { try { if(StringUtils.isNotBlank(outerTradeNo)) { //查詢當前訂單信息 OrderInfo info=this.orderInfoService.getOrderInfoByOrderNumber(outerTradeNo); if(info==null) { logger.error("[AlipayNotifyController][alipayNotify] info:" +info); out.print("fail"); return; } //訂單信息 OrderInfo orderInfo=new OrderInfo(); orderInfo.setOrderNumber(outerTradeNo); orderInfo.setCardCode("alipay"); orderInfo.setCardName("支付寶"); orderInfo.setCardNumber(buyerLogonId); orderInfo.setCardOwner(buyerId); orderInfo.setPayTime(new Date()); orderInfo.setOuterTradeNo(alipayTradeNo); orderInfo.setPayStatus(PayStatus.PAY_SUCCESS); orderInfo.setTradeStatus(TradeStatus.TRADE_PROGRESS); orderInfo.setPayAmount(new BigDecimal(money)); orderInfo.setBuyerId(info.getBuyerId()); //付款日誌 PayLogs payLogs=new PayLogs(); payLogs.setOrderNumber(outerTradeNo); payLogs.setOuterTradeNo(alipayTradeNo); payLogs.setStatus(PAY_LOGS_SUCCESS); orderInfoService.payReturn(orderInfo,payLogs); out.print("success"); return; } }catch(Exception ex) { logger.error("[AlipayNotifyController][payReturn] 出現了異常:",ex); out.print("success"); return; } }else { out.print("fail"); return; } } /** * 檢驗支付寶 * @param request * @return */ @SuppressWarnings("rawtypes") public boolean verifyAlipay(HttpServletRequest request) { boolean flag=true; // 獲取支付寶POST過來反饋信息 Map<String, String> params = new HashMap<String, String>(); Map requestParams = request.getParameterMap(); for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) { String name = (String) iter.next(); String[] values = (String[]) requestParams.get(name); String valueStr = ""; for (int i = 0; i < values.length; i++) { valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ","; } params.put(name, valueStr); } try { flag= AlipaySignature.rsaCheckV1(params, ALIPAYPUBLICKEY, CHARSET, "RSA2"); } catch (AlipayApiException e) { e.printStackTrace(); flag=false; } return flag; } }
APP支付成功後,其實支付寶或者微信都會告訴你是否成功,只是這種通知是不可靠的,最可靠的的一種方式是支付寶或者微信的服務端回調。
根據回調處理相關的業務。
相應的微信回調代碼以下:(注意,這個業務模塊屬於買家。)
APP調用微信:
/*** * APP端調用請求微信 */ @RestController @RequestMapping("/buyer") public class WeiXinController extends BaseController { private static final Logger logger = LoggerFactory.getLogger(WeiXinController.class); /**待支付*/ private static final int PAY_LOGS_READY=0; @Autowired private OrderInfoService orderInfoService; @Autowired private BuyerService buyerService; @Autowired private PayLogsService payLogsService; @Autowired private CouponReceiveService couponReceiveService; /** * APP端請求調用微信 * @param request * @param response * @return */ @RequestMapping(value="/weixin/invoke",method={RequestMethod.GET,RequestMethod.POST}) public JsonResult weixinInvoke(HttpServletRequest req, HttpServletResponse resp) { try { /**訂單號*/ String orderNumber=this.getNotNull("orderNumber", req); /**金額*/ String money=this.getNotNull("money", req); /**優惠券id*/ String couponReceiveId = req.getParameter("couponReceiveId"); if(StringUtils.isBlank(orderNumber) || StringUtils.isBlank(money)) { return new JsonResult(JsonResultCode.FAILURE,"請求參數有誤,請稍後重試",""); } //對比金額 OrderInfo orderInfo=this.orderInfoService.getOrderInfoByOrderNumber(orderNumber); if(orderInfo==null) { return new JsonResult(JsonResultCode.FAILURE,"訂單號不存在,請稍後重試",""); } //獲取訂單的金額 BigDecimal orderAmount=orderInfo.getOrderAmount(); //減餘額 Long buyerId=orderInfo.getBuyerId(); Buyer buyer=this.buyerService.getBuyerById(buyerId); //用戶餘額 BigDecimal balanceMoney=buyer.getBalanceMoney(); //計算最終須要給微信的金額 BigDecimal payAmount=orderAmount.subtract(balanceMoney); //買家支付時抵扣優惠券 if(StringUtils.isNotBlank(couponReceiveId)){ Long id = Long.parseLong(couponReceiveId); payAmount = couponReceiveService.deductionCouponMoney(id, orderNumber, payAmount); } logger.info("[WeiXinController][weixinInvoke] orderNumber:" +orderNumber +" money:" +money+" orderAmount:"+orderAmount+" balanceMoney:"+balanceMoney+" payAmount:" +payAmount); //考慮重複訂單的問題,會產生重複日誌 PayLogs payLogs=this.payLogsService.getPayLogsByOrderNumber(orderNumber); if(payLogs==null) { //建立訂單日誌 PayLogs logs=new PayLogs(); logs.setUserId(buyerId); logs.setOrderId(orderInfo.getOrderId()); logs.setOrderNumber(orderNumber); logs.setOrderAmount(payAmount); logs.setStatus(PAY_LOGS_READY); logs.setCreateTime(new Date()); int payLogsResult=payLogsService.addPayLogs(logs); logger.info("[WeiXinController][weixinInvoke] 建立訂單日誌結果:" + (payLogsResult>0)); }else { logger.info("[WeiXinController][weixinInvoke] 建立重複訂單"); } //微信開始 SortedMap<Object,Object> paramMap = new TreeMap<Object,Object>(); paramMap.put("appid", WeiXinUtil.APPID); paramMap.put("mch_id", WeiXinUtil.MCHID); String nonceStr=RandomUtil.generateString(8); // 隨機字符串 paramMap.put("nonce_str", nonceStr); paramMap.put("body","魔笛食材");// 商品描述 paramMap.put("out_trade_no", orderNumber);// 商戶訂單編號 paramMap.put("total_fee",Math.round(payAmount.doubleValue()*100)); //IP地址 String ip=IpUtils.getIpAddr(req); paramMap.put("spbill_create_ip",ip); paramMap.put("notify_url", WeiXinUtil.NOTIFYURL);// 回調地址 paramMap.put("trade_type",WeiXinUtil.TRADETYPE);// 交易類型APP String sign=createSign(paramMap); paramMap.put("sign", sign);// 數字簽證 logger.info("weixin支付請求IP:" +ip+ " sign:" +sign); String xml = getRequestXML(paramMap); String content = HttpClientUtil.getInstance().sendHttpPost(WeiXinUtil.URL,xml,"UTF-8"); logger.info("weixin支付請求的內容content:" +content); JSONObject jsonObject = JSONObject.fromObject(XmltoJsonUtil.xml2JSON(content)); JSONObject resultXml = jsonObject.getJSONObject("xml"); //返回的編碼 JSONArray returnCodeArray =resultXml.getJSONArray("return_code"); //返回的消息 JSONArray returnMsgArray =resultXml.getJSONArray("return_msg"); //結果編碼 JSONArray resultCodeArray =resultXml.getJSONArray("result_code"); String returnCode= (String)returnCodeArray.get(0); String returnMsg= (String)returnMsgArray.get(0); String resultCode = (String)resultCodeArray.get(0); logger.info("[WeiXinController][weixinInvoke] returnCode: " +returnCode+" returnMsg:"+returnMsg +" resultCode:"+resultCode); if(resultCode.equalsIgnoreCase("FAIL")) { return new JsonResult(JsonResultCode.FAILURE,"微信統一訂單下單失敗",""); } if(resultCode.equalsIgnoreCase("SUCCESS")) { JSONArray prepayIdArray =resultXml.getJSONArray("prepay_id"); String prepayId= (String)prepayIdArray.get(0); WeiXinBean weixin=new WeiXinBean(); weixin.setAppid(WeiXinUtil.APPID); weixin.setPartnerid(WeiXinUtil.MCHID); weixin.setNoncestr(nonceStr); weixin.setPrepayid(prepayId); String timestamp=System.currentTimeMillis()/1000+""; weixin.setTimestamp(timestamp); //最終返回簽名 SortedMap<Object,Object> apiMap = new TreeMap<Object,Object>(); apiMap.put("appid", WeiXinUtil.APPID); apiMap.put("partnerid", WeiXinUtil.MCHID); apiMap.put("prepayid", prepayId); apiMap.put("package","Sign=WXPay"); apiMap.put("noncestr",nonceStr); apiMap.put("timestamp", timestamp); //再次簽名 weixin.setSign(createSign(apiMap)); return new JsonResult(JsonResultCode.SUCCESS,"微信統一訂單下單成功",weixin); } return new JsonResult(JsonResultCode.FAILURE,"操做失敗",""); } catch (Exception ex) { logger.error("[WeiXinController][weixinInvoke] exception:",ex); return new JsonResult(JsonResultCode.FAILURE,"系統錯誤,請稍後重試",""); } } //拼接xml 請求路徑 @SuppressWarnings({"rawtypes"}) private String getRequestXML(SortedMap<Object, Object> parame){ StringBuffer buffer = new StringBuffer(); buffer.append("<xml>"); Set set = parame.entrySet(); Iterator iterator = set.iterator(); while(iterator.hasNext()){ Map.Entry entry = (Map.Entry) iterator.next(); String key =String.valueOf(entry.getKey()); String value = String.valueOf(entry.getValue()); //過濾相關字段sign if("sign".equalsIgnoreCase(key)){ buffer.append("<"+key+">"+"<![CDATA["+value+"]]>"+"</"+key+">"); }else{ buffer.append("<"+key+">"+value+"</"+key+">"); } } buffer.append("</xml>"); return buffer.toString(); } //建立md5 數字簽證 @SuppressWarnings({"rawtypes"}) private String createSign(SortedMap<Object, Object> param){ StringBuffer buffer = new StringBuffer(); Set set = param.entrySet(); Iterator<?> iterator = set.iterator(); while(iterator.hasNext()){ Map.Entry entry = (Map.Entry) iterator.next(); String key = String.valueOf(entry.getKey()); Object value =String.valueOf(entry.getValue()); if(null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)){ buffer.append(key+"="+value+"&"); } } buffer.append("key="+WeiXinUtil.APIKEY); String sign =EncryptUtil.getMD5(buffer.toString()).toUpperCase(); return sign; } }
微信回調接口:
/** * weixin 微信服務端回調 */ @Controller @RequestMapping("/buyer") public class WeiXinNotifyController extends BaseController { private static final Logger logger = LoggerFactory.getLogger(WeiXinNotifyController.class); /**支付成功*/ private static final int PAY_LOGS_SUCCESS=1; @Autowired private OrderInfoService orderInfoService; @Autowired private CouponReceiveService couponReceiveService; @Autowired private GroupsService groupsService; @RequestMapping(value = "/weixin/notify", method = { RequestMethod.GET, RequestMethod.POST }) public void weixinNotify(HttpServletRequest request, HttpServletResponse response) throws IOException { String url=request.getRequestURL().toString(); logger.info("WeiXinNotifyController.weixinNotify.start-->url:" +url); try { StringBuffer buf = new StringBuffer(); if (request.getMethod().equalsIgnoreCase("POST")) { Enumeration<String> em = request.getParameterNames(); for (; em.hasMoreElements();) { Object o = em.nextElement(); buf.append(o).append("=").append(request.getParameter(o.toString())).append(","); } logger.info("回調 method:post]http://" + request.getServerName() + request.getServletPath() + " [<prams:" + buf + ">]"); } else { buf.append(request.getQueryString()); logger.info("回調 method:get]http://" + request.getServerName() + request.getServletPath() + "?" + request.getQueryString()); } request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); response.setHeader("Access-Control-Allow-Origin", "*"); InputStream in=request.getInputStream(); ByteArrayOutputStream out=new ByteArrayOutputStream(); byte[] buffer =new byte[1024]; int len=0; while((len=in.read(buffer))!=-1){ out.write(buffer, 0, len); } out.close(); in.close(); String content=new String(out.toByteArray(),"utf-8");//xml數據 logger.info("[WeiXinNotifyController][weixinNotify] content:" +content); //日誌顯示,存在爲空的狀況. if(StringUtils.isBlank(content)) { logger.error("[WeiXinNotifyController][weixinNotify] content is blank,please check it"); response.getWriter().write(setXml("FAIL", "ERROR")); return; } JSONObject jsonObject = JSONObject.fromObject(XmltoJsonUtil.xml2JSON(content)); JSONObject resultXml = jsonObject.getJSONObject("xml"); JSONArray returnCode = resultXml.getJSONArray("return_code"); String code = (String)returnCode.get(0); if(code.equalsIgnoreCase("FAIL")) { response.getWriter().write(setXml("SUCCESS", "OK")); return; }else if(code.equalsIgnoreCase("SUCCESS")) { //商戶訂單號即訂單編號 String outerTradeNo =String.valueOf(resultXml.getJSONArray("out_trade_no").get(0)); //微信交易訂單號 String tradeNo = String.valueOf(resultXml.getJSONArray("transaction_id").get(0)); //交易狀態 String resultCode = String.valueOf(resultXml.getJSONArray("result_code").get(0)); //金額 String money =String.valueOf(resultXml.getJSONArray("total_fee").get(0)); //微信的用戶ID String openId =String.valueOf(resultXml.getJSONArray("openid").get(0)); logger.info("[WeiXinNotifyController][weixinNotify] resultCode:" +resultCode+" money:"+money); //根據這個判斷來獲取訂單交易是否OK if(!resultCode.equalsIgnoreCase("SUCCESS")) { response.getWriter().write(setXml("FAIL", "ERROR")); return; } try { if(StringUtils.isNotBlank(outerTradeNo)) { //查詢當前訂單信息 OrderInfo info=this.orderInfoService.getOrderInfoByOrderNumber(outerTradeNo); if(info==null) { logger.error("[WeiXinNotifyController][weixinNotify] info:" +info); response.getWriter().write(setXml("FAIL", "ERROR")); return; } //訂單信息 OrderInfo orderInfo=new OrderInfo(); orderInfo.setOrderNumber(outerTradeNo); orderInfo.setCardCode("weixin"); orderInfo.setCardName("微信支付"); orderInfo.setCardNumber(openId); orderInfo.setCardOwner(openId); orderInfo.setPayTime(new Date()); orderInfo.setOuterTradeNo(tradeNo); orderInfo.setPayStatus(PayStatus.PAY_SUCCESS); orderInfo.setTradeStatus(TradeStatus.TRADE_PROGRESS); orderInfo.setPayAmount(new BigDecimal(money).divide(new BigDecimal(100))); orderInfo.setBuyerId(info.getBuyerId()); //付款日誌 PayLogs payLogs=new PayLogs(); payLogs.setOrderNumber(outerTradeNo); payLogs.setOuterTradeNo(tradeNo); payLogs.setStatus(PAY_LOGS_SUCCESS); orderInfoService.payReturn(orderInfo,payLogs); //回寫團購成功狀態 groupsService.updateGbStatusByOrderNumber(outerTradeNo); response.getWriter().write(setXml("SUCCESS", "OK")); try { //更新優惠券狀態爲已用 couponReceiveService.updateStatus(outerTradeNo); }catch(Exception ex){ logger.error("[AlipayNotifyController][payReturn] 更新優惠券狀態異常:",ex); } return; } }catch(Exception ex) { logger.error("[WeiXinNotifyController][payReturn] 出現了異常:",ex); response.getWriter().write(setXml("SUCCESS", "OK")); return; } } }catch(Exception e){ logger.error("[WeiXinNotifyController][weixinNotify] exception:" ,e); response.getWriter().write(setXml("SUCCESS", "OK")); return; } } //返回微信服務 private String setXml(String returnCode,String returnMsg) { return "<xml><return_code><![CDATA["+returnCode+"]]></return_code><return_msg><![CDATA["+returnMsg+"]]></return_msg></xml>"; } }
Java開源生鮮電商平臺-支付表的設計與架構(源碼可下載),若是須要下載的話,能夠在個人github下面進行下載。
相關的運營截圖以下:
聯繫QQ:137071249
QQ羣:793305035