entorno de desarrollo
- java1.8
- experto 3.3.9
- springboot 2.1.3.LIBERAR
Paso 1: Abrir pago JSAPI
Paso 2: Acoplamiento de la tecnología SpringBoot
Primer vistazo al proceso de pago de WeChat
La principal interacción entre el sistema comercial y el sistema de pago WeChat:
1. Llame a la interfaz de inicio de sesión en el applet para obtener el ID abierto del usuario. Para la API, consulte la API pública 【API de inicio de sesión de programa pequeño】
2. El servidor comercial llama al pago para realizar un pedido de forma unificada. Para la API, consulte la API pública 【API de pedidos unificados】
3. Llamadas del servidor comercial para volver a firmar, api ver api pública [ re-firmar ]
4. El servidor del comerciante recibe una notificación de pago, api ve la api pública 【API de notificación de resultados de pago】
5. Resultado del pago de la consulta del servidor del comerciante, api ver api pública 【API de orden de consulta】
Tenga en cuenta que hay dos firmas arriba
1. Clase de archivo de configuración
1 2 3 public final class WxConfig { 4 public final static String appId="wxe86f60xxxxxxx"; // applet appid 5 public final static String mchId="15365xxxxx";// ID del comerciante 6 public final static String key="Ucsdfl782167bjslNC3skJD12986" // La clave acordada con el pago de WeChat 7 public final static String notificarPath="/admin/wxnotify"; // Dirección de devolución de llamada 8 public final static String payUrl="https://api.mch.weixin.qq.com/pay /unifiedorder" ; // Dirección de orden unificada 9 public final static String tradeType="JSAPI"; // Método de pago 10 11 }
2. Clase de herramienta Wechat, orden unificado, firma y generación de cadenas aleatorias. .
4 importar lombok.extern.slf4j.Slf4j; 5 importar org.apache.http.HttpEntity; 6 importar org.apache.http.HttpResponse; 7 importar org.apache.http.client.HttpClient; 8 importar org.apache.http.client.config.RequestConfig; 9 importar org.apache.http.client.methods.HttpPost; 10 importar org.apache.http.config.RegistryBuilder; 11 import org.apache.http.conn.socket.ConnectionSocketFactory; 12 import org.apache.http.conn.socket.PlainConnectionSocketFactory; 13 importar org.apache.http.conn.ssl.SSLConnectionSocketFactory; 14 importar org.apache.http.entity.StringEntity; 15 import org.apache.http.impl.client.HttpClientBuilder; 16 importar org.apache.http.impl.conn.BasicHttpClientConnectionManager; 17 importar org.apache.http.util.EntityUtils; 18 importar org.slf4j.Logger; 19 importar org.slf4j.LoggerFactory; 20 importar org.w3c.dom.Documento; 21 importar org.w3c.dom.Element; 22 importar org.w3c.dom.Node; 23 importar org.w3c.dom.NodeList; 24 25 importar javax.crypto.Mac; 26 importar javax.crypto.spec.SecretKeySpec; 27 importar javax.xml.XMLConstants; 28 importar javax.xml.parsers.DocumentBuilder; 29 import javax.xml.parsers.DocumentBuilderFactory; 30 import javax.xml.parsers.ParserConfigurationException; 31 importar javax.xml.transform.OutputKeys; 34 importar javax.xml.transform.dom.DOMSource; 32 importar javax.xml.transform.Transformer; 33 importar javax.xml.transform.TransformerFactory; 35 importar javax.xml.transform.stream.StreamResult; 36 importar java.io.ByteArrayInputStream; 37 importar java.io.InputStream; 38 importar java.io.StringWriter; 39 import java.security.MessageDigest; 40 import java.security.SecureRandom; 41 import java.time.Instant; 42 import java.util.*; 43 44 @Slf4j 45 public class WxUtil { 46 private static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9"; 47 Private static final String USER_AGENT = WXPAYSDK_VERSION + 48 " (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System. 49 ") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion(); 51 privado estático final 50 String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 52 privado estático final Random RANDOM = new SecureRandom(); 53 // Interfaz de orden unificado 54 public static Map<String, String> unifiedOrder(Map<String, String> reqData) throws Exception { 55 // El formato del mapa para el método xml está por debajo de 56 String reqBody = mapToXml(reqData); 57 // El contenido del método de solicitud para iniciar una orden unificada está por debajo de 58 String responseBody = requestOnce(WxConfig.payUrl, reqBody); 59 // Convierte el resultado obtenido de formato xml a formato de mapa El contenido del método está en debajo 60 Map<String,String> response= processResponseXml(responseBody); 61 // Obtener prepayId 62 String prepayId = response.get("prepay_id"); 63 // ¿Por qué el parámetro de ensamblaje package_str es así? Debido a que la firma secundaria WeChat estipula dicho formato 64 String package_str = "prepay_id="+prepayId; 65 Map<String,String> payParameters = new HashMap<>(); 66 long epochSecond = Instant.now().getEpochSecond(); 67 payParameters.put("appId",WxConfig.appId); 68 payParameters.put("nonceStr", WxUtil.generateNonceStr()); 69 payParameters.put("paquete", package_str); 70 payParameters.put("signType" , SignType.MD5.name()); 71 payParameters.put("timeStamp", String.valueOf(epochSecond)); 72 // firma secundaria 73 payParameters.put("paySign", WxUtil.generateSignature(payParameters, WxConfig.key, SignType.MD5)); 75 return payParameters; 76 } 77 78 79 /** 80 * Genera una firma. Tenga en cuenta que si el campo sign_type es incluido, debe ser coherente con el parámetro signType. 81 * 82 * @param data datos que se firmarán 83 * @param key API key 84 * @param signType método de firma 85 * @return signature 86 */ 87 public static String generateSignature(final Map<String, String> data, String key , SignType signType) arroja una excepción { 88 Set<String> keySet = data.keySet(); 89 String[] keyArray = keySet.toArray(new String[keySet.size()]); 90 Arrays.sort(keyArray); 91 StringBuilder sb = nuevo StringBuilder(); 92 for (String k : keyArray) { 93 if (k.equals("sign")) { 94 continue; 95 } 96 if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名 104 return HMACSHA256(sb. 97 sb.append(k).append("=").append(data.get(k).trim()).append("&"); 98 } 99 sb.append("clave=").append(clave); 100 if (SignType.MD5.equals(signType)) { 101 return MD5(sb.toString()).toUpperCase(); 102 } 103 else if (SignType.HMACSHA256.equals(signType)) { 105 } 106 else { 107 throw new Exception(String.format("Invalid sign_type: %s", signType)); 108 } 109 } 110 111 /** 112 * 生成 MD5 113 * 114 * @param data 待处理数据 115 * @return MD5结果 116 */ 117 private static String MD5(String data) throws Exception { 118 MessageDigest md = MessageDigest.getInstance("MD5"); matriz de 119 bytes[] = md.digest(data.getBytes("UTF-8")); 120 StringBuilder sb = nuevo StringBuilder(); 121 para (elemento de byte: matriz) { 122 sb.append(Integer.toHexString((elemento & 0xFF) | 0x100).subcadena(1, 3)); 123 } 124 return sb.toString().toUpperCase(); 127 public static String generarNonceStr() { 128 char[] nonceChars = new char[32]; 129 for (int index = 0; index < nonceChars.length; ++index) { 130 nonceChars[índice] = SÍMBOLOS.charAt(ALEATORIO.nextInt(SÍMBOLOS.longitud())); 131 } 132 devuelve una nueva cadena (nonceChars); 133 } 134 135 public static String mapToXml(Map<String, String> data) throws Exception { 136 Documento documento = nuevoDocumento(); 137 Raíz del elemento = document.createElement("xml"); 138 documento.appendChild(raíz); 139 for (Clave de cadena: data.keySet()) { 140 Valor de cadena = data.get(key); 141 si (valor == nulo) { 142 valor = ""; 143 } 144 valor = valor.trim(); 145 Elemento archivado = documento.createElement(clave); 146 archivado.appendChild(document.createTextNode(valor)); 147 root.appendChild(archivado); 148 } 149 TransformerFactory tf = TransformerFactory.newInstance(); 150 Transformador transformador = tf.newTransformer(); 151 fuente DOMSource = new DOMSource(documento); 152 transformador.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 153 transformador.setOutputProperty(OutputKeys.INDENT, "sí"); 154 StringWriter escritor = new StringWriter(); 155 resultado de StreamResult = new StreamResult(escritor); 156 transformador.transformar(fuente, resultado); 157 Cadena de salida = escritor.getBuffer().toString(); //.reemplazarTodo("\n|\r", ""); 158 prueba { 159 escritor.cerrar(); 160 } 161 catch (Excepción ex) { 162 } 163 return salida; 164 } 165 166 // 判断签名是否有效 167 private static Map<String, String> processResponseXml(String xmlStr) throws Exception { 168 String RETURN_CODE = "return_code"; 169 Cadena código_retorno; 170 Mapa<Cadena, Cadena> respData = xmlToMap(xmlStr); 171 if (respData.containsKey(RETURN_CODE)) { 172 return_code = respData.get(RETURN_CODE); 173 } 175 else { 176 throw new Exception(String.format("Sin `return_code` en XML: %s", xmlStr)); 177 } 178 179 if (return_code.equals("FAIL")) { 180 return respData; 181 } 182 else if (return_code.equals("SUCCESS")) { 183 if (isResponseSignatureValid(respData)) { 184 return respData; 185 } 186 else { 187 throw new Exception(String.format("Valor de signo no válido en XML: %s", xmlStr)); 188 } 189 } 190 más { 191 throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr)); 192 } 193 } 194 // 判断签名 195 booleano estático privado isResponseSignatureValid(Map<String, String> data) throws Exception { 196 String signKeyword = "sign"; 197 if (!data.containsKey(signKeyword) ) { 198 return false; 199 } 200 String sign = data.get(signKeyword); 201 return generateSignature(data, WxConfig.key, SignType.MD5).equals(sign); 202 } 203 204 // 发起一次请求 205 privado estático String requestOnce(String payUrl, String data) arroja una excepción { 206 BasicHttpClientConnectionManager connManager; 219 .build(); 207 connManager = new BasicHttpClientConnectionManager( 208 RegistryBuilder.<ConnectionSocketFactory>create() 209 .register("http", PlainConnectionSocketFactory.getSocketFactory()) 210 .register("https", SSLConnectionSocketFactory.getSocketFactory()) 211 .build(), 212 nulo, 213 nulo, 214 nulo 215); 216 217 HttpClient httpClient = HttpClientBuilder.create() 218 .setConnectionManager(connManager) 221 220 HttpPost httpPost = nuevo HttpPost(payUrl); 222 RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(8000).setConnectTimeout(6000).build(); 223 httpPost.setConfig(solicitudConfig); 224 225 StringEntity postEntity = new StringEntity(datos, "UTF-8"); 226 httpPost.addHeader("Tipo de contenido", "texto/xml"); 227 httpPost.addHeader("User-Agent", USER_AGENT + " " + WxConfig.mchId); 228 httpPost.setEntity(postEntity); 229 230 HttpResponse httpResponse = httpClient.execute(httpPost); 231 HttpEntity httpEntity = httpResponse.getEntity(); 232 devuelve EntityUtils.toString(httpEntity, "UTF-8"); 238 cadena estática privada HMACSHA256 (datos de cadena, clave de cadena) arroja una excepción { 239 Mac sha256_HMAC = Mac.getInstance ("HmacSHA256"); 240 SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); 241 sha256_HMAC.init(clave_secreta); matriz de 242 bytes[] = sha256_HMAC.doFinal(data.getBytes("UTF-8")); 243 StringBuilder sb = nuevo StringBuilder(); 244 para (elemento de byte: matriz) { 245 sb.append(Integer.toHexString((elemento & 0xFF) | 0x100).subcadena(1, 3)); 246 } 247 return sb.toString().toUpperCase(); 249 251 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 252 documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 253 documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); 254 documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 255 documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 256 documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 257 documentBuilderFactory.setXIncludeAware(falso); 258 documentBuilderFactory.setExpandEntityReferences(false); 259 260 return documentBuilderFactory.newDocumentBuilder(); 261 } 262 263 documento estático privado newDocument() throws ParserConfigurationException { 264 return newDocumentBuilder().newDocument(); 265 } 267 271 DocumentBuilder documentBuilder = newDocumentBuilder(); 267 268 public static Map<String, String> xmlToMap(String strXML) throws Exception { 269 try { 270 Map<String, String> data = new HashMap<String, String>(); 272 Flujo InputStream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); 273 org.w3c.dom.Document doc = documentBuilder.parse(flujo); 274 doc.getDocumentElement().normalize(); 275 NodeList nodeList = doc.getDocumentElement().getChildNodes(); 276 for (int idx = 0; idx < nodeList.getLength(); ++idx) { 277 Nodo nodo = nodeList.item(idx); 278 if (nodo.getNodeType() == Nodo.ELEMENT_NODE) { 279 org.w3c.dom.Element elemento = (org.w3c.dom.Element) nodo; 280 data.put(element.getNodeName(), element.getTextContent()); 281 } 282 } 283 prueba { 284 stream.close(); 285 } catch (excepción ex) { 286 // no hacer nada 287 } 288 devolver datos; 289 } catch (excepción ex) { 290 getLogger().warn("XML no válido, no se puede convertir a mapa. Mensaje de error: {}. Contenido XML: {}", ex .getMessage(), strXML); 291 throw ex; 292 } 293 294 } 295 /** 296 * 日志 297 * @return 298 */ 299 registrador estático privado getLogger() { 300 registrador registrador = LoggerFactory.getLogger("wxpay java sdk"); 301 devolver registrador ; 302 } 303 304 305 306 /** 307 * Determinar si la firma es correcta 308 * 309 * @param xmlStr Datos en formato XML 310 * @param key API key 311 * @return si la firma es correcta 312 * @throws Exception 313 */ 314 public static boolean isSignatureValid(String xmlStr, String key) throws Exception { 315 Map<String, String> data = xmlToMap(xmlStr ); 316 if (!data. containsKey("sign") ) { 317 return false; 318 } 319 Cadena signo = data.get("signo"); 320 return generateSignature(datos, clave,SignType.MD5).equals(signo); 321 } 322 323 324 325 326 327 }
3. El applet inicia una solicitud para ensamblar los parámetros necesarios para iniciar una orden unificada
1 @PostMapping("/recharge/wx") 2 public Map recharge(HttpServletRequest request, @RequestParam(value = "vipType",required = true) VipType vipType) throws Exception { 3 // Este caso se basa en Cambiar la situación real según sus propias necesidades 4 Integer loginDealerId = MySecurityUtil.getLoginDealerId(); 5 // Obtenga la dirección IP necesaria para iniciar un pedido unificado 6 String ipAddress = HttpUtil.getIpAddress(request); 7 // Genere un pedido prepago y guárdelo en el base de datos La devolución de llamada es exitosa y el estado del pedido se modifica 8 PrepaidOrder prepaidOrder = payService.recharge(loginDealerId, vipType, ipAddress); 9 // Ensamblar el mapa de datos requerido para la colocación unificada de pedidos 10 Map<String, String> stringStringMap = prepaidOrder.toWxPayParameters (); 11 // invocar pago unificado 12 Map<String, String> payParameters =WxUtil.unifiedOrder(stringStringMap); 13 parámetros de pago de retorno; 14 }
Generar código de pedido prepago (generado de acuerdo con las necesidades reales, aquí están mis necesidades, solo como referencia)
27 @Service("WXPayService") 28 @Slf4j 29 public class PayServiceImpl implements PayService { 30 33 @Resource 34 PrepaidOrderDao prepaidOrderDao; 35 36 @Recurso 37 VipDao vipDao; 38 39 @Resource 40 DealerDao dealerDao; 41 42 @Resource 43 ApplicationContext applicationContext; 44 @Override 45 @Transactional 46 public PrepaidOrder recarga(Integer dealerId, VipType vipType, String userIp) { 47 Dealer dealer = dealerDao.getDealerById(dealerId); 48 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); 49 Cadena nuevaFecha = sdf.format(nueva Fecha()); 50 Aleatorio aleatorio = nuevo Aleatorio(); 51 Cadena orderNumber = newDate + random.nextInt(1000000); 52 Cantidad BigDecimal = nulo; 53 // 如果不是生产环境 付一分钱 54 if (!applicationContext.getEnvironment().getActiveProfiles()[0].contains("prod")){ 55 cantidad = BigDecimal.valueOf(0.01); 56 }else if (vipType.equals(VipType.YEAR)){ 57 cantidad= BigDecimal.valueOf(999); 58 }else { 59 cantidad = BigDecimal.valueOf(365); 60 } 61 PrepaidOrder prepaidOrder = new PrepaidOrder(); 62 prepaidOrder.setDealerId(dealerId); 63 prepaidOrder.setOpenId(dealer.getOpenId()); // Este es el openid requerido por WeChat 64 prepaidOrder. setVipType(vipType); 65 prepaidOrder.setUserIp(userIp); // Este es el parámetro userIp requerido por WeChat 66 prepaidOrder.setOrderStatus(OrderStatus.EN CURSO); 67 prepaidOrder. setAmount(amount); // Este es el parámetro total_fee requerido por WeChat 68 prepaidOrder.setOrderNumber(orderNumber); // Este es el parámetro out_trade_no requerido por WeChat 69 // Agregar pedido prepago 70 prepaidOrderDao.addPrepaidOrder(prepaidOrder); 71 devolver pedido prepago;/ / devolver pedido prepago
72 } 73 }
Hacer la encapsulación de parámetros final en la clase de entidad
1 @Data 2 public class PrepaidOrder extends BaseModel { 3 private String orderNumber; 4 private Integer dealerId; 5 número privado entero versión; 6 monto BigDecimal privado; 7 privado OrderStatus orderStatus=OrderStatus.EN CURSO; 8 privado LocalDateTime SuccessTime; 9 IP de usuario de cadena privada; 10 cadena privada openId; 11 privado VipType vipType; 12 13 public Map<String, String> toWxPayParameters() throws Exception { 14 Map map = new HashMap(); 15 mapa.put("cuerpo",obtenerCuerpo()); // 商品名字 16 map.put("appid", WxConfig.appId); // 小程序appid 17 map.put("mch_id", WxConfig.mchId); // ID de usuario 18 map.put("nonce_str", WxUtil.generateNonceStr()); // cadena aleatoria 19 map.put("notify_url", AppConst.host+WxConfig.notifyPath); // dirección de devolución de llamada 20 map.put("openid " ,this.openId); // openid del usuario que inicia el pago de WeChat 21 map.put("out_trade_no",this.orderNumber); // número de pedido 22 map.put("spbill_create_ip",this.userIp); // La dirección IP del usuario que inicia el pago de WeChat 23 map.put("total_fee", parseAmount()); // cantidad (puntos de unidad) 24 map.put("trade_type",WxConfig.tradeType); // tipo de pago 25 // La firma de datos también es la primera firma 26 map.put("sign", WxUtil.generateSignature(map, WxConfig.key, SignType.MD5 )); 27 return map; 28 } 31 if (vipType.equals(VipType.YEAR)){ 29 30 public String getBody(){ 32 return "年度会员"; 33 }else { 34 return "季度会员"; 35 } 36 } 37 38 public String parseAmount(){ 39 BigDecimal multiplicar = cantidad.multiplicar(BigDecimal.valueOf(100)); 40 resultado BigDecimal = multiplicar; 41 if (multiply.compareTo(BigDecimal.valueOf(1))==0){ 42 resultado = BigDecimal.valueOf(1); 43 } 44 return resultado.toString(); 45 } 46 47 @Override 48 public String toString() { 49 return "PrepaidOrder{" + 50 "orderNumber='" + orderNumber + '\'' + 51 ", dealerId=" + dealerId + 52 ", versionNum=" + versionNum + 53 ", cantidad=" + cantidad + 54 ", orderStatus=" + orderStatus + 55 ", SuccessTime=" + SuccessTime + 56 ", userIp='" + userIp + '\'' + 57 ", openId='" + openId + '\'' + 58 ", vipType=" + vipType + 59 ' }'; 60 } 61 }
4. La clase de enumeración de tipo de firma public enum SignType { MD5, HMACSHA256 }
5. Obtener clase de herramienta de IP de usuario
1 public static String getIpAddress(HttpServletRequest request) { 2 String ip = request.getHeader("x-forwarded-for"); 3 if (ip == null || ip.length() == 0 || "desconocido".equalsIgnoreCase(ip)) { 4 ip = request.getHeader("Proxy-Client-IP"); 5 } 6 if (ip == null || ip.length() == 0 || "desconocido".equalsIgnoreCase(ip)) { 7 ip = request.getHeader("WL-Proxy-Client-IP"); 8 } 9 if (ip == null || ip.length() == 0 || "desconocido".equalsIgnoreCase(ip)) { 10 ip = request.getHeader("HTTP_CLIENT_IP"); 11 } 12 if (ip == nulo || ip.longitud() == 0 || " 13 ip = solicitud.getHeader(" 15 if (ip == null || ip.length() == 0 || "desconocido".equalsIgnoreCase(ip)) { 16 ip = request.getRemoteAddr(); 17 } 18 devolver ip; 19 }
El mini programa inicia el pago de WeChat -> el controlador obtiene la información necesaria del usuario -> el servicio genera el pedido prepago -> encapsulación de parámetros de clase de entidad -> WxUtil inicia el pedido unificado -> devolver el resultado
Me tomó 2 meses ordenar un conjunto de materiales técnicos de desarrollo de JAVA, cubriendo los conceptos básicos de Java, microservicios distribuidos y otros materiales técnicos principales, incluida la experiencia cara a cara de Dachang, notas de estudio, folletos de código fuente, combate real del proyecto y videos explicativos.
Espero poder ayudar a algunos amigos que quieran mejorar sus habilidades a través del autoaprendizaje, obtengan la información, escaneen el código y presten atención.
Recuerda estar atento a la cuenta oficial [ Brother Coding ]
Obtenga más materiales de aprendizaje