对接微信JSAPI支付

– 微信支付 –



前提条件

  • 微信支付商户号:必须有一个有效的微信支付商户号。
  • 公众号或小程序:需要有一个经过认证的微信公众号或小程序,用于生成支付授权。
  • 微信支付API证书:用于在服务器端调用微信支付API时进行身份验证

引入依赖

<!--微信支付-->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.12</version>
</dependency>

编写配置文件

  • 这里默认已经获得微信支付需要的相关参数
  • 下面直接进行代码Coding阶段
wx:
  # 微信小程序appid
  app-id: wxcb25xxxxxxxxxxxx
  # 商户号
  mch-id: 16xxxxxxxx
  # 证书序列号
  mch-serial-no: 59BAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  # api密钥
  api-key: tonxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  # 回调接口地址(订单不做回调可以不用配)
  notify-url: https://15x.xx.xxx.xx/pay/payCallBack
  # 证书地址
  key-path: /apiclient_key.pem
  cert-path: /apiclient_cert.pem
  cert-p12-path: /apiclient_cert.p12
  msgDataFormat: JSON

编写配置WxProperties

/**
 * @Author:Ccoo
 * @Date:2024/3/21 20:40
 */
@Component
@ConfigurationProperties(prefix = "wx")
@Data
@ToString
public class WxProperties {
    /**
     * 设置微信小程序的appid
     */
    private String appId;

    /**
     * 微信支付分配的商户号
     */
    private String mchId;

    /**
     * 密钥文件的路径
     */
    private String keyPath;

    /**
     * 商户证书的路径
     */
    private String certPath;

    /**
     * 商户密钥,用于签名和验证
     */
    private String apiKey;

    /**
     * 接收微信支付异步通知的URL
     */
    private String notifyUrl;

    /**
     * 商户API证书序列号
     */
    private String mchSerialNo;

    /**
     * 消息格式,XML或者JSON
     */
    private String msgDataFormat;
}

编写配置WechatAutoConfiguration

import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import com.wechat.pay.java.core.Config;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.cert.*;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class WechatAutoConfiguration {


	private final WxProperties wxProperties;
	private final ResourceLoader resourceLoader;

	private static final String CLASS_PATH = "classpath:";

	/**
	 * 自动更新证书
	 *
	 * @return RSAAutoCertificateConfig
	 */
	@Bean
	public Config config() throws IOException {
		String merchantSerialNumber = getCertificateSerialNumber(wxProperties.getCertPath());
		String privateKey = readResourceAsString(wxProperties.getKeyPath());

		return new RSAAutoCertificateConfig.Builder()
				.merchantId(wxProperties.getMchId())
				.privateKey(privateKey)
				.merchantSerialNumber(merchantSerialNumber)
				.apiV3Key(wxProperties.getApiKey())
				.build();
	}

	/**
	 * 微信支付对象
	 *
	 * @param config Config
	 * @return JsapiServiceExtension
	 */
	@Bean
	public JsapiServiceExtension jsapiServiceExtension(Config config) {
		return new JsapiServiceExtension.Builder().config(config).build();
	}

	/**
	 * 微信回调对象
	 *
	 * @param config Config
	 * @return NotificationParser
	 */
	@Bean
	public NotificationParser notificationParser(Config config) {
		return new NotificationParser((NotificationConfig) config);
	}

	/**
	 * 读取私钥文件,将文件流读取成 string
	 *
	 * @param path 文件路径
	 * @return 文件内容
	 * @throws IOException
	 */
	private String readResourceAsString(String path) throws IOException {
		Resource resource = resourceLoader.getResource(CLASS_PATH + path);
		try (InputStream inputStream = resource.getInputStream();
		     BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
			StringBuilder stringBuilder = new StringBuilder();
			String line;
			while ((line = reader.readLine()) != null) {
				stringBuilder.append(line);
			}
			return stringBuilder.toString();
		}
	}

	/**
	 * 获取证书序列号
	 *
	 * @param certPath 证书路径
	 * @return 证书序列号
	 * @throws IOException
	 */
	private String getCertificateSerialNumber(String certPath) throws IOException {
		Resource resource = resourceLoader.getResource(CLASS_PATH + certPath);
		try (InputStream inputStream = resource.getInputStream()) {
			X509Certificate certificate = getCertificate(inputStream);
			return certificate.getSerialNumber().toString(16).toUpperCase();
		}
	}

	/**
	 * 获取证书,将文件流转成证书文件
	 *
	 * @param inputStream 证书文件流
	 * @return {@link X509Certificate} 证书对象
	 */
	public static X509Certificate getCertificate(InputStream inputStream) {
		try {
			CertificateFactory cf = CertificateFactory.getInstance("X509");
			X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
			cert.checkValidity();
			return cert;
		} catch (CertificateExpiredException e) {
			throw new RuntimeException("证书已过期", e);
		} catch (CertificateNotYetValidException e) {
			throw new RuntimeException("证书尚未生效", e);
		} catch (CertificateException e) {
			throw new RuntimeException("无效的证书", e);
		}
	}
}
  • 微信JSAPI支付官方文档

JSAPI下单 - JSAPI支付 | 微信支付商户文档中心


微信支付相关代码

Controller控制器代码
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author Ccoo
 * @since 2024-8-23
 */
@RestController
@RequestMapping("/wechat")
public class WeChatController {

	@Autowired
	private WeChatService wechatService;

	@ApiOperation("微信预支付")
	@PostMapping("/prePay")
	public R<?> createPreOrder(@RequestBody PrePayReq prePayReq) {
		return R.ok(wechatService.prepayWithRequest(prePayReq));
	}

	@ApiOperation("查询订单")
	@GetMapping("/queryOrder")
	public R<?> queryOrder(@RequestParam String outTradeNo) {
		return R.ok(wechatService.queryStatus(outTradeNo));
	}

	@ApiOperation("取消订单")
	@PostMapping("/closeOrder")
	public R<?> closeOrder(@RequestParam String outTradeNo) {
		//方法没有返回值,意味着成功时API返回204 No Content
		return R.ok(wechatService.closeOrder(outTradeNo));
	}
    
}


Service接口层代码
import com.itheima.mp.domain.R;
import com.itheima.mp.domain.wechat.PrePayReq;

/**
 * @author Ccoo
 * 2024/8/23
 */
public interface WeChatService {

	/**
	 * 微信预支付
	 * @param prePayReq 预支付请求
	 * @return 结果
	 */
	R<?> prepayWithRequest(PrePayReq prePayReq);

	/**
	 * 查询订单状态
	 * @param outTradeNo 订单号
	 * @return 结果
	 */
	R<?> queryStatus(String outTradeNo);

	/**
	 * 关闭订单
	 * @param outTradeNo 订单号
	 */
	R<?> closeOrder(String outTradeNo);

}


ServiceImpl实现层代码
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.security.SecureRandom;

/**
 * @author Ccoo
 * 2024/8/23
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatServiceImpl implements WeChatService {

	private final WxProperties wxProperties;
	private final JsapiServiceExtension jsapiService;

	/**
	 * 微信预支付
	 * @param prePayReq 预支付请求
	 * @return 结果
	 * 商户系统先调用该接口在微信支付服务后台生成预支付交易单,
	 * 返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
	 */
	@Override
	public R<?> prepayWithRequest(PrePayReq prePayReq) {

		// 判断参数是否为空
		if (prePayReq == null || prePayReq.getAmount() == null || prePayReq.getDescription() == null || prePayReq.getOpenid() == null) {
			return R.fail(HttpStatus.BAD_REQUEST, "参数错误");
		}

		PrepayRequest request = new PrepayRequest();
		String outTradeNo = System.currentTimeMillis() + String.format("%06d", new SecureRandom().nextInt(999999));

		// 1. 设置AppId
		request.setAppid(wxProperties.getAppId());
		// 2. 设置商户号
		request.setMchid(wxProperties.getMchId());
		// 3. 设置商品描述
		request.setDescription(prePayReq.getDescription());
		// 4. 设置商户订单号
		request.setOutTradeNo(outTradeNo);
		// 5. 设置支付回调地址
		request.setNotifyUrl(wxProperties.getNotifyUrl());
		// 6. 设置金额
		Amount amount = new Amount();
		amount.setTotal(prePayReq.getAmount());
		amount.setCurrency("CNY");
		request.setAmount(amount);
		// 7. 设置支付者信息
		Payer payer = new Payer();
		payer.setOpenid(prePayReq.getOpenid());
		request.setPayer(payer);

		try {
			// 请求微信服务器JSAPI下单
			PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(request);
			return R.ok(response);
		} catch (ServiceException e) {
			return R.fail(e.getHttpStatusCode(), e.getErrorMessage());
		}

	}

	/**
	 * 查询订单
	 * @param outTradeNo 订单号
	 * @return 结果
	 * 商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。
	 * 查询订单可通过微信支付订单号 (opens new window)和商户订单号 (opens new window)两种方式查询。
	 * 需要调用查询接口的情况:
	 * 1.当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知。
	 * 2.调用支付接口后,返回系统错误或未知交易状态情况。
	 * 3.调用付款码支付API,返回USERPAYING的状态。
	 * 4.调用关单或撤销接口API之前,需确认支付状态。
	 */
	@Override
	public R<?> queryStatus(String outTradeNo) {

		// 判断参数是否为空
		if (outTradeNo == null || outTradeNo.isEmpty()) {
			return R.fail(HttpStatus.BAD_REQUEST, "参数错误");
		}

		QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
		// 1. 设置商户订单号
		request.setOutTradeNo(outTradeNo);
		// 2. 设置商户号
		request.setMchid(wxProperties.getMchId());

		try {
			// 请求微信服务器查询订单
			Transaction response = jsapiService.queryOrderByOutTradeNo(request);
			return R.ok(response);
		} catch (ServiceException e) {
			return R.fail(e.getHttpStatusCode(), e.getErrorMessage());
		}

	}

	/**
	 * 取消订单
	 * @param outTradeNo 订单号
	 * 关闭订单,以下情况需要调用关单接口:
	 * 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
	 * 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
	 */
	@Override
	public R<?> closeOrder(String outTradeNo) {
		// 判断参数是否为空
		if (outTradeNo == null || outTradeNo.isEmpty()) {
			return R.fail(HttpStatus.BAD_REQUEST, "参数错误");
		}

		CloseOrderRequest request = new CloseOrderRequest();
		// 1. 设置商户订单号
		request.setOutTradeNo(outTradeNo);
		// 2. 设置商户号
		request.setMchid(wxProperties.getMchId());

		try {
			// 请求微信服务器关闭订单
			jsapiService.closeOrder(request);
			return R.ok();
		} catch (ServiceException e) {
			return R.fail(e.getHttpStatusCode(), e.getErrorMessage());
		}

	}

}


部分实体类

统一响应实体类
package com.itheima.mp.domain;

import java.io.Serializable;

/**
 * 响应信息主体
 *
 * @author Ccoo
 */
public class R<T> implements Serializable
{
    private static final long serialVersionUID = 1L;

    /** 成功 */
    public static final int SUCCESS = 200;

    /** 失败 */
    public static final int FAIL = 500;

    private int code;

    private String msg;

    private T data;

    public static <T> R<T> ok()
    {
        return restResult(null, SUCCESS, "操作成功");
    }
    public static <T> R<T> ok(String msg)
    {
        return restResult(null, SUCCESS, msg);
    }

    public static <T> R<T> ok(T data)
    {
        return restResult(data, SUCCESS, "操作成功");
    }

    public static <T> R<T> ok(T data, String msg)
    {
        return restResult(data, SUCCESS, msg);
    }

    public static <T> R<T> fail()
    {
        return restResult(null, FAIL, "操作失败");
    }

    public static <T> R<T> fail(String msg)
    {
        return restResult(null, FAIL, msg);
    }

    public static <T> R<T> fail(T data)
    {
        return restResult(data, FAIL, "操作失败");
    }

    public static <T> R<T> fail(T data, String msg)
    {
        return restResult(data, FAIL, msg);
    }

    public static <T> R<T> fail(int code, String msg)
    {
        return restResult(null, code, msg);
    }

    private static <T> R<T> restResult(T data, int code, String msg)
    {
        R<T> apiResult = new R<>();
        apiResult.setCode(code);
        apiResult.setData(data);
        apiResult.setMsg(msg);
        return apiResult;
    }

    public int getCode()
    {
        return code;
    }

    public void setCode(int code)
    {
        this.code = code;
    }

    public String getMsg()
    {
        return msg;
    }

    public void setMsg(String msg)
    {
        this.msg = msg;
    }

    public T getData()
    {
        return data;
    }

    public void setData(T data)
    {
        this.data = data;
    }
}


预支付订单请求实体类
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @author Ccoo
 * 2024/8/24
 */
@Data
@ApiModel(description = "订单预支付实体类")
public class PrePayReq {

	@ApiModelProperty("商品描述")
	private String description;

	@ApiModelProperty("订单金额")
	private Integer amount;

	@ApiModelProperty("用户标识")
	private String openid;

}


响应状态码枚举类
/**
 * 返回状态码
 * 
 * @author ruoyi
 */
public class HttpStatus
{
    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    /**
     * 对象创建成功
     */
    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    /**
     * 操作已经执行成功,但是没有返回数据
     */
    public static final int NO_CONTENT = 204;

    /**
     * 资源已被移除
     */
    public static final int MOVED_PERM = 301;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 资源没有被修改
     */
    public static final int NOT_MODIFIED = 304;

    /**
     * 参数列表错误(缺少,格式不匹配)
     */
    public static final int BAD_REQUEST = 400;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限,授权过期
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    /**
     * 不允许的http方法
     */
    public static final int BAD_METHOD = 405;

    /**
     * 资源冲突,或者资源被锁
     */
    public static final int CONFLICT = 409;

    /**
     * 不支持的数据,媒体类型
     */
    public static final int UNSUPPORTED_TYPE = 415;

    /**
     * 系统内部错误
     */
    public static final int ERROR = 500;

    /**
     * 接口未实现
     */
    public static final int NOT_IMPLEMENTED = 501;

    /**
     * 系统警告消息
     */
    public static final int WARN = 601;
}

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值