|
@@ -0,0 +1,740 @@
|
|
|
|
|
+package shop.alien.store.strategy.payment.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.google.gson.annotations.SerializedName;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import okhttp3.*;
|
|
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+import shop.alien.entity.result.R;
|
|
|
|
|
+import shop.alien.entity.store.StoreInfo;
|
|
|
|
|
+import shop.alien.entity.store.StoreOrder;
|
|
|
|
|
+import shop.alien.entity.store.StorePaymentConfig;
|
|
|
|
|
+import shop.alien.entity.store.LifeDiscountCouponUser;
|
|
|
|
|
+import shop.alien.entity.store.RefundRecord;
|
|
|
|
|
+import shop.alien.mapper.LifeDiscountCouponUserMapper;
|
|
|
|
|
+import shop.alien.mapper.StoreInfoMapper;
|
|
|
|
|
+import shop.alien.mapper.StoreOrderMapper;
|
|
|
|
|
+import shop.alien.store.feign.DiningServiceFeign;
|
|
|
|
|
+import shop.alien.store.service.RefundRecordService;
|
|
|
|
|
+import shop.alien.store.service.StorePaymentConfigService;
|
|
|
|
|
+import shop.alien.store.strategy.payment.PaymentStrategy;
|
|
|
|
|
+import shop.alien.store.util.WXPayUtility;
|
|
|
|
|
+import shop.alien.util.common.UniqueRandomNumGenerator;
|
|
|
|
|
+import shop.alien.util.common.constant.DiscountCouponEnum;
|
|
|
|
|
+import shop.alien.util.common.constant.PaymentEnum;
|
|
|
|
|
+import shop.alien.util.system.OSUtil;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.io.UncheckedIOException;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.security.PrivateKey;
|
|
|
|
|
+import java.security.PublicKey;
|
|
|
|
|
+import java.security.Signature;
|
|
|
|
|
+import java.util.Base64;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.Enumeration;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 微信支付 — 服务商模式(特约商户 / partner)
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 使用微信支付 APIv3 服务商接口:下单、查单、退款均走 sp_mchid + sub_mchid。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * <b>【需您提供并写入配置】</b>(前缀 {@code payment.wechatPartnerPay},见各字段注释)
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>服务商商户号 {@code sp_mchid}、子商户号 {@code sub_mchid}</li>
|
|
|
|
|
+ * <li>服务商 AppID {@code sp_appid};若子商户有独立移动应用 AppID 则配置 {@code sub_appid}</li>
|
|
|
|
|
+ * <li>API 证书私钥、证书序列号、平台公钥、APIv3 密钥、回调 URL 等(与直连类似,但商户号为服务商)</li>
|
|
|
|
|
+ * <li>各接口路径若微信有变更,以官方文档为准,可通过配置覆盖默认值</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012062547">服务商模式产品文档</a>
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Component
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
|
|
|
|
|
+
|
|
|
|
|
+ private final RefundRecordService refundRecordService;
|
|
|
|
|
+ /** 按门店读取 store_info.wechat_sub_mchid(特约商户号),与前端传入的 storeId 对应 */
|
|
|
|
|
+ private final StoreInfoMapper storeInfoMapper;
|
|
|
|
|
+ /** 预下单时按业务订单号更新 store_order */
|
|
|
|
|
+ private final StoreOrderMapper storeOrderMapper;
|
|
|
|
|
+ /** 按门店解析子商户 AppID(sub_appid),与小程序策略一致 */
|
|
|
|
|
+ private final StorePaymentConfigService storePaymentConfigService;
|
|
|
|
|
+ /** 支付成功回调:核销优惠券(与小程序服务商策略一致) */
|
|
|
|
|
+ private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
|
|
|
|
|
+ /** 支付成功回调:重置餐桌/购物车逻辑在 alien-dining,通过 Feign 调用 */
|
|
|
|
|
+ private final DiningServiceFeign diningServiceFeign;
|
|
|
|
|
+ /** 复用直连实现中的退款单模型、HTTP 工具与 RefundRecord 构建逻辑(构建后覆盖 payType 为服务商类型) */
|
|
|
|
|
+ private final WeChatPaymentStrategyImpl weChatPaymentStrategy;
|
|
|
|
|
+
|
|
|
|
|
+ // ———— 【需您配置】基础域名(一般保持默认) ————
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.host:https://api.mch.weixin.qq.com}")
|
|
|
|
|
+ private String wechatPayApiHost;
|
|
|
|
|
+
|
|
|
|
|
+ /** 【需您配置】服务商 APP 下单路径,默认服务商 APP 支付 */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.prePayPath:/v3/pay/partner/transactions/app}")
|
|
|
|
|
+ private String prePayPath;
|
|
|
|
|
+
|
|
|
|
|
+ /** 【需您配置】按商户订单号查单(路径模板中的 {out_trade_no} 与直连一致) */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.searchOrderByOutTradeNoPath:/v3/pay/partner/transactions/out-trade-no/{out_trade_no}}")
|
|
|
|
|
+ private String searchOrderByOutTradeNoPath;
|
|
|
|
|
+
|
|
|
|
|
+ /** 【需您配置】退款申请路径(与直连相同) */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.refundPath:/v3/refund/domestic/refunds}")
|
|
|
|
|
+ private String refundPath;
|
|
|
|
|
+
|
|
|
|
|
+ // ———— 【需您配置】服务商与子商户标识 ————
|
|
|
|
|
+ /** 【需您配置】服务商 AppID(sp_appid) */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.spAppId}")
|
|
|
|
|
+ private String spAppId;
|
|
|
|
|
+
|
|
|
|
|
+ /** 【需您配置】服务商商户号(sp_mchid),请求 Authorization 与查单参数均使用 */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.spMchId}")
|
|
|
|
|
+ private String spMchId;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 子商户号 sub_mchid 从 {@link StoreInfo#getWechatSubMchid()}(表 store_info.wechat_sub_mchid)按前端传入的 storeId 查询,不再使用全局配置。
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 【可选配置】子商户 AppID(sub_appid)。
|
|
|
|
|
+ * 若与服务商共用或仅 sp_appid 调起支付,可留空;调起客户端支付包签名时优先用此值。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.subAppId:}")
|
|
|
|
|
+ private String subAppId;
|
|
|
|
|
+
|
|
|
|
|
+ // ———— 【需您配置】证书与密钥(与直连结构一致,一般为服务商商户证书) ————
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.win.privateKeyPath}")
|
|
|
|
|
+ private String privateWinKeyPath;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.linux.privateKeyPath}")
|
|
|
|
|
+ private String privateLinuxKeyPath;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.win.wechatPayPublicKeyFilePath}")
|
|
|
|
|
+ private String wechatWinPayPublicKeyFilePath;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.linux.wechatPayPublicKeyFilePath}")
|
|
|
|
|
+ private String wechatLinuxPayPublicKeyFilePath;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.merchantSerialNumber}")
|
|
|
|
|
+ private String merchantSerialNumber;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.wechatPayPublicKeyId}")
|
|
|
|
|
+ private String wechatPayPublicKeyId;
|
|
|
|
|
+
|
|
|
|
|
+ /** 【需您配置】支付结果通知 URL */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.prePayNotifyUrl}")
|
|
|
|
|
+ private String prePayNotifyUrl;
|
|
|
|
|
+
|
|
|
|
|
+ /** 【需您配置】退款结果通知 URL */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.refundNotifyUrl}")
|
|
|
|
|
+ private String refundNotifyUrl;
|
|
|
|
|
+
|
|
|
|
|
+ /** 支付结果通知 resource 解密(与小程序服务商策略一致,回调验签通过后解密) */
|
|
|
|
|
+ @Value("${payment.wechatPartnerPay.business.apiV3Key:}")
|
|
|
|
|
+ private String apiV3Key;
|
|
|
|
|
+
|
|
|
|
|
+ private PrivateKey privateKey;
|
|
|
|
|
+ private PublicKey wechatPayPublicKey;
|
|
|
|
|
+
|
|
|
|
|
+ private static final String POST_METHOD = "POST";
|
|
|
|
|
+ private static final String GET_METHOD = "GET";
|
|
|
|
|
+
|
|
|
|
|
+ @PostConstruct
|
|
|
|
|
+ public void loadPartnerCertificates() {
|
|
|
|
|
+ String privateKeyPath;
|
|
|
|
|
+ String wechatPayPublicKeyFilePath;
|
|
|
|
|
+ if ("windows".equals(OSUtil.getOsName())) {
|
|
|
|
|
+ privateKeyPath = privateWinKeyPath;
|
|
|
|
|
+ wechatPayPublicKeyFilePath = wechatWinPayPublicKeyFilePath;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ privateKeyPath = privateLinuxKeyPath;
|
|
|
|
|
+ wechatPayPublicKeyFilePath = wechatLinuxPayPublicKeyFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("[WeChatPartner] 加载服务商商户证书私钥与平台公钥,os={}, keyPath={}", OSUtil.getOsName(), privateKeyPath);
|
|
|
|
|
+ this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyPath);
|
|
|
|
|
+ this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R createPrePayOrder(String price, String subject) throws Exception {
|
|
|
|
|
+ return createPrePayOrder(price, subject, null, null, null, null, null, null, null, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R createPreAliPayOrder(String orderId, String amount, String subject, String smid) throws Exception {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R createPrePayOrder(String price, String subject, Integer storeId) throws Exception {
|
|
|
|
|
+ return createPrePayOrder(price, subject, null, null, storeId, null, null, null, null, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 与 {}
|
|
|
|
|
+ * 参数对齐:有 orderNo 时刷新 {@link StoreOrder} 的 pay_trade_no 及费用字段,商户单号与微信 out_trade_no 一致。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId,
|
|
|
|
|
+ Integer couponId, Integer payerId, String serviceFee, String discountAmount,
|
|
|
|
|
+ String payAmount) throws Exception {
|
|
|
|
|
+ String subMchid = resolveSubMchidFromStore(storeId);
|
|
|
|
|
+ log.info("[WeChatPartner] 创建预支付订单,price={}, subject={}, spMchId={}, storeId={}, orderNo={}, subMchid={}",
|
|
|
|
|
+ price, subject, spMchId, storeId, orderNo, subMchid != null ? subMchid : "(未解析)");
|
|
|
|
|
+ // 与小程序策略入参一致;服务商 APP 下单无需 sp_openid,仅记录便于与前端联调对照
|
|
|
|
|
+ log.debug("[WeChatPartner] 扩展参数 payer(APP 不使用)={}, couponId={}, payerId={}, serviceFee={}, discountAmount={}, payAmount={}",
|
|
|
|
|
+ payer, couponId, payerId, serviceFee, discountAmount, payAmount);
|
|
|
|
|
+ if (subMchid == null) {
|
|
|
|
|
+ return R.fail("请传入门店 storeId,且门店需在 store_info 中维护微信特约商户号 wechat_sub_mchid");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (price == null || price.trim().isEmpty()) {
|
|
|
|
|
+ return R.fail("价格不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (subject == null || subject.trim().isEmpty()) {
|
|
|
|
|
+ return R.fail("订单描述不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ BigDecimal amount = new BigDecimal(price);
|
|
|
|
|
+ if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
|
|
+ return R.fail("价格必须大于0");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return R.fail("价格格式不正确");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String wechatOutTradeNo;
|
|
|
|
|
+ if (StringUtils.isNotBlank(orderNo)) {
|
|
|
|
|
+ wechatOutTradeNo = orderNo.trim();
|
|
|
|
|
+ LambdaQueryWrapper<StoreOrder> ow = new LambdaQueryWrapper<>();
|
|
|
|
|
+ ow.eq(StoreOrder::getOrderNo, wechatOutTradeNo);
|
|
|
|
|
+ ow.eq(StoreOrder::getDeleteFlag, 0);
|
|
|
|
|
+ StoreOrder storeOrder = storeOrderMapper.selectOne(ow);
|
|
|
|
|
+ log.info("[WeChatPartner] createPrePayOrder orderNo={}, storeOrderFound={}, subMchid={}", orderNo, storeOrder != null, subMchid);
|
|
|
|
|
+
|
|
|
|
|
+ if (storeOrder != null) {
|
|
|
|
|
+ if (storeOrder.getPayStatus() != null && storeOrder.getPayStatus() == 1) {
|
|
|
|
|
+ return R.fail("订单已支付");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (storeOrder.getPayTradeNo() != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse wxOrder =
|
|
|
|
|
+ partnerSearchOrderRun(storeOrder.getPayTradeNo(), subMchid);
|
|
|
|
|
+ if (wxOrder != null && "SUCCESS".equals(wxOrder.tradeState)) {
|
|
|
|
|
+ return R.fail("该支付单已在微信侧支付成功,请勿重复发起支付");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (WXPayUtility.ApiException e) {
|
|
|
|
|
+ if (e.getStatusCode() != 404 && !"ORDER_NOT_EXIST".equals(e.getErrorCode())) {
|
|
|
|
|
+ log.error("[WeChatPartner] 预支付前查单失败 payTradeNo={}, code={}, msg={}",
|
|
|
|
|
+ storeOrder.getPayTradeNo(), e.getErrorCode(), e.getErrorMessage());
|
|
|
|
|
+ return R.fail("查询微信支付订单失败:" + (e.getErrorMessage() != null ? e.getErrorMessage() : e.getMessage()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ String newPayTradeNo = "WX" + storeOrder.getId() + "_" + System.currentTimeMillis();
|
|
|
|
|
+ storeOrder.setPayTradeNo(newPayTradeNo);
|
|
|
|
|
+ wechatOutTradeNo = newPayTradeNo;
|
|
|
|
|
+ log.info("[WeChatPartner] 换新商户单号 orderNo={}, payTradeNo={}", orderNo, newPayTradeNo);
|
|
|
|
|
+ storeOrder.setCouponId(couponId);
|
|
|
|
|
+ storeOrder.setPayUserId(payerId);
|
|
|
|
|
+ storeOrder.setServiceFee(parseAmountOrZero(serviceFee));
|
|
|
|
|
+ storeOrder.setDiscountAmount(parseAmountOrZero(discountAmount));
|
|
|
|
|
+ storeOrder.setPayAmount(parseAmountOrZero(payAmount));
|
|
|
|
|
+ if (storeOrderMapper.updateById(storeOrder) <= 0) {
|
|
|
|
|
+ log.error("[WeChatPartner] 更新订单失败 orderNo={}", orderNo);
|
|
|
|
|
+ return R.fail("更新订单失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ wechatOutTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ PartnerAppPrepayRequest request = new PartnerAppPrepayRequest();
|
|
|
|
|
+ request.spAppid = spAppId;
|
|
|
|
|
+ request.spMchid = spMchId;
|
|
|
|
|
+ request.subMchid = subMchid;
|
|
|
|
|
+ request.description = subject;
|
|
|
|
|
+ request.outTradeNo = wechatOutTradeNo;
|
|
|
|
|
+ request.notifyUrl = prePayNotifyUrl;
|
|
|
|
|
+ request.amount = new WeChatPaymentStrategyImpl.CommonAmountInfo();
|
|
|
|
|
+ request.amount.total = new BigDecimal(price).multiply(new BigDecimal(100)).longValue();
|
|
|
|
|
+ request.amount.currency = "CNY";
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse response = partnerPrePayOrderRun(request);
|
|
|
|
|
+ log.info("[WeChatPartner] 预下单成功 prepayId={}, outTradeNo={}", response.prepayId, request.outTradeNo);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, String> result = new HashMap<>();
|
|
|
|
|
+ result.put("prepayId", response.prepayId);
|
|
|
|
|
+ result.put("appId", spAppId);
|
|
|
|
|
+ result.put("spAppId", spAppId);
|
|
|
|
|
+ result.put("spMchId", spMchId);
|
|
|
|
|
+ result.put("subMchId", subMchid);
|
|
|
|
|
+ result.put("orderNo", request.outTradeNo);
|
|
|
|
|
+
|
|
|
|
|
+ long timestamp = System.currentTimeMillis() / 1000;
|
|
|
|
|
+ String nonce = WXPayUtility.createNonce(32);
|
|
|
|
|
+ String prepayId = response.prepayId;
|
|
|
|
|
+ String message = String.format("%s\n%s\n%s\n", timestamp, nonce, prepayId);
|
|
|
|
|
+ Signature sign = Signature.getInstance("SHA256withRSA");
|
|
|
|
|
+ sign.initSign(privateKey);
|
|
|
|
|
+ sign.update(message.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ result.put("sign", Base64.getEncoder().encodeToString(sign.sign()));
|
|
|
|
|
+ result.put("timestamp", String.valueOf(timestamp));
|
|
|
|
|
+ result.put("nonce", nonce);
|
|
|
|
|
+ return R.data(result);
|
|
|
|
|
+ } catch (WXPayUtility.ApiException e) {
|
|
|
|
|
+ log.error("[WeChatPartner] 预下单失败 code={}, body={}", e.getErrorCode(), e.getMessage());
|
|
|
|
|
+ return R.fail(e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 子商户 AppID:门店支付配置优先,否则使用全局 {@code payment.wechatPartnerPay.business.subAppId}。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String resolveSubAppId(StorePaymentConfig config) {
|
|
|
|
|
+ if (config != null) {
|
|
|
|
|
+ if (org.springframework.util.StringUtils.hasText(config.getWechatMiniAppId())) {
|
|
|
|
|
+ return config.getWechatMiniAppId().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (org.springframework.util.StringUtils.hasText(config.getWechatAppId())) {
|
|
|
|
|
+ return config.getWechatAppId().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isNotBlank(subAppId)) {
|
|
|
|
|
+ return subAppId.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 解析金额字符串,空或非法时记日志并返回 0,避免更新订单时 NPE */
|
|
|
|
|
+ private static BigDecimal parseAmountOrZero(String raw) {
|
|
|
|
|
+ if (raw == null || raw.trim().isEmpty()) {
|
|
|
|
|
+ return BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new BigDecimal(raw.trim());
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R handleNotify(String notifyData) throws Exception {
|
|
|
|
|
+ log.warn("[WeChatPartner回调] 缺少 HttpServletRequest,无法进行 APIv3 验签,请使用 POST /payment/weChatPartnerNotify");
|
|
|
|
|
+ return R.fail("请使用微信支付回调专用接口(需携带 Wechatpay-* 请求头)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 与 {} 对齐:
|
|
|
|
|
+ * 验签、解密 resource,异步更新订单/优惠券,并通过 Feign 调用 dining 重置餐桌。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R handleNotify(String notifyData, HttpServletRequest request) throws Exception {
|
|
|
|
|
+ log.info("[WeChatPartner回调] 进入 handleNotify, len={}", notifyData != null ? notifyData.length() : 0);
|
|
|
|
|
+ if (request == null) {
|
|
|
|
|
+ return R.fail("请求上下文缺失");
|
|
|
|
|
+ }
|
|
|
|
|
+ String serial = request.getHeader("Wechatpay-Serial");
|
|
|
|
|
+ String signature = request.getHeader("Wechatpay-Signature");
|
|
|
|
|
+ String timestamp = request.getHeader("Wechatpay-Timestamp");
|
|
|
|
|
+ String nonce = request.getHeader("Wechatpay-Nonce");
|
|
|
|
|
+
|
|
|
|
|
+ if (serial == null || signature == null || timestamp == null || nonce == null) {
|
|
|
|
|
+ log.warn("[WeChatPartner回调] 验签参数缺失 serial={}, signature={}, timestamp={}, nonce={}",
|
|
|
|
|
+ serial, signature != null, timestamp, nonce);
|
|
|
|
|
+ return R.fail("验签参数缺失");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (signature.startsWith("WECHATPAY/SIGNTEST/")) {
|
|
|
|
|
+ log.info("[WeChatPartner回调] 签名探测请求,直接成功");
|
|
|
|
|
+ return R.success("OK");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!StringUtils.equals(serial, wechatPayPublicKeyId)) {
|
|
|
|
|
+ log.warn("[WeChatPartner回调] Wechatpay-Serial 与配置不符 serial={}, expected={}", serial, wechatPayPublicKeyId);
|
|
|
|
|
+ return R.fail("公钥序列号不匹配");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (wechatPayPublicKey == null) {
|
|
|
|
|
+ log.error("[WeChatPartner回调] 平台公钥未加载");
|
|
|
|
|
+ return R.fail("平台公钥未就绪");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Headers okHeaders = buildOkHttpHeaders(request);
|
|
|
|
|
+
|
|
|
|
|
+ if (!org.springframework.util.StringUtils.hasText(apiV3Key)) {
|
|
|
|
|
+ log.error("[WeChatPartner回调] 未配置 payment.wechatPartnerPay.business.apiV3Key,无法解密");
|
|
|
|
|
+ return R.fail("APIv3 密钥未配置");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ WXPayUtility.Notification parsed = WXPayUtility.parseNotification(apiV3Key, wechatPayPublicKeyId,
|
|
|
|
|
+ wechatPayPublicKey, okHeaders, notifyData);
|
|
|
|
|
+ String plaintext = parsed.getPlaintext();
|
|
|
|
|
+ if (plaintext == null || plaintext.isEmpty()) {
|
|
|
|
|
+ log.warn("[WeChatPartner回调] 解密后业务数据为空");
|
|
|
|
|
+ return R.fail("解密结果为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ final String plainCopy = plaintext;
|
|
|
|
|
+ CompletableFuture.runAsync(() -> processPartnerNotifyBusiness(plainCopy));
|
|
|
|
|
+ return R.success("OK");
|
|
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
|
|
+ log.error("[WeChatPartner回调] 验签或解密失败: {}", e.getMessage());
|
|
|
|
|
+ return R.fail(e.getMessage() != null ? e.getMessage() : "处理失败");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[WeChatPartner回调] 处理异常", e);
|
|
|
|
|
+ return R.fail(e.getMessage() != null ? e.getMessage() : "处理异常");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Headers buildOkHttpHeaders(HttpServletRequest request) {
|
|
|
|
|
+ Headers.Builder b = new Headers.Builder();
|
|
|
|
|
+ Enumeration<String> names = request.getHeaderNames();
|
|
|
|
|
+ while (names != null && names.hasMoreElements()) {
|
|
|
|
|
+ String name = names.nextElement();
|
|
|
|
|
+ Enumeration<String> values = request.getHeaders(name);
|
|
|
|
|
+ while (values != null && values.hasMoreElements()) {
|
|
|
|
|
+ b.add(name, values.nextElement());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return b.build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 异步:解析支付成功报文,更新 store_order、优惠券,并通知 dining 重置餐桌(与小程序服务商回调业务一致)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void processPartnerNotifyBusiness(String plaintext) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ JSONObject jsonObject = JSONObject.parseObject(plaintext);
|
|
|
|
|
+ String tradeState = jsonObject.getString("trade_state");
|
|
|
|
|
+ if (!"SUCCESS".equals(tradeState)) {
|
|
|
|
|
+ log.info("[WeChatPartner回调] trade_state 非 SUCCESS: {}", tradeState);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ String outTradeNo = jsonObject.getString("out_trade_no");
|
|
|
|
|
+ if (outTradeNo == null || outTradeNo.isEmpty()) {
|
|
|
|
|
+ log.warn("[WeChatPartner回调] 缺少 out_trade_no");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ LambdaQueryWrapper<StoreOrder> byOrderNo = new LambdaQueryWrapper<>();
|
|
|
|
|
+ byOrderNo.eq(StoreOrder::getOrderNo, outTradeNo).eq(StoreOrder::getDeleteFlag, 0);
|
|
|
|
|
+ StoreOrder storeOrder = storeOrderMapper.selectOne(byOrderNo);
|
|
|
|
|
+ if (storeOrder == null) {
|
|
|
|
|
+ LambdaQueryWrapper<StoreOrder> byPayTrade = new LambdaQueryWrapper<>();
|
|
|
|
|
+ byPayTrade.eq(StoreOrder::getPayTradeNo, outTradeNo).eq(StoreOrder::getDeleteFlag, 0);
|
|
|
|
|
+ storeOrder = storeOrderMapper.selectOne(byPayTrade);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (storeOrder != null && !Integer.valueOf(1).equals(storeOrder.getPayStatus())) {
|
|
|
|
|
+ storeOrder.setPayStatus(1);
|
|
|
|
|
+ storeOrder.setOrderStatus(1);
|
|
|
|
|
+ storeOrder.setPayType(1);
|
|
|
|
|
+ storeOrder.setPayTime(new Date());
|
|
|
|
|
+ int rows = storeOrderMapper.updateById(storeOrder);
|
|
|
|
|
+ if (rows > 0) {
|
|
|
|
|
+ log.info("[WeChatPartner回调] 更新订单支付成功 outTradeNo={}", outTradeNo);
|
|
|
|
|
+ if (storeOrder.getCouponId() != null && storeOrder.getPayUserId() != null) {
|
|
|
|
|
+ LambdaQueryWrapper<LifeDiscountCouponUser> cw = new LambdaQueryWrapper<>();
|
|
|
|
|
+ cw.eq(LifeDiscountCouponUser::getUserId, storeOrder.getPayUserId());
|
|
|
|
|
+ cw.eq(LifeDiscountCouponUser::getCouponId, storeOrder.getCouponId());
|
|
|
|
|
+ cw.eq(LifeDiscountCouponUser::getStatus, Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
|
|
|
|
|
+ cw.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
|
|
|
|
|
+ cw.orderByDesc(LifeDiscountCouponUser::getCreatedTime);
|
|
|
|
|
+ cw.last("LIMIT 1");
|
|
|
|
|
+ LifeDiscountCouponUser couponUser = lifeDiscountCouponUserMapper.selectOne(cw);
|
|
|
|
|
+ if (couponUser != null) {
|
|
|
|
|
+ couponUser.setStatus(Integer.parseInt(DiscountCouponEnum.HAVE_BEEN_USED.getValue()));
|
|
|
|
|
+ couponUser.setUseTime(new Date());
|
|
|
|
|
+ lifeDiscountCouponUserMapper.updateById(couponUser);
|
|
|
|
|
+ log.info("[WeChatPartner回调] 优惠券已使用 id={}", couponUser.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ R<Void> feignRet =
|
|
|
|
|
+ diningServiceFeign.resetTableAfterPaymentInternal(storeOrder.getTableId(), storeOrder.getMenuType());
|
|
|
|
|
+ if (!R.isSuccess(feignRet)) {
|
|
|
|
|
+ log.error("[WeChatPartner回调] Feign 重置餐桌失败 tableId={}, msg={}",
|
|
|
|
|
+ storeOrder.getTableId(), feignRet != null ? feignRet.getMsg() : "null");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.info("[WeChatPartner回调] 已请求 dining 重置餐桌 tableId={}", storeOrder.getTableId());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[WeChatPartner回调] Feign 重置餐桌异常 tableId={}", storeOrder.getTableId(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.warn("[WeChatPartner回调] 更新订单影响行数为 0, orderId={}", storeOrder.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[WeChatPartner回调] 异步处理异常", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R searchOrderByOutTradeNoPath(String outTradeNo) throws Exception {
|
|
|
|
|
+ return searchOrderByOutTradeNoPath(outTradeNo, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R searchOrderByOutTradeNoPath(String outTradeNo, Integer storeId) throws Exception {
|
|
|
|
|
+ String subMchid = resolveSubMchidFromStore(storeId);
|
|
|
|
|
+ log.info("[WeChatPartner] 查单 outTradeNo={}, storeId={}, subMchid={}", outTradeNo, storeId,
|
|
|
|
|
+ subMchid != null ? subMchid : "(未解析)");
|
|
|
|
|
+ if (subMchid == null) {
|
|
|
|
|
+ return R.fail("请传入门店 storeId,且门店需在 store_info 中维护微信特约商户号 wechat_sub_mchid");
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse response = partnerSearchOrderRun(outTradeNo, subMchid);
|
|
|
|
|
+ return R.data(response);
|
|
|
|
|
+ } catch (WXPayUtility.ApiException e) {
|
|
|
|
|
+ log.error("[WeChatPartner] 查单失败 code={}, msg={}", e.getErrorCode(), e.getMessage());
|
|
|
|
|
+ return R.fail(e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String handleRefund(Map<String, String> params) throws Exception {
|
|
|
|
|
+ Integer refundStoreId = parseStoreIdFromParams(params);
|
|
|
|
|
+ String subMchid = resolveSubMchidFromStore(refundStoreId);
|
|
|
|
|
+ log.info("[WeChatPartner] 退款请求 storeId={}, subMchid={}, outTradeNo={}",
|
|
|
|
|
+ refundStoreId, subMchid != null ? subMchid : "(未解析)", params.get("outTradeNo"));
|
|
|
|
|
+ if (subMchid == null) {
|
|
|
|
|
+ return "退款失败:请在参数中传入 storeId,且门店需在 store_info 中维护微信特约商户号 wechat_sub_mchid";
|
|
|
|
|
+ }
|
|
|
|
|
+ PartnerRefundCreateRequest request = new PartnerRefundCreateRequest();
|
|
|
|
|
+ request.subMchid = subMchid;
|
|
|
|
|
+ request.outTradeNo = params.get("outTradeNo");
|
|
|
|
|
+ request.outRefundNo = UniqueRandomNumGenerator.generateUniqueCode(19);
|
|
|
|
|
+ request.reason = params.get("reason");
|
|
|
|
|
+ request.notifyUrl = refundNotifyUrl;
|
|
|
|
|
+ request.amount = new WeChatPaymentStrategyImpl.AmountReq();
|
|
|
|
|
+ request.amount.refund = new BigDecimal(params.get("refundAmount")).longValue();
|
|
|
|
|
+ request.amount.total = new BigDecimal(params.get("totalAmount")).longValue();
|
|
|
|
|
+ request.amount.currency = "CNY";
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[WeChatPartner] 退款 outTradeNo={}, outRefundNo={}, refundFen={}, totalFen={}",
|
|
|
|
|
+ request.outTradeNo, request.outRefundNo, request.amount.refund, request.amount.total);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ WeChatPaymentStrategyImpl.Refund response = partnerRefundRun(request);
|
|
|
|
|
+ String status = response.status != null ? response.status.name() : "UNKNOWN";
|
|
|
|
|
+ if ("SUCCESS".equals(status) || "PROCESSING".equals(status)) {
|
|
|
|
|
+ saveRefundRecord(() -> patchPayType(weChatPaymentStrategy.buildRefundRecordFromWeChatResponse(response,
|
|
|
|
|
+ request, params)));
|
|
|
|
|
+ return "调用成功";
|
|
|
|
|
+ }
|
|
|
|
|
+ log.error("[WeChatPartner] 退款未成功 status={}, outTradeNo={}", status, request.outTradeNo);
|
|
|
|
|
+ saveRefundRecord(() -> patchPayType(weChatPaymentStrategy.buildRefundRecordFromWeChatError(response, request, params, status)));
|
|
|
|
|
+ return "退款失败";
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[WeChatPartner] 退款异常 outTradeNo={}", request.outTradeNo, e);
|
|
|
|
|
+ saveRefundRecord(() -> patchPayType(weChatPaymentStrategy.buildRefundRecordFromWeChatException(request, params, e)));
|
|
|
|
|
+ return "退款处理异常:" + e.getMessage();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private RefundRecord patchPayType(RefundRecord record) {
|
|
|
|
|
+ if (record != null) {
|
|
|
|
|
+ record.setPayType(PaymentEnum.WECHAT_PAY_PARTNER.getType());
|
|
|
|
|
+ }
|
|
|
|
|
+ return record;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void saveRefundRecord(java.util.function.Supplier<RefundRecord> supplier) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ RefundRecord refundRecord = supplier.get();
|
|
|
|
|
+ if (refundRecord == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ long count = refundRecordService.lambdaQuery()
|
|
|
|
|
+ .eq(RefundRecord::getOutRefundNo, refundRecord.getOutRefundNo())
|
|
|
|
|
+ .count();
|
|
|
|
|
+ if (count == 0) {
|
|
|
|
|
+ refundRecordService.save(refundRecord);
|
|
|
|
|
+ log.info("[WeChatPartner] 已写入 RefundRecord,outRefundNo={}", refundRecord.getOutRefundNo());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[WeChatPartner] 保存 RefundRecord 失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R searchRefundRecordByOutRefundNo(String outRefundNo) throws Exception {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String getType() {
|
|
|
|
|
+ return PaymentEnum.WECHAT_PAY_PARTNER.getType();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据门店主键查询特约商户号;缺失或空则返回 null 并打日志,便于排查配置问题。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String resolveSubMchidFromStore(Integer storeId) {
|
|
|
|
|
+ if (storeId == null) {
|
|
|
|
|
+ log.warn("[WeChatPartner] storeId 为空,无法解析 wechat_sub_mchid");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ StoreInfo info = storeInfoMapper.selectById(storeId);
|
|
|
|
|
+ if (info == null) {
|
|
|
|
|
+ log.warn("[WeChatPartner] 门店不存在 storeId={}", storeId);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isBlank(info.getWechatSubMchid())) {
|
|
|
|
|
+ log.warn("[WeChatPartner] 门店未配置 wechat_sub_mchid storeId={}", storeId);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return info.getWechatSubMchid().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Integer parseStoreIdFromParams(Map<String, String> params) {
|
|
|
|
|
+ if (params == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String raw = params.get("storeId");
|
|
|
|
|
+ if (raw == null || raw.trim().isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Integer.valueOf(raw.trim());
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ log.warn("[WeChatPartner] 退款参数 storeId 非整数: {}", raw);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse partnerPrePayOrderRun(PartnerAppPrepayRequest request) {
|
|
|
|
|
+ String uri = prePayPath;
|
|
|
|
|
+ String reqBody = WXPayUtility.toJson(request);
|
|
|
|
|
+ log.debug("[WeChatPartner] POST {} bodyLen={}", uri, reqBody.length());
|
|
|
|
|
+
|
|
|
|
|
+ Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
|
|
|
|
|
+ reqBuilder.addHeader("Accept", "application/json");
|
|
|
|
|
+ reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
|
|
|
|
+ reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, POST_METHOD, uri, reqBody));
|
|
|
|
|
+ reqBuilder.addHeader("Content-Type", "application/json");
|
|
|
|
|
+ RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
|
|
|
|
+ reqBuilder.method(POST_METHOD, body);
|
|
|
|
|
+ Request httpRequest = reqBuilder.build();
|
|
|
|
|
+
|
|
|
|
|
+ OkHttpClient client = new OkHttpClient.Builder().build();
|
|
|
|
|
+ try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
|
|
|
|
+ String respBody = WXPayUtility.extractBody(httpResponse);
|
|
|
|
|
+ if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
|
|
|
|
+ WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
|
|
|
|
|
+ return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse.class);
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw new UncheckedIOException("WeChatPartner prepay request failed: " + uri, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse partnerSearchOrderRun(String outTradeNo, String subMchid) {
|
|
|
|
|
+ String uri = searchOrderByOutTradeNoPath.replace("{out_trade_no}", WXPayUtility.urlEncode(outTradeNo));
|
|
|
|
|
+ Map<String, Object> args = new HashMap<>();
|
|
|
|
|
+ args.put("sp_mchid", spMchId);
|
|
|
|
|
+ args.put("sub_mchid", subMchid);
|
|
|
|
|
+ String queryString = WXPayUtility.urlEncode(args);
|
|
|
|
|
+ if (!queryString.isEmpty()) {
|
|
|
|
|
+ uri = uri + "?" + queryString;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
|
|
|
|
|
+ reqBuilder.addHeader("Accept", "application/json");
|
|
|
|
|
+ reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
|
|
|
|
+ reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, GET_METHOD, uri, null));
|
|
|
|
|
+ reqBuilder.method(GET_METHOD, null);
|
|
|
|
|
+ Request httpRequest = reqBuilder.build();
|
|
|
|
|
+
|
|
|
|
|
+ OkHttpClient client = new OkHttpClient.Builder().build();
|
|
|
|
|
+ try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
|
|
|
|
+ String respBody = WXPayUtility.extractBody(httpResponse);
|
|
|
|
|
+ if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
|
|
|
|
+ WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
|
|
|
|
|
+ return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse.class);
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw new UncheckedIOException("WeChatPartner query order failed: " + uri, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private WeChatPaymentStrategyImpl.Refund partnerRefundRun(PartnerRefundCreateRequest request) {
|
|
|
|
|
+ String uri = refundPath;
|
|
|
|
|
+ String reqBody = WXPayUtility.toJson(request);
|
|
|
|
|
+
|
|
|
|
|
+ Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
|
|
|
|
|
+ reqBuilder.addHeader("Accept", "application/json");
|
|
|
|
|
+ reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
|
|
|
|
+ reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, POST_METHOD, uri, reqBody));
|
|
|
|
|
+ reqBuilder.addHeader("Content-Type", "application/json");
|
|
|
|
|
+ RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
|
|
|
|
+ reqBuilder.method(POST_METHOD, body);
|
|
|
|
|
+ Request httpRequest = reqBuilder.build();
|
|
|
|
|
+
|
|
|
|
|
+ OkHttpClient client = new OkHttpClient.Builder().build();
|
|
|
|
|
+ try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
|
|
|
|
+ String respBody = WXPayUtility.extractBody(httpResponse);
|
|
|
|
|
+ if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
|
|
|
|
+ WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
|
|
|
|
|
+ return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.Refund.class);
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw new UncheckedIOException("WeChatPartner refund failed: " + uri, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 服务商 APP 下单请求体(字段名与微信 APIv3 一致)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static class PartnerAppPrepayRequest {
|
|
|
|
|
+ @SerializedName("sp_appid")
|
|
|
|
|
+ public String spAppid;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("sp_mchid")
|
|
|
|
|
+ public String spMchid;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("sub_appid")
|
|
|
|
|
+ public String subAppid;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("sub_mchid")
|
|
|
|
|
+ public String subMchid;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("description")
|
|
|
|
|
+ public String description;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("out_trade_no")
|
|
|
|
|
+ public String outTradeNo;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("notify_url")
|
|
|
|
|
+ public String notifyUrl;
|
|
|
|
|
+
|
|
|
|
|
+ @SerializedName("amount")
|
|
|
|
|
+ public WeChatPaymentStrategyImpl.CommonAmountInfo amount;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 退款请求:在直连 {@link WeChatPaymentStrategyImpl.CreateRequest} 基础上增加 sub_mchid(服务商退款必填)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static class PartnerRefundCreateRequest extends WeChatPaymentStrategyImpl.CreateRequest {
|
|
|
|
|
+ @SerializedName("sub_mchid")
|
|
|
|
|
+ public String subMchid;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|