Registro de integración de WeChat Pay Score
Pasos para integrar WeChat Pay Score
- Completar el formulario de solicitud: Este paso requiere aproximadamente 1-3 días hábiles para la revisión.
- Obtener credenciales: Necesitarás serviceId, appid, appSecret, clave API (para firmas) y el archivo de certificado apiclient_cert.xxx (para reembolsos).
- 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:
- Foro de desarrollo WeChat Pay
- Correo: wepayTS@tencent.com (asunto: teléfono móvil + correo + problema)
- WeChat: WePayTS1, WePayTS2, WePayTS3, etc.
Nota: El soporte telefónico generalmente no está disponible.