Integración de WeChat Pay Score (Pagar Después) con Java

Registro de integración de WeChat Pay Score

Pasos para integrar WeChat Pay Score

  1. Completar el formulario de solicitud: Este paso requiere aproximadamente 1-3 días hábiles para la revisión.
  2. Obtener credenciales: Necesitarás serviceId, appid, appSecret, clave API (para firmas) y el archivo de certificado apiclient_cert.xxx (para reembolsos).
  3. Revisar documentación oficial.

Flujo de integración para casos de uso (ej: transporte)

1. Usuario realiza un pedido

El usuario realiza un pedido en el lado del comerciante. Si no puede usar el servicio, se muestra una página para activar WeChat Pay Score.

2. Verificar si el usuario puede usar el servicio

  • Interfaz: Consulta de estado de servicio del usuario
  • Documento:【Pagar Después - Activar servicio de Pay Score】

3.1 Crear orden

  • Interfaz: Crear orden de Pagar Después
  • Documento:【Pagar Después - Crear orden】

3.2 Guía para activar servicio

  • Interfaz: Redirección a miniprograma (activar servicio)
  • Documento:【Pagar Después - Guía para activar】

4. Inicio del servicio

Después de crear la orden, si el usuario activa el servicio exitosamente y pasa la evaluación de riesgo, se recibirá una notificación asíncrona de WeChat Pay.

  • Interfaz: Notificación de confirmación de orden del usuario
  • Documento:【Pagar Después - Confirmación de orden】

5. Fin del servicio

El usuario completa el servicio.

6. Solicitar finalización de orden

El comerciante solicita finalizar la orden y realizar el cobro.

  • Interfaz: Finalizar orden de Pagar Después
  • Documento:【Pagar Después - Finalizar orden】

7. Cobro exitoso

WeChat Pay completa el cobro y notifica al comerciante.

  • Interfaz: Notificación de cobro exitoso
  • Documento:【Pagar Después - Callback de cobro】

Documentación importante de firmas

Las reglas de firma se encuentran en: https://wechatpay-api.gitbook.io/wechatpay-api-v3/

Proceso de integración en Java

Paso 1: Revisar documentación

Consultar: https://wechatpay-api.gitbook.io/wechatpay-api-v3/

Paso 2: Actualización a certificado V3

WeChat Pay API v3 requiere certificdaos de CA de terceros. Los comerciantes nuevos deben obtener el certificado API. Los comerciantes existentes deben seguir la guía de actualización.

Nota: La actualización del certificado API no afecta la clave API existente. Solo reemplazar el archivo apiclient_cert.p12.

Paso 3: Obtener archivos de certificado y clave

Paso 4: Agregar dependencia Maven

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.1.4-SNAPSHOT</version>
</dependency>

Paso 5: Configuración de parámetros

// ID de aplicación proporcionado por WeChat Pay
private String appId;

// Secreto de la aplicación
private String appSecret;

// Número de comerciante
private String merchantId;

// Clave API de 32 caracteres para firmar pagos
private String partnerKey;

// OpenID del usuario en WeChat
private String openId;

// URL de callback asíncrono (puerto 80, sin parámetros)
private String notifyUrl;

// URL de retorno síncrono (puerto 80, sin parámetros)
private String returnUrl;

// Ruta del certificado apiclient_cert.p12
private String certPath;

// ID de servicio proporcionado por WeChat Pay Score
private String serviceId;

// Contenido de la clave privada apiclient_key.pem
private String privateKey;

// Contenido del certificado apiclient_cert.pem
private String certificate;

// Clave APIv3 de 32 caracteres
private String aesKey = "xxx";

// Número de serie del certificado del comerciante
private String merchantSerialNumber = "xxxxx";

Paso 6: Llamadas a la API de Pay Score

Ejemplo: Consultar estado de servicio del usuario

private static final String USER_SERVICE_STATE_URL = 
    "https://api.mch.weixin.qq.com/payscore/user-service-state";

public JSONObject queryUserServiceState(String serviceId, String appId, String openId) {
    try {
        // Cargar clave privada
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
            new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
        
        // Construir cliente HTTP
        CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
            .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
            .withValidator(response -> true)
            .build();
        
        // Construir URL con parámetros
        URIBuilder uriBuilder = new URIBuilder(USER_SERVICE_STATE_URL);
        uriBuilder.setParameter("service_id", serviceId);
        uriBuilder.setParameter("appid", appId);
        uriBuilder.setParameter("openid", openId);
        
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        httpGet.addHeader("Accept", "application/json");
        
        CloseableHttpResponse response = httpClient.execute(httpGet);
        
        try {
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                String result = EntityUtils.toString(response.getEntity());
                return JSONObject.parseObject(result);
            } else {
                String result = EntityUtils.toString(response.getEntity());
                log.info("Respuesta: url={} resultado={}", uriBuilder.build(), result);
            }
        } finally {
            if (response != null) {
                response.close();
            }
        }
    } catch (Exception e) {
        log.error("Error en solicitud: ", e);
    }
    return null;
}

Ejemplo: Crear orden de Pay Score

private static final String PAYSCORE_PAYAFTER_ORDERS_URL = 
    "https://api.mch.weixin.qq.com/v3/payscore/payafter-orders";

public JSONObject createPayAfterOrder(PayAfterOrderRequest request) {
    try {
        // Configurar modelo de orden
        PayAfterOrderRequest orderModel = PayAfterOrderRequest.builder()
            .appid(appId)
            .needUserConfirm(false)
            .openid(request.getOpenId())
            .riskAmount(request.getRiskAmount())
            .outOrderNo(request.getOrderId())
            .serviceStartTime(formatDate(request.getStartTime()))
            .serviceStartLocation(subString(request.getStartLocation(), 20))
            .serviceIntroduction(request.getServiceDesc())
            .serviceId(serviceId)
            .build();
        
        // Agregar información de tarifas
        List<FeeDetail> feeList = new ArrayList<>();
        FeeDetail fee = new FeeDetail();
        fee.setFeeName("Tarifa de servicio");
        fee.setFeeAmount(request.getAmount());
        fee.setFeeDesc("Descripción del servicio");
        feeList.add(fee);
        orderModel.setFees(feeList);
        
        // Cargar clave privada
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
            new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
        
        // Construir cliente HTTP
        CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
            .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
            .withValidator(response -> true)
            .build();
        
        // Realizar solicitud POST
        HttpPost httpPost = new HttpPost(PAYSCORE_PAYAFTER_ORDERS_URL);
        StringEntity reqEntity = new StringEntity(
            JSONObject.toJSONString(orderModel),
            ContentType.create("application/json", StandardCharsets.UTF_8));
        httpPost.setEntity(reqEntity);
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-Type", "application/json");
        
        CloseableHttpResponse response = httpClient.execute(httpPost);
        
        try {
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                String result = EntityUtils.toString(response.getEntity());
                return JSONObject.parseObject(result);
            } else {
                String result = EntityUtils.toString(response.getEntity());
                log.info("Resultado: url={} respuesta={}", 
                    PAYSCORE_PAYAFTER_ORDERS_URL, result);
            }
        } finally {
            response.close();
        }
    } catch (Exception e) {
        log.error("Error al crear orden: ", e);
    }
    return null;
}

Nota sobre finalización de orden

Al solicitar la finaliazción de la orden, se requiere el campo finish_ticket que solo está disponible después de que el usuario confirma la orden. Para órdenes sin confirmación, se debe consultar los detalles de la orden después de crearla.

Paso 7: Manejo de callbacks

@Override
public ResultModel handlePaymentCallback(HttpServletRequest request) {
    try {
        String bodyContent = getRequestBody(request);
        log.info("Callback contenido: {}", bodyContent);
        
        if (StringUtils.isBlank(bodyContent)) {
            return ResultModel.error("Cuerpo vacío");
        }
        
        JSONObject jsonObject = JSONObject.parseObject(bodyContent);
        
        if (!jsonObject.containsKey("resource")) {
            return ResultModel.error("Campo resource no encontrado");
        }
        
        JSONObject resource = jsonObject.getJSONObject("resource");
        
        // Descifrar datos usando AES-GCM
        String decryptedData = decryptAesGcm(
            resource.getString("associated_data"),
            resource.getString("nonce"),
            resource.getString("ciphertext")
        );
        
        JSONObject paymentData = JSONObject.parseObject(decryptedData);
        
        if (paymentData.containsKey("out_request_no") && 
            paymentData.containsKey("state") && 
            paymentData.containsKey("pay_succ_time")) {
            
            String outRequestNo = paymentData.getString("out_request_no");
            
            if ("USER_PAID".equals(paymentData.getString("state"))) {
                // Procesar pago exitoso
                return ResultModel.success();
            } else {
                return ResultModel.error("Pago fallido");
            }
        }
        
        return ResultModel.error("Formato de datos inválido");
    } catch (Exception e) {
        log.error("Error en callback: ", e);
        return ResultModel.error("Error al parsear: " + e.getMessage());
    }
}

@RequestMapping(value = "payscore/paymentCallback", method = RequestMethod.POST)
public void handleCallback(HttpServletRequest request, HttpServletResponse response) {
    Map<String, String> resultMap = new HashMap<>();
    
    try {
        ResultModel result = paymentService.handlePaymentCallback(request);
        
        if (result.isSuccess()) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            resultMap.put("message", result.getMessage());
            resultMap.put("code", result.getCode().toString());
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    } catch (Exception e) {
        log.error("Error en callback de pago: ", e);
        resultMap.put("code", "999");
        resultMap.put("message", "Error: " + e.getMessage());
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
    
    try {
        response.setContentType("application/json;charset=UTF-8");
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        
        String jsonResponse = JSONObject.toJSONString(resultMap);
        response.getWriter().write(jsonResponse);
        response.getWriter().flush();
    } catch (IOException e) {
        log.error("Error al escribir respuesta: ", e);
    }
}

Método de descifrado AES-GCM

private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int NONCE_LENGTH_BYTE = 12;
private static final String API_V3_KEY = "xxxx";

public static String decryptAesGcm(String aad, String nonce, String cipherText) throws Exception {
    // Habilitar política de cifrado ilimitado
    Security.setProperty("crypto.policy", "unlimited");
    
    final Cipher cipher = Cipher.getInstance(ALGORITHM, "SunJCE");
    SecretKeySpec key = new SecretKeySpec(API_V3_KEY.getBytes(StandardCharsets.UTF_8), "AES");
    
    GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce.getBytes(StandardCharsets.UTF_8));
    cipher.init(Cipher.DECRYPT_MODE, key, spec);
    cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
    
    byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
    return new String(decrypted, StandardCharsets.UTF_8);
}

Notas importantes

Error InvalidKeyException: Illegal key size

Por restricciones legales de EE.UU., las versiones antiguas de Java no soportan claves AES de 256 bits. Soluciones:

  • Actualizar Java 8 a versión 162 o superior (recomendado)
  • Java 8u151-8u152: Agregar Security.setProperty("crypto.policy", "unlimited");
  • Descargar e instalar policy files de jurisdicción ilimitada
  • Java 9 y superiores no tienen esta restricción

Recursos de soporte

Para problemas técnicos, consultar:

Nota: El soporte telefónico generalmente no está disponible.

Etiquetas: wechat-pay java payment-integration api-v3 aes-gcm

Publicado el 7-12 23:37