feat(hjc): 支持公众号H5 JSAPI支付 + 微信网页授权/JS-SDK签名

- 新增 WechatJsapiStrategy(WECHAT):wechatpay-java JsapiServiceExtension.prepayWithRequestPayment
  返回 wx.chooseWXPay 所需参数;带 openid 走 JSAPI,否则回退 Native 扫码
- PaymentServiceImpl.getPaymentStrategy:WECHAT 按 openid 有无分流 JSAPI/Native
- HjcOrderController.pay:读取 openid,改用 WECHAT 类型
- 新增 HjcWechatController:/authorize 生成公众号授权地址、/oauth/callback code换openid回跳、
  /jsapi-sign 生成 wx.config 签名(jsapi_ticket,公众号配置存 redis cache{tenantId}:setting:wx-official)
- ADR 0003 记录 JSAPI 决策
This commit is contained in:
2026-09-09 16:38:16 +08:00
parent 9b050c9dc5
commit 32a41bde6d
5 changed files with 490 additions and 11 deletions
+12
View File
@@ -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」的弹性分流,非公众号环境(如普通浏览器/开发联调)仍可扫码支付,便于无真实公众号配置时验证。
@@ -120,6 +120,7 @@ public class HjcOrderController extends BaseController {
if (orderNo == null) { if (orderNo == null) {
return fail("订单号不能为空"); return fail("订单号不能为空");
} }
String openid = body.get("openid") == null ? null : String.valueOf(body.get("openid"));
HjcOrder order = hjcOrderService.getByOrderNo(orderNo); HjcOrder order = hjcOrderService.getByOrderNo(orderNo);
if (order == null) { if (order == null) {
return fail("订单不存在"); return fail("订单不存在");
@@ -134,7 +135,9 @@ public class HjcOrderController extends BaseController {
PaymentRequest request = new PaymentRequest(); PaymentRequest request = new PaymentRequest();
request.setTenantId(order.getTenantId()); request.setTenantId(order.getTenantId());
request.setUserId(userId); request.setUserId(userId);
request.setPaymentType(PaymentType.WECHAT_NATIVE); // 公众号内带 openid 走 JSAPI;否则回退 Native 扫码
request.setPaymentType(PaymentType.WECHAT);
request.setOpenId(openid);
request.setAmount(order.getTotalAmount()); request.setAmount(order.getTotalAmount());
request.setSubject(truncate(order.getProjectName(), 127)); request.setSubject(truncate(order.getProjectName(), 127));
request.setOrderNo(order.getOrderNo()); request.setOrderNo(order.getOrderNo());
@@ -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<String, Object> 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 +
"&timestamp=" + timestamp +
"&url=" + url;
String signature = SecureUtil.sha1(signatureStr);
Map<String, Object> 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;
}
}
@@ -93,7 +93,7 @@ public class PaymentServiceImpl implements PaymentService {
validatePaymentRequest(request); validatePaymentRequest(request);
// 获取支付策略 // 获取支付策略
PaymentStrategy strategy = getPaymentStrategy(request.getPaymentType()); PaymentStrategy strategy = getPaymentStrategy(request.getPaymentType(), request);
// 执行支付 // 执行支付
PaymentResponse response = strategy.createPayment(request); PaymentResponse response = strategy.createPayment(request);
@@ -161,7 +161,7 @@ public class PaymentServiceImpl implements PaymentService {
validateQueryParams(orderNo, paymentType, tenantId); validateQueryParams(orderNo, paymentType, tenantId);
// 获取支付策略 // 获取支付策略
PaymentStrategy strategy = getPaymentStrategy(paymentType); PaymentStrategy strategy = getPaymentStrategy(paymentType, null);
// 检查是否支持查询 // 检查是否支持查询
if (!strategy.supportQuery()) { if (!strategy.supportQuery()) {
@@ -197,7 +197,7 @@ public class PaymentServiceImpl implements PaymentService {
validateNotifyParams(paymentType, headers, body, tenantId); validateNotifyParams(paymentType, headers, body, tenantId);
// 获取支付策略 // 获取支付策略
PaymentStrategy strategy = getPaymentStrategy(paymentType); PaymentStrategy strategy = getPaymentStrategy(paymentType, null);
// 检查是否需要异步通知 // 检查是否需要异步通知
if (!strategy.needNotify()) { if (!strategy.needNotify()) {
@@ -236,7 +236,7 @@ public class PaymentServiceImpl implements PaymentService {
validateRefundParams(orderNo, refundNo, paymentType, totalAmount, refundAmount, tenantId); validateRefundParams(orderNo, refundNo, paymentType, totalAmount, refundAmount, tenantId);
// 获取支付策略 // 获取支付策略
PaymentStrategy strategy = getPaymentStrategy(paymentType); PaymentStrategy strategy = getPaymentStrategy(paymentType, null);
// 检查是否支持退款 // 检查是否支持退款
if (!strategy.supportRefund()) { if (!strategy.supportRefund()) {
@@ -272,7 +272,7 @@ public class PaymentServiceImpl implements PaymentService {
validateRefundQueryParams(refundNo, paymentType, tenantId); validateRefundQueryParams(refundNo, paymentType, tenantId);
// 获取支付策略 // 获取支付策略
PaymentStrategy strategy = getPaymentStrategy(paymentType); PaymentStrategy strategy = getPaymentStrategy(paymentType, null);
// 检查是否支持退款查询 // 检查是否支持退款查询
if (!strategy.supportRefund()) { if (!strategy.supportRefund()) {
@@ -308,7 +308,7 @@ public class PaymentServiceImpl implements PaymentService {
validateCloseParams(orderNo, paymentType, tenantId); validateCloseParams(orderNo, paymentType, tenantId);
// 获取支付策略 // 获取支付策略
PaymentStrategy strategy = getPaymentStrategy(paymentType); PaymentStrategy strategy = getPaymentStrategy(paymentType, null);
// 检查是否支持关闭订单 // 检查是否支持关闭订单
if (!strategy.supportClose()) { if (!strategy.supportClose()) {
@@ -456,12 +456,14 @@ public class PaymentServiceImpl implements PaymentService {
/** /**
* 获取支付策略 * 获取支付策略
*/ */
private PaymentStrategy getPaymentStrategy(PaymentType paymentType) throws PaymentException { private PaymentStrategy getPaymentStrategy(PaymentType paymentType, PaymentRequest request) throws PaymentException {
// 如果是WECHAT支付类型,转换为WECHAT_NATIVE // WECHAT 是通用类型:带 openid 走 JSAPI(公众号内唤起),否则走 Native 扫码
// 因为WECHAT是一个通用类型,实际的支付策略是WECHAT_NATIVE
PaymentType actualPaymentType = paymentType; PaymentType actualPaymentType = paymentType;
if (paymentType == PaymentType.WECHAT) { 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); PaymentStrategy strategy = strategyMap.get(actualPaymentType);
@@ -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<String, String> 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;
}
}
}