[微信小程序]授权登录和微信支付

685 阅读7分钟

一. 微信小程序授权登录


引入依赖

<!--微信小程序-->
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-miniapp</artifactId>
    <version>4.3.0</version>
</dependency>

授权登录代码

  1. 先判断Redis中缓存是否命中, 如果命中则直接返回缓存信息
  2. 如果未命中缓存, 则使用临时校验码code去微信换取openid
  3. 查询数据库该openid对应用户信息是否存在, 存在则直接返回, 不存在则注册用户信息
  4. 最后将用户信息存入redis, 并返回相应的token供下次授权登录前判断是否缓存信息
@ApiOperation("微信授权登录")
@PostMapping("/customer_login")
public R<SysWxUser> customerLogin(@RequestBody WXAuth wxAuth) {
    return weixinService.customerLogin(wxAuth);
}
R<SysWxUser> customerLogin(WXAuth wxAuth);
/**
 * 授权登录
 *
 * @param wxAuth 授权信息
 * @return 结果
 */
@Override
public R<SysWxUser> customerLogin(WXAuth wxAuth) {
    String code = wxAuth.getCode();
    String iv = wxAuth.getIv();
    String token = wxAuth.getToken();
    String encryptedData = wxAuth.getEncryptedData();

    if(StrUtil.isBlank(code) && StrUtil.isBlank(token)){
        return R.fail(MessageConstants.PARAMS_ERROR);
    }

    // 判断登录态是否在有效期
    if(StrUtil.isNotBlank(token)){
        // token不为空  判断token是否过期
        String content = redisTemplate.opsForValue().get(RedisKey.WX_SESSION_KEY + token);
        if(content == null){
            return R.fail(MessageConstants.TOKEN_NOT_EXIST);
        }
        // 查询token对应用户信息
        SysWxUser info = JSONUtil.toBean(content, SysWxUser.class);
        // 刷新token有效时间
        redisTemplate.opsForValue().set(RedisKey.WX_SESSION_KEY + token, content, 3600, TimeUnit.SECONDS);
        return R.ok(info);
    }

    SysWxUser wxUser = null;
    WxMaJscode2SessionResult sessionInfo = null;
    try {
        sessionInfo = wxMaService.getUserService().getSessionInfo(code);
        WxMaUserInfo userInfo = wxMaService.getUserService().getUserInfo(sessionInfo.getSessionKey(), encryptedData, iv);
        
        String openid = sessionInfo.getOpenid();
        
        // 使用微信openId查询是否有此用户(WxUser)
        wxUser = lambdaQuery().eq(SysWxUser::getOpenid, openid).one();
        
        // 不存在 构建用户信息进行注册
        if(wxUser == null) {
            wxUser = new SysWxUser();
            wxUser.setOpenid(openid);
            wxUser.setWxName(Constants.WX_PREFIX + RandomUtil.randomString(6));
            wxUser.setAvatarUrl(userInfo.getAvatarUrl());
            int insert = wxUserMapper.insert(wxUser);
            if (insert == 0) {
                return R.fail(MessageConstants.USER_BIND_ERROR);
            }
        }
        
    } catch (Exception e) {
        e.printStackTrace();
        log.error(MessageConstants.SYSTEM_ERROR);
    }

    String sessionKey = sessionInfo.getSessionKey();
    String cacheKey = RedisKey.WX_SESSION_KEY + sessionKey;

    // 将 openid / sessionKey 存入redis
    redisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(wxUser), 3600, TimeUnit.SECONDS);
    return R.ok(wxUser,Constants.TOKEN_PRE + sessionKey);
}

部分实体类

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

    /** 成功 */
    public static final int SUCCESS = HttpStatus.SUCCESS;

    /** 失败 */
    public static final int FAIL = HttpStatus.ERROR;

    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(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;
    }

    public static <T> Boolean isError(R<T> ret)
    {
        return !isSuccess(ret);
    }

    public static <T> Boolean isSuccess(R<T> ret)
    {
        return R.SUCCESS == ret.getCode();
    }
}
/**
 * 用户微信信息表
 * @TableName sys_wx_user
 */
@TableName(value ="sys_wx_user")
@Data
public class SysWxUser implements Serializable {

    /**
     * 用户id
     */
    @TableId
    private Long id;

    /**
     * 用户姓名
     */
    private String name;

    /**
     * 用户绑定微信的唯一标识
     */
    private String openid;

    /**
     * 联系电话
     */
    private String phone;

    /**
     * 微信名称
     */
    private String wxName;

    /**
     * 微信头像在线地址
     */
    private String avatarUrl;

    /**
     * 删除标志
     */
    private String delFlag;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}
/**
 * @Author: Ccoo
 * @Date: 2024-03-21 16:42
 * version 1.0
 */
@Data
@Accessors(chain = true)
public class WXAuth {

    @ApiModelProperty("临时授权码")
    private String code;

    @ApiModelProperty("加密数据")
    private String encryptedData;

    @ApiModelProperty("加密算法的初始向量")
    private String iv;

    @ApiModelProperty("token")
    private String token;
    
}

二. 微信支付


引入依赖

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

编写配置文件

具体的参数配置需要去微信支付平台申请

微信支付 | 微信开放文档

wx:
  # 微信小程序appid
  app-id: wxcb25xxxxxxxxxxxx
  # 商户号
  mch-id: 16xxxxxxxx
  # 证书序列号
  mch-serial-no: 59BAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  # 小程序密钥
  app-secret: 8ccxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  # 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

编写相关配置

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

    /**
     * 设置微信小程序的Secret
     */
    private String appSecret;

    /**
     * 微信支付分配的商户号
     */
    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;
}
/**
 * 微信支付相关自动配置
 * @author Ccoo
 * 2024/3/24
 */
@Slf4j
@Configuration
public class WechatPayAutoConfiguration {

	@Autowired
	private WxProperties wxProperties;

	@Autowired
	private ResourceLoader resourceLoader;

	private static final String CLASS_PATH = "classpath:";


	/**
	 * 自动更新证书
	 *
	 * @return RSAAutoCertificateConfig
	 */
	@Bean
	public Config config() throws IOException {
		String path = CLASS_PATH + wxProperties.getCertPath();
		Resource resourceCert = resourceLoader.getResource(path);
		X509Certificate certificate = getCertificate(resourceCert.getInputStream());
		String merchantSerialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
		String privatePath = CLASS_PATH + wxProperties.getKeyPath();
		Resource resourcePrivate = resourceLoader.getResource(privatePath);
		String privateKey = inputStreamToString(resourcePrivate.getInputStream());
		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){
		JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
		return service;
	}

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

	/**
	 * 读取私钥文件,将文件流读取成string
	 *
	 * @param inputStream
	 * @return
	 * @throws IOException
	 */
	public String inputStreamToString(InputStream inputStream) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		StringBuilder stringBuilder = new StringBuilder();
		String line;
		while ((line = reader.readLine()) != null) {
			stringBuilder.append(line);
		}
		reader.close();
		return stringBuilder.toString();
	}

	/**
	 * 获取证书 将文件流转成证书文件
	 *
	 * @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);
		}
	}

}

微信支付相关代码

@Api(tags = {"支付管理接口"})
@Slf4j
@RestController
@RequestMapping("/pay")
@RequiredArgsConstructor
public class Paycontroller {
    private final Payservice payservice;
    private final WechatPayExternalService wechatPayService;

    @ApiOperation("微信预支付")
    @PostMapping("/createPreOrder")
    public R<?> createPreOrder(@RequestBody CreateOrderPayRequest createOrderPay) {
        return R.ok(wechatPayService.prepayWithRequestPayment(createOrderPay));
    }
    
    @ApiOperation("查询订单")
    @GetMapping("/queryOrder")
    public R<?> queryOrder(@RequestParam String outTradeNo) {
        return R.ok(wechatPayService.queryStatus(outTradeNo));
    }
    
    @ApiOperation("取消订单")
    @PostMapping("/closeOrder")
    public void closeOrder(@RequestParam String outTradeNo) {
        //方法没有返回值,意味着成功时API返回204 No Content
        wechatPayService.closeOrder(outTradeNo);
    }
    
}
/**
 * 提交预支付请求付款
 *
 * @param createOrderPay 订单请求体
 * @return PrepayWithRequestPaymentResponse 预付费与请求付款响应
 */
PayResponse prepayWithRequestPayment(CreateOrderPayRequest createOrderPay);
/**
 * 查询状态
 *
 * @param outTradeNo 商户支付no
 * @return 状态信息
 */
Transaction queryStatus(String outTradeNo);
/**
 * 取消订单
 *
 * @param outTradeNo
 */
void closeOrder(String outTradeNo);
/**
 * @author Ccoo
 * 2024/3/24
 */
@Slf4j
@Service
public class WechatPayExternalServiceImpl implements WechatPayExternalService {

	@Resource
	private WxProperties wxProperties;
	@Resource
	private JsapiServiceExtension jsapiServiceExtension;

	@Override
	public PayResponse prepayWithRequestPayment(CreateOrderPayRequest createOrderPay) {
		PrepayRequest request = new PrepayRequest();
		String orderId = createOrderPay.getOutTradeNo();

		// 设置支付金额
		Amount amount = new Amount();
		BigDecimal payMoney = createOrderPay.getPayMoney();
		BigDecimal amountTotal = payMoney.multiply(new BigDecimal("100").setScale(0, RoundingMode.DOWN));
		amount.setTotal(amountTotal.intValue());
		request.setAmount(amount);

		// 设置支付者
		Payer payer = new Payer();
		payer.setOpenid(createOrderPay.getOpenId());
		request.setPayer(payer);

		// 设置支付超时时间 10分钟
		request.setTimeExpire(getExpiredTimeStr());
		// 设置商户信息
		request.setAppid(wxProperties.getAppId());
		request.setMchid(wxProperties.getMchId());
		// 设置附加数据
		request.setAttach(String.valueOf(createOrderPay.getId()));
		// 设置商品描述
		request.setDescription(createOrderPay.getPayContent());
		// 设置通知地址
		request.setNotifyUrl(wxProperties.getNotifyUrl());

		// 这里生成流水号,后续用这个流水号与微信交互,查询订单状态
		request.setOutTradeNo(orderId);
		PrepayWithRequestPaymentResponse result;

		try {
			result = jsapiServiceExtension.prepayWithRequestPayment(request);
		} catch (HttpException e) {
			log.error("微信下单发送HTTP请求失败,错误信息:{}", e.getHttpRequest());
			throw new RuntimeException("微信下单发送HTTP请求失败", e);
		} catch (ServiceException e) {
			// 服务返回状态小于200或大于等于300,例如500
			log.error("微信下单服务状态错误,错误信息:{}", e.getErrorMessage());
			throw new RuntimeException("微信下单服务状态错误", e);
		} catch (MalformedMessageException e) {
			// 服务返回成功,返回体类型不合法,或者解析返回体失败
			log.error("服务返回成功,返回体类型不合法,或者解析返回体失败,错误信息:{}", e.getMessage());
			throw new RuntimeException("服务返回成功,返回体类型不合法,或者解析返回体失败", e);
		}

		return BeanUtil.copyProperties(result, PayResponse.class);
	}

	/**
	 * 查询订单状态
	 * @param outTradeNo 商户支付no
	 * @return 状态信息
	 */
	@Override
	public Transaction queryStatus(String outTradeNo) {

		QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();

		// 设置商户信息
		request.setMchid(wxProperties.getMchId());
		// 设置订单号
		request.setOutTradeNo(outTradeNo);

		try {
			return jsapiServiceExtension.queryOrderByOutTradeNo(request);
		} catch (ServiceException e) {
			log.error("订单查询失败,返回码:{},返回信息:{}", e.getErrorCode(), e.getErrorMessage());
			throw new RuntimeException("订单查询失败", e);
		}

	}

	/**
	 * 关闭订单
	 * @param outTradeNo 订单号
	 */
	@Override
	public void closeOrder(String outTradeNo) {
		CloseOrderRequest closeRequest = new CloseOrderRequest();

		// 设置商户信息
		closeRequest.setMchid(wxProperties.getMchId());
		// 设置订单号
		closeRequest.setOutTradeNo(outTradeNo);

		try {
			//方法没有返回值,意味着成功时API返回204 No Content
			jsapiServiceExtension.closeOrder(closeRequest);
		} catch (ServiceException e) {
			log.error("订单关闭失败,返回码:{},返回信息:{}", e.getErrorCode(), e.getErrorMessage());
			throw new RuntimeException("订单关闭失败", e);
		}

	}
}

部分实体类

/**
 * @author Ccoo
 * 2024/3/24
 */
@Data
public class CreateOrderPayRequest {

	/**
	 * 主键id
	 */
	private Long id;

	/**
	 * 商户支付no 和微信交互 查询订单使用(outTradeNo)
	 */
	private String outTradeNo;

	/**
	 * 用户openid
	 */
	private String openId;

	/**
	 * 支付金额
	 */
	private BigDecimal payMoney;

	/**
	 * 支付内容
	 */
	private String payContent;

}