之前也一直对接微信,支付宝,银联,第三方支付公司的支付流程,但是记录的还是比较少的。今天重新梳理支付中心,简单的记录下心得
怎么开通商户号和公众号我这就不说了。
1、只是微信支付必须要公众号appID,所以先在商户号上关联一个服务号。
2、设置秘钥,这个秘钥是自己设置的,我这里直接就是32位随机数。
3、开通产品和设置域名
ok,到这里基本前期的准备工作差不多了。微信作为支付体系的一个大部分,肯定是需要封装到支付中心的。项目结构
然后再解释一个东西,微信证书,这个是在某些敏感接口需要进行证书验证的,具体申请流程比较简单,这里就不详细描述。
开发开始:
1、SDK准备
去官网下载SDK文件:https://pay.weixin.qq.com/wiki/doc/api/download/WxPayAPI_JAVA.zip
当然,说是SDK嘛,其实很多东西就是一个开放接口,还是需要自己具体去实现。所以这里新建2个实现类
对请求域名进行处理,实现容灾
public class IWXPayDomainImpl implements IWXPayDomain {
//3 minutes
private final int MIN_SWITCH_PRIMARY_MSEC = 3 * 60 * 1000;
private long switchToAlternateDomainTime = 0;
private Map<String, DomainStatics> domainData = new HashMap();
private IWXPayDomainImpl(){
}
private static class WxPayDomainHolder{
private static IWXPayDomain holder = new IWXPayDomainImpl();
}
public static IWXPayDomain instance(){
return WxPayDomainHolder.holder;
}
@Override
public void report(String domain, long elapsedTimeMillis, Exception ex) {
DomainStatics info = domainData.get(domain);
if(info == null){
info = new DomainStatics(domain);
domainData.put(domain, info);
}
if(ex == null){ //success
if(info.succCount >= 2){ //continue succ, clear error count
info.connectTimeoutCount = info.dnsErrorCount = info.otherErrorCount = 0;
}else{
++info.succCount;
}
}else if(ex instanceof ConnectTimeoutException){
info.succCount = info.dnsErrorCount = 0;
++info.connectTimeoutCount;
}else if(ex instanceof UnknownHostException){
info.succCount = 0;
++info.dnsErrorCount;
}else{
info.succCount = 0;
++info.otherErrorCount;
}
}
@Override
public DomainInfo getDomain(WXPayConfig config) {
DomainStatics primaryDomain = domainData.get(WXPayConstants.DOMAIN_API);
if(primaryDomain == null ||
primaryDomain.isGood()) {
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
long now = System.currentTimeMillis();
if(switchToAlternateDomainTime == 0){ //first switch
switchToAlternateDomainTime = now;
return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
}else if(now - switchToAlternateDomainTime < MIN_SWITCH_PRIMARY_MSEC){
DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
if(alternateDomain == null ||
alternateDomain.isGood() ||
alternateDomain.badCount() < primaryDomain.badCount()){
return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
}else{
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
}else{ //force switch back
switchToAlternateDomainTime = 0;
primaryDomain.resetCount();
DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
if(alternateDomain != null) {
alternateDomain.resetCount();
}
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
}
static class DomainStatics {
final String domain;
int succCount = 0;
int connectTimeoutCount = 0;
int dnsErrorCount =0;
int otherErrorCount = 0;
DomainStatics(String domain) {
this.domain = domain;
}
void resetCount(){
succCount = connectTimeoutCount = dnsErrorCount = otherErrorCount = 0;
}
boolean isGood(){ return connectTimeoutCount <= 2 && dnsErrorCount <= 2; }
int badCount(){
return connectTimeoutCount + dnsErrorCount * 5 + otherErrorCount / 4;
}
}
}
配置实现:
@Slf4j
public class WxPayConfigImpl extends WXPayConfig {
private WxProperties wxProperties;
private static WxPayConfigImpl INSTANCE;
private byte[] certData;
private WxPayConfigImpl() {
}
@Override
public String getAppID() {
return wxProperties.getAppId();
}
@Override
public String getMchID() {
return wxProperties.getMchId();
}
@Override
public String getKey() {
return wxProperties.getKey();
}
@Override
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
@Override
public IWXPayDomain getWXPayDomain() {
return IWXPayDomainImpl.instance();
}
/**
* 静态内部类保证现成安全
*/
private static class SingleWxPayConfig{
/**
* 静态对象初始化,由JVM保证线程安全
*/
private static WxPayConfigImpl instance = new WxPayConfigImpl();
}
/**
* 单例调用
* @return
*/
public static WxPayConfigImpl getInstance(){
return SingleWxPayConfig.instance;
}
public void setWxProperties(WxProperties wxProperties) {
this.wxProperties = wxProperties;
}
public void setCertData(byte[] certData) {
this.certData = certData;
}
}
增加相关配置实现,进行部分功能初始化,包含证书信息。
@Configuration
@EnableConfigurationProperties(WxProperties.class)
@Slf4j
public class MyWxPayConfig {
@Autowired
private WxProperties wxProperties;
@Bean
public WXPay wxPay(){
byte[] certData = null;
try {
File file = new File(wxProperties.getCertPath());
InputStream certStream = new FileInputStream(file);
certData = new byte[(int) file.length()];
certStream.read(certData);
certStream.close();
} catch (Exception e) {
e.printStackTrace();
log.error("读取证书出错!错误原因:{}",e.getMessage());
}
WxPayConfigImpl wxPayConfig = WxPayConfigImpl.getInstance();
wxPayConfig.setWxProperties(wxProperties);
wxPayConfig.setCertData(certData);
WXPay wxpay = null;
try {
wxpay = new WXPay(wxPayConfig);
} catch (Exception e) {
e.printStackTrace();
log.error("微信初始化出错");
}
return wxpay;
}
}
配置类实现:
@Data
@ConfigurationProperties(prefix = "XX.pay.wx")
public class WxProperties {
private String appId;
private String mchId;
private String key;
private String certPath;
}
增加YML文件配置:
XX:
pay:
wx:
appId: XX
mchId: XX
key: XX
certPath: /app_server/pay/apiclient_cert.p12
到这里SDK相关准备工作完成,相关配置内容就位
第二步:业务实现
下单,这其中的H5OrderDto 是业务方具体的传递参数。用到微信支付的地方可能不止一个地方,抽离公共请求参数,让支付脱离业务:
public String h5Pay(H5OrderDto orderDto) {
log.info("当前请求参数:{}", JSONObject.toJSONString(orderDto));
Map<String,String> reqData = new HashMap<>();
//公众号
reqData.put("appid", wxPay.getConfig().getAppID());
//商户号
reqData.put("mch_id", wxPay.getConfig().getMchID());
//设备信息
reqData.put("device_info", "WEB");
//随机字符串
reqData.put("nonce_str", PayUtil.getRandom());
//签名类型
reqData.put("sign_type", "MD5");
//商品描述
reqData.put("body", orderDto.getBody());
//订单号
reqData.put("out_trade_no", orderDto.getOrderId());
//价格
reqData.put("total_fee",String.valueOf(orderDto.getPrice()));
//终端IP
reqData.put("spbill_create_ip", orderDto.getIp());
log.info("id地址"+reqData.get("spbill_create_ip"));
//回调地址
reqData.put("notify_url", orderDto.getCallBackUrl());
//交易类型
reqData.put("trade_type", "MWEB");//h5支付
//商品编号
reqData.put("product_id", orderDto.getProductId());//h5支付
JSONObject sceneInfo = new JSONObject();
JSONObject sceneItem = new JSONObject();
sceneItem.put("type", "Wap");
sceneItem.put("wap_url", orderDto.getPageUrl());
sceneItem.put("wap_name", orderDto.getPageDesc());
sceneInfo.put("h5_info", sceneItem);
reqData.put("scene_info", sceneInfo.toString());//h5支付
//回传参数
reqData.put("attach", orderDto.getTelephone());
Map<String, String> result = null;
try {
result = wxPay.unifiedOrder(reqData);
}catch (Exception e){
e.printStackTrace();
log.error("下单出错!");
throw new PayException("下单失败");
}
if(result!=null&&WXPayConstants.SUCCESS.equals(result.get("return_code"))){
return result.get("mweb_url");
}
throw new PayException("下单失败");
}
支付成功回调处理,未了摆脱业务验证,这里抽离其业务验证实现
@Override
public String backDeal(String requestXML,String className) {
log.info("请求回调参数====:{}",requestXML);
if(requestXML==null){
return getBackXml(WXPayConstants.FAIL, "缺失请求参数");
}
try {
Map<String, String> doc = WXPayUtil.xmlToMap(requestXML);
log.info("转化后的参数:{}",doc);
String returnCode = doc.get("return_code"); // 获取返回状态
String returnMsg = doc.get("return_msg"); // 获取返回信息
if(!WXPayConstants.SUCCESS.equals(returnCode)){
return getBackXml(WXPayConstants.FAIL, returnMsg);
}
String resultCode = doc.get("result_code"); // 获取业务结果
String errCodeDes = doc.get("err_code_des"); // 获取业务结果
if(!WXPayConstants.SUCCESS.equals(resultCode)){
return getBackXml(WXPayConstants.FAIL, errCodeDes);
}
String appid = doc.get("appid"); // 公众账号ID
log.info("appid:{}",appid);
if(!wxPay.getConfig().getAppID().equals(appid)){
return getBackXml(WXPayConstants.FAIL, "公众号不匹配");
}
String mch_id = doc.get("mch_id"); // 商户号
if(!wxPay.getConfig().getMchID().equals(mch_id)){
return getBackXml(WXPayConstants.FAIL, "商户号不匹配");
}
log.info("基础参数判断完毕,{}",wxPay.getConfig().getKey());
if(!WXPayUtil.isSignatureValid(requestXML, wxPay.getConfig().getKey())&&!WXPayUtil.isSignatureValid(WXPayUtil.xmlToMap(requestXML), wxPay.getConfig().getKey(), WXPayConstants.SignType.HMACSHA256)){
log.error("签名验证失败");
return getBackXml(WXPayConstants.FAIL, "签名不匹配");
}
String out_trade_no = doc.get("out_trade_no"); // 商户订单号
log.error("签名验证成功,订单号:{}",out_trade_no);
String attachEle =doc.get("attach"); // 回传参数,回传参数在此处未用到,可以用来处理优惠相关业务
String total_fee = doc.get("total_fee"); // 订单金额
//进入业务流程判断,判断逻辑
//1、查询订单是否存在
//2、判断订单状态,如果是已支付,返回SUCCESS,不做处理
//3、如果订单未支付,判断返回金额与订单金额是否匹配
OrderService orderService = null;
if(className.equals("zjcOrderService")){
orderService = zjcOrderService;
}
if(orderService.dealOrder(out_trade_no,total_fee)){
return getBackXml(WXPayConstants.SUCCESS, "OK");
}else{
return getBackXml(WXPayConstants.FAIL, "跟业务逻辑冲突");
}
}catch (Exception e){
e.printStackTrace();
}
return getBackXml(WXPayConstants.FAIL, "未知错误");
}
/**
* 获取返回的XML
* @param result
* @param message
* @return
*/
private String getBackXml(String result,String message){
return "<xml><return_code><![CDATA["+result+"]]></return_code><return_msg><![CDATA["+message+"]]></return_msg></xml>";
}
抽象父类接口:
public interface OrderService {
/**
* 处理订单
* @param orderId
* @param total
* @return
*/
Boolean dealOrder(String orderId,String total);
}
具体业务判断调用:
@FeignClient("uc")
public interface ZjcOrderService extends OrderService {
/**
* 处理订单业务
* @param orderId
* @return
*/
@GetMapping("/zjc/{orderId}/{total}")
@Override
Boolean dealOrder(@PathVariable("orderId") String orderId,@PathVariable("total") String total);
}
当然这里处理的还是比较粗超的,大家可以想想怎么处理更细腻和完美一些。我这里的具体支付成功业务比较简单
@Override
public Boolean dealOrder(String orderId, String total) {
log.info("开始验证订单:{},金额:{}",orderId,total);
ZjcOrder zjcOrder = zjcOrderMapper.getByOrderId(orderId);
log.info("订单信息:{}",JSONObject.toJSONString(zjcOrder));
if(zjcOrder==null){
return false;
}
if(zjcOrder.getStatus()==1){
return true;
}
if(!total.equals(String.valueOf(zjcOrder.getPrice()))){
return false;
}
zjcOrderMapper.updateStatus(orderId);
return true;
}
到这里后端流程基本OK。
前端下单就比较简单,返回url即可。然后前端进行跳转即可