在当今移动应用市场中,微信小程序已经成为了许多企业和开发者推广业务和服务的首选平台之一。微信小程序具有快速加载、无需下载安装、与微信生态相互支持等优势,深受用户和开发者的喜爱。然而,要实现微信小程序的登录和支付功能,需要处理诸多繁琐的接口调用和逻辑处理。为了简化开发过程,我们可以借助开源框架wxJava来实现这些功能。

介绍wxJava

wxJava 是一个基于 Java 语言的微信开发 SDK,由微信开放社区维护和支持。它提供了丰富的接口和工具,帮助开发者快速搭建和开发微信相关的应用和服务。无论是公众号、小程序还是企业号,wxJava都提供了完整的支持,让开发者可以轻松实现与微信接口的对接。

开源地址:

GitHub - Wechat-Group/WxJava: 微信开发 Java SDK ,支持包括微信支付,开放平台,小程序,企业微信,视频号,公众号等的后端开发

实现微信小程序登录和授权手机号

微信小程序登录是用户进入小程序后进行身份认证的重要环节。使用wxJava,我们可以快速集成微信小程序登录功能,使用户能够通过微信账号进行登录。

流程

步骤

  1. 引入wxJava依赖:

    <dependency>
      <groupId>com.github.binarywang</groupId>
      <artifactId>weixin-java-miniapp</artifactId>
      <version>4.6.0</version>
    </dependency>
    
  2. 获取微信开发平台配置:在微信开发平台上创建小程序,并获取小程序的 AppID 和 AppSecret,获取流程。

    • 打开微信公众平台 → 扫码登入 → 左侧菜单选择开发 → 点击开发管理 → 即可。

    微信公众平台

    wx:
      miniapp:
        configs:
          - appid: # appId
            secret: # secret
            token: #微信小程序消息服务器配置的token (可选)
            aesKey: #微信小程序消息服务器配置的EncodingAESKey (可选)
            msgDataFormat: JSON
    
  3. 初始化配置:

    @Data
    @ConfigurationProperties(prefix = "wx.miniapp")
    @Configuration
    public class WxMaProperties {
    
        private List<Config> configs;
    
        @Data
        public static class Config {
            /**
             * 设置微信小程序的appid
             */
            private String appid;
    
            /**
             * 设置微信小程序的Secret
             */
            private String secret;
    
            /**
             * 设置微信小程序消息服务器配置的token
             */
            private String token;
    
            /**
             * 设置微信小程序消息服务器配置的EncodingAESKey
             */
            private String aesKey;
    
            /**
             * 消息格式,XML或者JSON
             */
            private String msgDataFormat;
        }
    }
    
    /**
     * 微信配置
     *
     * @author Mr.An
     * @date 2024/02/26
     */
    @Configuration
    public class WeChatConfig {
    
    
        @Resource
        private WxMaProperties properties;
    
        /**
         * 微信 服务
         *
         * @return {@link WxMaService}
         */
        @Bean
        public WxMaService wxMaService() {
            List<WxMaProperties.Config> configs = this.properties.getConfigs();
            if (configs == null) {
                throw new WxRuntimeException("WeChat Login Config be null!");
            }
            WxMaService maService = new WxMaServiceImpl();
            maService.setMultiConfigs(
                    configs.stream()
                            .map(a -> {
                                WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
                                config.setAppid(a.getAppid());
                                config.setSecret(a.getSecret());
                                config.setToken(a.getToken());
                                config.setAesKey(a.getAesKey());
                                config.setMsgDataFormat(a.getMsgDataFormat());
                                return config;
                            }).collect(Collectors.toMap(WxMaDefaultConfigImpl::getAppid, a -> a, (o, n) -> o)));
            return maService;
        }
    }
    
    
  4. 调用wxJava接口:使用wxJava提供的接口,进行用户登录的处理和逻辑实现。

        @Resource
        private WxMaService wxMaService;
    
        /**
         * 小程序登录
         *
         * @param param 参数
         * @return {@link Map}<{@link String}, {@link String}>
         */
        @Override
        public WeChatUserInfo login(WeChatLoginParam param) throws WxErrorException {
            // 1.根据 前端返回的code 或取 sessionKey
            WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService()
                    .getSessionInfo(param.getCode());
    
            // 2.根据 sessionKey 和其他参数获取用户的基本信息。
            WxMaUserInfo wxMaUserInfo = wxMaService.getUserService()
                    .getUserInfo(sessionInfo.getSessionKey(), param.getEncryptedData(), param.getIv());
    
            String openid = sessionInfo.getOpenid();
    
            // 3. 根据OpenId 判断使用是否是第一次登入,是:返回 Token;否:新建用户,返回Token。
            // .....
    
        }
    
  5. 返回用户基本数据和token即可开,openid 不需要返回。

授权手机号

需要支付等敏感环节需要先获取用户手机号

  1. 根据 HttpServletRequest 获取用户的token。

  2. 根据token获取用户信息。

  3. 根据前端返回的code查询微信服务获取手机号

     @Resource
     private WxMaService wxMaService;
    
     public WxMaPhoneNumberInfo loginGetPhoneNumber(String code) throws WxErrorException {
            if (StrUtil.isBlank(code)) throw new WeChatAppletLoginException("code不能为空");
            WxMaPhoneNumberInfo phoneNoInfo = wxMaService.getUserService().getPhoneNoInfo(code);
            String phoneNumber = phoneNoInfo.getPhoneNumber();
            // 将手机号存入 用户对象中,更新用户即可。
            // ...
       return phoneNoInfo;
    }
    
  4. 返回用户基本信息,注意手机号要脱敏。

实现微信小程序支付

微信小程序支付是小程序商业化的重要组成部分,能够为用户提供便捷的支付方式,促进交易的完成。利用wxJava,我们可以轻松实现微信小程序支付功能,为用户提供便捷的支付体验。

什么是JSAPI ?

JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。

详细可查看微信支付文档:

s产品介绍 - JSAPI支付 | 微信支付商户文档中心

流程

业务要求

  • 用户必须已经登入并携带token。

  • 用户已经授权手机号。

步骤

  1. 引入依赖

    <dependency>
      <groupId>com.github.binarywang</groupId>
      <artifactId>weixin-java-pay</artifactId>
      <version>4.6.0</version>
    </dependency>
    
  2. 获取基本配置。由于微信不支持沙箱环境,所以必须先完成各种配置:

    • 打开微信支付商户平台 https://pay.weixin.qq.com/

      • appid:产品中心 → AppID账号管理 → 选择即可。

      • mchId:账户中心 → 左侧商户信息 → 选择微信支付商户号即可。

      • apiV3Key:账户中心 → API安全 → 设置APIv3密钥 → 申请即可。

      • keyPath、privateKeyPath、privateCertPath:账户中心 → API安全 → 申请API证书 → 根据微信提示的流程会得到这三个文件,配置路径即可。

      • notifyUrl:产品中心 → 开发配置 → 支付配置 → JSAPI支付,Native支付。

        • 由于微信回调不支持本地测试,可以使用frp 内网穿透测试,具体可查看相关文档。
    wx:
      pay:
        appId:  #微信公众号或者小程序等的appid
        mchId:  #微信支付商户号
        mchKey:  #微信支付商户密钥
        apiV3Key:  # ApiV3Key
        #    subAppId: #服务商模式下的子商户公众账号ID
        #    subMchId: #服务商模式下的子商户号
        keyPath: classpath:wechat/apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
        privateKeyPath: classpath:wechat/apiclient_key.pem # 「商户私钥」文件
        privateCertPath: classpath:wechat/apiclient_cert.pem #「商户证书」文件
        certSerialNo:  # V3 证书序列号
        notifyUrl:  # 回调地址
        tradeType: JSAPI # 支付类型
    
  3. 配置初始化

    /**
     * WeChat 支付属性
     *
     * @author Mr.An
     * @date 2024/03/05
     */
    @Component
    @ConfigurationProperties(prefix = "wx.pay")
    @Data
    public class WeChatPayProperties {
        /**
         * 设置微信公众号或者小程序等的appid
         */
        private String appId;
    
        /**
         * 微信支付商户号
         */
        private String mchId;
    
        /**
         * 微信支付商户密钥
         */
        private String mchKey;
    
        /**
         * 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除
         */
        private String subAppId;
    
        /**
         * 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除
         */
        private String subMchId;
    
        /**
         * API v3 密钥
         */
        private String apiV3Key;
    
        /**
         * 回调地址
         */
        private String notifyUrl;
    
        /**
         * 支付类型
         */
        private String tradeType;
    
        /**
         * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
         */
        private String keyPath;
    
        /**
         * 专用证书路径 apiclient_cert.pem
         */
        private String privateCertPath;
    
        /**
         * 私钥路径 apiclient_key.pem
         */
        private String privateKeyPath;
    
        /**
         * V3 证书序列号
         */
        private String certSerialNo;
    }
    
    
    /**
     * We Chat 支付配置
     *
     * @author Mr.An
     * @date 2024/02/27
     */
    @Configuration
    @ConditionalOnClass(WxPayService.class)
    public class WeChatPayConfig {
    
        @Resource
        private WeChatPayProperties properties;
    
        /**
         * WX服务
         *
         * @return {@link WxPayService}
         */
        @Bean
        @ConditionalOnMissingBean
        public WxPayService wxPayService() {
    
            WxPayConfig payConfig = new WxPayConfig();
            payConfig.setSubAppId(properties.getSubAppId());
            payConfig.setSubMchId(properties.getSubMchId());
    
    
            payConfig.setAppId(isBlankThrow(properties.getAppId(), "appId(微信公众号或者小程序等的appid) be null!"));
            payConfig.setMchId(isBlankThrow(properties.getMchId(), "mchId(微信支付商户号) be null!"));
            payConfig.setMchKey(isBlankThrow(properties.getMchKey(), "mchKey(微信支付商户密钥) be null!"));
    
            payConfig.setNotifyUrl(isBlankThrow(properties.getNotifyUrl(),"notifyUrl(支付结果通知url) be null!"));
            payConfig.setTradeType(isBlankThrow(properties.getTradeType(),"tradeType(交易类型)be null!"));
    
            payConfig.setKeyPath(isBlankThrow(properties.getKeyPath(),"keyPath(证书路径,apiclient_cert.p12) be null!"));
    
            // ---- V3 版本需要 ----
            payConfig.setApiV3Key(isBlankThrow(properties.getApiV3Key(),"apiV3Key(微信支付v3秘钥) be null!"));
            payConfig.setPrivateKeyPath(isBlankThrow(properties.getPrivateKeyPath(),"privateKeyPath(商户私钥文件路径,apiclient_key.pem) be null!"));
            payConfig.setPrivateCertPath(isBlankThrow(properties.getPrivateCertPath(),"privateCertPath(商户私钥文件路径,apiclient_cert.pem) be null!"));
            // --------------
    
            // 可以指定是否使用沙箱环境
    //        payConfig.setUseSandboxEnv(false);
    
            WxPayService wxPayService = new WxPayServiceImpl();
            wxPayService.setConfig(payConfig);
            return wxPayService;
        }
    
        /**
         * 是空白投掷
         */
        private String isBlankThrow(String property, String propertyName) {
            return Opt.ofBlankAble(property)
                    .map(StringUtils::trimToNull)
                    .orElseThrow(() -> new WeChatAppletPayException(propertyName));
        }
    
  4. 创建订单 v3, 返回微信预支付订单给前端

        @Resource
        private  WxPayService wxPayService;
    
        /**
         * 调用统一下单接口,并组装生成支付所需参数对象.
         *
         * @param outTradeNo 出交易号
         * @return {@link Result}<{@link ?}>
         * @throws WxPayException 
         */
        @ApiOperation("统一下单,并组装所需支付参数")
        @PostMapping("/createOrderV3")
        public Result<?> createOrderV3(@RequestBody String outTradeNo, HttpServletRequest request) throws WxPayException {
    
            // TODO 根据 outTradeNo 查询数据库,获取订单的基本数据,并替换测试数据....
            outTradeNo = RandomUtil.randomNumbers(32);  // Hutools 随机生成 32 位纯数字交易号
            WxPayUnifiedOrderV3Request.Amount amount = new WxPayUnifiedOrderV3Request.Amount();
            amount.setTotal(1); // 一毛钱
            amount.setCurrency("CNY");
    
            WxPayUnifiedOrderV3Request requestV3 = new WxPayUnifiedOrderV3Request();
            requestV3.setAmount(amount);
            requestV3.setOutTradeNo(outTradeNo);
            requestV3.setDescription("测试商品");
    
            // TODO --------------------------
    
            // 根据Token 获取用户信息(根据你自己系统的业务来获取)
            SysUser sysUser = sysUserService.getUserByName(JwtUtil.getUserNameByToken(request));
    
            // 获取该用户信息的OpenId 
            requestV3.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(sysUser.getOpenId()));
    
            // JSAPI返回方法
            WxPayUnifiedOrderV3Result.JsapiResult result = wxPayService.createOrderV3(TradeTypeEnum.JSAPI, requestV3);
    
            return Result.ok(result.setAppId(null)); // 隐藏 AppId
        }
    
  5. 发起支付请求:将生成的支付参数传递给前端页面,引导用户打开微信支付页面完成支付操作。

  6. 处理支付结果:在支付完成后,微信会将支付结果通知到开发者服务器,开发者需要处理支付结果并更新订单状态。

        @Resource
        private  WxPayService wxPayService;    
    
        /**
         * 解析支付结果通知
         * 通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m
         *
         * @param notifyData 通知数据
         */
        @ApiOperation("支付回调通知处理")
        @PostMapping("/notify/order")
        public Result<?> parseOrderNotifyResult(@RequestBody String notifyData, HttpServletRequest request) throws WxPayException {
            log.info("【支付回调通知处理】:{}", notifyData);
            WxPayNotifyV3Result result = wxPayService.parseOrderNotifyV3Result(notifyData, WeChatPayUtils.getRequestHeader(request)); // 工具类在文章结尾
    
            // 解密后的数据
            switch (result.getResult().getTradeState()) {
                case WxpayTradeStatus.SUCCESS:
                    runnable.run();
                    log.info("【支付回调通知处理成功】result:{}", result);
                    break;
                case WxpayTradeStatus.PAY_ERROR:
                    log.error("【支付回调通知失败】:{}", result);
                    throw new WxPayException("微信支付-回调失败!");
                case WxpayTradeStatus.CLOSED:
                    log.warn("【支付回调通知,用户取消支付】:{}", result);
                default:
                    log.error("【支付回调通知失败】:{}", result);
            }
    
    
            return Result.OK(result);
        }
    

退款实现

退款前提需要支付成功,需要拿到商户订单号或者微信支付订单号,才可以进行退款。

  • 申请退款API(支持单品)。详见 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml

    应用场景 当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。

    注意:

    1. 交易时间超过一年的订单无法提交退款。

    2. 微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号。

    3. 错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次。

    4. 每个支付订单的部分退款次数不能超过50次。

    5. 如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败。

    6. 申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果。

    7. 一个月之前的订单申请退款频率限制为:5000/min。

    接口地址: https://api.mch.weixin.qq.com/v3/refund/domestic/refunds

    
        @Resource
        private  WxPayService wxPayService;
    
        public WxPayRefundV3Result refundV3(String outTradeNo) throws WxPayException {
    
    
            WxPayRefundV3Request request = new WxPayRefundV3Request();
            request.setOutTradeNo(outTradeNo);
            request.setNotifyUrl("填写下边的解析退款结果通知地址,需要域名,可以使用frp内网穿透在本地测试。");
            request.setOutRefundNo(RandomUtil.randomNumbers(32)); // Hutools
    
    
            // TODO 根据 outTradeNo 查询数据库,获取订单的基本数据,并替换测试数据....
            // 以下是测试数据,替换为正规数据
            WxPayRefundV3Request.Amount amount = new WxPayRefundV3Request
                    .Amount()
                    .setTotal(100)
                    .setRefund(100)
                    .setCurrency("CNY");
            request.setAmount(amount);
            // TODO --------------------------
            return wxPayService.refundV3(request); // 将敏感数据隐藏
        }
    
  • 解析退款结果通知,详见 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=9

        @Resource
        private  WxPayService wxPayService;
    
        public WxPayRefundNotifyV3Result parseRefundNotifyV3Result(String notifyData, HttpServletRequest request throws WxPayException {
    
            log.info("【退款回调通知处理】:{}", notifyData);
    
            // 1. 解析回调通知数据
            WxPayRefundNotifyV3Result result = wxPayService
                    .parseRefundNotifyV3Result(notifyData, WeChatPayUtils.getRequestHeader(request)); // 工具类在文章结尾
    
            // 2. 不同的处理
            switch (result.getResult().getRefundStatus()) {
                case RefundStatus.SUCCESS:
                    log.info("【退款回调通知处理成功】result:{}", result);
                    break;
                case RefundStatus.PROCESSING:
                    log.info("【退款处理中】result:{}", result);
                    break;
                case RefundStatus.ABNORMAL:
                    log.error("【退款回调通知失败】:{}", result);
                    throw new WxPayException("退款回调通知失败!");
                default:
                    log.error("【退款回调通知失败】:{}", result);
            }
    
            return result;
        }
    

文章代码中的使用的工具类

/**
 * 微信 支付实工具
 *
 * @author Mr.An
 * @date 2024/03/05
 */
public class WeChatPayUtils {

    /**
     * 获取回调请求头:签名相关
     *
     * @param request HttpServletRequest
     * @return SignatureHeader
     */
    public static SignatureHeader getRequestHeader(HttpServletRequest request) {
        // 获取通知签名
        String signature = request.getHeader("Wechatpay-Signature");
        String nonce = request.getHeader("Wechatpay-Nonce");
        String serial = request.getHeader("Wechatpay-Serial");
        String timestamp = request.getHeader("Wechatpay-Timestamp");

        SignatureHeader signatureHeader = new SignatureHeader();
        signatureHeader.setSignature(signature);
        signatureHeader.setNonce(nonce);
        signatureHeader.setSerial(serial);
        signatureHeader.setTimeStamp(timestamp);
        return signatureHeader;
    }
}

结语

通过使用开源框架wxJava,我们可以轻松实现微信小程序的登录和支付功能,大大简化了开发流程,提高了开发效率。同时,wxJava作为一个活跃的开源社区项目,也能够及时跟进微信官方的更新,保证开发者能够使用到最新的微信功能和特性。因此,如果你正在开发微信小程序,不妨考虑使用wxJava来实现登录和支付功能,为用户提供更加完善的用户体验。