diff --git a/docs/adr/0003-h5-wechat-jsapi-payment.md b/docs/adr/0003-h5-wechat-jsapi-payment.md new file mode 100644 index 0000000..143cf3f --- /dev/null +++ b/docs/adr/0003-h5-wechat-jsapi-payment.md @@ -0,0 +1,12 @@ +# H5 端(公众号内)支付采用微信 JSAPI 直接唤起 + +汇吉采移动端 H5(`hjc-h5`,在微信「公众号」内打开)的支付采用**微信 JSAPI**(`PaymentType.WECHAT` + `openid`),由后端 `WechatJsapiStrategy` 用 `wechatpay-java` 的 `JsapiServiceExtension.prepayWithRequestPayment` 直接返回前端 `wx.chooseWXPay` 所需的 `appId/timeStamp/nonceStr/package/signType/paySign`;无 `openid`(非公众号环境)时**回退 Native 扫码**展示 `codeUrl`。 + +选择理由:公众号内直接「唤起支付」免去另开微信扫二维码的操作,转化率与体验均优于 Native 扫码;后端支付组件(`PaymentRequest.openId`、`PaymentResponse.wechatJsapi`、`PaymentChannel.WECHAT_JSAPI`、`WechatPayType.JSAPI`)早已为 JSAPI 预留,只差一个策略实现,改造成本低。 + +代价/偏离: +- **需要用户 openid**,因此引入微信**网页授权**(静默 `snsapi_base`):`/hjc/wechat/authorize` 生成授权地址,`/hjc/wechat/oauth/callback` 用 `code` 换 `openid` 后回跳 H5 并附带 `openid`。 +- **需要 `wx.config` 签名**:新增 `/hjc/wechat/jsapi-sign`(基于 `jsapi_ticket` 做 SHA1 签名),公众号配置(appId/appSecret)存于 Redis `cache{tenantId}:setting:wx-official`。 +- **不改 Native 扫码逻辑**:两者共用同一商户号/回调地址;`PaymentServiceImpl.getPaymentStrategy` 在 `WECHAT` 类型下按 `openid` 有无分流 JSAPI/Native,未破坏既有 Native 调用方。 + +兼容与说明:本方案是「带 openid 走 JSAPI,否则 Native」的弹性分流,非公众号环境(如普通浏览器/开发联调)仍可扫码支付,便于无真实公众号配置时验证。 diff --git a/src/main/java/com/gxwebsoft/hjc/controller/HjcOrderController.java b/src/main/java/com/gxwebsoft/hjc/controller/HjcOrderController.java index 8566ce1..5a1134d 100644 --- a/src/main/java/com/gxwebsoft/hjc/controller/HjcOrderController.java +++ b/src/main/java/com/gxwebsoft/hjc/controller/HjcOrderController.java @@ -120,6 +120,7 @@ public class HjcOrderController extends BaseController { if (orderNo == null) { return fail("订单号不能为空"); } + String openid = body.get("openid") == null ? null : String.valueOf(body.get("openid")); HjcOrder order = hjcOrderService.getByOrderNo(orderNo); if (order == null) { return fail("订单不存在"); @@ -134,7 +135,9 @@ public class HjcOrderController extends BaseController { PaymentRequest request = new PaymentRequest(); request.setTenantId(order.getTenantId()); request.setUserId(userId); - request.setPaymentType(PaymentType.WECHAT_NATIVE); + // 公众号内带 openid 走 JSAPI;否则回退 Native 扫码 + request.setPaymentType(PaymentType.WECHAT); + request.setOpenId(openid); request.setAmount(order.getTotalAmount()); request.setSubject(truncate(order.getProjectName(), 127)); request.setOrderNo(order.getOrderNo()); diff --git a/src/main/java/com/gxwebsoft/hjc/controller/HjcWechatController.java b/src/main/java/com/gxwebsoft/hjc/controller/HjcWechatController.java new file mode 100644 index 0000000..de4e33c --- /dev/null +++ b/src/main/java/com/gxwebsoft/hjc/controller/HjcWechatController.java @@ -0,0 +1,194 @@ +package com.gxwebsoft.hjc.controller; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSONObject; +import com.gxwebsoft.common.core.config.ConfigProperties; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 汇吉采微信公众号 H5 对接:网页授权(取 openid) 与 JS-SDK 签名(用于公众号内唤起支付)。 + * + * 依赖公众号配置:Redis 键 cache{tenantId}:setting:wx-official -> {"appId":"...","appSecret":"..."} + * + * @author WebSoft + * @since 2026-09 + */ +@Tag(name = "汇吉采-微信公众号") +@RestController +@RequestMapping("/api/hjc/wechat") +public class HjcWechatController extends BaseController { + + private static final String SETTING_KEY_PREFIX = "cache"; + private static final String SETTING_KEY_SUFFIX = ":setting:wx-official"; + private static final String ACCESS_TOKEN_KEY = "wx:jsapi:access_token:"; + private static final String JSAPI_TICKET_KEY = "wx:jsapi:ticket:"; + private static final long EXPIRE_SECONDS = 7000L; + + @Resource + private StringRedisTemplate stringRedisTemplate; + @Resource + private ConfigProperties configProperties; + + @Value("${spring.profiles.active:dev}") + private String activeProfile; + + @Operation(summary = "获取公众号网页授权地址(静默 snsapi_base)") + @GetMapping("/authorize") + public ApiResult authorize(@RequestParam(value = "redirect", required = false) String redirect) { + WxOfficialConfig cfg = loadConfig(); + if (cfg == null) { + return fail("公众号配置未找到,请先配置微信官方账号(appId/appSecret)"); + } + try { + String callback = configProperties.getServerUrl() + "/api/hjc/wechat/oauth/callback"; + String encodedCallback = URLEncoder.encode(callback, "UTF-8"); + String state = redirect == null ? configProperties.getServerUrl() : redirect; + String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + cfg.appId + + "&redirect_uri=" + encodedCallback + + "&response_type=code&scope=snsapi_base" + + "&state=" + URLEncoder.encode(state, "UTF-8") + + "#wechat_redirect"; + Map data = new HashMap<>(); + data.put("url", url); + return success("获取成功", data); + } catch (Exception e) { + return fail("获取授权地址失败:" + e.getMessage()); + } + } + + @Operation(summary = "公众号网页授权回调(code 换 openid 后回跳 H5)") + @GetMapping("/oauth/callback") + public void oauthCallback(@RequestParam(value = "code", required = false) String code, + @RequestParam(value = "state", required = false) String state, + HttpServletResponse response) throws IOException { + WxOfficialConfig cfg = loadConfig(); + if (cfg == null) { + response.sendRedirect(configProperties.getServerUrl()); + return; + } + String target = state == null || state.isEmpty() ? configProperties.getServerUrl() : state; + try { + String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + cfg.appId + + "&secret=" + cfg.appSecret + "&code=" + code + "&grant_type=authorization_code"; + String respBody = HttpUtil.get(url); + JSONObject json = JSONObject.parseObject(respBody); + String openid = json.getString("openid"); + if (openid != null && !openid.isEmpty()) { + target += (target.contains("?") ? "&" : "?") + "openid=" + openid; + } + } catch (Exception e) { + // 忽略,仍回跳 + } + response.sendRedirect(target); + } + + @Operation(summary = "生成微信 JS-SDK 签名(用于 wx.config)") + @GetMapping("/jsapi-sign") + public ApiResult jsapiSign(@RequestParam(value = "url", required = false) String url) { + WxOfficialConfig cfg = loadConfig(); + if (cfg == null) { + return fail("公众号配置未找到"); + } + if (url == null || url.isEmpty()) { + return fail("url参数不能为空"); + } + try { + String accessToken = getAccessToken(cfg); + String jsapiTicket = getJsapiTicket(cfg, accessToken); + String nonceStr = RandomUtil.randomString(16); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + String signatureStr = "jsapi_ticket=" + jsapiTicket + + "&noncestr=" + nonceStr + + "×tamp=" + timestamp + + "&url=" + url; + String signature = SecureUtil.sha1(signatureStr); + Map data = new HashMap<>(); + data.put("appId", cfg.appId); + data.put("timestamp", timestamp); + data.put("nonceStr", nonceStr); + data.put("signature", signature); + return success("获取成功", data); + } catch (Exception e) { + return fail("生成JS-SDK签名失败:" + e.getMessage()); + } + } + + private String getAccessToken(WxOfficialConfig cfg) { + String key = ACCESS_TOKEN_KEY + getTenantId(); + String cached = stringRedisTemplate.opsForValue().get(key); + if (cached != null && !cached.isEmpty()) { + return cached; + } + String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + cfg.appId + "&secret=" + cfg.appSecret; + String respBody = HttpUtil.get(url); + JSONObject json = JSONObject.parseObject(respBody); + String token = json.getString("access_token"); + if (token != null) { + stringRedisTemplate.opsForValue().set(key, token, EXPIRE_SECONDS, TimeUnit.SECONDS); + } + if (token == null) { + throw new IllegalStateException("获取微信access_token失败: " + json.getString("errmsg")); + } + return token; + } + + private String getJsapiTicket(WxOfficialConfig cfg, String accessToken) { + String key = JSAPI_TICKET_KEY + getTenantId(); + String cached = stringRedisTemplate.opsForValue().get(key); + if (cached != null && !cached.isEmpty()) { + return cached; + } + String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + accessToken + "&type=jsapi"; + String respBody = HttpUtil.get(url); + JSONObject json = JSONObject.parseObject(respBody); + String ticket = json.getString("ticket"); + if (ticket != null) { + stringRedisTemplate.opsForValue().set(key, ticket, EXPIRE_SECONDS, TimeUnit.SECONDS); + } + if (ticket == null) { + throw new IllegalStateException("获取微信jsapi_ticket失败: " + json.getString("errmsg")); + } + return ticket; + } + + private WxOfficialConfig loadConfig() { + Integer tenantId = getTenantId(); + String key = SETTING_KEY_PREFIX + tenantId + SETTING_KEY_SUFFIX; + String raw = stringRedisTemplate.opsForValue().get(key); + if (raw == null || raw.isEmpty()) { + return null; + } + JSONObject json = JSONObject.parseObject(raw); + WxOfficialConfig cfg = new WxOfficialConfig(); + cfg.appId = json.getString("appId"); + cfg.appSecret = json.getString("appSecret"); + if (cfg.appId == null || cfg.appSecret == null) { + return null; + } + return cfg; + } + + private static class WxOfficialConfig { + String appId; + String appSecret; + } +} diff --git a/src/main/java/com/gxwebsoft/payment/service/impl/PaymentServiceImpl.java b/src/main/java/com/gxwebsoft/payment/service/impl/PaymentServiceImpl.java index a01dafe..7b7d6c9 100644 --- a/src/main/java/com/gxwebsoft/payment/service/impl/PaymentServiceImpl.java +++ b/src/main/java/com/gxwebsoft/payment/service/impl/PaymentServiceImpl.java @@ -93,7 +93,7 @@ public class PaymentServiceImpl implements PaymentService { validatePaymentRequest(request); // 获取支付策略 - PaymentStrategy strategy = getPaymentStrategy(request.getPaymentType()); + PaymentStrategy strategy = getPaymentStrategy(request.getPaymentType(), request); // 执行支付 PaymentResponse response = strategy.createPayment(request); @@ -161,7 +161,7 @@ public class PaymentServiceImpl implements PaymentService { validateQueryParams(orderNo, paymentType, tenantId); // 获取支付策略 - PaymentStrategy strategy = getPaymentStrategy(paymentType); + PaymentStrategy strategy = getPaymentStrategy(paymentType, null); // 检查是否支持查询 if (!strategy.supportQuery()) { @@ -197,7 +197,7 @@ public class PaymentServiceImpl implements PaymentService { validateNotifyParams(paymentType, headers, body, tenantId); // 获取支付策略 - PaymentStrategy strategy = getPaymentStrategy(paymentType); + PaymentStrategy strategy = getPaymentStrategy(paymentType, null); // 检查是否需要异步通知 if (!strategy.needNotify()) { @@ -236,7 +236,7 @@ public class PaymentServiceImpl implements PaymentService { validateRefundParams(orderNo, refundNo, paymentType, totalAmount, refundAmount, tenantId); // 获取支付策略 - PaymentStrategy strategy = getPaymentStrategy(paymentType); + PaymentStrategy strategy = getPaymentStrategy(paymentType, null); // 检查是否支持退款 if (!strategy.supportRefund()) { @@ -272,7 +272,7 @@ public class PaymentServiceImpl implements PaymentService { validateRefundQueryParams(refundNo, paymentType, tenantId); // 获取支付策略 - PaymentStrategy strategy = getPaymentStrategy(paymentType); + PaymentStrategy strategy = getPaymentStrategy(paymentType, null); // 检查是否支持退款查询 if (!strategy.supportRefund()) { @@ -308,7 +308,7 @@ public class PaymentServiceImpl implements PaymentService { validateCloseParams(orderNo, paymentType, tenantId); // 获取支付策略 - PaymentStrategy strategy = getPaymentStrategy(paymentType); + PaymentStrategy strategy = getPaymentStrategy(paymentType, null); // 检查是否支持关闭订单 if (!strategy.supportClose()) { @@ -456,12 +456,14 @@ public class PaymentServiceImpl implements PaymentService { /** * 获取支付策略 */ - private PaymentStrategy getPaymentStrategy(PaymentType paymentType) throws PaymentException { - // 如果是WECHAT支付类型,转换为WECHAT_NATIVE - // 因为WECHAT是一个通用类型,实际的支付策略是WECHAT_NATIVE + private PaymentStrategy getPaymentStrategy(PaymentType paymentType, PaymentRequest request) throws PaymentException { + // WECHAT 是通用类型:带 openid 走 JSAPI(公众号内唤起),否则走 Native 扫码 PaymentType actualPaymentType = paymentType; if (paymentType == PaymentType.WECHAT) { - actualPaymentType = PaymentType.WECHAT_NATIVE; + boolean hasOpenId = request != null + && request.getOpenId() != null + && !request.getOpenId().trim().isEmpty(); + actualPaymentType = hasOpenId ? PaymentType.WECHAT : PaymentType.WECHAT_NATIVE; } PaymentStrategy strategy = strategyMap.get(actualPaymentType); diff --git a/src/main/java/com/gxwebsoft/payment/strategy/WechatJsapiStrategy.java b/src/main/java/com/gxwebsoft/payment/strategy/WechatJsapiStrategy.java new file mode 100644 index 0000000..2e48d0c --- /dev/null +++ b/src/main/java/com/gxwebsoft/payment/strategy/WechatJsapiStrategy.java @@ -0,0 +1,268 @@ +package com.gxwebsoft.payment.strategy; + +import cn.hutool.core.util.IdUtil; +import com.gxwebsoft.common.system.entity.Payment; +import com.gxwebsoft.payment.constants.PaymentConstants; +import com.gxwebsoft.payment.dto.PaymentRequest; +import com.gxwebsoft.payment.dto.PaymentResponse; +import com.gxwebsoft.payment.enums.PaymentStatus; +import com.gxwebsoft.payment.enums.PaymentType; +import com.gxwebsoft.payment.exception.PaymentException; +import com.gxwebsoft.payment.service.WxPayConfigService; +import com.gxwebsoft.payment.service.WxPayNotifyService; +import com.wechat.pay.java.core.Config; +import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension; +import com.wechat.pay.java.service.payments.jsapi.model.Amount; +import com.wechat.pay.java.service.payments.jsapi.model.Payer; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse; +import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest; +import com.wechat.pay.java.service.payments.model.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.Map; + +/** + * 微信公众号 JSAPI 支付策略实现 + * 用于公众号内 H5 直接唤起微信支付(需用户 openid)。 + * + * @author WebSoft + * @since 2026-09 + */ +@Slf4j +@Component +public class WechatJsapiStrategy implements PaymentStrategy { + + @Resource + private WxPayConfigService wxPayConfigService; + @Resource + private WxPayNotifyService wxPayNotifyService; + + @Override + public PaymentType getSupportedPaymentType() { + return PaymentType.WECHAT; + } + + @Override + public void validateRequest(PaymentRequest request) throws PaymentException { + if (request == null) { + throw PaymentException.paramError("支付请求不能为空"); + } + if (request.getTenantId() == null) { + throw PaymentException.paramError("租户ID不能为空"); + } + if (request.getUserId() == null) { + throw PaymentException.paramError("用户ID不能为空"); + } + if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) { + throw PaymentException.amountError("支付金额必须大于0"); + } + if (!StringUtils.hasText(request.getSubject())) { + throw PaymentException.paramError("订单标题不能为空"); + } + // JSAPI 必须携带 openid + if (!StringUtils.hasText(request.getOpenId())) { + throw PaymentException.paramError("微信JSAPI支付必须提供openid"); + } + if (request.getAmount().compareTo(new BigDecimal("0.01")) < 0) { + throw PaymentException.amountError("支付金额不能小于0.01元"); + } + if (request.getAmount().compareTo(new BigDecimal("999999.99")) > 0) { + throw PaymentException.amountError("支付金额不能超过999999.99元"); + } + } + + @Override + public PaymentResponse createPayment(PaymentRequest request) throws PaymentException { + log.info("{}, 支付类型: {}, 租户ID: {}, 金额: {}", PaymentConstants.LogMessage.PAYMENT_START, getSupportedPaymentType(), request.getTenantId(), request.getFormattedAmount()); + try { + validateRequest(request); + String orderNo = generateOrderNo(request); + + Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(request.getTenantId()); + Config wxPayConfig = wxPayConfigService.getWxPayConfig(request.getTenantId()); + + PrepayRequest prepayRequest = buildPrepayRequest(request, orderNo, paymentConfig); + + JsapiServiceExtension extension = new JsapiServiceExtension.Builder().config(wxPayConfig).build(); + PrepayWithRequestPaymentResponse resp = extension.prepayWithRequestPayment(prepayRequest); + if (resp == null) { + throw PaymentException.networkError("微信JSAPI支付API返回数据异常", PaymentType.WECHAT, null); + } + + PaymentResponse.WechatPayParams params = new PaymentResponse.WechatPayParams(); + params.setAppId(resp.getAppId()); + params.setTimeStamp(resp.getTimeStamp()); + params.setNonceStr(resp.getNonceStr()); + params.setPackageValue(resp.getPackageVal()); + params.setSignType(resp.getSignType()); + params.setPaySign(resp.getPaySign()); + + PaymentResponse response = PaymentResponse.wechatJsapi(orderNo, params, request.getAmount(), request.getTenantId()); + response.setUserId(request.getUserId()); + + log.info("{}, 支付类型: {}, 租户ID: {}, 订单号: {}, 金额: {}", PaymentConstants.LogMessage.PAYMENT_SUCCESS, getSupportedPaymentType(), request.getTenantId(), orderNo, request.getFormattedAmount()); + return response; + } catch (PaymentException e) { + log.error("{}, 支付类型: {}, 租户ID: {}, 错误: {}", PaymentConstants.LogMessage.PAYMENT_FAILED, getSupportedPaymentType(), request.getTenantId(), e.getMessage()); + throw e; + } catch (Exception e) { + log.error("{}, 支付类型: {}, 租户ID: {}, 系统错误: {}", PaymentConstants.LogMessage.PAYMENT_FAILED, getSupportedPaymentType(), request.getTenantId(), e.getMessage(), e); + throw PaymentException.systemError("微信JSAPI支付创建失败: " + e.getMessage(), e); + } + } + + @Override + public PaymentResponse queryPayment(String orderNo, Integer tenantId) throws PaymentException { + log.info("开始查询微信JSAPI支付状态, 订单号: {}, 租户ID: {}", orderNo, tenantId); + try { + if (!StringUtils.hasText(orderNo)) { + throw PaymentException.paramError("订单号不能为空"); + } + if (tenantId == null) { + throw PaymentException.paramError("租户ID不能为空"); + } + Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(tenantId); + Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId); + QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest(); + queryRequest.setOutTradeNo(orderNo); + queryRequest.setMchid(paymentConfig.getMchId()); + + JsapiServiceExtension extension = new JsapiServiceExtension.Builder().config(wxPayConfig).build(); + Transaction transaction = extension.queryOrderByOutTradeNo(queryRequest); + if (transaction == null) { + throw PaymentException.systemError("微信支付查询返回空结果", null); + } + PaymentResponse response = new PaymentResponse(); + response.setSuccess(true); + response.setOrderNo(orderNo); + response.setPaymentStatus(convertWechatPaymentStatus(transaction.getTradeState())); + response.setTenantId(tenantId); + response.setPaymentType(PaymentType.WECHAT); + if (transaction.getAmount() != null) { + response.setAmount(new BigDecimal(transaction.getAmount().getTotal()).divide(new BigDecimal("100"))); + } + if (transaction.getTransactionId() != null) { + response.setTransactionId(transaction.getTransactionId()); + } + log.info("微信JSAPI支付状态查询成功, 订单号: {}, 状态: {}", orderNo, response.getPaymentStatus()); + return response; + } catch (PaymentException e) { + throw e; + } catch (Exception e) { + log.error("查询微信JSAPI支付状态失败, 订单号: {}, 错误: {}", orderNo, e.getMessage(), e); + throw PaymentException.networkError("查询微信JSAPI支付状态失败: " + e.getMessage(), PaymentType.WECHAT, e); + } + } + + @Override + public String handleNotify(Map headers, String body, Integer tenantId) throws PaymentException { + log.info("{}, 支付类型: {}, 租户ID: {}", PaymentConstants.LogMessage.NOTIFY_START, getSupportedPaymentType(), tenantId); + try { + return wxPayNotifyService.handlePaymentNotify(headers, body, tenantId); + } catch (Exception e) { + log.error("{}, 支付类型: {}, 租户ID: {}, 错误: {}", PaymentConstants.LogMessage.NOTIFY_FAILED, getSupportedPaymentType(), tenantId, e.getMessage()); + throw PaymentException.systemError("微信支付回调处理失败: " + e.getMessage(), e); + } + } + + @Override + public PaymentResponse refund(String orderNo, String refundNo, BigDecimal totalAmount, BigDecimal refundAmount, String reason, Integer tenantId) throws PaymentException { + throw PaymentException.unsupportedPayment("微信JSAPI支付暂不支持直接退款,请走订单退款流程", PaymentType.WECHAT); + } + + @Override + public PaymentResponse queryRefund(String refundNo, Integer tenantId) throws PaymentException { + throw PaymentException.unsupportedPayment("微信JSAPI支付暂不支持退款查询", PaymentType.WECHAT); + } + + @Override + public boolean closeOrder(String orderNo, Integer tenantId) throws PaymentException { + throw PaymentException.unsupportedPayment("暂不支持微信订单关闭", PaymentType.WECHAT); + } + + @Override + public boolean supportRefund() { + return false; + } + + @Override + public boolean supportQuery() { + return true; + } + + @Override + public boolean supportClose() { + return false; + } + + @Override + public boolean needNotify() { + return true; + } + + private String generateOrderNo(PaymentRequest request) { + if (StringUtils.hasText(request.getOrderNo())) { + return request.getOrderNo(); + } + return Long.toString(IdUtil.getSnowflakeNextId()); + } + + private PrepayRequest buildPrepayRequest(PaymentRequest request, String orderNo, Payment paymentConfig) { + PrepayRequest prepayRequest = new PrepayRequest(); + prepayRequest.setAppid(paymentConfig.getAppId()); + prepayRequest.setMchid(paymentConfig.getMchId()); + + Amount amount = new Amount(); + amount.setTotal(request.getAmountInCents()); + amount.setCurrency(PaymentConstants.Wechat.CURRENCY); + prepayRequest.setAmount(amount); + + prepayRequest.setOutTradeNo(orderNo); + prepayRequest.setDescription(request.getEffectiveDescription()); + + Payer payer = new Payer(); + payer.setOpenid(request.getOpenId()); + prepayRequest.setPayer(payer); + + String notifyUrl = null; + if (StringUtils.hasText(request.getNotifyUrl())) { + notifyUrl = request.getNotifyUrl(); + } else if (StringUtils.hasText(paymentConfig.getNotifyUrl())) { + notifyUrl = paymentConfig.getNotifyUrl(); + } else { + throw new RuntimeException("回调通知地址不能为空,请在支付请求中设置notifyUrl或在支付配置中设置notifyUrl"); + } + prepayRequest.setNotifyUrl(notifyUrl); + + log.info("创建微信JSAPI支付订单 - 订单号: {}, 商户号: {}, 金额: {}分, openid: {}", orderNo, paymentConfig.getMchId(), request.getAmountInCents(), request.getOpenId()); + return prepayRequest; + } + + private PaymentStatus convertWechatPaymentStatus(Transaction.TradeStateEnum tradeState) { + if (tradeState == null) { + return PaymentStatus.PENDING; + } + switch (tradeState) { + case SUCCESS: + return PaymentStatus.SUCCESS; + case REFUND: + return PaymentStatus.REFUNDED; + case NOTPAY: + return PaymentStatus.PENDING; + case CLOSED: + case REVOKED: + return PaymentStatus.CANCELLED; + case USERPAYING: + return PaymentStatus.PROCESSING; + case PAYERROR: + return PaymentStatus.FAILED; + default: + return PaymentStatus.PENDING; + } + } +}