Răsfoiți Sursa

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro

# Conflicts:
#	yudao-dependencies/pom.xml
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java
#	yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java
YunaiV 6 luni în urmă
părinte
comite
e633c7cfe3
38 a modificat fișierele cu 697 adăugiri și 81 ștergeri
  1. 1 1
      yudao-dependencies/pom.xml
  2. 7 0
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java
  3. 16 6
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java
  4. 7 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java
  5. 23 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java
  6. 8 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java
  7. 7 1
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java
  8. 2 2
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java
  9. 65 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java
  10. 4 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
  11. 19 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java
  12. 18 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
  13. 15 5
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java
  14. 6 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java
  15. 13 3
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  16. 21 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeOrderHandler.java
  17. 86 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeStatusSyncToWxaOrderHandler.java
  18. 15 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderRespDTO.java
  19. 42 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/PayChannelEnum.java
  20. 9 6
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java
  21. 3 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java
  22. 6 3
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  23. 3 6
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java
  24. 15 15
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  25. 21 5
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
  26. 3 3
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
  27. 33 17
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  28. 8 3
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java
  29. 1 1
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java
  30. 16 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java
  31. 30 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderNotifyConfirmReceiveReqDTO.java
  32. 67 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaOrderUploadShippingInfoReqDTO.java
  33. 2 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
  34. 10 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
  35. 6 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http
  36. 18 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java
  37. 67 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
  38. 4 0
      yudao-server/src/main/resources/application.yaml

+ 1 - 1
yudao-dependencies/pom.xml

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

+ 7 - 0
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<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
         if (CollUtil.isEmpty(rules)) {

+ 16 - 6
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> T executeIgnore(Callable<T> callable) {
-        DataPermission dataPermission = getDisableDataPermissionDisable();
-        DataPermissionContextHolder.add(dataPermission);
+        addDisableDataPermission();
         try {
             // 执行 callable
             return callable.call();
         } finally {
-            DataPermissionContextHolder.remove();
+            removeDataPermission();
         }
     }
 
+    /**
+     * 添加忽略数据权限
+     */
+    public static void addDisableDataPermission(){
+        DataPermission dataPermission = getDisableDataPermissionDisable();
+        DataPermissionContextHolder.add(dataPermission);
+    }
+
+    public static void removeDataPermission(){
+        DataPermissionContextHolder.remove();
+    }
+
 }

+ 7 - 0
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<String> ignoreUrls = new HashSet<>();
 
+    /**
+     * 需要忽略跨(切换)租户访问的请求
+     *
+     * 原因是:某些接口,访问的是个人信息,在跨租户是获取不到的!
+     */
+    private Set<String> ignoreVisitUrls = Collections.emptySet();
+
     /**
      * 需要忽略多租户的表
      *

+ 23 - 0
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

+ 8 - 0
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";
+
 }

+ 7 - 1
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 {

+ 2 - 2
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);

+ 65 - 0
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());
+        }
+    }
+
+}

+ 4 - 0
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<String, Object> context;
+    /**
+     * 访问的租户编号
+     */
+    private Long visitTenantId;
 
     public void setContext(String key, Object value) {
         if (context == null) {

+ 19 - 0
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;

+ 18 - 0
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());
+    }
+
 }

+ 15 - 5
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);
     }

+ 6 - 0
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;
+
 }

+ 13 - 3
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));
     }
 
     /**

+ 21 - 0
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) {}
+
     // ========== 公用方法 ==========
 
     /**

+ 86 - 0
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
+
+}

+ 15 - 0
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;
+
 }

+ 42 - 0
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", "钱包支付");
+
+    /**
+     * 编码
+     *
+     * 参考 <a href="https://www.pingxx.com/api/支付渠道属性值.html">支付渠道属性值</a>
+     */
+    private final String code;
+    /**
+     * 名字
+     */
+    private final String name;
+
+}

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

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

+ 6 - 3
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<String, String> params, String body);
+    PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body, Map<String, String> headers);
 
     /**
      * 获得支付订单信息
@@ -66,9 +67,10 @@ public interface PayClient {
      *
      * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
      * @param body HTTP 回调接口的 request body
+     * @param headers HTTP 回调接口的 request headers
      * @return 支付订单信息
      */
-    PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
+    PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body, Map<String, String> headers);
 
     /**
      * 获得退款订单信息
@@ -103,8 +105,9 @@ public interface PayClient {
      *
      * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
      * @param body HTTP 回调接口的 request body
+     * @param headers HTTP 回调接口的 request headers
      * @return 转账信息
      */
-    PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body);
+    PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body, Map<String, String> headers);
 
 }

+ 3 - 6
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 {
 
     /**

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

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

+ 3 - 3
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<NonePayClientConfig> {
     }
 
     @Override
-    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("未实现");
     }
 
     @Override
-    protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
+    protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("模拟支付无退款回调");
     }
 
     @Override
-    protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
+    protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("模拟支付无支付回调");
     }
 

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

+ 8 - 3
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版本

+ 1 - 1
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();

+ 16 - 0
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);
+
 }

+ 30 - 0
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 <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/shopping-order/normal-shopping-detail/uploadShoppingInfo.html">上传购物详情</a>
+ * @author 芋道源码
+ */
+@Data
+public class SocialWxaOrderNotifyConfirmReceiveReqDTO {
+
+    /**
+     * 原支付交易对应的微信订单号
+     */
+    @NotEmpty(message = "原支付交易对应的微信订单号不能为空")
+    private String transactionId;
+
+    /**
+     * 快递签收时间
+     */
+    @NotNull(message = "快递签收时间不能为空")
+    private LocalDateTime receivedTime;
+
+}

+ 67 - 0
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 <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/shopping-order/normal-shopping-detail/uploadShoppingInfo.html">上传购物详情</a>
+ * @author 芋道源码
+ */
+@Data
+public class SocialWxaOrderUploadShippingInfoReqDTO {
+
+    /**
+     * 物流模式 - 实体物流配送采用快递公司进行实体物流配送形式
+     */
+    public static final Integer LOGISTICS_TYPE_EXPRESS = 1;
+    /**
+     * 物流模式 - 虚拟商品,虚拟商品,例如话费充值,点卡等,无实体配送形式
+     */
+    public static final Integer LOGISTICS_TYPE_VIRTUAL = 3;
+    /**
+     * 物流模式 - 用户自提
+     */
+    public static final Integer LOGISTICS_TYPE_PICK_UP = 4;
+
+    /**
+     * 支付者,支付者信息(openid)
+     */
+    @NotEmpty(message = "支付者,支付者信息(openid)不能为空")
+    private String openid;
+
+    /**
+     * 原支付交易对应的微信订单号
+     */
+    @NotEmpty(message = "原支付交易对应的微信订单号不能为空")
+    private String transactionId;
+
+    /**
+     * 物流模式
+     */
+    @NotNull(message = "物流模式不能为空")
+    private Integer logisticsType;
+    /**
+     * 物流发货单号
+     */
+    private String logisticsNo;
+    /**
+     * 物流公司编号
+     *
+     * @see <a href="https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/express/business/express_search.html#%E8%8E%B7%E5%8F%96%E8%BF%90%E5%8A%9Bid%E5%88%97%E8%A1%A8get-delivery-list">物流查询插件简介</a>
+     */
+    private String expressCompany;
+    /**
+     * 商品信息
+     */
+    @NotEmpty(message = "商品信息不能为空")
+    private String itemDesc;
+    /**
+     * 收件人手机号
+     */
+    @NotEmpty(message = "收件人手机号")
+    private String receiverContact;
+
+}

+ 2 - 1
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 客户端编号已存在");

+ 10 - 0
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);
+    }
+
 }

+ 6 - 0
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

+ 18 - 0
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);
+
     // =================== 客户端管理 ===================
 
     /**

+ 67 - 0
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<ShippingListBean> shippingList;
+        if (Objects.equals(reqDTO.getLogisticsType(), SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_EXPRESS)) {
+            shippingList = singletonList(ShippingListBean.builder()
+                    .trackingNo(reqDTO.getLogisticsNo())
+                    .expressCompany(reqDTO.getExpressCompany())
+                    .itemDesc(reqDTO.getItemDesc())
+                    .contact(ContactBean.builder().receiverContact(DesensitizedUtil.mobilePhone(reqDTO.getReceiverContact())).build())
+                    .build());
+        } else {
+            shippingList = singletonList(ShippingListBean.builder().itemDesc(reqDTO.getItemDesc()).build());
+        }
+        WxMaOrderShippingInfoUploadRequest request = WxMaOrderShippingInfoUploadRequest.builder()
+                .orderKey(OrderKeyBean.builder()
+                        .orderNumberType(2) // 使用原支付交易对应的微信订单号,即渠道单号
+                        .transactionId(reqDTO.getTransactionId())
+                        .build())
+                .logisticsType(reqDTO.getLogisticsType()) // 配送方式
+                .deliveryMode(1) // 统一发货
+                .shippingList(shippingList)
+                .payer(PayerBean.builder().openid(reqDTO.getOpenid()).build())
+                .uploadTime(LocalDateTimeUtil.format(LocalDateTime.now(), UTC_MS_WITH_XXX_OFFSET_PATTERN))
+                .build();
+        try {
+            WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request);
+            if (response.getErrCode() != 0) {
+                log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({}) response({})]", request, response);
+                throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, response.getErrMsg());
+            }
+            log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功:request({}) response({})]", request, response);
+        } catch (WxErrorException ex) {
+            log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({})]", request, ex);
+            throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, ex.getError().getErrorMsg());
+        }
+    }
+
+    @Override
+    public void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO) {
+        WxMaService service = getWxMaService(userType);
+        WxMaOrderShippingInfoNotifyConfirmRequest request = WxMaOrderShippingInfoNotifyConfirmRequest.builder()
+                .transactionId(reqDTO.getTransactionId())
+                .receivedTime(LocalDateTimeUtil.toEpochMilli(reqDTO.getReceivedTime()))
+                .build();
+        try {
+            WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().notifyConfirmReceive(request);
+            if (response.getErrCode() != 0) {
+                log.error("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序失败:request({}) response({})]", request, response);
+                throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR, response.getErrMsg());
+            }
+            log.info("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序成功:request({}) response({})]", request, response);
+        } catch (WxErrorException ex) {
+            log.error("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序失败:request({})]", request, ex);
+            throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR, ex.getError().getErrorMsg());
+        }
+    }
+
     /**
      * 获得 clientId + clientSecret 对应的 WxMpService 对象
      *

+ 4 - 0
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: