diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index ba048c6cb3..dde84077f3 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -77,7 +77,7 @@ 1.16.7 1.4.0 1.9.4 - 4.7.2.B + 4.7.4.B 1.2.13 diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java index a2778734be..477bfd99d3 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java @@ -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 rules = ruleFactory.getDataPermissionRule(mappedStatementId); if (CollUtil.isEmpty(rules)) { diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java index 583c482b23..eb7b22371f 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java @@ -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 executeIgnore(Callable 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(); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java index 06c67694e6..e059e28090 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java @@ -33,6 +33,13 @@ public class TenantProperties { */ private Set ignoreUrls = new HashSet<>(); + /** + * 需要忽略跨(切换)租户访问的请求 + * + * 原因是:某些接口,访问的是个人信息,在跨租户是获取不到的! + */ + private Set ignoreVisitUrls = Collections.emptySet(); + /** * 需要忽略多租户的表 * diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java index 6df5ef8adc..a115b2eb50 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java @@ -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; @@ -35,6 +37,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; @@ -115,6 +119,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 diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java index a0b477421f..7ed9ade7fe 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java @@ -21,4 +21,12 @@ import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface TenantIgnore { + + /** + * 是否开启忽略租户,默认为 true 开启 + * + * 支持 Spring EL 表达式,如果返回 true 则满足条件,进行租户的忽略 + */ + String enable() default "true"; + } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java index b7d0fa3624..91514d50cd 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java @@ -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 { diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java index 1300a79fde..47e5df004c 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java @@ -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); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java new file mode 100644 index 0000000000..9ac70d8313 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java @@ -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()); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java index e23c3a7a2f..a2cd9e50b6 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java @@ -56,6 +56,10 @@ public class LoginUser { */ @JsonIgnore private Map context; + /** + * 访问的租户编号 + */ + private Long visitTenantId; public void setContext(String key, Object value) { if (context == null) { diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java index b04b072215..b72ab382da 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java @@ -9,6 +9,7 @@ import lombok.AllArgsConstructor; import java.util.Arrays; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck; /** * 默认的 {@link SecurityFrameworkService} 实现类 @@ -27,6 +28,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { @Override public boolean hasAnyPermissions(String... permissions) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return true; + } + + // 权限校验 Long userId = getLoginUserId(); if (userId == null) { return false; @@ -41,6 +48,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { @Override public boolean hasAnyRoles(String... roles) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return true; + } + + // 权限校验 Long userId = getLoginUserId(); if (userId == null) { return false; @@ -55,6 +68,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; diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java index f85d77c361..ffc5861a34 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -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; @@ -137,4 +138,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()); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java index b5a52c09a7..b4aa301676 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java @@ -1,19 +1,16 @@ package cn.iocoder.yudao.framework.web.core.util; import cn.hutool.core.util.NumberUtil; -import cn.hutool.extra.servlet.ServletUtil; import cn.iocoder.yudao.framework.common.enums.TerminalEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.web.config.WebProperties; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; - /** * 专属于 web 包的工具类 * @@ -27,6 +24,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 @@ -53,6 +51,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); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java index 89cd776239..755db9f681 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java @@ -47,4 +47,10 @@ public class TradeOrderProperties { @NotNull(message = "评论超时时间不能为空") private Duration commentExpireTime; + /** + * 是否同步订单状态到微信小程序 + */ + @NotNull(message = "是否同步订单状态到微信小程序不能为空") + private Boolean statusSyncToWxaEnable; + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index e9bf0e6305..ce7830e4ce 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -401,6 +401,11 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { .setOrderId(order.getId()).setUserId(order.getUserId()).setMessage(null)); // 4.2 发送订阅消息 getSelf().sendDeliveryOrderMessage(order, deliveryReqVO); + + // 5. 处理订单发货后逻辑 + order.setLogisticsId(updateOrderObj.getLogisticsId()).setLogisticsNo(updateOrderObj.getLogisticsNo()) + .setStatus(updateOrderObj.getStatus()).setDeliveryTime(updateOrderObj.getDeliveryTime()); + tradeOrderHandlers.forEach(handler -> handler.afterDeliveryOrder(order)); } @Async @@ -499,15 +504,20 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { * @param order 订单 */ private void receiveOrder0(TradeOrderDO order) { - // 更新 TradeOrderDO 状态为已完成 + // 1. 更新 TradeOrderDO 状态为已完成 + LocalDateTime receiveTime = LocalDateTime.now(); int updateCount = tradeOrderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), - new TradeOrderDO().setStatus(TradeOrderStatusEnum.COMPLETED.getStatus()).setReceiveTime(LocalDateTime.now())); + new TradeOrderDO().setStatus(TradeOrderStatusEnum.COMPLETED.getStatus()).setReceiveTime(receiveTime)); if (updateCount == 0) { throw exception(ORDER_RECEIVE_FAIL_STATUS_NOT_DELIVERED); } - // 插入订单日志 + // 2. 插入订单日志 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), TradeOrderStatusEnum.COMPLETED.getStatus()); + + // 3. 执行 TradeOrderHandler 后置处理 + order.setStatus(TradeOrderStatusEnum.COMPLETED.getStatus()).setReceiveTime(receiveTime); + tradeOrderHandlers.forEach(handler -> handler.afterReceiveOrder(order)); } /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeOrderHandler.java index 4cc5c69a37..1b3d2f7fde 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeOrderHandler.java @@ -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) {} + // ========== 公用方法 ========== /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java new file mode 100644 index 0000000000..8bd1a802ea --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java @@ -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()); + 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); + } 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()); + SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO = new SocialWxaOrderNotifyConfirmReceiveReqDTO() + .setTransactionId(payOrder.getChannelOrderNo()) + .setReceivedTime(order.getReceiveTime()); + try { + socialClientApi.notifyWxaOrderConfirmReceive(UserTypeEnum.MEMBER.getValue(), reqDTO); + } 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 + +} diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderRespDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderRespDTO.java index a1c9c32b3b..1287ce7a39 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderRespDTO.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderRespDTO.java @@ -32,6 +32,10 @@ public class PayOrderRespDTO { private String merchantOrderId; // ========== 订单相关字段 ========== + /** + * 商品标题 + */ + private String subject; /** * 支付金额,单位:分 */ @@ -50,4 +54,15 @@ public class PayOrderRespDTO { // ========== 渠道相关字段 ========== + /** + * 渠道用户编号 + * + * 例如说,微信 openid、支付宝账号 + */ + private String channelUserId; + /** + * 渠道订单号 + */ + private String channelOrderNo; + } diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/PayChannelEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/PayChannelEnum.java new file mode 100644 index 0000000000..52d35d2aa9 --- /dev/null +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/PayChannelEnum.java @@ -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", "钱包支付"); + + /** + * 编码 + * + * 参考 支付渠道属性值 + */ + private final String code; + /** + * 名字 + */ + private final String name; + +} diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java index 2d646b8021..bcc79c0c88 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java @@ -66,7 +66,8 @@ public class PayNotifyController { @TenantIgnore public String notifyOrder(@PathVariable("channelId") Long channelId, @RequestParam(required = false) Map params, - @RequestBody(required = false) String body) { + @RequestBody(required = false) String body, + @RequestHeader Map 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 params, - @RequestBody(required = false) String body) { + @RequestBody(required = false) String body, + @RequestHeader Map 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 params, - @RequestBody(required = false) String body) { + @RequestBody(required = false) String body, + @RequestHeader Map 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"; } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java index 63fc92ba3c..2dd5893fd8 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java @@ -89,7 +89,7 @@ public class WalletPayClient extends AbstractPayClient { } @Override - protected PayOrderRespDTO doParseOrderNotify(Map params, String body) { + protected PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) { throw new UnsupportedOperationException("钱包支付无支付回调"); } @@ -144,7 +144,7 @@ public class WalletPayClient extends AbstractPayClient { } @Override - protected PayRefundRespDTO doParseRefundNotify(Map params, String body) { + protected PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) { throw new UnsupportedOperationException("钱包支付无退款回调"); } @@ -178,7 +178,7 @@ public class WalletPayClient extends AbstractPayClient { } @Override - protected PayTransferRespDTO doParseTransferNotify(Map params, String body) throws Throwable { + protected PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) { throw new UnsupportedOperationException("未实现"); } diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java index 816b6fef72..64a56a5ca3 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java @@ -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 params, String body); + PayOrderRespDTO parseOrderNotify(Map params, String body, Map 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 params, String body); + PayRefundRespDTO parseRefundNotify(Map params, String body, Map 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 params, String body); + PayTransferRespDTO parseTransferNotify(Map params, String body, Map headers); } diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java index 5a38430342..9df52cd61c 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java @@ -1,12 +1,8 @@ package cn.iocoder.yudao.framework.pay.core.client; -import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonTypeInfo; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Validator; -import java.util.Set; +import jakarta.validation.Validator; /** * 支付客户端的配置,本质是支付渠道的配置 @@ -18,6 +14,7 @@ import java.util.Set; // @JsonTypeInfo 注解的作用,Jackson 多态 // 1. 序列化到时数据库时,增加 @class 属性。 // 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 +@JsonIgnoreProperties(ignoreUnknown = true) // 目的:忽略未知的属性,避免反序列化失败 public interface PayClientConfig { /** diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java index 5c47504026..a478d4e629 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java @@ -101,19 +101,19 @@ public abstract class AbstractPayClient implemen throws Throwable; @Override - public final PayOrderRespDTO parseOrderNotify(Map params, String body) { + public final PayOrderRespDTO parseOrderNotify(Map params, String body, Map 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 params, String body) + protected abstract PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) throws Throwable; @Override @@ -155,19 +155,19 @@ public abstract class AbstractPayClient implemen protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; @Override - public final PayRefundRespDTO parseRefundNotify(Map params, String body) { + public final PayRefundRespDTO parseRefundNotify(Map params, String body, Map 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 params, String body) + protected abstract PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) throws Throwable; @Override @@ -220,19 +220,19 @@ public abstract class AbstractPayClient implemen } @Override - public final PayTransferRespDTO parseTransferNotify(Map params, String body) { + public final PayTransferRespDTO parseTransferNotify(Map params, String body, Map 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 params, String body) + protected abstract PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) throws Throwable; @Override diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index b3075eb03d..b0f728eecf 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java @@ -23,6 +23,8 @@ import com.alipay.api.AlipayResponse; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.domain.*; import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.internal.util.AntCertificationUtil; +import com.alipay.api.internal.util.codec.Base64; import com.alipay.api.request.*; import com.alipay.api.response.*; import lombok.Getter; @@ -30,6 +32,7 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; import java.time.LocalDateTime; import java.util.Collections; import java.util.Map; @@ -41,6 +44,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; +import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY; /** * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) @@ -79,11 +83,23 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient params, String body) throws Throwable { + public PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) throws Throwable { // 1. 校验回调数据 Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); - AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), - StandardCharsets.UTF_8.name(), config.getSignType()); + boolean verify; + if (Objects.equals(config.getMode(), MODE_PUBLIC_KEY)) { + verify = AlipaySignature.rsaCheckV1(params, config.getAlipayPublicKey(), + StandardCharsets.UTF_8.name(), config.getSignType()); + } else if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + // 由于 rsaCertCheckV1 的第二个参数是 path,所以不能这么调用!!!通过阅读源码,发现可以采用如下方式! + X509Certificate cert = AntCertificationUtil.getCertFromContent(config.getAlipayPublicCertContent()); + String publicKey = Base64.encodeBase64String(cert.getEncoded()); + verify = AlipaySignature.rsaCheckV1(bodyObj, publicKey, + StandardCharsets.UTF_8.name(), config.getSignType()); + } else { + throw new IllegalArgumentException("未知的公钥类型:" + config.getMode()); + } + Assert.isTrue(verify, "验签结果不通过"); // 2. 解析订单的状态 // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂 @@ -175,7 +191,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient params, String body) { + public PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) { // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 @@ -327,7 +343,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient params, String body) throws Throwable { + protected PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) { throw new UnsupportedOperationException("未实现"); } diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java index 34b1d5db6f..d8f08bbf2f 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java @@ -58,17 +58,17 @@ public class MockPayClient extends AbstractPayClient { } @Override - protected PayTransferRespDTO doParseTransferNotify(Map params, String body) throws Throwable { + protected PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) { throw new UnsupportedOperationException("未实现"); } @Override - protected PayRefundRespDTO doParseRefundNotify(Map params, String body) { + protected PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) { throw new UnsupportedOperationException("模拟支付无退款回调"); } @Override - protected PayOrderRespDTO doParseOrderNotify(Map params, String body) { + protected PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) { throw new UnsupportedOperationException("模拟支付无支付回调"); } diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java index 374afccad6..e681cfbea4 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -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 params, String body) throws WxPayException { + public PayOrderRespDTO doParseOrderNotify(Map params, String body, Map 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,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient headers) throws WxPayException { // 1. 解析回调 - WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, null); +// SignatureHeader signatureHeader = getRequestHeader(headers); + SignatureHeader signatureHeader = null; + WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, signatureHeader); WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult(); // 2. 构建结果 Integer status = parseStatus(result.getTradeState()); @@ -321,12 +321,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient params, String body) throws WxPayException { + public PayRefundRespDTO doParseRefundNotify(Map params, String body, Map 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 +344,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient 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 +358,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient params, String body) throws WxPayException { + public PayTransferRespDTO doParseTransferNotify(Map params, String body, Map 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 +369,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient 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 +515,20 @@ public abstract class AbstractWxPayClient extends AbstractPayClient官方示例 + */ + private SignatureHeader getRequestHeader(Map 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); } diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java index 55a1716842..e79fa1e3ef 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java @@ -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版本 diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java index 9842560636..9c9bce14fd 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java @@ -60,7 +60,7 @@ public class PayClientFactoryImplIntegrationTest { config.setMchId("1545083881"); config.setApiVersion(WxPayClientConfig.API_VERSION_V3); config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"))); - config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); +// config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase"); // 创建客户端 Long channelId = RandomUtil.randomLong(); diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java index ef02dd1086..5f0a0b5673 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java @@ -66,4 +66,20 @@ public interface SocialClientApi { */ void sendWxaSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO); + /** + * 上传订单发货到微信小程序 + * + * @param userType 用户类型 + * @param reqDTO 请求 + */ + void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO); + + /** + * 通知订单收货到微信小程序 + * + * @param userType 用户类型 + * @param reqDTO 请求 + */ + void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO); + } diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderNotifyConfirmReceiveReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderNotifyConfirmReceiveReqDTO.java new file mode 100644 index 0000000000..d2bf62c0d6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderNotifyConfirmReceiveReqDTO.java @@ -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 上传购物详情 + * @author 芋道源码 + */ +@Data +public class SocialWxaOrderNotifyConfirmReceiveReqDTO { + + /** + * 原支付交易对应的微信订单号 + */ + @NotEmpty(message = "原支付交易对应的微信订单号不能为空") + private String transactionId; + + /** + * 快递签收时间 + */ + @NotNull(message = "快递签收时间不能为空") + private LocalDateTime receivedTime; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderUploadShippingInfoReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderUploadShippingInfoReqDTO.java new file mode 100644 index 0000000000..555da62a4d --- /dev/null +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderUploadShippingInfoReqDTO.java @@ -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 上传购物详情 + * @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 物流查询插件简介 + */ + private String expressCompany; + /** + * 商品信息 + */ + @NotEmpty(message = "商品信息不能为空") + private String itemDesc; + /** + * 收件人手机号 + */ + @NotEmpty(message = "收件人手机号") + private String receiverContact; + +} diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index 6ed77aeef7..c89a5dcdb2 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -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 客户端编号已存在"); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java index 47f5e7e6ac..808c465049 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java @@ -94,4 +94,14 @@ public class SocialClientApiImpl implements SocialClientApi { socialClientService.sendSubscribeMessage(reqDTO, template.getPriTmplId(), socialUser.getOpenid()); } + @Override + public void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO) { + socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO); + } + + @Override + public void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO) { + socialClientService.notifyWxaOrderConfirmReceive(userType, reqDTO); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http index 90f0c98324..0ca69150aa 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http @@ -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 \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java index 716b1c0dbc..6a9538fa4d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java @@ -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); + // =================== 客户端管理 =================== /** diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java index cdb38ea3b3..198b109104 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java @@ -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.stereotype.Service; import javax.annotation.Resource; 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 实现类 @@ -329,6 +338,64 @@ public class SocialClientServiceImpl implements SocialClientService { return subscribeMessage; } + @Override + public void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO) { + WxMaService service = getWxMaService(userType); + List 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()); + } + } + /** * 获得 clientId + clientSecret 对应的 WxMpService 对象 * diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index f4da9e1389..bdb30b49f0 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -274,6 +274,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 @@ -296,6 +299,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: