微信工具包
这里介绍一下微信工具包的使用。
功能介绍
支持的功能有:
引入公共依赖:
<dependency>
<groupId>com.wmeimob.fastboot</groupId>
<artifactId>wmeimob-spring-boot-starter-wechat-miniapp</artifactId>
</dependency>
引入公共依赖:
<dependency>
<groupId>com.wmeimob.fastboot</groupId>
<artifactId>wmeimob-spring-boot-starter-wechat-cp</artifactId>
</dependency>
引入公共依赖:
<dependency>
<groupId>com.wmeimob.fastboot</groupId>
<artifactId>wmeimob-spring-boot-starter-wechat-mp</artifactId>
</dependency>
引入公共依赖:
<dependency>
<groupId>com.wmeimob.fastboot</groupId>
<artifactId>wmeimob-spring-boot-starter-pay</artifactId>
</dependency>
微信小程序
最简配置如下:
wx:
miniapp:
appid: xxx
secret: xxxx
这是一个单 appid 场景。如果需要支持多 appid 场景,可以这么设置:
wx:
miniapp:
configs:
- appid: xxx
secret: xxxx
- appid: xxx
secret: xxxx
除此之外,还可以进行如下配置:
wx:
miniapp:
configStorage:
type: Memory #存储类型,支持(Memory,Jedis,RedisTemplate),默认 Memory
keyPrefix: wa #存储 key 前缀,默认 wa
httpClientType: HttpClient #http客户端类型,支持(HttpClient,OkHttp,JoddHttp),默认 HttpClient
httpProxyHost: #http代理主机
httpProxyPort: #http代理端口
httpProxyUsername: #http代理用户名
httpProxyPassword: #http代理密码
前台服务如果部署多台服务器,那储存类型可以根据实际情况调整成 Jedis 或 RedisTemplate。
代码中使用:
/**
* 小程序码控制器
*
* @author mjyang
* @date 2022/3/30 23:06
*/
@Api(tags = {"小程序码"})
@RestController
@RequestMapping("/api/qrCode")
@RequiredArgsConstructor
public class QrCodeController extends BaseController {
private final WxMaService wxMaService;
@ApiOperation(value = "获取小程序码 - getUnlimited")
@PostMapping("getUnlimited")
public JsonResult<String> getUnlimited(@RequestBody @Validated UnlimitedInputDto dto) throws WxErrorException {
if (StrUtil.isEmpty(dto.version)) {
dto.setVersion("release");
}
if (ObjectUtil.isNull(dto.getHyaline())) {
dto.setHyaline(Boolean.FALSE);
}
File file = this.wxMaService.getQrcodeService().createWxaCodeUnlimit(dto.scene, dto.page, false, dto.version,
430, true, null, dto.hyaline);
String base64 = "data:image/png;base64," + Base64.encode(file);
return JsonResult.ok(base64);
}
}
/**
* 小程序码控制器
*
* @author mjyang
* @date 2022/3/30 23:06
*/
@Api(tags = {"小程序码"})
@RestController
@RequestMapping("/api/qrCode")
@RequiredArgsConstructor
public class QrCodeController extends BaseController {
private final WxMaService wxMaService;
@ApiOperation(value = "获取小程序码 - getUnlimited")
@PostMapping("getUnlimited")
public JsonResult<String> getUnlimited(@RequestBody @Validated UnlimitedInputDto dto) throws WxErrorException {
String appid = wxMaConfigProperties.getAppid(dto.getType());
AssertUtil.isTrue(wxMaService.switchover(appid), "不支持的小程序类型");
if (StrUtil.isEmpty(dto.version)) {
dto.setVersion("release");
}
if (ObjectUtil.isNull(dto.getHyaline())) {
dto.setHyaline(Boolean.FALSE);
}
try {
File file = this.wxMaService.getQrcodeService().createWxaCodeUnlimit(dto.scene, dto.page, false, dto.version,
430, true, null, dto.hyaline);
String base64 = "data:image/png;base64," + Base64.encode(file);
return JsonResult.ok(base64);
} catch (WxErrorException e) {
log.error("获取小程序码 - getUnlimited failed! reason:", e);
throw SimpleException.getInstance(SysResStatus.ERROR_DATA_NULL, "小程序码生成失败");
} finally {
WxMaConfigHolder.remove();
}
}
}
多 appid 场景要注意新增的代码
微信企微
用法基本与小程序一致。配置 miniapp 换成 cp,WxMaService 换成 WxCpService。其中包里定义了一系列工具类。例如 CpUtils 里的通知回调验证。以及默认的路由处理策略。
微信公众号
用法基本与小程序一致。配置 miniapp 换成 mp,WxMaService 换成 WxMpService。
微信支付、退款
wx:
pay:
appId: ${wmeimob.wx.miniapp.appid}
mchId: xxx #支付商户号
mchKey: xxx #商户密钥
apiv3Key: xxx #v3密钥
publicKeyId: xxx #v3公钥id
publicKeyPath: xxx #v3公钥文件路径
publicKeyString: xxx #v3公钥字符串
host: #回调域名
pay-notify-url: ${wmeimob.wx.pay.host}/wechat/wepay/notify #支付回调地址
refund-notify-url: ${wmeimob.wx.pay.host}/wechat/wepay/refund-notify #退款回调地址
test: false #true表示测试,不需要发起支付
wx:
pay:
appId: ${wmeimob.wx.miniapp.appid}
mchId: xxx #支付商户号
mchKey: xxx #商户密钥
apiv3Key: xxx #v3密钥
publicKeyId: xxx #v3公钥id
publicKeyPath: xxx #v3公钥文件路径
publicKeyString: xxx #v3公钥字符串
host: #回调域名
pay-notify-url: ${wmeimob.wx.pay.host}/wechat/wepay/notify #支付回调地址
refund-notify-url: ${wmeimob.wx.pay.host}/wechat/wepay/refund-notify #退款回调地址
keyPath: xxxx #商户apiclient_cert.p12
privateKeyPath: xxxx #商户apiclient_key.pem
privateCertPath: xxxx #商户apiclient_cert.pem
test: false #true表示测试,不需要发起支付
注意 test 开关的配置,推荐根据环境去配置。例如 dev 为 true,prod 为 false。支付与退款需要同步设置。
v3支付需要传入公钥信息,参见这里
与标品的集成:
/**
* 支付服务策略
*
* @author mjyang
* @date 2022/8/26 10:01
*/
public interface PayService extends AbstractStrategy<Void, Void> {
/**
* 获取支付链接
*
* @param dto dto
* @return str
*/
Object payment(PaymentInputDto dto);
/**
* 查询支付结果
*
* @param dto dto
* @return obj
*/
default PaymentQueryOutputDto queryPayResult(PaymentQueryInputDto dto) {
throw SimpleException.getInstance("不支持该方式");
}
/**
* 解析支付结果
*
* @param data object data
* @return obj
*/
default Object parseOrderNotifyResult(Object data) {
throw SimpleException.getInstance("不支持该方式");
}
/**
* 解析退款结果
*
* @param data data
* @return obj
*/
default Object parseRefundNotifyResult(Object data) {
throw SimpleException.getInstance("不支持该方式");
}
/**
* 退款
*
* @param dto dto
*/
void refund(RefundInputDto dto);
/**
* 转账
*
* @param dto dto
* @return object
*/
default PartnerTransferResult transfer(TransferInputDto dto) {
throw SimpleException.getInstance("不支持该方式");
}
}
/**
* 微信支付服务类
*
* @author mjyang
* @date 2024/2/28 15:35
*/
@Service
@Slf4j
public class WxPayServiceImpl implements PayService {
@Resource
private WxPayService wxPayService;
@Override
@SneakyThrows
public Object payment(PaymentInputDto dto) {
WxPayUnifiedOrderRequest orderRequest = new WxPayUnifiedOrderRequest();
orderRequest.setBody(dto.getDescription());
orderRequest.setOutTradeNo(dto.getOrderNo());
// 元转成分
orderRequest.setTotalFee(NumberUtil.mul(dto.getPayAmount(), 100).intValue());
orderRequest.setSpbillCreateIp(IPUtils.getIpAddr());
orderRequest.setTimeStart(DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN));
orderRequest.setNotifyUrl(dto.getNotifyUrl());
orderRequest.setTradeType(dto.getTradeType());
if (StrUtil.equals(orderRequest.getTradeType(), WxPayConstants.TradeType.JSAPI)) {
orderRequest.setOpenid(dto.getUserId());
}
return this.wxPayService.createOrder(orderRequest);
}
@Override
public void refund(RefundInputDto dto) {
WxPayRefundRequest request = new WxPayRefundRequest();
request.setTransactionId(dto.getPaysn());
request.setOutRefundNo(dto.getOutRefundNo());
request.setTotalFee(NumberUtil.mul(dto.getTotalFee(), 100).intValue());
request.setRefundFee(NumberUtil.mul(dto.getRefundFee(), 100).intValue());
request.setNotifyUrl(dto.getNotifyUrl());
try {
this.wxPayService.refund(request);
} catch (WxPayException e) {
log.error("微信退款出现未知错误:" + dto.getOutRefundNo(), e);
if (StrUtil.equals(e.getErrCode(), WxPayErrorCode.Refund.NOTENOUGH)) {
// 余额不足
throw SimpleException.getInstance(e.getErrCodeDes());
}
throw SimpleException.getInstance("微信退款出现未知错误");
}
}
@Override
@SneakyThrows
public Object parseOrderNotifyResult(Object data) {
return this.wxPayService.parseOrderNotifyResult((String) data);
}
@Override
@SneakyThrows
public Object parseRefundNotifyResult(Object data) {
return this.wxPayService.parseRefundNotifyResult((String) data);
}
@Override
@SneakyThrows
public PartnerTransferResult transfer(TransferInputDto dto) {
PartnerTransferRequest transferRequest = new PartnerTransferRequest();
if (StrUtil.isBlank(this.wxPayService.getConfig().getSubMchId())) {
transferRequest.setSubMchid(this.wxPayService.getConfig().getMchId());
} else {
transferRequest.setSubMchid(this.wxPayService.getConfig().getSubMchId());
}
if (StrUtil.isBlank(this.wxPayService.getConfig().getSubAppId())) {
transferRequest.setSubAppid(this.wxPayService.getConfig().getAppId());
} else {
transferRequest.setSubAppid(this.wxPayService.getConfig().getSubAppId());
}
transferRequest.setAuthorizationType("INFORMATION_AUTHORIZATION_TYPE");
transferRequest.setOutBatchNo(dto.getOrderNo());
transferRequest.setBatchName(dto.getRemark());
transferRequest.setBatchRemark(dto.getRemark());
transferRequest.setTotalAmount(NumberUtil.mul(dto.getPayAmount(), 100).intValue());
transferRequest.setTotalNum(1);
List<PartnerTransferRequest.TransferDetail> transferDetails = new ArrayList<>(1);
PartnerTransferRequest.TransferDetail transferDetail = new PartnerTransferRequest.TransferDetail();
transferDetail.setOutDetailNo(dto.getOrderNo());
transferDetail.setTransferAmount(transferRequest.getTotalAmount());
transferDetail.setTransferRemark(transferRequest.getBatchRemark());
transferDetail.setOpenid(dto.getUserId());
transferDetail.setUserName(dto.getUserName());
transferDetails.add(transferDetail);
transferRequest.setTransferDetailList(transferDetails);
return this.wxPayService.getPartnerTransferService().batchTransfer(transferRequest);
}
@Override
public String mark() {
return PayType.WECHAT.getPayChannelStrategyKey();
}
}
例如下单使用:
@Resource
private AbstractStrategyFactory abstractStrategyFactory;
@ApiOperation("待支付订单获取支付参数")
@GetMapping("pay/{orderNo}")
public JsonResult<OrderPayDTO> payOrderJsApi(@PathVariable("orderNo") String orderNo,
@RequestParam("tradeType") @ApiParam(name = "tradeType", value = "交易方式", type = "com.wmeimob.mall.core.mall.order.value.TradeType") String tradeType,
@RequestParam("payType") @ApiParam(name = "payType", value = "支付类型", type = "com.wmeimob.mall.core.mall.order.value.PayType") Integer payType) {
return payOrder(
orderNo,
StrUtil.blankToDefault(tradeType, TradeType.JSAPI.name()),
Optional.ofNullable(payType).orElse(PayType.Wechat.getValue())
);
}
private JsonResult<OrderPayDTO> payOrder(String orderNo, String tradeType, Integer payType) {
AssertUtil.notNull(payType, "支付方式不能为空");
AssertUtil.isTrue(PayType.exist(payType), "错误的支付类型");
AssertUtil.isTrue(TradeType.exist(tradeType), "错误的交易类型");
OrderVO orderVO = this.orderService.payOrderCheck(orderNo, this.teancyUser.getUserId());
if (this.payProperties.getTest()) {
orderVO.setTransactionId(orderNo);
orderVO.setPayType(payType);
this.orderService.updateForPaySuccess(orderVO);
return JsonResult.ok();
}
String openId = MapUtil.getStr(this.teancyUser.getExtendInfo(), "openId");
Object payment = ((PayService) this.abstractStrategyFactory
.get(PayType.getInstance(payType).getPayChannelStrategyKey()))
.payment(new PaymentInputDto()
.setOrderNo(orderNo)
.setPayAmount(orderVO.getPayAmount())
.setDescription("普通订单")
.setNotifyUrl(this.payProperties.getPayNotifyUrl())
.setTradeType(tradeType)
.setUserId(openId)
);
OrderPayDTO orderPayDTO = new OrderPayDTO();
orderPayDTO.setPayParam(payment);
return JsonResult.ok(orderPayDTO);
}
TIP
注意这里用到了 策略 模式,后续有在新增支付渠道,只要在增加一个实现类即可。 同时标品里涉及到支付退款的地方有如下几处:
com.wmeimob.mall.web.controller.mall.OrdersController#payOrderJsApi
com.wmeimob.mall.service.mall.user.card.impl.MemCardServiceImpl#purchase
com.wmeimob.mall.service.mall.order.impl.RefundServiceImpl#refund com.wmeimob.mall.service.mall.user.balance.BalanceService#recharge #开启余额功能,则需要调整
更多详情可查看 这里