# Conflicts:
#	yudao-dependencies/pom.xml
#	yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java
#	yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java
#	yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java
#	yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java
#	yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java
#	yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java
pull/193/head
YunaiV 2025-05-06 20:56:21 +08:00
commit 829d200302
46 changed files with 692 additions and 88 deletions

View File

@ -84,7 +84,7 @@
<justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>1.9.4</jimureport.version>
<weixin-java.version>4.7.2.B</weixin-java.version>
<weixin-java.version>4.7.4.B</weixin-java.version>
<!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
</properties>

View File

@ -12,6 +12,8 @@ import net.sf.jsqlparser.schema.Table;
import java.util.List;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
/**
* {@link DataPermissionRule}
*
@ -27,6 +29,11 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
@Override
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
// 特殊:跨租户访问
if (skipPermissionCheck()) {
return null;
}
// 获得 Mapper 对应的数据权限的规则
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
if (CollUtil.isEmpty(rules)) {

View File

@ -32,13 +32,12 @@ public class DataPermissionUtils {
* @param runnable
*/
public static void executeIgnore(Runnable runnable) {
DataPermission dataPermission = getDisableDataPermissionDisable();
DataPermissionContextHolder.add(dataPermission);
addDisableDataPermission();
try {
// 执行 runnable
runnable.run();
} finally {
DataPermissionContextHolder.remove();
removeDataPermission();
}
}
@ -50,14 +49,25 @@ public class DataPermissionUtils {
*/
@SneakyThrows
public static <T> T executeIgnore(Callable<T> callable) {
DataPermission dataPermission = getDisableDataPermissionDisable();
DataPermissionContextHolder.add(dataPermission);
addDisableDataPermission();
try {
// 执行 callable
return callable.call();
} finally {
DataPermissionContextHolder.remove();
removeDataPermission();
}
}
/**
*
*/
public static void addDisableDataPermission(){
DataPermission dataPermission = getDisableDataPermissionDisable();
DataPermissionContextHolder.add(dataPermission);
}
public static void removeDataPermission(){
DataPermissionContextHolder.remove();
}
}

View File

@ -33,6 +33,13 @@ public class TenantProperties {
*/
private Set<String> ignoreUrls = new HashSet<>();
/**
* 访
*
* 访
*/
private Set<String> ignoreVisitUrls = Collections.emptySet();
/**
*
*

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.tenant.config;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
@ -15,6 +16,7 @@ import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
import cn.iocoder.yudao.framework.tenant.core.web.TenantVisitContextInterceptor;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
@ -36,6 +38,8 @@ import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
@ -116,6 +120,25 @@ public class YudaoTenantAutoConfiguration {
}
}
@Bean
public TenantVisitContextInterceptor tenantVisitContextInterceptor(TenantProperties tenantProperties,
SecurityFrameworkService securityFrameworkService) {
return new TenantVisitContextInterceptor(tenantProperties, securityFrameworkService);
}
@Bean
public WebMvcConfigurer tenantWebMvcConfigurer(TenantProperties tenantProperties,
TenantVisitContextInterceptor tenantVisitContextInterceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tenantVisitContextInterceptor)
.excludePathPatterns(tenantProperties.getIgnoreVisitUrls().toArray(new String[0]));
}
};
}
// ========== Security ==========
@Bean
@ -183,4 +206,5 @@ public class YudaoTenantAutoConfiguration {
// 创建 TenantRedisCacheManager 对象
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
}
}

View File

@ -21,4 +21,12 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface TenantIgnore {
/**
* true
*
* Spring EL true
*/
String enable() default "true";
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.tenant.core.aop;
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import lombok.extern.slf4j.Slf4j;
@ -24,7 +25,12 @@ public class TenantIgnoreAspect {
public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
Boolean oldIgnore = TenantContextHolder.isIgnore();
try {
TenantContextHolder.setIgnore(true);
// 计算条件,满足的情况下,才进行忽略
Object enable = SpringExpressionUtils.parseExpression(tenantIgnore.enable());
if (Boolean.TRUE.equals(enable)) {
TenantContextHolder.setIgnore(true);
}
// 执行逻辑
return joinPoint.proceed();
} finally {

View File

@ -66,14 +66,14 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
}
private boolean computeIgnoreTable(String tableName) {
// 找不到的表,说明不是 yudao 项目里的,不进行拦截
// 找不到的表,说明不是 yudao 项目里的,不进行拦截(忽略租户)
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
if (tableInfo == null) {
return true;
}
// 如果继承了 TenantBaseDO 基类,显然不忽略租户
if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
return true;
return false;
}
// 如果添加了 @TenantIgnore 注解,显然也不忽略租户
TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class);

View File

@ -0,0 +1,65 @@
package cn.iocoder.yudao.framework.tenant.core.web;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
@RequiredArgsConstructor
@Slf4j
public class TenantVisitContextInterceptor implements HandlerInterceptor {
private static final String PERMISSION = "system:tenant:visit";
private final TenantProperties tenantProperties;
private final SecurityFrameworkService securityFrameworkService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 如果和当前租户编号一致,则直接跳过
Long visitTenantId = WebFrameworkUtils.getVisitTenantId(request);
if (visitTenantId == null) {
return true;
}
if (ObjUtil.equal(visitTenantId, TenantContextHolder.getTenantId())) {
return true;
}
// 必须是登录用户
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser == null) {
return true;
}
// 校验用户是否可切换租户
if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) {
throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换租户");
}
// 【重点】切换租户编号
loginUser.setVisitTenantId(visitTenantId);
TenantContextHolder.setTenantId(visitTenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 【重点】清理切换,换回原租户编号
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser != null && loginUser.getTenantId() != null) {
TenantContextHolder.setTenantId(loginUser.getTenantId());
}
}
}

View File

@ -56,6 +56,10 @@ public class LoginUser {
*/
@JsonIgnore
private Map<String, Object> context;
/**
* 访
*/
private Long visitTenantId;
public void setContext(String key, Object value) {
if (context == null) {

View File

@ -16,6 +16,7 @@ import java.util.List;
import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildCache;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
/**
* {@link SecurityFrameworkService}
@ -63,6 +64,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override
@SneakyThrows
public boolean hasAnyPermissions(String... permissions) {
// 特殊:跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
Long userId = getLoginUserId();
if (userId == null) {
return false;
@ -78,6 +85,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override
@SneakyThrows
public boolean hasAnyRoles(String... roles) {
// 特殊:跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
Long userId = getLoginUserId();
if (userId == null) {
return false;
@ -92,6 +105,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
@Override
public boolean hasAnyScopes(String... scope) {
// 特殊:跨租户访问
if (skipPermissionCheck()) {
return true;
}
// 权限校验
LoginUser user = SecurityFrameworkUtils.getLoginUser();
if (user == null) {
return false;

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.security.core.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
@ -139,4 +140,21 @@ public class SecurityFrameworkUtils {
return authenticationToken;
}
/**
*
*
* @return
*/
public static boolean skipPermissionCheck() {
LoginUser loginUser = getLoginUser();
if (loginUser == null) {
return false;
}
if (loginUser.getVisitTenantId() == null) {
return false;
}
// 重点:跨租户访问时,无法进行权限校验
return ObjUtil.notEqual(loginUser.getVisitTenantId(), loginUser.getTenantId());
}
}

View File

@ -28,6 +28,7 @@ public class WebFrameworkUtils {
private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
public static final String HEADER_TENANT_ID = "tenant-id";
public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id";
/**
* Header
@ -54,6 +55,18 @@ public class WebFrameworkUtils {
return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null;
}
/**
* 访 header
* framework 使 WebFrameworkUtils
*
* @param request
* @return
*/
public static Long getVisitTenantId(HttpServletRequest request) {
String tenantId = request.getHeader(HEADER_VISIT_TENANT_ID);
return NumberUtil.isNumber(tenantId)? Long.valueOf(tenantId) : null;
}
public static void setLoginUserId(ServletRequest request, Long userId) {
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
}

View File

@ -47,4 +47,10 @@ public class TradeOrderProperties {
@NotNull(message = "评论超时时间不能为空")
private Duration commentExpireTime;
/**
*
*/
@NotNull(message = "是否同步订单状态到微信小程序不能为空")
private Boolean statusSyncToWxaEnable;
}

View File

@ -62,6 +62,27 @@ public interface TradeOrderHandler {
*/
default void beforeDeliveryOrder(TradeOrderDO order) {}
/**
*
*
* @param order
*/
default void afterDeliveryOrder(TradeOrderDO order) {}
/**
*
*
* @param order
*/
default void beforeReceiveOrder(TradeOrderDO order) {}
/**
*
*
* @param order
*/
default void afterReceiveOrder(TradeOrderDO order) {}
// ========== 公用方法 ==========
/**

View File

@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.trade.service.order.handler;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderNotifyConfirmReceiveReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* {@link TradeOrderHandler}
*
* = =
*
*/
@Slf4j
@Component
@ConditionalOnProperty(prefix = "yudao.trade.order", value = "status-sync-to-wxa-enable")
public class TradeStatusSyncToWxaOrderHandler implements TradeOrderHandler {
@Resource
private PayOrderApi payOrderApi;
@Resource
private SocialClientApi socialClientApi;
@Resource
private DeliveryExpressService expressService;
@Override
public void afterDeliveryOrder(TradeOrderDO order) {
// 注意:只有微信小程序支付的订单,才需要同步
if (ObjUtil.notEqual(order.getPayChannelCode(), PayChannelEnum.WX_LITE.getCode())) {
return;
}
PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId()).getCheckedData();
SocialWxaOrderUploadShippingInfoReqDTO reqDTO = new SocialWxaOrderUploadShippingInfoReqDTO()
.setTransactionId(payOrder.getChannelOrderNo())
.setOpenid(payOrder.getChannelUserId())
.setItemDesc(payOrder.getSubject())
.setReceiverContact(order.getReceiverMobile());
if (DeliveryTypeEnum.EXPRESS.getType().equals(order.getDeliveryType()) && StrUtil.isNotEmpty(order.getLogisticsNo())) {
reqDTO.setLogisticsType(SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_EXPRESS)
.setExpressCompany(expressService.getDeliveryExpress(order.getLogisticsId()).getCode())
.setLogisticsNo(order.getLogisticsNo());
} else if (DeliveryTypeEnum.PICK_UP.getType().equals(order.getDeliveryType())) {
reqDTO.setLogisticsType(SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_PICK_UP);
} else {
reqDTO.setLogisticsType(SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_VIRTUAL);
}
try {
socialClientApi.uploadWxaOrderShippingInfo(UserTypeEnum.MEMBER.getValue(), reqDTO).checkError();
} catch (Exception ex) {
log.error("[afterDeliveryOrder][订单({}) 上传订单物流信息到微信小程序失败]", order, ex);
}
}
@Override
public void afterReceiveOrder(TradeOrderDO order) {
// 注意:只有微信小程序支付的订单,才需要同步
if (ObjUtil.notEqual(order.getPayChannelCode(), PayChannelEnum.WX_LITE.getCode())) {
return;
}
PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId()).getCheckedData();
SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO = new SocialWxaOrderNotifyConfirmReceiveReqDTO()
.setTransactionId(payOrder.getChannelOrderNo())
.setReceivedTime(order.getReceiveTime());
try {
socialClientApi.notifyWxaOrderConfirmReceive(UserTypeEnum.MEMBER.getValue(), reqDTO).getCheckedData();
} catch (Exception ex) {
log.error("[afterReceiveOrder][订单({}) 通知订单收货到微信小程序失败]", order, ex);
}
}
// TODO @芋艿:【设置路径】 https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html#%E5%85%AD%E3%80%81%E6%B6%88%E6%81%AF%E8%B7%B3%E8%BD%AC%E8%B7%AF%E5%BE%84%E8%AE%BE%E7%BD%AE%E6%8E%A5%E5%8F%A3
}

View File

@ -128,6 +128,7 @@ yudao:
pay-expire-time: 2h # 支付的过期时间
receive-expire-time: 14d # 收货的过期时间
comment-expire-time: 7d # 评论的过期时间
status-sync-to-wxa-enable: true # 是否同步订单状态到微信小程序
express:
client: kd_100
kd-niao:

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.pay.api.order.dto;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Response DTO
*
@ -30,6 +32,10 @@ public class PayOrderRespDTO {
private String merchantOrderId;
// ========== 订单相关字段 ==========
/**
*
*/
private String subject;
/**
*
*/
@ -41,6 +47,22 @@ public class PayOrderRespDTO {
*/
private Integer status;
/**
*
*/
private LocalDateTime successTime;
// ========== 渠道相关字段 ==========
/**
*
*
* openid
*/
private String channelUserId;
/**
*
*/
private String channelOrderNo;
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.pay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
*
*
* @author
*/
@Getter
@AllArgsConstructor
public enum PayChannelEnum {
WX_PUB("wx_pub", "微信 JSAPI 支付"), // 公众号网页
WX_LITE("wx_lite", "微信小程序支付"),
WX_APP("wx_app", "微信 App 支付"),
WX_NATIVE("wx_native", "微信 Native 支付"),
WX_WAP("wx_wap", "微信 Wap 网站支付"), // H5 网页
WX_BAR("wx_bar", "微信付款码支付"),
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付"),
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付"),
ALIPAY_APP("alipay_app", "支付宝App 支付"),
ALIPAY_QR("alipay_qr", "支付宝扫码支付"),
ALIPAY_BAR("alipay_bar", "支付宝条码支付"),
MOCK("mock", "模拟支付"),
WALLET("wallet", "钱包支付");
/**
*
*
* <a href="https://www.pingxx.com/api/支付渠道属性值.html"></a>
*/
private final String code;
/**
*
*/
private final String name;
}

View File

@ -66,7 +66,8 @@ public class PayNotifyController {
@TenantIgnore
public String notifyOrder(@PathVariable("channelId") Long channelId,
@RequestParam(required = false) Map<String, String> params,
@RequestBody(required = false) String body) {
@RequestBody(required = false) String body,
@RequestHeader Map<String, String> headers) {
log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
// 1. 校验支付渠道是否存在
PayClient payClient = channelService.getPayClient(channelId);
@ -76,7 +77,7 @@ public class PayNotifyController {
}
// 2. 解析通知数据
PayOrderRespDTO notify = payClient.parseOrderNotify(params, body);
PayOrderRespDTO notify = payClient.parseOrderNotify(params, body, headers);
orderService.notifyOrder(channelId, notify);
return "success";
}
@ -87,7 +88,8 @@ public class PayNotifyController {
@TenantIgnore
public String notifyRefund(@PathVariable("channelId") Long channelId,
@RequestParam(required = false) Map<String, String> params,
@RequestBody(required = false) String body) {
@RequestBody(required = false) String body,
@RequestHeader Map<String, String> headers) {
log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body);
// 1. 校验支付渠道是否存在
PayClient payClient = channelService.getPayClient(channelId);
@ -97,7 +99,7 @@ public class PayNotifyController {
}
// 2. 解析通知数据
PayRefundRespDTO notify = payClient.parseRefundNotify(params, body);
PayRefundRespDTO notify = payClient.parseRefundNotify(params, body, headers);
refundService.notifyRefund(channelId, notify);
return "success";
}
@ -108,7 +110,8 @@ public class PayNotifyController {
@TenantIgnore
public String notifyTransfer(@PathVariable("channelId") Long channelId,
@RequestParam(required = false) Map<String, String> params,
@RequestBody(required = false) String body) {
@RequestBody(required = false) String body,
@RequestHeader Map<String, String> headers) {
log.info("[notifyTransfer][channelId({}) 回调数据({}/{})]", channelId, params, body);
// 1. 校验支付渠道是否存在
PayClient payClient = channelService.getPayClient(channelId);
@ -118,7 +121,7 @@ public class PayNotifyController {
}
// 2. 解析通知数据
PayTransferRespDTO notify = payClient.parseTransferNotify(params, body);
PayTransferRespDTO notify = payClient.parseTransferNotify(params, body, headers);
payTransferService.notifyTransfer(channelId, notify);
return "success";
}

View File

@ -89,7 +89,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
}
@Override
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("钱包支付无支付回调");
}
@ -144,7 +144,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
}
@Override
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("钱包支付无退款回调");
}
@ -178,7 +178,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
}
@Override
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("未实现");
}

View File

@ -39,9 +39,10 @@ public interface PayClient {
*
* @param params HTTP content type application/x-www-form-urlencoded
* @param body HTTP request body
* @param headers HTTP request headers
* @return
*/
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body, Map<String, String> headers);
/**
*
@ -66,9 +67,10 @@ public interface PayClient {
*
* @param params HTTP content type application/x-www-form-urlencoded
* @param body HTTP request body
* @param headers HTTP request headers
* @return
*/
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body, Map<String, String> headers);
/**
* 退
@ -103,8 +105,9 @@ public interface PayClient {
*
* @param params HTTP content type application/x-www-form-urlencoded
* @param body HTTP request body
* @param headers HTTP request headers
* @return
*/
PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body);
PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body, Map<String, String> headers);
}

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.framework.pay.core.client;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import javax.validation.Validator;
import jakarta.validation.Validator;
/**
*
@ -14,6 +14,7 @@ import javax.validation.Validator;
// @JsonTypeInfo 注解的作用Jackson 多态
// 1. 序列化到时数据库时,增加 @class 属性。
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
@JsonIgnoreProperties(ignoreUnknown = true) // 目的:忽略未知的属性,避免反序列化失败
public interface PayClientConfig {
/**

View File

@ -1,13 +1,13 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Map;

View File

@ -1,5 +1,8 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -7,10 +10,6 @@ import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 退 Request DTO
*

View File

@ -2,18 +2,18 @@ package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Map;
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*;
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.Alipay;
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.WxPay;
/**
* Request DTO

View File

@ -101,19 +101,19 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
throws Throwable;
@Override
public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
try {
return doParseOrderNotify(params, body);
return doParseOrderNotify(params, body, headers);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]",
getId(), params, body, ex);
log.error("[parseOrderNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
getId(), params, body, headers, ex);
throw buildPayException(ex);
}
}
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers)
throws Throwable;
@Override
@ -155,19 +155,19 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
@Override
public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
try {
return doParseRefundNotify(params, body);
return doParseRefundNotify(params, body, headers);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]",
getId(), params, body, ex);
log.error("[parseRefundNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
getId(), params, body, headers, ex);
throw buildPayException(ex);
}
}
protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body)
protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers)
throws Throwable;
@Override
@ -220,19 +220,19 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
}
@Override
public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body) {
public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
try {
return doParseTransferNotify(params, body);
return doParseTransferNotify(params, body, headers);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[doParseTransferNotify][客户端({}) params({}) body({}) 解析失败]",
getId(), params, body, ex);
log.error("[doParseTransferNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
getId(), params, body, headers, ex);
throw buildPayException(ex);
}
}
protected abstract PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body)
protected abstract PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers)
throws Throwable;
@Override

View File

@ -1,10 +1,9 @@
package cn.iocoder.yudao.framework.pay.core.client.impl;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import jakarta.validation.Validator;
import lombok.Data;
import javax.validation.Validator;
/**
* PayClientConfig
*

View File

@ -79,7 +79,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws Throwable {
// 1. 校验回调数据
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
@ -175,7 +175,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
}
@Override
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
// ② 全部退款Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
@ -327,7 +327,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
// TODO @chihuo这里是不是也要实现支付宝的。
@Override
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("未实现");
}

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;

View File

@ -5,7 +5,6 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;

View File

@ -2,12 +2,11 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* PayClientConfig
* {@link com.alipay.api.AlipayConfig}

View File

@ -4,7 +4,6 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.Method;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;

View File

@ -58,17 +58,17 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
}
@Override
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("未实现");
}
@Override
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("模拟支付无退款回调");
}
@Override
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("模拟支付无支付回调");
}

View File

@ -18,10 +18,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.WxPayTransferPart
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.*;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
@ -67,13 +64,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
protected void doInit(String tradeType) {
// 创建 config 配置
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent");
BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent", "publicKeyContent");
payConfig.setTradeType(tradeType);
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
if (Objects.equals(config.getApiVersion(), API_VERSION_V2)) {
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
} else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) {
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
payConfig.setPublicKeyPath(FileUtils.createTempFile(config.getPublicKeyContent()).getPath());
}
// 创建 client 客户端
@ -157,12 +155,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseOrderNotifyV2(body);
case API_VERSION_V3:
return doParseOrderNotifyV3(body);
return doParseOrderNotifyV3(body, headers);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
@ -179,9 +177,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
response.getOutTradeNo(), body);
}
private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
private PayOrderRespDTO doParseOrderNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
SignatureHeader signatureHeader = getRequestHeader(headers);
WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, signatureHeader);
WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
Integer status = parseStatus(result.getTradeState());
@ -321,12 +320,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
}
@Override
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) throws WxPayException {
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseRefundNotifyV2(body);
case API_VERSION_V3:
return parseRefundNotifyV3(body);
return parseRefundNotifyV3(body, headers);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
@ -344,9 +343,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
}
private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
private PayRefundRespDTO parseRefundNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
SignatureHeader signatureHeader = getRequestHeader(headers);
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, signatureHeader);
WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
@ -357,10 +357,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
}
@Override
public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws WxPayException {
public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V3:
return parseTransferNotifyV3(body);
return parseTransferNotifyV3(body, headers);
case API_VERSION_V2:
throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
default:
@ -368,10 +368,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
}
}
private PayTransferRespDTO parseTransferNotifyV3(String body) throws WxPayException {
private PayTransferRespDTO parseTransferNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
// TODO @luchi这个可以复用 wxjava 里的类么?
WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, null, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, signatureHeader, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
WxPayTransferPartnerNotifyV3Result.TransferNotifyResult result = response.getResult();
// 2. 构建结果
if (Objects.equals("FINISHED", result.getBatchStatus())) {
@ -513,6 +514,20 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
// ========== 各种工具方法 ==========
/**
*
*
* @see <a href="https://github.com/binarywang/weixin-java-pay-demo/blob/master/src/main/java/com/github/binarywang/demo/wx/pay/controller/WxPayV3Controller.java#L202-L221"></a>
*/
private SignatureHeader getRequestHeader(Map<String, String> headers) {
return SignatureHeader.builder()
.signature(headers.get("wechatpay-signature"))
.nonce(headers.get("wechatpay-nonce"))
.serial(headers.get("wechatpay-serial"))
.timeStamp(headers.get("wechatpay-timestamp"))
.build();
}
static String formatDateV2(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
}

View File

@ -74,13 +74,18 @@ public class WxPayClientConfig implements PayClientConfig {
@NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
private String apiV3Key;
/**
*
* merchantSerialNumber
*/
@NotBlank(message = "证书序列号不能为空", groups = V3.class)
private String certSerialNo;
@Deprecated // TODO 芋艿V2.3.0 进行移除
private String privateCertContent;
/**
* pub_key.pem
*/
@NotBlank(message = "pub_key.pem 不能为空", groups = V3.class)
private String publicKeyContent;
@NotBlank(message = "publicKeyId 不能为空", groups = V3.class)
private String publicKeyId;
/**
* v2

View File

@ -64,4 +64,20 @@ public interface SocialClientApi {
@Operation(summary = "发送微信小程序订阅消息")
CommonResult<Boolean> sendWxaSubscribeMessage(@Valid @RequestBody SocialWxaSubscribeMessageSendReqDTO reqDTO);
/**
*
*
* @param userType
* @param reqDTO
*/
@PostMapping(PREFIX + "/upload-wxa-order-shipping-info")
@Operation(summary = "上传订单发货到微信小程序")
CommonResult<Boolean> uploadWxaOrderShippingInfo(@RequestParam("userType") Integer userType,
@Valid @RequestBody SocialWxaOrderUploadShippingInfoReqDTO reqDTO);
@PostMapping(PREFIX + "/notify-wxa-order-confirm-receive")
@Operation(summary = "通知订单收货到微信小程序")
CommonResult<Boolean> notifyWxaOrderConfirmReceive(@RequestParam("userType") Integer userType,
@Valid @RequestBody SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO);
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.system.api.social.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
/**
*
*
* @see <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/shopping-order/normal-shopping-detail/uploadShoppingInfo.html"></a>
* @author
*/
@Data
public class SocialWxaOrderNotifyConfirmReceiveReqDTO {
/**
*
*/
@NotEmpty(message = "原支付交易对应的微信订单号不能为空")
private String transactionId;
/**
*
*/
@NotNull(message = "快递签收时间不能为空")
private LocalDateTime receivedTime;
}

View File

@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.system.api.social.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
*
*
* @see <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/shopping-order/normal-shopping-detail/uploadShoppingInfo.html"></a>
* @author
*/
@Data
public class SocialWxaOrderUploadShippingInfoReqDTO {
/**
* -
*/
public static final Integer LOGISTICS_TYPE_EXPRESS = 1;
/**
* -
*/
public static final Integer LOGISTICS_TYPE_VIRTUAL = 3;
/**
* -
*/
public static final Integer LOGISTICS_TYPE_PICK_UP = 4;
/**
* (openid)
*/
@NotEmpty(message = "支付者,支付者信息(openid)不能为空")
private String openid;
/**
*
*/
@NotEmpty(message = "原支付交易对应的微信订单号不能为空")
private String transactionId;
/**
*
*/
@NotNull(message = "物流模式不能为空")
private Integer logisticsType;
/**
*
*/
private String logisticsNo;
/**
*
*
* @see <a href="https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#%E8%8E%B7%E5%8F%96%E8%BF%90%E5%8A%9Bid%E5%88%97%E8%A1%A8get-delivery-list"></a>
*/
private String expressCompany;
/**
*
*/
@NotEmpty(message = "商品信息不能为空")
private String itemDesc;
/**
*
*/
@NotEmpty(message = "收件人手机号")
private String receiverContact;
}

View File

@ -124,10 +124,11 @@ public interface ErrorCodeConstants {
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR = new ErrorCode(1_002_018_201, "获得小程序码失败");
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR = new ErrorCode(1_002_018_202, "获得小程序订阅消息模版失败");
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR = new ErrorCode(1_002_018_203, "发送小程序订阅消息失败");
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR = new ErrorCode(1_002_018_204, "上传微信小程序发货信息失败");
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR = new ErrorCode(1_002_018_205, "上传微信小程序订单收货信息失败");
ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_210, "社交客户端不存在");
ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_211, "社交客户端已存在配置");
// ========== OAuth2 客户端 1-002-020-000 =========
ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在");
ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1_002_020_001, "OAuth2 客户端编号已存在");

View File

@ -96,4 +96,16 @@ public class SocialClientApiImpl implements SocialClientApi {
return success(true);
}
@Override
public CommonResult<Boolean> uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO) {
socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO);
return success(true);
}
@Override
public CommonResult<Boolean> notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO) {
socialClientService.notifyWxaOrderConfirmReceive(userType, reqDTO);
return success(true);
}
}

View File

@ -3,3 +3,9 @@ GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}}
#Authorization: Bearer test100
tenant-id: {{adminTenantId}}
### 请求 /system/user/page 接口(测试访问别的租户)
GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
visit-tenant-id: 122

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.system.service.social;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderNotifyConfirmReceiveReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
@ -92,6 +94,22 @@ public interface SocialClientService {
*/
void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId);
/**
*
*
* @param userType
* @param reqDTO
*/
void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO);
/**
*
*
* @param userType
* @param reqDTO
*/
void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO);
// =================== 客户端管理 ===================
/**

View File

@ -5,11 +5,15 @@ import cn.binarywang.wx.miniapp.api.WxMaSubscribeService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.*;
import cn.binarywang.wx.miniapp.bean.shop.response.WxMaOrderShippingInfoBaseResponse;
import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
import cn.binarywang.wx.miniapp.constant.WxMaConstants;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@ -19,6 +23,8 @@ import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderNotifyConfirmReceiveReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
@ -55,14 +61,17 @@ import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static java.util.Collections.singletonList;
/**
* Service
@ -307,6 +316,64 @@ public class SocialClientServiceImpl implements SocialClientService {
}
}
@Override
public void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO) {
WxMaService service = getWxMaService(userType);
List<ShippingListBean> shippingList;
if (Objects.equals(reqDTO.getLogisticsType(), SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_EXPRESS)) {
shippingList = singletonList(ShippingListBean.builder()
.trackingNo(reqDTO.getLogisticsNo())
.expressCompany(reqDTO.getExpressCompany())
.itemDesc(reqDTO.getItemDesc())
.contact(ContactBean.builder().receiverContact(DesensitizedUtil.mobilePhone(reqDTO.getReceiverContact())).build())
.build());
} else {
shippingList = singletonList(ShippingListBean.builder().itemDesc(reqDTO.getItemDesc()).build());
}
WxMaOrderShippingInfoUploadRequest request = WxMaOrderShippingInfoUploadRequest.builder()
.orderKey(OrderKeyBean.builder()
.orderNumberType(2) // 使用原支付交易对应的微信订单号,即渠道单号
.transactionId(reqDTO.getTransactionId())
.build())
.logisticsType(reqDTO.getLogisticsType()) // 配送方式
.deliveryMode(1) // 统一发货
.shippingList(shippingList)
.payer(PayerBean.builder().openid(reqDTO.getOpenid()).build())
.uploadTime(LocalDateTimeUtil.format(LocalDateTime.now(), UTC_MS_WITH_XXX_OFFSET_PATTERN))
.build();
try {
WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request);
if (response.getErrCode() != 0) {
log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败request({}) response({})]", request, response);
throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, response.getErrMsg());
}
log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功request({}) response({})]", request, response);
} catch (WxErrorException ex) {
log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败request({})]", request, ex);
throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, ex.getError().getErrorMsg());
}
}
@Override
public void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO) {
WxMaService service = getWxMaService(userType);
WxMaOrderShippingInfoNotifyConfirmRequest request = WxMaOrderShippingInfoNotifyConfirmRequest.builder()
.transactionId(reqDTO.getTransactionId())
.receivedTime(LocalDateTimeUtil.toEpochMilli(reqDTO.getReceivedTime()))
.build();
try {
WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().notifyConfirmReceive(request);
if (response.getErrCode() != 0) {
log.error("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序失败request({}) response({})]", request, response);
throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR, response.getErrMsg());
}
log.info("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序成功request({}) response({})]", request, response);
} catch (WxErrorException ex) {
log.error("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序失败request({})]", request, ex);
throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR, ex.getError().getErrorMsg());
}
}
/**
*
*

View File

@ -165,6 +165,9 @@ yudao:
tenant: # 多租户相关配置项
enable: true
ignore-urls:
ignore-visit-urls:
- /admin-api/system/user/profile/**
- /admin-api/system/auth/**
ignore-tables:
ignore-caches:
- user_role_ids

View File

@ -298,6 +298,9 @@ yudao:
enable: true
ignore-urls:
- /jmreport/* # 积木报表,无法携带租户编号
ignore-visit-urls:
- /admin-api/system/user/profile/**
- /admin-api/system/auth/**
ignore-tables:
ignore-caches:
- user_role_ids
@ -320,6 +323,7 @@ yudao:
pay-expire-time: 2h # 支付的过期时间
receive-expire-time: 14d # 收货的过期时间
comment-expire-time: 7d # 评论的过期时间
status-sync-to-wxa-enable: true # 是否同步订单状态到微信小程序
express:
client: kd_100
kd-niao: