【微信小程序开发】用户一键登录 + 获取手机号 + 微信支付V3

一、技术点摘要

  1. 基于Springboot搭建微信小程序后端框架
  2. 获取并缓存微信api调用凭证AccessToken
  3. 封装微信一键登录、获取手机号API
  4. 封装微信支付客户端
  5. 封装函数式接口,统一捕获微信支付异常,减少重复代码

二、微信支付pom依赖

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

三、微信相关配置

3.1 配置文件

ai-wxapp:
  wechat:
    # 应用id(小程序id)
    appid: xxxxx
    # 应用秘钥(小程序秘钥)
    secret: xxxxxx
  wechat-pay:
    # 商户号
    merchantId: xxxxxx
    # 商户API私钥路径
    privateKeyPath: D:\\Develop\\Wechat\\apiclient_key.pem
    # 商户证书序列号
    merchantSerialNumber: xxxxxx
    # 商户APIv3密钥
    apiV3Key: xxxxxx
    # 支付成功回调url,可公网https访问的
    payNotifyUrl: https://xxxxxx/wx/pay/payCallback
    # 退款成功回调url,可公网https访问的
    refundNotifyUrl: xxx

3.2 配置类

@Component
@ConfigurationProperties(prefix = "ai-wxapp.wechat")
@Data
public class WechatAppConfig {

    private String appid;

    private String secret;
}
@Data
@Component
@ConfigurationProperties(prefix = "ai-wxapp.wechat-pay")
public class WechatPayConfig {

    /**
     * 商户id
     */
    private String merchantId;

    /**
     * 商户API私钥
     */
    private String privateKeyPath;

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

    /**
     * 商户APIv3密钥
     */
    private String apiV3Key;

    /**
     * 支付成功回调url,可公网https访问的
     */
    private String payNotifyUrl;

    /**
     * 退款成功回调url,可公网https访问的
     */
    private String refundNotifyUrl;
}

四、微信一键登录+获取手机号

4.1 缓存微信api调用凭证:AccessToken

微信开放文档:获取接口调用凭据

@Slf4j
@Component
public class WechatAccessTokenService {

    @Resource
    private RedisUtils redisUtils;
    @Resource
    private WechatAppConfig wechatConfig;

    private final String getAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token";
    private final String access_token = "access_token";
    private final String expires_in = "expires_in";

    /**
     * 获取微信api调用凭证,可用于后续获取手机号等
     *
     * @return AccessToken
     * @author shn 2024/05/24 12:33
     */
    @Nullable
    public String getAccessToken() {
        String key = RedisKeys.WX_ACCESS_TOKEN;
        // 先查缓存
        if (redisUtils.hasKey(key) && redisUtils.getExpire(key) > 0L) {
            return redisUtils.get(key).toString();
        }
        // 缓存中没有,则调用微信接口获取
        Map<String, String> params = new HashMap<>();
        params.put("appid", wechatConfig.getAppid());
        params.put("secret", wechatConfig.getSecret());
        params.put("grant_type", "client_credential");
        String response = OkHttpUtil.httpGet(getAccessTokenUrl, null, params);
        if (StringUtils.isBlank(response)) {
            log.error("调用微信API获取AccessToken失败");
            return null;
        }
        JSONObject jsonObject = JSON.parseObject(response);
        if (StringUtils.isBlank(jsonObject.getString(access_token))) {
            return null;
        }
        // 凭证有效时间,单位:秒。目前是7200秒之内的值。
        int expiresIn = jsonObject.getIntValue(expires_in);
        // 缓存并设置过期时间,设置为提前30秒过期
        redisUtils.set(key, jsonObject.getString(access_token), expiresIn - 30, TimeUnit.SECONDS);
        return jsonObject.getString(access_token);
    }
}

4.2 微信一键登录 + 获取手机号

微信开放文档:小程序登录
微信开放文档:获取手机号

@Slf4j
@Component
public class WechatAuthApiService {

    @Resource
    private WechatAppConfig wechatConfig;
    @Resource
    private WechatAccessTokenService accessTokenService;

    private final String code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session";
    private final String session_key = "session_key";
    private final String unionid = "unionid";
    private final String openid = "openid";

    private final String getPhoneNumberUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s";
    private final String error_code = "errcode";
    private final String phone_info = "phone_info";
    private final String purePhoneNumber = "purePhoneNumber";

    /**
     * 微信登录
     *
     * @param code 前端调用wx.login获取到的code
     * @return com.shunxi.ai.common.sdk.weixin.WechatSessionEntity
     * @author shn 2024/05/24 12:32
     */
    @Nullable
    public WechatAuthEntity code2Session(String code) {
        Map<String, String> params = new HashMap<>();
        params.put("appid", wechatConfig.getAppid());
        params.put("secret", wechatConfig.getSecret());
        params.put("js_code", code);
        params.put("grant_type", "authorization_code");
        String response = OkHttpUtil.httpGet(code2SessionUrl, null, params);
        if (StringUtils.isBlank(response)) {
            return null;
        }
        JSONObject jsonObject = JSON.parseObject(response);
        if (StringUtils.isBlank(jsonObject.getString(session_key))) {
            return null;
        }
        return WechatAuthEntity.builder()
                .sessionKey(jsonObject.getString(session_key))
                .unionId(jsonObject.getString(unionid))
                .openId(jsonObject.getString(openid))
                .build();
    }

    /**
     * 获取用户手机号
     *
     * @param code 手机号获取凭证,这个code和wx.login获取到的code作用不一样,不能混用!!!
     * @return 用户手机号
     * @author shn 2024/05/24 12:34
     */
    @Nullable
    public String getPhoneNumber(String code) {
        String accessToken = accessTokenService.getAccessToken();
        if (StringUtils.isBlank(accessToken)) {
            return null;
        }
        String url = String.format(getPhoneNumberUrl, accessToken);
        String response = OkHttpUtil.postJson(url, null, Collections.singletonMap("code", code));
        if (StringUtils.isBlank(response)) {
            return null;
        }
        JSONObject jsonObject = JSON.parseObject(response);
        if (jsonObject.getIntValue(error_code) != 0) {
            return null;
        }
        return jsonObject.getJSONObject(phone_info).getString(purePhoneNumber);
    }
}

五、微信支付

微信支付开放文档:微信支付
微信支付V3版本sdk:wechatpay-apiv3

5.1 微信支付Service封装

PaySuccessParamEntity:

/**
 * @ClassName PaySuccessParamEntity
 * @Description 支付成功回调参数实体
 * @Author shn
 * @Date 2024/6/21 18:36
 * @Version 1.0
 */
@Data
public class PaySuccessParamEntity {
    private String orderId;
    private LocalDateTime paidTime;
    private String wxTransactionId;
}

WechatPayService:

  1. 封装微信支付客户端
  2. 封装函数式接口,对调用微信支付相关异常,统一捕获处理
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.shunxi.ai.wxapp.common.config.WechatPayConfig;
import com.shunxi.ai.wxapp.common.exception.ServiceException;
import com.shunxi.ai.wxapp.domain.wechat.model.entity.PaySuccessParamEntity;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * 微信支付服务
 *
 * @author shn 2024/05/28 14:05
 */
@Slf4j
@Service
public class WechatPayService {

    private static final int KEY_LENGTH_BYTE = 32;
    private static final int TAG_LENGTH_BIT = 128;

    @Resource
    private WechatPayConfig wechatPayConfig;

    private volatile JsapiServiceExtension jsapiServiceExtension;

    private volatile Config config;

    /**
     * 初始化微信Config
     *
     * @return com.wechat.pay.java.core.Config
     * @author shn 2024/05/28 14:06
     */
    private Config getConfig() {
        // 懒加载创建单例的微信支付Config对象
        if (config == null) {
            synchronized (this) {
                if (config == null) {
                    config = new RSAAutoCertificateConfig.Builder()
                            .merchantId(Objects.requireNonNull(
                                    wechatPayConfig.getMerchantId(), "【商户号】不能为空"))
                            .privateKeyFromPath(Objects.requireNonNull(
                                    wechatPayConfig.getPrivateKeyPath(), "【商户API私钥文件存放路径】不能为空"))
                            .merchantSerialNumber(Objects.requireNonNull(
                                    wechatPayConfig.getMerchantSerialNumber(), "【商户证书序列号】不能为空"))
                            .apiV3Key(Objects.requireNonNull(
                                    wechatPayConfig.getApiV3Key(), "【商户API V3秘钥】不能为空"))
                            .build();

                }
            }
        }
        return config;
    }

    /**
     * 微信支付-小程序客户端
     *
     * @return com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension
     * @author shn 2024/05/28 14:06
     */
    public JsapiServiceExtension getJsapiServiceExtension() {
        // 懒加载创建单例的小程序支付客户端
        if (jsapiServiceExtension == null) {
            synchronized (this) {
                if (jsapiServiceExtension == null) {
                    jsapiServiceExtension = new JsapiServiceExtension.Builder()
                            .config(this.getConfig())
                            .signType("RSA")
                            .build();
                }
            }
        }
        return jsapiServiceExtension;
    }

    /**
     * 封装函数式接口,对调用微信支付相关异常,统一捕获处理
     */
    public <T> T payExecute(Supplier<T> supplier) {
        try {
            T t = supplier.get();
            log.info("【调用微信接口】返回结果: {}", JSON.toJSONString(t));
            return t;
        } catch (HttpException e) {
            // 发送HTTP请求失败
            log.error("HttpException:{}", e.getMessage());
            throw new ServiceException(e.getMessage());
        } catch (com.wechat.pay.java.core.exception.ServiceException e) {
            // 服务返回状态小于200或大于等于300,例如500
            log.error("ServiceException:{}", e.getResponseBody());
            throw new ServiceException(e.getResponseBody());
        } catch (MalformedMessageException e) {
            // 服务返回成功,返回体类型不合法,或者解析返回体失败
            log.error("MalformedMessageException:{}", e.getMessage());
            throw new ServiceException(e.getMessage());
        }
    }

    /**
     * 根据支付回调结果,执行相应业务逻辑
     */
    public Boolean payCallbackExecute(JSONObject jsonObject,
                                      Consumer<PaySuccessParamEntity> consumer) {
        log.info("【微信支付回调】回调参数:{}", jsonObject.toJSONString());
        String orderId = "";
        try {
            // 解密
            String decryptData = this.decrypt2String(jsonObject);
            if (StringUtils.isBlank(decryptData)) {
                return Boolean.FALSE;
            }
            // 处理后续业务逻辑
            JSONObject dataJsonObj = JSONObject.parseObject(decryptData, JSONObject.class);
            if ("SUCCESS".equalsIgnoreCase(dataJsonObj.getString("trade_state"))) {
                // out_trade_no:商户订单号,是我们自己系统生成的订单号,下单时传给微信的。
                orderId = dataJsonObj.getString("out_trade_no");
                log.info("【微信支付回调】商户订单号: {},支付成功,开始执行业务逻辑", orderId);
                try {
                    PaySuccessParamEntity entity = new PaySuccessParamEntity();
                    entity.setOrderId(orderId);
                    // 支付时间
                    String successTime = dataJsonObj.getString("success_time");
                    entity.setPaidTime(OffsetDateTime
                            .parse(successTime, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
                            .toLocalDateTime());
                    // 微信交易流水号
                    entity.setWxTransactionId(dataJsonObj.getString("transaction_id"));
                    consumer.accept(entity);
                } catch (Exception e) {
                    log.error("【微信支付回调】商户订单号: {},支付成功,执行业务逻辑异常", orderId, e);
                    return Boolean.FALSE;
                }
            }
        } catch (Exception e) {
            log.error("【微信支付回调】解析回调参数异常", e);
            return Boolean.FALSE;
        }

        log.info("【微信支付回调】商户订单号:{},回调成功,业务逻辑执行完成! ", orderId);
        return Boolean.TRUE;
    }

    /**
     * 支付通知API文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml">...</a>
     * <p>
     * 1. 使用AEAD_AES_256_GCM算法,取得对应的参数nonce和associated_data
     * <p>
     * 2. 使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象
     */
    private String decrypt2String(JSONObject jsonObject) {
        try {
            cn.hutool.json.JSON json = JSONUtil.parse(jsonObject.toString());
            String ciphertext = (String) JSONUtil.getByPath(json, "resource.ciphertext");
            String associatedData = (String) JSONUtil.getByPath(json, "resource.associated_data");
            String nonce = (String) JSONUtil.getByPath(json, "resource.nonce");

            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec key = new SecretKeySpec(
                    wechatPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8), "AES");
            cipher.init(Cipher.DECRYPT_MODE, key,
                    new GCMParameterSpec(TAG_LENGTH_BIT, nonce.getBytes(StandardCharsets.UTF_8)));
            cipher.updateAAD(
                    associatedData.getBytes(StandardCharsets.UTF_8));

            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("【微信支付回调】参数解密失败", e);
        }
        return null;
    }
}

5.2 创建微信预支付订单 + 支付回调

@Slf4j
@RestController
@RequestMapping("/wx/pay")
@Api(value = "微信支付相关接口", tags = "微信支付相关接口")
public class WxPayController {

    @Resource
    private WechatAppConfig wechatAppConfig;
    @Resource
    private WechatPayConfig wechatPayConfig;
    @Resource
    private WechatPayService wechatPayService;
    @Resource
    private IOrderService orderService;

    @WebLog
    @ApiOperation("创建微信预支付订单,返回预支付信息")
    @PostMapping("/createWxPrepareOrder")
    public Result<PrepayWithRequestPaymentResponse>
    createWxPrepareOrder(@Valid @RequestBody CreateWxPrepareOrderRequestDTO param) {

        // 查询订单信息
        OrdersEntity entity = orderService.getByOrderId(param.getOrderId());

        // 调用微信预下单接口
        JsapiServiceExtension service = wechatPayService.getJsapiServiceExtension();

        PrepayRequest prepayRequest = new PrepayRequest();
        // 商品描述
        prepayRequest.setDescription("订单支付");

        // 小程序id
        prepayRequest.setAppid(wechatAppConfig.getAppid());

        // 商户id
        prepayRequest.setMchid(wechatPayConfig.getMerchantId());

        // 商户订单号
        prepayRequest.setOutTradeNo(entity.getOrderId());

        // 订单失效时间
        String timeExpire = buildTimeExpire(entity.getOrderId(), entity.getOrderTime());
        prepayRequest.setTimeExpire(timeExpire);

        // 支付回调接口
        prepayRequest.setNotifyUrl(wechatPayConfig.getPayNotifyUrl());

        // 支付金额,单位为分
        Amount amount = new Amount();
        amount.setCurrency("CNY");
        amount.setTotal(entity.getTotalAmount().multiply(new BigDecimal(100)).intValue());
        prepayRequest.setAmount(amount);

        // 支付人
        Payer payer = new Payer();
        payer.setOpenid(entity.getOpenId());
        prepayRequest.setPayer(payer);

        PrepayWithRequestPaymentResponse response = wechatPayService.payExecute(() ->
                service.prepayWithRequestPayment(prepayRequest));
        return Result.ok(response, "操作成功");
    }

    @WebLog
    @ApiOperation("支付回调接口")
    @PostMapping("/payCallback")
    @ApiIgnore
    public Map<String, String> payCallback(@RequestBody JSONObject jsonObject) {
        // 执行回调逻辑
        Boolean success = wechatPayService
                .payCallbackExecute(jsonObject, entity -> orderService.paySuccess(entity));

        // 给微信返回结果
        Map<String, String> result = new HashMap<>();
        result.put("code", success ? "SUCCESS" : "FAIL");
        result.put("message", success ? "成功" : "失败");
        return result;
    }

    /**
     * 构建订单失效时间(下单2小时内有效)
     * 示例值:2018-06-08T10:34:56+08:00
     */
    private static String buildTimeExpire(String orderId, LocalDateTime orderTime) {
        String timeExpire = orderTime.plusHours(2)
                .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
                .split("\\.")[0] + ZoneOffset.ofHours(8).getId();

        log.info("创建微信预支付订单,订单id: {} 订单失效时间: {}", orderId, timeExpire);

        return timeExpire;
    }
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
微信小程序可以通过getPhoneNumber方法获取用户微信绑定的手机号并实现一键登录功能。在前端页面中,可以使用openType="getPhoneNumber"的nut-button组件来触发获取手机号的操作。\[1\]在获取手机号的事件处理函数wxLogin中,可以通过调用wx.login获取唯一的code,然后使用该code调用后台接口获取openid。接着,根据获取到的openid调用后台接口获取token,如果存在token则进入首页,否则返回登录页面。\[2\]在代码中,需要注意@getphonenumber必须全部小写。\[3\]通过这样的流程,就可以实现微信小程序获取本机手机号一键登录的功能。 #### 引用[.reference_title] - *1* *3* [Taro:微信小程序通过获取手机号实现一键登录](https://blog.csdn.net/sg_knight/article/details/126901115)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [mpVue 微信小程序授权登录流程(即登录鉴权流程)及获取手机号一键登录教程(getPhoneNumber使用)——新增...](https://blog.csdn.net/cwin8951/article/details/131518107)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值