微信JSAPI支付(二)代码重构之统一下单,查询,下载等功能实现

862 阅读5分钟

前一篇介绍了如何实现微信的统一下单,但在实际生产中,不建议直接使用。开发中的代码,需要可移植,低耦合。因此,特地重构了关于微信支付的代码,希望为感兴趣的朋友能提供一些帮助。

重构步骤

配置类注入

1.新建实体类,封装微信公众平台的配置参数

@Data
public class WCPConfigParams {
    // 公众号id
    private String appId;
    // app密钥
    private String appSecret;
    // 商户号
    private String muchId;
    // api密钥
    private String apiKey;
    // 公众号注册域名
    private String registerDomain;
    // jsapi支付目录
    private String jsapiPaymentAuthDir;
    // js安全域名
    private String jsDomain;
    // 网页授权域名
    private String webAuthDomain;
    // 证书目录
    private String apiclientCert;
    // 支付回调地址
    private String notifyUrl;
    // 退款回调地址
    private String notifyUrlRefund;
}

2.新建属性文件保存配置参数的信息

# 微信分配的公众账号ID(企业号corpid即为此appId)
wcp.APP_ID=
# 接口密钥
wcp.APPSECRET=
# 微信支付分配的商户号
wcp.MCH_ID=
# API密钥
wcp.API_KEY=
# 注册域名
wcp.REGISTER_DOMAIN=
# JSAPI支付授权目录
WCP.JSAPI_PAYMENT_AUTH_DIR=
# JS接口安全域名
wcp.JS_DOMAIN=
# 网页授权域名
wcp.WEB_AUTH_DOMAIN=
# 证书路径
wcp.APICLIENT_CERT=
# 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
wcp.NOTIFY_URL=
# 异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不允许带参数,如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效。
wcp.NOTIFY_URL_REFUND=

3.新建配置类,注入配置参数的bean

@Configuration
@PropertySource("classpath:config/wechat-pay.properties")
public class WCPConfig {

    @Autowired
    private Environment env;

    @Bean
    public WCPConfigParams wcpConfigParams() {
        WCPConfigParams params = new WCPConfigParams();
        params.setAppId(env.getProperty("wcp.APP_ID"));
        params.setAppSecret(env.getProperty("wcp.APPSECRET"));
        params.setMuchId(env.getProperty("wcp.MCH_ID"));
        params.setApiKey(env.getProperty("wcp.API_KEY"));
        params.setRegisterDomain(env.getProperty("wcp.REGISTER_DOMAIN"));
        params.setJsapiPaymentAuthDir(env.getProperty("wcp.JSAPI_PAYMENT_AUTH_DIR"));
        params.setJsDomain(env.getProperty("wcp.JS_DOMAIN"));
        params.setWebAuthDomain(env.getProperty("wcp.webAuthDomain"));
        params.setApiclientCert(env.getProperty("wcp.APICLIENT_CERT"));
        params.setNotifyUrl(env.getProperty("wcp.NOTIFY_URL"));
        params.setNotifyUrlRefund(env.getProperty("wcp.NOTIFY_URL_REFUND"));
        return params;
    }
}    

4.sdk中的WXPayConfig由抽象类改为接口

interface WXPayConfig {
    /**
     * 获取 App ID
     */
    String getAppID();
	// ...
    /**
     * 获取商户证书内容
     */
    InputStream getCertStream();
    /**
     * HTTP(S) 连接超时时间,单位毫秒
     */
    default int getHttpConnectTimeoutMs() {
        return 6 * 1000;
    }
	// ...
}

5.实现该接口

public class WXPayConfigImpl implements WXPayConfig {

    private WCPConfigParams wcpConfigParams;

    private byte[] certData;

    public WXPayConfigImpl(WCPConfigParams wcpConfigParams) throws IOException {
        this.wcpConfigParams = wcpConfigParams;
        String certPath = wcpConfigParams.getApiclientCert();
        File file = new File(certPath);
        InputStream certStream = new FileInputStream(file);
        this.certData = new byte[(int)file.length()];
        certStream.read(this.certData);
        certStream.close();
    }

    public WXPayConfigImpl() {}

    public void setWcpConfigParams(WCPConfigParams wcpConfigParams) {
        this.wcpConfigParams = wcpConfigParams;
    }

    @Override
    public String getAppID() {
        return wcpConfigParams.getAppId();
    }

	// ...
}

6.配置类中新增注入的bean

@Bean
@DependsOn(value = "wcpConfigParams")
public WXPayConfigImpl wxPayConfigImpl() {
	WXPayConfigImpl wxPayConfigImpl = new WXPayConfigImpl();
	wxPayConfigImpl.setWcpConfigParams(wcpConfigParams());
	return wxPayConfigImpl;
}

@Bean(name = "wxPayDefault")
@DependsOn(value = "wxPayConfigImpl")
public WXPay wxPayDefault() throws Exception {
	WXPay wxPay = new WXPay(wxPayConfigImpl());
	return wxPay;
}

虽然Spring是从上到下的执行顺序,建议加上bean的依赖关系

微信支付功能开发

统一下单

1.新增统一下单的实体类

@Data
public class UnifiedOrderRequestEntity {
    /**
     * 公众账号ID
     */
    private String appid;
    /**
     * 商户号
     */
    @JSONField(name = "mch_id")
    private String mchId;
    /**
     * 设备号
     */
    @JSONField(name = "device_info")
    private String deviceInfo;

	// ...
}

2.新增微信支付后端工具类

@Component
public class WCPBackendUtil {

    @Autowired
    @Qualifier("wxPayDefault")
    private WXPay wxPayDefault;

    @Autowired
    private WCPConfigParams wcpConfigParams;

    @Autowired
    private WPPSignatureUtil wppSignatureUtil;

    @Autowired
    private WPPBackendUtil wppBackendUtil;

    /**
     * 统一下单接口,输入指定参数,只关心必要参数
     * @param openid        用户在公众号的唯一识别号
     * @param tradeType     交易类型
     * @param price         价格
     * @param productDesc   商品描述
     * @param terminalIP    终端ip
     * @param requestUrl    请求来源的url
     * @return  返回js校验参数的的map
     */
    public Map<String, Object> unifiedorder(String openid, String tradeType, String price, String productDesc,
        String terminalIP, String requestUrl) {
        try {
            UnifiedOrderRequestEntity requestEntity = new UnifiedOrderRequestEntity();
            requestEntity.setBody(productDesc);
            requestEntity.setOutTradeNo(generateRandomOrderNo());
            requestEntity.setTotalFee(Utility.Yuan2Fen(Double.parseDouble(price)).toString());
            requestEntity.setSpbillCreateIp(terminalIP);
            requestEntity.setOpenid(openid);
            requestEntity.setTradeType(tradeType);
            String nonceStr = WXPayUtil.generateNonceStr();
            requestEntity.setNonceStr(nonceStr);
            requestEntity.setNotifyUrl(wcpConfigParams.getNotifyUrl());

            // 利用sdk统一下单,已自动调用wxpay.fillRequestData(data);
            Map<String, String> respMap = wxPayDefault.unifiedOrder(beanToMap(requestEntity));

            // 统一下单接口调用成功
            if (respMap.get("return_code").equals(WXPayConstants.SUCCESS)
                && respMap.get("result_code").equals((WXPayConstants.SUCCESS))) {
                String prepayId = respMap.get("prepay_id");
                return wppSignatureUtil.permissionValidate(wcpConfigParams.getAppId(), nonceStr, requestUrl, prepayId,
                    wcpConfigParams.getApiKey(),wppBackendUtil.getJsApiTicket(wppBackendUtil.getAccessToken()));
            } else if (!respMap.get("return_code").equals(WXPayConstants.SUCCESS)) {
                Map<String, Object> map = new HashMap<>();
                for (String key : respMap.keySet()) {
                    map.put(key, respMap.get(key));
                }
                return map;
            }
        } catch (Exception e) {
            // log ...
        }
        return null;	// 返回包含错误提示的map
    }

    /**
     * 通用微信支付的调用方法,参数灵活
     * @param requestEntity UnifiedOrderRequestEntity统一下单的实体类
     * @param requestUrl    请求来源的url
     * @return
     */
    public Map<String, Object> unifiedorder(UnifiedOrderRequestEntity requestEntity, String requestUrl) {
        try {
            String nonceStr = requestEntity.getNonceStr();
            Map<String, String> respMap = wxPayDefault.unifiedOrder(beanToMap(requestEntity));

            // 统一下单接口调用成功
            if (respMap.get("return_code").equals(WXPayConstants.SUCCESS)
                && respMap.get("result_code").equals((WXPayConstants.SUCCESS))) {
                String prepayId = respMap.get("prepay_id");
                return wppSignatureUtil.permissionValidate(wcpConfigParams.getAppId(), nonceStr, requestUrl, prepayId,
                        wcpConfigParams.getApiKey(),wppBackendUtil.getJsApiTicket(wppBackendUtil.getAccessToken()));
            } else if (!respMap.get("return_code").equals(WXPayConstants.SUCCESS)) {
                Map<String, Object> map = new HashMap<>();
                for (String key : respMap.keySet()) {
                    map.put(key, respMap.get(key));
                }
                return map;
            }
        } catch (Exception e) {
            // log ...
        }
        return null;
    }
}    

3.方法测试

@SpringBootTest
class WppApplicationTests {

    @Autowired
    private WCPConfigParams wcpConfigParams;

    @Autowired
    private WCPBackendUtil wcpBackendUtil;

    @Test
    void testUnifiedOrder() {
        String openid = "o4036jqo2PN9isV6N2FHGRsGRVqg";		// 在**公众号下的openid
        String ipAddr = "127.0.0.1";
        String url = "http://chety.mynatapp.cc";
        Map<String, Object> result1 = wcpBackendUtil.unifiedorder(openid, WCPBackendConst.TradeType.JSAPI.toString(), "1", "Test", ipAddr, url);
        UnifiedOrderRequestEntity requestEntity = new UnifiedOrderRequestEntity();
        requestEntity.setOutTradeNo(wcpBackendUtil.generateRandomOrderNo());
        requestEntity.setBody("Test");
        requestEntity.setOpenid("o4036jqo2PN9isV6N2FHGRsGRVqg");
        requestEntity.setSpbillCreateIp(ipAddr);
        requestEntity.setTradeType(WCPBackendConst.TradeType.JSAPI.toString());
        requestEntity.setTotalFee("1");
        requestEntity.setNotifyUrl("1");
        requestEntity.setNonceStr(WXPayUtil.generateNonceStr());
        requestEntity.setNotifyUrl(wcpConfigParams.getNotifyUrl());
        Map<String, Object> result2 = wcpBackendUtil.unifiedorder(requestEntity,url);
        System.out.println(result1);
        System.out.println(result2);
    }
}    

4.返回结果,如图

emmm... 这个公众号出了点问题,正常的返回结果应该类似这样:

{
    "configMap": {
        "appId": "wxa02348cd5ec17d28",
        "nonceStr": "K2pJsNrRIlhQbeCAVDrPLFTrRo0q1zNS",
        "signature": "f62e3c2a0e89973e548b046e8dd2d45f787d8b09",
        "timestamp": "1568187468"
    },
    "payMap": {
        "timeStamp": "1568187468",
        "package": "prepay_id=wx11153748107050b2c5f8d4df1082400300",
        "packageStr": "prepay_id=wx11153748107050b2c5f8d4df1082400300",
        "paySign": "C7F135081A4476F434C67686403D741D",
        "appId": "wxa02348cd5ec17d28",
        "signType": "MD5",
        "nonceStr": "K2pJsNrRIlhQbeCAVDrPLFTrRo0q1zNS"
    }
}

同理,我们再实现一个查询订单的功能

查询订单

1.新建查询订单的实体类

@Data
public class OrderQueryRequestEntity {
    /**
     * 公众账号ID
     */
    private String appid;
    /**
     * 商户号
     */
    @JSONField(name = "mch_id")
    private String mchId;
    /**
     * 微信订单号
     */
    @JSONField(name = "transaction_id")
    private String transactionId;
    // ...
}

2.新建查询订单的方法

   /**
     * 查询订单
     * @param outTradeNo 商户订单号
     * @return
     */
    public Map<String, String> orderquery(String outTradeNo) {
        try {
            OrderQueryRequestEntity requestEntity = new OrderQueryRequestEntity();
            requestEntity.setOutTradeNo(outTradeNo);
            requestEntity.setNonceStr(WXPayUtil.generateNonceStr());
            Map<String, String> map = orderquery(requestEntity);
            return map;
        } catch (Exception e) {
            // log ...
        }
        return null;
    }

    /**
     * 查询订单
     * @param requestEntity     OrderQueryRequestEntity订单查询的请求实体
     * @return
     */
    public Map<String, String> orderquery(OrderQueryRequestEntity requestEntity) {
        try {
            return wxPayDefault.orderQuery(beanToMap(requestEntity));
        } catch (Exception e) {
            // log ...
        }
        return null;
    }

3.测试查询订单

@Test
void testQuery() {
    Map<String, String> result1 = wcpBackendUtil.orderquery("201907051128063699");     // 该订单可以是统一下单时生成的商户订单号
    OrderQueryRequestEntity requestEntity = new OrderQueryRequestEntity();
    requestEntity.setOutTradeNo("201907051128063699");
    requestEntity.setNonceStr(WXPayUtil.generateNonceStr());
    Map<String, String> result2 = wcpBackendUtil.orderquery(requestEntity);
    System.out.println(result1);
    System.out.println(result2);
}

4.查询结果

结束

从而,微信支付的其他方法可以类似的展开, 如关闭订单closeorder,瑞款refund(需要证书),退款查询refundquery,下载对账单downloadbill,拉取订单评价数据batchquerycomment等都是相似的调用过程。


目前重构的代码,已经尽量接触耦合,提高复用性,且都已测试。 但是仍然又很多需要改进的地方,如微信支付业务加入商家交易业务,token等在redis储存,前端页面完善等,后面会持续更新,欢迎评论留下修改意见。

源代码已上传:github.com/chetwhy/wpp