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: