ソースを参照

更新商户端

gaohp 1 年間 前
コミット
7c02f81d17
77 ファイル変更4314 行追加0 行削除
  1. 83 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/pom.xml
  2. 132 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/config/FeifanTenantAutoConfiguration.java
  3. 42 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/config/TenantProperties.java
  4. 18 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/aop/TenantIgnore.java
  5. 36 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/aop/TenantIgnoreAspect.java
  6. 79 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/context/ShopTenantContextHolder.java
  7. 79 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/context/TenantContextHolder.java
  8. 26 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/db/TenantBaseDO.java
  9. 49 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/db/TenantDatabaseInterceptor.java
  10. 14 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/job/TenantJob.java
  11. 56 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/job/TenantJobAspect.java
  12. 37 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java
  13. 48 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java
  14. 23 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java
  15. 32 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java
  16. 43 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/redis/TenantRedisMessageInterceptor.java
  17. 47 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java
  18. 53 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java
  19. 37 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java
  20. 39 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/redis/TenantRedisCacheManager.java
  21. 118 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/security/TenantSecurityWebFilter.java
  22. 26 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/service/TenantFrameworkService.java
  23. 73 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/service/TenantFrameworkServiceImpl.java
  24. 94 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/util/TenantUtils.java
  25. 38 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/web/TenantContextWebFilter.java
  26. 255 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java
  27. 2 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/resources/META-INF/spring.factories
  28. 1 0
      feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  29. 38 0
      feifan-module-mall/feifan-module-product-api/src/main/java/cn/newfeifan/mall/module/product/enums/spu/SpuTypeStatusEnum.java
  30. 23 0
      feifan-module-mall/feifan-module-promotion-biz/src/main/java/cn/newfeifan/mall/module/promotion/controller/admin/diy/vo/template/DiyTemplateCreateReqByMerVO.java
  31. 173 0
      feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/TradeOrderDetailController.java
  32. 26 0
      feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/TradeOrderCountRespVO.java
  33. 46 0
      feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderAfterSaleVo.java
  34. 4 0
      feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderCommentVo.java
  35. 4 0
      feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderCustomeServiceVo.java
  36. 42 0
      feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderDeliveryVO.java
  37. 4 0
      feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderGoodsVo.java
  38. 34 0
      feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderPayVo.java
  39. 28 0
      feifan-module-sale/feifan-module-sale-api/pom.xml
  40. 16 0
      feifan-module-sale/feifan-module-sale-api/src/main/java/cn/newfeifan/mall/module/enums/ErrorCodeConstants.java
  41. 38 0
      feifan-module-sale/feifan-module-sale-api/src/main/java/cn/newfeifan/mall/module/enums/MerchantStatusEnum.java
  42. 114 0
      feifan-module-sale/feifan-module-sale-biz/pom.xml
  43. 134 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/MerchantController.java
  44. 80 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/vo/MerchantPageReqVO.java
  45. 109 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/vo/MerchantRespVO.java
  46. 21 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/vo/MerchantSaveReqByMobileVO.java
  47. 79 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/vo/MerchantSaveReqVO.java
  48. 152 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shop/ShopController.java
  49. 34 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shop/vo/ShopPageReqVO.java
  50. 48 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shop/vo/ShopRespVO.java
  51. 38 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shop/vo/ShopSaveReqVO.java
  52. 95 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shopstatus/ShopStatusController.java
  53. 25 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shopstatus/vo/ShopStatusPageReqVO.java
  54. 28 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shopstatus/vo/ShopStatusRespVO.java
  55. 19 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shopstatus/vo/ShopStatusSaveReqVO.java
  56. 98 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/dataobject/merchant/MerchantDO.java
  57. 53 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/dataobject/shop/ShopDO.java
  58. 35 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/dataobject/shopstatus/ShopStatusDO.java
  59. 41 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/mysql/merchant/MerchantMapper.java
  60. 28 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/mysql/shop/ShopMapper.java
  61. 25 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/mysql/shopstatus/ShopStatusMapper.java
  62. 61 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/merchant/MerchantService.java
  63. 133 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/merchant/MerchantServiceImpl.java
  64. 93 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/shop/ShopService.java
  65. 122 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/shop/ShopServiceImpl.java
  66. 54 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/shopstatus/ShopStatusService.java
  67. 71 0
      feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/shopstatus/ShopStatusServiceImpl.java
  68. 12 0
      feifan-module-sale/feifan-module-sale-biz/src/main/resources/mapper/merchant/MerchantMapper.xml
  69. 12 0
      feifan-module-sale/feifan-module-sale-biz/src/main/resources/mapper/shop/ShopMapper.xml
  70. 12 0
      feifan-module-sale/feifan-module-sale-biz/src/main/resources/mapper/shopstatus/ShopStatusMapper.xml
  71. 26 0
      feifan-module-sale/pom.xml
  72. 32 0
      script/feifan-admin-server.sh
  73. 162 0
      sql/mysql/建空库SQL/1_20240227.sql
  74. 120 0
      sql/mysql/建空库SQL/2_20240307.sql
  75. 39 0
      sql/mysql/建空库SQL/3_20240320.sql
  76. 53 0
      sql/mysql/建空库SQL/aaa.json
  77. BIN
      sql/mysql/建空库SQL/建最初始数据库的文件/feifansql.zip

+ 83 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/pom.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.newfeifan.zx</groupId>
+        <artifactId>feifan-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>feifan-spring-boot-starter-biz-shop-tenant</artifactId>
+
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>多租户</description>
+    <dependencies>
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-redis</artifactId>
+        </dependency>
+
+        <!-- Job 定时任务相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-job</artifactId>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-mq</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.kafka</groupId>
+            <artifactId>spring-kafka</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.amqp</groupId>
+            <artifactId>spring-rabbit</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 132 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/config/FeifanTenantAutoConfiguration.java

@@ -0,0 +1,132 @@
+package cn.newfeifan.mall.framework.shop.tenant.config;
+
+import cn.newfeifan.mall.framework.common.enums.WebFilterOrderEnum;
+import cn.newfeifan.mall.framework.mybatis.core.util.MyBatisUtils;
+import cn.newfeifan.mall.framework.redis.config.FeifanCacheProperties;
+import cn.newfeifan.mall.framework.shop.tenant.core.aop.TenantIgnoreAspect;
+import cn.newfeifan.mall.framework.shop.tenant.core.db.TenantDatabaseInterceptor;
+import cn.newfeifan.mall.framework.shop.tenant.core.job.TenantJobAspect;
+import cn.newfeifan.mall.framework.shop.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer;
+import cn.newfeifan.mall.framework.shop.tenant.core.mq.redis.TenantRedisMessageInterceptor;
+import cn.newfeifan.mall.framework.shop.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
+import cn.newfeifan.mall.framework.shop.tenant.core.redis.TenantRedisCacheManager;
+import cn.newfeifan.mall.framework.shop.tenant.core.security.TenantSecurityWebFilter;
+import cn.newfeifan.mall.framework.shop.tenant.core.service.TenantFrameworkService;
+import cn.newfeifan.mall.framework.shop.tenant.core.service.TenantFrameworkServiceImpl;
+import cn.newfeifan.mall.framework.shop.tenant.core.web.TenantContextWebFilter;
+import cn.newfeifan.mall.framework.web.config.WebProperties;
+import cn.newfeifan.mall.framework.web.core.handler.GlobalExceptionHandler;
+import cn.newfeifan.mall.module.system.api.tenant.TenantApi;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.redis.cache.BatchStrategies;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.util.Objects;
+
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "feifan.tenant.shop", value = "enable", matchIfMissing = true) // 允许使用 feifan.tenant.enable=false 禁用多租户
+@EnableConfigurationProperties(TenantProperties.class)
+public class FeifanTenantAutoConfiguration {
+
+    @Bean
+    public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) {
+        return new TenantFrameworkServiceImpl(tenantApi);
+    }
+
+    // ========== AOP ==========
+
+    @Bean
+    public TenantIgnoreAspect tenantIgnoreAspect() {
+        return new TenantIgnoreAspect();
+    }
+
+    // ========== DB ==========
+
+    @Bean
+    public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
+                                                                 MybatisPlusInterceptor interceptor) {
+        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
+        // 添加到 interceptor 中
+        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
+        MyBatisUtils.addInterceptor(interceptor, inner, 0);
+        return inner;
+    }
+
+    // ========== WEB ==========
+
+    @Bean
+    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
+        FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantContextWebFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
+        return registrationBean;
+    }
+
+    // ========== Security ==========
+
+    @Bean
+    public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
+                                                                                   WebProperties webProperties,
+                                                                                   GlobalExceptionHandler globalExceptionHandler,
+                                                                                   TenantFrameworkService tenantFrameworkService) {
+        FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties,
+                globalExceptionHandler, tenantFrameworkService));
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
+        return registrationBean;
+    }
+
+    // ========== MQ ==========
+
+    @Bean
+    public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
+        return new TenantRedisMessageInterceptor();
+    }
+
+    @Bean
+    @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
+    public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
+        return new TenantRabbitMQInitializer();
+    }
+
+    @Bean
+    @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
+    public TenantRocketMQInitializer tenantRocketMQInitializer() {
+        return new TenantRocketMQInitializer();
+    }
+
+    // ========== Job ==========
+
+    @Bean
+    public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
+        return new TenantJobAspect(tenantFrameworkService);
+    }
+
+    // ========== Redis ==========
+
+    @Bean
+    @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
+    public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
+                                                     RedisCacheConfiguration redisCacheConfiguration,
+                                                     FeifanCacheProperties feifanCacheProperties) {
+        // 创建 RedisCacheWriter 对象
+        RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
+        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
+                BatchStrategies.scan(feifanCacheProperties.getRedisScanBatchSize()));
+        // 创建 TenantRedisCacheManager 对象
+        return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration);
+    }
+
+}

+ 42 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/config/TenantProperties.java

@@ -0,0 +1,42 @@
+package cn.newfeifan.mall.framework.shop.tenant.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * 多租户配置
+ *
+ * @author 非繁源码
+ */
+@ConfigurationProperties(prefix = "feifan.tenant.shop")
+@Data
+public class TenantProperties {
+
+    /**
+     * 租户是否开启
+     */
+    private static final Boolean ENABLE_DEFAULT = true;
+
+    /**
+     * 是否开启
+     */
+    private Boolean enable = ENABLE_DEFAULT;
+
+    /**
+     * 需要忽略多租户的请求
+     *
+     * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
+     */
+    private Set<String> ignoreUrls = Collections.emptySet();
+
+    /**
+     * 需要忽略多租户的表
+     *
+     * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
+     */
+    private Set<String> ignoreTables = Collections.emptySet();
+
+}

+ 18 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/aop/TenantIgnore.java

@@ -0,0 +1,18 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.aop;
+
+import java.lang.annotation.*;
+
+/**
+ * 忽略租户,标记指定方法不进行租户的自动过滤
+ *
+ * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
+ * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
+ * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
+ *
+ * @author 非繁源码
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TenantIgnore {
+}

+ 36 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/aop/TenantIgnoreAspect.java

@@ -0,0 +1,36 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.aop;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.util.TenantUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+/**
+ * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。
+ * 例如说,一个定时任务,读取所有数据,进行处理。
+ * 又例如说,读取所有数据,进行缓存。
+ *
+ * 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致
+ *
+ * @author 非繁源码
+ */
+@Aspect
+@Slf4j
+public class TenantIgnoreAspect {
+
+    @Around("@annotation(tenantIgnore)")
+    public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
+        Boolean oldIgnore = ShopTenantContextHolder.isIgnore();
+        try {
+            ShopTenantContextHolder.setIgnore(true);
+            // 执行逻辑
+            return joinPoint.proceed();
+        } finally {
+            ShopTenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+}

+ 79 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/context/ShopTenantContextHolder.java

@@ -0,0 +1,79 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.context;
+
+import cn.hutool.core.util.StrUtil;
+import cn.newfeifan.mall.framework.common.enums.DocumentEnum;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+/**
+ * 多租户上下文 Holder
+ *
+ * @author 非繁源码
+ */
+public class ShopTenantContextHolder {
+
+    /**
+     * 当前租户编号
+     */
+    private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
+
+    /**
+     * 是否忽略租户
+     */
+    private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
+
+    /**
+     * 获得租户编号
+     *
+     * @return 租户编号
+     */
+    public static Long getTenantId() {
+        return TENANT_ID.get();
+    }
+
+    /**
+     * 获得租户编号 String
+     *
+     * @return 租户编号
+     */
+    public static String getTenantIdStr() {
+        Long tenantId = getTenantId();
+        return StrUtil.toStringOrNull(tenantId);
+    }
+
+    /**
+     * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
+     *
+     * @return 租户编号
+     */
+    public static Long getRequiredTenantId() {
+        Long tenantId = getTenantId();
+        if (tenantId == null) {
+            throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
+                + DocumentEnum.TENANT.getUrl());
+        }
+        return tenantId;
+    }
+
+    public static void setTenantId(Long tenantId) {
+        TENANT_ID.set(tenantId);
+    }
+
+    public static void setIgnore(Boolean ignore) {
+        IGNORE.set(ignore);
+    }
+
+    /**
+     * 当前是否忽略租户
+     *
+     * @return 是否忽略
+     */
+    public static boolean isIgnore() {
+        return Boolean.TRUE.equals(IGNORE.get());
+    }
+
+    public static void clear() {
+        TENANT_ID.remove();
+        IGNORE.remove();
+    }
+
+}

+ 79 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/context/TenantContextHolder.java

@@ -0,0 +1,79 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.context;
+
+import cn.hutool.core.util.StrUtil;
+import cn.newfeifan.mall.framework.common.enums.DocumentEnum;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+/**
+ * 多租户上下文 Holder
+ *
+ * @author 非繁源码
+ */
+public class TenantContextHolder {
+
+    /**
+     * 当前租户编号
+     */
+    private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
+
+    /**
+     * 是否忽略租户
+     */
+    private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
+
+    /**
+     * 获得租户编号
+     *
+     * @return 租户编号
+     */
+    public static Long getTenantId() {
+        return TENANT_ID.get();
+    }
+
+    /**
+     * 获得租户编号 String
+     *
+     * @return 租户编号
+     */
+    public static String getTenantIdStr() {
+        Long tenantId = getTenantId();
+        return StrUtil.toStringOrNull(tenantId);
+    }
+
+    /**
+     * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
+     *
+     * @return 租户编号
+     */
+    public static Long getRequiredTenantId() {
+        Long tenantId = getTenantId();
+        if (tenantId == null) {
+            throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
+                + DocumentEnum.TENANT.getUrl());
+        }
+        return tenantId;
+    }
+
+    public static void setTenantId(Long tenantId) {
+        TENANT_ID.set(tenantId);
+    }
+
+    public static void setIgnore(Boolean ignore) {
+        IGNORE.set(ignore);
+    }
+
+    /**
+     * 当前是否忽略租户
+     *
+     * @return 是否忽略
+     */
+    public static boolean isIgnore() {
+        return Boolean.TRUE.equals(IGNORE.get());
+    }
+
+    public static void clear() {
+        TENANT_ID.remove();
+        IGNORE.remove();
+    }
+
+}

+ 26 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/db/TenantBaseDO.java

@@ -0,0 +1,26 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.db;
+
+import cn.newfeifan.mall.framework.mybatis.core.dataobject.BaseDO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 拓展多租户的 BaseDO 基类
+ *
+ * @author 非繁源码
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class TenantBaseDO extends BaseDO {
+
+    /**
+     * 多租户编号
+     */
+    private Long tenantId;
+
+    /**
+     * 多店铺编号
+     */
+    private Long shopId;
+
+}

+ 49 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/db/TenantDatabaseInterceptor.java

@@ -0,0 +1,49 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.newfeifan.mall.framework.shop.tenant.config.TenantProperties;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
+ *
+ * @author 非繁源码
+ */
+public class TenantDatabaseInterceptor implements TenantLineHandler {
+
+    @Override
+    public String getTenantIdColumn() {
+        return TenantLineHandler.super.getTenantIdColumn();
+    }
+
+    private final Set<String> ignoreTables = new HashSet<>();
+
+    public TenantDatabaseInterceptor(TenantProperties properties) {
+        // 不同 DB 下,大小写的习惯不同,所以需要都添加进去
+        properties.getIgnoreTables().forEach(table -> {
+            ignoreTables.add(table.toLowerCase());
+            ignoreTables.add(table.toUpperCase());
+        });
+        // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
+        ignoreTables.add("DUAL");
+    }
+
+    @Override
+    public Expression getTenantId() {
+        return new LongValue(ShopTenantContextHolder.getRequiredTenantId());
+    }
+
+    @Override
+    public boolean ignoreTable(String tableName) {
+        return ShopTenantContextHolder.isIgnore() // 情况一,全局忽略多租户
+            || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表
+    }
+
+}

+ 14 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/job/TenantJob.java

@@ -0,0 +1,14 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.job;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 多租户 Job 注解
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TenantJob {
+}

+ 56 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/job/TenantJobAspect.java

@@ -0,0 +1,56 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.newfeifan.mall.framework.common.util.json.JsonUtils;
+import cn.newfeifan.mall.framework.shop.tenant.core.service.TenantFrameworkService;
+import cn.newfeifan.mall.framework.shop.tenant.core.util.TenantUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 多租户 JobHandler AOP
+ * 任务执行时,会按照租户逐个执行 Job 的逻辑
+ *
+ * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
+ *
+ * @author 非繁源码
+ */
+@Aspect
+@RequiredArgsConstructor
+@Slf4j
+public class TenantJobAspect {
+
+    private final TenantFrameworkService tenantFrameworkService;
+
+    @Around("@annotation(tenantJob)")
+    public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) {
+        // 获得租户列表
+        List<Long> tenantIds = tenantFrameworkService.getTenantIds();
+        if (CollUtil.isEmpty(tenantIds)) {
+            return null;
+        }
+
+        // 逐个租户,执行 Job
+        Map<Long, String> results = new ConcurrentHashMap<>();
+        tenantIds.parallelStream().forEach(tenantId -> {
+            // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
+            TenantUtils.execute(tenantId, () -> {
+                try {
+                    joinPoint.proceed();
+                } catch (Throwable e) {
+                    results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
+                }
+            });
+        });
+        return JsonUtils.toJsonString(results);
+    }
+
+}

+ 37 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java

@@ -0,0 +1,37 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.kafka;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类
+ *
+ * Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器
+ *
+ * @author 非繁源码
+ */
+@Slf4j
+public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor {
+
+    private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes";
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+        // 添加 TenantKafkaProducerInterceptor 拦截器
+        try {
+            String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES);
+            if (StrUtil.isEmpty(value)) {
+                value = TenantKafkaProducerInterceptor.class.getName();
+            } else {
+                value += "," + TenantKafkaProducerInterceptor.class.getName();
+            }
+            environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value);
+        } catch (NoClassDefFoundError ignore) {
+            // 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖
+        }
+    }
+
+}

+ 48 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java

@@ -0,0 +1,48 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.kafka;
+
+import cn.hutool.core.util.ReflectUtil;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import org.apache.kafka.clients.producer.ProducerInterceptor;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.kafka.clients.producer.RecordMetadata;
+import org.apache.kafka.common.header.Headers;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import java.util.Map;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author 非繁源码
+ */
+public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> {
+
+    @Override
+    public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射
+            headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes());
+        }
+        return record;
+    }
+
+    @Override
+    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public void configure(Map<String, ?> configs) {
+    }
+
+}

+ 23 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java

@@ -0,0 +1,23 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rabbitmq;
+
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * 多租户的 RabbitMQ 初始化器
+ *
+ * @author 非繁源码
+ */
+public class TenantRabbitMQInitializer implements BeanPostProcessor {
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof RabbitTemplate) {
+            RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
+            rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor());
+        }
+        return bean;
+    }
+
+}

+ 32 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java

@@ -0,0 +1,32 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rabbitmq;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import org.apache.kafka.clients.producer.ProducerInterceptor;
+import org.springframework.amqp.AmqpException;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.core.MessagePostProcessor;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author 非繁源码
+ */
+public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor {
+
+    @Override
+    public Message postProcessMessage(Message message) throws AmqpException {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId);
+        }
+        return message;
+    }
+
+}

+ 43 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/redis/TenantRedisMessageInterceptor.java

@@ -0,0 +1,43 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.redis;
+
+import cn.hutool.core.util.StrUtil;
+import cn.newfeifan.mall.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import cn.newfeifan.mall.framework.mq.redis.core.message.AbstractRedisMessage;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * 多租户 {@link AbstractRedisMessage} 拦截器
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中
+ *
+ * @author 非繁源码
+ */
+public class TenantRedisMessageInterceptor implements RedisMessageInterceptor {
+
+    @Override
+    public void sendMessageBefore(AbstractRedisMessage message) {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            message.addHeader(HEADER_TENANT_ID, tenantId.toString());
+        }
+    }
+
+    @Override
+    public void consumeMessageBefore(AbstractRedisMessage message) {
+        String tenantIdStr = message.getHeader(HEADER_TENANT_ID);
+        if (StrUtil.isNotEmpty(tenantIdStr)) {
+            ShopTenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
+        }
+    }
+
+    @Override
+    public void consumeMessageAfter(AbstractRedisMessage message) {
+        // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
+        ShopTenantContextHolder.clear();
+    }
+
+}

+ 47 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java

@@ -0,0 +1,47 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rocketmq;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import org.apache.rocketmq.client.hook.ConsumeMessageContext;
+import org.apache.rocketmq.client.hook.ConsumeMessageHook;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
+
+import java.util.List;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类
+ *
+ * Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
+ *
+ * @author 非繁源码
+ */
+public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook {
+
+    @Override
+    public String hookName() {
+        return getClass().getSimpleName();
+    }
+
+    @Override
+    public void consumeMessageBefore(ConsumeMessageContext context) {
+        // 校验,消息必须是单条,不然设置租户可能不正确
+        List<MessageExt> messages = context.getMsgList();
+        Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size());
+        // 设置租户编号
+        String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID);
+        if (StrUtil.isNotEmpty(tenantId)) {
+            ShopTenantContextHolder.setTenantId(Long.parseLong(tenantId));
+        }
+    }
+
+    @Override
+    public void consumeMessageAfter(ConsumeMessageContext context) {
+        ShopTenantContextHolder.clear();
+    }
+
+}

+ 53 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java

@@ -0,0 +1,53 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rocketmq;
+
+import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
+import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl;
+import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl;
+import org.apache.rocketmq.client.producer.DefaultMQProducer;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * 多租户的 RocketMQ 初始化器
+ *
+ * @author 非繁源码
+ */
+public class TenantRocketMQInitializer implements BeanPostProcessor {
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof DefaultRocketMQListenerContainer) {
+            DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
+            initTenantConsumer(container.getConsumer());
+        } else if (bean instanceof RocketMQTemplate) {
+            RocketMQTemplate template = (RocketMQTemplate) bean;
+            initTenantProducer(template.getProducer());
+        }
+        return bean;
+    }
+
+    private void initTenantProducer(DefaultMQProducer producer) {
+        if (producer == null) {
+            return;
+        }
+        DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl();
+        if (producerImpl == null) {
+            return;
+        }
+        producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook());
+    }
+
+    private void initTenantConsumer(DefaultMQPushConsumer consumer) {
+        if (consumer == null) {
+            return;
+        }
+        DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl();
+        if (consumerImpl == null) {
+            return;
+        }
+        consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
+    }
+
+}

+ 37 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java

@@ -0,0 +1,37 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.mq.rocketmq;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import org.apache.rocketmq.client.hook.SendMessageContext;
+import org.apache.rocketmq.client.hook.SendMessageHook;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类
+ *
+ * Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ *
+ * @author 非繁源码
+ */
+public class TenantRocketMQSendMessageHook implements SendMessageHook {
+
+    @Override
+    public String hookName() {
+        return getClass().getSimpleName();
+    }
+
+    @Override
+    public void sendMessageBefore(SendMessageContext sendMessageContext) {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        if (tenantId == null) {
+            return;
+        }
+        sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString());
+    }
+
+    @Override
+    public void sendMessageAfter(SendMessageContext sendMessageContext) {
+    }
+
+}

+ 39 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/redis/TenantRedisCacheManager.java

@@ -0,0 +1,39 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.redis;
+
+import cn.newfeifan.mall.framework.redis.core.TimeoutRedisCacheManager;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.Cache;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+
+/**
+ * 多租户的 {@link RedisCacheManager} 实现类
+ *
+ * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀
+ *
+ * @author airhead
+ */
+@Slf4j
+public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
+
+    public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
+                                   RedisCacheConfiguration defaultCacheConfiguration) {
+        super(cacheWriter, defaultCacheConfiguration);
+    }
+
+    @Override
+    public Cache getCache(String name) {
+        // 如果开启多租户,则 name 拼接租户后缀
+        if (!ShopTenantContextHolder.isIgnore()
+            && ShopTenantContextHolder.getTenantId() != null) {
+            name = name + ":" + ShopTenantContextHolder.getTenantId();
+        }
+
+        // 继续基于父方法
+        return super.getCache(name);
+    }
+
+}

+ 118 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/security/TenantSecurityWebFilter.java

@@ -0,0 +1,118 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.security;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.newfeifan.mall.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.newfeifan.mall.framework.common.pojo.CommonResult;
+import cn.newfeifan.mall.framework.common.util.servlet.ServletUtils;
+import cn.newfeifan.mall.framework.security.core.LoginUser;
+import cn.newfeifan.mall.framework.security.core.util.SecurityFrameworkUtils;
+import cn.newfeifan.mall.framework.shop.tenant.config.TenantProperties;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.service.TenantFrameworkService;
+import cn.newfeifan.mall.framework.web.config.WebProperties;
+import cn.newfeifan.mall.framework.web.core.filter.ApiRequestFilter;
+import cn.newfeifan.mall.framework.web.core.handler.GlobalExceptionHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.AntPathMatcher;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * 多租户 Security Web 过滤器
+ * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
+ * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。
+ * 3. 校验租户是合法,例如说被禁用、到期
+ *
+ * @author 非繁源码
+ */
+@Slf4j
+public class TenantSecurityWebFilter extends ApiRequestFilter {
+
+    private final TenantProperties tenantProperties;
+
+    private final AntPathMatcher pathMatcher;
+
+    private final GlobalExceptionHandler globalExceptionHandler;
+    private final TenantFrameworkService tenantFrameworkService;
+
+    public TenantSecurityWebFilter(TenantProperties tenantProperties,
+                                   WebProperties webProperties,
+                                   GlobalExceptionHandler globalExceptionHandler,
+                                   TenantFrameworkService tenantFrameworkService) {
+        super(webProperties);
+        this.tenantProperties = tenantProperties;
+        this.pathMatcher = new AntPathMatcher();
+        this.globalExceptionHandler = globalExceptionHandler;
+        this.tenantFrameworkService = tenantFrameworkService;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        Long tenantId = ShopTenantContextHolder.getTenantId();
+        // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
+        LoginUser user = SecurityFrameworkUtils.getLoginUser();
+        if (user != null) {
+            // 如果获取不到租户编号,则尝试使用登陆用户的租户编号
+            if (tenantId == null) {
+                tenantId = user.getTenantId();
+                ShopTenantContextHolder.setTenantId(tenantId);
+            // 如果传递了租户编号,则进行比对租户编号,避免越权问题
+            } else if (!Objects.equals(user.getTenantId(), ShopTenantContextHolder.getTenantId())) {
+                log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
+                        user.getTenantId(), user.getId(), user.getUserType(),
+                        ShopTenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
+                ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
+                        "您无权访问该租户的数据"));
+                return;
+            }
+        }
+
+        // 如果非允许忽略租户的 URL,则校验租户是否合法
+        if (!isIgnoreUrl(request)) {
+            // 2. 如果请求未带租户的编号,不允许访问。
+            if (tenantId == null) {
+                log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod());
+                ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),
+                        "请求的租户标识未传递,请进行排查"));
+                return;
+            }
+            // 3. 校验租户是合法,例如说被禁用、到期
+            try {
+                tenantFrameworkService.validTenant(tenantId);
+            } catch (Throwable ex) {
+                CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
+                ServletUtils.writeJSON(response, result);
+                return;
+            }
+        } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错
+            if (tenantId == null) {
+                ShopTenantContextHolder.setIgnore(true);
+            }
+        }
+
+        // 继续过滤
+        chain.doFilter(request, response);
+    }
+
+    private boolean isIgnoreUrl(HttpServletRequest request) {
+        // 快速匹配,保证性能
+        if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
+            return true;
+        }
+        // 逐个 Ant 路径匹配
+        for (String url : tenantProperties.getIgnoreUrls()) {
+            if (pathMatcher.match(url, request.getRequestURI())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

+ 26 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/service/TenantFrameworkService.java

@@ -0,0 +1,26 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.service;
+
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 接口,定义获取租户信息
+ *
+ * @author 非繁源码
+ */
+public interface TenantFrameworkService {
+
+    /**
+     * 获得所有租户
+     *
+     * @return 租户编号数组
+     */
+    List<Long> getTenantIds();
+
+    /**
+     * 校验租户是否合法
+     *
+     * @param id 租户编号
+     */
+    void validTenant(Long id);
+
+}

+ 73 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/service/TenantFrameworkServiceImpl.java

@@ -0,0 +1,73 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.service;
+
+import cn.newfeifan.mall.framework.common.exception.ServiceException;
+import cn.newfeifan.mall.framework.common.util.cache.CacheUtils;
+import cn.newfeifan.mall.module.system.api.tenant.TenantApi;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 实现类
+ *
+ * @author 非繁源码
+ */
+@RequiredArgsConstructor
+public class TenantFrameworkServiceImpl implements TenantFrameworkService {
+
+    private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException();
+
+    private final TenantApi tenantApi;
+
+    /**
+     * 针对 {@link #getTenantIds()} 的缓存
+     */
+    private final LoadingCache<Object, List<Long>> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Object, List<Long>>() {
+
+                @Override
+                public List<Long> load(Object key) {
+                    return tenantApi.getTenantIdList();
+                }
+
+            });
+
+    /**
+     * 针对 {@link #validTenant(Long)} 的缓存
+     */
+    private final LoadingCache<Long, ServiceException> validTenantCache = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Long, ServiceException>() {
+
+                @Override
+                public ServiceException load(Long id) {
+                    try {
+                        tenantApi.validateTenant(id);
+                        return SERVICE_EXCEPTION_NULL;
+                    } catch (ServiceException ex) {
+                        return ex;
+                    }
+                }
+
+            });
+
+    @Override
+    @SneakyThrows
+    public List<Long> getTenantIds() {
+        return getTenantIdsCache.get(Boolean.TRUE);
+    }
+
+    @Override
+    public void validTenant(Long id) {
+        ServiceException serviceException = validTenantCache.getUnchecked(id);
+        if (serviceException != SERVICE_EXCEPTION_NULL) {
+            throw serviceException;
+        }
+    }
+
+}

+ 94 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/util/TenantUtils.java

@@ -0,0 +1,94 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.util;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * 多租户 Util
+ *
+ * @author 非繁源码
+ */
+public class TenantUtils {
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param runnable 逻辑
+     */
+    public static void execute(Long tenantId, Runnable runnable) {
+        Long oldTenantId = ShopTenantContextHolder.getTenantId();
+        Boolean oldIgnore = ShopTenantContextHolder.isIgnore();
+        try {
+            ShopTenantContextHolder.setTenantId(tenantId);
+            ShopTenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            ShopTenantContextHolder.setTenantId(oldTenantId);
+            ShopTenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param callable 逻辑
+     */
+    public static <V> V execute(Long tenantId, Callable<V> callable) {
+        Long oldTenantId = ShopTenantContextHolder.getTenantId();
+        Boolean oldIgnore = ShopTenantContextHolder.isIgnore();
+        try {
+            ShopTenantContextHolder.setTenantId(tenantId);
+            ShopTenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            return callable.call();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            ShopTenantContextHolder.setTenantId(oldTenantId);
+            ShopTenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 忽略租户,执行对应的逻辑
+     *
+     * @param runnable 逻辑
+     */
+    public static void executeIgnore(Runnable runnable) {
+        Boolean oldIgnore = ShopTenantContextHolder.isIgnore();
+        try {
+            ShopTenantContextHolder.setIgnore(true);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            ShopTenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+    /**
+     * 将多租户编号,添加到 header 中
+     *
+     * @param headers HTTP 请求 headers
+     * @param tenantId 租户编号
+     */
+    public static void addTenantHeader(Map<String, String> headers, Long tenantId) {
+        if (tenantId != null) {
+            headers.put(HEADER_TENANT_ID, tenantId.toString());
+        }
+    }
+
+}

+ 38 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/cn/newfeifan/mall/framework/shop/tenant/core/web/TenantContextWebFilter.java

@@ -0,0 +1,38 @@
+package cn.newfeifan.mall.framework.shop.tenant.core.web;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.ShopTenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 多租户 Context Web 过滤器
+ * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
+ *
+ * @author 非繁源码
+ */
+public class TenantContextWebFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        // 设置
+        Long tenantId = WebFrameworkUtils.getTenantId(request);
+        if (tenantId != null) {
+            ShopTenantContextHolder.setTenantId(tenantId);
+        }
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            // 清理
+            ShopTenantContextHolder.clear();
+        }
+    }
+
+}

+ 255 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java

@@ -0,0 +1,255 @@
+
+
+package org.springframework.messaging.handler.invocation;
+
+import cn.newfeifan.mall.framework.shop.tenant.core.context.TenantContextHolder;
+import cn.newfeifan.mall.framework.shop.tenant.core.util.TenantUtils;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.ResolvableType;
+import org.springframework.lang.Nullable;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.handler.HandlerMethod;
+import org.springframework.util.ObjectUtils;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+
+import static cn.newfeifan.mall.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * Extension of {@link HandlerMethod} that invokes the underlying method with
+ * argument values resolved from the current HTTP request through a list of
+ * {@link HandlerMethodArgumentResolver}.
+ *
+ * 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中
+ * TODO 芋艿:持续跟进,看看有没新的拓展点
+ *
+ * @author Rossen Stoyanchev
+ * @author Juergen Hoeller
+ * @since 4.0
+ */
+public class InvocableHandlerMethod extends HandlerMethod {
+
+    private static final Object[] EMPTY_ARGS = new Object[0];
+
+    private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
+
+    private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
+
+    /**
+     * Create an instance from a {@code HandlerMethod}.
+     */
+    public InvocableHandlerMethod(HandlerMethod handlerMethod) {
+        super(handlerMethod);
+    }
+
+    /**
+     * Create an instance from a bean instance and a method.
+     */
+    public InvocableHandlerMethod(Object bean, Method method) {
+        super(bean, method);
+    }
+
+    /**
+     * Construct a new handler method with the given bean instance, method name and parameters.
+     * @param bean the object bean
+     * @param methodName the method name
+     * @param parameterTypes the method parameter types
+     * @throws NoSuchMethodException when the method cannot be found
+     */
+    public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
+            throws NoSuchMethodException {
+
+        super(bean, methodName, parameterTypes);
+    }
+
+    /**
+     * Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values.
+     */
+    public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
+        this.resolvers = argumentResolvers;
+    }
+
+    /**
+     * Set the ParameterNameDiscoverer for resolving parameter names when needed
+     * (e.g. default request attribute name).
+     * <p>Default is a {@link DefaultParameterNameDiscoverer}.
+     */
+    public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
+        this.parameterNameDiscoverer = parameterNameDiscoverer;
+    }
+
+    /**
+     * Invoke the method after resolving its argument values in the context of the given message.
+     * <p>Argument values are commonly resolved through
+     * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
+     * The {@code providedArgs} parameter however may supply argument values to be used directly,
+     * i.e. without argument resolution.
+     * <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
+     * resolved arguments.
+     * @param message the current message being processed
+     * @param providedArgs "given" arguments matched by type, not resolved
+     * @return the raw value returned by the invoked method
+     * @throws Exception raised if no suitable argument resolver can be found,
+     * or if the method raised an exception
+     * @see #getMethodArgumentValues
+     * @see #doInvoke
+     */
+    @Nullable
+    public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
+        Object[] args = getMethodArgumentValues(message, providedArgs);
+        if (logger.isTraceEnabled()) {
+            logger.trace("Arguments: " + Arrays.toString(args));
+        }
+        // 注意:如下是本类的改动点!!!
+        // 情况一:无租户编号的情况
+        Long tenantId= parseTenantId(message);
+        if (tenantId == null) {
+            return doInvoke(args);
+        }
+        // 情况二:有租户的情况下
+        return TenantUtils.execute(tenantId, () -> doInvoke(args));
+    }
+
+    private Long parseTenantId(Message<?> message) {
+        Object tenantId = message.getHeaders().get(HEADER_TENANT_ID);
+        if (tenantId == null) {
+            return null;
+        }
+        if (tenantId instanceof Long) {
+            return (Long) tenantId;
+        }
+        if (tenantId instanceof Number) {
+            return ((Number) tenantId).longValue();
+        }
+        if (tenantId instanceof String) {
+            return Long.parseLong((String) tenantId);
+        }
+        if (tenantId instanceof byte[]) {
+            return Long.parseLong(new String((byte[]) tenantId));
+        }
+        throw new IllegalArgumentException("未知的数据类型:" + tenantId);
+    }
+
+    /**
+     * Get the method argument values for the current message, checking the provided
+     * argument values and falling back to the configured argument resolvers.
+     * <p>The resulting array will be passed into {@link #doInvoke}.
+     * @since 5.1.2
+     */
+    protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
+        MethodParameter[] parameters = getMethodParameters();
+        if (ObjectUtils.isEmpty(parameters)) {
+            return EMPTY_ARGS;
+        }
+
+        Object[] args = new Object[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            MethodParameter parameter = parameters[i];
+            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
+            args[i] = findProvidedArgument(parameter, providedArgs);
+            if (args[i] != null) {
+                continue;
+            }
+            if (!this.resolvers.supportsParameter(parameter)) {
+                throw new MethodArgumentResolutionException(
+                        message, parameter, formatArgumentError(parameter, "No suitable resolver"));
+            }
+            try {
+                args[i] = this.resolvers.resolveArgument(parameter, message);
+            }
+            catch (Exception ex) {
+                // Leave stack trace for later, exception may actually be resolved and handled...
+                if (logger.isDebugEnabled()) {
+                    String exMsg = ex.getMessage();
+                    if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
+                        logger.debug(formatArgumentError(parameter, exMsg));
+                    }
+                }
+                throw ex;
+            }
+        }
+        return args;
+    }
+
+    /**
+     * Invoke the handler method with the given argument values.
+     */
+    @Nullable
+    protected Object doInvoke(Object... args) throws Exception {
+        try {
+            return getBridgedMethod().invoke(getBean(), args);
+        }
+        catch (IllegalArgumentException ex) {
+            assertTargetBean(getBridgedMethod(), getBean(), args);
+            String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
+            throw new IllegalStateException(formatInvokeError(text, args), ex);
+        }
+        catch (InvocationTargetException ex) {
+            // Unwrap for HandlerExceptionResolvers ...
+            Throwable targetException = ex.getTargetException();
+            if (targetException instanceof RuntimeException) {
+                throw (RuntimeException) targetException;
+            }
+            else if (targetException instanceof Error) {
+                throw (Error) targetException;
+            }
+            else if (targetException instanceof Exception) {
+                throw (Exception) targetException;
+            }
+            else {
+                throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
+            }
+        }
+    }
+
+    MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) {
+        return new AsyncResultMethodParameter(returnValue);
+    }
+
+    private class AsyncResultMethodParameter extends HandlerMethodParameter {
+
+        @Nullable
+        private final Object returnValue;
+
+        private final ResolvableType returnType;
+
+        public AsyncResultMethodParameter(@Nullable Object returnValue) {
+            super(-1);
+            this.returnValue = returnValue;
+            this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
+        }
+
+        protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
+            super(original);
+            this.returnValue = original.returnValue;
+            this.returnType = original.returnType;
+        }
+
+        @Override
+        public Class<?> getParameterType() {
+            if (this.returnValue != null) {
+                return this.returnValue.getClass();
+            }
+            if (!ResolvableType.NONE.equals(this.returnType)) {
+                return this.returnType.toClass();
+            }
+            return super.getParameterType();
+        }
+
+        @Override
+        public Type getGenericParameterType() {
+            return this.returnType.getType();
+        }
+
+        @Override
+        public AsyncResultMethodParameter clone() {
+            return new AsyncResultMethodParameter(this);
+        }
+    }
+
+}

+ 2 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.env.EnvironmentPostProcessor=\
+  cn.newfeifan.mall.framework.shop.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor

+ 1 - 0
feifan-framework/feifan-spring-boot-starter-biz-shop-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+cn.newfeifan.mall.framework.shop.tenant.config.FeifanTenantAutoConfiguration

+ 38 - 0
feifan-module-mall/feifan-module-product-api/src/main/java/cn/newfeifan/mall/module/product/enums/spu/SpuTypeStatusEnum.java

@@ -0,0 +1,38 @@
+package cn.newfeifan.mall.module.product.enums.spu;
+
+import cn.newfeifan.mall.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 商品 SPU 状态
+ *
+ * @author 非繁源码
+ */
+@Getter
+@AllArgsConstructor
+public enum SpuTypeStatusEnum implements IntArrayValuable {
+
+    VIRTUAL_SPU(1, "虚拟商品"),
+    REAL_SPU(2, "实体商品");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SpuTypeStatusEnum::getStatus).toArray();
+
+    /**
+     * 状态
+     */
+    private final Integer status;
+    /**
+     * 状态名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+
+}

+ 23 - 0
feifan-module-mall/feifan-module-promotion-biz/src/main/java/cn/newfeifan/mall/module/promotion/controller/admin/diy/vo/template/DiyTemplateCreateReqByMerVO.java

@@ -0,0 +1,23 @@
+package cn.newfeifan.mall.module.promotion.controller.admin.diy.vo.template;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+@Schema(description = "管理后台 - 装修模板创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class DiyTemplateCreateReqByMerVO extends DiyTemplateBaseVO {
+
+    @Schema(description = "店铺ID",  example = "123")
+    private Long shopId;
+
+    @Schema(description = "商户ID", example = "12")
+    private Long merId;
+
+}

+ 173 - 0
feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/TradeOrderDetailController.java

@@ -0,0 +1,173 @@
+package cn.newfeifan.mall.module.trade.controller.admin.order;
+
+import cn.newfeifan.mall.framework.common.pojo.CommonResult;
+import cn.newfeifan.mall.module.member.api.user.MemberUserApi;
+import cn.newfeifan.mall.module.member.api.user.dto.MemberUserRespDTO;
+//import cn.newfeifan.mall.module.pay.dal.dataobject.order.PayOrderDO;
+import cn.newfeifan.mall.module.pay.dal.dataobject.order.PayOrderDO;
+import cn.newfeifan.mall.module.pay.service.order.PayOrderService;
+import cn.newfeifan.mall.module.trade.controller.admin.aftersale.vo.AfterSaleBaseVO;
+import cn.newfeifan.mall.module.trade.controller.admin.order.vo.*;
+import cn.newfeifan.mall.module.trade.controller.admin.order.vo.detailvo.TradeOrderDeliveryVO;
+import cn.newfeifan.mall.module.trade.controller.admin.order.vo.detailvo.TradeOrderPayVo;
+import cn.newfeifan.mall.module.trade.controller.app.order.vo.AppTradeOrderDetailRespVO;
+import cn.newfeifan.mall.module.trade.convert.aftersale.AfterSaleConvert;
+import cn.newfeifan.mall.module.trade.convert.order.TradeOrderConvert;
+import cn.newfeifan.mall.module.trade.dal.dataobject.aftersale.AfterSaleDO;
+import cn.newfeifan.mall.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
+import cn.newfeifan.mall.module.trade.dal.dataobject.order.TradeOrderDO;
+import cn.newfeifan.mall.module.trade.dal.dataobject.order.TradeOrderItemDO;
+import cn.newfeifan.mall.module.trade.dal.dataobject.order.TradeOrderLogDO;
+import cn.newfeifan.mall.module.trade.enums.delivery.DeliveryTypeEnum;
+import cn.newfeifan.mall.module.trade.framework.order.config.TradeOrderProperties;
+import cn.newfeifan.mall.module.trade.service.aftersale.AfterSaleService;
+import cn.newfeifan.mall.module.trade.service.delivery.DeliveryExpressService;
+import cn.newfeifan.mall.module.trade.service.order.TradeOrderLogService;
+import cn.newfeifan.mall.module.trade.service.order.TradeOrderQueryService;
+import cn.newfeifan.mall.module.trade.service.order.TradeOrderUpdateService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.newfeifan.mall.framework.common.pojo.CommonResult.success;
+import static cn.newfeifan.mall.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.newfeifan.mall.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.newfeifan.mall.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - 订单详情")
+@RestController
+@RequestMapping("/trade/order-detail")
+@Validated
+@Slf4j
+public class TradeOrderDetailController {
+
+    @Resource
+    private TradeOrderUpdateService tradeOrderUpdateService;
+    @Resource
+    private TradeOrderQueryService tradeOrderQueryService;
+    @Resource
+    private TradeOrderLogService tradeOrderLogService;
+    @Resource
+    private TradeOrderProperties tradeOrderProperties;
+
+    @Resource
+    private PayOrderService payOrderService;
+
+    @Resource
+    private MemberUserApi memberUserApi;
+    @Resource
+    private AfterSaleService afterSaleService;
+
+    @Resource
+    private DeliveryExpressService deliveryExpressService;
+
+    @GetMapping("/get-detail")
+    @Operation(summary = "订单基本信息")
+    @Parameter(name = "id", description = "订单编号", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('trade:order:query')")
+    public CommonResult<TradeOrderDetailRespVO> getOrderDetail(@RequestParam("id") Long id) {
+        // 查询订单
+        TradeOrderDO order = tradeOrderQueryService.getOrder(id);
+        if (order == null) {
+            return success(null);
+        }
+        // 查询订单项
+        List<TradeOrderItemDO> orderItems = tradeOrderQueryService.getOrderItemListByOrderId(id);
+
+        // 拼接数据
+        MemberUserRespDTO user = memberUserApi.getUser(order.getUserId());
+        MemberUserRespDTO brokerageUser = order.getBrokerageUserId() != null ?
+                memberUserApi.getUser(order.getBrokerageUserId()) : null;
+        List<TradeOrderLogDO> orderLogs = tradeOrderLogService.getOrderLogListByOrderId(id);
+        return success(TradeOrderConvert.INSTANCE.convert(order, orderItems, orderLogs, user, brokerageUser));
+    }
+
+    @GetMapping("/after_sale")
+    @Operation(summary = "新增接口20240229-售后信息")
+    @Parameter(name = "id", description = "订单编号", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('trade:order:query')")
+    public CommonResult<AfterSaleBaseVO> afterSale(@RequestParam("id") Long id) {
+        // 查询订单
+        TradeOrderDO order = tradeOrderQueryService.getOrder(id);
+        if (order == null) {
+            return success(null);
+        }
+        // 查询售后订单
+        AfterSaleDO afterSaleByOrderNo = afterSaleService.getAfterSaleByOrderNo(order.getNo());
+        return success(AfterSaleConvert.INSTANCE.convert02(afterSaleByOrderNo));
+    }
+
+    @GetMapping("/delivery")
+    @Operation(summary = "新增接口20240229-物流信息")
+    @Parameter(name = "id", description = "订单编号", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('trade:order:query')")
+    public CommonResult<TradeOrderDeliveryVO> delivery(@RequestParam("id") Long id) {
+        // 查询订单
+        TradeOrderDO order = tradeOrderQueryService.getOrder(id);
+        if (order == null) {
+            return success(null);
+        }
+        TradeOrderDeliveryVO build = TradeOrderDeliveryVO.builder()
+                .logisticsNo(order.getLogisticsNo())
+                .deliveryType(DeliveryTypeEnum.getNameByValue(order.getDeliveryType()))
+                .deliveryPrice(order.getDeliveryPrice())
+                .logisticsId(order.getLogisticsId())
+                .remark(order.getRemark()).build();
+        return success(build);
+    }
+
+    @GetMapping("/payInfo")
+    @Operation(summary = "新增接口20240229-支付信息")
+    @Parameter(name = "id", description = "订单编号", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('trade:order:query')")
+    public CommonResult<TradeOrderPayVo> payInfo(@RequestParam("id") Long id) {
+        // 查询订单
+        TradeOrderDO order = tradeOrderQueryService.getOrder(id);
+        if (order == null) {
+            return success(null);
+        }
+        // 查询支付状态
+        PayOrderDO payOrderDO = payOrderService.getOrderByTradeOrderId(id);
+        TradeOrderPayVo build = TradeOrderPayVo.builder()
+                .userRemark(order.getUserRemark())
+                .payChannelCode(order.getPayChannelCode())
+                .path("")
+                .payPrice(order.getPayPrice())
+                .build();
+        if (payOrderDO == null){
+            build.setPayStatus("未支付");
+        }else {
+            build.setPayStatus(payOrderDO.getStatus() == 0 ? "未支付" : "已支付");
+        }
+        return success(null);
+    }
+
+    @GetMapping("/productInfo")
+    @Operation(summary = "新增接口20240229-商品订单信息信息")
+    @Parameter(name = "id", description = "订单编号", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('trade:order:query')")
+    public CommonResult<AppTradeOrderDetailRespVO> productInfo(@RequestParam("id") Long id) {
+        // 查询订单
+        TradeOrderDO order = tradeOrderQueryService.getOrder(id);
+        if (order == null) {
+            return success(null);
+        }
+
+        // 查询订单项
+        List<TradeOrderItemDO> orderItems = tradeOrderQueryService.getOrderItemListByOrderId(order.getId());
+        // 查询物流公司
+        DeliveryExpressDO express = order.getLogisticsId() != null && order.getLogisticsId() > 0 ?
+                deliveryExpressService.getDeliveryExpress(order.getLogisticsId()) : null;
+        // 最终组合
+        return success(TradeOrderConvert.INSTANCE.convert02(order, orderItems, tradeOrderProperties, express));
+    }
+
+
+}

+ 26 - 0
feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/TradeOrderCountRespVO.java

@@ -0,0 +1,26 @@
+package cn.newfeifan.mall.module.trade.controller.admin.order.vo;
+
+import cn.newfeifan.mall.framework.common.validation.InEnum;
+import cn.newfeifan.mall.module.trade.enums.order.TradeOrderStatusEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Schema(description = "管理后台 - 订单数量 Response VO")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TradeOrderCountRespVO {
+
+    @Schema(description = "订单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long orderCount;
+
+    @Schema(description = "订单状态", example = "1")
+    @InEnum(value = TradeOrderStatusEnum.class, message = "订单状态必须是 {value}")
+    private Integer status;
+
+
+}

+ 46 - 0
feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderAfterSaleVo.java

@@ -0,0 +1,46 @@
+package cn.newfeifan.mall.module.trade.controller.admin.order.vo.detailvo;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 售后服务 Response VO")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TradeOrderAfterSaleVo {
+
+    // 退款金额
+    private Integer  price;
+
+    // 原因
+    private String season;
+
+    // 描述
+    private String mark;
+
+    // 退款类型
+    private String typeName;
+
+    // 产品名称
+    private String goodName;
+
+    // 产品型号
+    private String skuName;
+
+    // 退款凭证图片
+    private List<String> afterSaleUrls;
+
+    // 售后单号
+    private String afterSaleNo;
+
+
+
+
+}

+ 4 - 0
feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderCommentVo.java

@@ -0,0 +1,4 @@
+package cn.newfeifan.mall.module.trade.controller.admin.order.vo.detailvo;
+
+public class TradeOrderCommentVo {
+}

+ 4 - 0
feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderCustomeServiceVo.java

@@ -0,0 +1,4 @@
+package cn.newfeifan.mall.module.trade.controller.admin.order.vo.detailvo;
+
+public class TradeOrderCustomeServiceVo {
+}

+ 42 - 0
feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderDeliveryVO.java

@@ -0,0 +1,42 @@
+package cn.newfeifan.mall.module.trade.controller.admin.order.vo.detailvo;
+
+import cn.newfeifan.mall.framework.common.validation.InEnum;
+import cn.newfeifan.mall.module.trade.enums.delivery.DeliveryTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 订单发货 Request VO")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TradeOrderDeliveryVO {
+
+    @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "订单编号不能为空")
+    private Long id;
+
+    @Schema(description = "发货物流公司编号", example = "1")
+    @NotNull(message = "发货物流公司不能为空")
+    private Long logisticsId;
+
+    @Schema(description = "发货物流单号", example = "SF123456789")
+    private String logisticsNo;
+
+    @Schema(description = "运费 单位分", example = "10")
+    private Integer deliveryPrice;
+
+    @Schema(description = "配送方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @InEnum(value = DeliveryTypeEnum.class, message = "配送方式不正确")
+    private String deliveryType;
+
+    @Schema(description = "商家备注", example = "你好")
+    private String remark;
+
+
+}

+ 4 - 0
feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderGoodsVo.java

@@ -0,0 +1,4 @@
+package cn.newfeifan.mall.module.trade.controller.admin.order.vo.detailvo;
+
+public class TradeOrderGoodsVo {
+}

+ 34 - 0
feifan-module-mall/feifan-module-trade-biz/src/main/java/cn/newfeifan/mall/module/trade/controller/admin/order/vo/detailvo/TradeOrderPayVo.java

@@ -0,0 +1,34 @@
+package cn.newfeifan.mall.module.trade.controller.admin.order.vo.detailvo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Schema(description = "管理后台 - 支付信息")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TradeOrderPayVo {
+
+
+
+    @Schema(description = "总金额", example = "102")
+    private Integer payPrice;
+
+    @Schema(description = "支付状态", example = "未支付")
+    private String payStatus;
+
+    @Schema(description = "支付方式 支付渠道", example = "微信支付")
+    private String payChannelCode;
+
+
+    @Schema(description = "路径", example = "?")
+    private String path;
+
+    @Schema(description = "备注", example = "你好,用户备注")
+    private String userRemark;
+
+}

+ 28 - 0
feifan-module-sale/feifan-module-sale-api/pom.xml

@@ -0,0 +1,28 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.newfeifan.sale</groupId>
+        <artifactId>feifan-module-sale</artifactId>
+        <version>${revision}</version> <!-- 1. 修改 version 为 ${revision} -->
+    </parent>
+
+    <artifactId>feifan-module-sale-api</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name> <!-- 3. 新增 name 为 ${project.artifactId} -->
+    <url>http://maven.apache.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <description> <!-- 4. 新增 description 为该模块的描述 -->
+        sale 模块,主要实现  商户 店铺 等功能。
+    </description>
+    <dependencies>  <!-- 5. 新增 yudao-common 依赖 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-common</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 16 - 0
feifan-module-sale/feifan-module-sale-api/src/main/java/cn/newfeifan/mall/module/enums/ErrorCodeConstants.java

@@ -0,0 +1,16 @@
+package cn.newfeifan.mall.module.enums;
+
+import cn.newfeifan.mall.framework.common.exception.ErrorCode;
+
+/**
+ * System 错误码枚举类
+ *
+ * system 系统,使用 1-002-000-000 段
+ */
+public interface ErrorCodeConstants {
+    // ========== 店铺商户信息 1-002-029-000 ==========
+    ErrorCode SHOP_STATUS_NOT_EXISTS = new ErrorCode(1_002_029_000 , "店铺状态不存在");
+    ErrorCode MERCHANT_NOT_EXISTS = new ErrorCode(1_002_029_001, "商户不存在");
+    ErrorCode SHOP_NOT_EXISTS = new ErrorCode(1_002_029_002, "店铺不存在不存在");
+
+}

+ 38 - 0
feifan-module-sale/feifan-module-sale-api/src/main/java/cn/newfeifan/mall/module/enums/MerchantStatusEnum.java

@@ -0,0 +1,38 @@
+package cn.newfeifan.mall.module.enums;
+
+import cn.newfeifan.mall.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 交易订单 - 关闭类型
+ *
+ * @author Sin
+ */
+@RequiredArgsConstructor
+@Getter
+public enum MerchantStatusEnum implements IntArrayValuable {
+
+    USING(10, "使用中"),
+    OVERDUE(20, "过期"),
+    STOP(30, "停用");
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MerchantStatusEnum::getType).toArray();
+
+    /**
+     * 关闭类型
+     */
+    private final Integer type;
+    /**
+     * 关闭类型名
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 114 - 0
feifan-module-sale/feifan-module-sale-biz/pom.xml

@@ -0,0 +1,114 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.newfeifan.sale</groupId>
+        <artifactId>feifan-module-sale</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>feifan-module-sale-biz</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <url>http://maven.apache.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <description> <!-- 4. 新增 description 为该模块的描述 -->
+        sale 模块,主要实现 商户 店铺 等功能。
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.newfeifan.sale</groupId>
+            <artifactId>feifan-module-sale-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-module-system-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-module-infra-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-module-system-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-module-product-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-module-trade-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-biz-operatelog</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-biz-tenant</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-redis</artifactId>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-mq</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-excel</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.newfeifan.zx</groupId>
+            <artifactId>feifan-spring-boot-starter-biz-ip</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 134 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/MerchantController.java

@@ -0,0 +1,134 @@
+package cn.newfeifan.mall.sale.controller.admin.merchant;
+
+import cn.newfeifan.mall.module.system.service.user.AdminUserService;
+import cn.newfeifan.mall.module.trade.service.order.TradeOrderQueryService;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantPageReqVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantRespVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantSaveReqByMobileVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantSaveReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.merchant.MerchantDO;
+import cn.newfeifan.mall.sale.service.merchant.MerchantService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+import java.util.stream.Collectors;
+
+import cn.newfeifan.mall.framework.common.pojo.PageParam;
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.framework.common.pojo.CommonResult;
+import cn.newfeifan.mall.framework.common.util.object.BeanUtils;
+
+import static cn.newfeifan.mall.framework.common.pojo.CommonResult.error;
+import static cn.newfeifan.mall.framework.common.pojo.CommonResult.success;
+
+import cn.newfeifan.mall.framework.excel.core.util.ExcelUtils;
+
+import cn.newfeifan.mall.framework.operatelog.core.annotations.OperateLog;
+
+import static cn.newfeifan.mall.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+
+@Tag(name = "管理后台 - 商户")
+@RestController
+@RequestMapping("/sale/merchant")
+@Validated
+public class MerchantController {
+
+    @Resource
+    private AdminUserService adminUserService;
+    @Resource
+    private MerchantService merchantService;
+
+    @Resource
+    private TradeOrderQueryService tradeOrderQueryService;
+
+
+    @PostMapping("/create")
+    @Operation(summary = "创建商户")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:create')")
+    public CommonResult<HashMap<String, Long>> createMerchant(@Valid @RequestBody MerchantSaveReqVO createReqVO) {
+        return success(merchantService.createMerchant(createReqVO));
+    }
+
+    /*@PostMapping("/createByMobile")
+    @Operation(summary = "通过手机号创建一个用户")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:create')")
+    public CommonResult<String> createByMobile(@RequestBody MerchantSaveReqByMobileVO createReqVO) {
+        adminUserService.createUserByMobile(merchant.getId(), createReqVO.getContactNumber(), createReqVO.getContact());
+        return success("创建成功");
+    }*/
+
+
+    @PutMapping("/update")
+    @Operation(summary = "更新商户")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:update')")
+    public CommonResult<Boolean> updateMerchant(@Valid @RequestBody MerchantSaveReqVO updateReqVO) {
+        /*LoginUser loginUser =  SecurityFrameworkUtils.getLoginUser();
+        if (loginUser==null){
+            return error(UNAUTHORIZED);
+        }
+        // 添加校验, 如果是平台用户的话, 就不能修改商户信息
+        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
+            return error(FORBIDDEN);
+        }*/
+        merchantService.updateMerchant(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除商户")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('sale:merchant:delete')")
+    public CommonResult<Boolean> deleteMerchant(@RequestParam("id") Long id) {
+        merchantService.deleteMerchant(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得商户")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:query')")
+    public CommonResult<MerchantRespVO> getMerchant(@RequestParam("id") Long id) {
+        MerchantDO merchant = merchantService.getMerchant(id);
+        return success(BeanUtils.toBean(merchant, MerchantRespVO.class));
+    }
+
+
+    @GetMapping("/page")
+    @Operation(summary = "获得商户分页")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:query')")
+    public CommonResult<PageResult<MerchantRespVO>> getMerchantPage(@Valid MerchantPageReqVO pageReqVO) {
+        PageResult<MerchantDO> pageResult = merchantService.getMerchantPage(pageReqVO);
+        PageResult<MerchantRespVO> merchantRespVOPageResult = BeanUtils.toBean(pageResult, MerchantRespVO.class);
+
+        Map<Long, MerchantRespVO> map = merchantRespVOPageResult.getList().stream().collect(Collectors.toMap(MerchantRespVO::getId, k -> k));
+        List<MerchantRespVO> merchantRespVOS = merchantService.salesVolume(map);
+        merchantRespVOPageResult.setList(merchantRespVOS);
+        return success(merchantRespVOPageResult);
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出商户 Excel")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:export')")
+    @OperateLog(type = EXPORT)
+    public void exportMerchantExcel(@Valid MerchantPageReqVO pageReqVO,
+                                    HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<MerchantDO> list = merchantService.getMerchantPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "商户.xls", "数据", MerchantRespVO.class,
+                BeanUtils.toBean(list, MerchantRespVO.class));
+    }
+
+}

+ 80 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/vo/MerchantPageReqVO.java

@@ -0,0 +1,80 @@
+package cn.newfeifan.mall.sale.controller.admin.merchant.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.newfeifan.mall.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.newfeifan.mall.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 商户分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MerchantPageReqVO extends PageParam {
+
+    @Schema(description = "商户名称", example = "李四")
+    private String name;
+
+    @Schema(description = "商户状态,关联商户状态表", example = "2")
+    private Integer status;
+
+    @Schema(description = "简介", example = "随便")
+    private String description;
+
+    @Schema(description = "负责人")
+    private String contact;
+
+    @Schema(description = "所在地")
+    private String address;
+
+    @Schema(description = "负责人电话")
+    private String contactNumber;
+
+    @Schema(description = "域名")
+    private String website;
+
+    @Schema(description = "官网")
+    private String businessUrl;
+
+    @Schema(description = "维权电话")
+    private String complaintsHotline;
+
+    @Schema(description = "客服电话")
+    private String customerServiceHotline;
+
+    @Schema(description = "邮箱")
+    private String email;
+
+    @Schema(description = "营业执照图片路径")
+    private String businessLicensePicture;
+
+    @Schema(description = "服务到期时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] expireTime;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "logo地址", example = "https://www.zhongxing.cn")
+    private String logoUrl;
+
+/*    @Schema(description = "商户负责人姓名", example = "芋艿")
+    private String contactName;
+
+    @Schema(description = "商户负责人手机")
+    private String contactMobile;*/
+
+    @Schema(description = "服务套餐", example = "8463")
+    private Integer packageId;
+
+    @Schema(description = "店铺数量", example = "11326")
+    private Integer shopCount;
+
+    @Schema(description = "区域id,对应文件系统 src/main/resources/area.csv 中的id一列", example = "22725")
+    private Long areaId;
+}

+ 109 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/vo/MerchantRespVO.java

@@ -0,0 +1,109 @@
+package cn.newfeifan.mall.sale.controller.admin.merchant.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+
+@Schema(description = "管理后台 - 商户 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class MerchantRespVO {
+
+    @Schema(description = "商户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "24252")
+    @ExcelProperty("商户id")
+    private Long id;
+
+    @Schema(description = "商户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @ExcelProperty("商户名称")
+    private String name;
+
+    @Schema(description = "商户状态,关联商户状态表", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("商户状态,关联商户状态表")
+    private Integer status;
+
+    @Schema(description = "简介", example = "随便")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "负责人")
+    @ExcelProperty("负责人")
+    private String contact;
+
+    @Schema(description = "所在地")
+    @ExcelProperty("所在地")
+    private String address;
+
+    @Schema(description = "负责人电话")
+    @ExcelProperty("负责人电话")
+    private String contactNumber;
+
+    @Schema(description = "域名")
+    @ExcelProperty("域名")
+    private String website;
+
+    @Schema(description = "官网")
+    @ExcelProperty("官网")
+    private String businessUrl;
+
+    @Schema(description = "维权电话")
+    @ExcelProperty("维权电话")
+    private String complaintsHotline;
+
+    @Schema(description = "客服电话")
+    @ExcelProperty("客服电话")
+    private String customerServiceHotline;
+
+    @Schema(description = "邮箱")
+    @ExcelProperty("邮箱")
+    private String email;
+
+    @Schema(description = "营业执照图片路径")
+    @ExcelProperty("营业执照图片路径")
+    private String businessLicensePicture;
+
+    @Schema(description = "服务到期时间")
+    @ExcelProperty("服务到期时间")
+    private LocalDateTime expireTime;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "logo地址", example = "https://www.zhongxing.cn")
+    @ExcelProperty("logo地址")
+    private String logoUrl;
+
+
+/*    @Schema(description = "商户负责人姓名", example = "芋艿")
+    @ExcelProperty("商户负责人姓名")
+    private String contactName;
+
+    @Schema(description = "商户负责人手机")
+    @ExcelProperty("商户负责人手机")
+    private String contactMobile;*/
+
+    @Schema(description = "服务套餐", example = "8463")
+    @ExcelProperty("服务套餐")
+    private Integer packageId;
+
+    @Schema(description = "店铺数量", example = "11326")
+    @ExcelProperty("店铺数量")
+    private Integer shopCount;
+
+    @Schema(description = "区域id,对应文件系统 src/main/resources/area.csv 中的id一列", example = "22725")
+    @ExcelProperty("区域id,对应文件系统 src/main/resources/area.csv 中的id一列")
+    private Long areaId;
+
+    @Schema(description = "商户总销量", example = "8463")
+    @ExcelProperty("商户总销量")
+    private Integer salesVolume ;
+
+    @Schema(description = "商品总数", example = "8463")
+    @ExcelProperty("商品总数")
+    private Integer skuCount;
+
+}

+ 21 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/vo/MerchantSaveReqByMobileVO.java

@@ -0,0 +1,21 @@
+package cn.newfeifan.mall.sale.controller.admin.merchant.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 商户新增/修改 Request VO")
+@Data
+public class MerchantSaveReqByMobileVO {
+
+    @Schema(description = "负责人")
+    private String contact;
+
+    @Schema(description = "负责人电话")
+    private String contactNumber;
+
+
+}

+ 79 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/merchant/vo/MerchantSaveReqVO.java

@@ -0,0 +1,79 @@
+package cn.newfeifan.mall.sale.controller.admin.merchant.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 商户新增/修改 Request VO")
+@Data
+public class MerchantSaveReqVO {
+
+    @Schema(description = "商户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "24252")
+    private Long id;
+
+    @Schema(description = "商户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotEmpty(message = "商户名称不能为空")
+    private String name;
+
+    @Schema(description = "商户状态,关联商户状态表", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "商户状态,关联商户状态表不能为空")
+    private Integer status;
+
+    @Schema(description = "简介", example = "随便")
+    private String description;
+
+    @Schema(description = "负责人")
+    private String contact;
+
+    @Schema(description = "所在地")
+    private String address;
+
+    @Schema(description = "负责人电话")
+    private String contactNumber;
+
+    @Schema(description = "域名")
+    private String website;
+
+    @Schema(description = "官网")
+    private String businessUrl;
+
+    @Schema(description = "维权电话")
+    private String complaintsHotline;
+
+    @Schema(description = "客服电话")
+    private String customerServiceHotline;
+
+    @Schema(description = "邮箱")
+    private String email;
+
+    @Schema(description = "营业执照图片路径")
+    private String businessLicensePicture;
+
+    @Schema(description = "服务到期时间")
+    private LocalDateTime expireTime;
+
+    @Schema(description = "logo地址", example = "https://www.zhongxing.cn")
+    private String logoUrl;
+
+//    @Schema(description = "商户负责人姓名", example = "芋艿")
+//    private String contactName;
+//
+//    @Schema(description = "商户负责人手机")
+//    private String contactMobile;
+
+    @Schema(description = "服务套餐", example = "8463")
+    private Integer packageId;
+
+    @Schema(description = "店铺数量", example = "11326")
+    private Integer shopCount;
+
+    @Schema(description = "区域id,对应文件系统 src/main/resources/area.csv 中的id一列", example = "22725")
+    private Long areaId;
+
+
+
+
+}

+ 152 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shop/ShopController.java

@@ -0,0 +1,152 @@
+package cn.newfeifan.mall.sale.controller.admin.shop;
+
+import cn.newfeifan.mall.module.enums.ErrorCodeConstants;
+import cn.newfeifan.mall.module.system.api.user.dto.AdminUserRespDTO;
+import cn.newfeifan.mall.module.system.controller.admin.user.vo.user.UserRespVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantRespVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopPageReqVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopRespVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopSaveReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.merchant.MerchantDO;
+import cn.newfeifan.mall.sale.service.merchant.MerchantService;
+import cn.newfeifan.mall.sale.service.shop.ShopService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+import java.util.stream.Collectors;
+
+import cn.newfeifan.mall.framework.common.pojo.PageParam;
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.framework.common.pojo.CommonResult;
+import cn.newfeifan.mall.framework.common.util.object.BeanUtils;
+
+import static cn.newfeifan.mall.framework.common.pojo.CommonResult.error;
+import static cn.newfeifan.mall.framework.common.pojo.CommonResult.success;
+
+import cn.newfeifan.mall.framework.excel.core.util.ExcelUtils;
+
+import cn.newfeifan.mall.framework.operatelog.core.annotations.OperateLog;
+
+import static cn.newfeifan.mall.framework.operatelog.core.enums.OperateTypeEnum.*;
+import static cn.newfeifan.mall.module.enums.ErrorCodeConstants.MERCHANT_NOT_EXISTS;
+
+import cn.newfeifan.mall.sale.dal.dataobject.shop.ShopDO;
+
+@Tag(name = "管理后台 - 店铺")
+@RestController
+@RequestMapping("/sale/shop")
+@Validated
+public class ShopController {
+
+
+    @Resource
+    private MerchantService merchantService;
+
+    @Resource
+    private ShopService shopService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建店铺")
+    @PreAuthorize("@ss.hasPermission('sale:shop:create')")
+    public CommonResult<Long> createShop(@Valid @RequestBody ShopSaveReqVO createReqVO) {
+        return success(shopService.createShop(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新店铺")
+    @PreAuthorize("@ss.hasPermission('sale:shop:update')")
+    public CommonResult<Boolean> updateShop(@Valid @RequestBody ShopSaveReqVO updateReqVO) {
+        shopService.updateShop(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除店铺")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('sale:shop:delete')")
+    public CommonResult<Boolean> deleteShop(@RequestParam("id") Long id) {
+        shopService.deleteShop(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得店铺")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('sale:shop:query')")
+    public CommonResult<ShopRespVO> getShop(@RequestParam("id") Long id) {
+        ShopDO shop = shopService.getShop(id);
+        return success(BeanUtils.toBean(shop, ShopRespVO.class));
+    }
+
+
+    @GetMapping("/getAllShop")
+    @Operation(summary = "获得商户下所有店铺")
+    @Parameter(name = "merId", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:query')")
+    public CommonResult<List<ShopRespVO>> getMerchantAllShop(@RequestParam("merId") Long merId) {
+        // 获取所有店铺
+        List<ShopRespVO> shops = shopService.getMerchantAllShop(merId);
+        return success(shops);
+    }
+
+
+    @GetMapping("/getAllUser")
+    @Operation(summary = "获取店铺下的所有人员列表")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:query')")
+    public CommonResult<List<UserRespVO>> getAllUser(@RequestParam("id") Long id) {
+        // 获取所有店铺
+        List<UserRespVO> shops = shopService.getAllUser(id);
+        return success(shops);
+    }
+
+
+    @GetMapping("/getAllUserByMer")
+    @Operation(summary = "获取商户下的所有人员列表")
+    @Parameter(name = "merId", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('sale:merchant:query')")
+    public CommonResult<List<UserRespVO>> getAllUserByMer(@RequestParam("merId") Long merId) {
+        // 获取所有店铺
+        List<ShopRespVO> shops = shopService.getMerchantAllShop(merId);
+        if (shops.isEmpty()) {
+            return success(Collections.emptyList());
+        }
+        List<Long> shopIds = shops.stream().map(ShopRespVO::getId).collect(Collectors.toList());
+        List<UserRespVO> userRespVOS = shopService.getAllUser(shopIds);
+        return success(userRespVOS);
+    }
+
+
+    @GetMapping("/page")
+    @Operation(summary = "获得店铺分页")
+    @PreAuthorize("@ss.hasPermission('sale:shop:query')")
+    public CommonResult<PageResult<ShopRespVO>> getShopPage(@Valid ShopPageReqVO pageReqVO) {
+        PageResult<ShopDO> pageResult = shopService.getShopPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, ShopRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出店铺 Excel")
+    @PreAuthorize("@ss.hasPermission('sale:shop:export')")
+    @OperateLog(type = EXPORT)
+    public void exportShopExcel(@Valid ShopPageReqVO pageReqVO,
+                                HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ShopDO> list = shopService.getShopPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "店铺.xls", "数据", ShopRespVO.class,
+                BeanUtils.toBean(list, ShopRespVO.class));
+    }
+
+}

+ 34 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shop/vo/ShopPageReqVO.java

@@ -0,0 +1,34 @@
+package cn.newfeifan.mall.sale.controller.admin.shop.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.newfeifan.mall.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.newfeifan.mall.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 店铺分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ShopPageReqVO extends PageParam {
+
+    @Schema(description = "店铺名称", example = "李四")
+    private String name;
+
+    @Schema(description = "店铺状态", example = "1")
+    private Integer status;
+
+    @Schema(description = "商户id", example = "3447")
+    private Long merchantId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+    @Schema(description = "logo地址", example = "https://www.zhongxing.cn")
+    private String logoUrl;
+
+}

+ 48 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shop/vo/ShopRespVO.java

@@ -0,0 +1,48 @@
+package cn.newfeifan.mall.sale.controller.admin.shop.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+
+@Schema(description = "管理后台 - 店铺 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ShopRespVO {
+
+    @Schema(description = "店铺id", requiredMode = Schema.RequiredMode.REQUIRED, example = "15084")
+    @ExcelProperty("店铺id")
+    private Long id;
+
+    @Schema(description = "店铺名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @ExcelProperty("店铺名称")
+    private String name;
+
+    @Schema(description = "店铺状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("店铺状态")
+    private Integer status;
+
+    @Schema(description = "商户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "3447")
+    @ExcelProperty("商户id")
+    private Long merchantId;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "logo地址", example = "https://www.zhongxing.cn")
+    @ExcelProperty("logo地址")
+    private String logoUrl;
+
+    @Schema(description = "域名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @ExcelProperty("域名")
+    private String website;
+
+    @Schema(description = "最后更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("最后更新时间")
+    private LocalDateTime updateTime;
+
+}

+ 38 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shop/vo/ShopSaveReqVO.java

@@ -0,0 +1,38 @@
+package cn.newfeifan.mall.sale.controller.admin.shop.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 店铺新增/修改 Request VO")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ShopSaveReqVO {
+
+    @Schema(description = "店铺id", requiredMode = Schema.RequiredMode.REQUIRED, example = "15084")
+    private Long id;
+
+    @Schema(description = "店铺名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotEmpty(message = "店铺名称不能为空")
+    private String name;
+
+    @Schema(description = "店铺状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "店铺状态不能为空")
+    private Integer status;
+
+    @Schema(description = "商户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "3447")
+    @NotNull(message = "商户id不能为空")
+    private Long merchantId;
+
+    @Schema(description = "logo地址", example = "https://www.zhongxing.cn")
+    private String logoUrl;
+
+    @Schema(description = "域名", example = "https://www.zhongxing.cn")
+    private String website;
+
+
+}

+ 95 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shopstatus/ShopStatusController.java

@@ -0,0 +1,95 @@
+package cn.newfeifan.mall.sale.controller.admin.shopstatus;
+
+import cn.newfeifan.mall.sale.controller.admin.shopstatus.vo.ShopStatusRespVO;
+import cn.newfeifan.mall.sale.dal.dataobject.shopstatus.ShopStatusDO;
+import cn.newfeifan.mall.sale.service.shopstatus.ShopStatusService;
+import cn.newfeifan.mall.sale.controller.admin.shopstatus.vo.ShopStatusPageReqVO;
+import cn.newfeifan.mall.sale.controller.admin.shopstatus.vo.ShopStatusSaveReqVO;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.newfeifan.mall.framework.common.pojo.PageParam;
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.framework.common.pojo.CommonResult;
+import cn.newfeifan.mall.framework.common.util.object.BeanUtils;
+import static cn.newfeifan.mall.framework.common.pojo.CommonResult.success;
+
+import cn.newfeifan.mall.framework.excel.core.util.ExcelUtils;
+
+import cn.newfeifan.mall.framework.operatelog.core.annotations.OperateLog;
+import static cn.newfeifan.mall.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+@Tag(name = "管理后台 - 店铺状态")
+@RestController
+@RequestMapping("/sale/shop-status")
+@Validated
+public class ShopStatusController {
+
+    @Resource
+    private ShopStatusService shopStatusService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建店铺状态")
+    @PreAuthorize("@ss.hasPermission('sale:shop-status:create')")
+    public CommonResult<Long> createShopStatus(@Valid @RequestBody ShopStatusSaveReqVO createReqVO) {
+        return success(shopStatusService.createShopStatus(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新店铺状态")
+    @PreAuthorize("@ss.hasPermission('sale:shop-status:update')")
+    public CommonResult<Boolean> updateShopStatus(@Valid @RequestBody ShopStatusSaveReqVO updateReqVO) {
+        shopStatusService.updateShopStatus(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除店铺状态")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('sale:shop-status:delete')")
+    public CommonResult<Boolean> deleteShopStatus(@RequestParam("id") Long id) {
+        shopStatusService.deleteShopStatus(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得店铺状态")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('sale:shop-status:query')")
+    public CommonResult<ShopStatusRespVO> getShopStatus(@RequestParam("id") Long id) {
+        ShopStatusDO shopStatus = shopStatusService.getShopStatus(id);
+        return success(BeanUtils.toBean(shopStatus, ShopStatusRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得店铺状态分页")
+    @PreAuthorize("@ss.hasPermission('sale:shop-status:query')")
+    public CommonResult<PageResult<ShopStatusRespVO>> getShopStatusPage(@Valid ShopStatusPageReqVO pageReqVO) {
+        PageResult<ShopStatusDO> pageResult = shopStatusService.getShopStatusPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, ShopStatusRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出店铺状态 Excel")
+    @PreAuthorize("@ss.hasPermission('sale:shop-status:export')")
+    @OperateLog(type = EXPORT)
+    public void exportShopStatusExcel(@Valid ShopStatusPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ShopStatusDO> list = shopStatusService.getShopStatusPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "店铺状态.xls", "数据", ShopStatusRespVO.class,
+                        BeanUtils.toBean(list, ShopStatusRespVO.class));
+    }
+
+}

+ 25 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shopstatus/vo/ShopStatusPageReqVO.java

@@ -0,0 +1,25 @@
+package cn.newfeifan.mall.sale.controller.admin.shopstatus.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.newfeifan.mall.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.newfeifan.mall.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 店铺状态分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ShopStatusPageReqVO extends PageParam {
+
+    @Schema(description = "名称", example = "王五")
+    private String name;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 28 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shopstatus/vo/ShopStatusRespVO.java

@@ -0,0 +1,28 @@
+package cn.newfeifan.mall.sale.controller.admin.shopstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+
+@Schema(description = "管理后台 - 店铺状态 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ShopStatusRespVO {
+
+    @Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "31352")
+    @ExcelProperty("id")
+    private Long id;
+
+    @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
+    @ExcelProperty("名称")
+    private String name;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 19 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/controller/admin/shopstatus/vo/ShopStatusSaveReqVO.java

@@ -0,0 +1,19 @@
+package cn.newfeifan.mall.sale.controller.admin.shopstatus.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 店铺状态新增/修改 Request VO")
+@Data
+public class ShopStatusSaveReqVO {
+
+    @Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "31352")
+    private Long id;
+
+    @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
+    @NotEmpty(message = "名称不能为空")
+    private String name;
+
+}

+ 98 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/dataobject/merchant/MerchantDO.java

@@ -0,0 +1,98 @@
+package cn.newfeifan.mall.sale.dal.dataobject.merchant;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.newfeifan.mall.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 商户 DO
+ *
+ * @author 非繁人
+ */
+@TableName("sale_merchant")
+@KeySequence("sale_merchant_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MerchantDO extends BaseDO {
+
+    /**
+     * 商户id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 商户名称
+     */
+    private String name;
+    /**
+     * 商户状态,关联商户状态表
+     */
+    private Integer status;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 负责人
+     */
+    private String contact;
+    /**
+     * 所在地
+     */
+    private String address;
+    /**
+     * 负责人电话
+     */
+    private String contactNumber;
+    /**
+     * 官网
+     */
+    private String website;
+    /**
+     * 维权电话
+     */
+    private String complaintsHotline;
+    /**
+     * 客服电话
+     */
+    private String customerServiceHotline;
+    /**
+     * 邮箱
+     */
+    private String email;
+    /**
+     * 营业执照图片路径
+     */
+    private String businessLicensePicture;
+    /**
+     * 服务到期时间
+     */
+    private LocalDateTime expireTime;
+    /**
+     * logo地址
+     */
+    private String logoUrl;
+
+    /**
+     * 服务套餐
+     */
+    private Integer packageId;
+    /**
+     * 店铺数量
+     */
+    private Integer shopCount;
+    /**
+     * 区域id,对应文件系统 src/main/resources/area.csv 中的id一列
+     */
+    private Long areaId;
+
+
+}

+ 53 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/dataobject/shop/ShopDO.java

@@ -0,0 +1,53 @@
+package cn.newfeifan.mall.sale.dal.dataobject.shop;
+
+import lombok.*;
+
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.*;
+import cn.newfeifan.mall.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 店铺 DO
+ *
+ * @author 非繁人
+ */
+@TableName("sale_shop")
+@KeySequence("sale_shop_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ShopDO extends BaseDO {
+
+    /**
+     * 店铺id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 店铺名称
+     */
+    private String name;
+    /**
+     * 店铺状态
+     */
+    private Integer status;
+    /**
+     * 商户id
+     */
+    private Long merchantId;
+    /**
+     * logo地址
+     */
+    private String logoUrl;
+    /**
+     * 域名
+     */
+    private String website;
+
+}

+ 35 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/dataobject/shopstatus/ShopStatusDO.java

@@ -0,0 +1,35 @@
+package cn.newfeifan.mall.sale.dal.dataobject.shopstatus;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.newfeifan.mall.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 店铺状态 DO
+ *
+ * @author 非繁人
+ */
+@TableName("sale_shop_status")
+@KeySequence("sale_shop_status_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ShopStatusDO extends BaseDO {
+
+    /**
+     * id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名称
+     */
+    private String name;
+
+}

+ 41 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/mysql/merchant/MerchantMapper.java

@@ -0,0 +1,41 @@
+package cn.newfeifan.mall.sale.dal.mysql.merchant;
+
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.newfeifan.mall.framework.mybatis.core.mapper.BaseMapperX;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantPageReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.merchant.MerchantDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 商户 Mapper
+ *
+ * @author 非繁人
+ */
+@Mapper
+public interface MerchantMapper extends BaseMapperX<MerchantDO> {
+
+    default PageResult<MerchantDO> selectPage(MerchantPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MerchantDO>()
+                .likeIfPresent(MerchantDO::getName, reqVO.getName())
+                .eqIfPresent(MerchantDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(MerchantDO::getDescription, reqVO.getDescription())
+                .eqIfPresent(MerchantDO::getAddress, reqVO.getAddress())
+                .eqIfPresent(MerchantDO::getContactNumber, reqVO.getContactNumber())
+                .eqIfPresent(MerchantDO::getWebsite, reqVO.getWebsite())
+                .eqIfPresent(MerchantDO::getComplaintsHotline, reqVO.getComplaintsHotline())
+                .eqIfPresent(MerchantDO::getCustomerServiceHotline, reqVO.getCustomerServiceHotline())
+                .eqIfPresent(MerchantDO::getEmail, reqVO.getEmail())
+                .eqIfPresent(MerchantDO::getBusinessLicensePicture, reqVO.getBusinessLicensePicture())
+                .betweenIfPresent(MerchantDO::getExpireTime, reqVO.getExpireTime())
+                .betweenIfPresent(MerchantDO::getCreateTime, reqVO.getCreateTime())
+                .eqIfPresent(MerchantDO::getLogoUrl, reqVO.getLogoUrl())
+                .eqIfPresent(MerchantDO::getContact, reqVO.getContact())
+
+                .eqIfPresent(MerchantDO::getPackageId, reqVO.getPackageId())
+                .eqIfPresent(MerchantDO::getShopCount, reqVO.getShopCount())
+                .eqIfPresent(MerchantDO::getAreaId, reqVO.getAreaId())
+                .orderByDesc(MerchantDO::getId));
+    }
+
+}

+ 28 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/mysql/shop/ShopMapper.java

@@ -0,0 +1,28 @@
+package cn.newfeifan.mall.sale.dal.mysql.shop;
+
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.newfeifan.mall.framework.mybatis.core.mapper.BaseMapperX;
+import cn.newfeifan.mall.sale.dal.dataobject.shop.ShopDO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopPageReqVO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 店铺 Mapper
+ *
+ * @author 非繁人
+ */
+@Mapper
+public interface ShopMapper extends BaseMapperX<ShopDO> {
+
+    default PageResult<ShopDO> selectPage(ShopPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ShopDO>()
+                .likeIfPresent(ShopDO::getName, reqVO.getName())
+                .eqIfPresent(ShopDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(ShopDO::getMerchantId, reqVO.getMerchantId())
+                .betweenIfPresent(ShopDO::getCreateTime, reqVO.getCreateTime())
+                .eqIfPresent(ShopDO::getLogoUrl, reqVO.getLogoUrl())
+                .orderByDesc(ShopDO::getId));
+    }
+
+}

+ 25 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/dal/mysql/shopstatus/ShopStatusMapper.java

@@ -0,0 +1,25 @@
+package cn.newfeifan.mall.sale.dal.mysql.shopstatus;
+
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.newfeifan.mall.framework.mybatis.core.mapper.BaseMapperX;
+import cn.newfeifan.mall.sale.controller.admin.shopstatus.vo.ShopStatusPageReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.shopstatus.ShopStatusDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 店铺状态 Mapper
+ *
+ * @author 非繁人
+ */
+@Mapper
+public interface ShopStatusMapper extends BaseMapperX<ShopStatusDO> {
+
+    default PageResult<ShopStatusDO> selectPage(ShopStatusPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ShopStatusDO>()
+                .likeIfPresent(ShopStatusDO::getName, reqVO.getName())
+                .betweenIfPresent(ShopStatusDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ShopStatusDO::getId));
+    }
+
+}

+ 61 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/merchant/MerchantService.java

@@ -0,0 +1,61 @@
+package cn.newfeifan.mall.sale.service.merchant;
+
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantPageReqVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantRespVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantSaveReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.merchant.MerchantDO;
+
+import javax.validation.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 商户 Service 接口
+ *
+ * @author 非繁人
+ */
+public interface MerchantService {
+
+    /**
+     * 创建商户
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    HashMap<String, Long> createMerchant(@Valid MerchantSaveReqVO createReqVO);
+
+    /**
+     * 更新商户
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateMerchant(@Valid MerchantSaveReqVO updateReqVO);
+
+    /**
+     * 删除商户
+     *
+     * @param id 编号
+     */
+    void deleteMerchant(Long id);
+
+    /**
+     * 获得商户
+     *
+     * @param id 编号
+     * @return 商户
+     */
+    MerchantDO getMerchant(Long id);
+
+    /**
+     * 获得商户分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 商户分页
+     */
+    PageResult<MerchantDO> getMerchantPage(MerchantPageReqVO pageReqVO);
+
+    List<MerchantRespVO> salesVolume(Map<Long, MerchantRespVO> map);
+
+}

+ 133 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/merchant/MerchantServiceImpl.java

@@ -0,0 +1,133 @@
+package cn.newfeifan.mall.sale.service.merchant;
+
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.module.product.service.sku.ProductSkuService;
+import cn.newfeifan.mall.module.system.dal.dataobject.user.AdminUserDO;
+import cn.newfeifan.mall.module.system.dal.mysql.user.AdminUserMapper;
+import cn.newfeifan.mall.module.system.service.user.AdminUserService;
+import cn.newfeifan.mall.module.trade.service.order.TradeOrderQueryService;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantPageReqVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantRespVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantSaveReqVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopSaveReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.merchant.MerchantDO;
+import cn.newfeifan.mall.sale.dal.mysql.merchant.MerchantMapper;
+import cn.newfeifan.mall.sale.service.shop.ShopService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import cn.newfeifan.mall.framework.common.util.object.BeanUtils;
+
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static cn.newfeifan.mall.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.newfeifan.mall.module.enums.ErrorCodeConstants.MERCHANT_NOT_EXISTS;
+import static cn.newfeifan.mall.module.system.enums.ErrorCodeConstants.USER_MOBILE_EXISTS;
+
+/**
+ * 商户 Service 实现类
+ *
+ * @author 非繁人
+ */
+@Service
+@Validated
+public class MerchantServiceImpl implements MerchantService {
+
+    @Resource
+    private MerchantMapper merchantMapper;
+
+    @Resource
+    private AdminUserMapper userMapper;
+    @Resource
+    private AdminUserService adminUserService;
+
+    @Resource
+    private ShopService shopService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public HashMap<String, Long> createMerchant(MerchantSaveReqVO createReqVO) {
+        // 插入
+        MerchantDO merchant = BeanUtils.toBean(createReqVO, MerchantDO.class);
+        merchantMapper.insert(merchant);
+
+        // 创建一个店铺
+        Long shopId = shopService.createShop(merchant);
+        // 创建商户的时候, 根据手机号, 创建一个新的账号
+        // 如果这个手机号已经存在, 则提示该手机号已经存在
+        AdminUserDO adminUserDO = userMapper.selectByMobile(merchant.getContactNumber());
+        if (adminUserDO != null) {
+            throw exception(USER_MOBILE_EXISTS);
+        }
+        adminUserService.createUserByMobile(merchant.getId(), shopId, merchant.getContactNumber(), merchant.getContact());
+        // todo 设置直推人关联关系
+        HashMap<String, Long> stringLongHashMap = new HashMap<>();
+        stringLongHashMap.put("shopId", shopId);
+        stringLongHashMap.put("merId", merchant.getId());
+        return stringLongHashMap;
+    }
+
+    @Override
+    public void updateMerchant(MerchantSaveReqVO updateReqVO) {
+        // 校验存在
+        validateMerchantExists(updateReqVO.getId());
+        // 更新
+        MerchantDO updateObj = BeanUtils.toBean(updateReqVO, MerchantDO.class);
+        merchantMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteMerchant(Long id) {
+        // 校验存在
+        validateMerchantExists(id);
+        // 删除
+        merchantMapper.deleteById(id);
+    }
+
+    private void validateMerchantExists(Long id) {
+        if (merchantMapper.selectById(id) == null) {
+            throw exception(MERCHANT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public MerchantDO getMerchant(Long id) {
+        return merchantMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<MerchantDO> getMerchantPage(MerchantPageReqVO pageReqVO) {
+        return merchantMapper.selectPage(pageReqVO);
+    }
+
+    @Resource
+    private TradeOrderQueryService tradeOrderQueryService;
+
+    @Resource
+    private ProductSkuService productSkuService;
+
+    @Override
+    public List<MerchantRespVO> salesVolume(Map<Long, MerchantRespVO> map) {
+
+        for (Long key : map.keySet()) {
+            System.out.println("Key = " + key);
+            MerchantRespVO value = map.get(key);
+            Integer saleVolume = tradeOrderQueryService.getShopSalesVolume(key);
+            Integer skuCount = productSkuService.getSkuCount(key);
+            value.setSalesVolume(saleVolume);
+            value.setSkuCount(skuCount);
+            map.put(key, value);
+        }
+        return new ArrayList<>(map.values());
+    }
+
+}

+ 93 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/shop/ShopService.java

@@ -0,0 +1,93 @@
+package cn.newfeifan.mall.sale.service.shop;
+
+import javax.validation.*;
+
+import cn.newfeifan.mall.module.system.controller.admin.user.vo.user.UserRespVO;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantSaveReqVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopRespVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopSaveReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.merchant.MerchantDO;
+import cn.newfeifan.mall.sale.dal.dataobject.shop.ShopDO;
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopPageReqVO;
+
+import java.util.List;
+
+/**
+ * 店铺 Service 接口
+ *
+ * @author 非繁人
+ */
+public interface ShopService {
+
+    /**
+     * 创建店铺
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createShop(@Valid ShopSaveReqVO createReqVO);
+
+    /**
+     * 创建店铺
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createShop( MerchantDO createReqVO);
+
+
+    /**
+     * 更新店铺
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateShop(@Valid ShopSaveReqVO updateReqVO);
+
+    /**
+     * 删除店铺
+     *
+     * @param id 编号
+     */
+    void deleteShop(Long id);
+
+    /**
+     * 获得店铺
+     *
+     * @param id 编号
+     * @return 店铺
+     */
+    ShopDO getShop(Long id);
+
+    /**
+     * 获得店铺分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 店铺分页
+     */
+    PageResult<ShopDO> getShopPage(ShopPageReqVO pageReqVO);
+
+
+    /**
+     * 获取商户下的所有店铺
+     * @param merId
+     * @return
+     */
+    List<ShopRespVO> getMerchantAllShop(Long merId);
+
+
+    /**
+     * 获取店铺下的所有用户
+     * @param id
+     * @return
+     */
+    List<UserRespVO> getAllUser(Long id);
+
+    /**
+     * 根据店铺列表获取所有的用户
+     * @param shopIds
+     * @return
+     */
+    List<UserRespVO> getAllUser(List<Long> shopIds);
+
+}

+ 122 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/shop/ShopServiceImpl.java

@@ -0,0 +1,122 @@
+package cn.newfeifan.mall.sale.service.shop;
+
+import cn.newfeifan.mall.module.system.controller.admin.user.vo.user.UserRespVO;
+import cn.newfeifan.mall.module.system.dal.dataobject.user.AdminUserDO;
+import cn.newfeifan.mall.module.system.service.user.AdminUserService;
+import cn.newfeifan.mall.sale.controller.admin.merchant.vo.MerchantSaveReqVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopPageReqVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopRespVO;
+import cn.newfeifan.mall.sale.controller.admin.shop.vo.ShopSaveReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.merchant.MerchantDO;
+import cn.newfeifan.mall.sale.dal.mysql.shop.ShopMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+import org.springframework.validation.annotation.Validated;
+
+import cn.newfeifan.mall.sale.dal.dataobject.shop.ShopDO;
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.framework.common.util.object.BeanUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static cn.newfeifan.mall.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.newfeifan.mall.module.enums.ErrorCodeConstants.SHOP_NOT_EXISTS;
+
+/**
+ * 店铺 Service 实现类
+ *
+ * @author 非繁人
+ */
+@Service
+@Validated
+public class ShopServiceImpl implements ShopService {
+
+    @Resource
+    private ShopMapper shopMapper;
+
+    @Override
+    public Long createShop(ShopSaveReqVO createReqVO) {
+        // 插入
+        ShopDO shop = BeanUtils.toBean(createReqVO, ShopDO.class);
+        shopMapper.insert(shop);
+        // 返回
+        return shop.getId();
+    }
+
+    @Override
+    public Long createShop(MerchantDO createReqVO) {
+        ShopSaveReqVO build = ShopSaveReqVO.builder()
+                .logoUrl("")
+                .status(1)
+                .website(createReqVO.getWebsite())
+                .name(createReqVO.getName())
+                .merchantId(createReqVO.getId()).build();
+        ShopDO shop = BeanUtils.toBean(build, ShopDO.class);
+        shopMapper.insert(shop);
+        return shop.getId();
+    }
+
+    @Override
+    public void updateShop(ShopSaveReqVO updateReqVO) {
+        // 校验存在
+        validateShopExists(updateReqVO.getId());
+        // 更新
+        ShopDO updateObj = BeanUtils.toBean(updateReqVO, ShopDO.class);
+        shopMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteShop(Long id) {
+        // 校验存在
+        validateShopExists(id);
+        // 删除
+        shopMapper.deleteById(id);
+    }
+
+    private void validateShopExists(Long id) {
+        if (shopMapper.selectById(id) == null) {
+            throw exception(SHOP_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ShopDO getShop(Long id) {
+        return shopMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ShopDO> getShopPage(ShopPageReqVO pageReqVO) {
+        return shopMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<ShopRespVO> getMerchantAllShop(Long merId) {
+        List<ShopDO> shopDOS = shopMapper.selectList();
+        // 筛选
+        shopDOS = shopDOS.stream().filter(k -> k.getMerchantId() != null && k.getMerchantId().equals(merId)).collect(Collectors.toList());
+        return BeanUtils.toBean(shopDOS, ShopRespVO.class);
+    }
+
+    @Resource
+    private AdminUserService userService;
+
+    @Override
+    public List<UserRespVO> getAllUser(Long id) {
+
+        List<AdminUserDO> userRespVO = userService.getUserByShopId(id);
+
+        return BeanUtils.toBean(userRespVO, UserRespVO.class);
+    }
+
+    @Override
+    public List<UserRespVO> getAllUser(List<Long> shopIds) {
+
+        List<AdminUserDO> userRespVO = userService.getUserByShopIds(shopIds);
+
+        return BeanUtils.toBean(userRespVO, UserRespVO.class);
+    }
+
+}

+ 54 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/shopstatus/ShopStatusService.java

@@ -0,0 +1,54 @@
+package cn.newfeifan.mall.sale.service.shopstatus;
+
+import javax.validation.*;
+import cn.newfeifan.mall.sale.dal.dataobject.shopstatus.ShopStatusDO;
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.sale.controller.admin.shopstatus.vo.ShopStatusPageReqVO;
+import cn.newfeifan.mall.sale.controller.admin.shopstatus.vo.ShopStatusSaveReqVO;
+
+/**
+ * 店铺状态 Service 接口
+ *
+ * @author 非繁人
+ */
+public interface ShopStatusService {
+
+    /**
+     * 创建店铺状态
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createShopStatus(@Valid ShopStatusSaveReqVO createReqVO);
+
+    /**
+     * 更新店铺状态
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateShopStatus(@Valid ShopStatusSaveReqVO updateReqVO);
+
+    /**
+     * 删除店铺状态
+     *
+     * @param id 编号
+     */
+    void deleteShopStatus(Long id);
+
+    /**
+     * 获得店铺状态
+     *
+     * @param id 编号
+     * @return 店铺状态
+     */
+    ShopStatusDO getShopStatus(Long id);
+
+    /**
+     * 获得店铺状态分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 店铺状态分页
+     */
+    PageResult<ShopStatusDO> getShopStatusPage(ShopStatusPageReqVO pageReqVO);
+
+}

+ 71 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/java/cn/newfeifan/mall/sale/service/shopstatus/ShopStatusServiceImpl.java

@@ -0,0 +1,71 @@
+package cn.newfeifan.mall.sale.service.shopstatus;
+
+import cn.newfeifan.mall.framework.common.exception.util.ServiceExceptionUtil;
+import cn.newfeifan.mall.sale.controller.admin.shopstatus.vo.ShopStatusPageReqVO;
+import cn.newfeifan.mall.sale.controller.admin.shopstatus.vo.ShopStatusSaveReqVO;
+import cn.newfeifan.mall.sale.dal.dataobject.shopstatus.ShopStatusDO;
+import cn.newfeifan.mall.sale.dal.mysql.shopstatus.ShopStatusMapper;
+import cn.newfeifan.mall.module.enums.ErrorCodeConstants;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import cn.newfeifan.mall.framework.common.pojo.PageResult;
+import cn.newfeifan.mall.framework.common.util.object.BeanUtils;
+
+
+/**
+ * 店铺状态 Service 实现类
+ *
+ * @author 非繁人
+ */
+@Service
+@Validated
+public class ShopStatusServiceImpl implements ShopStatusService {
+
+    @Resource
+    private ShopStatusMapper shopStatusMapper;
+
+    @Override
+    public Long createShopStatus(ShopStatusSaveReqVO createReqVO) {
+        // 插入
+        ShopStatusDO shopStatus = BeanUtils.toBean(createReqVO, ShopStatusDO.class);
+        shopStatusMapper.insert(shopStatus);
+        // 返回
+        return shopStatus.getId();
+    }
+
+    @Override
+    public void updateShopStatus(ShopStatusSaveReqVO updateReqVO) {
+        // 校验存在
+        validateShopStatusExists(updateReqVO.getId());
+        // 更新
+        ShopStatusDO updateObj = BeanUtils.toBean(updateReqVO, ShopStatusDO.class);
+        shopStatusMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteShopStatus(Long id) {
+        // 校验存在
+        validateShopStatusExists(id);
+        // 删除
+        shopStatusMapper.deleteById(id);
+    }
+
+    private void validateShopStatusExists(Long id) {
+        if (shopStatusMapper.selectById(id) == null) {
+            throw ServiceExceptionUtil.exception(ErrorCodeConstants.SHOP_STATUS_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ShopStatusDO getShopStatus(Long id) {
+        return shopStatusMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ShopStatusDO> getShopStatusPage(ShopStatusPageReqVO pageReqVO) {
+        return shopStatusMapper.selectPage(pageReqVO);
+    }
+
+}

+ 12 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/resources/mapper/merchant/MerchantMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.newfeifan.mall.sale.dal.mysql.merchant.MerchantMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.zhongxing.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 12 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/resources/mapper/shop/ShopMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.newfeifan.mall.sale.dal.mysql.shop.ShopMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.zhongxing.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 12 - 0
feifan-module-sale/feifan-module-sale-biz/src/main/resources/mapper/shopstatus/ShopStatusMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.newfeifan.mall.sale.dal.mysql.shopstatus.ShopStatusMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.zhongxing.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 26 - 0
feifan-module-sale/pom.xml

@@ -0,0 +1,26 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.newfeifan.zx</groupId>
+        <artifactId>feifan</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <groupId>cn.newfeifan.sale</groupId>
+    <artifactId>feifan-module-sale</artifactId>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name> <!-- 3. 新增 name 为 ${project.artifactId} -->
+    <modules>
+        <module>feifan-module-sale-api</module>
+        <module>feifan-module-sale-biz</module>
+    </modules>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <description> <!-- 4. 新增 description 为该模块的描述 -->
+        demo 模块,主要实现 XXX、YYY、ZZZ 等功能。
+    </description>
+</project>

+ 32 - 0
script/feifan-admin-server.sh

@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+app_name='feifan-server'
+module_name='admin'
+current_time=$(date +"%Y-%m-%d %H:%M:%S")
+port=48080
+pid=$(lsof -ti :$port)
+
+# 备份原来的jar包
+cd /data/projects/backend/${module_name}
+mv  ${app_name}.jar  ${app_name}-${current_time}.jar
+pwd
+ls
+cd /data/jenkins_home/workspace/feifan-backend-zx-${module_name}/${app_name}/target
+pwd
+ls
+
+# 获取jar包, 直接运行
+echo "--- 开始执行 ${app_name} 项目 ---"
+
+if [ -z "${pid}" ]; then
+  echo "端口 ${port} 未被占用。"
+else
+  kill -9 ${pid}
+  echo "进程 ${pid} 已被终止。"
+fi
+
+echo '----启动项目 60s后结束控制台信息----'
+mv  ./${app_name}.jar /data/projects/backend/${module_name}
+cd /data/projects/backend/${module_name}
+nohup /usr/local/jar/jdk1.8.0_371/bin/java  -jar  ${app_name}.jar > nohup.log 2>&1 &
+timeout 60s tail -f -n 1000 nohup.log
+echo '----项目启动成功----'

+ 162 - 0
sql/mysql/建空库SQL/1_20240227.sql

@@ -0,0 +1,162 @@
+/*增加店铺表,同时为相关表添加店铺id*/
+
+/*增加店铺表*/
+CREATE TABLE `sale_shop`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '店铺id',
+  `name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '店铺名称',
+  `status` tinyint NOT NULL DEFAULT 1 COMMENT '店铺状态',
+  `merchant_id` bigint NOT NULL DEFAULT 0 COMMENT '商户id',
+
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '店铺表' ROW_FORMAT = Dynamic;
+
+
+
+ALTER TABLE product_property ADD COLUMN shop_id bigint COMMENT '店铺id';  --  商品属性项表加店铺id
+ALTER TABLE product_property_value ADD COLUMN shop_id bigint COMMENT '店铺id';  --  商品属性值表加店铺id
+ALTER TABLE product_spu ADD COLUMN shop_id bigint COMMENT '店铺id';  --  商品SPU表加店铺id
+ALTER TABLE product_sku ADD COLUMN shop_id bigint COMMENT '店铺id';  --  商品SKU表加店铺id
+ALTER TABLE trade_order ADD COLUMN shop_id bigint COMMENT '店铺id';  --  订单主表加店铺id
+ALTER TABLE product_comment ADD COLUMN shop_id bigint COMMENT '店铺id';  --  商品评价表加店铺id
+ALTER TABLE trade_after_sale ADD COLUMN shop_id bigint COMMENT '店铺id';  --  售后订单表加店铺id
+ALTER TABLE trade_delivery_express_template ADD COLUMN shop_id bigint COMMENT '店铺id';  --  快递运费模板主表加店铺id
+ALTER TABLE trade_delivery_pick_up_store ADD COLUMN shop_id bigint COMMENT '店铺id';  --  自提门店表加店铺id
+
+/*增加商户表*/
+CREATE TABLE `sale_merchant`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商户id',
+  `name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '商户名称',
+  `status` tinyint NOT NULL DEFAULT 1 COMMENT '商户状态,关联商户状态表',
+  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '简介',
+  `contact` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '负责人',
+  `address` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '详细地址',
+  `contact_number` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '负责人电话',
+  `website` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '官网',
+  `complaints_hotline` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '维权电话',
+  `customer_service_hotline` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '客服电话',
+  `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '邮箱',
+  `business_license_picture` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '营业执照图片路径',
+  `expire_time` datetime NULL COMMENT '服务到期时间',
+  
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '店铺表' ROW_FORMAT = Dynamic;
+
+ALTER TABLE product_property ADD COLUMN merchant_id bigint COMMENT '商户id';  --  商品属性项表加商户id
+ALTER TABLE product_property_value ADD COLUMN merchant_id bigint COMMENT '商户id';  --  商品属性值表加商户id
+ALTER TABLE product_spu ADD COLUMN merchant_id bigint COMMENT '商户id';  --  商品SPU表加商户id
+ALTER TABLE product_sku ADD COLUMN merchant_id bigint COMMENT '商户id';  --  商品SKU表加商户id
+ALTER TABLE trade_order ADD COLUMN merchant_id bigint COMMENT '商户id';  --  订单主表加商户id
+ALTER TABLE product_comment ADD COLUMN merchant_id bigint COMMENT '商户id';  --  商品评价表加商户id
+ALTER TABLE trade_after_sale ADD COLUMN merchant_id bigint COMMENT '商户id';  --  售后订单表加商户id
+ALTER TABLE trade_delivery_express_template ADD COLUMN merchant_id bigint COMMENT '商户id';  --  快递运费模板主表加商户id
+ALTER TABLE trade_delivery_pick_up_store ADD COLUMN merchant_id bigint COMMENT '商户id';  --  自提门店表加商户id
+
+
+ALTER TABLE product_spu ADD COLUMN hidden bit(1) NOT NULL DEFAULT b'0' COMMENT '是否隐藏。隐藏:1,不隐藏:0';  --  控制商品是否显示。
+
+
+/*增加店铺状态表*/
+CREATE TABLE `sale_shop_status`  (
+  `id` bigint NOT NULL COMMENT 'id',
+  `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '店铺状态表' ROW_FORMAT = Dynamic;
+
+INSERT INTO sale_shop_status (id,name) VALUES(1,'营业中');
+INSERT INTO sale_shop_status (id,name) VALUES(2,'暂停营业');
+INSERT INTO sale_shop_status (id,name) VALUES(3,'停用');
+
+commit;
+
+/*增加用户类型表*/
+CREATE TABLE `system_user_category`  (
+  `id` bigint NOT NULL COMMENT 'id',
+  `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '店铺状态表' ROW_FORMAT = Dynamic;
+
+INSERT INTO system_user_category (id,name) VALUES(1,'系统用户');
+INSERT INTO system_user_category (id,name) VALUES(5,'店铺用户');
+commit;
+
+/*用户表、角色、菜单表 添加用户类型字段。*/
+ALTER TABLE system_users ADD COLUMN category_id bigint NULL COMMENT '用户类型'; -- 用户表添加用户类型字段
+ALTER TABLE system_role ADD COLUMN category_id bigint NULL COMMENT '用户类型'; -- 角色表添加用户类型字段
+ALTER TABLE system_menu ADD COLUMN category_id bigint NULL COMMENT '用户类型'; -- 菜单表添加用户类型字段
+
+/*用户表system_users还要添加店铺ID、商户ID字段*/
+ALTER TABLE system_users ADD COLUMN shop_id bigint NULL COMMENT '店铺id';
+ALTER TABLE system_users ADD COLUMN merchant_id bigint NULL COMMENT '商户id';
+
+ALTER TABLE `member_user` MODIFY COLUMN `future_member_worth_value` bigint(0) NULL DEFAULT NULL COMMENT '未来身价值,未来身价值=当前身价值+预增加身价值' AFTER `pre_added_member_worth_value`;
+ALTER TABLE `sale_merchant` ADD COLUMN `logo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'logo地址' AFTER `id`;
+ALTER TABLE `sale_shop` ADD COLUMN `logo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'logo地址' AFTER `id`;
+
+
+-- 修改spu以及sku表字段
+ALTER TABLE `product_sku` ADD COLUMN `Promotion_fee` int(0) NULL DEFAULT NULL COMMENT '推广费, 单位: 分' AFTER `price`;
+ALTER TABLE `product_sku` ADD COLUMN `mark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注' AFTER `Promotion_fee`;
+
+ALTER TABLE `product_spu` ADD COLUMN `producer_area` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '产地' AFTER `description`;
+ALTER TABLE `product_spu` ADD COLUMN `spu_type` bit(1) NULL DEFAULT NULL COMMENT '商品类别' AFTER `producer_area`;
+
+
+
+
+
+
+ALTER TABLE `member_user` MODIFY COLUMN `future_member_worth_value` bigint(0) NULL DEFAULT NULL COMMENT '未来身价值,未来身价值=当前身价值+预增加身价值' AFTER `pre_added_member_worth_value`;
+ALTER TABLE `sale_merchant` ADD COLUMN `logo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'logo地址' AFTER `id`;
+ALTER TABLE `sale_shop` ADD COLUMN `logo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'logo地址' AFTER `id`;
+
+-- 扩展商品sku表 推广费金额、推广费占比
+ALTER TABLE product_sku ADD COLUMN promotion_expenses int NULL COMMENT '推广费(以分为单位)';
+ALTER TABLE product_sku ADD COLUMN promotion_expenses_proportion int NULL COMMENT '推广费占比,推广费占比=推广费/销售价*100';
+
+-- 扩展商户表,增加“区域”字段,对应页面是省、市、区 级联菜单
+ALTER TABLE `sale_merchant` ADD COLUMN `area_id` bigint NULL DEFAULT NULL COMMENT '区域id,对应文件系统 src/main/resources/area.csv 中的id一列';
+
+ALTER TABLE `sale_shop` ADD COLUMN `website` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '店铺域名' AFTER `deleted`;
+
+ALTER TABLE `sale_merchant` MODIFY COLUMN `website` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '域名' AFTER `contact_number`;
+
+ALTER TABLE `sale_merchant` ADD COLUMN `business_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商户官网' AFTER `website`;
+
+-- 2024 04 01 给商户模板diy页面以及模板页面添加商户ID以及店铺ID
+
+ALTER TABLE `promotion_diy_template` ADD COLUMN `merchant_id` bigint(0) NULL DEFAULT NULL COMMENT '商户ID' AFTER `deleted`;
+
+ALTER TABLE `promotion_diy_template` ADD COLUMN `shop_id` bigint(0) NULL DEFAULT NULL COMMENT '店铺ID' AFTER `merchant_id`;
+
+ALTER TABLE `promotion_diy_page` ADD COLUMN `shop_id` bigint(0) NULL DEFAULT NULL COMMENT '店铺ID' AFTER `deleted`;
+
+ALTER TABLE `promotion_diy_page` ADD COLUMN `merchant_id` bigint(0) NULL DEFAULT NULL COMMENT '商户ID' AFTER `shop_id`;
+
+
+

+ 120 - 0
sql/mysql/建空库SQL/2_20240307.sql

@@ -0,0 +1,120 @@
+-- 扩展消费者(会员)“身价”相关修改
+
+-- 为消费者(即会员表)增加当前身价值、预增加的身价值、增加后的身价总值、 直接推荐人id字段等
+ALTER TABLE `member_user` 
+ADD COLUMN `recommenderd_id` bigint COMMENT '直接推荐人id',
+ADD COLUMN `current_member_worth_value` bigint COMMENT '当前身价值',
+ADD COLUMN `pre_added_member_worth_value` bigint COMMENT '预增加身价值',
+ADD COLUMN `future_member_worth_value` bigint COMMENT '未来身价值,未来身价值=当前身价值+预增加身价值',
+ADD COLUMN `current_member_worth_level_name` varchar(64) COMMENT '当前身价级别名称',
+ADD COLUMN `current_member_worth_level_id` bigint COMMENT '当前身价级别id',
+ADD COLUMN `last_sign_in_time` datetime COMMENT '最后签到时间',
+ADD COLUMN `continuous_sign_in_days` datetime COMMENT '连续签到天数';
+
+-- 身价级别表设置表
+CREATE TABLE `member_worth_level_setting`  (
+  `id` bigint NOT NULL COMMENT '身价级别id',
+  `level_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '身价级别名称',
+  `member_worth_value` bigint NOT NULL DEFAULT 0 COMMENT '本级别需达到的身价值',
+
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '铺身价级别表设置表' ROW_FORMAT = Dynamic;
+
+
+-- 增加 会员资产变化类别表, 此表主要用于设置各种导致用户资产发生变化的行为的分值变化
+-- 字段有id、类别码(数字类型,在java中对应常量)、类别名称、类别描述、变化值(就是签到身价加100那个100)
+CREATE TABLE `member_assets_change_category`  (
+  `id` bigint NOT NULL COMMENT '会员资产变化类别id',
+  `name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '类别名称',
+  `code` int NOT NULL DEFAULT -1 COMMENT '类别码',
+  `variation_value` int NOT NULL DEFAULT 0 COMMENT '身价、积分、看点在本类别操作的变化值',
+  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '描述',
+
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员资产变化类别表,含身价、积分、看点变化类别' ROW_FORMAT = Dynamic;
+
+--  会员身价变化日志表
+--  字段有会员资产变化类别id、分值、操作时间、对象名(如 订单表名)、对象id(如 订单id)、是否已生效、计划生效时间、实际生效时间、描述
+CREATE TABLE `member_worth_value_change_log`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '会员身价变化日志id',
+  `member_assets_change_category_id` bigint NOT NULL COMMENT '会员资产变化类别id',-- 新加字段
+  `member_worth_value` bigint NOT NULL COMMENT '身价值',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
+  `object_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作对应业务对象名称',
+  `object_id` bigint NOT NULL DEFAULT 0 COMMENT '操作对应业务对象id',
+  `effective` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已生效',
+  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '描述',
+  `plan_effective_time` datetime NULL COMMENT '计划生效时间',
+  `actual_effective_time` datetime NULL COMMENT '实际生效时间',
+
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '店铺表' ROW_FORMAT = Dynamic;
+
+-- 增加 会员签到表
+CREATE TABLE `member_sign_in`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '会员签到表id',
+  `member_user_id` bigint NOT NULL DEFAULT 0 COMMENT '会员用户id',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间',
+
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '店铺表' ROW_FORMAT = Dynamic;
+
+
+
+-- 为消费者(即会员表)增加当前的积分值、预增加的积分值、累计消费额(只算人民币)
+ALTER TABLE `member_user` 
+ADD COLUMN `current_member_points` bigint COMMENT '当前积分值',
+ADD COLUMN `pre_added_member_worth` bigint COMMENT '预增加积分值'
+;
+
+
+-- 消费者(即会员表)增加累计消费金额(只算人民币)
+ALTER TABLE `member_user` 
+ADD COLUMN `cumulative_spending` bigint COMMENT '累计消费金额(只算人民币,单位为分)';
+
+
+/* 会员积分变化日志表:会员资产变化类别id、分值、操作时间、对象名(如 订单表名)、对象id(如 订单id)、
+   是否已生效、计划生效时间、实际生效时间、描述。定时器直接扫描本表未生效,且过了计划生效时间的数据进行处理。
+*/
+CREATE TABLE `member_points_change_log`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '会员积分变化日志id',
+  `member_assets_change_category_id` bigint NOT NULL COMMENT '会员资产变化类别id',
+  `member_points` bigint NOT NULL COMMENT '积分值',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
+  `object_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作对应业务对象名称(如 订单表名)',
+  `object_id` bigint NOT NULL DEFAULT 0 COMMENT '操作对应业务对象id(如 订单id)',
+  `effective` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已生效',
+  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '描述',
+  `plan_effective_time` datetime NULL COMMENT '计划生效时间',
+  `actual_effective_time` datetime NULL COMMENT '实际生效时间',
+
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '店铺表' ROW_FORMAT = Dynamic;
+

+ 39 - 0
sql/mysql/建空库SQL/3_20240320.sql

@@ -0,0 +1,39 @@
+/* 订单相关数据库扩展
+
+订单主表`trade_order`增加字段pay_points(支付积分)、increase_points(用户获得积分);
+订单主表`trade_order`原有use_point、give_point 字段、refund_point('退还的使用的积分') 字段:记录订单项的积分信息。但是系统原来的积分与我们需求的设定不同,安全起见不使用这些积分字段。
+
+
+ */
+
+/* 因为原来的 trade_config 表是保存整个商城的全局配置,不适合保存单个店铺的贝式学习免邮设置等信息。
+   所以在表 sale_shop 增加字段保存
+ */
+ALTER TABLE `sale_shop`
+    ADD COLUMN `delivery_express_free_enabled` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用全场包邮';
+
+ALTER TABLE `sale_shop`
+    ADD COLUMN `delivery_express_free_price` int NOT NULL DEFAULT 0 COMMENT '全场包邮的最小金额,单位:分';
+
+ALTER TABLE `sale_shop`
+    ADD COLUMN `delivery_pick_up_enabled` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否开启自提';
+
+-- 订单主表`trade_order`增加字段pay_points(支付积分)、increase_points(用户获得积分)
+ ALTER TABLE `trade_order` 
+ADD COLUMN `pay_points` int COMMENT '支付积分';
+
+ ALTER TABLE `trade_order` 
+ADD COLUMN `increase_points` int COMMENT '用户获得积分';
+
+ALTER TABLE `trade_order`
+    ADD COLUMN `refund_points` int COMMENT '退还的使用的积分';
+
+ALTER TABLE `trade_order`
+    ADD COLUMN `pay_rmb` int COMMENT '实付人民币,单位:分';
+
+-- 订单项表`trade_order_item`增加字段pay_points(支付积分)
+ALTER TABLE `trade_order_item` ADD COLUMN `pay_points` int COMMENT '支付积分';
+ALTER TABLE `trade_order_item` ADD COLUMN `increase_points` int COMMENT '用户获得积分';
+ALTER TABLE `trade_order_item` ADD COLUMN `refund_points` int COMMENT '退还的使用的积分';
+ALTER TABLE `trade_order_item` ADD COLUMN `pay_rmb` int COMMENT '实付人民币,单位:分';
+

+ 53 - 0
sql/mysql/建空库SQL/aaa.json

@@ -0,0 +1,53 @@
+{
+  "page": {
+    "description": "",
+    "backgroundColor": "#f5f5f5",
+    "backgroundImage": ""
+  },
+  "navigationBar": {
+    "title": "页面标题",
+    "description": "",
+    "navBarHeight": 35,
+    "backgroundColor": "#fff",
+    "backgroundImage": "",
+    "styleType": "default",
+    "alwaysShow": true,
+    "showGoBack": true
+  },
+  "tabBar": {
+    "theme": "red",
+    "style": {
+      "bgType": "color",
+      "bgColor": "#fff",
+      "color": "#333",
+      "activeColor": "#0e932e"
+    },
+    "items": [
+      {
+        "text": "首页",
+        "url": "/pages/index/index",
+        "iconUrl": "https://mall-ffkj.oss-cn-guangzhou.aliyuncs.com/7e4bb72e76663640d94f779181637ab276641051d05f6646f8074edf0ace3a52.png",
+        "activeIconUrl": "https://mall-ffkj.oss-cn-guangzhou.aliyuncs.com/2fe2055a1d0798b8e77e4393e28009fa2eee290473ebbb6f6d697b8b62d4b837.png"
+      },
+      {
+        "text": "分类",
+        "url": "/pages/index/category?id=3",
+        "iconUrl": "https://mall-ffkj.oss-cn-guangzhou.aliyuncs.com/bf4d78fba19bbf9425dd256c2b44144d83994318d33b326d9f60ac0fc36fdae4.png",
+        "activeIconUrl": "https://mall-ffkj.oss-cn-guangzhou.aliyuncs.com/8343f8dd7d9601ba65d12aa6502941f4a05ce33ef436ef02294dfd194717dca7.png"
+      },
+      {
+        "text": "购物车",
+        "url": "/pages/index/cart",
+        "iconUrl": "https://mall-ffkj.oss-cn-guangzhou.aliyuncs.com/8c8144c5028619c38b4a7f85fa1b3d447225c04a4964c62197f462cde08cc1e8.png",
+        "activeIconUrl": "https://mall-ffkj.oss-cn-guangzhou.aliyuncs.com/bc2963eb016174393334090aa329bb8b8234c25c497f3f1ced1af8e40323da90.png"
+      },
+      {
+        "text": "我的",
+        "url": "/pages/index/user",
+        "iconUrl": "https://mall-ffkj.oss-cn-guangzhou.aliyuncs.com/4cf1bab7d8c1ab32fabc6dec3e4996e4294e477ddeb3b3daf6d86d7460e52619.png",
+        "activeIconUrl": "https://mall-ffkj.oss-cn-guangzhou.aliyuncs.com/50ce62ab19ab2a33d90e0bbadfc6d1aff4bcf7343cb8994ab5c9b589813ac446.png"
+      }
+    ]
+  },
+  "components": []
+}

BIN
sql/mysql/建空库SQL/建最初始数据库的文件/feifansql.zip